@north-light/crouter 0.3.220 → 0.3.222

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 (271) hide show
  1. package/dist/api/client.d.ts +29 -0
  2. package/dist/api/client.js +44 -0
  3. package/dist/api/dto/chat-inventory.d.ts +56 -0
  4. package/dist/api/dto/chat-inventory.js +11 -0
  5. package/dist/api/dto/human-requests.d.ts +88 -0
  6. package/dist/api/dto/human-requests.js +4 -0
  7. package/dist/api/dto/human.d.ts +3 -0
  8. package/dist/api/dto/profiles.d.ts +19 -5
  9. package/dist/api/dto/profiles.js +2 -1
  10. package/dist/api/dto/reviews.d.ts +2 -0
  11. package/dist/api/index.d.ts +2 -0
  12. package/dist/api/index.js +2 -0
  13. package/dist/api/routes.d.ts +8 -0
  14. package/dist/api/routes.js +11 -0
  15. package/dist/build-root.d.ts +2 -6
  16. package/dist/build-root.js +51 -4
  17. package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
  18. package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
  19. package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
  20. package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
  21. package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
  22. package/dist/builtin-memory/04-base-worker.md +4 -8
  23. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  24. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
  25. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
  26. package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
  27. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
  28. package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
  29. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
  30. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
  31. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
  32. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
  33. package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
  34. package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
  35. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
  36. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
  37. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
  38. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
  39. package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
  40. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
  41. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
  42. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
  43. package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
  44. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
  45. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
  46. package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
  47. package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
  48. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
  49. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
  50. package/dist/builtin-memory/design/guide.md +35 -0
  51. package/dist/builtin-memory/design/roadmap.md +21 -0
  52. package/dist/builtin-memory/insights/capture.md +1 -1
  53. package/dist/builtin-memory/internal/agent-shaping.md +3 -1
  54. package/dist/builtin-memory/internal/memory-loading.md +4 -0
  55. package/dist/builtin-memory/internal/plugins.md +10 -1
  56. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  57. package/dist/builtin-memory/plan/roadmap.md +6 -22
  58. package/dist/builtin-memory/spec/guide.md +23 -6
  59. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
  60. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
  61. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +34 -16
  62. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
  63. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
  64. package/dist/clients/attach/overlays/file-review.js +2 -2
  65. package/dist/clients/attach/render/markdown-source.js +106 -1
  66. package/dist/clients/attach/session/file-links.d.ts +13 -4
  67. package/dist/clients/attach/session/file-links.js +54 -58
  68. package/dist/clients/attach/session/keys.d.ts +1 -1
  69. package/dist/clients/attach/session/profile-files.js +1 -1
  70. package/dist/clients/attach/viewer.js +698 -696
  71. package/dist/clients/inbox/controller.js +1 -1
  72. package/dist/clients/inbox/resolve.d.ts +1 -0
  73. package/dist/clients/inbox/review/launch.d.ts +8 -4
  74. package/dist/clients/inbox/review/launch.js +55 -5
  75. package/dist/clients/inbox/review/review-client.d.ts +1 -0
  76. package/dist/clients/inbox/review/review-client.js +7 -1
  77. package/dist/clients/inbox/review-adapter.d.ts +1 -8
  78. package/dist/clients/inbox/review-adapter.js +4 -52
  79. package/dist/commands/__tests__/human.test.js +2 -2
  80. package/dist/commands/human/request.d.ts +2 -0
  81. package/dist/commands/human/request.js +281 -0
  82. package/dist/commands/human.js +5 -2
  83. package/dist/commands/memory/lint.js +2 -1
  84. package/dist/commands/memory/read.js +1 -0
  85. package/dist/commands/memory.js +1 -1
  86. package/dist/commands/pkg/market-manage.js +165 -75
  87. package/dist/commands/pkg/plugin-inspect.js +19 -2
  88. package/dist/commands/pkg/plugin-manage.d.ts +8 -3
  89. package/dist/commands/pkg/plugin-manage.js +72 -24
  90. package/dist/commands/profile/default.js +6 -10
  91. package/dist/commands/profile/list.js +5 -3
  92. package/dist/commands/profile/new.js +21 -8
  93. package/dist/commands/profile/project.js +25 -19
  94. package/dist/commands/profile/show.js +3 -3
  95. package/dist/commands/surface-inbox.js +1 -0
  96. package/dist/commands/sys/__tests__/migrate.test.js +16 -5
  97. package/dist/commands/sys/config.js +2 -2
  98. package/dist/commands/sys/doctor.js +87 -5
  99. package/dist/commands/sys/migrate.js +38 -19
  100. package/dist/commands/sys/setup-core.js +1 -1
  101. package/dist/commands/sys/sync-project-guidance.js +1 -1
  102. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
  103. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
  104. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
  105. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
  106. package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
  107. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
  108. package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
  109. package/dist/core/__tests__/human-action-delivery.test.js +140 -0
  110. package/dist/core/__tests__/human-actions.test.d.ts +1 -0
  111. package/dist/core/__tests__/human-actions.test.js +116 -0
  112. package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
  113. package/dist/core/__tests__/profile-project-memory-delivery.test.d.ts +1 -0
  114. package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
  115. package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
  116. package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
  117. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
  118. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
  119. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
  120. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
  121. package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
  122. package/dist/core/bootstrap.js +6 -0
  123. package/dist/core/canvas/browse/app.js +5 -2
  124. package/dist/core/canvas/browse/model.d.ts +25 -15
  125. package/dist/core/canvas/browse/model.js +86 -65
  126. package/dist/core/canvas/db.js +23 -0
  127. package/dist/core/canvas/human-deliveries.d.ts +53 -0
  128. package/dist/core/canvas/human-deliveries.js +75 -0
  129. package/dist/core/canvas/render-source.d.ts +6 -0
  130. package/dist/core/canvas/render-source.js +7 -1
  131. package/dist/core/canvas/render.js +10 -2
  132. package/dist/core/command-hooks/artifact.d.ts +10 -0
  133. package/dist/core/command-hooks/artifact.js +129 -0
  134. package/dist/core/command-hooks/catalog.d.ts +14 -0
  135. package/dist/core/command-hooks/catalog.js +38 -0
  136. package/dist/core/command-hooks/compose.d.ts +15 -0
  137. package/dist/core/command-hooks/compose.js +99 -0
  138. package/dist/core/command-hooks/discovery.d.ts +87 -0
  139. package/dist/core/command-hooks/discovery.js +174 -0
  140. package/dist/core/command-hooks/help.d.ts +5 -0
  141. package/dist/core/command-hooks/help.js +18 -0
  142. package/dist/core/command-hooks/index.d.ts +6 -0
  143. package/dist/core/command-hooks/index.js +6 -0
  144. package/dist/core/command-hooks/report.d.ts +23 -0
  145. package/dist/core/command-hooks/report.js +19 -0
  146. package/dist/core/command-hooks/schema.d.ts +27 -0
  147. package/dist/core/command-hooks/schema.js +68 -0
  148. package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
  149. package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
  150. package/dist/core/command-plugins/presence.d.ts +2 -0
  151. package/dist/core/command-plugins/presence.js +17 -0
  152. package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
  153. package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
  154. package/dist/core/command.d.ts +8 -1
  155. package/dist/core/command.js +12 -10
  156. package/dist/core/config.d.ts +13 -1
  157. package/dist/core/config.js +51 -1
  158. package/dist/core/feed/inbox.d.ts +6 -0
  159. package/dist/core/feed/inbox.js +9 -1
  160. package/dist/core/help.d.ts +7 -1
  161. package/dist/core/human/action-binding.d.ts +21 -0
  162. package/dist/core/human/action-binding.js +40 -0
  163. package/dist/core/human/completion.d.ts +38 -0
  164. package/dist/core/human/completion.js +27 -0
  165. package/dist/core/human/convention.d.ts +2 -0
  166. package/dist/core/human/convention.js +2 -0
  167. package/dist/core/human/tickets.d.ts +25 -6
  168. package/dist/core/human/tickets.js +19 -13
  169. package/dist/core/human/types.d.ts +5 -0
  170. package/dist/core/human-actions.d.ts +25 -0
  171. package/dist/core/human-actions.js +101 -0
  172. package/dist/core/io.d.ts +9 -1
  173. package/dist/core/io.js +44 -2
  174. package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
  175. package/dist/core/memory/inline-ref-inventory.js +15 -8
  176. package/dist/core/memory-resolver.d.ts +13 -1
  177. package/dist/core/memory-resolver.js +26 -20
  178. package/dist/core/profiles/manifest.d.ts +13 -2
  179. package/dist/core/profiles/manifest.js +84 -18
  180. package/dist/core/profiles/select.d.ts +2 -0
  181. package/dist/core/profiles/select.js +29 -12
  182. package/dist/core/render.js +11 -0
  183. package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
  184. package/dist/core/runtime/advertised-command-invocation.js +233 -0
  185. package/dist/core/runtime/bearings.js +1 -1
  186. package/dist/core/runtime/broker/event-projection.js +7 -0
  187. package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
  188. package/dist/core/runtime/broker/frame-dispatch.js +13 -11
  189. package/dist/core/runtime/broker/read-ops.d.ts +4 -0
  190. package/dist/core/runtime/broker/read-ops.js +6 -2
  191. package/dist/core/runtime/broker-extension-render.js +1 -1
  192. package/dist/core/runtime/broker-inventory.d.ts +4 -0
  193. package/dist/core/runtime/broker-inventory.js +116 -0
  194. package/dist/core/runtime/broker-persona-guidance.js +1 -1
  195. package/dist/core/runtime/broker-protocol.d.ts +9 -2
  196. package/dist/core/runtime/broker.js +10 -1
  197. package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
  198. package/dist/core/runtime/chat-inventory-rows.js +105 -0
  199. package/dist/core/runtime/command-surface.d.ts +38 -0
  200. package/dist/core/runtime/command-surface.js +117 -0
  201. package/dist/core/runtime/launch-target.d.ts +25 -0
  202. package/dist/core/runtime/launch-target.js +54 -0
  203. package/dist/core/runtime/node-read.js +5 -0
  204. package/dist/core/runtime/persona.js +3 -3
  205. package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
  206. package/dist/core/runtime/prospective-inventory-cli.js +61 -0
  207. package/dist/core/runtime/prospective-inventory.d.ts +10 -0
  208. package/dist/core/runtime/prospective-inventory.js +88 -0
  209. package/dist/core/runtime/spawn.d.ts +3 -1
  210. package/dist/core/runtime/spawn.js +5 -3
  211. package/dist/core/scope.d.ts +26 -1
  212. package/dist/core/scope.js +52 -12
  213. package/dist/core/substrate/on-read.d.ts +7 -1
  214. package/dist/core/substrate/on-read.js +30 -32
  215. package/dist/core/substrate/render-node.d.ts +3 -2
  216. package/dist/core/substrate/render-node.js +3 -2
  217. package/dist/core/substrate/render.js +65 -24
  218. package/dist/core/substrate/schema.d.ts +16 -2
  219. package/dist/core/substrate/schema.js +14 -5
  220. package/dist/core/user-settings.d.ts +4 -0
  221. package/dist/core/user-settings.js +1 -0
  222. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +56 -7
  223. package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
  224. package/dist/daemon/api/handlers/chat-inventory.js +25 -0
  225. package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
  226. package/dist/daemon/api/handlers/human-requests.js +409 -0
  227. package/dist/daemon/api/handlers/human.js +3 -0
  228. package/dist/daemon/api/handlers/inbox.js +3 -0
  229. package/dist/daemon/api/handlers/nodes.d.ts +1 -3
  230. package/dist/daemon/api/handlers/nodes.js +11 -46
  231. package/dist/daemon/api/handlers/profiles.js +7 -1
  232. package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
  233. package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
  234. package/dist/daemon/api/handlers/reviews.js +10 -2
  235. package/dist/daemon/api/map.d.ts +2 -1
  236. package/dist/daemon/api/map.js +3 -2
  237. package/dist/daemon/api/server.js +6 -0
  238. package/dist/daemon/crtrd.js +6 -0
  239. package/dist/daemon/human/deliver-action.d.ts +16 -0
  240. package/dist/daemon/human/deliver-action.js +168 -0
  241. package/dist/daemon/human/finish.d.ts +8 -5
  242. package/dist/daemon/human/finish.js +45 -6
  243. package/dist/daemon/human/sweep.js +4 -1
  244. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
  245. package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
  246. package/dist/daemon/review/finish.d.ts +8 -3
  247. package/dist/daemon/review/finish.js +19 -1
  248. package/dist/hook-authoring.d.ts +75 -0
  249. package/dist/hook-authoring.js +358 -0
  250. package/dist/hook-process.d.ts +7 -0
  251. package/dist/hook-process.js +34 -0
  252. package/dist/index.d.ts +2 -0
  253. package/dist/index.js +2 -0
  254. package/dist/migrations/002-profile-project-memory.d.ts +2 -0
  255. package/dist/migrations/002-profile-project-memory.js +71 -0
  256. package/dist/migrations/profile-manifests.d.ts +30 -0
  257. package/dist/migrations/profile-manifests.js +70 -0
  258. package/dist/migrations/registry.js +10 -5
  259. package/dist/migrations/types.d.ts +28 -1
  260. package/dist/migrations/types.js +15 -9
  261. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
  262. package/dist/pi-extensions/canvas-structured-output.js +85 -2
  263. package/dist/types.d.ts +23 -6
  264. package/dist/types.js +1 -0
  265. package/package.json +1 -1
  266. package/runtime.lock.json +2 -2
  267. package/dist/builtin-memory/00-runtime-base.md +0 -55
  268. package/dist/builtin-memory/design.md +0 -55
  269. package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
  270. /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
  271. /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/fixtures/memory-slash-live-probe.d.ts} +0 -0
