@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
@@ -191,6 +191,39 @@ export function paneExists(pane) {
191
191
  const r = tmux(['display-message', '-p', '-t', pane, '#{pane_id}']);
192
192
  return r.ok && r.stdout === pane;
193
193
  }
194
+ /** Every live pane id on the server (`list-panes -a`), as a Set for membership
195
+ * probes. Returns null when tmux is unreachable (no server / transient failure)
196
+ * so callers can tell "no panes" apart from "can't tell" — a GC pass must skip,
197
+ * never mass-delete, on a failed probe. One subprocess call total, so batch
198
+ * liveness sweeps (e.g. the daemon's stale-focus GC) don't pay a per-pane
199
+ * display-message each. */
200
+ export function listLivePanes() {
201
+ const r = tmux(['list-panes', '-a', '-F', '#{pane_id}']);
202
+ if (!r.ok)
203
+ return null;
204
+ return new Set(r.stdout.split('\n').filter((p) => p !== ''));
205
+ }
206
+ /** The working directory of a pane (`display-message -p -t <pane>
207
+ * '#{pane_current_path}'`). Used to preserve a view monitor's cwd across a
208
+ * view-cycle respawn so project-scoped views still resolve. Null if tmux fails. */
209
+ export function paneCurrentPath(pane) {
210
+ const r = tmux(['display-message', '-p', '-t', pane, '#{pane_current_path}']);
211
+ return r.ok && r.stdout !== '' ? r.stdout : null;
212
+ }
213
+ /** Set a PANE-scoped tmux option (`tmux set-option -p -t <pane> <name> <value>`).
214
+ * Used to tag a pane with the view id it currently hosts (`@crtr_view`) so the
215
+ * view-nav cycle can read it back and switch to the next/prev view in place.
216
+ * Best-effort; never throws. */
217
+ export function setPaneOption(pane, name, value) {
218
+ return tmux(['set-option', '-p', '-t', pane, name, value]).ok;
219
+ }
220
+ /** Read a PANE-scoped tmux option value (`tmux show-options -p -t <pane> -q -v
221
+ * <name>`): `-v` prints only the value, `-q` suppresses the unknown-option
222
+ * error so an unset option yields an empty string. undefined if tmux fails. */
223
+ export function getPaneOption(pane, name) {
224
+ const r = tmux(['show-options', '-p', '-t', pane, '-q', '-v', name]);
225
+ return r.ok ? r.stdout : undefined;
226
+ }
194
227
  /** Relocate a pane into another session as its own window WITHOUT killing the
195
228
  * process in it — `break-pane -d` moves the pane out of its current window (the
196
229
  * pi keeps generating) into a fresh window in `session`; `-d` leaves the caller's
@@ -375,12 +408,13 @@ export function installMenuBinding() {
375
408
  // a whole-canvas picker (incl. dormant nodes) and revives the choice via
376
409
  // `crtr node focus` — the only sync-safe open (routes through reviveNode).
377
410
  { name: 'resume node', key: 'r', cmd: `send-keys -t '#{pane_id}' '/resume-node' Enter` },
378
- // `d` demotes the agent to TERMINAL in place: no finalize, no kill — it keeps
379
- // running where it is, and because it is now terminal it is forced to push a
380
- // final up the spine when it finishes. `D` ALSO detaches it to the background
381
- // `crtr` session (frees the pane; the pi keeps generating). Neither ends it.
382
- { name: 'demote to terminal', key: 'd', cmd: `run-shell "crtr node lifecycle terminal --pane '#{pane_id}' >/dev/null 2>&1"` },
383
- { name: 'detach to background', key: 'D', cmd: `run-shell "crtr node lifecycle terminal --pane '#{pane_id}' --detach >/dev/null 2>&1"` },
411
+ // `d` runs `node demote`: flip the agent to TERMINAL in place no finalize,
412
+ // no kill — it keeps running where it is, and because it is now terminal it
413
+ // is forced to push a final up the spine when it finishes. `D` runs `node
414
+ // demote --detach`, which ALSO detaches it to the background `crtr` session
415
+ // (frees the pane; the pi keeps generating). Neither ends it.
416
+ { name: 'demote to terminal', key: 'd', cmd: `run-shell "crtr node demote --pane '#{pane_id}' >/dev/null 2>&1"` },
417
+ { name: 'detach to background', key: 'D', cmd: `run-shell "crtr node demote --pane '#{pane_id}' --detach >/dev/null 2>&1"` },
384
418
  // Close cascades down the subscribes_to spine (kills the subtree's windows,
385
419
  // marks them canceled); revivable. Output discarded — the keypress just acts.
386
420
  { name: 'close agent + subtree', key: 'x', cmd: `run-shell "crtr node close --pane '#{pane_id}' >/dev/null 2>&1"` },
@@ -455,3 +489,43 @@ export function installNavBindings() {
455
489
  ]).ok;
456
490
  return next && prev;
457
491
  }
492
+ // ---------------------------------------------------------------------------
493
+ // View-nav bindings — Alt+V then ] / [ cycle the view hosted in a MONITOR pane
494
+ // to the next/prev available view, in place. A VIEW-PREFIXED CHORD, not a bare
495
+ // Alt pair: Alt+V switches into a private one-shot `crtr-view` key table, then
496
+ // ] (next) / [ (prev) fire the cycle — mirroring node cycle's bracket DIRECTION
497
+ // grammar (Alt+] next / Alt+[ prev) while NAMESPACING the brackets so they can
498
+ // never shadow root bindings. This is collision-proof: only ONE root key (M-v)
499
+ // is claimed, and bare Alt pairs are a minefield in real configs (e.g. Alt+,/.
500
+ // and Alt+]/.[ commonly bound to window/pane nav). The bracket lives in the
501
+ // private table, so it also sidesteps the M-[ vs CSI-introducer ambiguity that
502
+ // dogs installNavBindings' Alt+[. Each key shells `crtr view cycle`, passing the
503
+ // active pane; cycle reads the pane's @crtr_view tag and respawns it on the
504
+ // next/prev view. Output discarded so the keypress never pops a results view.
505
+ // Installed at root boot alongside the node nav + Alt+C menu.
506
+ //
507
+ // NOTE: `M-v` is a GLOBAL root binding — it intercepts Alt+V in every pane/app
508
+ // (e.g. swallowed from the pi editor), the same tradeoff crtr already takes for
509
+ // M-c / M-] / M-[. Intended: crtr owns a small set of Alt chords server-wide.
510
+ // ---------------------------------------------------------------------------
511
+ /** Bind Alt+V → (], [) to the view-monitor cycle. `M-v` enters the private
512
+ * `crtr-view` key table (switch-client -T), then ] cycles next / [ cycles prev.
513
+ * Best-effort; false if any of the three binds fail. Deliberately distinct from
514
+ * installNavBindings' bare Alt+]/Alt+[ (node cycle): the two cycles coexist on
515
+ * the same server — brackets alone walk the node graph, Alt+V-then-bracket flips
516
+ * view monitors. The bracket keys are bound in the private table, so they NEVER
517
+ * shadow the user's root ]/[ and carry no CSI-introducer ambiguity. */
518
+ export function installViewNavBindings() {
519
+ const enter = tmux([
520
+ 'bind-key', '-n', 'M-v', 'switch-client', '-T', 'crtr-view',
521
+ ]).ok;
522
+ const next = tmux([
523
+ 'bind-key', '-T', 'crtr-view', ']', 'run-shell',
524
+ `crtr view cycle --dir next --pane '#{pane_id}' >/dev/null 2>&1`,
525
+ ]).ok;
526
+ const prev = tmux([
527
+ 'bind-key', '-T', 'crtr-view', '[', 'run-shell',
528
+ `crtr view cycle --dir prev --pane '#{pane_id}' >/dev/null 2>&1`,
529
+ ]).ok;
530
+ return enter && next && prev;
531
+ }
@@ -1,5 +1,7 @@
1
1
  import type { Scope } from '../types.js';
