@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
@@ -21,6 +21,8 @@
21
21
  // MNEMOM_AGENT_BU_PARENT_PID exit when this pid is gone
22
22
  // MNEMOM_AGENT_BU_PORT listen port (default 0 = ephemeral)
23
23
  // MNEMOM_AGENT_TURN_HEADER 0/off disables the turn header (the proxy still forwards)
24
+ // MNEMOM_AGENT_BU_RECORD_STATE write the gateway's x-mnemom-lineage-key and x-mnemom-record-url
25
+ // (both non-secret) here as JSON, for the conduct receipt (optional)
24
26
  // CLAUDE_CONFIG_DIR Claude Code's home (default ~/.claude)
25
27
  //
26
28
  // stdout: exactly one line, `READY <port>`, once listening. Node >= 18, no deps.
@@ -45,6 +47,7 @@ if (DOOR.protocol !== "https:") die("MNEMOM_AGENT_BU_DOOR must be https");
45
47
  const LOGFILE = env.MNEMOM_AGENT_BU_LOG || "";
46
48
  const PARENT = Number(env.MNEMOM_AGENT_BU_PARENT_PID || 0);
47
49
  const PORT = Number(env.MNEMOM_AGENT_BU_PORT || 0);
50
+ const RECORD_STATE = env.MNEMOM_AGENT_BU_RECORD_STATE || "";
48
51
  const TURNS = turnHeaderEnabled(env)
49
52
  ? new TurnOrigins({ cwd: env.MNEMOM_AGENT_BU_CWD || process.cwd() })
50
53
  : null;
@@ -72,6 +75,32 @@ const HOP = new Set([
72
75
  "trailer",
73
76
  "transfer-encoding",
74
77
  ]);
78
+ const LINEAGE_KEY_RE = /^[A-Za-z0-9:._-]{1,256}$/;
79
+ let recorded = "";
80
+ // The conduct receipt's inputs, from the gateway's own response headers: the newest well-formed
81
+ // lineage key and https record URL. Written whole (tmp + rename), only when they change. Never a
82
+ // request header, never a body. A write failure only means no receipt figures.
83
+ function recordState(h) {
84
+ if (!RECORD_STATE) return;
85
+ const key = typeof h["x-mnemom-lineage-key"] === "string" ? h["x-mnemom-lineage-key"] : "";
86
+ const url = typeof h["x-mnemom-record-url"] === "string" ? h["x-mnemom-record-url"] : "";
87
+ // A response carrying one header keeps the other one an earlier response gave.
88
+ const state = recorded ? JSON.parse(recorded) : {};
89
+ if (LINEAGE_KEY_RE.test(key)) state.lineage_key = key;
90
+ // Printable ASCII only: the URL is printed to the user's terminal, so no escape bytes.
91
+ if (url.length <= 2048 && /^https:\/\/[\x21-\x7e]+$/.test(url)) state.record_url = url;
92
+ if (!state.lineage_key && !state.record_url) return;
93
+ const next = JSON.stringify(state);
94
+ if (next === recorded) return;
95
+ try {
96
+ fs.writeFileSync(`${RECORD_STATE}.tmp`, next, { mode: 0o600 });
97
+ fs.renameSync(`${RECORD_STATE}.tmp`, RECORD_STATE);
98
+ recorded = next;
99
+ } catch (e) {
100
+ log(`record state not written: ${e.message}`);
101
+ }
102
+ }
103
+
75
104
  const agent = new https.Agent({ keepAlive: true, maxSockets: 32 });
76
105
  const doorPath = DOOR.pathname.replace(/\/$/, "");
77
106
  const isMessages = (u) => u === "/v1/messages" || u.startsWith("/v1/messages?");
