@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.
Files changed (198) hide show
  1. package/CHANGELOG.md +158 -5
  2. package/LICENSE +66 -21
  3. package/README.md +97 -33
  4. package/aesop.config.example.json +6 -0
  5. package/bin/CLAUDE.md +12 -3
  6. package/bin/cli.js +211 -40
  7. package/daemons/CLAUDE.md +1 -1
  8. package/daemons/backup-fleet.sh +209 -51
  9. package/daemons/run-watchdog.sh +208 -62
  10. package/dash/dash-extra.mjs +73 -4
  11. package/dash/watchdog-gui.sh +41 -35
  12. package/docs/ARCHITECTURE.md +296 -0
  13. package/docs/CONCEPTS.md +254 -0
  14. package/docs/CONFIGURE.md +256 -0
  15. package/docs/FIRST-WAVE.md +276 -0
  16. package/docs/INSTALL.md +224 -0
  17. package/docs/README.md +176 -27
  18. package/docs/autonomous-swe.md +149 -0
  19. package/docs/reproduce.md +121 -0
  20. package/hooks/CLAUDE.md +28 -0
  21. package/hooks/install-waveguard.sh +68 -0
  22. package/hooks/pre-commit-waveguard.sh +26 -0
  23. package/hooks/pre-push-policy.sh +253 -15
  24. package/monitor/CLAUDE.md +3 -3
  25. package/monitor/collect-signals.mjs +133 -20
  26. package/package.json +15 -7
  27. package/scan/CLAUDE.md +30 -0
  28. package/skills/CLAUDE.md +1 -0
  29. package/state_store/ingest.py +18 -1
  30. package/state_store/projections.py +78 -4
  31. package/state_store/store.py +102 -7
  32. package/tools/CLAUDE.md +25 -18
  33. package/tools/alert_bridge.py +14 -10
  34. package/tools/bench_runner.py +438 -0
  35. package/tools/buildlog.py +4 -7
  36. package/tools/ci_merge_wait.py +215 -34
  37. package/tools/common.py +62 -0
  38. package/tools/cost_ceiling.py +191 -0
  39. package/tools/dash.js +102 -0
  40. package/tools/doctor.js +220 -0
  41. package/tools/fleet_ledger.py +189 -17
  42. package/tools/halt.py +172 -0
  43. package/tools/healthcheck.py +24 -21
  44. package/tools/heartbeat.py +4 -7
  45. package/tools/metrics_gate.py +8 -2
  46. package/tools/orchestrator_status.py +4 -9
  47. package/tools/reconcile.py +277 -0
  48. package/tools/rotate_logs.py +152 -62
  49. package/tools/scanner_selftest.py +28 -3
  50. package/tools/secret_scan.py +359 -90
  51. package/tools/self_stats.py +292 -4
  52. package/tools/status.js +187 -0
  53. package/tools/verify_agent_inspector.py +289 -0
  54. package/tools/verify_prboard.py +344 -0
  55. package/tools/watch.js +49 -0
  56. package/ui/CLAUDE.md +2 -0
  57. package/ui/agents.py +332 -33
  58. package/ui/api/tracker.py +15 -7
  59. package/ui/collectors.py +125 -32
  60. package/ui/config.py +22 -2
  61. package/ui/cost.py +98 -3
  62. package/ui/csrf.py +2 -1
  63. package/ui/handler.py +209 -21
  64. package/ui/sse.py +102 -16
  65. package/ui/wave_prs.py +230 -0
  66. package/ui/web/dist/assets/index-0qQYnvMC.js +9 -0
  67. package/ui/web/dist/assets/index-BdIlFieV.css +1 -0
  68. package/ui/web/dist/index.html +2 -2
  69. package/monitor/.signal-state.json +0 -3
  70. package/monitor/ACTIONS.log +0 -1
  71. package/monitor/BRIEF.md +0 -24
  72. package/monitor/SIGNALS.json +0 -54
  73. package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
  74. package/state_store/__pycache__/api.cpython-314.pyc +0 -0
  75. package/state_store/__pycache__/export.cpython-314.pyc +0 -0
  76. package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
  77. package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
  78. package/state_store/__pycache__/store.cpython-314.pyc +0 -0
  79. package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
  80. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  81. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  82. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  83. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  84. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  85. package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
  86. package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
  87. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  88. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  89. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  90. package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
  91. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  92. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  93. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  94. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  95. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  96. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  97. package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
  98. package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
  99. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  100. package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
  101. package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
  102. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  103. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  104. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  105. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  106. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  107. package/ui/__pycache__/cost.cpython-314.pyc +0 -0
  108. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  109. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  110. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  111. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  112. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  113. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  114. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  115. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  116. package/ui/web/.gitattributes +0 -13
  117. package/ui/web/dist/assets/index-2LZDQirC.js +0 -9
  118. package/ui/web/dist/assets/index-D4M1qyOv.css +0 -1
  119. package/ui/web/index.html +0 -13
  120. package/ui/web/package-lock.json +0 -2225
  121. package/ui/web/package.json +0 -26
  122. package/ui/web/src/App.test.tsx +0 -74
  123. package/ui/web/src/App.tsx +0 -142
  124. package/ui/web/src/CONTRIBUTING-UI.md +0 -49
  125. package/ui/web/src/components/AgentRow.css +0 -187
  126. package/ui/web/src/components/AgentRow.test.tsx +0 -209
  127. package/ui/web/src/components/AgentRow.tsx +0 -207
  128. package/ui/web/src/components/AgentsPanel.css +0 -108
  129. package/ui/web/src/components/AgentsPanel.test.tsx +0 -41
  130. package/ui/web/src/components/AgentsPanel.tsx +0 -58
  131. package/ui/web/src/components/AlertsPanel.css +0 -88
  132. package/ui/web/src/components/AlertsPanel.test.tsx +0 -51
  133. package/ui/web/src/components/AlertsPanel.tsx +0 -67
  134. package/ui/web/src/components/BacklogPanel.test.tsx +0 -126
  135. package/ui/web/src/components/BacklogPanel.tsx +0 -122
  136. package/ui/web/src/components/CostChart.css +0 -110
  137. package/ui/web/src/components/CostChart.test.tsx +0 -144
  138. package/ui/web/src/components/CostChart.tsx +0 -152
  139. package/ui/web/src/components/CostTable.css +0 -93
  140. package/ui/web/src/components/CostTable.test.tsx +0 -165
  141. package/ui/web/src/components/CostTable.tsx +0 -94
  142. package/ui/web/src/components/EventsFeed.css +0 -68
  143. package/ui/web/src/components/EventsFeed.test.tsx +0 -36
  144. package/ui/web/src/components/EventsFeed.tsx +0 -31
  145. package/ui/web/src/components/HealthHeader.css +0 -137
  146. package/ui/web/src/components/HealthHeader.test.tsx +0 -278
  147. package/ui/web/src/components/HealthHeader.tsx +0 -281
  148. package/ui/web/src/components/InboxForm.css +0 -135
  149. package/ui/web/src/components/InboxForm.test.tsx +0 -208
  150. package/ui/web/src/components/InboxForm.tsx +0 -116
  151. package/ui/web/src/components/MessagesTail.module.css +0 -144
  152. package/ui/web/src/components/MessagesTail.test.tsx +0 -176
  153. package/ui/web/src/components/MessagesTail.tsx +0 -94
  154. package/ui/web/src/components/ReposPanel.css +0 -90
  155. package/ui/web/src/components/ReposPanel.test.tsx +0 -45
  156. package/ui/web/src/components/ReposPanel.tsx +0 -67
  157. package/ui/web/src/components/Scorecard.css +0 -106
  158. package/ui/web/src/components/Scorecard.test.tsx +0 -117
  159. package/ui/web/src/components/Scorecard.tsx +0 -85
  160. package/ui/web/src/components/Timeline.module.css +0 -151
  161. package/ui/web/src/components/Timeline.test.tsx +0 -215
  162. package/ui/web/src/components/Timeline.tsx +0 -99
  163. package/ui/web/src/components/TrackerBoard.test.tsx +0 -121
  164. package/ui/web/src/components/TrackerBoard.tsx +0 -107
  165. package/ui/web/src/components/TrackerCard.test.tsx +0 -180
  166. package/ui/web/src/components/TrackerCard.tsx +0 -160
  167. package/ui/web/src/components/TrackerForm.test.tsx +0 -189
  168. package/ui/web/src/components/TrackerForm.tsx +0 -144
  169. package/ui/web/src/lib/api.ts +0 -218
  170. package/ui/web/src/lib/format.test.ts +0 -89
  171. package/ui/web/src/lib/format.ts +0 -103
  172. package/ui/web/src/lib/sanitizeUrl.test.ts +0 -84
  173. package/ui/web/src/lib/sanitizeUrl.ts +0 -38
  174. package/ui/web/src/lib/types.ts +0 -230
  175. package/ui/web/src/lib/useHashRoute.test.ts +0 -60
  176. package/ui/web/src/lib/useHashRoute.ts +0 -23
  177. package/ui/web/src/lib/useSSE.ts +0 -175
  178. package/ui/web/src/main.tsx +0 -10
  179. package/ui/web/src/styles/global.css +0 -179
  180. package/ui/web/src/styles/theme.css +0 -184
  181. package/ui/web/src/styles/work.css +0 -572
  182. package/ui/web/src/test/fixtures.ts +0 -385
  183. package/ui/web/src/test/setup.ts +0 -49
  184. package/ui/web/src/views/Activity.module.css +0 -43
  185. package/ui/web/src/views/Activity.test.tsx +0 -89
  186. package/ui/web/src/views/Activity.tsx +0 -31
  187. package/ui/web/src/views/Cost.css +0 -87
  188. package/ui/web/src/views/Cost.test.tsx +0 -142
  189. package/ui/web/src/views/Cost.tsx +0 -54
  190. package/ui/web/src/views/Overview.css +0 -51
  191. package/ui/web/src/views/Overview.test.tsx +0 -76
  192. package/ui/web/src/views/Overview.tsx +0 -46
  193. package/ui/web/src/views/Work.test.tsx +0 -82
  194. package/ui/web/src/views/Work.tsx +0 -79
  195. package/ui/web/src/vite-env.d.ts +0 -10
  196. package/ui/web/tsconfig.json +0 -22
  197. package/ui/web/vite.config.ts +0 -25
  198. 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
