@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
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared test fixtures + the data-testid naming contract.
|
|
3
|
+
*
|
|
4
|
+
* TESTIDS is the SINGLE SOURCE OF TRUTH for data-testid values across the app.
|
|
5
|
+
* U4–U7 components MUST attach these testids; U8's Playwright proofs assert
|
|
6
|
+
* ONLY via these hooks (never CSS internals). Add new ids here first, then
|
|
7
|
+
* use them from components — never inline a bare testid string.
|
|
8
|
+
*
|
|
9
|
+
* Fixture objects are realistic samples of every API payload type in
|
|
10
|
+
* src/lib/types.ts. U4–U7 import these for component tests.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
Agent,
|
|
15
|
+
AgentDetail,
|
|
16
|
+
Alert,
|
|
17
|
+
AuditBacklog,
|
|
18
|
+
CostSummary,
|
|
19
|
+
DashboardData,
|
|
20
|
+
FullState,
|
|
21
|
+
HeartbeatStatus,
|
|
22
|
+
Message,
|
|
23
|
+
OrchestratorStatus,
|
|
24
|
+
RepoStatus,
|
|
25
|
+
TrackerItem,
|
|
26
|
+
TrackerSnapshot,
|
|
27
|
+
} from '../lib/types';
|
|
28
|
+
|
|
29
|
+
/* ------------------------------------------------------------------ */
|
|
30
|
+
/* data-testid contract */
|
|
31
|
+
/* ------------------------------------------------------------------ */
|
|
32
|
+
|
|
33
|
+
export const TESTIDS = {
|
|
34
|
+
// Health header (sticky, always visible)
|
|
35
|
+
healthHeader: 'health-header',
|
|
36
|
+
healthWatchdog: 'health-watchdog',
|
|
37
|
+
healthMonitor: 'health-monitor',
|
|
38
|
+
healthOrchestrator: 'health-orchestrator',
|
|
39
|
+
healthAgentsCount: 'health-agents-count',
|
|
40
|
+
healthAlertsCount: 'health-alerts-count',
|
|
41
|
+
sseStatus: 'sse-status',
|
|
42
|
+
themeToggle: 'theme-toggle',
|
|
43
|
+
refreshButton: 'refresh-button',
|
|
44
|
+
|
|
45
|
+
// Overview view
|
|
46
|
+
viewOverview: 'view-overview',
|
|
47
|
+
agentRow: 'agent-row',
|
|
48
|
+
agentRowDetail: 'agent-row-detail',
|
|
49
|
+
alertLine: 'alert-line',
|
|
50
|
+
alertsPanel: 'alerts-panel',
|
|
51
|
+
eventsFeed: 'events-feed',
|
|
52
|
+
reposPanel: 'repos-panel',
|
|
53
|
+
inboxForm: 'inbox-form',
|
|
54
|
+
inboxInput: 'inbox-input',
|
|
55
|
+
inboxSubmit: 'inbox-submit',
|
|
56
|
+
|
|
57
|
+
// Work view
|
|
58
|
+
viewWork: 'view-work',
|
|
59
|
+
trackerBoard: 'tracker-board',
|
|
60
|
+
trackerLane: 'tracker-lane',
|
|
61
|
+
trackerCard: 'tracker-card',
|
|
62
|
+
trackerForm: 'tracker-form',
|
|
63
|
+
trackerFormTitle: 'tracker-form-title',
|
|
64
|
+
trackerFormSubmit: 'tracker-form-submit',
|
|
65
|
+
backlogPanel: 'backlog-panel',
|
|
66
|
+
|
|
67
|
+
// Activity view
|
|
68
|
+
viewActivity: 'view-activity',
|
|
69
|
+
timeline: 'timeline',
|
|
70
|
+
timelineBar: 'timeline-bar',
|
|
71
|
+
messagesTail: 'messages-tail',
|
|
72
|
+
messagesFollowToggle: 'messages-follow-toggle',
|
|
73
|
+
|
|
74
|
+
// Cost view
|
|
75
|
+
viewCost: 'view-cost',
|
|
76
|
+
costTable: 'cost-table',
|
|
77
|
+
costChart: 'cost-chart',
|
|
78
|
+
scorecard: 'scorecard',
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
81
|
+
export type TestId = (typeof TESTIDS)[keyof typeof TESTIDS];
|
|
82
|
+
|
|
83
|
+
/* ------------------------------------------------------------------ */
|
|
84
|
+
/* Fixtures */
|
|
85
|
+
/* ------------------------------------------------------------------ */
|
|
86
|
+
|
|
87
|
+
export const fixtureWatchdog: HeartbeatStatus = {
|
|
88
|
+
alive: 'ALIVE',
|
|
89
|
+
age: 3,
|
|
90
|
+
threshold: 300,
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const fixtureWatchdogStale: HeartbeatStatus = {
|
|
94
|
+
alive: 'STALE',
|
|
95
|
+
age: 642,
|
|
96
|
+
threshold: 300,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export const fixtureMonitor: HeartbeatStatus = {
|
|
100
|
+
alive: 'ALIVE',
|
|
101
|
+
age: 45,
|
|
102
|
+
threshold: 3600,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export const fixtureMonitorNotRunning: HeartbeatStatus = {
|
|
106
|
+
alive: 'not running',
|
|
107
|
+
age: -1,
|
|
108
|
+
threshold: 3600,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const fixtureAgents: Agent[] = [
|
|
112
|
+
{
|
|
113
|
+
id: 'a77b995bcdb95',
|
|
114
|
+
project: 'aesop',
|
|
115
|
+
status: 'running',
|
|
116
|
+
age_s: 12,
|
|
117
|
+
hint: 'wave-14 U4 overview components',
|
|
118
|
+
startedAt: '2026-07-13T14:02:11.000Z',
|
|
119
|
+
lastActivity: '2026-07-13T14:31:47.000Z',
|
|
120
|
+
runtimeSeconds: 1776,
|
|
121
|
+
tokensUsed: 48213,
|
|
122
|
+
taskLabel: 'Wave-14 unit U4 (overview view components) for aesop.',
|
|
123
|
+
promptFull: 'Wave-14 unit U4 (overview view components) for aesop. Read the plan first.',
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: 'b12c4d99ef012',
|
|
127
|
+
project: 'aesop',
|
|
128
|
+
status: 'idle',
|
|
129
|
+
age_s: 341,
|
|
130
|
+
hint: 'tracker lane bucketing tests',
|
|
131
|
+
startedAt: '2026-07-13T13:40:00.000Z',
|
|
132
|
+
lastActivity: '2026-07-13T14:26:02.000Z',
|
|
133
|
+
runtimeSeconds: 2762,
|
|
134
|
+
tokensUsed: 102455,
|
|
135
|
+
taskLabel: 'Wave-14 unit U5 (work view components) for aesop.',
|
|
136
|
+
promptFull: 'Wave-14 unit U5 (work view components) for aesop.',
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
id: 'c99ff00aa1122',
|
|
140
|
+
project: 'tr-sample-tracker',
|
|
141
|
+
status: 'SUSPICIOUS',
|
|
142
|
+
age_s: 45,
|
|
143
|
+
hint: 'unexpected file write outside worktree',
|
|
144
|
+
startedAt: '2026-07-13T14:20:00.000Z',
|
|
145
|
+
lastActivity: '2026-07-13T14:31:15.000Z',
|
|
146
|
+
runtimeSeconds: 675,
|
|
147
|
+
tokensUsed: 8102,
|
|
148
|
+
taskLabel: 'Fix flaky test in sample tracker suite.',
|
|
149
|
+
promptFull: 'Fix flaky test in sample tracker suite.',
|
|
150
|
+
},
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
export const fixtureAgentDetail: AgentDetail = {
|
|
154
|
+
id: 'a77b995bcdb95',
|
|
155
|
+
dispatch_prompt:
|
|
156
|
+
'Wave-14 unit U4 (overview view components) for aesop. Read the plan FIRST, then implement AgentsPanel, AlertsPanel, EventsFeed with tests.',
|
|
157
|
+
dispatcher: 'main thread',
|
|
158
|
+
model: 'claude-haiku-4-5-20251001',
|
|
159
|
+
message_count: 87,
|
|
160
|
+
first_seen: 1783346531,
|
|
161
|
+
last_activity: 1783348307,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
export const fixtureAlerts: Alert = {
|
|
165
|
+
count: 2,
|
|
166
|
+
lines: [
|
|
167
|
+
'2026-07-13T09:11:02Z HIGH agent-c99ff00 wrote outside its worktree: C:/Users/matt8/aesop/ui/serve.py',
|
|
168
|
+
'2026-07-13T11:45:38Z MED agent-b12c4d9 3 consecutive test failures on feat/wave14-u5-work',
|
|
169
|
+
],
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export const fixtureAlertsEmpty: Alert = { count: 0, lines: [] };
|
|
173
|
+
|
|
174
|
+
export const fixtureRepos: RepoStatus[] = [
|
|
175
|
+
{ repo: 'aesop', state: 'clean' },
|
|
176
|
+
{ repo: 'tr-sample-tracker', state: 'dirty: 3 files' },
|
|
177
|
+
{ repo: 'ecm-ai', state: 'clean' },
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
export const fixtureEvents: string[] = [
|
|
181
|
+
'2026-07-13 14:00:01 BACKUP aesop -> bundle OK (12.4 MB)',
|
|
182
|
+
'2026-07-13 14:00:04 BACKUP tr-sample-tracker -> bundle OK (3.1 MB)',
|
|
183
|
+
'2026-07-13 14:05:00 SCAN secret_scan --staged exit 0',
|
|
184
|
+
'2026-07-13 14:10:22 PUSH aesop feat/wave14-u1-foundation OK',
|
|
185
|
+
];
|
|
186
|
+
|
|
187
|
+
export const fixtureMessages: Message[] = [
|
|
188
|
+
{
|
|
189
|
+
role: 'user',
|
|
190
|
+
text: 'Run wave 14: dashboard rewrite, start with the foundation unit.',
|
|
191
|
+
timestamp: '2026-07-13T14:00:12.000Z',
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
role: 'assistant',
|
|
195
|
+
text: 'Dispatching U1 (foundation) to a worktree agent; U3 cost collector runs in parallel.',
|
|
196
|
+
timestamp: '2026-07-13T14:01:03.000Z',
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
role: 'assistant',
|
|
200
|
+
text: 'U1 scaffold complete: vite build green, vitest 41 passing. Moving to fixtures.',
|
|
201
|
+
timestamp: '2026-07-13T14:29:44.000Z',
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
|
|
205
|
+
export const fixtureDashboardData: DashboardData = {
|
|
206
|
+
watchdog: fixtureWatchdog,
|
|
207
|
+
monitor: fixtureMonitor,
|
|
208
|
+
agents: fixtureAgents,
|
|
209
|
+
repos: fixtureRepos,
|
|
210
|
+
events: fixtureEvents,
|
|
211
|
+
alerts: fixtureAlerts,
|
|
212
|
+
messages: fixtureMessages,
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
export const fixtureBacklog: AuditBacklog = {
|
|
216
|
+
tiers: [
|
|
217
|
+
{
|
|
218
|
+
tier: 'P0',
|
|
219
|
+
items: [
|
|
220
|
+
{ status: '✅', tag: '[sec]', title: 'Origin fail-closed on /api/session' },
|
|
221
|
+
{ status: '🔵', tag: '[ui]', title: 'Dashboard rewrite foundation (U1)' },
|
|
222
|
+
{ status: '⬜', tag: '[ui]', title: 'Cutover / to dist index (U9)' },
|
|
223
|
+
],
|
|
224
|
+
done: 1,
|
|
225
|
+
inflight: 1,
|
|
226
|
+
todo: 1,
|
|
227
|
+
total: 3,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
tier: 'P1',
|
|
231
|
+
items: [
|
|
232
|
+
{ status: '⬜', tag: '[perf]', title: 'SSE keepalive tuning' },
|
|
233
|
+
{ status: '⏸', tag: '[arch]', title: 'Hierarchical orchestration seams' },
|
|
234
|
+
],
|
|
235
|
+
done: 0,
|
|
236
|
+
inflight: 0,
|
|
237
|
+
todo: 1,
|
|
238
|
+
total: 2,
|
|
239
|
+
},
|
|
240
|
+
],
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const fixtureTrackerItems: TrackerItem[] = [
|
|
244
|
+
{
|
|
245
|
+
id: '3f9a1b2c4d5e',
|
|
246
|
+
title: 'Dashboard rewrite: foundation scaffold',
|
|
247
|
+
priority: 'P0',
|
|
248
|
+
status: 'in-progress',
|
|
249
|
+
lane: 'in-progress',
|
|
250
|
+
source: 'wave14-plan',
|
|
251
|
+
tags: ['ui', 'wave-14'],
|
|
252
|
+
notes: 'U1 trunk unit; U4-U7 build on its types/fixtures/shell.',
|
|
253
|
+
pr_link: 'https://github.com/matt82198/aesop/pull/113',
|
|
254
|
+
created_at: '2026-07-12T18:30:00Z',
|
|
255
|
+
completed_at: null,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
id: '8c7d6e5f4a3b',
|
|
259
|
+
title: 'Cost collector parses OUTCOMES-LEDGER.md',
|
|
260
|
+
priority: 'P1',
|
|
261
|
+
status: 'done',
|
|
262
|
+
lane: 'done',
|
|
263
|
+
source: 'wave14-plan',
|
|
264
|
+
tags: ['cost'],
|
|
265
|
+
notes: null,
|
|
266
|
+
pr_link: null,
|
|
267
|
+
created_at: '2026-07-12T18:31:00Z',
|
|
268
|
+
completed_at: '2026-07-13T09:15:00Z',
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
id: '1a2b3c4d5e6f',
|
|
272
|
+
title: 'Agent timeline read-only v1',
|
|
273
|
+
priority: 'P2',
|
|
274
|
+
status: 'todo',
|
|
275
|
+
lane: 'ranked',
|
|
276
|
+
source: 'audit-backlog-migration',
|
|
277
|
+
tags: ['activity'],
|
|
278
|
+
notes: null,
|
|
279
|
+
pr_link: null,
|
|
280
|
+
created_at: '2026-07-11T10:00:00Z',
|
|
281
|
+
completed_at: null,
|
|
282
|
+
},
|
|
283
|
+
{
|
|
284
|
+
id: '9e8d7c6b5a40',
|
|
285
|
+
title: 'Replay slider for agent timeline',
|
|
286
|
+
priority: 'P2',
|
|
287
|
+
status: 'todo',
|
|
288
|
+
lane: 'proposed',
|
|
289
|
+
source: 'ideation',
|
|
290
|
+
tags: ['activity', 'later'],
|
|
291
|
+
notes: 'v2 — read-only timeline ships first.',
|
|
292
|
+
pr_link: null,
|
|
293
|
+
created_at: '2026-07-11T10:05:00Z',
|
|
294
|
+
completed_at: null,
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
id: 'aa11bb22cc33',
|
|
298
|
+
title: 'Old dashboard patch attempt (superseded)',
|
|
299
|
+
priority: 'P2',
|
|
300
|
+
status: 'archived',
|
|
301
|
+
lane: 'done',
|
|
302
|
+
source: 'manual',
|
|
303
|
+
tags: [],
|
|
304
|
+
notes: null,
|
|
305
|
+
pr_link: 'javascript:alert(1)', // deliberately hostile: must render inert via sanitizeUrl
|
|
306
|
+
created_at: '2026-07-01T08:00:00Z',
|
|
307
|
+
completed_at: '2026-07-02T08:00:00Z',
|
|
308
|
+
},
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
export const fixtureTracker: TrackerSnapshot = { items: fixtureTrackerItems };
|
|
312
|
+
|
|
313
|
+
export const fixtureStatus: OrchestratorStatus = {
|
|
314
|
+
orchestrators: [
|
|
315
|
+
{
|
|
316
|
+
id: 'main',
|
|
317
|
+
role: 'orchestrator',
|
|
318
|
+
age_seconds: 42,
|
|
319
|
+
stale: false,
|
|
320
|
+
updated_at: '2026-07-13T14:31:00Z',
|
|
321
|
+
},
|
|
322
|
+
],
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
export const fixtureCost: CostSummary = {
|
|
326
|
+
models: {
|
|
327
|
+
'claude-haiku-4-5-20251001': {
|
|
328
|
+
runs: 128,
|
|
329
|
+
tokens_in: 2140050,
|
|
330
|
+
tokens_out: 512300,
|
|
331
|
+
verdicts: { OK: 119, FAILED: 6, EMPTY: 2, HUNG: 1 },
|
|
332
|
+
},
|
|
333
|
+
'claude-sonnet-4-5-20250929': {
|
|
334
|
+
runs: 14,
|
|
335
|
+
tokens_in: 890120,
|
|
336
|
+
tokens_out: 210440,
|
|
337
|
+
verdicts: { OK: 13, FAILED: 1, EMPTY: 0, HUNG: 0 },
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
daily_totals: {
|
|
341
|
+
'2026-07-11': { tokens_in: 1204000, tokens_out: 280100 },
|
|
342
|
+
'2026-07-12': { tokens_in: 986170, tokens_out: 262300 },
|
|
343
|
+
'2026-07-13': { tokens_in: 840000, tokens_out: 180340 },
|
|
344
|
+
},
|
|
345
|
+
overall_scorecard: {
|
|
346
|
+
total_runs: 142,
|
|
347
|
+
ok_count: 132,
|
|
348
|
+
failed_count: 7,
|
|
349
|
+
empty_count: 2,
|
|
350
|
+
hung_count: 1,
|
|
351
|
+
ok_rate: 0.9296,
|
|
352
|
+
failed_rate: 0.0493,
|
|
353
|
+
empty_rate: 0.0141,
|
|
354
|
+
hung_rate: 0.007,
|
|
355
|
+
},
|
|
356
|
+
skipped_lines: 3,
|
|
357
|
+
has_pricing: false,
|
|
358
|
+
estimates_by_model: {},
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export const fixtureCostWithPricing: CostSummary = {
|
|
362
|
+
...fixtureCost,
|
|
363
|
+
has_pricing: true,
|
|
364
|
+
estimates_by_model: {
|
|
365
|
+
'claude-haiku-4-5-20251001': {
|
|
366
|
+
input_cost: 2.14,
|
|
367
|
+
output_cost: 2.05,
|
|
368
|
+
total_cost: 4.19,
|
|
369
|
+
},
|
|
370
|
+
'claude-sonnet-4-5-20250929': {
|
|
371
|
+
input_cost: 2.67,
|
|
372
|
+
output_cost: 3.16,
|
|
373
|
+
total_cost: 5.83,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
export const fixtureFullState: FullState = {
|
|
379
|
+
data: fixtureDashboardData,
|
|
380
|
+
backlog: fixtureBacklog,
|
|
381
|
+
agents: fixtureAgents,
|
|
382
|
+
tracker: fixtureTracker,
|
|
383
|
+
status: fixtureStatus,
|
|
384
|
+
cost: fixtureCost,
|
|
385
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest setup — jsdom environment extensions shared by all test files.
|
|
3
|
+
*/
|
|
4
|
+
import '@testing-library/jest-dom/vitest';
|
|
5
|
+
import { afterEach } from 'vitest';
|
|
6
|
+
import { cleanup } from '@testing-library/react';
|
|
7
|
+
|
|
8
|
+
// Unmount React trees between tests.
|
|
9
|
+
afterEach(() => {
|
|
10
|
+
cleanup();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// jsdom does not implement EventSource; give tests a minimal stub so code
|
|
14
|
+
// under test (useSSE) can construct one without exploding. Tests that need
|
|
15
|
+
// to drive SSE events should replace this with their own mock.
|
|
16
|
+
if (typeof (globalThis as any).EventSource === 'undefined') {
|
|
17
|
+
class StubEventSource {
|
|
18
|
+
url: string;
|
|
19
|
+
readyState = 0;
|
|
20
|
+
onerror: ((ev: unknown) => void) | null = null;
|
|
21
|
+
onmessage: ((ev: unknown) => void) | null = null;
|
|
22
|
+
onopen: ((ev: unknown) => void) | null = null;
|
|
23
|
+
private listeners = new Map<string, Set<(ev: MessageEvent) => void>>();
|
|
24
|
+
|
|
25
|
+
constructor(url: string) {
|
|
26
|
+
this.url = url;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
addEventListener(type: string, cb: (ev: MessageEvent) => void) {
|
|
30
|
+
if (!this.listeners.has(type)) this.listeners.set(type, new Set());
|
|
31
|
+
this.listeners.get(type)!.add(cb);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
removeEventListener(type: string, cb: (ev: MessageEvent) => void) {
|
|
35
|
+
this.listeners.get(type)?.delete(cb);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Test helper: dispatch a named SSE event with a JSON payload. */
|
|
39
|
+
emit(type: string, data: string) {
|
|
40
|
+
const ev = new MessageEvent(type, { data });
|
|
41
|
+
this.listeners.get(type)?.forEach((cb) => cb(ev));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
close() {
|
|
45
|
+
this.readyState = 2;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
(globalThis as any).EventSource = StubEventSource;
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity view layout — timeline + messages tail stacked vertically.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
.container {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: var(--space-6);
|
|
9
|
+
height: 100%;
|
|
10
|
+
overflow-y: auto;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.section {
|
|
14
|
+
flex: 0 1 auto;
|
|
15
|
+
min-height: 200px;
|
|
16
|
+
display: flex;
|
|
17
|
+
flex-direction: column;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.section:first-child {
|
|
21
|
+
flex: 0 1 40%;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.section:last-child {
|
|
25
|
+
flex: 1 1 60%;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* Responsive: allow more height for each section on small screens */
|
|
29
|
+
@media (max-width: 768px) {
|
|
30
|
+
.container {
|
|
31
|
+
gap: var(--space-4);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.section:first-child {
|
|
35
|
+
flex: 0 1 auto;
|
|
36
|
+
min-height: auto;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.section:last-child {
|
|
40
|
+
flex: 1 1 auto;
|
|
41
|
+
min-height: 250px;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity view — agent timeline + main-thread messages tail.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { render, screen } from '@testing-library/react';
|
|
7
|
+
import Activity from './Activity';
|
|
8
|
+
import { fixtureAgents, fixtureMessages } from '../test/fixtures';
|
|
9
|
+
import { TESTIDS } from '../test/fixtures';
|
|
10
|
+
import type { FullState } from '../lib/types';
|
|
11
|
+
|
|
12
|
+
describe('Activity view', () => {
|
|
13
|
+
const fixtureState: FullState = {
|
|
14
|
+
data: {
|
|
15
|
+
watchdog: { alive: 'ALIVE', age: 3, threshold: 300 },
|
|
16
|
+
monitor: { alive: 'ALIVE', age: 45, threshold: 3600 },
|
|
17
|
+
agents: fixtureAgents,
|
|
18
|
+
repos: [],
|
|
19
|
+
events: [],
|
|
20
|
+
alerts: { count: 0, lines: [] },
|
|
21
|
+
messages: fixtureMessages,
|
|
22
|
+
},
|
|
23
|
+
backlog: { tiers: [] },
|
|
24
|
+
agents: fixtureAgents,
|
|
25
|
+
tracker: { items: [] },
|
|
26
|
+
status: { orchestrators: [] },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
it('renders activity view container with testid', () => {
|
|
30
|
+
render(<Activity state={fixtureState} />);
|
|
31
|
+
expect(screen.getByTestId(TESTIDS.viewActivity)).toBeInTheDocument();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('renders timeline component with agents', () => {
|
|
35
|
+
render(<Activity state={fixtureState} />);
|
|
36
|
+
expect(screen.getByTestId(TESTIDS.timeline)).toBeInTheDocument();
|
|
37
|
+
// Timeline should render agent ids
|
|
38
|
+
fixtureAgents.forEach((agent) => {
|
|
39
|
+
expect(screen.getByText(agent.id)).toBeInTheDocument();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('renders messages tail component', () => {
|
|
44
|
+
render(<Activity state={fixtureState} />);
|
|
45
|
+
expect(screen.getByTestId(TESTIDS.messagesTail)).toBeInTheDocument();
|
|
46
|
+
// Should render messages
|
|
47
|
+
expect(screen.getByText(/Run wave 14/)).toBeInTheDocument();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('shows empty state when no agents or messages', () => {
|
|
51
|
+
const emptyState: FullState = {
|
|
52
|
+
...fixtureState,
|
|
53
|
+
agents: [],
|
|
54
|
+
data: {
|
|
55
|
+
...fixtureState.data,
|
|
56
|
+
agents: [],
|
|
57
|
+
messages: [],
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
render(<Activity state={emptyState} />);
|
|
61
|
+
const timeline = screen.getByTestId(TESTIDS.timeline);
|
|
62
|
+
expect(timeline.textContent).toContain('no agents');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('handles partial data gracefully (no messages)', () => {
|
|
66
|
+
const partialState: FullState = {
|
|
67
|
+
...fixtureState,
|
|
68
|
+
data: {
|
|
69
|
+
...fixtureState.data,
|
|
70
|
+
messages: [],
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
render(<Activity state={partialState} />);
|
|
74
|
+
expect(screen.getByTestId(TESTIDS.viewActivity)).toBeInTheDocument();
|
|
75
|
+
const messagesTail = screen.getByTestId(TESTIDS.messagesTail);
|
|
76
|
+
expect(messagesTail.textContent).toContain('no messages');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('renders timeline above messages (visual hierarchy)', () => {
|
|
80
|
+
render(<Activity state={fixtureState} />);
|
|
81
|
+
const timeline = screen.getByTestId(TESTIDS.timeline);
|
|
82
|
+
const messagesTail = screen.getByTestId(TESTIDS.messagesTail);
|
|
83
|
+
|
|
84
|
+
// Timeline should appear before messages in DOM (by finding their positions)
|
|
85
|
+
const timelinePosition = timeline.compareDocumentPosition(messagesTail);
|
|
86
|
+
// DOCUMENT_POSITION_FOLLOWING = 4, meaning timeline comes before messages
|
|
87
|
+
expect(timelinePosition & 4).toBe(4);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity view — Agent timeline + main-thread messages tail.
|
|
3
|
+
* Read-only observability: shows agent execution spans and orchestrator reasoning.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Timeline from '../components/Timeline';
|
|
7
|
+
import MessagesTail from '../components/MessagesTail';
|
|
8
|
+
import { TESTIDS } from '../test/fixtures';
|
|
9
|
+
import type { SSEState } from '../lib/useSSE';
|
|
10
|
+
import styles from './Activity.module.css';
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
state: Pick<SSEState, 'agents' | 'data'>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function Activity({ state }: Props) {
|
|
17
|
+
const agents = state.agents || [];
|
|
18
|
+
const messages = state.data?.messages || [];
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div data-testid={TESTIDS.viewActivity} className={styles.container}>
|
|
22
|
+
<section className={styles.section}>
|
|
23
|
+
<Timeline agents={agents} />
|
|
24
|
+
</section>
|
|
25
|
+
|
|
26
|
+
<section className={styles.section}>
|
|
27
|
+
<MessagesTail messages={messages} />
|
|
28
|
+
</section>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/* Cost view — layout for table, chart, scorecard */
|
|
2
|
+
|
|
3
|
+
.view-cost {
|
|
4
|
+
padding: var(--space-5);
|
|
5
|
+
max-width: var(--content-max-width);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.view-cost h2 {
|
|
9
|
+
margin-top: 0;
|
|
10
|
+
margin-bottom: var(--space-5);
|
|
11
|
+
font-size: var(--font-size-xl);
|
|
12
|
+
color: var(--color-text);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.view-cost h3 {
|
|
16
|
+
font-size: var(--font-size-lg);
|
|
17
|
+
color: var(--color-text);
|
|
18
|
+
margin-top: var(--space-5);
|
|
19
|
+
margin-bottom: var(--space-3);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* Configure pricing callout */
|
|
23
|
+
.cost-callout {
|
|
24
|
+
padding: var(--space-4);
|
|
25
|
+
border-left: 4px solid var(--color-accent);
|
|
26
|
+
border-radius: var(--radius-md);
|
|
27
|
+
background: var(--color-bg-subtle);
|
|
28
|
+
margin-bottom: var(--space-5);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.cost-callout h3 {
|
|
32
|
+
margin-top: 0;
|
|
33
|
+
margin-bottom: var(--space-2);
|
|
34
|
+
font-size: var(--font-size-md);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.cost-callout p {
|
|
38
|
+
margin: 0;
|
|
39
|
+
color: var(--color-text);
|
|
40
|
+
font-size: var(--font-size-sm);
|
|
41
|
+
line-height: var(--line-height-normal);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.cost-callout code {
|
|
45
|
+
background: var(--color-bg-inset);
|
|
46
|
+
padding: 0.2em 0.4em;
|
|
47
|
+
border-radius: 3px;
|
|
48
|
+
font-family: var(--font-mono);
|
|
49
|
+
font-size: 0.9em;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.cost-callout--info {
|
|
53
|
+
border-left-color: var(--color-status-info);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/* Layout for the three sections */
|
|
57
|
+
.cost-layout {
|
|
58
|
+
display: grid;
|
|
59
|
+
gap: var(--space-5);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.cost-section {
|
|
63
|
+
border: 1px solid var(--color-border);
|
|
64
|
+
border-radius: var(--radius-md);
|
|
65
|
+
padding: var(--space-4);
|
|
66
|
+
background: var(--color-surface);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.cost-section h3 {
|
|
70
|
+
margin-top: 0;
|
|
71
|
+
margin-bottom: var(--space-3);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* Responsive: stack sections vertically on smaller screens */
|
|
75
|
+
@media (max-width: 768px) {
|
|
76
|
+
.view-cost {
|
|
77
|
+
padding: var(--space-3);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.cost-layout {
|
|
81
|
+
gap: var(--space-4);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.cost-section {
|
|
85
|
+
padding: var(--space-3);
|
|
86
|
+
}
|
|
87
|
+
}
|