@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
@@ -40,6 +40,10 @@ import {
40
40
  import { initializeTables } from './schema.js';
41
41
  import { computeHealthAudit, type HealthDataProvider } from './health-audit.js';
42
42
  import { enrichEntryMetadata } from './metadata-enricher.js';
43
+ import {
44
+ computeEditDistance,
45
+ normalizeTags as normalizeTagsCanonical,
46
+ } from '../vault/tag-normalizer.js';
43
47
 
44
48
  // ─── Constants ──────────────────────────────────────────────────────
45
49
 
@@ -359,15 +363,141 @@ export class Curator {
359
363
  if (batch.length < DEFAULT_BATCH_SIZE) break;
360
364
  offset += DEFAULT_BATCH_SIZE;
361
365
  }
366
+
367
+ // Synonym merge: detect tag pairs with edit-distance ≤ 1 and merge lower-frequency into higher
368
+ const synonymMerges = this.mergeSynonymTags();
369
+
362
370
  return {
363
371
  totalEntries,
364
372
  groomedCount: totalEntries,
365
373
  tagsNormalized,
366
374
  staleCount,
367
375
  durationMs: Date.now() - start,
376
+ synonymMerges,
368
377
  };
369
378
  }
370
379
 
380
+ /**
381
+ * Detect tag pairs where edit-distance ≤ 1 (e.g. 'workflow'/'workflows') and merge
382
+ * the lower-frequency tag into the higher-frequency one across all entries.
383
+ * Returns count of tags merged.
384
+ */
385
+ private mergeSynonymTags(): number {
386
+ // Collect all unique tags and their usage counts
387
+ const rows = this.provider.all<{ tags: string }>(
388
+ 'SELECT tags FROM entries WHERE tags IS NOT NULL',
389
+ );
390
+ const tagCounts = new Map<string, number>();
391
+
392
+ for (const row of rows) {
393
+ let tags: string[];
394
+ try {
395
+ tags = JSON.parse(row.tags) as string[];
396
+ } catch {
397
+ continue;
398
+ }
399
+ for (const tag of tags) {
400
+ if (typeof tag === 'string' && tag.length > 0) {
401
+ tagCounts.set(tag, (tagCounts.get(tag) ?? 0) + 1);
402
+ }
403
+ }
404
+ }
405
+
406
+ const allTags = Array.from(tagCounts.keys());
407
+ if (allTags.length < 2) return 0;
408
+
409
+ // Build synonym merge map: minorTag → majorTag
410
+ // Only merge if edit-distance ≤ 1 and major has higher or equal frequency
411
+ const mergeMap = new Map<string, string>(); // minor → major
412
+ const processed = new Set<string>();
413
+
414
+ // Bucket tags by length to reduce comparisons from O(n²) to O(n * avg_bucket_size)
415
+ const buckets = new Map<number, string[]>();
416
+ for (const tag of allTags) {
417
+ const len = tag.length;
418
+ const bucket = buckets.get(len);
419
+ if (bucket) {
420
+ bucket.push(tag);
421
+ } else {
422
+ buckets.set(len, [tag]);
423
+ }
424
+ }
425
+
426
+ for (const a of allTags) {
427
+ if (processed.has(a)) continue;
428
+ // Only compare against tags of the same or adjacent length (edit distance ≤ 1)
429
+ const candidates: string[] = [
430
+ ...(buckets.get(a.length) ?? []),
431
+ ...(buckets.get(a.length - 1) ?? []),
432
+ ...(buckets.get(a.length + 1) ?? []),
433
+ ];
434
+ for (const b of candidates) {
435
+ if (b === a) continue;
436
+ if (processed.has(a) || processed.has(b)) continue;
437
+ if (computeEditDistance(a, b) <= 1) {
438
+ const countA = tagCounts.get(a) ?? 0;
439
+ const countB = tagCounts.get(b) ?? 0;
440
+ // Merge lower-frequency into higher-frequency
441
+ if (countA >= countB) {
442
+ mergeMap.set(b, a);
443
+ processed.add(b);
444
+ } else {
445
+ mergeMap.set(a, b);
446
+ processed.add(a);
447
+ }
448
+ }
449
+ }
450
+ }
451
+
452
+ if (mergeMap.size === 0) return 0;
453
+
454
+ // Apply merges to all affected entries
455
+ let mergeCount = 0;
456
+ const allEntryRows = this.provider.all<{ id: string; tags: string }>(
457
+ 'SELECT id, tags FROM entries WHERE tags IS NOT NULL',
458
+ );
459
+
460
+ for (const row of allEntryRows) {
461
+ let tags: string[];
462
+ try {
463
+ tags = JSON.parse(row.tags) as string[];
464
+ } catch {
465
+ continue;
466
+ }
467
+
468
+ let changed = false;
469
+ const updated = [
470
+ ...new Set(
471
+ tags.map((tag) => {
472
+ const replacement = mergeMap.get(tag);
473
+ if (replacement) {
474
+ changed = true;
475
+ return replacement;
476
+ }
477
+ return tag;
478
+ }),
479
+ ),
480
+ ];
481
+
482
+ if (changed) {
483
+ this.provider.run('UPDATE entries SET tags = ?, updated_at = unixepoch() WHERE id = ?', [
484
+ JSON.stringify(updated),
485
+ row.id,
486
+ ]);
487
+ this.logChange(
488
+ 'synonym_merge',
489
+ row.id,
490
+ JSON.stringify(tags),
491
+ JSON.stringify(updated),
492
+ 'Synonym tag merge (edit-distance ≤ 1)',
493
+ );
494
+ mergeCount++;
495
+ }
496
+ }
497
+
498
+ return mergeCount;
499
+ }
500
+
371
501
  // ─── Consolidation ───────────────────────────────────────────
