@mutmutco/cli 2.32.4 → 2.34.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 (3) hide show
  1. package/dist/main.cjs +780 -153
  2. package/dist/saga.cjs +420 -0
  3. package/package.json +1 -1
package/dist/main.cjs CHANGED
@@ -4450,6 +4450,309 @@ function formatCaptureFailure(status, message) {
4450
4450
  return `saga: HTTP ${status}`;
4451
4451
  }
4452
4452
 
4453
+ // src/saga-snapshot.ts
4454
+ var GS_OPEN = "gs-open:";
4455
+ var GS_RESOLVED = "gs-resolved:";
4456
+ var GS_CEILING = "gs-ceiling:";
4457
+ var BS_DONE = "bs-done:";
4458
+ var BS_PROGRESS = "bs-progress:";
4459
+ var BS_BLOCKED = "bs-blocked:";
4460
+ var BS_TIER = "bs-tier:";
4461
+ var BS_CEILING = "bs-ceiling:";
4462
+ var BS_HALT = "bs-halt:";
4463
+ var GRIND_PREFIX = /^grind\s+/i;
4464
+ var META_ACTION_SEP = " | ";
4465
+ var LEGACY_BLOCKER_OPEN = /^blocker:(.+)$/i;
4466
+ var LEGACY_BLOCKER_RESOLVED = /^resolved:(.+)$/i;
4467
+ var LEGACY_CEILING = /^ceiling:(.+)$/i;
4468
+ var LEGACY_BUILD_DONE = /^done:(.+)$/i;
4469
+ var LEGACY_BUILD_PROGRESS = /^progress:(.+)$/i;
4470
+ var LEGACY_BUILD_BLOCKED = /^blocked:([^|]+)\|([^|]+)(?:\|(.+))?$/i;
4471
+ var LEGACY_BUILD_TIER = /^tier:([^|]+)\|([^|]+)(?:\|(.+))?$/i;
4472
+ var LEGACY_BUILD_CEILING = /^ceiling:([^|]+)\|(.+)$/i;
4473
+ var LEGACY_BUILD_HALT = /^halt:(.+)$/i;
4474
+ function isGrindSnapshotItem(text) {
4475
+ return text.startsWith(GS_OPEN) || text.startsWith(GS_RESOLVED) || text.startsWith(GS_CEILING) || LEGACY_BLOCKER_OPEN.test(text) || LEGACY_BLOCKER_RESOLVED.test(text) || LEGACY_CEILING.test(text);
4476
+ }
4477
+ function isBuildSnapshotItem(text) {
4478
+ return text.startsWith(BS_DONE) || text.startsWith(BS_PROGRESS) || text.startsWith(BS_BLOCKED) || text.startsWith(BS_TIER) || text.startsWith(BS_CEILING) || text.startsWith(BS_HALT) || LEGACY_BUILD_DONE.test(text) || LEGACY_BUILD_PROGRESS.test(text) || LEGACY_BUILD_BLOCKED.test(text) || LEGACY_BUILD_TIER.test(text) || LEGACY_BUILD_CEILING.test(text) || LEGACY_BUILD_HALT.test(text);
4479
+ }
4480
+ function snapshotClearIndices(kind, head) {
4481
+ const pred = kind === "grind" ? isGrindSnapshotItem : isBuildSnapshotItem;
4482
+ return (head.queued ?? []).map((item, index) => ({ item, index })).filter(({ item }) => !item.done && pred(item.text)).map(({ index }) => index);
4483
+ }
4484
+ function parseGrindNext(next) {
4485
+ if (!next?.trim()) return {};
4486
+ const raw = next.replace(GRIND_PREFIX, "").trim();
4487
+ const pipe = raw.indexOf(META_ACTION_SEP);
4488
+ const meta = pipe >= 0 ? raw.slice(0, pipe).trim() : raw;
4489
+ const nextAction = pipe >= 0 ? raw.slice(pipe + META_ACTION_SEP.length).trim() : void 0;
4490
+ const out = { nextAction };
4491
+ for (const part of meta.split(/\s+/)) {
4492
+ const eq = part.indexOf("=");
4493
+ if (eq < 0) continue;
4494
+ const k = part.slice(0, eq);
4495
+ const v = part.slice(eq + 1);
4496
+ if (k === "class") out.class = v;
4497
+ else if (k === "routing") out.routing = v;
4498
+ else if (k === "ultra") out.ultra = v;
4499
+ else if (k === "phase") out.phase = v;
4500
+ else if (k === "verify") {
4501
+ const [r, c] = v.split("/");
4502
+ const rn = Number(r);
4503
+ const cn = Number(c);
4504
+ if (Number.isFinite(rn)) out.verifyRound = rn;
4505
+ if (Number.isFinite(cn)) out.verifyCap = cn;
4506
+ } else if (k === "resolved" && v) {
4507
+ out.resolvedBlockerIds = v.split(",").map((s) => s.trim()).filter(Boolean);
4508
+ }
4509
+ }
4510
+ return out;
4511
+ }
4512
+ function formatGrindNext(s) {
4513
+ const parts = ["grind"];
4514
+ if (s.class) parts.push(`class=${s.class}`);
4515
+ if (s.routing) parts.push(`routing=${s.routing}`);
4516
+ if (s.ultra) parts.push(`ultra=${s.ultra}`);
4517
+ if (s.phase) parts.push(`phase=${s.phase}`);
4518
+ if (Number.isFinite(s.verifyRound) && Number.isFinite(s.verifyCap)) parts.push(`verify=${s.verifyRound}/${s.verifyCap}`);
4519
+ if (s.resolvedBlockerIds.length) parts.push(`resolved=${s.resolvedBlockerIds.join(",")}`);
4520
+ const meta = parts.join(" ");
4521
+ return s.nextAction ? `${meta}${META_ACTION_SEP}${s.nextAction}` : meta;
4522
+ }
4523
+ function parseGrindQueueItem(item, open, resolved, ceiling) {
4524
+ if (item.text.startsWith(GS_OPEN) && !item.done) open.push(item.text.slice(GS_OPEN.length).trim());
4525
+ else if (item.text.startsWith(GS_RESOLVED) && !item.done) resolved.push(item.text.slice(GS_RESOLVED.length).trim());
4526
+ else if (item.text.startsWith(GS_CEILING) && !item.done) ceiling.value = item.text.slice(GS_CEILING.length).trim();
4527
+ else {
4528
+ const legacyOpen = item.text.match(LEGACY_BLOCKER_OPEN);
4529
+ if (legacyOpen && !item.done) open.push(legacyOpen[1].trim());
4530
+ const legacyResolved = item.text.match(LEGACY_BLOCKER_RESOLVED);
4531
+ if (legacyResolved && item.done) resolved.push(legacyResolved[1].trim());
4532
+ const legacyCeil = item.text.match(LEGACY_CEILING);
4533
+ if (legacyCeil && !item.done) ceiling.value = legacyCeil[1].trim();
4534
+ }
4535
+ }
4536
+ function parseGrindSnapshot(head) {
4537
+ const fromNext = parseGrindNext(head.next);
4538
+ const openBlockerIds = [];
4539
+ const resolvedFromQueue = [];
4540
+ const ceiling = { value: void 0 };
4541
+ for (const item of head.queued ?? []) {
4542
+ parseGrindQueueItem(item, openBlockerIds, resolvedFromQueue, ceiling);
4543
+ }
4544
+ const resolvedBlockerIds = [.../* @__PURE__ */ new Set([...resolvedFromQueue, ...fromNext.resolvedBlockerIds ?? []])];
4545
+ const { resolvedBlockerIds: _drop, ...restNext } = fromNext;
4546
+ return {
4547
+ kind: "grind",
4548
+ criteria: head.anchor?.intent,
4549
+ openBlockerIds,
4550
+ resolvedBlockerIds,
4551
+ verificationCeiling: ceiling.value,
4552
+ ...restNext
4553
+ };
4554
+ }
4555
+ function parseBuildSnapshot(head) {
4556
+ const doneLastTurn = [];
4557
+ const inProgress = [];
4558
+ const blocked = [];
4559
+ const tierLedger = [];
4560
+ const verificationCeiling = [];
4561
+ let haltReason;
4562
+ for (const item of head.queued ?? []) {
4563
+ if (item.text.startsWith(BS_DONE) && !item.done) {
4564
+ doneLastTurn.push(item.text.slice(BS_DONE.length).trim());
4565
+ continue;
4566
+ }
4567
+ if (item.text.startsWith(BS_PROGRESS) && !item.done) {
4568
+ inProgress.push(item.text.slice(BS_PROGRESS.length).trim());
4569
+ continue;
4570
+ }
4571
+ if (item.text.startsWith(BS_BLOCKED) && !item.done) {
4572
+ const body = item.text.slice(BS_BLOCKED.length);
4573
+ const [site, decision, issue2] = body.split("|");
4574
+ blocked.push({ site: site?.trim() ?? "", decision: decision?.trim() ?? "", issue: issue2?.trim() });
4575
+ continue;
4576
+ }
4577
+ if (item.text.startsWith(BS_TIER) && !item.done) {
4578
+ const body = item.text.slice(BS_TIER.length);
4579
+ const [site, tier2, signals] = body.split("|");
4580
+ tierLedger.push({ site: site?.trim() ?? "", tier: tier2?.trim() ?? "", signals: signals?.trim() });
4581
+ continue;
4582
+ }
4583
+ if (item.text.startsWith(BS_CEILING) && !item.done) {
4584
+ const body = item.text.slice(BS_CEILING.length);
4585
+ const [site, rung] = body.split("|");
4586
+ verificationCeiling.push({ site: site?.trim() ?? "", rung: rung?.trim() ?? "" });
4587
+ continue;
4588
+ }
4589
+ if (item.text.startsWith(BS_HALT) && !item.done) {
4590
+ haltReason = item.text.slice(BS_HALT.length).trim();
4591
+ continue;
4592
+ }
4593
+ if (item.done) {
4594
+ const d = item.text.match(LEGACY_BUILD_DONE);
4595
+ if (d) doneLastTurn.push(d[1].trim());
4596
+ continue;
4597
+ }
4598
+ const prog = item.text.match(LEGACY_BUILD_PROGRESS);
4599
+ if (prog) {
4600
+ inProgress.push(prog[1].trim());
4601
+ continue;
4602
+ }
4603
+ const blk = item.text.match(LEGACY_BUILD_BLOCKED);
4604
+ if (blk) {
4605
+ blocked.push({ site: blk[1].trim(), decision: blk[2].trim(), issue: blk[3]?.trim() });
4606
+ continue;
4607
+ }
4608
+ const tier = item.text.match(LEGACY_BUILD_TIER);
4609
+ if (tier) {
4610
+ tierLedger.push({ site: tier[1].trim(), tier: tier[2].trim(), signals: tier[3]?.trim() });
4611
+ continue;
4612
+ }
4613
+ const ceil = item.text.match(LEGACY_BUILD_CEILING);
4614
+ if (ceil) {
4615
+ verificationCeiling.push({ site: ceil[1].trim(), rung: ceil[2].trim() });
4616
+ continue;
4617
+ }
4618
+ const halt = item.text.match(LEGACY_BUILD_HALT);
4619
+ if (halt) haltReason = halt[1].trim();
4620
+ }
4621
+ const nextFrontier = [];
4622
+ if (head.next?.trim()) {
4623
+ for (const line of head.next.split(";").map((s) => s.trim()).filter(Boolean)) {
4624
+ nextFrontier.push(line);
4625
+ }
4626
+ }
4627
+ return {
4628
+ kind: "build",
4629
+ milestone: head.anchor?.intent,
4630
+ northStarSlug: head.anchor?.slug,
4631
+ doneLastTurn,
4632
+ inProgress,
4633
+ blocked,
4634
+ nextFrontier,
4635
+ tierLedger,
4636
+ verificationCeiling,
4637
+ haltReason
4638
+ };
4639
+ }
4640
+ function parseSnapshot(kind, head) {
4641
+ return kind === "grind" ? parseGrindSnapshot(head) : parseBuildSnapshot(head);
4642
+ }
4643
+ function strOrU(v) {
4644
+ return typeof v === "string" && v.length ? v : void 0;
4645
+ }
4646
+ function numOrU(v) {
4647
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
4648
+ }
4649
+ function asStringArray(v) {
4650
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
4651
+ }
4652
+ function normalizeSnapshot(kind, raw) {
4653
+ if (!raw || typeof raw !== "object") throw new Error("snapshot JSON must be an object");
4654
+ const o = raw;
4655
+ if (o.kind !== kind) throw new Error(`snapshot kind ${JSON.stringify(o.kind)} does not match --kind ${kind}`);
4656
+ if (kind === "grind") {
4657
+ return {
4658
+ kind: "grind",
4659
+ class: strOrU(o.class),
4660
+ routing: strOrU(o.routing),
4661
+ ultra: strOrU(o.ultra),
4662
+ criteria: strOrU(o.criteria),
4663
+ phase: strOrU(o.phase),
4664
+ verifyRound: numOrU(o.verifyRound),
4665
+ verifyCap: numOrU(o.verifyCap),
4666
+ openBlockerIds: asStringArray(o.openBlockerIds),
4667
+ resolvedBlockerIds: asStringArray(o.resolvedBlockerIds),
4668
+ verificationCeiling: strOrU(o.verificationCeiling),
4669
+ nextAction: strOrU(o.nextAction)
4670
+ };
4671
+ }
4672
+ const blocked = Array.isArray(o.blocked) ? o.blocked.filter((b) => !!b && typeof b === "object").map((b) => ({ site: strOrU(b.site) ?? "", decision: strOrU(b.decision) ?? "", issue: strOrU(b.issue) })) : [];
4673
+ const tierLedger = Array.isArray(o.tierLedger) ? o.tierLedger.filter((t) => !!t && typeof t === "object").map((t) => ({ site: strOrU(t.site) ?? "", tier: strOrU(t.tier) ?? "", signals: strOrU(t.signals) })) : [];
4674
+ const verificationCeiling = Array.isArray(o.verificationCeiling) ? o.verificationCeiling.filter((v) => !!v && typeof v === "object").map((v) => ({ site: strOrU(v.site) ?? "", rung: strOrU(v.rung) ?? "" })) : [];
4675
+ return {
4676
+ kind: "build",
4677
+ milestone: strOrU(o.milestone),
4678
+ northStarSlug: strOrU(o.northStarSlug),
4679
+ doneLastTurn: asStringArray(o.doneLastTurn),
4680
+ inProgress: asStringArray(o.inProgress),
4681
+ blocked,
4682
+ nextFrontier: asStringArray(o.nextFrontier),
4683
+ tierLedger,
4684
+ verificationCeiling,
4685
+ haltReason: strOrU(o.haltReason)
4686
+ };
4687
+ }
4688
+ function planGrindSnapshotWrite(s, summary = "grind snapshot refresh") {
4689
+ const queueOps = [];
4690
+ for (const id of s.openBlockerIds) queueOps.push({ text: `${GS_OPEN}${id}` });
4691
+ for (const id of s.resolvedBlockerIds) queueOps.push({ text: `${GS_RESOLVED}${id}` });
4692
+ if (s.verificationCeiling) queueOps.push({ text: `${GS_CEILING}${s.verificationCeiling}` });
4693
+ return {
4694
+ summary,
4695
+ next: formatGrindNext(s),
4696
+ anchor: s.criteria,
4697
+ queueOps,
4698
+ clearIndices: []
4699
+ };
4700
+ }
4701
+ function planBuildSnapshotWrite(s, summary = "build snapshot refresh") {
4702
+ const queueOps = [];
4703
+ for (const site of s.doneLastTurn) queueOps.push({ text: `${BS_DONE}${site}` });
4704
+ for (const site of s.inProgress) queueOps.push({ text: `${BS_PROGRESS}${site}` });
4705
+ for (const b of s.blocked) {
4706
+ queueOps.push({ text: `${BS_BLOCKED}${b.site}|${b.decision}${b.issue ? `|${b.issue}` : ""}` });
4707
+ }
4708
+ for (const t of s.tierLedger) {
4709
+ queueOps.push({ text: `${BS_TIER}${t.site}|${t.tier}${t.signals ? `|${t.signals}` : ""}` });
4710
+ }
4711
+ for (const v of s.verificationCeiling) queueOps.push({ text: `${BS_CEILING}${v.site}|${v.rung}` });
4712
+ if (s.haltReason) queueOps.push({ text: `${BS_HALT}${s.haltReason}` });
4713
+ return {
4714
+ summary,
4715
+ next: s.nextFrontier.length ? s.nextFrontier.join("; ") : void 0,
4716
+ anchor: s.milestone,
4717
+ anchorSlug: s.northStarSlug,
4718
+ queueOps,
4719
+ clearIndices: []
4720
+ };
4721
+ }
4722
+ function planSnapshotWrite(snapshot, head, summary) {
4723
+ const plan2 = snapshot.kind === "grind" ? planGrindSnapshotWrite(snapshot, summary) : planBuildSnapshotWrite(snapshot, summary);
4724
+ plan2.clearIndices = snapshotClearIndices(snapshot.kind, head);
4725
+ return plan2;
4726
+ }
4727
+ function formatSnapshotHuman(snapshot) {
4728
+ if (snapshot.kind === "grind") {
4729
+ const lines2 = [
4730
+ `kind: grind`,
4731
+ snapshot.class ? `class: ${snapshot.class}` : "",
4732
+ snapshot.routing ? `routing: ${snapshot.routing}` : "",
4733
+ snapshot.ultra ? `ultra: ${snapshot.ultra}` : "",
4734
+ snapshot.criteria ? `criteria: ${snapshot.criteria}` : "",
4735
+ snapshot.phase ? `phase: ${snapshot.phase}` : "",
4736
+ snapshot.verifyRound != null ? `verify: ${snapshot.verifyRound}/${snapshot.verifyCap ?? "?"}` : "",
4737
+ snapshot.openBlockerIds.length ? `open blockers: ${snapshot.openBlockerIds.join(", ")}` : "open blockers: (none)",
4738
+ snapshot.resolvedBlockerIds.length ? `resolved: ${snapshot.resolvedBlockerIds.join(", ")}` : "",
4739
+ snapshot.verificationCeiling ? `ceiling: ${snapshot.verificationCeiling}` : "",
4740
+ snapshot.nextAction ? `next: ${snapshot.nextAction}` : ""
4741
+ ].filter(Boolean);
4742
+ return lines2.join("\n");
4743
+ }
4744
+ const lines = [
4745
+ `kind: build`,
4746
+ snapshot.milestone ? `milestone: ${snapshot.milestone}` : "",
4747
+ snapshot.northStarSlug ? `north star: ${snapshot.northStarSlug}` : "",
4748
+ snapshot.doneLastTurn.length ? `done: ${snapshot.doneLastTurn.join(", ")}` : "",
4749
+ snapshot.inProgress.length ? `in progress: ${snapshot.inProgress.join(", ")}` : "",
4750
+ snapshot.nextFrontier.length ? `next frontier: ${snapshot.nextFrontier.join("; ")}` : "",
4751
+ snapshot.haltReason ? `halt: ${snapshot.haltReason}` : ""
4752
+ ].filter(Boolean);
4753
+ return lines.join("\n");
4754
+ }
4755
+
4453
4756
  // src/saga-commands.ts
