@crouton-kit/crouter 0.3.27 → 0.3.29

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 (343) hide show
  1. package/dist/build-root.js +4 -2
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  6. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  7. package/dist/builtin-memory/design.md +1 -1
  8. package/dist/builtin-memory/development.md +1 -1
  9. package/dist/builtin-memory/internal/INDEX.md +1 -1
  10. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  12. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  13. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  14. package/dist/builtin-memory/planning.md +1 -1
  15. package/dist/builtin-memory/spec.md +6 -4
  16. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  17. package/dist/builtin-personas/runtime-base.md +8 -4
  18. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  19. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  20. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  21. package/dist/builtin-views/canvas/core.mjs +586 -0
  22. package/dist/builtin-views/canvas/text.mjs +58 -0
  23. package/dist/builtin-views/canvas/tui.mjs +165 -0
  24. package/dist/builtin-views/canvas/web.jsx +120 -0
  25. package/dist/builtin-views/git-pr/core.mjs +673 -0
  26. package/dist/builtin-views/git-pr/text.mjs +84 -0
  27. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  28. package/dist/builtin-views/git-pr/web.jsx +216 -0
  29. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  30. package/dist/builtin-views/inbox/core.mjs +1273 -0
  31. package/dist/builtin-views/inbox/text.mjs +73 -0
  32. package/dist/builtin-views/inbox/tui.mjs +312 -0
  33. package/dist/builtin-views/inbox/web.jsx +188 -0
  34. package/dist/builtin-views/linkedin/core.mjs +906 -0
  35. package/dist/builtin-views/linkedin/text.mjs +69 -0
  36. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  37. package/dist/builtin-views/linkedin/web.jsx +206 -0
  38. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  39. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  40. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  41. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  42. package/dist/cli.js +3 -3
  43. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  44. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  45. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  46. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  47. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  48. package/dist/clients/attach/__tests__/reconnect-giveup.test.js +30 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.d.ts +1 -0
  51. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  52. package/dist/clients/attach/attach-cmd.js +445 -41
  53. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  54. package/dist/clients/attach/auth-pickers.js +194 -0
  55. package/dist/clients/attach/canvas-panels.d.ts +12 -0
  56. package/dist/clients/attach/canvas-panels.js +78 -0
  57. package/dist/clients/attach/chat-view.d.ts +27 -2
  58. package/dist/clients/attach/chat-view.js +98 -9
  59. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  60. package/dist/clients/attach/clipboard-image.js +150 -26
  61. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  62. package/dist/clients/attach/clipboard-text.js +39 -0
  63. package/dist/clients/attach/config-load.d.ts +54 -7
  64. package/dist/clients/attach/config-load.js +99 -3
  65. package/dist/clients/attach/context-message.d.ts +12 -0
  66. package/dist/clients/attach/context-message.js +72 -0
  67. package/dist/clients/attach/git-info.d.ts +20 -0
  68. package/dist/clients/attach/git-info.js +44 -0
  69. package/dist/clients/attach/graph-overlay.d.ts +43 -0
  70. package/dist/clients/attach/graph-overlay.js +306 -0
  71. package/dist/clients/attach/input-controller.d.ts +81 -13
  72. package/dist/clients/attach/input-controller.js +307 -67
  73. package/dist/clients/attach/pickers.d.ts +58 -0
  74. package/dist/clients/attach/pickers.js +171 -0
  75. package/dist/clients/attach/slash-commands.d.ts +44 -1
  76. package/dist/clients/attach/slash-commands.js +250 -27
  77. package/dist/clients/attach/titled-editor.d.ts +33 -0
  78. package/dist/clients/attach/titled-editor.js +84 -0
  79. package/dist/clients/attach/view-socket.d.ts +37 -2
  80. package/dist/clients/attach/view-socket.js +131 -7
  81. package/dist/clients/web/dev-server.d.ts +7 -0
  82. package/dist/clients/web/dev-server.js +59 -0
  83. package/dist/clients/web/events.d.ts +14 -0
  84. package/dist/clients/web/events.js +151 -0
  85. package/dist/clients/web/server.d.ts +18 -0
  86. package/dist/clients/web/server.js +450 -0
  87. package/dist/clients/web/web-cmd.d.ts +2 -0
  88. package/dist/clients/web/web-cmd.js +120 -0
  89. package/dist/commands/canvas.js +1 -2
  90. package/dist/commands/chord.js +1 -1
  91. package/dist/commands/dashboard.js +5 -1
  92. package/dist/commands/human/prompts.js +3 -3
  93. package/dist/commands/human/queue.d.ts +17 -0
  94. package/dist/commands/human/queue.js +111 -4
  95. package/dist/commands/memory/__tests__/lint-schema.test.js +27 -4
  96. package/dist/commands/memory/lint.d.ts +5 -4
  97. package/dist/commands/memory/lint.js +23 -7
  98. package/dist/commands/memory/read.js +2 -2
  99. package/dist/commands/memory/shared.d.ts +7 -1
  100. package/dist/commands/memory/shared.js +18 -3
  101. package/dist/commands/memory/write.js +20 -7
  102. package/dist/commands/memory.js +4 -4
  103. package/dist/commands/node.d.ts +1 -1
  104. package/dist/commands/node.js +64 -65
  105. package/dist/commands/pkg.js +1 -2
  106. package/dist/commands/revive.js +11 -3
  107. package/dist/commands/sys/feedback.d.ts +1 -0
  108. package/dist/commands/sys/feedback.js +163 -0
  109. package/dist/commands/sys/sync.d.ts +1 -0
  110. package/dist/commands/sys/sync.js +187 -0
  111. package/dist/commands/sys.js +4 -2
  112. package/dist/commands/view-cycle.js +2 -2
  113. package/dist/commands/view-list.js +8 -8
  114. package/dist/commands/view-new.js +21 -17
  115. package/dist/commands/view-pick.js +1 -1
  116. package/dist/commands/view-run.js +35 -14
  117. package/dist/commands/view.js +8 -6
  118. package/dist/commands/workspace.d.ts +2 -0
  119. package/dist/commands/workspace.js +161 -0
  120. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  121. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  122. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  123. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  124. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  125. package/dist/core/__tests__/broker-snapshot-history.test.d.ts +1 -0
  126. package/dist/core/__tests__/broker-snapshot-history.test.js +105 -0
  127. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  128. package/dist/core/__tests__/close.test.js +14 -20
  129. package/dist/core/__tests__/context-intro.test.js +19 -19
  130. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  131. package/dist/core/__tests__/draw-style.test.js +1 -1
  132. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  133. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  134. package/dist/core/__tests__/fixtures/fake-engine.d.ts +24 -1
  135. package/dist/core/__tests__/fixtures/fake-engine.js +32 -2
  136. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  137. package/dist/core/__tests__/focuses.test.js +25 -13
  138. package/dist/core/__tests__/fork.test.js +22 -5
  139. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  140. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  141. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  142. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  143. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  144. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  145. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  146. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  147. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  148. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  149. package/dist/core/__tests__/helpers/harness.js +19 -36
  150. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  151. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  152. package/dist/core/__tests__/human-stranded-deliver.test.d.ts +1 -0
  153. package/dist/core/__tests__/human-stranded-deliver.test.js +108 -0
  154. package/dist/core/__tests__/lifecycle.test.js +1 -19
  155. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  156. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  157. package/dist/core/__tests__/on-read-dedup-resume.test.d.ts +1 -0
  158. package/dist/core/__tests__/on-read-dedup-resume.test.js +81 -0
  159. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  160. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  161. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  162. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  163. package/dist/core/__tests__/reset.test.js +19 -79
  164. package/dist/core/__tests__/steer-note.test.js +9 -7
  165. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  166. package/dist/core/bootstrap.d.ts +0 -3
  167. package/dist/core/bootstrap.js +1 -143
  168. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  169. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  170. package/dist/core/canvas/browse/app.js +131 -8
  171. package/dist/core/canvas/browse/model.d.ts +23 -5
  172. package/dist/core/canvas/browse/model.js +68 -13
  173. package/dist/core/canvas/browse/render.d.ts +7 -1
  174. package/dist/core/canvas/browse/render.js +178 -47
  175. package/dist/core/canvas/canvas.js +1 -1
  176. package/dist/core/canvas/focuses.d.ts +4 -4
  177. package/dist/core/canvas/focuses.js +7 -7
  178. package/dist/core/canvas/nav-model.d.ts +162 -0
  179. package/dist/core/canvas/nav-model.js +487 -0
  180. package/dist/core/canvas/paths.d.ts +7 -0
  181. package/dist/core/canvas/paths.js +9 -0
  182. package/dist/core/canvas/render.d.ts +68 -9
  183. package/dist/core/canvas/render.js +195 -61
  184. package/dist/core/canvas/types.d.ts +16 -23
  185. package/dist/core/config.js +1 -4
  186. package/dist/core/memory-resolver.js +43 -5
  187. package/dist/core/runtime/bearings.d.ts +1 -1
  188. package/dist/core/runtime/bearings.js +8 -8
  189. package/dist/core/runtime/branded-host.d.ts +16 -0
  190. package/dist/core/runtime/branded-host.js +127 -0
  191. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  192. package/dist/core/runtime/broker-sdk.d.ts +0 -12
  193. package/dist/core/runtime/broker-sdk.js +77 -6
  194. package/dist/core/runtime/broker.d.ts +5 -1
  195. package/dist/core/runtime/broker.js +520 -59
  196. package/dist/core/runtime/close.d.ts +12 -2
  197. package/dist/core/runtime/close.js +37 -16
  198. package/dist/core/runtime/front-door.js +14 -7
  199. package/dist/core/runtime/host.d.ts +14 -34
  200. package/dist/core/runtime/host.js +14 -50
  201. package/dist/core/runtime/launch.d.ts +12 -9
  202. package/dist/core/runtime/launch.js +27 -15
  203. package/dist/core/runtime/lifecycle.d.ts +1 -1
  204. package/dist/core/runtime/lifecycle.js +15 -19
  205. package/dist/core/runtime/nodes.d.ts +0 -55
  206. package/dist/core/runtime/nodes.js +6 -74
  207. package/dist/core/runtime/placement.d.ts +91 -334
  208. package/dist/core/runtime/placement.js +262 -828
  209. package/dist/core/runtime/promote.d.ts +2 -0
  210. package/dist/core/runtime/promote.js +24 -6
  211. package/dist/core/runtime/recap.d.ts +8 -0
  212. package/dist/core/runtime/recap.js +107 -0
  213. package/dist/core/runtime/recycle.js +25 -61
  214. package/dist/core/runtime/reset.d.ts +43 -37
  215. package/dist/core/runtime/reset.js +131 -218
  216. package/dist/core/runtime/revive.d.ts +9 -29
  217. package/dist/core/runtime/revive.js +45 -135
  218. package/dist/core/runtime/spawn.d.ts +15 -11
  219. package/dist/core/runtime/spawn.js +168 -136
  220. package/dist/core/runtime/surface-bg.d.ts +11 -0
  221. package/dist/core/runtime/surface-bg.js +68 -0
  222. package/dist/core/runtime/tmux.d.ts +15 -43
  223. package/dist/core/runtime/tmux.js +38 -80
  224. package/dist/core/scope.d.ts +1 -2
  225. package/dist/core/scope.js +1 -5
  226. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  227. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  228. package/dist/core/skill-sync/builtins.d.ts +42 -0
  229. package/dist/core/skill-sync/builtins.js +112 -0
  230. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  231. package/dist/core/skill-sync/claude-plugins.js +71 -0
  232. package/dist/core/skill-sync/engine.d.ts +42 -0
  233. package/dist/core/skill-sync/engine.js +633 -0
  234. package/dist/core/skill-sync/export.d.ts +23 -0
  235. package/dist/core/skill-sync/export.js +86 -0
  236. package/dist/core/skill-sync/manifest.d.ts +64 -0
  237. package/dist/core/skill-sync/manifest.js +181 -0
  238. package/dist/core/skill-sync/profile.d.ts +76 -0
  239. package/dist/core/skill-sync/profile.js +173 -0
  240. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  241. package/dist/core/skill-sync/snapshot.js +120 -0
  242. package/dist/core/spawn.d.ts +14 -0
  243. package/dist/core/spawn.js +29 -9
  244. package/dist/core/substrate/index.d.ts +2 -2
  245. package/dist/core/substrate/index.js +7 -7
  246. package/dist/core/substrate/injected-store.d.ts +10 -0
  247. package/dist/core/substrate/injected-store.js +55 -0
  248. package/dist/core/substrate/on-read.js +68 -6
  249. package/dist/core/substrate/render.d.ts +8 -11
  250. package/dist/core/substrate/render.js +29 -43
  251. package/dist/core/substrate/schema.d.ts +16 -11
  252. package/dist/core/substrate/schema.js +32 -31
  253. package/dist/core/tui/host.d.ts +12 -4
  254. package/dist/core/tui/host.js +280 -149
  255. package/dist/core/view/bridge.d.ts +10 -0
  256. package/dist/core/view/bridge.js +31 -0
  257. package/dist/core/view/chrome.d.ts +9 -0
  258. package/dist/core/view/chrome.js +22 -0
  259. package/dist/core/view/contract.d.ts +171 -0
  260. package/dist/core/view/contract.js +23 -0
  261. package/dist/core/view/loader.d.ts +31 -0
  262. package/dist/core/view/loader.js +188 -0
  263. package/dist/core/view/transport-local.d.ts +7 -0
  264. package/dist/core/view/transport-local.js +70 -0
  265. package/dist/core/view/transport.d.ts +4 -0
  266. package/dist/core/view/transport.js +15 -0
  267. package/dist/daemon/crtrd.d.ts +38 -27
  268. package/dist/daemon/crtrd.js +303 -376
  269. package/dist/daemon/manage.js +6 -1
  270. package/dist/index.js +1 -1
  271. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  272. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  273. package/dist/pi-extensions/canvas-doc-substrate.js +29 -20
  274. package/dist/pi-extensions/canvas-nav.js +35 -385
  275. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  276. package/dist/pi-extensions/canvas-recap.js +236 -0
  277. package/dist/pi-extensions/canvas-resume.js +2 -1
  278. package/dist/pi-extensions/canvas-stophook.d.ts +15 -13
  279. package/dist/pi-extensions/canvas-stophook.js +119 -103
  280. package/dist/pi-extensions/canvas-view.js +2 -1
  281. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  282. package/dist/pi-extensions/widget-order-bus.js +34 -0
  283. package/dist/prompts/view.d.ts +2 -2
  284. package/dist/prompts/view.js +145 -73
  285. package/dist/types.d.ts +1 -6
  286. package/dist/types.js +1 -3
  287. package/dist/web/ViewChrome.d.ts +7 -0
  288. package/dist/web/ViewChrome.js +28 -0
  289. package/dist/web/ViewPane.d.ts +39 -0
  290. package/dist/web/ViewPane.js +48 -0
  291. package/dist/web/index.d.ts +6 -0
  292. package/dist/web/index.js +16 -0
  293. package/dist/web/runtime.d.ts +39 -0
  294. package/dist/web/runtime.js +133 -0
  295. package/dist/web/states.d.ts +24 -0
  296. package/dist/web/states.js +24 -0
  297. package/dist/web/transport-http.d.ts +5 -0
  298. package/dist/web/transport-http.js +28 -0
  299. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  300. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  301. package/dist/web-client/index.html +13 -0
  302. package/package.json +20 -6
  303. package/dist/builtin-views/canvas/client.mjs +0 -303
  304. package/dist/builtin-views/canvas/view.mjs +0 -576
  305. package/dist/builtin-views/git-pr/client.mjs +0 -440
  306. package/dist/builtin-views/git-pr/view.mjs +0 -675
  307. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  308. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  309. package/dist/builtin-views/inbox/view.mjs +0 -889
  310. package/dist/builtin-views/linkedin/client.mjs +0 -610
  311. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  312. package/dist/commands/pkg/bridge.d.ts +0 -1
  313. package/dist/commands/pkg/bridge.js +0 -137
  314. package/dist/commands/skill/author.d.ts +0 -3
  315. package/dist/commands/skill/author.js +0 -140
  316. package/dist/commands/skill/shared.d.ts +0 -3
  317. package/dist/commands/skill/shared.js +0 -19
  318. package/dist/commands/skill.d.ts +0 -2
  319. package/dist/commands/skill.js +0 -21
  320. package/dist/commands/tmux-spread.d.ts +0 -2
  321. package/dist/commands/tmux-spread.js +0 -144
  322. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  323. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  324. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  325. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  326. package/dist/core/__tests__/full/placement-teardown.test.js +0 -221
  327. package/dist/core/__tests__/home-session.test.js +0 -186
  328. package/dist/core/__tests__/relaunch.test.js +0 -335
  329. package/dist/core/bridge-map.d.ts +0 -19
  330. package/dist/core/bridge-map.js +0 -73
  331. package/dist/core/tui/contract.d.ts +0 -83
  332. package/dist/core/tui/contract.js +0 -8
  333. package/dist/core/tui/loader.d.ts +0 -16
  334. package/dist/core/tui/loader.js +0 -94
  335. package/dist/prompts/skill.d.ts +0 -2
  336. package/dist/prompts/skill.js +0 -650
  337. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  338. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  339. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  340. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  341. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  342. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/reconnect-giveup.test.d.ts} +0 -0
  343. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
