@mutmutco/cli 2.23.0 → 2.24.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/README.md CHANGED
@@ -29,9 +29,12 @@ mmi-cli doctor --json
29
29
 
30
30
  - `mmi-cli rules sync` delivers the org-owned `AGENTS.md`, `CLAUDE.md`, and Claude settings files.
31
31
  - `mmi-cli docs sync` refreshes repo-owned `README.md` and `architecture.md` without clobbering dirty files.
32
- - `mmi-cli saga note`, `saga show`, `saga health`, `saga session`, `saga capture`, and `saga head-update` write and inspect session continuity through a cached Hub session token.
32
+ - `mmi-cli saga note`, `saga show`, `saga health`, `saga session`, `saga capture`, and `saga head-update` write and inspect session continuity through a cached Hub session token. Saga writes are local-first: a transient server miss queues the note in a local pending file and a detached flush worker delivers it — a queued note is normal, not a failure.
33
33
  - `mmi-cli kb get` and `kb list` read the MM KB source (`kb list [prefix]` lists document paths, optionally under a prefix).
34
- - `mmi-cli northstar push|pull|list|delete|graduate` manages North Star, the per-user plan/SSOT store.
34
+ - `mmi-cli northstar push|pull|list|status|sync|delete|graduate` manages North Star, the per-user plan/SSOT store.
35
+ `northstar push` is async by default — it queues a durable background push and prints a "queued" line,
36
+ which is the expected success path. `northstar status` shows pending/conflicted pushes; `northstar sync`
37
+ (or `push --wait`) gives durable server confirmation. Never treat a queued push as failure.
35
38
  `northstar graduate <slug> --merged-pr <url-or-number> --org-visible` marks a built-and-merged plan for
36
39
  KB curation without echoing the plan body.
37
40
  `mmi-cli plan` remains a compatibility alias.
package/dist/main.cjs CHANGED
@@ -3582,6 +3582,20 @@ var FLUSH_LOCK_STALE_MS = 5 * 6e4;
3582
3582
  function pendingPath(dir = ".mmi") {
3583
3583
  return (0, import_node_path3.join)(dir, PENDING_FILE);
3584
3584
  }
3585
+ var PENDING_TMP_STALE_MS = 6e4;
3586
+ function sweepStaleTmp(dir = ".mmi", now = Date.now()) {
3587
+ try {
3588
+ for (const name of (0, import_node_fs3.readdirSync)(dir)) {
3589
+ if (!name.startsWith(`${PENDING_FILE}.`) || !name.endsWith(".tmp")) continue;
3590
+ const path2 = (0, import_node_path3.join)(dir, name);
3591
+ try {
3592
+ if (now - (0, import_node_fs3.statSync)(path2).mtimeMs > PENDING_TMP_STALE_MS) (0, import_node_fs3.unlinkSync)(path2);
3593
+ } catch {
3594
+ }
3595
+ }
3596
+ } catch {
3597
+ }
3598
+ }
3585
3599
  function flushLockPath(dir = ".mmi") {
3586
3600
  return (0, import_node_path3.join)(dir, FLUSH_LOCK_FILE);
3587
3601
  }
@@ -3625,15 +3639,20 @@ function readPending(dir = ".mmi") {
3625
3639
  return out;
3626
3640
  }
