@mutmutco/cli 3.98.0 → 3.100.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 +719 -273
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -3417,7 +3417,7 @@ var program = new Command();
|
|
|
3417
3417
|
// src/index.ts
|
|
3418
3418
|
var import_promises11 = require("node:fs/promises");
|
|
3419
3419
|
var import_node_fs40 = require("node:fs");
|
|
3420
|
-
var
|
|
3420
|
+
var import_node_child_process19 = require("node:child_process");
|
|
3421
3421
|
|
|
3422
3422
|
// src/cli-shared.ts
|
|
3423
3423
|
var import_node_child_process3 = require("node:child_process");
|
|
@@ -5779,6 +5779,7 @@ function buildGcPlan(inputs) {
|
|
|
5779
5779
|
const preservedBranches2 = new Set(inputs.preservedBranches ?? []);
|
|
5780
5780
|
const preservedWorktrees = new Set((inputs.worktrees ?? []).filter((w) => w.preserved).map((w) => w.branch));
|
|
5781
5781
|
const mergedIntoBase = new Set((inputs.mergedIntoBase ?? []).map((b) => b.trim()).filter(Boolean));
|
|
5782
|
+
const prLookupFailures = new Map((inputs.prLookupFailures ?? []).map((f) => [f.branch.trim(), f.detail]));
|
|
5782
5783
|
const skipped = [];
|
|
5783
5784
|
const branches = [];
|
|
5784
5785
|
const skipTrackingBranches = /* @__PURE__ */ new Set();
|
|
@@ -5794,6 +5795,11 @@ function buildGcPlan(inputs) {
|
|
|
5794
5795
|
skipped.push({ branch, reason: "protected" });
|
|
5795
5796
|
continue;
|
|
5796
5797
|
}
|
|
5798
|
+
if (prLookupFailures.has(branch)) {
|
|
5799
|
+
skipped.push({ branch, reason: "pr-lookup-failed", detail: prLookupFailures.get(branch) });
|
|
5800
|
+
skipTrackingBranches.add(branch);
|
|
5801
|
+
continue;
|
|
5802
|
+
}
|
|
5797
5803
|
const prSet = prs.get(branch);
|
|
5798
5804
|
if (prSet?.some((pr2) => pr2.state === "OPEN")) {
|
|
5799
5805
|
skipped.push({ branch, reason: "open-pr" });
|
|
@@ -5838,6 +5844,7 @@ function buildGcPlan(inputs) {
|
|
|
5838
5844
|
const branch = branchForTrackingRef(ref, remote);
|
|
5839
5845
|
if (!branch || protectedBranches.has(branch)) return null;
|
|
5840
5846
|
if (branch === inputs.currentBranch || dirtyWorktrees.has(branch) || unpushedWorktrees.has(branch) || preservedBranches2.has(branch) || preservedWorktrees.has(branch) || skipTrackingBranches.has(branch)) return null;
|
|
5847
|
+
if (prLookupFailures.has(branch)) return null;
|
|
5841
5848
|
const prSet = prs.get(branch);
|
|
5842
5849
|
if (prSet?.some((pr2) => pr2.state === "OPEN")) return null;
|
|
5843
5850
|
const state = closedState(prSet);
|
|
@@ -13797,23 +13804,6 @@ async function fetchSchedulesList(deps) {
|
|
|
13797
13804
|
return null;
|
|
13798
13805
|
}
|
|
13799
13806
|
}
|
|
13800
|
-
async function fetchDocsAuditList(deps) {
|
|
13801
|
-
if (!deps.baseUrl) return { notArmed: true };
|
|
13802
|
-
try {
|
|
13803
|
-
const token = await deps.token();
|
|
13804
|
-
if (!token) return { notArmed: true };
|
|
13805
|
-
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/docs-audit/list`, {
|
|
13806
|
-
method: "GET",
|
|
13807
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
13808
|
-
});
|
|
13809
|
-
if (res.status === 404) return { notArmed: true };
|
|
13810
|
-
if (!res.ok) return { ok: false, error: `docs-audit list HTTP ${res.status}` };
|
|
13811
|
-
const body = await res.json();
|
|
13812
|
-
return { ok: true, rows: Array.isArray(body?.docsAudits) ? body.docsAudits : [] };
|
|
13813
|
-
} catch (e) {
|
|
13814
|
-
return { ok: false, error: e.message };
|
|
13815
|
-
}
|
|
13816
|
-
}
|
|
13817
13807
|
async function fetchOrgConfig(deps) {
|
|
13818
13808
|
if (!deps.baseUrl) return null;
|
|
13819
13809
|
const token = await deps.token();
|
|
@@ -13864,9 +13854,6 @@ async function retireProject(slug, deps) {
|
|
|
13864
13854
|
async function setDeployCoords(slug, payload, deps) {
|
|
13865
13855
|
return postJson(`/projects/${encodeURIComponent(slug)}/deploy`, payload, deps);
|
|
13866
13856
|
}
|
|
13867
|
-
async function recordDocsAudit(verdict, deps) {
|
|
13868
|
-
return postJson("/docs-audit/record", { ...verdict }, deps);
|
|
13869
|
-
}
|
|
13870
13857
|
async function postSchedulesLift(repo, schedules, deps) {
|
|
13871
13858
|
const res = await postJson("/schedules/lift", { repo, schedules }, deps);
|
|
13872
13859
|
if (!res.ok && res.status === 404) return { ...res, unreachable: "route-absent" };
|
|
@@ -15587,13 +15574,27 @@ async function isOrgRegisteredRepo(cfg, deps = {}) {
|
|
|
15587
15574
|
if (!read.ok) return false;
|
|
15588
15575
|
return read.project !== null;
|
|
15589
15576
|
}
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
const
|
|
15593
|
-
|
|
15594
|
-
|
|
15595
|
-
]
|
|
15596
|
-
|
|
15577
|
+
var GC_PR_LOOKUP_CONCURRENCY = 8;
|
|
15578
|
+
async function resolveBranchPrs(branches, limit) {
|
|
15579
|
+
const queue = [...new Set(branches.map((b) => b.trim()).filter(Boolean))];
|
|
15580
|
+
const prs = [];
|
|
15581
|
+
const failures = [];
|
|
15582
|
+
const args = (branch) => ["pr", "list", "--head", branch, "--state", "all", "--limit", String(limit), "--json", "number,headRefName,headRefOid,state"];
|
|
15583
|
+
let next = 0;
|
|
15584
|
+
const worker = async () => {
|
|
15585
|
+
for (let i = next++; i < queue.length; i = next++) {
|
|
15586
|
+
const branch = queue[i];
|
|
15587
|
+
try {
|
|
15588
|
+
const { stdout } = await execFileP2("gh", args(branch), { timeout: GC_GH_TIMEOUT_MS });
|
|
15589
|
+
const rows = JSON.parse(stdout || "[]");
|
|
15590
|
+
prs.push(...rows.filter((pr2) => pr2.headRefName?.trim() === branch));
|
|
15591
|
+
} catch (e) {
|
|
15592
|
+
failures.push({ branch, detail: e.message });
|
|
15593
|
+
}
|
|
15594
|
+
}
|
|
15595
|
+
};
|
|
15596
|
+
await Promise.all(Array.from({ length: Math.min(GC_PR_LOOKUP_CONCURRENCY, queue.length) }, worker));
|
|
15597
|
+
return { prs, failures };
|
|
15597
15598
|
}
|
|
15598
15599
|
async function localBranchHeads() {
|
|
15599
15600
|
try {
|
|
@@ -15737,21 +15738,26 @@ function resolveExplicitScanRoot(explicitRoot, repoRoot2) {
|
|
|
15737
15738
|
return explicitRepoWorktreesRoot(explicitRoot, repoRoot2, rootDirs);
|
|
15738
15739
|
}
|
|
15739
15740
|
async function gcPlan(remote, limit, opts = {}) {
|
|
15740
|
-
const [branches, heads, current, stale,
|
|
15741
|
+
const [branches, heads, current, stale, worktrees, siblingDirs, preserved, mergedIntoBase] = await Promise.all([
|
|
15741
15742
|
gitOut(["branch", "--format=%(refname:short)"]),
|
|
15742
15743
|
localBranchHeads(),
|
|
15743
15744
|
gitOut(["rev-parse", "--abbrev-ref", "HEAD"]),
|
|
15744
15745
|
collectStaleTrackingRefs(remote, {
|
|
15745
15746
|
execGit: (args) => execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })
|
|
15746
15747
|
}),
|
|
15747
|
-
ghPrs(limit),
|
|
15748
15748
|
worktreeBranches(),
|
|
15749
15749
|
siblingWorktreeDirs(opts.root),
|
|
15750
15750
|
preservedBranches(),
|
|
15751
15751
|
branchesMergedIntoBase(remote)
|
|
15752
15752
|
]);
|
|
15753
|
+
const localBranches = branches.split(/\r?\n/).map((b) => b.trim()).filter(Boolean);
|
|
15754
|
+
const { prs, failures } = await resolveBranchPrs(
|
|
15755
|
+
[...localBranches, ...stale.map((ref) => branchForTrackingRef(ref, remote)).filter((b) => Boolean(b))].filter((b) => !isProtectedBranch(b)),
|
|
15756
|
+
limit
|
|
15757
|
+
);
|
|
15753
15758
|
return buildGcPlan({
|
|
15754
|
-
localBranches
|
|
15759
|
+
localBranches,
|
|
15760
|
+
prLookupFailures: failures,
|
|
15755
15761
|
localBranchHeads: heads,
|
|
15756
15762
|
currentBranch: current,
|
|
15757
15763
|
siblingWorktreeDirs: siblingDirs,
|
|
@@ -17853,8 +17859,6 @@ function consolidateCommandNamespaces(program3) {
|
|
|
17853
17859
|
move(program3, plugin, "plugin-heal", "heal");
|
|
17854
17860
|
move(program3, plugin, "plugin-prune", "prune");
|
|
17855
17861
|
move(program3, plugin, "session-start");
|
|
17856
|
-
const docs2 = child(program3, "docs");
|
|
17857
|
-
move(program3, docs2, "docs-audit", "audit");
|
|
17858
17862
|
const train = child(program3, "train");
|
|
17859
17863
|
const fullTrack2 = child(program3, "full-track");
|
|
17860
17864
|
move(fullTrack2, train, "readiness");
|
|
@@ -18156,6 +18160,22 @@ function healPiPluginRegistration(home, env, installedVersion) {
|
|
|
18156
18160
|
}
|
|
18157
18161
|
}
|
|
18158
18162
|
|
|
18163
|
+
// src/statusline-invalidate.ts
|
|
18164
|
+
var import_node_child_process10 = require("node:child_process");
|
|
18165
|
+
function invalidateStatuslineBoardCache() {
|
|
18166
|
+
try {
|
|
18167
|
+
const child2 = (0, import_node_child_process10.spawn)("jerv-cli", ["lane", "ops", "invalidate-board"], {
|
|
18168
|
+
detached: true,
|
|
18169
|
+
stdio: "ignore",
|
|
18170
|
+
windowsHide: true
|
|
18171
|
+
});
|
|
18172
|
+
child2.on("error", () => {
|
|
18173
|
+
});
|
|
18174
|
+
child2.unref();
|
|
18175
|
+
} catch {
|
|
18176
|
+
}
|
|
18177
|
+
}
|
|
18178
|
+
|
|
18159
18179
|
// src/skill-lesson.ts
|
|
18160
18180
|
var SKILL_LESSON_LABEL = "skill-lesson";
|
|
18161
18181
|
var SKILL_NAMES = ["bootstrap", "browser-automation", "doctor", "epic", "hotfix", "mmi", "onboard", "rcand", "release", "resume", "secrets", "stage", "worktree"];
|
|
@@ -18395,12 +18415,12 @@ function renderVerifyBroker(input) {
|
|
|
18395
18415
|
}
|
|
18396
18416
|
|
|
18397
18417
|
// src/hotfix-coverage.ts
|
|
18398
|
-
var
|
|
18418
|
+
var import_node_child_process11 = require("node:child_process");
|
|
18399
18419
|
var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
18400
18420
|
function checkHotfixCoverage(options = {}) {
|
|
18401
18421
|
const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
|
|
18402
18422
|
const ack = (options.ack ?? []).filter(Boolean);
|
|
18403
|
-
const git2 = options.git ?? ((args, opts) => (0,
|
|
18423
|
+
const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
18404
18424
|
const revList = (range) => {
|
|
18405
18425
|
const out = git2(["rev-list", "--no-merges", range]).trim();
|
|
18406
18426
|
return out ? out.split("\n") : [];
|
|
@@ -18468,7 +18488,7 @@ function checkHotfixCoverage(options = {}) {
|
|
|
18468
18488
|
}
|
|
18469
18489
|
function checkHotfixCarries(options) {
|
|
18470
18490
|
const { cwd = process.cwd(), branch, baseRef, targets } = options;
|
|
18471
|
-
const git2 = options.git ?? ((args, opts) => (0,
|
|
18491
|
+
const git2 = options.git ?? ((args, opts) => (0, import_node_child_process11.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
|
|
18472
18492
|
const isAncestor = (sha, ref) => {
|
|
18473
18493
|
try {
|
|
18474
18494
|
git2(["merge-base", "--is-ancestor", sha, ref]);
|
|
@@ -19582,7 +19602,7 @@ function renderAccessReport(report) {
|
|
|
19582
19602
|
|
|
19583
19603
|
// src/repo-index.ts
|
|
19584
19604
|
var import_node_crypto4 = require("node:crypto");
|
|
19585
|
-
var
|
|
19605
|
+
var import_node_child_process12 = require("node:child_process");
|
|
19586
19606
|
var import_node_fs23 = require("node:fs");
|
|
19587
19607
|
var import_node_path21 = require("node:path");
|
|
19588
19608
|
var REPO_INDEX_SCHEMA = 1;
|
|
@@ -19728,7 +19748,7 @@ function loadReadmeHints(cwd, candidatePaths) {
|
|
|
19728
19748
|
function toPosix(p) {
|
|
19729
19749
|
return p.split(import_node_path21.sep).join("/");
|
|
19730
19750
|
}
|
|
19731
|
-
function listCandidatePaths(cwd, exec =
|
|
19751
|
+
function listCandidatePaths(cwd, exec = import_node_child_process12.execFileSync) {
|
|
19732
19752
|
try {
|
|
19733
19753
|
const out = exec("git", ["ls-files", "-z", "-c", "-o", "--exclude-standard"], {
|
|
19734
19754
|
cwd,
|
|
@@ -19851,7 +19871,7 @@ function searchRepoIndex(idx, query, limit = 20) {
|
|
|
19851
19871
|
}
|
|
19852
19872
|
return out;
|
|
19853
19873
|
}
|
|
19854
|
-
function inferRepoSlug(cwd, exec =
|
|
19874
|
+
function inferRepoSlug(cwd, exec = import_node_child_process12.execFileSync) {
|
|
19855
19875
|
try {
|
|
19856
19876
|
const url = String(exec("git", ["remote", "get-url", "origin"], { cwd, encoding: "utf8" })).trim();
|
|
19857
19877
|
const m = /[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(url);
|
|
@@ -20001,7 +20021,7 @@ async function gcRepoIndexCloud(deps) {
|
|
|
20001
20021
|
var import_node_fs24 = require("node:fs");
|
|
20002
20022
|
var import_node_os9 = require("node:os");
|
|
20003
20023
|
var import_node_path22 = require("node:path");
|
|
20004
|
-
var
|
|
20024
|
+
var import_node_child_process13 = require("node:child_process");
|
|
20005
20025
|
var MAX_EMBED_BACKFILL_ROUNDS = 40;
|
|
20006
20026
|
function normalizeRepo(raw) {
|
|
20007
20027
|
const t = raw.trim().replace(/\.git$/, "");
|
|
@@ -20021,7 +20041,7 @@ function rosterRepos(projects) {
|
|
|
20021
20041
|
}
|
|
20022
20042
|
function shallowClone(repo, dest, token) {
|
|
20023
20043
|
const basic = Buffer.from(`x-access-token:${token}`, "utf8").toString("base64");
|
|
20024
|
-
(0,
|
|
20044
|
+
(0, import_node_child_process13.execFileSync)(
|
|
20025
20045
|
"git",
|
|
20026
20046
|
["-c", `http.extraHeader=Authorization: Basic ${basic}`, "clone", "--depth", "1", "--single-branch", `https://github.com/${repo}.git`, dest],
|
|
20027
20047
|
{ encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], windowsHide: true }
|
|
@@ -20288,7 +20308,7 @@ async function runRepoIndexHealth(opts) {
|
|
|
20288
20308
|
}
|
|
20289
20309
|
|
|
20290
20310
|
// src/spawn-policy-core.ts
|
|
20291
|
-
var
|
|
20311
|
+
var import_node_child_process14 = require("node:child_process");
|
|
20292
20312
|
var import_node_fs26 = require("node:fs");
|
|
20293
20313
|
var import_node_path23 = require("node:path");
|
|
20294
20314
|
var SPAWNERS = ["spawn", "spawnSync", "exec", "execSync", "execFile", "execFileSync"];
|
|
@@ -20359,7 +20379,7 @@ function findViolationsInSource(raw) {
|
|
|
20359
20379
|
return found;
|
|
20360
20380
|
}
|
|
20361
20381
|
function policedFiles(root) {
|
|
20362
|
-
const r = (0,
|
|
20382
|
+
const r = (0, import_node_child_process14.spawnSync)("git", ["ls-files", "-z", "--cached", "--others", "--exclude-standard"], {
|
|
20363
20383
|
cwd: root,
|
|
20364
20384
|
encoding: "utf8",
|
|
20365
20385
|
windowsHide: true,
|
|
@@ -20393,7 +20413,7 @@ function runSpawnPolicy(root) {
|
|
|
20393
20413
|
}
|
|
20394
20414
|
|
|
20395
20415
|
// src/test-policy-core.ts
|
|
20396
|
-
var
|
|
20416
|
+
var import_node_child_process15 = require("node:child_process");
|
|
20397
20417
|
var import_node_fs27 = require("node:fs");
|
|
20398
20418
|
var import_node_path24 = require("node:path");
|
|
20399
20419
|
var POLICY_FILE = "test-policy.json";
|
|
@@ -20523,14 +20543,14 @@ function evaluate(changed, policy, present = () => false) {
|
|
|
20523
20543
|
return findings;
|
|
20524
20544
|
}
|
|
20525
20545
|
function git(args, cwd) {
|
|
20526
|
-
return (0,
|
|
20546
|
+
return (0, import_node_child_process15.execFileSync)("git", args, { windowsHide: true, cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 });
|
|
20527
20547
|
}
|
|
20528
20548
|
var COAUTHOR_KEY = "Co-authored-by";
|
|
20529
20549
|
var GH_MESSAGE_SEPARATOR = /^-{5,}$/;
|
|
20530
20550
|
var LIFTED_KEYS = new RegExp(`^(?:${TRAILER_KEY}|${COAUTHOR_KEY}):`, "i");
|
|
20531
20551
|
function parseTrailers(message, cwd) {
|
|
20532
20552
|
try {
|
|
20533
|
-
return (0,
|
|
20553
|
+
return (0, import_node_child_process15.execFileSync)("git", ["interpret-trailers", "--parse", "--unfold"], {
|
|
20534
20554
|
windowsHide: true,
|
|
20535
20555
|
cwd,
|
|
20536
20556
|
input: message,
|
|
@@ -20715,124 +20735,6 @@ function runTestPolicy(root, deps = {}) {
|
|
|
20715
20735
|
return result;
|
|
20716
20736
|
}
|
|
20717
20737
|
|
|
20718
|
-
// src/docs-audit-command.ts
|
|
20719
|
-
function serializeOutcome(outcome) {
|
|
20720
|
-
switch (outcome.kind) {
|
|
20721
|
-
case "clean":
|
|
20722
|
-
return "clean";
|
|
20723
|
-
case "refreshed":
|
|
20724
|
-
return `refreshed-${outcome.count}`;
|
|
20725
|
-
case "failed":
|
|
20726
|
-
return "failed";
|
|
20727
|
-
}
|
|
20728
|
-
}
|
|
20729
|
-
var ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
20730
|
-
function isValidIsoDate(date) {
|
|
20731
|
-
if (typeof date !== "string" || !ISO_DATE_RE.test(date)) return false;
|
|
20732
|
-
const parsed = Date.parse(`${date}T00:00:00Z`);
|
|
20733
|
-
if (Number.isNaN(parsed)) return false;
|
|
20734
|
-
return new Date(parsed).toISOString().slice(0, 10) === date;
|
|
20735
|
-
}
|
|
20736
|
-
var OUTCOME_WIRE_RE = /^(clean|refreshed-[1-9]\d*|failed)$/;
|
|
20737
|
-
function isValidOutcomeWire(outcome) {
|
|
20738
|
-
return typeof outcome === "string" && OUTCOME_WIRE_RE.test(outcome);
|
|
20739
|
-
}
|
|
20740
|
-
function docsAuditRecord(input) {
|
|
20741
|
-
const repo = input.repo.trim();
|
|
20742
|
-
const date = input.date.trim();
|
|
20743
|
-
const shaRange = input.shaRange.trim();
|
|
20744
|
-
const checkerVendor = input.checkerVendor.trim();
|
|
20745
|
-
if (!repo) throw new Error("docs audit record: repo is required");
|
|
20746
|
-
if (!date) throw new Error("docs audit record: date is required");
|
|
20747
|
-
if (!isValidIsoDate(date)) throw new Error(`docs audit record: date must be a real ISO date (YYYY-MM-DD), got "${date}"`);
|
|
20748
|
-
if (!shaRange) throw new Error("docs audit record: shaRange is required");
|
|
20749
|
-
if (!checkerVendor) throw new Error("docs audit record: checkerVendor is required");
|
|
20750
|
-
if (input.outcome.kind === "refreshed" && !(Number.isInteger(input.outcome.count) && input.outcome.count > 0)) {
|
|
20751
|
-
throw new Error("docs audit record: a `refreshed` outcome must carry a positive integer count");
|
|
20752
|
-
}
|
|
20753
|
-
if (input.outcome.kind === "failed" && !input.outcome.reason.trim()) {
|
|
20754
|
-
throw new Error("docs audit record: a `failed` outcome must carry a reason");
|
|
20755
|
-
}
|
|
20756
|
-
const outcome = serializeOutcome(input.outcome);
|
|
20757
|
-
if (!isValidOutcomeWire(outcome)) {
|
|
20758
|
-
throw new Error(`docs audit record: outcome serialized to an invalid wire form "${outcome}"`);
|
|
20759
|
-
}
|
|
20760
|
-
return { repo, date, shaRange, outcome, checkerVendor };
|
|
20761
|
-
}
|
|
20762
|
-
function ageInDays(verdictDate, today) {
|
|
20763
|
-
const a = Date.parse(`${verdictDate}T00:00:00Z`);
|
|
20764
|
-
const b = Date.parse(`${today}T00:00:00Z`);
|
|
20765
|
-
return Math.max(0, Math.round((b - a) / 864e5));
|
|
20766
|
-
}
|
|
20767
|
-
function docsAuditStatus(fetch2, opts) {
|
|
20768
|
-
if ("notArmed" in fetch2) {
|
|
20769
|
-
return { ok: true, state: "not-armed", line: `docs audit: ${opts.repo} janitor not armed (registry route not live yet)` };
|
|
20770
|
-
}
|
|
20771
|
-
if (!fetch2.ok) {
|
|
20772
|
-
return { ok: false, state: "error", line: `docs audit: ${opts.repo} registry read failed \u2014 ${fetch2.error}` };
|
|
20773
|
-
}
|
|
20774
|
-
if (!isValidIsoDate(opts.today)) {
|
|
20775
|
-
throw new Error(`docs audit status: today must be a real ISO date (YYYY-MM-DD), got "${opts.today}"`);
|
|
20776
|
-
}
|
|
20777
|
-
if (fetch2.verdict === null) {
|
|
20778
|
-
const armedAt = opts.armedAt;
|
|
20779
|
-
if (armedAt && isValidIsoDate(armedAt)) {
|
|
20780
|
-
const cadenceDays = opts.cadenceDays ?? 7;
|
|
20781
|
-
const graceDays = opts.graceDays ?? 3;
|
|
20782
|
-
const sinceArmed = ageInDays(armedAt, opts.today);
|
|
20783
|
-
if (sinceArmed <= cadenceDays + graceDays) {
|
|
20784
|
-
return {
|
|
20785
|
-
ok: true,
|
|
20786
|
-
state: "awaiting-first-tick",
|
|
20787
|
-
line: `docs audit: ${opts.repo} armed ${armedAt} (${sinceArmed}d ago) \u2014 no verdict expected until the first tick`
|
|
20788
|
-
};
|
|
20789
|
-
}
|
|
20790
|
-
return {
|
|
20791
|
-
ok: false,
|
|
20792
|
-
state: "missing",
|
|
20793
|
-
line: `docs audit: ${opts.repo} no verdict on record ${sinceArmed}d after arming ${armedAt} \u2014 janitor blind or dead`
|
|
20794
|
-
};
|
|
20795
|
-
}
|
|
20796
|
-
return { ok: false, state: "missing", line: `docs audit: ${opts.repo} no verdict on record \u2014 janitor blind or dead` };
|
|
20797
|
-
}
|
|
20798
|
-
const verdict = fetch2.verdict;
|
|
20799
|
-
for (const field of ["repo", "shaRange", "checkerVendor"]) {
|
|
20800
|
-
const value = verdict[field];
|
|
20801
|
-
if (typeof value !== "string" || !value.trim()) {
|
|
20802
|
-
const shown = typeof value === "string" ? `"${value}"` : `type ${Array.isArray(value) ? "array" : typeof value}`;
|
|
20803
|
-
return {
|
|
20804
|
-
ok: false,
|
|
20805
|
-
state: "malformed",
|
|
20806
|
-
line: `docs audit: ${opts.repo} malformed verdict ${field} (${shown}, expected a non-empty string) \u2014 registry record corrupt, treating as RED`
|
|
20807
|
-
};
|
|
20808
|
-
}
|
|
20809
|
-
}
|
|
20810
|
-
if (!isValidIsoDate(verdict.date)) {
|
|
20811
|
-
return {
|
|
20812
|
-
ok: false,
|
|
20813
|
-
state: "malformed",
|
|
20814
|
-
line: `docs audit: ${opts.repo} malformed verdict date "${verdict.date}" \u2014 registry record corrupt, treating as RED`
|
|
20815
|
-
};
|
|
20816
|
-
}
|
|
20817
|
-
if (!isValidOutcomeWire(verdict.outcome)) {
|
|
20818
|
-
return {
|
|
20819
|
-
ok: false,
|
|
20820
|
-
state: "malformed",
|
|
20821
|
-
line: `docs audit: ${opts.repo} malformed verdict outcome "${verdict.outcome}" (expected clean|refreshed-N|failed) \u2014 registry record corrupt, treating as RED`
|
|
20822
|
-
};
|
|
20823
|
-
}
|
|
20824
|
-
const cadence = opts.cadenceDays ?? 7;
|
|
20825
|
-
const grace = opts.graceDays ?? 3;
|
|
20826
|
-
const age = ageInDays(verdict.date, opts.today);
|
|
20827
|
-
if (age > cadence + grace) {
|
|
20828
|
-
return { ok: false, state: "stale", line: `docs audit: ${opts.repo} last verdict ${age}d old \u2014 janitor blind or dead` };
|
|
20829
|
-
}
|
|
20830
|
-
if (verdict.outcome === "failed") {
|
|
20831
|
-
return { ok: false, state: "failed", line: `docs audit: ${opts.repo} last run FAILED (${verdict.date}) \u2014 janitor needs attention` };
|
|
20832
|
-
}
|
|
20833
|
-
return { ok: true, state: "clean", line: `docs audit: ${opts.repo} ${verdict.outcome} (${verdict.date}, ${verdict.checkerVendor})` };
|
|
20834
|
-
}
|
|
20835
|
-
|
|
20836
20738
|
// src/project-info-sync.ts
|
|
20837
20739
|
var import_node_fs28 = require("node:fs");
|
|
20838
20740
|
var import_node_path25 = require("node:path");
|
|
@@ -21021,7 +20923,7 @@ function authorizeBodyHasMismatch(body) {
|
|
|
21021
20923
|
}
|
|
21022
20924
|
|
|
21023
20925
|
// src/project-set.ts
|
|
21024
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate"];
|
|
20926
|
+
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate", "seedCanary"];
|
|
21025
20927
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
21026
20928
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
21027
20929
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -21275,6 +21177,11 @@ function parseRuntimeVaultOnlyVar(raw) {
|
|
|
21275
21177
|
if (raw === "false") return false;
|
|
21276
21178
|
throw new Error("org project set: runtimeVaultOnly must be true or false");
|
|
21277
21179
|
}
|
|
21180
|
+
function parseSeedCanaryVar(raw) {
|
|
21181
|
+
if (raw === "true") return true;
|
|
21182
|
+
if (raw === "false") return false;
|
|
21183
|
+
throw new Error("org project set: seedCanary must be true or false");
|
|
21184
|
+
}
|
|
21278
21185
|
function parseConsumesDesignSystemVar(raw) {
|
|
21279
21186
|
if (raw === "fofu") return raw;
|
|
21280
21187
|
throw new Error('org project set: consumesDesignSystem must be "fofu"');
|
|
@@ -21364,7 +21271,8 @@ var SETTABLE_VAR_KEYS = [
|
|
|
21364
21271
|
"ci",
|
|
21365
21272
|
"requiredChecks",
|
|
21366
21273
|
"gate",
|
|
21367
|
-
"secrets"
|
|
21274
|
+
"secrets",
|
|
21275
|
+
"seedCanary"
|
|
21368
21276
|
];
|
|
21369
21277
|
var SETTABLE_VAR_KEY_SET = new Set(SETTABLE_VAR_KEYS);
|
|
21370
21278
|
var SETTABLE_VAR_HINTS = {
|
|
@@ -21375,6 +21283,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
21375
21283
|
fofuEnabled: "true|false",
|
|
21376
21284
|
consumesDesignSystem: '"fofu"',
|
|
21377
21285
|
runtimeVaultOnly: "true|false",
|
|
21286
|
+
seedCanary: "true|false",
|
|
21378
21287
|
repos: 'JSON array, e.g. ["mutmutco/mm-foo"]',
|
|
21379
21288
|
oauth: "JSON {subdomains,domains,callbackPath,fofuSubdomain}",
|
|
21380
21289
|
requiredGcpApis: "comma-string",
|
|
@@ -21471,6 +21380,8 @@ function buildProjectSetPatch(input) {
|
|
|
21471
21380
|
patch[key] = parseFofuEnabledVar(raw);
|
|
21472
21381
|
} else if (key === "runtimeVaultOnly") {
|
|
21473
21382
|
patch[key] = parseRuntimeVaultOnlyVar(raw);
|
|
21383
|
+
} else if (key === "seedCanary") {
|
|
21384
|
+
patch[key] = parseSeedCanaryVar(raw);
|
|
21474
21385
|
} else if (key === "consumesDesignSystem") {
|
|
21475
21386
|
patch[key] = parseConsumesDesignSystemVar(raw);
|
|
21476
21387
|
} else if (key === "publishDir") {
|
|
@@ -22503,7 +22414,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
22503
22414
|
|
|
22504
22415
|
// src/schedules-commands.ts
|
|
22505
22416
|
var import_promises4 = require("node:fs/promises");
|
|
22506
|
-
var
|
|
22417
|
+
var import_node_child_process16 = require("node:child_process");
|
|
22507
22418
|
var import_node_util7 = require("node:util");
|
|
22508
22419
|
|
|
22509
22420
|
// src/schedules.ts
|
|
@@ -22814,7 +22725,7 @@ function cadenceStale(registryCadence, liveCadence) {
|
|
|
22814
22725
|
if (!liveCrons.length) return false;
|
|
22815
22726
|
return !liveCrons.every((cron) => registered.has(cron));
|
|
22816
22727
|
}
|
|
22817
|
-
function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22728
|
+
function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set(), disabledWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22818
22729
|
const registryGithub = registry2.filter((r) => r.executor === "github-actions");
|
|
22819
22730
|
const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
|
|
22820
22731
|
const registryById = new Map(registryGithub.map((r) => [r.id, r]));
|
|
@@ -22845,6 +22756,18 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22845
22756
|
continue;
|
|
22846
22757
|
}
|
|
22847
22758
|
if (activeWorkflowNames.has(r.id)) continue;
|
|
22759
|
+
if (disabledWorkflowNames.has(r.id)) {
|
|
22760
|
+
if (readRepos.has(r.repo)) {
|
|
22761
|
+
drifts.push({
|
|
22762
|
+
class: "registered-but-disabled",
|
|
22763
|
+
name: r.id,
|
|
22764
|
+
executor: r.executor || "github-actions",
|
|
22765
|
+
detail: "workflow file present but DISABLED in GitHub Actions \u2014 the dispatcher cannot run it",
|
|
22766
|
+
remedy: `\`gh workflow enable\` it in ${r.repo} to re-arm (or, if it was retired on purpose, re-run schedules register so the replace prunes the SCHEDULE# row) \u2014 never prune a lane parked on purpose`
|
|
22767
|
+
});
|
|
22768
|
+
}
|
|
22769
|
+
continue;
|
|
22770
|
+
}
|
|
22848
22771
|
if (readRepos.has(r.repo)) {
|
|
22849
22772
|
drifts.push({
|
|
22850
22773
|
class: "registered-but-dead",
|
|
@@ -22920,7 +22843,7 @@ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
|
|
|
22920
22843
|
}
|
|
22921
22844
|
return drifts.sort((a, b) => a.name.localeCompare(b.name));
|
|
22922
22845
|
}
|
|
22923
|
-
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}) {
|
|
22846
|
+
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}, disabledWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22924
22847
|
if (registry2 === null) {
|
|
22925
22848
|
return {
|
|
22926
22849
|
reconciliation: [],
|
|
@@ -22930,7 +22853,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
|
|
|
22930
22853
|
}
|
|
22931
22854
|
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
22932
22855
|
const drifts = [
|
|
22933
|
-
...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames),
|
|
22856
|
+
...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames),
|
|
22934
22857
|
...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
|
|
22935
22858
|
schedulerRead: Boolean(harbour.schedulerRead),
|
|
22936
22859
|
now: harbour.now
|
|
@@ -22940,7 +22863,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
|
|
|
22940
22863
|
}
|
|
22941
22864
|
|
|
22942
22865
|
// src/schedules-commands.ts
|
|
22943
|
-
var execFileP5 = (0, import_node_util7.promisify)(
|
|
22866
|
+
var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process16.execFile);
|
|
22944
22867
|
var AWS_REGION = "eu-central-1";
|
|
22945
22868
|
var AWS_TIMEOUT_MS = 3e4;
|
|
22946
22869
|
var AWS_RETRY_DELAY_MS = 1500;
|
|
@@ -22964,9 +22887,15 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
22964
22887
|
const entries = [];
|
|
22965
22888
|
const failures = [];
|
|
22966
22889
|
const workflows = [];
|
|
22890
|
+
const disabled = [];
|
|
22967
22891
|
for (const wf of workflowsList) {
|
|
22968
|
-
if (
|
|
22892
|
+
if (typeof wf?.path !== "string" || !wf.path) continue;
|
|
22969
22893
|
if (!wf.path.startsWith(".github/workflows/")) continue;
|
|
22894
|
+
if (wf.state !== "active") {
|
|
22895
|
+
const basename5 = wf.path.split("/").pop() ?? wf.path;
|
|
22896
|
+
disabled.push(`${repo}/${basename5.replace(/\.ya?ml$/, "")}`);
|
|
22897
|
+
continue;
|
|
22898
|
+
}
|
|
22970
22899
|
try {
|
|
22971
22900
|
const contents = await client.rest(
|
|
22972
22901
|
"GET",
|
|
@@ -22985,7 +22914,7 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
22985
22914
|
else throw e;
|
|
22986
22915
|
}
|
|
22987
22916
|
}
|
|
22988
|
-
return { entries, failures, workflows };
|
|
22917
|
+
return { entries, failures, workflows, disabled };
|
|
22989
22918
|
}
|
|
22990
22919
|
async function githubEntries(client) {
|
|
22991
22920
|
const entries = [];
|
|
@@ -22993,12 +22922,13 @@ async function githubEntries(client) {
|
|
|
22993
22922
|
const drift = [];
|
|
22994
22923
|
const readRepos = [];
|
|
22995
22924
|
const workflows = [];
|
|
22925
|
+
const disabledWorkflowNames = [];
|
|
22996
22926
|
let repos;
|
|
22997
22927
|
try {
|
|
22998
22928
|
const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
|
|
22999
22929
|
repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
|
|
23000
22930
|
} catch (e) {
|
|
23001
|
-
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [] };
|
|
22931
|
+
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [], disabledWorkflowNames: [] };
|
|
23002
22932
|
}
|
|
23003
22933
|
const results = await Promise.all(
|
|
23004
22934
|
repos.map(async (repo) => {
|
|
@@ -23015,6 +22945,7 @@ async function githubEntries(client) {
|
|
|
23015
22945
|
readRepos.push(r.repo);
|
|
23016
22946
|
entries.push(...r.entries);
|
|
23017
22947
|
workflows.push(...r.workflows);
|
|
22948
|
+
disabledWorkflowNames.push(...r.disabled);
|
|
23018
22949
|
if (r.failures.length) {
|
|
23019
22950
|
drift.push(`${r.repo}: ${r.failures.length} workflow record(s) listed active with no file on the default branch (deleted one-offs?) \u2014 deregister them: ${r.failures.map((f) => f.split(": ")[1]).join(", ")}`);
|
|
23020
22951
|
}
|
|
@@ -23022,7 +22953,7 @@ async function githubEntries(client) {
|
|
|
23022
22953
|
}
|
|
23023
22954
|
const reconciliation = [...strayCronDrifts(workflows), ...unlauncheredLlmDrifts(entries)];
|
|
23024
22955
|
drift.push(...reconciliation.map(renderDrift));
|
|
23025
|
-
return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name) };
|
|
22956
|
+
return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name), disabledWorkflowNames };
|
|
23026
22957
|
}
|
|
23027
22958
|
async function awsJson(args) {
|
|
23028
22959
|
const run = async () => {
|
|
@@ -23077,7 +23008,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
23077
23008
|
// #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
|
|
23078
23009
|
awsEntries: aws.entries,
|
|
23079
23010
|
schedulerRead: Boolean(aws.schedulerRead)
|
|
23080
|
-
});
|
|
23011
|
+
}, new Set(gh.disabledWorkflowNames));
|
|
23081
23012
|
const selfManaged = /* @__PURE__ */ new Set();
|
|
23082
23013
|
for (const proj of projects ?? []) {
|
|
23083
23014
|
if (proj?.schedulesMode !== "self-managed") continue;
|
|
@@ -23926,6 +23857,7 @@ var import_node_os11 = require("node:os");
|
|
|
23926
23857
|
var import_node_path29 = require("node:path");
|
|
23927
23858
|
|
|
23928
23859
|
// src/bootstrap-drift.ts
|
|
23860
|
+
var import_node_crypto6 = require("node:crypto");
|
|
23929
23861
|
function byteComparableSeeds(manifest, cls) {
|
|
23930
23862
|
return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
|
|
23931
23863
|
}
|
|
@@ -23934,6 +23866,9 @@ function compareSeedBytes(hubContent, repoContent) {
|
|
|
23934
23866
|
const normalize = (s) => s.replace(/\r\n/g, "\n");
|
|
23935
23867
|
return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
|
|
23936
23868
|
}
|
|
23869
|
+
function seedContentHash(content) {
|
|
23870
|
+
return (0, import_node_crypto6.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
|
|
23871
|
+
}
|
|
23937
23872
|
function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
23938
23873
|
const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
|
|
23939
23874
|
const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
|
|
@@ -23956,11 +23891,14 @@ function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
|
23956
23891
|
findings.push({ repo, target: seed.target, state: "waived", detail: `${state} \u2014 waived: ${why}` });
|
|
23957
23892
|
continue;
|
|
23958
23893
|
}
|
|
23894
|
+
const repoContent = byTarget.get(seed.target);
|
|
23959
23895
|
findings.push({
|
|
23960
23896
|
repo,
|
|
23961
23897
|
target: seed.target,
|
|
23962
23898
|
state,
|
|
23963
|
-
detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 propagate
|
|
23899
|
+
detail: state === "absent" ? "declared org-owned in the manifest but not present on the base branch" : "differs from MMI-Hub's copy \u2014 fix this in MMI-Hub and let it propagate (#4233); `mmi-cli bootstrap apply <repo> --only <target> --execute` remains for THIS repo's own bootstrap/onboarding, never a fleet-wide fan-out (#4241 fails a hand-edited fleet copy at the merge gate on repos carrying the new seed), or, if this repo is RIGHT to differ, declare a waiver for it on the seed in the manifest (#3842)",
|
|
23900
|
+
// #4242: only a real 'drift' has bytes worth hashing — 'absent' has none to compare against history.
|
|
23901
|
+
...state === "drift" && repoContent != null ? { contentHash: seedContentHash(repoContent) } : {}
|
|
23964
23902
|
});
|
|
23965
23903
|
}
|
|
23966
23904
|
return findings;
|
|
@@ -23979,6 +23917,171 @@ function renderSeedDriftReport(findings, reposAudited, seedsPerRepo) {
|
|
|
23979
23917
|
return lines.join("\n");
|
|
23980
23918
|
}
|
|
23981
23919
|
|
|
23920
|
+
// src/bootstrap-propagate.ts
|
|
23921
|
+
function assertPropagationCoverage(rosterCount, independentRegistryCount) {
|
|
23922
|
+
if (rosterCount === 0) {
|
|
23923
|
+
throw new Error("bootstrap propagate: roster read 0 repos \u2014 refusing to report a plan from a roster this command could not establish (#4232 coverage invariant)");
|
|
23924
|
+
}
|
|
23925
|
+
if (rosterCount < independentRegistryCount) {
|
|
23926
|
+
throw new Error(
|
|
23927
|
+
`bootstrap propagate: roster read ${rosterCount} repo(s) but the independent registry count is ${independentRegistryCount} \u2014 this read did not cover the fleet (#4232 coverage invariant)`
|
|
23928
|
+
);
|
|
23929
|
+
}
|
|
23930
|
+
}
|
|
23931
|
+
function assignWaves(repos, canarySlug) {
|
|
23932
|
+
const waves = /* @__PURE__ */ new Map();
|
|
23933
|
+
if (!canarySlug) return waves;
|
|
23934
|
+
const rest = repos.filter((r) => r.slug !== canarySlug).slice().sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
|
|
23935
|
+
const canary = repos.find((r) => r.slug === canarySlug);
|
|
23936
|
+
if (canary) waves.set(canary.repo, 0);
|
|
23937
|
+
const wave1Count = Math.ceil(rest.length * 0.25);
|
|
23938
|
+
rest.forEach((r, i) => waves.set(r.repo, i < wave1Count ? 1 : 2));
|
|
23939
|
+
return waves;
|
|
23940
|
+
}
|
|
23941
|
+
function statusFor(read) {
|
|
23942
|
+
if (!read) return { status: "pending", record: {} };
|
|
23943
|
+
if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
|
|
23944
|
+
const pr2 = read.pr;
|
|
23945
|
+
if (!pr2) return { status: "pending", record: {} };
|
|
23946
|
+
if (pr2.state === "closed") return { status: "closed-unmerged", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
23947
|
+
if (pr2.checks === "red") return { status: "red", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
23948
|
+
return { status: "open-pending", record: { prNumber: pr2.number, prUrl: pr2.url, mergeSha: pr2.mergeSha } };
|
|
23949
|
+
}
|
|
23950
|
+
var HALTING_STATUSES = /* @__PURE__ */ new Set(["red", "closed-unmerged"]);
|
|
23951
|
+
function planPropagationTick(input) {
|
|
23952
|
+
const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied } = input;
|
|
23953
|
+
const readByRepo = new Map(reads.map((r) => [r.repo, r]));
|
|
23954
|
+
const records = [];
|
|
23955
|
+
const opened = [];
|
|
23956
|
+
const waived = repos.filter((r) => r.waiver);
|
|
23957
|
+
for (const r of waived) {
|
|
23958
|
+
records.push({ repo: r.repo, target, wave: null, status: "skipped-waived", action: "none", detail: `waived: ${r.waiver}` });
|
|
23959
|
+
}
|
|
23960
|
+
const eligible = repos.filter((r) => !r.waiver);
|
|
23961
|
+
const refusedNoCanary = !canarySlug || !eligible.some((r) => r.slug === canarySlug);
|
|
23962
|
+
if (refusedNoCanary) {
|
|
23963
|
+
for (const r of eligible) {
|
|
23964
|
+
records.push({ repo: r.repo, target, wave: null, status: "not-yet-reached", action: "none", detail: "no canary declared for this target \u2014 refusing to plan any wave (#4238 canary requirement)" });
|
|
23965
|
+
}
|
|
23966
|
+
return { target, reposInScope: repos.length, canary: null, refusedNoCanary: true, halted: false, haltReason: null, opened, records };
|
|
23967
|
+
}
|
|
23968
|
+
const waveOf = assignWaves(eligible, canarySlug);
|
|
23969
|
+
const byWave = { 0: [], 1: [], 2: [] };
|
|
23970
|
+
for (const r of eligible) byWave[waveOf.get(r.repo) ?? 2].push(r);
|
|
23971
|
+
let halted = false;
|
|
23972
|
+
let haltReason = null;
|
|
23973
|
+
let waveGateOpen = true;
|
|
23974
|
+
for (const waveNum of [0, 1, 2]) {
|
|
23975
|
+
const waveRepos = byWave[waveNum];
|
|
23976
|
+
if (!waveRepos.length) continue;
|
|
23977
|
+
if (!waveGateOpen || halted) {
|
|
23978
|
+
for (const r of waveRepos) {
|
|
23979
|
+
records.push({ repo: r.repo, target, wave: waveNum, status: "not-yet-reached", action: "none", detail: "prior wave not yet converged" });
|
|
23980
|
+
}
|
|
23981
|
+
continue;
|
|
23982
|
+
}
|
|
23983
|
+
let waveAllMatch = true;
|
|
23984
|
+
let waveHasRed = false;
|
|
23985
|
+
for (const r of waveRepos) {
|
|
23986
|
+
const { status, record } = statusFor(readByRepo.get(r.repo));
|
|
23987
|
+
const shouldOpen = status === "pending";
|
|
23988
|
+
if (shouldOpen) opened.push(r.repo);
|
|
23989
|
+
records.push({
|
|
23990
|
+
repo: r.repo,
|
|
23991
|
+
target,
|
|
23992
|
+
wave: waveNum,
|
|
23993
|
+
status,
|
|
23994
|
+
action: shouldOpen ? "open-pr" : "none",
|
|
23995
|
+
detail: shouldOpen ? "no open propagation PR and not yet matching \u2014 opening this tick" : status,
|
|
23996
|
+
...record
|
|
23997
|
+
});
|
|
23998
|
+
if (status !== "match") waveAllMatch = false;
|
|
23999
|
+
if (HALTING_STATUSES.has(status)) waveHasRed = true;
|
|
24000
|
+
}
|
|
24001
|
+
if (waveHasRed) {
|
|
24002
|
+
halted = true;
|
|
24003
|
+
haltReason = `wave ${waveNum} has a red or closed-unmerged PR \u2014 halting; no further wave opens (#4238 halt-and-alarm)`;
|
|
24004
|
+
waveGateOpen = false;
|
|
24005
|
+
continue;
|
|
24006
|
+
}
|
|
24007
|
+
if (waveNum === 0 && isWorkflowSeed && !functionGateSatisfied) {
|
|
24008
|
+
waveGateOpen = false;
|
|
24009
|
+
continue;
|
|
24010
|
+
}
|
|
24011
|
+
waveGateOpen = waveAllMatch;
|
|
24012
|
+
}
|
|
24013
|
+
return { target, reposInScope: repos.length, canary: canarySlug, refusedNoCanary: false, halted, haltReason, opened, records };
|
|
24014
|
+
}
|
|
24015
|
+
function renderPropagationReport(plan) {
|
|
24016
|
+
const lines = [`bootstrap propagate \u2014 target ${plan.target}: ${plan.reposInScope} repo(s) in scope, canary=${plan.canary ?? "NONE"}`];
|
|
24017
|
+
if (plan.refusedNoCanary) {
|
|
24018
|
+
lines.push(" REFUSED \u2014 no canary declared for this target; plan every repo not-yet-reached");
|
|
24019
|
+
return lines.join("\n");
|
|
24020
|
+
}
|
|
24021
|
+
for (const r of plan.records) {
|
|
24022
|
+
const wave2 = r.wave == null ? "-" : String(r.wave);
|
|
24023
|
+
lines.push(` wave${wave2.padEnd(2)} ${r.status.padEnd(16)} ${r.repo}${r.prNumber ? ` PR#${r.prNumber}` : ""} \u2014 ${r.detail}`);
|
|
24024
|
+
}
|
|
24025
|
+
lines.push(plan.halted ? ` HALTED \u2014 ${plan.haltReason}` : ` opened this tick: ${plan.opened.length ? plan.opened.join(", ") : "(none)"}`);
|
|
24026
|
+
return lines.join("\n");
|
|
24027
|
+
}
|
|
24028
|
+
|
|
24029
|
+
// src/bootstrap-rollback.ts
|
|
24030
|
+
function resolveRollbackRecord(records, repo, target) {
|
|
24031
|
+
const candidates = records.filter((r) => r.repo === repo && r.target === target && r.mergeSha);
|
|
24032
|
+
if (candidates.length === 0) {
|
|
24033
|
+
return { found: false, reason: `no propagation record resolves for ${repo} + ${target} \u2014 refusing to guess a commit to revert` };
|
|
24034
|
+
}
|
|
24035
|
+
const clean4 = candidates.filter((r) => r.files.length === 1 && r.files[0] === target);
|
|
24036
|
+
if (clean4.length === 0) {
|
|
24037
|
+
return {
|
|
24038
|
+
found: false,
|
|
24039
|
+
reason: `${candidates.length} merged seed-propagate PR(s) found for ${repo} + ${target}, but none is a clean single-file propagation of exactly this target \u2014 refusing to guess a commit to revert`
|
|
24040
|
+
};
|
|
24041
|
+
}
|
|
24042
|
+
const latest = clean4.slice().sort((a, b) => a.mergedAt < b.mergedAt ? 1 : a.mergedAt > b.mergedAt ? -1 : 0)[0];
|
|
24043
|
+
return { found: true, record: latest };
|
|
24044
|
+
}
|
|
24045
|
+
function planRollback(repo, target, slug, records) {
|
|
24046
|
+
const resolution = resolveRollbackRecord(records, repo, target);
|
|
24047
|
+
if (!resolution.found) return { repo, target, resolution };
|
|
24048
|
+
const branch = `seed-rollback-${slug}`;
|
|
24049
|
+
const title = `revert: rollback org-owned ${target} in ${repo} (seed PR #${resolution.record.number})`;
|
|
24050
|
+
const body = renderRollbackPrBody(resolution.record);
|
|
24051
|
+
return { repo, target, resolution, branch, title, body };
|
|
24052
|
+
}
|
|
24053
|
+
function renderRollbackPrBody(record) {
|
|
24054
|
+
return [
|
|
24055
|
+
`Per-repo emergency revert of \`${record.target}\`'s seed-propagate merge (\`mmi-cli bootstrap rollback\`, #4240).`,
|
|
24056
|
+
"",
|
|
24057
|
+
`Reverts ${record.url} (merge ${record.mergeSha}) \u2014 restores this repo's copy of \`${record.target}\` to its content immediately before that merge. Nothing else in this repo changes.`,
|
|
24058
|
+
"",
|
|
24059
|
+
"This is the emergency stop, not the fix: MMI-Hub is still the source of truth during a rollback. Closure is the Hub reverting (or fixing forward) the bad seed commit on `development` \u2014 once it does, this repo matches the Hub again and the drift alarm closes itself. A revert with no Hub-side follow-up re-alarms as drift within a week \u2014 deliberately, so an emergency divergence can never silently become permanent.",
|
|
24060
|
+
"",
|
|
24061
|
+
"Never re-run `bootstrap propagate` with the pre-revert bytes before the Hub itself is fixed \u2014 that reopens exactly what this PR closes."
|
|
24062
|
+
].join("\n");
|
|
24063
|
+
}
|
|
24064
|
+
function renderRollbackReport(plan) {
|
|
24065
|
+
if (!plan.resolution.found) {
|
|
24066
|
+
return `bootstrap rollback \u2014 ${plan.repo} / ${plan.target}: REFUSED \u2014 ${plan.resolution.reason}`;
|
|
24067
|
+
}
|
|
24068
|
+
const r = plan.resolution.record;
|
|
24069
|
+
const opened = plan.prUrl ? ` \u2014 opened ${plan.prUrl}` : "";
|
|
24070
|
+
return `bootstrap rollback \u2014 ${plan.repo} / ${plan.target}: reverting PR#${r.number} (merge ${r.mergeSha}${r.mergedAt ? `, merged ${r.mergedAt}` : ""}) on branch ${plan.branch}${opened}`;
|
|
24071
|
+
}
|
|
24072
|
+
function seedPrRecordFromPropagationRecord(record) {
|
|
24073
|
+
if (!record.mergeSha || record.prNumber == null || !record.prUrl) return null;
|
|
24074
|
+
return {
|
|
24075
|
+
repo: record.repo,
|
|
24076
|
+
target: record.target,
|
|
24077
|
+
number: record.prNumber,
|
|
24078
|
+
url: record.prUrl,
|
|
24079
|
+
mergeSha: record.mergeSha,
|
|
24080
|
+
mergedAt: "",
|
|
24081
|
+
files: [record.target]
|
|
24082
|
+
};
|
|
24083
|
+
}
|
|
24084
|
+
|
|
23982
24085
|
// src/bootstrap-verify.ts
|
|
23983
24086
|
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
23984
24087
|
var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
|
|
@@ -25065,6 +25168,304 @@ LIVE apply to ${repo}:
|
|
|
25065
25168
|
${applied.join("\n ")}`);
|
|
25066
25169
|
}
|
|
25067
25170
|
});
|
|
25171
|
+
bootstrap.command("propagate").description("#4238: re-entrant canary\u2192wave tick \u2014 plan (or, with --execute, open) per-repo PRs fanning an org-owned seed out to the fleet").option("--target <path>", "the manifest target to propagate (an ownership:org + source:self seed, e.g. .github/workflows/agent-pr.yml)").option("--execute", "LIVE tick via gh (master-gated) \u2014 opens/reuses per-repo seed-propagate PRs; dry-run prints the plan only").option("--json", "machine-readable output").action(async () => {
|
|
25172
|
+
const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
|
|
25173
|
+
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
25174
|
+
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap propagate: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the desired state this tick propagates`);
|
|
25175
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
25176
|
+
const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
|
|
25177
|
+
if (!o.target) {
|
|
25178
|
+
return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
|
|
25179
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25180
|
+
}
|
|
25181
|
+
const seed = propagatable.find((s) => s.target === o.target);
|
|
25182
|
+
if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable targets:
|
|
25183
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25184
|
+
if (!(0, import_node_fs31.existsSync)(seed.target)) return fail(`bootstrap propagate: the Hub's own copy of '${seed.target}' is missing \u2014 nothing to propagate`);
|
|
25185
|
+
const hubContent = (0, import_node_fs31.readFileSync)(seed.target, "utf8");
|
|
25186
|
+
const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
|
|
25187
|
+
const cfg = await loadConfig();
|
|
25188
|
+
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
25189
|
+
if (!projects || projects.length === 0) {
|
|
25190
|
+
return failGraceful("bootstrap propagate: the registry roster is unreadable or empty \u2014 refusing to plan a tick from a scope this command could not establish (#4232 coverage invariant)");
|
|
25191
|
+
}
|
|
25192
|
+
const rosterRepos2 = collectRegistryRepos(projects).filter((r) => r.toLowerCase() !== "mutmutco/mmi-hub");
|
|
25193
|
+
let independentCount = rosterRepos2.length;
|
|
25194
|
+
if ((0, import_node_fs31.existsSync)("projects.json")) {
|
|
25195
|
+
try {
|
|
25196
|
+
const local = JSON.parse((0, import_node_fs31.readFileSync)("projects.json", "utf8"));
|
|
25197
|
+
const localRepos = /* @__PURE__ */ new Set();
|
|
25198
|
+
for (const p of local.projects ?? []) for (const r of p.repos ?? []) {
|
|
25199
|
+
const full = (r.includes("/") ? r : `mutmutco/${r}`).toLowerCase();
|
|
25200
|
+
if (full !== "mutmutco/mmi-hub") localRepos.add(full);
|
|
25201
|
+
}
|
|
25202
|
+
if (localRepos.size > 0) independentCount = localRepos.size;
|
|
25203
|
+
} catch {
|
|
25204
|
+
}
|
|
25205
|
+
}
|
|
25206
|
+
try {
|
|
25207
|
+
assertPropagationCoverage(rosterRepos2.length, independentCount);
|
|
25208
|
+
} catch (e) {
|
|
25209
|
+
return fail(e.message);
|
|
25210
|
+
}
|
|
25211
|
+
const bySlugMeta = new Map(projects.flatMap((p) => (p.repos ?? []).map((r) => [(r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p])));
|
|
25212
|
+
const classOf = (repo) => bySlugMeta.get(repo.toLowerCase())?.class ?? "deployable";
|
|
25213
|
+
const canaryProject = projects.find((p) => p.seedCanary === true);
|
|
25214
|
+
const canarySlug = canaryProject ? (canaryProject.repos ?? [])[0]?.split("/").pop()?.toLowerCase() ?? null : null;
|
|
25215
|
+
const repos = rosterRepos2.map((repo) => {
|
|
25216
|
+
const slug = repo.split("/").pop().toLowerCase();
|
|
25217
|
+
const waiver = seed.waivers?.[slug];
|
|
25218
|
+
return { repo, slug, waiver };
|
|
25219
|
+
});
|
|
25220
|
+
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
25221
|
+
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
25222
|
+
const branchPrefix = "seed-propagate";
|
|
25223
|
+
const reads = [];
|
|
25224
|
+
for (const r of repos) {
|
|
25225
|
+
if (r.waiver) continue;
|
|
25226
|
+
const baseBranch = classOf(r.repo) === "content" ? "main" : "development";
|
|
25227
|
+
let content = null;
|
|
25228
|
+
try {
|
|
25229
|
+
const resp = await gh(["api", `repos/${r.repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
|
|
25230
|
+
const parsed = JSON.parse(resp.stdout);
|
|
25231
|
+
content = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
|
|
25232
|
+
} catch {
|
|
25233
|
+
content = null;
|
|
25234
|
+
}
|
|
25235
|
+
const drift = compareSeedBytes(hubContent, content);
|
|
25236
|
+
let pr2;
|
|
25237
|
+
try {
|
|
25238
|
+
const branch = `${branchPrefix}-${r.slug}`;
|
|
25239
|
+
const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit", "--limit", "1"]);
|
|
25240
|
+
const arr = JSON.parse(listed.stdout || "[]");
|
|
25241
|
+
const p = arr[0];
|
|
25242
|
+
if (p) {
|
|
25243
|
+
const rollup = p.statusCheckRollup ?? [];
|
|
25244
|
+
const checks = rollup.length === 0 ? "none" : rollup.some((c) => c.conclusion === "FAILURE" || c.state === "FAILURE") ? "red" : rollup.every((c) => c.conclusion === "SUCCESS" || c.state === "SUCCESS") ? "success" : "pending";
|
|
25245
|
+
pr2 = { number: p.number, url: p.url, state: p.state === "MERGED" ? "merged" : p.state === "CLOSED" ? "closed" : "open", checks, mergeSha: p.mergeCommit?.oid };
|
|
25246
|
+
}
|
|
25247
|
+
} catch {
|
|
25248
|
+
pr2 = void 0;
|
|
25249
|
+
}
|
|
25250
|
+
reads.push({ repo: r.repo, drift: drift === "waived" ? "match" : drift, pr: pr2 });
|
|
25251
|
+
}
|
|
25252
|
+
let functionGateSatisfied = !isWorkflowSeed;
|
|
25253
|
+
if (isWorkflowSeed && canarySlug) {
|
|
25254
|
+
const canaryRepo = repos.find((r) => r.slug === canarySlug)?.repo;
|
|
25255
|
+
const canaryRead = reads.find((r) => r.repo === canaryRepo);
|
|
25256
|
+
if (canaryRead?.drift === "match") {
|
|
25257
|
+
try {
|
|
25258
|
+
const workflowFile = seed.target.split("/").pop();
|
|
25259
|
+
const runs = await gh(["api", `repos/${canaryRepo}/actions/workflows/${workflowFile}/runs?status=success&per_page=1`]);
|
|
25260
|
+
const parsed = JSON.parse(runs.stdout);
|
|
25261
|
+
functionGateSatisfied = Array.isArray(parsed.workflow_runs) && parsed.workflow_runs.length > 0;
|
|
25262
|
+
} catch {
|
|
25263
|
+
functionGateSatisfied = false;
|
|
25264
|
+
}
|
|
25265
|
+
}
|
|
25266
|
+
}
|
|
25267
|
+
const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied });
|
|
25268
|
+
if (o.execute) {
|
|
25269
|
+
if (plan.refusedNoCanary) return fail("bootstrap propagate --execute: no canary declared for this target \u2014 refusing to write (set seedCanary:true on exactly one registry repo)");
|
|
25270
|
+
const headSha = (await gh(["api", "repos/mutmutco/MMI-Hub/commits/development", "--jq", ".sha"])).stdout.trim();
|
|
25271
|
+
for (const rec of plan.records) {
|
|
25272
|
+
if (rec.action !== "open-pr") continue;
|
|
25273
|
+
const repoEntry = repos.find((r) => r.repo === rec.repo);
|
|
25274
|
+
const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
|
|
25275
|
+
const branch = `${branchPrefix}-${repoEntry.slug}`;
|
|
25276
|
+
const baseRef = await gh(["api", `repos/${rec.repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
|
|
25277
|
+
const baseSha = baseRef.stdout.trim();
|
|
25278
|
+
let branchExists = true;
|
|
25279
|
+
try {
|
|
25280
|
+
await gh(["api", `repos/${rec.repo}/git/ref/heads/${branch}`]);
|
|
25281
|
+
} catch {
|
|
25282
|
+
branchExists = false;
|
|
25283
|
+
}
|
|
25284
|
+
if (!branchExists) await gh(["api", `repos/${rec.repo}/git/refs`, "-f", `ref=refs/heads/${branch}`, "-f", `sha=${baseSha}`]);
|
|
25285
|
+
let existingSha;
|
|
25286
|
+
try {
|
|
25287
|
+
const cur = await gh(["api", `repos/${rec.repo}/contents/${enc(seed.target)}?ref=${branch}`]);
|
|
25288
|
+
existingSha = JSON.parse(cur.stdout).sha;
|
|
25289
|
+
} catch {
|
|
25290
|
+
existingSha = void 0;
|
|
25291
|
+
}
|
|
25292
|
+
const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-propagate-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
25293
|
+
(0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, hubContent, branch, existingSha)), "utf8");
|
|
25294
|
+
try {
|
|
25295
|
+
await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
|
|
25296
|
+
} finally {
|
|
25297
|
+
try {
|
|
25298
|
+
(0, import_node_fs31.unlinkSync)(tmp);
|
|
25299
|
+
} catch {
|
|
25300
|
+
}
|
|
25301
|
+
}
|
|
25302
|
+
const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
25303
|
+
const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
25304
|
+
let prUrl;
|
|
25305
|
+
if (prDecision.action === "reuse") {
|
|
25306
|
+
prUrl = prDecision.url;
|
|
25307
|
+
} else {
|
|
25308
|
+
const created = await ghCreate([
|
|
25309
|
+
"pr",
|
|
25310
|
+
"create",
|
|
25311
|
+
"--repo",
|
|
25312
|
+
rec.repo,
|
|
25313
|
+
"--base",
|
|
25314
|
+
baseBranch,
|
|
25315
|
+
"--head",
|
|
25316
|
+
branch,
|
|
25317
|
+
"--title",
|
|
25318
|
+
`chore: propagate org-owned ${seed.target} from MMI-Hub (wave ${rec.wave})`,
|
|
25319
|
+
"--body",
|
|
25320
|
+
`Auto-opened by \`mmi-cli bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
|
|
25321
|
+
|
|
25322
|
+
Propagates MMI-Hub@${headSha} 's copy of \`${seed.target}\` to this repo \u2014 the file this PR carries and nothing else.
|
|
25323
|
+
|
|
25324
|
+
Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --execute\` (#4240) reverts this PR's merge commit; never re-run propagate with old bytes.`
|
|
25325
|
+
]);
|
|
25326
|
+
prUrl = created.url;
|
|
25327
|
+
}
|
|
25328
|
+
if (rec.wave !== 0) {
|
|
25329
|
+
await gh(["pr", "merge", prUrl, "--repo", rec.repo, "--auto", "--squash"]).catch(() => {
|
|
25330
|
+
});
|
|
25331
|
+
}
|
|
25332
|
+
rec.prUrl = prUrl;
|
|
25333
|
+
}
|
|
25334
|
+
}
|
|
25335
|
+
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25336
|
+
else console.log(renderPropagationReport(plan));
|
|
25337
|
+
if (plan.halted) process.exitCode = 1;
|
|
25338
|
+
});
|
|
25339
|
+
bootstrap.command("rollback <repo>").description("#4240: open a per-repo revert PR of the recorded seed-propagate merge \u2014 never a fleet-wide overwrite; dry-run unless --execute").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).option("--target <path>", "the manifest target to roll back (an ownership:org + source:self seed, e.g. .github/workflows/agent-pr.yml)").option("--record <path>", "read propagation candidates from a persisted `bootstrap propagate --json` report instead of the live seed-propagate PR history").option("--execute", "LIVE revert via gh (master-gated) \u2014 opens/reuses the seed-rollback PR; dry-run prints the plan only").option("--json", "machine-readable output").action(async (repo) => {
|
|
25340
|
+
const o = {
|
|
25341
|
+
class: rawValue("--class", "deployable"),
|
|
25342
|
+
target: rawValue("--target", ""),
|
|
25343
|
+
record: rawValue("--record", ""),
|
|
25344
|
+
execute: rawFlag("--execute"),
|
|
25345
|
+
json: rawFlag("--json")
|
|
25346
|
+
};
|
|
25347
|
+
if (o.class !== "deployable" && o.class !== "content") return fail("bootstrap rollback: --class must be deployable or content");
|
|
25348
|
+
let parsedRepo;
|
|
25349
|
+
try {
|
|
25350
|
+
parsedRepo = parseOwnerRepo(repo);
|
|
25351
|
+
} catch (e) {
|
|
25352
|
+
return fail(`bootstrap rollback: ${e.message}`);
|
|
25353
|
+
}
|
|
25354
|
+
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
25355
|
+
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap rollback: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the manifest names which targets are org-owned and therefore propagated (and rollback-able)`);
|
|
25356
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
25357
|
+
const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
|
|
25358
|
+
if (!o.target) {
|
|
25359
|
+
return fail(`bootstrap rollback: --target <path> is required \u2014 one of:
|
|
25360
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25361
|
+
}
|
|
25362
|
+
const seed = propagatable.find((s) => s.target === o.target);
|
|
25363
|
+
if (!seed) return fail(`bootstrap rollback: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable (hence rollback-able) targets:
|
|
25364
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25365
|
+
const slug = parsedRepo.slug;
|
|
25366
|
+
const baseBranch = o.class === "content" ? "main" : "development";
|
|
25367
|
+
const branchPrefix = "seed-propagate";
|
|
25368
|
+
const propagateBranch = `${branchPrefix}-${slug}`;
|
|
25369
|
+
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
25370
|
+
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
25371
|
+
let candidates;
|
|
25372
|
+
if (o.record) {
|
|
25373
|
+
if (!(0, import_node_fs31.existsSync)(o.record)) return fail(`bootstrap rollback: --record '${o.record}' not found`);
|
|
25374
|
+
let parsed;
|
|
25375
|
+
try {
|
|
25376
|
+
parsed = JSON.parse((0, import_node_fs31.readFileSync)(o.record, "utf8"));
|
|
25377
|
+
} catch (e) {
|
|
25378
|
+
return fail(`bootstrap rollback: --record '${o.record}' is not valid JSON: ${e.message}`);
|
|
25379
|
+
}
|
|
25380
|
+
const recs = Array.isArray(parsed?.records) ? parsed.records : Array.isArray(parsed) ? parsed : [];
|
|
25381
|
+
candidates = recs.map((r) => {
|
|
25382
|
+
try {
|
|
25383
|
+
return seedPrRecordFromPropagationRecord(r);
|
|
25384
|
+
} catch {
|
|
25385
|
+
return null;
|
|
25386
|
+
}
|
|
25387
|
+
}).filter((r) => r !== null);
|
|
25388
|
+
} else {
|
|
25389
|
+
candidates = [];
|
|
25390
|
+
try {
|
|
25391
|
+
const listed = await gh(["pr", "list", "--repo", repo, "--head", propagateBranch, "--base", baseBranch, "--state", "merged", "--json", "number,url,mergedAt,mergeCommit,files", "--limit", "20"]);
|
|
25392
|
+
const arr = JSON.parse(listed.stdout || "[]");
|
|
25393
|
+
for (const p of arr) {
|
|
25394
|
+
if (!p.mergeCommit?.oid) continue;
|
|
25395
|
+
candidates.push({
|
|
25396
|
+
repo,
|
|
25397
|
+
target: seed.target,
|
|
25398
|
+
number: p.number,
|
|
25399
|
+
url: p.url,
|
|
25400
|
+
mergeSha: p.mergeCommit.oid,
|
|
25401
|
+
mergedAt: p.mergedAt ?? "",
|
|
25402
|
+
files: (p.files ?? []).map((f) => f.path)
|
|
25403
|
+
});
|
|
25404
|
+
}
|
|
25405
|
+
} catch (e) {
|
|
25406
|
+
return fail(`bootstrap rollback: could not read ${repo}'s merged ${propagateBranch} PR history: ${e.message}`);
|
|
25407
|
+
}
|
|
25408
|
+
}
|
|
25409
|
+
const plan = planRollback(repo, seed.target, slug, candidates);
|
|
25410
|
+
if (!plan.resolution.found) {
|
|
25411
|
+
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25412
|
+
else console.log(renderRollbackReport(plan));
|
|
25413
|
+
return fail(`bootstrap rollback: ${plan.resolution.reason}`);
|
|
25414
|
+
}
|
|
25415
|
+
if (o.execute) {
|
|
25416
|
+
const record = plan.resolution.record;
|
|
25417
|
+
const parentResp = await gh(["api", `repos/${repo}/commits/${record.mergeSha}`, "--jq", ".parents[0].sha"]);
|
|
25418
|
+
const parentSha = parentResp.stdout.trim();
|
|
25419
|
+
if (!parentSha) return fail(`bootstrap rollback: could not resolve ${repo}@${record.mergeSha}'s parent commit \u2014 refusing to guess what to restore`);
|
|
25420
|
+
let preSeedContent = null;
|
|
25421
|
+
try {
|
|
25422
|
+
const resp = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${parentSha}`]);
|
|
25423
|
+
const parsed = JSON.parse(resp.stdout);
|
|
25424
|
+
preSeedContent = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
|
|
25425
|
+
} catch {
|
|
25426
|
+
preSeedContent = null;
|
|
25427
|
+
}
|
|
25428
|
+
if (preSeedContent == null) return fail(`bootstrap rollback: '${seed.target}' did not exist in ${repo} at ${parentSha} (the commit before the seed merge) \u2014 nothing to restore; refusing to guess`);
|
|
25429
|
+
const baseRef = await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
|
|
25430
|
+
const baseSha = baseRef.stdout.trim();
|
|
25431
|
+
let branchExists = true;
|
|
25432
|
+
try {
|
|
25433
|
+
await gh(["api", `repos/${repo}/git/ref/heads/${plan.branch}`]);
|
|
25434
|
+
} catch {
|
|
25435
|
+
branchExists = false;
|
|
25436
|
+
}
|
|
25437
|
+
if (!branchExists) await gh(["api", `repos/${repo}/git/refs`, "-f", `ref=refs/heads/${plan.branch}`, "-f", `sha=${baseSha}`]);
|
|
25438
|
+
let existingSha;
|
|
25439
|
+
try {
|
|
25440
|
+
const cur = await gh(["api", `repos/${repo}/contents/${enc(seed.target)}?ref=${plan.branch}`]);
|
|
25441
|
+
existingSha = JSON.parse(cur.stdout).sha;
|
|
25442
|
+
} catch {
|
|
25443
|
+
existingSha = void 0;
|
|
25444
|
+
}
|
|
25445
|
+
const tmp = (0, import_node_path29.join)((0, import_node_os11.tmpdir)(), `mmi-rollback-${process.pid}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}.json`);
|
|
25446
|
+
(0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, preSeedContent, plan.branch, existingSha)), "utf8");
|
|
25447
|
+
try {
|
|
25448
|
+
await gh(contentPutInputArgs(repo, seed.target, tmp));
|
|
25449
|
+
} finally {
|
|
25450
|
+
try {
|
|
25451
|
+
(0, import_node_fs31.unlinkSync)(tmp);
|
|
25452
|
+
} catch {
|
|
25453
|
+
}
|
|
25454
|
+
}
|
|
25455
|
+
const openPrs = await gh(["pr", "list", "--repo", repo, "--head", plan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
25456
|
+
const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
25457
|
+
let prUrl;
|
|
25458
|
+
if (prDecision.action === "reuse") {
|
|
25459
|
+
prUrl = prDecision.url;
|
|
25460
|
+
} else {
|
|
25461
|
+
const created = await ghCreate(["pr", "create", "--repo", repo, "--base", baseBranch, "--head", plan.branch, "--title", plan.title, "--body", plan.body]);
|
|
25462
|
+
prUrl = created.url;
|
|
25463
|
+
}
|
|
25464
|
+
plan.prUrl = prUrl;
|
|
25465
|
+
}
|
|
25466
|
+
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25467
|
+
else console.log(renderRollbackReport(plan));
|
|
25468
|
+
});
|
|
25068
25469
|
}
|
|
25069
25470
|
|
|
25070
25471
|
// src/stage-commands.ts
|
|
@@ -25605,6 +26006,7 @@ function registerBoardCommands(program3) {
|
|
|
25605
26006
|
force: o.force,
|
|
25606
26007
|
allowPartial: o.allowPartial
|
|
25607
26008
|
});
|
|
26009
|
+
invalidateStatuslineBoardCache();
|
|
25608
26010
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25609
26011
|
console.log(result.partial ? `Partially claimed ${result.item.ref}: ${result.warning}` : result.alreadyClaimed ? `Already claimed ${result.item.ref} - In Progress (no change)` : `Claimed ${result.item.ref} - In Progress`);
|
|
25610
26012
|
} catch (e) {
|
|
@@ -25621,6 +26023,7 @@ function registerBoardCommands(program3) {
|
|
|
25621
26023
|
force: o.force,
|
|
25622
26024
|
allowPartial: o.allowPartial
|
|
25623
26025
|
});
|
|
26026
|
+
if (bulk.results.some((r) => r.claimed)) invalidateStatuslineBoardCache();
|
|
25624
26027
|
if (o.json) {
|
|
25625
26028
|
console.log(JSON.stringify(bulk.results));
|
|
25626
26029
|
} else {
|
|
@@ -25665,6 +26068,7 @@ function registerBoardCommands(program3) {
|
|
|
25665
26068
|
repo: o.repo,
|
|
25666
26069
|
allowPartial: o.allowPartial
|
|
25667
26070
|
});
|
|
26071
|
+
if (bulk.results.some((r) => r.moved)) invalidateStatuslineBoardCache();
|
|
25668
26072
|
if (o.json) {
|
|
25669
26073
|
console.log(JSON.stringify(bulk.results));
|
|
25670
26074
|
} else {
|
|
@@ -25689,6 +26093,7 @@ function registerBoardCommands(program3) {
|
|
|
25689
26093
|
if (issueRefs.length === 1) {
|
|
25690
26094
|
try {
|
|
25691
26095
|
const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: canonicalStatus, repo: o.repo, allowPartial: o.allowPartial });
|
|
26096
|
+
invalidateStatuslineBoardCache();
|
|
25692
26097
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25693
26098
|
console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> ${result.status}`);
|
|
25694
26099
|
} catch (e) {
|
|
@@ -25740,6 +26145,7 @@ function registerBoardCommands(program3) {
|
|
|
25740
26145
|
force: o.force,
|
|
25741
26146
|
allowPartial: o.allowPartial
|
|
25742
26147
|
});
|
|
26148
|
+
invalidateStatuslineBoardCache();
|
|
25743
26149
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25744
26150
|
console.log(result.partial ? `Partially unclaimed ${result.item.ref}: ${result.warning}` : `Unclaimed ${result.item.ref} -> ${result.status}`);
|
|
25745
26151
|
} catch (e) {
|
|
@@ -25778,7 +26184,7 @@ var import_node_fs34 = require("node:fs");
|
|
|
25778
26184
|
var import_promises8 = require("node:fs/promises");
|
|
25779
26185
|
var import_node_path33 = require("node:path");
|
|
25780
26186
|
var import_node_os12 = require("node:os");
|
|
25781
|
-
var
|
|
26187
|
+
var import_node_child_process17 = require("node:child_process");
|
|
25782
26188
|
|
|
25783
26189
|
// src/board-advance.ts
|
|
25784
26190
|
function repoOf2(ref) {
|
|
@@ -26061,6 +26467,22 @@ function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
|
|
|
26061
26467
|
if (verdict.blocked) throw new Error(verdict.reason);
|
|
26062
26468
|
return housekeeping;
|
|
26063
26469
|
}
|
|
26470
|
+
async function bestEffortLeaseClose(wtPath, exec = (cmd, args) => execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })) {
|
|
26471
|
+
const step = "close jerv worktree lease";
|
|
26472
|
+
try {
|
|
26473
|
+
await exec("jerv-cli", ["lease", "close", "--ref", wtPath]);
|
|
26474
|
+
return { step, status: "done" };
|
|
26475
|
+
} catch (e) {
|
|
26476
|
+
const err = e;
|
|
26477
|
+
const detail = `${err.message}
|
|
26478
|
+
${err.stderr ?? ""}`;
|
|
26479
|
+
if (/ENOENT|not found|not recognized/i.test(detail)) {
|
|
26480
|
+
return { step, status: "skipped: jerv-cli not on PATH" };
|
|
26481
|
+
}
|
|
26482
|
+
const msg = (err.stderr?.trim() || err.message).split("\n")[0];
|
|
26483
|
+
return { step, status: `failed: ${msg}` };
|
|
26484
|
+
}
|
|
26485
|
+
}
|
|
26064
26486
|
async function applyGcPlan(plan, remote, opts = {}) {
|
|
26065
26487
|
const result = { removedBranches: [], removedRemoteBranches: [], removedTrackingRefs: [], removedWorktreeDirs: [], refused: [], failed: [], pruned: false };
|
|
26066
26488
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
@@ -26109,6 +26531,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
26109
26531
|
removeWorktreeDir: wtDeps.removeWorktreeDir,
|
|
26110
26532
|
removalContext: { primaryRoot: primaryRepoRoot, actor: gcActor, command: "worktree gc", force: opts.force }
|
|
26111
26533
|
});
|
|
26534
|
+
if (cleanup.worktree?.status === "removed") await bestEffortLeaseClose(cleanup.worktree.path);
|
|
26112
26535
|
return cleanup;
|
|
26113
26536
|
},
|
|
26114
26537
|
cleanupRemoteBranch: (branch, expectedHeadOid) => deleteReviewedRemoteBranch(remote, branch.branch, expectedHeadOid),
|
|
@@ -26155,6 +26578,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
26155
26578
|
owner,
|
|
26156
26579
|
reason: owner ? "branch merged/closed or directory dead; owner registration was stale" : "branch merged/closed or directory dead; no owner registered"
|
|
26157
26580
|
});
|
|
26581
|
+
await bestEffortLeaseClose(wt.path);
|
|
26158
26582
|
} catch (e) {
|
|
26159
26583
|
const error = e.message.split("\n")[0];
|
|
26160
26584
|
result.failed.push(`${wt.path}: ${error}`);
|
|
@@ -26327,7 +26751,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
26327
26751
|
}
|
|
26328
26752
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
26329
26753
|
function spawnDeferredGcSweep() {
|
|
26330
|
-
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn:
|
|
26754
|
+
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process17.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
26331
26755
|
}
|
|
26332
26756
|
async function createDeferredWorktreeStore() {
|
|
26333
26757
|
try {
|
|
@@ -26736,6 +27160,7 @@ var import_node_fs35 = require("node:fs");
|
|
|
26736
27160
|
var import_promises9 = require("node:fs/promises");
|
|
26737
27161
|
var import_node_path34 = require("node:path");
|
|
26738
27162
|
var GH_TIMEOUT_MS = 2e4;
|
|
27163
|
+
var STALE_PR_LOOKUP_LIMIT = 20;
|
|
26739
27164
|
var DEFAULT_BASE = "origin/development";
|
|
26740
27165
|
var DEFAULT_REMOTE = "origin";
|
|
26741
27166
|
var PROTECTED_BRANCHES2 = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
|
|
@@ -26895,13 +27320,16 @@ function scanOrphanDirs(worktreesRoot, worktreeGitRoot, deps = defaultOrphanDirS
|
|
|
26895
27320
|
}
|
|
26896
27321
|
return candidates;
|
|
26897
27322
|
}
|
|
26898
|
-
function formatStaleLeaks(leaks) {
|
|
26899
|
-
|
|
26900
|
-
const lines = [`worktree list --stale: ${leaks.length} leak(s)`];
|
|
27323
|
+
function formatStaleLeaks(leaks, prLookupFailures = []) {
|
|
27324
|
+
const lines = leaks.length ? [`worktree list --stale: ${leaks.length} leak(s)`] : ["worktree list --stale: no leaks found"];
|
|
26901
27325
|
for (const leak of leaks) {
|
|
26902
27326
|
lines.push(` [${leak.kind}] ${leak.ref} \u2014 ${leak.detail}`);
|
|
26903
27327
|
lines.push(` fix: ${leak.remediation}`);
|
|
26904
27328
|
}
|
|
27329
|
+
if (prLookupFailures.length) {
|
|
27330
|
+
lines.push(` INCOMPLETE: merge state could not be read for ${prLookupFailures.length} branch(es) \u2014 they are NOT covered above`);
|
|
27331
|
+
for (const f of prLookupFailures) lines.push(` ${f.branch}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
|
|
27332
|
+
}
|
|
26905
27333
|
return lines.join("\n");
|
|
26906
27334
|
}
|
|
26907
27335
|
async function repoRootOf() {
|
|
@@ -27172,6 +27600,7 @@ function registerWorktreeCommands(program3) {
|
|
|
27172
27600
|
}
|
|
27173
27601
|
}
|
|
27174
27602
|
report.push(await bestEffortGit(["worktree", "prune"], primaryCheckout, "prune worktree metadata"));
|
|
27603
|
+
report.push(await bestEffortLeaseClose(wtPath));
|
|
27175
27604
|
const result = {
|
|
27176
27605
|
dryRun: false,
|
|
27177
27606
|
...plan,
|
|
@@ -27202,8 +27631,9 @@ function registerWorktreeCommands(program3) {
|
|
|
27202
27631
|
const ctx = await gatherWorktreeContext();
|
|
27203
27632
|
if (o.stale) {
|
|
27204
27633
|
const leaks = classifyStaleLeaks(ctx);
|
|
27205
|
-
|
|
27206
|
-
return console.log(
|
|
27634
|
+
const failures = ctx.prLookupFailures ?? [];
|
|
27635
|
+
if (o.json) return console.log(JSON.stringify({ stale: leaks, count: leaks.length, prLookupFailures: failures, complete: failures.length === 0 }, null, 2));
|
|
27636
|
+
return console.log(formatStaleLeaks(leaks, failures));
|
|
27207
27637
|
}
|
|
27208
27638
|
if (o.json) return console.log(JSON.stringify({ worktrees: ctx.worktrees }, null, 2));
|
|
27209
27639
|
if (!ctx.worktrees.length) return console.log("worktree list: no worktrees");
|
|
@@ -27227,22 +27657,18 @@ async function gatherWorktreeContext() {
|
|
|
27227
27657
|
const branchOut = (await execFileP2("git", ["branch", "--format=%(refname:short)"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
|
|
27228
27658
|
const localBranches = branchOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
27229
27659
|
const currentBranch2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || void 0;
|
|
27660
|
+
const { prs, failures: prLookupFailures } = await resolveBranchPrs(localBranches.filter((b) => !PROTECTED_BRANCHES2.has(b)), STALE_PR_LOOKUP_LIMIT);
|
|
27230
27661
|
const openPrBranches = /* @__PURE__ */ new Set();
|
|
27231
27662
|
const closedBranches = /* @__PURE__ */ new Set();
|
|
27232
|
-
|
|
27233
|
-
|
|
27234
|
-
const
|
|
27235
|
-
|
|
27236
|
-
|
|
27237
|
-
|
|
27238
|
-
|
|
27239
|
-
|
|
27240
|
-
|
|
27241
|
-
for (const [br, states] of byBranch) {
|
|
27242
|
-
if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
|
|
27243
|
-
else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
|
|
27244
|
-
}
|
|
27245
|
-
} catch {
|
|
27663
|
+
const byBranch = /* @__PURE__ */ new Map();
|
|
27664
|
+
for (const pr2 of prs) {
|
|
27665
|
+
const arr = byBranch.get(pr2.headRefName) ?? [];
|
|
27666
|
+
arr.push(pr2.state);
|
|
27667
|
+
byBranch.set(pr2.headRefName, arr);
|
|
27668
|
+
}
|
|
27669
|
+
for (const [br, states] of byBranch) {
|
|
27670
|
+
if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
|
|
27671
|
+
else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
|
|
27246
27672
|
}
|
|
27247
27673
|
const stages = [];
|
|
27248
27674
|
for (const wt of worktrees) {
|
|
@@ -27259,7 +27685,7 @@ async function gatherWorktreeContext() {
|
|
|
27259
27685
|
listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
|
|
27260
27686
|
});
|
|
27261
27687
|
}
|
|
27262
|
-
return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
|
|
27688
|
+
return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs, prLookupFailures };
|
|
27263
27689
|
}
|
|
27264
27690
|
async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
|
|
27265
27691
|
try {
|
|
@@ -27280,7 +27706,7 @@ ${err.stderr ?? ""}`;
|
|
|
27280
27706
|
|
|
27281
27707
|
// src/issue-commands.ts
|
|
27282
27708
|
var import_node_fs36 = require("node:fs");
|
|
27283
|
-
var
|
|
27709
|
+
var import_node_crypto7 = require("node:crypto");
|
|
27284
27710
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
27285
27711
|
var ReparentConflictError = class extends Error {
|
|
27286
27712
|
constructor(message, payload) {
|
|
@@ -27356,6 +27782,44 @@ async function editIssue(client, options, deps = {}) {
|
|
|
27356
27782
|
...parentResult ? { parent: parentResult } : {}
|
|
27357
27783
|
};
|
|
27358
27784
|
}
|
|
27785
|
+
var EVIDENCE_COMMENT_URL_RE = /^https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)#issuecomment-(\d+)$/;
|
|
27786
|
+
async function verifyCloseEvidence(client, evidence, repo, issueNumber, duplicateOf) {
|
|
27787
|
+
const match = evidence.trim().match(EVIDENCE_COMMENT_URL_RE);
|
|
27788
|
+
if (!match) {
|
|
27789
|
+
throw new Error(
|
|
27790
|
+
`--evidence must be a canonical issue-comment URL (https://github.com/<owner>/<repo>/issues/<n>#issuecomment-<id>), got: ${evidence}`
|
|
27791
|
+
);
|
|
27792
|
+
}
|
|
27793
|
+
const [, evidenceRepo, evidenceIssue, commentId] = match;
|
|
27794
|
+
if (evidenceRepo.toLowerCase() !== repo.toLowerCase() || Number(evidenceIssue) !== issueNumber) {
|
|
27795
|
+
throw new Error(
|
|
27796
|
+
`--evidence comment belongs to ${evidenceRepo}#${evidenceIssue}, not the issue being closed (${repo}#${issueNumber})`
|
|
27797
|
+
);
|
|
27798
|
+
}
|
|
27799
|
+
let comment;
|
|
27800
|
+
try {
|
|
27801
|
+
comment = await client.rest(
|
|
27802
|
+
"GET",
|
|
27803
|
+
`repos/${repo}/issues/comments/${commentId}`
|
|
27804
|
+
);
|
|
27805
|
+
} catch (e) {
|
|
27806
|
+
if (e instanceof GitHubApiError && e.status === 404) {
|
|
27807
|
+
throw new Error(`--evidence comment ${commentId} does not exist on ${repo} \u2014 nothing to anchor the close to`);
|
|
27808
|
+
}
|
|
27809
|
+
throw e;
|
|
27810
|
+
}
|
|
27811
|
+
const expectedAnchor = `/issues/${issueNumber}#issuecomment-${commentId}`;
|
|
27812
|
+
if (!comment.html_url || !comment.html_url.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
|
|
27813
|
+
throw new Error(
|
|
27814
|
+
`--evidence comment ${commentId} is not a comment on ${repo}#${issueNumber} (it lives at ${comment.html_url ?? "unknown"})`
|
|
27815
|
+
);
|
|
27816
|
+
}
|
|
27817
|
+
if (duplicateOf !== void 0 && !new RegExp(`#${duplicateOf}\\b`).test(comment.body ?? "")) {
|
|
27818
|
+
throw new Error(
|
|
27819
|
+
`--reason duplicate-of ${duplicateOf}: the --evidence comment must cite #${duplicateOf} so the duplicate has an auditable destination`
|
|
27820
|
+
);
|
|
27821
|
+
}
|
|
27822
|
+
}
|
|
27359
27823
|
async function closeIssue(client, options, deps = {}) {
|
|
27360
27824
|
const parsed = parseIssueRef(options.ref);
|
|
27361
27825
|
const repo = parsed.repo ?? options.defaultRepo;
|
|
@@ -27363,6 +27827,9 @@ async function closeIssue(client, options, deps = {}) {
|
|
|
27363
27827
|
const url = `https://github.com/${repo}/issues/${parsed.number}`;
|
|
27364
27828
|
const reason = options.reason ?? "completed";
|
|
27365
27829
|
const stateReason = reason === "duplicate-of" ? "not_planned" : reason === "not-planned" ? "not_planned" : "completed";
|
|
27830
|
+
if (options.evidence !== void 0) {
|
|
27831
|
+
await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
|
|
27832
|
+
}
|
|
27366
27833
|
await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
|
|
27367
27834
|
body: { state: "closed", state_reason: stateReason }
|
|
27368
27835
|
});
|
|
@@ -27530,7 +27997,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
27530
27997
|
const identity = `${spec.type}
|
|
27531
27998
|
${spec.title.trim()}
|
|
27532
27999
|
${spec.body ?? ""}`;
|
|
27533
|
-
const hash = (0,
|
|
28000
|
+
const hash = (0, import_node_crypto7.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
27534
28001
|
return `${batchKey}:${hash}`;
|
|
27535
28002
|
}
|
|
27536
28003
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -27749,7 +28216,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
27749
28216
|
}
|
|
27750
28217
|
});
|
|
27751
28218
|
mutating(
|
|
27752
|
-
issue2.command("close <ref>").description("close an issue and move its board item to Done (--reason completed|not-planned|duplicate-of --duplicate-of <n>)").option("--reason <reason>", "completed | not-planned | duplicate-of (defaults to completed)").option("--duplicate-of <number>", "the issue number this duplicates (use with --reason duplicate-of)").option("--comment <text>", "a closing comment to post").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
|
|
28219
|
+
issue2.command("close <ref>").description("close an issue and move its board item to Done (--reason completed|not-planned|duplicate-of --duplicate-of <n>)").option("--reason <reason>", "completed | not-planned | duplicate-of (defaults to completed)").option("--duplicate-of <number>", "the issue number this duplicates (use with --reason duplicate-of)").option("--comment <text>", "a closing comment to post").option("--evidence <comment-url>", "URL of an existing comment on this issue carrying the close evidence \u2014 verified before closing (JPT#4668; the sanctioned agent-shell close shape)").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)"),
|
|
27753
28220
|
(opts, args) => {
|
|
27754
28221
|
let reason;
|
|
27755
28222
|
try {
|
|
@@ -27778,7 +28245,8 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
27778
28245
|
defaultRepo,
|
|
27779
28246
|
reason: parsed.reason,
|
|
27780
28247
|
duplicateOf: o.duplicateOf ? Number(o.duplicateOf) : void 0,
|
|
27781
|
-
comment: o.comment
|
|
28248
|
+
comment: o.comment,
|
|
28249
|
+
evidence: o.evidence
|
|
27782
28250
|
});
|
|
27783
28251
|
console.log(JSON.stringify(result));
|
|
27784
28252
|
} catch (e) {
|
|
@@ -27909,6 +28377,7 @@ ${lines}`, {
|
|
|
27909
28377
|
surface: batchSurface,
|
|
27910
28378
|
noSurface: batchNoSurface
|
|
27911
28379
|
}, { attach: batchAttach });
|
|
28380
|
+
if (result.created.some((row) => !row.idempotent)) invalidateStatuslineBoardCache();
|
|
27912
28381
|
console.log(JSON.stringify(result));
|
|
27913
28382
|
if (result.failures.length) process.exitCode = 1;
|
|
27914
28383
|
} catch (e) {
|
|
@@ -31286,9 +31755,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
|
|
|
31286
31755
|
var import_node_fs39 = require("node:fs");
|
|
31287
31756
|
var import_node_os14 = require("node:os");
|
|
31288
31757
|
var import_node_path37 = require("node:path");
|
|
31289
|
-
var
|
|
31758
|
+
var import_node_child_process18 = require("node:child_process");
|
|
31290
31759
|
var import_node_util8 = require("node:util");
|
|
31291
|
-
var execFileP6 = (0, import_node_util8.promisify)(
|
|
31760
|
+
var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process18.execFile);
|
|
31292
31761
|
var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
31293
31762
|
function installedClaudePluginVersion() {
|
|
31294
31763
|
try {
|
|
@@ -31334,12 +31803,12 @@ function installedSurfacePluginVersion(surface) {
|
|
|
31334
31803
|
if (token === "claude") return installedClaudePluginVersion();
|
|
31335
31804
|
if (token !== "codex") return void 0;
|
|
31336
31805
|
try {
|
|
31337
|
-
const raw = process.platform === "win32" ? (0,
|
|
31806
|
+
const raw = process.platform === "win32" ? (0, import_node_child_process18.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
|
|
31338
31807
|
encoding: "utf8",
|
|
31339
31808
|
stdio: ["ignore", "pipe", "ignore"],
|
|
31340
31809
|
timeout: 15e3,
|
|
31341
31810
|
windowsHide: true
|
|
31342
|
-
}) : (0,
|
|
31811
|
+
}) : (0, import_node_child_process18.execFileSync)("codex", ["plugin", "list", "--json"], {
|
|
31343
31812
|
encoding: "utf8",
|
|
31344
31813
|
stdio: ["ignore", "pipe", "ignore"],
|
|
31345
31814
|
timeout: 15e3,
|
|
@@ -31357,7 +31826,7 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
|
|
|
31357
31826
|
}
|
|
31358
31827
|
function worktreeRootSync() {
|
|
31359
31828
|
try {
|
|
31360
|
-
const out = (0,
|
|
31829
|
+
const out = (0, import_node_child_process18.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
31361
31830
|
let root = out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
31362
31831
|
if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
|
|
31363
31832
|
return root || null;
|
|
@@ -31411,23 +31880,6 @@ function hasRepoLocalWorktrees() {
|
|
|
31411
31880
|
|
|
31412
31881
|
// src/index.ts
|
|
31413
31882
|
var execFileGitRun = async (file, args) => (await execFileP2(file, args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
31414
|
-
async function currentRepoFullName() {
|
|
31415
|
-
const remote = (await gitOut(["remote", "get-url", "origin"])).replace(/\.git$/, "");
|
|
31416
|
-
const parts = remote.split(/[:/]/).filter(Boolean);
|
|
31417
|
-
if (parts.length >= 2) return `${parts[parts.length - 2]}/${parts[parts.length - 1]}`;
|
|
31418
|
-
return `mutmutco/${await repoSlug()}`;
|
|
31419
|
-
}
|
|
31420
|
-
async function readDocsAuditFetch(repo) {
|
|
31421
|
-
const list = await fetchDocsAuditList(registryClientDeps(await loadConfig()));
|
|
31422
|
-
if ("notArmed" in list) return { notArmed: true };
|
|
31423
|
-
if (!list.ok) return { ok: false, error: list.error };
|
|
31424
|
-
const wanted = repo.toLowerCase();
|
|
31425
|
-
const row = list.rows.find((r) => String(r.repo ?? "").toLowerCase() === wanted);
|
|
31426
|
-
return {
|
|
31427
|
-
ok: true,
|
|
31428
|
-
verdict: row ? { repo: row.repo, date: row.date, shaRange: row.shaRange, outcome: row.outcome, checkerVendor: row.checkerVendor } : null
|
|
31429
|
-
};
|
|
31430
|
-
}
|
|
31431
31883
|
async function githubRepoReachProbe() {
|
|
31432
31884
|
const remote = await execFileP2("git", ["remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS }).then((r) => r.stdout).catch(() => "");
|
|
31433
31885
|
const repo = parseOriginRepo(remote);
|
|
@@ -31694,6 +32146,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
31694
32146
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
31695
32147
|
removalContext
|
|
31696
32148
|
);
|
|
32149
|
+
for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
|
|
31697
32150
|
return {
|
|
31698
32151
|
removed: result.removed,
|
|
31699
32152
|
stillQueued: result.stillDeferred.length,
|
|
@@ -32148,6 +32601,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
32148
32601
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
32149
32602
|
{ removalContext }
|
|
32150
32603
|
);
|
|
32604
|
+
for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
|
|
32151
32605
|
if (o.json) return console.log(JSON.stringify(result));
|
|
32152
32606
|
if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
|
|
32153
32607
|
if (result.removed.length) console.log(`worktree gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
|
|
@@ -32163,7 +32617,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
32163
32617
|
process.exit(process.exitCode ?? 0);
|
|
32164
32618
|
});
|
|
32165
32619
|
});
|
|
32166
|
-
gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to
|
|
32620
|
+
gcCmd.option("--dry-run", "show what would be deleted (default)").option("--apply", "delete only the listed clean merged/closed PR local+remote branches, linked worktrees, and stale tracking refs").option("--json", "machine-readable output").option("--scratch", "prune safe local scratch (#1864) instead of git branches/refs; local plans are surfaced advisory-only, never auto-pruned").option("--remote <name>", "remote name", "origin").option("--limit <n>", "PRs to read PER BRANCH ? an effort bound, not a correctness one: merge state is resolved per branch, so no branch is ever skipped for being old (#4227)", "200").option("--force", "remove worktrees even when the ownership registry shows another session created or worked in them recently (#3580)").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) ? descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
|
|
32167
32621
|
if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
|
|
32168
32622
|
if (o.scratch) {
|
|
32169
32623
|
try {
|
|
@@ -32195,13 +32649,14 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
32195
32649
|
if (o.apply) {
|
|
32196
32650
|
const deferredStore = await createDeferredWorktreeStore();
|
|
32197
32651
|
const removalContext = await currentWorktreeRemovalContext("worktree gc", o.force);
|
|
32198
|
-
await sweepDeferredWorktrees(
|
|
32652
|
+
const sweepResult = await sweepDeferredWorktrees(
|
|
32199
32653
|
deferredStore,
|
|
32200
32654
|
// #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker ? keep a
|
|
32201
32655
|
// git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
|
|
32202
32656
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
32203
32657
|
removalContext
|
|
32204
32658
|
).catch(() => void 0);
|
|
32659
|
+
for (const removedPath of sweepResult?.removed ?? []) await bestEffortLeaseClose(removedPath);
|
|
32205
32660
|
applyResult = await applyGcPlan(plan, o.remote, { root, force: o.force });
|
|
32206
32661
|
}
|
|
32207
32662
|
if (o.json) {
|
|
@@ -32222,11 +32677,11 @@ var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
|
|
|
32222
32677
|
function runWorktreeInstall(command, cwd, quiet, opts) {
|
|
32223
32678
|
const stdio = quiet ? "ignore" : "inherit";
|
|
32224
32679
|
return new Promise((resolve5, reject) => {
|
|
32225
|
-
const child2 = opts?.shell ? (0,
|
|
32680
|
+
const child2 = opts?.shell ? (0, import_node_child_process19.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
|
|
32226
32681
|
const [bin, ...args] = command.split(" ");
|
|
32227
32682
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
32228
32683
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
32229
|
-
return (0,
|
|
32684
|
+
return (0, import_node_child_process19.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
|
|
32230
32685
|
})();
|
|
32231
32686
|
const timer = setTimeout(() => {
|
|
32232
32687
|
try {
|
|
@@ -32437,6 +32892,34 @@ withExamples(mutating(
|
|
|
32437
32892
|
const owner = { path: wtPath, branch, createdAt, lastSeenAt: createdAt, actor: createActor };
|
|
32438
32893
|
recordWorktreeOwner(repoRoot2, owner);
|
|
32439
32894
|
appendWorktreeEvent(repoRoot2, { action: "created", command: "worktree create", target: wtPath, branch, actor: owner.actor });
|
|
32895
|
+
let lease;
|
|
32896
|
+
try {
|
|
32897
|
+
await execFileP2("jerv-cli", [
|
|
32898
|
+
"lease",
|
|
32899
|
+
"open",
|
|
32900
|
+
"--kind",
|
|
32901
|
+
"worktree",
|
|
32902
|
+
"--ref",
|
|
32903
|
+
wtPath,
|
|
32904
|
+
...selector ? ["--repo", selector.repo] : [],
|
|
32905
|
+
// 72h, not the 24h default: a feature worktree routinely sits untouched over a weekend, and
|
|
32906
|
+
// an expired lease is what makes a CLEAN, unheld tree reapable. Erring long costs disk;
|
|
32907
|
+
// erring short reaps a tree someone is still using between sessions.
|
|
32908
|
+
"--ttl",
|
|
32909
|
+
"72",
|
|
32910
|
+
"--note",
|
|
32911
|
+
`${branch} via mmi-cli worktree create`
|
|
32912
|
+
], { timeout: GIT_TIMEOUT_MS });
|
|
32913
|
+
lease = { ok: true };
|
|
32914
|
+
} catch (e) {
|
|
32915
|
+
const detail = (e.stderr?.trim() || e.message.trim()).split("\n")[0];
|
|
32916
|
+
lease = { ok: false, error: detail };
|
|
32917
|
+
if (!o.json) {
|
|
32918
|
+
console.error(
|
|
32919
|
+
` worktree created, but no jerv lease was opened for it: ${detail} \u2014 nothing on the lease plane can expire this tree, so it will need manual cleanup (MMI-Hub#4231).`
|
|
32920
|
+
);
|
|
32921
|
+
}
|
|
32922
|
+
}
|
|
32440
32923
|
if (o.json) {
|
|
32441
32924
|
return console.log(JSON.stringify({
|
|
32442
32925
|
branch,
|
|
@@ -32444,6 +32927,7 @@ withExamples(mutating(
|
|
|
32444
32927
|
base,
|
|
32445
32928
|
resumed,
|
|
32446
32929
|
...report,
|
|
32930
|
+
lease,
|
|
32447
32931
|
...issueForm && selector ? { issue: `${selector.repo}#${selector.number}` } : {},
|
|
32448
32932
|
...issueForm && o.claim ? { claim: claimError ? { ok: false, error: claimError } : claim } : {}
|
|
32449
32933
|
}, null, 2));
|
|
@@ -32555,7 +33039,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
32555
33039
|
try {
|
|
32556
33040
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
|
|
32557
33041
|
if (o.repo) args.push("--repo", o.repo);
|
|
32558
|
-
spawnDetachedSelf(args, { spawn:
|
|
33042
|
+
spawnDetachedSelf(args, { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
|
|
32559
33043
|
} catch {
|
|
32560
33044
|
}
|
|
32561
33045
|
}
|
|
@@ -32883,48 +33367,6 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
|
|
|
32883
33367
|
await failGraceful(e.message);
|
|
32884
33368
|
}
|
|
32885
33369
|
});
|
|
32886
|
-
var docsAudit = program2.command("docs-audit").description("the docs janitor verdict ledger ? record a dated run verdict, or read the dead-man status back");
|
|
32887
|
-
docsAudit.command("record").description("write a dated janitor verdict for one repo to the registry ledger (master-only server-side)").option("--repo <owner/name>", "the repo the verdict is for (default: the current repo)").option("--date <YYYY-MM-DD>", "the ISO day the run examined (default: today)").requiredOption("--sha-range <a..b>", "the git range the janitor read (e.g. <lastVerdictSha>..HEAD)").addOption(new Option("--outcome <kind>", "clean | refreshed | failed").makeOptionMandatory().choices(["clean", "refreshed", "failed"])).option("--count <n>", "docs refreshed (required when --outcome refreshed)").option("--reason <text>", "why the run failed (required when --outcome failed)").requiredOption("--checker-vendor <vendor>", "which vendor's model actually ran the check").action(async (o) => {
|
|
32888
|
-
try {
|
|
32889
|
-
const repo = o.repo ?? await currentRepoFullName();
|
|
32890
|
-
const date = o.date ?? (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
32891
|
-
let outcome;
|
|
32892
|
-
if (o.outcome === "clean") outcome = { kind: "clean" };
|
|
32893
|
-
else if (o.outcome === "refreshed") outcome = { kind: "refreshed", count: Number(o.count) };
|
|
32894
|
-
else if (o.outcome === "failed") outcome = { kind: "failed", reason: o.reason ?? "" };
|
|
32895
|
-
else return failGraceful(`docs audit record: --outcome must be clean|refreshed|failed, got "${o.outcome}"`);
|
|
32896
|
-
const verdict = docsAuditRecord({ repo, date, shaRange: o.shaRange, outcome, checkerVendor: o.checkerVendor });
|
|
32897
|
-
await reportWrite("docs-audit record", await recordDocsAudit(verdict, registryClientDeps(await loadConfig())));
|
|
32898
|
-
} catch (e) {
|
|
32899
|
-
await failGraceful(e.message);
|
|
32900
|
-
}
|
|
32901
|
-
});
|
|
32902
|
-
docsAudit.command("status").description("read the janitor dead-man verdict back for one repo ? missing/stale/failed is RED; a not-yet-armed registry route is an informational exit 0").option("--repo <owner/name>", "the repo to check (default: the current repo)").option("--json", "machine-readable output ? the discrete state rather than the sentence").action(async (o) => {
|
|
32903
|
-
try {
|
|
32904
|
-
const repo = o.repo ?? await currentRepoFullName();
|
|
32905
|
-
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
32906
|
-
const fetched = await readDocsAuditFetch(repo);
|
|
32907
|
-
const result = docsAuditStatus(fetched, { repo, today });
|
|
32908
|
-
if (o.json) {
|
|
32909
|
-
const verdict = "ok" in fetched && fetched.ok ? fetched.verdict : null;
|
|
32910
|
-
console.log(JSON.stringify({
|
|
32911
|
-
repo,
|
|
32912
|
-
armed: !("notArmed" in fetched),
|
|
32913
|
-
ok: result.ok,
|
|
32914
|
-
state: result.state,
|
|
32915
|
-
date: verdict?.date ?? null,
|
|
32916
|
-
outcome: verdict?.outcome ?? null,
|
|
32917
|
-
checkerVendor: verdict?.checkerVendor ?? null,
|
|
32918
|
-
line: result.line
|
|
32919
|
-
}, null, 2));
|
|
32920
|
-
} else {
|
|
32921
|
-
console.log(result.line);
|
|
32922
|
-
}
|
|
32923
|
-
if (!result.ok) process.exitCode = 1;
|
|
32924
|
-
} catch (e) {
|
|
32925
|
-
await failGraceful(e.message);
|
|
32926
|
-
}
|
|
32927
|
-
});
|
|
32928
33370
|
async function reportWrite(label, res) {
|
|
32929
33371
|
if (res.ok) {
|
|
32930
33372
|
console.log(JSON.stringify(res.body));
|
|
@@ -33584,6 +34026,7 @@ withExamples(mutating(
|
|
|
33584
34026
|
}
|
|
33585
34027
|
}
|
|
33586
34028
|
if (o.related !== false) scheduleRelatedDiscovery({ repo: o.repo, number: created.number, title, body });
|
|
34029
|
+
invalidateStatuslineBoardCache();
|
|
33587
34030
|
console.log(JSON.stringify({
|
|
33588
34031
|
...created,
|
|
33589
34032
|
label: issueType,
|
|
@@ -33884,6 +34327,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
33884
34327
|
return fail(`pr create: ${docsCheck.detail} ? ${docsCheck.fix}`);
|
|
33885
34328
|
}
|
|
33886
34329
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
34330
|
+
invalidateStatuslineBoardCache();
|
|
33887
34331
|
console.log(JSON.stringify(created));
|
|
33888
34332
|
}), [
|
|
33889
34333
|
'mmi-cli pr create --title "Add the schema" --body "Closes #2680"',
|
|
@@ -34283,6 +34727,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
34283
34727
|
preserveWorktree: o.preserveWorktree,
|
|
34284
34728
|
removalContext
|
|
34285
34729
|
});
|
|
34730
|
+
if (localCleanup.worktree?.status === "removed") await bestEffortLeaseClose(localCleanup.worktree.path);
|
|
34286
34731
|
} catch (e) {
|
|
34287
34732
|
localCleanup = {
|
|
34288
34733
|
branch: headRef,
|
|
@@ -34295,6 +34740,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
34295
34740
|
};
|
|
34296
34741
|
}
|
|
34297
34742
|
const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
|
|
34743
|
+
invalidateStatuslineBoardCache();
|
|
34298
34744
|
console.log(JSON.stringify({
|
|
34299
34745
|
...buildPrMergeResultPayload({
|
|
34300
34746
|
number,
|
|
@@ -34941,7 +35387,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
34941
35387
|
for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
|
|
34942
35388
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
34943
35389
|
if (worktreeBanner) {
|
|
34944
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
35390
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
34945
35391
|
bannerIo.log(worktreeBanner);
|
|
34946
35392
|
}
|
|
34947
35393
|
if (isLinkedWorktree(process.cwd())) {
|