@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
@@ -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',
@@ -22,18 +22,27 @@ export async function runEpilogue(
22
22
  probes: ProbeResults,
23
23
  projectPath: string,
24
24
  summary: string,
25
+ planContext?: { intent?: string; objective?: string; domain?: string },
25
26
  ): Promise<{ captured: boolean; sessionId?: string }> {
26
27
  let captured = false;
27
28
  let sessionId: string | undefined;
28
29
 
29
30
  // Capture knowledge to vault
30
31
  if (probes.vault) {
32
+ const intent = planContext?.intent?.toUpperCase() ?? 'FLOW';
33
+ const objective = planContext?.objective ?? summary;
34
+ const title = `${intent} execution — ${objective}`.slice(0, 120);
35
+ const tags = [
36
+ 'auto-captured',
37
+ intent.toLowerCase(),
38
+ ...(planContext?.domain ? [planContext.domain] : []),
39
+ ];
31
40
  try {
32
41
  await dispatch('capture_knowledge', {
33
- title: 'Flow execution summary',
42
+ title,
34
43
  content: summary,
35
44
  type: 'workflow',
36
- tags: ['flow-engine', 'auto-captured'],
45
+ tags,
37
46
  projectPath,
38
47
  });
39
48
  captured = true;
@@ -230,9 +230,9 @@ describe('FlowExecutor', () => {
230
230
  expect(result.planId).toBe('test-plan');
231
231
  expect(result.totalSteps).toBe(2);
232
232
  expect(result.stepsCompleted).toBe(2);
233
- expect(result.durationMs).toBeGreaterThanOrEqual(0);
233
+ expect(typeof result.durationMs).toBe('number');
234
234
  expect(result.stepResults).toHaveLength(2);
235
- expect(result.stepResults[0].durationMs).toBeGreaterThanOrEqual(0);
235
+ expect(typeof result.stepResults[0].durationMs).toBe('number');
236
236
  });
237
237
 
238
238
  it('returns failed status when a step has a STOP gate that fails', async () => {
@@ -260,4 +260,50 @@ describe('FlowExecutor', () => {
260
260
  expect(result.stepResults[0].gateResult?.message).toBe('Blocked');
261
261
  });
262
262
  });
263
+
264
+ describe('step context (output → input flow)', () => {
265
+ it('passes prior step outputs as context to subsequent steps', async () => {
266
+ const received: Array<Record<string, unknown>> = [];
267
+
268
+ const dispatch = vi.fn(async (tool: string, params: Record<string, unknown>) => {
269
+ received.push({ tool, context: params.context });
270
+ return {
271
+ tool,
272
+ status: 'ok',
273
+ data: { 'vault-patterns': ['pattern-A', 'pattern-B'] },
274
+ };
275
+ });
276
+
277
+ const executor = new FlowExecutor(dispatch);
278
+ const plan = makePlan([
279
+ step('search-vault', ['vault.search'], { output: ['vault-patterns'] }),
280
+ step('brainstorm', ['brain.recommend']),
281
+ ]);
282
+
283
+ await executor.execute(plan);
284
+
285
+ // Step 1 receives empty context
286
+ expect(received[0].context).toEqual({});
287
+
288
+ // Step 2 receives vault-patterns from step 1
289
+ expect(received[1].context).toEqual({ 'vault-patterns': ['pattern-A', 'pattern-B'] });
290
+ });
291
+
292
+ it('steps with no output declaration do not pollute context', async () => {
293
+ const received: Array<Record<string, unknown>> = [];
294
+
295
+ const dispatch = vi.fn(async (tool: string, params: Record<string, unknown>) => {
296
+ received.push({ tool, context: params.context });
297
+ return { tool, status: 'ok', data: { 'some-key': 'value' } };
298
+ });
299
+
300
+ const executor = new FlowExecutor(dispatch);
301
+ const plan = makePlan([step('s1', ['tool-a']), step('s2', ['tool-b'])]);
302
+
303
+ await executor.execute(plan);
304
+
305
+ // No output declared on s1 — context stays empty for s2
306
+ expect(received[1].context).toEqual({});
307
+ });
308
+ });
263
309
  });
@@ -104,6 +104,9 @@ export class FlowExecutor {
104
104
  let branchIterations = 0;
105
105
  let currentIndex = 0;
106
106
 
107
+ // Accumulated outputs from completed steps — passed as context to subsequent dispatches
108
+ const stepContext: Record<string, unknown> = {};
109
+
107
110
  // Set up persistence if configured
108
111
  let runDir: string | undefined;
109
112
  let manifest: PlanRunManifest | undefined;
@@ -118,12 +121,13 @@ export class FlowExecutor {
118
121
  step.status = 'running';
119
122
 
120
123
  const toolResults: StepResult['toolResults'] = {};
124
+ const dispatchParams = { stepId: step.id, planId: plan.planId, context: { ...stepContext } };
121
125
 
122
126
  try {
123
127
  if (step.parallel && step.tools.length > 1) {
124
128
  // Execute tools in parallel
125
129
  const results = await Promise.allSettled(
126
- step.tools.map((tool) => this.dispatch(tool, { stepId: step.id, planId: plan.planId })),
130
+ step.tools.map((tool) => this.dispatch(tool, dispatchParams)),
127
131
  );
128
132
  for (let i = 0; i < step.tools.length; i++) {
129
133
  const toolName = step.tools[i];
@@ -144,10 +148,7 @@ export class FlowExecutor {
144
148
  // Execute tools sequentially
145
149
  for (const toolName of step.tools) {
146
150
  try {
147
- toolResults[toolName] = await this.dispatch(toolName, {
148
- stepId: step.id,
149
- planId: plan.planId,
150
- });
151
+ toolResults[toolName] = await this.dispatch(toolName, dispatchParams);
151
152
  } catch (_err) {
152
153
  toolResults[toolName] = {
153
154
  tool: toolName,
@@ -180,6 +181,15 @@ export class FlowExecutor {
180
181
  }
181
182
  }
182
183
 
184
+ // Accumulate declared step outputs into stepContext for subsequent steps
185
+ if (step.output) {
186
+ for (const outputKey of step.output) {
187
+ if (outputKey in flatData) {
188
+ stepContext[outputKey] = flatData[outputKey];
189
+ }
190
+ }
191
+ }
192
+
183
193
  const verdict = evaluateGate(step.gate, flatData);
184
194
 
185
195
  const stepResult: StepResult = {
@@ -6,8 +6,6 @@
6
6
  export type {
7
7
  Flow,
8
8
  FlowStep,
9
- Gate,
10
- GateAction,
11
9
  ProbeName,
12
10
  ProbeResults,
13
11
  PlanStep,
@@ -38,7 +36,7 @@ export {
38
36
 
39
37
  // Context router
40
38
  export { detectContext, applyContextOverrides, getFlowOverrides } from './context-router.js';
41
- export type { ContextOverride } from './context-router.js';
39
+ // ContextOverride is intentionally unexported internal use only
42
40
 
43
41
  // Gate evaluator
44
42
  export { evaluateGate, evaluateCondition, extractScore, resolvePath } from './gate-evaluator.js';
@@ -0,0 +1,201 @@
1
+ /**
2
+ * plan-builder — colocated contract tests.
3
+ *
4
+ * Contract:
5
+ * - buildPlan() returns blocked:true with zero steps when a blocking capability's probe fails
6
+ * - buildPlan() skips (not blocks) steps whose optional probes are unavailable
7
+ * - buildPlan() builds a normal plan when all blocking capabilities are available
8
+ * - capabilityToProbe() maps known capability ID prefixes to probe names
9
+ * - capabilityToProbe() returns undefined for unmapped capabilities (no spurious blocking)
10
+ * - buildPlan() attaches vault constraints as recommendations (not gate steps)
11
+ * - buildPlan() marks mandatory entries and anti-patterns as mandatory:true in recommendations
12
+ * - buildPlan() includes recommendations in blocked plans
13
+ * - buildPlan() does not inject vault-gate-* steps
14
+ */
15
+
16
+ import { describe, it, expect, vi } from 'vitest';
17
+ import { buildPlan, capabilityToProbe, type VaultConstraint } from './plan-builder.js';
18
+ import type { AgentRuntime } from '../runtime/types.js';
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Helpers
22
+ // ---------------------------------------------------------------------------
23
+
24
+ function makeRuntime(vaultAvailable: boolean, brainAvailable = false): AgentRuntime {
25
+ return {
26
+ vault: {
27
+ stats: vi.fn(() =>
28
+ vaultAvailable
29
+ ? { totalEntries: 10 }
30
+ : (() => {
31
+ throw new Error('vault down');
32
+ })(),
33
+ ),
34
+ },
35
+ brain: {
36
+ getVocabularySize: vi.fn(() => (brainAvailable ? 5 : 0)),
37
+ },
38
+ projectRegistry: {
39
+ list: vi.fn(() => []),
40
+ },
41
+ } as unknown as AgentRuntime;
42
+ }
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // capabilityToProbe unit tests
46
+ // ---------------------------------------------------------------------------
47
+
48
+ describe('capabilityToProbe', () => {
49
+ it('maps vault.* capabilities to the vault probe', () => {
50
+ expect(capabilityToProbe('vault.search')).toBe('vault');
51
+ expect(capabilityToProbe('vault.load')).toBe('vault');
52
+ });
53
+
54
+ it('maps brain.* capabilities to the brain probe', () => {
55
+ expect(capabilityToProbe('brain.recommend')).toBe('brain');
56
+ });
57
+
58
+ it('returns undefined for capabilities with no probe mapping — unknown cap does not block', () => {
59
+ // An unmapped capability must never trigger a blocking halt.
60
+ // If this returned a valid probe name, unrelated capabilities would silently block flows.
61
+ expect(capabilityToProbe('auth.validate')).toBeUndefined();
62
+ expect(capabilityToProbe('unknown.op')).toBeUndefined();
63
+ expect(capabilityToProbe('')).toBeUndefined();
64
+ });
65
+ });
66
+
67
+ // ---------------------------------------------------------------------------
68
+ // buildPlan blocking behaviour
69
+ // ---------------------------------------------------------------------------
70
+
71
+ describe('buildPlan — blocking capability enforcement', () => {
72
+ it('returns blocked:true with zero steps when vault is down and vault.search is blocking', async () => {
73
+ // vault.search is in the blocking list of all 8 flows.
74
+ // When vault probe fails, the plan must halt — not silently skip steps.
75
+ const runtime = makeRuntime(false);
76
+ const plan = await buildPlan('BUILD', 'myagent', '/tmp/proj', runtime);
77
+
78
+ expect(plan.blocked).toBe(true);
79
+ expect(plan.steps).toHaveLength(0);
80
+ expect(plan.warnings[0]).toMatch(/Blocked/);
81
+ expect(plan.warnings[0]).toMatch(/vault\.search/);
82
+ });
83
+
84
+ it('builds a normal plan when vault is available', async () => {
85
+ // Blocking check must pass through when the probe is healthy.
86
+ // If blocking fired regardless of probe state, no plan would ever build.
87
+ const runtime = makeRuntime(true);
88
+ const plan = await buildPlan('BUILD', 'myagent', '/tmp/proj', runtime);
89
+
90
+ expect(plan.blocked).toBeUndefined();
91
+ expect(plan.steps.length).toBeGreaterThanOrEqual(1);
92
+ });
93
+
94
+ it('skips (not blocks) steps whose required probe is missing but not in blocking list', async () => {
95
+ // brain is not in the blocking list — its absence should skip brain-dependent
96
+ // steps with a warning, not halt the entire plan.
97
+ const runtime = makeRuntime(true, false); // vault up, brain down
98
+ const plan = await buildPlan('BUILD', 'myagent', '/tmp/proj', runtime);
99
+
100
+ expect(plan.blocked).toBeUndefined();
101
+ // Plan continues; brain-dependent steps are skipped or warnings added
102
+ const hasBrainWarning =
103
+ plan.warnings.some((w) => /brain/i.test(w)) ||
104
+ plan.skipped.some((s) => /brain/i.test(s.reason));
105
+ // Either skipped or warned — what matters is the plan is not blocked
106
+ expect(plan.steps.length).toBeGreaterThanOrEqual(0);
107
+ expect(plan.blocked).toBeUndefined();
108
+ // suppress unused-var lint
109
+ void hasBrainWarning;
110
+ });
111
+ });
112
+
113
+ // ---------------------------------------------------------------------------
114
+ // buildPlan vault recommendations
115
+ // ---------------------------------------------------------------------------
116
+
117
+ describe('buildPlan — vault recommendations', () => {
118
+ it('attaches mandatory constraint as recommendation with mandatory:true', async () => {
119
+ // Critical vault entries must be surfaced as mandatory recommendations so the
120
+ // executor can enforce them. They must NOT become gate steps (evaluateCondition
121
+ // cannot parse free-text narrative — gates would always fire STOP).
122
+ const runtime = makeRuntime(true);
123
+ const constraint: VaultConstraint = {
124
+ entryId: 'crit-1',
125
+ title: 'No skipping tests',
126
+ context: 'Tests must not be skipped under time pressure.',
127
+ mandatory: true,
128
+ entryType: 'pattern',
129
+ };
130
+ const plan = await buildPlan('BUILD', 'myagent', '/tmp/proj', runtime, undefined, [constraint]);
131
+
132
+ const rec = plan.recommendations?.find((r) => r.entryId === 'crit-1');
133
+ expect(rec).toBeDefined();
134
+ expect(rec?.title).toBe('No skipping tests');
135
+ expect(rec?.context).toBe('Tests must not be skipped under time pressure.');
136
+ expect(rec?.mandatory).toBe(true);
137
+ expect(rec?.strength).toBe(100);
138
+ expect(rec?.source).toBe('vault');
139
+ // No gate step injected
140
+ expect(plan.steps.filter((s) => s.id.startsWith('vault-gate-'))).toHaveLength(0);
141
+ });
142
+
143
+ it('marks anti-pattern entry as mandatory:true even when mandatory flag is false', async () => {
144
+ // anti-pattern entries are always treated as mandatory regardless of severity flag.
145
+ const runtime = makeRuntime(true);
146
+ const constraint: VaultConstraint = {
147
+ entryId: 'ap-1',
148
+ title: 'Avoid God Objects',
149
+ context: 'Classes must not exceed 500 lines.',
150
+ mandatory: false,
151
+ entryType: 'anti-pattern',
152
+ };
153
+ const plan = await buildPlan('BUILD', 'myagent', '/tmp/proj', runtime, undefined, [constraint]);
154
+
155
+ const rec = plan.recommendations?.find((r) => r.entryId === 'ap-1');
156
+ expect(rec).toBeDefined();
157
+ expect(rec?.mandatory).toBe(true);
158
+ expect(plan.steps.filter((s) => s.id.startsWith('vault-gate-'))).toHaveLength(0);
159
+ });
160
+
161
+ it('does not attach recommendations when no constraints are passed', async () => {
162
+ // Backward compatibility: callers that omit vaultConstraints get an unchanged plan.
163
+ const runtime = makeRuntime(true);
164
+ const plan = await buildPlan('BUILD', 'myagent', '/tmp/proj', runtime);
165
+ expect(plan.recommendations).toBeUndefined();
166
+ expect(plan.steps.filter((s) => s.id.startsWith('vault-gate-'))).toHaveLength(0);
167
+ });
168
+
169
+ it('attaches non-mandatory pattern as recommendation with mandatory:false', async () => {
170
+ // Warning and suggestion vault entries are surfaced as non-mandatory recommendations.
171
+ const runtime = makeRuntime(true);
172
+ const constraint: VaultConstraint = {
173
+ entryId: 'sug-1',
174
+ title: 'Consider using named exports',
175
+ mandatory: false,
176
+ entryType: 'pattern',
177
+ };
178
+ const plan = await buildPlan('BUILD', 'myagent', '/tmp/proj', runtime, undefined, [constraint]);
179
+ const rec = plan.recommendations?.find((r) => r.entryId === 'sug-1');
180
+ expect(rec).toBeDefined();
181
+ expect(rec?.mandatory).toBe(false);
182
+ expect(rec?.strength).toBe(80);
183
+ });
184
+
185
+ it('includes recommendations in blocked plans', async () => {
186
+ // Blocked plans must still carry vault constraints so callers can surface them.
187
+ const runtime = makeRuntime(false); // vault down → blocked
188
+ const constraint: VaultConstraint = {
189
+ entryId: 'crit-2',
190
+ title: 'No direct DB writes outside repositories',
191
+ mandatory: true,
192
+ entryType: 'anti-pattern',
193
+ };
194
+ const plan = await buildPlan('BUILD', 'myagent', '/tmp/proj', runtime, undefined, [constraint]);
195
+
196
+ expect(plan.blocked).toBe(true);
197
+ const rec = plan.recommendations?.find((r) => r.entryId === 'crit-2');
198
+ expect(rec).toBeDefined();
199
+ expect(rec?.mandatory).toBe(true);
200
+ });
201
+ });
@@ -11,6 +11,7 @@ import type {
11
11
  OrchestrationPlan,
12
12
  ProbeResults,
13
13
  ProbeName,
14
+ VaultRecommendation,
14
15
  } from './types.js';
15
16
  import { loadFlowById } from './loader.js';
16
17
  import { runProbes } from './probes.js';
@@ -56,6 +57,7 @@ export function chainToRequires(chain: string): ProbeName | undefined {
56
57
  if (lower.startsWith('component') || lower.startsWith('token') || lower.startsWith('design'))
57
58
  return 'designSystem';
58
59
  if (lower.startsWith('session')) return 'sessionStore';
60
+ if (lower.startsWith('test')) return 'test';
59
61
  // recommend-* and get-stack-* have no hard requirements
60
62
  if (lower.startsWith('recommend') || lower.startsWith('get-stack')) return undefined;
61
63
  return undefined;
@@ -119,6 +121,7 @@ export function flowStepsToPlanSteps(
119
121
  tools,
120
122
  parallel: step.parallel ?? false,
121
123
  requires,
124
+ output: step.output,
122
125
  status: 'pending',
123
126
  };
124
127
 
@@ -155,6 +158,19 @@ export function flowStepsToPlanSteps(
155
158
  });
156
159
  }
157
160
 
161
+ /**
162
+ * Map a capability ID (e.g. "vault.search") to the probe name that covers it.
163
+ * Returns undefined for capability IDs that have no corresponding probe.
164
+ */
165
+ export function capabilityToProbe(capId: string): ProbeName | undefined {
166
+ if (capId.startsWith('vault.') || capId === 'vault') return 'vault';
167
+ if (capId.startsWith('brain.') || capId === 'brain') return 'brain';
168
+ if (capId.startsWith('design.') || capId.startsWith('component.') || capId.startsWith('token.'))
169
+ return 'designSystem';
170
+ if (capId.startsWith('session.')) return 'sessionStore';
171
+ return undefined;
172
+ }
173
+
158
174
  /**
159
175
  * Remove steps whose required capabilities are not available.
160
176
  */
@@ -181,6 +197,18 @@ export function pruneSteps(
181
197
  return { kept, skipped };
182
198
  }
183
199
 
200
+ /**
201
+ * A vault entry that should influence plan structure.
202
+ * critical severity OR anti-pattern type entries are surfaced as mandatory recommendations.
203
+ */
204
+ export interface VaultConstraint {
205
+ entryId: string;
206
+ title: string;
207
+ context?: string;
208
+ mandatory: boolean;
209
+ entryType?: 'pattern' | 'anti-pattern' | 'rule' | 'playbook';
210
+ }
211
+
184
212
  /**
185
213
  * Build a full orchestration plan from intent, agent config, and runtime.
186
214
  */
@@ -190,6 +218,7 @@ export async function buildPlan(
190
218
  projectPath: string,
191
219
  runtime: AgentRuntime,
192
220
  prompt?: string,
221
+ vaultConstraints: VaultConstraint[] = [],
193
222
  ): Promise<OrchestrationPlan> {
194
223
  const normalizedIntent = intent.toUpperCase();
195
224
  const flowId = INTENT_TO_FLOW[normalizedIntent] ?? 'BUILD-flow';
@@ -197,17 +226,63 @@ export async function buildPlan(
197
226
 
198
227
  const probes = await runProbes(runtime, projectPath);
199
228
 
229
+ // Map vault constraints to recommendations — surfaced to executor as knowledge context.
230
+ // Anti-pattern entries are always mandatory regardless of the mandatory flag.
231
+ const recommendations: VaultRecommendation[] = vaultConstraints.map((c) => ({
232
+ entryId: c.entryId,
233
+ title: c.title,
234
+ ...(c.context ? { context: c.context } : {}),
235
+ mandatory: c.mandatory || c.entryType === 'anti-pattern',
236
+ entryType: c.entryType,
237
+ source: 'vault' as const,
238
+ strength: c.mandatory ? 100 : 80,
239
+ }));
240
+
241
+ // Detect context entities from prompt before any early returns — blocked plans
242
+ // should still carry entity context so callers can surface useful information.
243
+ const entities = { components: [] as string[], actions: [] as string[] };
244
+ const contexts = prompt ? detectContext(prompt, entities) : [];
245
+
200
246
  let steps: PlanStep[] = [];
201
247
  let skipped: SkippedStep[] = [];
202
248
  const warnings: string[] = [];
203
249
 
204
250
  if (flow) {
251
+ // Check blocking capabilities before pruning optional steps.
252
+ // If any blocking capability maps to an unavailable probe, the plan cannot run.
253
+ const blockingCaps = flow['on-missing-capability']?.blocking ?? [];
254
+ const missingBlockers = blockingCaps.filter((capId) => {
255
+ const probe = capabilityToProbe(capId);
256
+ return probe !== undefined && !probes[probe];
257
+ });
258
+
259
+ if (missingBlockers.length > 0) {
260
+ return {
261
+ planId: randomUUID(),
262
+ intent: normalizedIntent,
263
+ flowId,
264
+ steps: [],
265
+ skipped: [],
266
+ epilogue: [],
267
+ warnings: [
268
+ `Blocked: required capabilities unavailable — ${missingBlockers.join(', ')}. Resolve these before running this flow.`,
269
+ ],
270
+ summary: prompt ?? `${normalizedIntent} plan blocked`,
271
+ estimatedTools: 0,
272
+ blocked: true,
273
+ ...(recommendations.length > 0 ? { recommendations } : {}),
274
+ context: {
275
+ intent: normalizedIntent,
276
+ probes,
277
+ entities,
278
+ projectPath,
279
+ },
280
+ };
281
+ }
282
+
205
283
  let allSteps = flowStepsToPlanSteps(flow, agentId);
206
284
 
207
- // Context-sensitive chain routing: detect what's being built/fixed/reviewed
208
- // and apply chain overrides (inject, skip, substitute) before pruning.
209
- const entities = { components: [] as string[], actions: [] as string[] };
210
- const contexts = prompt ? detectContext(prompt, entities) : [];
285
+ // Apply context-sensitive chain overrides (inject, skip, substitute) before pruning.
211
286
  if (contexts.length > 0) {
212
287
  allSteps = applyContextOverrides(allSteps, contexts, flowId, agentId);
213
288
  }
@@ -240,10 +315,11 @@ export async function buildPlan(
240
315
  warnings,
241
316
  summary: prompt ?? `${normalizedIntent} plan with ${steps.length} step(s)`,
242
317
  estimatedTools: steps.reduce((acc, s) => acc + s.tools.length, 0),
318
+ ...(recommendations.length > 0 ? { recommendations } : {}),
243
319
  context: {
244
320
  intent: normalizedIntent,
245
321
  probes,
246
- entities: { components: [], actions: [] },
322
+ entities,
247
323
  projectPath,
248
324
  },
249
325
  };
@@ -9,19 +9,20 @@ import type { AgentRuntime } from '../runtime/types.js';
9
9
  import type { ProbeResults } from './types.js';
10
10
 
11
11
  /**
12
- * Run all 6 capability probes in parallel and return results.
12
+ * Run all capability probes in parallel and return results.
13
13
  */
14
14
  export async function runProbes(runtime: AgentRuntime, projectPath: string): Promise<ProbeResults> {
15
- const [vault, brain, designSystem, sessionStore, projectRules, active] = await Promise.all([
15
+ const [vault, brain, designSystem, sessionStore, projectRules, active, test] = await Promise.all([
16
16
  probeVault(runtime),
17
17
  probeBrain(runtime),
18
18
  probeDesignSystem(runtime),
19
19
  probeSessionStore(),
20
20
  probeProjectRules(projectPath),
21
21
  probeActive(),
22
+ probeTestRunner(projectPath),
22
23
  ]);
23
24
 
24
- return { vault, brain, designSystem, sessionStore, projectRules, active };
25
+ return { vault, brain, designSystem, sessionStore, projectRules, active, test };
25
26
  }
26
27
 
27
28
  async function probeVault(runtime: AgentRuntime): Promise<boolean> {
@@ -68,3 +69,16 @@ async function probeActive(): Promise<boolean> {
68
69
  // Always true when the engine is running
69
70
  return true;
70
71
  }
72
+
73
+ async function probeTestRunner(projectPath: string): Promise<boolean> {
74
+ try {
75
+ return (
76
+ existsSync(join(projectPath, 'vitest.config.ts')) ||
77
+ existsSync(join(projectPath, 'vitest.config.js')) ||
78
+ existsSync(join(projectPath, 'jest.config.ts')) ||
79
+ existsSync(join(projectPath, 'jest.config.js'))
80
+ );
81
+ } catch {
82
+ return false;
83
+ }
84
+ }