@north-light/crouter 0.3.220 → 0.3.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/dist/api/client.d.ts +29 -0
  2. package/dist/api/client.js +44 -0
  3. package/dist/api/dto/chat-inventory.d.ts +56 -0
  4. package/dist/api/dto/chat-inventory.js +11 -0
  5. package/dist/api/dto/human-requests.d.ts +88 -0
  6. package/dist/api/dto/human-requests.js +4 -0
  7. package/dist/api/dto/human.d.ts +3 -0
  8. package/dist/api/dto/profiles.d.ts +19 -5
  9. package/dist/api/dto/profiles.js +2 -1
  10. package/dist/api/dto/reviews.d.ts +2 -0
  11. package/dist/api/index.d.ts +2 -0
  12. package/dist/api/index.js +2 -0
  13. package/dist/api/routes.d.ts +8 -0
  14. package/dist/api/routes.js +11 -0
  15. package/dist/build-root.d.ts +2 -6
  16. package/dist/build-root.js +51 -4
  17. package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
  18. package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
  19. package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
  20. package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
  21. package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
  22. package/dist/builtin-memory/04-base-worker.md +4 -8
  23. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  24. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
  25. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
  26. package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
  27. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
  28. package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
  29. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
  30. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
  31. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
  32. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
  33. package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
  34. package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
  35. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
  36. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
  37. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
  38. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
  39. package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
  40. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
  41. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
  42. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
  43. package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
  44. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
  45. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
  46. package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
  47. package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
  48. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
  49. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
  50. package/dist/builtin-memory/design/guide.md +35 -0
  51. package/dist/builtin-memory/design/roadmap.md +21 -0
  52. package/dist/builtin-memory/insights/capture.md +1 -1
  53. package/dist/builtin-memory/internal/agent-shaping.md +3 -1
  54. package/dist/builtin-memory/internal/memory-loading.md +4 -0
  55. package/dist/builtin-memory/internal/plugins.md +10 -1
  56. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  57. package/dist/builtin-memory/plan/roadmap.md +6 -22
  58. package/dist/builtin-memory/spec/guide.md +23 -6
  59. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
  60. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
  61. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +34 -16
  62. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
  63. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
  64. package/dist/clients/attach/overlays/file-review.js +2 -2
  65. package/dist/clients/attach/render/markdown-source.js +106 -1
  66. package/dist/clients/attach/session/file-links.d.ts +13 -4
  67. package/dist/clients/attach/session/file-links.js +54 -58
  68. package/dist/clients/attach/session/keys.d.ts +1 -1
  69. package/dist/clients/attach/session/profile-files.js +1 -1
  70. package/dist/clients/attach/viewer.js +698 -696
  71. package/dist/clients/inbox/controller.js +1 -1
  72. package/dist/clients/inbox/resolve.d.ts +1 -0
  73. package/dist/clients/inbox/review/launch.d.ts +8 -4
  74. package/dist/clients/inbox/review/launch.js +55 -5
  75. package/dist/clients/inbox/review/review-client.d.ts +1 -0
  76. package/dist/clients/inbox/review/review-client.js +7 -1
  77. package/dist/clients/inbox/review-adapter.d.ts +1 -8
  78. package/dist/clients/inbox/review-adapter.js +4 -52
  79. package/dist/commands/__tests__/human.test.js +2 -2
  80. package/dist/commands/human/request.d.ts +2 -0
  81. package/dist/commands/human/request.js +281 -0
  82. package/dist/commands/human.js +5 -2
  83. package/dist/commands/memory/lint.js +2 -1
  84. package/dist/commands/memory/read.js +1 -0
  85. package/dist/commands/memory.js +1 -1
  86. package/dist/commands/pkg/market-manage.js +165 -75
  87. package/dist/commands/pkg/plugin-inspect.js +19 -2
  88. package/dist/commands/pkg/plugin-manage.d.ts +8 -3
  89. package/dist/commands/pkg/plugin-manage.js +72 -24
  90. package/dist/commands/profile/default.js +6 -10
  91. package/dist/commands/profile/list.js +5 -3
  92. package/dist/commands/profile/new.js +21 -8
  93. package/dist/commands/profile/project.js +25 -19
  94. package/dist/commands/profile/show.js +3 -3
  95. package/dist/commands/surface-inbox.js +1 -0
  96. package/dist/commands/sys/__tests__/migrate.test.js +16 -5
  97. package/dist/commands/sys/config.js +2 -2
  98. package/dist/commands/sys/doctor.js +87 -5
  99. package/dist/commands/sys/migrate.js +38 -19
  100. package/dist/commands/sys/setup-core.js +1 -1
  101. package/dist/commands/sys/sync-project-guidance.js +1 -1
  102. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
  103. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
  104. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
  105. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
  106. package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
  107. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
  108. package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
  109. package/dist/core/__tests__/human-action-delivery.test.js +140 -0
  110. package/dist/core/__tests__/human-actions.test.d.ts +1 -0
  111. package/dist/core/__tests__/human-actions.test.js +116 -0
  112. package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
  113. package/dist/core/__tests__/profile-project-memory-delivery.test.d.ts +1 -0
  114. package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
  115. package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
  116. package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
  117. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
  118. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
  119. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
  120. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
  121. package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
  122. package/dist/core/bootstrap.js +6 -0
  123. package/dist/core/canvas/browse/app.js +5 -2
  124. package/dist/core/canvas/browse/model.d.ts +25 -15
  125. package/dist/core/canvas/browse/model.js +86 -65
  126. package/dist/core/canvas/db.js +23 -0
  127. package/dist/core/canvas/human-deliveries.d.ts +53 -0
  128. package/dist/core/canvas/human-deliveries.js +75 -0
  129. package/dist/core/canvas/render-source.d.ts +6 -0
  130. package/dist/core/canvas/render-source.js +7 -1
  131. package/dist/core/canvas/render.js +10 -2
  132. package/dist/core/command-hooks/artifact.d.ts +10 -0
  133. package/dist/core/command-hooks/artifact.js +129 -0
  134. package/dist/core/command-hooks/catalog.d.ts +14 -0
  135. package/dist/core/command-hooks/catalog.js +38 -0
  136. package/dist/core/command-hooks/compose.d.ts +15 -0
  137. package/dist/core/command-hooks/compose.js +99 -0
  138. package/dist/core/command-hooks/discovery.d.ts +87 -0
  139. package/dist/core/command-hooks/discovery.js +174 -0
  140. package/dist/core/command-hooks/help.d.ts +5 -0
  141. package/dist/core/command-hooks/help.js +18 -0
  142. package/dist/core/command-hooks/index.d.ts +6 -0
  143. package/dist/core/command-hooks/index.js +6 -0
  144. package/dist/core/command-hooks/report.d.ts +23 -0
  145. package/dist/core/command-hooks/report.js +19 -0
  146. package/dist/core/command-hooks/schema.d.ts +27 -0
  147. package/dist/core/command-hooks/schema.js +68 -0
  148. package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
  149. package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
  150. package/dist/core/command-plugins/presence.d.ts +2 -0
  151. package/dist/core/command-plugins/presence.js +17 -0
  152. package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
  153. package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
  154. package/dist/core/command.d.ts +8 -1
  155. package/dist/core/command.js +12 -10
  156. package/dist/core/config.d.ts +13 -1
  157. package/dist/core/config.js +51 -1
  158. package/dist/core/feed/inbox.d.ts +6 -0
  159. package/dist/core/feed/inbox.js +9 -1
  160. package/dist/core/help.d.ts +7 -1
  161. package/dist/core/human/action-binding.d.ts +21 -0
  162. package/dist/core/human/action-binding.js +40 -0
  163. package/dist/core/human/completion.d.ts +38 -0
  164. package/dist/core/human/completion.js +27 -0
  165. package/dist/core/human/convention.d.ts +2 -0
  166. package/dist/core/human/convention.js +2 -0
  167. package/dist/core/human/tickets.d.ts +25 -6
  168. package/dist/core/human/tickets.js +19 -13
  169. package/dist/core/human/types.d.ts +5 -0
  170. package/dist/core/human-actions.d.ts +25 -0
  171. package/dist/core/human-actions.js +101 -0
  172. package/dist/core/io.d.ts +9 -1
  173. package/dist/core/io.js +44 -2
  174. package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
  175. package/dist/core/memory/inline-ref-inventory.js +15 -8
  176. package/dist/core/memory-resolver.d.ts +13 -1
  177. package/dist/core/memory-resolver.js +26 -20
  178. package/dist/core/profiles/manifest.d.ts +13 -2
  179. package/dist/core/profiles/manifest.js +84 -18
  180. package/dist/core/profiles/select.d.ts +2 -0
  181. package/dist/core/profiles/select.js +29 -12
  182. package/dist/core/render.js +11 -0
  183. package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
  184. package/dist/core/runtime/advertised-command-invocation.js +233 -0
  185. package/dist/core/runtime/bearings.js +1 -1
  186. package/dist/core/runtime/broker/event-projection.js +7 -0
  187. package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
  188. package/dist/core/runtime/broker/frame-dispatch.js +13 -11
  189. package/dist/core/runtime/broker/read-ops.d.ts +4 -0
  190. package/dist/core/runtime/broker/read-ops.js +6 -2
  191. package/dist/core/runtime/broker-extension-render.js +1 -1
  192. package/dist/core/runtime/broker-inventory.d.ts +4 -0
  193. package/dist/core/runtime/broker-inventory.js +116 -0
  194. package/dist/core/runtime/broker-persona-guidance.js +1 -1
  195. package/dist/core/runtime/broker-protocol.d.ts +9 -2
  196. package/dist/core/runtime/broker.js +10 -1
  197. package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
  198. package/dist/core/runtime/chat-inventory-rows.js +105 -0
  199. package/dist/core/runtime/command-surface.d.ts +38 -0
  200. package/dist/core/runtime/command-surface.js +117 -0
  201. package/dist/core/runtime/launch-target.d.ts +25 -0
  202. package/dist/core/runtime/launch-target.js +54 -0
  203. package/dist/core/runtime/node-read.js +5 -0
  204. package/dist/core/runtime/persona.js +3 -3
  205. package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
  206. package/dist/core/runtime/prospective-inventory-cli.js +61 -0
  207. package/dist/core/runtime/prospective-inventory.d.ts +10 -0
  208. package/dist/core/runtime/prospective-inventory.js +88 -0
  209. package/dist/core/runtime/spawn.d.ts +3 -1
  210. package/dist/core/runtime/spawn.js +5 -3
  211. package/dist/core/scope.d.ts +26 -1
  212. package/dist/core/scope.js +52 -12
  213. package/dist/core/substrate/on-read.d.ts +7 -1
  214. package/dist/core/substrate/on-read.js +30 -32
  215. package/dist/core/substrate/render-node.d.ts +3 -2
  216. package/dist/core/substrate/render-node.js +3 -2
  217. package/dist/core/substrate/render.js +65 -24
  218. package/dist/core/substrate/schema.d.ts +16 -2
  219. package/dist/core/substrate/schema.js +14 -5
  220. package/dist/core/user-settings.d.ts +4 -0
  221. package/dist/core/user-settings.js +1 -0
  222. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +56 -7
  223. package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
  224. package/dist/daemon/api/handlers/chat-inventory.js +25 -0
  225. package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
  226. package/dist/daemon/api/handlers/human-requests.js +409 -0
  227. package/dist/daemon/api/handlers/human.js +3 -0
  228. package/dist/daemon/api/handlers/inbox.js +3 -0
  229. package/dist/daemon/api/handlers/nodes.d.ts +1 -3
  230. package/dist/daemon/api/handlers/nodes.js +11 -46
  231. package/dist/daemon/api/handlers/profiles.js +7 -1
  232. package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
  233. package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
  234. package/dist/daemon/api/handlers/reviews.js +10 -2
  235. package/dist/daemon/api/map.d.ts +2 -1
  236. package/dist/daemon/api/map.js +3 -2
  237. package/dist/daemon/api/server.js +6 -0
  238. package/dist/daemon/crtrd.js +6 -0
  239. package/dist/daemon/human/deliver-action.d.ts +16 -0
  240. package/dist/daemon/human/deliver-action.js +168 -0
  241. package/dist/daemon/human/finish.d.ts +8 -5
  242. package/dist/daemon/human/finish.js +45 -6
  243. package/dist/daemon/human/sweep.js +4 -1
  244. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
  245. package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
  246. package/dist/daemon/review/finish.d.ts +8 -3
  247. package/dist/daemon/review/finish.js +19 -1
  248. package/dist/hook-authoring.d.ts +75 -0
  249. package/dist/hook-authoring.js +358 -0
  250. package/dist/hook-process.d.ts +7 -0
  251. package/dist/hook-process.js +34 -0
  252. package/dist/index.d.ts +2 -0
  253. package/dist/index.js +2 -0
  254. package/dist/migrations/002-profile-project-memory.d.ts +2 -0
  255. package/dist/migrations/002-profile-project-memory.js +71 -0
  256. package/dist/migrations/profile-manifests.d.ts +30 -0
  257. package/dist/migrations/profile-manifests.js +70 -0
  258. package/dist/migrations/registry.js +10 -5
  259. package/dist/migrations/types.d.ts +28 -1
  260. package/dist/migrations/types.js +15 -9
  261. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
  262. package/dist/pi-extensions/canvas-structured-output.js +85 -2
  263. package/dist/types.d.ts +23 -6
  264. package/dist/types.js +1 -0
  265. package/package.json +1 -1
  266. package/runtime.lock.json +2 -2
  267. package/dist/builtin-memory/00-runtime-base.md +0 -55
  268. package/dist/builtin-memory/design.md +0 -55
  269. package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
  270. /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
  271. /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/fixtures/memory-slash-live-probe.d.ts} +0 -0
