@north-light/crouter 0.3.219 → 0.3.221

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 (155) hide show
  1. package/dist/api/client.d.ts +9 -0
  2. package/dist/api/client.js +10 -0
  3. package/dist/api/dto/chat-inventory.d.ts +43 -0
  4. package/dist/api/dto/chat-inventory.js +11 -0
  5. package/dist/api/dto/profiles.d.ts +19 -5
  6. package/dist/api/dto/profiles.js +2 -1
  7. package/dist/api/index.d.ts +1 -0
  8. package/dist/api/index.js +1 -0
  9. package/dist/api/routes.d.ts +1 -0
  10. package/dist/api/routes.js +1 -0
  11. package/dist/build-root.d.ts +2 -6
  12. package/dist/build-root.js +51 -4
  13. package/dist/builtin-memory/internal/agent-shaping.md +3 -1
  14. package/dist/builtin-memory/internal/memory-loading.md +4 -0
  15. package/dist/builtin-memory/plan/roadmap.md +7 -1
  16. package/dist/builtin-memory/spec/guide.md +7 -1
  17. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
  18. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
  19. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +7 -2
  20. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
  21. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
  22. package/dist/clients/attach/overlays/file-review.js +2 -2
  23. package/dist/clients/attach/session/keys.d.ts +1 -1
  24. package/dist/clients/attach/session/profile-files.js +1 -1
  25. package/dist/clients/attach/viewer.js +690 -690
  26. package/dist/clients/inbox/review/launch.d.ts +8 -4
  27. package/dist/clients/inbox/review/launch.js +55 -5
  28. package/dist/clients/inbox/review/review-client.d.ts +1 -0
  29. package/dist/clients/inbox/review/review-client.js +4 -0
  30. package/dist/clients/inbox/review-adapter.d.ts +1 -8
  31. package/dist/clients/inbox/review-adapter.js +4 -52
  32. package/dist/commands/memory/lint.js +2 -1
  33. package/dist/commands/memory/read.js +1 -0
  34. package/dist/commands/memory.js +1 -1
  35. package/dist/commands/pkg/market-manage.js +165 -75
  36. package/dist/commands/pkg/plugin-inspect.js +19 -2
  37. package/dist/commands/pkg/plugin-manage.d.ts +8 -3
  38. package/dist/commands/pkg/plugin-manage.js +72 -24
  39. package/dist/commands/profile/default.js +6 -10
  40. package/dist/commands/profile/list.js +5 -3
  41. package/dist/commands/profile/new.js +21 -8
  42. package/dist/commands/profile/project.js +25 -19
  43. package/dist/commands/profile/show.js +3 -3
  44. package/dist/commands/surface-inbox.js +1 -0
  45. package/dist/commands/sys/__tests__/migrate.test.js +16 -5
  46. package/dist/commands/sys/doctor.js +35 -5
  47. package/dist/commands/sys/migrate.js +38 -19
  48. package/dist/commands/sys/panels/broker-limits-panel.js +3 -3
  49. package/dist/commands/sys/setup-core.js +1 -1
  50. package/dist/commands/sys/sync-project-guidance.js +1 -1
  51. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
  52. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
  53. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
  54. package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
  55. package/dist/core/__tests__/helpers/broker-clients.d.ts +1 -0
  56. package/dist/core/__tests__/helpers/broker-clients.js +1 -0
  57. package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
  58. package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
  59. package/dist/core/__tests__/seam/dormancy-release.test.js +37 -1
  60. package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
  61. package/dist/core/bootstrap.js +6 -0
  62. package/dist/core/canvas/browse/app.js +5 -2
  63. package/dist/core/canvas/browse/model.d.ts +25 -15
  64. package/dist/core/canvas/browse/model.js +86 -65
  65. package/dist/core/canvas/render-source.d.ts +6 -0
  66. package/dist/core/canvas/render-source.js +7 -1
  67. package/dist/core/canvas/render.js +10 -2
  68. package/dist/core/command-hooks/artifact.d.ts +10 -0
  69. package/dist/core/command-hooks/artifact.js +129 -0
  70. package/dist/core/command-hooks/catalog.d.ts +14 -0
  71. package/dist/core/command-hooks/catalog.js +38 -0
  72. package/dist/core/command-hooks/compose.d.ts +15 -0
  73. package/dist/core/command-hooks/compose.js +99 -0
  74. package/dist/core/command-hooks/discovery.d.ts +87 -0
  75. package/dist/core/command-hooks/discovery.js +174 -0
  76. package/dist/core/command-hooks/help.d.ts +5 -0
  77. package/dist/core/command-hooks/help.js +18 -0
  78. package/dist/core/command-hooks/index.d.ts +6 -0
  79. package/dist/core/command-hooks/index.js +6 -0
  80. package/dist/core/command-hooks/report.d.ts +23 -0
  81. package/dist/core/command-hooks/report.js +19 -0
  82. package/dist/core/command-hooks/schema.d.ts +27 -0
  83. package/dist/core/command-hooks/schema.js +68 -0
  84. package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
  85. package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
  86. package/dist/core/command-plugins/presence.d.ts +2 -0
  87. package/dist/core/command-plugins/presence.js +17 -0
  88. package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
  89. package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
  90. package/dist/core/command.d.ts +8 -1
  91. package/dist/core/command.js +12 -10
  92. package/dist/core/help.d.ts +7 -1
  93. package/dist/core/io.d.ts +9 -1
  94. package/dist/core/io.js +44 -2
  95. package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
  96. package/dist/core/memory/inline-ref-inventory.js +15 -8
  97. package/dist/core/memory-resolver.d.ts +13 -1
  98. package/dist/core/memory-resolver.js +25 -19
  99. package/dist/core/profiles/manifest.d.ts +13 -2
  100. package/dist/core/profiles/manifest.js +84 -18
  101. package/dist/core/profiles/select.js +9 -9
  102. package/dist/core/render.js +11 -0
  103. package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
  104. package/dist/core/runtime/advertised-command-invocation.js +233 -0
  105. package/dist/core/runtime/bearings.js +1 -1
  106. package/dist/core/runtime/broker/client-registry.d.ts +6 -3
  107. package/dist/core/runtime/broker/client-registry.js +6 -4
  108. package/dist/core/runtime/broker/event-projection.js +7 -0
  109. package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
  110. package/dist/core/runtime/broker/frame-dispatch.js +16 -10
  111. package/dist/core/runtime/broker/read-ops.d.ts +4 -0
  112. package/dist/core/runtime/broker/read-ops.js +6 -2
  113. package/dist/core/runtime/broker-extension-render.js +1 -1
  114. package/dist/core/runtime/broker-inventory.d.ts +5 -0
  115. package/dist/core/runtime/broker-inventory.js +191 -0
  116. package/dist/core/runtime/broker-protocol.d.ts +13 -2
  117. package/dist/core/runtime/broker.js +10 -1
  118. package/dist/core/runtime/command-surface.d.ts +33 -0
  119. package/dist/core/runtime/command-surface.js +81 -0
  120. package/dist/core/runtime/node-read.js +5 -0
  121. package/dist/core/scope.d.ts +26 -1
  122. package/dist/core/scope.js +52 -12
  123. package/dist/core/substrate/on-read.d.ts +7 -1
  124. package/dist/core/substrate/on-read.js +13 -4
  125. package/dist/core/substrate/render.js +14 -5
  126. package/dist/core/substrate/schema.d.ts +11 -1
  127. package/dist/core/substrate/schema.js +11 -2
  128. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +4 -4
  129. package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
  130. package/dist/daemon/api/handlers/chat-inventory.js +25 -0
  131. package/dist/daemon/api/handlers/profiles.js +7 -1
  132. package/dist/daemon/api/map.d.ts +2 -1
  133. package/dist/daemon/api/map.js +3 -2
  134. package/dist/daemon/api/server.js +2 -0
  135. package/dist/hook-authoring.d.ts +75 -0
  136. package/dist/hook-authoring.js +358 -0
  137. package/dist/hook-process.d.ts +7 -0
  138. package/dist/hook-process.js +34 -0
  139. package/dist/index.d.ts +2 -0
  140. package/dist/index.js +2 -0
  141. package/dist/migrations/002-profile-project-memory.d.ts +2 -0
  142. package/dist/migrations/002-profile-project-memory.js +71 -0
  143. package/dist/migrations/profile-manifests.d.ts +30 -0
  144. package/dist/migrations/profile-manifests.js +70 -0
  145. package/dist/migrations/registry.js +10 -5
  146. package/dist/migrations/types.d.ts +28 -1
  147. package/dist/migrations/types.js +15 -9
  148. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
  149. package/dist/pi-extensions/canvas-structured-output.js +85 -2
  150. package/dist/types.d.ts +15 -6
  151. package/dist/types.js +5 -1
  152. package/package.json +1 -1
  153. package/runtime.lock.json +2 -2
  154. package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
  155. /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/profile-project-memory-delivery.test.d.ts} +0 -0
