@bli-cockpit/cli 0.2.49 → 0.2.51

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 (57) hide show
  1. package/dist/adapters/raw-evidence-claude-reader.js +108 -0
  2. package/dist/adapters/raw-evidence-codex-reader.js +147 -0
  3. package/dist/adapters/raw-evidence-collection-state.js +199 -0
  4. package/dist/adapters/raw-evidence-facts.js +338 -0
  5. package/dist/adapters/raw-evidence-git-diff-reader.js +187 -0
  6. package/dist/adapters/raw-evidence-image-reader.js +107 -0
  7. package/dist/adapters/raw-evidence-sanitize.js +56 -0
  8. package/dist/adapters/raw-evidence-transcript-file.js +182 -0
  9. package/dist/adapters/raw-evidence.js +63 -1183
  10. package/dist/commands/backfill-batches.js +34 -0
  11. package/dist/commands/backfill-candidates.js +54 -0
  12. package/dist/commands/backfill-checkpoint.js +101 -0
  13. package/dist/commands/backfill-command-line.js +70 -0
  14. package/dist/commands/backfill-evidence-outcomes.js +104 -0
  15. package/dist/commands/backfill-issues.js +265 -0
  16. package/dist/commands/backfill-output.js +75 -0
  17. package/dist/commands/backfill-plan.js +71 -0
  18. package/dist/commands/backfill-reasons.js +107 -0
  19. package/dist/commands/backfill-report.js +298 -0
  20. package/dist/commands/backfill-result.js +150 -0
  21. package/dist/commands/backfill-scan.js +274 -0
  22. package/dist/commands/backfill-scope.js +114 -0
  23. package/dist/commands/backfill-session-report.js +145 -0
  24. package/dist/commands/backfill-types.js +1 -0
  25. package/dist/commands/backfill-upload.js +212 -0
  26. package/dist/commands/backfill.js +41 -1961
  27. package/dist/commands/doctor.js +57 -0
  28. package/dist/commands/jarvis-trace.js +184 -0
  29. package/dist/commands/jarvis.js +144 -4
  30. package/dist/commands/local-args-collector.js +26 -0
  31. package/dist/commands/local-args-tower.js +21 -0
  32. package/dist/commands/local-args.js +3 -1
  33. package/dist/commands/local-help.js +19 -2
  34. package/dist/commands/local.js +3 -0
  35. package/dist/commands/memory-install-claude.js +294 -0
  36. package/dist/commands/memory-install-codex.js +205 -0
  37. package/dist/commands/memory-install-contract.js +286 -0
  38. package/dist/commands/memory-install-files.js +63 -0
  39. package/dist/commands/memory-install-skills.js +121 -0
  40. package/dist/commands/memory-install-toml.js +265 -0
  41. package/dist/commands/memory-install.js +465 -0
  42. package/dist/commands/public-root.js +1 -1
  43. package/dist/commands/sync-followups.js +105 -0
  44. package/dist/commands/sync.js +7 -1
  45. package/dist/local-state-attributed-target.js +75 -0
  46. package/dist/local-state-config.js +147 -0
  47. package/dist/local-state-files.js +59 -0
  48. package/dist/local-state-identity.js +73 -0
  49. package/dist/local-state-pairing.js +263 -0
  50. package/dist/local-state-paths.js +61 -0
  51. package/dist/local-state-session.js +68 -0
  52. package/dist/local-state-status.js +163 -0
  53. package/dist/local-state-work-context.js +190 -0
  54. package/dist/local-state.js +34 -848
  55. package/dist/tower-client.js +3 -2
  56. package/dist/tower-stream.js +57 -3
  57. package/package.json +2 -1