4454
4757
  async function runNote(summary, o) {
4455
4758
  const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
@@ -4582,6 +4885,60 @@ async function runSagaHealth(o, io = consoleIo) {
4582
4885
  if (report.warnings.length) io.log(report.warnings.map((w) => ` - ${w}`).join("\n"));
4583
4886
  if (report.pendingNotes > 0) io.log(` - ${report.pendingNotes} note(s) queued locally \u2014 \`mmi-cli saga flush\` to roll forward`);
4584
4887
  }
4888
+ async function fetchSagaHead(io = consoleIo) {
4889
+ const cfg = await loadConfig();
4890
+ if (!cfg.sagaApiUrl) {
4891
+ io.err("saga snapshot: Hub API URL not configured");
4892
+ return null;
4893
+ }
4894
+ try {
4895
+ const key = await sagaKey(cfg);
4896
+ const qs = new URLSearchParams(key).toString();
4897
+ const res = await fetch(`${cfg.sagaApiUrl}/saga/state?${qs}`, { headers: await hubHeaders(), signal: AbortSignal.timeout(8e3) });
4898
+ if (!res.ok) {
4899
+ io.err(`saga snapshot: HTTP ${res.status}`);
4900
+ return null;
4901
+ }
4902
+ const state = await res.json();
4903
+ return state.head ?? {};
4904
+ } catch (e) {
4905
+ io.err(`saga snapshot: ${e.message}`);
4906
+ return null;
4907
+ }
4908
+ }
4909
+ async function postSnapshotNotes(plan2, anchorForce) {
4910
+ const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
4911
+ const evidence = { sha: sha || void 0, branch: key.branch };
4912
+ for (const idx of [...plan2.clearIndices].sort((a, b) => b - a)) {
4913
+ const cap = buildNoteCapture("snapshot retire", { queueDone: String(idx) }, (0, import_node_crypto3.randomUUID)(), evidence);
4914
+ await postCapture(cap);
4915
+ }
4916
+ const primary = buildNoteCapture(plan2.summary, {
4917
+ next: plan2.next,
4918
+ anchor: plan2.anchor,
4919
+ anchorSlug: plan2.anchorSlug,
4920
+ anchorForce
4921
+ }, (0, import_node_crypto3.randomUUID)(), evidence);
4922
+ await postCapture(primary);
4923
+ for (const op of plan2.queueOps) {
4924
+ const cap = buildNoteCapture("snapshot checklist", { queueAdd: op.text }, (0, import_node_crypto3.randomUUID)(), evidence);
4925
+ await postCapture(cap);
4926
+ }
4927
+ }
4928
+ async function runSagaSnapshotShow(opts, io = consoleIo) {
4929
+ const head = await fetchSagaHead(io);
4930
+ if (!head) return;
4931
+ const snapshot = parseSnapshot(opts.kind, head);
4932
+ if (opts.json) return io.log(JSON.stringify(snapshot, null, 2));
4933
+ io.log(formatSnapshotHuman(snapshot));
4934
+ }
4935
+ async function runSagaSnapshotSet(snapshot, opts = {}, io = consoleIo) {
4936
+ const head = await fetchSagaHead(io) ?? { queued: [] };
4937
+ const plan2 = planSnapshotWrite(snapshot, head);
4938
+ await postSnapshotNotes(plan2, opts.anchorForce);
4939
+ if (opts.json) return io.log(JSON.stringify({ ok: true, snapshot, retired: plan2.clearIndices.length, queued: plan2.queueOps.length + 1 }));
4940
+ io.log(`saga snapshot: wrote ${snapshot.kind} snapshot (retired ${plan2.clearIndices.length}, ${plan2.queueOps.length + 1} capture(s))`);
4941
+ }
4585
4942
  function registerSagaCommands(program3) {
4586
4943
  const saga = program3.command("saga").description("per-session continuity");
4587
4944
  saga.command("note [summary]").description("record a one-line structured note into your saga (the per-turn capture)").option("--next <text>", 'set "where I left off" (NEXT)').option("--decision <text>", "append a verbatim decision").option("--queue-add <text>", "add a worklist item").option("--queue-done <n>", "mark worklist item N done").option("--verified", "mark this claim as checked against source (state: verified, else asserted)").option("--diagnostic", "isolate a probe write (state: diagnostic, source: probe) \u2014 never resume/LAST 5").option("--supersedes <key>", "retire prior decisions matching an evidence key (pr:N | file:path)").option("--anchor <intent>", "set the sprint North-Star (write-protected; needs --anchor-force to change)").option("--anchor-slug <slug>", "bind the anchor to a North Star plan slug (SSOT at plans/.../<slug>.md)").option("--anchor-force", "overwrite an existing anchor").option("--message-file <path|->", "read the summary from a UTF-8 file, or from stdin with - (avoids cmd.exe quoting)").action(async (summary, o) => {
@@ -4647,6 +5004,69 @@ function registerSagaCommands(program3) {
4647
5004
  console.log(`project=${key.project} branch=${key.branch} session=${key.sessionId} source=${source}`);
4648
5005
  });
4649
5006
  saga.command("health").option("--json", "machine-readable output").option("--banner", "one-line SessionStart banner; silent when healthy").option("--quiet", "suppress detail output").description("zero-write health check: auth, backend reachability, resolved key").action((o) => runSagaHealth(o));
5007
+ const snapshot = saga.command("snapshot").description("structured grind/build resume snapshot over saga HEAD");
5008
+ snapshot.command("show").description("read the structured resume snapshot from current saga HEAD").requiredOption("--kind <kind>", "grind | build").option("--json", "output JSON").action(async (o) => {
5009
+ const kind = o.kind;
5010
+ if (kind !== "grind" && kind !== "build") return fail("saga snapshot show: --kind must be grind or build");
5011
+ await runSagaSnapshotShow({ kind, json: o.json });
5012
+ });
5013
+ snapshot.command("set").description("write a structured resume snapshot (maps to saga note HEAD primitives)").requiredOption("--kind <kind>", "grind | build").option("--json-file <path>", "full snapshot JSON (overrides field flags)").option("--anchor-force", "overwrite an existing anchor").option("--json", "output result JSON").option("--class <c>", "grind class").option("--routing <r>", "grind routing tier").option("--ultra <u>", "ultra mode").option("--criteria <text>", "grind criteria / frame").option("--phase <p>", "phase reached").option("--verify-round <n>", "verify round", (v) => parseInt(v, 10)).option("--verify-cap <n>", "verify cap", (v) => parseInt(v, 10)).option("--open-blocker <id>", "open blocker id (repeatable)", (v, prev) => [...prev, v], []).option("--resolved-blocker <id>", "resolved blocker id (repeatable)", (v, prev) => [...prev, v], []).option("--verification-ceiling <text>", "verification ceiling").option("--next-action <text>", "next action").option("--milestone <text>", "build milestone one-liner").option("--north-star-slug <slug>", "North Star slug").option("--done <site>", "done last turn site (repeatable)", (v, prev) => [...prev, v], []).option("--in-progress <site>", "in-progress site (repeatable)", (v, prev) => [...prev, v], []).option("--blocked <spec>", "site|decision|issue (repeatable)", (v, prev) => [...prev, v], []).option("--next-frontier <item>", "frontier item (repeatable)", (v, prev) => [...prev, v], []).option("--tier <spec>", "site|tier|signals (repeatable)", (v, prev) => [...prev, v], []).option("--site-ceiling <spec>", "site|rung (repeatable)", (v, prev) => [...prev, v], []).option("--halt-reason <text>", "halt reason").action(async (o) => {
5014
+ const kind = o.kind;
5015
+ if (kind !== "grind" && kind !== "build") return fail("saga snapshot set: --kind must be grind or build");
5016
+ let snap;
5017
+ if (o.jsonFile) {
5018
+ const raw = await (0, import_promises2.readFile)(o.jsonFile, "utf8");
5019
+ let parsed;
5020
+ try {
5021
+ parsed = JSON.parse(raw);
5022
+ } catch (e) {
5023
+ return fail(`saga snapshot set: invalid JSON in ${o.jsonFile}: ${e.message}`);
5024
+ }
5025
+ try {
5026
+ snap = normalizeSnapshot(kind, parsed);
5027
+ } catch (e) {
5028
+ return fail(`saga snapshot set: ${e.message}`);
5029
+ }
5030
+ } else if (kind === "grind") {
5031
+ snap = {
5032
+ kind: "grind",
5033
+ class: o.class,
5034
+ routing: o.routing,
5035
+ ultra: o.ultra,
5036
+ criteria: o.criteria,
5037
+ phase: o.phase,
5038
+ verifyRound: o.verifyRound,
5039
+ verifyCap: o.verifyCap,
5040
+ openBlockerIds: o.openBlocker ?? [],
5041
+ resolvedBlockerIds: o.resolvedBlocker ?? [],
5042
+ verificationCeiling: o.verificationCeiling,
5043
+ nextAction: o.nextAction
5044
+ };
5045
+ } else {
5046
+ snap = {
5047
+ kind: "build",
5048
+ milestone: o.milestone,
5049
+ northStarSlug: o.northStarSlug,
5050
+ doneLastTurn: o.done ?? [],
5051
+ inProgress: o.inProgress ?? [],
5052
+ blocked: (o.blocked ?? []).map((spec) => {
5053
+ const [site, decision, issue2] = spec.split("|");
5054
+ return { site: site ?? "", decision: decision ?? "", issue: issue2 };
5055
+ }),
5056
+ nextFrontier: o.nextFrontier ?? [],
5057
+ tierLedger: (o.tier ?? []).map((spec) => {
5058
+ const [site, tier, signals] = spec.split("|");
5059
+ return { site: site ?? "", tier: tier ?? "", signals };
5060
+ }),
5061
+ verificationCeiling: (o.siteCeiling ?? []).map((spec) => {
5062
+ const [site, rung] = spec.split("|");
5063
+ return { site: site ?? "", rung: rung ?? "" };
5064
+ }),
5065
+ haltReason: o.haltReason
5066
+ };
5067
+ }
5068
+ await runSagaSnapshotSet(snap, { anchorForce: o.anchorForce, json: o.json });
5069
+ });
4650
5070
  }
4651
5071
 
4652
5072
  // src/honcho-commands.ts
@@ -7229,7 +7649,7 @@ ${buildReportBody(body, sourceRepo)}`;
7229
7649
 
7230
7650
  // src/skill-lesson.ts
7231
7651
  var SKILL_LESSON_LABEL = "skill-lesson";
7232
- var SKILL_NAMES = ["bootstrap", "browser-automation", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
7652
+ var SKILL_NAMES = ["bootstrap", "browser-automation", "build", "grind", "hotfix", "mmi", "rcand", "release", "secrets", "stage"];
7233
7653
  function assertSkillName(name) {
7234
7654
  const match = SKILL_NAMES.find((skill) => skill === name);
7235
7655
  if (!match) throw new Error(`unknown skill "${name}" \u2014 expected one of: ${SKILL_NAMES.join(", ")}`);
@@ -7355,127 +7775,173 @@ function buildPanelPlan(input) {
7355
7775
  };
7356
7776
  }
7357
7777
 
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"
7778
+ // src/build-policy.ts
7779
+ var TIER_BUDGETS = {
7780
+ light: {
7781
+ tier: "light",
7782
+ agents: 2,
7783
+ planners: 0,
7784
+ parallelSitesCap: 1,
7785
+ verificationDepth: "panel-once",
7786
+ reasoningEffort: "standard"
7787
+ },
7788
+ standard: {
7789
+ tier: "standard",
7790
+ agents: 3,
7791
+ planners: 0,
7792
+ parallelSitesCap: 2,
7793
+ verificationDepth: "full-panel",
7794
+ reasoningEffort: "standard-high"
7795
+ },
7796
+ deep: {
7797
+ tier: "deep",
7798
+ agents: 4,
7799
+ planners: 2,
7800
+ parallelSitesCap: 3,
7801
+ verificationDepth: "full-double-pass",
7802
+ reasoningEffort: "high"
7803
+ },
7804
+ max: {
7805
+ tier: "max",
7806
+ agents: 5,
7807
+ planners: 3,
7808
+ parallelSitesCap: 4,
7809
+ verificationDepth: "integration-checkpoints",
7810
+ reasoningEffort: "max"
7811
+ }
7372
7812
  };
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");
7813
+ function getTierBudgets(tier) {
7814
+ return TIER_BUDGETS[tier];
7815
+ }
7816
+ function isWideOrArchitectural(scope) {
7817
+ return scope === "wide" || scope === "architectural";
7818
+ }
7819
+ function isCrossModuleOrProduct(blast) {
7820
+ return blast === "cross-module" || blast === "product";
7821
+ }
7822
+ function pickEffortTier(signals) {
7823
+ if (signals.explicitTier) {
7824
+ return {
7825
+ tier: signals.explicitTier,
7826
+ reason: `explicit user override \u2192 ${signals.explicitTier}`,
7827
+ budgets: getTierBudgets(signals.explicitTier)
7828
+ };
7389
7829
  }
7390
- if (models.synthesizer === models.builder) {
7391
- throw new Error("fusion plan: synthesizer must not equal builder");
7830
+ if (signals.risk === "critical") {
7831
+ return { tier: "max", reason: "critical risk", budgets: getTierBudgets("max") };
7392
7832
  }
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);
7833
+ if (signals.foundational && (isWideOrArchitectural(signals.scope) || signals.blastRadius === "product")) {
7414
7834
  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
7835
+ tier: "max",
7836
+ reason: "foundational seam with wide/architectural scope or product blast radius",
7837
+ budgets: getTierBudgets("max")
7422
7838
  };
7423
7839
  }
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
- }));
7840
+ if (signals.risk === "high") {
7841
+ return { tier: "deep", reason: "high risk", budgets: getTierBudgets("deep") };
7842
+ }
7843
+ if (signals.ambiguity === "high") {
7844
+ return { tier: "deep", reason: "high ambiguity", budgets: getTierBudgets("deep") };
7845
+ }
7846
+ if (signals.scope === "architectural") {
7847
+ return { tier: "deep", reason: "architectural scope", budgets: getTierBudgets("deep") };
7848
+ }
7849
+ if (isCrossModuleOrProduct(signals.blastRadius)) {
7850
+ return {
7851
+ tier: "deep",
7852
+ reason: `${signals.blastRadius} blast radius`,
7853
+ budgets: getTierBudgets("deep")
7854
+ };
7855
+ }
7856
+ if (signals.scope === "trivial" && signals.ambiguity === "low" && signals.risk === "low") {
7857
+ return {
7858
+ tier: "light",
7859
+ reason: "trivial scope with low risk and low ambiguity",
7860
+ budgets: getTierBudgets("light")
7861
+ };
7862
+ }
7863
+ return { tier: "standard", reason: "default \u2014 normal slice", budgets: getTierBudgets("standard") };
7864
+ }
7865
+ var VERIFICATION_DEPTH_ROUNDS = {
7866
+ "panel-once": 1,
7867
+ "full-panel": 2,
7868
+ "full-double-pass": 4,
7869
+ "integration-checkpoints": 6
7870
+ };
7871
+ var TIER_LADDER = ["light", "standard", "deep", "max"];
7872
+ var CAMPAIGN_ITERATION_CAP = 15;
7873
+ var COST_CEILING = 500;
7874
+ function estimateWorstCaseCost(input) {
7875
+ const tier = input.tier ?? (input.signals ? pickEffortTier(input.signals).tier : "standard");
7876
+ const budgets = getTierBudgets(tier);
7877
+ const verifyRounds = VERIFICATION_DEPTH_ROUNDS[budgets.verificationDepth];
7878
+ const sites = Math.max(1, input.plannedSiteCount ?? 1);
7879
+ const fusionMultiplier = budgets.planners > 0 ? 1.5 : 1;
7880
+ const costUnits = Math.ceil(budgets.agents * verifyRounds * sites * fusionMultiplier);
7881
+ const breakdown = `tier=${tier} agents=${budgets.agents} verifyRounds=${verifyRounds} sites=${sites} fusion\xD7${fusionMultiplier}`;
7432
7882
  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 ?? []
7883
+ costUnits,
7884
+ breakdown,
7885
+ exceedsCeiling: costUnits > COST_CEILING
7440
7886
  };
7441
7887
  }
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" };
7888
+ function decideOverCeiling(estimate, tier) {
7889
+ if (!estimate.exceedsCeiling) return { action: "ok" };
7890
+ if (tier === "light") return { action: "halt" };
7891
+ const idx = TIER_LADDER.indexOf(tier);
7892
+ const suggestedTier = idx > 0 ? TIER_LADDER[idx - 1] : "light";
7893
+ return { action: "lower-tier", suggestedTier };
7894
+ }
7895
+ function frontierExhausted(state) {
7896
+ const noUnblocked = (state.openUnblockedClaims ?? 0) === 0;
7897
+ const noOpenPrs = (state.openInflightPrs ?? 0) === 0;
7898
+ const frontierEmpty = state.nextFrontierEmpty ?? false;
7899
+ const noUnresolvedParked = !(state.parkedSitesUnresolved ?? false);
7900
+ return frontierEmpty && noUnblocked && noOpenPrs && noUnresolvedParked;
7901
+ }
7902
+ function campaignIterationCapExceeded(state) {
7903
+ const cap = state.iterationCapOverride ?? CAMPAIGN_ITERATION_CAP;
7904
+ return (state.orientCount ?? 0) >= cap;
7905
+ }
7906
+ function evaluateBuildFrontier(state) {
7907
+ const iterationCap = state.iterationCapOverride ?? CAMPAIGN_ITERATION_CAP;
7908
+ const exhausted = frontierExhausted(state);
7909
+ const capHit = campaignIterationCapExceeded(state);
7910
+ if (exhausted) {
7911
+ return { frontierExhausted: true, iterationCapExceeded: capHit, iterationCap, recommend: "halt", haltReason: "frontier-exhausted" };
7446
7912
  }
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 };
7913
+ if (capHit) {
7914
+ return { frontierExhausted: false, iterationCapExceeded: true, iterationCap, recommend: "halt", haltReason: "iteration-cap" };
7472
7915
  }
7916
+ return { frontierExhausted: false, iterationCapExceeded: false, iterationCap, recommend: "continue" };
7473
7917
  }
7474
- function parseFusionLenses(raw) {
7475
- return raw.split(",").map((s) => assertGrindLens(s.trim()));
7476
- }
7477
- function parseFusionRouting(raw) {
7478
- return assertVerifyRouting(raw);
7918
+
7919
+ // src/grind-policy.ts
7920
+ function isExplicitUltraYolo(input) {
7921
+ return Boolean(input.ultra);
7922
+ }
7923
+ function getUltraCaps(input) {
7924
+ if (isExplicitUltraYolo(input)) return { verifyRounds: 8, ciFixRounds: 5 };
7925
+ return { verifyRounds: 5, ciFixRounds: 3 };
7926
+ }
7927
+ var GRIND_LENS_COUNT = 5;
7928
+ var GRIND_COST_CEILING = 200;
7929
+ var MIN_STABLE_CLEAN_ROUNDS = 2;
7930
+ function estimateGrindWorstCaseCost(opts) {
7931
+ const caps = getUltraCaps(opts.input);
7932
+ const lenses = opts.lensCount ?? GRIND_LENS_COUNT;
7933
+ const models = opts.modelCount ?? (isExplicitUltraYolo(opts.input) || opts.input.autoUltra ? 3 : 2);
7934
+ const verifyCost = caps.verifyRounds * lenses * models;
7935
+ const synthesisCost = caps.verifyRounds;
7936
+ const ciFixCost = opts.includeCiFix ? caps.ciFixRounds * 2 : 0;
7937
+ const perPass = verifyCost + synthesisCost + ciFixCost;
7938
+ const costUnits = perPass * MIN_STABLE_CLEAN_ROUNDS;
7939
+ const breakdown = `verifyRounds=${caps.verifyRounds} lenses=${lenses} models=${models} synthesis=${synthesisCost} ciFix=${ciFixCost} stableRounds=${MIN_STABLE_CLEAN_ROUNDS}`;
7940
+ return {
7941
+ costUnits,
7942
+ breakdown,
7943
+ exceedsCeiling: costUnits > GRIND_COST_CEILING
7944
+ };
7479
7945
  }
7480
7946
 
7481
7947
  // src/gc.ts
@@ -8661,7 +9127,7 @@ function buildMmiPluginCacheCleanupCheck(input) {
8661
9127
  };
8662
9128
  }
8663
9129
  var NESTED_PLUGIN_TREE_LABEL = "self-nested MMI plugin cache tree (#1126)";
8664
- var NESTED_PLUGIN_TREE_FIX = "run `mmi-cli doctor` to surface the MAX_PATH-safe robocopy cleanup for a self-nested MMI plugin cache tree (#1126)";
9130
+ var NESTED_PLUGIN_TREE_FIX = "SessionStart and mmi-cli doctor auto-clear when possible; if this persists, run the MAX_PATH-safe robocopy cleanup for a self-nested MMI plugin cache tree (#1126)";
8665
9131
  function nestedPluginTreeCleanupCommand(paths, isWindows) {
8666
9132
  if (paths.length === 0) return "";
8667
9133
  if (isWindows) {
@@ -10704,9 +11170,8 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
10704
11170
  if (releaseExists) {
10705
11171
  releaseNote = `Release ${tag} already exists \u2014 resumed without recreating`;
10706
11172
  } 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)})`;
11173
+ await deps.run("gh", ["release", "create", tag, "--repo", ctx.repo, "--target", "main", "--generate-notes", "--latest"]);
11174
+ releaseNote = `Release ${tag} created (target main)`;
10710
11175
  if (deps.announce) {
10711
11176
  announceNote = (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note;
10712
11177
  }
@@ -11561,6 +12026,13 @@ async function verifyBootstrap(repo, repoClass, deps, releaseTrack) {
11561
12026
  ok: Boolean(config?.projectOwner && config?.projectNumber && config?.projectId && config?.statusFieldId && config?.statusOptions),
11562
12027
  label: "registry project board META exists"
11563
12028
  });
12029
+ if (config?.projectId && config.projectNumber == null) {
12030
+ checks.push({
12031
+ ok: false,
12032
+ label: "registry projectNumber present when projectId set",
12033
+ detail: "projectNumber is missing \u2014 bootstrap apply must pass PROJECT_NUMBER or derive it from the live board GraphQL query"
12034
+ });
12035
+ }
11564
12036
  if (config?.projectOwner && config.projectNumber != null) {
11565
12037
  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
12038
  const fields = await (async () => {
@@ -12187,6 +12659,25 @@ function boardRegistryGaps(meta) {
12187
12659
  if (meta.projectNumber != null) return [];
12188
12660
  return ["projectNumber"];
12189
12661
  }
12662
+ function previewRegistryMetaMerge(existing, patch) {
12663
+ const out = { ...existing ?? {} };
12664
+ for (const [key, value] of Object.entries(patch)) {
12665
+ if (value === null) delete out[key];
12666
+ else out[key] = value;
12667
+ }
12668
+ return out;
12669
+ }
12670
+ function boardLinkWriteError(patch, existing) {
12671
+ const patchHasProjectId = typeof patch.projectId === "string" && patch.projectId.length > 0;
12672
+ const patchHasProjectNumber = typeof patch.projectNumber === "number" && Number.isFinite(patch.projectNumber);
12673
+ if (patchHasProjectId && !patchHasProjectNumber && existing?.projectNumber == null) {
12674
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
12675
+ }
12676
+ if (patch.projectNumber === null && boardRegistryGaps(previewRegistryMetaMerge(existing, patch)).length) {
12677
+ return "projectId requires projectNumber in registry META \u2014 pass projectNumber with board coords";
12678
+ }
12679
+ return null;
12680
+ }
12190
12681
  function boardRegistryGapMessage(repo) {
12191
12682
  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
12683
  }
@@ -14404,6 +14895,9 @@ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project M
14404
14895
  } catch (e) {
14405
14896
  return fail(e.message.replace(/^project set: /, "project set: "));
14406
14897
  }
14898
+ const existing = await fetchProjectBySlug(slug, registryClientDeps(cfg));
14899
+ const boardError = boardLinkWriteError(patch, existing);
14900
+ if (boardError) return fail(`project set: ${boardError}`);
14407
14901
  const res = await upsertProject(slug, patch, registryClientDeps(cfg));
14408
14902
  return reportWrite("project set", res);
14409
14903
  });
@@ -14680,39 +15174,112 @@ verify.command("synthesize").description("merge lens JSON array into a PanelRepo
14680
15174
  return fail(`verify synthesize: ${e.message}`);
14681
15175
  }
14682
15176
  });
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}`);
15177
+ var build = program2.command("build").description("Build skill helpers \u2014 effort-tier selection and milestone partitioning");
15178
+ 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) => {
15179
+ const signals = {
15180
+ scope: opts.scope,
15181
+ risk: opts.risk,
15182
+ ambiguity: opts.ambiguity,
15183
+ blastRadius: opts.blastRadius,
15184
+ foundational: opts.foundational ?? void 0,
15185
+ explicitTier: opts.explicit
15186
+ };
15187
+ const decision = pickEffortTier(signals);
15188
+ if (opts.json) console.log(JSON.stringify(decision, null, 2));
15189
+ else {
15190
+ console.log(`tier: ${decision.tier}`);
15191
+ console.log(`reason: ${decision.reason}`);
15192
+ console.log(`agents: ${decision.budgets.agents}`);
15193
+ console.log(`planners: ${decision.budgets.planners}`);
15194
+ console.log(`parallel: ${decision.budgets.parallelSitesCap} sites`);
15195
+ console.log(`verify: ${decision.budgets.verificationDepth}`);
15196
+ console.log(`reasoning: ${decision.budgets.reasoningEffort}`);
14705
15197
  }
14706
15198
  });
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}`);
15199
+ build.command("estimate").description("Worst-case cost proxy (agent-call units) for a milestone run \u2014 not a dollar promise").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("--sites <n>", "planned L1 site count", (v) => parseInt(v, 10)).option("--json", "output JSON").action((opts) => {
15200
+ const signals = {
15201
+ scope: opts.scope,
15202
+ risk: opts.risk,
15203
+ ambiguity: opts.ambiguity,
15204
+ blastRadius: opts.blastRadius,
15205
+ foundational: opts.foundational ?? void 0,
15206
+ explicitTier: opts.explicit
15207
+ };
15208
+ const tierDecision = pickEffortTier(signals);
15209
+ const estimate = estimateWorstCaseCost({
15210
+ signals,
15211
+ tier: tierDecision.tier,
15212
+ plannedSiteCount: opts.sites
15213
+ });
15214
+ const overCeiling = decideOverCeiling(estimate, tierDecision.tier);
15215
+ const payload = {
15216
+ tier: tierDecision.tier,
15217
+ estimate,
15218
+ ceiling: COST_CEILING,
15219
+ overCeiling,
15220
+ iterationCap: CAMPAIGN_ITERATION_CAP,
15221
+ verificationDepthRounds: VERIFICATION_DEPTH_ROUNDS
15222
+ };
15223
+ if (opts.json) console.log(JSON.stringify(payload, null, 2));
15224
+ else {
15225
+ console.log(`tier: ${tierDecision.tier}`);
15226
+ console.log(`estimate: ${estimate.costUnits} agent-call units (proxy, not dollars)`);
15227
+ console.log(`breakdown: ${estimate.breakdown}`);
15228
+ const ceilingMsg = estimate.exceedsCeiling ? `EXCEEDS \u2192 ${overCeiling.action}${overCeiling.suggestedTier ? ` (try ${overCeiling.suggestedTier})` : ""}` : "within";
15229
+ console.log(`ceiling: ${COST_CEILING} units \u2014 ${ceilingMsg}`);
15230
+ console.log(`L0 cap: ${CAMPAIGN_ITERATION_CAP} orients (Phase 0 may override)`);
15231
+ }
15232
+ });
15233
+ build.command("frontier").description("Evaluate external frontier exhaustion + L0 iteration cap (Ralph Wiggum guard)").option("--next-frontier-empty", "in-hand North Star has no unblocked frontier").option("--open-claims <n>", "open unblocked board claims", (v) => parseInt(v, 10)).option("--open-prs <n>", "in-flight PRs for milestone", (v) => parseInt(v, 10)).option("--parked-unresolved", "parked sites still waiting on hard decisions").option("--orient-count <n>", "L0 orient count this run", (v) => parseInt(v, 10)).option("--iteration-cap <n>", "Phase 0 override for L0 cap", (v) => parseInt(v, 10)).option("--json-file <path>", "full BuildExternalState JSON").option("--json", "output JSON").action(async (opts) => {
15234
+ let state = {
15235
+ nextFrontierEmpty: opts.nextFrontierEmpty ?? void 0,
15236
+ openUnblockedClaims: opts.openClaims,
15237
+ openInflightPrs: opts.openPrs,
15238
+ parkedSitesUnresolved: opts.parkedUnresolved ?? void 0,
15239
+ orientCount: opts.orientCount,
15240
+ iterationCapOverride: opts.iterationCap
15241
+ };
15242
+ if (opts.jsonFile) {
15243
+ const raw = await (0, import_promises5.readFile)(opts.jsonFile, "utf8");
15244
+ state = { ...state, ...JSON.parse(raw) };
15245
+ }
15246
+ const result = evaluateBuildFrontier(state);
15247
+ if (opts.json) console.log(JSON.stringify(result, null, 2));
15248
+ else {
15249
+ console.log(`frontierExhausted: ${result.frontierExhausted}`);
15250
+ console.log(`iterationCapExceeded: ${result.iterationCapExceeded} (cap ${result.iterationCap})`);
15251
+ console.log(`recommend: ${result.recommend}${result.haltReason ? ` (${result.haltReason})` : ""}`);
15252
+ }
15253
+ });
15254
+ 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) => {
15255
+ const plan2 = {
15256
+ waves: issues.length > 0 ? [{ wave: 0, mode: "parallel", issues }] : [],
15257
+ note: issues.length === 0 ? "No issues provided." : "v1 partitioning: single parallel wave. Use grind --auto Phase 00 patterns for deeper analysis."
15258
+ };
15259
+ if (opts.json) console.log(JSON.stringify(plan2, null, 2));
15260
+ else {
15261
+ console.log(plan2.note);
15262
+ for (const w of plan2.waves) console.log(`wave ${w.wave} (${w.mode}): ${w.issues.join(" ")}`);
15263
+ }
15264
+ });
15265
+ var grindCmd = program2.command("grind").description("Grind skill helpers \u2014 routing and cost proxies");
15266
+ grindCmd.command("estimate").description("Worst-case cost proxy (agent-call units) for one grind \u2014 not a dollar promise").option("--class <c>", "bug|feature|research|task|security-critical", "feature").option("--ultra", "explicit --ultra YOLO").option("--auto-ultra", "auto-ultra verify uplift (3 models)").option("--auto", "include CI-fix rounds").option("--models <n>", "panel model count", (v) => parseInt(v, 10)).option("--json", "output JSON").action((opts) => {
15267
+ const input = {
15268
+ grindClass: opts.class ?? "feature",
15269
+ ultra: opts.ultra ?? void 0,
15270
+ autoUltra: opts.autoUltra ?? void 0
15271
+ };
15272
+ const estimate = estimateGrindWorstCaseCost({
15273
+ input,
15274
+ modelCount: opts.models,
15275
+ includeCiFix: opts.auto ?? false
15276
+ });
15277
+ const payload = { estimate, ceiling: GRIND_COST_CEILING, input };
15278
+ if (opts.json) console.log(JSON.stringify(payload, null, 2));
15279
+ else {
15280
+ console.log(`estimate: ${estimate.costUnits} agent-call units (proxy, not dollars)`);
15281
+ console.log(`breakdown: ${estimate.breakdown}`);
15282
+ console.log(`ceiling: ${GRIND_COST_CEILING} units \u2014 ${estimate.exceedsCeiling ? "EXCEEDS \u2192 ask human (cap/stuck path)" : "within"}`);
14716
15283
  }
14717
15284
  });
