@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/saga.cjs CHANGED
@@ -3900,6 +3900,309 @@ function formatCaptureFailure(status, message) {
3900
3900
  return `saga: HTTP ${status}`;
3901
3901
  }
3902
3902
 
3903
+ // src/saga-snapshot.ts
3904
+ var GS_OPEN = "gs-open:";
3905
+ var GS_RESOLVED = "gs-resolved:";
3906
+ var GS_CEILING = "gs-ceiling:";
3907
+ var BS_DONE = "bs-done:";
3908
+ var BS_PROGRESS = "bs-progress:";
3909
+ var BS_BLOCKED = "bs-blocked:";
3910
+ var BS_TIER = "bs-tier:";
3911
+ var BS_CEILING = "bs-ceiling:";
3912
+ var BS_HALT = "bs-halt:";
3913
+ var GRIND_PREFIX = /^grind\s+/i;
3914
+ var META_ACTION_SEP = " | ";
3915
+ var LEGACY_BLOCKER_OPEN = /^blocker:(.+)$/i;
3916
+ var LEGACY_BLOCKER_RESOLVED = /^resolved:(.+)$/i;
3917
+ var LEGACY_CEILING = /^ceiling:(.+)$/i;
3918
+ var LEGACY_BUILD_DONE = /^done:(.+)$/i;
3919
+ var LEGACY_BUILD_PROGRESS = /^progress:(.+)$/i;
3920
+ var LEGACY_BUILD_BLOCKED = /^blocked:([^|]+)\|([^|]+)(?:\|(.+))?$/i;
3921
+ var LEGACY_BUILD_TIER = /^tier:([^|]+)\|([^|]+)(?:\|(.+))?$/i;
3922
+ var LEGACY_BUILD_CEILING = /^ceiling:([^|]+)\|(.+)$/i;
3923
+ var LEGACY_BUILD_HALT = /^halt:(.+)$/i;
3924
+ function isGrindSnapshotItem(text) {
3925
+ 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);
3926
+ }
3927
+ function isBuildSnapshotItem(text) {
3928
+ 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);
3929
+ }
3930
+ function snapshotClearIndices(kind, head) {
3931
+ const pred = kind === "grind" ? isGrindSnapshotItem : isBuildSnapshotItem;
3932
+ return (head.queued ?? []).map((item, index) => ({ item, index })).filter(({ item }) => !item.done && pred(item.text)).map(({ index }) => index);
3933
+ }
3934
+ function parseGrindNext(next) {
3935
+ if (!next?.trim()) return {};
3936
+ const raw = next.replace(GRIND_PREFIX, "").trim();
3937
+ const pipe = raw.indexOf(META_ACTION_SEP);
3938
+ const meta = pipe >= 0 ? raw.slice(0, pipe).trim() : raw;
3939
+ const nextAction = pipe >= 0 ? raw.slice(pipe + META_ACTION_SEP.length).trim() : void 0;
3940
+ const out = { nextAction };
3941
+ for (const part of meta.split(/\s+/)) {
3942
+ const eq = part.indexOf("=");
3943
+ if (eq < 0) continue;
3944
+ const k = part.slice(0, eq);
3945
+ const v = part.slice(eq + 1);
3946
+ if (k === "class") out.class = v;
3947
+ else if (k === "routing") out.routing = v;
3948
+ else if (k === "ultra") out.ultra = v;
3949
+ else if (k === "phase") out.phase = v;
3950
+ else if (k === "verify") {
3951
+ const [r, c] = v.split("/");
3952
+ const rn = Number(r);
3953
+ const cn = Number(c);
3954
+ if (Number.isFinite(rn)) out.verifyRound = rn;
3955
+ if (Number.isFinite(cn)) out.verifyCap = cn;
3956
+ } else if (k === "resolved" && v) {
3957
+ out.resolvedBlockerIds = v.split(",").map((s) => s.trim()).filter(Boolean);
3958
+ }
3959
+ }
3960
+ return out;
3961
+ }
3962
+ function formatGrindNext(s) {
3963
+ const parts = ["grind"];
3964
+ if (s.class) parts.push(`class=${s.class}`);
3965
+ if (s.routing) parts.push(`routing=${s.routing}`);
3966
+ if (s.ultra) parts.push(`ultra=${s.ultra}`);
3967
+ if (s.phase) parts.push(`phase=${s.phase}`);
3968
+ if (Number.isFinite(s.verifyRound) && Number.isFinite(s.verifyCap)) parts.push(`verify=${s.verifyRound}/${s.verifyCap}`);
3969
+ if (s.resolvedBlockerIds.length) parts.push(`resolved=${s.resolvedBlockerIds.join(",")}`);
3970
+ const meta = parts.join(" ");
3971
+ return s.nextAction ? `${meta}${META_ACTION_SEP}${s.nextAction}` : meta;
3972
+ }
3973
+ function parseGrindQueueItem(item, open, resolved, ceiling) {
3974
+ if (item.text.startsWith(GS_OPEN) && !item.done) open.push(item.text.slice(GS_OPEN.length).trim());
3975
+ else if (item.text.startsWith(GS_RESOLVED) && !item.done) resolved.push(item.text.slice(GS_RESOLVED.length).trim());
3976
+ else if (item.text.startsWith(GS_CEILING) && !item.done) ceiling.value = item.text.slice(GS_CEILING.length).trim();
3977
+ else {
3978
+ const legacyOpen = item.text.match(LEGACY_BLOCKER_OPEN);
3979
+ if (legacyOpen && !item.done) open.push(legacyOpen[1].trim());
3980
+ const legacyResolved = item.text.match(LEGACY_BLOCKER_RESOLVED);
3981
+ if (legacyResolved && item.done) resolved.push(legacyResolved[1].trim());
3982
+ const legacyCeil = item.text.match(LEGACY_CEILING);
3983
+ if (legacyCeil && !item.done) ceiling.value = legacyCeil[1].trim();
3984
+ }
3985
+ }
3986
+ function parseGrindSnapshot(head) {
3987
+ const fromNext = parseGrindNext(head.next);
3988
+ const openBlockerIds = [];
3989
+ const resolvedFromQueue = [];
3990
+ const ceiling = { value: void 0 };
3991
+ for (const item of head.queued ?? []) {
3992
+ parseGrindQueueItem(item, openBlockerIds, resolvedFromQueue, ceiling);
3993
+ }
3994
+ const resolvedBlockerIds = [.../* @__PURE__ */ new Set([...resolvedFromQueue, ...fromNext.resolvedBlockerIds ?? []])];
3995
+ const { resolvedBlockerIds: _drop, ...restNext } = fromNext;
3996
+ return {
3997
+ kind: "grind",
3998
+ criteria: head.anchor?.intent,
3999
+ openBlockerIds,
4000
+ resolvedBlockerIds,
4001
+ verificationCeiling: ceiling.value,
4002
+ ...restNext
4003
+ };
4004
+ }
4005
+ function parseBuildSnapshot(head) {
4006
+ const doneLastTurn = [];
4007
+ const inProgress = [];
4008
+ const blocked = [];
4009
+ const tierLedger = [];
4010
+ const verificationCeiling = [];
4011
+ let haltReason;
4012
+ for (const item of head.queued ?? []) {
4013
+ if (item.text.startsWith(BS_DONE) && !item.done) {
4014
+ doneLastTurn.push(item.text.slice(BS_DONE.length).trim());
4015
+ continue;
4016
+ }
4017
+ if (item.text.startsWith(BS_PROGRESS) && !item.done) {
4018
+ inProgress.push(item.text.slice(BS_PROGRESS.length).trim());
4019
+ continue;
4020
+ }
4021
+ if (item.text.startsWith(BS_BLOCKED) && !item.done) {
4022
+ const body = item.text.slice(BS_BLOCKED.length);
4023
+ const [site, decision, issue] = body.split("|");
4024
+ blocked.push({ site: site?.trim() ?? "", decision: decision?.trim() ?? "", issue: issue?.trim() });
4025
+ continue;
4026
+ }
4027
+ if (item.text.startsWith(BS_TIER) && !item.done) {
4028
+ const body = item.text.slice(BS_TIER.length);
4029
+ const [site, tier2, signals] = body.split("|");
4030
+ tierLedger.push({ site: site?.trim() ?? "", tier: tier2?.trim() ?? "", signals: signals?.trim() });
4031
+ continue;
4032
+ }
4033
+ if (item.text.startsWith(BS_CEILING) && !item.done) {
4034
+ const body = item.text.slice(BS_CEILING.length);
4035
+ const [site, rung] = body.split("|");
4036
+ verificationCeiling.push({ site: site?.trim() ?? "", rung: rung?.trim() ?? "" });
4037
+ continue;
4038
+ }
4039
+ if (item.text.startsWith(BS_HALT) && !item.done) {
4040
+ haltReason = item.text.slice(BS_HALT.length).trim();
4041
+ continue;
4042
+ }
4043
+ if (item.done) {
4044
+ const d = item.text.match(LEGACY_BUILD_DONE);
4045
+ if (d) doneLastTurn.push(d[1].trim());
4046
+ continue;
4047
+ }
4048
+ const prog = item.text.match(LEGACY_BUILD_PROGRESS);
4049
+ if (prog) {
4050
+ inProgress.push(prog[1].trim());
4051
+ continue;
4052
+ }
4053
+ const blk = item.text.match(LEGACY_BUILD_BLOCKED);
4054
+ if (blk) {
4055
+ blocked.push({ site: blk[1].trim(), decision: blk[2].trim(), issue: blk[3]?.trim() });
4056
+ continue;
4057
+ }
4058
+ const tier = item.text.match(LEGACY_BUILD_TIER);
4059
+ if (tier) {
4060
+ tierLedger.push({ site: tier[1].trim(), tier: tier[2].trim(), signals: tier[3]?.trim() });
4061
+ continue;
4062
+ }
4063
+ const ceil = item.text.match(LEGACY_BUILD_CEILING);
4064
+ if (ceil) {
4065
+ verificationCeiling.push({ site: ceil[1].trim(), rung: ceil[2].trim() });
4066
+ continue;
4067
+ }
4068
+ const halt = item.text.match(LEGACY_BUILD_HALT);
4069
+ if (halt) haltReason = halt[1].trim();
4070
+ }
4071
+ const nextFrontier = [];
4072
+ if (head.next?.trim()) {
4073
+ for (const line of head.next.split(";").map((s) => s.trim()).filter(Boolean)) {
4074
+ nextFrontier.push(line);
4075
+ }
4076
+ }
4077
+ return {
4078
+ kind: "build",
4079
+ milestone: head.anchor?.intent,
4080
+ northStarSlug: head.anchor?.slug,
4081
+ doneLastTurn,
4082
+ inProgress,
4083
+ blocked,
4084
+ nextFrontier,
4085
+ tierLedger,
4086
+ verificationCeiling,
4087
+ haltReason
4088
+ };
4089
+ }
4090
+ function parseSnapshot(kind, head) {
4091
+ return kind === "grind" ? parseGrindSnapshot(head) : parseBuildSnapshot(head);
4092
+ }
4093
+ function strOrU(v) {
4094
+ return typeof v === "string" && v.length ? v : void 0;
4095
+ }
4096
+ function numOrU(v) {
4097
+ return typeof v === "number" && Number.isFinite(v) ? v : void 0;
4098
+ }
4099
+ function asStringArray(v) {
4100
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
4101
+ }
4102
+ function normalizeSnapshot(kind, raw) {
4103
+ if (!raw || typeof raw !== "object") throw new Error("snapshot JSON must be an object");
4104
+ const o = raw;
4105
+ if (o.kind !== kind) throw new Error(`snapshot kind ${JSON.stringify(o.kind)} does not match --kind ${kind}`);
4106
+ if (kind === "grind") {
4107
+ return {
4108
+ kind: "grind",
4109
+ class: strOrU(o.class),
4110
+ routing: strOrU(o.routing),
4111
+ ultra: strOrU(o.ultra),
4112
+ criteria: strOrU(o.criteria),
4113
+ phase: strOrU(o.phase),
4114
+ verifyRound: numOrU(o.verifyRound),
4115
+ verifyCap: numOrU(o.verifyCap),
4116
+ openBlockerIds: asStringArray(o.openBlockerIds),
4117
+ resolvedBlockerIds: asStringArray(o.resolvedBlockerIds),
4118
+ verificationCeiling: strOrU(o.verificationCeiling),
4119
+ nextAction: strOrU(o.nextAction)
4120
+ };
4121
+ }
4122
+ 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) })) : [];
4123
+ 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) })) : [];
4124
+ const verificationCeiling = Array.isArray(o.verificationCeiling) ? o.verificationCeiling.filter((v) => !!v && typeof v === "object").map((v) => ({ site: strOrU(v.site) ?? "", rung: strOrU(v.rung) ?? "" })) : [];
4125
+ return {
4126
+ kind: "build",
4127
+ milestone: strOrU(o.milestone),
4128
+ northStarSlug: strOrU(o.northStarSlug),
4129
+ doneLastTurn: asStringArray(o.doneLastTurn),
4130
+ inProgress: asStringArray(o.inProgress),
4131
+ blocked,
4132
+ nextFrontier: asStringArray(o.nextFrontier),
4133
+ tierLedger,
4134
+ verificationCeiling,
4135
+ haltReason: strOrU(o.haltReason)
4136
+ };
4137
+ }
4138
+ function planGrindSnapshotWrite(s, summary = "grind snapshot refresh") {
4139
+ const queueOps = [];
4140
+ for (const id of s.openBlockerIds) queueOps.push({ text: `${GS_OPEN}${id}` });
4141
+ for (const id of s.resolvedBlockerIds) queueOps.push({ text: `${GS_RESOLVED}${id}` });
4142
+ if (s.verificationCeiling) queueOps.push({ text: `${GS_CEILING}${s.verificationCeiling}` });
4143
+ return {
4144
+ summary,
4145
+ next: formatGrindNext(s),
4146
+ anchor: s.criteria,
4147
+ queueOps,
4148
+ clearIndices: []
4149
+ };
4150
+ }
4151
+ function planBuildSnapshotWrite(s, summary = "build snapshot refresh") {
4152
+ const queueOps = [];
4153
+ for (const site of s.doneLastTurn) queueOps.push({ text: `${BS_DONE}${site}` });
4154
+ for (const site of s.inProgress) queueOps.push({ text: `${BS_PROGRESS}${site}` });
4155
+ for (const b of s.blocked) {
4156
+ queueOps.push({ text: `${BS_BLOCKED}${b.site}|${b.decision}${b.issue ? `|${b.issue}` : ""}` });
4157
+ }
4158
+ for (const t of s.tierLedger) {
4159
+ queueOps.push({ text: `${BS_TIER}${t.site}|${t.tier}${t.signals ? `|${t.signals}` : ""}` });
4160
+ }
4161
+ for (const v of s.verificationCeiling) queueOps.push({ text: `${BS_CEILING}${v.site}|${v.rung}` });
4162
+ if (s.haltReason) queueOps.push({ text: `${BS_HALT}${s.haltReason}` });
4163
+ return {
4164
+ summary,
4165
+ next: s.nextFrontier.length ? s.nextFrontier.join("; ") : void 0,
4166
+ anchor: s.milestone,
4167
+ anchorSlug: s.northStarSlug,
4168
+ queueOps,
4169
+ clearIndices: []
4170
+ };
4171
+ }
4172
+ function planSnapshotWrite(snapshot, head, summary) {
4173
+ const plan = snapshot.kind === "grind" ? planGrindSnapshotWrite(snapshot, summary) : planBuildSnapshotWrite(snapshot, summary);
4174
+ plan.clearIndices = snapshotClearIndices(snapshot.kind, head);
4175
+ return plan;
4176
+ }
4177
+ function formatSnapshotHuman(snapshot) {
4178
+ if (snapshot.kind === "grind") {
4179
+ const lines2 = [
4180
+ `kind: grind`,
4181
+ snapshot.class ? `class: ${snapshot.class}` : "",
4182
+ snapshot.routing ? `routing: ${snapshot.routing}` : "",
4183
+ snapshot.ultra ? `ultra: ${snapshot.ultra}` : "",
4184
+ snapshot.criteria ? `criteria: ${snapshot.criteria}` : "",
4185
+ snapshot.phase ? `phase: ${snapshot.phase}` : "",
4186
+ snapshot.verifyRound != null ? `verify: ${snapshot.verifyRound}/${snapshot.verifyCap ?? "?"}` : "",
4187
+ snapshot.openBlockerIds.length ? `open blockers: ${snapshot.openBlockerIds.join(", ")}` : "open blockers: (none)",
4188
+ snapshot.resolvedBlockerIds.length ? `resolved: ${snapshot.resolvedBlockerIds.join(", ")}` : "",
4189
+ snapshot.verificationCeiling ? `ceiling: ${snapshot.verificationCeiling}` : "",
4190
+ snapshot.nextAction ? `next: ${snapshot.nextAction}` : ""
4191
+ ].filter(Boolean);
4192
+ return lines2.join("\n");
4193
+ }
4194
+ const lines = [
4195
+ `kind: build`,
4196
+ snapshot.milestone ? `milestone: ${snapshot.milestone}` : "",
4197
+ snapshot.northStarSlug ? `north star: ${snapshot.northStarSlug}` : "",
4198
+ snapshot.doneLastTurn.length ? `done: ${snapshot.doneLastTurn.join(", ")}` : "",
4199
+ snapshot.inProgress.length ? `in progress: ${snapshot.inProgress.join(", ")}` : "",
4200
+ snapshot.nextFrontier.length ? `next frontier: ${snapshot.nextFrontier.join("; ")}` : "",
4201
+ snapshot.haltReason ? `halt: ${snapshot.haltReason}` : ""
4202
+ ].filter(Boolean);
4203
+ return lines.join("\n");
4204
+ }
4205
+
3903
4206
  // src/hub-auth.ts
