@mutmutco/cli 2.32.3 → 2.33.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.
Files changed (2) hide show
  1. package/dist/main.cjs +170 -165
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -3423,7 +3423,7 @@ function resolveRulesBase(orgRulesSource, defaultBase) {
3423
3423
  }
3424
3424
 
3425
3425
  // src/index.ts
3426
- var import_node_child_process9 = require("node:child_process");
3426
+ var import_node_child_process10 = require("node:child_process");
3427
3427
 
3428
3428
  // src/cli-shared.ts
3429
3429
  var import_promises = require("node:fs/promises");
@@ -7229,7 +7229,7 @@ ${buildReportBody(body, sourceRepo)}`;
7229
7229
 
7230
7230
  // src/skill-lesson.ts
7231
7231
  var SKILL_LESSON_LABEL = "skill-lesson";
7232
- var SKILL_NAMES = ["bootstrap", "browser-automation", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
7232
+ var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
7233
7233
  function assertSkillName(name) {
7234
7234
  const match = SKILL_NAMES.find((skill) => skill === name);
7235
7235
  if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
@@ -7355,127 +7355,92 @@ function buildPanelPlan(input) {
7355
7355
  };
7356
7356
  }
7357
7357
 
7358
- // src/grind-policy.ts
7359
- var DEFAULT_SEARCH_DENY_DOMAINS = [
7360
- "stackoverflow.com",
7361
- "stackexchange.com",
7362
- "github.com/issues",
7363
- "reddit.com"
7364
- ];
7365
-
7366
- // src/verify-fusion.ts
7367
- var DEFAULT_MODELS = {
7368
- builder: "builder-slot",
7369
- verifier: "verifier-slot",
7370
- third: "third-slot",
7371
- synthesizer: "verifier-slot"
7358
+ // src/build-policy.ts
7359
+ var TIER_BUDGETS = {
7360
+ light: {
7361
+ tier: "light",
7362
+ agents: 2,
7363
+ planners: 0,
7364
+ parallelSitesCap: 1,
7365
+ verificationDepth: "panel-once",
7366
+ reasoningEffort: "standard"
7367
+ },
7368
+ standard: {
7369
+ tier: "standard",
7370
+ agents: 3,
7371
+ planners: 0,
7372
+ parallelSitesCap: 2,
7373
+ verificationDepth: "full-panel",
7374
+ reasoningEffort: "standard-high"
7375
+ },
7376
+ deep: {
7377
+ tier: "deep",
7378
+ agents: 4,
7379
+ planners: 2,
7380
+ parallelSitesCap: 3,
7381
+ verificationDepth: "full-double-pass",
7382
+ reasoningEffort: "high"
7383
+ },
7384
+ max: {
7385
+ tier: "max",
7386
+ agents: 5,
7387
+ planners: 3,
7388
+ parallelSitesCap: 4,
7389
+ verificationDepth: "integration-checkpoints",
7390
+ reasoningEffort: "max"
7391
+ }
7372
7392
  };
7373
- function resolveFusionProviderUrl(explicit) {
7374
- if (explicit) return explicit;
7375
- const env = process.env.MMI_FUSION_PROVIDER_URL?.trim();
7376
- return env || null;
7377
- }
7378
- function buildFusionPlan(input) {
7379
- const routing = input.routing;
7380
- const lenses = input.lenses ?? [...GRIND_LENSES];
7381
- const provider = resolveFusionProviderUrl(input.providerUrl ?? void 0);
7382
- const models = {
7383
- ...DEFAULT_MODELS,
7384
- ...input.models,
7385
- synthesizer: input.models?.synthesizer ?? input.models?.third ?? input.models?.verifier ?? DEFAULT_MODELS.synthesizer
7386
- };
7387
- if (models.verifier === models.builder) {
7388
- throw new Error("fusion plan: verifier must not equal builder");
7393
+ function getTierBudgets(tier) {
7394
+ return TIER_BUDGETS[tier];
7395
+ }
7396
+ function isWideOrArchitectural(scope) {
7397
+ return scope === "wide" || scope === "architectural";
7398
+ }
7399
+ function isCrossModuleOrProduct(blast) {
7400
+ return blast === "cross-module" || blast === "product";
7401
+ }
7402
+ function pickEffortTier(signals) {
7403
+ if (signals.explicitTier) {
7404
+ return {
7405
+ tier: signals.explicitTier,
7406
+ reason: `explicit user override \u2192 ${signals.explicitTier}`,
7407
+ budgets: getTierBudgets(signals.explicitTier)
7408
+ };
7389
7409
  }
7390
- if (models.synthesizer === models.builder) {
7391
- throw new Error("fusion plan: synthesizer must not equal builder");
7410
+ if (signals.risk === "critical") {
7411
+ return { tier: "max", reason: "critical risk", budgets: getTierBudgets("max") };
7392
7412
  }
7393
- const toolPolicy = {
7394
- webSearch: Boolean(input.toolPolicy?.webSearch),
7395
- maxQueriesPerLens: input.toolPolicy?.maxQueriesPerLens ?? 3,
7396
- denyDomains: input.toolPolicy?.denyDomains ?? []
7397
- };
7398
- return {
7399
- provider,
7400
- routing,
7401
- lenses,
7402
- models,
7403
- toolPolicy,
7404
- criteria: input.criteria,
7405
- diff: input.diff,
7406
- fallback: "host-panel",
7407
- instructions: "Hosted fusion when provider is configured; else spawn host lenses and pipe JSON to `mmi-cli verify synthesize`. Synthesizer slot must differ from builder."
7408
- };
7409
- }
7410
- function adaptFusionResponse(raw) {
7411
- if (raw.lenses) {
7412
- const lenses = parseLensResults(raw.lenses);
7413
- const base2 = synthesizePanelReport(lenses);
7413
+ if (signals.foundational && (isWideOrArchitectural(signals.scope) || signals.blastRadius === "product")) {
7414
7414
  return {
7415
- ...base2,
7416
- consensus: raw.consensus?.length ? raw.consensus : base2.consensus,
7417
- contradictions: raw.contradictions ?? base2.contradictions,
7418
- partial_coverage: raw.partial_coverage ?? base2.partial_coverage,
7419
- unique_insights: raw.unique_insights ?? base2.unique_insights,
7420
- blind_spots: raw.blind_spots ?? base2.blind_spots,
7421
- nits: raw.nits ?? base2.nits
7415
+ tier: "max",
7416
+ reason: "foundational seam with wide/architectural scope or product blast radius",
7417
+ budgets: getTierBudgets("max")
7422
7418
  };
7423
7419
  }
7424
- const blockers = (raw.blockers ?? []).map((b, i) => ({
7425
- id: `fusion-${i}`,
7426
- title: b.title,
7427
- file: b.file,
7428
- line: b.line,
7429
- why: b.why,
7430
- sources: b.sources ?? ["hosted-fusion"]
7431
- }));
7432
- return {
7433
- consensus: raw.consensus ?? [],
7434
- contradictions: raw.contradictions ?? [],
7435
- partial_coverage: raw.partial_coverage ?? [],
7436
- unique_insights: raw.unique_insights ?? [],
7437
- blind_spots: raw.blind_spots ?? [],
7438
- blockers,
7439
- nits: raw.nits ?? []
7440
- };
7441
- }
7442
- async function runFusionProvider(plan2, deps = {}) {
7443
- const url = resolveFusionProviderUrl(deps.providerUrl ?? plan2.provider ?? void 0);
7444
- if (!url) {
7445
- return { ok: false, source: "fallback", error: "no fusion provider configured" };
7420
+ if (signals.risk === "high") {
7421
+ return { tier: "deep", reason: "high risk", budgets: getTierBudgets("deep") };
7446
7422
  }
7447
- const fetchImpl = deps.fetch ?? fetch;
7448
- const apiKey = deps.apiKey ?? process.env.MMI_FUSION_API_KEY?.trim();
7449
- const headers = { "content-type": "application/json" };
7450
- if (apiKey) headers.authorization = `Bearer ${apiKey}`;
7451
- try {
7452
- const res = await fetchImpl(url, {
7453
- method: "POST",
7454
- headers,
7455
- body: JSON.stringify({
7456
- routing: plan2.routing,
7457
- lenses: plan2.lenses,
7458
- models: plan2.models,
7459
- toolPolicy: plan2.toolPolicy,
7460
- criteria: plan2.criteria,
7461
- diff: plan2.diff
7462
- }),
7463
- signal: AbortSignal.timeout(3e4)
7464
- });
7465
- if (!res.ok) {
7466
- return { ok: false, source: "fallback", error: `provider HTTP ${res.status}` };
7467
- }
7468
- const body = await res.json();
7469
- return { ok: true, source: "hosted-fusion", report: adaptFusionResponse(body) };
7470
- } catch (e) {
7471
- return { ok: false, source: "fallback", error: e.message };
7423
+ if (signals.ambiguity === "high") {
7424
+ return { tier: "deep", reason: "high ambiguity", budgets: getTierBudgets("deep") };
7472
7425
  }
7473
- }
7474
- function parseFusionLenses(raw) {
7475
- return raw.split(",").map((s) => assertGrindLens(s.trim()));
7476
- }
7477
- function parseFusionRouting(raw) {
7478
- return assertVerifyRouting(raw);
7426
+ if (signals.scope === "architectural") {
7427
+ return { tier: "deep", reason: "architectural scope", budgets: getTierBudgets("deep") };
7428
+ }
7429
+ if (isCrossModuleOrProduct(signals.blastRadius)) {
7430
+ return {
7431
+ tier: "deep",
7432
+ reason: `${signals.blastRadius} blast radius`,
7433
+ budgets: getTierBudgets("deep")
7434
+ };
7435
+ }
7436
+ if (signals.scope === "trivial" && signals.ambiguity === "low" && signals.risk === "low") {
7437
+ return {
7438
+ tier: "light",
7439
+ reason: "trivial scope with low risk and low ambiguity",
7440
+ budgets: getTierBudgets("light")
7441
+ };
7442
+ }
7443
+ return { tier: "standard", reason: "default \u2014 normal slice", budgets: getTierBudgets("standard") };
7479
7444
  }
7480
7445
 
7481
7446
  // src/gc.ts
@@ -8248,15 +8213,24 @@ function decideStage(inputs) {
8248
8213
  }
8249
8214
 
8250
8215
  // src/cursor-plugin-seed.ts
8216
+ var import_node_child_process7 = require("node:child_process");
8251
8217
  var import_node_fs11 = require("node:fs");
8252
8218
  var import_node_os4 = require("node:os");
8253
8219
  var import_node_path11 = require("node:path");
8220
+ var import_node_util6 = require("node:util");
8254
8221
  function isSemverVersion(v) {
8255
8222
  return typeof v === "string" && /^v?\d+\.\d+\.\d+/.test(v.trim());
8256
8223
  }
8257
8224
  var MMI_HUB_REPO = "mutmutco/MMI-Hub";
8258
8225
  var CURSOR_THIRD_PARTY_STATE_KEY = "cursor/thirdPartyExtensibilityEnabled";
8259
8226
  var PLUGIN_JSON_REL = ".cursor-plugin/plugin.json";
8227
+ var execFileBuffer = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
8228
+ function gitFetchReleaseTagArgs(hubCheckout, tag) {
8229
+ return ["-C", hubCheckout, "fetch", "origin", "tag", tag, "--quiet"];
8230
+ }
8231
+ function ghReleaseTarballApiArgs(tag) {
8232
+ return ["api", `repos/${MMI_HUB_REPO}/tarball/refs/tags/${tag}`];
8233
+ }
8260
8234
  function cursorUserGlobalStatePath() {
8261
8235
  if (process.platform === "win32") {
8262
8236
  const base2 = process.env.APPDATA || (0, import_node_path11.join)((0, import_node_os4.homedir)(), "AppData", "Roaming");
@@ -8295,7 +8269,7 @@ function releaseTag(releasedVersion) {
8295
8269
  async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5) {
8296
8270
  const tarFile = (0, import_node_path11.join)(tmpRoot, "archive.tar");
8297
8271
  try {
8298
- await execFileP5("git", ["-C", hubCheckout, "fetch", "origin", `tag ${tag}`, "--quiet"], { timeout: 6e4 });
8272
+ await execFileP5("git", gitFetchReleaseTagArgs(hubCheckout, tag), { timeout: 6e4 });
8299
8273
  await execFileP5("git", ["-C", hubCheckout, "archive", "--format=tar", `--output=${tarFile}`, tag, "plugins/mmi"], {
8300
8274
  timeout: 6e4
8301
8275
  });
@@ -8306,13 +8280,18 @@ async function extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFi
8306
8280
  return void 0;
8307
8281
  }
8308
8282
  }
8309
- async function downloadPluginMmiViaGh(tag, tmpRoot, execFileP5) {
8283
+ async function downloadPluginMmiViaGh(tag, tmpRoot) {
8310
8284
  const tarPath = (0, import_node_path11.join)(tmpRoot, "repo.tgz");
8311
8285
  try {
8312
- await execFileP5("gh", ["api", `repos/${MMI_HUB_REPO}/tarball/refs/tags/${tag}`, "--output", tarPath], {
8313
- timeout: 12e4
8286
+ (0, import_node_fs11.mkdirSync)(tmpRoot, { recursive: true });
8287
+ const { stdout } = await execFileBuffer("gh", ghReleaseTarballApiArgs(tag), {
8288
+ timeout: 12e4,
8289
+ maxBuffer: 100 * 1024 * 1024,
8290
+ encoding: "buffer",
8291
+ windowsHide: true
8314
8292
  });
8315
- await execFileP5("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4 });
8293
+ (0, import_node_fs11.writeFileSync)(tarPath, stdout);
8294
+ await execFileBuffer("tar", ["-xzf", tarPath, "-C", tmpRoot], { timeout: 12e4, windowsHide: true });
8316
8295
  const top = (0, import_node_fs11.readdirSync)(tmpRoot).find((entry) => entry !== "repo.tgz");
8317
8296
  if (!top) return void 0;
8318
8297
  const pluginMmi = (0, import_node_path11.join)(tmpRoot, top, "plugins", "mmi");
@@ -8328,7 +8307,7 @@ async function resolvePluginMmiSource(releasedVersion, hubCheckout, tmpRoot, exe
8328
8307
  const fromHub = await extractPluginMmiFromHubCheckout(hubCheckout, tag, tmpRoot, execFileP5);
8329
8308
  if (fromHub) return fromHub;
8330
8309
  }
8331
- return downloadPluginMmiViaGh(tag, (0, import_node_path11.join)(tmpRoot, "gh"), execFileP5);
8310
+ return downloadPluginMmiViaGh(tag, (0, import_node_path11.join)(tmpRoot, "gh"));
8332
8311
  }
8333
8312
  function cursorPluginPinsNeedingSeed(pins, releasedVersion) {
8334
8313
  if (!isSemverVersion(releasedVersion)) return pins.filter((pin) => !pin.hasPluginJson || !pin.hasHooksJson || pin.isEmpty);
@@ -9054,12 +9033,12 @@ async function runStageLiveDown(deps, t) {
9054
9033
  }
9055
9034
 
9056
9035
  // src/stage-runner.ts
9057
- var import_node_child_process7 = require("node:child_process");
9036
+ var import_node_child_process8 = require("node:child_process");
9058
9037
  var import_node_fs12 = require("node:fs");
9059
9038
  var import_node_path12 = require("node:path");
9060
9039
  var import_node_net2 = require("node:net");
9061
- var import_node_util6 = require("node:util");
9062
- var execFileP4 = (0, import_node_util6.promisify)(import_node_child_process7.execFile);
9040
+ var import_node_util7 = require("node:util");
9041
+ var execFileP4 = (0, import_node_util7.promisify)(import_node_child_process8.execFile);
9063
9042
  var EARLY_EXIT_GRACE_MS = 2e3;
9064
9043
  function waitForProcessStability(child, graceMs = EARLY_EXIT_GRACE_MS) {
9065
9044
  return new Promise((resolve, reject) => {
@@ -9255,7 +9234,7 @@ async function startStage(config = {}, opts = {}) {
9255
9234
  const extraEnv = {};
9256
9235
  for (const [k, v] of Object.entries(config.env ?? {})) extraEnv[k] = sub(v) ?? v;
9257
9236
  const up = sub(config.up.trim());
9258
- const child = (0, import_node_child_process7.spawn)(up, {
9237
+ const child = (0, import_node_child_process8.spawn)(up, {
9259
9238
  cwd,
9260
9239
  shell: true,
9261
9240
  // POSIX-only: the process group exists for the group-kill in stopStage. On win32 teardown is
@@ -10346,12 +10325,12 @@ async function runTenantRedeploy(deps, options) {
10346
10325
  }
10347
10326
 
10348
10327
  // src/hotfix-coverage.ts
10349
- var import_node_child_process8 = require("node:child_process");
10328
+ var import_node_child_process9 = require("node:child_process");
10350
10329
  var CHERRY_TRAILER = /\(cherry picked from commit ([0-9a-f]{7,40})\)/g;
10351
10330
  function checkHotfixCoverage(options = {}) {
10352
10331
  const { cwd = process.cwd(), mainRef = "origin/main", rcRef = "origin/rc", manifestPaths = [] } = options;
10353
10332
  const ack = (options.ack ?? []).filter(Boolean);
10354
- const git = options.git ?? ((args, opts) => (0, import_node_child_process8.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
10333
+ const git = options.git ?? ((args, opts) => (0, import_node_child_process9.execFileSync)("git", args, { cwd, encoding: "utf8", input: opts?.input, stdio: ["pipe", "pipe", "pipe"] }));
10355
10334
  const revList = (range) => {
10356
10335
  const out = git(["rev-list", "--no-merges", range]).trim();
10357
10336
  return out ? out.split("\n") : [];
@@ -10690,9 +10669,8 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
10690
10669
  if (releaseExists) {
10691
10670
  releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
10692
10671
  } else {
10693
- const tagCommit = clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
10694
- await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", tagCommit, "--generate-notes", "--latest"]);
10695
- releaseNote = `Release ${tag} created (target ${tagCommit.slice(0, 7)})`;
10672
+ await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", "main", "--generate-notes", "--latest"]);
10673
+ releaseNote = `Release ${tag} created (target main)`;
10696
10674
  if (deps.announce) {
10697
10675
  announceNote = (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note;
10698
10676
  }
@@ -11547,6 +11525,13 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
11547
11525
  ok: Boolean(config?.projectOwner && config?.projectNumber && config?.projectId && config?.statusFieldId && config?.statusOptions),
11548
11526
  label: "registry project board META exists"
11549
11527
  });
11528
+ if (config?.projectId && config.projectNumber == null) {
11529
+ checks.push({
11530
+ ok: false,
11531
+ label: "registry projectNumber present when projectId set",
11532
+ detail: "projectNumber is missing \u2014 bootstrap apply must pass PROJECT_NUMBER or derive it from the live board GraphQL query"
11533
+ });
11534
+ }
11550
11535
  if (config?.projectOwner && config.projectNumber != null) {
11551
11536
  const fieldsQuery = `query($login: String!, $number: Int!) { organization(login: $login) { projectV2(number: $number) { fields(first: 50) { nodes { ... on ProjectV2FieldCommon { id name } ... on ProjectV2SingleSelectField { id name options { id name } } } } } } }`;
11552
11537
  const fields = await (async () => {
@@ -12173,6 +12158,25 @@ function boardRegistryGaps(meta) {
12173
12158
  if (meta.projectNumber != null) return [];
12174
12159
  return ["projectNumber"];
12175
12160
  }
12161
+ function previewRegistryMetaMerge(existing, patch) {
12162
+ const out = { ...existing ?? {} };
12163
+ for (const [key, value] of Object.entries(patch)) {
12164
+ if (value === null) delete out[key];
12165
+ else out[key] = value;
12166
+ }
12167
+ return out;
12168
+ }
12169
+ function boardLinkWriteError(patch, existing) {
12170
+ const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
12171
+ const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
12172
+ if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
12173
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
12174
+ }
12175
+ if (patch.projectNumber === null && boardRegistryGaps(previewRegistryMetaMerge(existing, patch)).length) {
12176
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
12177
+ }
12178
+ return null;
12179
+ }
12176
12180
  function boardRegistryGapMessage(repo) {
12177
12181
  return `Board META incomplete for ${repo}: registry has projectId but no projectNumber \u2014 board claim and auto-add will fail until projectNumber is backfilled (re-run \`node infra/migrate/seed-registry.mjs\` or \`mmi-cli bootstrap apply --execute\` with board vars)`;
12178
12182
  }
