@north-light/crouter 0.3.220 → 0.3.222

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/dist/api/client.d.ts +29 -0
  2. package/dist/api/client.js +44 -0
  3. package/dist/api/dto/chat-inventory.d.ts +56 -0
  4. package/dist/api/dto/chat-inventory.js +11 -0
  5. package/dist/api/dto/human-requests.d.ts +88 -0
  6. package/dist/api/dto/human-requests.js +4 -0
  7. package/dist/api/dto/human.d.ts +3 -0
  8. package/dist/api/dto/profiles.d.ts +19 -5
  9. package/dist/api/dto/profiles.js +2 -1
  10. package/dist/api/dto/reviews.d.ts +2 -0
  11. package/dist/api/index.d.ts +2 -0
  12. package/dist/api/index.js +2 -0
  13. package/dist/api/routes.d.ts +8 -0
  14. package/dist/api/routes.js +11 -0
  15. package/dist/build-root.d.ts +2 -6
  16. package/dist/build-root.js +51 -4
  17. package/dist/builtin-memory/00-runtime-base/00-authoring.md +31 -0
  18. package/dist/builtin-memory/00-runtime-base/01-escalation.md +14 -0
  19. package/dist/builtin-memory/{insights/listen.md → 00-runtime-base/02-insight-capture.md} +1 -0
  20. package/dist/builtin-memory/02-turn-lifecycle/00-ending-a-turn.md +27 -0
  21. package/dist/builtin-memory/{02-lifecycle/01-resident.md → 02-turn-lifecycle/02-resident.md} +5 -0
  22. package/dist/builtin-memory/04-base-worker.md +4 -8
  23. package/dist/builtin-memory/04-orchestration-kernel.md +1 -1
  24. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +1 -0
  25. package/dist/builtin-memory/05-kinds/advisor/advice-contract.md +1 -0
  26. package/dist/builtin-memory/05-kinds/design/00-base.md +2 -1
  27. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +2 -1
  28. package/dist/builtin-memory/05-kinds/design/design-contract.md +19 -0
  29. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -0
  30. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +1 -0
  31. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -0
  32. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -0
  33. package/dist/builtin-memory/05-kinds/general/00-base.md +1 -0
  34. package/dist/builtin-memory/05-kinds/plan/00-base.md +2 -1
  35. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +2 -1
  36. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +28 -0
  37. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -0
  38. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -0
  39. package/dist/builtin-memory/05-kinds/plan/reviewers/lens-contract.md +1 -0
  40. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -0
  41. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -0
  42. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -0
  43. package/dist/builtin-memory/05-kinds/review/00-base.md +1 -0
  44. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +1 -0
  45. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +1 -0
  46. package/dist/builtin-memory/05-kinds/review/security-findings.md +1 -0
  47. package/dist/builtin-memory/05-kinds/spec/00-base.md +4 -3
  48. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +1 -0
  49. package/dist/builtin-memory/05-kinds/spec/requirements.md +1 -0
  50. package/dist/builtin-memory/design/guide.md +35 -0
  51. package/dist/builtin-memory/design/roadmap.md +21 -0
  52. package/dist/builtin-memory/insights/capture.md +1 -1
  53. package/dist/builtin-memory/internal/agent-shaping.md +3 -1
  54. package/dist/builtin-memory/internal/memory-loading.md +4 -0
  55. package/dist/builtin-memory/internal/plugins.md +10 -1
  56. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  57. package/dist/builtin-memory/plan/roadmap.md +6 -22
  58. package/dist/builtin-memory/spec/guide.md +23 -6
  59. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +1 -1
  60. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/crtr-commands/index.ts +7 -1
  61. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +34 -16
  62. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +1 -1
  63. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +1 -1
  64. package/dist/clients/attach/overlays/file-review.js +2 -2
  65. package/dist/clients/attach/render/markdown-source.js +106 -1
  66. package/dist/clients/attach/session/file-links.d.ts +13 -4
  67. package/dist/clients/attach/session/file-links.js +54 -58
  68. package/dist/clients/attach/session/keys.d.ts +1 -1
  69. package/dist/clients/attach/session/profile-files.js +1 -1
  70. package/dist/clients/attach/viewer.js +698 -696
  71. package/dist/clients/inbox/controller.js +1 -1
  72. package/dist/clients/inbox/resolve.d.ts +1 -0
  73. package/dist/clients/inbox/review/launch.d.ts +8 -4
  74. package/dist/clients/inbox/review/launch.js +55 -5
  75. package/dist/clients/inbox/review/review-client.d.ts +1 -0
  76. package/dist/clients/inbox/review/review-client.js +7 -1
  77. package/dist/clients/inbox/review-adapter.d.ts +1 -8
  78. package/dist/clients/inbox/review-adapter.js +4 -52
  79. package/dist/commands/__tests__/human.test.js +2 -2
  80. package/dist/commands/human/request.d.ts +2 -0
  81. package/dist/commands/human/request.js +281 -0
  82. package/dist/commands/human.js +5 -2
  83. package/dist/commands/memory/lint.js +2 -1
  84. package/dist/commands/memory/read.js +1 -0
  85. package/dist/commands/memory.js +1 -1
  86. package/dist/commands/pkg/market-manage.js +165 -75
  87. package/dist/commands/pkg/plugin-inspect.js +19 -2
  88. package/dist/commands/pkg/plugin-manage.d.ts +8 -3
  89. package/dist/commands/pkg/plugin-manage.js +72 -24
  90. package/dist/commands/profile/default.js +6 -10
  91. package/dist/commands/profile/list.js +5 -3
  92. package/dist/commands/profile/new.js +21 -8
  93. package/dist/commands/profile/project.js +25 -19
  94. package/dist/commands/profile/show.js +3 -3
  95. package/dist/commands/surface-inbox.js +1 -0
  96. package/dist/commands/sys/__tests__/migrate.test.js +16 -5
  97. package/dist/commands/sys/config.js +2 -2
  98. package/dist/commands/sys/doctor.js +87 -5
  99. package/dist/commands/sys/migrate.js +38 -19
  100. package/dist/commands/sys/setup-core.js +1 -1
  101. package/dist/commands/sys/sync-project-guidance.js +1 -1
  102. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +7 -4
  103. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +1 -1
  104. package/dist/core/__tests__/fixtures/c5-command-boundary-ext.js +24 -0
  105. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -18
  106. package/dist/core/__tests__/fixtures/fake-engine.js +8 -1
  107. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +71 -0
  108. package/dist/core/__tests__/human-action-delivery.test.d.ts +1 -0
  109. package/dist/core/__tests__/human-action-delivery.test.js +140 -0
  110. package/dist/core/__tests__/human-actions.test.d.ts +1 -0
  111. package/dist/core/__tests__/human-actions.test.js +116 -0
  112. package/dist/core/__tests__/inline-memory-refs.test.js +36 -2
  113. package/dist/core/__tests__/profile-project-memory-delivery.test.d.ts +1 -0
  114. package/dist/core/__tests__/profile-project-memory-delivery.test.js +217 -0
  115. package/dist/core/__tests__/prospective-inventory-capability-parity.test.d.ts +1 -0
  116. package/dist/core/__tests__/prospective-inventory-capability-parity.test.js +91 -0
  117. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.d.ts +1 -0
  118. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +127 -0
  119. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.d.ts +1 -0
  120. package/dist/core/__tests__/seam/prospective-inventory-stdout.test.js +31 -0
  121. package/dist/core/__tests__/serial/broker-sdk-wiring.test.js +102 -2
  122. package/dist/core/bootstrap.js +6 -0
  123. package/dist/core/canvas/browse/app.js +5 -2
  124. package/dist/core/canvas/browse/model.d.ts +25 -15
  125. package/dist/core/canvas/browse/model.js +86 -65
  126. package/dist/core/canvas/db.js +23 -0
  127. package/dist/core/canvas/human-deliveries.d.ts +53 -0
  128. package/dist/core/canvas/human-deliveries.js +75 -0
  129. package/dist/core/canvas/render-source.d.ts +6 -0
  130. package/dist/core/canvas/render-source.js +7 -1
  131. package/dist/core/canvas/render.js +10 -2
  132. package/dist/core/command-hooks/artifact.d.ts +10 -0
  133. package/dist/core/command-hooks/artifact.js +129 -0
  134. package/dist/core/command-hooks/catalog.d.ts +14 -0
  135. package/dist/core/command-hooks/catalog.js +38 -0
  136. package/dist/core/command-hooks/compose.d.ts +15 -0
  137. package/dist/core/command-hooks/compose.js +99 -0
  138. package/dist/core/command-hooks/discovery.d.ts +87 -0
  139. package/dist/core/command-hooks/discovery.js +174 -0
  140. package/dist/core/command-hooks/help.d.ts +5 -0
  141. package/dist/core/command-hooks/help.js +18 -0
  142. package/dist/core/command-hooks/index.d.ts +6 -0
  143. package/dist/core/command-hooks/index.js +6 -0
  144. package/dist/core/command-hooks/report.d.ts +23 -0
  145. package/dist/core/command-hooks/report.js +19 -0
  146. package/dist/core/command-hooks/schema.d.ts +27 -0
  147. package/dist/core/command-hooks/schema.js +68 -0
  148. package/dist/core/command-hooks/transport/exec-invoke.d.ts +22 -0
  149. package/dist/core/command-hooks/transport/exec-invoke.js +274 -0
  150. package/dist/core/command-plugins/presence.d.ts +2 -0
  151. package/dist/core/command-plugins/presence.js +17 -0
  152. package/dist/core/command-plugins/transport/exec-invoke.d.ts +5 -0
  153. package/dist/core/command-plugins/transport/exec-invoke.js +58 -5
  154. package/dist/core/command.d.ts +8 -1
  155. package/dist/core/command.js +12 -10
  156. package/dist/core/config.d.ts +13 -1
  157. package/dist/core/config.js +51 -1
  158. package/dist/core/feed/inbox.d.ts +6 -0
  159. package/dist/core/feed/inbox.js +9 -1
  160. package/dist/core/help.d.ts +7 -1
  161. package/dist/core/human/action-binding.d.ts +21 -0
  162. package/dist/core/human/action-binding.js +40 -0
  163. package/dist/core/human/completion.d.ts +38 -0
  164. package/dist/core/human/completion.js +27 -0
  165. package/dist/core/human/convention.d.ts +2 -0
  166. package/dist/core/human/convention.js +2 -0
  167. package/dist/core/human/tickets.d.ts +25 -6
  168. package/dist/core/human/tickets.js +19 -13
  169. package/dist/core/human/types.d.ts +5 -0
  170. package/dist/core/human-actions.d.ts +25 -0
  171. package/dist/core/human-actions.js +101 -0
  172. package/dist/core/io.d.ts +9 -1
  173. package/dist/core/io.js +44 -2
  174. package/dist/core/memory/inline-ref-inventory.d.ts +2 -1
  175. package/dist/core/memory/inline-ref-inventory.js +15 -8
  176. package/dist/core/memory-resolver.d.ts +13 -1
  177. package/dist/core/memory-resolver.js +26 -20
  178. package/dist/core/profiles/manifest.d.ts +13 -2
  179. package/dist/core/profiles/manifest.js +84 -18
  180. package/dist/core/profiles/select.d.ts +2 -0
  181. package/dist/core/profiles/select.js +29 -12
  182. package/dist/core/render.js +11 -0
  183. package/dist/core/runtime/advertised-command-invocation.d.ts +20 -0
  184. package/dist/core/runtime/advertised-command-invocation.js +233 -0
  185. package/dist/core/runtime/bearings.js +1 -1
  186. package/dist/core/runtime/broker/event-projection.js +7 -0
  187. package/dist/core/runtime/broker/frame-dispatch.d.ts +1 -1
  188. package/dist/core/runtime/broker/frame-dispatch.js +13 -11
  189. package/dist/core/runtime/broker/read-ops.d.ts +4 -0
  190. package/dist/core/runtime/broker/read-ops.js +6 -2
  191. package/dist/core/runtime/broker-extension-render.js +1 -1
  192. package/dist/core/runtime/broker-inventory.d.ts +4 -0
  193. package/dist/core/runtime/broker-inventory.js +116 -0
  194. package/dist/core/runtime/broker-persona-guidance.js +1 -1
  195. package/dist/core/runtime/broker-protocol.d.ts +9 -2
  196. package/dist/core/runtime/broker.js +10 -1
  197. package/dist/core/runtime/chat-inventory-rows.d.ts +8 -0
  198. package/dist/core/runtime/chat-inventory-rows.js +105 -0
  199. package/dist/core/runtime/command-surface.d.ts +38 -0
  200. package/dist/core/runtime/command-surface.js +117 -0
  201. package/dist/core/runtime/launch-target.d.ts +25 -0
  202. package/dist/core/runtime/launch-target.js +54 -0
  203. package/dist/core/runtime/node-read.js +5 -0
  204. package/dist/core/runtime/persona.js +3 -3
  205. package/dist/core/runtime/prospective-inventory-cli.d.ts +1 -0
  206. package/dist/core/runtime/prospective-inventory-cli.js +61 -0
  207. package/dist/core/runtime/prospective-inventory.d.ts +10 -0
  208. package/dist/core/runtime/prospective-inventory.js +88 -0
  209. package/dist/core/runtime/spawn.d.ts +3 -1
  210. package/dist/core/runtime/spawn.js +5 -3
  211. package/dist/core/scope.d.ts +26 -1
  212. package/dist/core/scope.js +52 -12
  213. package/dist/core/substrate/on-read.d.ts +7 -1
  214. package/dist/core/substrate/on-read.js +30 -32
  215. package/dist/core/substrate/render-node.d.ts +3 -2
  216. package/dist/core/substrate/render-node.js +3 -2
  217. package/dist/core/substrate/render.js +65 -24
  218. package/dist/core/substrate/schema.d.ts +16 -2
  219. package/dist/core/substrate/schema.js +14 -5
  220. package/dist/core/user-settings.d.ts +4 -0
  221. package/dist/core/user-settings.js +1 -0
  222. package/dist/daemon/api/__tests__/profile-launch-gates.test.js +56 -7
  223. package/dist/daemon/api/handlers/chat-inventory.d.ts +2 -0
  224. package/dist/daemon/api/handlers/chat-inventory.js +25 -0
  225. package/dist/daemon/api/handlers/human-requests.d.ts +2 -0
  226. package/dist/daemon/api/handlers/human-requests.js +409 -0
  227. package/dist/daemon/api/handlers/human.js +3 -0
  228. package/dist/daemon/api/handlers/inbox.js +3 -0
  229. package/dist/daemon/api/handlers/nodes.d.ts +1 -3
  230. package/dist/daemon/api/handlers/nodes.js +11 -46
  231. package/dist/daemon/api/handlers/profiles.js +7 -1
  232. package/dist/daemon/api/handlers/prospective-chat-inventory.d.ts +2 -0
  233. package/dist/daemon/api/handlers/prospective-chat-inventory.js +59 -0
  234. package/dist/daemon/api/handlers/reviews.js +10 -2
  235. package/dist/daemon/api/map.d.ts +2 -1
  236. package/dist/daemon/api/map.js +3 -2
  237. package/dist/daemon/api/server.js +6 -0
  238. package/dist/daemon/crtrd.js +6 -0
  239. package/dist/daemon/human/deliver-action.d.ts +16 -0
  240. package/dist/daemon/human/deliver-action.js +168 -0
  241. package/dist/daemon/human/finish.d.ts +8 -5
  242. package/dist/daemon/human/finish.js +45 -6
  243. package/dist/daemon/human/sweep.js +4 -1
  244. package/dist/daemon/reconcilers/human-delivery-lane.d.ts +10 -0
  245. package/dist/daemon/reconcilers/human-delivery-lane.js +41 -0
  246. package/dist/daemon/review/finish.d.ts +8 -3
  247. package/dist/daemon/review/finish.js +19 -1
  248. package/dist/hook-authoring.d.ts +75 -0
  249. package/dist/hook-authoring.js +358 -0
  250. package/dist/hook-process.d.ts +7 -0
  251. package/dist/hook-process.js +34 -0
  252. package/dist/index.d.ts +2 -0
  253. package/dist/index.js +2 -0
  254. package/dist/migrations/002-profile-project-memory.d.ts +2 -0
  255. package/dist/migrations/002-profile-project-memory.js +71 -0
  256. package/dist/migrations/profile-manifests.d.ts +30 -0
  257. package/dist/migrations/profile-manifests.js +70 -0
  258. package/dist/migrations/registry.js +10 -5
  259. package/dist/migrations/types.d.ts +28 -1
  260. package/dist/migrations/types.js +15 -9
  261. package/dist/pi-extensions/__tests__/canvas-structured-output.test.js +21 -4
  262. package/dist/pi-extensions/canvas-structured-output.js +85 -2
  263. package/dist/types.d.ts +23 -6
  264. package/dist/types.js +1 -0
  265. package/package.json +1 -1
  266. package/runtime.lock.json +2 -2
  267. package/dist/builtin-memory/00-runtime-base.md +0 -55
  268. package/dist/builtin-memory/design.md +0 -55
  269. package/dist/clients/attach/__tests__/file-review-focus.test.js +0 -49
  270. /package/dist/builtin-memory/{02-lifecycle/00-terminal.md → 02-turn-lifecycle/01-terminal.md} +0 -0
  271. /package/dist/{clients/attach/__tests__/file-review-focus.test.d.ts → core/__tests__/fixtures/memory-slash-live-probe.d.ts} +0 -0
