@seanyao/roll 4.719.3 → 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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v4.720.1 — 2026-07-20
6
+
7
+ ### 稳定性
8
+ - `roll worktree cleanup` 现在能认出压缩合并的交付,未交付分支仍会保留(FIX-1471)[loop]
9
+ - 暂停后可用 `roll loop go --cards` 只跑指定卡,其他启动方式继续保持暂停(FIX-1472)[loop]
10
+
5
11
  ## v4.719.3 — 2026-07-19
6
12
 
7
13
  ### 稳定性
package/dist/roll.mjs CHANGED
@@ -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()) {
@@ -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
  }
@@ -288829,7 +288857,7 @@ import { execFileSync as execFileSync32 } from "node:child_process";
288829
288857
  import { homedir as homedir34 } from "node:os";
288830
288858
  import { appendFileSync as appendFileSync20, existsSync as existsSync134, mkdirSync as mkdirSync73, rmSync as rmSync25 } from "node:fs";
288831
288859
  import { dirname as dirname73, join as join157, relative as relative14, resolve as resolve14, sep as sep8 } from "node:path";
288832
- var MERGED_KINDS = /* @__PURE__ */ new Set(["ancestor", "patch_equivalent"]);
288860
+ var MERGED_KINDS = /* @__PURE__ */ new Set(["ancestor", "patch_equivalent", "final_tree"]);
288833
288861
  function isSafelyDisposable(rec) {
288834
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;
288835
288863
  }
@@ -288906,9 +288934,12 @@ function planWorktreeCleanup(audit, threshold, standaloneMergedBranches = []) {
288906
288934
  preserved
288907
288935
  };
288908
288936
  }
