@matt82198/aesop 0.1.0-beta.5 → 0.1.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 +158 -5
- package/LICENSE +66 -21
- package/README.md +97 -33
- package/aesop.config.example.json +6 -0
- package/bin/CLAUDE.md +12 -3
- package/bin/cli.js +211 -40
- package/daemons/CLAUDE.md +1 -1
- package/daemons/backup-fleet.sh +209 -51
- package/daemons/run-watchdog.sh +208 -62
- package/dash/dash-extra.mjs +73 -4
- package/dash/watchdog-gui.sh +41 -35
- package/docs/ARCHITECTURE.md +296 -0
- package/docs/CONCEPTS.md +254 -0
- package/docs/CONFIGURE.md +256 -0
- package/docs/FIRST-WAVE.md +276 -0
- package/docs/INSTALL.md +224 -0
- package/docs/README.md +176 -27
- package/docs/autonomous-swe.md +149 -0
- package/docs/reproduce.md +121 -0
- package/hooks/CLAUDE.md +28 -0
- package/hooks/install-waveguard.sh +68 -0
- package/hooks/pre-commit-waveguard.sh +26 -0
- package/hooks/pre-push-policy.sh +253 -15
- package/monitor/CLAUDE.md +3 -3
- package/monitor/collect-signals.mjs +133 -20
- package/package.json +15 -7
- package/scan/CLAUDE.md +30 -0
- package/skills/CLAUDE.md +1 -0
- package/state_store/ingest.py +18 -1
- package/state_store/projections.py +78 -4
- package/state_store/store.py +102 -7
- package/tools/CLAUDE.md +25 -18
- package/tools/alert_bridge.py +14 -10
- package/tools/bench_runner.py +438 -0
- package/tools/buildlog.py +4 -7
- package/tools/ci_merge_wait.py +215 -34
- package/tools/common.py +62 -0
- package/tools/cost_ceiling.py +191 -0
- package/tools/dash.js +102 -0
- package/tools/doctor.js +220 -0
- package/tools/fleet_ledger.py +189 -17
- package/tools/halt.py +172 -0
- package/tools/healthcheck.py +24 -21
- package/tools/heartbeat.py +4 -7
- package/tools/metrics_gate.py +8 -2
- package/tools/orchestrator_status.py +4 -9
- package/tools/reconcile.py +277 -0
- package/tools/rotate_logs.py +152 -62
- package/tools/scanner_selftest.py +28 -3
- package/tools/secret_scan.py +359 -90
- package/tools/self_stats.py +292 -4
- package/tools/status.js +187 -0
- package/tools/verify_agent_inspector.py +289 -0
- package/tools/verify_prboard.py +344 -0
- package/tools/watch.js +49 -0
- package/ui/CLAUDE.md +2 -0
- package/ui/agents.py +332 -33
- package/ui/api/tracker.py +15 -7
- package/ui/collectors.py +125 -32
- package/ui/config.py +22 -2
- package/ui/cost.py +98 -3
- package/ui/csrf.py +2 -1
- package/ui/handler.py +209 -21
- package/ui/sse.py +102 -16
- package/ui/wave_prs.py +230 -0
- package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
- package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
- package/ui/web/dist/index.html +2 -2
- package/monitor/.signal-state.json +0 -3
- package/monitor/ACTIONS.log +0 -1
- package/monitor/BRIEF.md +0 -24
- package/monitor/SIGNALS.json +0 -54
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/web/.gitattributes +0 -13
- package/ui/web/dist/assets/index-2LZDQirC.js +0 -9
- package/ui/web/dist/assets/index-D4M1qyOv.css +0 -1
- package/ui/web/index.html +0 -13
- package/ui/web/package-lock.json +0 -2225
- package/ui/web/package.json +0 -26
- package/ui/web/src/App.test.tsx +0 -74
- package/ui/web/src/App.tsx +0 -142
- package/ui/web/src/CONTRIBUTING-UI.md +0 -49
- package/ui/web/src/components/AgentRow.css +0 -187
- package/ui/web/src/components/AgentRow.test.tsx +0 -209
- package/ui/web/src/components/AgentRow.tsx +0 -207
- package/ui/web/src/components/AgentsPanel.css +0 -108
- package/ui/web/src/components/AgentsPanel.test.tsx +0 -41
- package/ui/web/src/components/AgentsPanel.tsx +0 -58
- package/ui/web/src/components/AlertsPanel.css +0 -88
- package/ui/web/src/components/AlertsPanel.test.tsx +0 -51
- package/ui/web/src/components/AlertsPanel.tsx +0 -67
- package/ui/web/src/components/BacklogPanel.test.tsx +0 -126
- package/ui/web/src/components/BacklogPanel.tsx +0 -122
- package/ui/web/src/components/CostChart.css +0 -110
- package/ui/web/src/components/CostChart.test.tsx +0 -144
- package/ui/web/src/components/CostChart.tsx +0 -152
- package/ui/web/src/components/CostTable.css +0 -93
- package/ui/web/src/components/CostTable.test.tsx +0 -165
- package/ui/web/src/components/CostTable.tsx +0 -94
- package/ui/web/src/components/EventsFeed.css +0 -68
- package/ui/web/src/components/EventsFeed.test.tsx +0 -36
- package/ui/web/src/components/EventsFeed.tsx +0 -31
- package/ui/web/src/components/HealthHeader.css +0 -137
- package/ui/web/src/components/HealthHeader.test.tsx +0 -278
- package/ui/web/src/components/HealthHeader.tsx +0 -281
- package/ui/web/src/components/InboxForm.css +0 -135
- package/ui/web/src/components/InboxForm.test.tsx +0 -208
- package/ui/web/src/components/InboxForm.tsx +0 -116
- package/ui/web/src/components/MessagesTail.module.css +0 -144
- package/ui/web/src/components/MessagesTail.test.tsx +0 -176
- package/ui/web/src/components/MessagesTail.tsx +0 -94
- package/ui/web/src/components/ReposPanel.css +0 -90
- package/ui/web/src/components/ReposPanel.test.tsx +0 -45
- package/ui/web/src/components/ReposPanel.tsx +0 -67
- package/ui/web/src/components/Scorecard.css +0 -106
- package/ui/web/src/components/Scorecard.test.tsx +0 -117
- package/ui/web/src/components/Scorecard.tsx +0 -85
- package/ui/web/src/components/Timeline.module.css +0 -151
- package/ui/web/src/components/Timeline.test.tsx +0 -215
- package/ui/web/src/components/Timeline.tsx +0 -99
- package/ui/web/src/components/TrackerBoard.test.tsx +0 -121
- package/ui/web/src/components/TrackerBoard.tsx +0 -107
- package/ui/web/src/components/TrackerCard.test.tsx +0 -180
- package/ui/web/src/components/TrackerCard.tsx +0 -160
- package/ui/web/src/components/TrackerForm.test.tsx +0 -189
- package/ui/web/src/components/TrackerForm.tsx +0 -144
- package/ui/web/src/lib/api.ts +0 -218
- package/ui/web/src/lib/format.test.ts +0 -89
- package/ui/web/src/lib/format.ts +0 -103
- package/ui/web/src/lib/sanitizeUrl.test.ts +0 -84
- package/ui/web/src/lib/sanitizeUrl.ts +0 -38
- package/ui/web/src/lib/types.ts +0 -230
- package/ui/web/src/lib/useHashRoute.test.ts +0 -60
- package/ui/web/src/lib/useHashRoute.ts +0 -23
- package/ui/web/src/lib/useSSE.ts +0 -175
- package/ui/web/src/main.tsx +0 -10
- package/ui/web/src/styles/global.css +0 -179
- package/ui/web/src/styles/theme.css +0 -184
- package/ui/web/src/styles/work.css +0 -572
- package/ui/web/src/test/fixtures.ts +0 -385
- package/ui/web/src/test/setup.ts +0 -49
- package/ui/web/src/views/Activity.module.css +0 -43
- package/ui/web/src/views/Activity.test.tsx +0 -89
- package/ui/web/src/views/Activity.tsx +0 -31
- package/ui/web/src/views/Cost.css +0 -87
- package/ui/web/src/views/Cost.test.tsx +0 -142
- package/ui/web/src/views/Cost.tsx +0 -54
- package/ui/web/src/views/Overview.css +0 -51
- package/ui/web/src/views/Overview.test.tsx +0 -76
- package/ui/web/src/views/Overview.tsx +0 -46
- package/ui/web/src/views/Work.test.tsx +0 -82
- package/ui/web/src/views/Work.tsx +0 -79
- package/ui/web/src/vite-env.d.ts +0 -10
- package/ui/web/tsconfig.json +0 -22
- package/ui/web/vite.config.ts +0 -25
- package/ui/web/vitest.config.ts +0 -12
package/skills/CLAUDE.md
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
# skills/ — Orchestration and priming skills
|
|
2
2
|
|
|
3
3
|
- **power/** — /power skill: prime the filesystem brain (load rules, dispatch model, memory, state) and report system health — see power/SKILL.md
|
|
4
|
+
- **healthcheck/** — /healthcheck skill: check fleet health — one colored ball (heartbeats, alerts, orchestrator, tracker items) — see healthcheck/SKILL.md
|
package/state_store/ingest.py
CHANGED
|
@@ -15,10 +15,27 @@ def ingest_tracker_json(api, tracker_json_path: str, actor: str = "migration") -
|
|
|
15
15
|
"""Append one ``item_created`` per item in ``tracker_json_path``.
|
|
16
16
|
|
|
17
17
|
Returns the number of items ingested.
|
|
18
|
+
Validates that each item is a dict; rejects malformed events with a clear error.
|
|
18
19
|
"""
|
|
19
20
|
with open(tracker_json_path, "r", encoding="utf-8") as fh:
|
|
20
21
|
data = json.load(fh)
|
|
22
|
+
|
|
23
|
+
if not isinstance(data, dict):
|
|
24
|
+
raise TypeError(
|
|
25
|
+
f"tracker.json root must be a dict, got {type(data).__name__}"
|
|
26
|
+
)
|
|
27
|
+
|
|
21
28
|
items = data.get("items", [])
|
|
22
|
-
|
|
29
|
+
if not isinstance(items, list):
|
|
30
|
+
raise TypeError(
|
|
31
|
+
f"tracker.json 'items' field must be a list, got {type(items).__name__}"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
for i, item in enumerate(items):
|
|
35
|
+
if not isinstance(item, dict):
|
|
36
|
+
raise TypeError(
|
|
37
|
+
f"tracker.json item at index {i} must be a dict, got {type(item).__name__}"
|
|
38
|
+
)
|
|
23
39
|
api.append("tracker", "item_created", item, actor)
|
|
40
|
+
|
|
24
41
|
return len(items)
|
|
@@ -12,16 +12,35 @@
|
|
|
12
12
|
|
|
13
13
|
Unknown ids on update/archive and unknown event types are ignored, keeping the
|
|
14
14
|
fold tolerant of partial/legacy streams.
|
|
15
|
+
|
|
16
|
+
Snapshots enable O(n) tail-replay (instead of replaying full log each time):
|
|
17
|
+
- save_snapshot(store, stream, event_version, projection): persist materialized state
|
|
18
|
+
- project_tracker_with_snapshot(store, stream, events): load snapshot, fold tail events
|
|
15
19
|
"""
|
|
16
20
|
from __future__ import annotations
|
|
17
21
|
|
|
18
22
|
TRACKER_VERSION = 1
|
|
19
23
|
|
|
20
24
|
|
|
21
|
-
def
|
|
22
|
-
"""
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
def _fold_events(events: list, order: list | None = None, items: dict | None = None) -> tuple:
|
|
26
|
+
"""Fold events into (order, items) state.
|
|
27
|
+
|
|
28
|
+
Helper used by both project_tracker and project_tracker_with_snapshot to avoid
|
|
29
|
+
duplicating the folding logic. Mutates order and items in place.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
events: list of event dicts
|
|
33
|
+
order: existing order list (item ids in first-seen order); mutated in place
|
|
34
|
+
items: existing items dict (id -> item dict); mutated in place
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
(order, items) tuple with accumulated state
|
|
38
|
+
"""
|
|
39
|
+
if order is None:
|
|
40
|
+
order = []
|
|
41
|
+
if items is None:
|
|
42
|
+
items = {}
|
|
43
|
+
|
|
25
44
|
for ev in events:
|
|
26
45
|
etype = ev.get("type")
|
|
27
46
|
payload = ev.get("payload") or {}
|
|
@@ -49,4 +68,59 @@ def project_tracker(events: list) -> dict:
|
|
|
49
68
|
merged["completed_at"] = payload["completed_at"]
|
|
50
69
|
items[iid] = merged
|
|
51
70
|
# any other event type is ignored
|
|
71
|
+
return (order, items)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def project_tracker(events: list) -> dict:
|
|
75
|
+
"""Return the current tracker state projected from ``events``.
|
|
76
|
+
|
|
77
|
+
This is the original full-replay projection. For production use with snapshots,
|
|
78
|
+
prefer project_tracker_with_snapshot() which enables tail-replay.
|
|
79
|
+
"""
|
|
80
|
+
order, items = _fold_events(events)
|
|
81
|
+
return {"version": TRACKER_VERSION, "items": [items[i] for i in order]}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def project_tracker_with_snapshot(store, stream: str, events: list) -> dict:
|
|
85
|
+
"""Project tracker state using snapshots for O(n) tail-replay instead of O(n²) full replay.
|
|
86
|
+
|
|
87
|
+
If a valid snapshot exists for the stream, starts from the snapshot state and folds
|
|
88
|
+
only events after the snapshot's version. If snapshot is missing or corrupt, falls
|
|
89
|
+
back to full replay from event 1 (graceful degradation).
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
store: EventStore instance with read_snapshot() method
|
|
93
|
+
stream: stream name (e.g., "tracker")
|
|
94
|
+
events: list of all events for the stream (from store.read(stream))
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
dict: {"version": 1, "items": [...]} projection, identical to project_tracker()
|
|
98
|
+
"""
|
|
99
|
+
snapshot = store.read_snapshot(stream)
|
|
100
|
+
|
|
101
|
+
if snapshot is not None:
|
|
102
|
+
# Snapshot exists and is valid; start from it and fold tail events
|
|
103
|
+
snapshot_version, projection, _ = snapshot
|
|
104
|
+
order = [item["id"] for item in projection.get("items", [])]
|
|
105
|
+
items = {item["id"]: dict(item) for item in projection.get("items", [])}
|
|
106
|
+
|
|
107
|
+
# Fold only events after the snapshot
|
|
108
|
+
tail_events = [ev for ev in events if ev.get("version", 0) > snapshot_version]
|
|
109
|
+
order, items = _fold_events(tail_events, order, items)
|
|
110
|
+
else:
|
|
111
|
+
# No valid snapshot; do full replay
|
|
112
|
+
order, items = _fold_events(events)
|
|
113
|
+
|
|
52
114
|
return {"version": TRACKER_VERSION, "items": [items[i] for i in order]}
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def save_snapshot(store, stream: str, event_version: int, projection: dict) -> None:
|
|
118
|
+
"""Persist a materialized projection snapshot.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
store: EventStore instance with save_snapshot() method
|
|
122
|
+
stream: stream name (e.g., "tracker")
|
|
123
|
+
event_version: the per-stream event version this snapshot was computed through
|
|
124
|
+
projection: the materialized state dict (e.g. full tracker projection)
|
|
125
|
+
"""
|
|
126
|
+
store.save_snapshot(stream, event_version, projection)
|
package/state_store/store.py
CHANGED
|
@@ -10,8 +10,10 @@ Stdlib only (sqlite3, json, time) per aesop's no-external-deps invariant.
|
|
|
10
10
|
"""
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import hashlib
|
|
13
14
|
import json
|
|
14
15
|
import sqlite3
|
|
16
|
+
import sys
|
|
15
17
|
import time
|
|
16
18
|
|
|
17
19
|
|
|
@@ -48,6 +50,22 @@ class EventStore:
|
|
|
48
50
|
"CREATE INDEX IF NOT EXISTS idx_events_stream_version "
|
|
49
51
|
"ON events(stream, version)"
|
|
50
52
|
)
|
|
53
|
+
conn.execute(
|
|
54
|
+
"""
|
|
55
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
56
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
57
|
+
ts REAL NOT NULL,
|
|
58
|
+
stream TEXT NOT NULL,
|
|
59
|
+
event_version INTEGER NOT NULL,
|
|
60
|
+
projection TEXT NOT NULL,
|
|
61
|
+
checksum TEXT NOT NULL
|
|
62
|
+
)
|
|
63
|
+
"""
|
|
64
|
+
)
|
|
65
|
+
conn.execute(
|
|
66
|
+
"CREATE INDEX IF NOT EXISTS idx_snapshots_stream_version "
|
|
67
|
+
"ON snapshots(stream, event_version)"
|
|
68
|
+
)
|
|
51
69
|
conn.commit()
|
|
52
70
|
finally:
|
|
53
71
|
conn.close()
|
|
@@ -91,12 +109,89 @@ class EventStore:
|
|
|
91
109
|
"SELECT id, ts, actor, stream, type, payload, version FROM events " + clause,
|
|
92
110
|
params,
|
|
93
111
|
)
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
112
|
+
rows = []
|
|
113
|
+
for r in cur.fetchall():
|
|
114
|
+
try:
|
|
115
|
+
payload = json.loads(r[5])
|
|
116
|
+
rows.append({
|
|
117
|
+
"id": r[0], "ts": r[1], "actor": r[2], "stream": r[3],
|
|
118
|
+
"type": r[4], "payload": payload, "version": r[6],
|
|
119
|
+
})
|
|
120
|
+
except json.JSONDecodeError as e:
|
|
121
|
+
# Log corrupt event (stream id + sequence) to stderr and skip
|
|
122
|
+
print(
|
|
123
|
+
f"WARNING: corrupt JSON payload in stream={r[3]} id={r[0]}: {e}",
|
|
124
|
+
file=sys.stderr,
|
|
125
|
+
)
|
|
126
|
+
return rows
|
|
127
|
+
finally:
|
|
128
|
+
conn.close()
|
|
129
|
+
|
|
130
|
+
def save_snapshot(self, stream: str, event_version: int, projection: dict) -> None:
|
|
131
|
+
"""Save a materialized projection snapshot at a specific event version.
|
|
132
|
+
|
|
133
|
+
The snapshot enables tail-replay: instead of replaying from event 1,
|
|
134
|
+
project() can resume from this snapshot's version and fold only newer events.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
stream: the stream name (e.g. "tracker")
|
|
138
|
+
event_version: the per-stream event version this snapshot was computed through
|
|
139
|
+
projection: the materialized state dict (e.g. the full tracker projection)
|
|
140
|
+
"""
|
|
141
|
+
conn = sqlite3.connect(self.db_path)
|
|
142
|
+
try:
|
|
143
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
144
|
+
projection_json = json.dumps(projection, separators=(",", ":"), sort_keys=True)
|
|
145
|
+
checksum = hashlib.sha256(projection_json.encode()).hexdigest()
|
|
146
|
+
conn.execute(
|
|
147
|
+
"INSERT INTO snapshots (ts, stream, event_version, projection, checksum) "
|
|
148
|
+
"VALUES (?, ?, ?, ?, ?)",
|
|
149
|
+
(time.time(), stream, event_version, projection_json, checksum),
|
|
150
|
+
)
|
|
151
|
+
conn.commit()
|
|
152
|
+
finally:
|
|
153
|
+
conn.close()
|
|
154
|
+
|
|
155
|
+
def read_snapshot(self, stream: str) -> tuple | None:
|
|
156
|
+
"""Read the most recent snapshot for a stream.
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
A tuple (event_version, projection_dict, checksum) if a valid snapshot exists,
|
|
160
|
+
or None if no snapshot found.
|
|
161
|
+
|
|
162
|
+
On corrupt/unreadable snapshot, logs a warning and returns None (falls back to
|
|
163
|
+
full replay).
|
|
164
|
+
"""
|
|
165
|
+
conn = sqlite3.connect(self.db_path)
|
|
166
|
+
try:
|
|
167
|
+
conn.execute("PRAGMA busy_timeout=5000")
|
|
168
|
+
cur = conn.execute(
|
|
169
|
+
"SELECT event_version, projection, checksum FROM snapshots "
|
|
170
|
+
"WHERE stream = ? ORDER BY event_version DESC LIMIT 1",
|
|
171
|
+
(stream,),
|
|
172
|
+
)
|
|
173
|
+
row = cur.fetchone()
|
|
174
|
+
if row is None:
|
|
175
|
+
return None
|
|
176
|
+
event_version, projection_json, checksum = row
|
|
177
|
+
try:
|
|
178
|
+
projection = json.loads(projection_json)
|
|
179
|
+
# Verify checksum
|
|
180
|
+
computed_checksum = hashlib.sha256(projection_json.encode()).hexdigest()
|
|
181
|
+
if computed_checksum != checksum:
|
|
182
|
+
print(
|
|
183
|
+
f"WARNING: snapshot checksum mismatch for stream={stream} "
|
|
184
|
+
f"version={event_version}; falling back to full replay",
|
|
185
|
+
file=sys.stderr,
|
|
186
|
+
)
|
|
187
|
+
return None
|
|
188
|
+
return (event_version, projection, checksum)
|
|
189
|
+
except json.JSONDecodeError as e:
|
|
190
|
+
print(
|
|
191
|
+
f"WARNING: corrupt JSON in snapshot for stream={stream} "
|
|
192
|
+
f"version={event_version}: {e}; falling back to full replay",
|
|
193
|
+
file=sys.stderr,
|
|
194
|
+
)
|
|
195
|
+
return None
|
|
101
196
|
finally:
|
|
102
197
|
conn.close()
|
package/tools/CLAUDE.md
CHANGED
|
@@ -4,36 +4,43 @@ Local-only Python (stdlib only, no external deps), bash (POSIX, CRLF-safe). Neve
|
|
|
4
4
|
|
|
5
5
|
## FILES
|
|
6
6
|
|
|
7
|
-
- `lock.mjs` — Fail-closed atomic lock acquisition (exponential backoff + stale-lock detection)
|
|
8
|
-
- `secret_scan.py` — Pre-push secret/credential detection gate
|
|
9
|
-
- `scanner_selftest.py` — Regression harness for secret_scan.py
|
|
10
|
-
- `prepublish_scan.py` — Pre-publish full history + staged-changes scan gate
|
|
11
|
-
- `metrics_gate.py` — PR gate for hard numeric claims in markdown
|
|
12
|
-
- `self_stats.py` — Git-derived metrics counter + README block generator
|
|
13
|
-
- `verify_dash.py` — Browser proof for realtime SSE dashboard
|
|
14
|
-
- `verify_submit_encoding.py` — Browser proof for /submit UTF-8 inbox bootstrap
|
|
15
|
-
- `ci_merge_wait.py` — CI-gated merge helper (polls gh pr view until SUCCESS)
|
|
16
7
|
- `alert_bridge.py` — Slack/Discord webhook bridge for SECURITY-ALERTS
|
|
17
|
-
- `
|
|
18
|
-
- `power_selftest.py` — Health check harness for /power bootstrap
|
|
19
|
-
- `healthcheck.py` — Fleet health aggregator (heartbeat/alert/orchestrator status)
|
|
8
|
+
- `bench_runner.py` — Held-out benchmark runner + scorer with accuracy + cost axis (offline mock runner; pluggable Haiku/Sonnet/Opus runners return text or (text, usage))
|
|
20
9
|
- `buildlog.py` — Uniform BUILDLOG.md appender
|
|
10
|
+
- `ci_merge_wait.py` — CI-gated merge helper (polls gh pr view until SUCCESS)
|
|
11
|
+
- `common.py` — Shared utilities (state directory resolution, heartbeat staleness checks)
|
|
12
|
+
- `cost_ceiling.py` — Cost-ceiling checker: trips the HALT kill-switch when configured token limits are exceeded
|
|
21
13
|
- `ensure_state.py` — Scaffold STATE.md and BUILDLOG.md templates
|
|
14
|
+
- `eod_sweep.py` — End-of-day safety check (dirty trees, unpushed commits)
|
|
22
15
|
- `fleet_ledger.py` — Append-only cost ledger with harvest/rotate
|
|
16
|
+
- `fleet_prompt_extractor.py` — Extract and deduplicate Agent/Task spawn prompts
|
|
17
|
+
- `halt.py` — Kill-switch: writes/reads/clears the `.HALT` sentinel that daemons/dispatch check to stop the fleet
|
|
18
|
+
- `healthcheck.py` — Fleet health aggregator (heartbeat/alert/orchestrator status)
|
|
23
19
|
- `heartbeat.py` — Single-instance loop liveness registry
|
|
24
|
-
- `stall_check.py` — Automated agent transcript stall detector
|
|
25
20
|
- `inbox_drain.py` — Drain UI inbox submissions
|
|
21
|
+
- `launch_tui.py` — Spawn bash TUI script in detached terminal
|
|
22
|
+
- `lock.mjs` — Fail-closed atomic lock acquisition (exponential backoff + stale-lock detection)
|
|
23
|
+
- `metrics_gate.py` — PR gate for hard numeric claims in markdown
|
|
26
24
|
- `orchestrator_status.py` — Atomic orchestrator status updates
|
|
25
|
+
- `power_selftest.py` — Health check harness for /power bootstrap
|
|
26
|
+
- `prepublish_scan.py` — Pre-publish full history + staged-changes scan gate
|
|
27
|
+
- `proposals.mjs` — Proposal lifecycle manager (list/accept/reject)
|
|
28
|
+
- `reconcile.py` — Detect/resolve drift between git STATE.md (phase) and the state_store projection (git-authoritative; --resolve appends to SQLite only)
|
|
27
29
|
- `reconstitute.sh` — Clone/fetch repos from config with security validation
|
|
28
|
-
- `eod_sweep.py` — End-of-day safety check (dirty trees, unpushed commits)
|
|
29
|
-
- `agent-forensics.sh` — Incident forensics / behavior reconstruction
|
|
30
|
-
- `launch_tui.py` — Spawn bash TUI script in detached terminal
|
|
31
30
|
- `rotate_logs.py` — Log rotation utility with size/line thresholds
|
|
31
|
+
- `scanner_selftest.py` — Regression harness for secret_scan.py
|
|
32
|
+
- `secret_scan.py` — Pre-push secret/credential detection gate
|
|
33
|
+
- `self_stats.py` — Git-derived metrics counter + README block generator
|
|
32
34
|
- `session_usage_summary.py` — Aggregate token usage across session transcripts
|
|
35
|
+
- `stall_check.py` — Automated agent transcript stall detector
|
|
36
|
+
- `svg_to_png.mjs` — Rasterize SVG to PNG via @resvg/resvg-js (with lazy import error handling)
|
|
33
37
|
- `transcript_replay.py` — Replay post-commit edits from transcripts to recover work
|
|
34
38
|
- `transcript_timeline.py` — Extract Write/Edit/Read timeline from transcripts
|
|
35
|
-
- `
|
|
36
|
-
- `
|
|
39
|
+
- `verify_dash.py` — Browser proof for realtime SSE dashboard
|
|
40
|
+
- `verify_submit_encoding.py` — Browser proof for /submit UTF-8 inbox bootstrap
|
|
41
|
+
- `verify_prboard.py` — Browser proof for the Wave PR Board (/api/wave/prs), gh stubbed via AESOP_GH_BIN
|
|
42
|
+
- `verify_agent_inspector.py` — Browser proof for the Agent Inspector drawer (/api/agent?id=), agents + transcript stubbed via a temp AESOP_ROOT
|
|
43
|
+
- `agent-forensics.sh` — Incident forensics / behavior reconstruction
|
|
37
44
|
|
|
38
45
|
## secret_scan.py — Pre-push secret/credential detection gate
|
|
39
46
|
|
package/tools/alert_bridge.py
CHANGED
|
@@ -30,6 +30,11 @@ import urllib.request
|
|
|
30
30
|
import urllib.error
|
|
31
31
|
from pathlib import Path
|
|
32
32
|
|
|
33
|
+
try:
|
|
34
|
+
from common import check_heartbeat_staleness as check_hb_staleness
|
|
35
|
+
except ImportError:
|
|
36
|
+
from tools.common import check_heartbeat_staleness as check_hb_staleness
|
|
37
|
+
|
|
33
38
|
|
|
34
39
|
# ==============================================================================
|
|
35
40
|
# Configuration Loading
|
|
@@ -181,20 +186,19 @@ def check_heartbeat_staleness(state_root, stall_threshold_s):
|
|
|
181
186
|
return False, None
|
|
182
187
|
|
|
183
188
|
hb_file = state_root / ".watchdog-heartbeat"
|
|
184
|
-
|
|
185
|
-
return True, "Watchdog heartbeat missing"
|
|
189
|
+
is_stale, age_seconds, info = check_hb_staleness(hb_file, stall_threshold_s)
|
|
186
190
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
except (ValueError, IOError):
|
|
190
|
-
return True, "Watchdog heartbeat unreadable"
|
|
191
|
+
if not is_stale:
|
|
192
|
+
return False, None
|
|
191
193
|
|
|
192
|
-
|
|
193
|
-
if age_seconds
|
|
194
|
+
# Map the common function's result to our return format
|
|
195
|
+
if age_seconds == 0:
|
|
196
|
+
# File missing/empty/unreadable
|
|
197
|
+
return True, info
|
|
198
|
+
else:
|
|
199
|
+
# File exists but is stale
|
|
194
200
|
return True, f"Watchdog heartbeat stale ({age_seconds}s >= {stall_threshold_s}s)"
|
|
195
201
|
|
|
196
|
-
return False, None
|
|
197
|
-
|
|
198
202
|
|
|
199
203
|
# ==============================================================================
|
|
200
204
|
# Payload Formatting
|