@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5

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 (234) hide show
  1. package/CHANGELOG.md +79 -95
  2. package/CLAUDE-TEMPLATE.md +115 -0
  3. package/README.md +126 -245
  4. package/aesop.config.example.json +34 -26
  5. package/bin/CLAUDE.md +67 -0
  6. package/bin/cli.js +593 -36
  7. package/daemons/CLAUDE.md +36 -0
  8. package/daemons/backup-fleet.sh +92 -21
  9. package/daemons/run-watchdog.sh +95 -12
  10. package/dash/CLAUDE.md +32 -0
  11. package/dash/dash-extra.mjs +194 -21
  12. package/dash/watchdog-gui.sh +23 -6
  13. package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
  14. package/docs/CHECKPOINTING.md +100 -0
  15. package/docs/DISPATCH-MODEL.md +11 -7
  16. package/docs/FORENSICS.md +204 -0
  17. package/docs/GOVERNANCE.md +4 -2
  18. package/docs/HOOK-INSTALL.md +307 -0
  19. package/docs/HOW-THE-LOOP-WORKS.md +175 -0
  20. package/docs/MEMORY-TEMPLATE.md +61 -0
  21. package/docs/README.md +44 -0
  22. package/docs/RELEASING.md +159 -0
  23. package/docs/RELIABILITY.md +61 -0
  24. package/docs/RESTORE.md +397 -0
  25. package/docs/SCRIPTS-POLICY.md +97 -0
  26. package/docs/archive/README.md +3 -0
  27. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
  28. package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
  29. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
  30. package/docs/archive/spikes/tiered-cognition/README.md +27 -0
  31. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
  32. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
  33. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
  34. package/docs/case-study-portfolio.md +61 -0
  35. package/docs/self-stats-data.json +11 -0
  36. package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
  37. package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
  38. package/docs/templates/PROPOSALS-LOG.example.md +64 -0
  39. package/hooks/CLAUDE.md +61 -0
  40. package/hooks/claude/force-model-policy.mjs +159 -0
  41. package/hooks/pre-push-policy.sh +753 -0
  42. package/mcp/CLAUDE.md +213 -0
  43. package/mcp/package.json +26 -0
  44. package/mcp/server.mjs +543 -0
  45. package/monitor/.signal-state.json +3 -0
  46. package/monitor/ACTIONS.log +1 -0
  47. package/monitor/BRIEF.md +24 -0
  48. package/monitor/CHARTER.md +75 -88
  49. package/monitor/CLAUDE.md +40 -0
  50. package/monitor/SIGNALS.json +54 -0
  51. package/monitor/collect-signals.mjs +613 -61
  52. package/package.json +15 -3
  53. package/scan/fleet-scan.example.mjs +292 -0
  54. package/skills/CLAUDE.md +3 -0
  55. package/skills/healthcheck/SKILL.md +44 -0
  56. package/skills/power/SKILL.md +161 -0
  57. package/state_store/CLAUDE.md +39 -0
  58. package/state_store/__init__.py +24 -0
  59. package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
  60. package/state_store/__pycache__/api.cpython-314.pyc +0 -0
  61. package/state_store/__pycache__/export.cpython-314.pyc +0 -0
  62. package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
  63. package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
  64. package/state_store/__pycache__/store.cpython-314.pyc +0 -0
  65. package/state_store/api.py +35 -0
  66. package/state_store/export.py +19 -0
  67. package/state_store/ingest.py +24 -0
  68. package/state_store/projections.py +52 -0
  69. package/state_store/store.py +102 -0
  70. package/tools/CLAUDE.md +72 -0
  71. package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
  72. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  73. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  74. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  75. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  76. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  77. package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
  78. package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
  79. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  80. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  81. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  82. package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
  83. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  84. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  85. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  86. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  87. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  88. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  89. package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
  90. package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
  91. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  92. package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
  93. package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
  94. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  95. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  96. package/tools/agent-forensics.sh +186 -0
  97. package/tools/alert_bridge.py +449 -0
  98. package/tools/buildlog.py +182 -0
  99. package/tools/ci_merge_wait.py +317 -0
  100. package/tools/ensure_state.py +94 -0
  101. package/tools/eod_sweep.py +239 -0
  102. package/tools/fleet_ledger.py +272 -0
  103. package/tools/fleet_prompt_extractor.py +134 -0
  104. package/tools/healthcheck.py +296 -0
  105. package/tools/heartbeat.py +131 -0
  106. package/tools/inbox_drain.py +249 -0
  107. package/tools/launch_tui.py +51 -3
  108. package/tools/lock.mjs +211 -0
  109. package/tools/metrics_gate.py +205 -0
  110. package/tools/orchestrator_status.py +99 -0
  111. package/tools/power_selftest.py +386 -0
  112. package/tools/prepublish_scan.py +84 -0
  113. package/tools/proposals.mjs +248 -0
  114. package/tools/reconstitute.sh +467 -0
  115. package/tools/rotate_logs.py +228 -0
  116. package/tools/scanner_selftest.py +303 -0
  117. package/tools/secret_scan.py +138 -31
  118. package/tools/self_stats.py +509 -0
  119. package/tools/session_usage_summary.py +198 -0
  120. package/tools/stall_check.py +172 -0
  121. package/tools/svg_to_png.mjs +50 -0
  122. package/tools/transcript_replay.py +236 -0
  123. package/tools/transcript_timeline.py +184 -0
  124. package/tools/verify_dash.py +513 -0
  125. package/tools/verify_submit_encoding.py +202 -0
  126. package/ui/CLAUDE.md +125 -0
  127. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  128. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  129. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  130. package/ui/__pycache__/cost.cpython-314.pyc +0 -0
  131. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  132. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  133. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  134. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  135. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  136. package/ui/agents.py +183 -0
  137. package/ui/api/__init__.py +84 -0
  138. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  140. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  141. package/ui/api/submit.py +97 -0
  142. package/ui/api/tracker.py +116 -0
  143. package/ui/collectors.py +735 -0
  144. package/ui/config.py +123 -0
  145. package/ui/cost.py +260 -0
  146. package/ui/csrf.py +144 -0
  147. package/ui/handler.py +676 -0
  148. package/ui/render.py +49 -0
  149. package/ui/serve.py +68 -796
  150. package/ui/sse.py +183 -0
  151. package/ui/web/.gitattributes +13 -0
  152. package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
  153. package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
  154. package/ui/web/dist/index.html +14 -0
  155. package/ui/web/index.html +13 -0
  156. package/ui/web/package-lock.json +2225 -0
  157. package/ui/web/package.json +26 -0
  158. package/ui/web/src/App.test.tsx +74 -0
  159. package/ui/web/src/App.tsx +142 -0
  160. package/ui/web/src/CONTRIBUTING-UI.md +49 -0
  161. package/ui/web/src/components/AgentRow.css +187 -0
  162. package/ui/web/src/components/AgentRow.test.tsx +209 -0
  163. package/ui/web/src/components/AgentRow.tsx +207 -0
  164. package/ui/web/src/components/AgentsPanel.css +108 -0
  165. package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
  166. package/ui/web/src/components/AgentsPanel.tsx +58 -0
  167. package/ui/web/src/components/AlertsPanel.css +88 -0
  168. package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
  169. package/ui/web/src/components/AlertsPanel.tsx +67 -0
  170. package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
  171. package/ui/web/src/components/BacklogPanel.tsx +122 -0
  172. package/ui/web/src/components/CostChart.css +110 -0
  173. package/ui/web/src/components/CostChart.test.tsx +144 -0
  174. package/ui/web/src/components/CostChart.tsx +152 -0
  175. package/ui/web/src/components/CostTable.css +93 -0
  176. package/ui/web/src/components/CostTable.test.tsx +165 -0
  177. package/ui/web/src/components/CostTable.tsx +94 -0
  178. package/ui/web/src/components/EventsFeed.css +68 -0
  179. package/ui/web/src/components/EventsFeed.test.tsx +36 -0
  180. package/ui/web/src/components/EventsFeed.tsx +31 -0
  181. package/ui/web/src/components/HealthHeader.css +137 -0
  182. package/ui/web/src/components/HealthHeader.test.tsx +278 -0
  183. package/ui/web/src/components/HealthHeader.tsx +281 -0
  184. package/ui/web/src/components/InboxForm.css +135 -0
  185. package/ui/web/src/components/InboxForm.test.tsx +208 -0
  186. package/ui/web/src/components/InboxForm.tsx +116 -0
  187. package/ui/web/src/components/MessagesTail.module.css +144 -0
  188. package/ui/web/src/components/MessagesTail.test.tsx +176 -0
  189. package/ui/web/src/components/MessagesTail.tsx +94 -0
  190. package/ui/web/src/components/ReposPanel.css +90 -0
  191. package/ui/web/src/components/ReposPanel.test.tsx +45 -0
  192. package/ui/web/src/components/ReposPanel.tsx +67 -0
  193. package/ui/web/src/components/Scorecard.css +106 -0
  194. package/ui/web/src/components/Scorecard.test.tsx +117 -0
  195. package/ui/web/src/components/Scorecard.tsx +85 -0
  196. package/ui/web/src/components/Timeline.module.css +151 -0
  197. package/ui/web/src/components/Timeline.test.tsx +215 -0
  198. package/ui/web/src/components/Timeline.tsx +99 -0
  199. package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
  200. package/ui/web/src/components/TrackerBoard.tsx +107 -0
  201. package/ui/web/src/components/TrackerCard.test.tsx +180 -0
  202. package/ui/web/src/components/TrackerCard.tsx +160 -0
  203. package/ui/web/src/components/TrackerForm.test.tsx +189 -0
  204. package/ui/web/src/components/TrackerForm.tsx +144 -0
  205. package/ui/web/src/lib/api.ts +218 -0
  206. package/ui/web/src/lib/format.test.ts +89 -0
  207. package/ui/web/src/lib/format.ts +103 -0
  208. package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
  209. package/ui/web/src/lib/sanitizeUrl.ts +38 -0
  210. package/ui/web/src/lib/types.ts +230 -0
  211. package/ui/web/src/lib/useHashRoute.test.ts +60 -0
  212. package/ui/web/src/lib/useHashRoute.ts +23 -0
  213. package/ui/web/src/lib/useSSE.ts +175 -0
  214. package/ui/web/src/main.tsx +10 -0
  215. package/ui/web/src/styles/global.css +179 -0
  216. package/ui/web/src/styles/theme.css +184 -0
  217. package/ui/web/src/styles/work.css +572 -0
  218. package/ui/web/src/test/fixtures.ts +385 -0
  219. package/ui/web/src/test/setup.ts +49 -0
  220. package/ui/web/src/views/Activity.module.css +43 -0
  221. package/ui/web/src/views/Activity.test.tsx +89 -0
  222. package/ui/web/src/views/Activity.tsx +31 -0
  223. package/ui/web/src/views/Cost.css +87 -0
  224. package/ui/web/src/views/Cost.test.tsx +142 -0
  225. package/ui/web/src/views/Cost.tsx +54 -0
  226. package/ui/web/src/views/Overview.css +51 -0
  227. package/ui/web/src/views/Overview.test.tsx +76 -0
  228. package/ui/web/src/views/Overview.tsx +46 -0
  229. package/ui/web/src/views/Work.test.tsx +82 -0
  230. package/ui/web/src/views/Work.tsx +79 -0
  231. package/ui/web/src/vite-env.d.ts +10 -0
  232. package/ui/web/tsconfig.json +22 -0
  233. package/ui/web/vite.config.ts +25 -0
  234. package/ui/web/vitest.config.ts +12 -0
