@mutmutco/cli 3.100.0 → 3.102.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 +239 -46
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -7536,6 +7536,39 @@ function missingPlaceholders(rendered) {
|
|
|
7536
7536
|
for (const m of rendered.matchAll(PLACEHOLDER_RE)) out.add(m[1]);
|
|
7537
7537
|
return [...out];
|
|
7538
7538
|
}
|
|
7539
|
+
async function resolveHubSeedSource(execGit, defaultBranch = "development") {
|
|
7540
|
+
let status;
|
|
7541
|
+
try {
|
|
7542
|
+
status = (await execGit(["status", "--porcelain"])).trim();
|
|
7543
|
+
} catch (e) {
|
|
7544
|
+
return { ok: false, reason: `could not read git status of this checkout (${e.message}) \u2014 refusing to treat an unknown working tree as the fleet's desired state` };
|
|
7545
|
+
}
|
|
7546
|
+
if (status) {
|
|
7547
|
+
return {
|
|
7548
|
+
ok: false,
|
|
7549
|
+
reason: `this checkout has uncommitted changes \u2014 refusing to read seeds from a working tree that has not been reviewed or merged:
|
|
7550
|
+
${status}`
|
|
7551
|
+
};
|
|
7552
|
+
}
|
|
7553
|
+
let head;
|
|
7554
|
+
let remoteSha;
|
|
7555
|
+
try {
|
|
7556
|
+
head = (await execGit(["rev-parse", "HEAD"])).trim();
|
|
7557
|
+
remoteSha = (await execGit(["ls-remote", "origin", `refs/heads/${defaultBranch}`])).trim().split(/\s+/)[0] ?? "";
|
|
7558
|
+
} catch (e) {
|
|
7559
|
+
return { ok: false, reason: `could not resolve this checkout's HEAD against origin/${defaultBranch} (${e.message}) \u2014 refusing to read seeds without a known upstream revision` };
|
|
7560
|
+
}
|
|
7561
|
+
if (!remoteSha) {
|
|
7562
|
+
return { ok: false, reason: `\`git ls-remote origin refs/heads/${defaultBranch}\` returned nothing \u2014 refusing to read seeds without a known upstream revision` };
|
|
7563
|
+
}
|
|
7564
|
+
if (head !== remoteSha) {
|
|
7565
|
+
return {
|
|
7566
|
+
ok: false,
|
|
7567
|
+
reason: `this checkout (${head}) is not origin/${defaultBranch} (${remoteSha}) \u2014 fast-forward to origin/${defaultBranch} first; a stale or ahead checkout must not seed the fleet`
|
|
7568
|
+
};
|
|
7569
|
+
}
|
|
7570
|
+
return { ok: true, sha: head };
|
|
7571
|
+
}
|
|
7539
7572
|
var GITIGNORE_MANAGED_BEGIN = "# >>> mmi-managed >>>";
|
|
7540
7573
|
var GITIGNORE_MANAGED_END = "# <<< mmi-managed <<<";
|
|
7541
7574
|
var MANAGED_GITIGNORE_LINES = [
|
|
@@ -12168,6 +12201,16 @@ async function auditRepoCi(repo, deps) {
|
|
|
12168
12201
|
});
|
|
12169
12202
|
}
|
|
12170
12203
|
}
|
|
12204
|
+
if (repoClass === "deployable") {
|
|
12205
|
+
const exemptReason = meta?.ciExemptReason?.trim();
|
|
12206
|
+
const declaredExempt = statusChecks.size === 0 && !!exemptReason;
|
|
12207
|
+
checks.push({
|
|
12208
|
+
ok: statusChecks.size > 0 || declaredExempt,
|
|
12209
|
+
label: declaredExempt ? "deployable repo declared CI-exempt, not required to gate (#4265)" : "deployable repo requires at least one status check (#4260)",
|
|
12210
|
+
detail: declaredExempt ? `declared-exempt: ${exemptReason}` : statusChecks.size === 0 ? "zero required contexts on an active branch ruleset \u2014 any PR, including an automation seed PR, merges on an empty gate" : void 0,
|
|
12211
|
+
remediation: declaredExempt || statusChecks.size > 0 ? void 0 : `Determine this repo's correct gate context from its own PR-triggered workflow, set it via mmi-cli org project set ${repo} --var requiredChecks=[...], then mmi-cli ci reconcile --repo ${repo} --apply, or, if this repo genuinely has no CI surface, declare it via mmi-cli org project set ${repo} --var ciExemptReason="<why>" (#4265)`
|
|
12212
|
+
});
|
|
12213
|
+
}
|
|
12171
12214
|
if (deployableGated || repoClass === "hub") {
|
|
12172
12215
|
const gate = await readDefaultBranchGate(repo, deps.client, baseBranch, Date.now(), gateWorkflowFiles(prWorkflowPaths));
|
|
12173
12216
|
checks.push({
|
|
@@ -13865,6 +13908,12 @@ async function postSchedulesMode(slug, mode, deps) {
|
|
|
13865
13908
|
async function postSchedulesRun(id, deps) {
|
|
13866
13909
|
return postJson("/schedules/run", { id }, deps, "POST", { noRetry: true });
|
|
13867
13910
|
}
|
|
13911
|
+
async function postSchedulesPark(id, reason, deps) {
|
|
13912
|
+
return postJson("/schedules/park", { id, reason }, deps);
|
|
13913
|
+
}
|
|
13914
|
+
async function postSchedulesUnpark(id, deps) {
|
|
13915
|
+
return postJson("/schedules/unpark", { id }, deps);
|
|
13916
|
+
}
|
|
13868
13917
|
async function tenantControl(payload, deps) {
|
|
13869
13918
|
return postJson("/tenant-control", payload, deps, "POST", { noRetry: true });
|
|
13870
13919
|
}
|
|
@@ -20923,7 +20972,7 @@ function authorizeBodyHasMismatch(body) {
|
|
|
20923
20972
|
}
|
|
20924
20973
|
|
|
20925
20974
|
// src/project-set.ts
|
|
20926
|
-
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "gate", "seedCanary"];
|
|
20975
|
+
var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "requiredBuildSecrets", "secrets", "edgeDomains", "requiredGcpApis", "publishRequired", "publishDir", "dsManifestPath", "fofuEnabled", "consumesDesignSystem", "ci", "requiredChecks", "ciExemptReason", "gate", "seedCanary"];
|
|
20927
20976
|
var UNSET_KEY_SET = new Set(UNSET_KEYS);
|
|
20928
20977
|
var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
|
|
20929
20978
|
var SECRET_CONSUMERS = ["runtime", "build", "lambda", "actions", "agent", "box"];
|
|
@@ -21218,6 +21267,11 @@ function parseRequiredChecksVar(raw) {
|
|
|
21218
21267
|
}
|
|
21219
21268
|
return parsed.map((c) => c.trim());
|
|
21220
21269
|
}
|
|
21270
|
+
function parseCiExemptReasonVar(raw) {
|
|
21271
|
+
const trimmed = raw.trim();
|
|
21272
|
+
if (!trimmed) throw new Error("org project set: ciExemptReason must be a non-empty reason (or use --unset ciExemptReason to clear it)");
|
|
21273
|
+
return trimmed;
|
|
21274
|
+
}
|
|
21221
21275
|
function parseGateVar(raw) {
|
|
21222
21276
|
let parsed;
|
|
21223
21277
|
try {
|
|
@@ -21270,6 +21324,7 @@ var SETTABLE_VAR_KEYS = [
|
|
|
21270
21324
|
"portRange",
|
|
21271
21325
|
"ci",
|
|
21272
21326
|
"requiredChecks",
|
|
21327
|
+
"ciExemptReason",
|
|
21273
21328
|
"gate",
|
|
21274
21329
|
"secrets",
|
|
21275
21330
|
"seedCanary"
|
|
@@ -21296,6 +21351,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
21296
21351
|
portRange: "JSON {start,end} or [start,end]",
|
|
21297
21352
|
ci: "none \u2014 declare intentional no-ci",
|
|
21298
21353
|
requiredChecks: 'JSON array, e.g. ["gate"] or [] for no-ci',
|
|
21354
|
+
ciExemptReason: "non-empty string \u2014 declares this deployable repo has no CI surface (#4265)",
|
|
21299
21355
|
gate: "JSON {runtime,cmd,workdir,cacheDepPath,pyVersion}"
|
|
21300
21356
|
};
|
|
21301
21357
|
function settableVarHelp() {
|
|
@@ -21393,6 +21449,8 @@ function buildProjectSetPatch(input) {
|
|
|
21393
21449
|
patch[key] = raw;
|
|
21394
21450
|
} else if (key === "requiredChecks") {
|
|
21395
21451
|
patch[key] = parseRequiredChecksVar(raw);
|
|
21452
|
+
} else if (key === "ciExemptReason") {
|
|
21453
|
+
patch[key] = parseCiExemptReasonVar(raw);
|
|
21396
21454
|
} else if (key === "gate") {
|
|
21397
21455
|
patch[key] = parseGateVar(raw);
|
|
21398
21456
|
} else {
|
|
@@ -22730,6 +22788,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22730
22788
|
const liveByName = new Map(liveGithub.map((e) => [e.name, e]));
|
|
22731
22789
|
const registryById = new Map(registryGithub.map((r) => [r.id, r]));
|
|
22732
22790
|
const drifts = [];
|
|
22791
|
+
const parked = [];
|
|
22733
22792
|
for (const e of liveGithub) {
|
|
22734
22793
|
if (!registryById.has(e.name)) {
|
|
22735
22794
|
drifts.push({
|
|
@@ -22742,6 +22801,10 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22742
22801
|
}
|
|
22743
22802
|
}
|
|
22744
22803
|
for (const r of registryGithub) {
|
|
22804
|
+
if (r.parkedReason) {
|
|
22805
|
+
parked.push({ id: r.id, executor: r.executor, repo: r.repo, reason: r.parkedReason });
|
|
22806
|
+
continue;
|
|
22807
|
+
}
|
|
22745
22808
|
const live = liveByName.get(r.id);
|
|
22746
22809
|
if (live) {
|
|
22747
22810
|
if (cadenceStale(r.cadence, live.cadence)) {
|
|
@@ -22763,7 +22826,7 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22763
22826
|
name: r.id,
|
|
22764
22827
|
executor: r.executor || "github-actions",
|
|
22765
22828
|
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
|
|
22829
|
+
remedy: `\`gh workflow enable\` it in ${r.repo} to re-arm, or if this is a deliberate hold run \`mmi-cli org schedules park ${r.id} --reason <ref>\` (#4257) so it stops reading as drift \u2014 never prune a lane parked on purpose`
|
|
22767
22830
|
});
|
|
22768
22831
|
}
|
|
22769
22832
|
continue;
|
|
@@ -22778,7 +22841,10 @@ function reconcileGithubActions(liveGithub, registry2, readRepos, activeWorkflow
|
|
|
22778
22841
|
});
|
|
22779
22842
|
}
|
|
22780
22843
|
}
|
|
22781
|
-
return
|
|
22844
|
+
return {
|
|
22845
|
+
drifts: drifts.sort((a, b) => a.class.localeCompare(b.class) || a.name.localeCompare(b.name)),
|
|
22846
|
+
parked: parked.sort((a, b) => a.id.localeCompare(b.id))
|
|
22847
|
+
};
|
|
22782
22848
|
}
|
|
22783
22849
|
function suppressSelfManagedDrifts(drifts, selfManagedRepos) {
|
|
22784
22850
|
return drifts.filter((d) => {
|
|
@@ -22789,6 +22855,9 @@ function suppressSelfManagedDrifts(drifts, selfManagedRepos) {
|
|
|
22789
22855
|
function renderDrift(d) {
|
|
22790
22856
|
return `${d.class}: ${d.name} (${d.executor}) \u2014 ${d.detail}; remedy: ${d.remedy}`;
|
|
22791
22857
|
}
|
|
22858
|
+
function renderParked(p) {
|
|
22859
|
+
return `parked (${p.reason}): ${p.id}`;
|
|
22860
|
+
}
|
|
22792
22861
|
function strayCronDrift(name) {
|
|
22793
22862
|
return {
|
|
22794
22863
|
class: "stray-cron",
|
|
@@ -22824,12 +22893,17 @@ function unlauncheredLlmDrifts(entries) {
|
|
|
22824
22893
|
}
|
|
22825
22894
|
var HARBOUR_ARM_GRACE_MS = 30 * 60 * 1e3;
|
|
22826
22895
|
function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
|
|
22827
|
-
if (!opts.schedulerRead) return [];
|
|
22896
|
+
if (!opts.schedulerRead) return { drifts: [], parked: [] };
|
|
22828
22897
|
const now = opts.now ?? Date.now();
|
|
22829
22898
|
const armed = new Set(liveEntries.map((e) => e.scheduleId).filter((id) => Boolean(id)));
|
|
22830
22899
|
const drifts = [];
|
|
22900
|
+
const parked = [];
|
|
22831
22901
|
for (const row of registry2) {
|
|
22832
22902
|
if (row.executor === "github-actions") continue;
|
|
22903
|
+
if (row.parkedReason) {
|
|
22904
|
+
parked.push({ id: row.id, executor: row.executor, repo: row.repo, reason: row.parkedReason });
|
|
22905
|
+
continue;
|
|
22906
|
+
}
|
|
22833
22907
|
if (armed.has(row.id)) continue;
|
|
22834
22908
|
const stamped = row.updatedAt ? Date.parse(row.updatedAt) : NaN;
|
|
22835
22909
|
if (Number.isFinite(stamped) && now - stamped < HARBOUR_ARM_GRACE_MS) continue;
|
|
@@ -22841,25 +22915,36 @@ function registeredButUnarmedDrifts(registry2, liveEntries, opts) {
|
|
|
22841
22915
|
remedy: "check the fleet-clock reconciler tick; re-run `mmi-cli org schedules register` for the repo and verify with `mmi-cli org schedules`"
|
|
22842
22916
|
});
|
|
22843
22917
|
}
|
|
22844
|
-
return
|
|
22918
|
+
return {
|
|
22919
|
+
drifts: drifts.sort((a, b) => a.name.localeCompare(b.name)),
|
|
22920
|
+
parked: parked.sort((a, b) => a.id.localeCompare(b.id))
|
|
22921
|
+
};
|
|
22845
22922
|
}
|
|
22846
22923
|
function assembleReconciliation(githubEntries2, readRepos, registry2, activeWorkflowNames = /* @__PURE__ */ new Set(), harbour = {}, disabledWorkflowNames = /* @__PURE__ */ new Set()) {
|
|
22847
22924
|
if (registry2 === null) {
|
|
22848
22925
|
return {
|
|
22849
22926
|
reconciliation: [],
|
|
22850
22927
|
driftLines: [],
|
|
22928
|
+
parked: [],
|
|
22929
|
+
parkedLines: [],
|
|
22851
22930
|
incomplete: ["registry: could not read GET /schedules/list \u2014 reconciliation skipped (Hub API unreachable or not authenticated)"]
|
|
22852
22931
|
};
|
|
22853
22932
|
}
|
|
22854
22933
|
const live = githubEntries2.filter((e) => e.executor === "github-actions");
|
|
22855
|
-
const
|
|
22856
|
-
|
|
22857
|
-
|
|
22858
|
-
|
|
22859
|
-
|
|
22860
|
-
|
|
22861
|
-
];
|
|
22862
|
-
return {
|
|
22934
|
+
const githubResult = reconcileGithubActions(live, registry2, readRepos, activeWorkflowNames, disabledWorkflowNames);
|
|
22935
|
+
const harbourResult = registeredButUnarmedDrifts(registry2, harbour.awsEntries ?? [], {
|
|
22936
|
+
schedulerRead: Boolean(harbour.schedulerRead),
|
|
22937
|
+
now: harbour.now
|
|
22938
|
+
});
|
|
22939
|
+
const drifts = [...githubResult.drifts, ...harbourResult.drifts];
|
|
22940
|
+
const parked = [...githubResult.parked, ...harbourResult.parked].sort((a, b) => a.id.localeCompare(b.id));
|
|
22941
|
+
return {
|
|
22942
|
+
reconciliation: drifts,
|
|
22943
|
+
driftLines: drifts.map(renderDrift),
|
|
22944
|
+
parked,
|
|
22945
|
+
parkedLines: parked.map(renderParked),
|
|
22946
|
+
incomplete: []
|
|
22947
|
+
};
|
|
22863
22948
|
}
|
|
22864
22949
|
|
|
22865
22950
|
// src/schedules-commands.ts
|
|
@@ -23023,7 +23108,9 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
23023
23108
|
entries: sortEntries([...gh.entries, ...aws.entries, ...DECLARED_ENTRIES]),
|
|
23024
23109
|
incomplete: [...gh.incomplete, ...aws.incomplete, ...recon.incomplete],
|
|
23025
23110
|
drift: driftLines,
|
|
23026
|
-
reconciliation
|
|
23111
|
+
reconciliation,
|
|
23112
|
+
parked: recon.parked,
|
|
23113
|
+
parkedLines: recon.parkedLines
|
|
23027
23114
|
};
|
|
23028
23115
|
}
|
|
23029
23116
|
async function defaultProjectsRead() {
|
|
@@ -23036,10 +23123,13 @@ function warnIncomplete2(incomplete) {
|
|
|
23036
23123
|
function reportDrift(drift) {
|
|
23037
23124
|
for (const d of drift) console.error(`org schedules: DRIFT \u2014 ${d}`);
|
|
23038
23125
|
}
|
|
23126
|
+
function reportParked(parked) {
|
|
23127
|
+
for (const p of parked) console.log(`org schedules: ${p}`);
|
|
23128
|
+
}
|
|
23039
23129
|
function registerSchedulesCommands(program3) {
|
|
23040
23130
|
const schedules = program3.command("schedules").description("the org schedules notebook \u2014 every armed cron, timer, and scheduled LLM lane, resolved live (jerv side lives in jerv-cli schedules)").option("--json", "machine-readable output (consumed by jerv-cli schedules --all)").option("--doc <path>", "optional snapshot only \u2014 splice inventory between schedules:inventory markers; live `org schedules` / --json remains SSOT (Hub#4120)").action(async (o) => {
|
|
23041
23131
|
try {
|
|
23042
|
-
const { entries, incomplete, drift, reconciliation } = await fetchNotebook();
|
|
23132
|
+
const { entries, incomplete, drift, reconciliation, parked, parkedLines } = await fetchNotebook();
|
|
23043
23133
|
if (o.doc) {
|
|
23044
23134
|
if (incomplete.length) {
|
|
23045
23135
|
warnIncomplete2(incomplete);
|
|
@@ -23055,9 +23145,10 @@ function registerSchedulesCommands(program3) {
|
|
|
23055
23145
|
}
|
|
23056
23146
|
if (o.json) {
|
|
23057
23147
|
const now = /* @__PURE__ */ new Date();
|
|
23058
|
-
console.log(JSON.stringify({ org: entries.map((e) => withTickState(e, now)), incomplete, drift, reconciliation }, null, 2));
|
|
23148
|
+
console.log(JSON.stringify({ org: entries.map((e) => withTickState(e, now)), incomplete, drift, reconciliation, parked }, null, 2));
|
|
23059
23149
|
} else {
|
|
23060
23150
|
console.log(formatSchedulesTable(entries));
|
|
23151
|
+
reportParked(parkedLines);
|
|
23061
23152
|
reportDrift(drift);
|
|
23062
23153
|
warnIncomplete2(incomplete);
|
|
23063
23154
|
}
|
|
@@ -23078,6 +23169,30 @@ function registerSchedulesCommands(program3) {
|
|
|
23078
23169
|
await failGraceful(e.message);
|
|
23079
23170
|
}
|
|
23080
23171
|
});
|
|
23172
|
+
schedules.command("park <schedule-id>").description("record a deliberate hold on a SCHEDULE# row \u2014 the reconciler skips it and `org schedules` renders `parked (<reason>)` instead of DRIFT (#4257)").requiredOption("--reason <ref>", "why the lane is held, e.g. an issue ref (#1774)").action(async (scheduleId, o) => {
|
|
23173
|
+
try {
|
|
23174
|
+
const res = await postSchedulesPark(scheduleId, o.reason, registryClientDeps(await loadConfig()));
|
|
23175
|
+
if (!res.ok) {
|
|
23176
|
+
await failGraceful(`schedules park: HTTP ${res.status} \u2014 ${JSON.stringify(res.body ?? res.error ?? "failed")}`);
|
|
23177
|
+
return;
|
|
23178
|
+
}
|
|
23179
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
23180
|
+
} catch (e) {
|
|
23181
|
+
await failGraceful(e.message);
|
|
23182
|
+
}
|
|
23183
|
+
});
|
|
23184
|
+
schedules.command("unpark <schedule-id>").description("clear a deliberate hold on a SCHEDULE# row \u2014 reconciliation resumes normally (#4257)").action(async (scheduleId) => {
|
|
23185
|
+
try {
|
|
23186
|
+
const res = await postSchedulesUnpark(scheduleId, registryClientDeps(await loadConfig()));
|
|
23187
|
+
if (!res.ok) {
|
|
23188
|
+
await failGraceful(`schedules unpark: HTTP ${res.status} \u2014 ${JSON.stringify(res.body ?? res.error ?? "failed")}`);
|
|
23189
|
+
return;
|
|
23190
|
+
}
|
|
23191
|
+
console.log(JSON.stringify(res.body, null, 2));
|
|
23192
|
+
} catch (e) {
|
|
23193
|
+
await failGraceful(e.message);
|
|
23194
|
+
}
|
|
23195
|
+
});
|
|
23081
23196
|
schedules.command("mode <mode>").description("fleet schedules doctrine for a repo: 'managed' (default \u2014 Hub registry + drift enforcement) or 'self-managed' (opt out; the repo owns its crons). Project-admin self-serve (#3228).").option("--repo <name>", "bare repo name (defaults to the origin remote basename)").action(async (mode, o) => {
|
|
23082
23197
|
try {
|
|
23083
23198
|
if (mode !== "managed" && mode !== "self-managed") {
|
|
@@ -23861,6 +23976,16 @@ var import_node_crypto6 = require("node:crypto");
|
|
|
23861
23976
|
function byteComparableSeeds(manifest, cls) {
|
|
23862
23977
|
return manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self" && s.classes.includes(cls));
|
|
23863
23978
|
}
|
|
23979
|
+
function declaredRenderWaivers(manifest, repo) {
|
|
23980
|
+
const slug = repo.includes("/") ? repo.slice(repo.indexOf("/") + 1).toLowerCase() : repo.toLowerCase();
|
|
23981
|
+
const findings = [];
|
|
23982
|
+
for (const seed of manifest.seeds) {
|
|
23983
|
+
if (seed.ownership !== "org" || seed.source === "self") continue;
|
|
23984
|
+
const why = seed.waivers?.[slug];
|
|
23985
|
+
if (why) findings.push({ repo, target: seed.target, state: "waived", detail: `hand-authored, not rendered \u2014 waived: ${why}` });
|
|
23986
|
+
}
|
|
23987
|
+
return findings;
|
|
23988
|
+
}
|
|
23864
23989
|
function compareSeedBytes(hubContent, repoContent) {
|
|
23865
23990
|
if (repoContent === null) return "absent";
|
|
23866
23991
|
const normalize = (s) => s.replace(/\r\n/g, "\n");
|
|
@@ -23938,18 +24063,21 @@ function assignWaves(repos, canarySlug) {
|
|
|
23938
24063
|
rest.forEach((r, i) => waves.set(r.repo, i < wave1Count ? 1 : 2));
|
|
23939
24064
|
return waves;
|
|
23940
24065
|
}
|
|
23941
|
-
function statusFor(read) {
|
|
24066
|
+
function statusFor(read, currentSha) {
|
|
23942
24067
|
if (!read) return { status: "pending", record: {} };
|
|
23943
24068
|
if (read.drift === "match") return { status: "match", record: { mergeSha: read.pr?.mergeSha } };
|
|
23944
24069
|
const pr2 = read.pr;
|
|
23945
24070
|
if (!pr2) return { status: "pending", record: {} };
|
|
23946
24071
|
if (pr2.state === "closed") return { status: "closed-unmerged", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
24072
|
+
if (pr2.state === "merged" && pr2.sourceSha && pr2.sourceSha !== currentSha) {
|
|
24073
|
+
return { status: "pending", record: {} };
|
|
24074
|
+
}
|
|
23947
24075
|
if (pr2.checks === "red") return { status: "red", record: { prNumber: pr2.number, prUrl: pr2.url } };
|
|
23948
24076
|
return { status: "open-pending", record: { prNumber: pr2.number, prUrl: pr2.url, mergeSha: pr2.mergeSha } };
|
|
23949
24077
|
}
|
|
23950
24078
|
var HALTING_STATUSES = /* @__PURE__ */ new Set(["red", "closed-unmerged"]);
|
|
23951
24079
|
function planPropagationTick(input) {
|
|
23952
|
-
const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied } = input;
|
|
24080
|
+
const { target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied, currentSha } = input;
|
|
23953
24081
|
const readByRepo = new Map(reads.map((r) => [r.repo, r]));
|
|
23954
24082
|
const records = [];
|
|
23955
24083
|
const opened = [];
|
|
@@ -23983,7 +24111,7 @@ function planPropagationTick(input) {
|
|
|
23983
24111
|
let waveAllMatch = true;
|
|
23984
24112
|
let waveHasRed = false;
|
|
23985
24113
|
for (const r of waveRepos) {
|
|
23986
|
-
const { status, record } = statusFor(readByRepo.get(r.repo));
|
|
24114
|
+
const { status, record } = statusFor(readByRepo.get(r.repo), currentSha);
|
|
23987
24115
|
const shouldOpen = status === "pending";
|
|
23988
24116
|
if (shouldOpen) opened.push(r.repo);
|
|
23989
24117
|
records.push({
|
|
@@ -24725,6 +24853,7 @@ async function reconcileOrgNoAgentFilesRuleset(plan, client, org = ORG_LOGIN) {
|
|
|
24725
24853
|
}
|
|
24726
24854
|
|
|
24727
24855
|
// src/bootstrap-commands.ts
|
|
24856
|
+
var execGitForSeedSource = async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
24728
24857
|
function registerBootstrapCommands(program3) {
|
|
24729
24858
|
const bootstrap = program3.command("bootstrap").description("plan repo bootstrap operations; mutations require master-admin approval").option("--repo <owner/repo>", "target repo").addOption(new Option("--class <class>", "deployable | content").default("deployable").choices(["deployable", "content"])).option("--json", "machine-readable output").action((o) => {
|
|
24730
24859
|
if (!o.repo) return fail("bootstrap: required option --repo <owner/repo> not specified");
|
|
@@ -24804,6 +24933,8 @@ function registerBootstrapCommands(program3) {
|
|
|
24804
24933
|
const o = { repo: rawValue("--repo", ""), json: rawFlag("--json") };
|
|
24805
24934
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
24806
24935
|
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap drift: ${manifestPath} not found; run from the MMI-Hub repo root \u2014 the Hub's copies ARE the reference this compares against`);
|
|
24936
|
+
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
24937
|
+
if (!seedSource.ok) return fail(`bootstrap drift: ${seedSource.reason}`);
|
|
24807
24938
|
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
24808
24939
|
const hubContents = /* @__PURE__ */ new Map();
|
|
24809
24940
|
for (const s of manifest.seeds) {
|
|
@@ -24846,12 +24977,14 @@ function registerBootstrapCommands(program3) {
|
|
|
24846
24977
|
reads.push({ target: seed.target, content });
|
|
24847
24978
|
}
|
|
24848
24979
|
findings.push(...auditRepoSeedDrift(repo, seeds, hubContents, reads));
|
|
24980
|
+
findings.push(...declaredRenderWaivers(manifest, repo));
|
|
24849
24981
|
}
|
|
24850
24982
|
if (o.json) {
|
|
24851
24983
|
console.log(JSON.stringify({
|
|
24852
24984
|
// #3842: `ok` reflects real findings; waivers ride the payload so a consumer can see every
|
|
24853
24985
|
// standing exception without them counting as drift.
|
|
24854
24986
|
ok: findings.every((f) => f.state === "waived"),
|
|
24987
|
+
sourceSha: seedSource.sha,
|
|
24855
24988
|
scope: o.repo ? "single-repo" : "fleet",
|
|
24856
24989
|
reposAudited: targets.length,
|
|
24857
24990
|
seedsPerRepo,
|
|
@@ -24886,6 +25019,8 @@ function registerBootstrapCommands(program3) {
|
|
|
24886
25019
|
}
|
|
24887
25020
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
24888
25021
|
if (!(0, import_node_fs31.existsSync)(manifestPath)) return fail(`bootstrap apply: ${manifestPath} not found; bootstrap runs from the MMI-Hub repo root by design \u2014 it stamps org-level resources (Project, Ruleset, secrets, access) through the GitHub App, which is only authorized from the Hub checkout`);
|
|
25022
|
+
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
25023
|
+
if (!seedSource.ok) return fail(`bootstrap apply: ${seedSource.reason}`);
|
|
24889
25024
|
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
24890
25025
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
24891
25026
|
const slug = parsedRepo.slug;
|
|
@@ -25062,9 +25197,9 @@ function registerBootstrapCommands(program3) {
|
|
|
25062
25197
|
"--body",
|
|
25063
25198
|
onlyTarget ? `Auto-opened by \`mmi-cli bootstrap apply ${repo} --only ${onlyTarget} --execute\` (#3818).
|
|
25064
25199
|
|
|
25065
|
-
\`${onlyTarget}\` is an org-owned seed declared in MMI-Hub's \`skills/bootstrap/seeds/manifest.json
|
|
25200
|
+
\`${onlyTarget}\` is an org-owned seed declared in MMI-Hub's \`skills/bootstrap/seeds/manifest.json\` at MMI-Hub@${seedSource.sha}; this PR brings this repo's copy to the Hub's. It carries that file and nothing else \u2014 no labels, ruleset, merge settings or registry META were touched.
|
|
25066
25201
|
|
|
25067
|
-
\`${baseBranch}\` is protected (${seedPlan.reason}), so delivery goes via this branch + PR \u2014 a direct contents PUT 409s on a protected base.` : `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected").`
|
|
25202
|
+
\`${baseBranch}\` is protected (${seedPlan.reason}), so delivery goes via this branch + PR \u2014 a direct contents PUT 409s on a protected base.` : `Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected"). Seeds are from MMI-Hub@${seedSource.sha}.`
|
|
25068
25203
|
]);
|
|
25069
25204
|
seedPrUrl = created.url;
|
|
25070
25205
|
}
|
|
@@ -25160,7 +25295,7 @@ function registerBootstrapCommands(program3) {
|
|
|
25160
25295
|
applied.push(`ddb register ${registerPayload.slug} (failed: ${why})`);
|
|
25161
25296
|
}
|
|
25162
25297
|
}
|
|
25163
|
-
if (o.json) console.log(JSON.stringify({ repo, class: o.class, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
|
|
25298
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, sourceSha: seedSource.sha, only: onlyTarget || null, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites }, null, 2));
|
|
25164
25299
|
else {
|
|
25165
25300
|
console.log(renderSeedPlan(actions));
|
|
25166
25301
|
if (o.execute) console.log(`
|
|
@@ -25172,6 +25307,8 @@ LIVE apply to ${repo}:
|
|
|
25172
25307
|
const o = { target: rawValue("--target", ""), execute: rawFlag("--execute"), json: rawFlag("--json") };
|
|
25173
25308
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
25174
25309
|
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`);
|
|
25310
|
+
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
25311
|
+
if (!seedSource.ok) return fail(`bootstrap propagate: ${seedSource.reason}`);
|
|
25175
25312
|
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
25176
25313
|
const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
|
|
25177
25314
|
if (!o.target) {
|
|
@@ -25210,9 +25347,28 @@ LIVE apply to ${repo}:
|
|
|
25210
25347
|
}
|
|
25211
25348
|
const bySlugMeta = new Map(projects.flatMap((p) => (p.repos ?? []).map((r) => [(r.includes("/") ? r : `mutmutco/${r}`).toLowerCase(), p])));
|
|
25212
25349
|
const classOf = (repo) => bySlugMeta.get(repo.toLowerCase())?.class ?? "deployable";
|
|
25213
|
-
const
|
|
25214
|
-
const
|
|
25215
|
-
|
|
25350
|
+
const inClassRepos = rosterRepos2.filter((repo) => seed.classes.includes(classOf(repo)));
|
|
25351
|
+
const classPriority = [.../* @__PURE__ */ new Set(["deployable", ...seed.classes])].filter((c) => seed.classes.includes(c));
|
|
25352
|
+
let canaryProject = null;
|
|
25353
|
+
for (const cls of classPriority) {
|
|
25354
|
+
const inClass = projects.filter((p) => p.seedCanary === true && (p.class ?? "deployable") === cls);
|
|
25355
|
+
if (inClass.length === 0) continue;
|
|
25356
|
+
if (inClass.length > 1) {
|
|
25357
|
+
const slugs = inClass.map((p) => (p.repos ?? [])[0]?.split("/").pop() ?? "?").join(", ");
|
|
25358
|
+
return fail(
|
|
25359
|
+
`bootstrap propagate: target '${seed.target}' (classes [${seed.classes.join(", ")}]) matches ${inClass.length} seedCanary:true repos in class '${cls}' (${slugs}) \u2014 exactly one seedCanary:true repo per class is required`
|
|
25360
|
+
);
|
|
25361
|
+
}
|
|
25362
|
+
canaryProject = inClass[0];
|
|
25363
|
+
break;
|
|
25364
|
+
}
|
|
25365
|
+
if (!canaryProject) {
|
|
25366
|
+
return fail(
|
|
25367
|
+
`bootstrap propagate: target '${seed.target}' needs classes [${seed.classes.join(", ")}] but no registry repo with seedCanary:true carries a matching class \u2014 set seedCanary:true on an in-class repo (mmi-cli org project set <slug> --var seedCanary=true)`
|
|
25368
|
+
);
|
|
25369
|
+
}
|
|
25370
|
+
const canarySlug = (canaryProject.repos ?? [])[0]?.split("/").pop()?.toLowerCase() ?? null;
|
|
25371
|
+
const repos = inClassRepos.map((repo) => {
|
|
25216
25372
|
const slug = repo.split("/").pop().toLowerCase();
|
|
25217
25373
|
const waiver = seed.waivers?.[slug];
|
|
25218
25374
|
return { repo, slug, waiver };
|
|
@@ -25236,13 +25392,14 @@ LIVE apply to ${repo}:
|
|
|
25236
25392
|
let pr2;
|
|
25237
25393
|
try {
|
|
25238
25394
|
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"]);
|
|
25395
|
+
const listed = await gh(["pr", "list", "--repo", r.repo, "--head", branch, "--base", baseBranch, "--state", "all", "--json", "number,url,state,statusCheckRollup,mergeCommit,body", "--limit", "1"]);
|
|
25240
25396
|
const arr = JSON.parse(listed.stdout || "[]");
|
|
25241
25397
|
const p = arr[0];
|
|
25242
25398
|
if (p) {
|
|
25243
25399
|
const rollup = p.statusCheckRollup ?? [];
|
|
25244
25400
|
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
|
-
|
|
25401
|
+
const sourceSha = p.body?.match(/Propagates MMI-Hub@([0-9a-fA-F]{7,40})/)?.[1];
|
|
25402
|
+
pr2 = { number: p.number, url: p.url, state: p.state === "MERGED" ? "merged" : p.state === "CLOSED" ? "closed" : "open", checks, mergeSha: p.mergeCommit?.oid, sourceSha };
|
|
25246
25403
|
}
|
|
25247
25404
|
} catch {
|
|
25248
25405
|
pr2 = void 0;
|
|
@@ -25264,10 +25421,10 @@ LIVE apply to ${repo}:
|
|
|
25264
25421
|
}
|
|
25265
25422
|
}
|
|
25266
25423
|
}
|
|
25267
|
-
const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied });
|
|
25424
|
+
const plan = planPropagationTick({ target: seed.target, repos, canarySlug, reads, isWorkflowSeed, functionGateSatisfied, currentSha: seedSource.sha });
|
|
25268
25425
|
if (o.execute) {
|
|
25269
25426
|
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 =
|
|
25427
|
+
const headSha = seedSource.sha;
|
|
25271
25428
|
for (const rec of plan.records) {
|
|
25272
25429
|
if (rec.action !== "open-pr") continue;
|
|
25273
25430
|
const repoEntry = repos.find((r) => r.repo === rec.repo);
|
|
@@ -25332,8 +25489,9 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
|
|
|
25332
25489
|
rec.prUrl = prUrl;
|
|
25333
25490
|
}
|
|
25334
25491
|
}
|
|
25335
|
-
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25336
|
-
else console.log(renderPropagationReport(plan)
|
|
25492
|
+
if (o.json) console.log(JSON.stringify({ ...plan, sourceSha: seedSource.sha }, null, 2));
|
|
25493
|
+
else console.log(`${renderPropagationReport(plan)}
|
|
25494
|
+
source: MMI-Hub@${seedSource.sha}`);
|
|
25337
25495
|
if (plan.halted) process.exitCode = 1;
|
|
25338
25496
|
});
|
|
25339
25497
|
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) => {
|
|
@@ -25353,6 +25511,8 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
|
|
|
25353
25511
|
}
|
|
25354
25512
|
const manifestPath = "skills/bootstrap/seeds/manifest.json";
|
|
25355
25513
|
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)`);
|
|
25514
|
+
const seedSource = await resolveHubSeedSource(execGitForSeedSource);
|
|
25515
|
+
if (!seedSource.ok) return fail(`bootstrap rollback: ${seedSource.reason}`);
|
|
25356
25516
|
const manifest = loadBootstrapSeeds((0, import_node_fs31.readFileSync)(manifestPath, "utf8"));
|
|
25357
25517
|
const propagatable = manifest.seeds.filter((s) => s.ownership === "org" && s.source === "self");
|
|
25358
25518
|
if (!o.target) {
|
|
@@ -25408,7 +25568,7 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
|
|
|
25408
25568
|
}
|
|
25409
25569
|
const plan = planRollback(repo, seed.target, slug, candidates);
|
|
25410
25570
|
if (!plan.resolution.found) {
|
|
25411
|
-
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25571
|
+
if (o.json) console.log(JSON.stringify({ ...plan, sourceSha: seedSource.sha }, null, 2));
|
|
25412
25572
|
else console.log(renderRollbackReport(plan));
|
|
25413
25573
|
return fail(`bootstrap rollback: ${plan.resolution.reason}`);
|
|
25414
25574
|
}
|
|
@@ -25463,8 +25623,9 @@ Rollback: \`mmi-cli bootstrap rollback ${rec.repo} --target ${seed.target} --exe
|
|
|
25463
25623
|
}
|
|
25464
25624
|
plan.prUrl = prUrl;
|
|
25465
25625
|
}
|
|
25466
|
-
if (o.json) console.log(JSON.stringify(plan, null, 2));
|
|
25467
|
-
else console.log(renderRollbackReport(plan)
|
|
25626
|
+
if (o.json) console.log(JSON.stringify({ ...plan, sourceSha: seedSource.sha }, null, 2));
|
|
25627
|
+
else console.log(`${renderRollbackReport(plan)}
|
|
25628
|
+
manifest verified against: MMI-Hub@${seedSource.sha}`);
|
|
25468
25629
|
});
|
|
25469
25630
|
}
|
|
25470
25631
|
|
|
@@ -29481,6 +29642,25 @@ function findNegatedClosings(text, closingNumbers) {
|
|
|
29481
29642
|
return forIssue.length > 0 && forIssue.every((m) => m.negated);
|
|
29482
29643
|
});
|
|
29483
29644
|
}
|
|
29645
|
+
var CROSS_REPO_TOKEN_RE = /\b([\w.-]+\/[\w.-]+)#(\d+)\b/g;
|
|
29646
|
+
function findCrossRepoAmbiguousClosings(text, closingNumbers, repo) {
|
|
29647
|
+
if (!closingNumbers.length) return [];
|
|
29648
|
+
const closing = new Set(closingNumbers);
|
|
29649
|
+
const flagged = /* @__PURE__ */ new Set();
|
|
29650
|
+
const repoLower = repo.toLowerCase();
|
|
29651
|
+
for (const match of text.matchAll(CROSS_REPO_TOKEN_RE)) {
|
|
29652
|
+
const n = Number(match[2]);
|
|
29653
|
+
if (!closing.has(n)) continue;
|
|
29654
|
+
if ((match[1] ?? "").toLowerCase() === repoLower) continue;
|
|
29655
|
+
flagged.add(n);
|
|
29656
|
+
}
|
|
29657
|
+
return [...flagged];
|
|
29658
|
+
}
|
|
29659
|
+
function crossRepoAmbiguousRefusalMessage(ambiguous, repo, context = "pr merge") {
|
|
29660
|
+
const named = ambiguous.map((n) => `${repo}#${n}`).join(", ");
|
|
29661
|
+
const first = `#${ambiguous[0] ?? "N"}`;
|
|
29662
|
+
return `${context}: REFUSED \u2014 GitHub will close ${named} on merge (a bare \`#N\` after a closing keyword always means the repo the PR lives in), but this body also mentions ${first} qualified against a DIFFERENT repo elsewhere \u2014 check whether that closing keyword was meant to carry an explicit owner/repo#N qualifier instead. Reword the closing reference with the full owner/repo#N form, or re-run with --force to merge anyway and let ${named} close.`;
|
|
29663
|
+
}
|
|
29484
29664
|
function commitMessagesText(commits) {
|
|
29485
29665
|
if (!Array.isArray(commits)) return "";
|
|
29486
29666
|
return commits.map((commit) => {
|
|
@@ -29489,7 +29669,7 @@ function commitMessagesText(commits) {
|
|
|
29489
29669
|
${typeof messageBody === "string" ? messageBody : ""}`;
|
|
29490
29670
|
}).join("\n");
|
|
29491
29671
|
}
|
|
29492
|
-
function parseClosingGuardInput(raw) {
|
|
29672
|
+
function parseClosingGuardInput(raw, repo) {
|
|
29493
29673
|
if (!raw || typeof raw !== "object") return void 0;
|
|
29494
29674
|
const { title, body, state, closingIssuesReferences, commits } = raw;
|
|
29495
29675
|
if (typeof state !== "string" || !Array.isArray(closingIssuesReferences)) return void 0;
|
|
@@ -29502,7 +29682,7 @@ function parseClosingGuardInput(raw) {
|
|
|
29502
29682
|
const commitClosing = [...new Set(findClosingMentions(commitMessagesText(commits)).map((m) => m.issue))];
|
|
29503
29683
|
const text = `${typeof title === "string" ? title : ""}
|
|
29504
29684
|
${typeof body === "string" ? body : ""}`;
|
|
29505
|
-
return { state, text, closing, commitClosing };
|
|
29685
|
+
return { state, text, closing, commitClosing, repo };
|
|
29506
29686
|
}
|
|
29507
29687
|
function negatedClosingRefusalMessage(negated, context = "pr merge") {
|
|
29508
29688
|
const named = negated.map((n) => `#${n}`).join(", ");
|
|
@@ -29531,12 +29711,24 @@ function evaluateClosingGuard(input, opts) {
|
|
|
29531
29711
|
}
|
|
29532
29712
|
if (input.closing.length === 0) return { blocked: false };
|
|
29533
29713
|
const negated = findNegatedClosings(input.text, input.closing);
|
|
29534
|
-
if (
|
|
29535
|
-
|
|
29536
|
-
|
|
29537
|
-
|
|
29538
|
-
|
|
29539
|
-
|
|
29714
|
+
if (negated.length) {
|
|
29715
|
+
if (!opts.force) return { blocked: true, message: negatedClosingRefusalMessage(negated, opts.context) };
|
|
29716
|
+
return {
|
|
29717
|
+
blocked: false,
|
|
29718
|
+
message: `${opts.context}: --force past the negated-closing guard \u2014 GitHub will still close ${negated.map((n) => `#${n}`).join(", ")} on merge although the PR body says it does not.`
|
|
29719
|
+
};
|
|
29720
|
+
}
|
|
29721
|
+
if (input.repo) {
|
|
29722
|
+
const ambiguous = findCrossRepoAmbiguousClosings(input.text, input.closing, input.repo);
|
|
29723
|
+
if (ambiguous.length) {
|
|
29724
|
+
if (!opts.force) return { blocked: true, message: crossRepoAmbiguousRefusalMessage(ambiguous, input.repo, opts.context) };
|
|
29725
|
+
return {
|
|
29726
|
+
blocked: false,
|
|
29727
|
+
message: `${opts.context}: --force past the cross-repo-ambiguous closing guard \u2014 GitHub will still close ${ambiguous.map((n) => `${input.repo}#${n}`).join(", ")} on merge.`
|
|
29728
|
+
};
|
|
29729
|
+
}
|
|
29730
|
+
}
|
|
29731
|
+
return { blocked: false };
|
|
29540
29732
|
}
|
|
29541
29733
|
|
|
29542
29734
|
// src/session-report.ts
|
|
@@ -34455,12 +34647,13 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
|
|
|
34455
34647
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
34456
34648
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
34457
34649
|
});
|
|
34458
|
-
pr.command("land <number>").description("agent merge path (#1440): train probe ? checks-wait ? merge --auto ? poll enqueued ? development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--preserve-worktree", "after merge, keep the local PR worktree/stage/branch for an active batch (#1888)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword
|
|
34650
|
+
pr.command("land <number>").description("agent merge path (#1440): train probe ? checks-wait ? merge --auto ? poll enqueued ? development PRs only").option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the PR repo)").option("--no-require-train", "skip train-authority preflight (not recommended for autonomous agents)").option("--preserve-worktree", "after merge, keep the local PR worktree/stage/branch for an active batch (#1888)").option("--force", "acknowledge and land past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal").action(async (number, o) => {
|
|
34459
34651
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
34460
34652
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
34461
34653
|
assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
|
|
34462
34654
|
const landClosingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences,commits"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
|
|
34463
|
-
const
|
|
34655
|
+
const landRepoForGuard = await resolveRepo(o.repo).catch(() => void 0) ?? o.repo;
|
|
34656
|
+
const landClosingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(landClosingGuardRaw, landRepoForGuard), { force: o.force, context: "pr land" });
|
|
34464
34657
|
if (landClosingGuardVerdict.blocked) {
|
|
34465
34658
|
console.error(landClosingGuardVerdict.message);
|
|
34466
34659
|
process.exitCode = 1;
|
|
@@ -34571,14 +34764,14 @@ pr.command("land <number>").description("agent merge path (#1440): train probe ?
|
|
|
34571
34764
|
}
|
|
34572
34765
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
34573
34766
|
});
|
|
34574
|
-
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree ? no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge ? merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword
|
|
34767
|
+
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree ? no leftover local branch; on no-ci repos run pr ci-policy / checks-wait first (#1432)").option("--squash", "squash merge (default)").option("--merge", "create a merge commit").option("--rebase", "rebase merge").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--auto", "enable auto-merge ? merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "keep the local PR worktree/stage/branch for an active multi-issue batch (#1888)").option("--force", "acknowledge and merge past a remaining prunable/severe scratch housekeeping block (advisory kept plans/ never blocks, #3012) or a negated-closing-keyword (#3718) or ambiguous-cross-repo-closing (#4279) refusal")).action(async (number, o) => {
|
|
34575
34768
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
34576
34769
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
34577
34770
|
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
34578
34771
|
const [headRef, baseRef, headRefOid] = (await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "headRefName,baseRefName,headRefOid", "--jq", '.headRefName + " " + .baseRefName + " " + (.headRefOid // "")'], { timeout: GC_GH_TIMEOUT_MS2 })).stdout.trim().split(/\s+/);
|
|
34579
34772
|
const headIsProtected = isProtectedBranch(headRef);
|
|
34580
34773
|
const closingGuardRaw = await execFileP2("gh", ["pr", "view", number, ...repoArgs, "--json", "title,body,state,closingIssuesReferences,commits"], { timeout: GC_GH_TIMEOUT_MS2 }).then((r) => JSON.parse(r.stdout)).catch(() => void 0);
|
|
34581
|
-
const closingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(closingGuardRaw), { force: o.force, context: "pr merge" });
|
|
34774
|
+
const closingGuardVerdict = evaluateClosingGuard(parseClosingGuardInput(closingGuardRaw, repoForPostCleanup), { force: o.force, context: "pr merge" });
|
|
34582
34775
|
if (closingGuardVerdict.blocked) {
|
|
34583
34776
|
console.error(closingGuardVerdict.message);
|
|
34584
34777
|
process.exitCode = 1;
|
package/package.json
CHANGED