@mnemom/mnemom 0.17.2-next.2 → 0.17.2-next.4

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/index.js CHANGED
@@ -11,6 +11,7 @@ import { policyInitCommand, policyValidateCommand, policyPublishCommand, policyL
11
11
  import { protectionShowCommand, protectionPublishCommand, protectionValidateCommand, protectionEditCommand, protectionDriftCommand, } from "./commands/protection.js";
12
12
  import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./commands/agents.js";
13
13
  import { agentSessionsCommand, agentShowCommand, liveSessionDeps, } from "./commands/agent-sessions.js";
14
+ import { agentGoalPinCommand, agentGoalResetCommand, agentGoalShowCommand, liveGoalDeps, } from "./commands/agent-goal.js";
14
15
  import { orgListCommand, orgShowCommand, orgUseCommand } from "./commands/org.js";
15
16
  import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
16
17
  import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
@@ -113,8 +114,8 @@ const agentCmd = program
113
114
  .option("--budget <usd>", "Guardrail: spend ceiling in USD (positive number)")
114
115
  .option("--max-turns <n>", "Guardrail: turn ceiling (positive integer)")
115
116
  .option("--stall <n>", "Guardrail: consecutive no-write turns (positive integer)")
116
- .option("--implicit", "Goal alignment without a sealed contract: the gateway infers the goal from your messages and revises it as you steer (not combinable with --goal/--requirement/--allow/--forbid/--goal-id; the guardrail ceilings still apply)")
117
- .option("--no-implicit", "Goal alignment off for this launch even when launcher.toml sets goal_mode = implicit")
117
+ .option("--implicit", "Implicit goal alignment (the default): the gateway infers the goal from your messages and revises it as you steer (not combinable with --goal/--requirement/--allow/--forbid/--goal-id; the guardrail ceilings still apply)")
118
+ .option("--no-implicit", "Turn goal alignment OFF for this launch (implicit alignment is on by default; also overrides launcher.toml goal_mode = implicit)")
118
119
  .option("--conversation-id <id>", "x-mnemom-conversation-id (default: auto-generated)")
119
120
  .option("--cli <name|path>", "Coding-agent CLI to launch: a name or full path (default: claude)")
120
121
  .option("--gateway <url>", "Gateway host override (default: https://gateway.mnemom.ai; or MNEMOM_AGENT_GATEWAY)")
@@ -136,6 +137,8 @@ const agentCmd = program
136
137
  .option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
137
138
  .option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
138
139
  .option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
140
+ .option("--context-handoff", "EXPERIMENTAL, off by default: register the `handoff(note)` MCP tool and tell the gateway to turn on the context-handoff capability for this session")
141
+ .option("--no-context-handoff", "Keep context handoff off for this launch even when launcher.toml (context_handoff = true) or MNEMOM_AGENT_CONTEXT_HANDOFF turns it on")
139
142
  .option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
140
143
  .action(async (scenario, passthrough, opts) => {
141
144
  try {
@@ -218,6 +221,7 @@ const agentCmd = program
218
221
  yolo: opts.yolo,
219
222
  contextHint: opts.contextHint,
220
223
  context: opts.context,
224
+ contextHandoff: opts.contextHandoff,
221
225
  setup: opts.setup,
222
226
  keySource: cfg.keySource,
223
227
  passthrough,
@@ -730,6 +734,69 @@ agentCmd
730
734
  process.exit(1);
731
735
  }
732
736
  });
737
+ // `mnemom agent goal show|pin|reset` — the implicit goal the gateway inferred for a session
738
+ // (implicit-goal CLI). The handlers set process.exitCode themselves (1 / 2); the catch is for
739
+ // the unexpected.
740
+ const agentGoalCmd = agentCmd
741
+ .command("goal")
742
+ .description("Read and steer the goal the gateway inferred for an implicit-mode session (show / pin / reset). " +
743
+ "Authenticates with the same key and agent the session launched with");
744
+ const collectRepeat = (value, prev = []) => [...prev, value];
745
+ agentGoalCmd
746
+ .command("show [ref]")
747
+ .description("Show the session's live inferred goal: revision, pin, statement, requirements, fences and the latest verdict. " +
748
+ "`ref` is a session name (newest match wins) or a unique conversation-id prefix; omitted = the newest session")
749
+ .option("--thread <id>", "Goal thread: main (default) or a thread id such as a.<agent-id>")
750
+ .option("--all-threads", "List every goal thread of the conversation (main + subagents)")
751
+ .option("--history", "Add the revision history table")
752
+ .option("--json", "Output the gateway body (source gateway) plus the ledger row (source ledger)")
753
+ .action(async (ref, opts) => {
754
+ try {
755
+ await agentGoalShowCommand(ref, opts, liveGoalDeps);
756
+ }
757
+ catch (error) {
758
+ console.error("Error:", error instanceof Error ? error.message : error);
759
+ process.exit(1);
760
+ }
761
+ });
762
+ agentGoalCmd
763
+ .command("pin [ref]")
764
+ .description("Pin the current inferred goal so new messages no longer revise it (--off unpins)")
765
+ .option("--thread <id>", "Goal thread: main (default) or a thread id such as a.<agent-id>")
766
+ .option("--off", "Unpin: new directions revise the goal again")
767
+ .option("--json", "Output the gateway body as JSON")
768
+ .action(async (ref, opts) => {
769
+ try {
770
+ await agentGoalPinCommand(ref, opts, liveGoalDeps);
771
+ }
772
+ catch (error) {
773
+ console.error("Error:", error instanceof Error ? error.message : error);
774
+ process.exit(1);
775
+ }
776
+ });
777
+ agentGoalCmd
778
+ .command("reset [ref]")
779
+ .description("Clear the inferred goal (the next message re-infers it), or replace it with --statement. " +
780
+ "Revisions keep counting up; spend and turn ceilings are never refunded")
781
+ .option("--thread <id>", "Goal thread: main (default) or a thread id such as a.<agent-id>")
782
+ .option("--statement <s>", "Replace the goal with this statement instead of clearing it")
783
+ .option("--requirement <r>", "A requirement for --statement (repeatable)", collectRepeat)
784
+ .option("--yes", "Skip the confirmation (required without a TTY)")
785
+ .option("--json", "Output the gateway body as JSON")
786
+ .action(async (ref, opts, cmd) => {
787
+ try {
788
+ // `mnemom agent` itself declares `--requirement` (a contract flag), and commander hands a
789
+ // parent's option to the parent wherever it appears, so the values land on `agent`.
790
+ // Read them from there when this command saw none.
791
+ const inherited = cmd.optsWithGlobals().requirement;
792
+ const requirement = opts.requirement ?? inherited;
793
+ await agentGoalResetCommand(ref, { ...opts, requirement }, liveGoalDeps);
794
+ }
795
+ catch (error) {
796
+ console.error("Error:", error instanceof Error ? error.message : error);
797
+ process.exit(1);
798
+ }
799
+ });
733
800
  // ============================================================================
734
801
  // Organization management (ADR-044, Piece 1 of T1-3.1)
735
802
  // ============================================================================
@@ -55,6 +55,14 @@ export interface SessionRecord {
55
55
  * captured).
56
56
  */
57
57
  goal_mode?: GoalMode;
58
+ /**
59
+ * The Claude Code session id the launch pinned with `--session-id` (or the one the caller
60
+ * passed via `--session-id` / `--resume <id>`), so the ledger joins conversation_id ↔ the
61
+ * `x-claude-code-session-id` the gateway logs. `null` when Claude Code picks it (`--continue`,
62
+ * a bare `--resume`, `--fork-session`) or the shape hosts many sessions (`--server`). Optional:
63
+ * records written before this field existed omit it (still `v: 1`).
64
+ */
65
+ cc_session_id?: string | null;
58
66
  }
