@mnemom/mnemom 0.17.3-next.5 → 0.17.3-next.6
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-session.d.ts +7 -6
- package/dist/commands/agent-session.js +10 -11
- package/dist/commands/agent-sessions.js +3 -0
- package/dist/commands/agent.d.ts +9 -0
- package/dist/commands/agent.js +19 -4
- 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 +82 -16
- package/dist/lib/agent-ledger.d.ts +5 -0
- package/dist/lib/agent-ledger.js +5 -0
- package/dist/lib/agent.d.ts +25 -4
- package/dist/lib/agent.js +23 -8
- 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 +25 -0
- package/package.json +1 -1
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
* Settable keys are the single source of truth in lib/code-config.ts CONFIG_KEYS.
|
|
14
14
|
* The secret Anthropic key is NOT a config key — it lives in ~/.mnemom/launcher.json.
|
|
15
15
|
*/
|
|
16
|
+
import { type ExperimentalId } from "../lib/experimental.js";
|
|
16
17
|
/** Entry point for `mnemom agent config <verb> [args...]`. Throws on misuse. */
|
|
17
|
-
export declare function agentConfigCommand(argv: string[]
|
|
18
|
+
export declare function agentConfigCommand(argv: string[], opts?: {
|
|
19
|
+
enabled?: readonly ExperimentalId[];
|
|
20
|
+
}): Promise<void>;
|
|
18
21
|
/**
|
|
19
22
|
* `$VISUAL` / `$EDITOR` is a program plus optional plain flags (`code --wait`, `emacsclient -t`),
|
|
20
23
|
* never shell syntax: the value is split on whitespace and every token must be a bare
|
|
@@ -16,15 +16,17 @@
|
|
|
16
16
|
import { spawnSync } from "node:child_process";
|
|
17
17
|
import { existsSync } from "node:fs";
|
|
18
18
|
import { LAUNCHER_CONFIG_PATH, CONFIG_KEYS, configKeySpec, loadLauncherConfig, writeLauncherConfig, } from "../lib/launcher-config.js";
|
|
19
|
+
import { loadExperimentalState } from "../lib/cli-config.js";
|
|
20
|
+
import { configKeyMark } from "../lib/experimental.js";
|
|
19
21
|
const out = (line = "") => console.log(line);
|
|
20
22
|
const err = (line = "") => console.error(line);
|
|
21
23
|
/** Entry point for `mnemom agent config <verb> [args...]`. Throws on misuse. */
|
|
22
|
-
export async function agentConfigCommand(argv) {
|
|
24
|
+
export async function agentConfigCommand(argv, opts = {}) {
|
|
23
25
|
const [verb, ...rest] = argv;
|
|
24
26
|
switch (verb) {
|
|
25
27
|
case undefined:
|
|
26
28
|
case "list":
|
|
27
|
-
return listConfig();
|
|
29
|
+
return listConfig(opts.enabled ?? loadExperimentalState().enabled);
|
|
28
30
|
case "path":
|
|
29
31
|
out(LAUNCHER_CONFIG_PATH);
|
|
30
32
|
return;
|
|
@@ -40,7 +42,8 @@ export async function agentConfigCommand(argv) {
|
|
|
40
42
|
throw new Error(`mnemom agent config: unknown verb '${verb}'. Use list | get | set | unset | edit | path.`);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
|
-
|
|
45
|
+
/** Settings of experimental features that are off are marked `(experimental, off)`. */
|
|
46
|
+
function listConfig(enabled) {
|
|
44
47
|
const cfg = loadLauncherConfig();
|
|
45
48
|
const rows = [];
|
|
46
49
|
for (const spec of CONFIG_KEYS) {
|
|
@@ -54,13 +57,13 @@ function listConfig() {
|
|
|
54
57
|
out("");
|
|
55
58
|
out("Settable keys:");
|
|
56
59
|
for (const spec of CONFIG_KEYS)
|
|
57
|
-
out(` ${spec.key.padEnd(22)} ${spec.describe}`);
|
|
60
|
+
out(` ${spec.key.padEnd(22)} ${spec.describe}${configKeyMark(spec.key, undefined, enabled)}`);
|
|
58
61
|
return;
|
|
59
62
|
}
|
|
60
63
|
const width = Math.max(...rows.map(([k]) => k.length));
|
|
61
64
|
out(`# ${LAUNCHER_CONFIG_PATH}`);
|
|
62
65
|
for (const [k, v] of rows)
|
|
63
|
-
out(`${k.padEnd(width)} ${v}`);
|
|
66
|
+
out(`${k.padEnd(width)} ${v}${configKeyMark(k, v, enabled)}`);
|
|
64
67
|
}
|
|
65
68
|
function getConfig(key) {
|
|
66
69
|
if (!key)
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
* 1. a SESSION NAME — labels the session in traces and the claude.ai session
|
|
9
9
|
* list (defaults to the current folder's name).
|
|
10
10
|
*
|
|
11
|
-
* GOAL ALIGNMENT is not prompted for
|
|
12
|
-
*
|
|
13
|
-
* (`x-mnemom-goal-mode: implicit`, no contract)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* GOAL ALIGNMENT is not prompted for. `--goal` seals a one-line contract
|
|
12
|
+
* (`x-mnemom-contract`). Implicit alignment — the goal inferred from your own
|
|
13
|
+
* messages (`x-mnemom-goal-mode: implicit`, no contract) — is the experimental
|
|
14
|
+
* `implicit` feature: while it is on a bare launch runs implicit, while it is off
|
|
15
|
+
* (the default) a bare launch runs with alignment off. `--no-implicit` (or
|
|
16
|
+
* launcher.toml `goal_mode = "off"`) turns it off. The mode is resolved upstream
|
|
17
|
+
* (`resolveImplicit`) and announced on the launch line.
|
|
17
18
|
*
|
|
18
19
|
* Non-interactive runs (pipes, CI) never prompt — they get a clear error asking
|
|
19
20
|
* for a name. Pure apart from the injected `ask` functions, so every arm is
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
* 1. a SESSION NAME — labels the session in traces and the claude.ai session
|
|
9
9
|
* list (defaults to the current folder's name).
|
|
10
10
|
*
|
|
11
|
-
* GOAL ALIGNMENT is not prompted for
|
|
12
|
-
*
|
|
13
|
-
* (`x-mnemom-goal-mode: implicit`, no contract)
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* GOAL ALIGNMENT is not prompted for. `--goal` seals a one-line contract
|
|
12
|
+
* (`x-mnemom-contract`). Implicit alignment — the goal inferred from your own
|
|
13
|
+
* messages (`x-mnemom-goal-mode: implicit`, no contract) — is the experimental
|
|
14
|
+
* `implicit` feature: while it is on a bare launch runs implicit, while it is off
|
|
15
|
+
* (the default) a bare launch runs with alignment off. `--no-implicit` (or
|
|
16
|
+
* launcher.toml `goal_mode = "off"`) turns it off. The mode is resolved upstream
|
|
17
|
+
* (`resolveImplicit`) and announced on the launch line.
|
|
17
18
|
*
|
|
18
19
|
* Non-interactive runs (pipes, CI) never prompt — they get a clear error asking
|
|
19
20
|
* for a name. Pure apart from the injected `ask` functions, so every arm is
|
|
@@ -65,11 +66,9 @@ export async function resolveSessionSetup(input, ask) {
|
|
|
65
66
|
say(` (Next time you can pass it directly: mnemom agent <name>.)`);
|
|
66
67
|
const typed = await ask.askInput(`Session name [${fallback}]:`);
|
|
67
68
|
const scenario = sanitizeAgentName(typed.trim()) || fallback;
|
|
68
|
-
// ── 2. goal alignment — resolved upstream, no prompt
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// `--implicit` / `goal_mode = "implicit"`) is implicit, and `--no-implicit` /
|
|
72
|
-
// `goal_mode = "off"` turns it off. The launch line announces the mode.
|
|
69
|
+
// ── 2. goal alignment — resolved upstream, no prompt: `--goal` seals a contract,
|
|
70
|
+
// the `implicit` experimental feature (when on) makes implicit the default, and
|
|
71
|
+
// `--no-implicit` / `goal_mode = "off"` turns it off. The launch line announces the mode.
|
|
73
72
|
if (flagGoal) {
|
|
74
73
|
return { scenario, goal: flagGoal, goalAlignment: "on", prompted: true };
|
|
75
74
|
}
|
|
@@ -180,6 +180,9 @@ export function buildShowView(local, remote, reason) {
|
|
|
180
180
|
if (local.context_profile)
|
|
181
181
|
launchBits.push(`context ${local.context_profile}`);
|
|
182
182
|
L.push(fmt.label("launch: ", launchBits.join(" · ")));
|
|
183
|
+
if (local.experimental?.length) {
|
|
184
|
+
L.push(fmt.label("experimental: ", local.experimental.join(", ")));
|
|
185
|
+
}
|
|
183
186
|
L.push(fmt.label("conversation: ", local.conversation_id));
|
|
184
187
|
const goalMode = resolveGoalMode(local, remote);
|
|
185
188
|
L.push(fmt.label("goal alignment:", goalBadge(goalMode)));
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* line, or the proxy's stdin under RC). It is never logged, printed, or passed as
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
|
+
import { type ExperimentalId } from "../lib/experimental.js";
|
|
28
29
|
import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
|
|
29
30
|
export interface AgentOptions {
|
|
30
31
|
goal?: string;
|
|
@@ -87,6 +88,12 @@ export interface AgentOptions {
|
|
|
87
88
|
* Undefined = the full default order.
|
|
88
89
|
*/
|
|
89
90
|
keySource?: "env" | "store" | "prompt";
|
|
91
|
+
/**
|
|
92
|
+
* The experimental features on for this launch (`mnemom experimental`), already gated
|
|
93
|
+
* upstream in index.ts. Announced on stderr, recorded in the ledger, and sent in the
|
|
94
|
+
* session-config header's `experimental` array. Absent/empty ⇒ none of the three.
|
|
95
|
+
*/
|
|
96
|
+
experimental?: readonly ExperimentalId[];
|
|
90
97
|
passthrough?: string[];
|
|
91
98
|
/** Print the resolved launch plan and exit — no key read, no spawn. */
|
|
92
99
|
dryRun?: boolean;
|
|
@@ -245,4 +252,6 @@ export declare function printPlan(ctx: {
|
|
|
245
252
|
contextProfile: ContextProfile;
|
|
246
253
|
/** The decoded `x-mnemom-session-config` overlay (`buildSessionConfig`); empty ⇒ no header. */
|
|
247
254
|
sessionConfig: Record<string, string>;
|
|
255
|
+
/** The experimental features on for this launch (the header's `experimental` array). */
|
|
256
|
+
experimental?: readonly string[];
|
|
248
257
|
}): void;
|
package/dist/commands/agent.js
CHANGED
|
@@ -36,6 +36,7 @@ import { getAgentByName } from "../lib/api.js";
|
|
|
36
36
|
import { LEDGER_FILE, appendSessionRecord, resolveRetentionDays, toLedgerContract, } from "../lib/agent-ledger.js";
|
|
37
37
|
import { loadCliConfig } from "../lib/cli-config.js";
|
|
38
38
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
39
|
+
import { experimentalNotice } from "../lib/experimental.js";
|
|
39
40
|
import { fmt } from "../lib/format.js";
|
|
40
41
|
import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
41
42
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
@@ -60,9 +61,9 @@ function sayContextProfile(profile) {
|
|
|
60
61
|
}
|
|
61
62
|
/** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
|
|
62
63
|
export async function agentCommand(scenarioArg, options = {}) {
|
|
63
|
-
// ── session name + goal ── (a bare `mnemom agent` in a terminal asks for
|
|
64
|
-
// goal alignment is
|
|
65
|
-
//
|
|
64
|
+
// ── session name + goal ── (a bare `mnemom agent` in a terminal asks for the name;
|
|
65
|
+
// goal alignment is resolved upstream: --goal seals a contract, implicit alignment
|
|
66
|
+
// is on by default only while the `implicit` experimental feature is on.
|
|
66
67
|
const session = await resolveSessionSetup({
|
|
67
68
|
scenario: scenarioArg,
|
|
68
69
|
goal: options.goal,
|
|
@@ -121,8 +122,14 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
121
122
|
env,
|
|
122
123
|
});
|
|
123
124
|
const contextHandoff = resolveContextHandoff({ contextHandoff: options.contextHandoff, env });
|
|
125
|
+
const experimental = options.experimental ?? [];
|
|
124
126
|
const sessionConfig = buildSessionConfig({ profile: contextProfile, contextHandoff });
|
|
125
|
-
const sessionConfigB64 = buildSessionConfigB64({
|
|
127
|
+
const sessionConfigB64 = buildSessionConfigB64({
|
|
128
|
+
profile: contextProfile,
|
|
129
|
+
contextHandoff,
|
|
130
|
+
experimental,
|
|
131
|
+
});
|
|
132
|
+
const expNotice = experimentalNotice(experimental);
|
|
126
133
|
// ── which CLI executable (name or full path) ── resolve GRACEFULLY: if it's not
|
|
127
134
|
// found we don't crash with a technical error — we ask for the path at launch.
|
|
128
135
|
let cli;
|
|
@@ -163,7 +170,10 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
163
170
|
cli: planCli,
|
|
164
171
|
contextProfile,
|
|
165
172
|
sessionConfig,
|
|
173
|
+
experimental,
|
|
166
174
|
});
|
|
175
|
+
if (expNotice)
|
|
176
|
+
say(expNotice);
|
|
167
177
|
if (previewShape === "remote-control") {
|
|
168
178
|
say("mnemom agent: the plan shows Remote Control because this machine can run it; the real launch uses it only if your account is entitled (internal Mnemom accounts), else terminal-only.");
|
|
169
179
|
}
|
|
@@ -290,6 +300,8 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
290
300
|
contextProfile,
|
|
291
301
|
});
|
|
292
302
|
logLaunch({ shape, door, agent, conversationId, cellNote, envelope, goalMode, cli });
|
|
303
|
+
if (expNotice)
|
|
304
|
+
say(expNotice);
|
|
293
305
|
say(goalAlignmentLine(goalAlignmentMode(envelope, goalMode)));
|
|
294
306
|
sayContextProfile(contextProfile);
|
|
295
307
|
// ── handoff MCP tool (MNE-7564) ── OFF by default: absent/false ⇒ handoffMcpConfigPath stays
|
|
@@ -396,6 +408,7 @@ export async function recordSessionLaunch(ctx) {
|
|
|
396
408
|
goal_mode: goalAlignmentMode(ctx.envelope, ctx.goalMode),
|
|
397
409
|
context_profile: ctx.contextProfile,
|
|
398
410
|
cc_session_id: ctx.ccSessionId ?? null,
|
|
411
|
+
...(ctx.options.experimental?.length ? { experimental: [...ctx.options.experimental] } : {}),
|
|
399
412
|
}, {
|
|
400
413
|
dir: MNEMOM_DIR,
|
|
401
414
|
now: () => new Date(),
|
|
@@ -1110,6 +1123,8 @@ export function printPlan(ctx) {
|
|
|
1110
1123
|
for (const [k, v] of overlay)
|
|
1111
1124
|
L.push(` ${k}=${v}`);
|
|
1112
1125
|
}
|
|
1126
|
+
if (ctx.experimental?.length)
|
|
1127
|
+
L.push(` experimental: ${ctx.experimental.join(", ")}`);
|
|
1113
1128
|
if (ctx.envelope) {
|
|
1114
1129
|
L.push(` contract: ${ctx.envelope.canonical}`);
|
|
1115
1130
|
L.push(` contract sha: ${ctx.envelope.sha}`);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom experimental list [--json] | enable <id> | disable <id>` — turn experimental
|
|
3
|
+
* features on and off (registry: lib/experimental.ts; stored in ~/.mnemom/config.json).
|
|
4
|
+
*/
|
|
5
|
+
import { type ExperimentalId, type ExperimentalPrefs } from "../lib/experimental.js";
|
|
6
|
+
export interface ExperimentalDeps {
|
|
7
|
+
stored(): ExperimentalPrefs | undefined;
|
|
8
|
+
persist(ids: ExperimentalId[]): void;
|
|
9
|
+
env: Record<string, string | undefined>;
|
|
10
|
+
log(line: string): void;
|
|
11
|
+
}
|
|
12
|
+
export declare function experimentalListCommand(opts?: {
|
|
13
|
+
json?: boolean;
|
|
14
|
+
}, deps?: ExperimentalDeps): void;
|
|
15
|
+
export declare function experimentalEnableCommand(id: string | undefined, deps?: ExperimentalDeps): void;
|
|
16
|
+
export declare function experimentalDisableCommand(id: string | undefined, deps?: ExperimentalDeps): void;
|
|
17
|
+
/** The one-time migration (lib/experimental.ts migrateExperimental) against the real files. */
|
|
18
|
+
export declare function runExperimentalMigration(): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom experimental list [--json] | enable <id> | disable <id>` — turn experimental
|
|
3
|
+
* features on and off (registry: lib/experimental.ts; stored in ~/.mnemom/config.json).
|
|
4
|
+
*/
|
|
5
|
+
import { EXPERIMENTAL_CONTRACT, EXPERIMENTAL_ENV, EXPERIMENTAL_FEATURES, EXPERIMENTAL_IDS, isExperimentalId, migrateExperimental, resolveExperimental, } from "../lib/experimental.js";
|
|
6
|
+
import { loadCliConfig, setExperimentalEnabled } from "../lib/cli-config.js";
|
|
7
|
+
import { launcherEntries, loadLauncherConfig } from "../lib/launcher-config.js";
|
|
8
|
+
function defaultDeps() {
|
|
9
|
+
return {
|
|
10
|
+
stored: () => loadCliConfig().experimental,
|
|
11
|
+
persist: (ids) => setExperimentalEnabled(ids),
|
|
12
|
+
env: process.env,
|
|
13
|
+
log: (line) => console.log(line),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function requireId(id, verb) {
|
|
17
|
+
if (!id)
|
|
18
|
+
throw new Error(`mnemom experimental ${verb}: an <id> is required.`);
|
|
19
|
+
const v = id.trim().toLowerCase();
|
|
20
|
+
if (!isExperimentalId(v)) {
|
|
21
|
+
throw new Error(`Unknown experimental feature '${id}'. Valid ids: ${EXPERIMENTAL_IDS.join(", ")}.`);
|
|
22
|
+
}
|
|
23
|
+
return v;
|
|
24
|
+
}
|
|
25
|
+
export function experimentalListCommand(opts = {}, deps = defaultDeps()) {
|
|
26
|
+
const state = resolveExperimental({ stored: deps.stored(), env: deps.env });
|
|
27
|
+
const rows = EXPERIMENTAL_FEATURES.map((f) => ({
|
|
28
|
+
id: f.id,
|
|
29
|
+
enabled: state.enabled.includes(f.id),
|
|
30
|
+
summary: f.summary,
|
|
31
|
+
since: f.since,
|
|
32
|
+
gates: f.gates,
|
|
33
|
+
}));
|
|
34
|
+
if (opts.json) {
|
|
35
|
+
deps.log(JSON.stringify({ contract: EXPERIMENTAL_CONTRACT, source: state.source, features: rows }, null, 2));
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
deps.log(EXPERIMENTAL_CONTRACT);
|
|
39
|
+
deps.log("");
|
|
40
|
+
const width = Math.max(...rows.map((r) => r.id.length));
|
|
41
|
+
for (const r of rows) {
|
|
42
|
+
deps.log(`${r.enabled ? "on " : "off"} ${r.id.padEnd(width)} ${r.summary}`);
|
|
43
|
+
}
|
|
44
|
+
deps.log("");
|
|
45
|
+
if (state.source === "env") {
|
|
46
|
+
deps.log(`(set by ${EXPERIMENTAL_ENV} in your environment — it overrides config.json)`);
|
|
47
|
+
}
|
|
48
|
+
deps.log("Turn one on with `mnemom experimental enable <id>`.");
|
|
49
|
+
}
|
|
50
|
+
function toggle(verb, rawId, deps) {
|
|
51
|
+
const id = requireId(rawId, verb);
|
|
52
|
+
const current = new Set(resolveExperimental({ stored: deps.stored(), env: {} }).enabled);
|
|
53
|
+
if (verb === "enable")
|
|
54
|
+
current.add(id);
|
|
55
|
+
else
|
|
56
|
+
current.delete(id);
|
|
57
|
+
const next = EXPERIMENTAL_IDS.filter((x) => current.has(x));
|
|
58
|
+
deps.persist(next);
|
|
59
|
+
deps.log(verb === "enable" ? `${id}: on. ${EXPERIMENTAL_CONTRACT}` : `${id}: off.`);
|
|
60
|
+
if ((deps.env[EXPERIMENTAL_ENV] ?? "").trim()) {
|
|
61
|
+
deps.log(`note: ${EXPERIMENTAL_ENV} is set in your environment and overrides this.`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
export function experimentalEnableCommand(id, deps = defaultDeps()) {
|
|
65
|
+
toggle("enable", id, deps);
|
|
66
|
+
}
|
|
67
|
+
export function experimentalDisableCommand(id, deps = defaultDeps()) {
|
|
68
|
+
toggle("disable", id, deps);
|
|
69
|
+
}
|
|
70
|
+
/** The one-time migration (lib/experimental.ts migrateExperimental) against the real files. */
|
|
71
|
+
export function runExperimentalMigration() {
|
|
72
|
+
migrateExperimental({
|
|
73
|
+
stored: () => loadCliConfig().experimental,
|
|
74
|
+
launcherEntries: () => launcherEntries(loadLauncherConfig()),
|
|
75
|
+
persist: (ids) => setExperimentalEnabled(ids),
|
|
76
|
+
writeErr: (line) => void process.stderr.write(line),
|
|
77
|
+
});
|
|
78
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export { program };
|
|
|
7
7
|
* unknown — so it bypasses commander entirely and reaches the sub-verb parser
|
|
8
8
|
* verbatim. Exported (not auto-run) so the bypass is unit-testable.
|
|
9
9
|
*/
|
|
10
|
-
export declare function runEntrypoint(argv?: string[]): void;
|
|
10
|
+
export declare function runEntrypoint(argv?: string[], migrate?: () => void): void;
|
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";
|
|
@@ -103,6 +106,19 @@ program
|
|
|
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,8 +135,8 @@ 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
142
|
.option("--gateway <url>", "Gateway host override (default: https://gateway.mnemom.ai; or MNEMOM_AGENT_GATEWAY)")
|
|
@@ -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
|
@@ -324,6 +324,20 @@ export declare function buildSessionConfig(opts: {
|
|
|
324
324
|
profile: ContextProfile;
|
|
325
325
|
contextHandoff: boolean;
|
|
326
326
|
}): Record<string, string>;
|
|
327
|
+
/**
|
|
328
|
+
* The `x-mnemom-session-config` payload: `{ v: 1, config, experimental? }`. The active
|
|
329
|
+
* experimental ids ride as a top-level array, beside (not inside) the env overlay, so the
|
|
330
|
+
* gateway's allow-list never sees them as env keys; it ignores unknown envelope fields.
|
|
331
|
+
*/
|
|
332
|
+
export declare function buildSessionConfigPayload(opts: {
|
|
333
|
+
profile: ContextProfile;
|
|
334
|
+
contextHandoff: boolean;
|
|
335
|
+
experimental?: readonly string[];
|
|
336
|
+
}): {
|
|
337
|
+
v: 1;
|
|
338
|
+
config: Record<string, string>;
|
|
339
|
+
experimental?: string[];
|
|
340
|
+
} | undefined;
|
|
327
341
|
/**
|
|
328
342
|
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
329
343
|
* nothing to override (the gateway's own defaults stand — `balanced` with handoff off,
|
|
@@ -339,6 +353,7 @@ export declare function buildSessionConfig(opts: {
|
|
|
339
353
|
export declare function buildSessionConfigB64(opts: {
|
|
340
354
|
profile: ContextProfile;
|
|
341
355
|
contextHandoff: boolean;
|
|
356
|
+
experimental?: readonly string[];
|
|
342
357
|
}): string | undefined;
|
|
343
358
|
/**
|
|
344
359
|
* Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
|
|
@@ -427,10 +442,10 @@ export type GoalModeDefault = "implicit" | "off";
|
|
|
427
442
|
* standing config default must never silently override;
|
|
428
443
|
* 3. the config default `goal_mode` — `"off"` turns alignment off, `"implicit"`
|
|
429
444
|
* keeps it implicit;
|
|
430
|
-
* 4.
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
*
|
|
445
|
+
* 4. with the `implicit` experimental feature ON, IMPLICIT: the gateway infers
|
|
446
|
+
* the goal from the user's own messages (turn it off for a launch with
|
|
447
|
+
* `--no-implicit`, or standing with `goal_mode = "off"`); with it OFF (the
|
|
448
|
+
* default), OFF.
|
|
434
449
|
* Pure so every arm is unit-tested; the caller passes the resolved value on to
|
|
435
450
|
* `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
|
|
436
451
|
* combined with a contract flag — that conflict is EXPLICIT and is reported).
|
|
@@ -439,4 +454,10 @@ export declare function resolveImplicit(opts: {
|
|
|
439
454
|
goal?: string;
|
|
440
455
|
implicit?: boolean;
|
|
441
456
|
configGoalMode?: GoalModeDefault;
|
|
457
|
+
/**
|
|
458
|
+
* Is the `implicit` experimental feature on? Off ⇒ the default is OFF and
|
|
459
|
+
* `goal_mode = implicit` is ignored (`--implicit` is refused upstream, before
|
|
460
|
+
* this runs). Omitted ⇒ on.
|
|
461
|
+
*/
|
|
462
|
+
experimentalOn?: boolean;
|
|
442
463
|
}): boolean;
|
package/dist/lib/agent.js
CHANGED
|
@@ -513,6 +513,20 @@ export function buildSessionConfig(opts) {
|
|
|
513
513
|
config[CONTEXT_HANDOFF_KEY] = "true";
|
|
514
514
|
return config;
|
|
515
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* The `x-mnemom-session-config` payload: `{ v: 1, config, experimental? }`. The active
|
|
518
|
+
* experimental ids ride as a top-level array, beside (not inside) the env overlay, so the
|
|
519
|
+
* gateway's allow-list never sees them as env keys; it ignores unknown envelope fields.
|
|
520
|
+
*/
|
|
521
|
+
export function buildSessionConfigPayload(opts) {
|
|
522
|
+
const config = buildSessionConfig(opts);
|
|
523
|
+
const experimental = opts.experimental ?? [];
|
|
524
|
+
if (Object.keys(config).length === 0 && experimental.length === 0)
|
|
525
|
+
return undefined;
|
|
526
|
+
return experimental.length > 0
|
|
527
|
+
? { v: 1, config, experimental: [...experimental] }
|
|
528
|
+
: { v: 1, config };
|
|
529
|
+
}
|
|
516
530
|
/**
|
|
517
531
|
* The `x-mnemom-session-config` value for this launch, or `undefined` when there is
|
|
518
532
|
* nothing to override (the gateway's own defaults stand — `balanced` with handoff off,
|
|
@@ -526,11 +540,10 @@ export function buildSessionConfig(opts) {
|
|
|
526
540
|
* base64url so it survives the gateway's inbound strip.
|
|
527
541
|
*/
|
|
528
542
|
export function buildSessionConfigB64(opts) {
|
|
529
|
-
const
|
|
530
|
-
if (
|
|
543
|
+
const payload = buildSessionConfigPayload(opts);
|
|
544
|
+
if (!payload)
|
|
531
545
|
return undefined;
|
|
532
|
-
|
|
533
|
-
return Buffer.from(payload, "utf8").toString("base64url");
|
|
546
|
+
return Buffer.from(JSON.stringify(payload), "utf8").toString("base64url");
|
|
534
547
|
}
|
|
535
548
|
/**
|
|
536
549
|
* Is the `handoff` MCP tool + `CONTEXT_HANDOFF_ENABLED` session-config header ON for
|
|
@@ -587,10 +600,10 @@ export function buildHandoffMcpConfigJson(p) {
|
|
|
587
600
|
* standing config default must never silently override;
|
|
588
601
|
* 3. the config default `goal_mode` — `"off"` turns alignment off, `"implicit"`
|
|
589
602
|
* keeps it implicit;
|
|
590
|
-
* 4.
|
|
591
|
-
*
|
|
592
|
-
*
|
|
593
|
-
*
|
|
603
|
+
* 4. with the `implicit` experimental feature ON, IMPLICIT: the gateway infers
|
|
604
|
+
* the goal from the user's own messages (turn it off for a launch with
|
|
605
|
+
* `--no-implicit`, or standing with `goal_mode = "off"`); with it OFF (the
|
|
606
|
+
* default), OFF.
|
|
594
607
|
* Pure so every arm is unit-tested; the caller passes the resolved value on to
|
|
595
608
|
* `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
|
|
596
609
|
* combined with a contract flag — that conflict is EXPLICIT and is reported).
|
|
@@ -600,5 +613,7 @@ export function resolveImplicit(opts) {
|
|
|
600
613
|
return opts.implicit;
|
|
601
614
|
if (hasText(opts.goal))
|
|
602
615
|
return false;
|
|
616
|
+
if (opts.experimentalOn === false)
|
|
617
|
+
return false;
|
|
603
618
|
return opts.configGoalMode !== "off";
|
|
604
619
|
}
|
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
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
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 const EXPERIMENTAL_FEATURES = [
|
|
16
|
+
{
|
|
17
|
+
id: "context-handoff",
|
|
18
|
+
summary: "Register the `handoff(note)` MCP tool and turn on the gateway's context-handoff capability for the session",
|
|
19
|
+
since: "0.17.3",
|
|
20
|
+
gates: {
|
|
21
|
+
flags: ["--context-handoff"],
|
|
22
|
+
configKeys: ["context_handoff"],
|
|
23
|
+
commands: [],
|
|
24
|
+
env: ["MNEMOM_AGENT_CONTEXT_HANDOFF"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "slack",
|
|
29
|
+
summary: "Attach the Slack channel to a session: messages addressed to it arrive in it, and it can reply",
|
|
30
|
+
since: "0.17.3",
|
|
31
|
+
gates: {
|
|
32
|
+
flags: ["--slack"],
|
|
33
|
+
configKeys: ["slack_channel", "slack_listen", "slack_allow"],
|
|
34
|
+
commands: ["mnemom agent slack"],
|
|
35
|
+
env: [],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "implicit",
|
|
40
|
+
summary: "Implicit goal alignment: the gateway infers the goal from your messages and revises it as you steer",
|
|
41
|
+
since: "0.17.3",
|
|
42
|
+
gates: {
|
|
43
|
+
flags: ["--implicit"],
|
|
44
|
+
configKeys: ["goal_mode = implicit"],
|
|
45
|
+
commands: ["mnemom agent goal show|pin|reset"],
|
|
46
|
+
env: [],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
export const EXPERIMENTAL_IDS = EXPERIMENTAL_FEATURES.map((f) => f.id);
|
|
51
|
+
export const EXPERIMENTAL_ENV = "MNEMOM_EXPERIMENTAL";
|
|
52
|
+
export const EXPERIMENTAL_CONTRACT = "Experimental features may change or be removed in any release, with no deprecation period.";
|
|
53
|
+
export function isExperimentalId(value) {
|
|
54
|
+
return EXPERIMENTAL_IDS.includes(value);
|
|
55
|
+
}
|
|
56
|
+
/** Registry order, deduplicated, unknown ids dropped. */
|
|
57
|
+
function normalize(ids) {
|
|
58
|
+
const set = new Set(ids);
|
|
59
|
+
return EXPERIMENTAL_IDS.filter((id) => set.has(id));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Which experimental features are on. Precedence: MNEMOM_EXPERIMENTAL (a comma
|
|
63
|
+
* list of ids, `all`, or `none`) replaces the stored set; else config.json
|
|
64
|
+
* `experimental.enabled`; else everything off.
|
|
65
|
+
*/
|
|
66
|
+
export function resolveExperimental(opts) {
|
|
67
|
+
const raw = (opts.env[EXPERIMENTAL_ENV] ?? "").trim();
|
|
68
|
+
if (raw !== "") {
|
|
69
|
+
const tokens = raw
|
|
70
|
+
.split(",")
|
|
71
|
+
.map((t) => t.trim().toLowerCase())
|
|
72
|
+
.filter(Boolean);
|
|
73
|
+
if (tokens.includes("all"))
|
|
74
|
+
return { enabled: [...EXPERIMENTAL_IDS], source: "env", unknown: [] };
|
|
75
|
+
const listed = tokens.filter((t) => t !== "none");
|
|
76
|
+
return {
|
|
77
|
+
enabled: normalize(listed),
|
|
78
|
+
source: "env",
|
|
79
|
+
unknown: listed.filter((t) => !isExperimentalId(t)),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
const stored = Array.isArray(opts.stored?.enabled) ? opts.stored.enabled.map(String) : undefined;
|
|
83
|
+
if (stored === undefined)
|
|
84
|
+
return { enabled: [], source: "default", unknown: [] };
|
|
85
|
+
return {
|
|
86
|
+
enabled: normalize(stored),
|
|
87
|
+
source: "config",
|
|
88
|
+
unknown: stored.filter((t) => !isExperimentalId(t)),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/** The one-line stderr notice for a launch with experimental features on; null when none are. */
|
|
92
|
+
export function experimentalNotice(enabled) {
|
|
93
|
+
if (enabled.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
return `mnemom agent: experimental: ${enabled.join(", ")} — may change or break without notice`;
|
|
96
|
+
}
|
|
97
|
+
/** The error for using an experimental flag or command while its feature is off. */
|
|
98
|
+
export function gatedMessage(what, id) {
|
|
99
|
+
return `${what} is experimental. Turn it on with \`mnemom experimental enable ${id}\`.`;
|
|
100
|
+
}
|
|
101
|
+
/** Thrown when a gated flag or command is used while its feature is off; the CLI exits 2. */
|
|
102
|
+
export class ExperimentalGateError extends Error {
|
|
103
|
+
exitCode = 2;
|
|
104
|
+
constructor(what, id) {
|
|
105
|
+
super(gatedMessage(what, id));
|
|
106
|
+
this.name = "ExperimentalGateError";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/** Throw for the first use whose feature is off. */
|
|
110
|
+
export function assertEnabled(uses, enabled) {
|
|
111
|
+
for (const u of uses) {
|
|
112
|
+
if (u.used && !enabled.includes(u.id))
|
|
113
|
+
throw new ExperimentalGateError(u.what, u.id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* The feature a launcher.toml key/value belongs to, when setting it only has an
|
|
118
|
+
* effect with that feature on. `goal_mode = off`, `context_handoff = false` and
|
|
119
|
+
* `slack_listen = false` are harmless either way.
|
|
120
|
+
*/
|
|
121
|
+
export function experimentalIdForConfigKey(key, value) {
|
|
122
|
+
switch (key) {
|
|
123
|
+
case "slack_channel":
|
|
124
|
+
case "slack_allow":
|
|
125
|
+
return "slack";
|
|
126
|
+
case "slack_listen":
|
|
127
|
+
return value === "true" ? "slack" : undefined;
|
|
128
|
+
case "context_handoff":
|
|
129
|
+
return value === "true" ? "context-handoff" : undefined;
|
|
130
|
+
case "goal_mode":
|
|
131
|
+
return value === "implicit" ? "implicit" : undefined;
|
|
132
|
+
default:
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Is an env value one of the truthy spellings resolveContextHandoff accepts? */
|
|
137
|
+
function truthy(raw) {
|
|
138
|
+
return ["1", "true", "on", "yes"].includes((raw ?? "").trim().toLowerCase());
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The stderr lines for experimental settings a launch ignores because their feature
|
|
142
|
+
* is off: launcher.toml keys (`entries` as `[key, value]`) and MNEMOM_AGENT_CONTEXT_HANDOFF.
|
|
143
|
+
*/
|
|
144
|
+
export function ignoredSettingLines(opts) {
|
|
145
|
+
const lines = [];
|
|
146
|
+
for (const [key, value] of opts.entries) {
|
|
147
|
+
const id = experimentalIdForConfigKey(key, value);
|
|
148
|
+
if (id && !opts.enabled.includes(id)) {
|
|
149
|
+
lines.push(`mnemom agent: ignoring launcher.toml ${gatedMessage(key, id)}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (truthy(opts.env.MNEMOM_AGENT_CONTEXT_HANDOFF) && !opts.enabled.includes("context-handoff")) {
|
|
153
|
+
lines.push(`mnemom agent: ignoring ${gatedMessage("MNEMOM_AGENT_CONTEXT_HANDOFF", "context-handoff")}`);
|
|
154
|
+
}
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
/** The features an existing launcher.toml already uses (the one-time migration). */
|
|
158
|
+
export function featuresInUse(entries) {
|
|
159
|
+
return normalize(entries.flatMap(([k, v]) => {
|
|
160
|
+
const id = experimentalIdForConfigKey(k, v);
|
|
161
|
+
return id ? [id] : [];
|
|
162
|
+
}));
|
|
163
|
+
}
|
|
164
|
+
/** One-time notice for users who had implicit goal alignment by default (no goal_mode set). */
|
|
165
|
+
export const IMPLICIT_NOW_OFF_NOTICE = "mnemom: implicit goal alignment is now experimental and OFF by default (it used to be on). " +
|
|
166
|
+
"Turn it back on with `mnemom experimental enable implicit`, or seal a goal per launch with --goal.\n";
|
|
167
|
+
/**
|
|
168
|
+
* First run after the upgrade that introduced experimental features: config.json has
|
|
169
|
+
* no `experimental` section yet, so turn on whatever the user's launcher.toml already
|
|
170
|
+
* uses, persist (an empty list too — the section's presence marks the migration
|
|
171
|
+
* done), and say once which were kept on. A malformed launcher.toml skips the
|
|
172
|
+
* migration this run (the launch reports that file itself) so it retries next time.
|
|
173
|
+
* Never throws.
|
|
174
|
+
*/
|
|
175
|
+
export function migrateExperimental(deps) {
|
|
176
|
+
try {
|
|
177
|
+
if (deps.stored() !== undefined)
|
|
178
|
+
return null;
|
|
179
|
+
const entries = deps.launcherEntries();
|
|
180
|
+
const ids = featuresInUse(entries);
|
|
181
|
+
deps.persist(ids);
|
|
182
|
+
if (ids.length > 0) {
|
|
183
|
+
deps.writeErr(`mnemom: ${ids.join(", ")} ${ids.length === 1 ? "is" : "are"} now experimental and kept on because your launcher.toml uses ${ids.length === 1 ? "it" : "them"} — see \`mnemom experimental list\`.\n`);
|
|
184
|
+
}
|
|
185
|
+
// Implicit was the unconfigured DEFAULT, so most users relied on it without a
|
|
186
|
+
// goal_mode entry. Don't let it switch off silently for them: say so once.
|
|
187
|
+
const optedOut = entries.some(([k, v]) => k === "goal_mode" && v === "off");
|
|
188
|
+
if (!ids.includes("implicit") && !optedOut)
|
|
189
|
+
deps.writeErr(IMPLICIT_NOW_OFF_NOTICE);
|
|
190
|
+
return ids;
|
|
191
|
+
}
|
|
192
|
+
catch {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/** The feature a launcher.toml key belongs to at all (for marking `mnemom agent config list`). */
|
|
197
|
+
export function configKeyFeature(key) {
|
|
198
|
+
if (key === "slack_channel" || key === "slack_listen" || key === "slack_allow")
|
|
199
|
+
return "slack";
|
|
200
|
+
if (key === "context_handoff")
|
|
201
|
+
return "context-handoff";
|
|
202
|
+
if (key === "goal_mode")
|
|
203
|
+
return "implicit";
|
|
204
|
+
return undefined;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* ` (experimental, off)` for a launcher.toml key whose feature is off, else "". With a
|
|
208
|
+
* `value`, only a setting that would take effect is marked (`goal_mode = off` is not).
|
|
209
|
+
*/
|
|
210
|
+
export function configKeyMark(key, value, enabled) {
|
|
211
|
+
const id = value === undefined ? configKeyFeature(key) : experimentalIdForConfigKey(key, value);
|
|
212
|
+
return id && !enabled.includes(id) ? " (experimental, off)" : "";
|
|
213
|
+
}
|
|
214
|
+
/** `null` when `id` is on, else the gated-use message. */
|
|
215
|
+
export function gateCheck(what, id, enabled) {
|
|
216
|
+
return enabled.includes(id) ? null : gatedMessage(what, id);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* How a command's catch reports an error: a gated use prints its guidance bare and
|
|
220
|
+
* exits 2; anything else keeps the CLI's `Error: <message>` and exit 1.
|
|
221
|
+
*/
|
|
222
|
+
export function reportCliError(error) {
|
|
223
|
+
if (error instanceof ExperimentalGateError)
|
|
224
|
+
return { args: [error.message], code: 2 };
|
|
225
|
+
return { args: ["Error:", error instanceof Error ? error.message : error], code: 1 };
|
|
226
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* resolvers in lib/code.ts keep the built-in default as the final fallback);
|
|
16
16
|
* the caller folds the plain-value settings under the CLI flags.
|
|
17
17
|
*/
|
|
18
|
+
import type { ExperimentalId } from "./experimental.js";
|
|
18
19
|
import { type ContextProfile, type GoalModeDefault, type LaunchShape } from "./agent.js";
|
|
19
20
|
/** The human-editable settings file. Secret key lives elsewhere (launcher.json). */
|
|
20
21
|
export declare const LAUNCHER_CONFIG_PATH: string;
|
|
@@ -98,3 +99,11 @@ export declare function writeLauncherConfig(cfg: LauncherConfig, path?: string):
|
|
|
98
99
|
* (see resolveLaunchShape): terminal→"0", remote-control→"1", server→"server".
|
|
99
100
|
*/
|
|
100
101
|
export declare function applyConfigToEnv(cfg: LauncherConfig, env?: NodeJS.ProcessEnv): void;
|
|
102
|
+
/** The set keys of `cfg` as `[key, value]` pairs (CONFIG_KEYS order, values stringified). */
|
|
103
|
+
export declare function launcherEntries(cfg: LauncherConfig): Array<[string, string]>;
|
|
104
|
+
/**
|
|
105
|
+
* `cfg` without the settings of experimental features that are off (a copy; `cfg` is
|
|
106
|
+
* untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
|
|
107
|
+
* off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
|
|
108
|
+
*/
|
|
109
|
+
export declare function withoutDisabledExperimental(cfg: LauncherConfig, enabled: readonly ExperimentalId[]): LauncherConfig;
|
|
@@ -402,3 +402,28 @@ export function applyConfigToEnv(cfg, env = process.env) {
|
|
|
402
402
|
env.MNEMOM_AGENT_CONTEXT_PROFILE = cfg.contextProfile;
|
|
403
403
|
}
|
|
404
404
|
}
|
|
405
|
+
/** The set keys of `cfg` as `[key, value]` pairs (CONFIG_KEYS order, values stringified). */
|
|
406
|
+
export function launcherEntries(cfg) {
|
|
407
|
+
return CONFIG_KEYS.flatMap((spec) => {
|
|
408
|
+
const v = spec.get(cfg);
|
|
409
|
+
return v === undefined ? [] : [[spec.key, String(v)]];
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* `cfg` without the settings of experimental features that are off (a copy; `cfg` is
|
|
414
|
+
* untouched): `slack_*` while `slack` is off, `context_handoff` while `context-handoff` is
|
|
415
|
+
* off, and `goal_mode = implicit` while `implicit` is off (`goal_mode = off` is kept).
|
|
416
|
+
*/
|
|
417
|
+
export function withoutDisabledExperimental(cfg, enabled) {
|
|
418
|
+
const out = { ...cfg };
|
|
419
|
+
if (!enabled.includes("slack")) {
|
|
420
|
+
delete out.slackChannel;
|
|
421
|
+
delete out.slackListen;
|
|
422
|
+
delete out.slackAllow;
|
|
423
|
+
}
|
|
424
|
+
if (!enabled.includes("context-handoff"))
|
|
425
|
+
delete out.contextHandoff;
|
|
426
|
+
if (!enabled.includes("implicit") && out.goalMode === "implicit")
|
|
427
|
+
delete out.goalMode;
|
|
428
|
+
return out;
|
|
429
|
+
}
|