@soleri/core 2.4.0 → 2.6.0

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 (328) hide show
  1. package/dist/brain/brain.d.ts +7 -0
  2. package/dist/brain/brain.d.ts.map +1 -1
  3. package/dist/brain/brain.js +56 -9
  4. package/dist/brain/brain.js.map +1 -1
  5. package/dist/brain/intelligence.d.ts +1 -0
  6. package/dist/brain/intelligence.d.ts.map +1 -1
  7. package/dist/brain/intelligence.js +164 -148
  8. package/dist/brain/intelligence.js.map +1 -1
  9. package/dist/brain/types.d.ts +2 -2
  10. package/dist/brain/types.d.ts.map +1 -1
  11. package/dist/cognee/client.d.ts +3 -0
  12. package/dist/cognee/client.d.ts.map +1 -1
  13. package/dist/cognee/client.js +17 -0
  14. package/dist/cognee/client.js.map +1 -1
  15. package/dist/cognee/sync-manager.d.ts +94 -0
  16. package/dist/cognee/sync-manager.d.ts.map +1 -0
  17. package/dist/cognee/sync-manager.js +293 -0
  18. package/dist/cognee/sync-manager.js.map +1 -0
  19. package/dist/control/identity-manager.d.ts +3 -1
  20. package/dist/control/identity-manager.d.ts.map +1 -1
  21. package/dist/control/identity-manager.js +49 -51
  22. package/dist/control/identity-manager.js.map +1 -1
  23. package/dist/control/intent-router.d.ts +1 -0
  24. package/dist/control/intent-router.d.ts.map +1 -1
  25. package/dist/control/intent-router.js +32 -32
  26. package/dist/control/intent-router.js.map +1 -1
  27. package/dist/curator/curator.d.ts +9 -1
  28. package/dist/curator/curator.d.ts.map +1 -1
  29. package/dist/curator/curator.js +104 -92
  30. package/dist/curator/curator.js.map +1 -1
  31. package/dist/errors/classify.d.ts +13 -0
  32. package/dist/errors/classify.d.ts.map +1 -0
  33. package/dist/errors/classify.js +97 -0
  34. package/dist/errors/classify.js.map +1 -0
  35. package/dist/errors/index.d.ts +6 -0
  36. package/dist/errors/index.d.ts.map +1 -0
  37. package/dist/errors/index.js +4 -0
  38. package/dist/errors/index.js.map +1 -0
  39. package/dist/errors/retry.d.ts +40 -0
  40. package/dist/errors/retry.d.ts.map +1 -0
  41. package/dist/errors/retry.js +97 -0
  42. package/dist/errors/retry.js.map +1 -0
  43. package/dist/errors/types.d.ts +48 -0
  44. package/dist/errors/types.d.ts.map +1 -0
  45. package/dist/errors/types.js +59 -0
  46. package/dist/errors/types.js.map +1 -0
  47. package/dist/governance/governance.d.ts +1 -0
  48. package/dist/governance/governance.d.ts.map +1 -1
  49. package/dist/governance/governance.js +51 -68
  50. package/dist/governance/governance.js.map +1 -1
  51. package/dist/index.d.ts +26 -5
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +22 -3
  54. package/dist/index.js.map +1 -1
  55. package/dist/intake/content-classifier.d.ts +14 -0
  56. package/dist/intake/content-classifier.d.ts.map +1 -0
  57. package/dist/intake/content-classifier.js +125 -0
  58. package/dist/intake/content-classifier.js.map +1 -0
  59. package/dist/intake/dedup-gate.d.ts +17 -0
  60. package/dist/intake/dedup-gate.d.ts.map +1 -0
  61. package/dist/intake/dedup-gate.js +66 -0
  62. package/dist/intake/dedup-gate.js.map +1 -0
  63. package/dist/intake/intake-pipeline.d.ts +63 -0
  64. package/dist/intake/intake-pipeline.d.ts.map +1 -0
  65. package/dist/intake/intake-pipeline.js +373 -0
  66. package/dist/intake/intake-pipeline.js.map +1 -0
  67. package/dist/intake/types.d.ts +65 -0
  68. package/dist/intake/types.d.ts.map +1 -0
  69. package/dist/intake/types.js +3 -0
  70. package/dist/intake/types.js.map +1 -0
  71. package/dist/intelligence/loader.js +1 -1
  72. package/dist/intelligence/loader.js.map +1 -1
  73. package/dist/intelligence/types.d.ts +3 -1
  74. package/dist/intelligence/types.d.ts.map +1 -1
  75. package/dist/loop/loop-manager.d.ts +58 -7
  76. package/dist/loop/loop-manager.d.ts.map +1 -1
  77. package/dist/loop/loop-manager.js +280 -6
  78. package/dist/loop/loop-manager.js.map +1 -1
  79. package/dist/loop/types.d.ts +69 -1
  80. package/dist/loop/types.d.ts.map +1 -1
  81. package/dist/loop/types.js +4 -1
  82. package/dist/loop/types.js.map +1 -1
  83. package/dist/persistence/index.d.ts +4 -0
  84. package/dist/persistence/index.d.ts.map +1 -0
  85. package/dist/persistence/index.js +3 -0
  86. package/dist/persistence/index.js.map +1 -0
  87. package/dist/persistence/postgres-provider.d.ts +46 -0
  88. package/dist/persistence/postgres-provider.d.ts.map +1 -0
  89. package/dist/persistence/postgres-provider.js +115 -0
  90. package/dist/persistence/postgres-provider.js.map +1 -0
  91. package/dist/persistence/sqlite-provider.d.ts +28 -0
  92. package/dist/persistence/sqlite-provider.d.ts.map +1 -0
  93. package/dist/persistence/sqlite-provider.js +97 -0
  94. package/dist/persistence/sqlite-provider.js.map +1 -0
  95. package/dist/persistence/types.d.ts +58 -0
  96. package/dist/persistence/types.d.ts.map +1 -0
  97. package/dist/persistence/types.js +8 -0
  98. package/dist/persistence/types.js.map +1 -0
  99. package/dist/planning/gap-analysis.d.ts +47 -4
  100. package/dist/planning/gap-analysis.d.ts.map +1 -1
  101. package/dist/planning/gap-analysis.js +190 -13
  102. package/dist/planning/gap-analysis.js.map +1 -1
  103. package/dist/planning/gap-types.d.ts +1 -1
  104. package/dist/planning/gap-types.d.ts.map +1 -1
  105. package/dist/planning/gap-types.js.map +1 -1
  106. package/dist/planning/planner.d.ts +277 -9
  107. package/dist/planning/planner.d.ts.map +1 -1
  108. package/dist/planning/planner.js +611 -46
  109. package/dist/planning/planner.js.map +1 -1
  110. package/dist/playbooks/generic/brainstorming.d.ts +9 -0
  111. package/dist/playbooks/generic/brainstorming.d.ts.map +1 -0
  112. package/dist/playbooks/generic/brainstorming.js +105 -0
  113. package/dist/playbooks/generic/brainstorming.js.map +1 -0
  114. package/dist/playbooks/generic/code-review.d.ts +11 -0
  115. package/dist/playbooks/generic/code-review.d.ts.map +1 -0
  116. package/dist/playbooks/generic/code-review.js +176 -0
  117. package/dist/playbooks/generic/code-review.js.map +1 -0
  118. package/dist/playbooks/generic/subagent-execution.d.ts +9 -0
  119. package/dist/playbooks/generic/subagent-execution.d.ts.map +1 -0
  120. package/dist/playbooks/generic/subagent-execution.js +68 -0
  121. package/dist/playbooks/generic/subagent-execution.js.map +1 -0
  122. package/dist/playbooks/generic/systematic-debugging.d.ts +9 -0
  123. package/dist/playbooks/generic/systematic-debugging.d.ts.map +1 -0
  124. package/dist/playbooks/generic/systematic-debugging.js +87 -0
  125. package/dist/playbooks/generic/systematic-debugging.js.map +1 -0
  126. package/dist/playbooks/generic/tdd.d.ts +9 -0
  127. package/dist/playbooks/generic/tdd.d.ts.map +1 -0
  128. package/dist/playbooks/generic/tdd.js +70 -0
  129. package/dist/playbooks/generic/tdd.js.map +1 -0
  130. package/dist/playbooks/generic/verification.d.ts +9 -0
  131. package/dist/playbooks/generic/verification.d.ts.map +1 -0
  132. package/dist/playbooks/generic/verification.js +74 -0
  133. package/dist/playbooks/generic/verification.js.map +1 -0
  134. package/dist/playbooks/index.d.ts +4 -0
  135. package/dist/playbooks/index.d.ts.map +1 -0
  136. package/dist/playbooks/index.js +5 -0
  137. package/dist/playbooks/index.js.map +1 -0
  138. package/dist/playbooks/playbook-registry.d.ts +42 -0
  139. package/dist/playbooks/playbook-registry.d.ts.map +1 -0
  140. package/dist/playbooks/playbook-registry.js +227 -0
  141. package/dist/playbooks/playbook-registry.js.map +1 -0
  142. package/dist/playbooks/playbook-seeder.d.ts +47 -0
  143. package/dist/playbooks/playbook-seeder.d.ts.map +1 -0
  144. package/dist/playbooks/playbook-seeder.js +104 -0
  145. package/dist/playbooks/playbook-seeder.js.map +1 -0
  146. package/dist/playbooks/playbook-types.d.ts +132 -0
  147. package/dist/playbooks/playbook-types.d.ts.map +1 -0
  148. package/dist/playbooks/playbook-types.js +12 -0
  149. package/dist/playbooks/playbook-types.js.map +1 -0
  150. package/dist/project/project-registry.d.ts +4 -4
  151. package/dist/project/project-registry.d.ts.map +1 -1
  152. package/dist/project/project-registry.js +30 -57
  153. package/dist/project/project-registry.js.map +1 -1
  154. package/dist/prompts/index.d.ts +4 -0
  155. package/dist/prompts/index.d.ts.map +1 -0
  156. package/dist/prompts/index.js +3 -0
  157. package/dist/prompts/index.js.map +1 -0
  158. package/dist/prompts/parser.d.ts +17 -0
  159. package/dist/prompts/parser.d.ts.map +1 -0
  160. package/dist/prompts/parser.js +47 -0
  161. package/dist/prompts/parser.js.map +1 -0
  162. package/dist/prompts/template-manager.d.ts +25 -0
  163. package/dist/prompts/template-manager.d.ts.map +1 -0
  164. package/dist/prompts/template-manager.js +71 -0
  165. package/dist/prompts/template-manager.js.map +1 -0
  166. package/dist/prompts/types.d.ts +26 -0
  167. package/dist/prompts/types.d.ts.map +1 -0
  168. package/dist/prompts/types.js +5 -0
  169. package/dist/prompts/types.js.map +1 -0
  170. package/dist/runtime/admin-extra-ops.d.ts +5 -3
  171. package/dist/runtime/admin-extra-ops.d.ts.map +1 -1
  172. package/dist/runtime/admin-extra-ops.js +348 -11
  173. package/dist/runtime/admin-extra-ops.js.map +1 -1
  174. package/dist/runtime/admin-ops.d.ts.map +1 -1
  175. package/dist/runtime/admin-ops.js +10 -3
  176. package/dist/runtime/admin-ops.js.map +1 -1
  177. package/dist/runtime/capture-ops.d.ts.map +1 -1
  178. package/dist/runtime/capture-ops.js +20 -2
  179. package/dist/runtime/capture-ops.js.map +1 -1
  180. package/dist/runtime/cognee-sync-ops.d.ts +12 -0
  181. package/dist/runtime/cognee-sync-ops.d.ts.map +1 -0
  182. package/dist/runtime/cognee-sync-ops.js +55 -0
  183. package/dist/runtime/cognee-sync-ops.js.map +1 -0
  184. package/dist/runtime/core-ops.d.ts +8 -6
  185. package/dist/runtime/core-ops.d.ts.map +1 -1
  186. package/dist/runtime/core-ops.js +226 -9
  187. package/dist/runtime/core-ops.js.map +1 -1
  188. package/dist/runtime/curator-extra-ops.d.ts +2 -2
  189. package/dist/runtime/curator-extra-ops.d.ts.map +1 -1
  190. package/dist/runtime/curator-extra-ops.js +15 -3
  191. package/dist/runtime/curator-extra-ops.js.map +1 -1
  192. package/dist/runtime/domain-ops.js +2 -2
  193. package/dist/runtime/domain-ops.js.map +1 -1
  194. package/dist/runtime/grading-ops.d.ts.map +1 -1
  195. package/dist/runtime/grading-ops.js.map +1 -1
  196. package/dist/runtime/intake-ops.d.ts +14 -0
  197. package/dist/runtime/intake-ops.d.ts.map +1 -0
  198. package/dist/runtime/intake-ops.js +110 -0
  199. package/dist/runtime/intake-ops.js.map +1 -0
  200. package/dist/runtime/loop-ops.d.ts +5 -4
  201. package/dist/runtime/loop-ops.d.ts.map +1 -1
  202. package/dist/runtime/loop-ops.js +84 -12
  203. package/dist/runtime/loop-ops.js.map +1 -1
  204. package/dist/runtime/memory-cross-project-ops.d.ts.map +1 -1
  205. package/dist/runtime/memory-cross-project-ops.js.map +1 -1
  206. package/dist/runtime/memory-extra-ops.js +5 -5
  207. package/dist/runtime/memory-extra-ops.js.map +1 -1
  208. package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
  209. package/dist/runtime/orchestrate-ops.js +8 -2
  210. package/dist/runtime/orchestrate-ops.js.map +1 -1
  211. package/dist/runtime/planning-extra-ops.d.ts +13 -5
  212. package/dist/runtime/planning-extra-ops.d.ts.map +1 -1
  213. package/dist/runtime/planning-extra-ops.js +381 -18
  214. package/dist/runtime/planning-extra-ops.js.map +1 -1
  215. package/dist/runtime/playbook-ops.d.ts +14 -0
  216. package/dist/runtime/playbook-ops.d.ts.map +1 -0
  217. package/dist/runtime/playbook-ops.js +141 -0
  218. package/dist/runtime/playbook-ops.js.map +1 -0
  219. package/dist/runtime/project-ops.d.ts.map +1 -1
  220. package/dist/runtime/project-ops.js +7 -2
  221. package/dist/runtime/project-ops.js.map +1 -1
  222. package/dist/runtime/runtime.d.ts.map +1 -1
  223. package/dist/runtime/runtime.js +28 -9
  224. package/dist/runtime/runtime.js.map +1 -1
  225. package/dist/runtime/types.d.ts +8 -0
  226. package/dist/runtime/types.d.ts.map +1 -1
  227. package/dist/runtime/vault-extra-ops.d.ts +4 -2
  228. package/dist/runtime/vault-extra-ops.d.ts.map +1 -1
  229. package/dist/runtime/vault-extra-ops.js +383 -4
  230. package/dist/runtime/vault-extra-ops.js.map +1 -1
  231. package/dist/vault/playbook.d.ts +34 -0
  232. package/dist/vault/playbook.d.ts.map +1 -0
  233. package/dist/vault/playbook.js +60 -0
  234. package/dist/vault/playbook.js.map +1 -0
  235. package/dist/vault/vault.d.ts +52 -32
  236. package/dist/vault/vault.d.ts.map +1 -1
  237. package/dist/vault/vault.js +300 -181
  238. package/dist/vault/vault.js.map +1 -1
  239. package/package.json +9 -3
  240. package/src/__tests__/admin-extra-ops.test.ts +62 -15
  241. package/src/__tests__/admin-ops.test.ts +2 -2
  242. package/src/__tests__/brain.test.ts +3 -3
  243. package/src/__tests__/cognee-integration.test.ts +80 -0
  244. package/src/__tests__/cognee-sync-manager.test.ts +103 -0
  245. package/src/__tests__/core-ops.test.ts +36 -4
  246. package/src/__tests__/curator-extra-ops.test.ts +24 -2
  247. package/src/__tests__/errors.test.ts +388 -0
  248. package/src/__tests__/grading-ops.test.ts +28 -7
  249. package/src/__tests__/intake-pipeline.test.ts +162 -0
  250. package/src/__tests__/loop-ops.test.ts +74 -3
  251. package/src/__tests__/memory-cross-project-ops.test.ts +3 -1
  252. package/src/__tests__/orchestrate-ops.test.ts +8 -3
  253. package/src/__tests__/persistence.test.ts +291 -0
  254. package/src/__tests__/planner.test.ts +99 -21
  255. package/src/__tests__/planning-extra-ops.test.ts +168 -10
  256. package/src/__tests__/playbook-registry.test.ts +326 -0
  257. package/src/__tests__/playbook-seeder.test.ts +163 -0
  258. package/src/__tests__/playbook.test.ts +389 -0
  259. package/src/__tests__/postgres-provider.test.ts +58 -0
  260. package/src/__tests__/project-ops.test.ts +18 -4
  261. package/src/__tests__/template-manager.test.ts +222 -0
  262. package/src/__tests__/vault-extra-ops.test.ts +82 -7
  263. package/src/__tests__/vault.test.ts +184 -0
  264. package/src/brain/brain.ts +71 -9
  265. package/src/brain/intelligence.ts +258 -307
  266. package/src/brain/types.ts +2 -2
  267. package/src/cognee/client.ts +18 -0
  268. package/src/cognee/sync-manager.ts +389 -0
  269. package/src/control/identity-manager.ts +77 -75
  270. package/src/control/intent-router.ts +55 -57
  271. package/src/curator/curator.ts +199 -139
  272. package/src/errors/classify.ts +102 -0
  273. package/src/errors/index.ts +5 -0
  274. package/src/errors/retry.ts +132 -0
  275. package/src/errors/types.ts +81 -0
  276. package/src/governance/governance.ts +90 -107
  277. package/src/index.ts +116 -3
  278. package/src/intake/content-classifier.ts +146 -0
  279. package/src/intake/dedup-gate.ts +92 -0
  280. package/src/intake/intake-pipeline.ts +503 -0
  281. package/src/intake/types.ts +69 -0
  282. package/src/intelligence/loader.ts +1 -1
  283. package/src/intelligence/types.ts +3 -1
  284. package/src/loop/loop-manager.ts +325 -7
  285. package/src/loop/types.ts +72 -1
  286. package/src/persistence/index.ts +9 -0
  287. package/src/persistence/postgres-provider.ts +157 -0
  288. package/src/persistence/sqlite-provider.ts +115 -0
  289. package/src/persistence/types.ts +74 -0
  290. package/src/planning/gap-analysis.ts +286 -17
  291. package/src/planning/gap-types.ts +4 -1
  292. package/src/planning/planner.ts +828 -55
  293. package/src/playbooks/generic/brainstorming.ts +110 -0
  294. package/src/playbooks/generic/code-review.ts +181 -0
  295. package/src/playbooks/generic/subagent-execution.ts +74 -0
  296. package/src/playbooks/generic/systematic-debugging.ts +92 -0
  297. package/src/playbooks/generic/tdd.ts +75 -0
  298. package/src/playbooks/generic/verification.ts +79 -0
  299. package/src/playbooks/index.ts +27 -0
  300. package/src/playbooks/playbook-registry.ts +284 -0
  301. package/src/playbooks/playbook-seeder.ts +119 -0
  302. package/src/playbooks/playbook-types.ts +162 -0
  303. package/src/project/project-registry.ts +81 -74
  304. package/src/prompts/index.ts +3 -0
  305. package/src/prompts/parser.ts +59 -0
  306. package/src/prompts/template-manager.ts +77 -0
  307. package/src/prompts/types.ts +28 -0
  308. package/src/runtime/admin-extra-ops.ts +391 -13
  309. package/src/runtime/admin-ops.ts +17 -6
  310. package/src/runtime/capture-ops.ts +25 -6
  311. package/src/runtime/cognee-sync-ops.ts +63 -0
  312. package/src/runtime/core-ops.ts +258 -8
  313. package/src/runtime/curator-extra-ops.ts +17 -3
  314. package/src/runtime/domain-ops.ts +2 -2
  315. package/src/runtime/grading-ops.ts +11 -2
  316. package/src/runtime/intake-ops.ts +126 -0
  317. package/src/runtime/loop-ops.ts +96 -13
  318. package/src/runtime/memory-cross-project-ops.ts +1 -2
  319. package/src/runtime/memory-extra-ops.ts +5 -5
  320. package/src/runtime/orchestrate-ops.ts +8 -2
  321. package/src/runtime/planning-extra-ops.ts +414 -23
  322. package/src/runtime/playbook-ops.ts +169 -0
  323. package/src/runtime/project-ops.ts +9 -3
  324. package/src/runtime/runtime.ts +36 -10
  325. package/src/runtime/types.ts +8 -0
  326. package/src/runtime/vault-extra-ops.ts +425 -4
  327. package/src/vault/playbook.ts +87 -0
  328. package/src/vault/vault.ts +419 -235