@@ -95,6 +124,7 @@ function handle(req, res) {
95
124
  const forward = (note) => {
96
125
  const target = new URL(DOOR.origin + doorPath + req.url);
97
126
  const up = https.request(target, { method: req.method, headers, agent }, (ur) => {
127
+ recordState(ur.headers);
98
128
  res.writeHead(ur.statusCode, ur.headers);
99
129
  ur.pipe(res);
100
130
  ur.on("end", () => log(`${tag} -> ${ur.statusCode} ${Date.now() - started}ms${note}`));
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `mnemom agent fuel <file>` — the fuel step's CLI verb (Doors W3.2, decision 17).
3
+ *
4
+ * One confirmation seals N job contracts: the verb reads the contracts the lead drafted, checks and
5
+ * hashes each one exactly as the gateway would (`lib/contract-hash.ts`), shows them, asks once, and
6
+ * sends the whole batch to the batch-seal endpoint, which writes N seals in one api transaction.
7
+ * It then writes each sealed contract to `.mnemom/contracts/<sha256>.json` in the repo and commits
8
+ * only those files, so the contract text is in the repo the jobs run against.
9
+ *
10
+ * Every arm is unit-tested through `FuelDeps`; `liveFuelDeps` is the thin driver.
11
+ */
12
+ import { type BatchSealRequest, type BatchSealResponse, type OrgListItem } from "../lib/api.js";
13
+ import { type Contract } from "../lib/contract-hash.js";
14
+ export interface FuelOptions {
15
+ repo?: string;
16
+ org?: string;
17
+ yes?: boolean;
18
+ commit?: boolean;
19
+ json?: boolean;
20
+ /** Replay a batch already sealed with this Idempotency-Key (printed when a seal's local step fails). */
21
+ key?: string;
22
+ }
23
+ export interface FuelDeps {
24
+ readInput(path: string): string;
25
+ listOrgs(): Promise<OrgListItem[]>;
26
+ seal(orgId: string, body: BatchSealRequest, idempotencyKey: string, githubToken: string): Promise<BatchSealResponse>;
27
+ /** The caller's own GitHub token (GH_TOKEN, GITHUB_TOKEN, else `gh auth token`), or null. */
28
+ githubToken(): string | null;
29
+ newKey(): string;
30
+ /** Ask one yes/no question; resolves false when there is no terminal to ask on. */
31
+ confirm(question: string): Promise<boolean>;
32
+ isInteractive(): boolean;
33
+ /** The repo's top-level directory, or null outside a git work tree. */
34
+ gitRoot(): string | null;
35
+ /** `owner/name` of the `origin` remote, or null. */
36
+ originRepo(): string | null;
37
+ writeFile(path: string, text: string): void;
38
+ /**
39
+ * Stage and commit exactly `paths` (pathspec-limited); returns the new commit sha, or null when
40
+ * the files were already committed unchanged (a replayed batch).
41
+ */
42
+ commit(root: string, paths: string[], message: string): string | null;
43
+ /** The bytes of `path` as committed at HEAD. */
44
+ readCommitted(root: string, path: string): string;
45
+ out(line: string): void;
46
+ err(line: string): void;
47
+ }
48
+ /** A contract the lead drafted, checked and hashed locally before anything is sent. */
49
+ interface DraftedContract {
50
+ contract: Contract;
51
+ jobKey?: string;
52
+ sha256: string;
53
+ text: string;
54
+ }
55
+ /** The batch size the api enforces. */
56
+ export declare const MAX_CONTRACTS = 50;
57
+ /** `owner/name` from a GitHub remote URL (https or ssh), or null. */
58
+ export declare function repoFromRemote(url: string): string | null;
59
+ /**
60
+ * Parse the input: a JSON array, or `{ "contracts": [...] }`. Each item is a contract object, or
61
+ * `{ "contract": {...}, "job_key": "..." }`. Every contract must pass the gateway's seal-time
62
+ * check; the first failure is reported with its index and nothing is sent.
63
+ */
64
+ export declare function parseFuelInput(raw: string): DraftedContract[];
65
+ export declare function agentFuelCommand(file: string, opts: FuelOptions, deps: FuelDeps): Promise<void>;
66
+ /** Runs a command and returns its trimmed stdout; throws when it cannot run or exits non-zero. */
67
+ export type RunCommand = (cmd: string, args: string[]) => string;
68
+ /** `gh auth token`, or null when gh is missing or logged out. */
69
+ export declare function ghAuthToken(run?: RunCommand): string | null;
70
+ /** The caller's GitHub token: GH_TOKEN, then GITHUB_TOKEN, then `gh auth token`; null when none. */
71
+ export declare function resolveGithubToken(env?: NodeJS.ProcessEnv, gh?: () => string | null): string | null;
72
+ /** The contracts file; `-` reads stdin (fd 0). */
73
+ export declare function readFuelInput(path: string, read?: (file: string | number, enc: "utf8") => string): string;
74
+ export declare const liveFuelDeps: FuelDeps;
75
+ export {};
@@ -0,0 +1,298 @@
1
+ /**
2
+ * `mnemom agent fuel <file>` — the fuel step's CLI verb (Doors W3.2, decision 17).
3
+ *
4
+ * One confirmation seals N job contracts: the verb reads the contracts the lead drafted, checks and
5
+ * hashes each one exactly as the gateway would (`lib/contract-hash.ts`), shows them, asks once, and
6
+ * sends the whole batch to the batch-seal endpoint, which writes N seals in one api transaction.
7
+ * It then writes each sealed contract to `.mnemom/contracts/<sha256>.json` in the repo and commits
8
+ * only those files, so the contract text is in the repo the jobs run against.
9
+ *
10
+ * Every arm is unit-tested through `FuelDeps`; `liveFuelDeps` is the thin driver.
11
+ */
12
+ import { execFileSync } from "node:child_process";
13
+ import { createHash } from "node:crypto";
14
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import { createInterface } from "node:readline/promises";
17
+ import { listMyOrgs, MnemomApiError, newIdempotencyKey, sealContractBatch, } from "../lib/api.js";
18
+ import { checkContract, contractFileText, contractRepoPath, contractSha256, } from "../lib/contract-hash.js";
19
+ /** `owner/name` as GitHub issues it, the same rule the api applies: never `.` or `..` as a name. */
20
+ const REPO_RE = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,38})\/(?!\.\.?$)[A-Za-z0-9_.-]{1,100}$/;
21
+ /** The batch size the api enforces. */
22
+ export const MAX_CONTRACTS = 50;
23
+ /** `owner/name` from a GitHub remote URL (https or ssh), or null. */
24
+ export function repoFromRemote(url) {
25
+ const m = /github\.com[:/]+([^/\s]+)\/([^/\s]+?)(?:\.git)?\/?$/.exec(url.trim());
26
+ return m ? `${m[1]}/${m[2]}` : null;
27
+ }
28
+ /**
29
+ * Parse the input: a JSON array, or `{ "contracts": [...] }`. Each item is a contract object, or
30
+ * `{ "contract": {...}, "job_key": "..." }`. Every contract must pass the gateway's seal-time
31
+ * check; the first failure is reported with its index and nothing is sent.
32
+ */
33
+ export function parseFuelInput(raw) {
34
+ let parsed;
35
+ try {
36
+ parsed = JSON.parse(raw);
37
+ }
38
+ catch {
39
+ throw new Error("The contracts file is not valid JSON.");
40
+ }
41
+ const items = Array.isArray(parsed)
42
+ ? parsed
43
+ : parsed &&
44
+ typeof parsed === "object" &&
45
+ Array.isArray(parsed.contracts)
46
+ ? parsed.contracts
47
+ : null;
48
+ if (!items)
49
+ throw new Error('Expected a JSON array of contracts, or { "contracts": [...] }.');
50
+ if (items.length === 0)
51
+ throw new Error("The contracts file holds no contracts.");
52
+ if (items.length > MAX_CONTRACTS) {
53
+ throw new Error(`A batch holds at most ${MAX_CONTRACTS} contracts (got ${items.length}).`);
54
+ }
55
+ const drafted = [];
56
+ const seen = new Map();
57
+ items.forEach((item, i) => {
58
+ const wrapped = item !== null &&
59
+ typeof item === "object" &&
60
+ !Array.isArray(item) &&
61
+ "contract" in item &&
62
+ typeof item.contract === "object";
63
+ const value = wrapped ? item.contract : item;
64
+ const rawKey = wrapped ? item.job_key : undefined;
65
+ const check = checkContract(value, `contracts[${i}]`);
66
+ if (!check.ok)
67
+ throw new Error(`contracts[${i}]: ${check.message} (${check.reason}).`);
68
+ const sha256 = contractSha256(check.contract);
69
+ const dup = seen.get(sha256);
70
+ if (dup !== undefined)
71
+ throw new Error(`contracts[${i}] is the same contract as contracts[${dup}].`);
72
+ seen.set(sha256, i);
73
+ drafted.push({
74
+ contract: check.contract,
75
+ jobKey: typeof rawKey === "string" && rawKey.trim() ? rawKey.trim() : undefined,
76
+ sha256,
77
+ text: contractFileText(check.contract),
78
+ });
79
+ });
80
+ return drafted;
81
+ }
82
+ async function resolveOrg(opts, deps) {
83
+ if (opts.org)
84
+ return opts.org;
85
+ const orgs = (await deps.listOrgs()).filter((o) => !o.is_personal);
86
+ if (orgs.length === 1)
87
+ return orgs[0].org_id;
88
+ if (orgs.length === 0)
89
+ throw new Error("You belong to no organization; pass --org <org_id>.");
90
+ throw new Error(`You belong to ${orgs.length} organizations; pass --org <org_id> (${orgs.map((o) => o.org_id).join(", ")}).`);
91
+ }
92
+ /**
93
+ * The statement's first line for the confirmation list. Control characters (ANSI escapes, carriage
94
+ * returns) are shown as `?`, so a statement cannot redraw the prompt it is confirmed from.
95
+ */
96
+ // eslint-disable-next-line no-control-regex
97
+ const CONTROL_RE = /[\u0000-\u001f\u007f-\u009f]/g;
98
+ function firstLine(text, max = 72) {
99
+ const line = text.split("\n")[0].trim().replace(CONTROL_RE, "?");
100
+ return line.length > max ? `${line.slice(0, max - 1)}…` : line;
101
+ }
102
+ export async function agentFuelCommand(file, opts, deps) {
103
+ const drafted = parseFuelInput(deps.readInput(file));
104
+ const root = deps.gitRoot();
105
+ const repo = opts.repo ?? deps.originRepo();
106
+ if (!repo || !REPO_RE.test(repo)) {
107
+ throw new Error("Pass --repo <owner/name> (no GitHub origin remote to read it from).");
108
+ }
109
+ const commit = opts.commit !== false;
110
+ if (commit && !root) {
111
+ throw new Error("Run inside the repo's git work tree to commit the contracts, or pass --no-commit.");
112
+ }
113
+ const orgId = await resolveOrg(opts, deps);
114
+ // The api checks with this token that you can write to the repo (OD-10); ask for it before
115
+ // the confirmation, so a missing token never costs one.
116
+ const githubToken = deps.githubToken();
117
+ if (!githubToken) {
118
+ throw new Error("Sealing checks you can write to the repo with your GitHub token: set GH_TOKEN, or run `gh auth login`.");
119
+ }
120
+ if (!opts.json) {
121
+ deps.out(`Seal ${drafted.length} contract${drafted.length === 1 ? "" : "s"} for ${repo} (org ${orgId}):`);
122
+ drafted.forEach((d, i) => {
123
+ const key = d.jobKey ? ` [${d.jobKey}]` : "";
124
+ deps.out(` ${i + 1}. ${d.sha256.slice(0, 16)}${key} ${firstLine(d.contract.statement)}`);
125
+ });
126
+ }
127
+ if (!opts.yes) {
128
+ if (!deps.isInteractive()) {
129
+ throw new Error("Sealing needs one confirmation: run in a terminal, or pass --yes.");
130
+ }
131
+ if (!(await deps.confirm(`Seal all ${drafted.length}?`))) {
132
+ deps.err("Nothing sealed.");
133
+ process.exitCode = 1;
134
+ return;
135
+ }
136
+ }
137
+ // One key for this confirmation, held across the retry, so a retried send replays the batch.
138
+ // --key replays a batch whose local write or commit failed, instead of sealing it twice.
139
+ const key = opts.key?.trim() || deps.newKey();
140
+ const body = {
141
+ repo,
142
+ source: "cli",
143
+ contracts: drafted.map((d) => d.jobKey ? { contract: d.contract, job_key: d.jobKey } : { contract: d.contract }),
144
+ };
145
+ let result;
146
+ try {
147
+ result = await deps.seal(orgId, body, key, githubToken);
148
+ }
149
+ catch (error) {
150
+ // A 5xx or a network fault may have reached the api, so retry once with the same key. A 4xx
151
+ // is the api's answer and is not retried.
152
+ if (!(error instanceof MnemomApiError) || error.effectiveStatus >= 500) {
153
+ try {
154
+ result = await deps.seal(orgId, body, key, githubToken);
155
+ }
156
+ catch (retryError) {
157
+ // The api may have sealed before failing; the same key replays that batch, never a second one.
158
+ const why = retryError instanceof Error ? retryError.message : String(retryError);
159
+ throw new Error(`Could not seal the contracts: ${why} Rerun with --key ${key} so a batch the api did seal is replayed, not sealed twice.`, { cause: retryError });
160
+ }
161
+ }
162
+ else {
163
+ throw error;
164
+ }
165
+ }
166
+ // The server must have sealed exactly what was confirmed, in order.
167
+ if (result.seals.length !== drafted.length) {
168
+ throw new Error(`The api sealed ${result.seals.length} contracts, not ${drafted.length}; batch ${result.batch_id}.`);
169
+ }
170
+ result.seals.forEach((s, i) => {
171
+ if (s.contract_sha256 !== drafted[i].sha256) {
172
+ throw new Error(`contracts[${i}] was sealed as ${s.contract_sha256}, not ${drafted[i].sha256}; batch ${result.batch_id}.`);
173
+ }
174
+ });
175
+ let commitSha = null;
176
+ const paths = drafted.map((d) => contractRepoPath(d.sha256));
177
+ if (commit && root) {
178
+ try {
179
+ commitSha = writeAndCommit(deps, root, drafted, paths, result.batch_id);
180
+ }
181
+ catch (error) {
182
+ const why = error instanceof Error ? error.message : String(error);
183
+ throw new Error(`Sealed as batch ${result.batch_id}, but committing the contract files failed: ${why} ` +
184
+ `Fix that, then rerun with --key ${key} to replay this batch without sealing it twice.`, { cause: error });
185
+ }
186
+ }
187
+ if (opts.json) {
188
+ deps.out(JSON.stringify({ ...result, files: paths, commit: commitSha }, null, 2));
189
+ return;
190
+ }
191
+ deps.out(`${result.replayed ? "Already sealed" : "Sealed"} ${result.seals.length} as batch ${result.batch_id}. Dispatch: ${result.dispatch}.`);
192
+ for (const s of result.seals)
193
+ deps.out(` ${s.node_id} canon ${s.canon_status}`);
194
+ if (commit && !commitSha)
195
+ deps.out("The contract files were already committed.");
196
+ if (commitSha)
197
+ deps.out(`Committed ${paths.length} contract file${paths.length === 1 ? "" : "s"} in ${commitSha.slice(0, 12)}.`);
198
+ }
199
+ /** Write each sealed contract's exact bytes and commit only those paths; returns the commit sha. */
200
+ function writeAndCommit(deps, root, drafted, paths, batchId) {
201
+ drafted.forEach((d, i) => deps.writeFile(join(root, paths[i]), d.text));
202
+ const sha = deps.commit(root, paths, `chore(mnemom): seal ${drafted.length} contract${drafted.length === 1 ? "" : "s"}\n\nMnemom-Seal-Batch: ${batchId}`);
203
+ // A repo hook (a formatter, an end-of-file fixer) may rewrite a staged file. The committed
204
+ // bytes must still hash to the sealed sha256, or the runner would refuse the job.
205
+ paths.forEach((p, i) => {
206
+ const committed = createHash("sha256")
207
+ .update(deps.readCommitted(root, p), "utf8")
208
+ .digest("hex");
209
+ if (committed !== drafted[i].sha256) {
210
+ throw new Error(`${p} was changed on commit (a repo hook?) and no longer hashes to its sealed sha256; restore its exact bytes and commit with the hook skipped.`);
211
+ }
212
+ });
213
+ return sha;
214
+ }
215
+ function git(cwd, args) {
216
+ return execFileSync("git", args, {
217
+ cwd,
218
+ encoding: "utf8",
219
+ stdio: ["ignore", "pipe", "pipe"],
220
+ }).trim();
221
+ }
222
+ const runQuiet = (cmd, args) => execFileSync(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
223
+ /** `gh auth token`, or null when gh is missing or logged out. */
224
+ export function ghAuthToken(run = runQuiet) {
225
+ try {
226
+ return run("gh", ["auth", "token"]) || null;
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ }
232
+ /** The caller's GitHub token: GH_TOKEN, then GITHUB_TOKEN, then `gh auth token`; null when none. */
233
+ export function resolveGithubToken(env = process.env, gh = ghAuthToken) {
234
+ const fromEnv = (env.GH_TOKEN ?? env.GITHUB_TOKEN ?? "").trim();
235
+ return fromEnv || gh();
236
+ }
237
+ /** The contracts file; `-` reads stdin (fd 0). */
238
+ export function readFuelInput(path, read = readFileSync) {
239
+ return read(path === "-" ? 0 : path, "utf8");
240
+ }
241
+ export const liveFuelDeps = {
242
+ readInput: (path) => readFuelInput(path),
243
+ listOrgs: () => listMyOrgs(),
244
+ seal: (orgId, body, key, token) => sealContractBatch(orgId, body, key, token),
245
+ githubToken: () => resolveGithubToken(),
246
+ newKey: () => newIdempotencyKey(),
247
+ async confirm(question) {
248
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
249
+ try {
250
+ return /^y(es)?$/i.test((await rl.question(`${question} [y/N] `)).trim());
251
+ }
252
+ finally {
253
+ rl.close();
254
+ }
255
+ },
256
+ isInteractive: () => Boolean(process.stdin.isTTY && process.stdout.isTTY),
257
+ gitRoot() {
258
+ try {
259
+ return git(undefined, ["rev-parse", "--show-toplevel"]) || null;
260
+ }
261
+ catch {
262
+ return null;
263
+ }
264
+ },
265
+ originRepo() {
266
+ try {
267
+ return repoFromRemote(git(undefined, ["remote", "get-url", "origin"]));
268
+ }
269
+ catch {
270
+ return null;
271
+ }
272
+ },
273
+ writeFile(path, text) {
274
+ mkdirSync(dirname(path), { recursive: true });
275
+ writeFileSync(path, text, "utf8");
276
+ },
277
+ commit(root, paths, message) {
278
+ git(root, ["add", "--", ...paths]);
279
+ try {
280
+ git(root, ["diff", "--cached", "--quiet", "--", ...paths]);
281
+ return null;
282
+ }
283
+ catch {
284
+ // Exit 1: there are staged changes to commit.
285
+ }
286
+ git(root, ["commit", "-m", message, "--only", "--", ...paths]);
287
+ return git(root, ["rev-parse", "HEAD"]);
288
+ },
289
+ readCommitted(root, path) {
290
+ return execFileSync("git", ["cat-file", "blob", `HEAD:${path}`], {
291
+ cwd: root,
292
+ encoding: "utf8",
293
+ stdio: ["ignore", "pipe", "pipe"],
294
+ });
295
+ },
296
+ out: (line) => console.log(line),
297
+ err: (line) => console.error(line),
298
+ };
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The one-time notice for door one's T16 default (decisions 12 and 22, OD-13): on a
3
+ * prerelease (`next`) build an interactive or Remote Control launch with nothing
4
+ * configured runs with an implicit goal. The first launch that gets that default says
5
+ * so once, with both opt-outs; every later launch is silent.
6
+ *
7
+ * State file (~/.mnemom/implicit-notice.json): { shown_at }. A launch with an explicit
8
+ * mode (a flag, `--goal`, `goal_mode`, the experimental feature) never reaches this, and
9
+ * --dry-run neither prints nor records it. A state file that cannot be read counts as
10
+ * not shown; one that cannot be written never fails the launch.
11
+ */
12
+ export declare const IMPLICIT_NOTICE_FILE = "implicit-notice.json";
13
+ export declare const IMPLICIT_DEFAULT_NOTICE: string;
14
+ export interface ImplicitNoticeDeps {
15
+ /** The state file's text, or null when it is missing or unreadable. */
16
+ read(): string | null;
17
+ write(text: string): void;
18
+ note(line: string): void;
19
+ now(): Date;
20
+ }
21
+ /** Has the notice been shown on this machine? Any parseable `shown_at` string counts. */
22
+ export declare function noticeShown(text: string | null): boolean;
23
+ /** Print the notice unless it was shown before, then record it. Returns whether it printed. */
24
+ export declare function maybeShowImplicitNotice(opts: {
25
+ dryRun?: boolean;
26
+ }, deps: ImplicitNoticeDeps): boolean;
27
+ export declare function defaultImplicitNoticeDeps(mnemomDir?: string): ImplicitNoticeDeps;
28
+ /** Called by the launch when its implicit goal came from the prerelease default. */
29
+ export declare function showImplicitDefaultNoticeOnce(opts: {
30
+ dryRun?: boolean;
31
+ }, deps?: ImplicitNoticeDeps): boolean;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The one-time notice for door one's T16 default (decisions 12 and 22, OD-13): on a
3
+ * prerelease (`next`) build an interactive or Remote Control launch with nothing
4
+ * configured runs with an implicit goal. The first launch that gets that default says
5
+ * so once, with both opt-outs; every later launch is silent.
6
+ *
7
+ * State file (~/.mnemom/implicit-notice.json): { shown_at }. A launch with an explicit
8
+ * mode (a flag, `--goal`, `goal_mode`, the experimental feature) never reaches this, and
9
+ * --dry-run neither prints nor records it. A state file that cannot be read counts as
10
+ * not shown; one that cannot be written never fails the launch.
11
+ */
12
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
13
+ import { dirname, join } from "node:path";
14
+ import { MNEMOM_DIR } from "../lib/config.js";
15
+ export const IMPLICIT_NOTICE_FILE = "implicit-notice.json";
16
+ export const IMPLICIT_DEFAULT_NOTICE = "mnemom: implicit goals are on by default in this prerelease. The gateway infers the " +
17
+ "session's goal from your messages, and the record says the goal was implicit, never sealed. " +
18
+ "Turn it off for one launch with --no-implicit, or for every launch with " +
19
+ "`mnemom agent config set goal_mode off`.";
20
+ /** Has the notice been shown on this machine? Any parseable `shown_at` string counts. */
21
+ export function noticeShown(text) {
22
+ if (text === null)
23
+ return false;
24
+ try {
25
+ const v = JSON.parse(text);
26
+ return (typeof v === "object" &&
27
+ v !== null &&
28
+ typeof v.shown_at === "string");
29
+ }
30
+ catch {
31
+ return false;
32
+ }
33
+ }
34
+ /** Print the notice unless it was shown before, then record it. Returns whether it printed. */
35
+ export function maybeShowImplicitNotice(opts, deps) {
36
+ if (opts.dryRun)
37
+ return false;
38
+ if (noticeShown(deps.read()))
39
+ return false;
40
+ deps.note(IMPLICIT_DEFAULT_NOTICE);
41
+ try {
42
+ deps.write(`${JSON.stringify({ shown_at: deps.now().toISOString() })}\n`);
43
+ }
44
+ catch {
45
+ // Never fail a launch over a notice.
46
+ }
47
+ return true;
48
+ }
49
+ export function defaultImplicitNoticeDeps(mnemomDir = MNEMOM_DIR) {
50
+ const path = join(mnemomDir, IMPLICIT_NOTICE_FILE);
51
+ return {
52
+ read: () => {
53
+ try {
54
+ return readFileSync(path, "utf8");
55
+ }
56
+ catch {
57
+ return null;
58
+ }
59
+ },
60
+ write: (text) => {
61
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
62
+ const tmp = `${path}.${process.pid}.tmp`;
63
+ writeFileSync(tmp, text, { mode: 0o600 });
64
+ renameSync(tmp, path);
65
+ },
66
+ note: (line) => console.error(line),
67
+ now: () => new Date(),
68
+ };
69
+ }
70
+ /** Called by the launch when its implicit goal came from the prerelease default. */
71
+ export function showImplicitDefaultNoticeOnce(opts, deps = defaultImplicitNoticeDeps()) {
72
+ return maybeShowImplicitNotice(opts, deps);
73
+ }
@@ -12,7 +12,8 @@
12
12
  * (`x-mnemom-contract`). Implicit alignment — the goal inferred from your own
13
13
  * messages (`x-mnemom-goal-mode: implicit`, no contract) — is the experimental
14
14
  * `implicit` feature: while it is on a bare launch runs implicit, while it is off
15
- * (the default) a bare launch runs with alignment off. `--no-implicit` (or
15
+ * (the default) a bare launch runs with alignment off. On a prerelease (`next`)
16
+ * build a bare launch runs implicit unless it is headless `-p` (T16). `--no-implicit` (or
16
17
  * launcher.toml `goal_mode = "off"`) turns it off. The mode is resolved upstream
17
18
  * (`resolveImplicit`) and announced on the launch line.
18
19
  *
@@ -12,7 +12,8 @@
12
12
  * (`x-mnemom-contract`). Implicit alignment — the goal inferred from your own
13
13
  * messages (`x-mnemom-goal-mode: implicit`, no contract) — is the experimental
14
14
  * `implicit` feature: while it is on a bare launch runs implicit, while it is off
15
- * (the default) a bare launch runs with alignment off. `--no-implicit` (or
15
+ * (the default) a bare launch runs with alignment off. On a prerelease (`next`)
16
+ * build a bare launch runs implicit unless it is headless `-p` (T16). `--no-implicit` (or
16
17
  * launcher.toml `goal_mode = "off"`) turns it off. The mode is resolved upstream
17
18
  * (`resolveImplicit`) and announced on the launch line.
18
19
  *
@@ -28,6 +28,7 @@
28
28
  import { type MnemomKeyDeps } from "../lib/subscription-login.js";
29
29
  import { type SessionRecord } from "../lib/agent-ledger.js";
30
30
  import { type ExperimentalId } from "../lib/experimental.js";
31
+ import { conductReceiptFor, type ReceiptLaunch } from "../lib/conduct-receipt.js";
31
32
  import { type GovernanceOptIn } from "./agent-setup.js";
32
33
  import { type ContextProfile, type LaunchShape, type SealedContractEnvelope } from "../lib/agent.js";
33
34
  export interface AgentOptions {
@@ -310,11 +311,57 @@ export interface RcCapability {
310
311
  * and by the auto-default selection (a bare launch prefers RC when capable).
311
312
  */
312
313
  export declare function checkRcCapability(cli: ResolvedCli): RcCapability;
314
+ /**
315
+ * Whether the proxy captures the gateway's record state: the conduct receipt reads it at exit,
316
+ * and the fed status line reads its lineage key while the session runs.
317
+ */
318
+ export declare function wantsRecordState(receipt: boolean, fed: boolean): boolean;
319
+ /**
320
+ * Point Claude Code at a ready proxy and, on a fed launch, start the status-line feed on the
321
+ * proxy's record state (no state file, no feed: the line stays "mnemom").
322
+ */
323
+ export declare function onBaseUrlProxyReady(baseUrl: string | null, recordStateFile: string | undefined, childEnv: NodeJS.ProcessEnv, feed: {
324
+ feedFile: string;
325
+ orgId: string;
326
+ } | null, start?: (o: {
327
+ recordStateFile: string;
328
+ feedFile: string;
329
+ orgId: string;
330
+ }) => unknown): void;
331
+ /**
332
+ * The status-line feed file for a fed launch (and FEED_ENV set on the child), or null: the
333
+ * segment off, no billing org, or {@link fedStatuslineWanted} false.
334
+ */
335
+ export declare function prepareStatuslineFeed(o: {
336
+ statusline: boolean;
337
+ orgId?: string;
338
+ version: string;
339
+ goalMode: boolean;
340
+ env: NodeJS.ProcessEnv;
341
+ childEnv: NodeJS.ProcessEnv;
342
+ files?: () => {
343
+ feedFile: string;
344
+ };
345
+ }): {
346
+ feedFile: string;
347
+ } | null;
348
+ /**
349
+ * Whether this launch feeds the status line measured model cost and μ (W1.4): prerelease
350
+ * builds only (stable stays "mnemom", decision 14), and only when the turn proxy runs, since
351
+ * the proxy is what learns the gateway's lineage key for the session.
352
+ */
353
+ export declare function fedStatuslineWanted(version: string, goalMode: boolean, env: NodeJS.ProcessEnv): boolean;
313
354
  /**
314
355
  * Add 127.0.0.1 and localhost to BOTH NO_PROXY and no_proxy (proxy clients disagree on which one
315
356
  * wins, and some read only the lowercase one), keeping whatever each already listed.
316
357
  */
317
358
  export declare function exemptLoopbackFromProxy(env: NodeJS.ProcessEnv): void;
359
+ /**
360
+ * Print the conduct receipt (experimental `record`) — the exit call site here, and the one a
361
+ * durable park calls. Never throws: a failure is one warning line, and the caller's exit code
362
+ * is untouched.
363
+ */
364
+ export declare function sayConductReceipt(launch: ReceiptLaunch, trigger?: "exit" | "park", receipt?: typeof conductReceiptFor): Promise<void>;
318
365
  /** Whether a terminal implicit launch runs the turn-origin proxy (MNEMOM_AGENT_TURN_HEADER=0 opts out). */
319
366
  export declare function turnProxyWanted(env: NodeJS.ProcessEnv): boolean;
320
367
  /** Test seams for startBaseUrlProxy (every real caller omits them). */
@@ -325,6 +372,10 @@ export interface BaseUrlProxyOptions {
325
372
  node?: string;
326
373
  /** How long to wait for READY before launching straight to the door (default 5 s). */
327
374
  readyTimeoutMs?: number;
375
+ /** Send x-mnemom-turn (default true); false sets MNEMOM_AGENT_TURN_HEADER=0 for the proxy. */
376
+ turnHeader?: boolean;
377
+ /** Capture the conduct receipt's record state; its path is `done`'s second argument. */
378
+ recordState?: boolean;
328
379
  }
329
380
  /**
330
381
  * Start base-url-proxy.mjs in front of `door`; calls `done` exactly once with its
@@ -333,7 +384,7 @@ export interface BaseUrlProxyOptions {
333
384
  * script is the same case (node exits 1 before READY). The proxy holds no secret (auth headers
334
385
  * pass through it), exits with this process, and is killed on exit.
335
386
  */
336
- export declare function startBaseUrlProxy(door: string, done: (baseUrl: string | null) => void, opts?: BaseUrlProxyOptions): void;
387
+ export declare function startBaseUrlProxy(door: string, done: (baseUrl: string | null, recordStatePath?: string) => void, opts?: BaseUrlProxyOptions): void;
337
388
  /**
338
389
  * The three secret lines the Remote Control proxy reads on stdin (anthropic, mnemom, openai). The
339
390
  * Mnemom key rides line 2 on BOTH auth modes when the launch has one — the proxy forwards it as