@soleri/core 9.14.4 → 9.16.7

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 (355) hide show
  1. package/data/flows/deliver.flow.yaml +11 -0
  2. package/data/flows/design.flow.yaml +4 -14
  3. package/data/flows/enhance.flow.yaml +10 -0
  4. package/data/flows/explore.flow.yaml +16 -0
  5. package/data/flows/fix.flow.yaml +1 -1
  6. package/data/flows/review.flow.yaml +13 -4
  7. package/dist/brain/brain.d.ts +9 -0
  8. package/dist/brain/brain.d.ts.map +1 -1
  9. package/dist/brain/brain.js +11 -1
  10. package/dist/brain/brain.js.map +1 -1
  11. package/dist/brain/intelligence.d.ts.map +1 -1
  12. package/dist/brain/intelligence.js +24 -0
  13. package/dist/brain/intelligence.js.map +1 -1
  14. package/dist/brain/types.d.ts +1 -0
  15. package/dist/brain/types.d.ts.map +1 -1
  16. package/dist/capabilities/chain-mapping.d.ts.map +1 -1
  17. package/dist/capabilities/chain-mapping.js +5 -4
  18. package/dist/capabilities/chain-mapping.js.map +1 -1
  19. package/dist/capabilities/registry.d.ts +6 -0
  20. package/dist/capabilities/registry.d.ts.map +1 -1
  21. package/dist/capabilities/registry.js +3 -2
  22. package/dist/capabilities/registry.js.map +1 -1
  23. package/dist/chat/chat-session.d.ts +6 -0
  24. package/dist/chat/chat-session.d.ts.map +1 -1
  25. package/dist/chat/chat-session.js +68 -17
  26. package/dist/chat/chat-session.js.map +1 -1
  27. package/dist/context/context-engine.js +1 -1
  28. package/dist/context/context-engine.js.map +1 -1
  29. package/dist/curator/curator.d.ts +6 -0
  30. package/dist/curator/curator.d.ts.map +1 -1
  31. package/dist/curator/curator.js +138 -0
  32. package/dist/curator/curator.js.map +1 -1
  33. package/dist/curator/types.d.ts +10 -0
  34. package/dist/curator/types.d.ts.map +1 -1
  35. package/dist/engine/bin/soleri-engine.js +0 -0
  36. package/dist/engine/core-ops.d.ts.map +1 -1
  37. package/dist/engine/core-ops.js +38 -1
  38. package/dist/engine/core-ops.js.map +1 -1
  39. package/dist/flows/epilogue.d.ts +5 -1
  40. package/dist/flows/epilogue.d.ts.map +1 -1
  41. package/dist/flows/epilogue.js +11 -3
  42. package/dist/flows/epilogue.js.map +1 -1
  43. package/dist/flows/executor.d.ts.map +1 -1
  44. package/dist/flows/executor.js +13 -5
  45. package/dist/flows/executor.js.map +1 -1
  46. package/dist/flows/index.d.ts +1 -2
  47. package/dist/flows/index.d.ts.map +1 -1
  48. package/dist/flows/index.js +1 -0
  49. package/dist/flows/index.js.map +1 -1
  50. package/dist/flows/plan-builder.d.ts +17 -1
  51. package/dist/flows/plan-builder.d.ts.map +1 -1
  52. package/dist/flows/plan-builder.js +67 -6
  53. package/dist/flows/plan-builder.js.map +1 -1
  54. package/dist/flows/probes.d.ts +1 -1
  55. package/dist/flows/probes.d.ts.map +1 -1
  56. package/dist/flows/probes.js +15 -3
  57. package/dist/flows/probes.js.map +1 -1
  58. package/dist/flows/types.d.ts +47 -20
  59. package/dist/flows/types.d.ts.map +1 -1
  60. package/dist/flows/types.js +6 -1
  61. package/dist/flows/types.js.map +1 -1
  62. package/dist/index.d.ts +10 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/index.js +9 -0
  65. package/dist/index.js.map +1 -1
  66. package/dist/intake/content-classifier.d.ts +10 -4
  67. package/dist/intake/content-classifier.d.ts.map +1 -1
  68. package/dist/intake/content-classifier.js +19 -5
  69. package/dist/intake/content-classifier.js.map +1 -1
  70. package/dist/intake/text-ingester.d.ts +18 -0
  71. package/dist/intake/text-ingester.d.ts.map +1 -1
  72. package/dist/intake/text-ingester.js +37 -13
  73. package/dist/intake/text-ingester.js.map +1 -1
  74. package/dist/packs/pack-installer.d.ts.map +1 -1
  75. package/dist/packs/pack-installer.js +28 -2
  76. package/dist/packs/pack-installer.js.map +1 -1
  77. package/dist/planning/planner-types.d.ts +2 -0
  78. package/dist/planning/planner-types.d.ts.map +1 -1
  79. package/dist/planning/planner.d.ts +4 -0
  80. package/dist/planning/planner.d.ts.map +1 -1
  81. package/dist/planning/planner.js +50 -4
  82. package/dist/planning/planner.js.map +1 -1
  83. package/dist/playbooks/playbook-executor.d.ts +10 -1
  84. package/dist/playbooks/playbook-executor.d.ts.map +1 -1
  85. package/dist/playbooks/playbook-executor.js +8 -2
  86. package/dist/playbooks/playbook-executor.js.map +1 -1
  87. package/dist/playbooks/playbook-types.d.ts +8 -0
  88. package/dist/playbooks/playbook-types.d.ts.map +1 -1
  89. package/dist/plugins/types.d.ts +2 -2
  90. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  91. package/dist/runtime/admin-extra-ops.js +30 -0
  92. package/dist/runtime/admin-extra-ops.js.map +1 -1
  93. package/dist/runtime/admin-ops.d.ts.map +1 -1
  94. package/dist/runtime/admin-ops.js +60 -21
  95. package/dist/runtime/admin-ops.js.map +1 -1
  96. package/dist/runtime/admin-setup-ops.d.ts +11 -0
  97. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  98. package/dist/runtime/admin-setup-ops.js +146 -37
  99. package/dist/runtime/admin-setup-ops.js.map +1 -1
  100. package/dist/runtime/capture-ops.d.ts.map +1 -1
  101. package/dist/runtime/capture-ops.js +38 -12
  102. package/dist/runtime/capture-ops.js.map +1 -1
  103. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  104. package/dist/runtime/facades/brain-facade.js +16 -4
  105. package/dist/runtime/facades/brain-facade.js.map +1 -1
  106. package/dist/runtime/facades/context-facade.d.ts.map +1 -1
  107. package/dist/runtime/facades/context-facade.js +9 -3
  108. package/dist/runtime/facades/context-facade.js.map +1 -1
  109. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  110. package/dist/runtime/facades/memory-facade.js +20 -7
  111. package/dist/runtime/facades/memory-facade.js.map +1 -1
  112. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  113. package/dist/runtime/facades/orchestrate-facade.js +40 -1
  114. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  115. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  116. package/dist/runtime/facades/plan-facade.js +113 -4
  117. package/dist/runtime/facades/plan-facade.js.map +1 -1
  118. package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
  119. package/dist/runtime/facades/vault-facade.js +24 -3
  120. package/dist/runtime/facades/vault-facade.js.map +1 -1
  121. package/dist/runtime/orchestrate-ops.d.ts +21 -0
  122. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  123. package/dist/runtime/orchestrate-ops.js +132 -38
  124. package/dist/runtime/orchestrate-ops.js.map +1 -1
  125. package/dist/runtime/runtime.d.ts.map +1 -1
  126. package/dist/runtime/runtime.js +16 -0
  127. package/dist/runtime/runtime.js.map +1 -1
  128. package/dist/runtime/schema-helpers.d.ts.map +1 -1
  129. package/dist/runtime/schema-helpers.js +4 -0
  130. package/dist/runtime/schema-helpers.js.map +1 -1
  131. package/dist/runtime/types.d.ts +19 -0
  132. package/dist/runtime/types.d.ts.map +1 -1
  133. package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
  134. package/dist/runtime/vault-linking-ops.js +16 -3
  135. package/dist/runtime/vault-linking-ops.js.map +1 -1
  136. package/dist/scheduler/cron-validator.d.ts +15 -0
  137. package/dist/scheduler/cron-validator.d.ts.map +1 -0
  138. package/dist/scheduler/cron-validator.js +93 -0
  139. package/dist/scheduler/cron-validator.js.map +1 -0
  140. package/dist/scheduler/platform-linux.d.ts +14 -0
  141. package/dist/scheduler/platform-linux.d.ts.map +1 -0
  142. package/dist/scheduler/platform-linux.js +107 -0
  143. package/dist/scheduler/platform-linux.js.map +1 -0
  144. package/dist/scheduler/platform-macos.d.ts +15 -0
  145. package/dist/scheduler/platform-macos.d.ts.map +1 -0
  146. package/dist/scheduler/platform-macos.js +131 -0
  147. package/dist/scheduler/platform-macos.js.map +1 -0
  148. package/dist/scheduler/scheduler-ops.d.ts +14 -0
  149. package/dist/scheduler/scheduler-ops.d.ts.map +1 -0
  150. package/dist/scheduler/scheduler-ops.js +77 -0
  151. package/dist/scheduler/scheduler-ops.js.map +1 -0
  152. package/dist/scheduler/scheduler.d.ts +55 -0
  153. package/dist/scheduler/scheduler.d.ts.map +1 -0
  154. package/dist/scheduler/scheduler.js +144 -0
  155. package/dist/scheduler/scheduler.js.map +1 -0
  156. package/dist/scheduler/types.d.ts +48 -0
  157. package/dist/scheduler/types.d.ts.map +1 -0
  158. package/dist/scheduler/types.js +6 -0
  159. package/dist/scheduler/types.js.map +1 -0
  160. package/dist/skills/sync-skills.d.ts +11 -0
  161. package/dist/skills/sync-skills.d.ts.map +1 -1
  162. package/dist/skills/sync-skills.js +132 -38
  163. package/dist/skills/sync-skills.js.map +1 -1
  164. package/dist/skills/validate-skills.d.ts +32 -0
  165. package/dist/skills/validate-skills.d.ts.map +1 -0
  166. package/dist/skills/validate-skills.js +396 -0
  167. package/dist/skills/validate-skills.js.map +1 -0
  168. package/dist/utils/worktree-reaper.d.ts +38 -0
  169. package/dist/utils/worktree-reaper.d.ts.map +1 -0
  170. package/dist/utils/worktree-reaper.js +85 -0
  171. package/dist/utils/worktree-reaper.js.map +1 -0
  172. package/dist/vault/default-canonical-tags.d.ts +15 -0
  173. package/dist/vault/default-canonical-tags.d.ts.map +1 -0
  174. package/dist/vault/default-canonical-tags.js +65 -0
  175. package/dist/vault/default-canonical-tags.js.map +1 -0
  176. package/dist/vault/scope-detector.d.ts.map +1 -1
  177. package/dist/vault/scope-detector.js +37 -4
  178. package/dist/vault/scope-detector.js.map +1 -1
  179. package/dist/vault/tag-normalizer.d.ts +42 -0
  180. package/dist/vault/tag-normalizer.d.ts.map +1 -0
  181. package/dist/vault/tag-normalizer.js +157 -0
  182. package/dist/vault/tag-normalizer.js.map +1 -0
  183. package/dist/vault/vault-entries.d.ts.map +1 -1
  184. package/dist/vault/vault-entries.js +3 -1
  185. package/dist/vault/vault-entries.js.map +1 -1
  186. package/package.json +5 -1
  187. package/src/__tests__/embeddings.test.ts +3 -3
  188. package/src/agency/agency-manager.test.ts +4 -4
  189. package/src/agency/default-rules.test.ts +0 -13
  190. package/src/brain/brain-intelligence.test.ts +0 -5
  191. package/src/brain/brain.ts +25 -1
  192. package/src/brain/intelligence.ts +25 -0
  193. package/src/brain/second-brain-features.test.ts +2 -14
  194. package/src/brain/types.ts +1 -0
  195. package/src/capabilities/chain-mapping.test.ts +1 -6
  196. package/src/capabilities/chain-mapping.ts +6 -4
  197. package/src/capabilities/registry.test.ts +1 -1
  198. package/src/capabilities/registry.ts +9 -2
  199. package/src/chat/agent-loop.test.ts +1 -1
  200. package/src/chat/chat-enhanced.test.ts +0 -8
  201. package/src/chat/chat-session.ts +75 -17
  202. package/src/chat/chat-transport.test.ts +31 -1
  203. package/src/claudemd/compose.test.ts +0 -5
  204. package/src/context/context-engine.test.ts +0 -1
  205. package/src/context/context-engine.ts +1 -1
  206. package/src/control/intent-router.test.ts +2 -2
  207. package/src/curator/curator.ts +180 -0
  208. package/src/curator/tag-manager.test.ts +0 -4
  209. package/src/curator/types.ts +10 -0
  210. package/src/domain-packs/types.test.ts +0 -5
  211. package/src/dream/dream.test.ts +0 -7
  212. package/src/enforcement/registry.test.ts +2 -2
  213. package/src/engine/core-ops.test.ts +4 -22
  214. package/src/engine/core-ops.ts +36 -1
  215. package/src/engine/module-manifest.test.ts +1 -31
  216. package/src/engine/register-engine.test.ts +3 -33
  217. package/src/errors/retry.test.ts +3 -1
  218. package/src/flows/chain-runner.test.ts +0 -6
  219. package/src/flows/context-router.test.ts +3 -3
  220. package/src/flows/epilogue.test.ts +40 -2
  221. package/src/flows/epilogue.ts +11 -2
  222. package/src/flows/executor.test.ts +48 -2
  223. package/src/flows/executor.ts +15 -5
  224. package/src/flows/index.ts +1 -3
  225. package/src/flows/plan-builder.test.ts +201 -0
  226. package/src/flows/plan-builder.ts +81 -5
  227. package/src/flows/probes.ts +17 -3
  228. package/src/flows/types.ts +31 -2
  229. package/src/health/health-registry.test.ts +3 -1
  230. package/src/index.ts +24 -0
  231. package/src/intake/content-classifier.ts +22 -4
  232. package/src/intake/dedup-gate.test.ts +2 -6
  233. package/src/intake/text-ingester.test.ts +3 -4
  234. package/src/intake/text-ingester.ts +61 -12
  235. package/src/llm/llm-client.test.ts +1 -1
  236. package/src/llm/utils.test.ts +1 -1
  237. package/src/migrations/migration-runner.test.ts +0 -1
  238. package/src/operator/operator-context-store.test.ts +0 -13
  239. package/src/operator/operator-profile.test.ts +2 -20
  240. package/src/packs/pack-installer.ts +28 -2
  241. package/src/packs/pack-system.test.ts +2 -2
  242. package/src/persona/defaults.test.ts +19 -19
  243. package/src/planning/gap-passes.test.ts +0 -46
  244. package/src/planning/gap-patterns.test.ts +0 -42
  245. package/src/planning/goal-ancestry.test.ts +3 -1
  246. package/src/planning/plan-lifecycle.test.ts +15 -7
  247. package/src/planning/planner-types.ts +2 -0
  248. package/src/planning/planner.test.ts +86 -90
  249. package/src/planning/planner.ts +56 -4
  250. package/src/planning/reconciliation-engine.test.ts +3 -10
  251. package/src/planning/task-complexity-assessor.test.ts +0 -5
  252. package/src/planning/task-verifier.test.ts +3 -1
  253. package/src/playbooks/generic/generic-playbooks.test.ts +0 -28
  254. package/src/playbooks/index.test.ts +0 -55
  255. package/src/playbooks/playbook-executor.test.ts +76 -0
  256. package/src/playbooks/playbook-executor.ts +24 -3
  257. package/src/playbooks/playbook-types.ts +8 -0
  258. package/src/plugins/plugin-registry.test.ts +6 -2
  259. package/src/project/project-registry.test.ts +2 -0
  260. package/src/queue/async-infrastructure.test.ts +6 -4
  261. package/src/queue/job-queue.test.ts +13 -7
  262. package/src/runtime/admin-extra-ops.test.ts +35 -30
  263. package/src/runtime/admin-extra-ops.ts +30 -0
  264. package/src/runtime/admin-ops.test.ts +0 -4
  265. package/src/runtime/admin-ops.ts +63 -21
  266. package/src/runtime/admin-setup-ops.test.ts +229 -13
  267. package/src/runtime/admin-setup-ops.ts +145 -36
  268. package/src/runtime/archive-ops.test.ts +0 -28
  269. package/src/runtime/branching-ops.test.ts +0 -17
  270. package/src/runtime/capture-ops.test.ts +41 -16
  271. package/src/runtime/capture-ops.ts +78 -46
  272. package/src/runtime/chain-ops.test.ts +0 -21
  273. package/src/runtime/facades/admin-facade.test.ts +0 -34
  274. package/src/runtime/facades/agency-facade.test.ts +0 -39
  275. package/src/runtime/facades/archive-facade.test.ts +0 -43
  276. package/src/runtime/facades/brain-facade.test.ts +8 -99
  277. package/src/runtime/facades/brain-facade.ts +29 -12
  278. package/src/runtime/facades/branching-facade.test.ts +30 -17
  279. package/src/runtime/facades/chat-facade.test.ts +0 -91
  280. package/src/runtime/facades/chat-service-ops.test.ts +0 -24
  281. package/src/runtime/facades/chat-session-ops.test.ts +0 -12
  282. package/src/runtime/facades/chat-transport-ops.test.ts +0 -23
  283. package/src/runtime/facades/context-facade.test.ts +0 -17
  284. package/src/runtime/facades/context-facade.ts +11 -4
  285. package/src/runtime/facades/control-facade.test.ts +0 -30
  286. package/src/runtime/facades/curator-facade.test.ts +0 -33
  287. package/src/runtime/facades/intake-facade.test.ts +0 -33
  288. package/src/runtime/facades/links-facade.test.ts +0 -37
  289. package/src/runtime/facades/loop-facade.test.ts +0 -26
  290. package/src/runtime/facades/memory-facade.test.ts +0 -18
  291. package/src/runtime/facades/memory-facade.ts +27 -11
  292. package/src/runtime/facades/operator-facade.test.ts +0 -31
  293. package/src/runtime/facades/orchestrate-facade.test.ts +0 -21
  294. package/src/runtime/facades/orchestrate-facade.ts +39 -1
  295. package/src/runtime/facades/plan-facade.test.ts +7 -32
  296. package/src/runtime/facades/plan-facade.ts +137 -4
  297. package/src/runtime/facades/review-facade.test.ts +1 -49
  298. package/src/runtime/facades/sync-facade.test.ts +24 -41
  299. package/src/runtime/facades/tier-facade.test.ts +30 -22
  300. package/src/runtime/facades/vault-facade.test.ts +0 -41
  301. package/src/runtime/facades/vault-facade.ts +26 -3
  302. package/src/runtime/grading-ops.test.ts +0 -27
  303. package/src/runtime/intake-ops.test.ts +0 -19
  304. package/src/runtime/loop-ops.test.ts +0 -48
  305. package/src/runtime/memory-cross-project-ops.test.ts +0 -14
  306. package/src/runtime/memory-extra-ops.test.ts +4 -8
  307. package/src/runtime/orchestrate-ops.test.ts +238 -19
  308. package/src/runtime/orchestrate-ops.ts +166 -41
  309. package/src/runtime/pack-ops.test.ts +0 -26
  310. package/src/runtime/planning-extra-ops.test.ts +2 -14
  311. package/src/runtime/playbook-ops-execution.test.ts +9 -20
  312. package/src/runtime/playbook-ops.test.ts +4 -67
  313. package/src/runtime/review-ops.test.ts +0 -15
  314. package/src/runtime/runtime.ts +18 -0
  315. package/src/runtime/schema-helpers.ts +4 -0
  316. package/src/runtime/sync-ops.test.ts +0 -18
  317. package/src/runtime/tier-ops.test.ts +0 -21
  318. package/src/runtime/types.ts +19 -0
  319. package/src/runtime/vault-extra-ops.test.ts +0 -12
  320. package/src/runtime/vault-linking-ops.test.ts +0 -4
  321. package/src/runtime/vault-linking-ops.ts +26 -8
  322. package/src/runtime/vault-sharing-ops.test.ts +0 -9
  323. package/src/scheduler/cron-validator.ts +101 -0
  324. package/src/scheduler/platform-linux.ts +122 -0
  325. package/src/scheduler/platform-macos.ts +150 -0
  326. package/src/scheduler/scheduler-ops.ts +77 -0
  327. package/src/scheduler/scheduler.test.ts +247 -0
  328. package/src/scheduler/scheduler.ts +174 -0
  329. package/src/scheduler/types.ts +52 -0
  330. package/src/skills/__tests__/sync-skills.test.ts +6 -17
  331. package/src/skills/global-claude-md.test.ts +113 -0
  332. package/src/skills/sync-skills.ts +143 -35
  333. package/src/skills/validate-skills.test.ts +206 -0
  334. package/src/skills/validate-skills.ts +470 -0
  335. package/src/telemetry/telemetry.test.ts +1 -0
  336. package/src/transport/http-server.test.ts +3 -0
  337. package/src/transport/session-manager.test.ts +3 -1
  338. package/src/transport/token-auth.test.ts +6 -9
  339. package/src/transport/ws-server.test.ts +10 -2
  340. package/src/utils/worktree-reaper.ts +113 -0
  341. package/src/vault/__tests__/vault-characterization.test.ts +0 -108
  342. package/src/vault/default-canonical-tags.ts +64 -0
  343. package/src/vault/linking.test.ts +0 -2
  344. package/src/vault/playbook.test.ts +4 -1
  345. package/src/vault/scope-detector.test.ts +3 -1
  346. package/src/vault/scope-detector.ts +42 -4
  347. package/src/vault/tag-normalizer.test.ts +214 -0
  348. package/src/vault/tag-normalizer.ts +188 -0
  349. package/src/vault/vault-connect.test.ts +1 -1
  350. package/src/vault/vault-entries.ts +3 -1
  351. package/src/vault/vault.test.ts +23 -8
  352. package/dist/embeddings/index.d.ts +0 -5
  353. package/dist/embeddings/index.d.ts.map +0 -1
  354. package/dist/embeddings/index.js +0 -3
  355. package/dist/embeddings/index.js.map +0 -1
