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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/commands/agent-run.d.ts +16 -6
  2. package/dist/commands/agent-run.js +52 -27
  3. package/dist/commands/agent-share-test-issuer.d.ts +67 -0
  4. package/dist/commands/agent-share-test-issuer.js +139 -0
  5. package/dist/commands/agent-share.d.ts +7 -1
  6. package/dist/commands/agent-share.js +14 -4
  7. package/dist/commands/agent.d.ts +7 -5
  8. package/dist/commands/agent.js +84 -45
  9. package/dist/commands/durable-lock.d.ts +8 -1
  10. package/dist/commands/durable-lock.js +9 -4
  11. package/dist/commands/scan.d.ts +12 -1
  12. package/dist/commands/scan.js +22 -3
  13. package/dist/commands/wrap.js +1 -1
  14. package/dist/durable/adopt-live.js +5 -8
  15. package/dist/durable/adopt.d.ts +8 -0
  16. package/dist/durable/adopt.js +27 -6
  17. package/dist/durable/cwd-rule.d.ts +9 -1
  18. package/dist/durable/cwd-rule.js +18 -0
  19. package/dist/durable/launch-spec.d.ts +2 -0
  20. package/dist/durable/launch-spec.js +1 -1
  21. package/dist/durable/test-mode.mjs +25 -1
  22. package/dist/host/client.d.ts +3 -1
  23. package/dist/host/daemon.d.ts +7 -3
  24. package/dist/host/daemon.js +13 -3
  25. package/dist/host/owner.d.ts +3 -0
  26. package/dist/host/owner.js +2 -0
  27. package/dist/host/run.d.ts +2 -3
  28. package/dist/host/run.js +5 -20
  29. package/dist/host/s1-names.d.ts +1 -0
  30. package/dist/host/s1-names.js +1 -0
  31. package/dist/host/verbs.d.ts +2 -0
  32. package/dist/host/verbs.js +11 -0
  33. package/dist/index.js +13 -9
  34. package/dist/lib/agent.d.ts +5 -3
  35. package/dist/lib/agent.js +6 -3
  36. package/dist/lib/claude-bin.d.ts +6 -0
  37. package/dist/lib/claude-bin.js +26 -0
  38. package/dist/lib/commit-report.d.ts +5 -4
  39. package/dist/lib/commit-report.js +6 -9
  40. package/dist/lib/config.d.ts +8 -1
  41. package/dist/lib/config.js +28 -0
  42. package/dist/lib/durable/launch.d.ts +28 -11
  43. package/dist/lib/durable/launch.js +33 -15
  44. package/dist/lib/durable/live.d.ts +15 -19
  45. package/dist/lib/durable/live.js +41 -43
  46. package/dist/lib/launcher-config.d.ts +6 -0
  47. package/dist/lib/launcher-config.js +11 -0
  48. package/dist/lib/runner/exec-append.d.ts +15 -0
  49. package/dist/lib/runner/exec-append.js +2 -0
  50. package/dist/lib/runner/executor-client.d.ts +2 -1
  51. package/dist/lib/runner/executor-state.d.ts +10 -0
  52. package/dist/lib/runner/executor-state.js +24 -0
  53. package/dist/lib/runner/github-base.d.ts +24 -0
  54. package/dist/lib/runner/github-base.js +27 -0
  55. package/dist/lib/runner/job.d.ts +11 -4
  56. package/dist/lib/runner/job.js +59 -16
  57. package/dist/lib/runner/oidc.d.ts +3 -2
  58. package/dist/lib/runner/oidc.js +7 -4
  59. package/dist/lib/runner/options.d.ts +6 -0
  60. package/dist/lib/runner/options.js +19 -1
  61. package/dist/lib/runner/outcome.js +2 -3
  62. package/dist/lib/runner/phases.d.ts +7 -4
  63. package/dist/lib/runner/phases.js +8 -4
  64. package/dist/lib/runner/proxy.d.ts +5 -1
  65. package/dist/lib/runner/proxy.js +7 -1
  66. package/dist/lib/scan/cost-per-pr.d.ts +93 -0
  67. package/dist/lib/scan/cost-per-pr.js +101 -0
  68. package/dist/lib/scan/record.d.ts +27 -1
  69. package/dist/lib/scan/record.js +114 -16
  70. package/dist/lib/scan/report-email.d.ts +25 -5
  71. package/dist/lib/scan/report-email.js +23 -2
  72. package/dist/lib/scan/scan.d.ts +3 -0
  73. package/dist/lib/scan/scan.js +13 -1
  74. package/dist/lib/session-key.d.ts +4 -3
  75. package/dist/vendor/durable-contract/index.js +207 -169
  76. package/dist/vendor/shared/durable-contract/doors.d.ts +26 -1
  77. package/dist/vendor/shared/durable-contract/doors.js +37 -1
  78. package/dist/vendor/shared/durable-contract/events.d.ts +1 -1
  79. package/dist/vendor/shared/durable-contract/events.js +13 -3
  80. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +2 -2
  81. package/dist/vendor/shared/durable-contract/schema.d.ts +5 -0
  82. package/dist/vendor/shared/durable-contract/schema.js +6 -0
  83. package/dist/vendor/shared/durable-contract/states.d.ts +2 -1
  84. package/dist/vendor/shared/durable-contract/states.js +2 -1
  85. package/package.json +1 -1
  86. package/dist/lib/durable/first-turn.d.ts +0 -50
  87. package/dist/lib/durable/first-turn.js +0 -102