@@ -14,6 +14,7 @@ import { resetScopeCache } from '../scope.js';
14
14
  import { clearSessionCache } from '../substrate/session-cache.js';
15
15
  import { memoryDir as nodeMemoryDir } from '../runtime/memory.js';
16
16
  import { buildRefInventory } from '../memory/inline-ref-inventory.js';
17
+ import { gatewayMemoryRows } from '../runtime/chat-inventory-rows.js';
17
18
  // ---------------------------------------------------------------------------
18
19
  // Wave C4 — buildRefInventory: the authoritative corpus builder. Fixture-
19
20
  // backed (node/project/user scope docs on disk), since precedence dedup,
@@ -47,6 +48,21 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
47
48
  function nodeScopeMemoryDir() {
48
49
  return nodeMemoryDir(NODE_ID);
49
50
  }
51
+ function plantNorthlightVisibilityExtension() {
52
+ const manifestDir = join(userHome, '.crouter', 'plugins', 'northlight', '.crouter-plugin');
53
+ mkdirSync(manifestDir, { recursive: true });
54
+ writeFileSync(join(manifestDir, 'plugin.json'), JSON.stringify({
55
+ name: 'northlight',
56
+ memory_extensions: {
57
+ visibility: {
58
+ type: 'boolean',
59
+ default: true,
60
+ write_help: 'Control chat visibility.',
61
+ edit_help: 'Control chat visibility.',
62
+ },
63
+ },
64
+ }));
65
+ }
50
66
  before(() => {
51
67
  prevCanvasHome = process.env['CRTR_HOME'];
52
68
  prevUserHome = process.env['HOME'];
@@ -148,12 +164,30 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
148
164
  // A sibling doc in the same corpus is unaffected.
149
165
  assert.ok(names.has('crtr-inline-ref-fixture-sibling'));
150
166
  });
