@mutmutco/cli 2.32.4 → 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 +139 -148
  2. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -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
@@ -10704,9 +10669,8 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
10704
10669
  if (releaseExists) {
10705
10670
  releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
10706
10671
  } else {
10707
- const tagCommit = clean2(await deps.run("git", ["rev-parse", `${tag}^{commit}`]));
10708
- await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", tagCommit, "--generate-notes", "--latest"]);
10709
- 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)`;
10710
10674
  if (deps.announce) {
10711
10675
  announceNote = (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note;
10712
10676
  }
@@ -11561,6 +11525,13 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
11561
11525
  ok: Boolean(config?.projectOwner && config?.projectNumber && config?.projectId && config?.statusFieldId && config?.statusOptions),
11562
11526
  label: "registry project board META exists"
11563
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
+ }
11564
11535
  if (config?.projectOwner && config.projectNumber != null) {
11565
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 } } } } } } }`;
11566
11537
  const fields = await (async () => {
@@ -12187,6 +12158,25 @@ function boardRegistryGaps(meta) {
12187
12158
  if (meta.projectNumber != null) return [];
12188
12159
  return ["projectNumber"];
12189
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
+ }
12190
12180
  function boardRegistryGapMessage(repo) {
12191
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)`;
12192
12182
  }
@@ -14404,6 +14394,9 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
14404
14394
  } catch (e) {
14405
14395
  return fail(e.message.replace(/^project set: /, "project set: "));
14406
14396
  }
14397
+ const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
14398
+ const boardError = boardLinkWriteError(patch, existing);
14399
+ if (boardError) return fail(`project set: ${boardError}`);
14407
14400
  const res = await upsertProject(slug, patch, registryClientDeps(cfg));
14408
14401
  return reportWrite("project set", res);
14409
14402
  });
@@ -14680,39 +14673,37 @@ verify.command("synthesize").description("merge lens JSON array into a PanelRepo
14680
14673
  return fail(`verify synthesize: ${e.message}`);
14681
14674
  }
14682
14675
  });
14683
- var fusion = verify.command("fusion").description("optional hosted fusion provider for grind verify (#1377)");
14684
- 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) => {
14685
- try {
14686
- const routing = parseFusionRouting(o.routing);
14687
- const lenses = parseFusionLenses(o.lenses);
14688
- const criteria = await (0, import_promises5.readFile)(o.criteriaFile, "utf8");
14689
- const diff = await (0, import_promises5.readFile)(o.diffFile, "utf8");
14690
- const plan2 = buildFusionPlan({
14691
- routing,
14692
- lenses,
14693
- criteria,
14694
- diff,
14695
- providerUrl: o.providerUrl ?? null,
14696
- toolPolicy: {
14697
- webSearch: Boolean(o.webSearch),
14698
- maxQueriesPerLens: 3,
14699
- denyDomains: [...DEFAULT_SEARCH_DENY_DOMAINS]
14700
- }
14701
- });
14702
- console.log(JSON.stringify(plan2));
14703
- } catch (e) {
14704
- 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}`);
14705
14696
  }
14706
14697
  });
14707
- 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) => {
14708
- try {
14709
- const raw = await (0, import_promises5.readFile)(o.planFile, "utf8");
14710
- const plan2 = JSON.parse(raw);
14711
- const result = await runFusionProvider(plan2, { providerUrl: o.providerUrl ?? plan2.provider ?? null });
14712
- console.log(JSON.stringify(result));
14713
- if (!result.ok) process.exitCode = 1;
14714
- } catch (e) {
14715
- 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(" ")}`);
14716
14707
  }
14717
14708
  });
14718
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) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.32.4",
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",