ref-agents 1.0.0__py3-none-any.whl

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 (175) hide show
  1. ref_agents/__init__.py +9 -0
  2. ref_agents/api_keys.json.example +8 -0
  3. ref_agents/auth.py +129 -0
  4. ref_agents/codemap/..md +62 -0
  5. ref_agents/codemap/CODE_MAP.md +37 -0
  6. ref_agents/codemap/core.md +43 -0
  7. ref_agents/codemap/models.md +43 -0
  8. ref_agents/codemap/prompts.md +40 -0
  9. ref_agents/codemap/security.md +45 -0
  10. ref_agents/codemap/tools.md +94 -0
  11. ref_agents/codemap/tools_browser.md +44 -0
  12. ref_agents/codemap/utils.md +42 -0
  13. ref_agents/codemap/workflow.md +42 -0
  14. ref_agents/config/ai_patterns.yaml +101 -0
  15. ref_agents/config/frameworks/angular.yaml +104 -0
  16. ref_agents/config/frameworks/aspnet.yaml +84 -0
  17. ref_agents/config/frameworks/ef_core.yaml +81 -0
  18. ref_agents/config/frameworks/react.yaml +111 -0
  19. ref_agents/config/frameworks/spring_boot.yaml +117 -0
  20. ref_agents/config/languages/csharp.yaml +153 -0
  21. ref_agents/config/languages/java.yaml +188 -0
  22. ref_agents/config/languages/javascript.yaml +172 -0
  23. ref_agents/config/languages/python.yaml +153 -0
  24. ref_agents/config/languages/typescript.yaml +193 -0
  25. ref_agents/constants.py +553 -0
  26. ref_agents/core/__init__.py +15 -0
  27. ref_agents/core/config_loader.py +160 -0
  28. ref_agents/core/config_models.py +167 -0
  29. ref_agents/core/config_parsing.py +84 -0
  30. ref_agents/core/language_detector.py +388 -0
  31. ref_agents/core/validation_models.py +66 -0
  32. ref_agents/core/validation_primitives.py +176 -0
  33. ref_agents/errors.py +34 -0
  34. ref_agents/license_client.py +247 -0
  35. ref_agents/models/__init__.py +22 -0
  36. ref_agents/models/gherkin.py +45 -0
  37. ref_agents/models/hierarchy.py +80 -0
  38. ref_agents/models/invest.py +59 -0
  39. ref_agents/models/version.py +49 -0
  40. ref_agents/prompts/__init__.py +9 -0
  41. ref_agents/prompts/start_agent.py +772 -0
  42. ref_agents/rules/architecture/backend_patterns.md +43 -0
  43. ref_agents/rules/architecture/diagramming.md +100 -0
  44. ref_agents/rules/architecture/frontend_patterns.md +40 -0
  45. ref_agents/rules/architecture/impact_analysis.md +129 -0
  46. ref_agents/rules/architecture/migration_strategy.md +208 -0
  47. ref_agents/rules/architecture/regression_protocol.md +77 -0
  48. ref_agents/rules/architecture/system_design.md +97 -0
  49. ref_agents/rules/common/codemap_standard.md +97 -0
  50. ref_agents/rules/common/core_protocol.md +59 -0
  51. ref_agents/rules/common/prompt_engineering.md +294 -0
  52. ref_agents/rules/development/debugging.md +32 -0
  53. ref_agents/rules/development/implementation.md +205 -0
  54. ref_agents/rules/operations/completion.md +119 -0
  55. ref_agents/rules/operations/cutover_protocol.md +218 -0
  56. ref_agents/rules/operations/discovery.md +179 -0
  57. ref_agents/rules/operations/fix_workflow.md +87 -0
  58. ref_agents/rules/operations/forensics.md +278 -0
  59. ref_agents/rules/operations/platform.md +263 -0
  60. ref_agents/rules/operations/synchronous_flow.md +25 -0
  61. ref_agents/rules/product/ac_validation.md +25 -0
  62. ref_agents/rules/product/brainstorming.md +27 -0
  63. ref_agents/rules/product/ref_flow.md +101 -0
  64. ref_agents/rules/product/requirements_std.md +114 -0
  65. ref_agents/rules/product/spec_writing.md +235 -0
  66. ref_agents/rules/product/strategy.md +96 -0
  67. ref_agents/rules/quality/documentation_standards.md +46 -0
  68. ref_agents/rules/quality/parity_testing.md +234 -0
  69. ref_agents/rules/quality/project_documentation.md +56 -0
  70. ref_agents/rules/quality/qa_lead.md +111 -0
  71. ref_agents/rules/quality/test_design.md +146 -0
  72. ref_agents/rules/quality/testing_standards.md +293 -0
  73. ref_agents/rules/review/pr_review.md +116 -0
  74. ref_agents/rules/security/security_audit.md +83 -0
  75. ref_agents/security/__init__.py +22 -0
  76. ref_agents/security/dependency_audit.py +188 -0
  77. ref_agents/security/file_audit.py +208 -0
  78. ref_agents/security/network_scan.py +179 -0
  79. ref_agents/security/report_generator.py +313 -0
  80. ref_agents/security/secret_scan.py +252 -0
  81. ref_agents/security/url_scan.py +240 -0
  82. ref_agents/security_scan.py +236 -0
  83. ref_agents/server.py +1586 -0
  84. ref_agents/session.py +100 -0
  85. ref_agents/tool_names.py +55 -0
  86. ref_agents/tools/__init__.py +8 -0
  87. ref_agents/tools/agents_generator.py +315 -0
  88. ref_agents/tools/ai_pattern_detector.py +815 -0
  89. ref_agents/tools/brownfield_populator.py +529 -0
  90. ref_agents/tools/browser/__init__.py +50 -0
  91. ref_agents/tools/browser/evidence_verifier.py +302 -0
  92. ref_agents/tools/browser/execution_logger.py +249 -0
  93. ref_agents/tools/browser/playwright_mcp_client.py +259 -0
  94. ref_agents/tools/browser/screenshot_utils.py +184 -0
  95. ref_agents/tools/browser/test_executor.py +537 -0
  96. ref_agents/tools/code_quality_scanner.py +629 -0
  97. ref_agents/tools/codemap/..md +93 -0
  98. ref_agents/tools/codemap/CODE_MAP.md +30 -0
  99. ref_agents/tools/codemap/browser.md +44 -0
  100. ref_agents/tools/codemap.py +403 -0
  101. ref_agents/tools/codemap_freshness.py +234 -0
  102. ref_agents/tools/comment_smell_scanner.py +346 -0
  103. ref_agents/tools/complexity.py +436 -0
  104. ref_agents/tools/complexity_ast.py +333 -0
  105. ref_agents/tools/compliance.py +246 -0
  106. ref_agents/tools/compliance_remediation.py +846 -0
  107. ref_agents/tools/context_graph.py +839 -0
  108. ref_agents/tools/context_manager.py +550 -0
  109. ref_agents/tools/context_tools.py +121 -0
  110. ref_agents/tools/cross_repo_linker.py +393 -0
  111. ref_agents/tools/dead_code_scanner.py +637 -0
  112. ref_agents/tools/debt_scanner.py +1092 -0
  113. ref_agents/tools/dependency_graph.py +272 -0
  114. ref_agents/tools/discovery_audit.py +372 -0
  115. ref_agents/tools/docs_scanner.py +600 -0
  116. ref_agents/tools/evaluate_gate.py +119 -0
  117. ref_agents/tools/external_detector.py +524 -0
  118. ref_agents/tools/features_generator.py +282 -0
  119. ref_agents/tools/flow_gap_detector.py +373 -0
  120. ref_agents/tools/flow_mapper.py +327 -0
  121. ref_agents/tools/full_suite_runner.py +740 -0
  122. ref_agents/tools/gherkin_parser.py +227 -0
  123. ref_agents/tools/guard_tools.py +139 -0
  124. ref_agents/tools/handoff_tools.py +282 -0
  125. ref_agents/tools/health_scanner.py +1211 -0
  126. ref_agents/tools/hierarchy_manager.py +289 -0
  127. ref_agents/tools/invest_scorer.py +249 -0
  128. ref_agents/tools/jira_confluence_export.py +306 -0
  129. ref_agents/tools/json_output.py +76 -0
  130. ref_agents/tools/migration_mapper.py +946 -0
  131. ref_agents/tools/migration_readiness_scanner.py +209 -0
  132. ref_agents/tools/pattern_learner.py +522 -0
  133. ref_agents/tools/report_utils.py +155 -0
  134. ref_agents/tools/requirements_serializer.py +225 -0
  135. ref_agents/tools/security_audit_tool.py +106 -0
  136. ref_agents/tools/sequencing_engine.py +288 -0
  137. ref_agents/tools/summary_generator.py +275 -0
  138. ref_agents/tools/symbol_resolver.py +306 -0
  139. ref_agents/tools/symbol_smoke_runner.py +336 -0
  140. ref_agents/tools/test_plan_validator.py +189 -0
  141. ref_agents/tools/test_smell_walker.py +902 -0
  142. ref_agents/tools/tier1_fixer.py +502 -0
  143. ref_agents/tools/validators/__init__.py +419 -0
  144. ref_agents/tools/validators/architect.py +268 -0
  145. ref_agents/tools/validators/cutover_engineer.py +167 -0
  146. ref_agents/tools/validators/developer.py +180 -0
  147. ref_agents/tools/validators/discovery.py +150 -0
  148. ref_agents/tools/validators/forensic_engineer.py +191 -0
  149. ref_agents/tools/validators/impact_architect.py +181 -0
  150. ref_agents/tools/validators/migration_planner.py +166 -0
  151. ref_agents/tools/validators/parity_tester.py +155 -0
  152. ref_agents/tools/validators/platform_engineer.py +134 -0
  153. ref_agents/tools/validators/pr_reviewer.py +129 -0
  154. ref_agents/tools/validators/product_manager.py +291 -0
  155. ref_agents/tools/validators/qa_lead.py +172 -0
  156. ref_agents/tools/validators/scrum_master.py +212 -0
  157. ref_agents/tools/validators/security_owner.py +162 -0
  158. ref_agents/tools/validators/specifier.py +134 -0
  159. ref_agents/tools/validators/strategist.py +149 -0
  160. ref_agents/tools/validators/tester.py +121 -0
  161. ref_agents/tools/version_manager.py +202 -0
  162. ref_agents/tools/workflow_tools.py +1549 -0
  163. ref_agents/utils/__init__.py +21 -0
  164. ref_agents/utils/git_utils.py +351 -0
  165. ref_agents/utils/handoff_logger.py +368 -0
  166. ref_agents/utils/ignore_matcher.py +270 -0
  167. ref_agents/workflow/__init__.py +19 -0
  168. ref_agents/workflow/capabilities.py +328 -0
  169. ref_agents/workflow/state_machine.py +708 -0
  170. ref_agents/workflow/transitions.py +658 -0
  171. ref_agents-1.0.0.dist-info/METADATA +365 -0
  172. ref_agents-1.0.0.dist-info/RECORD +175 -0
  173. ref_agents-1.0.0.dist-info/WHEEL +4 -0
  174. ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
  175. ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
