@pjmendonca/devflow 1.13.2 → 1.18.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.
- package/.claude/commands/agent.md +1 -1
- package/.claude/commands/bugfix.md +21 -0
- package/.claude/commands/checkpoint.md +0 -1
- package/.claude/commands/collab.md +0 -1
- package/.claude/commands/costs.md +88 -18
- package/.claude/commands/devflow.md +26 -0
- package/.claude/commands/handoff.md +0 -1
- package/.claude/commands/init.md +287 -0
- package/.claude/commands/memory.md +0 -1
- package/.claude/commands/pair.md +0 -1
- package/.claude/commands/review.md +27 -0
- package/.claude/commands/route.md +0 -1
- package/.claude/commands/swarm.md +0 -1
- package/.claude/commands/validate.md +55 -0
- package/.claude/hooks/session-notification.sh +44 -0
- package/.claude/hooks/session-startup.sh +427 -0
- package/.claude/hooks/session-stop.sh +38 -0
- package/.claude/hooks/session_tracker.py +272 -0
- package/.claude/settings.json +38 -0
- package/.claude/skills/costs/SKILL.md +156 -0
- package/.claude/skills/validate/SKILL.md +101 -0
- package/CHANGELOG.md +243 -0
- package/README.md +207 -10
- package/bin/devflow-install.js +2 -1
- package/bin/devflow.js +4 -0
- package/lib/constants.js +0 -1
- package/lib/exec-python.js +1 -1
- package/package.json +1 -1
- package/tooling/.automation/.checkpoint_lock +1 -0
- package/tooling/.automation/agents/architect.md +19 -0
- package/tooling/.automation/agents/ba.md +19 -0
- package/tooling/.automation/agents/maintainer.md +19 -0
- package/tooling/.automation/agents/pm.md +19 -0
- package/tooling/.automation/agents/reviewer.md +1 -1
- package/tooling/.automation/agents/writer.md +19 -0
- package/tooling/.automation/benchmarks/benchmark_20251230_100119.json +314 -0
- package/tooling/.automation/benchmarks/benchmark_20251230_100216.json +314 -0
- package/tooling/.automation/costs/config.json +31 -0
- package/tooling/.automation/costs/sessions/2025-12-29_20251229_164128.json +22 -0
- package/tooling/.automation/memory/knowledge/kg_integration-test.json +707 -1
- package/tooling/.automation/memory/knowledge/kg_test-story.json +3273 -2
- package/tooling/.automation/memory/shared/shared_integration-test.json +181 -1
- package/tooling/.automation/memory/shared/shared_test-story.json +721 -1
- package/tooling/.automation/memory/shared/shared_test.json +1254 -0
- package/tooling/.automation/memory/shared/shared_validation-check.json +227 -0
- package/tooling/.automation/overrides/templates/architect/cloud-native.yaml +5 -5
- package/tooling/.automation/overrides/templates/architect/enterprise-architect.yaml +23 -5
- package/tooling/.automation/overrides/templates/architect/pragmatic-minimalist.yaml +24 -6
- package/tooling/.automation/overrides/templates/ba/agile-storyteller.yaml +4 -4
- package/tooling/.automation/overrides/templates/ba/domain-expert.yaml +4 -4
- package/tooling/.automation/overrides/templates/ba/requirements-engineer.yaml +4 -4
- package/tooling/.automation/overrides/templates/dev/performance-engineer.yaml +18 -0
- package/tooling/.automation/overrides/templates/dev/rapid-prototyper.yaml +19 -1
- package/tooling/.automation/overrides/templates/dev/security-focused.yaml +18 -0
- package/tooling/.automation/overrides/templates/dev/user-advocate.yaml +54 -0
- package/tooling/.automation/overrides/templates/maintainer/devops-maintainer.yaml +4 -4
- package/tooling/.automation/overrides/templates/maintainer/legacy-steward.yaml +4 -4
- package/tooling/.automation/overrides/templates/maintainer/oss-maintainer.yaml +4 -4
- package/tooling/.automation/overrides/templates/maintainer/reliability-engineer.yaml +55 -0
- package/tooling/.automation/overrides/templates/pm/agile-pm.yaml +4 -4
- package/tooling/.automation/overrides/templates/pm/hybrid-delivery.yaml +3 -3
- package/tooling/.automation/overrides/templates/pm/traditional-pm.yaml +4 -4
- package/tooling/.automation/overrides/templates/reviewer/quick-sanity.yaml +18 -0
- package/tooling/.automation/overrides/templates/reviewer/thorough-critic.yaml +18 -0
- package/tooling/.automation/overrides/templates/sm/agile-coach.yaml +2 -2
- package/tooling/.automation/overrides/templates/sm/startup-pm.yaml +3 -3
- package/tooling/.automation/overrides/templates/writer/api-documentarian.yaml +5 -5
- package/tooling/.automation/overrides/templates/writer/docs-as-code.yaml +4 -4
- package/tooling/.automation/overrides/templates/writer/user-guide-author.yaml +5 -5
- package/tooling/.automation/validation/history/2025-12-29_val_002a28c1.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_01273bb1.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_03369914.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_07a449ba.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_0df1f0a2.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_10ff3d34.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_110771d7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_13f3a7f9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_17ba9d21.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_22247089.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_227ea6a4.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2335d5ae.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_246824bb.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_28b4b9cd.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2abd12cc.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2c801b2f.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2c8cfa8e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_2ce76eb0.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_30351948.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_30eb7229.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_34df0e77.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_376e4d6a.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_3a4e8a1a.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_3b77a628.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_3ea4e1cf.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_44aacdb4.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_457ddfa8.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_45af6238.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_4735dba1.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_486b203c.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_49dc56cd.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_4d863d6d.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_5149a808.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_52e0bb43.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_585d6319.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_5b2d859a.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_635a7081.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_64df4905.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_70634cee.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_714553f9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_7f7bfdbf.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_7faad91d.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_81821f8f.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8249f3c9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8422b50f.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8446c134.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_879f4e26.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8b6d5bd7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_8c5cd787.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_91d20bc7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_958a12b7.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_95d91108.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_980dbb74.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_9e40c79b.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_9f499b7c.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_9f7c3b57.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_a30d5bd4.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_a6eb09c7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_a86f7b83.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ad5347e1.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_b0a5a993.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_bcb0192e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_bf3c9aaa.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c461ff88.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c4f4e258.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c7f0fa6d.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_c911b0e6.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_cc581964.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_cdd5a33b.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_cfd42495.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d1c7a4ee.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d2280d0e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d2a6ff69.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d8c53ab2.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d9c1247a.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_d9d58569.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_dabb4fd9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_dd8fe359.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_decdffc9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_e3a95476.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_e776dfca.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ea70969f.json +59 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ef41ea95.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_f384f9b1.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_f8adc38c.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_fa40b69e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_fc538d54.json +41 -0
- package/tooling/.automation/validation/history/2025-12-29_val_fe814665.json +32 -0
- package/tooling/.automation/validation/history/2025-12-29_val_ffea4b12.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_02d001e5.json +59 -0
- package/tooling/.automation/validation/history/2025-12-30_val_0b8966dc.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_15455fbf.json +59 -0
- package/tooling/.automation/validation/history/2025-12-30_val_157e34b9.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_28d1d933.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_3442a52c.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_37f1ce1e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_4f1d8a93.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_56ff1de3.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_664fd4e2.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_66afb0a7.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_7634663c.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_8ea830c3.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_998957c2.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_a52177db.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_a5b65a63.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ae391d0e.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_c7895339.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ca416593.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_cee19422.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ddd4f4e6.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_f2e1394b.json +32 -0
- package/tooling/.automation/validation/history/2025-12-30_val_f4a7fa06.json +41 -0
- package/tooling/.automation/validation/history/2025-12-30_val_ffea3369.json +32 -0
- package/tooling/.automation/validation-config.yaml +103 -0
- package/tooling/completions/DevflowCompletion.ps1 +21 -21
- package/tooling/completions/_run-story +3 -3
- package/tooling/completions/run-story-completion.bash +8 -8
- package/tooling/docs/DOC-STANDARD.md +14 -14
- package/tooling/docs/templates/migration-spec.md +4 -4
- package/tooling/scripts/context_checkpoint.py +5 -15
- package/tooling/scripts/cost_dashboard.py +610 -13
- package/tooling/scripts/create-persona.py +1 -12
- package/tooling/scripts/create-persona.sh +44 -44
- package/tooling/scripts/lib/__init__.py +12 -1
- package/tooling/scripts/lib/agent_handoff.py +11 -2
- package/tooling/scripts/lib/agent_router.py +31 -10
- package/tooling/scripts/lib/colors.py +106 -0
- package/tooling/scripts/lib/context_monitor.py +766 -0
- package/tooling/scripts/lib/cost_config.py +229 -10
- package/tooling/scripts/lib/cost_display.py +20 -45
- package/tooling/scripts/lib/cost_tracker.py +462 -15
- package/tooling/scripts/lib/currency_converter.py +28 -5
- package/tooling/scripts/lib/pair_programming.py +102 -3
- package/tooling/scripts/lib/personality_system.py +949 -0
- package/tooling/scripts/lib/platform.py +55 -0
- package/tooling/scripts/lib/shared_memory.py +9 -3
- package/tooling/scripts/lib/swarm_orchestrator.py +514 -75
- package/tooling/scripts/lib/validation_loop.py +1014 -0
- package/tooling/scripts/memory_summarize.py +9 -2
- package/tooling/scripts/new-doc.py +2 -9
- package/tooling/scripts/personalize_agent.py +1 -12
- package/tooling/scripts/rollback-migration.sh +60 -60
- package/tooling/scripts/run-collab.ps1 +16 -16
- package/tooling/scripts/run-collab.py +88 -53
- package/tooling/scripts/run-collab.sh +4 -4
- package/tooling/scripts/run-story.py +278 -20
- package/tooling/scripts/run-story.sh +3 -3
- package/tooling/scripts/setup-checkpoint-service.py +2 -9
- package/tooling/scripts/tech-debt-tracker.py +1 -12
- package/tooling/scripts/test_adversarial_swarm.py +452 -0
- package/tooling/scripts/validate-overrides.py +1 -10
- package/tooling/scripts/validate-overrides.sh +40 -40
- package/tooling/scripts/validate_loop.py +162 -0
- package/tooling/scripts/validate_setup.py +2 -30
- 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)
|