59
67
  /** Goal-alignment mode as recorded at launch. */
60
68
  export type GoalMode = "off" | "on" | "implicit";
@@ -76,6 +76,16 @@ export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv): string;
76
76
  export declare function resolveAnthropicDoor(env?: NodeJS.ProcessEnv): string;
77
77
  /** True when the resolved gateway host is the us-2/prod cell (used for the launch banner). */
78
78
  export declare function isProdGateway(env?: NodeJS.ProcessEnv): boolean;
79
+ /**
80
+ * The gateway origin the `handoff` MCP tool's own `POST /mnemom/context/handoff` must hit —
81
+ * always the SAME cell the launched session's own `/v1/messages` traffic goes to. Derived from
82
+ * the already-resolved Anthropic `door` (scheme+host only, via `new URL(...).origin`) rather
83
+ * than `resolveGatewayHost` directly, because `MNEMOM_AGENT_ANTHROPIC_GATEWAY` can override the
84
+ * door to a DIFFERENT host than `MNEMOM_AGENT_GATEWAY`/the default (e.g. a test cell) — reading
85
+ * `resolveGatewayHost` here would silently send handoff calls to prod (or vice versa) while the
86
+ * session's model traffic went elsewhere (MNE-7564 review).
87
+ */
88
+ export declare function resolveHandoffGatewayHost(door: string): string;
79
89
  /**
80
90
  * Sanitise a header value into a safe wire token: lowercase, alphanumerics and
81
91
  * single hyphens only, no leading/trailing hyphen. Returns "" when nothing
@@ -135,6 +145,15 @@ export declare const GOAL_MODE_HEADER = "x-mnemom-goal-mode";
135
145
  export declare const SESSION_CONFIG_HEADER = "x-mnemom-session-config";
136
146
  /** The gateway env key that gates context folding (allow-listed for per-session overlay). */
