@mutmutco/claude-plugin 4.6.25 → 4.6.27
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/.claude-plugin/plugin.json +1 -1
- package/cli/dist/main.cjs +99 -41
- package/cli/package.json +1 -1
- package/package.json +1 -1
- package/skills/bootstrap/seeds/gate.template.yml +4 -0
- package/skills/bootstrap/seeds/suite-gate.template.yml +6 -0
- package/skills/bootstrap/seeds/suite-guide.template.md +5 -1
- package/skills/org-guide/references/design.md +10 -1
- package/skills/org-guide/references/work-and-releases.md +2 -0
- package/skills/release/SKILL.md +2 -0
- package/skills/release/train-common.md +5 -1
package/cli/dist/main.cjs
CHANGED
|
@@ -25243,6 +25243,9 @@ var CMD_RE = /`mmi-cli ((?:[a-z][a-z-]*)(?: [a-z][a-z-]*){0,3})/g;
|
|
|
25243
25243
|
var RETIRED_RE = /\b(retired|historical|removed|deleted|gone|no longer|superseded|legacy)\b/i;
|
|
25244
25244
|
var ROOT_DOCS = ["README.md", "architecture.md"];
|
|
25245
25245
|
var SKIP_WALK = ["docs/decisions/"];
|
|
25246
|
+
function formatDocRefFinding(finding) {
|
|
25247
|
+
return `${finding.doc}:${finding.line} [${finding.kind}] ${finding.detail}${finding.suggest?.length ? ` (did you mean ${finding.suggest[0]}?)` : ""}`;
|
|
25248
|
+
}
|
|
25246
25249
|
function refFirstSegment(ref) {
|
|
25247
25250
|
return ref.replace(/^(\.\/)+/, "").split("/")[0];
|
|
25248
25251
|
}
|
|
@@ -25663,21 +25666,14 @@ function defaultIsIgnored(root, relPaths, exec3 = import_node_child_process6.exe
|
|
|
25663
25666
|
throw error;
|
|
25664
25667
|
}
|
|
25665
25668
|
}
|
|
25666
|
-
function
|
|
25667
|
-
if (firstSegments.length === 0) return /* @__PURE__ */ new Set();
|
|
25669
|
+
function defaultTrackedFiles(root, firstSegments, exec3 = import_node_child_process6.execFileSync) {
|
|
25668
25670
|
try {
|
|
25669
25671
|
const out = exec3("git", ["ls-files", "-z", "--", ...firstSegments.map((s) => `:(literal)${s}`)], {
|
|
25670
25672
|
cwd: root,
|
|
25671
25673
|
encoding: "utf8",
|
|
25672
25674
|
maxBuffer: CHECK_IGNORE_MAX_BUFFER
|
|
25673
25675
|
});
|
|
25674
|
-
|
|
25675
|
-
for (const path2 of out.split("\0")) {
|
|
25676
|
-
if (path2) for (const segment of firstSegments) {
|
|
25677
|
-
if (path2 === segment || path2.startsWith(`${segment}/`)) tracked.add(segment);
|
|
25678
|
-
}
|
|
25679
|
-
}
|
|
25680
|
-
return tracked;
|
|
25676
|
+
return out.split("\0").filter(Boolean);
|
|
25681
25677
|
} catch (error) {
|
|
25682
25678
|
if (error?.status === 128) return null;
|
|
25683
25679
|
if (error?.code === "ENOENT") return null;
|
|
@@ -25721,7 +25717,12 @@ function runDocRefs(root, deps = {}) {
|
|
|
25721
25717
|
const exists = deps.exists ?? import_node_fs19.existsSync;
|
|
25722
25718
|
const listDocs = deps.listDocs ?? defaultListDocs;
|
|
25723
25719
|
const isIgnored = deps.isIgnored ?? ((paths) => defaultIsIgnored(root, paths));
|
|
25724
|
-
|
|
25720
|
+
let inventory;
|
|
25721
|
+
const trackedFiles = () => inventory === void 0 ? inventory = deps.trackedFiles ? deps.trackedFiles() : deps.trackedFirstSegments ? null : defaultTrackedFiles(root, []) : inventory;
|
|
25722
|
+
const trackedFirstSegments = deps.trackedFirstSegments ?? ((segs) => {
|
|
25723
|
+
const files = trackedFiles();
|
|
25724
|
+
return files == null ? null : new Set(segs.filter((seg) => files.some((path2) => path2 === seg || path2.startsWith(`${seg}/`))));
|
|
25725
|
+
});
|
|
25725
25726
|
const commandPaths = deps.commandPaths ?? null;
|
|
25726
25727
|
const walked = listDocs(root);
|
|
25727
25728
|
const ignoredDocs = walked.length ? isIgnored(walked) : /* @__PURE__ */ new Set();
|
|
@@ -25761,6 +25762,18 @@ ${sourceMessages}`;
|
|
|
25761
25762
|
(override ? warnings : findings).push(...stale);
|
|
25762
25763
|
if (!/^Seed-Runbook-Affected:[ \t]*\S.*$/m.test(allMessages)) findings.push(...seedStale);
|
|
25763
25764
|
}
|
|
25765
|
+
for (const finding of findings) {
|
|
25766
|
+
if (finding.kind !== "missing-path") continue;
|
|
25767
|
+
const directory = import_node_path16.posix.dirname(import_node_path16.posix.normalize(finding.detail)).split("/");
|
|
25768
|
+
const distance = (path2) => {
|
|
25769
|
+
const parts = import_node_path16.posix.dirname(path2).split("/");
|
|
25770
|
+
let common = 0;
|
|
25771
|
+
while (common < directory.length && common < parts.length && directory[common] === parts[common]) common += 1;
|
|
25772
|
+
return directory.length + parts.length - 2 * common;
|
|
25773
|
+
};
|
|
25774
|
+
const suggest = [...new Set(trackedFiles() ?? [])].filter((path2) => import_node_path16.posix.basename(path2) === import_node_path16.posix.basename(finding.detail) && exists((0, import_node_path16.join)(root, path2))).sort((a, b) => distance(a) - distance(b) || a.localeCompare(b)).slice(0, 3);
|
|
25775
|
+
if (suggest.length) finding.suggest = suggest;
|
|
25776
|
+
}
|
|
25764
25777
|
return {
|
|
25765
25778
|
ok: findings.length === 0,
|
|
25766
25779
|
findings,
|
|
@@ -28166,10 +28179,10 @@ var rollout_plan_default = {
|
|
|
28166
28179
|
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)."
|
|
28167
28180
|
},
|
|
28168
28181
|
baseline: {
|
|
28169
|
-
version: "4.6.
|
|
28170
|
-
tag: "v4.6.
|
|
28171
|
-
commit: "
|
|
28172
|
-
npm: "@mutmutco/cli@4.6.
|
|
28182
|
+
version: "4.6.27",
|
|
28183
|
+
tag: "v4.6.27",
|
|
28184
|
+
commit: "b83c0c39cb1f",
|
|
28185
|
+
npm: "@mutmutco/cli@4.6.27"
|
|
28173
28186
|
},
|
|
28174
28187
|
exitCriterion: "fleet-n-of-n",
|
|
28175
28188
|
hubOnlyShortcut: "forbidden",
|
|
@@ -28186,14 +28199,14 @@ var rollout_plan_default = {
|
|
|
28186
28199
|
repo: "mutmutco/mmi-hub",
|
|
28187
28200
|
role: "canary",
|
|
28188
28201
|
schedule: "train",
|
|
28189
|
-
v3Target: "v4.6.
|
|
28202
|
+
v3Target: "v4.6.27"
|
|
28190
28203
|
}
|
|
28191
28204
|
],
|
|
28192
28205
|
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.",
|
|
28193
28206
|
rollback: {
|
|
28194
28207
|
independent: true,
|
|
28195
|
-
mechanism: "npm dist-tag latest -> 4.6.
|
|
28196
|
-
v3Target: "v4.6.
|
|
28208
|
+
mechanism: "npm dist-tag latest -> 4.6.27 and redeploy the Hub Lambda from tag v4.6.27 (b83c0c39cb1f); installed clients repair via `mmi-cli doctor`. No other cohort is touched.",
|
|
28209
|
+
v3Target: "v4.6.27 (@mutmutco/cli@4.6.27, tag commit b83c0c39cb1f \u2014 last known-good release carrying the repo-index v4-only contract)"
|
|
28197
28210
|
}
|
|
28198
28211
|
},
|
|
28199
28212
|
{
|
|
@@ -43918,6 +43931,20 @@ ${list}
|
|
|
43918
43931
|
Co-authored-by: GitHub <noreply@github.com>
|
|
43919
43932
|
`;
|
|
43920
43933
|
}
|
|
43934
|
+
function checkQueueSquashOverride(title, pr, messages, root) {
|
|
43935
|
+
if (messages.length <= 1 || !messages.some((message2) => /^\s*Test-Policy-Override\s*:/im.test(`${message2.headline}
|
|
43936
|
+
${message2.body}`)) || readMessageOverride2(githubQueueSquashMessage(title, pr, messages), root)) return void 0;
|
|
43937
|
+
return {
|
|
43938
|
+
mergeStatus: "failed",
|
|
43939
|
+
preparation: {
|
|
43940
|
+
status: "preparation-needed",
|
|
43941
|
+
reason: "queue-message-override",
|
|
43942
|
+
issueTargets: [],
|
|
43943
|
+
nextAction: "PR author: this repository squashes with COMMIT_MESSAGES, and GitHub composes a multi-commit squash as a list of every commit message followed by its own co-author block, so this Test-Policy-Override trailer would not be in the final trailer block the queue check reads. First, add one commit on this same branch whose message ends with a trailer block carrying the Test-Policy-Override waiver, push (no force-push), then rerun pr land on the same PR. This makes the waiver the final trailer block of the LAST commit. As a fallback, put the change in ONE commit whose message ends with a trailer block carrying the waiver: push a new branch whose single commit has the same tree, parented on the PR base; open it as a new PR and close this one, then rerun pr land. Keep repository settings unchanged."
|
|
43944
|
+
},
|
|
43945
|
+
error: "queue preparation needed: the multi-commit COMMIT_MESSAGES squash would bury the Test-Policy-Override waiver mid-message, so the queue check would fail malformed-override-trailer"
|
|
43946
|
+
};
|
|
43947
|
+
}
|
|
43921
43948
|
async function inspectQueuePreparation(pr, repo, options) {
|
|
43922
43949
|
const read = options.read ?? readApi;
|
|
43923
43950
|
const queueRead = options.read ?? actorReadApi;
|
|
@@ -44031,21 +44058,9 @@ ${pull.body}`;
|
|
|
44031
44058
|
])];
|
|
44032
44059
|
return { kind: "refused", result: preparation("queue-message", targets, `GitHub's unchanged ${policy.body} message is unsafe for ${targets.map((number) => `${repo}#${number}`).join(", ")}. ${closingGuardDiagnostic(verdict.message)} See preparation.nextAction for the exact \`mmi-cli devops pr edit ${pr} --repo ${repo} --body-file .jerv/tmp/pr-${pr}-body.md\` remedy.`) };
|
|
44033
44060
|
}
|
|
44034
|
-
if (method === "SQUASH" && policy.body === "COMMIT_MESSAGES" &&
|
|
44035
|
-
|
|
44036
|
-
return {
|
|
44037
|
-
kind: "refused",
|
|
44038
|
-
result: {
|
|
44039
|
-
mergeStatus: "failed",
|
|
44040
|
-
preparation: {
|
|
44041
|
-
status: "preparation-needed",
|
|
44042
|
-
reason: "queue-message-override",
|
|
44043
|
-
issueTargets: [],
|
|
44044
|
-
nextAction: "PR author: this repository squashes with COMMIT_MESSAGES, and GitHub composes a multi-commit squash as a list of every commit message followed by its own co-author block, so this Test-Policy-Override trailer would not be in the final trailer block the queue check reads. Put the change in ONE commit whose message ends with a trailer block carrying the waiver (no force-push: push a new branch whose single commit has the same tree, parented on the PR base; open it as a new PR and close this one), or make the waiver the final trailer block of the LAST commit, then rerun pr land. Keep repository settings unchanged."
|
|
44045
|
-
},
|
|
44046
|
-
error: "queue preparation needed: the multi-commit COMMIT_MESSAGES squash would bury the Test-Policy-Override waiver mid-message, so the queue check would fail malformed-override-trailer"
|
|
44047
|
-
}
|
|
44048
|
-
};
|
|
44061
|
+
if (method === "SQUASH" && policy.body === "COMMIT_MESSAGES" && options.bodyText == null) {
|
|
44062
|
+
const refusal = checkQueueSquashOverride(pull.title, pr, messages, process.cwd());
|
|
44063
|
+
if (refusal) return { kind: "refused", result: refusal };
|
|
44049
44064
|
}
|
|
44050
44065
|
const requiredBody = options.bodyText ?? composedBody;
|
|
44051
44066
|
const prBodyPreservesOverrides = requiredBody != null && method === "SQUASH" && policy.body === "PR_BODY" && options.bodyText == null && prBodyPreservesCommitOverrides(messages, headline, pull.body, process.cwd());
|
|
@@ -47934,6 +47949,16 @@ function evaluateFilelessComposeGuard(input) {
|
|
|
47934
47949
|
}
|
|
47935
47950
|
return { ok: true };
|
|
47936
47951
|
}
|
|
47952
|
+
function positionalOrRepoFlagTarget(commandName, positional, repoFlag) {
|
|
47953
|
+
const pos = positional?.trim() || void 0;
|
|
47954
|
+
const flag = repoFlag?.trim() || void 0;
|
|
47955
|
+
if (!pos || !flag) return pos ?? flag;
|
|
47956
|
+
const [a, b] = [pos.toLowerCase(), flag.toLowerCase()];
|
|
47957
|
+
const tail = (s) => s.split("/").pop();
|
|
47958
|
+
const same2 = a === b || (!a.includes("/") || !b.includes("/")) && tail(a) === tail(b);
|
|
47959
|
+
if (!same2) throw new Error(`${commandName}: positional target ${pos} and --repo ${flag} name different repos; pass one of them`);
|
|
47960
|
+
return pos;
|
|
47961
|
+
}
|
|
47937
47962
|
function requireProjectTarget(commandName, explicitTarget, currentRepo) {
|
|
47938
47963
|
const target = explicitTarget?.trim() || currentRepo?.trim();
|
|
47939
47964
|
if (!target) {
|
|
@@ -56260,6 +56285,7 @@ function githubCustodyStore(repo, baseSha, api = defaultGitHubClient()) {
|
|
|
56260
56285
|
}
|
|
56261
56286
|
};
|
|
56262
56287
|
}
|
|
56288
|
+
var DISPATCH_ABSENT_AFTER_MS = 18e4;
|
|
56263
56289
|
var ReleaseCustodySession = class _ReleaseCustodySession {
|
|
56264
56290
|
constructor(store, sha, state) {
|
|
56265
56291
|
this.store = store;
|
|
@@ -56358,6 +56384,13 @@ var ReleaseCustodySession = class _ReleaseCustodySession {
|
|
|
56358
56384
|
async reconcile(observer) {
|
|
56359
56385
|
for (const effect of Object.values(this.state.effects).filter((e) => e.state === "intent")) {
|
|
56360
56386
|
const observation = await observer(effect);
|
|
56387
|
+
if (!observation.observed && observation.absent) {
|
|
56388
|
+
await this.change((state) => {
|
|
56389
|
+
const { [effect.id]: _dropped, ...effects } = state.effects;
|
|
56390
|
+
return { ...state, effects };
|
|
56391
|
+
});
|
|
56392
|
+
continue;
|
|
56393
|
+
}
|
|
56361
56394
|
if (!observation.observed) throw new ReleaseCustodyError(`effect ${effect.kind} (${effect.id}) has no proven outcome; observe it before retrying`);
|
|
56362
56395
|
await this.change((state) => ({ ...state, effects: {
|
|
56363
56396
|
...state.effects,
|
|
@@ -56528,7 +56561,8 @@ function effectObserver(api) {
|
|
|
56528
56561
|
const fields = i.fields;
|
|
56529
56562
|
const title = effect.kind === "tenant-deploy" ? `tenant-deploy ${i.slug} ${i.stage} @ ${i.ref}` : workflow === "get-publish.yml" ? `get-publish (${fields.product} ${fields.mode || "full"})` : workflow === "tenant-deploy.yml" ? `tenant-deploy ${fields.slug} ${fields.stage} @ ${fields.ref}` : null;
|
|
56530
56563
|
const matches = runs2.workflow_runs.filter((run) => title && run.display_title === title && run.event === "workflow_dispatch" && run.head_sha === i.sha && !i.beforeRunIds?.includes(run.id) && Date.parse(run.created_at) >= Date.parse(effect.startedAt) - 1e3 && Date.parse(run.created_at) <= Date.parse(effect.startedAt) + 12e4);
|
|
56531
|
-
|
|
56564
|
+
if (matches.length === 1) return { observed: true, result: effect.kind === "tenant-deploy" ? null : "" };
|
|
56565
|
+
return matches.length === 0 && Date.now() > Date.parse(effect.startedAt) + DISPATCH_ABSENT_AFTER_MS ? { observed: false, absent: true } : { observed: false };
|
|
56532
56566
|
}
|
|
56533
56567
|
return { observed: false };
|
|
56534
56568
|
};
|
|
@@ -60815,6 +60849,25 @@ function createPreparationDeps(input, io = {}) {
|
|
|
60815
60849
|
if (!await docs.refResolves(candidate.root, candidate.headSha) || !await docs.refResolves(candidate.root, candidate.baseSha)) {
|
|
60816
60850
|
return result("skipped", "exact remote candidate objects are not available locally; committed test policy was not checked");
|
|
60817
60851
|
}
|
|
60852
|
+
const log = (await exec3("git", ["-C", candidate.root, "log", "--reverse", "--format=%B%x00", `${candidate.baseSha}..${candidate.headSha}`], { timeout: GIT_TIMEOUT_MS })).stdout;
|
|
60853
|
+
const messages = log.split("\0").filter((message2) => message2.trim()).map((message2) => {
|
|
60854
|
+
const [headline, ...body] = message2.trim().split("\n");
|
|
60855
|
+
return { headline, body: body.join("\n") };
|
|
60856
|
+
});
|
|
60857
|
+
const refusal = checkQueueSquashOverride("Candidate PR", "0", messages, candidate.root);
|
|
60858
|
+
if (refusal) {
|
|
60859
|
+
const [owner, name] = candidate.repo.split("/");
|
|
60860
|
+
const metadata = await client.graphql(
|
|
60861
|
+
"query($owner:String!,$name:String!,$base:String!) { repository(owner:$owner,name:$name) { squashMergeCommitMessage mergeQueue(branch:$base) { configuration { mergeMethod } } } }",
|
|
60862
|
+
{ owner, name, base: candidate.base }
|
|
60863
|
+
);
|
|
60864
|
+
const rules = await client.restPaginate(`repos/${candidate.repo}/rules/branches/${encodeURIComponent(candidate.base)}?per_page=100`);
|
|
60865
|
+
const methods = [...new Set(rules.filter((rule) => rule.type === "merge_queue").map((rule) => rule.parameters?.merge_method))];
|
|
60866
|
+
const method = methods.length === 1 ? methods[0] : !methods.length ? metadata.repository.mergeQueue?.configuration?.mergeMethod : void 0;
|
|
60867
|
+
if (method === "SQUASH" && metadata.repository.squashMergeCommitMessage === "COMMIT_MESSAGES") {
|
|
60868
|
+
return result("failed", `${refusal.error} \u2014 ${refusal.preparation.nextAction}`);
|
|
60869
|
+
}
|
|
60870
|
+
}
|
|
60818
60871
|
const policy = runTestPolicy(candidate.root, { candidate: { head: candidate.headSha, base: candidate.baseSha } });
|
|
60819
60872
|
return result(policy.ok ? "passed" : "failed", policy.ok ? `committed test policy passed at ${candidate.headSha}` : policy.findings.map((finding) => finding.detail).join("\n"));
|
|
60820
60873
|
}
|
|
@@ -72535,7 +72588,7 @@ function registerDeveloperCommands(program3) {
|
|
|
72535
72588
|
if (!result.ok) process.exitCode = 1;
|
|
72536
72589
|
return;
|
|
72537
72590
|
}
|
|
72538
|
-
for (const w of result.warnings) console.warn(`WARNING: ${w
|
|
72591
|
+
for (const w of result.warnings) console.warn(`WARNING: ${formatDocRefFinding(w)}`);
|
|
72539
72592
|
if (result.ok) {
|
|
72540
72593
|
console.log(
|
|
72541
72594
|
`doc refs: ${result.docCount} docs, every reference resolves` + (result.warnings.length ? ` (${result.warnings.length} warning(s))` : "")
|
|
@@ -72545,7 +72598,7 @@ function registerDeveloperCommands(program3) {
|
|
|
72545
72598
|
const hasStale = result.findings.some((f) => f.kind === "runbook-section-stale" || f.kind === "seed-runbook-unreviewed");
|
|
72546
72599
|
const hasRefs = result.findings.some((f) => f.kind !== "runbook-section-stale" && f.kind !== "seed-runbook-unreviewed");
|
|
72547
72600
|
console.error(hasStale && !hasRefs ? "doc refs: stale runbook section(s)" : hasStale && hasRefs ? "doc refs: unresolved references and stale runbook section(s)" : "doc refs: unresolved references");
|
|
72548
|
-
for (const f of result.findings) console.error(`- ${f
|
|
72601
|
+
for (const f of result.findings) console.error(`- ${formatDocRefFinding(f)}`);
|
|
72549
72602
|
process.exitCode = 1;
|
|
72550
72603
|
} catch (e) {
|
|
72551
72604
|
await failGraceful(e.message);
|
|
@@ -73536,13 +73589,13 @@ project.command("sync-info [owner/repo]").description("synchronize the owning Gi
|
|
|
73536
73589
|
}
|
|
73537
73590
|
});
|
|
73538
73591
|
async function runProjectDeployGet(repoOrSlug, o) {
|
|
73539
|
-
const cfg = await loadConfig();
|
|
73540
73592
|
let target;
|
|
73541
73593
|
try {
|
|
73542
|
-
target = await projectTarget("org project deploy get", repoOrSlug);
|
|
73594
|
+
target = await projectTarget("org project deploy get", positionalOrRepoFlagTarget("org project deploy get", repoOrSlug, o.repo));
|
|
73543
73595
|
} catch (e) {
|
|
73544
73596
|
return fail(e.message);
|
|
73545
73597
|
}
|
|
73598
|
+
const cfg = await loadConfig();
|
|
73546
73599
|
const out = await fetchDeployFactsBySlug(slugOf(target), registryClientDeps(cfg));
|
|
73547
73600
|
if (!out) return failGraceful(`org project deploy get: Hub deploy facts read failed for ${target}`);
|
|
73548
73601
|
const stage = o.stage?.trim();
|
|
@@ -73550,8 +73603,12 @@ async function runProjectDeployGet(repoOrSlug, o) {
|
|
|
73550
73603
|
const payload = stage ? { slug: out.slug, stage, deploy: out.stages[stage] ?? null } : out;
|
|
73551
73604
|
console.log(JSON.stringify(payload, null, 2));
|
|
73552
73605
|
}
|
|
73553
|
-
var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)").argument("[ownerRepo]", "shorthand for `get <owner/repo>`").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (ownerRepo, o) => runProjectDeployGet(ownerRepo, o));
|
|
73554
|
-
projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--json", "machine-readable output").action(async (repoOrSlug, o) => runProjectDeployGet(repoOrSlug, {
|
|
73606
|
+
var projectDeploy = project.command("deploy").description("read nonsecret DEPLOY# facts (domain, port, deploy path, substrate, host presence)").argument("[ownerRepo]", "shorthand for `get <owner/repo>`").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--repo <owner/repo>", "target repo (alias for the positional; defaults to the current repo)").option("--json", "machine-readable output").action(async (ownerRepo, o) => runProjectDeployGet(ownerRepo, o));
|
|
73607
|
+
projectDeploy.command("get [owner/repo]").description("read nonsecret DEPLOY# facts for one project; defaults to the current repo").addOption(new Option("--stage <stage>", "dev | rc | main").choices(["dev", "rc", "main"])).option("--repo <owner/repo>", "target repo (alias for the positional; defaults to the current repo)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => runProjectDeployGet(repoOrSlug, {
|
|
73608
|
+
stage: projectDeploy.opts().stage,
|
|
73609
|
+
json: o.json,
|
|
73610
|
+
repo: projectDeploy.opts().repo ?? o.repo
|
|
73611
|
+
}));
|
|
73555
73612
|
projectDeploy.command("doctor").description("read-only estate port scan; --box lists its DEPLOY# owners and suggests free 8xxx ports (master-only)").option("--box <name>", "resolve box from the live Hetzner inventory and inspect its ports").option("--json", "machine-readable output").action(async (o) => {
|
|
73556
73613
|
const cfg = await loadConfig();
|
|
73557
73614
|
const report = await fetchDeployPortCollisions(registryClientDeps(cfg));
|
|
@@ -73580,13 +73637,14 @@ projectDeploy.command("doctor").description("read-only estate port scan; --box l
|
|
|
73580
73637
|
if (report.error && report.status !== 403) return failGraceful(`org project deploy doctor: ${report.error}`);
|
|
73581
73638
|
if (!report.ok && !report.error) process.exitCode = 1;
|
|
73582
73639
|
});
|
|
73583
|
-
projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one stage's DEPLOY# row for a project that no longer deploys through it (#7095, master-only). --stage is required. DRY-RUN by default \u2014 pass --apply to actually write; defaults to the current repo. Refuses a tenant-container project's live main row.").addOption(new Option("--stage <stage>", "dev | rc | main (required)").choices(["dev", "rc", "main"])).option("--apply", "actually retire (default is a dry-run preview of the row that would be retired)").option("--decommissioned", "main only (#8148): retire a tenant-container main row after `runtime tenant control <repo> main retire`; the box must show main gone").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
73640
|
+
projectDeploy.command("retire [owner/repo]").description("retire (tombstone) one stage's DEPLOY# row for a project that no longer deploys through it (#7095, master-only). --stage is required. DRY-RUN by default \u2014 pass --apply to actually write; defaults to the current repo. Refuses a tenant-container project's live main row.").addOption(new Option("--stage <stage>", "dev | rc | main (required)").choices(["dev", "rc", "main"])).option("--apply", "actually retire (default is a dry-run preview of the row that would be retired)").option("--decommissioned", "main only (#8148): retire a tenant-container main row after `runtime tenant control <repo> main retire`; the box must show main gone").option("--repo <owner/repo>", "target repo (alias for the positional; defaults to the current repo)").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
|
|
73584
73641
|
const stage = projectDeploy.opts().stage;
|
|
73585
73642
|
if (!stage) return fail("org project deploy retire: --stage is required (dev | rc | main)");
|
|
73586
73643
|
const cfg = await loadConfig();
|
|
73587
73644
|
let target;
|
|
73588
73645
|
try {
|
|
73589
|
-
|
|
73646
|
+
const repoFlag = projectDeploy.opts().repo ?? o.repo;
|
|
73647
|
+
target = await projectTarget("org project deploy retire", positionalOrRepoFlagTarget("org project deploy retire", repoOrSlug, repoFlag));
|
|
73590
73648
|
} catch (e) {
|
|
73591
73649
|
return fail(e.message);
|
|
73592
73650
|
}
|
package/cli/package.json
CHANGED
package/package.json
CHANGED
|
@@ -172,11 +172,15 @@ jobs:
|
|
|
172
172
|
# MMI-Hub#5871: declare the npm MAJOR rather than inheriting whatever the runner's node bundles.
|
|
173
173
|
# A bare major, never an exact patch (it goes stale and then prescribes downgrading operator
|
|
174
174
|
# hosts toward it, #5862) and never `latest` (it takes the next major silently, #4782).
|
|
175
|
+
# MMI-Hub#9850: this single gate job is the designated hosted npm cache writer (auto saves only on a
|
|
176
|
+
# default-branch push), keyed on the one lockfile its install uses.
|
|
175
177
|
- if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'node') }}
|
|
176
178
|
uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
|
|
177
179
|
with:
|
|
178
180
|
node-version: 24
|
|
179
181
|
npm-version: '12'
|
|
182
|
+
hosted-npm-cache: auto
|
|
183
|
+
hosted-npm-cache-lockfiles: {{GATE_CACHE_DEP_PATH}}
|
|
180
184
|
- if: ${{ steps.development-gate.outputs.reuse != 'true' && ('{{GATE_RUNTIME}}' == 'python') }}
|
|
181
185
|
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
|
182
186
|
with: { python-version: '{{GATE_PY_VERSION}}' }
|
|
@@ -37,11 +37,17 @@ jobs:
|
|
|
37
37
|
- name: Isolate npm cache
|
|
38
38
|
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
39
39
|
run: echo "NPM_CONFIG_CACHE=$RUNNER_TEMP/npm-cache" >> "$GITHUB_ENV"
|
|
40
|
+
# MMI-Hub#9850: the single gate job is the designated hosted npm cache writer (auto saves only on a
|
|
41
|
+
# default-branch push), keyed on both lockfiles its two installs below use.
|
|
40
42
|
- uses: mutmutco/MMI-Hub/.github/actions/runner-node-toolchain@{{GATE_RUNNER_SHA}}
|
|
41
43
|
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
42
44
|
with:
|
|
43
45
|
node-version: 24
|
|
44
46
|
npm-version: '12'
|
|
47
|
+
hosted-npm-cache: auto
|
|
48
|
+
hosted-npm-cache-lockfiles: |
|
|
49
|
+
package-lock.json
|
|
50
|
+
module/package-lock.json
|
|
45
51
|
- run: npm ci --no-audit --no-fund
|
|
46
52
|
if: ${{ steps.development-gate.outputs.reuse != 'true' }}
|
|
47
53
|
- run: npm ci --prefix module --no-audit --no-fund
|
|
@@ -114,7 +114,11 @@ requests like this should follow this structure, not prose guide."
|
|
|
114
114
|
Before building visual A/B boards, use front-loaded **Impeccable v4.7.0** alongside **21st.dev MCP**
|
|
115
115
|
(`https://21st.dev/api/mcp`; verbatim owner ruling: `"we don't need the mcp better design yet,
|
|
116
116
|
impeccable and 21st is enough"`, dropping Better Design MCP) to study high-craft reference patterns,
|
|
117
|
-
layout proportions, and interaction details.
|
|
117
|
+
layout proportions, and interaction details. Actually query the 21st.dev MCP; in the Phase 0
|
|
118
|
+
handoff/A-B brief, report the queries issued and returned component/pattern names and IDs
|
|
119
|
+
(where provided). If unreachable, report the exact error; if empty, report "no results".
|
|
120
|
+
Neither case supports a 21st inspiration claim. Never describe patterns as 21st-inspired
|
|
121
|
+
without this query/result record. Always translate inspired patterns into zero-GPU
|
|
118
122
|
`@mutmutco/mmwork-ds` tokens (`--card-blur: none`, `--card-tint-dark: 0%`, `--card-tint-light: 0%`,
|
|
119
123
|
sleek translucent glass cards) and Atomic Design layers; never copy raw third-party CSS/HTML or
|
|
120
124
|
GPU-heavy effects. `MM-Inspo` is strictly a customer/tenant module inside the MM-Work suite, never
|
|
@@ -56,6 +56,11 @@ design work follows one harmonized **Front-Loaded Impeccable v4.7.0 + MM Artifac
|
|
|
56
56
|
before choosing pieces; `exists` reports whether the requested guide exists. Never guess
|
|
57
57
|
repo-root `node_modules/@mutmutco/mmwork-ds/docs/components/<Piece>.md`; component guides
|
|
58
58
|
live in `<Piece>/guide.md`. No install/cache means capability remains unknown.
|
|
59
|
+
**Loading state comes from the content (#9851).** A page or component renders its loading state
|
|
60
|
+
from the same component tree as its real content: the kit Page `busy` or the component's own
|
|
61
|
+
loading state, drawing the real rows inert or hidden. Never hand-author a separate placeholder
|
|
62
|
+
skeleton kept in sync by hand. Every UI change checks the loading and loaded DOM structure match,
|
|
63
|
+
so an indicator cannot drift from the content it fronts.
|
|
59
64
|
|
|
60
65
|
How the five pillars fit together (owner, 2026-10-25):
|
|
61
66
|
|
|
@@ -285,7 +290,11 @@ requests like this should follow this structure, not prose guide."
|
|
|
285
290
|
Before building visual A/B boards, use front-loaded **Impeccable v4.7.0** alongside **21st.dev MCP**
|
|
286
291
|
(`https://21st.dev/api/mcp`; verbatim owner ruling: `"we don't need the mcp better design yet,
|
|
287
292
|
impeccable and 21st is enough"`, dropping Better Design MCP) to study high-craft reference patterns,
|
|
288
|
-
layout proportions, and interaction details.
|
|
293
|
+
layout proportions, and interaction details. Actually query the 21st.dev MCP; in the Phase 0
|
|
294
|
+
handoff/A-B brief, report the queries issued and returned component/pattern names and IDs
|
|
295
|
+
(where provided). If unreachable, report the exact error; if empty, report "no results".
|
|
296
|
+
Neither case supports a 21st inspiration claim. Never describe patterns as 21st-inspired
|
|
297
|
+
without this query/result record. Always translate inspired patterns into zero-GPU
|
|
289
298
|
`@mutmutco/mmwork-ds` tokens (`--card-blur: none`, `--card-tint-dark: 0%`, `--card-tint-light: 0%`,
|
|
290
299
|
sleek translucent glass cards) and Atomic Design layers; never copy raw third-party CSS/HTML or
|
|
291
300
|
GPU-heavy effects. `MM-Inspo` is strictly a customer/tenant module inside the MM-Work suite, never
|
|
@@ -161,6 +161,8 @@ Production changes require explicit authorization from the authorized human.
|
|
|
161
161
|
Read the selected release skill before operating the train. Let it choose versions, run gates,
|
|
162
162
|
publish, deploy, and verify the resulting release. A refusal requires fixing its cause; do not
|
|
163
163
|
finish by hand, bypass checks, or start a duplicate release.
|
|
164
|
+
Finish the board before a release cut: every item of this cycle is merged and `Done` first; a queued
|
|
165
|
+
lane is not a landed one.
|
|
164
166
|
Versions: patch is the default for fixes and small compatible additions. Minor is a deliberate call
|
|
165
167
|
for a real feature, with a one-line reason. Major is a new start or direction and needs the owner's
|
|
166
168
|
fresh approval before tagging. A breaking change refuses until it is shimmed or made an approved
|
package/skills/release/SKILL.md
CHANGED
|
@@ -15,6 +15,8 @@ Read `mmi-cli oracle org project get {owner}/{repo} --json` and name the lane ([
|
|
|
15
15
|
|
|
16
16
|
## Preflight
|
|
17
17
|
|
|
18
|
+
Board first: `mmi-cli oracle board read --repo <owner/repo>`; cut only once every in-flight item of this cycle is merged and `Done` ([train-common.md](train-common.md#board-first)).
|
|
19
|
+
|
|
18
20
|
`mmi-cli devops train doctor --lane release --heal --json`; read it per [train-common.md](train-common.md#doctor).
|
|
19
21
|
|
|
20
22
|
## Apply
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
# Train skills: shared doctor, recovery, merge floor, report and retro
|
|
1
|
+
# Train skills: shared board-first, doctor, recovery, merge floor, report and retro
|
|
2
2
|
|
|
3
3
|
`/release`, `/rcand` and `/hotfix` read this one file (from a sibling skill: `../release/train-common.md`). Each skill keeps only what differs by lane.
|
|
4
4
|
|
|
5
|
+
## Board first
|
|
6
|
+
|
|
7
|
+
Before any release cut, read the repo's own board: `mmi-cli oracle board read --repo <owner/repo>`. Every in-flight item of this cycle — claimed, in review, queued in the autolander, or awaiting landing — must be merged and `Done` first. A queued lane is not a landed one: wait for its landing to report merge and board `Done`. The cut is the last step of a board cycle, never interleaved with pending landings; otherwise the version ships without them and needs a second cut. Standing backlog nobody is working is not in flight and does not block.
|
|
8
|
+
|
|
5
9
|
## Doctor
|
|
6
10
|
|
|
7
11
|
The preflight JSON is on stdout alone: a stale CLI first prints its self-update notice on stderr and re-runs itself, so parse stdout only, never `2>&1`. Read `ready`, then `findings[]`: each carries `code`, `severity` (`blocker` · `warning` · `healed` · `info`), `source` (`local` · `origin`), `remedy` and `anchor`. `ready: false` → stop and quote every blocker's `code` and `remedy`; an unread origin is never green. The apply step (`release --apply`, `rcand --apply`, `hotfix start`, `hotfix release`) re-runs the doctor first, with heal on.
|