@seanyao/roll 4.719.1 → 4.719.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ ## v4.719.2 — 2026-07-19
6
+
7
+ ### 稳定性
8
+ - 修复致命回归:`roll worktree cleanup` 会把"历史 PR 被 squash 合并、但分支 tip 上还有未交付独立提交"的 `loop/cycle-*` 分支当成可删(仅凭已合并 PR 授权),导致丢失独立工作;现在删分支必须有 git 层面证据证明每个提交都已交付(是主干祖先,或 `git cherry` 显示每个提交都有等价补丁),只要有一个 `+` 未交付补丁就整条保留;已合并 PR 本身不再作为删除授权 (FIX-1458)
9
+
5
10
  ## v4.719.1 — 2026-07-19
6
11
 
7
12
  ### 稳定性
package/dist/roll.mjs CHANGED
@@ -288769,7 +288769,7 @@ import { execFileSync as execFileSync32 } from "node:child_process";
288769
288769
  import { homedir as homedir34 } from "node:os";
288770
288770
  import { appendFileSync as appendFileSync20, mkdirSync as mkdirSync73 } from "node:fs";
288771
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", "pr_merged", "patch_equivalent"]);
288772
+ var MERGED_KINDS = /* @__PURE__ */ new Set(["ancestor", "patch_equivalent"]);
288773
288773
  function isSafelyDisposable(rec) {
288774
288774
  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
288775
  }
@@ -289112,6 +289112,21 @@ function resolveThreshold() {
289112
289112
  function gitCap(repoRoot3, args) {
289113
289113
  return execFileSync32("git", ["-C", repoRoot3, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
289114
289114
  }
289115
+ function classifyBranchMerge(branch, integrationBranch, git5) {
289116
+ if (git5(["merge-base", "--is-ancestor", branch, integrationBranch]).ok)
289117
+ return "ancestor";
289118
+ const cherry = git5(["cherry", integrationBranch, branch]);
289119
+ if (!cherry.ok)
289120
+ return null;
289121
+ const lines3 = cherry.stdout.split("\n").map((l) => l.trim()).filter((l) => l !== "");
289122
+ if (lines3.length === 0)
289123
+ return null;
289124
+ if (lines3.some((l) => l.startsWith("+")))
289125
+ return null;
289126
+ if (lines3.every((l) => l.startsWith("-")))
289127
+ return "patch_equivalent";
289128
+ return null;
289129
+ }
289115
289130
  function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
289116
289131
  const attachedBranches = new Set(audit.records.filter((r) => r.owner === "loop" && typeof r.branch === "string" && r.branch !== "").map((r) => r.branch.replace(/^refs\/heads\//, "")));
289117
289132
  let currentBranch2 = null;
@@ -289130,27 +289145,19 @@ function buildStandaloneBranchDeps(repoRoot3, audit, integrationBranch) {
289130
289145
  return null;
289131
289146
  }
289132
289147
  },
289133
- branchMerge: (branch) => {
289134
- try {
289135
- execFileSync32("git", ["-C", repoRoot3, "merge-base", "--is-ancestor", branch, integrationBranch], { stdio: "ignore" });
289136
- return "ancestor";
289137
- } catch {
289138
- }
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).
289153
+ branchMerge: (branch) => classifyBranchMerge(branch, integrationBranch, (args) => {
289139
289154
  try {
289140
- for (const line of gitCap(repoRoot3, ["branch", "--merged", integrationBranch]).split("\n")) {
289141
- if (line.replace(/^[*+]?\s+/, "").trim() === branch)
289142
- return "pr_merged";
289143
- }
289144
- } catch {
289145
- }
289146
- try {
289147
- const n = execFileSync32("gh", ["pr", "list", "--head", branch, "--state", "merged", "--json", "number", "--jq", "length"], { cwd: repoRoot3, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
289148
- if (parseInt(n, 10) > 0)
289149
- return "pr_merged";
289150
- } catch {
289155
+ return { ok: true, stdout: gitCap(repoRoot3, args) };
289156
+ } catch (err16) {
289157
+ const stdout = typeof err16.stdout === "string" ? err16.stdout : "";
289158
+ return { ok: false, stdout };
289151
289159
  }
289152
- return null;
289153
- }
289160
+ })
289154
289161
  };
289155
289162
  }
289156
289163
  function resolveIntegrationForCleanup(repoRoot3) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seanyao/roll",
3
- "version": "4.719.1",
3
+ "version": "4.719.2",
4
4
  "description": "Roll — Roll out features with AI agents",
5
5
  "packageManager": "pnpm@11.1.3",
6
6
  "scripts": {