@humanbased/crosscheck 1.3.0-beta.93 → 1.3.0-beta.95

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.
@@ -0,0 +1,88 @@
1
+ // The separate-fix-PR path is the fallback taken when a fix cannot be pushed onto the
2
+ // PR's own branch. Two things about it were broken in practice, and both are visible in
3
+ // real logs: 43 fix PRs opened, 19 `base: invalid` API failures, 12 push rejections.
4
+ //
5
+ // 1. It opened the PR against the source PR's head branch — which is missing in exactly
6
+ // the case that sends us down this path (PR merged, branch auto-deleted). The
7
+ // fallback failed for its own primary trigger, after having already pushed a branch,
8
+ // leaving that branch orphaned.
9
+ // 2. It pushed with a plain `git push`, so a fix branch left behind by an earlier round
10
+ // of the same PR made the push non-fast-forward and the fix was lost.
11
+ import { autoFixBranchName, isCrosscheckAutoFixPR } from '../github/superseded-fix-pr.js';
12
+ export function planAutoFixDelivery(baseBranchExists, baseBranch) {
13
+ return baseBranchExists
14
+ ? { kind: 'pull_request', base: baseBranch }
15
+ : { kind: 'comment', reason: 'base_branch_gone' };
16
+ }
17
+ // `--force-with-lease=<ref>:<oid>` states the remote value we believe we are replacing.
18
+ // With an empty oid it means "expect this ref not to exist", so one form covers both
19
+ // creating the branch and replacing a superseded one — and a branch someone else moved
20
+ // in between fails the lease instead of being overwritten.
21
+ //
22
+ // Force is appropriate here only because the branch is crosscheck's own artifact for
23
+ // one PR and one finding: the newer fix is built on the newer PR head and supersedes
24
+ // whatever the previous round pushed. It is never used on a branch a human owns.
25
+ export function forceWithLeaseArgs(branch, remoteOid) {
26
+ return [
27
+ 'push',
28
+ `--force-with-lease=refs/heads/${branch}:${remoteOid ?? ''}`,
29
+ 'origin',
30
+ `HEAD:${branch}`,
31
+ ];
32
+ }
33
+ // `git ls-remote --heads origin <branch>` prints "<oid>\t<ref>" per match, and nothing
34
+ // at all when the branch does not exist.
35
+ export function parseLsRemoteOid(output) {
36
+ const line = output.split('\n').map(l => l.trim()).find(Boolean);
37
+ if (!line)
38
+ return null;
39
+ const oid = line.split(/\s+/)[0];
40
+ return /^[0-9a-f]{40}$/i.test(oid) ? oid : null;
41
+ }
42
+ export function isLeaseRejection(message) {
43
+ return /stale info|force-with-lease|fetch first|non-fast-forward/i.test(message);
44
+ }
45
+ // Whether the fix branch that already exists on the remote is crosscheck's own artifact,
46
+ // and so safe to replace.
47
+ //
48
+ // Anything the pusher controls is not evidence: a commit message trailer, an author or
49
+ // committer name, and the branch name itself are all free-form strings that whoever
50
+ // creates `fix/cr-<n>-review-issues` can set to whatever they like — so a human branch
51
+ // carrying a `Crosscheck-Reviewer:` trailer would read as crosscheck's and be
52
+ // force-overwritten. Ownership is instead taken from GitHub's own record: a pull request
53
+ // **opened by the identity crosscheck authenticates as**, from this exact branch, whose
54
+ // body carries the auto-fix marker. Another actor cannot produce that record without
55
+ // crosscheck's credentials.
56
+ //
57
+ // The residual case is an install whose token belongs to a human who also works by hand:
58
+ // there, "crosscheck" and "that person" are one account and no check can separate them.
59
+ //
60
+ // Closed PRs count — the branch is still crosscheck's artifact once it has opened a PR
61
+ // from it. A branch with no such PR (an orphan left by a round that pushed before PR
62
+ // creation failed) is deliberately not replaced: the fix is delivered as a diff instead,
63
+ // which loses the follow-up PR but never someone else's commits.
64
+ export function assessFixBranchOwnership(input) {
65
+ if (!input.crosscheckLogin)
66
+ return { owned: false, reason: 'identity_unknown' };
67
+ const login = input.crosscheckLogin.toLowerCase();
68
+ const fixBranch = autoFixBranchName(input.sourcePrNumber);
69
+ const ours = input.candidates.find(candidate => candidate.head?.ref === fixBranch
70
+ && candidate.user?.login?.toLowerCase() === login
71
+ && isCrosscheckAutoFixPR(candidate.body, input.sourcePrNumber));
72
+ return ours ? { owned: true, fixPrNumber: ours.number } : { owned: false, reason: 'no_crosscheck_fix_pr' };
73
+ }
74
+ // `pulls.create` rejecting the base branch, which is how a source PR that merges between
75
+ // the pre-flight check and the create call reports itself: GitHub deleted the head branch
76
+ // in that window, so the base we validated is gone by the time we ask for the PR.
77
+ export function isInvalidBaseError(err) {
78
+ const failure = err;
79
+ if (failure?.status !== 422)
80
+ return false;
81
+ const errors = failure.response?.data?.errors ?? failure.errors ?? [];
82
+ if (errors.some(entry => entry?.field === 'base' && entry?.code === 'invalid'))
83
+ return true;
84
+ // Octokit's RequestError embeds the response body in its message; a shape we don't
85
+ // recognise field-by-field is still readable there.
86
+ return /"field"\s*:\s*"base"/.test(failure.message ?? '');
87
+ }
88
+ //# sourceMappingURL=auto-fix-branch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auto-fix-branch.js","sourceRoot":"","sources":["../../src/lib/auto-fix-branch.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,wFAAwF;AACxF,qFAAqF;AACrF,EAAE;AACF,wFAAwF;AACxF,iFAAiF;AACjF,wFAAwF;AACxF,mCAAmC;AACnC,wFAAwF;AACxF,yEAAyE;AAEzE,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,gCAAgC,CAAA;AAUzF,MAAM,UAAU,mBAAmB,CAAC,gBAAyB,EAAE,UAAkB;IAC/E,OAAO,gBAAgB;QACrB,CAAC,CAAC,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,UAAU,EAAE;QAC5C,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAA;AACrD,CAAC;AAED,wFAAwF;AACxF,qFAAqF;AACrF,uFAAuF;AACvF,2DAA2D;AAC3D,EAAE;AACF,qFAAqF;AACrF,qFAAqF;AACrF,iFAAiF;AACjF,MAAM,UAAU,kBAAkB,CAAC,MAAc,EAAE,SAAwB;IACzE,OAAO;QACL,MAAM;QACN,iCAAiC,MAAM,IAAI,SAAS,IAAI,EAAE,EAAE;QAC5D,QAAQ;QACR,QAAQ,MAAM,EAAE;KACjB,CAAA;AACH,CAAC;AAED,uFAAuF;AACvF,yCAAyC;AACzC,MAAM,UAAU,gBAAgB,CAAC,MAAc;IAC7C,MAAM,IAAI,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAChE,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAA;IACtB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAA;IAChC,OAAO,iBAAiB,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAA;AACjD,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,OAAe;IAC9C,OAAO,2DAA2D,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;AAClF,CAAC;AAeD,yFAAyF;AACzF,0BAA0B;AAC1B,EAAE;AACF,uFAAuF;AACvF,oFAAoF;AACpF,uFAAuF;AACvF,8EAA8E;AAC9E,yFAAyF;AACzF,wFAAwF;AACxF,qFAAqF;AACrF,4BAA4B;AAC5B,EAAE;AACF,yFAAyF;AACzF,wFAAwF;AACxF,EAAE;AACF,uFAAuF;AACvF,qFAAqF;AACrF,yFAAyF;AACzF,iEAAiE;AACjE,MAAM,UAAU,wBAAwB,CAAC,KAIxC;IACC,IAAI,CAAC,KAAK,CAAC,eAAe;QAAE,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,CAAA;IAC/E,MAAM,KAAK,GAAG,KAAK,CAAC,eAAe,CAAC,WAAW,EAAE,CAAA;IACjD,MAAM,SAAS,GAAG,iBAAiB,CAAC,KAAK,CAAC,cAAc,CAAC,CAAA;IACzD,MAAM,IAAI,GAAG,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,CAC7C,SAAS,CAAC,IAAI,EAAE,GAAG,KAAK,SAAS;WAC9B,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,KAAK;WAC9C,qBAAqB,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC,CAAA;IACjE,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,sBAAsB,EAAE,CAAA;AAC5G,CAAC;AAED,yFAAyF;AACzF,0FAA0F;AAC1F,kFAAkF;AAClF,MAAM,UAAU,kBAAkB,CAAC,GAAY;IAC7C,MAAM,OAAO,GAAG,GAKf,CAAA;IACD,IAAI,OAAO,EAAE,MAAM,KAAK,GAAG;QAAE,OAAO,KAAK,CAAA;IACzC,MAAM,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,IAAI,EAAE,MAAM,IAAI,OAAO,CAAC,MAAM,IAAI,EAAE,CAAA;IACrE,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,MAAM,IAAI,KAAK,EAAE,IAAI,KAAK,SAAS,CAAC;QAAE,OAAO,IAAI,CAAA;IAC3F,mFAAmF;IACnF,oDAAoD;IACpD,OAAO,sBAAsB,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,IAAI,EAAE,CAAC,CAAA;AAC3D,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAoB,MAAM,qBAAqB,CAAA;AAEnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAA;AAQjE,OAAO,EAAiC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAQ9F,OAAO,EAAuE,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAKjJ,OAAO,EAAuE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACzH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAsB9C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGzD;AAaD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,MAAM,CAEpF;AAWD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAqB1G;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnF;AAOD,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,CAAA;AAE7F,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACnC,cAAc,EAAE,OAAO,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAOD,MAAM,MAAM,mBAAmB,GAC3B,WAAW,GACX,OAAO,GACP,eAAe,GACf,gBAAgB,GAChB,cAAc,CAAA;AAElB,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,sBAAsB,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsCzB;AAKD,wBAAgB,gBAAgB,CAC9B,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,EACxB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAOT;AAKD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,OAAO,CAE1E;AAED,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAA;AAanE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,yBAAyB,GAAG,SAAS,CAAA;AACzE,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,eAAe,GAAG,UAAU,CAM3E;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,QAAQ,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1B,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,IAAI,CAAA;IAE1D,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAG3B,YAAY,CAAC,EAAE,OAAO,CAAA;IAEtB,KAAK,CAAC,EAAE,MAAM,CAAA;IAGd,MAAM,CAAC,EAAE,OAAO,CAAA;IAGhB,UAAU,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAGtC,KAAK,CAAC,EAAE,OAAO,eAAe,EAAE,YAAY,EAAE,CAAA;IAG9C,mBAAmB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;IAMxC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IAIrB,oBAAoB,CAAC,EAAE;QACrB,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IAID,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAG1B,SAAS,CAAC,EAAE,OAAO,GAAG,WAAW,CAAA;IAGjC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B,OAAO,CAAC,EAAE,eAAe,CAAA;IAIzB,YAAY,CAAC,EAAE,MAAM,CAAA;IAIrB,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAChH;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB;uEACmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAA;IAGxB,eAAe,CAAC,EAAE,MAAM,CAAA;IAIxB,aAAa,CAAC,EAAE,MAAM,CAAA;IAItB,mBAAmB,CAAC,EAAE;QACpB,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IACD;;;uDAGmD;IACnD,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B;wEACoE;IACpE,GAAG,EAAE,MAAM,EAAE,CAAA;IACb;kCAC8B;IAC9B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5C;AAKD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAClC,YAAY,CAQd;AAWD,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,YAAY,GAAG,SAAS,EAC9B,IAAI,EAAE,YAAY,GAAG,SAAS,GAC7B,YAAY,GAAG,SAAS,CAY1B;AAkDD,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1F;AA8DD,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAC5B;IAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAC;IAAC,uBAAuB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;CAAE,CAEjH;AAKD,wBAAgB,4BAA4B,CAC1C,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAC5B;IAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAC;IAAC,uBAAuB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;CAAE,CAEjH;AAgCD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,SAAS,GAAG,IAAI,CAsCrE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,EAC7C,QAAQ,EAAE,gBAAgB,GAAG,IAAI,GAChC,eAAe,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAGtC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EAC1D,MAAM,EAAE,CAAC,EACT,QAAQ,EAAE,gBAAgB,GAAG,IAAI,EACjC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,CAAC,CAKH;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACjC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,EACjC,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAIT;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,CAKlF;AAED,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAA;IACzC,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAA;IACvC,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,gBAAgB,GAAG,IAAI,EACjC,KAAK,EAAE,MAAM,GACZ,cAAc,CA4ChB;AAuHD,wBAAsB,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA2qC/E"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/lib/runner.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,MAAM,EAAoB,MAAM,qBAAqB,CAAA;AAEnE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAA;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,kBAAkB,CAAA;AASjE,OAAO,EAAiC,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAA;AAQ9F,OAAO,EAAuE,KAAK,SAAS,EAAE,KAAK,gBAAgB,EAAE,MAAM,sBAAsB,CAAA;AAOjJ,OAAO,EAAuE,KAAK,UAAU,EAAE,MAAM,oBAAoB,CAAA;AACzH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAA;AAsB9C,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAGzD;AAaD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,GAAG,MAAM,CAEpF;AAWD,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,MAAM,CAAA;IACb;;;;OAIG;IACH,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,wBAAgB,mCAAmC,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAqB1G;AAED,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAEnF;AAOD,MAAM,MAAM,eAAe,GAAG,KAAK,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,SAAS,CAAA;AAE7F,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,UAAU,EAAE,MAAM,CAAA;IAClB,aAAa,EAAE,MAAM,CAAA;IACrB,QAAQ,EAAE,MAAM,EAAE,CAAA;IAClB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;IACnC,cAAc,EAAE,OAAO,CAAA;IACvB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,OAAO,CAAC,EAAE,eAAe,CAAA;IACzB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,CAAC,EAAE,OAAO,CAAA;IACvB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,GAAG,CAAC,EAAE,MAAM,CAAA;CACb;AAOD,MAAM,MAAM,mBAAmB,GAC3B,WAAW,GACX,OAAO,GACP,eAAe,GACf,gBAAgB,GAChB,cAAc,CAAA;AAElB,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,sBAAsB,GAC7B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAsCzB;AAKD,wBAAgB,gBAAgB,CAC9B,aAAa,EAAE,MAAM,EACrB,gBAAgB,EAAE,MAAM,EACxB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,MAAM,GAAG,SAAS,GACxB,OAAO,CAOT;AAKD,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAAG,OAAO,CAE1E;AAED,MAAM,MAAM,eAAe,GAAG,cAAc,GAAG,QAAQ,GAAG,SAAS,CAAA;AAanE,MAAM,MAAM,UAAU,GAAG,QAAQ,GAAG,yBAAyB,GAAG,SAAS,CAAA;AACzE,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,eAAe,GAAG,UAAU,CAM3E;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IAC9B,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,MAAM,CAAA;IAChB,QAAQ,EAAE,MAAM,CAAA;IAChB,EAAE,EAAE,OAAO,CAAC,cAAc,CAAC,CAAA;IAC3B,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,MAAM,CAAA;IACb,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,QAAQ,CAAA;IAChB,WAAW,EAAE,MAAM,CAAA;IACnB,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAA;IAC1B,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,IAAI,CAAA;IAE1D,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;IAG3B,YAAY,CAAC,EAAE,OAAO,CAAA;IAEtB,KAAK,CAAC,EAAE,MAAM,CAAA;IAGd,MAAM,CAAC,EAAE,OAAO,CAAA;IAGhB,UAAU,CAAC,EAAE,kBAAkB,GAAG,IAAI,CAAA;IAGtC,KAAK,CAAC,EAAE,OAAO,eAAe,EAAE,YAAY,EAAE,CAAA;IAG9C,mBAAmB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;IAMxC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;IAIrB,oBAAoB,CAAC,EAAE;QACrB,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IAID,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAG1B,SAAS,CAAC,EAAE,OAAO,GAAG,WAAW,CAAA;IAGjC,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAE1B,OAAO,CAAC,EAAE,eAAe,CAAA;IAIzB,YAAY,CAAC,EAAE,MAAM,CAAA;IAIrB,aAAa,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAChH;AAED,MAAM,WAAW,cAAc;IAC7B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB;uEACmE;IACnE,eAAe,CAAC,EAAE,MAAM,CAAA;IAGxB,eAAe,CAAC,EAAE,MAAM,CAAA;IAIxB,aAAa,CAAC,EAAE,MAAM,CAAA;IAItB,mBAAmB,CAAC,EAAE;QACpB,EAAE,CAAC,EAAE,MAAM,CAAA;QACX,IAAI,EAAE,MAAM,CAAA;KACb,CAAA;IACD;;;uDAGmD;IACnD,YAAY,CAAC,EAAE,YAAY,CAAA;CAC5B;AAED,MAAM,WAAW,YAAY;IAC3B;wEACoE;IACpE,GAAG,EAAE,MAAM,EAAE,CAAA;IACb;kCAC8B;IAC9B,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAC5C;AAKD,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,GAClC,YAAY,CAQd;AAWD,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,YAAY,GAAG,SAAS,EAC9B,IAAI,EAAE,YAAY,GAAG,SAAS,GAC7B,YAAY,GAAG,SAAS,CAY1B;AAkDD,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7E;AAED,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,wBAAgB,4BAA4B,CAAC,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAE1F;AA8DD,wBAAgB,gBAAgB,CAC9B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAC5B;IAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAC;IAAC,uBAAuB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;CAAE,CAEjH;AAKD,wBAAgB,4BAA4B,CAC1C,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,QAAQ,EAChB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,GAC5B;IAAE,MAAM,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAAC;IAAC,iBAAiB,EAAE,OAAO,CAAC;IAAC,uBAAuB,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAA;CAAE,CAEjH;AAgCD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,SAAS,GAAG,IAAI,CAsCrE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,OAAO,EAAE,eAAe,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,EAC7C,QAAQ,EAAE,gBAAgB,GAAG,IAAI,GAChC,eAAe,CAAC,QAAQ,CAAC,CAAC,SAAS,CAAC,CAGtC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,EAC1D,MAAM,EAAE,CAAC,EACT,QAAQ,EAAE,gBAAgB,GAAG,IAAI,EACjC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,CAAC,CAKH;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,EACjC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,EACjC,aAAa,CAAC,EAAE,MAAM,GACrB,OAAO,CAIT;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,eAAe,GAAG,gBAAgB,GAAG,IAAI,CAKlF;AAED,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CAAA;IACjC,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,CAAA;IAC1B,YAAY,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,QAAQ,CAAC,CAAA;IACzC,WAAW,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,CAAA;IACvC,iFAAiF;IACjF,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,OAAO,CAAA;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,gBAAgB,GAAG,IAAI,EACjC,KAAK,EAAE,MAAM,GACZ,cAAc,CA4ChB;AAuHD,wBAAsB,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAu2C/E"}
@@ -11,6 +11,7 @@ import { runFixStep, runCodexFixStep } from '../reviewers/fix.js';
11
11
  import { runConflictResolveStep, findConflictedFiles } from '../reviewers/conflict-resolve.js';
