@crouton-kit/crouter 0.3.28 → 0.3.30

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 (324) hide show
  1. package/dist/build-root.d.ts +15 -7
  2. package/dist/build-root.js +48 -34
  3. package/dist/builtin-memory/crouter-development/marketplaces.md +7 -6
  4. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +1 -1
  5. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +1 -1
  6. package/dist/builtin-memory/crouter-development/personas.md +1 -1
  7. package/dist/builtin-memory/crouter-development/plugins.md +20 -18
  8. package/dist/builtin-memory/design.md +1 -1
  9. package/dist/builtin-memory/development.md +1 -1
  10. package/dist/builtin-memory/internal/INDEX.md +1 -1
  11. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  12. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  13. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  14. package/dist/builtin-memory/internal/storage-tiers.md +1 -1
  15. package/dist/builtin-memory/planning.md +1 -1
  16. package/dist/builtin-memory/spec.md +6 -4
  17. package/dist/builtin-personas/orchestration-kernel.md +4 -5
  18. package/dist/builtin-personas/runtime-base.md +5 -4
  19. package/dist/builtin-personas/spec/PERSONA.md +8 -4
  20. package/dist/builtin-personas/spec/orchestrator.md +5 -3
  21. package/dist/builtin-personas/spec/requirements/PERSONA.md +10 -0
  22. package/dist/builtin-views/canvas/core.mjs +586 -0
  23. package/dist/builtin-views/canvas/text.mjs +58 -0
  24. package/dist/builtin-views/canvas/tui.mjs +165 -0
  25. package/dist/builtin-views/canvas/web.jsx +120 -0
  26. package/dist/builtin-views/git-pr/core.mjs +673 -0
  27. package/dist/builtin-views/git-pr/text.mjs +84 -0
  28. package/dist/builtin-views/git-pr/tui.mjs +302 -0
  29. package/dist/builtin-views/git-pr/web.jsx +216 -0
  30. package/dist/builtin-views/inbox/_lib/render.mjs +1 -3
  31. package/dist/builtin-views/inbox/core.mjs +1273 -0
  32. package/dist/builtin-views/inbox/text.mjs +73 -0
  33. package/dist/builtin-views/inbox/tui.mjs +312 -0
  34. package/dist/builtin-views/inbox/web.jsx +188 -0
  35. package/dist/builtin-views/linkedin/core.mjs +906 -0
  36. package/dist/builtin-views/linkedin/text.mjs +69 -0
  37. package/dist/builtin-views/linkedin/tui.mjs +427 -0
  38. package/dist/builtin-views/linkedin/web.jsx +206 -0
  39. package/dist/builtin-views/workspace-sidebar/core.mjs +621 -0
  40. package/dist/builtin-views/workspace-sidebar/text.mjs +53 -0
  41. package/dist/builtin-views/workspace-sidebar/tui.mjs +142 -0
  42. package/dist/builtin-views/workspace-sidebar/web.jsx +109 -0
  43. package/dist/cli.js +22 -17
  44. package/dist/clients/attach/__tests__/action-parity.test.js +48 -0
  45. package/dist/clients/attach/__tests__/clipboard-image.test.js +76 -0
  46. package/dist/clients/attach/__tests__/editor-newline.test.js +57 -0
  47. package/dist/clients/attach/__tests__/git-info.test.js +31 -0
  48. package/dist/clients/attach/__tests__/onrequest-wired.test.js +20 -0
  49. package/dist/clients/attach/__tests__/slash-quit-copy.test.js +42 -0
  50. package/dist/clients/attach/__tests__/titled-editor.test.js +39 -0
  51. package/dist/clients/attach/attach-cmd.js +282 -46
  52. package/dist/clients/attach/auth-pickers.d.ts +9 -0
  53. package/dist/clients/attach/auth-pickers.js +194 -0
  54. package/dist/clients/attach/canvas-panels.d.ts +4 -2
  55. package/dist/clients/attach/canvas-panels.js +45 -17
  56. package/dist/clients/attach/chat-view.d.ts +27 -2
  57. package/dist/clients/attach/chat-view.js +98 -9
  58. package/dist/clients/attach/clipboard-image.d.ts +47 -8
  59. package/dist/clients/attach/clipboard-image.js +150 -26
  60. package/dist/clients/attach/clipboard-text.d.ts +3 -0
  61. package/dist/clients/attach/clipboard-text.js +39 -0
  62. package/dist/clients/attach/config-load.d.ts +54 -7
  63. package/dist/clients/attach/config-load.js +99 -3
  64. package/dist/clients/attach/context-message.d.ts +12 -0
  65. package/dist/clients/attach/context-message.js +72 -0
  66. package/dist/clients/attach/git-info.d.ts +20 -0
  67. package/dist/clients/attach/git-info.js +44 -0
  68. package/dist/clients/attach/graph-overlay.d.ts +13 -4
  69. package/dist/clients/attach/graph-overlay.js +59 -19
  70. package/dist/clients/attach/input-controller.d.ts +75 -13
  71. package/dist/clients/attach/input-controller.js +305 -67
  72. package/dist/clients/attach/pickers.d.ts +58 -0
  73. package/dist/clients/attach/pickers.js +171 -0
  74. package/dist/clients/attach/slash-commands.d.ts +22 -0
  75. package/dist/clients/attach/slash-commands.js +91 -25
  76. package/dist/clients/attach/titled-editor.d.ts +33 -0
  77. package/dist/clients/attach/titled-editor.js +84 -0
  78. package/dist/clients/attach/view-socket.d.ts +18 -1
  79. package/dist/clients/attach/view-socket.js +70 -1
  80. package/dist/clients/web/dev-server.d.ts +7 -0
  81. package/dist/clients/web/dev-server.js +59 -0
  82. package/dist/clients/web/events.d.ts +14 -0
  83. package/dist/clients/web/events.js +151 -0
  84. package/dist/clients/web/server.d.ts +18 -0
  85. package/dist/clients/web/server.js +450 -0
  86. package/dist/clients/web/web-cmd.d.ts +2 -0
  87. package/dist/clients/web/web-cmd.js +120 -0
  88. package/dist/commands/canvas.js +1 -2
  89. package/dist/commands/chord.js +4 -25
  90. package/dist/commands/dashboard.js +5 -1
  91. package/dist/commands/memory/__tests__/lint-schema.test.js +6 -6
  92. package/dist/commands/memory/lint.js +14 -2
  93. package/dist/commands/memory/read.js +2 -2
  94. package/dist/commands/memory/shared.d.ts +7 -1
  95. package/dist/commands/memory/shared.js +18 -3
  96. package/dist/commands/memory/write.js +8 -4
  97. package/dist/commands/memory.js +4 -4
  98. package/dist/commands/node.d.ts +1 -1
  99. package/dist/commands/node.js +64 -65
  100. package/dist/commands/pkg.js +1 -2
  101. package/dist/commands/revive.js +11 -3
  102. package/dist/commands/sys/sync.d.ts +1 -0
  103. package/dist/commands/sys/sync.js +187 -0
  104. package/dist/commands/sys.js +3 -2
  105. package/dist/commands/view-cycle.js +2 -2
  106. package/dist/commands/view-list.js +8 -8
  107. package/dist/commands/view-new.js +21 -17
  108. package/dist/commands/view-pick.js +1 -1
  109. package/dist/commands/view-run.js +35 -14
  110. package/dist/commands/view.js +8 -6
  111. package/dist/commands/workspace.d.ts +2 -0
  112. package/dist/commands/workspace.js +161 -0
  113. package/dist/core/__tests__/broker-double-spawn.test.d.ts +1 -0
  114. package/dist/core/__tests__/broker-double-spawn.test.js +143 -0
  115. package/dist/core/__tests__/broker-fork-seam.test.d.ts +1 -0
  116. package/dist/core/__tests__/broker-fork-seam.test.js +102 -0
  117. package/dist/core/__tests__/broker-sdk-wiring.test.js +2 -0
  118. package/dist/core/__tests__/child-death-wake.test.js +56 -111
  119. package/dist/core/__tests__/close.test.js +14 -20
  120. package/dist/core/__tests__/context-intro.test.js +19 -19
  121. package/dist/core/__tests__/daemon-boot.test.js +30 -23
  122. package/dist/core/__tests__/draw-style.test.js +1 -1
  123. package/dist/core/__tests__/error-stall-recycle.test.d.ts +1 -0
  124. package/dist/core/__tests__/error-stall-recycle.test.js +141 -0
  125. package/dist/core/__tests__/fixtures/fake-engine.d.ts +17 -1
  126. package/dist/core/__tests__/fixtures/fake-engine.js +22 -2
  127. package/dist/core/__tests__/fixtures/fake-pi-host.js +2 -2
  128. package/dist/core/__tests__/focuses.test.js +25 -13
  129. package/dist/core/__tests__/fork.test.js +22 -5
  130. package/dist/core/__tests__/full/broker-control-preempt.test.d.ts +1 -0
  131. package/dist/core/__tests__/full/broker-control-preempt.test.js +61 -0
  132. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.d.ts +1 -0
  133. package/dist/core/__tests__/full/broker-model-changed-broadcast.test.js +57 -0
  134. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.d.ts +1 -0
  135. package/dist/core/__tests__/full/broker-navigate-tree-rewelcome.test.js +80 -0
  136. package/dist/core/__tests__/full/broker-pane-resolution.test.js +90 -0
  137. package/dist/core/__tests__/full/cascade-close.test.js +22 -8
  138. package/dist/core/__tests__/full/detach-focus.test.js +66 -169
  139. package/dist/core/__tests__/helpers/harness.d.ts +6 -9
  140. package/dist/core/__tests__/helpers/harness.js +19 -36
  141. package/dist/core/__tests__/human-node-not-supervised.test.d.ts +1 -0
  142. package/dist/core/__tests__/human-node-not-supervised.test.js +83 -0
  143. package/dist/core/__tests__/lifecycle.test.js +1 -19
  144. package/dist/core/__tests__/listing-completeness.test.js +2 -2
  145. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  146. package/dist/core/__tests__/memory-resolver.test.js +1 -1
  147. package/dist/core/__tests__/on-read-dedup-resume.test.js +2 -2
  148. package/dist/core/__tests__/refresh-stall-recycle.test.d.ts +1 -0
  149. package/dist/core/__tests__/refresh-stall-recycle.test.js +88 -0
  150. package/dist/core/__tests__/relaunch-root.test.d.ts +1 -0
  151. package/dist/core/__tests__/relaunch-root.test.js +114 -0
  152. package/dist/core/__tests__/reset.test.js +19 -79
  153. package/dist/core/__tests__/steer-note.test.js +9 -7
  154. package/dist/core/__tests__/tmux-surface.test.js +13 -7
  155. package/dist/core/bootstrap.d.ts +0 -3
  156. package/dist/core/bootstrap.js +1 -143
  157. package/dist/core/canvas/browse/__tests__/model.test.js +86 -1
  158. package/dist/core/canvas/browse/__tests__/render.test.js +2 -1
  159. package/dist/core/canvas/browse/app.js +131 -8
  160. package/dist/core/canvas/browse/model.d.ts +23 -5
  161. package/dist/core/canvas/browse/model.js +68 -13
  162. package/dist/core/canvas/browse/render.d.ts +7 -1
  163. package/dist/core/canvas/browse/render.js +178 -47
  164. package/dist/core/canvas/canvas.js +1 -1
  165. package/dist/core/canvas/focuses.d.ts +4 -4
  166. package/dist/core/canvas/focuses.js +7 -7
  167. package/dist/core/canvas/nav-model.js +8 -7
  168. package/dist/core/canvas/render.d.ts +68 -9
  169. package/dist/core/canvas/render.js +195 -61
  170. package/dist/core/canvas/types.d.ts +16 -23
  171. package/dist/core/config.js +1 -4
  172. package/dist/core/memory-resolver.js +43 -5
  173. package/dist/core/runtime/bearings.d.ts +1 -1
  174. package/dist/core/runtime/bearings.js +8 -8
  175. package/dist/core/runtime/branded-host.d.ts +16 -0
  176. package/dist/core/runtime/branded-host.js +127 -0
  177. package/dist/core/runtime/broker-protocol.d.ts +212 -6
  178. package/dist/core/runtime/broker.d.ts +4 -1
  179. package/dist/core/runtime/broker.js +494 -58
  180. package/dist/core/runtime/close.d.ts +12 -2
  181. package/dist/core/runtime/close.js +37 -16
  182. package/dist/core/runtime/front-door.js +6 -14
  183. package/dist/core/runtime/host.d.ts +14 -34
  184. package/dist/core/runtime/host.js +14 -50
  185. package/dist/core/runtime/launch.d.ts +12 -9
  186. package/dist/core/runtime/launch.js +27 -15
  187. package/dist/core/runtime/lifecycle.d.ts +1 -1
  188. package/dist/core/runtime/lifecycle.js +15 -19
  189. package/dist/core/runtime/nodes.d.ts +0 -55
  190. package/dist/core/runtime/nodes.js +6 -74
  191. package/dist/core/runtime/placement.d.ts +91 -335
  192. package/dist/core/runtime/placement.js +291 -858
  193. package/dist/core/runtime/promote.d.ts +2 -0
  194. package/dist/core/runtime/promote.js +24 -6
  195. package/dist/core/runtime/recap.d.ts +8 -0
  196. package/dist/core/runtime/recap.js +107 -0
  197. package/dist/core/runtime/recycle.js +25 -61
  198. package/dist/core/runtime/reset.d.ts +43 -37
  199. package/dist/core/runtime/reset.js +131 -218
  200. package/dist/core/runtime/revive.d.ts +9 -29
  201. package/dist/core/runtime/revive.js +40 -139
  202. package/dist/core/runtime/spawn.d.ts +15 -16
  203. package/dist/core/runtime/spawn.js +165 -194
  204. package/dist/core/runtime/surface-bg.d.ts +11 -0
  205. package/dist/core/runtime/surface-bg.js +68 -0
  206. package/dist/core/runtime/tmux.d.ts +15 -52
  207. package/dist/core/runtime/tmux.js +35 -99
  208. package/dist/core/scope.d.ts +1 -2
  209. package/dist/core/scope.js +1 -5
  210. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.d.ts +1 -0
  211. package/dist/core/skill-sync/__tests__/dry-run-wrote-count.test.js +57 -0
  212. package/dist/core/skill-sync/builtins.d.ts +42 -0
  213. package/dist/core/skill-sync/builtins.js +112 -0
  214. package/dist/core/skill-sync/claude-plugins.d.ts +23 -0
  215. package/dist/core/skill-sync/claude-plugins.js +71 -0
  216. package/dist/core/skill-sync/engine.d.ts +42 -0
  217. package/dist/core/skill-sync/engine.js +633 -0
  218. package/dist/core/skill-sync/export.d.ts +23 -0
  219. package/dist/core/skill-sync/export.js +86 -0
  220. package/dist/core/skill-sync/manifest.d.ts +64 -0
  221. package/dist/core/skill-sync/manifest.js +181 -0
  222. package/dist/core/skill-sync/profile.d.ts +76 -0
  223. package/dist/core/skill-sync/profile.js +173 -0
  224. package/dist/core/skill-sync/snapshot.d.ts +57 -0
  225. package/dist/core/skill-sync/snapshot.js +120 -0
  226. package/dist/core/substrate/index.d.ts +1 -1
  227. package/dist/core/substrate/index.js +1 -1
  228. package/dist/core/substrate/injected-store.js +3 -3
  229. package/dist/core/substrate/on-read.js +68 -6
  230. package/dist/core/substrate/render.d.ts +8 -11
  231. package/dist/core/substrate/render.js +29 -43
  232. package/dist/core/substrate/schema.d.ts +10 -3
  233. package/dist/core/substrate/schema.js +6 -3
  234. package/dist/core/tui/host.d.ts +12 -4
  235. package/dist/core/tui/host.js +280 -149
  236. package/dist/core/view/bridge.d.ts +10 -0
  237. package/dist/core/view/bridge.js +31 -0
  238. package/dist/core/view/chrome.d.ts +9 -0
  239. package/dist/core/view/chrome.js +22 -0
  240. package/dist/core/view/contract.d.ts +171 -0
  241. package/dist/core/view/contract.js +23 -0
  242. package/dist/core/view/loader.d.ts +31 -0
  243. package/dist/core/view/loader.js +188 -0
  244. package/dist/core/view/transport-local.d.ts +7 -0
  245. package/dist/core/view/transport-local.js +70 -0
  246. package/dist/core/view/transport.d.ts +4 -0
  247. package/dist/core/view/transport.js +15 -0
  248. package/dist/daemon/crtrd.d.ts +38 -27
  249. package/dist/daemon/crtrd.js +303 -376
  250. package/dist/daemon/manage.js +6 -1
  251. package/dist/index.js +1 -1
  252. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +28 -20
  253. package/dist/pi-extensions/canvas-context-intro.js +4 -4
  254. package/dist/pi-extensions/canvas-doc-substrate.js +16 -16
  255. package/dist/pi-extensions/canvas-nav.js +5 -0
  256. package/dist/pi-extensions/canvas-recap.d.ts +37 -0
  257. package/dist/pi-extensions/canvas-recap.js +236 -0
  258. package/dist/pi-extensions/canvas-resume.js +2 -1
  259. package/dist/pi-extensions/canvas-stophook.d.ts +14 -12
  260. package/dist/pi-extensions/canvas-stophook.js +87 -101
  261. package/dist/pi-extensions/canvas-view.js +2 -1
  262. package/dist/pi-extensions/widget-order-bus.d.ts +6 -0
  263. package/dist/pi-extensions/widget-order-bus.js +34 -0
  264. package/dist/prompts/view.d.ts +2 -2
  265. package/dist/prompts/view.js +145 -73
  266. package/dist/types.d.ts +1 -6
  267. package/dist/types.js +1 -3
  268. package/dist/web/ViewChrome.d.ts +7 -0
  269. package/dist/web/ViewChrome.js +28 -0
  270. package/dist/web/ViewPane.d.ts +39 -0
  271. package/dist/web/ViewPane.js +48 -0
  272. package/dist/web/index.d.ts +6 -0
  273. package/dist/web/index.js +16 -0
  274. package/dist/web/runtime.d.ts +39 -0
  275. package/dist/web/runtime.js +133 -0
  276. package/dist/web/states.d.ts +24 -0
  277. package/dist/web/states.js +24 -0
  278. package/dist/web/transport-http.d.ts +5 -0
  279. package/dist/web/transport-http.js +28 -0
  280. package/dist/web-client/assets/index-BUvQb4hR.css +2 -0
  281. package/dist/web-client/assets/index-ClLQXYAE.js +10 -0
  282. package/dist/web-client/index.html +13 -0
  283. package/package.json +20 -6
  284. package/dist/builtin-views/canvas/client.mjs +0 -303
  285. package/dist/builtin-views/canvas/view.mjs +0 -576
  286. package/dist/builtin-views/git-pr/client.mjs +0 -440
  287. package/dist/builtin-views/git-pr/view.mjs +0 -675
  288. package/dist/builtin-views/inbox/sources/gmail.mjs +0 -965
  289. package/dist/builtin-views/inbox/sources/linkedin.mjs +0 -427
  290. package/dist/builtin-views/inbox/view.mjs +0 -889
  291. package/dist/builtin-views/linkedin/client.mjs +0 -610
  292. package/dist/builtin-views/linkedin/view.mjs +0 -1171
  293. package/dist/commands/pkg/bridge.d.ts +0 -1
  294. package/dist/commands/pkg/bridge.js +0 -137
  295. package/dist/commands/skill/author.d.ts +0 -3
  296. package/dist/commands/skill/author.js +0 -140
  297. package/dist/commands/skill/shared.d.ts +0 -3
  298. package/dist/commands/skill/shared.js +0 -19
  299. package/dist/commands/skill.d.ts +0 -2
  300. package/dist/commands/skill.js +0 -21
  301. package/dist/commands/tmux-spread.d.ts +0 -2
  302. package/dist/commands/tmux-spread.js +0 -144
  303. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +0 -292
  304. package/dist/core/__tests__/full/placement-focus.test.js +0 -309
  305. package/dist/core/__tests__/full/placement-reconcile.test.js +0 -212
  306. package/dist/core/__tests__/full/placement-revive.test.js +0 -238
  307. package/dist/core/__tests__/full/placement-teardown.test.js +0 -297
  308. package/dist/core/__tests__/home-session.test.js +0 -186
  309. package/dist/core/__tests__/relaunch.test.js +0 -335
  310. package/dist/core/bridge-map.d.ts +0 -19
  311. package/dist/core/bridge-map.js +0 -73
  312. package/dist/core/tui/contract.d.ts +0 -83
  313. package/dist/core/tui/contract.js +0 -8
  314. package/dist/core/tui/loader.d.ts +0 -16
  315. package/dist/core/tui/loader.js +0 -94
  316. package/dist/prompts/skill.d.ts +0 -2
  317. package/dist/prompts/skill.js +0 -650
  318. /package/dist/{core/__tests__/full/daemon-liveness-pane.full.test.d.ts → clients/attach/__tests__/action-parity.test.d.ts} +0 -0
  319. /package/dist/{core/__tests__/full/placement-focus.test.d.ts → clients/attach/__tests__/clipboard-image.test.d.ts} +0 -0
  320. /package/dist/{core/__tests__/full/placement-reconcile.test.d.ts → clients/attach/__tests__/editor-newline.test.d.ts} +0 -0
  321. /package/dist/{core/__tests__/full/placement-revive.test.d.ts → clients/attach/__tests__/git-info.test.d.ts} +0 -0
  322. /package/dist/{core/__tests__/full/placement-teardown.test.d.ts → clients/attach/__tests__/onrequest-wired.test.d.ts} +0 -0
  323. /package/dist/{core/__tests__/home-session.test.d.ts → clients/attach/__tests__/slash-quit-copy.test.d.ts} +0 -0
  324. /package/dist/{core/__tests__/relaunch.test.d.ts → clients/attach/__tests__/titled-editor.test.d.ts} +0 -0
