@pjmendonca/devflow 1.13.2 → 1.19.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 (236) hide show
  1. package/.claude/commands/agent.md +1 -1
  2. package/.claude/commands/brainstorm.md +28 -0
  3. package/.claude/commands/bugfix.md +21 -0
  4. package/.claude/commands/checkpoint.md +0 -1
  5. package/.claude/commands/collab.md +0 -1
  6. package/.claude/commands/costs.md +88 -18
  7. package/.claude/commands/devflow.md +26 -0
  8. package/.claude/commands/handoff.md +0 -1
  9. package/.claude/commands/init.md +383 -0
  10. package/.claude/commands/memory.md +0 -1
  11. package/.claude/commands/pair.md +0 -1
  12. package/.claude/commands/review.md +27 -0
  13. package/.claude/commands/route.md +0 -1
  14. package/.claude/commands/swarm.md +0 -1
  15. package/.claude/commands/validate.md +55 -0
  16. package/.claude/hooks/session-notification.sh +44 -0
  17. package/.claude/hooks/session-startup.sh +427 -0
  18. package/.claude/hooks/session-stop.sh +38 -0
  19. package/.claude/hooks/session_tracker.py +272 -0
  20. package/.claude/settings.json +38 -0
  21. package/.claude/skills/brainstorm/SKILL.md +531 -0
  22. package/.claude/skills/costs/SKILL.md +156 -0
  23. package/.claude/skills/validate/SKILL.md +101 -0
  24. package/CHANGELOG.md +284 -0
  25. package/README.md +207 -10
  26. package/bin/devflow-install.js +2 -1
  27. package/bin/devflow.js +4 -0
  28. package/lib/constants.js +0 -1
  29. package/lib/exec-python.js +1 -1
  30. package/package.json +1 -1
  31. package/tooling/.automation/.checkpoint_lock +1 -0
  32. package/tooling/.automation/agents/architect.md +19 -0
  33. package/tooling/.automation/agents/ba.md +19 -0
  34. package/tooling/.automation/agents/maintainer.md +19 -0
  35. package/tooling/.automation/agents/pm.md +19 -0
  36. package/tooling/.automation/agents/reviewer.md +1 -1
  37. package/tooling/.automation/agents/writer.md +19 -0
  38. package/tooling/.automation/benchmarks/benchmark_20251230_100119.json +314 -0
  39. package/tooling/.automation/benchmarks/benchmark_20251230_100216.json +314 -0
  40. package/tooling/.automation/costs/config.json +31 -0
  41. package/tooling/.automation/costs/sessions/2025-12-29_20251229_164128.json +22 -0
  42. package/tooling/.automation/memory/knowledge/kg_integration-test.json +738 -1
  43. package/tooling/.automation/memory/knowledge/kg_test-story.json +3381 -2
  44. package/tooling/.automation/memory/shared/shared_integration-test.json +193 -1
  45. package/tooling/.automation/memory/shared/shared_test-story.json +757 -1
  46. package/tooling/.automation/memory/shared/shared_test.json +1332 -0
  47. package/tooling/.automation/memory/shared/shared_validation-check.json +240 -0
  48. package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +5 -5
  49. package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +23 -5
  50. package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +24 -6
  51. package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +4 -4
  52. package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +4 -4
  53. package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +4 -4
  54. package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +18 -0
  55. package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +19 -1
  56. package/tooling/.automation/overrides/templates/dev/security-focused.yaml +18 -0
  57. package/tooling/.automation/overrides/templates/dev/user-advocate.yaml +54 -0
  58. package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +4 -4
  59. package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +4 -4
  60. package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +4 -4
  61. package/tooling/.automation/overrides/templates/maintainer/reliability-engineer.yaml +55 -0
  62. package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +4 -4
  63. package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +3 -3
  64. package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +4 -4
  65. package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +18 -0
  66. package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +18 -0
  67. package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +2 -2
  68. package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +3 -3
  69. package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +5 -5
  70. package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +4 -4
  71. package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +5 -5
  72. package/tooling/.automation/validation/history/2025-12-29_val_002a28c1.json +32 -0
  73. package/tooling/.automation/validation/history/2025-12-29_val_01273bb1.json +32 -0
  74. package/tooling/.automation/validation/history/2025-12-29_val_03369914.json +41 -0
  75. package/tooling/.automation/validation/history/2025-12-29_val_07a449ba.json +32 -0
  76. package/tooling/.automation/validation/history/2025-12-29_val_0df1f0a2.json +41 -0
  77. package/tooling/.automation/validation/history/2025-12-29_val_10ff3d34.json +41 -0
  78. package/tooling/.automation/validation/history/2025-12-29_val_110771d7.json +32 -0
  79. package/tooling/.automation/validation/history/2025-12-29_val_13f3a7f9.json +32 -0
  80. package/tooling/.automation/validation/history/2025-12-29_val_17ba9d21.json +41 -0
  81. package/tooling/.automation/validation/history/2025-12-29_val_22247089.json +32 -0
  82. package/tooling/.automation/validation/history/2025-12-29_val_227ea6a4.json +32 -0
  83. package/tooling/.automation/validation/history/2025-12-29_val_2335d5ae.json +32 -0
  84. package/tooling/.automation/validation/history/2025-12-29_val_246824bb.json +41 -0
  85. package/tooling/.automation/validation/history/2025-12-29_val_28b4b9cd.json +32 -0
  86. package/tooling/.automation/validation/history/2025-12-29_val_2abd12cc.json +32 -0
  87. package/tooling/.automation/validation/history/2025-12-29_val_2c801b2f.json +59 -0
  88. package/tooling/.automation/validation/history/2025-12-29_val_2c8cfa8e.json +32 -0
  89. package/tooling/.automation/validation/history/2025-12-29_val_2ce76eb0.json +32 -0
  90. package/tooling/.automation/validation/history/2025-12-29_val_30351948.json +41 -0
  91. package/tooling/.automation/validation/history/2025-12-29_val_30eb7229.json +41 -0
  92. package/tooling/.automation/validation/history/2025-12-29_val_34df0e77.json +41 -0
  93. package/tooling/.automation/validation/history/2025-12-29_val_376e4d6a.json +32 -0
  94. package/tooling/.automation/validation/history/2025-12-29_val_3a4e8a1a.json +59 -0
  95. package/tooling/.automation/validation/history/2025-12-29_val_3b77a628.json +32 -0
  96. package/tooling/.automation/validation/history/2025-12-29_val_3ea4e1cf.json +59 -0
  97. package/tooling/.automation/validation/history/2025-12-29_val_44aacdb4.json +59 -0
  98. package/tooling/.automation/validation/history/2025-12-29_val_457ddfa8.json +32 -0
  99. package/tooling/.automation/validation/history/2025-12-29_val_45af6238.json +41 -0
  100. package/tooling/.automation/validation/history/2025-12-29_val_4735dba1.json +41 -0
  101. package/tooling/.automation/validation/history/2025-12-29_val_486b203c.json +41 -0
  102. package/tooling/.automation/validation/history/2025-12-29_val_49dc56cd.json +59 -0
  103. package/tooling/.automation/validation/history/2025-12-29_val_4d863d6d.json +32 -0
  104. package/tooling/.automation/validation/history/2025-12-29_val_5149a808.json +59 -0
  105. package/tooling/.automation/validation/history/2025-12-29_val_52e0bb43.json +32 -0
  106. package/tooling/.automation/validation/history/2025-12-29_val_585d6319.json +59 -0
  107. package/tooling/.automation/validation/history/2025-12-29_val_5b2d859a.json +32 -0
  108. package/tooling/.automation/validation/history/2025-12-29_val_635a7081.json +41 -0
  109. package/tooling/.automation/validation/history/2025-12-29_val_64df4905.json +32 -0
  110. package/tooling/.automation/validation/history/2025-12-29_val_70634cee.json +41 -0
  111. package/tooling/.automation/validation/history/2025-12-29_val_714553f9.json +32 -0
  112. package/tooling/.automation/validation/history/2025-12-29_val_7f7bfdbf.json +41 -0
  113. package/tooling/.automation/validation/history/2025-12-29_val_7faad91d.json +32 -0
  114. package/tooling/.automation/validation/history/2025-12-29_val_81821f8f.json +41 -0
  115. package/tooling/.automation/validation/history/2025-12-29_val_8249f3c9.json +32 -0
  116. package/tooling/.automation/validation/history/2025-12-29_val_8422b50f.json +41 -0
  117. package/tooling/.automation/validation/history/2025-12-29_val_8446c134.json +32 -0
  118. package/tooling/.automation/validation/history/2025-12-29_val_879f4e26.json +59 -0
  119. package/tooling/.automation/validation/history/2025-12-29_val_8b6d5bd7.json +32 -0
  120. package/tooling/.automation/validation/history/2025-12-29_val_8c5cd787.json +32 -0
  121. package/tooling/.automation/validation/history/2025-12-29_val_91d20bc7.json +32 -0
  122. package/tooling/.automation/validation/history/2025-12-29_val_958a12b7.json +41 -0
  123. package/tooling/.automation/validation/history/2025-12-29_val_95d91108.json +41 -0
  124. package/tooling/.automation/validation/history/2025-12-29_val_980dbb74.json +32 -0
  125. package/tooling/.automation/validation/history/2025-12-29_val_9e40c79b.json +32 -0
  126. package/tooling/.automation/validation/history/2025-12-29_val_9f499b7c.json +32 -0
  127. package/tooling/.automation/validation/history/2025-12-29_val_9f7c3b57.json +32 -0
  128. package/tooling/.automation/validation/history/2025-12-29_val_a30d5bd4.json +32 -0
  129. package/tooling/.automation/validation/history/2025-12-29_val_a6eb09c7.json +32 -0
  130. package/tooling/.automation/validation/history/2025-12-29_val_a86f7b83.json +41 -0
  131. package/tooling/.automation/validation/history/2025-12-29_val_ad5347e1.json +41 -0
  132. package/tooling/.automation/validation/history/2025-12-29_val_b0a5a993.json +32 -0
  133. package/tooling/.automation/validation/history/2025-12-29_val_bcb0192e.json +32 -0
  134. package/tooling/.automation/validation/history/2025-12-29_val_bf3c9aaa.json +32 -0
  135. package/tooling/.automation/validation/history/2025-12-29_val_c461ff88.json +32 -0
  136. package/tooling/.automation/validation/history/2025-12-29_val_c4f4e258.json +41 -0
  137. package/tooling/.automation/validation/history/2025-12-29_val_c7f0fa6d.json +41 -0
  138. package/tooling/.automation/validation/history/2025-12-29_val_c911b0e6.json +32 -0
  139. package/tooling/.automation/validation/history/2025-12-29_val_cc581964.json +32 -0
  140. package/tooling/.automation/validation/history/2025-12-29_val_cdd5a33b.json +32 -0
  141. package/tooling/.automation/validation/history/2025-12-29_val_cfd42495.json +32 -0
  142. package/tooling/.automation/validation/history/2025-12-29_val_d1c7a4ee.json +41 -0
  143. package/tooling/.automation/validation/history/2025-12-29_val_d2280d0e.json +32 -0
  144. package/tooling/.automation/validation/history/2025-12-29_val_d2a6ff69.json +32 -0
  145. package/tooling/.automation/validation/history/2025-12-29_val_d8c53ab2.json +59 -0
  146. package/tooling/.automation/validation/history/2025-12-29_val_d9c1247a.json +41 -0
  147. package/tooling/.automation/validation/history/2025-12-29_val_d9d58569.json +32 -0
  148. package/tooling/.automation/validation/history/2025-12-29_val_dabb4fd9.json +32 -0
  149. package/tooling/.automation/validation/history/2025-12-29_val_dd8fe359.json +32 -0
  150. package/tooling/.automation/validation/history/2025-12-29_val_decdffc9.json +32 -0
  151. package/tooling/.automation/validation/history/2025-12-29_val_e3a95476.json +59 -0
  152. package/tooling/.automation/validation/history/2025-12-29_val_e776dfca.json +32 -0
  153. package/tooling/.automation/validation/history/2025-12-29_val_ea70969f.json +59 -0
  154. package/tooling/.automation/validation/history/2025-12-29_val_ef41ea95.json +32 -0
  155. package/tooling/.automation/validation/history/2025-12-29_val_f384f9b1.json +32 -0
  156. package/tooling/.automation/validation/history/2025-12-29_val_f8adc38c.json +41 -0
  157. package/tooling/.automation/validation/history/2025-12-29_val_fa40b69e.json +32 -0
  158. package/tooling/.automation/validation/history/2025-12-29_val_fc538d54.json +41 -0
  159. package/tooling/.automation/validation/history/2025-12-29_val_fe814665.json +32 -0
  160. package/tooling/.automation/validation/history/2025-12-29_val_ffea4b12.json +32 -0
  161. package/tooling/.automation/validation/history/2025-12-30_val_02d001e5.json +59 -0
  162. package/tooling/.automation/validation/history/2025-12-30_val_0b8966dc.json +32 -0
  163. package/tooling/.automation/validation/history/2025-12-30_val_15455fbf.json +59 -0
  164. package/tooling/.automation/validation/history/2025-12-30_val_157e34b9.json +32 -0
  165. package/tooling/.automation/validation/history/2025-12-30_val_28d1d933.json +32 -0
  166. package/tooling/.automation/validation/history/2025-12-30_val_3442a52c.json +32 -0
  167. package/tooling/.automation/validation/history/2025-12-30_val_37f1ce1e.json +32 -0
  168. package/tooling/.automation/validation/history/2025-12-30_val_4f1d8a93.json +32 -0
  169. package/tooling/.automation/validation/history/2025-12-30_val_56ff1de3.json +32 -0
  170. package/tooling/.automation/validation/history/2025-12-30_val_664fd4e2.json +41 -0
  171. package/tooling/.automation/validation/history/2025-12-30_val_66afb0a7.json +32 -0
  172. package/tooling/.automation/validation/history/2025-12-30_val_7634663c.json +41 -0
  173. package/tooling/.automation/validation/history/2025-12-30_val_8ea830c3.json +41 -0
  174. package/tooling/.automation/validation/history/2025-12-30_val_998957c2.json +32 -0
  175. package/tooling/.automation/validation/history/2025-12-30_val_a52177db.json +32 -0
  176. package/tooling/.automation/validation/history/2025-12-30_val_a5b65a63.json +32 -0
  177. package/tooling/.automation/validation/history/2025-12-30_val_ae391d0e.json +32 -0
  178. package/tooling/.automation/validation/history/2025-12-30_val_c7895339.json +41 -0
  179. package/tooling/.automation/validation/history/2025-12-30_val_ca416593.json +41 -0
  180. package/tooling/.automation/validation/history/2025-12-30_val_cee19422.json +32 -0
  181. package/tooling/.automation/validation/history/2025-12-30_val_ddd4f4e6.json +32 -0
  182. package/tooling/.automation/validation/history/2025-12-30_val_f2e1394b.json +32 -0
  183. package/tooling/.automation/validation/history/2025-12-30_val_f4a7fa06.json +41 -0
  184. package/tooling/.automation/validation/history/2025-12-30_val_ffea3369.json +32 -0
  185. package/tooling/.automation/validation/history/2026-01-03_val_1287a74c.json +41 -0
  186. package/tooling/.automation/validation/history/2026-01-03_val_3b24071f.json +32 -0
  187. package/tooling/.automation/validation/history/2026-01-03_val_44d77573.json +32 -0
  188. package/tooling/.automation/validation/history/2026-01-03_val_5b31dc51.json +32 -0
  189. package/tooling/.automation/validation/history/2026-01-03_val_74267244.json +32 -0
  190. package/tooling/.automation/validation/history/2026-01-03_val_8b2d95c7.json +59 -0
  191. package/tooling/.automation/validation/history/2026-01-03_val_d875b297.json +41 -0
  192. package/tooling/.automation/validation-config.yaml +103 -0
  193. package/tooling/completions/DevflowCompletion.ps1 +21 -21
  194. package/tooling/completions/_run-story +3 -3
  195. package/tooling/completions/run-story-completion.bash +8 -8
  196. package/tooling/docs/DOC-STANDARD.md +14 -14
  197. package/tooling/docs/stories/.gitkeep +0 -0
  198. package/tooling/docs/templates/brainstorm-guide.md +314 -0
  199. package/tooling/docs/templates/migration-spec.md +4 -4
  200. package/tooling/docs/templates/story.md +66 -0
  201. package/tooling/scripts/context_checkpoint.py +5 -15
  202. package/tooling/scripts/cost_dashboard.py +610 -13
  203. package/tooling/scripts/create-persona.py +1 -12
  204. package/tooling/scripts/create-persona.sh +44 -44
  205. package/tooling/scripts/lib/__init__.py +12 -1
  206. package/tooling/scripts/lib/agent_handoff.py +11 -2
  207. package/tooling/scripts/lib/agent_router.py +31 -10
  208. package/tooling/scripts/lib/colors.py +106 -0
  209. package/tooling/scripts/lib/context_monitor.py +766 -0
  210. package/tooling/scripts/lib/cost_config.py +229 -10
  211. package/tooling/scripts/lib/cost_display.py +20 -45
  212. package/tooling/scripts/lib/cost_tracker.py +462 -15
  213. package/tooling/scripts/lib/currency_converter.py +28 -5
  214. package/tooling/scripts/lib/pair_programming.py +102 -3
  215. package/tooling/scripts/lib/personality_system.py +949 -0
  216. package/tooling/scripts/lib/platform.py +55 -0
  217. package/tooling/scripts/lib/shared_memory.py +9 -3
  218. package/tooling/scripts/lib/swarm_orchestrator.py +514 -75
  219. package/tooling/scripts/lib/validation_loop.py +1014 -0
  220. package/tooling/scripts/memory_summarize.py +9 -2
  221. package/tooling/scripts/new-doc.py +2 -9
  222. package/tooling/scripts/personalize_agent.py +1 -12
  223. package/tooling/scripts/rollback-migration.sh +60 -60
  224. package/tooling/scripts/run-collab.ps1 +16 -16
  225. package/tooling/scripts/run-collab.py +88 -53
  226. package/tooling/scripts/run-collab.sh +4 -4
  227. package/tooling/scripts/run-story.py +278 -20
  228. package/tooling/scripts/run-story.sh +3 -3
  229. package/tooling/scripts/setup-checkpoint-service.py +2 -9
  230. package/tooling/scripts/tech-debt-tracker.py +1 -12
  231. package/tooling/scripts/test_adversarial_swarm.py +452 -0
  232. package/tooling/scripts/validate-overrides.py +1 -10
  233. package/tooling/scripts/validate-overrides.sh +40 -40
  234. package/tooling/scripts/validate_loop.py +162 -0
  235. package/tooling/scripts/validate_setup.py +2 -30
  236. package/.claude/skills/init/SKILL.md +0 -496