12
12
  import { parseVerdict, prependVerdictToComment, NULL_VERDICT_WARNING, applySeverityGate, SEVERITY_GATE_NOTE } from '../lib/verdict.js';
13
13
  import { createGithubClient, postReviewComment, getLastCrossCheckCommentId, getLastCrossCheckReviewComment } from '../github/client.js';
14
+ import { autoFixBranchName, autoFixPRIntro, sourcePRHasMerged } from '../github/superseded-fix-pr.js';
14
15
  import { verifyReviewedSha, isVerifiedReviewedSha, reviewedShaRejection } from '../github/reviewed-sha.js';
15
16
  import { resolveLinearAuth, withWorker } from '../linear/identity.js';
16
17
  import { notifyLinear } from '../linear/notify.js';
@@ -23,6 +24,7 @@ import { resolveClaudeModel, resolveCodexModel } from '../lib/review-models.js';
23
24
  import { resolveReviewStrategy, escalate, clampToLevels } from './review-strategy.js';
24
25
  import { CLAUDE_EFFORT_LEVELS, CODEX_EFFORT_LEVELS } from '../config/schema.js';
25
26
  import { buildStepIdentityFields } from '../lib/event-fields.js';
27
+ import { planAutoFixDelivery, forceWithLeaseArgs, parseLsRemoteOid, isLeaseRejection, assessFixBranchOwnership, isInvalidBaseError } from '../lib/auto-fix-branch.js';
26
28
  import { prOpenToVerdictMs } from '../lib/adoption.js';