372
502
 
373
503
  consolidate(options?: ConsolidationOptions): ConsolidationResult {
@@ -419,6 +549,55 @@ export class Curator {
419
549
  }
420
550
  }
421
551
  }
552
+
553
+ // Retag: run all entries through canonical normalization if requested
554
+ let retagged: number | undefined;
555
+ if (options?.retag && options.canonicalTags && options.canonicalTags.length > 0) {
556
+ const tagMode = options.tagConstraintMode ?? 'suggest';
557
+ const metaPrefixes = options.metadataTagPrefixes ?? ['source:'];
558
+ retagged = 0;
559
+
560
+ const entryRows = this.provider.all<{ id: string; tags: string }>(
561
+ 'SELECT id, tags FROM entries WHERE tags IS NOT NULL',
562
+ );
563
+
564
+ for (const row of entryRows) {
565
+ let tags: string[];
566
+ try {
567
+ tags = JSON.parse(row.tags) as string[];
568
+ } catch {
569
+ continue;
570
+ }
571
+
572
+ const normalized = normalizeTagsCanonical(
573
+ tags,
574
+ options.canonicalTags,
575
+ tagMode,
576
+ metaPrefixes,
577
+ );
578
+ const tagsChanged =
579
+ normalized.length !== tags.length || normalized.some((t, i) => t !== tags[i]);
580
+
581
+ if (tagsChanged) {
582
+ if (!dryRun) {
583
+ this.provider.run(
584
+ 'UPDATE entries SET tags = ?, updated_at = unixepoch() WHERE id = ?',
585
+ [JSON.stringify(normalized), row.id],
586
+ );
587
+ this.logChange(
588
+ 'retag',
589
+ row.id,
590
+ JSON.stringify(tags),
591
+ JSON.stringify(normalized),
592
+ 'Canonical retag during consolidation',
593
+ );
594
+ mutations++;
595
+ }
596
+ retagged++;
597
+ }
598
+ }
599
+ }
600
+
422
601
  return {
423
602
  dryRun,
424
603
  duplicates,
@@ -426,6 +605,7 @@ export class Curator {
426
605
  contradictions,
427
606
  mutations,
428
607
  durationMs: Date.now() - start,
608
+ retagged,
429
609
  };
430
610
  }
431
611
 
@@ -41,10 +41,6 @@ describe('tag-manager', () => {
41
41
  });
42
42
 