137
147
  export declare const CONTEXT_RUNTIME_KEY = "CONTEXT_RUNTIME_ENABLED";
148
+ /**
149
+ * The gateway env key that gates the whole context-handoff capability (level lines +
150
+ * `/mnemom/context/handoff` route + the digest note section) for a single session
151
+ * (MNE-7561..7565). Off by default on the gateway; turned on ONLY when this launch's
152
+ * `contextHandoff` setting is true (see `resolveContextHandoff`), which also registers the
153
+ * `handoff` MCP tool (MNE-7564) — a session never gets the tool without the header, or the
154
+ * header without the tool.
155
+ */
156
+ export declare const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
138
157
  /**
139
158
  * The `x-mnemom-goal-mode` header value for `--implicit`, or null when the flag
140
159
  * is absent: `implicit[; max_turns=N][; budget_usd=X][; stall_turns=N]` — the
@@ -165,6 +184,16 @@ export declare function launchPreferenceExplicit(opts: {
165
184
  remoteControl?: boolean;
166
185
  server?: boolean;
167
186
  }, env?: NodeJS.ProcessEnv): boolean;
187
+ /**
188
+ * Has an orchestrator already run the session + µ-balance preflight for this
189
+ * launch? `MNEMOM_AGENT_PREFLIGHTED=1` is for orchestrators (the ADW fleet) that
190
+ * checked sign-in and balance themselves before spawning many launches; it skips
191
+ * the per-launch session/personal-org/balance round trips. The Mnemom Agent
192
+ * entitlement (which also gates Remote Control, fail-closed) is still fetched.
193
+ * Only the exact value `1` counts — anything else, including `true` or ` 1`, is
194
+ * a normal launch.
195
+ */
196
+ export declare function launchPreflighted(env?: NodeJS.ProcessEnv): boolean;
168
197
  /**
169
198
  * Is this account allowed to use Remote Control (a session steerable from
170
199
  * claude.ai and the Claude app)? Remote Control is an INTERNAL Mnemom capability
@@ -228,11 +257,95 @@ export declare function resolveContextFold(opts: {
228
257
  env?: NodeJS.ProcessEnv;
229
258
  }): boolean;
230
259
  /**
231
- * The `x-mnemom-session-config` value that turns context folding OFF, or
232
- * `undefined` when folding is ON (the default — nothing is sent, the gateway's
233
- * own default stands). base64url so it survives the gateway's inbound strip.
260
+ * The `x-mnemom-session-config` value for this launch, or `undefined` when there is
261
+ * nothing to override (the gateway's own defaults stand — the common case). Two
262
+ * independent overlays, both OFF by default (folding is ON by default so only its OFF
263
+ * state is ever sent; handoff is OFF by default so only its ON state is ever sent):
264
+ * - `fold: false` → `CONTEXT_RUNTIME_ENABLED: "false"` (`--no-context`).
265
+ * - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
266
+ * setting) — the SAME header the `handoff` MCP tool's own call to
267
+ * `/mnemom/context/handoff` must also send, since the gateway resolves the flag
268
+ * per request (see `commands/agent.ts` handoff MCP wiring).
269
+ * base64url so it survives the gateway's inbound strip.
234
270
  */
