@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
@@ -7,104 +7,165 @@ import { existsSync, readdirSync, realpathSync, rmdirSync, statSync } from 'node
7
7
  import { dirname, join, resolve as resolvePath } from 'node:path';
8
8
  import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
9
9
  import { stateBlock } from '../../core/help.js';
10
+ import { loadStoreMemoryDocs, openProjectMemoryStore, } from '../../core/memory-resolver.js';
11
+ import { canonicalSegments, physicalRelativePathFor, stripNamespace, validateExplicitLocalName, } from '../../core/memory/identity.js';
12
+ import { pathExists, realpathOrSelf } from '../../core/fs-utils.js';
10
13
  import { usage } from '../../core/errors.js';
11
14
  import { memoryExtensionValidationCatalog, } from '../../core/memory/extensions.js';
12
15
  import { CRTR_DIR_NAME } from '../../types.js';
13
- import { scopeMemoryDir, projectScopeRoot, ensureProjectScopeRoot, resetScopeCache, } from '../../core/scope.js';
16
+ import { NEUTRAL_PROJECT_MEMORY, scopeMemoryDir, projectScopeRoot, ensureProjectScopeRoot, resetScopeCache, } from '../../core/scope.js';
14
17
  import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
15
18
  import { memoryDir as nodeMemoryDir } from '../../core/runtime/memory.js';
16
19
  import { SURFACE_EVENTS, SURFACE_RUNGS } from '../../core/substrate/schema.js';
20
+ import { exposureTarget, loadContextExposureState, registerExposure, saveContextExposureState, } from '../../core/substrate/injected-store.js';
17
21
  // The two memory kinds — knowledge (consult: procedural playbooks + factual
18
22
  // references merged) vs preference (behave: standing directives). Used as the
19
23
  // `--kind` enum choices everywhere.
20
24
  export const MEMORY_KINDS = ['knowledge', 'preference'];
21
- // Scope choices for filtering / targeting (builtin is read-only, not writable).
25
+ // Scope choices for READ-ONLY source filtering — every scope a target view
26
+ // mounts, builtin included. A scope narrows the source stack only: the query is
27
+ // still the full canonical name.
28
+ export const MEMORY_READ_SCOPES = ['user', 'project', 'profile', 'node', 'builtin'];
29
+ // Scope choices a mutation may target. builtin ships with the package and
30
+ // installed-plugin stores are managed by `crtr pkg`, so neither is writable.
22
31
  // `node` is the this-node store, writable only inside a running node.
23
- export const MEMORY_SCOPES = ['user', 'project', 'profile', 'node'];
32
+ export const MEMORY_WRITE_SCOPES = ['user', 'project', 'profile', 'node'];
33
+ // Retained for the leaves that have not yet split their read filter from their
34
+ // write target; each cuts over to the pair above in its own phase.
35
+ export const MEMORY_SCOPES = MEMORY_WRITE_SCOPES;
24
36
  /** Scope sort weight matching resolution precedence (node > project stack >
25
37
  * profile > user > builtin). Used by `list` for its "scope then kind then
26
38
  * name" ordering. */
27
39
  export function scopeRank(scope) {
28
40
  return scope === 'node' ? -1 : scope === 'project' ? 0 : scope === 'profile' ? 1 : scope === 'user' ? 2 : 3;
29
41
  }