@@ -0,0 +1,1014 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Validation Loop - Automated feedback and validation system for agent pipelines.
4
+
5
+ Provides a three-tier validation framework:
6
+ - Tier 1: Pre-flight validation (before any agent runs)
7
+ - Tier 2: Inter-phase validation (between agents/phases)
8
+ - Tier 3: Post-completion validation (after pipeline completes)
9
+
10
+ Features:
11
+ - Configurable validation gates with retry logic
12
+ - Integration with shared memory for learning
13
+ - Cost-aware validation (respects budget limits)
14
+ - Automated fix suggestions and escalation
15
+ - Detailed validation history tracking
16
+
17
+ Usage:
18
+ from lib.validation_loop import ValidationLoop, ValidationGate, ValidationResult
19
+
20
+ # Create gates
21
+ gates = [
22
+ ValidationGate(
23
+ name="tests_pass",
24
+ validator=lambda ctx: run_tests(),
25
+ on_fail="retry",
26
+ max_retries=2
27
+ ),
28
+ ]
29
+
30
+ # Run with validation
31
+ loop = ValidationLoop(gates, config)
32
+ result = loop.run_with_validation(pipeline_func, context)
33
+ """
34
+
35
+ import json
36
+ import subprocess
37
+ import sys
38
+ import uuid
39
+ from dataclasses import asdict, dataclass, field
40
+ from datetime import datetime
41
+ from enum import Enum
42
+ from pathlib import Path
43
+ from typing import Any, Callable, Optional
44
+
45
+ # Add parent directory for imports
46
+ sys.path.insert(0, str(Path(__file__).parent))
47
+
48
+ # Try to import dependencies
49
+ try:
50
+ from shared_memory import KnowledgeGraph, SharedMemory
51
+
52
+ HAS_SHARED_MEMORY = True
53
+ except ImportError:
54
+ HAS_SHARED_MEMORY = False
55
+
56
+ try:
57
+ from cost_tracker import get_tracker
58
+
59
+ HAS_COST_TRACKER = True
60
+ except ImportError:
61
+ HAS_COST_TRACKER = False
62
+
63
+ try:
64
+ from errors import log_error, log_info, log_warning
65
+
66
+ HAS_ERRORS = True
67
+ except ImportError:
68
+ HAS_ERRORS = False
69
+
70
+ def log_info(msg: str) -> None:
71
+ print(f"[INFO] {msg}")
72
+
73
+ def log_warning(msg: str) -> None:
74
+ print(f"[WARNING] {msg}", file=sys.stderr)
75
+
76
+ def log_error(msg: str) -> None:
77
+ print(f"[ERROR] {msg}", file=sys.stderr)
78
+
79
+
80
+ # Storage paths
81
+ PROJECT_ROOT = Path(__file__).parent.parent.parent.parent
82
+ VALIDATION_DIR = PROJECT_ROOT / "tooling" / ".automation" / "validation"
83
+ VALIDATION_HISTORY_DIR = VALIDATION_DIR / "history"
84
+
85
+
86
+ class ValidationResult(Enum):
87
+ """Result of a validation gate check."""
88
+
89
+ PASS = "pass"
90
+ WARN = "warn"
91
+ FAIL = "fail"
92
+ SKIP = "skip"
93
+ ERROR = "error"
94
+
95
+
96
+ class FailureAction(Enum):
97
+ """Action to take on validation failure."""
98
+
99
+ BLOCK = "block" # Stop execution immediately
100
+ WARN = "warn" # Log warning but continue
101
+ RETRY = "retry" # Retry with specified agent
102
+ ESCALATE = "escalate" # Ask user for decision
103
+
104
+
105
+ @dataclass
106
+ class ValidationGate:
107
+ """A single validation checkpoint."""
108
+
109
+ name: str
110
+ validator: Callable[..., bool]
111
+ description: str = ""
112
+ on_fail: FailureAction = FailureAction.WARN
113
+ max_retries: int = 3
114
+ retry_with_agent: Optional[str] = None
115
+ auto_fix: Optional[Callable[..., bool]] = None
116
+ timeout_seconds: int = 60
117
+ tier: int = 2 # 1=preflight, 2=inter-phase, 3=post-completion
118
+ tags: list[str] = field(default_factory=list)
119
+
120
+ def __post_init__(self):
121
+ if isinstance(self.on_fail, str):
122
+ self.on_fail = FailureAction(self.on_fail)
123
+
124
+
125
+ @dataclass
126
+ class GateResult:
127
+ """Result from running a validation gate."""
128
+
129
+ gate_name: str
130
+ result: ValidationResult
131
+ message: str = ""
132
+ duration_ms: float = 0
133
+ retry_count: int = 0
134
+ auto_fixed: bool = False
135
+ details: dict[str, Any] = field(default_factory=dict)
136
+
137
+ def to_dict(self) -> dict:
138
+ return {
139
+ **asdict(self),
140
+ "result": self.result.value,
141
+ }
142
+
143
+
144
+ @dataclass
145
+ class LoopContext:
146
+ """Context for a validation loop execution."""
147
+
148
+ story_key: str
149
+ iteration: int = 0
150
+ max_iterations: int = 3
151
+ accumulated_issues: list[str] = field(default_factory=list)
152
+ accumulated_fixes: list[str] = field(default_factory=list)
153
+ cost_so_far: float = 0.0
154
+ time_elapsed_seconds: float = 0.0
155
+ phase: str = ""
156
+ from_agent: str = ""
157
+ to_agent: str = ""
158
+ pipeline_output: Any = None
159
+
160
+ def to_dict(self) -> dict:
161
+ return {
162
+ "story_key": self.story_key,
163
+ "iteration": self.iteration,
164
+ "max_iterations": self.max_iterations,
165
+ "accumulated_issues": self.accumulated_issues,
166
+ "accumulated_fixes": self.accumulated_fixes,
167
+ "cost_so_far": self.cost_so_far,
168
+ "time_elapsed_seconds": self.time_elapsed_seconds,
169
+ "phase": self.phase,
170
+ "from_agent": self.from_agent,
171
+ "to_agent": self.to_agent,
172
+ }
173
+
174
+
175
+ @dataclass
176
+ class ValidationReport:
177
+ """Complete validation report for a run."""
178
+
179
+ id: str
180
+ timestamp: str
181
+ story_key: str
182
+ tier: int
183
+ gate_results: list[GateResult]
184
+ overall_result: ValidationResult
185
+ total_duration_ms: float
186
+ context: dict[str, Any] = field(default_factory=dict)
187
+
188
+ @property
189
+ def passed(self) -> bool:
190
+ return self.overall_result in (ValidationResult.PASS, ValidationResult.WARN)
191
+
192
+ @property
193
+ def failed(self) -> bool:
194
+ return self.overall_result in (ValidationResult.FAIL, ValidationResult.ERROR)
195
+
196
+ @property
197
+ def warnings(self) -> list[GateResult]:
198
+ return [g for g in self.gate_results if g.result == ValidationResult.WARN]
199
+
200
+ @property
201
+ def failures(self) -> list[GateResult]:
202
+ return [g for g in self.gate_results if g.result == ValidationResult.FAIL]
203
+
204
+ def to_dict(self) -> dict:
205
+ return {
206
+ "id": self.id,
207
+ "timestamp": self.timestamp,
208
+ "story_key": self.story_key,
209
+ "tier": self.tier,
210
+ "gate_results": [g.to_dict() for g in self.gate_results],
211
+ "overall_result": self.overall_result.value,
212
+ "total_duration_ms": self.total_duration_ms,
213
+ "passed": self.passed,
214
+ "context": self.context,
215
+ }
216
+
217
+ def to_summary(self) -> str:
218
+ """Generate human-readable summary."""
219
+ lines = [
220
+ f"Validation Report: {self.overall_result.value.upper()}",
221
+ f"Story: {self.story_key} | Tier: {self.tier} | Duration: {self.total_duration_ms:.0f}ms",
222
+ "",
223
+ ]
224
+
225
+ passed = [g for g in self.gate_results if g.result == ValidationResult.PASS]
226
+ if passed:
227
+ lines.append(f"[PASS] {len(passed)} gate(s) passed")
228
+ for g in passed:
229
+ lines.append(f" - {g.gate_name}")
230
+
231
+ if self.warnings:
232
+ lines.append(f"[WARN] {len(self.warnings)} warning(s)")
233
+ for g in self.warnings:
234
+ lines.append(f" - {g.gate_name}: {g.message}")
235
+
236
+ if self.failures:
237
+ lines.append(f"[FAIL] {len(self.failures)} failure(s)")
238
+ for g in self.failures:
239
+ lines.append(f" - {g.gate_name}: {g.message}")
240
+
241
+ return "\n".join(lines)
242
+
243
+
244
+ class ValidationLoop:
245
+ """
246
+ Main validation loop orchestrator.
247
+
248
+ Runs validation gates and manages retry logic, escalation,
249
+ and feedback recording.
250
+ """
251
+
252
+ def __init__(
253
+ self,
254
+ gates: list[ValidationGate],
255
+ config: Optional[dict[str, Any]] = None,
256
+ story_key: Optional[str] = None,
257
+ ):
258
+ self.gates = gates
259
+ self.config = config or {}
260
+ self.story_key = story_key
261
+
262
+ # Initialize storage
263
+ VALIDATION_DIR.mkdir(parents=True, exist_ok=True)
264
+ VALIDATION_HISTORY_DIR.mkdir(parents=True, exist_ok=True)
265
+
266
+ # Optional integrations
267
+ self.shared_memory = SharedMemory(story_key) if HAS_SHARED_MEMORY else None
268
+ self.knowledge_graph = KnowledgeGraph(story_key) if HAS_SHARED_MEMORY else None
269
+
270
+ # History tracking
271
+ self.reports: list[ValidationReport] = []
272
+
273
+ def run_gate(
274
+ self,
275
+ gate: ValidationGate,
276
+ context: LoopContext,
277
+ ) -> GateResult:
278
+ """Run a single validation gate."""
279
+ import time
280
+
281
+ start_time = time.time()
282
+ retry_count = 0
283
+ auto_fixed = False
284
+ message = ""
285
+ details: dict[str, Any] = {}
286
+
287
+ try:
288
+ # Run validator
289
+ passed = gate.validator(context)
290
+
291
+ if passed:
292
+ result = ValidationResult.PASS
293
+ message = "Validation passed"
294
+ else:
295
+ # Try auto-fix if available
296
+ if gate.auto_fix and self.config.get("auto_fix_enabled", True):
297
+ try:
298
+ fix_result = gate.auto_fix(context)
299
+ if fix_result:
300
+ # Re-run validation after fix
301
+ passed = gate.validator(context)
302
+ if passed:
303
+ result = ValidationResult.PASS
304
+ auto_fixed = True
305
+ message = "Passed after auto-fix"
306
+ else:
307
+ result = ValidationResult.FAIL
308
+ message = "Auto-fix applied but validation still failed"
309
+ else:
310
+ result = ValidationResult.FAIL
311
+ message = "Auto-fix failed"
312
+ except Exception as e:
313
+ result = ValidationResult.FAIL
314
+ message = f"Auto-fix error: {e}"
315
+ else:
316
+ result = ValidationResult.FAIL
317
+ message = "Validation failed"
318
+
319
+ except TimeoutError:
320
+ result = ValidationResult.ERROR
321
+ message = f"Validation timed out after {gate.timeout_seconds}s"
322
+ except Exception as e:
323
+ result = ValidationResult.ERROR
324
+ message = f"Validation error: {e}"
325
+ details["exception"] = str(e)
326
+
327
+ duration_ms = (time.time() - start_time) * 1000
328
+
329
+ return GateResult(
330
+ gate_name=gate.name,
331
+ result=result,
332
+ message=message,
333
+ duration_ms=duration_ms,
334
+ retry_count=retry_count,
335
+ auto_fixed=auto_fixed,
336
+ details=details,
337
+ )
338
+
339
+ def run_gates(
340
+ self,
341
+ context: LoopContext,
342
+ tier: Optional[int] = None,
343
+ ) -> ValidationReport:
344
+ """Run all gates (optionally filtered by tier)."""
345
+ import time
346
+
347
+ start_time = time.time()
348
+ gate_results: list[GateResult] = []
349
+
350
+ # Filter gates by tier if specified
351
+ gates_to_run = self.gates
352
+ if tier is not None:
353
+ gates_to_run = [g for g in self.gates if g.tier == tier]
354
+
355
+ for gate in gates_to_run:
356
+ result = self.run_gate(gate, context)
357
+ gate_results.append(result)
358
+
359
+ # Handle blocking failures
360
+ if result.result == ValidationResult.FAIL:
361
+ if gate.on_fail == FailureAction.BLOCK:
362
+ log_error(f"Gate '{gate.name}' failed with BLOCK action. Stopping.")
363
+ break
364
+
365
+ # Determine overall result
366
+ if any(r.result == ValidationResult.ERROR for r in gate_results):
367
+ overall = ValidationResult.ERROR
368
+ elif any(r.result == ValidationResult.FAIL for r in gate_results):
369
+ overall = ValidationResult.FAIL
370
+ elif any(r.result == ValidationResult.WARN for r in gate_results):
371
+ overall = ValidationResult.WARN
372
+ else:
373
+ overall = ValidationResult.PASS
374
+
375
+ total_duration = (time.time() - start_time) * 1000
376
+
377
+ report = ValidationReport(
378
+ id=f"val_{uuid.uuid4().hex[:8]}",
379
+ timestamp=datetime.now().isoformat(),
380
+ story_key=context.story_key,
381
+ tier=tier or 0,
382
+ gate_results=gate_results,
383
+ overall_result=overall,
384
+ total_duration_ms=total_duration,
385
+ context=context.to_dict(),
386
+ )
387
+
388
+ # Record in history
389
+ self.reports.append(report)
390
+ self._save_report(report)
391
+
392
+ # Record in shared memory
393
+ if self.shared_memory:
394
+ self.shared_memory.add(
395
+ agent="VALIDATOR",
396
+ content=f"Validation {overall.value}: {len(gate_results)} gates, "
397
+ f"{len([r for r in gate_results if r.result == ValidationResult.PASS])} passed",
398
+ tags=["validation", f"tier-{tier or 0}", overall.value],
399
+ )
400
+
401
+ return report
402
+
403
+ def run_with_validation(
404
+ self,
405
+ pipeline_func: Callable[..., Any],
406
+ context: LoopContext,
407
+ tier: int = 2,
408
+ ) -> tuple[Any, ValidationReport]:
409
+ """
410
+ Run a pipeline function with validation loop.
411
+
412
+ Returns:
413
+ Tuple of (pipeline_result, validation_report)
414
+ """
415
+ for iteration in range(context.max_iterations):
416
+ context.iteration = iteration
417
+
418
+ # Run the pipeline
419
+ try:
420
+ result = pipeline_func()
421
+ context.pipeline_output = result
422
+ except Exception as e:
423
+ log_error(f"Pipeline failed: {e}")
424
+ context.accumulated_issues.append(str(e))
425
+ continue
426
+
427
+ # Run validation
428
+ report = self.run_gates(context, tier=tier)
429
+
430
+ if report.passed:
431
+ log_info(f"Validation passed on iteration {iteration + 1}")
432
+ return result, report
433
+
434
+ # Collect issues for retry
435
+ for gate_result in report.failures:
436
+ context.accumulated_issues.append(f"{gate_result.gate_name}: {gate_result.message}")
437
+
438
+ # Check if any gate wants to retry
439
+ gates_by_name = {g.name: g for g in self.gates}
440
+ should_retry = False
441
+ for gate_result in report.failures:
442
+ gate = gates_by_name.get(gate_result.gate_name)
443
+ if gate and gate.on_fail == FailureAction.RETRY:
444
+ if iteration < gate.max_retries:
445
+ should_retry = True
446
+ log_warning(
447
+ f"Gate '{gate.name}' requesting retry "
448
+ f"({iteration + 1}/{gate.max_retries})"
449
+ )
450
+
451
+ if not should_retry:
452
+ log_error("Validation failed, no retry available")
453
+ break
454
+
455
+ # Return last result with failed validation
456
+ return context.pipeline_output, report
457
+
458
+ def run_preflight(self, context: LoopContext) -> ValidationReport:
459
+ """Run tier 1 (pre-flight) validation."""
460
+ return self.run_gates(context, tier=1)
461
+
462
+ def run_inter_phase(self, context: LoopContext) -> ValidationReport:
463
+ """Run tier 2 (inter-phase) validation."""
464
+ return self.run_gates(context, tier=2)
465
+
466
+ def run_post_completion(self, context: LoopContext) -> ValidationReport:
467
+ """Run tier 3 (post-completion) validation."""
468
+ return self.run_gates(context, tier=3)
469
+
470
+ def _save_report(self, report: ValidationReport):
471
+ """Save validation report to disk."""
472
+ filename = f"{datetime.now().strftime('%Y-%m-%d')}_{report.id}.json"
473
+ filepath = VALIDATION_HISTORY_DIR / filename
474
+ try:
475
+ with open(filepath, "w") as f:
476
+ json.dump(report.to_dict(), f, indent=2)
477
+ except OSError as e:
478
+ log_warning(f"Failed to save validation report: {e}")
479
+
480
+ def get_actionable_feedback(self, report: ValidationReport) -> dict[str, Any]:
481
+ """Extract actionable feedback from a validation report."""
482
+ feedback = {
483
+ "issues": [],
484
+ "suggestions": [],
485
+ "auto_fixes_available": [],
486
+ }
487
+
488
+ gates_by_name = {g.name: g for g in self.gates}
489
+
490
+ for gate_result in report.failures:
491
+ gate = gates_by_name.get(gate_result.gate_name)
492
+ feedback["issues"].append(
493
+ {
494
+ "gate": gate_result.gate_name,
495
+ "message": gate_result.message,
496
+ "action": gate.on_fail.value if gate else "unknown",
497
+ }
498
+ )
499
+
500
+ if gate and gate.auto_fix:
501
+ feedback["auto_fixes_available"].append(gate.name)
502
+
503
+ if gate and gate.retry_with_agent:
504
+ feedback["suggestions"].append(
505
+ f"Retry '{gate.name}' with {gate.retry_with_agent} agent"
506
+ )
507
+
508
+ return feedback
509
+
510
+
511
+ # =============================================================================
512
+ # Pre-defined Validation Gates
513
+ # =============================================================================
514
+
515
+
516
+ def _check_story_exists(context: LoopContext) -> bool:
517
+ """Check if story file exists."""
518
+ stories_dir = PROJECT_ROOT / "tooling" / ".automation" / "stories"
519
+ story_file = stories_dir / f"{context.story_key}.md"
520
+ return story_file.exists()
521
+
522
+
523
+ def _check_budget_available(context: LoopContext) -> bool:
524
+ """Check if budget is available."""
525
+ if not HAS_COST_TRACKER:
526
+ return True # Skip check if no cost tracker
527
+
528
+ tracker = get_tracker()
529
+ if not tracker:
530
+ return True # No active tracker, assume OK
531
+
532
+ ok, _, _ = tracker.check_budget()
533
+ return ok
534
+
535
+
536
+ def _check_git_clean(context: LoopContext) -> bool:
537
+ """Check if git working directory is clean or has only staged changes."""
538
+ try:
539
+ result = subprocess.run(
540
+ ["git", "status", "--porcelain"],
541
+ capture_output=True,
542
+ text=True,
543
+ cwd=PROJECT_ROOT,
544
+ timeout=10,
545
+ )
546
+ # Allow staged changes (prefixed with M, A, etc. in first column)
547
+ # but not unstaged changes (prefixed in second column)
548
+ for line in result.stdout.strip().split("\n"):
549
+ if line and line[1] != " ": # Second column is not space
550
+ return False
551
+ return True
552
+ except Exception:
553
+ return True # Don't block on git check failure
554
+
555
+
556
+ def _check_dependencies_valid(context: LoopContext) -> bool:
557
+ """Run validate_setup.py in check mode."""
558
+ validate_script = PROJECT_ROOT / "tooling" / "scripts" / "validate_setup.py"
559
+ if not validate_script.exists():
560
+ return True # Skip if script doesn't exist
561
+
562
+ try:
563
+ result = subprocess.run(
564
+ [sys.executable, str(validate_script), "--quiet"],
565
+ capture_output=True,
566
+ text=True,
567
+ cwd=PROJECT_ROOT,
568
+ timeout=30,
569
+ )
570
+ return result.returncode == 0
571
+ except Exception:
572
+ return True # Don't block on validation failure
573
+
574
+
575
+ def _check_tests_pass(context: LoopContext) -> bool:
576
+ """Run pytest and check if tests pass."""
577
+ try:
578
+ result = subprocess.run(
579
+ [sys.executable, "-m", "pytest", "-x", "-q", "--tb=no"],
580
+ capture_output=True,
581
+ text=True,
582
+ cwd=PROJECT_ROOT,
583
+ timeout=300,
584
+ )
585
+ return result.returncode == 0
586
+ except Exception:
587
+ return False
588
+
589
+
590
+ def _check_lint_pass(context: LoopContext) -> bool:
591
+ """Run ruff linter and check for errors."""
592
+ try:
593
+ result = subprocess.run(
594
+ [sys.executable, "-m", "ruff", "check", "."],
595
+ capture_output=True,
596
+ text=True,
597
+ cwd=PROJECT_ROOT,
598
+ timeout=60,
599
+ )
600
+ # Don't block if ruff not installed
601
+ if "No module named" in result.stderr:
602
+ return True
603
+ return result.returncode == 0
604
+ except Exception:
605
+ return True # Don't block on errors
606
+
607
+
608
+ def _check_types_pass(context: LoopContext) -> bool:
609
+ """Run mypy type checker."""
610
+ try:
611
+ result = subprocess.run(
612
+ [sys.executable, "-m", "mypy", "--ignore-missing-imports", "."],
613
+ capture_output=True,
614
+ text=True,
615
+ cwd=PROJECT_ROOT,
616
+ timeout=120,
617
+ )
618
+ # Don't block if mypy not installed
619
+ if "No module named" in result.stderr:
620
+ return True
621
+ return result.returncode == 0
622
+ except Exception:
623
+ return True # Don't block on errors
624
+
625
+
626
+ def _check_version_synced(context: LoopContext) -> bool:
627
+ """Check if versions are synchronized."""
628
+ sync_script = PROJECT_ROOT / "tooling" / "scripts" / "update_version.py"
629
+ if not sync_script.exists():
630
+ return True
631
+
632
+ try:
633
+ result = subprocess.run(
634
+ [sys.executable, str(sync_script), "--check"],
635
+ capture_output=True,
636
+ text=True,
637
+ cwd=PROJECT_ROOT,
638
+ timeout=30,
639
+ )
640
+ return result.returncode == 0
641
+ except Exception:
642
+ return True
643
+
644
+
645
+ def _check_changelog_updated(context: LoopContext) -> bool:
646
+ """Check if CHANGELOG.md has been updated (for story-related changes)."""
647
+ changelog = PROJECT_ROOT / "CHANGELOG.md"
648
+ if not changelog.exists():
649
+ return True
650
+
651
+ try:
652
+ # Check if CHANGELOG.md has uncommitted changes
653
+ result = subprocess.run(
654
+ ["git", "diff", "--name-only", "HEAD"],
655
+ capture_output=True,
656
+ text=True,
657
+ cwd=PROJECT_ROOT,
658
+ timeout=10,
659
+ )
660
+ return "CHANGELOG.md" in result.stdout
661
+ except Exception:
662
+ return True
663
+
664
+
665
+ def _auto_fix_lint(context: LoopContext) -> bool:
666
+ """Auto-fix lint issues using ruff."""
667
+ try:
668
+ result = subprocess.run(
669
+ [sys.executable, "-m", "ruff", "check", "--fix", "."],
670
+ capture_output=True,
671
+ text=True,
672
+ cwd=PROJECT_ROOT,
673
+ timeout=60,
674
+ )
675
+ # Don't block if ruff not installed
676
+ if "No module named" in result.stderr:
677
+ return True
678
+ return result.returncode == 0
679
+ except Exception:
680
+ return True # Don't block on errors
681
+
682
+
683
+ def _auto_fix_format(context: LoopContext) -> bool:
684
+ """Auto-fix formatting using ruff format."""
685
+ try:
686
+ result = subprocess.run(
687
+ [sys.executable, "-m", "ruff", "format", "."],
688
+ capture_output=True,
689
+ text=True,
690
+ cwd=PROJECT_ROOT,
691
+ timeout=60,
692
+ )
693
+ # Don't block if ruff not installed
694
+ if "No module named" in result.stderr:
695
+ return True
696
+ return result.returncode == 0
697
+ except Exception:
698
+ return True # Don't block on errors
699
+
700
+
701
+ # =============================================================================
702
+ # Pre-defined Gate Sets
703
+ # =============================================================================
704
+
705
+ PREFLIGHT_GATES = [
706
+ ValidationGate(
707
+ name="story_exists",
708
+ description="Verify story file exists",
709
+ validator=_check_story_exists,
710
+ on_fail=FailureAction.BLOCK,
711
+ tier=1,
712
+ tags=["preflight", "required"],
713
+ ),
714
+ ValidationGate(
715
+ name="budget_available",
716
+ description="Check budget is not exceeded",
717
+ validator=_check_budget_available,
718
+ on_fail=FailureAction.BLOCK,
719
+ tier=1,
720
+ tags=["preflight", "budget"],
721
+ ),
722
+ ValidationGate(
723
+ name="dependencies_valid",
724
+ description="Validate setup and dependencies",
725
+ validator=_check_dependencies_valid,
726
+ on_fail=FailureAction.WARN,
727
+ tier=1,
728
+ tags=["preflight", "setup"],
729
+ ),
730
+ ]
731
+
732
+ INTER_PHASE_GATES = [
733
+ ValidationGate(
734
+ name="git_clean",
735
+ description="Check git working directory state",
736
+ validator=_check_git_clean,
737
+ on_fail=FailureAction.WARN,
738
+ tier=2,
739
+ tags=["inter-phase", "git"],
740
+ ),
741
+ ValidationGate(
742
+ name="lint_pass",
743
+ description="Run linter checks",
744
+ validator=_check_lint_pass,
745
+ on_fail=FailureAction.RETRY,
746
+ auto_fix=_auto_fix_lint,
747
+ max_retries=2,
748
+ retry_with_agent="DEV",
749
+ tier=2,
750
+ tags=["inter-phase", "quality"],
751
+ ),
752
+ ]
753
+
754
+ POST_COMPLETION_GATES = [
755
+ ValidationGate(
756
+ name="tests_pass",
757
+ description="Run test suite",
758
+ validator=_check_tests_pass,
759
+ on_fail=FailureAction.BLOCK,
760
+ timeout_seconds=300,
761
+ tier=3,
762
+ tags=["post-completion", "tests"],
763
+ ),
764
+ ValidationGate(
765
+ name="lint_clean",
766
+ description="Verify no lint errors",
767
+ validator=_check_lint_pass,
768
+ on_fail=FailureAction.WARN,
769
+ auto_fix=_auto_fix_lint,
770
+ tier=3,
771
+ tags=["post-completion", "quality"],
772
+ ),
773
+ ValidationGate(
774
+ name="types_valid",
775
+ description="Run type checker",
776
+ validator=_check_types_pass,
777
+ on_fail=FailureAction.WARN,
778
+ tier=3,
779
+ tags=["post-completion", "types"],
780
+ ),
781
+ ValidationGate(
782
+ name="version_synced",
783
+ description="Check version synchronization",
784
+ validator=_check_version_synced,
785
+ on_fail=FailureAction.WARN,
786
+ tier=3,
787
+ tags=["post-completion", "release"],
788
+ ),
789
+ ]
790
+
791
+ ALL_GATES = PREFLIGHT_GATES + INTER_PHASE_GATES + POST_COMPLETION_GATES
792
+
793
+
794
+ # =============================================================================
795
+ # Phase Transition Gates
796
+ # =============================================================================
797
+
798
+ PHASE_TRANSITION_GATES: dict[tuple[str, str], list[ValidationGate]] = {
799
+ ("CONTEXT", "DEV"): [
800
+ ValidationGate(
801
+ name="context_complete",
802
+ description="Verify context phase produced required outputs",
803
+ validator=lambda ctx: ctx.pipeline_output is not None,
804
+ on_fail=FailureAction.RETRY,
805
+ retry_with_agent="SM",
806
+ tier=2,
807
+ ),
808
+ ],
809
+ ("DEV", "REVIEW"): [
810
+ ValidationGate(
811
+ name="code_compiles",
812
+ description="Verify code compiles/parses without errors",
813
+ validator=_check_lint_pass,
814
+ on_fail=FailureAction.RETRY,
815
+ auto_fix=_auto_fix_lint,
816
+ retry_with_agent="DEV",
817
+ tier=2,
818
+ ),
819
+ ],
820
+ ("REVIEW", "COMPLETE"): [
821
+ ValidationGate(
822
+ name="review_approved",
823
+ description="Verify review was approved",
824
+ validator=lambda ctx: "approved" in str(ctx.pipeline_output).lower(),
825
+ on_fail=FailureAction.RETRY,
826
+ retry_with_agent="DEV",
827
+ tier=2,
828
+ ),
829
+ ],
830
+ }
831
+
832
+
833
+ def get_phase_gates(from_phase: str, to_phase: str) -> list[ValidationGate]:
834
+ """Get validation gates for a specific phase transition."""
835
+ key = (from_phase.upper(), to_phase.upper())
836
+ return PHASE_TRANSITION_GATES.get(key, [])
837
+
838
+
839
+ # =============================================================================
840
+ # Validation Memory (extends SharedMemory patterns)
841
+ # =============================================================================
842
+
843
+
844
+ class ValidationMemory:
845
+ """
846
+ Extended memory system for tracking validation history and patterns.
847
+
848
+ Integrates with SharedMemory and KnowledgeGraph to:
849
+ - Record validation results
850
+ - Track common failure patterns
851
+ - Suggest preventive measures
852
+ """
853
+
854
+ def __init__(self, story_key: Optional[str] = None):
855
+ self.story_key = story_key
856
+ self.validation_dir = VALIDATION_DIR
857
+ self.validation_dir.mkdir(parents=True, exist_ok=True)
858
+
859
+ if HAS_SHARED_MEMORY:
860
+ self.shared_memory = SharedMemory(story_key)
861
+ self.knowledge_graph = KnowledgeGraph(story_key)
862
+ else:
863
+ self.shared_memory = None
864
+ self.knowledge_graph = None
865
+
866
+ def record_validation(
867
+ self,
868
+ gate_name: str,
869
+ result: ValidationResult,
870
+ context: LoopContext,
871
+ details: Optional[dict[str, Any]] = None,
872
+ ):
873
+ """Record a validation result."""
874
+ if self.shared_memory:
875
+ self.shared_memory.add(
876
+ agent="VALIDATOR",
877
+ content=f"Gate '{gate_name}': {result.value}",
878
+ tags=["validation", gate_name, result.value],
879
+ )
880
+
881
+ if self.knowledge_graph:
882
+ self.knowledge_graph.add_decision(
883
+ agent="VALIDATOR",
884
+ topic=f"validation:{gate_name}",
885
+ decision=result.value,
886
+ context={
887
+ "iteration": context.iteration,
888
+ "phase": context.phase,
889
+ "details": details or {},
890
+ },
891
+ )
892
+
893
+ def get_common_failures(self, limit: int = 10) -> list[dict[str, Any]]:
894
+ """Get most common validation failures for learning."""
895
+ if not self.knowledge_graph:
896
+ return []
897
+
898
+ failures = []
899
+ for dec in self.knowledge_graph.decisions.values():
900
+ if dec.topic.startswith("validation:") and dec.decision == "fail":
901
+ failures.append(
902
+ {
903
+ "gate": dec.topic.replace("validation:", ""),
904
+ "timestamp": dec.timestamp,
905
+ "context": dec.context,
906
+ }
907
+ )
908
+
909
+ return sorted(failures, key=lambda x: x["timestamp"], reverse=True)[:limit]
910
+
911
+ def get_success_rate(self, gate_name: str) -> float:
912
+ """Calculate success rate for a specific gate."""
913
+ if not self.knowledge_graph:
914
+ return 1.0
915
+
916
+ total = 0
917
+ passed = 0
918
+ for dec in self.knowledge_graph.decisions.values():
919
+ if dec.topic == f"validation:{gate_name}":
920
+ total += 1
921
+ if dec.decision == "pass":
922
+ passed += 1
923
+
924
+ return passed / total if total > 0 else 1.0
925
+
926
+
927
+ # =============================================================================
928
+ # Convenience Functions
929
+ # =============================================================================
930
+
931
+
932
+ def create_validation_loop(
933
+ story_key: str,
934
+ gates: Optional[list[ValidationGate]] = None,
935
+ config: Optional[dict[str, Any]] = None,
936
+ ) -> ValidationLoop:
937
+ """Create a validation loop with default or custom gates."""
938
+ return ValidationLoop(
939
+ gates=gates or ALL_GATES,
940
+ config=config or {"auto_fix_enabled": True},
941
+ story_key=story_key,
942
+ )
943
+
944
+
945
+ def run_preflight_validation(story_key: str) -> ValidationReport:
946
+ """Quick function to run pre-flight validation."""
947
+ loop = ValidationLoop(PREFLIGHT_GATES, story_key=story_key)
948
+ context = LoopContext(story_key=story_key)
949
+ return loop.run_preflight(context)
950
+
951
+
952
+ def run_post_completion_validation(story_key: str) -> ValidationReport:
953
+ """Quick function to run post-completion validation."""
954
+ loop = ValidationLoop(POST_COMPLETION_GATES, story_key=story_key)
955
+ context = LoopContext(story_key=story_key)
956
+ return loop.run_post_completion(context)
957
+
958
+
959
+ # =============================================================================
960
+ # CLI Entry Point
961
+ # =============================================================================
962
+
963
+ if __name__ == "__main__":
964
+ import argparse
965
+
966
+ parser = argparse.ArgumentParser(description="Run validation loop")
967
+ parser.add_argument("--story", "-s", help="Story key to validate")
968
+ parser.add_argument(
969
+ "--tier",
970
+ "-t",
971
+ type=int,
972
+ choices=[1, 2, 3],
973
+ help="Validation tier (1=preflight, 2=inter-phase, 3=post-completion)",
974
+ )
975
+ parser.add_argument("--all", "-a", action="store_true", help="Run all tiers")
976
+ parser.add_argument("--json", action="store_true", help="Output as JSON")
977
+ parser.add_argument("--quiet", "-q", action="store_true", help="Minimal output")
978
+
979
+ args = parser.parse_args()
980
+
981
+ story_key = args.story or "validation-test"
982
+ context = LoopContext(story_key=story_key)
983
+
984
+ if args.all:
985
+ loop = ValidationLoop(ALL_GATES, story_key=story_key)
986
+ reports = []
987
+ for tier in [1, 2, 3]:
988
+ report = loop.run_gates(context, tier=tier)
989
+ reports.append(report)
990
+
991
+ if args.json:
992
+ print(json.dumps([r.to_dict() for r in reports], indent=2))
993
+ else:
994
+ for report in reports:
995
+ print(report.to_summary())
996
+ print()
997
+ else:
998
+ tier = args.tier or 1
999
+ if tier == 1:
1000
+ gates = PREFLIGHT_GATES
1001
+ elif tier == 2:
1002
+ gates = INTER_PHASE_GATES
1003
+ else:
1004
+ gates = POST_COMPLETION_GATES
1005
+
1006
+ loop = ValidationLoop(gates, story_key=story_key)
1007
+ report = loop.run_gates(context, tier=tier)
1008
+
1009
+ if args.json:
1010
+ print(json.dumps(report.to_dict(), indent=2))
1011
+ elif not args.quiet:
1012
+ print(report.to_summary())
1013
+
1014
+ sys.exit(0 if report.passed else 1)