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

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 (108) hide show
  1. package/CHANGELOG.md +37 -94
  2. package/CLAUDE-TEMPLATE.md +115 -0
  3. package/README.md +89 -264
  4. package/aesop.config.example.json +27 -26
  5. package/bin/CLAUDE.md +39 -0
  6. package/bin/cli.js +368 -24
  7. package/daemons/CLAUDE.md +33 -0
  8. package/daemons/backup-fleet.sh +92 -21
  9. package/daemons/run-watchdog.sh +90 -10
  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/RELIABILITY.md +61 -0
  23. package/docs/RESTORE.md +397 -0
  24. package/docs/SCRIPTS-POLICY.md +97 -0
  25. package/docs/spikes/tiered-cognition/ACTIVATION.md +125 -0
  26. package/docs/spikes/tiered-cognition/DESIGN.md +287 -0
  27. package/docs/spikes/tiered-cognition/FINDINGS.md +113 -0
  28. package/docs/spikes/tiered-cognition/README.md +29 -0
  29. package/docs/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
  30. package/docs/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
  31. package/docs/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
  32. package/hooks/CLAUDE.md +38 -0
  33. package/hooks/claude/force-model-policy.mjs +159 -0
  34. package/hooks/pre-push-policy.sh +753 -0
  35. package/monitor/.signal-state.json +3 -0
  36. package/monitor/ACTIONS.log +1 -0
  37. package/monitor/BRIEF.md +24 -0
  38. package/monitor/CHARTER.md +29 -8
  39. package/monitor/CLAUDE.md +40 -0
  40. package/monitor/SIGNALS.json +54 -0
  41. package/monitor/collect-signals.mjs +529 -62
  42. package/package.json +12 -3
  43. package/skills/CLAUDE.md +3 -0
  44. package/skills/power/SKILL.md +161 -0
  45. package/tools/CLAUDE.md +191 -0
  46. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  47. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  48. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  49. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  50. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  51. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  52. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  53. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  54. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  55. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  56. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  57. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  58. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  59. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  60. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  61. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  62. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  63. package/tools/agent-forensics.sh +186 -0
  64. package/tools/buildlog.py +182 -0
  65. package/tools/ci_merge_wait.py +204 -0
  66. package/tools/ensure_state.py +94 -0
  67. package/tools/eod_sweep.py +239 -0
  68. package/tools/fleet_ledger.py +272 -0
  69. package/tools/heartbeat.py +131 -0
  70. package/tools/inbox_drain.py +249 -0
  71. package/tools/launch_tui.py +51 -3
  72. package/tools/lock.mjs +211 -0
  73. package/tools/metrics_gate.py +205 -0
  74. package/tools/orchestrator_status.py +99 -0
  75. package/tools/power_selftest.py +386 -0
  76. package/tools/prepublish_scan.py +84 -0
  77. package/tools/proposals.mjs +248 -0
  78. package/tools/reconstitute.sh +467 -0
  79. package/tools/rotate_logs.py +228 -0
  80. package/tools/scanner_selftest.py +303 -0
  81. package/tools/secret_scan.py +131 -31
  82. package/tools/stall_check.py +172 -0
  83. package/tools/verify_dash.py +694 -0
  84. package/tools/verify_submit_encoding.py +194 -0
  85. package/ui/CLAUDE.md +109 -0
  86. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  87. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  88. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  89. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  90. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  91. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  92. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  93. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  94. package/ui/agents.py +179 -0
  95. package/ui/api/__init__.py +84 -0
  96. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  97. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  98. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  99. package/ui/api/submit.py +58 -0
  100. package/ui/api/tracker.py +116 -0
  101. package/ui/collectors.py +586 -0
  102. package/ui/config.py +114 -0
  103. package/ui/csrf.py +140 -0
  104. package/ui/handler.py +426 -0
  105. package/ui/render.py +29 -0
  106. package/ui/serve.py +68 -796
  107. package/ui/sse.py +168 -0
  108. package/ui/templates/dashboard.html +1202 -0
