@north-light/crouter 0.3.227 → 0.3.228

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 (286) hide show
  1. package/dist/api/client.d.ts +5 -5
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/dto/broker-ops.d.ts +8 -0
  4. package/dist/api/dto/memory.d.ts +8 -7
  5. package/dist/api/dto/memory.js +2 -2
  6. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
  7. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
  8. package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
  9. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
  10. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
  11. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
  12. package/dist/builtin-memory/design/guide.md +38 -16
  13. package/dist/builtin-memory/design/roadmap.md +4 -4
  14. package/dist/builtin-memory/insights/init.md +5 -5
  15. package/dist/builtin-memory/internal/INDEX.md +1 -1
  16. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  17. package/dist/builtin-memory/internal/memory-loading.md +6 -6
  18. package/dist/builtin-memory/internal/plugins.md +1 -1
  19. package/dist/builtin-memory/plan/guide.md +53 -0
  20. package/dist/builtin-memory/plan/roadmap.md +10 -8
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
  22. package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
  23. package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
  24. package/dist/clients/attach/chrome/inbox-strip.js +11 -7
  25. package/dist/clients/attach/session/pane-tag.d.ts +28 -2
  26. package/dist/clients/attach/session/pane-tag.js +48 -22
  27. package/dist/clients/attach/session/profile-files.d.ts +6 -0
  28. package/dist/clients/attach/session/profile-files.js +44 -11
  29. package/dist/clients/attach/slash/dispatch.js +4 -7
  30. package/dist/clients/attach/viewer.js +588 -596
  31. package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
  32. package/dist/commands/api-client.d.ts +3 -7
  33. package/dist/commands/api-client.js +7 -12
  34. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
  35. package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
  36. package/dist/commands/memory/delete.js +51 -24
  37. package/dist/commands/memory/edit.js +50 -10
  38. package/dist/commands/memory/find.js +105 -56
  39. package/dist/commands/memory/history.js +51 -31
  40. package/dist/commands/memory/lint.d.ts +0 -9
  41. package/dist/commands/memory/lint.js +78 -322
  42. package/dist/commands/memory/list.d.ts +12 -4
  43. package/dist/commands/memory/list.js +64 -25
  44. package/dist/commands/memory/move.js +103 -73
  45. package/dist/commands/memory/origin.js +35 -5
  46. package/dist/commands/memory/read.d.ts +4 -0
  47. package/dist/commands/memory/read.js +132 -80
  48. package/dist/commands/memory/shared.d.ts +115 -25
  49. package/dist/commands/memory/shared.js +331 -74
  50. package/dist/commands/memory/write.js +72 -21
  51. package/dist/commands/memory.js +2 -2
  52. package/dist/commands/node/lifecycle.js +2 -2
  53. package/dist/commands/pkg/market-manage.js +25 -24
  54. package/dist/commands/pkg/plugin-manage.d.ts +22 -6
  55. package/dist/commands/pkg/plugin-manage.js +118 -25
  56. package/dist/commands/pkg/shared.d.ts +8 -0
  57. package/dist/commands/pkg/shared.js +23 -20
  58. package/dist/commands/surface-reload.d.ts +5 -0
  59. package/dist/commands/surface-reload.js +9 -1
  60. package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
  61. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
  62. package/dist/commands/sys/migrate.js +126 -140
  63. package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
  64. package/dist/commands/sys/panels/profiles-panel.js +599 -0
  65. package/dist/commands/sys/settings-shell.d.ts +4 -2
  66. package/dist/commands/sys/settings-shell.js +35 -4
  67. package/dist/commands/sys/settings.js +21 -5
  68. package/dist/commands/sys/sync-deps.d.ts +2 -0
  69. package/dist/commands/sys/sync-deps.js +26 -15
  70. package/dist/commands/sys/sync-project-guidance.js +228 -145
  71. package/dist/commands/sys/sync-skills.js +28 -17
  72. package/dist/commands/sys/update.js +11 -3
  73. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
  74. package/dist/core/__tests__/config-change-delta.test.js +106 -0
  75. package/dist/core/__tests__/context-intro.test.js +13 -9
  76. package/dist/core/__tests__/daemon-boot.test.js +22 -16
  77. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
  78. package/dist/core/__tests__/human-deliver.test.js +19 -0
  79. package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
  80. package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
  81. package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
  82. package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
  83. package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
  84. package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
  85. package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
  86. package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
  87. package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
  88. package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
  89. package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
  90. package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
  91. package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
  92. package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
  93. package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
  94. package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
  95. package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
  96. package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
  97. package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
  98. package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
  99. package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
  100. package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
  101. package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
  102. package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
  103. package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
  104. package/dist/core/__tests__/kickoff.test.js +16 -5
  105. package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
  106. package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
  107. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  108. package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
  109. package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
  110. package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
  111. package/dist/core/__tests__/repository-association.test.d.ts +1 -0
  112. package/dist/core/__tests__/repository-association.test.js +153 -0
  113. package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
  114. package/dist/core/__tests__/repository-root-identity.test.js +219 -0
  115. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
  116. package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
  117. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
  118. package/dist/core/canvas/canvas.d.ts +3 -0
  119. package/dist/core/canvas/canvas.js +30 -10
  120. package/dist/core/canvas/db.js +28 -1
  121. package/dist/core/canvas/paths.d.ts +4 -6
  122. package/dist/core/canvas/paths.js +8 -6
  123. package/dist/core/canvas/render-source.js +2 -2
  124. package/dist/core/canvas/types.d.ts +9 -13
  125. package/dist/core/exclusive-lock.d.ts +2 -0
  126. package/dist/core/exclusive-lock.js +21 -0
  127. package/dist/core/git.d.ts +0 -1
  128. package/dist/core/git.js +0 -3
  129. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
  130. package/dist/core/human/feedback-companion.js +3 -0
  131. package/dist/core/human/scan.js +8 -1
  132. package/dist/core/keybindings/catalog.d.ts +2 -2
  133. package/dist/core/keybindings/catalog.js +2 -1
  134. package/dist/core/memory/doc-link-grammar.js +2 -2
  135. package/dist/core/memory/history.d.ts +24 -0
  136. package/dist/core/memory/history.js +66 -1
  137. package/dist/core/memory/identity.d.ts +65 -0
  138. package/dist/core/memory/identity.js +185 -0
  139. package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
  140. package/dist/core/memory/inline-ref-guidance.js +1 -1
  141. package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
  142. package/dist/core/memory/inline-ref-inventory.js +30 -62
  143. package/dist/core/memory/lint.d.ts +129 -0
  144. package/dist/core/memory/lint.js +517 -0
  145. package/dist/core/memory/project-namespace.d.ts +31 -0
  146. package/dist/core/memory/project-namespace.js +85 -0
  147. package/dist/core/memory/repository-association.d.ts +31 -0
  148. package/dist/core/memory/repository-association.js +129 -0
  149. package/dist/core/memory/tree.d.ts +39 -0
  150. package/dist/core/memory/tree.js +93 -0
  151. package/dist/core/memory-resolver.d.ts +193 -90
  152. package/dist/core/memory-resolver.js +461 -375
  153. package/dist/core/nested-stores.js +6 -13
  154. package/dist/core/profiles/select.d.ts +4 -1
  155. package/dist/core/profiles/select.js +108 -48
  156. package/dist/core/review/__tests__/capture-origin.test.js +2 -2
  157. package/dist/core/review/__tests__/stage-identity.test.js +2 -2
  158. package/dist/core/review/companion.js +11 -2
  159. package/dist/core/runtime/bearings.d.ts +2 -2
  160. package/dist/core/runtime/bearings.js +3 -3
  161. package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
  162. package/dist/core/runtime/broker/rebind.js +5 -0
  163. package/dist/core/runtime/broker-extension-render.d.ts +7 -3
  164. package/dist/core/runtime/broker-extension-render.js +10 -5
  165. package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
  166. package/dist/core/runtime/broker-persona-guidance.js +122 -27
  167. package/dist/core/runtime/deliver-live.d.ts +16 -4
  168. package/dist/core/runtime/deliver-live.js +29 -15
  169. package/dist/core/runtime/kickoff.d.ts +3 -3
  170. package/dist/core/runtime/kickoff.js +8 -16
  171. package/dist/core/runtime/lifecycle.js +2 -3
  172. package/dist/core/runtime/nodes.d.ts +3 -4
  173. package/dist/core/runtime/nodes.js +3 -4
  174. package/dist/core/runtime/persona.d.ts +8 -12
  175. package/dist/core/runtime/persona.js +24 -96
  176. package/dist/core/runtime/promote.d.ts +2 -2
  177. package/dist/core/runtime/promote.js +8 -21
  178. package/dist/core/runtime/revive.js +20 -17
  179. package/dist/core/runtime/spawn.js +18 -7
  180. package/dist/core/runtime/tmux-bindings.js +2 -3
  181. package/dist/core/runtime/warm-pool.js +3 -4
  182. package/dist/core/scope.js +2 -0
  183. package/dist/core/self-update.d.ts +0 -2
  184. package/dist/core/self-update.js +2 -35
  185. package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
  186. package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
  187. package/dist/core/substrate/index.d.ts +2 -2
  188. package/dist/core/substrate/index.js +1 -1
  189. package/dist/core/substrate/injected-store.d.ts +43 -27
  190. package/dist/core/substrate/injected-store.js +208 -104
  191. package/dist/core/substrate/listings.d.ts +19 -12
  192. package/dist/core/substrate/listings.js +75 -52
  193. package/dist/core/substrate/on-read-node.d.ts +4 -7
  194. package/dist/core/substrate/on-read-node.js +6 -8
  195. package/dist/core/substrate/on-read.d.ts +22 -25
  196. package/dist/core/substrate/on-read.js +103 -147
  197. package/dist/core/substrate/render-node.d.ts +4 -7
  198. package/dist/core/substrate/render-node.js +5 -7
  199. package/dist/core/substrate/render.d.ts +21 -3
  200. package/dist/core/substrate/render.js +291 -223
  201. package/dist/core/substrate/schema.d.ts +1 -13
  202. package/dist/core/substrate/schema.js +5 -40
  203. package/dist/core/substrate/session-cache.d.ts +14 -4
  204. package/dist/core/substrate/session-cache.js +40 -22
  205. package/dist/core/substrate/surface-match.d.ts +9 -7
  206. package/dist/core/substrate/surface-match.js +26 -25
  207. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
  208. package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
  209. package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
  210. package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
  211. package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +21 -5
  212. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  213. package/dist/daemon/api/handlers/memory.js +2 -0
  214. package/dist/daemon/crtrd.js +2 -0
  215. package/dist/daemon/human/finish.js +8 -1
  216. package/dist/daemon/manage.d.ts +0 -1
  217. package/dist/daemon/manage.js +7 -16
  218. package/dist/daemon/startup-policy.d.ts +1 -0
  219. package/dist/daemon/startup-policy.js +1 -0
  220. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  221. package/dist/migrations/002-profile-project-memory.js +1 -0
  222. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  223. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  224. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  225. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  226. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  227. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  228. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  229. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  230. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  231. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  232. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  233. package/dist/migrations/__tests__/activation.test.js +149 -0
  234. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  235. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  236. package/dist/migrations/activation.d.ts +16 -0
  237. package/dist/migrations/activation.js +78 -0
  238. package/dist/migrations/convergent.d.ts +14 -3
  239. package/dist/migrations/convergent.js +21 -10
  240. package/dist/migrations/corpus.d.ts +78 -0
  241. package/dist/migrations/corpus.js +497 -0
  242. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  243. package/dist/migrations/frontmatter-splice.js +176 -0
  244. package/dist/migrations/registry.d.ts +6 -1
  245. package/dist/migrations/registry.js +7 -2
  246. package/dist/migrations/runner.d.ts +41 -0
  247. package/dist/migrations/runner.js +81 -0
  248. package/dist/migrations/types.d.ts +148 -9
  249. package/dist/migrations/types.js +9 -2
  250. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  251. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  252. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  253. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  254. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  255. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  256. package/dist/pi-extensions/canvas-stophook.js +6 -13
  257. package/dist/shared/generated-context.d.ts +0 -3
  258. package/dist/shared/generated-context.js +0 -57
  259. package/dist/shared/tool-groups.js +2 -3
  260. package/package.json +5 -4
  261. package/runtime.lock.json +2 -2
  262. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  263. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  264. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  265. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  266. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  267. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  268. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  269. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  270. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  271. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  272. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  273. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  274. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  275. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  276. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  277. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  278. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  279. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  280. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  281. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  282. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  283. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  284. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  285. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  286. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -42,11 +42,17 @@ function freshNode(nodeId) {
42
42
  }