@@ -1,13 +1,113 @@
1
1
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
2
+ import { createHash } from 'node:crypto';
2
3
  import { dirname } from 'node:path';
3
4
  import type { PlanGap } from './gap-types.js';
4
5
  import { SEVERITY_WEIGHTS, CATEGORY_PENALTY_CAPS } from './gap-types.js';
5
6
  import { runGapAnalysis } from './gap-analysis.js';
6
7
  import type { GapAnalysisOptions } from './gap-analysis.js';
7
8
 
8
- export type PlanStatus = 'draft' | 'approved' | 'executing' | 'completed';
9
+ /**
10
+ * Plan lifecycle status.
11
+ * Ported from Salvador's PlanLifecycleStatus with full 8-state lifecycle.
12
+ *
13
+ * Lifecycle: brainstorming → draft → approved → executing → [validating] → reconciling → completed → archived
14
+ */
15
+ export type PlanStatus =
16
+ | 'brainstorming'
17
+ | 'draft'
18
+ | 'approved'
19
+ | 'executing'
20
+ | 'validating'
21
+ | 'reconciling'
22
+ | 'completed'
23
+ | 'archived';
24
+
25
+ /**
26
+ * Valid status transitions.
27
+ * Each key maps to the set of statuses it can transition to.
28
+ * Ported from Salvador's LIFECYCLE_TRANSITIONS.
29
+ */
30
+ export const LIFECYCLE_TRANSITIONS: Record<PlanStatus, PlanStatus[]> = {
31
+ brainstorming: ['draft'],
32
+ draft: ['approved'],
33
+ approved: ['executing'],
34
+ executing: ['validating', 'reconciling'],
35
+ validating: ['reconciling', 'executing'],
36
+ reconciling: ['completed'],
37
+ completed: ['archived'],
38
+ archived: [],
39
+ };
40
+
41
+ /**
42
+ * Statuses where the 30-minute TTL should NOT apply.
43
+ * Plans in these states may span multiple sessions.
44
+ */
45
+ export const NON_EXPIRING_STATUSES: PlanStatus[] = [
46
+ 'brainstorming',
47
+ 'executing',
48
+ 'validating',
49
+ 'reconciling',
50
+ ];
51
+
52
+ /**
53
+ * Validate a lifecycle status transition.
54
+ * Returns true if the transition is valid, false otherwise.
55
+ */
56
+ export function isValidTransition(from: PlanStatus, to: PlanStatus): boolean {
57
+ return LIFECYCLE_TRANSITIONS[from].includes(to);
58
+ }
59
+
60
+ /**
61
+ * Get the valid next statuses for a given status.
62
+ */
63
+ export function getValidNextStatuses(status: PlanStatus): PlanStatus[] {
64
+ return LIFECYCLE_TRANSITIONS[status];
65
+ }
66
+
67
+ /**
68
+ * Check if a status should have TTL expiration.
69
+ * Plans in executing/reconciling states persist indefinitely.
70
+ */
71
+ export function shouldExpire(status: PlanStatus): boolean {
72
+ return !NON_EXPIRING_STATUSES.includes(status);
73
+ }
74
+
9
75
  export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed';