@@ -13866,7 +13870,7 @@ function scheduleRelatedDiscovery(o) {
13866
13870
  try {
13867
13871
  const args = ["issue", "discover-related", "--number", String(o.number), "--title", o.title, "--body", o.body];
13868
13872
  if (o.repo) args.push("--repo", o.repo);
13869
- (0, import_node_child_process9.spawn)(process.execPath, [process.argv[1], ...args], {
13873
+ (0, import_node_child_process10.spawn)(process.execPath, [process.argv[1], ...args], {
13870
13874
  detached: true,
13871
13875
  stdio: "ignore",
13872
13876
  windowsHide: true,
@@ -13880,7 +13884,7 @@ function detachPlanSync() {
13880
13884
  if (planSyncDetached) return;
13881
13885
  planSyncDetached = true;
13882
13886
  try {
13883
- (0, import_node_child_process9.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
13887
+ (0, import_node_child_process10.spawn)(process.execPath, [process.argv[1], "northstar", "sync", "--quiet"], {
13884
13888
  detached: true,
13885
13889
  stdio: "ignore",
13886
13890
  windowsHide: true,
@@ -13959,7 +13963,7 @@ function openInEditor(path2) {
13959
13963
  return;
13960
13964
  }
13961
13965
  try {
13962
- (0, import_node_child_process9.spawn)(editor, [path2], { stdio: "inherit" });
13966
+ (0, import_node_child_process10.spawn)(editor, [path2], { stdio: "inherit" });
13963
13967
  } catch {
13964
13968
  console.log(`open ${path2} manually`);
13965
13969
  }
@@ -14390,6 +14394,9 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
14390
14394
  } catch (e) {
14391
14395
  return fail(e.message.replace(/^project set: /, "project set: "));
14392
14396
  }
14397
+ const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
14398
+ const boardError = boardLinkWriteError(patch, existing);
14399
+ if (boardError) return fail(`project set: ${boardError}`);
14393
14400
  const res = await upsertProject(slug, patch, registryClientDeps(cfg));
14394
14401
  return reportWrite("project set", res);
14395
14402
  });
@@ -14666,39 +14673,37 @@ verify.command("synthesize").description("merge lens JSON array into a PanelRepo
14666
14673
  return fail(`verify synthesize: ${e.message}`);
14667
14674
  }
14668
14675
  });
14669
- var fusion = verify.command("fusion").description("optional hosted fusion provider for grind verify (#1377)");
14670
- fusion.command("plan").description("plan a hosted fusion job \u2014 print FusionPlan JSON (falls back to host panel when provider unset)").requiredOption("--criteria-file <path>", "UTF-8 file with success criteria").requiredOption("--diff-file <path>", "UTF-8 file with git diff output").option("--routing <routing>", "Balanced | Budget | Paranoid", "Balanced").option("--lenses <list>", `comma-separated lens names (default: ${GRIND_LENSES.join(",")})`, GRIND_LENSES.join(",")).option("--provider-url <url>", "fusion provider base URL (else MMI_FUSION_PROVIDER_URL)").option("--web-search", "enable bounded web search in fusion tool policy").action(async (o) => {
14671
- try {
14672
- const routing = parseFusionRouting(o.routing);
14673
- const lenses = parseFusionLenses(o.lenses);
14674
- const criteria = await (0, import_promises5.readFile)(o.criteriaFile, "utf8");
14675
- const diff = await (0, import_promises5.readFile)(o.diffFile, "utf8");
14676
- const plan2 = buildFusionPlan({
14677
- routing,
14678
- lenses,
14679
- criteria,
14680
- diff,
14681
- providerUrl: o.providerUrl ?? null,
14682
- toolPolicy: {
14683
- webSearch: Boolean(o.webSearch),
14684
- maxQueriesPerLens: 3,
14685
- denyDomains: [...DEFAULT_SEARCH_DENY_DOMAINS]
14686
- }
14687
- });
14688
- console.log(JSON.stringify(plan2));
14689
- } catch (e) {
14690
- return fail(`verify fusion plan: ${e.message}`);
14676
+ var build = program2.command("build").description("Build skill helpers \u2014 effort-tier selection and milestone partitioning");
14677
+ build.command("tier").description("Recommend an effort tier (light|standard|deep|max) from scope/risk/ambiguity signals").option("--scope <s>", "trivial|narrow|normal|wide|architectural").option("--risk <r>", "low|medium|high|critical").option("--ambiguity <a>", "low|medium|high").option("--blast-radius <b>", "isolated|module|cross-module|product").option("--foundational", "touches shared seams").option("--explicit <t>", "force a tier: light|standard|deep|max").option("--json", "output JSON").action((opts) => {
14678
+ const signals = {
14679
+ scope: opts.scope,
14680
+ risk: opts.risk,
14681
+ ambiguity: opts.ambiguity,
14682
+ blastRadius: opts.blastRadius,
14683
+ foundational: opts.foundational ?? void 0,
14684
+ explicitTier: opts.explicit
14685
+ };
14686
+ const decision = pickEffortTier(signals);
14687
+ if (opts.json) console.log(JSON.stringify(decision, null, 2));
14688
+ else {
14689
+ console.log(`tier: ${decision.tier}`);
14690
+ console.log(`reason: ${decision.reason}`);
14691
+ console.log(`agents: ${decision.budgets.agents}`);
14692
+ console.log(`planners: ${decision.budgets.planners}`);
14693
+ console.log(`parallel: ${decision.budgets.parallelSitesCap} sites`);
14694
+ console.log(`verify: ${decision.budgets.verificationDepth}`);
14695
+ console.log(`reasoning: ${decision.budgets.reasoningEffort}`);
14691
14696
  }
14692
14697
  });
14693
- fusion.command("run").description("execute hosted fusion from a FusionPlan JSON file; prints PanelReport or fallback envelope").requiredOption("--plan-file <path>", "UTF-8 FusionPlan JSON from verify fusion plan").option("--provider-url <url>", "override fusion provider URL (else plan.provider or MMI_FUSION_PROVIDER_URL)").action(async (o) => {
14694
- try {
14695
- const raw = await (0, import_promises5.readFile)(o.planFile, "utf8");
14696
- const plan2 = JSON.parse(raw);
14697
- const result = await runFusionProvider(plan2, { providerUrl: o.providerUrl ?? plan2.provider ?? null });
14698
- console.log(JSON.stringify(result));
14699
- if (!result.ok) process.exitCode = 1;
14700
- } catch (e) {
14701
- return fail(`verify fusion run: ${e.message}`);
14698
+ build.command("plan").description("Partition a set of issue refs into sites + waves (parallel/serialize/batch) for milestone construction").argument("[issues...]", "issue refs like owner/repo#N or #N").option("--json", "output JSON").action((issues, opts) => {
14699
+ const plan2 = {
14700
+ waves: issues.length > 0 ? [{ wave: 0, mode: "parallel", issues }] : [],
14701
+ note: issues.length === 0 ? "No issues provided." : "v1 partitioning: single parallel wave. Use grind --auto Phase 00 patterns for deeper analysis."
14702
+ };
14703
+ if (opts.json) console.log(JSON.stringify(plan2, null, 2));
14704
+ else {
14705
+ console.log(plan2.note);
14706
+ for (const w of plan2.waves) console.log(`wave ${w.wave} (${w.mode}): ${w.issues.join(" ")}`);
14702
14707
  }
14703
14708
  });
14704
14709
  program2.command("skill-lesson").description("file a skill-lesson on the Hub board (GitHub auth, dedups open lessons) and print {number,url} JSON").requiredOption("--skill <name>", `which skill misfired (${SKILL_NAMES.join(" | ")})`).option("--title <title>", "one-line summary of what misfired").option("--title-file <path|->", "read the one-line summary from a UTF-8 file, or from stdin with -").option("--body <body>", "lesson body: what misfired, the evidence, and the proposed amendment (markdown)").option("--body-file <path|->", "read the lesson body from a UTF-8 file, or from stdin with -").option("--priority <priority>", "urgent | high | medium | low (board Priority field when configured)", "medium").option("--repo <owner/repo>", `target repo (defaults to the org Hub: ${HUB_REPO})`).option("--force", "file a new issue even when an open lesson looks like a duplicate").option("--json", "machine-readable output (already the default \u2014 skill-lesson always prints JSON)").action(async (o) => {
@@ -16166,7 +16171,7 @@ program2.command("session-start").description("run the SessionStart verbs (rules
16166
16171
  } catch (e) {
16167
16172
  console.error(`[mmi-hook] saga session failed: ${e.message}`);
16168
16173
  }
16169
- spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process9.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
16174
+ spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process10.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
16170
16175
  let northstarInjected = false;
16171
16176
  const { parallel, sequential } = buildSessionStartPlan({
16172
16177
  rulesSync: (io) => runRulesSync({ quiet: true }, io),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.32.3",
3
+ "version": "2.33.0",
4
4
  "description": "MMI Future CLI — delivers the org rules (whole-file), plus saga and KB access. The cross-IDE engine the plugin's SessionStart hook drives.",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",