@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
package/hooks/pre-push-policy.sh
CHANGED
|
@@ -252,21 +252,42 @@ check_branch_policy() {
|
|
|
252
252
|
# Finding 3: Handle tty mode and final line without trailing newline
|
|
253
253
|
if [ -t 0 ]; then
|
|
254
254
|
# Running interactively on a tty; skip stdin processing with note
|
|
255
|
-
# but still check current branch as fallback
|
|
255
|
+
# but still check current branch as fallback. Note: main() blocks tty before this can matter.
|
|
256
256
|
:
|
|
257
257
|
else
|
|
258
258
|
# Not a tty; read stdin normally
|
|
259
|
+
local saw_tuple=0
|
|
260
|
+
local saw_nondelete=0
|
|
259
261
|
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha || [ -n "$local_ref" ]; do
|
|
260
262
|
# Skip empty lines
|
|
261
263
|
if [ -z "$remote_ref" ]; then
|
|
262
264
|
continue
|
|
263
265
|
fi
|
|
266
|
+
saw_tuple=1
|
|
267
|
+
|
|
268
|
+
# Delete refspec (local sha all zeros): removes a remote ref, pushes no
|
|
269
|
+
# content. Deleting refs/heads/main|master is still blocked below; other
|
|
270
|
+
# deletions never constitute a push TO main.
|
|
271
|
+
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
272
|
+
if [ "$remote_ref" = "refs/heads/main" ] || [ "$remote_ref" = "refs/heads/master" ]; then
|
|
273
|
+
return 1
|
|
274
|
+
fi
|
|
275
|
+
continue
|
|
276
|
+
fi
|
|
277
|
+
saw_nondelete=1
|
|
264
278
|
|
|
265
279
|
# Block if attempting to push to main or master
|
|
266
280
|
if [ "$remote_ref" = "refs/heads/main" ] || [ "$remote_ref" = "refs/heads/master" ]; then
|
|
267
281
|
return 1
|
|
268
282
|
fi
|
|
269
283
|
done
|
|
284
|
+
|
|
285
|
+
# Delete-only push (tuples seen, none pushing content): allowed regardless
|
|
286
|
+
# of the currently checked-out branch -- branch deletion from a main
|
|
287
|
+
# checkout is administrative, not a push to main.
|
|
288
|
+
if [ "$saw_tuple" = "1" ] && [ "$saw_nondelete" = "0" ]; then
|
|
289
|
+
return 0
|
|
290
|
+
fi
|
|
270
291
|
fi
|
|
271
292
|
|
|
272
293
|
# If no protected branch in stdin, also check current branch as fallback
|
|
@@ -292,19 +313,46 @@ get_commit_range() {
|
|
|
292
313
|
# FIRST tuple, so a multi-ref push only ever scanned the first branch's
|
|
293
314
|
# range and every other ref in the same push silently bypassed the secret
|
|
294
315
|
# scan. Returns 0 if at least one range was emitted, 1 if none could be
|
|
295
|
-
# parsed (
|
|
296
|
-
#
|
|
316
|
+
# parsed (malformed/invalid tuples), 2 if delete-only (tuples present but
|
|
317
|
+
# all deletions), 3 if truly empty stdin (no tuples at all, e.g. up-to-date
|
|
318
|
+
# push) -- single-ref callers see exactly the same one-line output as before.
|
|
319
|
+
# P1 bug fix: distinguish empty stdin (rc=3, allow) from malformed stdin
|
|
320
|
+
# (rc=1, fail-closed).
|
|
297
321
|
local local_ref local_sha remote_ref remote_sha
|
|
298
322
|
local found=0
|
|
323
|
+
local saw_any_tuple=0
|
|
299
324
|
|
|
300
325
|
if [ -t 0 ]; then
|
|
301
|
-
# Running interactively on a tty; no stdin to parse
|
|
326
|
+
# Running interactively on a tty; no stdin to parse → fail-closed (direct hook invocation)
|
|
327
|
+
# main() blocks tty before this is reached via the normal flow; nearly dead code but tests exercise it
|
|
328
|
+
printf 'Error: No stdin on tty; cannot parse pre-push ref tuples (interactive hook invocation)\n' >&2
|
|
302
329
|
return 1
|
|
303
330
|
fi
|
|
304
331
|
|
|
332
|
+
local saw_delete=0
|
|
305
333
|
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha || [ -n "$local_ref" ]; do
|
|
306
|
-
# Skip empty lines
|
|
307
|
-
if
|
|
334
|
+
# Skip truly empty lines (no fields at all)
|
|
335
|
+
# A line with any content (even if malformed) will have at least one non-empty field
|
|
336
|
+
if [ -z "$local_ref" ] && [ -z "$local_sha" ] && [ -z "$remote_ref" ] && [ -z "$remote_sha" ]; then
|
|
337
|
+
continue
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
# Harden tuple parsing: require all 4 fields present (P1 fix)
|
|
341
|
+
# If we have some content but not all 4 fields, it's malformed
|
|
342
|
+
if [ -z "$remote_ref" ] || [ -z "$remote_sha" ]; then
|
|
343
|
+
# Malformed line: has fewer than 4 fields
|
|
344
|
+
printf 'Error: Malformed pre-push stdin: insufficient fields (expected 4, got fewer) in line: %s %s %s %s\n' "$local_ref" "$local_sha" "$remote_ref" "$remote_sha" >&2
|
|
345
|
+
return 1
|
|
346
|
+
fi
|
|
347
|
+
|
|
348
|
+
saw_any_tuple=1
|
|
349
|
+
|
|
350
|
+
# Delete refspec (local sha all zeros): no content is pushed, so there is
|
|
351
|
+
# no commit range to scan. Skipped here; if the WHOLE push is deletes we
|
|
352
|
+
# return 2 so the caller can pass without weakening fail-closed behavior
|
|
353
|
+
# for unparseable stdin (which stays return 1).
|
|
354
|
+
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
355
|
+
saw_delete=1
|
|
308
356
|
continue
|
|
309
357
|
fi
|
|
310
358
|
|
|
@@ -327,6 +375,15 @@ get_commit_range() {
|
|
|
327
375
|
if [ "$found" -eq 1 ]; then
|
|
328
376
|
return 0
|
|
329
377
|
fi
|
|
378
|
+
if [ "$saw_delete" -eq 1 ]; then
|
|
379
|
+
# Delete-only push: nothing to scan (distinct from unparseable stdin)
|
|
380
|
+
return 2
|
|
381
|
+
fi
|
|
382
|
+
if [ "$saw_any_tuple" -eq 0 ]; then
|
|
383
|
+
# Truly empty stdin: no tuples at all (e.g., up-to-date push) (P1 fix: rc=3)
|
|
384
|
+
return 3
|
|
385
|
+
fi
|
|
386
|
+
# Malformed stdin: tuples present but none were valid
|
|
330
387
|
return 1
|
|
331
388
|
}
|
|
332
389
|
|
|
@@ -340,30 +397,51 @@ check_secret_scan() {
|
|
|
340
397
|
scan_bin="python"
|
|
341
398
|
fi
|
|
342
399
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
# Scanner not found: log event and warn to stderr, but don't block (fail-open)
|
|
348
|
-
log_event "secret_scan_unavailable"
|
|
349
|
-
printf 'Warning: secret_scan.py not found at %s\n' "$scan_script" >&2
|
|
350
|
-
return 0
|
|
351
|
-
fi
|
|
352
|
-
|
|
353
|
-
# Parse pre-push stdin to get commit range(s), then scan files in each range.
|
|
400
|
+
# Parse pre-push stdin FIRST: a delete-only push carries no content, so it
|
|
401
|
+
# needs no scanner at all — the availability check below only applies when
|
|
402
|
+
# there is actually content to scan (ordering matters: in scanner-less
|
|
403
|
+
# environments a branch deletion must still be possible).
|
|
354
404
|
# A multi-ref push (git push --all, or multiple branches/tags in one
|
|
355
405
|
# invocation) yields one range per line from get_commit_range(); scan EVERY
|
|
356
406
|
# one and fail if ANY range is dirty (P3 wave-25 fix: previously only the
|
|
357
407
|
# first ref tuple's range was ever scanned).
|
|
408
|
+
# P1 bug fix: distinguish empty stdin (rc=3, allow) from malformed stdin
|
|
409
|
+
# (rc=1, fail-closed).
|
|
358
410
|
local commit_ranges
|
|
359
411
|
commit_ranges=$(get_commit_range)
|
|
360
412
|
local parse_exit_code=$?
|
|
361
413
|
|
|
414
|
+
if [ $parse_exit_code -eq 2 ]; then
|
|
415
|
+
# Delete-only push: no content pushed, nothing to scan. Explicitly allowed
|
|
416
|
+
# (rc=2 is only emitted when tuples WERE parsed and all were deletions);
|
|
417
|
+
# unparseable/empty stdin still fails closed below.
|
|
418
|
+
log_event "secret_scan_skipped_delete_only_push"
|
|
419
|
+
return 0
|
|
420
|
+
fi
|
|
421
|
+
|
|
422
|
+
if [ $parse_exit_code -eq 3 ]; then
|
|
423
|
+
# Empty stdin: no tuples at all (e.g., up-to-date push), nothing to scan (P1 fix: rc=3)
|
|
424
|
+
# This is distinct from malformed stdin (rc=1) and is explicitly allowed
|
|
425
|
+
log_event "secret_scan_skipped_empty_stdin"
|
|
426
|
+
return 0
|
|
427
|
+
fi
|
|
428
|
+
|
|
429
|
+
local aesop_root="${AESOP_ROOT:-$HOME/aesop}"
|
|
430
|
+
local scan_script="$aesop_root/tools/secret_scan.py"
|
|
431
|
+
|
|
432
|
+
if [ ! -f "$scan_script" ] || [ ! -x "$scan_script" ]; then
|
|
433
|
+
# Scanner not found or not executable: fail-closed (cannot verify => deny)
|
|
434
|
+
log_block "secret_scan_unavailable"
|
|
435
|
+
printf 'FATAL: secret_scan.py not found or not executable at %s\n' "$scan_script" >&2
|
|
436
|
+
return 1
|
|
437
|
+
fi
|
|
438
|
+
|
|
362
439
|
if [ $parse_exit_code -ne 0 ] || [ -z "$commit_ranges" ]; then
|
|
363
|
-
# Malformed stdin
|
|
440
|
+
# Malformed stdin: fail-CLOSED (security P1 fix)
|
|
364
441
|
# Only fail-open for missing scanner tool, not for malformed input
|
|
442
|
+
# rc=1 is returned for malformed tuples (e.g., 3-field lines), which must fail-closed
|
|
365
443
|
log_block "secret_scan_stdin_parse_failed"
|
|
366
|
-
printf 'Error: Could not parse pre-push stdin for commit range (malformed
|
|
444
|
+
printf 'Error: Could not parse pre-push stdin for commit range (malformed tuple)\n' >&2
|
|
367
445
|
return 1
|
|
368
446
|
fi
|
|
369
447
|
|
|
@@ -417,7 +495,7 @@ log_event() {
|
|
|
417
495
|
local seq
|
|
418
496
|
seq=$(get_next_seq "$audit_log")
|
|
419
497
|
|
|
420
|
-
printf '{"seq":%d,"prev_hash":"%s","ts":"%s","repo":"%s","event":"%s","user":"%s"}\n' "$seq" "$prev_hash" "$ts" "$repo_name" "$(json_escape "$event_type")" "$(json_escape "$user")" >> "$audit_log" 2>/dev/null
|
|
498
|
+
printf '{"seq":%d,"prev_hash":"%s","ts":"%s","repo":"%s","event":"%s","user":"%s"}\n' "$seq" "$prev_hash" "$ts" "$(json_escape "$repo_name")" "$(json_escape "$event_type")" "$(json_escape "$user")" >> "$audit_log" 2>/dev/null
|
|
421
499
|
|
|
422
500
|
# Update tail hash sidecar
|
|
423
501
|
if [ -s "$audit_log" ]; then
|
|
@@ -454,7 +532,7 @@ log_block() {
|
|
|
454
532
|
local seq
|
|
455
533
|
seq=$(get_next_seq "$audit_log")
|
|
456
534
|
|
|
457
|
-
printf '{"seq":%d,"prev_hash":"%s","ts":"%s","repo":"%s","event":"push_blocked","reason":"%s","user":"%s"}\n' "$seq" "$prev_hash" "$ts" "$repo_name" "$(json_escape "$reason")" "$(json_escape "$user")" >> "$audit_log" 2>/dev/null
|
|
535
|
+
printf '{"seq":%d,"prev_hash":"%s","ts":"%s","repo":"%s","event":"push_blocked","reason":"%s","user":"%s"}\n' "$seq" "$prev_hash" "$ts" "$(json_escape "$repo_name")" "$(json_escape "$reason")" "$(json_escape "$user")" >> "$audit_log" 2>/dev/null
|
|
458
536
|
|
|
459
537
|
# Update tail hash sidecar
|
|
460
538
|
if [ -s "$audit_log" ]; then
|
|
@@ -547,6 +625,13 @@ run_test_mode() {
|
|
|
547
625
|
(
|
|
548
626
|
export AESOP_ROOT="$tmpdir/aesop"
|
|
549
627
|
mkdir -p "$AESOP_ROOT/state"
|
|
628
|
+
# SCOPED (identity-polluter class): quoted-name check runs inside an
|
|
629
|
+
# isolated fixture repo; must NEVER touch the live repo config.
|
|
630
|
+
SELFTEST_REPO="$tmpdir/selftest_identity_repo"
|
|
631
|
+
mkdir -p "$SELFTEST_REPO"
|
|
632
|
+
cd "$SELFTEST_REPO" || exit 1
|
|
633
|
+
git init -q
|
|
634
|
+
git config user.email "test@example.com"
|
|
550
635
|
git config user.name 'John "Jack" Doe'
|
|
551
636
|
log_block "reason_with_backslash\\test"
|
|
552
637
|
|
|
@@ -631,13 +716,18 @@ run_test_mode() {
|
|
|
631
716
|
export AESOP_ROOT="$tmpdir/aesop_no_scanner"
|
|
632
717
|
mkdir -p "$AESOP_ROOT/state"
|
|
633
718
|
|
|
719
|
+
# Provide a valid 4-field tuple to test the scanner-missing condition
|
|
720
|
+
# (empty stdin would now skip the scan with log_event "secret_scan_skipped_empty_stdin")
|
|
721
|
+
# Format: <local-ref> <local-sha> <remote-ref> <remote-sha>
|
|
722
|
+
stdin_input="refs/heads/feature/test abc123def456 refs/heads/feature/test 0000000000000000000000000000000000000000"
|
|
723
|
+
|
|
634
724
|
# Capture stderr to verify warning is printed
|
|
635
|
-
stderr_output=$( { check_secret_scan; } 2>&1 1>/dev/null )
|
|
725
|
+
stderr_output=$( { printf '%s\n' "$stdin_input" | check_secret_scan; } 2>&1 1>/dev/null )
|
|
636
726
|
exit_code=$?
|
|
637
727
|
|
|
638
|
-
# Should return
|
|
639
|
-
if [ "$exit_code" -ne
|
|
640
|
-
printf 'FAIL: check_secret_scan should return
|
|
728
|
+
# Should return 1 (fail-closed, security-safe default)
|
|
729
|
+
if [ "$exit_code" -ne 1 ]; then
|
|
730
|
+
printf 'FAIL: check_secret_scan should return 1 when scanner missing (fail-closed)\n'
|
|
641
731
|
exit 1
|
|
642
732
|
fi
|
|
643
733
|
|
|
@@ -656,22 +746,22 @@ run_test_mode() {
|
|
|
656
746
|
exit 1
|
|
657
747
|
fi
|
|
658
748
|
|
|
659
|
-
# Verify event type is "
|
|
660
|
-
if ! printf '%s' "$audit_line" | grep -q '"event":"
|
|
749
|
+
# Verify event type is "push_blocked" (fail-closed logged as blocked)
|
|
750
|
+
if ! printf '%s' "$audit_line" | grep -q '"event":"push_blocked"'; then
|
|
661
751
|
printf 'FAIL: Audit log entry missing correct event type\n'
|
|
662
|
-
printf 'Expected event: "
|
|
752
|
+
printf 'Expected event: "push_blocked"\n'
|
|
663
753
|
printf 'Entry: %s\n' "$audit_line"
|
|
664
754
|
exit 1
|
|
665
755
|
fi
|
|
666
756
|
|
|
667
757
|
# Verify a warning was printed to stderr
|
|
668
|
-
if ! printf '%s' "$stderr_output" | grep -q -i 'unavailable\|missing\|not found'; then
|
|
758
|
+
if ! printf '%s' "$stderr_output" | grep -q -i 'unavailable\|missing\|not found\|fatal'; then
|
|
669
759
|
printf 'FAIL: No warning message printed to stderr\n'
|
|
670
760
|
printf 'stderr was: %s\n' "$stderr_output"
|
|
671
761
|
exit 1
|
|
672
762
|
fi
|
|
673
763
|
|
|
674
|
-
printf 'PASS: Secret scan unavailable
|
|
764
|
+
printf 'PASS: Secret scan unavailable fails-closed and logged as push_blocked\n'
|
|
675
765
|
)
|
|
676
766
|
if [ $? -eq 0 ]; then
|
|
677
767
|
test_passed=$((test_passed + 1))
|
|
@@ -957,15 +1047,23 @@ main() {
|
|
|
957
1047
|
exit $?
|
|
958
1048
|
fi
|
|
959
1049
|
|
|
1050
|
+
# Option B: Fail-closed TTY semantics — block interactive invocation before stdin capture.
|
|
1051
|
+
# git pre-push ALWAYS pipes stdin; tty means human ran hook directly (not via git push).
|
|
1052
|
+
# Refusing to skip security checks in interactive mode closes the window where empty stdin
|
|
1053
|
+
# could be misinterpreted as a legitimate up-to-date push.
|
|
1054
|
+
if [ -t 0 ]; then
|
|
1055
|
+
printf 'Error: interactive invocation: this hook runs under git push; refusing to skip security checks (fail-closed)\n' >&2
|
|
1056
|
+
log_block "interactive_invocation_blocked"
|
|
1057
|
+
exit 1
|
|
1058
|
+
fi
|
|
1059
|
+
|
|
960
1060
|
# git pre-push provides ref info on stdin, and BOTH check_branch_policy and
|
|
961
1061
|
# check_secret_scan (via get_commit_range) need to read every ref tuple.
|
|
962
1062
|
# Capture the real pipe ONCE here and hand each consumer its own here-string
|
|
963
1063
|
# copy -- reading a pipe twice on the same fd starves the second reader
|
|
964
1064
|
# (once check_branch_policy drains it, get_commit_range would see nothing
|
|
965
1065
|
# but EOF and fail-closed on every push). A here-string preserves each
|
|
966
|
-
# function's existing tty-vs-pipe read semantics unchanged
|
|
967
|
-
# tty yields no captured content, which both functions already treat the
|
|
968
|
-
# same way as "no ref tuples" via their existing fallback/fail-closed paths).
|
|
1066
|
+
# function's existing tty-vs-pipe read semantics unchanged.
|
|
969
1067
|
local prepush_stdin=""
|
|
970
1068
|
if [ ! -t 0 ]; then
|
|
971
1069
|
prepush_stdin=$(cat)
|
package/mcp/server.mjs
CHANGED
|
@@ -6,10 +6,14 @@
|
|
|
6
6
|
* Resolves AESOP_ROOT from env or --root flag; gracefully handles missing state files.
|
|
7
7
|
*
|
|
8
8
|
* Tools:
|
|
9
|
-
* fleet_status
|
|
10
|
-
* fleet_agents
|
|
11
|
-
* fleet_tracker
|
|
12
|
-
* fleet_cost
|
|
9
|
+
* fleet_status - heartbeat ages + orchestrator status + alert count
|
|
10
|
+
* fleet_agents - active agents from transcripts via dash-extra.mjs passthrough
|
|
11
|
+
* fleet_tracker - open items by lane from state/tracker.json
|
|
12
|
+
* fleet_cost - per-model token totals from state/ledger/OUTCOMES-LEDGER.md
|
|
13
|
+
* fleet_cost_by_wave - per-wave token totals from state/ledger/OUTCOMES-LEDGER.md
|
|
14
|
+
* fleet_budget - cost ceiling, current spend, remaining headroom, halt status
|
|
15
|
+
* fleet_cost_trend - per-wave token trend over last N waves from ledger
|
|
16
|
+
* fleet_verify_stats - defect escape stats (first-try-green, fix-forward rate) if available
|
|
13
17
|
*
|
|
14
18
|
* All tools are read-only; no state mutations, no file writes.
|
|
15
19
|
*/
|
|
@@ -399,6 +403,268 @@ function getFleetCost() {
|
|
|
399
403
|
return result;
|
|
400
404
|
}
|
|
401
405
|
|
|
406
|
+
/**
|
|
407
|
+
* fleet_cost_by_wave: Parse ledger and aggregate token counts by wave
|
|
408
|
+
*/
|
|
409
|
+
function getFleetCostByWave() {
|
|
410
|
+
const result = {
|
|
411
|
+
absent: !fs.existsSync(LEDGER_FILE),
|
|
412
|
+
by_wave: {},
|
|
413
|
+
total_tokens_in: 0,
|
|
414
|
+
total_tokens_out: 0
|
|
415
|
+
};
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
if (fs.existsSync(LEDGER_FILE)) {
|
|
419
|
+
const lines = fs.readFileSync(LEDGER_FILE, 'utf8').split('\n');
|
|
420
|
+
|
|
421
|
+
for (const line of lines) {
|
|
422
|
+
// Parse markdown table row: | ts | agent_type | model | dur | tokens_in | tokens_out | verdict | phase | wave |
|
|
423
|
+
// Regex to extract all pipe-delimited columns
|
|
424
|
+
if (!line.startsWith('|') || !line.endsWith('|')) continue;
|
|
425
|
+
|
|
426
|
+
const parts = line.split('|').map(p => p.trim()).filter(p => p !== '');
|
|
427
|
+
|
|
428
|
+
// Need at least 9 parts (ts, agent_type, model, dur, tokens_in, tokens_out, verdict, phase, wave)
|
|
429
|
+
if (parts.length < 9) continue;
|
|
430
|
+
|
|
431
|
+
// Skip header line
|
|
432
|
+
if (parts[0].toLowerCase() === 'iso ts' || parts[0].toLowerCase() === 'timestamp') continue;
|
|
433
|
+
if (parts[0].toLowerCase().includes('iso') || parts[1].toLowerCase() === 'agent_type') continue;
|
|
434
|
+
|
|
435
|
+
// Skip separator lines (all dashes and pipes)
|
|
436
|
+
if (/^-+$/.test(parts[0])) continue;
|
|
437
|
+
|
|
438
|
+
try {
|
|
439
|
+
const tokensIn = parseInt(parts[4], 10);
|
|
440
|
+
const tokensOut = parseInt(parts[5], 10);
|
|
441
|
+
|
|
442
|
+
// Skip if tokens are not valid numbers
|
|
443
|
+
if (isNaN(tokensIn) || isNaN(tokensOut)) continue;
|
|
444
|
+
|
|
445
|
+
const wave = parts[8].trim() || 'unknown';
|
|
446
|
+
|
|
447
|
+
if (!result.by_wave[wave]) {
|
|
448
|
+
result.by_wave[wave] = {
|
|
449
|
+
tokens_in: 0,
|
|
450
|
+
tokens_out: 0,
|
|
451
|
+
total_tokens: 0,
|
|
452
|
+
count: 0
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
result.by_wave[wave].tokens_in += tokensIn;
|
|
457
|
+
result.by_wave[wave].tokens_out += tokensOut;
|
|
458
|
+
result.by_wave[wave].total_tokens += tokensIn + tokensOut;
|
|
459
|
+
result.by_wave[wave].count += 1;
|
|
460
|
+
|
|
461
|
+
result.total_tokens_in += tokensIn;
|
|
462
|
+
result.total_tokens_out += tokensOut;
|
|
463
|
+
} catch (e) {
|
|
464
|
+
// Skip malformed rows
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
} catch (e) {
|
|
470
|
+
// Silently ignore errors
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* fleet_budget: Read cost ceiling from config and calculate remaining headroom
|
|
478
|
+
*/
|
|
479
|
+
function getFleetBudget() {
|
|
480
|
+
const result = {
|
|
481
|
+
period: 'wave',
|
|
482
|
+
ceiling: null,
|
|
483
|
+
spent: 0,
|
|
484
|
+
remaining: null,
|
|
485
|
+
halted: false,
|
|
486
|
+
halt_reason: null,
|
|
487
|
+
halt_timestamp: null
|
|
488
|
+
};
|
|
489
|
+
|
|
490
|
+
try {
|
|
491
|
+
// Check if halted
|
|
492
|
+
const haltSentinelPath = path.join(STATE_ROOT, '.HALT');
|
|
493
|
+
if (fs.existsSync(haltSentinelPath)) {
|
|
494
|
+
result.halted = true;
|
|
495
|
+
try {
|
|
496
|
+
const haltData = JSON.parse(fs.readFileSync(haltSentinelPath, 'utf8'));
|
|
497
|
+
result.halt_reason = haltData.reason || null;
|
|
498
|
+
result.halt_timestamp = haltData.timestamp || null;
|
|
499
|
+
} catch (e) {
|
|
500
|
+
result.halt_reason = '(unreadable sentinel)';
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Read ceiling from config
|
|
505
|
+
if (config && config.limits && config.limits.max_wave_tokens !== null) {
|
|
506
|
+
result.ceiling = config.limits.max_wave_tokens;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Calculate spent tokens from ledger
|
|
510
|
+
if (fs.existsSync(LEDGER_FILE)) {
|
|
511
|
+
const lines = fs.readFileSync(LEDGER_FILE, 'utf8').split('\n');
|
|
512
|
+
|
|
513
|
+
for (const line of lines) {
|
|
514
|
+
if (!line.startsWith('|') || !line.endsWith('|')) continue;
|
|
515
|
+
|
|
516
|
+
const parts = line.split('|').map(p => p.trim()).filter(p => p !== '');
|
|
517
|
+
|
|
518
|
+
// Need at least 7 parts (ts, agent_type, model, dur, tokens_in, tokens_out, verdict, ...)
|
|
519
|
+
if (parts.length < 7) continue;
|
|
520
|
+
|
|
521
|
+
// Skip header lines
|
|
522
|
+
if (parts[0].toLowerCase() === 'iso ts' || parts[0].toLowerCase() === 'timestamp') continue;
|
|
523
|
+
if (parts[0].toLowerCase().includes('iso') || parts[1].toLowerCase() === 'agent_type') continue;
|
|
524
|
+
|
|
525
|
+
// Skip separator lines (all dashes)
|
|
526
|
+
if (/^-+$/.test(parts[0])) continue;
|
|
527
|
+
|
|
528
|
+
try {
|
|
529
|
+
const tokensIn = parseInt(parts[4], 10);
|
|
530
|
+
const tokensOut = parseInt(parts[5], 10);
|
|
531
|
+
|
|
532
|
+
// Skip if tokens are not valid numbers
|
|
533
|
+
if (isNaN(tokensIn) || isNaN(tokensOut)) continue;
|
|
534
|
+
|
|
535
|
+
result.spent += tokensIn + tokensOut;
|
|
536
|
+
} catch (e) {
|
|
537
|
+
// Skip malformed rows
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Calculate remaining headroom
|
|
544
|
+
if (result.ceiling !== null) {
|
|
545
|
+
result.remaining = Math.max(0, result.ceiling - result.spent);
|
|
546
|
+
}
|
|
547
|
+
} catch (e) {
|
|
548
|
+
// Silently ignore errors
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return result;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* fleet_cost_trend: Parse ledger and return per-wave token trend over last N waves
|
|
556
|
+
*/
|
|
557
|
+
function getFleetCostTrend(params = {}) {
|
|
558
|
+
const result = {
|
|
559
|
+
absent: !fs.existsSync(LEDGER_FILE),
|
|
560
|
+
trend: [],
|
|
561
|
+
period_count: 0
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const N = params.n || 10;
|
|
565
|
+
|
|
566
|
+
try {
|
|
567
|
+
if (fs.existsSync(LEDGER_FILE)) {
|
|
568
|
+
const lines = fs.readFileSync(LEDGER_FILE, 'utf8').split('\n');
|
|
569
|
+
|
|
570
|
+
// Parse all waves from ledger
|
|
571
|
+
const waveData = {};
|
|
572
|
+
const waveOrder = [];
|
|
573
|
+
|
|
574
|
+
for (const line of lines) {
|
|
575
|
+
if (!line.startsWith('|') || !line.endsWith('|')) continue;
|
|
576
|
+
|
|
577
|
+
const parts = line.split('|').map(p => p.trim()).filter(p => p !== '');
|
|
578
|
+
|
|
579
|
+
if (parts.length < 9) continue;
|
|
580
|
+
|
|
581
|
+
// Skip header and separator lines
|
|
582
|
+
if (parts[0].toLowerCase() === 'iso ts' || parts[0].toLowerCase() === 'timestamp') continue;
|
|
583
|
+
if (parts[0].toLowerCase().includes('iso') || parts[1].toLowerCase() === 'agent_type') continue;
|
|
584
|
+
if (/^-+$/.test(parts[0])) continue;
|
|
585
|
+
|
|
586
|
+
try {
|
|
587
|
+
const tokensIn = parseInt(parts[4], 10);
|
|
588
|
+
const tokensOut = parseInt(parts[5], 10);
|
|
589
|
+
|
|
590
|
+
if (isNaN(tokensIn) || isNaN(tokensOut)) continue;
|
|
591
|
+
|
|
592
|
+
const wave = parts[8].trim() || 'unknown';
|
|
593
|
+
|
|
594
|
+
if (!waveData[wave]) {
|
|
595
|
+
waveData[wave] = {
|
|
596
|
+
tokens_in: 0,
|
|
597
|
+
tokens_out: 0,
|
|
598
|
+
total_tokens: 0,
|
|
599
|
+
count: 0
|
|
600
|
+
};
|
|
601
|
+
waveOrder.push(wave);
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
waveData[wave].tokens_in += tokensIn;
|
|
605
|
+
waveData[wave].tokens_out += tokensOut;
|
|
606
|
+
waveData[wave].total_tokens += tokensIn + tokensOut;
|
|
607
|
+
waveData[wave].count += 1;
|
|
608
|
+
} catch (e) {
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
// Get last N waves in chronological order
|
|
614
|
+
const lastNWaves = waveOrder.slice(-N);
|
|
615
|
+
result.trend = lastNWaves.map(wave => ({
|
|
616
|
+
wave,
|
|
617
|
+
total_tokens: waveData[wave].total_tokens,
|
|
618
|
+
tokens_in: waveData[wave].tokens_in,
|
|
619
|
+
tokens_out: waveData[wave].tokens_out,
|
|
620
|
+
count: waveData[wave].count
|
|
621
|
+
}));
|
|
622
|
+
result.period_count = lastNWaves.length;
|
|
623
|
+
|
|
624
|
+
if (result.period_count === 0) {
|
|
625
|
+
result.absent = true;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
} catch (e) {
|
|
629
|
+
// Silently ignore errors
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return result;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* fleet_verify_stats: Read defect escape stats if available
|
|
637
|
+
* Attempts to read pre-computed state/verify-stats.json or invoke defect_escape.py
|
|
638
|
+
*/
|
|
639
|
+
function getFleetVerifyStats() {
|
|
640
|
+
const result = {
|
|
641
|
+
absent: true,
|
|
642
|
+
first_try_green: null,
|
|
643
|
+
fix_forward_rate: null,
|
|
644
|
+
source: null
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
// Try to read pre-computed stats file
|
|
648
|
+
const statsFile = path.join(STATE_ROOT, 'verify-stats.json');
|
|
649
|
+
if (fs.existsSync(statsFile)) {
|
|
650
|
+
try {
|
|
651
|
+
const content = fs.readFileSync(statsFile, 'utf8').trim();
|
|
652
|
+
if (content) {
|
|
653
|
+
const stats = JSON.parse(content);
|
|
654
|
+
result.absent = false;
|
|
655
|
+
result.first_try_green = stats.first_try_estimate || null;
|
|
656
|
+
result.fix_forward_rate = stats.fixforward_rate || null;
|
|
657
|
+
result.source = 'verify-stats.json';
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
} catch (e) {
|
|
661
|
+
// Silently ignore parse errors
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
return result;
|
|
666
|
+
}
|
|
667
|
+
|
|
402
668
|
// ============================================================================
|
|
403
669
|
// MCP Tool & Resource Definitions
|
|
404
670
|
// ============================================================================
|
|
@@ -435,6 +701,44 @@ const TOOLS = [
|
|
|
435
701
|
type: 'object',
|
|
436
702
|
properties: {}
|
|
437
703
|
}
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: 'fleet_cost_by_wave',
|
|
707
|
+
description: 'Get per-wave token usage totals from outcomes ledger, grouped by wave column',
|
|
708
|
+
inputSchema: {
|
|
709
|
+
type: 'object',
|
|
710
|
+
properties: {}
|
|
711
|
+
}
|
|
712
|
+
},
|
|
713
|
+
{
|
|
714
|
+
name: 'fleet_budget',
|
|
715
|
+
description: 'Get cost budget status: configured ceiling, current spend, remaining headroom, and halt status',
|
|
716
|
+
inputSchema: {
|
|
717
|
+
type: 'object',
|
|
718
|
+
properties: {}
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
{
|
|
722
|
+
name: 'fleet_cost_trend',
|
|
723
|
+
description: 'Get per-wave token usage trend over the last N waves from outcomes ledger',
|
|
724
|
+
inputSchema: {
|
|
725
|
+
type: 'object',
|
|
726
|
+
properties: {
|
|
727
|
+
n: {
|
|
728
|
+
type: 'integer',
|
|
729
|
+
description: 'Number of waves to return (default: 10)',
|
|
730
|
+
default: 10
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
},
|
|
735
|
+
{
|
|
736
|
+
name: 'fleet_verify_stats',
|
|
737
|
+
description: 'Get defect escape stats (first-try-green rate, fix-forward rate) if available',
|
|
738
|
+
inputSchema: {
|
|
739
|
+
type: 'object',
|
|
740
|
+
properties: {}
|
|
741
|
+
}
|
|
438
742
|
}
|
|
439
743
|
];
|
|
440
744
|
|
|
@@ -479,6 +783,18 @@ async function handleToolCall(requestId, params) {
|
|
|
479
783
|
case 'fleet_cost':
|
|
480
784
|
result = getFleetCost();
|
|
481
785
|
break;
|
|
786
|
+
case 'fleet_cost_by_wave':
|
|
787
|
+
result = getFleetCostByWave();
|
|
788
|
+
break;
|
|
789
|
+
case 'fleet_budget':
|
|
790
|
+
result = getFleetBudget();
|
|
791
|
+
break;
|
|
792
|
+
case 'fleet_cost_trend':
|
|
793
|
+
result = getFleetCostTrend(args);
|
|
794
|
+
break;
|
|
795
|
+
case 'fleet_verify_stats':
|
|
796
|
+
result = getFleetVerifyStats();
|
|
797
|
+
break;
|
|
482
798
|
default:
|
|
483
799
|
mcp.writeError(requestId, -32601, `Unknown tool: ${name}`);
|
|
484
800
|
return;
|