@mutmutco/cli 2.28.0 → 2.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/saga.cjs CHANGED
@@ -3430,6 +3430,26 @@ function clientVersionHeaders() {
3430
3430
 
3431
3431
  // src/saga-commands.ts
3432
3432
  var import_node_crypto3 = require("node:crypto");
3433
+ var import_promises2 = require("node:fs/promises");
3434
+
3435
+ // src/issue-body.ts
3436
+ async function resolveTextArg(input, deps, labels) {
3437
+ const hasValue = input.value !== void 0;
3438
+ const hasFile = input.file !== void 0;
3439
+ if (hasValue && hasFile) {
3440
+ throw new Error(`pass only one of ${labels.value} or ${labels.file}`);
3441
+ }
3442
+ if (!hasValue && !hasFile) {
3443
+ throw new Error(`pass ${labels.value} or ${labels.file}`);
3444
+ }
3445
+ if (hasValue) return input.value ?? "";
3446
+ const source = input.file ?? "";
3447
+ const text = source === "-" ? await deps.readStdin() : await deps.readFile(source, "utf8");
3448
+ if (text.trim().length === 0) {
3449
+ throw new Error(`${labels.file} produced an empty ${labels.noun}`);
3450
+ }
3451
+ return text;
3452
+ }
3433
3453
 
3434
3454
  // src/saga-capture.ts
3435
3455
  function parseHookInput(stdin) {
@@ -3566,6 +3586,7 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
3566
3586
 
3567
3587
  // src/saga-health.ts
3568
3588
  var MEMORY_STALE_DAYS = 14;
3589
+ var SESSION_START_LIVENESS = { attempts: 1, timeoutMs: 3e3 };
3569
3590
  function buildHealth(i) {
3570
3591
  const problems = [];
3571
3592
  if (!i.sagaApiUrl) problems.push("Hub API URL not configured");
@@ -3591,6 +3612,7 @@ function buildHealth(i) {
3591
3612
  authorized: i.authorized,
3592
3613
  sagaApiUrl: i.sagaApiUrl,
3593
3614
  pendingNotes: i.pendingNotes ?? 0,
3615
+ honchoPending: i.honchoPending ?? 0,
3594
3616
  key: i.key,
3595
3617
  source: i.source,
3596
3618
  problems,
@@ -3607,73 +3629,14 @@ function healthBanner(report) {
3607
3629
  if (report.warnings.length) return `saga health: NOTE - ${report.warnings.join("; ")}`;
3608
3630
  return null;
3609
3631
  }
3610
- function resumeCue() {
3611
- return '> STATUS/RESUME CUE \u2014 For any status, resume, or "where do I stand" report: read THIS saga HEAD first (`mmi-cli saga show`), then reconcile its NEXT / LAST 5 / DECISIONS against the live board + git/gh before reporting. Do not rebuild the picture from board/issues/memory while skipping the HEAD. PRECEDENCE: the HEAD is prior-session belief and MAY BE SUPERSEDED \u2014 the current live user/master instruction WINS over any conflicting HEAD anchor, NEXT, or checklist; follow the live instruction and treat the stale HEAD item as superseded.';
3612
- }
3613
-
3614
- // src/fetch-retry.ts
3615
- async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3616
- const attempts = opts.attempts ?? 3;
3617
- const baseDelayMs = opts.baseDelayMs ?? 250;
3618
- const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
3619
- const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3620
- let lastErr;
3621
- for (let i = 0; i < attempts; i++) {
3622
- const isLast = i === attempts - 1;
3623
- const attemptInit = opts.timeoutMs ? { ...init, signal: AbortSignal.timeout(opts.timeoutMs) } : init;
3624
- try {
3625
- const res = await fetchImpl(url, attemptInit);
3626
- if (!isLast && retryOn(res)) {
3627
- await sleep(baseDelayMs * 2 ** i);
3628
- continue;
3629
- }
3630
- return res;
3631
- } catch (e) {
3632
- lastErr = e;
3633
- if (isLast) throw e;
3634
- await sleep(baseDelayMs * 2 ** i);
3635
- }
3636
- }
3637
- throw lastErr;
3638
- }
3639
-
3640
- // src/saga-note.ts
3641
- var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
3642
- var ROUTE_LEVEL_403 = "saga API route-level 403 from HubSessionAuthorizer/session policy";
3643
- function agentSurface(env = process.env) {
3644
- const surface = env.MMI_AGENT_SURFACE?.trim() || (env.CODEX_THREAD_ID?.trim() && !env.CLAUDE_SESSION_ID?.trim() ? "codex" : "claude");
3645
- if (AGENT_SURFACE_TOKENS.includes(surface)) return surface;
3646
- throw new Error(`MMI_AGENT_SURFACE must be one of: ${AGENT_SURFACE_TOKENS.join(", ")}`);
3647
- }
3648
- function buildNoteCapture(summary, o, id, evidence) {
3649
- const queueOp = o.queueAdd ? { op: "add", text: o.queueAdd } : o.queueDone != null ? { op: "done", index: Number(o.queueDone) } : void 0;
3650
- const state = o.diagnostic ? "diagnostic" : o.verified ? "verified" : "asserted";
3651
- const source = o.diagnostic ? "probe" : "note";
3652
- const ev = {};
3653
- if (evidence.sha) ev.sha = evidence.sha;
3654
- if (evidence.branch) ev.branch = evidence.branch;
3655
- if (evidence.pr) ev.pr = evidence.pr;
3656
- if (evidence.file) ev.file = evidence.file;
3657
- const anchor = o.anchor ? { intent: o.anchor, setAt: (/* @__PURE__ */ new Date()).toISOString() } : void 0;
3658
- return {
3659
- event: "note",
3660
- id,
3661
- summary,
3662
- next: o.next,
3663
- decision: o.decision,
3664
- queueOp,
3665
- state,
3666
- source,
3667
- evidence: Object.keys(ev).length ? ev : void 0,
3668
- surface: agentSurface(),
3669
- supersedes: o.supersedes,
3670
- anchor,
3671
- anchorForce: o.anchorForce || void 0
3672
- };
3673
- }
3674
- function formatCaptureFailure(status, message) {
3675
- if (status === 403 && message === "Forbidden") return `saga: ${ROUTE_LEVEL_403} (HTTP 403)`;
3676
- return `saga: HTTP ${status}`;
3632
+ function memorySyncBanner(report) {
3633
+ const saga = report.pendingNotes;
3634
+ const honcho = report.honchoPending;
3635
+ if (saga <= 0 && honcho <= 0) return null;
3636
+ const parts = [];
3637
+ if (saga > 0) parts.push(`${saga} saga`);
3638
+ if (honcho > 0) parts.push(`${honcho} honcho`);
3639
+ return `MEMORY SYNC \u2014 ${parts.join(" + ")} write(s) queued locally \u2014 run \`mmi-cli saga flush\` / \`honcho flush\` (this device only).`;
3677
3640
  }
3678
3641
 
3679
3642
  // src/saga-pending.ts
@@ -3852,6 +3815,81 @@ async function flushPending(post, dir = ".mmi") {
3852
3815
  return { flushed, dropped, remaining: readPending(dir).length };
3853
3816
  }
3854
3817
 
3818
+ // src/honcho-pending.ts
3819
+ var HONCHO_QUEUE_DIR = ".mmi/honcho";
3820
+ function readHonchoPending(dir = HONCHO_QUEUE_DIR) {
3821
+ return readPending(dir);
3822
+ }
3823
+
3824
+ // src/fetch-retry.ts
3825
+ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3826
+ const attempts = opts.attempts ?? 3;
3827
+ const baseDelayMs = opts.baseDelayMs ?? 250;
3828
+ const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
3829
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3830
+ let lastErr;
3831
+ for (let i = 0; i < attempts; i++) {
3832
+ const isLast = i === attempts - 1;
3833
+ const attemptInit = opts.timeoutMs ? { ...init, signal: AbortSignal.timeout(opts.timeoutMs) } : init;
3834
+ try {
3835
+ const res = await fetchImpl(url, attemptInit);
3836
+ if (!isLast && retryOn(res)) {
3837
+ await sleep(baseDelayMs * 2 ** i);
3838
+ continue;
3839
+ }
3840
+ return res;
3841
+ } catch (e) {
3842
+ lastErr = e;
3843
+ if (isLast) throw e;
3844
+ await sleep(baseDelayMs * 2 ** i);
3845
+ }
3846
+ }
3847
+ throw lastErr;
3848
+ }
3849
+
3850
+ // src/saga-note.ts
3851
+ var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
3852
+ var ROUTE_LEVEL_403 = "saga API route-level 403 from HubSessionAuthorizer/session policy";
3853
+ function agentSurface(env = process.env) {
3854
+ const surface = env.MMI_AGENT_SURFACE?.trim() || (env.CODEX_THREAD_ID?.trim() && !env.CLAUDE_SESSION_ID?.trim() ? "codex" : "claude");
3855
+ if (AGENT_SURFACE_TOKENS.includes(surface)) return surface;
3856
+ throw new Error(`MMI_AGENT_SURFACE must be one of: ${AGENT_SURFACE_TOKENS.join(", ")}`);
3857
+ }
3858
+ function buildNoteCapture(summary, o, id, evidence) {
3859
+ const queueOp = o.queueAdd ? { op: "add", text: o.queueAdd } : o.queueDone != null ? { op: "done", index: Number(o.queueDone) } : void 0;
3860
+ const state = o.diagnostic ? "diagnostic" : o.verified ? "verified" : "asserted";
3861
+ const source = o.diagnostic ? "probe" : "note";
3862
+ const ev = {};
3863
+ if (evidence.sha) ev.sha = evidence.sha;
3864
+ if (evidence.branch) ev.branch = evidence.branch;
3865
+ if (evidence.pr) ev.pr = evidence.pr;
3866
+ if (evidence.file) ev.file = evidence.file;
3867
+ const anchor = o.anchor ? {
3868
+ intent: o.anchor,
3869
+ ...o.anchorSlug ? { slug: o.anchorSlug } : {},
3870
+ setAt: (/* @__PURE__ */ new Date()).toISOString()
3871
+ } : void 0;
3872
+ return {
3873
+ event: "note",
3874
+ id,
3875
+ summary,
3876
+ next: o.next,
3877
+ decision: o.decision,
3878
+ queueOp,
3879
+ state,
3880
+ source,
3881
+ evidence: Object.keys(ev).length ? ev : void 0,
3882
+ surface: agentSurface(),
3883
+ supersedes: o.supersedes,
3884
+ anchor,
3885
+ anchorForce: o.anchorForce || void 0
3886
+ };
3887
+ }
3888
+ function formatCaptureFailure(status, message) {
3889
+ if (status === 403 && message === "Forbidden") return `saga: ${ROUTE_LEVEL_403} (HTTP 403)`;
3890
+ return `saga: HTTP ${status}`;
3891
+ }
3892
+
3855
3893
  // src/hub-auth.ts
3856
3894
  var import_node_crypto = require("node:crypto");
3857
3895
  var import_node_fs5 = require("node:fs");
@@ -4147,6 +4185,13 @@ async function runNote(summary, o) {
4147
4185
  const capture = buildNoteCapture(summary, o, (0, import_node_crypto3.randomUUID)(), { sha: sha || void 0, branch: key.branch });
4148
4186
  await postCapture(capture);
4149
4187
  }
4188
+ function resolveSummary(summary, o) {
4189
+ return resolveTextArg({ value: summary, file: o.messageFile }, { readFile: import_promises2.readFile, readStdin }, {
4190
+ value: "a summary argument",
4191
+ file: "--message-file",
4192
+ noun: "message"
4193
+ });
4194
+ }
4150
4195
  async function runSagaFlush(o, io = consoleIo) {
4151
4196
  if (o.run) {
4152
4197
  try {
@@ -4181,7 +4226,6 @@ async function runSagaShow(opts, io = consoleIo) {
4181
4226
  const qs = opts.latestAnywhere ? "scope=anywhere" : new URLSearchParams({ project: key.project, branch: key.branch }).toString();
4182
4227
  const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await hubHeaders() }, { attempts: 2, timeoutMs: 3e3 });
4183
4228
  if (res.ok) {
4184
- io.log(resumeCue());
4185
4229
  return io.log(await res.text());
4186
4230
  }
4187
4231
  if (!opts.quiet) io.log(`saga show failed: HTTP ${res.status}`);
@@ -4192,9 +4236,9 @@ async function runSagaShow(opts, io = consoleIo) {
4192
4236
  }
4193
4237
  }
4194
4238
  }
4195
- async function probeBackend(url) {
4239
+ async function probeBackend(url, opts = { attempts: 3, timeoutMs: 4e3 }) {
4196
4240
  try {
4197
- const res = await fetchWithRetry(fetch, `${url}/saga/head`, { headers: await hubHeaders() }, { attempts: 3, timeoutMs: 4e3 });
4241
+ const res = await fetchWithRetry(fetch, `${url}/saga/head`, { headers: await hubHeaders() }, opts);
4198
4242
  let message = "";
4199
4243
  try {
4200
4244
  const body = await res.clone().json();
@@ -4233,12 +4277,13 @@ async function runSagaHealth(o, io = consoleIo) {
4233
4277
  const session = resolveSessionId();
4234
4278
  const key = await sagaKey(cfg, session);
4235
4279
  const source = session.source;
4280
+ const livenessOpts = o.banner ? SESSION_START_LIVENESS : { attempts: 3, timeoutMs: 4e3 };
4236
4281
  const [identity, liveness] = await Promise.all([
4237
4282
  hubAuthSession({ baseUrl: cfg.sagaApiUrl ?? defaultHubUrl(), githubToken }).then((s) => s?.login),
4238
- cfg.sagaApiUrl ? probeBackend(cfg.sagaApiUrl) : Promise.resolve({ reachable: false })
4283
+ cfg.sagaApiUrl ? probeBackend(cfg.sagaApiUrl, livenessOpts) : Promise.resolve({ reachable: false })
4239
4284
  ]);
4240
- const authorized = cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
4241
- const memoryAgeDays = cfg.sagaApiUrl && liveness.reachable ? await fetchMemoryAge(cfg.sagaApiUrl, key.project) : void 0;
4285
+ const authorized = o.banner ? void 0 : cfg.sagaApiUrl && liveness.reachable ? await probeSagaAccess(cfg.sagaApiUrl, key) : void 0;
4286
+ const memoryAgeDays = o.banner ? void 0 : cfg.sagaApiUrl && liveness.reachable ? await fetchMemoryAge(cfg.sagaApiUrl, key.project) : void 0;
4242
4287
  const report = buildHealth({
4243
4288
  key,
4244
4289
  source,
@@ -4249,12 +4294,15 @@ async function runSagaHealth(o, io = consoleIo) {
4249
4294
  authorized,
4250
4295
  sagaApiUrl: cfg.sagaApiUrl,
4251
4296
  pendingNotes: readPending().length,
4297
+ honchoPending: readHonchoPending().length,
4252
4298
  memoryAgeDays
4253
4299
  });
4254
4300
  if (o.json) return io.log(JSON.stringify(report));
4255
4301
  if (o.banner) {
4256
4302
  const banner = healthBanner(report);
4257
4303
  if (banner) io.log(banner);
4304
+ const sync = memorySyncBanner(report);
4305
+ if (sync) io.log(sync);
4258
4306
  return;
4259
4307
  }
4260
4308
  if (o.quiet) return;
@@ -4265,8 +4313,24 @@ async function runSagaHealth(o, io = consoleIo) {
4265
4313
  }
4266
4314
  function registerSagaCommands(program2) {
4267
4315
  const saga = program2.command("saga").description("per-session continuity");
4268
- 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-force", "overwrite an existing anchor").action((summary, o) => runNote(summary, o));
4269
- saga.command("probe <summary>").description("record a diagnostic probe note (alias for `saga note --diagnostic`)").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").action((summary, o) => runNote(summary, { ...o, diagnostic: true }));
4316
+ 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) => {
4317
+ let text;
4318
+ try {
4319
+ text = await resolveSummary(summary, o);
4320
+ } catch (e) {
4321
+ return fail(`saga note: ${e.message}`);
4322
+ }
4323
+ await runNote(text, o);
4324
+ });
4325
+ saga.command("probe [summary]").description("record a diagnostic probe note (alias for `saga note --diagnostic`)").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("--message-file <path|->", "read the summary from a UTF-8 file, or from stdin with - (avoids cmd.exe quoting)").action(async (summary, o) => {
4326
+ let text;
4327
+ try {
4328
+ text = await resolveSummary(summary, o);
4329
+ } catch (e) {
4330
+ return fail(`saga probe: ${e.message}`);
4331
+ }
4332
+ await runNote(text, { ...o, diagnostic: true });
4333
+ });
4270
4334
  saga.command("flush").option("--json", "machine-readable {flushed, dropped, remaining}").option("--run", "detached worker: drain the queue silently (spawned by note/capture)").description("roll the local pending-note queue forward (re-POST queued saga writes); reports what landed").action((o) => runSagaFlush(o));
4271
4335
  saga.command("show").option("--quiet", "no-op silently when unconfigured/unreachable (SessionStart hook)").option("--latest-anywhere", "resume the newest saga across all repos (default: current repo)").description("print your resume block \u2014 current repo HEAD + project memory (where you left off)").action((opts) => runSagaShow(opts));
4272
4336
  saga.command("capture").option("--quiet", "capture silently (for the Stop hook)").description("per-turn deterministic capture (Stop hook): turn boundary + current sha + gated HEAD-update").action(async (opts) => {
@@ -4380,8 +4444,16 @@ function socketPath(env = process.env, platform = process.platform, user) {
4380
4444
  return platform === "win32" ? `\\\\.\\pipe\\mmi-cli-${hash}` : (0, import_node_path6.join)(daemonDir(env), `mmi-cli-${hash}.sock`);
4381
4445
  }
4382
4446
  var HOT_VERBS = /* @__PURE__ */ new Set(["note", "probe", "capture", "session", "head-update"]);
4447
+ function argvReadsStdin(args) {
4448
+ for (let i = 0; i < args.length; i++) {
4449
+ const a = args[i];
4450
+ if (a.endsWith("-file=-")) return true;
4451
+ if (a.endsWith("-file") && args[i + 1] === "-") return true;
4452
+ }
4453
+ return false;
4454
+ }
4383
4455
  function daemonEligible(args) {
4384
- return args[0] === "saga" && HOT_VERBS.has(args[1] ?? "") && !args.includes("--run") && !args.includes("--help") && !args.includes("-h");
4456
+ return args[0] === "saga" && HOT_VERBS.has(args[1] ?? "") && !args.includes("--run") && !args.includes("--help") && !args.includes("-h") && !argvReadsStdin(args);
4385
4457
  }
4386
4458
  function buildStamp(version, bundleMtimeMs) {
4387
4459
  return `${version}#${Math.trunc(bundleMtimeMs)}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.28.0",
3
+ "version": "2.30.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",
@@ -39,7 +39,7 @@
39
39
  },
40
40
  "devDependencies": {
41
41
  "@types/node": "^22.0.0",
42
- "esbuild": "^0.28.0",
42
+ "esbuild": "^0.28.1",
43
43
  "typescript": "^5.7.0",
44
44
  "vitest": "^4.1.0"
45
45
  }