2
2
  export declare function builtinSkillsRoot(): string;
3
+ export declare function builtinViewsRoot(): string;
4
+ export declare function builtinMemoryRoot(): string;
3
5
  export declare function userScopeRoot(): string;
4
6
  export declare function findProjectScopeRoot(startDir?: string): string | null;
5
7
  export declare function projectScopeRoot(startDir?: string): string | null;
@@ -9,6 +11,15 @@ export declare function ensureProjectScopeRoot(startDir?: string): string;
9
11
  export declare function pluginsDir(scope: Scope): string | null;
10
12
  export declare function marketplacesDir(scope: Scope): string | null;
11
13
  export declare function scopeSkillsDir(scope: Scope): string | null;
14
+ /** Where substrate memory documents live per scope. Builtin memory is the
15
+ * special case — its own package dir (builtinMemoryRoot, a sibling of
16
+ * builtin-skills), NOT scopeRoot('builtin')/memory — mirroring viewsDir's
17
+ * builtin handling. User and project memory live at scopeRoot(scope)/memory. */
18
+ export declare function scopeMemoryDir(scope: Scope): string | null;
19
+ /** Where view definition dirs live per scope. Builtin views sit directly under
20
+ * builtinViewsRoot() (no `views/` segment — they ARE the builtin views dir),
21
+ * matching the loader's `<root>/<name>/view.mjs` resolution. */
22
+ export declare function viewsDir(scope: Scope): string | null;
12
23
  export declare function resolveScopeArg(scopeArg: string | undefined): Scope | 'all';
