@mutmutco/cli 3.64.0 → 3.66.0
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 +262 -49
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -5165,16 +5165,20 @@ function validateDeadWorktreeDirContent(planned, inspected) {
|
|
|
5165
5165
|
return { ok: false, reason: "unknown dead worktree cleanup kind" };
|
|
5166
5166
|
}
|
|
5167
5167
|
function validateBranchCleanupHead(planned, currentHeads) {
|
|
5168
|
+
const currentHeadOid = currentHeads.find((h) => h.branch === planned.branch)?.oid;
|
|
5169
|
+
if (planned.containedInBase && planned.plannedHeadOid) {
|
|
5170
|
+
if (!currentHeadOid) return { ok: false, reason: "local branch head could not be read" };
|
|
5171
|
+
return currentHeadOid === planned.plannedHeadOid ? { ok: true } : { ok: false, reason: `branch moved since planning (${currentHeadOid} != ${planned.plannedHeadOid})` };
|
|
5172
|
+
}
|
|
5168
5173
|
const reviewedHeadOids = planned.reviewedHeadOids?.filter(Boolean) ?? [];
|
|
5169
5174
|
if (!reviewedHeadOids.length) {
|
|
5170
5175
|
return { ok: false, reason: "planned branch head was not verified against a PR head" };
|
|
5171
5176
|
}
|
|
5172
|
-
|
|
5173
|
-
if (!currentHead) {
|
|
5177
|
+
if (!currentHeadOid) {
|
|
5174
5178
|
return { ok: false, reason: "local branch head could not be verified against the PR head" };
|
|
5175
5179
|
}
|
|
5176
|
-
if (!reviewedHeadOids.includes(
|
|
5177
|
-
return { ok: false, reason: `${
|
|
5180
|
+
if (!reviewedHeadOids.includes(currentHeadOid)) {
|
|
5181
|
+
return { ok: false, reason: `${currentHeadOid} != ${reviewedHeadOids.join("|")}` };
|
|
5178
5182
|
}
|
|
5179
5183
|
return { ok: true };
|
|
5180
5184
|
}
|
|
@@ -5274,6 +5278,7 @@ function buildGcPlan(inputs) {
|
|
|
5274
5278
|
const unpushedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.unpushed).map((w) => w.branch));
|
|
5275
5279
|
const preservedBranches2 = new Set(inputs.preservedBranches ?? []);
|
|
5276
5280
|
const preservedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.preserved).map((w) => w.branch));
|
|
5281
|
+
const mergedIntoBase = new Set((inputs.mergedIntoBase ?? []).map((b) => b.trim()).filter(Boolean));
|
|
5277
5282
|
const skipped = [];
|
|
5278
5283
|
const branches = [];
|
|
5279
5284
|
const skipTrackingBranches = /* @__PURE__ */ new Set();
|
|
@@ -5313,7 +5318,8 @@ function buildGcPlan(inputs) {
|
|
|
5313
5318
|
continue;
|
|
5314
5319
|
}
|
|
5315
5320
|
const localHead = branchHeads?.get(branch);
|
|
5316
|
-
|
|
5321
|
+
const containedInBase = mergedIntoBase.has(branch);
|
|
5322
|
+
if (!containedInBase && (worktree2?.unpushed || branchHeads && (!localHead || !state.headOids.length || !state.headOids.includes(localHead)))) {
|
|
5317
5323
|
const detail = worktree2?.unpushed ? worktree2.path : localHead && state.headOids.length ? `${localHead} != ${state.headOids.join("|")}` : "local branch head could not be verified against the PR head";
|
|
5318
5324
|
skipped.push({ branch, reason: "unpushed-branch", detail });
|
|
5319
5325
|
skipTrackingBranches.add(branch);
|
|
@@ -5324,7 +5330,8 @@ function buildGcPlan(inputs) {
|
|
|
5324
5330
|
prState: state.state,
|
|
5325
5331
|
prNumbers: state.numbers,
|
|
5326
5332
|
worktreePath: worktree2?.path,
|
|
5327
|
-
...state.headOids.length ? { reviewedHeadOids: state.headOids } : {}
|
|
5333
|
+
...state.headOids.length ? { reviewedHeadOids: state.headOids } : {},
|
|
5334
|
+
...containedInBase ? { containedInBase: true, ...localHead ? { plannedHeadOid: localHead } : {} } : {}
|
|
5328
5335
|
});
|
|
5329
5336
|
}
|
|
5330
5337
|
const trackingRefs = [...new Set(inputs.staleTrackingRefs ?? [])].map((ref) => {
|
|
@@ -6121,6 +6128,32 @@ var DEFAULT_SURFACE = "claude";
|
|
|
6121
6128
|
function activityLogPath(cwd) {
|
|
6122
6129
|
return repoRuntimeStatePath(cwd, "hooks", "activity.jsonl");
|
|
6123
6130
|
}
|
|
6131
|
+
var REDACTOR_WINDOW_MS = 48 * 60 * 60 * 1e3;
|
|
6132
|
+
var REDACTOR_SCAN_BYTES = 512 * 1024;
|
|
6133
|
+
function redactorLivenessProbe(cwd, now = /* @__PURE__ */ new Date()) {
|
|
6134
|
+
try {
|
|
6135
|
+
const raw = (0, import_node_fs10.readFileSync)(activityLogPath(cwd), "utf8");
|
|
6136
|
+
const tail = raw.length > REDACTOR_SCAN_BYTES ? raw.slice(raw.length - REDACTOR_SCAN_BYTES) : raw;
|
|
6137
|
+
const floor = now.getTime() - REDACTOR_WINDOW_MS;
|
|
6138
|
+
let failed = 0;
|
|
6139
|
+
let lastTs;
|
|
6140
|
+
for (const line of tail.split("\n")) {
|
|
6141
|
+
if (!line.includes('"secret-redact"') || !line.includes('"failed"')) continue;
|
|
6142
|
+
try {
|
|
6143
|
+
const row = JSON.parse(line);
|
|
6144
|
+
if (row.script !== "secret-redact" || row.outcome !== "failed") continue;
|
|
6145
|
+
const ts = row.ts ? Date.parse(row.ts) : Number.NaN;
|
|
6146
|
+
if (Number.isNaN(ts) || ts < floor) continue;
|
|
6147
|
+
failed += 1;
|
|
6148
|
+
if (!lastTs || row.ts > lastTs) lastTs = row.ts;
|
|
6149
|
+
} catch {
|
|
6150
|
+
}
|
|
6151
|
+
}
|
|
6152
|
+
return { failed, ...lastTs ? { lastTs } : {} };
|
|
6153
|
+
} catch {
|
|
6154
|
+
return void 0;
|
|
6155
|
+
}
|
|
6156
|
+
}
|
|
6124
6157
|
function appendHookActivity(cwd, entry) {
|
|
6125
6158
|
try {
|
|
6126
6159
|
const path2 = activityLogPath(cwd);
|
|
@@ -10811,7 +10844,7 @@ function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
|
|
|
10811
10844
|
return explicitRepoWorktreesRoot(explicitRoot, repoRoot2, rootDirs);
|
|
10812
10845
|
}
|
|
10813
10846
|
async function gcPlan(remote, limit, opts = {}) {
|
|
10814
|
-
const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved] = await Promise.all([
|
|
10847
|
+
const [branches, heads, current, stale, prs, worktrees, siblingDirs, preserved, mergedIntoBase] = await Promise.all([
|
|
10815
10848
|
gitOut(["branch", "--format=%(refname:short)"]),
|
|
10816
10849
|
localBranchHeads(),
|
|
10817
10850
|
gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
@@ -10821,7 +10854,8 @@ async function gcPlan(remote, limit, opts = {}) {
|
|
|
10821
10854
|
ghPrs(limit),
|
|
10822
10855
|
worktreeBranches(),
|
|
10823
10856
|
siblingWorktreeDirs(opts.root),
|
|
10824
|
-
preservedBranches()
|
|
10857
|
+
preservedBranches(),
|
|
10858
|
+
branchesMergedIntoBase(remote)
|
|
10825
10859
|
]);
|
|
10826
10860
|
return buildGcPlan({
|
|
10827
10861
|
localBranches: branches.split(/\r?\n/).map((b) => b.trim()).filter(Boolean),
|
|
@@ -10832,9 +10866,23 @@ async function gcPlan(remote, limit, opts = {}) {
|
|
|
10832
10866
|
pullRequests: prs,
|
|
10833
10867
|
worktrees,
|
|
10834
10868
|
remote,
|
|
10835
|
-
preservedBranches: preserved
|
|
10869
|
+
preservedBranches: preserved,
|
|
10870
|
+
mergedIntoBase
|
|
10836
10871
|
});
|
|
10837
10872
|
}
|
|
10873
|
+
async function branchesMergedIntoBase(remote) {
|
|
10874
|
+
const out = /* @__PURE__ */ new Set();
|
|
10875
|
+
for (const base of ["development", "main", "master"]) {
|
|
10876
|
+
let listed;
|
|
10877
|
+
try {
|
|
10878
|
+
listed = await gitOut(["branch", "--merged", `${remote}/${base}`, "--format=%(refname:short)"]);
|
|
10879
|
+
} catch {
|
|
10880
|
+
continue;
|
|
10881
|
+
}
|
|
10882
|
+
for (const b of listed.split(/\r?\n/).map((x) => x.trim()).filter(Boolean)) out.add(b);
|
|
10883
|
+
}
|
|
10884
|
+
return [...out];
|
|
10885
|
+
}
|
|
10838
10886
|
var STRAY_ROOT_WALK_BUDGET_MS = 4e3;
|
|
10839
10887
|
function measureWorktreeRoot(root, deadline) {
|
|
10840
10888
|
let dirs = 0;
|
|
@@ -16375,7 +16423,17 @@ var import_node_path17 = require("node:path");
|
|
|
16375
16423
|
var POLICY_FILE = "test-policy.json";
|
|
16376
16424
|
var TEST_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
16377
16425
|
var PY_TEST_RE = /(?:^|\/)test_[^/]*\.py$|_test\.py$/;
|
|
16426
|
+
var TRAILER_KEY = "Test-Policy-Override";
|
|
16378
16427
|
var OVERRIDE_RE = /^Test-Policy-Override:\s*(.+)$/im;
|
|
16428
|
+
var REC = "";
|
|
16429
|
+
var FLD = "";
|
|
16430
|
+
var WAIVABLE_KINDS = [
|
|
16431
|
+
"mandatory-zone-untested",
|
|
16432
|
+
"unrequested-test-file",
|
|
16433
|
+
"protected-removed",
|
|
16434
|
+
"stale-protected-entry",
|
|
16435
|
+
"stale-satisfied-by"
|
|
16436
|
+
];
|
|
16379
16437
|
function translate(glob) {
|
|
16380
16438
|
let out = "";
|
|
16381
16439
|
for (let i = 0; i < glob.length; i++) {
|
|
@@ -16429,20 +16487,37 @@ function readFileOrNull2(path2) {
|
|
|
16429
16487
|
return null;
|
|
16430
16488
|
}
|
|
16431
16489
|
}
|
|
16432
|
-
function
|
|
16490
|
+
function removedPaths(changed) {
|
|
16491
|
+
return new Set(
|
|
16492
|
+
changed.map((f) => f.status === "D" ? f.path : f.status === "R" || f.status === "C" ? f.from : void 0).filter((p) => typeof p === "string")
|
|
16493
|
+
);
|
|
16494
|
+
}
|
|
16495
|
+
function classify(changed, policy, present = () => false) {
|
|
16433
16496
|
const matchers = (policy.mandatory ?? []).map((m) => ({ ...m, re: globToRegExp(m.glob) }));
|
|
16434
16497
|
const mandatoryHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path)));
|
|
16435
16498
|
const testChanges = changed.filter((f) => isTestPath(f.path));
|
|
16436
16499
|
const addedTests = testChanges.filter((f) => f.status === "A");
|
|
16500
|
+
const removed = removedPaths(changed);
|
|
16501
|
+
const discharged = (m) => (m.satisfiedBy?.length ?? 0) > 0 && m.satisfiedBy.every((p) => present(p) && !removed.has(p));
|
|
16502
|
+
const untestedHits = changed.filter((f) => matchers.some((m) => m.re.test(f.path) && !discharged(m)));
|
|
16437
16503
|
const protectedBy = new Map((policy.protected ?? []).map((p) => [p.path, p.why ?? ""]));
|
|
16438
|
-
|
|
16439
|
-
|
|
16504
|
+
for (const m of policy.mandatory ?? []) {
|
|
16505
|
+
for (const p of m.satisfiedBy ?? []) {
|
|
16506
|
+
if (!protectedBy.has(p)) protectedBy.set(p, `the standing coverage \`${m.glob}\` is discharged by. Removing it silently empties that glob.`);
|
|
16507
|
+
}
|
|
16508
|
+
}
|
|
16509
|
+
const removedProtected = [...removed].filter((p) => protectedBy.has(p)).map((p) => ({ path: p, why: protectedBy.get(p) ?? "" }));
|
|
16510
|
+
return { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected };
|
|
16440
16511
|
}
|
|
16441
16512
|
function unresolvedProtectedEntries(policy, root, exists = (path2) => (0, import_node_fs19.existsSync)(path2)) {
|
|
16442
16513
|
return (policy.protected ?? []).map((p) => p.path).filter((p) => !exists((0, import_node_path17.join)(root, p)));
|
|
16443
16514
|
}
|
|
16444
|
-
function
|
|
16445
|
-
const
|
|
16515
|
+
function unresolvedSatisfiers(policy, root, exists = (path2) => (0, import_node_fs19.existsSync)(path2)) {
|
|
16516
|
+
const declared = (policy.mandatory ?? []).flatMap((m) => m.satisfiedBy ?? []);
|
|
16517
|
+
return [...new Set(declared)].filter((p) => !exists((0, import_node_path17.join)(root, p)));
|
|
16518
|
+
}
|
|
16519
|
+
function evaluate(changed, policy, present = () => false) {
|
|
16520
|
+
const { mandatoryHits, untestedHits, testChanges, addedTests, removedProtected } = classify(changed, policy, present);
|
|
16446
16521
|
const findings = [];
|
|
16447
16522
|
if (removedProtected.length > 0) {
|
|
16448
16523
|
findings.push({
|
|
@@ -16453,12 +16528,12 @@ function evaluate(changed, policy) {
|
|
|
16453
16528
|
${f.why}`).join("\n") + "\n If you are removing one because it looked unrequired, read the reason above first \u2014 it is\n the answer to exactly that question. If this removal was asked for, add a commit trailer:\n Test-Policy-Override: <reason>"
|
|
16454
16529
|
});
|
|
16455
16530
|
}
|
|
16456
|
-
if (
|
|
16531
|
+
if (untestedHits.length > 0 && testChanges.length === 0) {
|
|
16457
16532
|
findings.push({
|
|
16458
16533
|
kind: "mandatory-zone-untested",
|
|
16459
|
-
paths:
|
|
16460
|
-
detail: `MANDATORY ZONE UNTESTED \u2014 this change touches ${
|
|
16461
|
-
` +
|
|
16534
|
+
paths: untestedHits.map((f) => f.path),
|
|
16535
|
+
detail: `MANDATORY ZONE UNTESTED \u2014 this change touches ${untestedHits.length} path(s) in the mandatory zone but changes no test:
|
|
16536
|
+
` + untestedHits.map((f) => ` ${f.path}`).join("\n") + "\n These are the paths where a silent failure costs real money, real data, a security breach,\n or production. Add or update a test that would fail if the guarded property broke."
|
|
16462
16537
|
});
|
|
16463
16538
|
}
|
|
16464
16539
|
if (policy.declared !== false && addedTests.length > 0 && mandatoryHits.length === 0) {
|
|
@@ -16484,6 +16559,89 @@ function resolveBase(cwd, explicit) {
|
|
|
16484
16559
|
}
|
|
16485
16560
|
return null;
|
|
16486
16561
|
}
|
|
16562
|
+
function isShallowRepository(cwd) {
|
|
16563
|
+
try {
|
|
16564
|
+
return git(["rev-parse", "--is-shallow-repository"], cwd).trim() !== "false";
|
|
16565
|
+
} catch {
|
|
16566
|
+
return true;
|
|
16567
|
+
}
|
|
16568
|
+
}
|
|
16569
|
+
function untrustworthyRange(cwd, base) {
|
|
16570
|
+
if (!base) {
|
|
16571
|
+
return {
|
|
16572
|
+
kind: "unresolvable-base",
|
|
16573
|
+
paths: [],
|
|
16574
|
+
detail: "NO COMPARISON BASE \u2014 none of --base, TEST_POLICY_BASE, origin/development or origin/main resolved.\n With no base there is no change set, and a gate that cannot see the change set has nothing it\n can honestly pass. Fetch the default branch, or name the base with --base / TEST_POLICY_BASE."
|
|
16575
|
+
};
|
|
16576
|
+
}
|
|
16577
|
+
if (!isShallowRepository(cwd)) return null;
|
|
16578
|
+
return {
|
|
16579
|
+
kind: "untrusted-range",
|
|
16580
|
+
paths: [],
|
|
16581
|
+
detail: `UNTRUSTWORTHY RANGE \u2014 this clone is SHALLOW, so ${base.slice(0, 8)}..HEAD is not the change set.
|
|
16582
|
+
A grafted commit has no visible parents, so excluding the base excludes the base ALONE and the
|
|
16583
|
+
range silently widens to everything reachable from HEAD. Measured in the fleet (#3628): an
|
|
16584
|
+
earlier CI step ran \`git fetch --depth=1\`, and a Test-Policy-Override trailer from a commit
|
|
16585
|
+
merged the previous day then passed every PR green. A gate that cannot identify the change set
|
|
16586
|
+
must not evaluate one, and must never waive one.
|
|
16587
|
+
Fetch full history \u2014 actions/checkout with \`fetch-depth: 0\`, or \`git fetch --unshallow\`.`
|
|
16588
|
+
};
|
|
16589
|
+
}
|
|
16590
|
+
function parseScope(value) {
|
|
16591
|
+
const scoped = /^\[([^\]]*)\]\s*([\s\S]*)$/.exec(value);
|
|
16592
|
+
if (!scoped) return { kinds: [...WAIVABLE_KINDS], reason: value, unknown: [] };
|
|
16593
|
+
const named = scoped[1].split(",").map((k) => k.trim()).filter(Boolean);
|
|
16594
|
+
return {
|
|
16595
|
+
kinds: named,
|
|
16596
|
+
reason: scoped[2].trim(),
|
|
16597
|
+
unknown: named.filter((k) => !WAIVABLE_KINDS.includes(k))
|
|
16598
|
+
};
|
|
16599
|
+
}
|
|
16600
|
+
function invisibleTrailer(sha, line) {
|
|
16601
|
+
return {
|
|
16602
|
+
kind: "malformed-override-trailer",
|
|
16603
|
+
paths: [],
|
|
16604
|
+
detail: `OVERRIDE TRAILER GIT CANNOT SEE \u2014 commit ${sha.slice(0, 8)} carries
|
|
16605
|
+
${line}
|
|
16606
|
+
but \`git log --format='%(trailers:key=${TRAILER_KEY})'\` reports nothing for it, so the waiver would
|
|
16607
|
+
exist only to the regex that granted it. The trailer was chosen over a flag because it can be
|
|
16608
|
+
AUDITED, and a reason the auditor cannot see is not a reason this gate accepts (#3628).
|
|
16609
|
+
Git reads trailers from the LAST paragraph only, and every line of that paragraph must be a
|
|
16610
|
+
trailer or an indented continuation \u2014 one bare line such as \`Closes #123\` disqualifies the whole
|
|
16611
|
+
block. Indent the continuation lines, and leave nothing but trailers in that paragraph.`
|
|
16612
|
+
};
|
|
16613
|
+
}
|
|
16614
|
+
function unknownScope(sha, unknown) {
|
|
16615
|
+
return {
|
|
16616
|
+
kind: "malformed-override-trailer",
|
|
16617
|
+
paths: [],
|
|
16618
|
+
detail: `UNKNOWN OVERRIDE SCOPE \u2014 commit ${sha.slice(0, 8)} scopes its waiver to ${unknown.join(", ")}, which this
|
|
16619
|
+
gate cannot report.
|
|
16620
|
+
Waivable kinds: ${WAIVABLE_KINDS.join(", ")}.
|
|
16621
|
+
Refused rather than widened: treating a typo as "waive everything" is how a scoped waiver turns
|
|
16622
|
+
into a blanket exemption without anyone deciding it should.`
|
|
16623
|
+
};
|
|
16624
|
+
}
|
|
16625
|
+
function readOverride(base, cwd) {
|
|
16626
|
+
const format = `%H${FLD}%(trailers:key=${TRAILER_KEY},valueonly,unfold)${FLD}%B${REC}`;
|
|
16627
|
+
const refusals = [];
|
|
16628
|
+
let override = null;
|
|
16629
|
+
for (const record of git(["log", `${base}..HEAD`, `--format=${format}`], cwd).split(REC)) {
|
|
16630
|
+
const [sha, trailer, body] = record.replace(/^\s+/, "").split(FLD);
|
|
16631
|
+
if (!sha) continue;
|
|
16632
|
+
const value = (trailer ?? "").trim();
|
|
16633
|
+
if (!value) {
|
|
16634
|
+
const shaped = OVERRIDE_RE.exec(body ?? "");
|
|
16635
|
+
if (shaped) refusals.push(invisibleTrailer(sha, shaped[0].trim()));
|
|
16636
|
+
continue;
|
|
16637
|
+
}
|
|
16638
|
+
if (override) continue;
|
|
16639
|
+
const { kinds, reason, unknown } = parseScope(value);
|
|
16640
|
+
if (unknown.length > 0) refusals.push(unknownScope(sha, unknown));
|
|
16641
|
+
else override = { sha, reason, kinds };
|
|
16642
|
+
}
|
|
16643
|
+
return { override, refusals };
|
|
16644
|
+
}
|
|
16487
16645
|
function changedFilesSince(base, cwd) {
|
|
16488
16646
|
const raw = git(["diff", "--name-status", "-M", `${base}...HEAD`], cwd).trim();
|
|
16489
16647
|
if (!raw) return [];
|
|
@@ -16499,29 +16657,45 @@ function runTestPolicy(root, deps = {}) {
|
|
|
16499
16657
|
const policy = deps.policy ?? loadPolicy(root);
|
|
16500
16658
|
const exists = deps.exists ?? ((path2) => (0, import_node_fs19.existsSync)(path2));
|
|
16501
16659
|
const counts = { mandatoryCount: (policy.mandatory ?? []).length, protectedCount: (policy.protected ?? []).length };
|
|
16502
|
-
const
|
|
16660
|
+
const base = deps.changed ? "(injected)" : resolveBase(root, deps.base);
|
|
16661
|
+
const refusal = deps.changed ? null : untrustworthyRange(root, base);
|
|
16662
|
+
const changed = deps.changed ?? (refusal ? [] : changedFilesSince(base, root));
|
|
16663
|
+
const lookup = deps.override !== void 0 ? { override: deps.override, refusals: [] } : !deps.changed && !refusal ? readOverride(base, root) : { override: null, refusals: [] };
|
|
16664
|
+
const present = (path2) => exists((0, import_node_path17.join)(root, path2));
|
|
16665
|
+
const removedByThisDiff = removedPaths(changed);
|
|
16666
|
+
const staleFindings = [];
|
|
16667
|
+
const unresolved = unresolvedProtectedEntries(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
|
|
16503
16668
|
if (unresolved.length > 0) {
|
|
16504
|
-
|
|
16505
|
-
|
|
16506
|
-
|
|
16507
|
-
|
|
16508
|
-
...counts,
|
|
16509
|
-
findings: [{
|
|
16510
|
-
kind: "stale-protected-entry",
|
|
16511
|
-
paths: unresolved,
|
|
16512
|
-
detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist:
|
|
16669
|
+
staleFindings.push({
|
|
16670
|
+
kind: "stale-protected-entry",
|
|
16671
|
+
paths: unresolved,
|
|
16672
|
+
detail: `STALE PROTECTED ENTRY \u2014 test-policy.json protects ${unresolved.length} path(s) that do not exist:
|
|
16513
16673
|
` + unresolved.map((p) => ` ${p}`).join("\n") + "\n An entry naming a missing file reads as protection while protecting nothing.\n Restore the file, or remove its entry."
|
|
16514
|
-
|
|
16515
|
-
};
|
|
16674
|
+
});
|
|
16516
16675
|
}
|
|
16517
|
-
const
|
|
16518
|
-
if (
|
|
16519
|
-
|
|
16520
|
-
|
|
16521
|
-
|
|
16522
|
-
|
|
16523
|
-
|
|
16524
|
-
|
|
16676
|
+
const staleSatisfiers = unresolvedSatisfiers(policy, root, exists).filter((p) => !removedByThisDiff.has(p));
|
|
16677
|
+
if (staleSatisfiers.length > 0) {
|
|
16678
|
+
staleFindings.push({
|
|
16679
|
+
kind: "stale-satisfied-by",
|
|
16680
|
+
paths: staleSatisfiers,
|
|
16681
|
+
detail: `STALE STANDING COVERAGE \u2014 a mandatory glob claims ${staleSatisfiers.length} path(s) as satisfiedBy that do not exist:
|
|
16682
|
+
` + staleSatisfiers.map((p) => ` ${p}`).join("\n") + "\n A glob discharged by coverage that is not there is a glob enforcing nothing, quietly.\n Restore the file, or drop it from satisfiedBy so the glob asks for a test again."
|
|
16683
|
+
});
|
|
16684
|
+
}
|
|
16685
|
+
const override = lookup.override;
|
|
16686
|
+
const waived = [];
|
|
16687
|
+
const sift = (found) => {
|
|
16688
|
+
waived.push(...found.filter((f) => override?.kinds.includes(f.kind)));
|
|
16689
|
+
return found.filter((f) => !override?.kinds.includes(f.kind));
|
|
16690
|
+
};
|
|
16691
|
+
const blocking = sift([...refusal ? [refusal] : [], ...lookup.refusals, ...staleFindings]);
|
|
16692
|
+
const findings = blocking.length > 0 ? blocking : sift(evaluate(changed, policy, present));
|
|
16693
|
+
const result = { ok: findings.length === 0, findings, changedCount: changed.length, base, ...counts };
|
|
16694
|
+
if (override) {
|
|
16695
|
+
result.overriddenBy = override;
|
|
16696
|
+
result.waived = waived;
|
|
16697
|
+
}
|
|
16698
|
+
return result;
|
|
16525
16699
|
}
|
|
16526
16700
|
|
|
16527
16701
|
// src/docs-audit-command.ts
|
|
@@ -21875,7 +22049,18 @@ function recordGcWorktreeRemoval(primaryRoot, actor, owners, path2, branch) {
|
|
|
21875
22049
|
});
|
|
21876
22050
|
dropWorktreeOwner(primaryRoot, path2);
|
|
21877
22051
|
}
|
|
21878
|
-
function
|
|
22052
|
+
function renderPrLandCleanupLines(cleanup) {
|
|
22053
|
+
const report = cleanup;
|
|
22054
|
+
const wt = report?.worktree;
|
|
22055
|
+
if (!wt?.path) return [];
|
|
22056
|
+
const lines = [];
|
|
22057
|
+
if (wt.status === "removed") lines.push(`pr land: removed worktree ${wt.path} \u2014 that directory is gone; cd elsewhere before your next command`);
|
|
22058
|
+
else if (wt.status === "deferred") lines.push(`pr land: worktree ${wt.path} could not be removed yet and is registered for a later sweep`);
|
|
22059
|
+
else lines.push(`pr land: worktree ${wt.path} \u2014 ${wt.status ?? "unknown"}${wt.reason ? ` (${wt.reason})` : ""}`);
|
|
22060
|
+
if (wt.deferredNote) lines.push(`pr land: ${wt.deferredNote}`);
|
|
22061
|
+
return lines;
|
|
22062
|
+
}
|
|
22063
|
+
function renderGcApplyResult(result, skipped = []) {
|
|
21879
22064
|
const lines = ["worktree gc apply result:"];
|
|
21880
22065
|
lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
|
|
21881
22066
|
lines.push(` remote branches removed: ${result.removedRemoteBranches.length ? result.removedRemoteBranches.join(", ") : "none"}`);
|
|
@@ -21892,9 +22077,15 @@ function renderGcApplyResult(result) {
|
|
|
21892
22077
|
}
|
|
21893
22078
|
const removedNothing = result.removedBranches.length === 0 && result.removedRemoteBranches.length === 0 && result.removedTrackingRefs.length === 0 && result.removedWorktreeDirs.length === 0;
|
|
21894
22079
|
if (removedNothing && !result.refused.length) {
|
|
21895
|
-
|
|
21896
|
-
|
|
21897
|
-
|
|
22080
|
+
const actionable = skipped.filter((s) => s.reason !== "protected" && s.reason !== "current-branch");
|
|
22081
|
+
if (actionable.length) {
|
|
22082
|
+
lines.push(` NOTHING WAS REMOVED \u2014 every candidate was skipped for a stated reason (${actionable.length}):`);
|
|
22083
|
+
for (const s of actionable) lines.push(` - ${s.branch}: ${s.reason}${s.detail ? ` (${s.detail})` : ""}`);
|
|
22084
|
+
} else {
|
|
22085
|
+
lines.push(" NOTHING WAS REMOVED. A stale worktree whose directory still exists is not reachable by");
|
|
22086
|
+
lines.push(" this verb \u2014 remove it from inside it with `mmi-cli worktree land --apply`, or delete the");
|
|
22087
|
+
lines.push(" directory and re-run to clear its registration.");
|
|
22088
|
+
}
|
|
21898
22089
|
}
|
|
21899
22090
|
return lines.join("\n");
|
|
21900
22091
|
}
|
|
@@ -24877,6 +25068,22 @@ function checkDocsAudit(probe) {
|
|
|
24877
25068
|
}
|
|
24878
25069
|
return { ok: true, id: "docs-audit", label: "docs-audit", detail: probe.detail, verbose: [probe.detail] };
|
|
24879
25070
|
}
|
|
25071
|
+
function checkRedactorLiveness(probe) {
|
|
25072
|
+
if (!probe) return null;
|
|
25073
|
+
const evidence = [`secret-redact failed rows in the last 48h: ${probe.failed}${probe.lastTs ? ` (last ${probe.lastTs})` : ""}`];
|
|
25074
|
+
if (probe.failed === 0) {
|
|
25075
|
+
return { ok: true, id: "redactor-liveness", label: "secret-redact liveness", verbose: evidence };
|
|
25076
|
+
}
|
|
25077
|
+
return {
|
|
25078
|
+
ok: false,
|
|
25079
|
+
id: "redactor-liveness",
|
|
25080
|
+
label: "secret-redact liveness",
|
|
25081
|
+
reportOnly: true,
|
|
25082
|
+
detail: `${probe.failed} crash marker${probe.failed === 1 ? "" : "s"} in 48h`,
|
|
25083
|
+
fix: "the redactor is crashing during scans \u2014 read `.git/mmi-runtime/hooks/activity.jsonl` for the error and fix it; every crash is a scan that never ran",
|
|
25084
|
+
verbose: evidence
|
|
25085
|
+
};
|
|
25086
|
+
}
|
|
24880
25087
|
function checkSessionPayload(probe) {
|
|
24881
25088
|
if (!probe) return null;
|
|
24882
25089
|
const { chars } = probe;
|
|
@@ -25054,6 +25261,10 @@ async function runDoctorClean(opts, io, deps) {
|
|
|
25054
25261
|
const sched = checkSchedules(probe);
|
|
25055
25262
|
if (sched) checks.push(sched);
|
|
25056
25263
|
}
|
|
25264
|
+
if (!opts.fast && !opts.banner && !opts.preflight && deps.redactorLiveness) {
|
|
25265
|
+
const redactor = checkRedactorLiveness(deps.redactorLiveness());
|
|
25266
|
+
if (redactor) checks.push(redactor);
|
|
25267
|
+
}
|
|
25057
25268
|
if (!opts.fast && !opts.banner && !opts.preflight && deps.docsAudit) {
|
|
25058
25269
|
const probe = await deps.docsAudit().catch((e) => ({
|
|
25059
25270
|
armed: true,
|
|
@@ -25494,6 +25705,9 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
25494
25705
|
// #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
|
|
25495
25706
|
// A local record read — cheap enough for every lane, including the banner.
|
|
25496
25707
|
sessionPayload: () => readSessionPayload(process.cwd()),
|
|
25708
|
+
// #3630: recent secret-redact crash markers from the shared hook-activity trace — with the Stop
|
|
25709
|
+
// hook retired, the full/scheduled doctor is the trace's only reader. Local tail-scan, fail-soft.
|
|
25710
|
+
redactorLiveness: () => redactorLivenessProbe(process.cwd()),
|
|
25497
25711
|
// #3485 items 9 and 8: why the MMI plugin has never printed an "updated — please restart" notice, and
|
|
25498
25712
|
// which branch it would pick one up from. Two local file reads, no network, fail-soft to no rows.
|
|
25499
25713
|
marketplaceRows: () => {
|
|
@@ -25848,7 +26062,7 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
25848
26062
|
console.log(formatGcPlan(plan, false));
|
|
25849
26063
|
} else {
|
|
25850
26064
|
if (applyResult) console.log(`
|
|
25851
|
-
${renderGcApplyResult(applyResult)}`);
|
|
26065
|
+
${renderGcApplyResult(applyResult, plan.skipped)}`);
|
|
25852
26066
|
}
|
|
25853
26067
|
if (applyResult?.failed.length) process.exitCode = 1;
|
|
25854
26068
|
} catch (e) {
|
|
@@ -26190,12 +26404,10 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
|
|
|
26190
26404
|
if (!result.ok) process.exitCode = 1;
|
|
26191
26405
|
return;
|
|
26192
26406
|
}
|
|
26193
|
-
if (result.
|
|
26194
|
-
|
|
26195
|
-
|
|
26196
|
-
|
|
26197
|
-
if (result.overriddenBy) {
|
|
26198
|
-
console.log(`tests policy: overridden by commit trailer \u2014 ${result.overriddenBy}`);
|
|
26407
|
+
if (result.overriddenBy && result.ok) {
|
|
26408
|
+
const { sha, reason, kinds } = result.overriddenBy;
|
|
26409
|
+
const scope = (result.waived ?? []).map((f) => f.kind).join(", ") || "nothing";
|
|
26410
|
+
console.log(`tests policy: overridden by ${sha.slice(0, 8)} (waiving ${scope}; scope ${kinds.join(", ")}) \u2014 ${reason}`);
|
|
26199
26411
|
return;
|
|
26200
26412
|
}
|
|
26201
26413
|
if (result.ok) {
|
|
@@ -27268,6 +27480,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe \
|
|
|
27268
27480
|
if (o.json) printLine(JSON.stringify(result));
|
|
27269
27481
|
else {
|
|
27270
27482
|
printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
27483
|
+
for (const line of renderPrLandCleanupLines(result.cleanup)) printLine(line);
|
|
27271
27484
|
if (result.cleanupError) printLine(`pr land cleanup: ${result.cleanupError}`);
|
|
27272
27485
|
}
|
|
27273
27486
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mutmutco/cli",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.66.0",
|
|
4
4
|
"description": "MMI Future CLI — the org dev toolbox (board, registry, keyless secrets, release train, bootstrap, doctor) and the cross-IDE engine the plugin's session-start hook drives.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|