@mutmutco/cli 3.114.0 → 3.116.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 +220 -185
- package/package.json +1 -1
package/dist/main.cjs
CHANGED
|
@@ -7602,26 +7602,32 @@ function branchForTrackingRef(ref, remote) {
|
|
|
7602
7602
|
function sameWorktreeMetadataPath(a, b) {
|
|
7603
7603
|
return a.replace(/\\/g, "/").replace(/\/+$/, "") === b.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
7604
7604
|
}
|
|
7605
|
-
function resolveSafeSiblingWorktreeCleanupTarget(worktreePath,
|
|
7606
|
-
let rootReal;
|
|
7605
|
+
function resolveSafeSiblingWorktreeCleanupTarget(worktreePath, siblingRoots, deps) {
|
|
7607
7606
|
let worktreeReal;
|
|
7608
|
-
try {
|
|
7609
|
-
rootReal = deps.realpath(siblingRoot);
|
|
7610
|
-
} catch {
|
|
7611
|
-
return { ok: false, reason: "sibling root could not be resolved" };
|
|
7612
|
-
}
|
|
7613
|
-
if (!isPathUnderDirectory2(rootReal, siblingRoot) || !isPathUnderDirectory2(siblingRoot, rootReal)) {
|
|
7614
|
-
return { ok: false, reason: "sibling root resolves outside expected path" };
|
|
7615
|
-
}
|
|
7616
7607
|
try {
|
|
7617
7608
|
worktreeReal = deps.realpath(worktreePath);
|
|
7618
7609
|
} catch {
|
|
7619
7610
|
return { ok: false, reason: "worktree path could not be resolved" };
|
|
7620
7611
|
}
|
|
7621
|
-
|
|
7622
|
-
|
|
7612
|
+
const reasons = [];
|
|
7613
|
+
for (const siblingRoot of siblingRoots) {
|
|
7614
|
+
let rootReal;
|
|
7615
|
+
try {
|
|
7616
|
+
rootReal = deps.realpath(siblingRoot);
|
|
7617
|
+
} catch {
|
|
7618
|
+
reasons.push(`${siblingRoot}: root could not be resolved`);
|
|
7619
|
+
continue;
|
|
7620
|
+
}
|
|
7621
|
+
if (!isPathUnderDirectory2(rootReal, siblingRoot) || !isPathUnderDirectory2(siblingRoot, rootReal)) {
|
|
7622
|
+
reasons.push(`${siblingRoot}: root resolves outside expected path`);
|
|
7623
|
+
continue;
|
|
7624
|
+
}
|
|
7625
|
+
if (isPathUnderDirectory2(worktreeReal, rootReal)) {
|
|
7626
|
+
return { ok: true, path: worktreePath };
|
|
7627
|
+
}
|
|
7628
|
+
reasons.push(`${siblingRoot}: resolved worktree path outside this root`);
|
|
7623
7629
|
}
|
|
7624
|
-
return { ok:
|
|
7630
|
+
return { ok: false, reason: reasons.join("; ") || "no worktrees root to check against" };
|
|
7625
7631
|
}
|
|
7626
7632
|
function siblingMmiWorktreesRoot(repoRoot2) {
|
|
7627
7633
|
const parent = (0, import_node_path9.dirname)(repoRoot2);
|
|
@@ -7630,6 +7636,9 @@ function siblingMmiWorktreesRoot(repoRoot2) {
|
|
|
7630
7636
|
if ((0, import_node_path9.basename)(grandparent).toLowerCase() === "mmi-worktrees") return grandparent;
|
|
7631
7637
|
return (0, import_node_path9.join)(parent, "mmi-worktrees");
|
|
7632
7638
|
}
|
|
7639
|
+
function agentWorktreesRoot(repoRoot2) {
|
|
7640
|
+
return (0, import_node_path9.join)(repoRoot2, ".claude", "worktrees");
|
|
7641
|
+
}
|
|
7633
7642
|
function worktreeScanDirs(root, repoRoot2, listDirs, isRepoCheckout) {
|
|
7634
7643
|
const projectsDir = (0, import_node_path9.dirname)(root);
|
|
7635
7644
|
const ownName = (0, import_node_path9.basename)(repoRoot2).toLowerCase();
|
|
@@ -15419,7 +15428,8 @@ async function siblingWorktreeDirs(explicitRoot) {
|
|
|
15419
15428
|
const primaryRepoRoot = worktreeGitRoot ? (0, import_node_path18.dirname)((0, import_node_path18.dirname)(worktreeGitRoot)) : repoRoot2;
|
|
15420
15429
|
try {
|
|
15421
15430
|
const dirs = explicitRoot ? listDirsIn(resolveExplicitScanRoot(explicitRoot, primaryRepoRoot)) : worktreeScanDirs(siblingMmiWorktreesRoot(primaryRepoRoot), primaryRepoRoot, listDirsIn, isRepoCheckoutDir);
|
|
15422
|
-
|
|
15431
|
+
const agentDirs2 = listDirsIn(agentWorktreesRoot(primaryRepoRoot));
|
|
15432
|
+
return [...dirs, ...agentDirs2].map((dir) => inspectSiblingWorktreeDir(dir, worktreeGitRoot)).filter((entry) => Boolean(entry));
|
|
15423
15433
|
} catch {
|
|
15424
15434
|
return [];
|
|
15425
15435
|
}
|
|
@@ -15769,10 +15779,10 @@ var rollout_plan_default = {
|
|
|
15769
15779
|
note: "The v4.0.0 stamp happens at cut time (D6e #4463); until then the candidate is the origin/development head artifacts (built cli/dist + npm pack), identity proven by dist content hash (D6a)."
|
|
15770
15780
|
},
|
|
15771
15781
|
baseline: {
|
|
15772
|
-
version: "3.
|
|
15773
|
-
tag: "v3.
|
|
15774
|
-
commit: "
|
|
15775
|
-
npm: "@mutmutco/cli@3.
|
|
15782
|
+
version: "3.116.0",
|
|
15783
|
+
tag: "v3.116.0",
|
|
15784
|
+
commit: "639a07a118e9",
|
|
15785
|
+
npm: "@mutmutco/cli@3.116.0"
|
|
15776
15786
|
},
|
|
15777
15787
|
exitCriterion: "fleet-n-of-n",
|
|
15778
15788
|
hubOnlyShortcut: "forbidden",
|
|
@@ -15789,14 +15799,14 @@ var rollout_plan_default = {
|
|
|
15789
15799
|
repo: "mutmutco/mmi-hub",
|
|
15790
15800
|
role: "canary",
|
|
15791
15801
|
schedule: "train",
|
|
15792
|
-
v3Target: "v3.
|
|
15802
|
+
v3Target: "v3.116.0"
|
|
15793
15803
|
}
|
|
15794
15804
|
],
|
|
15795
15805
|
rollbackTrigger: "Any red inside the post-cut soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a v3 client refused while the compat window must still admit it (SUPPORTED_MINOR_WINDOW=2, MIN_CLIENT_VERSION 0.0.0 \u2014 D6a), or npm consumer install/doctor failure on the v4 dist.",
|
|
15796
15806
|
rollback: {
|
|
15797
15807
|
independent: true,
|
|
15798
|
-
mechanism: "npm dist-tag latest -> 3.
|
|
15799
|
-
v3Target: "v3.
|
|
15808
|
+
mechanism: "npm dist-tag latest -> 3.116.0 and redeploy the Hub Lambda from tag v3.116.0 (639a07a118e9); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
15809
|
+
v3Target: "v3.116.0 (@mutmutco/cli@3.116.0, tag commit 639a07a118e9 \u2014 the preserved latest-v3 distribution, D6b)"
|
|
15800
15810
|
}
|
|
15801
15811
|
},
|
|
15802
15812
|
{
|
|
@@ -32936,12 +32946,15 @@ async function applyGcPlan(plan, remote, opts = {}) {
|
|
|
32936
32946
|
result.removedTrackingRefs.push(...branchTracking.removedTrackingRefs);
|
|
32937
32947
|
result.failed.push(...branchTracking.failed);
|
|
32938
32948
|
const removeDeps = worktreeRemoveDeps(async (args) => (await execFileP2("git", args, { timeout: GIT_TIMEOUT_MS })).stdout);
|
|
32939
|
-
const
|
|
32949
|
+
const cleanupRoots = [
|
|
32950
|
+
opts.root ? resolveExplicitScanRoot(opts.root, primaryRepoRoot) : siblingMmiWorktreesRoot(primaryRepoRoot),
|
|
32951
|
+
agentWorktreesRoot(primaryRepoRoot)
|
|
32952
|
+
];
|
|
32940
32953
|
for (const wt of worktreeDirsToRemove) {
|
|
32941
32954
|
const owner = findWorktreeOwner(owners, wt.path);
|
|
32942
32955
|
let removalAttempted = false;
|
|
32943
32956
|
try {
|
|
32944
|
-
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path,
|
|
32957
|
+
const cleanupTarget = resolveSafeSiblingWorktreeCleanupTarget(wt.path, cleanupRoots, {
|
|
32945
32958
|
realpath: (path2) => (0, import_node_fs38.realpathSync)(path2)
|
|
32946
32959
|
});
|
|
32947
32960
|
if (!cleanupTarget.ok) {
|
|
@@ -36820,6 +36833,16 @@ function defaultRegistrationHeal(home, env) {
|
|
|
36820
36833
|
|
|
36821
36834
|
// src/doctor-render.ts
|
|
36822
36835
|
var RESTART_LINE = "\u21BB Restart Claude to finish.";
|
|
36836
|
+
var HOST_RESTART_LINES = {
|
|
36837
|
+
codex: "\u21BB Restart Codex to finish.",
|
|
36838
|
+
kimi: "\u21BB Restart Kimi to finish.",
|
|
36839
|
+
kilo: "\u21BB Restart Kilo to finish.",
|
|
36840
|
+
cursor: "\u21BB Restart Cursor to finish.",
|
|
36841
|
+
jervcode: "\u21BB Restart JervCode to finish."
|
|
36842
|
+
};
|
|
36843
|
+
function restartLineForHost(host) {
|
|
36844
|
+
return host && HOST_RESTART_LINES[host] || RESTART_LINE;
|
|
36845
|
+
}
|
|
36823
36846
|
var VERBOSE_INDENT = " ";
|
|
36824
36847
|
function checkGlyph(check) {
|
|
36825
36848
|
if (!check.ok) return "\u2717";
|
|
@@ -36835,7 +36858,7 @@ function renderCheckLine(check) {
|
|
|
36835
36858
|
}
|
|
36836
36859
|
function renderReport(checks, opts) {
|
|
36837
36860
|
const lines = checks.map(renderCheckLine);
|
|
36838
|
-
if (opts.restartPending) lines.push(
|
|
36861
|
+
if (opts.restartPending) lines.push(restartLineForHost(opts.host));
|
|
36839
36862
|
return lines.join("\n");
|
|
36840
36863
|
}
|
|
36841
36864
|
function renderTally(checks) {
|
|
@@ -39195,9 +39218,9 @@ async function withEnvHealLock(what, run) {
|
|
|
39195
39218
|
);
|
|
39196
39219
|
} catch (e) {
|
|
39197
39220
|
if (e instanceof FileLockBusyError) {
|
|
39198
|
-
return { ok: false, skipped: true, detail: `${what} skipped
|
|
39221
|
+
return { ok: false, skipped: true, detail: `${what} skipped \u2014 ${e.message}` };
|
|
39199
39222
|
}
|
|
39200
|
-
return { ok: false, detail: `${what} could not take the env-heal lock
|
|
39223
|
+
return { ok: false, detail: `${what} could not take the env-heal lock \u2014 ${e.message}` };
|
|
39201
39224
|
}
|
|
39202
39225
|
}
|
|
39203
39226
|
function throttledReleasedVersion() {
|
|
@@ -39253,13 +39276,13 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39253
39276
|
pluginTrustState: () => codexHookTrustState(),
|
|
39254
39277
|
releasedVersion: throttled ? throttled.read : fetchNpmReleasedVersion,
|
|
39255
39278
|
releasedVersionNote: throttled ? throttled.note : void 0,
|
|
39256
|
-
// #3272: the --apply self-heal for a stale running CLI
|
|
39279
|
+
// #3272: the --apply self-heal for a stale running CLI — npm global, shadows any plugin shim (#2879).
|
|
39257
39280
|
// #3489: serialised. Both heals mutate machine-global state (the npm global prefix; the Claude
|
|
39258
39281
|
// marketplace clone + plugin cache) and neither is idempotent under concurrency. They share ONE lock
|
|
39259
39282
|
// rather than one each, because they are not independent: the plugin heal reinstalls a bundle that
|
|
39260
39283
|
// carries a CLI shim, so a concurrent npm global install races the same PATH surface.
|
|
39261
39284
|
updateCli: (target, onStep) => withEnvHealLock("npm global CLI self-update", () => npmSelfUpdateCli(target, onStep)),
|
|
39262
|
-
// #3282: the --apply self-heal for a stale/unresolved Claude plugin
|
|
39285
|
+
// #3282: the --apply self-heal for a stale/unresolved Claude plugin — the same marketplace reinstall
|
|
39263
39286
|
// `mmi-cli plugin heal` drives. Takes effect on the next Claude reload, so the row asks for a restart.
|
|
39264
39287
|
healPlugin: (onStep) => {
|
|
39265
39288
|
const surface = detectSurface(process.env);
|
|
@@ -39277,7 +39300,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39277
39300
|
// #2759: same fetch+ff-only sync SessionStart runs, wired here too so an on-demand `mmi-cli doctor`
|
|
39278
39301
|
// self-heals a checkout a long-running session left stale.
|
|
39279
39302
|
syncTrain: () => syncLocalTrainBranches(execFileGitRun),
|
|
39280
|
-
// #2903: doctor DETECTS stale cached plugin versions and defers the delete to `plugin-prune`
|
|
39303
|
+
// #2903: doctor DETECTS stale cached plugin versions and defers the delete to `plugin-prune` — it never
|
|
39281
39304
|
// writes to the harness-owned cache itself. Same plan builder the verb uses, so the two never disagree.
|
|
39282
39305
|
// No `withBytes` here: doctor runs on EVERY SessionStart, and sizing means recursively stat'ing every
|
|
39283
39306
|
// stale version tree. The count comes from one cheap readdir; `plugin-prune` reports the MB.
|
|
@@ -39361,9 +39384,9 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39361
39384
|
// binary is exactly the half-applied state being repaired.
|
|
39362
39385
|
healClaudeBinary: (onStep) => withEnvHealLock("claude binary restore", async () => healClaudeBinary({}, onStep)),
|
|
39363
39386
|
// #3470: what the SessionStart hook LAST emitted, measured at emission by the session-start verb below.
|
|
39364
|
-
// A local record read
|
|
39387
|
+
// A local record read — cheap enough for every lane, including the banner.
|
|
39365
39388
|
sessionPayload: () => readSessionPayload(process.cwd()),
|
|
39366
|
-
// #3485 items 9 and 8: why the MMI plugin has never printed an "updated
|
|
39389
|
+
// #3485 items 9 and 8: why the MMI plugin has never printed an "updated — please restart" notice, and
|
|
39367
39390
|
// which branch it would pick one up from. Two local file reads, no network, fail-soft to no rows.
|
|
39368
39391
|
marketplaceRows: (hostBlocked) => {
|
|
39369
39392
|
try {
|
|
@@ -39420,13 +39443,13 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39420
39443
|
}
|
|
39421
39444
|
},
|
|
39422
39445
|
// #4091: the same `docs index --check` comparison the required CI gate runs, in-process. Rooted at the
|
|
39423
|
-
// repo root doctor already resolved
|
|
39446
|
+
// repo root doctor already resolved — never `process.cwd()`, which would have `mmi-cli doctor` run from
|
|
39424
39447
|
// `cli/` measure a `cli/docs/` tree that does not exist.
|
|
39425
39448
|
//
|
|
39426
39449
|
// `existsSync` on `docs/index.md` is the adoption gate, and it sits UNDER the table's org-repo gate
|
|
39427
39450
|
// rather than replacing it: a missing index is drift by construction, so without this a repo that never
|
|
39428
39451
|
// adopted the generated routing index (MMG-Unlive: `docs/Archive/**` only, no index, no gate.yml) would
|
|
39429
|
-
// get a permanent
|
|
39452
|
+
// get a permanent — demanding an artifact it never asked for.
|
|
39430
39453
|
docsIndexState: (root) => {
|
|
39431
39454
|
if (!(0, import_node_fs46.existsSync)((0, import_node_path43.join)(root, DOCS_INDEX_PATH))) return void 0;
|
|
39432
39455
|
const real = createDocsIndexDeps(root);
|
|
@@ -39514,7 +39537,7 @@ function mmiDoctorDeps(opts = {}) {
|
|
|
39514
39537
|
const names = new Set((opts.program?.commands ?? []).map((c) => c.name()));
|
|
39515
39538
|
return required.filter((n) => !names.has(n));
|
|
39516
39539
|
},
|
|
39517
|
-
// #4156: short-timeout Hub status for the current repo. Fail-soft
|
|
39540
|
+
// #4156: short-timeout Hub status for the current repo. Fail-soft — never throws to doctor.
|
|
39518
39541
|
repoIndexCloudState: async (root) => {
|
|
39519
39542
|
const local = repoIndexStatus(root);
|
|
39520
39543
|
try {
|
|
@@ -39574,7 +39597,7 @@ async function requireFreshTrainCli(commandName) {
|
|
|
39574
39597
|
const releasedVersion = await fetchNpmReleasedVersion();
|
|
39575
39598
|
if (!releasedVersion) {
|
|
39576
39599
|
throw new Error(
|
|
39577
|
-
`${commandName}: the train staleness gate could not read the released CLI version (\`npm view @mutmutco/cli version\` returned nothing
|
|
39600
|
+
`${commandName}: the train staleness gate could not read the released CLI version (\`npm view @mutmutco/cli version\` returned nothing \u2014 registry unreachable, npm missing, or output unparseable). That is a FAILED read, not "the running CLI is fresh", so this train command refuses rather than running unproven. This gate runs before any train mutation, so this invocation changed nothing. Retry once the registry answers, or set MMI_TRAIN_FRESH_OVERRIDE=1 to run this command with the staleness check deliberately skipped.`
|
|
39578
39601
|
);
|
|
39579
39602
|
}
|
|
39580
39603
|
const report = buildVersionLagReport({
|
|
@@ -39619,7 +39642,7 @@ function argvWantsJson2() {
|
|
|
39619
39642
|
var unknownFlagJsonHandled = false;
|
|
39620
39643
|
var PARSE_HINT_SENTINEL = "@@mmi-parse-hint@@";
|
|
39621
39644
|
var DISCOVERY_HINT = "run `mmi-cli commands` for the agentic-coding map, `mmi-cli explain <command>` for detail, or `mmi-cli commands --json --primary` for machine discovery";
|
|
39622
|
-
var STALE_HINT = `(${DISCOVERY_HINT}). If you expected this command to exist, your installed mmi-cli may be stale
|
|
39645
|
+
var STALE_HINT = `(${DISCOVERY_HINT}). If you expected this command to exist, your installed mmi-cli may be stale \u2014 update it (see: mmi-cli doctor)`;
|
|
39623
39646
|
var lastParseErrorKind = "other";
|
|
39624
39647
|
var lastUnknownCommand;
|
|
39625
39648
|
function classifyParseError(plain) {
|
|
@@ -39685,7 +39708,7 @@ function resolveParseHint() {
|
|
|
39685
39708
|
const path2 = commandPath2(cmd);
|
|
39686
39709
|
return [
|
|
39687
39710
|
`Usage: mmi-cli ${path2} ${cmd.usage()}`,
|
|
39688
|
-
"This command exists and your mmi-cli is not the problem
|
|
39711
|
+
"This command exists and your mmi-cli is not the problem \u2014 the ARGUMENTS did not parse.",
|
|
39689
39712
|
`Run \`mmi-cli ${path2} --help\` for its signature, \`mmi-cli explain ${path2}\` for detail, or \`mmi-cli commands\` for the full map.`
|
|
39690
39713
|
].join("\n");
|
|
39691
39714
|
}
|
|
@@ -39722,7 +39745,7 @@ function envelopeAwareWriteErr(str) {
|
|
|
39722
39745
|
const positional = TARGET_SELECTOR_FLAGS.has(flag) ? positionalTargetForm(invoked, { flag, argv }) : void 0;
|
|
39723
39746
|
if (argvWantsJson2()) {
|
|
39724
39747
|
const suggestion = positional ? void 0 : didYouMean(flag, commandOwnLongFlags(invoked));
|
|
39725
|
-
const corrected = suggestion ? `mmi-cli ${argv.map((a) => a === flag ? suggestion : a).join(" ")}` : void 0;
|
|
39748
|
+
const corrected = suggestion ? `mmi-cli ${canonicalArgvFor(argv.map((a) => a === flag ? suggestion : a)).join(" ")}` : void 0;
|
|
39726
39749
|
const message = positional ? unknownTargetFlagMessage(flag, positional) : `unknown option '${flag}'`;
|
|
39727
39750
|
process.stderr.write(
|
|
39728
39751
|
formatErrorEnvelope(message, {
|
|
@@ -39746,7 +39769,7 @@ function envelopeAwareWriteErr(str) {
|
|
|
39746
39769
|
process.stderr.write(str);
|
|
39747
39770
|
}
|
|
39748
39771
|
var program2 = new Command();
|
|
39749
|
-
program2.name("mmi-cli").description("MMI Future Hub CLI
|
|
39772
|
+
program2.name("mmi-cli").description("MMI Future Hub CLI \u2014 the org control plane for agentic coding.").version(resolveClientVersion()).configureOutput({ writeErr: envelopeAwareWriteErr }).showHelpAfterError(PARSE_HINT_SENTINEL);
|
|
39750
39773
|
program2.addHelpText(
|
|
39751
39774
|
"before",
|
|
39752
39775
|
`Houses (canonical roots): oracle \xB7 harbour \xB7 devops \xB7 vault \xB7 learning
|
|
@@ -39807,7 +39830,7 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
|
|
|
39807
39830
|
return;
|
|
39808
39831
|
}
|
|
39809
39832
|
if (plan.changed) {
|
|
39810
|
-
console.error(`mmi-cli devops org rules gitignore: managed block drift (${drift})
|
|
39833
|
+
console.error(`mmi-cli devops org rules gitignore: managed block drift (${drift}) \u2014 run \`mmi-cli devops org rules gitignore --write\` and commit`);
|
|
39811
39834
|
process.exitCode = 1;
|
|
39812
39835
|
} else {
|
|
39813
39836
|
console.log("mmi-cli devops org rules gitignore: up to date");
|
|
@@ -39913,7 +39936,7 @@ wave.command("land").description("serial merge open PRs to development with reba
|
|
|
39913
39936
|
});
|
|
39914
39937
|
var gcCmd = program2.command("gc").description("dry-run cleanup for merged/closed PR local+remote branches, linked worktrees, stale tracking refs, and worktree metadata (--scratch: safe local scratch + confirmed-synced plans)");
|
|
39915
39938
|
var DEFERRED_SWEEP_HARD_TIMEOUT_MS = 12e4;
|
|
39916
|
-
gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree removals with backoff (#1932)
|
|
39939
|
+
gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree removals with backoff (#1932) \u2014 SessionStart + pr land spawn this detached").option("--quiet", "silent when registry empty or all cleared").option("--json", "machine-readable output").action(async (o) => {
|
|
39917
39940
|
await runWithSweepWatchdog(async () => {
|
|
39918
39941
|
try {
|
|
39919
39942
|
const deferredStore = await createDeferredWorktreeStore();
|
|
@@ -39921,7 +39944,7 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
39921
39944
|
const result = await sweepDeferredWorktreesWithRetry(
|
|
39922
39945
|
deferredStore,
|
|
39923
39946
|
// #2841: `-c core.fsmonitor=false` stops a git fsmonitor daemon from inheriting this detached
|
|
39924
|
-
// worker's stdio pipe (the likely Windows hang
|
|
39947
|
+
// worker's stdio pipe (the likely Windows hang — execFile then never sees EOF and never resolves).
|
|
39925
39948
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
39926
39949
|
{ removalContext }
|
|
39927
39950
|
);
|
|
@@ -39937,11 +39960,11 @@ gcCmd.command("sweep-deferred").description("retry IDE-locked deferred worktree
|
|
|
39937
39960
|
fail(`worktree gc sweep-deferred: ${e.message}`);
|
|
39938
39961
|
}
|
|
39939
39962
|
}, DEFERRED_SWEEP_HARD_TIMEOUT_MS, () => {
|
|
39940
|
-
console.error("worktree gc sweep-deferred: exceeded hard timeout
|
|
39963
|
+
console.error("worktree gc sweep-deferred: exceeded hard timeout \u2014 exiting so the detached worker cannot leak (#2841)");
|
|
39941
39964
|
process.exit(process.exitCode ?? 0);
|
|
39942
39965
|
});
|
|
39943
39966
|
});
|
|
39944
|
-
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
|
|
39967
|
+
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 \u2014 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), or when a dead worktree directory still holds leftover files (#4779)").option("--root <path>", "sweep an explicitly named worktrees root instead of the authoritative ../mmi-worktrees (#3471) \u2014 descends into this repo container when present; ownership, dead-dir, and content guards still apply").action(async (o) => {
|
|
39945
39968
|
if (o.apply && o.dryRun) return fail("worktree gc: choose either --dry-run or --apply");
|
|
39946
39969
|
if (o.scratch) {
|
|
39947
39970
|
try {
|
|
@@ -39962,7 +39985,7 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
39962
39985
|
if (!(0, import_node_fs46.existsSync)(root) || !(0, import_node_fs46.statSync)(root).isDirectory()) return fail(`worktree gc: --root ${o.root} is not a directory`);
|
|
39963
39986
|
const gcRepoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
|
|
39964
39987
|
if (isPathUnderDirectory2(gcRepoRoot, root)) {
|
|
39965
|
-
return fail(`worktree gc: --root ${root} contains this checkout
|
|
39988
|
+
return fail(`worktree gc: --root ${root} contains this checkout \u2014 name a worktrees root, not the repo or an ancestor of it`);
|
|
39966
39989
|
}
|
|
39967
39990
|
}
|
|
39968
39991
|
try {
|
|
@@ -39976,7 +39999,7 @@ gcCmd.option("--dry-run", "show what would be deleted (default)").option("--appl
|
|
|
39976
39999
|
const removalContext = await currentWorktreeRemovalContext("worktree gc", o.force);
|
|
39977
40000
|
const sweepResult = await sweepDeferredWorktrees(
|
|
39978
40001
|
deferredStore,
|
|
39979
|
-
// #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker
|
|
40002
|
+
// #2841: same `-c core.fsmonitor=false` guard as the detached `gc sweep-deferred` worker — keep a
|
|
39980
40003
|
// git fsmonitor daemon from inheriting this sweep's stdio pipe and wedging the git call on Windows.
|
|
39981
40004
|
worktreeRemoveDeps(async (args) => (await execFileP2("git", ["-c", "core.fsmonitor=false", ...args], { timeout: GIT_TIMEOUT_MS })).stdout),
|
|
39982
40005
|
removalContext
|
|
@@ -40063,7 +40086,7 @@ async function unprovenWorktreeReason(wtPath, repoRoot2) {
|
|
|
40063
40086
|
return `'git worktree list' could not be read from ${repoRoot2}, so ${wtPath} could not be proven registered`;
|
|
40064
40087
|
}
|
|
40065
40088
|
if (!registered.some((w) => samePath(w.path, wtPath))) {
|
|
40066
|
-
return `${wtPath} exists on disk but 'git worktree list' does not name it
|
|
40089
|
+
return `${wtPath} exists on disk but 'git worktree list' does not name it \u2014 its git metadata is gone, so it is not a usable worktree`;
|
|
40067
40090
|
}
|
|
40068
40091
|
return void 0;
|
|
40069
40092
|
}
|
|
@@ -40113,15 +40136,15 @@ function acquireWorktreeSetupLock(worktreeRoot) {
|
|
|
40113
40136
|
return null;
|
|
40114
40137
|
}
|
|
40115
40138
|
}
|
|
40116
|
-
var worktree = program2.command("worktree").description("self-provisioning worktrees
|
|
40139
|
+
var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
|
|
40117
40140
|
withExamples(mutating(
|
|
40118
|
-
worktree.command("create <branch-or-issue>").description("create a worktree from a base ref and provision it (install deps + copy local-only config); an issue ref derives the <issue>-<slug> branch, and --claim claims its board item (#2996, formerly worktree new)").option("--from <ref>", "base ref to branch from", "origin/development").option("--base <ref>", "alias of --from (git's own word for the base ref)").option("--path <path>", "worktree path (default: ../mmi-worktrees/<RepoName>/<branch>
|
|
40141
|
+
worktree.command("create <branch-or-issue>").description("create a worktree from a base ref and provision it (install deps + copy local-only config); an issue ref derives the <issue>-<slug> branch, and --claim claims its board item (#2996, formerly worktree new)").option("--from <ref>", "base ref to branch from", "origin/development").option("--base <ref>", "alias of --from (git's own word for the base ref)").option("--path <path>", "worktree path (default: ../mmi-worktrees/<RepoName>/<branch> \u2014 authoritative; the <RepoName> segment is what makes ownership provable from the path, #3471. gc/list read this root in BOTH the nested and the legacy flat shape)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--slug <slug>", "issue-ref form: override the slug derived from the issue title").option("--claim", "issue-ref form: claim the issue's board item after provisioning").option("--for <login>", "with --claim: assign the board item to this login instead of @me"),
|
|
40119
40142
|
worktreeCreatePlan
|
|
40120
40143
|
).action(async (target, o, cmd) => {
|
|
40121
40144
|
let step = "resolve the branch name";
|
|
40122
40145
|
try {
|
|
40123
40146
|
if (o.base !== void 0 && cmd.getOptionValueSource("from") === "cli" && o.base !== o.from) {
|
|
40124
|
-
return fail(`worktree create: --from and --base are the same option (--base is an alias); got --from ${o.from} and --base ${o.base}
|
|
40147
|
+
return fail(`worktree create: --from and --base are the same option (--base is an alias); got --from ${o.from} and --base ${o.base} \u2014 pass one`);
|
|
40125
40148
|
}
|
|
40126
40149
|
const fromRef = o.base ?? o.from;
|
|
40127
40150
|
const issueForm = isIssueRef(target);
|
|
@@ -40139,7 +40162,7 @@ withExamples(mutating(
|
|
|
40139
40162
|
const slug = o.slug ?? await fetchIssueTitle(selector.repo, selector.number).then((t) => t ? slugifyIssueTitle(t) : "");
|
|
40140
40163
|
branch = buildNewBranchName(selector.number, slug ?? "");
|
|
40141
40164
|
if (PROTECTED_BRANCHES2.has(branch)) {
|
|
40142
|
-
return fail(`worktree create: generated branch name '${branch}' collides with a protected train branch
|
|
40165
|
+
return fail(`worktree create: generated branch name '${branch}' collides with a protected train branch \u2014 pass a branch name instead`);
|
|
40143
40166
|
}
|
|
40144
40167
|
} else if (o.claim || o.for) {
|
|
40145
40168
|
return fail("worktree create: --claim/--for need an issue-ref argument (e.g. worktree create 2687 --claim)");
|
|
@@ -40179,7 +40202,7 @@ withExamples(mutating(
|
|
|
40179
40202
|
return fail(`worktree create: ${wtPath} is already registered for '${exact.branch ?? "detached HEAD"}', not '${branch}'`);
|
|
40180
40203
|
}
|
|
40181
40204
|
const status = (await execFileP2("git", ["-C", wtPath, "status", "--porcelain"], { timeout: GIT_TIMEOUT_MS })).stdout.trim();
|
|
40182
|
-
if (status) return fail(`worktree create: refusing to resume ${wtPath}
|
|
40205
|
+
if (status) return fail(`worktree create: refusing to resume ${wtPath} \u2014 it has uncommitted changes`);
|
|
40183
40206
|
const head = await revParseRef(`refs/heads/${branch}`);
|
|
40184
40207
|
const baseOid = await revParseRef(base);
|
|
40185
40208
|
if (!head || !baseOid) return fail(`worktree create: could not prove '${branch}' and '${base}' before resume`);
|
|
@@ -40190,7 +40213,7 @@ withExamples(mutating(
|
|
|
40190
40213
|
// io-census-allow: an unprovable ancestor probe conservatively refuses the resume below rather than fast-forwarding onto unproven history
|
|
40191
40214
|
).then(() => true).catch(() => false);
|
|
40192
40215
|
if (!canFastForward) {
|
|
40193
|
-
return fail(`worktree create: refusing to resume '${branch}'
|
|
40216
|
+
return fail(`worktree create: refusing to resume '${branch}' \u2014 it has local commits or diverges from ${base}`);
|
|
40194
40217
|
}
|
|
40195
40218
|
await execFileP2("git", ["-C", wtPath, "merge", "--ff-only", base], { timeout: GIT_TIMEOUT_MS });
|
|
40196
40219
|
resumed = true;
|
|
@@ -40292,11 +40315,11 @@ withExamples(mutating(
|
|
|
40292
40315
|
console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
|
|
40293
40316
|
console.log(` copied: ${report.copied.join(", ") || "none"}`);
|
|
40294
40317
|
if (issueForm && o.claim && selector) {
|
|
40295
|
-
if (claimError) console.error(` warning: board claim failed (${claimError})
|
|
40318
|
+
if (claimError) console.error(` warning: board claim failed (${claimError}) \u2014 worktree is ready, finish the claim manually`);
|
|
40296
40319
|
else console.log(` board item ${selector.repo}#${selector.number} claimed`);
|
|
40297
40320
|
}
|
|
40298
40321
|
} catch (e) {
|
|
40299
|
-
fail(`worktree create: failed at step '${step}'
|
|
40322
|
+
fail(`worktree create: failed at step '${step}' \u2014 ${e.message}`);
|
|
40300
40323
|
}
|
|
40301
40324
|
}), [
|
|
40302
40325
|
"mmi-cli worktree create fix/gc-sweep-2687",
|
|
@@ -40307,7 +40330,7 @@ worktree.command("setup [path]").description("provision an existing worktree (in
|
|
|
40307
40330
|
const root = path2 ?? process.cwd();
|
|
40308
40331
|
const release = acquireWorktreeSetupLock(root);
|
|
40309
40332
|
if (!release) {
|
|
40310
|
-
if (!o.quiet && !o.json) console.log("worktree setup: another provision is in progress
|
|
40333
|
+
if (!o.quiet && !o.json) console.log("worktree setup: another provision is in progress \u2014 skipping");
|
|
40311
40334
|
return;
|
|
40312
40335
|
}
|
|
40313
40336
|
try {
|
|
@@ -40327,7 +40350,7 @@ worktree.command("setup [path]").description("provision an existing worktree (in
|
|
|
40327
40350
|
release();
|
|
40328
40351
|
}
|
|
40329
40352
|
});
|
|
40330
|
-
worktree.command("events").description("who created and who removed this repo's worktrees
|
|
40353
|
+
worktree.command("events").description("who created and who removed this repo's worktrees \u2014 the append-only attribution log (#3580)").option("--limit <n>", "most recent events to show", "50").option("--json", "machine-readable output").action(async (o) => {
|
|
40331
40354
|
const limit = Number.parseInt(o.limit, 10);
|
|
40332
40355
|
if (!Number.isFinite(limit) || limit < 1) return fail("worktree events: --limit must be a positive integer");
|
|
40333
40356
|
const primaryRoot = await primaryCheckoutRoot(process.cwd()) ?? process.cwd();
|
|
@@ -40345,11 +40368,11 @@ async function attachToProject(issueNumber, repo, priority) {
|
|
|
40345
40368
|
try {
|
|
40346
40369
|
cfg = await loadConfigForRepo(targetRepo2);
|
|
40347
40370
|
} catch (e) {
|
|
40348
|
-
console.error(`issue create: board attach skipped
|
|
40371
|
+
console.error(`issue create: board attach skipped \u2014 ${e.message}`);
|
|
40349
40372
|
return { onBoard: false };
|
|
40350
40373
|
}
|
|
40351
40374
|
if (!cfg.projectId) {
|
|
40352
|
-
console.error(`issue create: board attach skipped
|
|
40375
|
+
console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli oracle org project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
|
|
40353
40376
|
return { onBoard: false };
|
|
40354
40377
|
}
|
|
40355
40378
|
try {
|
|
@@ -40404,7 +40427,7 @@ registerEdgeCommands(program2);
|
|
|
40404
40427
|
registerBoxCommands(program2);
|
|
40405
40428
|
registerSchedulesCommands(program2);
|
|
40406
40429
|
registerSchedulesLiftCommand(program2);
|
|
40407
|
-
var repoIndex = program2.command("repo-index").description("Hub cloud + local pointer index
|
|
40430
|
+
var repoIndex = program2.command("repo-index").description("Hub cloud + local pointer index \u2014 paths/symbols only, never wiki prose (Hub#4133)");
|
|
40408
40431
|
repoIndex.command("rebuild").description("walk the checkout (git ls-files + gitignore/secrets walls), rebuild the local pointer index, drop orphans").option("--json", "machine-readable projection summary").action(async (o) => {
|
|
40409
40432
|
try {
|
|
40410
40433
|
const root = await repoRoot();
|
|
@@ -40422,7 +40445,7 @@ repoIndex.command("rebuild").description("walk the checkout (git ls-files + giti
|
|
|
40422
40445
|
}, null, 2));
|
|
40423
40446
|
return;
|
|
40424
40447
|
}
|
|
40425
|
-
console.log(`repo-index: rebuilt ${built.repo}
|
|
40448
|
+
console.log(`repo-index: rebuilt ${built.repo} \u2014 ${built.entries.length} files, ${symbolCount} symbols`);
|
|
40426
40449
|
} catch (e) {
|
|
40427
40450
|
await failGraceful(e.message);
|
|
40428
40451
|
}
|
|
@@ -40446,7 +40469,7 @@ repoIndex.command("publish").description("publish this checkout's projection to
|
|
|
40446
40469
|
const embRequested = Number(res.body.embRequested ?? 0);
|
|
40447
40470
|
const embCap = Number(res.body.embCap ?? 0);
|
|
40448
40471
|
const capNote = o.embed && embRequested > embCap && embCap > 0 ? ` (emb=${embCount} of ${embRequested} requested \u2014 capped at ${embCap}/publish, run \`--embed\` again to continue)` : "";
|
|
40449
|
-
console.log(`repo-index: published ${res.body.repo}
|
|
40472
|
+
console.log(`repo-index: published ${res.body.repo} \u2014 ${res.body.fileCount} files, emb=${embCount}${capNote}`);
|
|
40450
40473
|
} catch (e) {
|
|
40451
40474
|
return await failGraceful(e.message);
|
|
40452
40475
|
}
|
|
@@ -40522,7 +40545,7 @@ repoIndex.command("status").description("show local and/or cloud repo-index stat
|
|
|
40522
40545
|
const embGap = Number(st2.embGap ?? Math.max(0, fileCount - embCount));
|
|
40523
40546
|
const stale = st2.staleBuiltAt === true ? " stale-builtAt" : "";
|
|
40524
40547
|
console.log(
|
|
40525
|
-
`repo-index: cloud ${st2.repo} built ${st2.builtAt}
|
|
40548
|
+
`repo-index: cloud ${st2.repo} built ${st2.builtAt} \u2014 ${fileCount} files, emb=${embCount}/${fileCount} gap=${embGap}${stale}`
|
|
40526
40549
|
);
|
|
40527
40550
|
return;
|
|
40528
40551
|
}
|
|
@@ -40531,7 +40554,7 @@ repoIndex.command("status").description("show local and/or cloud repo-index stat
|
|
|
40531
40554
|
const present = st2.present;
|
|
40532
40555
|
if (count === 0 || present === false) {
|
|
40533
40556
|
console.error(
|
|
40534
|
-
"repo-index: no cloud projection yet
|
|
40557
|
+
"repo-index: no cloud projection yet \u2014 after Hub deploy, run harbour `repo-index-reconcile` or `mmi-cli oracle repo-index sync-estate` (see docs/Guides/repo-index-runbook.md)."
|
|
40535
40558
|
);
|
|
40536
40559
|
}
|
|
40537
40560
|
return;
|
|
@@ -40543,10 +40566,10 @@ repoIndex.command("status").description("show local and/or cloud repo-index stat
|
|
|
40543
40566
|
return;
|
|
40544
40567
|
}
|
|
40545
40568
|
if (!st.present) {
|
|
40546
|
-
console.log(`repo-index: local missing
|
|
40569
|
+
console.log(`repo-index: local missing \u2014 cloud search needs no rebuild; optional \`repo-index rebuild\` for --local`);
|
|
40547
40570
|
return;
|
|
40548
40571
|
}
|
|
40549
|
-
console.log(`repo-index: local ${st.repo} built ${st.builtAt}
|
|
40572
|
+
console.log(`repo-index: local ${st.repo} built ${st.builtAt} \u2014 ${st.fileCount} files, ${st.symbolCount} symbols`);
|
|
40550
40573
|
} catch (e) {
|
|
40551
40574
|
return await failGraceful(e.message);
|
|
40552
40575
|
}
|
|
@@ -40593,7 +40616,7 @@ repoIndex.command("health").description("post-deploy health gate: status + golde
|
|
|
40593
40616
|
return await failGraceful(e.message);
|
|
40594
40617
|
}
|
|
40595
40618
|
});
|
|
40596
|
-
repoIndex.command("gc").description("remove cloud projections for repos no longer on the registry roster").option("--cloud", "required
|
|
40619
|
+
repoIndex.command("gc").description("remove cloud projections for repos no longer on the registry roster").option("--cloud", "required \u2014 GC only applies to Hub cloud").option("--json", "machine-readable result").action(async (o) => {
|
|
40597
40620
|
try {
|
|
40598
40621
|
if (!o.cloud) return await failGraceful("repo-index gc requires --cloud");
|
|
40599
40622
|
const cfg = await loadConfig();
|
|
@@ -40627,7 +40650,7 @@ repoIndex.command("sync-estate").description("Hub indexer: shallow-clone registr
|
|
|
40627
40650
|
for (const p of res.published) {
|
|
40628
40651
|
const gap = p.embGap ?? Math.max(0, p.fileCount - (p.embCount ?? 0));
|
|
40629
40652
|
const trunc = p.embTruncated ? " truncated" : "";
|
|
40630
|
-
console.log(`repo-index: published ${p.repo}
|
|
40653
|
+
console.log(`repo-index: published ${p.repo} \u2014 ${p.fileCount} files emb=${p.embCount ?? 0}/${p.fileCount} gap=${gap}${trunc}`);
|
|
40631
40654
|
}
|
|
40632
40655
|
for (const f of res.failed) {
|
|
40633
40656
|
console.error(`repo-index: FAILED ${f.repo}: ${f.error}`);
|
|
@@ -40637,14 +40660,14 @@ repoIndex.command("sync-estate").description("Hub indexer: shallow-clone registr
|
|
|
40637
40660
|
await failGraceful(e.message);
|
|
40638
40661
|
}
|
|
40639
40662
|
});
|
|
40640
|
-
var docs = program2.command("docs").description("generated docs surfaces
|
|
40641
|
-
docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check)
|
|
40663
|
+
var docs = program2.command("docs").description("generated docs surfaces \u2014 the routing index (org knowledge layer)");
|
|
40664
|
+
docs.command("index").description("regenerate docs/index.md from the docs/ tree (--write, the default) or fail on drift (--check) \u2014 the generated routing index, never hand-maintained").option("--check", "compare against the committed docs/index.md and exit 1 on drift; never write").option("--write", "regenerate docs/index.md when it has drifted (the default)").action(async (o) => {
|
|
40642
40665
|
try {
|
|
40643
40666
|
const root = await repoRoot();
|
|
40644
40667
|
const result = docsIndex(createDocsIndexDeps(root), { check: Boolean(o.check) });
|
|
40645
40668
|
if (o.check) {
|
|
40646
40669
|
if (result.drift) {
|
|
40647
|
-
return failGraceful(`docs index: ${DOCS_INDEX_PATH} is stale
|
|
40670
|
+
return failGraceful(`docs index: ${DOCS_INDEX_PATH} is stale \u2014 run \`mmi-cli oracle docs index --write\` and commit the result`);
|
|
40648
40671
|
}
|
|
40649
40672
|
console.log(`docs index: ${DOCS_INDEX_PATH} is current`);
|
|
40650
40673
|
return;
|
|
@@ -40654,7 +40677,7 @@ docs.command("index").description("regenerate docs/index.md from the docs/ tree
|
|
|
40654
40677
|
await failGraceful(e.message);
|
|
40655
40678
|
}
|
|
40656
40679
|
});
|
|
40657
|
-
docs.command("refs").description("deterministic doc reference gate: every backticked repo path, relative .md link, `mmi-cli` command ref, and `<!-- pinned by
|
|
40680
|
+
docs.command("refs").description("deterministic doc reference gate: every backticked repo path, relative .md link, `mmi-cli` command ref, and `<!-- pinned by \u2014 -->` comment across docs/** + README.md + architecture.md must resolve, or exit 1 (fleet-portable port of the Hub gate scripts/check-doc-refs.mjs, #3339)").option("--json", "machine-readable findings list: { ok, docCount, findings[], warnings[] }").action(async (o) => {
|
|
40658
40681
|
try {
|
|
40659
40682
|
const root = await repoRoot();
|
|
40660
40683
|
const commandPaths = new Set(
|
|
@@ -40682,7 +40705,7 @@ docs.command("refs").description("deterministic doc reference gate: every backti
|
|
|
40682
40705
|
await failGraceful(e.message);
|
|
40683
40706
|
}
|
|
40684
40707
|
});
|
|
40685
|
-
var spawnCmd = program2.command("spawn").description("this repo's process-spawn contract
|
|
40708
|
+
var spawnCmd = program2.command("spawn").description("this repo's process-spawn contract \u2014 every child process must be unable to pop a console window");
|
|
40686
40709
|
spawnCmd.command("policy").description("enforce the windowsHide contract across this repo's tracked source: a child_process call must set windowsHide, or take its options from a named constant, a type annotation, or a forwarded caller bag that does. Waive a call the scan cannot classify with a `// windows-hide-exempt: <reason>` comment on the line above it (#3979)").option("--json", "machine-readable result: { ok, scannedCount, findings[] }").action(async (o) => {
|
|
40687
40710
|
try {
|
|
40688
40711
|
const root = await repoRoot();
|
|
@@ -40698,14 +40721,14 @@ spawnCmd.command("policy").description("enforce the windowsHide contract across
|
|
|
40698
40721
|
}
|
|
40699
40722
|
for (const f of result.findings) console.error(`spawn policy: ${f.detail}`);
|
|
40700
40723
|
console.error(
|
|
40701
|
-
"\nA process spawned without windowsHide allocates a console when it has none to inherit.\nWhere Windows Terminal is the default terminal, that console becomes a desktop window the\nhost never reaps
|
|
40724
|
+
"\nA process spawned without windowsHide allocates a console when it has none to inherit.\nWhere Windows Terminal is the default terminal, that console becomes a desktop window the\nhost never reaps \u2014 it outlives the process as an empty frame. Set windowsHide: true, or\nroute the call through a helper that does."
|
|
40702
40725
|
);
|
|
40703
40726
|
process.exitCode = 1;
|
|
40704
40727
|
} catch (e) {
|
|
40705
40728
|
await failGraceful(e.message);
|
|
40706
40729
|
}
|
|
40707
40730
|
});
|
|
40708
|
-
var tests = program2.command("tests").description("a repo's test-policy.json
|
|
40731
|
+
var tests = program2.command("tests").description("a repo's test-policy.json \u2014 the opt-in test contract and its enforcement");
|
|
40709
40732
|
tests.command("policy").description("enforce this repo's test-policy.json against the diff: a mandatory-zone change must carry a test, an unrequested new test file is refused, a `protected` test file may not be deleted or renamed away, and a `protected` entry naming a missing file is refused. Override any of them with a `Test-Policy-Override: <reason>` commit trailer (#3605)").option("--json", "machine-readable result: { ok, base, changedCount, findings[] }").option("--base <ref>", "comparison base (default: TEST_POLICY_BASE, then origin/development, then origin/main)").action(async (o) => {
|
|
40710
40733
|
try {
|
|
40711
40734
|
const root = await repoRoot();
|
|
@@ -40718,7 +40741,7 @@ tests.command("policy").description("enforce this repo's test-policy.json agains
|
|
|
40718
40741
|
if (result.overriddenBy && result.ok) {
|
|
40719
40742
|
const { sha, reason, kinds } = result.overriddenBy;
|
|
40720
40743
|
const scope = (result.waived ?? []).map((f) => f.kind).join(", ") || "nothing";
|
|
40721
|
-
console.log(`tests policy: overridden by ${sha.slice(0, 8)} (waiving ${scope}; scope ${kinds.join(", ")})
|
|
40744
|
+
console.log(`tests policy: overridden by ${sha.slice(0, 8)} (waiving ${scope}; scope ${kinds.join(", ")}) \u2014 ${reason}`);
|
|
40722
40745
|
return;
|
|
40723
40746
|
}
|
|
40724
40747
|
if (result.ok) {
|
|
@@ -40740,7 +40763,7 @@ async function reportWrite(label, res) {
|
|
|
40740
40763
|
}
|
|
40741
40764
|
if (res.error) return failGraceful(`${label}: ${res.error}`);
|
|
40742
40765
|
const detail = res.body?.error ?? "";
|
|
40743
|
-
return failGraceful(`${label}: HTTP ${res.status}${detail ? `
|
|
40766
|
+
return failGraceful(`${label}: HTTP ${res.status}${detail ? ` \u2014 ${detail}` : ""}`);
|
|
40744
40767
|
}
|
|
40745
40768
|
var tenant = program2.command("tenant").description("tenant runtime control through Hub authority");
|
|
40746
40769
|
tenant.command("control <owner/repo> <stage> <action>").description("bounded tenant control plus value-free vault/broker verification; project-admin own dev/rc, master main").option("--watch", "block on the dispatched run and report its conclusion (status/retire/verify-secrets/verify-broker/logs watch by default)").option("--lines <n>", "logs only: trailing lines of the tenant service to return (1-2000, default 200)").option("--json", "machine-readable output").action(async (repo, stage, action, o) => {
|
|
@@ -40767,7 +40790,7 @@ tenant.command("control <owner/repo> <stage> <action>").description("bounded ten
|
|
|
40767
40790
|
printLine(o.json ? JSON.stringify(result, null, 2) : renderTenantControl(result));
|
|
40768
40791
|
}
|
|
40769
40792
|
if (result.conclusion === "failure") {
|
|
40770
|
-
return failGraceful(`runtime tenant control ${stage} ${action}: ${result.category ?? "failed"}
|
|
40793
|
+
return failGraceful(`runtime tenant control ${stage} ${action}: ${result.category ?? "failed"} \u2014 ${result.note}`);
|
|
40771
40794
|
}
|
|
40772
40795
|
} catch (e) {
|
|
40773
40796
|
return failGraceful(`runtime tenant control: ${e.message}`);
|
|
@@ -40776,7 +40799,7 @@ tenant.command("control <owner/repo> <stage> <action>").description("bounded ten
|
|
|
40776
40799
|
tenant.command("reconcile <owner/repo> <stage>").description("re-render this tenant stage's generated box assets from registry truth; project-admin own dev/rc, master main; watches by default").option("--watch", "wait for the tenant-reconcile.yml run (default)").option("--no-watch", "return after dispatch; do not redeploy until the reconcile run succeeds").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
40777
40800
|
try {
|
|
40778
40801
|
const result = await runTenantReconcile(trainApplyDeps(), { repo, stage, watch: o.watch });
|
|
40779
|
-
printLine(o.json ? JSON.stringify(result, null, 2) : `${result.note}${result.runUrl ? `
|
|
40802
|
+
printLine(o.json ? JSON.stringify(result, null, 2) : `${result.note}${result.runUrl ? ` \u2014 ${result.runUrl}` : ""}`);
|
|
40780
40803
|
if (result.conclusion === "failure" || !result.dispatched) process.exitCode = 1;
|
|
40781
40804
|
} catch (e) {
|
|
40782
40805
|
return failGraceful(`runtime tenant reconcile: ${e.message}`);
|
|
@@ -40789,7 +40812,7 @@ tenant.command("status <owner/repo> <stage>").description("read tenant runtime r
|
|
|
40789
40812
|
console.log(JSON.stringify(result, null, 2));
|
|
40790
40813
|
if (result.publicProbe?.ok === false) process.exitCode = 1;
|
|
40791
40814
|
});
|
|
40792
|
-
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main, or `development` for dev
|
|
40815
|
+
tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the central tenant-deploy.yml for an already-promoted ref (no re-tag/merge); train-authority gated").option("--ref <ref>", "ref to deploy (defaults to the stage branch rc/main, or `development` for dev \u2014 the promoted/staging ref)").option("--watch", "block on the dispatched run and report its outcome (gh run watch --exit-status)").option("--json", "machine-readable output").action(async (repo, stage, o) => {
|
|
40793
40816
|
if (stage !== "dev" && stage !== "rc" && stage !== "main") return fail("runtime tenant redeploy: <stage> must be dev, rc, or main");
|
|
40794
40817
|
try {
|
|
40795
40818
|
const result = await runTenantRedeploy(trainApplyDeps(), { repo, stage, ref: o.ref, watch: o.watch });
|
|
@@ -40798,9 +40821,9 @@ tenant.command("redeploy <owner/repo> <stage>").description("re-dispatch the cen
|
|
|
40798
40821
|
return failGraceful(`runtime tenant redeploy: ${e.message}`);
|
|
40799
40822
|
}
|
|
40800
40823
|
});
|
|
40801
|
-
tenant.command("sweep-rc").description("discover (and optionally retire) running rc tenant runtimes across tenant-containers
|
|
40824
|
+
tenant.command("sweep-rc").description("discover (and optionally retire) running rc tenant runtimes across tenant-containers \u2014 orphan cleanup after a failed post-release retire (#942)").option("--retire", "retire every running rc runtime found (requires --yes) \u2014 WARNING: tears down a legitimately-staged rc too").option("--yes", "confirm the destructive --retire").option("--json", "machine-readable output").action(async (o) => {
|
|
40802
40825
|
if (o.retire && !o.yes) {
|
|
40803
|
-
return fail("runtime tenant sweep-rc --retire is destructive (it tears down EVERY running rc, including one legitimately staged between /rcand and /release)
|
|
40826
|
+
return fail("runtime tenant sweep-rc --retire is destructive (it tears down EVERY running rc, including one legitimately staged between /rcand and /release) \u2014 re-run with --yes to confirm");
|
|
40804
40827
|
}
|
|
40805
40828
|
const cfg = await loadConfig();
|
|
40806
40829
|
const cdeps = registryClientDeps(cfg);
|
|
@@ -40853,7 +40876,7 @@ async function buildTenantRuntimeStatusFor(target, stage, cfg) {
|
|
|
40853
40876
|
lastTenantDeployRun: (await fetchLastTenantDeployRun(slug, stage)).run
|
|
40854
40877
|
});
|
|
40855
40878
|
}
|
|
40856
|
-
var project = program2.command("project").description("the DDB org registry
|
|
40879
|
+
var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); set is master-only");
|
|
40857
40880
|
async function projectTarget(commandName, explicitTarget) {
|
|
40858
40881
|
return requireProjectTarget(commandName, explicitTarget, explicitTarget ? void 0 : await resolveRepo());
|
|
40859
40882
|
}
|
|
@@ -40897,7 +40920,7 @@ project.command("list").description("list all projects (identity + board, never
|
|
|
40897
40920
|
console.log(`${p.slug ?? "?"} - ${p.name ?? ""}${p.division ? ` [${p.division}]` : ""}${p.class ? ` (${p.class})` : ""}${p.projectType ? ` <${p.projectType}>` : ""}${p.deployModel ? ` {${p.deployModel}}` : ""}`);
|
|
40898
40921
|
}
|
|
40899
40922
|
});
|
|
40900
|
-
project.command("get [owner/repo]").description("a project's META (board ids + pointers) by repo or slug; defaults to the current repo
|
|
40923
|
+
project.command("get [owner/repo]").description("a project's META (board ids + pointers) by repo or slug; defaults to the current repo \u2014 identity, NOT deploy coords").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
40901
40924
|
const cfg = await loadConfig();
|
|
40902
40925
|
let target;
|
|
40903
40926
|
try {
|
|
@@ -40907,7 +40930,7 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
|
|
|
40907
40930
|
}
|
|
40908
40931
|
const read = await fetchProjectBySlugChecked(slugOf(target), registryClientDeps(cfg));
|
|
40909
40932
|
if (!read.ok) {
|
|
40910
|
-
return failGraceful(`org project get: Hub registry read failed (${read.error})
|
|
40933
|
+
return failGraceful(`org project get: Hub registry read failed (${read.error}) \u2014 likely transient (cold start, network, or auth blip); retry shortly`);
|
|
40911
40934
|
}
|
|
40912
40935
|
if (!read.project) {
|
|
40913
40936
|
return failGraceful(`org project get: no registry META for ${target} (unknown or unbootstrapped)`);
|
|
@@ -40917,10 +40940,10 @@ project.command("get [owner/repo]").description("a project's META (board ids + p
|
|
|
40917
40940
|
const m = read.project;
|
|
40918
40941
|
const track = resolveReleaseTrack(m, void 0, target);
|
|
40919
40942
|
const stages = branchesForTrack(track).join(" -> ");
|
|
40920
|
-
const note = track === "direct" ? " (direct
|
|
40943
|
+
const note = track === "direct" ? " (direct \u2014 no rc; /rcand refuses, /release ships development -> main)" : track === "trunk" ? " (trunk \u2014 main only)" : "";
|
|
40921
40944
|
console.error(
|
|
40922
|
-
`${m.name ?? target}
|
|
40923
|
-
release track: ${track}
|
|
40945
|
+
`${m.name ?? target} \u2014 class ${m.class ?? "?"} \u2014 deploy ${m.deployModel ?? "?"}
|
|
40946
|
+
release track: ${track} \u2014 stages: ${stages}${note}
|
|
40924
40947
|
deploys run centrally (tenant-deploy.yml); product repos carry no deploy files. Inspect nonsecret DEPLOY facts with \`mmi-cli oracle org project deploy get\`; full coords remain OIDC-gated.`
|
|
40925
40948
|
);
|
|
40926
40949
|
}
|
|
@@ -40994,7 +41017,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
40994
41017
|
const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
|
|
40995
41018
|
return reportWrite("org project set", res);
|
|
40996
41019
|
});
|
|
40997
|
-
project.command("retire [owner/repo]").description("retire an orphaned registry slug (master-only): soft-delete the PROJECT# META row + drop the repo's board items; secrets/vault left alone. DRY-RUN by default
|
|
41020
|
+
project.command("retire [owner/repo]").description("retire an orphaned registry slug (master-only): soft-delete the PROJECT# META row + drop the repo's board items; secrets/vault left alone. DRY-RUN by default \u2014 pass --apply to actually delete; defaults to the current repo").option("--apply", "actually delete (default is a dry-run preview of what would be removed)").option("--skip-board", "retire the registry META only \u2014 leave board items in place").option("--json", "machine-readable {slug, removedMeta, removedBoardItem} output").action(async (repoOrSlug, o) => {
|
|
40998
41021
|
const cfg = await loadConfig();
|
|
40999
41022
|
let target;
|
|
41000
41023
|
try {
|
|
@@ -41026,9 +41049,9 @@ project.command("retire [owner/repo]").description("retire an orphaned registry
|
|
|
41026
41049
|
const verb = result.applied ? "retired" : "WOULD retire (dry-run; pass --apply to delete)";
|
|
41027
41050
|
printLine(`org project retire: ${verb} ${result.slug} (${result.repo})`);
|
|
41028
41051
|
if (result.applied) {
|
|
41029
|
-
printLine(` META: ${result.removedMeta.ok ? `removed=${result.removedMeta.removed ?? "unknown"}` : `FAILED
|
|
41052
|
+
printLine(` META: ${result.removedMeta.ok ? `removed=${result.removedMeta.removed ?? "unknown"}` : `FAILED \u2014 ${result.removedMeta.error}`}`);
|
|
41030
41053
|
} else {
|
|
41031
|
-
printLine(` META: ${result.removedMeta.ok ? result.removedMeta.existing ? "live row present
|
|
41054
|
+
printLine(` META: ${result.removedMeta.ok ? result.removedMeta.existing ? "live row present \u2014 would be tombstoned" : "no live row (already absent/tombstoned)" : `read failed \u2014 ${result.removedMeta.error}`}`);
|
|
41032
41055
|
}
|
|
41033
41056
|
if (result.removedBoardItem) {
|
|
41034
41057
|
const b = result.removedBoardItem;
|
|
@@ -41039,7 +41062,7 @@ project.command("retire [owner/repo]").description("retire an orphaned registry
|
|
|
41039
41062
|
printLine(` ${result.vaultNote}`);
|
|
41040
41063
|
}
|
|
41041
41064
|
if (result.applied && !result.removedMeta.ok) {
|
|
41042
|
-
return failGraceful(`org project retire: META delete failed
|
|
41065
|
+
return failGraceful(`org project retire: META delete failed \u2014 ${result.removedMeta.error}`);
|
|
41043
41066
|
}
|
|
41044
41067
|
});
|
|
41045
41068
|
var fullTrack = program2.command("full-track").description("direct-to-train readiness audits");
|
|
@@ -41067,7 +41090,7 @@ fullTrack.command("readiness <owner/repo>").description("aggregate branch topolo
|
|
|
41067
41090
|
console.log(JSON.stringify(report, null, 2));
|
|
41068
41091
|
if (!report.rcand.canApply) process.exitCode = 1;
|
|
41069
41092
|
});
|
|
41070
|
-
project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row
|
|
41093
|
+
project.command("set-deploy [owner/repo]").description("patch a tenant DEPLOY row \u2014 project-admin may set only --no-env-file on their own existing dev/rc row; master may seed/change all coords and main; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").makeOptionMandatory().choices(["dev", "rc", "main"])).option("--ssh-host <host>", "the box address the deploy ssh-es into; omit to keep the stored value (required only for a NEW hetzner-ssh row \u2014 `mmi-cli devops runtime box list` finds it)").option("--ssh-user <user>", "ssh user; omit to leave the row unchanged (default root on a new row)").option("--port <port>", "loopback port the container binds / Caddy upstream (1..65535); omit to leave the row unchanged").addOption(new Option("--substrate <substrate>", "hetzner-ssh | s3-cloudfront; omit to leave the row unchanged").choices([...DEPLOY_SUBSTRATES])).option("--deploy-role-arn <arn>", "s3-cloudfront: the scoped OIDC deployer role tenant-deploy.yml assumes for this stage").option("--app-bucket <bucket>", "s3-cloudfront: the S3 bucket the built dist/ is synced into").option("--distribution-id <id>", "s3-cloudfront: the CloudFront distribution invalidated after the sync").option("--distribution-domain <domain>", "s3-cloudfront: the host the post-deploy security-header proof is measured against").option("--deploy-path <path>", "on-box per-stage release root; omit to leave the row unchanged (default /opt/mmi/<slug>/<stage> on a new row)").option("--service <name>", "systemd/compose service name; omit to leave the row unchanged (default the slug on a new row)").option("--domain <domain>", "canonical serving host; omit to leave the row unchanged").option("--alias <domain...>", "extra serving hostname the box Caddy answers (repeatable); omit to leave the stored aliases unchanged").option("--clear-aliases", "remove EVERY serving alias from the row (#2986) \u2014 omitting --alias preserves them, so clearing needs saying out loud").option("--no-env-file <bool>", "set the DEPLOY# fileless flag (true|false) \u2014 own-repo project-admin dev/rc; master main; true = env passthrough with no .env symlink").option("--force", "explicit recovery override: skip fileless-compose verification only after independent proof").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
41071
41094
|
const cfg = await loadConfig();
|
|
41072
41095
|
let target;
|
|
41073
41096
|
try {
|
|
@@ -41093,7 +41116,7 @@ ${filelessTransitionGuide(target, o.stage)}`);
|
|
|
41093
41116
|
if (verdict.warn) console.error(`org project set-deploy: ${verdict.warn}`);
|
|
41094
41117
|
} else if (!o.force) {
|
|
41095
41118
|
return fail(
|
|
41096
|
-
`org project set-deploy: cannot verify ${target} ${branch} from this checkout
|
|
41119
|
+
`org project set-deploy: cannot verify ${target} ${branch} from this checkout \u2014 refusing to change noEnvFile. Run from the target repo with origin/${branch} fetched.
|
|
41097
41120
|
${filelessTransitionGuide(target, o.stage)}`
|
|
41098
41121
|
);
|
|
41099
41122
|
} else {
|
|
@@ -41128,14 +41151,14 @@ ${filelessTransitionGuide(target, o.stage)}`
|
|
|
41128
41151
|
const res = await setDeployCoords(slug, body, registryClientDeps(cfg));
|
|
41129
41152
|
return reportWrite("org project set-deploy", res);
|
|
41130
41153
|
});
|
|
41131
|
-
var registry = program2.command("registry").description("the DDB org registry
|
|
41154
|
+
var registry = program2.command("registry").description("the DDB org registry \u2014 org-level constants");
|
|
41132
41155
|
registry.command("org").description("the org config (account id, region, orgProjectId, sagaApiUrl)").option("--json", "machine-readable output").action(async (_o) => {
|
|
41133
41156
|
const cfg = await loadConfig();
|
|
41134
41157
|
const org = await fetchOrgConfig(registryClientDeps(cfg));
|
|
41135
41158
|
if (!org) return failGraceful("org config get: Hub API unreachable, unseeded, or this repo is not bootstrapped");
|
|
41136
41159
|
console.log(JSON.stringify(org));
|
|
41137
41160
|
});
|
|
41138
|
-
var oauth = program2.command("oauth").description("per-repo Google OAuth
|
|
41161
|
+
var oauth = program2.command("oauth").description("per-repo Google OAuth \u2014 plan the canonical URI set, verify the client is port-agnostic");
|
|
41139
41162
|
oauth.command("plan", { isDefault: true }).description("print the canonical JS origins + redirect URIs + SSM cred param names for this repo").option("--repo <owner/repo>", "slug source (defaults to the current repo)").option("--json", "machine-readable output").action(async (o) => {
|
|
41140
41163
|
const cfg = await loadConfig();
|
|
41141
41164
|
const slug = (o.repo ? o.repo.split("/").pop() : cfg.project ?? await repoSlug()).toLowerCase();
|
|
@@ -41149,7 +41172,7 @@ oauth.command("plan", { isDefault: true }).description("print the canonical JS o
|
|
|
41149
41172
|
return failGraceful(
|
|
41150
41173
|
`org oauth plan: ${message}. Declare it in the registry META first:
|
|
41151
41174
|
mmi-cli oracle org project set ${o.repo ?? `mutmutco/${slug}`} --var 'oauth={"subdomains":["${defaultSubdomain(slug)}"],"callbackPath":"${DEFAULT_CALLBACK_PATH}"}'
|
|
41152
|
-
New repo? The GCP project and its Google Auth Platform consent screen must exist too
|
|
41175
|
+
New repo? The GCP project and its Google Auth Platform consent screen must exist too \u2014 docs/Guides/oauth-provision.md \u2014 New repo.`
|
|
41153
41176
|
);
|
|
41154
41177
|
}
|
|
41155
41178
|
return failGraceful(`org oauth plan: ${message}`);
|
|
@@ -41175,7 +41198,7 @@ SSM cred params (under /mmi-future/${slug}/):`);
|
|
|
41175
41198
|
oauth.command("set-creds").description('store the OAuth client into the canonical stageless GOOGLE_CLIENT_ID/SECRET pair (pipe the Console "Download JSON" on stdin)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
|
|
41176
41199
|
const raw = await readStdin();
|
|
41177
41200
|
if (!raw.trim()) {
|
|
41178
|
-
return fail("org oauth set-creds: pipe the Google client JSON on stdin
|
|
41201
|
+
return fail("org oauth set-creds: pipe the Google client JSON on stdin \u2014 e.g.\n mmi-cli vault org oauth set-creds --repo <owner/repo> < client.json");
|
|
41179
41202
|
}
|
|
41180
41203
|
let creds;
|
|
41181
41204
|
try {
|
|
@@ -41225,9 +41248,9 @@ oauth.command("verify").description("probe Google authorize with an arbitrary po
|
|
|
41225
41248
|
if (o.json) {
|
|
41226
41249
|
console.log(JSON.stringify({ slug, redirectUri, portAgnostic: !mismatch }));
|
|
41227
41250
|
} else if (mismatch) {
|
|
41228
|
-
console.error(`FAIL ${slug}: redirect_uri_mismatch for ${redirectUri}
|
|
41251
|
+
console.error(`FAIL ${slug}: redirect_uri_mismatch for ${redirectUri} \u2014 client is not port-agnostic (run /oauth-provision)`);
|
|
41229
41252
|
} else {
|
|
41230
|
-
console.log(`PASS ${slug}: ${redirectUri} accepted
|
|
41253
|
+
console.log(`PASS ${slug}: ${redirectUri} accepted \u2014 port-agnostic OAuth is live`);
|
|
41231
41254
|
}
|
|
41232
41255
|
if (mismatch) process.exitCode = 1;
|
|
41233
41256
|
});
|
|
@@ -41240,7 +41263,7 @@ function resolveCreatePriority(raw, command) {
|
|
|
41240
41263
|
try {
|
|
41241
41264
|
return normalizePriority(raw);
|
|
41242
41265
|
} catch {
|
|
41243
|
-
fail(`${command}: unknown priority "${raw}"
|
|
41266
|
+
fail(`${command}: unknown priority "${raw}" \u2014 expected one of: ${CLI_PRIORITIES.join(", ")}`, {
|
|
41244
41267
|
code: ERROR_CODES.ERR_BAD_ENUM,
|
|
41245
41268
|
offending_flag: "--priority",
|
|
41246
41269
|
expected: [...CLI_PRIORITIES]
|
|
@@ -41250,15 +41273,25 @@ function resolveCreatePriority(raw, command) {
|
|
|
41250
41273
|
function resolveCreateType(raw, command, labels) {
|
|
41251
41274
|
if (raw === void 0 || raw === "") {
|
|
41252
41275
|
const nearMiss = labels?.find((l) => ISSUE_TYPES.includes(l));
|
|
41253
|
-
const hint = nearMiss ? ` (you passed --label ${nearMiss}
|
|
41276
|
+
const hint = nearMiss ? ` (you passed --label ${nearMiss} \u2014 did you mean --type ${nearMiss}?)` : "";
|
|
41277
|
+
let correctedCommand;
|
|
41278
|
+
if (nearMiss) {
|
|
41279
|
+
const argv = process.argv.slice(2);
|
|
41280
|
+
const collisionIndex = argv.findIndex((a, i) => a === "--label" && argv[i + 1] === nearMiss);
|
|
41281
|
+
if (collisionIndex !== -1) {
|
|
41282
|
+
const rewritten = argv.slice();
|
|
41283
|
+
rewritten[collisionIndex] = "--type";
|
|
41284
|
+
correctedCommand = `mmi-cli ${canonicalArgvFor(rewritten).join(" ")}`;
|
|
41285
|
+
}
|
|
41286
|
+
}
|
|
41254
41287
|
fail(`${command}: --type is required (bug | feature | task) unless --batch supplies it per row${hint}`, {
|
|
41255
41288
|
code: ERROR_CODES.ERR_MISSING_FLAG,
|
|
41256
41289
|
offending_flag: "--type",
|
|
41257
|
-
...
|
|
41290
|
+
...correctedCommand ? { corrected_command: correctedCommand } : {}
|
|
41258
41291
|
});
|
|
41259
41292
|
}
|
|
41260
41293
|
if (!ISSUE_TYPES.includes(raw)) {
|
|
41261
|
-
fail(`${command}: unknown type "${raw}"
|
|
41294
|
+
fail(`${command}: unknown type "${raw}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`, {
|
|
41262
41295
|
code: ERROR_CODES.ERR_BAD_ENUM,
|
|
41263
41296
|
offending_flag: "--type",
|
|
41264
41297
|
expected: [...ISSUE_TYPES]
|
|
@@ -41272,9 +41305,9 @@ function resolveCreateSurface(opts) {
|
|
|
41272
41305
|
function surfaceWaived() {
|
|
41273
41306
|
return rawFlag("--no-surface");
|
|
41274
41307
|
}
|
|
41275
|
-
var issue = program2.command("issue").description("issues
|
|
41308
|
+
var issue = program2.command("issue").description("issues \u2014 reliable create with structured output");
|
|
41276
41309
|
withExamples(mutating(
|
|
41277
|
-
issue.command("create").description("create an issue (type
|
|
41310
|
+
issue.command("create").description("create an issue (type \u2014 label) and print {number,url,label} JSON").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label; required unless --batch)").choices([...ISSUE_TYPES])).option("--title <title>", "issue title").option("--title-file <path|->", "read the issue title from a UTF-8 file, or from stdin with -").option("--body <body>", "issue body (markdown)").option("--body-file <path|->", "read issue body from a UTF-8 file. `-` (stdin) needs a heredoc, which the agent inline-body guard denies (#1473/#2125) \u2014 prefer a real path; a title with backticks needs --title-file for the same reason (#3381)").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only \u2014 never a priority:* label, #416)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--label <label...>", "extra label(s) to attach (repeatable; auto-created if missing)").option("--surface <surface>", "issue surface, with or without the surface: prefix (#3789). Required when the target repo runs the one-surface-label board rule; any value satisfies it, so this is not a closed enum").option("--no-surface", "file without a surface label on a repo that requires one \u2014 for a genuinely exempt filing (e.g. a coop proof issue that spans every surface)").option("--parent <ref>", "file as a native sub-issue of this parent (#123, owner/repo#123, or URL)").option("--no-related", "skip the auto related-issues comment"),
|
|
41278
41311
|
// --dry-run/--validate-only plan: resolve the same title source and validate the same type, priority,
|
|
41279
41312
|
// and surface contract as the real action. A plan that echoes the title-file PATH instead of its value
|
|
41280
41313
|
// is not a plan of the mutation that will run (#3914).
|
|
@@ -41341,7 +41374,7 @@ withExamples(mutating(
|
|
|
41341
41374
|
}
|
|
41342
41375
|
targetRepo2 = await resolveRepo(o.repo);
|
|
41343
41376
|
if (!targetRepo2) {
|
|
41344
|
-
return fail("issue create: could not resolve the target repo
|
|
41377
|
+
return fail("issue create: could not resolve the target repo \u2014 run inside a git checkout or pass --repo <owner/repo>");
|
|
41345
41378
|
}
|
|
41346
41379
|
args = buildIssueArgs({
|
|
41347
41380
|
type: issueType,
|
|
@@ -41370,7 +41403,7 @@ withExamples(mutating(
|
|
|
41370
41403
|
labels: extraLabels.length ? extraLabels : void 0
|
|
41371
41404
|
});
|
|
41372
41405
|
process.stderr.write(
|
|
41373
|
-
`warning: --surface ${surfaceFlagLabel} was dropped
|
|
41406
|
+
`warning: --surface ${surfaceFlagLabel} was dropped \u2014 ${targetRepo2} defines no surface:* labels, and creating one here would switch the one-surface-label rule on for every later filing in it. Use --label ${surfaceFlagLabel} if you really mean to start that taxonomy.
|
|
41374
41407
|
`
|
|
41375
41408
|
);
|
|
41376
41409
|
}
|
|
@@ -41420,7 +41453,7 @@ async function readParentField(number, repo) {
|
|
|
41420
41453
|
}
|
|
41421
41454
|
return resolveParentField(payload);
|
|
41422
41455
|
}
|
|
41423
|
-
issue.command("view <number>").description('read an issue as structured JSON
|
|
41456
|
+
issue.command("view <number>").description('read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347). --comments folds in every comment; --context also adds linkedPrs + children (the one-shot "load the whole item" read, #2894)').option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "number,title,body,url"').option("--comments", 'include every comment (body + comments in one call) \u2014 the agentic "read the whole item before working it" read (#2894)').option("--context", "full working context in one call: implies --comments and also adds linkedPrs and, for an epic, a children summary (#2894)").action(async (number, o) => {
|
|
41424
41457
|
const n = Number(number);
|
|
41425
41458
|
if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
|
|
41426
41459
|
const repo = await resolveRepo(o.repo);
|
|
@@ -41514,7 +41547,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
|
|
|
41514
41547
|
return fail(`issue comment: ${e.message}`);
|
|
41515
41548
|
}
|
|
41516
41549
|
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
41517
|
-
if (!repo) return fail("issue comment: could not resolve repo
|
|
41550
|
+
if (!repo) return fail("issue comment: could not resolve repo \u2014 pass --repo owner/repo");
|
|
41518
41551
|
try {
|
|
41519
41552
|
const result = await postIssueComment(defaultGitHubClient(), { ref, defaultRepo: repo, body });
|
|
41520
41553
|
console.log(JSON.stringify(result));
|
|
@@ -41523,7 +41556,7 @@ jsonParity(issue.command("comment <ref>").description("post a Markdown comment t
|
|
|
41523
41556
|
return failGraceful(`issue comment: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
41524
41557
|
}
|
|
41525
41558
|
});
|
|
41526
|
-
jsonParity(issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match
|
|
41559
|
+
jsonParity(issue.command("check <ref>").description("tick (or with --off untick) a task-list checkbox in an issue/epic body by its item text and print {number,repo,item,checked,changed} JSON").requiredOption("--item <text>", "the checklist item to match \u2014 exact item text, else a unique substring").option("--off", "untick the item ([x] \u2014 [ ]) instead of ticking it").option("--repo <owner/repo>", "repo for a bare ref (defaults to the current repo)")).action(async (ref, o) => {
|
|
41527
41560
|
let parsed;
|
|
41528
41561
|
try {
|
|
41529
41562
|
parsed = parseIssueRef(ref);
|
|
@@ -41531,7 +41564,7 @@ jsonParity(issue.command("check <ref>").description("tick (or with --off untick)
|
|
|
41531
41564
|
return fail(`issue check: ${e.message}`);
|
|
41532
41565
|
}
|
|
41533
41566
|
const repo = await resolveRepo(parsed.repo ?? o.repo);
|
|
41534
|
-
if (!repo) return fail("issue check: could not resolve repo
|
|
41567
|
+
if (!repo) return fail("issue check: could not resolve repo \u2014 pass --repo owner/repo");
|
|
41535
41568
|
const checked = o.off !== true;
|
|
41536
41569
|
let body;
|
|
41537
41570
|
try {
|
|
@@ -41544,7 +41577,7 @@ jsonParity(issue.command("check <ref>").description("tick (or with --off untick)
|
|
|
41544
41577
|
if (!result.ok) {
|
|
41545
41578
|
if (result.reason === "ambiguous") {
|
|
41546
41579
|
const list = result.matches.map((m) => ` - ${m.text}`).join("\n");
|
|
41547
|
-
return fail(`issue check: "${o.item}" matches ${result.matches.length} checklist items in ${repo}#${parsed.number}
|
|
41580
|
+
return fail(`issue check: "${o.item}" matches ${result.matches.length} checklist items in ${repo}#${parsed.number} \u2014 narrow the text:
|
|
41548
41581
|
${list}`);
|
|
41549
41582
|
}
|
|
41550
41583
|
return fail(`issue check: no checklist item matching "${o.item}" in ${repo}#${parsed.number}`);
|
|
@@ -41563,7 +41596,7 @@ ${list}`);
|
|
|
41563
41596
|
}
|
|
41564
41597
|
console.log(JSON.stringify({ number: parsed.number, repo, item: result.item.text, checked, changed: true }));
|
|
41565
41598
|
});
|
|
41566
|
-
program2.command("report").description("file a friction report on the Hub board (Hub session auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label)").default("task").choices([...ISSUE_TYPES])).option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", 'attribute the report to a different source repo than the current checkout for the "Filed via..." footer (rare
|
|
41599
|
+
program2.command("report").description("file a friction report on the Hub board (Hub session auth, dedups open reports) and print {number,url} JSON").option("--title <title>", "one-line friction summary").option("--title-file <path|->", "read the friction summary from a UTF-8 file, or from stdin with -").option("--body <body>", "report body (markdown)").option("--body-file <path|->", "read report body from a UTF-8 file, or from stdin with -").addOption(new Option("--type <type>", "bug | feature | task (sets the matching label)").default("task").choices([...ISSUE_TYPES])).option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", 'attribute the report to a different source repo than the current checkout for the "Filed via..." footer (rare \u2014 usually auto-detected; every report always lands on the org Hub, never an alternate target, #263)').option("--force", "file a new issue even when an open report looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 report always prints JSON; #682)").action(async (o) => {
|
|
41567
41600
|
let body;
|
|
41568
41601
|
let priority;
|
|
41569
41602
|
let title;
|
|
@@ -41573,14 +41606,16 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
41573
41606
|
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises11.readFile, readStdin });
|
|
41574
41607
|
priority = resolveCreatePriority(o.priority, "report");
|
|
41575
41608
|
if (!ISSUE_TYPES.includes(o.type)) {
|
|
41576
|
-
throw new Error(`unknown issue type "${o.type}"
|
|
41609
|
+
throw new Error(`unknown issue type "${o.type}" \u2014 expected one of: ${ISSUE_TYPES.join(", ")}`);
|
|
41577
41610
|
}
|
|
41578
41611
|
} catch (e) {
|
|
41579
41612
|
const m = e.message;
|
|
41580
41613
|
const payload = m === "pass --title or --title-file" ? {
|
|
41581
41614
|
code: ERROR_CODES.ERR_MISSING_FLAG,
|
|
41582
41615
|
offending_flag: "--title",
|
|
41583
|
-
|
|
41616
|
+
// #4803: process.argv's house token is already spliced by resolveHouseShim (#4438);
|
|
41617
|
+
// canonicalArgvFor re-prepends it so this stays the runnable house-prefixed form.
|
|
41618
|
+
corrected_command: `mmi-cli ${canonicalArgvFor(process.argv.slice(2)).join(" ")} --title "..."`
|
|
41584
41619
|
} : void 0;
|
|
41585
41620
|
return fail(`report: ${m}`, payload);
|
|
41586
41621
|
}
|
|
@@ -41610,7 +41645,7 @@ async function resolvePluginSha() {
|
|
|
41610
41645
|
return void 0;
|
|
41611
41646
|
}
|
|
41612
41647
|
}
|
|
41613
|
-
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").addOption(new Option("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).makeOptionMandatory().choices([...SKILL_NAMES])).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default
|
|
41648
|
+
program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").addOption(new Option("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).makeOptionMandatory().choices([...SKILL_NAMES])).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (defaults to medium; sets the board Priority field only, #416)").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
|
|
41614
41649
|
const targetRepo2 = o.repo ?? HUB_REPO;
|
|
41615
41650
|
const sourceRepo = await resolveRepo(void 0);
|
|
41616
41651
|
const pluginSha = await resolvePluginSha();
|
|
@@ -41665,7 +41700,7 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
41665
41700
|
repo: targetRepo2,
|
|
41666
41701
|
labels: [SKILL_LESSON_LABEL],
|
|
41667
41702
|
command: "skill-lesson",
|
|
41668
|
-
waiver: { reason: "tooling lesson spans product surfaces
|
|
41703
|
+
waiver: { reason: "tooling lesson spans product surfaces \u2014 coop-proof class (#3789)" }
|
|
41669
41704
|
});
|
|
41670
41705
|
if (surfaceWarn) process.stderr.write(`${surfaceWarn}
|
|
41671
41706
|
`);
|
|
@@ -41677,7 +41712,7 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
41677
41712
|
const { projectItemId, onBoard } = await attachToProject(created.number, targetRepo2, priority);
|
|
41678
41713
|
console.log(JSON.stringify({ ...created, deduped: false, label: SKILL_LESSON_LABEL, skill, priority, projectItemId, onBoard }));
|
|
41679
41714
|
});
|
|
41680
|
-
var pr = program2.command("pr").description("pull requests
|
|
41715
|
+
var pr = program2.command("pr").description("pull requests \u2014 reliable create with structured output");
|
|
41681
41716
|
withExamples(pr.command("create").description("create a PR and print {number,url} JSON").option("--title <title>", "PR title").option("--title-file <path|->", "read the PR title from a UTF-8 file, or from stdin with -").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--draft", "open the PR in draft state (#2667)").option("--json", "machine-readable output (default; accepted for parity)").action(async (o) => {
|
|
41682
41717
|
let body;
|
|
41683
41718
|
let title;
|
|
@@ -41691,7 +41726,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
41691
41726
|
body = rewriteNegatedClosingPhrases(body);
|
|
41692
41727
|
const docsCheck = await checkDocsIndexAtHead({ repo: o.repo, head: o.head }, createPrCreateDocsIndexDeps());
|
|
41693
41728
|
if (docsCheck && !docsCheck.ok) {
|
|
41694
|
-
return fail(`pr create: ${docsCheck.detail}
|
|
41729
|
+
return fail(`pr create: ${docsCheck.detail} \u2014 ${docsCheck.fix}`);
|
|
41695
41730
|
}
|
|
41696
41731
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo, draft: o.draft }));
|
|
41697
41732
|
invalidateStatuslineBoardCache();
|
|
@@ -41704,7 +41739,7 @@ withExamples(pr.command("create").description("create a PR and print {number,url
|
|
|
41704
41739
|
"Use --body-file for multiline PR bodies instead of shell-escaped inline markdown.",
|
|
41705
41740
|
"Write that file under .jerv/ inside a worktree (#4405): any other untracked path makes `worktree land` refuse cleanup as untracked-files."
|
|
41706
41741
|
]);
|
|
41707
|
-
pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit)
|
|
41742
|
+
pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347). --comments folds in every comment; --context also adds linkedIssues (the issues it closes/references, #2894)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json [fields...]", 'gh --json field list (overrides the default field set). Accepts commas, spaces, or repeated --json flags \u2014 in PowerShell an unquoted comma list is an array literal, so QUOTE it: --json "state,baseRefName,mergeCommit"').option("--comments", "include every comment (body + comments in one call) \u2014 read the whole PR before landing it (#2894)").option("--context", "full working context in one call: implies --comments and also adds linkedIssues (the issues the PR closes/references) (#2894)").action(async (number, o) => {
|
|
41708
41743
|
const n = Number(number);
|
|
41709
41744
|
if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
|
|
41710
41745
|
const repo = await resolveRepo(o.repo);
|
|
@@ -41814,7 +41849,7 @@ pr.command("ci-policy").description("report merge CI policy: wait-for-checks vs
|
|
|
41814
41849
|
if (o.json) return printLine(JSON.stringify(result));
|
|
41815
41850
|
printLine(`merge CI policy: ${result.policy} (${result.reason})`);
|
|
41816
41851
|
});
|
|
41817
|
-
pr.command("checks-wait <number>").description(`bounded wait for PR checks; skips immediately on no-ci repos (#1432), fails immediately on a CONFLICTING PR
|
|
41852
|
+
pr.command("checks-wait <number>").description(`bounded wait for PR checks; skips immediately on no-ci repos (#1432), fails immediately on a CONFLICTING PR \u2014 GitHub never queues checks for one (#2970). REST-only polling with a pool floor (#3024). Default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m; --timeout raises it. Exit 1 = a check FAILED or the PR is CONFLICTING, exit ${PR_CHECKS_TIMEOUT_EXIT_CODE} = the wait window expired or the API pool ran dry (re-arm)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--timeout <minutes>", `wait budget in minutes (default ${PR_CHECKS_TIMEOUT_MS / 6e4}) \u2014 raise it for serial self-hosted e2e queues`).action(async (number, o) => {
|
|
41818
41853
|
let timeoutMs;
|
|
41819
41854
|
if (o.timeout !== void 0) {
|
|
41820
41855
|
const minutes = Number(o.timeout);
|
|
@@ -41844,27 +41879,27 @@ pr.command("checks-wait <number>").description(`bounded wait for PR checks; skip
|
|
|
41844
41879
|
timeoutMs,
|
|
41845
41880
|
// Liveness on stderr, one line per poll. A silent bounded wait is indistinguishable from a hang, and
|
|
41846
41881
|
// an agent harness kills it on its own (shorter) deadline before the verdict ever prints (#2940).
|
|
41847
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state}
|
|
41882
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr checks-wait: ${state} \u2014 ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
41848
41883
|
});
|
|
41849
41884
|
if (o.json) printLine(JSON.stringify(result));
|
|
41850
41885
|
else if (result.status === "conflicting") {
|
|
41851
|
-
printLine(`pr checks-wait: conflicting
|
|
41886
|
+
printLine(`pr checks-wait: conflicting \u2014 ${result.reason}`);
|
|
41852
41887
|
} else if (result.status === "timeout") {
|
|
41853
41888
|
const stuckQueuing = /no-checks-reported/i.test(result.detail ?? "");
|
|
41854
41889
|
printLine(
|
|
41855
|
-
`pr checks-wait: timeout
|
|
41890
|
+
`pr checks-wait: timeout \u2014 waited ${Math.round((result.waitedMs ?? 0) / 6e4)}m, last state: ${result.detail ?? "pending"}. No check failed; re-run to keep waiting, or pass --timeout <minutes>.` + (stuckQueuing ? " Tip: jobs never appeared \u2014 mmi-live may be saturated or Actions may be failing before checkout; check runner health (docs/Guides/gh-runner-runbook.md) and re-run the workflow." : "")
|
|
41856
41891
|
);
|
|
41857
41892
|
} else if (result.status === "rate-limited") {
|
|
41858
|
-
printLine(`pr checks-wait: rate-limited
|
|
41893
|
+
printLine(`pr checks-wait: rate-limited \u2014 ${result.reason ?? "REST pool below floor"}. No check failed; re-run after the pool resets.`);
|
|
41859
41894
|
} else if (result.detail === "runner-infra") {
|
|
41860
|
-
printLine(`pr checks-wait: failure (runner infrastructure, NOT a test failure)
|
|
41895
|
+
printLine(`pr checks-wait: failure (runner infrastructure, NOT a test failure) \u2014 ${result.reason}`);
|
|
41861
41896
|
} else if (result.detail === "stale-head") {
|
|
41862
|
-
printLine(`pr checks-wait: failure (stale PR head, NOT a test failure)
|
|
41863
|
-
} else printLine(`pr checks-wait: ${result.status}${result.reason ? `
|
|
41897
|
+
printLine(`pr checks-wait: failure (stale PR head, NOT a test failure) \u2014 ${result.reason}`);
|
|
41898
|
+
} else printLine(`pr checks-wait: ${result.status}${result.reason ? ` \u2014 ${result.reason}` : ""}${result.detail ? ` (${result.detail})` : ""}`);
|
|
41864
41899
|
if (result.status === "failure" || result.status === "conflicting") process.exitCode = 1;
|
|
41865
41900
|
if (result.status === "timeout" || result.status === "rate-limited") process.exitCode = PR_CHECKS_TIMEOUT_EXIT_CODE;
|
|
41866
41901
|
});
|
|
41867
|
-
pr.command("land <number>").description("agent merge path (#1440): train probe
|
|
41902
|
+
pr.command("land <number>").description("agent merge path (#1440): train probe \u2014 checks-wait \u2014 merge --auto \u2014 poll enqueued \u2014 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) => {
|
|
41868
41903
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
41869
41904
|
const startingPath = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim();
|
|
41870
41905
|
assertPrMergeHousekeepingClean(startingPath || process.cwd(), "pr land", { force: o.force });
|
|
@@ -41898,8 +41933,8 @@ pr.command("land <number>").description("agent merge path (#1440): train probe ?
|
|
|
41898
41933
|
} catch {
|
|
41899
41934
|
}
|
|
41900
41935
|
if (isPromotionBase(base, track)) {
|
|
41901
|
-
const shape = track ? `${track} track` : "unresolved track
|
|
41902
|
-
throw new Error(`pr land: base branch ${base} is a promotion target (${shape})
|
|
41936
|
+
const shape = track ? `${track} track` : "unresolved track \u2014 strict reading";
|
|
41937
|
+
throw new Error(`pr land: base branch ${base} is a promotion target (${shape}) \u2014 promotion merges stay human-only`);
|
|
41903
41938
|
}
|
|
41904
41939
|
return repo;
|
|
41905
41940
|
},
|
|
@@ -41909,26 +41944,26 @@ pr.command("land <number>").description("agent merge path (#1440): train probe ?
|
|
|
41909
41944
|
const requiredContexts = await fetchRequiredCheckContexts(repo, "development").catch(() => null);
|
|
41910
41945
|
return waitForPrChecks({
|
|
41911
41946
|
resolvePolicy: () => resolveRepoMergeCiPolicy(repo, ciAuditDeps()),
|
|
41912
|
-
// #3024: REST-only wait loop
|
|
41947
|
+
// #3024: REST-only wait loop — runPrLand's resolveRepo already guarantees `repo` is set.
|
|
41913
41948
|
pollChecks: () => pollRestPrChecks(prNumber, repo, void 0, requiredContexts),
|
|
41914
41949
|
// #2970: `pr land` only ever lands PRs based on development (resolveRepo above already rejects any
|
|
41915
41950
|
// other base), so the fast-fail message's base branch is always 'development' here.
|
|
41916
41951
|
pollMergeable: () => pollRestPrMergeable(prNumber, repo),
|
|
41917
41952
|
pollRateLimit: () => waitLoopCorePool("pr land"),
|
|
41918
|
-
// #3388: `pr land` is the batch path
|
|
41953
|
+
// #3388: `pr land` is the batch path — the one most likely to self-DOS the shared runner and
|
|
41919
41954
|
// then read its own wall-clock kill as a broken diff.
|
|
41920
41955
|
diagnoseFailure: () => waitLoopDiagnosis("pr land", prNumber, repo),
|
|
41921
41956
|
baseBranch: "development",
|
|
41922
41957
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
41923
41958
|
log: (message) => console.warn(message),
|
|
41924
|
-
// `pr land` inherits the same (raised) checks budget, so it needs the same liveness
|
|
41959
|
+
// `pr land` inherits the same (raised) checks budget, so it needs the same liveness — otherwise the
|
|
41925
41960
|
// 30m wait is SILENT and reads exactly like the hang #2940 was filed about, only three times longer.
|
|
41926
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr land: waiting on checks
|
|
41961
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr land: waiting on checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
41927
41962
|
});
|
|
41928
41963
|
},
|
|
41929
|
-
// #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying
|
|
41964
|
+
// #2425: re-probe used ONLY to decide whether a failed `gh pr merge --auto` is worth retrying — a
|
|
41930
41965
|
// fresh read of state/mergeable/checks, independent of the merge call's own (possibly stale) error.
|
|
41931
|
-
// #4396: same required-status scoping as the wait loop above
|
|
41966
|
+
// #4396: same required-status scoping as the wait loop above — a non-required red must not read
|
|
41932
41967
|
// as "not ready to retry" any more than it should have blocked the wait itself.
|
|
41933
41968
|
probeMergeReady: async (prNumber, repo) => {
|
|
41934
41969
|
const [snapshot, requiredContexts] = await Promise.all([
|
|
@@ -41958,7 +41993,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe ?
|
|
|
41958
41993
|
if (read.state === "merged") return true;
|
|
41959
41994
|
if (read.state === "failed") {
|
|
41960
41995
|
lastFailure = read.error;
|
|
41961
|
-
console.warn(`pr land: merged-state read FAILED (${read.error})
|
|
41996
|
+
console.warn(`pr land: merged-state read FAILED (${read.error}) \u2014 retrying; this is not evidence the PR is unmerged`);
|
|
41962
41997
|
} else {
|
|
41963
41998
|
lastFailure = void 0;
|
|
41964
41999
|
}
|
|
@@ -41966,7 +42001,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe ?
|
|
|
41966
42001
|
}
|
|
41967
42002
|
if (lastFailure) {
|
|
41968
42003
|
throw new Error(
|
|
41969
|
-
`pr land: could not verify whether PR #${prNumber} merged
|
|
42004
|
+
`pr land: could not verify whether PR #${prNumber} merged \u2014 the last merged-state read FAILED (${lastFailure}). The auto-merge enqueue stands; read the PR directly (gh pr view ${prNumber} --repo ${repo} --json merged,state) and rerun cleanup if it merged.`
|
|
41970
42005
|
);
|
|
41971
42006
|
}
|
|
41972
42007
|
return false;
|
|
@@ -41974,7 +42009,7 @@ pr.command("land <number>").description("agent merge path (#1440): train probe ?
|
|
|
41974
42009
|
});
|
|
41975
42010
|
if (result.status !== "failed") {
|
|
41976
42011
|
const repoFlag = result.repo ? ` --repo ${result.repo}` : "";
|
|
41977
|
-
console.warn(`pr land: merge confirmed; cleanup pending
|
|
42012
|
+
console.warn(`pr land: merge confirmed; cleanup pending \u2014 if this process is interrupted, resume with: mmi-cli devops pr merge ${number}${repoFlag} --squash`);
|
|
41978
42013
|
}
|
|
41979
42014
|
if (result.status !== "failed") {
|
|
41980
42015
|
try {
|
|
@@ -42005,13 +42040,13 @@ pr.command("land <number>").description("agent merge path (#1440): train probe ?
|
|
|
42005
42040
|
}
|
|
42006
42041
|
if (o.json) printLine(JSON.stringify(result));
|
|
42007
42042
|
else {
|
|
42008
|
-
printLine(`pr land: ${result.status}${result.error ? `
|
|
42043
|
+
printLine(`pr land: ${result.status}${result.error ? ` \u2014 ${result.error}` : ""}`);
|
|
42009
42044
|
for (const line of renderPrLandCleanupLines(result.cleanup)) printLine(line);
|
|
42010
42045
|
if (result.cleanupError) printLine(`pr land cleanup: ${result.cleanupError}`);
|
|
42011
42046
|
}
|
|
42012
42047
|
if (result.status === "failed" || result.cleanupError) process.exitCode = 1;
|
|
42013
42048
|
});
|
|
42014
|
-
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree
|
|
42049
|
+
jsonParity(pr.command("merge <number>").description("merge a PR (squash by default) and clean up its branch + worktree \u2014 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 \u2014 merge once the base-branch policy is satisfied (use for policy-gated repos)").option("--wait", `wait for checks to reach a terminal passing verdict before merging (default budget ${PR_CHECKS_TIMEOUT_MS / 6e4}m)`).option("--preserve-worktree", "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) => {
|
|
42015
42050
|
const method = o.rebase ? "--rebase" : o.merge ? "--merge" : "--squash";
|
|
42016
42051
|
const repoArgs = o.repo ? ["--repo", o.repo] : [];
|
|
42017
42052
|
const repoForPostCleanup = await resolveRepo(o.repo) ?? o.repo;
|
|
@@ -42064,10 +42099,10 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
42064
42099
|
baseBranch,
|
|
42065
42100
|
sleep: (ms) => new Promise((resolve5) => setTimeout(resolve5, ms)),
|
|
42066
42101
|
log: (message) => console.warn(message),
|
|
42067
|
-
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks
|
|
42102
|
+
progress: ({ state, elapsedMs, remainingMs }) => console.warn(`pr merge: --wait checks \u2014 ${state}, ${Math.round(elapsedMs / 1e3)}s elapsed, ${Math.round(remainingMs / 6e4)}m left`)
|
|
42068
42103
|
});
|
|
42069
42104
|
if (wait.status !== "success" && wait.status !== "skipped") {
|
|
42070
|
-
console.warn(`pr merge: --wait stopped before merge
|
|
42105
|
+
console.warn(`pr merge: --wait stopped before merge \u2014 ${wait.status}${wait.reason ? `: ${wait.reason}` : ""}${wait.detail ? ` (${wait.detail})` : ""}`);
|
|
42071
42106
|
process.exitCode = wait.status === "timeout" || wait.status === "rate-limited" ? PR_CHECKS_TIMEOUT_EXIT_CODE : 1;
|
|
42072
42107
|
return;
|
|
42073
42108
|
}
|
|
@@ -42123,7 +42158,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
42123
42158
|
return;
|
|
42124
42159
|
}
|
|
42125
42160
|
if (!o.auto && basePolicyBlocksImmediateMerge(message)) {
|
|
42126
|
-
console.warn(`pr merge: the base-branch policy blocks an immediate merge
|
|
42161
|
+
console.warn(`pr merge: the base-branch policy blocks an immediate merge \u2014 upgrading to --auto (merges once required checks pass).`);
|
|
42127
42162
|
upgradedToAuto = true;
|
|
42128
42163
|
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: true, deleteBranch: !headIsProtected, bodyFile }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e2) => {
|
|
42129
42164
|
const m2 = String(e2.message || "");
|
|
@@ -42158,7 +42193,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
42158
42193
|
const state = stateRead.ok ? stateRead.state : "";
|
|
42159
42194
|
const enqueued = describePrMergeEnqueuedReason(await pollGhPrChecks(number, repoArgs).catch(() => void 0));
|
|
42160
42195
|
console.log(JSON.stringify({ mergeStatus: "auto-merge-enqueued", enqueuedReason: enqueued.reason, pr: number, branch: headRef, state: state || "unknown", upgradedToAuto: upgradedToAuto || void 0 }));
|
|
42161
|
-
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged
|
|
42196
|
+
console.warn(`pr merge: PR #${number} is ENQUEUED, not merged \u2014 ${enqueued.message}.`);
|
|
42162
42197
|
if (upgradedToAuto && !o.auto) {
|
|
42163
42198
|
console.warn(`pr merge: exiting ${PR_MERGE_ENQUEUED_EXIT_CODE} so a chained command does not treat this as a completed merge.`);
|
|
42164
42199
|
process.exitCode = PR_MERGE_ENQUEUED_EXIT_CODE;
|
|
@@ -42174,7 +42209,7 @@ jsonParity(pr.command("merge <number>").description("merge a PR (squash by defau
|
|
|
42174
42209
|
state: stateRead.ok ? stateRead.state : "unknown",
|
|
42175
42210
|
cleanupStatus: "skipped"
|
|
42176
42211
|
}));
|
|
42177
|
-
console.error(`pr merge: ${gate.message}. Nothing was deleted
|
|
42212
|
+
console.error(`pr merge: ${gate.message}. Nothing was deleted \u2014 re-check the PR and retry.`);
|
|
42178
42213
|
process.exitCode = 1;
|
|
42179
42214
|
return;
|
|
42180
42215
|
}
|
|
@@ -42277,7 +42312,7 @@ function trainApplyDeps() {
|
|
|
42277
42312
|
const verdict = await fetchTrainAuthority(repo, registryClientDeps(await loadConfig()));
|
|
42278
42313
|
return verdict.ok ? { ok: true, role: verdict.authority.role, train: verdict.authority.train } : verdict;
|
|
42279
42314
|
},
|
|
42280
|
-
// Hub-App-authority dispatch of the central tenant deploy (#953)
|
|
42315
|
+
// Hub-App-authority dispatch of the central tenant deploy (#953) — the Hub fires the
|
|
42281
42316
|
// workflow_dispatch with its App token, so the caller needs no MMI-Hub Actions write.
|
|
42282
42317
|
dispatchTenantDeploy: async ({ repo, stage, ref }) => {
|
|
42283
42318
|
const res = await tenantDeploy({ repo, stage, ref }, registryClientDeps(await loadConfig()));
|
|
@@ -42286,7 +42321,7 @@ function trainApplyDeps() {
|
|
|
42286
42321
|
throw new Error(`tenant deploy dispatch failed: ${detail}`);
|
|
42287
42322
|
}
|
|
42288
42323
|
},
|
|
42289
|
-
// Hub-App-authority dispatch of the central tenant-control.yml (#1717)
|
|
42324
|
+
// Hub-App-authority dispatch of the central tenant-control.yml (#1717) — the Hub fires the
|
|
42290
42325
|
// workflow_dispatch with its App token. Never throws for an expected rejection: it returns the dispatch
|
|
42291
42326
|
// outcome so runTenantControl can map a 5xx (transport-failed, retryable) vs a 4xx (rejected) vs ok.
|
|
42292
42327
|
dispatchTenantControl: async ({ repo, stage, action, lines }) => {
|
|
@@ -42302,7 +42337,7 @@ function trainApplyDeps() {
|
|
|
42302
42337
|
return { ok: false, category: body?.category, error: body?.error ?? res.error };
|
|
42303
42338
|
},
|
|
42304
42339
|
// Hotfix-coverage guard (#958): runs against the local clone via real git. manifestPaths exempts the
|
|
42305
|
-
// release version fold (#976)
|
|
42340
|
+
// release version fold (#976) — a main-only commit touching ONLY the root package manifest is the
|
|
42306
42341
|
// fold's version metadata, which the candidate replaces with its own. (The Hub's wider distribution
|
|
42307
42342
|
// set never reaches this guard: the Hub is direct-track.)
|
|
42308
42343
|
hotfixCoverage: (input) => checkHotfixCoverage({ ...input, manifestPaths: ["package.json", "package-lock.json"] }),
|
|
@@ -42318,14 +42353,14 @@ function trainApplyDeps() {
|
|
|
42318
42353
|
removeFile: (path2) => (0, import_promises11.unlink)(path2)
|
|
42319
42354
|
}, args),
|
|
42320
42355
|
// #4713 (I/O-boundary census): `null` used to mean BOTH "this project configures no edge domains"
|
|
42321
|
-
// (a real answer) and "the registry read missed"
|
|
42356
|
+
// (a real answer) and "the registry read missed" — so a release verdict printed an environments block
|
|
42322
42357
|
// with no domains as though the project had none. The checked read separates them: an absent project
|
|
42323
42358
|
// or an absent edgeDomains block still answers null; a FAILED read refuses by name, and the caller's
|
|
42324
42359
|
// own best-effort catch degrades the block instead of the read pretending it saw an answer.
|
|
42325
42360
|
fetchEdgeDomains: async (slug) => {
|
|
42326
42361
|
const read = await fetchProjectBySlugChecked(slug, registryClientDeps(await loadConfig()));
|
|
42327
42362
|
if (!read.ok) {
|
|
42328
|
-
const message = `release environments: Hub registry read FAILED for ${slug} (${read.error})
|
|
42363
|
+
const message = `release environments: Hub registry read FAILED for ${slug} (${read.error}) \u2014 edge domains are UNREAD, not absent`;
|
|
42329
42364
|
consoleIo.err(`mmi-cli: ${message}`);
|
|
42330
42365
|
throw new Error(message);
|
|
42331
42366
|
}
|
|
@@ -42347,13 +42382,13 @@ function renderDeployLine(d) {
|
|
|
42347
42382
|
else if (d.runUrl) parts.push(`run ${d.runUrl}`);
|
|
42348
42383
|
if (d.deployStatus === "success") parts.push("deploy: SUCCEEDED");
|
|
42349
42384
|
else if (d.deployStatus === "failure") parts.push("deploy: FAILED (promotion stands; retry the deploy, do not re-tag)");
|
|
42350
|
-
else if (d.runId != null) parts.push(`deploy: UNVERIFIED
|
|
42351
|
-
else if (d.workflowRuns?.length) parts.push("deploy: UNVERIFIED
|
|
42352
|
-
else parts.push("deploy: UNVERIFIED
|
|
42385
|
+
else if (d.runId != null) parts.push(`deploy: UNVERIFIED \u2014 dispatched, not resolved (watch: gh run watch ${d.runId} --repo mutmutco/MMI-Hub --exit-status)`);
|
|
42386
|
+
else if (d.workflowRuns?.length) parts.push("deploy: UNVERIFIED \u2014 the runs above are enumerated, not resolved; watch each to conclusion before calling this release healthy (#3322)");
|
|
42387
|
+
else parts.push("deploy: UNVERIFIED \u2014 no run correlated or watched; resolve every workflow run on the release SHA before calling this release healthy (#3322)");
|
|
42353
42388
|
return parts.join("; ");
|
|
42354
42389
|
}
|
|
42355
42390
|
function renderReleaseResume(r) {
|
|
42356
|
-
const lines = [`mmi-cli devops release --resume: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]
|
|
42391
|
+
const lines = [`mmi-cli devops release --resume: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}] \u2014 ${r.note}`];
|
|
42357
42392
|
for (const step of r.steps) lines.push(` - ${step}`);
|
|
42358
42393
|
if (r.releaseUrl) lines.push(` release: ${r.releaseUrl}`);
|
|
42359
42394
|
if (r.announceNote) lines.push(` announce: ${r.announceNote}`);
|
|
@@ -42363,23 +42398,23 @@ function renderReleaseResume(r) {
|
|
|
42363
42398
|
return lines.join("\n");
|
|
42364
42399
|
}
|
|
42365
42400
|
function renderReleaseAbort(r) {
|
|
42366
|
-
return `mmi-cli devops release --abort --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)})
|
|
42401
|
+
return `mmi-cli devops release --abort --apply: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}`;
|
|
42367
42402
|
}
|
|
42368
42403
|
function renderReleasePublishRetry(r) {
|
|
42369
|
-
return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)})
|
|
42404
|
+
return `mmi-cli devops release --retry-publish: ${r.repo} ${r.tag} (${r.tagSha.slice(0, 12)}) \u2014 ${r.note}; ${r.runUrl}`;
|
|
42370
42405
|
}
|
|
42371
42406
|
function renderRcandResume(r) {
|
|
42372
|
-
return `mmi-cli devops rcand --resume: promoted ${r.repo}
|
|
42407
|
+
return `mmi-cli devops rcand --resume: promoted ${r.repo} \u2014 rc at ${r.tag} (${r.tagSha.slice(0, 12)}) [${r.deployModel}]; ${renderDeployLine(r)}; ${r.note}`;
|
|
42373
42408
|
}
|
|
42374
42409
|
function renderAlignment(label, alignment) {
|
|
42375
42410
|
if (alignment.status !== "pr-pending") return `${label}: ${alignment.note}`;
|
|
42376
42411
|
if (alignment.autoMergeEnqueued) {
|
|
42377
42412
|
return `${label}: alignment PR #${alignment.prNumber ?? "?"} AUTO-MERGE ENQUEUED (merges when checks pass)${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
|
|
42378
42413
|
}
|
|
42379
|
-
return `${label}: ALIGNMENT PR PENDING
|
|
42414
|
+
return `${label}: ALIGNMENT PR PENDING \u2014 land it with \`mmi-cli devops pr merge ${alignment.prNumber ?? "<number>"} --auto --merge\`${alignment.prUrl ? ` (${alignment.prUrl})` : ""}`;
|
|
42380
42415
|
}
|
|
42381
42416
|
function renderTrainApply(commandName, r) {
|
|
42382
|
-
let base = `mmi-cli ${commandName}: promoted ${r.repo}
|
|
42417
|
+
let base = `mmi-cli ${commandName}: promoted ${r.repo} \u2014 ${r.stage} at ${r.tag} [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
42383
42418
|
if (r.versionFold) base = `${base}; ${r.versionFold}`;
|
|
42384
42419
|
if (r.resumeNote) base = `${base}; ${r.resumeNote}`;
|
|
42385
42420
|
if (r.devNote) base = `${base}; ${r.devNote}`;
|
|
@@ -42442,16 +42477,16 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
42442
42477
|
return fail(`${commandName}: ${e.message}`);
|
|
42443
42478
|
}
|
|
42444
42479
|
if (o.ack && commandName !== "release") {
|
|
42445
|
-
return fail(`${commandName}: --ack applies only to release
|
|
42480
|
+
return fail(`${commandName}: --ack applies only to release \u2014 it overrides the rc -> main hotfix-coverage guard, which rcand does not run. Run: mmi-cli devops release --ack <shas>`);
|
|
42446
42481
|
}
|
|
42447
42482
|
if (o.dev && commandName !== "release") {
|
|
42448
|
-
return fail(`${commandName}: --dev applies only to release
|
|
42483
|
+
return fail(`${commandName}: --dev applies only to release \u2014 it ships development -> main skipping rc, which rcand cannot do. Run: mmi-cli devops release --dev`);
|
|
42449
42484
|
}
|
|
42450
42485
|
if (o.announceSummaryFile && commandName !== "release") {
|
|
42451
|
-
return fail(`${commandName}: --announce-summary-file applies only to release
|
|
42486
|
+
return fail(`${commandName}: --announce-summary-file applies only to release \u2014 rcand posts no Hub Slack announcement. Run: mmi-cli devops release --announce-summary-file <path>`);
|
|
42452
42487
|
}
|
|
42453
42488
|
if (o.abort && commandName !== "release") {
|
|
42454
|
-
return fail(`${commandName}: --abort applies only to release
|
|
42489
|
+
return fail(`${commandName}: --abort applies only to release \u2014 it rolls back a proven unpublished Hub release tag. Run: mmi-cli devops release --abort --apply`);
|
|
42455
42490
|
}
|
|
42456
42491
|
if (o.retryPublish && commandName !== "release") {
|
|
42457
42492
|
return fail(`${commandName}: --retry-publish applies only to release. Run: mmi-cli devops release --retry-publish <run-id> --apply --watch`);
|
|
@@ -42481,7 +42516,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
42481
42516
|
}
|
|
42482
42517
|
}
|
|
42483
42518
|
if (o.abort) {
|
|
42484
|
-
if (o.resume) return fail("release: --abort and --resume are mutually exclusive
|
|
42519
|
+
if (o.resume) return fail("release: --abort and --resume are mutually exclusive \u2014 abort removes an unpublished tag while resume preserves and promotes it");
|
|
42485
42520
|
if (!o.apply) return fail("release: --abort requires --apply after explicit approval; nothing was written");
|
|
42486
42521
|
if (o.watch || o.announceSummaryFile || o.ack || o.dev) {
|
|
42487
42522
|
return fail("release: --abort accepts only --apply, --repo and --json; promotion flags cannot be combined with rollback");
|
|
@@ -42498,7 +42533,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
42498
42533
|
}
|
|
42499
42534
|
}
|
|
42500
42535
|
if (o.resume) {
|
|
42501
|
-
if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive
|
|
42536
|
+
if (o.apply) return fail(`${commandName}: --resume and --apply are mutually exclusive \u2014 --apply cuts the NEXT version, --resume finishes the immutable tag already on origin`);
|
|
42502
42537
|
try {
|
|
42503
42538
|
if (commandName === "rcand") {
|
|
42504
42539
|
const result2 = await runRcandResume(trainApplyDeps(), { watch: o.watch });
|
|
@@ -42554,7 +42589,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
42554
42589
|
projectInfoSync = await runProjectInfoSync(result.repo, true);
|
|
42555
42590
|
} catch (e) {
|
|
42556
42591
|
const error = e.message;
|
|
42557
|
-
projectInfoSync = { applied: false, note: `FAILED
|
|
42592
|
+
projectInfoSync = { applied: false, note: `FAILED \u2014 ${error}`, error };
|
|
42558
42593
|
}
|
|
42559
42594
|
}
|
|
42560
42595
|
const alignmentPending = result.devRollForward?.status === "pr-pending" || result.rcAlignment?.status === "pr-pending";
|
|
@@ -42596,7 +42631,7 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
42596
42631
|
}
|
|
42597
42632
|
if (!read.ok) {
|
|
42598
42633
|
return failGraceful(
|
|
42599
|
-
`${commandName}: Hub registry read failed for ${slugOf(repo)} (${read.error})
|
|
42634
|
+
`${commandName}: Hub registry read failed for ${slugOf(repo)} (${read.error}) \u2014 the release track behind this plan is unproven, and a dry-run plan built on the isHub fallback would name the wrong branches. Likely transient (cold start, network, or auth blip); retry shortly.`
|
|
42600
42635
|
);
|
|
42601
42636
|
}
|
|
42602
42637
|
const raw = read.project?.releaseTrack;
|
|
@@ -42622,13 +42657,13 @@ function renderHotfixRelease(r) {
|
|
|
42622
42657
|
` - ${r.verifyNote}`,
|
|
42623
42658
|
...r.announceNote ? [` - announce: ${r.announceNote}`] : [],
|
|
42624
42659
|
` - fold: ${r.foldNote}`,
|
|
42625
|
-
` - next: mmi-cli devops hotfix status ${r.tag} (no back-merge of the FIX
|
|
42660
|
+
` - next: mmi-cli devops hotfix status ${r.tag} (no back-merge of the FIX \u2014 development already has it; the version fold above is ported for you, #4410)`
|
|
42626
42661
|
].join("\n");
|
|
42627
42662
|
}
|
|
42628
42663
|
function renderHotfixStatus(r) {
|
|
42629
42664
|
return [
|
|
42630
|
-
`mmi-cli devops hotfix status: ${r.tag} on ${r.repo}
|
|
42631
|
-
` - branch: ${r.branchExists ? "pushed" : "absent"}
|
|
42665
|
+
`mmi-cli devops hotfix status: ${r.tag} on ${r.repo} \u2014 ${r.state}`,
|
|
42666
|
+
` - branch: ${r.branchExists ? "pushed" : "absent"} \u2014 PR: ${r.pr ? `#${r.pr.number} ${r.pr.state}` : "none"} \u2014 tag: ${r.tagPushed ? "pushed" : "absent"} \u2014 Release: ${r.releaseExists ? "exists" : "absent"}`,
|
|
42632
42667
|
...r.runs.map((run) => ` - ${run.workflow}: ${run.conclusion}${run.url ? ` (${run.url})` : ""}`),
|
|
42633
42668
|
` - npm @mutmutco/cli: ${r.npmVersion}`,
|
|
42634
42669
|
` - next: ${r.next}`,
|
|
@@ -42658,13 +42693,13 @@ async function runHotfixSub(sub, body, json, render) {
|
|
|
42658
42693
|
return failGraceful(`hotfix ${sub}: ${e.message}`);
|
|
42659
42694
|
}
|
|
42660
42695
|
}
|
|
42661
|
-
var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestrator: start
|
|
42696
|
+
var hotfixCmd = program2.command("hotfix").description("stepwise hotfix orchestrator: start \u2014 release, with status (bare command prints the dry-run plan; no back-merge \u2014 #839)").option("--json", "machine-readable output").option("--apply", "not a verb; use the hotfix subcommands (start/release/status)").action(async (o) => {
|
|
42662
42697
|
try {
|
|
42663
42698
|
await requireFreshTrainCli("hotfix");
|
|
42664
42699
|
} catch (e) {
|
|
42665
42700
|
return fail(`hotfix: ${e.message}`);
|
|
42666
42701
|
}
|
|
42667
|
-
if (o.apply) return fail("hotfix: use the stepwise subcommands
|
|
42702
|
+
if (o.apply) return fail("hotfix: use the stepwise subcommands \u2014 mmi-cli devops hotfix start --from <pr#|sha> \u2014 release <vX.Y.Z> \u2014 status [vX.Y.Z]");
|
|
42668
42703
|
const steps = trainPlan("hotfix");
|
|
42669
42704
|
console.log(o.json ? JSON.stringify({ command: "hotfix", steps }, null, 2) : renderSteps("mmi-cli devops hotfix: dry-run plan", steps));
|
|
42670
42705
|
});
|
|
@@ -42696,7 +42731,7 @@ ci.command("audit").description("read-only fleet scan: gate workflow, ruleset co
|
|
|
42696
42731
|
else console.log(renderCiAuditText(report));
|
|
42697
42732
|
if (!report.ok) process.exitCode = 1;
|
|
42698
42733
|
});
|
|
42699
|
-
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required); activation is skipped while the repo's gate has never passed (#3694)").option("--park-ruleset", "stop the product ruleset enforcing without deleting it
|
|
42734
|
+
ci.command("reconcile").description("audit + optionally apply merge settings and product ruleset activation (master-admin)").option("--json", "machine-readable output").option("--repo <owner/repo>", "reconcile one repo instead of the full registry").option("--apply", "PATCH merge settings + activate product ruleset when missing (master role required); activation is skipped while the repo's gate has never passed (#3694)").option("--park-ruleset", "stop the product ruleset enforcing without deleting it \u2014 keeps its required contexts (master role required)").action(async (o) => {
|
|
42700
42735
|
if (o.apply && o.parkRuleset) {
|
|
42701
42736
|
return fail("ci reconcile: --apply and --park-ruleset ask for opposite things; pass one");
|
|
42702
42737
|
}
|
|
@@ -42719,7 +42754,7 @@ ci.command("reconcile").description("audit + optionally apply merge settings and
|
|
|
42719
42754
|
${r.repo}: applied=[${r.applied.join("; ")}] skipped=[${r.skipped.join("; ")}]${r.errors.length ? ` errors=[${r.errors.join("; ")}]` : ""}`);
|
|
42720
42755
|
}
|
|
42721
42756
|
} else {
|
|
42722
|
-
console.log("\nDry-run
|
|
42757
|
+
console.log("\nDry-run \u2014 re-run with --apply to patch merge settings and activate product rulesets (master-admin).");
|
|
42723
42758
|
}
|
|
42724
42759
|
}
|
|
42725
42760
|
const applyFailed = applyResults.some((result) => result.errors.length > 0 || result.postApply?.state === "failed");
|
|
@@ -42747,7 +42782,7 @@ access.command("role [repo]").description("D14 train authority for a repo (serve
|
|
|
42747
42782
|
if (!a.train) process.exitCode = 1;
|
|
42748
42783
|
return;
|
|
42749
42784
|
}
|
|
42750
|
-
console.log(`${a.repo}: @${a.login} is ${a.role}
|
|
42785
|
+
console.log(`${a.repo}: @${a.login} is ${a.role} \u2014 train ${a.train ? "AUTHORIZED" : "not authorized"}${a.hubTrainMasterOnly ? " (Hub train is master-only)" : ""}`);
|
|
42751
42786
|
if (!a.train) process.exitCode = 1;
|
|
42752
42787
|
});
|
|
42753
42788
|
access.command("audit").description("audit collaborator roles + train-branch push allowlists vs the locked state; read-only, emits gh remediation, never applies").option("--json", "machine-readable output").option("--repo <owner/repo>", "audit a single repo instead of the whole org").option("--class <class>", "repo class for --repo (deployable | content)", "deployable").action(async () => {
|
|
@@ -42782,9 +42817,9 @@ access.command("audit").description("audit collaborator roles + train-branch pus
|
|
|
42782
42817
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderAccessReport(report));
|
|
42783
42818
|
if (!report.ok) process.exitCode = 1;
|
|
42784
42819
|
});
|
|
42785
|
-
access.command("capabilities").description("enumerate your effective vault reach
|
|
42820
|
+
access.command("capabilities").description("enumerate your effective vault reach \u2014 every credential NAME + tier + scope you can read/use across project + org/master tiers (names only, no values) (#1615)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "machine-readable output").action((o) => withSecrets((d) => secretsCapabilities(d, o)));
|
|
42786
42821
|
var isWin2 = process.platform === "win32";
|
|
42787
|
-
program2.command("doctor").description("heal CLI/plugin wiring and clean up repo cruft
|
|
42822
|
+
program2.command("doctor").description("heal CLI/plugin wiring and clean up repo cruft \u2014 repairs run by default (#3975); use --verbose for the full audit checklist").option("--banner", "one-line resume summary; silent when all gates pass").option("--fast", "offline-safe fast lane; read-only, skips slow checks and network probes").option("--preflight", "read-only fast gate for automation: measures and reports with the shared exit code, zero writes (#4199, canon); heal env drift with a plain run or --no-repo-writes").option("--verbose", "print the evidence behind every check \u2014 probes, resolved paths, versions compared, and the names behind each count (#2977)").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output (an output format \u2014 repairs still run by lane)").option("--apply", "deprecated no-op: repairs run by default now (#3975); kept so older instructions still parse").option("--no-repo-writes", "env/plugin repairs only \u2014 never mutate the repo working tree; report pending managed .gitignore repairs with the follow-up command (for train preflights)").option("--self", "verify CLI/plugin version parity and gh auth reach; suggests plugin-heal on a hard gap (reads the published version, so not offline-safe) (#2689)").addHelpText("after", "\nExit codes:\n 0 no hard gaps remain; advisory gaps may exist\n 1 one or more included hard checks failed\n\nA plain run heals env drift (CLI, plugin, marketplace pins) and cleans repo cruft (gitignore block,\nmerged branches, dead worktrees, aged scratch) automatically (#3975). --no-repo-writes keeps the\nworking tree untouched for train preflights; --banner/--fast/--self are read-only lanes.\n\n--banner keeps the legacy SessionStart contract and returns 0 unless the process crashes.\n").action(async (opts) => {
|
|
42788
42823
|
if (opts.guide) {
|
|
42789
42824
|
consoleIo.log("MMI Agentic Onboarding: docs/Architecture/agentic-dev-environment.md");
|
|
42790
42825
|
return;
|
|
@@ -42938,7 +42973,7 @@ program2.command("plugin-release-catchup").description("install a newer released
|
|
|
42938
42973
|
});
|
|
42939
42974
|
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
|
|
42940
42975
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
42941
|
-
console.error("[mmi-hook] plugin session-start: cwd is a repository SUBDIRECTORY
|
|
42976
|
+
console.error("[mmi-hook] plugin session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
42942
42977
|
appendHookActivity(process.cwd(), { event: "SessionStart", script: "session-start", outcome: "ran", action: "skip (repo subdirectory)" });
|
|
42943
42978
|
return;
|
|
42944
42979
|
}
|
|
@@ -42951,7 +42986,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
42951
42986
|
const bannerIo = measured.io;
|
|
42952
42987
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
42953
42988
|
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
42954
|
-
// when unknown
|
|
42989
|
+
// when unknown — a missing gh login must not noise or fail the banner.
|
|
42955
42990
|
whoami: async (io) => {
|
|
42956
42991
|
const report = await resolveWhoami({
|
|
42957
42992
|
hubSession: async () => hubAuthSession({ baseUrl: (await loadConfig()).sagaApiUrl ?? defaultHubUrl(), githubToken }),
|
|
@@ -42963,14 +42998,14 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
42963
42998
|
if (authority) io.log(authority);
|
|
42964
42999
|
},
|
|
42965
43000
|
// command-ladder hint (#2609): compact gh?mmi-cli cheat sheet so agents reach for mmi-cli first,
|
|
42966
|
-
// not after a denied gh call. <1KB, pure in-memory
|
|
43001
|
+
// not after a denied gh call. <1KB, pure in-memory — no network, no fs, fail-soft.
|
|
42967
43002
|
commandLadderHint: async (io) => {
|
|
42968
43003
|
const hint = commandLadderHint();
|
|
42969
43004
|
if (hint) io.log(hint);
|
|
42970
43005
|
},
|
|
42971
43006
|
// stale worktrees: fast LOCAL-only git check (worktree list + branch -vv gone status). Flags leaked
|
|
42972
43007
|
// worktrees / merged-but-undeleted branches so the session never starts atop leftover state. No
|
|
42973
|
-
// network
|
|
43008
|
+
// network — never the ~20s `gh pr list` path of `worktree list --stale`. Fail-soft.
|
|
42974
43009
|
staleWorktrees: async (io) => {
|
|
42975
43010
|
const line = await gatherStaleWorktreeWarning();
|
|
42976
43011
|
if (line) io.log(line);
|
|
@@ -42983,7 +43018,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
42983
43018
|
const line = localTrainSyncBannerLine(result);
|
|
42984
43019
|
if (line) io.log(line);
|
|
42985
43020
|
},
|
|
42986
|
-
// #3485 item 7: the ONLY lane that throttles the npm read
|
|
43021
|
+
// #3485 item 7: the ONLY lane that throttles the npm read — it runs on every session start, on a
|
|
42987
43022
|
// blocking hook. Every interactive lane still reads live.
|
|
42988
43023
|
doctor: async (io) => {
|
|
42989
43024
|
await runDoctorClean({ banner: true }, io, mmiDoctorDeps({ throttleReleasedRead: true, program: program2 }));
|