@mnemom/mnemom 0.17.0-next.0 → 0.17.0-next.2

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.
@@ -5,12 +5,16 @@
5
5
  * If a customer names no agent, `mnemom code` launches under a per-customer
6
6
  * governed identity `code-<handle>`. This module makes that identity REAL in
7
7
  * their Mnemom org: it births the agent through the gateway, claims it (so the
8
- * customer owns it), and publishes a sane default coding posture nudge/nudge:
9
- * - alignment card: autonomy_mode + integrity_mode = nudge,
8
+ * customer owns it), and publishes a sane default coding posture. The CARDS are
9
+ * observe-only (visibility, no active intervention); the goal contract is what
10
+ * nudges (re-anchor on drift). Specifically:
11
+ * - alignment card: autonomy_mode + integrity_mode = observe,
10
12
  * principal.relationship = delegated_authority, coding bounded/forbidden
11
13
  * actions, escalate-on-irreversible;
12
- * - protection card: mode = nudge, thresholds 0.60/0.80/0.95, all four
13
- * screen_surfaces on.
14
+ * - protection card: mode = observe, thresholds 0.60/0.80/0.95, all four
15
+ * screen_surfaces on (screened for observation).
16
+ * - goal-alignment: nudge — sealed per-conversation via the goal contract
17
+ * (--goal …), graded and re-anchored by the gateway.
14
18
  *
15
19
  * All account writes are gated: interactive runs show the plan and confirm;
16
20
  * non-interactive runs provision only with an explicit `--setup`, and otherwise
@@ -5,12 +5,16 @@
5
5
  * If a customer names no agent, `mnemom code` launches under a per-customer
6
6
  * governed identity `code-<handle>`. This module makes that identity REAL in
7
7
  * their Mnemom org: it births the agent through the gateway, claims it (so the
8
- * customer owns it), and publishes a sane default coding posture nudge/nudge:
9
- * - alignment card: autonomy_mode + integrity_mode = nudge,
8
+ * customer owns it), and publishes a sane default coding posture. The CARDS are
9
+ * observe-only (visibility, no active intervention); the goal contract is what
10
+ * nudges (re-anchor on drift). Specifically:
11
+ * - alignment card: autonomy_mode + integrity_mode = observe,
10
12
  * principal.relationship = delegated_authority, coding bounded/forbidden
11
13
  * actions, escalate-on-irreversible;
12
- * - protection card: mode = nudge, thresholds 0.60/0.80/0.95, all four
13
- * screen_surfaces on.
14
+ * - protection card: mode = observe, thresholds 0.60/0.80/0.95, all four
15
+ * screen_surfaces on (screened for observation).
16
+ * - goal-alignment: nudge — sealed per-conversation via the goal contract
17
+ * (--goal …), graded and re-anchored by the gateway.
14
18
  *
15
19
  * All account writes are gated: interactive runs show the plan and confirm;
16
20
  * non-interactive runs provision only with an explicit `--setup`, and otherwise
@@ -83,21 +87,21 @@ export async function ensureGovernedAgent(opts) {
83
87
  say(fmt.section("Set up a governed coding agent"));
84
88
  say(fmt.label(" Agent:", slug));
85
89
  say(fmt.label(" Org:", orgLabel));
86
- say(fmt.label(" Posture:", "nudge / nudge (alignment: autonomy + integrity nudge; protection: all surfaces screened)"));
90
+ say(fmt.label(" Posture:", "observe (alignment + protection observe-only, all surfaces screened); goal contracts nudge"));
87
91
  say(fmt.dim(" Births the agent through the gateway, claims it to your org, and publishes its cards."));
88
92
  if (!(await askYesNo("Provision and claim this agent now?", true))) {
89
93
  say(fmt.dim(" Skipped — launching under the unclaimed identity. Run `mnemom code setup` anytime."));
90
94
  return { slug, provisioned: false };
91
95
  }
92
96
  }
93
- // 5. Birth → claim → publish nudge/nudge cards. (Key never logged.)
97
+ // 5. Birth → claim → publish observe-only cards. (Key never logged.)
94
98
  const gatewayUrl = opts.gatewayHost.replace(/\/+$/, "");
