@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,41 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { claimActionDelivery, dueActionDeliveries } from '../../core/canvas/human-deliveries.js';
3
+ import { emitEvent } from '../../core/events/emit.js';
4
+ import { operationIdContext } from '../../core/events/operation-id.js';
5
+ import { deliverAction } from '../human/deliver-action.js';
6
+ /** One daemon-life identity for guarded action-delivery claims. */
7
+ export const humanDeliveryDaemonInstanceId = randomUUID();
8
+ /** Admit due action deliveries without making the daemon's serial tick await subprocesses. */
9
+ export class HumanDeliveryLaneReconciler {
10
+ run(now, ctx) {
11
+ if (!ctx.lifecycle.acceptsDetachedWork())
12
+ return;
13
+ let due;
14
+ try {
15
+ due = dueActionDeliveries(now);
16
+ }
17
+ catch (error) {
18
+ operationIdContext.fresh(() => {
19
+ emitEvent({ level: 'error', event: 'human.action_delivery.scan_failed', error });
20
+ });
21
+ return;
22
+ }
23
+ for (const candidate of due) {
24
+ try {
25
+ const claimed = claimActionDelivery(candidate.requestId, now, humanDeliveryDaemonInstanceId);
26
+ if (claimed === null)
27
+ continue;
28
+ ctx.lifecycle.registerDetached(deliverAction(claimed, humanDeliveryDaemonInstanceId).catch((error) => {
29
+ operationIdContext.fresh(() => {
30
+ emitEvent({ level: 'error', event: 'human.action_delivery.failed', error, fields: { request_id: claimed.requestId } });
31
+ });
32
+ }));
33
+ }
34
+ catch (error) {
35
+ operationIdContext.fresh(() => {
36
+ emitEvent({ level: 'error', event: 'human.action_delivery.claim_failed', error, fields: { request_id: candidate.requestId } });
37
+ });
38
+ }
39
+ }
40
+ }
41
+ }
@@ -40,8 +40,13 @@ export declare function completeRequestedSubmit(reviewId: string, opts: {
40
40
  /** The companion's turn ended: complete any submit the human left waiting on
41
41
  * it. Never throws — a settlement directive must not depend on delivery. */
42
42
  export declare function completeCompanionRequestedSubmit(companionNodeId: string): Promise<void>;
43
- /** Cancel an open review once; ticket cancellation remains owned by the retained human-ticket finisher. */
44
- export declare function cancelReview(reviewId: string, args: {
43
+ /** `disposition` rides through to the ticket result: the recipient closing a
44
+ * review in their inbox is a dismissal, while the requester withdrawing it
45
+ * stays a cancellation. */
46
+ export interface ReviewCancellationArgs {
45
47
  reason?: string;
46
48
  actor?: string;
47
- }): Promise<ReviewCancellationFinishOutcome>;
49
+ disposition?: 'canceled' | 'dismissed';
50
+ }
51
+ /** Cancel an open review once; ticket cancellation remains owned by the retained human-ticket finisher. */
52
+ export declare function cancelReview(reviewId: string, args: ReviewCancellationArgs): Promise<ReviewCancellationFinishOutcome>;
@@ -9,6 +9,7 @@ import { readReviewSource } from '../../core/review/document.js';
9
9
  import { signalReviewActivity } from '../../core/review/signal.js';
10
10
  import { approveReviewLocked, cancelReviewRow, getReview, getReviewByCompanion, markDelivered, markDeliveryError, markProjected, markProjectionError, requestReviewSubmit, requireVisibleReview, } from '../../core/review/store.js';
11
11
  import { ReviewOperationError } from '../../core/review/types.js';
12
+ import { cancelPendingEntries } from '../../core/feed/inbox.js';
12
13
  import { emitEvent } from '../../core/events/emit.js';
13
14
  import { retireCompanion } from '../companion-retire.js';
14
15
  import { deliverNodeMessage } from '../messaging/node-message.js';
@@ -68,6 +69,7 @@ async function serializedFinish(reviewId, work) {
68
69
  }
69
70
  }
70
71
  async function finishApproved(review, result) {
72
+ retractSubmitQueuedNote(review);
71
73
  let current = requireVisibleReview(review.review_id);
72
74
  if (current.projected_at === null) {
73
75
  try {
@@ -93,6 +95,7 @@ async function finishApproved(review, result) {
93
95
  return requireVisibleReview(review.review_id);
94
96
  }
95
97
  async function finishCanceled(review, winningWrite, args) {
98
+ retractSubmitQueuedNote(review);
96
99
  let current = requireVisibleReview(review.review_id);
97
100
  let canceledTicket;
98
101
  if (current.origin_kind === 'inline') {
@@ -153,6 +156,21 @@ async function finishCanceled(review, winningWrite, args) {
153
156
  await retireCompanion(current.companion_node_id, 'canceled');
154
157
  return { record: requireVisibleReview(review.review_id), ...(canceledTicket === undefined ? {} : { ticket_result: canceledTicket }) };
155
158
  }
159
+ const SUBMIT_QUEUED_NOTE = 'review-submit-queued';
160
+ /** Retract an undelivered queued-submit note once the review is settling. The
161
+ * note rides the deferred tier and so does not wake the origin, while the
162
+ * terminal result goes live — without this the origin reads "result to
163
+ * follow" only after it already acted on that result. */
164
+ function retractSubmitQueuedNote(review) {
165
+ try {
166
+ cancelPendingEntries(review.origin_node_id, (entry) => entry.kind === 'message'
167
+ && entry.data?.['note'] === SUBMIT_QUEUED_NOTE
168
+ && entry.data?.['review_id'] === review.review_id);
169
+ }
170
+ catch (error) {
171
+ emitEvent({ level: 'warn', event: 'review.submit_queued_note.retract_failed', error });
172
+ }
173
+ }
156
174
  /** Tell the origin its review was submitted but is finishing behind the
157
175
  * companion. Deliberately deferred: the origin is told nothing is needed of
158
176
  * it, so waking it to say so would be noise. The approval itself wakes it. */
@@ -163,7 +181,7 @@ async function noteSubmitQueued(review) {
163
181
  from: 'crtrd',
164
182
  mode: 'quiet',
165
183
  label: 'human review submitted — result to follow',
166
- data: { review_id: review.review_id },
184
+ data: { review_id: review.review_id, note: SUBMIT_QUEUED_NOTE },
167
185
  runtime: { kind: 'review-queued', facts: {} },
168
186
  body: `The human submitted their review of \`${review.file}\`; the document is still being edited. `
169
187
  + "You'll be notified with the result once the review completes. Nothing is needed from you now.",
@@ -0,0 +1,75 @@
1
+ export type HookJson = null | boolean | number | string | HookJson[] | {
2
+ [key: string]: HookJson;
3
+ };
4
+ export type HookPhase = 'before' | 'after' | 'replace';
5
+ export interface HookContext {
6
+ cwd: string;
7
+ providedParams: string[];
8
+ }
9
+ interface HookRequestBase {
10
+ protocolVersion: 1;
11
+ op: string;
12
+ command: string[];
13
+ operationId: string;
14
+ input: {
15
+ [key: string]: HookJson;
16
+ };
17
+ context: HookContext;
18
+ }
19
+ export interface BeforeHookRequest extends HookRequestBase {
20
+ phase: 'before';
21
+ }
22
+ export interface AfterHookRequest extends HookRequestBase {
23
+ phase: 'after';
24
+ result: HookJson;
25
+ }
26
+ export interface ReplaceHookRequest extends HookRequestBase {
27
+ phase: 'replace';
28
+ }
29
+ export type HookRequest = BeforeHookRequest | AfterHookRequest | ReplaceHookRequest;
30
+ export interface HookSuccess {
31
+ protocolVersion: 1;
32
+ ok: true;
33
+ result?: HookJson;
34
+ }
35
+ export interface HookError {
36
+ code: string;
37
+ message: string;
38
+ field?: string;
39
+ next?: string;
40
+ }
41
+ export interface HookFailure {
42
+ protocolVersion: 1;
43
+ ok: false;
44
+ error: HookError;
45
+ }
46
+ export type HookResponse = HookSuccess | HookFailure;
47
+ export type HookHandler = (request: HookRequest) => HookResponse | Promise<HookResponse>;
48
+ export type HookHandlers = Record<string, HookHandler>;
49
+ export interface RunHookExecutableOptions {
50
+ stdin?: NodeJS.ReadableStream;
51
+ stdout?: NodeJS.WritableStream;
52
+ stderr?: NodeJS.WritableStream;
53
+ }
54
+ export interface LocalHookProcessOptions {
55
+ timeoutMs: number;
56
+ cwd?: string;
57
+ env?: NodeJS.ProcessEnv;
58
+ input?: string | Uint8Array;
59
+ }
60
+ export interface LocalHookProcessSuccess {
61
+ ok: true;
62
+ stdout: string;
63
+ }
64
+ export type LocalHookProcessResult = LocalHookProcessSuccess | HookFailure;
65
+ export interface ForwardHookOptions {
66
+ headers?: HeadersInit;
67
+ bearer?: string;
68
+ timeoutMs: number;
69
+ maxResponseBytes: number;
70
+ }
71
+ export declare function hookError(code: string, message: string, details?: Omit<HookError, 'code' | 'message'>): HookFailure;
72
+ export declare function runHookExecutable(handlers: HookHandlers, options?: RunHookExecutableOptions): Promise<void>;
73
+ export declare function runLocalHookProcess(argv: readonly string[], options: LocalHookProcessOptions): Promise<LocalHookProcessResult>;
74
+ export declare function forwardHook(endpoint: string | URL, request: HookRequest, options: ForwardHookOptions): Promise<HookResponse>;
75
+ export {};
@@ -0,0 +1,358 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { Console } from 'node:console';
3
+ import { posixProcessGroupSpawnOptions, terminateProcessGroup } from './hook-process.js';
4
+ export function hookError(code, message, details = {}) {
5
+ return { protocolVersion: 1, ok: false, error: { code, message, ...details } };
6
+ }
7
+ export async function runHookExecutable(handlers, options = {}) {
8
+ const stdin = options.stdin ?? process.stdin;
9
+ const stdout = options.stdout ?? process.stdout;
10
+ const stderr = options.stderr ?? process.stderr;
11
+ let response;
12
+ try {
13
+ const raw = await readStream(stdin);
14
+ let value;
15
+ try {
16
+ value = JSON.parse(raw);
17
+ }
18
+ catch {
19
+ response = hookError('invalid_hook_request', 'stdin must contain one JSON hook request');
20
+ writeResponse(stdout, response);
21
+ return;
22
+ }
23
+ const request = validateRequest(value);
24
+ if (!request.valid) {
25
+ response = hookError('invalid_hook_request', request.message);
26
+ writeResponse(stdout, response);
27
+ return;
28
+ }
29
+ const handler = handlers[request.value.op];
30
+ if (handler === undefined) {
31
+ response = hookError('unsupported_hook_op', `no handler is registered for ${request.value.op}`);
32
+ writeResponse(stdout, response);
33
+ return;
34
+ }
35
+ try {
36
+ response = await withConsoleStdoutOnStderr(stderr, () => handler(request.value));
37
+ }
38
+ catch (error) {
39
+ stderr.write(`hook handler ${request.value.op} failed: ${describeError(error)}\n`);
40
+ response = hookError('hook_handler_failed', `handler failed for ${request.value.op}`);
41
+ }
42
+ const validated = validateResponse(response, request.value);
43
+ if (!validated.valid) {
44
+ stderr.write(`hook handler ${request.value.op} returned an invalid response: ${validated.message}\n`);
45
+ response = hookError('invalid_hook_response', validated.message);
46
+ }
47
+ }
48
+ catch (error) {
49
+ stderr.write(`hook executable failed: ${describeError(error)}\n`);
50
+ response = hookError('hook_executable_failed', 'hook executable could not read its request');
51
+ }
52
+ writeResponse(stdout, response);
53
+ }
54
+ export async function runLocalHookProcess(argv, options) {
55
+ const executable = argv[0];
56
+ if (executable === undefined || executable === '') {
57
+ return hookError('local_process_invalid', 'local process argv[0] must not be empty');
58
+ }
59
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
60
+ return hookError('local_process_invalid_options', 'timeoutMs must be greater than zero');
61
+ }
62
+ return new Promise((resolve) => {
63
+ let settled = false;
64
+ let timedOut = false;
65
+ let closed = false;
66
+ let cleanupComplete = false;
67
+ let timeout;
68
+ const stdout = [];
69
+ const settle = (result) => {
70
+ if (settled)
71
+ return;
72
+ settled = true;
73
+ if (timeout !== undefined)
74
+ clearTimeout(timeout);
75
+ resolve(result);
76
+ };
77
+ let child;
78
+ try {
79
+ child = spawn(executable, argv.slice(1), {
80
+ cwd: options.cwd,
81
+ env: options.env,
82
+ shell: false,
83
+ stdio: [options.input === undefined ? 'ignore' : 'pipe', 'pipe', 'inherit'],
84
+ ...posixProcessGroupSpawnOptions(),
85
+ });
86
+ }
87
+ catch (error) {
88
+ settle(hookError('local_process_spawn_failed', `could not start ${executable}: ${describeError(error)}`));
89
+ return;
90
+ }
91
+ const settleTimeout = () => {
92
+ if (timedOut && closed && cleanupComplete) {
93
+ settle(hookError('local_process_timeout', `${executable} did not finish within ${options.timeoutMs}ms`));
94
+ }
95
+ };
96
+ timeout = setTimeout(() => {
97
+ timedOut = true;
98
+ void terminateProcessGroup(child).then(() => {
99
+ cleanupComplete = true;
100
+ settleTimeout();
101
+ }, (error) => {
102
+ cleanupComplete = true;
103
+ settle(hookError('local_process_cleanup_failed', `could not stop ${executable}: ${describeError(error)}`));
104
+ });
105
+ }, options.timeoutMs);
106
+ child.stdout.on('data', (chunk) => stdout.push(chunk));
107
+ child.once('error', (error) => {
108
+ if (child.pid !== undefined)
109
+ return;
110
+ settle(hookError('local_process_spawn_failed', `could not start ${executable}: ${describeError(error)}`));
111
+ });
112
+ child.once('close', (code, signal) => {
113
+ closed = true;
114
+ if (timedOut) {
115
+ settleTimeout();
116
+ return;
117
+ }
118
+ if (code === 0) {
119
+ settle({ ok: true, stdout: Buffer.concat(stdout).toString('utf8') });
120
+ return;
121
+ }
122
+ const status = signal === null ? `exit code ${code ?? 'unknown'}` : `signal ${signal}`;
123
+ settle(hookError('local_process_failed', `${executable} exited with ${status}`));
124
+ });
125
+ if (options.input !== undefined && child.stdin !== null) {
126
+ child.stdin.end(options.input);
127
+ }
128
+ });
129
+ }
130
+ export async function forwardHook(endpoint, request, options) {
131
+ if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
132
+ return hookError('hook_forward_invalid_options', 'timeoutMs must be greater than zero');
133
+ }
134
+ if (!Number.isSafeInteger(options.maxResponseBytes) || options.maxResponseBytes <= 0) {
135
+ return hookError('hook_forward_invalid_options', 'maxResponseBytes must be a positive integer');
136
+ }
137
+ const headers = new Headers(options.headers);
138
+ if (!headers.has('content-type'))
139
+ headers.set('content-type', 'application/json');
140
+ if (options.bearer !== undefined)
141
+ headers.set('authorization', `Bearer ${options.bearer}`);
142
+ const controller = new AbortController();
143
+ let timedOut = false;
144
+ const timeout = setTimeout(() => {
145
+ timedOut = true;
146
+ controller.abort();
147
+ }, options.timeoutMs);
148
+ try {
149
+ const response = await fetch(endpoint, {
150
+ method: 'POST',
151
+ headers,
152
+ body: JSON.stringify(request),
153
+ signal: controller.signal,
154
+ });
155
+ const body = await readResponseBody(response, options.maxResponseBytes);
156
+ let value;
157
+ try {
158
+ value = JSON.parse(body);
159
+ }
160
+ catch {
161
+ return hookError('hook_forward_invalid_response', `hook endpoint returned invalid JSON (HTTP ${response.status})`);
162
+ }
163
+ // A valid protocol envelope is authoritative regardless of HTTP status.
164
+ const validated = validateResponse(value, request);
165
+ if (!validated.valid)
166
+ return hookError('hook_forward_invalid_response', `${validated.message} (HTTP ${response.status})`);
167
+ return validated.value;
168
+ }
169
+ catch (error) {
170
+ if (timedOut)
171
+ return hookError('hook_forward_timeout', `hook endpoint did not respond within ${options.timeoutMs}ms`);
172
+ if (error instanceof ResponseTooLargeError) {
173
+ return hookError('hook_forward_response_too_large', `hook endpoint exceeded ${options.maxResponseBytes} response bytes`);
174
+ }
175
+ return hookError('hook_forward_failed', `hook endpoint request failed: ${describeError(error)}`);
176
+ }
177
+ finally {
178
+ clearTimeout(timeout);
179
+ }
180
+ }
181
+ const STDOUT_CONSOLE_METHODS = ['log', 'info', 'debug', 'dir', 'table', 'group', 'groupCollapsed'];
182
+ async function withConsoleStdoutOnStderr(stderr, run) {
183
+ const redirected = new Console({ stdout: stderr, stderr, colorMode: false });
184
+ const original = new Map();
185
+ for (const method of STDOUT_CONSOLE_METHODS) {
186
+ original.set(method, console[method]);
187
+ console[method] = redirected[method].bind(redirected);
188
+ }
189
+ try {
190
+ return await run();
191
+ }
192
+ finally {
193
+ for (const method of STDOUT_CONSOLE_METHODS)
194
+ console[method] = original.get(method);
195
+ }
196
+ }
197
+ function writeResponse(stdout, response) {
198
+ stdout.write(`${JSON.stringify(response)}\n`);
199
+ }
200
+ async function readStream(stream) {
201
+ const chunks = [];
202
+ for await (const chunk of stream)
203
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
204
+ return Buffer.concat(chunks).toString('utf8');
205
+ }
206
+ async function readResponseBody(response, maxBytes) {
207
+ if (response.body === null)
208
+ return '';
209
+ const reader = response.body.getReader();
210
+ const chunks = [];
211
+ let total = 0;
212
+ try {
213
+ for (;;) {
214
+ const { done, value } = await reader.read();
215
+ if (done)
216
+ break;
217
+ total += value.byteLength;
218
+ if (total > maxBytes) {
219
+ await reader.cancel();
220
+ throw new ResponseTooLargeError();
221
+ }
222
+ chunks.push(value);
223
+ }
224
+ }
225
+ finally {
226
+ reader.releaseLock();
227
+ }
228
+ return new TextDecoder().decode(concatBytes(chunks, total));
229
+ }
230
+ function concatBytes(chunks, total) {
231
+ const result = new Uint8Array(total);
232
+ let offset = 0;
233
+ for (const chunk of chunks) {
234
+ result.set(chunk, offset);
235
+ offset += chunk.byteLength;
236
+ }
237
+ return result;
238
+ }
239
+ function validateRequest(value) {
240
+ if (!isRecord(value))
241
+ return invalid('request must be an object');
242
+ if (!hasOnlyKeys(value, ['protocolVersion', 'op', 'phase', 'command', 'operationId', 'input', 'context', 'result'])) {
243
+ return invalid('request contains unknown fields');
244
+ }
245
+ if (value['protocolVersion'] !== 1)
246
+ return invalid('unsupported hook protocol version');
247
+ if (typeof value['op'] !== 'string' || value['op'] === '')
248
+ return invalid('op must be a non-empty string');
249
+ if (!isStringArray(value['command']) || value['command'].length === 0)
250
+ return invalid('command must be a non-empty string array');
251
+ if (typeof value['operationId'] !== 'string' || !/^[0-9a-f]{32}$/.test(value['operationId'])) {
252
+ return invalid('operationId must be a 32-character lowercase hexadecimal string');
253
+ }
254
+ if (!isRecord(value['input']) || !isJson(value['input']))
255
+ return invalid('input must be a JSON object');
256
+ if (!isContext(value['context']))
257
+ return invalid('context must contain cwd and providedParams');
258
+ if (value['phase'] === 'after') {
259
+ if (!Object.hasOwn(value, 'result') || !isJson(value['result']))
260
+ return invalid('after requests require a JSON result');
261
+ return valid({
262
+ protocolVersion: 1,
263
+ op: value['op'],
264
+ phase: 'after',
265
+ command: value['command'],
266
+ operationId: value['operationId'],
267
+ input: value['input'],
268
+ context: value['context'],
269
+ result: value['result'],
270
+ });
271
+ }
272
+ if (value['phase'] !== 'before' && value['phase'] !== 'replace')
273
+ return invalid('phase must be before, after, or replace');
274
+ if (Object.hasOwn(value, 'result'))
275
+ return invalid('only after requests may contain result');
276
+ return valid({
277
+ protocolVersion: 1,
278
+ op: value['op'],
279
+ phase: value['phase'],
280
+ command: value['command'],
281
+ operationId: value['operationId'],
282
+ input: value['input'],
283
+ context: value['context'],
284
+ });
285
+ }
286
+ function validateResponse(value, request) {
287
+ if (!isRecord(value))
288
+ return invalid('response must be an object');
289
+ if (value['protocolVersion'] !== 1)
290
+ return invalid('response has an unsupported hook protocol version');
291
+ if (value['ok'] === true) {
292
+ if (!hasOnlyKeys(value, ['protocolVersion', 'ok', 'result']))
293
+ return invalid('successful response contains unknown fields');
294
+ const hasResult = Object.hasOwn(value, 'result');
295
+ if (request.phase === 'replace' && !hasResult)
296
+ return invalid('replace response requires result');
297
+ if (request.phase !== 'replace' && hasResult)
298
+ return invalid('only replace responses may contain result');
299
+ if (hasResult) {
300
+ const result = value['result'];
301
+ if (!isJson(result))
302
+ return invalid('response result must be JSON');
303
+ return valid({ protocolVersion: 1, ok: true, result });
304
+ }
305
+ return valid({ protocolVersion: 1, ok: true });
306
+ }
307
+ if (value['ok'] !== false || !hasOnlyKeys(value, ['protocolVersion', 'ok', 'error'])) {
308
+ return invalid('response must contain protocolVersion, ok, and error or result');
309
+ }
310
+ if (!isHookError(value['error']))
311
+ return invalid('response error must contain code and message strings');
312
+ return valid({ protocolVersion: 1, ok: false, error: value['error'] });
313
+ }
314
+ function isContext(value) {
315
+ return isRecord(value)
316
+ && hasOnlyKeys(value, ['cwd', 'providedParams'])
317
+ && typeof value['cwd'] === 'string'
318
+ && isStringArray(value['providedParams']);
319
+ }
320
+ function isHookError(value) {
321
+ return isRecord(value)
322
+ && hasOnlyKeys(value, ['code', 'message', 'field', 'next'])
323
+ && typeof value['code'] === 'string'
324
+ && value['code'] !== ''
325
+ && typeof value['message'] === 'string'
326
+ && value['message'] !== ''
327
+ && (value['field'] === undefined || typeof value['field'] === 'string')
328
+ && (value['next'] === undefined || typeof value['next'] === 'string');
329
+ }
330
+ function isRecord(value) {
331
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
332
+ }
333
+ function isStringArray(value) {
334
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
335
+ }
336
+ function isJson(value) {
337
+ if (value === null || typeof value === 'boolean' || typeof value === 'string')
338
+ return true;
339
+ if (typeof value === 'number')
340
+ return Number.isFinite(value);
341
+ if (Array.isArray(value))
342
+ return value.every(isJson);
343
+ return isRecord(value) && Object.values(value).every(isJson);
344
+ }
345
+ function hasOnlyKeys(value, keys) {
346
+ return Object.keys(value).every((key) => keys.includes(key));
347
+ }
348
+ function valid(value) {
349
+ return { valid: true, value };
350
+ }
351
+ function invalid(message) {
352
+ return { valid: false, message };
353
+ }
354
+ function describeError(error) {
355
+ return error instanceof Error ? error.message : String(error);
356
+ }
357
+ class ResponseTooLargeError extends Error {
358
+ }
@@ -0,0 +1,7 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+ export declare const PROCESS_GROUP_KILL_GRACE_MS = 250;
3
+ export declare function posixProcessGroupSpawnOptions(): {
4
+ detached: true;
5
+ };
6
+ /** Signal the isolated group, then unconditionally reap survivors after grace. */
7
+ export declare function terminateProcessGroup(child: ChildProcess, graceMs?: number): Promise<void>;
@@ -0,0 +1,34 @@
1
+ export const PROCESS_GROUP_KILL_GRACE_MS = 250;
2
+ export function posixProcessGroupSpawnOptions() {
3
+ if (process.platform === 'win32') {
4
+ throw new Error('hook process execution requires POSIX process groups');
5
+ }
6
+ return { detached: true };
7
+ }
8
+ /** Signal the isolated group, then unconditionally reap survivors after grace. */
9
+ export async function terminateProcessGroup(child, graceMs = PROCESS_GROUP_KILL_GRACE_MS) {
10
+ const pid = child.pid;
11
+ if (pid === undefined)
12
+ return;
13
+ signalProcessGroup(pid, 'SIGTERM');
14
+ await delay(graceMs);
15
+ signalProcessGroup(pid, 'SIGKILL');
16
+ }
17
+ function signalProcessGroup(pid, signal) {
18
+ try {
19
+ process.kill(-pid, signal);
20
+ }
21
+ catch (error) {
22
+ if (!isMissingProcess(error))
23
+ throw error;
24
+ }
25
+ }
26
+ function isMissingProcess(error) {
27
+ return typeof error === 'object'
28
+ && error !== null
29
+ && 'code' in error
30
+ && error.code === 'ESRCH';
31
+ }
32
+ function delay(ms) {
33
+ return new Promise((resolve) => setTimeout(resolve, ms));
34
+ }
package/dist/index.d.ts CHANGED
@@ -21,6 +21,8 @@ export type { CrtrError } from './core/errors.js';
21
21
  export { nowIso } from './core/fs-utils.js';
22
22
  export { validateOperationId, buildEnvelope, operationIdContext, errorClassCodes, serializeEventRecord, errorClassFromError, } from './core/events/index.js';
23
23
  export type { BuildEnvelopeInput, ErrorClass, ErrorClassCode, ErrorClassHints, EventComponent, EventEnvelope, EventError, EventField, EventLevel, EventOutcome, OperationId, OperationIdContext, RetryDisposition, } from './core/events/index.js';
24
+ export { forwardHook, hookError, runHookExecutable, runLocalHookProcess } from './hook-authoring.js';
25
+ export type { AfterHookRequest, BeforeHookRequest, ForwardHookOptions, HookContext, HookError, HookFailure, HookHandler, HookHandlers, HookJson, HookPhase, HookRequest, HookResponse, HookSuccess, LocalHookProcessOptions, LocalHookProcessResult, LocalHookProcessSuccess, ReplaceHookRequest, RunHookExecutableOptions, } from './hook-authoring.js';
24
26
  export { validateCommandManifest } from './core/command-manifests/manifest.js';
25
27
  export type { CommandManifestValidation, ValidatedCommandManifest } from './core/command-manifests/manifest.js';
26
28
  export type { CommandManifestIssue, TransportKind } from './core/command-manifests/schema.js';
package/dist/index.js CHANGED
@@ -41,6 +41,8 @@ export { nowIso } from './core/fs-utils.js';
41
41
  // Pure envelope helpers only. Local source binding and canonical append remain
42
42
  // internal so package consumers cannot select crouter runtime file paths.
43
43
  export { validateOperationId, buildEnvelope, operationIdContext, errorClassCodes, serializeEventRecord, errorClassFromError, } from './core/events/index.js';
44
+ // ── Hook authoring ───────────────────────────────────────────────────────
45
+ export { forwardHook, hookError, runHookExecutable, runLocalHookProcess } from './hook-authoring.js';
44
46
  // ── Command-manifest compatibility ───────────────────────────────────────
45
47
  export { validateCommandManifest } from './core/command-manifests/manifest.js';
46
48
  export { validatePluginBundle } from './core/command-plugins/bundle.js';
@@ -0,0 +1,2 @@
1
+ import type { ProfileManifestMigration } from './types.js';
2
+ export declare const profileProjectMemoryMigration: ProfileManifestMigration;