@@ -30,8 +30,9 @@
30
30
  // `/reload` — this module itself holds no cache beyond the session parse
31
31
  // cache it reads through.
32
32
  import { listAllMemoryDocs } from '../memory-resolver.js';
33
+ import { memoryExtensionEffectiveCatalog, projectEffectiveMemoryExtensions } from './extensions.js';
33
34
  import { parseSubstrateDoc } from '../substrate/schema.js';
34
- import { cachedSubstrateDocsInclusive } from '../substrate/session-cache.js';
35
+ import { cachedAllMemoryDocsInclusive, cachedSubstrateDocsInclusive } from '../substrate/session-cache.js';
35
36
  // Display-sort weight matching resolution precedence (node > project >
36
37
  // profile > user > builtin) — display-only; resolution itself is
37
38
  // name-only (`names.has(candidate)`), never scope-aware.
@@ -49,21 +50,27 @@ const SCOPE_SORT_RANK = {
49
50
  * (`refs`, stably sorted by winning scope then name) plus the resolution set
50
51
  * (`names` — doc names plus every proper directory prefix, because a bare-dir
51
52
  * ref is a legal listing link) that both the broker's per-submission resolver
52
- * and `buildGuidance` test tokens against.
53
+ * and `buildGuidance` test tokens against. `refs` additionally labels Gateway
54
+ * eligibility; the terminal viewer still receives every referenceable row.
53
55
  */
54
56
  export function buildRefInventory() {
55
- const docs = cachedSubstrateDocsInclusive(listAllMemoryDocs, parseSubstrateDoc);
57
+ const memoryDocs = cachedAllMemoryDocsInclusive(listAllMemoryDocs);
58
+ const substrateDocs = cachedSubstrateDocsInclusive(listAllMemoryDocs, parseSubstrateDoc);
59
+ const substrateDocsByPath = new Map(substrateDocs.map((doc) => [doc.path, doc]));
56
60
  // First-wins by canonical name: docs arrive already in precedence order
57
61
  // (listAllMemoryDocs' nearest-first source ordering), so the first doc seen
58
62
  // for a name is the winner.
59
63
  const winners = new Map();
60
- for (const doc of docs) {
61
- if (!winners.has(doc.name))
62
- winners.set(doc.name, doc);
64
+ for (const memoryDoc of memoryDocs) {
65
+ const doc = substrateDocsByPath.get(memoryDoc.path);
66
+ if (doc !== undefined && !winners.has(doc.name))
67
+ winners.set(doc.name, { doc, memoryDoc });
63
68
  }
64
69
  const refs = [];
65
- for (const [name, doc] of winners) {
66
- refs.push({ name, kind: doc.kind, scope: doc.scope, shortForm: doc.shortForm });
70
+ for (const [name, { doc, memoryDoc }] of winners) {
71
+ const gatewayVisible = doc.scope !== 'builtin'
72
+ && projectEffectiveMemoryExtensions(memoryDoc.frontmatter?.['extensions'], memoryExtensionEffectiveCatalog(memoryDoc))['northlight']?.['visibility'] !== false;
73
+ refs.push({ name, kind: doc.kind, scope: doc.scope, shortForm: doc.shortForm, gatewayVisible });
67
74
  }
68
75
  refs.sort((a, b) => {
69
76
  const scopeDelta = SCOPE_SORT_RANK[a.scope] - SCOPE_SORT_RANK[b.scope];
@@ -1,4 +1,5 @@
1
1
  import { type InstalledPlugin, type Scope } from '../types.js';
2
+ import type { ProfileProjectMemory } from '../api/dto/profiles.js';
2
3
  /**
3
4
  * Thin memory-document resolver for the document substrate. Resolution has
4
5
  * two passes. First, scopes/sources are tried NEAREST-FIRST (node > project
@@ -17,6 +18,11 @@ import { type InstalledPlugin, type Scope } from '../types.js';
17
18
  * a selected profile's `projects` manifest entries — see `findProjectScopeRoots`
18
19
  * in scope.ts), profile is the selected profile's own singleton `memory/` store
19
20
  * (from `CRTR_PROFILE_ID`), and user/builtin remain singleton scopes.
21
+ *
22
+ * Every doc also carries its project store's profile relationship ceiling
23
+ * (`projectMemory`) for the automatic delivery paths to clamp against.
24
+ * Resolution itself never consults it: the full corpus resolves and lists
25
+ * regardless of the ceiling.
20
26
  */
21
27
  export interface MemoryDoc {
22
28
  /** Resolver identity: the doc's explicit frontmatter `name` when present,
@@ -41,6 +47,12 @@ export interface MemoryDoc {
41
47
  * managed by `crtr pkg`, not writable/deletable as scope docs — consumers
42
48
  * that mutate the store gate on this. */
43
49
  plugin?: string;
50
+ /** The selected profile relationship's ceiling for the project store this doc
51
+ * came from: the maximum rung AUTOMATIC boot and workspace-open delivery may
52
+ * disclose it at. Always present; `content` (neutral, i.e. uncapped) for
53
+ * every non-project scope and for a project root no profile entry
54
+ * contributes. Resolution, listing, and targeted reads ignore it entirely. */
55
+ projectMemory: ProfileProjectMemory;
44
56
  }
45
57
  /** The memory-only scope union: the global `Scope` (`user|project|builtin`)
46
58
  * plus `profile` and `node`. Confined to the memory resolver/list/read/write/
@@ -107,7 +119,7 @@ export declare function listMemoryDocs(scope: MemoryScope, quiet?: boolean): Mem
107
119
  * namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
108
120
  * doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
109
121
  * slash-separated) then prefixing the plugin name. Builtin has no plugins. */
110
- export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope: MemoryScope, quiet?: boolean): MemoryDoc[];
122
+ export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope: MemoryScope, quiet?: boolean, projectMemory?: ProfileProjectMemory): MemoryDoc[];
111
123
  /** All project-scoped docs visible from an explicit node workspace/profile — the
112
124
  * project-only slice of a `MemoryTarget` view, used where only workspace docs
113
125
  * are wanted (a workspace-open render). For a full-precedence target-addressed
@@ -6,7 +6,7 @@ import { parseFrontmatterGeneric } from './frontmatter.js';
6
6
  import { listInstalledPlugins, listInstalledPluginsInRoot, parseSkillQualifier } from './resolver.js';
7
7
  import { ambiguous, notFound, usage } from './errors.js';
8
8
  import { warn } from './output.js';
9
- import { pluginMemoryDir, projectScopeRoots, scopeMemoryDir } from './scope.js';
9
+ import { NEUTRAL_PROJECT_MEMORY, pluginMemoryDir, projectScopeRoots, projectScopeRootsWithMemory, scopeMemoryDir } from './scope.js';
10
10
  import { effectiveDocKind, normalizeDocName, normalizeNameSegment, resolveDocName } from './substrate/schema.js';
11
11
  import { loadProfileManifest, profileMemoryDir } from './profiles/manifest.js';
12
12
  import { memoryDir as nodeMemoryDir } from './runtime/memory.js';
@@ -74,22 +74,26 @@ function memorySourcesInPrecedence(target, scope, includeDescendants = false) {
74
74
  const out = [];
75
75
  for (const s of scopesInPrecedence(target, scope)) {
76
76
  if (s === 'project') {
77
- const roots = projectScopeRoots(target.cwd, target.profileId);
78
- for (const root of roots) {
77
+ const annotated = projectScopeRootsWithMemory(target.cwd, target.profileId);
78
+ for (const { root, memory } of annotated) {
79
79
  out.push({
80
80
  scope: 'project',
81
81
  memoryDir: join(root, 'memory'),
82
82
  plugins: listInstalledPluginsInRoot('project', root).filter((p) => p.enabled),
83
+ projectMemory: memory,
83
84
  });
84
85
  }
85
86
  // Descendant stores rank after every ancestor source (and before
86
87
  // profile): an ancestor doc always beats a nested doc on a name tie.
88
+ // They carry the neutral ceiling — no profile entry names them, and
89
+ // descendant discovery is off for every automatic delivery path.
87
90
  if (includeDescendants) {
88
- for (const root of descendantStoreRoots(roots)) {
91
+ for (const root of descendantStoreRoots(annotated.map(({ root: r }) => r))) {
89
92
  out.push({
90
93
  scope: 'project',
91
94
  memoryDir: join(root, 'memory'),
92
95
  plugins: listInstalledPluginsInRoot('project', root).filter((p) => p.enabled),
96
+ projectMemory: NEUTRAL_PROJECT_MEMORY,
93
97
  });
94
98
  }
95
99
  }
@@ -99,7 +103,7 @@ function memorySourcesInPrecedence(target, scope, includeDescendants = false) {
99
103
  if (profileId !== '') {
100
104
  try {
101
105
  const { profileId: resolvedId } = loadProfileManifest(profileId);
102
- out.push({ scope: 'profile', memoryDir: profileMemoryDir(resolvedId), plugins: [] });
106
+ out.push({ scope: 'profile', memoryDir: profileMemoryDir(resolvedId), plugins: [], projectMemory: NEUTRAL_PROJECT_MEMORY });
103
107
  }
104
108
  catch {
105
109
  // Missing/deleted/invalid selected profile: no profile memory source.
@@ -111,27 +115,28 @@ function memorySourcesInPrecedence(target, scope, includeDescendants = false) {
111
115
  // Silently absent when the target names no node.
112
116
  const nodeId = target.nodeId ?? '';
113
117
  if (nodeId !== '')
114
- out.push({ scope: 'node', memoryDir: nodeMemoryDir(nodeId), plugins: [] });
118
+ out.push({ scope: 'node', memoryDir: nodeMemoryDir(nodeId), plugins: [], projectMemory: NEUTRAL_PROJECT_MEMORY });
115
119
  }
116
120
  else {
117
121
  out.push({
118
122
  scope: s,
119
123
  memoryDir: scopeMemoryDir(s),
120
124
  plugins: listInstalledPlugins(s).filter((p) => p.enabled),
125
+ projectMemory: NEUTRAL_PROJECT_MEMORY,
121
126
  });
122
127
  }
123
128
  }
124
129
  return out;
125
130
  }
126
- function loadMemoryDoc(scope, root, path, fallbackName) {
131
+ function loadMemoryDoc(scope, root, path, fallbackName, projectMemory = NEUTRAL_PROJECT_MEMORY) {
127
132
  const { data, body } = parseFrontmatterGeneric(readText(path));
128
- return { name: resolveDocName(data, fallbackName), scope, path, root, frontmatter: data, body };
133
+ return { name: resolveDocName(data, fallbackName), scope, path, root, frontmatter: data, body, projectMemory };
129
134
  }
130
135
  /** All memory docs in one memory/ dir, scanned recursively for *.md (topical
131
136
  * subdirs supported), sorted by resolver identity (explicit frontmatter name
132
137
  * when present, else path-derived). SKILL.md bundles are legacy Agent Skills
133
138
  * and are ignored; crouter memory docs are plain .md files under memory/. */
134
- function listMemoryDocsInDir(scope, dir, quiet = false) {
139
+ function listMemoryDocsInDir(scope, dir, quiet = false, projectMemory = NEUTRAL_PROJECT_MEMORY) {
135
140
  if (!dir || !pathExists(dir))
136
141
  return [];
137
142
  const docs = [];
@@ -171,7 +176,7 @@ function listMemoryDocsInDir(scope, dir, quiet = false) {
171
176
  // `quiet` suppresses the notice for a targeted resolve (a leaf-name read),
172
177
  // where another doc's health is irrelevant noise before the result.
173
178
  try {
174
- docs.push(loadMemoryDoc(scope, dir, path, name));
179
+ docs.push(loadMemoryDoc(scope, dir, path, name, projectMemory));
175
180
  }
176
181
  catch (e) {
177
182
  const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
@@ -214,7 +219,7 @@ export function listMemoryDocs(scope, quiet = false) {
214
219
  * namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
215
220
  * doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
216
221
  * slash-separated) then prefixing the plugin name. Builtin has no plugins. */
217
- export function listPluginMemoryDocs(plugin, scope, quiet = false) {
222
+ export function listPluginMemoryDocs(plugin, scope, quiet = false, projectMemory = NEUTRAL_PROJECT_MEMORY) {
218
223
  const dir = pluginMemoryDir(plugin);
219
224
  if (!pathExists(dir))
220
225
  return [];
@@ -225,7 +230,7 @@ export function listPluginMemoryDocs(plugin, scope, quiet = false) {
225
230
  continue;
226
231
  const name = `${plugin.name}/${derived}`;
227
232
  try {
228
- docs.push({ ...loadMemoryDoc(scope, dir, file, name), plugin: plugin.name });
233
+ docs.push({ ...loadMemoryDoc(scope, dir, file, name, projectMemory), plugin: plugin.name });
229
234
  }
230
235
  catch (e) {
231
236
  const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
@@ -241,8 +246,8 @@ export function listPluginMemoryDocs(plugin, scope, quiet = false) {
241
246
  * (consults one source's docs at a time, nearest source first). */
242
247
  function sourceMemoryDocs(source, quiet = false) {
243
248
  return [
244
- ...listMemoryDocsInDir(source.scope, source.memoryDir, quiet),
245
- ...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope, quiet)),
249
+ ...listMemoryDocsInDir(source.scope, source.memoryDir, quiet, source.projectMemory),
250
+ ...source.plugins.flatMap((p) => listPluginMemoryDocs(p, source.scope, quiet, source.projectMemory)),
246
251
  ];
247
252
  }
248
253
  /** All project-scoped docs visible from an explicit node workspace/profile — the
@@ -250,10 +255,11 @@ function sourceMemoryDocs(source, quiet = false) {
250
255
  * are wanted (a workspace-open render). For a full-precedence target-addressed
251
256
  * lookup, use `resolveMemoryDocForTarget`. */
252
257
  export function listProjectMemoryDocs(startDir = process.cwd(), profileId = selectedProfileId() || null, quiet = false) {
253
- return projectScopeRoots(startDir, profileId).flatMap((root) => sourceMemoryDocs({
258
+ return projectScopeRootsWithMemory(startDir, profileId).flatMap(({ root, memory }) => sourceMemoryDocs({
254
259
  scope: 'project',
255
260
  memoryDir: join(root, 'memory'),
256
261
  plugins: listInstalledPluginsInRoot('project', root).filter((plugin) => plugin.enabled),
262
+ projectMemory: memory,
257
263
  }, quiet));
258
264
  }
259
265
  /** All memory docs across the resolved sources, in precedence order: each
@@ -307,7 +313,7 @@ function matchNormalizedChild(dir, segment, want, ext = '.md') {
307
313
  * directory by normalized name, then resolve the final segment as either a
308
314
  * `.md` file or a directory. Either half is null when
309
315
  * the segment path does not resolve that way. This is what makes
310
- * `00-runtime-base.md` findable as `runtime-base` and `01-spine/00-has-manager`
316
+ * `00-runtime-base/00-authoring.md` findable as `runtime-base/authoring` and `01-spine/00-has-manager`
311
317
  * findable as `spine/has-manager` — the physical path keeps its pins, only
312
318
  * lookup is prefix-blind. */
313
319
  function resolveNormalizedPath(baseDir, segments, ext = '.md') {
@@ -351,11 +357,11 @@ function findMemoryMatchInSource(name, segments, source, legacyDirectoryIndex =
351
357
  if (dir) {
352
358
  const { filePath, dirPath } = resolveNormalizedPath(dir, segments);
353
359
  if (!isLegacySkillDoc && filePath !== null)
354
- return loadMemoryDoc(source.scope, dir, filePath, name);
360
+ return loadMemoryDoc(source.scope, dir, filePath, name, source.projectMemory);
355
361
  if (legacyDirectoryIndex && dirPath !== null) {
356
362
  const indexPath = join(dirPath, 'INDEX.md');
357
363
  if (pathExists(indexPath))
358
- return loadMemoryDoc(source.scope, dir, indexPath, name);
364
+ return loadMemoryDoc(source.scope, dir, indexPath, name, source.projectMemory);
359
365
  }
360
366
  }
361
367
  // Plugin memory dir: a `<plugin>/<rest>` name resolves against that enabled
@@ -374,14 +380,14 @@ function findMemoryMatchInSource(name, segments, source, legacyDirectoryIndex =
374
380
  if (restSegments.length > 0) {
375
381
  const { filePath } = resolveNormalizedPath(pdir, restSegments);
376
382
  if (restSegments.at(-1) !== 'SKILL' && filePath !== null)
377
- return loadMemoryDoc(source.scope, pdir, filePath, name);
383
+ return loadMemoryDoc(source.scope, pdir, filePath, name, source.projectMemory);
378
384
  }
379
385
  if (legacyDirectoryIndex) {
380
386
  const indexDir = rest === '' ? pdir : resolveNormalizedPath(pdir, restSegments).dirPath;
381
387
  if (indexDir !== null) {
382
388
  const indexPath = join(indexDir, 'INDEX.md');
383
389
  if (pathExists(indexPath))
384
- return loadMemoryDoc(source.scope, pdir, indexPath, name);
390
+ return loadMemoryDoc(source.scope, pdir, indexPath, name, source.projectMemory);
385
391
  }
386
392
  }
387
393
  }
@@ -1,9 +1,16 @@
1
1
  import type { ProfileManifest } from '../../types.js';
2
+ import type { ProfileProject, ProfileProjectMemory } from '../../api/dto/profiles.js';
2
3
  export declare const ROOT_PROFILE_ID = "root-00000000";
3
4
  export declare function profilesRoot(): string;
4
5
  export declare function profileRoot(profileId: string): string;
5
6
  export declare function profileManifestPath(profileId: string): string;
6
7
  export declare function profileMemoryDir(profileId: string): string;
8
+ /** Reject anything a valid project array may not carry. Shape only: takes
9
+ * `unknown` because an API body and a hand-edited manifest both prove nothing
10
+ * at runtime, and deliberately says nothing about whether a path exists —
11
+ * existence and realpath resolution belong to the create/add paths alone, so
12
+ * a request naming a since-deleted directory still validates. */
13
+ export declare function assertProfileProjects(entries: unknown): asserts entries is ProfileProject[];
7
14
  /** The directory a node running under `profileId` is pinned to — the profile's
8
15
  * home, but only while it still exists on disk (a node pinned to a deleted dir
9
16
  * cannot boot). Null for the root profile, a profile with no projects, a home
@@ -52,7 +59,7 @@ export declare function assertProfileMetadata(entries: unknown): asserts entries
52
59
  * Never throws; on an env-name collision the first entry wins (matching
53
60
  * injection, where a deterministic winner beats insertion-order luck). */
54
61
  export declare function sanitizeProfileMetadata(stored: unknown): Record<string, string>;
55
- export declare function createProfile(name: string, projects?: string[], opts?: {
62
+ export declare function createProfile(name: string, projects?: ProfileProject[], opts?: {
56
63
  defaultKind?: string;
57
64
  metadata?: Record<string, string>;
58
65
  }): ProfileEntry;
@@ -80,7 +87,11 @@ export declare function updateProfileMetadata(profileId: string, set: Record<str
80
87
  * over a bad profile id (mirrors `readProfileEnvVars`). */
81
88
  export declare function readProfileMetadata(profileId: string | null): Record<string, string>;
82
89
  export declare function renameProfile(profileId: string, name: string): ProfileEntry;
83
- export declare function addProfileProject(profileId: string, dir: string, opts?: {
90
+ /** Upsert: a new path appends, an existing one keeps its position and takes
91
+ * the supplied memory value. Every caller names a memory value — a profile
92
+ * relationship never acquires one by default. */
93
+ export declare function addProfileProject(profileId: string, dir: string, opts: {
94
+ memory: ProfileProjectMemory;
84
95
  home?: boolean;
85
96
  }): ProfileEntry;
86
97
  export declare function removeProfileProject(profileId: string, dir: string): ProfileEntry;
@@ -9,6 +9,7 @@ import { randomUUID } from 'node:crypto';
9
9
  import { existsSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, } from 'node:fs';
10
10
  import { dirname, join, relative, resolve as resolvePath, sep } from 'node:path';
11
11
  import { SCHEMA_VERSION, PROFILE_DIR } from '../../types.js';
12
+ import { PROFILE_PROJECT_MEMORY_VALUES } from '../../api/dto/profiles.js';
12
13
  import { userScopeRoot, resetScopeCache } from '../scope.js';
13
14
  import { ensureDir, nowIso } from '../fs-utils.js';
14
15
  import { usage, notFound, ambiguous, general } from '../errors.js';
@@ -84,15 +85,68 @@ export function profileMemoryDir(profileId) {
84
85
  * the user started them, not in whatever dir was last added to its purview. */
85
86
  function normalizeHome(profileId, manifest) {
86
87
  const stored = typeof manifest.home === 'string' && manifest.home !== '' ? manifest.home : null;
87
- const home = profileId === ROOT_PROFILE_ID ? stored : stored ?? manifest.projects[0] ?? null;
88
+ const home = profileId === ROOT_PROFILE_ID ? stored : stored ?? manifest.projects[0]?.path ?? null;
88
89
  const pausedAt = typeof manifest.paused_at === 'string' && manifest.paused_at !== '' ? manifest.paused_at : null;
89
90
  return { ...manifest, home, paused_at: pausedAt };
90
91
  }
92
+ /** Reject anything a valid project array may not carry. Shape only: takes
93
+ * `unknown` because an API body and a hand-edited manifest both prove nothing
94
+ * at runtime, and deliberately says nothing about whether a path exists —
95
+ * existence and realpath resolution belong to the create/add paths alone, so
96
+ * a request naming a since-deleted directory still validates. */
97
+ export function assertProfileProjects(entries) {
98
+ const memories = PROFILE_PROJECT_MEMORY_VALUES.join(', ');
99
+ if (!Array.isArray(entries)) {
100
+ throw usage('projects must be an array of {path, memory} objects', {
101
+ received: typeof entries,
102
+ field: 'projects',
103
+ next: `Pass entries like {"path": "/abs/dir", "memory": "content"} — memory is one of: ${memories}.`,
104
+ });
105
+ }
106
+ for (const entry of entries) {
107
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
108
+ throw usage(`invalid project entry: ${JSON.stringify(entry)}`, {
109
+ field: 'projects',
110
+ next: 'Every project is an object like {"path": "/abs/dir", "memory": "content"}.',
111
+ });
112
+ }
113
+ const { path, memory } = entry;
114
+ if (typeof path !== 'string' || path.trim() === '') {
115
+ throw usage(`project entry has no path: ${JSON.stringify(entry)}`, {
116
+ field: 'projects',
117
+ next: 'Give every project entry a non-empty directory path.',
118
+ });
119
+ }
120
+ if (typeof memory !== 'string' || !PROFILE_PROJECT_MEMORY_VALUES.includes(memory)) {
121
+ throw usage(`invalid project memory value: ${JSON.stringify(memory)}`, {
122
+ received: String(memory),
123
+ field: 'projects',
124
+ next: `Set memory on ${path} to one of: ${memories}.`,
125
+ });
126
+ }
127
+ }
128
+ }
129
+ function hasCurrentProjectShape(projects) {
130
+ try {
131
+ assertProfileProjects(projects);
132
+ return true;
133
+ }
134
+ catch {
135
+ return false;
136
+ }
137
+ }
138
+ /** A manifest whose projects are not the current object shape is unreadable —
139
+ * the same treatment as corrupt JSON, so nothing downstream ever guesses a
140
+ * memory value. `crtr sys migrate` reads raw bytes and is the only converter
141
+ * of pre-object manifests. */
91
142
  function readManifestFile(path, profileId) {
92
143
  if (!existsSync(path))
93
144
  return null;
94
145
  try {
95
- return normalizeHome(profileId, JSON.parse(readFileSync(path, 'utf8')));
146
+ const parsed = JSON.parse(readFileSync(path, 'utf8'));
147
+ if (!hasCurrentProjectShape(parsed.projects))
148
+ return null;
149
+ return normalizeHome(profileId, parsed);
96
150
  }
97
151
  catch {
98
152
  return null;
@@ -231,14 +285,15 @@ function resolveExistingProjectDir(dir) {
231
285
  return abs;
232
286
  }
233
287
  }
234
- function dedupeOrdered(values) {
288
+ /** First-wins by resolved path, manifest order preserved. */
289
+ function dedupeProjects(entries) {
235
290
  const seen = new Set();
236
291
  const out = [];
237
- for (const v of values) {
238
- if (seen.has(v))
292
+ for (const entry of entries) {
293
+ if (seen.has(entry.path))
239
294
  continue;
240
- seen.add(v);
241
- out.push(v);
295
+ seen.add(entry.path);
296
+ out.push(entry);
242
297
  }
243
298
  return out;
244
299
  }
@@ -374,14 +429,15 @@ export function createProfile(name, projects = [], opts = {}) {
374
429
  throw usage('profile name must not be empty');
375
430
  if (opts.metadata !== undefined)
376
431
  assertProfileMetadata(opts.metadata);
377
- const resolvedProjects = dedupeOrdered(projects.map(resolveExistingProjectDir));
432
+ assertProfileProjects(projects);
433
+ const resolvedProjects = dedupeProjects(projects.map((p) => ({ path: resolveExistingProjectDir(p.path), memory: p.memory })));
378
434
  const profileId = generateProfileId(trimmed);
379
435
  return withProfileManifestLock(profileId, () => {
380
436
  const manifest = {
381
437
  schema_version: SCHEMA_VERSION,
382
438
  name: trimmed,
383
439
  projects: resolvedProjects,
384
- home: resolvedProjects[0] ?? null,
440
+ home: resolvedProjects[0]?.path ?? null,
385
441
  paused_at: null,
386
442
  ...(opts.defaultKind !== undefined ? { default_kind: opts.defaultKind } : {}),
387
443
  ...(opts.metadata !== undefined && Object.keys(opts.metadata).length > 0 ? { metadata: { ...opts.metadata } } : {}),
@@ -510,13 +566,23 @@ export function renameProfile(profileId, name) {
510
566
  resetScopeCache();
511
567
  return result;
512
568
  }
513
- export function addProfileProject(profileId, dir, opts = {}) {
569
+ /** Upsert: a new path appends, an existing one keeps its position and takes
570
+ * the supplied memory value. Every caller names a memory value — a profile
571
+ * relationship never acquires one by default. */
572
+ export function addProfileProject(profileId, dir, opts) {
573
+ assertProfileProjects([{ path: dir, memory: opts.memory }]);
514
574
  const resolved = resolveExistingProjectDir(dir);
515
- const result = mutateManifest(profileId, (m) => ({
516
- ...m,
517
- projects: dedupeOrdered([...m.projects, resolved]),
518
- ...(opts.home === true ? { home: resolved } : {}),
519
- }));
575
+ const result = mutateManifest(profileId, (m) => {
576
+ const existing = m.projects.some((p) => p.path === resolved);
577
+ const projects = existing
578
+ ? m.projects.map((p) => (p.path === resolved ? { ...p, memory: opts.memory } : p))
579
+ : dedupeProjects([...m.projects, { path: resolved, memory: opts.memory }]);
580
+ return {
581
+ ...m,
582
+ projects,
583
+ ...(opts.home === true ? { home: resolved } : {}),
584
+ };
585
+ });
520
586
  resetScopeCache();
521
587
  return result;
522
588
  }
@@ -534,15 +600,15 @@ export function removeProfileProject(profileId, dir) {
534
600
  /* dir no longer exists — fall back to comparing by its resolved path */
535
601
  }
536
602
  const result = mutateManifest(profileId, (m) => {
537
- const filtered = m.projects.filter((p) => p !== real && p !== abs);
603
+ const filtered = m.projects.filter((p) => p.path !== real && p.path !== abs);
538
604
  // Dropping the home dir from purview falls the home back to the first
539
605
  // remaining project, exactly as `normalizeHome` would for a manifest that
540
606
  // never named one — never leaves the profile pointing outside its purview.
541
- const home = m.home === real || m.home === abs ? filtered[0] ?? null : m.home;
607
+ const home = m.home === real || m.home === abs ? filtered[0]?.path ?? null : m.home;
542
608
  if (filtered.length === m.projects.length) {
543
609
  throw notFound(`project not found on profile: ${dir}`, {
544
610
  received: dir,
545
- next: `Current projects: ${m.projects.length > 0 ? m.projects.join(', ') : '(none)'}.`,
611
+ next: `Current projects: ${m.projects.length > 0 ? m.projects.map((p) => p.path).join(', ') : '(none)'}.`,
546
612
  });
547
613
  }
548
614
  return { ...m, projects: filtered, home };
@@ -9,6 +9,8 @@ export declare function resolveProfileSearchAction(bindings: BindingResolution<B
9
9
  * `crtr profile default set`, which rejects pinning a profile that doesn't
10
10
  * cover cwd (the selector would ignore/self-heal such a pin anyway). */
11
11
  export declare function profileCoversCwd(entry: ProfileEntry, cwd: string): boolean;
12
+ /** Resolve the headless selector against existing state without changing it. */
13
+ export declare function selectProfileForCwdReadOnly(cwd: string): string;
12
14
  /** Select the profile a node about to boot at `cwd` should run under.
13
15
  *
14
16
  * 1. `explicitProfile` present → resolve id/name via `loadProfileManifest`. If
@@ -79,7 +79,7 @@ function projectCovers(cwd, project) {
79
79
  * `crtr profile default set`, which rejects pinning a profile that doesn't
80
80
  * cover cwd (the selector would ignore/self-heal such a pin anyway). */
81
81
  export function profileCoversCwd(entry, cwd) {
82
- return entry.manifest.projects.some((p) => projectCovers(cwd, p));
82
+ return entry.manifest.projects.some((p) => projectCovers(cwd, p.path));
83
83
  }
84
84
  /** `a` more recent than `b`, treating null as the oldest possible value (never
85
85
  * wins over a real timestamp). ISO 8601 strings compare correctly lexically. */
@@ -93,6 +93,26 @@ function isMoreRecent(a, b) {
93
93
  function pickMostRecent(entries) {
94
94
  return entries.reduce((best, cur) => isMoreRecent(cur.manifest.last_used_at, best.manifest.last_used_at) ? cur : best);
95
95
  }
96
+ function profileSelectionState(cwd) {
97
+ const resolvedCwd = resolveCwd(cwd);
98
+ const covering = listProfiles().filter((profile) => profileCoversCwd(profile, resolvedCwd));
99
+ const exact = covering.filter((profile) => profile.manifest.projects.some((project) => project.path === resolvedCwd));
100
+ const pinnedId = getDefaultProfileId(resolvedCwd);
101
+ const pinned = pinnedId === null ? null : covering.find((profile) => profile.profileId === pinnedId) ?? null;
102
+ return { covering, exact, pinnedId, pinned };
103
+ }
104
+ /** Resolve the headless selector against existing state without changing it. */
105
+ export function selectProfileForCwdReadOnly(cwd) {
106
+ const { covering, exact, pinned } = profileSelectionState(cwd);
107
+ if (pinned !== null)
108
+ return pinned.profileId;
109
+ if (exact.length === 1)
110
+ return exact[0].profileId;
111
+ const pool = exact.length > 0 ? exact : covering;
112
+ if (pool.length > 0)
113
+ return pickMostRecent(pool).profileId;
114
+ return ROOT_PROFILE_ID;
115
+ }
96
116
  function isInteractive() {
97
117
  return Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
98
118
  }
@@ -161,7 +181,7 @@ function purviewSummary(entry) {
161
181
  const first = entry.manifest.projects[0];
162
182
  if (first === undefined)
163
183
  return relativeUsed(entry.manifest.last_used_at);
164
- return n > 1 ? `${tildify(first)} \u00b7 ${n} dirs total` : tildify(first);
184
+ return n > 1 ? `${tildify(first.path)} \u00b7 ${n} dirs total` : tildify(first.path);
165
185
  }
166
186
  /** ▸ marks the highlighted row. */
167
187
  const HILITE = accent('\u25b8');
@@ -330,7 +350,7 @@ async function offerAddDirToProfile(rl, entry, cwd) {
330
350
  .toLowerCase();
331
351
  if (choice === 'n' || choice === 'no')
332
352
  return;
333
- addProfileProject(entry.profileId, cwd);
353
+ addProfileProject(entry.profileId, cwd, { memory: 'content' });
334
354
  process.stdout.write(` ${accent('\u2713')} ${dim(`added this directory to "${entry.manifest.name}"`)}\n`);
335
355
  }
336
356
  /** Search-and-pick among EVERY existing profile (root excluded — it is the
@@ -355,7 +375,7 @@ async function promptSelectExistingProfile(bindings) {
355
375
  const matchesOf = () => filter === ''
356
376
  ? all
357
377
  : all.filter((e) => e.manifest.name.toLowerCase().includes(filter) ||
358
- e.manifest.projects.some((p) => tildify(p).toLowerCase().includes(filter)));
378
+ e.manifest.projects.some((p) => tildify(p.path).toLowerCase().includes(filter)));
359
379
  return new Promise((resolve) => {
360
380
  let prevLines = 0;
361
381
  const draw = () => {
@@ -447,7 +467,7 @@ async function createProfileHere(rl, cwd) {
447
467
  const defaultName = basename(cwd) || 'profile';
448
468
  const nameInput = (await rl.question(caret(`name the new profile [${defaultName}]`))).trim();
449
469
  const name = nameInput !== '' ? nameInput : defaultName;
450
- const profileId = createProfile(name, [cwd]).profileId;
470
+ const profileId = createProfile(name, [{ path: cwd, memory: 'content' }]).profileId;
451
471
  process.stdout.write(` ${accent('\u2713')} ${dim(`created profile "${name}"`)}\n`);
452
472
  return profileId;
453
473
  }
@@ -524,9 +544,9 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId, bindi
524
544
  const detail = exact
525
545
  ? `${relativeUsed(entry.manifest.last_used_at)}${n > 1 ? ` \u00b7 ${n} dirs total` : ''}`
526
546
  : (() => {
527
- const md = entry.manifest.projects.find((p) => projectCovers(cwd, p)) ??
547
+ const md = entry.manifest.projects.find((p) => projectCovers(cwd, p.path)) ??
528
548
  entry.manifest.projects[0];
529
- return n > 1 ? `matches ${tildify(md)} \u00b7 ${n} dirs total` : `matches ${tildify(md)}`;
549
+ return n > 1 ? `matches ${tildify(md.path)} \u00b7 ${n} dirs total` : `matches ${tildify(md.path)}`;
530
550
  })();
531
551
  const defaultText = pinned !== null && ordered[0] === pinned ? 'default (pinned)' : 'default';
532
552
  return {
@@ -573,7 +593,7 @@ async function promptPickProfileOrCreate(candidates, cwd, exact, pinnedId, bindi
573
593
  process.stdout.write(` ${accent('\u2713')} ${dim(`"${picked.manifest.name}" is now the default here`)}\n`);
574
594
  }
575
595
  else if (res.action === 'crtr.profile.menu.add-directory') {
576
- addProfileProject(picked.profileId, cwd);
596
+ addProfileProject(picked.profileId, cwd, { memory: 'content' });
577
597
  process.stdout.write(` ${accent('\u2713')} ${dim(`added this directory to "${picked.manifest.name}"`)}\n`);
578
598
  }
579
599
  return picked.profileId;
@@ -620,14 +640,11 @@ export async function selectProfileForCwd(cwd, explicitProfile, forcePicker = fa
620
640
  updateProfileLastUsed(entry.profileId);
621
641
  return entry.profileId;
622
642
  }
623
- const covering = listProfiles().filter((p) => profileCoversCwd(p, resolvedCwd));
624
- const exact = covering.filter((p) => p.manifest.projects.some((proj) => proj === resolvedCwd));
643
+ const { covering, exact, pinnedId, pinned } = profileSelectionState(resolvedCwd);
625
644
  // A per-cwd pinned default (set from the menu with `d`) is the user's
626
645
  // explicit "default to THIS profile HERE". It outranks both the exact-single
627
646
  // auto-pick and global-MRU, so long as it still covers cwd; a stale pin
628
647
  // (profile deleted, or its dir removed from purview) is self-healed away.
629
- const pinnedId = getDefaultProfileId(resolvedCwd);
630
- const pinned = pinnedId !== null ? covering.find((p) => p.profileId === pinnedId) ?? null : null;
631
648
  if (pinnedId !== null && pinned === null)
632
649
  clearDefaultProfile(resolvedCwd);
633
650
  // A valid pin decides it outright and STOPS ASKING — the whole point of
@@ -32,6 +32,13 @@ function scalarStr(v) {
32
32
  return JSON.stringify(v);
33
33
  return String(v);
34
34
  }
35
+ function fencedJson(value) {
36
+ const json = JSON.stringify(value, null, 2);
37
+ let fence = '```';
38
+ while (json.includes(fence))
39
+ fence += '`';
40
+ return `${fence}json\n${json}\n${fence}`;
41
+ }
35
42
  /** A string field is prose when it is named as such, spans lines, or is long
36
43
  * enough that it reads as a sentence rather than an identifier. */
37
44
  function isProse(name, v) {
@@ -122,6 +129,10 @@ export function renderError(p) {
122
129
  // the value the caller supplied.
123
130
  if (p.http_status !== undefined)
124
131
  lines.push(`http status: ${p.http_status}`);
132
+ if ('primaryCompleted' in p && p.primaryCompleted === true) {
133
+ lines.push('primaryCompleted: true');
134
+ lines.push(`Primary result (the command already completed):\n${fencedJson(p.result)}`);
135
+ }
125
136
  lines.push(`Next: ${p.next}`);
126
137
  return `<error code="${attrEsc(p.error)}">\n${lines.join('\n')}\n</error>`;
127
138
  }
@@ -0,0 +1,20 @@
1
+ import type { BrokerSession } from './broker/read-ops.js';
2
+ export interface AdvertisedCommandInvocationMetadata {
3
+ crouterAdvertisedCommandInvocation: {
4
+ raw: string;
5
+ };
6
+ }
7
+ /** Restore extension-owned details while projecting persisted invocation
8
+ * metadata onto the runtime custom message. */
9
+ export declare function hydratePersistedAdvertisedCommandMessages(messages: readonly unknown[]): void;
10
+ /** Add live invocation metadata without changing extension-owned content or
11
+ * details. EventProjection calls this before relaying a custom message. */
12
+ export declare function hydrateLiveAdvertisedCommandMessage(message: unknown): void;
13
+ /** Bind the imminent message_end persistence call to the exact queued app
14
+ * message rather than to a payload-equality search across pending queues. */
15
+ export declare function prepareAdvertisedCommandPersistence(session: BrokerSession, message: unknown): void;
16
+ /** Install persistence/reload plumbing once on a newly built broker session. */
17
+ export declare function installAdvertisedCommandInvocationContract(session: BrokerSession): void;
18
+ /** Preserve a chat-advertised extension command's exact submitted text on the
19
+ * first displayed custom message its handler persists. */
20
+ export declare function promptWithAdvertisedCommandInvocation<T>(session: BrokerSession, raw: string, runPrompt: () => Promise<T>): Promise<T>;