@mnemom/mnemom 0.17.31 → 0.17.32-next.0

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.
Files changed (40) hide show
  1. package/dist/base-url-proxy.mjs +30 -0
  2. package/dist/commands/agent-fuel.d.ts +75 -0
  3. package/dist/commands/agent-fuel.js +298 -0
  4. package/dist/commands/agent-implicit-notice.d.ts +31 -0
  5. package/dist/commands/agent-implicit-notice.js +73 -0
  6. package/dist/commands/agent-session.d.ts +2 -1
  7. package/dist/commands/agent-session.js +2 -1
  8. package/dist/commands/agent.d.ts +52 -1
  9. package/dist/commands/agent.js +111 -26
  10. package/dist/commands/status.js +1 -1
  11. package/dist/commands/update.js +3 -1
  12. package/dist/commands/validate.d.ts +1 -1
  13. package/dist/commands/validate.js +1 -1
  14. package/dist/index.js +51 -12
  15. package/dist/lib/agent.d.ts +32 -19
  16. package/dist/lib/agent.js +34 -32
  17. package/dist/lib/api.d.ts +53 -0
  18. package/dist/lib/api.js +31 -10
  19. package/dist/lib/canonical-json.d.ts +12 -0
  20. package/dist/lib/canonical-json.js +22 -0
  21. package/dist/lib/conduct-receipt.d.ts +81 -0
  22. package/dist/lib/conduct-receipt.js +177 -0
  23. package/dist/lib/contract-hash.d.ts +61 -0
  24. package/dist/lib/contract-hash.js +126 -0
  25. package/dist/lib/experimental.d.ts +10 -1
  26. package/dist/lib/experimental.js +35 -1
  27. package/dist/lib/oauth.d.ts +2 -1
  28. package/dist/lib/oauth.js +2 -1
  29. package/dist/lib/statusline-bin.d.ts +2 -2
  30. package/dist/lib/statusline-bin.js +4 -7
  31. package/dist/lib/statusline-feed.d.ts +33 -0
  32. package/dist/lib/statusline-feed.js +68 -0
  33. package/dist/lib/statusline.d.ts +39 -88
  34. package/dist/lib/statusline.js +58 -250
  35. package/dist/lib/update/decide.d.ts +22 -3
  36. package/dist/lib/update/decide.js +35 -12
  37. package/dist/lib/update/doctor.js +5 -1
  38. package/dist/lib/update/run.d.ts +2 -0
  39. package/dist/lib/update/run.js +22 -8
  40. package/package.json +1 -1
@@ -31,7 +31,9 @@ import { randomUUID } from "node:crypto";
31
31
  import { tmpdir, homedir, hostname } from "node:os";
32
32
  import { basename, join, dirname } from "node:path";
33
33
  import { fileURLToPath } from "node:url";
34
- import { MEASURED_ENV, statuslineLaunch } from "../lib/statusline.js";
34
+ import { FEED_ENV, statuslineLaunch } from "../lib/statusline.js";
35
+ import { startFeed } from "../lib/statusline-feed.js";
36
+ import { isPrerelease } from "../lib/update/semver.js";
35
37
  import { ALLOW_AUTOCOMPACT_OFF_ENV, dropAutocompactOffEnv, hasOwnSettingsFlag, mergedSettingsArgs, planAutocompactGuard, } from "../lib/autocompact-guard.js";
36
38
  import { getApiUrl, getGatewayUrl, MNEMOM_DIR } from "../lib/config.js";
37
39
  import { createApiKey } from "../lib/api.js";
@@ -41,6 +43,7 @@ import { isLoggedIn } from "../lib/auth.js";
41
43
  import { loadCliConfig } from "../lib/cli-config.js";
42
44
  import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
43
45
  import { experimentalNotice } from "../lib/experimental.js";
46
+ import { conductReceiptFor } from "../lib/conduct-receipt.js";
44
47
  import { fmt } from "../lib/format.js";
45
48
  import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
46
49
  import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
@@ -169,7 +172,8 @@ export async function agentCommand(scenarioArg, options = {}) {
169
172
  liftPassthroughName(options.passthrough ?? []);
170
173
  // ── session name + goal ── (a bare `mnemom agent` in a terminal asks for the name;
171
174
  // goal alignment is resolved upstream: --goal seals a contract, implicit alignment
172
- // is on by default only while the `implicit` experimental feature is on.
175
+ // is on by default while the `implicit` experimental feature is on, and on a `next`
176
+ // build for every launch but `-p` (T16).
173
177
  const session = await resolveSessionSetup({
174
178
  scenario: scenarioArg,
175
179
  goal: options.goal,
@@ -729,6 +733,10 @@ export async function agentCommand(scenarioArg, options = {}) {
729
733
  }),
730
734
  knownCcSessionId: cc.id ?? continuedCcSessionId,
731
735
  plugin,
736
+ receipt: experimental.includes("record")
737
+ ? { orgId: account.orgId, agentId: servedAgentId }
738
+ : undefined,
739
+ orgId: account.orgId,
732
740
  });
733
741
  return; // launchTerminal exits the process
734
742
  }