@@ -0,0 +1,23 @@
1
+ import type { PluginManifest } from '../../types.js';
2
+ import type { HookDiscoveryIssue, HookPluginValidation } from './discovery.js';
3
+ import type { DeclaredHook } from './schema.js';
4
+ export declare const HOOK_TRUST_WARNING = "Hooks receive normalized inputs; after hooks receive results. They run implicitly and may block or replace declared targets. The local executable runs with caller authority and can transmit received data.";
5
+ export interface HookDeclarationReport {
6
+ target: string;
7
+ phase: DeclaredHook['phase'];
8
+ op: string;
9
+ description: string;
10
+ effects: readonly string[];
11
+ }
12
+ /** One static hook artifact report shared by plugin inspection and lifecycle
13
+ * output. It only reads declarations and filesystem metadata; it never runs
14
+ * the hook executable. */
15
+ export interface HookReport {
16
+ manifestPath?: string;
17
+ executablePath?: string;
18
+ declarations: readonly HookDeclarationReport[];
19
+ issues: readonly HookDiscoveryIssue[];
20
+ trust: typeof HOOK_TRUST_WARNING;
21
+ }
22
+ export declare function hasDeclaredHooks(manifest: Pick<PluginManifest, 'hooks' | 'hookExecutable'>): boolean;
23
+ export declare function hookReport(validation: HookPluginValidation, issues?: readonly HookDiscoveryIssue[]): HookReport;
@@ -0,0 +1,19 @@
1
+ export const HOOK_TRUST_WARNING = 'Hooks receive normalized inputs; after hooks receive results. They run implicitly and may block or replace declared targets. The local executable runs with caller authority and can transmit received data.';
2
+ export function hasDeclaredHooks(manifest) {
3
+ return manifest.hooks !== undefined || manifest.hookExecutable !== undefined;
4
+ }
5
+ export function hookReport(validation, issues = validation.issues) {
6
+ return Object.freeze({
7
+ ...(validation.manifestPath === undefined ? {} : { manifestPath: validation.manifestPath }),
8
+ ...(validation.executablePath === undefined ? {} : { executablePath: validation.executablePath }),
9
+ declarations: Object.freeze((validation.manifest?.hooks ?? []).map((hook) => Object.freeze({
10
+ target: hook.target,
11
+ phase: hook.phase,
12
+ op: hook.op,
13
+ description: hook.description,
14
+ effects: Object.freeze([...hook.effects]),
15
+ }))),
16
+ issues: Object.freeze([...issues]),
17
+ trust: HOOK_TRUST_WARNING,
18
+ });
19
+ }
@@ -0,0 +1,27 @@
1
+ export type HookPhase = 'before' | 'after' | 'replace';
2
+ export interface DeclaredHook {
3
+ target: string;
4
+ phase: HookPhase;
5
+ op: string;
6
+ description: string;
7
+ effects: string[];
8
+ }
9
+ export interface ValidatedHookManifest {
10
+ schemaVersion: 1;
11
+ hooks: DeclaredHook[];
12
+ }
13
+ export type HookIssueCode = 'hook_manifest_unreadable' | 'hook_manifest_invalid' | 'hook_schema_version' | 'hook_path_unsafe' | 'hook_not_executable';
14
+ export interface HookManifestIssue {
15
+ code: HookIssueCode;
16
+ path?: string;
17
+ message: string;
18
+ received: string;
19
+ expected: string;
20
+ next: string;
21
+ }
22
+ export interface HookManifestValidation {
23
+ manifest?: ValidatedHookManifest;
24
+ issues: HookManifestIssue[];
25
+ }
26
+ /** Validates the declarative hooks.json payload without filesystem access. */
27
+ export declare function validateHookManifest(raw: unknown): HookManifestValidation;
@@ -0,0 +1,68 @@
1
+ import { isRecord } from '../../shared/predicates.js';
2
+ function typeName(value) {
3
+ if (value === null)
4
+ return 'null';
5
+ if (Array.isArray(value))
6
+ return 'array';
7
+ return typeof value;
8
+ }
9
+ /** Validates the declarative hooks.json payload without filesystem access. */
10
+ export function validateHookManifest(raw) {
11
+ const issues = [];
12
+ const issue = (code, message, received, expected, next, path) => {
13
+ issues.push({ code, message, received, expected, next, ...(path === undefined ? {} : { path }) });
14
+ };
15
+ if (!isRecord(raw)) {
16
+ issue('hook_manifest_invalid', 'manifest must be an object', typeName(raw), '{ schemaVersion, hooks }', 'Provide a valid hooks.json manifest.');
17
+ return { issues };
18
+ }
19
+ const unknown = Object.keys(raw).filter((key) => key !== 'schemaVersion' && key !== 'hooks');
20
+ if (unknown.length > 0) {
21
+ issue('hook_manifest_invalid', 'unknown top-level keys', unknown.join(', '), 'only: schemaVersion, hooks', 'Remove the unknown keys.');
22
+ return { issues };
23
+ }
24
+ if (raw['schemaVersion'] !== 1) {
25
+ issue('hook_schema_version', 'schemaVersion must be exactly 1', String(raw['schemaVersion']), '1', 'Update the manifest schema version to 1.', 'schemaVersion');
26
+ return { issues };
27
+ }
28
+ if (!Array.isArray(raw['hooks']) || raw['hooks'].length === 0) {
29
+ issue('hook_manifest_invalid', 'hooks must be a non-empty array', typeName(raw['hooks']), 'a non-empty array of hook declarations', 'Declare at least one hook.', 'hooks');
30
+ return { issues };
31
+ }
32
+ const hooks = [];
33
+ for (let index = 0; index < raw['hooks'].length; index++) {
34
+ const value = raw['hooks'][index];
35
+ const path = `hooks[${index}]`;
36
+ if (!isRecord(value)) {
37
+ issue('hook_manifest_invalid', 'hook must be an object', typeName(value), '{ target, phase, op, description, effects }', 'Fix the hook declaration.', path);
38
+ return { issues };
39
+ }
40
+ const keys = Object.keys(value).filter((key) => !['target', 'phase', 'op', 'description', 'effects'].includes(key));
41
+ if (keys.length > 0) {
42
+ issue('hook_manifest_invalid', 'unknown hook keys', keys.join(', '), 'only: target, phase, op, description, effects', 'Remove the unknown keys.', path);
43
+ return { issues };
44
+ }
45
+ for (const field of ['target', 'op', 'description']) {
46
+ if (typeof value[field] !== 'string' || value[field].length === 0) {
47
+ issue('hook_manifest_invalid', `${field} must be a non-empty string`, typeName(value[field]), 'a non-empty string', `Set ${field}.`, `${path}.${field}`);
48
+ return { issues };
49
+ }
50
+ }
51
+ if (value['phase'] !== 'before' && value['phase'] !== 'after' && value['phase'] !== 'replace') {
52
+ issue('hook_manifest_invalid', 'phase must be before|after|replace', String(value['phase']), 'before | after | replace', 'Set phase to a supported hook phase.', `${path}.phase`);
53
+ return { issues };
54
+ }
55
+ if (!Array.isArray(value['effects']) || value['effects'].length === 0 || !value['effects'].every((effect) => typeof effect === 'string' && effect.length > 0)) {
56
+ issue('hook_manifest_invalid', 'effects must be a non-empty string array', typeName(value['effects']), 'a non-empty array of non-empty strings', 'Declare every persistent effect.', `${path}.effects`);
57
+ return { issues };
58
+ }
59
+ hooks.push({
60
+ target: value['target'],
61
+ phase: value['phase'],
62
+ op: value['op'],
63
+ description: value['description'],
64
+ effects: value['effects'],
65
+ });
66
+ }
67
+ return { manifest: { schemaVersion: 1, hooks }, issues };
68
+ }
@@ -0,0 +1,22 @@
1
+ import type { HookPhase } from '../../../hook-authoring.js';
2
+ import type { Field } from '../../help.js';
3
+ export declare const EXEC_HOOK_TIMEOUT_MS = 15000;
4
+ export interface ExecHookSpec {
5
+ plugin: string;
6
+ /** Absolute, validated hookExecutable path. */
7
+ executable: string;
8
+ op: string;
9
+ phase: HookPhase;
10
+ commandPath: readonly string[];
11
+ /** Required for replacement hooks; ignored for before and after hooks. */
12
+ output?: Field[];
13
+ }
14
+ export interface ExecHookInvocation {
15
+ operationId: string;
16
+ input: Record<string, unknown>;
17
+ providedParams: readonly string[];
18
+ /** The completed primary result, present only for an after hook. */
19
+ result?: Record<string, unknown>;
20
+ }
21
+ /** Invoke one hook executable; after failures retain the completed primary result. */
22
+ export declare function invokeExecHook(spec: ExecHookSpec, invocation: ExecHookInvocation): Promise<Record<string, unknown> | void>;
@@ -0,0 +1,274 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { posixProcessGroupSpawnOptions, terminateProcessGroup } from '../../../hook-process.js';
3
+ import { ExitCode } from '../../../types.js';
4
+ import { isRecord } from '../../../shared/predicates.js';
5
+ import { validateCoreDeclaredResult } from '../../command-plugins/transport/exec-invoke.js';
6
+ import { CrtrError } from '../../errors.js';
7
+ import { diag } from '../../io.js';
8
+ const MAX_STDOUT = 10 * 1024 * 1024;
9
+ export const EXEC_HOOK_TIMEOUT_MS = 15_000;
10
+ const RESERVED_CODES = new Set([
11
+ 'internal',
12
+ 'unknown_path',
13
+ 'command_collision',
14
+ 'command_hook_collision',
15
+ 'plugin_protocol_error',
16
+ 'hook_protocol_error',
17
+ ]);
18
+ const SNAKE = /^[a-z][a-z0-9]*(_[a-z0-9]+)*$/;
19
+ const OPERATION_ID = /^[0-9a-f]{32}$/;
20
+ const NEXT = 'Run `crtr pkg plugin show` or `crtr sys doctor` to inspect the plugin.';
21
+ /** Invoke one hook executable; after failures retain the completed primary result. */
22
+ export async function invokeExecHook(spec, invocation) {
23
+ const cwd = process.cwd();
24
+ const request = buildRequest(spec, invocation, cwd);
25
+ const res = await runHookProcess(spec, invocation, JSON.stringify(request), cwd);
26
+ if (res.signal !== null) {
27
+ throw protocolError(spec, invocation, `executable was killed by signal ${res.signal}`, 'a clean exit with one JSON envelope on stdout');
28
+ }
29
+ const envelope = parseEnvelope(spec, invocation, res.stdout);
30
+ if (envelope.ok && res.status !== 0) {
31
+ diag(`crtr: ${label(spec)} returned ok:true but exited ${res.status} (honoring the envelope)`);
32
+ }
33
+ else if (!envelope.ok && res.status === 0) {
34
+ diag(`crtr: ${label(spec)} returned ok:false but exited 0 (honoring the envelope)`);
35
+ }
36
+ if (!envelope.ok)
37
+ throwHookFailure(spec, invocation, envelope);
38
+ if (spec.phase !== 'replace')
39
+ return;
40
+ const result = envelope.result;
41
+ if (!isRecord(result)) {
42
+ throw protocolError(spec, invocation, 'replacement success result was not an object', 'result: an object matching the target output declaration');
43
+ }
44
+ const output = spec.output;
45
+ if (output === undefined) {
46
+ throw protocolError(spec, invocation, 'replacement target output declaration was missing', 'the target leaf output declaration');
47
+ }
48
+ const issue = validateCoreDeclaredResult(output, result);
49
+ if (issue !== null) {
50
+ const field = output.find((candidate) => candidate.name === issue.field);
51
+ if (issue.receivedType !== undefined) {
52
+ throw protocolError(spec, invocation, `replacement output field "${issue.field}" has the wrong type`, `${issue.field}: ${field?.type ?? 'unknown'}`);
53
+ }
54
+ throw protocolError(spec, invocation, `replacement result is missing required output field "${issue.field}"`, `a "${issue.field}" field of type ${field?.type ?? 'unknown'}`);
55
+ }
56
+ return result;
57
+ }
58
+ async function runHookProcess(spec, invocation, input, cwd) {
59
+ let child;
60
+ try {
61
+ child = spawn(spec.executable, ['--crtr-hook-protocol', '1'], {
62
+ cwd,
63
+ env: process.env,
64
+ stdio: ['pipe', 'pipe', 'inherit'],
65
+ ...posixProcessGroupSpawnOptions(),
66
+ });
67
+ }
68
+ catch (error) {
69
+ throw protocolError(spec, invocation, `executable could not be run (${describeError(error)})`, 'a runnable hook executable');
70
+ }
71
+ return new Promise((resolve, reject) => {
72
+ let stdoutBytes = 0;
73
+ const stdout = [];
74
+ let failure;
75
+ let closed = false;
76
+ let cleaned = false;
77
+ let status = null;
78
+ let signal = null;
79
+ let timeout;
80
+ const finish = () => {
81
+ if (!closed || (failure !== undefined && !cleaned))
82
+ return;
83
+ if (timeout !== undefined)
84
+ clearTimeout(timeout);
85
+ if (failure?.kind === 'timeout') {
86
+ reject(protocolError(spec, invocation, `executable timed out after ${EXEC_HOOK_TIMEOUT_MS}ms`, 'one JSON envelope before the execution timeout'));
87
+ return;
88
+ }
89
+ if (failure?.kind === 'output_limit') {
90
+ reject(protocolError(spec, invocation, 'stdout exceeded the 10 MiB cap', 'one JSON envelope under 10 MiB on stdout'));
91
+ return;
92
+ }
93
+ if (failure?.kind === 'spawn') {
94
+ reject(protocolError(spec, invocation, `executable could not be run (${describeError(failure.error)})`, 'a runnable hook executable'));
95
+ return;
96
+ }
97
+ if (failure?.kind === 'cleanup') {
98
+ reject(protocolError(spec, invocation, `executable cleanup failed (${describeError(failure.error)})`, 'a hook executable whose process group can be terminated'));
99
+ return;
100
+ }
101
+ resolve({ stdout: Buffer.concat(stdout).toString('utf8'), status, signal });
102
+ };
103
+ const terminate = (reason) => {
104
+ if (failure !== undefined)
105
+ return;
106
+ failure = reason;
107
+ if (timeout !== undefined)
108
+ clearTimeout(timeout);
109
+ void terminateProcessGroup(child).then(() => {
110
+ cleaned = true;
111
+ finish();
112
+ }, (error) => {
113
+ failure = { kind: 'cleanup', error };
114
+ cleaned = true;
115
+ finish();
116
+ });
117
+ };
118
+ child.stdout.on('data', (chunk) => {
119
+ stdoutBytes += chunk.byteLength;
120
+ if (stdoutBytes > MAX_STDOUT) {
121
+ terminate({ kind: 'output_limit' });
122
+ return;
123
+ }
124
+ stdout.push(chunk);
125
+ });
126
+ child.once('error', (error) => {
127
+ if (child.pid === undefined) {
128
+ failure = { kind: 'spawn', error };
129
+ closed = true;
130
+ cleaned = true;
131
+ finish();
132
+ return;
133
+ }
134
+ terminate({ kind: 'spawn', error });
135
+ });
136
+ child.once('close', (nextStatus, nextSignal) => {
137
+ status = nextStatus;
138
+ signal = nextSignal;
139
+ closed = true;
140
+ finish();
141
+ });
142
+ timeout = setTimeout(() => terminate({ kind: 'timeout' }), EXEC_HOOK_TIMEOUT_MS);
143
+ child.stdin.end(input);
144
+ });
145
+ }
146
+ function buildRequest(spec, invocation, cwd) {
147
+ if (!OPERATION_ID.test(invocation.operationId)) {
148
+ throw protocolError(spec, invocation, 'operationId was not a 32-character lowercase hexadecimal string', 'a valid crouter operation identity');
149
+ }
150
+ if (!isJson(invocation.input)) {
151
+ throw protocolError(spec, invocation, 'input was not JSON-compatible', 'a JSON object');
152
+ }
153
+ if (!invocation.providedParams.every((param) => typeof param === 'string')) {
154
+ throw protocolError(spec, invocation, 'providedParams contained a non-string value', 'an array of parameter names');
155
+ }
156
+ const base = {
157
+ protocolVersion: 1,
158
+ op: spec.op,
159
+ command: [...spec.commandPath],
160
+ operationId: invocation.operationId,
161
+ input: invocation.input,
162
+ context: { cwd, providedParams: [...invocation.providedParams] },
163
+ };
164
+ if (spec.phase === 'after') {
165
+ if (invocation.result === undefined || !isJson(invocation.result)) {
166
+ throw protocolError(spec, invocation, 'after hook result was missing or not JSON-compatible', 'the completed primary result');
167
+ }
168
+ return { ...base, phase: 'after', result: invocation.result };
169
+ }
170
+ if (invocation.result !== undefined) {
171
+ throw protocolError(spec, invocation, `${spec.phase} hook invocation included a result`, 'result only for after hooks');
172
+ }
173
+ return { ...base, phase: spec.phase };
174
+ }
175
+ function parseEnvelope(spec, invocation, stdout) {
176
+ let value;
177
+ try {
178
+ value = JSON.parse(stdout.trim());
179
+ }
180
+ catch {
181
+ throw protocolError(spec, invocation, 'stdout was not a single JSON envelope', 'exactly one JSON envelope on stdout');
182
+ }
183
+ if (!isRecord(value)) {
184
+ throw protocolError(spec, invocation, 'stdout was not a JSON object', 'a { protocolVersion: 1, ok: boolean, ... } envelope');
185
+ }
186
+ if (value['protocolVersion'] !== 1) {
187
+ throw protocolError(spec, invocation, `unsupported protocolVersion ${String(value['protocolVersion'])}`, 'protocolVersion exactly 1');
188
+ }
189
+ if (value['ok'] === true) {
190
+ const expectedKeys = spec.phase === 'replace' ? ['protocolVersion', 'ok', 'result'] : ['protocolVersion', 'ok'];
191
+ if (!hasExactKeys(value, expectedKeys)) {
192
+ throw protocolError(spec, invocation, 'success envelope had missing or extra fields', `{ ${expectedKeys.join(', ')} } only`);
193
+ }
194
+ if (spec.phase === 'replace') {
195
+ if (!isJson(value['result'])) {
196
+ throw protocolError(spec, invocation, 'replacement result was not JSON-compatible', 'a JSON result');
197
+ }
198
+ return { protocolVersion: 1, ok: true, result: value['result'] };
199
+ }
200
+ return { protocolVersion: 1, ok: true };
201
+ }
202
+ if (value['ok'] !== false || !hasExactKeys(value, ['protocolVersion', 'ok', 'error'])) {
203
+ throw protocolError(spec, invocation, 'failure envelope had missing or extra fields', '{ protocolVersion, ok, error } only');
204
+ }
205
+ const error = value['error'];
206
+ if (!isRecord(error) || !hasExactKeys(error, ['code', 'message'], ['field', 'next'])) {
207
+ throw protocolError(spec, invocation, 'error envelope was missing fields or contained extra fields', 'error: { code, message, field?, next? }');
208
+ }
209
+ if (typeof error['code'] !== 'string' || error['code'] === '' || typeof error['message'] !== 'string' || error['message'] === '') {
210
+ throw protocolError(spec, invocation, 'error envelope had an empty or non-string code/message', 'non-empty error code and message strings');
211
+ }
212
+ if ((error['field'] !== undefined && typeof error['field'] !== 'string') || (error['next'] !== undefined && typeof error['next'] !== 'string')) {
213
+ throw protocolError(spec, invocation, 'error envelope had a non-string field/next', 'string field and next values when present');
214
+ }
215
+ return {
216
+ protocolVersion: 1,
217
+ ok: false,
218
+ error: {
219
+ code: error['code'],
220
+ message: error['message'],
221
+ ...(error['field'] === undefined ? {} : { field: error['field'] }),
222
+ ...(error['next'] === undefined ? {} : { next: error['next'] }),
223
+ },
224
+ };
225
+ }
226
+ function throwHookFailure(spec, invocation, failure) {
227
+ const { code, message, field, next } = failure.error;
228
+ if (!SNAKE.test(code) || RESERVED_CODES.has(code)) {
229
+ throw protocolError(spec, invocation, `error code "${code}" was invalid or reserved`, 'a non-reserved lowercase snake_case code');
230
+ }
231
+ throw new CrtrError(code, `${label(spec)}: ${message}`, ExitCode.GENERAL, failureDetails(spec, invocation, field, next));
232
+ }
233
+ function protocolError(spec, invocation, received, expected) {
234
+ return new CrtrError('hook_protocol_error', `${label(spec)}: ${received}. Expected ${expected}.`, ExitCode.GENERAL, failureDetails(spec, invocation, undefined, NEXT, received));
235
+ }
236
+ function failureDetails(spec, invocation, field, next, received) {
237
+ const identity = {
238
+ plugin: spec.plugin,
239
+ op: spec.op,
240
+ phase: spec.phase,
241
+ command: [...spec.commandPath],
242
+ ...(received === undefined ? {} : { received }),
243
+ ...(field === undefined ? {} : { field }),
244
+ };
245
+ if (spec.phase !== 'after')
246
+ return { ...identity, next: next ?? NEXT };
247
+ const guidance = 'Do not retry the original command; its primary action completed. Inspect and reconcile the failed after hook.';
248
+ return {
249
+ ...identity,
250
+ primaryCompleted: true,
251
+ result: invocation.result,
252
+ next: next === undefined || next === NEXT ? guidance : `${next} ${guidance}`,
253
+ };
254
+ }
255
+ function label(spec) {
256
+ return `plugin "${spec.plugin}" ${spec.phase} hook "${spec.op}" for \`${spec.commandPath.join(' ')}\``;
257
+ }
258
+ function hasExactKeys(value, required, optional = []) {
259
+ const keys = Object.keys(value);
260
+ return required.every((key) => Object.hasOwn(value, key))
261
+ && keys.every((key) => required.includes(key) || optional.includes(key));
262
+ }
263
+ function isJson(value) {
264
+ if (value === null || typeof value === 'boolean' || typeof value === 'string')
265
+ return true;
266
+ if (typeof value === 'number')
267
+ return Number.isFinite(value);
268
+ if (Array.isArray(value))
269
+ return value.every(isJson);
270
+ return isRecord(value) && Object.values(value).every(isJson);
271
+ }
272
+ function describeError(error) {
273
+ return error instanceof Error ? error.message : String(error);
274
+ }
@@ -0,0 +1,2 @@
1
+ /** Whether an enabled command plugin survives nearest-scope name shadowing. */
2
+ export declare function hasEffectiveCommandPlugins(startDir?: string, profileId?: string | null): boolean;
@@ -0,0 +1,17 @@
1
+ import { listInstalledPlugins, listInstalledPluginsInRoot } from '../resolver.js';
2
+ import { projectScopeRoots } from '../scope.js';
3
+ /** Whether an enabled command plugin survives nearest-scope name shadowing. */
4
+ export function hasEffectiveCommandPlugins(startDir = process.cwd(), profileId) {
5
+ const seen = new Set();
6
+ const consider = (plugin) => {
7
+ if (seen.has(plugin.name))
8
+ return false;
9
+ seen.add(plugin.name);
10
+ return plugin.enabled && (plugin.manifest.commands !== undefined || plugin.manifest.transport !== undefined);
11
+ };
12
+ for (const root of projectScopeRoots(startDir, profileId)) {
13
+ if (listInstalledPluginsInRoot('project', root).some(consider))
14
+ return true;
15
+ }
16
+ return listInstalledPlugins('user').some(consider);
17
+ }
@@ -16,6 +16,11 @@ export declare function validateDeclaredResult(output: Field[], result: Record<s
16
16
  field: string;
17
17
  receivedType?: string;
18
18
  } | null;