@@ -0,0 +1,502 @@
1
+ """Tier 1 Deterministic Fixer.
2
+
3
+ Executes ruff-based fixes on files flagged by health scan.
4
+ Ruff is idempotent, has its own test suite, and exits non-zero on failure.
5
+ No custom AST transforms — only ruff-delegated fixes to eliminate regression risk.
6
+
7
+ Supported rules (all ruff-native, idempotent):
8
+ E722 — bare-except
9
+ T201 — print statements
10
+ F401 — unused imports
11
+ I001 — import order (isort)
12
+ LOG015 — logging.* instead of structlog (flagged, not auto-fixed)
13
+
14
+ Usage:
15
+ from ref_agents.tools.tier1_fixer import run_tier1_fixes
16
+ result = run_tier1_fixes(violations, project_root)
17
+ """
18
+
19
+ import subprocess
20
+ import shutil
21
+ from dataclasses import dataclass, field
22
+ from pathlib import Path
23
+
24
+ import structlog
25
+
26
+ logger = structlog.get_logger(__name__)
27
+
28
+ # Ruff rule IDs that map to each REF rule.
29
+ # Only rules ruff can fix automatically (--fix flag).
30
+ RUFF_FIXABLE_RULES: dict[str, list[str]] = {
31
+ "bare_except": ["E722"],
32
+ "print_statement": ["T201"],
33
+ "unused_imports": ["F401"],
34
+ "import_organisation": ["I001"],
35
+ "fstring_in_logger": ["G004"],
36
+ }
37
+
38
+ # Rules ruff detects but cannot safely auto-fix.
39
+ # These stay in Tier 2 work order for developer agent.
40
+ RUFF_DETECT_ONLY: dict[str, list[str]] = {
41
+ "standard_logging": ["LOG015"],
42
+ "missing_type_hints": ["ANN001", "ANN201"],
43
+ }
44
+
45
+ # REF rules handled by Tier 1 fixer (subset of VIOLATION_CLASSIFICATION).
46
+ TIER1_RULES: frozenset[str] = frozenset(RUFF_FIXABLE_RULES.keys())
47
+
48
+
49
+ @dataclass
50
+ class FileFixResult:
51
+ """Result of running ruff --fix on a single file."""
52
+
53
+ file: str
54
+ rules_applied: list[str] = field(default_factory=list)
55
+ violations_fixed: int = 0
56
+ violations_remaining: int = 0
57
+ ruff_available: bool = True
58
+ error: str = ""
59
+ skipped: bool = False
60
+ skip_reason: str = ""
61
+
62
+
63
+ @dataclass
64
+ class Tier1FixResult:
65
+ """Aggregate result of Tier 1 fix run."""
66
+
67
+ files_processed: int = 0
68
+ files_fixed: int = 0
69
+ files_skipped: int = 0
70
+ total_fixed: int = 0
71
+ total_remaining: int = 0
72
+ ruff_available: bool = True
73
+ stale_report_root: str = "" # Non-empty when report paths don't match project_root
74
+ file_results: list[FileFixResult] = field(default_factory=list)
75
+ errors: list[str] = field(default_factory=list)
76
+
77
+
78
+ def _remap_path(file_path: Path, project_root: Path) -> Path | None:
79
+ """Try to remap a stale absolute path into project_root by matching the longest suffix.
80
+
81
+ Example:
82
+ file_path = /old/repo/src/foo/bar.py
83
+ project_root = /new/repo
84
+ candidate = /new/repo/src/foo/bar.py → returned if it exists
85
+
86
+ Args:
87
+ file_path: Absolute path that does not exist.
88
+ project_root: Current project root to search under.
89
+
90
+ Returns:
91
+ Remapped Path if found, None otherwise.
92
+ """
93
+ parts = file_path.parts
94
+ # Walk from longest suffix to shortest, stop at first match
95
+ for i in range(1, len(parts)):
96
+ candidate = project_root.joinpath(*parts[i:])
97
+ if candidate.exists():
98
+ return candidate
99
+ return None
100
+
101
+
102
+ def _detect_stale_root(paths: list[str], project_root: Path) -> str:
103
+ """Return the foreign root prefix if all absolute paths share a root outside project_root.
104
+
105
+ Args:
106
+ paths: File path strings from violations.
107
+ project_root: Current project root.
108
+
109
+ Returns:
110
+ Foreign root string (e.g. '/Users/x/sage/ref-agents') if detected, else ''.
111
+ """
112
+ abs_paths = [Path(p) for p in paths if Path(p).is_absolute()]
113
+ if not abs_paths:
114
+ return ""
115
+ # All paths outside project_root
116
+ outside = [p for p in abs_paths if not str(p).startswith(str(project_root))]
117
+ if len(outside) != len(abs_paths):
118
+ return ""
119
+ # Find common ancestor by walking parts of first path vs all others
120
+ try:
121
+ reference = outside[0].parts
122
+ common_depth = len(reference)
123
+ for p in outside[1:]:
124
+ for i, (a, b) in enumerate(zip(reference, p.parts)):
125
+ if a != b:
126
+ common_depth = min(common_depth, i)
127
+ break
128
+ else:
129
+ common_depth = min(common_depth, len(p.parts))
130
+ common_parts = reference[:common_depth]
131
+ return (
132
+ str(Path(common_parts[0]).joinpath(*common_parts[1:]))
133
+ if common_parts
134
+ else str(outside[0].parent)
135
+ )
136
+ except (IndexError, OSError, ValueError):
137
+ return str(outside[0].parent)
138
+
139
+
140
+ def _find_ruff(project_root: Path) -> str | None:
141
+ """Find ruff executable: venv first, then PATH.
142
+
143
+ Args:
144
+ project_root: Project root to check for venv.
145
+
146
+ Returns:
147
+ Path to ruff executable, or None if not found.
148
+ """
149
+ # Check common venv locations relative to project root
150
+ candidates = [
151
+ project_root / ".venv" / "bin" / "ruff",
152
+ project_root / "venv" / "bin" / "ruff",
153
+ project_root / ".venv" / "Scripts" / "ruff.exe", # Windows
154
+ ]
155
+ for candidate in candidates:
156
+ if candidate.exists():
157
+ return str(candidate)
158
+
159
+ # Fall back to PATH
160
+ return shutil.which("ruff")
161
+
162
+
163
+ def _get_ruff_rules_for_violations(violations: list[dict]) -> list[str]:
164
+ """Extract ruff rule IDs for a list of violations.
165
+
166
+ Args:
167
+ violations: List of violation dicts from remediation session.
168
+
169
+ Returns:
170
+ Deduplicated list of ruff rule IDs to apply.
171
+ """
172
+ rules: list[str] = []
173
+ for v in violations:
174
+ rule = v.get("rule", "")
175
+ if rule in RUFF_FIXABLE_RULES:
176
+ rules.extend(RUFF_FIXABLE_RULES[rule])
177
+ return list(dict.fromkeys(rules)) # deduplicate, preserve order
178
+
179
+
180
+ def _run_ruff_fix(
181
+ ruff: str,
182
+ file_path: Path,
183
+ rules: list[str],
184
+ ) -> tuple[int, str, str]:
185
+ """Run ruff --fix on a single file with specific rules.
186
+
187
+ Args:
188
+ ruff: Path to ruff executable.
189
+ file_path: File to fix.
190
+ rules: Ruff rule codes to apply.
191
+
192
+ Returns:
193
+ Tuple of (returncode, stdout, stderr).
194
+ """
195
+ select = ",".join(rules)
196
+ cmd = [ruff, "check", "--fix", "--select", select, str(file_path)]
197
+ try:
198
+ result = subprocess.run(
199
+ cmd,
200
+ capture_output=True,
201
+ text=True,
202
+ timeout=30,
203
+ )
204
+ return result.returncode, result.stdout, result.stderr
205
+ except subprocess.TimeoutExpired:
206
+ return 1, "", f"ruff timed out on {file_path}"
207
+ except OSError as e:
208
+ return 1, "", str(e)
209
+
210
+
211
+ def _run_ruff_check(
212
+ ruff: str,
213
+ file_path: Path,
214
+ rules: list[str],
215
+ ) -> int:
216
+ """Count remaining violations after fix.
217
+
218
+ Args:
219
+ ruff: Path to ruff executable.
220
+ file_path: File to check.
221
+ rules: Ruff rule codes to count.
222
+
223
+ Returns:
224
+ Number of remaining violations (0 = clean).
225
+ """
226
+ select = ",".join(rules)
227
+ cmd = [ruff, "check", "--select", select, str(file_path)]
228
+ try:
229
+ result = subprocess.run(
230
+ cmd,
231
+ capture_output=True,
232
+ text=True,
233
+ timeout=30,
234
+ )
235
+ if result.returncode == 0:
236
+ return 0
237
+ # Count lines that look like violations (contain file:line:col)
238
+ return sum(1 for line in result.stdout.splitlines() if ".py:" in line)
239
+ except (subprocess.TimeoutExpired, OSError):
240
+ return -1 # unknown
241
+
242
+
243
+ def fix_file(
244
+ ruff: str,
245
+ file_path: Path,
246
+ violations: list[dict],
247
+ project_root: Path | None = None,
248
+ ) -> FileFixResult:
249
+ """Apply Tier 1 ruff fixes to a single file.
250
+
251
+ Args:
252
+ ruff: Path to ruff executable.
253
+ file_path: File to fix.
254
+ violations: Violations for this file from remediation session.
255
+ project_root: Used to attempt path remapping when file_path is stale.
256
+
257
+ Returns:
258
+ FileFixResult with outcome details.
259
+ """
260
+ result = FileFixResult(file=str(file_path))
261
+
262
+ if not file_path.exists():
263
+ if project_root and file_path.is_absolute():
264
+ remapped = _remap_path(file_path, project_root)
265
+ if remapped:
266
+ logger.warning(
267
+ "stale_path_remapped",
268
+ original=str(file_path),
269
+ remapped=str(remapped),
270
+ )
271
+ file_path = remapped
272
+ result.file = str(file_path)
273
+ else:
274
+ result.skipped = True
275
+ parts = file_path.parts
276
+ origin = (
277
+ "/".join(parts[:5]) if len(parts) >= 4 else str(file_path.parent) # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
278
+ )
279
+ result.skip_reason = (
280
+ f"stale path: report generated in {origin}"
281
+ f", not found under {project_root}"
282
+ )
283
+ logger.warning(
284
+ "stale_path_not_remapped",
285
+ file=str(file_path),
286
+ project_root=str(project_root),
287
+ )
288
+ return result
289
+ else:
290
+ result.skipped = True
291
+ result.skip_reason = "File not found"
292
+ return result
293
+
294
+ if not file_path.suffix == ".py":
295
+ result.skipped = True
296
+ result.skip_reason = "Not a Python file (ruff fixes Python only)"
297
+ return result
298
+
299
+ rules = _get_ruff_rules_for_violations(violations)
300
+ if not rules:
301
+ result.skipped = True
302
+ result.skip_reason = "No ruff-fixable rules in violation set"
303
+ return result
304
+
305
+ result.rules_applied = rules
306
+
307
+ # Count violations before fix
308
+ before = _run_ruff_check(ruff, file_path, rules)
309
+
310
+ # Apply fix
311
+ returncode, stdout, stderr = _run_ruff_fix(ruff, file_path, rules)
312
+
313
+ if returncode not in (0, 1):
314
+ # 0 = no issues, 1 = issues found/fixed, anything else = error
315
+ result.error = stderr or stdout or f"ruff exited {returncode}"
316
+ logger.error("ruff_fix_failed", file=str(file_path), error=result.error)
317
+ return result
318
+
319
+ # Count violations after fix
320
+ after = _run_ruff_check(ruff, file_path, rules)
321
+ result.violations_fixed = max(0, before - after) if before >= 0 else 0
322
+ result.violations_remaining = max(0, after) if after >= 0 else 0
323
+
324
+ if result.violations_fixed > 0:
325
+ logger.info(
326
+ "tier1_fixed",
327
+ file=str(file_path),
328
+ fixed=result.violations_fixed,
329
+ remaining=result.violations_remaining,
330
+ rules=rules,
331
+ )
332
+
333
+ return result
334
+
335
+
336
+ def run_tier1_fixes(
337
+ violations: list[dict],
338
+ project_root: Path,
339
+ ) -> Tier1FixResult:
340
+ """Run Tier 1 ruff fixes on all files with pending Tier 1 violations.
341
+
342
+ Groups violations by file, applies ruff --fix per file with only the
343
+ rules relevant to that file's violations. Verifies fix by re-checking.
344
+
345
+ Args:
346
+ violations: List of violation dicts from remediation session (Tier 1, pending).
347
+ project_root: Project root for locating ruff in venv.
348
+
349
+ Returns:
350
+ Tier1FixResult with per-file outcomes and aggregate counts.
351
+ """
352
+ aggregate = Tier1FixResult()
353
+
354
+ ruff = _find_ruff(project_root)
355
+ if not ruff:
356
+ aggregate.ruff_available = False
357
+ aggregate.errors.append(
358
+ "ruff not found. Install with: pip install ruff or uv add ruff"
359
+ )
360
+ logger.error("ruff_not_found", project_root=str(project_root))
361
+ return aggregate
362
+
363
+ logger.info("ruff_found", path=ruff)
364
+
365
+ # Group violations by file
366
+ by_file: dict[str, list[dict]] = {}
367
+ for v in violations:
368
+ f = v.get("file", "")
369
+ if f:
370
+ by_file.setdefault(f, []).append(v)
371
+
372
+ aggregate.files_processed = len(by_file)
373
+
374
+ # Detect stale report before processing any files
375
+ stale_root = _detect_stale_root(list(by_file.keys()), project_root)
376
+ if stale_root:
377
+ aggregate.stale_report_root = stale_root
378
+ aggregate.errors.append(
379
+ f"Health report was generated in a different repo: {stale_root}. "
380
+ f"Current project root: {project_root}. "
381
+ 'Attempting path remapping — re-run `scan(target="health")` to fix permanently.'
382
+ )
383
+ logger.error(
384
+ "stale_health_report_detected",
385
+ report_root=stale_root,
386
+ project_root=str(project_root),
387
+ )
388
+
389
+ for file_str, file_violations in by_file.items():
390
+ file_path = Path(file_str)
391
+ # Resolve relative paths against project root
392
+ if not file_path.is_absolute():
393
+ file_path = project_root / file_path
394
+
395
+ file_result = fix_file(ruff, file_path, file_violations, project_root)
396
+ aggregate.file_results.append(file_result)
397
+
398
+ if file_result.skipped:
399
+ aggregate.files_skipped += 1
400
+ elif file_result.error:
401
+ aggregate.errors.append(f"{file_str}: {file_result.error}")
402
+ else:
403
+ if file_result.violations_fixed > 0:
404
+ aggregate.files_fixed += 1
405
+ aggregate.total_fixed += file_result.violations_fixed
406
+ aggregate.total_remaining += file_result.violations_remaining
407
+
408
+ logger.info(
409
+ "tier1_complete",
410
+ files_processed=aggregate.files_processed,
411
+ files_fixed=aggregate.files_fixed,
412
+ total_fixed=aggregate.total_fixed,
413
+ total_remaining=aggregate.total_remaining,
414
+ )
415
+
416
+ return aggregate
417
+
418
+
419
+ def format_tier1_result(result: Tier1FixResult, session_id: str) -> str:
420
+ """Format Tier 1 fix result as agent-readable summary.
421
+
422
+ Args:
423
+ result: Tier1FixResult from run_tier1_fixes.
424
+ session_id: Remediation session ID for follow-up calls.
425
+
426
+ Returns:
427
+ Formatted string for MCP tool response.
428
+ """
429
+ if not result.ruff_available:
430
+ return (
431
+ "❌ Tier 1 fixes could not run: ruff not found in project venv or PATH.\n"
432
+ "Install ruff: `uv add ruff` or `pip install ruff`\n"
433
+ 'Then retry: `remediate(action="execute", session_id="'
434
+ + session_id
435
+ + '", tier="tier1")`'
436
+ )
437
+
438
+ if result.stale_report_root:
439
+ stale_block = (
440
+ "⚠️ **Stale health report detected.**\n"
441
+ f" Report was generated in: `{result.stale_report_root}`\n"
442
+ " Path remapping attempted. Files that could not be remapped are listed under Skipped.\n"
443
+ ' Fix permanently: `scan(target="health", directory="<current_project>")`\n'
444
+ f' Then restart: `remediate(action="start", directory="<current_project>")`\n'
445
+ )
446
+ else:
447
+ stale_block = ""
448
+
449
+ lines = [
450
+ "## ⚡ Tier 1 Fix Complete",
451
+ f"Files processed: {result.files_processed} | "
452
+ f"Fixed: {result.files_fixed} | "
453
+ f"Violations resolved: {result.total_fixed} | "
454
+ f"Remaining: {result.total_remaining}",
455
+ "",
456
+ ]
457
+
458
+ if stale_block:
459
+ lines.append(stale_block)
460
+
461
+ if result.errors:
462
+ lines.append("### Errors")
463
+ for e in result.errors:
464
+ lines.append(f" - {e}")
465
+ lines.append("")
466
+
467
+ fixed_files = [r for r in result.file_results if r.violations_fixed > 0]
468
+ if fixed_files:
469
+ lines.append("### Fixed")
470
+ for r in fixed_files:
471
+ lines.append(
472
+ f" - `{r.file}` — {r.violations_fixed} fixed, "
473
+ f"{r.violations_remaining} remaining "
474
+ f"(rules: {', '.join(r.rules_applied)})"
475
+ )
476
+ lines.append("")
477
+
478
+ skipped = [r for r in result.file_results if r.skipped]
479
+ if skipped:
480
+ lines.append("### Skipped")
481
+ for r in skipped:
482
+ lines.append(f" - `{r.file}` — {r.skip_reason}")
483
+ lines.append("")
484
+
485
+ if result.total_remaining > 0:
486
+ lines.append(
487
+ f"⚠️ {result.total_remaining} violations remain after Tier 1. "
488
+ "These require manual developer review."
489
+ )
490
+ lines.append("")
491
+
492
+ lines.append("### Next Step")
493
+ if result.total_remaining == 0 and not result.errors:
494
+ lines.append(
495
+ f'Run Tier 2 approval: `remediate(action="approve", session_id="{session_id}")`'
496
+ )
497
+ else:
498
+ lines.append(
499
+ f'Check full state: `remediate(action="state", session_id="{session_id}")`'
500
+ )
501
+
502
+ return "\n".join(lines)