@north-light/crouter 0.3.227 → 0.3.229

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 (312) 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/04-base-worker.md +1 -1
  7. package/dist/builtin-memory/insights/init.md +5 -5
  8. package/dist/builtin-memory/internal/INDEX.md +1 -1
  9. package/dist/builtin-memory/internal/agent-shaping.md +11 -11
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/memory-loading.md +6 -6
  12. package/dist/builtin-memory/internal/plugins.md +1 -1
  13. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
  14. package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
  15. package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
  16. package/dist/clients/attach/chrome/inbox-strip.js +11 -7
  17. package/dist/clients/attach/session/pane-tag.d.ts +28 -2
  18. package/dist/clients/attach/session/pane-tag.js +48 -22
  19. package/dist/clients/attach/session/profile-files.d.ts +6 -0
  20. package/dist/clients/attach/session/profile-files.js +44 -11
  21. package/dist/clients/attach/slash/dispatch.js +4 -7
  22. package/dist/clients/attach/viewer.js +588 -596
  23. package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
  24. package/dist/commands/api-client.d.ts +3 -7
  25. package/dist/commands/api-client.js +7 -12
  26. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
  27. package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
  28. package/dist/commands/memory/delete.js +51 -24
  29. package/dist/commands/memory/edit.js +50 -10
  30. package/dist/commands/memory/find.js +105 -56
  31. package/dist/commands/memory/history.js +51 -31
  32. package/dist/commands/memory/lint.d.ts +0 -9
  33. package/dist/commands/memory/lint.js +78 -322
  34. package/dist/commands/memory/list.d.ts +12 -4
  35. package/dist/commands/memory/list.js +64 -25
  36. package/dist/commands/memory/move.js +103 -73
  37. package/dist/commands/memory/origin.js +35 -5
  38. package/dist/commands/memory/read.d.ts +4 -0
  39. package/dist/commands/memory/read.js +132 -80
  40. package/dist/commands/memory/shared.d.ts +115 -25
  41. package/dist/commands/memory/shared.js +331 -74
  42. package/dist/commands/memory/write.js +72 -21
  43. package/dist/commands/memory.js +2 -2
  44. package/dist/commands/node/create.d.ts +1 -1
  45. package/dist/commands/node/create.js +1 -1
  46. package/dist/commands/node/inspect.js +1 -1
  47. package/dist/commands/node/lifecycle.js +5 -5
  48. package/dist/commands/pkg/market-manage.js +25 -24
  49. package/dist/commands/pkg/plugin-manage.d.ts +22 -6
  50. package/dist/commands/pkg/plugin-manage.js +118 -25
  51. package/dist/commands/pkg/shared.d.ts +8 -0
  52. package/dist/commands/pkg/shared.js +23 -20
  53. package/dist/commands/surface-reload.d.ts +5 -0
  54. package/dist/commands/surface-reload.js +9 -1
  55. package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
  56. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
  57. package/dist/commands/sys/migrate.js +126 -140
  58. package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
  59. package/dist/commands/sys/panels/profiles-panel.js +599 -0
  60. package/dist/commands/sys/settings-shell.d.ts +4 -2
  61. package/dist/commands/sys/settings-shell.js +35 -4
  62. package/dist/commands/sys/settings.js +21 -5
  63. package/dist/commands/sys/sync-deps.d.ts +2 -0
  64. package/dist/commands/sys/sync-deps.js +26 -15
  65. package/dist/commands/sys/sync-project-guidance.js +228 -145
  66. package/dist/commands/sys/sync-skills.js +28 -17
  67. package/dist/commands/sys/update.js +11 -3
  68. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
  69. package/dist/core/__tests__/config-change-delta.test.js +106 -0
  70. package/dist/core/__tests__/context-intro.test.js +13 -9
  71. package/dist/core/__tests__/daemon-boot.test.js +22 -16
  72. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
  73. package/dist/core/__tests__/human-deliver.test.js +19 -0
  74. package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
  75. package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
  76. package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
  77. package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
  78. package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
  79. package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
  80. package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
  81. package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
  82. package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
  83. package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
  84. package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
  85. package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
  86. package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
  87. package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
  88. package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
  89. package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
  90. package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
  91. package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
  92. package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
  93. package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
  94. package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
  95. package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
  96. package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
  97. package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
  98. package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
  99. package/dist/core/__tests__/kickoff.test.js +16 -5
  100. package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
  101. package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
  102. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  103. package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
  104. package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
  105. package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
  106. package/dist/core/__tests__/repository-association.test.d.ts +1 -0
  107. package/dist/core/__tests__/repository-association.test.js +153 -0
  108. package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
  109. package/dist/core/__tests__/repository-root-identity.test.js +219 -0
  110. package/dist/core/__tests__/review-model-floor.test.js +12 -4
  111. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
  112. package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
  113. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
  114. package/dist/core/canvas/canvas.d.ts +3 -0
  115. package/dist/core/canvas/canvas.js +30 -10
  116. package/dist/core/canvas/db.js +28 -1
  117. package/dist/core/canvas/paths.d.ts +4 -6
  118. package/dist/core/canvas/paths.js +8 -6
  119. package/dist/core/canvas/render-source.js +2 -2
  120. package/dist/core/canvas/types.d.ts +9 -13
  121. package/dist/core/config.d.ts +3 -3
  122. package/dist/core/config.js +3 -3
  123. package/dist/core/exclusive-lock.d.ts +2 -0
  124. package/dist/core/exclusive-lock.js +21 -0
  125. package/dist/core/git.d.ts +0 -1
  126. package/dist/core/git.js +0 -3
  127. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
  128. package/dist/core/human/feedback-companion.js +3 -0
  129. package/dist/core/human/scan.js +8 -1
  130. package/dist/core/keybindings/catalog.d.ts +2 -2
  131. package/dist/core/keybindings/catalog.js +2 -1
  132. package/dist/core/memory/doc-link-grammar.js +2 -2
  133. package/dist/core/memory/history.d.ts +24 -0
  134. package/dist/core/memory/history.js +66 -1
  135. package/dist/core/memory/identity.d.ts +65 -0
  136. package/dist/core/memory/identity.js +185 -0
  137. package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
  138. package/dist/core/memory/inline-ref-guidance.js +1 -1
  139. package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
  140. package/dist/core/memory/inline-ref-inventory.js +30 -62
  141. package/dist/core/memory/lint.d.ts +129 -0
  142. package/dist/core/memory/lint.js +517 -0
  143. package/dist/core/memory/project-namespace.d.ts +31 -0
  144. package/dist/core/memory/project-namespace.js +85 -0
  145. package/dist/core/memory/repository-association.d.ts +31 -0
  146. package/dist/core/memory/repository-association.js +129 -0
  147. package/dist/core/memory/tree.d.ts +39 -0
  148. package/dist/core/memory/tree.js +93 -0
  149. package/dist/core/memory-resolver.d.ts +193 -90
  150. package/dist/core/memory-resolver.js +461 -375
  151. package/dist/core/nested-stores.js +6 -13
  152. package/dist/core/profiles/select.d.ts +4 -1
  153. package/dist/core/profiles/select.js +108 -48
  154. package/dist/core/review/__tests__/capture-origin.test.js +2 -2
  155. package/dist/core/review/__tests__/stage-identity.test.js +2 -2
  156. package/dist/core/review/companion.js +11 -2
  157. package/dist/core/runtime/bearings.d.ts +2 -2
  158. package/dist/core/runtime/bearings.js +3 -3
  159. package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
  160. package/dist/core/runtime/broker/rebind.js +5 -0
  161. package/dist/core/runtime/broker-extension-render.d.ts +7 -3
  162. package/dist/core/runtime/broker-extension-render.js +10 -5
  163. package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
  164. package/dist/core/runtime/broker-persona-guidance.js +122 -27
  165. package/dist/core/runtime/deliver-live.d.ts +16 -4
  166. package/dist/core/runtime/deliver-live.js +29 -15
  167. package/dist/core/runtime/kickoff.d.ts +3 -3
  168. package/dist/core/runtime/kickoff.js +8 -16
  169. package/dist/core/runtime/launch.js +1 -1
  170. package/dist/core/runtime/lifecycle.js +2 -3
  171. package/dist/core/runtime/nodes.d.ts +3 -4
  172. package/dist/core/runtime/nodes.js +3 -4
  173. package/dist/core/runtime/persona.d.ts +8 -12
  174. package/dist/core/runtime/persona.js +24 -96
  175. package/dist/core/runtime/promote.d.ts +2 -2
  176. package/dist/core/runtime/promote.js +8 -21
  177. package/dist/core/runtime/revive.js +20 -17
  178. package/dist/core/runtime/spawn.js +18 -7
  179. package/dist/core/runtime/tmux-bindings.js +2 -3
  180. package/dist/core/runtime/warm-pool.js +3 -4
  181. package/dist/core/scope.js +2 -0
  182. package/dist/core/self-update.d.ts +0 -2
  183. package/dist/core/self-update.js +2 -35
  184. package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
  185. package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
  186. package/dist/core/substrate/index.d.ts +2 -2
  187. package/dist/core/substrate/index.js +1 -1
  188. package/dist/core/substrate/injected-store.d.ts +43 -27
  189. package/dist/core/substrate/injected-store.js +208 -104
  190. package/dist/core/substrate/listings.d.ts +19 -12
  191. package/dist/core/substrate/listings.js +75 -52
  192. package/dist/core/substrate/on-read-node.d.ts +4 -7
  193. package/dist/core/substrate/on-read-node.js +6 -8
  194. package/dist/core/substrate/on-read.d.ts +22 -25
  195. package/dist/core/substrate/on-read.js +103 -147
  196. package/dist/core/substrate/render-node.d.ts +4 -7
  197. package/dist/core/substrate/render-node.js +5 -7
  198. package/dist/core/substrate/render.d.ts +21 -3
  199. package/dist/core/substrate/render.js +291 -223
  200. package/dist/core/substrate/schema.d.ts +1 -13
  201. package/dist/core/substrate/schema.js +5 -40
  202. package/dist/core/substrate/session-cache.d.ts +14 -4
  203. package/dist/core/substrate/session-cache.js +40 -22
  204. package/dist/core/substrate/surface-match.d.ts +9 -7
  205. package/dist/core/substrate/surface-match.js +26 -25
  206. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
  207. package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
  208. package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
  209. package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
  210. package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +21 -5
  211. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  212. package/dist/daemon/api/handlers/memory.js +2 -0
  213. package/dist/daemon/crtrd.js +2 -0
  214. package/dist/daemon/human/finish.js +8 -1
  215. package/dist/daemon/manage.d.ts +0 -1
  216. package/dist/daemon/manage.js +7 -16
  217. package/dist/daemon/startup-policy.d.ts +1 -0
  218. package/dist/daemon/startup-policy.js +1 -0
  219. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  220. package/dist/migrations/002-profile-project-memory.js +1 -0
  221. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  222. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  223. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  224. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  225. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  226. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  227. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  228. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  229. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  230. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  231. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  232. package/dist/migrations/__tests__/activation.test.js +149 -0
  233. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  234. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  235. package/dist/migrations/activation.d.ts +16 -0
  236. package/dist/migrations/activation.js +78 -0
  237. package/dist/migrations/convergent.d.ts +14 -3
  238. package/dist/migrations/convergent.js +21 -10
  239. package/dist/migrations/corpus.d.ts +78 -0
  240. package/dist/migrations/corpus.js +497 -0
  241. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  242. package/dist/migrations/frontmatter-splice.js +176 -0
  243. package/dist/migrations/registry.d.ts +6 -1
  244. package/dist/migrations/registry.js +7 -2
  245. package/dist/migrations/runner.d.ts +41 -0
  246. package/dist/migrations/runner.js +81 -0
  247. package/dist/migrations/types.d.ts +148 -9
  248. package/dist/migrations/types.js +9 -2
  249. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  250. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  251. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  252. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  253. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  254. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  255. package/dist/pi-extensions/canvas-stophook.js +6 -13
  256. package/dist/shared/generated-context.d.ts +0 -3
  257. package/dist/shared/generated-context.js +0 -57
  258. package/dist/shared/tool-groups.js +2 -3
  259. package/dist/types.d.ts +8 -11
  260. package/dist/types.js +4 -31
  261. package/package.json +5 -4
  262. package/runtime.lock.json +2 -2
  263. package/dist/builtin-memory/05-kinds/design/00-base.md +0 -17
  264. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +0 -15
  265. package/dist/builtin-memory/05-kinds/design/design-contract.md +0 -19
  266. package/dist/builtin-memory/05-kinds/developer/00-base.md +0 -17
  267. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +0 -15
  268. package/dist/builtin-memory/05-kinds/plan/00-base.md +0 -17
  269. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +0 -17
  270. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +0 -28
  271. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +0 -15
  272. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +0 -15
  273. package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +0 -13
  274. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +0 -17
  275. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +0 -17
  276. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +0 -17
  277. package/dist/builtin-memory/05-kinds/spec/00-base.md +0 -17
  278. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +0 -15
  279. package/dist/builtin-memory/05-kinds/spec/requirements.md +0 -15
  280. package/dist/builtin-memory/design/guide.md +0 -35
  281. package/dist/builtin-memory/design/roadmap.md +0 -21
  282. package/dist/builtin-memory/development.md +0 -113
  283. package/dist/builtin-memory/plan/roadmap.md +0 -25
  284. package/dist/builtin-memory/spec/guide.md +0 -53
  285. package/dist/builtin-memory/spec/requirements.md +0 -29
  286. package/dist/builtin-memory/spec/roadmap.md +0 -36
  287. package/dist/builtin-memory/testing.md +0 -39
  288. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  289. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  290. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  291. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  292. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  293. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  294. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  295. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  296. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  297. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  298. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  299. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  300. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  301. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  302. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  303. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  304. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  305. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  306. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  307. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  308. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  309. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  310. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  311. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  312. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -0,0 +1,219 @@