14718
15285
  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) => {
@@ -15866,6 +16433,46 @@ function quarantinePluginCacheDirs(plan2) {
15866
16433
  }
15867
16434
  return moved;
15868
16435
  }
16436
+ async function robocopyMirrorEmpty(emptyDir, target) {
16437
+ try {
16438
+ await execFileP2("robocopy", [emptyDir, target, "/MIR", "/NJH", "/NJS", "/NFL", "/NDL"], { timeout: CLAUDE_PLUGIN_TIMEOUT_MS });
16439
+ } catch (e) {
16440
+ const code = e.code;
16441
+ if (typeof code === "number" && code <= 7) return;
16442
+ throw e;
16443
+ }
16444
+ }
16445
+ async function clearNestedPluginTreeDir(targetPath) {
16446
+ try {
16447
+ if (!(0, import_node_fs15.existsSync)(targetPath)) return true;
16448
+ if (isWin) {
16449
+ const emptyDir = (0, import_node_path14.join)((0, import_node_os5.tmpdir)(), `mmi-empty-${Date.now()}`);
16450
+ (0, import_node_fs15.mkdirSync)(emptyDir, { recursive: true });
16451
+ try {
16452
+ await robocopyMirrorEmpty(emptyDir, targetPath);
16453
+ (0, import_node_fs15.rmSync)(targetPath, { recursive: true, force: true });
16454
+ } finally {
16455
+ try {
16456
+ (0, import_node_fs15.rmSync)(emptyDir, { recursive: true, force: true });
16457
+ } catch {
16458
+ }
16459
+ }
16460
+ return !(0, import_node_fs15.existsSync)(targetPath);
16461
+ }
16462
+ (0, import_node_fs15.rmSync)(targetPath, { recursive: true, force: true });
16463
+ return !(0, import_node_fs15.existsSync)(targetPath);
16464
+ } catch {
16465
+ return false;
16466
+ }
16467
+ }
16468
+ async function applyNestedPluginTreeCleanup(paths, log) {
16469
+ if (paths.length === 0) return false;
16470
+ log(` \u21BB clearing ${paths.length} self-nested MMI plugin cache tree(s) (#1126)\u2026`);
16471
+ for (const path2 of paths) {
16472
+ if (!await clearNestedPluginTreeDir(path2)) return false;
16473
+ }
16474
+ return true;
16475
+ }
15869
16476
  var gitignorePath = () => (0, import_node_path14.join)(process.cwd(), ".gitignore");
