@seanyao/roll 4.719.2 → 4.720.1

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.
Files changed (3) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/roll.mjs +512 -208
  3. package/package.json +1 -1
package/dist/roll.mjs CHANGED
@@ -7285,8 +7285,8 @@ function writeSyncBlock(original, block) {
7285
7285
  }
7286
7286
  }
7287
7287
  if (start === -1) {
7288
- const sep8 = text2.endsWith("\n\n") || text2 === "" ? "" : text2.endsWith("\n") ? "\n" : "\n\n";
7289
- return `${text2}${sep8}${block}
7288
+ const sep9 = text2.endsWith("\n\n") || text2 === "" ? "" : text2.endsWith("\n") ? "\n" : "\n\n";
7289
+ return `${text2}${sep9}${block}
7290
7290
  `;
7291
7291
  }
7292
7292
  let end = start + 1;
@@ -16195,7 +16195,7 @@ function parseFlowMap2(body) {
16195
16195
  }
16196
16196
  return out3;
16197
16197
  }
16198
- function splitTopLevel(s, sep8) {
16198
+ function splitTopLevel(s, sep9) {
16199
16199
  const parts = [];
16200
16200
  let depth = 0;
16201
16201
  let cur = "";
@@ -16204,7 +16204,7 @@ function splitTopLevel(s, sep8) {
16204
16204
  depth++;
16205
16205
  else if (ch === "}")
16206
16206
  depth = Math.max(0, depth - 1);
16207
- if (ch === sep8 && depth === 0) {
16207
+ if (ch === sep9 && depth === 0) {
16208
16208
  parts.push(cur);
16209
16209
  cur = "";
16210
16210
  } else {
@@ -31174,15 +31174,15 @@ var init_mcp = __esm({
31174
31174
  }
31175
31175
  }
31176
31176
  tryParseMessage() {
31177
- const sep8 = this.buffer.indexOf("\r\n\r\n");
31178
- if (sep8 < 0)
31177
+ const sep9 = this.buffer.indexOf("\r\n\r\n");
31178
+ if (sep9 < 0)
31179
31179
  return void 0;
31180
- const header = this.buffer.slice(0, sep8).toString("utf8");
31180
+ const header = this.buffer.slice(0, sep9).toString("utf8");
31181
31181
  const length = /^Content-Length:\s*(\d+)$/im.exec(header)?.[1];
31182
31182
  if (length === void 0)
31183
31183
  return void 0;
31184
31184
  const bodyLength = Number.parseInt(length, 10);
31185
- const start = sep8 + 4;
31185
+ const start = sep9 + 4;
31186
31186
  if (this.buffer.length < start + bodyLength)
31187
31187
  return void 0;
31188
31188
  const body = this.buffer.slice(start, start + bodyLength);
@@ -80311,11 +80311,11 @@ ${lanes.join("\n")}
80311
80311
  if (i < rootLength) {
80312
80312
  return void 0;
80313
80313
  }
80314
- const sep8 = directory.lastIndexOf(directorySeparator, i - 1);
80315
- if (sep8 === -1) {
80314
+ const sep9 = directory.lastIndexOf(directorySeparator, i - 1);
80315
+ if (sep9 === -1) {
80316
80316
  return void 0;
80317
80317
  }
80318
- return directory.substr(0, Math.max(sep8, rootLength));
80318
+ return directory.substr(0, Math.max(sep9, rootLength));
80319
80319
  }
80320
80320
  }
80321
80321
  }
@@ -244439,7 +244439,7 @@ __export(showcase_exports, {
244439
244439
  showcaseCommand: () => showcaseCommand
244440
244440
  });
244441
244441
  import { spawnSync as spawnSync17 } from "node:child_process";
244442
- import { cpSync as cpSync2, existsSync as existsSync140, mkdirSync as mkdirSync78, mkdtempSync as mkdtempSync8, readdirSync as readdirSync53, readFileSync as readFileSync141, rmSync as rmSync26, statSync as statSync40, writeFileSync as writeFileSync77 } from "node:fs";
244442
+ import { cpSync as cpSync2, existsSync as existsSync141, mkdirSync as mkdirSync78, mkdtempSync as mkdtempSync8, readdirSync as readdirSync54, readFileSync as readFileSync141, rmSync as rmSync27, statSync as statSync40, writeFileSync as writeFileSync77 } from "node:fs";
244443
244443
  import { tmpdir as tmpdir13 } from "node:os";
244444
244444
  import { dirname as dirname77, join as join164 } from "node:path";
244445
244445
  import { fileURLToPath as fileURLToPath3 } from "node:url";
@@ -244482,7 +244482,7 @@ function parseArgs3(args) {
244482
244482
  function packageRoot() {
244483
244483
  let dir = dirname77(fileURLToPath3(import.meta.url));
244484
244484
  for (let i = 0; i < 12; i++) {
244485
- if (existsSync140(join164(dir, "conventions")))
244485
+ if (existsSync141(join164(dir, "conventions")))
244486
244486
  return dir;
244487
244487
  const parent = dirname77(dir);
244488
244488
  if (parent === dir)
@@ -244493,7 +244493,7 @@ function packageRoot() {
244493
244493
  }
244494
244494
  function rollBin3() {
244495
244495
  const devPath = join164(packageRoot(), "packages", "cli", "bin", "roll.js");
244496
- if (existsSync140(devPath))
244496
+ if (existsSync141(devPath))
244497
244497
  return devPath;
244498
244498
  return "roll";
244499
244499
  }
@@ -244520,7 +244520,7 @@ function runRoll(sandbox, rollHome4, args, opts = {}) {
244520
244520
  }
244521
244521
  function fileNonEmpty3(p) {
244522
244522
  try {
244523
- return existsSync140(p) && statSync40(p).size > 0;
244523
+ return existsSync141(p) && statSync40(p).size > 0;
244524
244524
  } catch {
244525
244525
  return false;
244526
244526
  }
@@ -244532,7 +244532,7 @@ function makeSandbox(sourceProject) {
244532
244532
  mkdirSync78(sandbox, { recursive: true });
244533
244533
  mkdirSync78(rollHome4, { recursive: true });
244534
244534
  const srcRoll = join164(sourceProject, ".roll");
244535
- if (existsSync140(srcRoll)) {
244535
+ if (existsSync141(srcRoll)) {
244536
244536
  cpSync2(srcRoll, join164(sandbox, ".roll"), { recursive: true });
244537
244537
  } else {
244538
244538
  mkdirSync78(join164(sandbox, ".roll", "features"), { recursive: true });
@@ -244543,11 +244543,11 @@ function findCardSpec2(sandbox, card) {
244543
244543
  const featuresDir = join164(sandbox, ".roll", "features");
244544
244544
  const candidates = [];
244545
244545
  const walk2 = (dir, depth) => {
244546
- if (depth > 3 || !existsSync140(dir))
244546
+ if (depth > 3 || !existsSync141(dir))
244547
244547
  return;
244548
244548
  let entries = [];
244549
244549
  try {
244550
- entries = readdirSync53(dir);
244550
+ entries = readdirSync54(dir);
244551
244551
  } catch {
244552
244552
  return;
244553
244553
  }
@@ -244560,7 +244560,7 @@ function findCardSpec2(sandbox, card) {
244560
244560
  continue;
244561
244561
  }
244562
244562
  if (isDir4) {
244563
- if (name === card && existsSync140(join164(full, "spec.md")))
244563
+ if (name === card && existsSync141(join164(full, "spec.md")))
244564
244564
  candidates.push(join164(full, "spec.md"));
244565
244565
  walk2(full, depth + 1);
244566
244566
  }
@@ -244574,7 +244574,7 @@ function resetSandbox(sandbox, card) {
244574
244574
  let reset2 = false;
244575
244575
  let ok12 = false;
244576
244576
  const backlogPath = join164(sandbox, ".roll", "backlog.md");
244577
- if (existsSync140(backlogPath)) {
244577
+ if (existsSync141(backlogPath)) {
244578
244578
  const before = readFileSync141(backlogPath, "utf8");
244579
244579
  const cardRow = before.split("\n").find((line) => line.startsWith("|") && line.includes(card));
244580
244580
  const hadStatusToken = cardRow !== void 0 && statusMarkerRe(false).test(cardRow);
@@ -244603,8 +244603,8 @@ function resetSandbox(sandbox, card) {
244603
244603
  notes.push("backlog: .roll/backlog.md absent");
244604
244604
  }
244605
244605
  const pulseCmd = join164(sandbox, "packages", "cli", "src", "commands", "pulse.ts");
244606
- if (existsSync140(pulseCmd)) {
244607
- rmSync26(pulseCmd, { force: true });
244606
+ if (existsSync141(pulseCmd)) {
244607
+ rmSync27(pulseCmd, { force: true });
244608
244608
  notes.push("removed prior pulse.ts surface");
244609
244609
  }
244610
244610
  return { ok: ok12, reset: reset2, notes };
@@ -244636,7 +244636,7 @@ async function captureScreenshots(sandbox, card) {
244636
244636
  const web = await infra.captureScreenshot({
244637
244637
  kind: "web",
244638
244638
  out: webOut,
244639
- url: existsSync140(nowPage) ? `file://${nowPage}#now` : "about:blank"
244639
+ url: existsSync141(nowPage) ? `file://${nowPage}#now` : "about:blank"
244640
244640
  });
