@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,180 @@
1
+ import { defineLeaf } from '../../core/command.js';
2
+ import { usage } from '../../core/errors.js';
3
+ import { listAllMemoryDocs } from '../../core/memory-resolver.js';
4
+ import { parseSubstrateDoc } from '../../core/substrate/index.js';
5
+ import { listAllSkills } from '../../core/resolver.js';
6
+ import { parseFrontmatter } from '../../core/frontmatter.js';
7
+ import { readText } from '../../core/fs-utils.js';
8
+ import { MEMORY_KINDS } from './shared.js';
9
+ function substrateUnit(d) {
10
+ return {
11
+ name: d.name,
12
+ kind: d.kind,
13
+ scope: d.scope,
14
+ path: d.path,
15
+ when: d.when,
16
+ why: d.why,
17
+ shortForm: d.shortForm,
18
+ loadBody: () => d.body,
19
+ };
20
+ }
21
+ function skillUnit(s) {
22
+ const desc = s.frontmatter.description ?? '';
23
+ const keywords = s.frontmatter.keywords ? s.frontmatter.keywords.join(' ') : '';
24
+ return {
25
+ name: s.name,
26
+ kind: 'skill',
27
+ scope: s.scope,
28
+ path: s.path,
29
+ // A skill's description plays the when+why role; keywords ride along so the
30
+ // ranker weighs them too. short_form surfaces the description as the hook.
31
+ when: [desc, keywords].filter((x) => x).join(' '),
32
+ why: '',
33
+ shortForm: desc,
34
+ loadBody: () => parseFrontmatter(readText(s.path)).body,
35
+ };
36
+ }
37
+ /** The unioned candidate set: every substrate memory document + every skill,
38
+ * optionally narrowed to one kind. find searches EVERYTHING — it never applies
39
+ * gate or visibility-rung filtering (design §11#3). The skill half is wrapped
40
+ * defensively: the skill corpus is parsed by the resolver, which currently
41
+ * hard-throws on at least one shipped skill's frontmatter (a pre-existing
42
+ * regression in the yaml-parser swap, owned by the frontmatter/skill track). A
43
+ * broken skill corpus must degrade the union to substrate-docs-only, never
44
+ * crash `memory find`.
45
+ *
46
+ * Dedup by (scope, name): the substrate and skill-plugin corpora overlap
47
+ * (builtin-memory/ contains migrated copies of builtin-skills/; a skill name
48
+ * can also appear in multiple installed plugins at the same scope). The FIRST
49
+ * unit encountered for a given identity wins — substrate docs before skill-plugin
50
+ * docs, which mirrors the memory resolver's own precedence. */
51
+ function candidates(kindFilter) {
52
+ const seen = new Set(); // "scope/name" → first wins
53
+ const add = (u) => {
54
+ const id = `${u.scope}/${u.name}`;
55
+ if (seen.has(id))
56
+ return;
57
+ seen.add(id);
58
+ units.push(u);
59
+ };
60
+ const units = [];
61
+ for (const doc of listAllMemoryDocs()) {
62
+ const sub = parseSubstrateDoc(doc);
63
+ if (sub === null)
64
+ continue;
65
+ if (kindFilter !== undefined && sub.kind !== kindFilter)
66
+ continue;
67
+ add(substrateUnit(sub));
68
+ }
69
+ if (kindFilter === undefined || kindFilter === 'skill') {
70
+ try {
71
+ for (const skill of listAllSkills())
72
+ add(skillUnit(skill));
73
+ }
74
+ catch {
75
+ /* skill corpus unavailable — search substrate documents alone */
76
+ }
77
+ }
78
+ return units;
79
+ }
80
+ export const findLeaf = defineLeaf({
81
+ name: 'find',
82
+ description: 'relevance search across memory documents',
83
+ whenToUse: 'you do not yet know which document applies and need to discover what is stored — ranks documents by relevance, weighted over name, when, why, and short-form (add --body to also weigh body text). Searches the full scope set regardless of any visibility gate. Use --grep instead when you need an exact regex or literal-string match across document bodies rather than a ranked topic match.',
84
+ help: {
85
+ name: 'memory find',
86
+ summary: 'relevance search across memory documents, weighted over name/when/why/short-form (and body with --body)',
87
+ params: [
88
+ { kind: 'positional', name: 'query', required: true, constraint: 'With ranked search (default): whitespace-separated terms, matched case-insensitively and weighted over name, when, why, and short-form (plus body with --body); documents matching more/stronger fields rank higher. With --grep: an ECMAScript regex applied to each document body line.' },
89
+ { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Filter to a single kind. Default: all kinds.' },
90
+ { kind: 'flag', name: 'grep', type: 'bool', required: false, constraint: 'Treat the query as an ECMAScript regex and match it against document bodies, instead of weighted relevance ranking. Mutually exclusive with --body.' },
91
+ { kind: 'flag', name: 'body', type: 'bool', required: false, constraint: 'Also weigh document body text in the relevance ranking (in addition to name/when/why/short-form). Ignored under --grep, which always scans bodies.' },
92
+ ],
93
+ output: [
94
+ { name: 'query', type: 'string', required: true, constraint: 'Echo of the input query.' },
95
+ { name: 'hits', type: 'object[]', required: true, constraint: 'Ranked mode — each: {name, kind, scope, score, short_form}, sorted by score descending. Under --grep instead — each: {path, line, text} for every body line matching the regex, sorted by path then line ascending.' },
96
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands for reading a hit in full or refining the search.' },
97
+ ],
98
+ outputKind: 'object',
99
+ effects: ['None. Read-only.'],
100
+ },
101
+ run: async (input) => {
102
+ const query = input['query'];
103
+ const kindFilter = input['kind'];
104
+ const grep = input['grep'];
105
+ const weighBody = input['body'];
106
+ if (grep && weighBody) {
107
+ throw usage('--grep and --body are mutually exclusive (--grep always scans bodies).');
108
+ }
109
+ const units = candidates(kindFilter);
110
+ // --- grep mode: regex over every unit's body, one row per matching line ---
111
+ if (grep) {
112
+ let regex;
113
+ try {
114
+ regex = new RegExp(query);
115
+ }
116
+ catch {
117
+ throw usage(`invalid regex pattern: ${query}`);
118
+ }
119
+ const matches = [];
120
+ for (const u of units) {
121
+ const lines = u.loadBody().split('\n');
122
+ lines.forEach((text, idx) => {
123
+ if (regex.test(text))
124
+ matches.push({ path: u.path, line: idx + 1, text });
125
+ });
126
+ }
127
+ matches.sort((a, b) => {
128
+ const pc = a.path.localeCompare(b.path);
129
+ return pc !== 0 ? pc : a.line - b.line;
130
+ });
131
+ return {
132
+ query,
133
+ hits: matches,
134
+ follow_up: 'Read a document in full with `crtr memory read <name>`. Drop --grep for a ranked topic search.',
135
+ };
136
+ }
137
+ // --- ranked mode: weighted relevance over name/when/why/short-form(+body) ---
138
+ const terms = query
139
+ .toLowerCase()
140
+ .split(/\s+/)
141
+ .filter((t) => t.length > 0);
142
+ if (terms.length === 0)
143
+ throw usage('query must contain at least one non-whitespace term');
144
+ const hits = [];
145
+ for (const u of units) {
146
+ const nameLc = u.name.toLowerCase();
147
+ const whenLc = u.when.toLowerCase();
148
+ const whyLc = u.why.toLowerCase();
149
+ const shortLc = u.shortForm.toLowerCase();
150
+ const bodyLc = weighBody ? u.loadBody().toLowerCase() : null;
151
+ let score = 0;
152
+ for (const term of terms) {
153
+ if (nameLc.includes(term))
154
+ score += 10;
155
+ if (whenLc.includes(term))
156
+ score += 5;
157
+ if (whyLc.includes(term))
158
+ score += 4;
159
+ if (shortLc.includes(term))
160
+ score += 3;
161
+ if (bodyLc !== null && bodyLc.includes(term))
162
+ score += 1;
163
+ }
164
+ if (score > 0)
165
+ hits.push({ unit: u, score });
166
+ }
167
+ hits.sort((a, b) => b.score - a.score || a.unit.name.localeCompare(b.unit.name));
168
+ return {
169
+ query,
170
+ hits: hits.map((h) => ({
171
+ name: h.unit.name,
172
+ kind: h.unit.kind,
173
+ scope: h.unit.scope,
174
+ score: h.score,
175
+ short_form: h.unit.shortForm,
176
+ })),
177
+ follow_up: 'Read a hit in full with `crtr memory read <name>`. Add --body to weigh body text, or --grep for an exact regex.',
178
+ };
179
+ },
180
+ });
@@ -0,0 +1 @@
1
+ export declare const lintLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,140 @@
1
+ // `crtr memory lint` — the permanent valid-YAML gate over the bounded corpus
2
+ // (the CTO green-checkpoint: zero frontmatter parse errors at authoring time,
3
+ // so an invalid doc fails HERE instead of being silently isolated at runtime).
4
+ // Bounded corpus = the substrate memory dirs (project/user/builtin) +
5
+ // every installed plugin's skills tree + ~/.claude/skills. Never a
6
+ // filesystem-wide scan.
7
+ import { homedir } from 'node:os';
8
+ import { basename, join, relative, sep } from 'node:path';
9
+ import { defineLeaf } from '../../core/command.js';
10
+ import { general } from '../../core/errors.js';
11
+ import { warn } from '../../core/output.js';
12
+ import { pathExists, readText, walkFiles } from '../../core/fs-utils.js';
13
+ import { parseFrontmatterGeneric } from '../../core/frontmatter.js';
14
+ import { listAllPlugins } from '../../core/resolver.js';
15
+ import { scopeMemoryDir } from '../../core/scope.js';
16
+ import { isDocKind, RUNGS } from '../../core/substrate/index.js';
17
+ import { SKILL_ENTRY_FILE, SKILLS_DIR } from '../../types.js';
18
+ /** The parser normalizes the `always` alias to `content`, so it lints valid. */
19
+ const VALID_RUNGS = [...RUNGS, 'always'];
20
+ const RUNG_FIELDS = ['system-prompt-visibility', 'file-read-visibility'];
21
+ /** Schema checks for a doc living in a substrate memory dir: a memory store
22
+ * holds ONLY substrate docs, so a missing/invalid `kind` is an authoring
23
+ * error here (elsewhere it just means "not a substrate doc"). Rung and gate
24
+ * values are checked RAW — the runtime parser silently falls back to kind
25
+ * defaults / inert gates, which is exactly the silent tolerance this lint
26
+ * exists to catch at authoring time. */
27
+ function lintSubstrateSchema(fm) {
28
+ if (fm === null)
29
+ return 'missing frontmatter: a memory store doc requires `kind: skill|reference|preference`';
30
+ if (!isDocKind(fm.kind)) {
31
+ return `invalid kind: ${JSON.stringify(fm.kind)} (expected skill|reference|preference)`;
32
+ }
33
+ for (const field of RUNG_FIELDS) {
34
+ const v = fm[field];
35
+ if (v !== undefined && (typeof v !== 'string' || !VALID_RUNGS.includes(v))) {
36
+ return `invalid ${field}: ${JSON.stringify(v)} (expected ${RUNGS.join('|')})`;
37
+ }
38
+ }
39
+ const gate = fm.gate;
40
+ if (gate !== undefined && (gate === null || typeof gate !== 'object' || Array.isArray(gate))) {
41
+ return `invalid gate: ${JSON.stringify(gate)} (expected a field→matcher object)`;
42
+ }
43
+ const appliesTo = fm['applies-to'];
44
+ if (appliesTo !== undefined &&
45
+ typeof appliesTo !== 'string' &&
46
+ !(Array.isArray(appliesTo) && appliesTo.every((g) => typeof g === 'string'))) {
47
+ return `invalid applies-to: ${JSON.stringify(appliesTo)} (expected a glob or glob list)`;
48
+ }
49
+ return null;
50
+ }
51
+ /** Strict-parse one file; push a finding on a YAML error, then run the
52
+ * schema check when the file lives in a substrate store. */
53
+ function lintFile(file, substrateStore, findings) {
54
+ let fm;
55
+ try {
56
+ fm = parseFrontmatterGeneric(readText(file)).data;
57
+ }
58
+ catch (e) {
59
+ const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
60
+ findings.push({ path: file, error: `invalid YAML frontmatter: ${msg}` });
61
+ return;
62
+ }
63
+ if (!substrateStore)
64
+ return;
65
+ const schemaError = lintSubstrateSchema(fm);
66
+ if (schemaError !== null)
67
+ findings.push({ path: file, error: schemaError });
68
+ }
69
+ export const lintLeaf = defineLeaf({
70
+ name: 'lint',
71
+ description: 'validate frontmatter across the whole bounded document corpus',
72
+ whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (memory stores, plugin skills, ~/.claude/skills) and fail loudly on any invalid YAML or substrate schema violation. Run it before shipping doc changes; CI-friendly (non-zero exit on any finding).',
73
+ help: {
74
+ name: 'memory lint',
75
+ summary: 'strict-parse frontmatter across the bounded corpus; non-zero exit on any finding',
76
+ params: [],
77
+ output: [
78
+ { name: 'checked', type: 'number', required: true, constraint: 'Files linted across all corpora.' },
79
+ { name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus file counts: {memory_stores, plugin_skills, claude_skills}.' },
80
+ { name: 'findings', type: 'object[]', required: true, constraint: 'One row per failure: {path, error}. Empty when green.' },
81
+ ],
82
+ outputKind: 'object',
83
+ effects: ['None. Read-only. Exits non-zero when any finding exists.'],
84
+ },
85
+ run: async () => {
86
+ const findings = [];
87
+ let memoryCount = 0;
88
+ let pluginCount = 0;
89
+ let claudeCount = 0;
90
+ // 1. Substrate memory stores (project/user/builtin), schema-aware.
91
+ // MEMORY.md index files are not substrate docs — YAML-parse only.
92
+ for (const scope of ['project', 'user', 'builtin']) {
93
+ const dir = scopeMemoryDir(scope);
94
+ if (!dir || !pathExists(dir))
95
+ continue;
96
+ for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
97
+ if (!relative(dir, file).split(sep).join('/'))
98
+ continue;
99
+ memoryCount += 1;
100
+ lintFile(file, basename(file) !== 'MEMORY.md', findings);
101
+ }
102
+ }
103
+ // 2. Every installed plugin's skills tree (builtin + plugin + marketplace).
104
+ for (const plugin of listAllPlugins()) {
105
+ const skillsRoot = join(plugin.root, SKILLS_DIR);
106
+ if (!pathExists(skillsRoot))
107
+ continue;
108
+ for (const file of walkFiles(skillsRoot, (n) => n === SKILL_ENTRY_FILE)) {
109
+ pluginCount += 1;
110
+ lintFile(file, false, findings);
111
+ }
112
+ }
113
+ // 3. ~/.claude/skills — read by the runtime, so it lints too.
114
+ const claudeSkills = join(homedir(), '.claude', SKILLS_DIR);
115
+ if (pathExists(claudeSkills)) {
116
+ for (const file of walkFiles(claudeSkills, (n) => n === SKILL_ENTRY_FILE)) {
117
+ claudeCount += 1;
118
+ lintFile(file, false, findings);
119
+ }
120
+ }
121
+ const checked = memoryCount + pluginCount + claudeCount;
122
+ if (findings.length > 0) {
123
+ // Human/agent path renders only the message — surface every offender as
124
+ // a scoped stderr notice (the --json path carries them in details too).
125
+ for (const f of findings)
126
+ warn(`memory lint: ${f.path}: ${f.error}`);
127
+ throw general(`memory lint: ${findings.length} finding(s) across ${checked} files`, {
128
+ checked,
129
+ findings: findings.map((f) => ({ path: f.path, error: f.error })),
130
+ next: 'Fix each doc (quote YAML values containing `: `; use a valid kind/rung/gate), then re-run `crtr memory lint`.',
131
+ });
132
+ }
133
+ return {
134
+ checked,
135
+ corpora: { memory_stores: memoryCount, plugin_skills: pluginCount, claude_skills: claudeCount },
136
+ findings: [],
137
+ follow_up: 'Corpus green — zero invalid frontmatter docs.',
138
+ };
139
+ },
140
+ });
@@ -0,0 +1 @@
1
+ export declare const listLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,79 @@
1
+ import { defineLeaf } from '../../core/command.js';
2
+ import { listAllMemoryDocs } from '../../core/memory-resolver.js';
3
+ import { parseSubstrateDoc } from '../../core/substrate/index.js';
4
+ import { listAllSkills } from '../../core/resolver.js';
5
+ import { MEMORY_KINDS, MEMORY_SCOPES, scopeRank } from './shared.js';
6
+ export const listLeaf = defineLeaf({
7
+ name: 'list',
8
+ description: 'inventory of stored memory documents',
9
+ whenToUse: 'browse everything stored — one line per document with its title, short-form hook, kind, and scope. This is the one surface that shows short-form. Reach for `crtr memory find` instead when you already have a topic or keyword in mind rather than wanting the whole inventory.',
10
+ help: {
11
+ name: 'memory list',
12
+ summary: 'inventory every memory document across the resolved scopes, one line each',
13
+ params: [
14
+ { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Filter to a single kind. Default: all kinds.' },
15
+ { kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Filter to a single scope. Default: all resolved scopes.' },
16
+ ],
17
+ output: [
18
+ { name: 'items', type: 'object[]', required: true, constraint: 'One row per document. Each: {name, title, short_form, kind, scope}. short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
19
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read a document in full or narrow the inventory.' },
20
+ ],
21
+ outputKind: 'object',
22
+ effects: ['None. Read-only.'],
23
+ },
24
+ run: async (input) => {
25
+ const kindFilter = input['kind'];
26
+ const scopeFilter = input['scope'];
27
+ const seen = new Set(); // "scope/name" → first wins
28
+ const items = [];
29
+ const addItem = (item) => {
30
+ const id = `${item.scope}/${item.name}`;
31
+ if (seen.has(id))
32
+ return;
33
+ seen.add(id);
34
+ items.push(item);
35
+ };
36
+ // Substrate memory docs (project > user > builtin precedence).
37
+ for (const doc of listAllMemoryDocs(scopeFilter)) {
38
+ const sub = parseSubstrateDoc(doc);
39
+ if (sub === null)
40
+ continue;
41
+ if (kindFilter !== undefined && sub.kind !== kindFilter)
42
+ continue;
43
+ addItem({ name: sub.name, kind: sub.kind, scope: sub.scope, shortForm: sub.shortForm });
44
+ }
45
+ // Skill-plugin corpus (scope-root skills + plugin/marketplace skills).
46
+ // Only include when the kindFilter is absent or explicitly 'skill'.
47
+ if (kindFilter === undefined || kindFilter === 'skill') {
48
+ try {
49
+ for (const skill of listAllSkills(scopeFilter)) {
50
+ const raw = skill.frontmatter.description;
51
+ const desc = typeof raw === 'string' ? raw : '';
52
+ addItem({ name: skill.name, kind: 'skill', scope: skill.scope, shortForm: desc });
53
+ }
54
+ }
55
+ catch {
56
+ /* skill corpus unavailable — list substrate documents alone */
57
+ }
58
+ }
59
+ items.sort((a, b) => {
60
+ const sr = scopeRank(a.scope) - scopeRank(b.scope);
61
+ if (sr !== 0)
62
+ return sr;
63
+ const kc = a.kind.localeCompare(b.kind);
64
+ if (kc !== 0)
65
+ return kc;
66
+ return a.name.localeCompare(b.name);
67
+ });
68
+ return {
69
+ items: items.map((d) => ({
70
+ name: d.name,
71
+ title: d.name,
72
+ short_form: d.shortForm,
73
+ kind: d.kind,
74
+ scope: d.scope,
75
+ })),
76
+ follow_up: 'Read one in full with `crtr memory read <name>`. Narrow with --kind / --scope, or search a topic with `crtr memory find <query>`.',
77
+ };
78
+ },
79
+ });
@@ -0,0 +1,103 @@
1
+ import { defineLeaf } from '../../core/command.js';
2
+ import { CrtrError, notFound } from '../../core/errors.js';
3
+ import { resolveMemoryDoc } from '../../core/memory-resolver.js';
4
+ import { parseSubstrateDoc } from '../../core/substrate/index.js';
5
+ import { resolveSkill } from '../../core/resolver.js';
6
+ import { parseFrontmatter } from '../../core/frontmatter.js';
7
+ import { readText } from '../../core/fs-utils.js';
8
+ import { MEMORY_KINDS } from './shared.js';
9
+ export const readLeaf = defineLeaf({
10
+ name: 'read',
11
+ description: 'load a memory document body by name',
12
+ whenToUse: 'a task in front of you matches a stored document and you already know its name — read it before improvising. Resolves the path-derived name across scopes by precedence (project > user > builtin), with leaf-name fallback like skills. You name the document by its crtr identifier, never a file path — do not cat or find the markdown off disk. Reach for `crtr memory find` first when you do not yet know which document applies.',
13
+ help: {
14
+ name: 'memory read',
15
+ summary: 'resolve a path-derived name to its document body, frontmatter stripped unless --frontmatter',
16
+ params: [
17
+ { kind: 'positional', name: 'name', required: true, constraint: 'Path-derived memory identifier (e.g. `topic` or `area/topic`). Resolved across scopes by precedence: project > user > builtin, with leaf-name fallback.' },
18
+ { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Narrows resolution when the name is ambiguous across kinds.' },
19
+ { kind: 'flag', name: 'frontmatter', type: 'bool', required: false, constraint: 'When present, includes the YAML frontmatter in the returned body. Off by default — only the body is returned.' },
20
+ ],
21
+ output: [
22
+ { name: 'name', type: 'string', required: true, constraint: 'Resolved document name.' },
23
+ { name: 'kind', type: 'string', required: true, constraint: 'Resolved kind: skill, reference, or preference.' },
24
+ { name: 'scope', type: 'string', required: true, constraint: 'Scope the document was resolved from: project, user, or builtin.' },
25
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the document on disk.' },
26
+ { name: 'content', type: 'string', required: true, constraint: 'Document body. Frontmatter stripped unless --frontmatter is set.' },
27
+ { name: 'follow_up', type: 'string', required: true, constraint: 'Hints at variant flags or next commands.' },
28
+ ],
29
+ outputKind: 'object',
30
+ effects: ['None. Read-only.'],
31
+ },
32
+ run: async (input) => {
33
+ const nameRaw = input['name'];
34
+ const kindFilter = input['kind'];
35
+ const includeFrontmatter = input['frontmatter'];
36
+ // 1) Primary: resolve a substrate/memory document (memory/ dirs across
37
+ // project>user>builtin). A not_found here is not fatal — it falls through
38
+ // to the skill resolver below (the D4 union).
39
+ let doc;
40
+ try {
41
+ doc = resolveMemoryDoc(nameRaw);
42
+ }
43
+ catch (e) {
44
+ if (!(e instanceof CrtrError && e.code === 'not_found'))
45
+ throw e;
46
+ }
47
+ if (doc !== undefined) {
48
+ const sub = parseSubstrateDoc(doc);
49
+ const kind = sub !== null
50
+ ? sub.kind
51
+ : typeof doc.frontmatter?.['kind'] === 'string'
52
+ ? doc.frontmatter['kind']
53
+ : 'reference';
54
+ // --kind asserts the resolved kind; a mismatch falls through to skills
55
+ // (which only satisfy --kind=skill).
56
+ if (kindFilter === undefined || kind === kindFilter) {
57
+ const content = includeFrontmatter ? readText(doc.path) : doc.body;
58
+ return {
59
+ name: doc.name,
60
+ kind,
61
+ scope: doc.scope,
62
+ path: doc.path,
63
+ content,
64
+ follow_up: 'Add --frontmatter to include the YAML header. Browse the inventory with `crtr memory list`.',
65
+ };
66
+ }
67
+ }
68
+ // 2) D4 union fallthrough: the name is not a memory document (or its kind
69
+ // did not match --kind) — resolve it as a plugin/marketplace/scope skill
70
+ // so skills are readable through `crtr memory read` too. Skills are kind
71
+ // "skill", so honor --kind by skipping this path for other kinds.
72
+ if (kindFilter === undefined || kindFilter === 'skill') {
73
+ let skill;
74
+ try {
75
+ skill = resolveSkill(nameRaw);
76
+ }
77
+ catch (e) {
78
+ // not_found → fall through to the memory-not-found below. A genuine
79
+ // CrtrError (e.g. ambiguous) propagates. Any other throw means the skill
80
+ // corpus is currently unparseable (the yaml-parser regression owned by
81
+ // the frontmatter/skill track) — degrade to not-found rather than 500.
82
+ if (e instanceof CrtrError && e.code !== 'not_found')
83
+ throw e;
84
+ }
85
+ if (skill !== undefined) {
86
+ const raw = readText(skill.path);
87
+ const content = includeFrontmatter ? raw : parseFrontmatter(raw).body;
88
+ return {
89
+ name: skill.name,
90
+ kind: 'skill',
91
+ scope: skill.scope,
92
+ path: skill.path,
93
+ content,
94
+ follow_up: 'Add --frontmatter to include the YAML header. Browse the inventory with `crtr memory list`.',
95
+ };
96
+ }
97
+ }
98
+ throw notFound(`memory document not found: ${nameRaw}`, {
99
+ memory: nameRaw,
100
+ next: 'Run `crtr memory find <query>` to discover documents, or `crtr memory list` to browse the inventory.',
101
+ });
102
+ },
103
+ });
@@ -0,0 +1,30 @@
1
+ import type { Scope } from '../../types.js';
2
+ export declare const MEMORY_KINDS: readonly ["skill", "reference", "preference"];
3
+ export declare const VISIBILITY_RUNGS: readonly ["none", "name", "preview", "content"];
4
+ export declare const MEMORY_SCOPES: readonly ["user", "project"];
5
+ /** Scope sort weight matching resolution precedence (project > user > builtin).
6
+ * Used by `list` for its "scope then kind then name" ordering. */
7
+ export declare function scopeRank(scope: Scope): number;
8
+ /** Resolve the write target scope + its memory dir. Default: project when a
9
+ * project scope exists for the cwd, else user. An explicit `--scope project`
10
+ * with no project scope yet scaffolds one (ensureProjectScopeRoot). User scope
11
+ * always resolves. Returns the absolute `<root>/memory` dir to write under. */
12
+ export declare function resolveWriteTarget(scopeArg: string | undefined): {
13
+ scope: Scope;
14
+ memoryDir: string;
15
+ };
16
+ /** Map a path-derived name (`topic` or `area/topic`) to its file path under a
17
+ * memory dir, guarding against traversal/absolute escapes. */
18
+ export declare function memoryFilePath(memoryDir: string, name: string): string;
19
+ export declare function coerceGate(raw: string): Record<string, unknown>;
20
+ /** Coerce a `--applies-to` string to the schema's glob form: a comma-separated
21
+ * list becomes an array, a single glob stays a string. */
22
+ export declare function coerceAppliesTo(raw: string): unknown;
23
+ /** Serialize a substrate frontmatter record + body into a complete `.md`
24
+ * document. Frontmatter is emitted as a `---` fenced YAML block (the `yaml`
25
+ * package — the same one the parser uses — so nested gate maps and applies-to
26
+ * arrays round-trip), in canonical field order with preserved extras last. The
27
+ * skill-shaped `serializeFrontmatter` in core can only represent
28
+ * name/description/type/keywords, so it cannot carry the substrate fields —
29
+ * hence this focused serializer. */
30
+ export declare function serializeMemoryDoc(frontmatter: Record<string, unknown>, body: string): string;
@@ -0,0 +1,122 @@
1
+ // Shared constants + helpers for the `crtr memory` command family (task B2).
2
+ // The leaf handlers (list/read/find/write) consume the resolver, substrate
3
+ // schema, scope, and skill-resolver modules and build their documented output
4
+ // objects on top of the small helpers here. Nothing in this file forks on kind
5
+ // or re-implements the schema/gate/resolver — it only composes them.
6
+ import { join } from 'node:path';
7
+ import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
8
+ import { usage } from '../../core/errors.js';
9
+ import { scopeMemoryDir, projectScopeRoot, ensureProjectScopeRoot, } from '../../core/scope.js';
10
+ // The three memory kinds — procedural (skill), referential (reference),
11
+ // preferential (preference). Used as the `--kind` enum choices everywhere.
12
+ export const MEMORY_KINDS = ['skill', 'reference', 'preference'];
13
+ // Visibility rungs — how much of a document surfaces (none → name → preview →
14
+ // content). Shared by --system-prompt-visibility and --file-read-visibility.
15
+ export const VISIBILITY_RUNGS = ['none', 'name', 'preview', 'content'];
16
+ // Scope choices for filtering / targeting (builtin is read-only, not writable).
17
+ export const MEMORY_SCOPES = ['user', 'project'];
18
+ /** Scope sort weight matching resolution precedence (project > user > builtin).
19
+ * Used by `list` for its "scope then kind then name" ordering. */
20
+ export function scopeRank(scope) {
21
+ return scope === 'project' ? 0 : scope === 'user' ? 1 : 2;
22
+ }
23
+ /** Resolve the write target scope + its memory dir. Default: project when a
24
+ * project scope exists for the cwd, else user. An explicit `--scope project`
25
+ * with no project scope yet scaffolds one (ensureProjectScopeRoot). User scope
26
+ * always resolves. Returns the absolute `<root>/memory` dir to write under. */
27
+ export function resolveWriteTarget(scopeArg) {
28
+ let scope;
29
+ if (scopeArg === 'user' || scopeArg === 'project') {
30
+ scope = scopeArg;
31
+ }
32
+ else if (scopeArg !== undefined) {
33
+ throw usage(`invalid --scope: ${scopeArg} (expected user|project)`);
34
+ }
35
+ else {
36
+ scope = projectScopeRoot() !== null ? 'project' : 'user';
37
+ }
38
+ let memoryDir = scopeMemoryDir(scope);
39
+ if (!memoryDir && scope === 'project') {
40
+ // Explicit --scope project with no project root yet → scaffold it.
41
+ memoryDir = join(ensureProjectScopeRoot(), 'memory');
42
+ }
43
+ if (!memoryDir)
44
+ throw usage(`no ${scope} scope available for writing memory documents`);
45
+ return { scope, memoryDir };
46
+ }
47
+ /** Map a path-derived name (`topic` or `area/topic`) to its file path under a
48
+ * memory dir, guarding against traversal/absolute escapes. */
49
+ export function memoryFilePath(memoryDir, name) {
50
+ const segments = name.split('/').filter((s) => s.length > 0);
51
+ if (segments.length === 0)
52
+ throw usage('memory document name required');
53
+ if (segments.some((s) => s === '.' || s === '..')) {
54
+ throw usage(`invalid memory document name: ${name}`);
55
+ }
56
+ return join(memoryDir, ...segments) + '.md';
57
+ }
58
+ /** Coerce a `--gate` string into a predicate tree. The gate field MUST be a
59
+ * YAML/JSON object (the field→matcher map the schema expects). Throws a usage
60
+ * error when the input fails to parse or does not parse to a non-null,
61
+ * non-array object — a scalar or array gate silently makes a doc never-eligible
62
+ * (the matcher engine ignores non-object predicates), so passing one is always
63
+ * a mistake and must be caught at authoring time rather than stored. */
64
+ function parseYamlObject(raw) {
65
+ const parsed = yamlParse(raw);
66
+ if (parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)) {
67
+ return parsed;
68
+ }
69
+ return `expected an object, got ${JSON.stringify(parsed)}`;
70
+ }
71
+ export function coerceGate(raw) {
72
+ const result = parseYamlObject(raw);
73
+ if (typeof result === 'string') {
74
+ throw usage(`--gate must be a YAML/JSON object (field→matcher map): ${result}. ` +
75
+ `Example: --gate '{kind: design}' or --gate '{orchestration.depth: {gte: 2}}'`);
76
+ }
77
+ return result;
78
+ }
79
+ /** Coerce a `--applies-to` string to the schema's glob form: a comma-separated
80
+ * list becomes an array, a single glob stays a string. */
81
+ export function coerceAppliesTo(raw) {
82
+ const parts = raw
83
+ .split(',')
84
+ .map((s) => s.trim())
85
+ .filter((s) => s.length > 0);
86
+ if (parts.length === 0)
87
+ return raw;
88
+ return parts.length === 1 ? parts[0] : parts;
89
+ }
90
+ // Canonical frontmatter field order for a substrate document. Known fields come
91
+ // first in this order; any preserved-on-update extras append after.
92
+ const FRONTMATTER_ORDER = [
93
+ 'kind',
94
+ 'when',
95
+ 'why',
96
+ 'short-form',
97
+ 'system-prompt-visibility',
98
+ 'file-read-visibility',
99
+ 'gate',
100
+ 'applies-to',
101
+ ];
102
+ /** Serialize a substrate frontmatter record + body into a complete `.md`
103
+ * document. Frontmatter is emitted as a `---` fenced YAML block (the `yaml`
104
+ * package — the same one the parser uses — so nested gate maps and applies-to
105
+ * arrays round-trip), in canonical field order with preserved extras last. The
106
+ * skill-shaped `serializeFrontmatter` in core can only represent
107
+ * name/description/type/keywords, so it cannot carry the substrate fields —
108
+ * hence this focused serializer. */
109
+ export function serializeMemoryDoc(frontmatter, body) {
110
+ const ordered = {};
111
+ for (const key of FRONTMATTER_ORDER) {
112
+ if (frontmatter[key] !== undefined)
113
+ ordered[key] = frontmatter[key];
114
+ }
115
+ for (const key of Object.keys(frontmatter)) {
116
+ if (!(key in ordered) && frontmatter[key] !== undefined)
117
+ ordered[key] = frontmatter[key];
118
+ }
119
+ const yamlText = yamlStringify(ordered).replace(/\n+$/, '');
120
+ const cleanBody = body.replace(/^\n+/, '').replace(/\s+$/, '');
121
+ return `---\n${yamlText}\n---\n\n${cleanBody}\n`;
122
+ }