43
43
  function makeFakePi() {
44
44
  const handlers = {};
45
+ let resolveFirstInjection;
46
+ const firstInjection = new Promise((resolve) => { resolveFirstInjection = resolve; });
45
47
  return {
46
48
  injected: [],
49
+ firstInjection,
47
50
  sentMessages: [],
48
51
  on(e, h) { handlers[e] = h; },
49
- sendUserMessage(content, options) { this.injected.push({ content, deliverAs: options?.deliverAs }); },
52
+ sendUserMessage(content, options) {
53
+ this.injected.push({ content, deliverAs: options?.deliverAs });
54
+ resolveFirstInjection?.();
55
+ },
50
56
  sendMessage(message) { this.sentMessages.push(message); },
51
57
  async fire(e, ev, ctx) { await handlers[e]?.(ev, ctx); },
52
58
  };
@@ -77,6 +83,13 @@ async function shutdown(pi, opts = {}) {
77
83
  await pi.fire('session_shutdown', {}, undefined);
78
84
  }
79
85
  const wait = (ms) => new Promise((r) => setTimeout(r, ms));
86
+ async function waitForFirstInjection(pi, timeoutMs = 2_000) {
87
+ const timeout = new Promise((_, reject) => {
88
+ const timer = setTimeout(() => reject(new Error('watcher did not inject the inbox handoff')), timeoutMs);
89
+ timer.unref();
90
+ });
91
+ await Promise.race([pi.firstInjection, timeout]);
92
+ }
80
93
  // Register the watcher, then fire session_start — the real lifecycle gate
81
94
  // that arms its poll interval and initial tick (see canvas-inbox-watcher.ts).
82
95
  // registerCanvasInboxWatcher() alone only wires up handlers; without this the
@@ -161,7 +174,7 @@ describe('canvas inbox watcher — hold + idle delivery', () => {
161
174
  pi.fire('agent_start', { type: 'agent_start' }, { isIdle: () => false });
162
175
  await wait(TICK_MS + 100);
163
176
  appendInbox('node-refresh', { from: 'child-x', tier: 'urgent', kind: 'final', label: 'done' });
164
- await wait(SETTLE_MS);
177
+ await waitForFirstInjection(pi);
165
178
  assert.equal(pi.injected.length, 1, 'the live broker receives the handoff before its durable cursor boundary');
166
179
  // The cursor operation decides the refresh hold at its durable commit
167
180
  // boundary; clearing intent does not manufacture a second handoff.
@@ -0,0 +1,106 @@
1
+ // A node whose kind/mode/lifecycle changes mid-context gets only memory newly
2
+ // visible or raised above the context's active exposure ranks. Removals stay
3
+ // silent because already loaded context cannot be unseen.
4
+ //
5
+ // The load-bearing case is the gated orchestration kernel: a base → orchestrator
6
+ // change must select its real builtin document through the ordinary renderer.
7
+ //
8
+ // Run: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/config-change-delta.test.ts
9
+ import { test, before, after } from 'node:test';
10
+ import assert from 'node:assert/strict';
11
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
12
+ import { tmpdir } from 'node:os';
13
+ import { join } from 'node:path';
14
+ import { resetScopeCache } from '../scope.js';
15
+ import { clearSessionCache } from '../substrate/session-cache.js';
16
+ import { renderConfigChangeDelta, renderKnowledgeForSubject, renderPreferencesForSubject, } from '../substrate/render.js';
17
+ import { emptyContextExposureState, exposureTarget, } from '../substrate/injected-store.js';
18
+ const NODE = 'n-delta-test';
19
+ const DEV_PREFERENCE = 'BODY-DELTA-DEV-PREFERENCE';
20
+ const DEV_KNOWLEDGE_WHEN = 'When the developer-only guide is routed';
21
+ const KERNEL_MARKER = '## You are an orchestrator';
22
+ let home;
23
+ let work;
24
+ let prevHome;
25
+ let prevCanvasHome;
26
+ let prevProfile;
27
+ let prevCwd;
28
+ function subject(over = {}) {
29
+ return {
30
+ kind: 'general',
31
+ mode: 'base',
32
+ lifecycle: 'terminal',
33
+ hasManager: true,
34
+ cwd: work,
35
+ scope: 'user',
36
+ orchestration: { depth: 1 },
37
+ profile: null,
38
+ ...over,
39
+ };
40
+ }
41
+ /** Everything the node's boot channels render for `s` — the baseline every
42
+ * later delta is diffed against. */
43
+ function seedBoot(state, s) {
44
+ renderPreferencesForSubject(s, exposureTarget(state, 'system'));
45
+ renderKnowledgeForSubject(s, NODE, exposureTarget(state, 'transcript'));
46
+ }
47
+ before(() => {
48
+ prevHome = process.env['HOME'];
49
+ prevCanvasHome = process.env['CRTR_HOME'];
50
+ prevProfile = process.env['CRTR_PROFILE_ID'];
51
+ prevCwd = process.cwd();
52
+ home = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-delta-home-')));
53
+ work = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-delta-work-')));
54
+ process.env['HOME'] = home;
55
+ process.env['CRTR_HOME'] = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-delta-canvas-')));
56
+ delete process.env['CRTR_PROFILE_ID'];
57
+ const user = join(home, '.crouter', 'memory');
58
+ mkdirSync(user, { recursive: true });
59
+ writeFileSync(join(user, 'delta-dev-preference.md'), ['---', 'kind: preference', 'gate: {kind: developer}', 'surfaces:', ' - {on: boot, at: content}', '---', '', DEV_PREFERENCE, ''].join('\n'));
60
+ writeFileSync(join(user, 'delta-dev-knowledge.md'), ['---', 'kind: knowledge', `when-and-why-to-read: "${DEV_KNOWLEDGE_WHEN}"`, 'gate: {kind: developer}', 'surfaces:', ' - {on: boot, at: preview}', '---', '', 'ignored body', ''].join('\n'));
61
+ process.chdir(work);
62
+ resetScopeCache();
63
+ clearSessionCache();
64
+ });
65
+ after(() => {
66
+ process.chdir(prevCwd);
67
+ resetScopeCache();
68
+ rmSync(home, { recursive: true, force: true });
69
+ rmSync(work, { recursive: true, force: true });
70
+ rmSync(process.env['CRTR_HOME'], { recursive: true, force: true });
71
+ if (prevHome === undefined)
72
+ delete process.env['HOME'];
73
+ else
74
+ process.env['HOME'] = prevHome;
75
+ if (prevCanvasHome === undefined)
76
+ delete process.env['CRTR_HOME'];
77
+ else
78
+ process.env['CRTR_HOME'] = prevCanvasHome;
79
+ if (prevProfile !== undefined)
80
+ process.env['CRTR_PROFILE_ID'] = prevProfile;
81
+ });
82
+ test('a config change delivers only what the new configuration newly exposes', () => {
83
+ const state = emptyContextExposureState();
84
+ const transcript = exposureTarget(state, 'transcript');
85
+ const base = subject();
86
+ seedBoot(state, base);
87
+ assert.equal(renderConfigChangeDelta(base, NODE, transcript), '', 'no change → nothing to deliver; boot already showed this configuration');
88
+ // base → orchestrator: the builtin orchestration kernel is gated
89
+ // `{mode: orchestrator}`, so this transition is what unlocks it.
90
+ const promoted = subject({ mode: 'orchestrator' });
91
+ const promoteDelta = renderConfigChangeDelta(promoted, NODE, transcript);
92
+ assert.ok(promoteDelta.includes(KERNEL_MARKER), 'the promote delta carries the builtin orchestration kernel');
93
+ assert.ok(!promoteDelta.includes('Waiting is a way to end a turn'), 'a doc boot already delivered is not repeated');
94
+ assert.equal(renderConfigChangeDelta(promoted, NODE, transcript), '', 'the same change twice delivers nothing the second time');
95
+ // kind flip: a content-rung preference and a preview-rung knowledge doc, each
96
+ // gated on the new kind, in their own blocks.
97
+ const developer = subject({ mode: 'orchestrator', kind: 'developer' });
98
+ const kindDelta = renderConfigChangeDelta(developer, NODE, transcript);
99
+ assert.ok(kindDelta.includes(DEV_PREFERENCE), 'the newly-eligible preference body is delivered');
100
+ assert.ok(kindDelta.includes('<memory kind="preference">'), 'agent-editable preferences ride a preference block');
101
+ assert.ok(kindDelta.includes(`delta-dev-knowledge # read when: ${DEV_KNOWLEDGE_WHEN}`), 'the newly-eligible knowledge doc is routed at its preview rung');
102
+ assert.ok(kindDelta.includes('<memory kind="knowledge">'), 'knowledge rides its own block');
103
+ assert.ok(!kindDelta.includes(KERNEL_MARKER), 'the kernel is not re-delivered — the promote already showed it');
104
+ // Removals are silent, and nothing already shown comes back.
105
+ assert.equal(renderConfigChangeDelta(subject({ mode: 'orchestrator' }), NODE, transcript), '', 'dropping back to the old kind announces nothing — the new prose overrides');
106
+ });
@@ -57,18 +57,22 @@ test('the stored situational card rides the bearings body; absent when unset', (
57
57
  assert.match(block, /<runtime kind="situational">/, 'card present once situational context is set');
58
58
  assert.match(block, /user is looking at the billing applet/, 'card carries the ambient text');
59
59
  });
60
- function makeFakePi() {
60
+ function makeFakePi(entries = []) {
61
61
  return {
62
62
  sent: [],
63
+ entries,
63
64
  renderers: {},
64
65
  on(e, h) { if (e === 'session_start')
65
66
  this.handler = h; },
66
- sendMessage(m) { this.sent.push(m); },
67
+ sendMessage(m) {
68
+ this.sent.push(m);
69
+ this.entries.push({ type: 'custom_message', ...m });
70
+ },
67
71
  registerMessageRenderer(t, r) { this.renderers[t] = r; },
68
72
  };
69
73
  }
70
- function fakeCtx(entries = []) {
71
- return { sessionManager: { getEntries: () => entries } };
74
+ function fakeCtx(pi) {
75
+ return { sessionManager: { getEntries: () => pi.entries } };
72
76
  }
73
77
  test('session_start injects the block as the first message of an empty session', async () => {
74
78
  const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
@@ -78,7 +82,7 @@ test('session_start injects the block as the first message of an empty session',
78
82
  assert.ok(pi.handler, 'session_start handler registered');
79
83
  // Empty session → inject the block as its own custom message (no delivery
80
84
  // options → it precedes the first prompt).
81
- await pi.handler({ reason: 'startup' }, fakeCtx());
85
+ await pi.handler({ reason: 'startup' }, fakeCtx(pi));
82
86
  assert.equal(pi.sent.length, 1, 'one message injected');
83
87
  assert.equal(pi.sent[0].customType, CONTEXT_INTRO_CUSTOM_TYPE);
84
88
  assert.equal(pi.sent[0].display, true);
@@ -92,19 +96,19 @@ test('session_start is idempotent across resume (skips if already in history)',
92
96
  // skips when the present bearings name THIS node (a fork inherits the
93
97
  // SOURCE's bearings, which name a different node — see canvas-context-intro
94
98
  // tests), so a realistic resume entry must carry the real block content.
95
- const pi = makeFakePi();
96
- registerCanvasContextIntro(pi);
97
99
  const entries = [
98
100
  { type: 'custom_message', customType: CONTEXT_INTRO_CUSTOM_TYPE, content: buildContextIntro(meta.node_id) },
99
101
  ];
100
- await pi.handler({ reason: 'resume' }, fakeCtx(entries));
102
+ const pi = makeFakePi(entries);
103
+ registerCanvasContextIntro(pi);
104
+ await pi.handler({ reason: 'resume' }, fakeCtx(pi));
101
105
  assert.equal(pi.sent.length, 0, 'our own block already in history → skip');
102
106
  });
103
107
  test('session_start is inert when CRTR_NODE_ID is absent', async () => {
104
108
  delete process.env['CRTR_NODE_ID'];
105
109
  const pi = makeFakePi();
106
110
  registerCanvasContextIntro(pi);
107
- await pi.handler({ reason: 'startup' }, fakeCtx());
111
+ await pi.handler({ reason: 'startup' }, fakeCtx(pi));
108
112
  assert.equal(pi.sent.length, 0);
109
113
  });
110
114
  test('renderer registered for the customType; collapsed hides body, ctrl+o expand reveals it', () => {
@@ -1,8 +1,9 @@
1
1
  import { test, before, after, beforeEach } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
6
7
  import { spawnSync, spawn } from 'node:child_process';
7
8
  import { createServer } from 'node:net';
8
9
  import { createNode, getNode, subscribe } from '../canvas/canvas.js';
@@ -177,6 +178,18 @@ test('verifyDaemonStartup waits for pidfile + live pid, then reports success for
177
178
  assert.equal(pidAliveReady, true, 'pid must have been alive before success');
178
179
  assert.equal(slept > 0, true, 'the guard must actually poll');
179
180
  });
181
+ test('runDaemon awaits activation before ownership and readiness', () => {
182
+ const source = readFileSync(fileURLToPath(new URL('../../daemon/crtrd.ts', import.meta.url)), 'utf8');
183
+ const runDaemonAt = source.indexOf('export async function runDaemon');
184
+ const activationAt = source.indexOf('await ensureOnDiskMigrations();', runDaemonAt);
185
+ const ownershipAt = source.indexOf('claimDaemonOwnership(', runDaemonAt);
186
+ const readinessAt = source.indexOf('writePidfile();', runDaemonAt);
187
+ assert.ok(runDaemonAt >= 0, 'runDaemon exists');
188
+ assert.ok(activationAt > runDaemonAt, 'runDaemon awaits on-disk activation');
189
+ assert.equal(source.slice(runDaemonAt, activationAt).includes('await '), false, 'activation is the first awaited operation');
190
+ assert.ok(activationAt < ownershipAt, 'activation precedes the ownership claim that opens canvas.db');
191
+ assert.ok(ownershipAt < readinessAt, 'ownership follows activation before readiness is published');
192
+ });
180
193
  // Regression for the cold-guest recreate blocker: the production startup window
181
194
  // must tolerate a slower cold start than the old 500ms bound.
182
195
  test('verifyDaemonStartup default window tolerates a slower cold start', async () => {
@@ -197,28 +210,21 @@ test('verifyDaemonStartup default window tolerates a slower cold start', async (
197
210
  });
198
211
  assert.equal(existingPid, null, 'the production startup window must allow a cold guest to settle');
199
212
  });
200
- // REGRESSION (#508): a fixed 5s startup window falsely failed a valid cold
201
- // boot right after `install-runtime`, where the daemon's first-ever module
202
- // load + possible db migration can push writePidfile() past 5s even though
203
- // the daemon is starting up fine. The production window must comfortably
204
- // tolerate a pidfile that only appears well past the old 5s bound.
205
- test('verifyDaemonStartup default window tolerates a cold start slower than the old 5s bound', async () => {
213
+ test('startup verification waits for delayed activation inside the shared window', async () => {
206
214
  let now = 0;
207
- let pidfileReady = false;
208
- let pidAliveReady = false;
215
+ let activationComplete = false;
209
216
  const existingPid = await verifyDaemonStartup(42, undefined, {
210
217
  now: () => now,
211
- readPidfile: () => (pidfileReady ? 42 : null),
212
- isPidAlive: () => pidAliveReady,
218
+ readPidfile: () => (activationComplete ? 42 : null),
219
+ isPidAlive: () => activationComplete,
213
220
  sleepMs: (ms) => {
214
221
  now += ms;
215
- if (now >= 12_000)
216
- pidfileReady = true;
217
- if (now >= 12_100)
218
- pidAliveReady = true;
222
+ if (now >= 25_000)
223
+ activationComplete = true;
219
224
  },
220
225
  });
221
- assert.equal(existingPid, null, 'the production startup window must survive a pidfile that appears well past the old 5s bound');
226
+ assert.equal(existingPid, null, 'activation beyond the old 20s bound still reaches readiness');
227
+ assert.equal(activationComplete, true, 'readiness follows activation completion');
222
228
  });
223
229
  // A concurrent start race is success when another live daemon already owns the
224
230
  // pidfile; the loser must return that existing pid instead of timing out.
@@ -1,4 +1,4 @@
1
- import { existsSync, writeFileSync } from 'node:fs';
1
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { DefaultResourceLoader, ExtensionRunner, } from '@earendil-works/pi-coding-agent';
4
4
  import { buildRefInventory } from '../../memory/inline-ref-inventory.js';
@@ -17,6 +17,10 @@ async function main() {
17
17
  const stateBeforeProspective = existsSync(home);
18
18
  const prospective = await runProspectiveInventory({ cwd, profileId: null, extensions: [extensionPath] });
19
19
  const stateAfterProspective = existsSync(home);
20
+ const nodeId = process.env['CRTR_NODE_ID'];
21
+ if (nodeId === undefined || nodeId === '')
22
+ throw new Error('CRTR_NODE_ID is required');
23
+ mkdirSync(join(home, 'nodes', nodeId), { recursive: true });
20
24
  const loader = new DefaultResourceLoader({
21
25
  cwd,
22
26
  agentDir: join(cwd, '.pi'),
@@ -6,6 +6,7 @@ import assert from 'node:assert/strict';
6
6
  import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { join } from 'node:path';
9
+ import { ApiError } from '../../api/index.js';
9
10
  import { createApiServer } from '../../daemon/api/server.js';
10
11
  import { cancelHumanTicket, deliverTerminalResult, resolvePageTicket, settleFeedbackCompanion } from '../../daemon/human/finish.js';
11
12
  import { humanSend } from '../../commands/human/prompts.js';
@@ -219,6 +220,24 @@ describe('terminal ticket delivery', () => {
219
220
  assert.equal(notes[0].tier, 'normal');
220
221
  assert.match(notes[0].label, /canceled/);
221
222
  });
223
+ test('cancellation reaches subscribers when the bridge is already done, and retries conflict rather than fail', async () => {
224
+ const actor = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'actor' }).node_id;
225
+ const bridge = spawnNode({ kind: 'human', parent: actor, cwd: home, name: 'human-page', lifecycle: 'terminal' }).node_id;
226
+ const other = spawnNode({ kind: 'general', parent: null, cwd: home, name: 'other' }).node_id;
227
+ const { subscribe } = await import('../canvas/canvas.js');
228
+ subscribe(other, bridge, true);
229
+ submitBridgePage(bridge);
230
+ // The bridge is parked done with no canonical final while its page is still
231
+ // pending — done is where cancellation wants it, so cancelling must deliver.
232
+ transition(bridge, 'finish');
233
+ const result = await cancelHumanTicket(bridge, { reason: 'stale', actor });
234
+ assert.equal(result.kind, 'canceled');
235
+ assert.equal(getNode(bridge)?.status, 'done');
236
+ assert.equal(readInboxSince(other).length, 1, 'the other waiter is told no answer is coming');
237
+ // The recorded cancellation is final: a retry is an already-resolved
238
+ // conflict, never a delivery failure that keeps repeating.
239
+ await assert.rejects(() => cancelHumanTicket(bridge, { reason: 'stale', actor }), (error) => error instanceof ApiError && error.status === 409 && error.code === 'ticket_already_resolved');
240
+ });
222
241
  });
223
242
  test('a display-only page publishes passively and creates no canvas node', async () => {
224
243
  const page = join(home, 'build-done.tsx');
@@ -25,6 +25,8 @@ import { gatewayMemoryRows } from '../runtime/chat-inventory-rows.js';
25
25
  // ---------------------------------------------------------------------------
26
26
  describe('buildRefInventory (fixture-backed corpus)', () => {
27
27
  const NODE_ID = 'crtr-inline-ref-fixture-node';
28
+ const PROJECT_NAMESPACE = 'crtr-inline-ref-fixture-project';
29
+ const projectName = (local) => `${PROJECT_NAMESPACE}/root/${local}`;
28
30
  let canvasHome;
29
31
  let userHome;
30
32
  let projectBase;
@@ -75,6 +77,10 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
75
77
  projectBase = mkdtempSync(join(tmpdir(), 'crtr-inline-ref-project-'));
76
78
  projectRoot = join(projectBase, 'root');
77
79
  mkdirSync(projectRoot, { recursive: true });
80
+ const repositoryMemory = join(projectBase, '.crouter', 'memory');
81
+ mkdirSync(repositoryMemory, { recursive: true });
82
+ mkdirSync(projectMemoryDir(), { recursive: true });
83
+ writeFileSync(join(repositoryMemory, 'INDEX.md'), `---\nnamespace: ${PROJECT_NAMESPACE}\n---\nProject front door.\n`, 'utf8');
78
84
  process.env['CRTR_HOME'] = canvasHome;
79
85
  process.env['HOME'] = userHome;
80
86
  process.env['CRTR_NODE_ID'] = NODE_ID;
@@ -102,28 +108,25 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
102
108
  else
103
109
  process.env['CRTR_NODE_ID'] = prevNodeId;
104
110
  });
105
- test('nearest scope wins by canonical name: node > project > user', () => {
106
- const name = 'crtr-inline-ref-fixture-shared';
107
- writeDoc(userMemoryDir(), `${name}.md`, "kind: knowledge\nshort-form: 'user copy'");
108
- writeDoc(projectMemoryDir(), `${name}.md`, "kind: knowledge\nshort-form: 'project copy'");
109
- writeDoc(nodeScopeMemoryDir(), `${name}.md`, "kind: knowledge\nshort-form: 'node copy'");
111
+ test('canonical qualification removes project shadowing while equal identities keep precedence', () => {
112
+ const local = 'crtr-inline-ref-fixture-shared';
113
+ const qualified = projectName(local);
114
+ writeDoc(userMemoryDir(), `${local}.md`, "kind: knowledge\nshort-form: 'user copy'");
115
+ writeDoc(projectMemoryDir(), `${local}.md`, "kind: knowledge\nshort-form: 'project copy'");
116
+ writeDoc(nodeScopeMemoryDir(), `${local}.md`, "kind: knowledge\nshort-form: 'node copy'");
110
117
  let { refs, names } = buildRefInventory();
111
- assert.ok(names.has(name));
112
- let winner = refs.find((r) => r.name === name);
118
+ assert.ok(names.has(local));
119
+ assert.ok(names.has(qualified));
120
+ let winner = refs.find((r) => r.name === local);
113
121
  assert.equal(winner?.scope, 'node');
114
122
  assert.equal(winner?.shortForm, 'node copy');
115
- // Remove the node-scope copy: project now wins.
116
- rmSync(join(nodeScopeMemoryDir(), `${name}.md`));
117
- clearSessionCache();
118
- ({ refs, names } = buildRefInventory());
119
- winner = refs.find((r) => r.name === name);
123
+ winner = refs.find((r) => r.name === qualified);
120
124
  assert.equal(winner?.scope, 'project');
121
125
  assert.equal(winner?.shortForm, 'project copy');
122
- // Remove the project-scope copy too: user is the last resort.
123
- rmSync(join(projectMemoryDir(), `${name}.md`));
126
+ rmSync(join(nodeScopeMemoryDir(), `${local}.md`));
124
127
  clearSessionCache();
125
128
  ({ refs, names } = buildRefInventory());
126
- winner = refs.find((r) => r.name === name);
129
+ winner = refs.find((r) => r.name === local);
127
130
  assert.equal(winner?.scope, 'user');
128
131
  assert.equal(winner?.shortForm, 'user copy');
129
132
  });
@@ -133,41 +136,41 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
133
136
  writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-gated.md', 'kind: knowledge\ngate:\n never-matches: true');
134
137
  writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-slash.md', 'kind: knowledge\nslash: true');
135
138
  const { refs, names } = buildRefInventory();
136
- for (const name of [
139
+ for (const local of [
137
140
  'crtr-inline-ref-fixture-knowledge',
138
141
  'crtr-inline-ref-fixture-preference',
139
142
  'crtr-inline-ref-fixture-gated',
140
143
  'crtr-inline-ref-fixture-slash',
141
144
  ]) {
145
+ const name = projectName(local);
142
146
  assert.ok(names.has(name), `expected ${name} to be referenceable`);
143
147
  }
144
- assert.equal(refs.find((r) => r.name === 'crtr-inline-ref-fixture-knowledge')?.kind, 'knowledge');
145
- assert.equal(refs.find((r) => r.name === 'crtr-inline-ref-fixture-preference')?.kind, 'preference');
148
+ assert.equal(refs.find((r) => r.name === projectName('crtr-inline-ref-fixture-knowledge'))?.kind, 'knowledge');
149
+ assert.equal(refs.find((r) => r.name === projectName('crtr-inline-ref-fixture-preference'))?.kind, 'preference');
146
150
  });
147
- test('an INDEX doc keeps its canonical name; the bare dir name is a browse link, not a ref row', () => {
148
- writeDoc(join(projectMemoryDir(), 'crtr-inline-ref-fixture-taste'), 'INDEX.md', "kind: knowledge\nshort-form: 'taste index'");
151
+ test('a collapsed directory document keeps one canonical name and registers its browse node', () => {
152
+ const local = 'crtr-inline-ref-fixture-taste';
153
+ const name = projectName(local);
154
+ writeDoc(join(projectMemoryDir(), local), 'INDEX.md', "kind: knowledge\nshort-form: 'taste index'");
149
155
  const { refs, names } = buildRefInventory();
150
- assert.ok(names.has('crtr-inline-ref-fixture-taste/INDEX'));
151
- // The directory name resolves as a listing link — in `names` for linkify,
152
- // never a RefMeta row (no doc answers for a directory).
153
- assert.ok(names.has('crtr-inline-ref-fixture-taste'));
154
- assert.ok(!refs.some((r) => r.name === 'crtr-inline-ref-fixture-taste'));
155
- const winner = refs.find((r) => r.name === 'crtr-inline-ref-fixture-taste/INDEX');
156
- assert.equal(winner?.shortForm, 'taste index');
156
+ assert.ok(!names.has(`${name}/INDEX`));
157
+ assert.ok(names.has(name));
158
+ assert.ok(refs.some((r) => r.name === name));
159
+ assert.equal(refs.find((r) => r.name === name)?.shortForm, 'taste index');
157
160
  });
158
- test('a root INDEX doc is omitted, never exposed as an empty ref name', () => {
159
- writeDoc(projectMemoryDir(), 'INDEX.md', "kind: knowledge\nshort-form: 'project root index'");
160
- writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-sibling.md', 'kind: knowledge');
161
+ test('a project root INDEX document is omitted from refs but its namespace remains usable', () => {
162
+ writeDoc(join(projectBase, '.crouter', 'memory'), 'INDEX.md', `namespace: ${PROJECT_NAMESPACE}\nkind: knowledge\nshort-form: 'project root index'`);
163
+ const local = 'crtr-inline-ref-fixture-sibling';
164
+ writeDoc(projectMemoryDir(), `${local}.md`, 'kind: knowledge');
161
165
  const { refs, names } = buildRefInventory();
162
166
  assert.ok(!names.has(''));
163
167
  assert.ok(!refs.some((r) => r.name === ''));
164
- // A sibling doc in the same corpus is unaffected.
165
- assert.ok(names.has('crtr-inline-ref-fixture-sibling'));
168
+ assert.ok(names.has(projectName(local)));
166
169
  });
167
170
  test('refs are metadata-only: exactly {name, kind, scope, shortForm, gatewayVisible}, no body/path', () => {
168
171
  writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-meta.md', "kind: knowledge\nshort-form: 'hook'");
169
172
  const { refs } = buildRefInventory();
170
- const ref = refs.find((r) => r.name === 'crtr-inline-ref-fixture-meta');
173
+ const ref = refs.find((r) => r.name === projectName('crtr-inline-ref-fixture-meta'));
171
174
  assert.ok(ref);
172
175
  assert.deepEqual(Object.keys(ref).sort(), ['gatewayVisible', 'kind', 'name', 'scope', 'shortForm']);
173
176
  });
@@ -176,30 +179,31 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
176
179
  writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-hidden.md', 'kind: knowledge\nextensions:\n northlight:\n visibility: false');
177
180
  writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-visible.md', 'kind: knowledge');
178
181
  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');
182
+ const hidden = refs.find((ref) => ref.name === projectName('crtr-inline-ref-fixture-hidden'));
183
+ const visible = refs.find((ref) => ref.name === projectName('crtr-inline-ref-fixture-visible'));
181
184
  assert.equal(hidden?.gatewayVisible, false);
182
185
  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'));
186
+ assert.ok(names.has(projectName('crtr-inline-ref-fixture-hidden')));
187
+ assert.ok(names.has(projectName('crtr-inline-ref-fixture-visible')));
185
188
  assert.ok(refs.some((ref) => ref.scope === 'builtin'));
186
189
  assert.ok(refs.filter((ref) => ref.scope === 'builtin').every((ref) => !ref.gatewayVisible));
187
190
  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'));
191
+ assert.ok(!chatRows.some((ref) => ref.name === projectName('crtr-inline-ref-fixture-hidden')));
192
+ assert.ok(chatRows.some((ref) => ref.name === projectName('crtr-inline-ref-fixture-visible')));
190
193
  assert.ok(!chatRows.some((ref) => ref.scope === 'builtin'));
191
194
  });
192
195
  test('a malformed doc (no valid kind) is omitted from the inventory', () => {
193
196
  writeDoc(projectMemoryDir(), 'crtr-inline-ref-fixture-malformed.md', "short-form: 'no kind here'");
194
197
  const { names } = buildRefInventory();
195
- assert.ok(!names.has('crtr-inline-ref-fixture-malformed'));
198
+ assert.ok(!names.has(projectName('crtr-inline-ref-fixture-malformed')));
196
199
  });
197
- test('duplicate distinct-scope docs still produce ONE inventory row (not one per scope)', () => {
198
- const name = 'crtr-inline-ref-fixture-dedup';
199
- writeDoc(userMemoryDir(), `${name}.md`, 'kind: knowledge');
200
- writeDoc(projectMemoryDir(), `${name}.md`, 'kind: knowledge');
200
+ test('project qualification keeps distinct scope identities separate', () => {
201
+ const local = 'crtr-inline-ref-fixture-dedup';
202
+ writeDoc(userMemoryDir(), `${local}.md`, 'kind: knowledge');
203
+ writeDoc(projectMemoryDir(), `${local}.md`, 'kind: knowledge');
201
204
  const { refs } = buildRefInventory();
202
- assert.equal(refs.filter((r) => r.name === name).length, 1);
205
+ assert.equal(refs.filter((r) => r.name === local).length, 1);
206
+ assert.equal(refs.filter((r) => r.name === projectName(local)).length, 1);
203
207
  });
204
208
  test('refs are stably sorted by winning scope precedence then name', () => {
205
209
  writeDoc(nodeScopeMemoryDir(), 'crtr-inline-ref-fixture-b-node.md', 'kind: knowledge');
@@ -209,7 +213,7 @@ describe('buildRefInventory (fixture-backed corpus)', () => {
209
213
  const fixtureRefs = refs.filter((r) => r.name.startsWith('crtr-inline-ref-fixture-'));
210
214
  const idxA = fixtureRefs.findIndex((r) => r.name === 'crtr-inline-ref-fixture-a-node');
211
215
  const idxB = fixtureRefs.findIndex((r) => r.name === 'crtr-inline-ref-fixture-b-node');
212
- const idxC = fixtureRefs.findIndex((r) => r.name === 'crtr-inline-ref-fixture-c-project');
216
+ const idxC = fixtureRefs.findIndex((r) => r.name === projectName('crtr-inline-ref-fixture-c-project'));
213
217
  // Node-scope docs (a, b) sort before the project-scope doc (c); within the
214
218
  // same scope, alphabetically by name.
215
219
  assert.ok(idxA < idxB, 'a-node before b-node (alphabetical within node scope)');
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // BUG-REGRESSION (broker-universal-host cut, design §B.broker; U1 report
4
4
  // mq8ftenr-5d55313d). Before the cut, the headless broker had NO fork path — its
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // REAL-SDK regression tests for the latent broker gaps the viewer-reuse scout
4
4
  // (node mq5thyli) found (findings-viewer-reuse.md §Q2). These deliberately drive
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // BUG-REGRESSION (real, observed; surfaced during the crouter-web build, proven
4
4
  // broker-side). After a revive resumes a previously-CLOSED pi session, the