@mutmutco/cli 3.98.0 → 3.99.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 +522 -271
- 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");
|
|
@@ -22503,7 +22405,7 @@ ${SSH_RECIPE_AGENT_NOTE}`);
|
|
|
22503
22405
|
|
|
22504
22406
|
// src/schedules-commands.ts
|
|
22505
22407
|
var import_promises4 = require("node:fs/promises");
|
|
22506
|
-
var
|
|
22408
|
+
var import_node_child_process16 = require("node:child_process");
|
|
22507
22409
|
var import_node_util7 = require("node:util");
|
|
22508
22410
|
|
|
22509
22411
|
// src/schedules.ts
|
|
@@ -22814,7 +22716,7 @@ function cadenceStale(registryCadence, liveCadence) {
|
|
|
22814
22716
|
if (!liveCrons.length) return false;
|
|
22815
22717
|
return !liveCrons.every((cron) => registered.has(cron));
|
|
22816
22718
|
}
|
|
22817
|
-
function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22719
|
+
function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflowNames = /* @__PURE__ */ new Set(), disabledWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22818
22720
|
const registryGithub = registry2.filter((r) => r.executor === "github-actions");
|
|
22819
22721
|
const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
|
|
22820
22722
|
const registryById = new Map(registryGithub.map((r) => [r.id, r]));
|
|
@@ -22845,6 +22747,18 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22845
22747
|
continue;
|
|
22846
22748
|
}
|
|
22847
22749
|
if (activeWorkflowNames.has(r.id)) continue;
|
|
22750
|
+
if (disabledWorkflowNames.has(r.id)) {
|
|
22751
|
+
if (readRepos.has(r.repo)) {
|
|
22752
|
+
drifts.push({
|
|
22753
|
+
class: "registered-but-disabled",
|
|
22754
|
+
name: r.id,
|
|
22755
|
+
executor: r.executor || "github-actions",
|
|
22756
|
+
detail: "workflow file present but DISABLED in GitHub Actions \u2014 the dispatcher cannot run it",
|
|
22757
|
+
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`
|
|
22758
|
+
});
|
|
22759
|
+
}
|
|
22760
|
+
continue;
|
|
22761
|
+
}
|
|
22848
22762
|
if (readRepos.has(r.repo)) {
|
|
22849
22763
|
drifts.push({
|
|
22850
22764
|
class: "registered-but-dead",
|
|
@@ -22920,7 +22834,7 @@ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
|
|
|
22920
22834
|
}
|
|
22921
22835
|
return drifts.sort((a, b) => a.name.localeCompare(b.name));
|
|
22922
22836
|
}
|
|
22923
|
-
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}) {
|
|
22837
|
+
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}, disabledWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22924
22838
|
if (registry2 === null) {
|
|
22925
22839
|
return {
|
|
22926
22840
|
reconciliation: [],
|
|
@@ -22930,7 +22844,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
|
|
|
22930
22844
|
}
|
|
22931
22845
|
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
22932
22846
|
const drifts = [
|
|
22933
|
-
...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames),
|
|
22847
|
+
...reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames),
|
|
22934
22848
|
...registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
|
|
22935
22849
|
schedulerRead: Boolean(harbour.schedulerRead),
|
|
22936
22850
|
now: harbour.now
|
|
@@ -22940,7 +22854,7 @@ function assembleReconciliation(githubEntries2, readRepos, registry2, activeWork
|
|
|
22940
22854
|
}
|
|
22941
22855
|
|
|
22942
22856
|
// src/schedules-commands.ts
|
|
22943
|
-
var execFileP5 = (0, import_node_util7.promisify)(
|
|
22857
|
+
var execFileP5 = (0, import_node_util7.promisify)(import_node_child_process16.execFile);
|
|
22944
22858
|
var AWS_REGION = "eu-central-1";
|
|
22945
22859
|
var AWS_TIMEOUT_MS = 3e4;
|
|
22946
22860
|
var AWS_RETRY_DELAY_MS = 1500;
|
|
@@ -22964,9 +22878,15 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
22964
22878
|
const entries = [];
|
|
22965
22879
|
const failures = [];
|
|
22966
22880
|
const workflows = [];
|
|
22881
|
+
const disabled = [];
|
|
22967
22882
|
for (const wf of workflowsList) {
|
|
22968
|
-
if (
|
|
22883
|
+
if (typeof wf?.path !== "string" || !wf.path) continue;
|
|
22969
22884
|
if (!wf.path.startsWith(".github/workflows/")) continue;
|
|
22885
|
+
if (wf.state !== "active") {
|
|
22886
|
+
const basename5 = wf.path.split("/").pop() ?? wf.path;
|
|
22887
|
+
disabled.push(`${repo}/${basename5.replace(/\.ya?ml$/, "")}`);
|
|
22888
|
+
continue;
|
|
22889
|
+
}
|
|
22970
22890
|
try {
|
|
22971
22891
|
const contents = await client.rest(
|
|
22972
22892
|
"GET",
|
|
@@ -22985,7 +22905,7 @@ async function repoWorkflowEntries(client, repo) {
|
|
|
22985
22905
|
else throw e;
|
|
22986
22906
|
}
|
|
22987
22907
|
}
|
|
22988
|
-
return { entries, failures, workflows };
|
|
22908
|
+
return { entries, failures, workflows, disabled };
|
|
22989
22909
|
}
|
|
22990
22910
|
async function githubEntries(client) {
|
|
22991
22911
|
const entries = [];
|
|
@@ -22993,12 +22913,13 @@ async function githubEntries(client) {
|
|
|
22993
22913
|
const drift = [];
|
|
22994
22914
|
const readRepos = [];
|
|
22995
22915
|
const workflows = [];
|
|
22916
|
+
const disabledWorkflowNames = [];
|
|
22996
22917
|
let repos;
|
|
22997
22918
|
try {
|
|
22998
22919
|
const listing = await client.restPaginate(`/orgs/${ORG}/repos?per_page=100`);
|
|
22999
22920
|
repos = listing.filter((r) => typeof r?.name === "string" && r.archived !== true).map((r) => r.name).sort();
|
|
23000
22921
|
} catch (e) {
|
|
23001
|
-
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [] };
|
|
22922
|
+
return { entries, incomplete: [`github: could not list ${ORG} repos \u2014 ${e.message}`], drift, reconciliation: [], readRepos, workflowNames: [], disabledWorkflowNames: [] };
|
|
23002
22923
|
}
|
|
23003
22924
|
const results = await Promise.all(
|
|
23004
22925
|
repos.map(async (repo) => {
|
|
@@ -23015,6 +22936,7 @@ async function githubEntries(client) {
|
|
|
23015
22936
|
readRepos.push(r.repo);
|
|
23016
22937
|
entries.push(...r.entries);
|
|
23017
22938
|
workflows.push(...r.workflows);
|
|
22939
|
+
disabledWorkflowNames.push(...r.disabled);
|
|
23018
22940
|
if (r.failures.length) {
|
|
23019
22941
|
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
22942
|
}
|
|
@@ -23022,7 +22944,7 @@ async function githubEntries(client) {
|
|
|
23022
22944
|
}
|
|
23023
22945
|
const reconciliation = [...strayCronDrifts(workflows), ...unlauncheredLlmDrifts(entries)];
|
|
23024
22946
|
drift.push(...reconciliation.map(renderDrift));
|
|
23025
|
-
return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name) };
|
|
22947
|
+
return { entries, incomplete, drift, reconciliation, readRepos, workflowNames: workflows.map((w) => w.name), disabledWorkflowNames };
|
|
23026
22948
|
}
|
|
23027
22949
|
async function awsJson(args) {
|
|
23028
22950
|
const run = async () => {
|
|
@@ -23077,7 +22999,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
23077
22999
|
// #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
|
|
23078
23000
|
awsEntries: aws.entries,
|
|
23079
23001
|
schedulerRead: Boolean(aws.schedulerRead)
|
|
23080
|
-
});
|
|
23002
|
+
}, new Set(gh.disabledWorkflowNames));
|
|
23081
23003
|
const selfManaged = /* @__PURE__ */ new Set();
|
|
23082
23004
|
for (const proj of projects ?? []) {
|
|
23083
23005
|
if (proj?.schedulesMode !== "self-managed") continue;
|
|
@@ -23926,6 +23848,7 @@ var import_node_os11 = require("node:os");
|
|
|
23926
23848
|
var import_node_path29 = require("node:path");
|
|
23927
23849
|
|
|
23928
23850
|
// src/bootstrap-drift.ts
|
|
23851
|
+
var import_node_crypto6 = require("node:crypto");
|
|
23929
23852
|
function byteComparableSeeds(manifest, cls) {
|
|
23930
23853
|
return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
|
|
23931
23854
|
}
|
|
@@ -23934,6 +23857,9 @@ function compareSeedBytes(hubContent, repoContent) {
|
|
|
23934
23857
|
const normalize = (s) => s.replace(/\r\n/g, "\n");
|
|
23935
23858
|
return normalize(hubContent) === normalize(repoContent) ? "match" : "drift";
|
|
23936
23859
|
}
|
|
23860
|
+
function seedContentHash(content) {
|
|
23861
|
+
return (0, import_node_crypto6.createHash)("sha256").update(content.replace(/\r\n/g, "\n"), "utf8").digest("hex");
|
|
23862
|
+
}
|
|
23937
23863
|
function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
23938
23864
|
const byTarget = new Map(repoReads.map((r) => [r.target, r.content]));
|
|
23939
23865
|
const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
|
|
@@ -23956,11 +23882,14 @@ function auditRepoSeedDrift(repo, seeds, hubContents, repoReads) {
|
|
|
23956
23882
|
findings.push({ repo, target: seed.target, state: "waived", detail: `${state} \u2014 waived: ${why}` });
|
|
23957
23883
|
continue;
|
|
23958
23884
|
}
|
|
23885
|
+
const repoContent = byTarget.get(seed.target);
|
|
23959
23886
|
findings.push({
|
|
23960
23887
|
repo,
|
|
23961
23888
|
target: seed.target,
|
|
23962
23889
|
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
|
|
23890
|
+
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)",
|
|
23891
|
+
// #4242: only a real 'drift' has bytes worth hashing — 'absent' has none to compare against history.
|
|
23892
|
+
...state === "drift" && repoContent != null ? { contentHash: seedContentHash(repoContent) } : {}
|
|
23964
23893
|
});
|
|
23965
23894
|
}
|
|
23966
23895
|
return findings;
|
|
@@ -23979,6 +23908,115 @@ function renderSeedDriftReport(findings, reposAudited, seedsPerRepo) {
|
|
|
23979
23908
|
return lines.join("\n");
|
|
23980
23909
|
}
|
|
23981
23910
|
|
|
23911
|
+
// src/bootstrap-propagate.ts
|
|
23912
|
+
function assertPropagationCoverage(rosterCount, independentRegistryCount) {
|
|
23913
|
+
if (rosterCount === 0) {
|
|
23914
|
+
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)");
|
|
23915
|
+
}
|
|
23916
|
+
if (rosterCount < independentRegistryCount) {
|
|
23917
|
+
throw new Error(
|
|
23918
|
+
`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)`
|
|
23919
|
+
);
|
|
23920
|
+
}
|
|
23921
|
+
}
|
|
23922
|
+
function assignWaves(repos, canarySlug) {
|
|
23923
|
+
const waves = /* @__PURE__ */ new Map();
|
|
23924
|
+
if (!canarySlug) return waves;
|
|
23925
|
+
const rest = repos.filter((r) => r.slug !== canarySlug).slice().sort((a, b) => a.slug < b.slug ? -1 : a.slug > b.slug ? 1 : 0);
|
|
23926
|
+
const canary = repos.find((r) => r.slug === canarySlug);
|
|
23927
|
+
if (canary) waves.set(canary.repo, 0);
|
|
23928
|
+
const wave1Count = Math.ceil(rest.length * 0.25);
|
|
23929
|
+
rest.forEach((r, i) => waves.set(r.repo, i < wave1Count ? 1 : 2));
|
|
23930
|
+
return waves;
|
|
23931
|
+
}
|
|
23932
|
+
function statusFor(read) {
|
|
23933
|
+
if (!read) return { status: "pending", record: {} };
|
|
23934
|
+
if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
|
|
23935
|
+
const pr2 = read.pr;
|
|
23936
|
+
if (!pr2) return { status: "pending", record: {} };
|
|
23937
|
+
if (pr2.state === "closed") return { status: "closed-unmerged", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
23938
|
+
if (pr2.checks === "red") return { status: "red", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
23939
|
+
return { status: "open-pending", record: { prNumber: pr2.number, prUrl: pr2.url, mergeSha: pr2.mergeSha } };
|
|
23940
|
+
}
|
|
23941
|
+
var HALTING_STATUSES = /* @__PURE__ */ new Set(["red", "closed-unmerged"]);
|
|
23942
|
+
function planPropagationTick(input) {
|
|
23943
|
+
const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied } = input;
|
|
23944
|
+
const readByRepo = new Map(reads.map((r) => [r.repo, r]));
|
|
23945
|
+
const records = [];
|
|
23946
|
+
const opened = [];
|
|
23947
|
+
const waived = repos.filter((r) => r.waiver);
|
|
23948
|
+
for (const r of waived) {
|
|
23949
|
+
records.push({ repo: r.repo, target, wave: null, status: "skipped-waived", action: "none", detail: `waived: ${r.waiver}` });
|
|
23950
|
+
}
|
|
23951
|
+
const eligible = repos.filter((r) => !r.waiver);
|
|
23952
|
+
const refusedNoCanary = !canarySlug || !eligible.some((r) => r.slug === canarySlug);
|
|
23953
|
+
if (refusedNoCanary) {
|
|
23954
|
+
for (const r of eligible) {
|
|
23955
|
+
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)" });
|
|
23956
|
+
}
|
|
23957
|
+
return { target, reposInScope: repos.length, canary: null, refusedNoCanary: true, halted: false, haltReason: null, opened, records };
|
|
23958
|
+
}
|
|
23959
|
+
const waveOf = assignWaves(eligible, canarySlug);
|
|
23960
|
+
const byWave = { 0: [], 1: [], 2: [] };
|
|
23961
|
+
for (const r of eligible) byWave[waveOf.get(r.repo) ?? 2].push(r);
|
|
23962
|
+
let halted = false;
|
|
23963
|
+
let haltReason = null;
|
|
23964
|
+
let waveGateOpen = true;
|
|
23965
|
+
for (const waveNum of [0, 1, 2]) {
|
|
23966
|
+
const waveRepos = byWave[waveNum];
|
|
23967
|
+
if (!waveRepos.length) continue;
|
|
23968
|
+
if (!waveGateOpen || halted) {
|
|
23969
|
+
for (const r of waveRepos) {
|
|
23970
|
+
records.push({ repo: r.repo, target, wave: waveNum, status: "not-yet-reached", action: "none", detail: "prior wave not yet converged" });
|
|
23971
|
+
}
|
|
23972
|
+
continue;
|
|
23973
|
+
}
|
|
23974
|
+
let waveAllMatch = true;
|
|
23975
|
+
let waveHasRed = false;
|
|
23976
|
+
for (const r of waveRepos) {
|
|
23977
|
+
const { status, record } = statusFor(readByRepo.get(r.repo));
|
|
23978
|
+
const shouldOpen = status === "pending";
|
|
23979
|
+
if (shouldOpen) opened.push(r.repo);
|
|
23980
|
+
records.push({
|
|
23981
|
+
repo: r.repo,
|
|
23982
|
+
target,
|
|
23983
|
+
wave: waveNum,
|
|
23984
|
+
status,
|
|
23985
|
+
action: shouldOpen ? "open-pr" : "none",
|
|
23986
|
+
detail: shouldOpen ? "no open propagation PR and not yet matching \u2014 opening this tick" : status,
|
|
23987
|
+
...record
|
|
23988
|
+
});
|
|
23989
|
+
if (status !== "match") waveAllMatch = false;
|
|
23990
|
+
if (HALTING_STATUSES.has(status)) waveHasRed = true;
|
|
23991
|
+
}
|
|
23992
|
+
if (waveHasRed) {
|
|
23993
|
+
halted = true;
|
|
23994
|
+
haltReason = `wave ${waveNum} has a red or closed-unmerged PR \u2014 halting; no further wave opens (#4238 halt-and-alarm)`;
|
|
23995
|
+
waveGateOpen = false;
|
|
23996
|
+
continue;
|
|
23997
|
+
}
|
|
23998
|
+
if (waveNum === 0 && isWorkflowSeed && !functionGateSatisfied) {
|
|
23999
|
+
waveGateOpen = false;
|
|
24000
|
+
continue;
|
|
24001
|
+
}
|
|
24002
|
+
waveGateOpen = waveAllMatch;
|
|
24003
|
+
}
|
|
24004
|
+
return { target, reposInScope: repos.length, canary: canarySlug, refusedNoCanary: false, halted, haltReason, opened, records };
|
|
24005
|
+
}
|
|
24006
|
+
function renderPropagationReport(plan) {
|
|
24007
|
+
const lines = [`bootstrap propagate \u2014 target ${plan.target}: ${plan.reposInScope} repo(s) in scope, canary=${plan.canary ?? "NONE"}`];
|
|
24008
|
+
if (plan.refusedNoCanary) {
|
|
24009
|
+
lines.push(" REFUSED \u2014 no canary declared for this target; plan every repo not-yet-reached");
|
|
24010
|
+
return lines.join("\n");
|
|
24011
|
+
}
|
|
24012
|
+
for (const r of plan.records) {
|
|
24013
|
+
const wave2 = r.wave == null ? "-" : String(r.wave);
|
|
24014
|
+
lines.push(` wave${wave2.padEnd(2)} ${r.status.padEnd(16)} ${r.repo}${r.prNumber ? ` PR#${r.prNumber}` : ""} \u2014 ${r.detail}`);
|
|
24015
|
+
}
|
|
24016
|
+
lines.push(plan.halted ? ` HALTED \u2014 ${plan.haltReason}` : ` opened this tick: ${plan.opened.length ? plan.opened.join(", ") : "(none)"}`);
|
|
24017
|
+
return lines.join("\n");
|
|
24018
|
+
}
|
|
24019
|
+
|
|
23982
24020
|
// src/bootstrap-verify.ts
|
|
23983
24021
|
var TRAIN_BRANCHES2 = ["development", "rc", "main"];
|
|
23984
24022
|
var requiredDocs = ["README.md", "architecture.md", "docs/decisions/README.md", "docs/index.md"];
|
|
@@ -25065,6 +25103,174 @@ LIVE apply to ${repo}:
|
|
|
25065
25103
|
${applied.join("\n ")}`);
|
|
25066
25104
|
}
|
|
25067
25105
|
});
|
|
25106
|
+
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 () => {
|
|
25107
|
+
const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
|
|
25108
|
+
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
25109
|
+
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`);
|
|
25110
|
+
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
25111
|
+
const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
|
|
25112
|
+
if (!o.target) {
|
|
25113
|
+
return fail(`bootstrap propagate: --target <path> is required \u2014 one of:
|
|
25114
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25115
|
+
}
|
|
25116
|
+
const seed = propagatable.find((s) => s.target === o.target);
|
|
25117
|
+
if (!seed) return fail(`bootstrap propagate: --target '${o.target}' names no ownership:org + source:self seed in ${manifestPath}. Propagatable targets:
|
|
25118
|
+
${propagatable.map((s) => s.target).join("\n ")}`);
|
|
25119
|
+
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`);
|
|
25120
|
+
const hubContent = (0, import_node_fs31.readFileSync)(seed.target, "utf8");
|
|
25121
|
+
const isWorkflowSeed = seed.target.startsWith(".github/workflows/");
|
|
25122
|
+
const cfg = await loadConfig();
|
|
25123
|
+
const projects = await fetchProjectsList(registryClientDeps(cfg));
|
|
25124
|
+
if (!projects || projects.length === 0) {
|
|
25125
|
+
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)");
|
|
25126
|
+
}
|
|
25127
|
+
const rosterRepos2 = collectRegistryRepos(projects).filter((r) => r.toLowerCase() !== "mutmutco/mmi-hub");
|
|
25128
|
+
let independentCount = rosterRepos2.length;
|
|
25129
|
+
if ((0, import_node_fs31.existsSync)("projects.json")) {
|
|
25130
|
+
try {
|
|
25131
|
+
const local = JSON.parse((0, import_node_fs31.readFileSync)("projects.json", "utf8"));
|
|
25132
|
+
const localRepos = /* @__PURE__ */ new Set();
|
|
25133
|
+
for (const p of local.projects ?? []) for (const r of p.repos ?? []) {
|
|
25134
|
+
const full = (r.includes("/") ? r : `mutmutco/${r}`).toLowerCase();
|
|
25135
|
+
if (full !== "mutmutco/mmi-hub") localRepos.add(full);
|
|
25136
|
+
}
|
|
25137
|
+
if (localRepos.size > 0) independentCount = localRepos.size;
|
|
25138
|
+
} catch {
|
|
25139
|
+
}
|
|
25140
|
+
}
|
|
25141
|
+
try {
|
|
25142
|
+
assertPropagationCoverage(rosterRepos2.length, independentCount);
|
|
25143
|
+
} catch (e) {
|
|
25144
|
+
return fail(e.message);
|
|
25145
|
+
}
|
|
25146
|
+
const bySlugMeta = new Map(projects.flatMap((p) => (p.repos ?? []).map((r) => [(r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p])));
|
|
25147
|
+
const classOf = (repo) => bySlugMeta.get(repo.toLowerCase())?.class ?? "deployable";
|
|
25148
|
+
const canaryProject = projects.find((p) => p.seedCanary === true);
|
|
25149
|
+
const canarySlug = canaryProject ? (canaryProject.repos ?? [])[0]?.split("/").pop()?.toLowerCase() ?? null : null;
|
|
25150
|
+
const repos = rosterRepos2.map((repo) => {
|
|
25151
|
+
const slug = repo.split("/").pop().toLowerCase();
|
|
25152
|
+
const waiver = seed.waivers?.[slug];
|
|
25153
|
+
return { repo, slug, waiver };
|
|
25154
|
+
});
|
|
25155
|
+
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
25156
|
+
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
25157
|
+
const branchPrefix = "seed-propagate";
|
|
25158
|
+
const reads = [];
|
|
25159
|
+
for (const r of repos) {
|
|
25160
|
+
if (r.waiver) continue;
|
|
25161
|
+
const baseBranch = classOf(r.repo) === "content" ? "main" : "development";
|
|
25162
|
+
let content = null;
|
|
25163
|
+
try {
|
|
25164
|
+
const resp = await gh(["api", `repos/${r.repo}/contents/${enc(seed.target)}?ref=${baseBranch}`]);
|
|
25165
|
+
const parsed = JSON.parse(resp.stdout);
|
|
25166
|
+
content = parsed.encoding === "base64" && typeof parsed.content === "string" ? Buffer.from(parsed.content, "base64").toString("utf8") : null;
|
|
25167
|
+
} catch {
|
|
25168
|
+
content = null;
|
|
25169
|
+
}
|
|
25170
|
+
const drift = compareSeedBytes(hubContent, content);
|
|
25171
|
+
let pr2;
|
|
25172
|
+
try {
|
|
25173
|
+
const branch = `${branchPrefix}-${r.slug}`;
|
|
25174
|
+
const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit", "--limit", "1"]);
|
|
25175
|
+
const arr = JSON.parse(listed.stdout || "[]");
|
|
25176
|
+
const p = arr[0];
|
|
25177
|
+
if (p) {
|
|
25178
|
+
const rollup = p.statusCheckRollup ?? [];
|
|
25179
|
+
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";
|
|
25180
|
+
pr2 = { number: p.number, url: p.url, state: p.state === "MERGED" ? "merged" : p.state === "CLOSED" ? "closed" : "open", checks, mergeSha: p.mergeCommit?.oid };
|
|
25181
|
+
}
|
|
25182
|
+
} catch {
|
|
25183
|
+
pr2 = void 0;
|
|
25184
|
+
}
|
|
25185
|
+
reads.push({ repo: r.repo, drift: drift === "waived" ? "match" : drift, pr: pr2 });
|
|
25186
|
+
}
|
|
25187
|
+
let functionGateSatisfied = !isWorkflowSeed;
|
|
25188
|
+
if (isWorkflowSeed && canarySlug) {
|
|
25189
|
+
const canaryRepo = repos.find((r) => r.slug === canarySlug)?.repo;
|
|
25190
|
+
const canaryRead = reads.find((r) => r.repo === canaryRepo);
|
|
25191
|
+
if (canaryRead?.drift === "match") {
|
|
25192
|
+
try {
|
|
25193
|
+
const workflowFile = seed.target.split("/").pop();
|
|
25194
|
+
const runs = await gh(["api", `repos/${canaryRepo}/actions/workflows/${workflowFile}/runs?status=success&per_page=1`]);
|
|
25195
|
+
const parsed = JSON.parse(runs.stdout);
|
|
25196
|
+
functionGateSatisfied = Array.isArray(parsed.workflow_runs) && parsed.workflow_runs.length > 0;
|
|
25197
|
+
} catch {
|
|
25198
|
+
functionGateSatisfied = false;
|
|
25199
|
+
}
|
|
25200
|
+
}
|
|
25201
|
+
}
|
|
25202
|
+
const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied });
|
|
25203
|
+
if (o.execute) {
|
|
25204
|
+
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)");
|
|
25205
|
+
const headSha = (await gh(["api", "repos/mutmutco/MMI-Hub/commits/development", "--jq", ".sha"])).stdout.trim();
|
|
25206
|
+
for (const rec of plan.records) {
|
|
25207
|
+
if (rec.action !== "open-pr") continue;
|
|
25208
|
+
const repoEntry = repos.find((r) => r.repo === rec.repo);
|
|
25209
|
+
const baseBranch = classOf(rec.repo) === "content" ? "main" : "development";
|
|
25210
|
+
const branch = `${branchPrefix}-${repoEntry.slug}`;
|
|
25211
|
+
const baseRef = await gh(["api", `repos/${rec.repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"]);
|
|
25212
|
+
const baseSha = baseRef.stdout.trim();
|
|
25213
|
+
let branchExists = true;
|
|
25214
|
+
try {
|
|
25215
|
+
await gh(["api", `repos/${rec.repo}/git/ref/heads/${branch}`]);
|
|
25216
|
+
} catch {
|
|
25217
|
+
branchExists = false;
|
|
25218
|
+
}
|
|
25219
|
+
if (!branchExists) await gh(["api", `repos/${rec.repo}/git/refs`, "-f", `ref=refs/heads/${branch}`, "-f", `sha=${baseSha}`]);
|
|
25220
|
+
let existingSha;
|
|
25221
|
+
try {
|
|
25222
|
+
const cur = await gh(["api", `repos/${rec.repo}/contents/${enc(seed.target)}?ref=${branch}`]);
|
|
25223
|
+
existingSha = JSON.parse(cur.stdout).sha;
|
|
25224
|
+
} catch {
|
|
25225
|
+
existingSha = void 0;
|
|
25226
|
+
}
|
|
25227
|
+
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`);
|
|
25228
|
+
(0, import_node_fs31.writeFileSync)(tmp, JSON.stringify(contentPutBody(seed.target, hubContent, branch, existingSha)), "utf8");
|
|
25229
|
+
try {
|
|
25230
|
+
await gh(contentPutInputArgs(rec.repo, seed.target, tmp));
|
|
25231
|
+
} finally {
|
|
25232
|
+
try {
|
|
25233
|
+
(0, import_node_fs31.unlinkSync)(tmp);
|
|
25234
|
+
} catch {
|
|
25235
|
+
}
|
|
25236
|
+
}
|
|
25237
|
+
const openPrs = await gh(["pr", "list", "--repo", rec.repo, "--head", branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
25238
|
+
const prDecision = decideSeedPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
25239
|
+
let prUrl;
|
|
25240
|
+
if (prDecision.action === "reuse") {
|
|
25241
|
+
prUrl = prDecision.url;
|
|
25242
|
+
} else {
|
|
25243
|
+
const created = await ghCreate([
|
|
25244
|
+
"pr",
|
|
25245
|
+
"create",
|
|
25246
|
+
"--repo",
|
|
25247
|
+
rec.repo,
|
|
25248
|
+
"--base",
|
|
25249
|
+
baseBranch,
|
|
25250
|
+
"--head",
|
|
25251
|
+
branch,
|
|
25252
|
+
"--title",
|
|
25253
|
+
`chore: propagate org-owned ${seed.target} from MMI-Hub (wave ${rec.wave})`,
|
|
25254
|
+
"--body",
|
|
25255
|
+
`Auto-opened by \`mmi-cli bootstrap propagate --target ${seed.target} --execute\` (#4238), wave ${rec.wave}.
|
|
25256
|
+
|
|
25257
|
+
Propagates MMI-Hub@${headSha} 's copy of \`${seed.target}\` to this repo \u2014 the file this PR carries and nothing else.
|
|
25258
|
+
|
|
25259
|
+
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.`
|
|
25260
|
+
]);
|
|
25261
|
+
prUrl = created.url;
|
|
25262
|
+
}
|
|
25263
|
+
if (rec.wave !== 0) {
|
|
25264
|
+
await gh(["pr", "merge", prUrl, "--repo", rec.repo, "--auto", "--squash"]).catch(() => {
|
|
25265
|
+
});
|
|
25266
|
+
}
|
|
25267
|
+
rec.prUrl = prUrl;
|
|
25268
|
+
}
|
|
25269
|
+
}
|
|
25270
|
+
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25271
|
+
else console.log(renderPropagationReport(plan));
|
|
25272
|
+
if (plan.halted) process.exitCode = 1;
|
|
25273
|
+
});
|
|
25068
25274
|
}
|
|
25069
25275
|
|
|
25070
25276
|
// src/stage-commands.ts
|
|
@@ -25605,6 +25811,7 @@ function registerBoardCommands(program3) {
|
|
|
25605
25811
|
force: o.force,
|
|
25606
25812
|
allowPartial: o.allowPartial
|
|
25607
25813
|
});
|
|
25814
|
+
invalidateStatuslineBoardCache();
|
|
25608
25815
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25609
25816
|
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
25817
|
} catch (e) {
|
|
@@ -25621,6 +25828,7 @@ function registerBoardCommands(program3) {
|
|
|
25621
25828
|
force: o.force,
|
|
25622
25829
|
allowPartial: o.allowPartial
|
|
25623
25830
|
});
|
|
25831
|
+
if (bulk.results.some((r) => r.claimed)) invalidateStatuslineBoardCache();
|
|
25624
25832
|
if (o.json) {
|
|
25625
25833
|
console.log(JSON.stringify(bulk.results));
|
|
25626
25834
|
} else {
|
|
@@ -25665,6 +25873,7 @@ function registerBoardCommands(program3) {
|
|
|
25665
25873
|
repo: o.repo,
|
|
25666
25874
|
allowPartial: o.allowPartial
|
|
25667
25875
|
});
|
|
25876
|
+
if (bulk.results.some((r) => r.moved)) invalidateStatuslineBoardCache();
|
|
25668
25877
|
if (o.json) {
|
|
25669
25878
|
console.log(JSON.stringify(bulk.results));
|
|
25670
25879
|
} else {
|
|
@@ -25689,6 +25898,7 @@ function registerBoardCommands(program3) {
|
|
|
25689
25898
|
if (issueRefs.length === 1) {
|
|
25690
25899
|
try {
|
|
25691
25900
|
const result = await moveBoardItem({ config: await loadConfigForBoardSelector2(issueRefs[0], o.repo), selector: issueRefs[0], status: canonicalStatus, repo: o.repo, allowPartial: o.allowPartial });
|
|
25901
|
+
invalidateStatuslineBoardCache();
|
|
25692
25902
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25693
25903
|
console.log(result.partial ? `Partially moved ${result.item.ref}: ${result.warning}` : `Moved ${result.item.ref} -> ${result.status}`);
|
|
25694
25904
|
} catch (e) {
|
|
@@ -25740,6 +25950,7 @@ function registerBoardCommands(program3) {
|
|
|
25740
25950
|
force: o.force,
|
|
25741
25951
|
allowPartial: o.allowPartial
|
|
25742
25952
|
});
|
|
25953
|
+
invalidateStatuslineBoardCache();
|
|
25743
25954
|
if (o.json) return console.log(JSON.stringify(result));
|
|
25744
25955
|
console.log(result.partial ? `Partially unclaimed ${result.item.ref}: ${result.warning}` : `Unclaimed ${result.item.ref} -> ${result.status}`);
|
|
25745
25956
|
} catch (e) {
|
|
@@ -25778,7 +25989,7 @@ var import_node_fs34 = require("node:fs");
|
|
|
25778
25989
|
var import_promises8 = require("node:fs/promises");
|
|
25779
25990
|
var import_node_path33 = require("node:path");
|
|
25780
25991
|
var import_node_os12 = require("node:os");
|
|
25781
|
-
var
|
|
25992
|
+
var import_node_child_process17 = require("node:child_process");
|
|
25782
25993
|
|
|
25783
25994
|
// src/board-advance.ts
|
|
25784
25995
|
function repoOf2(ref) {
|
|
@@ -26061,6 +26272,22 @@ function assertPrMergeHousekeepingClean(startingPath, context, options = {}) {
|
|
|
26061
26272
|
if (verdict.blocked) throw new Error(verdict.reason);
|
|
26062
26273
|
return housekeeping;
|
|
26063
26274
|
}
|
|
26275
|
+
async function bestEffortLeaseClose(wtPath, exec = (cmd, args) => execFileP2(cmd, args, { timeout: GIT_TIMEOUT_MS })) {
|
|
26276
|
+
const step = "close jerv worktree lease";
|
|
26277
|
+
try {
|
|
26278
|
+
await exec("jerv-cli", ["lease", "close", "--ref", wtPath]);
|
|
26279
|
+
return { step, status: "done" };
|
|
26280
|
+
} catch (e) {
|
|
26281
|
+
const err = e;
|
|
26282
|
+
const detail = `${err.message}
|
|
26283
|
+
${err.stderr ?? ""}`;
|
|
26284
|
+
if (/ENOENT|not found|not recognized/i.test(detail)) {
|
|
26285
|
+
return { step, status: "skipped: jerv-cli not on PATH" };
|
|
26286
|
+
}
|
|
26287
|
+
const msg = (err.stderr?.trim() || err.message).split("\n")[0];
|
|
26288
|
+
return { step, status: `failed: ${msg}` };
|
|
26289
|
+
}
|
|
26290
|
+
}
|
|
26064
26291
|
async function applyGcPlan(plan, remote, opts = {}) {
|
|
26065
26292
|
const result = { removedBranches: [], removedRemoteBranches: [], removedTrackingRefs: [], removedWorktreeDirs: [], refused: [], failed: [], pruned: false };
|
|
26066
26293
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
@@ -26109,6 +26336,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
26109
26336
|
removeWorktreeDir: wtDeps.removeWorktreeDir,
|
|
26110
26337
|
removalContext: { primaryRoot: primaryRepoRoot, actor: gcActor, command: "worktree gc", force: opts.force }
|
|
26111
26338
|
});
|
|
26339
|
+
if (cleanup.worktree?.status === "removed") await bestEffortLeaseClose(cleanup.worktree.path);
|
|
26112
26340
|
return cleanup;
|
|
26113
26341
|
},
|
|
26114
26342
|
cleanupRemoteBranch: (branch, expectedHeadOid) => deleteReviewedRemoteBranch(remote, branch.branch, expectedHeadOid),
|
|
@@ -26155,6 +26383,7 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
26155
26383
|
owner,
|
|
26156
26384
|
reason: owner ? "branch merged/closed or directory dead; owner registration was stale" : "branch merged/closed or directory dead; no owner registered"
|
|
26157
26385
|
});
|
|
26386
|
+
await bestEffortLeaseClose(wt.path);
|
|
26158
26387
|
} catch (e) {
|
|
26159
26388
|
const error = e.message.split("\n")[0];
|
|
26160
26389
|
result.failed.push(`${wt.path}: ${error}`);
|
|
@@ -26327,7 +26556,7 @@ async function remoteBranchExists2(branch, options = {}) {
|
|
|
26327
26556
|
}
|
|
26328
26557
|
var COMPOSE_TIMEOUT_MS = 12e4;
|
|
26329
26558
|
function spawnDeferredGcSweep() {
|
|
26330
|
-
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn:
|
|
26559
|
+
spawnDetachedSelf(["worktree", "gc", "sweep-deferred", "--quiet"], { spawn: import_node_child_process17.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
26331
26560
|
}
|
|
26332
26561
|
async function createDeferredWorktreeStore() {
|
|
26333
26562
|
try {
|
|
@@ -26736,6 +26965,7 @@ var import_node_fs35 = require("node:fs");
|
|
|
26736
26965
|
var import_promises9 = require("node:fs/promises");
|
|
26737
26966
|
var import_node_path34 = require("node:path");
|
|
26738
26967
|
var GH_TIMEOUT_MS = 2e4;
|
|
26968
|
+
var STALE_PR_LOOKUP_LIMIT = 20;
|
|
26739
26969
|
var DEFAULT_BASE = "origin/development";
|
|
26740
26970
|
var DEFAULT_REMOTE = "origin";
|
|
26741
26971
|
var PROTECTED_BRANCHES2 = /* @__PURE__ */ new Set(["development", "main", "master", "rc"]);
|
|
@@ -26895,13 +27125,16 @@ function scanOrphanDirs(worktreesRoot, worktreeGitRoot, deps = defaultOrphanDirS
|
|
|
26895
27125
|
}
|
|
26896
27126
|
return candidates;
|
|
26897
27127
|
}
|
|
26898
|
-
function formatStaleLeaks(leaks) {
|
|
26899
|
-
|
|
26900
|
-
const lines = [`worktree list --stale: ${leaks.length} leak(s)`];
|
|
27128
|
+
function formatStaleLeaks(leaks, prLookupFailures = []) {
|
|
27129
|
+
const lines = leaks.length ? [`worktree list --stale: ${leaks.length} leak(s)`] : ["worktree list --stale: no leaks found"];
|
|
26901
27130
|
for (const leak of leaks) {
|
|
26902
27131
|
lines.push(` [${leak.kind}] ${leak.ref} \u2014 ${leak.detail}`);
|
|
26903
27132
|
lines.push(` fix: ${leak.remediation}`);
|
|
26904
27133
|
}
|
|
27134
|
+
if (prLookupFailures.length) {
|
|
27135
|
+
lines.push(` INCOMPLETE: merge state could not be read for ${prLookupFailures.length} branch(es) \u2014 they are NOT covered above`);
|
|
27136
|
+
for (const f of prLookupFailures) lines.push(` ${f.branch}${f.detail ? ` \u2014 ${f.detail}` : ""}`);
|
|
27137
|
+
}
|
|
26905
27138
|
return lines.join("\n");
|
|
26906
27139
|
}
|
|
26907
27140
|
async function repoRootOf() {
|
|
@@ -27172,6 +27405,7 @@ function registerWorktreeCommands(program3) {
|
|
|
27172
27405
|
}
|
|
27173
27406
|
}
|
|
27174
27407
|
report.push(await bestEffortGit(["worktree", "prune"], primaryCheckout, "prune worktree metadata"));
|
|
27408
|
+
report.push(await bestEffortLeaseClose(wtPath));
|
|
27175
27409
|
const result = {
|
|
27176
27410
|
dryRun: false,
|
|
27177
27411
|
...plan,
|
|
@@ -27202,8 +27436,9 @@ function registerWorktreeCommands(program3) {
|
|
|
27202
27436
|
const ctx = await gatherWorktreeContext();
|
|
27203
27437
|
if (o.stale) {
|
|
27204
27438
|
const leaks = classifyStaleLeaks(ctx);
|
|
27205
|
-
|
|
27206
|
-
return console.log(
|
|
27439
|
+
const failures = ctx.prLookupFailures ?? [];
|
|
27440
|
+
if (o.json) return console.log(JSON.stringify({ stale: leaks, count: leaks.length, prLookupFailures: failures, complete: failures.length === 0 }, null, 2));
|
|
27441
|
+
return console.log(formatStaleLeaks(leaks, failures));
|
|
27207
27442
|
}
|
|
27208
27443
|
if (o.json) return console.log(JSON.stringify({ worktrees: ctx.worktrees }, null, 2));
|
|
27209
27444
|
if (!ctx.worktrees.length) return console.log("worktree list: no worktrees");
|
|
@@ -27227,22 +27462,18 @@ async function gatherWorktreeContext() {
|
|
|
27227
27462
|
const branchOut = (await execFileP2("git", ["branch", "--format=%(refname:short)"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout;
|
|
27228
27463
|
const localBranches = branchOut.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
27229
27464
|
const currentBranch2 = (await execFileP2("git", ["rev-parse", "--abbrev-ref", "HEAD"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || void 0;
|
|
27465
|
+
const { prs, failures: prLookupFailures } = await resolveBranchPrs(localBranches.filter((b) => !PROTECTED_BRANCHES2.has(b)), STALE_PR_LOOKUP_LIMIT);
|
|
27230
27466
|
const openPrBranches = /* @__PURE__ */ new Set();
|
|
27231
27467
|
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 {
|
|
27468
|
+
const byBranch = /* @__PURE__ */ new Map();
|
|
27469
|
+
for (const pr2 of prs) {
|
|
27470
|
+
const arr = byBranch.get(pr2.headRefName) ?? [];
|
|
27471
|
+
arr.push(pr2.state);
|
|
27472
|
+
byBranch.set(pr2.headRefName, arr);
|
|
27473
|
+
}
|
|
27474
|
+
for (const [br, states] of byBranch) {
|
|
27475
|
+
if (states.some((s) => s === "OPEN")) openPrBranches.add(br);
|
|
27476
|
+
else if (states.some((s) => s === "MERGED" || s === "CLOSED")) closedBranches.add(br);
|
|
27246
27477
|
}
|
|
27247
27478
|
const stages = [];
|
|
27248
27479
|
for (const wt of worktrees) {
|
|
@@ -27259,7 +27490,7 @@ async function gatherWorktreeContext() {
|
|
|
27259
27490
|
listDirs: (root) => worktreeScanDirs(root, primaryRepoRoot, defaultOrphanDirScanDeps.listDirs, isRepoCheckoutDir)
|
|
27260
27491
|
});
|
|
27261
27492
|
}
|
|
27262
|
-
return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs };
|
|
27493
|
+
return { worktrees, localBranches, currentBranch: currentBranch2, openPrBranches, closedBranches, stages, orphanDirs, prLookupFailures };
|
|
27263
27494
|
}
|
|
27264
27495
|
async function bestEffortGit(args, cwd, step, timeoutMs = GIT_TIMEOUT_MS) {
|
|
27265
27496
|
try {
|
|
@@ -27280,7 +27511,7 @@ ${err.stderr ?? ""}`;
|
|
|
27280
27511
|
|
|
27281
27512
|
// src/issue-commands.ts
|
|
27282
27513
|
var import_node_fs36 = require("node:fs");
|
|
27283
|
-
var
|
|
27514
|
+
var import_node_crypto7 = require("node:crypto");
|
|
27284
27515
|
var ghRunner = async (args, timeoutMs) => (await execFileP2("gh", args, { timeout: timeoutMs })).stdout;
|
|
27285
27516
|
var ReparentConflictError = class extends Error {
|
|
27286
27517
|
constructor(message, payload) {
|
|
@@ -27356,6 +27587,44 @@ async function editIssue(client, options, deps = {}) {
|
|
|
27356
27587
|
...parentResult ? { parent: parentResult } : {}
|
|
27357
27588
|
};
|
|
27358
27589
|
}
|
|
27590
|
+
var EVIDENCE_COMMENT_URL_RE = /^https:\/\/github\.com\/([\w.-]+\/[\w.-]+)\/issues\/(\d+)#issuecomment-(\d+)$/;
|
|
27591
|
+
async function verifyCloseEvidence(client, evidence, repo, issueNumber, duplicateOf) {
|
|
27592
|
+
const match = evidence.trim().match(EVIDENCE_COMMENT_URL_RE);
|
|
27593
|
+
if (!match) {
|
|
27594
|
+
throw new Error(
|
|
27595
|
+
`--evidence must be a canonical issue-comment URL (https://github.com/<owner>/<repo>/issues/<n>#issuecomment-<id>), got: ${evidence}`
|
|
27596
|
+
);
|
|
27597
|
+
}
|
|
27598
|
+
const [, evidenceRepo, evidenceIssue, commentId] = match;
|
|
27599
|
+
if (evidenceRepo.toLowerCase() !== repo.toLowerCase() || Number(evidenceIssue) !== issueNumber) {
|
|
27600
|
+
throw new Error(
|
|
27601
|
+
`--evidence comment belongs to ${evidenceRepo}#${evidenceIssue}, not the issue being closed (${repo}#${issueNumber})`
|
|
27602
|
+
);
|
|
27603
|
+
}
|
|
27604
|
+
let comment;
|
|
27605
|
+
try {
|
|
27606
|
+
comment = await client.rest(
|
|
27607
|
+
"GET",
|
|
27608
|
+
`repos/${repo}/issues/comments/${commentId}`
|
|
27609
|
+
);
|
|
27610
|
+
} catch (e) {
|
|
27611
|
+
if (e instanceof GitHubApiError && e.status === 404) {
|
|
27612
|
+
throw new Error(`--evidence comment ${commentId} does not exist on ${repo} \u2014 nothing to anchor the close to`);
|
|
27613
|
+
}
|
|
27614
|
+
throw e;
|
|
27615
|
+
}
|
|
27616
|
+
const expectedAnchor = `/issues/${issueNumber}#issuecomment-${commentId}`;
|
|
27617
|
+
if (!comment.html_url || !comment.html_url.toLowerCase().endsWith(expectedAnchor.toLowerCase())) {
|
|
27618
|
+
throw new Error(
|
|
27619
|
+
`--evidence comment ${commentId} is not a comment on ${repo}#${issueNumber} (it lives at ${comment.html_url ?? "unknown"})`
|
|
27620
|
+
);
|
|
27621
|
+
}
|
|
27622
|
+
if (duplicateOf !== void 0 && !new RegExp(`#${duplicateOf}\\b`).test(comment.body ?? "")) {
|
|
27623
|
+
throw new Error(
|
|
27624
|
+
`--reason duplicate-of ${duplicateOf}: the --evidence comment must cite #${duplicateOf} so the duplicate has an auditable destination`
|
|
27625
|
+
);
|
|
27626
|
+
}
|
|
27627
|
+
}
|
|
27359
27628
|
async function closeIssue(client, options, deps = {}) {
|
|
27360
27629
|
const parsed = parseIssueRef(options.ref);
|
|
27361
27630
|
const repo = parsed.repo ?? options.defaultRepo;
|
|
@@ -27363,6 +27632,9 @@ async function closeIssue(client, options, deps = {}) {
|
|
|
27363
27632
|
const url = `https://github.com/${repo}/issues/${parsed.number}`;
|
|
27364
27633
|
const reason = options.reason ?? "completed";
|
|
27365
27634
|
const stateReason = reason === "duplicate-of" ? "not_planned" : reason === "not-planned" ? "not_planned" : "completed";
|
|
27635
|
+
if (options.evidence !== void 0) {
|
|
27636
|
+
await verifyCloseEvidence(client, options.evidence, repo, parsed.number, reason === "duplicate-of" ? options.duplicateOf : void 0);
|
|
27637
|
+
}
|
|
27366
27638
|
await client.rest("PATCH", `repos/${repo}/issues/${parsed.number}`, {
|
|
27367
27639
|
body: { state: "closed", state_reason: stateReason }
|
|
27368
27640
|
});
|
|
@@ -27530,7 +27802,7 @@ function rowIdempotencyKey(batchKey, spec) {
|
|
|
27530
27802
|
const identity = `${spec.type}
|
|
27531
27803
|
${spec.title.trim()}
|
|
27532
27804
|
${spec.body ?? ""}`;
|
|
27533
|
-
const hash = (0,
|
|
27805
|
+
const hash = (0, import_node_crypto7.createHash)("sha256").update(identity).digest("hex").slice(0, 16);
|
|
27534
27806
|
return `${batchKey}:${hash}`;
|
|
27535
27807
|
}
|
|
27536
27808
|
var BATCH_SPEC_KEYS = /* @__PURE__ */ new Set(["type", "title", "body", "priority", "labels", "label", "parent", "repo", "surface"]);
|
|
@@ -27749,7 +28021,7 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
27749
28021
|
}
|
|
27750
28022
|
});
|
|
27751
28023
|
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)"),
|
|
28024
|
+
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
28025
|
(opts, args) => {
|
|
27754
28026
|
let reason;
|
|
27755
28027
|
try {
|
|
@@ -27778,7 +28050,8 @@ function registerIssueLifecycleCommands(program3, deps = {}) {
|
|
|
27778
28050
|
defaultRepo,
|
|
27779
28051
|
reason: parsed.reason,
|
|
27780
28052
|
duplicateOf: o.duplicateOf ? Number(o.duplicateOf) : void 0,
|
|
27781
|
-
comment: o.comment
|
|
28053
|
+
comment: o.comment,
|
|
28054
|
+
evidence: o.evidence
|
|
27782
28055
|
});
|
|
27783
28056
|
console.log(JSON.stringify(result));
|
|
27784
28057
|
} catch (e) {
|
|
@@ -27909,6 +28182,7 @@ ${lines}`, {
|
|
|
27909
28182
|
surface: batchSurface,
|
|
27910
28183
|
noSurface: batchNoSurface
|
|
27911
28184
|
}, { attach: batchAttach });
|
|
28185
|
+
if (result.created.some((row) => !row.idempotent)) invalidateStatuslineBoardCache();
|
|
27912
28186
|
console.log(JSON.stringify(result));
|
|
27913
28187
|
if (result.failures.length) process.exitCode = 1;
|
|
27914
28188
|
} catch (e) {
|
|
@@ -31286,9 +31560,9 @@ function ghAccountCaveat(announcedLogin, accounts) {
|
|
|
31286
31560
|
var import_node_fs39 = require("node:fs");
|
|
31287
31561
|
var import_node_os14 = require("node:os");
|
|
31288
31562
|
var import_node_path37 = require("node:path");
|
|
31289
|
-
var
|
|
31563
|
+
var import_node_child_process18 = require("node:child_process");
|
|
31290
31564
|
var import_node_util8 = require("node:util");
|
|
31291
|
-
var execFileP6 = (0, import_node_util8.promisify)(
|
|
31565
|
+
var execFileP6 = (0, import_node_util8.promisify)(import_node_child_process18.execFile);
|
|
31292
31566
|
var MMI_PLUGIN_ID2 = "mmi@mutmutco";
|
|
31293
31567
|
function installedClaudePluginVersion() {
|
|
31294
31568
|
try {
|
|
@@ -31334,12 +31608,12 @@ function installedSurfacePluginVersion(surface) {
|
|
|
31334
31608
|
if (token === "claude") return installedClaudePluginVersion();
|
|
31335
31609
|
if (token !== "codex") return void 0;
|
|
31336
31610
|
try {
|
|
31337
|
-
const raw = process.platform === "win32" ? (0,
|
|
31611
|
+
const raw = process.platform === "win32" ? (0, import_node_child_process18.execFileSync)("cmd.exe", ["/c", "codex", "plugin", "list", "--json"], {
|
|
31338
31612
|
encoding: "utf8",
|
|
31339
31613
|
stdio: ["ignore", "pipe", "ignore"],
|
|
31340
31614
|
timeout: 15e3,
|
|
31341
31615
|
windowsHide: true
|
|
31342
|
-
}) : (0,
|
|
31616
|
+
}) : (0, import_node_child_process18.execFileSync)("codex", ["plugin", "list", "--json"], {
|
|
31343
31617
|
encoding: "utf8",
|
|
31344
31618
|
stdio: ["ignore", "pipe", "ignore"],
|
|
31345
31619
|
timeout: 15e3,
|
|
@@ -31357,7 +31631,7 @@ function installedActivePluginVersion(surface = detectSurface(process.env)) {
|
|
|
31357
31631
|
}
|
|
31358
31632
|
function worktreeRootSync() {
|
|
31359
31633
|
try {
|
|
31360
|
-
const out = (0,
|
|
31634
|
+
const out = (0, import_node_child_process18.execFileSync)("git", ["rev-parse", "--show-toplevel"], { windowsHide: true, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
31361
31635
|
let root = out.endsWith("\n") ? out.slice(0, -1) : out;
|
|
31362
31636
|
if (process.platform === "win32" && root.endsWith("\r")) root = root.slice(0, -1);
|
|
31363
31637
|
return root || null;
|
|
@@ -31411,23 +31685,6 @@ function hasRepoLocalWorktrees() {
|
|
|
31411
31685
|
|
|
31412
31686
|
// src/index.ts
|
|
31413
31687
|
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
31688
|
async function githubRepoReachProbe() {
|
|
31432
31689
|
const remote = await execFileP2("git", ["remote", "get-url", "origin"], { timeout: GIT_TIMEOUT_MS }).then((r) => r.stdout).catch(() => "");
|
|
31433
31690
|
const repo = parseOriginRepo(remote);
|
|
@@ -31694,6 +31951,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
31694
31951
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
31695
31952
|
removalContext
|
|
31696
31953
|
);
|
|
31954
|
+
for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
|
|
31697
31955
|
return {
|
|
31698
31956
|
removed: result.removed,
|
|
31699
31957
|
stillQueued: result.stillDeferred.length,
|
|
@@ -32148,6 +32406,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
32148
32406
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
32149
32407
|
{ removalContext }
|
|
32150
32408
|
);
|
|
32409
|
+
for (const removedPath of result.removed) await bestEffortLeaseClose(removedPath);
|
|
32151
32410
|
if (o.json) return console.log(JSON.stringify(result));
|
|
32152
32411
|
if (!o.quiet || result.removed.length || result.stillDeferred.length || result.skipped.length) {
|
|
32153
32412
|
if (result.removed.length) console.log(`worktree gc sweep-deferred: removed ${result.removed.length} worktree(s)`);
|
|
@@ -32163,7 +32422,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
32163
32422
|
process.exit(process.exitCode ?? 0);
|
|
32164
32423
|
});
|
|
32165
32424
|
});
|
|
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
|
|
32425
|
+
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
32426
|
if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
|
|
32168
32427
|
if (o.scratch) {
|
|
32169
32428
|
try {
|
|
@@ -32195,13 +32454,14 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
32195
32454
|
if (o.apply) {
|
|
32196
32455
|
const deferredStore = await createDeferredWorktreeStore();
|
|
32197
32456
|
const removalContext = await currentWorktreeRemovalContext("worktree gc", o.force);
|
|
32198
|
-
await sweepDeferredWorktrees(
|
|
32457
|
+
const sweepResult = await sweepDeferredWorktrees(
|
|
32199
32458
|
deferredStore,
|
|
32200
32459
|
// #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker ? keep a
|
|
32201
32460
|
// git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
|
|
32202
32461
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
32203
32462
|
removalContext
|
|
32204
32463
|
).catch(() => void 0);
|
|
32464
|
+
for (const removedPath of sweepResult?.removed ?? []) await bestEffortLeaseClose(removedPath);
|
|
32205
32465
|
applyResult = await applyGcPlan(plan, o.remote, { root, force: o.force });
|
|
32206
32466
|
}
|
|
32207
32467
|
if (o.json) {
|
|
@@ -32222,11 +32482,11 @@ var WORKTREE_SETUP_LOCK_TTL_MS = 10 * 6e4;
|
|
|
32222
32482
|
function runWorktreeInstall(command, cwd, quiet, opts) {
|
|
32223
32483
|
const stdio = quiet ? "ignore" : "inherit";
|
|
32224
32484
|
return new Promise((resolve5, reject) => {
|
|
32225
|
-
const child2 = opts?.shell ? (0,
|
|
32485
|
+
const child2 = opts?.shell ? (0, import_node_child_process19.spawn)(command, { cwd, stdio, windowsHide: true, shell: true }) : (() => {
|
|
32226
32486
|
const [bin, ...args] = command.split(" ");
|
|
32227
32487
|
const file = isWin2 ? "cmd.exe" : bin;
|
|
32228
32488
|
const spawnArgs = isWin2 ? ["/c", bin, ...args] : args;
|
|
32229
|
-
return (0,
|
|
32489
|
+
return (0, import_node_child_process19.spawn)(file, spawnArgs, { cwd, stdio, windowsHide: true });
|
|
32230
32490
|
})();
|
|
32231
32491
|
const timer = setTimeout(() => {
|
|
32232
32492
|
try {
|
|
@@ -32437,6 +32697,34 @@ withExamples(mutating(
|
|
|
32437
32697
|
const owner = { path: wtPath, branch, createdAt, lastSeenAt: createdAt, actor: createActor };
|
|
32438
32698
|
recordWorktreeOwner(repoRoot2, owner);
|
|
32439
32699
|
appendWorktreeEvent(repoRoot2, { action: "created", command: "worktree create", target: wtPath, branch, actor: owner.actor });
|
|
32700
|
+
let lease;
|
|
32701
|
+
try {
|
|
32702
|
+
await execFileP2("jerv-cli", [
|
|
32703
|
+
"lease",
|
|
32704
|
+
"open",
|
|
32705
|
+
"--kind",
|
|
32706
|
+
"worktree",
|
|
32707
|
+
"--ref",
|
|
32708
|
+
wtPath,
|
|
32709
|
+
...selector ? ["--repo", selector.repo] : [],
|
|
32710
|
+
// 72h, not the 24h default: a feature worktree routinely sits untouched over a weekend, and
|
|
32711
|
+
// an expired lease is what makes a CLEAN, unheld tree reapable. Erring long costs disk;
|
|
32712
|
+
// erring short reaps a tree someone is still using between sessions.
|
|
32713
|
+
"--ttl",
|
|
32714
|
+
"72",
|
|
32715
|
+
"--note",
|
|
32716
|
+
`${branch} via mmi-cli worktree create`
|
|
32717
|
+
], { timeout: GIT_TIMEOUT_MS });
|
|
32718
|
+
lease = { ok: true };
|
|
32719
|
+
} catch (e) {
|
|
32720
|
+
const detail = (e.stderr?.trim() || e.message.trim()).split("\n")[0];
|
|
32721
|
+
lease = { ok: false, error: detail };
|
|
32722
|
+
if (!o.json) {
|
|
32723
|
+
console.error(
|
|
32724
|
+
` 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).`
|
|
32725
|
+
);
|
|
32726
|
+
}
|
|
32727
|
+
}
|
|
32440
32728
|
if (o.json) {
|
|
32441
32729
|
return console.log(JSON.stringify({
|
|
32442
32730
|
branch,
|
|
@@ -32444,6 +32732,7 @@ withExamples(mutating(
|
|
|
32444
32732
|
base,
|
|
32445
32733
|
resumed,
|
|
32446
32734
|
...report,
|
|
32735
|
+
lease,
|
|
32447
32736
|
...issueForm && selector ? { issue: `${selector.repo}#${selector.number}` } : {},
|
|
32448
32737
|
...issueForm && o.claim ? { claim: claimError ? { ok: false, error: claimError } : claim } : {}
|
|
32449
32738
|
}, null, 2));
|
|
@@ -32555,7 +32844,7 @@ function scheduleRelatedDiscovery(o) {
|
|
|
32555
32844
|
try {
|
|
32556
32845
|
const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body, "--fail-soft"];
|
|
32557
32846
|
if (o.repo) args.push("--repo", o.repo);
|
|
32558
|
-
spawnDetachedSelf(args, { spawn:
|
|
32847
|
+
spawnDetachedSelf(args, { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] }, { cwd: process.cwd() });
|
|
32559
32848
|
} catch {
|
|
32560
32849
|
}
|
|
32561
32850
|
}
|
|
@@ -32883,48 +33172,6 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
|
|
|
32883
33172
|
await failGraceful(e.message);
|
|
32884
33173
|
}
|
|
32885
33174
|
});
|
|
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
33175
|
async function reportWrite(label, res) {
|
|
32929
33176
|
if (res.ok) {
|
|
32930
33177
|
console.log(JSON.stringify(res.body));
|
|
@@ -33584,6 +33831,7 @@ withExamples(mutating(
|
|
|
33584
33831
|
}
|
|
33585
33832
|
}
|
|
33586
33833
|
if (o.related !== false) scheduleRelatedDiscovery({ repo: o.repo, number: created.number, title, body });
|
|
33834
|
+
invalidateStatuslineBoardCache();
|
|
33587
33835
|
console.log(JSON.stringify({
|
|
33588
33836
|
...created,
|
|
33589
33837
|
label: issueType,
|
|
@@ -33884,6 +34132,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
33884
34132
|
return fail(`pr create: ${docsCheck.detail} ? ${docsCheck.fix}`);
|
|
33885
34133
|
}
|
|
33886
34134
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
34135
|
+
invalidateStatuslineBoardCache();
|
|
33887
34136
|
console.log(JSON.stringify(created));
|
|
33888
34137
|
}), [
|
|
33889
34138
|
'mmi-cli pr create --title "Add the schema" --body "Closes #2680"',
|
|
@@ -34283,6 +34532,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
34283
34532
|
preserveWorktree: o.preserveWorktree,
|
|
34284
34533
|
removalContext
|
|
34285
34534
|
});
|
|
34535
|
+
if (localCleanup.worktree?.status === "removed") await bestEffortLeaseClose(localCleanup.worktree.path);
|
|
34286
34536
|
} catch (e) {
|
|
34287
34537
|
localCleanup = {
|
|
34288
34538
|
branch: headRef,
|
|
@@ -34295,6 +34545,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
34295
34545
|
};
|
|
34296
34546
|
}
|
|
34297
34547
|
const boardAdvance = await advanceClosedIssuesToDone2(number, repoForPostCleanup);
|
|
34548
|
+
invalidateStatuslineBoardCache();
|
|
34298
34549
|
console.log(JSON.stringify({
|
|
34299
34550
|
...buildPrMergeResultPayload({
|
|
34300
34551
|
number,
|
|
@@ -34941,7 +35192,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
34941
35192
|
for (const line of scratchGcLines(process.cwd())) bannerIo.log(line);
|
|
34942
35193
|
const worktreeBanner = worktreeAutoProvisionBanner(process.cwd());
|
|
34943
35194
|
if (worktreeBanner) {
|
|
34944
|
-
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn:
|
|
35195
|
+
spawnDetachedSelf(["worktree", "setup", "--quiet"], { spawn: import_node_child_process19.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
34945
35196
|
bannerIo.log(worktreeBanner);
|
|
34946
35197
|
}
|
|
34947
35198
|
if (isLinkedWorktree(process.cwd())) {
|