@@ -173,25 +173,11 @@ describe('syncSkillsToClaudeCode — project-local install', () => {
173
173
  expect(stat.isSymbolicLink()).toBe(true);
174
174
  });
175
175
 
176
- it('cleans stale global entries with agent-soleri- prefix', async () => {
176
+ it('does NOT touch ~/.claude/skills/ during project-local install', async () => {
177
177
  createSourceSkill('soleri-vault-capture');
178
- // Simulate old global duplicates
178
+ // Global dir has ernesto-soleri-* entries — project-local sync must leave them alone
179
179
  createGlobalSkillDir('ernesto-soleri-vault-capture');
180
180
  createGlobalSkillDir('ernesto-soleri-vault-navigator');
181
-
182
- const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
183
- const result = syncSkillsToClaudeCode([sourceDir], 'Ernesto', {
184
- projectRoot: fakeProject,
185
- });
186
-
187
- expect(result.cleanedGlobal).toContain('ernesto-soleri-vault-capture');
188
- expect(result.cleanedGlobal).toContain('ernesto-soleri-vault-navigator');
189
- expect(globalDirExists('ernesto-soleri-vault-capture')).toBe(false);
190
- expect(globalDirExists('ernesto-soleri-vault-navigator')).toBe(false);
191
- });
192
-
193
- it('does not clean global entries that do not match agent-soleri- prefix', async () => {
194
- createSourceSkill('soleri-vault-capture');
195
181
  createGlobalSkillDir('other-agent-skill');
196
182
 
197
183
  const { syncSkillsToClaudeCode } = await import('../sync-skills.js');
@@ -199,7 +185,10 @@ describe('syncSkillsToClaudeCode — project-local install', () => {
199
185
  projectRoot: fakeProject,
200
186
  });
201
187
 
202
- expect(result.cleanedGlobal).not.toContain('other-agent-skill');
188
+ // cleanedGlobal must be empty — project-local sync must not remove global entries
189
+ expect(result.cleanedGlobal).toHaveLength(0);
190
+ expect(globalDirExists('ernesto-soleri-vault-capture')).toBe(true);
191
+ expect(globalDirExists('ernesto-soleri-vault-navigator')).toBe(true);
203
192
  expect(globalDirExists('other-agent-skill')).toBe(true);
204
193
  });
205
194
  });
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Unit tests for global ~/.claude/CLAUDE.md scaffolding functions.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdirSync, readFileSync, rmSync, existsSync } from 'node:fs';
7
+ import { join } from 'node:path';
8
+ import { tmpdir } from 'node:os';
9
+ import { scaffoldGlobalClaudeMd, removeAgentFromGlobalClaudeMd } from './sync-skills.js';
10
+
11
+ // ---------------------------------------------------------------------------
12
+ // Test harness — redirect homedir() to a temp dir via env override
13
+ // ---------------------------------------------------------------------------
14
+
15
+ let tmpHome: string;
16
+ let claudeDir: string;
17
+ let claudeMdPath: string;
18
+
19
+ // We patch the module by temporarily pointing HOME (and USERPROFILE on Windows) at a temp dir
20
+ beforeEach(() => {
21
+ tmpHome = join(tmpdir(), `soleri-claude-md-test-${Date.now()}`);
22
+ claudeDir = join(tmpHome, '.claude');
23
+ claudeMdPath = join(claudeDir, 'CLAUDE.md');
24
+ mkdirSync(claudeDir, { recursive: true });
25
+ process.env['HOME'] = tmpHome;
26
+ process.env['USERPROFILE'] = tmpHome; // Windows: homedir() reads USERPROFILE, not HOME
27
+ });
28
+
29
+ afterEach(() => {
30
+ rmSync(tmpHome, { recursive: true, force: true });
31
+ delete process.env['HOME'];
32
+ delete process.env['USERPROFILE'];
33
+ });
34
+
35
+ describe('scaffoldGlobalClaudeMd', () => {
36
+ it('creates CLAUDE.md with header and agent section when file does not exist', () => {
37
+ scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
38
+
39
+ expect(existsSync(claudeMdPath)).toBe(true);
40
+ const content = readFileSync(claudeMdPath, 'utf-8');
41
+ expect(content).toContain('# Soleri Engine');
42
+ expect(content).toContain('<!-- soleri:agent:ernesto start -->');
43
+ expect(content).toContain('<!-- soleri:agent:ernesto end -->');
44
+ expect(content).toContain('## Ernesto');
45
+ expect(content).toContain('ernesto_*');
46
+ });
47
+
48
+ it('replaces existing agent section on second call (idempotent)', () => {
49
+ scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
50
+ scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
51
+
52
+ const content = readFileSync(claudeMdPath, 'utf-8');
53
+ const startCount = (content.match(/<!-- soleri:agent:ernesto start -->/g) ?? []).length;
54
+ expect(startCount).toBe(1);
55
+ });
56
+
57
+ it('appends a second agent section without disturbing the first', () => {
58
+ scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
59
+ scaffoldGlobalClaudeMd('salvador', 'Salvador');
60
+
61
+ const content = readFileSync(claudeMdPath, 'utf-8');
62
+ expect(content).toContain('<!-- soleri:agent:ernesto start -->');
63
+ expect(content).toContain('<!-- soleri:agent:ernesto end -->');
64
+ expect(content).toContain('<!-- soleri:agent:salvador start -->');
65
+ expect(content).toContain('<!-- soleri:agent:salvador end -->');
66
+ });
67
+
68
+ it('updating one agent does not affect another agent section', () => {
69
+ scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
70
+ scaffoldGlobalClaudeMd('salvador', 'Salvador');
71
+ scaffoldGlobalClaudeMd('ernesto', 'Ernesto Updated');
72
+
73
+ const content = readFileSync(claudeMdPath, 'utf-8');
74
+ expect(content).toContain('Ernesto Updated');
75
+ expect(content).toContain('<!-- soleri:agent:salvador start -->');
76
+ // Only one ernesto section
77
+ const count = (content.match(/<!-- soleri:agent:ernesto start -->/g) ?? []).length;
78
+ expect(count).toBe(1);
79
+ });
80
+ });
81
+
82
+ describe('removeAgentFromGlobalClaudeMd', () => {
83
+ it('is a no-op when CLAUDE.md does not exist', () => {
84
+ expect(() => removeAgentFromGlobalClaudeMd('ernesto')).not.toThrow();
85
+ });
86
+
87
+ it('removes the agent section', () => {
88
+ scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
89
+ removeAgentFromGlobalClaudeMd('ernesto');
90
+
91
+ const content = readFileSync(claudeMdPath, 'utf-8');
92
+ expect(content).not.toContain('<!-- soleri:agent:ernesto start -->');
93
+ expect(content).not.toContain('<!-- soleri:agent:ernesto end -->');
94
+ });
95
+
96
+ it('only removes the target agent, leaving others intact', () => {
97
+ scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
98
+ scaffoldGlobalClaudeMd('salvador', 'Salvador');
99
+ removeAgentFromGlobalClaudeMd('ernesto');
100
+
101
+ const content = readFileSync(claudeMdPath, 'utf-8');
102
+ expect(content).not.toContain('<!-- soleri:agent:ernesto start -->');
103
+ expect(content).toContain('<!-- soleri:agent:salvador start -->');
104
+ });
105
+
106
+ it('is a no-op when the agent section is not in the file', () => {
107
+ scaffoldGlobalClaudeMd('ernesto', 'Ernesto');
108
+ const before = readFileSync(claudeMdPath, 'utf-8');
109
+ removeAgentFromGlobalClaudeMd('nonexistent');
110
+ const after = readFileSync(claudeMdPath, 'utf-8');
111
+ expect(after).toBe(before);
112
+ });
113
+ });
@@ -82,7 +82,18 @@ export function discoverSkills(skillsDirs: string[]): SkillEntry[] {
82
82
  if (!existsSync(dir)) continue;
83
83
  const entries = readdirSync(dir, { withFileTypes: true });
84
84
  for (const entry of entries) {
85
- if (!entry.isDirectory()) continue;
85
+ // Follow symlinks — project-local installs use symlinks to source skills
86
+ const isDir =
87
+ entry.isDirectory() ||
88
+ (entry.isSymbolicLink() &&
89
+ (() => {
90
+ try {
91
+ return statSync(join(dir, entry.name)).isDirectory();
92
+ } catch {
93
+ return false;
94
+ }
95
+ })());
96
+ if (!isDir) continue;
86
97
  const skillPath = join(dir, entry.name, 'SKILL.md');
87
98
  if (existsSync(skillPath)) {
88
99
  skills.push({ name: entry.name, sourcePath: skillPath });
@@ -248,51 +259,148 @@ export function syncSkillsToClaudeCode(
248
259
  }
249
260
  }
250
261
 
251
- // Task 3: Clean up stale global entries when doing a project-local install
252
- if (!isGlobal && agentName) {
253
- cleanStaleGlobalSkills(agentName, result);
262
+ // Task 3: (removed cleanStaleGlobalSkills was wiping valid ernesto-soleri-* entries
263
+ // that the global sync installed. Task 2 orphan removal handles stale entries during
264
+ // global sync; project-local sync must not touch ~/.claude/skills/.)
265
+
266
+ // Task 4: Scaffold global ~/.claude/CLAUDE.md when doing a global install
267
+ if (isGlobal && agentName && result.installed.length + result.updated.length > 0) {
268
+ try {
269
+ const agentId = agentName.toLowerCase().replace(/\s+/g, '-');
270
+ const displayName = agentName.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
271
+ scaffoldGlobalClaudeMd(agentId, displayName);
272
+ } catch {
273
+ // Best-effort — don't fail the sync
274
+ }
254
275
  }
255
276
 
256
277
  return result;
257
278
  }
258
279
 
280
+ // =============================================================================
281
+ // GLOBAL CLAUDE.MD SCAFFOLDING
282
+ // =============================================================================
283
+
259
284
  /**
260
- * Remove ALL `{agent}-soleri-*` entries from ~/.claude/skills/
261
- * to clean up duplicates left by the old global-install behavior.
262
- *
263
- * Cleans entries from ALL agents, not just the current one — any
264
- * `*-soleri-*` entry in the global dir is a stale copy from a previous
265
- * global install. Canonical skills now live in project-local .claude/skills/.
285
+ * Sentinel markers that bracket an agent's section in ~/.claude/CLAUDE.md.
286
+ * Using HTML comments so they don't render in markdown viewers.
266
287
  */
267
- function cleanStaleGlobalSkills(agentName: string, result: SyncResult): void {
268
- const globalSkillsDir = join(homedir(), '.claude', 'skills');
269
- if (!existsSync(globalSkillsDir)) return;
288
+ function agentSectionStart(agentId: string): string {
289
+ return `<!-- soleri:agent:${agentId} start -->`;
290
+ }
291
+ function agentSectionEnd(agentId: string): string {
292
+ return `<!-- soleri:agent:${agentId} end -->`;
293
+ }
270
294
 
271
- // Match any agent-prefixed soleri skill: <anything>-soleri-<skillname>
272
- // Canonical project-local names look like "soleri-*" (no agent prefix).
273
- const stalePattern = /^.+-soleri-.+$/;
295
+ /**
296
+ * Build the minimal routing section for one agent in the global CLAUDE.md.
297
+ */
298
+ function buildAgentSection(agentId: string, displayName: string): string {
299
+ return [
300
+ agentSectionStart(agentId),
301
+ `## ${displayName}`,
302
+ '',
303
+ `Skills for **${displayName}** are installed globally. Agent-specific instructions`,
304
+ `are in each project's \`CLAUDE.md\`.`,
305
+ '',
306
+ `**Routing:** When you see \`${agentId}_*\` MCP tools, follow the project's \`CLAUDE.md\`.`,
307
+ agentSectionEnd(agentId),
308
+ ].join('\n');
309
+ }
274
310
 
275
- try {
276
- const entries = readdirSync(globalSkillsDir, { withFileTypes: true });
277
- for (const entry of entries) {
278
- if (!entry.isDirectory()) continue;
279
- if (!stalePattern.test(entry.name)) continue;
280
-
281
- const staleDir = join(globalSkillsDir, entry.name);
282
- try {
283
- const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
284
- const stagingDir = join(homedir(), '.soleri', 'staging', timestamp);
285
- mkdirSync(stagingDir, { recursive: true });
286
- cpSync(staleDir, join(stagingDir, entry.name), { recursive: true });
287
- rmSync(staleDir, { recursive: true, force: true });
288
- result.cleanedGlobal.push(entry.name);
289
- } catch {
290
- // Best-effort cleanup — don't fail the sync
291
- }
311
+ /**
312
+ * Create or update ~/.claude/CLAUDE.md to include a routing section for the
313
+ * given agent. Idempotent — replaces the agent's existing section if present,
314
+ * appends otherwise. Does not touch other agents' sections.
315
+ */
316
+ export function scaffoldGlobalClaudeMd(agentId: string, displayName: string): void {
317
+ const claudeMdPath = join(homedir(), '.claude', 'CLAUDE.md');
318
+
319
+ const header = [
320
+ '# Soleri Engine',
321
+ '',
322
+ 'Active Soleri agents are installed on this system. Agent-specific instructions',
323
+ "are in each project's `CLAUDE.md`.",
324
+ '',
325
+ '## Routing',
326
+ '',
327
+ "- When working in a Soleri agent project, follow that project's `CLAUDE.md`",
328
+ '- Agent sections below are managed automatically — do not edit manually',
329
+ '',
330
+ ].join('\n');
331
+
332
+ const newSection = buildAgentSection(agentId, displayName);
333
+ const start = agentSectionStart(agentId);
334
+ const end = agentSectionEnd(agentId);
335
+
336
+ mkdirSync(join(homedir(), '.claude'), { recursive: true });
337
+
338
+ let existing = '';
339
+ if (existsSync(claudeMdPath)) {
340
+ existing = readFileSync(claudeMdPath, 'utf-8');
341
+ }
342
+
343
+ let startIdx = existing.indexOf(start);
344
+ let endIdx = existing.indexOf(end);
345
+
346
+ // Migrate legacy sentinel format: <!-- agent:{id}:mode --> / <!-- /agent:{id}:mode -->
347
+ if (startIdx === -1) {
348
+ const legacyStart = `<!-- agent:${agentId}:mode -->`;
349
+ const legacyEnd = `<!-- /agent:${agentId}:mode -->`;
350
+ const ls = existing.indexOf(legacyStart);
351
+ const le = existing.indexOf(legacyEnd);
352
+ if (ls !== -1 && le !== -1) {
353
+ startIdx = ls;
354
+ endIdx = le;
355
+ // Point past the legacy end sentinel so we replace the whole block
356
+ existing =
357
+ existing.slice(0, ls) +
358
+ start +
359
+ existing.slice(ls + legacyStart.length, le) +
360
+ end +
361
+ existing.slice(le + legacyEnd.length);
362
+ startIdx = existing.indexOf(start);
363
+ endIdx = existing.indexOf(end);
292
364
  }
293
- } catch {
294
- // Global skills dir unreadable — nothing to clean
295
365
  }
366
+
367
+ let updated: string;
368
+ if (startIdx !== -1 && endIdx !== -1) {
369
+ // Replace the existing section
370
+ updated = existing.slice(0, startIdx) + newSection + existing.slice(endIdx + end.length);
371
+ } else if (existing.length === 0) {
372
+ // New file — write header + section
373
+ updated = header + newSection + '\n';
374
+ } else {
375
+ // Append to existing file
376
+ updated = existing.trimEnd() + '\n\n' + newSection + '\n';
377
+ }
378
+
379
+ writeFileSync(claudeMdPath, updated);
380
+ }
381
+
382
+ /**
383
+ * Remove an agent's section from ~/.claude/CLAUDE.md.
384
+ * No-op if the file or section doesn't exist.
385
+ */
386
+ export function removeAgentFromGlobalClaudeMd(agentId: string): void {
387
+ const claudeMdPath = join(homedir(), '.claude', 'CLAUDE.md');
388
+ if (!existsSync(claudeMdPath)) return;
389
+
390
+ const content = readFileSync(claudeMdPath, 'utf-8');
391
+ const start = agentSectionStart(agentId);
392
+ const end = agentSectionEnd(agentId);
393
+
394
+ const startIdx = content.indexOf(start);
395
+ const endIdx = content.indexOf(end);
396
+ if (startIdx === -1 || endIdx === -1) return;
397
+
398
+ // Remove the section and any leading blank line before it
399
+ const before = content.slice(0, startIdx).trimEnd();
400
+ const after = content.slice(endIdx + end.length);
401
+
402
+ const updated = (before.length > 0 ? before + '\n' : '') + after.replace(/^\n+/, '\n');
403
+ writeFileSync(claudeMdPath, updated.trimEnd() + '\n');
296
404
  }
297
405
 
298
406
  // =============================================================================
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Unit tests for validate-skills — the user-installed SKILL.md validator.
3
+ */
4
+
5
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
6
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
7
+ import { tmpdir } from 'node:os';
8
+ import { join } from 'node:path';
9
+ import { validateSkillDocs } from './validate-skills.js';
10
+
11
+ // ── Helpers ──────────────────────────────────────────────────────────────
12
+
13
+ function createSkillsDir(): string {
14
+ return mkdtempSync(join(tmpdir(), 'soleri-validate-skills-test-'));
15
+ }
16
+
17
+ function addSkill(skillsDir: string, skillName: string, content: string): void {
18
+ const skillDir = join(skillsDir, skillName);
19
+ mkdirSync(skillDir, { recursive: true });
20
+ writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf-8');
21
+ }
22
+
23
+ // ── Tests ────────────────────────────────────────────────────────────────
24
+
25
+ describe('validateSkillDocs', () => {
26
+ let skillsDir: string;
27
+
28
+ beforeEach(() => {
29
+ skillsDir = createSkillsDir();
30
+ });
31
+
32
+ afterEach(() => {
33
+ rmSync(skillsDir, { recursive: true, force: true });
34
+ });
35
+
36
+ it('returns valid=true and no errors when the skills directory is empty', () => {
37
+ const result = validateSkillDocs(skillsDir);
38
+ expect(result.valid).toBe(true);
39
+ expect(result.errors).toHaveLength(0);
40
+ expect(result.totalFiles).toBe(0);
41
+ expect(result.totalExamples).toBe(0);
42
+ });
43
+
44
+ it('returns valid=true for a SKILL.md with no op-call examples', () => {
45
+ addSkill(
46
+ skillsDir,
47
+ 'my-skill',
48
+ `# My Skill
49
+
50
+ This skill does something useful.
51
+
52
+ ## Usage
53
+
54
+ Just invoke it.
55
+ `,
56
+ );
57
+
58
+ const result = validateSkillDocs(skillsDir);
59
+ expect(result.valid).toBe(true);
60
+ expect(result.errors).toHaveLength(0);
61
+ expect(result.totalFiles).toBe(1);
62
+ expect(result.totalExamples).toBe(0);
63
+ });
64
+
65
+ it('returns valid=true when op-call params match the schema', () => {
66
+ addSkill(
67
+ skillsDir,
68
+ 'capture-skill',
69
+ `# Capture Skill
70
+
71
+ Captures knowledge to the vault.
72
+
73
+ \`\`\`
74
+ YOUR_AGENT_core op:capture_knowledge params: { projectPath: ".", entries: [{ type: "pattern", domain: "testing", title: "Use vitest", description: "Prefer vitest for unit tests", severity: "info" }] }
75
+ \`\`\`
76
+ `,
77
+ );
78
+
79
+ const result = validateSkillDocs(skillsDir);
80
+ expect(result.valid).toBe(true);
81
+ expect(result.errors).toHaveLength(0);
82
+ });
83
+
84
+ it('reports an error when severity has an invalid enum value', () => {
85
+ // "suggestion" is not in the capture_knowledge severity enum (valid: critical, warning, info)
86
+ addSkill(
87
+ skillsDir,
88
+ 'bad-severity-skill',
89
+ `# Bad Skill
90
+
91
+ Example with wrong severity enum:
92
+
93
+ \`\`\`
94
+ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
95
+ \`\`\`
96
+ `,
97
+ );
98
+
99
+ const result = validateSkillDocs(skillsDir);
100
+ expect(result.valid).toBe(false);
101
+ expect(result.errors.length).toBeGreaterThan(0);
102
+
103
+ const severityError = result.errors.find(
104
+ (e) => e.op === 'capture_knowledge' && e.message.toLowerCase().includes('invalid'),
105
+ );
106
+ expect(severityError).toBeDefined();
107
+ expect(severityError!.file).toContain('bad-severity-skill');
108
+ });
109
+
110
+ it('reports an error when scope receives an object instead of a string', () => {
111
+ // create_plan scope expects z.string() but we pass an object
112
+ addSkill(
113
+ skillsDir,
114
+ 'bad-scope-skill',
115
+ `# Bad Scope Skill
116
+
117
+ Example with wrong scope type:
118
+
119
+ \`\`\`
120
+ YOUR_AGENT_core op:create_plan params: { title: "My Plan", objective: "Do something", scope: { included: [] } }
121
+ \`\`\`
122
+ `,
123
+ );
124
+
125
+ const result = validateSkillDocs(skillsDir);
126
+ expect(result.valid).toBe(false);
127
+ expect(result.errors.length).toBeGreaterThan(0);
128
+
129
+ const scopeError = result.errors.find(
130
+ (e) => e.op === 'create_plan' && e.message.includes('scope'),
131
+ );
132
+ expect(scopeError).toBeDefined();
133
+ expect(scopeError!.message).toContain('Expected string');
134
+ });
135
+
136
+ it('returns structured error objects with required fields', () => {
137
+ addSkill(
138
+ skillsDir,
139
+ 'structured-error-skill',
140
+ `# Structured Error Skill
141
+
142
+ \`\`\`
143
+ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
144
+ \`\`\`
145
+ `,
146
+ );
147
+
148
+ const result = validateSkillDocs(skillsDir);
149
+
150
+ // "suggestion" is not a valid severity — expect at least one error
151
+ expect(result.errors.length).toBeGreaterThan(0);
152
+ const err = result.errors[0];
153
+ expect(err).toHaveProperty('file');
154
+ expect(err).toHaveProperty('op');
155
+ expect(err).toHaveProperty('message');
156
+ expect(typeof err.file).toBe('string');
157
+ expect(typeof err.op).toBe('string');
158
+ expect(typeof err.message).toBe('string');
159
+ });
160
+
161
+ it('includes the file path and op name in each error', () => {
162
+ addSkill(
163
+ skillsDir,
164
+ 'named-skill',
165
+ `# Named Skill
166
+
167
+ \`\`\`
168
+ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", domain: "testing", title: "Test", description: "A test", severity: "suggestion" }] }
169
+ \`\`\`
170
+ `,
171
+ );
172
+
173
+ const result = validateSkillDocs(skillsDir);
174
+ expect(result.errors.length).toBeGreaterThan(0);
175
+
176
+ const err = result.errors[0];
177
+ expect(err.file).toContain('named-skill');
178
+ expect(err.op).toBe('capture_knowledge');
179
+ });
180
+
181
+ it('builds a schema registry covering core ops', () => {
182
+ const result = validateSkillDocs(skillsDir);
183
+ // Registry must cover: capture_knowledge, capture_quick, create_plan, approve_plan, etc.
184
+ expect(result.registrySize).toBeGreaterThanOrEqual(60);
185
+ });
186
+
187
+ it('handles a skills directory that does not exist', () => {
188
+ const nonExistentDir = join(skillsDir, 'does-not-exist');
189
+ const result = validateSkillDocs(nonExistentDir);
190
+ expect(result.valid).toBe(true);
191
+ expect(result.totalFiles).toBe(0);
192
+ expect(result.errors).toHaveLength(0);
193
+ });
194
+
195
+ it('counts multiple skill files correctly', () => {
196
+ addSkill(
197
+ skillsDir,
198
+ 'skill-one',
199
+ `# Skill One\n\n\`\`\`\nYOUR_AGENT_core op:capture_quick params: { title: "Test", content: "Content" }\n\`\`\`\n`,
200
+ );
201
+ addSkill(skillsDir, 'skill-two', `# Skill Two\n\nNo examples here.\n`);
202
+
203
+ const result = validateSkillDocs(skillsDir);
204
+ expect(result.totalFiles).toBe(2);
205
+ });
206
+ });