@@ -0,0 +1,163 @@
1
+ // `crtr sys feedback` — file a GitHub issue against the crtr repo when the
2
+ // harness itself misbehaves (a command errors unexpectedly, hangs, churns, or
3
+ // acts contrary to its documented contract). The agent supplies one prose
4
+ // description; everything else (version, node identity, recent activity) is
5
+ // auto-collected and templated into the issue body. gh-less / unauthed hosts
6
+ // fall back to a durable local file so the report is never lost.
7
+ import { spawnSync } from 'node:child_process';
8
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ import os from 'node:os';
11
+ import { defineLeaf } from '../../core/command.js';
12
+ import { usage } from '../../core/errors.js';
13
+ import { userScopeRoot } from '../../core/scope.js';
14
+ import { getNode, jobDir, readTelemetry } from '../../core/canvas/index.js';
15
+ import { readPackageVersion } from './shared.js';
16
+ /** Canonical repo for crtr harness feedback — feedback is always about the
17
+ * tool, regardless of which project the reporting agent is working in. */
18
+ const FEEDBACK_REPO = 'crouton-labs/crouter';
19
+ function collectContext() {
20
+ const env = process.env;
21
+ const nodeId = env['CRTR_NODE_ID'] ?? null;
22
+ let cycle = null;
23
+ let logTail = [];
24
+ if (nodeId !== null) {
25
+ const meta = getNode(nodeId);
26
+ if (meta !== null && typeof meta.cycles === 'number')
27
+ cycle = meta.cycles;
28
+ logTail = readLogTail(nodeId);
29
+ }
30
+ const contextTokens = nodeId !== null ? readTelemetry(nodeId).context_tokens ?? null : null;
31
+ return {
32
+ version: readPackageVersion(),
33
+ platform: `${os.platform()} ${os.release()} (${os.arch()})`,
34
+ nodeId,
35
+ kind: env['CRTR_KIND'] ?? null,
36
+ mode: env['CRTR_MODE'] ?? null,
37
+ lifecycle: env['CRTR_LIFECYCLE'] ?? null,
38
+ cwd: env['CRTR_NODE_CWD'] ?? process.cwd(),
39
+ cycle,
40
+ contextTokens,
41
+ logTail,
42
+ };
43
+ }
44
+ /** Last few lines of a node's job/log.jsonl — best-effort, never throws. Each
45
+ * line is truncated so a single fat event can't blow up the issue body. */
46
+ function readLogTail(nodeId, count = 15) {
47
+ try {
48
+ const path = join(jobDir(nodeId), 'log.jsonl');
49
+ if (!existsSync(path))
50
+ return [];
51
+ return readFileSync(path, 'utf8')
52
+ .split('\n')
53
+ .filter((l) => l.trim() !== '')
54
+ .slice(-count)
55
+ .map((l) => (l.length > 600 ? l.slice(0, 600) + '…' : l));
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
61
+ function deriveTitle(raw, message) {
62
+ const explicit = raw !== undefined && raw.trim() !== '' ? raw.trim() : undefined;
63
+ const base = explicit ?? message.split('\n')[0].trim();
64
+ const trimmed = base.length > 80 ? base.slice(0, 79) + '…' : base;
65
+ return `[feedback] ${trimmed}`;
66
+ }
67
+ function buildBody(message, ctx) {
68
+ const rows = [
69
+ `| crtr version | \`${ctx.version}\` |`,
70
+ `| platform | ${ctx.platform} |`,
71
+ ];
72
+ if (ctx.nodeId !== null) {
73
+ rows.push(`| node id | \`${ctx.nodeId}\` |`);
74
+ rows.push(`| kind / mode / lifecycle | ${ctx.kind ?? '?'} / ${ctx.mode ?? '?'} / ${ctx.lifecycle ?? '?'} |`);
75
+ if (ctx.cycle !== null)
76
+ rows.push(`| cycle | ${ctx.cycle} |`);
77
+ if (ctx.contextTokens !== null)
78
+ rows.push(`| context tokens | ${ctx.contextTokens.toLocaleString('en-US')} |`);
79
+ }
80
+ if (ctx.cwd !== null)
81
+ rows.push(`| working dir | \`${ctx.cwd}\` |`);
82
+ const parts = [
83
+ message.trim(),
84
+ '',
85
+ '---',
86
+ '_Filed via `crtr sys feedback` — context below auto-attached by the harness._',
87
+ '',
88
+ '### Environment',
89
+ '| | |',
90
+ '|---|---|',
91
+ ...rows,
92
+ ];
93
+ if (ctx.logTail.length > 0) {
94
+ parts.push('', `### Recent node activity (last ${ctx.logTail.length} log events)`, '```jsonl', ...ctx.logTail, '```');
95
+ }
96
+ return parts.join('\n') + '\n';
97
+ }
98
+ /** Persist a report locally when gh can't file it, so nothing is lost. Returns
99
+ * the absolute path written. */
100
+ function saveLocally(title, body) {
101
+ const dir = join(userScopeRoot(), 'feedback');
102
+ mkdirSync(dir, { recursive: true });
103
+ const ts = new Date().toISOString().replace(/[:.]/g, '-');
104
+ const path = join(dir, `${ts}.md`);
105
+ writeFileSync(path, `# ${title}\n\n${body}`, 'utf8');
106
+ return path;
107
+ }
108
+ export const sysFeedbackLeaf = defineLeaf({
109
+ name: 'feedback',
110
+ description: 'report a crtr harness bug as a GitHub issue',
111
+ whenToUse: 'a crtr command itself misbehaves — it errors unexpectedly, hangs, churns, double-spawns, or acts contrary to its documented contract. NOT for your task work or decisions a human should make (that is `crtr human ask`); this reports the TOOL, filing an issue against the crtr repo with your node context auto-attached.',
112
+ help: {
113
+ name: 'sys feedback',
114
+ summary: 'file a GitHub issue describing a crtr harness bug, with node context auto-attached',
115
+ params: [
116
+ {
117
+ kind: 'positional',
118
+ name: 'message',
119
+ required: true,
120
+ constraint: 'What broke, in your own words: the command you ran, what you expected, what happened instead. Be concrete — this is the only part a human writes; everything else is auto-collected.',
121
+ },
122
+ {
123
+ kind: 'flag',
124
+ name: 'title',
125
+ type: 'string',
126
+ required: false,
127
+ constraint: 'Issue title. Default: first line of the message, truncated.',
128
+ },
129
+ ],
130
+ output: [
131
+ { name: 'status', type: 'string', required: true, constraint: 'filed | saved-locally.' },
132
+ { name: 'url', type: 'string', required: false, constraint: 'Issue URL when status is filed.' },
133
+ { name: 'path', type: 'string', required: false, constraint: 'Local report path when status is saved-locally (gh unavailable).' },
134
+ { name: 'error', type: 'string', required: false, constraint: 'Why filing fell back to local save.' },
135
+ ],
136
+ outputKind: 'object',
137
+ effects: [
138
+ `Opens a public GitHub issue on ${FEEDBACK_REPO} via gh.`,
139
+ 'On gh failure (missing / unauthenticated), writes the report to ~/.crouter/feedback/ instead — never lost.',
140
+ ],
141
+ },
142
+ run: async (input) => {
143
+ const message = input['message']?.trim();
144
+ if (message === undefined || message === '') {
145
+ throw usage('feedback message is required: describe what broke. Run `crtr sys feedback -h`.');
146
+ }
147
+ const ctx = collectContext();
148
+ const title = deriveTitle(input['title'], message);
149
+ const body = buildBody(message, ctx);
150
+ const res = spawnSync('gh', ['issue', 'create', '--repo', FEEDBACK_REPO, '--title', title, '--body-file', '-'], { input: body, encoding: 'utf8' });
151
+ if (res.error === undefined && res.status === 0) {
152
+ const url = res.stdout.trim().split('\n').filter((l) => l.startsWith('http')).pop() ?? res.stdout.trim();
153
+ return { status: 'filed', url };
154
+ }
155
+ // gh missing, unauthenticated, or the API call failed — persist locally so
156
+ // the report survives, and tell the agent how to escalate.
157
+ const reason = res.error !== undefined
158
+ ? `gh not available: ${res.error.message}`
159
+ : (res.stderr?.trim() || `gh exited ${res.status}`);
160
+ const path = saveLocally(title, body);
161
+ return { status: 'saved-locally', path, error: reason };
162
+ },
163
+ });
@@ -0,0 +1 @@
1
+ export declare const sysSyncLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,187 @@
1
+ // `crtr sys sync` — the SOLE entry point for bidirectional crtr ↔ Claude skill
2
+ // sync (R-U1). Loads the layered manifest (P2), reconciles each enrolled pair
3
+ // once via the direction-agnostic engine (P6), summarizes the outcome, and
4
+ // sets the exit code: non-zero IFF any pair conflicted (R-E7). A malformed
5
+ // manifest (R-U7) or an unknown `--pair` id (OD-3) aborts hard before any
6
+ // reconcile runs — no lenient fallback, no partial write.
7
+ import { defineLeaf } from '../../core/command.js';
8
+ import { usage } from '../../core/errors.js';
9
+ import { emit, isJsonOutput } from '../../core/io.js';
10
+ import { ExitCode } from '../../types.js';
11
+ import { readLayeredManifest } from '../../core/skill-sync/manifest.js';
12
+ import { resolveProfile } from '../../core/skill-sync/profile.js';
13
+ import { reconcilePair } from '../../core/skill-sync/engine.js';
14
+ /** Render the per-pair status table + summary line as agent-ready markdown.
15
+ * Shown for both the clean and the conflict exit so the caller always sees the
16
+ * full partition. */
17
+ function renderSync(results, summary, dryRun) {
18
+ const parts = [];
19
+ parts.push(dryRun
20
+ ? 'crtr ↔ Claude skill sync — DRY RUN (nothing written):'
21
+ : 'crtr ↔ Claude skill sync:');
22
+ if (results.length === 0) {
23
+ parts.push('No enrolled pairs.');
24
+ }
25
+ else {
26
+ const rows = results
27
+ .map((r) => `| ${r.id} | ${r.status} | ${r.wrote} |`)
28
+ .join('\n');
29
+ parts.push(`| pair | status | wrote |\n| --- | --- | --- |\n${rows}`);
30
+ }
31
+ parts.push(`synced: ${summary.synced.length} · conflicts: ${summary.conflicts.length} · ` +
32
+ `noop: ${summary.noop.length} · files written: ${summary.wrote}`);
33
+ if (summary.conflicts.length > 0) {
34
+ parts.push(`Conflicts (NOT written): ${summary.conflicts.join(', ')}. ` +
35
+ 'Each conflicting pair has a git-style report under ' +
36
+ '`~/.crouter/skill-sync/conflicts/<id>.md` — resolve the endpoints by hand, then re-run.');
37
+ }
38
+ return parts.join('\n\n');
39
+ }
40
+ /** Drift label for the read-only `--status` view, derived from a dry-run
41
+ * reconcile: `noop` = already in agreement, `synced` = a clean merge the engine
42
+ * WOULD write, `conflict` = diverged and blocked. These are the only three
43
+ * statuses `reconcilePair` returns. */
44
+ const DRIFT = {
45
+ noop: 'in-sync',
46
+ synced: 'would-write',
47
+ conflict: 'conflict',
48
+ };
49
+ /** Render one endpoint as `scope/name` (or `<scope>:<plugin>/<name>` for a
50
+ * plugin-scoped side) so a reader sees where each half of the pair lives. */
51
+ function endpointLabel(ep) {
52
+ return ep.scope === 'plugin' || ep.scope === 'claude-plugin'
53
+ ? `${ep.scope}:${ep.plugin}/${ep.name}`
54
+ : `${ep.scope}/${ep.name}`;
55
+ }
56
+ /** Render the read-only `--status` inventory: every selected pair with its
57
+ * crtr↔Claude endpoints and drift state, computed via a dry-run reconcile so
58
+ * nothing is written. `results` is keyed back to `pairs` by id. */
59
+ function renderStatus(pairs, results) {
60
+ const parts = [];
61
+ parts.push('crtr ↔ Claude skill-sync pairs (read-only; nothing written):');
62
+ if (pairs.length === 0) {
63
+ parts.push('No enrolled pairs.');
64
+ return parts.join('\n\n');
65
+ }
66
+ const byId = new Map(results.map((r) => [r.id, r]));
67
+ const rows = pairs
68
+ .map((p) => {
69
+ const drift = DRIFT[byId.get(p.id).status];
70
+ return `| ${p.id} | ${endpointLabel(p.crtr)} | ${endpointLabel(p.claude)} | ${drift} |`;
71
+ })
72
+ .join('\n');
73
+ parts.push(`| pair | crtr | claude | drift |\n| --- | --- | --- | --- |\n${rows}`);
74
+ const inSync = results.filter((r) => r.status === 'noop').length;
75
+ const wouldWrite = results.filter((r) => r.status === 'synced').length;
76
+ const conflict = results.filter((r) => r.status === 'conflict').length;
77
+ parts.push(`in-sync: ${inSync} · would-write: ${wouldWrite} · conflict: ${conflict}`);
78
+ return parts.join('\n\n');
79
+ }
80
+ export const sysSyncLeaf = defineLeaf({
81
+ name: 'sync',
82
+ description: 'reconcile crtr ↔ Claude skill pairs (bidirectional)',
83
+ whenToUse: 'you want enrolled crtr skills and their Claude SKILL.md counterparts brought back into agreement. Performs the single, direction-agnostic 3-way reconcile over every pair in the skill-sync manifest (or just one via --pair), writing both sides + the merge base when clean and a conflict report when not.',
84
+ help: {
85
+ name: 'sys sync',
86
+ summary: 'reconcile enrolled crtr ↔ Claude skill pairs (bidirectional 3-way merge)',
87
+ params: [
88
+ {
89
+ kind: 'flag',
90
+ name: 'pair',
91
+ type: 'string',
92
+ required: false,
93
+ constraint: 'Reconcile only the pair with this id. Unknown id → hard error naming it + the valid ids, nothing written. Default: every enrolled pair.',
94
+ },
95
+ {
96
+ kind: 'flag',
97
+ name: 'dry-run',
98
+ type: 'bool',
99
+ required: false,
100
+ constraint: 'Compute every merge and report the would-be status, but write nothing — no endpoint, snapshot, or conflict report touched.',
101
+ },
102
+ {
103
+ kind: 'flag',
104
+ name: 'status',
105
+ type: 'bool',
106
+ required: false,
107
+ constraint: 'Read-only inventory mode: instead of reconciling, list every enrolled pair (honoring --pair) with its crtr↔Claude endpoints and drift state (in-sync / would-write / conflict), computed via a dry-run reconcile. Writes nothing and never exits non-zero on drift; a malformed manifest or unknown --pair id still aborts hard.',
108
+ },
109
+ ],
110
+ output: [
111
+ { name: 'synced', type: 'string[]', required: true, constraint: 'Pair ids written through cleanly to both endpoints + the merge base.' },
112
+ { name: 'conflicts', type: 'string[]', required: true, constraint: 'Pair ids that diverged and were NOT written; each has a report under ~/.crouter/skill-sync/conflicts/<id>.md. Non-empty → exit non-zero.' },
113
+ { name: 'noop', type: 'string[]', required: true, constraint: 'Pair ids already in agreement — nothing to do.' },
114
+ { name: 'wrote', type: 'number', required: true, constraint: 'Total endpoint files created/updated across all pairs (0 under --dry-run).' },
115
+ ],
116
+ outputKind: 'object',
117
+ effects: [
118
+ 'For each clean pair: writes the merged SKILL.md + assets to BOTH the crtr and Claude endpoints and updates the per-pair merge snapshot under ~/.crouter/skill-sync/snapshots/<id>/.',
119
+ 'For each conflicting pair: writes NOTHING to the endpoints; writes a git-style conflict report to ~/.crouter/skill-sync/conflicts/<id>.md.',
120
+ 'Exits non-zero iff any pair conflicted. A malformed manifest or an unknown --pair id aborts hard (non-zero) before any reconcile and writes nothing.',
121
+ 'With --dry-run: read-only — computes every merge and the would-be summary, touches no endpoint, snapshot, or report.',
122
+ 'With --status: read-only — emits the pair inventory + drift table (in-sync / would-write / conflict) instead of reconciling; writes nothing and exits zero regardless of drift.',
123
+ ],
124
+ },
125
+ run: async (input) => {
126
+ const pairFilter = input['pair'];
127
+ const dryRun = input['dryRun'] ?? false;
128
+ const statusMode = input['status'] ?? false;
129
+ // Malformed manifest throws here (R-U7) → dispatcher's handle() → non-zero.
130
+ const { pairs } = readLayeredManifest();
131
+ let selected = pairs;
132
+ if (pairFilter !== undefined) {
133
+ selected = pairs.filter((p) => p.id === pairFilter);
134
+ if (selected.length === 0) {
135
+ // OD-3 / R-O2 — unknown id aborts hard, naming the bad id + valid ids,
136
+ // nothing written.
137
+ const valid = pairs.map((p) => p.id);
138
+ throw usage(`unknown pair id: ${pairFilter}`, {
139
+ received: pairFilter,
140
+ next: `Valid pair ids: ${valid.length > 0 ? valid.join(', ') : '(none)'}. Run \`crtr sys sync -h\`.`,
141
+ });
142
+ }
143
+ }
144
+ // --status: read-only inventory. Run the same engine in dry-run mode and
145
+ // render endpoints + drift; never write, never exit non-zero on drift.
146
+ if (statusMode) {
147
+ const results = selected.map((p) => reconcilePair(p, resolveProfile(p.frontmatter), { dryRun: true }));
148
+ if (isJsonOutput()) {
149
+ emit({
150
+ pairs: selected.map((p, i) => ({
151
+ id: p.id,
152
+ crtr: p.crtr,
153
+ claude: p.claude,
154
+ drift: DRIFT[results[i].status],
155
+ })),
156
+ });
157
+ }
158
+ else {
159
+ process.stdout.write(renderStatus(selected, results) + '\n');
160
+ }
161
+ process.exit(ExitCode.SUCCESS);
162
+ }
163
+ // One conflict must NOT abort the loop (R-X3): collect every pair's result,
164
+ // partition, then decide the exit code once. A structural error (an
165
+ // unresolvable endpoint, both sides missing) is a hard config fault and is
166
+ // allowed to propagate — only content conflicts are a collected outcome.
167
+ const results = [];
168
+ for (const pair of selected) {
169
+ const profile = resolveProfile(pair.frontmatter);
170
+ results.push(reconcilePair(pair, profile, { dryRun }));
171
+ }
172
+ const summary = {
173
+ synced: results.filter((r) => r.status === 'synced').map((r) => r.id),
174
+ conflicts: results.filter((r) => r.status === 'conflict').map((r) => r.id),
175
+ noop: results.filter((r) => r.status === 'noop').map((r) => r.id),
176
+ wrote: results.reduce((n, r) => n + r.wrote, 0),
177
+ };
178
+ if (isJsonOutput()) {
179
+ emit(summary);
180
+ }
181
+ else {
182
+ process.stdout.write(renderSync(results, summary, dryRun) + '\n');
183
+ }
184
+ // R-E7 — exit non-zero IFF a conflict was surfaced.
185
+ process.exit(summary.conflicts.length > 0 ? ExitCode.GENERAL : ExitCode.SUCCESS);
186
+ },
187
+ });
@@ -3,19 +3,21 @@
3
3
  import { defineBranch } from '../core/command.js';
4
4
  import { configBranch } from './sys/config.js';
5
5
  import { sysDoctorLeaf } from './sys/doctor.js';
6
+ import { sysFeedbackLeaf } from './sys/feedback.js';
7
+ import { sysSyncLeaf } from './sys/sync.js';
6
8
  import { sysUpdateLeaf, sysVersionLeaf } from './sys/update.js';
7
9
  export function registerSys() {
8
10
  return defineBranch({
9
11
  name: 'sys',
10
12
  rootEntry: {
11
13
  concept: 'crtr configuration, diagnostics, and self-management',
12
- desc: 'config, doctor, update, version',
14
+ desc: 'config, doctor, sync, update, version, feedback',
13
15
  useWhen: 'managing the crtr installation',
14
16
  },
15
17
  help: {
16
18
  name: 'sys',
17
19
  summary: 'crtr system configuration, diagnostics, and self-management',
18
20
  },
19
- children: [configBranch, sysDoctorLeaf, sysUpdateLeaf, sysVersionLeaf],
21
+ children: [configBranch, sysDoctorLeaf, sysFeedbackLeaf, sysSyncLeaf, sysUpdateLeaf, sysVersionLeaf],
20
22
  });
21
23
  }
@@ -15,7 +15,7 @@
15
15
  // tmux-only; a pane with no @crtr_view tag (not a view monitor) is a no-op.
16
16
  // Output is discarded by the keybinding, so this just acts.
17
17
  import { defineLeaf } from '../core/command.js';
18
- import { listViews, resolveView, loadView } from '../core/tui/loader.js';
18
+ import { listViews, resolveView, loadCore } from '../core/view/loader.js';
19
19
  // Commands reach the tmux driver through placement.ts (the sanctioned
20
20
  // model-over-driver seam, §5.1) — never `./tmux.js` directly.
21
21
  import { inTmux, currentTmux, getPaneOption, setPaneOption, paneCurrentPath, respawnPaneSync, piCommand, } from '../core/runtime/placement.js';
@@ -82,7 +82,7 @@ export const viewCycleLeaf = defineLeaf({
82
82
  if (rv === null)
83
83
  continue;
84
84
  try {
85
- await loadView(rv);
85
+ await loadCore(rv);
86
86
  targetId = cand;
87
87
  break;
88
88
  }
@@ -1,11 +1,11 @@
1
1
  // `crtr view list` — enumerate every resolvable view across scopes.
2
2
  //
3
- // Lists id · title · description · scope. Loads each view to read its manifest
4
- // title/description; a malformed view is shown with an error marker (it never
5
- // crashes the listing). Plain text — pipeable as-is. The raw array is available
6
- // behind --json for tooling.
3
+ // Lists id · title · description · scope. Loads each view's core.mjs to read its
4
+ // manifest title/description; a malformed view is shown with an error marker (it
5
+ // never crashes the listing). Plain text — pipeable as-is. The raw array is
6
+ // available behind --json for tooling.
7
7
  import { defineLeaf } from '../core/command.js';
8
- import { listViews, loadView } from '../core/tui/loader.js';
8
+ import { listViews, loadCore } from '../core/view/loader.js';
9
9
  export const viewListLeaf = defineLeaf({
10
10
  name: 'list',
11
11
  description: 'enumerate available views (id · title · description · scope)',
@@ -24,11 +24,11 @@ export const viewListLeaf = defineLeaf({
24
24
  const rows = [];
25
25
  for (const r of listViews()) {
26
26
  try {
27
- const v = await loadView(r);
27
+ const core = await loadCore(r);
28
28
  rows.push({
29
29
  id: r.id,
30
- title: v.manifest.title,
31
- description: v.manifest.description ?? '',
30
+ title: core.manifest.title,
31
+ description: core.manifest.description ?? '',
32
32
  scope: r.scope,
33
33
  });
34
34
  }
@@ -1,24 +1,25 @@
1
- // `crtr view new <name>` — scaffold a runnable view.mjs into a scope dir.
1
+ // `crtr view new <name>` — scaffold a runnable dual-target view into a scope dir.
2
2
  //
3
- // Writes <viewsDir(scope)>/<name>/view.mjs from the stub in prompts/view.ts
4
- // (mkdir -p first). Refuses if a view of that name already resolves. The
5
- // scaffolded view runs as-is via `crtr view run <name>`. Mirrors how skill
6
- // authoring scaffolds (resolve scope ensure dir write point at how to run).
3
+ // Writes the four contract files (core.mjs + tui.mjs + web.jsx + text.mjs) under
4
+ // <viewsDir(scope)>/<name>/ from the stubs in prompts/view.ts (mkdir -p first).
5
+ // Refuses if a view of that name already resolves. The scaffold runs as-is via
6
+ // `crtr view run <name>` (TUI) and `crtr view serve <name>` (web). Mirrors how
7
+ // skill authoring scaffolds (resolve scope → ensure dir → write → point at run).
7
8
  import { mkdirSync, existsSync } from 'node:fs';
8
9
  import { writeFileSync } from 'node:fs';
9
10
  import { join } from 'node:path';
10
11
  import { defineLeaf } from '../core/command.js';
11
12
  import { usage } from '../core/errors.js';
12
- import { resolveView } from '../core/tui/loader.js';
13
+ import { resolveView, findViewDir } from '../core/view/loader.js';
13
14
  import { viewsDir, projectScopeRoot, ensureProjectScopeRoot } from '../core/scope.js';
14
15
  import { viewScaffold } from '../prompts/view.js';
15
16
  export const viewNewLeaf = defineLeaf({
16
17
  name: 'new',
17
- description: 'scaffold a runnable view.mjs stub into a scope dir',
18
- whenToUse: 'you want to author a new view — this writes a minimal, runnable view.mjs (with the contract + draw/host API documented inline) into the user or project scope, ready to open immediately with `crtr view run <name>` and to edit from there. Reach for it to start a new TUI surface rather than hand-writing the module',
18
+ description: 'scaffold a runnable dual-target view directory into a scope dir',
19
+ whenToUse: 'you want to author a new view — this writes the four contract files (core.mjs + tui.mjs + web.jsx + text.mjs, with the contract documented inline) into the user or project scope, ready to open immediately with `crtr view run <name>` (TUI) or `crtr view serve <name>` (web) and to edit from there. Reach for it to start a new view rather than hand-writing the directory',
19
20
  help: {
20
21
  name: 'view new',
21
- summary: 'scaffold <viewsDir(scope)>/<name>/view.mjs from the stub; refuses if the name already resolves',
22
+ summary: 'scaffold <viewsDir(scope)>/<name>/{core,tui,text}.mjs + web.jsx from the stubs; refuses if the name already resolves',
22
23
  params: [
23
24
  { kind: 'positional', name: 'name', required: true, constraint: 'View id / directory name. Must not already resolve as a view.' },
24
25
  { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Where to scaffold. Default: project if available, else user.' },
@@ -26,13 +27,14 @@ export const viewNewLeaf = defineLeaf({
26
27
  { kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Manifest description (picker + `view list`). Default: empty.' },
27
28
  ],
28
29
  output: [
29
- { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded view.mjs.' },
30
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded view directory.' },
31
+ { name: 'files', type: 'array', required: true, constraint: 'The filenames written into the view directory.' },
30
32
  { name: 'scope', type: 'string', required: true, constraint: 'The scope it was written to (user|project).' },
31
33
  { name: 'run', type: 'string', required: true, constraint: 'The command to run the new view.' },
32
34
  ],
33
35
  outputKind: 'object',
34
36
  effects: [
35
- 'Creates the view directory and view.mjs stub at the resolved location (mkdir -p).',
37
+ 'Creates the view directory and the four contract files (core.mjs + tui.mjs + web.jsx + text.mjs) at the resolved location (mkdir -p).',
36
38
  'Initializes the project scope dir if --scope project is chosen and none exists yet.',
37
39
  ],
38
40
  },
@@ -48,7 +50,7 @@ export const viewNewLeaf = defineLeaf({
48
50
  // Refuse if a view of this name already resolves (any scope wins).
49
51
  const existing = resolveView(name);
50
52
  if (existing !== null) {
51
- throw usage(`view already exists: ${name} (${existing.scope}) at ${existing.entry}. ` +
53
+ throw usage(`view already exists: ${name} (${existing.scope}) at ${existing.dir}. ` +
52
54
  `Pick another name, or edit it directly.`);
53
55
  }
54
56
  // Ensure the project scope root exists before computing its views dir.
@@ -59,12 +61,14 @@ export const viewNewLeaf = defineLeaf({
59
61
  throw usage(`no ${scope} scope available to scaffold into — try --scope user.`);
60
62
  }
61
63
  const dir = join(baseDir, name);
62
- const entry = join(dir, 'view.mjs');
63
- if (existsSync(entry)) {
64
- throw usage(`view file already exists: ${entry}`);
64
+ if (findViewDir(name) !== null || existsSync(join(dir, 'core.mjs'))) {
65
+ throw usage(`view directory already exists: ${dir}`);
65
66
  }
66
67
  mkdirSync(dir, { recursive: true });
67
- writeFileSync(entry, viewScaffold({ id: name, title, description }), 'utf8');
68
- return { path: entry, scope, run: `crtr view run ${name}` };
68
+ const files = viewScaffold({ id: name, title, description });
69
+ for (const [filename, content] of Object.entries(files)) {
70
+ writeFileSync(join(dir, filename), content, 'utf8');
71
+ }
72
+ return { path: dir, files: Object.keys(files), scope, run: `crtr view run ${name}` };
69
73
  },
70
74
  });
@@ -7,7 +7,7 @@
7
7
  // plain list and exits 0. Reuses the core/tui terminal + draw primitives.
8
8
  import { execFileSync } from 'node:child_process';
9
9
  import { defineLeaf } from '../core/command.js';
10
- import { listViews } from '../core/tui/loader.js';
10
+ import { listViews } from '../core/view/loader.js';
11
11
  import { setupTerminal, restoreTerminal, getTerminalSize, parseKeypress, } from '../core/tui/terminal.js';
12
12
  import { createDraw, detectColorCaps } from '../core/tui/draw.js';
13
13
  export const viewPickLeaf = defineLeaf({
@@ -1,9 +1,11 @@
1
- // `crtr view run <name>` — resolve, load, and host a view.
1
+ // `crtr view run <name>` — resolve, load, and host a view's TUI target.
2
2
  //
3
- // Resolves the view across scopes (project→user→builtin), dynamically imports
4
- // it, then hands it to the core/tui host. The host owns the screen and the
5
- // non-TTY path: when stdin is NOT a TTY it prints view.dump(state) and exits 0,
6
- // so `crtr view run <name> | cat` works anywhere. The interactive path is
3
+ // Resolves the view across scopes (project→user→builtin), loads its core.mjs +
4
+ // tui.mjs (+ optional text.mjs), then hands them to the dual-target core host.
5
+ // The host owns the screen and the non-TTY path: when stdin is NOT a TTY it
6
+ // prints the text presenter's dump and exits 0, so `crtr view run <name> | cat`
7
+ // works anywhere. A view that ships no core.mjs is not a view — run fails with a
8
+ // clear error naming the contract, never a fallback. The interactive path is
7
9
  // tmux-only (crtr is tmux-only) — outside tmux with a TTY we notify + no-op,
8
10
  // never a non-tmux fallback. --port / --target forward verbatim onto
9
11
  // host.options (camelCased, stringified); crtr does not interpret them.
@@ -17,8 +19,8 @@
17
19
  // a pipe (non-TTY) placement is meaningless, so they degrade to the static dump.
18
20
  import { defineLeaf } from '../core/command.js';
19
21
  import { InputError } from '../core/io.js';
20
- import { runView } from '../core/tui/host.js';
21
- import { resolveView, loadView, listViews } from '../core/tui/loader.js';
22
+ import { runCoreView } from '../core/tui/host.js';
23
+ import { resolveView, findViewDir, loadCore, loadTui, loadText, requireTui, listViews, } from '../core/view/loader.js';
22
24
  // Commands reach the tmux driver through placement.ts (the sanctioned
23
25
  // model-over-driver seam, §5.1) — never `./tmux.js` directly.
24
26
  import { inTmux, currentTmux, openNodeWindow, splitWindow, piCommand, setPaneOption, } from '../core/runtime/placement.js';
@@ -66,6 +68,17 @@ export const viewRunLeaf = defineLeaf({
66
68
  }
67
69
  const r = resolveView(name);
68
70
  if (r === null) {
71
+ // A directory exists but holds no core.mjs — it is not a valid view (e.g. a
72
+ // pre-contract `view.mjs`-only dir). Name the contract, don't fall back.
73
+ const dir = findViewDir(name);
74
+ if (dir !== null) {
75
+ throw new InputError({
76
+ error: 'view_invalid',
77
+ message: `'${name}' is not a valid view: ${dir} has no core.mjs`,
78
+ received: name,
79
+ next: `A view is a directory containing core.mjs (the portable core) plus optional tui.mjs (render + keymap), web.jsx, and text.mjs. Scaffold the shape with \`crtr view new ${name}\`.`,
80
+ });
81
+ }
69
82
  const avail = listViews().map((v) => v.id);
70
83
  throw new InputError({
71
84
  error: 'view_not_found',
@@ -81,13 +94,22 @@ export const viewRunLeaf = defineLeaf({
81
94
  options.port = String(port);
82
95
  if (target !== undefined)
83
96
  options.target = target;
97
+ // Load + host the resolved view via the matching path (works for both the
98
+ // non-TTY dump and the interactive alt-screen — each host handles the TTY
99
+ // gate internally).
100
+ const hostView = async () => {
101
+ requireTui(r);
102
+ const core = await loadCore(r);
103
+ const tui = await loadTui(r);
104
+ const txt = await loadText(r);
105
+ await runCoreView(core, tui, txt, { options });
106
+ };
84
107
  // --window / --split: open the view as a persistent monitor in a new pane.
85
108
  if (asWindow || asSplit) {
86
109
  // Placement is tmux-only. In a pipe (non-TTY) it is meaningless — degrade
87
- // to the static dump (runView handles the non-TTY path internally).
110
+ // to the static dump (the host handles the non-TTY path internally).
88
111
  if (!process.stdin.isTTY) {
89
- const v = await loadView(r);
90
- await runView(v, { options });
112
+ await hostView();
91
113
  return;
92
114
  }
93
115
  if (!inTmux()) {
@@ -136,9 +158,8 @@ export const viewRunLeaf = defineLeaf({
136
158
  }
137
159
  return { hosted: 'split', view: name, pane };
138
160
  }
139
- const v = await loadView(r);
140
161
  // The interactive path is tmux-only; the piped/non-TTY dump path works
141
- // anywhere (runView handles it internally), so only guard when stdin is a
162
+ // anywhere (the host handles it internally), so only guard when stdin is a
142
163
  // TTY. Outside tmux with a TTY: notify + no-op — never a non-tmux fallback.
143
164
  if (process.stdin.isTTY && !inTmux()) {
144
165
  throw new InputError({
@@ -162,10 +183,10 @@ export const viewRunLeaf = defineLeaf({
162
183
  catch { /* best-effort */ }
163
184
  }
164
185
  try {
165
- await runView(v, { options });
186
+ await hostView();
166
187
  }
167
188
  finally {
168
- // On a clean quit (q) runView returns to the shell/pi that launched this
189
+ // On a clean quit (q) the host returns to the shell/pi that launched this
169
190
  // pane — clear the monitor tag so a stray Alt+V ]/[ can't respawn-pane -k
170
191
  // (and kill) whatever now owns the pane. When the cycle itself replaces
171
192
  // this view it SIGKILLs us mid-await (this finally never runs); cycle + the