13
24
  export declare function listScopes(scopeArg: string | undefined): Scope[];
14
25
  export declare function resetScopeCache(): void;
@@ -12,6 +12,26 @@ export function builtinSkillsRoot() {
12
12
  const pkgDir = dirname(coreDir); // src/ or dist/
13
13
  return join(pkgDir, 'builtin-skills');
14
14
  }
15
+ export function builtinViewsRoot() {
16
+ // Resolved relative to this file exactly like builtinSkillsRoot — sibling of
17
+ // builtin-skills: dist/core/scope.js → dist/builtin-views/ (src/ at dev time).
18
+ const thisFile = fileURLToPath(import.meta.url);
19
+ const coreDir = dirname(thisFile);
20
+ const pkgDir = dirname(coreDir); // src/ or dist/
21
+ return join(pkgDir, 'builtin-views');
22
+ }
23
+ export function builtinMemoryRoot() {
24
+ // The substrate's shipped built-in documents live in their OWN package dir,
25
+ // a sibling of builtin-skills — resolved relative to this file exactly like
26
+ // builtinSkillsRoot/builtinViewsRoot: dist/core/scope.js → dist/builtin-memory/
27
+ // (src/ at dev time). This is a HARD special case (design verdict m2): builtin
28
+ // memory is NOT scopeRoot('builtin')/memory, which would land under
29
+ // builtin-skills/ — builtin's scopeRoot is builtinSkillsRoot().
30
+ const thisFile = fileURLToPath(import.meta.url);
31
+ const coreDir = dirname(thisFile);
32
+ const pkgDir = dirname(coreDir); // src/ or dist/
33
+ return join(pkgDir, 'builtin-memory');
34
+ }
15
35
  export function userScopeRoot() {
16
36
  return join(homedir(), CRTR_DIR_NAME);
17
37
  }
@@ -77,6 +97,25 @@ export function scopeSkillsDir(scope) {
77
97
  const root = scopeRoot(scope);
78
98
  return root ? join(root, 'skills') : null;
79
99
  }
