anvil-dev-framework 0.1.6

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 (190) hide show
  1. package/README.md +719 -0
  2. package/VERSION +1 -0
  3. package/docs/ANVIL-REPO-IMPLEMENTATION-PLAN.md +441 -0
  4. package/docs/FIRST-SKILL-TUTORIAL.md +408 -0
  5. package/docs/INSTALLATION-RETRO-NOTES.md +458 -0
  6. package/docs/INSTALLATION.md +984 -0
  7. package/docs/anvil-hud.md +469 -0
  8. package/docs/anvil-init.md +255 -0
  9. package/docs/anvil-state.md +210 -0
  10. package/docs/boris-cherny-ralph-wiggum-insights.md +608 -0
  11. package/docs/command-reference.md +2022 -0
  12. package/docs/hooks-tts.md +368 -0
  13. package/docs/implementation-guide.md +810 -0
  14. package/docs/linear-github-integration.md +247 -0
  15. package/docs/local-issues.md +677 -0
  16. package/docs/patterns/README.md +419 -0
  17. package/docs/planning-responsibilities.md +139 -0
  18. package/docs/session-workflow.md +573 -0
  19. package/docs/simplification-plan-template.md +297 -0
  20. package/docs/simplification-principles.md +129 -0
  21. package/docs/specifications/CCS-RALPH-INTEGRATION-DESIGN.md +633 -0
  22. package/docs/specifications/CCS-RESEARCH-REPORT.md +169 -0
  23. package/docs/specifications/PLAN-ANV-verification-ralph-wiggum.md +403 -0
  24. package/docs/specifications/PLAN-parallel-tracks-anvil-memory-ccs.md +494 -0
  25. package/docs/specifications/SPEC-ANV-VRW/component-01-verify.md +208 -0
  26. package/docs/specifications/SPEC-ANV-VRW/component-02-stop-gate.md +226 -0
  27. package/docs/specifications/SPEC-ANV-VRW/component-03-posttooluse.md +209 -0
  28. package/docs/specifications/SPEC-ANV-VRW/component-04-ralph-wiggum.md +604 -0
  29. package/docs/specifications/SPEC-ANV-VRW/component-05-atomic-actions.md +311 -0
  30. package/docs/specifications/SPEC-ANV-VRW/component-06-verify-subagent.md +264 -0
  31. package/docs/specifications/SPEC-ANV-VRW/component-07-claude-md.md +363 -0
  32. package/docs/specifications/SPEC-ANV-VRW/index.md +182 -0
  33. package/docs/specifications/SPEC-ANV-anvil-memory.md +573 -0
  34. package/docs/specifications/SPEC-ANV-context-checkpoints.md +781 -0
  35. package/docs/specifications/SPEC-ANV-verification-ralph-wiggum.md +789 -0
  36. package/docs/sync.md +122 -0
  37. package/global/CLAUDE.md +140 -0
  38. package/global/agents/verify-app.md +164 -0
  39. package/global/commands/anvil-settings.md +527 -0
  40. package/global/commands/anvil-sync.md +121 -0
  41. package/global/commands/change.md +197 -0
  42. package/global/commands/clarify.md +252 -0
  43. package/global/commands/cleanup.md +292 -0
  44. package/global/commands/commit-push-pr.md +207 -0
  45. package/global/commands/decay-review.md +127 -0
  46. package/global/commands/discover.md +158 -0
  47. package/global/commands/doc-coverage.md +122 -0
  48. package/global/commands/evidence.md +307 -0
  49. package/global/commands/explore.md +121 -0
  50. package/global/commands/force-exit.md +135 -0
  51. package/global/commands/handoff.md +191 -0
  52. package/global/commands/healthcheck.md +302 -0
  53. package/global/commands/hud.md +84 -0
  54. package/global/commands/insights.md +319 -0
  55. package/global/commands/linear-setup.md +184 -0
  56. package/global/commands/lint-fix.md +198 -0
  57. package/global/commands/orient.md +510 -0
  58. package/global/commands/plan.md +228 -0
  59. package/global/commands/ralph.md +346 -0
  60. package/global/commands/ready.md +182 -0
  61. package/global/commands/release.md +305 -0
  62. package/global/commands/retro.md +96 -0
  63. package/global/commands/shard.md +166 -0
  64. package/global/commands/spec.md +227 -0
  65. package/global/commands/sprint.md +184 -0
  66. package/global/commands/tasks.md +228 -0
  67. package/global/commands/test-and-commit.md +151 -0
  68. package/global/commands/validate.md +132 -0
  69. package/global/commands/verify.md +251 -0
  70. package/global/commands/weekly-review.md +156 -0
  71. package/global/hooks/__pycache__/ralph_context_monitor.cpython-314.pyc +0 -0
  72. package/global/hooks/__pycache__/statusline_agent_sync.cpython-314.pyc +0 -0
  73. package/global/hooks/anvil_memory_observe.ts +322 -0
  74. package/global/hooks/anvil_memory_session.ts +166 -0
  75. package/global/hooks/anvil_memory_stop.ts +187 -0
  76. package/global/hooks/parse_transcript.py +116 -0
  77. package/global/hooks/post_merge_cleanup.sh +132 -0
  78. package/global/hooks/post_tool_format.sh +215 -0
  79. package/global/hooks/ralph_context_monitor.py +240 -0
  80. package/global/hooks/ralph_stop.sh +502 -0
  81. package/global/hooks/statusline.sh +1110 -0
  82. package/global/hooks/statusline_agent_sync.py +224 -0
  83. package/global/hooks/stop_gate.sh +250 -0
  84. package/global/lib/.claude/anvil-state.json +21 -0
  85. package/global/lib/__pycache__/agent_registry.cpython-314.pyc +0 -0
  86. package/global/lib/__pycache__/claim_service.cpython-314.pyc +0 -0
  87. package/global/lib/__pycache__/coderabbit_service.cpython-314.pyc +0 -0
  88. package/global/lib/__pycache__/config_service.cpython-314.pyc +0 -0
  89. package/global/lib/__pycache__/coordination_service.cpython-314.pyc +0 -0
  90. package/global/lib/__pycache__/doc_coverage_service.cpython-314.pyc +0 -0
  91. package/global/lib/__pycache__/gate_logger.cpython-314.pyc +0 -0
  92. package/global/lib/__pycache__/github_service.cpython-314.pyc +0 -0
  93. package/global/lib/__pycache__/hygiene_service.cpython-314.pyc +0 -0
  94. package/global/lib/__pycache__/issue_models.cpython-314.pyc +0 -0
  95. package/global/lib/__pycache__/issue_provider.cpython-314.pyc +0 -0
  96. package/global/lib/__pycache__/linear_data_service.cpython-314.pyc +0 -0
  97. package/global/lib/__pycache__/linear_provider.cpython-314.pyc +0 -0
  98. package/global/lib/__pycache__/local_provider.cpython-314.pyc +0 -0
  99. package/global/lib/__pycache__/quality_service.cpython-314.pyc +0 -0
  100. package/global/lib/__pycache__/ralph_state.cpython-314.pyc +0 -0
  101. package/global/lib/__pycache__/state_manager.cpython-314.pyc +0 -0
  102. package/global/lib/__pycache__/transcript_parser.cpython-314.pyc +0 -0
  103. package/global/lib/__pycache__/verification_runner.cpython-314.pyc +0 -0
  104. package/global/lib/__pycache__/verify_iteration.cpython-314.pyc +0 -0
  105. package/global/lib/__pycache__/verify_subagent.cpython-314.pyc +0 -0
  106. package/global/lib/agent_registry.py +995 -0
  107. package/global/lib/anvil-state.sh +435 -0
  108. package/global/lib/claim_service.py +515 -0
  109. package/global/lib/coderabbit_service.py +314 -0
  110. package/global/lib/config_service.py +423 -0
  111. package/global/lib/coordination_service.py +331 -0
  112. package/global/lib/doc_coverage_service.py +1305 -0
  113. package/global/lib/gate_logger.py +316 -0
  114. package/global/lib/github_service.py +310 -0
  115. package/global/lib/handoff_generator.py +775 -0
  116. package/global/lib/hygiene_service.py +712 -0
  117. package/global/lib/issue_models.py +257 -0
  118. package/global/lib/issue_provider.py +339 -0
  119. package/global/lib/linear_data_service.py +210 -0
  120. package/global/lib/linear_provider.py +987 -0
  121. package/global/lib/linear_provider.py.backup +671 -0
  122. package/global/lib/local_provider.py +486 -0
  123. package/global/lib/orient_fast.py +457 -0
  124. package/global/lib/quality_service.py +470 -0
  125. package/global/lib/ralph_prompt_generator.py +563 -0
  126. package/global/lib/ralph_state.py +1202 -0
  127. package/global/lib/state_manager.py +417 -0
  128. package/global/lib/transcript_parser.py +597 -0
  129. package/global/lib/verification_runner.py +557 -0
  130. package/global/lib/verify_iteration.py +490 -0
  131. package/global/lib/verify_subagent.py +250 -0
  132. package/global/skills/README.md +155 -0
  133. package/global/skills/quality-gates/SKILL.md +252 -0
  134. package/global/skills/skill-template/SKILL.md +109 -0
  135. package/global/skills/testing-strategies/SKILL.md +337 -0
  136. package/global/templates/CHANGE-template.md +105 -0
  137. package/global/templates/HANDOFF-template.md +63 -0
  138. package/global/templates/PLAN-template.md +111 -0
  139. package/global/templates/SPEC-template.md +93 -0
  140. package/global/templates/ralph/PROMPT.md.template +89 -0
  141. package/global/templates/ralph/fix_plan.md.template +31 -0
  142. package/global/templates/ralph/progress.txt.template +23 -0
  143. package/global/tests/__pycache__/test_doc_coverage.cpython-314.pyc +0 -0
  144. package/global/tests/test_doc_coverage.py +520 -0
  145. package/global/tests/test_issue_models.py +299 -0
  146. package/global/tests/test_local_provider.py +323 -0
  147. package/global/tools/README.md +178 -0
  148. package/global/tools/__pycache__/anvil-hud.cpython-314.pyc +0 -0
  149. package/global/tools/anvil-hud.py +3622 -0
  150. package/global/tools/anvil-hud.py.bak +3318 -0
  151. package/global/tools/anvil-issue.py +432 -0
  152. package/global/tools/anvil-memory/CLAUDE.md +49 -0
  153. package/global/tools/anvil-memory/README.md +42 -0
  154. package/global/tools/anvil-memory/bun.lock +25 -0
  155. package/global/tools/anvil-memory/bunfig.toml +9 -0
  156. package/global/tools/anvil-memory/package.json +23 -0
  157. package/global/tools/anvil-memory/src/__tests__/ccs/context-monitor.test.ts +535 -0
  158. package/global/tools/anvil-memory/src/__tests__/ccs/edge-cases.test.ts +645 -0
  159. package/global/tools/anvil-memory/src/__tests__/ccs/fixtures.ts +363 -0
  160. package/global/tools/anvil-memory/src/__tests__/ccs/index.ts +8 -0
  161. package/global/tools/anvil-memory/src/__tests__/ccs/integration.test.ts +417 -0
  162. package/global/tools/anvil-memory/src/__tests__/ccs/prompt-generator.test.ts +571 -0
  163. package/global/tools/anvil-memory/src/__tests__/ccs/ralph-stop.test.ts +440 -0
  164. package/global/tools/anvil-memory/src/__tests__/ccs/test-utils.ts +252 -0
  165. package/global/tools/anvil-memory/src/__tests__/commands.test.ts +657 -0
  166. package/global/tools/anvil-memory/src/__tests__/db.test.ts +641 -0
  167. package/global/tools/anvil-memory/src/__tests__/hooks.test.ts +272 -0
  168. package/global/tools/anvil-memory/src/__tests__/performance.test.ts +427 -0
  169. package/global/tools/anvil-memory/src/__tests__/test-utils.ts +113 -0
  170. package/global/tools/anvil-memory/src/commands/checkpoint.ts +197 -0
  171. package/global/tools/anvil-memory/src/commands/get.ts +115 -0
  172. package/global/tools/anvil-memory/src/commands/init.ts +94 -0
  173. package/global/tools/anvil-memory/src/commands/observe.ts +163 -0
  174. package/global/tools/anvil-memory/src/commands/search.ts +112 -0
  175. package/global/tools/anvil-memory/src/db.ts +638 -0
  176. package/global/tools/anvil-memory/src/index.ts +205 -0
  177. package/global/tools/anvil-memory/src/types.ts +122 -0
  178. package/global/tools/anvil-memory/tsconfig.json +29 -0
  179. package/global/tools/ralph-loop.sh +359 -0
  180. package/package.json +45 -0
  181. package/scripts/anvil +822 -0
  182. package/scripts/extract_patterns.py +222 -0
  183. package/scripts/init-project.sh +541 -0
  184. package/scripts/install.sh +229 -0
  185. package/scripts/postinstall.js +41 -0
  186. package/scripts/rollback.sh +188 -0
  187. package/scripts/sync.sh +623 -0
  188. package/scripts/test-statusline.sh +248 -0
  189. package/scripts/update_claude_md.py +224 -0
  190. package/scripts/verify.sh +255 -0