235
- export declare function buildSessionConfigB64(fold: boolean): string | undefined;
271
+ export declare function buildSessionConfigB64(opts: {
272
+ fold: boolean;
273
+ contextHandoff: boolean;
274
+ }): string | undefined;
275
+ /**
276
+ * Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
277
+ * this launch? OFF by default (operator directive, curation 2026-09-25): an absent flag
278
+ * and an absent `context_handoff` setting are both "off", matching `resolveContextFold`'s
279
+ * shape but the opposite default. Precedence: an explicit `--context-handoff`/
280
+ * `--no-context-handoff` flag wins, else `MNEMOM_AGENT_CONTEXT_HANDOFF` (which
281
+ * `applyConfigToEnv` sets from the `context_handoff` config key), else OFF.
282
+ */
283
+ export declare function resolveContextHandoff(opts: {
284
+ contextHandoff?: boolean;
285
+ env?: NodeJS.ProcessEnv;
286
+ }): boolean;
287
+ /** The name the `handoff` MCP tool server is registered under in `--mcp-config`. */
288
+ export declare const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
289
+ /**
290
+ * The name of the non-`ANTHROPIC_*` env var `spawnRcCli` (commands/agent.ts) adds to Claude
291
+ * Code's Remote Control child env, carrying the real Anthropic key, ONLY when context handoff is
292
+ * on. `spawnRcCli` strips every `ANTHROPIC_*` auth var (including `ANTHROPIC_CUSTOM_HEADERS`) so
293
+ * the RC gate sees a clean first-party-OAuth setup, but that gate only inspects the recognized
294
+ * `ANTHROPIC_*` base-url/key/socket family — a custom-named var rides through ungated, exactly
295
+ * like `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` already do for the proxy. The handoff server inherits
296
+ * it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
297
+ */
298
+ export declare const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
299
+ /**
300
+ * How the `handoff` MCP server (cli/src/mcp/handoff-server.mjs) recovers the Anthropic key
301
+ * WITHOUT it ever being written into the `--mcp-config` file on disk: `envName` is the NAME of a
302
+ * var already present in Claude Code's OWN process env, which every stdio MCP server inherits in
303
+ * full (verified experimentally for MNE-7564: a probe server registered via `--mcp-config`
304
+ * received vars set only in the parent `claude` process's env, unfiltered).
305
+ * - Terminal shape: `{ envName: "ANTHROPIC_CUSTOM_HEADERS" }` — that var carries the real key
306
+ * as its `x-api-key:` line (`buildCustomHeaders` above), so the server parses that line out
307
+ * of its own inherited env at request time.
308
+ * - Remote Control (both `--remote-control` and `--server`): `{ envName:
309
+ * HANDOFF_RC_KEY_ENV_NAME }` — `spawnRcCli` sets that var directly (see above) since RC's
310
+ * child env carries the key nowhere else (it rides the local proxy's stdin instead, deleted
311
+ * from Claude Code's own env by the strip list).
312
+ * There is deliberately no embedded-value ("rawKey") variant: every launch shape has an
313
+ * inherited-env channel, so the config file never needs to carry the key itself.
314
+ */
315
+ export interface HandoffAnthropicKeySource {
316
+ envName: string;
317
+ }
318
+ /**
319
+ * The env vars the launcher hands the `handoff` MCP server so it can call
320
+ * `POST /mnemom/context/handoff` with the SAME identity + session config the launched
321
+ * coding-agent session's own `/v1/messages` requests carry. No secret rides in this object or
322
+ * the config file it produces, but the file is still written to a 0600 path and passed to
323
+ * `--mcp-config` as a PATH, never inlined into argv (see `commands/agent.ts`
324
+ * `writeHandoffMcpConfigFile`), since even an env-var NAME is worth protecting from a stray read.
325
+ */
326
+ export interface HandoffMcpParams {
327
+ /** The gateway ORIGIN (e.g. https://gateway.mnemom.ai) — see `resolveHandoffGatewayHost`. */
328
+ gatewayHost: string;
329
+ anthropicKeySource: HandoffAnthropicKeySource;
330
+ agent: string;
331
+ conversationId: string;
332
+ /** `buildSessionConfigB64` output — forwarded so the route resolves the SAME per-session flag. */
333
+ sessionConfigB64?: string;
334
+ /** Absolute path to the compiled server entry point (`resolveHandoffServerScript`). */
335
+ serverScript: string;
336
+ /** The node executable to run it with (the launcher's own `process.execPath`). */
337
+ nodeBin: string;
338
+ }
339
+ /** The `--mcp-config` document registering exactly the `handoff` tool. Pure — no I/O. */
340
+ export declare function buildHandoffMcpConfig(p: HandoffMcpParams): {
341
+ mcpServers: Record<string, {
342
+ command: string;
343
+ args: string[];
344
+ env: Record<string, string>;
345
+ }>;
346
+ };
347
+ /** `JSON.stringify(buildHandoffMcpConfig(p))` — the exact bytes written to the config file. */
348
+ export declare function buildHandoffMcpConfigJson(p: HandoffMcpParams): string;
236
349
  /**
237
350
  * The persistable goal-alignment default (`launcher.toml` `goal_mode`). Only the
238
351
  * two statement-free modes can be a standing default: a SEALED contract needs a
@@ -244,8 +357,12 @@ export type GoalModeDefault = "implicit" | "off";
244
357
  * 1. an explicit flag — `--implicit` (true) or `--no-implicit` (false);
245
358
  * 2. a `--goal` statement — a sealed contract is a per-launch choice that the
246
359
  * standing config default must never silently override;
247
- * 3. the config default `goal_mode = "implicit"`;
248
- * 4. off.
360
+ * 3. the config default `goal_mode` — `"off"` turns alignment off, `"implicit"`
361
+ * keeps it implicit;
362
+ * 4. IMPLICIT (the default). A bare `mnemom agent` runs with implicit goal
363
+ * alignment on: the gateway infers the goal from the user's own messages.
364
+ * Turn it off for a launch with `--no-implicit`, or standing with
365
+ * `goal_mode = "off"`.
249
366
  * Pure so every arm is unit-tested; the caller passes the resolved value on to
250
367
  * `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
251
368
  * combined with a contract flag — that conflict is EXPLICIT and is reported).
package/dist/lib/agent.js CHANGED
@@ -50,6 +50,18 @@ export function resolveAnthropicDoor(env = process.env) {
50
50
  export function isProdGateway(env = process.env) {
51
51
  return resolveGatewayHost(env) === PROD_GATEWAY_HOST;
52
52
  }
53
+ /**
54
+ * The gateway origin the `handoff` MCP tool's own `POST /mnemom/context/handoff` must hit —
55
+ * always the SAME cell the launched session's own `/v1/messages` traffic goes to. Derived from
56
+ * the already-resolved Anthropic `door` (scheme+host only, via `new URL(...).origin`) rather
57
+ * than `resolveGatewayHost` directly, because `MNEMOM_AGENT_ANTHROPIC_GATEWAY` can override the
58
+ * door to a DIFFERENT host than `MNEMOM_AGENT_GATEWAY`/the default (e.g. a test cell) — reading
59
+ * `resolveGatewayHost` here would silently send handoff calls to prod (or vice versa) while the
60
+ * session's model traffic went elsewhere (MNE-7564 review).
61
+ */
62
+ export function resolveHandoffGatewayHost(door) {
63
+ return new URL(door).origin;
64
+ }
53
65
  /**
54
66
  * Sanitise a header value into a safe wire token: lowercase, alphanumerics and
55
67
  * single hyphens only, no leading/trailing hyphen. Returns "" when nothing
@@ -195,6 +207,15 @@ export const GOAL_MODE_HEADER = "x-mnemom-goal-mode";
195
207
  export const SESSION_CONFIG_HEADER = "x-mnemom-session-config";
196
208
  /** The gateway env key that gates context folding (allow-listed for per-session overlay). */
