@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
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { createAdminSetupOps } from './admin-setup-ops.js';
2
+ import { createAdminSetupOps, syncHooksToClaudeSettings } from './admin-setup-ops.js';
3
3
  import type { AgentRuntime } from './types.js';
4
4
  import type { OpDefinition } from '../facades/types.js';
5
5
 
@@ -46,6 +46,11 @@ vi.mock('./claude-md-helpers.js', () => ({
46
46
  injectEngineRulesBlock: vi.fn((content: string) => content),
47
47
  }));
48
48
 
49
+ vi.mock('../paths.js', () => ({
50
+ agentPlansPath: vi.fn(() => '/mock-home/.soleri/test-agent/plans.json'),
51
+ agentVaultPath: vi.fn(() => '/mock-home/.soleri/test-agent/vault.db'),
52
+ }));
53
+
49
54
  vi.mock('../skills/sync-skills.js', () => ({
50
55
  discoverSkills: vi.fn(() => [{ name: 'skill-1', path: '/mock/skills/skill-1' }]),
51
56
  syncSkillsToClaudeCode: vi.fn(() => ({
@@ -101,18 +106,6 @@ describe('createAdminSetupOps', () => {
101
106
  ops = createAdminSetupOps(runtime);
102
107
  });
103
108
 
104
- it('returns 4 ops', () => {
105
- expect(ops).toHaveLength(4);
106
- });
107
-
108
- it('all ops have required fields', () => {
109
- for (const op of ops) {
110
- expect(op.name).toBeTruthy();
111
- expect(op.handler).toBeDefined();
112
- expect(['read', 'write', 'admin']).toContain(op.auth);
113
- }
114
- });
115
-
116
109
  describe('admin_inject_claude_md', () => {
117
110
  it('returns error when CLAUDE.md not found and createIfMissing is false', async () => {
118
111
  const result = (await findOp(ops, 'admin_inject_claude_md').handler({
@@ -290,6 +283,190 @@ describe('createAdminSetupOps', () => {
290
283
  });
291
284
  });
292
285
 
286
+ describe('syncHooksToClaudeSettings', () => {
287
+ it('installs SessionStart, PreCompact, and Stop hooks on fresh settings', () => {
288
+ const result = syncHooksToClaudeSettings('test-agent');
289
+ const written = mockFs['/mock-home/.claude/settings.json'];
290
+ expect(written).toBeDefined();
291
+ const settings = JSON.parse(written);
292
+ expect(settings.hooks.SessionStart).toHaveLength(2);
293
+ expect(settings.hooks.PreCompact).toHaveLength(1);
294
+ expect(settings.hooks.Stop).toHaveLength(1);
295
+ expect(result.installed).toContain('SessionStart');
296
+ expect(result.installed).toContain('PreCompact');
297
+ expect(result.installed).toContain('Stop');
298
+ expect(result.updated).toHaveLength(0);
299
+ expect(result.skipped).toHaveLength(0);
300
+ expect(result.error).toBeUndefined();
301
+ });
302
+
303
+ it('includes the {agentId}-mode skill hook in SessionStart', () => {
304
+ syncHooksToClaudeSettings('test-agent');
305
+ const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
306
+ const commands = settings.hooks.SessionStart.flatMap((g: { hooks: { command?: string }[] }) =>
307
+ g.hooks.map((h) => h.command),
308
+ );
309
+ expect(commands.some((c: string) => c.includes('test-agent-mode skill'))).toBe(true);
310
+ });
311
+
312
+ it('includes the admin_health hook in SessionStart', () => {
313
+ syncHooksToClaudeSettings('test-agent');
314
+ const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
315
+ const commands = settings.hooks.SessionStart.flatMap((g: { hooks: { command?: string }[] }) =>
316
+ g.hooks.map((h) => h.command),
317
+ );
318
+ expect(commands.some((c: string) => c.includes('admin_health'))).toBe(true);
319
+ });
320
+
321
+ it('is idempotent — running twice produces the same output', () => {
322
+ syncHooksToClaudeSettings('test-agent');
323
+ const after1 = mockFs['/mock-home/.claude/settings.json'];
324
+ const result2 = syncHooksToClaudeSettings('test-agent');
325
+ const after2 = mockFs['/mock-home/.claude/settings.json'];
326
+ expect(after1).toBe(after2);
327
+ expect(result2.skipped).toContain('SessionStart');
328
+ expect(result2.installed).toHaveLength(0);
329
+ expect(result2.updated).toHaveLength(0);
330
+ });
331
+
332
+ it('preserves non-agent hooks already in settings', () => {
333
+ mockFs['/mock-home/.claude/settings.json'] = JSON.stringify({
334
+ hooks: {
335
+ SessionStart: [{ hooks: [{ type: 'command', command: 'echo existing' }] }],
336
+ },
337
+ });
338
+ syncHooksToClaudeSettings('test-agent');
339
+ const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
340
+ const commands = settings.hooks.SessionStart.flatMap((g: { hooks: { command?: string }[] }) =>
341
+ g.hooks.map((h) => h.command),
342
+ );
343
+ expect(commands).toContain('echo existing');
344
+ expect(commands.some((c: string) => c.includes('admin_health'))).toBe(true);
345
+ });
346
+
347
+ it('updates stale agent hooks to match current defaults', () => {
348
+ // A stale hook contains the agent marker but outdated content
349
+ mockFs['/mock-home/.claude/settings.json'] = JSON.stringify({
350
+ hooks: {
351
+ SessionStart: [
352
+ {
353
+ hooks: [
354
+ {
355
+ type: 'command',
356
+ command: `root=$(git rev-parse --show-toplevel 2>/dev/null || echo "."); if grep -q '"test-agent"' "$root/.mcp.json" 2>/dev/null; then echo 'Call mcp__test-agent__test-agent_admin op:OLD_STALE_OP'; fi`,
357
+ timeout: 5000,
358
+ },
359
+ ],
360
+ },
361
+ ],
362
+ },
363
+ });
364
+ const result = syncHooksToClaudeSettings('test-agent');
365
+ const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
366
+ const commands = settings.hooks.SessionStart.flatMap((g: { hooks: { command?: string }[] }) =>
367
+ g.hooks.map((h) => h.command),
368
+ );
369
+ expect(commands.some((c: string) => c.includes('OLD_STALE_OP'))).toBe(false);
370
+ expect(commands.some((c: string) => c.includes('admin_health'))).toBe(true);
371
+ expect(result.updated).toContain('SessionStart');
372
+ expect(result.error).toBeUndefined();
373
+ });
374
+
375
+ it('returns error field when write fails', async () => {
376
+ const { writeFileSync } = await import('node:fs');
377
+ vi.mocked(writeFileSync).mockImplementationOnce(() => {
378
+ throw new Error('EACCES: permission denied');
379
+ });
380
+ const result = syncHooksToClaudeSettings('test-agent');
381
+ expect(result.error).toMatch(/EACCES/);
382
+ expect(result.installed).toHaveLength(0);
383
+ expect(result.updated).toHaveLength(0);
384
+ expect(result.skipped).toHaveLength(0);
385
+ });
386
+ });
387
+
388
+ describe('multi-agent hook coexistence', () => {
389
+ type HookGroup = { hooks: { command?: string }[] };
390
+
391
+ function getSessionStartCommands(): string[] {
392
+ const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
393
+ return (settings.hooks.SessionStart as HookGroup[]).flatMap((g) =>
394
+ g.hooks.map((h) => h.command ?? ''),
395
+ );
396
+ }
397
+
398
+ function getAgentCommands(agentId: string): string[] {
399
+ const settings = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
400
+ return (settings.hooks.SessionStart as HookGroup[])
401
+ .flatMap((g) => g.hooks)
402
+ .map((h) => h.command ?? '')
403
+ .filter((c) => c.includes(agentId));
404
+ }
405
+
406
+ it('install A then B — both sets present, no overlap', () => {
407
+ syncHooksToClaudeSettings('agent-a');
408
+ syncHooksToClaudeSettings('agent-b');
409
+
410
+ const commands = getSessionStartCommands();
411
+
412
+ // Both agents must have hooks present
413
+ expect(commands.some((c) => c.includes('agent-a'))).toBe(true);
414
+ expect(commands.some((c) => c.includes('agent-b'))).toBe(true);
415
+
416
+ // agent-a commands must not mention agent-b and vice versa
417
+ const aCommands = getAgentCommands('agent-a');
418
+ const bCommands = getAgentCommands('agent-b');
419
+
420
+ expect(aCommands.every((c) => !c.includes('agent-b'))).toBe(true);
421
+ expect(bCommands.every((c) => !c.includes('agent-a'))).toBe(true);
422
+ });
423
+
424
+ it('re-install A after B — B hooks untouched', () => {
425
+ syncHooksToClaudeSettings('agent-a');
426
+ syncHooksToClaudeSettings('agent-b');
427
+
428
+ const beforeB = getAgentCommands('agent-b');
429
+
430
+ syncHooksToClaudeSettings('agent-a'); // re-run A (e.g. after update)
431
+
432
+ const afterB = getAgentCommands('agent-b');
433
+
434
+ expect(afterB).toEqual(beforeB);
435
+ });
436
+
437
+ it('no duplicates after running both twice', () => {
438
+ syncHooksToClaudeSettings('agent-a');
439
+ syncHooksToClaudeSettings('agent-b');
440
+
441
+ const settings1 = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
442
+ const groupCountAfterTwo = (settings1.hooks.SessionStart as HookGroup[]).length;
443
+
444
+ syncHooksToClaudeSettings('agent-a');
445
+ syncHooksToClaudeSettings('agent-b');
446
+
447
+ const settings2 = JSON.parse(mockFs['/mock-home/.claude/settings.json']);
448
+ const groupCountAfterFour = (settings2.hooks.SessionStart as HookGroup[]).length;
449
+
450
+ expect(groupCountAfterFour).toBe(groupCountAfterTwo);
451
+ });
452
+
453
+ it('manually added non-agent hook survives all agent installs', () => {
454
+ // Pre-populate settings with a non-agent hook in SessionStart
455
+ mockFs['/mock-home/.claude/settings.json'] = JSON.stringify({
456
+ hooks: {
457
+ SessionStart: [{ hooks: [{ type: 'command', command: 'echo custom-non-agent-hook' }] }],
458
+ },
459
+ });
460
+
461
+ syncHooksToClaudeSettings('agent-a');
462
+ syncHooksToClaudeSettings('agent-b');
463
+ syncHooksToClaudeSettings('agent-a');
464
+
465
+ const commands = getSessionStartCommands();
466
+ expect(commands).toContain('echo custom-non-agent-hook');
467
+ });
468
+ });
469
+
293
470
  describe('admin_check_persistence', () => {
294
471
  it('returns NO_STORAGE_DIRECTORY when nothing exists', async () => {
295
472
  const result = (await findOp(ops, 'admin_check_persistence').handler({})) as Record<
@@ -330,5 +507,44 @@ describe('createAdminSetupOps', () => {
330
507
  expect(activePlans[0].status).toBe('executing');
331
508
  expect(result.recommendation).toContain('need attention');
332
509
  });
510
+
511
+ it('uses configured or resolved .soleri plan paths and understands planner stores', async () => {
512
+ runtime = {
513
+ ...createMockRuntime(),
514
+ config: {
515
+ agentId: 'test-agent',
516
+ dataDir: '/mock/agent-data',
517
+ agentDir: '/mock/agent-dir',
518
+ },
519
+ } as unknown as AgentRuntime;
520
+ ops = createAdminSetupOps(runtime);
521
+
522
+ mockDirs.add('/mock-home/.soleri/test-agent');
523
+ mockFs['/mock-home/.soleri/test-agent/vault.db'] = 'binary';
524
+ mockFs['/mock-home/.soleri/test-agent/plans.json'] = JSON.stringify({
525
+ version: '1.0',
526
+ plans: [
527
+ { id: 'plan-1', status: 'executing' },
528
+ { id: 'plan-2', status: 'completed' },
529
+ ],
530
+ });
531
+
532
+ const result = (await findOp(ops, 'admin_check_persistence').handler({})) as Record<
533
+ string,
534
+ unknown
535
+ >;
536
+
537
+ expect((result.storageDirectory as Record<string, unknown>).path).toBe(
538
+ '/mock-home/.soleri/test-agent',
539
+ );
540
+ expect(
541
+ ((result.files as Record<string, unknown>).plans as Record<string, unknown>).path,
542
+ ).toBe('/mock-home/.soleri/test-agent/plans.json');
543
+ expect(
544
+ ((result.files as Record<string, unknown>).plans as Record<string, unknown>).items,
545
+ ).toBe(2);
546
+ expect(result.status).toBe('PERSISTENCE_ACTIVE');
547
+ expect(result.activePlans).toEqual([{ id: 'plan-1', status: 'executing' }]);
548
+ });
333
549
  });
334
550
  });
@@ -27,6 +27,10 @@ import { join, resolve, dirname } from 'node:path';
27
27
  import { homedir } from 'node:os';
28
28
  import type { OpDefinition } from '../facades/types.js';
29
29
  import type { AgentRuntime } from './types.js';
30
+ import {
31
+ agentPlansPath as getAgentPlansPath,
32
+ agentVaultPath as getAgentVaultPath,
33
+ } from '../paths.js';
30
34
  import {
31
35
  hasSections,
32
36
  removeSections,
@@ -74,19 +78,63 @@ function getFileInfo(path: string): { exists: boolean; size: number; items: numb
74
78
  try {
75
79
  const stat = statSync(path);
76
80
  const content = JSON.parse(readFileSync(path, 'utf-8'));
77
- const items = content.items
78
- ? Object.keys(content.items).length
79
- : content.contexts
80
- ? content.contexts.length
81
- : Array.isArray(content)
82
- ? content.length
83
- : 0;
81
+ const items = countPersistedItems(content);
84
82
  return { exists: true, size: stat.size, items };
85
83
  } catch {
86
84
  return { exists: true, size: 0, items: -1 };
87
85
  }
88
86
  }
89
87
 
88
+ function countPersistedItems(content: unknown): number {
89
+ if (Array.isArray(content)) return content.length;
90
+ if (!content || typeof content !== 'object') return 0;
91
+
92
+ const data = content as Record<string, unknown>;
93
+ if (Array.isArray(data.plans)) return data.plans.length;
94
+ if (data.items && typeof data.items === 'object') return Object.keys(data.items).length;
95
+ if (Array.isArray(data.contexts)) return data.contexts.length;
96
+ return 0;
97
+ }
98
+
99
+ function extractActivePlans(content: unknown): Array<{ id: string; status: string }> {
100
+ if (!content || typeof content !== 'object') return [];
101
+
102
+ const plans = Array.isArray((content as Record<string, unknown>).plans)
103
+ ? ((content as Record<string, unknown>).plans as unknown[])
104
+ : null;
105
+ if (plans) {
106
+ return plans.flatMap((plan) => {
107
+ if (!plan || typeof plan !== 'object') return [];
108
+ const p = plan as Record<string, unknown>;
109
+ const id = typeof p.id === 'string' ? p.id : null;
110
+ const lifecycle =
111
+ typeof p.lifecycleStatus === 'string'
112
+ ? p.lifecycleStatus
113
+ : typeof p.status === 'string'
114
+ ? p.status
115
+ : null;
116
+ if (!id || (lifecycle !== 'executing' && lifecycle !== 'reconciling')) return [];
117
+ return [{ id, status: lifecycle }];
118
+ });
119
+ }
120
+
121
+ const items = (content as Record<string, unknown>).items;
122
+ if (!items || typeof items !== 'object') return [];
123
+
124
+ return Object.entries(items).flatMap(([id, plan]) => {
125
+ if (!plan || typeof plan !== 'object') return [];
126
+ const p = plan as Record<string, unknown>;
127
+ const lifecycle =
128
+ typeof p.lifecycleStatus === 'string'
129
+ ? p.lifecycleStatus
130
+ : typeof p.status === 'string'
131
+ ? p.status
132
+ : null;
133
+ if (lifecycle !== 'executing' && lifecycle !== 'reconciling') return [];
134
+ return [{ id, status: lifecycle }];
135
+ });
136
+ }
137
+
90
138
  /** Discover hookify rule files in a directory */
91
139
  function discoverHookifyFiles(dir: string): Array<{ name: string; path: string }> {
92
140
  if (!existsSync(dir)) return [];
@@ -97,6 +145,37 @@ function discoverHookifyFiles(dir: string): Array<{ name: string; path: string }
97
145
 
98
146
  // discoverSkills imported from '../skills/sync-skills.js'
99
147
 
148
+ // ─── Deep Equality Helper ─────────────────────────────────────────────
149
+
150
+ /** Recursively compare two values by structure, independent of key insertion order. */
151
+ function deepEqual(a: unknown, b: unknown): boolean {
152
+ if (a === b) return true;
153
+ if (a === null || b === null) return false;
154
+ if (typeof a !== typeof b) return false;
155
+ if (typeof a !== 'object') return false;
156
+
157
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
158
+
159
+ if (Array.isArray(a) && Array.isArray(b)) {
160
+ if (a.length !== b.length) return false;
161
+ for (let i = 0; i < a.length; i++) {
162
+ if (!deepEqual(a[i], b[i])) return false;
163
+ }
164
+ return true;
165
+ }
166
+
167
+ const objA = a as Record<string, unknown>;
168
+ const objB = b as Record<string, unknown>;
169
+ const keysA = Object.keys(objA);
170
+ const keysB = Object.keys(objB);
171
+ if (keysA.length !== keysB.length) return false;
172
+ for (const key of keysA) {
173
+ if (!Object.prototype.hasOwnProperty.call(objB, key)) return false;
174
+ if (!deepEqual(objA[key], objB[key])) return false;
175
+ }
176
+ return true;
177
+ }
178
+
100
179
  // ─── Settings.json Hook Merging ───────────────────────────────────────
101
180
 
102
181
  interface SettingsHook {
@@ -119,7 +198,7 @@ interface SettingsHookGroup {
119
198
  function buildConditionalHookCommand(agentId: string, instruction: string): string {
120
199
  // Escape single quotes in instruction for safe shell embedding
121
200
  const escaped = instruction.replace(/'/g, "'\\''");
122
- return `root=$(git rev-parse --show-toplevel 2>/dev/null || echo "."); if grep -q '"${agentId}"' "$root/.mcp.json" 2>/dev/null; then echo '${escaped}'; fi`;
201
+ return `root=$(git rev-parse --show-toplevel 2>/dev/null || echo "."); if grep -qF '"${agentId}"' "$root/.mcp.json" 2>/dev/null; then echo '${escaped}'; fi`;
123
202
  }
124
203
 
125
204
  /** Default lifecycle hooks for any Soleri agent */
@@ -141,6 +220,16 @@ function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookG
141
220
  },
142
221
  ],
143
222
  },
223
+ {
224
+ matcher: '',
225
+ hooks: [
226
+ {
227
+ type: 'command',
228
+ command: `echo 'SESSION_START: Invoke the ${agentId}-mode skill now to load full routing context and command reference.'`,
229
+ timeout: 5,
230
+ },
231
+ ],
232
+ },
144
233
  ],
145
234
  PreCompact: [
146
235
  {
@@ -178,8 +267,11 @@ function getDefaultLifecycleHooks(agentId: string): Record<string, SettingsHookG
178
267
  /** Check if a hook group belongs to this agent by inspecting prompts for the marker */
179
268
  function isAgentHookGroup(group: SettingsHookGroup, agentId: string): boolean {
180
269
  const marker = `mcp__${agentId}__${agentId}_`;
270
+ const skillMarker = `${agentId}-mode skill`;
181
271
  return group.hooks.some(
182
- (h) => (h.prompt && h.prompt.includes(marker)) || (h.command && h.command.includes(marker)),
272
+ (h) =>
273
+ (h.prompt && (h.prompt.includes(marker) || h.prompt.includes(skillMarker))) ||
274
+ (h.command && (h.command.includes(marker) || h.command.includes(skillMarker))),
183
275
  );
184
276
  }
185
277
 
@@ -206,29 +298,55 @@ function mergeSettingsHooks(
206
298
  continue;
207
299
  }
208
300
 
209
- // Check if agent group already exists
210
- const existingIdx = merged[event].findIndex((g) => isAgentHookGroup(g, agentId));
301
+ // Remove all existing agent-owned groups, keep non-agent hooks
302
+ const nonAgentGroups = merged[event].filter((g) => !isAgentHookGroup(g, agentId));
303
+ const existingAgentGroups = merged[event].filter((g) => isAgentHookGroup(g, agentId));
211
304
 
212
- if (existingIdx === -1) {
213
- // Append agent hooks (don't touch non-agent hooks)
214
- merged[event].push(...groups);
305
+ if (deepEqual(existingAgentGroups, groups)) {
306
+ skipped.push(event);
307
+ } else if (existingAgentGroups.length === 0) {
308
+ merged[event] = [...nonAgentGroups, ...groups];
215
309
  installed.push(event);
216
310
  } else {
217
- // Check if template matches
218
- const existing = JSON.stringify(merged[event][existingIdx]);
219
- const template = JSON.stringify(groups[0]);
220
- if (existing === template) {
221
- skipped.push(event);
222
- } else {
223
- merged[event][existingIdx] = groups[0];
224
- updated.push(event);
225
- }
311
+ // Replace all agent groups with current defaults
312
+ merged[event] = [...nonAgentGroups, ...groups];
313
+ updated.push(event);
226
314
  }
227
315
  }
228
316
 
229
317
  return { hooks: merged, installed, updated, skipped };
230
318
  }
231
319
 
320
+ /**
321
+ * Auto-sync lifecycle hooks into ~/.claude/settings.json at engine startup.
322
+ * Idempotent — skips hooks already present, updates stale ones.
323
+ * Returns a summary of what was installed, updated, or skipped.
324
+ */
325
+ export function syncHooksToClaudeSettings(agentId: string): {
326
+ installed: string[];
327
+ updated: string[];
328
+ skipped: string[];
329
+ error?: string;
330
+ } {
331
+ try {
332
+ const settings = readSettingsJson();
333
+ const currentHooks = (settings.hooks ?? {}) as Record<string, SettingsHookGroup[]>;
334
+ const { hooks, installed, updated, skipped } = mergeSettingsHooks(currentHooks, agentId);
335
+ if (installed.length > 0 || updated.length > 0) {
336
+ writeSettingsJson({ ...settings, hooks });
337
+ }
338
+ return { installed, updated, skipped };
339
+ } catch (err) {
340
+ // Non-fatal — hooks will be installed on next run or via admin_setup_global
341
+ return {
342
+ installed: [],
343
+ updated: [],
344
+ skipped: [],
345
+ error: err instanceof Error ? err.message : String(err),
346
+ };
347
+ }
348
+ }
349
+
232
350
  // ─── Op Definitions ───────────────────────────────────────────────────
233
351
 
234
352
  /**
@@ -621,15 +739,15 @@ export function createAdminSetupOps(runtime: AgentRuntime): OpDefinition[] {
621
739
  auth: 'read',
622
740
  handler: async () => {
623
741
  const { agentId, plansPath, vaultPath } = config;
624
- const storageDir = join(homedir(), `.${agentId}`);
742
+ const plansFile = plansPath ?? getAgentPlansPath(agentId);
743
+ const vaultFile = vaultPath ?? getAgentVaultPath(agentId);
744
+ const storageDir = dirname(plansFile);
625
745
  const storageDirExists = existsSync(storageDir);
626
746
 
627
747
  // Check plan storage
628
- const plansFile = plansPath ?? join(storageDir, 'plans.json');
629
748
  const plansInfo = getFileInfo(plansFile);
630
749
 
631
750
  // Check vault
632
- const vaultFile = vaultPath ?? join(storageDir, 'vault.db');
633
751
  const vaultExists = existsSync(vaultFile);
634
752
  let vaultSize = 0;
635
753
  if (vaultExists) {
@@ -655,16 +773,7 @@ export function createAdminSetupOps(runtime: AgentRuntime): OpDefinition[] {
655
773
  if (plansInfo.exists) {
656
774
  try {
657
775
  const plansData = JSON.parse(readFileSync(plansFile, 'utf-8'));
658
- const items = plansData.items ?? plansData;
659
- if (typeof items === 'object' && items !== null) {
660
- for (const [id, plan] of Object.entries(items)) {
661
- const p = plan as Record<string, unknown>;
662
- const lifecycle = (p.lifecycleStatus ?? p.status) as string | undefined;
663
- if (lifecycle === 'executing' || lifecycle === 'reconciling') {
664
- activePlans.push({ id, status: lifecycle });
665
- }
666
- }
667
- }
776
+ activePlans.push(...extractActivePlans(plansData));
668
777
  } catch {
669
778
  // Parse error — not critical
670
779
  }
@@ -83,34 +83,6 @@ describe('createArchiveOps', () => {
83
83
  ops = createArchiveOps(runtime);
84
84
  });
85
85
 
86
- it('returns 12 ops', () => {
87
- expect(ops).toHaveLength(12);
88
- });
89
-
90
- it('all ops have required fields', () => {
91
- for (const op of ops) {
92
- expect(op.name).toBeTruthy();
93
- expect(op.handler).toBeDefined();
94
- expect(['read', 'write', 'admin']).toContain(op.auth);
95
- }
96
- });
97
-
98
- it('contains expected op names', () => {
99
- const names = ops.map((o) => o.name);
100
- expect(names).toContain('vault_archive');
101
- expect(names).toContain('vault_restore');
102
- expect(names).toContain('vault_optimize');
103
- expect(names).toContain('vault_backup');
104
- expect(names).toContain('vault_age_report');
105
- expect(names).toContain('vault_set_temporal');
106
- expect(names).toContain('vault_find_expiring');
107
- expect(names).toContain('vault_find_expired');
108
- expect(names).toContain('knowledge_audit');
109
- expect(names).toContain('knowledge_health');
110
- expect(names).toContain('knowledge_merge');
111
- expect(names).toContain('knowledge_reorganize');
112
- });
113
-
114
86
  describe('vault_archive', () => {
115
87
  it('archives old entries', async () => {
116
88
  await findOp(ops, 'vault_archive').handler({
@@ -35,23 +35,6 @@ describe('branching-ops', () => {
35
35
  ops = captureOps(createBranchingOps(runtime));
36
36
  });
37
37
 
38
- it('registers all 5 branching ops', () => {
39
- expect(ops.size).toBe(5);
40
- expect(ops.has('vault_branch')).toBe(true);
41
- expect(ops.has('vault_branch_add')).toBe(true);
42
- expect(ops.has('vault_branch_list')).toBe(true);
43
- expect(ops.has('vault_merge_branch')).toBe(true);
44
- expect(ops.has('vault_delete_branch')).toBe(true);
45
- });
46
-
47
- it('has correct auth levels', () => {
48
- expect(ops.get('vault_branch')!.auth).toBe('write');
49
- expect(ops.get('vault_branch_add')!.auth).toBe('write');
50
- expect(ops.get('vault_branch_list')!.auth).toBe('read');
51
- expect(ops.get('vault_merge_branch')!.auth).toBe('admin');
52
- expect(ops.get('vault_delete_branch')!.auth).toBe('admin');
53
- });
54
-
55
38
  describe('vault_branch', () => {
56
39
  it('creates a branch', async () => {
57
40
  const result = await executeOp(ops, 'vault_branch', { name: 'experiment' });
@@ -83,18 +83,6 @@ describe('createCaptureOps', () => {
83
83
  ops = createCaptureOps(runtime);
84
84
  });
85
85
 
86
- it('returns 4 ops', () => {
87
- expect(ops).toHaveLength(4);
88
- });
89
-
90
- it('all ops have required fields', () => {
91
- for (const op of ops) {
92
- expect(op.name).toBeTruthy();
93
- expect(op.handler).toBeDefined();
94
- expect(['read', 'write']).toContain(op.auth);
95
- }
96
- });
97
-
98
86
  describe('capture_knowledge', () => {
99
87
  it('captures a single entry with governance approval', async () => {
100
88
  const result = (await findOp(ops, 'capture_knowledge').handler({
@@ -191,7 +179,7 @@ describe('createCaptureOps', () => {
191
179
  entries: [{ type: 'pattern', domain: 'a', title: 'A', description: 'a', tags: [] }],
192
180
  })) as Record<string, unknown>;
193
181
  expect(result.autoLinkedCount).toBe(1);
194
- expect(result.suggestedLinks).toBeDefined();
182
+ expect((result.suggestedLinks as unknown[]).length).toBe(1); // one suggestion returned by mock
195
183
  });
196
184
 
197
185
  it('maps extended types correctly', async () => {
@@ -252,7 +240,8 @@ describe('createCaptureOps', () => {
252
240
  description: 'A quick capture',
253
241
  })) as Record<string, unknown>;
254
242
  expect(result.captured).toBe(true);
255
- expect(result.id).toBeDefined();
243
+ expect(typeof result.id).toBe('string');
244
+ expect((result.id as string).startsWith('testing-')).toBe(true); // id is generated as <domain>-<timestamp>-<random>
256
245
  expect((result.governance as Record<string, unknown>).action).toBe('capture');
257
246
  });
258
247
 
@@ -380,6 +369,43 @@ describe('createCaptureOps', () => {
380
369
  // Capture should still succeed despite sync error
381
370
  expect(result.captured).toBe(true);
382
371
  });
372
+
373
+ it('adds planningNote when type is anti-pattern', async () => {
374
+ const result = (await findOp(ops, 'capture_quick').handler({
375
+ type: 'anti-pattern',
376
+ domain: 'testing',
377
+ title: 'No content body',
378
+ description: 'captured without context/example/why',
379
+ })) as Record<string, unknown>;
380
+ expect(result.captured).toBe(true);
381
+ expect(typeof result.planningNote).toBe('string');
382
+ expect(result.planningNote as string).toContain('capture_knowledge');
383
+ });
384
+
385
+ it('adds planningNote when tags include planning-gate', async () => {
386
+ const result = (await findOp(ops, 'capture_quick').handler({
387
+ type: 'rule',
388
+ domain: 'testing',
389
+ title: 'Gated rule',
390
+ description: 'should warn',
391
+ tags: ['planning-gate'],
392
+ })) as Record<string, unknown>;
393
+ expect(result.captured).toBe(true);
394
+ expect(typeof result.planningNote).toBe('string');
395
+ expect(result.planningNote as string).toContain('capture_knowledge');
396
+ });
397
+
398
+ it('does not add planningNote for non-planning types without planning tags', async () => {
399
+ const result = (await findOp(ops, 'capture_quick').handler({
400
+ type: 'reference',
401
+ domain: 'docs',
402
+ title: 'A plain reference',
403
+ description: 'no planning intent',
404
+ tags: ['docs'],
405
+ })) as Record<string, unknown>;
406
+ expect(result.captured).toBe(true);
407
+ expect(result.planningNote).toBeUndefined();
408
+ });
383
409
  });
384
410
 
385
411
  describe('search_intelligent', () => {
@@ -387,8 +413,7 @@ describe('createCaptureOps', () => {
387
413
  const result = (await findOp(ops, 'search_intelligent').handler({
388
414
  query: 'auth patterns',
389
415
  })) as Array<Record<string, unknown>>;
390
- expect(Array.isArray(result)).toBe(true);
391
- expect(result.length).toBeGreaterThan(0);
416
+ expect(result).toHaveLength(1); // brain.intelligentSearch mock returns exactly 1 result
392
417
  expect(result[0].source).toBe('vault');
393
418
  });
394
419