@north-light/crouter 0.3.173 → 0.3.175

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 (674) hide show
  1. package/bin/runtime-selector.mjs +8 -1
  2. package/dist/api/__tests__/{client.test.js → serial/client.test.js} +2 -135
  3. package/dist/api/client.d.ts +12 -21
  4. package/dist/api/client.js +22 -31
  5. package/dist/api/dto/broker-ops.d.ts +158 -0
  6. package/dist/api/dto/broker-ops.js +6 -0
  7. package/dist/api/dto/crons.d.ts +6 -0
  8. package/dist/api/dto/human.d.ts +7 -1
  9. package/dist/api/dto/inbox.d.ts +1 -71
  10. package/dist/api/dto/inbox.js +1 -9
  11. package/dist/api/dto/nodes.d.ts +6 -7
  12. package/dist/api/dto/reviews.d.ts +1 -11
  13. package/dist/api/routes.d.ts +7 -4
  14. package/dist/api/routes.js +8 -5
  15. package/dist/build-root.js +3 -3
  16. package/dist/builtin-memory/04-base-worker.md +1 -4
  17. package/dist/builtin-memory/05-kinds/advisor/00-base.md +1 -5
  18. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +2 -7
  19. package/dist/builtin-memory/05-kinds/design/00-base.md +1 -6
  20. package/dist/builtin-memory/05-kinds/developer/00-base.md +1 -6
  21. package/dist/builtin-memory/05-kinds/explore/00-base.md +1 -4
  22. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +1 -2
  23. package/dist/builtin-memory/05-kinds/general/00-base.md +1 -3
  24. package/dist/builtin-memory/05-kinds/plan/00-base.md +1 -6
  25. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +1 -2
  26. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +1 -3
  27. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +1 -3
  28. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +1 -3
  29. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +1 -3
  30. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +1 -4
  31. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +2 -3
  32. package/dist/builtin-memory/05-kinds/spec/00-base.md +1 -2
  33. package/dist/builtin-memory/advisor/council.md +1 -6
  34. package/dist/builtin-memory/design.md +2 -7
  35. package/dist/builtin-memory/development.md +9 -14
  36. package/dist/builtin-memory/internal/marketplaces.md +2 -6
  37. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  38. package/dist/builtin-memory/internal/plugins.md +2 -8
  39. package/dist/builtin-memory/internal/storage-tiers.md +1 -2
  40. package/dist/builtin-memory/testing.md +41 -0
  41. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{provider-rotation.test.ts → serial/provider-rotation.test.ts} +27 -179
  42. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/claude-plugin-commands.ts +5 -15
  43. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +1 -28
  44. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +43 -88
  45. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.js +7 -1
  46. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/provider-rotation.ts +9 -1
  47. package/dist/cli.js +2 -2
  48. package/dist/clients/attach/__tests__/attach-remote-readonly.test.js +37 -131
  49. package/dist/clients/attach/__tests__/chat-view-snapshot-ordering.test.js +71 -152
  50. package/dist/clients/attach/__tests__/context-message.test.js +16 -174
  51. package/dist/clients/attach/__tests__/crtr-output.test.js +36 -2069
  52. package/dist/clients/attach/__tests__/diagram.test.js +2 -2
  53. package/dist/clients/attach/__tests__/edit-diff.test.js +0 -4
  54. package/dist/clients/attach/__tests__/file-review-focus.test.js +2 -1
  55. package/dist/clients/attach/__tests__/group-activity.test.js +40 -229
  56. package/dist/clients/attach/__tests__/oauth-dialog-lifecycle.test.js +26 -76
  57. package/dist/clients/attach/__tests__/prompt-preview.test.js +4 -70
  58. package/dist/clients/attach/__tests__/ref-autocomplete.test.js +11 -125
  59. package/dist/clients/attach/__tests__/titled-editor-preview.test.js +14 -205
  60. package/dist/clients/attach/command.d.ts +2 -1
  61. package/dist/clients/attach/command.js +81 -76
  62. package/dist/clients/attach/input/capabilities.d.ts +27 -0
  63. package/dist/clients/attach/input/capabilities.js +96 -0
  64. package/dist/clients/attach/input/controller.d.ts +20 -182
  65. package/dist/clients/attach/input/controller.js +128 -537
  66. package/dist/clients/attach/input/overlay-owner.d.ts +41 -0
  67. package/dist/clients/attach/input/overlay-owner.js +199 -0
  68. package/dist/clients/attach/overlays/file-review.d.ts +0 -4
  69. package/dist/clients/attach/overlays/file-review.js +13 -5
  70. package/dist/clients/attach/overlays/graph.js +2 -10
  71. package/dist/clients/attach/render/chat-view.d.ts +4 -3
  72. package/dist/clients/attach/render/chat-view.js +39 -33
  73. package/dist/clients/attach/render/group-activity.d.ts +1 -0
  74. package/dist/clients/attach/render/group-activity.js +20 -13
  75. package/dist/clients/attach/render/group-recap.js +12 -3
  76. package/dist/clients/attach/session/connection.js +9 -15
  77. package/dist/clients/attach/session/editor-inventory.d.ts +6 -5
  78. package/dist/clients/attach/session/editor-inventory.js +7 -8
  79. package/dist/clients/attach/session/file-links.d.ts +13 -0
  80. package/dist/clients/attach/session/file-links.js +107 -0
  81. package/dist/clients/attach/session/frame.d.ts +3 -0
  82. package/dist/clients/attach/session/frame.js +10 -1
  83. package/dist/clients/attach/session/frames.js +2 -3
  84. package/dist/clients/attach/session/input-wiring.d.ts +4 -1
  85. package/dist/clients/attach/session/input-wiring.js +15 -14
  86. package/dist/clients/attach/session/mouse.d.ts +6 -1
  87. package/dist/clients/attach/session/mouse.js +8 -5
  88. package/dist/clients/attach/session/profile-files.js +18 -12
  89. package/dist/clients/attach/session/reconnect.js +1 -1
  90. package/dist/clients/attach/session/state-sync.js +1 -2
  91. package/dist/clients/attach/session/surface-host.d.ts +3 -2
  92. package/dist/clients/attach/session/surface-host.js +4 -4
  93. package/dist/clients/attach/slash/dispatch.d.ts +8 -31
  94. package/dist/clients/attach/slash/dispatch.js +34 -81
  95. package/dist/clients/attach/viewer.js +820 -860
  96. package/dist/clients/conversation/projection.d.ts +79 -0
  97. package/dist/clients/conversation/projection.js +341 -0
  98. package/dist/clients/inbox/__tests__/serial/inbox-controller.test.js +159 -0
  99. package/dist/clients/inbox/__tests__/serial/mount-panel.test.js +151 -0
  100. package/dist/clients/inbox/controller.d.ts +0 -4
  101. package/dist/clients/inbox/controller.js +0 -2
  102. package/dist/clients/inbox/review/__tests__/review-core.test.js +4 -293
  103. package/dist/clients/inbox/review/__tests__/teardown-notice.test.js +100 -0
  104. package/dist/clients/inbox/review/comments-client.d.ts +3 -2
  105. package/dist/clients/inbox/review/comments-client.js +12 -12
  106. package/dist/clients/inbox/review/companion-pane.d.ts +30 -0
  107. package/dist/clients/inbox/review/companion-pane.js +86 -0
  108. package/dist/clients/inbox/review/document-surface.d.ts +182 -0
  109. package/dist/clients/inbox/review/{document-region.js → document-surface.js} +252 -48
  110. package/dist/clients/inbox/review/frame.js +5 -4
  111. package/dist/clients/inbox/review/launch.d.ts +8 -8
  112. package/dist/clients/inbox/review/launch.js +52 -92
  113. package/dist/clients/inbox/review/nvim.d.ts +2 -0
  114. package/dist/clients/inbox/review/nvim.js +14 -12
  115. package/dist/clients/inbox/review/review-client.d.ts +12 -8
  116. package/dist/clients/inbox/review/review-client.js +46 -35
  117. package/dist/clients/inbox/review-adapter.d.ts +3 -10
  118. package/dist/clients/inbox/review-adapter.js +27 -19
  119. package/dist/clients/inbox/surface.d.ts +0 -4
  120. package/dist/clients/inbox/surface.js +0 -2
  121. package/dist/clients/surfaces/host.d.ts +11 -3
  122. package/dist/commands/__tests__/api-client-cold-start-diagnostic.test.js +7 -34
  123. package/dist/commands/__tests__/human.test.js +11 -322
  124. package/dist/commands/api-client.d.ts +5 -5
  125. package/dist/commands/api-client.js +6 -85
  126. package/dist/commands/canvas-browse.js +3 -29
  127. package/dist/commands/canvas-config.js +1 -1
  128. package/dist/commands/canvas-use.js +3 -3
  129. package/dist/commands/canvas.js +1 -1
  130. package/dist/commands/cron.js +5 -1
  131. package/dist/commands/human/queue.d.ts +0 -4
  132. package/dist/commands/human/queue.js +7 -40
  133. package/dist/commands/memory/list.d.ts +14 -0
  134. package/dist/commands/memory/list.js +35 -31
  135. package/dist/commands/memory/read.d.ts +7 -0
  136. package/dist/commands/memory/read.js +14 -8
  137. package/dist/commands/memory/write.js +13 -12
  138. package/dist/commands/node/bash.d.ts +1 -0
  139. package/dist/commands/node/bash.js +198 -0
  140. package/dist/commands/node/create.d.ts +23 -0
  141. package/dist/commands/node/create.js +207 -0
  142. package/dist/commands/node/inspect.d.ts +2 -0
  143. package/dist/commands/node/inspect.js +302 -0
  144. package/dist/commands/node/lifecycle.d.ts +4 -0
  145. package/dist/commands/node/lifecycle.js +415 -0
  146. package/dist/commands/node/message.d.ts +1 -0
  147. package/dist/commands/node/message.js +164 -0
  148. package/dist/commands/node/pyramid.d.ts +2 -0
  149. package/dist/commands/node/pyramid.js +79 -0
  150. package/dist/commands/node/subscription.d.ts +1 -0
  151. package/dist/commands/node/subscription.js +82 -0
  152. package/dist/commands/node/wait.d.ts +1 -0
  153. package/dist/commands/node/wait.js +159 -0
  154. package/dist/commands/node-lifecycle-revive.js +10 -57
  155. package/dist/commands/node.d.ts +0 -42
  156. package/dist/commands/node.js +11 -2065
  157. package/dist/commands/search.js +1 -1
  158. package/dist/commands/surface/node/cycle.d.ts +1 -0
  159. package/dist/commands/surface/node/cycle.js +83 -0
  160. package/dist/commands/surface/node/focus.d.ts +1 -0
  161. package/dist/commands/surface/node/focus.js +67 -0
  162. package/dist/commands/surface/node/id.d.ts +1 -0
  163. package/dist/commands/surface/node/id.js +38 -0
  164. package/dist/commands/surface/node/pin.d.ts +1 -0
  165. package/dist/commands/surface/node/pin.js +42 -0
  166. package/dist/commands/surface/node/placement.d.ts +46 -0
  167. package/dist/commands/surface/node/placement.js +124 -0
  168. package/dist/commands/surface-node.js +4 -1
  169. package/dist/commands/surface-reload.js +1 -2
  170. package/dist/commands/surface-tmux-spread.js +1 -1
  171. package/dist/commands/surface.js +5 -20
  172. package/dist/commands/sys/__tests__/setup-core.test.js +18 -271
  173. package/dist/commands/sys/config.js +6 -6
  174. package/dist/commands/sys/logs.js +1 -1
  175. package/dist/commands/sys/panels/broker-limits-panel.d.ts +17 -0
  176. package/dist/commands/sys/panels/broker-limits-panel.js +104 -0
  177. package/dist/commands/sys/panels/dependencies-panel.d.ts +14 -0
  178. package/dist/commands/sys/panels/dependencies-panel.js +80 -0
  179. package/dist/commands/sys/panels/keybindings-panel.d.ts +56 -0
  180. package/dist/commands/sys/panels/keybindings-panel.js +703 -0
  181. package/dist/commands/sys/panels/keys-panel.d.ts +16 -0
  182. package/dist/commands/sys/panels/keys-panel.js +60 -0
  183. package/dist/commands/sys/panels/models-panel.d.ts +77 -0
  184. package/dist/commands/sys/panels/models-panel.js +647 -0
  185. package/dist/commands/sys/panels/panel.d.ts +45 -0
  186. package/dist/commands/sys/panels/panel.js +137 -0
  187. package/dist/commands/sys/panels/plugins-panel.d.ts +16 -0
  188. package/dist/commands/sys/panels/plugins-panel.js +80 -0
  189. package/dist/commands/sys/panels/prefs-panel.d.ts +21 -0
  190. package/dist/commands/sys/panels/prefs-panel.js +85 -0
  191. package/dist/commands/sys/panels/provider-panel.d.ts +62 -0
  192. package/dist/commands/sys/panels/provider-panel.js +538 -0
  193. package/dist/commands/sys/settings-shell.d.ts +55 -0
  194. package/dist/commands/sys/settings-shell.js +400 -0
  195. package/dist/commands/sys/settings.d.ts +2 -0
  196. package/dist/commands/sys/settings.js +31 -0
  197. package/dist/commands/sys/setup-core.d.ts +3 -33
  198. package/dist/commands/sys/setup-core.js +27 -78
  199. package/dist/commands/sys/setup-settings-runner.d.ts +24 -0
  200. package/dist/commands/sys/setup-settings-runner.js +63 -0
  201. package/dist/commands/sys/setup-wizard.d.ts +26 -125
  202. package/dist/commands/sys/setup-wizard.js +209 -2473
  203. package/dist/commands/sys/setup.js +5 -7
  204. package/dist/commands/sys.js +3 -2
  205. package/dist/core/__tests__/broker-extension-canvas-db-boundary.test.js +50 -0
  206. package/dist/core/__tests__/broker-launch-candidates.test.js +20 -9
  207. package/dist/core/__tests__/broker-preflight.test.js +1 -1
  208. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +108 -27
  209. package/dist/core/__tests__/canvas-inbox-watcher.test.js +14 -0
  210. package/dist/core/__tests__/canvas.test.js +2 -36
  211. package/dist/core/__tests__/close.test.js +14 -3
  212. package/dist/core/__tests__/context-intro.test.js +32 -143
  213. package/dist/core/__tests__/cron-broker-capacity.test.js +22 -3
  214. package/dist/core/__tests__/daemon-boot.test.js +2 -20
  215. package/dist/core/__tests__/daemon-wedge.test.js +1 -1
  216. package/dist/core/__tests__/dead-node-policy-table.test.js +1 -18
  217. package/dist/core/__tests__/fixtures/fake-engine.d.ts +1 -1
  218. package/dist/core/__tests__/fixtures/fake-engine.js +2 -2
  219. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  220. package/dist/core/__tests__/focuses.test.js +4 -98
  221. package/dist/core/__tests__/helpers/broker-clients.d.ts +0 -3
  222. package/dist/core/__tests__/helpers/broker-clients.js +0 -14
  223. package/dist/core/__tests__/helpers/harness.d.ts +0 -8
  224. package/dist/core/__tests__/helpers/harness.js +7 -87
  225. package/dist/core/__tests__/helpers/viewer-panes.js +1 -1
  226. package/dist/core/__tests__/inline-memory-refs.test.js +5 -134
  227. package/dist/core/__tests__/kickoff.test.js +1 -1
  228. package/dist/core/__tests__/memory-resolver-precedence.test.js +29 -1
  229. package/dist/core/__tests__/migration.test.js +35 -48
  230. package/dist/core/__tests__/on-read-dedup-resume.test.js +22 -12
  231. package/dist/core/__tests__/passive-subscription.test.js +26 -49
  232. package/dist/core/__tests__/preview-registry-sync.test.js +1 -2
  233. package/dist/core/__tests__/prune.test.js +94 -1
  234. package/dist/core/__tests__/review-model-floor.test.js +0 -32
  235. package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +255 -0
  236. package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.js +39 -7
  237. package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.js +30 -29
  238. package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.js +54 -5
  239. package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.js +134 -82
  240. package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.js +15 -4
  241. package/dist/core/__tests__/{full/idle-release-settlement.test.js → seam/dormancy-release.test.js} +69 -17
  242. package/dist/core/__tests__/{full/yield-message-transaction.test.js → seam/yield-refresh-transaction.test.js} +31 -9
  243. package/dist/core/__tests__/secrets.test.js +1 -7
  244. package/dist/core/__tests__/{broker-fork-seam.test.js → serial/broker-fork-seam.test.js} +58 -7
  245. package/dist/core/__tests__/{broker-sdk-wiring.test.js → serial/broker-sdk-wiring.test.js} +18 -162
  246. package/dist/core/__tests__/{broker-snapshot-history.test.js → serial/broker-snapshot-history.test.js} +9 -9
  247. package/dist/core/__tests__/{command-plugins.test.js → serial/command-plugins.test.js} +105 -15
  248. package/dist/core/__tests__/{deferred-no-wake.test.js → serial/deferred-no-wake.test.js} +23 -20
  249. package/dist/core/__tests__/{flagship-lifecycle.test.js → serial/flagship-lifecycle.test.js} +7 -7
  250. package/dist/core/__tests__/{host-teardown-process-group.test.js → serial/host-teardown-process-group.test.js} +6 -6
  251. package/dist/core/__tests__/{human-deliver-e2e.test.js → serial/human-deliver-e2e.test.js} +12 -12
  252. package/dist/core/__tests__/{live-mutation.test.js → serial/live-mutation.test.js} +5 -5
  253. package/dist/core/__tests__/{refresh-stall-recycle.test.js → serial/refresh-stall-recycle.test.js} +5 -14
  254. package/dist/core/__tests__/{revive.test.js → serial/revive.test.js} +15 -100
  255. package/dist/core/__tests__/{spawn-root.test.js → serial/spawn-root.test.js} +32 -15
  256. package/dist/core/__tests__/serial/subscription-delivery.test.js +126 -0
  257. package/dist/core/__tests__/serial/tmux-surface.test.js +256 -0
  258. package/dist/core/__tests__/{worktree.test.js → serial/worktree.test.js} +15 -15
  259. package/dist/core/__tests__/session-model.test.js +0 -18
  260. package/dist/core/__tests__/spawn-worktree-id-conflict.test.js +9 -0
  261. package/dist/core/__tests__/watchdog-abort-arms-retry.test.js +50 -0
  262. package/dist/core/broker-client/__tests__/transport-relay.test.js +15 -16
  263. package/dist/core/broker-client/__tests__/transport-socket.test.js +28 -0
  264. package/dist/core/broker-client/client.d.ts +2 -2
  265. package/dist/core/broker-client/client.js +4 -7
  266. package/dist/core/broker-client/transport-relay.d.ts +13 -19
  267. package/dist/core/broker-client/transport-relay.js +19 -34
  268. package/dist/core/broker-client/transport-socket.d.ts +2 -2
  269. package/dist/core/broker-client/transport-socket.js +9 -2
  270. package/dist/core/broker-client/transport.d.ts +7 -3
  271. package/dist/core/broker-client/transport.js +4 -4
  272. package/dist/core/canvas/__tests__/attention.test.js +1 -18
  273. package/dist/core/canvas/__tests__/remote-canvas-source.test.js +7 -64
  274. package/dist/core/canvas/__tests__/remote-transport.test.js +2 -3
  275. package/dist/core/canvas/__tests__/render-remote.test.js +7 -52
  276. package/dist/core/canvas/browse/app.d.ts +2 -17
  277. package/dist/core/canvas/browse/app.js +16 -88
  278. package/dist/core/canvas/browse/model.d.ts +0 -4
  279. package/dist/core/canvas/browse/model.js +1 -12
  280. package/dist/core/canvas/browse/render.js +6 -3
  281. package/dist/core/canvas/crons.d.ts +8 -5
  282. package/dist/core/canvas/crons.js +7 -6
  283. package/dist/core/canvas/extensions.d.ts +7 -3
  284. package/dist/core/canvas/extensions.js +16 -4
  285. package/dist/core/canvas/nav-model.d.ts +0 -4
  286. package/dist/core/canvas/nav-model.js +2 -14
  287. package/dist/core/canvas/node-order.d.ts +3 -0
  288. package/dist/core/canvas/node-order.js +12 -0
  289. package/dist/core/canvas/remote-canvas-source.d.ts +3 -6
  290. package/dist/core/canvas/remote-canvas-source.js +8 -15
  291. package/dist/core/command.js +11 -4
  292. package/dist/core/config.js +3 -1
  293. package/dist/core/events/envelope.js +1 -1
  294. package/dist/core/events/read.js +7 -21
  295. package/dist/core/events/source.d.ts +0 -2
  296. package/dist/core/events/source.js +0 -16
  297. package/dist/core/events/types.d.ts +1 -1
  298. package/dist/core/fault-classifier.d.ts +1 -1
  299. package/dist/core/feed/passive.d.ts +0 -11
  300. package/dist/core/feed/passive.js +1 -62
  301. package/dist/core/human/__tests__/{inbox-core.test.js → serial/inbox-core.test.js} +6 -6
  302. package/dist/core/keybindings/__tests__/resolve.test.js +1 -96
  303. package/dist/core/keybindings/background-bash.js +3 -4
  304. package/dist/core/keybindings/catalog.js +1 -1
  305. package/dist/core/memory/inline-ref-grammar.js +3 -3
  306. package/dist/core/memory-resolver.d.ts +11 -25
  307. package/dist/core/memory-resolver.js +51 -33
  308. package/dist/core/model-routes.d.ts +15 -0
  309. package/dist/core/model-routes.js +12 -0
  310. package/dist/core/pool-credential-store.js +1 -1
  311. package/dist/core/preview-registry.js +8 -9
  312. package/dist/core/profiles/__tests__/root-profile.test.js +4 -14
  313. package/dist/core/review/realize.d.ts +0 -13
  314. package/dist/core/review/realize.js +0 -40
  315. package/dist/core/review/types.d.ts +1 -1
  316. package/dist/core/runtime/__tests__/engine-route.test.js +2 -2
  317. package/dist/core/runtime/bearings.js +1 -11
  318. package/dist/core/runtime/broker/client-registry.d.ts +33 -0
  319. package/dist/core/runtime/broker/client-registry.js +164 -0
  320. package/dist/core/runtime/broker/daemon-ops.d.ts +19 -0
  321. package/dist/core/runtime/broker/daemon-ops.js +49 -0
  322. package/dist/core/runtime/broker/event-projection.d.ts +42 -0
  323. package/dist/core/runtime/broker/event-projection.js +178 -0
  324. package/dist/core/runtime/broker/fault-retry.d.ts +39 -0
  325. package/dist/core/runtime/broker/fault-retry.js +277 -0
  326. package/dist/core/runtime/broker/frame-dispatch.d.ts +238 -0
  327. package/dist/core/runtime/broker/frame-dispatch.js +1537 -0
  328. package/dist/core/runtime/broker/inbox.d.ts +22 -0
  329. package/dist/core/runtime/broker/inbox.js +167 -0
  330. package/dist/core/runtime/broker/passive.d.ts +16 -0
  331. package/dist/core/runtime/broker/passive.js +44 -0
  332. package/dist/core/runtime/broker/read-ops.d.ts +23 -0
  333. package/dist/core/runtime/broker/read-ops.js +150 -0
  334. package/dist/core/runtime/broker/rebind.d.ts +74 -0
  335. package/dist/core/runtime/broker/rebind.js +211 -0
  336. package/dist/core/runtime/broker/tool-groups.d.ts +39 -0
  337. package/dist/core/runtime/broker/tool-groups.js +244 -0
  338. package/dist/core/runtime/broker-extension-render.d.ts +11 -0
  339. package/dist/core/runtime/broker-extension-render.js +181 -0
  340. package/dist/core/runtime/broker-persona-guidance.d.ts +13 -0
  341. package/dist/core/runtime/broker-persona-guidance.js +58 -0
  342. package/dist/core/runtime/broker-protocol.d.ts +1 -1
  343. package/dist/core/runtime/broker-protocol.js +1 -1
  344. package/dist/core/runtime/broker.d.ts +5 -196
  345. package/dist/core/runtime/broker.js +276 -3059
  346. package/dist/core/runtime/canvas-extensions.js +11 -2
  347. package/dist/core/runtime/fault.js +1 -1
  348. package/dist/core/runtime/model-selection.d.ts +9 -0
  349. package/dist/core/runtime/model-selection.js +12 -0
  350. package/dist/core/runtime/model-swap.d.ts +3 -3
  351. package/dist/core/runtime/model-swap.js +33 -25
  352. package/dist/core/runtime/placement-tmux.d.ts +3 -14
  353. package/dist/core/runtime/placement-tmux.js +11 -80
  354. package/dist/core/runtime/placement.d.ts +4 -2
  355. package/dist/core/runtime/placement.js +7 -12
  356. package/dist/core/runtime/promote.d.ts +14 -1
  357. package/dist/core/runtime/promote.js +1 -1
  358. package/dist/core/runtime/reset.d.ts +1 -1
  359. package/dist/core/runtime/roadmap.js +1 -1
  360. package/dist/core/runtime/spawn.d.ts +8 -0
  361. package/dist/core/runtime/spawn.js +36 -6
  362. package/dist/core/runtime/stop-guard.d.ts +4 -3
  363. package/dist/core/runtime/stop-guard.js +36 -18
  364. package/dist/core/runtime/stop-signals.d.ts +4 -1
  365. package/dist/core/runtime/stream-watchdog.js +4 -5
  366. package/dist/core/runtime/structured-output.d.ts +15 -2
  367. package/dist/core/runtime/structured-output.js +35 -12
  368. package/dist/core/runtime/tmux-bindings.d.ts +25 -0
  369. package/dist/core/runtime/tmux-bindings.js +682 -0
  370. package/dist/core/runtime/tmux-chrome.d.ts +2 -1
  371. package/dist/core/runtime/tmux-chrome.js +3 -2
  372. package/dist/core/runtime/{tmux.d.ts → tmux-driver.d.ts} +18 -26
  373. package/dist/core/runtime/tmux-driver.js +598 -0
  374. package/dist/core/runtime/view-socket.d.ts +14 -0
  375. package/dist/core/runtime/view-socket.js +83 -0
  376. package/dist/core/session-model/session-state.d.ts +1 -2
  377. package/dist/core/session-model/session-state.js +5 -8
  378. package/dist/core/spawn.d.ts +1 -1
  379. package/dist/core/spawn.js +2 -2
  380. package/dist/core/substrate/gate.d.ts +1 -1
  381. package/dist/core/substrate/on-read-pure.d.ts +7 -0
  382. package/dist/core/substrate/on-read-pure.js +272 -0
  383. package/dist/core/termrender/termrender.js +32 -2
  384. package/dist/core/user-settings.d.ts +23 -1
  385. package/dist/core/user-settings.js +18 -1
  386. package/dist/daemon/api/__tests__/node-model-validation.test.js +134 -0
  387. package/dist/daemon/api/__tests__/review-terminal-conflict.test.js +98 -0
  388. package/dist/daemon/api/__tests__/{full → seam}/api-server.test.js +6 -48
  389. package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.js +17 -44
  390. package/dist/daemon/api/__tests__/{full/push-parity.test.js → seam/leaf-api-parity.test.js} +61 -42
  391. package/dist/daemon/api/bridge.js +40 -81
  392. package/dist/daemon/api/handlers/broker-ops.d.ts +2 -0
  393. package/dist/daemon/api/handlers/broker-ops.js +383 -0
  394. package/dist/daemon/api/handlers/crons.js +9 -4
  395. package/dist/daemon/api/handlers/files.js +6 -7
  396. package/dist/daemon/api/handlers/inbox.js +2 -340
  397. package/dist/daemon/api/handlers/nodes.d.ts +10 -1
  398. package/dist/daemon/api/handlers/nodes.js +74 -39
  399. package/dist/daemon/api/handlers/review-comments.js +5 -1
  400. package/dist/daemon/api/handlers/reviews.js +0 -19
  401. package/dist/daemon/api/map.js +25 -4
  402. package/dist/daemon/api/router.js +2 -3
  403. package/dist/daemon/api/server.js +5 -7
  404. package/dist/daemon/cron-run.d.ts +4 -0
  405. package/dist/daemon/cron-run.js +30 -13
  406. package/dist/daemon/crtrd.d.ts +3 -58
  407. package/dist/daemon/crtrd.js +28 -1244
  408. package/dist/daemon/manage.js +0 -1
  409. package/dist/daemon/reconcilers/broker-supervision.d.ts +50 -0
  410. package/dist/daemon/reconcilers/broker-supervision.js +549 -0
  411. package/dist/daemon/reconcilers/cron-lane.d.ts +10 -0
  412. package/dist/daemon/reconcilers/cron-lane.js +32 -0
  413. package/dist/daemon/reconcilers/dormant-inbox.d.ts +18 -0
  414. package/dist/daemon/reconcilers/dormant-inbox.js +143 -0
  415. package/dist/daemon/reconcilers/storage-maintenance.d.ts +22 -0
  416. package/dist/daemon/reconcilers/storage-maintenance.js +213 -0
  417. package/dist/daemon/review/finish.js +2 -5
  418. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +34 -53
  419. package/dist/pi-extensions/__tests__/canvas-recap.test.js +16 -113
  420. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +72 -99
  421. package/dist/pi-extensions/__tests__/canvas-stophook-context-nudge.test.js +29 -12
  422. package/dist/pi-extensions/broker-local.d.ts +20 -0
  423. package/dist/pi-extensions/broker-local.js +89 -0
  424. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -3
  425. package/dist/pi-extensions/canvas-context-intro.js +22 -11
  426. package/dist/pi-extensions/canvas-doc-substrate.js +17 -34
  427. package/dist/pi-extensions/canvas-goal-capture.js +7 -12
  428. package/dist/pi-extensions/canvas-inbox-watcher.js +124 -139
  429. package/dist/pi-extensions/canvas-passive-context.d.ts +3 -3
  430. package/dist/pi-extensions/canvas-passive-context.js +22 -18
  431. package/dist/pi-extensions/canvas-recap.d.ts +2 -2
  432. package/dist/pi-extensions/canvas-recap.js +48 -21
  433. package/dist/pi-extensions/canvas-review-boundary.js +10 -4
  434. package/dist/pi-extensions/canvas-stophook.d.ts +0 -5
  435. package/dist/pi-extensions/canvas-stophook.js +149 -431
  436. package/dist/pi-extensions/canvas-structured-output.js +52 -30
  437. package/dist/pi-extensions/envelope-merge.d.ts +13 -0
  438. package/dist/pi-extensions/envelope-merge.js +29 -0
  439. package/dist/shared/working-activity.d.ts +3 -2
  440. package/dist/shared/working-activity.js +3 -2
  441. package/dist/types.d.ts +6 -5
  442. package/dist/types.js +2 -2
  443. package/docs/compat/hearth-crtr-v1.md +4 -12
  444. package/docs/compat/hearth-crtr-v2.md +3 -11
  445. package/docs/compat/hearth-crtr-v3.md +4 -12
  446. package/docs/compat/hearth-crtr-v4.md +3 -11
  447. package/docs/compat/hearth-crtr-v5.md +3 -11
  448. package/docs/public-api.md +3 -7
  449. package/package.json +7 -49
  450. package/runtime.lock.json +2766 -14533
  451. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/claude-plugin-commands.test.ts +0 -49
  452. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/memory-slash-commands.test.ts +0 -194
  453. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/statusline.test.ts +0 -68
  454. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/test.ts +0 -119
  455. package/dist/clients/attach/__tests__/attach-chrome-remote.test.js +0 -439
  456. package/dist/clients/attach/__tests__/attach-keybindings.test.js +0 -170
  457. package/dist/clients/attach/__tests__/crtr-output-coverage.test.js +0 -57
  458. package/dist/clients/attach/__tests__/editor-frame-title.test.js +0 -19
  459. package/dist/clients/attach/__tests__/input-controller-extension-command.test.js +0 -32
  460. package/dist/clients/attach/__tests__/markdown-headings.test.js +0 -35
  461. package/dist/clients/attach/__tests__/session-identity-refresh.test.js +0 -32
  462. package/dist/clients/conversation/conversation.d.ts +0 -65
  463. package/dist/clients/conversation/conversation.js +0 -318
  464. package/dist/clients/conversation/frames.d.ts +0 -30
  465. package/dist/clients/conversation/frames.js +0 -52
  466. package/dist/clients/conversation/region.d.ts +0 -51
  467. package/dist/clients/conversation/region.js +0 -265
  468. package/dist/clients/inbox/__tests__/inbox-controller.test.js +0 -247
  469. package/dist/clients/inbox/__tests__/mount-panel.test.js +0 -776
  470. package/dist/clients/inbox/review/__tests__/remap.test.js +0 -196
  471. package/dist/clients/inbox/review/columns.d.ts +0 -22
  472. package/dist/clients/inbox/review/columns.js +0 -40
  473. package/dist/clients/inbox/review/composition.d.ts +0 -60
  474. package/dist/clients/inbox/review/composition.js +0 -278
  475. package/dist/clients/inbox/review/document-region.d.ts +0 -120
  476. package/dist/clients/surfaces/region.d.ts +0 -16
  477. package/dist/clients/web/__tests__/push-engine.test.js +0 -158
  478. package/dist/clients/web/__tests__/push-registry.test.d.ts +0 -1
  479. package/dist/clients/web/__tests__/push-registry.test.js +0 -177
  480. package/dist/clients/web/dev-server.d.ts +0 -7
  481. package/dist/clients/web/dev-server.js +0 -46
  482. package/dist/clients/web/events.d.ts +0 -19
  483. package/dist/clients/web/events.js +0 -163
  484. package/dist/clients/web/push-engine.d.ts +0 -44
  485. package/dist/clients/web/push-engine.js +0 -125
  486. package/dist/clients/web/push-registry.d.ts +0 -85
  487. package/dist/clients/web/push-registry.js +0 -373
  488. package/dist/clients/web/server.d.ts +0 -19
  489. package/dist/clients/web/server.js +0 -661
  490. package/dist/clients/web/web-client/shared/protocol.d.ts +0 -214
  491. package/dist/clients/web/web-client/shared/protocol.js +0 -8
  492. package/dist/clients/web/web-cmd.d.ts +0 -2
  493. package/dist/clients/web/web-cmd.js +0 -141
  494. package/dist/commands/__tests__/api-canvas-source.test.d.ts +0 -1
  495. package/dist/commands/__tests__/api-canvas-source.test.js +0 -37
  496. package/dist/commands/__tests__/canvas-config.test.d.ts +0 -1
  497. package/dist/commands/__tests__/canvas-config.test.js +0 -107
  498. package/dist/commands/__tests__/node-no-kickoff-flag.test.d.ts +0 -1
  499. package/dist/commands/__tests__/node-no-kickoff-flag.test.js +0 -73
  500. package/dist/commands/__tests__/node-worktree-flag.test.d.ts +0 -1
  501. package/dist/commands/__tests__/node-worktree-flag.test.js +0 -49
  502. package/dist/commands/__tests__/search-contents.test.d.ts +0 -1
  503. package/dist/commands/__tests__/search-contents.test.js +0 -52
  504. package/dist/commands/memory/__tests__/lint.test.d.ts +0 -1
  505. package/dist/commands/memory/__tests__/lint.test.js +0 -15
  506. package/dist/commands/sys/__tests__/config-broker-thresholds.test.d.ts +0 -1
  507. package/dist/commands/sys/__tests__/config-broker-thresholds.test.js +0 -95
  508. package/dist/commands/sys/__tests__/config-keybindings.test.d.ts +0 -1
  509. package/dist/commands/sys/__tests__/config-keybindings.test.js +0 -55
  510. package/dist/commands/sys/__tests__/helpers/setup-runtime.d.ts +0 -5
  511. package/dist/commands/sys/__tests__/helpers/setup-runtime.js +0 -19
  512. package/dist/commands/sys/__tests__/setup-front-door.test.d.ts +0 -1
  513. package/dist/commands/sys/__tests__/setup-front-door.test.js +0 -102
  514. package/dist/commands/sys/__tests__/setup-models.test.d.ts +0 -1
  515. package/dist/commands/sys/__tests__/setup-models.test.js +0 -205
  516. package/dist/commands/sys/__tests__/setup-providers-viewport.test.d.ts +0 -1
  517. package/dist/commands/sys/__tests__/setup-providers-viewport.test.js +0 -155
  518. package/dist/commands/sys/__tests__/sync-deps.test.d.ts +0 -1
  519. package/dist/commands/sys/__tests__/sync-deps.test.js +0 -179
  520. package/dist/commands/sys/__tests__/sync-import.test.d.ts +0 -1
  521. package/dist/commands/sys/__tests__/sync-import.test.js +0 -315
  522. package/dist/core/__tests__/base-worker-prompt.test.d.ts +0 -1
  523. package/dist/core/__tests__/base-worker-prompt.test.js +0 -57
  524. package/dist/core/__tests__/command-plugins-surfaces.test.d.ts +0 -1
  525. package/dist/core/__tests__/command-plugins-surfaces.test.js +0 -359
  526. package/dist/core/__tests__/connection-reconnect-resume.test.d.ts +0 -1
  527. package/dist/core/__tests__/connection-reconnect-resume.test.js +0 -36
  528. package/dist/core/__tests__/finalization-migration.test.d.ts +0 -1
  529. package/dist/core/__tests__/finalization-migration.test.js +0 -38
  530. package/dist/core/__tests__/fixtures/extra-extension.d.ts +0 -1
  531. package/dist/core/__tests__/fixtures/extra-extension.js +0 -3
  532. package/dist/core/__tests__/fixtures/spawn-proof-target.test.d.ts +0 -1
  533. package/dist/core/__tests__/fixtures/spawn-proof-target.test.js +0 -15
  534. package/dist/core/__tests__/fork.test.d.ts +0 -1
  535. package/dist/core/__tests__/fork.test.js +0 -108
  536. package/dist/core/__tests__/full/broker-attach-limits.test.d.ts +0 -1
  537. package/dist/core/__tests__/full/broker-attach-limits.test.js +0 -147
  538. package/dist/core/__tests__/full/broker-deliver-ack.test.d.ts +0 -1
  539. package/dist/core/__tests__/full/broker-deliver-ack.test.js +0 -73
  540. package/dist/core/__tests__/full/broker-dialogs.test.d.ts +0 -1
  541. package/dist/core/__tests__/full/broker-dialogs.test.js +0 -180
  542. package/dist/core/__tests__/full/broker-display-snapshot.test.d.ts +0 -1
  543. package/dist/core/__tests__/full/broker-display-snapshot.test.js +0 -75
  544. package/dist/core/__tests__/full/broker-dormant-wake.test.d.ts +0 -1
  545. package/dist/core/__tests__/full/broker-dormant-wake.test.js +0 -130
  546. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +0 -1
  547. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +0 -57
  548. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +0 -1
  549. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +0 -80
  550. package/dist/core/__tests__/full/cascade-close.test.d.ts +0 -1
  551. package/dist/core/__tests__/full/cascade-close.test.js +0 -240
  552. package/dist/core/__tests__/full/detach-focus.test.d.ts +0 -1
  553. package/dist/core/__tests__/full/detach-focus.test.js +0 -126
  554. package/dist/core/__tests__/full/idle-release-settlement.test.d.ts +0 -1
  555. package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.d.ts +0 -1
  556. package/dist/core/__tests__/full/issue-146-yield-spawn-focus.test.js +0 -107
  557. package/dist/core/__tests__/full/park-clock-release.test.d.ts +0 -1
  558. package/dist/core/__tests__/full/park-clock-release.test.js +0 -56
  559. package/dist/core/__tests__/full/yield-message-transaction.test.d.ts +0 -1
  560. package/dist/core/__tests__/memory-index-render.test.d.ts +0 -1
  561. package/dist/core/__tests__/memory-index-render.test.js +0 -60
  562. package/dist/core/__tests__/model-routes.test.d.ts +0 -1
  563. package/dist/core/__tests__/model-routes.test.js +0 -41
  564. package/dist/core/__tests__/on-read-identity.test.d.ts +0 -1
  565. package/dist/core/__tests__/on-read-identity.test.js +0 -61
  566. package/dist/core/__tests__/phase4-review-migration.test.d.ts +0 -1
  567. package/dist/core/__tests__/phase4-review-migration.test.js +0 -100
  568. package/dist/core/__tests__/prune-to-limit.test.d.ts +0 -1
  569. package/dist/core/__tests__/prune-to-limit.test.js +0 -126
  570. package/dist/core/__tests__/reset.test.d.ts +0 -1
  571. package/dist/core/__tests__/reset.test.js +0 -69
  572. package/dist/core/__tests__/spawn-no-kickoff.test.d.ts +0 -1
  573. package/dist/core/__tests__/spawn-no-kickoff.test.js +0 -115
  574. package/dist/core/__tests__/steer-note.test.d.ts +0 -1
  575. package/dist/core/__tests__/steer-note.test.js +0 -41
  576. package/dist/core/__tests__/subcommand-tier.test.d.ts +0 -1
  577. package/dist/core/__tests__/subcommand-tier.test.js +0 -99
  578. package/dist/core/__tests__/subscription-delivery.test.js +0 -211
  579. package/dist/core/__tests__/test-runner-cheap-sharding.test.d.ts +0 -1
  580. package/dist/core/__tests__/test-runner-cheap-sharding.test.js +0 -97
  581. package/dist/core/__tests__/tmux-surface.test.js +0 -484
  582. package/dist/core/__tests__/wake-bearings.test.d.ts +0 -1
  583. package/dist/core/__tests__/wake-bearings.test.js +0 -102
  584. package/dist/core/canvas/__tests__/remote-event-stream.test.d.ts +0 -1
  585. package/dist/core/canvas/__tests__/remote-event-stream.test.js +0 -146
  586. package/dist/core/canvas/__tests__/source-resolve.test.d.ts +0 -1
  587. package/dist/core/canvas/__tests__/source-resolve.test.js +0 -77
  588. package/dist/core/canvas/browse/__tests__/model.test.d.ts +0 -1
  589. package/dist/core/canvas/browse/__tests__/model.test.js +0 -290
  590. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.d.ts +0 -1
  591. package/dist/core/canvas/browse/__tests__/rebuild-coalescer.test.js +0 -134
  592. package/dist/core/canvas/browse/__tests__/render.test.d.ts +0 -1
  593. package/dist/core/canvas/browse/__tests__/render.test.js +0 -111
  594. package/dist/core/canvas/browse/rebuild-coalescer.d.ts +0 -7
  595. package/dist/core/canvas/browse/rebuild-coalescer.js +0 -61
  596. package/dist/core/canvas/remote-event-stream.d.ts +0 -24
  597. package/dist/core/canvas/remote-event-stream.js +0 -94
  598. package/dist/core/feed/__tests__/coalesce-human.test.d.ts +0 -1
  599. package/dist/core/feed/__tests__/coalesce-human.test.js +0 -36
  600. package/dist/core/feed/__tests__/empty-node-guard.test.d.ts +0 -1
  601. package/dist/core/feed/__tests__/empty-node-guard.test.js +0 -45
  602. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.d.ts +0 -1
  603. package/dist/core/host-exports/__tests__/export-prunes-boot-skill.test.js +0 -84
  604. package/dist/core/human/__tests__/visible-paths.test.d.ts +0 -1
  605. package/dist/core/human/__tests__/visible-paths.test.js +0 -28
  606. package/dist/core/keybindings/__tests__/inbox-affordance.test.d.ts +0 -1
  607. package/dist/core/keybindings/__tests__/inbox-affordance.test.js +0 -38
  608. package/dist/core/review/__tests__/project.test.d.ts +0 -1
  609. package/dist/core/review/__tests__/project.test.js +0 -71
  610. package/dist/core/runtime/__tests__/command-expansion.test.d.ts +0 -1
  611. package/dist/core/runtime/__tests__/command-expansion.test.js +0 -25
  612. package/dist/core/runtime/__tests__/session-visibility.test.d.ts +0 -1
  613. package/dist/core/runtime/__tests__/session-visibility.test.js +0 -119
  614. package/dist/core/runtime/__tests__/spawn-at-given-id.test.d.ts +0 -1
  615. package/dist/core/runtime/__tests__/spawn-at-given-id.test.js +0 -34
  616. package/dist/core/runtime/tmux.js +0 -1314
  617. package/dist/daemon/api/__tests__/full/push-parity.test.d.ts +0 -1
  618. package/dist/daemon/api/__tests__/full/worktree-parity.test.d.ts +0 -1
  619. package/dist/daemon/api/__tests__/full/worktree-parity.test.js +0 -160
  620. package/dist/daemon/api/__tests__/inbox.test.d.ts +0 -1
  621. package/dist/daemon/api/__tests__/inbox.test.js +0 -489
  622. package/dist/web-client/apple-touch-icon.png +0 -0
  623. package/dist/web-client/assets/fragment-mono-latin-400-normal-BYwT3kSJ.woff +0 -0
  624. package/dist/web-client/assets/fragment-mono-latin-400-normal-yxdJ5AmL.woff2 +0 -0
  625. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-BbKYyvR9.woff2 +0 -0
  626. package/dist/web-client/assets/fragment-mono-latin-ext-400-normal-CT4YFKeK.woff +0 -0
  627. package/dist/web-client/assets/index-CCPaLeW7.css +0 -2
  628. package/dist/web-client/assets/index-kyOWsnZK.js +0 -86
  629. package/dist/web-client/assets/martian-mono-cyrillic-wght-normal-B84CD5C_.woff2 +0 -0
  630. package/dist/web-client/assets/martian-mono-latin-ext-wght-normal-DlL6xMw5.woff2 +0 -0
  631. package/dist/web-client/assets/martian-mono-latin-wght-normal-5W32yIyr.woff2 +0 -0
  632. package/dist/web-client/assets/workbox-window.prod.es5-Bd17z0YL.js +0 -2
  633. package/dist/web-client/icon-192.png +0 -0
  634. package/dist/web-client/icon-512.png +0 -0
  635. package/dist/web-client/icon-maskable-512.png +0 -0
  636. package/dist/web-client/index.html +0 -17
  637. package/dist/web-client/manifest.webmanifest +0 -1
  638. package/dist/web-client/sw.js +0 -1
  639. /package/dist/api/__tests__/{client.test.d.ts → serial/client.test.d.ts} +0 -0
  640. /package/dist/clients/inbox/__tests__/{inbox-controller.test.d.ts → serial/inbox-controller.test.d.ts} +0 -0
  641. /package/dist/clients/inbox/__tests__/{mount-panel.test.d.ts → serial/mount-panel.test.d.ts} +0 -0
  642. /package/dist/clients/{attach/__tests__/attach-chrome-remote.test.d.ts → inbox/review/__tests__/teardown-notice.test.d.ts} +0 -0
  643. /package/dist/{clients/attach/__tests__/attach-keybindings.test.d.ts → core/__tests__/broker-extension-canvas-db-boundary.test.d.ts} +0 -0
  644. /package/dist/{clients/attach/__tests__/crtr-output-coverage.test.d.ts → core/__tests__/seam/broker-attach-multiclient.test.d.ts} +0 -0
  645. /package/dist/core/__tests__/{full → seam}/broker-attach-stream.test.d.ts +0 -0
  646. /package/dist/core/__tests__/{full → seam}/broker-crash-teardown.test.d.ts +0 -0
  647. /package/dist/core/__tests__/{full → seam}/broker-pane-resolution.test.d.ts +0 -0
  648. /package/dist/core/__tests__/{full → seam}/broker-provider-retry.test.d.ts +0 -0
  649. /package/dist/core/__tests__/{full → seam}/broker-whip-interrupt.test.d.ts +0 -0
  650. /package/dist/{clients/attach/__tests__/editor-frame-title.test.d.ts → core/__tests__/seam/dormancy-release.test.d.ts} +0 -0
  651. /package/dist/{clients/attach/__tests__/input-controller-extension-command.test.d.ts → core/__tests__/seam/yield-refresh-transaction.test.d.ts} +0 -0
  652. /package/dist/core/__tests__/{broker-fork-seam.test.d.ts → serial/broker-fork-seam.test.d.ts} +0 -0
  653. /package/dist/core/__tests__/{broker-sdk-wiring.test.d.ts → serial/broker-sdk-wiring.test.d.ts} +0 -0
  654. /package/dist/core/__tests__/{broker-snapshot-history.test.d.ts → serial/broker-snapshot-history.test.d.ts} +0 -0
  655. /package/dist/core/__tests__/{command-plugins.test.d.ts → serial/command-plugins.test.d.ts} +0 -0
  656. /package/dist/core/__tests__/{deferred-no-wake.test.d.ts → serial/deferred-no-wake.test.d.ts} +0 -0
  657. /package/dist/core/__tests__/{flagship-lifecycle.test.d.ts → serial/flagship-lifecycle.test.d.ts} +0 -0
  658. /package/dist/core/__tests__/{host-teardown-process-group.test.d.ts → serial/host-teardown-process-group.test.d.ts} +0 -0
  659. /package/dist/core/__tests__/{human-deliver-e2e.test.d.ts → serial/human-deliver-e2e.test.d.ts} +0 -0
  660. /package/dist/core/__tests__/{live-mutation.test.d.ts → serial/live-mutation.test.d.ts} +0 -0
  661. /package/dist/core/__tests__/{refresh-stall-recycle.test.d.ts → serial/refresh-stall-recycle.test.d.ts} +0 -0
  662. /package/dist/core/__tests__/{revive.test.d.ts → serial/revive.test.d.ts} +0 -0
  663. /package/dist/core/__tests__/{spawn-root.test.d.ts → serial/spawn-root.test.d.ts} +0 -0
  664. /package/dist/core/__tests__/{subscription-delivery.test.d.ts → serial/subscription-delivery.test.d.ts} +0 -0
  665. /package/dist/core/__tests__/{tmux-surface.test.d.ts → serial/tmux-surface.test.d.ts} +0 -0
  666. /package/dist/core/__tests__/{worktree.test.d.ts → serial/worktree.test.d.ts} +0 -0
  667. /package/dist/{clients/attach/__tests__/markdown-headings.test.d.ts → core/__tests__/watchdog-abort-arms-retry.test.d.ts} +0 -0
  668. /package/dist/{clients/attach/__tests__/session-identity-refresh.test.d.ts → core/broker-client/__tests__/transport-socket.test.d.ts} +0 -0
  669. /package/dist/core/human/__tests__/{inbox-core.test.d.ts → serial/inbox-core.test.d.ts} +0 -0
  670. /package/dist/{clients/inbox/review/__tests__/remap.test.d.ts → daemon/api/__tests__/node-model-validation.test.d.ts} +0 -0
  671. /package/dist/{clients/surfaces/region.js → daemon/api/__tests__/review-terminal-conflict.test.d.ts} +0 -0
  672. /package/dist/daemon/api/__tests__/{full → seam}/api-server.test.d.ts +0 -0
  673. /package/dist/daemon/api/__tests__/{full → seam}/b10-attach-modelauth.test.d.ts +0 -0
  674. /package/dist/{clients/web/__tests__/push-engine.test.d.ts → daemon/api/__tests__/seam/leaf-api-parity.test.d.ts} +0 -0