197
209
  export const CONTEXT_RUNTIME_KEY = "CONTEXT_RUNTIME_ENABLED";
210
+ /**
211
+ * The gateway env key that gates the whole context-handoff capability (level lines +
212
+ * `/mnemom/context/handoff` route + the digest note section) for a single session
213
+ * (MNE-7561..7565). Off by default on the gateway; turned on ONLY when this launch's
214
+ * `contextHandoff` setting is true (see `resolveContextHandoff`), which also registers the
215
+ * `handoff` MCP tool (MNE-7564) — a session never gets the tool without the header, or the
216
+ * header without the tool.
217
+ */
218
+ export const CONTEXT_HANDOFF_KEY = "CONTEXT_HANDOFF_ENABLED";
198
219
  const SEALED_ONLY_FLAGS = "--goal/--requirement/--allow/--forbid/--goal-id";
199
220
  /**
200
221
  * `--implicit` and a sealed contract are mutually exclusive: on the gateway a
@@ -300,6 +321,18 @@ export function launchPreferenceExplicit(opts, env = process.env) {
300
321
  return true;
301
322
  return (env.MNEMOM_AGENT_REMOTE_CONTROL ?? "").trim() !== "";
302
323
  }
324
+ /**
325
+ * Has an orchestrator already run the session + µ-balance preflight for this
326
+ * launch? `MNEMOM_AGENT_PREFLIGHTED=1` is for orchestrators (the ADW fleet) that
327
+ * checked sign-in and balance themselves before spawning many launches; it skips
328
+ * the per-launch session/personal-org/balance round trips. The Mnemom Agent
329
+ * entitlement (which also gates Remote Control, fail-closed) is still fetched.
330
+ * Only the exact value `1` counts — anything else, including `true` or ` 1`, is
331
+ * a normal launch.
332
+ */
333
+ export function launchPreflighted(env = process.env) {
334
+ return env.MNEMOM_AGENT_PREFLIGHTED === "1";
335
+ }
303
336
  /**
304
337
  * Is this account allowed to use Remote Control (a session steerable from
305
338
  * claude.ai and the Claude app)? Remote Control is an INTERNAL Mnemom capability
@@ -381,23 +414,87 @@ export function resolveContextFold(opts) {
381
414
  return !(raw === "0" || raw === "false" || raw === "off" || raw === "no");
382
415
  }
383
416
  /**
384
- * The `x-mnemom-session-config` value that turns context folding OFF, or
385
- * `undefined` when folding is ON (the default — nothing is sent, the gateway's
386
- * own default stands). base64url so it survives the gateway's inbound strip.
417
+ * The `x-mnemom-session-config` value for this launch, or `undefined` when there is
418
+ * nothing to override (the gateway's own defaults stand — the common case). Two
419
+ * independent overlays, both OFF by default (folding is ON by default so only its OFF
420
+ * state is ever sent; handoff is OFF by default so only its ON state is ever sent):
421
+ * - `fold: false` → `CONTEXT_RUNTIME_ENABLED: "false"` (`--no-context`).
422
+ * - `contextHandoff: true` → `CONTEXT_HANDOFF_ENABLED: "true"` (the `context_handoff`
423
+ * setting) — the SAME header the `handoff` MCP tool's own call to
424
+ * `/mnemom/context/handoff` must also send, since the gateway resolves the flag
425
+ * per request (see `commands/agent.ts` handoff MCP wiring).
426
+ * base64url so it survives the gateway's inbound strip.
387
427
  */
