@mnemom/mnemom 0.17.20-next.0 → 0.17.21

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.
@@ -52,6 +52,9 @@ export declare function assertCodeEnabled(orgId: string, orgLabel: string): Prom
52
52
  /** Ensures a Mnemom session exists (injected so tests never touch the real login flow). */
53
53
  export type EnsureSession = (opts: {
54
54
  interactive?: boolean;
55
+ /** The invitation being redeemed — carried into the browser sign-in so a new
56
+ * invitee's sign-up skips the confirmation email. */
57
+ inviteToken?: string;
55
58
  }) => Promise<boolean>;
56
59
  /** The accept route's 403 codes (mnemom-api `product-invite-accept.ts`). */
57
60
  export declare const INVITE_ACCOUNT_MISMATCH = "invite_account_mismatch";
@@ -210,7 +210,7 @@ export async function agentInviteCommand(token, opts = {}) {
210
210
  }
211
211
  const interactive = opts.interactive ?? isInteractive();
212
212
  const ensureSession = opts.ensureSession ?? defaultEnsureSession;
213
- const haveSession = await ensureSession({ interactive });
213
+ const haveSession = await ensureSession({ interactive, inviteToken: trimmed });
214
214
  if (!haveSession) {
215
215
  throw new Error("mnemom agent --invite: redeeming needs a Mnemom account.\n" +
216
216
  " Sign in: mnemom login\n" +
@@ -110,11 +110,13 @@ export declare function formatTrailEntry(e: AgentSessionVerdictTrailEntry): stri
110
110
  */
111
111
  export declare function verdictTrailKey(e: Pick<AgentSessionVerdictTrailEntry, "request_id" | "turn">): string;
112
112
  /**
113
- * Best-effort "has this session ended" check for `--follow`'s exit condition. No field is locked
114
- * in on the wire yet: MNE-8149 adds an `expired` seal state on the gateway's row, and a later
115
- * mnemom-api card is expected to carry it through to this response — so this reads defensively,
116
- * off either the top-level response or the `sealed` block, and returns null (never throws) when
117
- * neither carries a recognized value, which is every server today.
113
+ * Best-effort "has this session ended" check for `--follow`'s exit condition. MNE-8234 wired the
114
+ * seal lifecycle through on the wire (`remote.seal.state`), so that is read first and is
115
+ * authoritative: `"expired"` there means the prior seal lapsed and ends the watch. The older
116
+ * top-level `state` and `remote.sealed.state` fallbacks predate that field actually landing on
117
+ * the response — neither has ever been observed non-null on any real server — and are kept
118
+ * defensively in case an even older/non-conformant server ever carries one of them; this never
119
+ * throws, returning null when nothing recognized is present.
118
120
  */
119
121
  export declare function sessionEndedState(remote: AgentSessionResponse): "expired" | "ended" | null;
120
122
  /** Default and floor for `--follow`'s poll interval. */
@@ -152,6 +154,14 @@ export interface ShowJson {
152
154
  guardrails: AgentSessionResponse["guardrails"];
153
155
  /** R10 (MNE-8200): oldest first (the gateway's own wire order), unlike the human view. */
154
156
  verdict_trail: AgentSessionResponse["verdict_trail"];
157
+ /**
158
+ * MNE-8234: the seal's lifecycle (state incl. `expired`, version, mode, hashes). Absent on
159
+ * an older server; `null` when the API reports no seal.
160
+ */
161
+ seal: AgentSessionResponse["seal"];
162
+ /** MNE-8234 (MNE-8166 R4): judging counts under the current seal. */
163
+ unjudged_turns: AgentSessionResponse["unjudged_turns"];
164
+ memoised_turns: AgentSessionResponse["memoised_turns"];
155
165
  } | {
156
166
  source: "gateway";
157
167
  available: false;
@@ -190,14 +190,21 @@ export function verdictTrailKey(e) {
190
190
  return `${e.request_id ?? ""}#${e.turn}`;
191
191
  }
192
192
  /**
193
- * Best-effort "has this session ended" check for `--follow`'s exit condition. No field is locked
194
- * in on the wire yet: MNE-8149 adds an `expired` seal state on the gateway's row, and a later
195
- * mnemom-api card is expected to carry it through to this response — so this reads defensively,
196
- * off either the top-level response or the `sealed` block, and returns null (never throws) when
197
- * neither carries a recognized value, which is every server today.
193
+ * Best-effort "has this session ended" check for `--follow`'s exit condition. MNE-8234 wired the
194
+ * seal lifecycle through on the wire (`remote.seal.state`), so that is read first and is
195
+ * authoritative: `"expired"` there means the prior seal lapsed and ends the watch. The older
196
+ * top-level `state` and `remote.sealed.state` fallbacks predate that field actually landing on
197
+ * the response — neither has ever been observed non-null on any real server — and are kept
198
+ * defensively in case an even older/non-conformant server ever carries one of them; this never
199
+ * throws, returning null when nothing recognized is present.
198
200
  */
199
201
  export function sessionEndedState(remote) {
200
202
  const as = (v) => v === "expired" || v === "ended" ? v : null;
203
+ if (remote.seal) {
204
+ const sealState = as(remote.seal.state);
205
+ if (sealState)
206
+ return sealState;
207
+ }
201
208
  const top = as(remote.state);
202
209
  if (top)
203
210
  return top;
@@ -349,6 +356,12 @@ export function buildShowView(local, remote, reason) {
349
356
  else {
350
357
  L.push(fmt.dim(" no sealed contract on the gateway for this conversation"));
351
358
  }
359
+ // MNE-8234: the seal's lifecycle — independent of `sealed` above (which carries the contract
360
+ // body, not whether the seal itself is still live or has lapsed). Absent on an older server.
361
+ if (remote.seal) {
362
+ const modeBit = remote.seal.state === "live" ? ` · mode ${remote.seal.mode}` : "";
363
+ L.push(fmt.label("seal: ", `${remote.seal.state.toUpperCase()} (v${remote.seal.version})${modeBit}`));
364
+ }
352
365
  if (remote.verdict) {
353
366
  const v = remote.verdict;
354
367
  L.push(fmt.label("verdict: ", `${v.verdict.toUpperCase()} at turn ${v.turn} — ${v.reason}`));
@@ -385,6 +398,14 @@ export function buildShowView(local, remote, reason) {
385
398
  if (remote.turn !== null) {
386
399
  L.push(fmt.label("turn: ", String(remote.turn)));
387
400
  }
401
+ // MNE-8234 (MNE-8166 R4): judging counts under the current seal. `null`/absent ⇒ no judge
402
+ // ledger counts (older server, or nothing judged under this seal yet) — no line then.
403
+ if (remote.unjudged_turns !== null && remote.unjudged_turns !== undefined) {
404
+ const memoisedBit = remote.memoised_turns !== null && remote.memoised_turns !== undefined
405
+ ? ` · memoised ${remote.memoised_turns}`
406
+ : "";
407
+ L.push(fmt.label("judging: ", `unjudged ${remote.unjudged_turns}${memoisedBit}`));
408
+ }
388
409
  return L;
389
410
  }
390
411
  export function buildShowJson(local, remote, reason) {
@@ -400,6 +421,9 @@ export function buildShowJson(local, remote, reason) {
400
421
  turn: remote.turn,
401
422
  guardrails: remote.guardrails,
402
423
  verdict_trail: remote.verdict_trail,
424
+ seal: remote.seal,
425
+ unjudged_turns: remote.unjudged_turns,
426
+ memoised_turns: remote.memoised_turns,
403
427
  }
404
428
  : { source: "gateway", available: false, reason: reason ?? "unknown" },
405
429
  };
@@ -218,6 +218,7 @@ export declare function isQuietPreambleExit(err: unknown): boolean;
218
218
  */
219
219
  export declare function ensureMnemomSession(opts?: {
220
220
  interactive?: boolean;
221
+ inviteToken?: string;
221
222
  }): Promise<boolean>;
222
223
  /**
223
224
  * Enforce a positive Mnemom Units balance on the org the agent lives in.
@@ -621,16 +621,26 @@ export async function ensureMnemomSession(opts = {}) {
621
621
  if (!(await askYesNo("Sign in or create your account now?", true)))
622
622
  return false;
623
623
  const device = headlessEnv();
624
+ // The device-code page has no way to carry the invitation, so a NEW invitee
625
+ // signing up there gets the confirmation email — say so instead of dropping
626
+ // the invitation silently. (The browser path carries it; see loginWithLoopback.)
627
+ const deviceSignIn = async () => {
628
+ if (opts.inviteToken) {
629
+ say(fmt.dim(" New to Mnemom? Signing up from a device code asks you to confirm your email first."));
630
+ say(fmt.dim(" To skip that, open the link in your invitation email on a machine with a browser."));
631
+ }
632
+ await loginWithDeviceFlow();
633
+ };
624
634
  try {
625
635
  if (device)
626
- await loginWithDeviceFlow();
636
+ await deviceSignIn();
627
637
  else
628
- await loginWithBrowser();
638
+ await loginWithBrowser({ inviteToken: opts.inviteToken });
629
639
  }
630
640
  catch (err) {
631
641
  if (!device) {
632
642
  say(fmt.dim(" Browser sign-in didn't work here — falling back to a device code…"));
633
- await loginWithDeviceFlow();
643
+ await deviceSignIn();
634
644
  }
635
645
  else {
636
646
  throw err;
@@ -124,6 +124,13 @@ export interface AgentOptions {
124
124
  * session-config header's `experimental` array. Absent/empty ⇒ none of the three.
125
125
  */
126
126
  experimental?: readonly ExperimentalId[];
127
+ /**
128
+ * Load the "Mnemom for Claude Code" plugin (`plugins/mnemom`: the Mnemom band,
129
+ * `/mnemom`, the gateway guard, Mnemom attribution) into the launched Claude Code.
130
+ * Default OFF (the plugin lands dark); `--plugin` ⇒ true; `--no-plugin` ⇒ false; undefined ⇒
131
+ * MNEMOM_AGENT_PLUGIN decides (lib/claude-plugin).
132
+ */
133
+ plugin?: boolean;
127
134
  passthrough?: string[];
128
135
  /** Print the resolved launch plan and exit — no key read, no spawn. */
129
136
  dryRun?: boolean;
@@ -335,6 +342,8 @@ export declare function buildCliArgs(options: AgentOptions, ctx: {
335
342
  allowCliFlags: boolean;
336
343
  ccSessionId?: string;
337
344
  handoffMcpConfigPath?: string;
345
+ /** The bundled Mnemom plugin folder → `--plugin-dir <dir>` (lib/claude-plugin). */
346
+ pluginDir?: string;
338
347
  }): string[];
339
348
  /**
340
349
  * The Claude Code session id a launch runs under, from the caller's passthrough (implicit-goal
@@ -365,6 +374,8 @@ export declare function logLaunch(ctx: {
365
374
  envelope: SealedContractEnvelope | null;
366
375
  goalMode: string | null;
367
376
  cli: ResolvedCli;
377
+ /** The plugin line (`pluginPlanLine`): `off`, `off (<why>)` or `<name> (<dir>)`. */
378
+ plugin: string;
368
379
  }): void;
369
380
  /** The launch line for the yolo default: permission prompts are skipped, and how to keep them. */
370
381
  export declare function permissionPromptsLine(cli: ResolvedCli): string;
@@ -392,5 +403,7 @@ export declare function printPlan(ctx: {
392
403
  sessionConfig: Record<string, string>;
393
404
  /** The experimental features on for this launch (the header's `experimental` array). */
394
405
  experimental?: readonly string[];
406
+ /** The bundled Claude Code plugin line (`pluginPlanLine`); always printed, like the launch log. */
407
+ plugin: string;
395
408
  }): void;
396
409
  export {};
@@ -26,7 +26,7 @@
26
26
  * an argv value.
27
27
  */
28
28
  import { spawn, spawnSync } from "node:child_process";
29
- import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
29
+ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, renameSync, } from "node:fs";
30
30
  import { randomUUID } from "node:crypto";
31
31
  import { tmpdir, homedir, hostname } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
@@ -48,6 +48,8 @@ import { defaultAgentSlug, ensureClaimedServedAgent, ensureSessionAndBalance, is
48
48
  import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfig, buildSessionConfigB64, assertCompactAtCompatible, assertOffProfileCompatible, compactAtWindowWarning, describeCompactAt, launchWindow, resolveCompactAt, applyGatewayHintHeaders, resolveCompactionIntercept, resolveContextHandoff, resolveContextProfile, resolveHandoffGatewayHost, resolveUsageSavings, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, applyToolSearchDefault, DISABLE_1M_ENV, readCcSettings, resolveWindowModel, resolveAnthropicDoor, resolveLaunchModel, resolveConversationId, resolveGatewayHost, singleCellError, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, resumeCommand, sessionDisplayName, SESSION_NAME_ENV, SESSION_CONFIG_HEADER, } from "../lib/agent.js";
49
49
  import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
50
50
  import { slackChannelArgs } from "../lib/slack-launch.js";
51
+ import { appendPluginDirsEnv, applyPluginEnv, pluginEnabled, pluginPlanLine, resolvePluginDir, } from "../lib/claude-plugin.js";
52
+ import { CLI_VERSION } from "../version.js";
51
53
  /** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
52
54
  const CODE_STORE = LAUNCHER_STORE_PATH;
53
55
  const say = (line = "") => console.error(line);
@@ -194,6 +196,13 @@ export async function agentCommand(scenarioArg, options = {}) {
194
196
  if (cellError)
195
197
  throw new Error(cellError);
196
198
  const cellNote = isProdGateway(env) ? "us-2/prod" : `custom (${gatewayHost})`;
199
+ // ── the bundled Claude Code plugin (lib/claude-plugin) ── resolved up front so the dry-run plan
200
+ // and the launch log print the same line. Only Claude Code proper takes `--plugin-dir`.
201
+ const pluginWanted = pluginEnabled(options.plugin, env);
202
+ const bundledPluginDir = pluginWanted ? resolvePluginDir() : null;
203
+ const pluginFor = (cliName) => cliName !== "claude" && pluginWanted
204
+ ? { dir: null, line: `off (${cliName} is not Claude Code)` }
205
+ : { dir: bundledPluginDir, line: pluginPlanLine(bundledPluginDir, pluginWanted) };
197
206
  const yolo = options.yolo !== false; // default ON
198
207
  const contextHint = options.contextHint !== false; // default ON
199
208
  // Context profile: none (null — the cell's own default, no fold key sent) unless
@@ -303,6 +312,7 @@ export async function agentCommand(scenarioArg, options = {}) {
303
312
  compactAt,
304
313
  sessionConfig,
305
314
  experimental,
315
+ plugin: pluginFor(planCli.name).line,
306
316
  });
307
317
  if (auth === "subscription") {
308
318
  say("mnemom agent: auth = subscription — Claude Code would use your claude.ai login (Team/Enterprise plans only) with a Mnemom API key; no Anthropic key.");
@@ -516,7 +526,46 @@ export async function agentCommand(scenarioArg, options = {}) {
516
526
  contextProfile,
517
527
  compactAt: contextProfile === "off" ? null : compactAt,
518
528
  });
519
- logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
529
+ const launchPlugin = pluginFor(cli.name);
530
+ logLaunch({
531
+ shape,
532
+ door,
533
+ agent,
534
+ conversationId,
535
+ cellNote,
536
+ envelope,
537
+ goalMode,
538
+ cli,
539
+ plugin: launchPlugin.line,
540
+ });
541
+ if (pluginWanted && cli.name === "claude" && !bundledPluginDir) {
542
+ say(fmt.warn("mnemom agent: the Mnemom plugin folder is not in this install — launching without the Mnemom band and /mnemom."));
543
+ }
544
+ // Non-secret launch facts for the plugin's band and `/mnemom status`. Built here, applied to the
545
+ // child env by each launch shape AFTER it has built that env (lib/claude-plugin, PLUGIN_ENV).
546
+ const plugin = launchPlugin.dir
547
+ ? {
548
+ dir: launchPlugin.dir,
549
+ launch: {
550
+ gatewayHost,
551
+ apiUrl: getApiUrl(),
552
+ agent,
553
+ conversationId,
554
+ cellNote,
555
+ shape,
556
+ contractSha: envelope?.sha,
557
+ contract: envelope?.canonical,
558
+ goalMode,
559
+ scenario,
560
+ launcherVersion: CLI_VERSION,
561
+ // The mod re-runs this CLI (`agent show --json`, `agent goal show --json`) for the live
562
+ // goal state; execArgv keeps a `tsx`/`--import` dev launch runnable the same way.
563
+ launcherExec: process.argv[1]
564
+ ? [process.execPath, ...process.execArgv, process.argv[1]]
565
+ : [],
566
+ },
567
+ }
568
+ : undefined;
520
569
  if (expNotice)
521
570
  say(expNotice);
522
571
  say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
@@ -586,6 +635,7 @@ export async function agentCommand(scenarioArg, options = {}) {
586
635
  ccSessionId: id,
587
636
  }),
588
637
  knownCcSessionId: cc.id ?? continuedCcSessionId,
638
+ plugin,
589
639
  });
590
640
  return; // launchTerminal exits the process
591
641
  }
@@ -602,6 +652,7 @@ export async function agentCommand(scenarioArg, options = {}) {
602
652
  goalMode,
603
653
  sessionConfigB64,
604
654
  handoffMcpConfigPath,
655
+ plugin,
605
656
  options: launchOptions,
606
657
  yolo,
607
658
  ccSessionId,
@@ -1081,6 +1132,7 @@ function launchTerminal(ctx) {
1081
1132
  allowCliFlags: true,
1082
1133
  ccSessionId: ctx.ccSessionId,
1083
1134
  handoffMcpConfigPath: ctx.handoffMcpConfigPath,
1135
+ pluginDir: ctx.plugin?.dir,
1084
1136
  });
1085
1137
  // Claude Code logged in via claude.ai (OAuth) PREEMPTS ANTHROPIC_API_KEY, so
1086
1138
  // it is force-unset; the real key rides in ANTHROPIC_CUSTOM_HEADERS and
@@ -1102,6 +1154,9 @@ function launchTerminal(ctx) {
1102
1154
  // Both auth arms point ANTHROPIC_BASE_URL at the gateway door, which makes Claude Code turn
1103
1155
  // tool search off and inline every tool schema on every request — unless told otherwise.
1104
1156
  applyToolSearchDefault(childEnv);
1157
+ // The plugin's non-secret launch facts (MNEMOM_CC_*): never a key, never a header value.
1158
+ if (ctx.plugin)
1159
+ applyPluginEnv(childEnv, ctx.plugin.launch);
1105
1160
  const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
1106
1161
  childEnv[SESSION_NAME_ENV] = sessionName;
1107
1162
  // Exact compaction signal for the gateway (x-claude-code-request-class: compaction), on every
@@ -1248,7 +1303,8 @@ function launchRemoteControl(ctx) {
1248
1303
  }
1249
1304
  }
1250
1305
  const proxyScript = resolveProxyScript();
1251
- const rcDir = mkdtempSync(join(tmpdir(), "mnemom-code-rc."));
1306
+ const rcDir = mkdtempSync(join(rcStateRoot(), "mnemom-code-rc."));
1307
+ writeFileSync(join(rcDir, "owner.pid"), `${process.pid}\n`, { mode: 0o600 }); // rc-state.mjs sweep
1252
1308
  const proxyLog = process.env.MNEMOM_AGENT_RC_PROXY_LOG || join(rcDir, "proxy.log");
1253
1309
  const proxy = spawn("node", [proxyScript], {
1254
1310
  env: {
@@ -1397,6 +1453,10 @@ function spawnRcCli(ctx) {
1397
1453
  if (ctx.handoffMcpConfigPath && ctx.anthropicKey) {
1398
1454
  childEnv[HANDOFF_RC_KEY_ENV_NAME] = ctx.anthropicKey;
1399
1455
  }
1456
+ // The plugin's non-secret launch facts (MNEMOM_CC_*), also after the strip loop: not an
1457
+ // ANTHROPIC_* var, so the RC gate ignores them, and nothing here is a key or a header value.
1458
+ if (ctx.plugin)
1459
+ applyPluginEnv(childEnv, ctx.plugin.launch);
1400
1460
  let args;
1401
1461
  if (ctx.shape === "server") {
1402
1462
  // Headless dispatcher: no --model/--effort/--trim-mcp/yolo flags; the model
@@ -1408,6 +1468,10 @@ function spawnRcCli(ctx) {
1408
1468
  say("mnemom agent: --slack is not applied to a headless server (a channel server needs an interactive confirmation); use the terminal or remote-control shape.");
1409
1469
  }
1410
1470
  args = ["remote-control", "--name", rcName, ...passthrough];
1471
+ // No `--plugin-dir` on the headless server: the env path list loads the plugin into every
1472
+ // session it spawns instead (a user's own list is kept).
1473
+ if (ctx.plugin)
1474
+ appendPluginDirsEnv(childEnv, ctx.plugin.dir);
1411
1475
  // Additive, never `--strict-mcp-config`: --trim-mcp is not applied under --server (every
1412
1476
  // dispatched session keeps its full MCP set, unrestricted — see warnServerDrops below), so
1413
1477
  // the handoff tool rides in on TOP of that set rather than replacing it.
@@ -1424,6 +1488,7 @@ function spawnRcCli(ctx) {
1424
1488
  allowCliFlags: true,
1425
1489
  ccSessionId: ctx.ccSessionId,
1426
1490
  handoffMcpConfigPath: ctx.handoffMcpConfigPath,
1491
+ pluginDir: ctx.plugin?.dir,
1427
1492
  });
1428
1493
  args = ["--remote-control", rcName, ...flagArgs];
1429
1494
  if (ctx.options.slack)
@@ -1455,6 +1520,23 @@ function warnServerDrops(options, yolo) {
1455
1520
  say("mnemom agent: yolo (--dangerously-skip-permissions) is not applied under --server; set permissions from the driven sessions.");
1456
1521
  }
1457
1522
  }
1523
+ /**
1524
+ * ~/.mnemom/rc, created 0700 — the same root src/rc-state.mjs gives the proxy's CA dir (tsc emits
1525
+ * no .mjs, so this mirrors its rcStateRoot() rather than importing it). Never the OS temp dir,
1526
+ * whose cleaners delete files behind a long session; that is only the fallback when home is not
1527
+ * writable. Read at call time, not from MNEMOM_DIR: HOME can move after import (tests do).
1528
+ */
1529
+ function rcStateRoot() {
1530
+ const root = join(homedir(), ".mnemom", "rc");
1531
+ try {
1532
+ mkdirSync(root, { recursive: true, mode: 0o700 });
1533
+ chmodSync(root, 0o700);
1534
+ return root;
1535
+ }
1536
+ catch {
1537
+ return tmpdir();
1538
+ }
1539
+ }
1458
1540
  function resolveProxyScript() {
1459
1541
  const here = dirname(fileURLToPath(import.meta.url));
1460
1542
  const cand = join(here, "..", "rc-proxy.mjs");
@@ -1540,6 +1622,8 @@ export function buildCliArgs(options, ctx) {
1540
1622
  args.push("--dangerously-skip-permissions");
1541
1623
  if (ctx.ccSessionId)
1542
1624
  args.push("--session-id", ctx.ccSessionId);
1625
+ if (ctx.pluginDir)
1626
+ args.push("--plugin-dir", ctx.pluginDir);
1543
1627
  }
1544
1628
  if (options.passthrough?.length)
1545
1629
  args.push(...options.passthrough);
@@ -1650,6 +1734,7 @@ export function goalAlignmentMode(envelope, goalMode) {
1650
1734
  }
1651
1735
  export function logLaunch(ctx) {
1652
1736
  say(`mnemom agent: routing ${ctx.cli.name} through ${ctx.door} [${ctx.cellNote}] as '${ctx.agent}' (conversation ${ctx.conversationId}).`);
1737
+ say(`mnemom agent: plugin: ${ctx.plugin}`);
1653
1738
  if (ctx.envelope) {
1654
1739
  say(`mnemom agent: contract statement: ${ctx.envelope.contract.statement}`);
1655
1740
  say(`mnemom agent: contract sha256[0:16]: ${ctx.envelope.sha}`);
@@ -1673,6 +1758,7 @@ export function printPlan(ctx) {
1673
1758
  L.push("mnemom agent — launch plan (dry run)");
1674
1759
  L.push(` scenario: ${ctx.scenario}`);
1675
1760
  L.push(` cli: ${ctx.cli.name} (${ctx.cli.bin})`);
1761
+ L.push(` plugin: ${ctx.plugin}`);
1676
1762
  L.push(` shape: ${ctx.shape}`);
1677
1763
  L.push(` door: ${ctx.door} [${ctx.cellNote}]`);
1678
1764
  L.push(` agent: ${ctx.agent}`);
@@ -3,7 +3,7 @@
3
3
  * integration). Surface for engineers to check whether staging is safe
4
4
  * to promote to production based on the most recent harness run state.
5
5
  *
6
- * mnemom validate safe-house [--against=staging] [--max-age=24h]
6
+ * mnemom validate safe-house [--against=test] [--max-age=24h]
7
7
  * [--lane=full|fast] [--strict] [--json]
8
8
  *
9
9
  * Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
@@ -3,7 +3,7 @@
3
3
  * integration). Surface for engineers to check whether staging is safe
4
4
  * to promote to production based on the most recent harness run state.
5
5
  *
6
- * mnemom validate safe-house [--against=staging] [--max-age=24h]
6
+ * mnemom validate safe-house [--against=test] [--max-age=24h]
7
7
  * [--lane=full|fast] [--strict] [--json]
8
8
  *
9
9
  * Wraps `GET /v1/admin/safe-house/harness-state` (mnemom-staff only).
@@ -76,10 +76,12 @@ export async function validateSafeHouseCommand(opts) {
76
76
  await requireAuth();
77
77
  // `--against` is informational only today (validation is always against
78
78
  // the api root the user is authenticated to). Keep the flag for forward-
79
- // compat: production gains a separate harness corpus per T1.
80
- const against = opts.against ?? "staging";
81
- if (against !== "staging" && against !== "production") {
82
- console.log(fmt.error(`--against must be 'staging' or 'production' (got '${against}')`) + "\n");
79
+ // compat: production gains a separate harness corpus per T1. The non-prod
80
+ // value is `test` (the Azure test cell): `staging` named the retired ring
81
+ // (MNE-7968) and is rejected so a stale script fails loud, not quietly.
82
+ const against = opts.against ?? "test";
83
+ if (against !== "test" && against !== "production") {
84
+ console.log(fmt.error(`--against must be 'test' or 'production' (got '${against}')`) + "\n");
83
85
  process.exit(1);
84
86
  return;
85
87
  }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import "./lib/retired-env-guard.js";
2
3
  import { program } from "commander";
3
4
  export { program };
4
5
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ // MUST stay the first import: it exits cleanly on MNEMOM_ENV=staging before any
3
+ // module below resolves the (retired) staging URLs at import time (MNE-7968).
4
+ import "./lib/retired-env-guard.js";
2
5
  import { program, InvalidArgumentError, Option } from "commander";
3
6
  import { isEntrypoint } from "./lib/entrypoint.js";
4
7
  import { CLI_VERSION } from "./version.js";
@@ -141,7 +144,7 @@ function printAgentHelp(verb) {
141
144
  const agentCmd = program
142
145
  .command("agent [session] [passthrough...]")
143
146
  .description("Launch your governed agent (Claude Code in v1) through the Mnemom gateway — governed identity + optional sealed goal contract. Plain `mnemom agent` asks for a session name. " +
144
- "Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `invite <token>` redeems a Mnemom Agent invitation; `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state; `help [verb]` prints help and never launches.")
147
+ "Sub-verbs: `setup` provisions your governed agent; `doctor` preflights prerequisites; `config …` manages persistent settings (~/.mnemom/launcher.toml); `--invite <token>` redeems a Mnemom Agent invitation (the one-liner your invitation email shows; `invite <token>` is its alias); `list` / `claim` / `move` manage the agents in your orgs; `sessions` lists your past launches and `show [ref]` merges one with its live goal state; `help [verb]` prints help and never launches.")
145
148
  .option("--goal <statement>", "Contract goal statement (required if any contract flag is given)")
146
149
  .option("--requirement <text>", "Contract requirement (repeatable)", collectRepeatable, [])
147
150
  .option("--allow <glob>", "Allowed path/glob (repeatable)", collectRepeatable, [])
@@ -182,6 +185,11 @@ const agentCmd = program
182
185
  .option("--no-governance", "Turn governance OFF for the agent's cards, autonomy policy checks included (by default new agents run autonomy observe, rules only; a card refresh otherwise keeps an existing agent's modes)")
183
186
  .option("--no-yolo", "Do NOT pass --dangerously-skip-permissions (yolo is on by default)")
184
187
  .option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
188
+ // The Mnemom plugin lands dark: off by default and not in the published package, so its two
189
+ // flags stay out of --help until the shipping PR turns it on (lib/claude-plugin.ts). They still
190
+ // parse, for a source checkout.
191
+ .addOption(new Option("--plugin", "Load the Mnemom plugin (the Mnemom band, /mnemom, the gateway guard) into Claude Code — off by default and not in the published package yet (needs a source checkout; MNEMOM_AGENT_PLUGIN=1 does the same)").hideHelp())
192
+ .addOption(new Option("--no-plugin", "Do NOT load the Mnemom plugin even when MNEMOM_AGENT_PLUGIN=1").hideHelp())
185
193
  .addOption(new Option("--slack", "EXPERIMENTAL (`mnemom experimental enable slack`): attach the Slack channel — messages addressed to this session arrive in it, and it can reply (see `mnemom agent slack`)").hideHelp(hiddenUnless("slack")))
186
194
  .option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
187
195
  .option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
@@ -366,6 +374,7 @@ const agentCmd = program
366
374
  trimMcp: opts.trimMcp,
367
375
  yolo: opts.yolo,
368
376
  contextHint: opts.contextHint,
377
+ plugin: opts.plugin,
369
378
  slack: opts.slack ?? cfg.slackListen,
370
379
  context: opts.context,
371
380
  contextProfile: opts.contextProfile,
@@ -1650,7 +1659,7 @@ const validateCmd = program
1650
1659
  validateCmd
1651
1660
  .command("safe-house")
1652
1661
  .description("Render Safe House harness state per lane (full/fast) — promotion-readiness signal")
1653
- .option("--against <env>", "Target environment ('staging' or 'production'; informational)", "staging")
1662
+ .option("--against <env>", "Target environment ('test' or 'production'; informational)", "test")
1654
1663
  .option("--max-age <duration>", "Reject runs older than this (e.g. 30m, 2h, 24h, 1d)", "24h")
1655
1664
  .option("--lane <lane>", "Show only one lane ('full', 'fast', or 'both')", "both")
1656
1665
  .option("--strict", "Exit non-zero if any selected lane is stale or red")
package/dist/lib/api.d.ts CHANGED
@@ -1305,6 +1305,29 @@ export interface AgentSessionVerdictTrailEntry {
1305
1305
  fence_breach: boolean | null;
1306
1306
  at: string;
1307
1307
  }
1308
+ /**
1309
+ * The seal's lifecycle (MNE-8149 R2, MNE-8199 D9/R9, MNE-8234/mnemom-api#3541). `live`: a sealed
1310
+ * contract currently governs — `mode` says whether a crossed guardrail only nudges or enforces
1311
+ * (refuses the next request). `expired`: the prior seal lapsed (24h idle) and the next engaged
1312
+ * request will seal afresh; no `mode` because nothing is being enforced.
1313
+ */
1314
+ export interface AgentSessionSealLive {
1315
+ state: "live";
1316
+ version: number;
1317
+ hash: string;
1318
+ sealed_at: string;
1319
+ reason: "first" | "after_expiry" | null;
1320
+ expired_hash: string | null;
1321
+ updated_from_hash: string | null;
1322
+ mode: "nudge" | "enforce";
1323
+ }
1324
+ export interface AgentSessionSealExpired {
1325
+ state: "expired";
1326
+ version: number;
1327
+ hash: string;
1328
+ sealed_at: string;
1329
+ }
1330
+ export type AgentSessionSeal = AgentSessionSealLive | AgentSessionSealExpired;
1308
1331
  /** `GET /v1/agent/sessions/{conversationId}` 200 body. Nulls mean "nothing stored" — absence is data. */
1309
1332
  export interface AgentSessionResponse {
1310
1333
  conversation_id: string;
@@ -1319,6 +1342,24 @@ export interface AgentSessionResponse {
1319
1342
  guardrails: AgentSessionGuardrails | null;
1320
1343
  /** Absent on an older server (back-compat) — never shown then, never an empty-array assumption. */
1321
1344
  verdict_trail?: AgentSessionVerdictTrailEntry[];
1345
+ /**
1346
+ * MNE-8234: the seal's lifecycle, incl. `expired`. Absent on an older server (back-compat —
1347
+ * never shown then). `null` means the API itself reports no seal: either none was ever made for
1348
+ * this conversation, or the gateway sent a seal payload out of contract and the API dropped it
1349
+ * rather than relay it half-trusted — the two are indistinguishable from this field alone.
1350
+ */
1351
+ seal?: AgentSessionSeal | null;
1352
+ /**
1353
+ * MNE-8166 R4 / MNE-8234: assistant turns under the current seal with no verdict (a cut-off/
1354
+ * aborted response, a judge that failed or never ran, a non-streamed final turn). Absent on an
1355
+ * older server (back-compat). `null` when no judge ledger counts under the current seal.
1356
+ */
1357
+ unjudged_turns?: number | null;
1358
+ /**
1359
+ * MNE-8166 R4 / MNE-8234: turns the judge memo graded (aligned by inheritance, no verdict
1360
+ * stored). Absent on an older server (back-compat). `null` alongside `unjudged_turns`.
1361
+ */
1362
+ memoised_turns?: number | null;
1322
1363
  }
1323
1364
  /**
1324
1365
  * The live goal state of one `mnemom agent` session, read through mnemom-api
@@ -99,7 +99,9 @@ export declare function isLoggedIn(): Promise<boolean>;
99
99
  * loopback redirect (the `wrangler login` pattern). Persists the resulting
100
100
  * SCOPED tokens (plus the client_id needed to refresh) to ~/.mnemom/auth.json.
101
101
  */
102
- export declare function loginWithBrowser(): Promise<AuthTokens>;
102
+ export declare function loginWithBrowser(opts?: {
103
+ inviteToken?: string;
104
+ }): Promise<AuthTokens>;
103
105
  /**
104
106
  * Headless login: RFC 8628 device authorization grant. Shows a user_code +
105
107
  * verification_uri for the user to approve in any browser (possibly on another
package/dist/lib/auth.js CHANGED
@@ -212,8 +212,8 @@ export async function isLoggedIn() {
212
212
  * loopback redirect (the `wrangler login` pattern). Persists the resulting
213
213
  * SCOPED tokens (plus the client_id needed to refresh) to ~/.mnemom/auth.json.
214
214
  */
215
- export async function loginWithBrowser() {
216
- const { tokens, clientId } = await loginWithLoopback();
215
+ export async function loginWithBrowser(opts = {}) {
216
+ const { tokens, clientId } = await loginWithLoopback(undefined, opts);
217
217
  const stored = fromOAuthTokens(tokens, clientId);
218
218
  saveAuthTokens(stored);
219
219
  return stored;