@matt82198/aesop 0.1.0 → 0.2.0
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.
- package/CHANGELOG.md +89 -2
- package/README.md +63 -12
- package/bin/cli.js +164 -41
- package/daemons/run-watchdog.sh +16 -4
- package/daemons/selfheal.sh +231 -0
- package/docs/ANY-REPO.md +427 -0
- package/docs/CONTRIBUTING.md +72 -0
- package/docs/DEMO.md +334 -0
- package/docs/INSTALL.md +70 -1
- package/docs/QUICKSTART.md +80 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +540 -0
- package/driver/CLAUDE.md +148 -0
- package/driver/README.md +383 -0
- package/driver/aesop.config.example.json +80 -0
- package/driver/agent_driver.py +355 -0
- package/driver/backend_config.py +253 -0
- package/driver/claude_code_driver.py +198 -0
- package/driver/codex_driver.py +626 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +146 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +246 -0
- package/driver/wave_loop.py +1041 -0
- package/hooks/pre-push-policy.sh +109 -28
- package/mcp/server.mjs +320 -4
- package/package.json +23 -15
- package/skills/CLAUDE.md +132 -2
- package/skills/buildsystem/SKILL.md +330 -0
- package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
- package/skills/fleet/SKILL.md +113 -0
- package/skills/power/SKILL.md +246 -131
- package/state_store/__init__.py +2 -1
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/store.py +185 -73
- package/templates/wave-presets/data.json +65 -0
- package/templates/wave-presets/library.json +65 -0
- package/templates/wave-presets/saas.json +64 -0
- package/tools/audit_report.py +388 -0
- package/tools/bench_runner.py +100 -3
- package/tools/ci_merge_wait.py +256 -35
- package/tools/ci_workflow_lint.py +430 -0
- package/tools/claudemd_drift.py +394 -0
- package/tools/claudemd_lint.py +359 -0
- package/tools/common.py +39 -3
- package/tools/cost_ceiling.py +63 -31
- package/tools/cost_econ.py +480 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +58 -0
- package/tools/fleet.js +260 -0
- package/tools/fleet_ledger.py +209 -7
- package/tools/git_identity_check.py +315 -0
- package/tools/health-score.js +40 -0
- package/tools/health_score.py +361 -0
- package/tools/metrics_gate.py +13 -4
- package/tools/mutation_test.py +401 -0
- package/tools/portability_check.py +206 -0
- package/tools/reconcile.py +7 -4
- package/tools/secret_scan.py +137 -50
- package/tools/self_stats.py +20 -0
- package/tools/transcript_digest.py +380 -0
- package/tools/verify_activity_filter.py +437 -0
- package/tools/verify_agent_inspector.py +2 -0
- package/tools/verify_dash.py +2 -0
- package/tools/verify_dispatch_panel.py +301 -0
- package/tools/verify_failure_drilldown.py +188 -0
- package/tools/verify_prboard.py +2 -0
- package/tools/verify_scorecards.py +281 -0
- package/tools/verify_submit_encoding.py +2 -0
- package/tools/verify_ui_trio.py +409 -0
- package/tools/verify_wave_telemetry.py +268 -0
- package/tools/wave_backlog_analyzer.py +490 -0
- package/tools/wave_ledger_hook.py +150 -0
- package/tools/wave_preflight.py +512 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +215 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +0 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +183 -0
- package/ui/quality_scorecard.py +232 -0
- package/ui/wave_audit_tail.py +213 -0
- package/ui/wave_dispatch.py +280 -0
- package/ui/wave_failure.py +288 -0
- package/ui/wave_gantt.py +152 -0
- package/ui/wave_reasoning_tail.py +176 -0
- package/ui/wave_telemetry.py +377 -0
- package/ui/web/dist/assets/index-BGbgw2Nh.js +9 -0
- package/ui/web/dist/assets/index-DqZLgwNg.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/bin/CLAUDE.md +0 -76
- package/daemons/CLAUDE.md +0 -36
- package/dash/CLAUDE.md +0 -32
- package/docs/archive/README.md +0 -3
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
- package/docs/archive/spikes/tiered-cognition/README.md +0 -27
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
- package/hooks/CLAUDE.md +0 -89
- package/mcp/CLAUDE.md +0 -213
- package/monitor/CLAUDE.md +0 -40
- package/scan/CLAUDE.md +0 -30
- package/state_store/CLAUDE.md +0 -39
- package/tools/CLAUDE.md +0 -79
- package/ui/CLAUDE.md +0 -127
- package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
- package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Quality scorecard collector — per-agent-specialty success rates and retry frequencies.
|
|
3
|
+
|
|
4
|
+
This module provides get_quality_scorecard() which parses the outcomes ledger
|
|
5
|
+
and returns per-agent-specialty quality metrics: success rate (green/total)
|
|
6
|
+
and retry/repair frequency, derived from ledger agent_type + verdict columns.
|
|
7
|
+
|
|
8
|
+
Ledger format (same as cost.py):
|
|
9
|
+
| ISO timestamp | agent_type | model | duration | tokens_in | tokens_out | verdict |
|
|
10
|
+
| 2026-07-11T22:08:17 | haiku | claude-haiku-4-5-20251001 | 0 | 8 | 186 | OK |
|
|
11
|
+
|
|
12
|
+
QualityScorecard JSON shape (returned by get_quality_scorecard()):
|
|
13
|
+
{
|
|
14
|
+
"specialties": {
|
|
15
|
+
"agent-type": {
|
|
16
|
+
"total_runs": int,
|
|
17
|
+
"success_count": int,
|
|
18
|
+
"failed_count": int,
|
|
19
|
+
"empty_count": int,
|
|
20
|
+
"hung_count": int,
|
|
21
|
+
"success_rate": float (0.0-1.0),
|
|
22
|
+
"repair_count": int (consecutive failures followed by success),
|
|
23
|
+
"retry_frequency": float (repairs / total_runs)
|
|
24
|
+
},
|
|
25
|
+
...
|
|
26
|
+
},
|
|
27
|
+
"top_by_success": [
|
|
28
|
+
{"agent_type": str, "success_rate": float, "total_runs": int},
|
|
29
|
+
...
|
|
30
|
+
],
|
|
31
|
+
"top_by_retry": [
|
|
32
|
+
{"agent_type": str, "retry_frequency": float, "total_runs": int},
|
|
33
|
+
...
|
|
34
|
+
],
|
|
35
|
+
"skipped_lines": int
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Key behavior:
|
|
39
|
+
- Missing ledger file: returns empty summary with documented shape.
|
|
40
|
+
- Malformed lines: skipped and counted in skipped_lines field.
|
|
41
|
+
- Config read at CALL time (not import time) for test isolation.
|
|
42
|
+
- UTF-8 explicit encoding for all file operations.
|
|
43
|
+
- No external dependencies (pure stdlib).
|
|
44
|
+
- Retry frequency calculated by detecting "failure -> success" transitions (repair cycles).
|
|
45
|
+
- Rankings sorted by metric descending (highest success rate / highest retry frequency first).
|
|
46
|
+
"""
|
|
47
|
+
import json
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
|
|
50
|
+
import config
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_quality_scorecard():
|
|
54
|
+
"""Parse the outcomes ledger and return per-agent-specialty quality metrics.
|
|
55
|
+
|
|
56
|
+
Reads from the ledger path exposed by config (config.STATE_DIR/ledger/OUTCOMES-LEDGER.md).
|
|
57
|
+
Returns an empty summary with documented shape if ledger is missing or empty.
|
|
58
|
+
Malformed lines are skipped and counted in skipped_lines.
|
|
59
|
+
|
|
60
|
+
Extracts agent_type from the ledger (haiku, sonnet, opus, orchestrator, etc.) and
|
|
61
|
+
aggregates success rates and repair (retry) frequencies.
|
|
62
|
+
|
|
63
|
+
Repair cycles detected by counting consecutive failures followed by a success
|
|
64
|
+
in the same agent_type's history.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
dict: QualityScorecard with specialties, top_by_success, top_by_retry,
|
|
68
|
+
and skipped_lines (or error field if invalid).
|
|
69
|
+
"""
|
|
70
|
+
import sys
|
|
71
|
+
|
|
72
|
+
# Read ledger path at call time
|
|
73
|
+
ledger_file = config.STATE_DIR / "ledger" / "OUTCOMES-LEDGER.md"
|
|
74
|
+
|
|
75
|
+
# Initialize result structure
|
|
76
|
+
result = {
|
|
77
|
+
"specialties": {},
|
|
78
|
+
"top_by_success": [],
|
|
79
|
+
"top_by_retry": [],
|
|
80
|
+
"skipped_lines": 0,
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# If ledger file doesn't exist, return empty summary
|
|
84
|
+
if not ledger_file.exists():
|
|
85
|
+
return result
|
|
86
|
+
|
|
87
|
+
# Read and parse ledger with explicit UTF-8 encoding
|
|
88
|
+
try:
|
|
89
|
+
content = ledger_file.read_text(encoding='utf-8')
|
|
90
|
+
except Exception:
|
|
91
|
+
# Graceful: if read fails, return empty summary
|
|
92
|
+
return result
|
|
93
|
+
|
|
94
|
+
lines = content.strip().split('\n')
|
|
95
|
+
|
|
96
|
+
# Parse each line and collect per-agent-type history
|
|
97
|
+
agent_verdicts = {} # agent_type -> [verdict1, verdict2, ...]
|
|
98
|
+
|
|
99
|
+
for line in lines:
|
|
100
|
+
line = line.strip()
|
|
101
|
+
|
|
102
|
+
# Skip empty lines
|
|
103
|
+
if not line:
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
# Skip header separator lines (all dashes, pipes, and spaces)
|
|
107
|
+
if all(c in '|- ' for c in line):
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
# Parse pipe-delimited row
|
|
111
|
+
if not line.startswith('|') or not line.endswith('|'):
|
|
112
|
+
result["skipped_lines"] += 1
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Split by pipe and strip whitespace
|
|
116
|
+
parts = [p.strip() for p in line.split('|')]
|
|
117
|
+
|
|
118
|
+
if len(parts) < 9: # Need at least 9 parts (empty + 7 columns + empty)
|
|
119
|
+
result["skipped_lines"] += 1
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# Extract columns (skip leading/trailing empty)
|
|
123
|
+
try:
|
|
124
|
+
timestamp = parts[1] # ISO timestamp
|
|
125
|
+
agent_type = parts[2] # "haiku", "sonnet", "orchestrator", etc.
|
|
126
|
+
model = parts[3]
|
|
127
|
+
duration_str = parts[4]
|
|
128
|
+
tokens_in_str = parts[5]
|
|
129
|
+
tokens_out_str = parts[6]
|
|
130
|
+
verdict = parts[7]
|
|
131
|
+
except IndexError:
|
|
132
|
+
result["skipped_lines"] += 1
|
|
133
|
+
continue
|
|
134
|
+
|
|
135
|
+
# Skip header line
|
|
136
|
+
if 'timestamp' in timestamp.lower() or 'iso' in timestamp.lower():
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
# Validate verdict before processing
|
|
140
|
+
if verdict not in ("OK", "FAILED", "EMPTY", "HUNG"):
|
|
141
|
+
result["skipped_lines"] += 1
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# Parse and validate numeric fields
|
|
145
|
+
try:
|
|
146
|
+
tokens_in = int(tokens_in_str)
|
|
147
|
+
tokens_out = int(tokens_out_str)
|
|
148
|
+
except ValueError:
|
|
149
|
+
result["skipped_lines"] += 1
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
# Initialize agent_type entry if not seen
|
|
153
|
+
if agent_type not in result["specialties"]:
|
|
154
|
+
result["specialties"][agent_type] = {
|
|
155
|
+
"total_runs": 0,
|
|
156
|
+
"success_count": 0,
|
|
157
|
+
"failed_count": 0,
|
|
158
|
+
"empty_count": 0,
|
|
159
|
+
"hung_count": 0,
|
|
160
|
+
"success_rate": 0.0,
|
|
161
|
+
"repair_count": 0,
|
|
162
|
+
"retry_frequency": 0.0,
|
|
163
|
+
}
|
|
164
|
+
agent_verdicts[agent_type] = []
|
|
165
|
+
|
|
166
|
+
# Increment verdict counters
|
|
167
|
+
result["specialties"][agent_type]["total_runs"] += 1
|
|
168
|
+
if verdict == "OK":
|
|
169
|
+
result["specialties"][agent_type]["success_count"] += 1
|
|
170
|
+
elif verdict == "FAILED":
|
|
171
|
+
result["specialties"][agent_type]["failed_count"] += 1
|
|
172
|
+
elif verdict == "EMPTY":
|
|
173
|
+
result["specialties"][agent_type]["empty_count"] += 1
|
|
174
|
+
elif verdict == "HUNG":
|
|
175
|
+
result["specialties"][agent_type]["hung_count"] += 1
|
|
176
|
+
|
|
177
|
+
# Collect verdict history for repair detection
|
|
178
|
+
agent_verdicts[agent_type].append(verdict)
|
|
179
|
+
|
|
180
|
+
# Calculate success rates and detect repair cycles
|
|
181
|
+
for agent_type, stats in result["specialties"].items():
|
|
182
|
+
total = stats["total_runs"]
|
|
183
|
+
if total > 0:
|
|
184
|
+
stats["success_rate"] = stats["success_count"] / total
|
|
185
|
+
stats["retry_frequency"] = stats["repair_count"] / total
|
|
186
|
+
|
|
187
|
+
# Detect repair cycles (consecutive failures followed by success)
|
|
188
|
+
verdicts = agent_verdicts.get(agent_type, [])
|
|
189
|
+
repair_count = 0
|
|
190
|
+
in_failure_sequence = False
|
|
191
|
+
|
|
192
|
+
for verdict in verdicts:
|
|
193
|
+
if verdict in ("FAILED", "EMPTY", "HUNG"):
|
|
194
|
+
in_failure_sequence = True
|
|
195
|
+
elif verdict == "OK" and in_failure_sequence:
|
|
196
|
+
# Success after failures = repair cycle
|
|
197
|
+
repair_count += 1
|
|
198
|
+
in_failure_sequence = False
|
|
199
|
+
|
|
200
|
+
stats["repair_count"] = repair_count
|
|
201
|
+
if total > 0:
|
|
202
|
+
stats["retry_frequency"] = repair_count / total
|
|
203
|
+
|
|
204
|
+
# Generate top-by-success ranking (highest success rate first)
|
|
205
|
+
success_ranking = sorted(
|
|
206
|
+
[
|
|
207
|
+
{
|
|
208
|
+
"agent_type": agent_type,
|
|
209
|
+
"success_rate": stats["success_rate"],
|
|
210
|
+
"total_runs": stats["total_runs"],
|
|
211
|
+
}
|
|
212
|
+
for agent_type, stats in result["specialties"].items()
|
|
213
|
+
],
|
|
214
|
+
key=lambda x: (-x["success_rate"], -x["total_runs"]), # Sort desc by success_rate, then runs
|
|
215
|
+
)
|
|
216
|
+
result["top_by_success"] = success_ranking
|
|
217
|
+
|
|
218
|
+
# Generate top-by-retry ranking (highest retry/repair frequency first)
|
|
219
|
+
retry_ranking = sorted(
|
|
220
|
+
[
|
|
221
|
+
{
|
|
222
|
+
"agent_type": agent_type,
|
|
223
|
+
"retry_frequency": stats["retry_frequency"],
|
|
224
|
+
"total_runs": stats["total_runs"],
|
|
225
|
+
}
|
|
226
|
+
for agent_type, stats in result["specialties"].items()
|
|
227
|
+
],
|
|
228
|
+
key=lambda x: (-x["retry_frequency"], -x["total_runs"]), # Sort desc by retry_frequency, then runs
|
|
229
|
+
)
|
|
230
|
+
result["top_by_retry"] = retry_ranking
|
|
231
|
+
|
|
232
|
+
return result
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Wave audit tail — newest audit/verification outcomes (adversarial findings, verdicts).
|
|
4
|
+
|
|
5
|
+
Reads audit backlog and ledger to surface latest verification outcomes.
|
|
6
|
+
Compact live tail panel showing recent audit events and findings.
|
|
7
|
+
|
|
8
|
+
Returns audit tail data for the Activity view.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
import config
|
|
17
|
+
import cost
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parse_audit_backlog_recent() -> List[Dict]:
|
|
21
|
+
"""Extract recent items from AUDIT-BACKLOG.md.
|
|
22
|
+
|
|
23
|
+
Returns the most recent 5-10 P0/P1 items (inflight, todo, or done).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
list: [{"status": "✅|🔵|⬜", "tier": "P0|P1", "tag": "[tag]", "title": str, ...}, ...]
|
|
27
|
+
"""
|
|
28
|
+
try:
|
|
29
|
+
backlog_file = config.AUDIT_BACKLOG_FILE
|
|
30
|
+
if not backlog_file.exists():
|
|
31
|
+
return []
|
|
32
|
+
|
|
33
|
+
content = backlog_file.read_text(encoding='utf-8')
|
|
34
|
+
lines = content.split('\n')
|
|
35
|
+
|
|
36
|
+
recent_items = []
|
|
37
|
+
current_tier = None
|
|
38
|
+
|
|
39
|
+
for line in lines:
|
|
40
|
+
line_stripped = line.strip()
|
|
41
|
+
|
|
42
|
+
# Detect tier headers
|
|
43
|
+
if re.match(r'^##\s+(P0|P1|P2)', line_stripped):
|
|
44
|
+
match = re.match(r'^##\s+(P0|P1|P2)', line_stripped)
|
|
45
|
+
current_tier = match.group(1) if match else None
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
if current_tier and line_stripped.startswith('- '):
|
|
49
|
+
# Extract status emoji, tag, and title
|
|
50
|
+
# Pattern: - 🔵 **[tag] Title**
|
|
51
|
+
match = re.search(r'(✅|🔵|⬜|⏸)\s+\*\*\[([^\]]+)\]\s+(.+?)\*\*', line_stripped)
|
|
52
|
+
if match:
|
|
53
|
+
status_emoji = match.group(1)
|
|
54
|
+
tag = match.group(2)
|
|
55
|
+
title = match.group(3)
|
|
56
|
+
|
|
57
|
+
recent_items.append({
|
|
58
|
+
"status": status_emoji,
|
|
59
|
+
"tier": current_tier,
|
|
60
|
+
"tag": tag,
|
|
61
|
+
"title": title,
|
|
62
|
+
"timestamp": None, # AUDIT-BACKLOG doesn't have timestamps
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
# Return most recent (last) items
|
|
66
|
+
return recent_items[-10:] if recent_items else []
|
|
67
|
+
except Exception as e:
|
|
68
|
+
print(f"[wave_audit_tail] Error parsing audit backlog: {e}")
|
|
69
|
+
return []
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse_ledger_recent_verdicts() -> List[Dict]:
|
|
73
|
+
"""Extract recent verdict outcomes from OUTCOMES-LEDGER.md.
|
|
74
|
+
|
|
75
|
+
Looks for recent lines showing agent results: OK/FAILED/EMPTY/HUNG.
|
|
76
|
+
|
|
77
|
+
Ledger format (9-column, optionally 7-column legacy):
|
|
78
|
+
| ISO ts | agent_type | model | duration_sec | tokens_in | tokens_out | verdict | phase | wave |
|
|
79
|
+
|
|
80
|
+
Verdict whitelist: only OK, FAILED, EMPTY, HUNG are valid.
|
|
81
|
+
Forged/invalid verdicts are skipped.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
list: [{"agent": str, "verdict": "OK|FAILED|EMPTY|HUNG", "timestamp": iso, ...}, ...]
|
|
85
|
+
"""
|
|
86
|
+
# Whitelist of valid verdicts
|
|
87
|
+
VALID_VERDICTS = {"OK", "FAILED", "EMPTY", "HUNG"}
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
ledger_file = config.LEDGER_FILE
|
|
91
|
+
if not ledger_file.exists():
|
|
92
|
+
return []
|
|
93
|
+
|
|
94
|
+
content = ledger_file.read_text(encoding='utf-8')
|
|
95
|
+
lines = content.split('\n')
|
|
96
|
+
|
|
97
|
+
recent_verdicts = []
|
|
98
|
+
|
|
99
|
+
# Parse ledger table rows (supports 7-column and 9-column formats)
|
|
100
|
+
for line in reversed(lines[-50:]): # Check last 50 lines
|
|
101
|
+
line_stripped = line.strip()
|
|
102
|
+
# Look for table rows with verdict info
|
|
103
|
+
# Pattern: | timestamp | agent_type | model | ... | verdict | ... |
|
|
104
|
+
if '|' not in line_stripped:
|
|
105
|
+
continue
|
|
106
|
+
|
|
107
|
+
parts = [p.strip() for p in line_stripped.split('|')]
|
|
108
|
+
|
|
109
|
+
# Accept both 7-column (9 parts) and 9-column (11 parts) formats
|
|
110
|
+
# 7 columns: ['', col1, col2, col3, col4, col5, col6, col7, ''] -> parts[7] = verdict
|
|
111
|
+
# 9 columns: ['', col1, col2, col3, col4, col5, col6, col7, col8, col9, ''] -> parts[7] = verdict
|
|
112
|
+
if len(parts) < 9:
|
|
113
|
+
continue
|
|
114
|
+
|
|
115
|
+
# Try to extract timestamp (parts[1]), agent_type (parts[2]), and verdict (parts[7])
|
|
116
|
+
try:
|
|
117
|
+
timestamp_str = parts[1]
|
|
118
|
+
agent_str = parts[2]
|
|
119
|
+
# IMPORTANT: verdict is at parts[7], not parts[3] (which is model)
|
|
120
|
+
verdict_str = parts[7].upper()
|
|
121
|
+
|
|
122
|
+
# Validate verdict against whitelist
|
|
123
|
+
if verdict_str not in VALID_VERDICTS:
|
|
124
|
+
# Skip forged/invalid verdicts
|
|
125
|
+
continue
|
|
126
|
+
|
|
127
|
+
# Verify timestamp looks like ISO format
|
|
128
|
+
if 'T' not in timestamp_str and '-' not in timestamp_str[:10]:
|
|
129
|
+
continue
|
|
130
|
+
|
|
131
|
+
recent_verdicts.append({
|
|
132
|
+
"timestamp": timestamp_str,
|
|
133
|
+
"agent": agent_str.split('-')[-1][:13], # Short agent ID
|
|
134
|
+
"verdict": verdict_str,
|
|
135
|
+
})
|
|
136
|
+
except (IndexError, ValueError):
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
# Return most recent (first in reversed list)
|
|
140
|
+
return recent_verdicts[-10:] if recent_verdicts else []
|
|
141
|
+
except Exception as e:
|
|
142
|
+
print(f"[wave_audit_tail] Error parsing ledger: {e}")
|
|
143
|
+
return []
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def get_wave_audit_tail() -> Dict:
|
|
147
|
+
"""Get audit tail data for the current wave.
|
|
148
|
+
|
|
149
|
+
Returns latest audit/verification outcomes as a compact tail.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
dict: {
|
|
153
|
+
"available": bool,
|
|
154
|
+
"audit_items": [
|
|
155
|
+
{
|
|
156
|
+
"type": "audit_backlog|verdict",
|
|
157
|
+
"timestamp": "2026-07-21T12:34:56Z" or null,
|
|
158
|
+
"status": "✅|🔵|⬜|...",
|
|
159
|
+
"tier": "P0|P1|P2",
|
|
160
|
+
"tag": "[sec]|[ui]|...",
|
|
161
|
+
"title": str,
|
|
162
|
+
"verdict": "OK|FAILED|...",
|
|
163
|
+
"agent": str,
|
|
164
|
+
},
|
|
165
|
+
...
|
|
166
|
+
],
|
|
167
|
+
"at": "2026-07-21T12:34:56Z"
|
|
168
|
+
}
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
audit_items = []
|
|
172
|
+
|
|
173
|
+
# Get recent backlog items
|
|
174
|
+
backlog_items = _parse_audit_backlog_recent()
|
|
175
|
+
for item in backlog_items:
|
|
176
|
+
audit_items.append({
|
|
177
|
+
"type": "audit_backlog",
|
|
178
|
+
**item,
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
# Get recent verdict outcomes
|
|
182
|
+
verdict_items = _parse_ledger_recent_verdicts()
|
|
183
|
+
for item in verdict_items:
|
|
184
|
+
audit_items.append({
|
|
185
|
+
"type": "verdict",
|
|
186
|
+
**item,
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
# Sort by timestamp (nulls last) and limit to 15 most recent
|
|
190
|
+
def sort_key(item):
|
|
191
|
+
ts = item.get("timestamp")
|
|
192
|
+
if ts is None:
|
|
193
|
+
return ""
|
|
194
|
+
return ts
|
|
195
|
+
|
|
196
|
+
audit_items.sort(key=sort_key, reverse=True)
|
|
197
|
+
audit_items = audit_items[:15]
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
"available": bool(audit_items),
|
|
201
|
+
"audit_items": audit_items,
|
|
202
|
+
"at": datetime.now(timezone.utc).isoformat() + "Z",
|
|
203
|
+
}
|
|
204
|
+
except Exception as e:
|
|
205
|
+
print(f"[wave_audit_tail] Uncaught error: {e}")
|
|
206
|
+
import traceback
|
|
207
|
+
traceback.print_exc()
|
|
208
|
+
return {
|
|
209
|
+
"available": False,
|
|
210
|
+
"error": str(e),
|
|
211
|
+
"audit_items": [],
|
|
212
|
+
"at": datetime.now(timezone.utc).isoformat() + "Z"
|
|
213
|
+
}
|