@crouton-kit/crouter 0.3.17 → 0.3.19

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 (283) 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/{base.md → PERSONA.md} +4 -0
  12. package/dist/builtin-personas/design/orchestrator.md +1 -1
  13. package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  15. package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
  16. package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
  17. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  18. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  19. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  20. package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
  21. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  22. package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
  23. package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
  24. package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
  25. package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
  26. package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
  27. package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
  28. package/dist/builtin-personas/runtime-base.md +6 -3
  29. package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
  30. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  31. package/dist/builtin-personas/waiting.md +8 -0
  32. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
  33. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
  34. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  35. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  36. package/dist/builtin-views/_lib/states.mjs +161 -0
  37. package/dist/builtin-views/canvas/client.mjs +303 -0
  38. package/dist/builtin-views/canvas/view.mjs +576 -0
  39. package/dist/builtin-views/git-pr/client.mjs +440 -0
  40. package/dist/builtin-views/git-pr/view.mjs +675 -0
  41. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  42. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  43. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  44. package/dist/builtin-views/inbox/view.mjs +889 -0
  45. package/dist/builtin-views/linkedin/client.mjs +610 -0
  46. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  47. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  48. package/dist/clients/attach/attach-cmd.js +354 -0
  49. package/dist/clients/attach/chat-view.d.ts +77 -0
  50. package/dist/clients/attach/chat-view.js +450 -0
  51. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  52. package/dist/clients/attach/clipboard-image.js +113 -0
  53. package/dist/clients/attach/config-load.d.ts +31 -0
  54. package/dist/clients/attach/config-load.js +113 -0
  55. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  56. package/dist/clients/attach/extension-dialogs.js +101 -0
  57. package/dist/clients/attach/input-controller.d.ts +54 -0
  58. package/dist/clients/attach/input-controller.js +204 -0
  59. package/dist/clients/attach/slash-commands.d.ts +36 -0
  60. package/dist/clients/attach/slash-commands.js +200 -0
  61. package/dist/clients/attach/view-socket.d.ts +48 -0
  62. package/dist/clients/attach/view-socket.js +126 -0
  63. package/dist/commands/attention.js +3 -3
  64. package/dist/commands/canvas-prune.js +1 -1
  65. package/dist/commands/daemon.js +4 -3
  66. package/dist/commands/human/prompts.js +4 -10
  67. package/dist/commands/human/queue.js +43 -8
  68. package/dist/commands/human/shared.d.ts +28 -1
  69. package/dist/commands/human/shared.js +48 -10
  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 +660 -65
  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-lifecycle.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  108. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  110. package/dist/core/__tests__/cascade-close.test.js +12 -2
  111. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  112. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  113. package/dist/core/__tests__/context-intro.test.js +76 -62
  114. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  115. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  116. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  117. package/dist/core/__tests__/detach-focus.test.js +206 -0
  118. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  119. package/dist/core/__tests__/draw-style.test.js +258 -0
  120. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  121. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  122. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  123. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  124. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  125. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  126. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  127. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  128. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  129. package/dist/core/__tests__/helpers/harness.js +111 -1
  130. package/dist/core/__tests__/home-session.test.js +41 -8
  131. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  132. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  133. package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
  134. package/dist/core/__tests__/human-surface-target.test.js +98 -0
  135. package/dist/core/__tests__/kickoff.test.js +37 -3
  136. package/dist/core/__tests__/live-mutation.test.js +50 -33
  137. package/dist/core/__tests__/memory.test.js +23 -115
  138. package/dist/core/__tests__/persona-subkind.test.js +18 -15
  139. package/dist/core/__tests__/placement-focus.test.js +5 -0
  140. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  141. package/dist/core/__tests__/relaunch.test.js +4 -3
  142. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  143. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  144. package/dist/core/__tests__/spawn-root.test.js +10 -0
  145. package/dist/core/__tests__/spike-harness.test.js +1 -0
  146. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  147. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  148. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  149. package/dist/core/__tests__/wake-origin.test.js +110 -0
  150. package/dist/core/bootstrap.js +1 -1
  151. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  152. package/dist/core/canvas/browse/app.js +24 -2
  153. package/dist/core/canvas/browse/model.d.ts +38 -2
  154. package/dist/core/canvas/browse/model.js +134 -10
  155. package/dist/core/canvas/browse/render.d.ts +6 -12
  156. package/dist/core/canvas/browse/render.js +72 -104
  157. package/dist/core/canvas/canvas.js +15 -22
  158. package/dist/core/canvas/db.js +46 -0
  159. package/dist/core/canvas/index.d.ts +1 -0
  160. package/dist/core/canvas/index.js +1 -0
  161. package/dist/core/canvas/paths.d.ts +4 -1
  162. package/dist/core/canvas/paths.js +10 -4
  163. package/dist/core/canvas/pid.d.ts +4 -0
  164. package/dist/core/canvas/pid.js +23 -0
  165. package/dist/core/canvas/render.d.ts +11 -2
  166. package/dist/core/canvas/render.js +69 -0
  167. package/dist/core/canvas/types.d.ts +85 -3
  168. package/dist/core/canvas/types.js +2 -2
  169. package/dist/core/canvas/wakeups.d.ts +76 -0
  170. package/dist/core/canvas/wakeups.js +185 -0
  171. package/dist/core/config.js +4 -1
  172. package/dist/core/frontmatter.js +37 -124
  173. package/dist/core/help.d.ts +6 -0
  174. package/dist/core/help.js +7 -0
  175. package/dist/core/memory-resolver.d.ts +49 -0
  176. package/dist/core/memory-resolver.js +141 -0
  177. package/dist/core/personas/index.d.ts +4 -3
  178. package/dist/core/personas/index.js +3 -2
  179. package/dist/core/personas/loader.d.ts +41 -16
  180. package/dist/core/personas/loader.js +133 -29
  181. package/dist/core/personas/resolve.d.ts +4 -4
  182. package/dist/core/personas/resolve.js +28 -16
  183. package/dist/core/predicate.d.ts +63 -0
  184. package/dist/core/predicate.js +189 -0
  185. package/dist/core/resolver.js +26 -5
  186. package/dist/core/runtime/bearings.d.ts +53 -12
  187. package/dist/core/runtime/bearings.js +132 -59
  188. package/dist/core/runtime/broker-cli.d.ts +1 -0
  189. package/dist/core/runtime/broker-cli.js +46 -0
  190. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  191. package/dist/core/runtime/broker-protocol.js +153 -0
  192. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  193. package/dist/core/runtime/broker-sdk.js +72 -0
  194. package/dist/core/runtime/broker.d.ts +55 -0
  195. package/dist/core/runtime/broker.js +1128 -0
  196. package/dist/core/runtime/close.js +35 -6
  197. package/dist/core/runtime/host.d.ts +53 -0
  198. package/dist/core/runtime/host.js +186 -0
  199. package/dist/core/runtime/kickoff.d.ts +2 -1
  200. package/dist/core/runtime/kickoff.js +91 -5
  201. package/dist/core/runtime/launch.d.ts +45 -2
  202. package/dist/core/runtime/launch.js +65 -2
  203. package/dist/core/runtime/lifecycle.js +23 -6
  204. package/dist/core/runtime/memory.d.ts +2 -42
  205. package/dist/core/runtime/memory.js +11 -162
  206. package/dist/core/runtime/nodes.d.ts +33 -0
  207. package/dist/core/runtime/nodes.js +59 -1
  208. package/dist/core/runtime/persona.js +21 -11
  209. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  210. package/dist/core/runtime/pi-vendored.js +49 -0
  211. package/dist/core/runtime/placement.d.ts +42 -14
  212. package/dist/core/runtime/placement.js +228 -38
  213. package/dist/core/runtime/promote.d.ts +0 -6
  214. package/dist/core/runtime/promote.js +1 -12
  215. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  216. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  217. package/dist/core/runtime/reset.js +8 -6
  218. package/dist/core/runtime/revive.d.ts +2 -0
  219. package/dist/core/runtime/revive.js +34 -34
  220. package/dist/core/runtime/spawn.d.ts +19 -0
  221. package/dist/core/runtime/spawn.js +75 -22
  222. package/dist/core/runtime/stop-guard.d.ts +1 -1
  223. package/dist/core/runtime/stop-guard.js +6 -1
  224. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  225. package/dist/core/runtime/tmux-chrome.js +1 -1
  226. package/dist/core/runtime/tmux.d.ts +28 -0
  227. package/dist/core/runtime/tmux.js +80 -6
  228. package/dist/core/scope.d.ts +11 -0
  229. package/dist/core/scope.js +39 -0
  230. package/dist/core/spawn.d.ts +25 -1
  231. package/dist/core/spawn.js +72 -7
  232. package/dist/core/substrate/gate.d.ts +13 -0
  233. package/dist/core/substrate/gate.js +21 -0
  234. package/dist/core/substrate/index.d.ts +7 -0
  235. package/dist/core/substrate/index.js +18 -0
  236. package/dist/core/substrate/on-read.d.ts +14 -0
  237. package/dist/core/substrate/on-read.js +292 -0
  238. package/dist/core/substrate/render.d.ts +25 -0
  239. package/dist/core/substrate/render.js +256 -0
  240. package/dist/core/substrate/schema.d.ts +76 -0
  241. package/dist/core/substrate/schema.js +124 -0
  242. package/dist/core/substrate/session-cache.d.ts +30 -0
  243. package/dist/core/substrate/session-cache.js +77 -0
  244. package/dist/core/substrate/subject.d.ts +41 -0
  245. package/dist/core/substrate/subject.js +54 -0
  246. package/dist/core/tui/contract.d.ts +83 -0
  247. package/dist/core/tui/contract.js +8 -0
  248. package/dist/core/tui/draw.d.ts +96 -0
  249. package/dist/core/tui/draw.js +339 -0
  250. package/dist/core/tui/host.d.ts +29 -0
  251. package/dist/core/tui/host.js +379 -0
  252. package/dist/core/tui/loader.d.ts +16 -0
  253. package/dist/core/tui/loader.js +94 -0
  254. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  255. package/dist/core/wake.d.ts +86 -0
  256. package/dist/core/wake.js +308 -0
  257. package/dist/daemon/crtrd.d.ts +29 -4
  258. package/dist/daemon/crtrd.js +662 -46
  259. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  260. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  261. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  262. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  263. package/dist/pi-extensions/canvas-commands.js +10 -0
  264. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  265. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  266. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  267. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  268. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-nav.js +145 -34
  270. package/dist/pi-extensions/canvas-stophook.js +17 -8
  271. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  272. package/dist/pi-extensions/canvas-view.js +75 -0
  273. package/dist/prompts/skill.js +19 -26
  274. package/dist/prompts/view.d.ts +7 -0
  275. package/dist/prompts/view.js +101 -0
  276. package/dist/types.d.ts +4 -0
  277. package/dist/types.js +1 -0
  278. package/package.json +9 -4
  279. package/dist/commands/skill/find.d.ts +0 -4
  280. package/dist/commands/skill/find.js +0 -257
  281. package/dist/commands/skill/read.js +0 -91
  282. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  283. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -30,8 +30,9 @@
