@crouton-kit/crouter 0.3.18 → 0.3.20

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 (289) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/PERSONA.md +1 -0
  12. package/dist/builtin-personas/design/orchestrator.md +2 -1
  13. package/dist/builtin-personas/developer/PERSONA.md +1 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +2 -1
  15. package/dist/builtin-personas/explore/PERSONA.md +1 -0
  16. package/dist/builtin-personas/explore/orchestrator.md +4 -0
  17. package/dist/builtin-personas/general/PERSONA.md +1 -0
  18. package/dist/builtin-personas/general/orchestrator.md +4 -0
  19. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  20. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  21. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  22. package/dist/builtin-personas/plan/PERSONA.md +1 -0
  23. package/dist/builtin-personas/plan/orchestrator.md +2 -1
  24. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
  25. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
  26. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
  27. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
  28. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
  29. package/dist/builtin-personas/review/PERSONA.md +1 -0
  30. package/dist/builtin-personas/review/orchestrator.md +4 -0
  31. package/dist/builtin-personas/runtime-base.md +6 -3
  32. package/dist/builtin-personas/spec/PERSONA.md +1 -0
  33. package/dist/builtin-personas/spec/orchestrator.md +2 -1
  34. package/dist/builtin-personas/waiting.md +8 -0
  35. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  36. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  37. package/dist/builtin-views/_lib/states.mjs +161 -0
  38. package/dist/builtin-views/canvas/client.mjs +303 -0
  39. package/dist/builtin-views/canvas/view.mjs +576 -0
  40. package/dist/builtin-views/git-pr/client.mjs +440 -0
  41. package/dist/builtin-views/git-pr/view.mjs +675 -0
  42. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  43. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  44. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  45. package/dist/builtin-views/inbox/view.mjs +889 -0
  46. package/dist/builtin-views/linkedin/client.mjs +610 -0
  47. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  48. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  49. package/dist/clients/attach/attach-cmd.js +354 -0
  50. package/dist/clients/attach/chat-view.d.ts +77 -0
  51. package/dist/clients/attach/chat-view.js +450 -0
  52. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  53. package/dist/clients/attach/clipboard-image.js +113 -0
  54. package/dist/clients/attach/config-load.d.ts +31 -0
  55. package/dist/clients/attach/config-load.js +113 -0
  56. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  57. package/dist/clients/attach/extension-dialogs.js +101 -0
  58. package/dist/clients/attach/input-controller.d.ts +54 -0
  59. package/dist/clients/attach/input-controller.js +204 -0
  60. package/dist/clients/attach/slash-commands.d.ts +36 -0
  61. package/dist/clients/attach/slash-commands.js +200 -0
  62. package/dist/clients/attach/view-socket.d.ts +48 -0
  63. package/dist/clients/attach/view-socket.js +126 -0
  64. package/dist/commands/attention.js +3 -3
  65. package/dist/commands/canvas-prune.js +1 -1
  66. package/dist/commands/daemon.js +3 -2
  67. package/dist/commands/human/prompts.js +1 -1
  68. package/dist/commands/human/queue.js +54 -8
  69. package/dist/commands/human/shared.d.ts +2 -0
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +610 -61
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-attach-limits.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
  108. package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
  110. package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
  111. package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
  112. package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-dialogs.test.js +126 -0
  114. package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
  116. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  117. package/dist/core/__tests__/broker-lifecycle.test.js +87 -0
  118. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  119. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  120. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
  121. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
  122. package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
  123. package/dist/core/__tests__/cascade-close.test.js +12 -2
  124. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  125. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  126. package/dist/core/__tests__/context-intro.test.js +76 -62
  127. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  128. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  129. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  130. package/dist/core/__tests__/detach-focus.test.js +206 -0
  131. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  132. package/dist/core/__tests__/draw-style.test.js +258 -0
  133. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  134. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  135. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  136. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  137. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  138. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  139. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  140. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  141. package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
  142. package/dist/core/__tests__/helpers/broker-clients.js +178 -0
  143. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  144. package/dist/core/__tests__/helpers/harness.js +111 -1
  145. package/dist/core/__tests__/home-session.test.js +41 -8
  146. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  147. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  148. package/dist/core/__tests__/kickoff.test.js +37 -3
  149. package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
  150. package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
  151. package/dist/core/__tests__/live-mutation.test.js +6 -132
  152. package/dist/core/__tests__/memory.test.js +23 -115
  153. package/dist/core/__tests__/placement-focus.test.js +5 -0
  154. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  155. package/dist/core/__tests__/relaunch.test.js +4 -3
  156. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  157. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  158. package/dist/core/__tests__/spawn-root.test.js +10 -0
  159. package/dist/core/__tests__/spike-harness.test.js +1 -0
  160. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  161. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  162. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  163. package/dist/core/__tests__/wake-origin.test.js +110 -0
  164. package/dist/core/bootstrap.js +1 -1
  165. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  166. package/dist/core/canvas/browse/app.js +24 -2
  167. package/dist/core/canvas/browse/model.d.ts +38 -2
  168. package/dist/core/canvas/browse/model.js +134 -10
  169. package/dist/core/canvas/browse/render.d.ts +6 -12
  170. package/dist/core/canvas/browse/render.js +72 -104
  171. package/dist/core/canvas/canvas.js +16 -23
  172. package/dist/core/canvas/db.js +46 -0
  173. package/dist/core/canvas/index.d.ts +1 -0
  174. package/dist/core/canvas/index.js +1 -0
  175. package/dist/core/canvas/pid.d.ts +4 -0
  176. package/dist/core/canvas/pid.js +23 -0
  177. package/dist/core/canvas/render.d.ts +11 -2
  178. package/dist/core/canvas/render.js +69 -0
  179. package/dist/core/canvas/types.d.ts +91 -3
  180. package/dist/core/canvas/wakeups.d.ts +76 -0
  181. package/dist/core/canvas/wakeups.js +185 -0
  182. package/dist/core/config.js +4 -1
  183. package/dist/core/frontmatter.js +37 -124
  184. package/dist/core/memory-resolver.d.ts +49 -0
  185. package/dist/core/memory-resolver.js +141 -0
  186. package/dist/core/personas/loader.d.ts +7 -0
  187. package/dist/core/personas/loader.js +36 -5
  188. package/dist/core/personas/resolve.js +13 -3
  189. package/dist/core/predicate.d.ts +63 -0
  190. package/dist/core/predicate.js +189 -0
  191. package/dist/core/resolver.js +26 -5
  192. package/dist/core/runtime/bearings.d.ts +53 -12
  193. package/dist/core/runtime/bearings.js +132 -59
  194. package/dist/core/runtime/broker-cli.d.ts +1 -0
  195. package/dist/core/runtime/broker-cli.js +46 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  197. package/dist/core/runtime/broker-protocol.js +153 -0
  198. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  199. package/dist/core/runtime/broker-sdk.js +72 -0
  200. package/dist/core/runtime/broker.d.ts +55 -0
  201. package/dist/core/runtime/broker.js +1128 -0
  202. package/dist/core/runtime/close.js +35 -6
  203. package/dist/core/runtime/host.d.ts +53 -0
  204. package/dist/core/runtime/host.js +186 -0
  205. package/dist/core/runtime/kickoff.d.ts +2 -1
  206. package/dist/core/runtime/kickoff.js +91 -5
  207. package/dist/core/runtime/launch.d.ts +54 -4
  208. package/dist/core/runtime/launch.js +85 -5
  209. package/dist/core/runtime/lifecycle.js +23 -6
  210. package/dist/core/runtime/memory.d.ts +2 -42
  211. package/dist/core/runtime/memory.js +11 -162
  212. package/dist/core/runtime/nodes.d.ts +37 -0
  213. package/dist/core/runtime/nodes.js +60 -1
  214. package/dist/core/runtime/persona.js +21 -11
  215. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  216. package/dist/core/runtime/pi-vendored.js +49 -0
  217. package/dist/core/runtime/placement.d.ts +32 -14
  218. package/dist/core/runtime/placement.js +206 -52
  219. package/dist/core/runtime/promote.d.ts +0 -6
  220. package/dist/core/runtime/promote.js +4 -12
  221. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  222. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  223. package/dist/core/runtime/reset.js +11 -8
  224. package/dist/core/runtime/revive.d.ts +2 -0
  225. package/dist/core/runtime/revive.js +34 -34
  226. package/dist/core/runtime/spawn.d.ts +23 -0
  227. package/dist/core/runtime/spawn.js +77 -23
  228. package/dist/core/runtime/stop-guard.d.ts +1 -1
  229. package/dist/core/runtime/stop-guard.js +6 -1
  230. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  231. package/dist/core/runtime/tmux-chrome.js +1 -1
  232. package/dist/core/runtime/tmux.d.ts +28 -0
  233. package/dist/core/runtime/tmux.js +80 -6
  234. package/dist/core/scope.d.ts +11 -0
  235. package/dist/core/scope.js +39 -0
  236. package/dist/core/spawn.d.ts +5 -0
  237. package/dist/core/spawn.js +20 -2
  238. package/dist/core/substrate/gate.d.ts +13 -0
  239. package/dist/core/substrate/gate.js +21 -0
  240. package/dist/core/substrate/index.d.ts +7 -0
  241. package/dist/core/substrate/index.js +18 -0
  242. package/dist/core/substrate/on-read.d.ts +14 -0
  243. package/dist/core/substrate/on-read.js +292 -0
  244. package/dist/core/substrate/render.d.ts +25 -0
  245. package/dist/core/substrate/render.js +256 -0
  246. package/dist/core/substrate/schema.d.ts +76 -0
  247. package/dist/core/substrate/schema.js +124 -0
  248. package/dist/core/substrate/session-cache.d.ts +30 -0
  249. package/dist/core/substrate/session-cache.js +77 -0
  250. package/dist/core/substrate/subject.d.ts +41 -0
  251. package/dist/core/substrate/subject.js +54 -0
  252. package/dist/core/tui/contract.d.ts +83 -0
  253. package/dist/core/tui/contract.js +8 -0
  254. package/dist/core/tui/draw.d.ts +96 -0
  255. package/dist/core/tui/draw.js +339 -0
  256. package/dist/core/tui/host.d.ts +29 -0
  257. package/dist/core/tui/host.js +379 -0
  258. package/dist/core/tui/loader.d.ts +16 -0
  259. package/dist/core/tui/loader.js +94 -0
  260. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  261. package/dist/core/wake.d.ts +86 -0
  262. package/dist/core/wake.js +308 -0
  263. package/dist/daemon/crtrd.d.ts +29 -4
  264. package/dist/daemon/crtrd.js +662 -46
  265. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  266. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  267. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  268. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-commands.js +10 -0
  270. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  271. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  272. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  273. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  274. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  275. package/dist/pi-extensions/canvas-nav.js +87 -23
  276. package/dist/pi-extensions/canvas-stophook.js +17 -8
  277. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  278. package/dist/pi-extensions/canvas-view.js +75 -0
  279. package/dist/prompts/skill.js +19 -26
  280. package/dist/prompts/view.d.ts +7 -0
  281. package/dist/prompts/view.js +101 -0
  282. package/dist/types.d.ts +4 -0
  283. package/dist/types.js +1 -0
  284. package/package.json +9 -4
  285. package/dist/commands/skill/find.d.ts +0 -4
  286. package/dist/commands/skill/find.js +0 -257
  287. package/dist/commands/skill/read.js +0 -91
  288. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  289. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -0,0 +1,200 @@