27
29
  import { buildAttributionFooter, buildFixAppliedCommentBody, buildFixFailedCommentBody, buildConflictResolvedCommentBody, buildRetriedReviewBanner } from '../lib/comment-bodies.js';
28
30
  import { linearWritePossible, loadWorkflow, loadHarnessSection, evaluateWhen } from '../lib/workflow.js';
@@ -1413,39 +1415,230 @@ export async function runWorkflow(ctx) {
1413
1415
  results[step.name] = { applied_count: appliedCount, tokens_used: fixTokensUsed, vendor: activeVendor };
1414
1416
  }
1415
1417
  else {
1416
- // Fallback: the fix could not land on the PR branch. Push the same commit to
1417
- // a dedicated branch and open a follow-up PR targeting the original branch.
1418
- const fixBranch = `fix/cr-${prNumber}-review-issues`;
1419
- execSync(`git push origin HEAD:${fixBranch}`, {
1420
- cwd: tmpDir,
1421
- env: { ...process.env, GITHUB_TOKEN: token, GH_TOKEN: token },
1422
- });
1418
+ // Fallback: the fix could not land on the PR branch. Push the same commit to a
1419
+ // dedicated branch and open a follow-up PR targeting the original branch.
1420
+ const fixBranch = autoFixBranchName(prNumber);
1421
+ const gitEnv = { ...process.env, GITHUB_TOKEN: token, GH_TOKEN: token };
1423
1422
  const octokit = createGithubClient(token);
1423
+ // The fallback commit exists only in this clone until something pushes it, so
1424
+ // every path that ends without delivering it as a PR has to put HEAD back on the
1425
+ // PR head. Otherwise a later recheck reviews that commit and stamps its verdict
1426
+ // either with a sha the repository cannot resolve — which verifyReviewedSha
1427
+ // refuses to post (#290) — or with one no open PR carries.
1428
+ const restorePRHeadInClone = () => {
1429
+ execSync('git reset --hard HEAD~1', { cwd: tmpDir, stdio: 'pipe' });
1430
+ };
1431
+ // Deliver the fix as a diff on the original PR. Used wherever opening the
1432
+ // follow-up PR would either create a stale artifact or overwrite a branch that
1433
+ // is not ours: the work still reaches a human, just not as a branch.
1434
+ const deliverFixAsComment = async (outcome) => {
1435
+ fileLog({ level: 'warn', event: outcome.event, repo: `${owner}/${repoName}`, pr: prNumber, branch: outcome.branch, reason: outcome.reason });
1436
+ log(chalk.yellow(`⚠ ${outcome.notice}`));
1437
+ let patch = '';
1438
+ try {
1439
+ patch = execSync('git diff HEAD~1', { cwd: tmpDir, encoding: 'utf8' });
1440
+ }
1441
+ catch { /* fall through to the empty check */ }
1442
+ if (patch) {
1443
+ await octokit.rest.issues.createComment({
1444
+ owner, repo: repoName, issue_number: prNumber,
1445
+ body: [
1446
+ '### Suggested fixes (crosscheck auto-fix)',
1447
+ '',
1448
+ outcome.explanation,
1449
+ '',
1450
+ '```diff',
1451
+ patch.slice(0, 16000),
1452
+ '```',
1453
+ '',
1454
+ fixAttributionFooter(),
1455
+ ].join('\n'),
1456
+ });
1457
+ }
1458
+ restorePRHeadInClone();
1459
+ onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: fixTokensUsed });
1460
+ fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, sha: newSha, delivery: 'comment', tokens_used: fixTokensUsed, skills_activated: activatedSkills.map(skill => skill.name), duration_ms: Date.now() - fixStepStart, ...triggerField });
1461
+ results[step.name] = { applied_count: appliedCount, tokens_used: fixTokensUsed, vendor: activeVendor };
1462
+ };
1463
+ // Ask whether the intended base still exists BEFORE pushing anything. The most
1464
+ // common reason the PR-branch push failed is that the PR merged and GitHub
1465
+ // deleted its branch — and `pulls.create` rejects a missing base with
1466
+ // `field: base, code: invalid`. That used to throw after the branch was already
1467
+ // pushed, losing the fix and orphaning the branch (19 times in logged runs).
1468
+ let baseBranchExists = true;
1469
+ try {
1470
+ await octokit.rest.repos.getBranch({ owner, repo: repoName, branch: pr.head.ref });
1471
+ }
1472
+ catch (err) {
1473
+ if (err?.status === 404)
1474
+ baseBranchExists = false;
1475
+ else
1476
+ throw err;
1477
+ }
1478
+ const delivery = planAutoFixDelivery(baseBranchExists, pr.head.ref);
1479
+ if (delivery.kind === 'comment') {
1480
+ // A PR cut from this snapshot would be stale the moment it opened — it is
1481
+ // exactly the artifact the superseded-auto-fix cleanup exists to close. Post
1482
+ // the fix as a diff instead, so the work is still delivered and reviewable.
1483
+ await deliverFixAsComment({
1484
+ event: 'fix_pr_skipped',
1485
+ reason: delivery.reason,
1486
+ branch: pr.head.ref,
1487
+ notice: `${pr.head.ref} no longer exists — posting the fix as a diff instead of opening a stale follow-up PR`,
1488
+ explanation: `\`${pr.head.ref}\` no longer exists, so this PR has merged and there is no branch to land on. The fix is below rather than in a follow-up PR — a PR cut from the pre-merge state would reintroduce whatever the merged version changed.`,
1489
+ });
1490
+ continue;
1491
+ }
1492
+ // A fix branch left behind by an earlier round of this PR made a plain push
1493
+ // non-fast-forward and lost the fix (12 times in logged runs). Replace it under
1494
+ // a lease: this round's fix is built on the newer PR head and supersedes it,
1495
+ // and the lease still refuses if anything other than us moved the branch.
1496
+ let remoteOid = null;
1497
+ try {
1498
+ remoteOid = parseLsRemoteOid(execFileSync('git', ['ls-remote', '--heads', 'origin', fixBranch], { cwd: tmpDir, env: gitEnv, encoding: 'utf8' }));
1499
+ }
1500
+ catch { /* treat an unreadable remote as "branch absent"; the lease catches a wrong guess */ }
1501
+ // `--force-with-lease` only protects against changes made *after* this lookup —
1502
+ // it happily accepts a branch a human just created or moved as the lease value
1503
+ // and overwrites it. So establish that the branch is crosscheck's own artifact
1504
+ // before replacing it, from GitHub's record of who opened the PR from it rather
1505
+ // than from anything the pusher can write into a commit.
1506
+ if (remoteOid !== null) {
1507
+ let crosscheckLogin = null;
1508
+ let candidates = [];
1509
+ try {
1510
+ const [{ data: viewer }, { data: branchPrs }] = await Promise.all([
1511
+ octokit.rest.users.getAuthenticated(),
1512
+ octokit.rest.pulls.list({ owner, repo: repoName, state: 'all', head: `${owner}:${fixBranch}` }),
1513
+ ]);
1514
+ crosscheckLogin = viewer.login;
1515
+ candidates = branchPrs;
1516
+ }
1517
+ catch { /* unreadable identity or PR list — treated as not ours, never overwrite */ }
1518
+ const ownership = assessFixBranchOwnership({ sourcePrNumber: prNumber, crosscheckLogin, candidates });
1519
+ if (!ownership.owned) {
1520
+ await deliverFixAsComment({
1521
+ event: 'fix_branch_push_skipped',
1522
+ reason: ownership.reason,
1523
+ branch: fixBranch,
1524
+ notice: `${fixBranch} exists but crosscheck has no PR of its own from it — leaving it in place and posting the fix as a diff instead`,
1525
+ explanation: `\`${fixBranch}\` already exists and crosscheck cannot show it opened a pull request from it, so the branch was left untouched rather than force-replaced. The fix is posted here instead.`,
1526
+ });
1527
+ continue;
1528
+ }
1529
+ }
1530
+ // From #296: the merged-PR handler sweeps for auto-fix PRs once, so a PR opened
1531
+ // after that sweep is never seen by it and stays open and mergeable against a
1532
+ // tree that no longer exists. The base-exists check above already covers a merge
1533
+ // that deleted the branch; this catches a merge in a repo that keeps its
1534
+ // branches. It sits immediately before the push — every lookup above widens the
1535
+ // window it has to close, and nothing has been written to the remote yet, so a
1536
+ // skip here cannot orphan a branch.
1537
+ if (await sourcePRHasMerged(octokit, owner, repoName, prNumber)) {
1538
+ restorePRHeadInClone();
1539
+ skipFix('source_pr_merged');
1540
+ continue;
1541
+ }
1542
+ try {
1543
+ execFileSync('git', forceWithLeaseArgs(fixBranch, remoteOid), { cwd: tmpDir, env: gitEnv, stdio: 'pipe' });
1544
+ }
1545
+ catch (pushErr) {
1546
+ const pushMsg = pushErr instanceof Error ? pushErr.message : String(pushErr);
1547
+ fileLog({
1548
+ level: 'error', event: 'fix_branch_push_failed', repo: `${owner}/${repoName}`, pr: prNumber,
1549
+ branch: fixBranch, lease: remoteOid ?? 'absent',
1550
+ reason: isLeaseRejection(pushMsg) ? 'lease_rejected' : 'push_failed',
1551
+ error: pushMsg.slice(0, 500),
1552
+ });
1553
+ throw pushErr;
1554
+ }
1555
+ if (remoteOid !== null) {
1556
+ fileLog({ level: 'info', event: 'fix_branch_replaced', repo: `${owner}/${repoName}`, pr: prNumber, branch: fixBranch, previous_sha: remoteOid, sha: newSha });
1557
+ }
1558
+ // Undo this run's push when the follow-up PR turns out not to be creatable after
1559
+ // all. Only a branch this run brought into existence is removed — one that was
1560
+ // already crosscheck's is left at the newer commit rather than deleted out from
1561
+ // under whatever else references it.
1562
+ const rollBackPushedFixBranch = () => {
1563
+ if (remoteOid !== null)
1564
+ return;
1565
+ try {
1566
+ execFileSync('git', ['push', 'origin', '--delete', fixBranch], { cwd: tmpDir, env: gitEnv, stdio: 'pipe' });
1567
+ fileLog({ level: 'info', event: 'fix_branch_rolled_back', repo: `${owner}/${repoName}`, pr: prNumber, branch: fixBranch, sha: newSha });
1568
+ }
1569
+ catch (deleteErr) {
1570
+ fileLog({ level: 'warn', event: 'fix_branch_rollback_failed', repo: `${owner}/${repoName}`, pr: prNumber, branch: fixBranch, error: deleteErr instanceof Error ? deleteErr.message.slice(0, 500) : String(deleteErr) });
1571
+ }
1572
+ };
1573
+ // The source PR can still merge in the window between the check above and this
1574
+ // point. Asking again here is what keeps the push from becoming a stale fix PR:
1575
+ // the branch is already on the remote, so the answer decides whether it stays.
1576
+ if (await sourcePRHasMerged(octokit, owner, repoName, prNumber)) {
1577
+ rollBackPushedFixBranch();
1578
+ restorePRHeadInClone();
1579
+ skipFix('source_pr_merged');
1580
+ continue;
1581
+ }
1424
1582
  const fixPrTitle = config.post_review.auto_fix.delivery.pr_title.replace('#{original_pr_title}', pr.title);