100
+ /** Where substrate memory documents live per scope. Builtin memory is the
101
+ * special case — its own package dir (builtinMemoryRoot, a sibling of
102
+ * builtin-skills), NOT scopeRoot('builtin')/memory — mirroring viewsDir's
103
+ * builtin handling. User and project memory live at scopeRoot(scope)/memory. */
104
+ export function scopeMemoryDir(scope) {
105
+ if (scope === 'builtin')
106
+ return builtinMemoryRoot();
107
+ const root = scope === 'user' ? userScopeRoot() : projectScopeRoot();
108
+ return root ? join(root, 'memory') : null;
109
+ }
110
+ /** Where view definition dirs live per scope. Builtin views sit directly under
111
+ * builtinViewsRoot() (no `views/` segment — they ARE the builtin views dir),
112
+ * matching the loader's `<root>/<name>/view.mjs` resolution. */
113
+ export function viewsDir(scope) {
114
+ if (scope === 'builtin')
115
+ return builtinViewsRoot();
116
+ const root = scope === 'user' ? userScopeRoot() : projectScopeRoot();
117
+ return root ? join(root, 'views') : null;
118
+ }
80
119
  export function resolveScopeArg(scopeArg) {
81
120
  if (scopeArg === undefined)
82
121
  return 'all';
@@ -11,6 +11,11 @@ export declare function countPanesInCurrentWindow(): number;
11
11
  * output on an unresolvable pane, so test for non-empty stdout, not just `.ok`.
12
12
  * False outside tmux / on error. */
13
13
  export declare function paneAlive(pane: string): boolean;
14
+ /** Resolve a tmux pane id to its `session:window_index` — the target form
15
+ * `new-window -t` accepts. tmux REJECTS a pane id for new-window ("can't
16
+ * specify pane here"); only split-window -t takes a pane. null outside tmux /
17
+ * on a bad pane id / on error / empty. */
18
+ export declare function paneWindowTarget(pane: string): string | null;
14
19
  /** The active pane of the user's attached tmux client — where they are looking
15
20
  * right now. `list-clients` first attached client, then its current pane. Used
16
21
  * to surface a human prompt in the user's view when nothing in the asking
@@ -41,6 +41,19 @@ export function paneAlive(pane) {
41
41
  });
42
42
  return r.status === 0 && r.stdout.trim() !== '';
43
43
  }
44
+ /** Resolve a tmux pane id to its `session:window_index` — the target form
45
+ * `new-window -t` accepts. tmux REJECTS a pane id for new-window ("can't
46
+ * specify pane here"); only split-window -t takes a pane. null outside tmux /
47
+ * on a bad pane id / on error / empty. */
48
+ export function paneWindowTarget(pane) {
49
+ if (!isInTmux() || !/^%\d+$/.test(pane))
50
+ return null;
51
+ const r = spawnSync('tmux', ['display-message', '-p', '-t', pane, '#{session_name}:#{window_index}'], { encoding: 'utf8' });
52
+ if (r.status !== 0)
53
+ return null;
54
+ const t = r.stdout.trim();
55
+ return t !== '' ? t : null;
56
+ }
44
57
  /** The active pane of the user's attached tmux client — where they are looking
45
58
  * right now. `list-clients` first attached client, then its current pane. Used
46
59
  * to surface a human prompt in the user's view when nothing in the asking
@@ -101,8 +114,13 @@ export function spawnAndDetach(opts) {
101
114
  if (opts.detached === true)
102
115
  splitArgs.push('-d'); // don't switch the client to it
103
116
  if (opts.targetPane !== undefined && opts.targetPane !== '') {
104
- // -a = insert after target window; -t <pane> resolves to that pane's window.
105
- splitArgs.push('-a', '-t', opts.targetPane);
117
+ // new-window -t REJECTS a pane id (tmux exits 1: "can't specify pane
118
+ // here") — only split-window -t accepts a pane. Resolve the target pane to
119
+ // its session:window first; -a then inserts the new window right after it.
120
+ // If the pane can't be resolved, fall back to no -t (tmux uses current).
121
+ const winTarget = paneWindowTarget(opts.targetPane);
122
+ if (winTarget !== null)
123
+ splitArgs.push('-a', '-t', winTarget);
106
124
  }
107
125
  }
108
126
  else {
@@ -0,0 +1,13 @@
1
+ import type { SubstrateDoc } from './schema.js';
2
+ import type { NodeConfigSubject } from './subject.js';
3
+ /** Does `doc` pass its gate for `subject`?
4
+ *
5
+ * - No gate (field absent) ⇒ `true`: always eligible (the common case).
6
+ * - A gate present ⇒ the matcher engine's verdict. An empty `gate: {}` is inert
7
+ * and returns `false` (never matches) per design §4 — so an author cannot
8
+ * write an always-eligible-via-empty-predicate doc.
9
+ *
10
+ * Eligibility only. A failing gate excludes the doc from BOTH automatic hooks
11
+ * for this node; it remains findable by `crtr memory find` (search ignores
12
+ * gate + rung). */
13
+ export declare function gatePasses(doc: SubstrateDoc, subject: NodeConfigSubject): boolean;
@@ -0,0 +1,21 @@
1
+ // gate.ts — gate evaluation. A document's optional `gate` predicate decides
2
+ // whether it is ELIGIBLE to surface at all for a given node (orthogonal to the
3
+ // rung, which decides how much). See design-substrate.md §4 + §6. Pure function
4
+ // over (parsed doc, assembled subject); the matcher engine in predicate.ts does
5
+ // the actual matching — we do NOT reimplement it here.
6
+ import { evalCondition } from '../predicate.js';
7
+ /** Does `doc` pass its gate for `subject`?
8
+ *
9
+ * - No gate (field absent) ⇒ `true`: always eligible (the common case).
10
+ * - A gate present ⇒ the matcher engine's verdict. An empty `gate: {}` is inert
11
+ * and returns `false` (never matches) per design §4 — so an author cannot
12
+ * write an always-eligible-via-empty-predicate doc.
13
+ *
14
+ * Eligibility only. A failing gate excludes the doc from BOTH automatic hooks
15
+ * for this node; it remains findable by `crtr memory find` (search ignores
16
+ * gate + rung). */
17
+ export function gatePasses(doc, subject) {
18
+ if (doc.gate === undefined)
19
+ return true;
20
+ return evalCondition(doc.gate, subject);
21
+ }
@@ -0,0 +1,7 @@
1
+ export { KINDS, isDocKind, RUNGS, rungRank, rungAtLeast, KIND_DEFAULT_RUNGS, parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, } from './schema.js';
2
+ export type { DocKind, Rung, GatePredicate, SubstrateSchema, SubstrateDoc } from './schema.js';
3
+ export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
4
+ export type { NodeConfigSubject } from './subject.js';
5
+ export { gatePasses } from './gate.js';
6
+ export { renderSkillsSection, renderPreferencesSection, renderReferencesBlock } from './render.js';
7
+ export { renderOnReadDocs } from './on-read.js';
@@ -0,0 +1,18 @@
1
+ // substrate/ — the unified document-substrate keystone: the frontmatter schema,
2
+ // the 4-rung visibility ladder, the kind→default-rungs table, the node-config
3
+ // subject assembly, and gate evaluation. Pure + well-typed; the shared base the
4
+ // CLI verbs, boot render, on-read render, and migrator all build on. See
5
+ // design-substrate.md §4/§9 + plan-substrate.md §2.
6
+ export {
7
+ // kinds
8
+ KINDS, isDocKind,
9
+ // ladder
10
+ RUNGS, rungRank, rungAtLeast,
11
+ // defaults
12
+ KIND_DEFAULT_RUNGS,
13
+ // parse + render-shared helpers
14
+ parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, } from './schema.js';
15
+ export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
16
+ export { gatePasses } from './gate.js';
17
+ export { renderSkillsSection, renderPreferencesSection, renderReferencesBlock } from './render.js';
18
+ export { renderOnReadDocs } from './on-read.js';
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Render the substrate docs that should surface alongside a just-read file.
3
+ *
4
+ * @param nodeId the canvas node whose subject gates the docs.
5
+ * @param readFilePath the path the `read` tool returned (absolute or not — it
6
+ * is resolved to a realpath internally).
7
+ * @param seen the CALLER-owned, per-session set of already-injected
8
+ * doc realpaths. Docs already present are skipped; newly
9
+ * injected docs are added. Pass the same set across reads
10
+ * within a session (clear it on session_start) to get the
11
+ * once-per-session dedup; omit it for a standalone render.
12
+ * @returns the `<auto-loaded-context>` envelope, or '' when nothing surfaces.
13
+ */
14
+ export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: Set<string>): string;
@@ -0,0 +1,292 @@
1
+ // on-read.ts — the file-read-visibility render for the document substrate.
2
+ //
3
+ // When a `read` tool call returns, the canvas-doc-substrate pi extension calls
4
+ // renderOnReadDocs() to surface the substrate docs that should appear ALONGSIDE
5
+ // the file just read — each at its FILE-READ-VISIBILITY rung (NOT the
6
+ // system-prompt rung the boot render uses). Two independent triggers decide
7
+ // which docs surface (design §4/§6; plan-substrate.md track D1):
8
+ //
9
+ // • POSITIONAL — walk the read file's ancestor dirs; any doc living in an
10
+ // ancestor's `.crouter/memory/` surfaces (a doc surfaces when a file
11
+ // beside/under its own scope dir is read). This mirrors nested-context's
12
+ // `.claude/rules` ancestor walk, but keyed on `.crouter/memory/`.
13
+ // • applies-to GLOB — any RESOLVED substrate doc (user/project/builtin scope)
14
+ // whose `appliesTo` glob matches the read file path surfaces, regardless of
15
+ // where the read file sits relative to the doc.
16
+ //
17
+ // Each candidate runs the substrate pipeline at its fileReadVisibility rung:
18
+ // parse → gatePasses(doc, assembleNodeSubject(nodeId)) → render
19
+ // (content → body, preview → previewLine, name → bare tag, none → skip).
20
+ // The result is the faithful envelope (verdict n1):
21
+ // <auto-loaded-context file="…">
22
+ // <doc kind="…" name="…" src="…" triggered-by="…">…body/preview…</doc>
23
+ // </auto-loaded-context>
24
+ // Returns '' when nothing surfaces.
25
+ //
26
+ // Pure + defensive: reads disk + the resolver + canvas-db subject assembly; no
27
+ // writes, no side effects. Every disk/parse/glob step is wrapped so one bad doc
28
+ // can never throw the whole render (the extension is additionally inert on
29
+ // error). The CALLER owns the per-session `seen` realpath set (cleared on
30
+ // session_start) and threads it in, so a given doc is injected at most once per
31
+ // session across repeated reads.
32
+ import { realpathSync } from 'node:fs';
33
+ import { homedir } from 'node:os';
34
+ import { basename, dirname, join, matchesGlob, parse, relative, sep } from 'node:path';
35
+ import { CRTR_DIR_NAME } from '../../types.js';
36
+ import { pathExists, readText, walkFiles } from '../fs-utils.js';
37
+ import { parseFrontmatterGeneric } from '../frontmatter.js';
38
+ import { listAllMemoryDocs } from '../memory-resolver.js';
39
+ import { assembleNodeSubject, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
40
+ import { cachedSubstrateDocs } from './session-cache.js';
41
+ // Ancestor dirs we never look inside for a `.crouter/memory/` store (the read
42
+ // file may live under a build/dependency tree; `.crouter` is NOT junk here — it
43
+ // is the segment we explicitly join onto each surviving ancestor).
44
+ const JUNK_DIRS = new Set(['node_modules', '.git', 'dist', 'build', '.next', '.cache', '.yalc']);
45
+ // ---------------------------------------------------------------------------
46
+ // Small path helpers (mirror the on-read precedent — nested-context /
47
+ // frontmatter-rules — so the injected envelope matches their faithful shape).
48
+ // ---------------------------------------------------------------------------
49
+ function realpathOrSelf(p) {
50
+ try {
51
+ return realpathSync(p);
52
+ }
53
+ catch {
54
+ return p;
55
+ }
56
+ }
57
+ /** Escape a value for an XML-ish attribute in the injected envelope. */
58
+ function attr(s) {
59
+ return s
60
+ .replace(/&/g, '&amp;')
61
+ .replace(/"/g, '&quot;')
62
+ .replace(/</g, '&lt;')
63
+ .replace(/>/g, '&gt;');
64
+ }
65
+ /** Nearest enclosing git repo root for a path (walk up looking for `.git`). */
66
+ function gitRootOf(p) {
67
+ let d = p;
68
+ const root = parse(d).root;
69
+ while (true) {
70
+ if (pathExists(join(d, '.git')))
71
+ return d;
72
+ if (d === root)
73
+ return null;
74
+ const parent = dirname(d);
75
+ if (parent === d)
76
+ return null;
77
+ d = parent;
78
+ }
79
+ }
80
+ /** Display a path relative to its nearest git repo root, else absolute. */
81
+ function disp(p) {
82
+ const root = gitRootOf(p);
83
+ if (!root)
84
+ return p;
85
+ const rel = relative(root, p);
86
+ return rel === '' ? '.' : rel;
87
+ }
88
+ function isJunkAncestor(dir) {
89
+ return dir.split(sep).some((seg) => JUNK_DIRS.has(seg));
90
+ }
91
+ /** Load one positionally-discovered `.crouter/memory/` file into a SubstrateDoc,
92
+ * or null when it is not a substrate doc / unreadable. `scope` is cosmetic here
93
+ * (gate eval keys off the NODE subject, render off name/body/rung). */
94
+ function loadPositionalDoc(file, memDir, scope) {
95
+ try {
96
+ const name = relative(memDir, file)
97
+ .replace(/\.md$/i, '')
98
+ .split(sep)
99
+ .join('/');
100
+ if (name === '')
101
+ return null;
102
+ const { data, body } = parseFrontmatterGeneric(readText(file));
103
+ const schema = parseSubstrateFrontmatter(data);
104
+ if (schema === null)
105
+ return null;
106
+ return { ...schema, name, scope, path: file, body };
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ function safeWalkMd(dir) {
113
+ try {
114
+ return walkFiles(dir, (n) => n.toLowerCase().endsWith('.md'));
115
+ }
116
+ catch {
117
+ return [];
118
+ }
119
+ }
120
+ /** POSITIONAL trigger: every substrate doc in an ancestor dir's
121
+ * `.crouter/memory/`, walking from the read file up to $HOME (or the
122
+ * filesystem root for a read outside $HOME), skipping junk ancestors. */
123
+ function positionalCandidates(absReadFile) {
124
+ const out = [];
125
+ const seenDocPaths = new Set();
126
+ const home = homedir();
127
+ const fsRoot = parse(absReadFile).root;
128
+ let dir = dirname(absReadFile);
129
+ let depth = 0;
130
+ while (true) {
131
+ if (!isJunkAncestor(dir)) {
132
+ const memDir = join(dir, CRTR_DIR_NAME, 'memory');
133
+ if (pathExists(memDir)) {
134
+ const scope = dir === home ? 'user' : 'project';
135
+ for (const file of safeWalkMd(memDir)) {
136
+ const real = realpathOrSelf(file);
137
+ if (seenDocPaths.has(real))
138
+ continue;
139
+ seenDocPaths.add(real);
140
+ const doc = loadPositionalDoc(file, memDir, scope);
141
+ if (doc)
142
+ out.push({ doc, realpath: real, order: depth });
143
+ }
144
+ }
145
+ }
146
+ if (dir === home || dir === fsRoot)
147
+ break;
148
+ const parent = dirname(dir);
149
+ if (parent === dir)
150
+ break;
151
+ dir = parent;
152
+ depth += 1;
153
+ }
154
+ return out;
155
+ }
156
+ /** The user/project scope root that owns a resolved doc: `<root>/.crouter/memory/…`
157
+ * → `<root>`. Builtin docs (no `.crouter` segment) return null. Used to test an
158
+ * `appliesTo` glob against a read path RELATIVE to the doc's own project root. */
159
+ function owningRootOf(doc) {
160
+ const parts = doc.path.split(sep);
161
+ const idx = parts.lastIndexOf(CRTR_DIR_NAME);
162
+ if (idx <= 0)
163
+ return null;
164
+ return parts.slice(0, idx).join(sep) || sep;
165
+ }
166
+ function globMatches(glob, absReadFile, owningRoot) {
167
+ const targets = [absReadFile, basename(absReadFile)];
168
+ if (owningRoot)
169
+ targets.push(relative(owningRoot, absReadFile));
170
+ return targets.some((t) => {
171
+ try {
172
+ return matchesGlob(t, glob);
173
+ }
174
+ catch {
175
+ return false; // an invalid glob never matches
176
+ }
177
+ });
178
+ }
179
+ /** applies-to GLOB trigger: every RESOLVED substrate doc whose `appliesTo` glob
180
+ * matches the read path. `taken` carries the realpaths already claimed by the
181
+ * positional pass, so a doc found both ways is not double-counted.
182
+ * Uses the per-session cache so the corpus is not re-walked+re-parsed on every
183
+ * read tool call (O(reads × corpus) without the cache). */
184
+ function appliesToCandidates(absReadFile, taken) {
185
+ let docs;
186
+ try {
187
+ docs = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
188
+ }
189
+ catch {
190
+ return [];
191
+ }
192
+ const out = [];
193
+ for (const doc of docs) {
194
+ const globs = doc.appliesTo;
195
+ if (!globs || globs.length === 0)
196
+ continue;
197
+ const real = realpathOrSelf(doc.path);
198
+ if (taken.has(real))
199
+ continue;
200
+ if (!globs.some((g) => globMatches(g, absReadFile, owningRootOf(doc))))
201
+ continue;
202
+ taken.add(real);
203
+ out.push({ doc, realpath: real, order: -1 });
204
+ }
205
+ return out;
206
+ }
207
+ // ---------------------------------------------------------------------------
208
+ // Per-doc envelope render.
209
+ // ---------------------------------------------------------------------------
210
+ /** One doc as a `<doc …>` element at its fileReadVisibility rung, or null when
211
+ * that rung is `none`. `content` → full body; `preview` → the routing line;
212
+ * `name` → a bare self-closed tag (the `name=` attribute IS the surface). */
213
+ function renderDocEnvelope(doc, absReadFile) {
214
+ const rung = doc.fileReadVisibility;
215
+ if (rung === 'none')
216
+ return null;
217
+ let body = '';
218
+ if (rung === 'content')
219
+ body = doc.body.trim();
220
+ else if (rung === 'preview')
221
+ body = previewLine(doc);
222
+ // 'name' → body stays '' (the tag's name attribute is the whole surface).
223
+ const attrs = `kind="${attr(doc.kind)}" name="${attr(doc.name)}" ` +
224
+ `src="${attr(disp(doc.path))}" triggered-by="${attr(disp(absReadFile))}"`;
225
+ return body === '' ? `<doc ${attrs} />` : `<doc ${attrs}>\n${body}\n</doc>`;
226
+ }
227
+ // ---------------------------------------------------------------------------
228
+ // Public entry point.
229
+ // ---------------------------------------------------------------------------
230
+ /**
231
+ * Render the substrate docs that should surface alongside a just-read file.
232
+ *
233
+ * @param nodeId the canvas node whose subject gates the docs.
234
+ * @param readFilePath the path the `read` tool returned (absolute or not — it
235
+ * is resolved to a realpath internally).
236
+ * @param seen the CALLER-owned, per-session set of already-injected
237
+ * doc realpaths. Docs already present are skipped; newly
238
+ * injected docs are added. Pass the same set across reads
239
+ * within a session (clear it on session_start) to get the
240
+ * once-per-session dedup; omit it for a standalone render.
241
+ * @returns the `<auto-loaded-context>` envelope, or '' when nothing surfaces.
242
+ */
243
+ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
244
+ let subject;
245
+ try {
246
+ subject = assembleNodeSubject(nodeId);
247
+ }
248
+ catch {
249
+ return '';
250
+ }
251
+ if (subject === null)
252
+ return '';
253
+ const absReadFile = realpathOrSelf(readFilePath);
254
+ let candidates;
255
+ try {
256
+ const positional = positionalCandidates(absReadFile);
257
+ const taken = new Set(positional.map((c) => c.realpath));
258
+ const byGlob = appliesToCandidates(absReadFile, taken);
259
+ candidates = [...positional, ...byGlob];
260
+ }
261
+ catch {
262
+ return '';
263
+ }
264
+ // Outermost-first: the nearest/most-specific doc reads last — closest to the
265
+ // file content that follows it (the applies-to set, order -1, trails).
266
+ candidates.sort((a, b) => b.order - a.order);
267
+ const rendered = [];
268
+ for (const c of candidates) {
269
+ // Never re-surface the doc the agent is literally reading.
270
+ if (c.realpath === absReadFile)
271
+ continue;
272
+ // Once-per-session dedup (caller-owned set).
273
+ if (seen.has(c.realpath))
274
+ continue;
275
+ let block;
276
+ try {
277
+ if (!gatePasses(c.doc, subject))
278
+ continue; // gated out for this node
279
+ block = renderDocEnvelope(c.doc, absReadFile);
280
+ }
281
+ catch {
282
+ continue; // a single bad doc never breaks the read
283
+ }
284
+ if (block === null)
285
+ continue; // fileReadVisibility 'none' — not a read surface
286
+ seen.add(c.realpath); // mark injected only once it actually surfaces
287
+ rendered.push(block);
288
+ }
289
+ if (rendered.length === 0)
290
+ return '';
291
+ return `<auto-loaded-context file="${attr(disp(absReadFile))}">\n${rendered.join('\n')}\n</auto-loaded-context>`;
292
+ }
@@ -0,0 +1,25 @@
1
+ /** The `## Skills` system-prompt section: every eligible `kind: skill` doc,
2
+ * rendered at its `system-prompt-visibility`. `name`-rung skills collapse into
3
+ * one compact catalog (group-collapsed, unioned with plugin/marketplace
4
+ * skills); `preview`/`content`-rung skills each get a `###` sub-section.
5
+ * Returns '' when nothing is eligible. */
6
+ export declare function renderSkillsSection(nodeId: string): string;
7
+ /** The `## Preferences` system-prompt section: every eligible `kind: preference`
8
+ * doc as its own `###` sub-section, at its `system-prompt-visibility` (the
9
+ * preference default rung is `preview` → the routing line). Returns '' when
10
+ * nothing is eligible. */
11
+ export declare function renderPreferencesSection(nodeId: string): string;
12
+ /** The `## References` block embedded INSIDE the `<crtr-context>` session_start
13
+ * message (the bearings caller pushes the returned string into the block, or
14
+ * drops it when ''). Holds every eligible `kind: reference` resolver doc at its
15
+ * `system-prompt-visibility` (reference boot default is `none`, so only
16
+ * author-promoted references show) PLUS the node-local memory docs (any kind),
17
+ * each a `###` sub-section. Replaces the old `<memory>` block. Returns '' when
18
+ * nothing is eligible.
19
+ *
20
+ * DEFENSIVE: each doc is rendered in its own try/catch so a single malformed
21
+ * doc drops only itself (with a loud stderr warning naming the offending path),
22
+ * never silently swallowing the entire block (identity included). Per the CTO
23
+ * ruling, strictness lives at the COLLECTION layer (memory-resolver.ts); this
24
+ * catch is error ISOLATION at the render layer, not a fallback parser. */
25
+ export declare function renderReferencesBlock(nodeId: string): string;