@mnemom/mnemom 0.17.3-next.5 → 0.17.3
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/commands/agent-config.d.ts +4 -1
- package/dist/commands/agent-config.js +8 -5
- package/dist/commands/agent-doctor.js +10 -1
- package/dist/commands/agent-session.d.ts +7 -6
- package/dist/commands/agent-session.js +10 -11
- package/dist/commands/agent-sessions.d.ts +5 -0
- package/dist/commands/agent-sessions.js +10 -1
- package/dist/commands/agent-setup.js +9 -2
- package/dist/commands/agent.d.ts +9 -0
- package/dist/commands/agent.js +24 -6
- package/dist/commands/experimental.d.ts +18 -0
- package/dist/commands/experimental.js +78 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +85 -19
- package/dist/lib/agent-ledger.d.ts +5 -0
- package/dist/lib/agent-ledger.js +5 -0
- package/dist/lib/agent.d.ts +46 -9
- package/dist/lib/agent.js +65 -13
- package/dist/lib/cli-config.d.ts +10 -0
- package/dist/lib/cli-config.js +11 -0
- package/dist/lib/experimental.d.ts +119 -0
- package/dist/lib/experimental.js +226 -0
- package/dist/lib/launcher-config.d.ts +9 -0
- package/dist/lib/launcher-config.js +26 -1
- package/dist/lib/slack-router.js +3 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28,7 +28,10 @@ import { agentSlackCommand } from "./commands/agent-slack.js";
|
|
|
28
28
|
import { agentDoctorCommand } from "./commands/agent-doctor.js";
|
|
29
29
|
import { agentSetupCommand } from "./commands/agent-setup.js";
|
|
30
30
|
import { agentInviteCommand, runAgentInvocation } from "./commands/agent-invite.js";
|
|
31
|
-
import { applyConfigToEnv, loadLauncherConfig } from "./lib/launcher-config.js";
|
|
31
|
+
import { applyConfigToEnv, launcherEntries, loadLauncherConfig, withoutDisabledExperimental, } from "./lib/launcher-config.js";
|
|
32
|
+
import { loadExperimentalState } from "./lib/cli-config.js";
|
|
33
|
+
import { assertEnabled, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
|
|
34
|
+
import { experimentalDisableCommand, experimentalEnableCommand, experimentalListCommand, runExperimentalMigration, } from "./commands/experimental.js";
|
|
32
35
|
import { parseContextProfile, resolveImplicit } from "./lib/agent.js";
|
|
33
36
|
import { wrapCommand } from "./commands/wrap.js";
|
|
34
37
|
import { onboardCommand } from "./commands/onboard.js";
|
|
@@ -97,12 +100,25 @@ program
|
|
|
97
100
|
// CONTRACT the gateway seals on the first turn. Three launch shapes — terminal
|
|
98
101
|
// (default), --remote-control (interactive RC in this terminal), --server
|
|
99
102
|
// (headless RC dispatcher) — with yolo on by default. `--cli` selects the
|
|
100
|
-
// executable (name or full path; default claude). The
|
|
101
|
-
//
|
|
103
|
+
// executable (name or full path; default claude). The door is the gateway of the
|
|
104
|
+
// cell the CLI uses (prod by default); a door on another cell is refused. The
|
|
102
105
|
// Anthropic key is resolved with zero 1Password dependency (env → ~/.mnemom store
|
|
103
106
|
// → interactive prompt) inside the launched process and never logged. See
|
|
104
107
|
// commands/code.ts.
|
|
105
108
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
+
// ── experimental features (lib/experimental.ts) ── While a feature is off its flags and
|
|
110
|
+
// commands are hidden from --help (decided once, at load) and using one fails with a
|
|
111
|
+
// pointer to `mnemom experimental enable <id>` (decided per invocation, in the action).
|
|
112
|
+
const experimentalAtLoad = loadExperimentalState().enabled;
|
|
113
|
+
const hiddenUnless = (id) => !experimentalAtLoad.includes(id);
|
|
114
|
+
/** preAction guard for a gated command: print the guidance and exit 2 while `id` is off. */
|
|
115
|
+
function requireExperimental(what, id) {
|
|
116
|
+
const message = gateCheck(what, id, loadExperimentalState().enabled);
|
|
117
|
+
if (message) {
|
|
118
|
+
console.error(message);
|
|
119
|
+
process.exit(2);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
106
122
|
const collectRepeatable = (value, previous) => {
|
|
107
123
|
previous.push(value);
|
|
108
124
|
return previous;
|
|
@@ -119,11 +135,11 @@ const agentCmd = program
|
|
|
119
135
|
.option("--budget <usd>", "Guardrail: spend ceiling in USD (positive number)")
|
|
120
136
|
.option("--max-turns <n>", "Guardrail: turn ceiling (positive integer)")
|
|
121
137
|
.option("--stall <n>", "Guardrail: consecutive no-write turns (positive integer)")
|
|
122
|
-
.
|
|
123
|
-
.option("--no-implicit", "Turn goal alignment OFF for this launch (
|
|
138
|
+
.addOption(new Option("--implicit", "EXPERIMENTAL (`mnemom experimental enable implicit`): implicit goal alignment — the gateway infers the goal from your messages and revises it as you steer (the default while the feature is on; not combinable with --goal/--requirement/--allow/--forbid/--goal-id; the guardrail ceilings still apply)").hideHelp(hiddenUnless("implicit")))
|
|
139
|
+
.option("--no-implicit", "Turn implicit goal alignment OFF for this launch (overrides launcher.toml goal_mode = implicit)")
|
|
124
140
|
.option("--conversation-id <id>", "x-mnemom-conversation-id (default: auto-generated)")
|
|
125
141
|
.option("--cli <name|path>", "Coding-agent CLI to launch: a name or full path (default: claude)")
|
|
126
|
-
.option("--gateway <url>", "Gateway host override (default:
|
|
142
|
+
.option("--gateway <url>", "Gateway host override — must be the same cell as the rest of the CLI (default: that cell's gateway; or MNEMOM_AGENT_GATEWAY)")
|
|
127
143
|
.option("--remote-control", "Interactive Remote Control session in THIS terminal (behind a local proxy)")
|
|
128
144
|
.option("--server", "Headless `claude remote-control` dispatcher (behind a local proxy)")
|
|
129
145
|
.option("--model <alias|id>", "Model to launch (CLI alias or concrete id)")
|
|
@@ -140,7 +156,7 @@ const agentCmd = program
|
|
|
140
156
|
.option("--no-setup", "Skip governed-agent provisioning entirely for this launch")
|
|
141
157
|
.option("--no-yolo", "Do NOT pass --dangerously-skip-permissions (yolo is on by default)")
|
|
142
158
|
.option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
|
|
143
|
-
.
|
|
159
|
+
.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")))
|
|
144
160
|
.option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
|
|
145
161
|
.option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
|
|
146
162
|
.addOption(new Option("--context-profile <profile>", 'How the gateway folds this session\'s context. "balanced" (default) keeps more of the earlier work verbatim and folds later — for long sessions that revisit earlier files and decisions. "aggressive" folds earlier and keeps a tighter working set — for cost-sensitive sessions and short, focused tasks. "off" leaves compaction to the coding agent. Also: config key context_profile, env MNEMOM_AGENT_CONTEXT_PROFILE.').argParser((v) => {
|
|
@@ -153,7 +169,7 @@ const agentCmd = program
|
|
|
153
169
|
throw new InvalidArgumentError(e.message);
|
|
154
170
|
}
|
|
155
171
|
}))
|
|
156
|
-
.
|
|
172
|
+
.addOption(new Option("--context-handoff", "EXPERIMENTAL (`mnemom experimental enable context-handoff`): register the `handoff(note)` MCP tool and tell the gateway to turn on the context-handoff capability for this session").hideHelp(hiddenUnless("context-handoff")))
|
|
157
173
|
.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")
|
|
158
174
|
.option("--dry-run", "Print the resolved launch plan and exit — no key read, no spawn")
|
|
159
175
|
.action(async (scenario, passthrough, opts) => {
|
|
@@ -168,29 +184,53 @@ const agentCmd = program
|
|
|
168
184
|
});
|
|
169
185
|
}
|
|
170
186
|
catch (error) {
|
|
171
|
-
|
|
172
|
-
|
|
187
|
+
const report = reportCliError(error);
|
|
188
|
+
console.error(...report.args);
|
|
189
|
+
process.exit(report.code);
|
|
173
190
|
}
|
|
174
191
|
async function launchAgent(scenario) {
|
|
192
|
+
const experimental = loadExperimentalState().enabled;
|
|
175
193
|
// Sub-verb: `mnemom agent config …` manages persistent settings and never
|
|
176
194
|
// launches. (A scenario literally named "config" is reserved for this.)
|
|
177
195
|
if (scenario === "config") {
|
|
178
|
-
await agentConfigCommand(passthrough);
|
|
196
|
+
await agentConfigCommand(passthrough, { enabled: experimental });
|
|
179
197
|
return;
|
|
180
198
|
}
|
|
181
199
|
// Sub-verb: `mnemom agent slack …` posts/receives Slack messages as this
|
|
182
|
-
// session (lib/slack.ts) and never launches.
|
|
200
|
+
// session (lib/slack.ts) and never launches. Experimental (`slack`).
|
|
183
201
|
if (scenario === "slack") {
|
|
202
|
+
assertEnabled([{ what: "`mnemom agent slack`", id: "slack", used: true }], experimental);
|
|
184
203
|
await agentSlackCommand(passthrough);
|
|
185
204
|
return;
|
|
186
205
|
}
|
|
206
|
+
// Experimental flags while their feature is off: refuse (exit 2) with the pointer.
|
|
207
|
+
// The `--no-*` spellings stay available — turning an off feature off is harmless.
|
|
208
|
+
assertEnabled([
|
|
209
|
+
{ what: "--slack", id: "slack", used: opts.slack === true },
|
|
210
|
+
{
|
|
211
|
+
what: "--context-handoff",
|
|
212
|
+
id: "context-handoff",
|
|
213
|
+
used: opts.contextHandoff === true,
|
|
214
|
+
},
|
|
215
|
+
{ what: "--implicit", id: "implicit", used: opts.implicit === true },
|
|
216
|
+
], experimental);
|
|
187
217
|
if (opts.gateway)
|
|
188
218
|
process.env.MNEMOM_AGENT_GATEWAY = opts.gateway;
|
|
189
219
|
// Persistent settings (~/.mnemom/launcher.toml) sit UNDER env and flags:
|
|
190
220
|
// precedence is flag > env > launcher.toml > built-in. Env-backed settings are
|
|
191
221
|
// folded into env only when unset (so env/flags still win); the resolvers
|
|
192
222
|
// in lib/code.ts keep the built-in default as the final fallback.
|
|
193
|
-
|
|
223
|
+
// Settings of experimental features that are off are ignored (with the pointer),
|
|
224
|
+
// before they can reach env or the launch.
|
|
225
|
+
const rawCfg = loadLauncherConfig();
|
|
226
|
+
for (const line of ignoredSettingLines({
|
|
227
|
+
entries: launcherEntries(rawCfg),
|
|
228
|
+
env: process.env,
|
|
229
|
+
enabled: experimental,
|
|
230
|
+
})) {
|
|
231
|
+
console.error(line);
|
|
232
|
+
}
|
|
233
|
+
const cfg = withoutDisabledExperimental(rawCfg, experimental);
|
|
194
234
|
applyConfigToEnv(cfg, process.env);
|
|
195
235
|
// Sub-verb: `mnemom agent doctor` runs a read-only preflight and never
|
|
196
236
|
// launches. Placed after config load so it reports the effective settings.
|
|
@@ -207,11 +247,13 @@ const agentCmd = program
|
|
|
207
247
|
const numToStr = (n) => n === undefined ? undefined : String(n);
|
|
208
248
|
// Implicit mode: --implicit / --no-implicit win; a --goal statement seals
|
|
209
249
|
// (the standing config default never overrides it); else launcher.toml
|
|
210
|
-
// `goal_mode`; else
|
|
250
|
+
// `goal_mode`; else implicit while the `implicit` feature is on, off while it is
|
|
251
|
+
// off. Pure + unit-tested in lib/agent.ts.
|
|
211
252
|
const implicit = resolveImplicit({
|
|
212
253
|
goal: opts.goal,
|
|
213
254
|
implicit: opts.implicit,
|
|
214
255
|
configGoalMode: cfg.goalMode,
|
|
256
|
+
experimentalOn: experimental.includes("implicit"),
|
|
215
257
|
});
|
|
216
258
|
// Config guardrails apply only when goal alignment is actually intended (a
|
|
217
259
|
// --goal is present, or implicit mode — where they ride on the mode header),
|
|
@@ -245,7 +287,9 @@ const agentCmd = program
|
|
|
245
287
|
slack: opts.slack ?? cfg.slackListen,
|
|
246
288
|
context: opts.context,
|
|
247
289
|
contextProfile: opts.contextProfile,
|
|
248
|
-
|
|
290
|
+
// Off ⇒ an explicit false, so MNEMOM_AGENT_CONTEXT_HANDOFF cannot turn it on.
|
|
291
|
+
contextHandoff: experimental.includes("context-handoff") ? opts.contextHandoff : false,
|
|
292
|
+
experimental,
|
|
249
293
|
setup: opts.setup,
|
|
250
294
|
keySource: cfg.keySource,
|
|
251
295
|
passthrough,
|
|
@@ -763,9 +807,10 @@ agentCmd
|
|
|
763
807
|
// (implicit-goal CLI). The handlers set process.exitCode themselves (1 / 2); the catch is for
|
|
764
808
|
// the unexpected.
|
|
765
809
|
const agentGoalCmd = agentCmd
|
|
766
|
-
.command("goal")
|
|
767
|
-
.description("
|
|
768
|
-
"Authenticates with the same key and agent the session launched with")
|
|
810
|
+
.command("goal", { hidden: hiddenUnless("implicit") })
|
|
811
|
+
.description("EXPERIMENTAL (`mnemom experimental enable implicit`): read and steer the goal the gateway inferred for an implicit-mode session (show / pin / reset). " +
|
|
812
|
+
"Authenticates with the same key and agent the session launched with")
|
|
813
|
+
.hook("preAction", () => requireExperimental("`mnemom agent goal`", "implicit"));
|
|
769
814
|
const collectRepeat = (value, prev = []) => [...prev, value];
|
|
770
815
|
agentGoalCmd
|
|
771
816
|
.command("show [ref]")
|
|
@@ -1928,6 +1973,23 @@ configCmd
|
|
|
1928
1973
|
.command("unset <key>")
|
|
1929
1974
|
.description("Reset a preference to its default")
|
|
1930
1975
|
.action((key) => runAction(() => configUnsetCommand(key)));
|
|
1976
|
+
// ── experimental (lib/experimental.ts) — turn experimental features on and off ──
|
|
1977
|
+
const experimentalCmd = program
|
|
1978
|
+
.command("experimental")
|
|
1979
|
+
.description(`List, enable and disable experimental features (stored in ~/.mnemom/config.json; MNEMOM_EXPERIMENTAL=<ids>|all overrides). ${EXPERIMENTAL_CONTRACT}`);
|
|
1980
|
+
experimentalCmd
|
|
1981
|
+
.command("list", { isDefault: true })
|
|
1982
|
+
.description("Show every experimental feature, whether it is on, and what it gates")
|
|
1983
|
+
.option("--json", "Emit machine-readable JSON")
|
|
1984
|
+
.action((options) => runAction(() => experimentalListCommand(options)));
|
|
1985
|
+
experimentalCmd
|
|
1986
|
+
.command("enable <id>")
|
|
1987
|
+
.description("Turn an experimental feature on")
|
|
1988
|
+
.action((id) => runAction(() => experimentalEnableCommand(id)));
|
|
1989
|
+
experimentalCmd
|
|
1990
|
+
.command("disable <id>")
|
|
1991
|
+
.description("Turn an experimental feature off")
|
|
1992
|
+
.action((id) => runAction(() => experimentalDisableCommand(id)));
|
|
1931
1993
|
// Export the fully-assembled commander program so tooling (e.g. the
|
|
1932
1994
|
// command-tree snapshot generator in scripts/gen-command-tree.mjs) can
|
|
1933
1995
|
// statically introspect the command surface WITHOUT executing the CLI.
|
|
@@ -1941,8 +2003,12 @@ export { program };
|
|
|
1941
2003
|
* unknown — so it bypasses commander entirely and reaches the sub-verb parser
|
|
1942
2004
|
* verbatim. Exported (not auto-run) so the bypass is unit-testable.
|
|
1943
2005
|
*/
|
|
1944
|
-
export function runEntrypoint(argv = process.argv.slice(2)) {
|
|
2006
|
+
export function runEntrypoint(argv = process.argv.slice(2), migrate = runExperimentalMigration) {
|
|
2007
|
+
// First run after the upgrade that introduced experimental features: keep on whatever
|
|
2008
|
+
// launcher.toml already uses (commands/experimental.ts; never throws).
|
|
2009
|
+
migrate();
|
|
1945
2010
|
if (argv[0] === "agent" && argv[1] === "slack") {
|
|
2011
|
+
requireExperimental("`mnemom agent slack`", "slack");
|
|
1946
2012
|
agentSlackCommand(argv.slice(2)).catch((err) => {
|
|
1947
2013
|
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1948
2014
|
process.exit(1);
|
|
@@ -81,6 +81,11 @@ export interface SessionRecord {
|
|
|
81
81
|
* before profiles existed omit it (still `v: 1`).
|
|
82
82
|
*/
|
|
83
83
|
context_profile?: ContextProfile;
|
|
84
|
+
/**
|
|
85
|
+
* The experimental features on for the launch (`mnemom experimental`). Optional and only
|
|
86
|
+
* written when at least one was on; older records omit it (still `v: 1`).
|
|
87
|
+
*/
|
|
88
|
+
experimental?: string[];
|
|
84
89
|
}
|
|
85
90
|
/** Goal-alignment mode as recorded at launch. */
|
|
86
91
|
export type GoalMode = "off" | "on" | "implicit";
|
package/dist/lib/agent-ledger.js
CHANGED
|
@@ -160,6 +160,11 @@ export function parseRecord(line) {
|
|
|
160
160
|
const rec = value;
|
|
161
161
|
if (rec.v !== 1)
|
|
162
162
|
return null;
|
|
163
|
+
// A hand-edited or future `experimental` that is not a string list is dropped, not trusted.
|
|
164
|
+
if (rec.experimental !== undefined &&
|
|
165
|
+
!(Array.isArray(rec.experimental) && rec.experimental.every((x) => typeof x === "string"))) {
|
|
166
|
+
delete rec.experimental;
|
|
167
|
+
}
|
|
163
168
|
return rec;
|
|
164
169
|
}
|
|
165
170
|
function nonEmptyLines(text) {
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -63,11 +63,27 @@ export interface ContractFlags {
|
|
|
63
63
|
export type LaunchShape = "terminal" | "remote-control" | "server";
|
|
64
64
|
/**
|
|
65
65
|
* Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
|
|
66
|
-
* `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
* `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the gateway of the cell the rest
|
|
67
|
+
* of the CLI uses (`getGatewayUrl()`: prod unless MNEMOM_ENV / MNEMOM_RING /
|
|
68
|
+
* MNEMOM_GATEWAY_URL name another). An override on a different cell is refused at
|
|
69
|
+
* launch — see `singleCellError`. Trailing slash trimmed so door concatenation is clean.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveGatewayHost(env?: NodeJS.ProcessEnv, cellGateway?: string): string;
|
|
72
|
+
/** Same scheme+host+port. An unparseable URL never matches. */
|
|
73
|
+
export declare function sameOrigin(a: string, b: string): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
|
|
76
|
+
* gateway of the cell the CLI signs in, resolves the agent and reads sessions on
|
|
77
|
+
* (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
|
|
78
|
+
* and runs as another cell's, so `sessions`/`show` never find its goal state.
|
|
79
|
+
* Returns the refusal message, or null. A loopback door is a local dev gateway and
|
|
80
|
+
* is exempt.
|
|
81
|
+
*/
|
|
82
|
+
export declare function singleCellError(opts: {
|
|
83
|
+
door: string;
|
|
84
|
+
apiUrl: string;
|
|
85
|
+
cellGateway: string;
|
|
86
|
+
}): string | null;
|
|
71
87
|
/**
|
|
72
88
|
* The Anthropic door the CLI points Claude Code at:
|
|
73
89
|
* `${MNEMOM_AGENT_GATEWAY}/anthropic`, or the full-door override
|
|
@@ -324,6 +340,20 @@ export declare function buildSessionConfig(opts: {
|
|
|
324
340
|
profile: ContextProfile;
|
|
325
341
|
contextHandoff: boolean;
|
|
326
342
|
}): Record<string, string>;
|
|
343
|
+
/**
|
|
344
|
+
* The `x-mnemom-session-config` payload: `{ v: 1, config, experimental? }`. The active
|
|
345
|
+
* experimental ids ride as a top-level array, beside (not inside) the env overlay, so the
|
|
346
|
+
* gateway's allow-list never sees them as env keys; it ignores unknown envelope fields.
|
|
347
|
+
*/
|
|
348
|
+
export declare function buildSessionConfigPayload(opts: {
|
|
349
|
+
profile: ContextProfile;
|
|
350
|
+
contextHandoff: boolean;
|
|
351
|
+
experimental?: readonly string[];
|
|
352
|
+
}): {
|
|
353
|
+
v: 1;
|
|
354
|
+
config: Record<string, string>;
|
|
355
|
+
experimental?: string[];
|
|
356
|
+
} | undefined;
|
|
327
357
|
/**
|
|
328
358
|
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
329
359
|
* nothing to override (the gateway's own defaults stand — `balanced` with handoff off,
|
|
@@ -339,6 +369,7 @@ export declare function buildSessionConfig(opts: {
|
|
|
339
369
|
export declare function buildSessionConfigB64(opts: {
|
|
340
370
|
profile: ContextProfile;
|
|
341
371
|
contextHandoff: boolean;
|
|
372
|
+
experimental?: readonly string[];
|
|
342
373
|
}): string | undefined;
|
|
343
374
|
/**
|
|
344
375
|
* Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
|
|
@@ -427,10 +458,10 @@ export type GoalModeDefault = "implicit" | "off";
|
|
|
427
458
|
* standing config default must never silently override;
|
|
428
459
|
* 3. the config default `goal_mode` — `"off"` turns alignment off, `"implicit"`
|
|
429
460
|
* keeps it implicit;
|
|
430
|
-
* 4.
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
461
|
+
* 4. with the `implicit` experimental feature ON, IMPLICIT: the gateway infers
|
|
462
|
+
* the goal from the user's own messages (turn it off for a launch with
|
|
463
|
+
* `--no-implicit`, or standing with `goal_mode = "off"`); with it OFF (the
|
|
464
|
+
* default), OFF.
|
|
434
465
|
* Pure so every arm is unit-tested; the caller passes the resolved value on to
|
|
435
466
|
* `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
|
|
436
467
|
* combined with a contract flag — that conflict is EXPLICIT and is reported).
|
|
@@ -439,4 +470,10 @@ export declare function resolveImplicit(opts: {
|
|
|
439
470
|
goal?: string;
|
|
440
471
|
implicit?: boolean;
|
|
441
472
|
configGoalMode?: GoalModeDefault;
|
|
473
|
+
/**
|
|
474
|
+
* Is the `implicit` experimental feature on? Off ⇒ the default is OFF and
|
|
475
|
+
* `goal_mode = implicit` is ignored (`--implicit` is refused upstream, before
|
|
476
|
+
* this runs). Omitted ⇒ on.
|
|
477
|
+
*/
|
|
478
|
+
experimentalOn?: boolean;
|
|
442
479
|
}): boolean;
|
package/dist/lib/agent.js
CHANGED
|
@@ -20,19 +20,56 @@
|
|
|
20
20
|
* NAMED), but prod is never selected silently — see `resolveGatewayHost`.
|
|
21
21
|
*/
|
|
22
22
|
import { createHash, randomUUID } from "node:crypto";
|
|
23
|
+
import { getGatewayUrl } from "./config.js";
|
|
23
24
|
/** The us-2/prod gateway host. The default door — never any other cell silently. */
|
|
24
25
|
export const PROD_GATEWAY_HOST = "https://gateway.mnemom.ai";
|
|
25
26
|
/**
|
|
26
27
|
* Resolve the gateway HOST (scheme + host, no path). An explicit, non-empty
|
|
27
|
-
* `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the
|
|
28
|
-
*
|
|
29
|
-
*
|
|
28
|
+
* `MNEMOM_AGENT_GATEWAY` wins verbatim; otherwise the gateway of the cell the rest
|
|
29
|
+
* of the CLI uses (`getGatewayUrl()`: prod unless MNEMOM_ENV / MNEMOM_RING /
|
|
30
|
+
* MNEMOM_GATEWAY_URL name another). An override on a different cell is refused at
|
|
31
|
+
* launch — see `singleCellError`. Trailing slash trimmed so door concatenation is clean.
|
|
30
32
|
*/
|
|
31
|
-
export function resolveGatewayHost(env = process.env) {
|
|
33
|
+
export function resolveGatewayHost(env = process.env, cellGateway = getGatewayUrl()) {
|
|
32
34
|
const raw = (env.MNEMOM_AGENT_GATEWAY ?? "").trim();
|
|
33
|
-
const host = raw.length > 0 ? raw :
|
|
35
|
+
const host = raw.length > 0 ? raw : cellGateway;
|
|
34
36
|
return host.replace(/\/+$/, "");
|
|
35
37
|
}
|
|
38
|
+
const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
39
|
+
/** Same scheme+host+port. An unparseable URL never matches. */
|
|
40
|
+
export function sameOrigin(a, b) {
|
|
41
|
+
try {
|
|
42
|
+
return new URL(a).origin === new URL(b).origin;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* A launch must use ONE cell: the Anthropic `door` its traffic goes to must be the
|
|
50
|
+
* gateway of the cell the CLI signs in, resolves the agent and reads sessions on
|
|
51
|
+
* (`cellGateway`, paired with `apiUrl`). A mixed launch records one cell's agent
|
|
52
|
+
* and runs as another cell's, so `sessions`/`show` never find its goal state.
|
|
53
|
+
* Returns the refusal message, or null. A loopback door is a local dev gateway and
|
|
54
|
+
* is exempt.
|
|
55
|
+
*/
|
|
56
|
+
export function singleCellError(opts) {
|
|
57
|
+
let doorUrl;
|
|
58
|
+
try {
|
|
59
|
+
doorUrl = new URL(opts.door);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return `mnemom agent: the gateway '${opts.door}' is not a valid URL.`;
|
|
63
|
+
}
|
|
64
|
+
if (LOOPBACK_HOSTS.has(doorUrl.hostname))
|
|
65
|
+
return null;
|
|
66
|
+
if (sameOrigin(opts.door, opts.cellGateway))
|
|
67
|
+
return null;
|
|
68
|
+
return (`mnemom agent: the gateway ${doorUrl.origin} is on a different cell from the one this CLI uses ` +
|
|
69
|
+
`(API ${opts.apiUrl}, gateway ${opts.cellGateway}). A launch must use one cell. Point the whole ` +
|
|
70
|
+
"CLI at that cell (MNEMOM_API_URL + MNEMOM_GATEWAY_URL, or MNEMOM_RING) and drop the gateway " +
|
|
71
|
+
"override (--gateway, MNEMOM_AGENT_GATEWAY, MNEMOM_AGENT_ANTHROPIC_GATEWAY, or `gateway` in launcher.toml).");
|
|
72
|
+
}
|
|
36
73
|
/**
|
|
37
74
|
* The Anthropic door the CLI points Claude Code at:
|
|
38
75
|
* `${MNEMOM_AGENT_GATEWAY}/anthropic`, or the full-door override
|
|
@@ -513,6 +550,20 @@ export function buildSessionConfig(opts) {
|
|
|
513
550
|
config[CONTEXT_HANDOFF_KEY] = "true";
|
|
514
551
|
return config;
|
|
515
552
|
}
|
|
553
|
+
/**
|
|
554
|
+
* The `x-mnemom-session-config` payload: `{ v: 1, config, experimental? }`. The active
|
|
555
|
+
* experimental ids ride as a top-level array, beside (not inside) the env overlay, so the
|
|
556
|
+
* gateway's allow-list never sees them as env keys; it ignores unknown envelope fields.
|
|
557
|
+
*/
|
|
558
|
+
export function buildSessionConfigPayload(opts) {
|
|
559
|
+
const config = buildSessionConfig(opts);
|
|
560
|
+
const experimental = opts.experimental ?? [];
|
|
561
|
+
if (Object.keys(config).length === 0 && experimental.length === 0)
|
|
562
|
+
return undefined;
|
|
563
|
+
return experimental.length > 0
|
|
564
|
+
? { v: 1, config, experimental: [...experimental] }
|
|
565
|
+
: { v: 1, config };
|
|
566
|
+
}
|
|
516
567
|
/**
|
|
517
568
|
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
518
569
|
* nothing to override (the gateway's own defaults stand — `balanced` with handoff off,
|
|
@@ -526,11 +577,10 @@ export function buildSessionConfig(opts) {
|
|
|
526
577
|
* base64url so it survives the gateway's inbound strip.
|
|
527
578
|
*/
|
|
528
579
|
export function buildSessionConfigB64(opts) {
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
580
|
+
const payload = buildSessionConfigPayload(opts);
|
|
581
|
+
if (!payload)
|
|
531
582
|
return undefined;
|
|
532
|
-
|
|
533
|
-
return Buffer.from(payload, "utf8").toString("base64url");
|
|
583
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
534
584
|
}
|
|
535
585
|
/**
|
|
536
586
|
* Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
|
|
@@ -587,10 +637,10 @@ export function buildHandoffMcpConfigJson(p) {
|
|
|
587
637
|
* standing config default must never silently override;
|
|
588
638
|
* 3. the config default `goal_mode` — `"off"` turns alignment off, `"implicit"`
|
|
589
639
|
* keeps it implicit;
|
|
590
|
-
* 4.
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
640
|
+
* 4. with the `implicit` experimental feature ON, IMPLICIT: the gateway infers
|
|
641
|
+
* the goal from the user's own messages (turn it off for a launch with
|
|
642
|
+
* `--no-implicit`, or standing with `goal_mode = "off"`); with it OFF (the
|
|
643
|
+
* default), OFF.
|
|
594
644
|
* Pure so every arm is unit-tested; the caller passes the resolved value on to
|
|
595
645
|
* `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
|
|
596
646
|
* combined with a contract flag — that conflict is EXPLICIT and is reported).
|
|
@@ -600,5 +650,7 @@ export function resolveImplicit(opts) {
|
|
|
600
650
|
return opts.implicit;
|
|
601
651
|
if (hasText(opts.goal))
|
|
602
652
|
return false;
|
|
653
|
+
if (opts.experimentalOn === false)
|
|
654
|
+
return false;
|
|
603
655
|
return opts.configGoalMode !== "off";
|
|
604
656
|
}
|
package/dist/lib/cli-config.d.ts
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* Membership can change after it's stored — consumers must treat a server
|
|
18
18
|
* 403 as the truth (the claim path already renders the teaching list).
|
|
19
19
|
*/
|
|
20
|
+
import { type ExperimentalId, type ExperimentalPrefs, type ExperimentalState } from "./experimental.js";
|
|
20
21
|
import type { UpdatePrefs } from "./update/decide.js";
|
|
21
22
|
export interface ActiveOrg {
|
|
22
23
|
org_id: string;
|
|
@@ -32,6 +33,11 @@ export interface CliConfig {
|
|
|
32
33
|
/** Auto-update preferences (`mnemom config set update.channel|update.mode`). */
|
|
33
34
|
update?: UpdatePrefs;
|
|
34
35
|
sessions?: SessionPrefs;
|
|
36
|
+
/**
|
|
37
|
+
* Experimental features the user turned on (`mnemom experimental enable <id>`). The
|
|
38
|
+
* section's presence (even `{ enabled: [] }`) also marks the one-time migration done.
|
|
39
|
+
*/
|
|
40
|
+
experimental?: ExperimentalPrefs;
|
|
35
41
|
}
|
|
36
42
|
/** The config.json sections `mnemom config` edits. */
|
|
37
43
|
export interface PrefSections {
|
|
@@ -52,3 +58,7 @@ export declare function getActiveOrg(): ActiveOrg | undefined;
|
|
|
52
58
|
export declare function setPref<S extends keyof PrefSections, K extends keyof PrefSections[S] & string>(section: S, key: K, value: PrefSections[S][K] | undefined): void;
|
|
53
59
|
/** One auto-update preference — `setPref("update", …)`. */
|
|
54
60
|
export declare function setUpdatePref<K extends keyof UpdatePrefs>(key: K, value: UpdatePrefs[K] | undefined): void;
|
|
61
|
+
/** Persist the full set of enabled experimental features (an empty list is kept). */
|
|
62
|
+
export declare function setExperimentalEnabled(ids: readonly ExperimentalId[]): void;
|
|
63
|
+
/** Which experimental features are on right now (MNEMOM_EXPERIMENTAL > config.json > off). */
|
|
64
|
+
export declare function loadExperimentalState(env?: Record<string, string | undefined>): ExperimentalState;
|
package/dist/lib/cli-config.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import * as fs from "node:fs";
|
|
21
21
|
import * as path from "node:path";
|
|
22
22
|
import { MNEMOM_DIR } from "./config.js";
|
|
23
|
+
import { resolveExperimental, } from "./experimental.js";
|
|
23
24
|
function configFile() {
|
|
24
25
|
return path.join(MNEMOM_DIR, "config.json");
|
|
25
26
|
}
|
|
@@ -84,3 +85,13 @@ export function setPref(section, key, value) {
|
|
|
84
85
|
export function setUpdatePref(key, value) {
|
|
85
86
|
setPref("update", key, value);
|
|
86
87
|
}
|
|
88
|
+
/** Persist the full set of enabled experimental features (an empty list is kept). */
|
|
89
|
+
export function setExperimentalEnabled(ids) {
|
|
90
|
+
const config = loadCliConfig();
|
|
91
|
+
config.experimental = { enabled: [...ids] };
|
|
92
|
+
saveCliConfig(config);
|
|
93
|
+
}
|
|
94
|
+
/** Which experimental features are on right now (MNEMOM_EXPERIMENTAL > config.json > off). */
|
|
95
|
+
export function loadExperimentalState(env = process.env) {
|
|
96
|
+
return resolveExperimental({ stored: loadCliConfig().experimental, env });
|
|
97
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Experimental features — the single registry and the pure logic around it.
|
|
3
|
+
*
|
|
4
|
+
* An experimental feature is OFF unless the user turns it on (`mnemom experimental
|
|
5
|
+
* enable <id>`, stored in ~/.mnemom/config.json `experimental.enabled`, or the
|
|
6
|
+
* MNEMOM_EXPERIMENTAL env var). While it is off its flags and commands are hidden
|
|
7
|
+
* from --help, using one fails with a pointer to `mnemom experimental enable`, and
|
|
8
|
+
* its launcher.toml keys are ignored with the same pointer. The contract: an
|
|
9
|
+
* experimental feature may change or be removed in any release, with no
|
|
10
|
+
* deprecation period.
|
|
11
|
+
*
|
|
12
|
+
* Everything here is pure or dependency-injected; index.ts and the command
|
|
13
|
+
* modules are the thin drivers.
|
|
14
|
+
*/
|
|
15
|
+
export type ExperimentalId = "context-handoff" | "slack" | "implicit";
|
|
16
|
+
export interface ExperimentalFeature {
|
|
17
|
+
id: ExperimentalId;
|
|
18
|
+
summary: string;
|
|
19
|
+
/** The first CLI release that shipped the feature as experimental. */
|
|
20
|
+
since: string;
|
|
21
|
+
/** What the feature gates while it is off (for `mnemom experimental list`). */
|
|
22
|
+
gates: {
|
|
23
|
+
flags: string[];
|
|
24
|
+
configKeys: string[];
|
|
25
|
+
commands: string[];
|
|
26
|
+
env: string[];
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export declare const EXPERIMENTAL_FEATURES: readonly ExperimentalFeature[];
|
|
30
|
+
export declare const EXPERIMENTAL_IDS: readonly ExperimentalId[];
|
|
31
|
+
export declare const EXPERIMENTAL_ENV = "MNEMOM_EXPERIMENTAL";
|
|
32
|
+
export declare const EXPERIMENTAL_CONTRACT = "Experimental features may change or be removed in any release, with no deprecation period.";
|
|
33
|
+
export declare function isExperimentalId(value: string): value is ExperimentalId;
|
|
34
|
+
/** The config.json `experimental` section. */
|
|
35
|
+
export interface ExperimentalPrefs {
|
|
36
|
+
enabled?: string[];
|
|
37
|
+
}
|
|
38
|
+
export interface ExperimentalState {
|
|
39
|
+
enabled: ExperimentalId[];
|
|
40
|
+
source: "env" | "config" | "default";
|
|
41
|
+
/** Ids named in MNEMOM_EXPERIMENTAL or config.json that this CLI does not know. */
|
|
42
|
+
unknown: string[];
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Which experimental features are on. Precedence: MNEMOM_EXPERIMENTAL (a comma
|
|
46
|
+
* list of ids, `all`, or `none`) replaces the stored set; else config.json
|
|
47
|
+
* `experimental.enabled`; else everything off.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveExperimental(opts: {
|
|
50
|
+
stored: ExperimentalPrefs | undefined;
|
|
51
|
+
env: Record<string, string | undefined>;
|
|
52
|
+
}): ExperimentalState;
|
|
53
|
+
/** The one-line stderr notice for a launch with experimental features on; null when none are. */
|
|
54
|
+
export declare function experimentalNotice(enabled: readonly ExperimentalId[]): string | null;
|
|
55
|
+
/** The error for using an experimental flag or command while its feature is off. */
|
|
56
|
+
export declare function gatedMessage(what: string, id: ExperimentalId): string;
|
|
57
|
+
/** Thrown when a gated flag or command is used while its feature is off; the CLI exits 2. */
|
|
58
|
+
export declare class ExperimentalGateError extends Error {
|
|
59
|
+
readonly exitCode = 2;
|
|
60
|
+
constructor(what: string, id: ExperimentalId);
|
|
61
|
+
}
|
|
62
|
+
/** Throw for the first use whose feature is off. */
|
|
63
|
+
export declare function assertEnabled(uses: ReadonlyArray<{
|
|
64
|
+
what: string;
|
|
65
|
+
id: ExperimentalId;
|
|
66
|
+
used: boolean;
|
|
67
|
+
}>, enabled: readonly ExperimentalId[]): void;
|
|
68
|
+
/**
|
|
69
|
+
* The feature a launcher.toml key/value belongs to, when setting it only has an
|
|
70
|
+
* effect with that feature on. `goal_mode = off`, `context_handoff = false` and
|
|
71
|
+
* `slack_listen = false` are harmless either way.
|
|
72
|
+
*/
|
|
73
|
+
export declare function experimentalIdForConfigKey(key: string, value: string): ExperimentalId | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* The stderr lines for experimental settings a launch ignores because their feature
|
|
76
|
+
* is off: launcher.toml keys (`entries` as `[key, value]`) and MNEMOM_AGENT_CONTEXT_HANDOFF.
|
|
77
|
+
*/
|
|
78
|
+
export declare function ignoredSettingLines(opts: {
|
|
79
|
+
entries: ReadonlyArray<[string, string]>;
|
|
80
|
+
env: Record<string, string | undefined>;
|
|
81
|
+
enabled: readonly ExperimentalId[];
|
|
82
|
+
}): string[];
|
|
83
|
+
/** The features an existing launcher.toml already uses (the one-time migration). */
|
|
84
|
+
export declare function featuresInUse(entries: ReadonlyArray<[string, string]>): ExperimentalId[];
|
|
85
|
+
export interface MigrationDeps {
|
|
86
|
+
stored(): ExperimentalPrefs | undefined;
|
|
87
|
+
/** launcher.toml as `[key, value]` pairs; may throw on a malformed file. */
|
|
88
|
+
launcherEntries(): Array<[string, string]>;
|
|
89
|
+
persist(ids: ExperimentalId[]): void;
|
|
90
|
+
writeErr(line: string): void;
|
|
91
|
+
}
|
|
92
|
+
/** One-time notice for users who had implicit goal alignment by default (no goal_mode set). */
|
|
93
|
+
export declare const IMPLICIT_NOW_OFF_NOTICE: string;
|
|
94
|
+
/**
|
|
95
|
+
* First run after the upgrade that introduced experimental features: config.json has
|
|
96
|
+
* no `experimental` section yet, so turn on whatever the user's launcher.toml already
|
|
97
|
+
* uses, persist (an empty list too — the section's presence marks the migration
|
|
98
|
+
* done), and say once which were kept on. A malformed launcher.toml skips the
|
|
99
|
+
* migration this run (the launch reports that file itself) so it retries next time.
|
|
100
|
+
* Never throws.
|
|
101
|
+
*/
|
|
102
|
+
export declare function migrateExperimental(deps: MigrationDeps): ExperimentalId[] | null;
|
|
103
|
+
/** The feature a launcher.toml key belongs to at all (for marking `mnemom agent config list`). */
|
|
104
|
+
export declare function configKeyFeature(key: string): ExperimentalId | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* ` (experimental, off)` for a launcher.toml key whose feature is off, else "". With a
|
|
107
|
+
* `value`, only a setting that would take effect is marked (`goal_mode = off` is not).
|
|
108
|
+
*/
|
|
109
|
+
export declare function configKeyMark(key: string, value: string | undefined, enabled: readonly ExperimentalId[]): string;
|
|
110
|
+
/** `null` when `id` is on, else the gated-use message. */
|
|
111
|
+
export declare function gateCheck(what: string, id: ExperimentalId, enabled: readonly ExperimentalId[]): string | null;
|
|
112
|
+
/**
|
|
113
|
+
* How a command's catch reports an error: a gated use prints its guidance bare and
|
|
114
|
+
* exits 2; anything else keeps the CLI's `Error: <message>` and exit 1.
|
|
115
|
+
*/
|
|
116
|
+
export declare function reportCliError(error: unknown): {
|
|
117
|
+
args: unknown[];
|
|
118
|
+
code: number;
|
|
119
|
+
};
|