1425
- const { data: fixPr } = await octokit.rest.pulls.create({
1426
- owner,
1427
- repo: repoName,
1428
- head: fixBranch,
1429
- base: pr.head.ref,
1430
- title: fixPrTitle,
1431
- body: [
1432
- `Auto-fix by crosscheck for CR issues found in #${prNumber}.`,
1433
- '',
1434
- `Review: https://github.com/${owner}/${repoName}/pull/${prNumber}`,
1435
- '',
1436
- fixAttributionFooter(),
1437
- ].join('\n'),
1583
+ // An earlier round may already have an open PR from this branch; the force-push
1584
+ // above updated it, so opening a second one would be rejected as a duplicate.
1585
+ const { data: existingFixPrs } = await octokit.rest.pulls.list({
1586
+ owner, repo: repoName, state: 'open', head: `${owner}:${fixBranch}`,
1438
1587
  });
1588
+ let fixPrNumber;
1589
+ if (existingFixPrs.length > 0) {
1590
+ fixPrNumber = existingFixPrs[0].number;
1591
+ fileLog({ level: 'info', event: 'fix_pr_updated', repo: `${owner}/${repoName}`, pr: prNumber, fix_pr: fixPrNumber, sha: newSha });
1592
+ }
1593
+ else {
1594
+ let fixPr;
1595
+ try {
1596
+ const created = await octokit.rest.pulls.create({
1597
+ owner,
1598
+ repo: repoName,
1599
+ head: fixBranch,
1600
+ base: delivery.base,
1601
+ title: fixPrTitle,
1602
+ body: [
1603
+ autoFixPRIntro(prNumber),
1604
+ '',
1605
+ `Review: https://github.com/${owner}/${repoName}/pull/${prNumber}`,
1606
+ '',
1607
+ fixAttributionFooter(),
1608
+ ].join('\n'),
1609
+ });
1610
+ fixPr = created.data;
1611
+ }
1612
+ catch (createErr) {
1613
+ // A merge landing in a repo that deletes its branches takes the base away
1614
+ // between the checks above and this call, and `pulls.create` reports that as
1615
+ // `field: base, code: invalid` — the original failure this fallback exists to
1616
+ // stop, just in a narrower window. Take the branch back out and deliver the
1617
+ // fix as a diff rather than leaving it orphaned.
1618
+ if (!isInvalidBaseError(createErr))
1619
+ throw createErr;
1620
+ rollBackPushedFixBranch();
1621
+ await deliverFixAsComment({
1622
+ event: 'fix_pr_create_failed',
1623
+ reason: 'base_branch_gone_after_push',
1624
+ branch: delivery.base,
1625
+ notice: `${delivery.base} disappeared while the fix branch was being pushed — posting the fix as a diff instead of opening a stale follow-up PR`,
1626
+ explanation: `\`${delivery.base}\` existed when this fix started and was gone by the time the follow-up PR was opened, so this PR merged mid-run. The fix is below rather than in a follow-up PR — a PR cut from the pre-merge state would reintroduce whatever the merged version changed.`,
1627
+ });
1628
+ continue;
1629
+ }
1630
+ fixPrNumber = fixPr.number;
1631
+ }
1439
1632
  if (config.post_review.auto_fix.delivery.label) {
1440
1633
  try {
1441
1634
  await octokit.rest.issues.addLabels({
1442
- owner, repo: repoName, issue_number: fixPr.number, labels: [config.post_review.auto_fix.delivery.label],
1635
+ owner, repo: repoName, issue_number: fixPrNumber, labels: [config.post_review.auto_fix.delivery.label],
1443
1636
  });
1444
1637
  }
1445
1638
  catch { /* label may not exist in this repo — skip */ }
1446
1639
  }
1447
1640
  onPhaseChange('fixed ✓', { fixCount: appliedCount, phase: 'fixed', fixTokens: fixTokensUsed });
1448
- fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, sha: newSha, delivery: 'pull_request', fix_pr: fixPr.number, tokens_used: fixTokensUsed, skills_activated: activatedSkills.map(skill => skill.name), duration_ms: Date.now() - fixStepStart, ...triggerField });
1641
+ fileLog({ level: 'info', event: 'fix_complete', repo: `${owner}/${repoName}`, pr: prNumber, vendor: activeVendor, applied_count: appliedCount, sha: newSha, delivery: 'pull_request', fix_pr: fixPrNumber, tokens_used: fixTokensUsed, skills_activated: activatedSkills.map(skill => skill.name), duration_ms: Date.now() - fixStepStart, ...triggerField });
1449
1642
  results[step.name] = { applied_count: appliedCount, tokens_used: fixTokensUsed, vendor: activeVendor };
1450
1643
  }
1451
1644
  }