@@ -0,0 +1,557 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ verification_runner.py - Verification suite runner for /verify command (ANV-149)
4
+
5
+ Executes test, lint, and type check commands with output capture and error parsing.
6
+ Supports project-specific configuration via .claude/settings.yaml.
7
+
8
+ Usage:
9
+ from verification_runner import VerificationRunner, VerificationResult
10
+
11
+ runner = VerificationRunner()
12
+ result = runner.run_all()
13
+
14
+ if result.passed:
15
+ print("All checks passed!")
16
+ else:
17
+ for error in result.errors:
18
+ print(f"{error.file}:{error.line} - {error.message}")
19
+ """
20
+
21
+ import json
22
+ import re
23
+ import subprocess
24
+ import time
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import Any, Dict, List, Literal, Optional, Tuple
28
+
29
+ try:
30
+ import yaml
31
+ HAS_YAML = True
32
+ except ImportError:
33
+ HAS_YAML = False
34
+
35
+
36
+ # =============================================================================
37
+ # Data Models
38
+ # =============================================================================
39
+
40
+ @dataclass
41
+ class VerificationError:
42
+ """A single verification error with location information."""
43
+ file: str
44
+ line: Optional[int] = None
45
+ column: Optional[int] = None
46
+ message: str = ""
47
+ severity: Literal["error", "warning"] = "error"
48
+ source: Literal["test", "lint", "types"] = "test"
49
+ raw_output: str = ""
50
+
51
+ def to_dict(self) -> Dict[str, Any]:
52
+ return {
53
+ "file": self.file,
54
+ "line": self.line,
55
+ "column": self.column,
56
+ "message": self.message,
57
+ "severity": self.severity,
58
+ "source": self.source,
59
+ }
60
+
61
+
62
+ @dataclass
63
+ class CheckResult:
64
+ """Result of a single verification check (test/lint/types)."""
65
+ check_type: Literal["test", "lint", "types"]
66
+ passed: bool
67
+ exit_code: int
68
+ duration_ms: int
69
+ output: str
70
+ errors: List[VerificationError] = field(default_factory=list)
71
+ count: Optional[int] = None # Number of tests passed, etc.
72
+
73
+ def to_dict(self) -> Dict[str, Any]:
74
+ return {
75
+ "check_type": self.check_type,
76
+ "passed": self.passed,
77
+ "exit_code": self.exit_code,
78
+ "duration_ms": self.duration_ms,
79
+ "errors": [e.to_dict() for e in self.errors],
80
+ "count": self.count,
81
+ }
82
+
83
+
84
+ @dataclass
85
+ class VerificationResult:
86
+ """Complete verification suite result."""
87
+ passed: bool
88
+ tests: Optional[CheckResult] = None
89
+ lint: Optional[CheckResult] = None
90
+ types: Optional[CheckResult] = None
91
+ total_duration_ms: int = 0
92
+ errors: List[VerificationError] = field(default_factory=list)
93
+
94
+ def to_dict(self) -> Dict[str, Any]:
95
+ return {
96
+ "passed": self.passed,
97
+ "tests": self.tests.to_dict() if self.tests else None,
98
+ "lint": self.lint.to_dict() if self.lint else None,
99
+ "types": self.types.to_dict() if self.types else None,
100
+ "total_duration_ms": self.total_duration_ms,
101
+ "error_count": len(self.errors),
102
+ }
103
+
104
+ def to_json(self) -> str:
105
+ return json.dumps(self.to_dict(), indent=2)
106
+
107
+
108
+ @dataclass
109
+ class VerificationConfig:
110
+ """Configuration for verification commands."""
111
+ test_command: Optional[str] = None
112
+ lint_command: Optional[str] = None
113
+ types_command: Optional[str] = None
114
+ max_iterations: int = 3
115
+ required_for_completion: bool = True
116
+ timeout_seconds: int = 300
117
+
118
+ @classmethod
119
+ def from_dict(cls, data: Dict[str, Any]) -> "VerificationConfig":
120
+ commands = data.get("commands", {})
121
+ return cls(
122
+ test_command=commands.get("test"),
123
+ lint_command=commands.get("lint"),
124
+ types_command=commands.get("types"),
125
+ max_iterations=data.get("max_iterations", 3),
126
+ required_for_completion=data.get("required_for_completion", True),
127
+ timeout_seconds=data.get("timeout_seconds", 300),
128
+ )
129
+
130
+
131
+ # =============================================================================
132
+ # Project Type Detection
133
+ # =============================================================================
134
+
135
+ class ProjectDetector:
136
+ """Detect project type and default verification commands."""
137
+
138
+ NODEJS_DEFAULTS = {
139
+ "test": "npm test",
140
+ "lint": "npm run lint",
141
+ "types": "npm run typecheck",
142
+ }
143
+
144
+ PYTHON_DEFAULTS = {
145
+ "test": "pytest",
146
+ "lint": "ruff check .",
147
+ "types": "mypy .",
148
+ }
149
+
150
+ RUST_DEFAULTS = {
151
+ "test": "cargo test",
152
+ "lint": "cargo clippy",
153
+ "types": "cargo check",
154
+ }
155
+
156
+ GO_DEFAULTS = {
157
+ "test": "go test ./...",
158
+ "lint": "golangci-lint run",
159
+ "types": None, # Go is statically typed at compile
160
+ }
161
+
162
+ @classmethod
163
+ def detect(cls, project_root: Path) -> Tuple[str, Dict[str, Optional[str]]]:
164
+ """Detect project type and return default commands."""
165
+ if (project_root / "package.json").exists():
166
+ return "nodejs", cls.NODEJS_DEFAULTS
167
+
168
+ if (project_root / "pyproject.toml").exists() or \
169
+ (project_root / "setup.py").exists() or \
170
+ (project_root / "requirements.txt").exists():
171
+ return "python", cls.PYTHON_DEFAULTS
172
+
173
+ if (project_root / "Cargo.toml").exists():
174
+ return "rust", cls.RUST_DEFAULTS
175
+
176
+ if (project_root / "go.mod").exists():
177
+ return "go", cls.GO_DEFAULTS
178
+
179
+ # Default to Node.js style
180
+ return "unknown", cls.NODEJS_DEFAULTS
181
+
182
+
183
+ # =============================================================================
184
+ # Error Parsers
185
+ # =============================================================================
186
+
187
+ class ErrorParser:
188
+ """Parse errors from command output."""
189
+
190
+ # Common patterns for extracting file:line:column
191
+ FILE_LINE_PATTERNS = [
192
+ # TypeScript/JavaScript: src/file.ts(10,5): error TS2345
193
+ re.compile(r"^(.+?)\((\d+),(\d+)\):\s*(.+)$", re.MULTILINE),
194
+ # ESLint/Ruff: src/file.ts:10:5: error message
195
+ re.compile(r"^(.+?):(\d+):(\d+):\s*(.+)$", re.MULTILINE),
196
+ # Python traceback: File "src/file.py", line 10
197
+ re.compile(r'File "(.+?)", line (\d+)', re.MULTILINE),
198
+ # Jest/Vitest: at Object.<anonymous> (src/file.test.ts:10:5)
199
+ re.compile(r"at .+? \((.+?):(\d+):(\d+)\)", re.MULTILINE),
200
+ # Generic: src/file.ts:10
201
+ re.compile(r"^(.+?):(\d+)(?::\d+)?(?:\s|$)", re.MULTILINE),
202
+ ]
203
+
204
+ # Test failure patterns
205
+ TEST_FAILURE_PATTERNS = [
206
+ # Jest/Vitest: FAIL src/file.test.ts
207
+ re.compile(r"FAIL\s+(.+\.(?:test|spec)\.[jt]sx?)"),
208
+ # Pytest: FAILED test_file.py::test_name
209
+ re.compile(r"FAILED\s+(.+?)::", re.MULTILINE),
210
+ ]
211
+
212
+ @classmethod
213
+ def parse_errors(
214
+ cls,
215
+ output: str,
216
+ source: Literal["test", "lint", "types"]
217
+ ) -> List[VerificationError]:
218
+ """Parse errors from command output."""
219
+ errors = []
220
+ seen = set()
221
+
222
+ for pattern in cls.FILE_LINE_PATTERNS:
223
+ for match in pattern.finditer(output):
224
+ groups = match.groups()
225
+ file_path = groups[0] if groups else None
226
+
227
+ if not file_path or file_path in seen:
228
+ continue
229
+
230
+ # Skip common false positives
231
+ if any(skip in file_path for skip in [
232
+ "node_modules", ".git", "__pycache__", "site-packages"
233
+ ]):
234
+ continue
235
+
236
+ line = int(groups[1]) if len(groups) > 1 and groups[1] else None
237
+ column = int(groups[2]) if len(groups) > 2 and groups[2] and groups[2].isdigit() else None
238
+ message = groups[3] if len(groups) > 3 else ""
239
+
240
+ # Clean up message
241
+ message = message.strip()[:200]
242
+
243
+ errors.append(VerificationError(
244
+ file=file_path,
245
+ line=line,
246
+ column=column,
247
+ message=message,
248
+ source=source,
249
+ ))
250
+ seen.add(file_path)
251
+
252
+ # Also check for test-specific failure patterns
253
+ if source == "test":
254
+ for pattern in cls.TEST_FAILURE_PATTERNS:
255
+ for match in pattern.finditer(output):
256
+ file_path = match.group(1)
257
+ if file_path and file_path not in seen:
258
+ errors.append(VerificationError(
259
+ file=file_path,
260
+ line=None,
261
+ message="Test failed",
262
+ source=source,
263
+ ))
264
+ seen.add(file_path)
265
+
266
+ return errors
267
+
268
+ @classmethod
269
+ def extract_test_count(cls, output: str) -> Optional[int]:
270
+ """Extract number of passed tests from output."""
271
+ patterns = [
272
+ # Jest/Vitest: Tests: 47 passed
273
+ re.compile(r"Tests?:\s*(\d+)\s*passed"),
274
+ # Pytest: 47 passed
275
+ re.compile(r"(\d+)\s*passed"),
276
+ # Cargo: test result: ok. 47 passed
277
+ re.compile(r"(\d+)\s*passed"),
278
+ ]
279
+ for pattern in patterns:
280
+ match = pattern.search(output)
281
+ if match:
282
+ return int(match.group(1))
283
+ return None
284
+
285
+
286
+ # =============================================================================
287
+ # Verification Runner
288
+ # =============================================================================
289
+
290
+ class VerificationRunner:
291
+ """Run verification suite with output capture and error parsing."""
292
+
293
+ def __init__(
294
+ self,
295
+ project_root: Optional[Path] = None,
296
+ config: Optional[VerificationConfig] = None,
297
+ ):
298
+ self.project_root = project_root or Path.cwd()
299
+ self.config = config or self._load_config()
300
+ self.project_type, self.default_commands = ProjectDetector.detect(self.project_root)
301
+
302
+ def _load_config(self) -> VerificationConfig:
303
+ """Load configuration from .claude/settings.yaml."""
304
+ config_path = self.project_root / ".claude" / "settings.yaml"
305
+
306
+ if not config_path.exists() or not HAS_YAML:
307
+ return VerificationConfig()
308
+
309
+ try:
310
+ with open(config_path) as f:
311
+ data = yaml.safe_load(f) or {}
312
+ verification_data = data.get("verification", {})
313
+ return VerificationConfig.from_dict(verification_data)
314
+ except Exception:
315
+ return VerificationConfig()
316
+
317
+ def _get_command(self, check_type: Literal["test", "lint", "types"]) -> Optional[str]:
318
+ """Get command for a check type, preferring config over defaults."""
319
+ if check_type == "test":
320
+ return self.config.test_command or self.default_commands.get("test")
321
+ elif check_type == "lint":
322
+ return self.config.lint_command or self.default_commands.get("lint")
323
+ elif check_type == "types":
324
+ return self.config.types_command or self.default_commands.get("types")
325
+ return None
326
+
327
+ def _run_command(
328
+ self,
329
+ command: str,
330
+ check_type: Literal["test", "lint", "types"],
331
+ ) -> CheckResult:
332
+ """Run a single verification command."""
333
+ start_time = time.time()
334
+
335
+ try:
336
+ result = subprocess.run(
337
+ command,
338
+ shell=True,
339
+ capture_output=True,
340
+ text=True,
341
+ timeout=self.config.timeout_seconds,
342
+ cwd=str(self.project_root),
343
+ )
344
+ output = result.stdout + "\n" + result.stderr
345
+ exit_code = result.returncode
346
+ passed = exit_code == 0
347
+
348
+ except subprocess.TimeoutExpired:
349
+ output = f"Command timed out after {self.config.timeout_seconds}s"
350
+ exit_code = -1
351
+ passed = False
352
+
353
+ except Exception as e:
354
+ output = f"Command failed: {str(e)}"
355
+ exit_code = -1
356
+ passed = False
357
+
358
+ duration_ms = int((time.time() - start_time) * 1000)
359
+
360
+ # Parse errors from output
361
+ errors = []
362
+ if not passed:
363
+ errors = ErrorParser.parse_errors(output, check_type)
364
+
365
+ # Extract test count if applicable
366
+ count = None
367
+ if check_type == "test" and passed:
368
+ count = ErrorParser.extract_test_count(output)
369
+
370
+ return CheckResult(
371
+ check_type=check_type,
372
+ passed=passed,
373
+ exit_code=exit_code,
374
+ duration_ms=duration_ms,
375
+ output=output,
376
+ errors=errors,
377
+ count=count,
378
+ )
379
+
380
+ def run_tests(self) -> Optional[CheckResult]:
381
+ """Run test suite."""
382
+ command = self._get_command("test")
383
+ if not command:
384
+ return None
385
+ return self._run_command(command, "test")
386
+
387
+ def run_lint(self) -> Optional[CheckResult]:
388
+ """Run linter."""
389
+ command = self._get_command("lint")
390
+ if not command:
391
+ return None
392
+ return self._run_command(command, "lint")
393
+
394
+ def run_types(self) -> Optional[CheckResult]:
395
+ """Run type checker."""
396
+ command = self._get_command("types")
397
+ if not command:
398
+ return None
399
+ return self._run_command(command, "types")
400
+
401
+ def run_all(
402
+ self,
403
+ skip_tests: bool = False,
404
+ skip_lint: bool = False,
405
+ skip_types: bool = False,
406
+ ) -> VerificationResult:
407
+ """Run all verification checks."""
408
+ start_time = time.time()
409
+
410
+ tests = None if skip_tests else self.run_tests()
411
+ lint = None if skip_lint else self.run_lint()
412
+ types = None if skip_types else self.run_types()
413
+
414
+ # Collect all errors
415
+ all_errors = []
416
+ if tests and not tests.passed:
417
+ all_errors.extend(tests.errors)
418
+ if lint and not lint.passed:
419
+ all_errors.extend(lint.errors)
420
+ if types and not types.passed:
421
+ all_errors.extend(types.errors)
422
+
423
+ # Determine overall pass/fail
424
+ passed = True
425
+ if tests and not tests.passed:
426
+ passed = False
427
+ if lint and not lint.passed:
428
+ passed = False
429
+ if types and not types.passed:
430
+ passed = False
431
+
432
+ total_duration_ms = int((time.time() - start_time) * 1000)
433
+
434
+ return VerificationResult(
435
+ passed=passed,
436
+ tests=tests,
437
+ lint=lint,
438
+ types=types,
439
+ total_duration_ms=total_duration_ms,
440
+ errors=all_errors,
441
+ )
442
+
443
+ def run_single(
444
+ self,
445
+ check_type: Literal["test", "lint", "types"]
446
+ ) -> Optional[CheckResult]:
447
+ """Run a single verification check."""
448
+ if check_type == "test":
449
+ return self.run_tests()
450
+ elif check_type == "lint":
451
+ return self.run_lint()
452
+ elif check_type == "types":
453
+ return self.run_types()
454
+ return None
455
+
456
+
457
+ # =============================================================================
458
+ # State Management
459
+ # =============================================================================
460
+
461
+ class VerificationState:
462
+ """Manage verification state persistence."""
463
+
464
+ STATE_FILE = ".claude/verification-state.json"
465
+
466
+ def __init__(self, project_root: Optional[Path] = None):
467
+ self.project_root = project_root or Path.cwd()
468
+ self.state_path = self.project_root / self.STATE_FILE
469
+
470
+ def save(self, result: VerificationResult, iteration: int = 0) -> None:
471
+ """Save verification state to file."""
472
+ from datetime import datetime
473
+
474
+ state = {
475
+ "last_run": datetime.now().isoformat(),
476
+ "status": "passed" if result.passed else "failed",
477
+ "iteration": iteration,
478
+ "results": result.to_dict(),
479
+ }
480
+
481
+ self.state_path.parent.mkdir(parents=True, exist_ok=True)
482
+ with open(self.state_path, "w") as f:
483
+ json.dump(state, f, indent=2)
484
+
485
+ def load(self) -> Optional[Dict[str, Any]]:
486
+ """Load verification state from file."""
487
+ if not self.state_path.exists():
488
+ return None
489
+
490
+ try:
491
+ with open(self.state_path) as f:
492
+ return json.load(f)
493
+ except Exception:
494
+ return None
495
+
496
+ def clear(self) -> None:
497
+ """Clear verification state."""
498
+ if self.state_path.exists():
499
+ self.state_path.unlink()
500
+
501
+
502
+ # =============================================================================
503
+ # CLI Interface
504
+ # =============================================================================
505
+
506
+ def main():
507
+ """CLI entry point for verification runner."""
508
+ import argparse
509
+
510
+ parser = argparse.ArgumentParser(description="Run verification suite")
511
+ parser.add_argument("--test-only", action="store_true", help="Run tests only")
512
+ parser.add_argument("--lint-only", action="store_true", help="Run lint only")
513
+ parser.add_argument("--types-only", action="store_true", help="Run types only")
514
+ parser.add_argument("--quick", action="store_true", help="Skip type checking")
515
+ parser.add_argument("--json", action="store_true", help="Output JSON")
516
+ parser.add_argument("--project", type=str, help="Project root path")
517
+
518
+ args = parser.parse_args()
519
+
520
+ project_root = Path(args.project) if args.project else Path.cwd()
521
+ runner = VerificationRunner(project_root=project_root)
522
+
523
+ if args.test_only:
524
+ result = runner.run_tests()
525
+ if result:
526
+ print(result.to_dict() if args.json else f"Tests: {'PASS' if result.passed else 'FAIL'}")
527
+ elif args.lint_only:
528
+ result = runner.run_lint()
529
+ if result:
530
+ print(result.to_dict() if args.json else f"Lint: {'PASS' if result.passed else 'FAIL'}")
531
+ elif args.types_only:
532
+ result = runner.run_types()
533
+ if result:
534
+ print(result.to_dict() if args.json else f"Types: {'PASS' if result.passed else 'FAIL'}")
535
+ else:
536
+ result = runner.run_all(skip_types=args.quick)
537
+ if args.json:
538
+ print(result.to_json())
539
+ else:
540
+ status = "PASS" if result.passed else "FAIL"
541
+ print(f"Verification: {status}")
542
+ if result.tests:
543
+ count_str = f" ({result.tests.count} passed)" if result.tests.count else ""
544
+ print(f" Tests: {'PASS' if result.tests.passed else 'FAIL'}{count_str}")
545
+ if result.lint:
546
+ print(f" Lint: {'PASS' if result.lint.passed else 'FAIL'}")
547
+ if result.types:
548
+ print(f" Types: {'PASS' if result.types.passed else 'FAIL'}")
549
+ if result.errors:
550
+ print(f"\nErrors ({len(result.errors)}):")
551
+ for err in result.errors[:10]:
552
+ loc = f"{err.file}:{err.line}" if err.line else err.file
553
+ print(f" - {loc}: {err.message[:80]}")
554
+
555
+
556
+ if __name__ == "__main__":
557
+ main()