@crouton-kit/crouter 0.3.18 → 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 (257) 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/orchestrator.md +1 -1
  12. package/dist/builtin-personas/developer/orchestrator.md +1 -1
  13. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  14. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  15. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  16. package/dist/builtin-personas/plan/orchestrator.md +1 -1
  17. package/dist/builtin-personas/runtime-base.md +6 -3
  18. package/dist/builtin-personas/spec/orchestrator.md +1 -1
  19. package/dist/builtin-personas/waiting.md +8 -0
  20. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  21. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  22. package/dist/builtin-views/_lib/states.mjs +161 -0
  23. package/dist/builtin-views/canvas/client.mjs +303 -0
  24. package/dist/builtin-views/canvas/view.mjs +576 -0
  25. package/dist/builtin-views/git-pr/client.mjs +440 -0
  26. package/dist/builtin-views/git-pr/view.mjs +675 -0
  27. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  28. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  29. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  30. package/dist/builtin-views/inbox/view.mjs +889 -0
  31. package/dist/builtin-views/linkedin/client.mjs +610 -0
  32. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  33. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  34. package/dist/clients/attach/attach-cmd.js +354 -0
  35. package/dist/clients/attach/chat-view.d.ts +77 -0
  36. package/dist/clients/attach/chat-view.js +450 -0
  37. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  38. package/dist/clients/attach/clipboard-image.js +113 -0
  39. package/dist/clients/attach/config-load.d.ts +31 -0
  40. package/dist/clients/attach/config-load.js +113 -0
  41. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  42. package/dist/clients/attach/extension-dialogs.js +101 -0
  43. package/dist/clients/attach/input-controller.d.ts +54 -0
  44. package/dist/clients/attach/input-controller.js +204 -0
  45. package/dist/clients/attach/slash-commands.d.ts +36 -0
  46. package/dist/clients/attach/slash-commands.js +200 -0
  47. package/dist/clients/attach/view-socket.d.ts +48 -0
  48. package/dist/clients/attach/view-socket.js +126 -0
  49. package/dist/commands/attention.js +3 -3
  50. package/dist/commands/canvas-prune.js +1 -1
  51. package/dist/commands/daemon.js +3 -2
  52. package/dist/commands/human/prompts.js +1 -1
  53. package/dist/commands/human/queue.js +43 -8
  54. package/dist/commands/human/shared.d.ts +2 -0
  55. package/dist/commands/memory/find.d.ts +1 -0
  56. package/dist/commands/memory/find.js +180 -0
  57. package/dist/commands/memory/lint.d.ts +1 -0
  58. package/dist/commands/memory/lint.js +140 -0
  59. package/dist/commands/memory/list.d.ts +1 -0
  60. package/dist/commands/memory/list.js +79 -0
  61. package/dist/commands/memory/read.js +103 -0
  62. package/dist/commands/memory/shared.d.ts +30 -0
  63. package/dist/commands/memory/shared.js +122 -0
  64. package/dist/commands/memory/write.d.ts +1 -0
  65. package/dist/commands/memory/write.js +85 -0
  66. package/dist/commands/memory.d.ts +2 -0
  67. package/dist/commands/memory.js +27 -0
  68. package/dist/commands/node.d.ts +3 -2
  69. package/dist/commands/node.js +607 -61
  70. package/dist/commands/pkg/market-manage.js +1 -1
  71. package/dist/commands/push.js +6 -6
  72. package/dist/commands/revive.js +1 -1
  73. package/dist/commands/skill/author.js +1 -1
  74. package/dist/commands/skill/shared.d.ts +1 -8
  75. package/dist/commands/skill/shared.js +2 -55
  76. package/dist/commands/skill.js +9 -14
  77. package/dist/commands/sys/doctor.js +1 -1
  78. package/dist/commands/sys/update.js +1 -1
  79. package/dist/commands/view-cycle.d.ts +2 -0
  80. package/dist/commands/view-cycle.js +125 -0
  81. package/dist/commands/view-list.d.ts +2 -0
  82. package/dist/commands/view-list.js +66 -0
  83. package/dist/commands/view-new.d.ts +2 -0
  84. package/dist/commands/view-new.js +70 -0
  85. package/dist/commands/view-pick.d.ts +2 -0
  86. package/dist/commands/view-pick.js +119 -0
  87. package/dist/commands/view-run.d.ts +2 -0
  88. package/dist/commands/view-run.js +191 -0
  89. package/dist/commands/view.d.ts +2 -0
  90. package/dist/commands/view.js +29 -0
  91. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  92. package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
  93. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  94. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  95. package/dist/core/__tests__/cascade-close.test.js +12 -2
  96. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  97. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  98. package/dist/core/__tests__/context-intro.test.js +76 -62
  99. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  100. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  101. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  102. package/dist/core/__tests__/detach-focus.test.js +206 -0
  103. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  104. package/dist/core/__tests__/draw-style.test.js +258 -0
  105. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  106. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  107. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  108. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  109. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  110. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  111. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  112. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  113. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  114. package/dist/core/__tests__/helpers/harness.js +111 -1
  115. package/dist/core/__tests__/home-session.test.js +41 -8
  116. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  117. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  118. package/dist/core/__tests__/kickoff.test.js +37 -3
  119. package/dist/core/__tests__/live-mutation.test.js +50 -33
  120. package/dist/core/__tests__/memory.test.js +23 -115
  121. package/dist/core/__tests__/placement-focus.test.js +5 -0
  122. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  123. package/dist/core/__tests__/relaunch.test.js +4 -3
  124. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  125. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  126. package/dist/core/__tests__/spawn-root.test.js +10 -0
  127. package/dist/core/__tests__/spike-harness.test.js +1 -0
  128. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  129. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  130. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  131. package/dist/core/__tests__/wake-origin.test.js +110 -0
  132. package/dist/core/bootstrap.js +1 -1
  133. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  134. package/dist/core/canvas/browse/app.js +24 -2
  135. package/dist/core/canvas/browse/model.d.ts +38 -2
  136. package/dist/core/canvas/browse/model.js +134 -10
  137. package/dist/core/canvas/browse/render.d.ts +6 -12
  138. package/dist/core/canvas/browse/render.js +72 -104
  139. package/dist/core/canvas/canvas.js +15 -22
  140. package/dist/core/canvas/db.js +46 -0
  141. package/dist/core/canvas/index.d.ts +1 -0
  142. package/dist/core/canvas/index.js +1 -0
  143. package/dist/core/canvas/pid.d.ts +4 -0
  144. package/dist/core/canvas/pid.js +23 -0
  145. package/dist/core/canvas/render.d.ts +11 -2
  146. package/dist/core/canvas/render.js +69 -0
  147. package/dist/core/canvas/types.d.ts +85 -3
  148. package/dist/core/canvas/wakeups.d.ts +76 -0
  149. package/dist/core/canvas/wakeups.js +185 -0
  150. package/dist/core/config.js +4 -1
  151. package/dist/core/frontmatter.js +37 -124
  152. package/dist/core/memory-resolver.d.ts +49 -0
  153. package/dist/core/memory-resolver.js +141 -0
  154. package/dist/core/personas/loader.d.ts +7 -0
  155. package/dist/core/personas/loader.js +36 -5
  156. package/dist/core/personas/resolve.js +13 -3
  157. package/dist/core/predicate.d.ts +63 -0
  158. package/dist/core/predicate.js +189 -0
  159. package/dist/core/resolver.js +26 -5
  160. package/dist/core/runtime/bearings.d.ts +53 -12
  161. package/dist/core/runtime/bearings.js +132 -59
  162. package/dist/core/runtime/broker-cli.d.ts +1 -0
  163. package/dist/core/runtime/broker-cli.js +46 -0
  164. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  165. package/dist/core/runtime/broker-protocol.js +153 -0
  166. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  167. package/dist/core/runtime/broker-sdk.js +72 -0
  168. package/dist/core/runtime/broker.d.ts +55 -0
  169. package/dist/core/runtime/broker.js +1128 -0
  170. package/dist/core/runtime/close.js +35 -6
  171. package/dist/core/runtime/host.d.ts +53 -0
  172. package/dist/core/runtime/host.js +186 -0
  173. package/dist/core/runtime/kickoff.d.ts +2 -1
  174. package/dist/core/runtime/kickoff.js +91 -5
  175. package/dist/core/runtime/launch.d.ts +45 -2
  176. package/dist/core/runtime/launch.js +65 -2
  177. package/dist/core/runtime/lifecycle.js +23 -6
  178. package/dist/core/runtime/memory.d.ts +2 -42
  179. package/dist/core/runtime/memory.js +11 -162
  180. package/dist/core/runtime/nodes.d.ts +33 -0
  181. package/dist/core/runtime/nodes.js +59 -1
  182. package/dist/core/runtime/persona.js +21 -11
  183. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  184. package/dist/core/runtime/pi-vendored.js +49 -0
  185. package/dist/core/runtime/placement.d.ts +32 -14
  186. package/dist/core/runtime/placement.js +206 -52
  187. package/dist/core/runtime/promote.d.ts +0 -6
  188. package/dist/core/runtime/promote.js +1 -12
  189. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  190. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  191. package/dist/core/runtime/reset.js +8 -6
  192. package/dist/core/runtime/revive.d.ts +2 -0
  193. package/dist/core/runtime/revive.js +34 -34
  194. package/dist/core/runtime/spawn.d.ts +19 -0
  195. package/dist/core/runtime/spawn.js +75 -22
  196. package/dist/core/runtime/stop-guard.d.ts +1 -1
  197. package/dist/core/runtime/stop-guard.js +6 -1
  198. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  199. package/dist/core/runtime/tmux-chrome.js +1 -1
  200. package/dist/core/runtime/tmux.d.ts +28 -0
  201. package/dist/core/runtime/tmux.js +80 -6
  202. package/dist/core/scope.d.ts +11 -0
  203. package/dist/core/scope.js +39 -0
  204. package/dist/core/spawn.d.ts +5 -0
  205. package/dist/core/spawn.js +20 -2
  206. package/dist/core/substrate/gate.d.ts +13 -0
  207. package/dist/core/substrate/gate.js +21 -0
  208. package/dist/core/substrate/index.d.ts +7 -0
  209. package/dist/core/substrate/index.js +18 -0
  210. package/dist/core/substrate/on-read.d.ts +14 -0
  211. package/dist/core/substrate/on-read.js +292 -0
  212. package/dist/core/substrate/render.d.ts +25 -0
  213. package/dist/core/substrate/render.js +256 -0
  214. package/dist/core/substrate/schema.d.ts +76 -0
  215. package/dist/core/substrate/schema.js +124 -0
  216. package/dist/core/substrate/session-cache.d.ts +30 -0
  217. package/dist/core/substrate/session-cache.js +77 -0
  218. package/dist/core/substrate/subject.d.ts +41 -0
  219. package/dist/core/substrate/subject.js +54 -0
  220. package/dist/core/tui/contract.d.ts +83 -0
  221. package/dist/core/tui/contract.js +8 -0
  222. package/dist/core/tui/draw.d.ts +96 -0
  223. package/dist/core/tui/draw.js +339 -0
  224. package/dist/core/tui/host.d.ts +29 -0
  225. package/dist/core/tui/host.js +379 -0
  226. package/dist/core/tui/loader.d.ts +16 -0
  227. package/dist/core/tui/loader.js +94 -0
  228. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  229. package/dist/core/wake.d.ts +86 -0
  230. package/dist/core/wake.js +308 -0
  231. package/dist/daemon/crtrd.d.ts +29 -4
  232. package/dist/daemon/crtrd.js +662 -46
  233. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  234. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  235. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  236. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  237. package/dist/pi-extensions/canvas-commands.js +10 -0
  238. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  239. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  240. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  241. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  242. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  243. package/dist/pi-extensions/canvas-nav.js +87 -23
  244. package/dist/pi-extensions/canvas-stophook.js +17 -8
  245. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  246. package/dist/pi-extensions/canvas-view.js +75 -0
  247. package/dist/prompts/skill.js +19 -26
  248. package/dist/prompts/view.d.ts +7 -0
  249. package/dist/prompts/view.js +101 -0
  250. package/dist/types.d.ts +4 -0
  251. package/dist/types.js +1 -0
  252. package/package.json +9 -4
  253. package/dist/commands/skill/find.d.ts +0 -4
  254. package/dist/commands/skill/find.js +0 -257
  255. package/dist/commands/skill/read.js +0 -91
  256. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  257. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -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;