package/ui/sse.py ADDED
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env python3
2
+ """Aesop UI — SSE client registry + broadcast + background collector loop (wave-9 split)."""
3
+ import hashlib
4
+ import json
5
+ import queue
6
+ import sys
7
+ import threading
8
+
9
+ import config
10
+ from collectors import (parse_audit_backlog, _snapshot_data, _snapshot_tracker,
11
+ _snapshot_orchestrator_status, drain_tracker_inbox)
12
+ from agents import get_fleet_agents, _transcripts_fingerprint
13
+
14
+
15
+ _sse_lock = threading.Lock()
16
+
17
+ _sse_clients = [] # list[queue.Queue]
18
+
19
+ _latest_lock = threading.Lock()
20
+
21
+ _latest_snapshots = {"data": None, "backlog": None, "agents": None, "tracker": None, "status": None} # name -> json str
22
+
23
+ _collector_lock = threading.Lock()
24
+
25
+ _collector_started = False
26
+
27
+ _collector_stop_event = threading.Event()
28
+
29
+
30
+ def reset_state():
31
+ """Reset collector/SSE singleton state for a fresh serve import.
32
+
33
+ The sse module object is cached in sys.modules, so per-test re-imports of
34
+ serve would otherwise share one collector thread + snapshot dict (a prior
35
+ test's thread keeps polling its own dir; later tests never see their state).
36
+ serve.py calls this at import to restore the per-import isolation the
37
+ pre-split monolith had. In production serve is imported once, so this is a
38
+ harmless no-op before the collector ever starts.
39
+ """
40
+ global _collector_started, _collector_stop_event
41
+ with _collector_lock:
42
+ _collector_stop_event.set() # stop a thread left over from a prior import
43
+ _collector_stop_event = threading.Event()
44
+ _collector_started = False
45
+ with _latest_lock:
46
+ for k in list(_latest_snapshots):
47
+ _latest_snapshots[k] = None
48
+ with _sse_lock:
49
+ _sse_clients.clear()
50
+
51
+
52
+ def register_sse_client():
53
+ """Register a new SSE client queue. Returns the queue to read events from, or None if cap exceeded."""
54
+ with _sse_lock:
55
+ if len(_sse_clients) >= config.SSE_MAX_CLIENTS:
56
+ return None # Caller will return HTTP 503
57
+ q = queue.Queue(maxsize=config.SSE_QUEUE_MAXSIZE)
58
+ _sse_clients.append(q)
59
+ return q
60
+
61
+ def unregister_sse_client(q):
62
+ """Remove a disconnected SSE client's queue."""
63
+ with _sse_lock:
64
+ if q in _sse_clients:
65
+ _sse_clients.remove(q)
66
+
67
+ def broadcast_sse(event_name, payload):
68
+ """Push (event_name, payload) onto every currently-registered client queue.
69
+
70
+ If a client queue is full, drop the oldest event to make room (bounded backpressure).
71
+ This prevents one slow client from blocking the broadcast.
72
+ """
73
+ with _sse_lock:
74
+ clients = list(_sse_clients)
75
+ for q in clients:
76
+ try:
77
+ q.put_nowait((event_name, payload))
78
+ except queue.Full:
79
+ # Queue is full: drop the oldest event and retry
80
+ try:
81
+ q.get_nowait() # Remove oldest
82
+ q.put_nowait((event_name, payload)) # Add new
83
+ except Exception as e:
84
+ print(f"[collector_loop] Exception: {type(e).__name__}: {e}", file=sys.stderr, flush=True)
85
+ except Exception:
86
+ pass
87
+
88
+ def _maybe_emit(name, snapshot, last_hashes):
89
+ """Hash-gate: only store + broadcast a section if its content actually changed."""
90
+ payload = json.dumps(snapshot, default=str, sort_keys=True)
91
+ digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()
92
+ if last_hashes.get(name) == digest:
93
+ return
94
+ last_hashes[name] = digest
95
+ with _latest_lock:
96
+ _latest_snapshots[name] = payload
97
+ broadcast_sse(name, payload)
98
+
99
+ def collector_loop(stop_event):
100
+ """Background loop: poll cheap sources, gate expensive ones, broadcast on change."""
101
+ last_hashes = {}
102
+ last_backlog_mtime = object() # sentinel guaranteed != any real mtime/None
103
+ last_agents_fingerprint = None
104
+ cached_backlog_snapshot = {"tiers": []}
105
+ cached_agents_snapshot = []
106
+ last_tracker_mtime = object()
107
+ last_status_mtime = object()
108
+ cached_tracker_snapshot = {'items': []}
109
+ cached_status_snapshot = {'orchestrators': []}
110
+
111
+ while not stop_event.is_set():
112
+ try:
113
+ _maybe_emit("data", _snapshot_data(), last_hashes)
114
+
115
+ try:
116
+ backlog_mtime = config.AUDIT_BACKLOG_FILE.stat().st_mtime if config.AUDIT_BACKLOG_FILE.exists() else None
117
+ except OSError:
118
+ backlog_mtime = None
119
+ if backlog_mtime != last_backlog_mtime:
120
+ last_backlog_mtime = backlog_mtime
121
+ cached_backlog_snapshot = parse_audit_backlog()
122
+ _maybe_emit("backlog", cached_backlog_snapshot, last_hashes)
123
+
124
+ fingerprint = _transcripts_fingerprint()
125
+ if fingerprint != last_agents_fingerprint:
126
+ last_agents_fingerprint = fingerprint
127
+ cached_agents_snapshot = get_fleet_agents()
128
+ _maybe_emit("agents", cached_agents_snapshot, last_hashes)
129
+
130
+ # Emit tracker section (mtime-gated)
131
+ try:
132
+ tracker_mtime = (config.STATE_DIR / "tracker.json").stat().st_mtime if (config.STATE_DIR / "tracker.json").exists() else None
133
+ except OSError:
134
+ tracker_mtime = None
135
+ if tracker_mtime != last_tracker_mtime:
136
+ last_tracker_mtime = tracker_mtime
137
+ cached_tracker_snapshot = _snapshot_tracker()
138
+ _maybe_emit("tracker", cached_tracker_snapshot, last_hashes)
139
+
140
+ # Emit status section (mtime-gated)
141
+ try:
142
+ status_mtime = (config.STATE_DIR / "orchestrator-status.json").stat().st_mtime if (config.STATE_DIR / "orchestrator-status.json").exists() else None
143
+ except OSError:
144
+ status_mtime = None
145
+ if status_mtime != last_status_mtime:
146
+ last_status_mtime = status_mtime
147
+ cached_status_snapshot = _snapshot_orchestrator_status()
148
+ _maybe_emit("status", cached_status_snapshot, last_hashes)
149
+
150
+ # Drain inbox
151
+ try:
152
+ drain_tracker_inbox()
153
+ except Exception as e:
154
+ print(f"[collector] Inbox drain error: {e}", file=sys.stderr, flush=True)
155
+ except Exception as e:
156
+ print(f"[collector_loop] Exception: {type(e).__name__}: {e}", file=sys.stderr, flush=True)
157
+ stop_event.wait(config.COLLECTOR_INTERVAL)
158
+
159
+ def start_collector_thread():
160
+ """Idempotently start the background collector daemon thread (safe to call from
161
+ multiple request handlers / run_server — only the first call actually starts it)."""
162
+ global _collector_started
163
+ with _collector_lock:
164
+ if _collector_started:
165
+ return
166
+ _collector_started = True
167
+ t = threading.Thread(target=collector_loop, args=(_collector_stop_event,), daemon=True)
168
+ t.start()