3904
4207
  var import_node_crypto = require("node:crypto");
3905
4208
  var import_node_fs5 = require("node:fs");
@@ -4321,6 +4624,60 @@ async function runSagaHealth(o, io = consoleIo) {
4321
4624
  if (report.warnings.length) io.log(report.warnings.map((w) => ` - ${w}`).join("\n"));
4322
4625
  if (report.pendingNotes > 0) io.log(` - ${report.pendingNotes} note(s) queued locally \u2014 \`mmi-cli saga flush\` to roll forward`);
4323
4626
  }
4627
+ async function fetchSagaHead(io = consoleIo) {
4628
+ const cfg = await loadConfig();
4629
+ if (!cfg.sagaApiUrl) {
4630
+ io.err("saga snapshot: Hub API URL not configured");
4631
+ return null;
4632
+ }
4633
+ try {
4634
+ const key = await sagaKey(cfg);
4635
+ const qs = new URLSearchParams(key).toString();
4636
+ const res = await fetch(`${cfg.sagaApiUrl}/saga/state?${qs}`, { headers: await hubHeaders(), signal: AbortSignal.timeout(8e3) });
4637
+ if (!res.ok) {
4638
+ io.err(`saga snapshot: HTTP ${res.status}`);
4639
+ return null;
4640
+ }
4641
+ const state = await res.json();
4642
+ return state.head ?? {};
4643
+ } catch (e) {
4644
+ io.err(`saga snapshot: ${e.message}`);
4645
+ return null;
4646
+ }
4647
+ }
4648
+ async function postSnapshotNotes(plan, anchorForce) {
4649
+ const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
4650
+ const evidence = { sha: sha || void 0, branch: key.branch };
4651
+ for (const idx of [...plan.clearIndices].sort((a, b) => b - a)) {
4652
+ const cap = buildNoteCapture("snapshot retire", { queueDone: String(idx) }, (0, import_node_crypto3.randomUUID)(), evidence);
4653
+ await postCapture(cap);
4654
+ }
4655
+ const primary = buildNoteCapture(plan.summary, {
4656
+ next: plan.next,
4657
+ anchor: plan.anchor,
4658
+ anchorSlug: plan.anchorSlug,
4659
+ anchorForce
4660
+ }, (0, import_node_crypto3.randomUUID)(), evidence);
4661
+ await postCapture(primary);
4662
+ for (const op of plan.queueOps) {
4663
+ const cap = buildNoteCapture("snapshot checklist", { queueAdd: op.text }, (0, import_node_crypto3.randomUUID)(), evidence);
4664
+ await postCapture(cap);
4665
+ }
4666
+ }
4667
+ async function runSagaSnapshotShow(opts, io = consoleIo) {
4668
+ const head = await fetchSagaHead(io);
4669
+ if (!head) return;
4670
+ const snapshot = parseSnapshot(opts.kind, head);
4671
+ if (opts.json) return io.log(JSON.stringify(snapshot, null, 2));
4672
+ io.log(formatSnapshotHuman(snapshot));
4673
+ }
4674
+ async function runSagaSnapshotSet(snapshot, opts = {}, io = consoleIo) {
4675
+ const head = await fetchSagaHead(io) ?? { queued: [] };
4676
+ const plan = planSnapshotWrite(snapshot, head);
4677
+ await postSnapshotNotes(plan, opts.anchorForce);
4678
+ if (opts.json) return io.log(JSON.stringify({ ok: true, snapshot, retired: plan.clearIndices.length, queued: plan.queueOps.length + 1 }));
4679
+ io.log(`saga snapshot: wrote ${snapshot.kind} snapshot (retired ${plan.clearIndices.length}, ${plan.queueOps.length + 1} capture(s))`);
4680
+ }
4324
4681
  function registerSagaCommands(program2) {
4325
4682
  const saga = program2.command("saga").description("per-session continuity");
4326
4683
  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) => {
@@ -4386,6 +4743,69 @@ function registerSagaCommands(program2) {
4386
4743
  console.log(`project=${key.project} branch=${key.branch} session=${key.sessionId} source=${source}`);
4387
4744
  });