3627
3641
  function writePending(entries, dir = ".mmi") {
3642
+ const tmp = `${pendingPath(dir)}.${process.pid}.tmp`;
3628
3643
  try {
3629
3644
  (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
3645
+ sweepStaleTmp(dir);
3630
3646
  const trimmed = entries.slice(-PENDING_MAX);
3631
3647
  const body = trimmed.map((e) => JSON.stringify(e)).join("\n");
3632
- const tmp = `${pendingPath(dir)}.${process.pid}.tmp`;
3633
3648
  (0, import_node_fs3.writeFileSync)(tmp, trimmed.length ? `${body}
3634
3649
  ` : "", "utf8");
3635
3650
  (0, import_node_fs3.renameSync)(tmp, pendingPath(dir));
3636
3651
  } catch {
3652
+ try {
3653
+ (0, import_node_fs3.unlinkSync)(tmp);
3654
+ } catch {
3655
+ }
3637
3656
  }
3638
3657
  }
3639
3658
  function enqueuePending(body, dir = ".mmi") {
@@ -3646,6 +3665,7 @@ function enqueuePending(body, dir = ".mmi") {
3646
3665
  return;
3647
3666
  }
3648
3667
  (0, import_node_fs3.mkdirSync)(dir, { recursive: true });
3668
+ sweepStaleTmp(dir);
3649
3669
  (0, import_node_fs3.appendFileSync)(pendingPath(dir), `${JSON.stringify({ id, body })}
3650
3670
  `, "utf8");
3651
3671
  } catch {
@@ -6423,7 +6443,8 @@ var MANAGED_GITIGNORE_LINES = [
6423
6443
  ".claude/worktrees/",
6424
6444
  ".mmi/.session",
6425
6445
  ".mmi/head-ts/",
6426
- ".mmi/saga-pending.jsonl",
6446
+ ".mmi/saga-pending.jsonl*",
6447
+ ".mmi/saga-flush.lock",
6427
6448
  ".aws-sam/",
6428
6449
  "/*.png"
6429
6450
  ];
@@ -7579,10 +7600,37 @@ async function resolveRcResumeTag(deps, base, sha) {
7579
7600
  const note = leftovers.length > 0 ? `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} (newest of ${sorted.length}); harmless leftover tag(s) on the same SHA: ${leftovers.join(", ")}` : `resuming existing RC tag ${newest} already on ${sha.slice(0, 7)} instead of minting a fresh rc`;
7580
7601
  return { tag: newest, note };
7581
7602
  }
7582
- async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha) {
7603
+ var DISPATCH_ATTEMPTS = 3;
7604
+ var DISPATCH_RETRY_DELAY_MS = 2e3;
7605
+ function isTransientDispatchFailure(e) {
7606
+ const msg = e instanceof Error ? e.message : String(e);
7607
+ return /timed? ?out|timeout|aborted|network|fetch failed|ECONNRESET|ECONNREFUSED|EAI_AGAIN/i.test(msg);
7608
+ }
7609
+ async function dispatchTenantDeployWithRetry(deps, input) {
7610
+ const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
7611
+ for (let attempt = 1; ; attempt++) {
7612
+ try {
7613
+ await deps.dispatchTenantDeploy(input);
7614
+ return;
7615
+ } catch (e) {
7616
+ if (attempt >= DISPATCH_ATTEMPTS || !isTransientDispatchFailure(e)) throw e;
7617
+ await sleep(DISPATCH_RETRY_DELAY_MS * attempt);
7618
+ }
7619
+ }
7620
+ }
7621
+ async function dispatchDeploy(deps, ctx, stage2, ref, model, watch, autoRunSince, autoRunHeadSha, dispatchFailure = "throw") {
7583
7622
  if (model === "tenant-container" || model === "solo-container") {
7584
7623
  const since = (deps.now ?? Date.now)();
7585
- await deps.dispatchTenantDeploy({ repo: ctx.repo, slug: ctx.slug, ref, stage: stage2 });
7624
+ try {
7625
+ await dispatchTenantDeployWithRetry(deps, { repo: ctx.repo, slug: ctx.slug, ref, stage: stage2 });
7626
+ } catch (e) {
7627
+ if (dispatchFailure === "throw") throw e;
7628
+ const msg = e instanceof Error ? e.message : String(e);
7629
+ return {
7630
+ note: `tenant-deploy dispatch FAILED: ${msg}. The promotion itself landed (merge/tag/Release pushed before the dispatch) \u2014 recover the deploy with \`mmi-cli tenant redeploy ${ctx.repo} ${stage2}\`; never re-tag.`,
7631
+ deployStatus: "failure"
7632
+ };
7633
+ }
7586
7634
  const { runId, runUrl } = await correlateTenantRun(deps, since);
7587
7635
  const deployStatus = watch ? await watchTenantRun(deps, runId) : "pending";
7588
7636
  return { note: `dispatched tenant-deploy.yml (slug=${ctx.slug}, ref=${ref}, stage=${stage2})`, runId, runUrl, deployStatus };
@@ -7716,7 +7764,7 @@ async function runTrainApply(command, deps, options = {}) {
7716
7764
  const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
7717
7765
  const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
7718
7766
  const autoRunSince2 = (deps.now ?? Date.now)();
7719
- const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2);
7767
+ const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2, "report");
7720
7768
  await deps.run("git", ["checkout", "development"]);
7721
7769
  await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
7722
7770
  await deps.run("git", ["merge", "main", "--no-edit"]);
@@ -7787,7 +7835,7 @@ async function runTrainApply(command, deps, options = {}) {
7787
7835
  const releaseUrl2 = clean(await deps.run("gh", ["release", "create", tag2, "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
7788
7836
  const announceNote2 = deps.announce ? (await deps.announce({ repo: ctx.repo, tag: tag2, summaryFile: options.announceSummaryFile })).note : void 0;
7789
7837
  const autoRunSince2 = (deps.now ?? Date.now)();
7790
- const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2);
7838
+ const d2 = await dispatchDeploy(deps, ctx, "main", "main", deployModel2, watch, autoRunSince2, releaseSha2, "report");
7791
7839
  const retirement2 = await retireRcRuntime(deps, ctx, deployModel2, d2.deployStatus, rcShaAtRelease);
7792
7840
  await deps.run("git", ["checkout", "development"]);
7793
7841
  await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
@@ -7867,7 +7915,7 @@ async function runTrainApply(command, deps, options = {}) {
7867
7915
  const releaseUrl = clean(await deps.run("gh", ["release", "create", tag, "--target", "main", "--generate-notes", "--latest", "--repo", ctx.repo])) || void 0;
7868
7916
  await verifyPublishedRelease(deps, ctx.repo, tag, "main", releaseSha);
7869
7917
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
7870
- const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha);
7918
+ const d = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report");
7871
7919
  const retirement = await retireRcRuntime(deps, ctx, deployModel, d.deployStatus, releasedRcSha);
7872
7920
  await deps.run("git", ["checkout", "development"]);
7873
7921
  await deps.run("git", ["pull", "--ff-only", "origin", "development"]);
@@ -9478,6 +9526,7 @@ var PROJECTS_ENVELOPE_KEY = "projects";
9478
9526
  // src/registry-client.ts
9479
9527
  var DEFAULT_TIMEOUT_MS2 = 8e3;
9480
9528
  var WAITED_TENANT_CONTROL_TIMEOUT_MS = 13e3;
9529
+ var TENANT_DEPLOY_TIMEOUT_MS = 12e4;
9481
9530
  var RETRY_ATTEMPTS = 3;
9482
9531
  function retriedFetch(deps, url, init) {
9483
9532
  const headers = { ...clientVersionHeaders(), ...init.headers };
@@ -9617,7 +9666,7 @@ async function tenantControl(payload, deps) {
9617
9666
  return postJson("/tenant-control", payload, deps, "POST", { noRetry, timeoutMs });
9618
9667
  }
9619
9668
  async function tenantDeploy(payload, deps) {
9620
- return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true });
9669
+ return postJson("/tenant-deploy", payload, deps, "POST", { noRetry: true, timeoutMs: TENANT_DEPLOY_TIMEOUT_MS });
9621
9670
  }
9622
9671
 
9623
9672
  // src/tenant-verify-secrets.ts
@@ -10514,7 +10563,7 @@ async function planPush(deps, slug, opts = {}) {
10514
10563
  queue.push({ project: project2, slug, hash: hashContent(normalizeEol(raw)), attempts: 0, queuedAt: deps.now(), ...opts.force ? { force: true } : {} });
10515
10564
  deps.writeQueueRaw(serializeQueue(queue));
10516
10565
  deps.detachSync();
10517
- deps.log(`queued ${slug} for background push (\`mmi-cli northstar status\` to check, --wait to push synchronously)`);
10566
+ deps.log(`queued ${slug} for background push \u2014 expected, not a failure (\`mmi-cli northstar status\` to check; \`mmi-cli northstar sync\` or --wait for durable confirmation)`);
10518
10567
  return true;
10519
10568
  }
10520
10569
  async function planPushNow(deps, slug, opts = {}) {
package/dist/saga.cjs CHANGED
@@ -3681,6 +3681,20 @@ var FLUSH_LOCK_STALE_MS = 5 * 6e4;
3681
3681
  function pendingPath(dir = ".mmi") {
3682
3682
  return (0, import_node_path4.join)(dir, PENDING_FILE);
3683
3683
  }
3684
+ var PENDING_TMP_STALE_MS = 6e4;
3685
+ function sweepStaleTmp(dir = ".mmi", now = Date.now()) {
3686
+ try {
3687
+ for (const name of (0, import_node_fs4.readdirSync)(dir)) {
3688
+ if (!name.startsWith(`${PENDING_FILE}.`) || !name.endsWith(".tmp")) continue;
3689
+ const path2 = (0, import_node_path4.join)(dir, name);
3690
+ try {
3691
+ if (now - (0, import_node_fs4.statSync)(path2).mtimeMs > PENDING_TMP_STALE_MS) (0, import_node_fs4.unlinkSync)(path2);
3692
+ } catch {
3693
+ }
3694
+ }
3695
+ } catch {
3696
+ }
3697
+ }
3684
3698
  function flushLockPath(dir = ".mmi") {
3685
3699
  return (0, import_node_path4.join)(dir, FLUSH_LOCK_FILE);
3686
3700
  }
@@ -3724,15 +3738,20 @@ function readPending(dir = ".mmi") {
3724
3738
  return out;
3725
3739
  }
3726
3740
  function writePending(entries, dir = ".mmi") {
3741
+ const tmp = `${pendingPath(dir)}.${process.pid}.tmp`;
3727
3742
  try {
3728
3743
  (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
3744
+ sweepStaleTmp(dir);
3729
3745
  const trimmed = entries.slice(-PENDING_MAX);
3730
3746
  const body = trimmed.map((e) => JSON.stringify(e)).join("\n");
3731
- const tmp = `${pendingPath(dir)}.${process.pid}.tmp`;
3732
3747
  (0, import_node_fs4.writeFileSync)(tmp, trimmed.length ? `${body}
3733
3748
  ` : "", "utf8");
3734
3749
  (0, import_node_fs4.renameSync)(tmp, pendingPath(dir));
3735
3750
  } catch {
3751
+ try {
3752
+ (0, import_node_fs4.unlinkSync)(tmp);
3753
+ } catch {
3754
+ }
3736
3755
  }
3737
3756
  }
3738
3757
  function enqueuePending(body, dir = ".mmi") {
@@ -3745,6 +3764,7 @@ function enqueuePending(body, dir = ".mmi") {
3745
3764
  return;
3746
3765
  }
3747
3766
  (0, import_node_fs4.mkdirSync)(dir, { recursive: true });
3767
+ sweepStaleTmp(dir);
3748
3768
  (0, import_node_fs4.appendFileSync)(pendingPath(dir), `${JSON.stringify({ id, body })}
3749
3769
  `, "utf8");
3750
3770
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.23.0",
3
+ "version": "2.24.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",