@@ -82,6 +82,10 @@ export interface HelloFrame {
82
82
  type: 'hello';
83
83
  role: ClientRole;
84
84
  client_id: string;
85
+ /** Whether this socket represents a person attending the conversation. Defaults
86
+ * true for backward compatibility; system observers set false without changing
87
+ * their read-only `role`. */
88
+ attends?: boolean;
85
89
  /** Per-attach welcome transcript bound; absent or invalid keeps the full history. */
86
90
  snapshot_tail?: number;
87
91
  /** Terminal geometry of a tmux-pane viewer; absent for a headless client. */
@@ -404,6 +408,11 @@ export interface ModelChangedFrame {
404
408
  * frames still parse. */
405
409
  spec?: string | null;
406
410
  }
411
+ /** Broadcast to attached observers after a successful reload has cleared the
412
+ * command and memory inventories. */
413
+ export interface ResourcesReloadedFrame {
414
+ type: 'resources_reloaded';
415
+ }
407
416
  /** The `error` control frame. SOURCED from the shared `@north-light/crouter-api`
408
417
  * contract (`src/api/dto/broker.ts`, the same zero-runtime-dep `/v1` module
409
418
  * Northlight Core imports) so the wire shape here and the shape Core consumes
@@ -504,12 +513,14 @@ export interface WireSettings extends SettingsConfig {
504
513
  * `'taste/foo'`) and what `crtr memory read <name>` expects; `scope` is the
505
514
  * WINNING scope after first-wins-by-name precedence dedup (display only —
506
515
  * resolution is by `name` alone). No body/source path: this is a metadata-only
507
- * inventory, never the document content. */
516
+ * inventory, never the document content. `gatewayVisible` is broker-computed
517
+ * chat-surface eligibility; terminal viewers retain every row. */
508
518
  export interface RefMeta {
509
519
  name: string;
510
520
  kind: 'knowledge' | 'preference';
511
521
  scope: 'node' | 'project' | 'profile' | 'user' | 'builtin';
512
522
  shortForm: string;
523
+ gatewayVisible: boolean;
513
524
  }
514
525
  export interface ListModelsData {
515
526
  type: 'data';
@@ -626,7 +637,7 @@ export interface ToolGroupSummaryFrame {
626
637
  /** Everything the broker can send. Live `AgentSessionEvent`s are relayed
627
638
  * verbatim (the broker adds nothing); the broker's own control frames carry
628
639
  * non-colliding `type` discriminants. */
629
- export type BrokerToClient = WelcomeFrame | WorkingActivityFrame | ModelChangedFrame | ErrorFrame | NodeNamedFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | ToolGroupSummaryFrame | AgentSessionEvent;
640
+ export type BrokerToClient = WelcomeFrame | WorkingActivityFrame | ModelChangedFrame | ResourcesReloadedFrame | ErrorFrame | NodeNamedFrame | AckFrame | BrokerDataFrame | BashStartFrame | BashOutputFrame | BashEndFrame | ExtensionUIRequestFrame | ExtensionUIDismissFrame | ToolGroupSummaryFrame | AgentSessionEvent;
630
641
  /** Encode one frame as a single newline-terminated JSON line. */
631
642
  export declare function encodeFrame(frame: ClientToBroker | BrokerToClient): string;
632
643
  /** Byte bounds for a {@link FrameDecoder} (C5). */
@@ -31,6 +31,7 @@ import { FRONT_DOOR_ENV } from './front-door.js';
31
31
  import { CANVAS_EXTENSIONS, piInvocationToSdkConfig, } from './launch.js';
32
32
  import { assertEngineVersion, loadBrokerEngine } from './broker-sdk.js';
33
33
  import { resolveBundledPiPackageDir } from './pi-cli.js';
34
+ import { captureSessionPromptTemplateChatSurfaces } from './command-surface.js';
34
35
  import { PoolCredentialStore } from '../pool-credential-store.js';
35
36
  import { clearCleanAbort, markBusy, markCleanAbort } from './busy.js';
36
37
  import { recordFaultForCurrentNode } from './fault.js';
@@ -45,7 +46,8 @@ import { ToolGroupTracker } from './broker/tool-groups.js';
45
46
  import { onNodeNamed } from './broker/node-named.js';
46
47
  import { FaultRetry } from './broker/fault-retry.js';
47
48
  import { EventProjection } from './broker/event-projection.js';
48
- import { createFrameDispatchContext, handleFrame } from './broker/frame-dispatch.js';
49
+ import { createFrameDispatchContext, handleFrame, } from './broker/frame-dispatch.js';
50
+ import { hydratePersistedAdvertisedCommandMessages, installAdvertisedCommandInvocationContract, } from './advertised-command-invocation.js';
49
51
  import { RebindDriver } from './broker/rebind.js';
50
52
  import { brokerExtensionState, commitBrokerModel } from './broker/daemon-ops.js';
51
53
  import { FrameOverflowError, normalizeSnapshotTail, } from './broker-protocol.js';
@@ -715,6 +717,9 @@ export function snapshotMessages(session, boundaryReviewId) {
715
717
  // `crtr-cycle` marker's `{cycle, fromLeaf}` shape — the fields we read off it
716
718
  // are ones ONLY this module's own `appendCustomEntry` call (above) ever writes.
717
719
  const messages = cycleAwareMessages(session.sessionManager);
720
+ if (messages !== undefined) {
721
+ hydratePersistedAdvertisedCommandMessages(messages.map(({ message }) => message));
722
+ }
718
723
  return messages === undefined ? undefined : visibleMessages(messages, { boundaryReviewId });
719
724
  }
720
725
  // ---------------------------------------------------------------------------
@@ -893,6 +898,10 @@ export async function buildBrokerSession(engine, cfg) {
893
898
  tools: cfg.tools,
894
899
  sessionStartEvent: o.sessionStartEvent,
895
900
  });
901
+ installAdvertisedCommandInvocationContract(created.session);
902
+ // The resource loader has completed its initial template load. Capture
903
+ // crouter-only template metadata before command disclosure can observe it.
904
+ captureSessionPromptTemplateChatSurfaces(created.session);
896
905
  // CONSTRUCTION-ONLY MASK — see modelLessRegistry: reveal now that model
897
906
  // selection (construction-time only) is done, so a later /login re-drive's
898
907
  // list_models/cycle_model/provider-rotation see the real registry.
@@ -0,0 +1,33 @@
1
+ /** The optional fields a shipped extension registration may add so its command
2
+ * is disclosed to chat surfaces. Registrations cast their options through
3
+ * `ChatCapableRegistration` rather than widening pi's own types. */
4
+ export interface ChatCommandMetadata {
5
+ /** Present and exactly `true` to opt this command into chat disclosure. */
6
+ gateway?: true;
7
+ /** Argument shape shown beside the name, e.g. `'[args]'`. */
8
+ argumentHint?: string;
9
+ }
10
+ /** Registration options plus the preserved custom fields crouter reads back off
11
+ * the resolved command. `T` is `Parameters<ExtensionAPI['registerCommand']>[1]`
12
+ * at the call site. */
13
+ export type ChatCapableRegistration<T> = T & ChatCommandMetadata;
14
+ /** What the command disclosure needs to know about one command's chat surface. */
15
+ export interface ChatCommandSurface {
16
+ gatewayCapable: boolean;
17
+ argumentHint?: string;
18
+ }
19
+ /** Read the chat surface off a resolved extension command. `command` is an
20
+ * arbitrary runtime object from another package, so every field is probed
21
+ * rather than trusted. */
22
+ export declare function registrationChatSurface(command: unknown): ChatCommandSurface;
23
+ /** Capture prompt-template chat metadata immediately after pi loads or reloads
24
+ * its templates. The opt-in is the template file's own frontmatter; the hint
25
+ * is pi's already-parsed `argument-hint`. A missing file, unreadable path, or
26
+ * invalid YAML is not chat-capable. */
27
+ export declare function capturePromptTemplateChatSurfaces(templates: unknown): void;
28
+ /** Capture every prompt template a session currently exposes. A broken getter
29
+ * leaves its templates uncaptured and therefore fail-closed. */
30
+ export declare function captureSessionPromptTemplateChatSurfaces(session: unknown): void;
31
+ /** Read the chat surface captured for this loaded template. An uncaptured
32
+ * template is not disclosed to chat surfaces. */
33
+ export declare function templateChatSurface(template: unknown): ChatCommandSurface;
@@ -0,0 +1,81 @@
1
+ // command-surface.ts — crouter's own metadata about the commands the engine
2
+ // registers, and the one place that decides whether a command can complete its
3
+ // outcome from a non-terminal chat surface.
4
+ //
5
+ // Eligibility is OPT-IN and fail-closed. An extension registration declares
6
+ // `gateway: true`; a prompt template declares `gateway: true` in frontmatter.
7
+ // Anything else — a missing flag, a malformed value, an unreadable file — is
8
+ // not chat-capable. Capability is NEVER inferred from a command's name, its
9
+ // source, or a hard-coded list, because the command set is whatever the active
10
+ // VM registered, not a catalog crouter ships.
11
+ //
12
+ // pi keeps unknown registration fields on the resolved command object, so
13
+ // `gateway`/`argumentHint` ride along beside `expansion` without touching pi's
14
+ // global registration types. pi's prompt-template loader is the opposite: it
15
+ // parses `argument-hint` into `argumentHint` and DROPS every other frontmatter
16
+ // key. Capture `gateway` from the template file when the loader finishes, so
17
+ // the eligibility and loaded template content share the same reload boundary.
18
+ import { readFileSync } from 'node:fs';
19
+ import { parseFrontmatterGeneric } from '../frontmatter.js';
20
+ const NOT_CAPABLE = { gatewayCapable: false };
21
+ // Pi drops unknown prompt frontmatter keys. This cache restores crouter's
22
+ // metadata without re-reading a template during command disclosure.
23
+ const templateChatSurfaces = new WeakMap();
24
+ function hintOf(value) {
25
+ return typeof value === 'string' && value.trim() !== '' ? { argumentHint: value } : {};
26
+ }
27
+ /** Read the chat surface off a resolved extension command. `command` is an
28
+ * arbitrary runtime object from another package, so every field is probed
29
+ * rather than trusted. */
30
+ export function registrationChatSurface(command) {
31
+ if (typeof command !== 'object' || command === null)
32
+ return NOT_CAPABLE;
33
+ const record = command;
34
+ if (record['gateway'] !== true)
35
+ return NOT_CAPABLE;
36
+ return { gatewayCapable: true, ...hintOf(record['argumentHint']) };
37
+ }
38
+ /** Capture prompt-template chat metadata immediately after pi loads or reloads
39
+ * its templates. The opt-in is the template file's own frontmatter; the hint
40
+ * is pi's already-parsed `argument-hint`. A missing file, unreadable path, or
41
+ * invalid YAML is not chat-capable. */
42
+ export function capturePromptTemplateChatSurfaces(templates) {
43
+ if (!Array.isArray(templates))
44
+ return;
45
+ for (const template of templates) {
46
+ if (typeof template !== 'object' || template === null)
47
+ continue;
48
+ const { filePath, argumentHint } = template;
49
+ let surface = NOT_CAPABLE;
50
+ if (typeof filePath === 'string' && filePath !== '') {
51
+ try {
52
+ const frontmatter = parseFrontmatterGeneric(readFileSync(filePath, 'utf8')).data;
53
+ if (frontmatter?.['gateway'] === true)
54
+ surface = { gatewayCapable: true, ...hintOf(argumentHint) };
55
+ }
56
+ catch {
57
+ // Fail closed for malformed or unreadable template metadata.
58
+ }
59
+ }
60
+ templateChatSurfaces.set(template, surface);
61
+ }
62
+ }
63
+ /** Capture every prompt template a session currently exposes. A broken getter
64
+ * leaves its templates uncaptured and therefore fail-closed. */
65
+ export function captureSessionPromptTemplateChatSurfaces(session) {
66
+ try {
67
+ if (typeof session !== 'object' || session === null)
68
+ return;
69
+ capturePromptTemplateChatSurfaces(session.promptTemplates);
70
+ }
71
+ catch {
72
+ // Fail closed when an engine cannot expose its prompt templates.
73
+ }
74
+ }
75
+ /** Read the chat surface captured for this loaded template. An uncaptured
76
+ * template is not disclosed to chat surfaces. */
77
+ export function templateChatSurface(template) {
78
+ if (typeof template !== 'object' || template === null)
79
+ return NOT_CAPABLE;
80
+ return templateChatSurfaces.get(template) ?? NOT_CAPABLE;
81
+ }
@@ -13,6 +13,7 @@ import { basename, join } from 'node:path';
13
13
  import { InputError } from '../io.js';
14
14
  import { fullName, getNode } from '../canvas/index.js';
15
15
  import { jobDir, nodeDir, sessionPtrPath } from '../canvas/paths.js';
16
+ import { hydratePersistedAdvertisedCommandMessages } from './advertised-command-invocation.js';
16
17
  import { SessionManager } from './broker-sdk.js';
17
18
  import { openModelRegistry } from './model-registry.js';
18
19
  import { BUILTIN_SLASH_COMMANDS } from './pi-vendored.js';
@@ -153,7 +154,11 @@ async function reconstructVisibleMessages(nodeId) {
153
154
  copyFileSync(sessionFile, copy);
154
155
  const manager = SessionManager.open(copy);
155
156
  const context = manager.buildSessionContext();
157
+ hydratePersistedAdvertisedCommandMessages(context.messages);
156
158
  const reconstructed = cycleAwareMessages(manager);
159
+ if (reconstructed !== undefined) {
160
+ hydratePersistedAdvertisedCommandMessages(reconstructed.map(({ message }) => message));
161
+ }
157
162
  const identified = reconstructed === undefined
158
163
  ? undefined
159
164
  : visibleMessages(reconstructed, { boundaryReviewId: node.review_binding?.review_id });
@@ -1,4 +1,17 @@
1
1
  import type { InstalledPlugin, Scope } from '../types.js';
2
+ import type { ProfileProjectMemory } from '../api/dto/profiles.js';
3
+ /** The ceiling a project root carries when no profile relationship contributes
4
+ * it — the top rung, i.e. no cap at all. */
5
+ export declare const NEUTRAL_PROJECT_MEMORY: ProfileProjectMemory;
6
+ /** One resolved project scope root plus the selected profile relationship's
7
+ * memory ceiling for it. The ceiling is INDEPENDENT of how the root was
8
+ * discovered: a root reached by both cwd and a profile entry still takes that
9
+ * entry's value (first entry in manifest order wins). */
10
+ export interface ProjectScopeRoot {
11
+ /** Absolute path of the `.crouter/` dir. */
12
+ root: string;
13
+ memory: ProfileProjectMemory;
14
+ }
2
15
  export declare function builtinMemoryRoot(): string;
3
16
  export declare function builtinPiPackagesDir(): string;
4
17
  export declare function builtinPiPackageDir(name: string): string;
@@ -15,10 +28,22 @@ export declare function findProjectScopeRoot(startDir?: string): string | null;
15
28
  * selected profile (`CRTR_PROFILE_ID`); a null/unresolvable profile collapses
16
29
  * to the single `[startDir]` pointer, reproducing today's behavior exactly.
17
30
  * Results are cached per (resolved startDir, profileId) — `resetScopeCache()`
18
- * invalidates on any profile mutation that can change project dirs or names. */
31
+ * invalidates on any profile mutation that can change project dirs or names.
32
+ *
33
+ * Each root also carries the selected profile relationship's memory ceiling
34
+ * (`findProjectScopeRootsWithMemory`), a projection layered onto this order
35
+ * rather than part of it: discovery provenance decides precedence, the
36
+ * relationship decides the ceiling, and the two never influence each other. */
19
37
  export declare function findProjectScopeRoots(startDir?: string, profileId?: string | null): string[];
38
+ /** `findProjectScopeRoots` with each root's profile memory ceiling attached —
39
+ * same roots, same order. A root contributed by a profile entry's ancestor
40
+ * walk takes that entry's `memory` (first entry in manifest order wins, even
41
+ * when cwd also reaches the root); every other root takes the neutral
42
+ * `content` ceiling. */
43
+ export declare function findProjectScopeRootsWithMemory(startDir?: string, profileId?: string | null): ProjectScopeRoot[];
20
44
  export declare function projectScopeRoot(startDir?: string): string | null;
21
45
  export declare function projectScopeRoots(startDir?: string, profileId?: string | null): string[];
46
+ export declare function projectScopeRootsWithMemory(startDir?: string, profileId?: string | null): ProjectScopeRoot[];
22
47
  export declare function scopeRoot(scope: Scope): string | null;
23
48
  export declare function requireScopeRoot(scope: Scope): string;
24
49
  export declare function ensureProjectScopeRoot(startDir?: string): string;
@@ -6,6 +6,9 @@ import { assetRoot } from './asset-root.js';
6
6
  import { usage } from './errors.js';
7
7
  import { loadProfileManifest } from './profiles/manifest.js';
8
8
  import { realpathOrSelf } from './fs-utils.js';
9
+ /** The ceiling a project root carries when no profile relationship contributes
10
+ * it — the top rung, i.e. no cap at all. */
11
+ export const NEUTRAL_PROJECT_MEMORY = 'content';
9
12
  // Cache of resolved project-scope-root STACKS, keyed by (resolved cwd, profile
10
13
  // id). A profile widens the pointer set beyond the bare cwd (see
11
14
  // findProjectScopeRoots below), so the cache key must carry both — the same
@@ -13,7 +16,8 @@ import { realpathOrSelf } from './fs-utils.js';
13
16
  // `resetScopeCache()` (called by every profile mutation in
14
17
  // src/core/profiles/manifest.ts) clears the whole cache; there is no
15
18
  // per-entry invalidation because a profile edit can change which pointer set
16
- // any cached cwd belongs to.
19
+ // any cached cwd belongs to. Path-only callers read `roots`, which is the same
20
+ // array in the same order as `annotated`.
17
21
  const scopeRootsCache = new Map();
18
22
  function scopeRootsCacheKey(resolvedStartDir, profileId) {
19
23
  return `${resolvedStartDir}\u0000${profileId ?? ''}`;
@@ -94,19 +98,40 @@ function collectAncestorScopeRoots(startDir, userRoot) {
94
98
  * selected profile (`CRTR_PROFILE_ID`); a null/unresolvable profile collapses
95
99
  * to the single `[startDir]` pointer, reproducing today's behavior exactly.
96
100
  * Results are cached per (resolved startDir, profileId) — `resetScopeCache()`
97
- * invalidates on any profile mutation that can change project dirs or names. */
101
+ * invalidates on any profile mutation that can change project dirs or names.
102
+ *
103
+ * Each root also carries the selected profile relationship's memory ceiling
104
+ * (`findProjectScopeRootsWithMemory`), a projection layered onto this order
105
+ * rather than part of it: discovery provenance decides precedence, the
106
+ * relationship decides the ceiling, and the two never influence each other. */
98
107
  export function findProjectScopeRoots(startDir = process.cwd(), profileId = process.env['CRTR_PROFILE_ID'] || null) {
108
+ return resolveProjectScopeRoots(startDir, profileId).roots;
109
+ }
110
+ /** `findProjectScopeRoots` with each root's profile memory ceiling attached —
111
+ * same roots, same order. A root contributed by a profile entry's ancestor
112
+ * walk takes that entry's `memory` (first entry in manifest order wins, even
113
+ * when cwd also reaches the root); every other root takes the neutral
114
+ * `content` ceiling. */
115
+ export function findProjectScopeRootsWithMemory(startDir = process.cwd(), profileId = process.env['CRTR_PROFILE_ID'] || null) {
116
+ return resolveProjectScopeRoots(startDir, profileId).annotated;
117
+ }
118
+ function resolveProjectScopeRoots(startDir, profileId) {
99
119
  const resolvedStart = resolve(startDir);
100
120
  const key = scopeRootsCacheKey(resolvedStart, profileId);
101
121
  const cached = scopeRootsCache.get(key);
102
122
  if (cached !== undefined)
103
123
  return cached;
104
124
  const userRoot = userScopeRoot();
105
- const pointers = [resolvedStart];
125
+ // A null `memory` marks the cwd pointer: it decides precedence like any
126
+ // other pointer but contributes no ceiling.
127
+ const pointers = [
128
+ { dir: resolvedStart, memory: null },
129
+ ];
106
130
  if (profileId !== null && profileId !== '') {
107
131
  try {
108
132
  const { manifest } = loadProfileManifest(profileId);
109
- pointers.push(...manifest.projects);
133
+ for (const project of manifest.projects)
134
+ pointers.push({ dir: project.path, memory: project.memory });
110
135
  }
111
136
  catch {
112
137
  // Missing/deleted/invalid profile: this is a hot path every memory and
@@ -116,16 +141,23 @@ export function findProjectScopeRoots(startDir = process.cwd(), profileId = proc
116
141
  }
117
142
  const seen = new Set();
118
143
  const roots = [];
144
+ const caps = new Map();
119
145
  for (const pointer of pointers) {
120
- for (const root of collectAncestorScopeRoots(pointer, userRoot)) {
121
- if (seen.has(root))
122
- continue;
123
- seen.add(root);
124
- roots.push(root);
146
+ for (const root of collectAncestorScopeRoots(pointer.dir, userRoot)) {
147
+ if (!seen.has(root)) {
148
+ seen.add(root);
149
+ roots.push(root);
150
+ }
151
+ // Independent of the order dedupe above: a root already discovered from
152
+ // cwd still takes the first profile entry's ceiling.
153
+ if (pointer.memory !== null && !caps.has(root))
154
+ caps.set(root, pointer.memory);
125
155
  }
126
156
  }
127
- scopeRootsCache.set(key, roots);
128
- return roots;
157
+ const annotated = roots.map((root) => ({ root, memory: caps.get(root) ?? NEUTRAL_PROJECT_MEMORY }));
158
+ const entry = { roots, annotated };
159
+ scopeRootsCache.set(key, entry);
160
+ return entry;
129
161
  }
130
162
  export function projectScopeRoot(startDir) {
131
163
  return findProjectScopeRoot(startDir);
@@ -133,6 +165,9 @@ export function projectScopeRoot(startDir) {
133
165
  export function projectScopeRoots(startDir, profileId) {
134
166
  return findProjectScopeRoots(startDir, profileId);
135
167
  }
168
+ export function projectScopeRootsWithMemory(startDir, profileId) {
169
+ return findProjectScopeRootsWithMemory(startDir, profileId);
170
+ }
136
171
  export function scopeRoot(scope) {
137
172
  // The builtin scope has no scope-root dir; its content lives in dedicated
138
173
  // package directories such as builtinMemoryRoot().
@@ -160,7 +195,12 @@ export function ensureProjectScopeRoot(startDir = process.cwd()) {
160
195
  const resolvedStart = resolve(startDir);
161
196
  const root = join(resolvedStart, CRTR_DIR_NAME);
162
197
  const profileId = process.env['CRTR_PROFILE_ID'] || null;
163
- scopeRootsCache.set(scopeRootsCacheKey(resolvedStart, profileId), [root]);
198
+ // Neutral ceiling: the empty cached stack proves no profile entry's ancestor
199
+ // walk reached this root, since it did not exist to be walked into.
200
+ scopeRootsCache.set(scopeRootsCacheKey(resolvedStart, profileId), {
201
+ roots: [root],
202
+ annotated: [{ root, memory: NEUTRAL_PROJECT_MEMORY }],
203
+ });
164
204
  return root;
165
205
  }
166
206
  export function pluginsDir(scope) {
@@ -15,7 +15,13 @@ export declare function renderOnReadDocsForSubject(subject: NodeConfigSubject, r
15
15
  /** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
16
16
  * lookup. `seen` is the transcript-scoped dedup set shared with the on-read
17
17
  * hook, so a front door delivered here never re-delivers on a later read
18
- * beneath its store. */
18
+ * beneath its store.
19
+ *
20
+ * Each candidate's authored rung is clamped to its project's profile
21
+ * relationship cap BEFORE the `none` filter, the ordering, the render, and the
22
+ * seen-map recording — so a capped doc never renders above its cap, a `none`
23
+ * project contributes nothing, and a later explicit `crtr memory read` can
24
+ * still upgrade what workspace-open disclosed at a lower rung. */
19
25
  export declare function renderWorkspaceOpenDocsForSubject(subject: NodeConfigSubject, cwd: string, profileId: string | null, seen?: InjectedDocs): string;
20
26
  /** Inner `<memory>` blocks for the memory-read event: docs whose
21
27
  * `memory-read` entries match the doc a `crtr memory read` just resolved
@@ -33,11 +33,11 @@ import { CRTR_DIR_NAME } from '../../types.js';
33
33
  import { pathExists, readText, walkFiles } from '../fs-utils.js';
34
34
  import { parseFrontmatterGeneric } from '../frontmatter.js';
35
35
  import { listAllMemoryDocs, listProjectMemoryDocs } from '../memory-resolver.js';
36
- import { userScopeRoot } from '../scope.js';
36
+ import { NEUTRAL_PROJECT_MEMORY, userScopeRoot } from '../scope.js';
37
37
  import { gatePasses } from './gate.js';
38
38
  import { commandDeliveryRung, memoryReadDeliveryRung, owningRootOf, readDeliveryRung, workspaceOpenRung } from './surface-match.js';
39
39
  import { deliveredAtOrAbove, recordDelivery } from './injected-store.js';
40
- import { normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName } from './schema.js';
40
+ import { minRung, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName } from './schema.js';
41
41
  import { cachedSubstrateDocsInclusive } from './session-cache.js';
42
42
  import { realpathOrSelf } from '../fs-utils.js';
43
43
  const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc']);
@@ -91,6 +91,9 @@ function loadProjectDoc(file, memoryDir) {
91
91
  scope: 'project',
92
92
  path: file,
93
93
  body,
94
+ // The read event is uncapped by design: this walk is reached only by an
95
+ // explicit file read, never by automatic boot/workspace-open delivery.
96
+ projectMemory: NEUTRAL_PROJECT_MEMORY,
94
97
  };
95
98
  }
96
99
  catch {
@@ -244,7 +247,13 @@ export function renderOnReadDocsForSubject(subject, readFilePath, seen = new Map
244
247
  /** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
245
248
  * lookup. `seen` is the transcript-scoped dedup set shared with the on-read
246
249
  * hook, so a front door delivered here never re-delivers on a later read
247
- * beneath its store. */
250
+ * beneath its store.
251
+ *
252
+ * Each candidate's authored rung is clamped to its project's profile
253
+ * relationship cap BEFORE the `none` filter, the ordering, the render, and the
254
+ * seen-map recording — so a capped doc never renders above its cap, a `none`
255
+ * project contributes nothing, and a later explicit `crtr memory read` can
256
+ * still upgrade what workspace-open disclosed at a lower rung. */
248
257
  export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, seen = new Map()) {
249
258
  let docs;
250
259
  try {
@@ -256,7 +265,7 @@ export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, seen
256
265
  return '';
257
266
  }
258
267
  const candidates = dedupeByPhysicalPath(docs)
259
- .map((doc) => ({ doc, rung: workspaceOpenRung(doc) }))
268
+ .map((doc) => ({ doc, rung: minRung(workspaceOpenRung(doc), doc.projectMemory) }))
260
269
  .filter(({ rung }) => rung !== 'none')
261
270
  .sort((a, b) => {
262
271
  const aRoot = owningRootOf(a.doc) ?? '';
@@ -57,14 +57,14 @@ import { subKindsAvailableTo } from '../config.js';
57
57
  import { parseFrontmatterGeneric } from '../frontmatter.js';
58
58
  import { pathExists, readText, realpathOrSelf, walkFiles } from '../fs-utils.js';
59
59
  import { memoryDir } from '../runtime/memory.js';
60
- import { projectScopeRoots } from '../scope.js';
60
+ import { NEUTRAL_PROJECT_MEMORY, projectScopeRoots } from '../scope.js';
61
61
  import { gatePasses } from './gate.js';
62
62
  // De-barreled to LEAF modules on purpose: importing from './index.js' would
63
63
  // pull subject.js (canvas-db) transitively, re-tainting every CLI consumer of
64
64
  // the pure render fns. The canvas-db wrappers (renderPreferencesSection /
65
65
  // renderKnowledgeBlock) live in render-node.js instead.
66
66
  import { recordDelivery } from './injected-store.js';
67
- import { bootRung, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, rungRank, } from './schema.js';
67
+ import { bootRung, minRung, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName, rungRank, } from './schema.js';
68
68
  import { cachedSubstrateDocs } from './session-cache.js';
69
69
  /** First-wins dedup by (already-normalized) name, preserving the input's
70
70
  * precedence order. The array's ORDER is the precedence signal — callers must
@@ -89,7 +89,13 @@ function dedupeFirstWins(docs) {
89
89
  * selection ONLY; display ordering is a separate, later step (see
90
90
  * renderGrouped) so a builtin doc can never mask a project/user override by
91
91
  * virtue of sorting first. Uses the per-session cache so the full corpus is
92
- * scanned + parsed at most once per session across the boot-render calls. */
92
+ * scanned + parsed at most once per session across the boot-render calls.
93
+ *
94
+ * The profile relationship's cap applies HERE, before dedup: a `none` project
95
+ * leaves the corpus entirely (it contributes nothing, not even a `[+N more]`
96
+ * count, and so cannot shadow a wider-scope automatic winner), and every
97
+ * surviving doc's authored boot rung is clamped to its cap before dedup, the
98
+ * session cache, and delivery seeding see it. */
93
99
  function selectWinners(subject, kind) {
94
100
  let docs;
95
101
  try {
@@ -100,8 +106,9 @@ function selectWinners(subject, kind) {
100
106
  }
101
107
  const eligible = docs
102
108
  .filter((d) => d.kind === kind)
109
+ .filter((d) => d.projectMemory !== 'none')
103
110
  .filter((d) => gatePasses(d, subject))
104
- .map((d) => ({ ...d, bootRung: bootRung(d) }));
111
+ .map((d) => ({ ...d, bootRung: minRung(bootRung(d), d.projectMemory) }));
105
112
  return dedupeFirstWins(eligible);
106
113
  }
107
114
  // ---------------------------------------------------------------------------
@@ -238,7 +245,9 @@ function nodeLocalDocs(nodeId, subject) {
238
245
  // node-local is NOT a resolver scope; `scope` is a placeholder never read
239
246
  // by gate eval (keyed off the NODE subject, not the doc) nor by the
240
247
  // renderers (keyed off name / body / rung).
241
- out.push({ ...schema, name, scope: 'user', path: file, body });
248
+ // Node-local is not a project store: no profile relationship names it,
249
+ // so it carries the neutral ceiling.
250
+ out.push({ ...schema, name, scope: 'user', path: file, body, projectMemory: NEUTRAL_PROJECT_MEMORY });
242
251
  }
243
252
  catch {
244
253
  // A single malformed file is skipped, never fatal to the render.
@@ -1,14 +1,18 @@
1
+ import { type ProfileProjectMemory } from '../../api/dto/profiles.js';
1
2
  import type { MemoryDoc, MemoryScope } from '../memory-resolver.js';
2
3
  export declare const KINDS: readonly ["knowledge", "preference"];
3
4
  export type DocKind = (typeof KINDS)[number];
4
5
  /** Is `v` one of the two valid document kinds? */
5
6
  export declare function isDocKind(v: unknown): v is DocKind;
6
7
  export declare const RUNGS: readonly ["none", "name", "preview", "content"];
7
- export type Rung = (typeof RUNGS)[number];
8
+ export type Rung = ProfileProjectMemory;
8
9
  /** Ordinal of a rung on the ladder (none=0 … content=3). */
9
10
  export declare function rungRank(r: Rung): number;
10
11
  /** Does rung `r` disclose at least as much as `min`? */
11
12
  export declare function rungAtLeast(r: Rung, min: Rung): boolean;
13
+ /** The lower of two rungs — the clamp automatic delivery applies to an
14
+ * authored rung against its project relationship's cap. */
15
+ export declare function minRung(a: Rung, b: Rung): Rung;
12
16
  /** Strip an optional `NN-` ordering prefix from ONE path segment (file or
13
17
  * directory display name). `00-runtime-base` -> `runtime-base`; `spine` (no
14
18
  * prefix) is unchanged. */
@@ -111,6 +115,12 @@ export interface SubstrateDoc extends SubstrateSchema {
111
115
  path: string;
112
116
  /** Document body, frontmatter stripped. */
113
117
  body: string;
118
+ /** The selected profile relationship's cap on the project store this doc
119
+ * resolved from — the maximum rung automatic boot and workspace-open
120
+ * delivery may disclose it at. `content` is the neutral ceiling every
121
+ * non-project doc and every uncapped project root carries. Targeted reads,
122
+ * command routing, and directory listings ignore it. */
123
+ projectMemory: ProfileProjectMemory;
114
124
  }
115
125
  /** Parse a raw frontmatter record (from `parseFrontmatterGeneric`, via the
116
126
  * resolver) into a typed schema with defaults applied. Returns `null` when the
@@ -7,6 +7,7 @@
7
7
  // The runtime parser is tolerant by contract (it maps over many docs and must
8
8
  // never throw): invalid `surfaces` entries are dropped rather than crashing a
9
9
  // render, and `crtr memory lint` owns strict authoring-time enforcement.
10
+ import { PROFILE_PROJECT_MEMORY_VALUES } from '../../api/dto/profiles.js';
10
11
  // ---------------------------------------------------------------------------
11
12
  // Kinds — the two semantic kinds (design §3): `knowledge` (consult — procedural
12
13
  // playbooks + factual references merged) vs `preference` (behave — standing
@@ -22,7 +23,10 @@ export function isDocKind(v) {
22
23
  // each rung is a superset of disclosure over the one before:
23
24
  // none < name < preview < content
24
25
  // ---------------------------------------------------------------------------
25
- export const RUNGS = ['none', 'name', 'preview', 'content'];
26
+ // The ladder is ONE union, owned by the public API DTO (`ProfileProjectMemory`)
27
+ // so a profile relationship's cap and a doc's authored rung are the same values
28
+ // and clamp against each other without a conversion.
29
+ export const RUNGS = PROFILE_PROJECT_MEMORY_VALUES;
26
30
  /** Ordinal of a rung on the ladder (none=0 … content=3). */
27
31
  export function rungRank(r) {
28
32
  return RUNGS.indexOf(r);
@@ -31,6 +35,11 @@ export function rungRank(r) {
31
35
  export function rungAtLeast(r, min) {
32
36
  return rungRank(r) >= rungRank(min);
33
37
  }
38
+ /** The lower of two rungs — the clamp automatic delivery applies to an
39
+ * authored rung against its project relationship's cap. */
40
+ export function minRung(a, b) {
41
+ return rungRank(a) <= rungRank(b) ? a : b;
42
+ }
34
43
  // ---------------------------------------------------------------------------
35
44
  // Display-name normalization — the optional `NN-` ordering pin. A doc's
36
45
  // physical path may carry a two-digit numeric prefix on a file or directory
@@ -133,7 +142,7 @@ export function parseSubstrateDoc(doc) {
133
142
  const schema = parseSubstrateFrontmatter(doc.frontmatter);
134
143
  if (schema === null)
135
144
  return null;
136
- return { ...schema, name: doc.name, scope: doc.scope, path: doc.path, body: doc.body };
145
+ return { ...schema, name: doc.name, scope: doc.scope, path: doc.path, body: doc.body, projectMemory: doc.projectMemory };
137
146
  }
138
147
  /** The kind a doc resolves to for `--kind` filtering/matching: the substrate
139
148
  * kind when the frontmatter parses as a valid substrate doc, else the raw
@@ -42,7 +42,7 @@ after(() => {
42
42
  resetScopeCache();
43
43
  });
44
44
  test('create rejects a paused profile before a node exists', async () => {
45
- const profile = createProfile('paused profile', [cwd]);
45
+ const profile = createProfile('paused profile', [{ path: cwd, memory: 'content' }]);
46
46
  pauseProfile(profile.profileId);
47
47
  const ctx = {
48
48
  method: 'POST',
@@ -62,7 +62,7 @@ test('create rejects a paused profile before a node exists', async () => {
62
62
  assert.equal(listNodes().length, beforeRows);
63
63
  });
64
64
  test('revive refuses a paused profile before launching a broker', () => {
65
- const profile = createProfile('paused revive', [cwd]);
65
+ const profile = createProfile('paused revive', [{ path: cwd, memory: 'content' }]);
66
66
  pauseProfile(profile.profileId);
67
67
  const nodeId = 'paused-revive-node';
68
68
  createNode({
@@ -90,7 +90,7 @@ test('revive refuses a paused profile before launching a broker', () => {
90
90
  // because they all reach the canvas through spawnNode and none of them call
91
91
  // handleCreate.
92
92
  test('the birth gate refuses a paused profile for a daemon-side spawn', () => {
93
- const profile = createProfile('paused spawn', [cwd]);
93
+ const profile = createProfile('paused spawn', [{ path: cwd, memory: 'content' }]);
94
94
  pauseProfile(profile.profileId);
95
95
  const beforeRows = listNodes().length;
96
96
  assert.throws(() => spawnNode({ kind: 'general', cwd, parent: null, profile_id: profile.profileId }), (error) => {
@@ -102,7 +102,7 @@ test('the birth gate refuses a paused profile for a daemon-side spawn', () => {
102
102
  assert.equal(listNodes().length, beforeRows);
103
103
  });
104
104
  test('create kind resolves from the profile default when omitted', () => {
105
- const profile = createProfile('default kind', [cwd]);
105
+ const profile = createProfile('default kind', [{ path: cwd, memory: 'content' }]);
106
106
  setProfileDefaultKind(profile.profileId, 'developer');
107
107
  const entry = loadProfileManifest(profile.profileId);
108
108
  assert.equal(resolveCreateKind(undefined, entry), 'developer');
@@ -0,0 +1,2 @@
1
+ import type { RouteTable } from '../router.js';
2
+ export declare const chatInventoryRoutes: RouteTable;
@@ -0,0 +1,25 @@
1
+ // Chat-inventory handler. `GET /v1/nodes/{id}/chat-inventory` answers what a
2
+ // non-terminal chat surface may offer for one node: the slash commands its live
3
+ // engine registered that opted into chat disclosure, and the memory documents an
4
+ // inline `/name` token resolves to.
5
+ //
6
+ // Read-only in both directions — it takes one observer round trip against a
7
+ // broker that is already running and writes no canvas state. A node whose broker
8
+ // is not live answers 200 with `broker_live: false` and empty arrays rather than
9
+ // 404 or a revival: "this node has nothing to offer right now" is a normal
10
+ // answer, not an error.
11
+ import { getNode } from '../../../core/canvas/canvas.js';
12
+ import { isSafeNodeId } from '../../../core/canvas/paths.js';
13
+ import { notFound, usage } from '../../../core/errors.js';
14
+ import { readChatInventory } from '../../../core/runtime/broker-inventory.js';
15
+ async function handleGet(ctx) {
16
+ const id = ctx.params['id'];
17
+ if (!isSafeNodeId(id))
18
+ throw usage(`invalid node id: ${id}`, { received: id });
19
+ if (getNode(id) === null)
20
+ throw notFound(`unknown node: ${id}`, { received: id });
21
+ return { status: 200, body: await readChatInventory(id) };
22
+ }
23
+ export const chatInventoryRoutes = [
24
+ { method: 'GET', pattern: '/v1/nodes/:id/chat-inventory', handler: handleGet },
25
+ ];