tapps-agents 3.5.41__py3-none-any.whl → 3.6.1__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.
- tapps_agents/__init__.py +2 -2
- tapps_agents/agents/reviewer/scoring.py +1566 -1566
- tapps_agents/agents/reviewer/tools/__init__.py +41 -41
- tapps_agents/cli/commands/health.py +665 -665
- tapps_agents/cli/commands/top_level.py +3586 -3586
- tapps_agents/core/artifact_context_builder.py +293 -0
- tapps_agents/core/config.py +33 -0
- tapps_agents/health/orchestrator.py +271 -271
- tapps_agents/resources/__init__.py +5 -0
- tapps_agents/resources/claude/__init__.py +1 -0
- tapps_agents/resources/claude/commands/README.md +156 -0
- tapps_agents/resources/claude/commands/__init__.py +1 -0
- tapps_agents/resources/claude/commands/build-fix.md +22 -0
- tapps_agents/resources/claude/commands/build.md +77 -0
- tapps_agents/resources/claude/commands/debug.md +53 -0
- tapps_agents/resources/claude/commands/design.md +68 -0
- tapps_agents/resources/claude/commands/docs.md +53 -0
- tapps_agents/resources/claude/commands/e2e.md +22 -0
- tapps_agents/resources/claude/commands/fix.md +54 -0
- tapps_agents/resources/claude/commands/implement.md +53 -0
- tapps_agents/resources/claude/commands/improve.md +53 -0
- tapps_agents/resources/claude/commands/library-docs.md +64 -0
- tapps_agents/resources/claude/commands/lint.md +52 -0
- tapps_agents/resources/claude/commands/plan.md +65 -0
- tapps_agents/resources/claude/commands/refactor-clean.md +21 -0
- tapps_agents/resources/claude/commands/refactor.md +55 -0
- tapps_agents/resources/claude/commands/review.md +67 -0
- tapps_agents/resources/claude/commands/score.md +60 -0
- tapps_agents/resources/claude/commands/security-review.md +22 -0
- tapps_agents/resources/claude/commands/security-scan.md +54 -0
- tapps_agents/resources/claude/commands/tdd.md +24 -0
- tapps_agents/resources/claude/commands/test-coverage.md +21 -0
- tapps_agents/resources/claude/commands/test.md +54 -0
- tapps_agents/resources/claude/commands/update-codemaps.md +20 -0
- tapps_agents/resources/claude/commands/update-docs.md +21 -0
- tapps_agents/resources/claude/skills/__init__.py +1 -0
- tapps_agents/resources/claude/skills/analyst/SKILL.md +272 -0
- tapps_agents/resources/claude/skills/analyst/__init__.py +1 -0
- tapps_agents/resources/claude/skills/architect/SKILL.md +282 -0
- tapps_agents/resources/claude/skills/architect/__init__.py +1 -0
- tapps_agents/resources/claude/skills/backend-patterns/SKILL.md +30 -0
- tapps_agents/resources/claude/skills/backend-patterns/__init__.py +1 -0
- tapps_agents/resources/claude/skills/coding-standards/SKILL.md +29 -0
- tapps_agents/resources/claude/skills/coding-standards/__init__.py +1 -0
- tapps_agents/resources/claude/skills/debugger/SKILL.md +203 -0
- tapps_agents/resources/claude/skills/debugger/__init__.py +1 -0
- tapps_agents/resources/claude/skills/designer/SKILL.md +243 -0
- tapps_agents/resources/claude/skills/designer/__init__.py +1 -0
- tapps_agents/resources/claude/skills/documenter/SKILL.md +252 -0
- tapps_agents/resources/claude/skills/documenter/__init__.py +1 -0
- tapps_agents/resources/claude/skills/enhancer/SKILL.md +307 -0
- tapps_agents/resources/claude/skills/enhancer/__init__.py +1 -0
- tapps_agents/resources/claude/skills/evaluator/SKILL.md +204 -0
- tapps_agents/resources/claude/skills/evaluator/__init__.py +1 -0
- tapps_agents/resources/claude/skills/frontend-patterns/SKILL.md +29 -0
- tapps_agents/resources/claude/skills/frontend-patterns/__init__.py +1 -0
- tapps_agents/resources/claude/skills/implementer/SKILL.md +188 -0
- tapps_agents/resources/claude/skills/implementer/__init__.py +1 -0
- tapps_agents/resources/claude/skills/improver/SKILL.md +218 -0
- tapps_agents/resources/claude/skills/improver/__init__.py +1 -0
- tapps_agents/resources/claude/skills/ops/SKILL.md +281 -0
- tapps_agents/resources/claude/skills/ops/__init__.py +1 -0
- tapps_agents/resources/claude/skills/orchestrator/SKILL.md +390 -0
- tapps_agents/resources/claude/skills/orchestrator/__init__.py +1 -0
- tapps_agents/resources/claude/skills/planner/SKILL.md +254 -0
- tapps_agents/resources/claude/skills/planner/__init__.py +1 -0
- tapps_agents/resources/claude/skills/reviewer/SKILL.md +434 -0
- tapps_agents/resources/claude/skills/reviewer/__init__.py +1 -0
- tapps_agents/resources/claude/skills/security-review/SKILL.md +31 -0
- tapps_agents/resources/claude/skills/security-review/__init__.py +1 -0
- tapps_agents/resources/claude/skills/simple-mode/SKILL.md +695 -0
- tapps_agents/resources/claude/skills/simple-mode/__init__.py +1 -0
- tapps_agents/resources/claude/skills/tester/SKILL.md +219 -0
- tapps_agents/resources/claude/skills/tester/__init__.py +1 -0
- tapps_agents/resources/cursor/.cursorignore +35 -0
- tapps_agents/resources/cursor/__init__.py +1 -0
- tapps_agents/resources/cursor/commands/__init__.py +1 -0
- tapps_agents/resources/cursor/commands/build-fix.md +11 -0
- tapps_agents/resources/cursor/commands/build.md +11 -0
- tapps_agents/resources/cursor/commands/e2e.md +11 -0
- tapps_agents/resources/cursor/commands/fix.md +11 -0
- tapps_agents/resources/cursor/commands/refactor-clean.md +11 -0
- tapps_agents/resources/cursor/commands/review.md +11 -0
- tapps_agents/resources/cursor/commands/security-review.md +11 -0
- tapps_agents/resources/cursor/commands/tdd.md +11 -0
- tapps_agents/resources/cursor/commands/test-coverage.md +11 -0
- tapps_agents/resources/cursor/commands/test.md +11 -0
- tapps_agents/resources/cursor/commands/update-codemaps.md +10 -0
- tapps_agents/resources/cursor/commands/update-docs.md +11 -0
- tapps_agents/resources/cursor/rules/__init__.py +1 -0
- tapps_agents/resources/cursor/rules/agent-capabilities.mdc +687 -0
- tapps_agents/resources/cursor/rules/coding-style.mdc +31 -0
- tapps_agents/resources/cursor/rules/command-reference.mdc +2081 -0
- tapps_agents/resources/cursor/rules/cursor-mode-usage.mdc +125 -0
- tapps_agents/resources/cursor/rules/git-workflow.mdc +29 -0
- tapps_agents/resources/cursor/rules/performance.mdc +29 -0
- tapps_agents/resources/cursor/rules/project-context.mdc +163 -0
- tapps_agents/resources/cursor/rules/project-profiling.mdc +197 -0
- tapps_agents/resources/cursor/rules/quick-reference.mdc +630 -0
- tapps_agents/resources/cursor/rules/security.mdc +32 -0
- tapps_agents/resources/cursor/rules/simple-mode.mdc +500 -0
- tapps_agents/resources/cursor/rules/testing.mdc +31 -0
- tapps_agents/resources/cursor/rules/when-to-use.mdc +156 -0
- tapps_agents/resources/cursor/rules/workflow-presets.mdc +179 -0
- tapps_agents/resources/customizations/__init__.py +1 -0
- tapps_agents/resources/customizations/example-custom.yaml +83 -0
- tapps_agents/resources/hooks/__init__.py +1 -0
- tapps_agents/resources/hooks/templates/README.md +5 -0
- tapps_agents/resources/hooks/templates/__init__.py +1 -0
- tapps_agents/resources/hooks/templates/add-project-context.yaml +8 -0
- tapps_agents/resources/hooks/templates/auto-format-js.yaml +10 -0
- tapps_agents/resources/hooks/templates/auto-format-python.yaml +10 -0
- tapps_agents/resources/hooks/templates/git-commit-check.yaml +7 -0
- tapps_agents/resources/hooks/templates/notify-on-complete.yaml +8 -0
- tapps_agents/resources/hooks/templates/quality-gate.yaml +8 -0
- tapps_agents/resources/hooks/templates/security-scan-on-edit.yaml +10 -0
- tapps_agents/resources/hooks/templates/session-end-log.yaml +7 -0
- tapps_agents/resources/hooks/templates/show-beads-ready.yaml +8 -0
- tapps_agents/resources/hooks/templates/test-on-edit.yaml +10 -0
- tapps_agents/resources/hooks/templates/update-docs-on-complete.yaml +8 -0
- tapps_agents/resources/hooks/templates/user-prompt-log.yaml +7 -0
- tapps_agents/resources/scripts/__init__.py +1 -0
- tapps_agents/resources/scripts/set_bd_path.ps1 +51 -0
- tapps_agents/resources/workflows/__init__.py +1 -0
- tapps_agents/resources/workflows/presets/__init__.py +1 -0
- tapps_agents/resources/workflows/presets/brownfield-analysis.yaml +235 -0
- tapps_agents/resources/workflows/presets/fix.yaml +78 -0
- tapps_agents/resources/workflows/presets/full-sdlc.yaml +122 -0
- tapps_agents/resources/workflows/presets/quality.yaml +82 -0
- tapps_agents/resources/workflows/presets/rapid-dev.yaml +84 -0
- tapps_agents/simple_mode/orchestrators/base.py +185 -185
- tapps_agents/simple_mode/orchestrators/build_orchestrator.py +2700 -2667
- tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +723 -723
- tapps_agents/workflow/cursor_executor.py +2337 -2337
- tapps_agents/workflow/message_formatter.py +188 -188
- {tapps_agents-3.5.41.dist-info → tapps_agents-3.6.1.dist-info}/METADATA +6 -6
- {tapps_agents-3.5.41.dist-info → tapps_agents-3.6.1.dist-info}/RECORD +141 -18
- {tapps_agents-3.5.41.dist-info → tapps_agents-3.6.1.dist-info}/WHEEL +0 -0
- {tapps_agents-3.5.41.dist-info → tapps_agents-3.6.1.dist-info}/entry_points.txt +0 -0
- {tapps_agents-3.5.41.dist-info → tapps_agents-3.6.1.dist-info}/licenses/LICENSE +0 -0
- {tapps_agents-3.5.41.dist-info → tapps_agents-3.6.1.dist-info}/top_level.txt +0 -0
|
@@ -1,271 +1,271 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Health Check Orchestrator.
|
|
3
|
-
|
|
4
|
-
Coordinates execution of all health checks and aggregates results.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import concurrent.futures
|
|
10
|
-
import logging
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
from typing import Any
|
|
13
|
-
|
|
14
|
-
from .base import HealthCheckResult
|
|
15
|
-
from .collector import HealthMetricsCollector
|
|
16
|
-
from .registry import HealthCheckRegistry
|
|
17
|
-
|
|
18
|
-
logger = logging.getLogger(__name__)
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class HealthOrchestrator:
|
|
22
|
-
"""Orchestrates health check execution and aggregation."""
|
|
23
|
-
|
|
24
|
-
def __init__(
|
|
25
|
-
self,
|
|
26
|
-
registry: HealthCheckRegistry | None = None,
|
|
27
|
-
metrics_collector: HealthMetricsCollector | None = None,
|
|
28
|
-
project_root: Path | None = None,
|
|
29
|
-
):
|
|
30
|
-
"""
|
|
31
|
-
Initialize health orchestrator.
|
|
32
|
-
|
|
33
|
-
Args:
|
|
34
|
-
registry: Health check registry (creates default if None)
|
|
35
|
-
metrics_collector: Metrics collector (creates default if None)
|
|
36
|
-
project_root: Project root directory
|
|
37
|
-
"""
|
|
38
|
-
self.registry = registry or HealthCheckRegistry()
|
|
39
|
-
self.metrics_collector = metrics_collector or HealthMetricsCollector(
|
|
40
|
-
project_root=project_root
|
|
41
|
-
)
|
|
42
|
-
self.project_root = project_root or Path.cwd()
|
|
43
|
-
|
|
44
|
-
def run_all_checks(
|
|
45
|
-
self, check_names: list[str] | None = None, save_metrics: bool = True
|
|
46
|
-
) -> dict[str, HealthCheckResult]:
|
|
47
|
-
"""
|
|
48
|
-
Run all health checks (or specified subset).
|
|
49
|
-
|
|
50
|
-
Args:
|
|
51
|
-
check_names: Optional list of check names to run. If None, runs all.
|
|
52
|
-
save_metrics: Whether to save results to metrics storage
|
|
53
|
-
|
|
54
|
-
Returns:
|
|
55
|
-
Dictionary mapping check names to HealthCheckResult instances
|
|
56
|
-
"""
|
|
57
|
-
results = self.registry.run_all(check_names)
|
|
58
|
-
|
|
59
|
-
# Save metrics if requested
|
|
60
|
-
if save_metrics:
|
|
61
|
-
for result in results.values():
|
|
62
|
-
if result:
|
|
63
|
-
self.metrics_collector.record_health_check_result(result)
|
|
64
|
-
|
|
65
|
-
return results
|
|
66
|
-
|
|
67
|
-
def run_checks_parallel(
|
|
68
|
-
self, check_names: list[str] | None = None, max_workers: int = 4
|
|
69
|
-
) -> dict[str, HealthCheckResult]:
|
|
70
|
-
"""
|
|
71
|
-
Run health checks in parallel where possible.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
check_names: Optional list of check names to run
|
|
75
|
-
max_workers: Maximum number of parallel workers
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
Dictionary mapping check names to HealthCheckResult instances
|
|
79
|
-
"""
|
|
80
|
-
if check_names is None:
|
|
81
|
-
check_names = self.registry.list_names()
|
|
82
|
-
|
|
83
|
-
# Build dependency graph
|
|
84
|
-
dependency_graph: dict[str, set[str]] = {}
|
|
85
|
-
independent_checks: list[str] = []
|
|
86
|
-
dependent_checks: list[str] = []
|
|
87
|
-
|
|
88
|
-
for name in check_names:
|
|
89
|
-
check = self.registry.get(name)
|
|
90
|
-
if check:
|
|
91
|
-
deps = check.get_dependencies()
|
|
92
|
-
dependency_graph[name] = set(deps)
|
|
93
|
-
if not deps:
|
|
94
|
-
independent_checks.append(name)
|
|
95
|
-
else:
|
|
96
|
-
dependent_checks.append(name)
|
|
97
|
-
|
|
98
|
-
results: dict[str, HealthCheckResult] = {}
|
|
99
|
-
|
|
100
|
-
# Run independent checks in parallel
|
|
101
|
-
if independent_checks:
|
|
102
|
-
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
103
|
-
future_to_check = {
|
|
104
|
-
executor.submit(self.registry.run, name): name for name in independent_checks
|
|
105
|
-
}
|
|
106
|
-
for future in concurrent.futures.as_completed(future_to_check):
|
|
107
|
-
check_name = future_to_check[future]
|
|
108
|
-
try:
|
|
109
|
-
result = future.result()
|
|
110
|
-
if result:
|
|
111
|
-
results[check_name] = result
|
|
112
|
-
except Exception as e:
|
|
113
|
-
logger.error(f"Error running check {check_name}: {e}", exc_info=True)
|
|
114
|
-
results[check_name] = HealthCheckResult(
|
|
115
|
-
name=check_name,
|
|
116
|
-
status="unhealthy",
|
|
117
|
-
score=0.0,
|
|
118
|
-
message=f"Check failed with error: {e}",
|
|
119
|
-
details={"error": str(e)},
|
|
120
|
-
)
|
|
121
|
-
|
|
122
|
-
# Run dependent checks sequentially (respecting dependencies)
|
|
123
|
-
execution_order = self.registry._topological_sort(dependent_checks)
|
|
124
|
-
for name in execution_order:
|
|
125
|
-
if name not in results:
|
|
126
|
-
result = self.registry.run(name)
|
|
127
|
-
if result:
|
|
128
|
-
results[name] = result
|
|
129
|
-
|
|
130
|
-
# Save metrics
|
|
131
|
-
for result in results.values():
|
|
132
|
-
if result:
|
|
133
|
-
self.metrics_collector.record_health_check_result(result)
|
|
134
|
-
|
|
135
|
-
return results
|
|
136
|
-
|
|
137
|
-
def get_overall_health(self, results: dict[str, HealthCheckResult] | None = None) -> dict[str, Any]:
|
|
138
|
-
"""
|
|
139
|
-
Calculate overall health status from check results.
|
|
140
|
-
|
|
141
|
-
Args:
|
|
142
|
-
results: Optional check results (runs all checks if None)
|
|
143
|
-
|
|
144
|
-
Returns:
|
|
145
|
-
Dictionary with overall health information
|
|
146
|
-
"""
|
|
147
|
-
if results is None:
|
|
148
|
-
results = self.run_all_checks(save_metrics=True)
|
|
149
|
-
|
|
150
|
-
if not results:
|
|
151
|
-
return {
|
|
152
|
-
"status": "unknown",
|
|
153
|
-
"score": 0.0,
|
|
154
|
-
"message": "No health checks available",
|
|
155
|
-
"checks_count": 0,
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
# Calculate weighted average score
|
|
159
|
-
# Critical checks (environment, execution) have higher weight
|
|
160
|
-
critical_checks = {"environment", "execution"}
|
|
161
|
-
total_weight = 0.0
|
|
162
|
-
weighted_score = 0.0
|
|
163
|
-
|
|
164
|
-
status_counts = {"healthy": 0, "degraded": 0, "unhealthy": 0}
|
|
165
|
-
|
|
166
|
-
for name, result in results.items():
|
|
167
|
-
if not result:
|
|
168
|
-
continue
|
|
169
|
-
|
|
170
|
-
weight = 2.0 if name in critical_checks else 1.0
|
|
171
|
-
total_weight += weight
|
|
172
|
-
weighted_score += result.score * weight
|
|
173
|
-
|
|
174
|
-
status_counts[result.status] = status_counts.get(result.status, 0) + 1
|
|
175
|
-
|
|
176
|
-
overall_score = weighted_score / total_weight if total_weight > 0 else 0.0
|
|
177
|
-
|
|
178
|
-
# Determine overall status (HM-001-S3: degraded when score >= 75 and only non-critical unhealthy)
|
|
179
|
-
critical_checks = {"environment", "execution"}
|
|
180
|
-
non_critical_checks = {"outcomes", "knowledge_base", "context7_cache", "automation"}
|
|
181
|
-
unhealthy_checks = [
|
|
182
|
-
name
|
|
183
|
-
for name, result in results.items()
|
|
184
|
-
if result and result.status == "unhealthy"
|
|
185
|
-
]
|
|
186
|
-
critical_healthy = all(
|
|
187
|
-
(results.get(name) and results[name].status != "unhealthy")
|
|
188
|
-
for name in critical_checks
|
|
189
|
-
if name in results
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
status_reason: str | None = None
|
|
193
|
-
if status_counts["unhealthy"] > 0:
|
|
194
|
-
overall_status = "unhealthy"
|
|
195
|
-
if (
|
|
196
|
-
overall_score >= 75.0
|
|
197
|
-
and critical_healthy
|
|
198
|
-
and unhealthy_checks
|
|
199
|
-
and all(c in non_critical_checks for c in unhealthy_checks)
|
|
200
|
-
):
|
|
201
|
-
overall_status = "degraded"
|
|
202
|
-
status_reason = (
|
|
203
|
-
"Status degraded due to non-critical checks; core functionality is healthy"
|
|
204
|
-
)
|
|
205
|
-
elif status_counts["degraded"] > 0:
|
|
206
|
-
overall_status = "degraded"
|
|
207
|
-
else:
|
|
208
|
-
overall_status = "healthy"
|
|
209
|
-
|
|
210
|
-
# Build remediation list (prioritized)
|
|
211
|
-
all_remediations: list[str] = []
|
|
212
|
-
for name, result in results.items():
|
|
213
|
-
if result and result.remediation:
|
|
214
|
-
if isinstance(result.remediation, list):
|
|
215
|
-
all_remediations.extend(result.remediation)
|
|
216
|
-
elif isinstance(result.remediation, str):
|
|
217
|
-
all_remediations.append(result.remediation)
|
|
218
|
-
|
|
219
|
-
# Deduplicate and prioritize
|
|
220
|
-
unique_remediations = []
|
|
221
|
-
seen = set()
|
|
222
|
-
for rem in all_remediations:
|
|
223
|
-
rem_lower = rem.lower()
|
|
224
|
-
if rem_lower not in seen:
|
|
225
|
-
seen.add(rem_lower)
|
|
226
|
-
unique_remediations.append(rem)
|
|
227
|
-
|
|
228
|
-
# Prioritize: unhealthy checks first, then degraded
|
|
229
|
-
prioritized_remediations = []
|
|
230
|
-
for name, result in results.items():
|
|
231
|
-
if result and result.remediation and result.status == "unhealthy":
|
|
232
|
-
if isinstance(result.remediation, list):
|
|
233
|
-
prioritized_remediations.extend(result.remediation)
|
|
234
|
-
else:
|
|
235
|
-
prioritized_remediations.append(result.remediation)
|
|
236
|
-
|
|
237
|
-
for name, result in results.items():
|
|
238
|
-
if result and result.remediation and result.status == "degraded":
|
|
239
|
-
if isinstance(result.remediation, list):
|
|
240
|
-
prioritized_remediations.extend(result.remediation)
|
|
241
|
-
else:
|
|
242
|
-
prioritized_remediations.append(result.remediation)
|
|
243
|
-
|
|
244
|
-
# Add remaining unique remediations
|
|
245
|
-
for rem in unique_remediations:
|
|
246
|
-
if rem not in prioritized_remediations:
|
|
247
|
-
prioritized_remediations.append(rem)
|
|
248
|
-
|
|
249
|
-
details: dict[str, Any] = {}
|
|
250
|
-
if status_reason:
|
|
251
|
-
details["status_reason"] = status_reason
|
|
252
|
-
|
|
253
|
-
return {
|
|
254
|
-
"status": overall_status,
|
|
255
|
-
"score": overall_score,
|
|
256
|
-
"message": f"Overall health: {overall_status} ({overall_score:.1f}/100)",
|
|
257
|
-
"checks_count": len(results),
|
|
258
|
-
"status_counts": status_counts,
|
|
259
|
-
"details": details,
|
|
260
|
-
"checks": {
|
|
261
|
-
name: {
|
|
262
|
-
"status": result.status,
|
|
263
|
-
"score": result.score,
|
|
264
|
-
"message": result.message,
|
|
265
|
-
}
|
|
266
|
-
for name, result in results.items()
|
|
267
|
-
if result
|
|
268
|
-
},
|
|
269
|
-
"remediation": prioritized_remediations[:5], # Top 5 remediation actions
|
|
270
|
-
}
|
|
271
|
-
|
|
1
|
+
"""
|
|
2
|
+
Health Check Orchestrator.
|
|
3
|
+
|
|
4
|
+
Coordinates execution of all health checks and aggregates results.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import concurrent.futures
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from .base import HealthCheckResult
|
|
15
|
+
from .collector import HealthMetricsCollector
|
|
16
|
+
from .registry import HealthCheckRegistry
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HealthOrchestrator:
|
|
22
|
+
"""Orchestrates health check execution and aggregation."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
registry: HealthCheckRegistry | None = None,
|
|
27
|
+
metrics_collector: HealthMetricsCollector | None = None,
|
|
28
|
+
project_root: Path | None = None,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
Initialize health orchestrator.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
registry: Health check registry (creates default if None)
|
|
35
|
+
metrics_collector: Metrics collector (creates default if None)
|
|
36
|
+
project_root: Project root directory
|
|
37
|
+
"""
|
|
38
|
+
self.registry = registry or HealthCheckRegistry()
|
|
39
|
+
self.metrics_collector = metrics_collector or HealthMetricsCollector(
|
|
40
|
+
project_root=project_root
|
|
41
|
+
)
|
|
42
|
+
self.project_root = project_root or Path.cwd()
|
|
43
|
+
|
|
44
|
+
def run_all_checks(
|
|
45
|
+
self, check_names: list[str] | None = None, save_metrics: bool = True
|
|
46
|
+
) -> dict[str, HealthCheckResult]:
|
|
47
|
+
"""
|
|
48
|
+
Run all health checks (or specified subset).
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
check_names: Optional list of check names to run. If None, runs all.
|
|
52
|
+
save_metrics: Whether to save results to metrics storage
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Dictionary mapping check names to HealthCheckResult instances
|
|
56
|
+
"""
|
|
57
|
+
results = self.registry.run_all(check_names)
|
|
58
|
+
|
|
59
|
+
# Save metrics if requested
|
|
60
|
+
if save_metrics:
|
|
61
|
+
for result in results.values():
|
|
62
|
+
if result:
|
|
63
|
+
self.metrics_collector.record_health_check_result(result)
|
|
64
|
+
|
|
65
|
+
return results
|
|
66
|
+
|
|
67
|
+
def run_checks_parallel(
|
|
68
|
+
self, check_names: list[str] | None = None, max_workers: int = 4
|
|
69
|
+
) -> dict[str, HealthCheckResult]:
|
|
70
|
+
"""
|
|
71
|
+
Run health checks in parallel where possible.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
check_names: Optional list of check names to run
|
|
75
|
+
max_workers: Maximum number of parallel workers
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
Dictionary mapping check names to HealthCheckResult instances
|
|
79
|
+
"""
|
|
80
|
+
if check_names is None:
|
|
81
|
+
check_names = self.registry.list_names()
|
|
82
|
+
|
|
83
|
+
# Build dependency graph
|
|
84
|
+
dependency_graph: dict[str, set[str]] = {}
|
|
85
|
+
independent_checks: list[str] = []
|
|
86
|
+
dependent_checks: list[str] = []
|
|
87
|
+
|
|
88
|
+
for name in check_names:
|
|
89
|
+
check = self.registry.get(name)
|
|
90
|
+
if check:
|
|
91
|
+
deps = check.get_dependencies()
|
|
92
|
+
dependency_graph[name] = set(deps)
|
|
93
|
+
if not deps:
|
|
94
|
+
independent_checks.append(name)
|
|
95
|
+
else:
|
|
96
|
+
dependent_checks.append(name)
|
|
97
|
+
|
|
98
|
+
results: dict[str, HealthCheckResult] = {}
|
|
99
|
+
|
|
100
|
+
# Run independent checks in parallel
|
|
101
|
+
if independent_checks:
|
|
102
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
103
|
+
future_to_check = {
|
|
104
|
+
executor.submit(self.registry.run, name): name for name in independent_checks
|
|
105
|
+
}
|
|
106
|
+
for future in concurrent.futures.as_completed(future_to_check):
|
|
107
|
+
check_name = future_to_check[future]
|
|
108
|
+
try:
|
|
109
|
+
result = future.result()
|
|
110
|
+
if result:
|
|
111
|
+
results[check_name] = result
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.error(f"Error running check {check_name}: {e}", exc_info=True)
|
|
114
|
+
results[check_name] = HealthCheckResult(
|
|
115
|
+
name=check_name,
|
|
116
|
+
status="unhealthy",
|
|
117
|
+
score=0.0,
|
|
118
|
+
message=f"Check failed with error: {e}",
|
|
119
|
+
details={"error": str(e)},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
# Run dependent checks sequentially (respecting dependencies)
|
|
123
|
+
execution_order = self.registry._topological_sort(dependent_checks)
|
|
124
|
+
for name in execution_order:
|
|
125
|
+
if name not in results:
|
|
126
|
+
result = self.registry.run(name)
|
|
127
|
+
if result:
|
|
128
|
+
results[name] = result
|
|
129
|
+
|
|
130
|
+
# Save metrics
|
|
131
|
+
for result in results.values():
|
|
132
|
+
if result:
|
|
133
|
+
self.metrics_collector.record_health_check_result(result)
|
|
134
|
+
|
|
135
|
+
return results
|
|
136
|
+
|
|
137
|
+
def get_overall_health(self, results: dict[str, HealthCheckResult] | None = None) -> dict[str, Any]:
|
|
138
|
+
"""
|
|
139
|
+
Calculate overall health status from check results.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
results: Optional check results (runs all checks if None)
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Dictionary with overall health information
|
|
146
|
+
"""
|
|
147
|
+
if results is None:
|
|
148
|
+
results = self.run_all_checks(save_metrics=True)
|
|
149
|
+
|
|
150
|
+
if not results:
|
|
151
|
+
return {
|
|
152
|
+
"status": "unknown",
|
|
153
|
+
"score": 0.0,
|
|
154
|
+
"message": "No health checks available",
|
|
155
|
+
"checks_count": 0,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# Calculate weighted average score
|
|
159
|
+
# Critical checks (environment, execution) have higher weight
|
|
160
|
+
critical_checks = {"environment", "execution"}
|
|
161
|
+
total_weight = 0.0
|
|
162
|
+
weighted_score = 0.0
|
|
163
|
+
|
|
164
|
+
status_counts = {"healthy": 0, "degraded": 0, "unhealthy": 0}
|
|
165
|
+
|
|
166
|
+
for name, result in results.items():
|
|
167
|
+
if not result:
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
weight = 2.0 if name in critical_checks else 1.0
|
|
171
|
+
total_weight += weight
|
|
172
|
+
weighted_score += result.score * weight
|
|
173
|
+
|
|
174
|
+
status_counts[result.status] = status_counts.get(result.status, 0) + 1
|
|
175
|
+
|
|
176
|
+
overall_score = weighted_score / total_weight if total_weight > 0 else 0.0
|
|
177
|
+
|
|
178
|
+
# Determine overall status (HM-001-S3: degraded when score >= 75 and only non-critical unhealthy)
|
|
179
|
+
critical_checks = {"environment", "execution"}
|
|
180
|
+
non_critical_checks = {"outcomes", "knowledge_base", "context7_cache", "automation"}
|
|
181
|
+
unhealthy_checks = [
|
|
182
|
+
name
|
|
183
|
+
for name, result in results.items()
|
|
184
|
+
if result and result.status == "unhealthy"
|
|
185
|
+
]
|
|
186
|
+
critical_healthy = all(
|
|
187
|
+
(results.get(name) and results[name].status != "unhealthy")
|
|
188
|
+
for name in critical_checks
|
|
189
|
+
if name in results
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
status_reason: str | None = None
|
|
193
|
+
if status_counts["unhealthy"] > 0:
|
|
194
|
+
overall_status = "unhealthy"
|
|
195
|
+
if (
|
|
196
|
+
overall_score >= 75.0
|
|
197
|
+
and critical_healthy
|
|
198
|
+
and unhealthy_checks
|
|
199
|
+
and all(c in non_critical_checks for c in unhealthy_checks)
|
|
200
|
+
):
|
|
201
|
+
overall_status = "degraded"
|
|
202
|
+
status_reason = (
|
|
203
|
+
"Status degraded due to non-critical checks; core functionality is healthy"
|
|
204
|
+
)
|
|
205
|
+
elif status_counts["degraded"] > 0:
|
|
206
|
+
overall_status = "degraded"
|
|
207
|
+
else:
|
|
208
|
+
overall_status = "healthy"
|
|
209
|
+
|
|
210
|
+
# Build remediation list (prioritized)
|
|
211
|
+
all_remediations: list[str] = []
|
|
212
|
+
for name, result in results.items():
|
|
213
|
+
if result and result.remediation:
|
|
214
|
+
if isinstance(result.remediation, list):
|
|
215
|
+
all_remediations.extend(result.remediation)
|
|
216
|
+
elif isinstance(result.remediation, str):
|
|
217
|
+
all_remediations.append(result.remediation)
|
|
218
|
+
|
|
219
|
+
# Deduplicate and prioritize
|
|
220
|
+
unique_remediations = []
|
|
221
|
+
seen = set()
|
|
222
|
+
for rem in all_remediations:
|
|
223
|
+
rem_lower = rem.lower()
|
|
224
|
+
if rem_lower not in seen:
|
|
225
|
+
seen.add(rem_lower)
|
|
226
|
+
unique_remediations.append(rem)
|
|
227
|
+
|
|
228
|
+
# Prioritize: unhealthy checks first, then degraded
|
|
229
|
+
prioritized_remediations = []
|
|
230
|
+
for name, result in results.items():
|
|
231
|
+
if result and result.remediation and result.status == "unhealthy":
|
|
232
|
+
if isinstance(result.remediation, list):
|
|
233
|
+
prioritized_remediations.extend(result.remediation)
|
|
234
|
+
else:
|
|
235
|
+
prioritized_remediations.append(result.remediation)
|
|
236
|
+
|
|
237
|
+
for name, result in results.items():
|
|
238
|
+
if result and result.remediation and result.status == "degraded":
|
|
239
|
+
if isinstance(result.remediation, list):
|
|
240
|
+
prioritized_remediations.extend(result.remediation)
|
|
241
|
+
else:
|
|
242
|
+
prioritized_remediations.append(result.remediation)
|
|
243
|
+
|
|
244
|
+
# Add remaining unique remediations
|
|
245
|
+
for rem in unique_remediations:
|
|
246
|
+
if rem not in prioritized_remediations:
|
|
247
|
+
prioritized_remediations.append(rem)
|
|
248
|
+
|
|
249
|
+
details: dict[str, Any] = {}
|
|
250
|
+
if status_reason:
|
|
251
|
+
details["status_reason"] = status_reason
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
"status": overall_status,
|
|
255
|
+
"score": overall_score,
|
|
256
|
+
"message": f"Overall health: {overall_status} ({overall_score:.1f}/100)",
|
|
257
|
+
"checks_count": len(results),
|
|
258
|
+
"status_counts": status_counts,
|
|
259
|
+
"details": details,
|
|
260
|
+
"checks": {
|
|
261
|
+
name: {
|
|
262
|
+
"status": result.status,
|
|
263
|
+
"score": result.score,
|
|
264
|
+
"message": result.message,
|
|
265
|
+
}
|
|
266
|
+
for name, result in results.items()
|
|
267
|
+
if result
|
|
268
|
+
},
|
|
269
|
+
"remediation": prioritized_remediations[:5], # Top 5 remediation actions
|
|
270
|
+
}
|
|
271
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Claude Desktop integration resources."""
|