@matt82198/aesop 0.1.0-beta.4 → 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.
- package/CHANGELOG.md +46 -5
- package/README.md +57 -1
- package/aesop.config.example.json +8 -1
- package/bin/CLAUDE.md +30 -2
- package/bin/cli.js +286 -73
- package/daemons/CLAUDE.md +4 -1
- package/daemons/run-watchdog.sh +5 -2
- package/docs/RELEASING.md +159 -0
- package/docs/archive/README.md +3 -0
- package/docs/{spikes → archive/spikes}/tiered-cognition/README.md +1 -3
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +23 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/CHARTER.md +76 -110
- package/monitor/collect-signals.mjs +85 -0
- package/package.json +4 -1
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +30 -149
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/ci_merge_wait.py +121 -8
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/secret_scan.py +8 -1
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +361 -542
- package/tools/verify_submit_encoding.py +12 -4
- package/ui/CLAUDE.md +57 -41
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +9 -5
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/submit.py +44 -5
- package/ui/collectors.py +184 -35
- package/ui/config.py +9 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +7 -3
- package/ui/handler.py +254 -4
- package/ui/render.py +26 -6
- package/ui/sse.py +16 -1
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
- package/ui/templates/dashboard.html +0 -1202
- /package/docs/{spikes → archive/spikes}/tiered-cognition/ACTIVATION.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/DESIGN.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/FINDINGS.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/aesop-cognition.example.md +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/force-model-policy.merged.mjs +0 -0
- /package/docs/{spikes → archive/spikes}/tiered-cognition/strip-tools-hook.mjs +0 -0
|
@@ -57,6 +57,14 @@ def build_fixture(root: Path):
|
|
|
57
57
|
(root / "state").mkdir(exist_ok=True)
|
|
58
58
|
(root / "transcripts").mkdir(exist_ok=True)
|
|
59
59
|
(root / "dash").mkdir(exist_ok=True)
|
|
60
|
+
|
|
61
|
+
# Copy ui/web/dist from the real repo so the server can serve the built React app
|
|
62
|
+
repo = Path(__file__).resolve().parent.parent
|
|
63
|
+
real_dist = repo / "ui" / "web" / "dist"
|
|
64
|
+
if real_dist.is_dir():
|
|
65
|
+
fixture_dist = root / "ui" / "web" / "dist"
|
|
66
|
+
shutil.copytree(real_dist, fixture_dist)
|
|
67
|
+
|
|
60
68
|
(root / "AUDIT-BACKLOG.md").write_text(
|
|
61
69
|
"# Audit backlog — verify_submit_encoding fixture\n\n## Landing log\n- fixture\n",
|
|
62
70
|
encoding="utf-8",
|
|
@@ -142,11 +150,11 @@ def main():
|
|
|
142
150
|
page = browser.new_page()
|
|
143
151
|
page.goto(f"http://127.0.0.1:{port}/", wait_until="domcontentloaded")
|
|
144
152
|
|
|
145
|
-
# Drive the real /submit flow:
|
|
153
|
+
# Drive the real /submit flow: inbox-input -> inbox-submit, real
|
|
146
154
|
# CSRF token (window.__AESOP_CSRF_TOKEN__ injected server-side).
|
|
147
|
-
page.wait_for_selector("
|
|
148
|
-
page.fill("
|
|
149
|
-
page.click("
|
|
155
|
+
page.wait_for_selector("[data-testid='inbox-input']", timeout=8000)
|
|
156
|
+
page.fill("[data-testid='inbox-input']", MARKER)
|
|
157
|
+
page.click("[data-testid='inbox-submit']")
|
|
150
158
|
|
|
151
159
|
# Wait for the inbox file to land on disk (async POST).
|
|
152
160
|
for _ in range(50):
|
package/ui/CLAUDE.md
CHANGED
|
@@ -2,28 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
**Purpose**: Stdlib-only local observability dashboard. Serves a dark-theme HTML dashboard on a configurable port (realtime via Server-Sent Events), enabling real-time fleet monitoring without external dependencies.
|
|
4
4
|
|
|
5
|
-
## Files (wave-
|
|
5
|
+
## Files (wave-14 U9 cutover: new React+Vite app structure)
|
|
6
6
|
|
|
7
|
+
**Backend (Python, unchanged from wave-9 split)**:
|
|
7
8
|
- **serve.py** — Thin (~65-line) entry point + composition layer. Wires the sibling modules, calls `config.reload()` / `csrf.init()` / `sse.reset_state()`, and re-exports their symbols so `serve.X` keeps resolving for the test suite (which loads serve.py by path) and for `python ui/serve.py`.
|
|
8
|
-
- **config.py** — Path / env / `aesop.config.json` resolution. `reload()` recomputes all path globals from the current environment. **Load-bearing rule: every other module reads `config.X` at call time (`import config`), never `from config import <path>` — a frozen import would go stale after reload() (breaks test-fixture isolation).**
|
|
9
|
+
- **config.py** — Path / env / `aesop.config.json` resolution. `reload()` recomputes all path globals from the current environment. **Load-bearing rule: every other module reads `config.X` at call time (`import config`), never `from config import <path>` — a frozen import would go stale after reload() (breaks test-fixture isolation).** Added in wave-14: `WEB_DIST` path pointing to `ui/web/dist/`.
|
|
9
10
|
- **csrf.py** — Session-token generation (atomic O_EXCL 0600) + `validate_csrf_request()`; `init()` sets `SESSION_TOKEN`.
|
|
10
11
|
- **collectors.py** — Read-only data collectors (heartbeats, repos, events, alerts, messages, backlog parse), tracker CRUD, and SSE section snapshots (incl. `read`-style tracker/orchestrator-status snapshots + inbox drain).
|
|
11
12
|
- **agents.py** — Agent transcript reading (`get_fleet_agents`, `extract_agent_dispatch_prompt`) and path-traversal-safe agent-id handling (`_AGENT_ID_FORBIDDEN`).
|
|
12
|
-
- **sse.py** — SSE client registry, bounded broadcast, hash-gated `_maybe_emit`, and the background `collector_loop`. `reset_state()` restores per-import collector isolation (the `sse` module is cached across test re-imports).
|
|
13
|
-
- **render.py** — Renders `
|
|
14
|
-
- **handler.py** — `DashboardHandler` (HTTP routing + all GET/POST endpoints incl. /api/tracker) and `run_server()`. Reads `config.X` / `csrf.SESSION_TOKEN` at call time.
|
|
15
|
-
- **
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
- **
|
|
23
|
-
- **
|
|
24
|
-
- **
|
|
25
|
-
|
|
26
|
-
|
|
13
|
+
- **sse.py** — SSE client registry, bounded broadcast, hash-gated `_maybe_emit`, and the background `collector_loop`. `reset_state()` restores per-import collector isolation (the `sse` module is cached across test re-imports). Extended in wave-14: emits "cost" as a 6th SSE section.
|
|
14
|
+
- **render.py** — Renders `ui/web/dist/index.html` (wave-14 U9 cutover: no fallback), substituting the CSRF token via a unique sentinel (the sentinel persists through Vite's build). Requires `template_path` parameter; legacy fallback removed.
|
|
15
|
+
- **handler.py** — `DashboardHandler` (HTTP routing + all GET/POST endpoints incl. /api/tracker) and `run_server()`. Wave-14 additions: static serving (`GET /assets/*` from `ui/web/dist/assets/`), `/api/state` consolidated snapshot, `/api/session` for Vite dev server, `/api/cost` scorecard. Reads `config.X` / `csrf.SESSION_TOKEN` at call time.
|
|
16
|
+
- **cost.py** — Parser for `state/ledger/OUTCOMES-LEDGER.md` (markdown table); returns per-model and per-day aggregates, verdict scorecards, and optional dollar estimates (only if `aesop.config.json` supplies `pricing` map).
|
|
17
|
+
|
|
18
|
+
## State Store Integration (Wave-15)
|
|
19
|
+
|
|
20
|
+
The tracker uses a **dual-path mutation model** backed by an event-sourced SQLite WAL store:
|
|
21
|
+
|
|
22
|
+
- **Write path**: Mutations append events to `tracker_events.db` (WAL mode) via the StateAPI layer (`state_store/api.py`). Each mutation (create/update/delete) produces an immutable event in the event log.
|
|
23
|
+
- **Read path**: `tracker.json` is re-rendered as an export (projection) from the event log on every read. This export is committed to git for checkpoint durability.
|
|
24
|
+
- **Location**: `state/tracker_events.db` (SQLite WAL, never committed). `tracker.json` (rendered export, committed).
|
|
25
|
+
- **Render-failure recovery**: If projection rendering fails, the UI falls back to the last-known good `tracker.json`; no data loss. Stale renders are repaired on the next successful mutation (idempotent re-render).
|
|
26
|
+
|
|
27
|
+
This design provides:
|
|
28
|
+
- **Durable event audit trail** (append-only events in WAL)
|
|
29
|
+
- **Git-friendly checkpoints** (rendered tracker.json snapshots)
|
|
30
|
+
- **Atomic mutations** (events append, projection re-renders)
|
|
31
|
+
- **Graceful degradation** (fallback to last-known-good on render failure)
|
|
32
|
+
|
|
33
|
+
**Frontend (React 18 + Vite + TypeScript, wave-14 U1–U8)**:
|
|
34
|
+
- **ui/web/** — Complete React application (TypeScript, Vite, TypeScript strict mode).
|
|
35
|
+
- **src/main.tsx** — Vite entry point; renders `<App />` to `#root`.
|
|
36
|
+
- **src/App.tsx** — App shell: header + hash-routed view slots (/#/, /#/work, /#/activity, /#/cost).
|
|
37
|
+
- **src/styles/tokens.css** + **src/styles/global.css** — Design tokens (light/dark color palettes, spacing, typography) + base resets.
|
|
38
|
+
- **src/views/** — Page-level components (Overview, Work, Activity, Cost) with SSE bindings.
|
|
39
|
+
- **src/components/** — Reusable UI components (HealthHeader, AgentsPanel, TrackerBoard, Timeline, CostChart, etc.).
|
|
40
|
+
- **src/lib/api.ts** — Typed fetch helpers + CSRF header injection + `/api/session` fallback for dev server.
|
|
41
|
+
- **src/lib/useSSE.ts** — EventSource hook with reconnect logic, per-section state, connection status.
|
|
42
|
+
- **src/lib/types.ts** — TypeScript types for all API payloads (contract with backend).
|
|
43
|
+
- **src/lib/sanitizeUrl.ts** — XSS-safe URL parsing (PR links inert on bad schemes).
|
|
44
|
+
- **vite.config.ts** — Vite config with API proxy for `/data`, `/api`, `/events`, `/agent`, `/submit` to :8770.
|
|
45
|
+
- **dist/** — Built static files (committed to git; served by Python handler). Filenames are content-hashed by Vite.
|
|
46
|
+
- **README.md** — User guide and configuration reference (updated for wave-14).
|
|
27
47
|
|
|
28
48
|
## Configuration & Path Precedence
|
|
29
49
|
|
|
@@ -62,43 +82,39 @@ Configuration is resolved in this order (first match wins):
|
|
|
62
82
|
- CLI tools read token from `state/.ui-session-token` (0600) to submit to /submit endpoint
|
|
63
83
|
- Legitimate browser clients: token injected into HTML template, sent by JavaScript
|
|
64
84
|
|
|
85
|
+
## API Endpoints (wave-14 wave additions)
|
|
86
|
+
|
|
87
|
+
**Read-only (no CSRF required)**:
|
|
88
|
+
- `GET /` — Renders `ui/web/dist/index.html` with CSRF token substituted (hard 500 if dist missing).
|
|
89
|
+
- `GET /assets/*` — Static files from `ui/web/dist/assets/` (content-hashed, immutable cache headers, path-traversal-safe).
|
|
90
|
+
- `GET /api/state` — Consolidated first-paint snapshot: `{data, backlog, agents, tracker, status, cost}` in one round trip (reuses latest snapshots).
|
|
91
|
+
- `GET /api/session` — Returns `{token}` for Vite dev server; Origin-checked fail-closed (only local origins).
|
|
92
|
+
- `GET /api/cost` — Cost/scorecard summary from `state/ledger/OUTCOMES-LEDGER.md` (per-model, per-day, verdicts, optional pricing estimates).
|
|
93
|
+
- `GET /events` — Server-Sent Events stream (read-only, no CSRF).
|
|
94
|
+
|
|
95
|
+
**Mutations (CSRF-gated)**:
|
|
96
|
+
- `POST /submit` — Append to inbox.
|
|
97
|
+
- `POST /api/tracker`, `POST /api/tracker/<id>` — Tracker CRUD.
|
|
98
|
+
|
|
65
99
|
## Server-Sent Events (SSE) Model
|
|
66
100
|
|
|
67
101
|
**Realtime streaming via GET /events**:
|
|
68
102
|
- ThreadingHTTPServer required (SSE holds one connection per client)
|
|
69
|
-
- Streams JSON frames: `event:
|
|
103
|
+
- Streams JSON frames: `event: <section>` / `data: <json>`
|
|
70
104
|
- Keepalive comment-line (`: keepalive`) sent every ~15s to prevent timeout
|
|
71
105
|
- Read-only stream; no mutations
|
|
72
106
|
|
|
73
|
-
**Sections emitted** (only on content change):
|
|
107
|
+
**Sections emitted** (only on content change, in order):
|
|
74
108
|
1. **data** — heartbeat status, daemon state, log tail
|
|
75
109
|
2. **backlog** — AUDIT-BACKLOG.md parsed into tier buckets (P0/P1/P2/Needs decision)
|
|
76
110
|
3. **agents** — fleet agent activity (from Claude transcripts directory)
|
|
111
|
+
4. **tracker** — tracker items (CRUD mutations reflected in realtime)
|
|
112
|
+
5. **status** — orchestrator phase + activity
|
|
113
|
+
6. **cost** — cost/scorecard summary (wave-14 addition)
|
|
77
114
|
|
|
78
115
|
## Background Collector Thread
|
|
79
116
|
|
|
80
|
-
|
|
81
|
-
- Started idempotently on first HTTP request (via `start_collector_thread()`)
|
|
82
|
-
- Runs as daemon thread (terminates when main process exits)
|
|
83
|
-
- Single-instance guard via `_collector_lock` (prevents double-start)
|
|
84
|
-
|
|
85
|
-
**Polling strategy**:
|
|
86
|
-
- Wakes every `COLLECTOR_INTERVAL` seconds (default: 1.0, overridable via `AESOP_UI_COLLECT_INTERVAL`)
|
|
87
|
-
- Polls cheap sources: heartbeat file mtimes, log tails, file fingerprints
|
|
88
|
-
- Only re-derives sections when underlying input changed (mtime/fingerprint gate)
|
|
89
|
-
- Only emits to clients when section content-hash changed (avoids redundant broadcasts)
|
|
90
|
-
|
|
91
|
-
**Cached data**:
|
|
92
|
-
- Last computed hash per section (data/backlog/agents)
|
|
93
|
-
- Last mtime of AUDIT-BACKLOG.md
|
|
94
|
-
- Last fingerprint of transcripts directory (to detect agent activity changes)
|
|
95
|
-
- Cached parsed backlog and agent snapshot (reused until inputs change)
|
|
96
|
-
|
|
97
|
-
**Content-change detection**:
|
|
98
|
-
- Mtime-gated: backlog section re-derived only if AUDIT-BACKLOG.md mtime changed
|
|
99
|
-
- Fingerprint-gated: agents section re-derived only if transcripts directory content changed
|
|
100
|
-
- Hash-gated: broadcast only sent if content-hash differs from last broadcast
|
|
101
|
-
- This avoids expensive operations (subprocess for dash-extra.mjs, directory walk) on every tick
|
|
117
|
+
Daemon thread polling heartbeats/logs via mtime/fingerprint gates; re-derives SSE sections only on input change, broadcasts only when content-hash differs (avoids expensive operations on every tick). Started idempotently on first HTTP request via `start_collector_thread()`, runs with single-instance guard `_collector_lock`, wakes every `COLLECTOR_INTERVAL` (default 1.0s, `AESOP_UI_COLLECT_INTERVAL`).
|
|
102
118
|
|
|
103
119
|
## Invariants & Gotchas
|
|
104
120
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/ui/agents.py
CHANGED
|
@@ -55,7 +55,7 @@ _AGENT_ID_FORBIDDEN = re.compile(r'\.\.|[/\\*?\[\]]')
|
|
|
55
55
|
|
|
56
56
|
def extract_agent_dispatch_prompt(agent_id):
|
|
57
57
|
"""
|
|
58
|
-
Extract dispatch prompt and metadata from agent
|
|
58
|
+
Extract dispatch prompt and metadata from agent jsonl transcript.
|
|
59
59
|
Returns dict with prompt, dispatcher, model, activity times, and message count.
|
|
60
60
|
Robust: missing/invalid file -> {error: "..."}
|
|
61
61
|
Security: rejects agent_id containing path-traversal or glob-metacharacter
|
|
@@ -64,20 +64,24 @@ def extract_agent_dispatch_prompt(agent_id):
|
|
|
64
64
|
carry "invalid": True when the input itself was rejected, so callers can
|
|
65
65
|
map that to an HTTP 400 rather than a plain 404.
|
|
66
66
|
|
|
67
|
-
CRITICAL: Use prefix-matching via glob, not exact match. The dashboard
|
|
68
|
-
|
|
67
|
+
CRITICAL: Use prefix-matching via glob, not exact match. The dashboard (dash-extra.mjs)
|
|
68
|
+
scans for agent-*.jsonl files and emits a 13-char truncated ID; files on disk carry
|
|
69
|
+
full IDs (e.g., agent-a77b995bcdb953e9c1234567.jsonl). This function must search for
|
|
70
|
+
the same file format that the dashboard actually finds.
|
|
69
71
|
"""
|
|
70
72
|
try:
|
|
71
73
|
if not agent_id or _AGENT_ID_FORBIDDEN.search(agent_id):
|
|
72
74
|
return {"error": "invalid agent id", "invalid": True}
|
|
73
75
|
|
|
74
|
-
# Prefix-match: search in config.TRANSCRIPTS_ROOT for files matching agent_id*.
|
|
76
|
+
# Prefix-match: search in config.TRANSCRIPTS_ROOT for files matching agent-agent_id*.jsonl
|
|
77
|
+
# The dashboard (dash-extra.mjs) scans for agent-*.jsonl files and emits
|
|
78
|
+
# a 13-char truncated ID; we must match against the same .jsonl files.
|
|
75
79
|
if not config.TRANSCRIPTS_ROOT.exists():
|
|
76
80
|
return {"error": f"transcripts root not found at {config.TRANSCRIPTS_ROOT}"}
|
|
77
81
|
|
|
78
82
|
# Glob for matching files (prefix-match handles truncated IDs)
|
|
79
83
|
matches = sorted(
|
|
80
|
-
config.TRANSCRIPTS_ROOT.glob(f"**/{agent_id}*.
|
|
84
|
+
config.TRANSCRIPTS_ROOT.glob(f"**/agent-{agent_id}*.jsonl"),
|
|
81
85
|
key=lambda p: p.stat().st_mtime, reverse=True,
|
|
82
86
|
)
|
|
83
87
|
if not matches:
|
|
Binary file
|
package/ui/api/submit.py
CHANGED
|
@@ -11,13 +11,50 @@ planted at config.INBOX_FILE).
|
|
|
11
11
|
Reads config.INBOX_FILE live via `import config` at call time, never
|
|
12
12
|
`from config import INBOX_FILE` -- a frozen import goes stale after
|
|
13
13
|
config.reload() (breaks test-fixture isolation). See ui/CLAUDE.md.
|
|
14
|
+
|
|
15
|
+
Detects both POSIX symlinks and Windows junctions/reparse points.
|
|
14
16
|
"""
|
|
15
17
|
import os
|
|
18
|
+
import stat
|
|
16
19
|
from datetime import datetime
|
|
17
20
|
|
|
18
21
|
import config
|
|
19
22
|
|
|
20
23
|
|
|
24
|
+
def _is_link_or_reparse(path):
|
|
25
|
+
"""Check if path is a symlink or Windows reparse point (junction/etc).
|
|
26
|
+
|
|
27
|
+
Returns True if:
|
|
28
|
+
- On all platforms: path is a POSIX symlink (detected via st_mode)
|
|
29
|
+
- On Windows: path has the FILE_ATTRIBUTE_REPARSE_POINT attribute
|
|
30
|
+
(which includes junctions, symlinks, and other reparse points)
|
|
31
|
+
|
|
32
|
+
Uses os.lstat() to detect without following links (includes dangling ones).
|
|
33
|
+
On error (e.g., path doesn't exist), returns False to let caller's
|
|
34
|
+
exists() check handle it normally.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
stat_result = os.lstat(str(path))
|
|
38
|
+
|
|
39
|
+
# On POSIX, check the symlink bit in st_mode
|
|
40
|
+
if stat.S_ISLNK(stat_result.st_mode):
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
# On Windows, check for reparse point attribute (junctions, symlinks, etc)
|
|
44
|
+
# FILE_ATTRIBUTE_REPARSE_POINT = 0x400 (1024 decimal)
|
|
45
|
+
# st_file_attributes only exists on Windows
|
|
46
|
+
if hasattr(stat_result, 'st_file_attributes'):
|
|
47
|
+
FILE_ATTRIBUTE_REPARSE_POINT = 0x400
|
|
48
|
+
if stat_result.st_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT:
|
|
49
|
+
return True
|
|
50
|
+
|
|
51
|
+
return False
|
|
52
|
+
except (OSError, AttributeError):
|
|
53
|
+
# If we can't stat it, let it fall through to the exists() check
|
|
54
|
+
# (file doesn't exist, which is fine)
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
|
|
21
58
|
def append_to_inbox(text):
|
|
22
59
|
"""Append one inbox line for `text` to config.INBOX_FILE.
|
|
23
60
|
|
|
@@ -35,11 +72,13 @@ def append_to_inbox(text):
|
|
|
35
72
|
"""
|
|
36
73
|
inbox_content = f"- [{datetime.now().isoformat()}] {text}\n"
|
|
37
74
|
|
|
38
|
-
# Security: reject symlinks (TOCTOU defense) FIRST —
|
|
39
|
-
# of exists(). Path.exists() follows the link,
|
|
40
|
-
# not yet created) returns False and would
|
|
41
|
-
# open(...,'w') would follow the link and
|
|
42
|
-
|
|
75
|
+
# Security: reject symlinks and Windows junctions (TOCTOU defense) FIRST —
|
|
76
|
+
# check link/reparse INDEPENDENT of exists(). Path.exists() follows the link,
|
|
77
|
+
# so a DANGLING symlink (target not yet created) returns False and would
|
|
78
|
+
# otherwise skip this check, then open(...,'w') would follow the link and
|
|
79
|
+
# create the attacker's target. Windows junctions are not detected by
|
|
80
|
+
# os.path.islink(), so we use _is_link_or_reparse() instead.
|
|
81
|
+
if _is_link_or_reparse(config.INBOX_FILE):
|
|
43
82
|
return False, (400, {"error": "Inbox file is a symlink (rejected for security)"})
|
|
44
83
|
|
|
45
84
|
if not config.INBOX_FILE.exists():
|
package/ui/collectors.py
CHANGED
|
@@ -316,7 +316,27 @@ def get_alerts():
|
|
|
316
316
|
return alerts
|
|
317
317
|
|
|
318
318
|
def load_tracker():
|
|
319
|
-
"""Load tracker.json, return empty tracker if missing or corrupt.
|
|
319
|
+
"""Load tracker.json, return empty tracker if missing or corrupt.
|
|
320
|
+
|
|
321
|
+
Defect 2 fix: Check dirty flag. If render previously failed, the event log
|
|
322
|
+
and tracker.json may be out of sync. Re-render from projection to self-heal.
|
|
323
|
+
"""
|
|
324
|
+
# Check dirty flag (indicates previous render failure)
|
|
325
|
+
dirty_file = config.STATE_DIR / ".tracker-render-dirty"
|
|
326
|
+
if dirty_file.exists():
|
|
327
|
+
try:
|
|
328
|
+
print("[tracker] Detected previous render failure; recovering...", file=sys.stderr)
|
|
329
|
+
api = _tracker_api()
|
|
330
|
+
# Force re-render from projection
|
|
331
|
+
try:
|
|
332
|
+
save_tracker(api.project("tracker"))
|
|
333
|
+
dirty_file.unlink()
|
|
334
|
+
print("[tracker] Recovery render completed", file=sys.stderr)
|
|
335
|
+
except Exception as e:
|
|
336
|
+
print(f"[tracker] Recovery render failed: {e}", file=sys.stderr)
|
|
337
|
+
except Exception as e:
|
|
338
|
+
print(f"[tracker] Failed to recover from dirty flag: {e}", file=sys.stderr)
|
|
339
|
+
|
|
320
340
|
if not config.TRACKER_FILE.exists():
|
|
321
341
|
return {"version": 1, "items": []}
|
|
322
342
|
|
|
@@ -418,9 +438,95 @@ def get_tracker_items(status=None, priority=None):
|
|
|
418
438
|
|
|
419
439
|
return items
|
|
420
440
|
|
|
441
|
+
# --- Event-sourced tracker backing (state_store) -----------------------------
|
|
442
|
+
# The event log is the write-authority + audit trail; tracker.json is kept as
|
|
443
|
+
# the rendered export that reads/SSE/UI consume. Each mutation appends an event
|
|
444
|
+
# and re-renders the file (atomic os.replace via save_tracker), replacing the
|
|
445
|
+
# old load->mutate->save read-modify-write that raced under concurrent writers.
|
|
446
|
+
# The live read path (load_tracker / get_tracker_items / SSE snapshot) is
|
|
447
|
+
# unchanged: it still reads tracker.json, which every write keeps current.
|
|
448
|
+
|
|
449
|
+
def _tracker_api():
|
|
450
|
+
"""Return a StateAPI over state/tracker_events.db (lazy import; call-time paths)."""
|
|
451
|
+
try:
|
|
452
|
+
from state_store import StateAPI
|
|
453
|
+
except ImportError:
|
|
454
|
+
from pathlib import Path as _Path
|
|
455
|
+
root = str(_Path(__file__).resolve().parents[1])
|
|
456
|
+
if root not in sys.path:
|
|
457
|
+
sys.path.insert(0, root)
|
|
458
|
+
from state_store import StateAPI
|
|
459
|
+
config.STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
460
|
+
return StateAPI(str(config.STATE_DIR / "tracker_events.db"))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _ensure_tracker_migrated(api):
|
|
464
|
+
"""Backfill the event log from the existing tracker.json once (idempotent).
|
|
465
|
+
|
|
466
|
+
Defect 3 fix: Guard migration with a marker event (migration_started with
|
|
467
|
+
version=1). The first caller to append this marker wins and performs the
|
|
468
|
+
backfill. Subsequent callers see the marker and skip the backfill. This
|
|
469
|
+
prevents concurrent callers from polluting the audit log with duplicate
|
|
470
|
+
item_created events.
|
|
471
|
+
"""
|
|
472
|
+
events = api.get("tracker")
|
|
473
|
+
|
|
474
|
+
# Check for migration marker (first event of type "migration_started")
|
|
475
|
+
has_migration_marker = any(e.get("type") == "migration_started" and
|
|
476
|
+
e.get("payload", {}).get("version") == 1
|
|
477
|
+
for e in events)
|
|
478
|
+
if has_migration_marker:
|
|
479
|
+
# Migration already completed (or in progress); skip backfill
|
|
480
|
+
return
|
|
481
|
+
|
|
482
|
+
# Append migration marker first to atomically claim the migration
|
|
483
|
+
try:
|
|
484
|
+
api.append("tracker", "migration_started", {"version": 1}, "system")
|
|
485
|
+
except Exception as e:
|
|
486
|
+
print(f"[tracker] Failed to append migration marker: {e}", file=sys.stderr)
|
|
487
|
+
return
|
|
488
|
+
|
|
489
|
+
# Now safe to backfill (other callers will see marker and skip)
|
|
490
|
+
if config.TRACKER_FILE.exists():
|
|
491
|
+
try:
|
|
492
|
+
data = json.loads(config.TRACKER_FILE.read_text(encoding='utf-8'))
|
|
493
|
+
except Exception:
|
|
494
|
+
data = {"items": []}
|
|
495
|
+
for item in data.get("items", []):
|
|
496
|
+
if isinstance(item, dict) and item.get("id"):
|
|
497
|
+
api.append("tracker", "item_created", item, "migration")
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _tracker_items_by_id(api):
|
|
501
|
+
return {it["id"]: it for it in api.project("tracker")["items"]}
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _render_tracker(api):
|
|
505
|
+
"""Materialize the projection back to tracker.json (atomic).
|
|
506
|
+
|
|
507
|
+
Defect 2 fix: Wrap render in try/except. On failure, log loudly and mark
|
|
508
|
+
a dirty flag so the next read triggers a recovery re-render. This ensures
|
|
509
|
+
that if the event log has events but tracker.json is stale, the next read
|
|
510
|
+
self-heals by detecting the mismatch and re-rendering.
|
|
511
|
+
"""
|
|
512
|
+
try:
|
|
513
|
+
save_tracker(api.project("tracker"))
|
|
514
|
+
except Exception as e:
|
|
515
|
+
print(f"[tracker] CRITICAL: Failed to render tracker after event append: {e}",
|
|
516
|
+
file=sys.stderr)
|
|
517
|
+
# Mark dirty flag for recovery on next read
|
|
518
|
+
dirty_file = config.STATE_DIR / ".tracker-render-dirty"
|
|
519
|
+
try:
|
|
520
|
+
dirty_file.write_text(str(time()), encoding='utf-8')
|
|
521
|
+
except Exception as e2:
|
|
522
|
+
print(f"[tracker] Failed to write dirty flag: {e2}", file=sys.stderr)
|
|
523
|
+
raise
|
|
524
|
+
|
|
525
|
+
|
|
421
526
|
def create_tracker_item(data):
|
|
422
|
-
"""Create a new tracker item."""
|
|
423
|
-
|
|
527
|
+
"""Create a new tracker item (event-sourced; tracker.json re-rendered)."""
|
|
528
|
+
api = _tracker_api()
|
|
529
|
+
_ensure_tracker_migrated(api)
|
|
424
530
|
|
|
425
531
|
item = {
|
|
426
532
|
"id": secrets.token_hex(6),
|
|
@@ -436,39 +542,43 @@ def create_tracker_item(data):
|
|
|
436
542
|
"completed_at": None
|
|
437
543
|
}
|
|
438
544
|
|
|
439
|
-
tracker["
|
|
440
|
-
|
|
545
|
+
api.append("tracker", "item_created", item, item["source"])
|
|
546
|
+
_render_tracker(api)
|
|
441
547
|
return item
|
|
442
548
|
|
|
443
549
|
def update_tracker_item(item_id, update_data):
|
|
444
|
-
"""Update a tracker item by id."""
|
|
445
|
-
|
|
550
|
+
"""Update a tracker item by id (event-sourced)."""
|
|
551
|
+
api = _tracker_api()
|
|
552
|
+
_ensure_tracker_migrated(api)
|
|
446
553
|
|
|
447
|
-
|
|
448
|
-
if not
|
|
554
|
+
current = _tracker_items_by_id(api)
|
|
555
|
+
if item_id not in current:
|
|
449
556
|
raise Exception(f"404 Item not found: {item_id}")
|
|
450
557
|
|
|
558
|
+
patch = {"id": item_id}
|
|
451
559
|
for key in ["status", "lane", "priority", "notes", "pr_link", "tags"]:
|
|
452
560
|
if key in update_data:
|
|
453
|
-
|
|
561
|
+
patch[key] = update_data[key]
|
|
454
562
|
|
|
455
|
-
if update_data.get("status") == "done" and not
|
|
456
|
-
|
|
563
|
+
if update_data.get("status") == "done" and not current[item_id].get("completed_at"):
|
|
564
|
+
patch["completed_at"] = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
|
|
457
565
|
|
|
458
|
-
|
|
459
|
-
|
|
566
|
+
api.append("tracker", "item_updated", patch, "api")
|
|
567
|
+
_render_tracker(api)
|
|
568
|
+
return _tracker_items_by_id(api)[item_id]
|
|
460
569
|
|
|
461
570
|
def delete_tracker_item(item_id):
|
|
462
|
-
"""Soft-delete a tracker item (mark as archived)."""
|
|
463
|
-
|
|
571
|
+
"""Soft-delete a tracker item (mark as archived; event-sourced)."""
|
|
572
|
+
api = _tracker_api()
|
|
573
|
+
_ensure_tracker_migrated(api)
|
|
464
574
|
|
|
465
|
-
|
|
466
|
-
if not
|
|
575
|
+
current = _tracker_items_by_id(api)
|
|
576
|
+
if item_id not in current:
|
|
467
577
|
raise Exception(f"404 Item not found: {item_id}")
|
|
468
578
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
return
|
|
579
|
+
api.append("tracker", "item_archived", {"id": item_id}, "api")
|
|
580
|
+
_render_tracker(api)
|
|
581
|
+
return _tracker_items_by_id(api)[item_id]
|
|
472
582
|
|
|
473
583
|
def _snapshot_data():
|
|
474
584
|
"""Everything the 'data' SSE section covers (header, repos, events, alerts, messages)."""
|
|
@@ -530,27 +640,66 @@ def _snapshot_orchestrator_status():
|
|
|
530
640
|
return {"orchestrators": []}
|
|
531
641
|
|
|
532
642
|
def drain_tracker_inbox():
|
|
533
|
-
"""Drain .tracker-inbox.jsonl, create items idempotently.
|
|
643
|
+
"""Drain .tracker-inbox.jsonl, create items idempotently.
|
|
644
|
+
|
|
645
|
+
Defect 1 fix: Atomically rename inbox file to unique processing name FIRST
|
|
646
|
+
to ensure only one caller processes it under concurrent access. Strengthen
|
|
647
|
+
dedup to check both tracker.json AND api.project("tracker") so items
|
|
648
|
+
in the event store but not yet rendered are also excluded.
|
|
649
|
+
"""
|
|
534
650
|
inbox_file = config.STATE_DIR / ".tracker-inbox.jsonl"
|
|
535
651
|
if not inbox_file.exists():
|
|
536
652
|
return []
|
|
537
|
-
|
|
653
|
+
|
|
538
654
|
created = []
|
|
539
655
|
try:
|
|
540
|
-
|
|
656
|
+
# Defect 1: Atomically rename inbox to unique processing name first.
|
|
657
|
+
# This ensures only one caller wins; others see no file.
|
|
658
|
+
processing_file = inbox_file.with_name(
|
|
659
|
+
f".tracker-inbox.jsonl.processing-{secrets.token_hex(8)}"
|
|
660
|
+
)
|
|
661
|
+
try:
|
|
662
|
+
os.replace(str(inbox_file), str(processing_file))
|
|
663
|
+
except FileNotFoundError:
|
|
664
|
+
# Another caller already renamed it; nothing to process
|
|
665
|
+
return []
|
|
666
|
+
except Exception as e:
|
|
667
|
+
print(f"[inbox] Failed to rename inbox: {e}", file=sys.stderr)
|
|
668
|
+
return []
|
|
669
|
+
|
|
670
|
+
content = processing_file.read_text(encoding='utf-8')
|
|
541
671
|
if not content.strip():
|
|
542
|
-
|
|
672
|
+
processing_file.unlink()
|
|
543
673
|
return []
|
|
544
|
-
|
|
674
|
+
|
|
545
675
|
lines = content.strip().splitlines()
|
|
546
|
-
|
|
676
|
+
|
|
677
|
+
# Defect 1: Build dedup hash set from both tracker.json AND event store projection.
|
|
678
|
+
# This catches items in the event store that haven't been rendered to tracker.json yet.
|
|
547
679
|
existing_hashes = set()
|
|
548
|
-
|
|
680
|
+
|
|
681
|
+
# Check rendered tracker.json
|
|
682
|
+
tracker_json = load_tracker()
|
|
683
|
+
for item in tracker_json.get("items", []):
|
|
549
684
|
source = item.get("source", "")
|
|
550
685
|
title = item.get("title", "")
|
|
551
686
|
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
552
687
|
existing_hashes.add(h)
|
|
553
|
-
|
|
688
|
+
|
|
689
|
+
# Also check event store projection (catches items in DB but not yet rendered)
|
|
690
|
+
try:
|
|
691
|
+
api = _tracker_api()
|
|
692
|
+
projected = api.project("tracker")
|
|
693
|
+
for item in projected.get("items", []):
|
|
694
|
+
source = item.get("source", "")
|
|
695
|
+
title = item.get("title", "")
|
|
696
|
+
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
697
|
+
existing_hashes.add(h)
|
|
698
|
+
except Exception as e:
|
|
699
|
+
print(f"[inbox] Failed to project tracker state: {e}", file=sys.stderr)
|
|
700
|
+
# Fall back to tracker.json only if projection fails
|
|
701
|
+
pass
|
|
702
|
+
|
|
554
703
|
rejects = []
|
|
555
704
|
for line in lines:
|
|
556
705
|
line = line.strip()
|
|
@@ -561,11 +710,11 @@ def drain_tracker_inbox():
|
|
|
561
710
|
if not isinstance(entry, dict):
|
|
562
711
|
rejects.append(line)
|
|
563
712
|
continue
|
|
564
|
-
|
|
713
|
+
|
|
565
714
|
source = entry.get("source", "")
|
|
566
715
|
title = entry.get("title", "")
|
|
567
716
|
h = hashlib.sha256((source + ":" + title).encode()).hexdigest()
|
|
568
|
-
|
|
717
|
+
|
|
569
718
|
if h not in existing_hashes:
|
|
570
719
|
item = create_tracker_item(entry)
|
|
571
720
|
created.append(item)
|
|
@@ -574,13 +723,13 @@ def drain_tracker_inbox():
|
|
|
574
723
|
rejects.append(line)
|
|
575
724
|
except Exception as e:
|
|
576
725
|
rejects.append(line + " # " + str(e))
|
|
577
|
-
|
|
726
|
+
|
|
578
727
|
if rejects:
|
|
579
728
|
rejects_file = inbox_file.with_name(".tracker-inbox.rejects")
|
|
580
729
|
rejects_file.write_text("\n".join(rejects) + "\n", encoding='utf-8')
|
|
581
|
-
|
|
582
|
-
|
|
730
|
+
|
|
731
|
+
processing_file.unlink()
|
|
583
732
|
except Exception as e:
|
|
584
733
|
print(f"[inbox] Drain error: {e}", file=sys.stderr)
|
|
585
|
-
|
|
734
|
+
|
|
586
735
|
return created
|
package/ui/config.py
CHANGED
|
@@ -28,6 +28,7 @@ def reload():
|
|
|
28
28
|
global WATCHDOG_HEARTBEAT, MONITOR_HEARTBEAT, REPOS_JSON, BACKUP_LOG
|
|
29
29
|
global ALERTS_LOG, INBOX_FILE, AUDIT_BACKLOG_FILE
|
|
30
30
|
global UI_SESSION_TOKEN_FILE, TRACKER_FILE, ORCH_STATUS_FILE
|
|
31
|
+
global WEB_DIST, LEDGER_FILE
|
|
31
32
|
global COLLECTOR_INTERVAL, SSE_KEEPALIVE_SECONDS, SSE_MAX_CLIENTS, SSE_QUEUE_MAXSIZE, SSE_WRITE_TIMEOUT
|
|
32
33
|
|
|
33
34
|
# PORT: env PORT > default 8770
|
|
@@ -75,6 +76,12 @@ def reload():
|
|
|
75
76
|
TRACKER_FILE = STATE_DIR / "tracker.json"
|
|
76
77
|
ORCH_STATUS_FILE = STATE_DIR / "orchestrator-status.json"
|
|
77
78
|
|
|
79
|
+
# Wave-14 dashboard rewrite (plan D3): built frontend + cost ledger paths.
|
|
80
|
+
# WEB_DIST: the committed Vite build output served at / and /assets/*.
|
|
81
|
+
WEB_DIST = AESOP_ROOT / "ui" / "web" / "dist"
|
|
82
|
+
# LEDGER_FILE: outcomes ledger parsed by ui/cost.py; sse.py mtime-gates on it.
|
|
83
|
+
LEDGER_FILE = STATE_DIR / "ledger" / "OUTCOMES-LEDGER.md"
|
|
84
|
+
|
|
78
85
|
# Collector and SSE configuration
|
|
79
86
|
COLLECTOR_INTERVAL = float(os.getenv("AESOP_UI_COLLECT_INTERVAL", "1.0"))
|
|
80
87
|
SSE_KEEPALIVE_SECONDS = 15
|
|
@@ -104,6 +111,8 @@ AUDIT_BACKLOG_FILE = AESOP_ROOT / "AUDIT-BACKLOG.md"
|
|
|
104
111
|
UI_SESSION_TOKEN_FILE = STATE_DIR / ".ui-session-token"
|
|
105
112
|
TRACKER_FILE = STATE_DIR / "tracker.json"
|
|
106
113
|
ORCH_STATUS_FILE = STATE_DIR / "orchestrator-status.json"
|
|
114
|
+
WEB_DIST = AESOP_ROOT / "ui" / "web" / "dist"
|
|
115
|
+
LEDGER_FILE = STATE_DIR / "ledger" / "OUTCOMES-LEDGER.md"
|
|
107
116
|
COLLECTOR_INTERVAL = 1.0
|
|
108
117
|
SSE_KEEPALIVE_SECONDS = 15
|
|
109
118
|
SSE_MAX_CLIENTS = 100
|