388
- export function buildSessionConfigB64(fold) {
389
- if (fold)
428
+ export function buildSessionConfigB64(opts) {
429
+ const config = {};
430
+ if (!opts.fold)
431
+ config[CONTEXT_RUNTIME_KEY] = "false";
432
+ if (opts.contextHandoff)
433
+ config[CONTEXT_HANDOFF_KEY] = "true";
434
+ if (Object.keys(config).length === 0)
390
435
  return undefined;
391
- const payload = JSON.stringify({ v: 1, config: { [CONTEXT_RUNTIME_KEY]: "false" } });
436
+ const payload = JSON.stringify({ v: 1, config });
392
437
  return Buffer.from(payload, "utf8").toString("base64url");
393
438
  }
439
+ /**
440
+ * Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
441
+ * this launch? OFF by default (operator directive, curation 2026-09-25): an absent flag
442
+ * and an absent `context_handoff` setting are both "off", matching `resolveContextFold`'s
443
+ * shape but the opposite default. Precedence: an explicit `--context-handoff`/
444
+ * `--no-context-handoff` flag wins, else `MNEMOM_AGENT_CONTEXT_HANDOFF` (which
445
+ * `applyConfigToEnv` sets from the `context_handoff` config key), else OFF.
446
+ */
447
+ export function resolveContextHandoff(opts) {
448
+ if (opts.contextHandoff !== undefined)
449
+ return opts.contextHandoff;
450
+ const raw = (opts.env?.MNEMOM_AGENT_CONTEXT_HANDOFF ?? "").trim().toLowerCase();
451
+ if (raw === "")
452
+ return false;
453
+ return raw === "1" || raw === "true" || raw === "on" || raw === "yes";
454
+ }
455
+ /** The name the `handoff` MCP tool server is registered under in `--mcp-config`. */
456
+ export const HANDOFF_MCP_SERVER_NAME = "mnemom-handoff";
457
+ /**
458
+ * The name of the non-`ANTHROPIC_*` env var `spawnRcCli` (commands/agent.ts) adds to Claude
459
+ * Code's Remote Control child env, carrying the real Anthropic key, ONLY when context handoff is
460
+ * on. `spawnRcCli` strips every `ANTHROPIC_*` auth var (including `ANTHROPIC_CUSTOM_HEADERS`) so
461
+ * the RC gate sees a clean first-party-OAuth setup, but that gate only inspects the recognized
462
+ * `ANTHROPIC_*` base-url/key/socket family — a custom-named var rides through ungated, exactly
463
+ * like `HTTPS_PROXY`/`NODE_EXTRA_CA_CERTS` already do for the proxy. The handoff server inherits
464
+ * it the same way it inherits `ANTHROPIC_CUSTOM_HEADERS` under the terminal shape.
465
+ */
466
+ export const HANDOFF_RC_KEY_ENV_NAME = "MNEMOM_HANDOFF_KEY";
467
+ /** The `--mcp-config` document registering exactly the `handoff` tool. Pure — no I/O. */
468
+ export function buildHandoffMcpConfig(p) {
469
+ const env = {
470
+ MNEMOM_HANDOFF_GATEWAY_HOST: p.gatewayHost,
471
+ MNEMOM_HANDOFF_ANTHROPIC_KEY_ENV_NAME: p.anthropicKeySource.envName,
472
+ MNEMOM_HANDOFF_AGENT: p.agent,
473
+ MNEMOM_HANDOFF_CONVERSATION_ID: p.conversationId,
474
+ };
475
+ if (p.sessionConfigB64)
476
+ env.MNEMOM_HANDOFF_SESSION_CONFIG = p.sessionConfigB64;
477
+ return {
478
+ mcpServers: {
479
+ [HANDOFF_MCP_SERVER_NAME]: { command: p.nodeBin, args: [p.serverScript], env },
480
+ },
481
+ };
482
+ }
483
+ /** `JSON.stringify(buildHandoffMcpConfig(p))` — the exact bytes written to the config file. */
484
+ export function buildHandoffMcpConfigJson(p) {
485
+ return JSON.stringify(buildHandoffMcpConfig(p));
486
+ }
394
487
  /**
395
488
  * Is this launch implicit-mode? Precedence, highest wins:
396
489
  * 1. an explicit flag — `--implicit` (true) or `--no-implicit` (false);
397
490
  * 2. a `--goal` statement — a sealed contract is a per-launch choice that the
398
491
  * standing config default must never silently override;
399
- * 3. the config default `goal_mode = "implicit"`;
400
- * 4. off.
492
+ * 3. the config default `goal_mode` — `"off"` turns alignment off, `"implicit"`
493
+ * keeps it implicit;
494
+ * 4. IMPLICIT (the default). A bare `mnemom agent` runs with implicit goal
495
+ * alignment on: the gateway infers the goal from the user's own messages.
496
+ * Turn it off for a launch with `--no-implicit`, or standing with
497
+ * `goal_mode = "off"`.
401
498
  * Pure so every arm is unit-tested; the caller passes the resolved value on to
402
499
  * `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
403
500
  * combined with a contract flag — that conflict is EXPLICIT and is reported).
@@ -407,5 +504,5 @@ export function resolveImplicit(opts) {
407
504
  return opts.implicit;
408
505
  if (hasText(opts.goal))
409
506
  return false;
410
- return opts.configGoalMode === "implicit";
507
+ return opts.configGoalMode !== "off";
411
508
  }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The client for the gateway's implicit-goal control route (implicit-goal CLI, cli-telemetry
3
+ * §1.2 / §1.4; the route is gateway/src/goal-intent-control.ts):
4
+ *
5
+ * GET https://<gateway_host>/mnemom/goal/intent?conversation_id=…&thread=…
6
+ * POST https://<gateway_host>/mnemom/goal/intent/pin {conversation_id, thread, pinned}
7
+ * POST https://<gateway_host>/mnemom/goal/intent/reset {conversation_id, thread, statement?, requirements?}
8
+ *
9
+ * Auth is the SAME credential the launch used: `x-api-key` (the Anthropic key from
10
+ * `resolveAnthropicKey`), `x-mnemom-agent: <record.agent_slug>`, and `x-mnemom-api-key` when the
11
+ * launch had a Mnemom key. The gateway hashes them exactly as a proxied request and only LOOKS
12
+ * UP the agent. Key material is never printed: errors carry the status, the gateway's error code
13
+ * and its message, nothing else.
14
+ *
15
+ * Pure apart from the injected `fetch`, so every arm is unit-tested.
16
+ */
17
+ import type { SessionRecord } from "./agent-ledger.js";
18
+ export type GoalOp = "show" | "pin" | "reset";
19
+ /**
20
+ * A thread id the gateway accepts (PLAN K3 plus `all` for reads, plus the `m.<sha16>` per-process
21
+ * main ids the gateway's goal-thread.ts THREAD_ID_RE also accepts). Validated locally so a typo
22
+ * never costs a round trip.
23
+ */
24
+ export declare const GOAL_THREAD_RE: RegExp;
25
+ export declare function isValidGoalThread(thread: string): boolean;
26
+ /** The ledger fields the control call needs. */
27
+ export type GoalControlRecord = Pick<SessionRecord, "gateway_host" | "agent_slug" | "conversation_id">;
28
+ export interface GoalControlCreds {
29
+ /** The Anthropic key the launch sent as `x-api-key`. */
30
+ anthropicKey: string;
31
+ /** The Mnemom key, only when the launch sent `x-mnemom-api-key`. */
32
+ mnemomKey?: string | null;
33
+ }
34
+ export type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
35
+ export interface GoalControlDeps {
36
+ fetch: FetchLike;
37
+ creds: GoalControlCreds;
38
+ }
39
+ export interface GoalShowBodyInput {
40
+ thread?: string;
41
+ }
42
+ export interface GoalPinBodyInput {
43
+ thread?: string;
44
+ pinned: boolean;
45
+ }
46
+ export interface GoalResetBodyInput {
47
+ thread?: string;
48
+ statement?: string;
49
+ requirements?: string[];
50
+ }
51
+ export interface GoalPinned {
52
+ at: string;
53
+ revision: number;
54
+ by: "cli";
55
+ }
56
+ export interface GoalHistoryEntry {
57
+ revision: number;
58
+ hash: string;
59
+ class: string;
60
+ ops: number;
61
+ at: string;
62
+ source: string | null;
63
+ }
64
+ export interface GoalIntentView {
65
+ intent_id: string;
66
+ revision: number;
67
+ contract_hash: string;
68
+ statement: string;
69
+ requirements: string[];
70
+ allowed_paths: string[];
71
+ forbidden_paths: string[];
72
+ pinned: GoalPinned | null;
73
+ pending_count: number;
74
+ history: GoalHistoryEntry[];
75
+ revisions_10m: number;
76
+ gates_10m: number;
77
+ updated_at: string;
78
+ }
79
+ export interface GoalVerdictView {
80
+ verdict: string;
81
+ turn: number;
82
+ at: string;
83
+ superseded: boolean;
84
+ }
85
+ export interface GoalThreadRow {
86
+ thread: string;
87
+ role: string;
88
+ revision: number;
89
+ statement: string | null;
90
+ pinned: boolean;
91
+ }
92
+ /** `GET /mnemom/goal/intent` (cli-telemetry §1.2 plus the `thread` echo and `last_revision`). */
93
+ export interface GoalShowResponse {
94
+ conversation_id: string;
95
+ thread: string;
96
+ mode: "implicit" | "sealed" | "none";
97
+ sealed_hash?: string;
98
+ intent: GoalIntentView | null;
99
+ verdict: GoalVerdictView | null;
100
+ last_revision: number;
101
+ threads?: GoalThreadRow[];
102
+ }
103
+ export interface GoalPinResponse {
104
+ conversation_id: string;
105
+ thread: string;
106
+ pinned: GoalPinned | null;
107
+ revision: number;
108
+ }
109
+ export interface GoalResetResponse {
110
+ conversation_id: string;
111
+ thread: string;
112
+ reset: true;
113
+ revision: number;
114
+ prior_revision: number;
115
+ intent: GoalIntentView | null;
116
+ verdict_superseded: boolean;
117
+ }
118
+ /** A non-2xx answer (status > 0) or a transport failure (status 0). Never carries key material. */
119
+ export declare class GoalControlError extends Error {
120
+ readonly status: number;
121
+ readonly code: string | null;
122
+ constructor(status: number, code: string | null, message: string);
123
+ }
124
+ /** `https://<host>` for a bare host; a host that already names a scheme is kept (trailing / dropped). */
125
+ export declare function gatewayOrigin(host: string): string;
126
+ export declare const GOAL_CONTROL_PATH = "/mnemom/goal/intent";
127
+ /** The route URL for one op (the show query carries the conversation id and thread). */
128
+ export declare function goalControlUrl(record: GoalControlRecord, op: GoalOp, thread?: string): string;
129
+ /** The launch's identity headers (never logged). */
130
+ export declare function goalControlHeaders(record: GoalControlRecord, creds: GoalControlCreds): Record<string, string>;
131
+ export declare function goalControlFetch(record: GoalControlRecord, op: "show", body: GoalShowBodyInput, deps: GoalControlDeps): Promise<GoalShowResponse>;
132
+ export declare function goalControlFetch(record: GoalControlRecord, op: "pin", body: GoalPinBodyInput, deps: GoalControlDeps): Promise<GoalPinResponse>;
133
+ export declare function goalControlFetch(record: GoalControlRecord, op: "reset", body: GoalResetBodyInput, deps: GoalControlDeps): Promise<GoalResetResponse>;