95
99
  const agentId = await birthThroughGateway(gatewayUrl, "anthropic", slug, opts.anthropicKey);
96
100
  const hashProof = deriveHashProof(opts.anthropicKey, slug);
97
101
  await claimAgent(agentId, { hashProof, orgId: org?.org_id });
98
102
  await putAlignmentCard(agentId, JSON.stringify(buildCodeAlignmentCard(agentId)), "application/json");
99
103
  await putProtectionCard(agentId, JSON.stringify(buildCodeProtectionCard(agentId)), "application/json");
100
- say(fmt.success(`mnemom code: provisioned + claimed '${slug}' (${agentId}) in ${orgLabel} — nudge/nudge posture published.`));
104
+ say(fmt.success(`mnemom code: provisioned + claimed '${slug}' (${agentId}) in ${orgLabel} — observe posture published (goal contracts nudge).`));
101
105
  // 6. Remember the slug so future launches skip straight to usable.
102
106
  persistAgentSlug(slug);
103
107
  return { slug, provisioned: true };
@@ -125,7 +129,7 @@ export async function codeSetupCommand(opts = {}) {
125
129
  }
126
130
  function warnUnclaimed(slug) {
127
131
  say(fmt.warn(`mnemom code: agent '${slug}' is not set up in your org — launching with an UNCLAIMED identity (governance is advisory/fail-open).`));
128
- say(fmt.dim(" Provision it (creates + claims the agent, publishes nudge/nudge cards): mnemom code setup"));
132
+ say(fmt.dim(" Provision it (creates + claims the agent, publishes observe cards): mnemom code setup"));
129
133
  }
