@soleri/core 9.15.0 → 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 (288) 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/capabilities/chain-mapping.d.ts.map +1 -1
  8. package/dist/capabilities/chain-mapping.js +5 -4
  9. package/dist/capabilities/chain-mapping.js.map +1 -1
  10. package/dist/capabilities/registry.d.ts +6 -0
  11. package/dist/capabilities/registry.d.ts.map +1 -1
  12. package/dist/capabilities/registry.js +3 -2
  13. package/dist/capabilities/registry.js.map +1 -1
  14. package/dist/context/context-engine.js +1 -1
  15. package/dist/context/context-engine.js.map +1 -1
  16. package/dist/engine/core-ops.d.ts.map +1 -1
  17. package/dist/engine/core-ops.js +38 -1
  18. package/dist/engine/core-ops.js.map +1 -1
  19. package/dist/flows/epilogue.d.ts +5 -1
  20. package/dist/flows/epilogue.d.ts.map +1 -1
  21. package/dist/flows/epilogue.js +11 -3
  22. package/dist/flows/epilogue.js.map +1 -1
  23. package/dist/flows/executor.d.ts.map +1 -1
  24. package/dist/flows/executor.js +13 -5
  25. package/dist/flows/executor.js.map +1 -1
  26. package/dist/flows/index.d.ts +1 -2
  27. package/dist/flows/index.d.ts.map +1 -1
  28. package/dist/flows/index.js +1 -0
  29. package/dist/flows/index.js.map +1 -1
  30. package/dist/flows/plan-builder.d.ts +17 -1
  31. package/dist/flows/plan-builder.d.ts.map +1 -1
  32. package/dist/flows/plan-builder.js +67 -6
  33. package/dist/flows/plan-builder.js.map +1 -1
  34. package/dist/flows/probes.d.ts +1 -1
  35. package/dist/flows/probes.d.ts.map +1 -1
  36. package/dist/flows/probes.js +15 -3
  37. package/dist/flows/probes.js.map +1 -1
  38. package/dist/flows/types.d.ts +31 -4
  39. package/dist/flows/types.d.ts.map +1 -1
  40. package/dist/flows/types.js +6 -1
  41. package/dist/flows/types.js.map +1 -1
  42. package/dist/index.d.ts +8 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +7 -0
  45. package/dist/index.js.map +1 -1
  46. package/dist/packs/pack-installer.d.ts.map +1 -1
  47. package/dist/packs/pack-installer.js +28 -2
  48. package/dist/packs/pack-installer.js.map +1 -1
  49. package/dist/planning/planner-types.d.ts +2 -0
  50. package/dist/planning/planner-types.d.ts.map +1 -1
  51. package/dist/planning/planner.d.ts +1 -0
  52. package/dist/planning/planner.d.ts.map +1 -1
  53. package/dist/planning/planner.js +7 -0
  54. package/dist/planning/planner.js.map +1 -1
  55. package/dist/playbooks/playbook-executor.d.ts +10 -1
  56. package/dist/playbooks/playbook-executor.d.ts.map +1 -1
  57. package/dist/playbooks/playbook-executor.js +8 -2
  58. package/dist/playbooks/playbook-executor.js.map +1 -1
  59. package/dist/playbooks/playbook-types.d.ts +8 -0
  60. package/dist/playbooks/playbook-types.d.ts.map +1 -1
  61. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  62. package/dist/runtime/admin-extra-ops.js +30 -0
  63. package/dist/runtime/admin-extra-ops.js.map +1 -1
  64. package/dist/runtime/admin-ops.d.ts.map +1 -1
  65. package/dist/runtime/admin-ops.js +60 -21
  66. package/dist/runtime/admin-ops.js.map +1 -1
  67. package/dist/runtime/admin-setup-ops.d.ts +11 -0
  68. package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
  69. package/dist/runtime/admin-setup-ops.js +87 -17
  70. package/dist/runtime/admin-setup-ops.js.map +1 -1
  71. package/dist/runtime/capture-ops.d.ts.map +1 -1
  72. package/dist/runtime/capture-ops.js +38 -12
  73. package/dist/runtime/capture-ops.js.map +1 -1
  74. package/dist/runtime/facades/brain-facade.d.ts.map +1 -1
  75. package/dist/runtime/facades/brain-facade.js +16 -4
  76. package/dist/runtime/facades/brain-facade.js.map +1 -1
  77. package/dist/runtime/facades/context-facade.d.ts.map +1 -1
  78. package/dist/runtime/facades/context-facade.js +9 -3
  79. package/dist/runtime/facades/context-facade.js.map +1 -1
  80. package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
  81. package/dist/runtime/facades/memory-facade.js +20 -7
  82. package/dist/runtime/facades/memory-facade.js.map +1 -1
  83. package/dist/runtime/facades/orchestrate-facade.d.ts.map +1 -1
  84. package/dist/runtime/facades/orchestrate-facade.js +12 -0
  85. package/dist/runtime/facades/orchestrate-facade.js.map +1 -1
  86. package/dist/runtime/facades/plan-facade.d.ts.map +1 -1
  87. package/dist/runtime/facades/plan-facade.js +113 -4
  88. package/dist/runtime/facades/plan-facade.js.map +1 -1
  89. package/dist/runtime/facades/vault-facade.d.ts.map +1 -1
  90. package/dist/runtime/facades/vault-facade.js +24 -3
  91. package/dist/runtime/facades/vault-facade.js.map +1 -1
  92. package/dist/runtime/orchestrate-ops.d.ts +21 -0
  93. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  94. package/dist/runtime/orchestrate-ops.js +132 -38
  95. package/dist/runtime/orchestrate-ops.js.map +1 -1
  96. package/dist/runtime/schema-helpers.d.ts.map +1 -1
  97. package/dist/runtime/schema-helpers.js +4 -0
  98. package/dist/runtime/schema-helpers.js.map +1 -1
  99. package/dist/runtime/vault-linking-ops.d.ts.map +1 -1
  100. package/dist/runtime/vault-linking-ops.js +16 -3
  101. package/dist/runtime/vault-linking-ops.js.map +1 -1
  102. package/dist/scheduler/cron-validator.d.ts +15 -0
  103. package/dist/scheduler/cron-validator.d.ts.map +1 -0
  104. package/dist/scheduler/cron-validator.js +93 -0
  105. package/dist/scheduler/cron-validator.js.map +1 -0
  106. package/dist/scheduler/platform-linux.d.ts +14 -0
  107. package/dist/scheduler/platform-linux.d.ts.map +1 -0
  108. package/dist/scheduler/platform-linux.js +107 -0
  109. package/dist/scheduler/platform-linux.js.map +1 -0
  110. package/dist/scheduler/platform-macos.d.ts +15 -0
  111. package/dist/scheduler/platform-macos.d.ts.map +1 -0
  112. package/dist/scheduler/platform-macos.js +131 -0
  113. package/dist/scheduler/platform-macos.js.map +1 -0
  114. package/dist/scheduler/scheduler-ops.d.ts +14 -0
  115. package/dist/scheduler/scheduler-ops.d.ts.map +1 -0
  116. package/dist/scheduler/scheduler-ops.js +77 -0
  117. package/dist/scheduler/scheduler-ops.js.map +1 -0
  118. package/dist/scheduler/scheduler.d.ts +55 -0
  119. package/dist/scheduler/scheduler.d.ts.map +1 -0
  120. package/dist/scheduler/scheduler.js +144 -0
  121. package/dist/scheduler/scheduler.js.map +1 -0
  122. package/dist/scheduler/types.d.ts +48 -0
  123. package/dist/scheduler/types.d.ts.map +1 -0
  124. package/dist/scheduler/types.js +6 -0
  125. package/dist/scheduler/types.js.map +1 -0
  126. package/dist/skills/sync-skills.d.ts +11 -0
  127. package/dist/skills/sync-skills.d.ts.map +1 -1
  128. package/dist/skills/sync-skills.js +132 -38
  129. package/dist/skills/sync-skills.js.map +1 -1
  130. package/dist/utils/worktree-reaper.d.ts +38 -0
  131. package/dist/utils/worktree-reaper.d.ts.map +1 -0
  132. package/dist/utils/worktree-reaper.js +85 -0
  133. package/dist/utils/worktree-reaper.js.map +1 -0
  134. package/dist/vault/scope-detector.d.ts.map +1 -1
  135. package/dist/vault/scope-detector.js +37 -4
  136. package/dist/vault/scope-detector.js.map +1 -1
  137. package/dist/vault/vault-entries.d.ts.map +1 -1
  138. package/dist/vault/vault-entries.js +3 -1
  139. package/dist/vault/vault-entries.js.map +1 -1
  140. package/package.json +1 -1
  141. package/src/agency/agency-manager.test.ts +4 -4
  142. package/src/agency/default-rules.test.ts +0 -13
  143. package/src/brain/brain-intelligence.test.ts +0 -5
  144. package/src/brain/second-brain-features.test.ts +2 -14
  145. package/src/capabilities/chain-mapping.test.ts +1 -6
  146. package/src/capabilities/chain-mapping.ts +6 -4
  147. package/src/capabilities/registry.test.ts +1 -1
  148. package/src/capabilities/registry.ts +9 -2
  149. package/src/chat/agent-loop.test.ts +1 -1
  150. package/src/chat/chat-enhanced.test.ts +0 -8
  151. package/src/claudemd/compose.test.ts +0 -5
  152. package/src/context/context-engine.test.ts +0 -1
  153. package/src/context/context-engine.ts +1 -1
  154. package/src/control/intent-router.test.ts +2 -2
  155. package/src/curator/tag-manager.test.ts +0 -4
  156. package/src/domain-packs/types.test.ts +0 -5
  157. package/src/dream/dream.test.ts +0 -7
  158. package/src/enforcement/registry.test.ts +2 -2
  159. package/src/engine/core-ops.test.ts +4 -22
  160. package/src/engine/core-ops.ts +36 -1
  161. package/src/engine/module-manifest.test.ts +1 -31
  162. package/src/engine/register-engine.test.ts +3 -33
  163. package/src/errors/retry.test.ts +3 -1
  164. package/src/flows/chain-runner.test.ts +0 -6
  165. package/src/flows/context-router.test.ts +3 -3
  166. package/src/flows/epilogue.test.ts +40 -2
  167. package/src/flows/epilogue.ts +11 -2
  168. package/src/flows/executor.test.ts +48 -2
  169. package/src/flows/executor.ts +15 -5
  170. package/src/flows/index.ts +1 -3
  171. package/src/flows/plan-builder.test.ts +201 -0
  172. package/src/flows/plan-builder.ts +81 -5
  173. package/src/flows/probes.ts +17 -3
  174. package/src/flows/types.ts +31 -2
  175. package/src/health/health-registry.test.ts +3 -1
  176. package/src/index.ts +17 -0
  177. package/src/intake/dedup-gate.test.ts +2 -6
  178. package/src/intake/text-ingester.test.ts +3 -4
  179. package/src/llm/llm-client.test.ts +1 -1
  180. package/src/llm/utils.test.ts +1 -1
  181. package/src/migrations/migration-runner.test.ts +0 -1
  182. package/src/operator/operator-context-store.test.ts +0 -13
  183. package/src/operator/operator-profile.test.ts +2 -20
  184. package/src/packs/pack-installer.ts +28 -2
  185. package/src/packs/pack-system.test.ts +2 -2
  186. package/src/persona/defaults.test.ts +19 -19
  187. package/src/planning/gap-passes.test.ts +0 -46
  188. package/src/planning/gap-patterns.test.ts +0 -42
  189. package/src/planning/goal-ancestry.test.ts +3 -1
  190. package/src/planning/plan-lifecycle.test.ts +15 -7
  191. package/src/planning/planner-types.ts +2 -0
  192. package/src/planning/planner.ts +8 -0
  193. package/src/planning/reconciliation-engine.test.ts +3 -10
  194. package/src/planning/task-complexity-assessor.test.ts +0 -5
  195. package/src/planning/task-verifier.test.ts +3 -1
  196. package/src/playbooks/generic/generic-playbooks.test.ts +0 -28
  197. package/src/playbooks/index.test.ts +0 -55
  198. package/src/playbooks/playbook-executor.test.ts +76 -0
  199. package/src/playbooks/playbook-executor.ts +24 -3
  200. package/src/playbooks/playbook-types.ts +8 -0
  201. package/src/plugins/plugin-registry.test.ts +6 -2
  202. package/src/project/project-registry.test.ts +2 -0
  203. package/src/queue/async-infrastructure.test.ts +6 -4
  204. package/src/queue/job-queue.test.ts +13 -7
  205. package/src/runtime/admin-extra-ops.test.ts +35 -30
  206. package/src/runtime/admin-extra-ops.ts +30 -0
  207. package/src/runtime/admin-ops.test.ts +0 -4
  208. package/src/runtime/admin-ops.ts +63 -21
  209. package/src/runtime/admin-setup-ops.test.ts +185 -13
  210. package/src/runtime/admin-setup-ops.ts +86 -16
  211. package/src/runtime/archive-ops.test.ts +0 -28
  212. package/src/runtime/branching-ops.test.ts +0 -17
  213. package/src/runtime/capture-ops.test.ts +41 -16
  214. package/src/runtime/capture-ops.ts +78 -46
  215. package/src/runtime/chain-ops.test.ts +0 -21
  216. package/src/runtime/facades/admin-facade.test.ts +0 -34
  217. package/src/runtime/facades/agency-facade.test.ts +0 -39
  218. package/src/runtime/facades/archive-facade.test.ts +0 -43
  219. package/src/runtime/facades/brain-facade.test.ts +8 -99
  220. package/src/runtime/facades/brain-facade.ts +29 -12
  221. package/src/runtime/facades/branching-facade.test.ts +30 -17
  222. package/src/runtime/facades/chat-facade.test.ts +0 -91
  223. package/src/runtime/facades/chat-service-ops.test.ts +0 -24
  224. package/src/runtime/facades/chat-session-ops.test.ts +0 -12
  225. package/src/runtime/facades/chat-transport-ops.test.ts +0 -23
  226. package/src/runtime/facades/context-facade.test.ts +0 -17
  227. package/src/runtime/facades/context-facade.ts +11 -4
  228. package/src/runtime/facades/control-facade.test.ts +0 -30
  229. package/src/runtime/facades/curator-facade.test.ts +0 -33
  230. package/src/runtime/facades/intake-facade.test.ts +0 -33
  231. package/src/runtime/facades/links-facade.test.ts +0 -37
  232. package/src/runtime/facades/loop-facade.test.ts +0 -26
  233. package/src/runtime/facades/memory-facade.test.ts +0 -18
  234. package/src/runtime/facades/memory-facade.ts +27 -11
  235. package/src/runtime/facades/operator-facade.test.ts +0 -31
  236. package/src/runtime/facades/orchestrate-facade.test.ts +0 -21
  237. package/src/runtime/facades/orchestrate-facade.ts +12 -0
  238. package/src/runtime/facades/plan-facade.test.ts +7 -32
  239. package/src/runtime/facades/plan-facade.ts +137 -4
  240. package/src/runtime/facades/review-facade.test.ts +1 -49
  241. package/src/runtime/facades/sync-facade.test.ts +24 -41
  242. package/src/runtime/facades/tier-facade.test.ts +30 -22
  243. package/src/runtime/facades/vault-facade.test.ts +0 -41
  244. package/src/runtime/facades/vault-facade.ts +26 -3
  245. package/src/runtime/grading-ops.test.ts +0 -27
  246. package/src/runtime/intake-ops.test.ts +0 -19
  247. package/src/runtime/loop-ops.test.ts +0 -48
  248. package/src/runtime/memory-cross-project-ops.test.ts +0 -14
  249. package/src/runtime/memory-extra-ops.test.ts +4 -8
  250. package/src/runtime/orchestrate-ops.test.ts +238 -19
  251. package/src/runtime/orchestrate-ops.ts +166 -41
  252. package/src/runtime/pack-ops.test.ts +0 -26
  253. package/src/runtime/planning-extra-ops.test.ts +2 -14
  254. package/src/runtime/playbook-ops-execution.test.ts +9 -20
  255. package/src/runtime/playbook-ops.test.ts +4 -67
  256. package/src/runtime/review-ops.test.ts +0 -15
  257. package/src/runtime/schema-helpers.ts +4 -0
  258. package/src/runtime/sync-ops.test.ts +0 -18
  259. package/src/runtime/tier-ops.test.ts +0 -21
  260. package/src/runtime/vault-extra-ops.test.ts +0 -12
  261. package/src/runtime/vault-linking-ops.test.ts +0 -4
  262. package/src/runtime/vault-linking-ops.ts +26 -8
  263. package/src/runtime/vault-sharing-ops.test.ts +0 -9
  264. package/src/scheduler/cron-validator.ts +101 -0
  265. package/src/scheduler/platform-linux.ts +122 -0
  266. package/src/scheduler/platform-macos.ts +150 -0
  267. package/src/scheduler/scheduler-ops.ts +77 -0
  268. package/src/scheduler/scheduler.test.ts +247 -0
  269. package/src/scheduler/scheduler.ts +174 -0
  270. package/src/scheduler/types.ts +52 -0
  271. package/src/skills/__tests__/sync-skills.test.ts +6 -17
  272. package/src/skills/global-claude-md.test.ts +113 -0
  273. package/src/skills/sync-skills.ts +143 -35
  274. package/src/skills/validate-skills.test.ts +12 -11
  275. package/src/telemetry/telemetry.test.ts +1 -0
  276. package/src/transport/http-server.test.ts +3 -0
  277. package/src/transport/session-manager.test.ts +3 -1
  278. package/src/transport/token-auth.test.ts +6 -9
  279. package/src/transport/ws-server.test.ts +10 -2
  280. package/src/utils/worktree-reaper.ts +113 -0
  281. package/src/vault/__tests__/vault-characterization.test.ts +0 -108
  282. package/src/vault/linking.test.ts +0 -2
  283. package/src/vault/playbook.test.ts +4 -1
  284. package/src/vault/scope-detector.test.ts +3 -1
  285. package/src/vault/scope-detector.ts +42 -4
  286. package/src/vault/vault-connect.test.ts +1 -1
  287. package/src/vault/vault-entries.ts +3 -1
  288. package/src/vault/vault.test.ts +23 -8
