@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
@@ -17,8 +17,10 @@ import { validatePluginCommands, validateEffectiveCommandPlugins } from '../../c
17
17
  import { normalizeEndpoint, validateEndpoint, validateHttpPluginTransport } from '../../core/command-plugins/endpoint.js';
18
18
  import { buildBrokerEnv, resolvePathExecutable } from '../../core/runtime/spawn-env.js';
19
19
  import { validatePluginBundle } from '../../core/command-plugins/bundle.js';
20
+ import { compileHookRegistry, discoverCandidateHookRegistry, discoverCandidateHookRegistryForCandidates, discoverHookRegistry } from '../../core/command-hooks/discovery.js';
21
+ import { hasDeclaredHooks, hookReport } from '../../core/command-hooks/report.js';
20
22
  import { fetchHttpPluginBundle } from '../../core/command-plugins/transport/http-fetch.js';
21
- import { SUBTREE_NAMES, coreCommandPaths } from '../../build-root.js';
23
+ import { SUBTREE_NAMES, coreCommandPaths, coreHookCatalog } from '../../build-root.js';
22
24
  import { isGitUrl, setPluginEnabled, resolveInstallScope } from './shared.js';
23
25
  /** Validate a just-installed/updated plugin's command manifest, if it declares
24
26
  * one. Returns undefined for plugins without a `commands` pointer. Never
@@ -121,9 +123,44 @@ function assertManifestPageComponentsValid(manifest, where) {
121
123
  throw general(`plugin manifest at ${where} declares an invalid page_components block:\n${reasons.map((r) => `- ${r}`).join('\n')}`, { issues: reasons });
122
124
  }
123
125
  }
126
+ /** Static hook report for an installed plugin. An inactive/shadowed plugin
127
+ * still receives its own artifact and target validation; only effective plugins
128
+ * additionally carry collisions from the complete active registry. */
129
+ async function hookLifecycleReport(name, scope) {
130
+ const plugin = findPluginByName(name, scope);
131
+ if (plugin === null || !hasDeclaredHooks(plugin.manifest))
132
+ return undefined;
133
+ const catalog = await coreHookCatalog();
134
+ const effective = discoverHookRegistry(catalog);
135
+ const validation = effective.validations.find((entry) => entry.plugin.root === plugin.root);
136
+ if (validation === undefined) {
137
+ const isolated = compileHookRegistry(catalog, [plugin]);
138
+ return hookReport(isolated.validations[0]);
139
+ }
140
+ return hookReport(validation, effective.issues.filter((issue) => issue.plugin === plugin.name && issue.scope === plugin.scope));
141
+ }
142
+ /** Validate a source candidate's complete hook behavior before activation. The
143
+ * candidate claims its future scope/name slot, so stale targets and replacement
144
+ * collisions reject the candidate without disturbing installed bytes. */
145
+ async function assertManifestHooksValid(manifest, root, scope, scopeRootPath, where, candidates) {
146
+ if (!hasDeclaredHooks(manifest))
147
+ return;
148
+ const candidate = { name: manifest.name, scope, root, manifest, enabled: true };
149
+ const registryCandidates = candidates ?? [{ plugin: candidate, scopeRoot: scopeRootPath }];
150
+ const catalog = await coreHookCatalog();
151
+ const effective = candidates === undefined
152
+ ? discoverCandidateHookRegistry(catalog, candidate, scopeRootPath)
153
+ : discoverCandidateHookRegistryForCandidates(catalog, registryCandidates);
154
+ const validation = effective.validations.find((entry) => entry.plugin.root === root);
155
+ const issues = validation === undefined
156
+ ? compileHookRegistry(catalog, [candidate]).issues
157
+ : effective.issues.filter((issue) => issue.plugin === candidate.name && issue.scope === candidate.scope);
158
+ if (issues.length > 0)
159
+ throw invalidBundleError(`plugin hook behavior at ${where} is invalid`, issues);
160
+ }
124
161
  /** Validate a package candidate before it gains an active plugin path. The
125
162
  * candidate namespace shadows any installed declaration of the same name. */