1
+ // slash-commands.ts — builtin slash-command parse + dispatch for `crtr attach` (C6).
2
+ //
3
+ // Builtin slash commands are NOT engine-interpreted: `session.prompt('/model')`
4
+ // ships "/model" to the LLM as literal text. So the viewer recognizes the 21
5
+ // vendored `BUILTIN_SLASH_COMMANDS` itself and dispatches the matching
6
+ // `ClientToBroker` command frame; only EXTENSION commands (anything the viewer
7
+ // doesn't recognize) fall through to be sent as a `prompt` for the engine's
8
+ // extension runner to handle.
9
+ //
10
+ // SELECTORS — the Phase-4 reality. The plan called for `/model`, `/resume`,
11
+ // `/fork`, `/tree`, `/settings` to open pi's exported `*SelectorComponent`s. The
12
+ // authoritative 0.78.1 .d.ts shows those components require ENGINE-side state a
13
+ // socket-only viewer does not have (`ModelSelectorComponent` needs the
14
+ // `ModelRegistry` + `SettingsManager`; `SessionSelectorComponent` needs session
15
+ // loaders; `TreeSelectorComponent` needs the `SessionTreeNode[]` tree), and the
16
+ // §1.2 command-frame floor set exposes NO read op to fetch any of it. So in
17
+ // Phase 4 these resolve via a TEXT ARGUMENT — `/model <id>`, `/resume <path>`,
18
+ // `/fork <entryId>`, `/tree <entryId>` — which sends the SAME resolved value the
19
+ // selector would have, i.e. zero engine capability is lost; only the interactive
20
+ // picker UI waits for a future read op. A no-arg invocation shows a one-line
21
+ // notice with the arg form. (This protocol gap is reported up to the parent.)
22
+ //
23
+ // SCOPED OUT (review m2 — no engine method): `/login`, `/logout`, `/share`,
24
+ // `/trust` — a brief "not supported in attach" notice, never a frame.
25
+ import { BUILTIN_SLASH_COMMANDS } from '../../core/runtime/pi-vendored.js';
26
+ /** Builtins with no Phase-4 engine method — scoped out (review m2). */
27
+ const SCOPED_OUT = new Set(['login', 'logout', 'share', 'trust']);
28
+ /** Valid `/settings thinking` levels — the `SetThinkingLevelFrame['level']`
29
+ * union (= AgentSession['thinkingLevel'], pi-agent-core ThinkingLevel),
30
+ * mirrored as a runtime set so an invalid arg is rejected with the options
31
+ * rather than shipped as a frame the engine would throw on. Built from a
32
+ * `Record<level, true>` so the build fails BOTH ways if the union drifts: a
33
+ * missing key (union GAINED a level) or an extra key (union dropped one). */
34
+ const THINKING_LEVELS = new Set(Object.keys({
35
+ off: true,
36
+ minimal: true,
37
+ low: true,
38
+ medium: true,
39
+ high: true,
40
+ xhigh: true,
41
+ }));
42
+ const BUILTIN_NAMES = new Set(BUILTIN_SLASH_COMMANDS.map((c) => c.name));
43
+ /** True if `text` is a leading-slash command (vs. a normal prompt). */
44
+ export function isSlashCommand(text) {
45
+ return text.trimStart().startsWith('/');
46
+ }
47
+ /**
48
+ * Parse + dispatch a leading-slash command. Returns `true` if it was handled
49
+ * (a recognized builtin or a scoped-out notice) — the caller then clears the
50
+ * editor and sends nothing else. Returns `false` for an UNRECOGNIZED command,
51
+ * which the caller forwards to the engine as a `prompt` (extension command).
52
+ */
53
+ export function dispatchSlashCommand(text, ctx) {
54
+ const m = /^\/(\S+)\s*([\s\S]*)$/.exec(text.trim());
55
+ if (!m)
56
+ return false;
57
+ const name = m[1];
58
+ const arg = m[2].trim();
59
+ if (SCOPED_OUT.has(name)) {
60
+ ctx.notify(`/${name} is not supported in attach (Phase 4)`);
61
+ return true;
62
+ }
63
+ // Unknown to us AND not a builtin → let the engine's extension runner have it.
64
+ if (!BUILTIN_NAMES.has(name))
65
+ return false;
66
+ switch (name) {
67
+ // --- direct frames -----------------------------------------------------
68
+ case 'new':
69
+ ctx.send({ type: 'new_session' });
70
+ return true;
71
+ case 'reload':
72
+ ctx.send({ type: 'reload' });
73
+ return true;
74
+ case 'compact':
75
+ ctx.send({ type: 'compact', instructions: arg || undefined });
76
+ return true;
77
+ case 'quit':
78
+ // A viewer "quit" DETACHES — the shared engine keeps running (one-writer).
79
+ ctx.send({ type: 'bye' });
80
+ ctx.notify('Detaching — the engine keeps running');
81
+ return true;
82
+ // --- frames needing one argument --------------------------------------
83
+ case 'name':
84
+ if (!arg)
85
+ return usage(ctx, '/name <display name>');
86
+ ctx.send({ type: 'set_session_name', name: arg });
87
+ return true;
88
+ case 'export': {
89
+ const { path, format } = parseExport(arg, ctx.cwd ?? process.cwd());
90
+ ctx.send({ type: 'export', path, format });
91
+ ctx.notify(`Exporting session → ${path}`);
92
+ return true;
93
+ }
94
+ case 'import':
95
+ // `/import` resumes a session built from a JSONL file → switch_session.
96
+ if (!arg)
97
+ return usage(ctx, '/import <path-to-session.jsonl>');
98
+ ctx.send({ type: 'switch_session', path: arg });
99
+ return true;
100
+ // --- value-picker builtins: arg form (selector deferred, see header) ---
101
+ case 'model':
102
+ if (!arg)
103
+ return pickerHint(ctx, 'model', '/model <id>');
104
+ ctx.send({ type: 'set_model', model: arg });
105
+ return true;
106
+ case 'resume':
107
+ if (!arg)
108
+ return pickerHint(ctx, 'session', '/resume <session-path>');
109
+ ctx.send({ type: 'switch_session', path: arg });
110
+ return true;
111
+ case 'fork':
112
+ if (!arg)
113
+ return pickerHint(ctx, 'fork point', '/fork <entryId>');
114
+ ctx.send({ type: 'fork', entryId: arg });
115
+ return true;
116
+ case 'tree':
117
+ if (!arg)
118
+ return pickerHint(ctx, 'tree', '/tree <entryId>');
119
+ ctx.send({ type: 'navigate_tree', targetId: arg });
120
+ return true;
121
+ // --- settings (sub-command form; interactive menu needs read ops) ------
122
+ case 'settings':
123
+ return handleSettings(arg, ctx);
124
+ // --- read-only / informational ----------------------------------------
125
+ case 'session':
126
+ ctx.notify(describeSession(ctx.state));
127
+ return true;
128
+ // --- recognized builtins with no Phase-4 wire path --------------------
129
+ case 'copy':
130
+ case 'changelog':
131
+ case 'hotkeys':
132
+ case 'scoped-models':
133
+ case 'clone':
134
+ ctx.notify(`/${name} is not available in attach (Phase 4)`);
135
+ return true;
136
+ default:
137
+ // A builtin we listed but didn't special-case: treat as unsupported
138
+ // rather than leak it to the LLM as literal text.
139
+ ctx.notify(`/${name} is not available in attach (Phase 4)`);
140
+ return true;
141
+ }
142
+ }
143
+ /** Build slash-command autocomplete entries from the merged command list (the
144
+ * broker's `get_commands` result, which T7 may inject) — defaults to the
145
+ * vendored builtins. Shaped for pi-tui's `CombinedAutocompleteProvider`. */
146
+ export function slashCommandList(commands = BUILTIN_SLASH_COMMANDS) {
147
+ return commands.map((c) => ({ name: c.name, description: c.description }));
148
+ }
149
+ /** Same list as flat autocomplete items (value/label/description). */
150
+ export function commandAutocompleteItems(commands = BUILTIN_SLASH_COMMANDS) {
151
+ return commands.map((c) => ({ value: `/${c.name}`, label: `/${c.name}`, description: c.description }));
152
+ }
153
+ // ---------------------------------------------------------------------------
154
+ function usage(ctx, form) {
155
+ ctx.notify(`Usage: ${form}`);
156
+ return true;
157
+ }
158
+ function pickerHint(ctx, what, form) {
159
+ ctx.notify(`Interactive ${what} picker isn't available over view.sock yet — use ${form}`);
160
+ return true;
161
+ }
162
+ function handleSettings(arg, ctx) {
163
+ const [sub, ...rest] = arg.split(/\s+/);
164
+ const value = rest.join(' ').trim();
165
+ switch (sub) {
166
+ case 'thinking':
167
+ if (!value)
168
+ return usage(ctx, '/settings thinking <level>');
169
+ if (!THINKING_LEVELS.has(value)) {
170
+ ctx.notify(`Invalid thinking level "${value}" — choose one of: ${[...THINKING_LEVELS].join(', ')}`);
171
+ return true;
172
+ }
173
+ ctx.send({ type: 'set_thinking_level', level: value });
174
+ return true;
175
+ case 'auto-retry':
176
+ ctx.send({ type: 'set_auto_retry', enabled: parseBool(value) });
177
+ return true;
178
+ case 'auto-compaction':
179
+ ctx.send({ type: 'set_auto_compaction', enabled: parseBool(value) });
180
+ return true;
181
+ default:
182
+ ctx.notify('Settings: /settings thinking <level> | auto-retry on|off | auto-compaction on|off ' +
183
+ '(the interactive settings menu needs data not exposed over view.sock in Phase 4)');
184
+ return true;
185
+ }
186
+ }
187
+ function parseBool(value) {
188
+ return /^(on|true|yes|1|enabled?)$/i.test(value.trim());
189
+ }
190
+ function parseExport(arg, cwd) {
191
+ const path = arg || `${cwd}/pi-export-${Date.now()}.html`;
192
+ const format = path.endsWith('.jsonl') ? 'jsonl' : 'html';
193
+ return { path, format };
194
+ }
195
+ function describeSession(state) {
196
+ if (!state)
197
+ return 'Session info unavailable';
198
+ const name = state.sessionName ? `"${state.sessionName}" ` : '';
199
+ return `Session ${name}${state.sessionId} · model ${state.model ?? 'unknown'} · thinking ${state.thinkingLevel}`;
200
+ }
@@ -0,0 +1,48 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { type BrokerToClient, type ClientToBroker } from '../../core/runtime/broker-protocol.js';
3
+ /** Surfaced when the node has no reachable broker at connect time (no socket
4
+ * file, or a stale socket with nothing listening). The command catches this to
5
+ * exit non-zero with a focus/revive hint. */
6
+ export declare class BrokerUnavailableError extends Error {
7
+ readonly nodeId: string;
8
+ constructor(nodeId: string);
9
+ }
10
+ /** The plan-fixed interface (Wave 3): `connect()`, `on('frame', …)`,
11
+ * `send(frame)`, `on('close', …)`, plus `connect`/`error` events. */
12
+ export interface ViewSocketClient {
13
+ on(event: 'connect', listener: () => void): this;
14
+ on(event: 'frame', listener: (frame: BrokerToClient) => void): this;
15
+ on(event: 'close', listener: () => void): this;
16
+ on(event: 'error', listener: (err: Error) => void): this;
17
+ once(event: 'connect', listener: () => void): this;
18
+ once(event: 'frame', listener: (frame: BrokerToClient) => void): this;
19
+ once(event: 'close', listener: () => void): this;
20
+ once(event: 'error', listener: (err: Error) => void): this;
21
+ off(event: 'connect', listener: () => void): this;
22
+ off(event: 'error', listener: (err: Error) => void): this;
23
+ }
24
+ export declare class ViewSocketClient extends EventEmitter {
25
+ private readonly nodeId;
26
+ private socket;
27
+ private readonly decoder;
28
+ private closeEmitted;
29
+ constructor(nodeId: string);
30
+ /** The broker binds `join(nodeDir(id), 'view.sock')` — resolve it the same way. */
31
+ get socketPath(): string;
32
+ /** Open the connection. Wire up listeners; emits `connect` on success or
33
+ * `error` (a {@link BrokerUnavailableError} for ECONNREFUSED/ENOENT) on
34
+ * failure. Idempotent guard is the caller's job — call once. */
35
+ connect(): void;
36
+ /** Encode + write one client→broker frame. No-op on a dead/absent socket
37
+ * (a `close` event drives teardown); never throws. */
38
+ send(frame: ClientToBroker): void;
39
+ /** Detach: destroy the socket. `close` fires → the caller tears down. */
40
+ close(): void;
41
+ private onData;
42
+ private onError;
43
+ private onClose;
44
+ private destroy;
45
+ /** Emit `error` only when a listener exists — a bare EventEmitter `error`
46
+ * with no listener throws, and this client must never throw uncaught. */
47
+ private emitError;
48
+ }
@@ -0,0 +1,126 @@
1
+ // view-socket.ts — the socket client half of `crtr attach` (Phase 4, T7).
2
+ //
3
+ // The §0 ONE-WRITER INVARIANT lives in this directory: a viewer has ONLY a
4
+ // socket. ViewSocketClient connects to a node's ALREADY-running headless broker
5
+ // over its unix socket (`nodeDir(id)/view.sock`) and speaks the LOCAL
6
+ // broker-protocol codec (`node:net` + the bounded `FrameDecoder`). It NEVER
7
+ // spawns pi, NEVER opens the session `.jsonl`, NEVER constructs an engine — if
8
+ // the broker is not running, connect fails and `attach-cmd` exits (focus/T9 is
9
+ // what keeps a broker alive, not attach). It also never uses pi's `RpcClient`
10
+ // (which would spawn its own engine).
11
+ //
12
+ // No reconnect: a dropped broker means the engine's turn-cycle settled and the
13
+ // broker disposed+exited; a fresh focus re-runs attach. On EOF/close → emit
14
+ // `close` (the caller renders "broker gone" + exits). On a connect-time
15
+ // ECONNREFUSED/ENOENT → a `BrokerUnavailableError` so the command exits non-zero
16
+ // with a clear message. Decode is bounded by `CLIENT_READ_CAPS`; a broker that
17
+ // somehow sends an oversized frame surfaces as a clean error+exit, not a crash.
18
+ import { EventEmitter } from 'node:events';
19
+ import { createConnection } from 'node:net';
20
+ import { join } from 'node:path';
21
+ import { nodeDir } from '../../core/canvas/paths.js';
22
+ import { CLIENT_READ_CAPS, encodeFrame, FrameDecoder, FrameOverflowError, } from '../../core/runtime/broker-protocol.js';
23
+ /** Surfaced when the node has no reachable broker at connect time (no socket
24
+ * file, or a stale socket with nothing listening). The command catches this to
25
+ * exit non-zero with a focus/revive hint. */
26
+ export class BrokerUnavailableError extends Error {
27
+ nodeId;
28
+ constructor(nodeId) {
29
+ super(`node ${nodeId} has no running broker — focus or revive it first`);
30
+ this.nodeId = nodeId;
31
+ this.name = 'BrokerUnavailableError';
32
+ }
33
+ }
34
+ export class ViewSocketClient extends EventEmitter {
35
+ nodeId;
36
+ socket;
37
+ decoder = new FrameDecoder(CLIENT_READ_CAPS);
38
+ closeEmitted = false;
39
+ constructor(nodeId) {
40
+ super();
41
+ this.nodeId = nodeId;
42
+ }
43
+ /** The broker binds `join(nodeDir(id), 'view.sock')` — resolve it the same way. */
44
+ get socketPath() {
45
+ return join(nodeDir(this.nodeId), 'view.sock');
46
+ }
47
+ /** Open the connection. Wire up listeners; emits `connect` on success or
48
+ * `error` (a {@link BrokerUnavailableError} for ECONNREFUSED/ENOENT) on
49
+ * failure. Idempotent guard is the caller's job — call once. */
50
+ connect() {
51
+ const socket = createConnection(this.socketPath);
52
+ this.socket = socket;
53
+ socket.on('connect', () => this.emit('connect'));
54
+ socket.on('data', (chunk) => this.onData(chunk));
55
+ socket.on('error', (err) => this.onError(err));
56
+ socket.on('close', () => this.onClose());
57
+ }
58
+ /** Encode + write one client→broker frame. No-op on a dead/absent socket
59
+ * (a `close` event drives teardown); never throws. */
60
+ send(frame) {
61
+ const sock = this.socket;
62
+ if (sock === undefined || sock.destroyed)
63
+ return;
64
+ try {
65
+ sock.write(encodeFrame(frame));
66
+ }
67
+ catch {
68
+ /* dead socket — 'close' will fire and drive teardown */
69
+ }
70
+ }
71
+ /** Detach: destroy the socket. `close` fires → the caller tears down. */
72
+ close() {
73
+ this.destroy();
74
+ }
75
+ onData(chunk) {
76
+ let frames;
77
+ try {
78
+ frames = this.decoder.push(chunk);
79
+ }
80
+ catch (err) {
81
+ // A broker that somehow emits an oversized frame (or a corrupt stream):
82
+ // clean error + drop, never a crash.
83
+ const msg = err instanceof FrameOverflowError
84
+ ? `broker sent an oversized frame (${err.message}) — disconnecting`
85
+ : `failed to decode a broker frame: ${String(err)}`;
86
+ this.emitError(new Error(msg));
87
+ this.destroy();
88
+ return;
89
+ }
90
+ for (const raw of frames) {
91
+ this.emit('frame', raw);
92
+ }
93
+ }
94
+ onError(err) {
95
+ if (err.code === 'ECONNREFUSED' || err.code === 'ENOENT') {
96
+ this.emitError(new BrokerUnavailableError(this.nodeId));
97
+ }
98
+ else {
99
+ this.emitError(new Error(`view socket error: ${err.message}`));
100
+ }
101
+ // 'close' typically follows 'error'; teardown converges there.
102
+ }
103
+ onClose() {
104
+ if (this.closeEmitted)
105
+ return;
106
+ this.closeEmitted = true;
107
+ this.emit('close');
108
+ }
109
+ destroy() {
110
+ const sock = this.socket;
111
+ if (sock !== undefined && !sock.destroyed) {
112
+ try {
113
+ sock.destroy();
114
+ }
115
+ catch {
116
+ /* ignore */
117
+ }
118
+ }
119
+ }
120
+ /** Emit `error` only when a listener exists — a bare EventEmitter `error`
121
+ * with no listener throws, and this client must never throw uncaught. */
122
+ emitError(err) {
123
+ if (this.listenerCount('error') > 0)
124
+ this.emit('error', err);
125
+ }
126
+ }
@@ -21,7 +21,7 @@ import { countAsks, pendingAsksForView, asksAcrossCanvas, asksForNodes, } from '
21
21
  const attentionCount = defineLeaf({
22
22
  name: 'count',
23
23
  description: 'total pending ask count (machine-parseable stdout.count)',
24
- whenToUse: 'getting a single pending-ask count for a script or nav chrome to read (stdout.count is machine-parseable); scope with --node or --view, or default to canvas-wide',
24
+ whenToUse: 'getting a single pending-ask count for a script or nav chrome to read; scope it to a node or a sub-DAG, or default to canvas-wide',
25
25
  help: {
26
26
  name: 'canvas attention count',
27
27
  // stdout.count is parsed directly by the nav chrome — keep the contract.
@@ -93,7 +93,7 @@ const attentionCount = defineLeaf({
93
93
  const attentionList = defineLeaf({
94
94
  name: 'list',
95
95
  description: 'itemised list of cwds with pending asks',
96
- whenToUse: 'finding which agents are blocked waiting on a human — an itemised list of the cwds with pending asks, oldest first, so you know where to go answer. Scope to a sub-DAG with --view or list canvas-wide. Use `canvas attention count` instead when a script just needs the number, or `canvas attention map` for per-node counts to label a UI',
96
+ whenToUse: 'finding which agents are blocked waiting on a human — an itemised list of the cwds with pending asks, oldest first, so you know where to go answer. Scope it to a sub-DAG or list canvas-wide. Use `canvas attention count` instead when a script just needs the number, or `canvas attention map` for per-node counts to label a UI',
97
97
  help: {
98
98
  name: 'canvas attention list',
99
99
  summary: 'list nodes with pending human asks, grouped by cwd, oldest first',
@@ -211,7 +211,7 @@ const attentionMap = defineLeaf({
211
211
  export const attentionBranch = defineBranch({
212
212
  name: 'attention',
213
213
  description: 'count/list pending human asks across the graph',
214
- whenToUse: 'checking whether any agent on the canvas is blocked waiting on a human, and where: count the pending asks, list the cwds that have them, or map per-node counts. Scope with --node or --view, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
214
+ whenToUse: 'checking whether any agent on the canvas is blocked waiting on a human, and where: count the pending asks, list the cwds that have them, or map per-node counts. Scope it to a node or a sub-DAG, or go canvas-wide. Use `canvas dashboard` instead for the graph SHAPE, or `node inspect list` for a plain node roster',
215
215
  help: {
216
216
  name: 'canvas attention',
217
217
  summary: 'aggregate pending human asks across the canvas',
@@ -13,7 +13,7 @@ const DEFAULT_TTL_DAYS = 14;
13
13
  export const canvasPruneLeaf = defineLeaf({
14
14
  name: 'prune',
15
15
  description: 'remove terminal nodes (dead/done/canceled) older than a TTL',
16
- whenToUse: 'you want to bound the canvas\u2019s on-disk growth \u2014 sweep away nodes that are finished (done), crashed (dead), or closed (canceled) and older than a retention window, reclaiming their rows, edges (cascade-deleted by the schema), and `nodes/<id>/` dirs. Run it as an operator/cron chore; live nodes (active/idle) are never touched. Pass `--dry-run` first to see exactly what would go, `--ttl <days>` to widen or tighten the window',
16
+ whenToUse: 'you want to bound the canvas\u2019s on-disk growth \u2014 sweep away nodes that are finished (done), crashed (dead), or closed (canceled) and older than a retention window, reclaiming their rows, edges (cascade-deleted by the schema), and `nodes/<id>/` dirs. Run it as an operator/cron chore; live nodes (active/idle) are never touched, and you can preview the sweep before it deletes anything',
17
17
  help: {
18
18
  name: 'canvas prune',
19
19
  summary: 'delete terminal nodes older than a TTL (edges cascade, dirs removed); --dry-run to preview',
@@ -1,7 +1,8 @@
1
1
  // `crtr canvas daemon` — thin supervisor daemon management.
2
2
  //
3
- // The daemon (crtrd) polls active+idle nodes and handles window exit:
4
- // crash (window gone, no intent) mark 'dead'
3
+ // The daemon (crtrd) polls active+idle nodes and handles engine-container exit
4
+ // (a tmux pane or a headless broker process):
5
+ // • crash (container gone, no intent) → mark 'dead'
5
6
  // • refresh-yield (intent=refresh) → fresh respawn
6
7
  //
7
8
  // This subtree starts, checks, and stops the daemon process.
@@ -132,7 +132,7 @@ export const humanReview = defineLeaf({
132
132
  outputKind: 'object',
133
133
  effects: [
134
134
  'Creates a kind:"human" node under you and writes run.json to the interaction dir.',
135
- 'Spawns a live, read-only review session (in a detached tmux pane when in tmux) that tracks the file and re-renders on save.',
135
+ 'Spawns a live review surface (detached tmux panes when in tmux): the raw source in a read-only editor for anchoring comments, beside a termrender-rendered view of the doc; both track the file and re-render on save.',
136
136
  'Returns instantly; the anchored comments fan into your inbox when the human submits (off-tmux, drain via `crtr human inbox`).',
137
137
  ],
138
138
  },
@@ -8,7 +8,8 @@ import { transition } from '../../core/runtime/lifecycle.js';
8
8
  import { appendInbox } from '../../core/feed/inbox.js';
9
9
  import { existsSync } from 'node:fs';
10
10
  import { join } from 'node:path';
11
- import { inbox, scanInbox, parseDeck, deckPath, responsePath, isResolved, atomicWriteJson, ask, launchReview, readJson, } from '@crouton-kit/humanloop';
11
+ import { spawnSync } from 'node:child_process';
12
+ import { inbox, scanInbox, parseDeck, deckPath, responsePath, isResolved, atomicWriteJson, ask, launchReview, readJson, display, } from '@crouton-kit/humanloop';
12
13
  import { killPane } from './shared.js';
13
14
  // ---------------------------------------------------------------------------
14
15
  // inbox (human-invoked, blocking)
@@ -136,6 +137,13 @@ export const humanCancel = defineLeaf({
136
137
  const rc = readJson(join(idir, 'run.json'));
137
138
  if (rc?.pane_id !== undefined && rc.pane_id !== '')
138
139
  killPane(rc.pane_id, idir);
140
+ // A review also opened a live termrender render pane beside the editor —
141
+ // kill it too or it outlives the canceled job. Verified against the
142
+ // reviewed file path, which termrender bakes into the pane's start command
143
+ // (`termrender doc watch ... <file>`), so this can only ever hit our pane.
144
+ if (rc?.render_pane_id !== undefined && rc.render_pane_id !== '' && typeof rc.file === 'string' && rc.file !== '') {
145
+ killPane(rc.render_pane_id, rc.file);
146
+ }
139
147
  // (2) Drop it from the human queue: a response.json marks the dir resolved,
140
148
  // so scanInbox (human list/inbox) skips it.
141
149
  if (existsSync(idir)) {
@@ -218,13 +226,51 @@ export const humanRun = defineLeaf({
218
226
  else if (rc.mode === 'review') {
219
227
  // The _run worker is already its own dedicated tmux pane with a TTY, so
220
228
  // run nvim directly in it (noTmux) instead of letting launchReview
221
- // split off a SECOND pane and sit polling. This matches how ask/approve
222
- // render in-place and avoids the redundant side pane.
223
- const res = await launchReview(rc.file, {
224
- output: rc.output,
225
- noTmux: true,
226
- });
227
- await pushFinal(rc.job_id, JSON.stringify(res));
229
+ // split off a SECOND pane and sit polling.
230
+ //
231
+ // BUG REGRESSION (raw-markdown review): the nvim buffer must stay the
232
+ // RAW source — anchored comments hang off source line numbers — so the
233
+ // termrender render the help promises (panels/callouts/mermaid) lives
234
+ // in its OWN live pane opened beside this worker. `display` spawns the
235
+ // managed termrender binary in watch mode, so it re-renders on every
236
+ // save exactly like the nvim buffer reloads. Best-effort: off-tmux or
237
+ // renderer-unavailable degrades to editor-only. The pane id is merged
238
+ // into run.json so `human cancel` can kill it; the finally clears it on
239
+ // every exit of the editor (submit, quit, or failure).
240
+ let renderPane;
241
+ try {
242
+ renderPane = display(rc.file, { window: 'split' }).paneId;
243
+ }
244
+ catch {
245
+ /* render pane is best-effort; the review itself must not die */
246
+ }
247
+ // Dock the render pane BESIDE this worker pane. tmux resolves an
248
+ // untargeted split-window against the attached client's current window
249
+ // (which wins over $TMUX_PANE), so the pane can land in whatever window
250
+ // the user happens to be looking at — away from the editor. move-pane
251
+ // with explicit src/dst is deterministic: raw source and rendered doc
252
+ // always sit side by side.
253
+ const selfPane = process.env['TMUX_PANE'];
254
+ if (renderPane !== undefined && selfPane !== undefined && selfPane !== '') {
255
+ spawnSync('tmux', ['move-pane', '-h', '-s', renderPane, '-t', selfPane], { stdio: 'ignore' });
256
+ }
257
+ if (renderPane !== undefined) {
258
+ const rcPath = join(dir, 'run.json');
259
+ const cur = readJson(rcPath);
260
+ if (cur !== null)
261
+ atomicWriteJson(rcPath, { ...cur, render_pane_id: renderPane });
262
+ }
263
+ try {
264
+ const res = await launchReview(rc.file, {
265
+ output: rc.output,
266
+ noTmux: true,
267
+ });
268
+ await pushFinal(rc.job_id, JSON.stringify(res));
269
+ }
270
+ finally {
271
+ if (renderPane !== undefined)
272
+ killPane(renderPane, rc.file);
273
+ }
228
274
  }
229
275
  }
230
276
  catch (e) {
@@ -8,6 +8,8 @@ export interface RunRecord {
8
8
  output?: string;
9
9
  /** tmux pane id of the detached TUI, recorded so `human cancel` can kill it. */
10
10
  pane_id?: string;
11
+ /** tmux pane id of a review's live termrender render pane, so cancel/finish can clear it. */
12
+ render_pane_id?: string;
11
13
  }
12
14
  export declare function resolveMaxPanes(): number;
13
15
  export declare function pickPlacement(targetPane?: string): 'split-h' | 'new-window';
@@ -0,0 +1 @@
1
+ export declare const findLeaf: import("../../core/command.js").LeafDef;