tapps-agents 3.5.39__py3-none-any.whl → 3.5.40__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/enhancer/agent.py +2728 -2728
- tapps_agents/agents/implementer/agent.py +35 -13
- tapps_agents/agents/reviewer/agent.py +43 -10
- tapps_agents/agents/reviewer/scoring.py +59 -68
- tapps_agents/agents/reviewer/tools/__init__.py +24 -0
- tapps_agents/agents/reviewer/tools/ruff_grouping.py +250 -0
- tapps_agents/agents/reviewer/tools/scoped_mypy.py +284 -0
- tapps_agents/beads/__init__.py +11 -0
- tapps_agents/beads/hydration.py +213 -0
- tapps_agents/beads/specs.py +206 -0
- tapps_agents/cli/commands/health.py +19 -3
- tapps_agents/cli/commands/simple_mode.py +842 -676
- tapps_agents/cli/commands/task.py +219 -0
- tapps_agents/cli/commands/top_level.py +13 -0
- tapps_agents/cli/main.py +658 -651
- tapps_agents/cli/parsers/top_level.py +1978 -1881
- tapps_agents/core/config.py +1622 -1622
- tapps_agents/core/init_project.py +3012 -2897
- tapps_agents/epic/markdown_sync.py +105 -0
- tapps_agents/epic/orchestrator.py +1 -2
- tapps_agents/epic/parser.py +427 -423
- tapps_agents/experts/adaptive_domain_detector.py +0 -2
- tapps_agents/experts/knowledge/api-design-integration/api-security-patterns.md +15 -15
- tapps_agents/experts/knowledge/api-design-integration/external-api-integration.md +19 -44
- tapps_agents/health/checks/outcomes.backup_20260204_064058.py +324 -0
- tapps_agents/health/checks/outcomes.backup_20260204_064256.py +324 -0
- tapps_agents/health/checks/outcomes.backup_20260204_064600.py +324 -0
- tapps_agents/health/checks/outcomes.py +134 -46
- tapps_agents/health/orchestrator.py +12 -4
- tapps_agents/hooks/__init__.py +33 -0
- tapps_agents/hooks/config.py +140 -0
- tapps_agents/hooks/events.py +135 -0
- tapps_agents/hooks/executor.py +128 -0
- tapps_agents/hooks/manager.py +143 -0
- tapps_agents/session/__init__.py +19 -0
- tapps_agents/session/manager.py +256 -0
- tapps_agents/simple_mode/code_snippet_handler.py +382 -0
- tapps_agents/simple_mode/intent_parser.py +29 -4
- tapps_agents/simple_mode/orchestrators/base.py +185 -59
- tapps_agents/simple_mode/orchestrators/build_orchestrator.py +2667 -2642
- tapps_agents/simple_mode/orchestrators/fix_orchestrator.py +2 -2
- tapps_agents/simple_mode/workflow_suggester.py +37 -3
- tapps_agents/workflow/agent_handlers/implementer_handler.py +18 -3
- tapps_agents/workflow/cursor_executor.py +2196 -2118
- tapps_agents/workflow/direct_execution_fallback.py +16 -3
- tapps_agents/workflow/message_formatter.py +2 -1
- tapps_agents/workflow/parallel_executor.py +43 -4
- tapps_agents/workflow/parser.py +375 -357
- tapps_agents/workflow/rules_generator.py +337 -337
- tapps_agents/workflow/skill_invoker.py +9 -3
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/METADATA +5 -1
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/RECORD +57 -53
- tapps_agents/agents/analyst/SKILL.md +0 -85
- tapps_agents/agents/architect/SKILL.md +0 -80
- tapps_agents/agents/debugger/SKILL.md +0 -66
- tapps_agents/agents/designer/SKILL.md +0 -78
- tapps_agents/agents/documenter/SKILL.md +0 -95
- tapps_agents/agents/enhancer/SKILL.md +0 -189
- tapps_agents/agents/implementer/SKILL.md +0 -117
- tapps_agents/agents/improver/SKILL.md +0 -55
- tapps_agents/agents/ops/SKILL.md +0 -64
- tapps_agents/agents/orchestrator/SKILL.md +0 -238
- tapps_agents/agents/planner/story_template.md +0 -37
- tapps_agents/agents/reviewer/templates/quality-dashboard.html.j2 +0 -150
- tapps_agents/agents/tester/SKILL.md +0 -71
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/WHEEL +0 -0
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/entry_points.txt +0 -0
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/licenses/LICENSE +0 -0
- {tapps_agents-3.5.39.dist-info → tapps_agents-3.5.40.dist-info}/top_level.txt +0 -0
|
@@ -47,7 +47,6 @@ class AdaptiveDomainDetector:
|
|
|
47
47
|
"refresh token",
|
|
48
48
|
"token expiry",
|
|
49
49
|
"access token refresh",
|
|
50
|
-
"zoho-oauthtoken",
|
|
51
50
|
],
|
|
52
51
|
"api-clients": [
|
|
53
52
|
"api client",
|
|
@@ -89,7 +88,6 @@ class AdaptiveDomainDetector:
|
|
|
89
88
|
r"access_token",
|
|
90
89
|
r"token_url",
|
|
91
90
|
r"expires_in",
|
|
92
|
-
r"Zoho-oauthtoken",
|
|
93
91
|
r"Bearer\s+token",
|
|
94
92
|
],
|
|
95
93
|
"api-clients": [
|
|
@@ -86,8 +86,8 @@ import requests
|
|
|
86
86
|
class OAuth2RefreshTokenClient:
|
|
87
87
|
"""
|
|
88
88
|
OAuth2 client using refresh-token flow for long-lived API access.
|
|
89
|
-
|
|
90
|
-
This pattern is used by many SaaS APIs
|
|
89
|
+
|
|
90
|
+
This pattern is used by many SaaS APIs that require
|
|
91
91
|
long-term access without user re-authentication.
|
|
92
92
|
"""
|
|
93
93
|
|
|
@@ -198,11 +198,11 @@ class OAuth2RefreshTokenClient:
|
|
|
198
198
|
|
|
199
199
|
**Best Practices:**
|
|
200
200
|
- **Refresh proactively:** Refresh tokens 60 seconds before expiry to avoid race conditions
|
|
201
|
-
- **Handle both expiry formats:** Some providers use `expires_in_sec`, others use `expires_in` (
|
|
201
|
+
- **Handle both expiry formats:** Some providers use `expires_in_sec`, others use `expires_in` (handle both field names)
|
|
202
202
|
- **Cache access tokens:** Store tokens until near expiry to reduce API calls
|
|
203
203
|
- **Secure storage:** Use environment variables or secret managers for refresh tokens (never hardcode)
|
|
204
204
|
- **Error handling:** Handle token refresh failures gracefully (retry, exponential backoff)
|
|
205
|
-
- **Multi-region support:** Some providers
|
|
205
|
+
- **Multi-region support:** Some providers have different endpoints for different data centers/regions
|
|
206
206
|
|
|
207
207
|
**Example Usage:**
|
|
208
208
|
```python
|
|
@@ -211,8 +211,8 @@ client = OAuth2RefreshTokenClient(
|
|
|
211
211
|
client_id=os.environ["OAUTH_CLIENT_ID"],
|
|
212
212
|
client_secret=os.environ["OAUTH_CLIENT_SECRET"],
|
|
213
213
|
refresh_token=os.environ["OAUTH_REFRESH_TOKEN"],
|
|
214
|
-
token_url="https://
|
|
215
|
-
api_base_url="https://
|
|
214
|
+
token_url="https://api.example.com/oauth/v2/token",
|
|
215
|
+
api_base_url="https://api.example.com/v1",
|
|
216
216
|
)
|
|
217
217
|
|
|
218
218
|
# Token refresh happens automatically
|
|
@@ -288,24 +288,24 @@ context.load_verify_locations('ca.crt')
|
|
|
288
288
|
**Overview:** Some APIs use non-standard authentication headers instead of the standard `Authorization: Bearer <token>` format.
|
|
289
289
|
|
|
290
290
|
**Common Custom Headers:**
|
|
291
|
-
- `Authorization: Zoho-oauthtoken <token>` (Zoho/Site24x7)
|
|
292
291
|
- `Authorization: Bearer <token>` (standard OAuth2)
|
|
293
292
|
- `X-API-Key: <key>` (API key authentication)
|
|
294
293
|
- `Authorization: Token <token>` (GitHub-style)
|
|
294
|
+
- `Authorization: <custom-prefix> <token>` (vendor-specific formats)
|
|
295
295
|
|
|
296
296
|
**Implementation:**
|
|
297
297
|
```python
|
|
298
298
|
def _headers(self) -> dict[str, str]:
|
|
299
299
|
"""
|
|
300
300
|
Get HTTP headers for authenticated requests.
|
|
301
|
-
|
|
301
|
+
|
|
302
302
|
Supports custom auth header formats based on API requirements.
|
|
303
303
|
"""
|
|
304
304
|
token = self._get_access_token()
|
|
305
|
-
|
|
306
|
-
# Custom header format (
|
|
305
|
+
|
|
306
|
+
# Custom header format (vendor-specific)
|
|
307
307
|
return {
|
|
308
|
-
"Authorization": f"
|
|
308
|
+
"Authorization": f"CustomPrefix {token}", # Replace with API-specific format
|
|
309
309
|
"Accept": "application/json",
|
|
310
310
|
}
|
|
311
311
|
|
|
@@ -329,14 +329,14 @@ def _headers_standard(self) -> dict[str, str]:
|
|
|
329
329
|
```python
|
|
330
330
|
class FlexibleOAuth2Client:
|
|
331
331
|
"""OAuth2 client that supports multiple auth header formats."""
|
|
332
|
-
|
|
332
|
+
|
|
333
333
|
def __init__(self, auth_header_format: str = "Bearer"):
|
|
334
334
|
"""
|
|
335
335
|
Args:
|
|
336
|
-
auth_header_format: Header format - "Bearer", "
|
|
336
|
+
auth_header_format: Header format - "Bearer", "Token", "CustomPrefix", etc.
|
|
337
337
|
"""
|
|
338
338
|
self.auth_header_format = auth_header_format
|
|
339
|
-
|
|
339
|
+
|
|
340
340
|
def _headers(self) -> dict[str, str]:
|
|
341
341
|
token = self._get_access_token()
|
|
342
342
|
return {
|
|
@@ -345,7 +345,7 @@ class FlexibleOAuth2Client:
|
|
|
345
345
|
}
|
|
346
346
|
|
|
347
347
|
# Usage:
|
|
348
|
-
|
|
348
|
+
custom_client = FlexibleOAuth2Client(auth_header_format="CustomPrefix")
|
|
349
349
|
github_client = FlexibleOAuth2Client(auth_header_format="Token")
|
|
350
350
|
standard_client = FlexibleOAuth2Client(auth_header_format="Bearer")
|
|
351
351
|
```
|
|
@@ -227,7 +227,7 @@ class ExternalAPIManager:
|
|
|
227
227
|
|
|
228
228
|
## OAuth2-Based External APIs
|
|
229
229
|
|
|
230
|
-
Many SaaS APIs
|
|
230
|
+
Many SaaS APIs use OAuth2 refresh-token flows for long-lived API access without user re-authentication.
|
|
231
231
|
|
|
232
232
|
### Pattern: OAuth2 Refresh-Token Client
|
|
233
233
|
|
|
@@ -302,7 +302,7 @@ class OAuth2ExternalAPIClient:
|
|
|
302
302
|
def _headers(self) -> dict[str, str]:
|
|
303
303
|
"""Get HTTP headers for authenticated requests."""
|
|
304
304
|
token = self._get_access_token()
|
|
305
|
-
# Note: Some APIs use custom headers
|
|
305
|
+
# Note: Some APIs use custom headers
|
|
306
306
|
# See api-security-patterns.md for custom header patterns
|
|
307
307
|
return {
|
|
308
308
|
"Authorization": f"Bearer {token}", # Standard OAuth2
|
|
@@ -337,54 +337,29 @@ class OAuth2ExternalAPIClient:
|
|
|
337
337
|
return resp.json()
|
|
338
338
|
```
|
|
339
339
|
|
|
340
|
-
###
|
|
340
|
+
### Example Usage
|
|
341
341
|
|
|
342
|
-
#### Zoho/Site24x7
|
|
343
342
|
```python
|
|
343
|
+
# Configure with your API's specific endpoints
|
|
344
344
|
client = OAuth2ExternalAPIClient(
|
|
345
|
-
client_id=os.environ["
|
|
346
|
-
client_secret=os.environ["
|
|
347
|
-
refresh_token=os.environ["
|
|
348
|
-
api_base_url="https://
|
|
349
|
-
token_url="https://
|
|
345
|
+
client_id=os.environ["API_CLIENT_ID"],
|
|
346
|
+
client_secret=os.environ["API_CLIENT_SECRET"],
|
|
347
|
+
refresh_token=os.environ["API_REFRESH_TOKEN"],
|
|
348
|
+
api_base_url="https://api.example.com/v1",
|
|
349
|
+
token_url="https://api.example.com/oauth/v2/token",
|
|
350
350
|
)
|
|
351
351
|
|
|
352
|
-
#
|
|
352
|
+
# For APIs with custom header formats:
|
|
353
353
|
# Override _headers() method for custom header format:
|
|
354
354
|
def _headers(self) -> dict[str, str]:
|
|
355
355
|
token = self._get_access_token()
|
|
356
356
|
return {
|
|
357
|
-
"Authorization": f"
|
|
357
|
+
"Authorization": f"CustomPrefix {token}", # Custom header format
|
|
358
358
|
"Accept": "application/json",
|
|
359
359
|
}
|
|
360
360
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
#### Okta
|
|
365
|
-
```python
|
|
366
|
-
client = OAuth2ExternalAPIClient(
|
|
367
|
-
client_id=os.environ["OKTA_CLIENT_ID"],
|
|
368
|
-
client_secret=os.environ["OKTA_CLIENT_SECRET"],
|
|
369
|
-
refresh_token=os.environ["OKTA_REFRESH_TOKEN"],
|
|
370
|
-
api_base_url=f"https://{org}.okta.com/api/v1",
|
|
371
|
-
token_url=f"https://{org}.okta.com/oauth2/v1/token",
|
|
372
|
-
)
|
|
373
|
-
|
|
374
|
-
users = client.get("/users")
|
|
375
|
-
```
|
|
376
|
-
|
|
377
|
-
#### Salesforce
|
|
378
|
-
```python
|
|
379
|
-
client = OAuth2ExternalAPIClient(
|
|
380
|
-
client_id=os.environ["SALESFORCE_CLIENT_ID"],
|
|
381
|
-
client_secret=os.environ["SALESFORCE_CLIENT_SECRET"],
|
|
382
|
-
refresh_token=os.environ["SALESFORCE_REFRESH_TOKEN"],
|
|
383
|
-
api_base_url="https://yourinstance.salesforce.com/services/data/v58.0",
|
|
384
|
-
token_url="https://login.salesforce.com/services/oauth2/token",
|
|
385
|
-
)
|
|
386
|
-
|
|
387
|
-
accounts = client.get("/sobjects/Account")
|
|
361
|
+
# Make authenticated requests
|
|
362
|
+
data = client.get("/resource")
|
|
388
363
|
```
|
|
389
364
|
|
|
390
365
|
### Best Practices for OAuth2 External APIs
|
|
@@ -413,15 +388,15 @@ accounts = client.get("/sobjects/Account")
|
|
|
413
388
|
# Some providers have different endpoints for different regions
|
|
414
389
|
REGIONS = {
|
|
415
390
|
"us": {
|
|
416
|
-
"api_base_url": "https://
|
|
417
|
-
"token_url": "https://
|
|
391
|
+
"api_base_url": "https://api.example.com/v1",
|
|
392
|
+
"token_url": "https://auth.example.com/oauth/v2/token",
|
|
418
393
|
},
|
|
419
394
|
"eu": {
|
|
420
|
-
"api_base_url": "https://
|
|
421
|
-
"token_url": "https://
|
|
395
|
+
"api_base_url": "https://api.example.eu/v1",
|
|
396
|
+
"token_url": "https://auth.example.eu/oauth/v2/token",
|
|
422
397
|
},
|
|
423
398
|
}
|
|
424
|
-
|
|
399
|
+
|
|
425
400
|
client = OAuth2ExternalAPIClient(
|
|
426
401
|
...,
|
|
427
402
|
**REGIONS["eu"], # Use EU endpoints
|
|
@@ -429,7 +404,7 @@ accounts = client.get("/sobjects/Account")
|
|
|
429
404
|
```
|
|
430
405
|
|
|
431
406
|
4. **Custom Header Formats:**
|
|
432
|
-
- Some APIs
|
|
407
|
+
- Some APIs use non-standard auth headers (e.g., GitHub's "Token" format)
|
|
433
408
|
- Make header format configurable or override `_headers()` method
|
|
434
409
|
- See `api-security-patterns.md` section "Custom Authentication Headers" for details
|
|
435
410
|
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Outcome Health Check.
|
|
3
|
+
|
|
4
|
+
Checks quality trends and improvement metrics.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from datetime import datetime, timedelta
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from ...workflow.analytics_dashboard_cursor import CursorAnalyticsAccessor
|
|
14
|
+
from ...workflow.review_artifact import ReviewArtifact
|
|
15
|
+
from ..base import HealthCheck, HealthCheckResult
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class OutcomeHealthCheck(HealthCheck):
|
|
19
|
+
"""Health check for quality trends and outcomes."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, project_root: Path | None = None, reports_dir: Path | None = None):
|
|
22
|
+
"""
|
|
23
|
+
Initialize outcome health check.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
project_root: Project root directory
|
|
27
|
+
reports_dir: Reports directory (defaults to .tapps-agents/reports)
|
|
28
|
+
"""
|
|
29
|
+
super().__init__(name="outcomes", dependencies=["environment", "execution"])
|
|
30
|
+
self.project_root = project_root or Path.cwd()
|
|
31
|
+
self.reports_dir = reports_dir or (self.project_root / ".tapps-agents" / "reports")
|
|
32
|
+
self.accessor = CursorAnalyticsAccessor()
|
|
33
|
+
|
|
34
|
+
def _compute_outcomes_from_execution_metrics(self, days: int = 30) -> dict:
|
|
35
|
+
"""
|
|
36
|
+
Compute outcomes from execution metrics when review artifacts don't exist.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
days: Number of days to look back for metrics
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Dictionary with review_executions_count, success_rate, and gate_pass_rate
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
from datetime import UTC
|
|
46
|
+
from ...workflow.execution_metrics import ExecutionMetricsCollector
|
|
47
|
+
import logging
|
|
48
|
+
|
|
49
|
+
collector = ExecutionMetricsCollector(project_root=self.project_root)
|
|
50
|
+
|
|
51
|
+
# Get metrics with reasonable limit (5000 max for ~30 days of heavy usage)
|
|
52
|
+
MAX_METRICS_TO_SCAN = 5000
|
|
53
|
+
all_metrics = collector.get_metrics(limit=MAX_METRICS_TO_SCAN)
|
|
54
|
+
|
|
55
|
+
# Log warning if we hit the limit
|
|
56
|
+
if len(all_metrics) >= MAX_METRICS_TO_SCAN:
|
|
57
|
+
logging.getLogger(__name__).warning(
|
|
58
|
+
"Hit metrics scan limit (%d); results may be incomplete",
|
|
59
|
+
MAX_METRICS_TO_SCAN
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Filter for review executions within the last N days (timezone-aware)
|
|
63
|
+
cutoff_date = datetime.now(UTC) - timedelta(days=days)
|
|
64
|
+
review_metrics = []
|
|
65
|
+
for m in all_metrics:
|
|
66
|
+
# Parse timestamp and ensure timezone-aware comparison
|
|
67
|
+
try:
|
|
68
|
+
ts = datetime.fromisoformat(m.started_at.replace("Z", "+00:00"))
|
|
69
|
+
# Convert naive datetime to UTC if needed
|
|
70
|
+
if ts.tzinfo is None:
|
|
71
|
+
from datetime import UTC
|
|
72
|
+
ts = ts.replace(tzinfo=UTC)
|
|
73
|
+
|
|
74
|
+
if ts >= cutoff_date:
|
|
75
|
+
if m.command == "review" or (m.skill and "reviewer" in (m.skill or "").lower()):
|
|
76
|
+
review_metrics.append(m)
|
|
77
|
+
except (ValueError, AttributeError):
|
|
78
|
+
# Skip metrics with invalid timestamps
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
if not review_metrics:
|
|
82
|
+
return {
|
|
83
|
+
"review_executions_count": 0,
|
|
84
|
+
"success_rate": 0.0,
|
|
85
|
+
"gate_pass_rate": None,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
total = len(review_metrics)
|
|
89
|
+
success_count = sum(1 for m in review_metrics if m.status == "success")
|
|
90
|
+
success_rate = (success_count / total * 100) if total > 0 else 0.0
|
|
91
|
+
|
|
92
|
+
# Calculate gate pass rate (only for metrics that have gate_pass field)
|
|
93
|
+
gate_pass_metrics = [m for m in review_metrics if m.gate_pass is not None]
|
|
94
|
+
if gate_pass_metrics:
|
|
95
|
+
gate_pass_count = sum(1 for m in gate_pass_metrics if m.gate_pass is True)
|
|
96
|
+
gate_pass_rate = (gate_pass_count / len(gate_pass_metrics) * 100)
|
|
97
|
+
else:
|
|
98
|
+
gate_pass_rate = None
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
"review_executions_count": total,
|
|
102
|
+
"success_rate": success_rate,
|
|
103
|
+
"gate_pass_rate": gate_pass_rate,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
# If fallback fails, log and return empty result
|
|
108
|
+
import logging
|
|
109
|
+
logging.getLogger(__name__).debug(
|
|
110
|
+
"Failed to compute outcomes from execution metrics: %s", e
|
|
111
|
+
)
|
|
112
|
+
return {
|
|
113
|
+
"review_executions_count": 0,
|
|
114
|
+
"success_rate": 0.0,
|
|
115
|
+
"gate_pass_rate": None,
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
def run(self) -> HealthCheckResult:
|
|
119
|
+
"""
|
|
120
|
+
Run outcome health check.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
HealthCheckResult with outcome trends
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
# Get analytics data for trends
|
|
127
|
+
dashboard_data = self.accessor.get_dashboard_data()
|
|
128
|
+
agents_data = dashboard_data.get("agents", [])
|
|
129
|
+
workflows_data = dashboard_data.get("workflows", [])
|
|
130
|
+
|
|
131
|
+
# Look for review artifacts in reports directory
|
|
132
|
+
review_artifacts = []
|
|
133
|
+
if self.reports_dir.exists():
|
|
134
|
+
for artifact_file in self.reports_dir.rglob("review_*.json"):
|
|
135
|
+
try:
|
|
136
|
+
with open(artifact_file, encoding="utf-8") as f:
|
|
137
|
+
data = json.load(f)
|
|
138
|
+
artifact = ReviewArtifact.from_dict(data)
|
|
139
|
+
if artifact.overall_score is not None:
|
|
140
|
+
review_artifacts.append(artifact)
|
|
141
|
+
except Exception:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# Calculate trends from review artifacts
|
|
145
|
+
score_trend = "unknown"
|
|
146
|
+
avg_score = 0.0
|
|
147
|
+
score_change = 0.0
|
|
148
|
+
|
|
149
|
+
if review_artifacts:
|
|
150
|
+
# Sort by timestamp
|
|
151
|
+
review_artifacts.sort(key=lambda a: a.timestamp)
|
|
152
|
+
|
|
153
|
+
# Get recent artifacts (last 30 days)
|
|
154
|
+
thirty_days_ago = datetime.now() - timedelta(days=30)
|
|
155
|
+
recent_artifacts = [
|
|
156
|
+
a
|
|
157
|
+
for a in review_artifacts
|
|
158
|
+
if datetime.fromisoformat(a.timestamp.replace("Z", "+00:00")) >= thirty_days_ago
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
if recent_artifacts:
|
|
162
|
+
scores = [a.overall_score for a in recent_artifacts if a.overall_score is not None]
|
|
163
|
+
if scores:
|
|
164
|
+
avg_score = sum(scores) / len(scores)
|
|
165
|
+
|
|
166
|
+
# Calculate trend (compare first half to second half)
|
|
167
|
+
if len(scores) >= 4:
|
|
168
|
+
first_half = scores[: len(scores) // 2]
|
|
169
|
+
second_half = scores[len(scores) // 2 :]
|
|
170
|
+
first_avg = sum(first_half) / len(first_half)
|
|
171
|
+
second_avg = sum(second_half) / len(second_half)
|
|
172
|
+
score_change = second_avg - first_avg
|
|
173
|
+
|
|
174
|
+
if score_change > 5.0:
|
|
175
|
+
score_trend = "improving"
|
|
176
|
+
elif score_change < -5.0:
|
|
177
|
+
score_trend = "degrading"
|
|
178
|
+
else:
|
|
179
|
+
score_trend = "stable"
|
|
180
|
+
|
|
181
|
+
# Count quality improvement workflows
|
|
182
|
+
quality_workflows = [
|
|
183
|
+
w
|
|
184
|
+
for w in workflows_data
|
|
185
|
+
if "quality" in w.get("workflow_name", "").lower()
|
|
186
|
+
or "improve" in w.get("workflow_name", "").lower()
|
|
187
|
+
]
|
|
188
|
+
improvement_cycles = len(quality_workflows)
|
|
189
|
+
|
|
190
|
+
# Calculate health score
|
|
191
|
+
score = 100.0
|
|
192
|
+
issues = []
|
|
193
|
+
remediation = []
|
|
194
|
+
|
|
195
|
+
# Check if we have any data; if not, try fallback to execution metrics (review steps)
|
|
196
|
+
if not review_artifacts and not agents_data:
|
|
197
|
+
# Fallback: derive outcomes from execution metrics (review steps, gate_pass)
|
|
198
|
+
import logging
|
|
199
|
+
fallback_data = self._compute_outcomes_from_execution_metrics(days=30)
|
|
200
|
+
|
|
201
|
+
if fallback_data["review_executions_count"] > 0:
|
|
202
|
+
total = fallback_data["review_executions_count"]
|
|
203
|
+
success_rate = fallback_data["success_rate"]
|
|
204
|
+
gate_pass_rate = fallback_data["gate_pass_rate"]
|
|
205
|
+
|
|
206
|
+
# Calculate score: 60 base + 10 if success_rate ≥80% + 5 if gate_pass_rate ≥70%
|
|
207
|
+
fallback_score = 60.0
|
|
208
|
+
if success_rate >= 80.0:
|
|
209
|
+
fallback_score += 10.0
|
|
210
|
+
if gate_pass_rate is not None and gate_pass_rate >= 70.0:
|
|
211
|
+
fallback_score += 5.0
|
|
212
|
+
|
|
213
|
+
# Build message
|
|
214
|
+
gate_msg = f"{gate_pass_rate:.0f}% passed gate" if gate_pass_rate is not None else "no gate data"
|
|
215
|
+
message = (
|
|
216
|
+
f"Outcomes derived from execution metrics: {total} review steps, "
|
|
217
|
+
f"{gate_msg}"
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
logging.getLogger(__name__).info(
|
|
221
|
+
"Outcomes fallback activated: %d review executions processed", total
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
return HealthCheckResult(
|
|
225
|
+
name=self.name,
|
|
226
|
+
status="degraded",
|
|
227
|
+
score=fallback_score,
|
|
228
|
+
message=message,
|
|
229
|
+
details={
|
|
230
|
+
"average_score": 0.0,
|
|
231
|
+
"score_trend": "unknown",
|
|
232
|
+
"score_change": 0.0,
|
|
233
|
+
"review_artifacts_count": 0,
|
|
234
|
+
"improvement_cycles": 0,
|
|
235
|
+
"reports_dir": str(self.reports_dir),
|
|
236
|
+
"fallback_used": True,
|
|
237
|
+
"fallback_source": "execution_metrics",
|
|
238
|
+
"review_executions_count": total,
|
|
239
|
+
"success_rate": success_rate,
|
|
240
|
+
"gate_pass_rate": gate_pass_rate,
|
|
241
|
+
"issues": [],
|
|
242
|
+
},
|
|
243
|
+
remediation=[
|
|
244
|
+
"Run reviewer agent or quality workflows to generate review artifacts"
|
|
245
|
+
],
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
score = 50.0
|
|
249
|
+
issues.append("No quality metrics available")
|
|
250
|
+
remediation.append("Run reviewer agent or quality workflows to generate metrics")
|
|
251
|
+
else:
|
|
252
|
+
# Check score trend
|
|
253
|
+
if score_trend == "degrading":
|
|
254
|
+
score -= 20.0
|
|
255
|
+
issues.append(f"Quality scores declining: {score_change:.1f} point change")
|
|
256
|
+
remediation.append("Investigate recent code changes causing quality decline")
|
|
257
|
+
elif score_trend == "improving":
|
|
258
|
+
# Bonus for improvement
|
|
259
|
+
score = min(100.0, score + 5.0)
|
|
260
|
+
|
|
261
|
+
# Check average score
|
|
262
|
+
if avg_score > 0:
|
|
263
|
+
if avg_score < 60.0:
|
|
264
|
+
score -= 30.0
|
|
265
|
+
issues.append(f"Low average quality score: {avg_score:.1f}/100")
|
|
266
|
+
remediation.append("Run quality improvement workflows")
|
|
267
|
+
elif avg_score < 75.0:
|
|
268
|
+
score -= 15.0
|
|
269
|
+
issues.append(f"Moderate quality score: {avg_score:.1f}/100")
|
|
270
|
+
|
|
271
|
+
# Check improvement activity
|
|
272
|
+
if improvement_cycles == 0:
|
|
273
|
+
score -= 10.0
|
|
274
|
+
issues.append("No quality improvement workflows run")
|
|
275
|
+
remediation.append("Run quality workflows to improve code quality")
|
|
276
|
+
|
|
277
|
+
# Determine status
|
|
278
|
+
if score >= 85.0:
|
|
279
|
+
status = "healthy"
|
|
280
|
+
elif score >= 70.0:
|
|
281
|
+
status = "degraded"
|
|
282
|
+
else:
|
|
283
|
+
status = "unhealthy"
|
|
284
|
+
|
|
285
|
+
# Build message
|
|
286
|
+
message_parts = []
|
|
287
|
+
if avg_score > 0:
|
|
288
|
+
message_parts.append(f"Avg score: {avg_score:.1f}")
|
|
289
|
+
if score_trend != "unknown":
|
|
290
|
+
message_parts.append(f"Trend: {score_trend}")
|
|
291
|
+
if improvement_cycles > 0:
|
|
292
|
+
message_parts.append(f"Improvements: {improvement_cycles}")
|
|
293
|
+
if not message_parts:
|
|
294
|
+
message = "No outcome data available"
|
|
295
|
+
else:
|
|
296
|
+
message = " | ".join(message_parts)
|
|
297
|
+
|
|
298
|
+
return HealthCheckResult(
|
|
299
|
+
name=self.name,
|
|
300
|
+
status=status,
|
|
301
|
+
score=max(0.0, score),
|
|
302
|
+
message=message,
|
|
303
|
+
details={
|
|
304
|
+
"average_score": avg_score,
|
|
305
|
+
"score_trend": score_trend,
|
|
306
|
+
"score_change": score_change,
|
|
307
|
+
"review_artifacts_count": len(review_artifacts),
|
|
308
|
+
"improvement_cycles": improvement_cycles,
|
|
309
|
+
"reports_dir": str(self.reports_dir),
|
|
310
|
+
"issues": issues,
|
|
311
|
+
},
|
|
312
|
+
remediation=remediation if remediation else None,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
return HealthCheckResult(
|
|
317
|
+
name=self.name,
|
|
318
|
+
status="unhealthy",
|
|
319
|
+
score=0.0,
|
|
320
|
+
message=f"Outcome check failed: {e}",
|
|
321
|
+
details={"error": str(e), "reports_dir": str(self.reports_dir)},
|
|
322
|
+
remediation=["Check reports directory and analytics access"],
|
|
323
|
+
)
|
|
324
|
+
|