126
- function assertPluginCandidateValid(manifest, root, scope, scopeRootPath, where) {
163
+ export async function assertPluginCandidateValid(manifest, root, scope, scopeRootPath, where, candidates) {
127
164
  try {
128
165
  validatePluginManifestMemoryExtensions(manifest);
129
166
  }
@@ -135,6 +172,11 @@ function assertPluginCandidateValid(manifest, root, scope, scopeRootPath, where)
135
172
  assertManifestPageComponentsValid(manifest, where);
136
173
  assertManifestBinValid(manifest, where);
137
174
  assertManifestRequiresValid(manifest, where);
175
+ const commandValidation = validatePluginCommands({ name: manifest.name, scope, root, manifest, enabled: true }, new Set(SUBTREE_NAMES), await coreCommandPaths());
176
+ if (commandValidation.issues.length > 0) {
177
+ throw invalidBundleError(`plugin command behavior at ${where} is invalid`, commandValidation.issues);
178
+ }
179
+ await assertManifestHooksValid(manifest, root, scope, scopeRootPath, where, candidates);
138
180
  const memoryDir = join(root, 'memory');
139
181
  const catalogDocPath = join(scopeRootPath, 'memory', '__plugin_candidate__.md');
140
182
  for (const file of walkFiles(memoryDir, (name) => name.endsWith('.md'), (dir) => dir.startsWith('.'))) {
@@ -176,7 +218,7 @@ function writeStagedBundle(stagingRoot, manifest, bundle) {
176
218
  }
177
219
  }
178
220
  async function validateStagedBundle(name, scope, root, scopeRootPath, manifest) {
179
- assertPluginCandidateValid(manifest, root, scope, scopeRootPath, `${root}/.crouter-plugin/plugin.json`);
221
+ await assertPluginCandidateValid(manifest, root, scope, scopeRootPath, `${root}/.crouter-plugin/plugin.json`);
180
222
  const staged = { name, scope, root, manifest, enabled: true };
181
223
  const validation = validatePluginCommands(staged, new Set(SUBTREE_NAMES), await coreCommandPaths());
182
224
  if (validation.issues.length > 0) {
@@ -300,10 +342,10 @@ function classifyInstallRef(raw) {
300
342
  throw usage(`invalid plugin install ref: ${raw}\n` +
301
343
  'Accepted forms: marketplace/plugin, git URL (https://, ssh://, git@, file://, or *.git), or local path starting ./, ../, or /. Bare plugin names are not install refs.');
302
344
  }
303
- function isMarketplaceRelativeSource(source) {
345
+ export function isMarketplaceRelativeSource(source) {
304
346
  return source.startsWith('./') || source.startsWith('../') || (!isAbsolute(source) && !isGitUrl(source));
305
347
  }
306
- function refreshMarketplace(name, cache, opts) {
348
+ async function refreshMarketplace(name, cache, opts) {
307
349
  const cached = cache.get(name);
308
350
  if (cached !== undefined)
309
351
  return cached;
@@ -349,7 +391,7 @@ function refreshMarketplace(name, cache, opts) {
349
391
  if (candidateManifest === null)
350
392
  throw notFound(`plugin manifest not found in marketplace candidate: ${candidateRoot}/.crouter-plugin/plugin.json`);
351
393
  const pluginScopeRoot = plugin.scope === 'project' ? requireScopeRoot('project') : userScopeRoot();
352
- assertPluginCandidateValid(candidateManifest, candidateRoot, plugin.scope, pluginScopeRoot, `${candidateRoot}/.crouter-plugin/plugin.json`);
394
+ await assertPluginCandidateValid(candidateManifest, candidateRoot, plugin.scope, pluginScopeRoot, `${candidateRoot}/.crouter-plugin/plugin.json`);
353
395
  }
354
396
  renameSync(marketplace.root, previous);
355
397
  try {
@@ -397,7 +439,7 @@ function updatePluginVersion(scope, name, version) {
397
439
  s.plugins[name].last_updated = nowIso();
398
440
  });
399
441
  }
400
- function installFromGit(source, ref, scope) {
442
+ async function installFromGit(source, ref, scope) {
401
443
  const scopeRootPath = scope === 'project' ? ensureProjectScopeRoot() : userScopeRoot();
402
444
  ensureScopeInitialized(scope, scopeRootPath);
403
445
  const pDir = join(scopeRootPath, 'plugins');
@@ -409,7 +451,7 @@ function installFromGit(source, ref, scope) {
409
451
  const manifest = readPluginManifest(staging);
410
452
  if (manifest === null)
411
453
  throw general(`cloned repo does not contain a valid .crouter-plugin/plugin.json: ${source}`);
412
- assertPluginCandidateValid(manifest, staging, scope, scopeRootPath, `${source}/.crouter-plugin/plugin.json`);
454
+ await assertPluginCandidateValid(manifest, staging, scope, scopeRootPath, `${source}/.crouter-plugin/plugin.json`);
413
455
  const finalDir = join(pDir, manifest.name);
414
456
  if (pathExists(finalDir))
415
457
  throw general(`plugin "${manifest.name}" is already installed at ${finalDir}`);
@@ -424,14 +466,14 @@ function installFromGit(source, ref, scope) {
424
466
  removePath(staging);
425
467
  }
426
468
  }
427
- function installFromLocal(source, scope) {
469
+ async function installFromLocal(source, scope) {
428
470
  const sourcePath = resolve(source);
429
471
  const manifest = readPluginManifest(sourcePath);
430
472
  if (manifest === null) {
431
473
  throw notFound(`plugin manifest not found at ${sourcePath}/.crouter-plugin/plugin.json`);
432
474
  }
433
475
  const scopeRootPath = scope === 'project' ? ensureProjectScopeRoot() : userScopeRoot();
434
- assertPluginCandidateValid(manifest, sourcePath, scope, scopeRootPath, `${sourcePath}/.crouter-plugin/plugin.json`);
476
+ await assertPluginCandidateValid(manifest, sourcePath, scope, scopeRootPath, `${sourcePath}/.crouter-plugin/plugin.json`);
435
477
  ensureScopeInitialized(scope, scopeRootPath);
436
478
  const destDir = join(scopeRootPath, 'plugins', manifest.name);
437
479
  if (pathExists(destDir)) {
@@ -449,8 +491,8 @@ function installFromLocal(source, scope) {
449
491
  /** Install one marketplace plugin into a scope. Exported so `sys setup` can
450
492
  * install the default plugin set through the same path `pkg plugin install`
451
493
  * uses — refresh the source, resolve the entry, link/clone, register. */
452
- export function installFromMarketplace(ref, scope) {
453
- const refreshed = refreshMarketplace(ref.marketplace, new Map(), { strict: true });
494
+ export async function installFromMarketplace(ref, scope) {
495
+ const refreshed = await refreshMarketplace(ref.marketplace, new Map(), { strict: true });
454
496
  if (refreshed === null)
455
497
  throw notFound(`marketplace not found: ${ref.marketplace}`);
456
498
  const entry = refreshed.manifest.plugins.find((p) => p.name === ref.plugin);
@@ -470,7 +512,7 @@ export function installFromMarketplace(ref, scope) {
470
512
  const candidateManifest = readPluginManifest(sourcePath);
471
513
  if (candidateManifest === null)
472
514
  throw notFound(`plugin manifest not found at ${sourcePath}/.crouter-plugin/plugin.json`);
473
- assertPluginCandidateValid(candidateManifest, sourcePath, scope, destRoot, `${sourcePath}/.crouter-plugin/plugin.json`);
515
+ await assertPluginCandidateValid(candidateManifest, sourcePath, scope, destRoot, `${sourcePath}/.crouter-plugin/plugin.json`);
474
516
  linkOrCopy(sourcePath, destPluginDir);
475
517
  pluginManifest = candidateManifest;
476
518
  }
@@ -482,7 +524,7 @@ export function installFromMarketplace(ref, scope) {
482
524
  const candidateManifest = readPluginManifest(staging);
483
525
  if (candidateManifest === null)
484
526
  throw notFound(`plugin manifest not found at ${staging}/.crouter-plugin/plugin.json`);
485
- assertPluginCandidateValid(candidateManifest, staging, scope, destRoot, `${staging}/.crouter-plugin/plugin.json`);
527
+ await assertPluginCandidateValid(candidateManifest, staging, scope, destRoot, `${staging}/.crouter-plugin/plugin.json`);
486
528
  ensureDir(join(destRoot, 'plugins'));
487
529
  renameSync(staging, destPluginDir);
488
530
  pluginManifest = candidateManifest;
@@ -503,13 +545,13 @@ export function installFromMarketplace(ref, scope) {
503
545
  });
504
546
  return { name: ref.plugin, scope, path: destPluginDir };
505
547
  }
506
- function updateGitPluginCandidate(plugin) {
548
+ async function updateGitPluginCandidate(plugin) {
507
549
  if (isSymlink(plugin.root)) {
508
550
  const manifest = readPluginManifest(plugin.root);
509
551
  if (manifest === null)
510
552
  throw notFound(`plugin manifest not found at ${plugin.root}/.crouter-plugin/plugin.json`);
511
553
  const scopeRootPath = plugin.scope === 'project' ? requireScopeRoot('project') : userScopeRoot();
512
- assertPluginCandidateValid(manifest, plugin.root, plugin.scope, scopeRootPath, `${plugin.root}/.crouter-plugin/plugin.json`);
554
+ await assertPluginCandidateValid(manifest, plugin.root, plugin.scope, scopeRootPath, `${plugin.root}/.crouter-plugin/plugin.json`);
513
555
  return { updated: false, sha: currentSha(plugin.root) ?? '', manifest };
514
556
  }
515
557
  if (!isGitRepo(plugin.root)) {
@@ -517,7 +559,7 @@ function updateGitPluginCandidate(plugin) {
517
559
  if (manifest === null)
518
560
  throw notFound(`plugin manifest not found at ${plugin.root}/.crouter-plugin/plugin.json`);
519
561
  const scopeRootPath = plugin.scope === 'project' ? requireScopeRoot('project') : userScopeRoot();
520
- assertPluginCandidateValid(manifest, plugin.root, plugin.scope, scopeRootPath, `${plugin.root}/.crouter-plugin/plugin.json`);
562
+ await assertPluginCandidateValid(manifest, plugin.root, plugin.scope, scopeRootPath, `${plugin.root}/.crouter-plugin/plugin.json`);
521
563
  return { updated: false, sha: '', manifest };
522
564
  }
523
565
  const remote = gitSync(['config', '--get', 'remote.origin.url'], plugin.root).stdout.trim();
@@ -536,7 +578,7 @@ function updateGitPluginCandidate(plugin) {
536
578
  const manifest = readPluginManifest(staging);
537
579
  if (manifest === null)
538
580
  throw notFound(`plugin manifest not found in update candidate: ${staging}/.crouter-plugin/plugin.json`);
539
- assertPluginCandidateValid(manifest, staging, plugin.scope, scopeRootPath, `${staging}/.crouter-plugin/plugin.json`);
581
+ await assertPluginCandidateValid(manifest, staging, plugin.scope, scopeRootPath, `${staging}/.crouter-plugin/plugin.json`);
540
582
  renameSync(plugin.root, previous);
541
583
  try {
542
584
  renameSync(staging, plugin.root);
@@ -566,16 +608,17 @@ async function updateOnePlugin(plugin, marketplaceCache, opts) {
566
608
  let marketplaceVersion;
567
609
  let marketplaceUpdated = false;
568
610
  if (plugin.sourceMarketplace !== undefined) {
569
- const refreshed = refreshMarketplace(plugin.sourceMarketplace, marketplaceCache, opts);
611
+ const refreshed = await refreshMarketplace(plugin.sourceMarketplace, marketplaceCache, opts);
570
612
  if (refreshed !== null) {
571
613
  const entry = refreshed.manifest.plugins.find((candidate) => candidate.name === plugin.name);
572
614
  marketplaceVersion = entry?.version;
573
615
  marketplaceUpdated = refreshed.pull.updated;
574
616
  }
575
617
  }
576
- const candidate = updateGitPluginCandidate(plugin);
618
+ const candidate = await updateGitPluginCandidate(plugin);
577
619
  updatePluginVersion(plugin.scope, plugin.name, marketplaceVersion ?? candidate.manifest.version);
578
620
  const commands = await commandReport(plugin.name, plugin.scope);
621
+ const hooks = await hookLifecycleReport(plugin.name, plugin.scope);
579
622
  const warnings = missingRequirementWarnings(candidate.manifest);
580
623
  return {
581
624
  name: plugin.name,
@@ -583,6 +626,7 @@ async function updateOnePlugin(plugin, marketplaceCache, opts) {
583
626
  updated: marketplaceUpdated || candidate.updated,
584
627
  sha: candidate.sha,
585
628
  ...(commands !== undefined ? { commands } : {}),
629
+ ...(hooks !== undefined ? { hooks } : {}),
586
630
  ...(warnings.length > 0 ? { warnings } : {}),
587
631
  };
588
632
  }
@@ -611,6 +655,7 @@ export const pluginInstall = defineLeaf({
611
655
  { name: 'kinds', type: 'integer', required: false, constraint: 'Kind-registry entry count declared by an --endpoint archive\u2019s bundle.json.' },
612
656
  { name: 'pageComponents', type: 'integer', required: false, constraint: 'Page-component registration count declared by an --endpoint archive\u2019s bundle.json.' },
613
657
  { name: 'commands', type: 'object', required: false, constraint: 'Present only when the plugin declares a command manifest. {mounts: string[] (accepted top-level command names now live for the next invocation), issues: object[] (typed validation issues that rejected a contribution — {code, path?, message, received, expected, next})}. Validated statically; commands are never executed.' },
658
+ { name: 'hooks', type: 'object', required: false, constraint: 'Present only when the source plugin declares hooks. {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], issues: object[], trust: string}. Declarations, eligible targets, and replacement collisions are validated statically; hook executables are never run.' },
614
659
  { name: 'warnings', type: 'string[]', required: false, constraint: 'Advisory missing PATH executable requirements declared by the installed plugin. The install still succeeds and leaves the plugin enabled.' },
615
660
  ],
616
661
  outputKind: 'object',
@@ -618,6 +663,7 @@ export const pluginInstall = defineLeaf({
618
663
  'A ref install clones, links, or copies the plugin into the scope plugins directory and registers it with enabled=true. Marketplace installs refresh the source marketplace before resolving the plugin entry.',
619
664
  'An --endpoint install fetches and validates one authenticated uncompressed tar archive before writing. It replaces the complete plugin directory with synthesized provenance and invocation metadata, commands.json, memory docs, any kind-registry entries the bundle.json declares (they join the launch registry below this scope\u2019s own config.json kinds), and any page components it declares (they join the active page-component catalog alongside the user config entries); a fetch or validation failure preserves the prior package.',
620
665
  'If the plugin declares a command manifest, its commands go live on the next crtr invocation. Exec commands run trusted local code only when explicitly invoked; HTTP commands call their declared endpoint only when explicitly invoked.',
666
+ 'Source plugin hooks go live on the next crtr invocation. They run implicitly with caller authority, may receive normalized inputs and after results, may block or replace declared targets, and may transmit received data. Hook executables are never run during install or update validation.',
621
667
  'A plugin manifest `requires` declaration is checked against the PATH a node bash receives after install. Missing executables emit a warning with the declared install hint but never fail the install, disable the plugin, or remove its bare-binary contributions.',
622
668
  ],
623
669
  },
@@ -644,16 +690,18 @@ export const pluginInstall = defineLeaf({
644
690
  if (authEnv !== undefined)
645
691
  throw usage('--auth-env is only valid with --endpoint');
646
692
  const classified = classifyInstallRef(rawRef);
647
- const installed = classified.kind === 'marketplace' ? installFromMarketplace(classified.value, scope)
648
- : classified.kind === 'local' ? installFromLocal(classified.value, scope)
649
- : installFromGit(classified.value, gitRef, scope);
693
+ const installed = classified.kind === 'marketplace' ? await installFromMarketplace(classified.value, scope)
694
+ : classified.kind === 'local' ? await installFromLocal(classified.value, scope)
695
+ : await installFromGit(classified.value, gitRef, scope);
650
696
  const plugin = findPluginByName(installed.name, installed.scope);
651
697
  const commands = await commandReport(installed.name, installed.scope);
698
+ const hooks = await hookLifecycleReport(installed.name, installed.scope);
652
699
  const warnings = plugin === null ? [] : missingRequirementWarnings(plugin.manifest);
653
700
  return {
654
701
  ...installed,
655
702
  ...(plugin?.manifest.transport !== undefined ? { transport: plugin.manifest.transport.kind } : {}),
656
703
  ...(commands !== undefined ? { commands } : {}),
704
+ ...(hooks !== undefined ? { hooks } : {}),
657
705
  ...(warnings.length > 0 ? { warnings } : {}),
658
706
  };
659
707
  },
@@ -766,7 +814,7 @@ export const pluginUpdate = defineLeaf({
766
814
  { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Narrows resolution.' },
767
815
  ],
768
816
  output: [
769
- { name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, kinds?, pageComponents?, commands?, warnings?, error?}. Archive plugins report transport=http and replace their complete directory after an authenticated refetch; version is the archive content identifier, docs is the written memory-document count, kinds is the declared kind-registry entry count, and pageComponents is the declared page-component registration count. sha is present for git updates. A bulk update reports and skips a failed archive plugin. commands is present for validated command plugins: {mounts: string[], issues: object[]}. warnings lists advisory PATH requirements that remain absent after update.' },
817
+ { name: 'updated', type: 'object[]', required: true, constraint: 'One entry per plugin processed: {name, transport?, updated, sha?, version?, docs?, kinds?, pageComponents?, commands?, hooks?, warnings?, error?}. Archive plugins report transport=http and replace their complete directory after an authenticated refetch; version is the archive content identifier, docs is the written memory-document count, kinds is the declared kind-registry entry count, and pageComponents is the declared page-component registration count. sha is present for git updates. A bulk update reports and skips a failed archive plugin. commands is present for validated command plugins: {mounts: string[], issues: object[]}. hooks is present for source hook plugins: {manifestPath?, executablePath?, declarations: {target, phase, op, description, effects}[], issues: object[], trust: string}; it is static and never executes hooks. warnings lists advisory PATH requirements that remain absent after update.' },
770
818
  ],
771
819
  outputKind: 'object',
772
820
  effects: ['Archive plugins unconditionally re-fetch, validate, and replace their complete package; a named failure exits nonzero and preserves the prior package. Bulk update reports a failed archive plugin and continues. Source-installed plugins retain their source update, version, and last_updated behavior.'],
@@ -1,8 +1,4 @@
1
- // `crtr profile default` — the per-cwd default-profile pin. A pin says "when a
2
- // node boots in THIS directory, use THIS profile" and stops the startup
3
- // chooser from asking. It is directory-scoped, not manifest state: independent
4
- // of a profile's project purview (`crtr profile project`). The startup menu's
5
- // `d` action writes the same pin; these verbs are the explicit surface for it.
1
+ // `crtr profile default` manages the per-cwd profile-selection pin.
6
2
  import { defineLeaf, defineBranch } from '../../core/command.js';
7
3
  import { InputError } from '../../core/io.js';
8
4
  import { loadProfileManifest } from '../../core/profiles/manifest.js';
@@ -11,7 +7,7 @@ import { getDefaultProfileId, setDefaultProfileId, clearDefaultProfile, resolveB
11
7
  const setLeaf = defineLeaf({
12
8
  name: 'set',
13
9
  description: 'pin a profile as the default for the current directory',
14
- whenToUse: 'you want crtr to stop asking which profile to use here and always default to a chosen one — the command form of the startup menu\u2019s `d` action. The profile must already cover this directory (add it with `crtr profile project add` first if not).',
10
+ whenToUse: 'you want crtr to stop asking which profile to use here and always default to a chosen one — the command form of the startup menu\u2019s `d` action. The profile must already cover this directory (add its purview relationship with `profile project add` and required `--memory` first if not).',
15
11
  help: {
16
12
  name: 'profile default set',
17
13
  summary: "pin a profile as the current directory's default, so the startup chooser stops asking",
@@ -20,7 +16,7 @@ const setLeaf = defineLeaf({
20
16
  kind: 'positional',
21
17
  name: 'profile',
22
18
  required: true,
23
- constraint: 'Exact profile id, or a unique manifest name. Must already cover the current directory (be at/above/below it in its project purview).',
19
+ constraint: 'Exact profile id, or a unique manifest name. Must already cover the current directory (be at/above/below it in its project purview); this pin does not change automatic memory disclosure.',
24
20
  },
25
21
  ],
26
22
  output: [
@@ -40,7 +36,7 @@ const setLeaf = defineLeaf({
40
36
  throw new InputError({
41
37
  error: 'not_covered',
42
38
  message: `profile "${entry.manifest.name}" does not cover ${resolved}, so a default pinned here would be ignored.`,
43
- next: `Widen its purview first: \`crtr profile project add ${entry.profileId} --dir ${resolved}\`, then re-run this.`,
39
+ next: `Widen its purview and choose its automatic memory disclosure cap first: \`crtr profile project add ${entry.profileId} --dir ${resolved} --memory <none|name|preview|content>\`, then re-run this.`,
44
40
  });
45
41
  }
46
42
  setDefaultProfileId(cwd, entry.profileId);
@@ -133,11 +129,11 @@ const showLeaf = defineLeaf({
133
129
  export const defaultBranch = defineBranch({
134
130
  name: 'default',
135
131
  description: "manage this directory's default-profile pin",
136
- whenToUse: "setting, clearing, or inspecting which profile a directory defaults to at startup — so crtr stops asking. Directory-scoped, independent of a profile's project purview.",
132
+ whenToUse: "setting, clearing, or inspecting which profile a directory defaults to at startup — so crtr stops asking. Directory-scoped, independent of project purview and automatic memory disclosure.",
137
133
  help: {
138
134
  name: 'profile default',
139
135
  summary: "the per-directory default profile — pin one so the startup chooser stops asking",
140
- model: "A pin binds the CURRENT directory to a profile: launches here auto-pick it with no prompt (`crtr --pick-profile` reopens the chooser once). `set` pins a profile (which must already cover this dir), `clear` removes the pin, `show` reports it. This is directory-scoped selection, NOT manifest state — independent of `crtr profile project` (a profile's purview). The startup menu's `d` action writes the same pin.",
136
+ model: "A pin binds the CURRENT directory to a profile: launches here auto-pick it with no prompt (`crtr --pick-profile` reopens the chooser once). `set` pins a profile (which must already cover this dir), `clear` removes the pin, `show` reports it. This is directory-scoped selection, independent of `crtr profile project`: project `path` values govern purview and each `memory` value caps automatic disclosure. The startup menu's `d` action writes the same pin.",
141
137
  },
142
138
  children: [setLeaf, clearLeaf, showLeaf],
143
139
  });
@@ -3,15 +3,16 @@ import { emitLine } from '../../core/io.js';
3
3
  import { listProfiles } from '../../core/profiles/manifest.js';
4
4
  export const listLeaf = defineLeaf({
5
5
  name: 'list',
6
- description: 'inventory every profile',
7
- whenToUse: 'browse every profile that exists — one line each with its id, name, project count, home directory, paused state, and last-used time. Reach for `profile show` once you have picked one, for its full manifest.',
6
+ description: 'inventory every profile and its project relationships',
7
+ whenToUse: 'browse every profile that exists — one line each with its id, name, explicit project relationships, home directory, paused state, and last-used time. Reach for `profile show` once you have picked one, for its full manifest.',
8
8
  help: {
9
9
  name: 'profile list',
10
10
  summary: 'every profile as JSONL, one line each, sorted by name then profile_id',
11
11
  output: [
12
12
  { name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
13
13
  { name: 'name', type: 'string', required: true, constraint: 'Manifest name.' },
14
- { name: 'projects_count', type: 'int', required: true, constraint: 'Number of project directories on the manifest.' },
14
+ { name: 'projects_count', type: 'int', required: true, constraint: 'Number of `{path, memory}` project relationships on the manifest.' },
15
+ { name: 'projects', type: 'ProfileProject[]', required: true, constraint: 'Explicit `{path, memory}` relationships in manifest order. `path` defines profile purview; `memory` caps automatic boot and workspace-open disclosure.' },
15
16
  { name: 'home', type: 'string | null', required: true, constraint: 'Directory the profile\u2019s nodes are pinned to, or null when it owns no projects.' },
16
17
  { name: 'paused_at', type: 'string | null', required: true, constraint: 'ISO 8601 pause timestamp, or null when the profile is active.' },
17
18
  { name: 'default_kind', type: 'string | null', required: true, constraint: 'Stored default node kind, or null when creates default to general.' },
@@ -30,6 +31,7 @@ export const listLeaf = defineLeaf({
30
31
  profile_id: profileId,
31
32
  name: manifest.name,
32
33
  projects_count: manifest.projects.length,
34
+ projects: manifest.projects,
33
35
  home: manifest.home,
34
36
  paused_at: manifest.paused_at,
35
37
  default_kind: manifest.default_kind ?? null,
@@ -2,6 +2,7 @@ import { defineLeaf } from '../../core/command.js';
2
2
  import { usage } from '../../core/errors.js';
3
3
  import { assertInstalledKind } from '../../core/config.js';
4
4
  import { createProfile, ensureRootProfile, profileRoot } from '../../core/profiles/manifest.js';
5
+ import { PROFILE_PROJECT_MEMORY_VALUES } from '../../api/dto/profiles.js';
5
6
  import { resolveMetaPairs } from './meta.js';
6
7
  export const newLeaf = defineLeaf({
7
8
  name: 'new',
@@ -23,14 +24,22 @@ export const newLeaf = defineLeaf({
23
24
  name: 'root',
24
25
  type: 'bool',
25
26
  required: false,
26
- constraint: 'Provision the canonical stable root profile (fixed id `root-00000000`, name `root`) — the fallback default identity for a home — instead of a fresh named one. Idempotent: re-running returns the existing root. Node-free (no `node new --root` needed). Mutually exclusive with `--name`/`--project`/`--default-kind`/`--meta`.',
27
+ constraint: 'Provision the canonical stable root profile (fixed id `root-00000000`, name `root`) — the fallback default identity for a home — instead of a fresh named one. Idempotent: re-running returns the existing root. Node-free (no `node new --root` needed). Mutually exclusive with `--name`/`--project`/`--memory`/`--default-kind`/`--meta`.',
27
28
  },
28
29
  {
29
30
  kind: 'flag',
30
31
  name: 'project',
31
32
  type: 'path',
32
33
  required: false,
33
- constraint: "One initial project directory. Must already exist and be a directory; resolved to its absolute real path before storing. Also becomes the profile's home — where its nodes are pinned. Singular — add more with `crtr profile project add` after creation.",
34
+ constraint: "One initial project directory. Must already exist and be a directory; resolved to its absolute real path before storing. Creates a purview relationship and becomes the profile's home — where its nodes are pinned. Requires paired `--memory`; singular — add more with `crtr profile project add` after creation.",
35
+ },
36
+ {
37
+ kind: 'flag',
38
+ name: 'memory',
39
+ type: 'enum',
40
+ choices: [...PROFILE_PROJECT_MEMORY_VALUES],
41
+ required: false,
42
+ constraint: 'Required with `--project`; invalid without it. Maximum automatic memory disclosure from this project at boot and workspace-open: `none`, `name`, `preview`, or `content`. Does not change purview or explicit `crtr memory read` access.',
34
43
  },
35
44
  {
36
45
  kind: 'flag',
@@ -52,7 +61,7 @@ export const newLeaf = defineLeaf({
52
61
  { name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id (`<slug>-<shortid>`). Pass this, or the name, to every other `profile` verb and to `--profile` elsewhere.' },
53
62
  { name: 'name', type: 'string', required: true, constraint: 'The manifest name as given.' },
54
63
  { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the profile directory.' },
55
- { name: 'projects', type: 'string[]', required: true, constraint: 'Resolved absolute project directories, in manifest order. Empty when --project was omitted.' },
64
+ { name: 'projects', type: 'ProfileProject[]', required: true, constraint: 'Explicit `{path, memory}` project relationships in manifest order. `path` defines purview; `memory` caps automatic boot and workspace-open disclosure. Empty when --project was omitted.' },
56
65
  { name: 'home', type: 'string | null', required: true, constraint: 'Directory the profile\u2019s nodes are pinned to — the initial project, or null when none was given.' },
57
66
  { name: 'default_kind', type: 'string | null', required: true, constraint: 'Stored default persona kind, or null when node creates default to general.' },
58
67
  { name: 'metadata', type: 'object', required: true, constraint: 'Stored metadata map; empty when --meta was omitted.' },
@@ -65,12 +74,13 @@ export const newLeaf = defineLeaf({
65
74
  run: async (input) => {
66
75
  const name = input['name'];
67
76
  const project = input['project'];
77
+ const memory = input['memory'];
68
78
  const defaultKind = input['defaultKind'];
69
79
  const metadata = resolveMetaPairs(input['meta'], 'meta');
70
80
  const root = input['root'] === true;
71
81
  if (root) {
72
- if (name !== undefined || project !== undefined || defaultKind !== undefined || Object.keys(metadata).length > 0) {
73
- throw usage('`--root` provisions the canonical stable root profile and takes no `--name`/`--project`/`--default-kind`/`--meta`.');
82
+ if (name !== undefined || project !== undefined || memory !== undefined || defaultKind !== undefined || Object.keys(metadata).length > 0) {
83
+ throw usage('`--root` provisions the canonical stable root profile and takes no `--name`/`--project`/`--memory`/`--default-kind`/`--meta`.');
74
84
  }
75
85
  const { profileId, manifest } = ensureRootProfile();
76
86
  return {
@@ -82,14 +92,17 @@ export const newLeaf = defineLeaf({
82
92
  default_kind: manifest.default_kind ?? null,
83
93
  metadata: manifest.metadata ?? {},
84
94
  created_at: manifest.created_at,
85
- follow_up: `Root profile ensured. Spawn nodes under it with \`--profile ${profileId}\`, or widen its purview with \`crtr profile project add ${profileId} --dir <path>\`.`,
95
+ follow_up: `Root profile ensured. Spawn nodes under it with \`--profile ${profileId}\`, or widen its purview and choose its automatic memory disclosure cap with \`crtr profile project add ${profileId} --dir <path> --memory <none|name|preview|content>\`.`,
86
96
  };
87
97
  }
88
98
  if (name === undefined)
89
99
  throw usage('provide `--name <name>` to create a profile, or `--root` to provision the stable root profile.');
100
+ if ((project === undefined) !== (memory === undefined)) {
101
+ throw usage('`--project` and `--memory` must be supplied together: `--project` establishes purview and `--memory` sets its automatic boot and workspace-open disclosure cap.');
102
+ }
90
103
  if (defaultKind !== undefined)
91
104
  assertInstalledKind(defaultKind);
92
- const { profileId, manifest } = createProfile(name, project !== undefined ? [project] : [], {
105
+ const { profileId, manifest } = createProfile(name, project !== undefined && memory !== undefined ? [{ path: project, memory }] : [], {
93
106
  defaultKind,
94
107
  ...(Object.keys(metadata).length > 0 ? { metadata } : {}),
95
108
  });
@@ -102,7 +115,7 @@ export const newLeaf = defineLeaf({
102
115
  default_kind: manifest.default_kind ?? null,
103
116
  metadata: manifest.metadata ?? {},
104
117
  created_at: manifest.created_at,
105
- follow_up: `Show it with \`crtr profile show ${profileId}\`, add another project with \`crtr profile project add ${profileId} --dir <path>\`, or list every profile with \`crtr profile list\`.`,
118
+ follow_up: `Show it with \`crtr profile show ${profileId}\`, add purview and choose its automatic memory disclosure cap with \`crtr profile project add ${profileId} --dir <path> --memory <none|name|preview|content>\`, or list every profile with \`crtr profile list\`.`,
106
119
  };
107
120
  },
108
121
  });
@@ -1,17 +1,14 @@
1
- // `crtr profile project` — the project-purview sub-noun of `profile`. A
2
- // profile's `projects` list is what it can SEE (memory + config resolution),
3
- // and its `home` — one of those dirs — is where its nodes actually run. These
4
- // two verbs widen or narrow that list after creation and re-point the home;
5
- // `crtr profile show` reads both back.
1
+ // `crtr profile project` manages purview relationships and their automatic-memory caps.
6
2
  import { defineLeaf, defineBranch } from '../../core/command.js';
7
3
  import { addProfileProject, removeProfileProject, loadProfileManifest } from '../../core/profiles/manifest.js';
4
+ import { PROFILE_PROJECT_MEMORY_VALUES } from '../../api/dto/profiles.js';
8
5
  const addLeaf = defineLeaf({
9
6
  name: 'add',
10
- description: "add a project directory to a profile's purview",
11
- whenToUse: "a profile needs to see (memory + config from) another project directory — extend its purview after creation, including mid-session from a node already running under that profile. Also the way to re-point the profile's home, with --home.",
7
+ description: "add or update a project's purview relationship",
8
+ whenToUse: "a profile needs purview over another project directory, or its automatic memory disclosure cap needs changing — extend or update the relationship after creation. Also the way to re-point the profile's home, with --home.",
12
9
  help: {
13
10
  name: 'profile project add',
14
- summary: "append a project directory to a profile's manifest, deduped by real path",
11
+ summary: "add or update a project relationship in a profile's manifest, deduped by real path",
15
12
  params: [
16
13
  {
17
14
  kind: 'positional',
@@ -24,7 +21,15 @@ const addLeaf = defineLeaf({
24
21
  name: 'dir',
25
22
  type: 'path',
26
23
  required: true,
27
- constraint: 'Directory to add. Must exist and be a directory; resolved to its absolute real path before storing. A path already on the manifest is a no-op, not an error.',
24
+ constraint: 'Directory to add. Must exist and be a directory; resolved to its absolute real path before storing. A path already on the manifest keeps its position and updates its `memory` value.',
25
+ },
26
+ {
27
+ kind: 'flag',
28
+ name: 'memory',
29
+ type: 'enum',
30
+ choices: [...PROFILE_PROJECT_MEMORY_VALUES],
31
+ required: true,
32
+ constraint: 'Maximum automatic memory disclosure from this project at boot and workspace-open: `none`, `name`, `preview`, or `content`. Does not change the profile’s purview or explicit `crtr memory read` access.',
28
33
  },
29
34
  {
30
35
  kind: 'flag',
@@ -36,16 +41,17 @@ const addLeaf = defineLeaf({
36
41
  ],
37
42
  output: [
38
43
  { name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
39
- { name: 'projects', type: 'string[]', required: true, constraint: 'The updated, deduped project list in manifest order.' },
44
+ { name: 'projects', type: 'ProfileProject[]', required: true, constraint: 'The updated `{path, memory}` relationships in manifest order. `path` defines purview; `memory` caps automatic boot and workspace-open disclosure.' },
40
45
  { name: 'home', type: 'string | null', required: true, constraint: "The profile's home directory after the change." },
41
46
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
42
47
  ],
43
48
  outputKind: 'object',
44
- effects: ["Appends to the profile manifest's `projects` array, and rewrites `home` when --home is given. Invalidates the process scope cache."],
49
+ effects: ["Appends a new project relationship or updates an existing relationship’s `memory` in the profile manifest, and rewrites `home` when --home is given. Purview follows `path`; `memory` caps automatic boot and workspace-open disclosure. Invalidates the process scope cache."],
45
50
  },
46
51
  run: async (input) => {
47
52
  const { profileId } = loadProfileManifest(input['profile']);
48
53
  const { manifest } = addProfileProject(profileId, input['dir'], {
54
+ memory: input['memory'],
49
55
  home: input['home'] === true,
50
56
  });
51
57
  return {
@@ -58,7 +64,7 @@ const addLeaf = defineLeaf({
58
64
  });
59
65
  const removeLeaf = defineLeaf({
60
66
  name: 'remove',
61
- description: "remove a project directory from a profile's purview",
67
+ description: "remove a project relationship from a profile's purview",
62
68
  whenToUse: "a profile no longer needs purview over one of its listed project directories — narrow it back. Works even if the directory itself was since deleted.",
63
69
  help: {
64
70
  name: 'profile project remove',
@@ -75,17 +81,17 @@ const removeLeaf = defineLeaf({
75
81
  name: 'dir',
76
82
  type: 'path',
77
83
  required: true,
78
- constraint: 'Directory to remove, matched by its resolved real path against the manifest. Does not require the directory to still exist on disk. Removing the home dir re-points the home at the first remaining project.',
84
+ constraint: 'Directory to remove, matched against the stored project `path`. Does not require the directory to still exist on disk. Removing the home dir re-points the home at the first remaining project.',
79
85
  },
80
86
  ],
81
87
  output: [
82
88
  { name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
83
- { name: 'projects', type: 'string[]', required: true, constraint: 'The updated project list in manifest order.' },
89
+ { name: 'projects', type: 'ProfileProject[]', required: true, constraint: 'The updated `{path, memory}` relationships in manifest order.' },
84
90
  { name: 'home', type: 'string | null', required: true, constraint: "The profile's home directory after the change." },
85
91
  { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands.' },
86
92
  ],
87
93
  outputKind: 'object',
88
- effects: ["Removes an entry from the profile manifest's `projects` array, re-pointing `home` when that entry was it. Invalidates the process scope cache."],
94
+ effects: ["Removes a project relationship from the profile manifest, re-pointing `home` when that entry was it. This narrows purview and removes its automatic memory disclosure cap. Invalidates the process scope cache."],
89
95
  },
90
96
  run: async (input) => {
91
97
  const { profileId } = loadProfileManifest(input['profile']);
@@ -100,12 +106,12 @@ const removeLeaf = defineLeaf({
100
106
  });
101
107
  export const projectBranch = defineBranch({
102
108
  name: 'project',
103
- description: "widen or narrow a profile's project purview",
104
- whenToUse: "changing which project directories a profile can see (its memory + config resolution) after creation — add or remove one.",
109
+ description: "manage a profile's project purview and automatic memory disclosure",
110
+ whenToUse: "changing a profile’s project purview or one relationship’s automatic memory disclosure cap after creation — add, update, or remove one.",
105
111
  help: {
106
112
  name: 'profile project',
107
- summary: "manage a profile's project purview — the directories it resolves memory and config from",
108
- model: "A profile's `projects` list is the set of directories it inherits memory and config from, and its `home` — the first project unless `add --home` says otherwise — is the directory its nodes are pinned to. `add` widens that purview by one directory (deduped by real path) and optionally re-points the home; `remove` narrows it, re-pointing the home when it removed it. Read both back with `crtr profile show`. Purview is independent of a per-cwd default pin (`crtr profile default`) — widening purview never changes which profile a directory defaults to.",
113
+ summary: "manage project purview and automatic memory disclosure caps",
114
+ model: "Each `{path, memory}` relationship widens purview: the profile resolves memory and config from `path`. Its `memory` value separately caps only automatic boot and workspace-open delivery; explicit reads remain available through purview. `add` appends a new real path or updates an existing relationship’s cap without moving it, and can re-point `home`; `remove` narrows purview and re-points `home` when needed. Purview and automatic disclosure are both independent of a per-cwd default pin (`crtr profile default`).",
109
115
  },
110
116
  children: [addLeaf, removeLeaf],
111
117
  });
@@ -21,7 +21,7 @@ export const showLeaf = defineLeaf({
21
21
  output: [
22
22
  { name: 'profile_id', type: 'string', required: true, constraint: 'Stable directory id.' },
23
23
  { name: 'name', type: 'string', required: true, constraint: 'Manifest name.' },
24
- { name: 'projects', type: 'string[]', required: true, constraint: 'Absolute project directories, in manifest order.' },
24
+ { name: 'projects', type: 'ProfileProject[]', required: true, constraint: 'Explicit `{path, memory}` relationships in manifest order. `path` defines profile purview; `memory` caps automatic boot and workspace-open disclosure without limiting explicit reads.' },
25
25
  { name: 'home', type: 'string | null', required: true, constraint: 'Where a node under this profile runs when its create names no directory at all — an explicit --cwd and the directory a root is launched from both win over it. Defaults to the first project; re-point it with `profile project add --home`. Null when the profile owns no projects (the root profile).' },
26
26
  { name: 'paused_at', type: 'string | null', required: true, constraint: 'ISO 8601 pause timestamp, or null when the profile is active.' },
27
27
  { name: 'default_kind', type: 'string | null', required: true, constraint: 'Stored default node kind, or null when creates default to general.' },
@@ -53,8 +53,8 @@ export const showLeaf = defineLeaf({
53
53
  config_path: configPath,
54
54
  config_exists: existsSync(configPath),
55
55
  follow_up: manifest.paused_at === null
56
- ? `Pause it with \`crtr profile pause ${profileId}\`, or set its default kind with \`crtr profile kind ${profileId} <kind>\`.`
57
- : `Resume it with \`crtr profile resume ${profileId}\`.`,
56
+ ? `Manage its purview and automatic memory disclosure with \`crtr profile project add ${profileId} --dir <path> --memory <none|name|preview|content>\`, pause it with \`crtr profile pause ${profileId}\`, or set its default kind with \`crtr profile kind ${profileId} <kind>\`.`
57
+ : `Manage its purview and automatic memory disclosure with \`crtr profile project add ${profileId} --dir <path> --memory <none|name|preview|content>\`, or resume it with \`crtr profile resume ${profileId}\`.`,
58
58
  };
59
59
  },
60
60
  });
@@ -51,6 +51,7 @@ export const surfaceInboxLeaf = defineLeaf({
51
51
  description: 'human inbox toggle dispatcher (bound to the inbox-toggle key; not run by hand)',
52
52
  whenToUse: 'never directly — the inbox-toggle keybinding routes the user’s own request through it. An agent that wants a decision uses crtr human send; how the user reaches their inbox is theirs.',
53
53
  tier: 'hidden',
54
+ hookEligible: false,
54
55
  help: {
55
56
  name: 'surface inbox',
56
57
  summary: 'deliver the human inbox toggle to a live viewer',