151
- test('refs are metadata-only: exactly {name, kind, scope, shortForm}, no body/path', () => {
167
+ test('refs are metadata-only: exactly {name, kind, scope, shortForm, gatewayVisible}, no body/path', () => {
152
168
  writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-meta.md', "kind: knowledge\nshort-form: 'hook'");
153
169
  const { refs } = buildRefInventory();
154
170
  const ref = refs.find((r) => r.name === 'crtr-inline-ref-fixture-meta');
155
171
  assert.ok(ref);
156
- assert.deepEqual(Object.keys(ref).sort(), ['kind', 'name', 'scope', 'shortForm']);
172
+ assert.deepEqual(Object.keys(ref).sort(), ['gatewayVisible', 'kind', 'name', 'scope', 'shortForm']);
173
+ });
174
+ test('Gateway eligibility excludes builtins and effective Northlight-hidden refs without changing resolution', () => {
175
+ plantNorthlightVisibilityExtension();
176
+ writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-hidden.md', 'kind: knowledge\nextensions:\n northlight:\n visibility: false');
177
+ writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-visible.md', 'kind: knowledge');
178
+ const { refs, names } = buildRefInventory();
179
+ const hidden = refs.find((ref) => ref.name === 'crtr-inline-ref-fixture-hidden');
180
+ const visible = refs.find((ref) => ref.name === 'crtr-inline-ref-fixture-visible');
181
+ assert.equal(hidden?.gatewayVisible, false);
182
+ assert.equal(visible?.gatewayVisible, true);
183
+ assert.ok(names.has('crtr-inline-ref-fixture-hidden'));
184
+ assert.ok(names.has('crtr-inline-ref-fixture-visible'));
185
+ assert.ok(refs.some((ref) => ref.scope === 'builtin'));
186
+ assert.ok(refs.filter((ref) => ref.scope === 'builtin').every((ref) => !ref.gatewayVisible));
187
+ const chatRows = gatewayMemoryRows(refs);
188
+ assert.ok(!chatRows.some((ref) => ref.name === 'crtr-inline-ref-fixture-hidden'));
189
+ assert.ok(chatRows.some((ref) => ref.name === 'crtr-inline-ref-fixture-visible'));
190
+ assert.ok(!chatRows.some((ref) => ref.scope === 'builtin'));
157
191
  });
158
192
  test('a malformed doc (no valid kind) is omitted from the inventory', () => {
159
193
  writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-malformed.md', "short-form: 'no kind here'");
@@ -0,0 +1,217 @@
1
+ // A selected profile's project relationship caps AUTOMATIC delivery — boot and
2
+ // workspace-open — and nothing else. The reported failure this suite exists for:
3
+ // a profile project that is also the node's cwd rendered its authored rung, as
4
+ // if the relationship applied only to projects reached through the manifest.
5
+ //
6
+ // One real fixture drives the real scope resolver, the real boot renderer, the
7
+ // real workspace-open renderer, and the real `crtr memory read` leaf: a profile
8
+ // whose four projects sit at none/name/preview/content in manifest order, with
9
+ // the `preview` project as cwd and canonical-name collisions across projects and
10
+ // against the wider user scope.
11
+ //
12
+ // Migration convergence is NOT here — it lives in migration.test.ts.
13
+ //
14
+ // Run: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/profile-project-memory-delivery.test.ts
15
+ import { test, before, beforeEach, after } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
18
+ import { tmpdir } from 'node:os';
19
+ import { dirname, join } from 'node:path';
20
+ import { closeDb } from '../canvas/db.js';
21
+ import { resetScopeCache } from '../scope.js';
22
+ import { createProfile } from '../profiles/manifest.js';
23
+ import { spawnNode } from '../runtime/nodes.js';
24
+ import { clearSessionCache } from '../substrate/session-cache.js';
25
+ import { renderKnowledgeBlock } from '../substrate/render-node.js';
26
+ import { renderWorkspaceOpenDocs } from '../substrate/on-read-node.js';
27
+ import { deliveredAtOrAbove, loadInjectedDocs, saveInjectedDocs, } from '../substrate/injected-store.js';
28
+ import { readLeaf } from '../../commands/memory/read.js';
29
+ const B = {
30
+ pnone: 'BODY-T9-PNONE',
31
+ pname: 'BODY-T9-PNAME',
32
+ ppreview: 'BODY-T9-PPREVIEW',
33
+ pcontent: 'BODY-T9-PCONTENT',
34
+ sharedFromPnone: 'BODY-T9-SHARED-FROM-PNONE',
35
+ sharedFromPcontent: 'BODY-T9-SHARED-FROM-PCONTENT',
36
+ nameFromPname: 'BODY-T9-NAMEDUP-FROM-PNAME',
37
+ nameFromPcontent: 'BODY-T9-NAMEDUP-FROM-PCONTENT',
38
+ userDupFromPnone: 'BODY-T9-USERDUP-FROM-PNONE',
39
+ userDupFromUser: 'BODY-T9-USERDUP-FROM-USER',
40
+ };
41
+ let home;
42
+ let canvasHome;
43
+ let profileId;
44
+ let prevHome;
45
+ let prevCanvasHome;
46
+ let prevProfile;
47
+ let prevNode;
48
+ let prevCwd;
49
+ let cwdProject;
50
+ let ppreviewDocPath;
51
+ /** A doc delivered automatically at the top rung by both automatic channels —
52
+ * every cap observed below is the relationship's, never the author's. */
53
+ function autoDoc(when, body) {
54
+ return [
55
+ '---',
56
+ 'kind: knowledge',
57
+ `when-and-why-to-read: "${when}"`,
58
+ 'surfaces:',
59
+ ' - {on: boot, at: content}',
60
+ ' - {on: workspace-open, at: content}',
61
+ '---',
62
+ '',
63
+ body,
64
+ '',
65
+ ].join('\n');
66
+ }
67
+ /** No surfaces at all: invisible at boot, counted into its dir's `[+N more]`. */
68
+ function bootlessDoc(when, body) {
69
+ return ['---', 'kind: knowledge', `when-and-why-to-read: "${when}"`, '---', '', body, ''].join('\n');
70
+ }
71
+ function writeDoc(storeDir, relName, contents) {
72
+ const path = join(storeDir, `${relName}.md`);
73
+ mkdirSync(dirname(path), { recursive: true });
74
+ writeFileSync(path, contents);
75
+ return path;
76
+ }
77
+ before(() => {
78
+ prevHome = process.env['HOME'];
79
+ prevCanvasHome = process.env['CRTR_HOME'];
80
+ prevProfile = process.env['CRTR_PROFILE_ID'];
81
+ prevNode = process.env['CRTR_NODE_ID'];
82
+ prevCwd = process.cwd();
83
+ // realpath: the manifest owner persists realpath-resolved project paths. A raw
84
+ // /var/folders path on macOS makes the cwd ancestor walk and the profile
85
+ // pointer name two different roots, and the cwd project then reads as uncapped
86
+ // for the wrong reason.
87
+ home = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-ppm-home-')));
88
+ canvasHome = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-ppm-canvas-')));
89
+ process.env['HOME'] = home;
90
+ process.env['CRTR_HOME'] = canvasHome;
91
+ const store = (slug) => {
92
+ const dir = join(home, slug, '.crouter', 'memory');
93
+ mkdirSync(dir, { recursive: true });
94
+ return dir;
95
+ };
96
+ const pnone = store('pnone');
97
+ const pname = store('pname');
98
+ const ppreview = store('ppreview');
99
+ const pcontent = store('pcontent');
100
+ const user = join(home, '.crouter', 'memory');
101
+ mkdirSync(user, { recursive: true });
102
+ writeDoc(pnone, 't9-pnone-doc', autoDoc('When the none project delivers', B.pnone));
103
+ writeDoc(pname, 't9-pname-doc', autoDoc('When the name project delivers', B.pname));
104
+ ppreviewDocPath = writeDoc(ppreview, 't9-ppreview-doc', autoDoc('When the cwd project delivers', B.ppreview));
105
+ writeDoc(pcontent, 't9-pcontent-doc', autoDoc('When the content project delivers', B.pcontent));
106
+ // Collision one: `none` (earlier in manifest order) vs `content`. The cap must
107
+ // apply before name dedupe, so the dropped entry cannot shadow the winner.
108
+ writeDoc(pnone, 't9-shared-doc', autoDoc('When the shared name resolves', B.sharedFromPnone));
109
+ writeDoc(pcontent, 't9-shared-doc', autoDoc('When the shared name resolves', B.sharedFromPcontent));
110
+ // Collision two: `name` vs `content`. A non-none cap keeps its project's
111
+ // precedence and discloses at the capped rung — it does not step aside.
112
+ writeDoc(pname, 't9-namedup-doc', autoDoc('When the name-capped duplicate resolves', B.nameFromPname));
113
+ writeDoc(pcontent, 't9-namedup-doc', autoDoc('When the name-capped duplicate resolves', B.nameFromPcontent));
114
+ // Collision three: `none` vs the wider user scope, which project precedence
115
+ // would otherwise beat.
116
+ writeDoc(pnone, 't9-userdup-doc', autoDoc('When the user duplicate resolves', B.userDupFromPnone));
117
+ writeDoc(user, 't9-userdup-doc', autoDoc('When the user duplicate resolves', B.userDupFromUser));
118
+ // Hidden-count pair: a top-level dir always renders, so a dir holding only a
119
+ // bootless doc shows as `<dir>/` + `[+1 more]` — unless its project is `none`.
120
+ writeDoc(pnone, join('t9-none-hidden', 'probe'), bootlessDoc('When counted', 'HIDDEN-NONE'));
121
+ writeDoc(pcontent, join('t9-hidden-control', 'probe'), bootlessDoc('When counted', 'HIDDEN-CONTROL'));
122
+ profileId = createProfile('t9', [
123
+ { path: join(home, 'pnone'), memory: 'none' },
124
+ { path: join(home, 'pname'), memory: 'name' },
125
+ { path: join(home, 'ppreview'), memory: 'preview' },
126
+ { path: join(home, 'pcontent'), memory: 'content' },
127
+ ]).profileId;
128
+ process.env['CRTR_PROFILE_ID'] = profileId;
129
+ // The node works INSIDE a capped project: the reported failure.
130
+ cwdProject = join(home, 'ppreview');
131
+ process.chdir(cwdProject);
132
+ });
133
+ beforeEach(() => {
134
+ resetScopeCache();
135
+ clearSessionCache();
136
+ });
137
+ after(() => {
138
+ closeDb();
139
+ resetScopeCache();
140
+ clearSessionCache();
141
+ process.chdir(prevCwd);
142
+ rmSync(home, { recursive: true, force: true });
143
+ rmSync(canvasHome, { recursive: true, force: true });
144
+ const restore = (key, value) => {
145
+ if (value === undefined)
146
+ delete process.env[key];
147
+ else
148
+ process.env[key] = value;
149
+ };
150
+ restore('HOME', prevHome);
151
+ restore('CRTR_HOME', prevCanvasHome);
152
+ restore('CRTR_PROFILE_ID', prevProfile);
153
+ restore('CRTR_NODE_ID', prevNode);
154
+ });
155
+ function newNode() {
156
+ return spawnNode({ kind: 'general', cwd: cwdProject, parent: null, profile_id: profileId }).node_id;
157
+ }
158
+ test('boot delivery caps every profile project at its relationship, including the one used as cwd', () => {
159
+ const boot = renderKnowledgeBlock(newNode(), new Map());
160
+ // The control: an uncapped relationship still renders its authored content.
161
+ assert.ok(boot.includes(B.pcontent), `the content-capped project renders its body; got:\n${boot}`);
162
+ // The reported failure. cwd reaches this root before the manifest pointer
163
+ // does, and the relationship still decides the ceiling.
164
+ assert.ok(!boot.includes(B.ppreview), `the cwd project is capped at preview — its body must not render; got:\n${boot}`);
165
+ assert.match(boot, /t9-ppreview-doc {2}# read when: When the cwd project delivers\./, `the cwd project's doc discloses at preview; got:\n${boot}`);
166
+ // `name` discloses the bare identifier and nothing else.
167
+ assert.ok(!boot.includes(B.pname), 'a name-capped project must not render a body');
168
+ assert.match(boot, /[├└]─ t9-pname-doc$/m, `the name-capped doc discloses as a bare name; got:\n${boot}`);
169
+ assert.ok(!boot.includes('When the name project delivers'), 'a name-capped doc discloses no preview line');
170
+ });
171
+ test('a `none` project contributes nothing to boot — no body, no name, no hidden count, no shadow', () => {
172
+ const boot = renderKnowledgeBlock(newNode(), new Map());
173
+ assert.ok(!boot.includes(B.pnone), 'a none-capped body must not render');
174
+ assert.ok(!boot.includes('t9-pnone-doc'), 'a none-capped doc must not appear as a name either');
175
+ // Hidden counts are disclosure too: the control dir proves a bootless doc
176
+ // does surface as a `[+N more]` count, so the none project's absence is the cap.
177
+ assert.match(boot, /t9-hidden-control\/\n.*\[\+1 more\]/, `the control dir shows its hidden count; got:\n${boot}`);
178
+ assert.ok(!boot.includes('t9-none-hidden'), 'a none-capped project contributes no `[+N more]` count');
179
+ // Shadowing: `none` wins name dedupe on precedence, so a cap applied after
180
+ // dedupe would silently delete the winner instead of deferring to it.
181
+ assert.ok(boot.includes(B.sharedFromPcontent) && !boot.includes(B.sharedFromPnone), `the content project wins the collision the none project would have taken; got:\n${boot}`);
182
+ assert.ok(boot.includes(B.userDupFromUser) && !boot.includes(B.userDupFromPnone), `a none project does not shadow the wider user-scope winner; got:\n${boot}`);
183
+ });
184
+ test('a non-none cap keeps its project precedence and discloses the duplicate at the capped rung', () => {
185
+ const boot = renderKnowledgeBlock(newNode(), new Map());
186
+ assert.match(boot, /[├└]─ t9-namedup-doc$/m, `the name-capped duplicate wins dedupe; got:\n${boot}`);
187
+ assert.ok(!boot.includes(B.nameFromPname), 'the winner discloses at `name`, never its body');
188
+ assert.ok(!boot.includes(B.nameFromPcontent), 'the content project does not take a collision the name project already won');
189
+ });
190
+ test('workspace-open clamps each project store the same way boot does', () => {
191
+ const opened = renderWorkspaceOpenDocs(newNode(), new Map());
192
+ assert.ok(opened.includes(B.pcontent), `the content-capped project delivers its body; got:\n${opened}`);
193
+ assert.ok(!opened.includes(B.ppreview), `the cwd project must not deliver its body; got:\n${opened}`);
194
+ assert.match(opened, /name="t9-ppreview-doc" readWhen="When the cwd project delivers\./, `the cwd project delivers its preview line; got:\n${opened}`);
195
+ assert.match(opened, /name="t9-pname-doc"[^>]*\/>/, `the name-capped doc delivers name-only; got:\n${opened}`);
196
+ assert.ok(!opened.includes('t9-pnone-doc'), 'the none-capped project delivers nothing at workspace-open');
197
+ assert.ok(!opened.includes(B.sharedFromPnone), 'nor its side of a name collision');
198
+ });
199
+ test('an explicit `crtr memory read` returns and records the full body a cap withheld', async () => {
200
+ const node = newNode();
201
+ process.env['CRTR_NODE_ID'] = node;
202
+ const seen = loadInjectedDocs(node);
203
+ const opened = renderWorkspaceOpenDocs(node, seen);
204
+ assert.ok(!opened.includes(B.ppreview), 'workspace-open disclosed the capped doc at preview only');
205
+ saveInjectedDocs(node, seen);
206
+ const result = (await readLeaf.run({ name: 't9-ppreview-doc', frontmatter: false }));
207
+ assert.equal(result.path, ppreviewDocPath, 'the read resolved the cwd project doc');
208
+ assert.ok(result.content.includes(B.ppreview), `an explicit read is uncapped and returns the full body; got:\n${result.content}`);
209
+ const afterRead = loadInjectedDocs(node);
210
+ assert.ok(deliveredAtOrAbove(afterRead, ppreviewDocPath, 'content'), 'the explicit read records a content delivery, upgrading the preview disclosure');
211
+ // A later automatic event runs at the capped rung, which is now below what the
212
+ // transcript holds, so it re-delivers nothing.
213
+ assert.equal(renderWorkspaceOpenDocs(node, afterRead), '', 'a later lower automatic event re-delivers nothing');
214
+ // ...and the emptiness is the dedup set, not a dead render.
215
+ assert.ok(renderWorkspaceOpenDocs(node, new Map()).includes('t9-ppreview-doc'), 'the same call against a fresh transcript still delivers');
216
+ delete process.env['CRTR_NODE_ID'];
217
+ });
@@ -0,0 +1,91 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { DefaultResourceLoader, ExtensionRunner, } from '@earendil-works/pi-coding-agent';
7
+ import { captureChatCapabilities, registrationChatSurface } from '../runtime/command-surface.js';
8
+ test('startup capability capture preserves same-name lifecycle replacements and excludes new lifecycle registrations', async () => {
9
+ const root = mkdtempSync(join(tmpdir(), 'crtr-prospective-capability-'));
10
+ const agentDir = join(root, '.pi');
11
+ const invoked = join(root, 'late-invoked');
12
+ mkdirSync(agentDir);
13
+ const first = join(root, 'first.js');
14
+ const second = join(root, 'second.js');
15
+ writeFileSync(first, `
16
+ import { writeFileSync } from 'node:fs';
17
+ export default function (pi) {
18
+ pi.registerCommand('collision', {
19
+ description: 'first factory command',
20
+ gateway: true,
21
+ argumentHint: '<first>',
22
+ handler: async () => {},
23
+ });
24
+ pi.on('session_start', () => {
25
+ pi.registerCommand('collision', {
26
+ description: 'lifecycle replacement',
27
+ argumentHint: '<replacement>',
28
+ handler: async () => {},
29
+ });
30
+ pi.registerCommand('late', {
31
+ description: 'lifecycle command',
32
+ gateway: true,
33
+ handler: async () => writeFileSync(${JSON.stringify(invoked)}, 'yes'),
34
+ });
35
+ });
36
+ }
37
+ `);
38
+ writeFileSync(second, `
39
+ export default function (pi) {
40
+ pi.registerCommand('collision', {
41
+ description: 'second factory command',
42
+ gateway: true,
43
+ argumentHint: '<second>',
44
+ handler: async () => {},
45
+ });
46
+ }
47
+ `);
48
+ try {
49
+ const loader = new DefaultResourceLoader({
50
+ cwd: root,
51
+ agentDir,
52
+ additionalExtensionPaths: [first, second],
53
+ noContextFiles: true,
54
+ });
55
+ await loader.reload();
56
+ const loaded = loader.getExtensions();
57
+ assert.deepEqual(loaded.errors, []);
58
+ const runner = new ExtensionRunner(loaded.extensions, loaded.runtime, root, undefined, undefined);
59
+ const startupCommands = runner.getRegisteredCommands();
60
+ captureChatCapabilities({ commands: startupCommands, templates: [] });
61
+ const startupRows = startupCommands.map((command) => ({
62
+ name: command.invocationName,
63
+ ...registrationChatSurface(command),
64
+ }));
65
+ assert.deepEqual(startupRows, [
66
+ { name: 'collision:1', gatewayCapable: true, argumentHint: '<first>' },
67
+ { name: 'collision:2', gatewayCapable: true, argumentHint: '<second>' },
68
+ ]);
69
+ for (const extension of loaded.extensions) {
70
+ for (const handler of extension.handlers.get('session_start') ?? []) {
71
+ await handler({ type: 'session_start' }, {});
72
+ }
73
+ }
74
+ const reloadedRows = runner.getRegisteredCommands().map((command) => ({
75
+ name: command.invocationName,
76
+ ...registrationChatSurface(command),
77
+ }));
78
+ assert.deepEqual(reloadedRows, [
79
+ { name: 'collision:1', gatewayCapable: true, argumentHint: '<replacement>' },
80
+ { name: 'late', gatewayCapable: false },
81
+ { name: 'collision:2', gatewayCapable: true, argumentHint: '<second>' },
82
+ ]);
83
+ const late = runner.getCommand('late');
84
+ assert.ok(late, 'the lifecycle command remains registered and addressable');
85
+ await late.handler('', {});
86
+ assert.equal(existsSync(invoked), true, 'the lifecycle command remains invokable');
87
+ }
88
+ finally {
89
+ rmSync(root, { recursive: true, force: true });
90
+ }
91
+ });
@@ -0,0 +1,127 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ import { spawn } from 'node:child_process';
7
+ import { test } from 'node:test';
8
+ import { loadedCommandPreview } from '../../../clients/attach/slash/prompt-preview.js';
9
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../../..');
10
+ const EXTENSION = join(ROOT, 'dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts');
11
+ const LIVE_PROBE = join(ROOT, 'dist/core/__tests__/fixtures/memory-slash-live-probe.js');
12
+ const COMMAND_NAMES = ['acceptance-context-token', 'acceptance-node-token', 'acceptance-independent'];
13
+ function writeSlashDoc(memoryDir, name, description, body) {
14
+ writeFileSync(join(memoryDir, `${name}.md`), `---\nkind: knowledge\nwhen-and-why-to-read: Acceptance fixture.\nshort-form: ${description}\nslash: true\n---\n${body}`);
15
+ }
16
+ function liveProbe(input) {
17
+ return new Promise((resolve, reject) => {
18
+ const child = spawn(process.execPath, [LIVE_PROBE, EXTENSION, JSON.stringify(input.invocations)], {
19
+ cwd: input.cwd,
20
+ env: {
21
+ ...process.env,
22
+ CRTR_HOME: input.home,
23
+ CRTR_NODE_ID: input.nodeId,
24
+ CRTR_PROFILE_ID: '',
25
+ PI_CODING_AGENT_DIR: join(input.cwd, '.pi'),
26
+ },
27
+ stdio: ['ignore', 'pipe', 'pipe', 'pipe'],
28
+ });
29
+ let stdout = '';
30
+ let stderr = '';
31
+ let response = '';
32
+ const stdoutStream = child.stdout;
33
+ const stderrStream = child.stderr;
34
+ const responseStream = child.stdio[3];
35
+ if (stdoutStream === null || stderrStream === null || responseStream === null) {
36
+ child.kill('SIGKILL');
37
+ reject(new Error('live probe response channel unavailable'));
38
+ return;
39
+ }
40
+ stdoutStream.setEncoding('utf8');
41
+ stderrStream.setEncoding('utf8');
42
+ responseStream.setEncoding('utf8');
43
+ stdoutStream.on('data', (chunk) => { stdout += chunk; });
44
+ stderrStream.on('data', (chunk) => { stderr += chunk; });
45
+ responseStream.on('data', (chunk) => { response += chunk; });
46
+ child.on('error', reject);
47
+ child.on('close', (code, signal) => {
48
+ if (code !== 0) {
49
+ reject(new Error(`live probe exited ${code ?? signal ?? 'unknown'}: ${stderr}${stdout}`));
50
+ return;
51
+ }
52
+ resolve(JSON.parse(response));
53
+ });
54
+ });
55
+ }
56
+ function fixtureRows(rows) {
57
+ return COMMAND_NAMES.map((name) => {
58
+ const row = rows.find((candidate) => candidate.name === name);
59
+ assert.ok(row, `Gateway advertises /${name}`);
60
+ return row;
61
+ });
62
+ }
63
+ test('node-relative memory slash commands omit preview metadata without changing live sends', async () => {
64
+ const cwd = mkdtempSync(join(tmpdir(), 'crtr-memory-slash-inventory-'));
65
+ const home = join(cwd, 'canvas');
66
+ const memoryDir = join(cwd, '.crouter', 'memory');
67
+ const nodeId = 'acceptance-live-node';
68
+ mkdirSync(memoryDir, { recursive: true });
69
+ writeSlashDoc(memoryDir, COMMAND_NAMES[0], 'Context-relative fixture.', 'Store $ARGUMENTS in $CRTR_CONTEXT_DIR/result.md.\n');
70
+ writeSlashDoc(memoryDir, COMMAND_NAMES[1], 'Node-relative fixture.', 'Node $CRTR_NODE_ID received $ARGUMENTS.\n');
71
+ writeSlashDoc(memoryDir, COMMAND_NAMES[2], 'Node-independent fixture.', 'Independent payload: $ARGUMENTS.\n');
72
+ try {
73
+ const live = await liveProbe({
74
+ cwd,
75
+ home,
76
+ nodeId,
77
+ invocations: COMMAND_NAMES.map((name) => ({ name, args: 'evidence' })),
78
+ });
79
+ assert.equal(live.stateBeforeProspective, false);
80
+ assert.equal(live.stateAfterProspective, false);
81
+ assert.deepEqual(live.commands, live.prospective.commands);
82
+ assert.deepEqual(live.memoryRefs, live.prospective.memory_refs);
83
+ const prospectiveRows = fixtureRows(live.prospective.commands);
84
+ const liveRows = fixtureRows(live.commands);
85
+ assert.deepEqual(liveRows, prospectiveRows);
86
+ assert.deepEqual(prospectiveRows, [
87
+ { name: COMMAND_NAMES[0], description: 'Context-relative fixture.', source: 'command' },
88
+ { name: COMMAND_NAMES[1], description: 'Node-relative fixture.', source: 'command' },
89
+ {
90
+ name: COMMAND_NAMES[2],
91
+ description: 'Node-independent fixture.',
92
+ source: 'command',
93
+ expansion: {
94
+ kind: 'memory-slash',
95
+ commandName: COMMAND_NAMES[2],
96
+ body: 'Independent payload: $ARGUMENTS.\n',
97
+ },
98
+ },
99
+ ]);
100
+ const insightsInit = live.commands.find((row) => row.name === 'insights:init');
101
+ assert.ok(insightsInit, 'Gateway completion includes /insights:init');
102
+ assert.equal(insightsInit.expansion, undefined);
103
+ assert.equal(loadedCommandPreview('/insights:init agent-facing CLI', live.commands), undefined);
104
+ assert.equal(loadedCommandPreview('/acceptance-independent first evidence', live.commands)?.content, 'Independent payload: first evidence.\n');
105
+ assert.equal(loadedCommandPreview('/acceptance-independent changed evidence', live.commands)?.content, 'Independent payload: changed evidence.\n');
106
+ assert.deepEqual(live.notifications, []);
107
+ const sentContent = Object.fromEntries(live.sent.map((send) => [send.name, send.message.content]));
108
+ assert.deepEqual(sentContent, {
109
+ [COMMAND_NAMES[0]]: `Store evidence in ${join(home, 'nodes', nodeId, 'context', 'result.md')}.\n`,
110
+ [COMMAND_NAMES[1]]: `Node ${nodeId} received evidence.\n`,
111
+ [COMMAND_NAMES[2]]: 'Independent payload: evidence.\n',
112
+ });
113
+ for (const send of live.sent) {
114
+ assert.deepEqual(send.message, {
115
+ customType: 'memory-slash-command',
116
+ content: sentContent[send.name],
117
+ display: true,
118
+ });
119
+ assert.deepEqual(send.options, { triggerTurn: true });
120
+ }
121
+ assert.doesNotMatch(sentContent[COMMAND_NAMES[0]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
122
+ assert.doesNotMatch(sentContent[COMMAND_NAMES[1]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
123
+ }
124
+ finally {
125
+ rmSync(cwd, { recursive: true, force: true });
126
+ }
127
+ });
@@ -0,0 +1,31 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { test } from 'node:test';
6
+ import { runProspectiveInventory } from '../../runtime/prospective-inventory.js';
7
+ test('prospective preflight keeps package stdout out of its response channel', async () => {
8
+ const cwd = mkdtempSync(join(tmpdir(), 'crtr-prospective-stdout-'));
9
+ const extension = join(cwd, 'loud-extension.mjs');
10
+ writeFileSync(extension, `
11
+ export default function (pi) {
12
+ console.log('harmless package diagnostic');
13
+ pi.registerCommand('stdout-probe', {
14
+ description: 'survives package stdout',
15
+ gateway: true,
16
+ handler: async () => {},
17
+ });
18
+ }
19
+ `);
20
+ try {
21
+ const rows = await runProspectiveInventory({ cwd, profileId: null, extensions: [extension] });
22
+ assert.deepEqual(rows.commands.find((row) => row.name === 'stdout-probe'), {
23
+ name: 'stdout-probe',
24
+ description: 'survives package stdout',
25
+ source: 'command',
26
+ });
27
+ }
28
+ finally {
29
+ rmSync(cwd, { recursive: true, force: true });
30
+ }
31
+ });