@mutmutco/codex-plugin 4.5.86 → 4.5.88
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/.codex-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +509 -437
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/board/SKILL.md +11 -3
- package/skills/bootstrap/SKILL.md +1 -1
- package/skills/hotfix/SKILL.md +1 -1
- package/skills/mmi-help/SKILL.md +1 -1
- package/skills/org-guide/references/getting-started.md +1 -1
- package/skills/rcand/SKILL.md +1 -1
- package/skills/release/SKILL.md +1 -1
- package/skills/secrets/SKILL.md +1 -1
- package/skills/stage/SKILL.md +1 -1
package/cli/dist/main.cjs
CHANGED
|
@@ -7447,25 +7447,6 @@ function workflowEntry(repo, workflowPath, yamlText) {
|
|
|
7447
7447
|
harbourUnmanaged: isHarbourUnmanaged(yamlText)
|
|
7448
7448
|
};
|
|
7449
7449
|
}
|
|
7450
|
-
function decorateAwsModel(entries, registry2, verdicts) {
|
|
7451
|
-
if (!registry2 || registry2.length === 0) return [...entries];
|
|
7452
|
-
const rowById = new Map(registry2.map((r) => [r.id, r]));
|
|
7453
|
-
const literalById = /* @__PURE__ */ new Map();
|
|
7454
|
-
for (const v of verdicts ?? []) {
|
|
7455
|
-
if (typeof v.modelId === "string" && v.modelId.trim() && !literalById.has(v.id)) {
|
|
7456
|
-
literalById.set(v.id, v.modelId);
|
|
7457
|
-
}
|
|
7458
|
-
}
|
|
7459
|
-
return entries.map((e) => {
|
|
7460
|
-
if (!e.scheduleId) return e;
|
|
7461
|
-
const row = rowById.get(e.scheduleId);
|
|
7462
|
-
if (!row || row.executor !== "cursor-agent") return e;
|
|
7463
|
-
const literal = literalById.get(e.scheduleId);
|
|
7464
|
-
if (literal) return { ...e, model: literal };
|
|
7465
|
-
if (row.model) return { ...e, model: `role: ${row.model}` };
|
|
7466
|
-
return { ...e, model: "org-default" };
|
|
7467
|
-
});
|
|
7468
|
-
}
|
|
7469
7450
|
var DECLARED_ENTRIES = [
|
|
7470
7451
|
{ name: "mmi-backup (mm-central, mm-work, mmi-oguz)", cadence: "17 2 * * *", executor: "box cron.d", llm: "no", resolved: "declared", source: "/etc/cron.d/mmi-backup \u2192 /opt/mmi-control/nightly-backup.sh; every Docker volume is tarred and every running Postgres container is pg_dumpall'd beside it (#7147; source scripts/nightly-backup.sh); zuber (46.225.58.31) is gone: deleted 2026-09-18 by the owner-approved ZuberShade decommission (MMC-ZuberShade#1838, snapshot kept); Hetzner has since reassigned that IP to another machine (#7351)" },
|
|
7471
7452
|
// #7148: a backup nobody restored is a hope. Monthly, restore the latest artifact of every
|
|
@@ -7511,12 +7492,10 @@ function formatSchedulesTable(entries, now = /* @__PURE__ */ new Date()) {
|
|
|
7511
7492
|
const cadW = w((e) => e.cadence, "CADENCE");
|
|
7512
7493
|
const exeW = w((e) => e.executor, "EXECUTOR");
|
|
7513
7494
|
const llmW = w((e) => e.llm, "LLM");
|
|
7514
|
-
const modelOf = (e) => e.model ?? "-";
|
|
7515
|
-
const modW = w((e) => modelOf(e), "MODEL");
|
|
7516
7495
|
const resW = w((e) => resolvedOf.get(e) ?? e.resolved, "RESOLVED");
|
|
7517
|
-
const row = (n, c, x, l,
|
|
7518
|
-
const lines2 = [row("JOB", "CADENCE", "EXECUTOR", "LLM", "
|
|
7519
|
-
for (const e of entries) lines2.push(row(e.name, e.cadence, e.executor, e.llm,
|
|
7496
|
+
const row = (n, c, x, l, r, s) => `${n.padEnd(nameW)} ${c.padEnd(cadW)} ${x.padEnd(exeW)} ${l.padEnd(llmW)} ${r.padEnd(resW)} ${s}`;
|
|
7497
|
+
const lines2 = [row("JOB", "CADENCE", "EXECUTOR", "LLM", "RESOLVED", "SOURCE")];
|
|
7498
|
+
for (const e of entries) lines2.push(row(e.name, e.cadence, e.executor, e.llm, resolvedOf.get(e) ?? e.resolved, e.source));
|
|
7520
7499
|
return lines2.join("\n");
|
|
7521
7500
|
}
|
|
7522
7501
|
function withTickState(entry, now = /* @__PURE__ */ new Date()) {
|
|
@@ -7524,11 +7503,11 @@ function withTickState(entry, now = /* @__PURE__ */ new Date()) {
|
|
|
7524
7503
|
}
|
|
7525
7504
|
function renderDocSection(entries, generatedAtIso) {
|
|
7526
7505
|
const lines2 = [
|
|
7527
|
-
"| Job | Cadence | Executor | LLM |
|
|
7528
|
-
"
|
|
7506
|
+
"| Job | Cadence | Executor | LLM | Resolved | Source |",
|
|
7507
|
+
"|---|---|---|---|---|---|"
|
|
7529
7508
|
];
|
|
7530
7509
|
for (const e of entries) {
|
|
7531
|
-
lines2.push(`| ${e.name} | \`${e.cadence}\` | ${e.executor} | ${e.llm} | ${e.
|
|
7510
|
+
lines2.push(`| ${e.name} | \`${e.cadence}\` | ${e.executor} | ${e.llm} | ${e.resolved} | ${e.source} |`);
|
|
7532
7511
|
}
|
|
7533
7512
|
return [
|
|
7534
7513
|
DOC_START_MARKER,
|
|
@@ -7546,10 +7525,6 @@ function spliceDoc(docText, generatedSection) {
|
|
|
7546
7525
|
}
|
|
7547
7526
|
return docText.slice(0, start) + generatedSection + docText.slice(end + DOC_END_MARKER.length);
|
|
7548
7527
|
}
|
|
7549
|
-
var HARBOUR_LLM_LAUNCHERS = /* @__PURE__ */ new Set(["cursor-agent"]);
|
|
7550
|
-
function isHarbourLlmLauncher(executor) {
|
|
7551
|
-
return HARBOUR_LLM_LAUNCHERS.has(executor);
|
|
7552
|
-
}
|
|
7553
7528
|
function splitCadence(cadence) {
|
|
7554
7529
|
return cadence.split("+").map((c) => c.replace(/\s+/g, " ").trim()).filter(Boolean);
|
|
7555
7530
|
}
|
|
@@ -7659,15 +7634,13 @@ function strayCronDrifts(workflows) {
|
|
|
7659
7634
|
}
|
|
7660
7635
|
function unlauncheredLlmDrift(entry) {
|
|
7661
7636
|
if (entry.llm !== "yes") return null;
|
|
7662
|
-
if (isHarbourLlmLauncher(entry.executor)) return null;
|
|
7663
7637
|
if (entry.executor === "aws-scheduler \u2192 mmi-hub-schedule-dispatcher") return null;
|
|
7664
|
-
const allowed = [...HARBOUR_LLM_LAUNCHERS].join(", ");
|
|
7665
7638
|
return {
|
|
7666
7639
|
class: "unlaunchered-llm",
|
|
7667
7640
|
name: entry.name,
|
|
7668
7641
|
executor: entry.executor,
|
|
7669
|
-
detail:
|
|
7670
|
-
remedy:
|
|
7642
|
+
detail: "llm: yes but no scheduled-LLM executor is supported (Cursor retired, #7779; Codex executor retired, #7821)",
|
|
7643
|
+
remedy: "retire the lane or set it to llm: no until the harbour redesign (#7821; docs/schedules.md rule 3)"
|
|
7671
7644
|
};
|
|
7672
7645
|
}
|
|
7673
7646
|
function unlauncheredLlmDrifts(entries) {
|
|
@@ -7739,8 +7712,6 @@ var SCHEDULE_HEADER_FIELDS = ["schedule", "what", "owner", "cadence", "executor"
|
|
|
7739
7712
|
var SCHEDULE_EXECUTORS = /* @__PURE__ */ new Set([
|
|
7740
7713
|
"github-actions",
|
|
7741
7714
|
// deterministic workflow — clocked by the harbour, fired via workflow_dispatch
|
|
7742
|
-
"cursor-agent",
|
|
7743
|
-
// LLM lane — the harbour's ONLY launcher (docs/schedules.md rule 5)
|
|
7744
7715
|
"lambda",
|
|
7745
7716
|
// fleet function row — requires the `# target: mmi-fleet-*` header (#3205/#3246)
|
|
7746
7717
|
"box systemd",
|
|
@@ -7821,8 +7792,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
7821
7792
|
if (llmFromHeader(yamlText) === "unknown") {
|
|
7822
7793
|
throw new Error(`schedules register: ${expectedId} header \`llm: ${h.llm}\` does not classify (yes / no / embeddings) \u2014 an unreadable declaration would register a row whose LLM column lies.`);
|
|
7823
7794
|
}
|
|
7824
|
-
if (llmFromHeader(yamlText) === "yes"
|
|
7825
|
-
throw new Error(`schedules register: ${expectedId} header \`llm: yes\`
|
|
7795
|
+
if (llmFromHeader(yamlText) === "yes") {
|
|
7796
|
+
throw new Error(`schedules register: ${expectedId} header \`llm: yes\` is refused \u2014 no scheduled-LLM executor is supported: Cursor (\`cursor-agent\`) was retired (#7779) and the Codex executor was retired (#7821). Set \`llm: no\` or retire the lane until the harbour redesign.`);
|
|
7826
7797
|
}
|
|
7827
7798
|
const target = header.target;
|
|
7828
7799
|
if (target !== void 0 && h.executor !== "lambda") {
|
|
@@ -7831,9 +7802,8 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
7831
7802
|
if (target !== void 0 && !/^mmi-fleet-[A-Za-z0-9_-]{1,130}$/.test(target)) {
|
|
7832
7803
|
throw new Error(`schedules register: ${expectedId} header \`target: ${target}\` must be a bare lambda function name in the reserved fleet namespace \`mmi-fleet-*\` (allowed after the prefix: A-Z a-z 0-9 _ -; total max 140). The dispatcher can only invoke mmi-fleet-* functions.`);
|
|
7833
7804
|
}
|
|
7834
|
-
|
|
7835
|
-
|
|
7836
|
-
throw new Error(`schedules register: ${expectedId} header \`model: ${model}\` must be a model ROLE KEY (lowercase kebab, max 31 chars \u2014 e.g. janitor, arbiter), never a model id \u2014 ids resolve from the vault at dispatch.`);
|
|
7805
|
+
if (header.model !== void 0) {
|
|
7806
|
+
throw new Error(`schedules register: ${expectedId} header \`model: ${header.model}\` is refused \u2014 a model role key is valid only on an LLM lane, and no scheduled-LLM executor is supported (Cursor retired #7779; Codex executor retired #7821). Remove the line.`);
|
|
7837
7807
|
}
|
|
7838
7808
|
return {
|
|
7839
7809
|
id: expectedId,
|
|
@@ -7847,8 +7817,7 @@ function scheduleRecordFromWorkflow(repo, workflowPath, yamlText) {
|
|
|
7847
7817
|
kill: h.kill,
|
|
7848
7818
|
repo,
|
|
7849
7819
|
sourcePath: workflowPath,
|
|
7850
|
-
...target !== void 0 ? { target } : {}
|
|
7851
|
-
...model !== void 0 ? { model } : {}
|
|
7820
|
+
...target !== void 0 ? { target } : {}
|
|
7852
7821
|
};
|
|
7853
7822
|
}
|
|
7854
7823
|
function scheduleRecordsFromWorkflows(repo, files) {
|
|
@@ -7993,22 +7962,6 @@ async function fetchSchedulesList(deps) {
|
|
|
7993
7962
|
return null;
|
|
7994
7963
|
}
|
|
7995
7964
|
}
|
|
7996
|
-
async function fetchRunVerdicts(deps) {
|
|
7997
|
-
if (!deps.baseUrl) return null;
|
|
7998
|
-
const token = await deps.token();
|
|
7999
|
-
if (!token) return null;
|
|
8000
|
-
try {
|
|
8001
|
-
const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/schedules/verdicts`, {
|
|
8002
|
-
method: "GET",
|
|
8003
|
-
headers: { Authorization: `Bearer ${token}` }
|
|
8004
|
-
});
|
|
8005
|
-
if (!res.ok) return null;
|
|
8006
|
-
const body = await res.json();
|
|
8007
|
-
return Array.isArray(body?.runVerdicts) ? body.runVerdicts : null;
|
|
8008
|
-
} catch {
|
|
8009
|
-
return null;
|
|
8010
|
-
}
|
|
8011
|
-
}
|
|
8012
7965
|
async function fetchOrgConfig(deps) {
|
|
8013
7966
|
if (!deps.baseUrl) return null;
|
|
8014
7967
|
const token = await deps.token();
|
|
@@ -11324,9 +11277,6 @@ function detectSurface(env) {
|
|
|
11324
11277
|
if (env.MMI_AGENT_SURFACE === "jervcode" || has("PI_SESSION_ID")) {
|
|
11325
11278
|
return "jervcode";
|
|
11326
11279
|
}
|
|
11327
|
-
if (env.MMI_AGENT_SURFACE === "cursor" || has("CURSOR_TRACE_ID") || has("CURSOR_USER") || has("CURSOR_SESSION_ID") || env.CURSOR_AGENT === "1" || has("CURSOR_EXTENSION_HOST_ROLE")) {
|
|
11328
|
-
return "cursor";
|
|
11329
|
-
}
|
|
11330
11280
|
if (env.MMI_AGENT_SURFACE === "opencode" || has("OPENCODE_CLIENT") || has("OPENCODE_SERVER_USERNAME")) return "opencode";
|
|
11331
11281
|
const isClaude = has("CLAUDECODE") || has("CLAUDE_CODE_ENTRYPOINT") || has("CLAUDE_PLUGIN_ROOT") || env.MMI_AGENT_SURFACE === "claude";
|
|
11332
11282
|
const isVscode = env.TERM_PROGRAM === "vscode" || has("VSCODE_PID") || has("VSCODE_GIT_ASKPASS_NODE");
|
|
@@ -11341,8 +11291,6 @@ function surfaceToken(surface) {
|
|
|
11341
11291
|
return "claude";
|
|
11342
11292
|
case "codex":
|
|
11343
11293
|
return "codex";
|
|
11344
|
-
case "cursor":
|
|
11345
|
-
return "cursor";
|
|
11346
11294
|
case "jervcode":
|
|
11347
11295
|
return "jervcode";
|
|
11348
11296
|
case "hermes":
|
|
@@ -11362,8 +11310,6 @@ function reloadAction(surface) {
|
|
|
11362
11310
|
return "restart Codex";
|
|
11363
11311
|
case "opencode":
|
|
11364
11312
|
return "restart OpenCode";
|
|
11365
|
-
case "cursor":
|
|
11366
|
-
return "reload the Cursor window";
|
|
11367
11313
|
case "jervcode":
|
|
11368
11314
|
return "restart JervCode (the package set is read at launch)";
|
|
11369
11315
|
case "hermes":
|
|
@@ -11376,7 +11322,6 @@ function reloadAction(surface) {
|
|
|
11376
11322
|
}
|
|
11377
11323
|
var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub --ref main && claude plugin install mmi@mutmutco`;
|
|
11378
11324
|
var CODEX_RECOVERY = "codex plugin remove mmi@mutmutco && codex plugin marketplace remove mutmutco && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mutmutco";
|
|
11379
|
-
var CURSOR_RECOVERY = "mmi-cli plugin heal # installs ~/.cursor/plugins/local/mmi from mutmutco/MMI-Hub";
|
|
11380
11325
|
var PLUGIN_SURFACE_HEAL = {
|
|
11381
11326
|
claude: {
|
|
11382
11327
|
delivery: "plugin-cli",
|
|
@@ -11402,23 +11347,13 @@ var PLUGIN_SURFACE_HEAL = {
|
|
|
11402
11347
|
],
|
|
11403
11348
|
fix: (surface) => `${CODEX_RECOVERY} # then ${reloadAction(surface)} to load MMI commands`,
|
|
11404
11349
|
updateRecipe: [CODEX_RECOVERY]
|
|
11405
|
-
},
|
|
11406
|
-
cursor: {
|
|
11407
|
-
delivery: "local-checkout",
|
|
11408
|
-
recovery: CURSOR_RECOVERY,
|
|
11409
|
-
healSteps: null,
|
|
11410
|
-
fix: (surface) => `${CURSOR_RECOVERY} # then ${reloadAction(surface)}`,
|
|
11411
|
-
updateRecipe: [CURSOR_RECOVERY]
|
|
11412
11350
|
}
|
|
11413
11351
|
};
|
|
11414
11352
|
function nonClaudeSurfaceHealMessage(surface) {
|
|
11415
11353
|
if (surface === "codex") {
|
|
11416
11354
|
return "Codex ships an MMI plugin (#3563). Install or repair it with:\n codex plugin marketplace add mutmutco/MMI-Hub && codex plugin add mmi@mutmutco\n Update the CLI too: npm i -g @mutmutco/cli";
|
|
11417
11355
|
}
|
|
11418
|
-
|
|
11419
|
-
return "Cursor ships an MMI plugin (#3920). Install or repair its managed local checkout with:\n mmi-cli plugin heal\n Then reload the Cursor window. For one-off CLI use, pass --plugin-dir <MMI-Hub checkout>.\n Update the CLI too: npm i -g @mutmutco/cli";
|
|
11420
|
-
}
|
|
11421
|
-
return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, Cursor, JervCode, and Hermes only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
|
|
11356
|
+
return "No Hub-shipped MMI plugin on this host \u2014 the Hub ships plugins for Claude, Codex, JervCode, and Hermes only.\n Update the CLI instead: npm i -g @mutmutco/cli (org rules ride an AGENTS.md authored outside the Hub)";
|
|
11422
11357
|
}
|
|
11423
11358
|
function healStepAborts(step, ok) {
|
|
11424
11359
|
return !ok && step.gated;
|
|
@@ -11468,7 +11403,6 @@ function hermesConfigRoot(env = process.env, home = (0, import_node_os7.homedir)
|
|
|
11468
11403
|
}
|
|
11469
11404
|
function surfaceConfigRoot(surface, env = process.env, home = (0, import_node_os7.homedir)()) {
|
|
11470
11405
|
if (surface === "codex") return env.CODEX_HOME?.trim() || (0, import_node_path10.join)(home, ".codex");
|
|
11471
|
-
if (surface === "cursor") return (0, import_node_path10.join)(home, ".cursor");
|
|
11472
11406
|
if (surface === "hermes") return hermesConfigRoot(env, home);
|
|
11473
11407
|
if (surface === "jervcode") {
|
|
11474
11408
|
return env.JERVCODE_CODING_AGENT_DIR?.trim() || env.PI_CODING_AGENT_DIR?.trim() || (0, import_node_path10.join)(home, ".jerv", "agent");
|
|
@@ -11493,7 +11427,7 @@ function marketplaceCloneCandidates(surface, home, env = process.env) {
|
|
|
11493
11427
|
(0, import_node_path10.join)(root, "plugins", "marketplaces", CODEX_MARKETPLACE)
|
|
11494
11428
|
];
|
|
11495
11429
|
}
|
|
11496
|
-
if (surface === "
|
|
11430
|
+
if (surface === "hermes") return [];
|
|
11497
11431
|
if (surface === "jervcode") return [];
|
|
11498
11432
|
return [(0, import_node_path10.join)(home, ".claude", "plugins", "marketplaces", "mutmutco")];
|
|
11499
11433
|
}
|
|
@@ -11538,15 +11472,6 @@ async function fetchNpmReleasedVersion() {
|
|
|
11538
11472
|
return void 0;
|
|
11539
11473
|
}
|
|
11540
11474
|
}
|
|
11541
|
-
function cursorLocalPluginRoot(env = process.env, home = (0, import_node_os7.homedir)()) {
|
|
11542
|
-
return (0, import_node_path10.join)(surfaceConfigRoot("cursor", env, home), "plugins", "local", "mmi");
|
|
11543
|
-
}
|
|
11544
|
-
function cursorPluginTreeHealthy(root, exists = import_node_fs12.existsSync) {
|
|
11545
|
-
return [
|
|
11546
|
-
".cursor-plugin/plugin.json",
|
|
11547
|
-
"skills/bootstrap/SKILL.md"
|
|
11548
|
-
].every((path2) => exists((0, import_node_path10.join)(root, ...path2.split("/"))));
|
|
11549
|
-
}
|
|
11550
11475
|
var JERVCODE_NPM_PACKAGE = "@mutmutco/pi-plugin";
|
|
11551
11476
|
function jervcodeAgentDirs(env = process.env, home = (0, import_node_os7.homedir)()) {
|
|
11552
11477
|
const primary = surfaceConfigRoot("jervcode", env, home);
|
|
@@ -11633,10 +11558,10 @@ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRep
|
|
|
11633
11558
|
// Codex's native projection is authoritative; its legacy installed_plugins.json may be empty
|
|
11634
11559
|
// while `plugin list --json` proves the managed plugin installed (#6111).
|
|
11635
11560
|
installRecordPresent: surface === "codex" ? codexStatus?.installed === true : hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()) || // #4188: jervcode's install record is the settings-file packages[] entry itself.
|
|
11636
|
-
surface === "jervcode" && piEntry !== null || surface === "
|
|
11561
|
+
surface === "jervcode" && piEntry !== null || surface === "hermes" && (0, import_node_fs12.existsSync)(hermesPluginRoot()),
|
|
11637
11562
|
// JervCode's settings entry and native local surfaces have no marketplace clone.
|
|
11638
|
-
marketplaceClonePresent: surface === "
|
|
11639
|
-
pluginCachePresent: surface === "jervcode" ? mmiPiPackageHealthy() : surface === "hermes" ? hermesPluginTreeHealthy(hermesPluginRoot()) : surface === "
|
|
11563
|
+
marketplaceClonePresent: surface === "jervcode" || surface === "hermes" ? true : marketplaceClonePresent(surface, (0, import_node_os7.homedir)()),
|
|
11564
|
+
pluginCachePresent: surface === "jervcode" ? mmiPiPackageHealthy() : surface === "hermes" ? hermesPluginTreeHealthy(hermesPluginRoot()) : surface === "codex" ? Boolean(
|
|
11640
11565
|
codexStatus?.installed && codexStatus.enabled && codexStatus.version && (0, import_node_fs12.existsSync)((0, import_node_path10.join)(root, "plugins", "cache", CODEX_MARKETPLACE, "mmi", codexStatus.version))
|
|
11641
11566
|
) : (0, import_node_fs12.existsSync)((0, import_node_path10.join)(root, "plugins", "cache", "mutmutco", "mmi"))
|
|
11642
11567
|
};
|
|
@@ -11652,84 +11577,6 @@ async function runPluginCli(bin, args, log) {
|
|
|
11652
11577
|
return false;
|
|
11653
11578
|
}
|
|
11654
11579
|
}
|
|
11655
|
-
function canonicalCursorRemote(remote) {
|
|
11656
|
-
return /^(?:https?:\/\/github\.com\/|ssh:\/\/git@github\.com\/|git@github\.com:|github\.com[:/])mutmutco\/MMI-Hub(?:\.git)?\/?$/i.test(remote.trim().replace(/\\/g, "/"));
|
|
11657
|
-
}
|
|
11658
|
-
function readJsonFile(path2) {
|
|
11659
|
-
try {
|
|
11660
|
-
return JSON.parse((0, import_node_fs12.readFileSync)(path2, "utf8"));
|
|
11661
|
-
} catch {
|
|
11662
|
-
return null;
|
|
11663
|
-
}
|
|
11664
|
-
}
|
|
11665
|
-
async function cursorTreeManaged(target) {
|
|
11666
|
-
if ((0, import_node_fs12.existsSync)((0, import_node_path10.join)(target, ".git"))) {
|
|
11667
|
-
try {
|
|
11668
|
-
const { stdout } = await runHostBin("git", ["-C", target, "remote", "get-url", "origin"], { timeout: 15e3 });
|
|
11669
|
-
if (canonicalCursorRemote(stdout)) return { ok: true };
|
|
11670
|
-
return { ok: false, detail: `refused to replace unrelated Cursor plugin directory at ${target}` };
|
|
11671
|
-
} catch {
|
|
11672
|
-
return { ok: false, detail: `refused to replace unmanaged Cursor plugin directory at ${target} (git checkout with no readable origin)` };
|
|
11673
|
-
}
|
|
11674
|
-
}
|
|
11675
|
-
if (readJsonFile((0, import_node_path10.join)(target, "package.json"))?.name === "@mutmutco/cursor-plugin") return { ok: true };
|
|
11676
|
-
if (readJsonFile((0, import_node_path10.join)(target, ".cursor-plugin", "plugin.json"))?.name === "mmi") return { ok: true };
|
|
11677
|
-
return {
|
|
11678
|
-
ok: false,
|
|
11679
|
-
detail: `refused to replace unmanaged Cursor plugin directory at ${target} \u2014 no canonical git remote, no @mutmutco/cursor-plugin package.json and no MMI plugin manifest`
|
|
11680
|
-
};
|
|
11681
|
-
}
|
|
11682
|
-
async function installCursorPluginCheckout(env = process.env) {
|
|
11683
|
-
const configRoot = surfaceConfigRoot("cursor", env);
|
|
11684
|
-
const pluginsRoot = (0, import_node_path10.join)(configRoot, "plugins");
|
|
11685
|
-
const target = (0, import_node_path10.join)(pluginsRoot, "local", "mmi");
|
|
11686
|
-
const source = env.MMI_CURSOR_PLUGIN_SOURCE?.trim();
|
|
11687
|
-
if ((0, import_node_fs12.existsSync)(target) && !source) {
|
|
11688
|
-
const managed = await cursorTreeManaged(target);
|
|
11689
|
-
if (!managed.ok) return { ok: false, detail: managed.detail };
|
|
11690
|
-
}
|
|
11691
|
-
(0, import_node_fs12.mkdirSync)((0, import_node_path10.join)(pluginsRoot, "local"), { recursive: true });
|
|
11692
|
-
(0, import_node_fs12.mkdirSync)((0, import_node_path10.join)(pluginsRoot, "staging"), { recursive: true });
|
|
11693
|
-
(0, import_node_fs12.mkdirSync)((0, import_node_path10.join)(pluginsRoot, "quarantine"), { recursive: true });
|
|
11694
|
-
const suffix = `${Date.now()}-${process.pid}`;
|
|
11695
|
-
const staged = (0, import_node_path10.join)(pluginsRoot, "staging", `mmi-${suffix}`);
|
|
11696
|
-
const quarantined = (0, import_node_path10.join)(pluginsRoot, "quarantine", `mmi-${suffix}`);
|
|
11697
|
-
try {
|
|
11698
|
-
if (source) {
|
|
11699
|
-
(0, import_node_fs12.cpSync)(source, staged, {
|
|
11700
|
-
recursive: true,
|
|
11701
|
-
filter: (path2) => !path2.split(/[\\/]/).some((part) => part === ".git" || part === "node_modules")
|
|
11702
|
-
});
|
|
11703
|
-
} else {
|
|
11704
|
-
await runHostBin("gh", ["repo", "clone", "mutmutco/MMI-Hub", staged, "--", "--branch", "main", "--depth", "1"], {
|
|
11705
|
-
timeout: CLAUDE_PLUGIN_TIMEOUT_MS,
|
|
11706
|
-
step: "gh repo clone mutmutco/MMI-Hub"
|
|
11707
|
-
});
|
|
11708
|
-
}
|
|
11709
|
-
if (!cursorPluginTreeHealthy(staged)) {
|
|
11710
|
-
(0, import_node_fs12.rmSync)(staged, { recursive: true, force: true });
|
|
11711
|
-
return { ok: false, detail: "downloaded Cursor plugin is incomplete; existing install was preserved" };
|
|
11712
|
-
}
|
|
11713
|
-
let movedOld = false;
|
|
11714
|
-
if ((0, import_node_fs12.existsSync)(target)) {
|
|
11715
|
-
(0, import_node_fs12.renameSync)(target, quarantined);
|
|
11716
|
-
movedOld = true;
|
|
11717
|
-
}
|
|
11718
|
-
try {
|
|
11719
|
-
(0, import_node_fs12.renameSync)(staged, target);
|
|
11720
|
-
} catch (error) {
|
|
11721
|
-
if (movedOld && !(0, import_node_fs12.existsSync)(target)) (0, import_node_fs12.renameSync)(quarantined, target);
|
|
11722
|
-
throw error;
|
|
11723
|
-
}
|
|
11724
|
-
return {
|
|
11725
|
-
ok: true,
|
|
11726
|
-
detail: movedOld ? `installed canonical Cursor plugin; previous checkout quarantined at ${quarantined}` : `installed canonical Cursor plugin at ${target}`
|
|
11727
|
-
};
|
|
11728
|
-
} catch (error) {
|
|
11729
|
-
if ((0, import_node_fs12.existsSync)(staged)) (0, import_node_fs12.rmSync)(staged, { recursive: true, force: true });
|
|
11730
|
-
return { ok: false, detail: error.message.trim().slice(0, 240).replace(/\s+/g, " ") };
|
|
11731
|
-
}
|
|
11732
|
-
}
|
|
11733
11580
|
async function marketplaceAddRefSupported(bin) {
|
|
11734
11581
|
try {
|
|
11735
11582
|
const { stdout, stderr } = await runHostBin(bin, ["plugin", "marketplace", "add", "--help"], {
|
|
@@ -11852,11 +11699,10 @@ async function healJervCodeForDoctor() {
|
|
|
11852
11699
|
}
|
|
11853
11700
|
async function healActivePluginForDoctor(surface = detectSurface(process.env), onStep) {
|
|
11854
11701
|
const token = surfaceToken(surface);
|
|
11855
|
-
if (token !== "claude" && token !== "codex" && token !== "
|
|
11702
|
+
if (token !== "claude" && token !== "codex" && token !== "jervcode") {
|
|
11856
11703
|
return { ok: false, detail: `not a supported plugin surface (${surface})` };
|
|
11857
11704
|
}
|
|
11858
11705
|
if (token === "claude") return healClaudePluginForDoctor(surface, onStep);
|
|
11859
|
-
if (token === "cursor") return installCursorPluginCheckout();
|
|
11860
11706
|
if (token === "jervcode") return healJervCodeForDoctor();
|
|
11861
11707
|
const steps = [];
|
|
11862
11708
|
const applied = await applyPluginHeal(surface, (msg) => {
|
|
@@ -11952,7 +11798,7 @@ async function runGuard(readOrigin) {
|
|
|
11952
11798
|
const surface = detectSurface(process.env);
|
|
11953
11799
|
try {
|
|
11954
11800
|
const token = surfaceToken(surface);
|
|
11955
|
-
if (token !== "claude" && token !== "codex" && token !== "
|
|
11801
|
+
if (token !== "claude" && token !== "codex" && token !== "jervcode") {
|
|
11956
11802
|
process.exitCode = 0;
|
|
11957
11803
|
return;
|
|
11958
11804
|
}
|
|
@@ -11966,9 +11812,8 @@ async function runGuard(readOrigin) {
|
|
|
11966
11812
|
if (surfaceToken(surface) === "codex") {
|
|
11967
11813
|
console.error("[mmi-guard] Could not inspect the active Codex plugin; run `mmi-cli plugin heal`.");
|
|
11968
11814
|
process.exitCode = 1;
|
|
11969
|
-
} else if (surfaceToken(surface) === "
|
|
11970
|
-
|
|
11971
|
-
console.error(`[mmi-guard] Could not inspect the active ${host} plugin; run \`mmi-cli plugin heal\`.`);
|
|
11815
|
+
} else if (surfaceToken(surface) === "jervcode") {
|
|
11816
|
+
console.error("[mmi-guard] Could not inspect the active JervCode plugin; run `mmi-cli plugin heal`.");
|
|
11972
11817
|
process.exitCode = 1;
|
|
11973
11818
|
} else {
|
|
11974
11819
|
process.exitCode = 0;
|
|
@@ -11977,7 +11822,7 @@ async function runGuard(readOrigin) {
|
|
|
11977
11822
|
}
|
|
11978
11823
|
async function runPluginHeal(surface = detectSurface(process.env)) {
|
|
11979
11824
|
const token = surfaceToken(surface);
|
|
11980
|
-
if (token !== "claude" && token !== "codex" && token !== "
|
|
11825
|
+
if (token !== "claude" && token !== "codex" && token !== "jervcode") {
|
|
11981
11826
|
console.log(nonClaudeSurfaceHealMessage(token ?? void 0));
|
|
11982
11827
|
return;
|
|
11983
11828
|
}
|
|
@@ -11994,17 +11839,15 @@ async function runPluginHeal(surface = detectSurface(process.env)) {
|
|
|
11994
11839
|
return;
|
|
11995
11840
|
}
|
|
11996
11841
|
const descriptor = PLUGIN_SURFACE_HEAL[token];
|
|
11997
|
-
const
|
|
11998
|
-
if (cursorResult) console.log(` \u21BB ${cursorResult.detail}`);
|
|
11999
|
-
const applied = cursorResult ? cursorResult.ok : await applyPluginHeal(surface, console.log, { force: true });
|
|
11842
|
+
const applied = await applyPluginHeal(surface, console.log, { force: true });
|
|
12000
11843
|
const healed = token !== "claude" ? applied && buildPluginGuardDecision(snapshotPluginGuardInput(surface, true)).state === "healthy" : applied;
|
|
12001
11844
|
if (healed) {
|
|
12002
11845
|
console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI skills and commands.`);
|
|
12003
11846
|
} else {
|
|
12004
11847
|
process.exitCode = 1;
|
|
12005
|
-
const refSupported =
|
|
12006
|
-
const recovery =
|
|
12007
|
-
const note =
|
|
11848
|
+
const refSupported = await marketplaceAddRefSupported(token);
|
|
11849
|
+
const recovery = refSupported ? descriptor.recovery : recoveryWithoutRef(descriptor.recovery);
|
|
11850
|
+
const note = refAbsenceNote(token, refSupported);
|
|
12008
11851
|
console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
|
|
12009
11852
|
${recovery}${note}${pluginReadGrantNote()}`);
|
|
12010
11853
|
}
|
|
@@ -12017,7 +11860,6 @@ var SESSION_ID_ENV_VARS = [
|
|
|
12017
11860
|
"CLAUDE_CODE_SESSION_ID",
|
|
12018
11861
|
"CODEX_SESSION_ID",
|
|
12019
11862
|
"CODEX_THREAD_ID",
|
|
12020
|
-
"CURSOR_SESSION_ID",
|
|
12021
11863
|
"HERMES_SESSION_ID",
|
|
12022
11864
|
"PI_SESSION_ID",
|
|
12023
11865
|
// #6193: lane runners spawn the CLI through the host's extension shell operations, which strip
|
|
@@ -12546,6 +12388,7 @@ function renderBoardItem(item) {
|
|
|
12546
12388
|
if (claim) {
|
|
12547
12389
|
const who = `${claim.holder}, ${formatClaimAge(claim.ageMs)} ago`;
|
|
12548
12390
|
lines2.push(claim.currentlyClaimed ? `Claimed by: ${who}` : `Last claimed by: ${who} (not currently claimed)`);
|
|
12391
|
+
if (claim.stale) lines2.push(`Claim stamp: STALE, idle ${formatClaimAge(claim.idleMs)}; any live session may claim it`);
|
|
12549
12392
|
}
|
|
12550
12393
|
lines2.push(
|
|
12551
12394
|
`Type: ${item.type ?? "item"}${item.labels.length ? ` \xB7 ${item.labels.join(", ")}` : ""}`,
|
|
@@ -12846,6 +12689,10 @@ async function readBoard(options, deps = {}) {
|
|
|
12846
12689
|
if (options.includeBundleDetails || options.includeAllBodies) {
|
|
12847
12690
|
await attachBundleDetails(report, client, options.allowPartial ?? false, { all: options.includeAllBodies });
|
|
12848
12691
|
}
|
|
12692
|
+
const readComments = claimMarkerCommentsReader(client);
|
|
12693
|
+
for (const scope of ["primary", "secondary"]) {
|
|
12694
|
+
await markStaleClaimStamps(report[scope], readComments, { pullRequests, writable: writableOrUnknown(writable) });
|
|
12695
|
+
}
|
|
12849
12696
|
for (const scope of ["primary", "secondary"]) {
|
|
12850
12697
|
const filtered = await filterDependencyBlockedClaimables(report[scope].claimable, client, {
|
|
12851
12698
|
requireBundledBody: Boolean(options.includeBundleDetails)
|
|
@@ -12854,7 +12701,7 @@ async function readBoard(options, deps = {}) {
|
|
|
12854
12701
|
report.warnings.push(...filtered.warnings);
|
|
12855
12702
|
}
|
|
12856
12703
|
for (const scope of ["primary", "secondary"]) {
|
|
12857
|
-
const demoted = await demoteLiveClaimedClaimables(report[scope], client);
|
|
12704
|
+
const demoted = await demoteLiveClaimedClaimables(report[scope], client, void 0, { fetchComments: readComments });
|
|
12858
12705
|
report.warnings.push(...demoted.warnings);
|
|
12859
12706
|
report.partial = report.partial || demoted.partial;
|
|
12860
12707
|
}
|
|
@@ -13144,7 +12991,7 @@ function renderClaimable(lines2, items) {
|
|
|
13144
12991
|
function renderTaken(lines2, items) {
|
|
13145
12992
|
if (!items.length) return;
|
|
13146
12993
|
lines2.push("Taken");
|
|
13147
|
-
for (const item of items) lines2.push(` ${item.ref} \xB7 ${item.status} \xB7 @${item.assignees.join(", @")}`);
|
|
12994
|
+
for (const item of items) lines2.push(` ${item.ref} \xB7 ${item.status} \xB7 @${item.assignees.join(", @")}${renderClaimLane(item)}`);
|
|
13148
12995
|
}
|
|
13149
12996
|
function renderUnowned(lines2, items) {
|
|
13150
12997
|
if (!items.length) return;
|
|
@@ -13155,7 +13002,11 @@ function renderTitledItem(item) {
|
|
|
13155
13002
|
const pri = item.priority ? ` \xB7 ${item.priority}` : "";
|
|
13156
13003
|
const loop = item.loopState ? ` \xB7 loop:${item.loopState.state}` : "";
|
|
13157
13004
|
const stale = item.loopStaleness?.stale ? ` \xB7 stale:${formatClaimAge(item.loopStaleness.idleMs)}` : "";
|
|
13158
|
-
return `${item.ref} - [${item.type ?? "item"}]${pri}${loop}${stale} ${item.title}`;
|
|
13005
|
+
return `${item.ref} - [${item.type ?? "item"}]${pri}${loop}${stale}${renderClaimLane(item)} ${item.title}`;
|
|
13006
|
+
}
|
|
13007
|
+
function renderClaimLane(item) {
|
|
13008
|
+
if (!item.claim) return "";
|
|
13009
|
+
return item.claim.stale ? ` \xB7 stale:${formatClaimAge(item.claim.idleMs)} (${item.claim.holder})` : ` \xB7 lane ${item.claim.holder}`;
|
|
13159
13010
|
}
|
|
13160
13011
|
function hasItems(buckets) {
|
|
13161
13012
|
return buckets.userOwned.length > 0 || buckets.claimable.length > 0 || buckets.taken.length > 0 || buckets.unownedInFlight.length > 0;
|
|
@@ -13280,6 +13131,7 @@ function deriveLoopStaleness(item, loopState, now = Date.now()) {
|
|
|
13280
13131
|
}
|
|
13281
13132
|
var CLAIM_MARKER_PATTERN = /<!--\s*mmi-claim:\s*(\{[\s\S]*?\})\s*-->/g;
|
|
13282
13133
|
var CLAIM_MARKER_LIVE_MS = 24 * 60 * 6e4;
|
|
13134
|
+
var CLAIM_STAMP_IDLE_MS = 4 * 60 * 6e4;
|
|
13283
13135
|
function isValidClaimMarker(parsed) {
|
|
13284
13136
|
if (!parsed || typeof parsed !== "object") return false;
|
|
13285
13137
|
const marker = parsed;
|
|
@@ -13316,34 +13168,86 @@ function describeClaimMarker(marker) {
|
|
|
13316
13168
|
}
|
|
13317
13169
|
function boardItemClaim(item) {
|
|
13318
13170
|
const marker = item.details ? latestClaimMarker(item.details.comments) : void 0;
|
|
13319
|
-
|
|
13171
|
+
return marker ? claimFromMarker(item, marker) : void 0;
|
|
13172
|
+
}
|
|
13173
|
+
function claimStampStaleness(marker, evidence = {}, now = Date.now()) {
|
|
13174
|
+
const times = [marker.ts, ...evidence.activityAt ?? []].map((t) => t ? Date.parse(t) : Number.NaN);
|
|
13175
|
+
const idleMs = now - Math.max(...times.filter(Number.isFinite));
|
|
13176
|
+
return { stale: !evidence.linkedWork && idleMs >= CLAIM_STAMP_IDLE_MS, idleMs };
|
|
13177
|
+
}
|
|
13178
|
+
function workIsLive(at, now = Date.now()) {
|
|
13179
|
+
const idle = now - Date.parse(at ?? "");
|
|
13180
|
+
return !Number.isFinite(idle) || idle < CLAIM_STAMP_IDLE_MS;
|
|
13181
|
+
}
|
|
13182
|
+
function claimFromMarker(item, marker, linked = {}, now = Date.now()) {
|
|
13320
13183
|
return {
|
|
13321
13184
|
marker,
|
|
13322
13185
|
holder: describeClaimMarker(marker),
|
|
13323
|
-
ageMs:
|
|
13324
|
-
currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress"
|
|
13186
|
+
ageMs: now - Date.parse(marker.ts),
|
|
13187
|
+
currentlyClaimed: item.assignees.length > 0 && item.status === "In Progress",
|
|
13188
|
+
...claimStampStaleness(marker, { activityAt: [item.updatedAt, ...linked.activityAt ?? []], linkedWork: linked.linkedWork }, now)
|
|
13325
13189
|
};
|
|
13326
13190
|
}
|
|
13327
|
-
function classifyClaimableMarker(comments, actor, now = Date.now()) {
|
|
13191
|
+
function classifyClaimableMarker(comments, actor, now = Date.now(), activityAt = []) {
|
|
13328
13192
|
const marker = comments ? latestClaimMarker(comments) : void 0;
|
|
13329
13193
|
if (!marker) return { verdict: "claimable" };
|
|
13330
13194
|
const ownership = laneOwnership(marker, actor);
|
|
13331
13195
|
if (ownership === "mine") return { verdict: "owned", holder: describeClaimMarker(marker) };
|
|
13332
|
-
|
|
13333
|
-
if (Number.isFinite(age) && age >= 0 && age < CLAIM_MARKER_LIVE_MS) {
|
|
13196
|
+
if (!claimStampStaleness(marker, { activityAt }, now).stale) {
|
|
13334
13197
|
return { verdict: "taken", holder: describeClaimMarker(marker) };
|
|
13335
13198
|
}
|
|
13336
13199
|
return { verdict: "claimable" };
|
|
13337
13200
|
}
|
|
13201
|
+
function claimMarkerCommentsReader(client) {
|
|
13202
|
+
const cache = /* @__PURE__ */ new Map();
|
|
13203
|
+
return (item) => {
|
|
13204
|
+
let read = cache.get(item);
|
|
13205
|
+
if (!read) {
|
|
13206
|
+
read = (async () => {
|
|
13207
|
+
if (item.detailsUnavailable) return void 0;
|
|
13208
|
+
if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
|
|
13209
|
+
const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
|
|
13210
|
+
return comments.map((comment) => ({ body: comment.body ?? "" }));
|
|
13211
|
+
})();
|
|
13212
|
+
cache.set(item, read);
|
|
13213
|
+
}
|
|
13214
|
+
return read;
|
|
13215
|
+
};
|
|
13216
|
+
}
|
|
13217
|
+
async function markStaleClaimStamps(scope, readComments, options = {}) {
|
|
13218
|
+
const actor = options.actor ?? describeSessionIdentity();
|
|
13219
|
+
const now = options.now ?? Date.now();
|
|
13220
|
+
const promoted = [];
|
|
13221
|
+
for (const bucket of ["userOwned", "claimable", "unownedInFlight", "taken"]) {
|
|
13222
|
+
const kept = [];
|
|
13223
|
+
for (const item of scope[bucket]) {
|
|
13224
|
+
kept.push(item);
|
|
13225
|
+
if (item.contentType !== "Issue") continue;
|
|
13226
|
+
const comments = await readComments(item).catch(() => void 0);
|
|
13227
|
+
const marker = comments ? latestClaimMarker(comments) : void 0;
|
|
13228
|
+
if (!marker) continue;
|
|
13229
|
+
const prs = (options.pullRequests ?? []).filter((pr) => pr.closesIssues.some((ref) => ref.toLowerCase() === item.ref.toLowerCase()));
|
|
13230
|
+
item.claim = claimFromMarker(item, marker, {
|
|
13231
|
+
activityAt: prs.map((pr) => pr.updatedAt),
|
|
13232
|
+
linkedWork: prs.some((pr) => !pr.updatedAt)
|
|
13233
|
+
}, now);
|
|
13234
|
+
const movable = bucket === "userOwned" || bucket === "unownedInFlight";
|
|
13235
|
+
if (movable && item.claim.stale && laneOwnership(marker, actor) !== "mine" && (!options.writable || options.writable.has(item.repository.toLowerCase()))) {
|
|
13236
|
+
kept.pop();
|
|
13237
|
+
promoted.push(item);
|
|
13238
|
+
}
|
|
13239
|
+
}
|
|
13240
|
+
scope[bucket] = kept;
|
|
13241
|
+
}
|
|
13242
|
+
if (promoted.length) {
|
|
13243
|
+
scope.claimable.push(...promoted);
|
|
13244
|
+
scope.claimable.sort(compareItems);
|
|
13245
|
+
}
|
|
13246
|
+
}
|
|
13338
13247
|
async function demoteLiveClaimedClaimables(scope, client, actor = describeSessionIdentity(), deps = {}) {
|
|
13339
13248
|
const warnings = [];
|
|
13340
13249
|
let partial = false;
|
|
13341
|
-
const fetchComments = deps.fetchComments ?? (
|
|
13342
|
-
if (item.detailsUnavailable) return void 0;
|
|
13343
|
-
if (item.details && !item.details.commentsUnread && !item.details.omittedComments) return item.details.comments;
|
|
13344
|
-
const comments = await client.restPaginate(`repos/${item.repository}/issues/${item.number}/comments`);
|
|
13345
|
-
return comments.map((comment) => ({ body: comment.body ?? "" }));
|
|
13346
|
-
});
|
|
13250
|
+
const fetchComments = deps.fetchComments ?? claimMarkerCommentsReader(client);
|
|
13347
13251
|
const stillClaimable = [];
|
|
13348
13252
|
const movedTaken = [];
|
|
13349
13253
|
const movedOwned = [];
|
|
@@ -13363,7 +13267,7 @@ async function demoteLiveClaimedClaimables(scope, client, actor = describeSessio
|
|
|
13363
13267
|
warnings.push(`${item.ref} left claimable: claim-marker evidence could not be read`);
|
|
13364
13268
|
continue;
|
|
13365
13269
|
}
|
|
13366
|
-
const verdict = classifyClaimableMarker(comments, actor, deps.now);
|
|
13270
|
+
const verdict = classifyClaimableMarker(comments, actor, deps.now, [item.updatedAt]);
|
|
13367
13271
|
if (verdict.verdict === "claimable") {
|
|
13368
13272
|
stillClaimable.push(item);
|
|
13369
13273
|
} else if (verdict.verdict === "owned") {
|
|
@@ -13458,6 +13362,12 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
|
|
|
13458
13362
|
const comments = await client.restPaginate(`repos/${repo}/issues/${number}/comments`);
|
|
13459
13363
|
const rows = comments.map((comment) => ({ body: comment.body ?? "", login: comment.user?.login ?? void 0 }));
|
|
13460
13364
|
out.marker = latestClaimMarker(rows);
|
|
13365
|
+
for (let i = comments.length - 1; i >= 0; i -= 1) {
|
|
13366
|
+
if (comments[i].created_at && !latestClaimMarker([rows[i]])) {
|
|
13367
|
+
out.lastCommentAt = comments[i].created_at;
|
|
13368
|
+
break;
|
|
13369
|
+
}
|
|
13370
|
+
}
|
|
13461
13371
|
if (out.marker) {
|
|
13462
13372
|
for (let i = rows.length - 1; i >= 0; i -= 1) {
|
|
13463
13373
|
if (latestClaimMarker([rows[i]])) {
|
|
@@ -13484,7 +13394,10 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
|
|
|
13484
13394
|
if (from && from.toLowerCase() !== repo.toLowerCase()) return false;
|
|
13485
13395
|
return pullClosesIssue(source, number);
|
|
13486
13396
|
});
|
|
13487
|
-
if (pr?.source?.issue?.number)
|
|
13397
|
+
if (pr?.source?.issue?.number) {
|
|
13398
|
+
out.openPr = `open PR #${pr.source.issue.number} would close it on merge`;
|
|
13399
|
+
out.openPrAt = pr.source.issue.updated_at ?? void 0;
|
|
13400
|
+
}
|
|
13488
13401
|
} catch {
|
|
13489
13402
|
out.failed.push("timeline");
|
|
13490
13403
|
}
|
|
@@ -13495,11 +13408,24 @@ async function gatherClaimLiveness(client, repo, number, fetchOpenPulls) {
|
|
|
13495
13408
|
} catch {
|
|
13496
13409
|
out.failed.push("branches");
|
|
13497
13410
|
}
|
|
13411
|
+
if (out.branch) {
|
|
13412
|
+
try {
|
|
13413
|
+
const head = await client.rest(
|
|
13414
|
+
"GET",
|
|
13415
|
+
`repos/${repo}/branches/${encodeURIComponent(out.branch)}`
|
|
13416
|
+
);
|
|
13417
|
+
out.branchAt = head?.commit?.commit?.committer?.date ?? void 0;
|
|
13418
|
+
} catch {
|
|
13419
|
+
}
|
|
13420
|
+
}
|
|
13498
13421
|
if (!out.branch) {
|
|
13499
13422
|
try {
|
|
13500
13423
|
const pulls = await fetchOpenPulls(repo);
|
|
13501
13424
|
const hit = pulls.find((pr) => pullClosesIssue(pr, number));
|
|
13502
|
-
if (hit?.head?.ref)
|
|
13425
|
+
if (hit?.head?.ref) {
|
|
13426
|
+
out.branch = `${hit.head.ref} (head of open PR #${hit.number} closing #${number})`;
|
|
13427
|
+
out.branchAt = hit.updated_at ?? void 0;
|
|
13428
|
+
}
|
|
13503
13429
|
} catch {
|
|
13504
13430
|
out.failed.push("pulls");
|
|
13505
13431
|
}
|
|
@@ -13510,13 +13436,24 @@ function liveEvidenceLines(evidence, repo) {
|
|
|
13510
13436
|
const live = [];
|
|
13511
13437
|
if (evidence.marker && evidence.sessionState === "live") {
|
|
13512
13438
|
live.push(`claim session ${describeClaimMarker(evidence.marker)} has recent local transcript activity`);
|
|
13513
|
-
} else if (evidence.marker && evidence.sessionState !== "dead"
|
|
13514
|
-
|
|
13439
|
+
} else if (evidence.marker && evidence.sessionState !== "dead") {
|
|
13440
|
+
const { stale, idleMs } = claimStampStaleness(evidence.marker, { activityAt: [evidence.lastCommentAt] });
|
|
13441
|
+
const marker = describeClaimMarker(evidence.marker);
|
|
13442
|
+
if (!stale) {
|
|
13443
|
+
live.push(evidence.lastCommentAt && Date.parse(evidence.lastCommentAt) > Date.parse(evidence.marker.ts) ? `claim marker from ${marker} is ${formatClaimAge(evidence.markerAgeMs)} old but the issue was commented ${formatClaimAge(idleMs)} ago (session not locally probeable)` : `claim marker from ${marker} is only ${formatClaimAge(evidence.markerAgeMs)} old (session not locally probeable)`);
|
|
13444
|
+
}
|
|
13515
13445
|
}
|
|
13516
|
-
if (evidence.openPr) live.push(evidence.openPr);
|
|
13517
|
-
if (evidence.branch) live.push(`live branch ${evidence.branch} on ${repo}`);
|
|
13446
|
+
if (evidence.openPr && workIsLive(evidence.openPrAt)) live.push(evidence.openPr);
|
|
13447
|
+
if (evidence.branch && workIsLive(evidence.branchAt)) live.push(`live branch ${evidence.branch} on ${repo}`);
|
|
13518
13448
|
return live;
|
|
13519
13449
|
}
|
|
13450
|
+
function staleWorkLines(evidence) {
|
|
13451
|
+
const age = (at) => formatClaimAge(Date.now() - Date.parse(at ?? ""));
|
|
13452
|
+
return [
|
|
13453
|
+
evidence.branch && !workIsLive(evidence.branchAt) ? `branch ${evidence.branch} (last commit ${age(evidence.branchAt)} ago)` : void 0,
|
|
13454
|
+
evidence.openPr && !workIsLive(evidence.openPrAt) ? `${evidence.openPr.replace(/ would close it on merge$/, "")} (updated ${age(evidence.openPrAt)} ago)` : void 0
|
|
13455
|
+
].filter((line) => Boolean(line));
|
|
13456
|
+
}
|
|
13520
13457
|
function sameClaimHost(a, b) {
|
|
13521
13458
|
return Boolean(a && b && a.toLowerCase() === b.toLowerCase());
|
|
13522
13459
|
}
|
|
@@ -13549,13 +13486,17 @@ async function checkLaneContest(client, item, actor = describeSessionIdentity(),
|
|
|
13549
13486
|
const resume = ownership !== "mine" && sameOwner ? laneResumeEvidence(evidence) : void 0;
|
|
13550
13487
|
const live = ownership === "mine" ? [] : liveEvidenceLines(evidence, item.repository);
|
|
13551
13488
|
const unverifiable = ownership === "mine" ? [] : evidence.failed;
|
|
13489
|
+
const contested = ownership !== "mine" && !resume && (live.length > 0 || unverifiable.length > 0);
|
|
13490
|
+
const carry = staleWorkLines(evidence);
|
|
13491
|
+
const staleStamp = !contested && !resume && ownership !== "mine" && evidence.marker ? `stale claim stamp ${describeClaimMarker(evidence.marker)} (idle ${formatClaimAge(claimStampStaleness(evidence.marker, { activityAt: [evidence.lastCommentAt] }).idleMs)})${carry.length ? `; continue its ${carry.join(" and ")}` : ""}` : void 0;
|
|
13552
13492
|
return {
|
|
13553
|
-
contested
|
|
13493
|
+
contested,
|
|
13554
13494
|
live,
|
|
13555
13495
|
unverifiable,
|
|
13556
13496
|
ownership,
|
|
13557
13497
|
marker: evidence.marker,
|
|
13558
|
-
resume
|
|
13498
|
+
resume,
|
|
13499
|
+
...staleStamp ? { staleStamp } : {}
|
|
13559
13500
|
};
|
|
13560
13501
|
}
|
|
13561
13502
|
function laneContestParts(contest) {
|
|
@@ -13999,11 +13940,13 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
13999
13940
|
};
|
|
14000
13941
|
let previousHolder;
|
|
14001
13942
|
let resumeEvidence;
|
|
13943
|
+
let staleStamp;
|
|
13944
|
+
const tookOver = () => ({ outcome: "took-over", holder, previousHolder, ...staleStamp ? { staleStamp } : {} });
|
|
14002
13945
|
const claimedReceipt = () => ({
|
|
14003
13946
|
...item.status === "In Review" ? { reclaimedFrom: "In Review" } : {},
|
|
14004
|
-
...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } :
|
|
13947
|
+
...previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : tookOver() : { outcome: "claimed", holder }
|
|
14005
13948
|
});
|
|
14006
|
-
const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } :
|
|
13949
|
+
const heldReceipt = () => previousHolder ? resumeEvidence ? { outcome: "resumed", holder, previousHolder, resumeEvidence } : tookOver() : { outcome: "held", holder };
|
|
14007
13950
|
if (flatItem.contentType !== "Issue") throw new Error(`${flatItem.ref} is not an issue`);
|
|
14008
13951
|
const pre = evaluateClaim(flatItem, assignedLogin);
|
|
14009
13952
|
if (!pre.ok) throw new NotClaimableError(pre.reason, { ref: flatItem.ref, status: flatItem.status });
|
|
@@ -14016,6 +13959,7 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
14016
13959
|
const refuseIfContested = async () => {
|
|
14017
13960
|
const contest = await checkLaneContest(client, item, ctx.session, report.viewer);
|
|
14018
13961
|
resumeEvidence = void 0;
|
|
13962
|
+
staleStamp = void 0;
|
|
14019
13963
|
const displaced = () => ({
|
|
14020
13964
|
login: assignedLogin,
|
|
14021
13965
|
...contest.marker ? {
|
|
@@ -14028,6 +13972,9 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
14028
13972
|
if (contest.resume && contest.marker) {
|
|
14029
13973
|
resumeEvidence = contest.resume;
|
|
14030
13974
|
previousHolder = displaced();
|
|
13975
|
+
} else if (contest.staleStamp) {
|
|
13976
|
+
staleStamp = contest.staleStamp;
|
|
13977
|
+
previousHolder = displaced();
|
|
14031
13978
|
}
|
|
14032
13979
|
return;
|
|
14033
13980
|
}
|
|
@@ -14042,7 +13989,7 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
14042
13989
|
if (options.check) {
|
|
14043
13990
|
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, checked: true };
|
|
14044
13991
|
}
|
|
14045
|
-
const renewed = await postClaimMarkerComment(client, item, ctx.session);
|
|
13992
|
+
const renewed = await postClaimMarkerComment(client, item, ctx.session, staleStamp);
|
|
14046
13993
|
return { item, viewer: report.viewer, repo: report.repo, status: "In Progress", partial: false, ...heldReceipt(), alreadyClaimed: true, renewed };
|
|
14047
13994
|
}
|
|
14048
13995
|
if (options.check) {
|
|
@@ -14054,7 +14001,7 @@ async function claimOneBoardItem(ctx, selector, options) {
|
|
|
14054
14001
|
} catch (e) {
|
|
14055
14002
|
throw new Error(`claim failed before board status changed: ${ghError(e)}`);
|
|
14056
14003
|
}
|
|
14057
|
-
await postClaimMarkerComment(client, item, ctx.session);
|
|
14004
|
+
await postClaimMarkerComment(client, item, ctx.session, staleStamp);
|
|
14058
14005
|
let credential = "user";
|
|
14059
14006
|
try {
|
|
14060
14007
|
({ credential } = await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, cfg.statusOptions["In Progress"]));
|
|
@@ -14108,7 +14055,7 @@ async function claimBoardIssues(options, deps = {}) {
|
|
|
14108
14055
|
const ref = `${selector.repo}#${selector.number}`;
|
|
14109
14056
|
try {
|
|
14110
14057
|
const result = await claimOneBoardItem(ctx, selector, { ...options, bulk: true });
|
|
14111
|
-
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked, credential: result.credential };
|
|
14058
|
+
results[index] = { ref: result.item.ref, claimed: true, item: result.item, status: result.status, partial: result.partial, warning: result.warning, outcome: result.outcome, holder: result.holder, previousHolder: result.previousHolder, resumeEvidence: result.resumeEvidence, staleStamp: result.staleStamp, reclaimedFrom: result.reclaimedFrom, alreadyClaimed: result.alreadyClaimed, renewed: result.renewed, checked: result.checked, credential: result.credential };
|
|
14112
14059
|
} catch (e) {
|
|
14113
14060
|
results[index] = { ref, claimed: false, reason: e.message };
|
|
14114
14061
|
}
|
|
@@ -14296,7 +14243,7 @@ async function setBoardPriorityForSelector(options, deps = {}) {
|
|
|
14296
14243
|
}
|
|
14297
14244
|
return { ref: `${options.selector.repo}#${options.selector.number}`, itemId, priority };
|
|
14298
14245
|
}
|
|
14299
|
-
async function postClaimMarkerComment(client, item, actor = describeSessionIdentity()) {
|
|
14246
|
+
async function postClaimMarkerComment(client, item, actor = describeSessionIdentity(), staleStamp) {
|
|
14300
14247
|
try {
|
|
14301
14248
|
const marker = {
|
|
14302
14249
|
v: 1,
|
|
@@ -14307,7 +14254,7 @@ async function postClaimMarkerComment(client, item, actor = describeSessionIdent
|
|
|
14307
14254
|
};
|
|
14308
14255
|
await postIssueComment(client, {
|
|
14309
14256
|
ref: item.ref,
|
|
14310
|
-
body: `Claimed by \`${describeClaimMarker(marker)}\` at ${marker.ts}
|
|
14257
|
+
body: `Claimed by \`${describeClaimMarker(marker)}\` at ${marker.ts}.${staleStamp ? ` Took over from ${staleStamp}.` : ""}
|
|
14311
14258
|
|
|
14312
14259
|
<!-- mmi-claim: ${JSON.stringify(marker)} -->`
|
|
14313
14260
|
});
|
|
@@ -16436,8 +16383,18 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
|
|
|
16436
16383
|
if (foldPaths.length === 0) return "no version manifest to fold \u2014 the tag is the version";
|
|
16437
16384
|
const version = tag.replace(/^v/, "");
|
|
16438
16385
|
let foldNoteExtra = "";
|
|
16386
|
+
const timings = [];
|
|
16387
|
+
const timed = async (label, fn) => {
|
|
16388
|
+
const t0 = (deps.now ?? Date.now)();
|
|
16389
|
+
try {
|
|
16390
|
+
return await fn();
|
|
16391
|
+
} finally {
|
|
16392
|
+
timings.push(`${label} ${(((deps.now ?? Date.now)() - t0) / 1e3).toFixed(1)}s`);
|
|
16393
|
+
}
|
|
16394
|
+
};
|
|
16395
|
+
const withTimings = (note) => `${note}; fold timings: ${timings.join(", ")}`;
|
|
16439
16396
|
if (model === "hub-serverless") {
|
|
16440
|
-
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", sourceCommit]);
|
|
16397
|
+
await timed("release-distribution prepare", () => deps.run("node", ["scripts/release-distribution.mjs", "prepare", version, "--source-commit", sourceCommit]));
|
|
16441
16398
|
if (verifyRelease) {
|
|
16442
16399
|
await refuseDivergentPublishedNpmPack(deps, version);
|
|
16443
16400
|
} else {
|
|
@@ -16445,10 +16402,10 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
|
|
|
16445
16402
|
if (restored) foldNoteExtra = `; ${restored}`;
|
|
16446
16403
|
}
|
|
16447
16404
|
} else {
|
|
16448
|
-
const refresh = await refreshSharedInstallerDeps(deps);
|
|
16405
|
+
const refresh = await timed("installer refresh", () => refreshSharedInstallerDeps(deps));
|
|
16449
16406
|
if (refresh) foldNoteExtra = `; ${refresh}`;
|
|
16450
|
-
await installAppFoldDeps(deps);
|
|
16451
|
-
await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
|
|
16407
|
+
await timed("npm ci", () => installAppFoldDeps(deps));
|
|
16408
|
+
await timed("npm version", () => deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]));
|
|
16452
16409
|
const patch = Number(version.split(".")[2] ?? "0");
|
|
16453
16410
|
if (model === "registry-publish" && Number.isFinite(patch) && patch > 0) {
|
|
16454
16411
|
await alignExistingHotfixReleaseMarker(deps, version);
|
|
@@ -16467,12 +16424,12 @@ async function foldReleaseVersion(deps, model, tag, foldPaths, sourceCommit = "H
|
|
|
16467
16424
|
);
|
|
16468
16425
|
}
|
|
16469
16426
|
const staged = (await deps.run("git", ["diff", "--cached", "--name-only"])).trim();
|
|
16470
|
-
if (!staged) return `version fold: manifests already at ${version} \u2014 nothing committed${foldNoteExtra}
|
|
16427
|
+
if (!staged) return withTimings(`version fold: manifests already at ${version} \u2014 nothing committed${foldNoteExtra}`);
|
|
16471
16428
|
await deps.run("git", ["commit", "-m", `chore(release): bump distribution to ${tag}`]);
|
|
16472
16429
|
if (model === "hub-serverless" && verifyRelease) {
|
|
16473
|
-
await deps.run("node", ["scripts/release-distribution.mjs", "verify", version, "--skip-npm-view"]);
|
|
16430
|
+
await timed("release-distribution verify", () => deps.run("node", ["scripts/release-distribution.mjs", "verify", version, "--skip-npm-view"]));
|
|
16474
16431
|
}
|
|
16475
|
-
return `version fold: distribution bumped to ${version} in the release commit${foldNoteExtra}
|
|
16432
|
+
return withTimings(`version fold: distribution bumped to ${version} in the release commit${foldNoteExtra}`);
|
|
16476
16433
|
}
|
|
16477
16434
|
function foldPortManualCommand(model, version) {
|
|
16478
16435
|
return model === "hub-serverless" ? `node scripts/release-distribution.mjs prepare ${version}` : `npm version ${version} --no-git-tag-version --allow-same-version`;
|
|
@@ -17268,10 +17225,10 @@ var rollout_plan_default = {
|
|
|
17268
17225
|
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)."
|
|
17269
17226
|
},
|
|
17270
17227
|
baseline: {
|
|
17271
|
-
version: "4.5.
|
|
17272
|
-
tag: "v4.5.
|
|
17273
|
-
commit: "
|
|
17274
|
-
npm: "@mutmutco/cli@4.5.
|
|
17228
|
+
version: "4.5.88",
|
|
17229
|
+
tag: "v4.5.88",
|
|
17230
|
+
commit: "6e198cd64755",
|
|
17231
|
+
npm: "@mutmutco/cli@4.5.88"
|
|
17275
17232
|
},
|
|
17276
17233
|
exitCriterion: "fleet-n-of-n",
|
|
17277
17234
|
hubOnlyShortcut: "forbidden",
|
|
@@ -17288,14 +17245,14 @@ var rollout_plan_default = {
|
|
|
17288
17245
|
repo: "mutmutco/mmi-hub",
|
|
17289
17246
|
role: "canary",
|
|
17290
17247
|
schedule: "train",
|
|
17291
|
-
v3Target: "v4.5.
|
|
17248
|
+
v3Target: "v4.5.88"
|
|
17292
17249
|
}
|
|
17293
17250
|
],
|
|
17294
17251
|
rollbackTrigger: "Any red inside the post-contract soak window: `devops train gate` FAIL attributable to the v4 doors, Hub endpoint health probe failure, a pre-v4 client admitted instead of receiving actionable HTTP 426, or npm consumer install/doctor failure on the v4-only dist.",
|
|
17295
17252
|
rollback: {
|
|
17296
17253
|
independent: true,
|
|
17297
|
-
mechanism: "npm dist-tag latest -> 4.5.
|
|
17298
|
-
v3Target: "v4.5.
|
|
17254
|
+
mechanism: "npm dist-tag latest -> 4.5.88 and redeploy the Hub Lambda from tag v4.5.88 (6e198cd64755); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
17255
|
+
v3Target: "v4.5.88 (@mutmutco/cli@4.5.88, tag commit 6e198cd64755 \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
17299
17256
|
}
|
|
17300
17257
|
},
|
|
17301
17258
|
{
|
|
@@ -19609,7 +19566,7 @@ var TRAIN_RULES_CONTEXTS_JQ = '[.[]|select(.type=="required_status_checks")|.par
|
|
|
19609
19566
|
var TRAIN_CHECK_ATTEMPTS = 40;
|
|
19610
19567
|
var TRAIN_QUEUED_CHECK_ATTEMPTS = 80;
|
|
19611
19568
|
var TRAIN_CHECK_DELAY_MS = 15e3;
|
|
19612
|
-
var TRAIN_PR_ONLY_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done"
|
|
19569
|
+
var TRAIN_PR_ONLY_CONTEXTS = /* @__PURE__ */ new Set(["add-to-project", "mark-merged-pr-done"]);
|
|
19613
19570
|
var TRAIN_PR_AUTOMATION_GRACE_ATTEMPTS = 3;
|
|
19614
19571
|
var TRAIN_FRESH_RUN_GRACE_ATTEMPTS = 4;
|
|
19615
19572
|
async function correlateRun(deps, args) {
|
|
@@ -19963,7 +19920,7 @@ async function alignRcForward(deps, ctx, tag) {
|
|
|
19963
19920
|
return enqueueAlignmentAutoMerge(deps, ctx, number, url || void 0, `rc requires checks (${required.join(", ")}); opened alignment PR ${url || "(url unavailable)"}${supersededNote}`);
|
|
19964
19921
|
}
|
|
19965
19922
|
var FLEET_FOREIGN_VERDICT_GRACE_ATTEMPTS = 3;
|
|
19966
|
-
function resolveContextState(context, checkRuns, statuses) {
|
|
19923
|
+
function resolveContextState(context, checkRuns, statuses, opts = {}) {
|
|
19967
19924
|
let sawFailure = false;
|
|
19968
19925
|
let sawSuccess = false;
|
|
19969
19926
|
let sawPending = false;
|
|
@@ -19982,11 +19939,12 @@ function resolveContextState(context, checkRuns, statuses) {
|
|
|
19982
19939
|
else if (s.state === "failure" || s.state === "error") sawFailure = true;
|
|
19983
19940
|
else sawPending = true;
|
|
19984
19941
|
}
|
|
19942
|
+
if (sawSuccess && opts.successWins) return "success";
|
|
19985
19943
|
if (sawPending) return "pending";
|
|
19986
19944
|
if (sawSuccess) return "success";
|
|
19987
19945
|
return sawFailure ? "failed" : "pending";
|
|
19988
19946
|
}
|
|
19989
|
-
async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince) {
|
|
19947
|
+
async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince, opts = {}) {
|
|
19990
19948
|
if (required.length === 0) {
|
|
19991
19949
|
return "no required status checks configured on the target branch \u2014 check wait skipped (GitHub push gate is the backstop)";
|
|
19992
19950
|
}
|
|
@@ -20016,6 +19974,8 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
20016
19974
|
let workflowRunNote = "";
|
|
20017
19975
|
let workflowRunEverSeen = false;
|
|
20018
19976
|
let hadQueuedWorkflowRun = false;
|
|
19977
|
+
let freshActiveRun = false;
|
|
19978
|
+
let anyActiveRun = false;
|
|
20019
19979
|
const everObserved = /* @__PURE__ */ new Set();
|
|
20020
19980
|
const autoSatisfied = /* @__PURE__ */ new Set();
|
|
20021
19981
|
let fleetForeignStreak = 0;
|
|
@@ -20048,7 +20008,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
20048
20008
|
"--limit",
|
|
20049
20009
|
"100",
|
|
20050
20010
|
"--json",
|
|
20051
|
-
"databaseId,status,conclusion,headSha"
|
|
20011
|
+
"databaseId,status,conclusion,headSha,createdAt"
|
|
20052
20012
|
]);
|
|
20053
20013
|
const parsedWorkflowRuns = JSON.parse(workflowOut);
|
|
20054
20014
|
if (!Array.isArray(parsedWorkflowRuns)) throw new Error("workflow-runs response was not an array");
|
|
@@ -20057,6 +20017,13 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
20057
20017
|
workflowRunRead = "found";
|
|
20058
20018
|
workflowRunEverSeen = true;
|
|
20059
20019
|
const activeRun = matchingRuns.find((run) => run.status === "queued" || run.status === "in_progress");
|
|
20020
|
+
freshActiveRun = freshThreshold !== void 0 && matchingRuns.some((run) => {
|
|
20021
|
+
if (!["queued", "in_progress", "waiting", "pending"].includes(String(run.status))) return false;
|
|
20022
|
+
const t = Date.parse(String(run.createdAt ?? ""));
|
|
20023
|
+
return Number.isFinite(t) && t >= freshThreshold;
|
|
20024
|
+
});
|
|
20025
|
+
if (freshActiveRun) hadQueuedWorkflowRun = true;
|
|
20026
|
+
anyActiveRun = matchingRuns.some((run) => ["queued", "in_progress", "waiting", "pending"].includes(String(run.status)));
|
|
20060
20027
|
if (activeRun) {
|
|
20061
20028
|
if (activeRun.status === "queued") hadQueuedWorkflowRun = true;
|
|
20062
20029
|
workflowRunNote = activeRun.status === "queued" ? "Actions run is queued behind busy runners" : "Actions run is in progress";
|
|
@@ -20065,10 +20032,14 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
20065
20032
|
}
|
|
20066
20033
|
} else {
|
|
20067
20034
|
workflowRunRead = "empty";
|
|
20035
|
+
freshActiveRun = false;
|
|
20036
|
+
anyActiveRun = false;
|
|
20068
20037
|
workflowRunNote = "";
|
|
20069
20038
|
}
|
|
20070
20039
|
} catch (e) {
|
|
20071
20040
|
workflowRunRead = "failed";
|
|
20041
|
+
freshActiveRun = false;
|
|
20042
|
+
anyActiveRun = false;
|
|
20072
20043
|
workflowRunNote = `workflow-run lookup failed: ${e.message || String(e)}`;
|
|
20073
20044
|
}
|
|
20074
20045
|
for (const c of required) {
|
|
@@ -20080,7 +20051,7 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
20080
20051
|
}
|
|
20081
20052
|
}
|
|
20082
20053
|
const pending = required.filter((c) => !autoSatisfied.has(c));
|
|
20083
|
-
const holdStale = freshThreshold !== void 0 && attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS;
|
|
20054
|
+
const holdStale = freshThreshold !== void 0 && (attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS || freshActiveRun);
|
|
20084
20055
|
const states = pending.map((c) => {
|
|
20085
20056
|
if (fleetPosted && c === FLEET_LOCKSTEP_CONTEXT) {
|
|
20086
20057
|
const resolution = resolveFleetVerdictState(fleetPosted, statuses);
|
|
@@ -20092,6 +20063,11 @@ async function waitForRequiredTrainChecks(deps, ctx, sha, required, freshSince)
|
|
|
20092
20063
|
fleetForeignStreak = 0;
|
|
20093
20064
|
return [c, resolution.state];
|
|
20094
20065
|
}
|
|
20066
|
+
if (opts.exactCommitGreenSuffices) {
|
|
20067
|
+
const state = resolveContextState(c, checkRuns, statuses, { successWins: true });
|
|
20068
|
+
const inGrace = freshThreshold !== void 0 && attempt < TRAIN_FRESH_RUN_GRACE_ATTEMPTS;
|
|
20069
|
+
return [c, state === "failed" && (anyActiveRun || inGrace) ? "pending" : state];
|
|
20070
|
+
}
|
|
20095
20071
|
if (holdStale) {
|
|
20096
20072
|
const runsFor = checkRuns.filter((r) => r.name === c);
|
|
20097
20073
|
if (runsFor.length > 0 && !runsFor.some(isFreshRun)) return [c, "pending"];
|
|
@@ -21372,6 +21348,10 @@ async function retireRcRuntime(deps, ctx, model, deployStatus, releasedRcSha) {
|
|
|
21372
21348
|
};
|
|
21373
21349
|
}
|
|
21374
21350
|
try {
|
|
21351
|
+
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
21352
|
+
if (isBundleOnlySuiteMember(ctx.repo, meta.suite)) {
|
|
21353
|
+
return { status: "not-applicable", note: "bundle-only suite member has no rc runtime to retire" };
|
|
21354
|
+
}
|
|
21375
21355
|
await deps.run("git", ["fetch", "origin", "rc"]);
|
|
21376
21356
|
const rcNow = clean2(await deps.run("git", ["rev-parse", "origin/rc"]));
|
|
21377
21357
|
if (rcNow !== releasedRcSha) {
|
|
@@ -21904,14 +21884,6 @@ function installedClaudePluginVersion() {
|
|
|
21904
21884
|
return void 0;
|
|
21905
21885
|
}
|
|
21906
21886
|
}
|
|
21907
|
-
function manifestVersion(path2) {
|
|
21908
|
-
try {
|
|
21909
|
-
const manifest = JSON.parse((0, import_node_fs20.readFileSync)(path2, "utf8"));
|
|
21910
|
-
return typeof manifest.version === "string" && manifest.version.trim() ? manifest.version.trim() : void 0;
|
|
21911
|
-
} catch {
|
|
21912
|
-
return void 0;
|
|
21913
|
-
}
|
|
21914
|
-
}
|
|
21915
21887
|
function readHermesPluginEvidence(env = process.env) {
|
|
21916
21888
|
const host = hermesConfigRoot(env);
|
|
21917
21889
|
const root = hermesPluginRoot(env);
|
|
@@ -21963,9 +21935,6 @@ function readHermesPluginEvidence(env = process.env) {
|
|
|
21963
21935
|
function installedSurfacePluginVersion(surface) {
|
|
21964
21936
|
const token = surfaceToken(surface);
|
|
21965
21937
|
if (token === "hermes") return readHermesPluginEvidence().installedVersion;
|
|
21966
|
-
if (token === "cursor") {
|
|
21967
|
-
return manifestVersion((0, import_node_path18.join)(cursorLocalPluginRoot(), ".cursor-plugin", "plugin.json"));
|
|
21968
|
-
}
|
|
21969
21938
|
if (token === "jervcode") {
|
|
21970
21939
|
return installedJervCodePackageVersion();
|
|
21971
21940
|
}
|
|
@@ -23310,7 +23279,12 @@ var TRANSIENT_TRANSPORT_MARKERS = [
|
|
|
23310
23279
|
];
|
|
23311
23280
|
function classifyTransientTransportFailure(evidence) {
|
|
23312
23281
|
if (!evidence) return false;
|
|
23313
|
-
return TRANSIENT_TRANSPORT_MARKERS.some((marker) => marker.test(evidence));
|
|
23282
|
+
return TRANSIENT_TRANSPORT_MARKERS.some((marker) => marker.test(evidence)) || isSetupBunSpawnEtxtbsy(evidence);
|
|
23283
|
+
}
|
|
23284
|
+
function isSetupBunSpawnEtxtbsy(evidence) {
|
|
23285
|
+
if (!/\bspawn ETXTBSY\b/.test(evidence)) return false;
|
|
23286
|
+
const steps = [...evidence.matchAll(/"steps":\[([^\]]*)\]/g)].flatMap((m) => [...m[1].matchAll(/"((?:[^"\\]|\\.)*)"/g)].map((s) => s[1]));
|
|
23287
|
+
return steps.length > 0 && steps.every((name) => /\bsetup[- ]bun\b|oven-sh\/setup-bun/i.test(name));
|
|
23314
23288
|
}
|
|
23315
23289
|
var TRANSIENT_EVIDENCE_MAX_CHARS = 16e3;
|
|
23316
23290
|
var AUTO_RERUN_MAX_RETRIES = 1;
|
|
@@ -23357,8 +23331,16 @@ async function transientEvidenceForRun(deps, repo, runId) {
|
|
|
23357
23331
|
parts.push(await deps.run("gh", ["run", "view", String(runId), "--repo", repo, "--log-failed"]));
|
|
23358
23332
|
} catch {
|
|
23359
23333
|
}
|
|
23360
|
-
const
|
|
23361
|
-
|
|
23334
|
+
const [head, ...rest] = parts.filter(Boolean);
|
|
23335
|
+
if (rest.length === 0 || !head?.startsWith("[")) {
|
|
23336
|
+
const evidence = parts.filter(Boolean).join("\n");
|
|
23337
|
+
return evidence.length > TRANSIENT_EVIDENCE_MAX_CHARS ? evidence.slice(-TRANSIENT_EVIDENCE_MAX_CHARS) : evidence;
|
|
23338
|
+
}
|
|
23339
|
+
const header = head.slice(0, TRANSIENT_EVIDENCE_MAX_CHARS / 4);
|
|
23340
|
+
const log = rest.join("\n");
|
|
23341
|
+
const room = TRANSIENT_EVIDENCE_MAX_CHARS - header.length - 1;
|
|
23342
|
+
return `${header}
|
|
23343
|
+
${log.length > room ? log.slice(-room) : log}`;
|
|
23362
23344
|
}
|
|
23363
23345
|
function extractFailedRequiredContexts(failure) {
|
|
23364
23346
|
const match = /required train check failed:\s*([^\n(]+)/.exec(failure);
|
|
@@ -23747,17 +23729,23 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
|
|
|
23747
23729
|
async (current) => {
|
|
23748
23730
|
if (!current) return { value: { ledger: void 0, notes } };
|
|
23749
23731
|
const phases = { ...current.phases };
|
|
23750
|
-
for (const phase of ["deploy", "publish"]) {
|
|
23732
|
+
for (const phase of input.phases ?? ["deploy", "publish"]) {
|
|
23751
23733
|
const rec = phases[phase];
|
|
23752
23734
|
if (rec.updatedAt === "never") continue;
|
|
23753
23735
|
if (rec.runId == null) {
|
|
23754
|
-
if (rec.state === "complete") notes.push(`${phase}: persisted complete without a run id \u2014
|
|
23736
|
+
if (rec.state === "complete") notes.push(`${phase}: persisted complete without a run id \u2014 live phase evidence decides the leg`);
|
|
23755
23737
|
continue;
|
|
23756
23738
|
}
|
|
23757
|
-
if (
|
|
23739
|
+
if (phase === "ordinaryCi" && rec.runRepo != null && rec.runRepo !== input.repo) {
|
|
23740
|
+
throw new ReleaseLedgerError("invalid-record", `the persisted ordinaryCi run ${rec.runId} belongs to ${rec.runRepo}, not the release repo ${input.repo} \u2014 refusing unrelated run evidence (#7807)`);
|
|
23741
|
+
}
|
|
23742
|
+
if (phase === "ordinaryCi" && rec.sha != null && rec.sha.toLowerCase() !== input.sha.toLowerCase()) {
|
|
23743
|
+
throw new ReleaseLedgerError("tag-sha-mismatch", `the persisted ordinaryCi boundary is for ${rec.sha.slice(0, 12)} but the active release tag is ${input.sha.slice(0, 12)} \u2014 refusing unrelated SHA evidence (#7807)`);
|
|
23744
|
+
}
|
|
23745
|
+
if (phase === "ordinaryCi" && rec.runProvenance !== "sha" || phase !== "ordinaryCi" && rec.runProvenance !== "sha" && rec.runProvenance !== "dispatch") {
|
|
23758
23746
|
throw new ReleaseLedgerError(
|
|
23759
23747
|
"invalid-record",
|
|
23760
|
-
`the persisted ${phase} boundary names run ${rec.runId} without valid runProvenance ('sha'|'dispatch' required, got ${JSON.stringify(rec.runProvenance ?? null)}) \u2014 a run-backed boundary without provenance cannot be re-verified; failing closed (#5987)`
|
|
23748
|
+
`the persisted ${phase} boundary names run ${rec.runId} without valid runProvenance ('sha'${phase === "ordinaryCi" ? "" : "|'dispatch'"} required, got ${JSON.stringify(rec.runProvenance ?? null)}) \u2014 a run-backed boundary without provenance cannot be re-verified; failing closed (#5987)`
|
|
23761
23749
|
);
|
|
23762
23750
|
}
|
|
23763
23751
|
const runRepo = rec.runRepo ?? input.repo;
|
|
@@ -23792,7 +23780,8 @@ async function reverifyPersistedRunPhases(deps, path2, anchors, input) {
|
|
|
23792
23780
|
}
|
|
23793
23781
|
if (meta.conclusion === "success") {
|
|
23794
23782
|
if (rec.state !== "complete") {
|
|
23795
|
-
|
|
23783
|
+
const { error: _error, ...withoutPriorError } = rec;
|
|
23784
|
+
phases[phase] = { ...withoutPriorError, state: "complete", note: `re-verified live: ${liveLabel} concluded success`, updatedAt: now().toISOString() };
|
|
23796
23785
|
notes.push(`${phase}: ${liveLabel} re-verified green live`);
|
|
23797
23786
|
}
|
|
23798
23787
|
continue;
|
|
@@ -25070,6 +25059,19 @@ async function runTrainDoctor(input) {
|
|
|
25070
25059
|
try {
|
|
25071
25060
|
let ledger = parseReleaseLedger(ledgerRaw);
|
|
25072
25061
|
let derived = deriveReleasePhaseStatus(ledger);
|
|
25062
|
+
const ordinaryCi = ledger.phases.ordinaryCi;
|
|
25063
|
+
if (healLedger && ordinaryCi.state === "failed" && ordinaryCi.runId != null) {
|
|
25064
|
+
const outcome = await reverifyPersistedRunPhases(train, ledgerPath, {
|
|
25065
|
+
repo: ledger.repo,
|
|
25066
|
+
tag: ledger.tag,
|
|
25067
|
+
tagSha: ledger.tagSha
|
|
25068
|
+
}, { sha: ledger.tagSha, repo: ledger.repo, phases: ["ordinaryCi"] });
|
|
25069
|
+
if (outcome.notes.some((note) => note.startsWith("ordinaryCi:") && note.includes("re-verified green live"))) {
|
|
25070
|
+
add({ code: "ledger-ordinary-ci-rerun", severity: "healed", source: "origin", title: `the exact ordinaryCi run ${ordinaryCi.runId} for ${ledger.tag} is green on its recorded release SHA (#7807)`, remedy: "nothing \u2014 the ordinaryCi ledger leg now records the live green run" });
|
|
25071
|
+
ledger = parseReleaseLedger(deps.readLedgerRaw(ledgerPath) ?? ledgerRaw);
|
|
25072
|
+
derived = deriveReleasePhaseStatus(ledger);
|
|
25073
|
+
}
|
|
25074
|
+
}
|
|
25073
25075
|
const deployModel = typeof meta?.deployModel === "string" && isDeployModel(meta.deployModel) ? meta.deployModel : void 0;
|
|
25074
25076
|
if (healLedger && deployModel && derived.legs.some((l) => (l.phase === "deploy" || l.phase === "publish") && l.state === "pending")) {
|
|
25075
25077
|
const run = await deps.healRunLegs(train, cwd, deployModel);
|
|
@@ -26174,7 +26176,9 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
|
|
|
26174
26176
|
const ledgerAnchors = { repo: ctx.repo, tag, tagSha: releaseSha };
|
|
26175
26177
|
try {
|
|
26176
26178
|
deps.warn?.("Release: Checking the package before publishing");
|
|
26179
|
+
const dryRunStart = (deps.now ?? Date.now)();
|
|
26177
26180
|
await verifyPublishDryRun(deps, ctx, meta, deployModel);
|
|
26181
|
+
deps.warn?.(`Release: publish dry run took ${(((deps.now ?? Date.now)() - dryRunStart) / 1e3).toFixed(1)}s`);
|
|
26178
26182
|
} catch (e) {
|
|
26179
26183
|
throw await recoverFailedFold(deps, e, startBranch, preFold.mainSha, resumeCommand);
|
|
26180
26184
|
}
|
|
@@ -26412,7 +26416,7 @@ async function runRcandResumeLocked(deps, options) {
|
|
|
26412
26416
|
await requireCleanTree(deps);
|
|
26413
26417
|
await runGitRemoteRead(deps, ["fetch", "origin", "--tags"]);
|
|
26414
26418
|
const meta = requireProjectMetaForTrain(await loadProjectMeta(deps, ctx), ctx.repo);
|
|
26415
|
-
const suiteRefusal = suiteMemberRefusal(ctx.repo, meta, "rcand",
|
|
26419
|
+
const suiteRefusal = suiteMemberRefusal(ctx.repo, meta, "rcand", options.suiteSolo === true, deps.suiteCut);
|
|
26416
26420
|
if (suiteRefusal) throw new Error(`rcand --resume: ${suiteRefusal}`);
|
|
26417
26421
|
const branchHints = await loadReleaseTrackBranchHints(deps);
|
|
26418
26422
|
if (isHubControlRepo(ctx.repo) || resolveReleaseTrack(meta, branchHints) === "direct") {
|
|
@@ -27725,7 +27729,6 @@ async function listWorkflowPaths(deps, repo, branch) {
|
|
|
27725
27729
|
return e?.status === 404 ? [] : void 0;
|
|
27726
27730
|
}
|
|
27727
27731
|
}
|
|
27728
|
-
var AGENT_PR_BOOKKEEPING_CONTEXTS = /* @__PURE__ */ new Set(["guard", "verdict"]);
|
|
27729
27732
|
function gateWorkflowFiles(prWorkflowPaths) {
|
|
27730
27733
|
const files = prWorkflowPaths.map((p) => p.slice(p.lastIndexOf("/") + 1)).filter((f) => f !== "agent-pr.yml");
|
|
27731
27734
|
return files.length ? files : DEFAULT_GATE_FILES;
|
|
@@ -28062,8 +28065,7 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
28062
28065
|
}
|
|
28063
28066
|
}
|
|
28064
28067
|
if (explicitNoCi) {
|
|
28065
|
-
const
|
|
28066
|
-
const emitted = resolved?.filter((c) => !AGENT_PR_BOOKKEEPING_CONTEXTS.has(c));
|
|
28068
|
+
const emitted = await getEmittedPrContexts();
|
|
28067
28069
|
if (emitted && emitted.length > 0) {
|
|
28068
28070
|
checks.push({
|
|
28069
28071
|
ok: false,
|
|
@@ -28194,7 +28196,7 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
28194
28196
|
if (hasRequiredChecks) {
|
|
28195
28197
|
const gateFiles = new Set(gateWorkflowFiles(prWorkflowPaths));
|
|
28196
28198
|
const gateWorkflows = (await getBranchWorkflows() ?? []).filter((wf) => gateFiles.has(wf.path.slice(wf.path.lastIndexOf("/") + 1)));
|
|
28197
|
-
const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !
|
|
28199
|
+
const unblocking = unblockingGateJobs(gateWorkflows, new Set(liveContexts)).filter((job) => !branchPlan?.pending.some((entry) => entry.branch === baseBranch && entry.contexts.some((context) => contextMatchesJobId(job, context))));
|
|
28198
28200
|
checks.push({
|
|
28199
28201
|
ok: unblocking.length === 0,
|
|
28200
28202
|
label: UNBLOCKING_GATE_JOBS_LABEL,
|
|
@@ -28217,8 +28219,7 @@ async function auditRepoCi(repo, callerDeps) {
|
|
|
28217
28219
|
for (const ref of releaseBranches) {
|
|
28218
28220
|
emittedByBranch[ref] = await resolveEmittedPrContexts(deps, repo, ref.replace("refs/heads/", ""));
|
|
28219
28221
|
}
|
|
28220
|
-
const
|
|
28221
|
-
const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore });
|
|
28222
|
+
const unreachable = unreachableReleaseContexts({ requiredContexts: liveContexts, releaseBranches, emittedByBranch, ignore: TRAIN_PR_ONLY_CONTEXTS });
|
|
28222
28223
|
checks.push({
|
|
28223
28224
|
ok: unreachable.length === 0,
|
|
28224
28225
|
label: RELEASE_BRANCH_REACHABLE_LABEL,
|
|
@@ -28685,8 +28686,7 @@ async function authoritativeSafetyHold(authority, deps, repo, baseBranch, meta)
|
|
|
28685
28686
|
if (authority.coversReleaseBranches && authority.source !== "registry META requiredChecks") {
|
|
28686
28687
|
const track = resolveReleaseTrack(meta, void 0, repo);
|
|
28687
28688
|
const releaseBranches = branchesForTrack(track).filter((b) => b !== baseBranch);
|
|
28688
|
-
const
|
|
28689
|
-
const requiredForReleaseBranches = authority.contexts.filter((context) => !ignore.has(context));
|
|
28689
|
+
const requiredForReleaseBranches = authority.contexts.filter((context) => !TRAIN_PR_ONLY_CONTEXTS.has(context));
|
|
28690
28690
|
for (const branch of releaseBranches) {
|
|
28691
28691
|
if (await branchPresence(deps, repo, branch) !== true) continue;
|
|
28692
28692
|
const emittedThere = await resolveEmittedPrContexts(deps, repo, branch);
|
|
@@ -30122,11 +30122,13 @@ function registerBoardCommands(program3) {
|
|
|
30122
30122
|
}
|
|
30123
30123
|
function claimVerdict(ref, result) {
|
|
30124
30124
|
const holder = formatClaimHolder(result.holder);
|
|
30125
|
-
const previousHolder = result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane";
|
|
30125
|
+
const previousHolder = result.staleStamp ?? (result.previousHolder ? formatClaimHolder(result.previousHolder) : "another lane");
|
|
30126
30126
|
const reclaimed = result.reclaimedFrom ? ` (reclaimed from ${result.reclaimedFrom})` : "";
|
|
30127
30127
|
if (result.checked) {
|
|
30128
30128
|
if (result.outcome === "held") return `Check ${ref}: held by ${holder} - claim would renew the lease (nothing written)`;
|
|
30129
|
-
if (result.outcome === "took-over")
|
|
30129
|
+
if (result.outcome === "took-over") {
|
|
30130
|
+
return result.staleStamp ? `Check ${ref}: ${previousHolder} - claim would take it over for ${holder} (nothing written)` : `Check ${ref}: held by ${previousHolder} - --force claim would take it over for ${holder} (nothing written)`;
|
|
30131
|
+
}
|
|
30130
30132
|
if (result.outcome === "resumed") return `Check ${ref}: prior lane ${previousHolder} is verifiably dead - claim would resume its work for ${holder} (nothing written; ${result.resumeEvidence})`;
|
|
30131
30133
|
return `Check ${ref}: free - claim would proceed for ${holder}${reclaimed} (nothing written)`;
|
|
30132
30134
|
}
|
|
@@ -30507,7 +30509,7 @@ function trainPlan(command, options = {}) {
|
|
|
30507
30509
|
{ label: "fold the version bump into the release commit \u2014 runs inside the apply step, no separate bump PR", gated: true },
|
|
30508
30510
|
{ label: "tag release and publish GitHub Release", gated: true },
|
|
30509
30511
|
{ label: "trigger the deploy path for this repo model, returning Hub Actions run id/url data (and, with --watch, its outcome)", command: "tenant-container: gh workflow run tenant-deploy.yml ... then gh run list/watch", gated: true },
|
|
30510
|
-
{ label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli devops runtime tenant control <owner/repo> rc retire", gated: true },
|
|
30512
|
+
...!isBundleOnlySuiteMember(options.repo ?? "", options.suite) ? [{ label: "retire the rc runtime (rc is ephemeral \u2014 non-fatal, reported as rcRetirement)", command: "mmi-cli devops runtime tenant control <owner/repo> rc retire", gated: true }] : [],
|
|
30511
30513
|
{ label: "roll development forward and align rc to the released main", gated: true },
|
|
30512
30514
|
{ label: "synchronize the owning Project short description + thin README from the repo docs and current member repos", command: "mmi-cli oracle org project sync-info --apply", gated: true }
|
|
30513
30515
|
];
|
|
@@ -30515,6 +30517,8 @@ function trainPlan(command, options = {}) {
|
|
|
30515
30517
|
return [
|
|
30516
30518
|
authority,
|
|
30517
30519
|
{ label: "verify current branch is rc", gated: true },
|
|
30520
|
+
// #7783: the version the apply will mint, read from the rc tag at origin/rc's head (never a stale higher cycle).
|
|
30521
|
+
{ label: `resolve the release tag from the rc head (${options.releaseTagError ? `error: ${options.releaseTagError}` : `releaseTag=${options.releaseTag ?? "(unresolved)"}`})`, command: "node scripts/next-version.mjs release", gated: true },
|
|
30518
30522
|
{ label: "verify registry META for this project", command: "mmi-cli oracle org project get <owner/repo>", gated: true },
|
|
30519
30523
|
{ label: "preflight required main secret names", command: "mmi-cli vault secrets preflight --stage main --repo <owner/repo>", gated: true },
|
|
30520
30524
|
{ label: "preflight GitHub Actions hosted job start (billing/spending) before minting a tag", command: "train dispatches actions-job-start-canary.yml on MMI-Hub (#5604); exact-run --retry-publish skips this", gated: true },
|
|
@@ -31409,7 +31413,7 @@ var SETTABLE_VAR_HINTS = {
|
|
|
31409
31413
|
requiredGcpApis: "comma-string",
|
|
31410
31414
|
requiredRuntimeSecrets: 'JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}',
|
|
31411
31415
|
requiredBuildSecrets: 'JSON flat array, e.g. ["NODE_AUTH_TOKEN=@github-packages-token"]',
|
|
31412
|
-
tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",secretOutput?:{path,vault:{repo,key},overwrite?}}}; use {artifact} in argv when required; a secretOutput is master-only',
|
|
31416
|
+
tenantTasks: 'JSON map {name:{service,command[],stages[],timeoutSeconds?,artifact?:"required",secretOutput?:{path,vault:{repo,key},overwrite?}}} \u2014 merged per entry, at most 20 stored in total; retire ONE task with --unset-task NAME; use {artifact} in argv when required; a secretOutput is master-only',
|
|
31413
31417
|
secrets: "JSON catalog map keyed by KEY {key,purpose,group,owner,stages[],consumers[]} \u2014 merged per entry; drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets; prefer --secrets-file",
|
|
31414
31418
|
edgeDomains: "JSON {dev,rc,main} domain map",
|
|
31415
31419
|
statusOptions: "JSON name\u2192id map",
|
|
@@ -31440,6 +31444,18 @@ function duplicateVarKeyAcrossFlags(vars, sets) {
|
|
|
31440
31444
|
}
|
|
31441
31445
|
return null;
|
|
31442
31446
|
}
|
|
31447
|
+
function tenantTaskWriteRefusal(existing, patch) {
|
|
31448
|
+
const remove2 = Array.isArray(patch.tenantTasksRemove) ? patch.tenantTasksRemove : [];
|
|
31449
|
+
const stored = existing?.tenantTasks ?? {};
|
|
31450
|
+
const slug = existing?.slug ?? "this project";
|
|
31451
|
+
const missing = remove2.filter((name) => stored[name] === void 0);
|
|
31452
|
+
if (missing.length > 0) return `org project set: --unset-task ${missing.join(", ")} not declared in ${slug}'s tenantTasks (${Object.keys(stored).length} declared) \u2014 nothing to remove`;
|
|
31453
|
+
const sent = patch.tenantTasks;
|
|
31454
|
+
if (sent === null || sent === void 0) return null;
|
|
31455
|
+
const merged = /* @__PURE__ */ new Set([...Object.keys(stored), ...Object.keys(sent)]);
|
|
31456
|
+
if (merged.size > 20) return `org project set: ${slug} would hold ${merged.size} tenantTasks (${Object.keys(stored).length} declared today); the cap is 20 \u2014 retire some first with --unset-task <name>`;
|
|
31457
|
+
return null;
|
|
31458
|
+
}
|
|
31443
31459
|
function secretRemovalRefusal(existing, patch) {
|
|
31444
31460
|
const remove2 = Array.isArray(patch.secretsRemove) ? patch.secretsRemove : [];
|
|
31445
31461
|
if (remove2.length === 0) return null;
|
|
@@ -31614,12 +31630,26 @@ function buildProjectSetPatch(input) {
|
|
|
31614
31630
|
}
|
|
31615
31631
|
patch.secretsRemove = keys;
|
|
31616
31632
|
}
|
|
31633
|
+
if (input.unsetTasks?.length) {
|
|
31634
|
+
if (patch.tenantTasks !== void 0) {
|
|
31635
|
+
throw new Error("org project set: --unset-task cannot be combined with --var tenantTasks or --unset tenantTasks \u2014 pass one or the other");
|
|
31636
|
+
}
|
|
31637
|
+
const names = [];
|
|
31638
|
+
for (const raw of input.unsetTasks) {
|
|
31639
|
+
const name = raw.trim();
|
|
31640
|
+
if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(name)) {
|
|
31641
|
+
throw new Error(`org project set: --unset-task must be a task name (lowercase kebab-case) \u2014 got ${JSON.stringify(raw)}`);
|
|
31642
|
+
}
|
|
31643
|
+
if (!names.includes(name)) names.push(name);
|
|
31644
|
+
}
|
|
31645
|
+
patch.tenantTasksRemove = names;
|
|
31646
|
+
}
|
|
31617
31647
|
if (input.clearWebProfile) {
|
|
31618
31648
|
patch.oauth = null;
|
|
31619
31649
|
patch.edgeDomains = null;
|
|
31620
31650
|
}
|
|
31621
31651
|
if (Object.keys(patch).length === 0) {
|
|
31622
|
-
throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, --unset-secret KEY, and/or --clear-web-profile");
|
|
31652
|
+
throw new Error("org project set: nothing to set - pass --class, --project-type, --deploy-model, --release-track, --var KEY=VALUE, --unset KEY, --unset-secret KEY, --unset-task NAME, and/or --clear-web-profile");
|
|
31623
31653
|
}
|
|
31624
31654
|
return patch;
|
|
31625
31655
|
}
|
|
@@ -35796,8 +35826,24 @@ async function readSuiteStatus(deps) {
|
|
|
35796
35826
|
})));
|
|
35797
35827
|
if (!deps.leadTags || !deps.registryAt) return { suite: "mm-work", members: rows };
|
|
35798
35828
|
const tags = await deps.leadTags().catch(() => []);
|
|
35799
|
-
const latest = (
|
|
35800
|
-
|
|
35829
|
+
const latest = (rc2) => tags.filter((t) => /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(t) && t.includes("-rc.") === rc2).sort((a, b) => compareVersions(b, a))[0];
|
|
35830
|
+
let rc;
|
|
35831
|
+
let rcLabel = "n/a";
|
|
35832
|
+
const rcTags = tags.filter((t) => /^v\d+\.\d+\.\d+-rc\.\d+$/.test(t)).sort((a, b) => compareVersions(b, a));
|
|
35833
|
+
if (rcTags.length) {
|
|
35834
|
+
const rcHead = await deps.head(rows[0].repo, "rc").catch(() => void 0);
|
|
35835
|
+
if (rcHead === void 0) rcLabel = "unknown";
|
|
35836
|
+
else {
|
|
35837
|
+
for (const t of rcTags) {
|
|
35838
|
+
if ((await deps.tagCommit(rows[0].repo, t).catch(() => void 0))?.toLowerCase() === rcHead?.toLowerCase()) {
|
|
35839
|
+
rc = t;
|
|
35840
|
+
break;
|
|
35841
|
+
}
|
|
35842
|
+
}
|
|
35843
|
+
rcLabel = rc ?? "none at head";
|
|
35844
|
+
}
|
|
35845
|
+
}
|
|
35846
|
+
const leadTag = { rc, main: latest(false) };
|
|
35801
35847
|
const lead = rows[0].stages;
|
|
35802
35848
|
for (const stage of ["rc", "main"]) {
|
|
35803
35849
|
const tag = leadTag[stage];
|
|
@@ -35813,7 +35859,7 @@ async function readSuiteStatus(deps) {
|
|
|
35813
35859
|
}
|
|
35814
35860
|
}
|
|
35815
35861
|
const recorded = leadTag.main && deps.leadReleaseBody ? parseSuiteMapping(await deps.leadReleaseBody(leadTag.main).catch(() => null)) : void 0;
|
|
35816
|
-
return { suite: "mm-work", lead: { dev: lead[0].head, rc:
|
|
35862
|
+
return { suite: "mm-work", lead: { dev: lead[0].head, rc: rcLabel, main: leadTag.main ?? "unknown", ...recorded ? { members: recorded } : {} }, members: rows };
|
|
35817
35863
|
}
|
|
35818
35864
|
var short = (v) => /^[a-f0-9]{40}$/i.test(v) ? v.slice(0, 12) : v;
|
|
35819
35865
|
var proofText = (p) => p ? p.state === "proven" ? "proven" : p.state === "mismatch" ? `mismatch (${p.detail})` : `unprovable (${p.detail})` : "";
|
|
@@ -35855,7 +35901,8 @@ async function setSuiteHold(deps, member, hold) {
|
|
|
35855
35901
|
const after = await deps.read(slug);
|
|
35856
35902
|
if (!after.ok || !after.project) throw new Error(`${found.repo}: write sent but read-back failed; verify with org project get`);
|
|
35857
35903
|
const got = after.project.suiteHold ?? null;
|
|
35858
|
-
|
|
35904
|
+
const same2 = got === null || hold === null ? got === hold : got.reason === hold.reason && got.since === hold.since;
|
|
35905
|
+
if (!same2) throw new Error(`${found.repo}: read-back suiteHold ${JSON.stringify(got)} does not match what was written`);
|
|
35859
35906
|
if (track && after.project.releaseTrack !== track) throw new Error(`${found.repo}: releaseTrack changed from ${track} to ${after.project.releaseTrack ?? "unset"} by the write; restore it with org project set --release-track ${track}`);
|
|
35860
35907
|
return hold ? `${found.repo}: HELD (${hold.reason})` : `${found.repo}: hold cleared`;
|
|
35861
35908
|
}
|
|
@@ -36037,7 +36084,7 @@ async function carriedPins(deps, member, bundles) {
|
|
|
36037
36084
|
}
|
|
36038
36085
|
throw new Error(`${member.repo}: nothing new since rc, but no pinned MM-Work bundle carries development ${member.development.slice(0, 12)}; pin it before the suite cut`);
|
|
36039
36086
|
}
|
|
36040
|
-
function
|
|
36087
|
+
function abandonableCut(recordPath, ownerGo) {
|
|
36041
36088
|
const record = readSuiteCut(recordPath);
|
|
36042
36089
|
if (!record) throw new Error("no suite cut record to abandon");
|
|
36043
36090
|
if (record.leadReleased || record.members.some((m) => m.released)) {
|
|
@@ -36046,6 +36093,19 @@ function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Dat
|
|
|
36046
36093
|
if (record.pinPr && !ownerGo) {
|
|
36047
36094
|
throw new Error(`the cut already opened pin PR ${record.pinPr}; its pins may be visible, so resume it (suite rcand --owner-go), release it (suite release), or retire it with suite abandon --owner-go`);
|
|
36048
36095
|
}
|
|
36096
|
+
return record;
|
|
36097
|
+
}
|
|
36098
|
+
async function suiteMarkAbandoned(deps, ownerGo = false) {
|
|
36099
|
+
const record = abandonableCut(deps.recordPath, ownerGo);
|
|
36100
|
+
const tags = [
|
|
36101
|
+
...record.members.flatMap((m) => m.tag && m.sha ? [{ repo: m.repo, tag: m.tag, sha: m.sha }] : []),
|
|
36102
|
+
...record.leadRcTag && record.leadRcSha ? [{ repo: SUITE_REPO, tag: record.leadRcTag, sha: record.leadRcSha }] : []
|
|
36103
|
+
];
|
|
36104
|
+
for (const t of tags) await deps.markAbandoned(t.repo, t.tag, t.sha);
|
|
36105
|
+
return tags.map((t) => `${t.repo}@${t.tag}`);
|
|
36106
|
+
}
|
|
36107
|
+
function suiteAbandon(recordPath, ownerGo = false, now = /* @__PURE__ */ new Date()) {
|
|
36108
|
+
const record = abandonableCut(recordPath, ownerGo);
|
|
36049
36109
|
const archive = `${recordPath}.abandoned-${record.leadDevelopment.slice(0, 12)}-${now.toISOString().replace(/[:.]/g, "")}.json`;
|
|
36050
36110
|
if ((0, import_node_fs29.existsSync)(archive)) throw new Error(`abandoned-cut archive already exists at ${archive}; inspect before abandoning again`);
|
|
36051
36111
|
(0, import_node_fs29.renameSync)(recordPath, archive);
|
|
@@ -36932,12 +36992,20 @@ ${lines2.join("\n")}
|
|
|
36932
36992
|
Land ONE development-based cite-only synthesis PR for the whole batch (#4643 supersedes the old cut-from-origin/main wording, which produced main-lineage PRs into development \u2014 the shape portFoldToDevelopment forbids, #4365/#4371): cut the branch FROM origin/development, regenerate the listed conflict content so it cherry-picks cleanly onto current origin/main, and land that development-parented PR with the cite-only marker (the proven #5096 flow), then rerun \`mmi-cli devops hotfix start --from ${batchFrom}\` with the port merge SHA(s) \u2014 never hand-resolve onto main; never a main-parented branch into development; never N serial ports (#3167) \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#hotfix-pick-refused`;
|
|
36933
36993
|
}
|
|
36934
36994
|
var CHERRY_PICKED_FROM = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
|
|
36935
|
-
async function
|
|
36936
|
-
if (sources.length === 0) return [];
|
|
36995
|
+
async function scanHotfixPickGaps(deps, sources, tolerated, rcRef) {
|
|
36996
|
+
if (sources.length === 0) return { gaps: [], rcCarried: 0 };
|
|
36937
36997
|
const namedShas = new Set(sources.map((s) => s.sha.toLowerCase()));
|
|
36938
36998
|
const carried = /* @__PURE__ */ new Set();
|
|
36939
36999
|
const mainOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", "origin/development..origin/main"]);
|
|
36940
37000
|
for (const m of mainOnly.matchAll(CHERRY_PICKED_FROM)) carried.add(m[1].toLowerCase());
|
|
37001
|
+
const notOnRc = /* @__PURE__ */ new Set();
|
|
37002
|
+
const rcPicked = /* @__PURE__ */ new Set();
|
|
37003
|
+
if (rcRef) {
|
|
37004
|
+
(await deps.run("git", ["log", "--no-merges", "--format=%H", `${rcRef}..origin/development`])).split("\n").map((s) => s.trim().toLowerCase()).filter(Boolean).forEach((s) => notOnRc.add(s));
|
|
37005
|
+
const rcOnly = await deps.run("git", ["log", "--no-merges", "--format=%B", `origin/development..${rcRef}`]);
|
|
37006
|
+
for (const m of rcOnly.matchAll(CHERRY_PICKED_FROM)) rcPicked.add(m[1].toLowerCase());
|
|
37007
|
+
}
|
|
37008
|
+
let rcCarried = 0;
|
|
36941
37009
|
const gaps = [];
|
|
36942
37010
|
const devOnly = (await deps.run("git", ["log", "--no-merges", "--reverse", "--format=%H%x09%s", "origin/main..origin/development"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
36943
37011
|
for (const line of devOnly) {
|
|
@@ -36945,11 +37013,37 @@ async function findHotfixPickGaps(deps, sources, tolerated) {
|
|
|
36945
37013
|
if (!sha) continue;
|
|
36946
37014
|
const lower = sha.toLowerCase();
|
|
36947
37015
|
if (namedShas.has(lower) || carried.has(lower)) continue;
|
|
37016
|
+
if (rcRef && (!notOnRc.has(lower) || rcPicked.has(lower))) {
|
|
37017
|
+
rcCarried++;
|
|
37018
|
+
continue;
|
|
37019
|
+
}
|
|
36948
37020
|
const paths = (await deps.run("git", ["show", "--no-color", "--name-only", "--pretty=format:", sha])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
36949
37021
|
if (paths.length === 0 || paths.every((p) => pathIsTolerated(p, tolerated))) continue;
|
|
36950
37022
|
gaps.push({ sha, subject: subjectParts.join(" ") });
|
|
36951
37023
|
}
|
|
36952
|
-
return gaps;
|
|
37024
|
+
return { gaps, rcCarried };
|
|
37025
|
+
}
|
|
37026
|
+
async function resolveActiveRcCandidate(deps) {
|
|
37027
|
+
try {
|
|
37028
|
+
await deps.run("git", ["rev-parse", "--verify", "--quiet", "origin/rc"]);
|
|
37029
|
+
} catch {
|
|
37030
|
+
return { reason: "origin/rc does not exist" };
|
|
37031
|
+
}
|
|
37032
|
+
try {
|
|
37033
|
+
const rcTags = (await deps.run("git", ["tag", "--points-at", "origin/rc", "--list", "v*"])).split("\n").map((s) => s.trim()).filter((t) => /^v\d+\.\d+\.\d+-rc\.\d+$/.test(t));
|
|
37034
|
+
const tag = rcTags.sort((a, b) => Number(a.split("-rc.")[1]) - Number(b.split("-rc.")[1])).pop();
|
|
37035
|
+
if (!tag) return { reason: "no rc tag points at the origin/rc head, so it is not an active candidate" };
|
|
37036
|
+
if ((await deps.run("git", ["ls-remote", "origin", `refs/abandoned-rc/${tag}`])).trim()) {
|
|
37037
|
+
return { reason: `${tag} was abandoned (marker refs/abandoned-rc/${tag} on origin), so it is not an active candidate` };
|
|
37038
|
+
}
|
|
37039
|
+
const released = tag.replace(/-rc\.\d+$/, "");
|
|
37040
|
+
if ((await deps.run("git", ["tag", "--list", released])).trim()) {
|
|
37041
|
+
return { reason: `${released} is already released, so ${tag} is no longer a pending candidate` };
|
|
37042
|
+
}
|
|
37043
|
+
return { ref: "origin/rc", tag };
|
|
37044
|
+
} catch (e) {
|
|
37045
|
+
return { reason: `the origin/rc candidate could not be read (${e.message ?? e})` };
|
|
37046
|
+
}
|
|
36953
37047
|
}
|
|
36954
37048
|
function formatHotfixPickGapRefusal(gaps, tag, fromSpecs) {
|
|
36955
37049
|
const lines2 = gaps.map((g) => ` - ${g.sha.slice(0, 7)} ${g.subject}`);
|
|
@@ -37169,7 +37263,16 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
37169
37263
|
const label = sources.map((s) => s.label).join(", ");
|
|
37170
37264
|
const { foldPaths, pickTolerated } = await resolveHotfixPickTolerated(deps, deployModel);
|
|
37171
37265
|
const ackedShas = (options.ack ?? []).map((a) => a.trim().toLowerCase()).filter(Boolean);
|
|
37172
|
-
|
|
37266
|
+
let rcCandidate;
|
|
37267
|
+
if (resolveReleaseTrack(meta, void 0, ctx.repo) === "full") {
|
|
37268
|
+
const found = await resolveActiveRcCandidate(deps);
|
|
37269
|
+
if ("reason" in found) notes.push(`pick-completeness guard checked against origin/main only: ${found.reason} (#7799)`);
|
|
37270
|
+
else rcCandidate = found;
|
|
37271
|
+
}
|
|
37272
|
+
const { gaps: pickGaps, rcCarried } = await scanHotfixPickGaps(deps, sources, pickTolerated, rcCandidate?.ref);
|
|
37273
|
+
if (rcCandidate && rcCarried > 0) {
|
|
37274
|
+
notes.push(`pick-completeness: ${rcCarried} development commit(s) carried by rc candidate ${rcCandidate.tag}, not counted as gaps (#7799)`);
|
|
37275
|
+
}
|
|
37173
37276
|
const unacked = pickGaps.filter((g) => {
|
|
37174
37277
|
const sha = g.sha.toLowerCase();
|
|
37175
37278
|
return !ackedShas.some((a) => sha.startsWith(a) || a.startsWith(sha));
|
|
@@ -37262,7 +37365,9 @@ async function runHotfixStartLocked(deps, options, doctor) {
|
|
|
37262
37365
|
|
|
37263
37366
|
<!-- mmi-hotfix-carries: ${activeSources.map((s) => s.sha).join(",")} -->${pickGaps.length > 0 ? `
|
|
37264
37367
|
|
|
37265
|
-
<!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}
|
|
37368
|
+
<!-- mmi-hotfix-acked-excluded: ${pickGaps.map((g) => g.sha).join(",")} (#6994 pick-completeness) -->` : ""}${rcCandidate && rcCarried > 0 ? `
|
|
37369
|
+
|
|
37370
|
+
<!-- mmi-hotfix-rc-carried: ${rcCarried} commits carried by ${rcCandidate.tag} -->` : ""}
|
|
37266
37371
|
|
|
37267
37372
|
Merge this PR (human-initiated), then run \`mmi-cli devops hotfix release ${tag}\`.`
|
|
37268
37373
|
]));
|
|
@@ -37502,7 +37607,7 @@ async function waitForHotfixRequiredChecks(deps, ctx, mergedSha, required, fresh
|
|
|
37502
37607
|
const recover = hooks.recover ?? transientRecoveryForFailedPhase;
|
|
37503
37608
|
let checks;
|
|
37504
37609
|
try {
|
|
37505
|
-
checks = await wait(deps, ctx, mergedSha, required, freshSince);
|
|
37610
|
+
checks = await wait(deps, ctx, mergedSha, required, freshSince, { exactCommitGreenSuffices: true });
|
|
37506
37611
|
} catch (e) {
|
|
37507
37612
|
const failureText = e instanceof Error ? e.message : String(e);
|
|
37508
37613
|
let recovery;
|
|
@@ -37531,7 +37636,7 @@ release ledger: ${recoveryFailure} - manual intervention required`);
|
|
|
37531
37636
|
|
|
37532
37637
|
${recovery.note}`);
|
|
37533
37638
|
try {
|
|
37534
|
-
checks = await wait(deps, ctx, mergedSha, required, void 0);
|
|
37639
|
+
checks = await wait(deps, ctx, mergedSha, required, void 0, { exactCommitGreenSuffices: true });
|
|
37535
37640
|
} catch (e2) {
|
|
37536
37641
|
const revalidationFailure = e2 instanceof Error ? e2.message : String(e2);
|
|
37537
37642
|
const prior2 = (await ledger.load(anchors).catch(() => void 0))?.phases.ordinaryCi;
|
|
@@ -40048,6 +40153,30 @@ function releaseProgressVerdict(status, subject = "Release") {
|
|
|
40048
40153
|
// src/suite-pin.ts
|
|
40049
40154
|
var import_promises6 = require("node:fs/promises");
|
|
40050
40155
|
var import_node_path34 = require("node:path");
|
|
40156
|
+
var SuitePinError = class _SuitePinError extends Error {
|
|
40157
|
+
diagnostic;
|
|
40158
|
+
constructor(repo, tag, error) {
|
|
40159
|
+
const e = error instanceof _SuitePinError ? { ...error.diagnostic } : error && typeof error === "object" ? error : {};
|
|
40160
|
+
const code = typeof e.code === "number" && Number.isInteger(e.code) && e.code > 0 && e.code <= 255 ? e.code : typeof e.code === "string" && /^(ETIMEDOUT|EACCES|EPERM|EBUSY|ENOENT|ENOSPC|EIO|EEXIST|ECONNRESET|ECONNREFUSED|EPIPE)$/.test(e.code) ? e.code : void 0;
|
|
40161
|
+
const signal = typeof e.signal === "string" && /^(SIGTERM|SIGKILL|SIGINT|SIGHUP|SIGABRT|SIGPIPE)$/.test(e.signal) ? e.signal : void 0;
|
|
40162
|
+
const hint = [error instanceof _SuitePinError ? error.message : e.message, e.stderr].filter((s) => typeof s === "string").map((s) => s.slice(0, 4096)).join("\n");
|
|
40163
|
+
const carriedCategory = error instanceof _SuitePinError && typeof e.category === "string" && ["timeout", "permission", "index-lock", "terminated", "exit", "unknown"].includes(e.category) ? e.category : void 0;
|
|
40164
|
+
const category = carriedCategory ?? (code === "ETIMEDOUT" ? "timeout" : code === "EACCES" || code === "EPERM" || /permission denied/i.test(hint) ? "permission" : /index\.lock.*(?:exists|unable|denied)|unable.*index\.lock/i.test(hint) ? "index-lock" : signal || e.killed === true ? "terminated" : typeof code === "number" ? "exit" : "unknown");
|
|
40165
|
+
const diagnostic = {
|
|
40166
|
+
category,
|
|
40167
|
+
...code !== void 0 ? { code } : {},
|
|
40168
|
+
...signal ? { signal } : {},
|
|
40169
|
+
...typeof e.killed === "boolean" ? { killed: e.killed } : {},
|
|
40170
|
+
...typeof e.durationMs === "number" && Number.isFinite(e.durationMs) && e.durationMs >= 0 && e.durationMs <= 864e5 ? { durationMs: e.durationMs } : {},
|
|
40171
|
+
...e.cleanupFailed === true ? { cleanupFailed: true } : {}
|
|
40172
|
+
};
|
|
40173
|
+
const safeRepo = /^[\w.-]+\/[\w.-]+$/.test(repo) && repo.length <= 200 ? repo : "[invalid repo]";
|
|
40174
|
+
const safeTag = /^v\d+\.\d+\.\d+(?:-rc\.\d+)?$/.test(tag) && tag.length <= 80 ? tag : "[invalid tag]";
|
|
40175
|
+
const reason = /closed without landing/.test(hint) ? "pin PR closed without landing" : /merged but development/.test(hint) ? "merged PR does not match the development pin" : /unreadable|META unreadable/.test(hint) ? "registry unreadable" : /not a declared MM-Work module/.test(hint) ? "not a declared MM-Work module" : /no .* bundle host/.test(hint) ? "bundle host missing" : diagnostic.category === "permission" ? "permission denied" : diagnostic.category;
|
|
40176
|
+
super(`suite pin PR FAILED for ${safeRepo} ${safeTag}: ${reason}; remedy: open/merge the ${SUITE_REPO} development pin PR for ${safeRepo} ${safeTag}, then rerun the same train command; do not recut the tag \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#suite-pin-pr`);
|
|
40177
|
+
this.diagnostic = diagnostic;
|
|
40178
|
+
}
|
|
40179
|
+
};
|
|
40051
40180
|
async function openSuiteCutPinPr(deps, input) {
|
|
40052
40181
|
if (!/^[a-f0-9]{40}$/.test(input.cut) || !input.pins.length) throw new Error("suite pin PR needs an anchored cut and at least one module");
|
|
40053
40182
|
const ids = input.pins.map((p) => p.module);
|
|
@@ -40190,8 +40319,8 @@ async function openSuiteRestorePinPr(deps, input) {
|
|
|
40190
40319
|
}
|
|
40191
40320
|
async function openSuitePinPr(deps, input) {
|
|
40192
40321
|
const { repo, tag, stage } = input;
|
|
40193
|
-
const remedy = `open/merge the ${SUITE_REPO} development pin PR for ${repo} ${tag}, then rerun the same train command; do not recut the tag \u2014 see https://github.com/mutmutco/MMI-Hub/blob/development/docs/Guides/train-troubleshooting.md#suite-pin-pr`;
|
|
40194
40322
|
let scratch = "";
|
|
40323
|
+
let failure;
|
|
40195
40324
|
try {
|
|
40196
40325
|
const get = JSON.parse(await deps.run("gh", ["api", `repos/${SUITE_REPO}/contents/seed/modules.json?ref=development`]));
|
|
40197
40326
|
if (!get.content) throw new Error("MM-Work seed/modules.json was unreadable");
|
|
@@ -40256,9 +40385,19 @@ async function openSuitePinPr(deps, input) {
|
|
|
40256
40385
|
await run("gh", ["pr", "close", old2.url, "--repo", SUITE_REPO, "--comment", `Superseded by ${pr} (${member.id} ${stage} at ${tag}).`]);
|
|
40257
40386
|
return { status: "pending", pr, url: pin };
|
|
40258
40387
|
} catch (e) {
|
|
40259
|
-
|
|
40388
|
+
failure = new SuitePinError(repo, tag, e);
|
|
40389
|
+
throw failure;
|
|
40260
40390
|
} finally {
|
|
40261
|
-
if (scratch)
|
|
40391
|
+
if (scratch) {
|
|
40392
|
+
try {
|
|
40393
|
+
await (0, import_promises6.rm)(scratch, { recursive: true, force: true });
|
|
40394
|
+
} catch (e) {
|
|
40395
|
+
const cleanupFailure = failure ?? new SuitePinError(repo, tag, e);
|
|
40396
|
+
cleanupFailure.diagnostic.cleanupFailed = true;
|
|
40397
|
+
cleanupFailure.message += "; owned scratch cleanup also failed";
|
|
40398
|
+
if (!failure) throw cleanupFailure;
|
|
40399
|
+
}
|
|
40400
|
+
}
|
|
40262
40401
|
}
|
|
40263
40402
|
}
|
|
40264
40403
|
|
|
@@ -45813,7 +45952,7 @@ async function requireFreshTrainCli(commandName) {
|
|
|
45813
45952
|
process.exit(converge.reexecCode);
|
|
45814
45953
|
}
|
|
45815
45954
|
var GH_TRAIN_TIMEOUT_MS = 3e4;
|
|
45816
|
-
var GH_RUN_WATCH_TIMEOUT_MS =
|
|
45955
|
+
var GH_RUN_WATCH_TIMEOUT_MS = 240 * 6e4;
|
|
45817
45956
|
var NODE_PREPARE_TIMEOUT_MS = 10 * 6e4;
|
|
45818
45957
|
var NODE_VERIFY_TIMEOUT_MS = 5 * 6e4;
|
|
45819
45958
|
var NPM_TRAIN_TIMEOUT_MS = 6e4;
|
|
@@ -46235,10 +46374,43 @@ function emitTrainResult(label, output, out) {
|
|
|
46235
46374
|
if (!out) printLine(output);
|
|
46236
46375
|
else printLine(`Wrote ${label} result to ${out} (UTF-8, ${writeUtf8Receipt(out, output)} bytes)`);
|
|
46237
46376
|
}
|
|
46377
|
+
var TrainPinFollowUpError = class extends Error {
|
|
46378
|
+
constructor(completed, error) {
|
|
46379
|
+
const failure = new SuitePinError(completed.repo, completed.tag, error);
|
|
46380
|
+
super(failure.message);
|
|
46381
|
+
this.completed = completed;
|
|
46382
|
+
this.diagnostic = failure.diagnostic;
|
|
46383
|
+
}
|
|
46384
|
+
completed;
|
|
46385
|
+
diagnostic;
|
|
46386
|
+
};
|
|
46238
46387
|
function writeTrainFailureReceipt(commandName, json, out, error, suiteSolo = false) {
|
|
46239
|
-
|
|
46240
|
-
|
|
46241
|
-
|
|
46388
|
+
const completed = error instanceof TrainPinFollowUpError ? error.completed : void 0;
|
|
46389
|
+
if (!out && !completed) return;
|
|
46390
|
+
const command = commandName.includes(" ") ? commandName : `${commandName} --apply`;
|
|
46391
|
+
const legs = completed?.releaseVerdict?.legs ?? (completed?.command === "hotfix-release" ? hotfixFollowUpLegs(completed.runs, completed.foldStatus, ledgerAlignmentFollowUpOutcome(completed.ledger), completed.foldNote, completed.deployNote) : []);
|
|
46392
|
+
const failedLeg = { leg: "suite-pin-pr", status: "failed", error: error.message };
|
|
46393
|
+
const failure = completed ? {
|
|
46394
|
+
...completed,
|
|
46395
|
+
suiteSolo: true,
|
|
46396
|
+
suitePin: { status: "failed", error: error.message, diagnostic: error.diagnostic },
|
|
46397
|
+
failedLeg: "suite-pin-pr",
|
|
46398
|
+
...completed.command === "hotfix-release" ? { legs: [...legs, failedLeg] } : {},
|
|
46399
|
+
releaseVerdict: {
|
|
46400
|
+
releaseUrl: null,
|
|
46401
|
+
...completed.releaseVerdict,
|
|
46402
|
+
releaseStatus: "succeeded",
|
|
46403
|
+
followUpStatus: "failed",
|
|
46404
|
+
promoted: true,
|
|
46405
|
+
tag: completed.tag,
|
|
46406
|
+
legs: [...legs, failedLeg]
|
|
46407
|
+
}
|
|
46408
|
+
} : { command, ...suiteSolo ? { suiteSolo: true } : {} };
|
|
46409
|
+
const evidence = JSON.stringify({ ...failure, verdict: "failed", error: error.message }, null, 2);
|
|
46410
|
+
const output = json ? evidence : `mmi-cli ${command}: FAILED \u2014 ${completed ? `${completed.repo} ${completed.tag} published; suite-pin-pr follow-up FAILED \u2014 ` : ""}${error.message}${completed ? `
|
|
46411
|
+
Completed train evidence and failed follow-up:
|
|
46412
|
+
${evidence}` : ""}`;
|
|
46413
|
+
emitTrainResult(command, output, out);
|
|
46242
46414
|
}
|
|
46243
46415
|
function renderTenantRedeploy(r) {
|
|
46244
46416
|
return `mmi-cli devops runtime tenant redeploy: ${r.repo} ${r.stage} (ref=${r.ref}) [${r.deployModel}]; ${renderDeployLine(r)}`;
|
|
@@ -46264,8 +46436,15 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46264
46436
|
const root = (await execFileP("git", ["rev-parse", "--show-toplevel"])).stdout.trim();
|
|
46265
46437
|
return openSuitePinPr({ root, run: async (file, args, cwd) => (await execFileP(file, args, { cwd, timeout: GIT_TIMEOUT_MS })).stdout }, { repo, tag, stage: "main" });
|
|
46266
46438
|
}
|
|
46439
|
+
async function pinCompletedMember(completed) {
|
|
46440
|
+
try {
|
|
46441
|
+
return await pinSoloMember(completed.repo, completed.tag);
|
|
46442
|
+
} catch (e) {
|
|
46443
|
+
throw new TrainPinFollowUpError(completed, e);
|
|
46444
|
+
}
|
|
46445
|
+
}
|
|
46267
46446
|
for (const commandName of ["rcand", "release"]) {
|
|
46268
|
-
const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit project-admin or master approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded repo-authorized train after explicit approval (Hub: master only)").option("--suite-solo", "release one suite module alone (requires --owner-go;
|
|
46447
|
+
const trainCmd = program3.command(commandName).description(`plan ${commandName} train operations; mutations require explicit project-admin or master approval`).option("--json", "machine-readable output").option("--watch", "block on the deploy/publish workflow runs and report their outcomes").option("--apply", "execute the guarded repo-authorized train after explicit approval (Hub: master only)").option("--suite-solo", "release one suite module alone, or cut the suite lead alone (requires --owner-go; a member rcand is always refused)").option("--owner-go", "explicit owner approval for a suite-solo backend release").option("--resume", commandName === "rcand" ? "finish a candidate whose immutable public rc tag passed policy but origin/rc was not pushed (#3881)" : "finish a partial release or its protected post-release alignment without re-cutting, republishing, or redeploying (#3851/#3885)").option("--out <path>", "write the terminal result (--apply, --resume, --abort, --retry-publish) to this file as UTF-8 (no BOM) instead of stdout \u2014 the shell-free receipt path (#5983/#6068)");
|
|
46269
46448
|
const RELEASE_ONLY_FLAGS = [
|
|
46270
46449
|
{ flags: "--announce-summary-file <path>", description: "agent-curated 3-6 line release Slack summary; required for a new MMI-Hub --apply (#883/#3901) and for a product repo whose registry META sets `releaseChannel` (#6523), in its `releaseLanguage`; nothing is posted to a project channel without it" },
|
|
46271
46450
|
{ flags: "--ack <shas>", description: "comma-separated dev shas a human verified are in the candidate, overriding the hotfix-coverage guard for a conflicted port whose -x trailer was lost (#958)" },
|
|
@@ -46451,7 +46630,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46451
46630
|
const raw2 = await withReleaseProgress(
|
|
46452
46631
|
trainApplyDeps(),
|
|
46453
46632
|
"Checking the existing release candidate",
|
|
46454
|
-
(deps) => runRcandResume(deps, { watch: o.watch })
|
|
46633
|
+
(deps) => runRcandResume(deps, { watch: o.watch, suiteSolo: o.suiteSolo })
|
|
46455
46634
|
);
|
|
46456
46635
|
return emitRcandContinuation("rcand --resume", raw2, o);
|
|
46457
46636
|
}
|
|
@@ -46467,7 +46646,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46467
46646
|
() => runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo)
|
|
46468
46647
|
);
|
|
46469
46648
|
const { followUpStatus, releaseVerdict } = buildResumeReleaseVerdict(result, projectInfoSync);
|
|
46470
|
-
const suitePin = o.suiteSolo ? await
|
|
46649
|
+
const suitePin = o.suiteSolo ? await pinCompletedMember({ ...result, projectInfoSync, releaseVerdict }) : void 0;
|
|
46471
46650
|
if (suitePin?.status === "pending") {
|
|
46472
46651
|
releaseVerdict.legs.push({ leg: "suite-pin-pr", status: "pending", error: `merge ${suitePin.pr} on MM-Work development, then verify the pin` });
|
|
46473
46652
|
releaseVerdict.followUpStatus = "pending";
|
|
@@ -46480,6 +46659,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46480
46659
|
} catch (e) {
|
|
46481
46660
|
applyTrainFollowUpExit("failed");
|
|
46482
46661
|
consoleIo.err(`mmi-cli: Release: ${progressSubject(commandName)} blocked \u2014 see the error below`);
|
|
46662
|
+
writeTrainFailureReceipt(`${commandName} --resume`, o.json === true, o.out, e, o.suiteSolo === true);
|
|
46483
46663
|
return failGraceful(`${commandName} --resume: ${e.message}`);
|
|
46484
46664
|
}
|
|
46485
46665
|
}
|
|
@@ -46533,8 +46713,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46533
46713
|
() => runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo)
|
|
46534
46714
|
);
|
|
46535
46715
|
}
|
|
46536
|
-
const suitePin = o.suiteSolo && commandName === "release" ? await pinSoloMember(result.repo, result.tag) : void 0;
|
|
46537
46716
|
const verdict = buildReleaseVerdict(commandName, result, projectInfoSync);
|
|
46717
|
+
const suitePin = o.suiteSolo && commandName === "release" ? await pinCompletedMember({ ...result, projectInfoSync, releaseVerdict: verdict.releaseVerdict }) : void 0;
|
|
46538
46718
|
if (suitePin?.status === "pending" && verdict.releaseVerdict) {
|
|
46539
46719
|
verdict.releaseVerdict.legs.push({ leg: "suite-pin-pr", status: "pending", error: `merge ${suitePin.pr} on MM-Work development, then verify the pin` });
|
|
46540
46720
|
verdict.releaseVerdict.followUpStatus = "pending";
|
|
@@ -46563,6 +46743,8 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46563
46743
|
const repo = o.repo ?? await resolveRepo();
|
|
46564
46744
|
const targets = commandName === "rcand" && !o.repo ? await resolveRcandPlanTargets() : void 0;
|
|
46565
46745
|
let releaseTrack;
|
|
46746
|
+
let suite;
|
|
46747
|
+
let preOneZeroPlan = false;
|
|
46566
46748
|
let releaseChannel = null;
|
|
46567
46749
|
let releaseLanguage = "en";
|
|
46568
46750
|
if (repo) {
|
|
@@ -46579,14 +46761,24 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46579
46761
|
}
|
|
46580
46762
|
const raw = read.project?.releaseTrack;
|
|
46581
46763
|
if (isReleaseTrack(raw)) releaseTrack = raw;
|
|
46764
|
+
suite = read.project?.suite;
|
|
46765
|
+
preOneZeroPlan = preOneZeroInForce(read.project ?? void 0);
|
|
46582
46766
|
releaseChannel = read.project?.releaseChannel?.trim() || null;
|
|
46583
46767
|
releaseLanguage = read.project?.releaseLanguage?.trim() || "en";
|
|
46584
46768
|
}
|
|
46585
|
-
|
|
46769
|
+
let releaseTarget;
|
|
46770
|
+
if (commandName === "release" && !o.repo && !o.dev && releaseTrack !== "direct" && releaseTrack !== "trunk" && repo?.toLowerCase() !== "mutmutco/mmi-hub") {
|
|
46771
|
+
try {
|
|
46772
|
+
releaseTarget = { releaseTag: (await execFileP("node", ["scripts/next-version.mjs", "release", ...preOneZeroPlan ? ["--pre-one-zero"] : []], { timeout: 15e3 })).stdout.trim() };
|
|
46773
|
+
} catch (e) {
|
|
46774
|
+
releaseTarget = { releaseTagError: (e.stderr || e.message).trim() };
|
|
46775
|
+
}
|
|
46776
|
+
}
|
|
46777
|
+
const steps = trainPlan(commandName, { ...targets ?? {}, ...releaseTarget ?? {}, repo, dev: o.dev, releaseTrack, suite });
|
|
46586
46778
|
let doctor;
|
|
46587
46779
|
if (repo && (!o.repo || (await resolveRepo())?.toLowerCase() === repo.toLowerCase())) {
|
|
46588
46780
|
try {
|
|
46589
|
-
doctor = await runTrainDoctor({ lane: commandName, dev: commandName === "release" && o.dev === true, heal: false, train: trainApplyDeps(), repo });
|
|
46781
|
+
doctor = await runTrainDoctor({ lane: commandName, dev: commandName === "release" && o.dev === true, heal: false, train: trainApplyDeps(), repo, suiteSolo: o.suiteSolo === true });
|
|
46590
46782
|
} catch (e) {
|
|
46591
46783
|
consoleIo.err(`mmi-cli: train doctor did not complete: ${e.message}`);
|
|
46592
46784
|
}
|
|
@@ -46674,10 +46866,10 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46674
46866
|
foldPort
|
|
46675
46867
|
}));
|
|
46676
46868
|
} catch (e) {
|
|
46677
|
-
if (sub === "release"
|
|
46678
|
-
|
|
46679
|
-
emitTrainResult("hotfix release", o.json ? JSON.stringify({ command: "hotfix release", suiteSolo: true, suitePin: { status: "failed", error }, verdict: "failed" }, null, 2) : `hotfix release: suite pin PR FAILED \u2014 ${error}`, o.out);
|
|
46869
|
+
if (sub === "release") {
|
|
46870
|
+
writeTrainFailureReceipt("hotfix release", o.json || Boolean(hotfixCmd.opts().json), o.out, e, o.suiteSolo === true);
|
|
46680
46871
|
}
|
|
46872
|
+
applyTrainFollowUpExit("failed");
|
|
46681
46873
|
return failGraceful(`hotfix ${sub}: ${e.message}`);
|
|
46682
46874
|
}
|
|
46683
46875
|
}
|
|
@@ -46705,7 +46897,7 @@ function registerTrainOperationsCommands(program3, runProjectInfoSyncCallback) {
|
|
|
46705
46897
|
}
|
|
46706
46898
|
return runHotfixSub("release", async () => {
|
|
46707
46899
|
const result = await runHotfixRelease(trainApplyDeps(), version, { announceSummaryFile: o.announceSummaryFile, carries: o.carries ? [o.carries] : [], suiteSolo: o.suiteSolo, ...o.ack ? { ack: o.ack.split(",").map((s) => s.trim()).filter(Boolean) } : {} });
|
|
46708
|
-
const suitePin = o.suiteSolo ? await
|
|
46900
|
+
const suitePin = o.suiteSolo ? await pinCompletedMember(result) : void 0;
|
|
46709
46901
|
const projectInfoSync = await runProjectInfoSyncLeg(runProjectInfoSyncCallback, result.repo);
|
|
46710
46902
|
return { ...result, projectInfoSync, ...o.suiteSolo ? { suiteSolo: true, suitePin } : {} };
|
|
46711
46903
|
}, o, renderHotfixRelease);
|
|
@@ -47041,6 +47233,13 @@ function suiteTrainIo(root, summaryFile, memberSummaryFiles = {}) {
|
|
|
47041
47233
|
publishMain,
|
|
47042
47234
|
preflightMain,
|
|
47043
47235
|
pin: (input) => openSuiteCutPinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
|
|
47236
|
+
markAbandoned: async (repo, tag, sha) => {
|
|
47237
|
+
try {
|
|
47238
|
+
await gh(["api", "-X", "POST", `repos/${repo}/git/refs`, "-f", `ref=refs/abandoned-rc/${tag}`, "-f", `sha=${sha}`]);
|
|
47239
|
+
} catch (e) {
|
|
47240
|
+
if (!/already exists/i.test(e.message)) throw e;
|
|
47241
|
+
}
|
|
47242
|
+
},
|
|
47044
47243
|
restoreMain: (input) => openSuiteRestorePinPr({ root, run: (file, args, cwd) => exec(file, args, cwd, GIT_TIMEOUT_MS) }, input),
|
|
47045
47244
|
verifyLeadCut: async (sha) => {
|
|
47046
47245
|
const current = await gh(["api", `repos/${SUITE_REPO}/git/ref/heads/development`, "--jq", ".object.sha"]);
|
|
@@ -47205,9 +47404,11 @@ function registerSuiteCommands(program3) {
|
|
|
47205
47404
|
const unlock = await acquireTrainWriterLock(root, "suite abandon");
|
|
47206
47405
|
let result;
|
|
47207
47406
|
let restored;
|
|
47407
|
+
let marked;
|
|
47208
47408
|
try {
|
|
47209
47409
|
const io = suiteTrainIo(root);
|
|
47210
47410
|
restored = o.ownerGo === true ? await suiteRestoreMainPins(io) : void 0;
|
|
47411
|
+
marked = await suiteMarkAbandoned(io, o.ownerGo === true);
|
|
47211
47412
|
result = suiteAbandon(io.recordPath, o.ownerGo === true);
|
|
47212
47413
|
} finally {
|
|
47213
47414
|
await unlock();
|
|
@@ -47216,6 +47417,7 @@ function registerSuiteCommands(program3) {
|
|
|
47216
47417
|
`suite abandon: archived the cut record to ${result.archive}`,
|
|
47217
47418
|
...result.orphanTags.length ? [" member rc tags left in place (immutable; the next cut reuses one still unreleased on its rc head, #7450):", ...result.orphanTags.map((t) => ` - ${t}`)] : [" no member rc tag was minted"],
|
|
47218
47419
|
...restored ? [` main pins the cut planned for unreleased tags are restored by ${restored.pr} (${restored.status}, #7758)`] : [],
|
|
47420
|
+
...marked.length ? [` marked abandoned on origin, so the hotfix guard stops counting them as active (#7801): ${marked.join(", ")}`] : [],
|
|
47219
47421
|
" the next suite rcand --owner-go re-snapshots every member"
|
|
47220
47422
|
].join("\n"));
|
|
47221
47423
|
} catch (e) {
|
|
@@ -47734,7 +47936,6 @@ Next command: ${report.nextCommand}`);
|
|
|
47734
47936
|
var RESTART_LINE = "\u21BB Restart Claude to finish.";
|
|
47735
47937
|
var HOST_RESTART_LINES = {
|
|
47736
47938
|
codex: "\u21BB Restart Codex to finish.",
|
|
47737
|
-
cursor: "\u21BB Restart Cursor to finish.",
|
|
47738
47939
|
jervcode: "\u21BB Restart JervCode to finish."
|
|
47739
47940
|
};
|
|
47740
47941
|
function restartLineForHost(host) {
|
|
@@ -47966,92 +48167,6 @@ var surfaces_default = {
|
|
|
47966
48167
|
"Hosted tools and disabled or untrusted hooks bypass local enforcement."
|
|
47967
48168
|
]
|
|
47968
48169
|
},
|
|
47969
|
-
{
|
|
47970
|
-
token: "cursor",
|
|
47971
|
-
displayName: "Cursor",
|
|
47972
|
-
lifecycle: "active",
|
|
47973
|
-
artifactIds: [
|
|
47974
|
-
"mmi-cursor-plugin",
|
|
47975
|
-
"mmi-skills",
|
|
47976
|
-
"mmi-cli"
|
|
47977
|
-
],
|
|
47978
|
-
plannedArtifactIds: [],
|
|
47979
|
-
assembly: {
|
|
47980
|
-
mode: "package-directory",
|
|
47981
|
-
rootPath: "packages/cursor-plugin",
|
|
47982
|
-
sync: [
|
|
47983
|
-
{
|
|
47984
|
-
mode: "directories",
|
|
47985
|
-
sourcePath: "skills",
|
|
47986
|
-
targetPath: "packages/cursor-plugin/skills"
|
|
47987
|
-
},
|
|
47988
|
-
{
|
|
47989
|
-
mode: "files",
|
|
47990
|
-
sourcePath: "bin",
|
|
47991
|
-
targetPath: "packages/cursor-plugin/bin",
|
|
47992
|
-
include: [
|
|
47993
|
-
"mmi-cli",
|
|
47994
|
-
"mmi-cli.cmd"
|
|
47995
|
-
]
|
|
47996
|
-
},
|
|
47997
|
-
{
|
|
47998
|
-
mode: "files",
|
|
47999
|
-
sourcePath: "cli",
|
|
48000
|
-
targetPath: "packages/cursor-plugin/cli",
|
|
48001
|
-
include: [
|
|
48002
|
-
"dist/index.cjs",
|
|
48003
|
-
"dist/main.cjs",
|
|
48004
|
-
"package.json"
|
|
48005
|
-
]
|
|
48006
|
-
},
|
|
48007
|
-
{
|
|
48008
|
-
mode: "files",
|
|
48009
|
-
sourcePath: ".cursor-plugin",
|
|
48010
|
-
targetPath: "packages/cursor-plugin/.cursor-plugin",
|
|
48011
|
-
include: [
|
|
48012
|
-
"plugin.json"
|
|
48013
|
-
]
|
|
48014
|
-
}
|
|
48015
|
-
]
|
|
48016
|
-
},
|
|
48017
|
-
install: {
|
|
48018
|
-
mechanism: "github-source",
|
|
48019
|
-
locator: "~/.cursor/plugins/local/mmi"
|
|
48020
|
-
},
|
|
48021
|
-
upgrade: {
|
|
48022
|
-
mechanism: "reinstall",
|
|
48023
|
-
reload: "workspace"
|
|
48024
|
-
},
|
|
48025
|
-
skills: {
|
|
48026
|
-
delivery: "plugin",
|
|
48027
|
-
sourceSurfaceId: "mmi-skills",
|
|
48028
|
-
artifactId: "mmi-skills",
|
|
48029
|
-
invocation: {
|
|
48030
|
-
entry: "/epic",
|
|
48031
|
-
any: "/<skill>"
|
|
48032
|
-
}
|
|
48033
|
-
},
|
|
48034
|
-
cli: {
|
|
48035
|
-
delivery: "standalone",
|
|
48036
|
-
artifactId: "mmi-cli"
|
|
48037
|
-
},
|
|
48038
|
-
ownership: {
|
|
48039
|
-
trust: "host",
|
|
48040
|
-
cache: "mmi-hub",
|
|
48041
|
-
repair: "mmi-hub"
|
|
48042
|
-
},
|
|
48043
|
-
certification: {
|
|
48044
|
-
hostCommand: "cursor-agent",
|
|
48045
|
-
versionArgs: [
|
|
48046
|
-
"--version"
|
|
48047
|
-
]
|
|
48048
|
-
},
|
|
48049
|
-
enforcementCeilings: [
|
|
48050
|
-
"Cursor can replace MCP results only; it cannot rewrite ordinary tool output after execution, though MMI still detects secret-shaped output.",
|
|
48051
|
-
"Final assistant output is outside Cursor hook control.",
|
|
48052
|
-
"Cloud agents do not run sessionStart or sessionEnd hooks, and disabled plugins or untrusted workspaces enforce nothing."
|
|
48053
|
-
]
|
|
48054
|
-
},
|
|
48055
48170
|
{
|
|
48056
48171
|
token: "jervcode",
|
|
48057
48172
|
displayName: "JervCode",
|
|
@@ -48281,38 +48396,6 @@ var surfaces_default = {
|
|
|
48281
48396
|
},
|
|
48282
48397
|
publishVisibility: "public"
|
|
48283
48398
|
},
|
|
48284
|
-
{
|
|
48285
|
-
id: "mmi-cursor-plugin",
|
|
48286
|
-
classification: "packaging",
|
|
48287
|
-
kind: "plugin",
|
|
48288
|
-
ownerPath: "packages/cursor-plugin/package.json",
|
|
48289
|
-
deliveryPath: "packages/cursor-plugin/package.json",
|
|
48290
|
-
delivery: "npm",
|
|
48291
|
-
applicability: "Cursor IDE and Cursor Agent CLI; npm package @mutmutco/cursor-plugin carrying the plugin manifest and skills. The managed checkout at ~/.cursor/plugins/local/mmi is switched to each release by the fleet updater (#4945).",
|
|
48292
|
-
versionCoordinated: true,
|
|
48293
|
-
surfaceToken: "cursor",
|
|
48294
|
-
versionPaths: [
|
|
48295
|
-
{
|
|
48296
|
-
path: ".cursor-plugin/plugin.json",
|
|
48297
|
-
pointer: "version"
|
|
48298
|
-
},
|
|
48299
|
-
{
|
|
48300
|
-
path: "packages/cursor-plugin/package.json",
|
|
48301
|
-
pointer: "version"
|
|
48302
|
-
}
|
|
48303
|
-
],
|
|
48304
|
-
additionalPaths: [
|
|
48305
|
-
"packages/cursor-plugin/.cursor-plugin",
|
|
48306
|
-
"packages/cursor-plugin/skills",
|
|
48307
|
-
"packages/cursor-plugin/bin",
|
|
48308
|
-
"packages/cursor-plugin/cli"
|
|
48309
|
-
],
|
|
48310
|
-
artifactIdentity: {
|
|
48311
|
-
kind: "npm-pack",
|
|
48312
|
-
packagePath: "packages/cursor-plugin"
|
|
48313
|
-
},
|
|
48314
|
-
publishVisibility: "public"
|
|
48315
|
-
},
|
|
48316
48399
|
{
|
|
48317
48400
|
id: "mmi-claude-marketplace",
|
|
48318
48401
|
classification: "delivery",
|
|
@@ -48604,7 +48687,7 @@ var surfaces_default = {
|
|
|
48604
48687
|
};
|
|
48605
48688
|
|
|
48606
48689
|
// src/surface-doctor.ts
|
|
48607
|
-
var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "
|
|
48690
|
+
var TOKENS = /* @__PURE__ */ new Set(["claude", "codex", "jervcode", "hermes"]);
|
|
48608
48691
|
function isActiveToken(value) {
|
|
48609
48692
|
return TOKENS.has(value);
|
|
48610
48693
|
}
|
|
@@ -50511,20 +50594,12 @@ async function readOrFail(read) {
|
|
|
50511
50594
|
async function defaultRegistryRead() {
|
|
50512
50595
|
return fetchSchedulesList(registryClientDeps(await loadConfig()));
|
|
50513
50596
|
}
|
|
50514
|
-
async function
|
|
50515
|
-
|
|
50516
|
-
}
|
|
50517
|
-
function modelRoleKeyOf(row) {
|
|
50518
|
-
const model = row.model;
|
|
50519
|
-
return typeof model === "string" && model.trim() ? model : void 0;
|
|
50520
|
-
}
|
|
50521
|
-
async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead, readProjects = defaultProjectsRead, readRunVerdicts = defaultRunVerdictRead) {
|
|
50522
|
-
const [gh2, aws, registryRead, projectsRead, runVerdictRead] = await Promise.all([
|
|
50597
|
+
async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defaultRegistryRead, readProjects = defaultProjectsRead) {
|
|
50598
|
+
const [gh2, aws, registryRead, projectsRead] = await Promise.all([
|
|
50523
50599
|
githubEntries(client),
|
|
50524
50600
|
awsEntries(),
|
|
50525
50601
|
readOrFail(readRegistry),
|
|
50526
|
-
readOrFail(readProjects)
|
|
50527
|
-
readOrFail(readRunVerdicts)
|
|
50602
|
+
readOrFail(readProjects)
|
|
50528
50603
|
]);
|
|
50529
50604
|
const readFailures = [];
|
|
50530
50605
|
if (registryRead.state === "failed") {
|
|
@@ -50534,16 +50609,10 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
50534
50609
|
readFailures.push(`registry: could not read the projects list \u2014 schedulesMode opt-outs unresolved (nothing suppressed) \u2014 ${projectsRead.error}`);
|
|
50535
50610
|
}
|
|
50536
50611
|
const registry2 = registryRead.state === "ok" ? registryRead.value : null;
|
|
50537
|
-
const runVerdicts = runVerdictRead.state === "ok" ? runVerdictRead.value : null;
|
|
50538
50612
|
const projects = projectsRead.state === "ok" ? projectsRead.value : null;
|
|
50539
|
-
const decoratedAws = decorateAwsModel(
|
|
50540
|
-
aws.entries,
|
|
50541
|
-
registry2?.map((r) => ({ id: r.id, executor: r.executor, model: modelRoleKeyOf(r) })) ?? null,
|
|
50542
|
-
runVerdicts
|
|
50543
|
-
);
|
|
50544
50613
|
const recon = assembleReconciliation(gh2.entries, new Set(gh2.readRepos), registry2, new Set(gh2.workflowNames), {
|
|
50545
50614
|
// #3286: the harbour side joins registry rows against the live aws-scheduler clocks by scheduleId.
|
|
50546
|
-
awsEntries:
|
|
50615
|
+
awsEntries: aws.entries,
|
|
50547
50616
|
schedulerRead: Boolean(aws.schedulerRead)
|
|
50548
50617
|
}, new Set(gh2.disabledWorkflowNames));
|
|
50549
50618
|
const selfManaged = /* @__PURE__ */ new Set();
|
|
@@ -50557,7 +50626,7 @@ async function fetchNotebook(client = defaultGitHubClient(), readRegistry = defa
|
|
|
50557
50626
|
);
|
|
50558
50627
|
const driftLines = reconciliation.map(renderDrift);
|
|
50559
50628
|
return {
|
|
50560
|
-
entries: sortEntries([...gh2.entries, ...
|
|
50629
|
+
entries: sortEntries([...gh2.entries, ...aws.entries, ...DECLARED_ENTRIES]),
|
|
50561
50630
|
incomplete: [...gh2.incomplete, ...aws.incomplete, ...recon.incomplete, ...readFailures],
|
|
50562
50631
|
drift: driftLines,
|
|
50563
50632
|
reconciliation,
|
|
@@ -53282,7 +53351,7 @@ projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one
|
|
|
53282
53351
|
}
|
|
53283
53352
|
if (!result.ok) return failGraceful(`org project deploy retire: ${result.error ?? "read failed"}`);
|
|
53284
53353
|
});
|
|
53285
|
-
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset-secret <KEY...>", "remove exactly these #2244 catalog entries (repeatable; KEY or KEY@dev|rc|main). Refuses while a stage still requires the key \u2014 drop it from requiredRuntimeSecrets/requiredBuildSecrets first").option("--confirm-clear-secrets", "required to let --unset secrets wipe the WHOLE catalog (the refusal names how many entries that removes)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets (the WHOLE catalog \u2014 needs --confirm-clear-secrets), edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
53354
|
+
project.command("set [owner/repo]").description("upsert project META (idempotent merge; master-gated)").addOption(new Option("--class <class>", "deployable | content").choices(["deployable", "content"])).addOption(new Option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (capability shape)`).choices([...PROJECT_TYPES])).addOption(new Option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).choices([...DEPLOY_MODELS])).addOption(new Option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).choices([...RELEASE_TRACKS])).option("--var <KEY=VALUE...>", settableVarHelp()).option("--set <KEY=VALUE...>", "alias of --var (one KEY=VALUE per flag; repeat the flag to set several)").option("--secrets-file <path>", 'read the #2244 secrets catalog map (JSON) from a file and merge it per entry into the existing catalog (drop ONE entry with --unset-secret KEY; clear ALL with --unset secrets --confirm-clear-secrets). SHAPE: a JSON object keyed by env name, each entry {"key":"UPPER_SNAKE" (repeat the env name), "purpose":"<non-empty>", "group":"<e.g. auth|database>", "owner":"<github login>", "stages":[] (empty = the one stageless shared value; else any of dev/rc/main), "consumers":["runtime"|"box"|?], "provider":"<optional>"}').option("--unset-secret <KEY...>", "remove exactly these #2244 catalog entries (repeatable; KEY or KEY@dev|rc|main). Refuses while a stage still requires the key \u2014 drop it from requiredRuntimeSecrets/requiredBuildSecrets first").option("--unset-task <NAME...>", "retire exactly these tenant tasks (repeatable; lowercase kebab-case names). Refuses a name that is not declared. tenantTasks merges per entry and is capped at 20 stored in total").option("--confirm-clear-secrets", "required to let --unset secrets wipe the WHOLE catalog (the refusal names how many entries that removes)").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, requiredBuildSecrets, secrets (the WHOLE catalog \u2014 needs --confirm-clear-secrets), edgeDomains, requiredGcpApis, publishRequired, releaseChannel, releaseLanguage").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
53286
53355
|
const cfg = await loadConfig();
|
|
53287
53356
|
let target;
|
|
53288
53357
|
try {
|
|
@@ -53313,6 +53382,7 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
53313
53382
|
vars,
|
|
53314
53383
|
unsets: o.unset ?? [],
|
|
53315
53384
|
unsetSecrets: o.unsetSecret ?? [],
|
|
53385
|
+
unsetTasks: o.unsetTask ?? [],
|
|
53316
53386
|
clearWebProfile: Boolean(o.clearWebProfile)
|
|
53317
53387
|
});
|
|
53318
53388
|
} catch (e) {
|
|
@@ -53323,6 +53393,8 @@ project.command("set [owner/repo]").description("upsert project META (idempotent
|
|
|
53323
53393
|
if (clearRefusal) return fail(clearRefusal);
|
|
53324
53394
|
const removalRefusal = secretRemovalRefusal(existing, patch);
|
|
53325
53395
|
if (removalRefusal) return fail(removalRefusal);
|
|
53396
|
+
const taskRefusal = tenantTaskWriteRefusal(existing, patch);
|
|
53397
|
+
if (taskRefusal) return fail(taskRefusal);
|
|
53326
53398
|
const boardError = boardLinkWriteError(patch, existing);
|
|
53327
53399
|
if (boardError) return fail(`org project set: ${boardError}`);
|
|
53328
53400
|
const res = await upsertProject(slug, { ...patch, repo }, registryClientDeps(cfg));
|