30
- /** Resolve the write target scope + its memory dir. Default: project when a
31
- * project scope exists for the cwd, else user. An explicit `--scope project`
32
- * with no project scope yet scaffolds one (ensureProjectScopeRoot). User scope
33
- * always resolves. `--scope profile` is NEVER a default — it requires a
34
- * selected profile, from `profileArg` (an explicit `--profile <id-or-name>`)
35
- * or else the process's `CRTR_PROFILE_ID`, resolved through the centralized
36
- * `loadProfileManifest` (never a raw path join). `dirArg` (an explicit
37
- * `--dir <path>`) pins the EXACT project directory, cwd-free: the target is
38
- * `<dir>/.crouter/memory/`, scaffolded if absent, never the cwd-ancestor walk
39
- * — the way a profiled agent writes into any project in its purview, and the
40
- * only way to target a dir shadowed by an ancestor store. `--scope node`
41
- * targets the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`) and
42
- * requires a running node. Returns the absolute `<root>/memory` dir to write
43
- * under. */
44
- export function resolveWriteTarget(scopeArg, profileArg, dirArg) {
45
- if (dirArg !== undefined && dirArg !== '') {
46
- if (scopeArg !== undefined && scopeArg !== 'project') {
47
- throw usage(`--dir targets a project store and cannot combine with --scope ${scopeArg}`);
48
- }
49
- if (profileArg !== undefined && profileArg !== '') {
50
- throw usage('--profile only applies to --scope profile; --dir targets a project store', {
51
- received: profileArg,
52
- next: 'Drop --profile when using --dir.',
53
- });
54
- }
55
- const abs = resolvePath(dirArg);
56
- if (!existsSync(abs) || !statSync(abs).isDirectory()) {
57
- throw usage(`--dir does not exist or is not a directory: ${dirArg}`, {
58
- received: dirArg,
59
- next: 'Pass an existing project directory.',
60
- });
61
- }
62
- let real = abs;
63
- try {
64
- real = realpathSync(abs);
65
- }
66
- catch {
67
- /* fall back to the resolved path */
68
- }
69
- // The scaffold (mkdir of `.crouter/memory/`) happens at write time via the
70
- // writer's ensureDir; drop the scope cache so this process's later
71
- // resolves see the new root.
72
- resetScopeCache();
73
- return { scope: 'project', memoryDir: join(real, CRTR_DIR_NAME, 'memory') };
42
+ export function registerMemoryListingExposure(identity) {
43
+ const nodeId = process.env['CRTR_NODE_ID'];
44
+ if (nodeId === undefined || nodeId === '')
45
+ return;
46
+ const state = loadContextExposureState(nodeId);
47
+ registerExposure(exposureTarget(state, 'transcript'), identity, 'content');
48
+ saveContextExposureState(nodeId, state);
49
+ }
50
+ function rejectDirConflicts(input) {
51
+ if (input.scope !== undefined && input.scope !== '') {
52
+ throw usage(`--dir selects the project store itself and cannot combine with --scope ${input.scope}`, {
53
+ received: `--dir ${input.dir} --scope ${input.scope}`,
54
+ next: 'Drop --scope: --dir already pins the exact project store.',
55
+ });
56
+ }
57
+ if (input.profile !== undefined && input.profile !== '') {
58
+ throw usage('--profile only applies to --scope profile; --dir selects a project store', {
59
+ received: input.profile,
60
+ next: 'Drop --profile when using --dir.',
61
+ });
62
+ }
63
+ }
64
+ /** The realpathed project owner directory `--dir` names. */
65
+ function ownerDirFromArg(dirArg) {
66
+ const abs = resolvePath(dirArg);
67
+ if (!existsSync(abs) || !statSync(abs).isDirectory()) {
68
+ throw usage(`--dir does not exist or is not a directory: ${dirArg}`, {
69
+ received: dirArg,
70
+ next: 'Pass an existing project directory.',
71
+ });
72
+ }
73
+ try {
74
+ return realpathSync(abs);
74
75
  }
75
- let scope;
76
- if (scopeArg === 'user' || scopeArg === 'project' || scopeArg === 'profile' || scopeArg === 'node') {
77
- scope = scopeArg;
76
+ catch {
77
+ return abs;
78
78
  }
79
- else if (scopeArg !== undefined) {
80
- throw usage(`invalid --scope: ${scopeArg} (expected user|project|profile|node)`);
79
+ }
80
+ function parseScope(scopeArg, allowed) {
81
+ if (scopeArg === undefined || scopeArg === '')
82
+ return undefined;
83
+ if (!allowed.includes(scopeArg)) {
84
+ throw usage(`invalid --scope: ${scopeArg} (expected ${allowed.join('|')})`);
81
85
  }
82
- else {
83
- scope = projectScopeRoot() !== null ? 'project' : 'user';
86
+ return scopeArg;
87
+ }
88
+ /** A store with no namespace of its own: user, profile, node. */
89
+ function nativeStoreDescriptor(scope, memoryDir) {
90
+ return {
91
+ scope,
92
+ storeRoot: memoryDir,
93
+ namespace: '',
94
+ mountStatus: pathExists(memoryDir) ? 'ready' : 'absent',
95
+ projectMemory: NEUTRAL_PROJECT_MEMORY,
96
+ };
97
+ }
98
+ /** Resolve the read selection: a scope filter, or the exact store `--dir`
99
+ * names. */
100
+ export function resolveReadSelector(input) {
101
+ if (input.dir !== undefined && input.dir !== '') {
102
+ rejectDirConflicts(input);
103
+ return { store: openProjectMemoryStore(ownerDirFromArg(input.dir)) };
104
+ }
105
+ if (input.profile !== undefined && input.profile !== '' && input.scope !== 'profile') {
106
+ throw usage(`--profile only applies to --scope profile (received scope: ${input.scope ?? 'none'})`, {
107
+ received: input.profile,
108
+ next: 'Drop --profile, or pass --scope profile to target a specific profile store.',
109
+ });
84
110
  }
85
- if (profileArg !== undefined && profileArg !== '' && scope !== 'profile') {
111
+ const scope = parseScope(input.scope, MEMORY_READ_SCOPES);
112
+ if (input.profile !== undefined && input.profile !== '') {
113
+ // Naming a profile selects THAT profile's store exactly; the ambient
114
+ // selection would otherwise answer for whichever profile this process runs
115
+ // under, silently ignoring the flag.
116
+ const { profileId } = loadProfileManifest(input.profile);
117
+ return { scope: 'profile', store: nativeStoreDescriptor('profile', profileMemoryDir(profileId)) };
118
+ }
119
+ return { ...(scope === undefined ? {} : { scope }), store: null };
120
+ }
121
+ /** Resolve the one store a mutation writes to. Default: project when a project
122
+ * scope exists for the cwd, else user. An explicit `--scope project` with no
123
+ * project scope yet scaffolds one (`ensureProjectScopeRoot`). `--scope
124
+ * profile` is NEVER a default — it requires a selected profile, from
125
+ * `--profile <id-or-name>` or else the process's `CRTR_PROFILE_ID`, resolved
126
+ * through the centralized `loadProfileManifest` (never a raw path join).
127
+ * `--scope node` targets the this-node store
128
+ * (`nodes/<CRTR_NODE_ID>/context/memory/`) and requires a running node. */
129
+ export function resolveWriteSelector(input) {
130
+ if (input.dir !== undefined && input.dir !== '') {
131
+ rejectDirConflicts(input);
132
+ const owner = ownerDirFromArg(input.dir);
133
+ // The scaffold (mkdir of `.crouter/memory/`) happens at write time via the
134
+ // writer's ensureDir; drop the scope cache so this process's later resolves
135
+ // see the new root.
136
+ resetScopeCache();
137
+ return {
138
+ scope: 'project',
139
+ memoryDir: join(owner, CRTR_DIR_NAME, 'memory'),
140
+ store: openProjectMemoryStore(owner),
141
+ };
142
+ }
143
+ const requested = parseScope(input.scope, MEMORY_WRITE_SCOPES);
144
+ const scope = requested ?? (projectScopeRoot() !== null ? 'project' : 'user');
145
+ if (input.profile !== undefined && input.profile !== '' && scope !== 'profile') {
86
146
  throw usage(`--profile only applies to --scope profile (resolved scope: ${scope})`, {
87
- received: profileArg,
147
+ received: input.profile,
88
148
  next: 'Drop --profile, or pass --scope profile to target a specific profile store.',
89
149
  });
90
150
  }
91
151
  if (scope === 'node') {
92
- // The this-node store: `nodes/<CRTR_NODE_ID>/context/memory/`. Available
93
- // only inside a running node; the same dir render.ts's nodeLocalDocs loads
94
- // at boot, so a doc written here rides into this node's knowledge block.
152
+ // The this-node store: the same dir render.ts's nodeLocalDocs loads at
153
+ // boot, so a doc written here rides into this node's knowledge block.
95
154
  const nodeId = process.env['CRTR_NODE_ID'] || '';
96
155
  if (nodeId === '') {
97
156
  throw usage('node scope requires a running node (CRTR_NODE_ID unset); rerun inside a node');
98
157
  }
99
- return { scope: 'node', memoryDir: nodeMemoryDir(nodeId) };
158
+ const memoryDir = nodeMemoryDir(nodeId);
159
+ return { scope, memoryDir, store: nativeStoreDescriptor(scope, memoryDir) };
100
160
  }
101
161
  if (scope === 'profile') {
102
- const profileIdOrName = profileArg && profileArg !== '' ? profileArg : process.env['CRTR_PROFILE_ID'] || '';
162
+ const profileIdOrName = input.profile && input.profile !== '' ? input.profile : process.env['CRTR_PROFILE_ID'] || '';
103
163
  if (profileIdOrName === '') {
104
164
  throw usage('profile scope requires a selected profile; rerun inside a profiled node or pass --profile');
105
165
  }
106
166
  const { profileId } = loadProfileManifest(profileIdOrName);
107
- return { scope: 'profile', memoryDir: profileMemoryDir(profileId) };
167
+ const memoryDir = profileMemoryDir(profileId);
168
+ return { scope, memoryDir, store: nativeStoreDescriptor(scope, memoryDir) };
108
169
  }
109
170
  let memoryDir = scopeMemoryDir(scope);
110
171
  if (!memoryDir && scope === 'project') {
@@ -113,17 +174,166 @@ export function resolveWriteTarget(scopeArg, profileArg, dirArg) {
113
174
  }
114
175
  if (!memoryDir)
115
176
  throw usage(`no ${scope} scope available for writing memory documents`);
177
+ if (scope === 'project') {
178
+ return { scope, memoryDir, store: openProjectMemoryStore(dirname(dirname(memoryDir))) };
179
+ }
180
+ return { scope, memoryDir, store: nativeStoreDescriptor(scope, memoryDir) };
181
+ }
182
+ /** The scope + memory dir a write lands in. Retained for the leaves that still
183
+ * place documents by path alone; they cut over to `resolveWriteSelector` and
184
+ * `planNewDocumentPlacement` in their own phases. */
185
+ export function resolveWriteTarget(scopeArg, profileArg, dirArg) {
186
+ const { scope, memoryDir } = resolveWriteSelector({ scope: scopeArg, profile: profileArg, dir: dirArg });
116
187
  return { scope, memoryDir };
117
188
  }
189
+ /** Refuse a project store the runtime will not resolve through. A project
190
+ * repository-root store carries the identity every canonical name in it composes under, so
191
+ * without a valid declaration no name can be validated, stripped, or placed;
192
+ * user, profile, and node stores have no declaration and always pass, even
193
+ * before their directory exists. */
194
+ export function requireMountedStore(store) {
195
+ if (store.scope !== 'project' || store.mountStatus === 'ready')
196
+ return store;
197
+ const owner = store.ownerDir ?? store.storeRoot;
198
+ if (store.mountStatus === 'absent') {
199
+ throw usage(`no memory store at ${store.storeRoot}`, {
200
+ next: `Initialize it with \`crtr memory write <namespace> --dir ${owner}\`, which declares the repository namespace.`,
201
+ });
202
+ }
203
+ const repositoryRoot = store.repositoryRoot ?? owner;
204
+ throw usage(store.diagnostic ?? `project memory store at ${store.storeRoot} does not declare a namespace`, {
205
+ next: `Run \`crtr sys migrate --dir ${repositoryRoot}\`.`,
206
+ });
207
+ }
208
+ // ---------------------------------------------------------------------------
209
+ // Canonical name → physical placement
210
+ //
211
+ // Every surface takes the FULL canonical name. Placement strips the selected
212
+ // store's namespace off it, checks that neither the identity nor either
213
+ // physical representation of the local name is already taken, and picks the
214
+ // form the new file takes.
215
+ // ---------------------------------------------------------------------------
216
+ /** Normalize and validate a full canonical name: ordering prefixes are
217
+ * physical-path-only, a trailing `INDEX` is never an address, and the segment
218
+ * charset is fixed. */
219
+ export function requireCanonicalName(rawName) {
220
+ const name = canonicalSegments(rawName.trim()).join('/');
221
+ if (name === '')
222
+ throw usage('memory document name required');
223
+ const valid = validateExplicitLocalName(name);
224
+ if (!valid.ok) {
225
+ throw usage(`invalid memory document name \`${rawName}\`: ${valid.reason}`, {
226
+ received: rawName,
227
+ next: 'Pass the full canonical name (a project document is <namespace>/<local name>); run `crtr memory find <leaf>` to look one up.',
228
+ });
229
+ }
230
+ return name;
231
+ }
232
+ /** The store-local name a full canonical name has inside one exact store, or a
233
+ * usage error when it falls outside that store's namespace. `''` names the
234
+ * store's own root document — a project store answers at its namespace. */
235
+ export function requireLocalNameInStore(store, rawName) {
236
+ requireMountedStore(store);
237
+ const name = requireCanonicalName(rawName);
238
+ if (store.namespace === '')
239
+ return name;
240
+ const local = stripNamespace(name, store.namespace);
241
+ if (local === null) {
242
+ throw usage(`\`${name}\` is not in the namespace of the store at ${store.storeRoot} (\`${store.namespace}\`)`, {
243
+ memory: name,
244
+ next: `Every document there answers at \`${store.namespace}/<local name>\`; the store's own document answers at \`${store.namespace}\`.`,
245
+ });
246
+ }
247
+ return local;
248
+ }
249
+ /** Where a NEW document (a `write`, a `move` destination) physically lands in
250
+ * one exact store, after proving the canonical identity is free.
251
+ *
252
+ * Both physical forms carry the same identity, so occupancy is checked against
253
+ * the loaded identities AND both `<local>.md` and `<local>/INDEX.md` — a file
254
+ * whose explicit `name:` points elsewhere still owns its path. The chosen form
255
+ * is `<local>/INDEX.md` when the canonical directory already has descendants
256
+ * in this store (the document fronts a directory that exists), else
257
+ * `<local>.md`; descendants created later never relocate it, because either
258
+ * form validly owns the identity. Pass `storeDocs` when the caller already
259
+ * loaded the store, and `source` when planning a move, whose own identity and
260
+ * file are being vacated. */
261
+ export function planNewDocumentPlacement(store, rawName, storeDocs, source) {
262
+ const localName = requireLocalNameInStore(store, rawName);
263
+ const canonicalName = requireCanonicalName(rawName);
264
+ const docs = storeDocs ?? loadStoreMemoryDocs(store, true);
265
+ // A move's own source occupies neither the identity nor the path it is
266
+ // vacating: retiring an explicit `name:` pin back to the file's path-derived
267
+ // identity lands the document exactly where it already sits.
268
+ const sourceReal = source === undefined ? null : realpathOrSelf(source.path);
269
+ const owner = docs.find((d) => d.name === canonicalName && realpathOrSelf(d.path) !== sourceReal);
270
+ if (owner !== undefined) {
271
+ throw usage(`${canonicalName} already exists in ${store.storeRoot}`, {
272
+ memory: canonicalName,
273
+ path: owner.path,
274
+ next: `Revise it with \`crtr memory edit ${canonicalName}\`, which records the change, or move it with \`crtr memory move\`.`,
275
+ });
276
+ }
277
+ const forms = localName === '' ? ['directory-index'] : ['directory-index', 'leaf-file'];
278
+ const taken = forms
279
+ .map((representation) => join(store.storeRoot, physicalRelativePathFor(localName, representation)))
280
+ .filter((path) => pathExists(path) && realpathOrSelf(path) !== sourceReal);
281
+ if (taken.length > 0) {
282
+ throw usage(`${canonicalName} cannot be created: ${taken.join(' and ')} already exists`, {
283
+ memory: canonicalName,
284
+ path: taken.join(', '),
285
+ next: 'Both `<name>.md` and `<name>/INDEX.md` carry that identity. Read the existing file, then edit, move, or delete it.',
286
+ });
287
+ }
288
+ const representation = localName === '' || docs.some((d) => d.localName.startsWith(`${localName}/`)) ? 'directory-index' : 'leaf-file';
289
+ const physicalRelativePath = physicalRelativePathFor(localName, representation);
290
+ return {
291
+ store,
292
+ canonicalName,
293
+ localName,
294
+ representation,
295
+ physicalRelativePath,
296
+ path: join(store.storeRoot, physicalRelativePath),
297
+ };
298
+ }
299
+ /** The document a mutation acts on, refusing an identity two files in ONE
300
+ * store both own. Reads stay answerable (`<name>/INDEX.md` deterministically
301
+ * wins), but a mutation would silently pick one of two files, so it stops with
302
+ * both paths. Candidates in OTHER stores are ordinary alternates — precedence
303
+ * picked the winner and `--dir` reaches the rest. */
304
+ export function requireUnambiguousDoc(set) {
305
+ const winner = set.winner;
306
+ const sameStore = set.candidates.filter((d) => d.store.storeRoot === winner.store.storeRoot);
307
+ if (sameStore.length > 1) {
308
+ throw usage(`${set.name} is owned by ${sameStore.length} files in one store: ${sameStore.map((d) => d.path).join(' and ')}`, {
309
+ memory: set.name,
310
+ path: sameStore.map((d) => d.path).join(', '),
311
+ next: `Recover the collision with \`crtr memory move ${set.name} --to <new-free-name>\`; it selects the first physical path in stable lexical order. Do not remove files by hand; \`crtr memory lint\` reports every such collision.`,
312
+ });
313
+ }
314
+ return winner;
315
+ }
316
+ export function selectMutationDoc(set) {
317
+ const sameStore = set.candidates.filter((d) => d.store.storeRoot === set.winner.store.storeRoot);
318
+ const ordered = [...sameStore].sort((a, b) => {
319
+ const byRelativePath = a.physicalRelativePath.localeCompare(b.physicalRelativePath);
320
+ return byRelativePath !== 0 ? byRelativePath : a.path.localeCompare(b.path);
321
+ });
322
+ return {
323
+ doc: ordered[0] ?? set.winner,
324
+ collisionPaths: ordered.length > 1 ? ordered.map((d) => d.path) : [],
325
+ };
326
+ }
118
327
  /** Remove a just-removed file's now-empty parent directories up to (never
119
- * including) its tree root, so removing the last entry under an `area/`
120
- * prefix does not orphan an empty directory. The root is derived by walking
121
- * up one dirname per name segment: a doc named `a/b` sits at `<root>/a/b.md`,
122
- * so its root is two dirnames above — the same arithmetic holds for a
123
- * `.history/<name>.jsonl` sidecar. Stops at the first non-empty ancestor. */
124
- export function pruneEmptyParents(filePath, nameSegments) {
328
+ * including) its store root, so removing the last entry under an `area/`
329
+ * prefix does not orphan an empty directory. `pathSegments` counts the
330
+ * PHYSICAL store-relative path (`doc.physicalRelativePath.split('/').length`),
331
+ * never the canonical name, which carries the store's namespace on top of it —
332
+ * the same count addresses a `.history/<path>.jsonl` sidecar. Stops at the
333
+ * first non-empty ancestor. */
334
+ export function pruneEmptyParents(filePath, pathSegments) {
125
335
  let treeRoot = filePath;
126
- for (let i = 0; i < nameSegments; i += 1)
336
+ for (let i = 0; i < pathSegments; i += 1)
127
337
  treeRoot = dirname(treeRoot);
128
338
  let dir = dirname(filePath);
129
339
  while (dir !== treeRoot && dir.startsWith(treeRoot) && existsSync(dir) && readdirSync(dir).length === 0) {
@@ -131,8 +341,10 @@ export function pruneEmptyParents(filePath, nameSegments) {
131
341
  dir = dirname(dir);
132
342
  }
133
343
  }
134
- /** Map a path-derived name (`topic` or `area/topic`) to its file path under a
135
- * memory dir, guarding against traversal/absolute escapes. */
344
+ /** Map a STORE-LOCAL name (`topic`, `area/topic`, `INDEX`) to its file path
345
+ * under a memory dir, guarding against traversal/absolute escapes. Physical
346
+ * mapping only: it composes no namespace and collapses no `INDEX`, so a
347
+ * canonical name reaches it through `planNewDocumentPlacement`. */
136
348
  export function memoryFilePath(memoryDir, name) {
137
349
  const segments = name.split('/').filter((s) => s.length > 0);
138
350
  if (segments.length === 0)
@@ -503,12 +715,56 @@ export function applyExtensionChanges(frontmatter, sets, unsets) {
503
715
  /** The frontmatter-overlay flags, keyed by flag name, carrying the prose true
504
716
  * on BOTH leaves. `write` appends its creation clauses with `withConstraint`;
505
717
  * neither leaf restates the other's rules. */
718
+ /** The selector flags, keyed by flag name, carrying the prose true on every
719
+ * leaf that accepts them. A leaf appends its own clause with `selectorParam`;
720
+ * none of them restates the shared rules. */
721
+ export const SELECTOR_PARAMS = {
722
+ 'scope-read': {
723
+ kind: 'flag',
724
+ name: 'scope',
725
+ type: 'enum',
726
+ choices: [...MEMORY_READ_SCOPES],
727
+ required: false,
728
+ constraint: 'Restrict resolution to one source class. A source filter only: the name stays the full canonical name, and project scope keeps its nearest-first precedence. Conflicts with --dir, which selects one exact store instead.',
729
+ },
730
+ 'scope-write': {
731
+ kind: 'flag',
732
+ name: 'scope',
733
+ type: 'enum',
734
+ choices: [...MEMORY_WRITE_SCOPES],
735
+ required: false,
736
+ constraint: 'Store to write to (builtin documents ship with the package and installed-plugin documents are managed by `crtr pkg`, so neither is writable). Default: project when cwd sits in a project, else user. `project` resolves to the NEAREST ancestor `.crouter/` walking up from cwd — in a nested workspace that can be a parent’s store, not the dir you are standing in; pass --dir to pin the exact project. `profile` requires a selected profile (CRTR_PROFILE_ID) or an explicit --profile. `node` writes the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`), the nearest scope, seen only by this running node. Conflicts with --dir.',
737
+ },
738
+ 'dir': {
739
+ kind: 'flag',
740
+ name: 'dir',
741
+ type: 'string',
742
+ required: false,
743
+ constraint: 'Exact project directory to operate in — selects `<dir>/.crouter/memory/` by realpath, regardless of cwd, ancestor stores, or which store a target view would pick. THE way to reach one specific project store: another project in your profile’s purview, a store shadowed by an ancestor, or a non-winning duplicate such as a linked worktree that declares the same namespace as its main checkout. The name is still the FULL canonical name — the repository namespace plus that store’s path under the repository root, plus the store-local name. It selects the store itself, so it conflicts with any explicit --scope and with --profile.',
744
+ },
745
+ 'profile': {
746
+ kind: 'flag',
747
+ name: 'profile',
748
+ type: 'string',
749
+ required: false,
750
+ constraint: 'Profile id or name to target, for --scope profile. Names that profile’s store exactly, whichever profile this process runs under. Default: the process CRTR_PROFILE_ID (the node’s selected profile). Resolved through the same profile lookup as `crtr profile show`. Rejected (usage error) unless --scope profile is also selected — never silently ignored for another scope — and rejected with --dir.',
751
+ },
752
+ };
753
+ /** One selector flag, with leaf-specific prose appended to the shared
754
+ * constraint and any leaf-specific schema override applied. */
755
+ export function selectorParam(name, overrides = {}, extraConstraint) {
756
+ const base = SELECTOR_PARAMS[name];
757
+ if (base === undefined)
758
+ throw new Error(`unknown selector param: ${name}`);
759
+ const constraint = extraConstraint === undefined ? base.constraint : `${base.constraint} ${extraConstraint}`;
760
+ return { ...base, ...overrides, constraint };
761
+ }
506
762
  export const FRONTMATTER_OVERLAY_PARAMS = {
507
763
  'kind': { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Document kind.' },
508
764
  'when-and-why-to-read': { kind: 'flag', name: 'when-and-why-to-read', type: 'string', required: false, constraint: 'ONE routing sentence: "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the reader\u2019s payoff \u2014 the consequence they secure for their task by reading \u2014 NEVER the doc summary, its rule, or that rule reworded as an outcome (a benefit-shaped restatement still fails). Rendered verbatim as the preview.' },
509
765
  'short-form': { kind: 'flag', name: 'short-form', type: 'string', required: false, constraint: 'Frontmatter short-form \u2014 a very abbreviated version of the content, the hook shown in `crtr memory list`.' },
510
766
  'unlisted': { kind: 'flag', name: 'unlisted', type: 'bool', required: false, default: false, constraint: 'Suppress this doc from directory listings. Suppression only — explicit reads, [[links]], and surfaces entries still work.' },
511
- 'surface': { kind: 'flag', name: 'surface', type: 'string', required: false, repeatable: true, constraint: 'One routing entry per occurrence, as a YAML/JSON object `{on, at, match?, match-frontmatter?, gate?}`; the flag set replaces the document’s whole surfaces list. `on` is boot|workspace-open|read|memory-read|command; `at` is name|preview|content. `match` holds the event’s globs — required on read/memory-read/command (a read entry may carry `match-frontmatter`, a predicate over the read file’s own frontmatter, instead), meaningless on boot/workspace-open. Optional entry `gate` is a node-config predicate using the same vocabulary as the document gate; its event constraints and gate both match before it participates. A `./`-anchored glob is relative: for `read` to the store’s owning repo dir, for `memory-read` to the doc’s own name directory. Participating entries fold to their highest `at`; there is no cross-entry deny precedence.' },
767
+ 'surface': { kind: 'flag', name: 'surface', type: 'string', required: false, repeatable: true, constraint: 'One routing entry per occurrence, as a YAML/JSON object `{on, at, match?, match-frontmatter?, gate?}`; the flag set replaces the document’s whole surfaces list. `on` is boot|workspace-open|read|memory-read|command; `at` is name|preview|content. `match` holds the event’s globs — required on read/memory-read/command (a read entry may carry `match-frontmatter`, a predicate over the read file’s own frontmatter, instead), meaningless on boot/workspace-open. Optional entry `gate` is a node-config predicate using the same vocabulary as the document gate; its event constraints and gate both match before it participates. A `./`-anchored glob is relative: for `read` to the store’s owning repo dir, for `memory-read` to this doc’s routing anchor — its own canonical name when the doc is its directory’s document (`<dir>/INDEX.md`), otherwise the canonical directory it sits in. Participating entries fold to their highest `at`; there is no cross-entry deny precedence.' },
512
768
  'gate': { kind: 'flag', name: 'gate', type: 'string', required: false, constraint: 'Frontmatter gate \u2014 YAML/JSON object predicate over node config using the same field/matcher vocabulary described in the guide.' },
513
769
  'slash': { kind: 'flag', name: 'slash', type: 'bool', required: false, default: false, constraint: 'Presence flags this doc invocable as a pi slash command (`/<name>`, `/` in a nested name rendered as `:`) \u2014 the doc body becomes the command\u2019s injected prompt. Default false: most docs are consulted, not invoked.' },
514
770
  };
@@ -526,7 +782,8 @@ export function overlayParam(name, overrides = {}, extraConstraint) {
526
782
  * `--doc-rationale`, because `--rationale` there means why THIS REVISION is
527
783
  * happening. Same field, same prose, two flag names that cannot be confused. */
528
784
  export const DOC_RATIONALE_CONSTRAINT = 'Frontmatter rationale \u2014 the observed agent failure that made this doc necessary. Maintainer-facing only: never ships in any delivered surface (boot render, on-read injection, `memory read` content), visible only via `memory read --frontmatter`. Omitting the flag preserves an existing rationale unchanged.';
529
- export const GUIDE_SURFACES = 'Every doc appears in its directory’s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing — entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc’s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file’s absolute path and basename, `./`-anchored globs vs its path relative to the store’s owning repo dir, `match-frontmatter` predicates over the read file’s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc’s canonical name, `./` anchored to this doc’s own name directory); `command` fires when a matching shell command runs (globs vs the whole command string, `*` crossing `/`). An entry may also carry `gate`, a node-config predicate using the same vocabulary as the document-level `gate`: event constraints and entry gate both must match, then all participating entries fold to the highest `at` (`content` > `preview` > `name`), with no cross-entry deny precedence. The document-level `gate` remains the hard eligibility check: when it fails, no automatic surface can deliver the document. `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Explicit `memory read` and listings remain deliberate access and ignore gates/rungs; only companion docs routed by their `memory-read` entries use entry gates. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly — a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.';
785
+ export const GUIDE_SURFACES = 'Every doc appears in its directory’s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing — entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc’s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file’s absolute path and basename, `./`-anchored globs vs its path relative to the store’s owning repo dir, `match-frontmatter` predicates over the read file’s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc’s canonical name, `./` anchored to this doc’s routing anchor — its own canonical name when it is its directory’s document, otherwise the canonical directory it sits in); `command` fires when a matching shell command runs (globs vs the whole command string, `*` crossing `/`). An entry may also carry `gate`, a node-config predicate using the same vocabulary as the document-level `gate`: event constraints and entry gate both must match, then all participating entries fold to the highest `at` (`content` > `preview` > `name`), with no cross-entry deny precedence. The document-level `gate` remains the hard eligibility check: when it fails, no automatic surface can deliver the document. `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Explicit `memory read` and listings remain deliberate access and ignore gates/rungs; only companion docs routed by their `memory-read` entries use entry gates. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly — a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.';
530
786
  export const GUIDE_ROUTING_LINE = 'The routing line (--when-and-why-to-read) is the only text an agent reads before deciding to load the doc. The test for its when-clause: it names an observable circumstance in the reader\u2019s current work, not the doc\u2019s topic reworded as an activity. If the WHEN can be inferred from the title alone, it is not a real trigger. Bad on a todo list: "When planning or prioritizing work across this profile." Good: "When the user mentions something from their todos, or asks what is still outstanding across this profile." The test for its because-clause: if it can be derived by paraphrasing the doc\u2019s advice, it is a restatement, not a payoff \u2014 a real payoff names a consequence in the reader\u2019s world that the document itself never asserts. Bad: "because only genuine first principles belong in taste memory." Bad: "because keeping the test loop fast and free of speculative tests protects the development pace" \u2014 the doc\u2019s rule as an outcome, derivable straight from its advice. Good: "because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation." Someone mid-task who has not read the doc must be able to decide from this line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory \u2014 ask the user one sharp question instead of improvising.';
531
787
  export const GUIDE_PREDICATE_VOCABULARY = 'Document gates, surface-entry gates, and match-frontmatter share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.';
532
- export const GUIDE_DOC_LINKS = 'Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes. A bare directory name is a valid link too: following it returns that directory\u2019s listing, a browse entrance rather than a doc. Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document or directory, and `crtr memory read` lists a doc\u2019s resolvable links alongside its body. There is no alias or label form.';
788
+ export const GUIDE_DOC_LINKS = 'Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes, namespace included for a project document. A bare directory name is a valid link too: following it returns that directory\u2019s own document when it has one, plus the directory listing. Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document or directory, and `crtr memory read` lists a doc\u2019s resolvable links alongside its body. There is no alias, label, or old-name form \u2014 a renamed target needs its links rewritten.';
789
+ export const GUIDE_CANONICAL_NAMES = 'A document has exactly one address: its canonical name, the store\u2019s namespace composed with the document\u2019s store-local name. A repository declares its namespace once, as `namespace:` on the root `INDEX.md` of `<repository root>/.crouter/memory/`; a store nested inside that repository composes under the repository namespace plus its own directory path relative to the repository root, and declares nothing itself. An installed plugin\u2019s store mounts under the plugin\u2019s own name; user, profile, node, and builtin stores have no namespace, so their local names are already canonical. A local doc `unit-tests` in a store that declares `namespace: acme/core` therefore answers only at `acme/core/unit-tests`, and the store\u2019s root document answers at `acme/core`; a store at `packages/api/.crouter/memory/` in a repository declaring `namespace: acme` answers under `acme/packages/api`. A non-root explicit `name:` in frontmatter is store-LOCAL \u2014 it never carries the namespace, and composition is unconditional, so a name that merely looks prefixed is prefixed again. The namespace itself is immutable through the memory commands: `crtr memory write <namespace> --dir <repository root>` declares it once in an empty repository-root store, `crtr sys migrate --dir <repository root>` converts a legacy one, and nothing else rewrites it. A trailing `INDEX` is never part of an address in any store: `area/INDEX.md` is the document attached to directory `area`, so it answers at `area` and reading `area` returns that body together with the directory\u2019s immediate listing. Resolution is exact \u2014 a file path, a bare leaf, a `<scope>/<name>` spelling, and a trailing `/INDEX` all fail \u2014 so run `crtr memory find <leaf>` when you do not know the canonical name.';