@@ -0,0 +1,19 @@
1
+ """state_store.export — render a projection back into git-tracked JSON.
2
+
3
+ In the DB-source-of-truth design git stops being *read* for state; this job
4
+ renders human-readable, diffable snapshots (e.g. tracker.json) FROM the
5
+ projections for durability and review. The JSON style (indent=2, ascii-escaped)
6
+ matches the existing state/tracker.json so a future cutover produces
7
+ minimal-diff snapshots.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+
13
+
14
+ def export_tracker(api, out_path: str) -> None:
15
+ """Write the tracker projection to ``out_path`` as pretty JSON."""
16
+ projection = api.project("tracker")
17
+ with open(out_path, "w", encoding="utf-8") as fh:
18
+ json.dump(projection, fh, indent=2)
19
+ fh.write("\n")
@@ -0,0 +1,24 @@
1
+ """state_store.ingest — backfill events from an existing tracker.json.
2
+
3
+ Migration path for the DB-source-of-truth cutover: read the current
4
+ tracker.json and emit one ``item_created`` event per item (full payload) so the
5
+ event store reproduces the current state. History is not reconstructed (only
6
+ final state exists on disk), but the projection round-trips to the same items,
7
+ so an export re-renders the identical tracker.json.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+
13
+
14
+ def ingest_tracker_json(api, tracker_json_path: str, actor: str = "migration") -> int:
15
+ """Append one ``item_created`` per item in ``tracker_json_path``.
16
+
17
+ Returns the number of items ingested.
18
+ """
19
+ with open(tracker_json_path, "r", encoding="utf-8") as fh:
20
+ data = json.load(fh)
21
+ items = data.get("items", [])
22
+ for item in items:
23
+ api.append("tracker", "item_created", item, actor)
24
+ return len(items)
@@ -0,0 +1,52 @@
1
+ """state_store.projections — fold tracker events into current state.
2
+
3
+ ``project_tracker`` reconstructs the tracker.json shape
4
+ (``{"version": 1, "items": [...]}``) from the ``tracker`` event stream:
5
+
6
+ - ``item_created`` : payload is the full item dict; establishes the item
7
+ (kept in first-seen order).
8
+ - ``item_updated`` : payload is ``{"id": <id>, ...partial fields}``; merges
9
+ the given fields onto the existing item.
10
+ - ``item_archived`` : payload is ``{"id": <id>}`` (+ optional
11
+ ``completed_at``); sets ``status`` to ``"archived"``.
12
+
13
+ Unknown ids on update/archive and unknown event types are ignored, keeping the
14
+ fold tolerant of partial/legacy streams.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ TRACKER_VERSION = 1
19
+
20
+
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
+ for ev in events:
26
+ etype = ev.get("type")
27
+ payload = ev.get("payload") or {}
28
+ if etype == "item_created":
29
+ iid = payload.get("id")
30
+ if iid is None:
31
+ continue
32
+ if iid not in items:
33
+ order.append(iid)
34
+ items[iid] = dict(payload)
35
+ elif etype == "item_updated":
36
+ iid = payload.get("id")
37
+ if iid in items:
38
+ merged = dict(items[iid])
39
+ for key, value in payload.items():
40
+ if key != "id":
41
+ merged[key] = value
42
+ items[iid] = merged
43
+ elif etype == "item_archived":
44
+ iid = payload.get("id")
45
+ if iid in items:
46
+ merged = dict(items[iid])
47
+ merged["status"] = "archived"
48
+ if "completed_at" in payload:
49
+ merged["completed_at"] = payload["completed_at"]
50
+ items[iid] = merged
51
+ # any other event type is ignored
52
+ return {"version": TRACKER_VERSION, "items": [items[i] for i in order]}
@@ -0,0 +1,102 @@
1
+ """state_store.store — SQLite-backed, append-only, concurrency-safe event log.
2
+
3
+ Durable substrate for aesop's event-sourced state layer (the DB-source-of-truth
4
+ design; git becomes a rendered export, not the coordination layer). Backend is
5
+ SQLite in WAL mode so many readers and serialized writers share one file; the
6
+ same interface is meant to swap to Postgres behind ``state_store.api.StateAPI``
7
+ for team scale without touching call sites.
8
+
9
+ Stdlib only (sqlite3, json, time) per aesop's no-external-deps invariant.
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import sqlite3
15
+ import time
16
+
17
+
18
+ class EventStore:
19
+ """Append-only event log stored at ``db_path``.
20
+
21
+ Safe under concurrent appends from multiple threads AND multiple EventStore
22
+ instances on the same file: every call opens its own connection with
23
+ ``PRAGMA busy_timeout`` and assigns the per-stream version inside a
24
+ ``BEGIN IMMEDIATE`` transaction, so the read-max-version-then-insert is
25
+ atomic and two writers can never collide or duplicate a version.
26
+ """
27
+
28
+ def __init__(self, db_path: str):
29
+ self.db_path = db_path
30
+ conn = sqlite3.connect(self.db_path)
31
+ try:
32
+ conn.execute("PRAGMA journal_mode=WAL")
33
+ conn.execute("PRAGMA busy_timeout=5000")
34
+ conn.execute(
35
+ """
36
+ CREATE TABLE IF NOT EXISTS events (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ ts REAL NOT NULL,
39
+ actor TEXT NOT NULL,
40
+ stream TEXT NOT NULL,
41
+ type TEXT NOT NULL,
42
+ payload TEXT NOT NULL,
43
+ version INTEGER NOT NULL
44
+ )
45
+ """
46
+ )
47
+ conn.execute(
48
+ "CREATE INDEX IF NOT EXISTS idx_events_stream_version "
49
+ "ON events(stream, version)"
50
+ )
51
+ conn.commit()
52
+ finally:
53
+ conn.close()
54
+
55
+ def append(self, stream: str, event_type: str, payload: dict, actor: str = "system") -> int:
56
+ """Append one event to ``stream``; return its new per-stream version (1-based)."""
57
+ conn = sqlite3.connect(self.db_path)
58
+ try:
59
+ conn.execute("PRAGMA busy_timeout=5000")
60
+ # BEGIN IMMEDIATE takes the write lock up front so the
61
+ # read-max-then-insert below is atomic under contention.
62
+ conn.execute("BEGIN IMMEDIATE")
63
+ row = conn.execute(
64
+ "SELECT COALESCE(MAX(version), 0) FROM events WHERE stream = ?",
65
+ (stream,),
66
+ ).fetchone()
67
+ version = row[0] + 1
68
+ conn.execute(
69
+ "INSERT INTO events (ts, actor, stream, type, payload, version) "
70
+ "VALUES (?, ?, ?, ?, ?, ?)",
71
+ (time.time(), actor, stream, event_type, json.dumps(payload), version),
72
+ )
73
+ conn.commit()
74
+ return version
75
+ finally:
76
+ conn.close()
77
+
78
+ def read(self, stream: str) -> list:
79
+ """Return all events for ``stream`` ascending by version (empty if none)."""
80
+ return self._rows("WHERE stream = ? ORDER BY version ASC", (stream,))
81
+
82
+ def read_all(self) -> list:
83
+ """Return all events across all streams ascending by id."""
84
+ return self._rows("ORDER BY id ASC", ())
85
+
86
+ def _rows(self, clause: str, params) -> list:
87
+ conn = sqlite3.connect(self.db_path)
88
+ try:
89
+ conn.execute("PRAGMA busy_timeout=5000")
90
+ cur = conn.execute(
91
+ "SELECT id, ts, actor, stream, type, payload, version FROM events " + clause,
92
+ params,
93
+ )
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
+ ]
101
+ finally:
102
+ conn.close()
@@ -0,0 +1,72 @@
1
+ # tools/ — Build utilities
2
+
3
+ Local-only Python (stdlib only, no external deps), bash (POSIX, CRLF-safe). Never print secrets — report by pattern name/location only.
4
+
5
+ ## FILES
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
+ - `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)
20
+ - `buildlog.py` — Uniform BUILDLOG.md appender
21
+ - `ensure_state.py` — Scaffold STATE.md and BUILDLOG.md templates
22
+ - `fleet_ledger.py` — Append-only cost ledger with harvest/rotate
23
+ - `heartbeat.py` — Single-instance loop liveness registry
24
+ - `stall_check.py` — Automated agent transcript stall detector
25
+ - `inbox_drain.py` — Drain UI inbox submissions
26
+ - `orchestrator_status.py` — Atomic orchestrator status updates
27
+ - `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
+ - `rotate_logs.py` — Log rotation utility with size/line thresholds
32
+ - `session_usage_summary.py` — Aggregate token usage across session transcripts
33
+ - `transcript_replay.py` — Replay post-commit edits from transcripts to recover work
34
+ - `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)
37
+
38
+ ## secret_scan.py — Pre-push secret/credential detection gate
39
+
40
+ Scans staged/history/paths for secrets by regex pattern and credential filenames; blocks pushes on findings.
41
+
42
+ - `secret_scan.py --staged [--repo PATH]` — scan git staged files
43
+ - `secret_scan.py --history [--repo PATH]` — scan all git history blobs
44
+ - `secret_scan.py PATH [PATH...]` — scan files/dirs directly (recurse)
45
+
46
+ Exit: 0=clean, 1=findings, 2=usage error. Output masks secrets as `xxxx...`.
47
+
48
+ **Pragma escape** (pattern findings only; credential filenames always fatal):
49
+ ```
50
+ # secretscan: allow-pattern-docs
51
+ ```
52
+ Mark file's first 10 lines to report rule-based findings as ALLOWED-DOC (non-fatal). Use only for deliberate pattern documentation.
53
+
54
+ ## agent-forensics.sh — Incident forensics / behavior reconstruction
55
+
56
+ Read-only git plumbing; reconstructs agent behavior snapshot or diffs behavior-controlling files.
57
+
58
+ - `bash tools/agent-forensics.sh <commit>` — print commit header, rules snapshot, CLAUDE.md, STATE.md, last 30 lines of BUILDLOG.md
59
+ - `bash tools/agent-forensics.sh --diff <commitA> <commitB>` — diff CLAUDE.md, STATE.md, docs/, monitor/CHARTER.md, hooks/ between commits
60
+
61
+ Exit: 0=success, 1=error (never raw git traces). Requires: git, head, tail, wc, grep.
62
+
63
+
64
+ ## Invariants
65
+
66
+ - **Dependency-light**: Python tools must work on base Python 3 (no pip installs).
67
+ - **CRLF-safe shell**: no line continuations in .sh scripts; Git Bash + Linux compatible.
68
+ - **Never print secrets**: mask as pattern name + masked value only.
69
+ - **Config-driven paths**: heartbeat/ledger/logs use AESOP_STATE_ROOT env var (default ./state) or CLI args; no hardcoded personal paths.
70
+ - **Fragment-assembled secrets in tests**: scanner_selftest.py concatenates dummy secrets at runtime so pattern text never appears contiguously (self-scan invariant).
71
+ - **verify_*.py are mandatory CI gates**: verify_dash.py and verify_submit_encoding.py are required pre-push gates; use `--allow-skip` only in truly browserless environments (CI must run both).
72
+ - **lock.mjs is the ONLY lock implementation**: never reimplement locking in proposals.mjs or elsewhere; all proposals/state updates must use fail-closed lock.mjs with exponential backoff + stale-lock breaking.
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env bash
2
+ # agent-forensics.sh — incident forensics tool
3
+ # Reconstruct agent behavior as of a commit using git plumbing.
4
+ # Usage:
5
+ # bash tools/agent-forensics.sh <commit>
6
+ # bash tools/agent-forensics.sh --diff <commitA> <commitB>
7
+ #
8
+ # Prints:
9
+ # - Commit header (hash, date, subject)
10
+ # - Rules snapshot (docs/CARDINAL-RULES.md)
11
+ # - CLAUDE.md (if present)
12
+ # - STATE.md (if present)
13
+ # - Last 30 lines of BUILDLOG.md (if tracked)
14
+ # - For --diff: behavior changes between two commits
15
+ #
16
+ # Exit codes: 0 on success, 1 on error (never raw git stack traces).
17
+ # Requires: git, head, tail, wc, grep
18
+ # CRLF-safe: no line continuations, Git Bash + Linux compatible.
19
+
20
+ set -e
21
+
22
+ show_error() {
23
+ printf 'Error: %s\n' "$1" >&2
24
+ exit 1
25
+ }
26
+
27
+ show_usage() {
28
+ cat <<'EOF'
29
+ Usage:
30
+ bash tools/agent-forensics.sh <commit> Forensics at <commit>
31
+ bash tools/agent-forensics.sh --diff <commitA> <commitB> Behavior diff
32
+ EOF
33
+ exit 1
34
+ }
35
+
36
+ # --- Commit header (hash, date, subject) ---
37
+ print_commit_header() {
38
+ local commit="$1"
39
+
40
+ # Verify commit exists
41
+ if ! git cat-file -t "$commit" >/dev/null 2>&1; then
42
+ show_error "unknown commit: $commit"
43
+ fi
44
+
45
+ local hash
46
+ local date
47
+ local subject
48
+
49
+ hash=$(git rev-parse "$commit" 2>/dev/null || show_error "commit parse failed: $commit")
50
+ date=$(git log -1 --format=%ai "$commit" 2>/dev/null || show_error "failed to read commit date")
51
+ subject=$(git log -1 --format=%s "$commit" 2>/dev/null || show_error "failed to read commit subject")
52
+
53
+ printf '# Forensics @ %s\n\n' "$hash"
54
+ printf 'Commit: %s\n' "$hash"
55
+ printf 'Date: %s\n' "$date"
56
+ printf 'Subject: %s\n\n' "$subject"
57
+ }
58
+
59
+ # --- Print first ~40 lines of docs/CARDINAL-RULES.md ---
60
+ print_rules() {
61
+ local commit="$1"
62
+
63
+ printf '## Rules (docs/CARDINAL-RULES.md)\n\n'
64
+
65
+ if ! git cat-file -e "$commit:docs/CARDINAL-RULES.md" 2>/dev/null; then
66
+ printf '(file not present at this commit)\n\n'
67
+ return
68
+ fi
69
+
70
+ git show "$commit:docs/CARDINAL-RULES.md" 2>/dev/null | head -40
71
+
72
+ # Check if file was truncated
73
+ local total_lines
74
+ total_lines=$(git show "$commit:docs/CARDINAL-RULES.md" 2>/dev/null | wc -l)
75
+ if [ "$total_lines" -gt 40 ]; then
76
+ printf '\n[... %d more lines; see git show %s:docs/CARDINAL-RULES.md]\n' "$((total_lines - 40))" "$commit"
77
+ fi
78
+ printf '\n\n'
79
+ }
80
+
81
+ # --- Print CLAUDE.md if present ---
82
+ print_claude_md() {
83
+ local commit="$1"
84
+
85
+ printf '## CLAUDE.md\n\n'
86
+
87
+ if ! git cat-file -e "$commit:CLAUDE.md" 2>/dev/null; then
88
+ printf '(not present at this commit)\n\n'
89
+ return
90
+ fi
91
+
92
+ git show "$commit:CLAUDE.md" 2>/dev/null
93
+ printf '\n\n'
94
+ }
95
+
96
+ # --- Print STATE.md if present ---
97
+ print_state_md() {
98
+ local commit="$1"
99
+
100
+ printf '## STATE.md\n\n'
101
+
102
+ if ! git cat-file -e "$commit:STATE.md" 2>/dev/null; then
103
+ printf '(not present at this commit)\n\n'
104
+ return
105
+ fi
106
+
107
+ git show "$commit:STATE.md" 2>/dev/null
108
+ printf '\n\n'
109
+ }
110
+
111
+ # --- Print last 30 lines of BUILDLOG.md ---
112
+ print_buildlog() {
113
+ local commit="$1"
114
+
115
+ printf '## BUILDLOG.md (last 30 lines)\n\n'
116
+
117
+ if ! git cat-file -e "$commit:BUILDLOG.md" 2>/dev/null; then
118
+ printf '(not tracked at this commit)\n\n'
119
+ return
120
+ fi
121
+
122
+ git show "$commit:BUILDLOG.md" 2>/dev/null | tail -30
123
+ printf '\n\n'
124
+ }
125
+
126
+ # --- Diff behavior between two commits ---
127
+ diff_behavior() {
128
+ local commitA="$1"
129
+ local commitB="$2"
130
+
131
+ # Verify both commits exist
132
+ if ! git cat-file -t "$commitA" >/dev/null 2>&1; then
133
+ show_error "unknown commit: $commitA"
134
+ fi
135
+ if ! git cat-file -t "$commitB" >/dev/null 2>&1; then
136
+ show_error "unknown commit: $commitB"
137
+ fi
138
+
139
+ printf '# Behavior Diff\n\n'
140
+ printf '%s -> %s\n\n' "$(git rev-parse "$commitA")" "$(git rev-parse "$commitB")"
141
+
142
+ # Files that affect behavior: rules, CLAUDE.md, STATE.md, hooks, monitor/CHARTER.md
143
+ local behavior_files
144
+ behavior_files="CLAUDE.md STATE.md docs/CARDINAL-RULES.md docs/DISPATCH-MODEL.md docs/GOVERNANCE.md hooks/ monitor/CHARTER.md"
145
+
146
+ printf '## Changes in behavior-controlling files\n\n'
147
+
148
+ local has_changes=0
149
+ for file in $behavior_files; do
150
+ if git diff "$commitA" "$commitB" --name-only -- "$file" 2>/dev/null | grep -q .; then
151
+ has_changes=1
152
+ printf '### %s\n\n' "$file"
153
+ git diff "$commitA" "$commitB" -- "$file" 2>/dev/null | head -100
154
+ printf '\n'
155
+ fi
156
+ done
157
+
158
+ if [ "$has_changes" -eq 0 ]; then
159
+ printf '(no changes in behavior-controlling files)\n\n'
160
+ fi
161
+ }
162
+
163
+ # --- Main dispatch ---
164
+ case "${1:-}" in
165
+ --diff)
166
+ if [ $# -lt 3 ]; then
167
+ show_error "--diff requires two commits: --diff <commitA> <commitB>"
168
+ fi
169
+ diff_behavior "$2" "$3"
170
+ ;;
171
+ --help|-h)
172
+ show_usage
173
+ ;;
174
+ '')
175
+ show_usage
176
+ ;;
177
+ *)
178
+ print_commit_header "$1"
179
+ print_rules "$1"
180
+ print_claude_md "$1"
181
+ print_state_md "$1"
182
+ print_buildlog "$1"
183
+ ;;
184
+ esac
185
+
186
+ exit 0