@matt82198/aesop 0.1.0 → 0.3.1
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 +117 -2
- package/README.md +59 -218
- package/bin/cli.js +168 -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/HOOK-INSTALL.md +15 -56
- package/docs/INSTALL.md +74 -4
- package/docs/PORTING.md +166 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +372 -0
- package/docs/reproduce.md +33 -3
- package/driver/CLAUDE.md +150 -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 +673 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +179 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +254 -0
- package/driver/wave_loop.py +1408 -0
- package/driver/wave_scheduler.py +890 -0
- package/hooks/pre-push-policy.sh +131 -33
- package/mcp/server.mjs +320 -4
- package/monitor/collect-signals.mjs +69 -0
- package/package.json +22 -14
- 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 +4 -2
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/projections.py +63 -0
- package/state_store/read_api.py +156 -0
- package/state_store/store.py +185 -73
- package/state_store/write_api.py +462 -0
- 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 +166 -43
- package/tools/cost_econ.py +480 -0
- package/tools/cost_projection.py +559 -0
- package/tools/crossos_drift.py +394 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +188 -26
- 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 +523 -0
- package/tools/portability_check.py +206 -0
- package/tools/proposals.mjs +47 -2
- package/tools/reconcile.py +7 -4
- package/tools/reproduce.js +405 -0
- package/tools/secret_scan.py +207 -65
- package/tools/self_stats.py +20 -0
- package/tools/stall_check.py +247 -16
- package/tools/stateapi_lint.py +325 -0
- package/tools/test_battery.py +173 -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_cost_panel.py +345 -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 +779 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +340 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +81 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +383 -55
- package/ui/quality_scorecard.py +232 -0
- package/ui/sse.py +3 -3
- 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 +383 -0
- package/ui/web/dist/assets/index-CNQxaiOW.css +1 -0
- package/ui/web/dist/assets/index-CP68RIh3.js +9 -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,361 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Health Score tool — deterministic readiness score for primed projects.
|
|
4
|
+
|
|
5
|
+
Calculates a weighted 0-100 score based on:
|
|
6
|
+
- Config validity (JSON parseable, required fields) — 15 points
|
|
7
|
+
- Git pre-push hook installed — 15 points
|
|
8
|
+
- CLAUDE.md present and linted clean — 15 points
|
|
9
|
+
- State directory writable — 10 points
|
|
10
|
+
- Daemon heartbeats fresh (watchdog/monitor) — 15 points
|
|
11
|
+
- Git identity configured (user.name + user.email) — 15 points
|
|
12
|
+
- Secret-scan tool runnable — 15 points
|
|
13
|
+
|
|
14
|
+
Total: 100 points across 7 weighted checks.
|
|
15
|
+
|
|
16
|
+
Output: human-readable (score card with per-check status) or --json (structured).
|
|
17
|
+
|
|
18
|
+
Exit: 0 on success (always produces a score; no score is not a failure).
|
|
19
|
+
"""
|
|
20
|
+
import argparse
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
from datetime import datetime, timezone
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from time import time
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from common import check_heartbeat_staleness
|
|
31
|
+
except ImportError:
|
|
32
|
+
from tools.common import check_heartbeat_staleness
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def calculate_score(cwd=None):
|
|
36
|
+
"""
|
|
37
|
+
Calculate health score for the primed project.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
cwd (str): Working directory to check (default: current directory)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
dict: {
|
|
44
|
+
"score": int (0-100),
|
|
45
|
+
"checks": [
|
|
46
|
+
{
|
|
47
|
+
"name": str,
|
|
48
|
+
"passed": bool,
|
|
49
|
+
"weight": int,
|
|
50
|
+
"detail": str
|
|
51
|
+
},
|
|
52
|
+
...
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
"""
|
|
56
|
+
if cwd is None:
|
|
57
|
+
cwd = os.getcwd()
|
|
58
|
+
|
|
59
|
+
cwd_path = Path(cwd)
|
|
60
|
+
|
|
61
|
+
checks = []
|
|
62
|
+
|
|
63
|
+
# 1. Config check (15 points)
|
|
64
|
+
config_check = _check_config(cwd_path)
|
|
65
|
+
checks.append({
|
|
66
|
+
"name": "config",
|
|
67
|
+
"weight": 15,
|
|
68
|
+
"passed": config_check["passed"],
|
|
69
|
+
"detail": config_check["detail"]
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
# 2. Pre-push hook check (15 points)
|
|
73
|
+
hook_check = _check_pre_push_hook(cwd_path)
|
|
74
|
+
checks.append({
|
|
75
|
+
"name": "hooks",
|
|
76
|
+
"weight": 15,
|
|
77
|
+
"passed": hook_check["passed"],
|
|
78
|
+
"detail": hook_check["detail"]
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
# 3. CLAUDE.md check (15 points)
|
|
82
|
+
claude_check = _check_claude_md(cwd_path)
|
|
83
|
+
checks.append({
|
|
84
|
+
"name": "claude",
|
|
85
|
+
"weight": 15,
|
|
86
|
+
"passed": claude_check["passed"],
|
|
87
|
+
"detail": claude_check["detail"]
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
# 4. State directory writable (10 points)
|
|
91
|
+
writable_check = _check_state_writable(cwd_path)
|
|
92
|
+
checks.append({
|
|
93
|
+
"name": "writable",
|
|
94
|
+
"weight": 10,
|
|
95
|
+
"passed": writable_check["passed"],
|
|
96
|
+
"detail": writable_check["detail"]
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
# 5. Heartbeats fresh (15 points)
|
|
100
|
+
heartbeat_check = _check_heartbeats(cwd_path)
|
|
101
|
+
checks.append({
|
|
102
|
+
"name": "heartbeat",
|
|
103
|
+
"weight": 15,
|
|
104
|
+
"passed": heartbeat_check["passed"],
|
|
105
|
+
"detail": heartbeat_check["detail"]
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
# 6. Git identity (15 points)
|
|
109
|
+
identity_check = _check_git_identity(cwd_path)
|
|
110
|
+
checks.append({
|
|
111
|
+
"name": "identity",
|
|
112
|
+
"weight": 15,
|
|
113
|
+
"passed": identity_check["passed"],
|
|
114
|
+
"detail": identity_check["detail"]
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
# 7. Secret-scan runnable (15 points)
|
|
118
|
+
secret_scan_check = _check_secret_scan_runnable(cwd_path)
|
|
119
|
+
checks.append({
|
|
120
|
+
"name": "secret-scan",
|
|
121
|
+
"weight": 15,
|
|
122
|
+
"passed": secret_scan_check["passed"],
|
|
123
|
+
"detail": secret_scan_check["detail"]
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# Calculate weighted score
|
|
127
|
+
total_weight = sum(c["weight"] for c in checks)
|
|
128
|
+
earned_points = sum(c["weight"] for c in checks if c["passed"])
|
|
129
|
+
score = int((earned_points / total_weight) * 100) if total_weight > 0 else 0
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
"score": score,
|
|
133
|
+
"checks": checks
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _check_config(cwd_path):
|
|
138
|
+
"""Check if aesop.config.json exists and is valid JSON."""
|
|
139
|
+
config_path = cwd_path / "aesop.config.json"
|
|
140
|
+
|
|
141
|
+
if not config_path.exists():
|
|
142
|
+
return {"passed": False, "detail": "aesop.config.json not found"}
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
content = config_path.read_text(encoding="utf-8")
|
|
146
|
+
json.loads(content)
|
|
147
|
+
return {"passed": True, "detail": "aesop.config.json valid"}
|
|
148
|
+
except json.JSONDecodeError as e:
|
|
149
|
+
return {"passed": False, "detail": f"Invalid JSON: {str(e)[:50]}"}
|
|
150
|
+
except Exception as e:
|
|
151
|
+
return {"passed": False, "detail": f"Config read error: {str(e)[:50]}"}
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _check_pre_push_hook(cwd_path):
|
|
155
|
+
"""Check if .git/hooks/pre-push exists and is executable."""
|
|
156
|
+
hook_path = cwd_path / ".git" / "hooks" / "pre-push"
|
|
157
|
+
|
|
158
|
+
if not hook_path.exists():
|
|
159
|
+
return {"passed": False, "detail": "Pre-push hook not installed"}
|
|
160
|
+
|
|
161
|
+
if not os.access(hook_path, os.X_OK):
|
|
162
|
+
return {"passed": False, "detail": "Pre-push hook not executable"}
|
|
163
|
+
|
|
164
|
+
return {"passed": True, "detail": "Pre-push hook installed"}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _check_claude_md(cwd_path):
|
|
168
|
+
"""Check if CLAUDE.md exists."""
|
|
169
|
+
claude_path = cwd_path / "CLAUDE.md"
|
|
170
|
+
|
|
171
|
+
if not claude_path.exists():
|
|
172
|
+
return {"passed": False, "detail": "CLAUDE.md not found"}
|
|
173
|
+
|
|
174
|
+
content = claude_path.read_text(encoding="utf-8").strip()
|
|
175
|
+
if not content:
|
|
176
|
+
return {"passed": False, "detail": "CLAUDE.md is empty"}
|
|
177
|
+
|
|
178
|
+
return {"passed": True, "detail": "CLAUDE.md present"}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _check_state_writable(cwd_path):
|
|
182
|
+
"""Check if state directory is writable."""
|
|
183
|
+
state_dir = cwd_path / "state"
|
|
184
|
+
|
|
185
|
+
# Create state dir if it doesn't exist
|
|
186
|
+
try:
|
|
187
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
188
|
+
except Exception as e:
|
|
189
|
+
return {"passed": False, "detail": f"Cannot create state dir: {str(e)[:40]}"}
|
|
190
|
+
|
|
191
|
+
# Try to write a test file
|
|
192
|
+
test_file = state_dir / ".health-score-test"
|
|
193
|
+
try:
|
|
194
|
+
test_file.write_text("test", encoding="utf-8")
|
|
195
|
+
test_file.unlink()
|
|
196
|
+
return {"passed": True, "detail": "State directory writable"}
|
|
197
|
+
except Exception as e:
|
|
198
|
+
return {"passed": False, "detail": f"State dir not writable: {str(e)[:40]}"}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _check_heartbeats(cwd_path):
|
|
202
|
+
"""Check if daemon heartbeats are fresh."""
|
|
203
|
+
state_dir = cwd_path / "state"
|
|
204
|
+
|
|
205
|
+
# Check watchdog heartbeat (threshold 300s)
|
|
206
|
+
watchdog_file = state_dir / ".watchdog-heartbeat"
|
|
207
|
+
is_stale_watchdog, age_watchdog, info_watchdog = check_heartbeat_staleness(
|
|
208
|
+
watchdog_file, threshold_s=300
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
# Check monitor heartbeat (threshold 3600s)
|
|
212
|
+
monitor_file = state_dir / ".monitor-heartbeat"
|
|
213
|
+
is_stale_monitor, age_monitor, info_monitor = check_heartbeat_staleness(
|
|
214
|
+
monitor_file, threshold_s=3600
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if is_stale_watchdog or is_stale_monitor:
|
|
218
|
+
details = []
|
|
219
|
+
if is_stale_watchdog:
|
|
220
|
+
if age_watchdog > 0:
|
|
221
|
+
details.append(f"watchdog stale ({age_watchdog}s)")
|
|
222
|
+
else:
|
|
223
|
+
details.append("watchdog missing")
|
|
224
|
+
if is_stale_monitor:
|
|
225
|
+
if age_monitor > 0:
|
|
226
|
+
details.append(f"monitor stale ({age_monitor}s)")
|
|
227
|
+
else:
|
|
228
|
+
details.append("monitor missing")
|
|
229
|
+
return {"passed": False, "detail": "; ".join(details)}
|
|
230
|
+
|
|
231
|
+
return {"passed": True, "detail": "Heartbeats fresh"}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _check_git_identity(cwd_path):
|
|
235
|
+
"""Check if git user.name and user.email are configured."""
|
|
236
|
+
try:
|
|
237
|
+
# Check local git config in the repo
|
|
238
|
+
name_result = subprocess.run(
|
|
239
|
+
["git", "config", "user.name"],
|
|
240
|
+
cwd=str(cwd_path),
|
|
241
|
+
capture_output=True,
|
|
242
|
+
text=True,
|
|
243
|
+
timeout=5
|
|
244
|
+
)
|
|
245
|
+
email_result = subprocess.run(
|
|
246
|
+
["git", "config", "user.email"],
|
|
247
|
+
cwd=str(cwd_path),
|
|
248
|
+
capture_output=True,
|
|
249
|
+
text=True,
|
|
250
|
+
timeout=5
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
name = name_result.stdout.strip()
|
|
254
|
+
email = email_result.stdout.strip()
|
|
255
|
+
|
|
256
|
+
if not name or not email:
|
|
257
|
+
missing = []
|
|
258
|
+
if not name:
|
|
259
|
+
missing.append("user.name")
|
|
260
|
+
if not email:
|
|
261
|
+
missing.append("user.email")
|
|
262
|
+
return {"passed": False, "detail": f"Git {', '.join(missing)} not set"}
|
|
263
|
+
|
|
264
|
+
return {"passed": True, "detail": "Git identity configured"}
|
|
265
|
+
except Exception as e:
|
|
266
|
+
return {"passed": False, "detail": f"Identity check error: {str(e)[:40]}"}
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _check_secret_scan_runnable(cwd_path):
|
|
270
|
+
"""Check if secret_scan.py is present and runnable."""
|
|
271
|
+
secret_scan = cwd_path / "tools" / "secret_scan.py"
|
|
272
|
+
|
|
273
|
+
if not secret_scan.exists():
|
|
274
|
+
return {"passed": False, "detail": "secret_scan.py not found"}
|
|
275
|
+
|
|
276
|
+
# Try to run with --help
|
|
277
|
+
try:
|
|
278
|
+
result = subprocess.run(
|
|
279
|
+
[sys.executable, str(secret_scan), "--help"],
|
|
280
|
+
capture_output=True,
|
|
281
|
+
text=True,
|
|
282
|
+
timeout=5
|
|
283
|
+
)
|
|
284
|
+
if result.returncode == 0 or "usage" in result.stdout.lower():
|
|
285
|
+
return {"passed": True, "detail": "secret-scan runnable"}
|
|
286
|
+
else:
|
|
287
|
+
return {"passed": False, "detail": "secret-scan not responding"}
|
|
288
|
+
except subprocess.TimeoutExpired:
|
|
289
|
+
return {"passed": False, "detail": "secret-scan timeout"}
|
|
290
|
+
except Exception as e:
|
|
291
|
+
return {"passed": False, "detail": f"secret-scan error: {str(e)[:40]}"}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def format_human(result):
|
|
295
|
+
"""Format score result as human-readable output."""
|
|
296
|
+
lines = []
|
|
297
|
+
score = result.get("score", 0)
|
|
298
|
+
checks = result.get("checks", [])
|
|
299
|
+
|
|
300
|
+
lines.append(f"\nHealth Score: {score}/100\n")
|
|
301
|
+
|
|
302
|
+
# Print check results
|
|
303
|
+
for check in checks:
|
|
304
|
+
status = "[PASS]" if check["passed"] else "[FAIL]"
|
|
305
|
+
name = check["name"].replace("-", " ").title()
|
|
306
|
+
detail = check.get("detail", "")
|
|
307
|
+
weight = check.get("weight", 0)
|
|
308
|
+
|
|
309
|
+
line = f" {status:8} {name:15} (weight: {weight:2})"
|
|
310
|
+
if detail:
|
|
311
|
+
line += f" — {detail}"
|
|
312
|
+
lines.append(line)
|
|
313
|
+
|
|
314
|
+
# Summary
|
|
315
|
+
passed = sum(1 for c in checks if c["passed"])
|
|
316
|
+
total = len(checks)
|
|
317
|
+
lines.append(f"\nSummary: {passed}/{total} checks passed")
|
|
318
|
+
|
|
319
|
+
return "\n".join(lines)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def format_json(result):
|
|
323
|
+
"""Format score result as JSON."""
|
|
324
|
+
return json.dumps(result, indent=2)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def main():
|
|
328
|
+
"""CLI entry point."""
|
|
329
|
+
parser = argparse.ArgumentParser(
|
|
330
|
+
description="Aesop health score — readiness assessment for primed projects"
|
|
331
|
+
)
|
|
332
|
+
parser.add_argument(
|
|
333
|
+
"--json",
|
|
334
|
+
action="store_true",
|
|
335
|
+
help="Output as JSON"
|
|
336
|
+
)
|
|
337
|
+
parser.add_argument(
|
|
338
|
+
"--cwd",
|
|
339
|
+
default=None,
|
|
340
|
+
help="Working directory to check (default: current directory)"
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
args = parser.parse_args()
|
|
344
|
+
|
|
345
|
+
try:
|
|
346
|
+
result = calculate_score(cwd=args.cwd)
|
|
347
|
+
|
|
348
|
+
if args.json:
|
|
349
|
+
output = format_json(result)
|
|
350
|
+
else:
|
|
351
|
+
output = format_human(result)
|
|
352
|
+
|
|
353
|
+
print(output)
|
|
354
|
+
sys.exit(0)
|
|
355
|
+
except Exception as e:
|
|
356
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
357
|
+
sys.exit(1)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
if __name__ == "__main__":
|
|
361
|
+
main()
|
package/tools/metrics_gate.py
CHANGED
|
@@ -75,9 +75,16 @@ class MetricsGate:
|
|
|
75
75
|
text=True,
|
|
76
76
|
check=False,
|
|
77
77
|
)
|
|
78
|
-
if result.returncode
|
|
79
|
-
#
|
|
78
|
+
if result.returncode == 0:
|
|
79
|
+
# Success, continue to parse
|
|
80
|
+
pass
|
|
81
|
+
elif result.returncode == 128:
|
|
82
|
+
# Invalid range (ref doesn't exist) - no diff available
|
|
80
83
|
return []
|
|
84
|
+
else:
|
|
85
|
+
# Other git error - fail closed (cannot verify => deny)
|
|
86
|
+
print(f"ERROR: git diff failed with exit code {result.returncode}", file=sys.stderr)
|
|
87
|
+
sys.exit(1)
|
|
81
88
|
|
|
82
89
|
diff_output = result.stdout
|
|
83
90
|
added_lines = []
|
|
@@ -96,8 +103,10 @@ class MetricsGate:
|
|
|
96
103
|
added_lines.append((current_file, content))
|
|
97
104
|
|
|
98
105
|
return added_lines
|
|
99
|
-
except Exception:
|
|
100
|
-
|
|
106
|
+
except Exception as e:
|
|
107
|
+
# Unexpected error (file I/O, parsing) - fail closed (cannot verify => deny)
|
|
108
|
+
print(f"ERROR: git diff operation failed: {e}", file=sys.stderr)
|
|
109
|
+
sys.exit(1)
|
|
101
110
|
|
|
102
111
|
def is_version_number(self, text: str) -> bool:
|
|
103
112
|
"""Check if text looks like a version number."""
|