@@ -0,0 +1,286 @@
1
+ /**
2
+ * The interface this installer consumes from `@bli-cockpit/memory-mcp`
3
+ * (BLI-3580, core slice on branch `edward/bli-3580-agent-memory-core`).
4
+ *
5
+ * That package does not exist on `main` yet, and the two pull requests must be
6
+ * able to land in either order — so the contract lives HERE, in one small
7
+ * module, and is stated twice on purpose:
8
+ *
9
+ * 1. `bli-memory-mcp --print-config --claude` prints the JSON below, and
10
+ * `--print-config --codex` prints the TOML table below. When the bin
11
+ * resolves, what it prints WINS: the server knows its own shape.
12
+ * 2. `builtinMemoryInstallConfig()` is the same shape, built locally, for a
13
+ * server that resolved but does not answer `--print-config` yet.
14
+ *
15
+ * Both require a RESOLVED bin. A machine that does not have the server gets
16
+ * nothing written at all and a `skipped bin_missing` receipt — the template is
17
+ * a fallback for an older server, never a substitute for an absent one
18
+ * (`no_bin_no_write`, memory-install.ts).
19
+ *
20
+ * The printed JSON contract (`--print-config --claude`):
21
+ *
22
+ * ```json
23
+ * {
24
+ * "schema": "bli-memory-install-config.v1",
25
+ * "server_id": "bli-memory",
26
+ * "mcp_server": { "command": "bli-memory-mcp", "args": [], "env": {} },
27
+ * "hooks": [
28
+ * { "event": "SessionStart", "command": "bli-memory-mcp hook session-start", "timeout_seconds": 30 },
29
+ * { "event": "UserPromptSubmit", "command": "bli-memory-mcp hook prompt", "timeout_seconds": 5 },
30
+ * { "event": "Stop", "command": "bli-memory-mcp hook stop", "timeout_seconds": 30 }
31
+ * ],
32
+ * "permissions_allow": ["mcp__bli-memory__search_memory"]
33
+ * }
34
+ * ```
35
+ *
36
+ * The printed TOML contract (`--print-config --codex`) is exactly one table:
37
+ *
38
+ * ```toml
39
+ * [mcp_servers.bli-memory]
40
+ * command = "bli-memory-mcp"
41
+ * args = []
42
+ * env = { COCKPIT_DASHBOARD_URL = "https://…" }
43
+ * ```
44
+ *
45
+ * **The printed `command` is a bare name and is never written as-is.** The bin
46
+ * cannot know where it was installed, and it lives nested inside
47
+ * `@bli-cockpit/cli`'s `node_modules` rather than on PATH — so
48
+ * `withResolvedBinPath` re-points every command at the absolute path this
49
+ * machine resolved before anything is written. Skipping that step registered
50
+ * three hooks that answered `command not found` on every turn.
51
+ *
52
+ * Nothing here touches the filesystem. The halves that do are
53
+ * `memory-install-claude.ts` and `memory-install-codex.ts`.
54
+ */
55
+ /** The published bin name. Both hosts spawn this. */
56
+ export const MEMORY_MCP_BIN = "bli-memory-mcp";
57
+ /** The MCP server id, as it appears in `mcp__<server>__<tool>`. */
58
+ export const MEMORY_MCP_SERVER_ID = "bli-memory";
59
+ /** The one env var the server is handed. A URL, never a token. */
60
+ export const MEMORY_DASHBOARD_URL_ENV = "COCKPIT_DASHBOARD_URL";
61
+ /** Claude Code hook events this installer registers, in the order it writes them. */
62
+ export const MEMORY_HOOK_EVENTS = [
63
+ "SessionStart",
64
+ "UserPromptSubmit",
65
+ "Stop",
66
+ ];
67
+ /**
68
+ * The bin's own hook subcommands. `session-start` recalls at session open,
69
+ * `prompt` recalls per turn, `stop` saves the turn — the same three jobs the
70
+ * Supermemory plugin's `session-start.js` / `recall-directive.js` / `capture.js`
71
+ * did, collapsed onto one bin.
72
+ */
73
+ export const MEMORY_HOOK_SUBCOMMAND = {
74
+ SessionStart: "hook session-start",
75
+ UserPromptSubmit: "hook prompt",
76
+ Stop: "hook stop",
77
+ };
78
+ /**
79
+ * Injection hooks must stay fast — Claude Code discards a hook that outlives
80
+ * its timeout, and a recall that arrives late is a recall that did not happen.
81
+ * These mirror the vendor plugin's own budgets (30 s / 5 s / 30 s).
82
+ */
83
+ export const MEMORY_HOOK_TIMEOUT_SECONDS = {
84
+ SessionStart: 30,
85
+ UserPromptSubmit: 5,
86
+ Stop: 30,
87
+ };
88
+ /**
89
+ * Auto-approved tools. READ ONLY, deliberately: the vendor's
90
+ * `recall-approve.js` allowed its read set and left every write prompting, and
91
+ * a memory the agent can silently overwrite is worse than one it has to ask
92
+ * about. `save_memory` / `update_memory` / `forget_memory` are absent on purpose.
93
+ */
94
+ export const MEMORY_AUTO_APPROVE_TOOLS = [
95
+ `mcp__${MEMORY_MCP_SERVER_ID}__search_memory`,
96
+ ];
97
+ /** Characters that would let a resolved path change the meaning of a hook command string. */
98
+ const UNSAFE_PATH_CHARACTERS = /["'`$;&|<>\r\n]/u;
99
+ export function isUnsafeBinPath(binPath) {
100
+ return UNSAFE_PATH_CHARACTERS.test(binPath);
101
+ }
102
+ /**
103
+ * A Claude Code hook `command` is a shell string by the platform's design, so
104
+ * the only defence is quoting — and a path that could not be quoted safely is
105
+ * refused upstream (see `resolveMemoryMcpBin`) rather than escaped cleverly.
106
+ */
107
+ export function shellQuoteBinPath(binPath) {
108
+ return /\s/u.test(binPath) ? `"${binPath}"` : binPath;
109
+ }
110
+ /**
111
+ * Windows cannot spawn an npm `.cmd` shim directly — Node refuses it without a
112
+ * shell, which is why `commands/editor.ts` REFUSES a `.cmd` editor rather than
113
+ * running it through cmd.exe with a person's path interpolated in. Here the
114
+ * path is ours, not a person's, and both hosts spawn the MCP server themselves
115
+ * with an argument array, so `cmd /c <shim>` is the correct and safe shape: the
116
+ * arguments never become one string we built.
117
+ */
118
+ export function memoryMcpServerEntry(options) {
119
+ const env = options.dashboardUrl
120
+ ? { [MEMORY_DASHBOARD_URL_ENV]: options.dashboardUrl }
121
+ : {};
122
+ if (options.platform === "win32") {
123
+ return { command: "cmd", args: ["/c", options.binPath], env };
124
+ }
125
+ return { command: options.binPath, args: [], env };
126
+ }
127
+ export function builtinMemoryInstallConfig(options) {
128
+ const quoted = shellQuoteBinPath(options.binPath);
129
+ return {
130
+ server_id: MEMORY_MCP_SERVER_ID,
131
+ mcp_server: memoryMcpServerEntry(options),
132
+ hooks: MEMORY_HOOK_EVENTS.map((event) => ({
133
+ event,
134
+ command: `${quoted} ${MEMORY_HOOK_SUBCOMMAND[event]}`,
135
+ timeout_seconds: MEMORY_HOOK_TIMEOUT_SECONDS[event],
136
+ })),
137
+ permissions_allow: [...MEMORY_AUTO_APPROVE_TOOLS],
138
+ };
139
+ }
140
+ /**
141
+ * Recognises a hook entry as OURS, wherever the bin ended up and however it was
142
+ * quoted. Identity is the bin name followed by `hook`, so an absolute path, a
143
+ * `.cmd` shim and a bare name all match — and somebody else's hook never does.
144
+ * This is what makes a re-run replace our three entries instead of appending a
145
+ * fourth copy.
146
+ */
147
+ export function isMemoryHookCommand(command) {
148
+ return new RegExp(`${escapeRegExp(MEMORY_MCP_BIN)}(?:\\.cmd|\\.exe|\\.bat)?["']?\\s+hook\\b`, "u").test(command);
149
+ }
150
+ function escapeRegExp(value) {
151
+ return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
152
+ }
153
+ /**
154
+ * Parses what `bli-memory-mcp --print-config --claude` printed. Anything that
155
+ * is not the documented shape returns null and the caller falls back to the
156
+ * built-in template with a named reason — a half-understood config is worse
157
+ * than the one we can write ourselves.
158
+ */
159
+ export function parsePrintedMemoryInstallConfig(stdout) {
160
+ let parsed;
161
+ try {
162
+ parsed = JSON.parse(stdout.trim());
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ if (!parsed || typeof parsed !== "object")
168
+ return null;
169
+ const record = parsed;
170
+ const server = record["mcp_server"];
171
+ if (!server || typeof server !== "object")
172
+ return null;
173
+ const serverRecord = server;
174
+ const command = serverRecord["command"];
175
+ if (typeof command !== "string" || !command.trim())
176
+ return null;
177
+ const args = normalizeStringArray(serverRecord["args"]);
178
+ const env = normalizeStringRecord(serverRecord["env"]);
179
+ if (args === null || env === null)
180
+ return null;
181
+ const rawHooks = record["hooks"];
182
+ if (!Array.isArray(rawHooks))
183
+ return null;
184
+ const hooks = [];
185
+ for (const rawHook of rawHooks) {
186
+ if (!rawHook || typeof rawHook !== "object")
187
+ return null;
188
+ const hookRecord = rawHook;
189
+ const event = hookRecord["event"];
190
+ const hookCommand = hookRecord["command"];
191
+ if (typeof event !== "string" || typeof hookCommand !== "string")
192
+ return null;
193
+ if (!MEMORY_HOOK_EVENTS.includes(event))
194
+ return null;
195
+ const timeout = hookRecord["timeout_seconds"];
196
+ hooks.push({
197
+ event: event,
198
+ command: hookCommand,
199
+ timeout_seconds: typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
200
+ ? timeout
201
+ : MEMORY_HOOK_TIMEOUT_SECONDS[event],
202
+ });
203
+ }
204
+ if (hooks.length === 0)
205
+ return null;
206
+ const allow = normalizeStringArray(record["permissions_allow"]);
207
+ const serverId = record["server_id"];
208
+ return {
209
+ server_id: typeof serverId === "string" && serverId.trim()
210
+ ? serverId.trim()
211
+ : MEMORY_MCP_SERVER_ID,
212
+ mcp_server: { command, args, env },
213
+ hooks,
214
+ permissions_allow: allow ?? [...MEMORY_AUTO_APPROVE_TOOLS],
215
+ };
216
+ }
217
+ /**
218
+ * Put THIS machine's resolved bin path into a config the bin printed.
219
+ *
220
+ * The printed contract says `command: "bli-memory-mcp"` — a bare name, because
221
+ * the bin has no idea where it was installed. `bli-memory-mcp` is a DEPENDENCY
222
+ * of `@bli-cockpit/cli`, nested inside its `node_modules`, and therefore **not
223
+ * on anybody's PATH**: a host handed the bare name answers `command not found`
224
+ * on every SessionStart, every prompt and every Stop. The installer is the only
225
+ * thing that knows the absolute path, so path qualification happens here and
226
+ * the printed strings are never written verbatim (BLI-3580; found by running
227
+ * the registered hook command in a simulated global install, not by reading
228
+ * the code).
229
+ *
230
+ * What the printed config still owns: which hook EVENTS exist, their
231
+ * subcommands, their timeouts, the server id and the allow-list. What this
232
+ * function owns: where the program is, and the per-platform launch shape.
233
+ *
234
+ * Returns null when a printed hook command cannot be understood well enough to
235
+ * re-point — the caller then falls back to the built-in template, which is
236
+ * always path-qualified. Guessing at a command we cannot parse would write a
237
+ * hook that runs something else.
238
+ */
239
+ export function withResolvedBinPath(config, options) {
240
+ const quoted = shellQuoteBinPath(options.binPath);
241
+ const hooks = [];
242
+ for (const hook of config.hooks) {
243
+ const tail = hookSubcommandTail(hook.command);
244
+ if (!tail)
245
+ return null;
246
+ hooks.push({ ...hook, command: `${quoted} ${tail}` });
247
+ }
248
+ const entry = memoryMcpServerEntry(options);
249
+ return {
250
+ ...config,
251
+ // The bin's own env wins nothing and loses nothing: it prints `{}`, and the
252
+ // dashboard URL is a machine fact the installer holds.
253
+ mcp_server: { ...entry, env: { ...config.mcp_server.env, ...entry.env } },
254
+ hooks,
255
+ };
256
+ }
257
+ /**
258
+ * `…/bli-memory-mcp hook prompt` → `hook prompt`. The subcommand is the part we
259
+ * keep; everything before it is a path that may be wrong for this machine.
260
+ */
261
+ function hookSubcommandTail(command) {
262
+ const match = /(^|\s)(hook\s+\S+.*)$/u.exec(command.trim());
263
+ return match?.[2]?.trim() ?? null;
264
+ }
265
+ function normalizeStringArray(value) {
266
+ if (value === undefined)
267
+ return [];
268
+ if (!Array.isArray(value))
269
+ return null;
270
+ if (!value.every((entry) => typeof entry === "string"))
271
+ return null;
272
+ return value;
273
+ }
274
+ function normalizeStringRecord(value) {
275
+ if (value === undefined)
276
+ return {};
277
+ if (!value || typeof value !== "object" || Array.isArray(value))
278
+ return null;
279
+ const out = {};
280
+ for (const [key, entry] of Object.entries(value)) {
281
+ if (typeof entry !== "string")
282
+ return null;
283
+ out[key] = entry;
284
+ }
285
+ return out;
286
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The one filesystem seam `cockpit memory install` writes through.
3
+ *
4
+ * It exists for two reasons. The first is production: every file this installer
5
+ * touches belongs to a host that may be RUNNING while we write — Claude Code
6
+ * rewrites `~/.claude.json` on its own schedule — so the write is staged beside
7
+ * the target and renamed over it, which is the closest thing to atomic either
8
+ * host family offers. A reader never sees a half-written config.
9
+ *
10
+ * The second is proof. The read-back check (BLI-2541) is only worth something if
11
+ * a test can make the stored bytes differ from the written ones; injecting this
12
+ * interface is how `memory-install.test.ts` proves `read_back_mismatch` fires
13
+ * instead of asserting that it would.
14
+ */
15
+ import { chmod, mkdir, readFile, rename, stat, unlink, writeFile, } from "node:fs/promises";
16
+ import path from "node:path";
17
+ import { randomBytes } from "node:crypto";
18
+ import { describeError, isMissingFileFailure } from "../health-detail.js";
19
+ export function defaultMemoryFileIo() {
20
+ return {
21
+ async readText(file) {
22
+ try {
23
+ return await readFile(file, "utf8");
24
+ }
25
+ catch (error) {
26
+ if (isMissingFileFailure(error))
27
+ return null;
28
+ throw error;
29
+ }
30
+ },
31
+ async writeText(file, contents) {
32
+ await mkdir(path.dirname(file), { recursive: true });
33
+ const staged = `${file}.${randomBytes(6).toString("hex")}.tmp`;
34
+ // A staged file is created with the process umask and the rename carries
35
+ // that mode onto the target — so a `~/.claude.json` or `~/.codex/config.toml`
36
+ // the person keeps at 0600 would come out 0644, quietly widened by an
37
+ // installer that was only meant to add one key. Carry the existing mode
38
+ // across; a file that does not exist yet keeps the umask default.
39
+ const existingMode = await stat(file)
40
+ .then((info) => info.mode & 0o777)
41
+ .catch(() => null);
42
+ try {
43
+ await writeFile(staged, contents, "utf8");
44
+ if (existingMode !== null)
45
+ await chmod(staged, existingMode);
46
+ await rename(staged, file);
47
+ }
48
+ catch (error) {
49
+ // A rename that will not land (a locked destination on Windows, a
50
+ // cross-device staging directory) must not lose the write — and it must
51
+ // say which of the two happened, because "install failed" on a config
52
+ // file is exactly the silent failure the fleet contract forbids.
53
+ console.error("[memory-install] staged write could not be renamed; writing in place", JSON.stringify({
54
+ reason: "staged_rename_failed",
55
+ byte_size: contents.length,
56
+ ...describeError(error),
57
+ }));
58
+ await unlink(staged).catch(() => undefined);
59
+ await writeFile(file, contents, "utf8");
60
+ }
61
+ },
62
+ };
63
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * The Codex skill `cockpit memory install` writes to
3
+ * `~/.codex/skills/bli-memory/` (BLI-3580).
4
+ *
5
+ * It replaces the role of the four vendor skills a Codex machine carries today
6
+ * (`supermemory-search`, `-save`, `-forget`, `-login`): one discoverable skill
7
+ * directory with `SKILL.md` as the index and one reference per operation —
8
+ * search, save, update, forget. There is no login reference because there is no
9
+ * fourth credential: the MCP server authenticates with the collector's own
10
+ * device token, the one `cockpit login` already put on the machine.
11
+ *
12
+ * Content is static prose. It is written whole and compared whole, so an
13
+ * install is idempotent to the byte and a drifted copy is replaced rather than
14
+ * merged — the same deal `cockpit agent-rules` offers for its managed block.
15
+ */
16
+ /** Relative path → exact file contents. The map IS the install. */
17
+ export function memoryCodexSkillFiles() {
18
+ return {
19
+ "SKILL.md": SKILL_MD,
20
+ "references/search.md": SEARCH_MD,
21
+ "references/save.md": SAVE_MD,
22
+ "references/update.md": UPDATE_MD,
23
+ "references/forget.md": FORGET_MD,
24
+ };
25
+ }
26
+ const SKILL_MD = `---
27
+ name: bli-memory
28
+ description: BLI Memory — durable memory for this machine. Use when you need to recall what was decided before, or when a session produced a durable decision, preference or correction worth keeping.
29
+ ---
30
+
31
+ # BLI Memory
32
+
33
+ Durable memory lives in BLI's own store, reached through the \`bli-memory\` MCP
34
+ server. Four tools, and nothing else:
35
+
36
+ | Tool | When |
37
+ |---|---|
38
+ | \`search_memory\` | before answering anything that depends on earlier work |
39
+ | \`save_memory\` | a durable decision, preference, correction or constraint appeared |
40
+ | \`update_memory\` | a saved memory is now wrong and has a successor |
41
+ | \`forget_memory\` | a saved memory should never surface again |
42
+
43
+ Rules that do not bend:
44
+
45
+ - **Recall before you assert.** An empty result means the record is silent, not
46
+ that nothing happened. Say which one you are looking at.
47
+ - **Save the decision, not the transcript.** The nightly rollups already
48
+ distil sessions; this store is for the durable line a person would repeat.
49
+ - **Never fuzzy-match when forgetting.** Exact id, or exact text inside one
50
+ container. Deleting the wrong memory is worse than keeping a stale one.
51
+ - **Say what you did.** A save that produced an id says so; a search that
52
+ answered from memory credits it.
53
+
54
+ References: \`references/search.md\`, \`references/save.md\`,
55
+ \`references/update.md\`, \`references/forget.md\`.
56
+
57
+ Installed and kept current by \`cockpit memory install\`. Do not edit by hand —
58
+ the next sync tick replaces it.
59
+ `;
60
+ const SEARCH_MD = `# search_memory
61
+
62
+ \`\`\`
63
+ in : { query: string (1-1000, required),
64
+ containerTag?: string, // omitted -> this machine's default container
65
+ limit?: 1-20 = 5,
66
+ includeProfile?: boolean = true }
67
+ \`\`\`
68
+
69
+ Returns relevance-ranked memories plus, when \`includeProfile\`, the container's
70
+ most recent context. Rows that were forgotten or superseded are unreachable by
71
+ construction and never come back.
72
+
73
+ An empty answer is a finding: report "the record is silent on this", never
74
+ "there is nothing".
75
+ `;
76
+ const SAVE_MD = `# save_memory
77
+
78
+ \`\`\`
79
+ in : { content: string (1-200000, required),
80
+ containerTag?: string,
81
+ customId?: string, // e.g. the session id — the upsert key
82
+ metadata?: object }
83
+ out: { id, containerTag, created|updated, chars }
84
+ \`\`\`
85
+
86
+ Stores what it is given; it does not summarise. Pass \`customId\` when a session
87
+ may save more than once, so the second save updates the first row instead of
88
+ adding a duplicate.
89
+
90
+ Save the durable thing: a decision and its reason, a preference, a correction, a
91
+ constraint discovered the hard way. Do not save the whole transcript.
92
+ `;
93
+ const UPDATE_MD = `# update_memory
94
+
95
+ \`\`\`
96
+ in : { id: uuid (required), content: string (required), metadata?: object }
97
+ out: { newId, supersededId, containerTag, reEmbedded: true }
98
+ \`\`\`
99
+
100
+ An update is an insert plus a link: the new text becomes a new row, the old one
101
+ is marked superseded and stops being reachable from recall while staying
102
+ readable for audit.
103
+
104
+ It refuses — loudly, never silently — when the id is unknown, already forgotten,
105
+ or already superseded. The last case names the successor so you can retry
106
+ against it.
107
+ `;
108
+ const FORGET_MD = `# forget_memory
109
+
110
+ \`\`\`
111
+ in : { id?: uuid, content?: string, containerTag?: string, reason?: string }
112
+ exactly one of id | content; containerTag is REQUIRED with content
113
+ out: { forgotten: [{ id, containerTag, textPreview, created_at }], count }
114
+ \`\`\`
115
+
116
+ Soft delete: the row is marked forgotten and never surfaces through recall
117
+ again. Matching by content is exact after whitespace normalisation — never
118
+ fuzzy, and never widened to every container.
119
+
120
+ It returns what it removed. A bare "done" is not an answer.
121
+ `;