10
76
 
77
+ export interface TaskEvidence {
78
+ /** What the evidence proves (maps to an acceptance criterion). */
79
+ criterion: string;
80
+ /** Evidence content — command output, URL, file path, description. */
81
+ content: string;
82
+ /** Evidence type. */
83
+ type: 'command_output' | 'url' | 'file' | 'description';
84
+ submittedAt: number;
85
+ }
86
+
87
+ export interface TaskMetrics {
88
+ durationMs?: number;
89
+ iterations?: number;
90
+ toolCalls?: number;
91
+ modelTier?: string;
92
+ estimatedCostUsd?: number;
93
+ }
94
+
95
+ export interface TaskDeliverable {
96
+ type: 'file' | 'vault_entry' | 'url';
97
+ path: string;
98
+ hash?: string;
99
+ verifiedAt?: number;
100
+ stale?: boolean;
101
+ }
102
+
103
+ export interface ExecutionSummary {
104
+ totalDurationMs: number;
105
+ tasksCompleted: number;
106
+ tasksSkipped: number;
107
+ tasksFailed: number;
108
+ avgTaskDurationMs: number;
109
+ }
110
+
11
111
  export interface PlanTask {
12
112
  id: string;
13
113
  title: string;
@@ -15,20 +115,64 @@ export interface PlanTask {
15
115
  status: TaskStatus;
16
116
  /** Optional dependency IDs — tasks that must complete before this one. */
17
117
  dependsOn?: string[];
118
+ /** Evidence submitted for task acceptance criteria. */
119
+ evidence?: TaskEvidence[];
120
+ /** Whether this task has been verified (all evidence checked + reviews passed). */
121
+ verified?: boolean;
122
+ /** Task-level acceptance criteria (for verification checking). */
123
+ acceptanceCriteria?: string[];
124
+ /** Timestamp when task was first moved to in_progress. */
125
+ startedAt?: number;
126
+ /** Timestamp when task reached a terminal state (completed/skipped/failed). */
127
+ completedAt?: number;
128
+ /** Per-task execution metrics. */
129
+ metrics?: TaskMetrics;
130
+ /** Deliverables produced by this task. */
131
+ deliverables?: TaskDeliverable[];
18
132
  updatedAt: number;
19
133
  }
20
134
 
21
135
  export interface DriftItem {
136
+ /** Type of drift */
22
137
  type: 'skipped' | 'added' | 'modified' | 'reordered';
138
+ /** What drifted */
23
139
  description: string;
140
+ /** How much this affected the plan */
24
141
  impact: 'low' | 'medium' | 'high';
142
+ /** Why the drift occurred */
25
143
  rationale: string;
26
144
  }
27
145
 
146
+ /**
147
+ * Severity weights for drift accuracy score calculation.
148
+ * Score = 100 - sum(drift_items * weight_per_impact)
149
+ * Ported from Salvador's plan-lifecycle-types.ts.
150
+ */
151
+ export const DRIFT_WEIGHTS: Record<DriftItem['impact'], number> = {
152
+ high: 20,
153
+ medium: 10,
154
+ low: 5,
155
+ };
156
+
157
+ /**
158
+ * Calculate drift accuracy score from drift items.
159
+ * Score = max(0, 100 - sum(weight_per_impact))
160
+ * Ported from Salvador's calculateDriftScore.
161
+ */
162
+ export function calculateDriftScore(items: DriftItem[]): number {
163
+ let deductions = 0;
164
+ for (const item of items) {
165
+ deductions += DRIFT_WEIGHTS[item.impact];
166
+ }
167
+ return Math.max(0, 100 - deductions);
168
+ }
169
+
28
170
  export interface ReconciliationReport {
29
171
  planId: string;
172
+ /** Accuracy score: 100 = perfect execution, 0 = total drift. Impact-weighted. */
30
173
  accuracy: number;
31
174
  driftItems: DriftItem[];
175
+ /** Human-readable summary of the drift */
32
176
  summary: string;
33
177
  reconciledAt: number;
34
178
  }
@@ -88,13 +232,38 @@ export function calculateScore(gaps: PlanGap[], iteration: number = 1): number {
88
232
  return Math.max(0, 100 - deductions);
89
233
  }
90
234
 
235
+ /**
236
+ * A structured decision with rationale.
237
+ * Ported from Salvador's PlanContent.decisions.
238
+ */
239
+ export interface PlanDecision {
240
+ decision: string;
241
+ rationale: string;
242
+ }
243
+
91
244
  export interface Plan {
92
245
  id: string;
93
246
  objective: string;
94
247
  scope: string;
95
248
  status: PlanStatus;
96
- decisions: string[];
249
+ /**
250
+ * Decisions can be flat strings (backward compat) or structured {decision, rationale}.
251
+ * New plans should prefer PlanDecision[].
252
+ */
253
+ decisions: (string | PlanDecision)[];
97
254
  tasks: PlanTask[];
255
+ /** High-level approach description. Ported from Salvador's PlanContent. */
256
+ approach?: string;
257
+ /** Additional context for the plan. */
258
+ context?: string;
259
+ /** Measurable success criteria. */
260
+ success_criteria?: string[];
261
+ /** Tools to use in execution order. */
262
+ tool_chain?: string[];
263
+ /** Flow definition to follow (e.g., 'developer', 'reviewer', 'designer'). */
264
+ flow?: string;
265
+ /** Target operational mode (e.g., 'build', 'review', 'fix'). */
266
+ target_mode?: string;
98
267
  /** Reconciliation report — populated by reconcile(). */
99
268
  reconciliation?: ReconciliationReport;
100
269
  /** Review evidence — populated by addReview(). */
@@ -103,6 +272,14 @@ export interface Plan {
103
272
  latestCheck?: PlanCheck;
104
273
  /** All check history. */
105
274
  checks: PlanCheck[];
275
+ /** Matched playbook info (set by orchestration layer via playbook_match). */
276
+ playbookMatch?: {
277
+ label: string;
278
+ genericId?: string;
279
+ domainId?: string;
280
+ };
281
+ /** Aggregate execution metrics — populated by reconcile() and complete(). */
282
+ executionSummary?: ExecutionSummary;
106
283
  createdAt: number;
107
284
  updatedAt: number;
108
285
  }
@@ -148,15 +325,23 @@ export class Planner {
148
325
  create(params: {
149
326
  objective: string;
150
327
  scope: string;
151
- decisions?: string[];
328
+ decisions?: (string | PlanDecision)[];
152
329
  tasks?: Array<{ title: string; description: string }>;
330
+ approach?: string;
331
+ context?: string;
332
+ success_criteria?: string[];
333
+ tool_chain?: string[];
334
+ flow?: string;
335
+ target_mode?: string;
336
+ /** Start in 'brainstorming' instead of 'draft'. Default: 'draft'. */
337
+ initialStatus?: 'brainstorming' | 'draft';
153
338
  }): Plan {
154
339
  const now = Date.now();
155
340
  const plan: Plan = {
156
341
  id: `plan-${now}-${Math.random().toString(36).slice(2, 8)}`,
157
342
  objective: params.objective,
158
343
  scope: params.scope,
159
- status: 'draft',
344
+ status: params.initialStatus ?? 'draft',
160
345
  decisions: params.decisions ?? [],
161
346
  tasks: (params.tasks ?? []).map((t, i) => ({
162
347
  id: `task-${i + 1}`,
@@ -165,6 +350,12 @@ export class Planner {
165
350
  status: 'pending' as TaskStatus,
166
351
  updatedAt: now,
167
352
  })),
353
+ ...(params.approach !== undefined && { approach: params.approach }),
354
+ ...(params.context !== undefined && { context: params.context }),
355
+ ...(params.success_criteria !== undefined && { success_criteria: params.success_criteria }),
356
+ ...(params.tool_chain !== undefined && { tool_chain: params.tool_chain }),
357
+ ...(params.flow !== undefined && { flow: params.flow }),
358
+ ...(params.target_mode !== undefined && { target_mode: params.target_mode }),
168
359
  checks: [],
169
360
  createdAt: now,
170
361
  updatedAt: now,
@@ -182,13 +373,38 @@ export class Planner {
182
373
  return [...this.store.plans];
183
374
  }
184
375
 
376
+ /**
377
+ * Transition a plan to a new status using the typed FSM.
378
+ * Validates that the transition is allowed before applying it.
379
+ */
380
+ private transition(plan: Plan, to: PlanStatus): void {
381
+ if (!isValidTransition(plan.status, to)) {
382
+ const valid = getValidNextStatuses(plan.status);
383
+ throw new Error(
384
+ `Invalid transition: '${plan.status}' → '${to}'. ` +
385
+ `Valid transitions from '${plan.status}': ${valid.length > 0 ? valid.join(', ') : 'none'}`,
386
+ );
387
+ }
388
+ plan.status = to;
389
+ plan.updatedAt = Date.now();
390
+ }
391
+
392
+ /**
393
+ * Promote a brainstorming plan to draft status.
394
+ * Only allowed from 'brainstorming'.
395
+ */
396
+ promoteToDraft(planId: string): Plan {
397
+ const plan = this.get(planId);
398
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
399
+ this.transition(plan, 'draft');
400
+ this.save();
401
+ return plan;
402
+ }
403
+
185
404
  approve(planId: string): Plan {
186
405
  const plan = this.get(planId);
187
406
  if (!plan) throw new Error(`Plan not found: ${planId}`);
188
- if (plan.status !== 'draft')
189
- throw new Error(`Cannot approve plan in '${plan.status}' status — must be 'draft'`);
190
- plan.status = 'approved';
191
- plan.updatedAt = Date.now();
407
+ this.transition(plan, 'approved');
192
408
  this.save();
193
409
  return plan;
194
410
  }
@@ -196,10 +412,7 @@ export class Planner {
196
412
  startExecution(planId: string): Plan {
197
413
  const plan = this.get(planId);
198
414
  if (!plan) throw new Error(`Plan not found: ${planId}`);
199
- if (plan.status !== 'approved')
200
- throw new Error(`Cannot execute plan in '${plan.status}' status — must be 'approved'`);
201
- plan.status = 'executing';
202
- plan.updatedAt = Date.now();
415
+ this.transition(plan, 'executing');
203
416
  this.save();
204
417
  return plan;
205
418
  }
@@ -207,37 +420,87 @@ export class Planner {
207
420
  updateTask(planId: string, taskId: string, status: TaskStatus): Plan {
208
421
  const plan = this.get(planId);
209
422
  if (!plan) throw new Error(`Plan not found: ${planId}`);
210
- if (plan.status !== 'executing')
423
+ if (plan.status !== 'executing' && plan.status !== 'validating')
211
424
  throw new Error(
212
- `Cannot update tasks on plan in '${plan.status}' status — must be 'executing'`,
425
+ `Cannot update tasks on plan in '${plan.status}' status — must be 'executing' or 'validating'`,
213
426
  );
214
427
  const task = plan.tasks.find((t) => t.id === taskId);
215
428
  if (!task) throw new Error(`Task not found: ${taskId}`);
429
+
430
+ const now = Date.now();
431
+
432
+ // Auto-set startedAt on first in_progress transition
433
+ if (status === 'in_progress' && !task.startedAt) {
434
+ task.startedAt = now;
435
+ }
436
+
437
+ // Auto-set completedAt and compute durationMs on terminal transitions
438
+ if (status === 'completed' || status === 'skipped' || status === 'failed') {
439
+ task.completedAt = now;
440
+ if (task.startedAt) {
441
+ if (!task.metrics) task.metrics = {};
442
+ task.metrics.durationMs = now - task.startedAt;
443
+ }
444
+ }
445
+
216
446
  task.status = status;
217
- task.updatedAt = Date.now();
218
- plan.updatedAt = Date.now();
447
+ task.updatedAt = now;
448
+ plan.updatedAt = now;
449
+ this.save();
450
+ return plan;
451
+ }
452
+
453
+ /**
454
+ * Transition plan to 'validating' state (post-execution verification).
455
+ * Only allowed from 'executing'.
456
+ */
457
+ startValidation(planId: string): Plan {
458
+ const plan = this.get(planId);
459
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
460
+ this.transition(plan, 'validating');
461
+ this.save();
462
+ return plan;
463
+ }
464
+
465
+ /**
466
+ * Transition plan to 'reconciling' state.
467
+ * Allowed from 'executing' or 'validating'.
468
+ */
469
+ startReconciliation(planId: string): Plan {
470
+ const plan = this.get(planId);
471
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
472
+ this.transition(plan, 'reconciling');
219
473
  this.save();
220
474
  return plan;
221
475
  }
222
476
 
477
+ /**
478
+ * Complete a plan. Only allowed from 'reconciling'.
479
+ * Use startReconciliation() + reconcile() + complete() for the full lifecycle,
480
+ * or reconcile() which auto-transitions through reconciling → completed.
481
+ */
223
482
  complete(planId: string): Plan {
224
483
  const plan = this.get(planId);
225
484
  if (!plan) throw new Error(`Plan not found: ${planId}`);
226
- if (plan.status !== 'executing')
227
- throw new Error(`Cannot complete plan in '${plan.status}' status — must be 'executing'`);
228
- plan.status = 'completed';
229
- plan.updatedAt = Date.now();
485
+ plan.executionSummary = this.computeExecutionSummary(plan);
486
+ this.transition(plan, 'completed');
230
487
  this.save();
231
488
  return plan;
232
489
  }
233
490
 
234
491
  getExecuting(): Plan[] {
235
- return this.store.plans.filter((p) => p.status === 'executing');
492
+ return this.store.plans.filter((p) => p.status === 'executing' || p.status === 'validating');
236
493
  }
237
494
 
238
495
  getActive(): Plan[] {
239
496
  return this.store.plans.filter(
240
- (p) => p.status === 'draft' || p.status === 'approved' || p.status === 'executing',
497
+ (p) =>
498
+ p.status === 'brainstorming' ||
499
+ p.status === 'draft' ||
500
+ p.status === 'approved' ||
501
+ p.status === 'executing' ||
502
+ p.status === 'validating' ||
503
+ p.status === 'reconciling',
241
504
  );
242
505
  }
243
506
 
@@ -250,20 +513,34 @@ export class Planner {
250
513
  changes: {
251
514
  objective?: string;
252
515
  scope?: string;
253
- decisions?: string[];
516
+ decisions?: (string | PlanDecision)[];
254
517
  addTasks?: Array<{ title: string; description: string }>;
255
518
  removeTasks?: string[];
519
+ approach?: string;
520
+ context?: string;
521
+ success_criteria?: string[];
522
+ tool_chain?: string[];
523
+ flow?: string;
524
+ target_mode?: string;
256
525
  },
257
526
  ): Plan {
258
527
  const plan = this.get(planId);
259
528
  if (!plan) throw new Error(`Plan not found: ${planId}`);
260
- if (plan.status !== 'draft')
261
- throw new Error(`Cannot iterate plan in '${plan.status}' status — must be 'draft'`);
529
+ if (plan.status !== 'draft' && plan.status !== 'brainstorming')
530
+ throw new Error(
531
+ `Cannot iterate plan in '${plan.status}' status — must be 'draft' or 'brainstorming'`,
532
+ );
262
533
 
263
534
  const now = Date.now();
264
535
  if (changes.objective !== undefined) plan.objective = changes.objective;
265
536
  if (changes.scope !== undefined) plan.scope = changes.scope;
266
537
  if (changes.decisions !== undefined) plan.decisions = changes.decisions;
538
+ if (changes.approach !== undefined) plan.approach = changes.approach;
539
+ if (changes.context !== undefined) plan.context = changes.context;
540
+ if (changes.success_criteria !== undefined) plan.success_criteria = changes.success_criteria;
541
+ if (changes.tool_chain !== undefined) plan.tool_chain = changes.tool_chain;
542
+ if (changes.flow !== undefined) plan.flow = changes.flow;
543
+ if (changes.target_mode !== undefined) plan.target_mode = changes.target_mode;
267
544
 
268
545
  // Remove tasks by ID
269
546
  if (changes.removeTasks && changes.removeTasks.length > 0) {
@@ -300,13 +577,18 @@ export class Planner {
300
577
  */
301
578
  splitTasks(
302
579
  planId: string,
303
- tasks: Array<{ title: string; description: string; dependsOn?: string[] }>,
580
+ tasks: Array<{
581
+ title: string;
582
+ description: string;
583
+ dependsOn?: string[];
584
+ acceptanceCriteria?: string[];
585
+ }>,
304
586
  ): Plan {
305
587
  const plan = this.get(planId);
306
588
  if (!plan) throw new Error(`Plan not found: ${planId}`);
307
- if (plan.status !== 'draft' && plan.status !== 'approved')
589
+ if (plan.status !== 'brainstorming' && plan.status !== 'draft' && plan.status !== 'approved')
308
590
  throw new Error(
309
- `Cannot split tasks on plan in '${plan.status}' status — must be 'draft' or 'approved'`,
591
+ `Cannot split tasks on plan in '${plan.status}' status — must be 'brainstorming', 'draft', or 'approved'`,
310
592
  );
311
593
 
312
594
  const now = Date.now();
@@ -316,6 +598,7 @@ export class Planner {
316
598
  description: t.description,
317
599
  status: 'pending' as TaskStatus,
318
600
  dependsOn: t.dependsOn,
601
+ ...(t.acceptanceCriteria && { acceptanceCriteria: t.acceptanceCriteria }),
319
602
  updatedAt: now,
320
603
  }));
321
604
 
@@ -338,39 +621,53 @@ export class Planner {
338
621
 
339
622
  /**
340
623
  * Reconcile a plan — compare what was planned vs what actually happened.
341
- * Only allowed on 'executing' or 'completed' plans.
624
+ * Uses impact-weighted drift scoring (ported from Salvador's calculateDriftScore).
625
+ *
626
+ * Transitions: executing → reconciling → completed (automatic).
627
+ * Also allowed from 'validating' and 'reconciling' states.
342
628
  */
343
629
  reconcile(
344
630
  planId: string,
345
631
  report: {
346
632
  actualOutcome: string;
347
633
  driftItems?: DriftItem[];
634
+ /** Who initiated the reconciliation. */
635
+ reconciledBy?: 'human' | 'auto';
348
636
  },
349
637
  ): Plan {
350
638
  const plan = this.get(planId);
351
639
  if (!plan) throw new Error(`Plan not found: ${planId}`);
352
- if (plan.status !== 'executing' && plan.status !== 'completed')
640
+ if (
641
+ plan.status !== 'executing' &&
642
+ plan.status !== 'validating' &&
643
+ plan.status !== 'reconciling'
644
+ )
353
645
  throw new Error(
354
- `Cannot reconcile plan in '${plan.status}' status — must be 'executing' or 'completed'`,
646
+ `Cannot reconcile plan in '${plan.status}' status — must be 'executing', 'validating', or 'reconciling'`,
355
647
  );
356
648
 
357
649
  const driftItems = report.driftItems ?? [];
358
- const totalTasks = plan.tasks.length;
359
- const driftCount = driftItems.length;
360
- const accuracy = totalTasks > 0 ? Math.round(((totalTasks - driftCount) / totalTasks) * 100) : 100;
650
+
651
+ // Impact-weighted drift scoring (ported from Salvador)
652
+ const accuracy = calculateDriftScore(driftItems);
361
653
 
362
654
  plan.reconciliation = {
363
655
  planId,
364
- accuracy: Math.max(0, Math.min(100, accuracy)),
656
+ accuracy,
365
657
  driftItems,
366
658
  summary: report.actualOutcome,
367
659
  reconciledAt: Date.now(),
368
660
  };
369
661
 
370
- // If still executing, mark completed
371
- if (plan.status === 'executing') {
372
- plan.status = 'completed';
662
+ // Compute execution summary from per-task metrics
663
+ plan.executionSummary = this.computeExecutionSummary(plan);
664
+
665
+ // Transition through reconciling → completed via FSM
666
+ if (plan.status === 'executing' || plan.status === 'validating') {
667
+ plan.status = 'reconciling';
373
668
  }
669
+ // Auto-complete after reconciliation
670
+ plan.status = 'completed';
374
671
  plan.updatedAt = Date.now();
375
672
  this.save();
376
673
  return plan;
@@ -418,7 +715,12 @@ export class Planner {
418
715
  getDispatch(
419
716
  planId: string,
420
717
  taskId: string,
421
- ): { task: PlanTask; unmetDependencies: PlanTask[]; ready: boolean } {
718
+ ): {
719
+ task: PlanTask;
720
+ unmetDependencies: PlanTask[];
721
+ ready: boolean;
722
+ deliverableStatus?: { count: number; staleCount: number };
723
+ } {
422
724
  const plan = this.get(planId);
423
725
  if (!plan) throw new Error(`Plan not found: ${planId}`);
424
726
  const task = plan.tasks.find((t) => t.id === taskId);
@@ -434,22 +736,476 @@ export class Planner {
434
736
  }
435
737
  }
436
738
 
437
- return { task, unmetDependencies, ready: unmetDependencies.length === 0 };
739
+ const result: {
740
+ task: PlanTask;
741
+ unmetDependencies: PlanTask[];
742
+ ready: boolean;
743
+ deliverableStatus?: { count: number; staleCount: number };
744
+ } = {
745
+ task,
746
+ unmetDependencies,
747
+ ready: unmetDependencies.length === 0,
748
+ };
749
+
750
+ // Include deliverable status if deliverables exist
751
+ if (task.deliverables && task.deliverables.length > 0) {
752
+ result.deliverableStatus = {
753
+ count: task.deliverables.length,
754
+ staleCount: task.deliverables.filter((d) => d.stale).length,
755
+ };
756
+ }
757
+
758
+ return result;
438
759
  }
439
760
 
761
+ // ─── Execution Metrics & Deliverables ──────────────────────────
762
+
440
763
  /**
441
- * Archive completed plans older than the given number of days.
442
- * Removes them from the active store and returns the archived plans.
764
+ * Compute aggregate execution summary from per-task metrics.
765
+ * Called from reconcile() and complete() to populate plan.executionSummary.
443
766
  */
444
- archive(olderThanDays: number): Plan[] {
445
- const cutoff = Date.now() - olderThanDays * 24 * 60 * 60 * 1000;
767
+ private computeExecutionSummary(plan: Plan): ExecutionSummary {
768
+ let totalDurationMs = 0;
769
+ let tasksCompleted = 0;
770
+ let tasksSkipped = 0;
771
+ let tasksFailed = 0;
772
+ let tasksWithDuration = 0;
773
+
774
+ for (const task of plan.tasks) {
775
+ if (task.status === 'completed') tasksCompleted++;
776
+ else if (task.status === 'skipped') tasksSkipped++;
777
+ else if (task.status === 'failed') tasksFailed++;
778
+
779
+ if (task.metrics?.durationMs) {
780
+ totalDurationMs += task.metrics.durationMs;
781
+ tasksWithDuration++;
782
+ }
783
+ }
784
+
785
+ return {
786
+ totalDurationMs,
787
+ tasksCompleted,
788
+ tasksSkipped,
789
+ tasksFailed,
790
+ avgTaskDurationMs:
791
+ tasksWithDuration > 0 ? Math.round(totalDurationMs / tasksWithDuration) : 0,
792
+ };
793
+ }
794
+
795
+ /**
796
+ * Submit a deliverable for a task. Auto-computes SHA-256 hash for file deliverables.
797
+ */
798
+ submitDeliverable(
799
+ planId: string,
800
+ taskId: string,
801
+ deliverable: { type: TaskDeliverable['type']; path: string; hash?: string },
802
+ ): PlanTask {
803
+ const plan = this.get(planId);
804
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
805
+ const task = plan.tasks.find((t) => t.id === taskId);
806
+ if (!task) throw new Error(`Task not found: ${taskId}`);
807
+
808
+ const entry: TaskDeliverable = {
809
+ type: deliverable.type,
810
+ path: deliverable.path,
811
+ };
812
+
813
+ // Auto-compute hash for file deliverables
814
+ if (deliverable.type === 'file' && !deliverable.hash) {
815
+ try {
816
+ if (existsSync(deliverable.path)) {
817
+ const content = readFileSync(deliverable.path);
818
+ entry.hash = createHash('sha256').update(content).digest('hex');
819
+ }
820
+ } catch {
821
+ // Graceful degradation — skip hash if file can't be read
822
+ }
823
+ } else if (deliverable.hash) {
824
+ entry.hash = deliverable.hash;
825
+ }
826
+
827
+ if (!task.deliverables) task.deliverables = [];
828
+ task.deliverables.push(entry);
829
+ task.updatedAt = Date.now();
830
+ plan.updatedAt = Date.now();
831
+ this.save();
832
+ return task;
833
+ }
834
+
835
+ /**
836
+ * Verify all deliverables for a task.
837
+ * - file: checks existsSync + SHA-256 hash match
838
+ * - vault_entry: checks vault.get(path) non-null (requires vault instance)
839
+ * - url: skips (just records, no fetch)
840
+ */
841
+ verifyDeliverables(
842
+ planId: string,
843
+ taskId: string,
844
+ vault?: { get(id: string): unknown | null },
845
+ ): { verified: boolean; deliverables: TaskDeliverable[]; staleCount: number } {
846
+ const plan = this.get(planId);
847
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
848
+ const task = plan.tasks.find((t) => t.id === taskId);
849
+ if (!task) throw new Error(`Task not found: ${taskId}`);
850
+
851
+ const deliverables = task.deliverables ?? [];
852
+ let staleCount = 0;
853
+ const now = Date.now();
854
+
855
+ for (const d of deliverables) {
856
+ d.stale = false;
857
+
858
+ if (d.type === 'file') {
859
+ if (!existsSync(d.path)) {
860
+ d.stale = true;
861
+ staleCount++;
862
+ } else if (d.hash) {
863
+ try {
864
+ const content = readFileSync(d.path);
865
+ const currentHash = createHash('sha256').update(content).digest('hex');
866
+ if (currentHash !== d.hash) {
867
+ d.stale = true;
868
+ staleCount++;
869
+ }
870
+ } catch {
871
+ d.stale = true;
872
+ staleCount++;
873
+ }
874
+ }
875
+ d.verifiedAt = now;
876
+ } else if (d.type === 'vault_entry') {
877
+ if (vault) {
878
+ const entry = vault.get(d.path);
879
+ if (!entry) {
880
+ d.stale = true;
881
+ staleCount++;
882
+ }
883
+ }
884
+ d.verifiedAt = now;
885
+ }
886
+ // url: skip — just record
887
+ }
888
+
889
+ plan.updatedAt = Date.now();
890
+ this.save();
891
+
892
+ return { verified: staleCount === 0, deliverables, staleCount };
893
+ }
894
+
895
+ // ─── Evidence & Verification ────────────────────────────────────
896
+
897
+ /**
898
+ * Submit evidence for a task acceptance criterion.
899
+ * Evidence is stored on the task and used by verifyTask() to check completeness.
900
+ */
901
+ submitEvidence(
902
+ planId: string,
903
+ taskId: string,
904
+ evidence: { criterion: string; content: string; type: TaskEvidence['type'] },
905
+ ): PlanTask {
906
+ const plan = this.get(planId);
907
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
908
+ const task = plan.tasks.find((t) => t.id === taskId);
909
+ if (!task) throw new Error(`Task not found: ${taskId}`);
910
+ if (!task.evidence) task.evidence = [];
911
+ task.evidence.push({
912
+ criterion: evidence.criterion,
913
+ content: evidence.content,
914
+ type: evidence.type,
915
+ submittedAt: Date.now(),
916
+ });
917
+ task.updatedAt = Date.now();
918
+ plan.updatedAt = Date.now();
919
+ this.save();
920
+ return task;
921
+ }
922
+
923
+ /**
924
+ * Verify a task — check that evidence exists for all acceptance criteria
925
+ * and any reviews have passed.
926
+ * Returns verification status with details.
927
+ */
928
+ verifyTask(
929
+ planId: string,
930
+ taskId: string,
931
+ ): {
932
+ verified: boolean;
933
+ task: PlanTask;
934
+ missingCriteria: string[];
935
+ reviewStatus: 'approved' | 'rejected' | 'needs_changes' | 'no_reviews';
936
+ } {
937
+ const plan = this.get(planId);
938
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
939
+ const task = plan.tasks.find((t) => t.id === taskId);
940
+ if (!task) throw new Error(`Task not found: ${taskId}`);
941
+
942
+ // Check evidence coverage
943
+ const criteria = task.acceptanceCriteria ?? [];
944
+ const evidencedCriteria = new Set((task.evidence ?? []).map((e) => e.criterion));
945
+ const missingCriteria = criteria.filter((c) => !evidencedCriteria.has(c));
946
+
947
+ // Check task-level reviews
948
+ const taskReviews = (plan.reviews ?? []).filter((r) => r.taskId === taskId);
949
+ let reviewStatus: 'approved' | 'rejected' | 'needs_changes' | 'no_reviews' = 'no_reviews';
950
+ if (taskReviews.length > 0) {
951
+ const latest = taskReviews[taskReviews.length - 1];
952
+ reviewStatus = latest.outcome;
953
+ }
954
+
955
+ const verified =
956
+ task.status === 'completed' &&
957
+ missingCriteria.length === 0 &&
958
+ (reviewStatus === 'approved' || reviewStatus === 'no_reviews');
959
+
960
+ if (verified !== task.verified) {
961
+ task.verified = verified;
962
+ task.updatedAt = Date.now();
963
+ plan.updatedAt = Date.now();
964
+ this.save();
965
+ }
966
+
967
+ return { verified, task, missingCriteria, reviewStatus };
968
+ }
969
+
970
+ /**
971
+ * Verify an entire plan — check all tasks are in a final state,
972
+ * all verification-required tasks have evidence, no tasks stuck in_progress.
973
+ * Returns a validation report.
974
+ */
975
+ verifyPlan(planId: string): {
976
+ valid: boolean;
977
+ planId: string;
978
+ issues: Array<{ taskId: string; issue: string }>;
979
+ summary: {
980
+ total: number;
981
+ completed: number;
982
+ skipped: number;
983
+ failed: number;
984
+ pending: number;
985
+ inProgress: number;
986
+ verified: number;
987
+ };
988
+ } {
989
+ const plan = this.get(planId);
990
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
991
+
992
+ const issues: Array<{ taskId: string; issue: string }> = [];
993
+ let verified = 0;
994
+ let completed = 0;
995
+ let skipped = 0;
996
+ let failed = 0;
997
+ let pending = 0;
998
+ let inProgress = 0;
999
+
1000
+ for (const task of plan.tasks) {
1001
+ switch (task.status) {
1002
+ case 'completed':
1003
+ completed++;
1004
+ break;
1005
+ case 'skipped':
1006
+ skipped++;
1007
+ break;
1008
+ case 'failed':
1009
+ failed++;
1010
+ break;
1011
+ case 'pending':
1012
+ pending++;
1013
+ break;
1014
+ case 'in_progress':
1015
+ inProgress++;
1016
+ break;
1017
+ }
1018
+
1019
+ if (task.verified) verified++;
1020
+
1021
+ // Check for stuck tasks
1022
+ if (task.status === 'in_progress') {
1023
+ issues.push({ taskId: task.id, issue: 'Task stuck in in_progress state' });
1024
+ }
1025
+ if (task.status === 'pending') {
1026
+ issues.push({ taskId: task.id, issue: 'Task still pending — not started' });
1027
+ }
1028
+
1029
+ // Check evidence for completed tasks with acceptance criteria
1030
+ if (
1031
+ task.status === 'completed' &&
1032
+ task.acceptanceCriteria &&
1033
+ task.acceptanceCriteria.length > 0
1034
+ ) {
1035
+ const evidencedCriteria = new Set((task.evidence ?? []).map((e) => e.criterion));
1036
+ const missing = task.acceptanceCriteria.filter((c) => !evidencedCriteria.has(c));
1037
+ if (missing.length > 0) {
1038
+ issues.push({
1039
+ taskId: task.id,
1040
+ issue: `Missing evidence for ${missing.length} criteria: ${missing.join(', ')}`,
1041
+ });
1042
+ }
1043
+ }
1044
+ }
1045
+
1046
+ const valid = issues.length === 0 && pending === 0 && inProgress === 0;
1047
+
1048
+ return {
1049
+ valid,
1050
+ planId,
1051
+ issues,
1052
+ summary: {
1053
+ total: plan.tasks.length,
1054
+ completed,
1055
+ skipped,
1056
+ failed,
1057
+ pending,
1058
+ inProgress,
1059
+ verified,
1060
+ },
1061
+ };
1062
+ }
1063
+
1064
+ /**
1065
+ * Auto-reconcile a plan — fast path for plans with minimal drift.
1066
+ * Checks all tasks are in final state, generates reconciliation report automatically.
1067
+ * Returns null if drift is too significant for auto-reconciliation (>2 non-completed tasks).
1068
+ */
1069
+ autoReconcile(planId: string): Plan | null {
1070
+ const plan = this.get(planId);
1071
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
1072
+ if (plan.status !== 'executing' && plan.status !== 'validating')
1073
+ throw new Error(
1074
+ `Cannot auto-reconcile plan in '${plan.status}' status — must be 'executing' or 'validating'`,
1075
+ );
1076
+
1077
+ const completed = plan.tasks.filter((t) => t.status === 'completed').length;
1078
+ const skipped = plan.tasks.filter((t) => t.status === 'skipped').length;
1079
+ const failed = plan.tasks.filter((t) => t.status === 'failed').length;
1080
+ const pending = plan.tasks.filter((t) => t.status === 'pending').length;
1081
+ const inProgress = plan.tasks.filter((t) => t.status === 'in_progress').length;
1082
+
1083
+ // Can't auto-reconcile if tasks are still in progress
1084
+ if (inProgress > 0) return null;
1085
+ // Can't auto-reconcile if too many non-completed tasks
1086
+ if (pending + failed > 2) return null;
1087
+
1088
+ const driftItems: DriftItem[] = [];
1089
+
1090
+ for (const task of plan.tasks) {
1091
+ if (task.status === 'skipped') {
1092
+ driftItems.push({
1093
+ type: 'skipped',
1094
+ description: `Task '${task.title}' was skipped`,
1095
+ impact: 'medium',
1096
+ rationale: 'Task not executed during plan implementation',
1097
+ });
1098
+ } else if (task.status === 'failed') {
1099
+ driftItems.push({
1100
+ type: 'modified',
1101
+ description: `Task '${task.title}' failed`,
1102
+ impact: 'high',
1103
+ rationale: 'Task execution failed',
1104
+ });
1105
+ } else if (task.status === 'pending') {
1106
+ driftItems.push({
1107
+ type: 'skipped',
1108
+ description: `Task '${task.title}' was never started`,
1109
+ impact: 'low',
1110
+ rationale: 'Task left in pending state',
1111
+ });
1112
+ }
1113
+ }
1114
+
1115
+ return this.reconcile(planId, {
1116
+ actualOutcome: `Auto-reconciled: ${completed}/${plan.tasks.length} tasks completed, ${skipped} skipped, ${failed} failed`,
1117
+ driftItems,
1118
+ reconciledBy: 'auto',
1119
+ });
1120
+ }
1121
+
1122
+ /**
1123
+ * Generate a review prompt for spec compliance checking.
1124
+ * Used by subagent dispatch — the controller generates the prompt, a subagent executes it.
1125
+ */
1126
+ generateReviewSpec(
1127
+ planId: string,
1128
+ taskId: string,
1129
+ ): { prompt: string; task: PlanTask; plan: Plan } {
1130
+ const plan = this.get(planId);
1131
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
1132
+ const task = plan.tasks.find((t) => t.id === taskId);
1133
+ if (!task) throw new Error(`Task not found: ${taskId}`);
1134
+
1135
+ const criteria = task.acceptanceCriteria?.length
1136
+ ? `\n\nAcceptance Criteria:\n${task.acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}`
1137
+ : '';
1138
+
1139
+ const prompt = [
1140
+ `# Spec Compliance Review`,
1141
+ ``,
1142
+ `## Task: ${task.title}`,
1143
+ `**Description:** ${task.description}`,
1144
+ `**Plan Objective:** ${plan.objective}${criteria}`,
1145
+ ``,
1146
+ `## Review Checklist`,
1147
+ `1. Does the implementation match the task description?`,
1148
+ `2. Are all acceptance criteria satisfied?`,
1149
+ `3. Does it align with the plan's overall objective?`,
1150
+ `4. Are there any spec deviations?`,
1151
+ ``,
1152
+ `Provide: outcome (approved/rejected/needs_changes) and detailed comments.`,
1153
+ ].join('\n');
1154
+
1155
+ return { prompt, task, plan };
1156
+ }
1157
+
1158
+ /**
1159
+ * Generate a review prompt for code quality checking.
1160
+ */
1161
+ generateReviewQuality(
1162
+ planId: string,
1163
+ taskId: string,
1164
+ ): { prompt: string; task: PlanTask; plan: Plan } {
1165
+ const plan = this.get(planId);
1166
+ if (!plan) throw new Error(`Plan not found: ${planId}`);
1167
+ const task = plan.tasks.find((t) => t.id === taskId);
1168
+ if (!task) throw new Error(`Task not found: ${taskId}`);
1169
+
1170
+ const prompt = [
1171
+ `# Code Quality Review`,
1172
+ ``,
1173
+ `## Task: ${task.title}`,
1174
+ `**Description:** ${task.description}`,
1175
+ ``,
1176
+ `## Quality Checklist`,
1177
+ `1. **Correctness** — Does it work as intended?`,
1178
+ `2. **Security** — No injection, XSS, or OWASP top 10 vulnerabilities?`,
1179
+ `3. **Performance** — No unnecessary allocations, N+1 queries, or blocking calls?`,
1180
+ `4. **Maintainability** — Clear naming, appropriate abstractions, documented intent?`,
1181
+ `5. **Testing** — Adequate test coverage for the changes?`,
1182
+ `6. **Error Handling** — Graceful degradation, no swallowed errors?`,
1183
+ `7. **Conventions** — Follows project coding standards?`,
1184
+ ``,
1185
+ `Provide: outcome (approved/rejected/needs_changes) and detailed comments.`,
1186
+ ].join('\n');
1187
+
1188
+ return { prompt, task, plan };
1189
+ }
1190
+
1191
+ /**
1192
+ * Archive completed plans — transitions them to 'archived' status.
1193
+ * If olderThanDays is provided, only archives plans older than that.
1194
+ * Returns the archived plans.
1195
+ */
1196
+ archive(olderThanDays?: number): Plan[] {
1197
+ const cutoff =
1198
+ olderThanDays !== undefined
1199
+ ? Date.now() - olderThanDays * 24 * 60 * 60 * 1000
1200
+ : Date.now() + 1; // +1ms so archive() with no args archives all completed plans
446
1201
  const toArchive = this.store.plans.filter(
447
1202
  (p) => p.status === 'completed' && p.updatedAt < cutoff,
448
1203
  );
1204
+ for (const plan of toArchive) {
1205
+ plan.status = 'archived';
1206
+ plan.updatedAt = Date.now();
1207
+ }
449
1208
  if (toArchive.length > 0) {
450
- this.store.plans = this.store.plans.filter(
451
- (p) => !(p.status === 'completed' && p.updatedAt < cutoff),
452
- );
453
1209
  this.save();
454
1210
  }
455
1211
  return toArchive;
@@ -466,7 +1222,16 @@ export class Planner {
466
1222
  tasksByStatus: Record<TaskStatus, number>;
467
1223
  } {
468
1224
  const plans = this.store.plans;
469
- const byStatus: Record<PlanStatus, number> = { draft: 0, approved: 0, executing: 0, completed: 0 };
1225
+ const byStatus: Record<PlanStatus, number> = {
1226
+ brainstorming: 0,
1227
+ draft: 0,
1228
+ approved: 0,
1229
+ executing: 0,
1230
+ validating: 0,
1231
+ reconciling: 0,
1232
+ completed: 0,
1233
+ archived: 0,
1234
+ };
470
1235
  const tasksByStatus: Record<TaskStatus, number> = {
471
1236
  pending: 0,
472
1237
  in_progress: 0,
@@ -496,9 +1261,11 @@ export class Planner {
496
1261
  // ─── Grading ──────────────────────────────────────────────────────
497
1262
 
498
1263
  /**
499
- * Grade a plan using 6-pass gap analysis with severity-weighted scoring.
1264
+ * Grade a plan using gap analysis with severity-weighted scoring.
500
1265
  * Ported from Salvador MCP's multi-pass grading engine.
501
1266
  *
1267
+ * 6 built-in passes + optional custom passes (domain-specific checks).
1268
+ *
502
1269
  * Scoring:
503
1270
  * - Each gap has a severity (critical=30, major=15, minor=2, info=0)
504
1271
  * - Deductions are per-category with optional caps
@@ -590,12 +1357,18 @@ export class Planner {
590
1357
 
591
1358
  private gradeToMinScore(grade: PlanGrade): number {
592
1359
  switch (grade) {
593
- case 'A+': return 95;
594
- case 'A': return 90;
595
- case 'B': return 80;
596
- case 'C': return 70;
597
- case 'D': return 60;
598
- case 'F': return 0;
1360
+ case 'A+':
1361
+ return 95;
1362
+ case 'A':
1363
+ return 90;
1364
+ case 'B':
1365
+ return 80;
1366
+ case 'C':
1367
+ return 70;
1368
+ case 'D':
1369
+ return 60;
1370
+ case 'F':
1371
+ return 0;
599
1372
  }
600
1373
  }
601
1374