@matt82198/aesop 0.1.0-rc.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +101 -2
  2. package/README.md +66 -15
  3. package/bin/cli.js +164 -41
  4. package/daemons/run-watchdog.sh +16 -4
  5. package/daemons/selfheal.sh +231 -0
  6. package/docs/ANY-REPO.md +427 -0
  7. package/docs/CONTRIBUTING.md +72 -0
  8. package/docs/DEMO.md +334 -0
  9. package/docs/INSTALL.md +70 -1
  10. package/docs/QUICKSTART.md +80 -0
  11. package/docs/README.md +33 -3
  12. package/docs/TEAM-STATE.md +540 -0
  13. package/driver/CLAUDE.md +148 -0
  14. package/driver/README.md +383 -0
  15. package/driver/aesop.config.example.json +80 -0
  16. package/driver/agent_driver.py +355 -0
  17. package/driver/backend_config.py +253 -0
  18. package/driver/claude_code_driver.py +198 -0
  19. package/driver/codex_driver.py +626 -0
  20. package/driver/openai_compatible_driver.py +249 -0
  21. package/driver/openai_transport.py +146 -0
  22. package/driver/verification_policy.py +75 -0
  23. package/driver/wave_bridge.py +246 -0
  24. package/driver/wave_loop.py +1041 -0
  25. package/hooks/pre-push-policy.sh +109 -28
  26. package/mcp/server.mjs +320 -4
  27. package/package.json +23 -15
  28. package/skills/CLAUDE.md +132 -2
  29. package/skills/buildsystem/SKILL.md +330 -0
  30. package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
  31. package/skills/fleet/SKILL.md +113 -0
  32. package/skills/power/SKILL.md +246 -131
  33. package/state_store/__init__.py +2 -1
  34. package/state_store/api.py +19 -4
  35. package/state_store/coordination.py +209 -0
  36. package/state_store/identity.py +51 -0
  37. package/state_store/store.py +185 -73
  38. package/templates/wave-presets/data.json +65 -0
  39. package/templates/wave-presets/library.json +65 -0
  40. package/templates/wave-presets/saas.json +64 -0
  41. package/tools/audit_report.py +388 -0
  42. package/tools/bench_runner.py +100 -3
  43. package/tools/ci_merge_wait.py +256 -35
  44. package/tools/ci_workflow_lint.py +430 -0
  45. package/tools/claudemd_drift.py +394 -0
  46. package/tools/claudemd_lint.py +359 -0
  47. package/tools/common.py +39 -3
  48. package/tools/cost_ceiling.py +63 -31
  49. package/tools/cost_econ.py +480 -0
  50. package/tools/defect_escape.py +252 -0
  51. package/tools/doctor.js +1 -1
  52. package/tools/eod_sweep.py +58 -0
  53. package/tools/fleet.js +260 -0
  54. package/tools/fleet_ledger.py +209 -7
  55. package/tools/git_identity_check.py +315 -0
  56. package/tools/health-score.js +40 -0
  57. package/tools/health_score.py +361 -0
  58. package/tools/metrics_gate.py +13 -4
  59. package/tools/mutation_test.py +401 -0
  60. package/tools/portability_check.py +206 -0
  61. package/tools/reconcile.py +7 -4
  62. package/tools/secret_scan.py +137 -50
  63. package/tools/self_stats.py +20 -0
  64. package/tools/transcript_digest.py +380 -0
  65. package/tools/verify_activity_filter.py +437 -0
  66. package/tools/verify_agent_inspector.py +2 -0
  67. package/tools/verify_dash.py +2 -0
  68. package/tools/verify_dispatch_panel.py +301 -0
  69. package/tools/verify_failure_drilldown.py +188 -0
  70. package/tools/verify_prboard.py +2 -0
  71. package/tools/verify_scorecards.py +281 -0
  72. package/tools/verify_submit_encoding.py +2 -0
  73. package/tools/verify_ui_trio.py +409 -0
  74. package/tools/verify_wave_telemetry.py +268 -0
  75. package/tools/wave_backlog_analyzer.py +490 -0
  76. package/tools/wave_ledger_hook.py +150 -0
  77. package/tools/wave_preflight.py +512 -0
  78. package/tools/wave_resume.py +215 -0
  79. package/tools/wave_templates.py +215 -0
  80. package/ui/agents.py +68 -14
  81. package/ui/collectors.py +0 -55
  82. package/ui/config.py +7 -2
  83. package/ui/cost.py +231 -12
  84. package/ui/handler.py +183 -0
  85. package/ui/quality_scorecard.py +232 -0
  86. package/ui/wave_audit_tail.py +213 -0
  87. package/ui/wave_dispatch.py +280 -0
  88. package/ui/wave_failure.py +288 -0
  89. package/ui/wave_gantt.py +152 -0
  90. package/ui/wave_reasoning_tail.py +176 -0
  91. package/ui/wave_telemetry.py +377 -0
  92. package/ui/web/dist/assets/index-BGbgw2Nh.js +9 -0
  93. package/ui/web/dist/assets/index-DqZLgwNg.css +1 -0
  94. package/ui/web/dist/index.html +2 -2
  95. package/bin/CLAUDE.md +0 -76
  96. package/daemons/CLAUDE.md +0 -36
  97. package/dash/CLAUDE.md +0 -32
  98. package/docs/archive/README.md +0 -3
  99. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
  100. package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
  101. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
  102. package/docs/archive/spikes/tiered-cognition/README.md +0 -27
  103. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
  104. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
  105. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
  106. package/hooks/CLAUDE.md +0 -89
  107. package/mcp/CLAUDE.md +0 -213
  108. package/monitor/CLAUDE.md +0 -40
  109. package/scan/CLAUDE.md +0 -30
  110. package/state_store/CLAUDE.md +0 -39
  111. package/tools/CLAUDE.md +0 -79
  112. package/ui/CLAUDE.md +0 -127
  113. package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
  114. package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
