@mutmutco/cli 2.52.1 → 2.53.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/main.cjs CHANGED
@@ -5174,12 +5174,13 @@ async function completeMainRelease(deps, ctx, meta, deployModel, watch, options,
5174
5174
  const announceNote = deps.announce ? (await deps.announce({ repo: ctx.repo, tag, summaryFile: options.announceSummaryFile })).note : void 0;
5175
5175
  const autoRunSince = (deps.now ?? Date.now)();
5176
5176
  const deployDispatch = await dispatchDeploy(deps, ctx, "main", "main", deployModel, watch, autoRunSince, releaseSha, "report", meta.publishDir);
5177
- const publishDispatch = deployDispatch.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report");
5177
+ const publishDispatch = deployDispatch.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, watch, "report") : null;
5178
5178
  let dispatch = appendPublishDispatch(deployDispatch, publishDispatch);
5179
- if (!publishDispatch && deployDispatch.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
5179
+ if (!publishDispatch && deployDispatch.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
5180
+ const reason = deployDispatch.deployStatus === "failure" ? "box deploy failed \u2014 redeploy the box before publishing" : "box deploy not confirmed (run with --watch) \u2014 publish after the box deploy lands";
5180
5181
  dispatch = {
5181
5182
  ...dispatch,
5182
- note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
5183
+ note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
5183
5184
  };
5184
5185
  }
5185
5186
  return { checks, releaseUrl, announceNote, dispatch };
@@ -6084,18 +6085,23 @@ function headPrompt(state) {
6084
6085
  const decisions = shownDecisions(state.decisions);
6085
6086
  const actions = (state.actionLog ?? []).slice(-HEAD_PROMPT_ACTION_LIMIT);
6086
6087
  return [
6087
- "You maintain ONE durable slot of a work-session: PINNED (things worth remembering). Given the CURRENT",
6088
- "HEAD and the recent TRANSCRIPT + DECISIONS, return an updated PINNED only. Keep it tight and concrete;",
6089
- "keep anything the user pinned; never invent; preserve Turkish characters (\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do",
6090
- "NOT manage next or the checklist \u2014 the note path owns those. The ANCHOR is the read-only North-Star \u2014",
6091
- "NEVER change it. Never restate an unverified artifact-claim (a named file, PR, flag, or board state)",
6092
- "as settled fact \u2014 keep it as the belief it was recorded as.",
6088
+ "You maintain two durable slots of a work-session: PINNED (things worth remembering) and a NEXT",
6089
+ "SUGGESTION (a best-effort one-line hint for the next useful step). Given the CURRENT HEAD and the",
6090
+ "recent TRANSCRIPT + DECISIONS, return an updated PINNED and, optionally, a next suggestion. Keep",
6091
+ "PINNED tight and concrete; keep anything the user pinned; never invent; preserve Turkish characters",
6092
+ "(\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do NOT manage the checklist \u2014 the note path owns that. The ANCHOR is the",
6093
+ "read-only North-Star \u2014 NEVER change it. Never restate an unverified artifact-claim (a named file,",
6094
+ "PR, flag, or board state) as settled fact \u2014 keep it as the belief it was recorded as.",
6093
6095
  "You MAY also propose supersessions: each DECISION is shown with its original stable 0-based index. Propose a",
6094
6096
  "supersession ONLY for a NEWER decision that directly contradicts/replaces an OLDER one where neither",
6095
6097
  "already carries a supersededBy. HIGH PRECISION \u2014 propose ONLY when you are confident the older claim",
6096
6098
  "is now false or obsolete; the newer decision's timestamp MUST be later than the older's (newer-supersedes-",
6097
- "older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor, next, or checklist.",
6098
- 'Output ONLY a JSON object: {"pinned":[string],"supersede":[{"older":int,"newer":int,"reason":string}]}.',
6099
+ "older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor or checklist.",
6100
+ 'For "next": propose a single concise actionable line (\u2264140 chars) describing the most useful next step',
6101
+ "for a future session, derived from the transcript. This is a best-effort suggestion shown only when",
6102
+ "the user has not set their own NEXT. Use an empty string if nothing concrete emerges \u2014 never invent.",
6103
+ "Preserve Turkish characters exactly.",
6104
+ 'Output ONLY a JSON object: {"pinned":[string],"next":string,"supersede":[{"older":int,"newer":int,"reason":string}]}.',
6099
6105
  "",
6100
6106
  "CURRENT HEAD:",
6101
6107
  JSON.stringify(state.head ?? {}, null, 2),
@@ -6119,6 +6125,9 @@ function parseHeadUpdate(raw) {
6119
6125
  if (!obj || typeof obj !== "object") return null;
6120
6126
  const u = {};
6121
6127
  if (Array.isArray(obj.pinned)) u.pinned = obj.pinned.filter((x) => typeof x === "string");
6128
+ if (typeof obj.next === "string" && obj.next.trim()) {
6129
+ u.nextAuto = obj.next.trim().slice(0, 280);
6130
+ }
6122
6131
  if (Array.isArray(obj.supersede)) {
6123
6132
  const supersede = obj.supersede.filter((e) => {
6124
6133
  if (!e || typeof e !== "object") return false;
@@ -7365,6 +7374,18 @@ function writeState(path2, state) {
7365
7374
  mkdirFor(path2);
7366
7375
  (0, import_node_fs9.writeFileSync)(path2, JSON.stringify(state, null, 2), "utf8");
7367
7376
  }
7377
+ function writeStagePortReservation(port, cwd, statePath, globalStatePath, now) {
7378
+ const reservation = {
7379
+ pid: 0,
7380
+ command: "",
7381
+ cwd,
7382
+ statePath,
7383
+ startedAt: now().toISOString(),
7384
+ port
7385
+ };
7386
+ writeState(statePath, reservation);
7387
+ if (globalStatePath && globalStatePath !== statePath) writeState(globalStatePath, reservation);
7388
+ }
7368
7389
  async function cleanupStageState(state, paths, timeoutMs, fallbackCwd) {
7369
7390
  await killTree(state.pid);
7370
7391
  if (state.teardown?.command.trim()) {
@@ -7505,6 +7526,8 @@ async function runStage(config = {}, opts = {}) {
7505
7526
  if (problems.length) throw new Error(problems.join("; "));
7506
7527
  const cwd = opts.cwd ?? process.cwd();
7507
7528
  const timeoutMs = opts.timeoutMs ?? 6e4;
7529
+ const statePath = opts.statePath ?? stageStatePath(cwd);
7530
+ const globalStatePath = await resolveGlobalStatePath(cwd, opts.globalStatePath);
7508
7531
  const portGuard = resolveStagePortGuard(opts);
7509
7532
  await stopStage({ ...opts, cwd, requiredIdentityCwd: opts.requiredIdentityCwd ?? cwd });
7510
7533
  const reserved = await reservedPortsForWorktree(cwd);
@@ -7515,11 +7538,20 @@ async function runStage(config = {}, opts = {}) {
7515
7538
  stagePort = await resolveStagePort(config, portGuard, reserved);
7516
7539
  }
7517
7540
  const sub = (s) => substituteStagePort(s, stagePort);
7518
- await ensureStageRuntimeEnv(config, opts, cwd);
7541
+ if (stagePort != null) {
7542
+ writeStagePortReservation(stagePort, cwd, statePath, globalStatePath, opts.now ?? (() => /* @__PURE__ */ new Date()));
7543
+ }
7519
7544
  const extraEnv = stageExtraEnv(config, stagePort);
7520
7545
  const build2 = config.build?.trim();
7521
7546
  const ranBuild = Boolean(build2);
7522
- if (build2) await shell(sub(build2), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
7547
+ try {
7548
+ await ensureStageRuntimeEnv(config, opts, cwd);
7549
+ if (build2) await shell(sub(build2), cwd, timeoutMs, stageProcessEnv(stagePort, extraEnv));
7550
+ } catch (e) {
7551
+ (0, import_node_fs9.rmSync)(statePath, { force: true });
7552
+ if (globalStatePath && globalStatePath !== statePath) (0, import_node_fs9.rmSync)(globalStatePath, { force: true });
7553
+ throw e;
7554
+ }
7523
7555
  const started = await startStage(config, {
7524
7556
  ...opts,
7525
7557
  cwd,
@@ -8519,15 +8551,28 @@ function planDirtyForAutosave(localHash, meta, project2, slug, queue) {
8519
8551
  if (metaEntry?.hash === localHash) return false;
8520
8552
  return true;
8521
8553
  }
8522
- function planSlugsNeedingAutosave(deps, project2, slugs) {
8554
+ function resolveAutosaveProject(meta, queue, defaultProject, slug) {
8555
+ const candidates = /* @__PURE__ */ new Set();
8556
+ const suffix = `/${slug}`;
8557
+ for (const key of Object.keys(meta)) {
8558
+ if (key.endsWith(suffix)) candidates.add(key.slice(0, -suffix.length));
8559
+ }
8560
+ for (const entry of queue) {
8561
+ if (entry.slug === slug) candidates.add(entry.project);
8562
+ }
8563
+ if (candidates.size === 1) return [...candidates][0];
8564
+ return defaultProject;
8565
+ }
8566
+ function plansNeedingAutosave(deps, project2, slugs) {
8523
8567
  const meta = parseMeta(deps.readMetaRaw());
8524
8568
  const queue = parseQueue(deps.readQueueRaw());
8525
8569
  const out = [];
8526
8570
  for (const slug of slugs) {
8527
8571
  const raw = deps.readLocal(slug);
8528
8572
  if (raw == null) continue;
8573
+ const autosaveProject = resolveAutosaveProject(meta, queue, project2, slug);
8529
8574
  const hash = hashContent(normalizeEol(raw));
8530
- if (planDirtyForAutosave(hash, meta, project2, slug, queue)) out.push(slug);
8575
+ if (planDirtyForAutosave(hash, meta, autosaveProject, slug, queue)) out.push({ project: autosaveProject, slug });
8531
8576
  }
8532
8577
  return out;
8533
8578
  }
@@ -8555,11 +8600,11 @@ async function planAutoEnqueueDirty(deps, opts = {}) {
8555
8600
  }
8556
8601
  }
8557
8602
  const enqueued = [];
8558
- for (const slug of planSlugsNeedingAutosave(deps, project2, slugs)) {
8559
- const raw = deps.readLocal(slug);
8603
+ for (const entry of plansNeedingAutosave(deps, project2, slugs)) {
8604
+ const raw = deps.readLocal(entry.slug);
8560
8605
  if (raw == null) continue;
8561
- enqueuePlanPush(deps, { project: project2, slug, hash: hashContent(normalizeEol(raw)) }, { detach: false });
8562
- enqueued.push(slug);
8606
+ enqueuePlanPush(deps, { project: entry.project, slug: entry.slug, hash: hashContent(normalizeEol(raw)) }, { detach: false });
8607
+ enqueued.push(entry.slug);
8563
8608
  }
8564
8609
  if (enqueued.length) deps.detachSync();
8565
8610
  return enqueued;
@@ -11210,6 +11255,11 @@ function defaultWorktreePath(repoRoot, branch) {
11210
11255
  const safe = branch.replace(/[/\\]+/g, "-");
11211
11256
  return (0, import_node_path15.join)((0, import_node_path15.dirname)(repoRoot), "mmi-worktrees", safe);
11212
11257
  }
11258
+ function resolveWorktreeBase(from, remote) {
11259
+ const remotePrefix = `${remote}/`;
11260
+ const fetchBranch = from.startsWith(remotePrefix) ? from.slice(remotePrefix.length) : void 0;
11261
+ return { base: from, fetchBranch };
11262
+ }
11213
11263
 
11214
11264
  // src/northstar-context.ts
11215
11265
  var SESSION_START_NORTHSTAR_TIMEOUT_MS = 3e3;
@@ -13621,35 +13671,18 @@ async function detectPublicIp(fetchImpl = fetch) {
13621
13671
  if (!validStageLiveIp(ip)) throw new Error(`public IP detection returned a non-IP body from ${IP_ECHO_URL}: "${ip.slice(0, 80)}"`);
13622
13672
  return ip;
13623
13673
  }
13624
- function ghDispatchArgs(workflow, inputs) {
13625
- const args = ["workflow", "run", workflow, "--repo", STAGE_LIVE_HUB_REPO];
13626
- for (const [key, value] of Object.entries(inputs)) args.push("-f", `${key}=${value}`);
13627
- return args;
13628
- }
13629
13674
  function stageLiveUpSteps(t) {
13630
13675
  return [
13631
13676
  { label: `detect your public IP (${IP_ECHO_URL}, bounded)` },
13632
- {
13633
- label: `deploy ${t.ref ?? "<current branch>"} to the ${t.slug} dev stage via the central deployer`,
13634
- command: `gh ${ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref ?? "<branch>", stage: "dev" }).join(" ")}`
13635
- },
13636
- {
13637
- label: `gate ${t.host} to your IP at the Cloudflare edge (ephemeral firewall_custom skip rule)`,
13638
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-allow", host: t.host, ip: "<your ip>" }).join(" ")}`
13639
- },
13677
+ { label: `deploy ${t.ref ?? "<current branch>"} to the ${t.slug} dev stage via the Hub backend (tenant-deploy)` },
13678
+ { label: `gate ${t.host} to your IP at the Cloudflare edge via the Hub backend (tenant-control cf-gate-allow)` },
13640
13679
  { label: "tear down when done", command: "mmi-cli stage --live --down --apply" }
13641
13680
  ];
13642
13681
  }
13643
13682
  function stageLiveDownSteps(t) {
13644
13683
  return [
13645
- {
13646
- label: `stop the ${t.slug} dev runtime`,
13647
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }).join(" ")}`
13648
- },
13649
- {
13650
- label: "remove the Cloudflare edge gate (the stage goes dark even if restarted)",
13651
- command: `gh ${ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-clear", host: t.host }).join(" ")}`
13652
- }
13684
+ { label: `stop the ${t.slug} dev runtime via the Hub backend (tenant-control stop)` },
13685
+ { label: `remove the Cloudflare edge gate for ${t.host} via the Hub backend (tenant-control cf-gate-clear)` }
13653
13686
  ];
13654
13687
  }
13655
13688
  async function runStageLiveUp(deps, t) {
@@ -13657,8 +13690,8 @@ async function runStageLiveUp(deps, t) {
13657
13690
  const ip = (await deps.detectIp()).trim();
13658
13691
  if (!validStageLiveIp(ip)) throw new Error(`stage --live: detected public IP is not a literal IPv4/IPv6 address: "${ip.slice(0, 80)}"`);
13659
13692
  if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
13660
- await deps.run("gh", ghDispatchArgs("tenant-deploy.yml", { slug: t.slug, repo: t.repo, ref: t.ref, stage: "dev" }));
13661
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-allow", host: t.host, ip }));
13693
+ await deps.deployDev({ repo: t.repo, ref: t.ref });
13694
+ await deps.control({ repo: t.repo, action: "cf-gate-allow", host: t.host, ip });
13662
13695
  return {
13663
13696
  command: "stage --live",
13664
13697
  mode: "up",
@@ -13672,8 +13705,8 @@ async function runStageLiveUp(deps, t) {
13672
13705
  }
13673
13706
  async function runStageLiveDown(deps, t) {
13674
13707
  if (!t.host?.trim()) throw new Error("stage --live: cannot resolve the dev edge host (registry edgeDomains.dev)");
13675
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "stop" }));
13676
- await deps.run("gh", ghDispatchArgs("tenant-control.yml", { slug: t.slug, stage: "dev", action: "cf-gate-clear", host: t.host }));
13708
+ await deps.control({ repo: t.repo, action: "stop" });
13709
+ await deps.control({ repo: t.repo, action: "cf-gate-clear", host: t.host });
13677
13710
  return {
13678
13711
  command: "stage --live",
13679
13712
  mode: "down",
@@ -14400,12 +14433,13 @@ async function runHotfixRelease(deps, versionInput, options = {}) {
14400
14433
  "report",
14401
14434
  meta.publishDir
14402
14435
  );
14403
- const publish = deploy.deployStatus === "failure" ? null : await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report");
14436
+ const publish = deploy.deployStatus === "success" ? await dispatchPublishIfRequired(deps, ctx, meta, deployModel, "main", tag, true, "report") : null;
14404
14437
  let dispatch = appendPublishDispatch(deploy, publish);
14405
- if (!publish && deploy.deployStatus === "failure" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
14438
+ if (!publish && deploy.deployStatus !== "success" && meta.publishRequired && (deployModel === "tenant-container" || deployModel === "solo-container")) {
14439
+ const reason = deploy.deployStatus === "failure" ? "box deploy failed \u2014 redeploy the box before publishing" : "box deploy not confirmed (run with --watch) \u2014 publish after the box deploy lands";
14406
14440
  dispatch = {
14407
14441
  ...dispatch,
14408
- note: `${dispatch.note}; tenant-publish.yml skipped (box deploy failed \u2014 redeploy the box before publishing)`
14442
+ note: `${dispatch.note}; tenant-publish.yml skipped (${reason})`
14409
14443
  };
14410
14444
  }
14411
14445
  deployNote = dispatch.note;
@@ -17598,6 +17632,23 @@ var import_promises7 = require("node:fs/promises");
17598
17632
  var import_node_path23 = require("node:path");
17599
17633
  var import_node_os5 = require("node:os");
17600
17634
 
17635
+ // src/plugin-guard.ts
17636
+ function buildPluginGuardDecision(i) {
17637
+ if (!i.isOrgRepo) return { state: "not-org" };
17638
+ if (!i.installRecordPresent) return { state: "no-install" };
17639
+ if (!i.marketplaceClonePresent || !i.pluginCachePresent) return { state: "unresolved" };
17640
+ return { state: "healthy" };
17641
+ }
17642
+ function buildGuardSessionStartLine(state, opts = {}) {
17643
+ if (state === "healthy" || state === "not-org") return { exitCode: 0 };
17644
+ const recovery = opts.recovery ?? "mmi-cli plugin-heal";
17645
+ const reason = state === "no-install" ? "MMI plugin is not installed for this user/session" : "MMI plugin is installed but its marketplace/cache is unresolved";
17646
+ return {
17647
+ line: `[mmi-guard] ${reason}; run ${recovery} and restart Claude Code / reload plugins.`,
17648
+ exitCode: 1
17649
+ };
17650
+ }
17651
+
17601
17652
  // src/cursor-plugin-seed.ts
17602
17653
  var import_node_child_process12 = require("node:child_process");
17603
17654
  var import_node_fs22 = require("node:fs");
@@ -17789,20 +17840,6 @@ function pluginInstallManualFix(projectPath, surface = "claude-cli") {
17789
17840
  const register = surface === "codex" ? `\`codex plugin add ${MMI_PLUGIN_ID}\`` : surface === "cursor" ? `import the MMI Team Marketplace in Cursor Dashboard \u2192 Settings \u2192 Plugins (or enable the MMI plugin from the marketplace panel)` : surface === "shell" ? `enable the MMI plugin in your client` : surface === "claude-vscode" ? `\`claude plugin enable ${MMI_PLUGIN_ID}\`` : `\`/plugin install ${MMI_PLUGIN_ID}\``;
17790
17841
  return `run ${register} then ${reloadAction(surface)} to register the install record for ${projectPath}`;
17791
17842
  }
17792
- function isMmiPluginEnabled(settings) {
17793
- return Boolean(settings?.enabledPlugins?.[MMI_PLUGIN_ID]);
17794
- }
17795
- function buildSettingsPluginDriftCheck(input) {
17796
- const base = {
17797
- ok: true,
17798
- label: "org plugin wiring in .claude/settings.json (mmi@mutmutco)",
17799
- fix: "the Claude Code app pruned mmi@mutmutco from the tracked .claude/settings.json (it does this at session start when the mutmutco marketplace source does not resolve, #1805); restore it with `git checkout -- .claude/settings.json` before committing, or the whole org skill set is disabled for the branch"
17800
- };
17801
- const enabled = input.settings?.enabledPlugins;
17802
- if (!input.isOrgRepo || !enabled || Object.keys(enabled).length === 0) return base;
17803
- if (!enabled[MMI_PLUGIN_ID]) return { ...base, ok: false };
17804
- return base;
17805
- }
17806
17843
  function hasProjectInstallRecord(file, pluginId, projectPath) {
17807
17844
  const records = file?.plugins?.[pluginId];
17808
17845
  if (!Array.isArray(records)) return false;
@@ -17825,7 +17862,7 @@ function buildPluginInstallRecordCheck(input) {
17825
17862
  fix: pluginInstallManualFix(input.projectPath, input.surface),
17826
17863
  pluginId
17827
17864
  };
17828
- if (!input.isOrgRepo || !isMmiPluginEnabled(input.settings)) return base;
17865
+ if (!input.isOrgRepo) return base;
17829
17866
  if (hasAnyPluginRecords(input.installed, LEGACY_MMI_PLUGIN_ID) && !hasAnyPluginRecords(input.installed, pluginId)) {
17830
17867
  return {
17831
17868
  ...base,
@@ -18086,8 +18123,9 @@ function reloadAction(surface) {
18086
18123
  return "restart Claude Code (or run /reload-plugins)";
18087
18124
  }
18088
18125
  }
18089
- var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub && claude plugin install mmi@mutmutco`;
18126
+ var CLAUDE_RECOVERY = `claude plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && claude plugin marketplace remove mutmutco && claude plugin marketplace add mutmutco/MMI-Hub --ref main && claude plugin install mmi@mutmutco`;
18090
18127
  var CODEX_RECOVERY = `codex plugin marketplace remove ${LEGACY_MMI_MARKETPLACE} && codex plugin marketplace remove mutmutco && codex plugin marketplace add mutmutco/MMI-Hub --ref main && codex plugin add mmi@mutmutco`;
18128
+ var CURSOR_RECOVERY = "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace";
18091
18129
  var OPENCODE_PLUGIN_PACKAGE = "@mutmutco/opencode-mmi";
18092
18130
  var OPENCODE_PLUGIN_SPEC = `${OPENCODE_PLUGIN_PACKAGE}@latest`;
18093
18131
  var OPENCODE_PLUGIN_INSTALL_COMMAND = `mmi-cli doctor --apply`;
@@ -18136,7 +18174,10 @@ var PLUGIN_SURFACE_HEAL = {
18136
18174
  healSteps: [
18137
18175
  { args: ["plugin", "marketplace", "remove", LEGACY_MMI_MARKETPLACE], gated: false },
18138
18176
  { args: ["plugin", "marketplace", "remove", "mutmutco"], gated: false },
18139
- { args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub"], gated: true },
18177
+ // Pin --ref main: the marketplace clone must track `main` (the released branch the plugin pins in
18178
+ // .claude-plugin/marketplace.json source.ref), removing the dev-vs-main skew that triggers the prune
18179
+ // and doubles fetch exposure. Mirrors the existing codex entry (#2038).
18180
+ { args: ["plugin", "marketplace", "add", "mutmutco/MMI-Hub", "--ref", "main"], gated: true },
18140
18181
  { args: ["plugin", "install", "mmi@mutmutco"], gated: true },
18141
18182
  { args: ["plugin", "enable", "mmi@mutmutco"], gated: false }
18142
18183
  ],
@@ -18159,10 +18200,10 @@ var PLUGIN_SURFACE_HEAL = {
18159
18200
  },
18160
18201
  cursor: {
18161
18202
  delivery: "cursor-cache",
18162
- recovery: "in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace",
18203
+ recovery: CURSOR_RECOVERY,
18163
18204
  healSteps: null,
18164
- fix: (surface) => `in Cursor Dashboard \u2192 Settings \u2192 Plugins, click Update next to the MMI Team Marketplace; then ${reloadAction(surface)} to reload MMI skills + hooks`,
18165
- updateRecipe: []
18205
+ fix: (surface) => `${CURSOR_RECOVERY}; then ${reloadAction(surface)} to reload MMI skills + hooks`,
18206
+ updateRecipe: [CURSOR_RECOVERY]
18166
18207
  },
18167
18208
  opencode: {
18168
18209
  delivery: "npm",
@@ -18203,9 +18244,21 @@ function surfaceToken(surface) {
18203
18244
  var PLUGIN_UPDATE_RECIPES = {
18204
18245
  claude: PLUGIN_SURFACE_HEAL.claude.updateRecipe,
18205
18246
  codex: PLUGIN_SURFACE_HEAL.codex.updateRecipe,
18247
+ cursor: PLUGIN_SURFACE_HEAL.cursor.updateRecipe,
18206
18248
  opencode: PLUGIN_SURFACE_HEAL.opencode.updateRecipe,
18207
18249
  cli: ["npm install -g @mutmutco/cli@latest"]
18208
18250
  };
18251
+ var PLUGIN_GUIDE_SURFACES = [
18252
+ { key: "cli", label: "npm CLI", versionKeys: ["cli"] },
18253
+ { key: "claude", label: "Claude Code", versionKeys: ["claudePlugin"] },
18254
+ { key: "codex", label: "Codex", versionKeys: ["codexMarketplace", "codexActiveCache"] },
18255
+ { key: "cursor", label: "Cursor", versionKeys: [] },
18256
+ { key: "opencode", label: "OpenCode", versionKeys: ["opencodePlugin"] }
18257
+ ];
18258
+ function renderSurfaceGuide(label, steps) {
18259
+ if (!steps.length) return [];
18260
+ return [` ${label}`, ...steps.map((step) => ` ${step}`)];
18261
+ }
18209
18262
  function highestSemver(versions) {
18210
18263
  return versions.reduce((best, v) => {
18211
18264
  if (!isSemverVersion2(v)) return best;
@@ -18233,20 +18286,24 @@ function buildPluginUpdateReport(input) {
18233
18286
  function renderPluginUpdateReport(report) {
18234
18287
  const v = report.versions;
18235
18288
  const show = (x) => x ?? "unknown";
18236
- return [
18237
- "MMI versions:",
18238
- ` CLI: ${show(v.cli)}`,
18239
- ` Claude plugin: ${show(v.claudePlugin)}`,
18240
- ` Codex marketplace: ${show(v.codexMarketplace)}`,
18241
- ` Codex active cache: ${show(v.codexActiveCache)}`,
18242
- ` OpenCode plugin: ${show(v.opencodePlugin)}`,
18243
- ` latest release: ${show(v.released)}`,
18244
- "Update recipes (per surface):",
18245
- ` Claude: ${report.recipes.claude.join(" ; ")}`,
18246
- ` Codex: ${report.recipes.codex.join(" ; ")}`,
18247
- ` OpenCode: ${report.recipes.opencode.join(" ; ")}`,
18248
- ` npm CLI: ${report.recipes.cli.join(" ; ")}`
18289
+ const versionRows = [
18290
+ ["mmi-cli", show(v.cli)],
18291
+ ["Claude plugin", show(v.claudePlugin)],
18292
+ ["Codex marketplace", show(v.codexMarketplace)],
18293
+ ["Codex active cache", show(v.codexActiveCache)],
18294
+ ["OpenCode plugin", show(v.opencodePlugin)]
18295
+ ];
18296
+ const pad = Math.max(...versionRows.map(([label]) => label.length));
18297
+ const lines = [
18298
+ `MMI versions (target release: ${show(v.released)})`,
18299
+ ...versionRows.map(([label, value]) => ` ${label.padEnd(pad)} ${value}`),
18300
+ "",
18301
+ "Update commands by surface"
18249
18302
  ];
18303
+ for (const surface of PLUGIN_GUIDE_SURFACES) {
18304
+ lines.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
18305
+ }
18306
+ return lines;
18250
18307
  }
18251
18308
  function buildDoctorJsonPayload(input) {
18252
18309
  return {
@@ -18757,18 +18814,21 @@ function renderPluginUpdateReportStaleOnly(report) {
18757
18814
  const released = v.released;
18758
18815
  if (!released) return [];
18759
18816
  const isStale = (current) => Boolean(current && isSemverVersion2(current) && compareVersions(current, released) < 0);
18760
- const recipeLines = [];
18761
- if (isStale(v.cli)) recipeLines.push(` npm CLI: ${report.recipes.cli.join(" ; ")}`);
18762
- if (isStale(v.claudePlugin)) recipeLines.push(` Claude: ${report.recipes.claude.join(" ; ")}`);
18763
- if (isStale(v.codexMarketplace) || isStale(v.codexActiveCache)) {
18764
- recipeLines.push(` Codex: ${report.recipes.codex.join(" ; ")}`);
18817
+ const blocks = [];
18818
+ for (const surface of PLUGIN_GUIDE_SURFACES) {
18819
+ if (!surface.versionKeys.some((k) => isStale(v[k]))) continue;
18820
+ blocks.push(...renderSurfaceGuide(surface.label, report.recipes[surface.key]));
18765
18821
  }
18766
- if (isStale(v.opencodePlugin)) recipeLines.push(` OpenCode: ${report.recipes.opencode.join(" ; ")}`);
18767
- if (!recipeLines.length) return [];
18768
- return ["Update recipes (stale surfaces):", ...recipeLines];
18822
+ if (!blocks.length) return [];
18823
+ return ["Update commands (stale surfaces):", ...blocks];
18769
18824
  }
18825
+ var DOCTOR_VERBOSE_HINT = "Run mmi-cli doctor --verbose for the full audit checklist + version report.";
18770
18826
  function renderTerseDoctorReport(input) {
18771
- if (!input.gaps.length) return [];
18827
+ const cliVersion = input.updateReport.versions.cli;
18828
+ const versionSuffix = cliVersion ? ` (mmi-cli ${cliVersion})` : "";
18829
+ if (!input.gaps.length) {
18830
+ return [`\u2713 MMI doctor: all checks passed${versionSuffix}.`, DOCTOR_VERBOSE_HINT];
18831
+ }
18772
18832
  const lines = [];
18773
18833
  for (const c of input.gaps) {
18774
18834
  lines.push(`\u2717 ${c.label}`);
@@ -18779,8 +18839,39 @@ function renderTerseDoctorReport(input) {
18779
18839
  lines.push("");
18780
18840
  lines.push(...stale);
18781
18841
  }
18842
+ lines.push("");
18843
+ lines.push(`\u26A0 ${input.gaps.length} item(s) need attention \u2014 ${DOCTOR_VERBOSE_HINT}`);
18782
18844
  return lines;
18783
18845
  }
18846
+ var PLUGIN_RESOLVABILITY_LABEL = "MMI plugin resolvability (marketplace + cache present)";
18847
+ function buildPluginResolvabilityCheck(input) {
18848
+ const { state } = buildPluginGuardDecision(input);
18849
+ const surface = input.surface ?? "shell";
18850
+ switch (state) {
18851
+ case "not-org":
18852
+ return { ok: true, label: PLUGIN_RESOLVABILITY_LABEL, fix: "", state };
18853
+ case "healthy":
18854
+ return { ok: true, label: PLUGIN_RESOLVABILITY_LABEL, fix: "", state };
18855
+ case "no-install":
18856
+ return {
18857
+ ok: false,
18858
+ label: PLUGIN_RESOLVABILITY_LABEL,
18859
+ fix: `run: ${PLUGIN_SURFACE_HEAL[surfaceToken(surface) ?? "claude"]?.recovery ?? PLUGIN_SURFACE_HEAL.claude.recovery}`,
18860
+ state
18861
+ };
18862
+ case "unresolved":
18863
+ return {
18864
+ ok: false,
18865
+ label: PLUGIN_RESOLVABILITY_LABEL,
18866
+ fix: "run: mmi-cli plugin-heal",
18867
+ state
18868
+ };
18869
+ default: {
18870
+ const _exhaustive = state;
18871
+ return _exhaustive;
18872
+ }
18873
+ }
18874
+ }
18784
18875
 
18785
18876
  // src/kb-drift-report.ts
18786
18877
  var import_node_fs23 = require("node:fs");
@@ -19151,13 +19242,23 @@ var installedPluginsPath = (surface = detectSurface(process.env)) => {
19151
19242
  const homeDir = surface === "codex" ? ".codex" : ".claude";
19152
19243
  return (0, import_node_path23.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "installed_plugins.json");
19153
19244
  };
19154
- function readInstalledPlugins() {
19245
+ function readInstalledPlugins(surface = detectSurface(process.env)) {
19155
19246
  try {
19156
- return JSON.parse((0, import_node_fs26.readFileSync)(installedPluginsPath(), "utf8"));
19247
+ return JSON.parse((0, import_node_fs26.readFileSync)(installedPluginsPath(surface), "utf8"));
19157
19248
  } catch {
19158
19249
  return null;
19159
19250
  }
19160
19251
  }
19252
+ function snapshotPluginGuardInput(surface = detectSurface(process.env), isOrgRepo = false) {
19253
+ const homeDir = surface === "codex" ? ".codex" : ".claude";
19254
+ const installed = readInstalledPlugins(surface);
19255
+ return {
19256
+ isOrgRepo,
19257
+ installRecordPresent: hasUserInstallRecord(installed, MMI_PLUGIN_ID) || hasProjectInstallRecord(installed, MMI_PLUGIN_ID, process.cwd()),
19258
+ marketplaceClonePresent: (0, import_node_fs26.existsSync)((0, import_node_path23.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "marketplaces", "mutmutco")),
19259
+ pluginCachePresent: (0, import_node_fs26.existsSync)((0, import_node_path23.join)((0, import_node_os5.homedir)(), homeDir, "plugins", "cache", "mutmutco", "mmi"))
19260
+ };
19261
+ }
19161
19262
  function installedPluginSources() {
19162
19263
  return ["claude", "codex"].map((surface) => {
19163
19264
  const recordPath = (0, import_node_path23.join)((0, import_node_os5.homedir)(), `.${surface}`, "plugins", "installed_plugins.json");
@@ -19734,7 +19835,6 @@ async function runDoctor(opts, io = consoleIo) {
19734
19835
  }
19735
19836
  }
19736
19837
  checks.push(pluginCheck);
19737
- checks.push(buildSettingsPluginDriftCheck({ isOrgRepo: Boolean(cfg.sagaApiUrl), settings: claudeSettings }));
19738
19838
  let legacyPluginCheck = buildLegacyPluginInstallCheck({
19739
19839
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19740
19840
  sources: installedPluginSources(),
@@ -19791,6 +19891,18 @@ async function runDoctor(opts, io = consoleIo) {
19791
19891
  }
19792
19892
  }
19793
19893
  checks.push(driftCheck);
19894
+ checks.push(
19895
+ buildPluginResolvabilityCheck({
19896
+ ...snapshotPluginGuardInput(surface, Boolean(cfg.sagaApiUrl)),
19897
+ surface
19898
+ })
19899
+ );
19900
+ if (repairFull && Boolean(cfg.sagaApiUrl) && (surfaceToken(surface) === "claude" || surfaceToken(surface) === "codex")) {
19901
+ const guardResult = ensureUserScopeGuardHook();
19902
+ if (guardResult === "written") {
19903
+ io.err(` \u21BB installed user-scope MMI guard hook (${surface === "codex" ? "~/.codex" : "~/.claude"}/settings.json) \u2014 survives a plugin prune`);
19904
+ }
19905
+ }
19794
19906
  let installedVersionCheck = buildInstalledPluginVersionCheck({
19795
19907
  isOrgRepo: Boolean(cfg.sagaApiUrl),
19796
19908
  sources: installedPluginSources(),
@@ -20175,6 +20287,79 @@ async function runDoctor(opts, io = consoleIo) {
20175
20287
  io.log(gaps.length ? `
20176
20288
  ${gaps.length} item(s) need attention.` : "\nAll set \u2014 you are ready.");
20177
20289
  }
20290
+ var USER_SCOPE_GUARD_MARKER = "mmi-guard:v1";
20291
+ var USER_SCOPE_GUARD_COMMAND = `mmi-cli guard --session-start || true # ${USER_SCOPE_GUARD_MARKER}`;
20292
+ function settingsHasGuardHook(settings) {
20293
+ const groups = settings?.hooks?.SessionStart;
20294
+ if (!Array.isArray(groups)) return false;
20295
+ return groups.some(
20296
+ (g) => Array.isArray(g?.hooks) && g.hooks.some((h) => typeof h?.command === "string" && h.command.includes(USER_SCOPE_GUARD_MARKER))
20297
+ );
20298
+ }
20299
+ function mergeGuardHook(settings) {
20300
+ const next = settings && typeof settings === "object" ? { ...settings } : {};
20301
+ if (settingsHasGuardHook(next)) return next;
20302
+ const hooks = { ...next.hooks ?? {} };
20303
+ const sessionStart = Array.isArray(hooks.SessionStart) ? [...hooks.SessionStart] : [];
20304
+ sessionStart.push({ hooks: [{ type: "command", command: USER_SCOPE_GUARD_COMMAND, timeout: 10 }] });
20305
+ hooks.SessionStart = sessionStart;
20306
+ next.hooks = hooks;
20307
+ return next;
20308
+ }
20309
+ var userScopeSettingsPath = (surface = detectSurface(process.env)) => (0, import_node_path23.join)((0, import_node_os5.homedir)(), surface === "codex" ? ".codex" : ".claude", "settings.json");
20310
+ function ensureUserScopeGuardHook(opts = {}) {
20311
+ const path2 = opts.settingsPath ?? userScopeSettingsPath();
20312
+ try {
20313
+ let current = null;
20314
+ if ((0, import_node_fs26.existsSync)(path2)) {
20315
+ try {
20316
+ current = JSON.parse((0, import_node_fs26.readFileSync)(path2, "utf8"));
20317
+ } catch {
20318
+ return "failed";
20319
+ }
20320
+ }
20321
+ if (settingsHasGuardHook(current)) return "already";
20322
+ const merged = mergeGuardHook(current);
20323
+ (0, import_node_fs26.mkdirSync)((0, import_node_path23.dirname)(path2), { recursive: true });
20324
+ if ((0, import_node_fs26.existsSync)(path2)) (0, import_node_fs26.copyFileSync)(path2, `${path2}.bak`);
20325
+ (0, import_node_fs26.writeFileSync)(path2, `${JSON.stringify(merged, null, 2)}
20326
+ `, "utf8");
20327
+ return "written";
20328
+ } catch {
20329
+ return "failed";
20330
+ }
20331
+ }
20332
+ async function runGuard(opts = {}) {
20333
+ void opts;
20334
+ try {
20335
+ const surface = detectSurface(process.env);
20336
+ const cfg = await loadConfig();
20337
+ const input = snapshotPluginGuardInput(surface, Boolean(cfg.sagaApiUrl));
20338
+ const { state } = buildPluginGuardDecision(input);
20339
+ const { line, exitCode } = buildGuardSessionStartLine(state);
20340
+ if (line) console.error(line);
20341
+ process.exitCode = exitCode;
20342
+ } catch {
20343
+ process.exitCode = 0;
20344
+ }
20345
+ }
20346
+ async function runPluginHeal(surface = detectSurface(process.env)) {
20347
+ const token = surface === "codex" ? "codex" : "claude";
20348
+ const tableKey = surfaceToken(surface) ?? "claude";
20349
+ const descriptor = PLUGIN_SURFACE_HEAL[tableKey];
20350
+ if (!descriptor.healSteps) {
20351
+ console.log(descriptor.recovery);
20352
+ console.log(`Then: ${reloadAction(surface)}`);
20353
+ return;
20354
+ }
20355
+ const healed = await applyPluginHeal(token, surface, console.log, { force: true });
20356
+ if (healed) {
20357
+ console.log(` \u2713 MMI plugin reinstalled \u2014 ${reloadAction(surface)} to load MMI commands`);
20358
+ } else {
20359
+ console.log(` \u2717 Auto-heal failed or was skipped. Run manually:
20360
+ ${descriptor.recovery}`);
20361
+ }
20362
+ }
20178
20363
 
20179
20364
  // src/index.ts
20180
20365
  var GC_GH_TIMEOUT_MS2 = 2e4;
@@ -20637,18 +20822,21 @@ function acquireWorktreeSetupLock(worktreeRoot) {
20637
20822
  }
20638
20823
  }
20639
20824
  var worktree = program2.command("worktree").description("self-provisioning worktrees \u2014 install deps + copy local-only config");
20640
- worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch the base from", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
20825
+ worktree.command("create <branch>").description("create a worktree from a base ref and provision it (install deps + copy local-only config)").option("--from <ref>", "base ref to branch from", "origin/development").option("--path <path>", "worktree path (default: ../mmi-worktrees/<branch>)").option("--remote <name>", "remote to fetch when --from is a <remote>/<branch> ref", "origin").option("--json", "machine-readable output").action(async (branch, o) => {
20641
20826
  try {
20642
20827
  const repoRoot = (await execFileP2("git", ["rev-parse", "--show-toplevel"], { timeout: GIT_TIMEOUT_MS }).catch(() => ({ stdout: "" }))).stdout.trim() || process.cwd();
20643
20828
  const wtPath = o.path ?? defaultWorktreePath(repoRoot, branch);
20644
- const baseBranch = o.from.startsWith(`${o.remote}/`) ? o.from.slice(o.remote.length + 1) : void 0;
20645
- if (baseBranch) await execFileP2("git", ["fetch", o.remote, baseBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).catch(() => void 0);
20646
- await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, o.from], { timeout: GH_MUTATION_TIMEOUT_MS });
20829
+ const { base, fetchBranch } = resolveWorktreeBase(o.from, o.remote);
20830
+ if (fetchBranch) {
20831
+ const fetchErr = await execFileP2("git", ["fetch", o.remote, fetchBranch], { timeout: GH_MUTATION_TIMEOUT_MS }).then(() => void 0).catch((e) => (e instanceof Error ? e.message : String(e)).split("\n")[0]);
20832
+ if (fetchErr) console.error(` warning: could not fetch ${o.remote}/${fetchBranch} (${fetchErr}); base ${base} may be stale`);
20833
+ }
20834
+ await execFileP2("git", ["worktree", "add", wtPath, "-b", branch, base], { timeout: GH_MUTATION_TIMEOUT_MS });
20647
20835
  const report = await provisionWorktree(wtPath, makeProvisionDeps(wtPath, Boolean(o.json), (m) => {
20648
20836
  if (!o.json) console.error(` ${m}`);
20649
20837
  }));
20650
- if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base: o.from, ...report }, null, 2));
20651
- console.log(`worktree ready: ${wtPath} (branch ${branch} from ${o.from})`);
20838
+ if (o.json) return console.log(JSON.stringify({ branch, path: wtPath, base, ...report }, null, 2));
20839
+ console.log(`worktree ready: ${wtPath} (branch ${branch} from ${base})`);
20652
20840
  console.log(` installed: ${report.installed.map((i) => i.dir || ".").join(", ") || "none"}`);
20653
20841
  console.log(` copied: ${report.copied.join(", ") || "none"}`);
20654
20842
  } catch (e) {
@@ -22261,9 +22449,17 @@ async function runStageLiveCommand(o) {
22261
22449
  if (o.json) return console.log(JSON.stringify({ command: "stage --live", mode, slug: target.slug, repo: target.repo, ref: o.down ? void 0 : target.ref, steps }, null, 2));
22262
22450
  return console.log(renderSteps(`mmi-cli stage --live${o.down ? " --down" : ""}: dry-run plan`, steps));
22263
22451
  }
22452
+ const rcDeps = registryClientDeps(await loadConfig());
22264
22453
  const deps = {
22265
22454
  detectIp: () => detectPublicIp(),
22266
- run: async (file, args) => (await execFileP2(file, args, { timeout: GH_MUTATION_TIMEOUT_MS })).stdout
22455
+ deployDev: async ({ repo, ref }) => {
22456
+ const res = await tenantDeploy({ repo, stage: "dev", ref }, rcDeps);
22457
+ if (!res.ok) throw new Error(`dev deploy dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
22458
+ },
22459
+ control: async ({ repo, action, host, ip }) => {
22460
+ const res = await tenantControl({ repo, stage: "dev", action, host, ip }, rcDeps);
22461
+ if (!res.ok) throw new Error(`tenant control ${action} dispatch failed: ${res.body?.error ?? res.error ?? `HTTP ${res.status}`}`);
22462
+ }
22267
22463
  };
22268
22464
  try {
22269
22465
  const result = o.down ? await runStageLiveDown(deps, target) : await runStageLiveUp(deps, target);
@@ -23012,6 +23208,8 @@ program2.command("doctor").description("check onboarding gates and auto-heal CLI
23012
23208
  // Commander maps `--no-repo-writes` to `repoWrites: false`; translate to the explicit `noRepoWrites` flag.
23013
23209
  runDoctor({ ...opts, noRepoWrites: opts.repoWrites === false })
23014
23210
  ));
23211
+ program2.command("guard").description("detect a pruned/unresolved MMI plugin on disk; loud one-line stderr at session start").option("--session-start", "run in user-scope SessionStart mode").action((opts) => runGuard({ sessionStart: opts.sessionStart }));
23212
+ program2.command("plugin-heal").description("reinstall + re-enable the MMI plugin (recover from a marketplace prune)").action(() => runPluginHeal());
23015
23213
  program2.command("session-start").description("run the SessionStart verbs (rules sync, saga session+show, saga health, whoami, doctor, plan-store check) in one process; docs sync runs detached").action(async () => {
23016
23214
  if (isInsideRepoSubdir(process.cwd())) {
23017
23215
  console.error("[mmi-hook] session-start: cwd is a repository SUBDIRECTORY \u2014 skipping the SessionStart hook (spine/docs/plan/saga delivery); run it from the repo root.");
package/dist/saga.cjs CHANGED
@@ -3522,18 +3522,23 @@ function headPrompt(state) {
3522
3522
  const decisions = shownDecisions(state.decisions);
3523
3523
  const actions = (state.actionLog ?? []).slice(-HEAD_PROMPT_ACTION_LIMIT);
3524
3524
  return [
3525
- "You maintain ONE durable slot of a work-session: PINNED (things worth remembering). Given the CURRENT",
3526
- "HEAD and the recent TRANSCRIPT + DECISIONS, return an updated PINNED only. Keep it tight and concrete;",
3527
- "keep anything the user pinned; never invent; preserve Turkish characters (\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do",
3528
- "NOT manage next or the checklist \u2014 the note path owns those. The ANCHOR is the read-only North-Star \u2014",
3529
- "NEVER change it. Never restate an unverified artifact-claim (a named file, PR, flag, or board state)",
3530
- "as settled fact \u2014 keep it as the belief it was recorded as.",
3525
+ "You maintain two durable slots of a work-session: PINNED (things worth remembering) and a NEXT",
3526
+ "SUGGESTION (a best-effort one-line hint for the next useful step). Given the CURRENT HEAD and the",
3527
+ "recent TRANSCRIPT + DECISIONS, return an updated PINNED and, optionally, a next suggestion. Keep",
3528
+ "PINNED tight and concrete; keep anything the user pinned; never invent; preserve Turkish characters",
3529
+ "(\xE7 \u011F \u0131 \u0130 \xF6 \u015F \xFC) exactly. Do NOT manage the checklist \u2014 the note path owns that. The ANCHOR is the",
3530
+ "read-only North-Star \u2014 NEVER change it. Never restate an unverified artifact-claim (a named file,",
3531
+ "PR, flag, or board state) as settled fact \u2014 keep it as the belief it was recorded as.",
3531
3532
  "You MAY also propose supersessions: each DECISION is shown with its original stable 0-based index. Propose a",
3532
3533
  "supersession ONLY for a NEWER decision that directly contradicts/replaces an OLDER one where neither",
3533
3534
  "already carries a supersededBy. HIGH PRECISION \u2014 propose ONLY when you are confident the older claim",
3534
3535
  "is now false or obsolete; the newer decision's timestamp MUST be later than the older's (newer-supersedes-",
3535
- "older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor, next, or checklist.",
3536
- 'Output ONLY a JSON object: {"pinned":[string],"supersede":[{"older":int,"newer":int,"reason":string}]}.',
3536
+ "older only, never the reverse). When unsure, propose nothing. NEVER touch the anchor or checklist.",
3537
+ 'For "next": propose a single concise actionable line (\u2264140 chars) describing the most useful next step',
3538
+ "for a future session, derived from the transcript. This is a best-effort suggestion shown only when",
3539
+ "the user has not set their own NEXT. Use an empty string if nothing concrete emerges \u2014 never invent.",
3540
+ "Preserve Turkish characters exactly.",
3541
+ 'Output ONLY a JSON object: {"pinned":[string],"next":string,"supersede":[{"older":int,"newer":int,"reason":string}]}.',
3537
3542
  "",
3538
3543
  "CURRENT HEAD:",
3539
3544
  JSON.stringify(state.head ?? {}, null, 2),
@@ -3557,6 +3562,9 @@ function parseHeadUpdate(raw) {
3557
3562
  if (!obj || typeof obj !== "object") return null;
3558
3563
  const u = {};
3559
3564
  if (Array.isArray(obj.pinned)) u.pinned = obj.pinned.filter((x) => typeof x === "string");
3565
+ if (typeof obj.next === "string" && obj.next.trim()) {
3566
+ u.nextAuto = obj.next.trim().slice(0, 280);
3567
+ }
3560
3568
  if (Array.isArray(obj.supersede)) {
3561
3569
  const supersede = obj.supersede.filter((e) => {
3562
3570
  if (!e || typeof e !== "object") return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mutmutco/cli",
3
- "version": "2.52.1",
3
+ "version": "2.53.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",