@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
@@ -12,6 +12,7 @@ import type { AttachEnsureRequest, AttachEnsureResultDTO } from './dto/attach.js
12
12
  import type { DeleteProfileRequest, DeleteProfileResultDTO, EnsureProfileRequest, ProfileDTO } from './dto/profiles.js';
13
13
  import type { FilePeekDTO } from './dto/files.js';
14
14
  import type { MemoryDocRefDTO } from './dto/memory.js';
15
+ import type { ChatInventoryDTO } from './dto/chat-inventory.js';
15
16
  import type { CredentialRemovalResultDTO, CredentialResultDTO, InstallCredentialRequest, ModelAuthListDTO } from './dto/modelauth.js';
16
17
  import type { CreateHumanBridgeRequest, HumanBridgeResultDTO, HumanCancelRequest, HumanCancelResultDTO, HumanResolveRequest, HumanResolveResultDTO } from './dto/human.js';
17
18
  import type { CancelReviewRequest, CreateReviewRequest, ListReviewsQuery, ReviewCancelResultDTO, ReviewDocumentBaseDTO, ReviewDTO, ReviewListDTO, ReviewSubmitResultDTO } from './dto/reviews.js';
@@ -151,6 +152,11 @@ export declare class CrtrClient {
151
152
  /** The node's conversation exactly as it ran — raw `.jsonl` bytes plus the
152
153
  * assembled system prompt. For exports; `getSnapshot` is for renderers. */
153
154
  getSession(id: string): Promise<NodeSessionDTO>;
155
+ /** What a non-terminal chat surface may offer for this node: the chat-capable
156
+ * slash commands its live engine registered, and the memory documents an
157
+ * inline `/name` token resolves to. Never revives — a node whose broker is
158
+ * not live answers `broker_live: false` with empty arrays. */
159
+ getChatInventory(id: string): Promise<ChatInventoryDTO>;
154
160
  getArtifacts(id: string, q?: ArtifactsQuery): Promise<ArtifactListDTO>;
155
161
  getContext(id: string): Promise<ContextListDTO>;
156
162
  /** Read an absolute host path as UTF-8 (capped, `truncated` when clipped) for
@@ -160,6 +166,9 @@ export declare class CrtrClient {
160
166
  * node would read — the node's own precedence chain, not this process's.
161
167
  * Pair with `peekFile` to render the document. */
162
168
  resolveMemoryDoc(name: string, nodeId: string): Promise<MemoryDocRefDTO>;
169
+ /** Create-or-return by name. Supplied `projects` are shape-checked even when
170
+ * the profile already exists; their directories are only required to exist
171
+ * when this call creates the profile. */
163
172
  ensureProfile(name: string, req?: EnsureProfileRequest): Promise<ProfileDTO>;
164
173
  listProfiles(): Promise<ProfileDTO[]>;
165
174
  getProfile(name: string): Promise<ProfileDTO>;
@@ -267,6 +267,13 @@ export class CrtrClient {
267
267
  getSession(id) {
268
268
  return this.request('GET', routes.nodeSession(this.nodePath(id)));
269
269
  }
270
+ /** What a non-terminal chat surface may offer for this node: the chat-capable
271
+ * slash commands its live engine registered, and the memory documents an
272
+ * inline `/name` token resolves to. Never revives — a node whose broker is
273
+ * not live answers `broker_live: false` with empty arrays. */
274
+ getChatInventory(id) {
275
+ return this.request('GET', routes.nodeChatInventory(this.nodePath(id)));
276
+ }
270
277
  getArtifacts(id, q) {
271
278
  return this.request('GET', withQuery(routes.nodeArtifacts(this.nodePath(id)), q));
272
279
  }
@@ -287,6 +294,9 @@ export class CrtrClient {
287
294
  return this.request('GET', withQuery(routes.memoryResolve(), { name, node: nodeId }));
288
295
  }
289
296
  // ---- Profiles ----------------------------------------------------------
297
+ /** Create-or-return by name. Supplied `projects` are shape-checked even when
298
+ * the profile already exists; their directories are only required to exist
299
+ * when this call creates the profile. */
290
300
  ensureProfile(name, req) {
291
301
  return this.request('PUT', routes.profile(name), req ?? {});
292
302
  }
@@ -0,0 +1,43 @@
1
+ /** One command a chat surface may advertise. Builtin and non-opted-in rows are
2
+ * dropped before this DTO exists, so `source` never carries `'builtin'`. */
3
+ export interface ChatInventoryCommandDTO {
4
+ /** No leading slash, exactly as the engine dispatches it. */
5
+ name: string;
6
+ description: string;
7
+ source: 'command' | 'template';
8
+ /** Argument shape to display beside the name, when the command supplies one. */
9
+ argument_hint?: string;
10
+ /** Deterministic expansion metadata for a memory-slash command — the same
11
+ * material the terminal preview uses, so a chat preview cannot drift from
12
+ * what submission sends. */
13
+ expansion?: {
14
+ kind: 'memory-slash';
15
+ commandName: string;
16
+ body: string;
17
+ };
18
+ /** Raw prompt-template content (frontmatter stripped), for `source: 'template'`. */
19
+ template?: string;
20
+ }
21
+ /** One resolvable inline memory reference. Metadata only — never the document
22
+ * body or its source path. `shortForm` stays camelCase to mirror the broker's
23
+ * own `RefMeta`, which is where these rows come from. */
24
+ export interface ChatInventoryMemoryRefDTO {
25
+ /** Canonical `/`-joined name, e.g. `taste/writing`. */
26
+ name: string;
27
+ kind: 'knowledge' | 'preference';
28
+ scope: 'node' | 'project' | 'profile' | 'user' | 'builtin';
29
+ shortForm: string;
30
+ }
31
+ /** `GET /v1/nodes/{id}/chat-inventory` result.
32
+ *
33
+ * `broker_live` reports whether the node's engine was reachable at all. There
34
+ * is no per-part error flag: a part that failed and a part that is genuinely
35
+ * empty both arrive as an empty array, and a client's behavior is identical
36
+ * for both. A dormant node answers 200 with `broker_live: false` — this read
37
+ * never revives an engine. */
38
+ export interface ChatInventoryDTO {
39
+ node_id: string;
40
+ broker_live: boolean;
41
+ commands: ChatInventoryCommandDTO[];
42
+ memory_refs: ChatInventoryMemoryRefDTO[];
43
+ }
@@ -0,0 +1,11 @@
1
+ // Chat-inventory DTO. Backs `GET /v1/nodes/{id}/chat-inventory` — the one read
2
+ // a non-terminal chat surface makes to learn what the node's live engine will
3
+ // accept: the slash commands that can complete their outcome from a chat
4
+ // conversation, and the memory documents an inline `/name` token resolves to.
5
+ //
6
+ // Eligibility is decided in crouter and disclosed here. A client never filters
7
+ // by name, infers capability, or invents a row: what is absent from these
8
+ // arrays is not offered.
9
+ //
10
+ // PURITY (spec §3.1): Node built-ins + `src/api/*` only.
11
+ export {};
@@ -1,8 +1,22 @@
1
+ /** The disclosure ladder, lowest to highest. Index order is the ordering. */
2
+ export declare const PROFILE_PROJECT_MEMORY_VALUES: readonly ["none", "name", "preview", "content"];
3
+ /** How much of a project's memory stores the profile relationship lets an
4
+ * automatic delivery disclose. */
5
+ export type ProfileProjectMemory = (typeof PROFILE_PROJECT_MEMORY_VALUES)[number];
6
+ /** One project directory in a profile's purview. */
7
+ export interface ProfileProject {
8
+ /** Absolute, real-path-resolved directory. */
9
+ path: string;
10
+ /** Maximum rung automatic boot and workspace-open delivery may reach from
11
+ * this project's memory stores, regardless of the node's working directory.
12
+ * An authored lower rung stays lower; targeted reads are never capped. */
13
+ memory: ProfileProjectMemory;
14
+ }
1
15
  /** `PUT /v1/profiles/{name}` body — idempotent ensure. Every field applies
2
16
  * only at create; an existing same-named profile is returned untouched. */
3
17
  export interface EnsureProfileRequest {
4
- /** Absolute project directories in the profile's purview. */
5
- projects?: string[];
18
+ /** Project directories in the profile's purview, each with its memory cap. */
19
+ projects?: ProfileProject[];
6
20
  /** Persona kind for node creates under the profile that omit kind. */
7
21
  default_kind?: string;
8
22
  /** Profile facts (identity, role); each entry reaches every broker
@@ -37,9 +51,9 @@ export interface ProfileDTO {
37
51
  /** Stable profile-directory id (`<slug>-<id>`). */
38
52
  id: string;
39
53
  name: string;
40
- projects: string[];
41
- /** Where nodes under this profile run — one of `projects` (the first unless
42
- * re-pointed), or null when the profile owns none. */
54
+ projects: ProfileProject[];
55
+ /** Where nodes under this profile run — one of `projects[].path` (the first
56
+ * unless re-pointed), or null when the profile owns none. */
43
57
  home: string | null;
44
58
  /** ISO timestamp when the profile was paused, or null while active. */
45
59
  paused_at: string | null;
@@ -1,3 +1,4 @@
1
1
  // Profile DTOs (spec §6.6). Profile deletion crosses profile, canvas, cron,
2
2
  // inbox, and review state, so every consumer routes it through crtrd.
3
- export {};
3
+ /** The disclosure ladder, lowest to highest. Index order is the ordering. */
4
+ export const PROFILE_PROJECT_MEMORY_VALUES = ['none', 'name', 'preview', 'content'];
@@ -27,3 +27,4 @@ export * from './dto/memory.js';
27
27
  export * from './dto/inbox.js';
28
28
  export * from './dto/reviews.js';
29
29
  export * from './dto/review-comments.js';
30
+ export * from './dto/chat-inventory.js';
package/dist/api/index.js CHANGED
@@ -28,3 +28,4 @@ export * from './dto/memory.js';
28
28
  export * from './dto/inbox.js';
29
29
  export * from './dto/reviews.js';
30
30
  export * from './dto/review-comments.js';
31
+ export * from './dto/chat-inventory.js';
@@ -11,6 +11,7 @@ export declare const routes: {
11
11
  readonly nodeSnapshot: (id: string) => string;
12
12
  readonly nodeSubject: (id: string) => string;
13
13
  readonly nodeSession: (id: string) => string;
14
+ readonly nodeChatInventory: (id: string) => string;
14
15
  readonly nodeTranscript: (id: string) => string;
15
16
  readonly nodeContext: (id: string) => string;
16
17
  readonly nodeArtifacts: (id: string) => string;
@@ -27,6 +27,7 @@ export const routes = {
27
27
  nodeSnapshot: (id) => `${V}/nodes/${id}/snapshot`,
28
28
  nodeSubject: (id) => `${V}/nodes/${id}/subject`,
29
29
  nodeSession: (id) => `${V}/nodes/${id}/session`,
30
+ nodeChatInventory: (id) => `${V}/nodes/${id}/chat-inventory`,
30
31
  nodeTranscript: (id) => `${V}/nodes/${id}/transcript`,
31
32
  nodeContext: (id) => `${V}/nodes/${id}/context`,
32
33
  nodeArtifacts: (id) => `${V}/nodes/${id}/artifacts`,
@@ -1,13 +1,9 @@
1
1
  import type { RootDef } from './core/command.js';
2
+ import { type CoreHookCatalog } from './core/command-hooks/catalog.js';
2
3
  /** Every shipped subtree name. Cheap (no module loading) — the front-door
3
4
  * recursion guard and the dispatcher's first-token routing need only names. */
4
5
  export declare const SUBTREE_NAMES: readonly string[];
5
- /** Every core command path, space-joined ("cron", "cron add", …) — the set
6
- * plugin `helpAddenda` keys are validated against at the strict gates
7
- * (install, bundle parse, doctor/inspect reports). Loads every core subtree,
8
- * so call it only from those gates, never on a dispatch path. Passthrough
9
- * branches are excluded: crtr never renders their help, so an addendum
10
- * targeting one could never appear. */
6
+ export declare function coreHookCatalog(): Promise<CoreHookCatalog>;
11
7
  export declare function coreCommandPaths(): Promise<ReadonlySet<string>>;
12
8
  /** Build a root that contains only the subtree `first` dispatches into.
13
9
  * Returns the FULL root when `first` is not a recognized subtree — bare `crtr`,
@@ -1,4 +1,7 @@
1
1
  import { defineRoot } from './core/command.js';
2
+ import { createCoreHookCatalog } from './core/command-hooks/catalog.js';
3
+ import { composeCoreHooks } from './core/command-hooks/compose.js';
4
+ import { mark } from './core/timing.js';
2
5
  const TAGLINE = 'crtr: agentic runtime.';
3
6
  /** Flags handled before dispatch, so they belong to no leaf schema and are
4
7
  * stated once at root (help.ts renders them as the Globals footer). */
@@ -43,8 +46,51 @@ export const SUBTREE_NAMES = Object.freeze(Object.keys(SUBTREE_LOADERS));
43
46
  * so call it only from those gates, never on a dispatch path. Passthrough
44
47
  * branches are excluded: crtr never renders their help, so an addendum
45
48
  * targeting one could never appear. */
49
+ /** Every core subtree, built once per process. The hook catalog and the full
50
+ * root both need the complete set; a recognized core first token still loads
51
+ * only its own subtree until something actually demands the whole tree. */
52
+ let coreSubtrees;
53
+ function loadCoreSubtrees() {
54
+ coreSubtrees ??= Promise.all(SUBTREE_NAMES.map((n) => SUBTREE_LOADERS[n]()));
55
+ return coreSubtrees;
56
+ }
57
+ /** Complete static catalog of hook-eligible core leaves. Lifecycle and recovery
58
+ * inspection share the same memoized subtree load as root composition. */
59
+ let hookCatalog;
60
+ export function coreHookCatalog() {
61
+ mark('cli.hooks.catalog');
62
+ hookCatalog ??= loadCoreSubtrees().then(createCoreHookCatalog);
63
+ return hookCatalog;
64
+ }
65
+ /** A loader for the effective hook registry, compiled over the COMPLETE core
66
+ * catalog so a hook targeting any other subtree is never mistaken for a stale
67
+ * target. Loading is lazy — only eligible leaf help or dispatch reaches it,
68
+ * and static help discovery never runs a plugin executable. The memoization
69
+ * is per composed root, never process-global: discovery reads
70
+ * stored plugin bytes and the caller's scope stack, so a plugin install,
71
+ * update, or disable (and a cwd change in a long-lived host) must govern the
72
+ * next root instead of being pinned by the first one. */
73
+ function hookRegistryLoader() {
74
+ let registry;
75
+ return () => {
76
+ // Dynamic import keeps hook discovery (manifest/artifact validation, plugin
77
+ // resolution) off the module graph of an invocation that neither renders
78
+ // eligible leaf help nor dispatches one, matching this file's lazy rationale.
79
+ registry ??= (async () => {
80
+ const { compileHookRegistry, effectiveHookPlugins } = await import('./core/command-hooks/discovery.js');
81
+ const plugins = effectiveHookPlugins();
82
+ mark('cli.hooks.effective_plugins', { count: plugins.length });
83
+ if (plugins.length === 0) {
84
+ mark('cli.hooks.empty_registry');
85
+ return compileHookRegistry(createCoreHookCatalog([]), []);
86
+ }
87
+ return compileHookRegistry(await coreHookCatalog(), plugins);
88
+ })();
89
+ return registry;
90
+ };
91
+ }
46
92
  export async function coreCommandPaths() {
47
- const core = await Promise.all(SUBTREE_NAMES.map((n) => SUBTREE_LOADERS[n]()));
93
+ const core = await loadCoreSubtrees();
48
94
  const paths = new Set();
49
95
  const visit = (node, prefix) => {
50
96
  if (node.kind === 'branch' && node.passthrough !== undefined)
@@ -67,7 +113,7 @@ export async function coreCommandPaths() {
67
113
  export async function resolveRoot(first) {
68
114
  const loader = first !== undefined ? SUBTREE_LOADERS[first] : undefined;
69
115
  if (loader !== undefined) {
70
- return defineRoot({ tagline: TAGLINE, globals: GLOBALS, subtrees: [await loader()] });
116
+ return defineRoot({ tagline: TAGLINE, globals: GLOBALS, subtrees: composeCoreHooks([await loader()], hookRegistryLoader()) });
71
117
  }
72
118
  return buildRoot();
73
119
  }
@@ -88,7 +134,7 @@ export async function resolveRoot(first) {
88
134
  * HTTP-transport plugin manifest replacement takes effect on the next invocation
89
135
  * automatically. Composition performs no network I/O. */
90
136
  export async function buildRoot() {
91
- const core = await Promise.all(SUBTREE_NAMES.map((n) => SUBTREE_LOADERS[n]()));
137
+ const core = await loadCoreSubtrees();
92
138
  // Dynamic import keeps external command discovery/compose (plugin +
93
139
  // HTTP-transport plugin, and their resolver/manifest/HTTP deps) OFF the hot leaf
94
140
  // path's module graph — they load only here, on the fallthrough, matching
@@ -102,5 +148,6 @@ export async function buildRoot() {
102
148
  // sys doctor). Reads only stored manifests — zero network.
103
149
  const snapshot = buildExternalCommandSnapshot(new Set(SUBTREE_NAMES));
104
150
  const external = composeExternalSubtrees(snapshot);
105
- return defineRoot({ tagline: TAGLINE, globals: GLOBALS, subtrees: [...core, ...external] });
151
+ // Only core leaves are hookable, so external contributions compose unwrapped.
152
+ return defineRoot({ tagline: TAGLINE, globals: GLOBALS, subtrees: [...composeCoreHooks(core, hookRegistryLoader()), ...external] });
106
153
  }
@@ -63,6 +63,8 @@ Every kind has both a `base` and an `orchestrator` persona; mode picks which one
63
63
 
64
64
  A profile is a stable **agent identity**: a fixed id, a display name, its own memory store, and a **purview** of project directories it resolves memory and config from. Select it at spawn with `crtr node new --profile <id-or-name>`; omit and a child inherits the caller's profile. Pin a directory's default profile with `crtr profile default` so the startup chooser stops asking. Manage the identity and purview with `crtr profile new/show/project/rename/delete` (see `crtr profile -h`).
65
65
 
66
+ Each project in the purview carries its own `memory` value, capping how much of that project's stores reach a node's automatic boot and workspace-open context from any directory that node works in. Give `content` to the repos whose front doors the profile's nodes should operate inside, `preview` or `name` to a project the profile must know exists but rarely enters, and `none` to purview held for config, plugins, and deliberate reads alone. It is a delivery dial, not an access one: `crtr memory read` and file- and command-routed docs still see those stores whole.
67
+
66
68
  Reach for a **new** profile when a distinct body of work has its own set of directories and its own conventions worth a dedicated store — not for every repo. The **profile memory scope** is exactly where cross-repo conventions and your stance toward that body of work belong (see the memory tiers below); a profile spanning several related dirs lets one doc reach every node working anywhere in that bundle.
67
69
 
68
70
  ## Memory tiers — where a doc lives decides who sees it
@@ -71,6 +73,6 @@ A memory doc's **scope** is a reach dial: the wider the scope, the more agents p
71
73
 
72
74
  - **node** (`nodes/<id>/context/memory/`) — only *this* running node sees it; rides its boot context and dies with the node. Scratch memory for one goal's cross-refresh state.
73
75
  - **profile** (the profile's own store) — every node running under that profile, across all the dirs in its purview. Cross-repo conventions and the user's stance toward that bundle of work.
74
- - **project** (`<project>/.crouter/memory/`) — any agent operating in that one repo. Facts and procedures tied to that codebase. Resolves to the nearest ancestor `.crouter/` walking up from cwd; `--dir` pins an exact repo.
76
+ - **project** (`<project>/.crouter/memory/`) — any agent operating in that one repo. Facts and procedures tied to that codebase. Resolves to the nearest ancestor `.crouter/` walking up from cwd, plus every project in the selected profile's purview; `--dir` pins an exact repo.
75
77
  - **user** (`~/.crouter/memory/`) — person-wide facts and preferences that follow the user everywhere, regardless of repo or profile.
76
78
  - **builtin** (`src/builtin-memory/` in the crouter repo) — ships inside crtr, so *every crtr user on every host* carries it. This tier is the runtime's own self-documentation (this doc lives here); a change here is a change to the product. Author here only for guidance every crouter user needs, never for anything person- or repo-specific.
@@ -47,6 +47,10 @@ Every delivery dedups per transcript keyed on (doc, rung), higher rungs piercing
47
47
 
48
48
  At boot/first-message assembly the runtime mounts: builtin docs, the user store (`~/.crouter/memory/`), the selected profile's store, and every project store — ancestor `.crouter/memory/` dirs walking up from cwd plus each project in the profile's purview. Physical duplicates are deduplicated; name collisions resolve nearest-first (project over profile over user over builtin), which is what lets a project doc shadow a builtin one.
49
49
 
50
+ Each project the selected profile has a relationship with carries a `memory` value — `none`, `name`, `preview`, or `content` — and that value is the maximum rung anything in that project's stores delivers at boot and workspace-open, whatever directory the node is working in. It only lowers: an entry authored below the maximum delivers at its authored rung. `none` contributes nothing to either automatic event, so a `none` project cannot shadow a same-named doc from a wider scope — that wider doc becomes the winner. A project store the selected profile has no relationship with delivers exactly what it authored.
51
+
52
+ The maximum reaches those two events and nothing else. Read, memory-read, and command entries fire at their authored rungs; directory listings, `crtr memory read`, `crtr memory find`, config resolution, and plugin discovery all see the full corpus. An explicit `crtr memory read` is itself a content delivery, so reading a capped doc returns its whole body and records content — the upgrade path for a doc the automatic events disclosed only by name or preview.
53
+
50
54
  A workspace's front door is an ordinary doc carrying the entry pair `{on: workspace-open, at: content}` + `{on: read, match: "./**", at: content}` — the operating guide loads when that workspace mounts or its files are read, not in every boot catalog. `crtr memory lint` requires exactly one workspace-open content doc per profile-managed project store. Multiple mounted roots render broad-to-specific.
51
55
 
52
56
  A `.crouter/memory/` store nested BELOW a mounted root (a package or subsystem dir) is delivered by the read path, not by boot: reading any file beneath its owning dir surfaces its read-routed docs, deduplicated against everything already in the transcript. Addressability is separate — the `crtr memory` leaves (list/read/find/lint/delete/origin) discover nested stores through a bounded walk (git-aware, depth- and time-capped), while the boot catalog stays ancestor+profile only, which is why a nested doc's boot entries are inert (lint warns; drop them).
@@ -4,7 +4,7 @@ when-and-why-to-read: When shaping a planning roadmap, deciding plan structure,
4
4
  short-form: Use when shaping a planning roadmap, deciding plan structure, or preparing a consequential plan for implementation.
5
5
  gate: {kind: plan}
6
6
  rationale: >-
7
- The original playbook required five parallel plan reviewers before every consequential implementation and made “passes all five lenses” the ready bar. Combined with the plan persona's re-review loop, this turned lenses into agents and resolution into reviewer polling rather than plan-owner judgment.
7
+ The original playbook required five parallel plan reviewers before every consequential implementation and made “passes all five lenses” the ready bar. Combined with the plan persona's re-review loop, this turned lenses into agents and resolution into reviewer polling rather than plan-owner judgment. Planners also turned plausible improvements outside the specification into implementation tasks without asking, silently expanding scope.
8
8
  surfaces:
9
9
  - on: boot
10
10
  at: preview
@@ -12,6 +12,12 @@ surfaces:
12
12
 
13
13
  # Planning Playbook
14
14
 
15
+ ## Hold the specified scope
16
+
17
+ Plan the simplest complete implementation of the specification and what it necessarily requires. Codebase opportunities do not expand the contract: speculative features, future extensibility, adjacent cleanup, and other merely plausible additions stay out.
18
+
19
+ When something seems likely desirable but is not explicitly or implicitly required by the specification, ask the user whether to include it through `crtr human` before finishing the plan (`crtr human send -h`), wait for their answer, and make the resulting boundary explicit. Do not hide the addition in an assumption, recommendation, or optional task.
20
+
15
21
  ## Plan Shapes and the Decomposition Decision
16
22
 
17
23
  Every planning effort produces either a flat plan or a decomposed plan (index + part-plans). Choose decomposition for worthwhile parallel planning, not raw size: a flat plan can span many yields, while part-plans add delegation and synthesis cost that independent slices must repay.
@@ -2,7 +2,7 @@
2
2
  kind: knowledge
3
3
  when-and-why-to-read: When eliciting or writing a specification, this knowledge should be read because downstream design and planning need settled intent without making the user answer avoidable questions or forcing every request through the same ceremony.
4
4
  short-form: Elicit only consequential uncertainty, then write a right-sized behavioral contract a downstream reader can use without guessing.
5
- rationale: Specification quality and elicitation guidance lived inside an always-loaded spec persona while the lightweight /spec command had almost none, leaving agents to choose between a vague one-shot and a fixed discovery workflow.
5
+ rationale: Specification quality and elicitation guidance lived inside an always-loaded spec persona while the lightweight /spec command had almost none, leaving agents to choose between a vague one-shot and a fixed discovery workflow. Spec writers also promoted plausible nice-to-haves into requirements without asking, silently expanding the requested work.
6
6
  ---
7
7
 
8
8
  # Writing a specification
@@ -17,6 +17,12 @@ Reflect a concrete interpretation of the request so the user can confirm or corr
17
17
 
18
18
  Spend attention where judgment is load-bearing, not where detail is merely available. Keep settled points moving and fold each answer into the specification as current truth. Stop eliciting when another answer would not materially change the behavioral contract. Explicit approval is warranted when the user is co-authoring the document or the remaining decision is consequential; ordinary reversible work does not need a ritual approval loop.
19
19
 
20
+ ## Hold the requested scope
21
+
22
+ Specify the smallest complete outcome the request and its necessary implications support. Discovered possibilities do not become requirements: speculative features, future extensibility, adjacent cleanup, and other merely plausible additions stay out.
23
+
24
+ When something seems likely desirable but is not explicitly or implicitly required by the request, ask the user whether to include it through `crtr human` before finishing the specification (`crtr human send -h`), wait for their answer, and make the resulting boundary explicit. Do not hide the addition in an assumption, recommendation, or optional requirement.
25
+
20
26
  ## The finished specification
21
27
 
22
28
  A downstream reader should be able to understand the required outcome and produce a design or plan without inventing intent. Include the dimensions that matter for this request rather than forcing a section template:
@@ -164,7 +164,7 @@ function selectedProfileProjects(): string[] {
164
164
  const profileId = process.env["CRTR_PROFILE_ID"] ?? "";
165
165
  if (profileId === "") return [];
166
166
  try {
167
- return loadProfileManifest(profileId).manifest.projects;
167
+ return loadProfileManifest(profileId).manifest.projects.map((project) => project.path);
168
168
  } catch {
169
169
  // A deleted or invalid selected profile must not hide cwd-local commands.
170
170
  return [];
@@ -4,6 +4,7 @@ import { promisify } from "node:util";
4
4
  import { readFileSync, existsSync, realpathSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { dirname, join } from "node:path";
7
+ import type { ChatCommandMetadata } from "../../../../core/runtime/command-surface.js";
7
8
 
8
9
  const exec = promisify(execFile);
9
10
 
@@ -149,6 +150,11 @@ export default async function (pi: ExtensionAPI) {
149
150
  const invocation = `Run \`crtr ${path.join(" ")} -h\` (pass args to run the real command)`;
150
151
  pi.registerCommand(name, {
151
152
  description: node.description ? `${node.description} — ${invocation}` : invocation,
153
+ // Chat-capable: the handler's outcome is the command's output placed into
154
+ // the conversation as a displayed message. The transient status line is
155
+ // decoration a surface without one simply does not show.
156
+ gateway: true as const,
157
+ argumentHint: "[args]",
152
158
  handler: async (args, ctx) => {
153
159
  const extra = (args ?? "").trim();
154
160
  // No args -> show help. With args -> run the real command.
@@ -165,7 +171,7 @@ export default async function (pi: ExtensionAPI) {
165
171
  { triggerTurn: TRIGGER_TURN },
166
172
  );
167
173
  },
168
- });
174
+ } as Parameters<ExtensionAPI["registerCommand"]>[1] & ChatCommandMetadata);
169
175
  };
170
176
 
171
177
  const filters = loadFilters();
@@ -6,6 +6,7 @@ import {
6
6
  type DeterministicCommandExpansion,
7
7
  } from "../../../core/runtime/command-expansion.js";
8
8
  import { expandShellBlocks, hasShellBlocks, DEFAULT_SHELL_TIMEOUT_MS } from "../../../core/runtime/shell-expansion.js";
9
+ import type { ChatCommandMetadata } from "../../../core/runtime/command-surface.js";
9
10
  import { piShellRunner } from "./pi-shell-runner.js";
10
11
 
11
12
  // ---------------------------------------------------------------------------
@@ -126,7 +127,11 @@ export default async function (pi: ExtensionAPI) {
126
127
  // drift from submission. A failed/dynamic command has no expansion metadata.
127
128
  pi.registerCommand(doc.commandName, {
128
129
  description: doc.description,
129
- ...(expansion === undefined ? {} : { expansion }),
130
+ // Chat-capable: the handler's whole outcome is a conversation turn
131
+ // carrying the expanded document, which any surface hosting the
132
+ // conversation receives. Only when the expansion loaded — without it the
133
+ // sole outcome is a terminal-only error notify, so it stays undisclosed.
134
+ ...(expansion === undefined ? {} : { expansion, gateway: true as const }),
130
135
  handler: async (args, ctx) => {
131
136
  if (expansion === undefined) {
132
137
  ctx.ui.notify(
@@ -157,6 +162,6 @@ export default async function (pi: ExtensionAPI) {
157
162
  { triggerTurn: true },
158
163
  );
159
164
  },
160
- } as Parameters<ExtensionAPI["registerCommand"]>[1] & { expansion?: DeterministicCommandExpansion });
165
+ } as Parameters<ExtensionAPI["registerCommand"]>[1] & { expansion?: DeterministicCommandExpansion } & ChatCommandMetadata);
161
166
  }
162
167
  }
@@ -2,7 +2,7 @@ import assert from 'node:assert/strict';
2
2
  import test from 'node:test';
3
3
  import { findRefCompletionContext, RefAwareAutocompleteProvider } from '../input/ref-autocomplete.js';
4
4
  function ref(name, shortForm = `${name} docs`) {
5
- return { name, kind: 'knowledge', scope: 'user', shortForm };
5
+ return { name, kind: 'knowledge', scope: 'user', shortForm, gatewayVisible: true };
6
6
  }
7
7
  /** The wrapped provider; ref handling must never reach it in a ref context. */
8
8
  const inertDelegate = {
@@ -47,7 +47,7 @@ test('Enter-confirming a non-leading ref completion accepts the token and return
47
47
  editor.onSubmit = (text) => {
48
48
  submitted = text;
49
49
  };
50
- const refs = [{ name: 'dev', kind: 'knowledge', scope: 'user', shortForm: 'dev docs' }];
50
+ const refs = [{ name: 'dev', kind: 'knowledge', scope: 'user', shortForm: 'dev docs', gatewayVisible: true }];
51
51
  const delegate = {
52
52
  async getSuggestions() {
53
53
  return null;
@@ -2,7 +2,7 @@ import { homedir } from 'node:os';
2
2
  import { isAbsolute, join, relative, resolve } from 'node:path';
3
3
  import { Input, SelectList, getKeybindings, } from '@earendil-works/pi-tui';
4
4
  import { getSelectListTheme } from '@earendil-works/pi-coding-agent';
5
- import { openReviewSurface } from '../../inbox/review/launch.js';
5
+ import { openReviewWindow } from '../../inbox/review/launch.js';
6
6
  import { createReviewClient, ReviewTerminalError, terminalRefusalNotice } from '../../inbox/review/review-client.js';
7
7
  import { checkReviewableFile, extractFilePaths } from '#core/human/visible-paths';
8
8
  const MAX_VISIBLE_ROWS = 12;
@@ -123,6 +123,6 @@ export async function launchFileReview(host, file, opts) {
123
123
  opts.onNotice(err instanceof ReviewTerminalError ? terminalRefusalNotice(err) : err instanceof Error ? err.message : String(err));
124
124
  return;
125
125
  }
126
- await openReviewSurface(host, { review });
126
+ openReviewWindow(host, review);
127
127
  }
128
128
  export { extractFilePaths };
@@ -31,7 +31,7 @@ export interface KeyHooks {
31
31
  toggleInboxStrip: () => void;
32
32
  cycleModelLadder: (direction: 'forward' | 'backward') => void;
33
33
  inspectLoadedCommand: () => void;
34
- /** The file-review chord — open the transcript-file picker + review surface. */
34
+ /** The file-review chord — pick a transcript file and open its review window. */
35
35
  openFileReview: () => void;
36
36
  /** Search the current node's local profile project files. */
37
37
  openProfileFiles: () => void;
@@ -50,7 +50,7 @@ function profileSearchRoots(profileId) {
50
50
  return { projects: [], memory: null };
51
51
  try {
52
52
  const { profileId: resolvedId, manifest } = loadProfileManifest(profileId);
53
- return { projects: manifest.projects, memory: profileMemoryDir(resolvedId) };
53
+ return { projects: manifest.projects.map((project) => project.path), memory: profileMemoryDir(resolvedId) };
54
54
  }
55
55
  catch {
56
56
  return { projects: [], memory: null };