@cairnkeep/cli 2.17.0 → 2.17.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,28 @@ All notable user-facing changes are documented here. This project follows
5
5
 
6
6
  ## [Unreleased]
7
7
 
8
+ ## [2.17.2] - 2026-09-02
9
+
10
+ ### Fixed
11
+
12
+ - Generate the project Codex memory entry as `mcp_servers.cairn-memory-local`
13
+ instead of `cairn-memory`: Codex merges the user-wide and project MCP server
14
+ tables by id, so a project stdio entry collided with a user-wide remote
15
+ (url) entry of the same id and Codex >= 0.152 refused to start ("url is not
16
+ supported for stdio"). `cairn doctor` now diagnoses that legacy conflict
17
+ with a targeted recovery, and still accepts the old id on machines without
18
+ a user-wide remote entry.
19
+
20
+ ## [2.17.1] - 2026-08-31
21
+
22
+ ### Fixed
23
+
24
+ - Release TTY stdin after every interactive `cairn setup` selector so setup
25
+ returns to the shell after completion, including through SSH terminals that
26
+ report stdin as already flowing.
27
+ - Keep the context-usage receipt smoke fixture relative to the test run so its
28
+ doctor assertion cannot fail merely because a hard-coded timestamp aged out.
29
+
8
30
  ## [2.17.0] - 2026-08-29
9
31
 
10
32
  ### Added
package/docs/ecosystem.md CHANGED
@@ -14,7 +14,6 @@ graph workflows have their own explicit companion requirements.
14
14
 
15
15
  | Tool | What it adds | Integration |
16
16
  |---|---|---|
17
- | [lean-ctx](https://github.com/yvgude/lean-ctx) | Compressed file reads, searches, shell output, and directory maps | Register it as an MCP server in the chosen harness |
18
17
  | [token-miser](https://github.com/cairnkeep/token-miser) | Model routing and compact codebase exploration | Configure `CAIRN_ROUTE_ENDPOINT` and/or `CAIRN_EXPLORE_BINARY` |
19
18
  | [rtk](https://github.com/rtk-ai/rtk) | Token-reduced output for common Git, npm, and Cargo commands | Install as a shell-level proxy; Cairnkeep wiring is not required |
20
19
 
@@ -229,11 +229,16 @@ Native Windows uses `.\.ai\start-codex.cmd`. The generated
229
229
  `.codex/config.toml` contains only:
230
230
 
231
231
  ```toml
232
- [mcp_servers.cairn-memory]
232
+ [mcp_servers.cairn-memory-local]
233
233
  command = "cairn"
234
234
  args = ["memory-server"]
235
235
  ```
236
236
 
237
+ The id is `cairn-memory-local`, not `cairn-memory`: Codex merges the user-wide
238
+ and project `mcp_servers` tables by id, so a project stdio entry named
239
+ `cairn-memory` collides with a user-wide remote (url) entry of the same id and
240
+ Codex >= 0.152 refuses to start.
241
+
237
242
  Review this file before accepting Codex's project-trust prompt. Cairnkeep does
238
243
  not grant trust, edit the user-wide Codex configuration, or start Codex during
239
244
  setup. If an operator-owned `.codex/config.toml` already differs, setup reports
@@ -252,10 +257,10 @@ writes during setup. An operator who has explicitly authorized a bounded,
252
257
  unattended workflow may add narrow per-tool policy to the isolated Codex config:
253
258
 
254
259
  ```toml
255
- [mcp_servers.cairn-memory.tools.memory_write]
260
+ [mcp_servers.cairn-memory-local.tools.memory_write]
256
261
  approval_mode = "approve"
257
262
 
258
- [mcp_servers.cairn-memory.tools.memory_supersede]
263
+ [mcp_servers.cairn-memory-local.tools.memory_supersede]
259
264
  approval_mode = "approve"
260
265
  ```
261
266
 
@@ -1,6 +1,6 @@
1
1
  # Curriculum Coverage Map
2
2
 
3
- **Baseline:** Cairnkeep 2.17.0
3
+ **Baseline:** Cairnkeep 2.17.2
4
4
  **Last reviewed:** 2026-08-19
5
5
 
6
6
  This map prevents the course from drifting behind the product. It assigns each
@@ -1,6 +1,6 @@
1
1
  # Cairnkeep feature guide
2
2
 
3
- **Baseline:** Cairnkeep 2.17.0
3
+ **Baseline:** Cairnkeep 2.17.2
4
4
 
5
5
  This is the step-back map for choosing and operating Cairnkeep features. The
6
6
  [course labs](https://github.com/cairnkeep/cairnkeep-course-labs) provide one
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 10 minutes
6
- **Tested with:** Cairnkeep 2.17.0
6
+ **Tested with:** Cairnkeep 2.17.2
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 15 minutes
6
- **Tested with:** Cairnkeep 2.17.0
6
+ **Tested with:** Cairnkeep 2.17.2
7
7
 
8
8
  ## Outcome
9
9
 
@@ -26,7 +26,7 @@ ephemeral; the named course volume is persistent.
26
26
  1. Pull the public image:
27
27
 
28
28
  ```bash
29
- podman pull ghcr.io/cairnkeep/cairnkeep:2.17.0
29
+ podman pull ghcr.io/cairnkeep/cairnkeep:2.17.1
30
30
  ```
31
31
 
32
32
  2. Start the stdio server with a read-only root filesystem and no Linux
@@ -38,7 +38,7 @@ ephemeral; the named course volume is persistent.
38
38
  --read-only --cap-drop=all --security-opt=no-new-privileges \
39
39
  --tmpfs=/tmp:rw,noexec,nosuid,size=64m,mode=1777 \
40
40
  --volume cairnkeep-course-data:/data:Z,U \
41
- ghcr.io/cairnkeep/cairnkeep:2.17.0 stdio
41
+ ghcr.io/cairnkeep/cairnkeep:2.17.1 stdio
42
42
  ```
43
43
 
44
44
  3. The process waits quietly for MCP input. Press `Ctrl-C` after confirming it
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 25 minutes
6
- **Tested with:** Cairnkeep 2.17.0 and Node.js 22 or newer
6
+ **Tested with:** Cairnkeep 2.17.2 and Node.js 22 or newer
7
7
 
8
8
  ## Outcome
9
9
 
@@ -47,7 +47,7 @@ sync. Codex project trust remains an explicit operator decision.
47
47
  2. Install Cairnkeep:
48
48
 
49
49
  ```bash
50
- npm install --global @cairnkeep/cli@2.17.0
50
+ npm install --global @cairnkeep/cli@2.17.1
51
51
  cairn --version
52
52
  ```
53
53
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart
5
5
  **Time:** 25 minutes
6
- **Tested with:** Cairnkeep 2.17.0
6
+ **Tested with:** Cairnkeep 2.17.2
7
7
 
8
8
  ## Outcome
9
9
 
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Evidence and Evaluation
5
5
  **Time:** 50 minutes
6
- **Tested with:** Cairnkeep 2.17.0
6
+ **Tested with:** Cairnkeep 2.17.2
7
7
 
8
8
  ## Outcome
9
9
 
@@ -1,7 +1,7 @@
1
1
  # L20 - Least-authority MCP tool profiles
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.17.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.17.2 and Node.js 22 or newer
5
5
  **Time:** 25 minutes
6
6
 
7
7
  ## Outcome
@@ -1,7 +1,7 @@
1
1
  # L21 - Immutable context packs and skill approval
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.17.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.17.2 and Node.js 22 or newer
5
5
  **Time:** 40 minutes
6
6
 
7
7
  ## Outcome
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Operator
5
5
  **Time:** 30 minutes
6
- **Tested with:** Cairnkeep 2.17.0, Windows x64, and Node.js 22 or newer
6
+ **Tested with:** Cairnkeep 2.17.2, Windows x64, and Node.js 22 or newer
7
7
 
8
8
  ## Outcome
9
9
 
@@ -16,7 +16,7 @@ perform a reversible uninstall without relying on WSL or Git Bash.
16
16
  From PowerShell:
17
17
 
18
18
  ```powershell
19
- npm install --global @cairnkeep/cli@2.17.0
19
+ npm install --global @cairnkeep/cli@2.17.1
20
20
  cairn version
21
21
 
22
22
  $Lab = Join-Path $env:TEMP 'Cairnkeep Windows Lab'
@@ -3,7 +3,7 @@
3
3
  **Status:** Ready
4
4
  **Track:** Quickstart and Operator
5
5
  **Time:** 35 minutes
6
- **Tested with:** Cairnkeep 2.17.0; Pi 0.84.1; Node.js 22, 24, and 26
6
+ **Tested with:** Cairnkeep 2.17.2; Pi 0.84.1; Node.js 22, 24, and 26
7
7
 
8
8
  ## Outcome
9
9
 
@@ -1,7 +1,7 @@
1
1
  # L24 - Reviewed knowledge exchange
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.17.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.17.2 and Node.js 22 or newer
5
5
  **Time:** 35 minutes
6
6
 
7
7
  ## Outcome
@@ -1,7 +1,7 @@
1
1
  # L25 - Bounded workflow playbooks
2
2
 
3
3
  **Status:** Ready
4
- **Tested with:** Cairnkeep 2.17.0 and Node.js 22 or newer
4
+ **Tested with:** Cairnkeep 2.17.2 and Node.js 22 or newer
5
5
  **Time:** 35 minutes
6
6
 
7
7
  ## Outcome
@@ -54,7 +54,7 @@ cairn doctor
54
54
  Review `.codex\config.toml` and accept Codex's project-trust prompt. Setup does
55
55
  not edit the user-wide Codex configuration or grant trust. It preserves a
56
56
  different existing project configuration and doctor verifies a manually merged
57
- `mcp_servers.cairn-memory` table.
57
+ `mcp_servers.cairn-memory-local` table.
58
58
 
59
59
  Setup and bootstrap write both the established Unix launchers and native Windows
60
60
  launchers. Each `.cmd` file enters `start-harness.ps1`, loads `.ai/.env`, changes
package/docs/operating.md CHANGED
@@ -127,7 +127,7 @@ cairn doctor
127
127
 
128
128
  Review `.codex/config.toml` and accept Codex's project-trust prompt before use.
129
129
  An existing different file is preserved and reported as skipped; merge the
130
- generated `mcp_servers.cairn-memory` table into it, then run `cairn doctor`.
130
+ generated `mcp_servers.cairn-memory-local` table into it, then run `cairn doctor`.
131
131
  Uninstall leaves that operator-owned file intact. Selecting
132
132
  `--memory none` creates the launcher but deliberately omits the MCP entry.
133
133
 
@@ -22,16 +22,19 @@ The wizard recommends the existing repository mode when the target is already
22
22
  a Git work tree. On native Windows, launch with `.\.ai\start-codex.cmd`.
23
23
 
24
24
  Before the first session, review `.codex/config.toml` and accept Codex's project
25
- trust prompt. The generated entry runs `cairn memory-server` locally. Setup
26
- does not edit the user-wide Codex configuration and does not enable networking,
27
- telemetry, embeddings, session capture, or context packs.
25
+ trust prompt. The generated entry runs `cairn memory-server` locally under the
26
+ server id `cairn-memory-local`: Codex merges the user-wide and project
27
+ `mcp_servers` tables by id, so a project entry named `cairn-memory` collides
28
+ with a user-wide remote (url) entry of the same id and Codex >= 0.152 refuses
29
+ to start. Setup does not edit the user-wide Codex configuration and does not
30
+ enable networking, telemetry, embeddings, session capture, or context packs.
28
31
 
29
32
  If `.codex/config.toml` already exists and differs, setup reports it as
30
33
  `skipped` and preserves it byte-for-byte. Merge this table into the existing
31
34
  file, then run `cairn doctor`:
32
35
 
33
36
  ```toml
34
- [mcp_servers.cairn-memory]
37
+ [mcp_servers.cairn-memory-local]
35
38
  command = "cairn"
36
39
  args = ["memory-server"]
37
40
  ```
@@ -1 +1 @@
1
- {"identifier":"cairn-offline-fake-v1","package_version":"2.17.0","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
1
+ {"identifier":"cairn-offline-fake-v1","package_version":"2.17.2","schema_version":1,"task_set_digest":"13e95379f35839593047ec34521a65a8fa3ab92bd3993bca9dc8bf5c4a9ff36f"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cairnkeep/cli",
3
- "version": "2.17.0",
3
+ "version": "2.17.2",
4
4
  "description": "A durable, harness-agnostic memory + context layer for coding agents and MCP clients.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -73,6 +73,6 @@
73
73
  "rag"
74
74
  ],
75
75
  "devDependencies": {
76
- "@cyclonedx/cyclonedx-npm": "6.0.0"
76
+ "@cyclonedx/cyclonedx-npm": "6.0.1"
77
77
  }
78
78
  }
package/scripts/setup.mjs CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  readFileSync,
11
11
  } from "node:fs";
12
12
  import { createHash } from "node:crypto";
13
+ import { homedir } from "node:os";
13
14
  import { join, resolve } from "node:path";
14
15
  import { createInterface } from "node:readline/promises";
15
16
  import { stdin as defaultInput, stdout as defaultOutput, stderr as defaultError } from "node:process";
@@ -353,33 +354,63 @@ function readPrivateState(path) {
353
354
  }
354
355
  }
355
356
 
356
- function hasCodexMemoryConfig(project) {
357
- const path = join(project, ".codex", "config.toml");
358
- if (!existsSync(path)) return false;
357
+ function readTomlConfig(path) {
358
+ if (!existsSync(path)) return null;
359
359
  const info = lstatSync(path);
360
- if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) return false;
360
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 1024 * 1024) return null;
361
361
  let descriptor;
362
- let text;
363
362
  try {
364
363
  descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
365
364
  const opened = fstatSync(descriptor);
366
- if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino || opened.size > 1024 * 1024) return false;
367
- text = readFileSync(descriptor, "utf8");
365
+ if (!opened.isFile() || opened.dev !== info.dev || opened.ino !== info.ino || opened.size > 1024 * 1024) return null;
366
+ return readFileSync(descriptor, "utf8");
368
367
  } finally {
369
368
  if (descriptor !== undefined) closeSync(descriptor);
370
369
  }
370
+ }
371
+
372
+ function codexMemorySection(text, id) {
371
373
  const lines = text.split(/\r?\n/);
372
374
  const headers = lines.flatMap((line, index) => (
373
- ["[mcp_servers.cairn-memory]", "[mcp_servers.\"cairn-memory\"]"].includes(line.trim()) ? [index] : []
375
+ [`[mcp_servers.${id}]`, `[mcp_servers."${id}"]`].includes(line.trim()) ? [index] : []
374
376
  ));
375
- if (headers.length !== 1) return false;
377
+ if (headers.length !== 1) return null;
376
378
  const [start] = headers;
377
379
  const next = lines.findIndex((line, index) => index > start && /^\s*\[[^\]]+\]\s*$/.test(line));
378
- const section = lines.slice(start + 1, next < 0 ? lines.length : next).join("\n");
380
+ return lines.slice(start + 1, next < 0 ? lines.length : next).join("\n");
381
+ }
382
+
383
+ function isLocalMemorySection(section) {
379
384
  return (section.match(/^\s*command\s*=\s*["']cairn["']\s*$/gm) ?? []).length === 1
380
385
  && (section.match(/^\s*args\s*=\s*\[\s*["']memory-server["']\s*\]\s*$/gm) ?? []).length === 1;
381
386
  }
382
387
 
388
+ function hasCodexMemoryConfig(project) {
389
+ const text = readTomlConfig(join(project, ".codex", "config.toml"));
390
+ if (text === null) return false;
391
+ // `cairn-memory-local` is the current template id; the bare `cairn-memory`
392
+ // stdio form is accepted so pre-existing setups still diagnose complete.
393
+ return ["cairn-memory-local", "cairn-memory"].some((id) => {
394
+ const section = codexMemorySection(text, id);
395
+ return section !== null && isLocalMemorySection(section);
396
+ });
397
+ }
398
+
399
+ // Codex merges the user-wide and project mcp_servers tables field by field, so
400
+ // a legacy project stdio `cairn-memory` collides with a user-wide remote (url)
401
+ // entry of the same id and Codex >= 0.152 refuses to start at all.
402
+ function hasCodexRemoteMemoryConflict(project) {
403
+ const projectText = readTomlConfig(join(project, ".codex", "config.toml"));
404
+ if (projectText === null) return false;
405
+ const legacy = codexMemorySection(projectText, "cairn-memory");
406
+ if (legacy === null || !isLocalMemorySection(legacy)) return false;
407
+ const userHome = process.env.CODEX_HOME || join(homedir(), ".codex");
408
+ const userText = readTomlConfig(join(userHome, "config.toml"));
409
+ if (userText === null) return false;
410
+ const remote = codexMemorySection(userText, "cairn-memory");
411
+ return remote !== null && /^\s*url\s*=/m.test(remote);
412
+ }
413
+
383
414
  export function diagnoseSetup(target = ".") {
384
415
  const project = resolve(target);
385
416
  const statePath = join(project, ".ai", "cairnkeep.json");
@@ -414,8 +445,11 @@ export function diagnoseSetup(target = ".") {
414
445
  }
415
446
  const requiredAssets = [...COMMON_SETUP_ASSETS, ...requiredHarnessAssetPaths(state.harnesses, state.memory)];
416
447
  if (requiredAssets.some((path) => !Object.hasOwn(state.assets, path))) return incompleteDiagnosis(recovery);
417
- if (state.memory === "local" && state.harnesses.includes("codex") && !hasCodexMemoryConfig(project)) {
418
- return incompleteDiagnosis(recovery);
448
+ if (state.memory === "local" && state.harnesses.includes("codex")) {
449
+ if (!hasCodexMemoryConfig(project)) return incompleteDiagnosis(recovery);
450
+ if (hasCodexRemoteMemoryConflict(project)) {
451
+ return incompleteDiagnosis(`rename the project table to [mcp_servers.cairn-memory-local] in .codex/config.toml (Codex >= 0.152 rejects a project stdio 'cairn-memory' when the user-wide config defines a remote 'cairn-memory'), or delete the file and re-run: ${recovery}`);
452
+ }
419
453
  }
420
454
  return Object.freeze({
421
455
  schema_version: 1,
@@ -37,7 +37,6 @@ async function runKeypressPrompt({ input, output, draw, accept }) {
37
37
  let settled = false;
38
38
  let keypressHandler = null;
39
39
  let rawChanged = false;
40
- let resumed = false;
41
40
  let cursorHidden = false;
42
41
 
43
42
  const render = () => {
@@ -59,7 +58,6 @@ async function runKeypressPrompt({ input, output, draw, accept }) {
59
58
  }
60
59
  if (wasPaused && typeof input.resume === "function") {
61
60
  input.resume();
62
- resumed = true;
63
61
  }
64
62
  output.write("\x1b[?25l");
65
63
  cursorHidden = true;
@@ -92,7 +90,11 @@ async function runKeypressPrompt({ input, output, draw, accept }) {
92
90
  clear();
93
91
  if (cursorHidden) output.write("\x1b[?25h");
94
92
  if (rawChanged) input.setRawMode(false);
95
- if (resumed && typeof input.pause === "function") input.pause();
93
+ // A TTY can report itself as already flowing before the first prompt (for
94
+ // example through an SSH pseudo-terminal). Leaving it flowing keeps Node's
95
+ // event loop alive after setup has printed its final result. Always pause
96
+ // prompt input after removing our listener; the next selector resumes it.
97
+ if (typeof input.pause === "function") input.pause();
96
98
  }
97
99
  }
98
100
 
@@ -57,7 +57,7 @@ const result = JSON.parse(setup.stdout);
57
57
  assert.deepEqual(result.harnesses, ["codex"]);
58
58
  assert.equal(result.machine_sync.command, null);
59
59
  assert.deepEqual(result.launch_commands, [".ai/start-codex.sh"]);
60
- assert.match(readFileSync(join(target, ".codex", "config.toml"), "utf8"), /\[mcp_servers\.cairn-memory\][\s\S]*command = "cairn"[\s\S]*args = \["memory-server"\]/);
60
+ assert.match(readFileSync(join(target, ".codex", "config.toml"), "utf8"), /\[mcp_servers\.cairn-memory-local\][\s\S]*command = "cairn"[\s\S]*args = \["memory-server"\]/);
61
61
 
62
62
  const fakeBin = join(sandbox, "bin");
63
63
  mkdirSync(fakeBin);
@@ -94,7 +94,21 @@ assert.equal(setupModule.diagnoseSetup(collision).status, "incomplete");
94
94
  writeFileSync(join(collision, ".codex", "config.toml"), [
95
95
  "[features]", "fixture = true", "", "[mcp_servers.cairn-memory]", 'command = "cairn"', 'args = ["memory-server"]', "",
96
96
  ].join("\n"));
97
+ // Isolate the user-wide Codex config: the legacy stdio id only conflicts when
98
+ // the user-wide layer defines a remote (url) cairn-memory.
99
+ const codexHome = join(sandbox, "codex-home");
100
+ mkdirSync(codexHome);
101
+ process.env.CODEX_HOME = codexHome;
97
102
  assert.equal(setupModule.diagnoseSetup(collision).status, "complete");
103
+ writeFileSync(join(codexHome, "config.toml"), '[mcp_servers.cairn-memory]\nurl = "http://127.0.0.1:8788/mcp"\n');
104
+ const conflict = setupModule.diagnoseSetup(collision);
105
+ assert.equal(conflict.status, "incomplete");
106
+ assert.match(conflict.recovery[0], /cairn-memory-local/);
107
+ writeFileSync(join(collision, ".codex", "config.toml"), [
108
+ "[features]", "fixture = true", "", "[mcp_servers.cairn-memory-local]", 'command = "cairn"', 'args = ["memory-server"]', "",
109
+ ].join("\n"));
110
+ assert.equal(setupModule.diagnoseSetup(collision).status, "complete");
111
+ delete process.env.CODEX_HOME;
98
112
  const uninstall = spawnSync(join(root, "bin", "cairn"), ["uninstall", "--yes", collision], {
99
113
  encoding: "utf8", shell: false, env: { ...process.env, HOME: join(sandbox, "home") },
100
114
  });
@@ -19,7 +19,7 @@ const manifest = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
19
19
  const lock = JSON.parse(fs.readFileSync(process.argv[3], "utf8"));
20
20
  const candidateWorkflow = fs.readFileSync(process.argv[4], "utf8");
21
21
  const publishWorkflow = fs.readFileSync(process.argv[5], "utf8");
22
- const expected = "6.0.0";
22
+ const expected = "6.0.1";
23
23
 
24
24
  if (manifest.devDependencies?.["@cyclonedx/cyclonedx-npm"] !== expected) {
25
25
  throw new Error(`package.json must pin @cyclonedx/cyclonedx-npm exactly to ${expected}`);
@@ -300,6 +300,19 @@ async function testTerminalSelectors(prompts) {
300
300
  await assert.rejects(cancelled, (error) => error?.status === 130 && /cancelled/i.test(error.message));
301
301
  assert.equal(input.isRaw, false);
302
302
  assert.equal(input.isPaused(), true);
303
+
304
+ const flowingInput = new FakeTTY();
305
+ flowingInput.paused = false;
306
+ const flowing = prompts.selectOnePrompt({
307
+ message: "Release flowing input",
308
+ choices: [{ value: "done", label: "Done" }],
309
+ input: flowingInput,
310
+ output: output.stream,
311
+ });
312
+ setImmediate(() => flowingInput.emit("keypress", "", { name: "return" }));
313
+ assert.equal(await flowing, "done");
314
+ assert.equal(flowingInput.isRaw, false);
315
+ assert.equal(flowingInput.isPaused(), true, "terminal prompt must release flowing stdin so the CLI can exit");
303
316
  }
304
317
 
305
318
  async function testStructuredSetupSelectors(setup, fixture) {
@@ -38,5 +38,4 @@ Scopes defined in `.opencode/memory.json`:
38
38
  ## Tool Roles
39
39
  - AgentFS is the durable structured memory layer.
40
40
  - AnythingLLM is the long-form document retrieval layer.
41
- - `lean-ctx` is the default context layer (reads, search, exploration), not durable project memory.
42
41
  - RTK is a recommended companion for shell-heavy sessions (git, listings, failing builds); Token Savior is not default.
@@ -1,5 +1,10 @@
1
1
  # Cairnkeep project-scoped memory server. Codex loads project configuration only
2
2
  # after the project has been trusted.
3
- [mcp_servers.cairn-memory]
3
+ #
4
+ # The id is `cairn-memory-local`, not `cairn-memory`: Codex merges the user-wide
5
+ # and project mcp_servers tables field by field, so a project stdio entry named
6
+ # `cairn-memory` collides with a user-wide remote (url) entry of the same id and
7
+ # Codex >= 0.152 refuses to start ("url is not supported for stdio").
8
+ [mcp_servers.cairn-memory-local]
4
9
  command = "cairn"
5
10
  args = ["memory-server"]