30
30
  // to by the agent being closed", generalized to any depth via a fixpoint.
31
31
  import { getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
32
32
  import { transition } from './lifecycle.js';
33
- import { tearDownNode } from './placement.js';
33
+ import { hostFor } from './host.js';
34
34
  import { appendInbox } from '../feed/inbox.js';
35
+ import { appendPassive } from '../feed/passive.js';
35
36
  /** The set of nodes to close: the root plus every descendant reachable down the
36
37
  * subscriptions spine, all of whose managers are themselves in the set. Grown
37
38
  * to a fixpoint — a node added this pass can qualify its own children next
@@ -128,11 +129,13 @@ export function closeNode(rootId) {
128
129
  .filter((c) => closing.has(c));
129
130
  // 1) Canceled + intent cleared BEFORE the window dies (daemon race).
130
131
  transition(id, 'cancel');
131
- // 2) Tear the node off its placement (pane-keyed): close any focus row it
132
- // occupies, kill its PANE (the window closes once its last pane goes, so
133
- // sibling nodes the user co-located in one window survive), and null its
134
- // LOCATION (closing the focus row is the record no pointer to clear).
135
- tearDownNode(id);
132
+ // 2) Tear the node's ENGINE down through the Host seam (hostFor): a tmux
133
+ // node runs tearDownNode (close any focus row it occupies, kill its
134
+ // PANE the window closes once its last pane goes, so sibling nodes the
135
+ // user co-located in one window surviveand null its LOCATION); a
136
+ // broker node sends the `shutdown` frame so the broker PROCESS exits and
137
+ // releases the sole .jsonl writer (a paneless node has no pane to kill).
138
+ hostFor(m).teardown(id);
136
139
  // 3) Leave the resume notice AFTER the watcher is gone, so it survives.
137
140
  appendInbox(id, {
138
141
  from: null,
@@ -141,6 +144,32 @@ export function closeNode(rootId) {
141
144
  label: cancellationLabel(id === rootId, deadChildren),
142
145
  data: { reason: 'user-close', cascade_root: rootId, canceled_children: deadChildren },
143
146
  });
147
+ // 4) Wake any SURVIVING manager subscribed to this node — the doctrine wake.
148
+ // A node going dormant trusts the runtime to wake it on a child's
149
+ // terminal outcome; D-1 found that `node close` notified ONLY the closed
150
+ // node itself, never its subscribers, so a parent that delegated then
151
+ // just stopped hangs forever when its child is closed out from under it.
152
+ // Only the close ROOT can have a surviving subscriber (closingSet adds a
153
+ // non-root node only when EVERY subscriber is itself closing), so this
154
+ // reaches the still-living manager(s) of a deliberately-closed child and
155
+ // never fires inside a self-contained cascade. Active → inbox (wakes a
156
+ // dormant manager via its live watcher / the daemon's dormant-revive
157
+ // second pass); passive → passive accumulator (delivered, not woken).
158
+ for (const sub of subscribersOf(id)) {
159
+ if (closing.has(sub.node_id))
160
+ continue; // also being torn down — pointless
161
+ const notice = {
162
+ from: id,
163
+ tier: 'normal',
164
+ kind: 'message',
165
+ label: `Child closed — ${m.name} (${id}) was closed from the canvas and is no longer running.`,
166
+ data: { reason: 'child-closed', child: id },
167
+ };
168
+ if (sub.active)
169
+ appendInbox(sub.node_id, notice);
170
+ else
171
+ appendPassive(sub.node_id, notice);
172
+ }
144
173
  closed.push(id);
145
174
  }
146
175
  catch {
@@ -0,0 +1,53 @@
1
+ import { type NodeMeta, type NodeRow } from '../canvas/index.js';
2
+ import type { PiInvocation } from './launch.js';
3
+ /** View-side launch hints the legacy host needs beyond the PiInvocation. This is
4
+ * today's ReviveLaunch (placement.ts) MINUS `command`/`env` — the host derives
5
+ * `command = piCommand(inv.argv)` and `env = inv.env` from the PiInvocation
6
+ * itself. The broker (Phase 3) uses only `cwd` + `resuming`. */
7
+ export interface LaunchPlacement {
8
+ cwd: string;
9
+ name: string;
10
+ resuming: boolean;
11
+ }
12
+ /** A handle to a node's running engine container.
13
+ * §3.2 specifies { kind, pid }; Phase 1 carries the placement coords too,
14
+ * because launch REPLACES reviveIntoPlacement (which returned PlacementResult)
15
+ * and reviveNode's unchanged ReviveResult is built from window+session. The
16
+ * daemon never reads window/session/pane (it supervises via pid / isAlive); the
17
+ * broker (Phase 3) returns window=null, pane=null. */
18
+ export interface HostHandle {
19
+ kind: 'tmux' | 'broker';
20
+ /** Supervised pid (signal-0 target). null at launch for TmuxPaneHost: the
21
+ * pane's fresh pi records its own pid via the stophook (session_start →
22
+ * recordPid), and reviveNode clearPid()s right after launch — so no Phase-1
23
+ * caller reads this. Present for the §3.2 contract + the daemon. */
24
+ pid: number | null;
25
+ window: string | null;
26
+ session: string;
27
+ pane: string | null;
28
+ }
29
+ export interface Host {
30
+ /** Bring a node's ENGINE into existence from its launch recipe; return a
31
+ * supervisable handle. TmuxPaneHost ALSO performs placement (the pane IS the
32
+ * engine container). */
33
+ launch(nodeId: string, inv: PiInvocation, opts: LaunchPlacement): HostHandle;
34
+ /** Is this node's engine container present? PURE / non-mutating. §3.2 types
35
+ * this `NodeRow`; we keep `string | NodeRow` to match isNodePaneAlive (which
36
+ * it IS) so both call sites — the guard (passes the id) and the Phase-2 daemon
37
+ * (passes a row) — stay one-token changes. */
38
+ isAlive(node: string | NodeRow): boolean;
39
+ /** Tear the engine down (close/cancel teardown). */
40
+ teardown(nodeId: string): void;
41
+ /** Deliver an OS signal to the engine container — present so the daemon never
42
+ * reaches around the abstraction. No Phase-1 call site. */
43
+ signal(nodeId: string, sig: NodeJS.Signals): void;
44
+ }
45
+ /** The sole Phase-1 host: today's behavior, wrapped. Stateless → a frozen
46
+ * singleton, no class needed. Every method delegates verbatim. */
47
+ export declare const tmuxPaneHost: Host;
48
+ export declare const headlessBrokerHost: Host;
49
+ /** Select the Host for a node by its persisted `host_kind` (NULL/'tmux' → the
50
+ * tmux host; 'broker' → the headless broker host). The param is `NodeMeta |
51
+ * NodeRow` so both call sites — reviveNode `hostFor(meta)` and the daemon
52
+ * `hostFor(row)` — share one selector. */
53
+ export declare function hostFor(node: NodeMeta | NodeRow): Host;
@@ -0,0 +1,186 @@
1
+ // src/core/runtime/host.ts
2
+ //
3
+ // The Host abstraction (design §3): owns an engine PROCESS's lifecycle, not its
4
+ // display. TWO impls live here: TmuxPaneHost — a thin wrapper over today's
5
+ // placement functions (the pane IS the engine container) — and, since Phase 3,
6
+ // HeadlessBrokerHost — a detached broker process with no tmux pane. `host_kind`
7
+ // (on the node row/meta since Wave 1) selects between them via hostFor().
8
+ //
9
+ // IMPORT-LINT (tmux-surface.test.ts §5.1): this file must NOT import './tmux.js'
10
+ // directly. It reaches every driver verb (piCommand, reviveIntoPlacement,
11
+ // isNodePaneAlive, tearDownNode) through placement.ts's re-exports — placement.ts
12
+ // and tmux-chrome.ts are the only sanctioned tmux.ts importers. The broker host
13
+ // needs no tmux verb at all.
14
+ import { spawn } from 'node:child_process';
15
+ import { mkdirSync, writeFileSync, existsSync, unlinkSync, openSync, closeSync } from 'node:fs';
16
+ import { dirname, join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+ import { connect } from 'node:net';
19
+ import { reviveIntoPlacement, isNodePaneAlive, tearDownNode, piCommand, } from './placement.js';
20
+ import { getNode } from '../canvas/index.js';
21
+ import { nodeDir, jobDir } from '../canvas/paths.js';
22
+ import { encodeFrame } from './broker-protocol.js';
23
+ import { FRONT_DOOR_ENV } from './front-door.js';
24
+ import { isPidAlive } from '../canvas/pid.js';
25
+ /** The sole Phase-1 host: today's behavior, wrapped. Stateless → a frozen
26
+ * singleton, no class needed. Every method delegates verbatim. */
27
+ export const tmuxPaneHost = {
28
+ launch(nodeId, inv, opts) {
29
+ const placed = reviveIntoPlacement(nodeId, {
30
+ command: piCommand(inv.argv),
31
+ env: inv.env,
32
+ cwd: opts.cwd,
33
+ name: opts.name,
34
+ resuming: opts.resuming,
35
+ });
36
+ return { kind: 'tmux', pid: null, window: placed.window, session: placed.session, pane: placed.pane };
37
+ },
38
+ isAlive(node) {
39
+ return isNodePaneAlive(node);
40
+ },
41
+ teardown(nodeId) {
42
+ tearDownNode(nodeId);
43
+ },
44
+ signal(nodeId, sig) {
45
+ const pid = getNode(nodeId)?.pi_pid;
46
+ if (pid != null) {
47
+ try {
48
+ process.kill(pid, sig);
49
+ }
50
+ catch {
51
+ /* already gone */
52
+ }
53
+ }
54
+ },
55
+ };
56
+ // ---------------------------------------------------------------------------
57
+ // HeadlessBrokerHost (design §3.3): a detached broker PROCESS, no tmux pane. It
58
+ // boots the dedicated broker-cli entry directly (NEVER src/cli.ts), supervises
59
+ // via the broker pid recorded as pi_pid, and tears down over the node's unix
60
+ // socket (graceful `shutdown` frame, SIGTERM fallback).
61
+ // ---------------------------------------------------------------------------
62
+ /** Resolve the absolute path to the broker entry. At runtime this file is
63
+ * dist/core/runtime/host.js; the entry lives at dist/core/runtime/broker-cli.js
64
+ * (sibling in the same directory) — the manage.ts/resolveCrtrdEntry pattern. */
65
+ function resolveBrokerEntry() {
66
+ const here = dirname(fileURLToPath(import.meta.url));
67
+ return join(here, 'broker-cli.js');
68
+ }
69
+ /** How long teardown waits after sending the graceful `shutdown` frame before
70
+ * SIGTERMing a broker that connected but never exited (e.g. a hung dispose()). */
71
+ const BROKER_SHUTDOWN_GRACE_MS = 2_000;
72
+ export const headlessBrokerHost = {
73
+ launch(nodeId, inv, opts) {
74
+ const dir = nodeDir(nodeId);
75
+ mkdirSync(dir, { recursive: true });
76
+ // The broker reads this recipe back via runBroker(nodeId) → broker-launch.json.
77
+ writeFileSync(join(dir, 'broker-launch.json'), JSON.stringify(inv));
78
+ // Redirect the detached broker's stdout+stderr to a per-node log under the
79
+ // node's existing job/ dir (alongside log.jsonl / telemetry.json). Without
80
+ // this (stdio:'ignore') EVERY broker diagnostic — the version-skew warning,
81
+ // model-not-found, socket errors, first-prompt failure, and broker-cli's
82
+ // fatal-crash stack — goes to /dev/null, which is exactly what makes a boot
83
+ // failure invisible (review M-2). Append-mode so a crash-revive keeps history.
84
+ const logDir = jobDir(nodeId);
85
+ mkdirSync(logDir, { recursive: true });
86
+ const logFd = openSync(join(logDir, 'broker.log'), 'a');
87
+ const child = spawn(process.execPath, [resolveBrokerEntry(), nodeId], {
88
+ cwd: opts.cwd,
89
+ detached: true,
90
+ stdio: ['ignore', logFd, logFd],
91
+ env: { ...process.env, ...inv.env, [FRONT_DOOR_ENV]: '1' },
92
+ });
93
+ // The child holds its own dup of the fd; release the parent's copy so the
94
+ // launching process (CLI or daemon) never leaks it.
95
+ closeSync(logFd);
96
+ child.unref();
97
+ return { kind: 'broker', pid: child.pid ?? null, window: null, session: '', pane: null };
98
+ },
99
+ isAlive(node) {
100
+ return isPidAlive((typeof node === 'string' ? getNode(node) : node)?.pi_pid);
101
+ },
102
+ teardown(nodeId) {
103
+ // Graceful: connect to view.sock + send a `shutdown` frame → the broker
104
+ // dispose()s the engine, unlinks the socket, exits 0. Status is already
105
+ // flipped done/canceled by the caller (crash-safe ordering), so the daemon
106
+ // won't revive. On connect failure (broker dead/crashed) fall back to a
107
+ // SIGTERM of the broker pid + unlink the stale socket.
108
+ const sockPath = join(nodeDir(nodeId), 'view.sock');
109
+ const cleanupSocket = () => {
110
+ try {
111
+ if (existsSync(sockPath))
112
+ unlinkSync(sockPath);
113
+ }
114
+ catch {
115
+ /* best-effort cleanup */
116
+ }
117
+ };
118
+ const sock = connect(sockPath);
119
+ let connected = false;
120
+ sock.on('error', () => {
121
+ // A post-connect error (broker exiting after we sent shutdown) is benign
122
+ // and swallowed; only a connect failure triggers the SIGTERM fallback.
123
+ if (connected)
124
+ return;
125
+ const pid = getNode(nodeId)?.pi_pid;
126
+ if (pid != null) {
127
+ try {
128
+ process.kill(pid, 'SIGTERM');
129
+ }
130
+ catch {
131
+ /* already gone */
132
+ }
133
+ }
134
+ cleanupSocket();
135
+ });
136
+ sock.once('connect', () => {
137
+ connected = true;
138
+ try {
139
+ sock.write(encodeFrame({ type: 'shutdown' }));
140
+ }
141
+ catch {
142
+ /* the broker may have raced us to exit */
143
+ }
144
+ sock.end();
145
+ cleanupSocket();
146
+ // Bounded exit confirmation (review Mn-3): a broker that connects fine but
147
+ // then HANGS inside session.dispose() (disposeAndExit catches a throw, not
148
+ // a hang) would leak the process holding the sole .jsonl writer. Arm a
149
+ // short UNREF'd timer; if the captured pid is still alive when it fires,
150
+ // SIGTERM it. unref'd so the happy path adds NO latency — a short-lived CLI
151
+ // caller's loop is kept alive only by the still-open socket, i.e. precisely
152
+ // the hung case where the fallback must run.
153
+ const pid = getNode(nodeId)?.pi_pid;
154
+ if (pid != null) {
155
+ setTimeout(() => {
156
+ if (isPidAlive(pid)) {
157
+ try {
158
+ process.kill(pid, 'SIGTERM');
159
+ }
160
+ catch {
161
+ /* already gone */
162
+ }
163
+ }
164
+ }, BROKER_SHUTDOWN_GRACE_MS).unref();
165
+ }
166
+ });
167
+ },
168
+ signal(nodeId, sig) {
169
+ const pid = getNode(nodeId)?.pi_pid;
170
+ if (pid != null) {
171
+ try {
172
+ process.kill(pid, sig);
173
+ }
174
+ catch {
175
+ /* already gone */
176
+ }
177
+ }
178
+ },
179
+ };
180
+ /** Select the Host for a node by its persisted `host_kind` (NULL/'tmux' → the
181
+ * tmux host; 'broker' → the headless broker host). The param is `NodeMeta |
182
+ * NodeRow` so both call sites — reviveNode `hostFor(meta)` and the daemon
183
+ * `hostFor(row)` — share one selector. */
184
+ export function hostFor(node) {
185
+ return node.host_kind === 'broker' ? headlessBrokerHost : tmuxPaneHost;
186
+ }
@@ -1,4 +1,5 @@
1
1
  import { type NodeMeta } from '../canvas/index.js';
2
+ import { type WakeOrigin } from './bearings.js';
2
3
  /** The goal file — the prompt/task a node was spawned with, persisted at birth
3
4
  * so a fresh revive can re-read its mandate. */
4
5
  export declare function goalPath(nodeId: string): string;
@@ -46,4 +47,4 @@ export declare function drainBearings(meta: NodeMeta): ReviveBearings;
46
47
  * node's goal, roadmap, and context dir, framed so the revived node can rebuild
47
48
  * its bearings in one turn. PURE: no state mutation, so calling it twice yields
48
49
  * the same string and consumes nothing — drainBearings owns the one-shot reads. */
49
- export declare function buildReviveKickoff(meta: NodeMeta, bearings: ReviveBearings): string;
50
+ export declare function buildReviveKickoff(meta: NodeMeta, bearings: ReviveBearings, wakeReason?: WakeOrigin): string;
@@ -19,8 +19,10 @@
19
19
  // yield-message is one-shot (consumed on the next revive).
20
20
  import { existsSync, readFileSync, writeFileSync, rmSync, mkdirSync, readdirSync, } from 'node:fs';
21
21
  import { join } from 'node:path';
22
- import { contextDir, getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
22
+ import { homedir } from 'node:os';
23
+ import { contextDir, reportsDir, getNode, subscriptionsOf, subscribersOf, } from '../canvas/index.js';
23
24
  import { readRoadmap, roadmapPath } from './roadmap.js';
25
+ import { buildWakeBearings } from './bearings.js';
24
26
  import { personaDrift, commitPersonaAck } from './persona.js';
25
27
  import { readInboxSince, readCursor, writeCursor, coalesce, } from '../feed/inbox.js';
26
28
  // ---------------------------------------------------------------------------
@@ -126,8 +128,60 @@ export function drainBearings(meta) {
126
128
  }
127
129
  return { yieldMsg, unreadDigest, driftGuidance };
128
130
  }
129
- /** Render the <feed> block PURELY: the live "awaiting" roster (a read) plus the
130
- * already-drained unread digest (from drainBearings). No cursor write here. */
131
+ // Fresh-revive catch-up bug: on a refresh-yield (resume:false) the old
132
+ // conversation is gone AND the monotonic inbox cursor has already advanced past
133
+ // everything drained pre-yield, so the revived node loses sight of reports its
134
+ // subscriptions pushed BEFORE the yield. The bodies are never lost — they persist
135
+ // at reports/<ts>-<kind>.md forever — but nothing pointed the revived node at
136
+ // them. reportHistoryLines renders those existing on-disk paths so it can catch
137
+ // up; PATHS ONLY (the node dereferences what it needs), no body read, no parse.
138
+ const REPORT_HISTORY_PER_SOURCE = 5;
139
+ const REPORT_HISTORY_TOTAL_CAP = 20;
140
+ /** Collapse the home-dir prefix of an absolute path to a leading `~`, so report
141
+ * paths render as `~/.crouter/canvas/nodes/<id>/reports/<ts>.md` instead of
142
+ * repeating the long absolute home prefix on every catch-up line — cheaper
143
+ * context, still dereferenceable by the revived node. A path NOT under the home
144
+ * dir (e.g. a CRTR_HOME / project-scope home elsewhere) is returned unchanged,
145
+ * so we never mangle a non-home path. */
146
+ function tildify(p) {
147
+ const home = homedir();
148
+ return p.startsWith(home) ? '~' + p.slice(home.length) : p;
149
+ }
150
+ /** Recent report file PATHS (most-recent-first), grouped by publisher, for each
151
+ * ACTIVE subscription of `nodeId` — independent of publisher liveness, so a
152
+ * finished worker's history still surfaces for catch-up. Skips subscriptions
153
+ * whose reports dir is empty/missing. Returns [] when the node has no active
154
+ * subscriptions or none have reports yet (caller renders nothing extra). */
155
+ function reportHistoryLines(nodeId) {
156
+ const lines = [];
157
+ let total = 0;
158
+ for (const sub of subscriptionsOf(nodeId).filter((s) => s.active)) {
159
+ if (total >= REPORT_HISTORY_TOTAL_CAP)
160
+ break;
161
+ const dir = reportsDir(sub.node_id);
162
+ if (!existsSync(dir))
163
+ continue;
164
+ const files = readdirSync(dir)
165
+ .filter((f) => f.endsWith('.md'))
166
+ .sort() // YYYYMMDDTHHmmss-<kind>.md — lexical sort = chronological
167
+ .reverse() // most recent first
168
+ .slice(0, REPORT_HISTORY_PER_SOURCE);
169
+ if (files.length === 0)
170
+ continue;
171
+ const pub = getNode(sub.node_id);
172
+ lines.push(` ${pub !== null ? `${pub.name} (${sub.node_id})` : sub.node_id}:`);
173
+ for (const f of files) {
174
+ if (total >= REPORT_HISTORY_TOTAL_CAP)
175
+ break;
176
+ lines.push(` ${tildify(join(dir, f))}`);
177
+ total++;
178
+ }
179
+ }
180
+ return lines;
181
+ }
182
+ /** Render the <feed> block PURELY: the live "awaiting" roster (a read), the
183
+ * already-drained unread digest (from drainBearings), and the on-disk report
184
+ * history of this node's subscriptions (catch-up pointers). No cursor write. */
131
185
  function feedBlock(nodeId, unreadDigest) {
132
186
  // Awaiting = active subscriptions whose publisher is still live (active|idle).
133
187
  const awaiting = subscriptionsOf(nodeId)
@@ -154,6 +208,17 @@ function feedBlock(nodeId, unreadDigest) {
154
208
  lines.push('Awaiting 0 nodes.');
155
209
  lines.push('', unreadDigest ?? '(no unread reports)');
156
210
  }
211
+ // Catch-up history. The unread digest above only covers what arrived since the
212
+ // cursor; on a refresh-revive the cursor has already passed everything drained
213
+ // pre-yield, so point the node at the durable report history that persists on
214
+ // disk. Renders nothing when the node has no active subscriptions with reports.
215
+ const history = reportHistoryLines(nodeId);
216
+ if (history.length > 0) {
217
+ lines.push('', 'Report history on disk — the nodes you subscribe to keep every push they made; ' +
218
+ 'these persist across your context refresh (the unread digest above only covers ' +
219
+ 'what arrived since your cursor). Most recent first; dereference any you need. ' +
220
+ '`crtr feed read --all` replays the full inbox history, cursor-independent.', ...history);
221
+ }
157
222
  return `<feed>\n${lines.join('\n')}\n</feed>`;
158
223
  }
159
224
  // ---------------------------------------------------------------------------
@@ -164,19 +229,40 @@ function feedBlock(nodeId, unreadDigest) {
164
229
  * node's goal, roadmap, and context dir, framed so the revived node can rebuild
165
230
  * its bearings in one turn. PURE: no state mutation, so calling it twice yields
166
231
  * the same string and consumes nothing — drainBearings owns the one-shot reads. */
167
- export function buildReviveKickoff(meta, bearings) {
232
+ export function buildReviveKickoff(meta, bearings, wakeReason) {
168
233
  const nodeId = meta.node_id;
169
234
  const parts = [
170
235
  `${REVIVE_KICKOFF_SENTINEL} — your previous in-memory ` +
171
236
  'context is gone, by design. Everything below was just read from disk; it is your ' +
172
237
  'full bearings. Rebuild from it and continue toward your goal.',
173
238
  ];
239
+ // Wake provenance (Invariant B/D): when a scheduled bare self-alarm fired this
240
+ // revive, the <crtr-wake> block reframes the generic "you were revived" above
241
+ // into "a TIMER woke you" — placed right after the sentinel (so the kickoff
242
+ // still STARTS with REVIVE_KICKOFF_SENTINEL, which goal-capture keys on) and
243
+ // before the roadmap/disk bearings, so "why you woke" precedes "what to rebuild
244
+ // from". Only the daemon's bare-wake branch passes wakeReason.
245
+ if (wakeReason !== undefined)
246
+ parts.push(buildWakeBearings(wakeReason));
174
247
  // The roadmap is the source of truth on a fresh revive: its frozen core holds
175
248
  // the goal/exit criteria, its body the live plan the node kept current. The
176
249
  // original spawn prompt (context/initial-prompt.md) is deliberately NOT injected
177
250
  // — it lives on disk only as a log, and by now it is usually stale.
178
251
  const roadmap = readRoadmap(nodeId);
179
- parts.push(`<roadmap file="${roadmapPath(nodeId)}">\n${roadmap !== null && roadmap.trim() !== '' ? roadmap.trim() : '(no roadmap on disk yet)'}\n</roadmap>`);
252
+ const hasRoadmapBody = roadmap !== null && roadmap.trim() !== '';
253
+ parts.push(`<roadmap file="${roadmapPath(nodeId)}">\n${hasRoadmapBody ? roadmap.trim() : '(no roadmap on disk yet)'}\n</roadmap>`);
254
+ // With NO roadmap the kickoff above carries no mandate at all, and an
255
+ // amnesiac fresh boot just stops (and is auto-finalized by the stop-guard) —
256
+ // observed when the daemon relaunches a spawned-but-never-launched node
257
+ // (audit 2026-06-09, Bug 4: mq45b6ch-9ecc2f03), whose only mandate on disk IS
258
+ // the goal. Surface the goal then. When a roadmap exists it stays the sole
259
+ // source of truth (the goal is usually stale by comparison) — unchanged.
260
+ if (!hasRoadmapBody) {
261
+ const goal = readGoal(nodeId);
262
+ if (goal !== null && goal.trim() !== '') {
263
+ parts.push(`<goal file="${goalPath(nodeId)}">\n${goal.trim()}\n</goal>`);
264
+ }
265
+ }
180
266
  const files = listContextDir(nodeId);
181
267
  parts.push(`<context-dir path="${contextDir(nodeId)}">\n${files.length > 0 ? files.join('\n') : '(empty)'}\n</context-dir>`);
182
268
  parts.push(feedBlock(nodeId, bearings.unreadDigest));
@@ -5,15 +5,20 @@ export declare const CANVAS_NAV_PATH: string;
5
5
  export declare const CANVAS_GOAL_CAPTURE_PATH: string;
6
6
  export declare const CANVAS_PASSIVE_CONTEXT_PATH: string;
7
7
  export declare const CANVAS_CONTEXT_INTRO_PATH: string;
8
+ export declare const CANVAS_DOC_SUBSTRATE_PATH: string;
8
9
  export declare const CANVAS_COMMANDS_PATH: string;
9
10
  export declare const CANVAS_RESUME_PATH: string;
11
+ export declare const CANVAS_VIEW_PATH: string;
10
12
  /** The canvas extensions every node loads, in order: stophook (routing +
11
13
  * telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
12
14
  * graph chrome), goal-capture (persist the first user message as the goal),
13
15
  * passive-context (drain passive backlog as pre-text on the next message),
14
16
  * context-intro (inject the <crtr-context> bearings block as its own session
15
- * message, once per brand-new chat), commands (the /promote slash-command),
16
- * resume (the /resume-node whole-canvas picker `crtr node focus`).
17
+ * message, once per brand-new chat), doc-substrate (the unified document
18
+ * substrate's two hooks: ## Skills + ## Preferences at boot, on-read context
19
+ * injection), commands (the /promote slash-command), resume (the /resume-node
20
+ * whole-canvas picker → `crtr node focus`), view (the /view popup → `crtr
21
+ * view pick` / `crtr view run <name>`).
17
22
  * All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
18
23
  * reads the raw user text. */
19
24
  export declare const CANVAS_EXTENSIONS: string[];
@@ -57,3 +62,41 @@ export declare function buildPiArgv(meta: NodeMeta, opts?: {
57
62
  resumeSessionPath?: string;
58
63
  forkFrom?: string;
59
64
  }): PiInvocation;
65
+ /** The pi-SDK launch config the headless broker drives an in-process engine
66
+ * with — the structural inverse of `buildPiArgv`'s flag vocabulary. Each field
67
+ * maps one of buildPiArgv's emitted flags back to its SDK option:
68
+ * `-e`→`extensionPaths`, `-n`→`editorName`, `--fork`→`forkFrom`,
69
+ * `--session`→`resumeSessionPath|resumeSessionId`, `--model`→`model`,
70
+ * `--tools`→`tools`, `--append-system-prompt`→`appendSystemPromptPath`, the
71
+ * trailing positional→`firstPrompt`. */
72
+ export interface BrokerSdkConfig {
73
+ /** The node's pinned working dir (from `CRTR_NODE_CWD`, else `process.cwd()`). */
74
+ cwd: string;
75
+ /** Absolute `.js` extension paths (the canvas extensions). */
76
+ extensionPaths: string[];
77
+ /** Session label (`-n`). */
78
+ editorName?: string;
79
+ /** Spawn-time fork source (`--fork <path|id>`). */
80
+ forkFrom?: string;
81
+ /** Resume by absolute `.jsonl` path (preferred — `SessionManager.open`). */
82
+ resumeSessionPath?: string;
83
+ /** Resume by bare session uuid (legacy fallback). */
84
+ resumeSessionId?: string;
85
+ /** Model spec, e.g. `anthropic/sonnet` (`--model`). */
86
+ model?: string;
87
+ /** Tool allowlist (`--tools a,b,c` → `['a','b','c']`). */
88
+ tools?: string[];
89
+ /** `--append-system-prompt` arg — a file path in practice (pi's loader
90
+ * resolves a path or literal text identically). */
91
+ appendSystemPromptPath?: string;
92
+ /** The fresh-start kickoff message (the trailing positional). */
93
+ firstPrompt?: string;
94
+ }
95
+ /** Translate a `PiInvocation` (the recipe `buildPiArgv` produced) into the SDK
96
+ * config the broker hosts an engine with. This is the EXACT inverse of
97
+ * `buildPiArgv` and must track its flag set 1:1 — co-located here so the two
98
+ * never drift. Safe because `buildPiArgv` is the SOLE producer of these flags
99
+ * (we own both ends). SIDE EFFECT: merges `inv.env` into `process.env` so the
100
+ * in-process engine + the bound canvas extensions see the same env a forked pi
101
+ * would (CRTR_NODE_ID, CRTR_ROOT_SESSION, CRTR_SUBTREE, …). */
102
+ export declare function piInvocationToSdkConfig(inv: PiInvocation): BrokerSdkConfig;
@@ -32,15 +32,20 @@ export const CANVAS_NAV_PATH = resolveExtension('canvas-nav');
32
32
  export const CANVAS_GOAL_CAPTURE_PATH = resolveExtension('canvas-goal-capture');
33
33
  export const CANVAS_PASSIVE_CONTEXT_PATH = resolveExtension('canvas-passive-context');
34
34
  export const CANVAS_CONTEXT_INTRO_PATH = resolveExtension('canvas-context-intro');
35
+ export const CANVAS_DOC_SUBSTRATE_PATH = resolveExtension('canvas-doc-substrate');
35
36
  export const CANVAS_COMMANDS_PATH = resolveExtension('canvas-commands');
36
37
  export const CANVAS_RESUME_PATH = resolveExtension('canvas-resume');
38
+ export const CANVAS_VIEW_PATH = resolveExtension('canvas-view');
37
39
  /** The canvas extensions every node loads, in order: stophook (routing +
38
40
  * telemetry + session-id capture), inbox-watcher (wake), nav (in-editor
39
41
  * graph chrome), goal-capture (persist the first user message as the goal),
40
42
  * passive-context (drain passive backlog as pre-text on the next message),
41
43
  * context-intro (inject the <crtr-context> bearings block as its own session
42
- * message, once per brand-new chat), commands (the /promote slash-command),
43
- * resume (the /resume-node whole-canvas picker `crtr node focus`).
44
+ * message, once per brand-new chat), doc-substrate (the unified document
45
+ * substrate's two hooks: ## Skills + ## Preferences at boot, on-read context
46
+ * injection), commands (the /promote slash-command), resume (the /resume-node
47
+ * whole-canvas picker → `crtr node focus`), view (the /view popup → `crtr
48
+ * view pick` / `crtr view run <name>`).
44
49
  * All self-gate on CRTR_NODE_ID. goal-capture precedes passive-context so it
45
50
  * reads the raw user text. */
46
51
  export const CANVAS_EXTENSIONS = [
@@ -50,8 +55,10 @@ export const CANVAS_EXTENSIONS = [
50
55
  CANVAS_GOAL_CAPTURE_PATH,
51
56
  CANVAS_PASSIVE_CONTEXT_PATH,
52
57
  CANVAS_CONTEXT_INTRO_PATH,
58
+ CANVAS_DOC_SUBSTRATE_PATH,
53
59
  CANVAS_COMMANDS_PATH,
54
60
  CANVAS_RESUME_PATH,
61
+ CANVAS_VIEW_PATH,
55
62
  ];
56
63
  /** Bare model aliases resolve to the anthropic provider under pi (avoids the
57
64
  * bedrock default). Anything with a `/` or an unknown name passes through. */
@@ -158,3 +165,59 @@ export function buildPiArgv(meta, opts = {}) {
158
165
  argv.push(opts.prompt);
159
166
  return { argv, env: nodeEnv(meta) };
160
167
  }
168
+ /** Translate a `PiInvocation` (the recipe `buildPiArgv` produced) into the SDK
169
+ * config the broker hosts an engine with. This is the EXACT inverse of
170
+ * `buildPiArgv` and must track its flag set 1:1 — co-located here so the two
171
+ * never drift. Safe because `buildPiArgv` is the SOLE producer of these flags
172
+ * (we own both ends). SIDE EFFECT: merges `inv.env` into `process.env` so the
173
+ * in-process engine + the bound canvas extensions see the same env a forked pi
174
+ * would (CRTR_NODE_ID, CRTR_ROOT_SESSION, CRTR_SUBTREE, …). */
175
+ export function piInvocationToSdkConfig(inv) {
176
+ for (const [k, v] of Object.entries(inv.env))
177
+ process.env[k] = v;
178
+ const cfg = {
179
+ cwd: inv.env['CRTR_NODE_CWD'] ?? process.cwd(),
180
+ extensionPaths: [],
181
+ };
182
+ const argv = inv.argv;
183
+ for (let i = 0; i < argv.length; i++) {
184
+ const tok = argv[i];
185
+ switch (tok) {
186
+ case '-e':
187
+ cfg.extensionPaths.push(argv[++i]);
188
+ break;
189
+ case '-n':
190
+ cfg.editorName = argv[++i];
191
+ break;
192
+ case '--fork':
193
+ cfg.forkFrom = argv[++i];
194
+ break;
195
+ case '--session': {
196
+ const v = argv[++i];
197
+ // A path (contains `/` or ends `.jsonl`) resumes via SessionManager.open;
198
+ // a bare uuid is the legacy fallback. Same classification buildPiArgv
199
+ // documents (path preferred, immune to cwd discrepancy).
200
+ if (v.includes('/') || v.endsWith('.jsonl'))
201
+ cfg.resumeSessionPath = v;
202
+ else
203
+ cfg.resumeSessionId = v;
204
+ break;
205
+ }
206
+ case '--model':
207
+ cfg.model = argv[++i];
208
+ break;
209
+ case '--tools':
210
+ cfg.tools = argv[++i].split(',').filter((t) => t !== '');
211
+ break;
212
+ case '--append-system-prompt':
213
+ cfg.appendSystemPromptPath = argv[++i];
214
+ break;
215
+ default:
216
+ // buildPiArgv consumes every flag's value via the cases above, so any
217
+ // token reaching here is the single trailing positional (the prompt).
218
+ cfg.firstPrompt = tok;
219
+ break;
220
+ }
221
+ }
222
+ return cfg;
223
+ }