@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
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { describe, it, expect, vi, beforeEach } from 'vitest';
4
- import { createOrchestrateOps } from './orchestrate-ops.js';
4
+ import { createOrchestrateOps, mapVaultResults } from './orchestrate-ops.js';
5
5
  import { assessTaskComplexity } from '../planning/task-complexity-assessor.js';
6
6
  import type { AgentRuntime } from './types.js';
7
7
 
@@ -104,6 +104,7 @@ function mockRuntime(): AgentRuntime {
104
104
  },
105
105
  brain: {
106
106
  recordFeedback: vi.fn(),
107
+ intelligentSearch: vi.fn().mockResolvedValue([]),
107
108
  },
108
109
  brainIntelligence: {
109
110
  recommend: vi.fn().mockReturnValue([]),
@@ -164,16 +165,6 @@ describe('createOrchestrateOps', () => {
164
165
  ops = createOrchestrateOps(rt);
165
166
  });
166
167
 
167
- it('returns all orchestrate ops', () => {
168
- expect(ops.length).toBeGreaterThanOrEqual(5);
169
- const names = ops.map((o) => o.name);
170
- expect(names).toContain('orchestrate_plan');
171
- expect(names).toContain('orchestrate_execute');
172
- expect(names).toContain('orchestrate_complete');
173
- expect(names).toContain('orchestrate_status');
174
- expect(names).toContain('orchestrate_quick_capture');
175
- });
176
-
177
168
  // ─── orchestrate_plan ─────────────────────────────────────────
178
169
 
179
170
  describe('orchestrate_plan', () => {
@@ -190,18 +181,159 @@ describe('createOrchestrateOps', () => {
190
181
  expect(flow.intent).toBe('BUILD');
191
182
  });
192
183
 
193
- it('falls back to vault search when brain has no recommendations', async () => {
184
+ it('vault results appear when brain recommend has no data', async () => {
185
+ // intelligentSearch succeeds; vault results appear even when brain.recommend throws.
194
186
  const op = findOp(ops, 'orchestrate_plan');
195
187
  vi.mocked(rt.brainIntelligence.recommend).mockImplementation(() => {
196
188
  throw new Error('no data');
197
189
  });
198
- vi.mocked(rt.vault.search).mockReturnValue([
190
+ vi.mocked(rt.brain.intelligentSearch).mockResolvedValue([
199
191
  { entry: { id: 'e1', title: 'Pattern A' }, score: 0.8 },
200
192
  ] as never);
201
193
  const result = (await op.handler({ prompt: 'fix a bug' })) as Record<string, unknown>;
202
194
  const recs = result.recommendations as Array<Record<string, unknown>>;
203
195
  expect(recs.length).toBe(1);
204
196
  expect(recs[0].pattern).toBe('Pattern A');
197
+ expect(recs[0].source).toBe('vault');
198
+ });
199
+
200
+ it('intelligentSearch is called for vault retrieval — semantic search is primary', async () => {
201
+ // The primary vault retrieval path must use semantic search, not keyword search.
202
+ // If intelligentSearch were not called, ranking would fall back to TF-IDF keyword
203
+ // frequency, missing semantically related entries.
204
+ const op = findOp(ops, 'orchestrate_plan');
205
+ vi.mocked(rt.brainIntelligence.recommend).mockReturnValue([
206
+ { pattern: 'Brain Pattern', strength: 70 },
207
+ ] as never);
208
+ vi.mocked(rt.brain.intelligentSearch).mockResolvedValue([
209
+ { entry: { id: 'v1', title: 'Vault Pattern' }, score: 0.9 },
210
+ ] as never);
211
+ await op.handler({ prompt: 'build a feature' });
212
+ expect(rt.brain.intelligentSearch).toHaveBeenCalled();
213
+ });
214
+
215
+ it('vault results precede brain results and brain does not duplicate vault entries', async () => {
216
+ // Vault patterns come first; brain adds only novel patterns.
217
+ const op = findOp(ops, 'orchestrate_plan');
218
+ vi.mocked(rt.brain.intelligentSearch).mockResolvedValue([
219
+ { entry: { id: 'v1', title: 'Vault Pattern' }, score: 0.9 },
220
+ ] as never);
221
+ vi.mocked(rt.brainIntelligence.recommend).mockReturnValue([
222
+ { pattern: 'Vault Pattern', strength: 70 }, // duplicate — should be dropped
223
+ { pattern: 'Brain Pattern', strength: 60 }, // novel — should be appended
224
+ ] as never);
225
+ const result = (await op.handler({ prompt: 'build something' })) as Record<string, unknown>;
226
+ const recs = result.recommendations as Array<Record<string, unknown>>;
227
+ expect(recs).toHaveLength(2);
228
+ expect(recs[0].source).toBe('vault');
229
+ expect(recs[0].pattern).toBe('Vault Pattern');
230
+ expect(recs[1].source).toBe('brain');
231
+ expect(recs[1].pattern).toBe('Brain Pattern');
232
+ });
233
+
234
+ it('falls back to vault.search when intelligentSearch throws', async () => {
235
+ // If the semantic layer is unavailable, keyword search covers it.
236
+ // Without this fallback, any intelligentSearch failure would silently drop all vault results.
237
+ const op = findOp(ops, 'orchestrate_plan');
238
+ vi.mocked(rt.brain.intelligentSearch).mockRejectedValue(new Error('embedding unavailable'));
239
+ vi.mocked(rt.vault.search).mockReturnValue([
240
+ { entry: { id: 'k1', title: 'Keyword Pattern' }, score: 0.6 },
241
+ ] as never);
242
+ const result = (await op.handler({ prompt: 'fix bug' })) as Record<string, unknown>;
243
+ const recs = result.recommendations as Array<Record<string, unknown>>;
244
+ expect(recs).toHaveLength(1);
245
+ expect(recs[0].pattern).toBe('Keyword Pattern');
246
+ expect(recs[0].source).toBe('vault');
247
+ });
248
+
249
+ it('brain results used alone when both vault search paths are unavailable', async () => {
250
+ // If intelligentSearch and vault.search both fail, brain results cover the gap.
251
+ const op = findOp(ops, 'orchestrate_plan');
252
+ vi.mocked(rt.brain.intelligentSearch).mockRejectedValue(new Error('down'));
253
+ vi.mocked(rt.vault.search).mockImplementation(() => {
254
+ throw new Error('vault down');
255
+ });
256
+ vi.mocked(rt.brainIntelligence.recommend).mockReturnValue([
257
+ { pattern: 'Brain Only Pattern', strength: 65 },
258
+ ] as never);
259
+ const result = (await op.handler({ prompt: 'fix bug' })) as Record<string, unknown>;
260
+ const recs = result.recommendations as Array<Record<string, unknown>>;
261
+ expect(recs).toHaveLength(1);
262
+ expect(recs[0].source).toBe('brain');
263
+ expect(recs[0].pattern).toBe('Brain Only Pattern');
264
+ });
265
+
266
+ it('includes context and example from vault entry body in recommendations', async () => {
267
+ // RankedResult.entry contains the full IntelligenceEntry — context and example
268
+ // must be forwarded into the recommendations payload, not dropped.
269
+ const op = findOp(ops, 'orchestrate_plan');
270
+ vi.mocked(rt.brain.intelligentSearch).mockResolvedValue([
271
+ {
272
+ entry: {
273
+ id: 'e-full',
274
+ title: 'Anti-Pattern: Skip Tests',
275
+ context: 'Never skip tests when under time pressure.',
276
+ example: 'Adding --passWithNoTests to CI.',
277
+ },
278
+ score: 0.9,
279
+ },
280
+ ] as never);
281
+ const result = (await op.handler({ prompt: 'write tests quickly' })) as Record<
282
+ string,
283
+ unknown
284
+ >;
285
+ const recs = result.recommendations as Array<Record<string, unknown>>;
286
+ expect(recs[0].context).toBe('Never skip tests when under time pressure.');
287
+ expect(recs[0].example).toBe('Adding --passWithNoTests to CI.');
288
+ });
289
+
290
+ it('omits context key when vault entry has no body', async () => {
291
+ // Title-only entries must not surface context: null or context: "" — the key should
292
+ // be absent so consumers can reliably check `if (rec.context)`.
293
+ const op = findOp(ops, 'orchestrate_plan');
294
+ vi.mocked(rt.brain.intelligentSearch).mockResolvedValue([
295
+ { entry: { id: 'e-bare', title: 'Pattern B' }, score: 0.7 },
296
+ ] as never);
297
+ const result = (await op.handler({ prompt: 'build something' })) as Record<string, unknown>;
298
+ const recs = result.recommendations as Array<Record<string, unknown>>;
299
+ expect('context' in recs[0]).toBe(false);
300
+ expect('example' in recs[0]).toBe(false);
301
+ });
302
+
303
+ it('sets mandatory:true for critical vault entries', async () => {
304
+ // A critical entry must surface as mandatory so gate injection can promote
305
+ // it to a hard stop. If severity is ignored, critical rules are treated
306
+ // identically to suggestions — the whole enforcement chain breaks.
307
+ const op = findOp(ops, 'orchestrate_plan');
308
+ vi.mocked(rt.brain.intelligentSearch).mockResolvedValue([
309
+ { entry: { id: 'c1', title: 'Critical Rule', severity: 'critical' }, score: 0.95 },
310
+ { entry: { id: 'w1', title: 'Warning Rule', severity: 'warning' }, score: 0.75 },
311
+ { entry: { id: 's1', title: 'Suggestion', severity: 'suggestion' }, score: 0.5 },
312
+ ] as never);
313
+ const result = (await op.handler({ prompt: 'plan something' })) as Record<string, unknown>;
314
+ const recs = result.recommendations as Array<Record<string, unknown>>;
315
+ const critical = recs.find((r) => r.pattern === 'Critical Rule');
316
+ const warning = recs.find((r) => r.pattern === 'Warning Rule');
317
+ const suggestion = recs.find((r) => r.pattern === 'Suggestion');
318
+ expect(critical?.mandatory).toBe(true);
319
+ expect(critical?.strength).toBe(100);
320
+ expect(warning?.mandatory).toBe(false);
321
+ expect(suggestion?.mandatory).toBe(false);
322
+ });
323
+
324
+ it('sets mandatory:false for all brain-sourced recommendations', async () => {
325
+ // Brain learns from usage frequency, not from curated rules — it cannot
326
+ // declare a rule mandatory. If brain recs were mandatory, spurious patterns
327
+ // from frequent usage would block plans with no policy basis.
328
+ const op = findOp(ops, 'orchestrate_plan');
329
+ vi.mocked(rt.brain.intelligentSearch).mockResolvedValue([] as never);
330
+ vi.mocked(rt.brainIntelligence.recommend).mockReturnValue([
331
+ { pattern: 'Brain Pattern', strength: 75 },
332
+ ] as never);
333
+ const result = (await op.handler({ prompt: 'build feature' })) as Record<string, unknown>;
334
+ const recs = result.recommendations as Array<Record<string, unknown>>;
335
+ expect(recs[0].source).toBe('brain');
336
+ expect(recs[0].mandatory).toBe(false);
205
337
  });
206
338
 
207
339
  it('creates a planner plan for lifecycle tracking', async () => {
@@ -209,6 +341,93 @@ describe('createOrchestrateOps', () => {
209
341
  await op.handler({ prompt: 'Build something' });
210
342
  expect(rt.planner.create).toHaveBeenCalled();
211
343
  });
344
+
345
+ it('builds plan with no recommendations when all three retrieval paths fail', async () => {
346
+ // Triple-failure: intelligentSearch down, vault.search down, brain.recommend down.
347
+ // The plan must still build — missing recommendations are non-fatal.
348
+ const op = findOp(ops, 'orchestrate_plan');
349
+ vi.mocked(rt.brain.intelligentSearch).mockRejectedValue(new Error('down'));
350
+ vi.mocked(rt.vault.search).mockImplementation(() => {
351
+ throw new Error('vault down');
352
+ });
353
+ vi.mocked(rt.brainIntelligence.recommend).mockImplementation(() => {
354
+ throw new Error('brain down');
355
+ });
356
+ const result = (await op.handler({ prompt: 'fix bug' })) as Record<string, unknown>;
357
+ const recs = result.recommendations as Array<Record<string, unknown>>;
358
+ expect(recs).toHaveLength(0);
359
+ expect(result.flow).toBeDefined();
360
+ });
361
+ });
362
+
363
+ // ─── mapVaultResults isolation ─────────────────────────────────
364
+
365
+ describe('mapVaultResults', () => {
366
+ it('maps critical severity to mandatory:true and strength:100', () => {
367
+ const results = [
368
+ {
369
+ entry: {
370
+ id: 'e1',
371
+ title: 'No skipping tests',
372
+ severity: 'critical',
373
+ type: 'anti-pattern',
374
+ },
375
+ score: 0.9,
376
+ breakdown: {} as never,
377
+ },
378
+ ];
379
+ const recs = mapVaultResults(results);
380
+ expect(recs[0].mandatory).toBe(true);
381
+ expect(recs[0].strength).toBe(100);
382
+ expect(recs[0].source).toBe('vault');
383
+ expect(recs[0].entryType).toBe('anti-pattern');
384
+ });
385
+
386
+ it('maps warning severity to mandatory:false and strength:80', () => {
387
+ const results = [
388
+ {
389
+ entry: { id: 'e2', title: 'Warning Rule', severity: 'warning', type: 'pattern' },
390
+ score: 0.7,
391
+ breakdown: {} as never,
392
+ },
393
+ ];
394
+ const recs = mapVaultResults(results);
395
+ expect(recs[0].mandatory).toBe(false);
396
+ expect(recs[0].strength).toBe(80);
397
+ });
398
+
399
+ it('forwards context and example when present', () => {
400
+ const results = [
401
+ {
402
+ entry: {
403
+ id: 'e3',
404
+ title: 'Rule',
405
+ severity: 'warning',
406
+ type: 'pattern',
407
+ context: 'ctx',
408
+ example: 'ex',
409
+ },
410
+ score: 0.5,
411
+ breakdown: {} as never,
412
+ },
413
+ ];
414
+ const recs = mapVaultResults(results);
415
+ expect(recs[0].context).toBe('ctx');
416
+ expect(recs[0].example).toBe('ex');
417
+ });
418
+
419
+ it('does not set context or example keys when absent', () => {
420
+ const results = [
421
+ {
422
+ entry: { id: 'e4', title: 'Bare Rule', severity: 'suggestion', type: 'pattern' },
423
+ score: 0.4,
424
+ breakdown: {} as never,
425
+ },
426
+ ];
427
+ const recs = mapVaultResults(results);
428
+ expect('context' in recs[0]).toBe(false);
429
+ expect('example' in recs[0]).toBe(false);
430
+ });
212
431
  });
213
432
 
214
433
  // ─── orchestrate_execute ──────────────────────────────────────
@@ -749,11 +968,10 @@ describe('createOrchestrateOps', () => {
749
968
  expect(rt.planner.complete).toHaveBeenCalledWith('plan-1');
750
969
  });
751
970
 
752
- it('assessment result includes non-empty reasoning for simple tasks', () => {
971
+ it('assessment result includes default reasoning when no signals triggered', () => {
753
972
  const result = assessTaskComplexity({ prompt: 'fix typo in README' });
754
973
  expect(result.classification).toBe('simple');
755
- expect(typeof result.reasoning).toBe('string');
756
- expect(result.reasoning.length).toBeGreaterThan(0);
974
+ expect(result.reasoning).toBe('No complexity signals detected — treating as simple task');
757
975
  });
758
976
 
759
977
  it('orchestrate_complete compounds operator signals when provided', async () => {
@@ -949,15 +1167,16 @@ describe('createOrchestrateOps', () => {
949
1167
  expect(fs.writeFileSync).not.toHaveBeenCalled();
950
1168
  });
951
1169
 
952
- it('assessment result includes non-empty reasoning for complex tasks', () => {
1170
+ it('assessment result lists triggered signals in reasoning for complex tasks', () => {
953
1171
  const result = assessTaskComplexity({
954
1172
  prompt: 'add authentication across all API routes',
955
1173
  filesEstimated: 8,
956
1174
  domains: ['auth', 'api', 'middleware'],
957
1175
  });
958
1176
  expect(result.classification).toBe('complex');
959
- expect(typeof result.reasoning).toBe('string');
960
- expect(result.reasoning.length).toBeGreaterThan(0);
1177
+ expect(result.reasoning).toBe(
1178
+ 'Complex: file-count, cross-cutting-keywords, multi-domain (score 50)',
1179
+ );
961
1180
  });
962
1181
  });
963
1182
  });
@@ -14,7 +14,48 @@ import path from 'node:path';
14
14
  import { z } from 'zod';
15
15
  import type { OpDefinition, FacadeConfig } from '../facades/types.js';
16
16
  import type { AgentRuntime } from './types.js';
17
- import { buildPlan } from '../flows/plan-builder.js';
17
+ import { buildPlan, type VaultConstraint } from '../flows/plan-builder.js';
18
+ import type { IntelligenceEntry } from '../intelligence/types.js';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Recommendation types + helpers (module-level for testability)
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export interface PlanRecommendation {
25
+ pattern: string;
26
+ strength: number;
27
+ entryId?: string;
28
+ source: 'vault' | 'brain';
29
+ context?: string;
30
+ example?: string;
31
+ mandatory: boolean;
32
+ entryType?: 'pattern' | 'anti-pattern' | 'rule' | 'playbook';
33
+ }
34
+
35
+ /**
36
+ * Map vault search results to PlanRecommendation[].
37
+ * Accepts both RankedResult[] (semantic search) and SearchResult[] (keyword fallback)
38
+ * since both share the same entry: IntelligenceEntry shape.
39
+ * Critical entries get strength:100 and mandatory:true; all others get 80/false.
40
+ */
41
+ export function mapVaultResults(
42
+ results: Array<{ entry: IntelligenceEntry; score: number }>,
43
+ ): PlanRecommendation[] {
44
+ return results.map((r) => {
45
+ const isCritical = r.entry.severity === 'critical';
46
+ const rec: PlanRecommendation = {
47
+ pattern: r.entry.title,
48
+ strength: isCritical ? 100 : 80,
49
+ entryId: r.entry.id,
50
+ source: 'vault',
51
+ mandatory: isCritical,
52
+ entryType: r.entry.type,
53
+ };
54
+ if (r.entry.context) rec.context = r.entry.context;
55
+ if (r.entry.example) rec.example = r.entry.example;
56
+ return rec;
57
+ });
58
+ }
18
59
  import { FlowExecutor, getPlanRunDir, loadManifest, saveManifest } from '../flows/executor.js';
19
60
  import { createDispatcher } from '../flows/dispatch-registry.js';
20
61
  import { runEpilogue } from '../flows/epilogue.js';
@@ -376,43 +417,67 @@ export function createOrchestrateOps(
376
417
  // 1. Detect intent from prompt
377
418
  const intent = detectIntent(prompt);
378
419
 
379
- // 2. Get brain recommendations — graceful degradation
380
- let recommendations: Array<{ pattern: string; strength: number; entryId?: string }> = [];
420
+ // 2. Build recommendations — vault first (authoritative), brain enriches (additive)
421
+ let recommendations: PlanRecommendation[] = [];
422
+
423
+ // Vault always runs first — curated explicit knowledge takes precedence.
424
+ // Prefer semantic search (vector-scored); fall back to keyword search.
425
+
381
426
  try {
382
- const raw = brainIntelligence.recommend({
427
+ const vaultResults = await brain.intelligentSearch(prompt, {
383
428
  domain,
384
- task: prompt,
385
429
  limit: 5,
386
430
  });
387
- recommendations = raw.map((r) => {
388
- // Look up vault entry ID by title for feedback tracking
389
- const entries = vault.search(r.pattern, { limit: 1 });
390
- const entryId =
391
- entries.length > 0 && entries[0].entry.title === r.pattern
392
- ? entries[0].entry.id
393
- : undefined;
394
- return { pattern: r.pattern, strength: r.strength, entryId };
395
- });
431
+ recommendations = mapVaultResults(vaultResults);
396
432
  } catch {
397
- // Brain has no data yet
398
- }
399
-
400
- // Fallback to vault if brain empty
401
- if (recommendations.length === 0) {
433
+ // Semantic search unavailable fall back to keyword search
402
434
  try {
403
435
  const vaultResults = vault.search(prompt, { domain, limit: 5 });
404
- recommendations = vaultResults.map((r) => ({
405
- pattern: r.entry.title,
406
- strength: 50,
407
- entryId: r.entry.id,
408
- }));
436
+ recommendations = mapVaultResults(vaultResults);
409
437
  } catch {
410
- // Vault search failed
438
+ // Vault unavailable — brain will cover below
439
+ }
440
+ }
441
+
442
+ // Brain enriches with learned usage patterns — additive, never replaces vault
443
+ try {
444
+ const brainResults = brainIntelligence.recommend({
445
+ domain,
446
+ task: prompt,
447
+ limit: 5,
448
+ });
449
+ for (const r of brainResults) {
450
+ if (!recommendations.find((rec) => rec.pattern === r.pattern)) {
451
+ recommendations.push({
452
+ pattern: r.pattern,
453
+ strength: r.strength,
454
+ source: 'brain',
455
+ mandatory: false,
456
+ });
457
+ }
411
458
  }
459
+ } catch {
460
+ // Brain has no data yet
412
461
  }
413
462
 
414
- // 3. Build flow-engine plan
415
- const plan = await buildPlan(intent, agentId, projectPath, runtime, prompt);
463
+ // 3. Build flow-engine plan — pass vault constraints for gate injection
464
+ const vaultConstraints: VaultConstraint[] = recommendations
465
+ .filter((r) => r.source === 'vault' && r.entryId)
466
+ .map((r) => ({
467
+ entryId: r.entryId!,
468
+ title: r.pattern,
469
+ context: r.context,
470
+ mandatory: r.mandatory,
471
+ entryType: r.entryType,
472
+ }));
473
+ const plan = await buildPlan(
474
+ intent,
475
+ agentId,
476
+ projectPath,
477
+ runtime,
478
+ prompt,
479
+ vaultConstraints,
480
+ );
416
481
 
417
482
  // 3b. Merge workflow overrides (gates + tools) if agent has a matching workflow
418
483
  let workflowApplied: string | undefined;
@@ -436,7 +501,8 @@ export function createOrchestrateOps(
436
501
 
437
502
  // 5. Also create a planner plan for lifecycle tracking (backward compat)
438
503
  const decisions = recommendations.map((r) => {
439
- const base = `Brain pattern: ${r.pattern} (strength: ${r.strength.toFixed(1)})`;
504
+ const label = r.source === 'vault' ? 'Vault pattern' : 'Brain pattern';
505
+ const base = `${label}: ${r.pattern} (strength: ${r.strength.toFixed(1)})`;
440
506
  return r.entryId ? `${base} [entryId:${r.entryId}]` : base;
441
507
  });
442
508
  const tasks = (params.tasks as Array<{ title: string; description: string }>) ?? [];
@@ -492,6 +558,7 @@ export function createOrchestrateOps(
492
558
  skippedCount: plan.skipped.length,
493
559
  warnings: plan.warnings,
494
560
  estimatedTools: plan.estimatedTools,
561
+ ...(plan.recommendations ? { vaultConstraints: plan.recommendations } : {}),
495
562
  ...(workflowApplied ? { workflowOverride: workflowApplied } : {}),
496
563
  },
497
564
  };
@@ -607,6 +674,27 @@ export function createOrchestrateOps(
607
674
  const healthStatus = contextHealth.check();
608
675
  const healthWarning = buildHealthWarning(healthStatus, vault);
609
676
 
677
+ // Check for subagent review stage requirements from matched playbook
678
+ const legacyPlanForReview = planner.get(planId);
679
+ let reviewStagesRequired: string[] | undefined;
680
+ if (legacyPlanForReview?.playbookSessionId && runtime.playbookExecutor) {
681
+ const session_ = runtime.playbookExecutor.getSession(
682
+ legacyPlanForReview.playbookSessionId,
683
+ );
684
+ if (session_) {
685
+ const postTaskGates = session_.gates.filter((g) => g.phase === 'post-task');
686
+ if (postTaskGates.length > 0) {
687
+ reviewStagesRequired = postTaskGates.map((g) => g.checkType);
688
+ }
689
+ }
690
+ } else if (
691
+ legacyPlanForReview?.playbookMatch?.genericId === 'generic-subagent-execution' ||
692
+ legacyPlanForReview?.playbookMatch?.label?.toLowerCase().includes('subagent')
693
+ ) {
694
+ // Playbook matched but no live session — surface known review stages
695
+ reviewStagesRequired = ['spec-review', 'quality-review'];
696
+ }
697
+
610
698
  return {
611
699
  plan: { id: planId, status: 'executing' },
612
700
  session,
@@ -618,6 +706,14 @@ export function createOrchestrateOps(
618
706
  durationMs: aggregated.durationMs,
619
707
  totalUsage: aggregated.totalUsage,
620
708
  },
709
+ ...(reviewStagesRequired
710
+ ? {
711
+ reviewStagesRequired,
712
+ reviewNote:
713
+ 'Subagent Execution playbook matched. Each completed task requires review evidence before status can be set to completed: ' +
714
+ reviewStagesRequired.join(' → '),
715
+ }
716
+ : {}),
621
717
  ...(reapedOrphans.length > 0 ? { reapedOrphans } : {}),
622
718
  ...(healthWarning ? { contextHealth: healthWarning } : {}),
623
719
  };
@@ -775,7 +871,10 @@ export function createOrchestrateOps(
775
871
  .string()
776
872
  .optional()
777
873
  .describe('ID of the executing plan to complete (optional for direct tasks)'),
778
- sessionId: z.string().describe('ID of the brain session to end'),
874
+ sessionId: z
875
+ .string()
876
+ .optional()
877
+ .describe('ID of the brain session to end (auto-resolved from planId if omitted)'),
779
878
  outcome: z
780
879
  .enum(['completed', 'abandoned', 'partial'])
781
880
  .optional()
@@ -842,7 +941,10 @@ export function createOrchestrateOps(
842
941
  }),
843
942
  handler: async (params) => {
844
943
  const planId = params.planId as string | undefined;
845
- const sessionId = params.sessionId as string;
944
+ const sessionId =
945
+ (params.sessionId as string | undefined) ??
946
+ (planId ? brainIntelligence.getSessionByPlanId(planId)?.id : undefined) ??
947
+ '';
846
948
  const outcome = (params.outcome as string) ?? 'completed';
847
949
  const completionSummary = (params.summary as string) ?? '';
848
950
  const toolsUsed = (params.toolsUsed as string[]) ?? [];
@@ -945,17 +1047,19 @@ export function createOrchestrateOps(
945
1047
  };
946
1048
  }
947
1049
 
948
- // End brain session — runs regardless of plan existence
1050
+ // End brain session — only if we have a valid sessionId
949
1051
  const fixTrail = evidenceReport ? buildFixTrailSummary(evidenceReport) : undefined;
950
- const session = brainIntelligence.lifecycle({
951
- action: 'end',
952
- sessionId,
953
- planId,
954
- planOutcome: outcome,
955
- toolsUsed,
956
- filesModified,
957
- ...(fixTrail ? { context: `Fix trail: ${fixTrail}` } : {}),
958
- });
1052
+ const session = sessionId
1053
+ ? brainIntelligence.lifecycle({
1054
+ action: 'end',
1055
+ sessionId,
1056
+ planId,
1057
+ planOutcome: outcome,
1058
+ toolsUsed,
1059
+ filesModified,
1060
+ ...(fixTrail ? { context: `Fix trail: ${fixTrail}` } : {}),
1061
+ })
1062
+ : null;
959
1063
 
960
1064
  // Record brain feedback for vault entries referenced in plan decisions
961
1065
  if (planObj && planObj.decisions) {
@@ -1018,6 +1122,11 @@ export function createOrchestrateOps(
1018
1122
  entry.plan.context.probes,
1019
1123
  entry.plan.context.projectPath,
1020
1124
  summary,
1125
+ {
1126
+ intent: entry.plan.intent,
1127
+ objective: completionSummary || entry.plan.summary,
1128
+ domain: entry.plan.context.entities?.technologies?.[0],
1129
+ },
1021
1130
  );
1022
1131
  } catch {
1023
1132
  // Epilogue is best-effort
@@ -1043,11 +1152,27 @@ export function createOrchestrateOps(
1043
1152
  }
1044
1153
  }
1045
1154
 
1155
+ // Best-effort worktree cleanup after plan completion
1156
+ try {
1157
+ const { worktreeReap } = await import('../utils/worktree-reaper.js');
1158
+ Promise.resolve()
1159
+ .then(() => worktreeReap((params.projectPath as string) ?? '.'))
1160
+ .catch(() => {
1161
+ /* best-effort */
1162
+ });
1163
+ } catch {
1164
+ /* skip silently */
1165
+ }
1166
+
1046
1167
  return {
1047
1168
  plan: completedPlan,
1048
1169
  session,
1049
1170
  extraction,
1050
- epilogue: epilogueResult,
1171
+ epilogue: epilogueResult ?? {
1172
+ completed: true,
1173
+ captured: false,
1174
+ note: 'no flow plan in store',
1175
+ },
1051
1176
  ...(impactReport ? { impactAnalysis: impactReport } : {}),
1052
1177
  evidenceReport,
1053
1178
  ...(warnings.length > 0 ? { warnings } : {}),
@@ -44,32 +44,6 @@ describe('createPackOps', () => {
44
44
  setHotRegister(null as never);
45
45
  });
46
46
 
47
- it('returns 4 ops', () => {
48
- runtime = makeMockRuntime();
49
- ops = createPackOps(runtime);
50
- expect(ops).toHaveLength(4);
51
- });
52
-
53
- it('has correct op names', () => {
54
- runtime = makeMockRuntime();
55
- ops = createPackOps(runtime);
56
- expect(ops.map((o) => o.name)).toEqual([
57
- 'pack_validate',
58
- 'pack_install',
59
- 'pack_list',
60
- 'pack_uninstall',
61
- ]);
62
- });
63
-
64
- it('assigns correct auth levels', () => {
65
- runtime = makeMockRuntime();
66
- ops = createPackOps(runtime);
67
- expect(findOp('pack_validate').auth).toBe('read');
68
- expect(findOp('pack_install').auth).toBe('admin');
69
- expect(findOp('pack_list').auth).toBe('read');
70
- expect(findOp('pack_uninstall').auth).toBe('admin');
71
- });
72
-
73
47
  describe('pack_validate', () => {
74
48
  it('delegates to packInstaller.validate', async () => {
75
49
  runtime = makeMockRuntime();