15870
16477
  function readTextFile(path2) {
15871
16478
  try {
@@ -16028,7 +16635,7 @@ async function runDoctor(opts, io = consoleIo) {
16028
16635
  releasedVersion,
16029
16636
  surface
16030
16637
  });
16031
- if (!installedVersionCheck.ok && repairFull) {
16638
+ if (!installedVersionCheck.ok && (repairFull || repairLocal)) {
16032
16639
  const claudeStale = installedVersionCheck.staleSurfaces?.some((s) => s.surface === "claude") ?? false;
16033
16640
  if (claudeStale && await applyClaudePluginHeal(surface, (m) => io.err(m))) {
16034
16641
  const healed = buildInstalledPluginVersionCheck({
@@ -16069,13 +16676,33 @@ async function runDoctor(opts, io = consoleIo) {
16069
16676
  };
16070
16677
  }
16071
16678
  checks.push(cacheCleanupCheck);
16072
- checks.push(
16073
- buildNestedPluginTreeCheck({
16074
- isOrgRepo: Boolean(cfg.sagaApiUrl),
16075
- isWindows: isWin,
16076
- entries: nestedPluginTreeSnapshot()
16077
- })
16078
- );
16679
+ let nestedPluginTreeCheck = buildNestedPluginTreeCheck({
16680
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
16681
+ isWindows: isWin,
16682
+ entries: nestedPluginTreeSnapshot()
16683
+ });
16684
+ if (!nestedPluginTreeCheck.ok && nestedPluginTreeCheck.nested?.length && repairLocal) {
16685
+ const nestedPaths = nestedPluginTreeCheck.nested.map((n) => n.path);
16686
+ if (await applyNestedPluginTreeCleanup(nestedPaths, (m) => io.err(m))) {
16687
+ nestedPluginTreeCheck = buildNestedPluginTreeCheck({
16688
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
16689
+ isWindows: isWin,
16690
+ entries: nestedPluginTreeSnapshot()
16691
+ });
16692
+ if (nestedPluginTreeCheck.ok) {
16693
+ io.err(` \u21BB cleared self-nested MMI plugin cache tree(s) \u2014 reinstalling plugin\u2026`);
16694
+ }
16695
+ if (await applyClaudePluginHeal(surface, (m) => io.err(m))) {
16696
+ io.err(` \u21BB reinstalled MMI plugin after nested-cache cleanup \u2014 ${reloadAction(surface)} to load MMI commands`);
16697
+ nestedPluginTreeCheck = buildNestedPluginTreeCheck({
16698
+ isOrgRepo: Boolean(cfg.sagaApiUrl),
16699
+ isWindows: isWin,
16700
+ entries: nestedPluginTreeSnapshot()
16701
+ });
16702
+ }
16703
+ }
16704
+ }
16705
+ checks.push(nestedPluginTreeCheck);
16079
16706
  const cursorCacheRoot = cursorPluginCacheRoot();
16080
16707
  let cursorPins = cursorPluginCachePinSnapshots() ?? [];
16081
16708
  let cursorPluginCheck = buildCursorPluginInstallCheck({