@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
package/hooks/pre-push-policy.sh
CHANGED
|
@@ -256,17 +256,38 @@ check_branch_policy() {
|
|
|
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,44 @@ 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
326
|
# Running interactively on a tty; no stdin to parse
|
|
302
327
|
return 1
|
|
303
328
|
fi
|
|
304
329
|
|
|
330
|
+
local saw_delete=0
|
|
305
331
|
while IFS=' ' read -r local_ref local_sha remote_ref remote_sha || [ -n "$local_ref" ]; do
|
|
306
|
-
# Skip empty lines
|
|
307
|
-
if
|
|
332
|
+
# Skip truly empty lines (no fields at all)
|
|
333
|
+
# A line with any content (even if malformed) will have at least one non-empty field
|
|
334
|
+
if [ -z "$local_ref" ] && [ -z "$local_sha" ] && [ -z "$remote_ref" ] && [ -z "$remote_sha" ]; then
|
|
335
|
+
continue
|
|
336
|
+
fi
|
|
337
|
+
|
|
338
|
+
# Harden tuple parsing: require all 4 fields present (P1 fix)
|
|
339
|
+
# If we have some content but not all 4 fields, it's malformed
|
|
340
|
+
if [ -z "$remote_ref" ] || [ -z "$remote_sha" ]; then
|
|
341
|
+
# Malformed line: has fewer than 4 fields
|
|
342
|
+
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
|
|
343
|
+
return 1
|
|
344
|
+
fi
|
|
345
|
+
|
|
346
|
+
saw_any_tuple=1
|
|
347
|
+
|
|
348
|
+
# Delete refspec (local sha all zeros): no content is pushed, so there is
|
|
349
|
+
# no commit range to scan. Skipped here; if the WHOLE push is deletes we
|
|
350
|
+
# return 2 so the caller can pass without weakening fail-closed behavior
|
|
351
|
+
# for unparseable stdin (which stays return 1).
|
|
352
|
+
if [ "$local_sha" = "0000000000000000000000000000000000000000" ]; then
|
|
353
|
+
saw_delete=1
|
|
308
354
|
continue
|
|
309
355
|
fi
|
|
310
356
|
|
|
@@ -327,6 +373,15 @@ get_commit_range() {
|
|
|
327
373
|
if [ "$found" -eq 1 ]; then
|
|
328
374
|
return 0
|
|
329
375
|
fi
|
|
376
|
+
if [ "$saw_delete" -eq 1 ]; then
|
|
377
|
+
# Delete-only push: nothing to scan (distinct from unparseable stdin)
|
|
378
|
+
return 2
|
|
379
|
+
fi
|
|
380
|
+
if [ "$saw_any_tuple" -eq 0 ]; then
|
|
381
|
+
# Truly empty stdin: no tuples at all (e.g., up-to-date push) (P1 fix: rc=3)
|
|
382
|
+
return 3
|
|
383
|
+
fi
|
|
384
|
+
# Malformed stdin: tuples present but none were valid
|
|
330
385
|
return 1
|
|
331
386
|
}
|
|
332
387
|
|
|
@@ -340,30 +395,51 @@ check_secret_scan() {
|
|
|
340
395
|
scan_bin="python"
|
|
341
396
|
fi
|
|
342
397
|
|
|
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.
|
|
398
|
+
# Parse pre-push stdin FIRST: a delete-only push carries no content, so it
|
|
399
|
+
# needs no scanner at all — the availability check below only applies when
|
|
400
|
+
# there is actually content to scan (ordering matters: in scanner-less
|
|
401
|
+
# environments a branch deletion must still be possible).
|
|
354
402
|
# A multi-ref push (git push --all, or multiple branches/tags in one
|
|
355
403
|
# invocation) yields one range per line from get_commit_range(); scan EVERY
|
|
356
404
|
# one and fail if ANY range is dirty (P3 wave-25 fix: previously only the
|
|
357
405
|
# first ref tuple's range was ever scanned).
|
|
406
|
+
# P1 bug fix: distinguish empty stdin (rc=3, allow) from malformed stdin
|
|
407
|
+
# (rc=1, fail-closed).
|
|
358
408
|
local commit_ranges
|
|
359
409
|
commit_ranges=$(get_commit_range)
|
|
360
410
|
local parse_exit_code=$?
|
|
361
411
|
|
|
412
|
+
if [ $parse_exit_code -eq 2 ]; then
|
|
413
|
+
# Delete-only push: no content pushed, nothing to scan. Explicitly allowed
|
|
414
|
+
# (rc=2 is only emitted when tuples WERE parsed and all were deletions);
|
|
415
|
+
# unparseable/empty stdin still fails closed below.
|
|
416
|
+
log_event "secret_scan_skipped_delete_only_push"
|
|
417
|
+
return 0
|
|
418
|
+
fi
|
|
419
|
+
|
|
420
|
+
if [ $parse_exit_code -eq 3 ]; then
|
|
421
|
+
# Empty stdin: no tuples at all (e.g., up-to-date push), nothing to scan (P1 fix: rc=3)
|
|
422
|
+
# This is distinct from malformed stdin (rc=1) and is explicitly allowed
|
|
423
|
+
log_event "secret_scan_skipped_empty_stdin"
|
|
424
|
+
return 0
|
|
425
|
+
fi
|
|
426
|
+
|
|
427
|
+
local aesop_root="${AESOP_ROOT:-$HOME/aesop}"
|
|
428
|
+
local scan_script="$aesop_root/tools/secret_scan.py"
|
|
429
|
+
|
|
430
|
+
if [ ! -f "$scan_script" ] || [ ! -x "$scan_script" ]; then
|
|
431
|
+
# Scanner not found or not executable: fail-closed (cannot verify => deny)
|
|
432
|
+
log_block "secret_scan_unavailable"
|
|
433
|
+
printf 'FATAL: secret_scan.py not found or not executable at %s\n' "$scan_script" >&2
|
|
434
|
+
return 1
|
|
435
|
+
fi
|
|
436
|
+
|
|
362
437
|
if [ $parse_exit_code -ne 0 ] || [ -z "$commit_ranges" ]; then
|
|
363
|
-
# Malformed stdin
|
|
438
|
+
# Malformed stdin: fail-CLOSED (security P1 fix)
|
|
364
439
|
# Only fail-open for missing scanner tool, not for malformed input
|
|
440
|
+
# rc=1 is returned for malformed tuples (e.g., 3-field lines), which must fail-closed
|
|
365
441
|
log_block "secret_scan_stdin_parse_failed"
|
|
366
|
-
printf 'Error: Could not parse pre-push stdin for commit range (malformed
|
|
442
|
+
printf 'Error: Could not parse pre-push stdin for commit range (malformed tuple)\n' >&2
|
|
367
443
|
return 1
|
|
368
444
|
fi
|
|
369
445
|
|
|
@@ -417,7 +493,7 @@ log_event() {
|
|
|
417
493
|
local seq
|
|
418
494
|
seq=$(get_next_seq "$audit_log")
|
|
419
495
|
|
|
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
|
|
496
|
+
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
497
|
|
|
422
498
|
# Update tail hash sidecar
|
|
423
499
|
if [ -s "$audit_log" ]; then
|
|
@@ -454,7 +530,7 @@ log_block() {
|
|
|
454
530
|
local seq
|
|
455
531
|
seq=$(get_next_seq "$audit_log")
|
|
456
532
|
|
|
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
|
|
533
|
+
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
534
|
|
|
459
535
|
# Update tail hash sidecar
|
|
460
536
|
if [ -s "$audit_log" ]; then
|
|
@@ -631,13 +707,18 @@ run_test_mode() {
|
|
|
631
707
|
export AESOP_ROOT="$tmpdir/aesop_no_scanner"
|
|
632
708
|
mkdir -p "$AESOP_ROOT/state"
|
|
633
709
|
|
|
710
|
+
# Provide a valid 4-field tuple to test the scanner-missing condition
|
|
711
|
+
# (empty stdin would now skip the scan with log_event "secret_scan_skipped_empty_stdin")
|
|
712
|
+
# Format: <local-ref> <local-sha> <remote-ref> <remote-sha>
|
|
713
|
+
stdin_input="refs/heads/feature/test abc123def456 refs/heads/feature/test 0000000000000000000000000000000000000000"
|
|
714
|
+
|
|
634
715
|
# Capture stderr to verify warning is printed
|
|
635
|
-
stderr_output=$( { check_secret_scan; } 2>&1 1>/dev/null )
|
|
716
|
+
stderr_output=$( { printf '%s\n' "$stdin_input" | check_secret_scan; } 2>&1 1>/dev/null )
|
|
636
717
|
exit_code=$?
|
|
637
718
|
|
|
638
|
-
# Should return
|
|
639
|
-
if [ "$exit_code" -ne
|
|
640
|
-
printf 'FAIL: check_secret_scan should return
|
|
719
|
+
# Should return 1 (fail-closed, security-safe default)
|
|
720
|
+
if [ "$exit_code" -ne 1 ]; then
|
|
721
|
+
printf 'FAIL: check_secret_scan should return 1 when scanner missing (fail-closed)\n'
|
|
641
722
|
exit 1
|
|
642
723
|
fi
|
|
643
724
|
|
|
@@ -656,22 +737,22 @@ run_test_mode() {
|
|
|
656
737
|
exit 1
|
|
657
738
|
fi
|
|
658
739
|
|
|
659
|
-
# Verify event type is "
|
|
660
|
-
if ! printf '%s' "$audit_line" | grep -q '"event":"
|
|
740
|
+
# Verify event type is "push_blocked" (fail-closed logged as blocked)
|
|
741
|
+
if ! printf '%s' "$audit_line" | grep -q '"event":"push_blocked"'; then
|
|
661
742
|
printf 'FAIL: Audit log entry missing correct event type\n'
|
|
662
|
-
printf 'Expected event: "
|
|
743
|
+
printf 'Expected event: "push_blocked"\n'
|
|
663
744
|
printf 'Entry: %s\n' "$audit_line"
|
|
664
745
|
exit 1
|
|
665
746
|
fi
|
|
666
747
|
|
|
667
748
|
# Verify a warning was printed to stderr
|
|
668
|
-
if ! printf '%s' "$stderr_output" | grep -q -i 'unavailable\|missing\|not found'; then
|
|
749
|
+
if ! printf '%s' "$stderr_output" | grep -q -i 'unavailable\|missing\|not found\|fatal'; then
|
|
669
750
|
printf 'FAIL: No warning message printed to stderr\n'
|
|
670
751
|
printf 'stderr was: %s\n' "$stderr_output"
|
|
671
752
|
exit 1
|
|
672
753
|
fi
|
|
673
754
|
|
|
674
|
-
printf 'PASS: Secret scan unavailable
|
|
755
|
+
printf 'PASS: Secret scan unavailable fails-closed and logged as push_blocked\n'
|
|
675
756
|
)
|
|
676
757
|
if [ $? -eq 0 ]; then
|
|
677
758
|
test_passed=$((test_passed + 1))
|
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;
|
package/package.json
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matt82198/aesop",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Multi-agent orchestration for AI coding agents, designed to survive failure \
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Multi-agent orchestration for AI coding agents, designed to survive failure \u2014 a plain-file brain, git as the only durable layer, cheap subagent fleets, and guardrails enforced in code.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"aesop": "bin/cli.js"
|
|
7
7
|
},
|
|
8
8
|
"type": "commonjs",
|
|
9
9
|
"files": [
|
|
10
|
-
"bin
|
|
11
|
-
"daemons
|
|
12
|
-
"dash
|
|
13
|
-
"
|
|
14
|
-
"
|
|
10
|
+
"bin/*.js",
|
|
11
|
+
"daemons/*.sh",
|
|
12
|
+
"dash/*.mjs",
|
|
13
|
+
"dash/*.sh",
|
|
14
|
+
"monitor/collect-signals.mjs",
|
|
15
|
+
"monitor/CHARTER.md",
|
|
16
|
+
"mcp/*.mjs",
|
|
17
|
+
"mcp/package.json",
|
|
15
18
|
"tools/*.py",
|
|
16
19
|
"tools/*.sh",
|
|
17
20
|
"tools/*.mjs",
|
|
18
21
|
"tools/*.js",
|
|
19
|
-
"tools/*.md",
|
|
20
22
|
"ui/*.py",
|
|
21
23
|
"ui/api/*.py",
|
|
22
|
-
"ui
|
|
24
|
+
"ui/README.md",
|
|
23
25
|
"ui/web/dist/",
|
|
24
|
-
"docs
|
|
25
|
-
"
|
|
26
|
+
"docs/*.md",
|
|
27
|
+
"docs/templates/",
|
|
28
|
+
"docs/*.json",
|
|
29
|
+
"templates/",
|
|
30
|
+
"hooks/*.sh",
|
|
31
|
+
"hooks/claude/**",
|
|
26
32
|
"skills/",
|
|
27
33
|
"state_store/*.py",
|
|
28
|
-
"
|
|
29
|
-
"
|
|
34
|
+
"driver/*.py",
|
|
35
|
+
"driver/*.md",
|
|
36
|
+
"driver/*.json",
|
|
37
|
+
"scan/*.mjs",
|
|
30
38
|
"aesop.config.example.json",
|
|
31
39
|
"CLAUDE-TEMPLATE.md",
|
|
32
40
|
"README.md",
|
|
@@ -62,8 +70,8 @@
|
|
|
62
70
|
"scripts": {
|
|
63
71
|
"test": "node --test --test-force-exit --test-timeout=60000 tests/*.test.mjs",
|
|
64
72
|
"test:node": "node --test --test-force-exit --test-timeout=60000 tests/*.test.mjs",
|
|
65
|
-
"test:sh": "bash tests/test_pre_push_policy.sh && bash tests/test-run-watchdog.sh && bash tests/backup-fleet.test.sh && bash tests/test_reconstitute.sh && bash tests/test_reconstitute_fixes.sh && bash tests/test_agent_forensics.sh && bash hooks/pre-push-policy.sh --test && bash tools/reconstitute.sh --test",
|
|
73
|
+
"test:sh": "bash tests/test_pre_push_policy.sh && bash tests/test-run-watchdog.sh && bash tests/test-run-watchdog-halt.sh && bash tests/test-run-watchdog-lockguard.sh && bash tests/backup-fleet.test.sh && bash tests/test_reconstitute.sh && bash tests/test_reconstitute_fixes.sh && bash tests/test_agent_forensics.sh && bash tests/test-selfheal.sh && bash hooks/pre-push-policy.sh --test && bash tools/reconstitute.sh --test",
|
|
66
74
|
"test:py": "python -m unittest discover -s tests",
|
|
67
75
|
"test:all": "npm run test:node && npm run test:sh && npm run test:py"
|
|
68
76
|
}
|
|
69
|
-
}
|
|
77
|
+
}
|