@@ -1,335 +0,0 @@
1
- // Run with: node --import tsx/esm --test src/core/__tests__/relaunch.test.ts
2
- //
3
- // Covers the `/new`-in-a-root relaunch (option C) + clean-exit termination
4
- // semantics. tmux/respawn is unavailable in CI, so the respawn is an INJECTED
5
- // test double (RelaunchDeps.relaunchRootInPane) and every assertion is on the
6
- // DB / edge / disk effects.
7
- import { test, before, after, beforeEach } from 'node:test';
8
- import assert from 'node:assert/strict';
9
- import { mkdtempSync, rmSync, existsSync, writeFileSync, readdirSync } from 'node:fs';
10
- import { tmpdir } from 'node:os';
11
- import { join } from 'node:path';
12
- import { createNode, getNode, subscribe, subscriptionsOf, view, listNodes, } from '../canvas/canvas.js';
13
- import { closeDb } from '../canvas/db.js';
14
- import { getFocusByNode, openFocusRow } from '../canvas/focuses.js';
15
- import { reportsDir, inboxPath, contextDir } from '../canvas/paths.js';
16
- import { roadmapPath } from '../runtime/roadmap.js';
17
- import { relaunchRoot, handleNewSession, markCleanExitDone, reapDescendants, } from '../runtime/reset.js';
18
- import { renderForest } from '../canvas/render.js';
19
- let home;
20
- function node(id, over = {}) {
21
- return {
22
- node_id: id,
23
- name: id,
24
- created: new Date().toISOString(),
25
- cwd: '/tmp/work',
26
- kind: 'general',
27
- mode: 'base',
28
- lifecycle: 'terminal',
29
- status: 'active',
30
- ...over,
31
- };
32
- }
33
- /** A respawn double that records its calls and never throws (dispatch ok). */
34
- function okRespawn() {
35
- const calls = [];
36
- return {
37
- calls,
38
- fn: (nodeId, pane) => { calls.push({ nodeId, pane }); },
39
- };
40
- }
41
- /** A respawn double that simulates a dispatch failure (throws). */
42
- function throwingRespawn() {
43
- const calls = [];
44
- return {
45
- calls,
46
- fn: (nodeId, pane) => {
47
- calls.push({ nodeId, pane });
48
- throw new Error('respawn-pane dispatch failed');
49
- },
50
- };
51
- }
52
- before(() => {
53
- home = mkdtempSync(join(tmpdir(), 'crtr-relaunch-'));
54
- process.env['CRTR_HOME'] = home;
55
- });
56
- beforeEach(() => {
57
- closeDb();
58
- rmSync(home, { recursive: true, force: true });
59
- });
60
- after(() => {
61
- closeDb();
62
- rmSync(home, { recursive: true, force: true });
63
- delete process.env['CRTR_HOME'];
64
- });
65
- // ---------------------------------------------------------------------------
66
- // #1 — relaunchRoot parks the old root, keeps edges, creates a fresh root
67
- // ---------------------------------------------------------------------------
68
- test('relaunchRoot parks the old root (canceled, edges intact, no wipe) and mints a fresh root', () => {
69
- createNode(node('root', {
70
- parent: null,
71
- lifecycle: 'resident',
72
- mode: 'orchestrator',
73
- pi_session_id: 'root-sess',
74
- pi_session_file: '/abs/root-sess.jsonl',
75
- tmux_session: 'crtr',
76
- window: '@7',
77
- }));
78
- createNode(node('child', { parent: 'root' }));
79
- createNode(node('grand', { parent: 'child' }));
80
- subscribe('root', 'child', true);
81
- subscribe('child', 'grand', true);
82
- openFocusRow('fRoot', '%root', 'crtr', 'root'); // the old root occupies focus #1
83
- // Working state on the old root that parking must PRESERVE (no wipe).
84
- writeFileSync(roadmapPath('root'), '# Roadmap\nold goal\n');
85
- writeFileSync(inboxPath('root'), '{"ts":"x","from":"child","tier":"normal","kind":"update","label":"hi"}\n');
86
- writeFileSync(join(reportsDir('root'), '20260101T000000-update.md'), 'a report');
87
- const respawn = okRespawn();
88
- const res = relaunchRoot('root', 'test-pane', { relaunchRootInPane: respawn.fn });
89
- assert.ok(res !== null, 'relaunchRoot returns the new node id');
90
- const newId = res.newNodeId;
91
- assert.notEqual(newId, 'root', 'a FRESH id, not the old one');
92
- // Respawn was dispatched against the NEW node in the given pane.
93
- assert.deepEqual(respawn.calls, [{ nodeId: newId, pane: 'test-pane' }]);
94
- // Old root: parked canceled, window detached, pi_session_id UNCHANGED (resumable).
95
- const old = getNode('root');
96
- assert.equal(old?.status, 'canceled', 'old root parked canceled');
97
- assert.equal(old?.window, null, 'old root window detached');
98
- assert.equal(old?.tmux_session, null, 'old root tmux_session detached');
99
- assert.equal(old?.intent, null, 'old root intent cleared');
100
- assert.equal(old?.pi_session_id, 'root-sess', 'pi_session_id preserved (resumable)');
101
- assert.equal(old?.pi_session_file, '/abs/root-sess.jsonl', 'pi_session_file preserved (resumable by path)');
102
- assert.equal(old?.parent, null, 'old root stays a root');
103
- // Descendants: CANCELED (not dead), but edges intact.
104
- assert.equal(getNode('child')?.status, 'canceled', 'child marked canceled (not a fault)');
105
- assert.equal(getNode('grand')?.status, 'canceled', 'grand marked canceled (not a fault)');
106
- assert.deepEqual(subscriptionsOf('root').map((s) => s.node_id), ['child'], 'root→child edge intact');
107
- assert.deepEqual(subscriptionsOf('child').map((s) => s.node_id), ['grand'], 'child→grand edge intact');
108
- // Old root working state PRESERVED (history, no wipe).
109
- assert.equal(existsSync(roadmapPath('root')), true, 'roadmap preserved');
110
- assert.equal(existsSync(inboxPath('root')), true, 'inbox preserved');
111
- assert.equal(existsSync(join(reportsDir('root'), '20260101T000000-update.md')), true, 'report preserved');
112
- // New root: fresh base resident, active, intent=refresh, a clean-slate context
113
- // dir (only the born-with memory store, nothing inherited), spawned_by=old,
114
- // focused.
115
- const fresh = getNode(newId);
116
- assert.equal(fresh?.parent, null, 'new node is a root');
117
- assert.equal(fresh?.mode, 'base');
118
- assert.equal(fresh?.lifecycle, 'resident');
119
- assert.equal(fresh?.status, 'active');
120
- assert.equal(fresh?.intent, 'refresh', 'safety-net intent until boot');
121
- assert.equal(fresh?.spawned_by, 'root', 'audit-only successor link to old root');
122
- assert.equal(fresh?.pi_pid, null, 'no pi yet');
123
- assert.equal(fresh?.tmux_session, 'crtr', 'adopted the old root window location');
124
- assert.equal(fresh?.window, '@7');
125
- assert.ok(fresh?.launch, 'a fresh base launch spec was written');
126
- assert.deepEqual(readdirSync(contextDir(newId)).sort(), ['memory'], 'fresh context dir holds only the born-with node-local memory store — nothing inherited from the old root');
127
- // Focus follows content: the focus row the old root held now shows the new root.
128
- assert.equal(getFocusByNode(newId)?.focus_id, 'fRoot', 'focus row repointed to the new root');
129
- assert.equal(getFocusByNode('root'), null, 'old root no longer occupies the focus');
130
- });
131
- // ---------------------------------------------------------------------------
132
- // #1b — handleNewSession success branch: root WITH a pane routes to relaunch
133
- // ---------------------------------------------------------------------------
134
- test('handleNewSession on a root with a pane returns path:relaunch + parks old, mints fresh', () => {
135
- createNode(node('root', {
136
- parent: null,
137
- lifecycle: 'resident',
138
- mode: 'orchestrator',
139
- pi_session_id: 'root-sess',
140
- tmux_session: 'crtr',
141
- window: '@7',
142
- }));
143
- createNode(node('child', { parent: 'root' }));
144
- subscribe('root', 'child', true);
145
- openFocusRow('fRoot', '%root', 'crtr', 'root'); // the old root occupies focus #1
146
- const respawn = okRespawn();
147
- const res = handleNewSession('root', 'newsess', 'test-pane', { relaunchRootInPane: respawn.fn });
148
- // The policy router's success return shape: relaunch + a fresh new node id.
149
- assert.equal(res.path, 'relaunch', 'root + pane routes to option C relaunch');
150
- assert.ok(res.newNodeId, 'newNodeId set on the relaunch path');
151
- assert.notEqual(res.newNodeId, 'root', 'a FRESH id, not the old root');
152
- // Respawn dispatched against the new node in the pane.
153
- assert.deepEqual(respawn.calls, [{ nodeId: res.newNodeId, pane: 'test-pane' }]);
154
- // Parked-old + fresh-new end state.
155
- const old = getNode('root');
156
- assert.equal(old?.status, 'canceled', 'old root parked canceled');
157
- assert.equal(old?.window, null, 'old root window detached');
158
- assert.equal(old?.pi_session_id, 'root-sess', 'pi_session_id preserved (resumable)');
159
- assert.equal(getNode('child')?.status, 'canceled', 'descendant reaped canceled');
160
- const fresh = getNode(res.newNodeId);
161
- assert.equal(fresh?.parent, null, 'new node is a root');
162
- assert.equal(fresh?.status, 'active', 'fresh root active');
163
- assert.equal(fresh?.spawned_by, 'root', 'audit-only successor link to old root');
164
- assert.equal(getFocusByNode(res.newNodeId)?.focus_id, 'fRoot', 'focus row repointed to the fresh root');
165
- });
166
- // ---------------------------------------------------------------------------
167
- // #2 — handleNewSession on a non-root → session-id refresh only
168
- // ---------------------------------------------------------------------------
169
- test('handleNewSession on a non-root child only refreshes its session id', () => {
170
- createNode(node('root', { parent: null }));
171
- createNode(node('child', { parent: 'root', pi_session_id: 'old', pi_session_file: '/abs/old.jsonl' }));
172
- subscribe('root', 'child', true);
173
- const before = listNodes().length;
174
- const res = handleNewSession('child', 'fresh', 'test-pane', {}, '/abs/fresh.jsonl');
175
- assert.equal(res.path, 'reset-child');
176
- assert.equal(res.newNodeId, undefined, 'no new node minted');
177
- assert.equal(getNode('child')?.pi_session_id, 'fresh', 'session id refreshed');
178
- assert.equal(getNode('child')?.pi_session_file, '/abs/fresh.jsonl', 'session FILE refreshed (path-based resume)');
179
- assert.equal(getNode('child')?.status, 'active', 'child not reaped');
180
- assert.equal(listNodes().length, before, 'no node added');
181
- });
182
- // ---------------------------------------------------------------------------
183
- // #3 — handleNewSession on a root with no pane → in-place reset fallback
184
- // ---------------------------------------------------------------------------
185
- test('handleNewSession on a root with no pane falls back to in-place resetRoot', () => {
186
- createNode(node('root', { parent: null, lifecycle: 'resident', mode: 'orchestrator' }));
187
- createNode(node('child', { parent: 'root' }));
188
- subscribe('root', 'child', true);
189
- writeFileSync(roadmapPath('root'), '# Roadmap\n');
190
- const before = listNodes().length;
191
- const res = handleNewSession('root', 'newsess', undefined);
192
- assert.equal(res.path, 'reset-root', 'documented degradation: in-place reset');
193
- assert.equal(res.newNodeId, undefined, 'NO new node created');
194
- assert.equal(listNodes().length, before, 'no node added (same id re-pointed)');
195
- // Same id re-pointed to a pristine base resident.
196
- const root = getNode('root');
197
- assert.equal(root?.status, 'active');
198
- assert.equal(root?.mode, 'base');
199
- assert.equal(root?.lifecycle, 'resident');
200
- assert.equal(root?.pi_session_id, 'newsess');
201
- assert.equal(view('root').length, 0, 'root view emptied');
202
- assert.equal(getNode('child')?.status, 'canceled', 'descendant marked canceled');
203
- assert.equal(existsSync(roadmapPath('root')), false, 'working state wiped');
204
- });
205
- // ---------------------------------------------------------------------------
206
- // #4 — rapid double /new guard
207
- // ---------------------------------------------------------------------------
208
- test('a second relaunchRoot on an already-parked root is a no-op', () => {
209
- createNode(node('root', { parent: null, lifecycle: 'resident' }));
210
- const respawn = okRespawn();
211
- const first = relaunchRoot('root', 'test-pane', { relaunchRootInPane: respawn.fn });
212
- assert.ok(first !== null, 'first /new parks + relaunches');
213
- const afterFirst = listNodes().length;
214
- // Old root is now `canceled`; a second session_start in the dying old pi must
215
- // no-op (no second parked node, no zombie new node).
216
- const second = relaunchRoot('root', 'test-pane', { relaunchRootInPane: respawn.fn });
217
- assert.equal(second, null, 'second relaunch is a no-op');
218
- assert.equal(listNodes().length, afterFirst, 'no second new node minted');
219
- assert.equal(respawn.calls.length, 1, 'respawn dispatched only once');
220
- assert.equal(getNode('root')?.status, 'canceled', 'old root unchanged (still parked)');
221
- });
222
- // ---------------------------------------------------------------------------
223
- // #5 — /new before the root ever spawned children
224
- // ---------------------------------------------------------------------------
225
- test('relaunchRoot on a childless root: reap is a no-op, new node minted', () => {
226
- createNode(node('root', { parent: null, lifecycle: 'resident', tmux_session: 'crtr', window: '@1' }));
227
- assert.deepEqual(reapDescendants('root'), [], 'no descendants to reap');
228
- const respawn = okRespawn();
229
- const res = relaunchRoot('root', 'test-pane', { relaunchRootInPane: respawn.fn });
230
- assert.ok(res !== null, 'new node minted without throwing');
231
- assert.equal(getNode('root')?.status, 'canceled', 'old root parked');
232
- assert.equal(getNode(res.newNodeId)?.status, 'active', 'fresh root active');
233
- });
234
- // ---------------------------------------------------------------------------
235
- // #6 — respawn dispatch failure → rollback + resetRoot
236
- // ---------------------------------------------------------------------------
237
- test('a respawn dispatch failure rolls the whole transaction back and degrades to resetRoot', () => {
238
- createNode(node('root', {
239
- parent: null,
240
- lifecycle: 'resident',
241
- mode: 'orchestrator',
242
- pi_session_id: 'root-sess',
243
- tmux_session: 'crtr',
244
- window: '@3',
245
- }));
246
- openFocusRow('fRoot', '%root', 'crtr', 'root'); // the old root occupies focus #1
247
- const respawn = throwingRespawn();
248
- const res = handleNewSession('root', 'newsess', 'test-pane', { relaunchRootInPane: respawn.fn });
249
- assert.equal(res.path, 'reset-root', 'degraded to in-place reset');
250
- // Old root: FULLY restored by the transaction rollback (active, with its
251
- // window/session intact), then re-pointed by the resetRoot degrade path.
252
- const old = getNode('root');
253
- assert.equal(old?.status, 'active', 'old root back to active');
254
- assert.equal(old?.window, '@3', 'window restored');
255
- assert.equal(old?.tmux_session, 'crtr', 'session restored');
256
- assert.equal(old?.mode, 'base', 'resetRoot re-pointed to base');
257
- assert.equal(old?.pi_session_id, 'newsess', 'resetRoot rebound the new session id');
258
- // No partial state: the half-built new node's ROW was rolled back WITH the
259
- // transaction — no zombie active node, and (unlike the old hand-rolled
260
- // compensation, which left a leaked `dead` row) no leftover row at all.
261
- const actives = listNodes({ status: ['active'] }).map((r) => r.node_id);
262
- assert.deepEqual(actives, ['root'], 'only the old root is active — no zombie');
263
- assert.equal(listNodes({ status: ['dead'] }).length, 0, 'no dead zombie — new node row rolled back');
264
- assert.equal(listNodes().length, 1, 'only the old root row exists — the mint was fully undone');
265
- // The focus repoint was INSIDE the txn, so ROLLBACK restored the old occupant.
266
- assert.equal(getFocusByNode('root')?.focus_id, 'fRoot', 'focus row restored to the old root (rollback undid the repoint)');
267
- });
268
- // ---------------------------------------------------------------------------
269
- // #7 — markCleanExitDone guard table (termination rule)
270
- // ---------------------------------------------------------------------------
271
- test('markCleanExitDone: quit on an active/intent-null node marks it done', () => {
272
- createNode(node('n', { status: 'active', intent: null }));
273
- assert.equal(markCleanExitDone('n', 'quit'), true);
274
- assert.equal(getNode('n')?.status, 'done');
275
- });
276
- test('markCleanExitDone does NOT clobber an idle-released node', () => {
277
- createNode(node('n', { status: 'idle', intent: 'idle-release' }));
278
- assert.equal(markCleanExitDone('n', 'quit'), false);
279
- assert.equal(getNode('n')?.status, 'idle', 'unchanged');
280
- assert.equal(getNode('n')?.intent, 'idle-release', 'intent unchanged');
281
- });
282
- test('markCleanExitDone does NOT clobber a node mid-refresh-yield', () => {
283
- createNode(node('n', { status: 'active', intent: 'refresh' }));
284
- assert.equal(markCleanExitDone('n', 'quit'), false);
285
- assert.equal(getNode('n')?.status, 'active', 'unchanged');
286
- assert.equal(getNode('n')?.intent, 'refresh', 'intent unchanged');
287
- });
288
- test('markCleanExitDone does NOT re-mark an already-done node', () => {
289
- createNode(node('n', { status: 'done', intent: null }));
290
- assert.equal(markCleanExitDone('n', 'quit'), false);
291
- assert.equal(getNode('n')?.status, 'done', 'unchanged');
292
- });
293
- test('markCleanExitDone is a no-op for every non-quit reason', () => {
294
- for (const reason of ['new', 'reload', 'resume', 'fork']) {
295
- closeDb();
296
- rmSync(home, { recursive: true, force: true });
297
- createNode(node('n', { status: 'active', intent: null }));
298
- assert.equal(markCleanExitDone('n', reason), false, `${reason} → no-op`);
299
- assert.equal(getNode('n')?.status, 'active', `${reason} leaves node unchanged`);
300
- }
301
- });
302
- test('markCleanExitDone is a no-op for an unknown node', () => {
303
- assert.equal(markCleanExitDone('ghost', 'quit'), false);
304
- });
305
- // ---------------------------------------------------------------------------
306
- // #8 — a parked / quit (done) node is invisible to the daemon's supervised set
307
- // ---------------------------------------------------------------------------
308
- test('a done node (parked or cleanly quit) is not in the supervised set', () => {
309
- // A done node with a live-LOOKING window — the daemon never sees it because
310
- // it only ever supervises listNodes({status:['active','idle']}).
311
- createNode(node('parked', { parent: null, status: 'done', tmux_session: 'crtr', window: '@9' }));
312
- createNode(node('live', { parent: null, status: 'active', tmux_session: 'crtr', window: '@1' }));
313
- const supervised = listNodes({ status: ['active', 'idle'] }).map((r) => r.node_id);
314
- assert.ok(!supervised.includes('parked'), 'parked done node excluded from supervision');
315
- assert.ok(supervised.includes('live'), 'live root still supervised');
316
- });
317
- // ---------------------------------------------------------------------------
318
- // #9 — renderForest excludes parked / dead / canceled roots
319
- // ---------------------------------------------------------------------------
320
- test('renderForest renders only LIVE (active|idle) roots', () => {
321
- const mk = (id, status) => {
322
- createNode(node(id, { parent: null, status, name: id, kind: 'general' }));
323
- };
324
- mk('liveroot', 'active');
325
- mk('idleroot', 'idle');
326
- mk('parkedroot', 'done');
327
- mk('deadroot', 'dead');
328
- mk('cancelroot', 'canceled');
329
- const out = renderForest();
330
- assert.ok(out.includes('liveroot'), 'active root rendered');
331
- assert.ok(out.includes('idleroot'), 'idle root rendered');
332
- assert.ok(!out.includes('parkedroot'), 'parked (done) root hidden');
333
- assert.ok(!out.includes('deadroot'), 'dead root hidden');
334
- assert.ok(!out.includes('cancelroot'), 'canceled root hidden');
335
- });
@@ -1,19 +0,0 @@
1
- export interface BridgeSkillInput {
2
- /** Source skill `name` frontmatter (identity only; not emitted as a field). */
3
- name: string;
4
- /** Source skill `description` frontmatter — the text we reshape + carry. */
5
- description: string;
6
- /** The skill body, copied verbatim below the generated frontmatter. */
7
- body: string;
8
- }
9
- /** Reshape a Claude `description` into a read-routing sentence per
10
- * taste/why-field-means-why-to-read: "When <situation>, this skill should be
11
- * read [because <gist>]." Claude descriptions are typically
12
- * "<gist>. Use when <situation>." — we lift the use-when clause into the
13
- * situation and the preceding text into the payoff. With no explicit clause,
14
- * the whole description becomes the situation. */
15
- export declare function reshapeWhenAndWhy(description: string): string;
16
- /** Map a parsed Claude SKILL.md to a crouter substrate-doc string: full
17
- * frontmatter (kind/when-and-why/short-form/visibility/generator) + the copied
18
- * body. Deterministic and pure. */
19
- export declare function buildBridgeDoc(input: BridgeSkillInput): string;
@@ -1,73 +0,0 @@
1
- // Claude SKILL.md → crouter substrate-doc mapping (design §2.3).
2
- // Pure, unit-testable. The bridge command (`crtr pkg bridge sync`) runs this
3
- // unattended every session, so the description→read-routing reshape is a
4
- // deterministic, conservative string rewrite — NOT a content paraphrase. An
5
- // author who wants durable tuning uses an INDEX (which the bridge never
6
- // touches), because a generator-stamped leaf is overwritten on the next sync.
7
- // "Use when …" / "Use this when …" / "This skill should be used when …" / "Used when …"
8
- const USE_WHEN_RE = /\b(?:use\s+this\s+skill\s+when|use\s+this\s+when|this\s+skill\s+should\s+be\s+used\s+when|used\s+when|use\s+when)\b\s*/i;
9
- /** Collapse whitespace, strip surrounding connective punctuation, and lowercase
10
- * a leading Capitalized word (but never an ACRONYM) so the clause reads inline
11
- * after "When " / "because ". */
12
- function cleanClause(s) {
13
- let c = s.replace(/\s+/g, ' ').trim();
14
- c = c.replace(/^[,;:.\s]+/, '').replace(/[,;:.\s]+$/, '');
15
- if (/^[A-Z][a-z]/.test(c))
16
- c = c.charAt(0).toLowerCase() + c.slice(1);
17
- return c;
18
- }
19
- /** Reshape a Claude `description` into a read-routing sentence per
20
- * taste/why-field-means-why-to-read: "When <situation>, this skill should be
21
- * read [because <gist>]." Claude descriptions are typically
22
- * "<gist>. Use when <situation>." — we lift the use-when clause into the
23
- * situation and the preceding text into the payoff. With no explicit clause,
24
- * the whole description becomes the situation. */
25
- export function reshapeWhenAndWhy(description) {
26
- const desc = description.replace(/\s+/g, ' ').trim();
27
- if (desc === '')
28
- return 'When this skill applies, this skill should be read.';
29
- const m = desc.match(USE_WHEN_RE);
30
- if (m && m.index !== undefined) {
31
- const gist = cleanClause(desc.slice(0, m.index));
32
- const situation = cleanClause(desc.slice(m.index + m[0].length));
33
- if (situation !== '') {
34
- let out = `When ${situation}, this skill should be read`;
35
- if (gist !== '')
36
- out += ` because ${gist}`;
37
- return out + '.';
38
- }
39
- }
40
- return `When ${cleanClause(desc)}, this skill should be read.`;
41
- }
42
- /** One-line short-form for the human inventory (never loaded into agent
43
- * context): the source description collapsed to a single line. */
44
- function toShortForm(description) {
45
- return description.replace(/\s+/g, ' ').trim();
46
- }
47
- /** Always-double-quote a YAML scalar, escaping backslashes and quotes. The two
48
- * generated fields are single-line, so this is safe regardless of colons,
49
- * commas, or other YAML-significant characters. */
50
- function yamlQuote(s) {
51
- return `"${s.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
52
- }
53
- /** Map a parsed Claude SKILL.md to a crouter substrate-doc string: full
54
- * frontmatter (kind/when-and-why/short-form/visibility/generator) + the copied
55
- * body. Deterministic and pure. */
56
- export function buildBridgeDoc(input) {
57
- const whenAndWhy = reshapeWhenAndWhy(input.description);
58
- const shortForm = toShortForm(input.description);
59
- const fm = [
60
- '---',
61
- 'kind: skill',
62
- `when-and-why-to-read: ${yamlQuote(whenAndWhy)}`,
63
- `short-form: ${yamlQuote(shortForm)}`,
64
- 'system-prompt-visibility: name',
65
- 'file-read-visibility: none',
66
- 'generator: claude-bridge',
67
- '---',
68
- '',
69
- ].join('\n');
70
- const body = input.body.replace(/^\n+/, '');
71
- const trailing = body.endsWith('\n') ? '' : '\n';
72
- return fm + '\n' + body + trailing;
73
- }
@@ -1,83 +0,0 @@
1
- import type { Key } from './terminal.js';
2
- import type { Draw, Rect } from './draw.js';
3
- export interface ViewManifest {
4
- id: string;
5
- title: string;
6
- subtitle?: string;
7
- description: string;
8
- refreshMs?: number;
9
- keymap?: KeyHint[];
10
- }
11
- export interface KeyHint {
12
- keys: string;
13
- label: string;
14
- }
15
- /** Severity of a {@link ViewHost.setBanner} banner — drives glyph + hue + the
16
- * derived state chip (error→blocked/red, action→attention/yellow, info→neutral). */
17
- export type BannerLevel = 'info' | 'action' | 'error';
18
- /** The current host banner, threaded into {@link ViewModule.dump} so the static
19
- * (non-TTY) path can surface guidance without the view mirroring it into state. */
20
- export interface Banner {
21
- msg: string;
22
- level: BannerLevel;
23
- }
24
- /** Optional host context passed to {@link ViewModule.dump} on the piped path. */
25
- export interface DumpContext {
26
- banner: Banner | null;
27
- }
28
- export interface ViewHost {
29
- /** CLI flags forwarded verbatim, e.g. { port: '9222', target: '...' }. */
30
- readonly options: Readonly<Record<string, string>>;
31
- /** Transient status line (left of the footer): "Loading…", "Sent". */
32
- setStatus(msg: string | null): void;
33
- /** Severity-coded guidance banner above the footer (info/action/error). The
34
- * level drives the banner glyph + hue AND the derived title state chip. */
35
- setBanner(msg: string, level: BannerLevel): void;
36
- /** Sticky error banner above the footer; null clears. Back-compat shorthand
37
- * for setBanner(msg, 'error'). */
38
- setError(msg: string | null): void;
39
- /** Dynamic title subtitle: a dim ` · <s>` after the bold title (e.g. the live
40
- * unread count "3 unread"). A non-null value OVERRIDES the static
41
- * {@link ViewManifest.subtitle}; `null` clears back to the manifest default
42
- * (or nothing). The title still leads — the subtitle is muted. */
43
- setSubtitle(s: string | null): void;
44
- /** Explicit interaction-mode chip override (compose/react). When set it WINS
45
- * the title state chip over the derived state, rendering the mode word in the
46
- * compose accent (yellow `33` bold) with the `✎` glyph (`react` → `☺`), so
47
- * entering an input mode is unmistakable. `null` returns to derived behavior. */
48
- setMode(mode: string | null): void;
49
- }
50
- export type ViewAction = {
51
- type: 'render';
52
- } | {
53
- type: 'refresh';
54
- } | {
55
- type: 'quit';
56
- } | {
57
- type: 'none';
58
- };
59
- export interface ViewKey {
60
- input: string;
61
- key: Key;
62
- }
63
- export interface ViewModule<S = unknown> {
64
- manifest: ViewManifest;
65
- /** Build initial state. CHEAP + synchronous-ish — no screen, no slow fetch.
66
- * Slow data loads on the first refresh() so the host can paint a loading state. */
67
- init(host: ViewHost): S | Promise<S>;
68
- /** Fetch/poll. Mutates state in place. Host calls it on launch, on `refreshMs`,
69
- * and whenever a hook returns { type:'refresh' }. The host runs it in the
70
- * single-flight lane (busy indicator; never re-entrant). */
71
- refresh?(state: S, host: ViewHost): Promise<void>;
72
- /** Paint the view into `content` (the body rect, host chrome excluded).
73
- * Pure: reads state, calls draw.*; returns nothing. NEVER writes ANSI. */
74
- render(state: S, draw: Draw, content: Rect): void;
75
- /** Handle one keystroke. Mutates state; returns the next action. May be async
76
- * (open thread / send) — the host serializes async hooks in the single lane. */
77
- onKey?(k: ViewKey, state: S, host: ViewHost): ViewAction | Promise<ViewAction>;
78
- /** Static text for the non-TTY / piped path (exit 0). Snapshot of current
79
- * state. The host threads its current banner via the optional `ctx` so a view
80
- * can surface guidance without mirroring it into state (older views ignore the
81
- * arg and read their own state). */
82
- dump(state: S, ctx?: DumpContext): string;
83
- }
@@ -1,8 +0,0 @@
1
- // contract.ts — the view contract every `crtr view` module implements.
2
- //
3
- // A view is a self-contained ESM module whose DEFAULT export is a `ViewModule`.
4
- // State is a single mutable object the view owns (mirrors browse/app.ts's
5
- // BrowseState); hooks mutate it in place and return a `ViewAction` telling the
6
- // host what to do next. The host injects `Draw` (from ./draw) + `ViewHost`; a
7
- // view imports NOTHING from crtr internals.
8
- export {};
@@ -1,16 +0,0 @@
1
- import type { ViewModule } from './contract.js';
2
- export interface ResolvedView {
3
- id: string;
4
- dir: string;
5
- entry: string;
6
- scope: 'project' | 'user' | 'builtin';
7
- }
8
- /** Scope search project→user→builtin; first hit wins. null if no view dir holds
9
- * a `<name>/view.mjs`. */
10
- export declare function resolveView(name: string): ResolvedView | null;
11
- /** Enumerate every resolvable view across scopes (first scope wins per id) —
12
- * backs `view list` / `view pick`. */
13
- export declare function listViews(): ResolvedView[];
14
- /** Dynamic-import a resolved view and validate it's a usable ViewModule. Throws
15
- * a guided error (with the entry path) if the module is malformed. */
16
- export declare function loadView(r: ResolvedView): Promise<ViewModule>;
@@ -1,94 +0,0 @@
1
- // loader.ts — resolve + dynamically import view modules across scopes.
2
- //
3
- // A view = a directory containing `view.mjs` (the entry). Resolution order in
4
- // resolveView, first hit wins: project → user → builtin (see scope.ts viewsDir).
5
- // loadView does a plain `import(pathToFileURL(entry).href)` — no transpile, no
6
- // bundle (the published binary is `node dist/cli.js` with no esbuild/tsx) — then
7
- // validates the module is a usable ViewModule, throwing a guided error if not.
8
- import { existsSync, readdirSync, statSync } from 'node:fs';
9
- import { join } from 'node:path';
10
- import { pathToFileURL } from 'node:url';
11
- import { viewsDir } from '../scope.js';
12
- const SCOPE_ORDER = ['project', 'user', 'builtin'];
13
- /** Scope search project→user→builtin; first hit wins. null if no view dir holds
14
- * a `<name>/view.mjs`. */
15
- export function resolveView(name) {
16
- for (const scope of SCOPE_ORDER) {
17
- const root = viewsDir(scope);
18
- if (!root)
19
- continue;
20
- const dir = join(root, name);
21
- const entry = join(dir, 'view.mjs');
22
- if (existsSync(entry))
23
- return { id: name, dir, entry, scope };
24
- }
25
- return null;
26
- }
27
- /** Enumerate every resolvable view across scopes (first scope wins per id) —
28
- * backs `view list` / `view pick`. */
29
- export function listViews() {
30
- const seen = new Set();
31
- const out = [];
32
- for (const scope of SCOPE_ORDER) {
33
- const root = viewsDir(scope);
34
- if (!root || !existsSync(root))
35
- continue;
36
- let names;
37
- try {
38
- names = readdirSync(root);
39
- }
40
- catch {
41
- continue;
42
- }
43
- for (const name of names.sort()) {
44
- if (seen.has(name))
45
- continue; // first scope wins
46
- const dir = join(root, name);
47
- let isDir = false;
48
- try {
49
- isDir = statSync(dir).isDirectory();
50
- }
51
- catch {
52
- isDir = false;
53
- }
54
- if (!isDir)
55
- continue;
56
- const entry = join(dir, 'view.mjs');
57
- if (!existsSync(entry))
58
- continue;
59
- seen.add(name);
60
- out.push({ id: name, dir, entry, scope });
61
- }
62
- }
63
- return out;
64
- }
65
- /** Dynamic-import a resolved view and validate it's a usable ViewModule. Throws
66
- * a guided error (with the entry path) if the module is malformed. */
67
- export async function loadView(r) {
68
- let mod;
69
- try {
70
- mod = (await import(pathToFileURL(r.entry).href));
71
- }
72
- catch (e) {
73
- throw guided(r, `failed to import the module: ${e instanceof Error ? e.message : String(e)}`);
74
- }
75
- const candidate = (mod['default'] ?? mod['view']);
76
- if (!candidate || typeof candidate !== 'object') {
77
- throw guided(r, 'no default export (or named `view`) that is a ViewModule object');
78
- }
79
- const m = candidate.manifest;
80
- if (!m || typeof m.id !== 'string' || typeof m.title !== 'string') {
81
- throw guided(r, 'manifest.id and manifest.title must be strings');
82
- }
83
- if (typeof candidate.init !== 'function')
84
- throw guided(r, 'init() is required');
85
- if (typeof candidate.render !== 'function')
86
- throw guided(r, 'render() is required');
87
- if (typeof candidate.dump !== 'function')
88
- throw guided(r, 'dump() is required');
89
- return candidate;
90
- }
91
- function guided(r, problem) {
92
- return new Error(`invalid view "${r.id}" (${r.scope}) at ${r.entry}: ${problem}.\n` +
93
- 'A view.mjs must `export default` a ViewModule with { manifest: { id, title }, init(), render(), dump() }.');
94
- }
@@ -1,2 +0,0 @@
1
- export declare function skillCreatePrompt(topic: string): string;
2
- export declare function skillTemplatePrompt(type: string, topic: string): string;