@mutmutco/cli 4.3.2 → 4.3.4
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/dist/main.cjs +190 -47
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5555,6 +5555,31 @@ function rewriteNegatedClosingPhrases(text) {
|
|
|
5555
5555
|
(seg, i) => i % 2 === 1 ? seg : seg.replace(NEGATED_CLOSING_PHRASE_RE, (_m, plain, linked) => `leaves #${plain ?? linked} open`)
|
|
5556
5556
|
).join("");
|
|
5557
5557
|
}
|
|
5558
|
+
var LINE_START_CLOSING_RE = /^\s*(?:[-*+]\s+)?(?:\*\*|__)?\s*(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+(?:#\d+|\[#\d+\]\()/i;
|
|
5559
|
+
var FENCE_LINE_RE = /^\s*(?:```|~~~)/;
|
|
5560
|
+
function rewriteCloseKeywordProse(text) {
|
|
5561
|
+
if (!text.includes("#")) return { text, rewritten: [] };
|
|
5562
|
+
const rewritten = /* @__PURE__ */ new Set();
|
|
5563
|
+
let inFence = false;
|
|
5564
|
+
const lines = text.split("\n").map((line) => {
|
|
5565
|
+
if (FENCE_LINE_RE.test(line)) {
|
|
5566
|
+
inFence = !inFence;
|
|
5567
|
+
return line;
|
|
5568
|
+
}
|
|
5569
|
+
if (inFence || LINE_START_CLOSING_RE.test(line)) return line;
|
|
5570
|
+
CLOSING_MENTION_RE.lastIndex = 0;
|
|
5571
|
+
return line.split(/(`[^`\n]*`)/g).map((seg, i) => {
|
|
5572
|
+
if (i % 2 === 1) return seg;
|
|
5573
|
+
return seg.replace(CLOSING_MENTION_RE, (match, _keyword, plain, linked) => {
|
|
5574
|
+
const n = Number(plain ?? linked);
|
|
5575
|
+
if (!Number.isInteger(n) || n <= 0) return match;
|
|
5576
|
+
rewritten.add(n);
|
|
5577
|
+
return `part of #${n}`;
|
|
5578
|
+
});
|
|
5579
|
+
}).join("");
|
|
5580
|
+
});
|
|
5581
|
+
return rewritten.size ? { text: lines.join("\n"), rewritten: [...rewritten] } : { text, rewritten: [] };
|
|
5582
|
+
}
|
|
5558
5583
|
function findClosingMentions(text) {
|
|
5559
5584
|
const mentions = [];
|
|
5560
5585
|
for (const match of text.matchAll(CLOSING_MENTION_RE)) {
|
|
@@ -5588,11 +5613,12 @@ var CROSS_REPO_TOKEN_RE = /\b([\w.-]+\/[\w.-]+)#(\d+)\b/g;
|
|
|
5588
5613
|
function findCrossRepoAmbiguousClosings(text, closingNumbers, repo) {
|
|
5589
5614
|
if (!closingNumbers.length) return [];
|
|
5590
5615
|
const closing = new Set(closingNumbers);
|
|
5616
|
+
const bareClosing = new Set(findClosingMentions(text).map((m) => m.issue));
|
|
5591
5617
|
const flagged = /* @__PURE__ */ new Set();
|
|
5592
5618
|
const repoLower = repo.toLowerCase();
|
|
5593
5619
|
for (const match of text.matchAll(CROSS_REPO_TOKEN_RE)) {
|
|
5594
5620
|
const n = Number(match[2]);
|
|
5595
|
-
if (!closing.has(n)) continue;
|
|
5621
|
+
if (!closing.has(n) || !bareClosing.has(n)) continue;
|
|
5596
5622
|
if ((match[1] ?? "").toLowerCase() === repoLower) continue;
|
|
5597
5623
|
flagged.add(n);
|
|
5598
5624
|
}
|
|
@@ -11708,7 +11734,16 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
|
|
|
11708
11734
|
const out = { failed: [] };
|
|
11709
11735
|
try {
|
|
11710
11736
|
const comments = await client.restPaginate(`repos/${repo}/issues/${number}/comments`);
|
|
11711
|
-
|
|
11737
|
+
const rows = comments.map((comment) => ({ body: comment.body ?? "", login: comment.user?.login ?? void 0 }));
|
|
11738
|
+
out.marker = latestClaimMarker(rows);
|
|
11739
|
+
if (out.marker) {
|
|
11740
|
+
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
11741
|
+
if (latestClaimMarker([rows[i]])) {
|
|
11742
|
+
out.markerBy = rows[i].login;
|
|
11743
|
+
break;
|
|
11744
|
+
}
|
|
11745
|
+
}
|
|
11746
|
+
}
|
|
11712
11747
|
out.markerAgeMs = out.marker ? Date.now() - Date.parse(out.marker.ts) : void 0;
|
|
11713
11748
|
if (out.marker) {
|
|
11714
11749
|
const state = probeLocalClaimSession(out.marker);
|
|
@@ -11775,17 +11810,26 @@ function laneOwnership(marker, current) {
|
|
|
11775
11810
|
}
|
|
11776
11811
|
return "unknown";
|
|
11777
11812
|
}
|
|
11778
|
-
|
|
11813
|
+
function laneResumeEvidence(evidence) {
|
|
11814
|
+
if (!evidence.marker || evidence.sessionState !== "dead" || evidence.failed.length > 0) return void 0;
|
|
11815
|
+
if (!isHostSessionId(evidence.marker.session)) return void 0;
|
|
11816
|
+
const artifacts = [evidence.openPr, evidence.branch ? `live branch ${evidence.branch}` : void 0].filter(Boolean);
|
|
11817
|
+
return `prior claim by lane ${describeClaimMarker(evidence.marker)} (@${evidence.markerBy}, ${formatClaimAge(evidence.markerAgeMs)} old) is verifiably not running on this host \u2014 its local session transcript is gone or stale${artifacts.length ? `; resuming over ${artifacts.join("; ")}` : ""}`;
|
|
11818
|
+
}
|
|
11819
|
+
async function checkLaneContest(client, item, actor = describeSessionIdentity(), viewerLogin) {
|
|
11779
11820
|
const evidence = await gatherClaimLiveness(client, item.repository, item.number, openPullsFetcher(client));
|
|
11780
11821
|
const ownership = laneOwnership(evidence.marker, actor);
|
|
11822
|
+
const sameOwner = Boolean(viewerLogin && evidence.markerBy && evidence.markerBy.toLowerCase() === viewerLogin.toLowerCase());
|
|
11823
|
+
const resume = ownership !== "mine" && sameOwner ? laneResumeEvidence(evidence) : void 0;
|
|
11781
11824
|
const live = ownership === "mine" ? [] : liveEvidenceLines(evidence, item.repository);
|
|
11782
11825
|
const unverifiable = ownership === "mine" ? [] : evidence.failed;
|
|
11783
11826
|
return {
|
|
11784
|
-
contested: ownership !== "mine" && (live.length > 0 || unverifiable.length > 0),
|
|
11827
|
+
contested: ownership !== "mine" && !resume && (live.length > 0 || unverifiable.length > 0),
|
|
11785
11828
|
live,
|
|
11786
11829
|
unverifiable,
|
|
11787
11830
|
ownership,
|
|
11788
|
-
marker: evidence.marker
|
|
11831
|
+
marker: evidence.marker,
|
|
11832
|
+
resume
|
|
11789
11833
|
};
|
|
11790
11834
|
}
|
|
11791
11835
|
function laneContestMessage(ref, contest, verb) {
|
|
@@ -12084,8 +12128,9 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12084
12128
|
host: ctx.session.host
|
|
12085
12129
|
};
|
|
12086
12130
|
let previousHolder;
|
|
12087
|
-
|
|
12088
|
-
const
|
|
12131
|
+
let resumeEvidence;
|
|
12132
|
+
const claimedReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "claimed", holder };
|
|
12133
|
+
const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : { outcome: "took-over", holder, previousHolder } : { outcome: "held", holder };
|
|
12089
12134
|
if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
|
|
12090
12135
|
const pre = evaluateClaim(flatItem, assignedLogin);
|
|
12091
12136
|
if (!pre.ok) throw new Error(pre.reason);
|
|
@@ -12096,17 +12141,25 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
12096
12141
|
if (!verdict.ok) throw new Error(verdict.reason);
|
|
12097
12142
|
item = fresh;
|
|
12098
12143
|
const refuseIfContested = async () => {
|
|
12099
|
-
const contest = await checkLaneContest(client, item, ctx.session);
|
|
12100
|
-
|
|
12101
|
-
|
|
12102
|
-
previousHolder = {
|
|
12144
|
+
const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
|
|
12145
|
+
resumeEvidence = void 0;
|
|
12146
|
+
const displaced = () => ({
|
|
12103
12147
|
login: assignedLogin,
|
|
12104
12148
|
...contest.marker ? {
|
|
12105
12149
|
session: contest.marker.session,
|
|
12106
12150
|
surface: contest.marker.surface,
|
|
12107
12151
|
host: contest.marker.host
|
|
12108
12152
|
} : {}
|
|
12109
|
-
};
|
|
12153
|
+
});
|
|
12154
|
+
if (!contest.contested) {
|
|
12155
|
+
if (contest.resume && contest.marker) {
|
|
12156
|
+
resumeEvidence = contest.resume;
|
|
12157
|
+
previousHolder = displaced();
|
|
12158
|
+
}
|
|
12159
|
+
return;
|
|
12160
|
+
}
|
|
12161
|
+
if (!options.force) throw new Error(laneContestMessage(item.ref, contest, "claim"));
|
|
12162
|
+
previousHolder = displaced();
|
|
12110
12163
|
};
|
|
12111
12164
|
await refuseIfContested();
|
|
12112
12165
|
if (verdict.alreadyClaimed) {
|
|
@@ -12176,7 +12229,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
12176
12229
|
const ref = `${selector.repo}#${selector.number}`;
|
|
12177
12230
|
try {
|
|
12178
12231
|
const result = await claimOneBoardItem(ctx, selector, options);
|
|
12179
|
-
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
12232
|
+
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, alreadyClaimed: result.alreadyClaimed, checked: result.checked };
|
|
12180
12233
|
} catch (e) {
|
|
12181
12234
|
results[index] = { ref, claimed: false, reason: e.message };
|
|
12182
12235
|
}
|
|
@@ -15322,10 +15375,10 @@ var rollout_plan_default = {
|
|
|
15322
15375
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
15323
15376
|
},
|
|
15324
15377
|
baseline: {
|
|
15325
|
-
version: "4.3.
|
|
15326
|
-
tag: "v4.3.
|
|
15327
|
-
commit: "
|
|
15328
|
-
npm: "@mutmutco/cli@4.3.
|
|
15378
|
+
version: "4.3.4",
|
|
15379
|
+
tag: "v4.3.4",
|
|
15380
|
+
commit: "cd7d0dd9cc3a",
|
|
15381
|
+
npm: "@mutmutco/cli@4.3.4"
|
|
15329
15382
|
},
|
|
15330
15383
|
exitCriterion: "fleet-n-of-n",
|
|
15331
15384
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15342,14 +15395,14 @@ var rollout_plan_default = {
|
|
|
15342
15395
|
repo: "mutmutco/mmi-hub",
|
|
15343
15396
|
role: "canary",
|
|
15344
15397
|
schedule: "train",
|
|
15345
|
-
v3Target: "v4.3.
|
|
15398
|
+
v3Target: "v4.3.4"
|
|
15346
15399
|
}
|
|
15347
15400
|
],
|
|
15348
15401
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
15349
15402
|
rollback: {
|
|
15350
15403
|
independent: true,
|
|
15351
|
-
mechanism: "npm dist-tag latest -> 4.3.
|
|
15352
|
-
v3Target: "v4.3.
|
|
15404
|
+
mechanism: "npm dist-tag latest -> 4.3.4 and redeploy the Hub Lambda from tag v4.3.4 (cd7d0dd9cc3a); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15405
|
+
v3Target: "v4.3.4 (@mutmutco/cli@4.3.4, tag commit cd7d0dd9cc3a \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
15353
15406
|
}
|
|
15354
15407
|
},
|
|
15355
15408
|
{
|
|
@@ -23765,19 +23818,23 @@ function registerBoardCommands(program3) {
|
|
|
23765
23818
|
if (result.checked) {
|
|
23766
23819
|
if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
|
|
23767
23820
|
if (result.outcome === "took-over") return `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
|
|
23821
|
+
if (result.outcome === "resumed") return `Check ${ref}: prior lane ${previousHolder} is verifiably dead - claim would resume its work for ${holder} (nothing written; ${result.resumeEvidence})`;
|
|
23768
23822
|
return `Check ${ref}: free - claim would proceed for ${holder} (nothing written)`;
|
|
23769
23823
|
}
|
|
23770
23824
|
if (result.partial) {
|
|
23771
|
-
|
|
23825
|
+
if (result.outcome === "took-over") return `Partially took over ${ref} from ${previousHolder}: ${result.warning}`;
|
|
23826
|
+
if (result.outcome === "resumed") return `Partially resumed ${ref} from ${previousHolder}: ${result.warning}`;
|
|
23827
|
+
return `Partially claimed ${ref} for ${holder}: ${result.warning}`;
|
|
23772
23828
|
}
|
|
23773
23829
|
if (result.outcome === "took-over") return `Took over ${ref} from ${previousHolder} for ${holder} - In Progress`;
|
|
23830
|
+
if (result.outcome === "resumed") return `Resumed ${ref} from ${previousHolder} for ${holder} - In Progress (${result.resumeEvidence})`;
|
|
23774
23831
|
if (result.outcome === "held") return `${ref} is held by ${holder} - In Progress`;
|
|
23775
23832
|
return `Claimed ${ref} for ${holder} - In Progress`;
|
|
23776
23833
|
}
|
|
23777
23834
|
const board = program3.command("board").description("read, claim, show, and move Project v2 work items for the current repo");
|
|
23778
23835
|
board.command("read", { isDefault: true }).alias("list").description("read the board and print user-owned, claimable, and taken items").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--direct", "bypass the Hub snapshot and read the live board through direct GitHub GraphQL").option("--bundle-details", "fetch body/comments only for user-owned and claimable issues").option("--bodies", "fetch body/comments for EVERY scoped row, including taken and unowned in-flight ones \u2014 for consumers that scope by Status rather than ownership (#4861); implies --bundle-details and costs one extra read per row").option("--allow-partial", "return partial board results when later page/detail reads fail").option("--out <path>", "write the output to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5802)").addHelpText("after", "\nread is always the authoritative live GitHub Project v2 board (#4926).\n--direct skips the Hub snapshot and uses the existing direct GitHub GraphQL read immediately.\n--allow-partial applies to the paginated path and detail reads.\n\nNever capture the JSON with a shell redirect on Windows: PowerShell 5.1's `> file.json` is Out-File,\nwhich writes UTF-16LE with a BOM, and Node reading it as 'utf8' then fails JSON.parse at position 1\n(#5802). Use --out instead \u2014 the CLI writes the file itself as UTF-8:\n mmi-cli oracle board read --json --out .jerv/tmp/board.json\n").action((o) => runBoardRead(o));
|
|
23779
23836
|
withExamples(mutating(
|
|
23780
|
-
board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").addHelpText("after", "\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
23837
|
+
board.command("claim <issues...>").description("claim issues: assign them and move their Project v2 Status to In Progress \u2014 idempotent, so an item already yours and In Progress succeeds unchanged (one or more refs)").addHelpText("after", "\nevery claim stamps a lane-identity marker comment on the issue (`<!-- mmi-claim: \u2026 -->`,\nsurface/session@host) so other agents can attribute the hold (#3727). The session is the\nhost-exported id when the surface provides one, otherwise a per-process `synth-` fallback \u2014\na claim is never anonymous (#5245). `board show`, doctor and unclaim read the latest marker.\n\nsame-owner resume (#6035): when the prior marker was posted by YOUR login on THIS host and its\nlocal session is verifiably dead (transcript probe), the claim proceeds as a resume without\n--force and names the evidence. A live, foreign-host, or unprobeable prior lane still refuses.\n").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").option("--for <login>", "assign to this login instead of @me \u2014 agent claims on behalf of the master").option("--force", "take an item already claimed by another lane that shows live evidence of active work (#3727)").option("--check", "read-only: run every claim gate and report the verdict, writing nothing \u2014 exits 1 with the same refusal a real claim would raise (#4511)").option("--allow-partial", "return success JSON if assignment succeeds but the status move fails"),
|
|
23781
23838
|
(_opts, args) => ({ command: "board claim", issues: args[0] ?? [] })
|
|
23782
23839
|
).action(async (issueRefs, o) => {
|
|
23783
23840
|
if (issueRefs.length === 1) {
|
|
@@ -35372,6 +35429,10 @@ function ghPrMergeLocalBranchDeleteWarning(message) {
|
|
|
35372
35429
|
function mergeAutoRejectedPrAlreadyClean(message) {
|
|
35373
35430
|
return /clean status \(enablePullRequestAutoMerge\)|is in clean status/i.test(message);
|
|
35374
35431
|
}
|
|
35432
|
+
function mergeRejectedBaseBranchModified(message) {
|
|
35433
|
+
return /base branch was modified\. review and try the merge again/i.test(message);
|
|
35434
|
+
}
|
|
35435
|
+
var PR_MERGE_BASE_RACE_RETRY_DELAY_MS = 3e3;
|
|
35375
35436
|
function assertRemoteBranchPreservedOnMerge(repo, deleteBranchOnMerge) {
|
|
35376
35437
|
if (deleteBranchOnMerge === false) return;
|
|
35377
35438
|
throw new Error(
|
|
@@ -35616,7 +35677,8 @@ async function mergeAutoEnqueueWithBody(prNumber, args, method, io, bodyFile) {
|
|
|
35616
35677
|
return confirmEnqueueOutcome();
|
|
35617
35678
|
}
|
|
35618
35679
|
function cleanupGitArgs(cwd, args) {
|
|
35619
|
-
|
|
35680
|
+
if (!cwd || args[0] === "-C") return args;
|
|
35681
|
+
return ["-C", cwd, ...args];
|
|
35620
35682
|
}
|
|
35621
35683
|
async function remoteBranchExists2(branch, options = {}) {
|
|
35622
35684
|
if (!branch) return void 0;
|
|
@@ -35651,14 +35713,16 @@ async function deleteMergedRemoteBranch(options) {
|
|
|
35651
35713
|
if (options.existedBefore === false) {
|
|
35652
35714
|
const exists2 = await options.branchExists(options.branch);
|
|
35653
35715
|
if (exists2 === false) return { branch: options.branch, existedBefore: false, attempted: false, status: "already-gone" };
|
|
35654
|
-
|
|
35655
|
-
|
|
35656
|
-
|
|
35657
|
-
|
|
35658
|
-
|
|
35659
|
-
|
|
35660
|
-
|
|
35661
|
-
|
|
35716
|
+
if (exists2 !== true) {
|
|
35717
|
+
return {
|
|
35718
|
+
branch: options.branch,
|
|
35719
|
+
existedBefore: false,
|
|
35720
|
+
attempted: false,
|
|
35721
|
+
status: "failed",
|
|
35722
|
+
error: `could not verify absence of origin/${options.branch}`,
|
|
35723
|
+
remediation
|
|
35724
|
+
};
|
|
35725
|
+
}
|
|
35662
35726
|
}
|
|
35663
35727
|
try {
|
|
35664
35728
|
await options.execGit(["push", "origin", "--delete", options.branch]);
|
|
@@ -36028,6 +36092,24 @@ async function checkDocsIndexAtHead(opts, deps) {
|
|
|
36028
36092
|
};
|
|
36029
36093
|
}
|
|
36030
36094
|
|
|
36095
|
+
// src/pr-create-claim-guard.ts
|
|
36096
|
+
async function prCreateClaimRefusal(body, repoOption, deps = {}) {
|
|
36097
|
+
const issues = [...new Set(findClosingMentions(body).map((mention) => mention.issue))];
|
|
36098
|
+
if (!issues.length) return void 0;
|
|
36099
|
+
const repo = await requireRepo(repoOption);
|
|
36100
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
36101
|
+
const actor = deps.actor ?? describeSessionIdentity();
|
|
36102
|
+
const checkContest = deps.checkContest ?? checkLaneContest;
|
|
36103
|
+
for (const number of issues) {
|
|
36104
|
+
const contest = await checkContest(client, { repository: repo, number }, actor);
|
|
36105
|
+
if (!contest.contested) continue;
|
|
36106
|
+
const ref = `${repo}#${number}`;
|
|
36107
|
+
const holder = contest.marker ? `lane ${describeClaimMarker(contest.marker)}` : "another lane";
|
|
36108
|
+
return `pr create: REFUSED \u2014 ${ref} is held by ${holder} with live or unreadable work evidence; run \`mmi-cli oracle board claim ${ref} --force\` before creating this PR`;
|
|
36109
|
+
}
|
|
36110
|
+
return void 0;
|
|
36111
|
+
}
|
|
36112
|
+
|
|
36031
36113
|
// src/worktree-merge-cleanup.ts
|
|
36032
36114
|
var import_node_fs40 = require("node:fs");
|
|
36033
36115
|
var import_node_path37 = require("node:path");
|
|
@@ -36313,6 +36395,22 @@ function samePath(a, b) {
|
|
|
36313
36395
|
const nb = normPath2(b);
|
|
36314
36396
|
return process.platform === "win32" ? na.toLowerCase() === nb.toLowerCase() : na === nb;
|
|
36315
36397
|
}
|
|
36398
|
+
function isPathAtOrWithin(path2, worktreePath) {
|
|
36399
|
+
if (!path2) return false;
|
|
36400
|
+
const candidate = normPath2(path2);
|
|
36401
|
+
const target = normPath2(worktreePath);
|
|
36402
|
+
const normalize = (value) => process.platform === "win32" ? value.toLowerCase() : value;
|
|
36403
|
+
const normalizedCandidate = normalize(candidate);
|
|
36404
|
+
const normalizedTarget = normalize(target);
|
|
36405
|
+
return normalizedCandidate === normalizedTarget || normalizedCandidate.startsWith(`${normalizedTarget}/`);
|
|
36406
|
+
}
|
|
36407
|
+
function isWindowsCwdLockResidueError(error) {
|
|
36408
|
+
return process.platform === "win32" && /\bEPERM\b|access(?: is)? denied/i.test(error);
|
|
36409
|
+
}
|
|
36410
|
+
function deferredCwdLockRemediation(primaryRoot, wtPath) {
|
|
36411
|
+
const quote = (path2) => path2.replace(/'/g, "''");
|
|
36412
|
+
return `After this command exits: Set-Location -LiteralPath '${quote(primaryRoot)}'; Remove-Item -LiteralPath '${quote(wtPath)}' -Recurse -Force`;
|
|
36413
|
+
}
|
|
36316
36414
|
function selectPrMergeCleanupWorktree(branch, before, after, startingPath) {
|
|
36317
36415
|
if (!branch) return void 0;
|
|
36318
36416
|
const current = after.find((w) => w.branch === branch)?.path;
|
|
@@ -36481,6 +36579,10 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
|
|
|
36481
36579
|
})
|
|
36482
36580
|
};
|
|
36483
36581
|
}
|
|
36582
|
+
function isPrMergeWorktreePartial(cleanup) {
|
|
36583
|
+
const worktree = cleanup?.worktree;
|
|
36584
|
+
return Boolean(worktree?.path && worktree.status !== "removed" && worktree.status !== "preserved" && worktree.status !== "retained-locked");
|
|
36585
|
+
}
|
|
36484
36586
|
function prMergeLocalCleanupExitCode(cleanup) {
|
|
36485
36587
|
return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
|
|
36486
36588
|
}
|
|
@@ -36651,11 +36753,17 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
36651
36753
|
if (residue.ok) {
|
|
36652
36754
|
report.worktree.residue = "swept";
|
|
36653
36755
|
} else {
|
|
36654
|
-
report.worktree.status = "failed";
|
|
36655
|
-
report.worktree.reason = "residue-remains";
|
|
36656
36756
|
report.worktree.residue = "left";
|
|
36657
36757
|
report.worktree.residueError = residue.error;
|
|
36658
|
-
|
|
36758
|
+
if (isWindowsCwdLockResidueError(residue.error) && isPathAtOrWithin(options.startingPath, wtPath)) {
|
|
36759
|
+
report.worktree.status = "retained-locked";
|
|
36760
|
+
report.worktree.reason = "parent-cwd-lock";
|
|
36761
|
+
report.worktree.remediation = deferredCwdLockRemediation(options.primaryRoot, wtPath);
|
|
36762
|
+
} else {
|
|
36763
|
+
report.worktree.status = "failed";
|
|
36764
|
+
report.worktree.reason = "residue-remains";
|
|
36765
|
+
report.worktree.remediation = `Remove-Item -LiteralPath '${wtPath.replace(/'/g, "''")}' -Force`;
|
|
36766
|
+
}
|
|
36659
36767
|
}
|
|
36660
36768
|
}
|
|
36661
36769
|
try {
|
|
@@ -36684,8 +36792,10 @@ function renderPrMergeCleanupLines(cleanup) {
|
|
|
36684
36792
|
lines.push(`pr merge: worktree ${wt.path} removal failed${wt.error ? ` \u2014 ${wt.error}` : ""}`);
|
|
36685
36793
|
} else if (wt.status === "preserved") {
|
|
36686
36794
|
lines.push(`pr merge: preserved worktree ${wt.path} (--preserve-worktree)`);
|
|
36795
|
+
} else if (wt.status === "retained-locked") {
|
|
36796
|
+
lines.push(`pr merge: worktree ${wt.path} cleanup deferred \u2014 its Windows parent shell still holds the cwd; ${wt.remediation ?? "exit the shell and remove the residue from the primary checkout"}`);
|
|
36687
36797
|
}
|
|
36688
|
-
if (wt.residue === "left") {
|
|
36798
|
+
if (wt.residue === "left" && wt.status !== "retained-locked") {
|
|
36689
36799
|
lines.push(`pr merge: worktree ${wt.path} registration is gone but residue remains \u2014 ${wt.residueError ?? wt.path}; remediate: ${wt.remediation ?? wt.path}`);
|
|
36690
36800
|
}
|
|
36691
36801
|
if (wt.artifactsArchive?.status === "archived" && wt.artifactsArchive.path) {
|
|
@@ -37331,10 +37441,20 @@ ${list}`);
|
|
|
37331
37441
|
}
|
|
37332
37442
|
body = normalizeClosingDirectives(body);
|
|
37333
37443
|
body = rewriteNegatedClosingPhrases(body);
|
|
37444
|
+
const prose = rewriteCloseKeywordProse(body);
|
|
37445
|
+
if (prose.rewritten.length) {
|
|
37446
|
+
body = prose.text;
|
|
37447
|
+
process.stderr.write(
|
|
37448
|
+
`pr create: WARNING \u2014 rewrote close-keyword prose to "part of #N" for ${prose.rewritten.map((n) => `#${n}`).join(", ")}: GitHub parses close/fix/resolve + #N even mid-sentence (#6039). Keep closing keywords on a dedicated "Closes #N" line.
|
|
37449
|
+
`
|
|
37450
|
+
);
|
|
37451
|
+
}
|
|
37334
37452
|
const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
|
|
37335
37453
|
if (docsCheck && !docsCheck.ok) {
|
|
37336
37454
|
return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
|
|
37337
37455
|
}
|
|
37456
|
+
const claimRefusal = await prCreateClaimRefusal(body, o.repo);
|
|
37457
|
+
if (claimRefusal) return fail(claimRefusal);
|
|
37338
37458
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
37339
37459
|
if (isGhCreateRateLimited(created)) {
|
|
37340
37460
|
console.log(JSON.stringify(created));
|
|
@@ -37349,7 +37469,8 @@ ${list}`);
|
|
|
37349
37469
|
], [
|
|
37350
37470
|
"--head and --base default to the current branch and the repo default; only pass them to override.",
|
|
37351
37471
|
"Use --body-file for multiline PR bodies instead of shell-escaped inline markdown.",
|
|
37352
|
-
"Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files."
|
|
37472
|
+
"Write that file under .jerv/ inside the host workspace (#4405); host policy governs untracked files.",
|
|
37473
|
+
'Keep closing keywords on a dedicated line ("Closes #N") \u2014 GitHub parses close/fix/resolve + #N even mid-sentence, so pr create rewrites such prose to "part of #N" (#6039).'
|
|
37353
37474
|
]);
|
|
37354
37475
|
pr.command("view <ref>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347). --comments folds in every comment; --context also adds linkedIssues (the issues it closes/references, #2894)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').option("--comments", "include every comment (body + comments in one call) \u2014 read the whole PR before landing it (#2894)").option("--context", "full working context in one call: implies --comments and also adds linkedIssues (the issues the PR closes/references) (#2894)").action(async (ref, o) => {
|
|
37355
37476
|
let parsed;
|
|
@@ -37738,7 +37859,7 @@ ${list}`);
|
|
|
37738
37859
|
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
37739
37860
|
if (result.status === "failed") process.exitCode = 1;
|
|
37740
37861
|
});
|
|
37741
|
-
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
|
|
37862
|
+
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default); archives gitignored tmp/** before worktree teardown; on no-ci repos run pr ci-policy / checks-wait first (#1432, #5679)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").addOption(new Option("--disable-auto", "disable a queued auto-merge without merging").conflicts(["auto", "wait", "squash", "merge", "rebase", "preserveWorktree", "gc", "squashBodyFile", "force"])).option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m) \u2014 run as a background/monitor task or under a shell timeout above that budget; a short foreground timeout (e.g. 120s) kills it after checks pass and leaves the PR open (#6027)`).option("--preserve-worktree", "after merge, keep the local PR worktree/branch for an active batch (#1888)").option("--gc", "acknowledge deleting unarchived gitignored tmp/** evidence newer than the branch base (#5679)").option("--squash-body-file <path>", "squash commit body (overrides GitHub COMMIT_MESSAGES); use when a pushed commit mentions close/fix/resolve + #N that must not close (#5723)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
|
|
37742
37863
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
37743
37864
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
37744
37865
|
if (o.disableAuto) {
|
|
@@ -37802,6 +37923,7 @@ ${list}`);
|
|
|
37802
37923
|
const ciHeadRef = repoForPostCleanup ? await prHeadRefForCiProbe(number, repoForPostCleanup) : void 0;
|
|
37803
37924
|
const ciPolicy = await resolveMergeCiPolicyForCheckout(o.repo, ciHeadRef);
|
|
37804
37925
|
if (o.wait) {
|
|
37926
|
+
console.warn(`pr merge: --wait can hold the shell up to the full ${PR_CHECKS_TIMEOUT_MS / 6e4}m budget plus the merge \u2014 run under a shell timeout above that budget or as a background/monitor task (#6027)`);
|
|
37805
37927
|
const repo = await requireRepo(o.repo);
|
|
37806
37928
|
const budgetMs = PR_CHECKS_TIMEOUT_MS;
|
|
37807
37929
|
const waitStarted = Date.now();
|
|
@@ -37887,6 +38009,21 @@ ${list}`);
|
|
|
37887
38009
|
});
|
|
37888
38010
|
return;
|
|
37889
38011
|
}
|
|
38012
|
+
if (!o.auto && mergeRejectedBaseBranchModified(message)) {
|
|
38013
|
+
console.warn(`pr merge: the base branch was modified by a concurrent merge \u2014 waiting ${PR_MERGE_BASE_RACE_RETRY_DELAY_MS / 1e3}s and retrying PR #${number}'s merge once (#6052).`);
|
|
38014
|
+
await new Promise((resolve7) => setTimeout(resolve7, PR_MERGE_BASE_RACE_RETRY_DELAY_MS));
|
|
38015
|
+
await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
|
|
38016
|
+
const m2 = String(e2.message || "");
|
|
38017
|
+
if (/already been merged/i.test(m2)) {
|
|
38018
|
+
remoteNotAttemptedReason = "pr-already-merged";
|
|
38019
|
+
return;
|
|
38020
|
+
}
|
|
38021
|
+
const note2 = timeoutKillNote(e2, GH_MUTATION_TIMEOUT_MS);
|
|
38022
|
+
if (note2) throw new Error(`gh pr merge ${number}: ${note2}`);
|
|
38023
|
+
if (!ghPrMergeLocalBranchDeleteWarning(m2)) throw e2;
|
|
38024
|
+
});
|
|
38025
|
+
return;
|
|
38026
|
+
}
|
|
37890
38027
|
if (o.auto && mergeAutoRejectedPrAlreadyClean(message)) {
|
|
37891
38028
|
await execFileP("gh", buildPrMergeArgs({ number, repoArgs, method, auto: false, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
|
|
37892
38029
|
const m2 = String(e2.message || "");
|
|
@@ -37995,12 +38132,13 @@ ${list}`);
|
|
|
37995
38132
|
execGit: async (args) => (await execFileP("git", cleanupGitArgs(primaryRoot, args), { timeout: GIT_TIMEOUT_MS })).stdout,
|
|
37996
38133
|
branchExists: (b) => remoteBranchExists2(b, { cwd: primaryRoot })
|
|
37997
38134
|
});
|
|
37998
|
-
const
|
|
38135
|
+
const worktree = localCleanup?.worktree;
|
|
38136
|
+
const worktreePartial = isPrMergeWorktreePartial(localCleanup) && worktree?.path ? {
|
|
37999
38137
|
kind: "worktree-directory",
|
|
38000
|
-
path:
|
|
38001
|
-
reason:
|
|
38002
|
-
error:
|
|
38003
|
-
remediation:
|
|
38138
|
+
path: worktree.path,
|
|
38139
|
+
reason: worktree.reason ?? worktree.status,
|
|
38140
|
+
error: worktree.error ?? worktree.residueError,
|
|
38141
|
+
remediation: worktree.remediation ?? `Remove-Item -LiteralPath '${worktree.path.replace(/'/g, "''")}' -Recurse -Force`
|
|
38004
38142
|
} : void 0;
|
|
38005
38143
|
const partialCleanup = [
|
|
38006
38144
|
...worktreePartial ? [worktreePartial] : [],
|
|
@@ -38345,11 +38483,16 @@ function registerBoxCommands(program3) {
|
|
|
38345
38483
|
await failGraceful("runtime box get: --script requires --ssh (it writes the ssh connect recipe)");
|
|
38346
38484
|
return;
|
|
38347
38485
|
}
|
|
38348
|
-
|
|
38349
|
-
|
|
38350
|
-
|
|
38351
|
-
console.log(
|
|
38352
|
-
|
|
38486
|
+
const wroteScript = o.ssh && o.script ? o.script : null;
|
|
38487
|
+
if (wroteScript) (0, import_node_fs42.writeFileSync)(wroteScript, sshRecipeScript(found), "utf8");
|
|
38488
|
+
if (o.json) {
|
|
38489
|
+
console.log(JSON.stringify({
|
|
38490
|
+
box: found,
|
|
38491
|
+
incomplete,
|
|
38492
|
+
...wroteScript ? { script: { path: wroteScript, run: `bash "${wroteScript}"` } } : {}
|
|
38493
|
+
}, null, 2));
|
|
38494
|
+
} else if (wroteScript) console.log(`wrote ${wroteScript} \u2014 run: bash "${wroteScript}"`);
|
|
38495
|
+
else if (o.ssh) console.log(`${formatSshRecipe(found)}
|
|
38353
38496
|
${SSH_RECIPE_AGENT_NOTE}`);
|
|
38354
38497
|
else console.log(formatBoxTable([found]));
|
|
38355
38498
|
if (!o.json) warnIncomplete2(incomplete);
|
package/package.json
CHANGED