@@ -270,7 +270,7 @@ export async function compareWithCommitted(built, committed) {
270
270
  export function launchSpecChangeFixLines(ctx) {
271
271
  const render = {
272
272
  launcherKey: ctx.launcherKey,
273
- nameHeld: true,
273
+ nameHeld: ctx.nameHeld ?? true,
274
274
  params: { name: ctx.name, flags: ctx.flags },
275
275
  };
276
276
  const lines = [renderFixLine("launch_spec_change", render)];
@@ -6,7 +6,8 @@
6
6
  // is a loopback literal; an unset URL counts as non-loopback. Otherwise a seam env is ignored.
7
7
  //
8
8
  // The CLI seams (closed list): T1 DURABLE_BOOT_ID_FILE, T2 DURABLE_HOST_TEST_PAUSE_AT, T3 the
9
- // credential proxy's http door on a loopback host.
9
+ // credential proxy's http door on a loopback host, and `agent run`'s GitHub base, read from the
10
+ // api's own T4 variable DURABLE_TEST_GITHUB_API_BASE (Durable lead, Doors W-L13).
10
11
 
11
12
  const TEST_MODE = "DURABLE_SESSIONS_TEST_MODE";
12
13
  /** S1 GUARD_LISTS.cli, in order. */
@@ -40,6 +41,29 @@ export function bootIdFileOverride(env = process.env) {
40
41
  return { file, ignored: false };
41
42
  }
42
43
 
44
+ /**
45
+ * `agent run`'s GitHub base on a lane: the same fake GitHub the api uses (T4,
46
+ * DURABLE_TEST_GITHUB_API_BASE), serving git and REST from one origin. Honoured only under the seam
47
+ * guard and only for an http(s) loopback URL with no credentials, query or fragment; anything else
48
+ * is null, and `ignored` says a value was set but not used (the caller warns).
49
+ */
50
+ export function testGithubBase(env = process.env) {
51
+ const raw = env.DURABLE_TEST_GITHUB_API_BASE;
52
+ if (typeof raw !== "string" || raw === "") return { base: null, ignored: false };
53
+ if (!testSeamsHonoured(env) || !isLoopbackUrl(raw)) return { base: null, ignored: true };
54
+ const u = new URL(raw);
55
+ if (
56
+ (u.protocol !== "http:" && u.protocol !== "https:") ||
57
+ u.username ||
58
+ u.password ||
59
+ u.search ||
60
+ u.hash
61
+ ) {
62
+ return { base: null, ignored: true };
63
+ }
64
+ return { base: `${u.origin}${u.pathname.replace(/\/+$/, "")}`, ignored: false };
65
+ }
66
+
43
67
  /** S1 T2_PAUSE_POINTS. */
44
68
  export const PAUSE_POINTS = Object.freeze([
45
69
  "after_claim_before_spawn",
@@ -1,4 +1,6 @@
1
1
  import { type ClaimOutcome, type ClaimUnavailableCause } from "../vendor/shared/durable-contract/codes.js";
2
+ import { type ExecAppendBody } from "./s1-names.js";
3
+ export type { ExecAppendBody };
2
4
  /** The S1 claim outcomes that grant the lease (CLAIM_OUTCOME_STATUS 200). */
3
5
  export type ClaimSuccess = Extract<ClaimOutcome, "acquired" | "reclaimed" | "epoch_bump">;
4
6
  /** The header that carries the executor key (ruling 33.28(1); S1 EXECUTOR_KEY_HEADER once frozen). */
@@ -64,7 +66,7 @@ export interface ExecutorApi {
64
66
  }>;
65
67
  attest(sessionId: string, nonce: string): Promise<HttpResult>;
66
68
  claim(sessionId: string, body: ClaimBody): Promise<HttpResult>;
67
- append(sessionId: string, body: Record<string, unknown>): Promise<HttpResult>;
69
+ append(sessionId: string, body: ExecAppendBody): Promise<HttpResult>;
68
70
  hello(sessionId: string, body: {
69
71
  epoch: number;
70
72
  nonce: string;
@@ -1,10 +1,9 @@
1
- import type { ClaimUnavailableCause } from "../vendor/shared/durable-contract/codes.js";
1
+ import type { ClaimUnavailableCause, CwdCheckCause } from "../vendor/shared/durable-contract/codes.js";
2
2
  import { type ExecutorApi, type SessionItem } from "./client.js";
3
3
  import { type SeatPane } from "./tmux.js";
4
4
  export declare const TICK_MS = 1000;
5
5
  export declare const ADOPT_RETRY_MS = 2000;
6
- export { SPAWNED_KIND } from "./s1-names.js";
7
- export declare const spawnIdempotencyKey: (sessionId: string, epoch: number, spawnId: string) => string;
6
+ export { SPAWNED_KIND, spawnIdempotencyKey } from "./s1-names.js";
8
7
  export type PausePoint = "after_claim_before_spawn" | "after_spawn_before_register" | "after_register_before_hello" | "after_first_batch";
9
8
  /** One seat this daemon holds (spawned or adopted). */
10
9
  export interface Seat {
@@ -71,6 +70,11 @@ export interface DaemonDeps {
71
70
  }>;
72
71
  /** Transcript guard: refresh for an item; returns the transcript path now on disk (or null). */
73
72
  readonly guard: (item: SessionItem) => string | null;
73
+ /**
74
+ * The spawn-time cwd check (C14-R04, mnemom-test #1002) of the session's committed cwd: null
75
+ * when it is a work tree's top level, else the S1 CWD_CHECK_CAUSES value.
76
+ */
77
+ readonly cwdCheck: (cwd: string | null) => CwdCheckCause | null;
74
78
  /** Before --resume: present, restored from the guard, or lost. */
75
79
  readonly ensureTranscript: (item: SessionItem) => {
76
80
  kind: "present" | "restored";
@@ -19,11 +19,10 @@ import { TERMINAL_STATES } from "../vendor/shared/durable-contract/states.js";
19
19
  import { IDLE_LOG } from "./gate.js";
20
20
  import { classifyClaim, errorCode, } from "./client.js";
21
21
  import { seatTmuxSession } from "./tmux.js";
22
- import { CODE, SEAT_UNKNOWN, SPAWNED_KIND } from "./s1-names.js";
22
+ import { CODE, SEAT_UNKNOWN, SPAWNED_KIND, spawnIdempotencyKey } from "./s1-names.js";
23
23
  export const TICK_MS = 1000;
24
24
  export const ADOPT_RETRY_MS = 2000;
25
- export { SPAWNED_KIND } from "./s1-names.js";
26
- export const spawnIdempotencyKey = (sessionId, epoch, spawnId) => `spawn:${sessionId}:${epoch}:${spawnId}`;
25
+ export { SPAWNED_KIND, spawnIdempotencyKey } from "./s1-names.js";
27
26
  /** S1 claim backoff: 2 s doubling to 60 s, scaled by 1 + jitter in [0, CLAIM_BACKOFF_JITTER) (R31). */
28
27
  export function backoffMs(count, random) {
29
28
  const base = Math.min(CLAIM_BACKOFF_MAX_S, CLAIM_BACKOFF_BASE_S * 2 ** Math.max(0, count - 1));
@@ -417,6 +416,11 @@ export class HostDaemon {
417
416
  const tokens = at.status === 200 && at.body && typeof at.body === "object"
418
417
  ? at.body.tokens
419
418
  : undefined;
419
+ // mnemom-test #1002 (lead ruling 20:00Z): the cwd check runs right before the claim, with no
420
+ // other work in between, and a failure rides on the claim as cwd_failure. The API suspends the
421
+ // session attestation_failed {check: cwd, cause} (after its own checks 2 and 7, before 12) and
422
+ // acquires nothing, so the claim answers ineligible and nothing is spawned.
423
+ const cwdFailure = this.d.cwdCheck(item.cwd);
420
424
  // mnemom-test #1001 (lead ruling, consistent with 33.28.5): a daemon with no local lease key
421
425
  // for the session asks for one on its first claim. It never holds one here: a session it
422
426
  // seats is not claimable, and the seat takes the key once. A bare claim and then a new_key
@@ -426,8 +430,14 @@ export class HostDaemon {
426
430
  nonce: this.d.nonce,
427
431
  ...(tokens !== undefined ? { tokens } : {}),
428
432
  new_key: true,
433
+ ...(cwdFailure !== null ? { cwd_failure: cwdFailure } : {}),
429
434
  });
430
435
  const c = classifyClaim(r, now);
436
+ // An API that did not apply the reported failure still never gets a seat in a bad cwd.
437
+ if (cwdFailure !== null && c.kind === "ok") {
438
+ did.push(`cwd_failure_unheeded:${item.id}:${cwdFailure}`);
439
+ return false;
440
+ }
431
441
  if (c.kind === "ok" && c.claim.leaseKey === null) {
432
442
  did.push(`claim_none:${item.id}:no_lease_key`);
433
443
  return false;
@@ -38,6 +38,9 @@ export interface EnrollStart {
38
38
  readonly expiresIn: number;
39
39
  readonly interval: number;
40
40
  readonly pollSecret: string;
41
+ /** The org the machine will join (mnemom-test #972); null when the api names none. */
42
+ readonly orgId: string | null;
43
+ readonly orgName: string | null;
41
44
  }
42
45
  export declare function parseEnrollStart(body: unknown): EnrollStart | null;
43
46
  export type PollOutcome = {
@@ -97,6 +97,8 @@ export function parseEnrollStart(body) {
97
97
  expiresIn: typeof o.expires_in === "number" ? o.expires_in : 900,
98
98
  interval: typeof o.interval === "number" && o.interval > 0 ? o.interval : 2,
99
99
  pollSecret: secret,
100
+ orgId: typeof o.org_id === "string" && o.org_id ? o.org_id : null,
101
+ orgName: typeof o.org_name === "string" && o.org_name ? o.org_name : null,
100
102
  };
101
103
  }
102
104
  export function classifyPoll(r) {
@@ -1,6 +1,7 @@
1
1
  import { type HostPaths, type HostInstance } from "./instance.js";
2
2
  import { type LoadedState } from "./state.js";
3
3
  import { type AutoContinueDeps } from "./auto-continue.js";
4
+ import { resolveClaudeBin } from "../lib/claude-bin.js";
4
5
  export interface RunIo {
5
6
  readonly env: Readonly<Record<string, string | undefined>>;
6
7
  readonly home: string;
@@ -8,8 +9,7 @@ export interface RunIo {
8
9
  }
9
10
  /** A tag that names this HOME on a shared tmux server (not secret: a hash of the path). */
10
11
  export declare const homeTag: (home: string) => string;
11
- /** The pinned claude: MNEMOM_AGENT_CLI when absolute, else `claude` on PATH, else null. */
12
- export declare function resolveClaudeBin(env: Readonly<Record<string, string | undefined>>): string | null;
12
+ export { resolveClaudeBin };
13
13
  /** The argv that runs this CLI again: node + the real entry script. */
14
14
  export declare function cliExec(): string[];
15
15
  type Startup = {
@@ -63,4 +63,3 @@ export declare function writeRuntimeOrLog(write: () => void, log: (line: string)
63
63
  * exit, since a normal exit would unlink that daemon's socket.
64
64
  */
65
65
  export declare function exitIfSocketLeft(released: "closed" | "left", exit: (code: number) => void): void;
66
- export {};
package/dist/host/run.js CHANGED
@@ -26,28 +26,11 @@ import { writeRuntime } from "./runtime.js";
26
26
  import { autoContinue, interruptedTurnId, turnContinuedJournal, } from "./auto-continue.js";
27
27
  import { hostTestPauseAt } from "../durable/test-mode.mjs";
28
28
  import { resolvePluginDir } from "../lib/claude-plugin.js";
29
+ import { liveCwdDeps, spawnCwdCause } from "../durable/cwd-rule.js";
30
+ import { resolveClaudeBin } from "../lib/claude-bin.js";
29
31
  /** A tag that names this HOME on a shared tmux server (not secret: a hash of the path). */
30
32
  export const homeTag = (home) => createHash("sha256").update(path.resolve(home)).digest("hex").slice(0, 16);
31
- /** The pinned claude: MNEMOM_AGENT_CLI when absolute, else `claude` on PATH, else null. */
32
- export function resolveClaudeBin(env) {
33
- const v = (env.MNEMOM_AGENT_CLI ?? "").trim();
34
- if (v.startsWith("/"))
35
- return v;
36
- const name = v || "claude";
37
- for (const dir of (env.PATH ?? "").split(":")) {
38
- if (!dir)
39
- continue;
40
- const p = path.join(dir, name);
41
- try {
42
- fs.accessSync(p, fs.constants.X_OK);
43
- return p;
44
- }
45
- catch {
46
- /* next */
47
- }
48
- }
49
- return null;
50
- }
33
+ export { resolveClaudeBin };
51
34
  /** The argv that runs this CLI again: node + the real entry script. */
52
35
  export function cliExec() {
53
36
  const entry = process.argv[1] ? fs.realpathSync(process.argv[1]) : "";
@@ -133,6 +116,7 @@ export async function hostRun(io) {
133
116
  const procs = defaultProcDeps();
134
117
  const tag = homeTag(io.home);
135
118
  const projects = ccProjectsDir(env, io.home);
119
+ const cwdDeps = liveCwdDeps(env);
136
120
  const sockDir = socketDir(paths.runDir, io.home);
137
121
  ensureSocketDir(sockDir);
138
122
  const control = controlSocketPath(sockDir);
@@ -207,6 +191,7 @@ export async function hostRun(io) {
207
191
  guardMode = g.mode;
208
192
  return orig;
209
193
  },
194
+ cwdCheck: (cwd) => spawnCwdCause(cwd, cwdDeps),
210
195
  ensureTranscript: (item) => ensureTranscript(io.home, item.id, projects, item.cwd, item.cc_session_id ?? "", lastKnown.get(item.id) ?? null),
211
196
  removeGuard: (id) => removeGuard(io.home, id),
212
197
  guardedIds: () => guardedSessionIds(io.home),
@@ -15,6 +15,7 @@ export declare const CODE: {
15
15
  export declare const SPAWNED_KIND: "executor.spawned";
16
16
  /** The executor.turn_continued kind: S1 EXECUTOR_WRITABLE_KINDS[2] (lead ruling 2026-10-28). */
17
17
  export declare const TURN_CONTINUED_KIND: "executor.turn_continued";
18
+ export { spawnIdempotencyKey, type ExecAppendBody } from "../lib/runner/exec-append.js";
18
19
  /**
19
20
  * The control socket's answer when no registered seat has the relayed spawn id. It is local to the
20
21
  * daemon and its seat proxy, never a wire code, so it is not a durable_* code (S1 owns those).
@@ -14,6 +14,7 @@ export const CODE = Object.freeze(Object.fromEntries(Object.keys(DURABLE_CODES).
14
14
  export const SPAWNED_KIND = EXECUTOR_WRITABLE_KINDS[0];
15
15
  /** The executor.turn_continued kind: S1 EXECUTOR_WRITABLE_KINDS[2] (lead ruling 2026-10-28). */
16
16
  export const TURN_CONTINUED_KIND = EXECUTOR_WRITABLE_KINDS[2];
17
+ export { spawnIdempotencyKey } from "../lib/runner/exec-append.js";
17
18
  /**
18
19
  * The control socket's answer when no registered seat has the relayed spawn id. It is local to the
19
20
  * daemon and its seat proxy, never a wire code, so it is not a durable_* code (S1 owns those).
@@ -38,6 +38,8 @@ export type Liveness = {
38
38
  export declare function executorLiveness(io: VerbIo, executorId: string): Promise<Liveness>;
39
39
  /** The S1 risk line (BACKBONE amendment 27): human text, or a JSON event in --json mode. Recorded nowhere. */
40
40
  export declare function printRiskLine(io: VerbIo): void;
41
+ /** "<org name> (<org_id>)", or the bare id when the api sent no name. */
42
+ export declare function joinTarget(orgId: string, orgName: string | null): string;
41
43
  /** The device step: start, show the code, poll, write the state file. Returns an exit code. */
42
44
  export declare function enrollDevice(io: VerbIo, opts?: {
43
45
  label?: string;
@@ -59,6 +59,10 @@ export function printRiskLine(io) {
59
59
  else
60
60
  io.out(RISK_LINE_TEXT);
61
61
  }
62
+ /** "<org name> (<org_id>)", or the bare id when the api sent no name. */
63
+ export function joinTarget(orgId, orgName) {
64
+ return orgName ? `${orgName} (${orgId})` : orgId;
65
+ }
62
66
  /** The device step: start, show the code, poll, write the state file. Returns an exit code. */
63
67
  export async function enrollDevice(io, opts = {}) {
64
68
  const { paths } = ctx(io);
@@ -85,6 +89,13 @@ export async function enrollDevice(io, opts = {}) {
85
89
  return 1;
86
90
  }
87
91
  printRiskLine(io);
92
+ // mnemom-test #972: name the org the machine will join before the device step. The human
93
+ // line is on stderr in every mode, so --json stdout stays JSON; --json adds one stdout event.
94
+ if (start.orgId) {
95
+ io.err(`This machine will join ${joinTarget(start.orgId, start.orgName)}`);
96
+ if (io.json)
97
+ io.out(JSON.stringify({ event: "enroll_org", org_id: start.orgId, org_name: start.orgName }));
98
+ }
88
99
  if (io.json)
89
100
  io.out(JSON.stringify({
90
101
  event: "enroll_started",
package/dist/index.js CHANGED
@@ -15,7 +15,7 @@ import { protectionShowCommand, protectionPublishCommand, protectionValidateComm
15
15
  import { agentsListCommand, agentsClaimCommand, agentsMoveCommand } from "./commands/agents.js";
16
16
  import { agentExportCommand, liveExportDeps } from "./commands/agent-export.js";
17
17
  import { agentShareCommand, liveAgentShareDeps } from "./commands/agent-share.js";
18
- import { agentShareFixtureEnabled, fixtureAgentShareParts, } from "./commands/agent-share-fixture.js";
18
+ import { selectAgentShareDeps } from "./commands/agent-share-test-issuer.js";
19
19
  import { agentSessionsCommand, agentShowCommand, liveSessionDeps, } from "./commands/agent-sessions.js";
20
20
  import { agentGoalPinCommand, agentGoalResetCommand, agentGoalShowCommand, liveGoalDeps, } from "./commands/agent-goal.js";
21
21
  import { agentFuelCommand, liveFuelDeps } from "./commands/agent-fuel.js";
@@ -37,7 +37,7 @@ import { agentDoctorCommand } from "./commands/agent-doctor.js";
37
37
  import { agentSetupCommand, parseGovernanceFlag } from "./commands/agent-setup.js";
38
38
  import { agentInviteCommand, runAgentInvocation } from "./commands/agent-invite.js";
39
39
  import { agentFirstRunCommand, offerFirstRunOnLaunch } from "./commands/agent-first-run.js";
40
- import { applyConfigToEnv, launcherEntries, loadLauncherConfig, withoutDisabledExperimental, } from "./lib/launcher-config.js";
40
+ import { applyConfigToEnv, durableLauncherKey, launcherEntries, loadLauncherConfig, withoutDisabledExperimental, } from "./lib/launcher-config.js";
41
41
  import { loadCliConfig, loadExperimentalState } from "./lib/cli-config.js";
42
42
  import { isSeatProxyArgv, runSeatProxyEntry } from "./durable/seat-proxy-entry.js";
43
43
  import { assertEnabled, EXPERIMENTAL_CONTRACT, gateCheck, ignoredSettingLines, reportCliError, } from "./lib/experimental.js";
@@ -355,6 +355,7 @@ const agentCmd = program
355
355
  await agentSetupCommand({
356
356
  ...selectLaunchAgent({
357
357
  flag: program.opts().agent,
358
+ env: process.env.MNEMOM_AGENT,
358
359
  configAgent: cfg.agent,
359
360
  configAgentId: cfg.agentId,
360
361
  }),
@@ -387,7 +388,8 @@ const agentCmd = program
387
388
  // Durable (gate on only, BACKBONE C1): refusals decided before any registry call or spawn
388
389
  // (nested launch, --server, the S1 name grammar); the registry plan runs in agentCommand.
389
390
  const durableOn = experimental.includes("durable");
390
- const durableKey = cfg.durable ?? "off";
391
+ // MNEMOM_AGENT_DURABLE wins over launcher.toml `durable` for this launch.
392
+ const durableKey = durableLauncherKey(process.env, cfg.durable);
391
393
  if (durableOn) {
392
394
  const refusal = durablePreflight({
393
395
  name: scenario,
@@ -413,9 +415,10 @@ const agentCmd = program
413
415
  maxTurns: opts.maxTurns ?? (contractIntended ? numToStr(cfg.guardrails?.maxTurns) : undefined),
414
416
  stall: opts.stall ?? (contractIntended ? numToStr(cfg.guardrails?.stallTurns) : undefined),
415
417
  mode: opts.mode,
416
- // --agent wins; else launcher.toml agent_id (a pin) over agent (a name).
418
+ // --agent wins, then MNEMOM_AGENT; else launcher.toml agent_id (a pin) over agent (a name).
417
419
  ...selectLaunchAgent({
418
420
  flag: parentOpts.agent,
421
+ env: process.env.MNEMOM_AGENT,
419
422
  configAgent: cfg.agent,
420
423
  configAgentId: cfg.agentId,
421
424
  }),
@@ -946,10 +949,11 @@ agentCmd
946
949
  .action(async (idOrName, opts) => {
947
950
  process.exitCode = await agentExportCommand(idOrName, opts, liveExportDeps);
948
951
  });
949
- // Hidden until MNEMOM_AGENT_SHARE=1 (MNE-9037): with the flag off the command behaves as unknown.
952
+ // Hidden from help; runnable only with MNEMOM_AGENT_SHARE=1 (MNE-9037) or when the CLI targets the
953
+ // test cell (MNEMOM_RING=test / test API + website): otherwise it behaves as unknown.
950
954
  agentCmd
951
955
  .command("share", { hidden: true })
952
- .description("Build a shareable card from lines of your signed statements and print the share-page link (card in the URL fragment; nothing is posted). Needs MNEMOM_AGENT_SHARE=1")
956
+ .description("Build a shareable card from lines of your signed statements and print the share-page link (card in the URL fragment; nothing is posted). Needs MNEMOM_AGENT_SHARE=1 (not needed on the test ring)")
953
957
  .option("--lines <ids>", "Comma-separated line ids to include (skips the picker)")
954
958
  .option("--period", "Period card: always include reverts (refused if none are available)")
955
959
  .option("--list", "List the lines available to share and exit")
@@ -957,9 +961,9 @@ agentCmd
957
961
  .action(async (opts) => {
958
962
  // MNEMOM_AGENT_SHARE_FIXTURE=1 (with MNEMOM_AGENT_SHARE=1) swaps in the test-only fixture
959
963
  // source/issuer; otherwise the not-yet-connected live deps are used, unchanged.
960
- const deps = agentShareFixtureEnabled()
961
- ? { ...liveAgentShareDeps, ...fixtureAgentShareParts }
962
- : liveAgentShareDeps;
964
+ // Against the TEST cell the test-key issuer is selected by the target, not a flag; the
965
+ // fixture flag is refused anywhere else (agent-share-test-issuer.ts).
966
+ const deps = selectAgentShareDeps(liveAgentShareDeps, process.env);
963
967
  process.exitCode = await agentShareCommand(opts, deps);
964
968
  });
965
969
  const agentSessionsCmd = agentCmd
@@ -171,13 +171,15 @@ export type AgentRef = {
171
171
  */
172
172
  export declare function parseAgentRef(raw: string): AgentRef;
173
173
  /**
174
- * Which agent a launch asks for, from the three places it can be named. `--agent` (flag) wins
174
+ * Which agent a launch asks for, from the four places it can be named. `--agent` (flag) wins
175
175
  * outright — an id there pins, a name there launches by name even when launcher.toml pins;
176
- * otherwise launcher.toml `agent_id` pins, and wins over launcher.toml `agent`. Returns the
177
- * `--agent`-shaped value (`agent`) and the config pin (`agentId`) for AgentOptions.
176
+ * then MNEMOM_AGENT (env), read exactly as the flag (the global `--agent`'s documented
177
+ * alternative); otherwise launcher.toml `agent_id` pins, and wins over launcher.toml `agent`.
178
+ * Returns the `--agent`-shaped value (`agent`) and the config pin (`agentId`) for AgentOptions.
178
179
  */
179
180
  export declare function selectLaunchAgent(sources: {
180
181
  flag?: string;
182
+ env?: string;
181
183
  configAgent?: string;
182
184
  configAgentId?: string;
183
185
  }): {
package/dist/lib/agent.js CHANGED
@@ -150,14 +150,17 @@ export function parseAgentRef(raw) {
150
150
  return { kind: "name", name: v };
151
151
  }
152
152
  /**
153
- * Which agent a launch asks for, from the three places it can be named. `--agent` (flag) wins
153
+ * Which agent a launch asks for, from the four places it can be named. `--agent` (flag) wins
154
154
  * outright — an id there pins, a name there launches by name even when launcher.toml pins;
155
- * otherwise launcher.toml `agent_id` pins, and wins over launcher.toml `agent`. Returns the
156
- * `--agent`-shaped value (`agent`) and the config pin (`agentId`) for AgentOptions.
155
+ * then MNEMOM_AGENT (env), read exactly as the flag (the global `--agent`'s documented
156
+ * alternative); otherwise launcher.toml `agent_id` pins, and wins over launcher.toml `agent`.
157
+ * Returns the `--agent`-shaped value (`agent`) and the config pin (`agentId`) for AgentOptions.
157
158
  */
158
159
  export function selectLaunchAgent(sources) {
159
160
  if (sources.flag?.trim())
160
161
  return { agent: sources.flag };
162
+ if (sources.env?.trim())
163
+ return { agent: sources.env };
161
164
  if (sources.configAgentId?.trim())
162
165
  return { agentId: sources.configAgentId };
163
166
  return sources.configAgent?.trim() ? { agent: sources.configAgent } : {};
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Which claude to run, shared by the host (seats) and `agent run` (served phases) so both pick
3
+ * the same binary. Kept apart from host/run.ts so the runner does not load the host daemon.
4
+ */
5
+ /** The pinned claude: MNEMOM_AGENT_CLI when absolute, else `claude` on PATH, else null. */
6
+ export declare function resolveClaudeBin(env: Readonly<Record<string, string | undefined>>): string | null;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Which claude to run, shared by the host (seats) and `agent run` (served phases) so both pick
3
+ * the same binary. Kept apart from host/run.ts so the runner does not load the host daemon.
4
+ */
5
+ import * as fs from "node:fs";
6
+ import * as path from "node:path";
7
+ /** The pinned claude: MNEMOM_AGENT_CLI when absolute, else `claude` on PATH, else null. */
8
+ export function resolveClaudeBin(env) {
9
+ const v = (env.MNEMOM_AGENT_CLI ?? "").trim();
10
+ if (v.startsWith("/"))
11
+ return v;
12
+ const name = v || "claude";
13
+ for (const dir of (env.PATH ?? "").split(":")) {
14
+ if (!dir)
15
+ continue;
16
+ const p = path.join(dir, name);
17
+ try {
18
+ fs.accessSync(p, fs.constants.X_OK);
19
+ return p;
20
+ }
21
+ catch {
22
+ /* next */
23
+ }
24
+ }
25
+ return null;
26
+ }
@@ -19,8 +19,9 @@
19
19
  * reported, trailer or not: a commit is bound when the index holds its sha or
20
20
  * patch-id, never by its trailer alone.
21
21
  *
22
- * PRERELEASE DEFAULT. On for a prerelease build (`next`), off for stable;
23
- * MNEMOM_AGENT_COMMIT_REPORT=on|off overrides, and any other value reads off.
22
+ * ON BY DEFAULT, stable and prerelease alike, so a PR from any `mnemom agent`
23
+ * session gets its commits bound in the record. MNEMOM_AGENT_COMMIT_REPORT=off
24
+ * (or 0, false, no) turns it off; any other value reads on.
24
25
  *
25
26
  * FAIL OPEN. Nothing here throws into the launch or changes its exit code. A
26
27
  * dark route (404) stops reporting for the session; a failed POST is retried on
@@ -36,8 +37,8 @@ export declare const CC_DEFAULT_COMMIT_ATTRIBUTION = "Co-Authored-By: Claude <no
36
37
  export declare const COMMIT_POLL_MS = 5000;
37
38
  /** The final sweep at exit shares W1.3's 2 s exit budget with the conduct receipt. */
38
39
  export declare const COMMIT_FINAL_DEADLINE_MS = 1500;
39
- /** on | off | unset (prerelease default); any other value reads off. */
40
- export declare function commitReportEnabled(raw: string | undefined, version: string): boolean;
40
+ /** On unless MNEMOM_AGENT_COMMIT_REPORT says off (off, 0, false, no). */
41
+ export declare function commitReportEnabled(raw: string | undefined): boolean;
41
42
  /**
42
43
  * The `--settings` fragment carrying the trailer. `user` is the commit attribution
43
44
  * the user's settings files give (highest precedence first; `false` hides all
@@ -19,8 +19,9 @@
19
19
  * reported, trailer or not: a commit is bound when the index holds its sha or
20
20
  * patch-id, never by its trailer alone.
21
21
  *
22
- * PRERELEASE DEFAULT. On for a prerelease build (`next`), off for stable;
23
- * MNEMOM_AGENT_COMMIT_REPORT=on|off overrides, and any other value reads off.
22
+ * ON BY DEFAULT, stable and prerelease alike, so a PR from any `mnemom agent`
23
+ * session gets its commits bound in the record. MNEMOM_AGENT_COMMIT_REPORT=off
24
+ * (or 0, false, no) turns it off; any other value reads on.
24
25
  *
25
26
  * FAIL OPEN. Nothing here throws into the launch or changes its exit code. A
26
27
  * dark route (404) stops reporting for the session; a failed POST is retried on
@@ -31,7 +32,6 @@
31
32
  import { spawnSync } from "node:child_process";
32
33
  import { postRecordCommit } from "./api.js";
33
34
  import { readLineage } from "./statusline-feed.js";
34
- import { isPrerelease } from "./update/semver.js";
35
35
  export const COMMIT_REPORT_ENV = "MNEMOM_AGENT_COMMIT_REPORT";
36
36
  export const TRAILER_KEY = "Mnemom-Conversation";
37
37
  /** Claude Code's own commit attribution when none is configured (settings reference, `attribution`). */
@@ -43,12 +43,9 @@ export const COMMIT_FINAL_DEADLINE_MS = 1_500;
43
43
  const HEX40 = /^[0-9a-f]{40}$/;
44
44
  const REPO_SLUG = /^[A-Za-z0-9_.-]{1,100}\/[A-Za-z0-9_.-]{1,100}$/;
45
45
  const BRANCH_MAX = 255;
46
- /** on | off | unset (prerelease default); any other value reads off. */
47
- export function commitReportEnabled(raw, version) {
48
- const v = (raw ?? "").trim().toLowerCase();
49
- if (v === "")
50
- return isPrerelease(version);
51
- return ["on", "1", "true", "yes"].includes(v);
46
+ /** On unless MNEMOM_AGENT_COMMIT_REPORT says off (off, 0, false, no). */
47
+ export function commitReportEnabled(raw) {
48
+ return !["off", "0", "false", "no"].includes((raw ?? "").trim().toLowerCase());
52
49
  }
53
50
  /**
54
51
  * The `--settings` fragment carrying the trailer. `user` is the commit attribution
@@ -30,7 +30,7 @@ export declare class RetiredEnvironmentError extends Error {
30
30
  * Defaults to production. `staging` throws (see RetiredEnvironmentError).
31
31
  */
32
32
  export declare function getEnvironment(): Environment;
33
- export type CellRing = "us-1";
33
+ export type CellRing = "us-1" | "test";
34
34
  /** Explicitly select a ring for this process (clears on undefined). Wins over MNEMOM_RING. */
35
35
  export declare function setRing(ring: CellRing | undefined): void;
36
36
  /**
@@ -45,3 +45,10 @@ export declare function setApiUrlOverride(url: string | undefined): void;
45
45
  export declare function getApiUrl(): string;
46
46
  export declare function getGatewayUrl(): string;
47
47
  export declare function getWebsiteUrl(): string;
48
+ /**
49
+ * Website base for links the CLI PRINTS (e.g. `agent share`). Same as getWebsiteUrl(), except
50
+ * when the CLI targets the test cell through its API alone (MNEMOM_API_URL / `--api`) and no
51
+ * website was chosen explicitly: then the link points at test.mnemom.ai, never www. An explicit
52
+ * MNEMOM_WEBSITE_URL or ring always wins; prod output is unchanged.
53
+ */
54
+ export declare function getShareWebsiteUrl(): string;
@@ -60,6 +60,13 @@ const RING_URLS = {
60
60
  gateway: "https://gateway-us1.mnemom.ai",
61
61
  website: "https://preview.mnemom.ai",
62
62
  },
63
+ // The durable Azure TEST cell (tools/gateway-tester/README.md: api-az-test / gateway-az-test;
64
+ // test.mnemom.ai serves the website). Never a prod host.
65
+ test: {
66
+ api: "https://api-az-test.mnemom.ai",
67
+ gateway: "https://gateway-az-test.mnemom.ai",
68
+ website: "https://test.mnemom.ai",
69
+ },
63
70
  };
64
71
  function isCellRing(value) {
65
72
  return Object.prototype.hasOwnProperty.call(RING_URLS, value);
@@ -115,3 +122,24 @@ export function getGatewayUrl() {
115
122
  export function getWebsiteUrl() {
116
123
  return envBase("MNEMOM_WEBSITE_URL") ?? ringUrls()?.website ?? WEBSITE_URLS[getEnvironment()];
117
124
  }
125
+ const TEST_CELL_API_HOST = "api-az-test.mnemom.ai";
126
+ /**
127
+ * Website base for links the CLI PRINTS (e.g. `agent share`). Same as getWebsiteUrl(), except
128
+ * when the CLI targets the test cell through its API alone (MNEMOM_API_URL / `--api`) and no
129
+ * website was chosen explicitly: then the link points at test.mnemom.ai, never www. An explicit
130
+ * MNEMOM_WEBSITE_URL or ring always wins; prod output is unchanged.
131
+ */
132
+ export function getShareWebsiteUrl() {
133
+ const website = getWebsiteUrl();
134
+ if (envBase("MNEMOM_WEBSITE_URL") || getRing())
135
+ return website;
136
+ try {
137
+ if (new URL(getApiUrl()).hostname.toLowerCase() === TEST_CELL_API_HOST) {
138
+ return RING_URLS.test.website;
139
+ }
140
+ }
141
+ catch {
142
+ /* unparseable API base: keep the default */
143
+ }
144
+ return website;
145
+ }
@@ -130,25 +130,42 @@ export declare function preReadRegistry(d: RegistryReads, name: string): Promise
130
130
  export declare function explicitCidRefusal(rows: RegistryRow[], explicitCid: string | null, name: string): DurableRefusal | null;
131
131
  export declare function planDurableLaunch(i: PlanInput, deps: PlanDeps): Promise<DurableLaunchPlan>;
132
132
  /**
133
- * What a launcher does after its lock attempt refused: a durable-only agent gets the
134
- * durable_only_agent refusal and exit 1, never a tracked spawn (C02-R24, final F02); any other
135
- * agent prints the lock refusal and its fix, then spawns tracked (exit null).
133
+ * The one stderr line a gate-on launch prints before its tracked Claude Code spawn when stdin is
134
+ * not a TTY and the passthrough has no -p/--print: Claude Code started that way waits forever and
135
+ * draws nothing. The spawn itself stays as today (C02-R23 a, b, f fallback); gate off prints
136
+ * nothing (C17-R11). No command line in it, so no placeholder (C11-R27). Null: print nothing.
137
+ */
138
+ export declare function trackedNoTtyLine(i: {
139
+ name: string;
140
+ gateOn: boolean;
141
+ stdinIsTTY: boolean;
142
+ cliName: string;
143
+ passthrough: readonly string[];
144
+ }): string | null;
145
+ /**
146
+ * What a launcher does after its lock attempt refused: it exits non-zero with the refusal's own fix
147
+ * line and never falls back to an attended (tracked) spawn, for every agent (Durable lead ruling
148
+ * 2026-10-30 on C02-R23 c, e). A durable-only agent gets the durable_only_agent refusal (C02-R24,
149
+ * final F02); any other agent prints the lock refusal itself.
136
150
  */
137
151
  export declare function afterLockRefusal(refusal: DurableRefusal, durableOnly: boolean, launcherKey: LauncherKey, name: string): {
138
152
  print: DurableRefusal;
139
- exit: number | null;
153
+ exit: number;
140
154
  };
141
155
  /**
142
- * A lock on a NEW Claude Code session (no transcript yet) waits for the session's first turn: the
143
- * seat resumes the transcript, so a lock before it exists fails transcript_lost. The launcher
144
- * spawns claude attended and locks once that turn has completed (lib/durable/first-turn.ts). A
145
- * continued session already has its transcript and locks instead of spawning, as before.
156
+ * A lock on a NEW Claude Code session (no transcript yet) is a create-and-lock (ruling A, which
157
+ * widens B5a to every agent): the session is born locked and its seat starts the transcript, so no
158
+ * attended claude ever runs first. A continued session already has its transcript and locks its
159
+ * registered row instead of spawning, as before.
146
160
  */
147
- export declare function locksAfterFirstTurn(plan: Extract<DurableLaunchPlan, {
161
+ export declare function locksAtCreate(plan: Extract<DurableLaunchPlan, {
148
162
  kind: "spawn";
149
163
  }>): boolean;
150
- /** The row the first-turn lock commits: the registered row, naming the CC session this launch pins. */
151
- export declare function firstTurnRow(row: RegistryRow, plan: Extract<DurableLaunchPlan, {
164
+ /**
165
+ * The registered row of a tracked launch, naming the CC session this launch pins: what a later lock
166
+ * from another terminal hands over (C02-R26).
167
+ */
168
+ export declare function launchRow(row: RegistryRow, plan: Extract<DurableLaunchPlan, {
152
169
  kind: "spawn";
153
170
  }>): RegistryRow;
154
171
  /** The create body the CLI sends (A2 owns the route; see notes-c1.md). */