43
43
  describe('DEFAULT_TAG_ALIASES', () => {
44
- it('exports expected alias count', () => {
45
- expect(DEFAULT_TAG_ALIASES.length).toBe(13);
46
- });
47
-
48
44
  it('includes common aliases', () => {
49
45
  const aliasMap = new Map(DEFAULT_TAG_ALIASES);
50
46
  expect(aliasMap.get('a11y')).toBe('accessibility');
@@ -62,6 +62,7 @@ export interface GroomAllResult {
62
62
  tagsNormalized: number;
63
63
  staleCount: number;
64
64
  durationMs: number;
65
+ synonymMerges: number;
65
66
  }
66
67
 
67
68
  // ─── Consolidation ──────────────────────────────────────────────────
@@ -71,6 +72,14 @@ export interface ConsolidationOptions {
71
72
  staleDaysThreshold?: number;
72
73
  duplicateThreshold?: number;
73
74
  contradictionThreshold?: number;
75
+ /** When true, run all entries through canonical tag normalization. Dry-run by default. */
76
+ retag?: boolean;
77
+ /** Canonical tag list for retag operation. Required when retag is true. */
78
+ canonicalTags?: string[];
79
+ /** Tag constraint mode for retag. Default: 'suggest'. */
80
+ tagConstraintMode?: 'enforce' | 'suggest' | 'off';
81
+ /** Metadata tag prefixes exempt from canonical normalization. Default: ['source:']. */
82
+ metadataTagPrefixes?: string[];
74
83
  }
75
84
 
76
85
  export interface ConsolidationResult {
@@ -80,6 +89,7 @@ export interface ConsolidationResult {
80
89
  contradictions: Contradiction[];
81
90
  mutations: number;
82
91
  durationMs: number;
92
+ retagged?: number;
83
93
  }
84
94
 
85
95
  // ─── Changelog & Health ─────────────────────────────────────────────
@@ -118,11 +118,6 @@ describe('validateDomainPack', () => {
118
118
  });
119
119
 
120
120
  describe('SEMANTIC_FACADE_NAMES', () => {
121
- it('is a readonly array (TypeScript enforced)', () => {
122
- expect(Array.isArray(SEMANTIC_FACADE_NAMES)).toBe(true);
123
- expect(SEMANTIC_FACADE_NAMES.length).toBeGreaterThan(0);
124
- });
125
-
126
121
  it('contains all core engine facades', () => {
127
122
  const expected = [
128
123
  'vault',
@@ -134,13 +134,6 @@ describe('dream ops', () => {
134
134
  vault.close();
135
135
  });
136
136
 
137
- it('creates 3 ops with correct names', () => {
138
- expect(ops).toHaveLength(3);
139
- expect(ops.map((o) => o.name).sort()).toEqual(
140
- ['dream_check_gate', 'dream_run', 'dream_status'].sort(),
141
- );
142
- });
143
-
144
137
  it('dream_status returns status', async () => {
145
138
  const result = (await findOp('dream_status').handler({})) as Record<string, unknown>;
146
139
  expect(result).toHaveProperty('sessionsSinceLastDream');
@@ -136,7 +136,7 @@ describe('EnforcementRegistry', () => {
136
136
  const result = registry.translate('claude-code');
137
137
  expect(result.host).toBe('claude-code');
138
138
  // Only r1 should be translated (r2 is disabled)
139
- expect(result.files.length).toBeGreaterThan(0);
139
+ expect(result.files.length).toBe(1);
140
140
  });
141
141
  });
142
142
 
@@ -250,7 +250,7 @@ describe('ClaudeCodeAdapter', () => {
250
250
  makeRule({ id: 'bad', trigger: 'on-save' }),
251
251
  ],
252
252
  });
253
- expect(result.files.length).toBeGreaterThan(0);
253
+ expect(result.files.length).toBe(1);
254
254
  expect(result.skipped).toHaveLength(1);
255
255
  expect(result.skipped[0].ruleId).toBe('bad');
256
256
  });
@@ -38,24 +38,6 @@ afterAll(() => {
38
38
  runtime.close();
39
39
  });
40
40
 
41
- describe('createCoreOps', () => {
42
- it('returns exactly 5 ops', () => {
43
- const opDefs = createCoreOps(runtime, TEST_IDENTITY);
44
- expect(opDefs).toHaveLength(5);
45
- });
46
-
47
- it('returns ops with expected names', () => {
48
- const names = [...ops.keys()];
49
- expect(names).toEqual(['health', 'identity', 'activate', 'session_start', 'setup']);
50
- });
51
-
52
- it('all ops have read or write auth level', () => {
53
- for (const op of ops.values()) {
54
- expect(['read', 'write']).toContain(op.auth);
55
- }
56
- });
57
- });
58
-
59
41
  describe('health op', () => {
60
42
  it('returns status ok with agent info', async () => {
61
43
  const result = await executeOp(ops, 'health');
@@ -75,7 +57,7 @@ describe('health op', () => {
75
57
  const data = result.data as Record<string, unknown>;
76
58
  const vault = data.vault as Record<string, unknown>;
77
59
  expect(typeof vault.entries).toBe('number');
78
- expect(Array.isArray(vault.domains)).toBe(true);
60
+ expect(vault.domains).toEqual([]);
79
61
  });
80
62
 
81
63
  it('has read auth level', () => {
@@ -122,7 +104,7 @@ describe('activate op', () => {
122
104
  const vault = data.vault as Record<string, unknown>;
123
105
  expect(vault.connected).toBe(true);
124
106
  expect(typeof vault.entries).toBe('number');
125
- expect(Array.isArray(vault.domains)).toBe(true);
107
+ expect(vault.domains).toEqual([]);
126
108
  });
127
109
 
128
110
  it('returns deactivation response when deactivate=true', async () => {
@@ -202,8 +184,8 @@ describe('setup op', () => {
202
184
  const data = result.data as Record<string, unknown>;
203
185
  const vault = data.vault as Record<string, unknown>;
204
186
  expect(typeof vault.entries).toBe('number');
205
- expect(Array.isArray(vault.domains)).toBe(true);
206
- expect(vault.byType).toBeDefined();
187
+ expect(vault.domains).toEqual([]);
188
+ expect(vault.byType).toEqual({});
207
189
  });
208
190
 
209
191
  it('recommends action when vault is empty', async () => {
@@ -8,10 +8,31 @@
8
8
  * Now they're created dynamically by the engine at startup.
9
9
  */
10
10
 
11
+ import { readFileSync } from 'node:fs';
12
+ import { dirname, join } from 'node:path';
13
+ import { fileURLToPath } from 'node:url';
11
14
  import { z } from 'zod';
12
15
  import type { OpDefinition } from '../facades/types.js';
13
16
  import type { AgentRuntime } from '../runtime/types.js';
14
17
 
18
+ function getCoreVersion(): string {
19
+ try {
20
+ const thisDir = dirname(fileURLToPath(import.meta.url));
21
+ let dir = thisDir;
22
+ for (let i = 0; i < 5; i++) {
23
+ try {
24
+ const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
25
+ return pkg.version ?? 'unknown';
26
+ } catch {
27
+ dir = dirname(dir);
28
+ }
29
+ }
30
+ } catch {
31
+ // import.meta.url unavailable in some test envs
32
+ }
33
+ return 'unknown';
34
+ }
35
+
15
36
  export interface AgentIdentityConfig {
16
37
  id: string;
17
38
  name: string;
@@ -38,10 +59,24 @@ export function createCoreOps(
38
59
  auth: 'read',
39
60
  handler: async () => {
40
61
  const s = runtime.vault.stats();
62
+ let vaultConnected = true;
63
+ try {
64
+ runtime.vault.stats();
65
+ } catch {
66
+ vaultConnected = false;
67
+ }
68
+ const runtimeAny = runtime as unknown as Record<string, unknown>;
69
+ const brainReady = typeof runtimeAny.brain === 'object' && runtimeAny.brain !== null;
41
70
  return {
42
71
  status: 'ok',
72
+ version: getCoreVersion(),
43
73
  agent: { name: identity.name, role: identity.role, format: 'filetree' },
44
- vault: { entries: s.totalEntries, domains: Object.keys(s.byDomain) },
74
+ vault: {
75
+ connected: vaultConnected,
76
+ entries: s.totalEntries,
77
+ domains: Object.keys(s.byDomain),
78
+ },
79
+ brain: { ready: brainReady },
45
80
  };
46
81
  },
47
82
  },
@@ -6,12 +6,7 @@
6
6
  */
7
7
 
8
8
  import { describe, it, expect } from 'vitest';
9
- import {
10
- ENGINE_MODULE_MANIFEST,
11
- CORE_KEY_OPS,
12
- ENGINE_MAJOR_VERSION,
13
- type ModuleManifestEntry,
14
- } from './module-manifest.js';
9
+ import { ENGINE_MODULE_MANIFEST, CORE_KEY_OPS, ENGINE_MAJOR_VERSION } from './module-manifest.js';
15
10
 
16
11
  describe('ENGINE_MODULE_MANIFEST', () => {
17
12
  it('contains all expected engine modules', () => {
@@ -32,10 +27,6 @@ describe('ENGINE_MODULE_MANIFEST', () => {
32
27
  expect(suffixes).toContain('intake');
33
28
  });
34
29
 
35
- it('has exactly 22 modules', () => {
36
- expect(ENGINE_MODULE_MANIFEST).toHaveLength(22);
37
- });
38
-
39
30
  it('has no duplicate suffixes', () => {
40
31
  const suffixes = ENGINE_MODULE_MANIFEST.map((m) => m.suffix);
41
32
  expect(new Set(suffixes).size).toBe(suffixes.length);
@@ -86,16 +77,6 @@ describe('ENGINE_MODULE_MANIFEST', () => {
86
77
  }
87
78
  });
88
79
 
89
- it('satisfies ModuleManifestEntry interface shape', () => {
90
- const testEntry: ModuleManifestEntry = {
91
- suffix: 'test',
92
- description: 'Test module',
93
- keyOps: ['op1'],
94
- };
95
- expect(testEntry.suffix).toBe('test');
96
- expect(testEntry.conditional).toBeUndefined();
97
- });
98
-
99
80
  it('intentSignals is optional and a Record<string, string> when present', () => {
100
81
  for (const entry of ENGINE_MODULE_MANIFEST) {
101
82
  if (entry.intentSignals !== undefined) {
@@ -144,20 +125,9 @@ describe('CORE_KEY_OPS', () => {
144
125
  it('contains the 4 core ops', () => {
145
126
  expect(CORE_KEY_OPS).toEqual(['health', 'identity', 'session_start', 'activate']);
146
127
  });
147
-
148
- it('is a string array', () => {
149
- for (const op of CORE_KEY_OPS) {
150
- expect(typeof op).toBe('string');
151
- }
152
- });
153
128
  });
154
129
 
155
130
  describe('ENGINE_MAJOR_VERSION', () => {
156
- it('is a positive integer', () => {
157
- expect(Number.isInteger(ENGINE_MAJOR_VERSION)).toBe(true);
158
- expect(ENGINE_MAJOR_VERSION).toBeGreaterThan(0);
159
- });
160
-
161
131
  it('is currently version 9', () => {
162
132
  expect(ENGINE_MAJOR_VERSION).toBe(9);
163
133
  });
@@ -63,10 +63,6 @@ describe('registerEngine — module completeness', () => {
63
63
  expect(moduleSuffixes).toEqual(manifestSuffixes);
64
64
  });
65
65
 
66
- it('ENGINE_MODULES and manifest have same count', () => {
67
- expect(ENGINE_MODULES.length).toBe(ENGINE_MODULE_MANIFEST.length);
68
- });
69
-
70
66
  it('registers all unconditional modules', () => {
71
67
  const server = makeServer();
72
68
  const result = registerEngine(server, runtime, { agentId: 'check' });
@@ -203,10 +199,9 @@ describe('registerEngine — return value', () => {
203
199
  it('returns tools array, totalOps count, and registerTool function', () => {
204
200
  const server = makeServer();
205
201
  const result = registerEngine(server, runtime, { agentId: 'ret' });
206
- expect(Array.isArray(result.tools)).toBe(true);
202
+ expect(result.tools.length).toBeGreaterThan(0);
207
203
  expect(typeof result.totalOps).toBe('number');
208
204
  expect(typeof result.registerTool).toBe('function');
209
- expect(result.totalOps).toBeGreaterThan(0);
210
205
  });
211
206
 
212
207
  it('registerTool adds a new tool at runtime', () => {
@@ -247,8 +242,8 @@ describe('registerEngine — op visibility', () => {
247
242
  expect(INTERNAL_OPS.has('create_plan')).toBe(false);
248
243
  });
249
244
 
250
- it('INTERNAL_OPS has at least 25 entries', () => {
251
- expect(INTERNAL_OPS.size).toBeGreaterThanOrEqual(25);
245
+ it('INTERNAL_OPS has exactly 29 entries', () => {
246
+ expect(INTERNAL_OPS.size).toBe(29);
252
247
  });
253
248
 
254
249
  it('ops without visibility field default to user (backward compat)', () => {
@@ -266,31 +261,6 @@ describe('registerEngine — op visibility', () => {
266
261
  expect(result.tools).toContain('vis_test');
267
262
  });
268
263
 
269
- it('ops with visibility: internal are excluded from MCP tool description but remain callable', () => {
270
- const server = makeServer();
271
- const visibleOp: OpDefinition = {
272
- name: 'public_op',
273
- description: 'Public op',
274
- auth: 'read',
275
- handler: async () => 'visible',
276
- };
277
- const internalOp: OpDefinition = {
278
- name: 'secret_op',
279
- description: 'Internal op',
280
- auth: 'admin',
281
- visibility: 'internal',
282
- handler: async () => 'hidden',
283
- };
284
- // Register both ops under a pack facade
285
- registerEngine(server, runtime, {
286
- agentId: 'vt',
287
- domainPacks: [{ name: 'test', facades: [{ name: 'check', ops: [visibleOp, internalOp] }] }],
288
- });
289
- // We can't easily inspect the MCP schema description string, but we verify
290
- // that registration succeeds with mixed visibility ops
291
- expect(true).toBe(true);
292
- });
293
-
294
264
  it('every INTERNAL_OPS entry corresponds to a real op in some facade', () => {
295
265
  // Collect all op names across all engine modules
296
266
  const allOpNames = new Set<string>();
@@ -33,9 +33,11 @@ describe('shouldRetry', () => {
33
33
  });
34
34
 
35
35
  describe('getRetryDelay', () => {
36
- it('should return a positive number', () => {
36
+ it('should return a non-negative number', () => {
37
37
  const delay = getRetryDelay(0, 'fast');
38
+ expect(typeof delay).toBe('number');
38
39
  expect(delay).toBeGreaterThanOrEqual(0);
40
+ expect(delay).toBeLessThanOrEqual(RETRY_PRESETS.fast.maxIntervalMs * 1.25);
39
41
  });
40
42
 
41
43
  it('should increase with attempt number', () => {
@@ -98,12 +98,6 @@ describe('ChainRunner', () => {
98
98
  runner = new ChainRunner(provider);
99
99
  });
100
100
 
101
- it('initializes the chain_instances table on construction', () => {
102
- expect(provider.execSql).toHaveBeenCalledWith(
103
- expect.stringContaining('CREATE TABLE IF NOT EXISTS chain_instances'),
104
- );
105
- });
106
-
107
101
  describe('execute', () => {
108
102
  it('runs all steps to completion', async () => {
109
103
  const dispatch: DispatchFn = vi.fn(async () => ({ result: 'ok' }));
@@ -11,7 +11,7 @@ import { getFlowOverrides, detectContext } from './context-router.js';
11
11
  describe('getFlowOverrides', () => {
12
12
  it('returns overrides for BUILD-flow', () => {
13
13
  const overrides = getFlowOverrides('BUILD-flow');
14
- expect(overrides.length).toBeGreaterThan(0);
14
+ expect(overrides).toHaveLength(4);
15
15
  const contexts = overrides.map((o) => o.context);
16
16
  expect(contexts).toContain('small-component');
17
17
  expect(contexts).toContain('large-component');
@@ -19,7 +19,7 @@ describe('getFlowOverrides', () => {
19
19
 
20
20
  it('returns overrides for FIX-flow', () => {
21
21
  const overrides = getFlowOverrides('FIX-flow');
22
- expect(overrides.length).toBeGreaterThan(0);
22
+ expect(overrides).toHaveLength(2);
23
23
  const contexts = overrides.map((o) => o.context);
24
24
  expect(contexts).toContain('design-fix');
25
25
  expect(contexts).toContain('a11y-fix');
@@ -27,7 +27,7 @@ describe('getFlowOverrides', () => {
27
27
 
28
28
  it('returns overrides for REVIEW-flow', () => {
29
29
  const overrides = getFlowOverrides('REVIEW-flow');
30
- expect(overrides.length).toBeGreaterThan(0);
30
+ expect(overrides).toHaveLength(2);
31
31
  const contexts = overrides.map((o) => o.context);
32
32
  expect(contexts).toContain('pr-review');
33
33
  expect(contexts).toContain('architecture-review');
@@ -2,11 +2,13 @@
2
2
  * Epilogue — colocated contract tests.
3
3
  *
4
4
  * Contract:
5
- * - runEpilogue() calls capture_knowledge when vault is available
5
+ * - runEpilogue() calls capture_knowledge with intent-specific title when vault is available
6
6
  * - runEpilogue() calls session_capture when sessionStore is available
7
7
  * - Returns { captured: true, sessionId } on success
8
8
  * - Silently ignores errors from dispatch (best-effort)
9
9
  * - Returns { captured: false } when no probes are available
10
+ * - Title format: "{INTENT} execution — {objective}" (max 120 chars)
11
+ * - Tags include intent (lowercase) and domain (if provided)
10
12
  */
11
13
 
12
14
  import { describe, it, expect, vi } from 'vitest';
@@ -33,7 +35,6 @@ describe('runEpilogue', () => {
33
35
  expect(dispatch).toHaveBeenCalledWith(
34
36
  'capture_knowledge',
35
37
  expect.objectContaining({
36
- title: 'Flow execution summary',
37
38
  content: 'summary',
38
39
  type: 'workflow',
39
40
  projectPath: '/project',
@@ -42,6 +43,43 @@ describe('runEpilogue', () => {
42
43
  expect(result.captured).toBe(true);
43
44
  });
44
45
 
46
+ it('uses intent-specific title when planContext is provided', async () => {
47
+ const dispatch = vi.fn(async () => ({ tool: 'capture_knowledge', status: 'ok', data: {} }));
48
+ await runEpilogue(dispatch, probes({ vault: true }), '/project', 'summary', {
49
+ intent: 'BUILD',
50
+ objective: 'add authentication module',
51
+ domain: 'auth',
52
+ });
53
+
54
+ expect(dispatch).toHaveBeenCalledWith(
55
+ 'capture_knowledge',
56
+ expect.objectContaining({
57
+ title: 'BUILD execution — add authentication module',
58
+ tags: expect.arrayContaining(['auto-captured', 'build', 'auth']),
59
+ }),
60
+ );
61
+ });
62
+
63
+ it('falls back to FLOW intent when planContext is absent', async () => {
64
+ const dispatch = vi.fn(async () => ({ tool: 'capture_knowledge', status: 'ok', data: {} }));
65
+ await runEpilogue(dispatch, probes({ vault: true }), '/project', 'done summary');
66
+
67
+ const call = dispatch.mock.calls[0]?.[1] as Record<string, unknown>;
68
+ expect((call.title as string).startsWith('FLOW execution —')).toBe(true);
69
+ expect((call.tags as string[]).includes('flow')).toBe(true);
70
+ });
71
+
72
+ it('omits domain tag when domain is not provided', async () => {
73
+ const dispatch = vi.fn(async () => ({ tool: 'capture_knowledge', status: 'ok', data: {} }));
74
+ await runEpilogue(dispatch, probes({ vault: true }), '/project', 'summary', {
75
+ intent: 'FIX',
76
+ objective: 'fix login bug',
77
+ });
78
+
79
+ const call = dispatch.mock.calls[0]?.[1] as Record<string, unknown>;
80
+ expect(call.tags).toEqual(['auto-captured', 'fix']);
81
+ });
82
+
45
83
  it('captures session when sessionStore is available', async () => {
46
84
  const dispatch = vi.fn(async () => ({
47
85
  tool: 'session_capture',