1
+ // Repository-root project-memory identity: real Git and non-Git fixtures prove
2
+ // that declarations live at a repository root while every nested store derives
3
+ // its effective prefix from its physical owner path.
4
+ //
5
+ // Run: node --conditions=crtr-src --import tsx/esm --test src/core/__tests__/repository-root-identity.test.ts
6
+ import { after, before, beforeEach, test } from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import { mkdtempSync, mkdirSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
9
+ import { tmpdir } from 'node:os';
10
+ import { join } from 'node:path';
11
+ import { gitSync } from '../git.js';
12
+ import { createProfile } from '../profiles/manifest.js';
13
+ import { loadMemoryStoreView, memoryStoresInPrecedence, openProjectMemoryStore, resolveMemoryDocForTarget, } from '../memory-resolver.js';
14
+ import { resetScopeCache } from '../scope.js';
15
+ const cleanups = [];
16
+ let previousHome;
17
+ before(() => {
18
+ previousHome = process.env.HOME;
19
+ });
20
+ beforeEach(() => {
21
+ process.env.HOME = fixtureDir('crtr-repository-root-identity-home-');
22
+ resetScopeCache();
23
+ });
24
+ after(() => {
25
+ resetScopeCache();
26
+ for (const path of cleanups)
27
+ rmSync(path, { recursive: true, force: true });
28
+ if (previousHome === undefined)
29
+ delete process.env.HOME;
30
+ else
31
+ process.env.HOME = previousHome;
32
+ });
33
+ function fixtureDir(prefix) {
34
+ const path = realpathSync(mkdtempSync(join(tmpdir(), prefix)));
35
+ cleanups.push(path);
36
+ return path;
37
+ }
38
+ function writeDoc(path, body, frontmatter) {
39
+ mkdirSync(join(path, '..'), { recursive: true });
40
+ const yaml = frontmatter === undefined ? '' : `---\n${frontmatter}\n---\n`;
41
+ writeFileSync(path, `${yaml}${body}\n`, 'utf8');
42
+ }
43
+ function writeStore(owner, namespace) {
44
+ const frontmatter = namespace === undefined ? undefined : `namespace: ${namespace}`;
45
+ writeDoc(join(owner, '.crouter', 'memory', 'INDEX.md'), 'STORE INDEX', frontmatter);
46
+ }
47
+ function initGit(repository) {
48
+ mkdirSync(repository, { recursive: true });
49
+ assert.equal(gitSync(['-C', repository, 'init', '-q'], undefined).status, 0, 'git init succeeds');
50
+ }
51
+ function commitInitial(repository) {
52
+ writeFileSync(join(repository, 'README.md'), 'fixture\n', 'utf8');
53
+ assert.equal(gitSync(['-C', repository, 'add', 'README.md'], undefined).status, 0);
54
+ assert.equal(gitSync(['-C', repository, '-c', 'user.name=Fixture', '-c', 'user.email=fixture@example.com', 'commit', '-qm', 'initial'], undefined).status, 0);
55
+ }
56
+ function target(cwd, profileId = null) {
57
+ return { cwd, profileId, nodeId: null };
58
+ }
59
+ test('a Git root declaration gives a metadata-free nested store its repository-relative prefix', () => {
60
+ const repository = join(process.env.HOME, 'repository');
61
+ const nested = join(repository, 'packages', 'agent');
62
+ initGit(repository);
63
+ writeStore(repository, 'acme');
64
+ mkdirSync(join(nested, '.crouter', 'memory'), { recursive: true });
65
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NESTED GUIDE');
66
+ const doc = resolveMemoryDocForTarget('acme/packages/agent/guide', target(nested), { scope: 'project' });
67
+ assert.equal(doc.path, join(nested, '.crouter', 'memory', 'guide.md'));
68
+ assert.equal(doc.name, 'acme/packages/agent/guide');
69
+ const store = openProjectMemoryStore(nested);
70
+ assert.equal(store.mountStatus, 'ready');
71
+ assert.equal(store.repositoryRoot, repository);
72
+ assert.equal(store.ownerRelativePath, 'packages/agent');
73
+ assert.equal(store.namespacePath, join(repository, '.crouter', 'memory', 'INDEX.md'));
74
+ });
75
+ test('repository-relative ordering prefixes are stripped before nested identity composition', () => {
76
+ const repository = join(process.env.HOME, 'repository');
77
+ const nested = join(repository, '01-api');
78
+ initGit(repository);
79
+ writeStore(repository, 'acme');
80
+ writeStore(nested);
81
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'ORDERED GUIDE');
82
+ const store = openProjectMemoryStore(nested);
83
+ assert.equal(store.mountStatus, 'ready');
84
+ assert.equal(store.ownerRelativePath, 'api');
85
+ assert.equal(store.namespace, 'acme/api');
86
+ const doc = resolveMemoryDocForTarget('acme/api/guide', target(nested), { scope: 'project' });
87
+ assert.equal(doc.path, join(nested, '.crouter', 'memory', 'guide.md'));
88
+ assert.equal(doc.name, 'acme/api/guide');
89
+ });
90
+ test('a metadata-free nested INDEX answers exactly at the nested effective prefix', () => {
91
+ const repository = join(process.env.HOME, 'repository');
92
+ const nested = join(repository, 'packages', 'agent');
93
+ initGit(repository);
94
+ writeStore(repository, 'acme');
95
+ writeStore(nested);
96
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NESTED GUIDE');
97
+ const store = openProjectMemoryStore(nested);
98
+ assert.equal(store.mountStatus, 'ready');
99
+ assert.equal(store.namespace, 'acme/packages/agent');
100
+ const view = loadMemoryStoreView(store, true);
101
+ assert.deepEqual(view.docs.map((doc) => doc.name), ['acme/packages/agent', 'acme/packages/agent/guide']);
102
+ assert.equal(resolveMemoryDocForTarget('acme/packages/agent', target(nested), { scope: 'project' }).path, join(nested, '.crouter', 'memory', 'INDEX.md'));
103
+ });
104
+ test('linked worktree owners have identical canonical names from different physical paths', () => {
105
+ const main = join(process.env.HOME, 'main');
106
+ const linked = join(process.env.HOME, 'linked');
107
+ initGit(main);
108
+ commitInitial(main);
109
+ assert.equal(gitSync(['-C', main, 'worktree', 'add', '-q', '-b', 'linked', linked], undefined).status, 0);
110
+ const mainOwner = join(main, 'packages', 'agent');
111
+ const linkedOwner = join(linked, 'packages', 'agent');
112
+ writeStore(main, 'acme');
113
+ writeStore(linked, 'acme');
114
+ writeStore(mainOwner);
115
+ writeStore(linkedOwner);
116
+ writeDoc(join(mainOwner, '.crouter', 'memory', 'guide.md'), 'MAIN GUIDE');
117
+ writeDoc(join(linkedOwner, '.crouter', 'memory', 'guide.md'), 'LINKED GUIDE');
118
+ const mainDoc = resolveMemoryDocForTarget('acme/packages/agent/guide', target(mainOwner), { scope: 'project' });
119
+ const linkedDoc = resolveMemoryDocForTarget('acme/packages/agent/guide', target(linkedOwner), { scope: 'project' });
120
+ assert.equal(mainDoc.name, linkedDoc.name);
121
+ assert.equal(mainDoc.path, join(mainOwner, '.crouter', 'memory', 'guide.md'));
122
+ assert.equal(linkedDoc.path, join(linkedOwner, '.crouter', 'memory', 'guide.md'));
123
+ const mainStore = openProjectMemoryStore(mainOwner);
124
+ const linkedStore = openProjectMemoryStore(linkedOwner);
125
+ assert.equal(mainStore.repositoryKey, linkedStore.repositoryKey);
126
+ assert.notEqual(mainStore.repositoryRoot, linkedStore.repositoryRoot);
127
+ });
128
+ test('an embedded Git repository uses its own declaration from cwd and explicit store opening', () => {
129
+ const outer = join(process.env.HOME, 'outer');
130
+ const embedded = join(outer, 'vendor');
131
+ const nested = join(embedded, 'pkg');
132
+ initGit(outer);
133
+ writeStore(outer, 'outer');
134
+ initGit(embedded);
135
+ writeStore(embedded, 'inner');
136
+ writeStore(nested);
137
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'EMBEDDED GUIDE');
138
+ const fromCwd = resolveMemoryDocForTarget('inner/pkg/guide', target(nested), { scope: 'project' });
139
+ assert.equal(fromCwd.path, join(nested, '.crouter', 'memory', 'guide.md'));
140
+ const explicit = openProjectMemoryStore(nested);
141
+ assert.equal(explicit.repositoryRoot, embedded);
142
+ assert.equal(explicit.namespace, 'inner/pkg');
143
+ assert.equal(loadMemoryStoreView(explicit, true).resolve('inner/pkg/guide').path, fromCwd.path);
144
+ });
145
+ test('a non-Git nested store derives from the outermost ancestor project store', () => {
146
+ const root = join(process.env.HOME, 'non-git');
147
+ const nested = join(root, 'packages', 'agent');
148
+ writeStore(root, 'workspace');
149
+ writeStore(nested);
150
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NON-GIT GUIDE');
151
+ const store = openProjectMemoryStore(nested);
152
+ assert.equal(store.mountStatus, 'ready');
153
+ assert.equal(store.repositoryRoot, root);
154
+ assert.equal(store.repositoryKey, `path:${root}`);
155
+ assert.equal(store.ownerRelativePath, 'packages/agent');
156
+ assert.equal(store.namespace, 'workspace/packages/agent');
157
+ assert.equal(resolveMemoryDocForTarget('workspace/packages/agent/guide', target(nested), { scope: 'project' }).path, join(nested, '.crouter', 'memory', 'guide.md'));
158
+ });
159
+ test('a Git descendant without a root store is undeclared until the root declaration is created', () => {
160
+ const repository = join(process.env.HOME, 'repository');
161
+ const nested = join(repository, 'packages', 'agent');
162
+ initGit(repository);
163
+ writeStore(nested);
164
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NESTED GUIDE');
165
+ const before = openProjectMemoryStore(nested);
166
+ assert.equal(before.mountStatus, 'undeclared');
167
+ assert.equal(before.mountFailure, 'undeclared-root');
168
+ assert.match(before.diagnostic ?? '', new RegExp(repository.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
169
+ writeStore(repository, 'acme');
170
+ const after = openProjectMemoryStore(nested);
171
+ assert.equal(after.mountStatus, 'ready');
172
+ assert.equal(after.namespace, 'acme/packages/agent');
173
+ });
174
+ test('sibling owner paths that normalize to one prefix, including ordering prefixes, leave both stores invalid', () => {
175
+ const repository = join(process.env.HOME, 'repository');
176
+ const first = join(repository, '01-My Notes');
177
+ const second = join(repository, 'my-notes');
178
+ initGit(repository);
179
+ writeStore(repository, 'acme');
180
+ writeStore(first);
181
+ writeStore(second);
182
+ const profileId = createProfile('collision', [{ path: second, memory: 'content' }]).profileId;
183
+ const stores = memoryStoresInPrecedence(target(first, profileId), 'project');
184
+ const colliding = stores.filter((store) => store.ownerDir === first || store.ownerDir === second);
185
+ assert.equal(colliding.length, 2);
186
+ for (const store of colliding) {
187
+ assert.equal(store.mountStatus, 'invalid');
188
+ assert.equal(store.mountFailure, 'collision');
189
+ assert.equal(store.namespace, 'acme/my-notes');
190
+ assert.match(store.diagnostic ?? '', /My Notes/);
191
+ assert.match(store.diagnostic ?? '', /my-notes/);
192
+ }
193
+ });
194
+ test('profile presence and order do not change a nested store canonical name', () => {
195
+ const repository = join(process.env.HOME, 'repository');
196
+ const nested = join(repository, 'packages', 'agent');
197
+ const unrelated = join(process.env.HOME, 'unrelated');
198
+ initGit(repository);
199
+ writeStore(repository, 'acme');
200
+ writeStore(nested);
201
+ writeDoc(join(nested, '.crouter', 'memory', 'guide.md'), 'NESTED GUIDE');
202
+ mkdirSync(unrelated, { recursive: true });
203
+ const absent = resolveMemoryDocForTarget('acme/packages/agent/guide', target(nested), { scope: 'project' });
204
+ const rootSecond = createProfile('root-second', [
205
+ { path: unrelated, memory: 'content' },
206
+ { path: repository, memory: 'content' },
207
+ ]).profileId;
208
+ const rootFirst = createProfile('root-first', [
209
+ { path: repository, memory: 'content' },
210
+ { path: unrelated, memory: 'content' },
211
+ ]).profileId;
212
+ const withRootSecond = resolveMemoryDocForTarget('acme/packages/agent/guide', target(nested, rootSecond), { scope: 'project' });
213
+ const withRootFirst = resolveMemoryDocForTarget('acme/packages/agent/guide', target(nested, rootFirst), { scope: 'project' });
214
+ assert.equal(absent.name, 'acme/packages/agent/guide');
215
+ assert.equal(withRootSecond.name, absent.name);
216
+ assert.equal(withRootFirst.name, absent.name);
217
+ assert.equal(withRootSecond.path, absent.path);
218
+ assert.equal(withRootFirst.path, absent.path);
219
+ });
@@ -5,7 +5,7 @@
5
5
  // persona keeps the stronger default needed to synthesise a broad review.
6
6
  import { after, before, test } from 'node:test';
7
7
  import assert from 'node:assert/strict';
8
- import { mkdtempSync, rmSync } from 'node:fs';
8
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
9
9
  import { tmpdir } from 'node:os';
10
10
  import { join } from 'node:path';
11
11
  import { defaultKindsConfig, defaultModelLaddersConfig } from '../../types.js';
@@ -19,6 +19,15 @@ let home = '';
19
19
  before(() => {
20
20
  home = mkdtempSync(join(tmpdir(), 'crtr-review-model-floor-'));
21
21
  process.env['HOME'] = home;
22
+ mkdirSync(join(home, '.crouter'), { recursive: true });
23
+ writeFileSync(join(home, '.crouter', 'config.json'), JSON.stringify({
24
+ kinds: {
25
+ 'audit/reviewers/security': {
26
+ whenToUse: 'Review one security surface.',
27
+ model: 'medium',
28
+ },
29
+ },
30
+ }));
22
31
  });
23
32
  after(() => {
24
33
  if (priorHome === undefined)
@@ -43,9 +52,8 @@ test('base review ignores a light override and keeps its medium floor', () => {
43
52
  assert.equal(spec('review', 'light'), expected(REVIEW_FLOOR));
44
53
  assert.equal(spec('review', 'haiku'), expected(REVIEW_FLOOR));
45
54
  });
46
- test('plan reviewer sub-kinds also keep their medium worker floor', () => {
47
- assert.equal(spec('plan/reviewers/security', 'light'), expected('medium'));
48
- assert.equal(spec('plan/reviewers/requirements-coverage', 'light'), expected('medium'));
55
+ test('plugin-shaped reviewer sub-kinds keep their configured worker floor', () => {
56
+ assert.equal(spec('audit/reviewers/security', 'light'), expected('medium'));
49
57
  });
50
58
  test('review orchestrator uses its stronger coordinating model', () => {
51
59
  assert.equal(buildLaunchSpec('review', 'orchestrator', { lifecycle: 'terminal', hasManager: true, cwd: home, profileId: null }).launch.model, expected(defaultKindsConfig()['review'].orchestratorModel));
@@ -1,5 +1,5 @@
1
1
  import assert from 'node:assert/strict';
2
- import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { dirname, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
@@ -9,7 +9,9 @@ import { loadedCommandPreview } from '../../../clients/attach/slash/prompt-previ
9
9
  const ROOT = join(dirname(fileURLToPath(import.meta.url)), '../../../..');
10
10
  const EXTENSION = join(ROOT, 'dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts');
11
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'];
12
+ const COMMAND_LOCALS = ['acceptance-context-token', 'acceptance-node-token', 'acceptance-independent'];
13
+ const PROJECT_NAMESPACE = 'workspace';
14
+ const COMMAND_NAMES = COMMAND_LOCALS.map((name) => `${PROJECT_NAMESPACE}:workspace:${name}`);
13
15
  function writeSlashDoc(memoryDir, name, description, body) {
14
16
  writeFileSync(join(memoryDir, `${name}.md`), `---\nkind: knowledge\nwhen-and-why-to-read: Acceptance fixture.\nshort-form: ${description}\nslash: true\n---\n${body}`);
15
17
  }
@@ -60,15 +62,19 @@ function fixtureRows(rows) {
60
62
  return row;
61
63
  });
62
64
  }
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');
65
+ test('node-relative memory slash commands omit preview metadata and register invoked bodies at content', async () => {
66
+ const repositoryRoot = mkdtempSync(join(tmpdir(), 'crtr-memory-slash-inventory-'));
67
+ const cwd = join(repositoryRoot, 'workspace');
68
+ const home = join(repositoryRoot, 'canvas');
66
69
  const memoryDir = join(cwd, '.crouter', 'memory');
70
+ const repositoryMemory = join(repositoryRoot, '.crouter', 'memory');
67
71
  const nodeId = 'acceptance-live-node';
68
72
  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');
73
+ mkdirSync(repositoryMemory, { recursive: true });
74
+ writeFileSync(join(repositoryMemory, 'INDEX.md'), `---\nnamespace: ${PROJECT_NAMESPACE}\n---\nWorkspace front door.\n`);
75
+ writeSlashDoc(memoryDir, COMMAND_LOCALS[0], 'Context-relative fixture.', 'Store $ARGUMENTS in $CRTR_CONTEXT_DIR/result.md.\n');
76
+ writeSlashDoc(memoryDir, COMMAND_LOCALS[1], 'Node-relative fixture.', 'Node $CRTR_NODE_ID received $ARGUMENTS.\n');
77
+ writeSlashDoc(memoryDir, COMMAND_LOCALS[2], 'Node-independent fixture.', 'Independent payload: $ARGUMENTS.\n');
72
78
  try {
73
79
  const live = await liveProbe({
74
80
  cwd,
@@ -83,6 +89,9 @@ test('node-relative memory slash commands omit preview metadata without changing
83
89
  const prospectiveRows = fixtureRows(live.prospective.commands);
84
90
  const liveRows = fixtureRows(live.commands);
85
91
  assert.deepEqual(liveRows, prospectiveRows);
92
+ for (const local of COMMAND_LOCALS) {
93
+ assert.equal(live.commands.some((row) => row.name === local), false, `/${local} is not retained as an old command`);
94
+ }
86
95
  assert.deepEqual(prospectiveRows, [
87
96
  { name: COMMAND_NAMES[0], description: 'Context-relative fixture.', source: 'command' },
88
97
  { name: COMMAND_NAMES[1], description: 'Node-relative fixture.', source: 'command' },
@@ -101,8 +110,9 @@ test('node-relative memory slash commands omit preview metadata without changing
101
110
  assert.ok(insightsInit, 'Gateway completion includes /insights:init');
102
111
  assert.equal(insightsInit.expansion, undefined);
103
112
  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');
113
+ assert.equal(loadedCommandPreview(`/${COMMAND_NAMES[2]} first evidence`, live.commands)?.content, 'Independent payload: first evidence.\n');
114
+ assert.equal(loadedCommandPreview(`/${COMMAND_NAMES[2]} changed evidence`, live.commands)?.content, 'Independent payload: changed evidence.\n');
115
+ assert.equal(loadedCommandPreview('/acceptance-independent first evidence', live.commands), undefined);
106
116
  assert.deepEqual(live.notifications, []);
107
117
  const sentContent = Object.fromEntries(live.sent.map((send) => [send.name, send.message.content]));
108
118
  assert.deepEqual(sentContent, {
@@ -120,8 +130,14 @@ test('node-relative memory slash commands omit preview metadata without changing
120
130
  }
121
131
  assert.doesNotMatch(sentContent[COMMAND_NAMES[0]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
122
132
  assert.doesNotMatch(sentContent[COMMAND_NAMES[1]], /\$CRTR_CONTEXT_DIR|\$CRTR_NODE_ID/);
133
+ const persisted = JSON.parse(readFileSync(join(home, 'nodes', nodeId, 'injected-docs.json'), 'utf8'));
134
+ assert.equal(persisted.v, 3);
135
+ for (const name of COMMAND_LOCALS) {
136
+ const path = realpathSync(join(memoryDir, `${name}.md`));
137
+ assert.deepEqual(persisted.exposures[path], { transcript: 3 }, `/${name} registers the invoked document body as transcript content`);
138
+ }
123
139
  }
124
140
  finally {
125
- rmSync(cwd, { recursive: true, force: true });
141
+ rmSync(repositoryRoot, { recursive: true, force: true });
126
142
  }
127
143
  });
@@ -0,0 +1,40 @@
1
+ // The warm-pool claim must leave a FROZEN preference snapshot behind.
2
+ //
3
+ // A spare boots with no claimant, so canvas-context-intro skips it and
4
+ // canvas-doc-substrate's before_agent_start is the only other freezer — and Pi
5
+ // emits that event on the typed-prompt path only. A claimed spare whose first
6
+ // turn is a slash command (`sendMessage({ triggerTurn: true })`) therefore
7
+ // reached the persona gate with no snapshot and died with "context preference
8
+ // snapshot is missing". Claim time is where the snapshot is frozen.
9
+ //
10
+ // Run: node --import tsx/esm --test src/core/__tests__/warm-claim-preference-snapshot.test.ts
11
+ import { test, before, beforeEach, after } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { mkdtempSync, mkdirSync, rmSync } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join } from 'node:path';
16
+ import { closeDb } from '../canvas/db.js';
17
+ import { spawnNode } from '../runtime/nodes.js';
18
+ import { deliverBearingsLive } from '../runtime/deliver-live.js';
19
+ import { loadContextExposureState } from '../substrate/injected-store.js';
20
+ let home;
21
+ before(() => {
22
+ home = mkdtempSync(join(tmpdir(), 'crtr-warm-snapshot-'));
23
+ process.env['CRTR_HOME'] = home;
24
+ });
25
+ beforeEach(() => {
26
+ closeDb();
27
+ rmSync(home, { recursive: true, force: true });
28
+ mkdirSync(home, { recursive: true });
29
+ });
30
+ after(() => {
31
+ closeDb();
32
+ rmSync(home, { recursive: true, force: true });
33
+ delete process.env['CRTR_HOME'];
34
+ });
35
+ test('the claim path persists a frozen preference snapshot', async () => {
36
+ const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
37
+ assert.equal(loadContextExposureState(meta.node_id).preferenceSnapshot, null, 'a spare mints with nothing frozen');
38
+ await deliverBearingsLive(meta.node_id);
39
+ assert.notEqual(loadContextExposureState(meta.node_id).preferenceSnapshot, null, 'claim freezes the snapshot the persona gate requires');
40
+ });
@@ -5,6 +5,9 @@ export declare function createNode(meta: NodeMeta): NodeMeta;
5
5
  /** The canonical node record: durable identity (meta.json) ∪ authoritative
6
6
  * runtime (the row). Null if unknown. */
7
7
  export declare function getNode(nodeId: string): NodeMeta | null;
8
+ export declare function canonicalSessionFile(sessionFile: string): string;
9
+ /** Resolve the crouter node that owns one exact Pi session file. */
10
+ export declare function findNodeBySessionFile(sessionFile: string): NodeMeta | null;
8
11
  /** The indexed row (from the db) — cheap for queries that don't need full meta. */
9
12
  export declare function getRow(nodeId: string): NodeRow | null;
10
13
  /** The node row whose durable LOCATION pane is `pane`, or null. Lets placement
@@ -4,8 +4,8 @@
4
4
  // Source-of-truth split: a node's meta.json is canonical for its own fields;
5
5
  // the db row is a queryable index re-derivable from it. The subscribes_to edges
6
6
  // are db-authoritative (mutable, many-writers — what WAL is for).
7
- import { existsSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
8
- import { join } from 'node:path';
7
+ import { existsSync, readFileSync, readdirSync, realpathSync, rmSync, statSync } from 'node:fs';
8
+ import { join, resolve } from 'node:path';
9
9
  import { openDb, withCanvasWrite } from './db.js';
10
10
  import { withMetaLock, readMetaObject, writeMetaObjectLocked, writeJsonFileDurably } from './meta-file.js';
11
11
  import { isPidAlive, recordedPidLiveness, capturePidIdentities, identitiesMatch, isLegacyTicksIdentity, isBootScopedIdentity, } from './pid.js';
@@ -22,7 +22,7 @@ import { ensureHome, ensureNodeDirs, nodeMetaPath, nodeDir, nodesRoot, jobDir, }
22
22
  * can ever leak onto disk even when a fully-hydrated NodeMeta is handed in. */
23
23
  const IDENTITY_KEYS = [
24
24
  'node_id', 'name', 'description', 'icon', 'cycles', 'created', 'cwd', 'host_kind', 'profile_id', 'kind', 'mode',
25
- 'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'fork_from', 'review_binding', 'passive_default', 'pi_session_id',
25
+ 'lifecycle', 'persona_ack', 'parent', 'spawned_by', 'fork_from', 'fork_source_file', 'review_binding', 'passive_default', 'pi_session_id',
26
26
  'pi_session_file', 'cycle_pending', 'model_override',
27
27
  'launch', 'managed_worktree',
28
28
  ];
@@ -33,6 +33,10 @@ function toIdentity(m) {
33
33
  if (m[k] !== undefined)
34
34
  out[k] = m[k];
35
35
  }
36
+ for (const key of ['pi_session_file', 'fork_source_file']) {
37
+ if (typeof out[key] === 'string')
38
+ out[key] = canonicalSessionFile(out[key]);
39
+ }
36
40
  return out;
37
41
  }
38
42
  function readMeta(nodeId) {
@@ -83,13 +87,14 @@ function writeMeta(meta) {
83
87
  * state. A fresh insert takes the schema defaults for runtime. */
84
88
  function upsertRow(meta) {
85
89
  openDb()
86
- .prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created)
87
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
90
+ .prepare(`INSERT INTO nodes (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file)
91
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
88
92
  ON CONFLICT(node_id) DO UPDATE SET
89
93
  name=excluded.name, kind=excluded.kind, mode=excluded.mode,
90
94
  lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
91
- profile_id=excluded.profile_id, parent=excluded.parent`)
92
- .run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created);
95
+ profile_id=excluded.profile_id, parent=excluded.parent,
96
+ pi_session_file=excluded.pi_session_file`)
97
+ .run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null);
93
98
  }
94
99
  /** Seed a node's row at BIRTH: identity columns + runtime columns taken from the
95
100
  * incoming meta (defaults: status='active', the rest null). The only writer
@@ -98,18 +103,19 @@ function upsertRow(meta) {
98
103
  function seedRow(meta) {
99
104
  openDb()
100
105
  .prepare(`INSERT INTO nodes
101
- (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created,
106
+ (node_id, name, kind, mode, lifecycle, cwd, host_kind, profile_id, parent, created, pi_session_file,
102
107
  status, intent, waiting_for, final_report, finalized_at, pi_pid, "window", tmux_session, pane)
103
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
108
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
104
109
  ON CONFLICT(node_id) DO UPDATE SET
105
110
  name=excluded.name, kind=excluded.kind, mode=excluded.mode,
106
111
  lifecycle=excluded.lifecycle, cwd=excluded.cwd, host_kind=excluded.host_kind,
107
112
  profile_id=excluded.profile_id, parent=excluded.parent,
113
+ pi_session_file=excluded.pi_session_file,
108
114
  status=excluded.status, intent=excluded.intent, waiting_for=excluded.waiting_for,
109
115
  final_report=excluded.final_report, finalized_at=excluded.finalized_at,
110
116
  pi_pid=excluded.pi_pid, "window"=excluded."window", tmux_session=excluded.tmux_session,
111
117
  pane=excluded.pane`)
112
- .run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created, meta.status ?? 'active', meta.intent ?? null, meta.waiting_for ?? null, meta.final_report ?? null, meta.finalized_at ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
118
+ .run(meta.node_id, meta.name, meta.kind, meta.mode, meta.lifecycle, meta.cwd, meta.host_kind ?? null, meta.profile_id ?? null, meta.parent ?? null, meta.created, typeof meta.pi_session_file === 'string' ? canonicalSessionFile(meta.pi_session_file) : null, meta.status ?? 'active', meta.intent ?? null, meta.waiting_for ?? null, meta.final_report ?? null, meta.finalized_at ?? null, meta.pi_pid ?? null, meta.window ?? null, meta.tmux_session ?? null, meta.pane ?? null);
113
119
  }
114
120
  function rowFrom(r) {
115
121
  return {
@@ -179,6 +185,20 @@ export function getNode(nodeId) {
179
185
  // exists yet (rare — pre-rebuild), keep whatever the meta carried.
180
186
  return { ...ident, ...(rt ?? {}) };
181
187
  }
188
+ export function canonicalSessionFile(sessionFile) {
189
+ const absolute = resolve(sessionFile);
190
+ return existsSync(absolute) ? realpathSync(absolute) : absolute;
191
+ }
192
+ /** Resolve the crouter node that owns one exact Pi session file. */
193
+ export function findNodeBySessionFile(sessionFile) {
194
+ const canonical = canonicalSessionFile(sessionFile);
195
+ const matches = openDb()
196
+ .prepare('SELECT node_id FROM nodes WHERE pi_session_file = ? ORDER BY created')
197
+ .all(canonical);
198
+ if (matches.length > 1)
199
+ throw new Error(`multiple nodes own pi session file: ${canonical}`);
200
+ return matches[0] === undefined ? null : getNode(matches[0].node_id);
201
+ }
182
202
  /** The indexed row (from the db) — cheap for queries that don't need full meta. */
183
203
  export function getRow(nodeId) {
184
204
  const r = openDb()
@@ -1175,6 +1175,27 @@ CREATE TABLE human_action_deliveries (
1175
1175
  CREATE INDEX idx_human_action_deliveries_due ON human_action_deliveries(state, next_attempt_at);
1176
1176
  `);
1177
1177
  }
1178
+ /** v34 — index the exact Pi session file coordinate used to recover fork
1179
+ * exposure provenance without hydrating every node meta. Meta remains authoritative;
1180
+ * normal identity upserts keep this projection current after the one-time backfill. */
1181
+ function addNodeSessionFileIndex(db) {
1182
+ const columns = nodeColumns(db);
1183
+ addNodeColumnIfMissing(db, columns, 'pi_session_file', 'ALTER TABLE nodes ADD COLUMN pi_session_file TEXT;');
1184
+ for (const entry of readdirSync(nodesRoot(), { withFileTypes: true })) {
1185
+ if (!entry.isDirectory())
1186
+ continue;
1187
+ try {
1188
+ const meta = JSON.parse(readFileSync(nodeMetaPath(entry.name), 'utf8'));
1189
+ if (typeof meta.pi_session_file === 'string') {
1190
+ db.prepare('UPDATE nodes SET pi_session_file = ? WHERE node_id = ?').run(meta.pi_session_file, entry.name);
1191
+ }
1192
+ }
1193
+ catch {
1194
+ // Corrupt node metadata is ignored by the ordinary canvas rebuild too.
1195
+ }
1196
+ }
1197
+ db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_pi_session_file ON nodes(pi_session_file);');
1198
+ }
1178
1199
  /** The journaled lane of the state-migration registry, adapted to this chain's
1179
1200
  * step shape. Registry order is preserved, so these versions stay stable as
1180
1201
  * long as the registry is append-only (same contract as the local list). */
@@ -1221,6 +1242,7 @@ export const MIGRATIONS = [
1221
1242
  /* v31 */ addReviewSubmitRequested,
1222
1243
  /* v32 */ addCronHeldColumns,
1223
1244
  /* v33 */ addHumanActionDeliveries,
1245
+ /* v34 */ addNodeSessionFileIndex,
1224
1246
  ...JOURNALED_STATE_MIGRATIONS,
1225
1247
  ];
1226
1248
  /** Migration indexes that manage their OWN transaction and therefore must not
@@ -1348,11 +1370,16 @@ export function openDb() {
1348
1370
  ensureHome();
1349
1371
  recordOpenLedger();
1350
1372
  const db = new DatabaseSync(path);
1373
+ // FIRST, before any pragma that can contend. `journal_mode = WAL` takes a
1374
+ // brief exclusive lock, so on the default zero busy_timeout a cold open that
1375
+ // races a concurrent writer throws `database is locked` outright — crtrd, a
1376
+ // CLI child and a broker all open this same file, and every one of them opens
1377
+ // it cold. Setting the wait window first makes that open wait its turn.
1378
+ db.exec('PRAGMA busy_timeout = 5000;');
1351
1379
  db.exec('PRAGMA journal_mode = WAL;');
1352
1380
  // Load-bearing as of migration v4: the edges→nodes FK (ON DELETE CASCADE)
1353
1381
  // needs this ON at the deleting connection so a node delete reaps its edges.
1354
1382
  db.exec('PRAGMA foreign_keys = ON;');
1355
- db.exec('PRAGMA busy_timeout = 5000;');
1356
1383
  migrate(db);
1357
1384
  handles.set(path, db);
1358
1385
  return db;
@@ -10,6 +10,8 @@ export declare function crtrHome(): string;
10
10
  * local disk — so the db shares the node data's location and survives a
11
11
  * sandbox recreate together with `nodes/`. */
12
12
  export declare function canvasDbPath(): string;
13
+ export declare function onDiskMigrationLockPath(): string;
14
+ export declare function onDiskMigrationMarkerPath(): string;
13
15
  /** Absolute path to a node's broker `view.sock` — always `view.sock` under the
14
16
  * node dir. Stale socket files (a host reboot, a sandbox recreate on a
15
17
  * durable volume) are harmless: the broker unlinks any existing socket before
@@ -73,12 +75,8 @@ export declare function situationalContextPath(nodeId: string): string;
73
75
  export declare function passivePath(nodeId: string): string;
74
76
  export declare function transcriptPath(nodeId: string): string;
75
77
  export declare function sessionPtrPath(nodeId: string): string;
76
- /** On-read injection dedup set — the doc realpaths already surfaced on-read in
77
- * this node's CURRENT conversation transcript. Persisted so the once-per-
78
- * transcript dedup survives a dormancy → revive(resume) cycle: a resume reuses
79
- * the same .jsonl transcript in a NEW pi process, so the in-memory set would
80
- * otherwise start empty and re-inject docs already present in the transcript.
81
- * Deleted by the launch paths that start a FRESH transcript. */
78
+ /** Frozen preference snapshot and rank-aware exposure state for the node's
79
+ * currently loaded LLM context. The filename remains stable across schema versions. */
82
80
  export declare function injectedDocsPath(nodeId: string): string;
83
81
  /** Create the full directory skeleton for a node. Idempotent. */
84
82
  export declare function ensureNodeDirs(nodeId: string): void;
@@ -36,6 +36,12 @@ export function crtrHome() {
36
36
  export function canvasDbPath() {
37
37
  return join(crtrHome(), 'canvas.db');
38
38
  }
39
+ export function onDiskMigrationLockPath() {
40
+ return join(crtrHome(), 'on-disk-migration.lock');
41
+ }
42
+ export function onDiskMigrationMarkerPath() {
43
+ return join(crtrHome(), 'on-disk-migrations.json');
44
+ }
39
45
  /** Absolute path to a node's broker `view.sock` — always `view.sock` under the
40
46
  * node dir. Stale socket files (a host reboot, a sandbox recreate on a
41
47
  * durable volume) are harmless: the broker unlinks any existing socket before
@@ -159,12 +165,8 @@ export function transcriptPath(nodeId) {
159
165
  export function sessionPtrPath(nodeId) {
160
166
  return join(nodeDir(nodeId), 'session.ptr');
161
167
  }
162
- /** On-read injection dedup set — the doc realpaths already surfaced on-read in
163
- * this node's CURRENT conversation transcript. Persisted so the once-per-
164
- * transcript dedup survives a dormancy → revive(resume) cycle: a resume reuses
165
- * the same .jsonl transcript in a NEW pi process, so the in-memory set would
166
- * otherwise start empty and re-inject docs already present in the transcript.
167
- * Deleted by the launch paths that start a FRESH transcript. */
168
+ /** Frozen preference snapshot and rank-aware exposure state for the node's
169
+ * currently loaded LLM context. The filename remains stable across schema versions. */
168
170
  export function injectedDocsPath(nodeId) {
169
171
  return join(nodeDir(nodeId), 'injected-docs.json');
170
172
  }
@@ -20,7 +20,7 @@ import { jobDir, contextDir } from './paths.js';
20
20
  import { isPidAlive } from './pid.js';
21
21
  import { resolveNodeVisual, faultSummary } from './status-glyph.js';
22
22
  import { readFault } from '../runtime/fault.js';
23
- import { isLegacyRuntimeText, parseCard } from '../../shared/generated-context.js';
23
+ import { parseCard } from '../../shared/generated-context.js';
24
24
  // ---------------------------------------------------------------------------
25
25
  // Active-fault overlay: stale daemon-owned provider faults are hidden once the
26
26
  // broker pid is gone; manual/client faults stay visible until cleared.
@@ -192,7 +192,7 @@ export function readSessionParts(sessionFile) {
192
192
  const text = extractUserText(rec.message.content);
193
193
  if (text === '')
194
194
  continue;
195
- if (parseCard({ role: 'user', content: text }) !== null || isLegacyRuntimeText(text))
195
+ if (parseCard({ role: 'user', content: text }) !== null)
196
196
  continue;
197
197
  const capped = text.length > CONVO_MSG_CAP ? text.slice(0, CONVO_MSG_CAP) : text;
198
198
  parts.push(capped);