@mutmutco/cli 4.1.15 → 4.1.17
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 +462 -104
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -13181,10 +13181,10 @@ var rollout_plan_default = {
|
|
|
13181
13181
|
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)."
|
|
13182
13182
|
},
|
|
13183
13183
|
baseline: {
|
|
13184
|
-
version: "4.1.
|
|
13185
|
-
tag: "v4.1.
|
|
13186
|
-
commit: "
|
|
13187
|
-
npm: "@mutmutco/cli@4.1.
|
|
13184
|
+
version: "4.1.17",
|
|
13185
|
+
tag: "v4.1.17",
|
|
13186
|
+
commit: "65f193ccd344",
|
|
13187
|
+
npm: "@mutmutco/cli@4.1.17"
|
|
13188
13188
|
},
|
|
13189
13189
|
exitCriterion: "fleet-n-of-n",
|
|
13190
13190
|
hubOnlyShortcut: "forbidden",
|
|
@@ -13201,14 +13201,14 @@ var rollout_plan_default = {
|
|
|
13201
13201
|
repo: "mutmutco/mmi-hub",
|
|
13202
13202
|
role: "canary",
|
|
13203
13203
|
schedule: "train",
|
|
13204
|
-
v3Target: "v4.1.
|
|
13204
|
+
v3Target: "v4.1.17"
|
|
13205
13205
|
}
|
|
13206
13206
|
],
|
|
13207
13207
|
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.",
|
|
13208
13208
|
rollback: {
|
|
13209
13209
|
independent: true,
|
|
13210
|
-
mechanism: "npm dist-tag latest -> 4.1.
|
|
13211
|
-
v3Target: "v4.1.
|
|
13210
|
+
mechanism: "npm dist-tag latest -> 4.1.17 and redeploy the Hub Lambda from tag v4.1.17 (65f193ccd344); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
13211
|
+
v3Target: "v4.1.17 (@mutmutco/cli@4.1.17, tag commit 65f193ccd344 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
13212
13212
|
}
|
|
13213
13213
|
},
|
|
13214
13214
|
{
|
|
@@ -14193,7 +14193,8 @@ async function runPrForIssue(deps, issueRef, opts) {
|
|
|
14193
14193
|
return extractPrForIssueResponse(resp);
|
|
14194
14194
|
}
|
|
14195
14195
|
async function runIssueViewContext(deps, opts) {
|
|
14196
|
-
const
|
|
14196
|
+
const baseFields = opts.context ? opts.jsonFields.split(",").map((field) => field.trim()).filter((field) => field !== "linkedPrs" && field !== "children").join(",") : opts.jsonFields;
|
|
14197
|
+
const fields = opts.comments || opts.context ? ensureField(baseFields, "comments") : baseFields;
|
|
14197
14198
|
const base = await deps.ghJson(["issue", "view", String(opts.number), "--repo", opts.repo, "--json", fields], GH_LIST_TIMEOUT_MS);
|
|
14198
14199
|
if (!opts.context) return base;
|
|
14199
14200
|
const result = { ...base };
|
|
@@ -20410,13 +20411,44 @@ function crossRepoAmbiguousRefusalMessage(ambiguous, repo, context = "pr merge")
|
|
|
20410
20411
|
const first = `#${ambiguous[0] ?? "N"}`;
|
|
20411
20412
|
return `${context}: REFUSED \u2014 GitHub will close ${named} on merge (a bare \`#N\` after a closing keyword always means the repo the PR lives in), but this body also mentions ${first} qualified against a DIFFERENT repo elsewhere \u2014 check whether that closing keyword was meant to carry an explicit owner/repo#N qualifier instead. Reword the closing reference with the full owner/repo#N form, or re-run with --force to merge anyway and let ${named} close.`;
|
|
20412
20413
|
}
|
|
20413
|
-
function
|
|
20414
|
-
if (!Array.isArray(commits)) return
|
|
20414
|
+
function parsedCommits(commits) {
|
|
20415
|
+
if (!Array.isArray(commits)) return [];
|
|
20415
20416
|
return commits.map((commit) => {
|
|
20416
20417
|
const { messageHeadline, messageBody } = commit ?? {};
|
|
20417
|
-
return
|
|
20418
|
-
|
|
20419
|
-
|
|
20418
|
+
return {
|
|
20419
|
+
headline: typeof messageHeadline === "string" ? messageHeadline : "",
|
|
20420
|
+
body: typeof messageBody === "string" ? messageBody : ""
|
|
20421
|
+
};
|
|
20422
|
+
});
|
|
20423
|
+
}
|
|
20424
|
+
function commitMessagesText(commits) {
|
|
20425
|
+
return parsedCommits(commits).map((commit) => `${commit.headline}
|
|
20426
|
+
${commit.body}`).join("\n");
|
|
20427
|
+
}
|
|
20428
|
+
function defaultSquashBodyFromCommits(commits) {
|
|
20429
|
+
if (commits.length <= 1) {
|
|
20430
|
+
const only = commits[0];
|
|
20431
|
+
if (!only) return "";
|
|
20432
|
+
const body = only.body.trim();
|
|
20433
|
+
return body || only.headline.trim();
|
|
20434
|
+
}
|
|
20435
|
+
return commits.map((commit) => `${commit.headline}
|
|
20436
|
+
|
|
20437
|
+
${commit.body}`.trim()).filter(Boolean).join("\n\n");
|
|
20438
|
+
}
|
|
20439
|
+
function rewriteUnregisteredClosingKeywords(text, allowedClosing) {
|
|
20440
|
+
if (!text.includes("#")) return text;
|
|
20441
|
+
const allowed = new Set(allowedClosing);
|
|
20442
|
+
CLOSING_MENTION_RE.lastIndex = 0;
|
|
20443
|
+
const segments = text.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~|`[^`\n]*`)/g);
|
|
20444
|
+
return segments.map((seg, i) => {
|
|
20445
|
+
if (i % 2 === 1) return seg;
|
|
20446
|
+
return seg.replace(CLOSING_MENTION_RE, (match, _keyword, plain, linked) => {
|
|
20447
|
+
const n = Number(plain ?? linked);
|
|
20448
|
+
if (!Number.isInteger(n) || n <= 0 || allowed.has(n)) return match;
|
|
20449
|
+
return `part of #${n}`;
|
|
20450
|
+
});
|
|
20451
|
+
}).join("");
|
|
20420
20452
|
}
|
|
20421
20453
|
function parseClosingGuardInput(raw, repo) {
|
|
20422
20454
|
if (!raw || typeof raw !== "object") return void 0;
|
|
@@ -20428,17 +20460,25 @@ function parseClosingGuardInput(raw, repo) {
|
|
|
20428
20460
|
if (typeof n !== "number" || !Number.isInteger(n) || n <= 0) return void 0;
|
|
20429
20461
|
closing.push(n);
|
|
20430
20462
|
}
|
|
20463
|
+
const parsed = Array.isArray(commits) ? parsedCommits(commits) : [];
|
|
20431
20464
|
const commitClosing = [...new Set(findClosingMentions(commitMessagesText(commits)).map((m) => m.issue))];
|
|
20432
20465
|
const text = `${typeof title === "string" ? title : ""}
|
|
20433
20466
|
${typeof body === "string" ? body : ""}`;
|
|
20434
|
-
return { state, text, closing, commitClosing, repo };
|
|
20467
|
+
return { state, text, closing, commitClosing, commits: parsed, repo };
|
|
20435
20468
|
}
|
|
20436
|
-
function
|
|
20437
|
-
if (!Array.isArray(commits)) return
|
|
20469
|
+
function parsedRestCommits(commits) {
|
|
20470
|
+
if (!Array.isArray(commits)) return [];
|
|
20438
20471
|
return commits.map((entry) => {
|
|
20439
20472
|
const message = (entry ?? {}).commit?.message;
|
|
20440
|
-
|
|
20441
|
-
|
|
20473
|
+
if (typeof message !== "string" || !message) return { headline: "", body: "" };
|
|
20474
|
+
const nl = message.indexOf("\n");
|
|
20475
|
+
if (nl === -1) return { headline: message, body: "" };
|
|
20476
|
+
return { headline: message.slice(0, nl), body: message.slice(nl + 1).replace(/^\n/, "") };
|
|
20477
|
+
});
|
|
20478
|
+
}
|
|
20479
|
+
function restCommitMessagesText(commits) {
|
|
20480
|
+
return parsedRestCommits(commits).map((commit) => `${commit.headline}
|
|
20481
|
+
${commit.body}`).join("\n");
|
|
20442
20482
|
}
|
|
20443
20483
|
function parseRestClosingGuardInput(prRaw, commitsRaw, repo) {
|
|
20444
20484
|
if (!prRaw || typeof prRaw !== "object") return void 0;
|
|
@@ -20446,10 +20486,11 @@ function parseRestClosingGuardInput(prRaw, commitsRaw, repo) {
|
|
|
20446
20486
|
if (typeof state !== "string") return void 0;
|
|
20447
20487
|
const bodyText = typeof body === "string" ? body : "";
|
|
20448
20488
|
const closing = [...new Set(findClosingMentions(bodyText).map((m) => m.issue))];
|
|
20489
|
+
const parsed = parsedRestCommits(commitsRaw);
|
|
20449
20490
|
const commitClosing = [...new Set(findClosingMentions(restCommitMessagesText(commitsRaw)).map((m) => m.issue))];
|
|
20450
20491
|
const text = `${typeof title === "string" ? title : ""}
|
|
20451
20492
|
${bodyText}`;
|
|
20452
|
-
return { state: merged === true ? "MERGED" : state.toUpperCase(), text, closing, commitClosing, repo };
|
|
20493
|
+
return { state: merged === true ? "MERGED" : state.toUpperCase(), text, closing, commitClosing, commits: parsed, repo };
|
|
20453
20494
|
}
|
|
20454
20495
|
function negatedClosingRefusalMessage(negated, context = "pr merge") {
|
|
20455
20496
|
const named = negated.map((n) => `#${n}`).join(", ");
|
|
@@ -20459,7 +20500,7 @@ function negatedClosingRefusalMessage(negated, context = "pr merge") {
|
|
|
20459
20500
|
function commitClosingRefusalMessage(closing, context = "pr merge") {
|
|
20460
20501
|
const named = closing.map((n) => `#${n}`).join(", ");
|
|
20461
20502
|
const first = `#${closing[0] ?? "N"}`;
|
|
20462
|
-
return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), or re-run with --force to merge anyway and let ${named} close.`;
|
|
20503
|
+
return `${context}: REFUSED \u2014 commit messages will close ${named} through the squash body, but GitHub omitted ${named} from closingIssuesReferences. Remove close/fix/resolve + ${first} from the commit message (if the issue must stay open, use "Part of ${first}" / "Refs ${first}" / "leaves ${first} open" \u2014 never "does not close ${first}"), edit the squash body at merge time (--squash-body-file), or re-run with --force to merge anyway and let ${named} close.`;
|
|
20463
20504
|
}
|
|
20464
20505
|
function alreadyClosedCommitClosingMessage(closed, context = "pr merge") {
|
|
20465
20506
|
const named = closed.map((n) => `#${n}`).join(", ");
|
|
@@ -20481,8 +20522,9 @@ function evaluateClosingGuard(input, opts) {
|
|
|
20481
20522
|
};
|
|
20482
20523
|
}
|
|
20483
20524
|
if (input.state !== "OPEN") return { blocked: false };
|
|
20525
|
+
const squashBodyClosing = opts.squashBodyText ? [...new Set(findClosingMentions(opts.squashBodyText).map((m) => m.issue))] : void 0;
|
|
20484
20526
|
const { open: commitClosing, skippedClosed } = partitionCommitClosings(
|
|
20485
|
-
input.commitClosing ?? [],
|
|
20527
|
+
squashBodyClosing ?? input.commitClosing ?? [],
|
|
20486
20528
|
input.closing,
|
|
20487
20529
|
input.alreadyClosed
|
|
20488
20530
|
);
|
|
@@ -25225,9 +25267,35 @@ function repoIndexPathTieBreak(path2) {
|
|
|
25225
25267
|
if (/\.(test|spec)\.[^.]+$/i.test(p)) return 1;
|
|
25226
25268
|
return 0;
|
|
25227
25269
|
}
|
|
25270
|
+
var LOCAL_REPO_INDEX_REFRESH_BUDGET_MS = 2e3;
|
|
25271
|
+
var LOCAL_REPO_INDEX_DELTA_FILE_LIMIT = 50;
|
|
25228
25272
|
function repoIndexStorePath(cwd) {
|
|
25229
25273
|
return repoRuntimeStatePath(cwd, "repo-index", "index.json");
|
|
25230
25274
|
}
|
|
25275
|
+
function localRepoIndexUsagePath(cwd) {
|
|
25276
|
+
return repoRuntimeStatePath(cwd, "repo-index", "local-usage.json");
|
|
25277
|
+
}
|
|
25278
|
+
function recordLocalRepoIndexQueryUsage(cwd, now = /* @__PURE__ */ new Date()) {
|
|
25279
|
+
const store = localRepoIndexUsagePath(cwd);
|
|
25280
|
+
let previous = 0;
|
|
25281
|
+
try {
|
|
25282
|
+
const raw = JSON.parse((0, import_node_fs24.readFileSync)(store, "utf8"));
|
|
25283
|
+
if (typeof raw.count === "number" && Number.isFinite(raw.count)) previous = raw.count;
|
|
25284
|
+
} catch {
|
|
25285
|
+
}
|
|
25286
|
+
(0, import_node_fs24.mkdirSync)((0, import_node_path21.dirname)(store), { recursive: true });
|
|
25287
|
+
(0, import_node_fs24.writeFileSync)(store, `${JSON.stringify({ count: previous + 1, updatedAt: now.toISOString() }, null, 2)}
|
|
25288
|
+
`, "utf8");
|
|
25289
|
+
}
|
|
25290
|
+
function readLocalRepoIndexQueryUsage(cwd) {
|
|
25291
|
+
try {
|
|
25292
|
+
const raw = JSON.parse((0, import_node_fs24.readFileSync)(localRepoIndexUsagePath(cwd), "utf8"));
|
|
25293
|
+
if (typeof raw.count !== "number" || !Number.isFinite(raw.count)) return null;
|
|
25294
|
+
return { count: raw.count, updatedAt: raw.updatedAt ?? "" };
|
|
25295
|
+
} catch {
|
|
25296
|
+
return null;
|
|
25297
|
+
}
|
|
25298
|
+
}
|
|
25231
25299
|
function isHardDeniedPath(relPosix) {
|
|
25232
25300
|
return isHardDeniedRepoIndexPath(relPosix);
|
|
25233
25301
|
}
|
|
@@ -25338,14 +25406,41 @@ function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync
|
|
|
25338
25406
|
return [];
|
|
25339
25407
|
}
|
|
25340
25408
|
}
|
|
25341
|
-
|
|
25342
|
-
|
|
25343
|
-
|
|
25344
|
-
|
|
25345
|
-
|
|
25409
|
+
var RepoIndexRefreshBudgetError = class extends Error {
|
|
25410
|
+
constructor() {
|
|
25411
|
+
super(`local index refresh exceeded ${LOCAL_REPO_INDEX_REFRESH_BUDGET_MS}ms`);
|
|
25412
|
+
}
|
|
25413
|
+
};
|
|
25414
|
+
function assertRefreshBudget(deadline) {
|
|
25415
|
+
if (deadline !== void 0 && Date.now() >= deadline) throw new RepoIndexRefreshBudgetError();
|
|
25416
|
+
}
|
|
25417
|
+
function deadlineExec(deadline) {
|
|
25418
|
+
return ((file, args, options) => {
|
|
25419
|
+
assertRefreshBudget(deadline);
|
|
25420
|
+
return (0, import_node_child_process12.execFileSync)(file, args, {
|
|
25421
|
+
...options,
|
|
25422
|
+
timeout: Math.max(1, deadline - Date.now()),
|
|
25423
|
+
windowsHide: true
|
|
25424
|
+
});
|
|
25425
|
+
});
|
|
25426
|
+
}
|
|
25427
|
+
function gitHead(cwd, exec = import_node_child_process12.execFileSync) {
|
|
25428
|
+
try {
|
|
25429
|
+
return String(exec("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf8", windowsHide: true })).trim() || void 0;
|
|
25430
|
+
} catch (error) {
|
|
25431
|
+
if (error instanceof RepoIndexRefreshBudgetError) throw error;
|
|
25432
|
+
return void 0;
|
|
25433
|
+
}
|
|
25434
|
+
}
|
|
25435
|
+
function sourceFilesForIndex(cwd, deadline) {
|
|
25436
|
+
const exec = deadline === void 0 ? import_node_child_process12.execFileSync : deadlineExec(deadline);
|
|
25437
|
+
const candidates = listCandidatePaths(cwd, exec).filter(isIndexablePath);
|
|
25438
|
+
assertRefreshBudget(deadline);
|
|
25439
|
+
const ignored = defaultIsIgnored(cwd, candidates, exec);
|
|
25440
|
+
const files = [];
|
|
25346
25441
|
for (const rel of candidates) {
|
|
25347
|
-
|
|
25348
|
-
if (isHardDeniedPath(rel)) continue;
|
|
25442
|
+
assertRefreshBudget(deadline);
|
|
25443
|
+
if (ignored.has(rel) || isHardDeniedPath(rel)) continue;
|
|
25349
25444
|
const abs = (0, import_node_path21.join)(cwd, ...rel.split("/"));
|
|
25350
25445
|
if (!(0, import_node_fs24.existsSync)(abs)) continue;
|
|
25351
25446
|
let text;
|
|
@@ -25355,31 +25450,118 @@ function rebuildRepoIndex(cwd, repoSlug3) {
|
|
|
25355
25450
|
continue;
|
|
25356
25451
|
}
|
|
25357
25452
|
if (text.length > 15e5) continue;
|
|
25358
|
-
|
|
25359
|
-
const symbols = extractSymbols(text);
|
|
25360
|
-
const docBlurb = extractModuleBlurb(text);
|
|
25361
|
-
const top = rel.includes("/") ? rel.split("/")[0] : "";
|
|
25362
|
-
const hint = docBlurb ?? (top ? readmeHints.get(top) : void 0) ?? readmeHints.get("");
|
|
25363
|
-
entries.push({
|
|
25453
|
+
files.push({
|
|
25364
25454
|
path: rel,
|
|
25365
|
-
hash,
|
|
25366
|
-
|
|
25367
|
-
...hint ? { blurb: hint.slice(0, BLURB_CAP) } : {}
|
|
25455
|
+
hash: (0, import_node_crypto6.createHash)("sha256").update(text).digest("hex").slice(0, 16),
|
|
25456
|
+
text
|
|
25368
25457
|
});
|
|
25369
25458
|
}
|
|
25459
|
+
return files;
|
|
25460
|
+
}
|
|
25461
|
+
function entryForSource(file, readmeHints) {
|
|
25462
|
+
const docBlurb = extractModuleBlurb(file.text);
|
|
25463
|
+
const top = file.path.includes("/") ? file.path.split("/")[0] : "";
|
|
25464
|
+
const hint = docBlurb ?? (top ? readmeHints.get(top) : void 0) ?? readmeHints.get("");
|
|
25465
|
+
return {
|
|
25466
|
+
path: file.path,
|
|
25467
|
+
hash: file.hash,
|
|
25468
|
+
symbols: extractSymbols(file.text),
|
|
25469
|
+
...hint ? { blurb: hint.slice(0, BLURB_CAP) } : {}
|
|
25470
|
+
};
|
|
25471
|
+
}
|
|
25472
|
+
function buildRepoIndexProjection(cwd, repoSlug3, deadline) {
|
|
25473
|
+
const files = sourceFilesForIndex(cwd, deadline);
|
|
25474
|
+
assertRefreshBudget(deadline);
|
|
25475
|
+
const readmeHints = loadReadmeHints(cwd, files.map((file) => file.path));
|
|
25476
|
+
const entries = files.map((file) => {
|
|
25477
|
+
assertRefreshBudget(deadline);
|
|
25478
|
+
return entryForSource(file, readmeHints);
|
|
25479
|
+
});
|
|
25370
25480
|
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
25371
|
-
const
|
|
25481
|
+
const exec = deadline === void 0 ? import_node_child_process12.execFileSync : deadlineExec(deadline);
|
|
25482
|
+
const indexHead = gitHead(cwd, exec);
|
|
25483
|
+
return {
|
|
25372
25484
|
schema: REPO_INDEX_SCHEMA,
|
|
25373
25485
|
repo: repoSlug3,
|
|
25374
25486
|
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25487
|
+
...indexHead ? { indexHead } : {},
|
|
25375
25488
|
entries
|
|
25376
25489
|
};
|
|
25490
|
+
}
|
|
25491
|
+
function writeRepoIndex(cwd, projection) {
|
|
25377
25492
|
const store = repoIndexStorePath(cwd);
|
|
25378
25493
|
(0, import_node_fs24.mkdirSync)((0, import_node_path21.dirname)(store), { recursive: true });
|
|
25379
25494
|
(0, import_node_fs24.writeFileSync)(store, `${JSON.stringify(projection, null, 2)}
|
|
25380
25495
|
`, "utf8");
|
|
25496
|
+
}
|
|
25497
|
+
function rebuildRepoIndex(cwd, repoSlug3) {
|
|
25498
|
+
const projection = buildRepoIndexProjection(cwd, repoSlug3);
|
|
25499
|
+
writeRepoIndex(cwd, projection);
|
|
25381
25500
|
return projection;
|
|
25382
25501
|
}
|
|
25502
|
+
function refreshRepoIndex(cwd, repoSlug3, options = {}) {
|
|
25503
|
+
const budgetMs = options.budgetMs ?? LOCAL_REPO_INDEX_REFRESH_BUDGET_MS;
|
|
25504
|
+
const deadline = Date.now() + Math.max(0, budgetMs);
|
|
25505
|
+
try {
|
|
25506
|
+
const exec = deadlineExec(deadline);
|
|
25507
|
+
const existing = loadRepoIndex(cwd);
|
|
25508
|
+
if (!existing) {
|
|
25509
|
+
const projection2 = buildRepoIndexProjection(cwd, repoSlug3, deadline);
|
|
25510
|
+
writeRepoIndex(cwd, projection2);
|
|
25511
|
+
return { status: "rebuilt", projection: projection2 };
|
|
25512
|
+
}
|
|
25513
|
+
const head = gitHead(cwd, exec);
|
|
25514
|
+
if (!head) return { status: "stale", reason: "checkout HEAD is unavailable" };
|
|
25515
|
+
const worktreeDirty = String(exec("git", ["status", "--porcelain", "--untracked-files=normal"], {
|
|
25516
|
+
cwd,
|
|
25517
|
+
encoding: "utf8",
|
|
25518
|
+
windowsHide: true
|
|
25519
|
+
})).length > 0;
|
|
25520
|
+
if (existing.indexHead === head && !worktreeDirty) return { status: "refreshed", projection: existing };
|
|
25521
|
+
if (!existing.indexHead) {
|
|
25522
|
+
const projection2 = buildRepoIndexProjection(cwd, repoSlug3, deadline);
|
|
25523
|
+
writeRepoIndex(cwd, projection2);
|
|
25524
|
+
return { status: "rebuilt", projection: projection2 };
|
|
25525
|
+
}
|
|
25526
|
+
const files = sourceFilesForIndex(cwd, deadline);
|
|
25527
|
+
const current = new Map(files.map((file) => [file.path, file]));
|
|
25528
|
+
const previous = new Map(existing.entries.map((entry) => [entry.path, entry]));
|
|
25529
|
+
const changed = /* @__PURE__ */ new Set();
|
|
25530
|
+
for (const [path2, file] of current) {
|
|
25531
|
+
if (previous.get(path2)?.hash !== file.hash) changed.add(path2);
|
|
25532
|
+
}
|
|
25533
|
+
for (const path2 of previous.keys()) {
|
|
25534
|
+
if (!current.has(path2)) changed.add(path2);
|
|
25535
|
+
}
|
|
25536
|
+
const readmeChanged = [...changed].some((path2) => path2 === "README.md" || /^[^/]+\/README\.md$/.test(path2));
|
|
25537
|
+
if (changed.size > LOCAL_REPO_INDEX_DELTA_FILE_LIMIT || readmeChanged) {
|
|
25538
|
+
const projection2 = buildRepoIndexProjection(cwd, repoSlug3, deadline);
|
|
25539
|
+
writeRepoIndex(cwd, projection2);
|
|
25540
|
+
return { status: "rebuilt", projection: projection2 };
|
|
25541
|
+
}
|
|
25542
|
+
const changedSources = [...changed].map((path2) => current.get(path2)).filter((file) => file !== void 0);
|
|
25543
|
+
const needsHint = changedSources.some((file) => !extractModuleBlurb(file.text));
|
|
25544
|
+
const readmeHints = needsHint ? loadReadmeHints(cwd, files.map((file) => file.path)) : /* @__PURE__ */ new Map();
|
|
25545
|
+
const entries = files.map((file) => {
|
|
25546
|
+
assertRefreshBudget(deadline);
|
|
25547
|
+
const old = previous.get(file.path);
|
|
25548
|
+
return old?.hash === file.hash ? old : entryForSource(file, readmeHints);
|
|
25549
|
+
});
|
|
25550
|
+
entries.sort((a, b) => a.path.localeCompare(b.path));
|
|
25551
|
+
const projection = {
|
|
25552
|
+
...existing,
|
|
25553
|
+
repo: repoSlug3,
|
|
25554
|
+
builtAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25555
|
+
indexHead: head,
|
|
25556
|
+
entries
|
|
25557
|
+
};
|
|
25558
|
+
writeRepoIndex(cwd, projection);
|
|
25559
|
+
return { status: "refreshed", projection };
|
|
25560
|
+
} catch (error) {
|
|
25561
|
+
const reason = error instanceof RepoIndexRefreshBudgetError ? error.message : `local index refresh failed: ${error.message}`;
|
|
25562
|
+
return { status: "stale", reason };
|
|
25563
|
+
}
|
|
25564
|
+
}
|
|
25383
25565
|
function loadRepoIndex(cwd) {
|
|
25384
25566
|
const store = repoIndexStorePath(cwd);
|
|
25385
25567
|
if (!(0, import_node_fs24.existsSync)(store)) return null;
|
|
@@ -25396,6 +25578,7 @@ function repoIndexStatus(cwd) {
|
|
|
25396
25578
|
const idx = loadRepoIndex(cwd);
|
|
25397
25579
|
if (!idx) return { present: false, path: path2 };
|
|
25398
25580
|
const symbolCount = idx.entries.reduce((n, e) => n + e.symbols.length, 0);
|
|
25581
|
+
const localUsage = readLocalRepoIndexQueryUsage(cwd);
|
|
25399
25582
|
return {
|
|
25400
25583
|
present: true,
|
|
25401
25584
|
path: path2,
|
|
@@ -25403,7 +25586,8 @@ function repoIndexStatus(cwd) {
|
|
|
25403
25586
|
repo: idx.repo,
|
|
25404
25587
|
builtAt: idx.builtAt,
|
|
25405
25588
|
fileCount: idx.entries.length,
|
|
25406
|
-
symbolCount
|
|
25589
|
+
symbolCount,
|
|
25590
|
+
...localUsage ? { localQueryCount: localUsage.count } : {}
|
|
25407
25591
|
};
|
|
25408
25592
|
}
|
|
25409
25593
|
function searchRepoIndex(idx, query, limit = 20) {
|
|
@@ -26197,9 +26381,9 @@ async function probeRepoIndexV4ReadinessCloud(queries, deps) {
|
|
|
26197
26381
|
if (!token) return { ok: false, error: "no Hub session token (run `gh auth login`)" };
|
|
26198
26382
|
const baseUrl = deps.baseUrl.replace(/\/$/, "");
|
|
26199
26383
|
const headers = { ...clientVersionHeaders(), Authorization: ["Bearer", token].join(" "), "content-type": "application/json" };
|
|
26200
|
-
const shadowPass = async (readinessProbe) => {
|
|
26384
|
+
const shadowPass = async (readinessProbe, passQueries = queries) => {
|
|
26201
26385
|
const ms = {};
|
|
26202
|
-
for (const query of
|
|
26386
|
+
for (const query of passQueries) {
|
|
26203
26387
|
const res = await fetchWithRetry(deps.fetch ?? fetch, `${baseUrl}/repo-index/v4/shadow`, {
|
|
26204
26388
|
method: "POST",
|
|
26205
26389
|
headers,
|
|
@@ -26221,6 +26405,11 @@ async function probeRepoIndexV4ReadinessCloud(queries, deps) {
|
|
|
26221
26405
|
}
|
|
26222
26406
|
const verdict = await shadowPass(true);
|
|
26223
26407
|
if (!verdict.ok) return verdict;
|
|
26408
|
+
const latencyRetries = queries.filter((query) => query.latencyMs !== void 0 && verdict.ms[query.id] !== void 0 && verdict.ms[query.id] > query.latencyMs);
|
|
26409
|
+
if (latencyRetries.length) {
|
|
26410
|
+
const retry = await shadowPass(true, latencyRetries);
|
|
26411
|
+
if (!retry.ok) return retry;
|
|
26412
|
+
}
|
|
26224
26413
|
const res = await fetchWithRetry(deps.fetch ?? fetch, `${baseUrl}/repo-index/status`, {
|
|
26225
26414
|
method: "GET",
|
|
26226
26415
|
headers: { ...clientVersionHeaders(), Authorization: ["Bearer", token].join(" ") }
|
|
@@ -26750,9 +26939,9 @@ function buildGraphEdges(cwd, repo, commit, rosterRepos2) {
|
|
|
26750
26939
|
}
|
|
26751
26940
|
|
|
26752
26941
|
// src/repo-index-sync.ts
|
|
26753
|
-
function execFileUtf8(file, args) {
|
|
26942
|
+
function execFileUtf8(file, args, env) {
|
|
26754
26943
|
return new Promise((resolve6, reject) => {
|
|
26755
|
-
(0, import_node_child_process15.execFile)(file, args, { encoding: "utf8", windowsHide: true }, (error, stdout) => {
|
|
26944
|
+
(0, import_node_child_process15.execFile)(file, args, { encoding: "utf8", windowsHide: true, ...env ? { env } : {} }, (error, stdout) => {
|
|
26756
26945
|
if (error) reject(error);
|
|
26757
26946
|
else resolve6(String(stdout ?? ""));
|
|
26758
26947
|
});
|
|
@@ -26760,6 +26949,9 @@ function execFileUtf8(file, args) {
|
|
|
26760
26949
|
}
|
|
26761
26950
|
var ESTATE_CLASSIFY_CONCURRENCY = 8;
|
|
26762
26951
|
var ESTATE_HEALTHY_CLASSIFY_BUDGET_MS = 5 * 601e3;
|
|
26952
|
+
function formatEstateRunFailureSummary(failed) {
|
|
26953
|
+
return failed.map((f) => `${f.repo} (${f.probe?.failureClass ?? "failed"})`).join(", ");
|
|
26954
|
+
}
|
|
26763
26955
|
var COMMIT3 = /^[a-f0-9]{40}$/;
|
|
26764
26956
|
var SHA256 = /^[a-f0-9]{64}$/;
|
|
26765
26957
|
var UTC_MILLIS = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
@@ -26795,14 +26987,57 @@ function checkoutExactCommit(repo, dest, token, commit) {
|
|
|
26795
26987
|
const head = git3(["rev-parse", "HEAD"]).trim().toLowerCase();
|
|
26796
26988
|
if (head !== commit) throw new Error(`checkout of ${repo} resolved ${head}, not the requested commit ${commit}`);
|
|
26797
26989
|
}
|
|
26990
|
+
var HeadProbeError = class extends Error {
|
|
26991
|
+
constructor(message, exitCode, failureClass) {
|
|
26992
|
+
super(message);
|
|
26993
|
+
this.exitCode = exitCode;
|
|
26994
|
+
this.failureClass = failureClass;
|
|
26995
|
+
}
|
|
26996
|
+
exitCode;
|
|
26997
|
+
failureClass;
|
|
26998
|
+
};
|
|
26999
|
+
function classifyProbeStderr(stderr) {
|
|
27000
|
+
const text = stderr.toLowerCase();
|
|
27001
|
+
if (/(could not read username|could not read password|authentication failed|invalid credentials|permission denied|terminal prompts disabled|returned error: 401|returned error: 403)/.test(text)) return "auth-denied";
|
|
27002
|
+
if (/(repository .* not found|couldn't find remote ref|not found)/.test(text)) return "not-found";
|
|
27003
|
+
if (/(timed out|timeout|operation timed out|connection timed out)/.test(text)) return "timeout";
|
|
27004
|
+
if (/(could not resolve host|network is unreachable|connection (refused|reset)|temporary failure|failed to connect|could not connect)/.test(text)) return "network";
|
|
27005
|
+
return "unreadable";
|
|
27006
|
+
}
|
|
27007
|
+
function headProbeEnv() {
|
|
27008
|
+
const env = { ...process.env };
|
|
27009
|
+
delete env.GIT_ASKPASS;
|
|
27010
|
+
delete env.SSH_ASKPASS;
|
|
27011
|
+
for (const key of Object.keys(env)) {
|
|
27012
|
+
if (key.startsWith("GIT_CONFIG_")) delete env[key];
|
|
27013
|
+
}
|
|
27014
|
+
env.GIT_TERMINAL_PROMPT = "0";
|
|
27015
|
+
return env;
|
|
27016
|
+
}
|
|
27017
|
+
function toHeadProbeError(error, repo) {
|
|
27018
|
+
const e = error;
|
|
27019
|
+
const exitCode = typeof e.code === "number" ? e.code : null;
|
|
27020
|
+
const stderr = typeof e.stderr === "string" ? e.stderr : "";
|
|
27021
|
+
return new HeadProbeError(
|
|
27022
|
+
`remote HEAD probe of ${repo} failed (exit ${exitCode ?? "unknown"}, ${classifyProbeStderr(stderr)})`,
|
|
27023
|
+
exitCode,
|
|
27024
|
+
classifyProbeStderr(stderr)
|
|
27025
|
+
);
|
|
27026
|
+
}
|
|
26798
27027
|
async function remoteHead(repo, token) {
|
|
26799
27028
|
const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
|
|
26800
|
-
|
|
26801
|
-
|
|
26802
|
-
|
|
26803
|
-
|
|
27029
|
+
let stdout;
|
|
27030
|
+
try {
|
|
27031
|
+
stdout = await execFileUtf8(
|
|
27032
|
+
"git",
|
|
27033
|
+
["-c", "credential.helper=", "-c", `http.extraHeader=Authorization: Basic ${basic}`, "ls-remote", "--exit-code", `https://github.com/${repo}.git`, "HEAD"],
|
|
27034
|
+
headProbeEnv()
|
|
27035
|
+
);
|
|
27036
|
+
} catch (error) {
|
|
27037
|
+
throw toHeadProbeError(error, repo);
|
|
27038
|
+
}
|
|
26804
27039
|
const match = stdout.match(/^([a-f0-9]{40})\s+HEAD$/m);
|
|
26805
|
-
if (!match) throw new
|
|
27040
|
+
if (!match) throw new HeadProbeError(`could not resolve remote HEAD for ${repo}`, null, "unreadable");
|
|
26806
27041
|
return match[1];
|
|
26807
27042
|
}
|
|
26808
27043
|
async function mapLimit(items, limit, fn) {
|
|
@@ -26871,6 +27106,7 @@ async function syncEstateRepoIndex(opts) {
|
|
|
26871
27106
|
const skipped = [];
|
|
26872
27107
|
const drift = [];
|
|
26873
27108
|
const needsFullRebuild = [];
|
|
27109
|
+
let rosterSize = 0;
|
|
26874
27110
|
const answer = (ok) => ({
|
|
26875
27111
|
ok: ok ?? failed.length === 0,
|
|
26876
27112
|
published,
|
|
@@ -26878,7 +27114,12 @@ async function syncEstateRepoIndex(opts) {
|
|
|
26878
27114
|
skipped,
|
|
26879
27115
|
provenanceToken: CURRENT_REPO_INDEX_PROVENANCE_TOKEN,
|
|
26880
27116
|
drift,
|
|
26881
|
-
needsFullRebuild
|
|
27117
|
+
needsFullRebuild,
|
|
27118
|
+
receipt: {
|
|
27119
|
+
changed: published.length,
|
|
27120
|
+
skipped: Math.max(0, rosterSize - published.length - failed.length),
|
|
27121
|
+
failed: failed.length
|
|
27122
|
+
}
|
|
26882
27123
|
});
|
|
26883
27124
|
const fullRebuildToken = (opts.fullRebuild ?? "").trim();
|
|
26884
27125
|
const forceFull = fullRebuildToken !== "";
|
|
@@ -26918,6 +27159,7 @@ async function syncEstateRepoIndex(opts) {
|
|
|
26918
27159
|
const busy = new Set(
|
|
26919
27160
|
(opts.skipRepos ?? []).map((repo) => normalizeRepoIndexRepo(repo)).filter((repo) => repo !== null)
|
|
26920
27161
|
);
|
|
27162
|
+
rosterSize = repos.length;
|
|
26921
27163
|
const emit = (progress) => {
|
|
26922
27164
|
try {
|
|
26923
27165
|
opts.onClassify?.(progress);
|
|
@@ -26950,12 +27192,12 @@ async function syncEstateRepoIndex(opts) {
|
|
|
26950
27192
|
}
|
|
26951
27193
|
let targetCommit = requestedCommit || void 0;
|
|
26952
27194
|
if (base && !forceFull) {
|
|
26953
|
-
let
|
|
27195
|
+
let headProbe = null;
|
|
26954
27196
|
if (!targetCommit) {
|
|
26955
27197
|
try {
|
|
26956
27198
|
targetCommit = await remoteHead(repo, opts.githubToken);
|
|
26957
|
-
} catch {
|
|
26958
|
-
|
|
27199
|
+
} catch (error) {
|
|
27200
|
+
headProbe = error instanceof HeadProbeError ? error : new HeadProbeError(error.message, null, "unreadable");
|
|
26959
27201
|
}
|
|
26960
27202
|
}
|
|
26961
27203
|
const provenance = await fetchRepoIndexV4ProvenanceCloud(repo, opts.deps).catch(
|
|
@@ -26979,11 +27221,10 @@ async function syncEstateRepoIndex(opts) {
|
|
|
26979
27221
|
emit({ row: row2, warning });
|
|
26980
27222
|
return { repo, row: row2, warning, base, expectedActiveDigest };
|
|
26981
27223
|
}
|
|
26982
|
-
if (
|
|
26983
|
-
const row2 = { repo, reason: "head-unreadable", action: "
|
|
26984
|
-
|
|
26985
|
-
|
|
26986
|
-
return { repo, row: row2, warning, base, expectedActiveDigest };
|
|
27224
|
+
if (headProbe) {
|
|
27225
|
+
const row2 = { repo, reason: "head-unreadable", action: "failed", activeCommit: base.commit };
|
|
27226
|
+
emit({ row: row2 });
|
|
27227
|
+
return { repo, row: row2, headProbe, base, expectedActiveDigest };
|
|
26987
27228
|
}
|
|
26988
27229
|
}
|
|
26989
27230
|
const row = {
|
|
@@ -27000,6 +27241,14 @@ async function syncEstateRepoIndex(opts) {
|
|
|
27000
27241
|
drift.push(entry.row);
|
|
27001
27242
|
if (entry.warning) skipped.push(entry.warning);
|
|
27002
27243
|
if (entry.row.action === "needs-full-rebuild") needsFullRebuild.push(entry.repo);
|
|
27244
|
+
if (entry.headProbe) {
|
|
27245
|
+
const exit = entry.headProbe.exitCode === null ? "unknown" : String(entry.headProbe.exitCode);
|
|
27246
|
+
failed.push({
|
|
27247
|
+
repo: entry.repo,
|
|
27248
|
+
error: `remote HEAD unreadable (exit ${exit}, ${entry.headProbe.failureClass}) \u2014 verified-ready authority at ${entry.row.activeCommit ?? "unknown"} was not re-probed; reconcile FAILED`,
|
|
27249
|
+
probe: { exitCode: entry.headProbe.exitCode, failureClass: entry.headProbe.failureClass }
|
|
27250
|
+
});
|
|
27251
|
+
}
|
|
27003
27252
|
}
|
|
27004
27253
|
if (opts.plan) return answer();
|
|
27005
27254
|
for (const entry of classified) {
|
|
@@ -33451,29 +33700,47 @@ ${err?.stdout ?? ""}`.split("\n").map((line) => line.trim()).find((line) => line
|
|
|
33451
33700
|
var defaultGhMergeAutoIo = {
|
|
33452
33701
|
gh: async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout
|
|
33453
33702
|
};
|
|
33454
|
-
|
|
33703
|
+
function resolveSquashMergeBodyText(commits, allowedClosing, cwd) {
|
|
33704
|
+
const policy = squashBodyWithOverride(commits, cwd);
|
|
33705
|
+
const base = policy ?? defaultSquashBodyFromCommits(commits);
|
|
33706
|
+
if (!base) return null;
|
|
33707
|
+
const rewritten = rewriteUnregisteredClosingKeywords(base, allowedClosing);
|
|
33708
|
+
if (policy) return rewritten;
|
|
33709
|
+
return rewritten === base ? null : rewritten;
|
|
33710
|
+
}
|
|
33711
|
+
function writeSquashBodyFile(body) {
|
|
33712
|
+
const dir = (0, import_node_fs40.mkdtempSync)((0, import_node_path37.join)((0, import_node_os18.tmpdir)(), "mmi-squash-body-"));
|
|
33713
|
+
const path2 = (0, import_node_path37.join)(dir, "body.txt");
|
|
33714
|
+
(0, import_node_fs40.writeFileSync)(path2, body.endsWith("\n") ? body : `${body}
|
|
33715
|
+
`, "utf8");
|
|
33716
|
+
return { path: path2, cleanup: () => {
|
|
33717
|
+
try {
|
|
33718
|
+
(0, import_node_fs40.rmSync)(dir, { recursive: true, force: true });
|
|
33719
|
+
} catch {
|
|
33720
|
+
}
|
|
33721
|
+
} };
|
|
33722
|
+
}
|
|
33723
|
+
async function composeOverrideBodyFile(prNumber, repoArgs, gh, opts) {
|
|
33724
|
+
if (opts?.bodyText != null && opts.bodyText !== "") {
|
|
33725
|
+
return { ...writeSquashBodyFile(opts.bodyText), text: opts.bodyText };
|
|
33726
|
+
}
|
|
33455
33727
|
try {
|
|
33456
33728
|
const raw = await gh(["pr", "view", prNumber, ...repoArgs, "--json", "commits"], GC_GH_TIMEOUT_MS);
|
|
33457
|
-
const commits = JSON.parse(raw).commits ?? [];
|
|
33458
|
-
const body =
|
|
33729
|
+
const commits = (JSON.parse(raw).commits ?? []).map((c) => ({ headline: c.messageHeadline ?? "", body: c.messageBody ?? "" }));
|
|
33730
|
+
const body = resolveSquashMergeBodyText(commits, opts?.allowedClosing ?? [], process.cwd());
|
|
33459
33731
|
if (!body) return void 0;
|
|
33460
|
-
|
|
33461
|
-
const path2 = (0, import_node_path37.join)(dir, "body.txt");
|
|
33462
|
-
(0, import_node_fs40.writeFileSync)(path2, `${body}
|
|
33463
|
-
`, "utf8");
|
|
33464
|
-
return { path: path2, cleanup: () => {
|
|
33465
|
-
try {
|
|
33466
|
-
(0, import_node_fs40.rmSync)(dir, { recursive: true, force: true });
|
|
33467
|
-
} catch {
|
|
33468
|
-
}
|
|
33469
|
-
} };
|
|
33732
|
+
return { ...writeSquashBodyFile(body), text: body };
|
|
33470
33733
|
} catch {
|
|
33471
33734
|
return void 0;
|
|
33472
33735
|
}
|
|
33473
33736
|
}
|
|
33474
|
-
async function ghMergeAutoEnqueue(prNumber, repo, method,
|
|
33737
|
+
async function ghMergeAutoEnqueue(prNumber, repo, method, extra) {
|
|
33738
|
+
const io = extra?.io ?? defaultGhMergeAutoIo;
|
|
33475
33739
|
const args = repo ? ["--repo", repo] : [];
|
|
33476
|
-
const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh
|
|
33740
|
+
const overrideBody = await composeOverrideBodyFile(prNumber, args, io.gh, {
|
|
33741
|
+
allowedClosing: method === "--squash" ? extra?.allowedClosing : void 0,
|
|
33742
|
+
bodyText: method === "--squash" ? extra?.bodyText : void 0
|
|
33743
|
+
});
|
|
33477
33744
|
try {
|
|
33478
33745
|
return await mergeAutoEnqueueWithBody(prNumber, args, method, io, overrideBody?.path);
|
|
33479
33746
|
} finally {
|
|
@@ -33831,34 +34098,60 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
|
|
|
33831
34098
|
return formatGitCommandError(e);
|
|
33832
34099
|
}
|
|
33833
34100
|
};
|
|
33834
|
-
const
|
|
33835
|
-
if (!firstError) {
|
|
33836
|
-
await git3(["worktree", "prune"]).catch(() => "");
|
|
33837
|
-
return { status: "removed" };
|
|
33838
|
-
}
|
|
33839
|
-
let worktrees = await listWorktrees().catch(() => []);
|
|
33840
|
-
if (!isWorktreePathRegistered(worktrees, wtPath)) {
|
|
34101
|
+
const pruneAndVerify = async (reason) => {
|
|
33841
34102
|
await git3(["worktree", "prune"]).catch(() => "");
|
|
33842
|
-
|
|
34103
|
+
let worktrees2;
|
|
34104
|
+
try {
|
|
34105
|
+
worktrees2 = await listWorktrees();
|
|
34106
|
+
} catch (e) {
|
|
34107
|
+
return {
|
|
34108
|
+
status: "failed",
|
|
34109
|
+
error: `could not verify worktree removal after prune: ${formatGitCommandError(e)}; path=${wtPath}`
|
|
34110
|
+
};
|
|
34111
|
+
}
|
|
34112
|
+
if (isWorktreePathRegistered(worktrees2, wtPath)) {
|
|
34113
|
+
return {
|
|
34114
|
+
status: "failed",
|
|
34115
|
+
error: `worktree remains registered after prune; path=${wtPath}`
|
|
34116
|
+
};
|
|
34117
|
+
}
|
|
34118
|
+
return { status: "removed", ...reason ? { reason } : {} };
|
|
34119
|
+
};
|
|
34120
|
+
const firstError = await attemptRemove();
|
|
34121
|
+
if (!firstError) return pruneAndVerify();
|
|
34122
|
+
let worktrees;
|
|
34123
|
+
try {
|
|
34124
|
+
worktrees = await listWorktrees();
|
|
34125
|
+
} catch (e) {
|
|
34126
|
+
return {
|
|
34127
|
+
status: "failed",
|
|
34128
|
+
error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
|
|
34129
|
+
};
|
|
33843
34130
|
}
|
|
34131
|
+
if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-unregistered");
|
|
33844
34132
|
const retryError = await attemptRemove();
|
|
33845
|
-
if (!retryError)
|
|
33846
|
-
await git3(["worktree", "prune"]).catch(() => "");
|
|
33847
|
-
return { status: "removed", reason: "reconciled-after-retry" };
|
|
33848
|
-
}
|
|
34133
|
+
if (!retryError) return pruneAndVerify("reconciled-after-retry");
|
|
33849
34134
|
if (isNotAWorkingTreeMessage(retryError) || isNotAWorkingTreeMessage(firstError)) {
|
|
33850
|
-
|
|
33851
|
-
|
|
33852
|
-
|
|
33853
|
-
return {
|
|
34135
|
+
try {
|
|
34136
|
+
worktrees = await listWorktrees();
|
|
34137
|
+
} catch (e) {
|
|
34138
|
+
return {
|
|
34139
|
+
status: "failed",
|
|
34140
|
+
error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
|
|
34141
|
+
};
|
|
33854
34142
|
}
|
|
34143
|
+
if (!isWorktreePathRegistered(worktrees, wtPath)) return pruneAndVerify("reconciled-not-a-working-tree");
|
|
33855
34144
|
}
|
|
33856
|
-
|
|
33857
|
-
|
|
33858
|
-
|
|
33859
|
-
|
|
33860
|
-
|
|
34145
|
+
try {
|
|
34146
|
+
worktrees = await listWorktrees();
|
|
34147
|
+
} catch (e) {
|
|
34148
|
+
return {
|
|
34149
|
+
status: "failed",
|
|
34150
|
+
error: `could not verify worktree registration after remove failure: ${formatGitCommandError(e)}; path=${wtPath}`
|
|
34151
|
+
};
|
|
33861
34152
|
}
|
|
34153
|
+
const registered = isWorktreePathRegistered(worktrees, wtPath);
|
|
34154
|
+
if (!registered) return pruneAndVerify("reconciled-unregistered");
|
|
33862
34155
|
return {
|
|
33863
34156
|
status: "failed",
|
|
33864
34157
|
error: formatWorktreeRemovalFailureDetail({
|
|
@@ -33869,6 +34162,9 @@ async function removeWorktreeWithReconcile(wtPath, git3, listWorktrees, pathExis
|
|
|
33869
34162
|
})
|
|
33870
34163
|
};
|
|
33871
34164
|
}
|
|
34165
|
+
function prMergeLocalCleanupExitCode(cleanup) {
|
|
34166
|
+
return cleanup?.worktree?.status === "failed" || cleanup?.localBranch?.status === "failed" ? 1 : void 0;
|
|
34167
|
+
}
|
|
33872
34168
|
async function cleanupPrMergeLocalBranch(branch, options) {
|
|
33873
34169
|
const report = { branch };
|
|
33874
34170
|
if (!branch) {
|
|
@@ -33975,7 +34271,7 @@ async function cleanupPrMergeLocalBranch(branch, options) {
|
|
|
33975
34271
|
const removal = await removeWorktreeWithReconcile(
|
|
33976
34272
|
wtPath,
|
|
33977
34273
|
git3,
|
|
33978
|
-
async () => parseGitWorktreePorcelain(await
|
|
34274
|
+
async () => parseGitWorktreePorcelain(await git3(["worktree", "list", "--porcelain"])),
|
|
33979
34275
|
pathExists
|
|
33980
34276
|
);
|
|
33981
34277
|
if (removal.status === "failed") {
|
|
@@ -41334,11 +41630,29 @@ async function runRepoIndexSearchCommand(query, o, defaultMode) {
|
|
|
41334
41630
|
const useLocal = o.local === true && o.cloud !== true;
|
|
41335
41631
|
if (useLocal) {
|
|
41336
41632
|
const root = await repoRoot();
|
|
41337
|
-
|
|
41338
|
-
if (!
|
|
41633
|
+
const refreshed = refreshRepoIndex(root, inferRepoSlug(root));
|
|
41634
|
+
if (!refreshed.projection) {
|
|
41635
|
+
const receipt = {
|
|
41636
|
+
ok: false,
|
|
41637
|
+
source: "local",
|
|
41638
|
+
repo: inferRepoSlug(root),
|
|
41639
|
+
query,
|
|
41640
|
+
hits: [],
|
|
41641
|
+
status: refreshed.status,
|
|
41642
|
+
reason: refreshed.reason ?? "local index refresh failed"
|
|
41643
|
+
};
|
|
41644
|
+
if (o.json) consoleIo.log(JSON.stringify(receipt, null, 2));
|
|
41645
|
+
else console.error(`repo-index: local index stale \u2014 ${receipt.reason}`);
|
|
41646
|
+
return await cleanExit(1);
|
|
41647
|
+
}
|
|
41648
|
+
const idx = refreshed.projection;
|
|
41339
41649
|
const hits = searchRepoIndex(idx, query, limit);
|
|
41650
|
+
try {
|
|
41651
|
+
recordLocalRepoIndexQueryUsage(root);
|
|
41652
|
+
} catch {
|
|
41653
|
+
}
|
|
41340
41654
|
if (o.json) {
|
|
41341
|
-
consoleIo.log(JSON.stringify({ ok: true, source: "local", repo: idx.repo, query, hits }, null, 2));
|
|
41655
|
+
consoleIo.log(JSON.stringify({ ok: true, source: "local", repo: idx.repo, query, hits, status: refreshed.status }, null, 2));
|
|
41342
41656
|
return;
|
|
41343
41657
|
}
|
|
41344
41658
|
if (hits.length === 0) {
|
|
@@ -41585,7 +41899,10 @@ repoIndex.command("sync-estate").description("Hub indexer: publish a pushed comm
|
|
|
41585
41899
|
for (const f of res.failed) {
|
|
41586
41900
|
console.error(`repo-index: FAILED ${f.repo}: ${f.error}`);
|
|
41587
41901
|
}
|
|
41588
|
-
|
|
41902
|
+
console.log(`repo-index: estate reconcile receipt \u2014 ${res.receipt.changed} changed, ${res.receipt.skipped} skipped, ${res.receipt.failed} failed`);
|
|
41903
|
+
if (!res.ok) {
|
|
41904
|
+
await failGraceful(`sync-estate incomplete (${res.failed.length} failed): ${formatEstateRunFailureSummary(res.failed)}`);
|
|
41905
|
+
}
|
|
41589
41906
|
} catch (e) {
|
|
41590
41907
|
await failGraceful(e.message);
|
|
41591
41908
|
}
|
|
@@ -42839,6 +43156,20 @@ async function readClosingGuardInput(number, repoArgs, repo, context) {
|
|
|
42839
43156
|
return input;
|
|
42840
43157
|
}
|
|
42841
43158
|
}
|
|
43159
|
+
function squashBodyTextForMerge(input, methodIsSquash, explicitBody) {
|
|
43160
|
+
if (!methodIsSquash) return void 0;
|
|
43161
|
+
if (explicitBody != null && explicitBody !== "") return explicitBody;
|
|
43162
|
+
if (!input) return void 0;
|
|
43163
|
+
return resolveSquashMergeBodyText(input.commits ?? [], input.closing, process.cwd()) ?? void 0;
|
|
43164
|
+
}
|
|
43165
|
+
function warnSquashBodyClosingStrip(context, input, squashBodyText) {
|
|
43166
|
+
if (!input || squashBodyText == null) return;
|
|
43167
|
+
const { open: open2 } = partitionCommitClosings(input.commitClosing ?? [], input.closing, input.alreadyClosed);
|
|
43168
|
+
if (!open2.length) return;
|
|
43169
|
+
console.warn(
|
|
43170
|
+
`${context}: composing a squash body that leaves unregistered ${open2.map((n) => `#${n}`).join(", ")} open \u2014 GitHub's default COMMIT_MESSAGES composition would close them (#5723).`
|
|
43171
|
+
);
|
|
43172
|
+
}
|
|
42842
43173
|
function ciAuditDeps() {
|
|
42843
43174
|
const cfgPromise = loadConfig();
|
|
42844
43175
|
const root = hubRoot();
|
|
@@ -42988,7 +43319,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
42988
43319
|
return typeof viewed.state === "string" ? viewed.state : void 0;
|
|
42989
43320
|
}
|
|
42990
43321
|
);
|
|
42991
|
-
const
|
|
43322
|
+
const landSquashBody = squashBodyTextForMerge(landClosingGuardInput, true);
|
|
43323
|
+
warnSquashBodyClosingStrip("pr land", landClosingGuardInput, landSquashBody);
|
|
43324
|
+
const landClosingGuardVerdict = evaluateClosingGuard(landClosingGuardInput, {
|
|
43325
|
+
force: o.force,
|
|
43326
|
+
context: "pr land",
|
|
43327
|
+
squashBodyText: landSquashBody
|
|
43328
|
+
});
|
|
42992
43329
|
if (landClosingGuardVerdict.blocked) {
|
|
42993
43330
|
console.error(landClosingGuardVerdict.message);
|
|
42994
43331
|
process.exitCode = 1;
|
|
@@ -43063,7 +43400,10 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
43063
43400
|
if (!result2.ok) throw new Error(`could not read PR state: ${result2.error}`);
|
|
43064
43401
|
return JSON.parse(result2.state);
|
|
43065
43402
|
},
|
|
43066
|
-
mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash"
|
|
43403
|
+
mergeAuto: (prNumber, repo) => ghMergeAutoEnqueue(prNumber, repo, "--squash", {
|
|
43404
|
+
allowedClosing: landClosingGuardInput?.closing,
|
|
43405
|
+
bodyText: landSquashBody
|
|
43406
|
+
}),
|
|
43067
43407
|
pollMerged: async (prNumber, repo, deadlineMs) => {
|
|
43068
43408
|
let lastFailure;
|
|
43069
43409
|
while (Date.now() < deadlineMs) {
|
|
@@ -43124,7 +43464,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
43124
43464
|
else printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
43125
43465
|
if (result.status === "failed") process.exitCode = 1;
|
|
43126
43466
|
});
|
|
43127
|
-
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)").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("--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) => {
|
|
43467
|
+
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)").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) => {
|
|
43128
43468
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
43129
43469
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
43130
43470
|
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
@@ -43147,7 +43487,20 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
43147
43487
|
return typeof viewed.state === "string" ? viewed.state : void 0;
|
|
43148
43488
|
}
|
|
43149
43489
|
);
|
|
43150
|
-
|
|
43490
|
+
if (o.squashBodyFile && method !== "--squash") {
|
|
43491
|
+
throw new Error("pr merge: --squash-body-file applies only to squash merges");
|
|
43492
|
+
}
|
|
43493
|
+
const mergeSquashBody = squashBodyTextForMerge(
|
|
43494
|
+
closingGuardInput,
|
|
43495
|
+
method === "--squash",
|
|
43496
|
+
o.squashBodyFile ? (0, import_node_fs48.readFileSync)(o.squashBodyFile, "utf8") : void 0
|
|
43497
|
+
);
|
|
43498
|
+
if (!o.squashBodyFile) warnSquashBodyClosingStrip("pr merge", closingGuardInput, mergeSquashBody);
|
|
43499
|
+
const closingGuardVerdict = evaluateClosingGuard(closingGuardInput, {
|
|
43500
|
+
force: o.force,
|
|
43501
|
+
context: "pr merge",
|
|
43502
|
+
squashBodyText: mergeSquashBody
|
|
43503
|
+
});
|
|
43151
43504
|
if (closingGuardVerdict.blocked) {
|
|
43152
43505
|
console.error(closingGuardVerdict.message);
|
|
43153
43506
|
process.exitCode = 1;
|
|
@@ -43202,7 +43555,12 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
43202
43555
|
const remoteBefore = await remoteBranchExists2(headRef);
|
|
43203
43556
|
let upgradedToAuto = false;
|
|
43204
43557
|
let remoteNotAttemptedReason = "host-owned-lifecycle";
|
|
43205
|
-
const overrideBody =
|
|
43558
|
+
const overrideBody = mergeSquashBody ? { ...writeSquashBodyFile(mergeSquashBody), text: mergeSquashBody } : await composeOverrideBodyFile(
|
|
43559
|
+
number,
|
|
43560
|
+
repoArgs,
|
|
43561
|
+
async (a, t) => (await execFileP2("gh", a, { timeout: t })).stdout,
|
|
43562
|
+
{ allowedClosing: method === "--squash" ? closingGuardInput?.closing : void 0 }
|
|
43563
|
+
);
|
|
43206
43564
|
const bodyFile = overrideBody?.path;
|
|
43207
43565
|
try {
|
|
43208
43566
|
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto, deleteBranch: false, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch(async (e) => {
|
|
@@ -43387,7 +43745,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
43387
43745
|
})) {
|
|
43388
43746
|
console.error(line);
|
|
43389
43747
|
}
|
|
43390
|
-
process.exitCode = postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
|
|
43748
|
+
process.exitCode = prMergeLocalCleanupExitCode(localCleanup) ?? postMergeReconExitCode({ boardAdvance, crossRepoFilingIssue }) ?? process.exitCode;
|
|
43391
43749
|
});
|
|
43392
43750
|
registerQueryCommands(program2);
|
|
43393
43751
|
registerIssueLifecycleCommands(program2, { attach: attachToProject });
|
package/package.json
CHANGED