@@ -1,1314 +0,0 @@
1
- // tmux placement — one window per active node.
2
- //
3
- // session = a root window = a node window 0 = optional dashboard
4
- //
5
- // Background windows run but don't render — only the current window draws. That
6
- // is the "detached but switchable" model: nothing tiles, you never see a node's
7
- // UI unless you switch to it. Bring one forefront with select-window (within a
8
- // root) or switch-client + select-window (across roots). done/dead nodes close
9
- // their window; reviving opens a fresh one.
10
- import { execFile, spawnSync } from 'node:child_process';
11
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
12
- import { homedir, tmpdir } from 'node:os';
13
- import { join } from 'node:path';
14
- import { ATTACH_CONTROL_BINDINGS, BINDING_CATALOG, BINDING_IDS, encodeAttachControlInput, formatGesture, isAttachPaneBinding, resolveUserKeybindingSettings, } from '../keybindings/index.js';
15
- import { normalizeTmuxPassthrough } from '../config.js';
16
- import { withExclusiveDirectoryLock } from '../human/convention.js';
17
- import { bundledPiCommandForShell } from './pi-cli.js';
18
- import { surfaceTmuxStyleArgs } from './surface-bg.js';
19
- // ---------------------------------------------------------------------------
20
- // Shell quoting + tmux invocation
21
- // ---------------------------------------------------------------------------
22
- /** POSIX single-quote escaping for one shell word. */
23
- export function shellQuote(s) {
24
- return `'${s.replace(/'/g, `'\\''`)}'`;
25
- }
26
- function tmux(args) {
27
- const r = spawnSync('tmux', args, { encoding: 'utf8' });
28
- return {
29
- ok: r.status === 0,
30
- stdout: (r.stdout ?? '').trim(),
31
- stderr: (r.stderr ?? '').trim(),
32
- };
33
- }
34
- function tmuxOnSocket(socket, args) {
35
- return socket === undefined || socket === '' ? tmux(args) : tmux(['-S', socket, ...args]);
36
- }
37
- export function inTmux() {
38
- return process.env['TMUX'] !== undefined && process.env['TMUX'] !== '';
39
- }
40
- /** The pane currently displaying `nodeId`, found by the authoritative `@crtr_node`
41
- * pane tag — set by EVERY viewer (a bare `surface attach`, a focus split, a
42
- * spawned viewer) on connect. Scans all panes (`list-panes -a`); the reverse of
43
- * `nodeInPane`. Unlike the focus TABLE (which only `registerViewerFocus` writes,
44
- * so it misses attach-viewed nodes), the tag is on any pane showing the node.
45
- * Returns the pane id, or null when no pane shows it. Works headless (no $TMUX). */
46
- export function paneOfNode(nodeId) {
47
- if (nodeId === '')
48
- return null;
49
- const r = tmux(['list-panes', '-a', '-F', '#{pane_id}\t#{@crtr_node}']);
50
- if (!r.ok)
51
- return null;
52
- for (const line of r.stdout.split('\n')) {
53
- const tab = line.indexOf('\t');
54
- if (tab < 0)
55
- continue;
56
- if (line.slice(tab + 1) === nodeId) {
57
- const pane = line.slice(0, tab);
58
- if (pane !== '')
59
- return pane;
60
- }
61
- }
62
- return null;
63
- }
64
- /** Where the caller currently is, or null if not inside tmux. */
65
- export function currentTmux() {
66
- if (!inTmux())
67
- return null;
68
- const r = tmux([
69
- 'display-message',
70
- '-p',
71
- '#{session_name}\t#{window_id}\t#{pane_id}',
72
- ]);
73
- if (!r.ok)
74
- return null;
75
- const [session, window, pane] = r.stdout.split('\t');
76
- return { session, window, pane };
77
- }
78
- // ---------------------------------------------------------------------------
79
- // Sessions + windows
80
- // ---------------------------------------------------------------------------
81
- export function sessionExists(name) {
82
- return tmux(['has-session', '-t', name]).ok;
83
- }
84
- /** Force bracketed-paste capabilities on for every terminal on this tmux server.
85
- *
86
- * Many terminfo entries lack the Enbp/Dsbp (enable/disable bracketed paste)
87
- * capabilities. Without them tmux never asks the outer terminal to wrap pastes,
88
- * so a multi-line paste arrives as raw `\r` and the inner app (pi, zsh, …)
89
- * submits on the first newline — i.e. one paste lands as several messages.
90
- * Declaring the caps for `*` restores bracketed paste so pastes stay atomic.
91
- *
92
- * `terminal-overrides` is a server option, so this is set once per server and
93
- * is idempotent when a valid escaped Enbp capability is already present,
94
- * including a user's own override. Must run only once a server exists —
95
- * set-option errors out with no server running — so callers invoke it AFTER
96
- * ensuring a session. */
97
- function ensureBracketedPaste() {
98
- const current = tmux(['show-options', '-s', 'terminal-overrides']).stdout;
99
- if (/Enbp=\\+E/.test(current))
100
- return;
101
- // Emit a LITERAL backslash-E (`\\E`) so tmux's option parser resolves it to ESC.
102
- // A single `\E` in this JS source collapses to the letter `E`, which stores a
103
- // broken `Enbp=E[?2004h` (no ESC) and silently disables bracketed paste.
104
- tmux(['set-option', '-sga', 'terminal-overrides', '*:Enbp=\\E[?2004h:Dsbp=\\E[?2004l']);
105
- }
106
- /** Server-level tmux prerequisites for crtr's viewer surfaces, applied once a
107
- * server exists (callers invoke via ensureSession). Currently: bracketed paste
108
- * capability (above) and `focus-events on`, which forwards terminal focus
109
- * in/out (DECSET 1004) to the program in each pane — the attach viewer uses it
110
- * to hollow out the editor cursor in blurred panes. */
111
- function ensureServerOptions() {
112
- ensureBracketedPaste();
113
- tmux(['set-option', '-s', 'focus-events', 'on']);
114
- }
115
- /** Create a detached session rooted at `cwd` if it doesn't exist. The session
116
- * name is a root's tmux home; every node under that root is a window in it. */
117
- export function ensureSession(name, cwd) {
118
- if (!sessionExists(name)) {
119
- tmux(['new-session', '-d', '-s', name, '-c', cwd]);
120
- }
121
- ensureServerOptions();
122
- }
123
- function envFlags(env) {
124
- const out = [];
125
- for (const [k, v] of Object.entries(env))
126
- out.push('-e', `${k}=${v}`);
127
- return out;
128
- }
129
- /** Open a background window for a node and run `command` in it. `-d` keeps it
130
- * detached so it doesn't steal focus or become the current window. Returns the
131
- * new window id AND the pane id it created (the durable `%pane_id`, LOCATION's
132
- * anchor) — callers that only need the window destructure `.window`.
133
- *
134
- * Target is `${session}:` (trailing colon = the session, no window index) plus
135
- * `-a` (insert after the current window) so tmux allocates the next free index.
136
- * Passing a bare session name resolves to the session's *active window*, which
137
- * makes new-window try to create AT that occupied index and fail with
138
- * "create window failed: index N in use" whenever the active window is not the
139
- * last one (common when base-index is 0 but the live window sits at index 1).
140
- * `-a` also keeps node windows off index 0, which is reserved for the optional
141
- * dashboard. The explicit `-t ${session}:` target is the §2.2 HARD DRIVER
142
- * INVARIANT — never let new-window fall back to tmux's global current session. */
143
- export function openNodeWindow(opts) {
144
- const r = tmux([
145
- 'new-window',
146
- '-d',
147
- '-a',
148
- '-P',
149
- '-F',
150
- '#{window_id}\t#{pane_id}',
151
- '-t',
152
- `${opts.session}:`,
153
- '-n',
154
- opts.name,
155
- '-c',
156
- opts.cwd,
157
- ...envFlags(opts.env),
158
- opts.command,
159
- ]);
160
- if (!r.ok)
161
- return null;
162
- const [window, pane] = r.stdout.split('\t');
163
- if (window === undefined || window === '' || pane === undefined || pane === '') {
164
- return null;
165
- }
166
- return { window, pane };
167
- }
168
- /** Split `targetPane`'s window, opening a NEW pane beside it running `command`,
169
- * and return the new pane id (the durable `%id`). The ONLY new-pane-beside verb
170
- * (Q3: a focus opened side-by-side). `-d` keeps the caller's pane active; `-h`
171
- * makes the split side-by-side (left/right), the default for a focus viewport.
172
- *
173
- * §2.2 HARD DRIVER INVARIANT: `targetPane` is REQUIRED — a bare `split-window`
174
- * would split tmux's global current pane, which can leak a pane into an
175
- * unrelated user session (the exact bug this design kills). The explicit
176
- * `-t <targetPane>` makes the destination structurally un-leakable. Returns
177
- * null if tmux fails. */
178
- export function splitWindow(targetPane, opts) {
179
- const r = tmux([
180
- 'split-window',
181
- '-d',
182
- ...(opts.vertical === true ? [] : ['-h']),
183
- ...(opts.before === true ? ['-b'] : []),
184
- ...(opts.size !== undefined ? ['-l', String(opts.size)] : []),
185
- '-P',
186
- '-F',
187
- '#{pane_id}',
188
- '-t',
189
- targetPane,
190
- '-c',
191
- opts.cwd,
192
- ...envFlags(opts.env),
193
- opts.command,
194
- ]);
195
- return r.ok && r.stdout !== '' ? r.stdout : null;
196
- }
197
- /** Bring a node's window forefront. Switches client across roots when needed. */
198
- export function focusWindow(session, window) {
199
- const here = currentTmux();
200
- const sameRoot = here?.session === session;
201
- if (!sameRoot) {
202
- if (!tmux(['switch-client', '-t', session]).ok)
203
- return false;
204
- }
205
- return tmux(['select-window', '-t', window]).ok;
206
- }
207
- /** Close a node's window (drop it from the UI). */
208
- export function closeWindow(window) {
209
- return tmux(['kill-window', '-t', window]).ok;
210
- }
211
- /** Rename a window (`tmux rename-window -t <window> <name>`). Used by the
212
- * root relaunch to re-title the viewer window when its pane is re-pointed at
213
- * the freshly-minted node. Best-effort; false if tmux fails. */
214
- export function renameWindow(window, name) {
215
- return tmux(['rename-window', '-t', window, name]).ok;
216
- }
217
- /** Break a single PANE out into a BRAND-NEW window of its own (`tmux break-pane
218
- * -d`). `-d` keeps the new window in the background (does not switch the client
219
- * to it); `-s <pane>` names the source pane — the §2.2 HARD DRIVER INVARIANT, so
220
- * the break never falls back to tmux's global current pane. The pane keeps its
221
- * durable `%id` across the move (only its window changes), so callers can keep
222
- * using the same pane handle. Returns the new `{window, pane}` location, or null
223
- * if tmux fails. Used by `surface tmux-spread` to lift the caller's viewer into a
224
- * fresh window before tiling sibling viewers beside it. */
225
- export function breakPane(pane) {
226
- const r = tmux([
227
- 'break-pane',
228
- '-d',
229
- '-P',
230
- '-F',
231
- '#{window_id}\t#{pane_id}',
232
- '-s',
233
- pane,
234
- ]);
235
- if (!r.ok)
236
- return null;
237
- const [window, p] = r.stdout.split('\t');
238
- if (window === undefined || window === '' || p === undefined || p === '')
239
- return null;
240
- return { window, pane: p };
241
- }
242
- /** Close a single PANE. Its window closes automatically once this was the last
243
- * pane, but sibling panes survive — so co-located nodes (several agents sharing
244
- * one window via swap-pane focus) are torn down one at a time instead of all
245
- * at once by a window kill. Pane ids are the stable vehicle handle; windows
246
- * shift under swap-pane focus, so pane-granular teardown is the correct unit. */
247
- export function closePane(pane) {
248
- return tmux(['kill-pane', '-t', pane]).ok;
249
- }
250
- /** Open a command in a tmux popup anchored to a pane. The command is a shell
251
- * string because tmux executes popup payloads through the user's shell. */
252
- export function displayPopup(pane, cwd, command) {
253
- return tmux(['display-popup', '-E', '-t', pane, '-d', cwd, '-w', '90%', '-h', '90%', command]).ok;
254
- }
255
- /** The active pane id of a window. Node windows are single-pane, so this is the
256
- * node's pane. Returns null if the window is gone or tmux fails. */
257
- export function paneOfWindow(session, window) {
258
- const r = tmux(['display-message', '-p', '-t', `${session}:${window}`, '#{pane_id}']);
259
- return r.ok && r.stdout !== '' ? r.stdout : null;
260
- }
261
- /** The window a pane currently lives in. Used after a swap-pane to learn which
262
- * slot the caller's pane occupied — pane ids are stable across swaps, windows
263
- * are not, so the node→window mapping must be re-derived from the pane. Returns
264
- * null if the pane is gone or tmux fails. */
265
- export function windowOfPane(pane) {
266
- const r = tmux(['display-message', '-p', '-t', pane, '#{window_id}']);
267
- return r.ok && r.stdout !== '' ? r.stdout : null;
268
- }
269
- /** The session + window a pane currently lives in (`display-message -p -t %id`).
270
- * The §2.4 reconciliation read-back: resolve a node's/focus's CURRENT
271
- * window/session from its durable pane id before any act, so crtr follows a
272
- * manual `move-pane`/`join-pane`/`break-pane` instead of fighting it. Null if
273
- * the pane is gone or tmux fails. */
274
- export function paneLocation(pane) {
275
- const r = tmux(['display-message', '-p', '-t', pane, '#{session_name}\t#{window_id}']);
276
- if (!r.ok)
277
- return null;
278
- const [session, window] = r.stdout.split('\t');
279
- if (session === undefined || session === '' || window === undefined || window === '')
280
- return null;
281
- return { session, window };
282
- }
283
- /** Does this pane id still exist? A `display-message` probe on the `%id` — the
284
- * v3 PRIMARY liveness probe (§1.2/§2.2), replacing window-existence so a user
285
- * moving a pane to another window/session never reads as "gone". True iff tmux
286
- * knows the pane.
287
- *
288
- * NOTE: `display-message -p -t <gone-pane>` EXITS 0 with EMPTY output (it does
289
- * not error on an unresolvable pane target) — so an `.ok` check alone would
290
- * report a dead pane as alive, defeating the whole point of pane-existence
291
- * liveness. We therefore require the echoed `#{pane_id}` to equal the requested
292
- * pane: a live pane echoes its own id, a gone/bogus one yields empty. */
293
- export function paneExists(pane) {
294
- const r = tmux(['display-message', '-p', '-t', pane, '#{pane_id}']);
295
- return r.ok && r.stdout === pane;
296
- }
297
- /** Does this pane exist AND have its command still RUNNING (`#{pane_dead}` = 0)?
298
- * Distinguishes a pane genuinely hosting a live process from a remain-on-exit
299
- * corpse frozen after exit (`pane_dead` = 1). Node viewer panes run `crtr surface attach`
300
- * as the pane command, so pane-running ⟹ a live viewer occupies it. */
301
- export function paneRunning(pane) {
302
- const r = tmux(['display-message', '-p', '-t', pane, '#{pane_id}\t#{pane_dead}']);
303
- return r.ok && r.stdout === `${pane}\t0`;
304
- }
305
- /** Every live pane id on the server (`list-panes -a`), as a Set for membership
306
- * probes. Returns null when tmux is unreachable (no server / transient failure)
307
- * so callers can tell "no panes" apart from "can't tell" — a GC pass must skip,
308
- * never mass-delete, on a failed probe. One subprocess call total, so batch
309
- * liveness sweeps (e.g. the daemon's stale-focus GC) don't pay a per-pane
310
- * display-message each. */
311
- export function listLivePanes() {
312
- const r = tmux(['list-panes', '-a', '-F', '#{pane_id}']);
313
- if (!r.ok)
314
- return null;
315
- return new Set(r.stdout.split('\n').filter((p) => p !== ''));
316
- }
317
- /** The working directory of a pane (`display-message -p -t <pane>
318
- * '#{pane_current_path}'`). Null if tmux fails. */
319
- export function paneCurrentPath(pane) {
320
- const r = tmux(['display-message', '-p', '-t', pane, '#{pane_current_path}']);
321
- return r.ok && r.stdout !== '' ? r.stdout : null;
322
- }
323
- /** Flash a transient status-line toast on the client showing `pane` (or the
324
- * current client when `pane` is omitted). `ms` sets the display duration. Used
325
- * by `crtr canvas issue new` to confirm a capture from inside its popup.
326
- * Best-effort; never throws. */
327
- export function displayMessage(message, pane, ms = 4000) {
328
- const args = ['display-message', '-d', String(ms)];
329
- if (pane !== undefined && pane !== '')
330
- args.push('-t', pane);
331
- args.push(message);
332
- return tmux(args).ok;
333
- }
334
- /** Set a PANE-scoped tmux option (`tmux set-option -p -t <pane> <name> <value>`).
335
- * Best-effort; never throws. */
336
- export function setPaneOption(pane, name, value) {
337
- return tmux(['set-option', '-p', '-t', pane, name, value]).ok;
338
- }
339
- /** Read a PANE-scoped tmux option value (`tmux show-options -p -t <pane> -q -v
340
- * <name>`): `-v` prints only the value, `-q` suppresses the unknown-option
341
- * error so an unset option yields an empty string. undefined if tmux fails. */
342
- export function getPaneOption(pane, name) {
343
- const r = tmux(['show-options', '-p', '-t', pane, '-q', '-v', name]);
344
- return r.ok ? r.stdout : undefined;
345
- }
346
- /** Pane option recording when an attach viewer was last started or focused.
347
- * `liveViewerPanes` uses it to select the most recently used viewer. */
348
- export const VIEWER_STAMP_OPTION = '@crtr_viewer_at';
349
- let lastViewerStampMs = 0;
350
- let viewerStampQueue = Promise.resolve();
351
- /** Stamp a viewer pane without blocking the input path that observed focus.
352
- * Stamps are monotonically increasing and their tmux writes are serialized, so
353
- * two rapid focus events cannot leave an older pane with the newer stamp. */
354
- export function stampViewerPaneAsync(pane) {
355
- if (pane === '')
356
- return;
357
- const stampMs = Math.max(Date.now(), lastViewerStampMs + 1);
358
- lastViewerStampMs = stampMs;
359
- viewerStampQueue = viewerStampQueue.then(() => new Promise((resolve) => {
360
- try {
361
- execFile('tmux', ['set-option', '-p', '-t', pane, VIEWER_STAMP_OPTION, String(stampMs)], () => resolve());
362
- }
363
- catch {
364
- resolve();
365
- }
366
- }));
367
- }
368
- function escapeRegex(text) {
369
- return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
370
- }
371
- function looksLikeViewerCommand(command, nodeId) {
372
- const unquoted = command.replace(/^"|"$/g, '');
373
- return new RegExp(`(?:^|\\s)surface\\s+attach\\s+to\\s+${escapeRegex(nodeId)}(?:\\s|$)`).test(unquoted);
374
- }
375
- function readProcessTree() {
376
- const result = spawnSync('ps', ['-ax', '-o', 'pid=,ppid=,command='], { encoding: 'utf8' });
377
- if (result.status !== 0 || typeof result.stdout !== 'string')
378
- return null;
379
- const childrenOf = new Map();
380
- const commandOf = new Map();
381
- for (const line of result.stdout.split('\n')) {
382
- const match = /^\s*(\d+)\s+(\d+)\s+(.*)$/.exec(line);
383
- if (match === null)
384
- continue;
385
- const pid = Number(match[1]);
386
- const parentPid = Number(match[2]);
387
- commandOf.set(pid, match[3]);
388
- const children = childrenOf.get(parentPid) ?? [];
389
- children.push(pid);
390
- childrenOf.set(parentPid, children);
391
- }
392
- return { childrenOf, commandOf };
393
- }
394
- function processTreeHasViewer(tree, rootPid, nodeId) {
395
- const pending = [rootPid];
396
- while (pending.length > 0) {
397
- const pid = pending.pop();
398
- if (looksLikeViewerCommand(tree.commandOf.get(pid) ?? '', nodeId))
399
- return true;
400
- const children = tree.childrenOf.get(pid);
401
- if (children !== undefined)
402
- pending.push(...children);
403
- }
404
- return false;
405
- }
406
- /** List viewer panes tagged by a running local attach viewer. Pane tags can
407
- * survive a killed process, so every tag must match a viewer for the same node.
408
- * Shell-hosted viewers are recognized through their live child process. Unstamped
409
- * valid viewers remain eligible at stamp zero while an older viewer rolls forward. */
410
- export function liveViewerPanes(socket) {
411
- const listed = tmuxOnSocket(socket, [
412
- 'list-panes', '-a', '-F',
413
- '#{pane_id}\t#{session_name}\t#{@crtr_node}\t#{@crtr_viewer_at}',
414
- ]);
415
- if (!listed.ok || listed.stdout === '')
416
- return [];
417
- const viewers = [];
418
- let processTree;
419
- for (const line of listed.stdout.split('\n')) {
420
- const [pane, session, nodeId, stamp] = line.split('\t');
421
- if (pane === undefined || pane === '' || session === undefined || session === '' || nodeId === undefined || nodeId === '')
422
- continue;
423
- const command = tmuxOnSocket(socket, [
424
- 'display-message', '-p', '-t', pane,
425
- '#{pane_current_command}\t#{pane_start_command}\t#{pane_pid}',
426
- ]);
427
- if (!command.ok)
428
- continue;
429
- const [currentCommand = '', startCommand = '', pid] = command.stdout.split('\t');
430
- const directViewer = looksLikeViewerCommand(currentCommand, nodeId) || looksLikeViewerCommand(startCommand, nodeId);
431
- if (!directViewer) {
432
- processTree ??= readProcessTree();
433
- const rootPid = Number.parseInt(pid, 10);
434
- if (processTree === null || !Number.isInteger(rootPid) || rootPid <= 0 || !processTreeHasViewer(processTree, rootPid, nodeId))
435
- continue;
436
- }
437
- const parsedStamp = Number.parseInt(stamp ?? '', 10);
438
- viewers.push({
439
- pane,
440
- session,
441
- nodeId,
442
- stampMs: Number.isSafeInteger(parsedStamp) && parsedStamp >= 0 ? parsedStamp : 0,
443
- });
444
- }
445
- return viewers;
446
- }
447
- /** Deliver one private attach-control sequence literally into a viewer pane.
448
- * `-l` bypasses tmux key-table lookup, so delivery cannot re-trigger a root
449
- * binding that initiated it. */
450
- export function sendAttachControl(socket, pane, actionId) {
451
- return tmuxOnSocket(socket, ['send-keys', '-l', '-t', pane, encodeAttachControlInput(actionId)]).ok;
452
- }
453
- /** Show a tmux status-line message on a specific client when one is known. */
454
- export function displayClientMessage(socket, client, text) {
455
- const args = ['display-message'];
456
- if (client !== undefined && client !== '')
457
- args.push('-c', client);
458
- args.push(text);
459
- return tmuxOnSocket(socket, args).ok;
460
- }
461
- /** The `respawn-pane -k` argv for `opts`. `-k` kills the pane's current process
462
- * (e.g. a yielding pi) and re-execs `command` in the SAME pane, preserving its
463
- * `%id` (§1.5 F3: a frozen focus pane resumes in place, no new window). The
464
- * explicit `-t opts.pane` is the §2.2 HARD DRIVER INVARIANT — respawn must name
465
- * its target pane, never tmux's global current pane. */
466
- function respawnPaneArgs(opts) {
467
- return [
468
- 'respawn-pane',
469
- '-k',
470
- '-c',
471
- opts.cwd,
472
- ...envFlags(opts.env),
473
- '-t',
474
- opts.pane,
475
- opts.command,
476
- ];
477
- }
478
- /** Re-exec a command in an EXISTING pane, in place — SYNCHRONOUS. Runs the
479
- * `respawn-pane` to completion and reports the real exit status. Used when the
480
- * caller is NOT the pane being respawned (e.g. the daemon resuming a frozen
481
- * focus pane), so it can confirm the respawn landed. Returns true on success. */
482
- export function respawnPaneSync(opts) {
483
- return tmux(respawnPaneArgs(opts)).ok;
484
- }
485
- /** The command line a pane was originally started with (`#{pane_start_command}`)
486
- * — the exact command `respawn-pane` with no argument re-runs. undefined when
487
- * tmux is unreachable or the pane is gone. */
488
- export function paneStartCommand(pane) {
489
- const r = tmux(['display-message', '-p', '-t', pane, '#{pane_start_command}']);
490
- return r.ok ? r.stdout : undefined;
491
- }
492
- /** The pane's root process pid (`#{pane_pid}`) — the process tmux launched in
493
- * the pane (the viewer itself, or the login shell hosting it). undefined when
494
- * tmux is unreachable or the pane is gone. */
495
- export function panePid(pane) {
496
- const r = tmux(['display-message', '-p', '-t', pane, '#{pane_pid}']);
497
- if (!r.ok)
498
- return undefined;
499
- const pid = Number.parseInt(r.stdout, 10);
500
- return Number.isInteger(pid) && pid > 0 ? pid : undefined;
501
- }
502
- /** Kill a pane's current process and re-run the SAME command line it was
503
- * started with (`respawn-pane -k` with no command reuses the pane's original
504
- * command). Used to hot-reload a viewer pane in place: the fresh process
505
- * re-resolves `crtr` and picks up the currently selected runtime generation,
506
- * while the pane id, geometry, and the node's broker are untouched. */
507
- export function respawnPaneInPlace(pane) {
508
- return tmux(['respawn-pane', '-k', '-t', pane]).ok;
509
- }
510
- // ---------------------------------------------------------------------------
511
- // pi command assembly
512
- // ---------------------------------------------------------------------------
513
- /** Turn a pi argv array into a single shell command string.
514
- *
515
- * The binary defaults to `CRTR_PI_BINARY` when that env var is set, else the
516
- * bundled pi CLI launched with the current Node binary. CRTR_PI_BINARY is a
517
- * TEST-ONLY substitution seam: the integration-test harness points it at a
518
- * deterministic fake-pi vehicle so a real `crtr node new` reaches the fake
519
- * instead of the LLM pi, without depending on tmux/shell PATH inheritance. An
520
- * explicit `binary` arg still overrides the env. The value may be a multi-word
521
- * launcher (e.g. `node --import tsx/esm host.ts`); only argv entries are
522
- * shell-quoted, so a multi-word binary is spliced verbatim ahead of them. */
523
- export function piCommand(argv, binary = process.env['CRTR_PI_BINARY'] ?? bundledPiCommandForShell(shellQuote)) {
524
- return [binary, ...argv.map(shellQuote)].join(' ');
525
- }
526
- // ---------------------------------------------------------------------------
527
- // Window liveness helpers (used by the supervisor daemon)
528
- // ---------------------------------------------------------------------------
529
- /** List all window ids present in `session`. Returns [] if the session does
530
- * not exist or tmux fails for any reason. Each entry is the raw window id
531
- * string reported by tmux (e.g. `@1`, `@2`, …). */
532
- export function listWindowIds(session) {
533
- const r = tmux(['list-windows', '-t', session, '-F', '#{window_id}']);
534
- if (!r.ok || r.stdout === '')
535
- return [];
536
- return r.stdout.split('\n').filter((s) => s !== '');
537
- }
538
- /** True when both `session` and `window` are present (non-null/undefined) and
539
- * the window currently exists inside the session. False whenever either arg
540
- * is absent, the session is gone, or tmux does not know the window. */
541
- export function windowAlive(session, window) {
542
- if (session == null || window == null)
543
- return false;
544
- return listWindowIds(session).includes(window);
545
- }
546
- // ---------------------------------------------------------------------------
547
- // Focus helpers (used by the placement layer)
548
- // ---------------------------------------------------------------------------
549
- /** Activate a window within its session (same-session navigation). Equivalent
550
- * to `tmux select-window -t <session>:<window>`. Best-effort; never throws. */
551
- export function selectWindow(session, window) {
552
- return tmux(['select-window', '-t', `${session}:${window}`]).ok;
553
- }
554
- /** Apply a named tmux layout to a window (`tmux select-layout -t <window>
555
- * <layout>`). `surface tmux-spread` calls it with `tiled` to evenly grid every
556
- * viewer pane in the spread window (and between splits, to redistribute space so
557
- * the next split has room). Best-effort; false if tmux fails. */
558
- export function selectLayout(window, layout) {
559
- return tmux(['select-layout', '-t', window, layout]).ok;
560
- }
561
- /** Make a pane the ACTIVE pane in its window (`tmux select-pane -t <pane>`). A
562
- * `split-window -d` keeps the CALLER active, so a freshly-opened viewer pane is
563
- * not focused until this runs — `focus` calls it so picking a node (e.g. from the
564
- * alt+g graph) lands the keyboard on the new viewer. Best-effort; false if tmux
565
- * fails. */
566
- export function selectPane(pane) {
567
- return tmux(['select-pane', '-t', pane]).ok;
568
- }
569
- /** Switch the tmux client to a different session (cross-session focus). Runs
570
- * `tmux switch-client -t <session>`. Best-effort; never throws. The caller is
571
- * responsible for following up with selectWindow to land on the right window. */
572
- export function switchClient(session) {
573
- return tmux(['switch-client', '-t', session]).ok;
574
- }
575
- // ---------------------------------------------------------------------------
576
- // send-keys (chrome — used by the prefix menu + nav bindings)
577
- // ---------------------------------------------------------------------------
578
- /** Type a literal (e.g. a `/graph` slash command) into a pane and press Enter
579
- * (`tmux send-keys -t <pane> '<text>' Enter`). Requires the pane's editor be
580
- * empty, same limitation as the menu's `/promote` item. Best-effort. */
581
- export function sendKeysEnter(pane, text) {
582
- return tmux(['send-keys', '-t', pane, text, 'Enter']).ok;
583
- }
584
- // ---------------------------------------------------------------------------
585
- // Crouter-owned tmux bindings
586
- // ---------------------------------------------------------------------------
587
- const TMUX_BINDING_MANIFEST = '@crtr_keybindings_manifest';
588
- const ROOT_TABLE = 'root';
589
- const LEAF_MENU_ACTIONS = {
590
- 'crtr.tmux.menu.promote': { description: 'promote to orchestrator', action: { kind: 'keys', keys: '/promote' } },
591
- 'crtr.tmux.menu.resume': { description: 'find node', action: { kind: 'keys', keys: '/resume-node' } },
592
- 'crtr.tmux.menu.fork': { description: 'fork into idle root', action: { kind: 'direct', run: 'node fork --pane {pane}' } },
593
- 'crtr.tmux.menu.demote': { description: 'demote (finish when done)', action: { kind: 'command', run: 'node lifecycle demote --pane {pane}' } },
594
- 'crtr.tmux.menu.detach': { description: 'demote + close viewer', action: { kind: 'command', run: 'node lifecycle demote --pane {pane} --detach' } },
595
- 'crtr.tmux.menu.close-subtree': { description: 'close agent + subtree', action: { kind: 'command', run: 'node lifecycle close --pane {pane}' } },
596
- 'crtr.tmux.menu.background-bash': { description: 'background running bash', action: { kind: 'direct', run: 'node bash background --pane {pane}' } },
597
- // M-S-r, not M-R: with extended-keys csi-u tmux encodes M-R as alt+uppercase-R
598
- // WITHOUT the shift modifier bit, which pi-tui's `alt+shift+r` matcher rejects;
599
- // M-S-r carries shift+alt and lands as the same chord the keyboard produces.
600
- 'crtr.tmux.menu.file-review': { description: 'review a file', action: { kind: 'raw-keys', keys: 'M-S-r' } },
601
- 'crtr.tmux.menu.metadata': { description: 'metadata', action: { kind: 'keys', keys: '/node-metadata' } },
602
- 'crtr.tmux.menu.fold-tools': { description: 'fold finished tool calls', action: { kind: 'keys', keys: '/fold-tools' } },
603
- 'crtr.tmux.menu.context': { description: 'context + reports', action: { kind: 'keys', keys: '/context' } },
604
- 'crtr.tmux.menu.providers': { description: 'settings', action: { kind: 'popup', run: 'sys setup' } },
605
- 'crtr.tmux.menu.graph': { description: 'canvas graph', action: { kind: 'keys', keys: '/graph' } },
606
- 'crtr.tmux.menu.focus-manager': { description: 'go to manager', action: { kind: 'command', run: 'surface node focus {manager}' } },
607
- 'crtr.tmux.menu.copy-id': { description: 'copy node id', action: { kind: 'command', run: 'surface node id --pane {pane}' } },
608
- 'crtr.tmux.menu.reload-viewer': { description: 'reload viewer', action: { kind: 'direct', run: 'surface reload --pane {pane}' } },
609
- 'crtr.tmux.menu.recap': { description: 'generate recap', action: { kind: 'keys', keys: '/recap' } },
610
- 'crtr.tmux.menu.pin': { description: 'pin conversation', action: { kind: 'direct', run: 'surface node pin --pane {pane}' } },
611
- 'crtr.tmux.menu.issue.idea': { description: 'idea', action: { kind: 'popup', run: 'canvas issue new --type idea' } },
612
- 'crtr.tmux.menu.issue.bug': { description: 'bug', action: { kind: 'popup', run: 'canvas issue new --type bug' } },
613
- };
614
- function escapeTmuxFormats(value) {
615
- return value.replace(/#/g, '##');
616
- }
617
- function tmuxCommand(args) {
618
- return args.map(shellQuote).join(' ');
619
- }
620
- function chordCommand(run, interactive = false) {
621
- const command = `crtr canvas chord --pane '#{pane_id}' --run ${shellQuote(escapeTmuxFormats(run))}${interactive ? ' --interactive' : ''}`;
622
- return interactive
623
- ? {
624
- argv: ['display-popup', '-E', '-w', '60%', '-h', '50%', '-d', '#{pane_current_path}', command],
625
- menu: `display-popup -E -w 60% -h 50% -d "#{pane_current_path}" ${shellQuote(command)}`,
626
- }
627
- : {
628
- argv: ['run-shell', `${command} >/dev/null 2>&1`],
629
- menu: `run-shell ${shellQuote(`${command} >/dev/null 2>&1`)}`,
630
- };
631
- }
632
- /** Dispatch an action straight to a `crtr` invocation, skipping the
633
- * `canvas chord` hop entirely. Only sound for an action whose sole
634
- * interpolation need is `{pane}` — tmux expands it natively as `#{pane_id}` at
635
- * keypress time (a real tmux format, substituted AFTER escaping the static
636
- * run text so the `#` here is never doubled), so chord's node-resolution
637
- * round trip buys nothing. `-b` keeps dispatch non-blocking. */
638
- function directCommand(run) {
639
- const argv = escapeTmuxFormats(run).replace('{pane}', '#{pane_id}');
640
- const command = `crtr ${argv} >/dev/null 2>&1`;
641
- return {
642
- argv: ['run-shell', '-b', command],
643
- menu: `run-shell -b ${shellQuote(command)}`,
644
- };
645
- }
646
- /** One command constructor serves both display-menu selections and direct root
647
- * shortcuts. Each result carries the argv bind-key needs plus the stable menu
648
- * serialization existing Alt+C installs already use. */
649
- function menuActionCommand(action) {
650
- switch (action.kind) {
651
- case 'disabled': return null;
652
- case 'separator': return null;
653
- // Choosing any menu item closes it; refresh-client is the deliberately
654
- // inert command that lets the prefix shortcut choose this close row.
655
- case 'dismiss': return { argv: ['refresh-client'], menu: 'refresh-client' };
656
- case 'keys': {
657
- const keys = escapeTmuxFormats(action.keys);
658
- return {
659
- // A root binding already runs in the pressed key's pane context. Its
660
- // target must stay implicit: bind-key does not expand `#{pane_id}` in
661
- // send-keys' target argument. A menu command is serialized later and
662
- // does need the pane captured when the menu opened.
663
- argv: ['send-keys', keys, 'Enter'],
664
- menu: `send-keys -t '#{pane_id}' ${shellQuote(keys)} Enter`,
665
- };
666
- }
667
- case 'raw-keys': {
668
- const keys = escapeTmuxFormats(action.keys);
669
- return {
670
- argv: ['send-keys', keys],
671
- menu: `send-keys -t '#{pane_id}' ${shellQuote(keys)}`,
672
- };
673
- }
674
- case 'literal-input': {
675
- const input = escapeTmuxFormats(action.input);
676
- return {
677
- argv: ['send-keys', '-l', input],
678
- menu: `send-keys -l -t '#{pane_id}' ${shellQuote(input)}`,
679
- };
680
- }
681
- case 'command': return chordCommand(action.run);
682
- case 'direct': return directCommand(action.run);
683
- case 'popup': return chordCommand(action.run, true);
684
- case 'submenu': {
685
- const argv = buildMenuArgs(action.title, action.items);
686
- return { argv, menu: tmuxCommand(argv) };
687
- }
688
- }
689
- }
690
- function renderMenuAction(action) {
691
- return menuActionCommand(action)?.menu ?? '';
692
- }
693
- /** Visible width of a menu item name: style markup (`#[...]`) takes no cells. */
694
- function menuItemWidth(description) {
695
- return description.replace(/#\[[^\]]*\]/g, '').length;
696
- }
697
- function buildMenuArgs(title, items) {
698
- const safeTitle = escapeTmuxFormats(title);
699
- const widestItem = items.reduce((width, item) => Math.max(width, menuItemWidth(item.description)), 0);
700
- const args = ['display-menu', '-T', `#[align=centre]${safeTitle}`, ...surfaceTmuxStyleArgs()];
701
- args.push('-x', `#{e|-:#{pane_right},${Math.max(safeTitle.length, widestItem) + 7}}`, '-y', '#{e|+:#{pane_top},3}', '--');
702
- for (const item of items) {
703
- // A separator is a single empty name with key and command omitted; tmux
704
- // draws it as a horizontal rule. A header (disabled) name is a crouter-owned
705
- // literal carrying `#[...]` style markup, so it skips format escaping.
706
- if (item.action.kind === 'separator') {
707
- args.push('');
708
- continue;
709
- }
710
- const name = item.action.kind === 'disabled' ? item.description : escapeTmuxFormats(item.description);
711
- args.push(name, item.selector, renderMenuAction(item.action));
712
- }
713
- args.push('', '-esc / q to close', '', '');
714
- return args;
715
- }
716
- const TMUX_NAMED_KEYS = {
717
- enter: 'Enter', escape: 'Escape', tab: 'Tab', backspace: 'BSpace', delete: 'DC',
718
- space: 'Space', up: 'Up', down: 'Down', left: 'Left', right: 'Right',
719
- pageup: 'PPage', pagedown: 'NPage', home: 'Home', end: 'End',
720
- };
721
- function normalizedStrokeParts(stroke) {
722
- if (stroke === '+')
723
- return { key: '+', modifiers: [] };
724
- if (stroke.endsWith('++'))
725
- return { key: '+', modifiers: stroke.slice(0, -2).split('+') };
726
- const parts = stroke.split('+');
727
- return { key: parts.pop(), modifiers: parts };
728
- }
729
- function tmuxKey(stroke) {
730
- let { key, modifiers: modifierParts } = normalizedStrokeParts(stroke);
731
- const modifiers = new Set(modifierParts);
732
- if (modifiers.has('shift') && key.length === 1 && /[a-z]/.test(key)) {
733
- // tmux names a shifted printable with its uppercase character even when an
734
- // extended-key terminal also preserves Shift as a modifier: Alt+Shift+F is
735
- // `M-S-F`. Shift by itself needs only the uppercase character (`F`).
736
- key = key.toUpperCase();
737
- if (!modifiers.has('ctrl') && !modifiers.has('alt'))
738
- modifiers.delete('shift');
739
- }
740
- key = TMUX_NAMED_KEYS[key] ?? key;
741
- const prefix = [
742
- modifiers.has('ctrl') ? 'C-' : '',
743
- modifiers.has('alt') ? 'M-' : '',
744
- modifiers.has('shift') ? 'S-' : '',
745
- ].join('');
746
- return `${prefix}${key}`;
747
- }
748
- function menuSelector(stroke) {
749
- const { key, modifiers } = normalizedStrokeParts(stroke);
750
- return modifiers.includes('shift') && /[a-z]/.test(key) ? key.toUpperCase() : key === 'space' ? ' ' : key;
751
- }
752
- function boundMenuItems(ids, entries, bindings) {
753
- const items = [];
754
- for (const id of ids) {
755
- const entry = entries.get(id);
756
- if (entry === undefined)
757
- continue;
758
- for (const gesture of bindings.gestures(id)) {
759
- items.push({ description: entry.description, selector: menuSelector(gesture), action: entry.action });
760
- }
761
- }
762
- return items;
763
- }
764
- /** Resolve the complete action descriptor set once. Submenu openers point at
765
- * the same child MenuItems the prefix menu renders, so their direct global
766
- * shortcuts cannot drift from the menu path. */
767
- function resolvedMenuActions(bindings) {
768
- const entries = new Map();
769
- for (const [id, entry] of Object.entries(LEAF_MENU_ACTIONS)) {
770
- if (entry !== undefined)
771
- entries.set(id, entry);
772
- }
773
- for (const { menuId, actionId } of ATTACH_CONTROL_BINDINGS) {
774
- const definition = BINDING_CATALOG.find((candidate) => candidate.id === actionId);
775
- if (definition === undefined)
776
- throw new Error(`missing attach binding definition ${actionId}`);
777
- entries.set(menuId, {
778
- description: definition.name.toLowerCase(),
779
- action: { kind: 'literal-input', input: encodeAttachControlInput(actionId) },
780
- });
781
- }
782
- const attachItems = boundMenuItems(ATTACH_CONTROL_BINDINGS.map(({ menuId }) => menuId), entries, bindings);
783
- if (attachItems.length > 0) {
784
- entries.set('crtr.tmux.menu.attach', {
785
- description: 'attach viewer ›',
786
- action: { kind: 'submenu', title: ' attach viewer ', items: attachItems },
787
- });
788
- }
789
- const showItems = boundMenuItems([
790
- 'crtr.tmux.menu.metadata',
791
- 'crtr.tmux.menu.context',
792
- ], entries, bindings);
793
- entries.set('crtr.tmux.menu.show', {
794
- description: 'inspect ›',
795
- action: { kind: 'submenu', title: ' inspect node ', items: showItems },
796
- });
797
- // How this viewer PAINTS the transcript, as opposed to what it shows. Each item
798
- // is a toggle the viewer applies to its own render state (a `/`-command sent to
799
- // the pane); a tmux menu is built once and cannot show live state, so the item
800
- // reads as the action and the viewer answers with a notice.
801
- const viewSettingsItems = boundMenuItems(['crtr.tmux.menu.fold-tools'], entries, bindings);
802
- entries.set('crtr.tmux.menu.view-settings', {
803
- description: 'display ›',
804
- action: { kind: 'submenu', title: ' display ', items: viewSettingsItems },
805
- });
806
- const issueItems = boundMenuItems([
807
- 'crtr.tmux.menu.issue.idea',
808
- 'crtr.tmux.menu.issue.bug',
809
- ], entries, bindings);
810
- entries.set('crtr.tmux.menu.issues', {
811
- description: 'create ›',
812
- action: { kind: 'submenu', title: ' create ', items: issueItems },
813
- });
814
- return entries;
815
- }
816
- function menuItems(bindings, entries) {
817
- const boundItems = (ids) => boundMenuItems(ids, entries, bindings);
818
- const items = [];
819
- const addGroup = (heading, groupItems) => {
820
- if (groupItems.length === 0)
821
- return;
822
- if (items.length > 0)
823
- items.push({ description: '', selector: '', action: { kind: 'separator' } });
824
- items.push({ description: `-#[bold]${heading}`, selector: '', action: { kind: 'disabled' } }, ...groupItems);
825
- };
826
- // Explore changes what is in view or opens a canvas-level browser. Current
827
- // node acts on the agent in this pane without changing its lifecycle.
828
- addGroup('Explore', boundItems([
829
- 'crtr.tmux.menu.resume',
830
- 'crtr.tmux.menu.graph',
831
- 'crtr.tmux.menu.focus-manager',
832
- 'crtr.tmux.menu.show',
833
- 'crtr.tmux.menu.view-settings',
834
- ]));
835
- addGroup('Current node', boundItems([
836
- 'crtr.tmux.menu.attach',
837
- 'crtr.tmux.menu.background-bash',
838
- 'crtr.tmux.menu.file-review',
839
- 'crtr.tmux.menu.pin',
840
- 'crtr.tmux.menu.copy-id',
841
- 'crtr.tmux.menu.reload-viewer',
842
- 'crtr.tmux.menu.recap',
843
- ]));
844
- addGroup('Lifecycle', boundItems([
845
- 'crtr.tmux.menu.fork',
846
- 'crtr.tmux.menu.promote',
847
- 'crtr.tmux.menu.demote',
848
- 'crtr.tmux.menu.detach',
849
- 'crtr.tmux.menu.close-subtree',
850
- ]));
851
- addGroup('System', boundItems([
852
- 'crtr.tmux.menu.providers',
853
- 'crtr.tmux.menu.issues',
854
- ]));
855
- const prefixGestures = bindings.gestures('crtr.tmux.menu.open');
856
- if (prefixGestures.length > 0 && items.length > 0) {
857
- items.push({ description: '', selector: '', action: { kind: 'separator' } });
858
- for (const gesture of prefixGestures) {
859
- items.push({
860
- description: `${formatGesture(gesture)} to close`,
861
- // Menu selectors accept tmux key notation, including modifiers. This
862
- // is intentionally the exact binding that opened the menu.
863
- selector: tmuxKey(gesture),
864
- action: { kind: 'dismiss' },
865
- });
866
- }
867
- }
868
- return items;
869
- }
870
- /** Read one token from tmux's sourceable `list-keys` output. Key names that
871
- * contain punctuation are quoted, so splitting on whitespace would make their
872
- * displayed token differ from the key name accepted by bind/unbind commands. */
873
- function readTmuxToken(line, from) {
874
- let index = from;
875
- while (index < line.length && /\s/.test(line[index]))
876
- index += 1;
877
- if (index >= line.length)
878
- return null;
879
- let quote = null;
880
- let value = '';
881
- while (index < line.length) {
882
- const char = line[index];
883
- if (quote === null && /\s/.test(char))
884
- break;
885
- if (quote === null && (char === "'" || char === '"')) {
886
- quote = char;
887
- index += 1;
888
- continue;
889
- }
890
- if (quote === char) {
891
- quote = null;
892
- index += 1;
893
- continue;
894
- }
895
- if (char === '\\' && quote !== "'" && index + 1 < line.length) {
896
- value += line[index + 1];
897
- index += 2;
898
- continue;
899
- }
900
- value += char;
901
- index += 1;
902
- }
903
- return { value, end: index };
904
- }
905
- function parseTmuxBinding(line) {
906
- const commandName = readTmuxToken(line, 0);
907
- if (commandName?.value !== 'bind-key')
908
- return null;
909
- let cursor = commandName.end;
910
- let repeat = false;
911
- while (true) {
912
- const token = readTmuxToken(line, cursor);
913
- if (token === null)
914
- return null;
915
- cursor = token.end;
916
- if (token.value === '-r') {
917
- repeat = true;
918
- continue;
919
- }
920
- if (token.value !== '-T')
921
- continue;
922
- const table = readTmuxToken(line, cursor);
923
- if (table === null)
924
- return null;
925
- const key = readTmuxToken(line, table.end);
926
- if (key === null)
927
- return null;
928
- return {
929
- table: table.value,
930
- key: key.value,
931
- command: line.slice(key.end).trimStart(),
932
- repeat,
933
- };
934
- }
935
- }
936
- function bindingAt(table, key) {
937
- const result = tmux(['list-keys', '-T', table]);
938
- if (!result.ok)
939
- return null;
940
- return result.stdout.split('\n').find((line) => {
941
- const parsed = parseTmuxBinding(line);
942
- return parsed?.table === table && parsed.key === key;
943
- }) ?? null;
944
- }
945
- /** Effective crouter gestures that tmux's root table currently consumes before
946
- * pane input can see them. One list-keys snapshot serves the whole query. */
947
- export function occupiedTmuxRootGestures(gestures) {
948
- const result = tmux(['list-keys', '-T', ROOT_TABLE]);
949
- if (!result.ok)
950
- return [];
951
- const occupied = new Set(result.stdout.split('\n').flatMap((line) => {
952
- const parsed = parseTmuxBinding(line);
953
- return parsed?.table === ROOT_TABLE ? [parsed.key] : [];
954
- }));
955
- return gestures.filter((gesture) => !gesture.includes(' > ') && occupied.has(tmuxKey(gesture)));
956
- }
957
- function isInstalledPair(value) {
958
- return typeof value === 'object' && value !== null
959
- && typeof value.table === 'string'
960
- && typeof value.key === 'string';
961
- }
962
- function isWrappedRootBinding(value) {
963
- if (typeof value !== 'object' || value === null)
964
- return false;
965
- const wrapper = value;
966
- return typeof wrapper.bindingId === 'string'
967
- && BINDING_IDS.has(wrapper.bindingId)
968
- && typeof wrapper.gesture === 'string'
969
- && typeof wrapper.key === 'string'
970
- && (typeof wrapper.original === 'string' || wrapper.original === null)
971
- && typeof wrapper.wrapper === 'string'
972
- && (wrapper.state === 'owned' || wrapper.state === 'relinquished');
973
- }
974
- function readManifest() {
975
- const option = tmux(['show-options', '-s', '-q', TMUX_BINDING_MANIFEST]);
976
- if (!option.ok)
977
- return null;
978
- if (option.stdout === '')
979
- return { pairs: [], wrappers: [] };
980
- const result = tmux(['show-options', '-s', '-qv', TMUX_BINDING_MANIFEST]);
981
- if (!result.ok)
982
- return null;
983
- try {
984
- const value = JSON.parse(result.stdout);
985
- // Before passthrough wrappers, the manifest was the pair array itself.
986
- if (Array.isArray(value))
987
- return value.every(isInstalledPair) ? { pairs: value, wrappers: [] } : null;
988
- if (typeof value !== 'object' || value === null)
989
- return null;
990
- const manifest = value;
991
- if (!Array.isArray(manifest.pairs) || !manifest.pairs.every(isInstalledPair)
992
- || !Array.isArray(manifest.wrappers) || !manifest.wrappers.every(isWrappedRootBinding))
993
- return null;
994
- return { pairs: manifest.pairs, wrappers: manifest.wrappers };
995
- }
996
- catch {
997
- return null;
998
- }
999
- }
1000
- function writeManifest(manifest) {
1001
- return tmux(['set-option', '-s', TMUX_BINDING_MANIFEST, JSON.stringify(manifest)]);
1002
- }
1003
- /** Tmux parses semicolons once more even when argv already isolated them. */
1004
- function tmuxKeyArgument(key) {
1005
- return key.replace(/\\/g, '\\\\').replace(/;/g, '\\;');
1006
- }
1007
- function tmuxCommandToken(value) {
1008
- return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/;/g, '\\;')}"`;
1009
- }
1010
- /** A top-level separator is printed as `\;` in a bind-key line, but the same
1011
- * sequence is literal after the command tail becomes one if-shell branch. */
1012
- function bindingCommandForBranch(command) {
1013
- let quote = null;
1014
- let out = '';
1015
- for (let index = 0; index < command.length; index += 1) {
1016
- const char = command[index];
1017
- if (quote === null && (char === "'" || char === '"')) {
1018
- quote = char;
1019
- out += char;
1020
- continue;
1021
- }
1022
- if (quote === char) {
1023
- quote = null;
1024
- out += char;
1025
- continue;
1026
- }
1027
- if (char === '\\' && index + 1 < command.length) {
1028
- const next = command[index + 1];
1029
- if (quote === null && next === ';') {
1030
- out += ';';
1031
- }
1032
- else {
1033
- out += char + next;
1034
- }
1035
- index += 1;
1036
- continue;
1037
- }
1038
- out += char;
1039
- }
1040
- return out;
1041
- }
1042
- function sourceBindingLine(line) {
1043
- const dir = mkdtempSync(join(tmpdir(), 'crtr-tmux-binding-'));
1044
- const file = join(dir, 'binding.conf');
1045
- try {
1046
- writeFileSync(file, `${line}\n`, 'utf8');
1047
- return tmux(['source-file', file]);
1048
- }
1049
- finally {
1050
- rmSync(dir, { recursive: true, force: true });
1051
- }
1052
- }
1053
- function restoreWrappedBinding(wrapper) {
1054
- return wrapper.original === null
1055
- ? tmux(['unbind-key', '-T', ROOT_TABLE, tmuxKeyArgument(wrapper.key)])
1056
- : sourceBindingLine(wrapper.original);
1057
- }
1058
- // Crouter's own pre-manifest root bindings. Reconciliation always adopts an
1059
- // untracked binding matching these crouter-specific markers, even when a partial
1060
- // ownership manifest already exists. Older installs could leave M-c outside a
1061
- // later manifest that tracked only newly-added bindings; treating that stale
1062
- // menu as foreign made every future menu update silently skip it.
1063
- const LEGACY_ROOT_BINDINGS = [
1064
- { key: 'M-c', markers: ['display-menu', 'promote to orchestrator', 'crtr canvas chord', 'new issue / todo'] },
1065
- // Later pre-manifest menu generations dropped the 'new issue / todo' row, so
1066
- // they need their own marker set (still crouter-specific via the chord verb).
1067
- { key: 'M-c', markers: ['display-menu', 'promote to orchestrator', 'crtr canvas chord'] },
1068
- { key: 'M-]', markers: ['run-shell', '--dir next', "--pane '#{pane_id}'"] },
1069
- { key: 'M-[', markers: ['run-shell', '--dir prev', "--pane '#{pane_id}'"] },
1070
- { key: 'M-v', markers: ['switch-client', '-T crtr-view'] },
1071
- ];
1072
- // The inbox toggle is a crouter-owned root binding, resolved from keybindings.json
1073
- // and installed through the manifest sweep like every sibling. Root bindings
1074
- // matching these markers that are NOT tracked in the manifest are hl-owned
1075
- // strays; reconciliation removes them (and the state file naming their key) so
1076
- // the manifest stays the sole owner of the inbox command.
1077
- const LEGACY_INBOX_MARKERS = ['run-shell', 'hl inbox toggle'];
1078
- const LEGACY_INBOX_DEFAULT_KEY = 'M-i';
1079
- export function legacyInboxKeyFile() {
1080
- const stateHome = process.env['XDG_STATE_HOME'] || join(homedir(), '.local', 'state');
1081
- return join(stateHome, 'humanloop', 'inbox-key');
1082
- }
1083
- function adoptLegacyBindings() {
1084
- for (const legacy of LEGACY_ROOT_BINDINGS) {
1085
- const existing = bindingAt(ROOT_TABLE, legacy.key);
1086
- if (existing !== null && legacy.markers.every((marker) => existing.includes(marker))) {
1087
- tmux(['unbind-key', '-T', ROOT_TABLE, legacy.key]);
1088
- }
1089
- }
1090
- }
1091
- // Enforce the manifest's sole ownership of the inbox command: any hl-owned
1092
- // inbox binding not tracked in the manifest is unbound, and the humanloop
1093
- // state file is discarded. Runs on every reconciliation regardless of whether
1094
- // a manifest already exists. Checks the default key plus the key the state
1095
- // file records (the file's value only locates a stray binding — it is never
1096
- // imported into crouter config). A key tracked in the manifest is crouter's
1097
- // own and left alone; a foreign non-hl binding never matches the markers and
1098
- // is preserved. Idempotent: with no file and no untracked hl binding, a no-op.
1099
- function removeLegacyInboxBindings(manifestPairs) {
1100
- const owned = new Set(manifestPairs.filter((pair) => pair.table === ROOT_TABLE).map((pair) => pair.key));
1101
- const keys = new Set([LEGACY_INBOX_DEFAULT_KEY]);
1102
- const file = legacyInboxKeyFile();
1103
- try {
1104
- const configured = readFileSync(file, 'utf8').trim();
1105
- if (configured !== '')
1106
- keys.add(configured);
1107
- }
1108
- catch { /* no state file: only the default key can hold a stale binding */ }
1109
- for (const key of keys) {
1110
- if (owned.has(key))
1111
- continue;
1112
- const existing = bindingAt(ROOT_TABLE, key);
1113
- if (existing !== null && LEGACY_INBOX_MARKERS.every((marker) => existing.includes(marker))) {
1114
- tmux(['unbind-key', '-T', ROOT_TABLE, key]);
1115
- }
1116
- }
1117
- try {
1118
- rmSync(file, { force: true });
1119
- }
1120
- catch { /* best-effort */ }
1121
- }
1122
- // --quiet keeps the background `run-shell -b` binding from printing its result JSON, which
1123
- // tmux would otherwise surface as a view-mode overlay on the active pane. The popup opening
1124
- // is the feedback; a genuine failure still prints through the CLI toggle action. This deliberately
1125
- // invokes bare `crtr` from PATH: it is installed once and must resolve to the selected generation,
1126
- // not the sealed generation that installed it. The neighboring `crtr surface node cycle` builders
1127
- // share that property.
1128
- export function inboxToggleTmuxCommand() {
1129
- return ['run-shell', '-b', 'crtr human inbox toggle --quiet --tmux-socket "#{socket_path}" --tmux-client "#{client_name}" --target-pane "#{pane_id}"'];
1130
- }
1131
- function actionCommand(id) {
1132
- switch (id) {
1133
- case 'crtr.tmux.node.next': return ['run-shell', `crtr surface node cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`];
1134
- case 'crtr.tmux.node.previous': return ['run-shell', `crtr surface node cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`];
1135
- case 'crtr.tmux.inbox.toggle': return inboxToggleTmuxCommand();
1136
- default: throw new Error(`no tmux command for ${id}`);
1137
- }
1138
- }
1139
- /** A directory beside the server socket serializes every process driving that
1140
- * exact tmux server. Outside tmux, ask the default server for its socket first;
1141
- * an unavailable server uses one per-user fallback lock for the error path. */
1142
- function tmuxBindingLockPath() {
1143
- const envSocket = process.env['TMUX']?.split(',')[0]?.trim();
1144
- if (envSocket !== undefined && envSocket !== '')
1145
- return `${envSocket}.crtr-keybindings.lock`;
1146
- const result = tmux(['display-message', '-p', '#{socket_path}']);
1147
- if (result.ok && result.stdout !== '')
1148
- return `${result.stdout}.crtr-keybindings.lock`;
1149
- const user = typeof process.getuid === 'function' ? process.getuid() : 'user';
1150
- return join(tmpdir(), `crtr-tmux-keybindings-${user}.lock`);
1151
- }
1152
- /** Reconcile the tmux server with one immutable crouter settings snapshot.
1153
- * Passing snapshots is hermetic; the no-argument front door reads both user
1154
- * settings from the same config read. The server lock covers the entire
1155
- * manifest read, restore, install, fingerprint, and write transition. */
1156
- export function installTmuxBindings(bindingSnapshot, passthroughSnapshot = []) {
1157
- return withExclusiveDirectoryLock(tmuxBindingLockPath(), () => reconcileTmuxBindings(bindingSnapshot, passthroughSnapshot));
1158
- }
1159
- function reconcileTmuxBindings(bindingSnapshot, passthroughSnapshot = []) {
1160
- const userSettings = bindingSnapshot === undefined ? resolveUserKeybindingSettings() : undefined;
1161
- const bindings = bindingSnapshot ?? userSettings.bindings;
1162
- const passthrough = bindingSnapshot === undefined
1163
- ? userSettings.tmuxPassthrough
1164
- : normalizeTmuxPassthrough(passthroughSnapshot);
1165
- const diagnostics = [];
1166
- const installed = [];
1167
- const manifest = readManifest();
1168
- if (manifest === null) {
1169
- diagnostics.push({ code: 'tmux-error', message: `tmux is unavailable or ${TMUX_BINDING_MANIFEST} is malformed` });
1170
- return { ok: false, installed, diagnostics };
1171
- }
1172
- const desiredWrappers = new Map();
1173
- for (const bindingId of passthrough) {
1174
- const definition = BINDING_CATALOG.find((candidate) => candidate.id === bindingId);
1175
- if (definition === undefined || !isAttachPaneBinding(definition))
1176
- continue;
1177
- for (const gesture of bindings.gestures(bindingId)) {
1178
- if (gesture.includes(' > '))
1179
- continue;
1180
- const key = tmuxKey(gesture);
1181
- if (!desiredWrappers.has(key))
1182
- desiredWrappers.set(key, { bindingId, gesture });
1183
- }
1184
- }
1185
- // Restore every wrapper crouter still owns before the ordinary binding sweep.
1186
- // A live line that differs from the fingerprint is a user's later rebind: it
1187
- // is protected from this and every later reconciliation until they toggle the
1188
- // passthrough preference off and on again.
1189
- const protectedRootKeys = new Set();
1190
- const wrappers = [];
1191
- for (const prior of manifest.wrappers) {
1192
- const desired = desiredWrappers.get(prior.key);
1193
- const live = bindingAt(ROOT_TABLE, prior.key);
1194
- if (prior.state === 'relinquished' || live !== prior.wrapper) {
1195
- protectedRootKeys.add(prior.key);
1196
- if (desired !== undefined) {
1197
- wrappers.push({ ...prior, ...desired, state: 'relinquished' });
1198
- }
1199
- continue;
1200
- }
1201
- const restored = restoreWrappedBinding(prior);
1202
- if (!restored.ok) {
1203
- protectedRootKeys.add(prior.key);
1204
- wrappers.push(prior);
1205
- diagnostics.push({
1206
- code: 'tmux-error',
1207
- bindingId: prior.bindingId,
1208
- gesture: prior.gesture,
1209
- message: `could not restore tmux binding ${ROOT_TABLE}:${prior.key}: ${restored.stderr}`,
1210
- });
1211
- }
1212
- }
1213
- adoptLegacyBindings();
1214
- removeLegacyInboxBindings(manifest.pairs);
1215
- for (const pair of manifest.pairs) {
1216
- if (pair.table === ROOT_TABLE && protectedRootKeys.has(pair.key))
1217
- continue;
1218
- const result = tmux(['unbind-key', '-T', pair.table, tmuxKeyArgument(pair.key)]);
1219
- if (!result.ok && bindingAt(pair.table, pair.key) !== null) {
1220
- diagnostics.push({ code: 'tmux-error', message: `could not remove crouter binding ${pair.table}:${pair.key}: ${result.stderr}` });
1221
- }
1222
- }
1223
- if (diagnostics.some((diagnostic) => diagnostic.code === 'tmux-error')) {
1224
- const save = writeManifest({ pairs: manifest.pairs, wrappers });
1225
- if (!save.ok)
1226
- diagnostics.push({ code: 'tmux-error', message: `could not save crouter tmux ownership manifest: ${save.stderr}` });
1227
- return { ok: false, installed, diagnostics };
1228
- }
1229
- const install = (table, key, command, id, gesture) => {
1230
- if (bindingAt(table, key) !== null) {
1231
- diagnostics.push({ code: 'occupied', bindingId: id, gesture, message: `${table}:${key} is already bound by tmux; ${id} was not installed` });
1232
- return false;
1233
- }
1234
- const result = tmux(['bind-key', '-T', table, tmuxKeyArgument(key), ...command]);
1235
- if (!result.ok) {
1236
- diagnostics.push({ code: 'tmux-error', bindingId: id, gesture, message: `could not install ${id}: ${result.stderr}` });
1237
- return false;
1238
- }
1239
- installed.push({ table, key });
1240
- return true;
1241
- };
1242
- const menuActions = resolvedMenuActions(bindings);
1243
- const menuCommand = buildMenuArgs(' crtr ', menuItems(bindings, menuActions));
1244
- for (const gesture of bindings.gestures('crtr.tmux.menu.open')) {
1245
- install(ROOT_TABLE, tmuxKey(gesture), menuCommand, 'crtr.tmux.menu.open', gesture);
1246
- }
1247
- for (const definition of BINDING_CATALOG) {
1248
- if (definition.pair?.field !== 'menu')
1249
- continue;
1250
- const paired = BINDING_CATALOG.find((candidate) => candidate.id === definition.pair?.pairedId);
1251
- if (paired?.pair?.field !== 'global')
1252
- continue;
1253
- const menuId = definition.id;
1254
- const globalId = paired.id;
1255
- const entry = menuActions.get(menuId);
1256
- const command = entry === undefined ? null : menuActionCommand(entry.action);
1257
- if (command === null)
1258
- throw new Error(`no direct tmux action for paired menu binding ${menuId}`);
1259
- for (const gesture of bindings.gestures(globalId)) {
1260
- install(ROOT_TABLE, tmuxKey(gesture), command.argv, globalId, gesture);
1261
- }
1262
- }
1263
- for (const id of ['crtr.tmux.node.next', 'crtr.tmux.node.previous', 'crtr.tmux.inbox.toggle']) {
1264
- for (const gesture of bindings.gestures(id))
1265
- install(ROOT_TABLE, tmuxKey(gesture), actionCommand(id), id, gesture);
1266
- }
1267
- // Wrap only occupied roots. Unoccupied gestures already reach the pane and do
1268
- // not need crouter ownership. Re-read the installed line as the fingerprint;
1269
- // tmux canonicalizes quoting and braces in ways hand serialization cannot.
1270
- const newlyWrapped = [];
1271
- for (const [key, desired] of desiredWrappers) {
1272
- if (protectedRootKeys.has(key))
1273
- continue;
1274
- const original = bindingAt(ROOT_TABLE, key);
1275
- if (original === null)
1276
- continue;
1277
- const parsed = parseTmuxBinding(original);
1278
- if (parsed === null || parsed.command === '') {
1279
- diagnostics.push({ code: 'tmux-error', bindingId: desired.bindingId, gesture: desired.gesture, message: `could not parse occupied tmux binding ${ROOT_TABLE}:${key}` });
1280
- continue;
1281
- }
1282
- const result = tmux([
1283
- 'bind-key',
1284
- ...(parsed.repeat ? ['-r'] : []),
1285
- '-T', ROOT_TABLE, tmuxKeyArgument(key),
1286
- 'if-shell', '-F', '#{!=:#{@crtr_attach},}',
1287
- `send-keys ${tmuxCommandToken(key)}`,
1288
- bindingCommandForBranch(parsed.command),
1289
- ]);
1290
- if (!result.ok) {
1291
- diagnostics.push({ code: 'tmux-error', bindingId: desired.bindingId, gesture: desired.gesture, message: `could not wrap ${ROOT_TABLE}:${key}: ${result.stderr}` });
1292
- continue;
1293
- }
1294
- const wrapper = bindingAt(ROOT_TABLE, key);
1295
- if (wrapper === null || !wrapper.includes('@crtr_attach')) {
1296
- diagnostics.push({ code: 'tmux-error', bindingId: desired.bindingId, gesture: desired.gesture, message: `could not fingerprint wrapped tmux binding ${ROOT_TABLE}:${key}` });
1297
- continue;
1298
- }
1299
- const record = { ...desired, key, original, wrapper, state: 'owned' };
1300
- wrappers.push(record);
1301
- newlyWrapped.push(record);
1302
- }
1303
- const save = writeManifest({ pairs: installed, wrappers });
1304
- if (!save.ok) {
1305
- diagnostics.push({ code: 'tmux-error', message: `could not save crouter tmux ownership manifest: ${save.stderr}` });
1306
- // A wrapper without restoration data is unsafe. Put each newly wrapped key
1307
- // back immediately when manifest persistence fails.
1308
- for (const wrapper of newlyWrapped) {
1309
- if (bindingAt(ROOT_TABLE, wrapper.key) === wrapper.wrapper)
1310
- restoreWrappedBinding(wrapper);
1311
- }
1312
- }
1313
- return { ok: diagnostics.every((diagnostic) => diagnostic.code !== 'tmux-error'), installed, diagnostics };
1314
- }