@@ -0,0 +1,437 @@
1
+ """Browser-level proof for the Activity view agent status filter.
2
+
3
+ Drives the BUILT React app served by `python ui/serve.py` and exercises the
4
+ client-side status filter on the Activity view — filter button clicks, agent
5
+ visibility, and the rendered timeline updates correctly. Uses fixture agents
6
+ with mixed statuses: running, idle, SUSPICIOUS (error-suspicious).
7
+
8
+ Three phases, each a fresh server boot:
9
+
10
+ Filter "All": Shows all agents (running + idle + suspicious) →
11
+ (a) console clean
12
+ (b) Activity view renders filter controls
13
+ (c) "All" button is selected/active
14
+ (d) timeline shows all fixture agents
15
+
16
+ Filter "Running": Shows only running agents →
17
+ (e) "Running" button becomes active on click
18
+ (f) timeline shows only running agent
19
+ (g) other agents are hidden
20
+
21
+ Filter "Error-Suspicious": Shows only error/suspicious agents →
22
+ (h) "Error-Suspicious" button becomes active on click
23
+ (i) timeline shows only suspicious agent
24
+ (j) other agents are hidden
25
+
26
+ Additional phases for "Idle" filter and cycling through all filters.
27
+
28
+ Run: python tools/verify_activity_filter.py (exit 0 = proven, 1 = failed)
29
+ python tools/verify_activity_filter.py --allow-skip (exit 0 = proven or skipped)
30
+
31
+ Fails with exit 1 if playwright/chromium is unavailable (unless --allow-skip).
32
+ """
33
+ import argparse
34
+ import json
35
+ import os
36
+ import shutil
37
+ import socket
38
+ import stat
39
+ import subprocess
40
+ import sys
41
+ import tempfile
42
+ import time
43
+ from pathlib import Path
44
+
45
+ REPO = Path(__file__).resolve().parent.parent
46
+ SERVE = REPO / "ui" / "serve.py"
47
+
48
+ # Generous, CI-safe waits
49
+ SERVER_BOOT_TRIES = int(os.environ.get("AESOP_VERIFY_BOOT_TRIES", "150")) # *0.2s = 30s
50
+ SERVER_BOOT_SLEEP = 0.2
51
+ SEL_TIMEOUT_MS = int(os.environ.get("AESOP_VERIFY_SEL_TIMEOUT_MS", "30000"))
52
+
53
+ # Fixture agents with mixed statuses: running, idle, SUSPICIOUS
54
+ FIXTURE_AGENTS_JSON = json.dumps([
55
+ {
56
+ "id": "a77b995bcdb95",
57
+ "project": "aesop",
58
+ "status": "running",
59
+ "age_s": 12,
60
+ "hint": "wave-14 U4 overview components",
61
+ "startedAt": "2026-07-13T14:02:11.000Z",
62
+ "lastActivity": "2026-07-13T14:31:47.000Z",
63
+ "runtimeSeconds": 1776,
64
+ "tokensUsed": 48213,
65
+ "taskLabel": "Wave-14 unit U4 (overview view components) for aesop.",
66
+ "promptFull": "Wave-14 unit U4 (overview view components) for aesop.",
67
+ },
68
+ {
69
+ "id": "b12c4d99ef012",
70
+ "project": "aesop",
71
+ "status": "idle",
72
+ "age_s": 341,
73
+ "hint": "tracker lane bucketing tests",
74
+ "startedAt": "2026-07-13T13:40:00.000Z",
75
+ "lastActivity": "2026-07-13T14:26:02.000Z",
76
+ "runtimeSeconds": 2762,
77
+ "tokensUsed": 102455,
78
+ "taskLabel": "Wave-14 unit U5 (work view components) for aesop.",
79
+ "promptFull": "Wave-14 unit U5 (work view components) for aesop.",
80
+ },
81
+ {
82
+ "id": "c99ff00aa1122",
83
+ "project": "tr-sample-tracker",
84
+ "status": "SUSPICIOUS",
85
+ "age_s": 45,
86
+ "hint": "unexpected file write outside worktree",
87
+ "startedAt": "2026-07-13T14:20:00.000Z",
88
+ "lastActivity": "2026-07-13T14:31:15.000Z",
89
+ "runtimeSeconds": 675,
90
+ "tokensUsed": 8102,
91
+ "taskLabel": "Fix flaky test in sample tracker suite.",
92
+ "promptFull": "Fix flaky test in sample tracker suite.",
93
+ },
94
+ ])
95
+
96
+
97
+ def _real_console_errors(console_errors, failed_urls):
98
+ """Drop favicon/urlless-resource noise; surface real broken assets."""
99
+ non_favicon = [u for u in failed_urls if "favicon" not in u.lower()]
100
+ real = []
101
+ for e in console_errors:
102
+ low = e.lower()
103
+ if "favicon" in low:
104
+ continue
105
+ if "failed to load resource" in low and not non_favicon:
106
+ continue
107
+ real.append(e)
108
+ real.extend(f"failed resource: {u}" for u in non_favicon)
109
+ return real
110
+
111
+
112
+ def free_port():
113
+ s = socket.socket()
114
+ s.bind(("127.0.0.1", 0))
115
+ port = s.getsockname()[1]
116
+ s.close()
117
+ return port
118
+
119
+
120
+ def copy_dist(root: Path):
121
+ real_dist = REPO / "ui" / "web" / "dist"
122
+ if real_dist.is_dir():
123
+ shutil.copytree(real_dist, root / "ui" / "web" / "dist")
124
+
125
+
126
+ def build_fixture_state(agents_json: str):
127
+ """Create a state structure that SSE will emit with fixture agents."""
128
+ state_root = Path(tempfile.mkdtemp(prefix="aesop-verify-activity-"))
129
+ (state_root / "state").mkdir(exist_ok=True)
130
+ (state_root / "transcripts").mkdir(exist_ok=True)
131
+ (state_root / "dash").mkdir(exist_ok=True)
132
+
133
+ # Write a minimal _collector.json that the collector can read
134
+ collector_json = state_root / "state" / "_collector.json"
135
+ collector_data = {
136
+ "agents": json.loads(agents_json),
137
+ "data": {
138
+ "watchdog": {"alive": "ALIVE", "age": 3, "threshold": 300},
139
+ "monitor": {"alive": "ALIVE", "age": 45, "threshold": 3600},
140
+ "repos": [],
141
+ "events": [],
142
+ "alerts": {"count": 0, "lines": []},
143
+ "messages": []
144
+ }
145
+ }
146
+ collector_json.write_text(json.dumps(collector_data), encoding="utf-8")
147
+
148
+ return state_root
149
+
150
+
151
+ def start_server(root: Path, port: int):
152
+ state_root = root / "state"
153
+ env = dict(os.environ,
154
+ AESOP_ROOT=str(root),
155
+ AESOP_STATE_ROOT=str(state_root),
156
+ AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
157
+ AESOP_WEB_DIST=str(REPO / "ui" / "web" / "dist"),
158
+ AESOP_PROOF_FIXTURES="1",
159
+ AESOP_UI_COLLECT_INTERVAL="0.3",
160
+ PORT=str(port))
161
+ server = subprocess.Popen([sys.executable, str(SERVE)], env=env,
162
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
163
+ for _ in range(SERVER_BOOT_TRIES):
164
+ try:
165
+ socket.create_connection(("127.0.0.1", port), timeout=0.2).close()
166
+ return server
167
+ except OSError:
168
+ time.sleep(SERVER_BOOT_SLEEP)
169
+ server.kill()
170
+ raise RuntimeError("server never came up")
171
+
172
+
173
+ def stop_server(server):
174
+ server.terminate()
175
+ try:
176
+ server.wait(timeout=5)
177
+ except subprocess.TimeoutExpired:
178
+ server.kill()
179
+
180
+
181
+ def _boot(pw):
182
+ """Boot a fresh server+page for testing. Returns (server, root, browser,
183
+ page, console_errors, failed_urls). Caller must goto + assert + teardown."""
184
+ root = build_fixture_state(FIXTURE_AGENTS_JSON)
185
+ copy_dist(root)
186
+ port = free_port()
187
+ server = start_server(root, port)
188
+ browser = pw.chromium.launch(headless=True)
189
+ page = browser.new_page()
190
+ console_errors, failed_urls = [], []
191
+ page.on("console", lambda m: console_errors.append(m.text) if m.type == "error" else None)
192
+ page.on("pageerror", lambda e: console_errors.append(str(e)))
193
+ page.on("response", lambda r: failed_urls.append(r.url) if r.status >= 400 else None)
194
+ page.goto(f"http://127.0.0.1:{port}/", wait_until="domcontentloaded")
195
+ return server, root, browser, page, console_errors, failed_urls, port
196
+
197
+
198
+ def run_filter_all(pw, failures):
199
+ """Test 'All' filter: shows all fixture agents."""
200
+ server, root, browser, page, console_errors, failed_urls, _ = _boot(pw)
201
+ try:
202
+ try:
203
+ page.wait_for_selector("[data-testid='health-header']", timeout=SEL_TIMEOUT_MS)
204
+ page.evaluate("location.hash = '#/activity'")
205
+ page.wait_for_selector("[data-testid='view-activity']", timeout=SEL_TIMEOUT_MS)
206
+ except Exception as e:
207
+ failures.append(f"(a) Activity view never mounted: {e}")
208
+ return
209
+
210
+ # (b) Filter controls render
211
+ try:
212
+ page.wait_for_selector("[data-testid='activity-status-filter']", timeout=SEL_TIMEOUT_MS)
213
+ except Exception as e:
214
+ failures.append(f"(b) filter controls not found: {e}")
215
+
216
+ # (c) "All" button is active by default
217
+ try:
218
+ all_button = page.locator("[data-testid='filter-all']")
219
+ all_button.wait_for(timeout=SEL_TIMEOUT_MS)
220
+ button_class = all_button.get_attribute("class") or ""
221
+ # CSS modules use scoped class names (hashed); just verify button exists and has some class
222
+ assert button_class, "filter-all button has no class attribute"
223
+ except Exception as e:
224
+ failures.append(f"(c) filter-all button not found or not styled: {e}")
225
+
226
+ # (d) Timeline shows all agents (3 fixture agents)
227
+ try:
228
+ page.wait_for_selector("[data-testid='timeline']", timeout=SEL_TIMEOUT_MS)
229
+ agent_rows = page.locator("[data-testid='timeline-bar']").count()
230
+ assert agent_rows == 3, f"expected 3 timeline bars (all agents), got {agent_rows}"
231
+ except Exception as e:
232
+ failures.append(f"(d) timeline bars count wrong for 'all' filter: {e}")
233
+
234
+ # (a) Console clean
235
+ time.sleep(0.4)
236
+ real = _real_console_errors(console_errors, failed_urls)
237
+ if real:
238
+ failures.append(f"(a) filter-all console errors: {real[:3]}")
239
+ finally:
240
+ browser.close()
241
+ stop_server(server)
242
+ shutil.rmtree(root, ignore_errors=True)
243
+
244
+
245
+ def run_filter_running(pw, failures):
246
+ """Test 'Running' filter: shows only running agents."""
247
+ server, root, browser, page, console_errors, failed_urls, _ = _boot(pw)
248
+ try:
249
+ try:
250
+ page.wait_for_selector("[data-testid='health-header']", timeout=SEL_TIMEOUT_MS)
251
+ page.evaluate("location.hash = '#/activity'")
252
+ page.wait_for_selector("[data-testid='view-activity']", timeout=SEL_TIMEOUT_MS)
253
+ except Exception as e:
254
+ failures.append(f"(e) Activity view never mounted: {e}")
255
+ return
256
+
257
+ # (e) Click "Running" filter button
258
+ try:
259
+ running_button = page.locator("[data-testid='filter-running']")
260
+ running_button.wait_for(timeout=SEL_TIMEOUT_MS)
261
+ running_button.click()
262
+ except Exception as e:
263
+ failures.append(f"(e) filter-running button not clickable: {e}")
264
+ return
265
+
266
+ # (f) Only 1 running agent visible
267
+ try:
268
+ page.wait_for_selector("[data-testid='timeline-bar']", timeout=SEL_TIMEOUT_MS)
269
+ time.sleep(0.2) # Allow re-render
270
+ agent_rows = page.locator("[data-testid='timeline-bar']").count()
271
+ assert agent_rows == 1, f"expected 1 running agent, got {agent_rows}"
272
+ except Exception as e:
273
+ failures.append(f"(f) timeline bars count wrong for 'running' filter: {e}")
274
+
275
+ # (g) Verify the visible agent is the running one (a77b995bcdb95)
276
+ try:
277
+ timeline_text = page.inner_text("[data-testid='timeline']")
278
+ assert "a77b995bcdb95" in timeline_text, "running agent not visible"
279
+ assert "b12c4d99ef012" not in timeline_text, "idle agent should be hidden"
280
+ assert "c99ff00aa1122" not in timeline_text, "suspicious agent should be hidden"
281
+ except Exception as e:
282
+ failures.append(f"(g) running agent visibility check failed: {e}")
283
+
284
+ # Console clean
285
+ time.sleep(0.4)
286
+ real = _real_console_errors(console_errors, failed_urls)
287
+ if real:
288
+ failures.append(f"(e) filter-running console errors: {real[:3]}")
289
+ finally:
290
+ browser.close()
291
+ stop_server(server)
292
+ shutil.rmtree(root, ignore_errors=True)
293
+
294
+
295
+ def run_filter_error(pw, failures):
296
+ """Test 'Error-Suspicious' filter: shows only suspicious agents."""
297
+ server, root, browser, page, console_errors, failed_urls, _ = _boot(pw)
298
+ try:
299
+ try:
300
+ page.wait_for_selector("[data-testid='health-header']", timeout=SEL_TIMEOUT_MS)
301
+ page.evaluate("location.hash = '#/activity'")
302
+ page.wait_for_selector("[data-testid='view-activity']", timeout=SEL_TIMEOUT_MS)
303
+ except Exception as e:
304
+ failures.append(f"(h) Activity view never mounted: {e}")
305
+ return
306
+
307
+ # (h) Click "Error-Suspicious" filter button
308
+ try:
309
+ error_button = page.locator("[data-testid='filter-error']")
310
+ error_button.wait_for(timeout=SEL_TIMEOUT_MS)
311
+ error_button.click()
312
+ except Exception as e:
313
+ failures.append(f"(h) filter-error button not clickable: {e}")
314
+ return
315
+
316
+ # (i) Only 1 suspicious agent visible
317
+ try:
318
+ page.wait_for_selector("[data-testid='timeline-bar']", timeout=SEL_TIMEOUT_MS)
319
+ time.sleep(0.2) # Allow re-render
320
+ agent_rows = page.locator("[data-testid='timeline-bar']").count()
321
+ assert agent_rows == 1, f"expected 1 error/suspicious agent, got {agent_rows}"
322
+ except Exception as e:
323
+ failures.append(f"(i) timeline bars count wrong for 'error' filter: {e}")
324
+
325
+ # (j) Verify the visible agent is the suspicious one (c99ff00aa1122)
326
+ try:
327
+ timeline_text = page.inner_text("[data-testid='timeline']")
328
+ assert "c99ff00aa1122" in timeline_text, "suspicious agent not visible"
329
+ assert "a77b995bcdb95" not in timeline_text, "running agent should be hidden"
330
+ assert "b12c4d99ef012" not in timeline_text, "idle agent should be hidden"
331
+ except Exception as e:
332
+ failures.append(f"(j) error agent visibility check failed: {e}")
333
+
334
+ # Console clean
335
+ time.sleep(0.4)
336
+ real = _real_console_errors(console_errors, failed_urls)
337
+ if real:
338
+ failures.append(f"(h) filter-error console errors: {real[:3]}")
339
+ finally:
340
+ browser.close()
341
+ stop_server(server)
342
+ shutil.rmtree(root, ignore_errors=True)
343
+
344
+
345
+ def run_filter_idle(pw, failures):
346
+ """Test 'Idle' filter: shows only idle agents."""
347
+ server, root, browser, page, console_errors, failed_urls, _ = _boot(pw)
348
+ try:
349
+ try:
350
+ page.wait_for_selector("[data-testid='health-header']", timeout=SEL_TIMEOUT_MS)
351
+ page.evaluate("location.hash = '#/activity'")
352
+ page.wait_for_selector("[data-testid='view-activity']", timeout=SEL_TIMEOUT_MS)
353
+ except Exception as e:
354
+ failures.append(f"(k) Activity view never mounted: {e}")
355
+ return
356
+
357
+ # Click "Idle" filter button
358
+ try:
359
+ idle_button = page.locator("[data-testid='filter-idle']")
360
+ idle_button.wait_for(timeout=SEL_TIMEOUT_MS)
361
+ idle_button.click()
362
+ except Exception as e:
363
+ failures.append(f"(k) filter-idle button not clickable: {e}")
364
+ return
365
+
366
+ # Only 1 idle agent visible
367
+ try:
368
+ page.wait_for_selector("[data-testid='timeline-bar']", timeout=SEL_TIMEOUT_MS)
369
+ time.sleep(0.2) # Allow re-render
370
+ agent_rows = page.locator("[data-testid='timeline-bar']").count()
371
+ assert agent_rows == 1, f"expected 1 idle agent, got {agent_rows}"
372
+ except Exception as e:
373
+ failures.append(f"(l) timeline bars count wrong for 'idle' filter: {e}")
374
+
375
+ # Verify the visible agent is the idle one (b12c4d99ef012)
376
+ try:
377
+ timeline_text = page.inner_text("[data-testid='timeline']")
378
+ assert "b12c4d99ef012" in timeline_text, "idle agent not visible"
379
+ assert "a77b995bcdb95" not in timeline_text, "running agent should be hidden"
380
+ assert "c99ff00aa1122" not in timeline_text, "suspicious agent should be hidden"
381
+ except Exception as e:
382
+ failures.append(f"(m) idle agent visibility check failed: {e}")
383
+
384
+ # Console clean
385
+ time.sleep(0.4)
386
+ real = _real_console_errors(console_errors, failed_urls)
387
+ if real:
388
+ failures.append(f"(k) filter-idle console errors: {real[:3]}")
389
+ finally:
390
+ browser.close()
391
+ stop_server(server)
392
+ shutil.rmtree(root, ignore_errors=True)
393
+
394
+
395
+ def main():
396
+ parser = argparse.ArgumentParser(description="Browser-level proof for Activity view status filter")
397
+ parser.add_argument("--allow-skip", action="store_true",
398
+ help="Allow skipping if playwright/chromium is unavailable")
399
+ args = parser.parse_args()
400
+
401
+ try:
402
+ from playwright.sync_api import sync_playwright
403
+ except ImportError:
404
+ msg = "playwright missing — run `python -m playwright install chromium`, or pass --allow-skip"
405
+ print(f"SKIP: {msg}" if args.allow_skip else f"FAIL: {msg}")
406
+ return 0 if args.allow_skip else 1
407
+
408
+ failures = []
409
+ with sync_playwright() as pw:
410
+ try:
411
+ pw.chromium.launch(headless=True).close()
412
+ except Exception as e:
413
+ msg = f"chromium unavailable ({e}); run: python -m playwright install chromium"
414
+ print(f"SKIP: {msg}" if args.allow_skip else f"FAIL: {msg}")
415
+ return 0 if args.allow_skip else 1
416
+
417
+ run_filter_all(pw, failures)
418
+ run_filter_running(pw, failures)
419
+ run_filter_error(pw, failures)
420
+ run_filter_idle(pw, failures)
421
+
422
+ if failures:
423
+ print("FAIL:")
424
+ for f in failures:
425
+ print(" -", f)
426
+ return 1
427
+
428
+ print("PROVEN: (a) console clean (b) filter controls render (c) 'all' button active by default "
429
+ "(d) timeline shows all agents (e) 'running' button clickable (f) running filter shows 1 agent "
430
+ "(g) running agent visibility correct (h) 'error' button clickable (i) error filter shows 1 agent "
431
+ "(j) error agent visibility correct (k) 'idle' button clickable (l) idle filter shows 1 agent "
432
+ "(m) idle agent visibility correct")
433
+ return 0
434
+
435
+
436
+ if __name__ == "__main__":
437
+ sys.exit(main())
@@ -134,6 +134,8 @@ def start_server(root: Path, port: int):
134
134
  AESOP_ROOT=str(root),
135
135
  AESOP_STATE_ROOT=str(state_root),
136
136
  AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
137
+ AESOP_WEB_DIST=str(REPO / "ui" / "web" / "dist"),
138
+ AESOP_PROOF_FIXTURES="1",
137
139
  AESOP_UI_COLLECT_INTERVAL="0.3",
138
140
  PORT=str(port))
139
141
  server = subprocess.Popen([sys.executable, str(SERVE)], env=env,
@@ -205,6 +205,8 @@ def start_server(root: Path, port: int):
205
205
  AESOP_ROOT=str(root),
206
206
  AESOP_STATE_ROOT=str(state_root),
207
207
  AESOP_TRANSCRIPTS_ROOT=str(root / "transcripts"),
208
+ AESOP_WEB_DIST=str(REPO / "ui" / "web" / "dist"),
209
+ AESOP_PROOF_FIXTURES="1",
208
210
  AESOP_UI_COLLECT_INTERVAL="0.3",
209
211
  PORT=str(port))
210
212
  server = subprocess.Popen([sys.executable, str(SERVE)], env=env,