@@ -1237,6 +1245,7 @@ function launchTerminal(ctx) {
1237
1245
  // A subscription launch (no Anthropic key) sets NO auth token: Claude Code then sends its
1238
1246
  // own claude.ai login to the gateway (lib/subscription-login.ts).
1239
1247
  let childEnv;
1248
+ let feed = null;
1240
1249
  if (ctx.anthropicKey) {
1241
1250
  childEnv = { ...process.env };
1242
1251
  delete childEnv.ANTHROPIC_API_KEY;
@@ -1262,10 +1271,8 @@ function launchTerminal(ctx) {
1262
1271
  applyGatewayHintHeaders(childEnv);
1263
1272
  if (ctx.options.slack)
1264
1273
  args.push(...slackChannelArgs(sessionName));
1265
- // The status-bar savings counter (lib/statusline.ts). Claude Code only; the
1266
- // user's own statusLine keeps running in front of it. Measured mode is this launch's
1267
- // decision alone: a value inherited from an outer `mnemom agent` session never rides along.
1268
- Reflect.deleteProperty(childEnv, MEASURED_ENV);
1274
+ // The status-bar segment (lib/statusline.ts). Claude Code only; the user's own
1275
+ // statusLine keeps running in front of it.
1269
1276
  if (ctx.cli.name === "claude") {
1270
1277
  const passthrough = ctx.options.passthrough ?? [];
1271
1278
  const sl = statuslineLaunch({
@@ -1274,10 +1281,17 @@ function launchTerminal(ctx) {
1274
1281
  cwd: process.cwd(),
1275
1282
  node: process.execPath,
1276
1283
  bin: join(dirname(fileURLToPath(import.meta.url)), "..", "lib", "statusline-bin.js"),
1277
- measured: ctx.usageSavings,
1278
1284
  });
1279
1285
  if (sl)
1280
1286
  Object.assign(childEnv, sl.env);
1287
+ feed = prepareStatuslineFeed({
1288
+ statusline: sl !== null,
1289
+ orgId: ctx.orgId,
1290
+ version: CLI_VERSION,
1291
+ goalMode: Boolean(ctx.goalMode),
1292
+ env: process.env,
1293
+ childEnv,
1294
+ });
1281
1295
  // Autocompact guard (lib/autocompact-guard.ts): one --settings carries both layers.
1282
1296
  const guard = applyAutocompactGuard(childEnv, ctx.options, !hasOwnSettingsFlag(passthrough));
1283
1297
  args.push(...mergedSettingsArgs(guard, sl?.settings ?? null));
@@ -1287,6 +1301,7 @@ function launchTerminal(ctx) {
1287
1301
  childEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
1288
1302
  childEnv.CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF = "1";
1289
1303
  }
1304
+ let recordStatePath;
1290
1305
  const run = () => {
1291
1306
  const child = spawn(ctx.cli.bin, args, { stdio: "inherit", env: childEnv });
1292
1307
  child.on("error", (err) => {
@@ -1296,33 +1311,84 @@ function launchTerminal(ctx) {
1296
1311
  child.on("exit", (code, signal) => {
1297
1312
  if (signal)
1298
1313
  process.kill(process.pid, signal);
1299
- else {
1300
- // Claude Code's own last line is "Resume this session with: claude --resume <id>",
1301
- // which reopens it OUTSIDE Mnemom (ungoverned, unbilled). Ours goes after it, last.
1302
- if (code === 0 && ctx.resumeHint && wantsResumeHint(ctx.cli, args)) {
1303
- const id = ctx.knownCcSessionId ?? newestCcSessionIn(process.cwd(), process.env);
1304
- say(ctx.resumeHint(id));
1305
- }
1306
- process.exit(code ?? 0);
1307
- }
1314
+ else
1315
+ void (async () => {
1316
+ // The conduct receipt never blocks exit (a hard deadline inside) and never changes
1317
+ // the exit code.
1318
+ if (ctx.receipt)
1319
+ await sayConductReceipt({ ...ctx.receipt, statePath: recordStatePath });
1320
+ // Claude Code's own last line is "Resume this session with: claude --resume <id>",
1321
+ // which reopens it OUTSIDE Mnemom (ungoverned, unbilled). Ours goes after it, last.
1322
+ if (code === 0 && ctx.resumeHint && wantsResumeHint(ctx.cli, args)) {
1323
+ const id = ctx.knownCcSessionId ?? newestCcSessionIn(process.cwd(), process.env);
1324
+ say(ctx.resumeHint(id));
1325
+ }
1326
+ process.exit(code ?? 0);
1327
+ })();
1308
1328
  });
1309
1329
  };
1310
1330
  // Implicit goal mode: put the local base-URL proxy in front of the door so each main request
1311
1331
  // carries x-mnemom-turn (where each recent user turn came from, read from Claude Code's own
1312
1332
  // transcript — base-url-proxy.mjs / turn-origin.mjs). The prompt id it joins on comes from the
1313
- // gateway hint headers set above. Fail-open: no proxy → the door directly, as before.
1314
- if (!ctx.goalMode || !turnProxyWanted(process.env)) {
1333
+ // gateway hint headers set above. Experimental `record` runs it too (turn header off unless
1334
+ // implicit), to capture the lineage key the conduct receipt reads. Fail-open: no proxy → the
1335
+ // door directly, as before, and a receipt that links instead of giving figures.
1336
+ const turnHeader = Boolean(ctx.goalMode) && turnProxyWanted(process.env);
1337
+ if (!turnHeader && !ctx.receipt) {
1315
1338
  run();
1316
1339
  return;
1317
1340
  }
1318
- startBaseUrlProxy(ctx.door, (baseUrl) => {
1319
- if (baseUrl) {
1320
- childEnv.ANTHROPIC_BASE_URL = baseUrl;
1321
- // An inherited HTTPS_PROXY must not carry the loopback hop.
1322
- exemptLoopbackFromProxy(childEnv);
1323
- }
1341
+ startBaseUrlProxy(ctx.door, (baseUrl, statePath) => {
1342
+ if (baseUrl)
1343
+ recordStatePath = statePath;
1344
+ onBaseUrlProxyReady(baseUrl, statePath, childEnv, feed && ctx.orgId ? { ...feed, orgId: ctx.orgId } : null);
1324
1345
  run();
1325
- });
1346
+ }, { turnHeader, recordState: wantsRecordState(Boolean(ctx.receipt), feed !== null) });
1347
+ }
1348
+ /**
1349
+ * Whether the proxy captures the gateway's record state: the conduct receipt reads it at exit,
1350
+ * and the fed status line reads its lineage key while the session runs.
1351
+ */
1352
+ export function wantsRecordState(receipt, fed) {
1353
+ return receipt || fed;
1354
+ }
1355
+ /**
1356
+ * Point Claude Code at a ready proxy and, on a fed launch, start the status-line feed on the
1357
+ * proxy's record state (no state file, no feed: the line stays "mnemom").
1358
+ */
1359
+ export function onBaseUrlProxyReady(baseUrl, recordStateFile, childEnv, feed, start = startFeed) {
1360
+ if (!baseUrl)
1361
+ return;
1362
+ childEnv.ANTHROPIC_BASE_URL = baseUrl;
1363
+ // An inherited HTTPS_PROXY must not carry the loopback hop.
1364
+ exemptLoopbackFromProxy(childEnv);
1365
+ if (feed && recordStateFile)
1366
+ start({ ...feed, recordStateFile });
1367
+ }
1368
+ /**
1369
+ * The status-line feed file for a fed launch (and FEED_ENV set on the child), or null: the
1370
+ * segment off, no billing org, or {@link fedStatuslineWanted} false.
1371
+ */
1372
+ export function prepareStatuslineFeed(o) {
1373
+ if (!o.statusline || !o.orgId || !fedStatuslineWanted(o.version, o.goalMode, o.env))
1374
+ return null;
1375
+ const feed = (o.files ?? fedStatuslineFiles)();
1376
+ o.childEnv[FEED_ENV] = feed.feedFile;
1377
+ return feed;
1378
+ }
1379
+ /**
1380
+ * Whether this launch feeds the status line measured model cost and μ (W1.4): prerelease
1381
+ * builds only (stable stays "mnemom", decision 14), and only when the turn proxy runs, since
1382
+ * the proxy is what learns the gateway's lineage key for the session.
1383
+ */
1384
+ export function fedStatuslineWanted(version, goalMode, env) {
1385
+ return isPrerelease(version) && goalMode && turnProxyWanted(env);
1386
+ }
1387
+ /** A private per-session dir holding the feed (launcher → bin). */
1388
+ function fedStatuslineFiles() {
1389
+ const dir = mkdtempSync(join(tmpdir(), "mnemom-statusline."));
1390
+ process.on("exit", () => rmSync(dir, { recursive: true, force: true }));
1391
+ return { feedFile: join(dir, "feed.json") };
1326
1392
  }
1327
1393
  /**
1328
1394
  * Add 127.0.0.1 and localhost to BOTH NO_PROXY and no_proxy (proxy clients disagree on which one
@@ -1334,6 +1400,20 @@ export function exemptLoopbackFromProxy(env) {
1334
1400
  env.NO_PROXY = [upper, "127.0.0.1,localhost"].filter(Boolean).join(",");
1335
1401
  env.no_proxy = [lower, "127.0.0.1,localhost"].filter(Boolean).join(",");
1336
1402
  }
1403
+ /**
1404
+ * Print the conduct receipt (experimental `record`) — the exit call site here, and the one a
1405
+ * durable park calls. Never throws: a failure is one warning line, and the caller's exit code
1406
+ * is untouched.
1407
+ */
1408
+ export async function sayConductReceipt(launch, trigger = "exit", receipt = conductReceiptFor) {
1409
+ try {
1410
+ for (const line of await receipt(launch, trigger))
1411
+ say(line);
1412
+ }
1413
+ catch (err) {
1414
+ say(fmt.warn(`mnemom agent: no conduct receipt (${err instanceof Error ? err.message : String(err)})`));
1415
+ }
1416
+ }
1337
1417
  /** Whether a terminal implicit launch runs the turn-origin proxy (MNEMOM_AGENT_TURN_HEADER=0 opts out). */
1338
1418
  export function turnProxyWanted(env) {
1339
1419
  const raw = (env.MNEMOM_AGENT_TURN_HEADER ?? "").trim().toLowerCase();
@@ -1350,6 +1430,9 @@ export function startBaseUrlProxy(door, done, opts = {}) {
1350
1430
  const script = opts.script ?? join(dirname(fileURLToPath(import.meta.url)), "..", "base-url-proxy.mjs");
1351
1431
  // The log carries method / path / status / latency / turn note only (never a header value or body).
1352
1432
  const logDir = mkdtempSync(join(tmpdir(), "mnemom-turn-proxy."));
1433
+ // The receipt's record state (lineage key, record URL: non-secret) lives beside the log and is
1434
+ // removed with it on exit, after the receipt has read it.
1435
+ const recordStatePath = opts.recordState ? join(logDir, "record.json") : undefined;
1353
1436
  const proxy = spawn(opts.node ?? process.execPath, [script], {
1354
1437
  env: {
1355
1438
  ...process.env,
@@ -1357,6 +1440,8 @@ export function startBaseUrlProxy(door, done, opts = {}) {
1357
1440
  MNEMOM_AGENT_BU_CWD: process.cwd(),
1358
1441
  MNEMOM_AGENT_BU_LOG: process.env.MNEMOM_AGENT_TURN_PROXY_LOG || join(logDir, "proxy.log"),
1359
1442
  MNEMOM_AGENT_BU_PARENT_PID: String(process.pid),
1443
+ ...(opts.turnHeader === false ? { MNEMOM_AGENT_TURN_HEADER: "0" } : {}),
1444
+ ...(recordStatePath ? { MNEMOM_AGENT_BU_RECORD_STATE: recordStatePath } : {}),
1360
1445
  },
1361
1446
  stdio: ["ignore", "pipe", "ignore"],
1362
1447
  });
@@ -1375,7 +1460,7 @@ export function startBaseUrlProxy(door, done, opts = {}) {
1375
1460
  if (!url) {
1376
1461
  say(fmt.warn(`mnemom agent: turn-origin proxy unavailable (${why}) — launching straight to the gateway; the gateway reads turn origins from the wire only.`));
1377
1462
  }
1378
- done(url);
1463
+ done(url, url ? recordStatePath : undefined);
1379
1464
  };
1380
1465
  const timeoutMs = opts.readyTimeoutMs ?? 5000;
1381
1466
  const timer = setTimeout(() => finish(null, `no READY within ${timeoutMs}ms`), timeoutMs);
@@ -138,7 +138,7 @@ async function checkApiConnectivity(agentId) {
138
138
  catch (error) {
139
139
  // 404 = agent not registered yet. Branch on effectiveStatus (enforce hook may
140
140
  // rewrite an undocumented 404 → synthetic 500 + spec_deviation.original_status;
141
- // effectiveStatus === status when documented). getAgent → fetchApi → MnemomApiError.
141
+ // effectiveStatus === status when documented). getAgent → readApiError → MnemomApiError.
142
142
  if (error instanceof MnemomApiError && error.effectiveStatus === 404) {
143
143
  return {
144
144
  name: "API",
@@ -43,7 +43,9 @@ export function formatUpdateResult(result, current) {
43
43
  lines.push(` Try manually: ${manual(action.version)}`);
44
44
  }
45
45
  else if (action.kind === "notify") {
46
- lines.push(`mnemom ${action.version} is available on ${result.channel} (you have ${current}).`);
46
+ lines.push(result.movedBack
47
+ ? `mnemom ${current} was withdrawn; ${result.channel} is back on ${action.version}.`
48
+ : `mnemom ${action.version} is available on ${result.channel} (you have ${current}).`);
47
49
  lines.push(` Run: ${canSelfUpdate(result.method ?? "unknown") ? "mnemom update" : manual(action.version)}`);
48
50
  }
49
51
  else {
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `mnemom validate ...` commands — Safe House Hardening T1-4 (CI gate
3
- * integration). Surface for engineers to check whether staging is safe
3
+ * integration). Surface for engineers to check whether the test cell is safe
4
4
  * to promote to production based on the most recent harness run state.
5
5
  *
6
6
  * mnemom validate safe-house [--against=test] [--max-age=24h]
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * `mnemom validate ...` commands — Safe House Hardening T1-4 (CI gate
3
- * integration). Surface for engineers to check whether staging is safe
3
+ * integration). Surface for engineers to check whether the test cell is safe
4
4
  * to promote to production based on the most recent harness run state.
5
5
  *
6
6
  * mnemom validate safe-house [--against=test] [--max-age=24h]
package/dist/index.js CHANGED
@@ -16,6 +16,7 @@ import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./comm
16
16
  import { agentExportCommand, liveExportDeps } from "./commands/agent-export.js";
17
17
  import { agentSessionsCommand, agentShowCommand, liveSessionDeps, } from "./commands/agent-sessions.js";
18
18
  import { agentGoalPinCommand, agentGoalResetCommand, agentGoalShowCommand, liveGoalDeps, } from "./commands/agent-goal.js";
19
+ import { agentFuelCommand, liveFuelDeps } from "./commands/agent-fuel.js";
19
20
  import { orgListCommand, orgShowCommand, orgUseCommand } from "./commands/org.js";
20
21
  import { teamListCommand, teamShowCommand, teamTemplateCommand, teamPreviewComposeCommand, teamAdminGrantCommand, teamAdminRevokeCommand, teamAdminListCommand, teamCoverageCommand, } from "./commands/team.js";
21
22
  import { advisoriesListCommand, advisoriesShowCommand } from "./commands/advisories.js";
@@ -33,11 +34,13 @@ import { agentDoctorCommand } from "./commands/agent-doctor.js";
33
34
  import { agentSetupCommand, parseGovernanceFlag } from "./commands/agent-setup.js";
34
35
  import { agentInviteCommand, runAgentInvocation } from "./commands/agent-invite.js";
35
36
  import { agentFirstRunCommand, offerFirstRunOnLaunch } from "./commands/agent-first-run.js";
37
+ import { showImplicitDefaultNoticeOnce } from "./commands/agent-implicit-notice.js";
38
+ import { isPrerelease } from "./lib/update/semver.js";
36
39
  import { applyConfigToEnv, launcherEntries, loadLauncherConfig, withoutDisabledExperimental, } from "./lib/launcher-config.js";
37
40
  import { loadExperimentalState } from "./lib/cli-config.js";
38
- import { assertEnabled, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
41
+ import { assertEnabled, availableFeatures, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
39
42
  import { experimentalDisableCommand, experimentalEnableCommand, experimentalListCommand, runExperimentalMigration, } from "./commands/experimental.js";
40
- import { parseCompactAt, parseContextProfile, resolveImplicit, selectLaunchAgent, splitSessionArg, } from "./lib/agent.js";
43
+ import { parseCompactAt, parseContextProfile, passthroughIsPrint, resolveImplicitSource, selectLaunchAgent, splitSessionArg, } from "./lib/agent.js";
41
44
  import { wrapCommand } from "./commands/wrap.js";
42
45
  import { onboardCommand } from "./commands/onboard.js";
43
46
  import { skillsListCommand, skillsDescribeCommand } from "./commands/skills.js";
@@ -114,11 +117,14 @@ program
114
117
  // ── experimental features (lib/experimental.ts) ── While a feature is off its flags and
115
118
  // commands are hidden from --help (decided once, at load) and using one fails with a
116
119
  // pointer to `mnemom experimental enable <id>` (decided per invocation, in the action).
117
- const experimentalAtLoad = loadExperimentalState().enabled;
120
+ // A prerelease (`next`) build also makes PRERELEASE_AVAILABLE usable (door one's T16).
121
+ const PRERELEASE_BUILD = isPrerelease(CLI_VERSION);
122
+ const loadAvailableFeatures = () => availableFeatures(loadExperimentalState().enabled, PRERELEASE_BUILD);
123
+ const experimentalAtLoad = loadAvailableFeatures();
118
124
  const hiddenUnless = (id) => !experimentalAtLoad.includes(id);
119
125
  /** preAction guard for a gated command: print the guidance and exit 2 while `id` is off. */
120
126
  function requireExperimental(what, id) {
121
- const message = gateCheck(what, id, loadExperimentalState().enabled);
127
+ const message = gateCheck(what, id, loadAvailableFeatures());
122
128
  if (message) {
123
129
  console.error(message);
124
130
  process.exit(2);
@@ -161,7 +167,7 @@ const agentCmd = program
161
167
  "platform cards (strictest wins). See `mnemom protection` for that mode.")
162
168
  .option("--lead-directed", "Seal the standard lead-directed requirement: own-branch push and draft/ready PR creation, " +
163
169
  "from a git worktree, on the lead's direction — not merging. Requires --goal; idempotent.")
164
- .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/--mode/--lead-directed; the guardrail ceilings still apply)").hideHelp(hiddenUnless("implicit")))
170
+ .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, and on a `next` build for every launch but `-p`; not combinable with --goal/--requirement/--allow/--forbid/--goal-id/--mode/--lead-directed; the guardrail ceilings still apply)").hideHelp(hiddenUnless("implicit")))
165
171
  .option("--no-implicit", "Turn implicit goal alignment OFF for this launch (overrides launcher.toml goal_mode = implicit)")
166
172
  .option("--conversation-id <id>", "x-mnemom-conversation-id (default: auto-generated)")
167
173
  .option("--cli <name|path>", "Coding-agent CLI to launch: a name or full path (default: claude)")
@@ -259,6 +265,8 @@ const agentCmd = program
259
265
  }
260
266
  async function launchAgent(scenario) {
261
267
  const experimental = loadExperimentalState().enabled;
268
+ // What a flag, key or command may use: the enabled features, plus implicit on `next`.
269
+ const available = availableFeatures(experimental, PRERELEASE_BUILD);
262
270
  // The one place the flag's raw string becomes `number | null` (see the argParser's
263
271
  // comment for why commander can't carry `null` through parseArg directly). Already
264
272
  // validated by the argParser, so this never throws.
@@ -266,7 +274,7 @@ const agentCmd = program
266
274
  // Sub-verb: `mnemom agent config …` manages persistent settings and never
267
275
  // launches. (A scenario literally named "config" is reserved for this.)
268
276
  if (scenario === "config") {
269
- await agentConfigCommand(passthrough, { enabled: experimental });
277
+ await agentConfigCommand(passthrough, { enabled: available });
270
278
  return;
271
279
  }
272
280
  // Sub-verb: `mnemom agent slack …` posts/receives Slack messages as this
@@ -286,7 +294,7 @@ const agentCmd = program
286
294
  used: opts.contextHandoff === true,
287
295
  },
288
296
  { what: "--implicit", id: "implicit", used: opts.implicit === true },
289
- ], experimental);
297
+ ], available);
290
298
  if (opts.gateway)
291
299
  process.env.MNEMOM_AGENT_GATEWAY = opts.gateway;
292
300
  // Persistent settings (~/.mnemom/launcher.toml) sit UNDER env and flags:
@@ -299,11 +307,11 @@ const agentCmd = program
299
307
  for (const line of ignoredSettingLines({
300
308
  entries: launcherEntries(rawCfg),
301
309
  env: process.env,
302
- enabled: experimental,
310
+ enabled: available,
303
311
  })) {
304
312
  console.error(line);
305
313
  }
306
- const cfg = withoutDisabledExperimental(rawCfg, experimental);
314
+ const cfg = withoutDisabledExperimental(rawCfg, available);
307
315
  applyConfigToEnv(cfg, process.env);
308
316
  // Sub-verb: `mnemom agent doctor` runs a read-only preflight and never
309
317
  // launches. Placed after config load so it reports the effective settings.
@@ -334,14 +342,22 @@ const agentCmd = program
334
342
  const numToStr = (n) => n === undefined ? undefined : String(n);
335
343
  // Implicit mode: --implicit / --no-implicit win; a --goal statement seals
336
344
  // (the standing config default never overrides it); else launcher.toml
337
- // `goal_mode`; else implicit while the `implicit` feature is on, off while it is
338
- // off. Pure + unit-tested in lib/agent.ts.
339
- const implicit = resolveImplicit({
345
+ // `goal_mode`; else implicit while the `implicit` feature is on; else, on a
346
+ // `next` build, implicit unless the launch is headless `-p` (T16); else off.
347
+ // Pure + unit-tested in lib/agent.ts.
348
+ const goalMode = resolveImplicitSource({
340
349
  goal: opts.goal,
341
350
  implicit: opts.implicit,
342
351
  configGoalMode: cfg.goalMode,
343
352
  experimentalOn: experimental.includes("implicit"),
353
+ prerelease: PRERELEASE_BUILD,
354
+ print: passthroughIsPrint(passthrough),
344
355
  });
356
+ const implicit = goalMode.implicit;
357
+ // The T16 default tells the user once, the first time it applies.
358
+ if (implicit && goalMode.source === "prerelease") {
359
+ showImplicitDefaultNoticeOnce({ dryRun: opts.dryRun });
360
+ }
345
361
  // Config guardrails apply only when goal alignment is actually intended (a
346
362
  // --goal is present, or implicit mode — where they ride on the mode header),
347
363
  // so a saved ceiling never forces an unrequested contract (which would
@@ -935,6 +951,29 @@ agentCmd
935
951
  process.exit(1);
936
952
  }
937
953
  });
954
+ // `mnemom agent fuel <file>` — the fuel step (Doors W3.2): one confirmation seals a batch of job
955
+ // contracts through the batch-seal endpoint and commits them to the repo.
956
+ agentCmd
957
+ .command("fuel <file>", { hidden: hiddenUnless("fuel") })
958
+ .description("EXPERIMENTAL (`mnemom experimental enable fuel`): seal a batch of job contracts with one confirmation. " +
959
+ '`file` is a JSON array of contracts (or `{ "contracts": [...] }`; `-` reads stdin); each is sealed, ' +
960
+ "then written to .mnemom/contracts/<sha256>.json and committed")
961
+ .option("--repo <owner/name>", "The repo the jobs run against (default: the GitHub origin remote)")
962
+ .option("--org <org_id>", "The org to seal under (default: your only organization)")
963
+ .option("--yes", "Seal without the confirmation prompt")
964
+ .option("--no-commit", "Seal only; do not write or commit the contract files")
965
+ .option("--json", "Output the batch-seal response, the files and the commit as JSON")
966
+ .option("--key <idempotency-key>", "Replay a batch already sealed with this key (printed when committing its files failed)")
967
+ .hook("preAction", () => requireExperimental("`mnemom agent fuel`", "fuel"))
968
+ .action(async (file, opts) => {
969
+ try {
970
+ await agentFuelCommand(file, opts, liveFuelDeps);
971
+ }
972
+ catch (error) {
973
+ console.error(error instanceof Error ? error.message : String(error));
974
+ process.exitCode = 1;
975
+ }
976
+ });
938
977
  // `mnemom agent goal show|pin|reset` — the implicit goal the gateway inferred for a session
939
978
  // (implicit-goal CLI). The handlers set process.exitCode themselves (1 / 2); the catch is for
940
979
  // the unexpected.
@@ -359,13 +359,7 @@ export declare function resolveConversationId(raw: string | undefined): string;
359
359
  export declare function isPositiveInt(value: unknown): boolean;
360
360
  /** A strictly-positive finite number (rejects 0, negatives, NaN, Infinity, non-numeric). */
361
361
  export declare function isPositiveNumber(value: unknown): boolean;
362
- /**
363
- * Canonical JSON — object keys sorted recursively, arrays in order, no
364
- * whitespace, `undefined` keys dropped. Byte-identical to the gateway's
365
- * `canonicalJson` (gateway/src/goal-contract.ts), so the base64 we send and the
366
- * sha we print match what the gateway seals.
367
- */
368
- export declare function canonicalJson(value: unknown): string;
362
+ export { canonicalJson } from "./canonical-json.js";
369
363
  /** sha256 hex (first 16 chars) of a string — the operator-eyeball contract hash. */
370
364
  export declare function sha256Hex16(text: string): string;
371
365
  /**
@@ -447,7 +441,8 @@ export declare const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
447
441
  /**
448
442
  * The gateway env key that has it measure each request's saving for a single session and put
449
443
  * it in the client-facing usage (`mnemom_savings` in the read `usage.iterations` entry), where
450
- * the status bar reads it from Claude Code's transcript. Default OFF on the gateway; sent `"on"`
444
+ * it lands in Claude Code's transcript. The status bar never shows it (decision 14: never
445
+ * "saved"). Default OFF on the gateway; sent `"on"`
451
446
  * only while the `measured-savings` experimental feature is on and the launched CLI is Claude
452
447
  * Code (`resolveUsageSavings`). Independent of folding, so every profile sends it — `off` too,
453
448
  * where the measured saving is honestly $0.00.
@@ -834,9 +829,9 @@ export declare function resolveCompactionIntercept(opts: {
834
829
  env?: NodeJS.ProcessEnv;
835
830
  }): boolean;
836
831
  /**
837
- * Does the gateway measure this launch's saving (`GATEWAY_USAGE_SAVINGS` in the header, and the
838
- * status bar in measured mode)? Only with the `measured-savings` experimental feature on, and
839
- * only for Claude Code — the one CLI whose status bar reads the measurement. No flag or
832
+ * Does the gateway measure this launch's saving (`GATEWAY_USAGE_SAVINGS` in the header)? Only
833
+ * with the `measured-savings` experimental feature on, and only for Claude Code, whose
834
+ * transcript keeps the measurement. No flag or
840
835
  * launcher.toml key: `mnemom experimental enable measured-savings` is the switch.
841
836
  */
842
837
  export declare function resolveUsageSavings(opts: {
@@ -921,29 +916,47 @@ export declare function buildHandoffMcpConfigJson(p: HandoffMcpParams): string;
921
916
  * per-session statement, so it is always a per-launch `--goal`.
922
917
  */
923
918
  export type GoalModeDefault = "implicit" | "off";
919
+ /**
920
+ * Where a launch's goal mode came from (`resolveImplicitSource`). `prerelease` is door
921
+ * one's T16 default (decisions 12 and 22): implicit by default on a `-next.N` build with
922
+ * nothing configured. It is the only source that shows the one-time notice.
923
+ */
924
+ export type ImplicitSource = "flag" | "goal" | "config" | "experimental" | "prerelease" | "default";
925
+ /** Does a Claude Code passthrough run headless (`-p` / `--print`)? */
926
+ export declare function passthroughIsPrint(passthrough: readonly string[]): boolean;
924
927
  /**
925
928
  * Is this launch implicit-mode? Precedence, highest wins:
926
929
  * 1. an explicit flag — `--implicit` (true) or `--no-implicit` (false);
927
930
  * 2. a `--goal` statement — a sealed contract is a per-launch choice that the
928
- * standing config default must never silently override;
931
+ * standing config default must never silently override, so a launch never
932
+ * runs both modes;
929
933
  * 3. the config default `goal_mode` — `"off"` turns alignment off, `"implicit"`
930
934
  * keeps it implicit;
931
- * 4. with the `implicit` experimental feature ON, IMPLICIT: the gateway infers
932
- * the goal from the user's own messages (turn it off for a launch with
933
- * `--no-implicit`, or standing with `goal_mode = "off"`); with it OFF (the
934
- * default), OFF.
935
+ * 4. with the `implicit` experimental feature ON, IMPLICIT (as before T16);
936
+ * 5. on a prerelease (`next`) build, IMPLICIT for an interactive or Remote Control
937
+ * launch and OFF for a headless `-p` one (OD-13: T16 never covers `-p`);
938
+ * 6. otherwise OFF — stable without the experimental feature behaves as today.
935
939
  * Pure so every arm is unit-tested; the caller passes the resolved value on to
936
940
  * `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
937
941
  * combined with a contract flag — that conflict is EXPLICIT and is reported).
938
942
  */
939
- export declare function resolveImplicit(opts: {
943
+ export declare function resolveImplicitSource(opts: {
940
944
  goal?: string;
941
945
  implicit?: boolean;
942
946
  configGoalMode?: GoalModeDefault;
943
947
  /**
944
948
  * Is the `implicit` experimental feature on? Off ⇒ the default is OFF and
945
949
  * `goal_mode = implicit` is ignored (`--implicit` is refused upstream, before
946
- * this runs). Omitted ⇒ on.
950
+ * this runs), unless this is a prerelease build. Omitted ⇒ on.
947
951
  */
948
952
  experimentalOn?: boolean;
949
- }): boolean;
953
+ /** Is this a prerelease (`-next.N`) CLI build? `isPrerelease(CLI_VERSION)`. Omitted ⇒ no. */
954
+ prerelease?: boolean;
955
+ /** Does the launch run Claude Code headless (`-p`)? `passthroughIsPrint`. */
956
+ print?: boolean;
957
+ }): {
958
+ implicit: boolean;
959
+ source: ImplicitSource;
960
+ };
961
+ /** `resolveImplicitSource(opts).implicit`. */
962
+ export declare function resolveImplicit(opts: Parameters<typeof resolveImplicitSource>[0]): boolean;
package/dist/lib/agent.js CHANGED
@@ -22,6 +22,7 @@
22
22
  import { createHash, randomUUID } from "node:crypto";
23
23
  import { readFileSync } from "node:fs";
24
24
  import { join } from "node:path";
25
+ import { canonicalJson } from "./canonical-json.js";
25
26
  import { getGatewayUrl } from "./config.js";
26
27
  import { normSession } from "./slack-route.js";
27
28
  /** The us-2/prod gateway host. The default door — never any other cell silently. */
@@ -438,6 +439,7 @@ export const AGENT_SUB_VERBS = new Set([
438
439
  "move",
439
440
  "export",
440
441
  "sessions",
442
+ "fuel",
441
443
  "show",
442
444
  "goal",
443
445
  ]);
@@ -480,23 +482,7 @@ export function isPositiveNumber(value) {
480
482
  const n = Number(s);
481
483
  return Number.isFinite(n) && n > 0;
482
484
  }
483
- /**
484
- * Canonical JSON — object keys sorted recursively, arrays in order, no
485
- * whitespace, `undefined` keys dropped. Byte-identical to the gateway's
486
- * `canonicalJson` (gateway/src/goal-contract.ts), so the base64 we send and the
487
- * sha we print match what the gateway seals.
488
- */
489
- export function canonicalJson(value) {
490
- if (value === null || typeof value !== "object")
491
- return JSON.stringify(value);
492
- if (Array.isArray(value))
493
- return `[${value.map(canonicalJson).join(",")}]`;
494
- const obj = value;
495
- const keys = Object.keys(obj)
496
- .filter((k) => obj[k] !== undefined)
497
- .sort();
498
- return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`).join(",")}}`;
499
- }
485
+ export { canonicalJson } from "./canonical-json.js";
500
486
  /** sha256 hex (first 16 chars) of a string — the operator-eyeball contract hash. */
501
487
  export function sha256Hex16(text) {
502
488
  return createHash("sha256").update(text, "utf8").digest("hex").slice(0, 16);
@@ -652,7 +638,8 @@ export const COMPACTION_INTERCEPT_KEY = "GATEWAY_COMPACTION_INTERCEPT";
652
638
  /**
653
639
  * The gateway env key that has it measure each request's saving for a single session and put
654
640
  * it in the client-facing usage (`mnemom_savings` in the read `usage.iterations` entry), where
655
- * the status bar reads it from Claude Code's transcript. Default OFF on the gateway; sent `"on"`
641
+ * it lands in Claude Code's transcript. The status bar never shows it (decision 14: never
642
+ * "saved"). Default OFF on the gateway; sent `"on"`
656
643
  * only while the `measured-savings` experimental feature is on and the launched CLI is Claude
657
644
  * Code (`resolveUsageSavings`). Independent of folding, so every profile sends it — `off` too,
658
645
  * where the measured saving is honestly $0.00.
@@ -1283,9 +1270,9 @@ export function resolveCompactionIntercept(opts) {
1283
1270
  return !(raw === "0" || raw === "false" || raw === "off" || raw === "no");
1284
1271
  }
1285
1272
  /**
1286
- * Does the gateway measure this launch's saving (`GATEWAY_USAGE_SAVINGS` in the header, and the
1287
- * status bar in measured mode)? Only with the `measured-savings` experimental feature on, and
1288
- * only for Claude Code — the one CLI whose status bar reads the measurement. No flag or
1273
+ * Does the gateway measure this launch's saving (`GATEWAY_USAGE_SAVINGS` in the header)? Only
1274
+ * with the `measured-savings` experimental feature on, and only for Claude Code, whose
1275
+ * transcript keeps the measurement. No flag or
1289
1276
  * launcher.toml key: `mnemom experimental enable measured-savings` is the switch.
1290
1277
  */
1291
1278
  export function resolveUsageSavings(opts) {
@@ -1333,27 +1320,42 @@ export function buildHandoffMcpConfig(p) {
1333
1320
  export function buildHandoffMcpConfigJson(p) {
1334
1321
  return JSON.stringify(buildHandoffMcpConfig(p));
1335
1322
  }
1323
+ /** Does a Claude Code passthrough run headless (`-p` / `--print`)? */
1324
+ export function passthroughIsPrint(passthrough) {
1325
+ return passthrough.some((a) => a === "-p" || a === "--print" || a.startsWith("--print="));
1326
+ }
1336
1327
  /**
1337
1328
  * Is this launch implicit-mode? Precedence, highest wins:
1338
1329
  * 1. an explicit flag — `--implicit` (true) or `--no-implicit` (false);
1339
1330
  * 2. a `--goal` statement — a sealed contract is a per-launch choice that the
1340
- * standing config default must never silently override;
1331
+ * standing config default must never silently override, so a launch never
1332
+ * runs both modes;
1341
1333
  * 3. the config default `goal_mode` — `"off"` turns alignment off, `"implicit"`
1342
1334
  * keeps it implicit;
1343
- * 4. with the `implicit` experimental feature ON, IMPLICIT: the gateway infers
1344
- * the goal from the user's own messages (turn it off for a launch with
1345
- * `--no-implicit`, or standing with `goal_mode = "off"`); with it OFF (the
1346
- * default), OFF.
1335
+ * 4. with the `implicit` experimental feature ON, IMPLICIT (as before T16);
1336
+ * 5. on a prerelease (`next`) build, IMPLICIT for an interactive or Remote Control
1337
+ * launch and OFF for a headless `-p` one (OD-13: T16 never covers `-p`);
1338
+ * 6. otherwise OFF — stable without the experimental feature behaves as today.
1347
1339
  * Pure so every arm is unit-tested; the caller passes the resolved value on to
1348
1340
  * `assembleContract` / `buildGoalModeHeader` (which still refuse `--implicit`
1349
1341
  * combined with a contract flag — that conflict is EXPLICIT and is reported).
1350
1342
  */
1351
- export function resolveImplicit(opts) {
1343
+ export function resolveImplicitSource(opts) {
1352
1344
  if (opts.implicit !== undefined)
1353
- return opts.implicit;
1345
+ return { implicit: opts.implicit, source: "flag" };
1354
1346
  if (hasText(opts.goal))
1355
- return false;
1356
- if (opts.experimentalOn === false)
1357
- return false;
1358
- return opts.configGoalMode !== "off";
1347
+ return { implicit: false, source: "goal" };
1348
+ const experimentalOn = opts.experimentalOn !== false;
1349
+ if (!experimentalOn && !opts.prerelease)
1350
+ return { implicit: false, source: "default" };
1351
+ if (opts.configGoalMode !== undefined) {
1352
+ return { implicit: opts.configGoalMode === "implicit", source: "config" };
1353
+ }
1354
+ if (experimentalOn)
1355
+ return { implicit: true, source: "experimental" };
1356
+ return { implicit: !opts.print, source: opts.print ? "default" : "prerelease" };
1357
+ }
1358
+ /** `resolveImplicitSource(opts).implicit`. */
1359
+ export function resolveImplicit(opts) {
1360
+ return resolveImplicitSource(opts).implicit;
1359
1361
  }