@mutmutco/cli 2.67.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -17
- package/dist/main.cjs +542 -380
- package/package.json +3 -3
package/dist/main.cjs
CHANGED
|
@@ -3407,42 +3407,104 @@ function useColor() {
|
|
|
3407
3407
|
var program = new Command();
|
|
3408
3408
|
|
|
3409
3409
|
// src/index.ts
|
|
3410
|
-
var
|
|
3410
|
+
var import_promises3 = require("node:fs/promises");
|
|
3411
3411
|
var import_node_fs18 = require("node:fs");
|
|
3412
3412
|
|
|
3413
|
-
// src/
|
|
3414
|
-
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3413
|
+
// src/bootstrap-org-ruleset.ts
|
|
3414
|
+
var ORG_NO_AGENT_FILES_RULESET_NAME = "mmi-no-agent-files-org";
|
|
3415
|
+
var ORG_LOGIN = "mutmutco";
|
|
3416
|
+
var ORG_NO_AGENT_FILES_EXCLUDED_REPOS = ["Jerv-PowerTools"];
|
|
3417
|
+
var NO_AGENT_FILES_GLOBS = [
|
|
3418
|
+
"AGENTS.md",
|
|
3419
|
+
"**/AGENTS.md",
|
|
3420
|
+
"CLAUDE.md",
|
|
3421
|
+
"**/CLAUDE.md",
|
|
3422
|
+
"GEMINI.md",
|
|
3423
|
+
"**/GEMINI.md",
|
|
3424
|
+
".claude/**",
|
|
3425
|
+
"**/.claude/**",
|
|
3426
|
+
".cursor/rules/**",
|
|
3427
|
+
"**/.cursor/rules/**",
|
|
3428
|
+
".codex/**",
|
|
3429
|
+
"**/.codex/**",
|
|
3430
|
+
".agents/**",
|
|
3431
|
+
"**/.agents/**"
|
|
3432
|
+
];
|
|
3433
|
+
function orgNoAgentFilesRulesetDesired() {
|
|
3434
|
+
return {
|
|
3435
|
+
name: ORG_NO_AGENT_FILES_RULESET_NAME,
|
|
3436
|
+
target: "push",
|
|
3437
|
+
enforcement: "active",
|
|
3438
|
+
bypass_actors: [],
|
|
3439
|
+
conditions: { repository_name: { include: ["~ALL"], exclude: [...ORG_NO_AGENT_FILES_EXCLUDED_REPOS] } },
|
|
3440
|
+
rules: [{ type: "file_path_restriction", parameters: { restricted_file_paths: [...NO_AGENT_FILES_GLOBS] } }]
|
|
3441
|
+
};
|
|
3420
3442
|
}
|
|
3421
|
-
function
|
|
3422
|
-
|
|
3443
|
+
function sameSet(a, b) {
|
|
3444
|
+
const A = new Set(a ?? []);
|
|
3445
|
+
const B = new Set(b ?? []);
|
|
3446
|
+
if (A.size !== B.size) return false;
|
|
3447
|
+
for (const v of A) if (!B.has(v)) return false;
|
|
3448
|
+
return true;
|
|
3423
3449
|
}
|
|
3424
|
-
function
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
}
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3450
|
+
function diffOrgRuleset(live, desired = orgNoAgentFilesRulesetDesired()) {
|
|
3451
|
+
const drift = [];
|
|
3452
|
+
if (!live) return [`ruleset '${desired.name}' is absent`];
|
|
3453
|
+
if (live.name !== desired.name) drift.push(`name: '${live.name}' != '${desired.name}'`);
|
|
3454
|
+
if (live.target !== desired.target) drift.push(`target: '${live.target}' != '${desired.target}'`);
|
|
3455
|
+
if (live.enforcement !== desired.enforcement) drift.push(`enforcement: '${live.enforcement}' != '${desired.enforcement}'`);
|
|
3456
|
+
const bypass = Array.isArray(live.bypass_actors) ? live.bypass_actors : [];
|
|
3457
|
+
if (bypass.length !== 0) drift.push(`bypass_actors: ${bypass.length} actor(s) present \u2014 must be empty (no bypass)`);
|
|
3458
|
+
const include = live.conditions?.repository_name?.include;
|
|
3459
|
+
const exclude = live.conditions?.repository_name?.exclude;
|
|
3460
|
+
if (!sameSet(include, desired.conditions.repository_name.include)) {
|
|
3461
|
+
drift.push(`conditions.include: [${(include ?? []).join(", ")}] != [${desired.conditions.repository_name.include.join(", ")}]`);
|
|
3462
|
+
}
|
|
3463
|
+
if (!sameSet(exclude, desired.conditions.repository_name.exclude)) {
|
|
3464
|
+
drift.push(`conditions.exclude: [${(exclude ?? []).join(", ")}] != [${desired.conditions.repository_name.exclude.join(", ")}]`);
|
|
3465
|
+
}
|
|
3466
|
+
const rules2 = Array.isArray(live.rules) ? live.rules : [];
|
|
3467
|
+
const fpr = rules2.filter((r) => r?.type === "file_path_restriction");
|
|
3468
|
+
if (fpr.length !== 1) {
|
|
3469
|
+
drift.push(`rules: expected exactly 1 file_path_restriction, found ${fpr.length}`);
|
|
3470
|
+
} else if (!sameSet(fpr[0]?.parameters?.restricted_file_paths, desired.rules[0].parameters.restricted_file_paths)) {
|
|
3471
|
+
const got = fpr[0]?.parameters?.restricted_file_paths ?? [];
|
|
3472
|
+
drift.push(`restricted_file_paths: ${got.length} glob(s) differ from the codified ${desired.rules[0].parameters.restricted_file_paths.length}`);
|
|
3473
|
+
}
|
|
3474
|
+
return drift;
|
|
3475
|
+
}
|
|
3476
|
+
function planOrgNoAgentFilesRuleset(live) {
|
|
3477
|
+
const desired = orgNoAgentFilesRulesetDesired();
|
|
3478
|
+
if (!live) return { action: "create", drift: [`ruleset '${desired.name}' is absent`], desired };
|
|
3479
|
+
const drift = diffOrgRuleset(live, desired);
|
|
3480
|
+
return { action: drift.length === 0 ? "noop" : "update", drift, desired, id: live.id };
|
|
3481
|
+
}
|
|
3482
|
+
function renderOrgRulesetDriftReport(plan) {
|
|
3483
|
+
const head = `mmi-cli bootstrap org-ruleset: ${ORG_NO_AGENT_FILES_RULESET_NAME} (org ${ORG_LOGIN})`;
|
|
3484
|
+
if (plan.action === "noop") {
|
|
3485
|
+
return `${head}
|
|
3486
|
+
OK \u2014 live ruleset matches the codified IaC (${NO_AGENT_FILES_GLOBS.length} globs, include ~ALL, empty bypass, exclude ${ORG_NO_AGENT_FILES_EXCLUDED_REPOS.join(", ")}).`;
|
|
3487
|
+
}
|
|
3488
|
+
const verb = plan.action === "create" ? "MISSING \u2014 codified IaC would CREATE it" : "DRIFT \u2014 live ruleset diverges from the codified IaC";
|
|
3489
|
+
return [
|
|
3490
|
+
`${head}`,
|
|
3491
|
+
verb,
|
|
3492
|
+
...plan.drift.map((d) => ` - ${d}`),
|
|
3493
|
+
"This command is read-only; it never mutates the live ruleset. Recreate/repair via the GitHub UI or a",
|
|
3494
|
+
"deliberate POST/PUT of orgNoAgentFilesRulesetDesired() \u2014 see cli/src/bootstrap-org-ruleset.ts."
|
|
3495
|
+
].join("\n");
|
|
3435
3496
|
}
|
|
3436
|
-
function
|
|
3437
|
-
const
|
|
3438
|
-
|
|
3497
|
+
async function fetchOrgNoAgentFilesRuleset(client, org = ORG_LOGIN) {
|
|
3498
|
+
const list = await client.rest("GET", `orgs/${org}/rulesets`, { timeoutMs: 2e4 });
|
|
3499
|
+
const summary = (list ?? []).find((r) => r?.name === ORG_NO_AGENT_FILES_RULESET_NAME);
|
|
3500
|
+
if (summary?.id == null) return null;
|
|
3501
|
+
return await client.rest("GET", `orgs/${org}/rulesets/${summary.id}`, { timeoutMs: 2e4 }) ?? null;
|
|
3439
3502
|
}
|
|
3440
3503
|
|
|
3441
3504
|
// src/index.ts
|
|
3442
3505
|
var import_node_child_process11 = require("node:child_process");
|
|
3443
3506
|
|
|
3444
3507
|
// src/cli-shared.ts
|
|
3445
|
-
var import_promises = require("node:fs/promises");
|
|
3446
3508
|
var import_node_child_process3 = require("node:child_process");
|
|
3447
3509
|
var import_node_util4 = require("node:util");
|
|
3448
3510
|
|
|
@@ -3887,16 +3949,8 @@ async function hubHeaders(extra = {}) {
|
|
|
3887
3949
|
const base = { ...clientVersionHeaders(), ...extra };
|
|
3888
3950
|
return t ? { ...base, Authorization: `Bearer ${t}` } : base;
|
|
3889
3951
|
}
|
|
3890
|
-
var CONFIG_FILE = ".mmi/config.json";
|
|
3891
3952
|
async function loadConfig() {
|
|
3892
|
-
|
|
3893
|
-
try {
|
|
3894
|
-
file = JSON.parse(await (0, import_promises.readFile)(CONFIG_FILE, "utf8"));
|
|
3895
|
-
} catch {
|
|
3896
|
-
file = {};
|
|
3897
|
-
}
|
|
3898
|
-
if (!file.sagaApiUrl) file.sagaApiUrl = defaultHubUrl();
|
|
3899
|
-
return file;
|
|
3953
|
+
return { sagaApiUrl: defaultHubUrl() };
|
|
3900
3954
|
}
|
|
3901
3955
|
async function originRemoteUrl() {
|
|
3902
3956
|
return gitOut(["config", "--get", "remote.origin.url"]);
|
|
@@ -3922,29 +3976,8 @@ function fail(msg) {
|
|
|
3922
3976
|
hardExit(1);
|
|
3923
3977
|
}
|
|
3924
3978
|
function installProcessBackstop() {
|
|
3925
|
-
process.on("unhandledRejection", (reason) =>
|
|
3926
|
-
process.on("uncaughtException", (err) =>
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
// src/docs-sync.ts
|
|
3930
|
-
var SYNCED_DOCS = ["README.md", "architecture.md"];
|
|
3931
|
-
async function syncDocs(deps, docs2 = SYNCED_DOCS) {
|
|
3932
|
-
const updated = [];
|
|
3933
|
-
const skippedDirty = [];
|
|
3934
|
-
for (const file of docs2) {
|
|
3935
|
-
if (await deps.isDirty(file)) {
|
|
3936
|
-
skippedDirty.push(file);
|
|
3937
|
-
continue;
|
|
3938
|
-
}
|
|
3939
|
-
const origin = await deps.originContent(file);
|
|
3940
|
-
if (origin === null) continue;
|
|
3941
|
-
const local = await deps.localContent(file);
|
|
3942
|
-
if (needsUpdate(origin, local)) {
|
|
3943
|
-
await deps.writeDoc(file, normalizeEol(origin));
|
|
3944
|
-
updated.push(file);
|
|
3945
|
-
}
|
|
3946
|
-
}
|
|
3947
|
-
return { updated, skippedDirty };
|
|
3979
|
+
process.on("unhandledRejection", (reason) => void failGraceful(reason instanceof Error ? reason.message : String(reason)));
|
|
3980
|
+
process.on("uncaughtException", (err) => void failGraceful(err instanceof Error ? err.message : String(err)));
|
|
3948
3981
|
}
|
|
3949
3982
|
|
|
3950
3983
|
// src/session-start.ts
|
|
@@ -3979,8 +4012,12 @@ function hashContent(s) {
|
|
|
3979
4012
|
return (h >>> 0).toString(16);
|
|
3980
4013
|
}
|
|
3981
4014
|
|
|
4015
|
+
// src/rules-sync.ts
|
|
4016
|
+
function normalizeEol(s) {
|
|
4017
|
+
return s.replace(/\r\n/g, "\n");
|
|
4018
|
+
}
|
|
4019
|
+
|
|
3982
4020
|
// src/scratch-gc.ts
|
|
3983
|
-
var HEAD_TS_STALE_MS = 48 * 36e5;
|
|
3984
4021
|
var PLAN_ADVISORY_AGE_MS = 30 * 24 * 36e5;
|
|
3985
4022
|
var ROOT_SCRATCH_STALE_MS = 24 * 36e5;
|
|
3986
4023
|
var SCRATCH_GC_THROTTLE_MS = 24 * 36e5;
|
|
@@ -4002,16 +4039,16 @@ function markScratchGcRun(stampPath, now = Date.now()) {
|
|
|
4002
4039
|
}
|
|
4003
4040
|
}
|
|
4004
4041
|
function executeScratchGc(repoRoot, opts, now = Date.now()) {
|
|
4005
|
-
const snap = collectScratchSnapshot(repoRoot);
|
|
4042
|
+
const snap = collectScratchSnapshot(repoRoot, { project: opts.project });
|
|
4006
4043
|
const plan = planScratchGc(snap, now);
|
|
4007
4044
|
if (!opts.apply) return { plan };
|
|
4008
|
-
return { plan, applied: applyScratchGc(plan,
|
|
4045
|
+
return { plan, applied: applyScratchGc(plan, repoRoot, now, opts.project) };
|
|
4009
4046
|
}
|
|
4010
|
-
function buildPrMergeScratchHousekeeping(repoRoot, deps = {}) {
|
|
4047
|
+
function buildPrMergeScratchHousekeeping(repoRoot, deps = {}, project2) {
|
|
4011
4048
|
const runScratchGc = deps.runScratchGc ?? executeScratchGc;
|
|
4012
4049
|
try {
|
|
4013
|
-
const scratchApply = runScratchGc(repoRoot, { apply: true });
|
|
4014
|
-
const scratchAfter = runScratchGc(repoRoot, { apply: false });
|
|
4050
|
+
const scratchApply = runScratchGc(repoRoot, { apply: true, project: project2 });
|
|
4051
|
+
const scratchAfter = runScratchGc(repoRoot, { apply: false, project: project2 });
|
|
4015
4052
|
const applied = scratchApply.applied;
|
|
4016
4053
|
const pruned = applied?.pruned.length ?? 0;
|
|
4017
4054
|
const skipped = applied?.skipped ?? 0;
|
|
@@ -4043,13 +4080,10 @@ function buildPrMergeScratchHousekeeping(repoRoot, deps = {}) {
|
|
|
4043
4080
|
}
|
|
4044
4081
|
var ROOT_SCRATCH_DIRS = /* @__PURE__ */ new Set(["tmp", ".playwright-mcp", "test-results", "playwright-report"]);
|
|
4045
4082
|
var ROOT_SCRATCH_FILE_PREFIXES = ["tmp_"];
|
|
4046
|
-
var NEVER_BASENAMES = /* @__PURE__ */ new Set([".session", "config.json"]);
|
|
4047
4083
|
function planScratchGc(snap, now = Date.now()) {
|
|
4048
4084
|
const candidates = [];
|
|
4049
4085
|
const normalizePath = (p) => p.replace(/\\/g, "/");
|
|
4050
|
-
const repoRoot = normalizePath(
|
|
4051
|
-
const headTsPrefix = `${snap.mmiRoot.replace(/[\\/]+$/, "")}/head-ts/`.replace(/\\/g, "/");
|
|
4052
|
-
const days = (ms) => `${Math.floor(ms / 864e5)}d`;
|
|
4086
|
+
const repoRoot = normalizePath(snap.repoRoot.replace(/[\\/]+$/, ""));
|
|
4053
4087
|
for (const f of snap.rootScratchFiles ?? []) {
|
|
4054
4088
|
const dir = normalizePath(f.dir).replace(/\/+$/, "");
|
|
4055
4089
|
const age = now - f.mtimeMs;
|
|
@@ -4078,15 +4112,6 @@ function planScratchGc(snap, now = Date.now()) {
|
|
|
4078
4112
|
});
|
|
4079
4113
|
}
|
|
4080
4114
|
}
|
|
4081
|
-
for (const f of snap.mmiFiles) {
|
|
4082
|
-
const age = now - f.mtimeMs;
|
|
4083
|
-
const add = (family, reason) => candidates.push({ path: f.path, family, kind: "file", tier: "safe-auto", reason, bytes: f.bytes });
|
|
4084
|
-
if (NEVER_BASENAMES.has(f.name)) continue;
|
|
4085
|
-
if (f.path.replace(/\\/g, "/").startsWith(headTsPrefix) && !f.name.startsWith(".")) {
|
|
4086
|
-
if (age > HEAD_TS_STALE_MS) add("head-ts", `stale throttle stamp (${days(age)} old)`);
|
|
4087
|
-
continue;
|
|
4088
|
-
}
|
|
4089
|
-
}
|
|
4090
4115
|
if (snap.syncQueueSlugs === null) {
|
|
4091
4116
|
for (const f of snap.planMdFiles) {
|
|
4092
4117
|
if (now - f.mtimeMs <= PLAN_ADVISORY_AGE_MS) continue;
|
|
@@ -4151,14 +4176,6 @@ function syncedPlanMetaEntry(meta, project2, slug, hash) {
|
|
|
4151
4176
|
const entry = meta[metaKey(project2, slug)];
|
|
4152
4177
|
return Boolean(entry?.hash === hash && entry.syncedAt);
|
|
4153
4178
|
}
|
|
4154
|
-
function readProject(repoRoot) {
|
|
4155
|
-
try {
|
|
4156
|
-
const cfg = JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(repoRoot, ".mmi", "config.json"), "utf8"));
|
|
4157
|
-
if (typeof cfg.project === "string" && cfg.project.trim()) return cfg.project.trim();
|
|
4158
|
-
} catch {
|
|
4159
|
-
}
|
|
4160
|
-
return void 0;
|
|
4161
|
-
}
|
|
4162
4179
|
function readPlanMeta(plansRoot) {
|
|
4163
4180
|
try {
|
|
4164
4181
|
return parseMeta((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(plansRoot, ".plan-meta.json"), "utf8"));
|
|
@@ -4196,7 +4213,7 @@ function physicalPlanCandidateStillAllowed(candidatePath, repoAnchor, lstat = im
|
|
|
4196
4213
|
}
|
|
4197
4214
|
}
|
|
4198
4215
|
function formatScratchGcPlan(plan, applied, applyResult) {
|
|
4199
|
-
if (plan.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local
|
|
4216
|
+
if (plan.candidates.length === 0) return "scratch GC: nothing to prune \u2014 local scratch is clean.";
|
|
4200
4217
|
const lines = [];
|
|
4201
4218
|
const bytes = applyResult?.bytes ?? plan.safeAuto.reduce((n, c) => n + c.bytes, 0);
|
|
4202
4219
|
const safeCount = applyResult ? applyResult.pruned.length : plan.safeAuto.length;
|
|
@@ -4240,34 +4257,22 @@ function treeOlderThan(root, now, floor) {
|
|
|
4240
4257
|
}
|
|
4241
4258
|
return true;
|
|
4242
4259
|
}
|
|
4243
|
-
function applyScratchGc(plan,
|
|
4260
|
+
function applyScratchGc(plan, repoRoot, now = Date.now(), project2) {
|
|
4244
4261
|
const result = { pruned: [], skipped: 0, bytes: 0 };
|
|
4245
4262
|
let repoAnchor;
|
|
4246
|
-
let anchor;
|
|
4247
4263
|
try {
|
|
4248
|
-
repoAnchor = (0, import_node_fs5.realpathSync)(
|
|
4264
|
+
repoAnchor = (0, import_node_fs5.realpathSync)(repoRoot).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4249
4265
|
} catch {
|
|
4250
4266
|
return result;
|
|
4251
4267
|
}
|
|
4252
|
-
try {
|
|
4253
|
-
anchor = (0, import_node_fs5.realpathSync)(mmiRoot).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
4254
|
-
} catch {
|
|
4255
|
-
anchor = void 0;
|
|
4256
|
-
}
|
|
4257
4268
|
for (const c of plan.safeAuto) {
|
|
4258
4269
|
try {
|
|
4259
4270
|
const real = (0, import_node_fs5.realpathSync)(c.path).replace(/\\/g, "/");
|
|
4260
|
-
|
|
4261
|
-
if (!rootScoped && !anchor) {
|
|
4271
|
+
if (!pathContained(real, repoAnchor) || !rootCandidateStillAllowed(c, repoAnchor)) {
|
|
4262
4272
|
result.skipped += 1;
|
|
4263
4273
|
continue;
|
|
4264
4274
|
}
|
|
4265
|
-
|
|
4266
|
-
if (!containment || !pathContained(real, containment) || rootScoped && !rootCandidateStillAllowed(c, repoAnchor)) {
|
|
4267
|
-
result.skipped += 1;
|
|
4268
|
-
continue;
|
|
4269
|
-
}
|
|
4270
|
-
if (rootScoped && trackedPathStatus(c, repoAnchor) !== false) {
|
|
4275
|
+
if (trackedPathStatus(c, repoAnchor) !== false) {
|
|
4271
4276
|
result.skipped += 1;
|
|
4272
4277
|
continue;
|
|
4273
4278
|
}
|
|
@@ -4298,14 +4303,13 @@ function applyScratchGc(plan, mmiRoot, now = Date.now()) {
|
|
|
4298
4303
|
result.skipped += 1;
|
|
4299
4304
|
continue;
|
|
4300
4305
|
}
|
|
4301
|
-
const project2 = readProject(repoAnchor);
|
|
4302
4306
|
const meta = readPlanMeta(plansRoot);
|
|
4303
4307
|
if (!meta || !syncedPlanMetaEntry(meta, project2, slug, currentHash)) {
|
|
4304
4308
|
result.skipped += 1;
|
|
4305
4309
|
continue;
|
|
4306
4310
|
}
|
|
4307
4311
|
}
|
|
4308
|
-
const floor = c.family === "
|
|
4312
|
+
const floor = c.family === "plan" ? PLAN_ADVISORY_AGE_MS : c.family === "scratch-dir" || c.family === "scratch-file" ? ROOT_SCRATCH_STALE_MS : 0;
|
|
4309
4313
|
if (floor > 0 && now - st.mtimeMs <= floor) {
|
|
4310
4314
|
result.skipped += 1;
|
|
4311
4315
|
continue;
|
|
@@ -4389,8 +4393,7 @@ function rootScratchDirSnapshot(root, readdir, stat) {
|
|
|
4389
4393
|
function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
4390
4394
|
const readdir = deps.readdir ?? import_node_fs5.readdirSync;
|
|
4391
4395
|
const stat = deps.stat ?? import_node_fs5.statSync;
|
|
4392
|
-
const
|
|
4393
|
-
const mmiRoot = (0, import_node_path5.join)(repoRoot, ".mmi");
|
|
4396
|
+
const readFile2 = deps.readFile ?? import_node_fs5.readFileSync;
|
|
4394
4397
|
const plansRoot = (0, import_node_path5.join)(repoRoot, "plans");
|
|
4395
4398
|
const rootScratchFiles = [];
|
|
4396
4399
|
try {
|
|
@@ -4414,20 +4417,6 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
|
4414
4417
|
}
|
|
4415
4418
|
} catch {
|
|
4416
4419
|
}
|
|
4417
|
-
const mmiFiles = [];
|
|
4418
|
-
try {
|
|
4419
|
-
for (const ent of readdir(mmiRoot, { recursive: true, withFileTypes: true })) {
|
|
4420
|
-
if (!ent.isFile()) continue;
|
|
4421
|
-
const dir = ent.parentPath ?? ent.path ?? mmiRoot;
|
|
4422
|
-
const full = (0, import_node_path5.join)(dir, ent.name);
|
|
4423
|
-
try {
|
|
4424
|
-
const st = stat(full);
|
|
4425
|
-
mmiFiles.push({ path: full, dir, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size });
|
|
4426
|
-
} catch {
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
} catch {
|
|
4430
|
-
}
|
|
4431
4420
|
const planMdFiles = [];
|
|
4432
4421
|
try {
|
|
4433
4422
|
for (const ent of readdir(plansRoot, { withFileTypes: true })) {
|
|
@@ -4435,7 +4424,7 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
|
4435
4424
|
const full = (0, import_node_path5.join)(plansRoot, ent.name);
|
|
4436
4425
|
try {
|
|
4437
4426
|
const st = stat(full);
|
|
4438
|
-
const raw =
|
|
4427
|
+
const raw = readFile2(full, "utf8");
|
|
4439
4428
|
planMdFiles.push({ path: full, dir: plansRoot, name: ent.name, mtimeMs: st.mtimeMs, bytes: st.size, kind: "file", hash: hashContent(normalizeEol(raw)) });
|
|
4440
4429
|
} catch {
|
|
4441
4430
|
}
|
|
@@ -4444,20 +4433,15 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
|
4444
4433
|
}
|
|
4445
4434
|
let planMeta = {};
|
|
4446
4435
|
try {
|
|
4447
|
-
planMeta = parseMeta(
|
|
4436
|
+
planMeta = parseMeta(readFile2((0, import_node_path5.join)(plansRoot, ".plan-meta.json"), "utf8"));
|
|
4448
4437
|
} catch {
|
|
4449
4438
|
planMeta = {};
|
|
4450
4439
|
}
|
|
4451
|
-
|
|
4452
|
-
try {
|
|
4453
|
-
const cfg = JSON.parse(readFile3((0, import_node_path5.join)(repoRoot, ".mmi", "config.json"), "utf8"));
|
|
4454
|
-
if (typeof cfg.project === "string" && cfg.project.trim()) project2 = cfg.project.trim();
|
|
4455
|
-
} catch {
|
|
4456
|
-
}
|
|
4440
|
+
const project2 = deps.project?.trim() || void 0;
|
|
4457
4441
|
const syncQueueSlugs = (() => {
|
|
4458
4442
|
let queueRaw;
|
|
4459
4443
|
try {
|
|
4460
|
-
queueRaw =
|
|
4444
|
+
queueRaw = readFile2((0, import_node_path5.join)(plansRoot, ".sync-queue.json"), "utf8");
|
|
4461
4445
|
} catch (e) {
|
|
4462
4446
|
const code = typeof e === "object" && e && "code" in e ? String(e.code ?? "") : "";
|
|
4463
4447
|
return code === "ENOENT" || code === "ENOTDIR" ? /* @__PURE__ */ new Set() : null;
|
|
@@ -4470,7 +4454,7 @@ function collectScratchSnapshot(repoRoot, deps = {}) {
|
|
|
4470
4454
|
return null;
|
|
4471
4455
|
}
|
|
4472
4456
|
})();
|
|
4473
|
-
return { repoRoot,
|
|
4457
|
+
return { repoRoot, rootScratchFiles, planMdFiles, planMeta, project: project2, syncQueueSlugs };
|
|
4474
4458
|
}
|
|
4475
4459
|
|
|
4476
4460
|
// src/repo-runtime-state.ts
|
|
@@ -5062,7 +5046,6 @@ async function moveBoardItem(options, deps = {}) {
|
|
|
5062
5046
|
const lookup = await fetchIssueProjectItem(client, cfg, selector);
|
|
5063
5047
|
const item = lookup.item;
|
|
5064
5048
|
if (!item) throw new Error(`${selector.repo}#${selector.number} is not on this project board`);
|
|
5065
|
-
if (item.contentType !== "Issue") throw new Error(`${item.ref} is not an issue`);
|
|
5066
5049
|
const optionId = cfg.statusOptions[options.status];
|
|
5067
5050
|
try {
|
|
5068
5051
|
await updateItemSingleSelect(client, cfg.projectId, item.itemId, cfg.statusFieldId, optionId);
|
|
@@ -5252,6 +5235,36 @@ async function setBoardItemPriority(client, cfg, itemId, priority) {
|
|
|
5252
5235
|
await updateItemSingleSelect(client, cfg.projectId, itemId, cfg.priorityFieldId, optionId);
|
|
5253
5236
|
return cliPriorityToFieldName(priority);
|
|
5254
5237
|
}
|
|
5238
|
+
var defaultRetrySleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
5239
|
+
async function resolveProjectItemIdWithRetry(client, cfg, selector, opts = {}) {
|
|
5240
|
+
const attempts = Math.max(1, opts.attempts ?? 5);
|
|
5241
|
+
const delayMs = opts.delayMs ?? 300;
|
|
5242
|
+
const sleep = opts.sleep ?? defaultRetrySleep;
|
|
5243
|
+
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
|
5244
|
+
const itemId = (await fetchIssueProjectItem(client, cfg, selector)).item?.itemId;
|
|
5245
|
+
if (itemId) return itemId;
|
|
5246
|
+
if (attempt < attempts - 1) await sleep(delayMs * (attempt + 1));
|
|
5247
|
+
}
|
|
5248
|
+
return void 0;
|
|
5249
|
+
}
|
|
5250
|
+
async function setBoardPriorityForSelector(options, deps = {}) {
|
|
5251
|
+
const cfg = resolveBoardConfig(options.config);
|
|
5252
|
+
if (!isPriorityFieldConfigured(cfg)) {
|
|
5253
|
+
throw new Error("priority field is not configured in Hub registry META (priorityFieldId + priorityOptions)");
|
|
5254
|
+
}
|
|
5255
|
+
const client = deps.client ?? defaultGitHubClient();
|
|
5256
|
+
const itemId = await resolveProjectItemIdWithRetry(client, cfg, options.selector, options.retry);
|
|
5257
|
+
if (!itemId) {
|
|
5258
|
+
throw new Error(
|
|
5259
|
+
`issue #${options.selector.number} is not on the ${options.selector.repo} board yet (no Project v2 item) \u2014 add it to the board first, then retry`
|
|
5260
|
+
);
|
|
5261
|
+
}
|
|
5262
|
+
const priority = await setBoardItemPriority(client, cfg, itemId, options.priority);
|
|
5263
|
+
if (!priority) {
|
|
5264
|
+
throw new Error("priority field is not configured in Hub registry META (priorityFieldId + priorityOptions)");
|
|
5265
|
+
}
|
|
5266
|
+
return { ref: `${options.selector.repo}#${options.selector.number}`, itemId, priority };
|
|
5267
|
+
}
|
|
5255
5268
|
async function backfillBoardPriorities(options, deps = {}) {
|
|
5256
5269
|
const cfg = resolveBoardConfig(options.config);
|
|
5257
5270
|
if (!isPriorityFieldConfigured(cfg)) {
|
|
@@ -5872,6 +5885,8 @@ function parseGhPrChecksResult(stdout, stderr) {
|
|
|
5872
5885
|
}
|
|
5873
5886
|
var PR_CHECKS_POLL_MS = 3e4;
|
|
5874
5887
|
var PR_CHECKS_TIMEOUT_MS = 10 * 6e4;
|
|
5888
|
+
var PR_CHECKS_SUCCESS_CONFIRMATIONS = 2;
|
|
5889
|
+
var PR_CHECKS_CONFIRM_MS = 5e3;
|
|
5875
5890
|
async function waitForPrChecks(deps) {
|
|
5876
5891
|
const { policy, reason } = await deps.resolvePolicy();
|
|
5877
5892
|
if (policy === "no-ci") {
|
|
@@ -5880,10 +5895,20 @@ async function waitForPrChecks(deps) {
|
|
|
5880
5895
|
const now = deps.now ?? (() => Date.now());
|
|
5881
5896
|
const deadline = now() + PR_CHECKS_TIMEOUT_MS;
|
|
5882
5897
|
let lastDetail = "pending";
|
|
5898
|
+
let successStreak = 0;
|
|
5883
5899
|
while (now() < deadline) {
|
|
5884
5900
|
const state = await deps.pollChecks();
|
|
5885
|
-
if (state === "success") return { policy, status: "success", detail: lastDetail };
|
|
5886
5901
|
if (state === "failure") return { policy, status: "failure", detail: lastDetail };
|
|
5902
|
+
if (state === "success") {
|
|
5903
|
+
successStreak += 1;
|
|
5904
|
+
if (successStreak >= PR_CHECKS_SUCCESS_CONFIRMATIONS) {
|
|
5905
|
+
return { policy, status: "success", detail: "checks-success" };
|
|
5906
|
+
}
|
|
5907
|
+
lastDetail = "confirming-success";
|
|
5908
|
+
await deps.sleep(PR_CHECKS_CONFIRM_MS);
|
|
5909
|
+
continue;
|
|
5910
|
+
}
|
|
5911
|
+
successStreak = 0;
|
|
5887
5912
|
if (state === "no-checks-reported") {
|
|
5888
5913
|
lastDetail = "no-checks-reported (waiting for workflow to queue)";
|
|
5889
5914
|
await deps.sleep(PR_CHECKS_POLL_MS);
|
|
@@ -6065,6 +6090,10 @@ var MANAGED_GITIGNORE_LINES = [
|
|
|
6065
6090
|
"docs/superpowers/",
|
|
6066
6091
|
".playwright-mcp/",
|
|
6067
6092
|
".claude/worktrees/",
|
|
6093
|
+
// #2321 doctrine: the canonical worktree home is the SIBLING `../mmi-worktrees/<branch>` (outside the tree,
|
|
6094
|
+
// via `mmi-cli worktree create`), so a repo-local `.worktrees/` is NOT un-ignored org-wide — `mmi-cli
|
|
6095
|
+
// doctor` flags one explicitly instead (buildRepoLocalWorktreeCheck) rather than baking the fallback path
|
|
6096
|
+
// into every repo's .gitignore.
|
|
6068
6097
|
".aws-sam/",
|
|
6069
6098
|
"/*.png",
|
|
6070
6099
|
// Runtime secrets/config are vault-only (AGENTS.md "Privileged ops") — never commit a local .env. The
|
|
@@ -6329,10 +6358,10 @@ function labelsToPrune(orgLabelNames) {
|
|
|
6329
6358
|
const org = new Set(orgLabelNames);
|
|
6330
6359
|
return GITHUB_DEFAULT_LABELS.filter((name) => !org.has(name));
|
|
6331
6360
|
}
|
|
6332
|
-
function resolveSeedContent(seed, vars,
|
|
6333
|
-
if (seed.source === "self") return
|
|
6361
|
+
function resolveSeedContent(seed, vars, readFile2) {
|
|
6362
|
+
if (seed.source === "self") return readFile2(seed.target);
|
|
6334
6363
|
if (seed.source.startsWith("seed:")) {
|
|
6335
|
-
const tmpl =
|
|
6364
|
+
const tmpl = readFile2(`skills/bootstrap/seeds/${seed.source.slice("seed:".length)}`);
|
|
6336
6365
|
return tmpl == null ? null : renderSeed(tmpl, vars);
|
|
6337
6366
|
}
|
|
6338
6367
|
return null;
|
|
@@ -6496,6 +6525,23 @@ function decideFanoutPrAction(openPrs) {
|
|
|
6496
6525
|
}
|
|
6497
6526
|
return { action: "create" };
|
|
6498
6527
|
}
|
|
6528
|
+
function decideSeedDelivery(branchRules) {
|
|
6529
|
+
const rules2 = Array.isArray(branchRules) ? branchRules : [];
|
|
6530
|
+
const types = new Set(
|
|
6531
|
+
rules2.map((r) => r?.type).filter((t) => typeof t === "string")
|
|
6532
|
+
);
|
|
6533
|
+
const gating = ["pull_request", "required_status_checks"].filter((t) => types.has(t));
|
|
6534
|
+
if (gating.length) return { mode: "pr", reason: `base branch is protected (${gating.join(", ")}) \u2014 seed via a branch + PR` };
|
|
6535
|
+
return { mode: "direct", reason: "base branch is unprotected \u2014 direct PUT is safe" };
|
|
6536
|
+
}
|
|
6537
|
+
function planSeedDelivery(branchRules, slug, baseBranch) {
|
|
6538
|
+
const decision = decideSeedDelivery(branchRules);
|
|
6539
|
+
if (decision.mode === "pr") {
|
|
6540
|
+
const branch = `bootstrap-seed-${slug}`;
|
|
6541
|
+
return { mode: "pr", ref: branch, branch, reason: decision.reason };
|
|
6542
|
+
}
|
|
6543
|
+
return { mode: "direct", ref: baseBranch, reason: decision.reason };
|
|
6544
|
+
}
|
|
6499
6545
|
function contentPutArgs(repo, path2, content, branch, sha) {
|
|
6500
6546
|
const args = [
|
|
6501
6547
|
"api",
|
|
@@ -7696,8 +7742,8 @@ async function fastForwardCurrentBranch(git) {
|
|
|
7696
7742
|
return { status: "failed", error: errorMessage(e) };
|
|
7697
7743
|
}
|
|
7698
7744
|
}
|
|
7699
|
-
async function returnCheckoutToBase(git, base, mergedBranch,
|
|
7700
|
-
if (
|
|
7745
|
+
async function returnCheckoutToBase(git, base, mergedBranch, currentBranch2) {
|
|
7746
|
+
if (currentBranch2 === mergedBranch) {
|
|
7701
7747
|
const dirty = (await git(["status", "--porcelain"]).catch(() => "dirty") || "").trim().length > 0;
|
|
7702
7748
|
if (dirty) {
|
|
7703
7749
|
return { report: { branch: base, switched: false, sync: "skipped", reason: "dirty-worktree" }, canDeleteBranch: false };
|
|
@@ -7713,7 +7759,7 @@ async function returnCheckoutToBase(git, base, mergedBranch, currentBranch) {
|
|
|
7713
7759
|
const ff2 = await fastForwardCurrentBranch(git);
|
|
7714
7760
|
return { report: { branch: base, switched: true, sync: ff2.status, ...ff2.error ? { error: ff2.error } : {} }, canDeleteBranch: true };
|
|
7715
7761
|
}
|
|
7716
|
-
if (
|
|
7762
|
+
if (currentBranch2 !== base) {
|
|
7717
7763
|
return { report: { branch: base, switched: false, sync: "skipped", reason: "not-on-base" }, canDeleteBranch: true };
|
|
7718
7764
|
}
|
|
7719
7765
|
const ff = await fastForwardCurrentBranch(git);
|
|
@@ -8599,7 +8645,7 @@ async function collectWaveStatus(deps) {
|
|
|
8599
8645
|
} catch {
|
|
8600
8646
|
dirty = true;
|
|
8601
8647
|
}
|
|
8602
|
-
const headText = await deps.
|
|
8648
|
+
const headText = await deps.fetchHeadText?.(wt.branch).catch(() => void 0);
|
|
8603
8649
|
rows.push({
|
|
8604
8650
|
path: wt.path,
|
|
8605
8651
|
branch: wt.branch,
|
|
@@ -8641,12 +8687,11 @@ async function executeWaveLand(plan, deps, opts = { preserveWorktree: true }) {
|
|
|
8641
8687
|
}
|
|
8642
8688
|
|
|
8643
8689
|
// src/attach-to-project.ts
|
|
8644
|
-
async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m)) {
|
|
8690
|
+
async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn = (m) => process.stderr.write(m), retry = {}) {
|
|
8645
8691
|
let projectItemId;
|
|
8646
8692
|
try {
|
|
8647
8693
|
const boardCfg = resolveBoardConfig(cfg);
|
|
8648
|
-
|
|
8649
|
-
projectItemId = lookup.item?.itemId;
|
|
8694
|
+
projectItemId = await resolveProjectItemIdWithRetry(client, boardCfg, selector, retry);
|
|
8650
8695
|
} catch (e) {
|
|
8651
8696
|
warn(`warning: issue #${selector.number} board item lookup failed after auto-add: ${e.message}
|
|
8652
8697
|
`);
|
|
@@ -8671,7 +8716,7 @@ async function resolveAutoAddBoardAttach(client, cfg, selector, priority, warn =
|
|
|
8671
8716
|
}
|
|
8672
8717
|
|
|
8673
8718
|
// src/gh-create.ts
|
|
8674
|
-
var
|
|
8719
|
+
var import_promises = require("node:fs/promises");
|
|
8675
8720
|
var import_node_os3 = require("node:os");
|
|
8676
8721
|
var import_node_path12 = require("node:path");
|
|
8677
8722
|
var import_node_crypto3 = require("node:crypto");
|
|
@@ -8712,9 +8757,13 @@ async function bodyArgsViaFile(args, deps = {}) {
|
|
|
8712
8757
|
const i = args.indexOf("--body");
|
|
8713
8758
|
if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
|
|
8714
8759
|
} };
|
|
8715
|
-
const write = deps.write ??
|
|
8716
|
-
const remove = deps.remove ??
|
|
8717
|
-
const
|
|
8760
|
+
const write = deps.write ?? import_promises.writeFile;
|
|
8761
|
+
const remove = deps.remove ?? import_promises.unlink;
|
|
8762
|
+
const ensureDir = deps.ensureDir ?? import_promises.mkdir;
|
|
8763
|
+
const dir = deps.dir ?? (0, import_node_os3.tmpdir)();
|
|
8764
|
+
const file = (0, import_node_path12.join)(dir, `mmi-gh-body-${process.pid}-${(0, import_node_crypto3.randomBytes)(4).toString("hex")}.md`);
|
|
8765
|
+
await ensureDir((0, import_node_path12.dirname)(file), { recursive: true }).catch(() => {
|
|
8766
|
+
});
|
|
8718
8767
|
await write(file, args[i + 1], "utf8");
|
|
8719
8768
|
return {
|
|
8720
8769
|
args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
|
|
@@ -9133,6 +9182,9 @@ function parseNpmVersion(stdout) {
|
|
|
9133
9182
|
const v = stdout.trim();
|
|
9134
9183
|
return /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/.test(v) ? v : void 0;
|
|
9135
9184
|
}
|
|
9185
|
+
function staleTrainCliMessage(report, commandName) {
|
|
9186
|
+
return `running mmi-cli ${report.currentVersion} is stale against released ${report.releasedVersion}; this is recoverable \u2014 run \`mmi-cli doctor --apply --no-repo-writes\` to update to ${report.releasedVersion}, then rerun ${commandName} so it uses the current train path`;
|
|
9187
|
+
}
|
|
9136
9188
|
function versionAutoUpdateAction(report, hasPluginRoot) {
|
|
9137
9189
|
if (report.ok || report.staleAgainst !== "released") return "none";
|
|
9138
9190
|
return hasPluginRoot ? "plugin-pull" : "npm";
|
|
@@ -9296,14 +9348,6 @@ function trainPlanTargetsFromTags(tags, explicitRaw) {
|
|
|
9296
9348
|
}
|
|
9297
9349
|
return targets;
|
|
9298
9350
|
}
|
|
9299
|
-
function stagePlan(stage2 = {}, stops = true) {
|
|
9300
|
-
return [
|
|
9301
|
-
...stops ? [{ label: "force-kill previous local stage", command: "mmi-cli stage stop --apply" }] : [],
|
|
9302
|
-
{ label: "run local build", command: stage2.build || "(no stage.build configured)" },
|
|
9303
|
-
{ label: "start local stage", command: stage2.up || "(no stage.up configured)" },
|
|
9304
|
-
{ label: "check health", command: stage2.healthUrl ? `curl --fail ${stage2.healthUrl}` : "(no stage.healthUrl configured)" }
|
|
9305
|
-
];
|
|
9306
|
-
}
|
|
9307
9351
|
function derivedStagePlan(derived, shell2, stops = true) {
|
|
9308
9352
|
const { port } = derived;
|
|
9309
9353
|
const envOrder = ["MMI_STAGE", "MMI_PORT", "COMPOSE_PROFILES"];
|
|
@@ -9463,55 +9507,8 @@ function deriveStage(inputs) {
|
|
|
9463
9507
|
function stageUrlForPort(port) {
|
|
9464
9508
|
return `http://127.0.0.1:${port}/`;
|
|
9465
9509
|
}
|
|
9466
|
-
var POSIX_ONLY = [
|
|
9467
|
-
/(^|[^&|])&&([^&]|$)/,
|
|
9468
|
-
// && chaining (cmd.exe uses it too, but PowerShell <7 / the typical agent shell does not)
|
|
9469
|
-
/\|\|/,
|
|
9470
|
-
// || chaining
|
|
9471
|
-
// `VAR=value cmd` LEADING env prefix only — anchored to a command boundary (start-of-string or after a
|
|
9472
|
-
// ; & | separator) so it never fires on a flag value mid-command (`-e DEBUG=true app`, `--env X=y svc`),
|
|
9473
|
-
// which are valid cross-shell. The value excludes separators so it can't span past the command boundary.
|
|
9474
|
-
/(?:^|[;&|])\s*[A-Za-z_][A-Za-z0-9_]*=[^\s;&|]+\s+\S/,
|
|
9475
|
-
/(^|[\s;&|])(cp|rm|mv|ln|cat|touch|export)\s/
|
|
9476
|
-
// bare POSIX file/env builtins
|
|
9477
|
-
];
|
|
9478
|
-
function looksPosixOnly(command) {
|
|
9479
|
-
if (!command?.trim()) return false;
|
|
9480
|
-
return POSIX_ONLY.some((re) => re.test(command));
|
|
9481
|
-
}
|
|
9482
|
-
function stalePosixFields(config, shell2) {
|
|
9483
|
-
if (shell2 !== "powershell") return [];
|
|
9484
|
-
const fields = [];
|
|
9485
|
-
if (looksPosixOnly(config?.build)) fields.push("build");
|
|
9486
|
-
if (looksPosixOnly(config?.up)) fields.push("up");
|
|
9487
|
-
return fields;
|
|
9488
|
-
}
|
|
9489
|
-
function sanitizeLocalStage(local, stale) {
|
|
9490
|
-
if (!stale.length) return local;
|
|
9491
|
-
const clean3 = { ...local };
|
|
9492
|
-
for (const field of stale) delete clean3[field];
|
|
9493
|
-
return clean3;
|
|
9494
|
-
}
|
|
9495
|
-
function staleNote(staleFields, outcome) {
|
|
9496
|
-
const list = staleFields.join(", ");
|
|
9497
|
-
const plural = staleFields.length > 1 ? "fields" : "field";
|
|
9498
|
-
return `local .mmi stage ${plural} ${list} ${staleFields.length > 1 ? "are" : "is"} POSIX-only and unusable on PowerShell \u2014 ${outcome}`;
|
|
9499
|
-
}
|
|
9500
9510
|
function decideStage(inputs) {
|
|
9501
|
-
const {
|
|
9502
|
-
const staleFields = stalePosixFields(local, shell2);
|
|
9503
|
-
const stale = staleFields.length > 0;
|
|
9504
|
-
const upStale = staleFields.includes("up");
|
|
9505
|
-
if (local?.up?.trim() && !upStale) {
|
|
9506
|
-
if (!stale) return { source: "local", config: local };
|
|
9507
|
-
return {
|
|
9508
|
-
source: "local",
|
|
9509
|
-
config: sanitizeLocalStage(local, staleFields),
|
|
9510
|
-
staleIgnored: true,
|
|
9511
|
-
staleFields,
|
|
9512
|
-
gap: staleNote(staleFields, `kept the cross-shell parts of the local recipe and ignored ${staleFields.join(", ")}`)
|
|
9513
|
-
};
|
|
9514
|
-
}
|
|
9511
|
+
const { registry: registry2, hasCompose, hasEnvExample } = inputs;
|
|
9515
9512
|
const deriveInputs = {
|
|
9516
9513
|
portRange: registry2.portRange,
|
|
9517
9514
|
deployModel: registry2.deployModel,
|
|
@@ -9519,19 +9516,10 @@ function decideStage(inputs) {
|
|
|
9519
9516
|
hasEnvExample
|
|
9520
9517
|
};
|
|
9521
9518
|
const derived = deriveStage(deriveInputs);
|
|
9522
|
-
if (derived) {
|
|
9523
|
-
return {
|
|
9524
|
-
source: "derived",
|
|
9525
|
-
derived,
|
|
9526
|
-
registryError: registry2.error,
|
|
9527
|
-
staleIgnored: stale || void 0,
|
|
9528
|
-
staleFields: stale ? staleFields : void 0
|
|
9529
|
-
};
|
|
9530
|
-
}
|
|
9519
|
+
if (derived) return { source: "derived", derived, registryError: registry2.error };
|
|
9531
9520
|
const registryGap = registry2.error ? `Hub registry read failed (${registry2.error}) \u2014 cannot derive a default local stage` : null;
|
|
9532
|
-
const
|
|
9533
|
-
|
|
9534
|
-
return { source: "none", gap, staleIgnored: stale || void 0, staleFields: stale ? staleFields : void 0, registryError: registry2.error };
|
|
9521
|
+
const gap = registryGap ?? deriveStageGap(deriveInputs) ?? "no registry-derived default available";
|
|
9522
|
+
return { source: "none", gap, registryError: registry2.error };
|
|
9535
9523
|
}
|
|
9536
9524
|
|
|
9537
9525
|
// src/stage-live.ts
|
|
@@ -9661,6 +9649,14 @@ function requireValue(value, label) {
|
|
|
9661
9649
|
if (!value) throw new Error(`${label} could not be resolved`);
|
|
9662
9650
|
return value;
|
|
9663
9651
|
}
|
|
9652
|
+
function planTrainApplyRepoGuard(applyRepo, cwdRepo, rerun) {
|
|
9653
|
+
if (cwdRepo && cwdRepo.toLowerCase() === applyRepo.toLowerCase()) return { ok: true };
|
|
9654
|
+
const where = cwdRepo ? `this checkout is ${cwdRepo}` : "this checkout could not be identified";
|
|
9655
|
+
return {
|
|
9656
|
+
ok: false,
|
|
9657
|
+
message: `--apply runs from the current checkout, not --repo (${where}). cd into the ${applyRepo} checkout and rerun without --repo: ${rerun}`
|
|
9658
|
+
};
|
|
9659
|
+
}
|
|
9664
9660
|
async function resolveFoldPaths(deps, model) {
|
|
9665
9661
|
if (model === "hub-serverless") {
|
|
9666
9662
|
return (await deps.run("node", ["scripts/release-distribution.mjs", "changed-files"])).split("\n").map((s) => s.trim()).filter(Boolean);
|
|
@@ -9672,12 +9668,17 @@ async function resolveFoldPaths(deps, model) {
|
|
|
9672
9668
|
}
|
|
9673
9669
|
return ["package.json", "package-lock.json"];
|
|
9674
9670
|
}
|
|
9671
|
+
async function installAppFoldDeps(deps) {
|
|
9672
|
+
const hasLock = await deps.run("git", ["cat-file", "-e", "HEAD:package-lock.json"]).then(() => true).catch(() => false);
|
|
9673
|
+
await deps.run("npm", hasLock ? ["ci"] : ["install"]);
|
|
9674
|
+
}
|
|
9675
9675
|
async function foldReleaseVersion(deps, model, tag, foldPaths) {
|
|
9676
9676
|
if (foldPaths.length === 0) return "no version manifest to fold \u2014 the tag is the version";
|
|
9677
9677
|
const version = tag.replace(/^v/, "");
|
|
9678
9678
|
if (model === "hub-serverless") {
|
|
9679
9679
|
await deps.run("node", ["scripts/release-distribution.mjs", "prepare", version]);
|
|
9680
9680
|
} else {
|
|
9681
|
+
await installAppFoldDeps(deps);
|
|
9681
9682
|
await deps.run("npm", ["version", version, "--no-git-tag-version", "--allow-same-version"]);
|
|
9682
9683
|
}
|
|
9683
9684
|
for (const path2 of foldPaths) {
|
|
@@ -9809,6 +9810,55 @@ async function requireBranch(deps, branch) {
|
|
|
9809
9810
|
const current = clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"]));
|
|
9810
9811
|
if (current !== branch) throw new Error(`must run from ${branch}, currently on ${current || "(unknown)"}`);
|
|
9811
9812
|
}
|
|
9813
|
+
async function currentBranch(deps) {
|
|
9814
|
+
return clean(await deps.run("git", ["rev-parse", "--abbrev-ref", "HEAD"])) || "(unknown)";
|
|
9815
|
+
}
|
|
9816
|
+
async function restoreCheckoutAfterRelease(deps, startBranch) {
|
|
9817
|
+
const status = await deps.run("git", ["status", "--porcelain"]);
|
|
9818
|
+
if (porcelainHasBlockingChanges(status)) {
|
|
9819
|
+
return {
|
|
9820
|
+
startBranch,
|
|
9821
|
+
finalBranch: await currentBranch(deps),
|
|
9822
|
+
status: "skipped",
|
|
9823
|
+
reason: "dirty-worktree",
|
|
9824
|
+
note: `checkout restoration skipped: working tree changed after release; inspect it before leaving ${startBranch}`
|
|
9825
|
+
};
|
|
9826
|
+
}
|
|
9827
|
+
try {
|
|
9828
|
+
await deps.run("git", ["checkout", startBranch]);
|
|
9829
|
+
} catch (e) {
|
|
9830
|
+
return {
|
|
9831
|
+
startBranch,
|
|
9832
|
+
finalBranch: await currentBranch(deps),
|
|
9833
|
+
status: "skipped",
|
|
9834
|
+
reason: "checkout-failed",
|
|
9835
|
+
note: `checkout restoration failed while returning to ${startBranch}: ${e instanceof Error ? e.message : String(e)}`
|
|
9836
|
+
};
|
|
9837
|
+
}
|
|
9838
|
+
try {
|
|
9839
|
+
await ffOnlyPull(deps, startBranch);
|
|
9840
|
+
return {
|
|
9841
|
+
startBranch,
|
|
9842
|
+
finalBranch: startBranch,
|
|
9843
|
+
status: "returned",
|
|
9844
|
+
note: `checkout returned to ${startBranch} and fast-forwarded from origin/${startBranch}`
|
|
9845
|
+
};
|
|
9846
|
+
} catch (e) {
|
|
9847
|
+
return {
|
|
9848
|
+
startBranch,
|
|
9849
|
+
finalBranch: startBranch,
|
|
9850
|
+
status: "skipped",
|
|
9851
|
+
reason: "pull-failed",
|
|
9852
|
+
note: `checkout returned to ${startBranch}, but origin/${startBranch} fast-forward failed: ${e instanceof Error ? e.message : String(e)}`
|
|
9853
|
+
};
|
|
9854
|
+
}
|
|
9855
|
+
}
|
|
9856
|
+
async function withCheckoutRestoredAfterRelease(deps, result, startBranch) {
|
|
9857
|
+
return {
|
|
9858
|
+
...result,
|
|
9859
|
+
checkout: await restoreCheckoutAfterRelease(deps, startBranch)
|
|
9860
|
+
};
|
|
9861
|
+
}
|
|
9812
9862
|
function normGitPath(p) {
|
|
9813
9863
|
return p.replace(/\\/g, "/").replace(/\/+$/, "").toLowerCase();
|
|
9814
9864
|
}
|
|
@@ -9932,8 +9982,8 @@ async function correlateRun(deps, args) {
|
|
|
9932
9982
|
function correlateTenantRun(deps, since, titleIncludes) {
|
|
9933
9983
|
return correlateRun(deps, { workflow: "tenant-deploy.yml", since, mode: "dispatch", titleIncludes });
|
|
9934
9984
|
}
|
|
9935
|
-
function correlatePublishRun(deps, since) {
|
|
9936
|
-
return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch" });
|
|
9985
|
+
function correlatePublishRun(deps, since, titleIncludes) {
|
|
9986
|
+
return correlateRun(deps, { workflow: "tenant-publish.yml", since, mode: "dispatch", titleIncludes });
|
|
9937
9987
|
}
|
|
9938
9988
|
function correlateControlRun(deps, since, titleIncludes) {
|
|
9939
9989
|
return correlateRun(deps, { workflow: "tenant-control.yml", since, mode: "dispatch", titleIncludes });
|
|
@@ -10219,7 +10269,7 @@ async function dispatchTenantPublish(deps, ctx, stage2, ref, watch, dispatchFail
|
|
|
10219
10269
|
deployStatus: "failure"
|
|
10220
10270
|
};
|
|
10221
10271
|
}
|
|
10222
|
-
const { runId, runUrl } = await correlatePublishRun(deps, since);
|
|
10272
|
+
const { runId, runUrl } = await correlatePublishRun(deps, since, [ctx.slug, stage2]);
|
|
10223
10273
|
const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
|
|
10224
10274
|
return { note: `dispatched tenant-publish.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
|
|
10225
10275
|
}
|
|
@@ -10359,7 +10409,7 @@ async function pushRcAlignment(deps) {
|
|
|
10359
10409
|
await deps.run("git", ["push", "origin", "main:rc"]);
|
|
10360
10410
|
return "origin/rc aligned to the released main";
|
|
10361
10411
|
} catch (e) {
|
|
10362
|
-
return `rc alignment push failed \u2014
|
|
10412
|
+
return `rc alignment push failed \u2014 do NOT bare-push rc (Step 5 forbids it; branch protection rejects it). rc realigns on the next \`mmi-cli rcand\`, or rerun via the train: ${e instanceof Error ? e.message : String(e)}`;
|
|
10363
10413
|
}
|
|
10364
10414
|
}
|
|
10365
10415
|
async function runTrainApplyPipeline(mode, input) {
|
|
@@ -10554,12 +10604,24 @@ async function runTrainApply(command, deps, options = {}) {
|
|
|
10554
10604
|
return runTrainApplyPipeline("rcand", pipelineInput);
|
|
10555
10605
|
}
|
|
10556
10606
|
if (directTrack) {
|
|
10557
|
-
return
|
|
10607
|
+
return withCheckoutRestoredAfterRelease(
|
|
10608
|
+
deps,
|
|
10609
|
+
await runTrainApplyPipeline("release-dev", { ...pipelineInput, directTrack: true }),
|
|
10610
|
+
"development"
|
|
10611
|
+
);
|
|
10558
10612
|
}
|
|
10559
10613
|
if (command === "release" && options.dev) {
|
|
10560
|
-
return
|
|
10614
|
+
return withCheckoutRestoredAfterRelease(
|
|
10615
|
+
deps,
|
|
10616
|
+
await runTrainApplyPipeline("release-dev", pipelineInput),
|
|
10617
|
+
"development"
|
|
10618
|
+
);
|
|
10561
10619
|
}
|
|
10562
|
-
return
|
|
10620
|
+
return withCheckoutRestoredAfterRelease(
|
|
10621
|
+
deps,
|
|
10622
|
+
await runTrainApplyPipeline("release-full", pipelineInput),
|
|
10623
|
+
"rc"
|
|
10624
|
+
);
|
|
10563
10625
|
}
|
|
10564
10626
|
async function buildEnvironments(deps, ctx, model, deployStatus, retirement) {
|
|
10565
10627
|
if (model !== "tenant-container") return void 0;
|
|
@@ -11551,9 +11613,7 @@ async function ensurePortRangeAtomic(repo, path2, allocate, opts = {}) {
|
|
|
11551
11613
|
var OWNER = "mutmutco";
|
|
11552
11614
|
var LOCKED_APP = "mmi-github-app";
|
|
11553
11615
|
var OVERGRANT_ROLES = /* @__PURE__ */ new Set(["admin", "maintain"]);
|
|
11554
|
-
var REQUIRED_DATA_ACCESS = {
|
|
11555
|
-
"mutmutco/MM-Chat": [{ name: "kb-projection-reader", dbRole: "kb_reader", vaultParamNeedle: "KB_READ_DB_URL" }]
|
|
11556
|
-
};
|
|
11616
|
+
var REQUIRED_DATA_ACCESS = {};
|
|
11557
11617
|
function lockedBranches(repoClass, releaseTrack) {
|
|
11558
11618
|
if (releaseTrack) return branchesForTrack(releaseTrack);
|
|
11559
11619
|
return repoClass === "content" ? ["main"] : ["development", "rc", "main"];
|
|
@@ -14265,7 +14325,7 @@ async function secretsUse(deps, key, opts) {
|
|
|
14265
14325
|
` \u2022 Keyless run: \`mmi-cli secrets use ${key}${opts.slug ? ` --slug ${opts.slug}` : ""} -- <command>\` injects it into the command's env (never printed). A granted non-master can USE an org secret this way.`,
|
|
14266
14326
|
` \u2022 Runtime / agents: read it keylessly at runtime via the box's OIDC role. Never bake it into an image or commit it.`,
|
|
14267
14327
|
` \u2022 CI (GitHub Actions): the workflow assumes its OIDC role and runs \`aws ssm get-parameter --with-decryption --name ${path2}\` \u2014 no GitHub secret.`,
|
|
14268
|
-
tier === "project" ? " \u2022
|
|
14328
|
+
tier === "project" ? " \u2022 One stageless value per repo (/mmi-future/<slug>/<KEY>) \u2014 no dev/rc/main copies. Project-admins self-serve their repo keys." : " \u2022 Org-infra/cross-slug keys remain master-gated unless granted; project-admins self-serve their own repo keys."
|
|
14269
14329
|
].join("\n")
|
|
14270
14330
|
);
|
|
14271
14331
|
return;
|
|
@@ -14559,7 +14619,7 @@ function registerEdgeCommands(program3) {
|
|
|
14559
14619
|
// src/doctor-run.ts
|
|
14560
14620
|
var import_node_fs17 = require("node:fs");
|
|
14561
14621
|
var import_node_child_process10 = require("node:child_process");
|
|
14562
|
-
var
|
|
14622
|
+
var import_promises2 = require("node:fs/promises");
|
|
14563
14623
|
var import_node_path17 = require("node:path");
|
|
14564
14624
|
var import_node_os6 = require("node:os");
|
|
14565
14625
|
|
|
@@ -14884,6 +14944,15 @@ function buildGitignoreManagedBlockCheck(input) {
|
|
|
14884
14944
|
const { added, removed, seeded } = diffManagedGitignoreBlock(input.content);
|
|
14885
14945
|
return { ...base, ok: false, contentToWrite: content, added, removed, seeded };
|
|
14886
14946
|
}
|
|
14947
|
+
var REPO_LOCAL_WORKTREE_LABEL = "worktree location (canonical sibling path)";
|
|
14948
|
+
var REPO_LOCAL_WORKTREE_FIX = "repo-local `.worktrees/` is not the canonical worktree path (#2321) \u2014 use `mmi-cli worktree create <branch>`, which provisions the sibling `../mmi-worktrees/<branch>` outside the tree. Move your work there, then remove the in-repo `.worktrees/`.";
|
|
14949
|
+
function buildRepoLocalWorktreeCheck(input) {
|
|
14950
|
+
return {
|
|
14951
|
+
ok: !(input.isOrgRepo && input.hasRepoLocalWorktrees),
|
|
14952
|
+
label: REPO_LOCAL_WORKTREE_LABEL,
|
|
14953
|
+
fix: REPO_LOCAL_WORKTREE_FIX
|
|
14954
|
+
};
|
|
14955
|
+
}
|
|
14887
14956
|
var SCRATCH_GC_LABEL = "scratch housekeeping (tmp/, plans/, browser artifacts)";
|
|
14888
14957
|
var SCRATCH_GC_FIX = "run `mmi-cli gc --scratch --dry-run` to inspect; run `mmi-cli gc --scratch --apply` to prune safe scratch; for kept plans run `mmi-cli northstar status` or `mmi-cli northstar sync --wait` first";
|
|
14889
14958
|
function buildScratchGcCheck(plan) {
|
|
@@ -15057,10 +15126,6 @@ var OPENCODE_WORKFLOW_COMMANDS = [
|
|
|
15057
15126
|
"release",
|
|
15058
15127
|
"hotfix",
|
|
15059
15128
|
"bootstrap",
|
|
15060
|
-
"grind",
|
|
15061
|
-
"build",
|
|
15062
|
-
"handoff",
|
|
15063
|
-
"coop",
|
|
15064
15129
|
"browser-automation"
|
|
15065
15130
|
];
|
|
15066
15131
|
function opencodeCommandDescription(command) {
|
|
@@ -15316,6 +15381,28 @@ function buildOpencodeVersionCheck(input) {
|
|
|
15316
15381
|
}
|
|
15317
15382
|
return { ...base, ok: false, installedVersion: input.installedVersion, releasedVersion: input.releasedVersion };
|
|
15318
15383
|
}
|
|
15384
|
+
function pickOpencodeActiveVersion(input) {
|
|
15385
|
+
for (const value of [input.envStamp, input.cacheVersion, input.diskVersion]) {
|
|
15386
|
+
const trimmed = value?.trim();
|
|
15387
|
+
if (trimmed) return trimmed;
|
|
15388
|
+
}
|
|
15389
|
+
return void 0;
|
|
15390
|
+
}
|
|
15391
|
+
function planOpencodePluginCacheQuarantine(input) {
|
|
15392
|
+
if (!input.cacheExists) return { quarantine: false };
|
|
15393
|
+
if (!isSemverVersion2(input.cacheVersion) || !isSemverVersion2(input.releasedVersion)) return { quarantine: false };
|
|
15394
|
+
if (compareVersions(input.cacheVersion, input.releasedVersion) < 0) {
|
|
15395
|
+
return { quarantine: true, cacheVersion: input.cacheVersion, releasedVersion: input.releasedVersion };
|
|
15396
|
+
}
|
|
15397
|
+
return { quarantine: false };
|
|
15398
|
+
}
|
|
15399
|
+
var OPENCODE_HOOK_ACTIVE_LABEL = "OpenCode MMI adapter hooks active (shell.env stamp)";
|
|
15400
|
+
function buildOpencodeHookActiveCheck(input) {
|
|
15401
|
+
const base = { ok: true, label: OPENCODE_HOOK_ACTIVE_LABEL, fix: OPENCODE_RECOVERY };
|
|
15402
|
+
if (!input.isOrgRepo || input.surface !== "opencode") return base;
|
|
15403
|
+
if (input.hookSurfaceStamp === "opencode") return { ...base, surfaceStamp: input.hookSurfaceStamp };
|
|
15404
|
+
return { ...base, ok: false, surfaceStamp: input.hookSurfaceStamp };
|
|
15405
|
+
}
|
|
15319
15406
|
var OPENCODE_CONFIG_PLUGIN_LABEL = "OpenCode MMI adapter config wiring";
|
|
15320
15407
|
var OPENCODE_SURFACE_ASSETS_LABEL = "OpenCode MMI commands and skills";
|
|
15321
15408
|
function opencodePluginEntryMatches(entry) {
|
|
@@ -16313,8 +16400,59 @@ async function forceInstallOpencodeMmiPlugins(snapshot, log) {
|
|
|
16313
16400
|
return false;
|
|
16314
16401
|
}
|
|
16315
16402
|
}
|
|
16403
|
+
function opencodePackagesRoot() {
|
|
16404
|
+
return (0, import_node_path17.join)((0, import_node_os6.homedir)(), ".cache", "opencode", "packages", "@mutmutco");
|
|
16405
|
+
}
|
|
16406
|
+
function opencodePluginCacheDirs() {
|
|
16407
|
+
const root = opencodePackagesRoot();
|
|
16408
|
+
try {
|
|
16409
|
+
return (0, import_node_fs17.readdirSync)(root, { withFileTypes: true }).filter((e) => e.isDirectory() && e.name.startsWith("opencode-mmi@") && !e.name.startsWith(".")).map((e) => (0, import_node_path17.join)(root, e.name));
|
|
16410
|
+
} catch {
|
|
16411
|
+
return [];
|
|
16412
|
+
}
|
|
16413
|
+
}
|
|
16414
|
+
function readOpencodeCacheDirVersion(cacheDir) {
|
|
16415
|
+
try {
|
|
16416
|
+
const parsed = JSON.parse(
|
|
16417
|
+
(0, import_node_fs17.readFileSync)((0, import_node_path17.join)(cacheDir, "node_modules", "@mutmutco", "opencode-mmi", "package.json"), "utf8")
|
|
16418
|
+
);
|
|
16419
|
+
return typeof parsed.version === "string" && parsed.version.trim() ? parsed.version.trim() : void 0;
|
|
16420
|
+
} catch {
|
|
16421
|
+
return void 0;
|
|
16422
|
+
}
|
|
16423
|
+
}
|
|
16424
|
+
function readOpencodeLoadedCacheVersion() {
|
|
16425
|
+
const versions = opencodePluginCacheDirs().map(readOpencodeCacheDirVersion).filter((v) => Boolean(v));
|
|
16426
|
+
if (!versions.length) return void 0;
|
|
16427
|
+
return versions.reduce((lowest, v) => compareVersions(v, lowest) < 0 ? v : lowest);
|
|
16428
|
+
}
|
|
16429
|
+
function quarantineStaleOpencodePluginCaches(releasedVersion, log) {
|
|
16430
|
+
let moved = false;
|
|
16431
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
16432
|
+
for (const cacheDir of opencodePluginCacheDirs()) {
|
|
16433
|
+
const plan = planOpencodePluginCacheQuarantine({
|
|
16434
|
+
cacheExists: true,
|
|
16435
|
+
cacheVersion: readOpencodeCacheDirVersion(cacheDir),
|
|
16436
|
+
releasedVersion
|
|
16437
|
+
});
|
|
16438
|
+
if (!plan.quarantine) continue;
|
|
16439
|
+
try {
|
|
16440
|
+
const quarantineRoot = (0, import_node_path17.join)(opencodePackagesRoot(), ".mmi-quarantine", stamp);
|
|
16441
|
+
(0, import_node_fs17.mkdirSync)(quarantineRoot, { recursive: true });
|
|
16442
|
+
(0, import_node_fs17.renameSync)(cacheDir, (0, import_node_path17.join)(quarantineRoot, (0, import_node_path17.basename)(cacheDir)));
|
|
16443
|
+
log(` \u21BB quarantined stale OpenCode plugin cache ${plan.cacheVersion} (< ${plan.releasedVersion}) \u2014 OpenCode reinstalls the current adapter on next start`);
|
|
16444
|
+
moved = true;
|
|
16445
|
+
} catch {
|
|
16446
|
+
}
|
|
16447
|
+
}
|
|
16448
|
+
return moved;
|
|
16449
|
+
}
|
|
16316
16450
|
function opencodeInstalledVersionForDoctor() {
|
|
16317
|
-
return
|
|
16451
|
+
return pickOpencodeActiveVersion({
|
|
16452
|
+
envStamp: process.env.MMI_OPENCODE_PLUGIN_VERSION,
|
|
16453
|
+
cacheVersion: readOpencodeLoadedCacheVersion(),
|
|
16454
|
+
diskVersion: readOpencodeAdapterDiskVersion()
|
|
16455
|
+
});
|
|
16318
16456
|
}
|
|
16319
16457
|
function opencodePluginVersionsForReport() {
|
|
16320
16458
|
return [process.env.MMI_OPENCODE_PLUGIN_VERSION, readOpencodeAdapterDiskVersion()].filter((v) => Boolean(v));
|
|
@@ -16752,6 +16890,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16752
16890
|
}
|
|
16753
16891
|
}
|
|
16754
16892
|
checks.push(gitignoreCheck);
|
|
16893
|
+
checks.push(buildRepoLocalWorktreeCheck({
|
|
16894
|
+
isOrgRepo,
|
|
16895
|
+
hasRepoLocalWorktrees: (0, import_node_fs17.existsSync)((0, import_node_path17.join)(process.cwd(), ".worktrees"))
|
|
16896
|
+
}));
|
|
16755
16897
|
let driftCheck = buildPluginConfigDriftCheck({ isOrgRepo, installed, surface });
|
|
16756
16898
|
if (!driftCheck.ok && driftCheck.recordsToWrite && repairLocal) {
|
|
16757
16899
|
if (backupAndWriteInstalledPlugins(driftCheck.recordsToWrite, driftCheck.pluginId)) {
|
|
@@ -16838,8 +16980,10 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16838
16980
|
releasedVersion
|
|
16839
16981
|
});
|
|
16840
16982
|
if (!opencodeVersionCheck.ok && repairFull) {
|
|
16841
|
-
|
|
16842
|
-
|
|
16983
|
+
const refreshed = await forceInstallOpencodeMmiPlugins(openCodeConfigSnapshot, (m) => io.err(m));
|
|
16984
|
+
const quarantined = quarantineStaleOpencodePluginCaches(releasedVersion, (m) => io.err(m));
|
|
16985
|
+
if (refreshed || quarantined) {
|
|
16986
|
+
opencodeInstalledVersion = opencodeInstalledVersionForDoctor() ?? opencodeInstalledVersion;
|
|
16843
16987
|
opencodeVersionCheck = buildOpencodeVersionCheck({
|
|
16844
16988
|
isOrgRepo,
|
|
16845
16989
|
installedVersion: opencodeInstalledVersion,
|
|
@@ -16852,6 +16996,16 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
16852
16996
|
}
|
|
16853
16997
|
}
|
|
16854
16998
|
checks.push(opencodeVersionCheck);
|
|
16999
|
+
const hookSurfaceStamp = process.env.MMI_AGENT_SURFACE;
|
|
17000
|
+
const hookActiveCheck = buildOpencodeHookActiveCheck({
|
|
17001
|
+
isOrgRepo,
|
|
17002
|
+
surface,
|
|
17003
|
+
hookSurfaceStamp
|
|
17004
|
+
});
|
|
17005
|
+
if (!hookActiveCheck.ok && repairLocal) {
|
|
17006
|
+
io.err(` \u21BB MMI adapter config wired but hooks not active this session \u2014 ${reloadAction("opencode")} to load MMI plugin hooks`);
|
|
17007
|
+
}
|
|
17008
|
+
checks.push(hookActiveCheck);
|
|
16855
17009
|
let surfaceAssetsCheck = buildOpencodeSurfaceAssetsCheck({
|
|
16856
17010
|
isOrgRepo,
|
|
16857
17011
|
configPath: openCodeConfigSnapshot.path,
|
|
@@ -17002,7 +17156,7 @@ async function runDoctor(opts, io = consoleIo, readOrigin) {
|
|
|
17002
17156
|
releasedVersion,
|
|
17003
17157
|
hubCheckout: hubCheckoutForCursorSeed(),
|
|
17004
17158
|
execFileP: execFileP2,
|
|
17005
|
-
mkdtemp: (prefix) => (0,
|
|
17159
|
+
mkdtemp: (prefix) => (0, import_promises2.mkdtemp)((0, import_node_path17.join)((0, import_node_os6.tmpdir)(), prefix)),
|
|
17006
17160
|
log: (m) => io.err(m)
|
|
17007
17161
|
});
|
|
17008
17162
|
if (seeded) {
|
|
@@ -17245,7 +17399,6 @@ function repoFromSelector(selector) {
|
|
|
17245
17399
|
async function loadConfigForBoardSelector(selector, repoOption) {
|
|
17246
17400
|
return loadConfigForRepo(repoFromSelector(selector) ?? repoOption);
|
|
17247
17401
|
}
|
|
17248
|
-
var DEFAULT_RULES_SOURCE = "https://raw.githubusercontent.com/mutmutco/MMI-Hub/development";
|
|
17249
17402
|
function renderGcApplyResult(result) {
|
|
17250
17403
|
const lines = ["gc apply result:"];
|
|
17251
17404
|
lines.push(` branches removed: ${result.removedBranches.length ? result.removedBranches.join(", ") : "none"}`);
|
|
@@ -17361,65 +17514,11 @@ async function requireFreshTrainCli(commandName) {
|
|
|
17361
17514
|
releasedVersion: await fetchNpmReleasedVersion()
|
|
17362
17515
|
});
|
|
17363
17516
|
if (report.ok) return;
|
|
17364
|
-
throw new Error(
|
|
17517
|
+
throw new Error(staleTrainCliMessage(report, commandName));
|
|
17365
17518
|
}
|
|
17366
17519
|
var program2 = new Command();
|
|
17367
17520
|
program2.name("mmi-cli").description("MMI Future CLI \u2014 org rules delivery, Jervaise-only continuity. The engine the plugin SessionStart hook drives.").version(resolveClientVersion()).showHelpAfterError("(run `mmi-cli commands` to list every subcommand + its flags, or `mmi-cli commands --json` to ground against it)");
|
|
17368
|
-
|
|
17369
|
-
const cfg = await loadConfig();
|
|
17370
|
-
if (isRulesSource(cfg.orgRulesSource)) {
|
|
17371
|
-
if (!opts.quiet) io.log('mmi-cli rules: source repo (orgRulesSource: "self") \u2014 skipping self-sync');
|
|
17372
|
-
return true;
|
|
17373
|
-
}
|
|
17374
|
-
if (!await isOrgRegisteredRepo(cfg)) {
|
|
17375
|
-
if (!opts.quiet) io.log("mmi-cli rules: not an org repo \u2014 skipping spine delivery");
|
|
17376
|
-
return true;
|
|
17377
|
-
}
|
|
17378
|
-
const base = resolveRulesBase(cfg.orgRulesSource, DEFAULT_RULES_SOURCE);
|
|
17379
|
-
const token = await githubToken();
|
|
17380
|
-
let changed = 0;
|
|
17381
|
-
const files = [
|
|
17382
|
-
"AGENTS.md",
|
|
17383
|
-
"CLAUDE.md",
|
|
17384
|
-
".claude/settings.json",
|
|
17385
|
-
".claude/output-styles/mmi-plain.md",
|
|
17386
|
-
".cursor/rules/mmi-plain-language.mdc",
|
|
17387
|
-
".cursor/rules/mmi-tool-economy.mdc",
|
|
17388
|
-
".cursor/rules/mmi-code-economy.mdc"
|
|
17389
|
-
];
|
|
17390
|
-
const fetched = await Promise.all(files.map(async (file) => {
|
|
17391
|
-
try {
|
|
17392
|
-
const url = `${base}/${file}`;
|
|
17393
|
-
const res = await fetch(url, { headers: rulesSourceAuthHeaders(url, token), signal: AbortSignal.timeout(1e4) });
|
|
17394
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
17395
|
-
return { file, source: await res.text() };
|
|
17396
|
-
} catch (e) {
|
|
17397
|
-
return { file, error: e.message };
|
|
17398
|
-
}
|
|
17399
|
-
}));
|
|
17400
|
-
const failures = fetched.filter((entry) => "error" in entry);
|
|
17401
|
-
for (const failure of failures) {
|
|
17402
|
-
io.err(`mmi-cli rules: could not fetch ${failure.file} (${failure.error}); left it untouched`);
|
|
17403
|
-
}
|
|
17404
|
-
for (const entry of fetched) {
|
|
17405
|
-
if ("error" in entry) continue;
|
|
17406
|
-
const { file, source } = entry;
|
|
17407
|
-
const current = (0, import_node_fs18.existsSync)(file) ? await (0, import_promises4.readFile)(file, "utf8") : null;
|
|
17408
|
-
if (needsUpdate(source, current)) {
|
|
17409
|
-
const slash = file.lastIndexOf("/");
|
|
17410
|
-
if (slash > 0) (0, import_node_fs18.mkdirSync)(file.slice(0, slash), { recursive: true });
|
|
17411
|
-
await (0, import_promises4.writeFile)(file, normalizeEol(source), "utf8");
|
|
17412
|
-
changed++;
|
|
17413
|
-
if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
|
|
17414
|
-
}
|
|
17415
|
-
}
|
|
17416
|
-
if (!opts.quiet && changed === 0) io.log("mmi-cli rules: up to date");
|
|
17417
|
-
return failures.length === 0;
|
|
17418
|
-
}
|
|
17419
|
-
var rules = program2.command("rules").description("org rules delivery");
|
|
17420
|
-
rules.command("sync").option("--quiet", "stay silent unless something changed or errored").description("fetch the org-delivered files (AGENTS.md / CLAUDE.md / .claude/settings.json / output style / Cursor rule) from MMI-Hub and write them verbatim (org-owned, whole-file)").action(async (opts) => {
|
|
17421
|
-
if (!await runRulesSync(opts)) process.exitCode = 1;
|
|
17422
|
-
});
|
|
17521
|
+
var rules = program2.command("rules").description("org-managed .gitignore delivery");
|
|
17423
17522
|
rules.command("gitignore").option("--write", "upsert the managed block into .gitignore (default: check only, non-zero exit on drift)").description("verify (or --write) this repo's org-managed .gitignore block matches the SSOT").action((opts) => {
|
|
17424
17523
|
const path2 = (0, import_node_path18.join)(process.cwd(), ".gitignore");
|
|
17425
17524
|
const current = (0, import_node_fs18.existsSync)(path2) ? (0, import_node_fs18.readFileSync)(path2, "utf8") : null;
|
|
@@ -17441,30 +17540,6 @@ rules.command("gitignore").option("--write", "upsert the managed block into .git
|
|
|
17441
17540
|
console.log("mmi-cli rules gitignore: up to date");
|
|
17442
17541
|
}
|
|
17443
17542
|
});
|
|
17444
|
-
async function runDocsSync(opts, io = consoleIo) {
|
|
17445
|
-
const ref = await gitOut(["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"]);
|
|
17446
|
-
const def = (ref.startsWith("origin/") ? ref.slice("origin/".length) : ref) || "development";
|
|
17447
|
-
await gitOut(["fetch", "origin", def, "--quiet"]);
|
|
17448
|
-
const result = await syncDocs({
|
|
17449
|
-
isDirty: async (f) => await gitOut(["status", "--porcelain", "--", f]) !== "",
|
|
17450
|
-
originContent: async (f) => {
|
|
17451
|
-
try {
|
|
17452
|
-
return (await execFileP2("git", ["show", `origin/${def}:${f}`], { maxBuffer: 10 * 1024 * 1024 })).stdout;
|
|
17453
|
-
} catch {
|
|
17454
|
-
return null;
|
|
17455
|
-
}
|
|
17456
|
-
},
|
|
17457
|
-
localContent: async (f) => (0, import_node_fs18.existsSync)(f) ? await (0, import_promises4.readFile)(f, "utf8") : null,
|
|
17458
|
-
writeDoc: async (f, c) => {
|
|
17459
|
-
await (0, import_promises4.writeFile)(f, c, "utf8");
|
|
17460
|
-
}
|
|
17461
|
-
});
|
|
17462
|
-
for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
|
|
17463
|
-
if (!opts.quiet && result.skippedDirty.length) io.log(`mmi-cli docs: kept local edits in ${result.skippedDirty.join(", ")}`);
|
|
17464
|
-
if (!opts.quiet && result.updated.length === 0 && result.skippedDirty.length === 0) io.log("mmi-cli docs: up to date");
|
|
17465
|
-
}
|
|
17466
|
-
var docs = program2.command("docs").description("repo-owned authoritative docs");
|
|
17467
|
-
docs.command("sync").option("--quiet", "stay silent unless something changed or errored").description("refresh README.md / architecture.md from the repo default branch; never clobbers uncommitted edits").action((opts) => runDocsSync(opts));
|
|
17468
17543
|
program2.command("commands").description("print the command manifest \u2014 every subcommand + its flags (ground against this instead of guessing)").option("--json", "machine-readable JSON: { name, version, tree, index } \u2014 index is a flat list of every leaf command path").action((o) => {
|
|
17469
17544
|
const manifest = buildCommandManifest(program2);
|
|
17470
17545
|
consoleIo.log(o.json ? JSON.stringify(manifest, null, 2) : formatManifestHuman(manifest));
|
|
@@ -18375,8 +18450,8 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
18375
18450
|
let northStarSlug;
|
|
18376
18451
|
let extraLabels = [];
|
|
18377
18452
|
try {
|
|
18378
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18379
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18453
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18454
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18380
18455
|
if (o.northStar !== void 0) {
|
|
18381
18456
|
northStarSlug = normalizeNorthStarSlug(o.northStar);
|
|
18382
18457
|
body = appendNorthStarLine(body, northStarSlug);
|
|
@@ -18433,6 +18508,20 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
|
|
|
18433
18508
|
...parentLinkFields(parent, parentLinkError)
|
|
18434
18509
|
}));
|
|
18435
18510
|
});
|
|
18511
|
+
issue.command("view <number>").description("read an issue as structured JSON \u2014 the mmi-cli path for non-board issue reads (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json <fields>", "comma-separated gh --json field list (overrides the default field set)").action(async (number, o) => {
|
|
18512
|
+
const n = Number(number);
|
|
18513
|
+
if (!Number.isInteger(n) || n <= 0) return fail("issue view: <number> must be a positive integer");
|
|
18514
|
+
const repo = await resolveRepo(o.repo);
|
|
18515
|
+
if (!repo) return fail("issue view: could not resolve repo (pass --repo <owner/repo>)");
|
|
18516
|
+
const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,labels,author,assignees,milestone,body";
|
|
18517
|
+
try {
|
|
18518
|
+
const data = await ghJson(["issue", "view", String(n), "--repo", repo, "--json", fields]);
|
|
18519
|
+
console.log(JSON.stringify(data));
|
|
18520
|
+
} catch (e) {
|
|
18521
|
+
const err = e;
|
|
18522
|
+
return fail(`issue view: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
18523
|
+
}
|
|
18524
|
+
});
|
|
18436
18525
|
issue.command("discover-related").description("find related issues for an existing issue and post only high-confidence links").requiredOption("--number <number>", "created issue number").requiredOption("--title <title>", "created issue title").requiredOption("--body <body>", "created issue body").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json", "print candidates instead of posting").action(async (o) => {
|
|
18437
18526
|
const number = Number(o.number);
|
|
18438
18527
|
if (!Number.isInteger(number) || number <= 0) return fail("issue discover-related: --number must be a positive integer");
|
|
@@ -18488,7 +18577,7 @@ issue.command("comment <ref>").description("post a Markdown comment to an issue
|
|
|
18488
18577
|
}
|
|
18489
18578
|
let body;
|
|
18490
18579
|
try {
|
|
18491
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18580
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18492
18581
|
} catch (e) {
|
|
18493
18582
|
return fail(`issue comment: ${e.message}`);
|
|
18494
18583
|
}
|
|
@@ -18550,8 +18639,8 @@ program2.command("report").description("file a friction report on the Hub board
|
|
|
18550
18639
|
const targetRepo2 = o.repo ?? HUB_REPO2;
|
|
18551
18640
|
const sourceRepo = await resolveRepo(void 0);
|
|
18552
18641
|
try {
|
|
18553
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18554
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18642
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18643
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18555
18644
|
priority = normalizePriority(o.priority);
|
|
18556
18645
|
args = buildIssueArgs({
|
|
18557
18646
|
type: o.type,
|
|
@@ -18624,8 +18713,8 @@ program2.command("skill-lesson").description("file a skill-lesson on the Hub boa
|
|
|
18624
18713
|
let args;
|
|
18625
18714
|
try {
|
|
18626
18715
|
skill = assertSkillName(o.skill);
|
|
18627
|
-
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18628
|
-
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18716
|
+
rawBody = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18717
|
+
const rawTitle = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18629
18718
|
title = buildSkillLessonTitle(skill, rawTitle);
|
|
18630
18719
|
priority = normalizePriority(o.priority);
|
|
18631
18720
|
body = buildSkillLessonBody(rawBody, sourceRepo, pluginSha);
|
|
@@ -18676,14 +18765,28 @@ pr.command("create").description("create a PR and print {number,url} JSON").opti
|
|
|
18676
18765
|
let body;
|
|
18677
18766
|
let title;
|
|
18678
18767
|
try {
|
|
18679
|
-
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile:
|
|
18680
|
-
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile:
|
|
18768
|
+
title = await resolveIssueTitle({ title: o.title, titleFile: o.titleFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18769
|
+
body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises3.readFile, readStdin });
|
|
18681
18770
|
} catch (e) {
|
|
18682
18771
|
return fail(`pr create: ${e.message}`);
|
|
18683
18772
|
}
|
|
18684
18773
|
const created = await ghCreate(buildPrArgs({ title, body, base: o.base, head: o.head, repo: o.repo }));
|
|
18685
18774
|
console.log(JSON.stringify(created));
|
|
18686
18775
|
});
|
|
18776
|
+
pr.command("view <number>").description("read a PR as structured JSON (merged state, head/base, URL, merge commit) \u2014 the mmi-cli read path (#2347)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").option("--json <fields>", "comma-separated gh --json field list (overrides the default field set)").action(async (number, o) => {
|
|
18777
|
+
const n = Number(number);
|
|
18778
|
+
if (!Number.isInteger(n) || n <= 0) return fail("pr view: <number> must be a positive integer");
|
|
18779
|
+
const repo = await resolveRepo(o.repo);
|
|
18780
|
+
if (!repo) return fail("pr view: could not resolve repo (pass --repo <owner/repo>)");
|
|
18781
|
+
const fields = o.json && o.json.trim() ? o.json.trim() : "number,title,state,url,isDraft,mergeable,mergedAt,mergeCommit,headRefName,baseRefName,author,labels";
|
|
18782
|
+
try {
|
|
18783
|
+
const data = await ghJson(["pr", "view", String(n), "--repo", repo, "--json", fields]);
|
|
18784
|
+
console.log(JSON.stringify(data));
|
|
18785
|
+
} catch (e) {
|
|
18786
|
+
const err = e;
|
|
18787
|
+
return fail(`pr view: ${(err.stderr || err.message || String(e)).trim()}`);
|
|
18788
|
+
}
|
|
18789
|
+
});
|
|
18687
18790
|
async function listCiWorkflowPaths(cwd = process.cwd()) {
|
|
18688
18791
|
const wfDir = (0, import_node_path18.join)(cwd, ".github", "workflows");
|
|
18689
18792
|
if (!(0, import_node_fs18.existsSync)(wfDir)) return [];
|
|
@@ -18852,15 +18955,15 @@ async function createDeferredWorktreeStore() {
|
|
|
18852
18955
|
return {
|
|
18853
18956
|
read: async () => {
|
|
18854
18957
|
try {
|
|
18855
|
-
return parseDeferredWorktreesFile(await (0,
|
|
18958
|
+
return parseDeferredWorktreesFile(await (0, import_promises3.readFile)(registryPath, "utf8"));
|
|
18856
18959
|
} catch {
|
|
18857
18960
|
return [];
|
|
18858
18961
|
}
|
|
18859
18962
|
},
|
|
18860
18963
|
write: async (entries) => {
|
|
18861
18964
|
try {
|
|
18862
|
-
await (0,
|
|
18863
|
-
await (0,
|
|
18965
|
+
await (0, import_promises3.mkdir)((0, import_node_path18.dirname)(registryPath), { recursive: true });
|
|
18966
|
+
await (0, import_promises3.writeFile)(registryPath, serializeDeferredWorktrees(entries), "utf8");
|
|
18864
18967
|
} catch {
|
|
18865
18968
|
}
|
|
18866
18969
|
}
|
|
@@ -18901,7 +19004,7 @@ var realWorktreeDirRemover = {
|
|
|
18901
19004
|
(0, import_node_fs18.unlinkSync)(p);
|
|
18902
19005
|
}
|
|
18903
19006
|
},
|
|
18904
|
-
removeTree: (p) => (0,
|
|
19007
|
+
removeTree: (p) => (0, import_promises3.rm)(p, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
|
|
18905
19008
|
};
|
|
18906
19009
|
async function resolvePrimaryCheckout(execGit) {
|
|
18907
19010
|
try {
|
|
@@ -18959,9 +19062,9 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
18959
19062
|
const beforeWorktrees = parseWorktreePorcelain(
|
|
18960
19063
|
(await execFileP2("git", ["worktree", "list", "--porcelain"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout
|
|
18961
19064
|
);
|
|
18962
|
-
const remoteBefore =
|
|
19065
|
+
const remoteBefore = await remoteBranchExists2(headRef);
|
|
18963
19066
|
let remoteDeleteAttempted = false;
|
|
18964
|
-
let remoteNotAttemptedReason
|
|
19067
|
+
let remoteNotAttemptedReason;
|
|
18965
19068
|
await execFileP2("gh", buildPrMergeArgs({ number, repoArgs, method, auto: o.auto }), { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
|
|
18966
19069
|
const message = String(e.message || "");
|
|
18967
19070
|
if (/already been merged/i.test(message)) {
|
|
@@ -18983,10 +19086,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
18983
19086
|
}
|
|
18984
19087
|
}
|
|
18985
19088
|
if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
|
|
18986
|
-
const remoteBranch =
|
|
18987
|
-
attempted: false,
|
|
18988
|
-
reason: remoteNotAttemptedReason
|
|
18989
|
-
}) : await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
19089
|
+
const remoteBranch = await buildPrMergeRemoteBranchCleanupReport(headRef, {
|
|
18990
19090
|
exists: remoteBranchExists2
|
|
18991
19091
|
}, {
|
|
18992
19092
|
attempted: remoteDeleteAttempted,
|
|
@@ -18996,11 +19096,7 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
|
|
|
18996
19096
|
const deferredStore = await createDeferredWorktreeStore();
|
|
18997
19097
|
let localCleanup;
|
|
18998
19098
|
try {
|
|
18999
|
-
localCleanup =
|
|
19000
|
-
branch: headRef,
|
|
19001
|
-
localBranch: { name: headRef, status: "not-attempted", reason: "repo-option" },
|
|
19002
|
-
worktree: void 0
|
|
19003
|
-
} : await cleanupPrMergeLocalBranch(headRef, {
|
|
19099
|
+
localCleanup = await cleanupPrMergeLocalBranch(headRef, {
|
|
19004
19100
|
beforeWorktrees,
|
|
19005
19101
|
startingPath,
|
|
19006
19102
|
pathExists: (p) => (0, import_node_fs18.existsSync)(p),
|
|
@@ -19127,6 +19223,30 @@ board.command("backfill-priority").description("set board Priority from priority
|
|
|
19127
19223
|
return failGraceful(`board backfill-priority failed: ${e.message}`);
|
|
19128
19224
|
}
|
|
19129
19225
|
});
|
|
19226
|
+
board.command("set-priority <issue> <priority>").description(`set a board item's Priority field (${CLI_PRIORITIES.join("|")}) \u2014 recovers a Priority lost on the create-time auto-add race (#2349)`).option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo for local issue numbers (defaults to git origin)").action(async (issueRef, priorityArg, o) => {
|
|
19227
|
+
let priority;
|
|
19228
|
+
try {
|
|
19229
|
+
priority = normalizePriority(priorityArg);
|
|
19230
|
+
} catch (e) {
|
|
19231
|
+
return fail(`board set-priority failed: ${e.message}`);
|
|
19232
|
+
}
|
|
19233
|
+
try {
|
|
19234
|
+
const defaultRepo = await resolveRepo(o.repo) ?? "";
|
|
19235
|
+
const selector = parseIssueSelector(issueRef, defaultRepo);
|
|
19236
|
+
if (!selector.repo) {
|
|
19237
|
+
return fail("board set-priority failed: could not resolve the repo \u2014 pass owner/repo#123 or use --repo");
|
|
19238
|
+
}
|
|
19239
|
+
const result = await setBoardPriorityForSelector({
|
|
19240
|
+
config: await loadConfigForBoardSelector(issueRef, o.repo),
|
|
19241
|
+
selector,
|
|
19242
|
+
priority
|
|
19243
|
+
});
|
|
19244
|
+
if (o.json) return console.log(JSON.stringify(result));
|
|
19245
|
+
console.log(`Set ${result.ref} Priority -> ${result.priority}`);
|
|
19246
|
+
} catch (e) {
|
|
19247
|
+
return failGraceful(`board set-priority failed: ${e.message}`);
|
|
19248
|
+
}
|
|
19249
|
+
});
|
|
19130
19250
|
board.command("prune-priority-labels").description("remove retired priority:* labels (#416) from issues whose board Priority field is already set").option("--json", "machine-readable output").option("--repo <owner/repo>", "current repo (defaults to git origin)").option("--dry-run", "report what would be removed without writing").option("--concurrency <n>", "parallel issue edits (default 8)", "8").action(async (o) => {
|
|
19131
19251
|
try {
|
|
19132
19252
|
const result = await prunePriorityLabels({
|
|
@@ -19208,14 +19328,11 @@ function stageKeepAlive() {
|
|
|
19208
19328
|
}
|
|
19209
19329
|
async function resolveStage() {
|
|
19210
19330
|
const cfg = await loadConfig();
|
|
19211
|
-
const local = cfg.stage;
|
|
19212
19331
|
const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(cfg)).catch((e) => ({ ok: false, error: e.message }));
|
|
19213
19332
|
const project2 = read.ok ? read.project : null;
|
|
19214
19333
|
const portRangeMeta = project2?.portRange ?? void 0;
|
|
19215
19334
|
const portRange = portRangeMeta && typeof portRangeMeta.start === "number" && typeof portRangeMeta.end === "number" ? [portRangeMeta.start, portRangeMeta.end] : void 0;
|
|
19216
19335
|
return decideStage({
|
|
19217
|
-
local,
|
|
19218
|
-
shell: shellFor(),
|
|
19219
19336
|
registry: { deployModel: project2?.deployModel, portRange, error: read.ok ? void 0 : read.error },
|
|
19220
19337
|
hasCompose: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), "docker-compose.yml")),
|
|
19221
19338
|
hasEnvExample: (0, import_node_fs18.existsSync)((0, import_node_path18.join)(process.cwd(), ".env.example"))
|
|
@@ -19239,19 +19356,8 @@ async function fetchStageVaultEnvMerge() {
|
|
|
19239
19356
|
}
|
|
19240
19357
|
function stageStepsFor(res, stops = true) {
|
|
19241
19358
|
if (res.source === "derived" && res.derived) return derivedStagePlan(res.derived, shellFor(), stops);
|
|
19242
|
-
if (res.source === "local") return stagePlan(res.config ?? {}, stops);
|
|
19243
19359
|
return [{ label: `no local stage to run \u2014 ${res.gap ?? "stage config gap"}` }];
|
|
19244
19360
|
}
|
|
19245
|
-
function staleStageNote(res) {
|
|
19246
|
-
if (!res.staleIgnored) return null;
|
|
19247
|
-
const fields = res.staleFields ?? [];
|
|
19248
|
-
const list = fields.join(", ");
|
|
19249
|
-
const label = fields.length > 1 ? `fields ${list}` : `field ${list || "build/up"}`;
|
|
19250
|
-
if (res.source === "local") {
|
|
19251
|
-
return `note: POSIX-only .mmi stage ${label} ignored on PowerShell \u2014 kept the rest of the local recipe`;
|
|
19252
|
-
}
|
|
19253
|
-
return `note: stale POSIX-only .mmi stage ${label} ignored on PowerShell \u2014 using the registry-derived default`;
|
|
19254
|
-
}
|
|
19255
19361
|
function reportedStageUrl(res, result) {
|
|
19256
19362
|
if (!res.derived) return void 0;
|
|
19257
19363
|
return result.port != null ? stageUrlForPort(result.port) : res.derived.url;
|
|
@@ -19342,7 +19448,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
|
|
|
19342
19448
|
const res = await resolveStage();
|
|
19343
19449
|
if (o.apply) {
|
|
19344
19450
|
if (res.source === "none") return failGraceful(`stage: ${res.gap}`);
|
|
19345
|
-
const cfg = res.
|
|
19451
|
+
const cfg = res.derived.config;
|
|
19346
19452
|
const hold = stageKeepAlive();
|
|
19347
19453
|
try {
|
|
19348
19454
|
const result = await runStage(cfg, stageScopedRunOpts({ timeoutMs: o.timeoutMs }));
|
|
@@ -19356,9 +19462,7 @@ var stage = program2.command("stage").description("plan or run the repo local st
|
|
|
19356
19462
|
}
|
|
19357
19463
|
}
|
|
19358
19464
|
const steps = stageStepsFor(res);
|
|
19359
|
-
if (o.json) return console.log(JSON.stringify({ command: "stage", source: res.source, url: res.derived?.url,
|
|
19360
|
-
const note = staleStageNote(res);
|
|
19361
|
-
if (note) printLine(note);
|
|
19465
|
+
if (o.json) return console.log(JSON.stringify({ command: "stage", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
|
|
19362
19466
|
console.log(renderSteps("mmi-cli stage: dry-run plan", steps));
|
|
19363
19467
|
});
|
|
19364
19468
|
stage.command("stop").description("stop the previous local stage process recorded in tmp/stage/state.json").option("--json", "machine-readable output").option("--apply", "kill the recorded process tree and remove the state file").action(async () => {
|
|
@@ -19379,14 +19483,12 @@ stage.command("start").description("start the configured local stage process and
|
|
|
19379
19483
|
const res = await resolveStage();
|
|
19380
19484
|
if (!o.apply) {
|
|
19381
19485
|
const steps = stageStepsFor(res, false);
|
|
19382
|
-
if (o.json) return printLine(JSON.stringify({ command: "stage start", source: res.source, url: res.derived?.url,
|
|
19383
|
-
const note = staleStageNote(res);
|
|
19384
|
-
if (note) printLine(note);
|
|
19486
|
+
if (o.json) return printLine(JSON.stringify({ command: "stage start", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
|
|
19385
19487
|
return printLine(renderSteps("mmi-cli stage start: dry-run plan", steps));
|
|
19386
19488
|
}
|
|
19387
19489
|
if (res.source === "none") return failGraceful(`stage start: ${res.gap}`);
|
|
19388
|
-
const cfg = res.
|
|
19389
|
-
const vaultEnvMerge =
|
|
19490
|
+
const cfg = res.derived.config;
|
|
19491
|
+
const vaultEnvMerge = await fetchStageVaultEnvMerge();
|
|
19390
19492
|
try {
|
|
19391
19493
|
const hold = stageKeepAlive();
|
|
19392
19494
|
let printed = false;
|
|
@@ -19414,14 +19516,12 @@ stage.command("run").description("force-stop previous stage, build, start, and h
|
|
|
19414
19516
|
const res = await resolveStage();
|
|
19415
19517
|
if (!o.apply) {
|
|
19416
19518
|
const steps = stageStepsFor(res);
|
|
19417
|
-
if (o.json) return printLine(JSON.stringify({ command: "stage run", source: res.source, url: res.derived?.url,
|
|
19418
|
-
const note = staleStageNote(res);
|
|
19419
|
-
if (note) printLine(note);
|
|
19519
|
+
if (o.json) return printLine(JSON.stringify({ command: "stage run", source: res.source, url: res.derived?.url, registryError: res.registryError, steps }, null, 2));
|
|
19420
19520
|
return printLine(renderSteps("mmi-cli stage run: dry-run plan", steps));
|
|
19421
19521
|
}
|
|
19422
19522
|
if (res.source === "none") return failGraceful(`stage run: ${res.gap}`);
|
|
19423
|
-
const cfg = res.
|
|
19424
|
-
const vaultEnvMerge =
|
|
19523
|
+
const cfg = res.derived.config;
|
|
19524
|
+
const vaultEnvMerge = await fetchStageVaultEnvMerge();
|
|
19425
19525
|
try {
|
|
19426
19526
|
const hold = stageKeepAlive();
|
|
19427
19527
|
let printed = false;
|
|
@@ -19491,8 +19591,8 @@ function trainApplyDeps() {
|
|
|
19491
19591
|
// Slack release announcement (#883): Hub-only + best-effort inside announceRelease itself.
|
|
19492
19592
|
announce: (args) => announceRelease({
|
|
19493
19593
|
run: async (file, cmdArgs) => (await execFileP2(file, cmdArgs, { timeout: GH_TRAIN_TIMEOUT_MS })).stdout,
|
|
19494
|
-
readFile: (path2) => (0,
|
|
19495
|
-
removeFile: (path2) => (0,
|
|
19594
|
+
readFile: (path2) => (0, import_promises3.readFile)(path2, "utf8"),
|
|
19595
|
+
removeFile: (path2) => (0, import_promises3.unlink)(path2)
|
|
19496
19596
|
}, args),
|
|
19497
19597
|
fetchEdgeDomains: async (slug) => {
|
|
19498
19598
|
const proj = await fetchProjectBySlug(slug, registryClientDeps(await loadConfig()));
|
|
@@ -19527,6 +19627,9 @@ function renderTrainApply(commandName, r) {
|
|
|
19527
19627
|
const f = r.devRollForward;
|
|
19528
19628
|
base = f.status === "pr-pending" ? `${base}; dev roll-forward: ALIGNMENT PR PENDING \u2014 land it with \`gh pr merge ${f.prNumber ?? "<number>"} --merge\`${f.prUrl ? ` (${f.prUrl})` : ""}` : `${base}; dev roll-forward: ${f.note}`;
|
|
19529
19629
|
}
|
|
19630
|
+
if (r.checkout) {
|
|
19631
|
+
base = `${base}; checkout: ${r.checkout.note}`;
|
|
19632
|
+
}
|
|
19530
19633
|
return r.announceNote ? `${base}; announce: ${r.announceNote}` : base;
|
|
19531
19634
|
}
|
|
19532
19635
|
function renderTenantRedeploy(r) {
|
|
@@ -19554,7 +19657,9 @@ for (const commandName of ["rcand", "release"]) {
|
|
|
19554
19657
|
return fail("--dev applies only to release: it ships development -> main skipping rc, which rcand cannot do");
|
|
19555
19658
|
}
|
|
19556
19659
|
if (o.apply && o.repo) {
|
|
19557
|
-
|
|
19660
|
+
const rerun = `mmi-cli ${commandName} --apply${o.watch ? " --watch" : ""}${o.dev ? " --dev" : ""}${o.json ? " --json" : ""}`;
|
|
19661
|
+
const guard = planTrainApplyRepoGuard(o.repo, await resolveRepo(), rerun);
|
|
19662
|
+
if (!guard.ok) return fail(`${commandName}: ${guard.message}`);
|
|
19558
19663
|
}
|
|
19559
19664
|
if (o.apply) {
|
|
19560
19665
|
try {
|
|
@@ -19707,6 +19812,14 @@ bootstrap.command("verify <repo>").description("audit whether an existing repo i
|
|
|
19707
19812
|
console.log(o.json ? JSON.stringify(report, null, 2) : renderBootstrapVerifyReport(report));
|
|
19708
19813
|
if (!report.ok) process.exitCode = 1;
|
|
19709
19814
|
});
|
|
19815
|
+
bootstrap.command("org-ruleset").description("drift-check the codified org no-agent-files push ruleset (mmi-no-agent-files-org) against live; read-only, never mutates (#2196)").option("--json", "machine-readable output").action(async () => {
|
|
19816
|
+
const json = rawFlag("--json");
|
|
19817
|
+
const live = await fetchOrgNoAgentFilesRuleset(defaultGitHubClient());
|
|
19818
|
+
const plan = planOrgNoAgentFilesRuleset(live);
|
|
19819
|
+
if (json) console.log(JSON.stringify({ action: plan.action, id: plan.id, drift: plan.drift, desired: plan.desired }, null, 2));
|
|
19820
|
+
else console.log(renderOrgRulesetDriftReport(plan));
|
|
19821
|
+
if (plan.action !== "noop") process.exitCode = 1;
|
|
19822
|
+
});
|
|
19710
19823
|
bootstrap.command("apply <repo>").description("idempotent seed apply from skills/bootstrap/seeds/manifest.json; dry-run unless --execute (live, master-gated)").option("--class <class>", "deployable | content", "deployable").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path)`).option("--release-track <track>", `${RELEASE_TRACKS.join(" | ")} (branch topology; direct skips rc)`).option("--execute", "LIVE apply via gh (master-gated) \u2014 stamps seed files + labels into the repo").option("--var <KEY=VALUE...>", "placeholder values for repo-owned templates (repeatable)").option("--json", "machine-readable output").action(async (repo) => {
|
|
19711
19824
|
const o = {
|
|
19712
19825
|
class: rawValue("--class", "deployable"),
|
|
@@ -19730,7 +19843,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19730
19843
|
const baseBranch = o.class === "content" ? "main" : "development";
|
|
19731
19844
|
const slug = parsedRepo.slug;
|
|
19732
19845
|
const gh = async (args) => execFileP2("gh", args, { timeout: 2e4 });
|
|
19733
|
-
const
|
|
19846
|
+
const readFile2 = (p) => (0, import_node_fs18.existsSync)(p) ? (0, import_node_fs18.readFileSync)(p, "utf8") : null;
|
|
19734
19847
|
const enc = (p) => p.split("/").map(encodeURIComponent).join("/");
|
|
19735
19848
|
const rawVars = {};
|
|
19736
19849
|
for (const value of rawValues("--var")) {
|
|
@@ -19763,6 +19876,25 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19763
19876
|
applyDeployModel = payload.deployModel;
|
|
19764
19877
|
} catch {
|
|
19765
19878
|
}
|
|
19879
|
+
let seedPlan = { mode: "direct", ref: baseBranch, reason: "dry-run (no protection probe)" };
|
|
19880
|
+
let seededToBranch = 0;
|
|
19881
|
+
if (o.execute) {
|
|
19882
|
+
let branchRules = [];
|
|
19883
|
+
try {
|
|
19884
|
+
branchRules = JSON.parse((await gh(["api", `repos/${repo}/rules/branches/${baseBranch}`])).stdout || "[]");
|
|
19885
|
+
} catch {
|
|
19886
|
+
branchRules = [];
|
|
19887
|
+
}
|
|
19888
|
+
seedPlan = planSeedDelivery(branchRules, slug, baseBranch);
|
|
19889
|
+
if (seedPlan.branch) {
|
|
19890
|
+
const headSha = (await gh(["api", `repos/${repo}/git/ref/heads/${baseBranch}`, "--jq", ".object.sha"])).stdout.trim();
|
|
19891
|
+
try {
|
|
19892
|
+
await gh(["api", "-X", "POST", `repos/${repo}/git/refs`, "-f", `ref=refs/heads/${seedPlan.branch}`, "-f", `sha=${headSha}`]);
|
|
19893
|
+
} catch (e) {
|
|
19894
|
+
if (!/Reference already exists|already exists/i.test(String(e.message ?? ""))) throw e;
|
|
19895
|
+
}
|
|
19896
|
+
}
|
|
19897
|
+
}
|
|
19766
19898
|
for (const seed of manifest.seeds) {
|
|
19767
19899
|
if (!seed.classes.includes(o.class)) continue;
|
|
19768
19900
|
if (!seedMatchesDeployModel(seed, applyDeployModel)) continue;
|
|
@@ -19772,7 +19904,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19772
19904
|
let remoteContent = null;
|
|
19773
19905
|
if (resolved.source !== "fanout") {
|
|
19774
19906
|
try {
|
|
19775
|
-
const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${
|
|
19907
|
+
const r = await gh(["api", `repos/${repo}/contents/${enc(resolved.target)}?ref=${seedPlan.ref}`]);
|
|
19776
19908
|
exists = true;
|
|
19777
19909
|
try {
|
|
19778
19910
|
const parsed = JSON.parse(r.stdout);
|
|
@@ -19788,13 +19920,44 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19788
19920
|
}
|
|
19789
19921
|
const planned = planSeedAction(resolved, exists);
|
|
19790
19922
|
const isBlock = resolved.source === "managed-block";
|
|
19791
|
-
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars,
|
|
19923
|
+
const content = planned.action === "create" || planned.action === "update" ? isBlock ? upsertManagedGitignoreBlock(remoteContent).content : resolveSeedContent(resolved, vars, readFile2) : null;
|
|
19792
19924
|
const action = reconcileSeedAction(planned, content, isBlock);
|
|
19793
19925
|
actions.push(action);
|
|
19794
19926
|
if (o.execute && (action.action === "create" || action.action === "update")) {
|
|
19795
|
-
await gh(contentPutArgs(repo, resolved.target, content,
|
|
19927
|
+
await gh(contentPutArgs(repo, resolved.target, content, seedPlan.ref, action.action === "update" ? sha : void 0));
|
|
19796
19928
|
applied.push(`${action.action} ${resolved.target}`);
|
|
19929
|
+
if (seedPlan.mode === "pr") seededToBranch++;
|
|
19930
|
+
}
|
|
19931
|
+
}
|
|
19932
|
+
let seedPrUrl;
|
|
19933
|
+
if (o.execute && seedPlan.mode === "pr" && seedPlan.branch && seededToBranch > 0) {
|
|
19934
|
+
await gh(["api", "-X", "PATCH", `repos/${repo}`, "-f", "allow_auto_merge=true", "-f", "allow_squash_merge=true", "-f", "delete_branch_on_merge=true"]).catch(() => {
|
|
19935
|
+
});
|
|
19936
|
+
const openPrs = await gh(["pr", "list", "--repo", repo, "--head", seedPlan.branch, "--base", baseBranch, "--state", "open", "--json", "number,url"]);
|
|
19937
|
+
const prDecision = decideFanoutPrAction(JSON.parse(openPrs.stdout || "[]"));
|
|
19938
|
+
if (prDecision.action === "reuse") {
|
|
19939
|
+
seedPrUrl = prDecision.url;
|
|
19940
|
+
} else {
|
|
19941
|
+
const created = await ghCreate([
|
|
19942
|
+
"pr",
|
|
19943
|
+
"create",
|
|
19944
|
+
"--repo",
|
|
19945
|
+
repo,
|
|
19946
|
+
"--base",
|
|
19947
|
+
baseBranch,
|
|
19948
|
+
"--head",
|
|
19949
|
+
seedPlan.branch,
|
|
19950
|
+
"--title",
|
|
19951
|
+
`bootstrap: seed ${parsedRepo.name} (${baseBranch} is protected)`,
|
|
19952
|
+
"--body",
|
|
19953
|
+
`Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#2286): \`${baseBranch}\` is protected (${seedPlan.reason}), so the org-managed seed files are delivered via this branch + PR \u2014 a direct contents PUT 409s on a protected base ("N of N required status checks are expected").`
|
|
19954
|
+
]);
|
|
19955
|
+
seedPrUrl = created.url;
|
|
19797
19956
|
}
|
|
19957
|
+
await gh(["pr", "merge", seedPrUrl, "--repo", repo, "--auto", "--squash"]).catch((e) => {
|
|
19958
|
+
if (!/already/i.test(String(e.message ?? ""))) throw e;
|
|
19959
|
+
});
|
|
19960
|
+
applied.push(`seed: PR ${seedPrUrl} (base ${baseBranch} protected; auto-merge enabled)`);
|
|
19798
19961
|
}
|
|
19799
19962
|
if (o.execute && o.class === "deployable") {
|
|
19800
19963
|
try {
|
|
@@ -19805,7 +19968,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19805
19968
|
}
|
|
19806
19969
|
const rulesetSeed = manifest.seeds.find((s) => s.target === ".github/rulesets/mmi-product-required-checks.json");
|
|
19807
19970
|
if (rulesetSeed) {
|
|
19808
|
-
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars,
|
|
19971
|
+
const rulesetContent = resolveSeedContent({ ...rulesetSeed, target: rulesetSeed.target.replace("{{REPO_SLUG}}", slug) }, vars, readFile2);
|
|
19809
19972
|
if (rulesetContent) {
|
|
19810
19973
|
try {
|
|
19811
19974
|
const activation = await activateProductRuleset(repo, stripRulesetComment(rulesetContent), defaultGitHubClient());
|
|
@@ -19918,9 +20081,9 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19918
20081
|
"--head",
|
|
19919
20082
|
branchName,
|
|
19920
20083
|
"--title",
|
|
19921
|
-
`bootstrap: register ${parsedRepo.name} for org-
|
|
20084
|
+
`bootstrap: register ${parsedRepo.name} for the org-managed .gitignore fanout`,
|
|
19922
20085
|
"--body",
|
|
19923
|
-
`Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#933): adds ${parsedRepo.name} to projects.json + .github/fanout-targets.json so the
|
|
20086
|
+
`Auto-opened by \`mmi-cli bootstrap apply --execute ${repo}\` (#933): adds ${parsedRepo.name} to projects.json + .github/fanout-targets.json so the org-managed .gitignore block reaches it (hub-v3 retired the whole-spine fanout).`
|
|
19924
20087
|
]);
|
|
19925
20088
|
fanoutPrUrl = created.url;
|
|
19926
20089
|
}
|
|
@@ -19933,7 +20096,7 @@ bootstrap.command("apply <repo>").description("idempotent seed apply from skills
|
|
|
19933
20096
|
return failGraceful(`bootstrap apply: fanout registration failed: ${e.message}`);
|
|
19934
20097
|
}
|
|
19935
20098
|
}
|
|
19936
|
-
if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
|
|
20099
|
+
if (o.json) console.log(JSON.stringify({ repo, class: o.class, execute: o.execute, seedDelivery: seedPlan.mode, seedPrUrl, actions, applied, ddbWrites, fanoutPrUrl }, null, 2));
|
|
19937
20100
|
else {
|
|
19938
20101
|
console.log(renderSeedPlan(actions));
|
|
19939
20102
|
if (o.execute) console.log(`
|
|
@@ -20000,13 +20163,12 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
|
|
|
20000
20163
|
));
|
|
20001
20164
|
program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
|
|
20002
20165
|
program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
|
|
20003
|
-
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process
|
|
20166
|
+
program2.command("session-start").description("run the SessionStart verbs (whoami, board slice, doctor) in one process").action(async () => {
|
|
20004
20167
|
if (isInsideRepoSubdir(process.cwd())) {
|
|
20005
20168
|
console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
|
|
20006
20169
|
return;
|
|
20007
20170
|
}
|
|
20008
20171
|
if (!await isOrgRepoRoot()) return;
|
|
20009
|
-
spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
|
|
20010
20172
|
spawnDeferredGcSweep();
|
|
20011
20173
|
const { parallel, sequential } = buildSessionStartPlan({
|
|
20012
20174
|
// whoami (#879): surface the resolved human so agents act --for them without asking. Silent
|
|
@@ -20022,7 +20184,7 @@ program2.command("session-start").description("run the SessionStart verbs (whoam
|
|
|
20022
20184
|
boardSlice: (io) => runBoardSlice(io, {
|
|
20023
20185
|
loadConfig: () => loadConfigForRepo(),
|
|
20024
20186
|
readBoard,
|
|
20025
|
-
// #1813: warm the slice cache out-of-band (detached
|
|
20187
|
+
// #1813: warm the slice cache out-of-band (detached) so the ~20s live read
|
|
20026
20188
|
// never costs banner time and next session's glance renders instantly within budget.
|
|
20027
20189
|
scheduleRefresh: () => spawnDetachedSelf(["board", "slice-refresh", "--quiet"], { spawn: import_node_child_process11.spawn, execPath: process.execPath, scriptPath: process.argv[1] })
|
|
20028
20190
|
}),
|