@@ -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
- for item in items:
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 project_tracker(events: list) -> dict:
22
- """Return the current tracker state projected from ``events``."""
23
- order = [] # item ids in first-seen order (stable output ordering)
24
- items = {} # id -> item dict
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)
@@ -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
- return [
95
- {
96
- "id": r[0], "ts": r[1], "actor": r[2], "stream": r[3],
97
- "type": r[4], "payload": json.loads(r[5]), "version": r[6],
98
- }
99
- for r in cur.fetchall()
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
- - `proposals.mjs` — Proposal lifecycle manager (list/accept/reject)
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
- - `fleet_prompt_extractor.py` — Extract and deduplicate Agent/Task spawn prompts
36
- - `svg_to_png.mjs` — Rasterize SVG to PNG via @resvg/resvg-js (with lazy import error handling)
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
 
@@ -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
- if not hb_file.exists():
185
- return True, "Watchdog heartbeat missing"
189
+ is_stale, age_seconds, info = check_hb_staleness(hb_file, stall_threshold_s)
186
190
 
187
- try:
188
- timestamp = int(hb_file.read_text(encoding="utf-8").strip())
189
- except (ValueError, IOError):
190
- return True, "Watchdog heartbeat unreadable"
191
+ if not is_stale:
192
+ return False, None
191
193
 
192
- age_seconds = int(time.time()) - timestamp
193
- if age_seconds >= stall_threshold_s:
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