288909
- 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
+ }
288910
288941
  try {
288911
- execFileSync32("git", ["-C", repositoryRoot, "branch", "-D", branch], {
288942
+ execFileSync32("git", ["-C", repositoryRoot, "update-ref", "-d", `refs/heads/${branch}`, expectedSha], {
288912
288943
  encoding: "utf8",
288913
288944
  stdio: ["ignore", "pipe", "pipe"]
288914
288945
  });
@@ -289111,7 +289142,7 @@ async function applyWorktreeCleanup(plan, options) {
289111
289142
  branchesRemoved.push(removalB);
289112
289143
  continue;
289113
289144
  }
289114
- const r = removeBranchFn(repositoryRoot, bc.branch);
289145
+ const r = removeBranchFn(repositoryRoot, bc.branch, bc.expectedSha);
289115
289146
  if (!r.ok) {
289116
289147
  refuseB(`delete-failed: ${r.detail}`);
289117
289148
  continue;
@@ -289257,7 +289288,7 @@ function renderResultHuman(result2) {
289257
289288
  }
289258
289289
  return lines3.join("\n").trimEnd() + "\n";
289259
289290
  }
289260
- 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 --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";
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";
289261
289292
  function resolveThreshold() {
289262
289293
  const parsed = parseInt(process.env["ROLL_BRANCH_CANARY_MAX"] ?? "", 10);
289263
289294
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_BRANCH_CANARY_MAX;
@@ -289265,20 +289296,41 @@ function resolveThreshold() {
289265
289296
  function gitCap(repoRoot3, args) {
289266
289297
  return execFileSync32("git", ["-C", repoRoot3, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
289267
289298
  }
289268
- function classifyBranchMerge(branch, integrationBranch, git5) {
289299
+ function classifyBranchMerge(branch, integrationBranch, git5, prMergeCommit) {
289269
289300
  if (git5(["merge-base", "--is-ancestor", branch, integrationBranch]).ok)
289270
289301
  return "ancestor";
289271
289302
  const cherry = git5(["cherry", integrationBranch, branch]);
289272
- 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)
289273
289313
  return null;
289274
- const lines3 = cherry.stdout.split("\n").map((l) => l.trim()).filter((l) => l !== "");
289275
- if (lines3.length === 0)
289314
+ const merge = rawMerge.trim();
289315
+ if (!isFullGitOid(merge))
289276
289316
  return null;
289277
- if (lines3.some((l) => l.startsWith("+")))
289317
+ if (!git5(["merge-base", "--is-ancestor", merge, integrationBranch]).ok)
289278
289318
  return null;
289279
- if (lines3.every((l) => l.startsWith("-")))
289280
- return "patch_equivalent";
289281
- 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";
289282
289334
  }
289283
289335
  function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
289284
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\//, "")));
@@ -289298,11 +289350,12 @@ function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
289298
289350
  return null;
289299
289351
  }
289300
289352
  },
289301
- // FIX-1458 (#1465): delivery is proven ONLY by fresh git patch evidence via
289302
- // classifyBranchMerge (ancestor OR `git cherry` patch-equivalence). A merged
289303
- // GitHub PR is deliberately NOT consulted: a squash merge leaves the exact
289304
- // branch tips undelivered, so authorizing deletion on a merged PR alone
289305
- // 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).
289306
289359
  branchMerge: (branch) => classifyBranchMerge(branch, integrationBranch, (args) => {
289307
289360
  try {
289308
289361
  return { ok: true, stdout: gitCap(repoRoot3, args) };
@@ -289310,9 +289363,39 @@ function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
289310
289363
  const stdout = typeof err16.stdout === "string" ? err16.stdout : "";
289311
289364
  return { ok: false, stdout };
289312
289365
  }
289313
- })
289366
+ }, (b) => ghMergedPrMergeCommit(repoRoot3, b))
289314
289367
  };
289315
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
+ }
289316
289399
  function resolveIntegrationForCleanup(repoRoot3) {
289317
289400
  try {
289318
289401
  const head = gitCap(repoRoot3, ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]).trim();
@@ -290111,7 +290194,7 @@ async function loopRunOnceCommand(args) {
290111
290194
  removePendingPublish(rt, storyId);
290112
290195
  }
290113
290196
  }
290114
- if (isLoopPaused(id.path, id.slug)) {
290197
+ if (isLoopPaused(id.path, id.slug) && !isGuidedRunOnce(allowedCards)) {
290115
290198
  const lang10 = resolveLang({
290116
290199
  rollLang: process.env["ROLL_LANG"],
290117
290200
  lcAll: process.env["LC_ALL"],
@@ -292823,28 +292906,49 @@ function assertTestProofFresh(exec) {
292823
292906
  throw new Error("code changed since last test run \u2014 run `roll test` on the current staged tree");
292824
292907
  }
292825
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
+ }
292826
292920
  function commitPushWithGate(opts) {
292827
292921
  const { branch, message, rollManaged, exec } = opts;
292828
292922
  const original = exec("git", ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
292923
+ const preparedChanges = hasOnlyPreparedReleaseChanges(exec);
292829
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
+ };
292830
292932
  const checkoutBranch = () => {
292933
+ let localSha = "";
292831
292934
  try {
292832
- const localSha = exec("git", ["rev-parse", "--verify", `refs/heads/${branch}`]).trim();
292833
- if (localSha !== "") {
292834
- exec("git", ["checkout", branch]);
292835
- return;
292836
- }
292935
+ localSha = exec("git", ["rev-parse", "--verify", `refs/heads/${branch}`]).trim();
292837
292936
  } catch {
292838
292937
  }
292938
+ if (localSha !== "") {
292939
+ checkoutExistingBranch();
292940
+ return;
292941
+ }
292942
+ let remote = "";
292839
292943
  try {
292840
- const remote = exec("git", ["ls-remote", "--heads", "origin", branch]).trim();
292841
- if (remote !== "") {
292842
- exec("git", ["fetch", "origin", `${branch}:${branch}`]);
292843
- exec("git", ["checkout", branch]);
292844
- return;
292845
- }
292944
+ remote = exec("git", ["ls-remote", "--heads", "origin", branch]).trim();
292846
292945
  } catch {
292847
292946
  }
292947
+ if (remote !== "") {
292948
+ exec("git", ["fetch", "origin", `${branch}:${branch}`]);
292949
+ checkoutExistingBranch();
292950
+ return;
292951
+ }
292848
292952
  createdLocal = true;
292849
292953
  exec("git", ["checkout", "-b", branch]);
292850
292954
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "4.719.3",
3
+ "version": "4.720.1",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "packageManager": "pnpm@11.1.3",
6
6
  "scripts": {