19
+ /** Core leaf declarations may explicitly require a nullable field. */
20
+ export declare function validateCoreDeclaredResult(output: Field[], result: Record<string, unknown>): {
21
+ field: string;
22
+ receivedType?: string;
23
+ } | null;
19
24
  /** Execute one external leaf and return its result object (crtr renders it via
20
25
  * the generic renderer; `--json` mirrors it). Throws a CrtrError for any
21
26
  * protocol violation or the plugin's own recoverable error. */
@@ -21,13 +21,23 @@ const NEXT = 'Run `crtr pkg plugin show` or `crtr sys doctor` to inspect the plu
21
21
  * Returns null if valid; otherwise returns a { field, receivedType } error.
22
22
  * Transport-neutral: used by exec and HTTP transport adapters. */
23
23
  export function validateDeclaredResult(output, result) {
24
+ return validateFields(output, result, false);
25
+ }
26
+ /** Core leaf declarations may explicitly require a nullable field. */
27
+ export function validateCoreDeclaredResult(output, result) {
28
+ return validateFields(output, result, true);
29
+ }
30
+ function validateFields(output, result, honorDeclaredNull) {
24
31
  for (const f of output) {
25
- const present = Object.prototype.hasOwnProperty.call(result, f.name) && result[f.name] !== undefined && result[f.name] !== null;
26
- if (f.required && !present) {
32
+ const value = result[f.name];
33
+ const present = Object.prototype.hasOwnProperty.call(result, f.name)
34
+ && value !== undefined
35
+ && (honorDeclaredNull || value !== null);
36
+ if (f.required && !present)
27
37
  return { field: f.name };
28
- }
29
- if (present && !typeMatches(f.type, result[f.name])) {
30
- return { field: f.name, receivedType: typeof result[f.name] };
38
+ const matches = honorDeclaredNull ? coreTypeMatches(f.type, value) : typeMatches(f.type, value);
39
+ if (present && !matches) {
40
+ return { field: f.name, receivedType: value === null ? 'null' : typeof value };
31
41
  }
32
42
  }
33
43
  return null;
@@ -150,6 +160,49 @@ function typeMatches(type, v) {
150
160
  return isRecord(v);
151
161
  return true;
152
162
  }
163
+ function coreTypeMatches(type, v) {
164
+ const alternatives = type.split('|').map((part) => part.trim()).filter(Boolean);
165
+ if (alternatives.length === 1 && !knownType(alternatives[0]))
166
+ return true;
167
+ const allKnown = alternatives.every((part) => knownType(part) || /^[a-z][a-z0-9_-]*$/.test(part));
168
+ if (!allKnown)
169
+ return true;
170
+ return alternatives.some((part) => atomMatches(part, v));
171
+ }
172
+ function knownType(type) {
173
+ const t = type.toLowerCase();
174
+ return t === 'null'
175
+ || t === 'string'
176
+ || t === 'path'
177
+ || t === 'markdown'
178
+ || t === 'int'
179
+ || t === 'integer'
180
+ || t === 'number'
181
+ || t === 'float'
182
+ || t === 'bool'
183
+ || t === 'boolean'
184
+ || t === 'array'
185
+ || t.endsWith('[]')
186
+ || t === 'object';
187
+ }
188
+ function atomMatches(type, v) {
189
+ const t = type.toLowerCase();
190
+ if (t === 'null')
191
+ return v === null;
192
+ if (t === 'string' || t === 'path' || t === 'markdown')
193
+ return typeof v === 'string';
194
+ if (t === 'int' || t === 'integer')
195
+ return typeof v === 'number' && Number.isInteger(v);
196
+ if (t === 'number' || t === 'float')
197
+ return typeof v === 'number';
198
+ if (t === 'bool' || t === 'boolean')
199
+ return typeof v === 'boolean';
200
+ if (t === 'array' || t.endsWith('[]'))
201
+ return Array.isArray(v);
202
+ if (t === 'object')
203
+ return isRecord(v);
204
+ return typeof v === 'string' && v === type;
205
+ }
153
206
  function protocolError(spec, received, expected) {
154
207
  return new CrtrError('plugin_protocol_error', `${label(spec)}: ${received}. Expected ${expected}.`, ExitCode.GENERAL, { received, next: NEXT });
155
208
  }
@@ -1,4 +1,4 @@
1
- import type { RootHelp, RootEntry, BranchHelp, LeafHelp, InputParam, SubTier } from './help.js';
1
+ import type { RootHelp, RootEntry, BranchHelp, LeafHelp, EffectiveLeafHelp, InputParam, SubTier } from './help.js';
2
2
  import { CrtrError } from './errors.js';
3
3
  /** Runtime context passed to a leaf's run function. Includes metadata about
4
4
  * which parameters were explicitly provided vs. defaulted. Used by configured
@@ -47,6 +47,11 @@ export interface LeafDef {
47
47
  * 'hidden' keeps an internal leaf out of every listing. */
48
48
  tier?: SubTier;
49
49
  help: LeafHelp;
50
+ /** Invocation-local immutable help view. Present only on hook-eligible core
51
+ * leaves; static discovery may change Effects but never executes hooks. */
52
+ effectiveHelp?: () => Promise<EffectiveLeafHelp>;
53
+ /** Opt this conditionally-void leaf out of core command hooks. */
54
+ hookEligible?: false;
50
55
  /** Opt into editor slash-command exposure (see SlashSpec). */
51
56
  slash?: SlashSpec;
52
57
  run: (input: Record<string, unknown>, context?: LeafRunContext) => Promise<Record<string, unknown> | void>;
@@ -100,6 +105,8 @@ export declare function defineLeaf(opts: {
100
105
  tier?: SubTier;
101
106
  help: LeafHelp;
102
107
  slash?: SlashSpec;
108
+ /** Opt this conditionally-void leaf out of core command hooks. */
109
+ hookEligible?: false;
103
110
  run: (input: Record<string, unknown>, context?: LeafRunContext) => Promise<Record<string, unknown> | void>;
104
111
  render?: (result: Record<string, unknown>) => string;
105
112
  }): LeafDef;
@@ -34,6 +34,7 @@ export function defineLeaf(opts) {
34
34
  tier: opts.tier,
35
35
  help: freezeCommandMetadata(opts.help),
36
36
  slash: opts.slash === undefined ? undefined : freezeCommandMetadata(opts.slash),
37
+ hookEligible: opts.hookEligible,
37
38
  run: opts.run,
38
39
  render: opts.render,
39
40
  });
@@ -187,19 +188,20 @@ function renderNode(node) {
187
188
  return renderBranch(node.help);
188
189
  return renderLeafArgv(node.help);
189
190
  }
190
- /** Render a node's help plus any plugin help addenda targeting its walked
191
- * command path — append-only, attributed blocks a plugin adds beneath core
192
- * contract text, never inside it. Plugin discovery loads only here (dynamic
193
- * import on the help path), so the dispatch path's module graph and cold
194
- * start are unchanged; the lookup reads stored manifest bytes only. The
195
- * lookup receives the install gate's exact strict-validation inputs (reserved
196
- * core names + the full core command path set, both from build-root), so a
197
- * manifest rejected at ingress contributes nothing here either — loading
198
- * every subtree for that path set is a help-path-only cost. */
191
+ /** Render effective Effects for a hook-eligible core leaf, then append any
192
+ * commands.json helpAddenda beneath the contract. Both lookups read stored
193
+ * manifest bytes only. Addenda validation receives the install gate's exact
194
+ * reserved core names and full core path set, so a manifest rejected at
195
+ * ingress contributes nothing here either. */
199
196
  async function renderNodeWithAddenda(node, path) {
200
- const body = renderNode(node);
197
+ const body = node.kind === 'leaf' && node.effectiveHelp !== undefined
198
+ ? renderLeafArgv(await node.effectiveHelp())
199
+ : renderNode(node);
201
200
  if (path.length === 0)
202
201
  return body;
202
+ const { hasEffectiveCommandPlugins } = await import('./command-plugins/presence.js');
203
+ if (!hasEffectiveCommandPlugins())
204
+ return body;
203
205
  const [{ collectHelpAddenda }, { SUBTREE_NAMES, coreCommandPaths }] = await Promise.all([
204
206
  import('./command-plugins/help-addenda.js'),
205
207
  import('../build-root.js'),
@@ -1,4 +1,4 @@
1
- import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
1
+ import type { PageComponentRegistration, Scope, ScopeConfig, ScopeState, HumanActionConfig, KindConfig, ModelLaddersConfig, ModelProvider, ModelStrength, ModelRouteConfig, ModelRoutingConfig } from '../types.js';
2
2
  import { type BindingId } from './keybindings/catalog.js';
3
3
  export declare function configPath(scope: Scope): string | null;
4
4
  export declare function statePath(scope: Scope): string | null;
@@ -40,6 +40,18 @@ export declare function binDeclarationIssues(raw: unknown): BinDeclarationIssue[
40
40
  * manifest's) to name → root-relative path. Invalid entries never become
41
41
  * commands; `binDeclarationIssues` keeps them visible to Doctor. */
42
42
  export declare function normalizeBin(raw: unknown): Record<string, string>;
43
+ export interface HumanActionDeclarationIssue {
44
+ name: string;
45
+ reason: string;
46
+ block?: true;
47
+ }
48
+ /** Validate `humanActions` declaration shape without touching the filesystem.
49
+ * Filesystem checks happen only when resolving an action or running Doctor. */
50
+ export declare function humanActionDeclarationIssues(raw: unknown): HumanActionDeclarationIssue[];
51
+ /** Normalize valid action declarations for scope-local config inspection.
52
+ * Invalid entries remain available through raw reads so resolution and Doctor
53
+ * can report them rather than silently treating them as absent. */
54
+ export declare function normalizeHumanActions(raw: unknown): Record<string, HumanActionConfig>;
43
55
  /** STRICT install-time validation for a plugin-declared `bin` block. Target
44
56
  * filesystem state is checked later by Doctor because it can change after
45
57
  * install. */