@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
@@ -0,0 +1,256 @@
1
+ // render.ts — the pure boot-render functions for the document substrate.
2
+ //
3
+ // Two boot targets, three functions:
4
+ // • the SYSTEM-PROMPT half — `## Skills` (renderSkillsSection) + `## Preferences`
5
+ // (renderPreferencesSection) — strings the D2 `before_agent_start` extension
6
+ // splices into the system prompt (built by a sibling AFTER this module);
7
+ // • the `<crtr-context>` half — `## References` (renderReferencesBlock) — the
8
+ // string wired into bearings.ts's session_start message, REPLACING the old
9
+ // `<memory>` block.
10
+ //
11
+ // Every doc flows through the same pipeline (design §4/§6/§9):
12
+ // MemoryDoc → parseSubstrateDoc → (null-filter non-substrate) →
13
+ // gatePasses(doc, assembleNodeSubject(nodeId)) → render at the doc's
14
+ // `system-prompt-visibility` rung.
15
+ //
16
+ // Pure + defensive: reads the resolver, canvas-db (subject assembly), and the
17
+ // node-local memory dir; performs no writes and no side effects. A single
18
+ // malformed doc must never throw the whole render — parseSubstrateDoc returns
19
+ // null for a non-substrate doc and is `.filter()`ed out; per-file loads are
20
+ // wrapped so one bad file is skipped, not fatal.
21
+ import { relative, sep } from 'node:path';
22
+ import { SCOPE_SKILL_PLUGIN } from '../../types.js';
23
+ import { listAllPlugins, listAllSkills } from '../resolver.js';
24
+ import { renderCatalogSection } from '../../commands/skill/shared.js';
25
+ import { listAllMemoryDocs } from '../memory-resolver.js';
26
+ import { parseFrontmatterGeneric } from '../frontmatter.js';
27
+ import { pathExists, readText, walkFiles } from '../fs-utils.js';
28
+ import { memoryDir } from '../runtime/memory.js';
29
+ import { assembleNodeSubject, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
30
+ import { cachedAllPlugins, cachedNodeSubject, cachedSubstrateDocs, } from './session-cache.js';
31
+ // ---------------------------------------------------------------------------
32
+ // The shared per-doc pipeline.
33
+ // ---------------------------------------------------------------------------
34
+ /** The resolver-provided substrate docs of one `kind`, eligible at boot for
35
+ * `subject`: parsed (non-substrate docs null-filtered), gate-passed, and at a
36
+ * system-prompt rung above `none`. Resolver = user + project + builtin scopes
37
+ * (precedence-ordered); node-local is loaded separately (see nodeLocalDocs).
38
+ * Uses the per-session cache so the full corpus is scanned+parsed at most once
39
+ * per session across the three boot-render calls. */
40
+ function resolverDocs(subject, kind) {
41
+ let docs;
42
+ try {
43
+ docs = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
44
+ }
45
+ catch {
46
+ return [];
47
+ }
48
+ return docs
49
+ .filter((d) => d.kind === kind)
50
+ .filter((d) => gatePasses(d, subject))
51
+ .filter((d) => d.systemPromptVisibility !== 'none');
52
+ }
53
+ /** The node-local memory docs eligible at boot. Node-local memory lives in the
54
+ * canvas home (`nodes/<id>/context/memory/`) and is OUTSIDE the scope resolver,
55
+ * so it is loaded directly here, its raw frontmatter run through
56
+ * parseSubstrateFrontmatter (mirroring the resolver pipeline), then gate-passed.
57
+ * Old-format / non-substrate files (incl. the MEMORY.md index, which has no
58
+ * `kind`) parse to null and drop out. Returned across ALL kinds — node-local is
59
+ * the catch-all this-node store and rides into the references block as the
60
+ * replacement for the old node-local `<memory>` stanza.
61
+ *
62
+ * IMPORTANT: node-local docs are NOT filtered by `systemPromptVisibility` rung.
63
+ * A migrated node-local reference defaults to rung `none`, which would make it
64
+ * invisible — but the design explicitly says "node-local rides into references"
65
+ * without qualification. Suppressing them by rung contradicts that contract.
66
+ * A `none`-rung node-local doc renders as a `### <name>` title stub (the `name`
67
+ * rung fallback in renderSubSection), which is the minimum meaningful surface.
68
+ * Only gate evaluation removes a node-local doc from the block. */
69
+ function nodeLocalDocs(nodeId, subject) {
70
+ const dir = memoryDir(nodeId);
71
+ if (!pathExists(dir))
72
+ return [];
73
+ const out = [];
74
+ for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
75
+ const name = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
76
+ if (!name)
77
+ continue;
78
+ try {
79
+ const { data, body } = parseFrontmatterGeneric(readText(file));
80
+ const schema = parseSubstrateFrontmatter(data);
81
+ if (schema === null)
82
+ continue;
83
+ // node-local is NOT a resolver scope; `scope` is a placeholder never read
84
+ // by gate eval (keyed off the NODE subject, not the doc) nor by the
85
+ // renderers (keyed off name / body / rung).
86
+ out.push({ ...schema, name, scope: 'user', path: file, body });
87
+ }
88
+ catch {
89
+ // A single malformed file is skipped, never fatal to the render.
90
+ continue;
91
+ }
92
+ }
93
+ // Gate-filter only: rung is NOT filtered (see comment above).
94
+ return out.filter((d) => gatePasses(d, subject));
95
+ }
96
+ // ---------------------------------------------------------------------------
97
+ // Sub-section render (preview / content / name) — the per-doc `###` block used
98
+ // by every kind EXCEPT name-rung skills (those fold into the compact catalog).
99
+ // ---------------------------------------------------------------------------
100
+ /** One doc rendered as its own `### <name>` sub-section, at its system-prompt
101
+ * rung: `preview` → the generated routing line; `content` → the full body;
102
+ * `name` → the title alone (`none` is filtered upstream, never reaches here). */
103
+ function renderSubSection(d) {
104
+ const header = `### ${d.name}`;
105
+ switch (d.systemPromptVisibility) {
106
+ case 'preview':
107
+ return `${header}\n${previewLine(d)}`;
108
+ case 'content': {
109
+ const body = d.body.trim();
110
+ return body === '' ? header : `${header}\n${body}`;
111
+ }
112
+ default: // 'name'
113
+ return header;
114
+ }
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // 1. Skills section — `## Skills` (system prompt).
118
+ // ---------------------------------------------------------------------------
119
+ /** The compact, group-collapsed `name`-rung catalog: the UNION of name-rung
120
+ * substrate `skill` docs (the migrated scope-local + builtin skills, rendered
121
+ * as scope-local `_` entries) WITH plugin/marketplace skills (which stay
122
+ * resolver-provided, NOT migrated — named-plugin skills at user/project scope;
123
+ * the SCOPE_SKILL_PLUGIN sentinel and builtin scope are EXCLUDED because they
124
+ * become substrate docs). Reuses skill/shared.ts's renderCatalogSection
125
+ * group-collapse so the boot catalog matches today's `<skills count=N>` shape.
126
+ * Returns '' when nothing is in the catalog. */
127
+ function renderSkillCatalog(nameRungSkillDocs) {
128
+ let pluginSkills;
129
+ try {
130
+ pluginSkills = listAllSkills().filter((s) => s.enabled && s.plugin !== SCOPE_SKILL_PLUGIN && s.scope !== 'builtin');
131
+ }
132
+ catch {
133
+ pluginSkills = [];
134
+ }
135
+ const entries = [
136
+ ...pluginSkills.map((s) => ({ scope: s.scope, plugin: s.plugin, name: s.name })),
137
+ ...nameRungSkillDocs.map((d) => ({ scope: d.scope, plugin: SCOPE_SKILL_PLUGIN, name: d.name })),
138
+ ];
139
+ if (entries.length === 0)
140
+ return '';
141
+ // Group by scope+plugin → forest-root names (drop nested children), mirroring
142
+ // buildSkillCatalog's source construction.
143
+ const bySource = new Map();
144
+ for (const e of entries) {
145
+ const key = `${e.scope}\t${e.plugin}`;
146
+ const arr = bySource.get(key);
147
+ if (arr)
148
+ arr.push(e);
149
+ else
150
+ bySource.set(key, [e]);
151
+ }
152
+ const projectSources = [];
153
+ const userSources = [];
154
+ for (const [key, group] of bySource) {
155
+ const [scope, plugin] = key.split('\t');
156
+ const names = group.map((g) => g.name);
157
+ const roots = names
158
+ .filter((n) => !names.some((m) => m !== n && n.startsWith(m + '/')))
159
+ .sort();
160
+ if (roots.length === 0)
161
+ continue;
162
+ (scope === 'project' ? projectSources : userSources).push({ plugin, roots });
163
+ }
164
+ const descriptions = new Map();
165
+ try {
166
+ for (const p of cachedAllPlugins(listAllPlugins)) {
167
+ if (p.manifest.description)
168
+ descriptions.set(p.name, p.manifest.description);
169
+ }
170
+ }
171
+ catch {
172
+ // descriptions are an optional suffix; render without them on failure.
173
+ }
174
+ const body = [];
175
+ renderCatalogSection('Project', projectSources, descriptions, body);
176
+ renderCatalogSection('User', userSources, descriptions, body);
177
+ // renderCatalogSection leads each section with a blank separator; drop it so
178
+ // the catalog starts on its first real line.
179
+ while (body.length > 0 && body[0] === '')
180
+ body.shift();
181
+ return body.length === 0 ? '' : body.join('\n');
182
+ }
183
+ /** The `## Skills` system-prompt section: every eligible `kind: skill` doc,
184
+ * rendered at its `system-prompt-visibility`. `name`-rung skills collapse into
185
+ * one compact catalog (group-collapsed, unioned with plugin/marketplace
186
+ * skills); `preview`/`content`-rung skills each get a `###` sub-section.
187
+ * Returns '' when nothing is eligible. */
188
+ export function renderSkillsSection(nodeId) {
189
+ const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
190
+ if (subject === null)
191
+ return '';
192
+ const skills = resolverDocs(subject, 'skill');
193
+ const catalog = renderSkillCatalog(skills.filter((d) => d.systemPromptVisibility === 'name'));
194
+ const elevated = skills
195
+ .filter((d) => d.systemPromptVisibility !== 'name')
196
+ .map(renderSubSection);
197
+ const blocks = [catalog, ...elevated].filter((s) => s !== '');
198
+ if (blocks.length === 0)
199
+ return '';
200
+ return `## Skills\n\n${blocks.join('\n\n')}`;
201
+ }
202
+ // ---------------------------------------------------------------------------
203
+ // 2. Preferences section — `## Preferences` (system prompt).
204
+ // ---------------------------------------------------------------------------
205
+ /** The `## Preferences` system-prompt section: every eligible `kind: preference`
206
+ * doc as its own `###` sub-section, at its `system-prompt-visibility` (the
207
+ * preference default rung is `preview` → the routing line). Returns '' when
208
+ * nothing is eligible. */
209
+ export function renderPreferencesSection(nodeId) {
210
+ const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
211
+ if (subject === null)
212
+ return '';
213
+ const subs = resolverDocs(subject, 'preference')
214
+ .map(renderSubSection)
215
+ .filter((s) => s !== '');
216
+ if (subs.length === 0)
217
+ return '';
218
+ return `## Preferences\n\n${subs.join('\n\n')}`;
219
+ }
220
+ // ---------------------------------------------------------------------------
221
+ // 3. References block — `## References` (inside the <crtr-context> message).
222
+ // ---------------------------------------------------------------------------
223
+ /** The `## References` block embedded INSIDE the `<crtr-context>` session_start
224
+ * message (the bearings caller pushes the returned string into the block, or
225
+ * drops it when ''). Holds every eligible `kind: reference` resolver doc at its
226
+ * `system-prompt-visibility` (reference boot default is `none`, so only
227
+ * author-promoted references show) PLUS the node-local memory docs (any kind),
228
+ * each a `###` sub-section. Replaces the old `<memory>` block. Returns '' when
229
+ * nothing is eligible.
230
+ *
231
+ * DEFENSIVE: each doc is rendered in its own try/catch so a single malformed
232
+ * doc drops only itself (with a loud stderr warning naming the offending path),
233
+ * never silently swallowing the entire block (identity included). Per the CTO
234
+ * ruling, strictness lives at the COLLECTION layer (memory-resolver.ts); this
235
+ * catch is error ISOLATION at the render layer, not a fallback parser. */
236
+ export function renderReferencesBlock(nodeId) {
237
+ const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
238
+ if (subject === null)
239
+ return '';
240
+ const docs = [...resolverDocs(subject, 'reference'), ...nodeLocalDocs(nodeId, subject)];
241
+ const subs = [];
242
+ for (const d of docs) {
243
+ try {
244
+ const rendered = renderSubSection(d);
245
+ if (rendered !== '')
246
+ subs.push(rendered);
247
+ }
248
+ catch (e) {
249
+ const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
250
+ process.stderr.write(`[crtr substrate] renderReferencesBlock: skipping doc "${d.path}": ${msg}\n`);
251
+ }
252
+ }
253
+ if (subs.length === 0)
254
+ return '';
255
+ return `## References\n\n${subs.join('\n\n')}`;
256
+ }
@@ -0,0 +1,76 @@
1
+ import type { Scope } from '../../types.js';
2
+ import type { MemoryDoc } from '../memory-resolver.js';
3
+ export declare const KINDS: readonly ["skill", "reference", "preference"];
4
+ export type DocKind = (typeof KINDS)[number];
5
+ /** Is `v` one of the three valid document kinds? */
6
+ export declare function isDocKind(v: unknown): v is DocKind;
7
+ export declare const RUNGS: readonly ["none", "name", "preview", "content"];
8
+ export type Rung = (typeof RUNGS)[number];
9
+ /** Ordinal of a rung on the ladder (none=0 … content=3). */
10
+ export declare function rungRank(r: Rung): number;
11
+ /** Does rung `r` disclose at least as much as `min`? — e.g.
12
+ * `rungAtLeast(doc.systemPromptVisibility, 'name')` ⇒ "shows at boot at all". */
13
+ export declare function rungAtLeast(r: Rung, min: Rung): boolean;
14
+ export declare const KIND_DEFAULT_RUNGS: Record<DocKind, {
15
+ systemPrompt: Rung;
16
+ fileRead: Rung;
17
+ }>;
18
+ /** A gate predicate tree, evaluated by predicate.ts (`evalCondition`) against
19
+ * the node-config subject. Typed loosely on purpose — the matcher engine owns
20
+ * validation; structurally it is a field→matcher map with optional
21
+ * `all`/`any`/`not` combinators (design §4). */
22
+ export type GatePredicate = Record<string, unknown>;
23
+ /** The frontmatter-derived schema of a substrate document, with kind-aware
24
+ * defaults applied. Required fields (`kind`/`when`/`why`) and optionals all
25
+ * resolved to concrete typed values. */
26
+ export interface SubstrateSchema {
27
+ /** Which of the three semantic kinds. */
28
+ kind: DocKind;
29
+ /** Routing condition — "When you are in a situation like X…" (design §4). */
30
+ when: string;
31
+ /** Payoff — "…because Z." Half of the generated preview line. */
32
+ why: string;
33
+ /** Human-facing abbreviation for `crtr memory list`. NEVER loaded into an
34
+ * agent's context (design §3). Empty string when absent. */
35
+ shortForm: string;
36
+ /** How much surfaces at boot (system prompt / autoloaded context). */
37
+ systemPromptVisibility: Rung;
38
+ /** How much surfaces on-read (when a related file is read). */
39
+ fileReadVisibility: Rung;
40
+ /** Optional eligibility predicate over the node's own config. Absent ⇒ always
41
+ * eligible. An empty `{}` is carried as-is and is inert (never matches) — see
42
+ * `gatePasses`. */
43
+ gate?: GatePredicate;
44
+ /** Optional glob list narrowing the on-read trigger to matching read files.
45
+ * Absent ⇒ positional trigger only. A single glob is normalized to a 1-list. */
46
+ appliesTo?: string[];
47
+ }
48
+ /** A fully-resolved substrate document: the parsed schema PLUS the resolver's
49
+ * path-derived identity and body. This single object flows through the whole
50
+ * pipeline (gate eval → boot/on-read render), so a renderer never re-parses. */
51
+ export interface SubstrateDoc extends SubstrateSchema {
52
+ /** Path-derived identity, e.g. `taste/document-substrate` (resolver-supplied). */
53
+ name: string;
54
+ /** The scope this doc resolved from. */
55
+ scope: Scope;
56
+ /** Absolute path to the source .md. */
57
+ path: string;
58
+ /** Document body, frontmatter stripped. */
59
+ body: string;
60
+ }
61
+ /** Parse a raw frontmatter record (from `parseFrontmatterGeneric`, via the
62
+ * resolver) into a typed schema with defaults applied. Returns `null` when the
63
+ * record is absent or carries no valid `kind` — i.e. it is not a substrate
64
+ * document and cannot be classified or defaulted. Tolerant of every other
65
+ * imperfection (missing `when`/`why` default to '', a bad rung falls back to
66
+ * the kind default), so a renderer mapping over many docs never throws. */
67
+ export declare function parseSubstrateFrontmatter(fm: Record<string, unknown> | null): SubstrateSchema | null;
68
+ /** Parse a resolved MemoryDoc into a fully-typed SubstrateDoc (schema + the
69
+ * resolver's name/scope/path/body). Returns `null` for a non-substrate doc
70
+ * (no valid `kind`), so callers can `docs.map(parseSubstrateDoc).filter(...)`. */
71
+ export declare function parseSubstrateDoc(doc: MemoryDoc): SubstrateDoc | null;
72
+ /** The generated `preview`-rung routing line (design §4), composed from the two
73
+ * required prose fields and the kind: `"{when}, read this {kind}. {why}."`.
74
+ * Both boot and on-read render render this identical line, so it lives once
75
+ * here to prevent drift. Light cleanup avoids doubled punctuation. */
76
+ export declare function previewLine(doc: Pick<SubstrateSchema, 'kind' | 'when' | 'why'>): string;
@@ -0,0 +1,124 @@
1
+ // schema.ts — the typed shape of a substrate document's frontmatter, the
2
+ // visibility ladder, the kind→default-rungs table, and the parse/validate
3
+ // function that turns a resolved MemoryDoc into a fully-typed SubstrateDoc with
4
+ // defaults applied. This is the keystone every downstream track (CLI verbs,
5
+ // boot render, on-read render, migrator) builds against — pure and side-effect
6
+ // free. See design-substrate.md §4 (schema) + §9 (defaults).
7
+ // ---------------------------------------------------------------------------
8
+ // Kinds — the three semantic kinds (design §3). `kind` is data, not a fork.
9
+ // ---------------------------------------------------------------------------
10
+ export const KINDS = ['skill', 'reference', 'preference'];
11
+ /** Is `v` one of the three valid document kinds? */
12
+ export function isDocKind(v) {
13
+ return typeof v === 'string' && KINDS.includes(v);
14
+ }
15
+ // ---------------------------------------------------------------------------
16
+ // The 4-rung visibility ladder (design §4). A strict, monotone ordering where
17
+ // each rung is a superset of disclosure over the one before:
18
+ // none < name < preview < content
19
+ // ---------------------------------------------------------------------------
20
+ export const RUNGS = ['none', 'name', 'preview', 'content'];
21
+ /** Ordinal of a rung on the ladder (none=0 … content=3). */
22
+ export function rungRank(r) {
23
+ return RUNGS.indexOf(r);
24
+ }
25
+ /** Does rung `r` disclose at least as much as `min`? — e.g.
26
+ * `rungAtLeast(doc.systemPromptVisibility, 'name')` ⇒ "shows at boot at all". */
27
+ export function rungAtLeast(r, min) {
28
+ return rungRank(r) >= rungRank(min);
29
+ }
30
+ // ---------------------------------------------------------------------------
31
+ // The kind→default-rungs table (design §9). When an author omits a visibility
32
+ // field, its default is a function of `kind` — chosen so the common case is
33
+ // correct with no thought and the migration is behavior-preserving for skills
34
+ // and preferences. One data row per kind, NOT a code fork.
35
+ //
36
+ // kind system-prompt-visibility file-read-visibility
37
+ // skill name none
38
+ // preference preview none
39
+ // reference none preview
40
+ // ---------------------------------------------------------------------------
41
+ export const KIND_DEFAULT_RUNGS = {
42
+ skill: { systemPrompt: 'name', fileRead: 'none' },
43
+ preference: { systemPrompt: 'preview', fileRead: 'none' },
44
+ reference: { systemPrompt: 'none', fileRead: 'preview' },
45
+ };
46
+ // ---------------------------------------------------------------------------
47
+ // Parse / validate.
48
+ // ---------------------------------------------------------------------------
49
+ /** Parse a raw frontmatter record (from `parseFrontmatterGeneric`, via the
50
+ * resolver) into a typed schema with defaults applied. Returns `null` when the
51
+ * record is absent or carries no valid `kind` — i.e. it is not a substrate
52
+ * document and cannot be classified or defaulted. Tolerant of every other
53
+ * imperfection (missing `when`/`why` default to '', a bad rung falls back to
54
+ * the kind default), so a renderer mapping over many docs never throws. */
55
+ export function parseSubstrateFrontmatter(fm) {
56
+ if (fm === null)
57
+ return null;
58
+ if (!isDocKind(fm.kind))
59
+ return null;
60
+ const kind = fm.kind;
61
+ const defaults = KIND_DEFAULT_RUNGS[kind];
62
+ return {
63
+ kind,
64
+ when: strField(fm.when),
65
+ why: strField(fm.why),
66
+ shortForm: strField(fm['short-form']),
67
+ systemPromptVisibility: parseRung(fm['system-prompt-visibility'], defaults.systemPrompt),
68
+ fileReadVisibility: parseRung(fm['file-read-visibility'], defaults.fileRead),
69
+ gate: parseGate(fm.gate),
70
+ appliesTo: parseAppliesTo(fm['applies-to']),
71
+ };
72
+ }
73
+ /** Parse a resolved MemoryDoc into a fully-typed SubstrateDoc (schema + the
74
+ * resolver's name/scope/path/body). Returns `null` for a non-substrate doc
75
+ * (no valid `kind`), so callers can `docs.map(parseSubstrateDoc).filter(...)`. */
76
+ export function parseSubstrateDoc(doc) {
77
+ const schema = parseSubstrateFrontmatter(doc.frontmatter);
78
+ if (schema === null)
79
+ return null;
80
+ return { ...schema, name: doc.name, scope: doc.scope, path: doc.path, body: doc.body };
81
+ }
82
+ /** The generated `preview`-rung routing line (design §4), composed from the two
83
+ * required prose fields and the kind: `"{when}, read this {kind}. {why}."`.
84
+ * Both boot and on-read render render this identical line, so it lives once
85
+ * here to prevent drift. Light cleanup avoids doubled punctuation. */
86
+ export function previewLine(doc) {
87
+ const when = doc.when.trim().replace(/[.,]+$/, '');
88
+ const why = doc.why.trim().replace(/\.+$/, '');
89
+ return `${when}, read this ${doc.kind}. ${why}.`;
90
+ }
91
+ // ---------------------------------------------------------------------------
92
+ // Field coercion helpers (private).
93
+ // ---------------------------------------------------------------------------
94
+ function strField(v) {
95
+ return typeof v === 'string' ? v : '';
96
+ }
97
+ /** Resolve a visibility field to a ladder rung, falling back to the kind
98
+ * default when absent/invalid. */
99
+ function parseRung(v, fallback) {
100
+ return typeof v === 'string' && RUNGS.includes(v)
101
+ ? v
102
+ : fallback;
103
+ }
104
+ /** A gate is engaged only when frontmatter carries a non-null, non-array object
105
+ * (the predicate vocabulary's field→matcher map). Anything else (absent, null,
106
+ * scalar, array) ⇒ no gate ⇒ always eligible — the safe default that never
107
+ * silently hides a doc. An empty `{}` IS carried (and is inert per design §4). */
108
+ function parseGate(v) {
109
+ return v !== null && typeof v === 'object' && !Array.isArray(v)
110
+ ? v
111
+ : undefined;
112
+ }
113
+ /** Normalize `applies-to` to a non-empty glob list, or undefined (positional
114
+ * trigger only). Accepts a single string or an array of strings. */
115
+ function parseAppliesTo(v) {
116
+ if (typeof v === 'string') {
117
+ return v.trim() === '' ? undefined : [v];
118
+ }
119
+ if (Array.isArray(v)) {
120
+ const globs = v.filter((g) => typeof g === 'string' && g.trim() !== '');
121
+ return globs.length > 0 ? globs : undefined;
122
+ }
123
+ return undefined;
124
+ }
@@ -0,0 +1,30 @@
1
+ import type { MemoryDoc } from '../memory-resolver.js';
2
+ import type { SubstrateDoc } from './schema.js';
3
+ type ListAllMemoryDocsFn = () => MemoryDoc[];
4
+ type ParseSubstrateDocFn = (doc: MemoryDoc) => SubstrateDoc | null;
5
+ type ListAllPluginsFn = () => {
6
+ name: string;
7
+ manifest: {
8
+ description?: string;
9
+ };
10
+ }[];
11
+ type AssembleNodeSubjectFn = (id: string) => import('./subject.js').NodeConfigSubject | null;
12
+ /** Called by canvas-doc-substrate.ts on every `session_start`.
13
+ * Resets all cached values so the next access triggers a fresh scan. */
14
+ export declare function clearSessionCache(): void;
15
+ /** All memory docs (listAllMemoryDocs()), scanned once per session.
16
+ * `listFn` is injected by the caller to avoid circular imports at module init. */
17
+ export declare function cachedAllMemoryDocs(listFn: ListAllMemoryDocsFn): MemoryDoc[];
18
+ /** allMemoryDocs mapped through parseSubstrateDoc + null-filtered.
19
+ * Both render.ts and on-read.ts consume this. */
20
+ export declare function cachedSubstrateDocs(listFn: ListAllMemoryDocsFn, parseFn: ParseSubstrateDocFn): SubstrateDoc[];
21
+ /** listAllPlugins() result, cached per session. */
22
+ export declare function cachedAllPlugins(listFn: ListAllPluginsFn): {
23
+ name: string;
24
+ manifest: {
25
+ description?: string;
26
+ };
27
+ }[];
28
+ /** assembleNodeSubject(nodeId), cached per (session × nodeId). */
29
+ export declare function cachedNodeSubject(nodeId: string, assembleFn: AssembleNodeSubjectFn): import('./subject.js').NodeConfigSubject | null;
30
+ export {};
@@ -0,0 +1,77 @@
1
+ // session-cache.ts — a per-session parse cache for the document substrate.
2
+ //
3
+ // Both substrate hooks (boot render in render.ts, on-read in on-read.ts) re-walk
4
+ // and re-YAML-parse the full corpus on every invocation without memoization. At
5
+ // the two highest-frequency hook sites this is O(reads × corpus) and O(turns ×
6
+ // corpus) respectively. This module provides a single session-scoped cache that
7
+ // the hooks read from, so the corpus is scanned and parsed AT MOST ONCE per
8
+ // session turn/read burst.
9
+ //
10
+ // Lifecycle:
11
+ // • clearSessionCache() — called on every `session_start` by canvas-doc-
12
+ // substrate.ts. It does NOT need to be called before the first use: a
13
+ // cold cache (null) triggers a fresh scan on the next read.
14
+ // • All getters lazily populate on first call after a clear.
15
+ // • The cache is a module-level singleton (one JS module instance per pi
16
+ // process, and each pi process hosts exactly one canvas node session).
17
+ //
18
+ // Cached entries:
19
+ // • allMemoryDocs — the full listAllMemoryDocs() corpus (parsed MemoryDocs,
20
+ // already YAML-parsed by the resolver). Shared by both
21
+ // render.ts and on-read.ts so each session call site only
22
+ // pays the filesystem walk+parse ONCE.
23
+ // • substrateDocs — allMemoryDocs mapped through parseSubstrateDoc and
24
+ // null-filtered. Used by render.ts's resolverDocs and
25
+ // on-read.ts's appliesToCandidates.
26
+ // • allPlugins — listAllPlugins() result. renderSkillCatalog was calling
27
+ // listAllPlugins() twice per turn (once via listAllSkills,
28
+ // once directly for descriptions).
29
+ // • nodeSubjects — assembleNodeSubject() result keyed by nodeId. The three
30
+ // boot-render functions each call it; caching avoids the
31
+ // meta.json read + sqlite spine-walk happening 2-3x per turn.
32
+ const _cache = {
33
+ allMemoryDocs: null,
34
+ substrateDocs: null,
35
+ allPlugins: null,
36
+ nodeSubjects: new Map(),
37
+ };
38
+ /** Called by canvas-doc-substrate.ts on every `session_start`.
39
+ * Resets all cached values so the next access triggers a fresh scan. */
40
+ export function clearSessionCache() {
41
+ _cache.allMemoryDocs = null;
42
+ _cache.substrateDocs = null;
43
+ _cache.allPlugins = null;
44
+ _cache.nodeSubjects.clear();
45
+ }
46
+ /** All memory docs (listAllMemoryDocs()), scanned once per session.
47
+ * `listFn` is injected by the caller to avoid circular imports at module init. */
48
+ export function cachedAllMemoryDocs(listFn) {
49
+ if (_cache.allMemoryDocs === null) {
50
+ _cache.allMemoryDocs = listFn();
51
+ }
52
+ return _cache.allMemoryDocs;
53
+ }
54
+ /** allMemoryDocs mapped through parseSubstrateDoc + null-filtered.
55
+ * Both render.ts and on-read.ts consume this. */
56
+ export function cachedSubstrateDocs(listFn, parseFn) {
57
+ if (_cache.substrateDocs === null) {
58
+ _cache.substrateDocs = cachedAllMemoryDocs(listFn)
59
+ .map(parseFn)
60
+ .filter((d) => d !== null);
61
+ }
62
+ return _cache.substrateDocs;
63
+ }
64
+ /** listAllPlugins() result, cached per session. */
65
+ export function cachedAllPlugins(listFn) {
66
+ if (_cache.allPlugins === null) {
67
+ _cache.allPlugins = listFn();
68
+ }
69
+ return _cache.allPlugins;
70
+ }
71
+ /** assembleNodeSubject(nodeId), cached per (session × nodeId). */
72
+ export function cachedNodeSubject(nodeId, assembleFn) {
73
+ if (!_cache.nodeSubjects.has(nodeId)) {
74
+ _cache.nodeSubjects.set(nodeId, assembleFn(nodeId));
75
+ }
76
+ return _cache.nodeSubjects.get(nodeId) ?? null;
77
+ }
@@ -0,0 +1,41 @@
1
+ import { type Mode, type Lifecycle } from '../canvas/index.js';
2
+ /** The gate input: a node's configuration as a structured object. Field names
3
+ * and shape are the predicate vocabulary's dotted paths (`orchestration.depth`,
4
+ * `hasManager`, …). There is intentionally NO `tags` field — NodeMeta has none. */
5
+ export interface NodeConfigSubject {
6
+ /** The node's role, free-form (developer | explore | design | …). NodeMeta.kind. */
7
+ kind: string;
8
+ /** base (hands-on worker) | orchestrator (delegating manager). NodeMeta.mode. */
9
+ mode: Mode;
10
+ /** terminal (worker) | resident (manager). NodeMeta.lifecycle. */
11
+ lifecycle: Lifecycle;
12
+ /** Spine position: does this node have a manager? (= parent !== null). */
13
+ hasManager: boolean;
14
+ /** The node's working directory on disk. NodeMeta.cwd. */
15
+ cwd: string;
16
+ /** DERIVED: the scope scope.ts resolves for `cwd` — `project` when a
17
+ * nearest-ancestor `.crouter/` exists at/above cwd, else `user`. */
18
+ scope: 'user' | 'project';
19
+ /** DERIVED orchestration metrics. */
20
+ orchestration: {
21
+ /** Spine distance (hops) from this node up to its root orchestrator. The
22
+ * root itself is 0; a direct child of the root is 1; etc. */
23
+ depth: number;
24
+ };
25
+ }
26
+ /** The scope a cwd resolves into: `project` when scope.ts finds a
27
+ * nearest-ancestor `.crouter/` at/above it, else `user`. Reuses the existing
28
+ * resolver (`projectScopeRoot`). NOTE: `findProjectScopeRoot` is process-cached
29
+ * on first call, so for a node assembling its OWN subject (cwd === process.cwd)
30
+ * this is exact; resolving an arbitrary unrelated cwd in the same process would
31
+ * return the cached root — fine for per-node subject assembly. */
32
+ export declare function scopeForCwd(cwd: string): 'user' | 'project';
33
+ /** Spine distance from `nodeId` up to its root orchestrator: count the hops
34
+ * walking the `parent` chain (canvas-db rows) until a row with no parent (the
35
+ * root). The root is depth 0. Cycle-guarded (parents must not cycle, but never
36
+ * loop forever) — mirrors `rootOfSpine` in runtime/placement.ts. */
37
+ export declare function spineDepth(nodeId: string): number;
38
+ /** Assemble the node-config subject for `nodeId` from its `NodeMeta` + the two
39
+ * derived fields. Returns `null` when the node has no canvas-db row (an
40
+ * unknown id). Pure aside from the canvas-db reads. */
41
+ export declare function assembleNodeSubject(nodeId: string): NodeConfigSubject | null;
@@ -0,0 +1,54 @@
1
+ // subject.ts — the node-config SUBJECT a document's `gate` predicate is
2
+ // evaluated against. Assembled per node from its canvas-db `NodeMeta` plus two
3
+ // DERIVED fields: `scope` (the scope.ts resolution for the node's cwd) and
4
+ // `orchestration.depth` (the node's spine distance to its root orchestrator).
5
+ // See design-substrate.md §4 + plan-substrate.md §2. Reads canvas-db; otherwise
6
+ // a thin, pure assembler.
7
+ import { getNode, getRow } from '../canvas/index.js';
8
+ import { projectScopeRoot } from '../scope.js';
9
+ /** The scope a cwd resolves into: `project` when scope.ts finds a
10
+ * nearest-ancestor `.crouter/` at/above it, else `user`. Reuses the existing
11
+ * resolver (`projectScopeRoot`). NOTE: `findProjectScopeRoot` is process-cached
12
+ * on first call, so for a node assembling its OWN subject (cwd === process.cwd)
13
+ * this is exact; resolving an arbitrary unrelated cwd in the same process would
14
+ * return the cached root — fine for per-node subject assembly. */
15
+ export function scopeForCwd(cwd) {
16
+ return projectScopeRoot(cwd) !== null ? 'project' : 'user';
17
+ }
18
+ /** Spine distance from `nodeId` up to its root orchestrator: count the hops
19
+ * walking the `parent` chain (canvas-db rows) until a row with no parent (the
20
+ * root). The root is depth 0. Cycle-guarded (parents must not cycle, but never
21
+ * loop forever) — mirrors `rootOfSpine` in runtime/placement.ts. */
22
+ export function spineDepth(nodeId) {
23
+ let cur = nodeId;
24
+ const seen = new Set();
25
+ let hops = 0;
26
+ for (;;) {
27
+ if (seen.has(cur))
28
+ break;
29
+ seen.add(cur);
30
+ const row = getRow(cur);
31
+ if (row === null || row.parent == null)
32
+ break;
33
+ cur = row.parent;
34
+ hops++;
35
+ }
36
+ return hops;
37
+ }
38
+ /** Assemble the node-config subject for `nodeId` from its `NodeMeta` + the two
39
+ * derived fields. Returns `null` when the node has no canvas-db row (an
40
+ * unknown id). Pure aside from the canvas-db reads. */
41
+ export function assembleNodeSubject(nodeId) {
42
+ const meta = getNode(nodeId);
43
+ if (meta === null)
44
+ return null;
45
+ return {
46
+ kind: meta.kind,
47
+ mode: meta.mode,
48
+ lifecycle: meta.lifecycle,
49
+ hasManager: (meta.parent ?? null) !== null,
50
+ cwd: meta.cwd,
51
+ scope: scopeForCwd(meta.cwd),
52
+ orchestration: { depth: spineDepth(nodeId) },
53
+ };
54
+ }