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.
- ref_agents/__init__.py +9 -0
- ref_agents/api_keys.json.example +8 -0
- ref_agents/auth.py +129 -0
- ref_agents/codemap/..md +62 -0
- ref_agents/codemap/CODE_MAP.md +37 -0
- ref_agents/codemap/core.md +43 -0
- ref_agents/codemap/models.md +43 -0
- ref_agents/codemap/prompts.md +40 -0
- ref_agents/codemap/security.md +45 -0
- ref_agents/codemap/tools.md +94 -0
- ref_agents/codemap/tools_browser.md +44 -0
- ref_agents/codemap/utils.md +42 -0
- ref_agents/codemap/workflow.md +42 -0
- ref_agents/config/ai_patterns.yaml +101 -0
- ref_agents/config/frameworks/angular.yaml +104 -0
- ref_agents/config/frameworks/aspnet.yaml +84 -0
- ref_agents/config/frameworks/ef_core.yaml +81 -0
- ref_agents/config/frameworks/react.yaml +111 -0
- ref_agents/config/frameworks/spring_boot.yaml +117 -0
- ref_agents/config/languages/csharp.yaml +153 -0
- ref_agents/config/languages/java.yaml +188 -0
- ref_agents/config/languages/javascript.yaml +172 -0
- ref_agents/config/languages/python.yaml +153 -0
- ref_agents/config/languages/typescript.yaml +193 -0
- ref_agents/constants.py +553 -0
- ref_agents/core/__init__.py +15 -0
- ref_agents/core/config_loader.py +160 -0
- ref_agents/core/config_models.py +167 -0
- ref_agents/core/config_parsing.py +84 -0
- ref_agents/core/language_detector.py +388 -0
- ref_agents/core/validation_models.py +66 -0
- ref_agents/core/validation_primitives.py +176 -0
- ref_agents/errors.py +34 -0
- ref_agents/license_client.py +247 -0
- ref_agents/models/__init__.py +22 -0
- ref_agents/models/gherkin.py +45 -0
- ref_agents/models/hierarchy.py +80 -0
- ref_agents/models/invest.py +59 -0
- ref_agents/models/version.py +49 -0
- ref_agents/prompts/__init__.py +9 -0
- ref_agents/prompts/start_agent.py +772 -0
- ref_agents/rules/architecture/backend_patterns.md +43 -0
- ref_agents/rules/architecture/diagramming.md +100 -0
- ref_agents/rules/architecture/frontend_patterns.md +40 -0
- ref_agents/rules/architecture/impact_analysis.md +129 -0
- ref_agents/rules/architecture/migration_strategy.md +208 -0
- ref_agents/rules/architecture/regression_protocol.md +77 -0
- ref_agents/rules/architecture/system_design.md +97 -0
- ref_agents/rules/common/codemap_standard.md +97 -0
- ref_agents/rules/common/core_protocol.md +59 -0
- ref_agents/rules/common/prompt_engineering.md +294 -0
- ref_agents/rules/development/debugging.md +32 -0
- ref_agents/rules/development/implementation.md +205 -0
- ref_agents/rules/operations/completion.md +119 -0
- ref_agents/rules/operations/cutover_protocol.md +218 -0
- ref_agents/rules/operations/discovery.md +179 -0
- ref_agents/rules/operations/fix_workflow.md +87 -0
- ref_agents/rules/operations/forensics.md +278 -0
- ref_agents/rules/operations/platform.md +263 -0
- ref_agents/rules/operations/synchronous_flow.md +25 -0
- ref_agents/rules/product/ac_validation.md +25 -0
- ref_agents/rules/product/brainstorming.md +27 -0
- ref_agents/rules/product/ref_flow.md +101 -0
- ref_agents/rules/product/requirements_std.md +114 -0
- ref_agents/rules/product/spec_writing.md +235 -0
- ref_agents/rules/product/strategy.md +96 -0
- ref_agents/rules/quality/documentation_standards.md +46 -0
- ref_agents/rules/quality/parity_testing.md +234 -0
- ref_agents/rules/quality/project_documentation.md +56 -0
- ref_agents/rules/quality/qa_lead.md +111 -0
- ref_agents/rules/quality/test_design.md +146 -0
- ref_agents/rules/quality/testing_standards.md +293 -0
- ref_agents/rules/review/pr_review.md +116 -0
- ref_agents/rules/security/security_audit.md +83 -0
- ref_agents/security/__init__.py +22 -0
- ref_agents/security/dependency_audit.py +188 -0
- ref_agents/security/file_audit.py +208 -0
- ref_agents/security/network_scan.py +179 -0
- ref_agents/security/report_generator.py +313 -0
- ref_agents/security/secret_scan.py +252 -0
- ref_agents/security/url_scan.py +240 -0
- ref_agents/security_scan.py +236 -0
- ref_agents/server.py +1586 -0
- ref_agents/session.py +100 -0
- ref_agents/tool_names.py +55 -0
- ref_agents/tools/__init__.py +8 -0
- ref_agents/tools/agents_generator.py +315 -0
- ref_agents/tools/ai_pattern_detector.py +815 -0
- ref_agents/tools/brownfield_populator.py +529 -0
- ref_agents/tools/browser/__init__.py +50 -0
- ref_agents/tools/browser/evidence_verifier.py +302 -0
- ref_agents/tools/browser/execution_logger.py +249 -0
- ref_agents/tools/browser/playwright_mcp_client.py +259 -0
- ref_agents/tools/browser/screenshot_utils.py +184 -0
- ref_agents/tools/browser/test_executor.py +537 -0
- ref_agents/tools/code_quality_scanner.py +629 -0
- ref_agents/tools/codemap/..md +93 -0
- ref_agents/tools/codemap/CODE_MAP.md +30 -0
- ref_agents/tools/codemap/browser.md +44 -0
- ref_agents/tools/codemap.py +403 -0
- ref_agents/tools/codemap_freshness.py +234 -0
- ref_agents/tools/comment_smell_scanner.py +346 -0
- ref_agents/tools/complexity.py +436 -0
- ref_agents/tools/complexity_ast.py +333 -0
- ref_agents/tools/compliance.py +246 -0
- ref_agents/tools/compliance_remediation.py +846 -0
- ref_agents/tools/context_graph.py +839 -0
- ref_agents/tools/context_manager.py +550 -0
- ref_agents/tools/context_tools.py +121 -0
- ref_agents/tools/cross_repo_linker.py +393 -0
- ref_agents/tools/dead_code_scanner.py +637 -0
- ref_agents/tools/debt_scanner.py +1092 -0
- ref_agents/tools/dependency_graph.py +272 -0
- ref_agents/tools/discovery_audit.py +372 -0
- ref_agents/tools/docs_scanner.py +600 -0
- ref_agents/tools/evaluate_gate.py +119 -0
- ref_agents/tools/external_detector.py +524 -0
- ref_agents/tools/features_generator.py +282 -0
- ref_agents/tools/flow_gap_detector.py +373 -0
- ref_agents/tools/flow_mapper.py +327 -0
- ref_agents/tools/full_suite_runner.py +740 -0
- ref_agents/tools/gherkin_parser.py +227 -0
- ref_agents/tools/guard_tools.py +139 -0
- ref_agents/tools/handoff_tools.py +282 -0
- ref_agents/tools/health_scanner.py +1211 -0
- ref_agents/tools/hierarchy_manager.py +289 -0
- ref_agents/tools/invest_scorer.py +249 -0
- ref_agents/tools/jira_confluence_export.py +306 -0
- ref_agents/tools/json_output.py +76 -0
- ref_agents/tools/migration_mapper.py +946 -0
- ref_agents/tools/migration_readiness_scanner.py +209 -0
- ref_agents/tools/pattern_learner.py +522 -0
- ref_agents/tools/report_utils.py +155 -0
- ref_agents/tools/requirements_serializer.py +225 -0
- ref_agents/tools/security_audit_tool.py +106 -0
- ref_agents/tools/sequencing_engine.py +288 -0
- ref_agents/tools/summary_generator.py +275 -0
- ref_agents/tools/symbol_resolver.py +306 -0
- ref_agents/tools/symbol_smoke_runner.py +336 -0
- ref_agents/tools/test_plan_validator.py +189 -0
- ref_agents/tools/test_smell_walker.py +902 -0
- ref_agents/tools/tier1_fixer.py +502 -0
- ref_agents/tools/validators/__init__.py +419 -0
- ref_agents/tools/validators/architect.py +268 -0
- ref_agents/tools/validators/cutover_engineer.py +167 -0
- ref_agents/tools/validators/developer.py +180 -0
- ref_agents/tools/validators/discovery.py +150 -0
- ref_agents/tools/validators/forensic_engineer.py +191 -0
- ref_agents/tools/validators/impact_architect.py +181 -0
- ref_agents/tools/validators/migration_planner.py +166 -0
- ref_agents/tools/validators/parity_tester.py +155 -0
- ref_agents/tools/validators/platform_engineer.py +134 -0
- ref_agents/tools/validators/pr_reviewer.py +129 -0
- ref_agents/tools/validators/product_manager.py +291 -0
- ref_agents/tools/validators/qa_lead.py +172 -0
- ref_agents/tools/validators/scrum_master.py +212 -0
- ref_agents/tools/validators/security_owner.py +162 -0
- ref_agents/tools/validators/specifier.py +134 -0
- ref_agents/tools/validators/strategist.py +149 -0
- ref_agents/tools/validators/tester.py +121 -0
- ref_agents/tools/version_manager.py +202 -0
- ref_agents/tools/workflow_tools.py +1549 -0
- ref_agents/utils/__init__.py +21 -0
- ref_agents/utils/git_utils.py +351 -0
- ref_agents/utils/handoff_logger.py +368 -0
- ref_agents/utils/ignore_matcher.py +270 -0
- ref_agents/workflow/__init__.py +19 -0
- ref_agents/workflow/capabilities.py +328 -0
- ref_agents/workflow/state_machine.py +708 -0
- ref_agents/workflow/transitions.py +658 -0
- ref_agents-1.0.0.dist-info/METADATA +365 -0
- ref_agents-1.0.0.dist-info/RECORD +175 -0
- ref_agents-1.0.0.dist-info/WHEEL +4 -0
- ref_agents-1.0.0.dist-info/entry_points.txt +2 -0
- ref_agents-1.0.0.dist-info/licenses/LICENSE +115 -0
|
@@ -0,0 +1,846 @@
|
|
|
1
|
+
"""Compliance Remediation Workflow.
|
|
2
|
+
|
|
3
|
+
Autonomous compliance fix pipeline with three-tier classification.
|
|
4
|
+
|
|
5
|
+
Tier 1: ruff-delegated fixes. Executed directly by MCP server. No LLM required.
|
|
6
|
+
Tier 2: Single approval gate. LLM agent executes with PR Reviewer gate.
|
|
7
|
+
Tier 3: Always HALT. Architectural/security decisions require user.
|
|
8
|
+
|
|
9
|
+
Design principles:
|
|
10
|
+
- start_remediation() reads health_report.json from disk. Never parses summary strings.
|
|
11
|
+
- execute_remediation(tier1) runs ruff fixes directly and returns verified results.
|
|
12
|
+
- execute_remediation(tier2) returns structured work order for developer/architect agent.
|
|
13
|
+
- All state persisted to .ref_cache/remediation/ as JSON.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import uuid
|
|
19
|
+
from dataclasses import asdict, dataclass, field
|
|
20
|
+
from datetime import datetime
|
|
21
|
+
from enum import Enum
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Literal
|
|
24
|
+
|
|
25
|
+
import structlog
|
|
26
|
+
|
|
27
|
+
from ref_agents.constants import Status
|
|
28
|
+
from ref_agents.utils.git_utils import get_ref_cache_dir
|
|
29
|
+
|
|
30
|
+
logger = structlog.get_logger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class RemediationTier(str, Enum):
|
|
34
|
+
TIER1 = "tier1"
|
|
35
|
+
TIER2 = "tier2"
|
|
36
|
+
TIER3 = "tier3"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ViolationStatus(str, Enum):
|
|
40
|
+
PENDING = "pending"
|
|
41
|
+
AUTO_FIXED = "auto_fixed"
|
|
42
|
+
AWAITING_APPROVAL = "awaiting_approval"
|
|
43
|
+
APPROVED = "approved"
|
|
44
|
+
DEFERRED = "deferred"
|
|
45
|
+
FAILED = "failed"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class RemediationStatus(str, Enum):
|
|
49
|
+
INITIALISED = "initialised"
|
|
50
|
+
TIER1_RUNNING = "tier1_running"
|
|
51
|
+
TIER1_COMPLETE = "tier1_complete"
|
|
52
|
+
TIER2_AWAITING_APPROVAL = "tier2_awaiting_approval"
|
|
53
|
+
TIER2_RUNNING = "tier2_running"
|
|
54
|
+
TIER2_COMPLETE = "tier2_complete"
|
|
55
|
+
TIER3_HALTED = "tier3_halted"
|
|
56
|
+
COMPLETE = "complete"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# Rule ID -> (tier, responsible_agent, fix_description)
|
|
60
|
+
# Tier 1: ruff-fixable only. No custom AST transforms.
|
|
61
|
+
VIOLATION_CLASSIFICATION: dict[str, tuple[RemediationTier, str, str]] = {
|
|
62
|
+
"bare_except": (
|
|
63
|
+
RemediationTier.TIER1,
|
|
64
|
+
"developer",
|
|
65
|
+
"ruff E722: replace bare except with specific exception",
|
|
66
|
+
),
|
|
67
|
+
"print_statement": (
|
|
68
|
+
RemediationTier.TIER1,
|
|
69
|
+
"developer",
|
|
70
|
+
"ruff T201: remove print() statement",
|
|
71
|
+
),
|
|
72
|
+
"unused_imports": (
|
|
73
|
+
RemediationTier.TIER1,
|
|
74
|
+
"developer",
|
|
75
|
+
"ruff F401: remove unused imports",
|
|
76
|
+
),
|
|
77
|
+
"import_organisation": (
|
|
78
|
+
RemediationTier.TIER1,
|
|
79
|
+
"developer",
|
|
80
|
+
"ruff I001: reorder imports stdlib > third-party > local",
|
|
81
|
+
),
|
|
82
|
+
"fstring_in_logger": (
|
|
83
|
+
RemediationTier.TIER1,
|
|
84
|
+
"developer",
|
|
85
|
+
"ruff G004: replace f-string in logger with structured args",
|
|
86
|
+
),
|
|
87
|
+
"missing_type_hints": (
|
|
88
|
+
RemediationTier.TIER2,
|
|
89
|
+
"developer",
|
|
90
|
+
"Add type hints to function signatures",
|
|
91
|
+
),
|
|
92
|
+
"missing_docstring": (
|
|
93
|
+
RemediationTier.TIER2,
|
|
94
|
+
"developer",
|
|
95
|
+
"Add Google-style docstring with Args/Returns sections",
|
|
96
|
+
),
|
|
97
|
+
"standard_logging": (
|
|
98
|
+
RemediationTier.TIER2,
|
|
99
|
+
"developer",
|
|
100
|
+
"Replace logging.* with structlog",
|
|
101
|
+
),
|
|
102
|
+
"complexity_violation": (
|
|
103
|
+
RemediationTier.TIER2,
|
|
104
|
+
"architect",
|
|
105
|
+
"Refactor function to reduce cyclomatic complexity below 15",
|
|
106
|
+
),
|
|
107
|
+
"dead_code": (RemediationTier.TIER2, "developer", "Remove confirmed dead code"),
|
|
108
|
+
"ai_pattern": (RemediationTier.TIER2, "developer", "Refactor AI anti-pattern"),
|
|
109
|
+
"missing_zod_validation": (
|
|
110
|
+
RemediationTier.TIER2,
|
|
111
|
+
"developer",
|
|
112
|
+
"Add Zod schema for external input validation",
|
|
113
|
+
),
|
|
114
|
+
"missing_test_coverage": (
|
|
115
|
+
RemediationTier.TIER2,
|
|
116
|
+
"tester",
|
|
117
|
+
"Write tests to bring coverage above 85%",
|
|
118
|
+
),
|
|
119
|
+
"docstring_fluff": (
|
|
120
|
+
RemediationTier.TIER2,
|
|
121
|
+
"developer",
|
|
122
|
+
"Remove AI fluff phrases from docstring",
|
|
123
|
+
),
|
|
124
|
+
"security_violation": (
|
|
125
|
+
RemediationTier.TIER3,
|
|
126
|
+
"security_owner",
|
|
127
|
+
"Security violation — HALT, user decision required",
|
|
128
|
+
),
|
|
129
|
+
"complexity_critical": (
|
|
130
|
+
RemediationTier.TIER3,
|
|
131
|
+
"architect",
|
|
132
|
+
"Complexity > 30 — full redesign required, not refactor",
|
|
133
|
+
),
|
|
134
|
+
"regression_risk_high": (
|
|
135
|
+
RemediationTier.TIER3,
|
|
136
|
+
"architect",
|
|
137
|
+
"High regression risk — architectural review required",
|
|
138
|
+
),
|
|
139
|
+
"integration_boundary": (
|
|
140
|
+
RemediationTier.TIER3,
|
|
141
|
+
"architect",
|
|
142
|
+
"Touches external integration boundary",
|
|
143
|
+
),
|
|
144
|
+
"cross_module_dependency": (
|
|
145
|
+
RemediationTier.TIER3,
|
|
146
|
+
"architect",
|
|
147
|
+
"Fix requires changes across module boundaries",
|
|
148
|
+
),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Scanner rule codes -> REF rule IDs
|
|
152
|
+
SCANNER_RULE_MAP: dict[str, str] = {
|
|
153
|
+
"E001": "bare_except",
|
|
154
|
+
"E002": "bare_except",
|
|
155
|
+
"T001": "missing_type_hints",
|
|
156
|
+
"D001": "missing_docstring",
|
|
157
|
+
"D002": "missing_docstring",
|
|
158
|
+
"D003": "missing_docstring",
|
|
159
|
+
"D004": "missing_docstring",
|
|
160
|
+
"L001": "print_statement",
|
|
161
|
+
"L002": "standard_logging",
|
|
162
|
+
"L003": "fstring_in_logger",
|
|
163
|
+
"I001": "import_organisation",
|
|
164
|
+
"F401": "unused_imports",
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
COMPLEXITY_TIER3_THRESHOLD = 30
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass
|
|
171
|
+
class Violation:
|
|
172
|
+
id: str
|
|
173
|
+
rule: str
|
|
174
|
+
file: str
|
|
175
|
+
line: int
|
|
176
|
+
message: str
|
|
177
|
+
severity: str
|
|
178
|
+
tier: RemediationTier
|
|
179
|
+
responsible_agent: str
|
|
180
|
+
fix_description: str
|
|
181
|
+
scanner: str = ""
|
|
182
|
+
status: ViolationStatus = ViolationStatus.PENDING
|
|
183
|
+
fix_notes: str = ""
|
|
184
|
+
|
|
185
|
+
def to_dict(self) -> dict:
|
|
186
|
+
return {
|
|
187
|
+
**asdict(self),
|
|
188
|
+
"tier": self.tier.value,
|
|
189
|
+
"status": self.status.value,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@dataclass
|
|
194
|
+
class Tier2Batch:
|
|
195
|
+
id: str
|
|
196
|
+
violations: list[str] = field(default_factory=list)
|
|
197
|
+
summary: str = ""
|
|
198
|
+
approved: bool = False
|
|
199
|
+
approved_at: str = ""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class RemediationSession:
|
|
204
|
+
session_id: str
|
|
205
|
+
story_id: str
|
|
206
|
+
directory: str
|
|
207
|
+
status: RemediationStatus = RemediationStatus.INITIALISED
|
|
208
|
+
violations: dict[str, dict] = field(default_factory=dict)
|
|
209
|
+
tier2_batch: dict | None = None
|
|
210
|
+
tier3_violations: list[str] = field(default_factory=list)
|
|
211
|
+
created_at: str = ""
|
|
212
|
+
updated_at: str = ""
|
|
213
|
+
stats: dict = field(default_factory=dict)
|
|
214
|
+
|
|
215
|
+
def __post_init__(self) -> None:
|
|
216
|
+
now = datetime.now().isoformat()
|
|
217
|
+
if not self.created_at:
|
|
218
|
+
self.created_at = now
|
|
219
|
+
self.updated_at = now
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _session_path(session_id: str) -> Path:
|
|
223
|
+
cache = get_ref_cache_dir() / "remediation"
|
|
224
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
225
|
+
return cache / f"{session_id}.json"
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _active_session_path() -> Path:
|
|
229
|
+
cache = get_ref_cache_dir() / "remediation"
|
|
230
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
return cache / "active.json"
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _save_session(session: RemediationSession) -> None:
|
|
235
|
+
session.updated_at = datetime.now().isoformat()
|
|
236
|
+
data = asdict(session)
|
|
237
|
+
data["status"] = session.status.value
|
|
238
|
+
_session_path(session.session_id).write_text(
|
|
239
|
+
json.dumps(data, indent=2), encoding="utf-8"
|
|
240
|
+
)
|
|
241
|
+
_active_session_path().write_text(
|
|
242
|
+
json.dumps({"session_id": session.session_id, "story_id": session.story_id}),
|
|
243
|
+
encoding="utf-8",
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _load_session(session_id: str) -> RemediationSession | None:
|
|
248
|
+
path = _session_path(session_id)
|
|
249
|
+
if not path.exists():
|
|
250
|
+
return None
|
|
251
|
+
try:
|
|
252
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
253
|
+
data["status"] = RemediationStatus(data["status"])
|
|
254
|
+
return RemediationSession(**data)
|
|
255
|
+
except (json.JSONDecodeError, KeyError, ValueError) as e:
|
|
256
|
+
logger.error("session_load_failed", session_id=session_id, error=str(e))
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _load_active_session() -> RemediationSession | None:
|
|
261
|
+
path = _active_session_path()
|
|
262
|
+
if not path.exists():
|
|
263
|
+
return None
|
|
264
|
+
try:
|
|
265
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
266
|
+
return _load_session(data["session_id"])
|
|
267
|
+
except (json.JSONDecodeError, KeyError):
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _find_health_report_json(directory: str) -> Path | None:
|
|
272
|
+
"""Locate health_report.json for the given project directory."""
|
|
273
|
+
candidates = [
|
|
274
|
+
Path(directory) / "ref-reports" / "platform_engineer" / "health_report.json",
|
|
275
|
+
]
|
|
276
|
+
try:
|
|
277
|
+
from ref_agents.session import SessionManager
|
|
278
|
+
|
|
279
|
+
root = SessionManager.get().project_root
|
|
280
|
+
if root:
|
|
281
|
+
candidates.append(
|
|
282
|
+
root / "ref-reports" / "platform_engineer" / "health_report.json"
|
|
283
|
+
)
|
|
284
|
+
except (ImportError, AttributeError):
|
|
285
|
+
pass
|
|
286
|
+
for path in candidates:
|
|
287
|
+
if path.exists():
|
|
288
|
+
logger.info("health_report_found", path=str(path))
|
|
289
|
+
return path
|
|
290
|
+
logger.warning("health_report_not_found", directory=directory)
|
|
291
|
+
return None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _load_violations_from_json(report_path: Path) -> list[dict]:
|
|
295
|
+
"""Load structured findings from health_report.json."""
|
|
296
|
+
try:
|
|
297
|
+
data = json.loads(report_path.read_text(encoding="utf-8"))
|
|
298
|
+
findings = data.get("findings", [])
|
|
299
|
+
logger.info("findings_loaded", count=len(findings))
|
|
300
|
+
return findings
|
|
301
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
302
|
+
logger.error("health_report_read_failed", path=str(report_path), error=str(e))
|
|
303
|
+
return []
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _classify_finding(finding: dict) -> tuple[RemediationTier, str, str]:
|
|
307
|
+
"""Classify a health finding into a remediation tier."""
|
|
308
|
+
rule_code = finding.get("rule", "")
|
|
309
|
+
severity = finding.get("severity", "medium").lower()
|
|
310
|
+
scanner = finding.get("scanner", "")
|
|
311
|
+
message = finding.get("message", "").lower()
|
|
312
|
+
|
|
313
|
+
# Direct rule code mapping
|
|
314
|
+
ref_rule = SCANNER_RULE_MAP.get(rule_code)
|
|
315
|
+
if ref_rule and ref_rule in VIOLATION_CLASSIFICATION:
|
|
316
|
+
return VIOLATION_CLASSIFICATION[ref_rule]
|
|
317
|
+
|
|
318
|
+
# Complexity scanner
|
|
319
|
+
if scanner == "complexity":
|
|
320
|
+
score_match = re.search(r"complexity\s+(\d+)", message)
|
|
321
|
+
if score_match and int(score_match.group(1)) > COMPLEXITY_TIER3_THRESHOLD:
|
|
322
|
+
return VIOLATION_CLASSIFICATION["complexity_critical"]
|
|
323
|
+
return VIOLATION_CLASSIFICATION["complexity_violation"]
|
|
324
|
+
|
|
325
|
+
# Message heuristics
|
|
326
|
+
if "bare except" in message:
|
|
327
|
+
return VIOLATION_CLASSIFICATION["bare_except"]
|
|
328
|
+
if "print(" in message:
|
|
329
|
+
return VIOLATION_CLASSIFICATION["print_statement"]
|
|
330
|
+
if "unused import" in message:
|
|
331
|
+
return VIOLATION_CLASSIFICATION["unused_imports"]
|
|
332
|
+
if "import order" in message:
|
|
333
|
+
return VIOLATION_CLASSIFICATION["import_organisation"]
|
|
334
|
+
if "f-string" in message and "log" in message:
|
|
335
|
+
return VIOLATION_CLASSIFICATION["fstring_in_logger"]
|
|
336
|
+
if "type hint" in message or "return type" in message:
|
|
337
|
+
return VIOLATION_CLASSIFICATION["missing_type_hints"]
|
|
338
|
+
if "docstring" in message:
|
|
339
|
+
return VIOLATION_CLASSIFICATION["missing_docstring"]
|
|
340
|
+
if "security" in message or "vulnerabilit" in message:
|
|
341
|
+
return VIOLATION_CLASSIFICATION["security_violation"]
|
|
342
|
+
if "dead code" in message:
|
|
343
|
+
return VIOLATION_CLASSIFICATION["dead_code"]
|
|
344
|
+
|
|
345
|
+
# Severity fallback
|
|
346
|
+
if severity == "critical":
|
|
347
|
+
return (
|
|
348
|
+
RemediationTier.TIER3,
|
|
349
|
+
"architect",
|
|
350
|
+
"Critical violation — manual review required",
|
|
351
|
+
)
|
|
352
|
+
if severity == "high":
|
|
353
|
+
return (
|
|
354
|
+
RemediationTier.TIER2,
|
|
355
|
+
"developer",
|
|
356
|
+
"High severity violation — review before fixing",
|
|
357
|
+
)
|
|
358
|
+
return (
|
|
359
|
+
RemediationTier.TIER1,
|
|
360
|
+
"developer",
|
|
361
|
+
"Apply standard fix per REF quality protocol",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _get_ref_rule_id(finding: dict) -> str:
|
|
366
|
+
rule_code = finding.get("rule", "")
|
|
367
|
+
return SCANNER_RULE_MAP.get(rule_code, rule_code or "unknown")
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def start_remediation(
|
|
371
|
+
story_id: str,
|
|
372
|
+
directory: str,
|
|
373
|
+
scan_output: str = "",
|
|
374
|
+
) -> str:
|
|
375
|
+
"""Initialise a remediation session from health_report.json.
|
|
376
|
+
|
|
377
|
+
Reads violations from the JSON report on disk. Does not parse
|
|
378
|
+
the scan summary string.
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
story_id: Active story identifier.
|
|
382
|
+
directory: Project directory that was scanned.
|
|
383
|
+
scan_output: Ignored. Kept for API compatibility.
|
|
384
|
+
|
|
385
|
+
Returns:
|
|
386
|
+
Remediation plan with tier breakdown and session ID.
|
|
387
|
+
"""
|
|
388
|
+
session_id = f"REM-{str(uuid.uuid4())[:8].upper()}"
|
|
389
|
+
|
|
390
|
+
report_path = _find_health_report_json(directory)
|
|
391
|
+
if not report_path:
|
|
392
|
+
return (
|
|
393
|
+
f"{Status.ERROR} health_report.json not found for: {directory}\n"
|
|
394
|
+
f'Run `scan(target="health", directory="{directory}")` first.'
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
raw_findings = _load_violations_from_json(report_path)
|
|
398
|
+
if not raw_findings:
|
|
399
|
+
return (
|
|
400
|
+
f"{Status.WARNING} No findings in health_report.json. Nothing to remediate."
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
classified: dict[str, dict] = {}
|
|
404
|
+
tier1_ids: list[str] = []
|
|
405
|
+
tier2_ids: list[str] = []
|
|
406
|
+
tier3_ids: list[str] = []
|
|
407
|
+
|
|
408
|
+
for finding in raw_findings:
|
|
409
|
+
tier, agent, fix_desc = _classify_finding(finding)
|
|
410
|
+
ref_rule = _get_ref_rule_id(finding)
|
|
411
|
+
vid = f"V-{str(uuid.uuid4())[:6].upper()}"
|
|
412
|
+
v = Violation(
|
|
413
|
+
id=vid,
|
|
414
|
+
rule=ref_rule,
|
|
415
|
+
file=finding.get("file", ""),
|
|
416
|
+
line=finding.get("line", 0),
|
|
417
|
+
message=finding.get("message", ""),
|
|
418
|
+
severity=finding.get("severity", "medium"),
|
|
419
|
+
tier=tier,
|
|
420
|
+
responsible_agent=agent,
|
|
421
|
+
fix_description=fix_desc,
|
|
422
|
+
scanner=finding.get("scanner", ""),
|
|
423
|
+
)
|
|
424
|
+
classified[vid] = v.to_dict()
|
|
425
|
+
if tier == RemediationTier.TIER1:
|
|
426
|
+
tier1_ids.append(vid)
|
|
427
|
+
elif tier == RemediationTier.TIER2:
|
|
428
|
+
tier2_ids.append(vid)
|
|
429
|
+
else:
|
|
430
|
+
tier3_ids.append(vid)
|
|
431
|
+
|
|
432
|
+
tier2_batch = None
|
|
433
|
+
if tier2_ids:
|
|
434
|
+
fix_counts: dict[str, int] = {}
|
|
435
|
+
for vid in tier2_ids:
|
|
436
|
+
desc = classified[vid]["fix_description"]
|
|
437
|
+
fix_counts[desc] = fix_counts.get(desc, 0) + 1
|
|
438
|
+
summary_lines = [f"- {count}x {desc}" for desc, count in fix_counts.items()]
|
|
439
|
+
tier2_batch = Tier2Batch(
|
|
440
|
+
id=f"BATCH-{str(uuid.uuid4())[:6].upper()}",
|
|
441
|
+
violations=tier2_ids,
|
|
442
|
+
summary="\n".join(summary_lines),
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
session = RemediationSession(
|
|
446
|
+
session_id=session_id,
|
|
447
|
+
story_id=story_id,
|
|
448
|
+
directory=directory,
|
|
449
|
+
violations=classified,
|
|
450
|
+
tier2_batch=asdict(tier2_batch) if tier2_batch else None,
|
|
451
|
+
tier3_violations=tier3_ids,
|
|
452
|
+
stats={
|
|
453
|
+
"total": len(classified),
|
|
454
|
+
"tier1": len(tier1_ids),
|
|
455
|
+
"tier2": len(tier2_ids),
|
|
456
|
+
"tier3": len(tier3_ids),
|
|
457
|
+
},
|
|
458
|
+
)
|
|
459
|
+
_save_session(session)
|
|
460
|
+
|
|
461
|
+
lines = [
|
|
462
|
+
f"{Status.SUCCESS} Remediation session {session_id} initialised.",
|
|
463
|
+
f"Directory: {directory} | Story: {story_id}",
|
|
464
|
+
f"Total findings: {len(classified)} from {report_path.name}",
|
|
465
|
+
"",
|
|
466
|
+
"## Tier Breakdown",
|
|
467
|
+
f"Tier 1 — ruff fixes (autonomous): {len(tier1_ids)}",
|
|
468
|
+
f"Tier 2 — developer/architect (one approval): {len(tier2_ids)}",
|
|
469
|
+
f"Tier 3 — HALT (user decision): {len(tier3_ids)}",
|
|
470
|
+
"",
|
|
471
|
+
]
|
|
472
|
+
|
|
473
|
+
if tier3_ids:
|
|
474
|
+
lines.append("## Tier 3 — Resolve before proceeding")
|
|
475
|
+
for vid in tier3_ids[:10]:
|
|
476
|
+
v = classified[vid]
|
|
477
|
+
lines.append(
|
|
478
|
+
f" [{v['severity'].upper()}] {v['rule']} — {v['file']}:{v['line']}"
|
|
479
|
+
)
|
|
480
|
+
lines.append(f" {v['fix_description']}")
|
|
481
|
+
if len(tier3_ids) > 10: # noqa: PLR2004 # TECH_DEBT: extract to named constant — G25
|
|
482
|
+
lines.append(f" ... and {len(tier3_ids) - 10} more.")
|
|
483
|
+
lines.append("")
|
|
484
|
+
|
|
485
|
+
lines.append("## Next Step")
|
|
486
|
+
if tier1_ids:
|
|
487
|
+
lines.append(
|
|
488
|
+
f'`remediate(action="execute", session_id="{session_id}", tier="tier1")`'
|
|
489
|
+
)
|
|
490
|
+
lines.append("Runs ruff --fix directly. No approval needed.")
|
|
491
|
+
elif tier2_ids:
|
|
492
|
+
lines.append(f'`remediate(action="approve", session_id="{session_id}")`')
|
|
493
|
+
else:
|
|
494
|
+
lines.append("Review Tier 3 violations above.")
|
|
495
|
+
|
|
496
|
+
return "\n".join(lines)
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def get_remediation_state(session_id: str = "") -> str:
|
|
500
|
+
"""Return current remediation session state."""
|
|
501
|
+
session = _load_session(session_id) if session_id else _load_active_session()
|
|
502
|
+
if not session:
|
|
503
|
+
return f"{Status.WARNING} No active remediation session."
|
|
504
|
+
|
|
505
|
+
stats = session.stats
|
|
506
|
+
pending = sum(
|
|
507
|
+
1
|
|
508
|
+
for v in session.violations.values()
|
|
509
|
+
if v["status"] == ViolationStatus.PENDING.value
|
|
510
|
+
)
|
|
511
|
+
fixed = sum(
|
|
512
|
+
1
|
|
513
|
+
for v in session.violations.values()
|
|
514
|
+
if v["status"] == ViolationStatus.AUTO_FIXED.value
|
|
515
|
+
)
|
|
516
|
+
deferred = sum(
|
|
517
|
+
1
|
|
518
|
+
for v in session.violations.values()
|
|
519
|
+
if v["status"] == ViolationStatus.DEFERRED.value
|
|
520
|
+
)
|
|
521
|
+
failed = sum(
|
|
522
|
+
1
|
|
523
|
+
for v in session.violations.values()
|
|
524
|
+
if v["status"] == ViolationStatus.FAILED.value
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
lines = [
|
|
528
|
+
f"## Remediation Session: {session.session_id}",
|
|
529
|
+
f"Story: {session.story_id} | Status: {session.status.value}",
|
|
530
|
+
f"Total: {stats.get('total', 0)} | Fixed: {fixed} | Pending: {pending} | Deferred: {deferred} | Failed: {failed}",
|
|
531
|
+
f"Tier 1: {stats.get('tier1', 0)} | Tier 2: {stats.get('tier2', 0)} | Tier 3: {stats.get('tier3', 0)}",
|
|
532
|
+
]
|
|
533
|
+
|
|
534
|
+
if session.tier3_violations:
|
|
535
|
+
lines.append("\nTier 3 violations requiring your decision:")
|
|
536
|
+
for vid in session.tier3_violations:
|
|
537
|
+
v = session.violations.get(vid, {})
|
|
538
|
+
lines.append(f" - {v.get('rule', vid)}: {v.get('fix_description', '')}")
|
|
539
|
+
|
|
540
|
+
return "\n".join(lines)
|
|
541
|
+
|
|
542
|
+
|
|
543
|
+
def approve_tier2_batch(session_id: str = "") -> str:
|
|
544
|
+
"""Approve the Tier 2 batch for execution."""
|
|
545
|
+
session = _load_session(session_id) if session_id else _load_active_session()
|
|
546
|
+
if not session:
|
|
547
|
+
return f"{Status.WARNING} No active remediation session found."
|
|
548
|
+
if not session.tier2_batch:
|
|
549
|
+
return f"{Status.WARNING} No Tier 2 batch in session {session.session_id}."
|
|
550
|
+
|
|
551
|
+
session.tier2_batch["approved"] = True
|
|
552
|
+
session.tier2_batch["approved_at"] = datetime.now().isoformat()
|
|
553
|
+
for vid in session.tier2_batch["violations"]:
|
|
554
|
+
if vid in session.violations:
|
|
555
|
+
session.violations[vid]["status"] = ViolationStatus.APPROVED.value
|
|
556
|
+
session.status = RemediationStatus.TIER2_RUNNING
|
|
557
|
+
_save_session(session)
|
|
558
|
+
|
|
559
|
+
count = len(session.tier2_batch["violations"])
|
|
560
|
+
return (
|
|
561
|
+
f"{Status.SUCCESS} Tier 2 batch approved. {count} violations queued.\n\n"
|
|
562
|
+
f'`remediate(action="execute", session_id="{session.session_id}", tier="tier2")`'
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def execute_remediation(
|
|
567
|
+
session_id: str = "",
|
|
568
|
+
tier: Literal["tier1", "tier2"] = "tier1",
|
|
569
|
+
) -> str:
|
|
570
|
+
"""Execute remediation for the specified tier.
|
|
571
|
+
|
|
572
|
+
Tier 1: Runs ruff --fix directly on files. Returns verified results.
|
|
573
|
+
Tier 2: Returns structured work order for developer/architect agent.
|
|
574
|
+
"""
|
|
575
|
+
session = _load_session(session_id) if session_id else _load_active_session()
|
|
576
|
+
if not session:
|
|
577
|
+
return f"{Status.WARNING} No active remediation session found."
|
|
578
|
+
if tier == "tier1":
|
|
579
|
+
return _execute_tier1(session)
|
|
580
|
+
return _execute_tier2(session)
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
def _execute_tier1(session: RemediationSession) -> str:
|
|
584
|
+
from ref_agents.tools.tier1_fixer import run_tier1_fixes, format_tier1_result
|
|
585
|
+
from ref_agents.utils.git_utils import find_project_root
|
|
586
|
+
|
|
587
|
+
pending = [
|
|
588
|
+
v
|
|
589
|
+
for v in session.violations.values()
|
|
590
|
+
if v["tier"] == RemediationTier.TIER1.value
|
|
591
|
+
and v["status"] == ViolationStatus.PENDING.value
|
|
592
|
+
]
|
|
593
|
+
if not pending:
|
|
594
|
+
return f"{Status.WARNING} No pending Tier 1 violations in session {session.session_id}."
|
|
595
|
+
|
|
596
|
+
project_root = Path(session.directory)
|
|
597
|
+
if not project_root.exists():
|
|
598
|
+
git_root = find_project_root(Path(session.directory))
|
|
599
|
+
project_root = git_root or Path(session.directory)
|
|
600
|
+
|
|
601
|
+
session.status = RemediationStatus.TIER1_RUNNING
|
|
602
|
+
_save_session(session)
|
|
603
|
+
|
|
604
|
+
result = run_tier1_fixes(pending, project_root)
|
|
605
|
+
|
|
606
|
+
fixed_files: set[str] = set()
|
|
607
|
+
failed_files: set[str] = set()
|
|
608
|
+
for file_result in result.file_results:
|
|
609
|
+
if file_result.error:
|
|
610
|
+
failed_files.add(file_result.file)
|
|
611
|
+
elif file_result.violations_fixed > 0:
|
|
612
|
+
fixed_files.add(file_result.file)
|
|
613
|
+
|
|
614
|
+
for vid, v in session.violations.items():
|
|
615
|
+
if v["tier"] != RemediationTier.TIER1.value:
|
|
616
|
+
continue
|
|
617
|
+
if v["status"] != ViolationStatus.PENDING.value:
|
|
618
|
+
continue
|
|
619
|
+
file = v.get("file", "")
|
|
620
|
+
if file in fixed_files:
|
|
621
|
+
session.violations[vid]["status"] = ViolationStatus.AUTO_FIXED.value
|
|
622
|
+
elif file in failed_files:
|
|
623
|
+
session.violations[vid]["status"] = ViolationStatus.FAILED.value
|
|
624
|
+
|
|
625
|
+
session.status = RemediationStatus.TIER1_COMPLETE
|
|
626
|
+
_save_session(session)
|
|
627
|
+
|
|
628
|
+
try:
|
|
629
|
+
from ref_agents.utils import handoff_logger
|
|
630
|
+
|
|
631
|
+
handoff_logger.log_phase_complete(
|
|
632
|
+
story_id=session.story_id,
|
|
633
|
+
phase="remediation_tier1",
|
|
634
|
+
outcome="passed",
|
|
635
|
+
next_phase="remediation_tier2_or_complete",
|
|
636
|
+
)
|
|
637
|
+
except (OSError, ValueError):
|
|
638
|
+
pass
|
|
639
|
+
|
|
640
|
+
return format_tier1_result(result, session.session_id)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
def _execute_tier2(session: RemediationSession) -> str:
|
|
644
|
+
approved = [
|
|
645
|
+
v
|
|
646
|
+
for v in session.violations.values()
|
|
647
|
+
if v["tier"] == RemediationTier.TIER2.value
|
|
648
|
+
and v["status"] == ViolationStatus.APPROVED.value
|
|
649
|
+
]
|
|
650
|
+
if not approved:
|
|
651
|
+
if session.tier2_batch and not session.tier2_batch.get("approved"):
|
|
652
|
+
return (
|
|
653
|
+
f"{Status.WARNING} Tier 2 batch not yet approved.\n"
|
|
654
|
+
f'`remediate(action="approve", session_id="{session.session_id}")`'
|
|
655
|
+
)
|
|
656
|
+
return f"{Status.WARNING} No approved Tier 2 violations in session {session.session_id}."
|
|
657
|
+
|
|
658
|
+
by_agent: dict[str, dict[str, list[dict]]] = {}
|
|
659
|
+
for v in approved:
|
|
660
|
+
by_agent.setdefault(v["responsible_agent"], {}).setdefault(
|
|
661
|
+
v["file"], []
|
|
662
|
+
).append(v)
|
|
663
|
+
|
|
664
|
+
lines = [
|
|
665
|
+
f"{Status.SUCCESS} Tier 2 work order — {len(approved)} violations.",
|
|
666
|
+
f"Session: {session.session_id}",
|
|
667
|
+
"",
|
|
668
|
+
"Each agent applies fixes, runs ruff + pyright, confirms exit 0.",
|
|
669
|
+
"",
|
|
670
|
+
]
|
|
671
|
+
step = 1
|
|
672
|
+
for agent, files in by_agent.items():
|
|
673
|
+
lines.append(f'### Step {step}: `agent(action="activate", name="{agent}")`')
|
|
674
|
+
for file, items in files.items():
|
|
675
|
+
lines.append(f" File: `{file}`")
|
|
676
|
+
for v in items:
|
|
677
|
+
lines.append(f" - [{v['id']}] {v['fix_description']}")
|
|
678
|
+
if v["message"]:
|
|
679
|
+
lines.append(f" {v['message'][:120]}")
|
|
680
|
+
lines.append("")
|
|
681
|
+
lines.append(" `ruff check --fix && ruff format && pyright`")
|
|
682
|
+
lines.append(
|
|
683
|
+
f' `remediate(action="update", session_id="{session.session_id}", '
|
|
684
|
+
f'violation_ids=[...], status="auto_fixed")`'
|
|
685
|
+
)
|
|
686
|
+
lines.append("")
|
|
687
|
+
step += 1
|
|
688
|
+
|
|
689
|
+
lines.append('### Final: `agent(action="activate", name="pr_reviewer")`')
|
|
690
|
+
lines.append(f'`remediate(action="complete", session_id="{session.session_id}")`')
|
|
691
|
+
return "\n".join(lines)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def defer_violation(session_id: str, violation_id: str, reason: str = "") -> str:
|
|
695
|
+
"""Defer a violation to TECH_DEBT.md."""
|
|
696
|
+
session = _load_session(session_id) if session_id else _load_active_session()
|
|
697
|
+
if not session:
|
|
698
|
+
return f"{Status.WARNING} Session not found."
|
|
699
|
+
if violation_id not in session.violations:
|
|
700
|
+
return f"{Status.ERROR} Violation {violation_id} not found."
|
|
701
|
+
session.violations[violation_id]["status"] = ViolationStatus.DEFERRED.value
|
|
702
|
+
session.violations[violation_id]["fix_notes"] = reason or "Deferred to TECH_DEBT.md"
|
|
703
|
+
_save_session(session)
|
|
704
|
+
v = session.violations[violation_id]
|
|
705
|
+
return (
|
|
706
|
+
f"{Status.SUCCESS} {violation_id} deferred.\n"
|
|
707
|
+
f'`log(event="debt", debt_type="{v["rule"]}", severity="{v["severity"]}", location="{v["file"]}")`'
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def update_violation_status(
|
|
712
|
+
session_id: str,
|
|
713
|
+
violation_ids: list[str],
|
|
714
|
+
status: str,
|
|
715
|
+
notes: str = "",
|
|
716
|
+
) -> str:
|
|
717
|
+
"""Update status of one or more violations after fixing."""
|
|
718
|
+
session = _load_session(session_id)
|
|
719
|
+
if not session:
|
|
720
|
+
return f"{Status.ERROR} Session {session_id} not found."
|
|
721
|
+
updated = []
|
|
722
|
+
for vid in violation_ids:
|
|
723
|
+
if vid in session.violations:
|
|
724
|
+
session.violations[vid]["status"] = status
|
|
725
|
+
if notes:
|
|
726
|
+
session.violations[vid]["fix_notes"] = notes
|
|
727
|
+
updated.append(vid)
|
|
728
|
+
_save_session(session)
|
|
729
|
+
return f"{Status.SUCCESS} Updated {len(updated)} violations to '{status}'."
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
def _group_tier3_violations(session: "RemediationSession") -> list[dict]:
|
|
733
|
+
"""Group unresolved Tier 3 violations by file for fix_flow spawning.
|
|
734
|
+
|
|
735
|
+
Returns:
|
|
736
|
+
List of dicts: {file, count, violations, rule_summary, responsible_agent}
|
|
737
|
+
Sorted by count descending. Max 20 groups.
|
|
738
|
+
"""
|
|
739
|
+
groups: dict[str, dict] = {}
|
|
740
|
+
for v in session.violations.values():
|
|
741
|
+
if v["tier"] != RemediationTier.TIER3.value:
|
|
742
|
+
continue
|
|
743
|
+
if v["status"] not in (
|
|
744
|
+
ViolationStatus.PENDING.value,
|
|
745
|
+
ViolationStatus.FAILED.value,
|
|
746
|
+
):
|
|
747
|
+
continue
|
|
748
|
+
file = v.get("file", "unknown")
|
|
749
|
+
if file not in groups:
|
|
750
|
+
groups[file] = {
|
|
751
|
+
"file": file,
|
|
752
|
+
"count": 0,
|
|
753
|
+
"violations": [],
|
|
754
|
+
"rules": set(),
|
|
755
|
+
"responsible_agent": v.get("responsible_agent", "architect"),
|
|
756
|
+
}
|
|
757
|
+
groups[file]["count"] += 1
|
|
758
|
+
groups[file]["violations"].append(v)
|
|
759
|
+
groups[file]["rules"].add(v.get("rule", "unknown"))
|
|
760
|
+
|
|
761
|
+
result = []
|
|
762
|
+
for g in sorted(groups.values(), key=lambda x: x["count"], reverse=True)[:20]:
|
|
763
|
+
result.append(
|
|
764
|
+
{
|
|
765
|
+
"file": g["file"],
|
|
766
|
+
"count": g["count"],
|
|
767
|
+
"violations": g["violations"],
|
|
768
|
+
"rule_summary": ", ".join(sorted(g["rules"])),
|
|
769
|
+
"responsible_agent": g["responsible_agent"],
|
|
770
|
+
}
|
|
771
|
+
)
|
|
772
|
+
return result
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def complete_remediation(session_id: str = "") -> str:
|
|
776
|
+
"""Mark remediation session complete after PR Reviewer gate passes."""
|
|
777
|
+
session = _load_session(session_id) if session_id else _load_active_session()
|
|
778
|
+
if not session:
|
|
779
|
+
return f"{Status.WARNING} No session found."
|
|
780
|
+
fixed = sum(
|
|
781
|
+
1
|
|
782
|
+
for v in session.violations.values()
|
|
783
|
+
if v["status"] == ViolationStatus.AUTO_FIXED.value
|
|
784
|
+
)
|
|
785
|
+
deferred = sum(
|
|
786
|
+
1
|
|
787
|
+
for v in session.violations.values()
|
|
788
|
+
if v["status"] == ViolationStatus.DEFERRED.value
|
|
789
|
+
)
|
|
790
|
+
failed = sum(
|
|
791
|
+
1
|
|
792
|
+
for v in session.violations.values()
|
|
793
|
+
if v["status"] == ViolationStatus.FAILED.value
|
|
794
|
+
)
|
|
795
|
+
session.status = RemediationStatus.COMPLETE
|
|
796
|
+
_save_session(session)
|
|
797
|
+
try:
|
|
798
|
+
from ref_agents.utils import handoff_logger
|
|
799
|
+
|
|
800
|
+
handoff_logger.log_phase_complete(
|
|
801
|
+
story_id=session.story_id,
|
|
802
|
+
phase="compliance_remediation",
|
|
803
|
+
outcome="passed",
|
|
804
|
+
next_phase="scrum_master",
|
|
805
|
+
)
|
|
806
|
+
handoff_logger.log_escalation(
|
|
807
|
+
level="L1",
|
|
808
|
+
from_agent="platform_engineer",
|
|
809
|
+
to_agents=["scrum_master"],
|
|
810
|
+
reason=f"Remediation {session.session_id} complete. Fixed: {fixed}, Deferred: {deferred}, Failed: {failed}",
|
|
811
|
+
story_id=session.story_id,
|
|
812
|
+
)
|
|
813
|
+
except (OSError, ValueError):
|
|
814
|
+
pass
|
|
815
|
+
# Surface Tier 3 groups with fix_flow instructions
|
|
816
|
+
tier3_groups = _group_tier3_violations(session)
|
|
817
|
+
if tier3_groups:
|
|
818
|
+
lines = [
|
|
819
|
+
f"{Status.SUCCESS} Remediation {session.session_id} complete.",
|
|
820
|
+
f"Fixed: {fixed} | Deferred: {deferred} | Failed: {failed}",
|
|
821
|
+
"",
|
|
822
|
+
f"{Status.HALT} {len(tier3_groups)} Tier 3 group(s) require investigation.",
|
|
823
|
+
"Confirm before spawning fix_flow: reply 'yes' to proceed, 'skip' to go straight to scrum_master.",
|
|
824
|
+
"",
|
|
825
|
+
"## Tier 3 Fix Flow Sessions (one per file group)",
|
|
826
|
+
]
|
|
827
|
+
for i, g in enumerate(tier3_groups, 1):
|
|
828
|
+
desc = f"{g['rule_summary']} ({g['count']} violation(s))"
|
|
829
|
+
lines.append(f"{i}. {g['file']}")
|
|
830
|
+
lines.append(
|
|
831
|
+
f' workflow(action="fix_flow", story_id="{session.story_id}", '
|
|
832
|
+
f'file_path="{g["file"]}", error_description="{desc}")'
|
|
833
|
+
)
|
|
834
|
+
lines += [
|
|
835
|
+
"",
|
|
836
|
+
"## After all fix_flow sessions complete:",
|
|
837
|
+
'`agent(action="activate", name="scrum_master")`',
|
|
838
|
+
]
|
|
839
|
+
return "\n".join(lines)
|
|
840
|
+
|
|
841
|
+
return (
|
|
842
|
+
f"{Status.SUCCESS} Remediation {session.session_id} complete.\n"
|
|
843
|
+
f"Fixed: {fixed} | Deferred: {deferred} | Failed: {failed}\n\n"
|
|
844
|
+
f"Story audit required. Call:\n"
|
|
845
|
+
f'`agent(action="activate", name="scrum_master")`'
|
|
846
|
+
)
|