244641
244641
  shots.push({
244642
244642
  surface: "web",
@@ -244648,7 +244648,7 @@ async function captureScreenshots(sandbox, card) {
244648
244648
  }
244649
244649
  function readBacklogStatus(sandbox, card) {
244650
244650
  const backlogPath = join164(sandbox, ".roll", "backlog.md");
244651
- if (!existsSync140(backlogPath))
244651
+ if (!existsSync141(backlogPath))
244652
244652
  return void 0;
244653
244653
  for (const line of readFileSync141(backlogPath, "utf8").split("\n")) {
244654
244654
  if (line.startsWith("|") && line.includes(card)) {
@@ -244661,7 +244661,7 @@ function readBacklogStatus(sandbox, card) {
244661
244661
  }
244662
244662
  function readTruthLadder(sandbox, card) {
244663
244663
  const truthPath = join164(sandbox, ".roll", "features", "truth.json");
244664
- if (!existsSync140(truthPath))
244664
+ if (!existsSync141(truthPath))
244665
244665
  return void 0;
244666
244666
  try {
244667
244667
  const snap = JSON.parse(readFileSync141(truthPath, "utf8"));
@@ -244673,7 +244673,7 @@ function readTruthLadder(sandbox, card) {
244673
244673
  }
244674
244674
  function readTcrCommits(sandbox) {
244675
244675
  const runsPath3 = join164(sandbox, ".roll", "loop", "runs.jsonl");
244676
- if (!existsSync140(runsPath3))
244676
+ if (!existsSync141(runsPath3))
244677
244677
  return [];
244678
244678
  const out3 = [];
244679
244679
  try {
@@ -244816,7 +244816,7 @@ ROLL_HOME: ${rollHome4}
244816
244816
  return 1;
244817
244817
  } finally {
244818
244818
  if (!keepSandbox) {
244819
- rmSync26(dirname77(sandbox), { recursive: true, force: true });
244819
+ rmSync27(dirname77(sandbox), { recursive: true, force: true });
244820
244820
  }
244821
244821
  }
244822
244822
  }
@@ -247550,6 +247550,7 @@ Loop \u5DF2\u5904\u4E8E\u6682\u505C
247550
247550
  Loop \u5DF2\u6682\u505C \u2014 \u540E\u7EED\u6392\u7A0B\u5468\u671F\u5C06\u8DF3\u8FC7
247551
247551
  `);
247552
247552
  process.stdout.write("mode: guided \u2014 scheduler will not start long-running Story execution until `roll loop resume`\n");
247553
+ process.stdout.write(" supervisor one-shot: `roll loop go --cards <ids> --max-cycles 1` runs exactly those cards while staying paused (no autonomous scheduling).\n");
247553
247554
  return 0;
247554
247555
  }
247555
247556
  async function loopResumeCommand(_args, deps = realDeps()) {
@@ -259883,9 +259884,9 @@ function writeFeatureStub(issue, featuresDir, epic = "backlog-lifecycle") {
259883
259884
  if (existsSync47(path)) {
259884
259885
  const existing = readFileSync47(path, "utf8");
259885
259886
  const block = ac ? ac + "\n" : "";
259886
- const sep8 = existing.endsWith("\n") || existing === "" ? "" : "\n";
259887
+ const sep9 = existing.endsWith("\n") || existing === "" ? "" : "\n";
259887
259888
  if (block)
259888
- appendFileSync5(path, sep8 + block);
259889
+ appendFileSync5(path, sep9 + block);
259889
259890
  return path;
259890
259891
  }
259891
259892
  writeFileSync26(path, featureStubContent(issue));
@@ -262225,8 +262226,8 @@ function writeSet(runtimeDir11, set) {
262225
262226
  writeFileSync30(tmp, JSON.stringify([...set].sort(), null, 2), "utf8");
262226
262227
  writeFileSync30(path, readFileSync53(tmp, "utf8"), "utf8");
262227
262228
  try {
262228
- const { rmSync: rmSync30 } = __require("node:fs");
262229
- rmSync30(tmp, { force: true });
262229
+ const { rmSync: rmSync31 } = __require("node:fs");
262230
+ rmSync31(tmp, { force: true });
262230
262231
  } catch {
262231
262232
  }
262232
262233
  }
@@ -264497,6 +264498,7 @@ import { createInterface as createInterface3 } from "node:readline";
264497
264498
  // packages/cli/dist/lib/goal-progress.js
264498
264499
  init_dist();
264499
264500
  var GOAL_ALLOWED_CARDS_ENV = "ROLL_LOOP_GO_ALLOWED_CARDS";
264501
+ var GOAL_GUIDED_ENV = "ROLL_LOOP_GO_GUIDED";
264500
264502
  var DELIVERY_STATUSES = /* @__PURE__ */ new Set(["built", "published", "done", "merged"]);
264501
264503
  var DELIVERY_OUTCOMES = /* @__PURE__ */ new Set(["published_pending_merge", "delivered"]);
264502
264504
  function parseAllowedCardsEnv(env = process.env) {
@@ -264505,6 +264507,11 @@ function parseAllowedCardsEnv(env = process.env) {
264505
264507
  const raw = env[GOAL_ALLOWED_CARDS_ENV] ?? "";
264506
264508
  return new Set(raw.split(",").map((s) => s.trim()).filter((s) => s !== ""));
264507
264509
  }
264510
+ function isGuidedRunOnce(allowed2, env = process.env) {
264511
+ if ((env[GOAL_GUIDED_ENV] ?? "").trim() !== "1")
264512
+ return false;
264513
+ return allowed2 !== void 0 && allowed2.size > 0;
264514
+ }
264508
264515
  var OUT_OF_SCOPE_STATUS = "\u{1F6AB} Hold (outside goal scope)";
264509
264516
  function scopeBacklogForAllowedCards(items, allowed2) {
264510
264517
  if (allowed2 === void 0)
@@ -265428,6 +265435,7 @@ function parseOptions(args) {
265428
265435
  let noTmux = false;
265429
265436
  let attach = false;
265430
265437
  let scopeSpecified = false;
265438
+ let cardsFlagSpecified = false;
265431
265439
  let reviewMode = "auto";
265432
265440
  let reviewModeSpecified = false;
265433
265441
  for (let i = 0; i < args.length; i += 1) {
@@ -265497,11 +265505,13 @@ function parseOptions(args) {
265497
265505
  }
265498
265506
  if (arg === "--cards") {
265499
265507
  cards.push(...parseCards(args[i + 1]));
265508
+ cardsFlagSpecified = true;
265500
265509
  i += 1;
265501
265510
  continue;
265502
265511
  }
265503
265512
  if (arg.startsWith("--cards=")) {
265504
265513
  cards.push(...parseCards(arg.slice("--cards=".length)));
265514
+ cardsFlagSpecified = true;
265505
265515
  continue;
265506
265516
  }
265507
265517
  if (!arg.startsWith("-"))
@@ -265517,6 +265527,7 @@ function parseOptions(args) {
265517
265527
  attach,
265518
265528
  scope,
265519
265529
  scopeSpecified,
265530
+ cardsFlagSpecified,
265520
265531
  reviewMode,
265521
265532
  reviewModeSpecified,
265522
265533
  ...maxCycles !== void 0 ? { maxCycles } : {},
@@ -265526,6 +265537,9 @@ function parseOptions(args) {
265526
265537
  function hasHelpArg2(args) {
265527
265538
  return args.includes("--help") || args.includes("-h");
265528
265539
  }
265540
+ function isGuidedRun(opts) {
265541
+ return opts.cardsFlagSpecified && opts.scopeSpecified && opts.scope.kind === "cards" && opts.scope.cards.length > 0;
265542
+ }
265529
265543
  function loopGoHelp() {
265530
265544
  return [
265531
265545
  "Usage: roll loop go [--epic <name>|--cards <ids>|--all] [--for <duration>] [--max-cycles <n>] [--review <auto|hetero|self|off>] [--attach] [--no-tmux]",
@@ -265550,6 +265564,12 @@ function loopGoHelp() {
265550
265564
  " The startup banner shows the EFFECTIVE scope; an inherited scope is flagged so a flagless go can never silently narrow. Pass --all to reset to the full backlog.",
265551
265565
  " \u542F\u52A8\u6A2A\u5E45\u663E\u793A\u751F\u6548\u7684 scope\uFF1B\u6CBF\u7528\u7684 scope \u4F1A\u88AB\u6807\u6CE8\uFF0Cflagless go \u7EDD\u4E0D\u9759\u9ED8\u6536\u7A84\u3002\u7528 --all \u91CD\u7F6E\u56DE\u5168\u91CF\u3002",
265552
265566
  "",
265567
+ "Guided one-shot while paused (FIX-1472):",
265568
+ " An explicit `roll loop go --cards X` runs the named card(s) even while `roll loop pause` is in effect \u2014 it is a supervisor action, not autonomous scheduling. It installs no scheduler and picks no other card; bound it with --max-cycles.",
265569
+ " \u663E\u5F0F\u7684 `roll loop go --cards X` \u5373\u4F7F\u5728 `roll loop pause` \u751F\u6548\u65F6\u4E5F\u4F1A\u6267\u884C\u6307\u5B9A\u5361\u2014\u2014\u8FD9\u662F Supervisor \u624B\u52A8\u52A8\u4F5C\uFF0C\u4E0D\u662F\u81EA\u4E3B\u6392\u7A0B\u3002\u5B83\u4E0D\u5B89\u88C5\u6392\u7A0B\u3001\u4E0D\u9009\u522B\u7684\u5361\uFF1B\u7528 --max-cycles \u9650\u5B9A\u6B21\u6570\u3002",
265570
+ " Pause still stops autonomous scheduling; only an explicitly-named --cards scope bypasses it (a flagless/inherited/--all/--epic go keeps honoring the pause).",
265571
+ " pause \u4ECD\u7136\u505C\u6B62\u81EA\u4E3B\u6392\u7A0B\uFF1B\u53EA\u6709\u672C\u6B21\u663E\u5F0F\u6307\u5B9A\u7684 --cards \u8303\u56F4\u624D\u4F1A\u7ED5\u8FC7\uFF08flagless/\u6CBF\u7528/--all/--epic \u7684 go \u4ECD\u9075\u5B88\u6682\u505C\uFF09\u3002",
265572
+ "",
265553
265573
  "Progress guardrails (the loop stops on NO PROGRESS, not on cost):",
265554
265574
  " productivity floor A cycle whose agent EXECUTED but produced 0 commits and no delivery is a `gave_up` terminal \u2014 alerted on the first occurrence (no streak).",
265555
265575
  " dead-loop breaker A card is skipped after consecutive no-progress cycles; the whole goal STOPS after K consecutive no-progress cycles (a loud ALERT) \u2014 an unmergeable card can never spin forever.",
@@ -265673,6 +265693,20 @@ function followGoLiveFeed(projectPath3, rollBin4) {
265673
265693
  watch.on("error", finish);
265674
265694
  });
265675
265695
  }
265696
+ function buildRunOnceChildEnv(input, base = process.env) {
265697
+ return {
265698
+ ...base,
265699
+ ROLL_LOOP_GO_CHILD: "1",
265700
+ // FIX-1022: unattended go-driver child must never trigger the macOS
265701
+ // screencapture TCC prompt (isTTY is unreliable under PTY wrapping).
265702
+ ROLL_NO_SCREENCAP: base["ROLL_NO_SCREENCAP"] ?? "1",
265703
+ ...input.allowedCards !== void 0 ? { [GOAL_ALLOWED_CARDS_ENV]: input.allowedCards.join(",") } : {},
265704
+ // FIX-1472: guided is set on EVERY call (never conditionally spread) so an
265705
+ // inherited ambient "1" cannot survive into an unguided child. Fail closed
265706
+ // to "0" — isGuidedRunOnce treats anything but "1" as not guided.
265707
+ [GOAL_GUIDED_ENV]: input.guided === true ? "1" : "0"
265708
+ };
265709
+ }
265676
265710
  function realRunOnce(input) {
265677
265711
  const bin = rollBin();
265678
265712
  const cmd = bin.endsWith(".js") || bin.endsWith(".mjs") ? process.execPath : bin;
@@ -265681,14 +265715,7 @@ function realRunOnce(input) {
265681
265715
  const child = spawn8(cmd, args, {
265682
265716
  cwd: input.projectPath,
265683
265717
  detached: true,
265684
- env: {
265685
- ...process.env,
265686
- ROLL_LOOP_GO_CHILD: "1",
265687
- // FIX-1022: unattended go-driver child must never trigger the macOS
265688
- // screencapture TCC prompt (isTTY is unreliable under PTY wrapping).
265689
- ROLL_NO_SCREENCAP: process.env["ROLL_NO_SCREENCAP"] ?? "1",
265690
- ...input.allowedCards !== void 0 ? { [GOAL_ALLOWED_CARDS_ENV]: input.allowedCards.join(",") } : {}
265691
- },
265718
+ env: buildRunOnceChildEnv(input),
265692
265719
  stdio: "inherit"
265693
265720
  });
265694
265721
  child.on("exit", (code) => resolve17(code ?? 1));
@@ -266587,11 +266614,12 @@ async function runGoWorker(id, opts, deps) {
266587
266614
  progress = progressFromGoal(goal);
266588
266615
  initialUsage = runSummaryFromGoal(goal);
266589
266616
  const deadlineSec = timeboxDeadlineSec(goal, opts, startedSec);
266617
+ const guided = isGuidedRun(opts);
266590
266618
  bus.appendEvent(evPath, { type: "goal:session_start", sessionId: sid, scope: goal.scope, ts: startedSec });
266591
266619
  while (true) {
266592
266620
  if (stopRequested)
266593
266621
  break;
266594
- if (existsSync64(pauseMarkerPath2(id.path, id.slug))) {
266622
+ if (!guided && existsSync64(pauseMarkerPath2(id.path, id.slug))) {
266595
266623
  stopReason2 = "pause_marker";
266596
266624
  break;
266597
266625
  }
@@ -266647,10 +266675,10 @@ async function runGoWorker(id, opts, deps) {
266647
266675
  }
266648
266676
  if (stopRequested)
266649
266677
  break;
266650
- if (existsSync64(pauseMarkerPath2(id.path, id.slug)))
266678
+ if (!guided && existsSync64(pauseMarkerPath2(id.path, id.slug)))
266651
266679
  continue;
266652
266680
  const before = readRunSnapshot(runsPath2(id.path));
266653
- await deps.runOnce({ projectPath: id.path, allowedCards });
266681
+ await deps.runOnce({ projectPath: id.path, allowedCards, ...guided ? { guided: true } : {} });
266654
266682
  goal = updateUsage(id.path, goal, baseline, initialUsage, deps.nowIso());
266655
266683
  writeGoal(gPath, goal);
266656
266684
  const after = readRunSnapshot(runsPath2(id.path));
@@ -266707,7 +266735,7 @@ async function runGoWorker(id, opts, deps) {
266707
266735
  stopReason2 = "safety_pause";
266708
266736
  break;
266709
266737
  }
266710
- if (existsSync64(pauseMarkerPath2(id.path, id.slug))) {
266738
+ if (!guided && existsSync64(pauseMarkerPath2(id.path, id.slug))) {
266711
266739
  stopReason2 = "pause_marker";
266712
266740
  break;
266713
266741
  }
@@ -271420,8 +271448,8 @@ function topBar(input) {
271420
271448
  const switcher = multi ? `<div class="proj-switch" style="position:relative;flex:none;"><button type="button" id="proj-switch-btn" class="proj-switch-btn" aria-haspopup="menu" aria-expanded="false" style="display:flex;align-items:center;gap:9px;cursor:pointer;background:transparent;border:0;padding:5px 7px;border-radius:8px;">` + dot + projName + `<span class="proj-caret" style="${MONO}font-size:9px;color:#8f98ad;flex:none;transition:transform .18s;">\u25BE</span></button><div id="proj-menu" class="proj-menu" role="menu" aria-label="switch project \xB7 \u5207\u6362\u9879\u76EE" hidden style="position:absolute;top:42px;left:0;min-width:240px;background:rgba(27,34,56,.99);border:1px solid #313a55;border-radius:10px;padding:6px;box-shadow:0 10px 30px rgba(7,10,20,.5);z-index:40;"><div style="${MONO}font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:#6f7892;padding:6px 13px 8px;">${esc4(input.brand.name)} \xB7 ${bi("this machine", "\u8FD9\u53F0\u673A\u5668")}</div>` + menuItems + `</div></div>` : `<a href="${homeHref}" class="proj-switch-btn" style="display:flex;align-items:center;gap:9px;cursor:pointer;flex:none;text-decoration:none;padding:5px 7px;">` + dot + projName + `</a>`;
271421
271449
  const crumbsNav = `<nav style="flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:9px;${MONO}font-size:12px;color:#8f98ad;overflow:hidden;" aria-label="machine layer \xB7 \u673A\u5668\u5C42"><span style="${MONO}font-size:9.5px;letter-spacing:.14em;text-transform:uppercase;color:#5b6480;flex:none;">${bi("Machine", "\u673A\u5668")}</span>` + MACHINE_NAV.map((m7, i) => {
271422
271450
  const active = input.machinePage === m7.key;
271423
- const sep8 = i > 0 ? `<span style="color:#3a4360;flex:none;">\xB7</span>` : "";
271424
- return sep8 + `<a class="mach-link${active ? " on" : ""}" data-machine="${m7.key}" href="${esc4(m7.href)}"${active ? ' aria-current="page"' : ""} style="text-decoration:none;color:${active ? "#fff" : "#8f98ad"};font-weight:${active ? "600" : "400"};white-space:nowrap;flex:none;">${bi(m7.en, m7.zh)}</a>`;
271451
+ const sep9 = i > 0 ? `<span style="color:#3a4360;flex:none;">\xB7</span>` : "";
271452
+ return sep9 + `<a class="mach-link${active ? " on" : ""}" data-machine="${m7.key}" href="${esc4(m7.href)}"${active ? ' aria-current="page"' : ""} style="text-decoration:none;color:${active ? "#fff" : "#8f98ad"};font-weight:${active ? "600" : "400"};white-space:nowrap;flex:none;">${bi(m7.en, m7.zh)}</a>`;
271425
271453
  }).join("") + `</nav>`;
271426
271454
  const releaseTag = input.snapshot.release?.latestTag;
271427
271455
  const releaseBadge = releaseTag !== void 0 && releaseTag !== "" ? `<span style="${MONO}font-size:11px;color:#6f7892;letter-spacing:.02em;white-space:nowrap;">${bi("release", "\u53D1\u7248")} <b style="color:#cfd5e3;font-weight:600;">${esc4(releaseTag)}</b></span>` : "";
@@ -277700,11 +277728,11 @@ function printMergeSummary(summary) {
277700
277728
  function summaryAction(summary, file) {
277701
277729
  for (let index = summary.length - 1; index >= 0; index -= 1) {
277702
277730
  const entry = summary[index] ?? "";
277703
- const sep8 = entry.indexOf("|");
277704
- if (sep8 < 0)
277731
+ const sep9 = entry.indexOf("|");
277732
+ if (sep9 < 0)
277705
277733
  continue;
277706
- if (entry.slice(sep8 + 1) === file)
277707
- return entry.slice(0, sep8);
277734
+ if (entry.slice(sep9 + 1) === file)
277735
+ return entry.slice(0, sep9);
277708
277736
  }
277709
277737
  return null;
277710
277738
  }
@@ -278249,10 +278277,10 @@ function emitInitUi(projectDir, hasAgents, syncStatus, summary, shouldNudge, mod
278249
278277
  lines3.push(" " + c(iconColor, icon) + " " + c(iconColor, fLabel, { bold: true }));
278250
278278
  const nudgeMsg = shouldNudge ? renderDesignNudge(msgLang6())[0] ?? "" : "";
278251
278279
  const nudgePair = shouldNudge && nudgeMsg !== "" ? (() => {
278252
- const sep8 = nudgeMsg.indexOf(" \u2014 ");
278253
- if (sep8 < 0)
278280
+ const sep9 = nudgeMsg.indexOf(" \u2014 ");
278281
+ if (sep9 < 0)
278254
278282
  return [nudgeMsg, ""];
278255
- return [nudgeMsg.slice(0, sep8), nudgeMsg.slice(sep8 + 3)];
278283
+ return [nudgeMsg.slice(0, sep9), nudgeMsg.slice(sep9 + 3)];
278256
278284
  })() : void 0;
278257
278285
  const nextItems = nextItemsOverride ?? (hasAgents ? [
278258
278286
  ...nudgePair ? [nudgePair] : [],
@@ -281224,7 +281252,7 @@ async function loopExhaustionSplitCommand(argv, deps = realExhaustionSplitDeps()
281224
281252
  init_dist2();
281225
281253
  init_dist();
281226
281254
  init_dist3();
281227
- import { appendFileSync as appendFileSync21, existsSync as existsSync134, mkdirSync as mkdirSync74, readdirSync as readdirSync48, readFileSync as readFileSync135, writeFileSync as writeFileSync72 } from "node:fs";
281255
+ import { appendFileSync as appendFileSync21, existsSync as existsSync135, mkdirSync as mkdirSync74, readdirSync as readdirSync49, readFileSync as readFileSync135, writeFileSync as writeFileSync72 } from "node:fs";
281228
281256
  import { dirname as dirname74, join as join158 } from "node:path";
281229
281257
 
281230
281258
  // packages/cli/dist/runner/executor.js
@@ -288367,7 +288395,7 @@ function applyCorrectionCircuitBreaker(projectPath3, slug3, eventsPath4, alertsP
288367
288395
  init_dist2();
288368
288396
  init_dist3();
288369
288397
  import { execFileSync as execFileSync31 } from "node:child_process";
288370
- import { readFileSync as readFileSync133 } from "node:fs";
288398
+ import { readFileSync as readFileSync133, readdirSync as readdirSync48, realpathSync as realpathSync20 } from "node:fs";
288371
288399
  import { homedir as homedir33 } from "node:os";
288372
288400
  import { basename as basename22, dirname as dirname72, join as join156, relative as relative13, resolve as resolve13 } from "node:path";
288373
288401
  function git3(args, cwd) {
@@ -288437,6 +288465,60 @@ function extractCycleId(dirName) {
288437
288465
  const m7 = /^(cycle-\d{8}-\d{6}-\d+)$/.exec(dirName);
288438
288466
  return m7 ? m7[1] : void 0;
288439
288467
  }
288468
+ var ORPHAN_DELIVERED_OUTCOMES = /* @__PURE__ */ new Set(["delivered", "merged"]);
288469
+ function realpathSafe2(p) {
288470
+ try {
288471
+ return realpathSync20(p);
288472
+ } catch {
288473
+ return p;
288474
+ }
288475
+ }
288476
+ function defaultReadDirNames(dir) {
288477
+ try {
288478
+ return readdirSync48(dir, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name);
288479
+ } catch {
288480
+ return [];
288481
+ }
288482
+ }
288483
+ function scanOrphanLoopWorktrees(repoRoot3, registeredPaths, cycles, deps) {
288484
+ const worktreesDir = join156(repoRoot3, ".roll", "loop", "worktrees");
288485
+ const names = deps.readDir ? deps.readDir(worktreesDir) : defaultReadDirNames(worktreesDir);
288486
+ const out3 = [];
288487
+ for (const name of [...names].sort()) {
288488
+ const absPath = resolve13(join156(worktreesDir, name));
288489
+ if (registeredPaths.has(realpathSafe2(absPath)))
288490
+ continue;
288491
+ const cycleId = extractCycleId(name);
288492
+ const ce = cycleId ? cycles.get(cycleId) : void 0;
288493
+ const outcome = ce?.outcome;
288494
+ const active = isActiveCycle(cycleId, repoRoot3, deps);
288495
+ const rec = {
288496
+ path: absPath,
288497
+ owner: "loop",
288498
+ ...cycleId ? { cycleId } : {},
288499
+ ...ce?.storyId ? { storyId: ce.storyId } : {},
288500
+ ...outcome ? { outcome } : {},
288501
+ dirtyTracked: "unknown",
288502
+ // no git metadata for a deregistered dir
288503
+ dirtyUntracked: "unknown",
288504
+ ahead: null,
288505
+ mergeEvidence: { kind: "none" },
288506
+ active,
288507
+ disposition: "preserved_orphan",
288508
+ reason: ""
288509
+ };
288510
+ if (active) {
288511
+ rec.reason = "orphan loop dir with an active cycle lock; never reclaimed";
288512
+ } else if (outcome && ORPHAN_DELIVERED_OUTCOMES.has(outcome)) {
288513
+ rec.disposition = "orphan_reclaimable";
288514
+ rec.reason = `orphan loop dir (deregistered from git); owning cycle outcome '${outcome}' is delivered \u2014 bounded reclaim`;
288515
+ } else {
288516
+ rec.reason = `orphan loop dir (deregistered from git); delivery not provable (cycle outcome '${outcome ?? "unknown"}') \u2014 preserved, reclaim manually after review`;
288517
+ }
288518
+ out3.push(rec);
288519
+ }
288520
+ return out3;
288521
+ }
288440
288522
  function detectDirty(wtPath, deps) {
288441
288523
  try {
288442
288524
  const g = deps.git ?? git3;
@@ -288637,6 +288719,10 @@ function auditWorktrees(deps) {
288637
288719
  baseRec.reason = disp.reason;
288638
288720
  records.push(baseRec);
288639
288721
  }
288722
+ const registeredLoopPaths = new Set(records.map((r) => realpathSafe2(resolve13(r.path))));
288723
+ for (const orphan of scanOrphanLoopWorktrees(repoRoot3, registeredLoopPaths, cycles, deps)) {
288724
+ records.push(orphan);
288725
+ }
288640
288726
  let ephemeralBranches = [];
288641
288727
  try {
288642
288728
  const g = deps.git ?? git3;
@@ -288695,7 +288781,9 @@ function renderHuman(output) {
288695
288781
  "preserved_unpublished",
288696
288782
  "preserved_dirty_no_tcr",
288697
288783
  "preserved_needs_review",
288784
+ "preserved_orphan",
288698
288785
  "disposable_candidate",
288786
+ "orphan_reclaimable",
288699
288787
  "external_unmanaged"
288700
288788
  ];
288701
288789
  for (const disp of order) {
@@ -288767,12 +288855,24 @@ function worktreeAuditCommand(args, deps) {
288767
288855
  init_dist2();
288768
288856
  import { execFileSync as execFileSync32 } from "node:child_process";
288769
288857
  import { homedir as homedir34 } from "node:os";
288770
- import { appendFileSync as appendFileSync20, mkdirSync as mkdirSync73 } from "node:fs";
288771
- import { dirname as dirname73, join as join157, relative as relative14, resolve as resolve14 } from "node:path";
288772
- var MERGED_KINDS = /* @__PURE__ */ new Set(["ancestor", "patch_equivalent"]);
288858
+ import { appendFileSync as appendFileSync20, existsSync as existsSync134, mkdirSync as mkdirSync73, rmSync as rmSync25 } from "node:fs";
288859
+ import { dirname as dirname73, join as join157, relative as relative14, resolve as resolve14, sep as sep8 } from "node:path";
288860
+ var MERGED_KINDS = /* @__PURE__ */ new Set(["ancestor", "patch_equivalent", "final_tree"]);
288773
288861
  function isSafelyDisposable(rec) {
288774
288862
  return rec.owner === "loop" && rec.active === false && rec.dirtyTracked === false && rec.disposition === "disposable_candidate" && MERGED_KINDS.has(rec.mergeEvidence.kind) && typeof rec.head === "string" && rec.head.length > 0;
288775
288863
  }
288864
+ function isReclaimableOrphan(rec) {
288865
+ return rec.owner === "loop" && rec.active === false && rec.disposition === "orphan_reclaimable";
288866
+ }
288867
+ function isBoundedLoopWorktreeDir(repoRoot3, path) {
288868
+ const base = resolve14(join157(repoRoot3, ".roll", "loop", "worktrees"));
288869
+ const abs = resolve14(path);
288870
+ if (abs === base)
288871
+ return false;
288872
+ if (!abs.startsWith(base + sep8))
288873
+ return false;
288874
+ return dirname73(abs) === base;
288875
+ }
288776
288876
  var PROTECTED_BRANCHES = /* @__PURE__ */ new Set(["main", "master", "HEAD"]);
288777
288877
  function resolveStandaloneMergedBranches(audit, deps) {
288778
288878
  const out3 = [];
@@ -288797,20 +288897,25 @@ function planWorktreeCleanup(audit, threshold, standaloneMergedBranches = []) {
288797
288897
  const loopWorktrees = audit.records.filter((r) => r.owner === "loop");
288798
288898
  const canaryTotal = audit.ephemeralBranches.length + loopWorktrees.length;
288799
288899
  const excess = canaryTotal - threshold;
288800
- const pool = audit.records.filter(isSafelyDisposable).sort((a, b) => a.path.localeCompare(b.path));
288900
+ const pool = audit.records.filter((r) => isSafelyDisposable(r) || isReclaimableOrphan(r)).sort((a, b) => a.path.localeCompare(b.path));
288801
288901
  const branchPool = [...standaloneMergedBranches].sort((a, b) => a.branch.localeCompare(b.branch));
288802
288902
  const excessN = excess > 0 ? excess : 0;
288803
288903
  const chosen = pool.slice(0, Math.min(excessN, pool.length));
288804
288904
  const remainingExcess = excessN - chosen.length;
288805
288905
  const chosenBranches = branchPool.slice(0, Math.min(remainingExcess, branchPool.length));
288806
288906
  const chosenPaths = new Set(chosen.map((r) => r.path));
288807
- const candidates = chosen.map((r) => ({
288808
- path: r.path,
288809
- ...r.cycleId ? { cycleId: r.cycleId } : {},
288810
- ...r.branch ? { branch: r.branch } : {},
288811
- expectedHead: r.head,
288812
- reason: "disposable_candidate"
288813
- }));
288907
+ const candidates = chosen.map((r) => {
288908
+ const orphan = isReclaimableOrphan(r);
288909
+ return {
288910
+ path: r.path,
288911
+ ...r.cycleId ? { cycleId: r.cycleId } : {},
288912
+ ...r.branch ? { branch: r.branch } : {},
288913
+ // An orphan has no registered HEAD; apply skips the head check for rm_dir.
288914
+ expectedHead: orphan ? "" : r.head,
288915
+ reason: orphan ? "orphan_reclaimable" : "disposable_candidate",
288916
+ reclaim: orphan ? "rm_dir" : "git_worktree"
288917
+ };
288918
+ });
288814
288919
  const preserved = audit.records.filter((r) => !chosenPaths.has(r.path)).map((r) => ({ path: r.path, disposition: r.disposition, reason: r.reason }));
288815
288920
  const countedWorktrees = loopWorktrees.map((r) => ({
288816
288921
  path: r.path,
@@ -288829,9 +288934,12 @@ function planWorktreeCleanup(audit, threshold, standaloneMergedBranches = []) {
288829
288934
  preserved
288830
288935
  };
288831
288936
  }
288832
- function defaultRemoveBranch(repositoryRoot, branch) {
288937
+ function defaultRemoveBranch(repositoryRoot, branch, expectedSha) {
288938
+ if (!isFullGitOid(expectedSha)) {
288939
+ return { ok: false, detail: `refused: expected sha ${expectedSha} is not a full git OID` };
288940
+ }
288833
288941
  try {
288834
- execFileSync32("git", ["-C", repositoryRoot, "branch", "-D", branch], {
288942
+ execFileSync32("git", ["-C", repositoryRoot, "update-ref", "-d", `refs/heads/${branch}`, expectedSha], {
288835
288943
  encoding: "utf8",
288836
288944
  stdio: ["ignore", "pipe", "pipe"]
288837
288945
  });
@@ -288841,13 +288949,47 @@ function defaultRemoveBranch(repositoryRoot, branch) {
288841
288949
  return { ok: true, detail: "" };
288842
288950
  }
288843
288951
  function defaultRemoveWorktree(repositoryRoot, path) {
288952
+ let gitErr = "";
288844
288953
  try {
288845
288954
  execFileSync32("git", ["-C", repositoryRoot, "worktree", "remove", "--force", path], {
288846
288955
  encoding: "utf8",
288847
288956
  stdio: ["ignore", "pipe", "pipe"]
288848
288957
  });
288849
288958
  } catch (err16) {
288850
- return { ok: false, detail: err16 instanceof Error ? err16.message : String(err16) };
288959
+ gitErr = err16 instanceof Error ? err16.message : String(err16);
288960
+ }
288961
+ if (existsSync134(path)) {
288962
+ if (!isBoundedLoopWorktreeDir(repositoryRoot, path)) {
288963
+ return { ok: false, detail: gitErr || `refused: ${path} is outside .roll/loop/worktrees` };
288964
+ }
288965
+ try {
288966
+ rmSync25(path, { recursive: true, force: true });
288967
+ } catch (e) {
288968
+ return { ok: false, detail: gitErr || (e instanceof Error ? e.message : String(e)) };
288969
+ }
288970
+ } else if (gitErr) {
288971
+ return { ok: false, detail: gitErr };
288972
+ }
288973
+ try {
288974
+ execFileSync32("git", ["-C", repositoryRoot, "worktree", "prune", "--expire", "now"], {
288975
+ encoding: "utf8",
288976
+ stdio: ["ignore", "pipe", "pipe"]
288977
+ });
288978
+ } catch {
288979
+ }
288980
+ return { ok: true, detail: "" };
288981
+ }
288982
+ function defaultReclaimOrphanDir(repositoryRoot, path) {
288983
+ if (!isBoundedLoopWorktreeDir(repositoryRoot, path)) {
288984
+ return { ok: false, detail: `refused: ${path} is outside .roll/loop/worktrees` };
288985
+ }
288986
+ if (!existsSync134(path)) {
288987
+ return { ok: false, detail: "missing: orphan dir already gone" };
288988
+ }
288989
+ try {
288990
+ rmSync25(path, { recursive: true, force: true });
288991
+ } catch (e) {
288992
+ return { ok: false, detail: e instanceof Error ? e.message : String(e) };
288851
288993
  }
288852
288994
  try {
288853
288995
  execFileSync32("git", ["-C", repositoryRoot, "worktree", "prune", "--expire", "now"], {
@@ -288862,6 +289004,7 @@ async function applyWorktreeCleanup(plan, options) {
288862
289004
  const repositoryRoot = resolve14(options.repositoryRoot);
288863
289005
  const auditFn = options.audit ?? (() => auditWorktrees({ repoRoot: repositoryRoot, home: homedir34() }));
288864
289006
  const removeFn = options.removeWorktree ?? defaultRemoveWorktree;
289007
+ const reclaimOrphanFn = options.reclaimOrphanDir ?? defaultReclaimOrphanDir;
288865
289008
  const removeBranchFn = options.removeBranch ?? defaultRemoveBranch;
288866
289009
  const emit4 = options.emit ?? (() => {
288867
289010
  });
@@ -288877,17 +289020,47 @@ async function applyWorktreeCleanup(plan, options) {
288877
289020
  emit4({ type: "worktree_cleanup_refused", path: candidate.path, reason, ts: nowMs2() });
288878
289021
  };
288879
289022
  if (!rec) {
288880
- refuse("missing: worktree no longer registered (already removed or pruned)");
288881
- continue;
288882
- }
288883
- if (rec.head !== candidate.expectedHead) {
288884
- refuse(`changed-head: expected ${candidate.expectedHead}, found ${rec.head ?? "none"}`);
289023
+ refuse(candidate.reclaim === "rm_dir" ? "missing: orphan dir no longer present (already reclaimed)" : "missing: worktree no longer registered (already removed or pruned)");
288885
289024
  continue;
288886
289025
  }
288887
289026
  if (rec.active) {
288888
289027
  refuse("active: worktree activated concurrently (fresh lock/heartbeat)");
288889
289028
  continue;
288890
289029
  }
289030
+ if (candidate.reclaim === "rm_dir") {
289031
+ if (!isReclaimableOrphan(rec)) {
289032
+ refuse(`disposition: fresh audit reports '${rec.disposition}' (orphan no longer provably delivered)`);
289033
+ continue;
289034
+ }
289035
+ const removal2 = {
289036
+ path: rec.path,
289037
+ expectedHead: "",
289038
+ reclaim: "rm_dir",
289039
+ ...rec.cycleId ? { cycleId: rec.cycleId } : {}
289040
+ };
289041
+ if (options.dryRun) {
289042
+ removed.push(removal2);
289043
+ continue;
289044
+ }
289045
+ const result3 = reclaimOrphanFn(repositoryRoot, rec.path);
289046
+ if (!result3.ok) {
289047
+ refuse(`reclaim-failed: ${result3.detail}`);
289048
+ continue;
289049
+ }
289050
+ removed.push(removal2);
289051
+ emit4({
289052
+ type: "worktree_cleanup_applied",
289053
+ path: rec.path,
289054
+ expectedHead: "",
289055
+ ...rec.cycleId ? { cycleId: rec.cycleId } : {},
289056
+ ts: nowMs2()
289057
+ });
289058
+ continue;
289059
+ }
289060
+ if (rec.head !== candidate.expectedHead) {
289061
+ refuse(`changed-head: expected ${candidate.expectedHead}, found ${rec.head ?? "none"}`);
289062
+ continue;
289063
+ }
288891
289064
  if (rec.dirtyTracked === true) {
288892
289065
  refuse("dirty: tracked changes appeared after planning");
288893
289066
  continue;
@@ -288903,6 +289076,7 @@ async function applyWorktreeCleanup(plan, options) {
288903
289076
  const removal = {
288904
289077
  path: rec.path,
288905
289078
  expectedHead: candidate.expectedHead,
289079
+ reclaim: "git_worktree",
288906
289080
  ...rec.branch ? { branch: rec.branch } : {},
288907
289081
  ...rec.cycleId ? { cycleId: rec.cycleId } : {}
288908
289082
  };
@@ -288968,7 +289142,7 @@ async function applyWorktreeCleanup(plan, options) {
288968
289142
  branchesRemoved.push(removalB);
288969
289143
  continue;
288970
289144
  }
288971
- const r = removeBranchFn(repositoryRoot, bc.branch);
289145
+ const r = removeBranchFn(repositoryRoot, bc.branch, bc.expectedSha);
288972
289146
  if (!r.ok) {
288973
289147
  refuseB(`delete-failed: ${r.detail}`);
288974
289148
  continue;
@@ -289058,12 +289232,22 @@ function renderPlanHuman(plan, mode) {
289058
289232
  lines3.push(`minimal candidate set (${plan.canaryTotal} \u2192 ${plan.projectedTotal})`);
289059
289233
  for (const c2 of plan.candidates) {
289060
289234
  const tags = [c2.branch, c2.cycleId].filter(Boolean).join(" ");
289061
- lines3.push(` worktree ${rel(c2.path)}${tags ? " " + tags : ""} [disposable_candidate]`);
289235
+ const label5 = c2.reclaim === "rm_dir" ? "orphan_reclaimable \xB7 bounded rm" : "disposable_candidate";
289236
+ const kind = c2.reclaim === "rm_dir" ? "orphan " : "worktree";
289237
+ lines3.push(` ${kind} ${rel(c2.path)}${tags ? " " + tags : ""} [${label5}]`);
289062
289238
  }
289063
289239
  for (const b of plan.branchCandidates) {
289064
289240
  lines3.push(` branch ${b.branch} ${b.expectedSha.slice(0, 9)} [merged: ${b.mergeKind}]`);
289065
289241
  }
289066
289242
  lines3.push("");
289243
+ const preservedOrphans = plan.preserved.filter((p) => p.disposition === "preserved_orphan");
289244
+ if (preservedOrphans.length > 0) {
289245
+ lines3.push(`preserved orphan dirs (${preservedOrphans.length}) \u2014 visible + counted, never auto-deleted`);
289246
+ for (const p of preservedOrphans)
289247
+ lines3.push(` ${rel(p.path)} \u2014 ${p.reason}`);
289248
+ lines3.push(" Reclaim one after review with: roll worktree cleanup --reclaim-orphan <path>");
289249
+ lines3.push("");
289250
+ }
289067
289251
  if (mode === "dry-run") {
289068
289252
  lines3.push("Dry run \u2014 no git state changed.");
289069
289253
  lines3.push("Apply the audited set with: roll worktree cleanup --apply");
@@ -289104,7 +289288,7 @@ function renderResultHuman(result2) {
289104
289288
  }
289105
289289
  return lines3.join("\n").trimEnd() + "\n";
289106
289290
  }
289107
- var CLEANUP_USAGE = "Usage: roll worktree cleanup [--dry-run | --apply] [--json] [--repo <path>]\n Safely recover from branch/worktree canary pressure using the worktree\n audit as the SOLE authority. Removes ONLY inactive, merged, clean\n `disposable_candidate` loop worktrees, plus (FIX-1454) standalone ephemeral\n branches that are verifiably merged (ancestor of the integration branch or a\n merged GitHub PR) and attached to no worktree \u2014 never a path/ref that is\n merely old or counted, and never a preserved (unpublished / dirty / active /\n external / current / protected / unmerged) one.\n\n Always dry-run first. Default (no flag) is --dry-run.\n --dry-run print counted refs/dirs, audit dispositions, and the minimal\n candidate set to clear pressure. Never mutates git state.\n --apply re-run the audit before EVERY removal; remove only revalidated\n candidates via git, prune registration, emit events. A changed\n head / new dirt / missing path / concurrent activation fails\n closed (no substitution). Then resume explicitly: roll loop resume\n --json emit the schema-1 plan (dry-run) or result (apply) as JSON\n --repo override the project root (default: current directory)\n\n \u5B89\u5168\u6E05\u7406:\u4EC5\u79FB\u9664\u5BA1\u8BA1\u5224\u5B9A\u4E3A\u5DF2\u5408\u5E76\u3001\u5E72\u51C0\u3001\u975E\u6D3B\u8DC3\u7684 disposable_candidate;\n \u5148\u8DD1 --dry-run,\u518D --apply,\u6700\u540E\u624B\u52A8 roll loop resume\u3002";
289291
+ var CLEANUP_USAGE = "Usage: roll worktree cleanup [--dry-run | --apply] [--json] [--repo <path>]\n Safely recover from branch/worktree canary pressure using the worktree\n audit as the SOLE authority. Removes ONLY inactive, merged, clean\n `disposable_candidate` loop worktrees, plus (FIX-1454) standalone ephemeral\n branches that are verifiably delivered and attached to no worktree. Delivery\n is PROVEN by one of: every commit is an ancestor of the integration branch;\n `git cherry` shows every commit already has an equivalent patch upstream; or\n (FIX-1471, squash merges) the branch tip tree is byte-identical to the merge\n commit of the branch's OWN merged GitHub PR (matched by exact head ref) AND\n that merge commit is an ancestor of the integration branch. A merged PR alone\n is NEVER sufficient, and no arbitrary same-tree commit on the integration\n branch is ever used. Never a path/ref that is merely old or counted, and never\n a preserved (unpublished / dirty / active / external / current / protected /\n unmerged) one.\n\n Always dry-run first. Default (no flag) is --dry-run.\n --dry-run print counted refs/dirs, audit dispositions, and the minimal\n candidate set to clear pressure. Never mutates git state.\n --apply re-run the audit before EVERY removal; remove only revalidated\n candidates via git, prune registration, emit events. A changed\n head / new dirt / missing path / concurrent activation fails\n closed (no substitution). Then resume explicitly: roll loop resume\n --json emit the schema-1 plan (dry-run) or result (apply) as JSON\n --repo override the project root (default: current directory)\n --reclaim-orphan <path> (FIX-1460) bounded-rm ONE named orphan loop dir\n (deregistered from git; delivery not auto-provable) after you\n review it. Fails closed unless it is an inactive loop orphan\n inside .roll/loop/worktrees. Auto-reclaim of provably-delivered\n orphans happens under --apply.\n\n \u5B89\u5168\u6E05\u7406:\u4EC5\u79FB\u9664\u5BA1\u8BA1\u5224\u5B9A\u4E3A\u5DF2\u5408\u5E76\u3001\u5E72\u51C0\u3001\u975E\u6D3B\u8DC3\u7684 disposable_candidate;\n \u5148\u8DD1 --dry-run,\u518D --apply,\u6700\u540E\u624B\u52A8 roll loop resume\u3002";
289108
289292
  function resolveThreshold() {
289109
289293
  const parsed = parseInt(process.env["ROLL_BRANCH_CANARY_MAX"] ?? "", 10);
289110
289294
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_BRANCH_CANARY_MAX;
@@ -289112,20 +289296,41 @@ function resolveThreshold() {
289112
289296
  function gitCap(repoRoot3, args) {
289113
289297
  return execFileSync32("git", ["-C", repoRoot3, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
289114
289298
  }
289115
- function classifyBranchMerge(branch, integrationBranch, git5) {
289299
+ function classifyBranchMerge(branch, integrationBranch, git5, prMergeCommit) {
289116
289300
  if (git5(["merge-base", "--is-ancestor", branch, integrationBranch]).ok)
289117
289301
  return "ancestor";
289118
289302
  const cherry = git5(["cherry", integrationBranch, branch]);
289119
- if (!cherry.ok)
289303
+ if (cherry.ok) {
289304
+ const lines3 = cherry.stdout.split("\n").map((l) => l.trim()).filter((l) => l !== "");
289305
+ if (lines3.length > 0 && lines3.every((l) => l.startsWith("-")))
289306
+ return "patch_equivalent";
289307
+ }
289308
+ return classifyFinalTreeDelivery(branch, integrationBranch, git5, prMergeCommit);
289309
+ }
289310
+ function classifyFinalTreeDelivery(branch, integrationBranch, git5, prMergeCommit) {
289311
+ const rawMerge = prMergeCommit(branch);
289312
+ if (rawMerge === null)
289120
289313
  return null;
289121
- const lines3 = cherry.stdout.split("\n").map((l) => l.trim()).filter((l) => l !== "");
289122
- if (lines3.length === 0)
289314
+ const merge = rawMerge.trim();
289315
+ if (!isFullGitOid(merge))
289123
289316
  return null;
289124
- if (lines3.some((l) => l.startsWith("+")))
289317
+ if (!git5(["merge-base", "--is-ancestor", merge, integrationBranch]).ok)
289125
289318
  return null;
289126
- if (lines3.every((l) => l.startsWith("-")))
289127
- return "patch_equivalent";
289128
- return null;
289319
+ const branchTreeProbe = git5(["rev-parse", `${branch}^{tree}`]);
289320
+ if (!branchTreeProbe.ok)
289321
+ return null;
289322
+ const branchTree = branchTreeProbe.stdout.trim();
289323
+ if (branchTree === "")
289324
+ return null;
289325
+ const mergeTreeProbe = git5(["rev-parse", `${merge}^{tree}`]);
289326
+ if (!mergeTreeProbe.ok)
289327
+ return null;
289328
+ const mergeTree = mergeTreeProbe.stdout.trim();
289329
+ if (mergeTree === "")
289330
+ return null;
289331
+ if (branchTree !== mergeTree)
289332
+ return null;
289333
+ return "final_tree";
289129
289334
  }
289130
289335
  function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
289131
289336
  const attachedBranches = new Set(audit.records.filter((r) => r.owner === "loop" && typeof r.branch === "string" && r.branch !== "").map((r) => r.branch.replace(/^refs\/heads\//, "")));
@@ -289145,11 +289350,12 @@ function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
289145
289350
  return null;
289146
289351
  }
289147
289352
  },
289148
- // FIX-1458 (#1465): delivery is proven ONLY by fresh git patch evidence via
289149
- // classifyBranchMerge (ancestor OR `git cherry` patch-equivalence). A merged
289150
- // GitHub PR is deliberately NOT consulted: a squash merge leaves the exact
289151
- // branch tips undelivered, so authorizing deletion on a merged PR alone
289152
- // silently discards unique commits (US-ORG-003/007/004 in the report).
289353
+ // FIX-1458 (#1465), FIX-1471: delivery is proven by fresh git patch evidence
289354
+ // (ancestor OR `git cherry` patch-equivalence) OR, for the squash-merge case,
289355
+ // by the branch tip tree matching the merge commit of the branch's OWN merged
289356
+ // GitHub PR (an ancestor of integration). A merged PR label alone never
289357
+ // authorizes deletion, and no arbitrary same-tree integration commit is ever
289358
+ // consulted — that would silently discard unique commits (US-ORG-003/007/004).
289153
289359
  branchMerge: (branch) => classifyBranchMerge(branch, integrationBranch, (args) => {
289154
289360
  try {
289155
289361
  return { ok: true, stdout: gitCap(repoRoot3, args) };
@@ -289157,9 +289363,39 @@ function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
289157
289363
  const stdout = typeof err16.stdout === "string" ? err16.stdout : "";
289158
289364
  return { ok: false, stdout };
289159
289365
  }
289160
- })
289366
+ }, (b) => ghMergedPrMergeCommit(repoRoot3, b))
289161
289367
  };
289162
289368
  }
289369
+ function isFullGitOid(value) {
289370
+ return /^[0-9a-f]{40}$/.test(value) || /^[0-9a-f]{64}$/.test(value);
289371
+ }
289372
+ function parseMergedPrMergeCommit(raw, branch) {
289373
+ let j;
289374
+ try {
289375
+ j = JSON.parse(raw);
289376
+ } catch {
289377
+ return null;
289378
+ }
289379
+ if (j.state !== "MERGED")
289380
+ return null;
289381
+ if (typeof j.mergedAt !== "string" || j.mergedAt === "")
289382
+ return null;
289383
+ if (typeof j.headRefName !== "string" || j.headRefName !== branch)
289384
+ return null;
289385
+ const oid = j.mergeCommit?.oid;
289386
+ if (typeof oid !== "string" || !isFullGitOid(oid))
289387
+ return null;
289388
+ return oid;
289389
+ }
289390
+ function ghMergedPrMergeCommit(repoRoot3, branch) {
289391
+ let out3;
289392
+ try {
289393
+ out3 = execFileSync32("gh", ["pr", "view", branch, "--json", "state,mergedAt,mergeCommit,headRefName"], { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
289394
+ } catch {
289395
+ return null;
289396
+ }
289397
+ return parseMergedPrMergeCommit(out3, branch);
289398
+ }
289163
289399
  function resolveIntegrationForCleanup(repoRoot3) {
289164
289400
  try {
289165
289401
  const head = gitCap(repoRoot3, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]).trim();
@@ -289189,10 +289425,57 @@ async function worktreeCleanupCommand(args, deps) {
289189
289425
  home: deps?.home ?? homedir34(),
289190
289426
  git: deps?.git,
289191
289427
  readFile: deps?.readFile,
289428
+ readDir: deps?.readDir,
289192
289429
  nowISO: deps?.nowISO,
289193
289430
  nowSec: deps?.nowSec,
289194
289431
  integrationBranch: deps?.integrationBranch
289195
289432
  };
289433
+ const reclaimIdx = args.indexOf("--reclaim-orphan");
289434
+ if (reclaimIdx >= 0) {
289435
+ const named2 = args[reclaimIdx + 1];
289436
+ if (!named2) {
289437
+ process.stderr.write("roll worktree cleanup: --reclaim-orphan requires a <path>.\n");
289438
+ return 2;
289439
+ }
289440
+ const fresh = auditWorktrees(fullDeps);
289441
+ const target = resolve14(named2);
289442
+ const rec = fresh.records.find((r2) => resolve14(r2.path) === target);
289443
+ if (!rec) {
289444
+ process.stderr.write(`refused: ${named2} is not in the worktree audit.
289445
+ `);
289446
+ return 2;
289447
+ }
289448
+ if (rec.owner !== "loop") {
289449
+ process.stderr.write(`refused: ${named2} is not a loop worktree (owner=${rec.owner}).
289450
+ `);
289451
+ return 2;
289452
+ }
289453
+ if (rec.active) {
289454
+ process.stderr.write(`refused: ${named2} has an active cycle lock.
289455
+ `);
289456
+ return 2;
289457
+ }
289458
+ if (rec.disposition !== "orphan_reclaimable" && rec.disposition !== "preserved_orphan") {
289459
+ process.stderr.write(`refused: ${named2} audits as '${rec.disposition}', not an orphan dir \u2014 use --apply for registered worktrees.
289460
+ `);
289461
+ return 2;
289462
+ }
289463
+ if (!isBoundedLoopWorktreeDir(repoRoot3, rec.path)) {
289464
+ process.stderr.write(`refused: ${named2} is outside .roll/loop/worktrees.
289465
+ `);
289466
+ return 2;
289467
+ }
289468
+ const reclaimFn = deps?.reclaimOrphanDir ?? defaultReclaimOrphanDir;
289469
+ const r = reclaimFn(repoRoot3, rec.path);
289470
+ if (!r.ok) {
289471
+ process.stderr.write(`reclaim-failed: ${r.detail}
289472
+ `);
289473
+ return 1;
289474
+ }
289475
+ process.stdout.write(`reclaimed orphan dir: ${rec.path}
289476
+ `);
289477
+ return 0;
289478
+ }
289196
289479
  const threshold = resolveThreshold();
289197
289480
  const integrationBranch = fullDeps.integrationBranch ?? resolveIntegrationForCleanup(repoRoot3);
289198
289481
  const auditNow = auditWorktrees(fullDeps);
@@ -289330,13 +289613,13 @@ function announceReport(projectPath3, slug3, storyId, opener = (p) => {
289330
289613
  const latest = join158(cardArchiveDir(projectPath3, storyId), "latest");
289331
289614
  const review = join158(latest, reviewFileName(storyId));
289332
289615
  const legacyReport = join158(latest, reportFileName(storyId));
289333
- const report = existsSync134(review) ? review : existsSync134(legacyReport) ? legacyReport : null;
289616
+ const report = existsSync135(review) ? review : existsSync135(legacyReport) ? legacyReport : null;
289334
289617
  if (report === null)
289335
289618
  return null;
289336
289619
  const label5 = currentLang() === "zh" ? "\u9A8C\u6536 Review Page" : "Acceptance Review Page";
289337
289620
  process.stdout.write(`${label5}: ${report}
289338
289621
  `);
289339
- const muted = existsSync134(join158(projectPath3, ".roll", "loop", `mute-${slug3}`)) || existsSync134(join158(process.env["ROLL_SHARED_ROOT"] || join158(process.env["HOME"] ?? "", ".shared", "roll"), "loop", `mute-${slug3}`));
289622
+ const muted = existsSync135(join158(projectPath3, ".roll", "loop", `mute-${slug3}`)) || existsSync135(join158(process.env["ROLL_SHARED_ROOT"] || join158(process.env["HOME"] ?? "", ".shared", "roll"), "loop", `mute-${slug3}`));
289340
289623
  if (!muted)
289341
289624
  opener(report);
289342
289625
  return report;
@@ -289360,7 +289643,7 @@ function cycleSignalTeardown(paths, cycleId, branch, sig, deps = {}) {
289360
289643
  }
289361
289644
  let owned = false;
289362
289645
  try {
289363
- owned = existsSync134(paths.lockPath) && readLockOwner(paths.lockPath)?.pid === pid;
289646
+ owned = existsSync135(paths.lockPath) && readLockOwner(paths.lockPath)?.pid === pid;
289364
289647
  } catch {
289365
289648
  owned = false;
289366
289649
  }
@@ -289505,7 +289788,7 @@ function incrementConsecutiveFails(projectPath3, slug3, alertsPath, eventsPath4,
289505
289788
  const counterFile = join158(rt, "consecutive-fails");
289506
289789
  let count = 0;
289507
289790
  try {
289508
- if (existsSync134(counterFile)) {
289791
+ if (existsSync135(counterFile)) {
289509
289792
  count = parseInt(readFileSync135(counterFile, "utf8").trim(), 10) || 0;
289510
289793
  }
289511
289794
  } catch {
@@ -289519,7 +289802,7 @@ function incrementConsecutiveFails(projectPath3, slug3, alertsPath, eventsPath4,
289519
289802
  if (count < threshold)
289520
289803
  return;
289521
289804
  const pauseMarker = join158(projectPath3, ".roll", "loop", `PAUSE-${slug3}`);
289522
- if (existsSync134(pauseMarker))
289805
+ if (existsSync135(pauseMarker))
289523
289806
  return;
289524
289807
  const alertMsg = `# ALERT \u2014 loop auto-paused after ${count} consecutive failures
289525
289808
 
@@ -289555,7 +289838,7 @@ loop run-once: \u8FDE\u7EED ${count} \u6B21\u5931\u8D25\u540E\u81EA\u52A8\u6682\
289555
289838
  }
289556
289839
  function writeRootCausePause(projectPath3, slug3, alertsPath, eventsPath4, cycleId, attribution, count, snapshotPath2) {
289557
289840
  const pauseMarker = join158(projectPath3, ".roll", "loop", `PAUSE-${slug3}`);
289558
- if (existsSync134(pauseMarker))
289841
+ if (existsSync135(pauseMarker))
289559
289842
  return;
289560
289843
  const playbook = playbookForFailure(attribution.failureClass, attribution.rootCauseKey);
289561
289844
  const alertMsg = `# ALERT \u2014 loop auto-paused on ${attribution.failureClass} failure
@@ -289617,7 +289900,7 @@ function handleNonCardFailure(projectPath3, slug3, alertsPath, eventsPath4, runt
289617
289900
  function readFailurePauseThreshold(projectPath3) {
289618
289901
  try {
289619
289902
  const policy = join158(projectPath3, ".roll", "policy.yaml");
289620
- if (!existsSync134(policy))
289903
+ if (!existsSync135(policy))
289621
289904
  return PAUSE_THRESHOLD;
289622
289905
  return parsePolicy(readFileSync135(policy, "utf8")).loopSafety.maxConsecutiveFailures;
289623
289906
  } catch {
@@ -289647,7 +289930,7 @@ function incrementConsecutiveIdle(projectPath3, slug3) {
289647
289930
  const file = idleCounterPath(projectPath3, slug3);
289648
289931
  let count = 0;
289649
289932
  try {
289650
- if (existsSync134(file)) {
289933
+ if (existsSync135(file)) {
289651
289934
  count = parseInt(readFileSync135(file, "utf8").trim(), 10) || 0;
289652
289935
  }
289653
289936
  } catch {
@@ -289717,7 +290000,7 @@ function readExternalBlock(eventsPath4, cycleId) {
289717
290000
  const authDetails = [];
289718
290001
  const networkDetails = [];
289719
290002
  try {
289720
- if (!existsSync134(eventsPath4))
290003
+ if (!existsSync135(eventsPath4))
289721
290004
  return null;
289722
290005
  for (const line of readFileSync135(eventsPath4, "utf8").split("\n")) {
289723
290006
  if (line.trim() === "" || !line.includes("agent:blocked"))
@@ -289782,7 +290065,7 @@ function writeReviewerBlockedAlert(projectPath3, slug3, alertsPath, eventsPath4,
289782
290065
  }
289783
290066
  if (block.cause === "auth") {
289784
290067
  const pauseMarker = join158(projectPath3, ".roll", "loop", `PAUSE-${slug3}`);
289785
- if (!existsSync134(pauseMarker)) {
290068
+ if (!existsSync135(pauseMarker)) {
289786
290069
  try {
289787
290070
  writeFileSync72(pauseMarker, msg7, "utf8");
289788
290071
  bus.appendEvent(eventsPath4, { type: "policy:safety_pause", loop: "ci", reason: `agent auth block: ${agents}`, ts: ts2 });
@@ -289810,7 +290093,7 @@ function buildLoopRouteDeps(projectPath3) {
289810
290093
  }
289811
290094
  return readRouteSlot(text2, slot);
289812
290095
  } catch {
289813
- if (existsSync134(agentsYaml)) {
290096
+ if (existsSync135(agentsYaml)) {
289814
290097
  throw new Error("legacy agent configuration is unsupported; run `roll agent migrate`");
289815
290098
  }
289816
290099
  return void 0;
@@ -289911,7 +290194,7 @@ async function loopRunOnceCommand(args) {
289911
290194
  removePendingPublish(rt, storyId);
289912
290195
  }
289913
290196
  }
289914
- if (isLoopPaused(id.path, id.slug)) {
290197
+ if (isLoopPaused(id.path, id.slug) && !isGuidedRunOnce(allowedCards)) {
289915
290198
  const lang10 = resolveLang({
289916
290199
  rollLang: process.env["ROLL_LANG"],
289917
290200
  lcAll: process.env["LC_ALL"],
@@ -290088,7 +290371,7 @@ loop run-once: \u8BC4\u5BA1\u5224\u5B9A ${resizeStory} \u8303\u56F4\u8FC7\u5927(
290088
290371
  slug: id.slug,
290089
290372
  count: idleCount,
290090
290373
  resolveState: () => resolveLoopRunState(id.path, id.slug),
290091
- readBacklog: () => existsSync134(backlogFile) ? readFileSync135(backlogFile, "utf8") : "",
290374
+ readBacklog: () => existsSync135(backlogFile) ? readFileSync135(backlogFile, "utf8") : "",
290092
290375
  scheduler: createScheduler(process.platform, { uid: process.getuid?.() ?? 0 }),
290093
290376
  loopLabel: launchdLabel("loop", id.slug),
290094
290377
  now: () => (/* @__PURE__ */ new Date()).toISOString(),
@@ -290277,7 +290560,7 @@ async function isOffline(resolve17 = (h) => lookup2(h)) {
290277
290560
  }
290278
290561
  }
290279
290562
  function isLoopPaused(projectPath3, slug3) {
290280
- return existsSync134(join158(projectPath3, ".roll", "loop", `PAUSE-${slug3}`));
290563
+ return existsSync135(join158(projectPath3, ".roll", "loop", `PAUSE-${slug3}`));
290281
290564
  }
290282
290565
  function branchCanaryTrips(projectPath3, slug3, rt, alertsPath) {
290283
290566
  let ephemeralBranchCount = 0;
@@ -290288,7 +290571,7 @@ function branchCanaryTrips(projectPath3, slug3, rt, alertsPath) {
290288
290571
  }
290289
290572
  let worktreeCount = 0;
290290
290573
  try {
290291
- worktreeCount = readdirSync48(join158(rt, "worktrees"), { withFileTypes: true }).filter((e) => e.isDirectory()).length;
290574
+ worktreeCount = readdirSync49(join158(rt, "worktrees"), { withFileTypes: true }).filter((e) => e.isDirectory()).length;
290292
290575
  } catch {
290293
290576
  }
290294
290577
  const parsed = parseInt(process.env["ROLL_BRANCH_CANARY_MAX"] ?? "", 10);
@@ -290396,7 +290679,7 @@ function checkCoreWorktreeContamination(cwd) {
290396
290679
  // packages/cli/dist/commands/offboard.js
290397
290680
  init_dist();
290398
290681
  import { spawnSync as spawnSync16 } from "node:child_process";
290399
- import { existsSync as existsSync135, readFileSync as readFileSync136, realpathSync as realpathSync20, renameSync as renameSync29, rmSync as rmSync25, writeFileSync as writeFileSync73 } from "node:fs";
290682
+ import { existsSync as existsSync136, readFileSync as readFileSync136, realpathSync as realpathSync21, renameSync as renameSync29, rmSync as rmSync26, writeFileSync as writeFileSync73 } from "node:fs";
290400
290683
  import { homedir as homedir35 } from "node:os";
290401
290684
  import { join as join159, resolve as resolve15 } from "node:path";
290402
290685
  function pal4() {
@@ -290497,7 +290780,7 @@ function writeFileAtomic2(path, text2) {
290497
290780
  writeFileSync73(tmp, text2);
290498
290781
  renameSync29(tmp, path);
290499
290782
  } catch (error) {
290500
- rmSync25(tmp, { force: true });
290783
+ rmSync26(tmp, { force: true });
290501
290784
  throw error;
290502
290785
  }
290503
290786
  }
@@ -290523,12 +290806,12 @@ function offboardCommand(args) {
290523
290806
  }
290524
290807
  let projectDir;
290525
290808
  try {
290526
- projectDir = realpathSync20(resolve15(process.cwd()));
290809
+ projectDir = realpathSync21(resolve15(process.cwd()));
290527
290810
  } catch {
290528
290811
  projectDir = process.cwd();
290529
290812
  }
290530
290813
  const changeset = changesetPath2(projectDir);
290531
- if (!existsSync135(changeset)) {
290814
+ if (!existsSync136(changeset)) {
290532
290815
  err10(m4("offboard.no_changeset_en"));
290533
290816
  err10(m4("offboard.no_changeset_zh"));
290534
290817
  process.stderr.write("\n");
@@ -290627,7 +290910,7 @@ function offboardCommand(args) {
290627
290910
  process.stdout.write(m4("offboard.applying_offboard") + "\n");
290628
290911
  for (const item of mergedFiles) {
290629
290912
  const path = resolveChangesetItem(projectDir, item);
290630
- if (!existsSync135(path))
290913
+ if (!existsSync136(path))
290631
290914
  continue;
290632
290915
  const before = readFileSync136(path, "utf8");
290633
290916
  const after = stripRollMergeBlocks(before);
@@ -290640,8 +290923,8 @@ function offboardCommand(args) {
290640
290923
  for (const item of files) {
290641
290924
  try {
290642
290925
  const path = resolveChangesetItem(projectDir, item);
290643
- rmSync25(path, { force: true });
290644
- if (!existsSync135(path))
290926
+ rmSync26(path, { force: true });
290927
+ if (!existsSync136(path))
290645
290928
  process.stdout.write(` removed file ${item}
290646
290929
  `);
290647
290930
  } catch {
@@ -290649,7 +290932,7 @@ function offboardCommand(args) {
290649
290932
  }
290650
290933
  for (const item of dirs) {
290651
290934
  try {
290652
- rmSync25(resolveChangesetItem(projectDir, item), { recursive: true, force: true });
290935
+ rmSync26(resolveChangesetItem(projectDir, item), { recursive: true, force: true });
290653
290936
  process.stdout.write(` removed dir ${item}
290654
290937
  `);
290655
290938
  } catch {
@@ -290657,7 +290940,7 @@ function offboardCommand(args) {
290657
290940
  }
290658
290941
  for (const item of giEntries) {
290659
290942
  const gi = join159(projectDir, ".gitignore");
290660
- if (existsSync135(gi)) {
290943
+ if (existsSync136(gi)) {
290661
290944
  const lines3 = readFileSync136(gi, "utf8").split("\n");
290662
290945
  if (lines3.includes(item)) {
290663
290946
  const kept = lines3.filter((l) => l !== item);
@@ -290673,21 +290956,21 @@ function offboardCommand(args) {
290673
290956
  if (r === 0)
290674
290957
  process.stdout.write(` unloaded ${item}
290675
290958
  `);
290676
- rmSync25(plistPath, { force: true });
290959
+ rmSync26(plistPath, { force: true });
290677
290960
  }
290678
- rmSync25(changeset, { force: true });
290961
+ rmSync26(changeset, { force: true });
290679
290962
  ok11(m4("offboard.offboard_complete_offboard"));
290680
290963
  return 0;
290681
290964
  }
290682
290965
 
290683
290966
  // packages/cli/dist/commands/prices.js
290684
290967
  init_dist();
290685
- import { existsSync as existsSync137, readdirSync as readdirSync50, readFileSync as readFileSync138 } from "node:fs";
290968
+ import { existsSync as existsSync138, readdirSync as readdirSync51, readFileSync as readFileSync138 } from "node:fs";
290686
290969
  import { join as join161 } from "node:path";
290687
290970
 
290688
290971
  // packages/cli/dist/commands/prices-refresh.js
290689
290972
  init_dist();
290690
- import { existsSync as existsSync136, mkdirSync as mkdirSync75, readdirSync as readdirSync49, readFileSync as readFileSync137, writeFileSync as writeFileSync74 } from "node:fs";
290973
+ import { existsSync as existsSync137, mkdirSync as mkdirSync75, readdirSync as readdirSync50, readFileSync as readFileSync137, writeFileSync as writeFileSync74 } from "node:fs";
290691
290974
  import { join as join160 } from "node:path";
290692
290975
  var FetchError = class extends Error {
290693
290976
  constructor(message) {
@@ -290909,9 +291192,9 @@ function vendorFromSnapshotName(name) {
290909
291192
  return match[2] ?? "anthropic";
290910
291193
  }
290911
291194
  function latestSnapshotPath(snapshotDir, vendor) {
290912
- if (!existsSync136(snapshotDir))
291195
+ if (!existsSync137(snapshotDir))
290913
291196
  return null;
290914
- const snaps = readdirSync49(snapshotDir).filter((name) => SNAPSHOT_RE.test(name) && vendorFromSnapshotName(name) === vendor).sort().map((name) => join160(snapshotDir, name));
291197
+ const snaps = readdirSync50(snapshotDir).filter((name) => SNAPSHOT_RE.test(name) && vendorFromSnapshotName(name) === vendor).sort().map((name) => join160(snapshotDir, name));
290915
291198
  return snaps[snaps.length - 1] ?? null;
290916
291199
  }
290917
291200
  function readPrices(path) {
@@ -291118,10 +291401,10 @@ async function pricesRefreshCommand(args, deps = {}) {
291118
291401
  // packages/cli/dist/commands/prices.js
291119
291402
  function loadSnapshots() {
291120
291403
  const dir = join161(repoRoot2(), "lib", "prices");
291121
- if (!existsSync137(dir)) {
291404
+ if (!existsSync138(dir)) {
291122
291405
  throw new Error(`no price snapshots found in ${dir}; run \`roll prices refresh\``);
291123
291406
  }
291124
- const files = readdirSync50(dir).filter((n) => n.startsWith("snapshot-") && n.endsWith(".json")).sort();
291407
+ const files = readdirSync51(dir).filter((n) => n.startsWith("snapshot-") && n.endsWith(".json")).sort();
291125
291408
  return files.map((name) => {
291126
291409
  const data = JSON.parse(readFileSync138(join161(dir, name), "utf8"));
291127
291410
  for (const key of ["version", "effective_at", "source_url", "prices"]) {
@@ -291302,7 +291585,7 @@ init_dist3();
291302
291585
  init_dist();
291303
291586
  init_render();
291304
291587
  import { execFileSync as execFileSync35 } from "node:child_process";
291305
- import { mkdirSync as mkdirSync79, readFileSync as readFileSync142, writeFileSync as writeFileSync78, existsSync as existsSync141 } from "node:fs";
291588
+ import { mkdirSync as mkdirSync79, readFileSync as readFileSync142, writeFileSync as writeFileSync78, existsSync as existsSync142 } from "node:fs";
291306
291589
  import { dirname as dirname78, join as join165 } from "node:path";
291307
291590
 
291308
291591
  // packages/cli/dist/lib/release-consistency.js
@@ -291311,7 +291594,7 @@ init_dist3();
291311
291594
  init_dist();
291312
291595
  init_render();
291313
291596
  import { execFileSync as execFileSync34 } from "node:child_process";
291314
- import { existsSync as existsSync139, mkdirSync as mkdirSync77, readFileSync as readFileSync140, readdirSync as readdirSync52, statSync as statSync39, writeFileSync as writeFileSync76 } from "node:fs";
291597
+ import { existsSync as existsSync140, mkdirSync as mkdirSync77, readFileSync as readFileSync140, readdirSync as readdirSync53, statSync as statSync39, writeFileSync as writeFileSync76 } from "node:fs";
291315
291598
  import { dirname as dirname76, join as join163 } from "node:path";
291316
291599
 
291317
291600
  // packages/cli/dist/lib/consistency-audit.js
@@ -291319,7 +291602,7 @@ init_dist2();
291319
291602
  init_dist3();
291320
291603
  init_dist();
291321
291604
  import { execFile as execFile19 } from "node:child_process";
291322
- import { existsSync as existsSync138, mkdirSync as mkdirSync76, readFileSync as readFileSync139, readdirSync as readdirSync51, writeFileSync as writeFileSync75 } from "node:fs";
291605
+ import { existsSync as existsSync139, mkdirSync as mkdirSync76, readFileSync as readFileSync139, readdirSync as readdirSync52, writeFileSync as writeFileSync75 } from "node:fs";
291323
291606
  import { dirname as dirname75, join as join162 } from "node:path";
291324
291607
  import { promisify as promisify18 } from "node:util";
291325
291608
  init_dist();
@@ -291328,7 +291611,7 @@ var PROBE_CAP = 20;
291328
291611
  var COUNT_WINDOW_SEC = 72 * 3600;
291329
291612
  var execFileAsync18 = promisify18(execFile19);
291330
291613
  function readJsonl(path) {
291331
- if (!existsSync138(path))
291614
+ if (!existsSync139(path))
291332
291615
  return [];
291333
291616
  const out3 = [];
291334
291617
  for (const line of readFileSync139(path, "utf8").split("\n")) {
@@ -291344,7 +291627,7 @@ function readJsonl(path) {
291344
291627
  return out3;
291345
291628
  }
291346
291629
  function readJson(path) {
291347
- if (!existsSync138(path))
291630
+ if (!existsSync139(path))
291348
291631
  return null;
291349
291632
  try {
291350
291633
  const parsed = JSON.parse(readFileSync139(path, "utf8"));
@@ -291372,10 +291655,10 @@ function hasScreenshotArtifact(reportPath) {
291372
291655
  }
291373
291656
  }
291374
291657
  const dir = join162(runDir, "screenshots");
291375
- if (!existsSync138(dir))
291658
+ if (!existsSync139(dir))
291376
291659
  return false;
291377
291660
  try {
291378
- return readdirSync51(dir).some((name) => /\.png$/i.test(name));
291661
+ return readdirSync52(dir).some((name) => /\.png$/i.test(name));
291379
291662
  } catch {
291380
291663
  return false;
291381
291664
  }
@@ -291409,7 +291692,7 @@ async function gatherAuditSnapshot(projectPath3, runtimeDir11, deps = {}) {
291409
291692
  const snapshot = emptyAuditSnapshot(nowSec, TERMINAL_SCHEMA_EPOCH_SEC);
291410
291693
  const skipped2 = [];
291411
291694
  const backlogPath = join162(projectPath3, ".roll", "backlog.md");
291412
- if (existsSync138(backlogPath)) {
291695
+ if (existsSync139(backlogPath)) {
291413
291696
  snapshot.backlog = parseBacklog(readFileSync139(backlogPath, "utf8")).map((r) => ({ id: r.id, status: r.status }));
291414
291697
  }
291415
291698
  snapshot.index = readIndex(projectPath3);
@@ -291418,7 +291701,7 @@ async function gatherAuditSnapshot(projectPath3, runtimeDir11, deps = {}) {
291418
291701
  let eventFailed = 0;
291419
291702
  const terminal = [];
291420
291703
  const eventsPath4 = join162(runtimeDir11, "events.ndjson");
291421
- if (existsSync138(eventsPath4)) {
291704
+ if (existsSync139(eventsPath4)) {
291422
291705
  for (const line of readFileSync139(eventsPath4, "utf8").split("\n")) {
291423
291706
  const e = parseEventLine(line);
291424
291707
  if (e === null)
@@ -291441,22 +291724,22 @@ async function gatherAuditSnapshot(projectPath3, runtimeDir11, deps = {}) {
291441
291724
  if (!row2.status.includes("\u2705"))
291442
291725
  continue;
291443
291726
  const card = cardArchiveDir(projectPath3, row2.id);
291444
- if (!existsSync138(card))
291727
+ if (!existsSync139(card))
291445
291728
  continue;
291446
291729
  const reportPath = join162(card, "latest", reportFileName(row2.id));
291447
291730
  const specPath = join162(card, "spec.md");
291448
291731
  let noVisualSurface = false;
291449
- if (existsSync138(specPath)) {
291732
+ if (existsSync139(specPath)) {
291450
291733
  try {
291451
291734
  noVisualSurface = !hasVisualEvidenceAc(readFileSync139(specPath, "utf8"));
291452
291735
  } catch {
291453
291736
  }
291454
291737
  }
291455
291738
  snapshot.attest[row2.id] = {
291456
- report: existsSync138(reportPath),
291457
- acMap: existsSync138(join162(card, "ac-map.json")),
291458
- visualEvidence: existsSync138(reportPath) ? hasScreenshotArtifact(reportPath) : false,
291459
- machineSkip: existsSync138(reportPath) ? hasMachineCaptureSkip2(reportPath) : false,
291739
+ report: existsSync139(reportPath),
291740
+ acMap: existsSync139(join162(card, "ac-map.json")),
291741
+ visualEvidence: existsSync139(reportPath) ? hasScreenshotArtifact(reportPath) : false,
291742
+ machineSkip: existsSync139(reportPath) ? hasMachineCaptureSkip2(reportPath) : false,
291460
291743
  noVisualSurface
291461
291744
  };
291462
291745
  }
@@ -291628,7 +291911,7 @@ function walkTextFiles(projectDir, relDir, extensions) {
291628
291911
  const dirAbs = join163(projectDir, dirRel);
291629
291912
  let entries;
291630
291913
  try {
291631
- entries = readdirSync52(dirAbs, { withFileTypes: true });
291914
+ entries = readdirSync53(dirAbs, { withFileTypes: true });
291632
291915
  } catch {
291633
291916
  return;
291634
291917
  }
@@ -291837,11 +292120,11 @@ function manualDirectMainDelivery(projectDir, id, mergeShas) {
291837
292120
  function findCardSpec(projectDir, id) {
291838
292121
  const featuresDir = join163(projectDir, ".roll", "features");
291839
292122
  try {
291840
- for (const epic of readdirSync52(featuresDir, { withFileTypes: true })) {
292123
+ for (const epic of readdirSync53(featuresDir, { withFileTypes: true })) {
291841
292124
  if (!epic.isDirectory())
291842
292125
  continue;
291843
292126
  const spec = join163(featuresDir, epic.name, id, "spec.md");
291844
- if (existsSync139(spec))
292127
+ if (existsSync140(spec))
291845
292128
  return spec;
291846
292129
  }
291847
292130
  } catch {
@@ -291860,12 +292143,12 @@ function cardChangelogExempt(projectDir, id) {
291860
292143
  }
291861
292144
  function checkFeaturesCatalog(projectDir) {
291862
292145
  const backlog = join163(projectDir, ".roll", "backlog.md");
291863
- if (!existsSync139(backlog))
292146
+ if (!existsSync140(backlog))
291864
292147
  return { status: "pass", gaps: [] };
291865
292148
  const backlogText = readText(backlog);
291866
292149
  const gaps = [];
291867
292150
  const features = join163(projectDir, ".roll", "features.md");
291868
- if (existsSync139(features)) {
292151
+ if (existsSync140(features)) {
291869
292152
  const featuresText = readText(features);
291870
292153
  for (const featName of readDoneFeatures(backlogText).keys()) {
291871
292154
  const escaped = escapeRegExp4(featName);
@@ -291889,7 +292172,7 @@ function checkFeaturesCatalog(projectDir) {
291889
292172
  }
291890
292173
  function checkTruthLive(projectDir) {
291891
292174
  const backlog = join163(projectDir, ".roll", "backlog.md");
291892
- if (!existsSync139(backlog))
292175
+ if (!existsSync140(backlog))
291893
292176
  return { status: "pass", gaps: [] };
291894
292177
  const backlogText = readText(backlog);
291895
292178
  const facts = backlogRowFacts(backlogText);
@@ -291934,17 +292217,17 @@ function checkTruthLive(projectDir) {
291934
292217
  function checkCards(projectDir) {
291935
292218
  const backlog = join163(projectDir, ".roll", "backlog.md");
291936
292219
  const featuresDir = join163(projectDir, ".roll", "features");
291937
- if (!existsSync139(backlog) || !existsSync139(featuresDir))
292220
+ if (!existsSync140(backlog) || !existsSync140(featuresDir))
291938
292221
  return { status: "pass", gaps: [] };
291939
292222
  const cardEpic = /* @__PURE__ */ new Map();
291940
292223
  try {
291941
- for (const epic of readdirSync52(featuresDir, { withFileTypes: true })) {
292224
+ for (const epic of readdirSync53(featuresDir, { withFileTypes: true })) {
291942
292225
  if (!epic.isDirectory())
291943
292226
  continue;
291944
- for (const card of readdirSync52(join163(featuresDir, epic.name), { withFileTypes: true })) {
292227
+ for (const card of readdirSync53(join163(featuresDir, epic.name), { withFileTypes: true })) {
291945
292228
  if (!card.isDirectory())
291946
292229
  continue;
291947
- if (existsSync139(join163(featuresDir, epic.name, card.name, "spec.md")) && !cardEpic.has(card.name)) {
292230
+ if (existsSync140(join163(featuresDir, epic.name, card.name, "spec.md")) && !cardEpic.has(card.name)) {
291948
292231
  cardEpic.set(card.name, epic.name);
291949
292232
  }
291950
292233
  }
@@ -291979,12 +292262,12 @@ function checkCards(projectDir) {
291979
292262
  const ev = /\[evidence\]\(([^)]+)\)/.exec(line);
291980
292263
  if (ev !== null) {
291981
292264
  const target = (ev[1] ?? "").replace(/^\.roll\//, "");
291982
- if (!existsSync139(join163(projectDir, ".roll", target))) {
292265
+ if (!existsSync140(join163(projectDir, ".roll", target))) {
291983
292266
  gaps.push(`Backlog row ${id} evidence link is broken: ${ev[1] ?? ""}`);
291984
292267
  }
291985
292268
  } else if (line.includes(STATUS_MARKER.done)) {
291986
292269
  const epic = cardEpic.get(id) ?? "";
291987
- if (!existsSync139(join163(featuresDir, epic, id, "latest", `${id}-report.html`))) {
292270
+ if (!existsSync140(join163(featuresDir, epic, id, "latest", `${id}-report.html`))) {
291988
292271
  if (hasAcBlock(epic, id)) {
291989
292272
  gaps.push(`Done backlog row ${id} has ACs but no attest report`);
291990
292273
  } else {
@@ -292007,7 +292290,7 @@ function checkCards(projectDir) {
292007
292290
  function checkDocs(projectDir) {
292008
292291
  const gaps = checkTopLevelCommands(activeDocsFiles(projectDir), DOC_RETIRED_TOP_LEVEL_COMMANDS);
292009
292292
  const changelogPath = join163(projectDir, "CHANGELOG.md");
292010
- if (existsSync139(changelogPath)) {
292293
+ if (existsSync140(changelogPath)) {
292011
292294
  const changelog = readText(changelogPath);
292012
292295
  for (const id of releaseDeltaCardIds(projectDir)) {
292013
292296
  const base = id.replace(/[a-z]$/, "");
@@ -292082,7 +292365,7 @@ function checkSite(projectDir) {
292082
292365
  const gaps = checkTopLevelCommands(activeSiteFiles(projectDir), SITE_HIDDEN_TOP_LEVEL_COMMANDS);
292083
292366
  const siteJs = join163(projectDir, "site", "roll-data.js");
292084
292367
  const backlog = join163(projectDir, ".roll", "backlog.md");
292085
- if (!existsSync139(siteJs) || !existsSync139(backlog))
292368
+ if (!existsSync140(siteJs) || !existsSync140(backlog))
292086
292369
  return { status: gaps.length === 0 ? "pass" : "fail", gaps };
292087
292370
  const siteText = readText(siteJs);
292088
292371
  const seenGuideRefs = /* @__PURE__ */ new Set();
@@ -292091,7 +292374,7 @@ function checkSite(projectDir) {
292091
292374
  if (seenGuideRefs.has(rel2))
292092
292375
  continue;
292093
292376
  seenGuideRefs.add(rel2);
292094
- if (!existsSync139(join163(projectDir, rel2))) {
292377
+ if (!existsSync140(join163(projectDir, rel2))) {
292095
292378
  gaps.push(`site/roll-data.js links a guide that does not exist: ${rel2}`);
292096
292379
  }
292097
292380
  }
@@ -292130,7 +292413,7 @@ function checkSite(projectDir) {
292130
292413
  }
292131
292414
  function listFiles2(dir) {
292132
292415
  try {
292133
- return readdirSync52(dir).filter((n) => {
292416
+ return readdirSync53(dir).filter((n) => {
292134
292417
  try {
292135
292418
  return statSync39(join163(dir, n)).isFile();
292136
292419
  } catch {
@@ -292145,7 +292428,7 @@ function checkI18n(projectDir) {
292145
292428
  const gaps = [];
292146
292429
  const guideEn = join163(projectDir, "guide", "en");
292147
292430
  const guideZh = join163(projectDir, "guide", "zh");
292148
- if (existsSync139(guideEn) && existsSync139(guideZh)) {
292431
+ if (existsSync140(guideEn) && existsSync140(guideZh)) {
292149
292432
  const enFiles = new Set(listFiles2(guideEn));
292150
292433
  const zhFiles = new Set(listFiles2(guideZh));
292151
292434
  const enOnly = [...enFiles].filter((f) => !zhFiles.has(f)).sort();
@@ -292156,12 +292439,12 @@ function checkI18n(projectDir) {
292156
292439
  gaps.push(`guide/zh/${f} has no corresponding guide/en/${f}`);
292157
292440
  }
292158
292441
  const i18nDir = join163(projectDir, "lib", "i18n");
292159
- if (existsSync139(i18nDir)) {
292442
+ if (existsSync140(i18nDir)) {
292160
292443
  const keysEn = /* @__PURE__ */ new Set();
292161
292444
  const keysZh = /* @__PURE__ */ new Set();
292162
292445
  let shFiles = [];
292163
292446
  try {
292164
- shFiles = readdirSync52(i18nDir).filter((n) => n.endsWith(".sh")).sort();
292447
+ shFiles = readdirSync53(i18nDir).filter((n) => n.endsWith(".sh")).sort();
292165
292448
  } catch {
292166
292449
  shFiles = [];
292167
292450
  }
@@ -292201,7 +292484,7 @@ function rglobBats(dir) {
292201
292484
  const walk2 = (d) => {
292202
292485
  let entries = [];
292203
292486
  try {
292204
- entries = readdirSync52(d);
292487
+ entries = readdirSync53(d);
292205
292488
  } catch {
292206
292489
  return;
292207
292490
  }
@@ -292226,7 +292509,7 @@ function checkTests(projectDir) {
292226
292509
  const gaps = [];
292227
292510
  const backlog = join163(projectDir, ".roll", "backlog.md");
292228
292511
  const testsDir = join163(projectDir, "tests");
292229
- if (!existsSync139(backlog))
292512
+ if (!existsSync140(backlog))
292230
292513
  return { status: "pass", gaps: [] };
292231
292514
  const backlogText = readText(backlog);
292232
292515
  const allFeatures = /* @__PURE__ */ new Set();
@@ -292251,7 +292534,7 @@ function checkTests(projectDir) {
292251
292534
  doneFeatures.push(currentFeature);
292252
292535
  }
292253
292536
  }
292254
- const testFiles = existsSync139(testsDir) ? rglobBats(testsDir) : [];
292537
+ const testFiles = existsSync140(testsDir) ? rglobBats(testsDir) : [];
292255
292538
  if (testFiles.length === 0)
292256
292539
  return { status: "pass", gaps: [] };
292257
292540
  for (const feat of doneFeatures) {
@@ -292603,7 +292886,7 @@ function writeReleaseTestProof(exec) {
292603
292886
  function assertTestProofFresh(exec) {
292604
292887
  const cwd = exec("git", ["rev-parse", "--show-toplevel"]).trim();
292605
292888
  const proofPath = join165(cwd, ".roll", "last-test-pass");
292606
- if (!existsSync141(proofPath)) {
292889
+ if (!existsSync142(proofPath)) {
292607
292890
  throw new Error("test-pass proof missing \u2014 run `roll test` before committing");
292608
292891
  }
292609
292892
  const body = readFileSync142(proofPath, "utf8");
@@ -292623,28 +292906,49 @@ function assertTestProofFresh(exec) {
292623
292906
  throw new Error("code changed since last test run \u2014 run `roll test` on the current staged tree");
292624
292907
  }
292625
292908
  }
292909
+ function hasOnlyPreparedReleaseChanges(exec) {
292910
+ const status2 = exec("git", ["status", "--porcelain", "--untracked-files=all"]).trim();
292911
+ if (status2 === "")
292912
+ return false;
292913
+ const allowed2 = /* @__PURE__ */ new Set(["package.json", "CHANGELOG.md"]);
292914
+ const unexpected = status2.split("\n").map((line) => line.slice(2).trim()).filter((path) => !allowed2.has(path));
292915
+ if (unexpected.length > 0) {
292916
+ throw new Error(`unexpected worktree changes during release recovery: ${unexpected.join(", ")}`);
292917
+ }
292918
+ return true;
292919
+ }
292626
292920
  function commitPushWithGate(opts) {
292627
292921
  const { branch, message, rollManaged, exec } = opts;
292628
292922
  const original = exec("git", ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
292923
+ const preparedChanges = hasOnlyPreparedReleaseChanges(exec);
292629
292924
  let createdLocal = false;
292925
+ const checkoutExistingBranch = () => {
292926
+ if (preparedChanges) {
292927
+ exec("git", ["checkout", "--merge", branch]);
292928
+ return;
292929
+ }
292930
+ exec("git", ["checkout", branch]);
292931
+ };
292630
292932
  const checkoutBranch = () => {
292933
+ let localSha = "";
292631
292934
  try {
292632
- const localSha = exec("git", ["rev-parse", "--verify", `refs/heads/${branch}`]).trim();
292633
- if (localSha !== "") {
292634
- exec("git", ["checkout", branch]);
292635
- return;
292636
- }
292935
+ localSha = exec("git", ["rev-parse", "--verify", `refs/heads/${branch}`]).trim();
292637
292936
  } catch {
292638
292937
  }
292938
+ if (localSha !== "") {
292939
+ checkoutExistingBranch();
292940
+ return;
292941
+ }
292942
+ let remote = "";
292639
292943
  try {
292640
- const remote = exec("git", ["ls-remote", "--heads", "origin", branch]).trim();
292641
- if (remote !== "") {
292642
- exec("git", ["fetch", "origin", `${branch}:${branch}`]);
292643
- exec("git", ["checkout", branch]);
292644
- return;
292645
- }
292944
+ remote = exec("git", ["ls-remote", "--heads", "origin", branch]).trim();
292646
292945
  } catch {
292647
292946
  }
292947
+ if (remote !== "") {
292948
+ exec("git", ["fetch", "origin", `${branch}:${branch}`]);
292949
+ checkoutExistingBranch();
292950
+ return;
292951
+ }
292648
292952
  createdLocal = true;
292649
292953
  exec("git", ["checkout", "-b", branch]);
292650
292954
  };
@@ -292747,7 +293051,7 @@ function realReleaseDeps() {
292747
293051
  commitPushWithGate({
292748
293052
  branch,
292749
293053
  message,
292750
- rollManaged: existsSync141(join165(cwd, ".roll")),
293054
+ rollManaged: existsSync142(join165(cwd, ".roll")),
292751
293055
  exec: (cmd, args) => execFileSync35(cmd, args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], timeout: 6e5 })
292752
293056
  });
292753
293057
  },
@@ -293072,13 +293376,13 @@ ${c("dim", "\u2192 Running the golden-path showcase (real models; its verdict do
293072
293376
  init_dist();
293073
293377
  init_render();
293074
293378
  import { spawnSync as spawnSync19 } from "node:child_process";
293075
- import { existsSync as existsSync143, lstatSync as lstatSync13, mkdirSync as mkdirSync81, readFileSync as readFileSync144, readdirSync as readdirSync55, readlinkSync as readlinkSync3, statSync as statSync42 } from "node:fs";
293379
+ import { existsSync as existsSync144, lstatSync as lstatSync13, mkdirSync as mkdirSync81, readFileSync as readFileSync144, readdirSync as readdirSync56, readlinkSync as readlinkSync3, statSync as statSync42 } from "node:fs";
293076
293380
  import { join as join167 } from "node:path";
293077
293381
 
293078
293382
  // packages/cli/dist/lib/roll-capture-install.js
293079
293383
  init_dist();
293080
293384
  import { spawnSync as spawnSync18 } from "node:child_process";
293081
- import { chmodSync as chmodSync2, existsSync as existsSync142, lstatSync as lstatSync12, mkdirSync as mkdirSync80, mkdtempSync as mkdtempSync9, readdirSync as readdirSync54, readFileSync as readFileSync143, renameSync as renameSync30, rmSync as rmSync27, statSync as statSync41, writeFileSync as writeFileSync79 } from "node:fs";
293385
+ import { chmodSync as chmodSync2, existsSync as existsSync143, lstatSync as lstatSync12, mkdirSync as mkdirSync80, mkdtempSync as mkdtempSync9, readdirSync as readdirSync55, readFileSync as readFileSync143, renameSync as renameSync30, rmSync as rmSync28, statSync as statSync41, writeFileSync as writeFileSync79 } from "node:fs";
293082
293386
  import { homedir as homedir36 } from "node:os";
293083
293387
  import { join as join166 } from "node:path";
293084
293388
  var RELEASE_API = "https://api.github.com/repos/seanyao/roll-capture/releases/latest";
@@ -293105,7 +293409,7 @@ function defaultRollCaptureInstallDeps() {
293105
293409
  home: homedir36(),
293106
293410
  uid: typeof process.getuid === "function" ? process.getuid() : void 0,
293107
293411
  ...platform3 === "darwin" && !isCi3(process.env) && process.env["ROLL_SKIP_CAPTURE_INSTALL"] !== "1" ? { hasAquaGUI: macosHasAquaGUI3(execFile20) } : {},
293108
- exists: existsSync142,
293412
+ exists: existsSync143,
293109
293413
  renamePath: renameSync30,
293110
293414
  execFile: execFile20,
293111
293415
  fetchLatestRelease: (timeoutMs) => defaultFetchLatestRelease(timeoutMs, process.env, execFile20),
@@ -293177,7 +293481,7 @@ async function installRollCapture(deps = defaultRollCaptureInstallDeps()) {
293177
293481
  } catch (error) {
293178
293482
  return manual(`install failed: ${errorMessage2(error)}`, release.tagName);
293179
293483
  } finally {
293180
- rmSync27(workDir, { recursive: true, force: true });
293484
+ rmSync28(workDir, { recursive: true, force: true });
293181
293485
  }
293182
293486
  }
293183
293487
  function renderRollCaptureInstallResult(result2, lang10) {
@@ -293313,7 +293617,7 @@ function macosHasAquaGUI3(execFile20) {
293313
293617
  }
293314
293618
  function findExtractedApp(dir) {
293315
293619
  try {
293316
- for (const name of readdirSync54(dir)) {
293620
+ for (const name of readdirSync55(dir)) {
293317
293621
  const candidate = join166(dir, name);
293318
293622
  const st = lstatSync12(candidate);
293319
293623
  if (st.isSymbolicLink())
@@ -293334,7 +293638,7 @@ function findExtractedApp(dir) {
293334
293638
  function findBundleExecutable(appPath) {
293335
293639
  const macosDir = join166(appPath, "Contents", "MacOS");
293336
293640
  try {
293337
- for (const name of readdirSync54(macosDir)) {
293641
+ for (const name of readdirSync55(macosDir)) {
293338
293642
  const candidate = join166(macosDir, name);
293339
293643
  const st = lstatSync12(candidate);
293340
293644
  if (st.isSymbolicLink())
@@ -293351,7 +293655,7 @@ function replaceAppAtomically(app, targetApp, deps) {
293351
293655
  const renamePath = deps.renamePath ?? renameSync30;
293352
293656
  const backupApp = `${targetApp}.bak`;
293353
293657
  const hadExisting = lstatExists(targetApp);
293354
- rmSync27(backupApp, { recursive: true, force: true });
293658
+ rmSync28(backupApp, { recursive: true, force: true });
293355
293659
  if (!hadExisting) {
293356
293660
  renamePath(app, targetApp);
293357
293661
  return;
@@ -293363,10 +293667,10 @@ function replaceAppAtomically(app, targetApp, deps) {
293363
293667
  backupCreated = true;
293364
293668
  renamePath(app, targetApp);
293365
293669
  targetReplaced = true;
293366
- rmSync27(backupApp, { recursive: true, force: true });
293670
+ rmSync28(backupApp, { recursive: true, force: true });
293367
293671
  } catch (error) {
293368
293672
  if (targetReplaced)
293369
- rmSync27(targetApp, { recursive: true, force: true });
293673
+ rmSync28(targetApp, { recursive: true, force: true });
293370
293674
  if (backupCreated) {
293371
293675
  try {
293372
293676
  renamePath(backupApp, targetApp);
@@ -293542,7 +293846,7 @@ function setupSnapshot(watch) {
293542
293846
  function walk(dir, lines3) {
293543
293847
  let names;
293544
293848
  try {
293545
- names = readdirSync55(dir);
293849
+ names = readdirSync56(dir);
293546
293850
  } catch {
293547
293851
  return;
293548
293852
  }
@@ -293617,11 +293921,11 @@ function tmuxPresent() {
293617
293921
  function submoduleGuard() {
293618
293922
  const pkg = rollPkgDir();
293619
293923
  const skills = join167(pkg, "skills");
293620
- const hasGit = existsSync143(join167(pkg, ".git"));
293621
- const hasMods = existsSync143(join167(pkg, ".gitmodules"));
293924
+ const hasGit = existsSync144(join167(pkg, ".git"));
293925
+ const hasMods = existsSync144(join167(pkg, ".gitmodules"));
293622
293926
  let empty = true;
293623
293927
  try {
293624
- empty = readdirSync55(skills).length === 0;
293928
+ empty = readdirSync56(skills).length === 0;
293625
293929
  } catch {
293626
293930
  empty = true;
293627
293931
  }
@@ -293735,7 +294039,7 @@ async function setupCommand2(args) {
293735
294039
  return 1;
293736
294040
  }
293737
294041
  }
293738
- if (!existsSync143(rollPkgConventions())) {
294042
+ if (!existsSync144(rollPkgConventions())) {
293739
294043
  err13(m5("shared.convention_source_not_found_at_2", rollPkgConventions()));
293740
294044
  err13(m5("shared.run_this_from_the_roll_repo"));
293741
294045
  return 1;
@@ -293826,7 +294130,7 @@ init_showcase2();
293826
294130
  // packages/cli/dist/commands/test.js
293827
294131
  init_dist2();
293828
294132
  import { spawnSync as spawnSync20 } from "node:child_process";
293829
- import { appendFileSync as appendFileSync22, existsSync as existsSync144, mkdirSync as mkdirSync82, readFileSync as readFileSync145, rmSync as rmSync28, writeFileSync as writeFileSync80 } from "node:fs";
294133
+ import { appendFileSync as appendFileSync22, existsSync as existsSync145, mkdirSync as mkdirSync82, readFileSync as readFileSync145, rmSync as rmSync29, writeFileSync as writeFileSync80 } from "node:fs";
293830
294134
  import { dirname as dirname79, join as join168, resolve as resolve16 } from "node:path";
293831
294135
  function pal6() {
293832
294136
  const noColor2 = (process.env["NO_COLOR"] ?? "") !== "";
@@ -293848,7 +294152,7 @@ function currentEvidenceFrame() {
293848
294152
  if (raw === "")
293849
294153
  return null;
293850
294154
  const runDir = resolve16(raw);
293851
- if (!existsSync144(runDir))
294155
+ if (!existsSync145(runDir))
293852
294156
  return null;
293853
294157
  const frame = {
293854
294158
  runDir,
@@ -293905,7 +294209,7 @@ function appendRollTestEvidence(frame, cmd, argv, status2, stdout, stderr) {
293905
294209
  }
293906
294210
  function isolationGetType() {
293907
294211
  const file = join168(process.cwd(), ".roll", "local.yaml");
293908
- if (!existsSync144(file))
294212
+ if (!existsSync145(file))
293909
294213
  return "none";
293910
294214
  let text2;
293911
294215
  try {
@@ -293957,11 +294261,11 @@ function resetLockPath() {
293957
294261
  return ".roll/.iso-reset.lock";
293958
294262
  }
293959
294263
  function resetLockHeld() {
293960
- return existsSync144(resetLockPath());
294264
+ return existsSync145(resetLockPath());
293961
294265
  }
293962
294266
  function resetAcquireLock() {
293963
294267
  const lock = resetLockPath();
293964
- if (existsSync144(lock))
294268
+ if (existsSync145(lock))
293965
294269
  return false;
293966
294270
  mkdirSync82(dirname79(lock), { recursive: true });
293967
294271
  writeFileSync80(lock, `${process.pid}
@@ -293969,7 +294273,7 @@ function resetAcquireLock() {
293969
294273
  return true;
293970
294274
  }
293971
294275
  function resetReleaseLock() {
293972
- rmSync28(resetLockPath(), { force: true });
294276
+ rmSync29(resetLockPath(), { force: true });
293973
294277
  }
293974
294278
  function runForward(cmd, argv) {
293975
294279
  const frame = currentEvidenceFrame();
@@ -293987,7 +294291,7 @@ function runForward(cmd, argv) {
293987
294291
  }
293988
294292
  function readTestScript(cwd) {
293989
294293
  const p = join168(cwd, "package.json");
293990
- if (!existsSync144(p))
294294
+ if (!existsSync145(p))
293991
294295
  return void 0;
293992
294296
  try {
293993
294297
  const pkg = JSON.parse(readFileSync145(p, "utf8"));
@@ -294001,7 +294305,7 @@ function detectVitestVersion(cwd) {
294001
294305
  let dir = cwd;
294002
294306
  for (let i = 0; i < 6; i++) {
294003
294307
  const vp = join168(dir, "node_modules", "vitest", "package.json");
294004
- if (existsSync144(vp)) {
294308
+ if (existsSync145(vp)) {
294005
294309
  try {
294006
294310
  const v = JSON.parse(readFileSync145(vp, "utf8")).version;
294007
294311
  if (typeof v === "string")
@@ -294039,14 +294343,14 @@ function writeTestProof(cwd, mode, command) {
294039
294343
  function ensureSkillsSubmoduleReady() {
294040
294344
  const pkg = rollPkgDir();
294041
294345
  const required2 = join168(pkg, "skills", "roll-onboard", "SKILL.md");
294042
- if (existsSync144(required2))
294346
+ if (existsSync145(required2))
294043
294347
  return true;
294044
- if (existsSync144(join168(pkg, ".git")) && existsSync144(join168(pkg, ".gitmodules"))) {
294348
+ if (existsSync145(join168(pkg, ".git")) && existsSync145(join168(pkg, ".gitmodules"))) {
294045
294349
  spawnSync20("git", ["submodule", "update", "--init", "--recursive", "--quiet", "skills"], {
294046
294350
  cwd: pkg,
294047
294351
  stdio: "ignore"
294048
294352
  });
294049
- if (existsSync144(required2))
294353
+ if (existsSync145(required2))
294050
294354
  return true;
294051
294355
  }
294052
294356
  err14("roll test: skills submodule is empty");
@@ -294055,7 +294359,7 @@ function ensureSkillsSubmoduleReady() {
294055
294359
  }
294056
294360
  function ensureNoneIsolation() {
294057
294361
  const type = isolationGetType();
294058
- if (type === "none" && !existsSync144(join168(process.cwd(), ".roll", "local.yaml"))) {
294362
+ if (type === "none" && !existsSync145(join168(process.cwd(), ".roll", "local.yaml"))) {
294059
294363
  infoErr("isolation: no test_isolation config, falling back to type=none (host)");
294060
294364
  }
294061
294365
  if (!SUPPORTED_TYPES.includes(type)) {
@@ -294170,7 +294474,7 @@ function testCommand(args) {
294170
294474
  }
294171
294475
  const cwd = process.cwd();
294172
294476
  const resolution = resolveGateCommand({
294173
- hasPackageJson: existsSync144(join168(cwd, "package.json")),
294477
+ hasPackageJson: existsSync145(join168(cwd, "package.json")),
294174
294478
  testScript: readTestScript(cwd),
294175
294479
  vitestVersion: detectVitestVersion(cwd)
294176
294480
  });
@@ -294596,7 +294900,7 @@ function emitRepair(json, outcome) {
294596
294900
  // packages/cli/dist/commands/truth.js
294597
294901
  init_dist();
294598
294902
  init_dist2();
294599
- import { statSync as statSync43, readFileSync as readFileSync147, writeFileSync as writeFileSync82, mkdirSync as mkdirSync84, existsSync as existsSync145 } from "node:fs";
294903
+ import { statSync as statSync43, readFileSync as readFileSync147, writeFileSync as writeFileSync82, mkdirSync as mkdirSync84, existsSync as existsSync146 } from "node:fs";
294600
294904
  import { dirname as dirname81, join as join171 } from "node:path";
294601
294905
  var TRUTH_USAGE = "Usage: roll truth <command>\n query <storyId> Deterministic delivery-truth query (structured, zero markdown parse).\n audit Bidirectional drift audit: backlog Done \u2194 projection truth.\n\u547D\u4EE4:\n query <storyId> \u7ED3\u6784\u5316\u4EA4\u4ED8\u771F\u76F8\u67E5\u8BE2\uFF08\u786E\u5B9A\u6027\uFF0C\u4E0D\u89E3\u6790 markdown\uFF09\n audit \u53CC\u5411\u6F02\u79FB\u5BA1\u8BA1\uFF1Abacklog Done \u2194 \u6295\u5F71\u771F\u76F8";
294602
294906
  function formatTruth(t2, lang10) {
@@ -294644,7 +294948,7 @@ function truthAuditCommand(args, lang10) {
294644
294948
  const cwd = process.cwd();
294645
294949
  const nowSec = Math.floor(Date.now() / 1e3);
294646
294950
  const backlogPath = join171(cwd, ".roll", "backlog.md");
294647
- const backlogRows = existsSync145(backlogPath) ? parseBacklog(readFileSync147(backlogPath, "utf8")).map((r) => ({ id: r.id, status: r.status })) : [];
294951
+ const backlogRows = existsSync146(backlogPath) ? parseBacklog(readFileSync147(backlogPath, "utf8")).map((r) => ({ id: r.id, status: r.status })) : [];
294648
294952
  const deliveries = ensureDeliveriesFresh(cwd, nodeFreshnessPort4, nodeExecPort);
294649
294953
  const snapshot = emptyAuditSnapshot(nowSec, TERMINAL_SCHEMA_EPOCH_SEC);
294650
294954
  snapshot.backlog = backlogRows;
@@ -294729,7 +295033,7 @@ function truthCommand(args) {
294729
295033
 
294730
295034
  // packages/cli/dist/commands/tune.js
294731
295035
  init_dist2();
294732
- import { existsSync as existsSync146, readFileSync as readFileSync148 } from "node:fs";
295036
+ import { existsSync as existsSync147, readFileSync as readFileSync148 } from "node:fs";
294733
295037
  import { join as join172 } from "node:path";
294734
295038
  function projectPath2() {
294735
295039
  return (process.env["ROLL_MAIN_PROJECT"] ?? "").trim() || process.cwd();
@@ -294738,7 +295042,7 @@ function str6(v, dflt = "") {
294738
295042
  return typeof v === "string" ? v : dflt;
294739
295043
  }
294740
295044
  function parseJsonl(path) {
294741
- if (!existsSync146(path))
295045
+ if (!existsSync147(path))
294742
295046
  return [];
294743
295047
  let text2;
294744
295048
  try {
@@ -294914,7 +295218,7 @@ function tuneCommand(argv) {
294914
295218
  // packages/cli/dist/commands/update.js
294915
295219
  init_dist();
294916
295220
  import { spawnSync as spawnSync21 } from "node:child_process";
294917
- import { existsSync as existsSync147, mkdtempSync as mkdtempSync10, readFileSync as readFileSync149, rmSync as rmSync29 } from "node:fs";
295221
+ import { existsSync as existsSync148, mkdtempSync as mkdtempSync10, readFileSync as readFileSync149, rmSync as rmSync30 } from "node:fs";
294918
295222
  import { tmpdir as tmpdir14 } from "node:os";
294919
295223
  import { join as join173 } from "node:path";
294920
295224
  function pal7() {
@@ -295004,7 +295308,7 @@ function downloadAndInstallCurl(tag) {
295004
295308
  }
295005
295309
  return { ok: true, newVersion: treeVersion(extractDir) };
295006
295310
  } finally {
295007
- rmSync29(tmpDir, { recursive: true, force: true });
295311
+ rmSync30(tmpDir, { recursive: true, force: true });
295008
295312
  }
295009
295313
  }
295010
295314
  function checkInstalledVersionOrRetry() {
@@ -295024,11 +295328,11 @@ function checkInstalledVersionOrRetry() {
295024
295328
  }
295025
295329
  }
295026
295330
  function invalidateUpdateCache() {
295027
- rmSync29(join173(rollHome(), ".update-check"), { force: true });
295331
+ rmSync30(join173(rollHome(), ".update-check"), { force: true });
295028
295332
  }
295029
295333
  function showChangelog() {
295030
295334
  const changelog = join173(rollPkgDir(), "CHANGELOG.md");
295031
- if (!existsSync147(changelog))
295335
+ if (!existsSync148(changelog))
295032
295336
  return;
295033
295337
  const { BOLD: BOLD2, CYAN, NC } = pal7();
295034
295338
  process.stdout.write(`${BOLD2}${m6("changelog.heading")}:${NC}
@@ -295059,7 +295363,7 @@ async function updateCommand(args) {
295059
295363
  info5(m6("update.current_version", rollVersion()));
295060
295364
  let installMethod = "npm";
295061
295365
  const methodFile = join173(rollPkgDir(), ".install-method");
295062
- if (existsSync147(methodFile)) {
295366
+ if (existsSync148(methodFile)) {
295063
295367
  installMethod = readFileSync149(methodFile, "utf8").trim() || "npm";
295064
295368
  }
295065
295369
  if (installMethod === "curl") {