@@ -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
  // =============================================================================
@@ -147,15 +147,15 @@ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", doma
147
147
 
148
148
  const result = validateSkillDocs(skillsDir);
149
149
 
150
- if (result.errors.length > 0) {
151
- const err = result.errors[0];
152
- expect(err).toHaveProperty('file');
153
- expect(err).toHaveProperty('op');
154
- expect(err).toHaveProperty('message');
155
- expect(typeof err.file).toBe('string');
156
- expect(typeof err.op).toBe('string');
157
- expect(typeof err.message).toBe('string');
158
- }
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
159
  });
160
160
 
161
161
  it('includes the file path and op name in each error', () => {
@@ -178,9 +178,10 @@ YOUR_AGENT_core op:capture_knowledge params: { entries: [{ type: "pattern", doma
178
178
  expect(err.op).toBe('capture_knowledge');
179
179
  });
180
180
 
181
- it('builds a non-empty schema registry', () => {
181
+ it('builds a schema registry covering core ops', () => {
182
182
  const result = validateSkillDocs(skillsDir);
183
- expect(result.registrySize).toBeGreaterThan(50);
183
+ // Registry must cover: capture_knowledge, capture_quick, create_plan, approve_plan, etc.
184
+ expect(result.registrySize).toBeGreaterThanOrEqual(60);
184
185
  });
185
186
 
186
187
  it('handles a skills directory that does not exist', () => {
@@ -36,6 +36,7 @@ describe('Telemetry', () => {
36
36
  expect(stats.callsByOp).toEqual({});
37
37
  expect(stats.errorsByOp).toEqual({});
38
38
  expect(stats.slowestOps).toEqual([]);
39
+ expect(typeof stats.since).toBe('number');
39
40
  expect(stats.since).toBeLessThanOrEqual(Date.now());
40
41
  });
41
42
 
@@ -128,7 +128,10 @@ describe('HttpMcpServer', () => {
128
128
  it('starts and stops without error', async () => {
129
129
  await server.start();
130
130
  const stats = server.getStats();
131
+ expect(typeof stats.uptime).toBe('number');
132
+ // uptime is elapsed ms since start — should be a small non-negative number
131
133
  expect(stats.uptime).toBeGreaterThanOrEqual(0);
134
+ expect(stats.uptime).toBeLessThan(5000); // must have completed in under 5s
132
135
  await server.stop();
133
136
  });
134
137
 
@@ -30,11 +30,13 @@ describe('SessionManager', () => {
30
30
 
31
31
  describe('add / get / remove', () => {
32
32
  it('adds and retrieves a session', () => {
33
+ const before = Date.now();
33
34
  const session = manager.add('s1', 'transport', 'server');
34
35
  expect(session.id).toBe('s1');
35
36
  expect(session.transport).toBe('transport');
36
37
  expect(session.server).toBe('server');
37
- expect(session.createdAt).toBeGreaterThan(0);
38
+ expect(session.createdAt).toBeGreaterThanOrEqual(before);
39
+ expect(session.createdAt).toBeLessThanOrEqual(Date.now());
38
40
  expect(manager.get('s1')).toBe(session);
39
41
  });
40
42
 
@@ -121,16 +121,13 @@ describe('loadToken / saveToken / getOrGenerateToken', () => {
121
121
  expect(result).toBe('trimmed-token');
122
122
  });
123
123
 
124
- it('loadToken returns undefined for empty env var', () => {
124
+ it('skips whitespace-only env var and does not return the raw whitespace value', () => {
125
125
  vi.stubEnv('MY_AGENT_HTTP_TOKEN', ' ');
126
+ // env var is whitespace-only — function must skip it and NOT return whitespace
126
127
  const result = loadToken('my-agent');
127
- // Falls through to file-based lookup
128
- expect(result === undefined || typeof result === 'string').toBe(true);
129
- });
130
-
131
- it('generateToken produces different tokens each call', () => {
132
- const t1 = generateToken();
133
- const t2 = generateToken();
134
- expect(t1).not.toBe(t2);
128
+ expect(result).not.toBe(' ');
129
+ if (result !== undefined) {
130
+ expect(result.trim().length).toBeGreaterThan(0);
131
+ }
135
132
  });
136
133
  });
@@ -124,7 +124,11 @@ describe('WsMcpServer', () => {
124
124
  describe('standalone start / stop', () => {
125
125
  it('starts and stops without error', async () => {
126
126
  await server.start(0);
127
- expect(server.getStats().uptime).toBeGreaterThanOrEqual(0);
127
+ const uptime = server.getStats().uptime;
128
+ expect(typeof uptime).toBe('number');
129
+ // uptime is elapsed ms since start — should be a small non-negative number
130
+ expect(uptime).toBeGreaterThanOrEqual(0);
131
+ expect(uptime).toBeLessThan(5000); // must have started in under 5s
128
132
  await server.stop();
129
133
  });
130
134
 
@@ -145,7 +149,11 @@ describe('WsMcpServer', () => {
145
149
  callbacks,
146
150
  );
147
151
  wsServer.attachTo(httpServer);
148
- expect(wsServer.getStats().uptime).toBeGreaterThanOrEqual(0);
152
+ const attachUptime = wsServer.getStats().uptime;
153
+ expect(typeof attachUptime).toBe('number');
154
+ // uptime is elapsed ms since attach — valid non-negative number
155
+ expect(attachUptime).toBeGreaterThanOrEqual(0);
156
+ expect(attachUptime).toBeLessThan(5000);
149
157
 
150
158
  await wsServer.stop();
151
159
  await new Promise<void>((resolve, reject) => {
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Worktree reaper — cleans up stale .claude/worktrees/ entries left by subagent execution.
3
+ *
4
+ * Claude Code creates worktrees via `isolation: "worktree"` for parallel subagent runs.
5
+ * If the agent commits changes, the worktree persists — nobody reaps it automatically.
6
+ *
7
+ * Usage: call worktreeReap() at session start and after plan completion (best-effort).
8
+ */
9
+
10
+ import { spawnSync } from 'node:child_process';
11
+ import { existsSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+
14
+ export interface ReapReport {
15
+ /** Number of worktrees successfully reaped */
16
+ reaped: number;
17
+ /** Paths of stale worktrees found */
18
+ found: string[];
19
+ /** Any errors encountered (non-fatal) */
20
+ errors: string[];
21
+ /** Whether git worktree prune ran successfully */
22
+ pruned: boolean;
23
+ }
24
+
25
+ export interface WorktreeStatus {
26
+ /** All .claude/worktrees/ entries found */
27
+ stale: Array<{ path: string; branch: string; commit: string }>;
28
+ /** Total count */
29
+ total: number;
30
+ }
31
+
32
+ /**
33
+ * Parse `git worktree list --porcelain` output into structured entries.
34
+ */
35
+ function parseWorktreeList(
36
+ output: string,
37
+ ): Array<{ path: string; branch: string; commit: string }> {
38
+ const entries: Array<{ path: string; branch: string; commit: string }> = [];
39
+ const blocks = output.trim().split(/\n\n+/);
40
+
41
+ for (const block of blocks) {
42
+ const lines = block.trim().split('\n');
43
+ let path = '';
44
+ let branch = '';
45
+ let commit = '';
46
+
47
+ for (const line of lines) {
48
+ if (line.startsWith('worktree ')) path = line.slice(9).trim();
49
+ else if (line.startsWith('HEAD ')) commit = line.slice(5).trim();
50
+ else if (line.startsWith('branch ')) branch = line.slice(7).trim();
51
+ }
52
+
53
+ if (path) entries.push({ path, branch, commit });
54
+ }
55
+
56
+ return entries;
57
+ }
58
+
59
+ /**
60
+ * Get status of stale worktrees under .claude/worktrees/ without removing them.
61
+ */
62
+ export function worktreeStatus(projectPath: string): WorktreeStatus {
63
+ const result = spawnSync('git', ['worktree', 'list', '--porcelain'], {
64
+ cwd: projectPath,
65
+ encoding: 'utf-8',
66
+ });
67
+
68
+ if (result.status !== 0 || !result.stdout) {
69
+ return { stale: [], total: 0 };
70
+ }
71
+
72
+ const all = parseWorktreeList(result.stdout);
73
+ const worktreeBase = join(projectPath, '.claude', 'worktrees');
74
+ const stale = all.filter((e) => e.path.startsWith(worktreeBase) && existsSync(e.path));
75
+
76
+ return { stale, total: stale.length };
77
+ }
78
+
79
+ /**
80
+ * Reap stale worktrees under .claude/worktrees/.
81
+ * Best-effort — errors are collected but never thrown.
82
+ */
83
+ export function worktreeReap(projectPath: string): ReapReport {
84
+ const report: ReapReport = { reaped: 0, found: [], errors: [], pruned: false };
85
+
86
+ try {
87
+ const { stale } = worktreeStatus(projectPath);
88
+
89
+ for (const { path } of stale) {
90
+ report.found.push(path);
91
+ const rm = spawnSync('git', ['worktree', 'remove', '--force', path], {
92
+ cwd: projectPath,
93
+ encoding: 'utf-8',
94
+ });
95
+ if (rm.status === 0) {
96
+ report.reaped++;
97
+ } else {
98
+ report.errors.push(`Failed to remove ${path}: ${rm.stderr?.trim() ?? 'unknown error'}`);
99
+ }
100
+ }
101
+
102
+ // Prune dangling refs
103
+ const prune = spawnSync('git', ['worktree', 'prune'], {
104
+ cwd: projectPath,
105
+ encoding: 'utf-8',
106
+ });
107
+ report.pruned = prune.status === 0;
108
+ } catch (err) {
109
+ report.errors.push(err instanceof Error ? err.message : String(err));
110
+ }
111
+
112
+ return report;
113
+ }