4388
4745
  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));
4746
+ const snapshot = saga.command("snapshot").description("structured grind/build resume snapshot over saga HEAD");
4747
+ 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) => {
4748
+ const kind = o.kind;
4749
+ if (kind !== "grind" && kind !== "build") return fail("saga snapshot show: --kind must be grind or build");
4750
+ await runSagaSnapshotShow({ kind, json: o.json });
4751
+ });
4752
+ 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) => {
4753
+ const kind = o.kind;
4754
+ if (kind !== "grind" && kind !== "build") return fail("saga snapshot set: --kind must be grind or build");
4755
+ let snap;
4756
+ if (o.jsonFile) {
4757
+ const raw = await (0, import_promises2.readFile)(o.jsonFile, "utf8");
4758
+ let parsed;
4759
+ try {
4760
+ parsed = JSON.parse(raw);
4761
+ } catch (e) {
4762
+ return fail(`saga snapshot set: invalid JSON in ${o.jsonFile}: ${e.message}`);
4763
+ }
4764
+ try {
4765
+ snap = normalizeSnapshot(kind, parsed);
4766
+ } catch (e) {
4767
+ return fail(`saga snapshot set: ${e.message}`);
4768
+ }
4769
+ } else if (kind === "grind") {
4770
+ snap = {
4771
+ kind: "grind",
4772
+ class: o.class,
4773
+ routing: o.routing,
4774
+ ultra: o.ultra,
4775
+ criteria: o.criteria,
4776
+ phase: o.phase,
4777
+ verifyRound: o.verifyRound,
4778
+ verifyCap: o.verifyCap,
4779
+ openBlockerIds: o.openBlocker ?? [],
4780
+ resolvedBlockerIds: o.resolvedBlocker ?? [],
4781
+ verificationCeiling: o.verificationCeiling,
4782
+ nextAction: o.nextAction
4783
+ };
4784
+ } else {
4785
+ snap = {
4786
+ kind: "build",
4787
+ milestone: o.milestone,
4788
+ northStarSlug: o.northStarSlug,
4789
+ doneLastTurn: o.done ?? [],
4790
+ inProgress: o.inProgress ?? [],
4791
+ blocked: (o.blocked ?? []).map((spec) => {
4792
+ const [site, decision, issue] = spec.split("|");
4793
+ return { site: site ?? "", decision: decision ?? "", issue };
4794
+ }),
4795
+ nextFrontier: o.nextFrontier ?? [],
4796
+ tierLedger: (o.tier ?? []).map((spec) => {
4797
+ const [site, tier, signals] = spec.split("|");
4798
+ return { site: site ?? "", tier: tier ?? "", signals };
4799
+ }),
4800
+ verificationCeiling: (o.siteCeiling ?? []).map((spec) => {
4801
+ const [site, rung] = spec.split("|");
4802
+ return { site: site ?? "", rung: rung ?? "" };
4803
+ }),
4804
+ haltReason: o.haltReason
4805
+ };
4806
+ }
4807
+ await runSagaSnapshotSet(snap, { anchorForce: o.anchorForce, json: o.json });
4808
+ });
4389
4809
  }
4390
4810
 
4391
4811
  // src/saga-boot.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.32.4",
3
+ "version": "2.34.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",