130
134
  function persistAgentSlug(slug) {
131
135
  try {
@@ -145,7 +149,7 @@ function persistAgentSlug(slug) {
145
149
  export class MuInsufficientError extends Error {
146
150
  orgLabel;
147
151
  constructor(orgLabel) {
148
- super("insufficient MU balance");
152
+ super("insufficient µ balance");
149
153
  this.orgLabel = orgLabel;
150
154
  this.name = "MuInsufficientError";
151
155
  }
@@ -176,7 +180,7 @@ export async function ensureMnemomSession(opts = {}) {
176
180
  return false;
177
181
  say(fmt.section("Sign in to Mnemom"));
178
182
  say(fmt.dim(" mnemom code runs your coding session governed and under your Mnemom account,"));
179
- say(fmt.dim(" so it shows up in your traces and bills to your org's Mnemom Units."));
183
+ say(fmt.dim(" so it shows up in your traces and draws on your org's µ balance."));
180
184
  say(fmt.dim(` New to Mnemom? You can create a free account in the same flow (or at ${getWebsiteUrl()}/signup).`));
181
185
  if (!(await askYesNo("Sign in or create your account now?", true)))
182
186
  return false;
@@ -237,21 +241,20 @@ function safeBigInt(v) {
237
241
  }
238
242
  }
239
243
  /**
240
- * The "you need Mnemom Units" message (no free tier). Voiced like the portal's
244
+ * The "you need µ" message (no free tier). Voiced like the portal's
241
245
  * enforcement-paused banner — safety features can't run without µ — but never
242
246
  * says "exhausted" (that assumes a prior balance; a new org may simply have none).
243
247
  */
244
248
  function printTopUp(orgLabel, balanceMu) {
245
249
  say();
246
- say(fmt.warn(`No Mnemom Units — ${orgLabel} has a balance of ${balanceMu} µ.`));
247
- say(fmt.dim(" mnemom code runs on Mnemom Units (µ), and there's no free tier. Without µ,"));
248
- say(fmt.dim(" Mnemom's safety features can't run, so the launch is blocked. Top up to launch"));
249
- say(fmt.dim(" a governed session:"));
250
+ say(fmt.warn(`No µ balance — ${orgLabel} is at ${balanceMu} µ.`));
251
+ say(fmt.dim(" mnemom code runs on µ, and there's no free tier. Without µ, Mnemom's safety"));
252
+ say(fmt.dim(" features can't run, so the launch is blocked. Top up to launch a governed session:"));
250
253
  say();
251
254
  say(` Top up µ: ${getWebsiteUrl()}/settings/billing`);
252
255
  say(fmt.dim(" If you don't manage billing for this org, ask an admin to top up."));
253
256
  say();
254
- say(fmt.dim(" Your balance updates the moment a top-up completes — then re-run `mnemom code`."));
257
+ say(fmt.dim(" Your µ balance updates the moment a top-up completes — then re-run `mnemom code`."));
255
258
  }
256
259
  /**
257
260
  * The shared pre-launch preamble for `mnemom code` (and `mnemom code setup`):
@@ -264,9 +267,9 @@ export async function ensureSessionAndBalance(opts) {
264
267
  const interactive = opts.interactive ?? isInteractive();
265
268
  const haveSession = await ensureMnemomSession({ interactive });
266
269
  if (!haveSession) {
267
- throw new Error("mnemom code needs a Mnemom account — it runs your coding session governed and on Mnemom Units.\n" +
270
+ throw new Error("mnemom code needs a Mnemom account — it runs your coding session governed and on your org's µ balance.\n" +
268
271
  " Sign in: mnemom login (or set MNEMOM_API_KEY)\n" +
269
- ` New here? create a free account, then add MUs: ${getWebsiteUrl()}/settings/billing`);
272
+ ` New here? create a free account, then top up µ: ${getWebsiteUrl()}/settings/billing`);
270
273
  }
271
274
  const active = getActiveOrg();
272
275
  const orgId = active?.org_id ?? (await getMyPersonalOrg()).org_id;
@@ -274,13 +277,13 @@ export async function ensureSessionAndBalance(opts) {
274
277
  await assertMuBalance(orgId, orgLabel);
275
278
  return { orgId, orgLabel };
276
279
  }
277
- // ── The default coding posture (nudge/nudge) ────────────────────────────────
280
+ // ── The default coding posture (cards observe-only; goal contracts nudge) ────
278
281
  function buildCodeAlignmentCard(agentId) {
279
282
  return {
280
283
  card_version: "unified/2026-04-26",
281
284
  agent_id: agentId,
282
- autonomy_mode: "nudge",
283
- integrity_mode: "nudge",
285
+ autonomy_mode: "observe",
286
+ integrity_mode: "observe",
284
287
  principal: { type: "agent", identifier: agentId, relationship: "delegated_authority" },
285
288
  values: {
286
289
  declared: ["transparency", "safety", "honesty", "user_control", "harm_prevention"],
@@ -317,7 +320,7 @@ function buildCodeProtectionCard(agentId) {
317
320
  return {
318
321
  card_version: "protection/2026-04-26",
319
322
  agent_id: agentId,
320
- mode: "nudge",
323
+ mode: "observe",
321
324
  thresholds: { warn: 0.6, quarantine: 0.8, block: 0.95 },
322
325
  screen_surfaces: {
323
326
  incoming: true,
@@ -100,7 +100,7 @@ export interface ResolvedCli {
100
100
  * a non-claude executable is launched through it with a warning.
101
101
  */
102
102
  export declare function resolveCliBin(cliOpt: string | undefined, env: NodeJS.ProcessEnv): ResolvedCli;
103
- export declare function commandExists(cmd: string): boolean;
103
+ export declare function promptForCliPath(cliOpt: string | undefined, env: NodeJS.ProcessEnv, origErr: Error | undefined): Promise<ResolvedCli>;
104
104
  /**
105
105
  * Does the resolved coding-agent CLI advertise the `--remote-control` startup
106
106
  * flag (Claude Code >= 2.1.269)? Determined by scanning its `--help`. Best-effort:
@@ -31,6 +31,7 @@ import { tmpdir, homedir } from "node:os";
31
31
  import { basename, join, dirname } from "node:path";
32
32
  import { fileURLToPath } from "node:url";
33
33
  import { MNEMOM_DIR } from "../lib/config.js";
34
+ import { configKeySpec, loadCodeConfig, writeCodeConfig } from "../lib/code-config.js";
34
35
  import { fmt } from "../lib/format.js";
35
36
  import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
36
37
  import { defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, MuInsufficientError, } from "./code-setup.js";
@@ -72,14 +73,25 @@ export async function codeCommand(scenario, options = {}) {
72
73
  const cellNote = isProdGateway(env) ? "us-2/prod" : `custom (${gatewayHost})`;
73
74
  const yolo = options.yolo !== false; // default ON
74
75
  const contextHint = options.contextHint !== false; // default ON
75
- // ── which CLI executable (name or full path) ──
76
- const cli = resolveCliBin(options.cli, env);
77
- // ── default launch shape: prefer Remote-Control-in-terminal (the good default
78
- // — one session steerable from this terminal AND claude.ai/the app) when the
79
- // machine can do it; otherwise fall back to terminal-only with a clear notice.
80
- // An explicit choice (flag / env / config launch=) is always respected. ──
81
- if (!shapeExplicit && shape === "terminal") {
82
- const rc = checkRcCapability(cli);
76
+ // ── which CLI executable (name or full path) ── resolve GRACEFULLY: if it's not
77
+ // found we don't crash with a technical error — we ask for the path at launch.
78
+ let cli;
79
+ let cliError;
80
+ try {
81
+ cli = resolveCliBin(options.cli, env);
82
+ }
83
+ catch (err) {
84
+ cliError = err instanceof Error ? err : new Error(String(err));
85
+ }
86
+ // Default launch shape: prefer Remote-Control-in-terminal (the good default —
87
+ // one session steerable from this terminal AND claude.ai/the app) when the
88
+ // machine can do it; otherwise terminal-only with a clear notice. An explicit
89
+ // choice (flag / env / config launch=) is always respected. Needs a resolved
90
+ // CLI to test RC capability, so it's applied once the CLI is known.
91
+ const applyAutoShape = (resolved) => {
92
+ if (shapeExplicit || shape !== "terminal")
93
+ return;
94
+ const rc = checkRcCapability(resolved);
83
95
  if (rc.ok) {
84
96
  shape = "remote-control";
85
97
  }
@@ -87,22 +99,35 @@ export async function codeCommand(scenario, options = {}) {
87
99
  say(`mnemom code: launching terminal-only (Remote Control unavailable: ${rc.missing.join("; ")}).`);
88
100
  say("mnemom code: once those are in place, `mnemom code <scenario> --remote-control` (or `config set launch remote-control`) gives you a session steerable from claude.ai and the Claude app too.");
89
101
  }
90
- }
102
+ };
103
+ if (cli)
104
+ applyAutoShape(cli);
91
105
  if (options.dryRun) {
92
- printPlan({ scenario, shape, agent, conversationId, door, cellNote, envelope, yolo, cli });
106
+ const planCli = cli ?? {
107
+ bin: "(not found — pass --cli <path> or `mnemom code config set cli <path>`)",
108
+ name: basename((options.cli || env.MNEMOM_CODE_CLI || "claude").trim()),
109
+ };
110
+ printPlan({ scenario, shape, agent, conversationId, door, cellNote, envelope, yolo, cli: planCli });
93
111
  return;
94
112
  }
113
+ // Real launch: make sure we actually have a CLI to run. If we couldn't find it,
114
+ // ask for the path (a good experience beats a hard technical error). A
115
+ // non-interactive session still gets the clear, actionable error.
116
+ if (!cli) {
117
+ cli = await promptForCliPath(options.cli, env, cliError);
118
+ applyAutoShape(cli);
119
+ }
95
120
  // ── resolve the Anthropic key (zero 1Password; never logged) ──
96
121
  const anthropicKey = await resolveAnthropicKey(env, {
97
122
  allowPrompt: true,
98
123
  keySource: options.keySource,
99
124
  });
100
- // ── account + Mnemom Units gate (no free tier) ──────────────────────────────
101
- // mnemom code runs governed and on Mnemom Units, so before we seal a contract
102
- // or launch we (1) ensure a Mnemom session — offering the try-me-style sign-in
103
- // / sign-up when there isn't one — and (2) enforce a positive MU balance on the
104
- // org the agent lives in (default: the user's personal org). No MUs ⇒ blocked
105
- // with a portal-style top-up message. Skipped only on --dry-run (handled above).
125
+ // ── account + µ-balance gate (no free tier) ─────────────────────────────────
126
+ // mnemom code runs governed and on µ, so before we seal a contract or launch we
127
+ // (1) ensure a Mnemom session — offering the try-me-style sign-in / sign-up when
128
+ // there isn't one — and (2) enforce a positive µ balance on the org the agent
129
+ // lives in (default: the user's personal org). No µ ⇒ blocked with a
130
+ // portal-style top-up message. Skipped only on --dry-run (handled above).
106
131
  try {
107
132
  await ensureSessionAndBalance({ interactive: isInteractive() });
108
133
  }
@@ -295,7 +320,52 @@ export function resolveCliBin(cliOpt, env) {
295
320
  (name === "claude" ? " and in the fnm/nvm/asdf node dirs" : "") +
296
321
  ".\n Pass --cli <name|/full/path> or set MNEMOM_CODE_CLI.");
297
322
  }
298
- export function commandExists(cmd) {
323
+ export /**
324
+ * Recover when the coding-agent CLI can't be found — a good experience beats a
325
+ * hard technical error. In an interactive session, tell the customer we couldn't
326
+ * find it and let them paste the full path (offering to save it as their default
327
+ * so they never retype it). A non-interactive session re-throws the clear,
328
+ * actionable error rather than hanging on a prompt.
329
+ */ async function promptForCliPath(cliOpt, env, origErr) {
330
+ const requested = (cliOpt || env.MNEMOM_CODE_CLI || "claude").trim();
331
+ if (!isInteractive()) {
332
+ throw origErr ?? new Error(`mnemom code: coding-agent CLI '${requested}' not found.`);
333
+ }
334
+ say(`mnemom code: couldn't find the '${requested}' CLI on your PATH.`);
335
+ if (requested === "claude") {
336
+ say(" If Claude Code is installed, paste its full path (find it with: which claude).");
337
+ say(" Not installed yet? Get it at https://claude.com/claude-code, then re-run.");
338
+ }
339
+ else {
340
+ say(" Paste its full path, or install it and re-run.");
341
+ }
342
+ for (let attempt = 0; attempt < 3; attempt++) {
343
+ const entered = (await askInput("Path to your coding-agent CLI (Enter to cancel):")).trim();
344
+ if (!entered) {
345
+ throw new Error("mnemom code: no coding-agent CLI available.\n" +
346
+ " Install Claude Code (https://claude.com/claude-code), or pass --cli <path> " +
347
+ "(or save it: mnemom code config set cli <path>).");
348
+ }
349
+ if (!existsSync(entered)) {
350
+ say(` '${entered}' doesn't exist — try again.`);
351
+ continue;
352
+ }
353
+ if (await askYesNo("Save this as your default CLI so you don't retype it?", true)) {
354
+ try {
355
+ const cfg = loadCodeConfig();
356
+ configKeySpec("cli").set(cfg, entered);
357
+ writeCodeConfig(cfg);
358
+ say(" Saved — future launches will use it.");
359
+ }
360
+ catch {
361
+ // best effort — the launch proceeds with the entered path either way
362
+ }
363
+ }
364
+ return { bin: entered, name: basename(entered) };
365
+ }
366
+ throw new Error("mnemom code: couldn't resolve a coding-agent CLI path — pass --cli <path>.");
367
+ }
368
+ function commandExists(cmd) {
299
369
  const res = spawnSync("sh", ["-c", `command -v "$1" >/dev/null 2>&1`, "sh", cmd], {
300
370
  stdio: "ignore",
301
371
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.0-next.0",
3
+ "version": "0.17.0-next.2",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {