@mutmutco/cli 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +404 -130
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3392,9 +3392,9 @@ function useColor() {
3392
3392
  var program = new Command();
3393
3393
 
3394
3394
  // src/index.ts
3395
- var import_promises = require("node:fs/promises");
3395
+ var import_promises2 = require("node:fs/promises");
3396
3396
  var import_node_fs5 = require("node:fs");
3397
- var import_node_crypto = require("node:crypto");
3397
+ var import_node_crypto2 = require("node:crypto");
3398
3398
 
3399
3399
  // src/rules-sync.ts
3400
3400
  function normalizeEol(s) {
@@ -3468,6 +3468,25 @@ async function runSessionStart(parallel, sequential, io) {
3468
3468
  for (const lines of buffered) flush(lines, io);
3469
3469
  for (const step of sequential) flush(await runBufferedStep(step), io);
3470
3470
  }
3471
+ function buildSessionStartPlan(verbs) {
3472
+ return {
3473
+ parallel: [
3474
+ { name: "rules sync", run: verbs.rulesSync },
3475
+ { name: "saga show", run: verbs.sagaShow },
3476
+ { name: "saga health", run: verbs.sagaHealth }
3477
+ ],
3478
+ sequential: [{ name: "doctor", run: verbs.doctor }]
3479
+ };
3480
+ }
3481
+ function spawnDetachedSelf(args, deps) {
3482
+ try {
3483
+ deps.spawn(deps.execPath, [deps.scriptPath, ...args], { detached: true, stdio: "ignore", windowsHide: true }).unref();
3484
+ } catch {
3485
+ }
3486
+ }
3487
+ function northstarPointer() {
3488
+ return "North Stars: run `mmi-cli northstar relevant` to load plans relevant to your task (`northstar list` for all).";
3489
+ }
3471
3490
 
3472
3491
  // src/saga-capture.ts
3473
3492
  function parseHookInput(stdin) {
@@ -3482,8 +3501,8 @@ function parseHookInput(stdin) {
3482
3501
  // src/index.ts
3483
3502
  var import_node_child_process6 = require("node:child_process");
3484
3503
  var import_node_util6 = require("node:util");
3485
- var import_node_path5 = require("node:path");
3486
- var import_node_os = require("node:os");
3504
+ var import_node_path6 = require("node:path");
3505
+ var import_node_os2 = require("node:os");
3487
3506
 
3488
3507
  // src/saga-head-maintainer.ts
3489
3508
  var import_node_child_process2 = require("node:child_process");
@@ -3608,6 +3627,12 @@ async function runHeadEngine(prompt, timeoutMs = HEAD_ENGINE_TIMEOUT_MS) {
3608
3627
  });
3609
3628
  }
3610
3629
 
3630
+ // src/gh-create.ts
3631
+ var import_promises = require("node:fs/promises");
3632
+ var import_node_os = require("node:os");
3633
+ var import_node_path3 = require("node:path");
3634
+ var import_node_crypto = require("node:crypto");
3635
+
3611
3636
  // src/board-priority.ts
3612
3637
  var BOARD_PRIORITY_NAMES = ["Urgent", "High", "Medium", "Low"];
3613
3638
  var CLI_PRIORITIES = ["urgent", "high", "medium", "low"];
@@ -3646,6 +3671,11 @@ function recoverPriorityFromEvents(events) {
3646
3671
 
3647
3672
  // src/gh-create.ts
3648
3673
  var ISSUE_TYPES = ["bug", "feature", "task"];
3674
+ var GH_MUTATION_TIMEOUT_MS = 12e4;
3675
+ function timeoutKillNote(err, timeoutMs) {
3676
+ if (typeof err !== "object" || err === null || !err.killed) return void 0;
3677
+ return `killed at the ${timeoutMs}ms timeout \u2014 the write may have completed server-side; verify before retrying`;
3678
+ }
3649
3679
  function normalizePriority(priority) {
3650
3680
  const p = priority.trim().toLowerCase();
3651
3681
  if (!CLI_PRIORITIES.includes(p)) {
@@ -3673,6 +3703,24 @@ function buildIssueArgs({ type, title, body, priority, repo, labels }) {
3673
3703
  for (const label of labels ?? []) args.push("--label", label);
3674
3704
  return args;
3675
3705
  }
3706
+ async function bodyArgsViaFile(args, deps = {}) {
3707
+ const i = args.indexOf("--body");
3708
+ if (i === -1 || i + 1 >= args.length) return { args, cleanup: async () => {
3709
+ } };
3710
+ const write = deps.write ?? import_promises.writeFile;
3711
+ const remove = deps.remove ?? import_promises.unlink;
3712
+ const file = (0, import_node_path3.join)(deps.dir ?? (0, import_node_os.tmpdir)(), `mmi-gh-body-${process.pid}-${(0, import_node_crypto.randomBytes)(4).toString("hex")}.md`);
3713
+ await write(file, args[i + 1], "utf8");
3714
+ return {
3715
+ args: [...args.slice(0, i), "--body-file", file, ...args.slice(i + 2)],
3716
+ cleanup: async () => {
3717
+ try {
3718
+ await remove(file);
3719
+ } catch {
3720
+ }
3721
+ }
3722
+ };
3723
+ }
3676
3724
  function buildAddToProjectArgs(projectId, contentId) {
3677
3725
  if (!projectId) throw new Error("addToProject: projectId is required");
3678
3726
  if (!contentId) throw new Error("addToProject: contentId is required");
@@ -3811,6 +3859,32 @@ function resumeCue() {
3811
3859
  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.';
3812
3860
  }
3813
3861
 
3862
+ // src/fetch-retry.ts
3863
+ async function fetchWithRetry(fetchImpl, url, init, opts = {}) {
3864
+ const attempts = opts.attempts ?? 3;
3865
+ const baseDelayMs = opts.baseDelayMs ?? 250;
3866
+ const retryOn = opts.retryOn ?? ((res) => res.status >= 500);
3867
+ const sleep = opts.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
3868
+ let lastErr;
3869
+ for (let i = 0; i < attempts; i++) {
3870
+ const isLast = i === attempts - 1;
3871
+ const attemptInit = opts.timeoutMs ? { ...init, signal: AbortSignal.timeout(opts.timeoutMs) } : init;
3872
+ try {
3873
+ const res = await fetchImpl(url, attemptInit);
3874
+ if (!isLast && retryOn(res)) {
3875
+ await sleep(baseDelayMs * 2 ** i);
3876
+ continue;
3877
+ }
3878
+ return res;
3879
+ } catch (e) {
3880
+ lastErr = e;
3881
+ if (isLast) throw e;
3882
+ await sleep(baseDelayMs * 2 ** i);
3883
+ }
3884
+ }
3885
+ throw lastErr;
3886
+ }
3887
+
3814
3888
  // src/saga-note.ts
3815
3889
  var AGENT_SURFACE_TOKENS = ["claude", "codex", "cursor", "gemini"];
3816
3890
  var ROUTE_LEVEL_403 = "saga API route-level 403 from GitHubAuthorizer cache/policy";
@@ -5401,12 +5475,12 @@ function buildInstalledPluginVersionCheck(input) {
5401
5475
  // src/stage-runner.ts
5402
5476
  var import_node_child_process5 = require("node:child_process");
5403
5477
  var import_node_fs3 = require("node:fs");
5404
- var import_node_path3 = require("node:path");
5478
+ var import_node_path4 = require("node:path");
5405
5479
  var import_node_net = require("node:net");
5406
5480
  var import_node_util5 = require("node:util");
5407
5481
  var execFileP3 = (0, import_node_util5.promisify)(import_node_child_process5.execFile);
5408
5482
  function stageStatePath(cwd = process.cwd()) {
5409
- return (0, import_node_path3.join)(cwd, "tmp", "stage", "state.json");
5483
+ return (0, import_node_path4.join)(cwd, "tmp", "stage", "state.json");
5410
5484
  }
5411
5485
  function validateStageConfig(config = {}, action) {
5412
5486
  const problems = [];
@@ -6476,16 +6550,21 @@ var PROJECTS_ENVELOPE_KEY = "projects";
6476
6550
 
6477
6551
  // src/registry-client.ts
6478
6552
  var DEFAULT_TIMEOUT_MS2 = 8e3;
6553
+ var RETRY_ATTEMPTS = 3;
6554
+ function retriedFetch(deps, url, init) {
6555
+ return fetchWithRetry(deps.fetch ?? fetch, url, init, {
6556
+ attempts: RETRY_ATTEMPTS,
6557
+ timeoutMs: deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2
6558
+ });
6559
+ }
6479
6560
  async function fetchTrainAuthority(repo, deps) {
6480
6561
  if (!deps.baseUrl) return { ok: false, error: "Hub API URL not configured" };
6481
6562
  const token = await deps.token();
6482
6563
  if (!token) return { ok: false, error: "no GitHub token (gh auth login)" };
6483
- const doFetch = deps.fetch ?? fetch;
6484
6564
  try {
6485
- const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/train-authority?repo=${encodeURIComponent(repo)}`, {
6565
+ const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/train-authority?repo=${encodeURIComponent(repo)}`, {
6486
6566
  method: "GET",
6487
- headers: { Authorization: `Bearer ${token}` },
6488
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6567
+ headers: { Authorization: `Bearer ${token}` }
6489
6568
  });
6490
6569
  if (!res.ok) return { ok: false, error: `train-authority HTTP ${res.status}` };
6491
6570
  const body = await res.json();
@@ -6499,12 +6578,10 @@ async function fetchProjectsList(deps) {
6499
6578
  if (!deps.baseUrl) return null;
6500
6579
  const token = await deps.token();
6501
6580
  if (!token) return null;
6502
- const doFetch = deps.fetch ?? fetch;
6503
6581
  try {
6504
- const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}${PROJECTS_LIST_PATH}`, {
6582
+ const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${PROJECTS_LIST_PATH}`, {
6505
6583
  method: "GET",
6506
- headers: { Authorization: `Bearer ${token}` },
6507
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6584
+ headers: { Authorization: `Bearer ${token}` }
6508
6585
  });
6509
6586
  if (!res.ok) return null;
6510
6587
  const body = await res.json();
@@ -6523,12 +6600,10 @@ async function fetchProjectBySlugChecked(slug, deps) {
6523
6600
  if (!slug) return { ok: false, error: "no slug" };
6524
6601
  const token = await deps.token();
6525
6602
  if (!token) return { ok: false, error: "no GitHub token (run `gh auth login`)" };
6526
- const doFetch = deps.fetch ?? fetch;
6527
6603
  try {
6528
- const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}`, {
6604
+ const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}`, {
6529
6605
  method: "GET",
6530
- headers: { Authorization: `Bearer ${token}` },
6531
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6606
+ headers: { Authorization: `Bearer ${token}` }
6532
6607
  });
6533
6608
  if (res.status === 404) return { ok: true, project: null };
6534
6609
  if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
@@ -6545,12 +6620,10 @@ async function fetchDeployStatusBySlug(slug, deps) {
6545
6620
  if (!deps.baseUrl || !slug) return null;
6546
6621
  const token = await deps.token();
6547
6622
  if (!token) return null;
6548
- const doFetch = deps.fetch ?? fetch;
6549
6623
  try {
6550
- const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
6624
+ const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}/projects/${encodeURIComponent(slug)}/deploy-status`, {
6551
6625
  method: "GET",
6552
- headers: { Authorization: `Bearer ${token}` },
6553
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6626
+ headers: { Authorization: `Bearer ${token}` }
6554
6627
  });
6555
6628
  if (!res.ok) return null;
6556
6629
  const body = await res.json();
@@ -6564,12 +6637,10 @@ async function fetchOrgConfig(deps) {
6564
6637
  if (!deps.baseUrl) return null;
6565
6638
  const token = await deps.token();
6566
6639
  if (!token) return null;
6567
- const doFetch = deps.fetch ?? fetch;
6568
6640
  try {
6569
- const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}${ORG_CONFIG_PATH}`, {
6641
+ const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${ORG_CONFIG_PATH}`, {
6570
6642
  method: "GET",
6571
- headers: { Authorization: `Bearer ${token}` },
6572
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6643
+ headers: { Authorization: `Bearer ${token}` }
6573
6644
  });
6574
6645
  if (!res.ok) return null;
6575
6646
  return await res.json();
@@ -6581,13 +6652,11 @@ async function postJson(pathSuffix, payload, deps, method = "POST") {
6581
6652
  if (!deps.baseUrl) return { ok: false, status: 0, body: null, error: "no Hub API URL (set MMI_HUB_URL or use a current MMI CLI/plugin build)" };
6582
6653
  const token = await deps.token();
6583
6654
  if (!token) return { ok: false, status: 0, body: null, error: "no GitHub token (run `gh auth login`)" };
6584
- const doFetch = deps.fetch ?? fetch;
6585
6655
  try {
6586
- const res = await doFetch(`${deps.baseUrl.replace(/\/$/, "")}${pathSuffix}`, {
6656
+ const res = await retriedFetch(deps, `${deps.baseUrl.replace(/\/$/, "")}${pathSuffix}`, {
6587
6657
  method,
6588
6658
  headers: { Authorization: `Bearer ${token}`, "content-type": "application/json" },
6589
- body: JSON.stringify(payload),
6590
- signal: AbortSignal.timeout(deps.timeoutMs ?? DEFAULT_TIMEOUT_MS2)
6659
+ body: JSON.stringify(payload)
6591
6660
  });
6592
6661
  let body = null;
6593
6662
  try {
@@ -6658,9 +6727,12 @@ function appAttestationOf(meta) {
6658
6727
  function attestedLine(att) {
6659
6728
  return `App-owned readiness attested by @${att.by} on ${att.at.slice(0, 10)} \u2014 the static checklist is cleared (the doctor reads no product repo files); re-run \`mmi-cli project attest\` after app-owned structural changes.`;
6660
6729
  }
6730
+ var CONTRACT_UNDECLARED_LINE = "No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).";
6661
6731
  function appGapsFor(meta, model, slug, projectType) {
6662
6732
  const attested = appAttestationOf(meta);
6663
- if (attested) return [attestedLine(attested)];
6733
+ const isTenantWeb = !(projectType === "content" || model === "content") && projectType !== "desktop-game" && !(projectType === "non-deployable" || model === "none") && model !== "hub-serverless" && model !== "serverless";
6734
+ const contractUndeclared = isTenantWeb && Boolean(meta) && !hasRuntimeSecretContract(meta?.requiredRuntimeSecrets);
6735
+ if (attested) return contractUndeclared ? [attestedLine(attested), CONTRACT_UNDECLARED_LINE] : [attestedLine(attested)];
6664
6736
  if (projectType === "content" || model === "content") return ["Content/KB repo: keep app-owned work to docs/content changes; release train does not apply."];
6665
6737
  if (projectType === "desktop-game") {
6666
6738
  return [
@@ -6692,8 +6764,8 @@ function appGapsFor(meta, model, slug, projectType) {
6692
6764
  "Make app config fail clearly for missing required env in prod/rc instead of relying on hidden defaults.",
6693
6765
  "Keep app-owned README.md and architecture.md aligned with v2 central deploy/secrets reality."
6694
6766
  ];
6695
- if (meta && !hasRuntimeSecretContract(meta.requiredRuntimeSecrets)) {
6696
- gaps.unshift("No runtime secrets declared \u2014 declare requiredRuntimeSecrets (a per-stage name map) in the registry META, or attest the app needs none with an explicit empty stage map ({ dev: [], rc: [], main: [] }).");
6767
+ if (contractUndeclared) {
6768
+ gaps.unshift(CONTRACT_UNDECLARED_LINE);
6697
6769
  }
6698
6770
  if (slug === "mmi-katip") {
6699
6771
  gaps.push("Katip-specific app plan: declare Google Workspace service-account requirements, use the service account numeric OAuth2 client ID for DWD, remove prod-hidden impersonation defaults, and make non-critical Google Workspace failures degrade instead of crash-looping.");
@@ -6773,7 +6845,29 @@ async function runV2Heal(repoOrSlug, opts, deps) {
6773
6845
  async function buildV2Doctor(repoOrSlug, deps) {
6774
6846
  const slug = slugOfRepo(repoOrSlug);
6775
6847
  const repo = repoFrom(repoOrSlug, slug);
6776
- const meta = await deps.getProject(slug);
6848
+ const read = await deps.getProject(slug);
6849
+ if (!read.ok) {
6850
+ const degradedSecrets = {
6851
+ dev: { required: [], present: [], missing: [] },
6852
+ rc: { required: [], present: [], missing: [] },
6853
+ main: { required: [], present: [], missing: [] }
6854
+ };
6855
+ const degradedStage = {
6856
+ dev: { ok: false, required: false },
6857
+ rc: { ok: false, required: false },
6858
+ main: { ok: false, required: false }
6859
+ };
6860
+ return {
6861
+ ok: false,
6862
+ repo,
6863
+ slug,
6864
+ registryError: read.error,
6865
+ hubOwned: { meta: { ok: false, missing: [] }, deployCoords: degradedStage, deployState: degradedStage, secrets: degradedSecrets },
6866
+ autoHealAvailable: [],
6867
+ appOwnedGaps: [`Hub registry read failed (${read.error}) \u2014 diagnosis degraded; nothing is known about META, coords, or gaps. Likely transient (cold start, network, or auth blip): retry \`mmi-cli project doctor\` shortly.`]
6868
+ };
6869
+ }
6870
+ const meta = read.project;
6777
6871
  const projectType = resolveProjectType(meta, repo);
6778
6872
  const model = resolveDeployModel(meta, repo);
6779
6873
  const autoHeal = buildV2HealPatch(repo, meta);
@@ -6879,6 +6973,30 @@ ${section}`.trim();
6879
6973
  // src/project-set.ts
6880
6974
  var UNSET_KEYS = ["oauth", "requiredRuntimeSecrets", "edgeDomains", "requiredGcpApis", "publishRequired"];
6881
6975
  var UNSET_KEY_SET = new Set(UNSET_KEYS);
6976
+ var RUNTIME_SECRET_STAGES = ["dev", "rc", "main"];
6977
+ function parseRuntimeSecretsVar(raw) {
6978
+ let parsed;
6979
+ try {
6980
+ parsed = JSON.parse(raw);
6981
+ } catch {
6982
+ throw new Error('project set: requiredRuntimeSecrets must be JSON, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]}');
6983
+ }
6984
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
6985
+ throw new Error("project set: requiredRuntimeSecrets must be a stage map (a flat array is not box-loadable)");
6986
+ }
6987
+ const map = parsed;
6988
+ const out = {};
6989
+ for (const [stage2, names] of Object.entries(map)) {
6990
+ if (!RUNTIME_SECRET_STAGES.includes(stage2)) {
6991
+ throw new Error(`project set: requiredRuntimeSecrets stage "${stage2}" \u2014 expected only ${RUNTIME_SECRET_STAGES.join("/")}`);
6992
+ }
6993
+ if (!Array.isArray(names) || names.some((n) => typeof n !== "string" || !n.trim())) {
6994
+ throw new Error(`project set: requiredRuntimeSecrets.${stage2} must be an array of non-empty secret names`);
6995
+ }
6996
+ out[stage2] = names;
6997
+ }
6998
+ return out;
6999
+ }
6882
7000
  function buildProjectSetPatch(input) {
6883
7001
  const patch = {};
6884
7002
  if (input.class) {
@@ -6908,6 +7026,8 @@ function buildProjectSetPatch(input) {
6908
7026
  const n = Number(raw);
6909
7027
  if (!Number.isFinite(n)) throw new Error("project set: projectNumber must be numeric");
6910
7028
  patch[key] = n;
7029
+ } else if (key === "requiredRuntimeSecrets") {
7030
+ patch[key] = parseRuntimeSecretsVar(raw);
6911
7031
  } else {
6912
7032
  patch[key] = raw;
6913
7033
  }
@@ -6967,10 +7087,91 @@ function parseKbTree(stdout, prefix) {
6967
7087
  }
6968
7088
 
6969
7089
  // src/plan.ts
6970
- var import_node_path4 = require("node:path");
7090
+ var import_node_path5 = require("node:path");
7091
+
7092
+ // src/frontmatter.ts
7093
+ function splitFrontmatter(content) {
7094
+ const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
7095
+ if (!match) return { entries: [], body: content };
7096
+ return { entries: match[1].split(/\r?\n/).filter((line) => line.trim()), body: content.slice(match[0].length) };
7097
+ }
7098
+ function entryKeyValue(line) {
7099
+ const i = line.indexOf(":");
7100
+ if (i < 0) return null;
7101
+ return { key: line.slice(0, i).trim().toLowerCase(), value: line.slice(i + 1).trim() };
7102
+ }
7103
+ function frontmatterValue(content, key) {
7104
+ const want = key.trim().toLowerCase();
7105
+ for (const line of splitFrontmatter(content).entries) {
7106
+ const kv = entryKeyValue(line);
7107
+ if (kv && kv.key === want) return kv.value || void 0;
7108
+ }
7109
+ return void 0;
7110
+ }
7111
+ function frontmatterList(content, key) {
7112
+ const raw = frontmatterValue(content, key);
7113
+ if (!raw) return [];
7114
+ return raw.replace(/^\[/, "").replace(/\]$/, "").split(",").map((s) => s.trim()).filter(Boolean);
7115
+ }
7116
+ function extractPlanMeta(content) {
7117
+ const meta = {};
7118
+ const status = frontmatterValue(content, "status");
7119
+ if (status) meta.status = status;
7120
+ const tags = frontmatterList(content, "topic-tags");
7121
+ if (tags.length) meta.topicTags = tags;
7122
+ const title = frontmatterValue(content, "title");
7123
+ if (title) meta.title = title;
7124
+ const supersedes = frontmatterValue(content, "supersedes");
7125
+ if (supersedes) meta.supersedes = supersedes;
7126
+ return meta;
7127
+ }
7128
+
7129
+ // src/plan-relevance.ts
7130
+ var STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "plan", "issue", "fix", "feat", "add", "wip", "src", "tsx", "mjs"]);
7131
+ function tokenize(s) {
7132
+ return (s ?? "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase().split(/[^a-z0-9]+/).filter((t) => t.length >= 3 && !STOP.has(t));
7133
+ }
7134
+ var SUPPRESSED = /* @__PURE__ */ new Set(["superseded", "graduated"]);
7135
+ function signalTokens(s) {
7136
+ const all = [
7137
+ ...tokenize(s.branch),
7138
+ ...tokenize(s.issueTitle),
7139
+ ...(s.issueLabels ?? []).flatMap(tokenize),
7140
+ ...(s.changedFiles ?? []).flatMap(tokenize)
7141
+ ];
7142
+ return new Set(all);
7143
+ }
7144
+ function rankPlansByRelevance(plans, signals, opts = {}) {
7145
+ const wanted = signalTokens(signals);
7146
+ const eligible = opts.includeAll ? plans : plans.filter((p) => !p.status || !SUPPRESSED.has(p.status.toLowerCase()));
7147
+ const ranked = eligible.map((plan2) => {
7148
+ const tagTokens = new Set((plan2.topicTags ?? []).flatMap(tokenize));
7149
+ const titleTokens = new Set(tokenize(plan2.title));
7150
+ const slugTokens = new Set(tokenize(plan2.slug));
7151
+ const matched = /* @__PURE__ */ new Set();
7152
+ let score = 0;
7153
+ for (const t of wanted) {
7154
+ if (tagTokens.has(t)) {
7155
+ score += 3;
7156
+ matched.add(t);
7157
+ } else if (titleTokens.has(t)) {
7158
+ score += 2;
7159
+ matched.add(t);
7160
+ } else if (slugTokens.has(t)) {
7161
+ score += 1;
7162
+ matched.add(t);
7163
+ }
7164
+ }
7165
+ return { plan: plan2, score, matched: [...matched] };
7166
+ });
7167
+ ranked.sort((a, b) => b.score - a.score || (b.plan.updatedAt ?? "").localeCompare(a.plan.updatedAt ?? ""));
7168
+ return ranked;
7169
+ }
7170
+
7171
+ // src/plan.ts
6971
7172
  var PLANS_DIR = "plans";
6972
- var META_FILE = (0, import_node_path4.join)(PLANS_DIR, ".plan-meta.json");
6973
- var planPath = (slug) => (0, import_node_path4.join)(PLANS_DIR, `${slug}.md`);
7173
+ var META_FILE = (0, import_node_path5.join)(PLANS_DIR, ".plan-meta.json");
7174
+ var planPath = (slug) => (0, import_node_path5.join)(PLANS_DIR, `${slug}.md`);
6974
7175
  var metaKey = (project2, slug) => `${project2}/${slug}`;
6975
7176
  function parseMeta(raw) {
6976
7177
  if (!raw) return {};
@@ -6999,12 +7200,7 @@ function formatPlanList(plans) {
6999
7200
  return plans.map((p) => `${p.slug} \xB7 ${p.updatedAt ?? "-"} \xB7 ${p.project}`).join("\n");
7000
7201
  }
7001
7202
  var TIMEOUT_MS = 8e3;
7002
- var GRADUATION_KEYS = /* @__PURE__ */ new Set(["northstar-graduation", "privacy", "merged-pr"]);
7003
- function splitFrontmatter(content) {
7004
- const match = /^---\n([\s\S]*?)\n---(?:\n|$)/.exec(content);
7005
- if (!match) return { entries: [], body: content };
7006
- return { entries: match[1].split(/\r?\n/).filter((line) => line.trim()), body: content.slice(match[0].length) };
7007
- }
7203
+ var GRADUATION_KEYS = /* @__PURE__ */ new Set(["northstar-graduation", "privacy", "merged-pr", "status"]);
7008
7204
  function markPlanGraduated(content, opts) {
7009
7205
  const { entries, body } = splitFrontmatter(normalizeEol(content));
7010
7206
  const preserved = entries.filter((line) => {
@@ -7014,6 +7210,7 @@ function markPlanGraduated(content, opts) {
7014
7210
  const next = [
7015
7211
  ...preserved,
7016
7212
  "northstar-graduation: built-and-merged",
7213
+ "status: graduated",
7017
7214
  "privacy: org",
7018
7215
  `merged-pr: ${opts.mergedPr}`
7019
7216
  ];
@@ -7033,6 +7230,8 @@ async function planPush(deps, slug, opts = {}) {
7033
7230
  const meta = parseMeta(deps.readMetaRaw());
7034
7231
  const entry = meta[metaKey(project2, slug)];
7035
7232
  const body = { project: project2, slug, content };
7233
+ const frontmatterMeta = extractPlanMeta(content);
7234
+ if (Object.keys(frontmatterMeta).length) body.meta = frontmatterMeta;
7036
7235
  if (opts.force) body.force = true;
7037
7236
  else if (entry?.etag) body.baseEtag = entry.etag;
7038
7237
  const res = await deps.fetch(`${deps.apiUrl}/plan/put`, {
@@ -7086,35 +7285,59 @@ async function planPull(deps, slug, opts = {}) {
7086
7285
  deps.log(`pulled ${slug} \u2192 ${planPath(slug)}`);
7087
7286
  return true;
7088
7287
  }
7288
+ async function fetchPlanList(deps, project2) {
7289
+ const qs = project2 ? `?${new URLSearchParams({ project: project2 }).toString()}` : "";
7290
+ const res = await deps.fetch(`${deps.apiUrl}/plan/list${qs}`, {
7291
+ method: "GET",
7292
+ headers: await deps.headers(),
7293
+ signal: AbortSignal.timeout(TIMEOUT_MS)
7294
+ });
7295
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
7296
+ const { plans } = await res.json();
7297
+ return plans ?? [];
7298
+ }
7089
7299
  async function planList(deps, opts = {}) {
7090
7300
  const project2 = opts.project ?? (opts.quiet ? await deps.project() : void 0);
7091
- const qs = project2 ? `?${new URLSearchParams({ project: project2 }).toString()}` : "";
7092
- let res;
7301
+ let plans;
7093
7302
  try {
7094
- res = await deps.fetch(`${deps.apiUrl}/plan/list${qs}`, {
7095
- method: "GET",
7096
- headers: await deps.headers(),
7097
- signal: AbortSignal.timeout(TIMEOUT_MS)
7098
- });
7303
+ plans = await fetchPlanList(deps, project2);
7099
7304
  } catch (e) {
7100
7305
  if (!opts.quiet) deps.err(`plan list: ${e.message}`);
7101
7306
  return;
7102
7307
  }
7103
- if (!res.ok) {
7104
- if (!opts.quiet) deps.err(`plan list failed: HTTP ${res.status}`);
7105
- return;
7106
- }
7107
- const { plans } = await res.json();
7108
- if (opts.json) {
7109
- deps.log(JSON.stringify(plans));
7110
- return;
7111
- }
7308
+ if (opts.json) return deps.log(JSON.stringify(plans));
7112
7309
  if (!plans.length) {
7113
7310
  if (!opts.quiet) deps.log("no plans");
7114
7311
  return;
7115
7312
  }
7116
7313
  deps.log(formatPlanList(plans));
7117
7314
  }
7315
+ function formatRelevant(ranked) {
7316
+ return ranked.map((r) => {
7317
+ const why = r.matched.length ? `matches ${r.matched.join(", ")}` : "recent";
7318
+ return `${r.plan.slug} \xB7 ${why} \xB7 mmi-cli northstar pull ${r.plan.slug}`;
7319
+ }).join("\n");
7320
+ }
7321
+ async function relevantPlans(deps, signals, opts = {}) {
7322
+ const project2 = opts.project ?? await deps.project();
7323
+ let plans;
7324
+ try {
7325
+ const scoped = await fetchPlanList(deps, project2);
7326
+ const unprojected = project2 === "-" ? [] : await fetchPlanList(deps, "-").catch(() => []);
7327
+ const seen = new Set(scoped.map((p) => `${p.project}/${p.slug}`));
7328
+ plans = [...scoped, ...unprojected.filter((p) => !seen.has(`${p.project}/${p.slug}`))];
7329
+ } catch (e) {
7330
+ deps.err(`northstar relevant: ${e.message}`);
7331
+ return;
7332
+ }
7333
+ if (!plans.length) return deps.log("no North Stars for this repo yet");
7334
+ const ranked = rankPlansByRelevance(plans, signals, { includeAll: opts.includeAll });
7335
+ const top = (opts.includeAll ? ranked : ranked.filter((r) => r.score > 0)).slice(0, opts.limit ?? 5);
7336
+ if (!top.length) {
7337
+ return deps.log(`no task-relevant North Stars among ${plans.length} for this repo \u2014 \`mmi-cli northstar relevant --all\` lists recent ones`);
7338
+ }
7339
+ deps.log(formatRelevant(top));
7340
+ }
7118
7341
  async function planDelete(deps, slug, opts = {}) {
7119
7342
  const project2 = opts.project ?? await deps.project();
7120
7343
  const res = await deps.fetch(`${deps.apiUrl}/plan/delete`, {
@@ -7571,7 +7794,7 @@ async function sagaHeaders(extra = {}) {
7571
7794
  async function loadConfig() {
7572
7795
  let file = {};
7573
7796
  try {
7574
- file = JSON.parse(await (0, import_promises.readFile)(".mmi/config.json", "utf8"));
7797
+ file = JSON.parse(await (0, import_promises2.readFile)(".mmi/config.json", "utf8"));
7575
7798
  } catch {
7576
7799
  file = {};
7577
7800
  }
@@ -7579,12 +7802,16 @@ async function loadConfig() {
7579
7802
  return file;
7580
7803
  }
7581
7804
  var discoveredConfig = null;
7805
+ function registryDegradeError(error) {
7806
+ return new Error(`Hub registry read failed (${error}) \u2014 board coords could not be discovered; likely transient (cold start, network, or auth blip) \u2014 retry shortly`);
7807
+ }
7582
7808
  async function loadConfigOrDiscover() {
7583
7809
  if (discoveredConfig) return discoveredConfig;
7584
7810
  const floor = await loadConfig();
7585
7811
  if (!floor.sagaApiUrl) return stripMutableBoardConfig(floor);
7586
- const meta = await fetchProjectBySlug(await repoSlug(), registryClientDeps(floor));
7587
- discoveredConfig = meta ? boardConfigFromProject(meta, floor) : stripMutableBoardConfig(floor);
7812
+ const read = await fetchProjectBySlugChecked(await repoSlug(), registryClientDeps(floor));
7813
+ if (!read.ok) throw registryDegradeError(read.error);
7814
+ discoveredConfig = read.project ? boardConfigFromProject(read.project, floor) : stripMutableBoardConfig(floor);
7588
7815
  return discoveredConfig;
7589
7816
  }
7590
7817
  async function loadConfigForRepo(targetRepo2) {
@@ -7592,9 +7819,10 @@ async function loadConfigForRepo(targetRepo2) {
7592
7819
  const cwdRepo = await resolveRepo();
7593
7820
  if (cwdRepo && targetRepo2.toLowerCase() === cwdRepo.toLowerCase()) return loadConfigOrDiscover();
7594
7821
  const floor = await loadConfig();
7595
- const meta = await fetchProjectBySlug(slugOf(targetRepo2), registryClientDeps(floor));
7596
- if (!meta) return stripMutableBoardConfig(floor);
7597
- return boardConfigFromProject(meta, floor);
7822
+ const read = await fetchProjectBySlugChecked(slugOf(targetRepo2), registryClientDeps(floor));
7823
+ if (!read.ok) throw registryDegradeError(read.error);
7824
+ if (!read.project) return stripMutableBoardConfig(floor);
7825
+ return boardConfigFromProject(read.project, floor);
7598
7826
  }
7599
7827
  function repoFromSelector(selector) {
7600
7828
  const trimmed = selector.trim();
@@ -7631,7 +7859,7 @@ function sessionDeps() {
7631
7859
  },
7632
7860
  writePersisted: (id) => persistSession(id),
7633
7861
  now: () => /* @__PURE__ */ new Date(),
7634
- rand: () => (0, import_node_crypto.randomBytes)(4).toString("hex")
7862
+ rand: () => (0, import_node_crypto2.randomBytes)(4).toString("hex")
7635
7863
  };
7636
7864
  }
7637
7865
  var resolveSessionId = () => resolveSession(sessionDeps());
@@ -7654,16 +7882,11 @@ async function postCapture(capture, quiet = false) {
7654
7882
  if (!quiet) console.error("mmi-cli saga: Hub API URL not configured");
7655
7883
  return;
7656
7884
  }
7657
- const res = await fetch(`${cfg.sagaApiUrl}/saga/capture`, {
7885
+ const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/capture`, {
7658
7886
  method: "POST",
7659
7887
  headers: await sagaHeaders({ "content-type": "application/json" }),
7660
- body: JSON.stringify({ ...capture, ...await sagaKey(cfg) }),
7661
- // Capture latency is high + variable (server-side HEAD render); 8s dropped larger notes. Match the
7662
- // head-write timeout (20s) so a continuity note isn't lost to a slow/cold backend. No client retry:
7663
- // the capture isn't guaranteed idempotent, so a retry after a server-side-completed write could
7664
- // duplicate the note. Backend capture-latency root cause tracked in #255.
7665
- signal: AbortSignal.timeout(2e4)
7666
- });
7888
+ body: JSON.stringify({ ...capture, ...await sagaKey(cfg) })
7889
+ }, { attempts: 2, timeoutMs: 2e4, retryOn: () => false });
7667
7890
  let message = "";
7668
7891
  if (!res.ok) {
7669
7892
  try {
@@ -7764,11 +7987,11 @@ async function applyGcPlan(plan2, remote) {
7764
7987
  }
7765
7988
  function resolveVersion() {
7766
7989
  try {
7767
- const manifest = (0, import_node_path5.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
7990
+ const manifest = (0, import_node_path6.join)(__dirname, "..", "..", ".claude-plugin", "plugin.json");
7768
7991
  return JSON.parse((0, import_node_fs5.readFileSync)(manifest, "utf8")).version || "0.0.0";
7769
7992
  } catch {
7770
7993
  try {
7771
- const pkg = (0, import_node_path5.join)(__dirname, "..", "package.json");
7994
+ const pkg = (0, import_node_path6.join)(__dirname, "..", "package.json");
7772
7995
  return JSON.parse((0, import_node_fs5.readFileSync)(pkg, "utf8")).version || "0.0.0";
7773
7996
  } catch {
7774
7997
  return "0.0.0";
@@ -7777,7 +8000,7 @@ function resolveVersion() {
7777
8000
  }
7778
8001
  function readRepoVersion() {
7779
8002
  try {
7780
- return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
8003
+ return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path6.join)(process.cwd(), ".claude-plugin", "plugin.json"), "utf8")).version || void 0;
7781
8004
  } catch {
7782
8005
  return void 0;
7783
8006
  }
@@ -7874,11 +8097,11 @@ async function runRulesSync(opts, io = consoleIo) {
7874
8097
  for (const entry of fetched) {
7875
8098
  if ("error" in entry) continue;
7876
8099
  const { file, source } = entry;
7877
- const current = (0, import_node_fs5.existsSync)(file) ? await (0, import_promises.readFile)(file, "utf8") : null;
8100
+ const current = (0, import_node_fs5.existsSync)(file) ? await (0, import_promises2.readFile)(file, "utf8") : null;
7878
8101
  if (needsUpdate(source, current)) {
7879
8102
  const slash = file.lastIndexOf("/");
7880
8103
  if (slash > 0) (0, import_node_fs5.mkdirSync)(file.slice(0, slash), { recursive: true });
7881
- await (0, import_promises.writeFile)(file, normalizeEol(source), "utf8");
8104
+ await (0, import_promises2.writeFile)(file, normalizeEol(source), "utf8");
7882
8105
  changed++;
7883
8106
  if (!opts.quiet) io.log(`mmi-cli rules: updated ${file}`);
7884
8107
  }
@@ -7903,9 +8126,9 @@ async function runDocsSync(opts, io = consoleIo) {
7903
8126
  return null;
7904
8127
  }
7905
8128
  },
7906
- localContent: async (f) => (0, import_node_fs5.existsSync)(f) ? await (0, import_promises.readFile)(f, "utf8") : null,
8129
+ localContent: async (f) => (0, import_node_fs5.existsSync)(f) ? await (0, import_promises2.readFile)(f, "utf8") : null,
7907
8130
  writeDoc: async (f, c) => {
7908
- await (0, import_promises.writeFile)(f, c, "utf8");
8131
+ await (0, import_promises2.writeFile)(f, c, "utf8");
7909
8132
  }
7910
8133
  });
7911
8134
  for (const f of result.updated) io.log(`mmi-cli docs: updated ${f} (from origin/${def})`);
@@ -7917,7 +8140,7 @@ docs.command("sync").option("--quiet", "stay silent unless something changed or
7917
8140
  var saga = program2.command("saga").description("per-session continuity");
7918
8141
  async function runNote(summary, o) {
7919
8142
  const [sha, key] = await Promise.all([gitOut(["rev-parse", "--short", "HEAD"]), sagaKey(await loadConfig())]);
7920
- const capture = buildNoteCapture(summary, o, (0, import_node_crypto.randomUUID)(), { sha: sha || void 0, branch: key.branch });
8143
+ const capture = buildNoteCapture(summary, o, (0, import_node_crypto2.randomUUID)(), { sha: sha || void 0, branch: key.branch });
7921
8144
  await postCapture(capture);
7922
8145
  }
7923
8146
  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));
@@ -7931,7 +8154,7 @@ async function runSagaShow(opts, io = consoleIo) {
7931
8154
  try {
7932
8155
  const key = await sagaKey(cfg);
7933
8156
  const qs = opts.latestAnywhere ? "scope=anywhere" : new URLSearchParams({ project: key.project, branch: key.branch }).toString();
7934
- const res = await fetch(`${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await sagaHeaders(), signal: AbortSignal.timeout(3e3) });
8157
+ const res = await fetchWithRetry(fetch, `${cfg.sagaApiUrl}/saga/head?${qs}`, { headers: await sagaHeaders() }, { attempts: 2, timeoutMs: 3e3 });
7935
8158
  if (res.ok) {
7936
8159
  io.log(resumeCue());
7937
8160
  return io.log(await res.text());
@@ -7939,7 +8162,7 @@ async function runSagaShow(opts, io = consoleIo) {
7939
8162
  if (!opts.quiet) io.log(`saga show failed: HTTP ${res.status}`);
7940
8163
  } catch (e) {
7941
8164
  if (!opts.quiet) {
7942
- const reason = e.name === "TimeoutError" ? "backend unreachable (timed out after 3s)" : e.message;
8165
+ const reason = e.name === "TimeoutError" ? "backend unreachable (timed out after 2 attempts)" : e.message;
7943
8166
  io.err(`saga show: ${reason} \u2014 continuing without saga; diagnose with \`mmi-cli saga health --json\``);
7944
8167
  }
7945
8168
  }
@@ -7948,7 +8171,7 @@ saga.command("show").option("--quiet", "no-op silently when unconfigured/unreach
7948
8171
  saga.command("capture").option("--quiet", "capture silently (for the Stop hook)").description("per-turn deterministic capture (Stop hook): turn boundary + current sha").action(async (opts) => {
7949
8172
  const hook = parseHookInput(await readStdin());
7950
8173
  if (hook.session_id) persistSession(hook.session_id);
7951
- await postCapture({ event: "stop", id: (0, import_node_crypto.randomUUID)(), source: "hook", sha: await gitOut(["rev-parse", "--short", "HEAD"]), surface: agentSurface() }, opts.quiet ?? false);
8174
+ await postCapture({ event: "stop", id: (0, import_node_crypto2.randomUUID)(), source: "hook", sha: await gitOut(["rev-parse", "--short", "HEAD"]), surface: agentSurface() }, opts.quiet ?? false);
7952
8175
  });
7953
8176
  saga.command("session").option("--quiet", "silent (for the SessionStart hook)").description("persist the harness session id for this repo (SessionStart hook)").action(async () => {
7954
8177
  const hook = parseHookInput(await readStdin());
@@ -8000,7 +8223,7 @@ saga.command("key").option("--json", "machine-readable output").description("pri
8000
8223
  });
8001
8224
  async function probeBackend(url) {
8002
8225
  try {
8003
- const res = await fetch(`${url}/saga/head`, { headers: await sagaHeaders(), signal: AbortSignal.timeout(8e3) });
8226
+ const res = await fetchWithRetry(fetch, `${url}/saga/head`, { headers: await sagaHeaders() }, { attempts: 3, timeoutMs: 4e3 });
8004
8227
  let message = "";
8005
8228
  try {
8006
8229
  const body = await res.clone().json();
@@ -8098,12 +8321,17 @@ kb.command("list [prefix]").description("list KB document paths (optionally unde
8098
8321
  }
8099
8322
  });
8100
8323
  async function ghCreate(args) {
8324
+ const swapped = await bodyArgsViaFile(args);
8101
8325
  try {
8102
- const { stdout } = await execFileP4("gh", args);
8326
+ const { stdout } = await execFileP4("gh", swapped.args, { timeout: GH_MUTATION_TIMEOUT_MS });
8103
8327
  return parseCreatedUrl(stdout);
8104
8328
  } catch (e) {
8329
+ await swapped.cleanup();
8105
8330
  const err = e;
8106
- fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}`);
8331
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
8332
+ fail(`gh ${args[0]} create failed: ${(err.stderr || err.message || String(e)).trim()}${note ? ` (${note})` : ""}`);
8333
+ } finally {
8334
+ await swapped.cleanup();
8107
8335
  }
8108
8336
  }
8109
8337
  async function ghJson(args, timeout = 1e4) {
@@ -8123,7 +8351,13 @@ async function resolveRepo(repo) {
8123
8351
  }
8124
8352
  async function attachToProject(issueNumber, repo, priority) {
8125
8353
  const targetRepo2 = await resolveRepo(repo);
8126
- const cfg = await loadConfigForRepo(targetRepo2);
8354
+ let cfg;
8355
+ try {
8356
+ cfg = await loadConfigForRepo(targetRepo2);
8357
+ } catch (e) {
8358
+ console.error(`issue create: board attach skipped \u2014 ${e.message}`);
8359
+ return void 0;
8360
+ }
8127
8361
  if (!cfg.projectId) {
8128
8362
  console.error(`issue create: board attach skipped \u2014 no Hub registry board META for ${targetRepo2 ?? "current repo"}; run \`mmi-cli project get ${targetRepo2 ?? "<owner/repo>"}\` and backfill board coords`);
8129
8363
  return void 0;
@@ -8134,7 +8368,7 @@ async function attachToProject(issueNumber, repo, priority) {
8134
8368
  const { stdout: idOut } = await execFileP4("gh", viewArgs, { timeout: 1e4 });
8135
8369
  const contentId = idOut.trim();
8136
8370
  if (!contentId) throw new Error("could not resolve issue node id");
8137
- const { stdout } = await execFileP4("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: 1e4 });
8371
+ const { stdout } = await execFileP4("gh", buildAddToProjectArgs(cfg.projectId, contentId), { timeout: GH_MUTATION_TIMEOUT_MS });
8138
8372
  const projectItemId = parseAddedItemId(stdout);
8139
8373
  if (projectItemId && priority) {
8140
8374
  try {
@@ -8226,6 +8460,26 @@ async function withPlan(quiet, run, io = consoleIo) {
8226
8460
  }
8227
8461
  await run(makePlanDeps(cfg, io));
8228
8462
  }
8463
+ async function gatherRelevanceSignals() {
8464
+ const branch = await gitOut(["rev-parse", "--abbrev-ref", "HEAD"]).catch(() => "") || void 0;
8465
+ const changed = (await gitOut(["diff", "--name-only", "HEAD"]).catch(() => "")).split("\n").map((s) => s.trim()).filter(Boolean);
8466
+ const signals = { branch, changedFiles: changed.length ? changed : void 0 };
8467
+ const issueNum = branch?.match(/\b(\d{2,})\b/)?.[1];
8468
+ if (issueNum) {
8469
+ try {
8470
+ const { stdout } = await execFileP4(
8471
+ "gh",
8472
+ ["issue", "view", issueNum, "--json", "title,labels", "--jq", "{title:.title,labels:[.labels[].name]}"],
8473
+ { timeout: 1e4 }
8474
+ );
8475
+ const j = JSON.parse(stdout);
8476
+ if (j.title) signals.issueTitle = j.title;
8477
+ if (j.labels?.length) signals.issueLabels = j.labels;
8478
+ } catch {
8479
+ }
8480
+ }
8481
+ return signals;
8482
+ }
8229
8483
  function registerNorthStarCommands(cmd) {
8230
8484
  cmd.command("push <slug>").description("push a local North Star plan (plans/<slug>.md) to the server").option("--project <name>", "override the project key").option("--force", "overwrite the remote even if it changed since your last sync").action((slug, o) => withPlan(false, async (d) => {
8231
8485
  const ok = await planPush(d, slug, o);
@@ -8236,6 +8490,10 @@ function registerNorthStarCommands(cmd) {
8236
8490
  if (!ok) process.exitCode = 1;
8237
8491
  }));
8238
8492
  cmd.command("list").description("list your North Star plans (cross-device)").option("--project <name>", "filter by project").option("--json", "machine-readable output").option("--quiet", "silent when unconfigured/empty/unreachable (SessionStart hook)").action((o) => withPlan(o.quiet ?? false, (d) => planList(d, o)));
8493
+ cmd.command("relevant").description("list North Stars relevant to your current task (branch + claimed issue + changed files)").option("--project <name>", "override the project key").option("--all", "include superseded/graduated and recent non-matching plans").option("--limit <n>", "max results (default 5)").action((o) => withPlan(false, async (d) => {
8494
+ const signals = await gatherRelevanceSignals();
8495
+ await relevantPlans(d, signals, { project: o.project, includeAll: o.all, limit: o.limit ? Number(o.limit) : void 0 });
8496
+ }));
8239
8497
  cmd.command("open <slug>").description("pull if needed, then open plans/<slug>.md in $EDITOR").option("--project <name>", "override the project key").action(
8240
8498
  (slug, o) => withPlan(false, async (d) => {
8241
8499
  const ok = await planPull(d, slug, { project: o.project });
@@ -8346,7 +8604,8 @@ tenant.command("control <owner/repo> <stage> <action>").description("run bounded
8346
8604
  async function v2ReadinessDeps(cfg) {
8347
8605
  const reg = registryClientDeps(cfg);
8348
8606
  return {
8349
- getProject: (slug) => fetchProjectBySlug(slug, reg),
8607
+ // Checked read (#727/#733): the doctor distinguishes a FAILED read (degraded report) from a 404.
8608
+ getProject: (slug) => fetchProjectBySlugChecked(slug, reg),
8350
8609
  hasDeployCoords: async (slug, stage2) => {
8351
8610
  const status = await fetchDeployStatusBySlug(slug, reg);
8352
8611
  return Boolean(status?.stages[stage2]);
@@ -8359,11 +8618,10 @@ async function v2ReadinessDeps(cfg) {
8359
8618
  const apiUrl = cfg.sagaApiUrl;
8360
8619
  if (!apiUrl) throw new Error("Hub API URL not configured \u2014 cannot verify secret names (set sagaApiUrl)");
8361
8620
  const qs = new URLSearchParams({ repo: targetRepo2 }).toString();
8362
- const res = await fetch(`${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
8621
+ const res = await fetchWithRetry(fetch, `${apiUrl.replace(/\/$/, "")}/secrets/list?${qs}`, {
8363
8622
  method: "GET",
8364
- headers: await sagaHeaders(),
8365
- signal: AbortSignal.timeout(8e3)
8366
- });
8623
+ headers: await sagaHeaders()
8624
+ }, { attempts: 2, timeoutMs: 5e3 });
8367
8625
  if (!res.ok) throw new Error(`secrets list failed for ${targetRepo2}: HTTP ${res.status}`);
8368
8626
  const body = await res.json();
8369
8627
  return (body.secrets ?? []).map((s) => s.key).filter(Boolean);
@@ -8377,15 +8635,25 @@ async function updateV2ReadinessIssue(repo, report, healed) {
8377
8635
  const issues = JSON.parse(list.stdout || "[]");
8378
8636
  const existing = issues.find((i) => i.title.toLowerCase().includes("v2 readiness"));
8379
8637
  if (!existing) {
8380
- const created = await execFileP4("gh", ["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"], { timeout: 2e4 });
8381
- const url = created.stdout.trim();
8382
- const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
8383
- return { number, url, action: "created" };
8638
+ const create = await bodyArgsViaFile(["issue", "create", "--repo", repo, "--title", title, "--body", freshBody, "--label", "feature"]);
8639
+ try {
8640
+ const created = await execFileP4("gh", create.args, { timeout: GH_MUTATION_TIMEOUT_MS });
8641
+ const url = created.stdout.trim();
8642
+ const number = Number(url.match(/\/issues\/(\d+)$/)?.[1] ?? 0);
8643
+ return { number, url, action: "created" };
8644
+ } finally {
8645
+ await create.cleanup();
8646
+ }
8384
8647
  }
8385
8648
  const view = await execFileP4("gh", ["issue", "view", String(existing.number), "--repo", repo, "--json", "body,url", "--jq", "{body:.body,url:.url}"], { timeout: 2e4 });
8386
8649
  const current = JSON.parse(view.stdout || "{}");
8387
8650
  const nextBody = renderReadinessIssueBody(current.body ?? "", report, { healed });
8388
- await execFileP4("gh", ["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody], { timeout: 2e4 });
8651
+ const edit = await bodyArgsViaFile(["issue", "edit", String(existing.number), "--repo", repo, "--body", nextBody]);
8652
+ try {
8653
+ await execFileP4("gh", edit.args, { timeout: GH_MUTATION_TIMEOUT_MS });
8654
+ } finally {
8655
+ await edit.cleanup();
8656
+ }
8389
8657
  return { number: existing.number, url: current.url ?? `https://github.com/${repo}/issues/${existing.number}`, action: "updated" };
8390
8658
  }
8391
8659
  var project = program2.command("project").description("the DDB org registry \u2014 list/get projects (any member); attest is project-admin; set is master-only");
@@ -8487,7 +8755,7 @@ project.command("attest [owner/repo]").description("attest this repo's app-owned
8487
8755
  const res = await attestAppGaps(slugOf(target), repo, registryClientDeps(cfg));
8488
8756
  reportWrite("project attest", res);
8489
8757
  });
8490
- project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project META (idempotent merge; defaults to the current repo; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--var <KEY=VALUE...>", "META field to set (repeatable): name, division, projectId, branch, wikiRepo, vaultPath, kbPointer").option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
8758
+ project.command("set [owner/repo]").description("MASTER-ONLY: upsert a project META (idempotent merge; defaults to the current repo; no clobber of unspecified fields)").option("--class <class>", "deployable | content").option("--project-type <type>", `${PROJECT_TYPES.join(" | ")} (v2 capability shape)`).option("--deploy-model <model>", `${DEPLOY_MODELS.join(" | ")} (release/deploy path; none means no Hub deploy registration)`).option("--var <KEY=VALUE...>", 'META field to set (repeatable): name, division, projectId, branch, wikiRepo, vaultPath, kbPointer, requiredRuntimeSecrets (JSON stage map, e.g. {"dev":["KEY"],"rc":["KEY"],"main":["KEY"]})').option("--unset <KEY...>", "META field to remove (repeatable): oauth, requiredRuntimeSecrets, edgeDomains, requiredGcpApis, publishRequired").option("--clear-web-profile", "remove web-only registry fields (oauth, edgeDomains) for non-web/content projects").option("--json", "machine-readable output").action(async (repoOrSlug, o) => {
8491
8759
  const cfg = await loadConfig();
8492
8760
  let target;
8493
8761
  try {
@@ -8591,7 +8859,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
8591
8859
  let priority;
8592
8860
  let body;
8593
8861
  try {
8594
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises.readFile, readStdin });
8862
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
8595
8863
  priority = normalizePriority(o.priority);
8596
8864
  args = buildIssueArgs({ type: o.type, title: o.title, body, priority, repo: o.repo, labels: o.label });
8597
8865
  } catch (e) {
@@ -8601,7 +8869,7 @@ issue.command("create").description("create an issue (type \u2192 label) and pri
8601
8869
  const la = ["label", "create", label, "--color", "ededed"];
8602
8870
  if (o.repo) la.push("--repo", o.repo);
8603
8871
  try {
8604
- await execFileP4("gh", la, { timeout: 1e4 });
8872
+ await execFileP4("gh", la, { timeout: GH_MUTATION_TIMEOUT_MS });
8605
8873
  } catch {
8606
8874
  }
8607
8875
  }
@@ -8641,7 +8909,7 @@ issue.command("discover-related").description("find related issues for an existi
8641
8909
  "comments"
8642
8910
  ]);
8643
8911
  if (viewed.comments.some((comment) => comment.body.includes(relatedMarker(number)))) return;
8644
- await execFileP4("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: 1e4 });
8912
+ await execFileP4("gh", ["issue", "comment", String(number), "--repo", repo, "--body", buildRelatedComment(number, candidates)], { timeout: GH_MUTATION_TIMEOUT_MS });
8645
8913
  } catch {
8646
8914
  }
8647
8915
  });
@@ -8652,7 +8920,7 @@ program2.command("report").description("file a friction report on the Hub board
8652
8920
  const targetRepo2 = o.repo ?? HUB_REPO;
8653
8921
  const sourceRepo = await resolveRepo(void 0);
8654
8922
  try {
8655
- body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises.readFile, readStdin });
8923
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
8656
8924
  priority = normalizePriority(o.priority);
8657
8925
  args = buildIssueArgs({
8658
8926
  type: o.type,
@@ -8687,7 +8955,7 @@ program2.command("report").description("file a friction report on the Hub board
8687
8955
  const dup = findDuplicateReport({ title: o.title, body }, openReports);
8688
8956
  if (dup) {
8689
8957
  try {
8690
- await execFileP4("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildDupComment(dup.number, body, sourceRepo)], { timeout: 1e4 });
8958
+ await execFileP4("gh", ["issue", "comment", String(dup.number), "--repo", targetRepo2, "--body", buildDupComment(dup.number, body, sourceRepo)], { timeout: GH_MUTATION_TIMEOUT_MS });
8691
8959
  } catch (e) {
8692
8960
  const err = e;
8693
8961
  return fail(`report: duplicate of #${dup.number} (${dup.url}) but the +1 comment failed: ${(err.stderr || err.message || String(e)).trim()}`);
@@ -8696,7 +8964,7 @@ program2.command("report").description("file a friction report on the Hub board
8696
8964
  }
8697
8965
  }
8698
8966
  try {
8699
- await execFileP4("gh", ["label", "create", REPORT_LABEL, "--color", "ededed", "--repo", targetRepo2], { timeout: 1e4 });
8967
+ await execFileP4("gh", ["label", "create", REPORT_LABEL, "--color", "ededed", "--repo", targetRepo2], { timeout: GH_MUTATION_TIMEOUT_MS });
8700
8968
  } catch {
8701
8969
  }
8702
8970
  const created = await ghCreate(args);
@@ -8704,8 +8972,14 @@ program2.command("report").description("file a friction report on the Hub board
8704
8972
  console.log(JSON.stringify({ ...created, deduped: false, label: REPORT_LABEL, priority, projectItemId }));
8705
8973
  });
8706
8974
  var pr = program2.command("pr").description("pull requests \u2014 reliable create with structured output");
8707
- pr.command("create").description("create a PR and print {number,url} JSON").requiredOption("--title <title>", "PR title").requiredOption("--body <body>", "PR body (markdown)").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
8708
- const created = await ghCreate(buildPrArgs({ title: o.title, body: o.body, base: o.base, head: o.head, repo: o.repo }));
8975
+ pr.command("create").description("create a PR and print {number,url} JSON").requiredOption("--title <title>", "PR title").option("--body <body>", "PR body (markdown)").option("--body-file <path|->", "read PR body from a UTF-8 file, or from stdin with -").option("--base <branch>", "base branch (defaults to the repo default)").option("--head <branch>", "head branch (defaults to the current branch)").option("--repo <owner/repo>", "target repo (defaults to the current repo)").action(async (o) => {
8976
+ let body;
8977
+ try {
8978
+ body = await resolveIssueBody({ body: o.body, bodyFile: o.bodyFile }, { readFile: import_promises2.readFile, readStdin });
8979
+ } catch (e) {
8980
+ return fail(`pr create: ${e.message}`);
8981
+ }
8982
+ const created = await ghCreate(buildPrArgs({ title: o.title, body, base: o.base, head: o.head, repo: o.repo }));
8709
8983
  console.log(JSON.stringify(created));
8710
8984
  });
8711
8985
  async function remoteBranchExists(branch) {
@@ -8728,12 +9002,14 @@ pr.command("merge <number>").description("merge a PR (squash by default) and cle
8728
9002
  const remoteBefore = repoArgs.length ? void 0 : await remoteBranchExists(headRef);
8729
9003
  let remoteDeleteAttempted = false;
8730
9004
  let remoteNotAttemptedReason = repoArgs.length ? "repo-option" : void 0;
8731
- await execFileP4("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GC_GH_TIMEOUT_MS }).catch((e) => {
9005
+ await execFileP4("gh", ["pr", "merge", number, ...repoArgs, method, "--delete-branch"], { timeout: GH_MUTATION_TIMEOUT_MS }).catch((e) => {
8732
9006
  const message = String(e.message || "");
8733
9007
  if (/already been merged/i.test(message)) {
8734
9008
  remoteNotAttemptedReason = "pr-already-merged";
8735
9009
  return;
8736
9010
  }
9011
+ const note = timeoutKillNote(e, GH_MUTATION_TIMEOUT_MS);
9012
+ if (note) throw new Error(`gh pr merge ${number}: ${note}`);
8737
9013
  if (!/used by worktree|cannot delete branch/i.test(message)) throw e;
8738
9014
  });
8739
9015
  if (!remoteNotAttemptedReason) remoteDeleteAttempted = true;
@@ -8874,9 +9150,9 @@ function stageKeepAlive() {
8874
9150
  return setTimeout(() => void 0, 5 * 60 * 1e3);
8875
9151
  }
8876
9152
  program2.command("port-range <repo>").description("assign (idempotently) + print the repo's local stage port block via the atomic ORG#config.portCursor allocator (committed-file fallback)").option("--json", "machine-readable output").action(async (repo, o) => {
8877
- const path2 = (0, import_node_path5.join)(process.cwd(), "infra", "port-ranges.json");
9153
+ const path2 = (0, import_node_path6.join)(process.cwd(), "infra", "port-ranges.json");
8878
9154
  const allocate = async (seed) => {
8879
- const { stdout } = await execFileP4("node", [(0, import_node_path5.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
9155
+ const { stdout } = await execFileP4("node", [(0, import_node_path6.join)(process.cwd(), "infra", "port-ddb.mjs"), String(seed)], { timeout: 15e3 });
8880
9156
  const parsed = JSON.parse(stdout);
8881
9157
  if (!Array.isArray(parsed.range) || parsed.range.length !== 2) throw new Error("port-ddb: no range in output");
8882
9158
  return parsed.range;
@@ -9204,7 +9480,7 @@ access.command("audit").description("audit collaborator roles + train-branch pus
9204
9480
  var isWin = process.platform === "win32";
9205
9481
  var installedPluginsPath = (surface = detectSurface(process.env)) => {
9206
9482
  const homeDir = surface === "codex" ? ".codex" : ".claude";
9207
- return (0, import_node_path5.join)((0, import_node_os.homedir)(), homeDir, "plugins", "installed_plugins.json");
9483
+ return (0, import_node_path6.join)((0, import_node_os2.homedir)(), homeDir, "plugins", "installed_plugins.json");
9208
9484
  };
9209
9485
  function readInstalledPlugins() {
9210
9486
  try {
@@ -9215,7 +9491,7 @@ function readInstalledPlugins() {
9215
9491
  }
9216
9492
  function readClaudeSettings() {
9217
9493
  try {
9218
- return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path5.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
9494
+ return JSON.parse((0, import_node_fs5.readFileSync)((0, import_node_path6.join)(process.cwd(), ".claude", "settings.json"), "utf8"));
9219
9495
  } catch {
9220
9496
  return null;
9221
9497
  }
@@ -9261,14 +9537,14 @@ function backupAndWriteInstalledPlugins(records, pluginId) {
9261
9537
  }
9262
9538
  function mmiPluginCacheRootSnapshots() {
9263
9539
  const roots = [
9264
- { surface: "claude", root: (0, import_node_path5.join)((0, import_node_os.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
9265
- { surface: "codex", root: (0, import_node_path5.join)((0, import_node_os.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
9540
+ { surface: "claude", root: (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".claude", "plugins", "cache", "mmi", "mmi") },
9541
+ { surface: "codex", root: (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".codex", "plugins", "cache", "mmi", "mmi") }
9266
9542
  ];
9267
9543
  return roots.flatMap(({ surface, root }) => {
9268
9544
  try {
9269
9545
  const entries = (0, import_node_fs5.readdirSync)(root, { withFileTypes: true }).map((entry) => ({
9270
9546
  name: entry.name,
9271
- path: (0, import_node_path5.join)(root, entry.name),
9547
+ path: (0, import_node_path6.join)(root, entry.name),
9272
9548
  isDirectory: entry.isDirectory()
9273
9549
  }));
9274
9550
  return [{ surface, root, entries }];
@@ -9291,7 +9567,7 @@ function quarantinePluginCacheDirs(plan2) {
9291
9567
  try {
9292
9568
  if (!(0, import_node_fs5.existsSync)(move.from)) continue;
9293
9569
  const target = uniqueQuarantineTarget(move.to);
9294
- (0, import_node_fs5.mkdirSync)((0, import_node_path5.dirname)(target), { recursive: true });
9570
+ (0, import_node_fs5.mkdirSync)((0, import_node_path6.dirname)(target), { recursive: true });
9295
9571
  (0, import_node_fs5.renameSync)(move.from, target);
9296
9572
  moved += 1;
9297
9573
  } catch {
@@ -9299,7 +9575,7 @@ function quarantinePluginCacheDirs(plan2) {
9299
9575
  }
9300
9576
  return moved;
9301
9577
  }
9302
- var gitignorePath = () => (0, import_node_path5.join)(process.cwd(), ".gitignore");
9578
+ var gitignorePath = () => (0, import_node_path6.join)(process.cwd(), ".gitignore");
9303
9579
  function readGitignore() {
9304
9580
  try {
9305
9581
  return (0, import_node_fs5.readFileSync)(gitignorePath(), "utf8");
@@ -9462,24 +9738,22 @@ async function runDoctor(opts, io = consoleIo) {
9462
9738
  ${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
9463
9739
  }
9464
9740
  program2.command("doctor").description("check onboarding gates (GitHub auth, mmi-cli on PATH, Hub API default, plugin git clone, plugin install record, .gitignore managed block, plugin config drift, installed plugin version) and print fixes").option("--banner", "one-line resume summary; silent when all gates pass").option("--guide", "print the MMI Agentic Onboarding guide URL").option("--json", "machine-readable output").action((opts) => runDoctor(opts));
9465
- program2.command("session-start").description("run the SessionStart verbs (rules/docs sync, saga session+show, plan list, saga health, doctor) in one process").action(async () => {
9741
+ program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, doctor) in one process; docs sync runs detached").action(async () => {
9466
9742
  try {
9467
9743
  const hook = parseHookInput(await readStdin());
9468
9744
  if (hook.session_id) persistSession(hook.session_id);
9469
9745
  } catch (e) {
9470
9746
  console.error(`[mmi-hook] saga session failed: ${e.message}`);
9471
9747
  }
9472
- const parallel = [
9473
- { name: "rules sync", run: (io) => runRulesSync({ quiet: true }, io) },
9474
- { name: "docs sync", run: (io) => runDocsSync({ quiet: true }, io) },
9475
- { name: "saga show", run: (io) => runSagaShow({ quiet: true }, io) },
9476
- { name: "plan list", run: (io) => withPlan(true, (d) => planList(d, { quiet: true }), io) },
9477
- { name: "saga health", run: (io) => runSagaHealth({ banner: true, quiet: true }, io) }
9478
- ];
9479
- const sequential = [
9480
- { name: "doctor", run: (io) => runDoctor({ banner: true }, io) }
9481
- ];
9748
+ spawnDetachedSelf(["docs", "sync", "--quiet"], { spawn: import_node_child_process6.spawn, execPath: process.execPath, scriptPath: process.argv[1] });
9749
+ const { parallel, sequential } = buildSessionStartPlan({
9750
+ rulesSync: (io) => runRulesSync({ quiet: true }, io),
9751
+ sagaShow: (io) => runSagaShow({ quiet: true }, io),
9752
+ sagaHealth: (io) => runSagaHealth({ banner: true, quiet: true }, io),
9753
+ doctor: (io) => runDoctor({ banner: true }, io)
9754
+ });
9482
9755
  await runSessionStart(parallel, sequential, consoleIo);
9756
+ consoleIo.log(northstarPointer());
9483
9757
  });
9484
9758
  function fail(msg) {
9485
9759
  console.error(`mmi-cli ${msg}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.11.0",
3
+ "version": "2.12.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",