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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. package/CHANGELOG.md +79 -95
  2. package/CLAUDE-TEMPLATE.md +115 -0
  3. package/README.md +126 -245
  4. package/aesop.config.example.json +34 -26
  5. package/bin/CLAUDE.md +67 -0
  6. package/bin/cli.js +593 -36
  7. package/daemons/CLAUDE.md +36 -0
  8. package/daemons/backup-fleet.sh +92 -21
  9. package/daemons/run-watchdog.sh +95 -12
  10. package/dash/CLAUDE.md +32 -0
  11. package/dash/dash-extra.mjs +194 -21
  12. package/dash/watchdog-gui.sh +23 -6
  13. package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
  14. package/docs/CHECKPOINTING.md +100 -0
  15. package/docs/DISPATCH-MODEL.md +11 -7
  16. package/docs/FORENSICS.md +204 -0
  17. package/docs/GOVERNANCE.md +4 -2
  18. package/docs/HOOK-INSTALL.md +307 -0
  19. package/docs/HOW-THE-LOOP-WORKS.md +175 -0
  20. package/docs/MEMORY-TEMPLATE.md +61 -0
  21. package/docs/README.md +44 -0
  22. package/docs/RELEASING.md +159 -0
  23. package/docs/RELIABILITY.md +61 -0
  24. package/docs/RESTORE.md +397 -0
  25. package/docs/SCRIPTS-POLICY.md +97 -0
  26. package/docs/archive/README.md +3 -0
  27. package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
  28. package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
  29. package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
  30. package/docs/archive/spikes/tiered-cognition/README.md +27 -0
  31. package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
  32. package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
  33. package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
  34. package/docs/case-study-portfolio.md +61 -0
  35. package/docs/self-stats-data.json +11 -0
  36. package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
  37. package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
  38. package/docs/templates/PROPOSALS-LOG.example.md +64 -0
  39. package/hooks/CLAUDE.md +61 -0
  40. package/hooks/claude/force-model-policy.mjs +159 -0
  41. package/hooks/pre-push-policy.sh +753 -0
  42. package/mcp/CLAUDE.md +213 -0
  43. package/mcp/package.json +26 -0
  44. package/mcp/server.mjs +543 -0
  45. package/monitor/.signal-state.json +3 -0
  46. package/monitor/ACTIONS.log +1 -0
  47. package/monitor/BRIEF.md +24 -0
  48. package/monitor/CHARTER.md +75 -88
  49. package/monitor/CLAUDE.md +40 -0
  50. package/monitor/SIGNALS.json +54 -0
  51. package/monitor/collect-signals.mjs +613 -61
  52. package/package.json +15 -3
  53. package/scan/fleet-scan.example.mjs +292 -0
  54. package/skills/CLAUDE.md +3 -0
  55. package/skills/healthcheck/SKILL.md +44 -0
  56. package/skills/power/SKILL.md +161 -0
  57. package/state_store/CLAUDE.md +39 -0
  58. package/state_store/__init__.py +24 -0
  59. package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
  60. package/state_store/__pycache__/api.cpython-314.pyc +0 -0
  61. package/state_store/__pycache__/export.cpython-314.pyc +0 -0
  62. package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
  63. package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
  64. package/state_store/__pycache__/store.cpython-314.pyc +0 -0
  65. package/state_store/api.py +35 -0
  66. package/state_store/export.py +19 -0
  67. package/state_store/ingest.py +24 -0
  68. package/state_store/projections.py +52 -0
  69. package/state_store/store.py +102 -0
  70. package/tools/CLAUDE.md +72 -0
  71. package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
  72. package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
  73. package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
  74. package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
  75. package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
  76. package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
  77. package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
  78. package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
  79. package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
  80. package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
  81. package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
  82. package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
  83. package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
  84. package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
  85. package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
  86. package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
  87. package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
  88. package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
  89. package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
  90. package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
  91. package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
  92. package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
  93. package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
  94. package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
  95. package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
  96. package/tools/agent-forensics.sh +186 -0
  97. package/tools/alert_bridge.py +449 -0
  98. package/tools/buildlog.py +182 -0
  99. package/tools/ci_merge_wait.py +317 -0
  100. package/tools/ensure_state.py +94 -0
  101. package/tools/eod_sweep.py +239 -0
  102. package/tools/fleet_ledger.py +272 -0
  103. package/tools/fleet_prompt_extractor.py +134 -0
  104. package/tools/healthcheck.py +296 -0
  105. package/tools/heartbeat.py +131 -0
  106. package/tools/inbox_drain.py +249 -0
  107. package/tools/launch_tui.py +51 -3
  108. package/tools/lock.mjs +211 -0
  109. package/tools/metrics_gate.py +205 -0
  110. package/tools/orchestrator_status.py +99 -0
  111. package/tools/power_selftest.py +386 -0
  112. package/tools/prepublish_scan.py +84 -0
  113. package/tools/proposals.mjs +248 -0
  114. package/tools/reconstitute.sh +467 -0
  115. package/tools/rotate_logs.py +228 -0
  116. package/tools/scanner_selftest.py +303 -0
  117. package/tools/secret_scan.py +138 -31
  118. package/tools/self_stats.py +509 -0
  119. package/tools/session_usage_summary.py +198 -0
  120. package/tools/stall_check.py +172 -0
  121. package/tools/svg_to_png.mjs +50 -0
  122. package/tools/transcript_replay.py +236 -0
  123. package/tools/transcript_timeline.py +184 -0
  124. package/tools/verify_dash.py +513 -0
  125. package/tools/verify_submit_encoding.py +202 -0
  126. package/ui/CLAUDE.md +125 -0
  127. package/ui/__pycache__/agents.cpython-314.pyc +0 -0
  128. package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
  129. package/ui/__pycache__/config.cpython-314.pyc +0 -0
  130. package/ui/__pycache__/cost.cpython-314.pyc +0 -0
  131. package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
  132. package/ui/__pycache__/handler.cpython-314.pyc +0 -0
  133. package/ui/__pycache__/render.cpython-314.pyc +0 -0
  134. package/ui/__pycache__/serve.cpython-314.pyc +0 -0
  135. package/ui/__pycache__/sse.cpython-314.pyc +0 -0
  136. package/ui/agents.py +183 -0
  137. package/ui/api/__init__.py +84 -0
  138. package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
  139. package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
  140. package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
  141. package/ui/api/submit.py +97 -0
  142. package/ui/api/tracker.py +116 -0
  143. package/ui/collectors.py +735 -0
  144. package/ui/config.py +123 -0
  145. package/ui/cost.py +260 -0
  146. package/ui/csrf.py +144 -0
  147. package/ui/handler.py +676 -0
  148. package/ui/render.py +49 -0
  149. package/ui/serve.py +68 -796
  150. package/ui/sse.py +183 -0
  151. package/ui/web/.gitattributes +13 -0
  152. package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
  153. package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
  154. package/ui/web/dist/index.html +14 -0
  155. package/ui/web/index.html +13 -0
  156. package/ui/web/package-lock.json +2225 -0
  157. package/ui/web/package.json +26 -0
  158. package/ui/web/src/App.test.tsx +74 -0
  159. package/ui/web/src/App.tsx +142 -0
  160. package/ui/web/src/CONTRIBUTING-UI.md +49 -0
  161. package/ui/web/src/components/AgentRow.css +187 -0
  162. package/ui/web/src/components/AgentRow.test.tsx +209 -0
  163. package/ui/web/src/components/AgentRow.tsx +207 -0
  164. package/ui/web/src/components/AgentsPanel.css +108 -0
  165. package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
  166. package/ui/web/src/components/AgentsPanel.tsx +58 -0
  167. package/ui/web/src/components/AlertsPanel.css +88 -0
  168. package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
  169. package/ui/web/src/components/AlertsPanel.tsx +67 -0
  170. package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
  171. package/ui/web/src/components/BacklogPanel.tsx +122 -0
  172. package/ui/web/src/components/CostChart.css +110 -0
  173. package/ui/web/src/components/CostChart.test.tsx +144 -0
  174. package/ui/web/src/components/CostChart.tsx +152 -0
  175. package/ui/web/src/components/CostTable.css +93 -0
  176. package/ui/web/src/components/CostTable.test.tsx +165 -0
  177. package/ui/web/src/components/CostTable.tsx +94 -0
  178. package/ui/web/src/components/EventsFeed.css +68 -0
  179. package/ui/web/src/components/EventsFeed.test.tsx +36 -0
  180. package/ui/web/src/components/EventsFeed.tsx +31 -0
  181. package/ui/web/src/components/HealthHeader.css +137 -0
  182. package/ui/web/src/components/HealthHeader.test.tsx +278 -0
  183. package/ui/web/src/components/HealthHeader.tsx +281 -0
  184. package/ui/web/src/components/InboxForm.css +135 -0
  185. package/ui/web/src/components/InboxForm.test.tsx +208 -0
  186. package/ui/web/src/components/InboxForm.tsx +116 -0
  187. package/ui/web/src/components/MessagesTail.module.css +144 -0
  188. package/ui/web/src/components/MessagesTail.test.tsx +176 -0
  189. package/ui/web/src/components/MessagesTail.tsx +94 -0
  190. package/ui/web/src/components/ReposPanel.css +90 -0
  191. package/ui/web/src/components/ReposPanel.test.tsx +45 -0
  192. package/ui/web/src/components/ReposPanel.tsx +67 -0
  193. package/ui/web/src/components/Scorecard.css +106 -0
  194. package/ui/web/src/components/Scorecard.test.tsx +117 -0
  195. package/ui/web/src/components/Scorecard.tsx +85 -0
  196. package/ui/web/src/components/Timeline.module.css +151 -0
  197. package/ui/web/src/components/Timeline.test.tsx +215 -0
  198. package/ui/web/src/components/Timeline.tsx +99 -0
  199. package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
  200. package/ui/web/src/components/TrackerBoard.tsx +107 -0
  201. package/ui/web/src/components/TrackerCard.test.tsx +180 -0
  202. package/ui/web/src/components/TrackerCard.tsx +160 -0
  203. package/ui/web/src/components/TrackerForm.test.tsx +189 -0
  204. package/ui/web/src/components/TrackerForm.tsx +144 -0
  205. package/ui/web/src/lib/api.ts +218 -0
  206. package/ui/web/src/lib/format.test.ts +89 -0
  207. package/ui/web/src/lib/format.ts +103 -0
  208. package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
  209. package/ui/web/src/lib/sanitizeUrl.ts +38 -0
  210. package/ui/web/src/lib/types.ts +230 -0
  211. package/ui/web/src/lib/useHashRoute.test.ts +60 -0
  212. package/ui/web/src/lib/useHashRoute.ts +23 -0
  213. package/ui/web/src/lib/useSSE.ts +175 -0
  214. package/ui/web/src/main.tsx +10 -0
  215. package/ui/web/src/styles/global.css +179 -0
  216. package/ui/web/src/styles/theme.css +184 -0
  217. package/ui/web/src/styles/work.css +572 -0
  218. package/ui/web/src/test/fixtures.ts +385 -0
  219. package/ui/web/src/test/setup.ts +49 -0
  220. package/ui/web/src/views/Activity.module.css +43 -0
  221. package/ui/web/src/views/Activity.test.tsx +89 -0
  222. package/ui/web/src/views/Activity.tsx +31 -0
  223. package/ui/web/src/views/Cost.css +87 -0
  224. package/ui/web/src/views/Cost.test.tsx +142 -0
  225. package/ui/web/src/views/Cost.tsx +54 -0
  226. package/ui/web/src/views/Overview.css +51 -0
  227. package/ui/web/src/views/Overview.test.tsx +76 -0
  228. package/ui/web/src/views/Overview.tsx +46 -0
  229. package/ui/web/src/views/Work.test.tsx +82 -0
  230. package/ui/web/src/views/Work.tsx +79 -0
  231. package/ui/web/src/vite-env.d.ts +10 -0
  232. package/ui/web/tsconfig.json +22 -0
  233. package/ui/web/vite.config.ts +25 -0
  234. package/ui/web/vitest.config.ts +12 -0
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Cost view tests — composition of CostTable, CostChart, and Scorecard.
3
+ * Tests empty state (has_pricing=false) with configure callout.
4
+ * Tests full state rendering.
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { render, screen } from '@testing-library/react';
9
+ import { Cost } from './Cost';
10
+ import { fixtureCost, fixtureCostWithPricing, TESTIDS } from '../test/fixtures';
11
+
12
+ describe('Cost view', () => {
13
+ it('renders with testid', () => {
14
+ render(<Cost cost={fixtureCost} />);
15
+ expect(screen.getByTestId(TESTIDS.viewCost)).toBeInTheDocument();
16
+ });
17
+
18
+ it('renders cost table, chart, and scorecard', () => {
19
+ render(<Cost cost={fixtureCostWithPricing} />);
20
+ expect(screen.getByTestId(TESTIDS.costTable)).toBeInTheDocument();
21
+ expect(screen.getByTestId(TESTIDS.costChart)).toBeInTheDocument();
22
+ expect(screen.getByTestId(TESTIDS.scorecard)).toBeInTheDocument();
23
+ });
24
+
25
+ describe('tokens-only mode (has_pricing=false)', () => {
26
+ it('shows "configure pricing" empty-state callout', () => {
27
+ render(<Cost cost={fixtureCost} />);
28
+ const view = screen.getByTestId(TESTIDS.viewCost);
29
+ // Should mention configure or pricing
30
+ expect(view.textContent).toMatch(/configure|pricing|aesop\.config/i);
31
+ });
32
+
33
+ it('callout references aesop.config.json', () => {
34
+ render(<Cost cost={fixtureCost} />);
35
+ const view = screen.getByTestId(TESTIDS.viewCost);
36
+ expect(view.textContent).toContain('aesop.config.json');
37
+ });
38
+
39
+ it('provides instructions for pricing configuration', () => {
40
+ render(<Cost cost={fixtureCost} />);
41
+ const view = screen.getByTestId(TESTIDS.viewCost);
42
+ // Should have some instructional text
43
+ expect(view.textContent?.length).toBeGreaterThan(50);
44
+ });
45
+
46
+ it('still renders table and chart even without pricing', () => {
47
+ render(<Cost cost={fixtureCost} />);
48
+ // Should show token data even without pricing
49
+ expect(screen.getByTestId(TESTIDS.costTable)).toBeInTheDocument();
50
+ expect(screen.getByTestId(TESTIDS.costChart)).toBeInTheDocument();
51
+ });
52
+ });
53
+
54
+ describe('pricing mode (has_pricing=true)', () => {
55
+ it('does not show "configure pricing" callout', () => {
56
+ render(<Cost cost={fixtureCostWithPricing} />);
57
+ const view = screen.getByTestId(TESTIDS.viewCost);
58
+ // Should NOT emphasize configuration when pricing exists
59
+ expect(view.textContent).not.toMatch(/configure pricing/i);
60
+ });
61
+
62
+ it('renders cost columns with dollar amounts', () => {
63
+ render(<Cost cost={fixtureCostWithPricing} />);
64
+ const table = screen.getByTestId(TESTIDS.costTable);
65
+ // Should display pricing info
66
+ expect(table.innerHTML).toContain('$');
67
+ });
68
+ });
69
+
70
+ it('renders as a proper section element', () => {
71
+ render(<Cost cost={fixtureCost} />);
72
+ const view = screen.getByTestId(TESTIDS.viewCost) as HTMLElement;
73
+ expect(['SECTION', 'DIV'].includes(view.tagName)).toBe(true);
74
+ });
75
+
76
+ it('has aria-label or heading describing the view', () => {
77
+ render(<Cost cost={fixtureCost} />);
78
+ const view = screen.getByTestId(TESTIDS.viewCost);
79
+ // Should have descriptive content or aria-label
80
+ expect(view.getAttribute('aria-label') || view.textContent).toBeTruthy();
81
+ });
82
+
83
+ it('layout is readable with all three components visible', () => {
84
+ render(<Cost cost={fixtureCostWithPricing} />);
85
+ // Should have all three sections rendered
86
+ expect(screen.getByTestId(TESTIDS.costTable)).toBeInTheDocument();
87
+ expect(screen.getByTestId(TESTIDS.costChart)).toBeInTheDocument();
88
+ expect(screen.getByTestId(TESTIDS.scorecard)).toBeInTheDocument();
89
+ });
90
+
91
+ it('scorecard displays verdict statistics', () => {
92
+ render(<Cost cost={fixtureCost} />);
93
+ // Should show total runs count
94
+ expect(screen.getByTestId(TESTIDS.scorecard).textContent).toContain('142');
95
+ });
96
+
97
+ it('chart shows per-day trend', () => {
98
+ render(<Cost cost={fixtureCost} />);
99
+ const chart = screen.getByTestId(TESTIDS.costChart);
100
+ // Should render 6 bars for the 3 days (2 per day: in/out)
101
+ const bars = chart.querySelectorAll('rect[data-day]');
102
+ expect(bars.length).toBe(6);
103
+ });
104
+
105
+ it('table shows per-model breakdown', () => {
106
+ render(<Cost cost={fixtureCost} />);
107
+ const table = screen.getByTestId(TESTIDS.costTable);
108
+ // Should list haiku and sonnet models
109
+ expect(table.textContent).toContain('haiku');
110
+ expect(table.textContent).toContain('sonnet');
111
+ });
112
+
113
+ it('handles cost object with skipped_lines footnote', () => {
114
+ render(<Cost cost={fixtureCost} />);
115
+ // scorecard mentions skipped lines when > 0
116
+ // At least the component renders without error
117
+ expect(screen.getByTestId(TESTIDS.viewCost)).toBeInTheDocument();
118
+ });
119
+
120
+ it('empty cost (0 runs) still renders all panels', () => {
121
+ const empty = {
122
+ ...fixtureCost,
123
+ models: {},
124
+ daily_totals: {},
125
+ overall_scorecard: {
126
+ total_runs: 0,
127
+ ok_count: 0,
128
+ failed_count: 0,
129
+ empty_count: 0,
130
+ hung_count: 0,
131
+ ok_rate: 0,
132
+ failed_rate: 0,
133
+ empty_rate: 0,
134
+ hung_rate: 0,
135
+ },
136
+ };
137
+ render(<Cost cost={empty} />);
138
+ expect(screen.getByTestId(TESTIDS.viewCost)).toBeInTheDocument();
139
+ expect(screen.getByTestId(TESTIDS.costTable)).toBeInTheDocument();
140
+ expect(screen.getByTestId(TESTIDS.costChart)).toBeInTheDocument();
141
+ });
142
+ });
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Cost view — composition of cost table, chart, and scorecard.
3
+ * Shows per-model breakdowns, per-day trends, and verdict quality metrics.
4
+ * When has_pricing=false, shows a "configure pricing" empty-state callout.
5
+ * When has_pricing=true, displays dollar estimates alongside tokens.
6
+ */
7
+
8
+ import type { CostSummary } from '../lib/types';
9
+ import { CostTable } from '../components/CostTable';
10
+ import { CostChart } from '../components/CostChart';
11
+ import { Scorecard } from '../components/Scorecard';
12
+ import { TESTIDS } from '../test/fixtures';
13
+ import './Cost.css';
14
+
15
+ interface CostProps {
16
+ cost: CostSummary;
17
+ }
18
+
19
+ export function Cost({ cost }: CostProps) {
20
+ return (
21
+ <section className="view-cost" data-testid={TESTIDS.viewCost} aria-label="Cost analytics">
22
+ <h2>Cost Analytics</h2>
23
+
24
+ {!cost.has_pricing && (
25
+ <div className="cost-callout cost-callout--info" role="status">
26
+ <h3>Configure Pricing</h3>
27
+ <p>
28
+ To see cost estimates, add a <code>pricing</code> map to your{' '}
29
+ <code>aesop.config.json</code> with per-model input and output rates (e.g.{' '}
30
+ <code>{'{input: 0.003, output: 0.015}'}</code>). Without pricing, token counts are
31
+ shown; no estimates are computed.
32
+ </p>
33
+ </div>
34
+ )}
35
+
36
+ <div className="cost-layout">
37
+ <div className="cost-section">
38
+ <h3>By Model</h3>
39
+ <CostTable cost={cost} />
40
+ </div>
41
+
42
+ <div className="cost-section">
43
+ <h3>Daily Trend</h3>
44
+ <CostChart cost={cost} />
45
+ </div>
46
+
47
+ <div className="cost-section">
48
+ <h3>Quality Scorecard</h3>
49
+ <Scorecard cost={cost} />
50
+ </div>
51
+ </div>
52
+ </section>
53
+ );
54
+ }
@@ -0,0 +1,51 @@
1
+ .overview {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: var(--space-6);
5
+ padding: var(--space-6);
6
+ max-width: var(--content-max-width);
7
+ margin: 0 auto;
8
+ width: 100%;
9
+ }
10
+
11
+ .overview__section {
12
+ display: flex;
13
+ gap: var(--space-5);
14
+ }
15
+
16
+ .overview__section--full {
17
+ width: 100%;
18
+ }
19
+
20
+ .overview__section--row {
21
+ display: grid;
22
+ grid-template-columns: repeat(3, 1fr);
23
+ gap: var(--space-5);
24
+ }
25
+
26
+ @media (max-width: 1200px) {
27
+ .overview__section--row {
28
+ grid-template-columns: repeat(2, 1fr);
29
+ }
30
+ }
31
+
32
+ @media (max-width: 768px) {
33
+ .overview {
34
+ gap: var(--space-4);
35
+ padding: var(--space-4);
36
+ }
37
+
38
+ .overview__section {
39
+ gap: var(--space-3);
40
+ }
41
+
42
+ .overview__section--row {
43
+ grid-template-columns: 1fr;
44
+ }
45
+ }
46
+
47
+ @media (prefers-reduced-motion: reduce) {
48
+ .overview {
49
+ scroll-behavior: auto;
50
+ }
51
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { Overview } from './Overview';
4
+ import {
5
+ fixtureAgents,
6
+ fixtureAlerts,
7
+ fixtureEvents,
8
+ fixtureRepos,
9
+ TESTIDS,
10
+ } from '../test/fixtures';
11
+
12
+ vi.mock('../components/AgentsPanel', () => ({
13
+ AgentsPanel: () => <div data-testid="agents-panel-mock">AgentsPanel</div>,
14
+ }));
15
+
16
+ vi.mock('../components/AlertsPanel', () => ({
17
+ AlertsPanel: () => <div data-testid="alerts-panel-mock">AlertsPanel</div>,
18
+ }));
19
+
20
+ vi.mock('../components/EventsFeed', () => ({
21
+ EventsFeed: () => <div data-testid="events-feed-mock">EventsFeed</div>,
22
+ }));
23
+
24
+ vi.mock('../components/ReposPanel', () => ({
25
+ ReposPanel: () => <div data-testid="repos-panel-mock">ReposPanel</div>,
26
+ }));
27
+
28
+ vi.mock('../components/InboxForm', () => ({
29
+ InboxForm: () => <div data-testid="inbox-form-mock">InboxForm</div>,
30
+ }));
31
+
32
+ describe('Overview', () => {
33
+ it('renders overview view with all components', () => {
34
+ render(
35
+ <Overview
36
+ agents={fixtureAgents}
37
+ alerts={fixtureAlerts}
38
+ events={fixtureEvents}
39
+ repos={fixtureRepos}
40
+ />
41
+ );
42
+
43
+ expect(screen.getByTestId(TESTIDS.viewOverview)).toBeInTheDocument();
44
+ expect(screen.getByTestId('agents-panel-mock')).toBeInTheDocument();
45
+ expect(screen.getByTestId('alerts-panel-mock')).toBeInTheDocument();
46
+ expect(screen.getByTestId('events-feed-mock')).toBeInTheDocument();
47
+ expect(screen.getByTestId('repos-panel-mock')).toBeInTheDocument();
48
+ expect(screen.getByTestId('inbox-form-mock')).toBeInTheDocument();
49
+ });
50
+
51
+ it('handles null values gracefully', () => {
52
+ render(
53
+ <Overview
54
+ agents={null}
55
+ alerts={null}
56
+ events={null}
57
+ repos={null}
58
+ />
59
+ );
60
+
61
+ expect(screen.getByTestId(TESTIDS.viewOverview)).toBeInTheDocument();
62
+ });
63
+
64
+ it('renders with empty arrays', () => {
65
+ render(
66
+ <Overview
67
+ agents={[]}
68
+ alerts={{ count: 0, lines: [] }}
69
+ events={[]}
70
+ repos={[]}
71
+ />
72
+ );
73
+
74
+ expect(screen.getByTestId(TESTIDS.viewOverview)).toBeInTheDocument();
75
+ });
76
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Overview view — main dashboard combining all overview components.
3
+ *
4
+ * Layout:
5
+ * - Top: AgentsPanel (full width)
6
+ * - Middle row: AlertsPanel | EventsFeed | ReposPanel
7
+ * - Bottom: InboxForm
8
+ *
9
+ * Props passed from App.tsx via SSE state.
10
+ */
11
+
12
+ import type { Agent, Alert } from '../lib/types';
13
+ import { AgentsPanel } from '../components/AgentsPanel';
14
+ import { AlertsPanel } from '../components/AlertsPanel';
15
+ import { EventsFeed } from '../components/EventsFeed';
16
+ import { ReposPanel } from '../components/ReposPanel';
17
+ import { InboxForm } from '../components/InboxForm';
18
+ import { TESTIDS } from '../test/fixtures';
19
+ import './Overview.css';
20
+
21
+ interface OverviewProps {
22
+ agents: Agent[] | null;
23
+ alerts: Alert | null;
24
+ events: string[] | null;
25
+ repos: any[] | null;
26
+ }
27
+
28
+ export function Overview({ agents, alerts, events, repos }: OverviewProps) {
29
+ return (
30
+ <div className="overview" data-testid={TESTIDS.viewOverview}>
31
+ <section className="overview__section overview__section--full">
32
+ <AgentsPanel agents={agents} />
33
+ </section>
34
+
35
+ <section className="overview__section overview__section--row">
36
+ <AlertsPanel alerts={alerts} />
37
+ <EventsFeed events={events} />
38
+ <ReposPanel repos={repos} />
39
+ </section>
40
+
41
+ <section className="overview__section overview__section--full">
42
+ <InboxForm />
43
+ </section>
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { Work } from './Work';
4
+ import { fixtureTrackerItems, fixtureBacklog, TESTIDS } from '../test/fixtures';
5
+
6
+ // Mock useSSE hook
7
+ vi.mock('../lib/useSSE', () => ({
8
+ useSSE: () => ({
9
+ tracker: { items: fixtureTrackerItems },
10
+ backlog: fixtureBacklog,
11
+ data: null,
12
+ agents: null,
13
+ status: null,
14
+ cost: null,
15
+ connectionStatus: { status: 'live' as const },
16
+ }),
17
+ }));
18
+
19
+ describe('Work view', () => {
20
+ it('renders the work view with correct testid', () => {
21
+ render(<Work />);
22
+
23
+ expect(screen.getByTestId(TESTIDS.viewWork)).toBeInTheDocument();
24
+ });
25
+
26
+ it('renders tracker board heading', () => {
27
+ render(<Work />);
28
+
29
+ expect(screen.getByText('Tracker Kanban')).toBeInTheDocument();
30
+ });
31
+
32
+ it('renders the add item button', () => {
33
+ render(<Work />);
34
+
35
+ const addButton = screen.getByRole('button', { name: /Add Item/ });
36
+ expect(addButton).toBeInTheDocument();
37
+ });
38
+
39
+ it('shows form when add item button is clicked', () => {
40
+ render(<Work />);
41
+
42
+ const addButton = screen.getByRole('button', { name: /Add Item/ });
43
+ expect(addButton).toHaveAttribute('aria-expanded', 'false');
44
+
45
+ // Form inputs should not be visible initially
46
+ expect(screen.queryByLabelText('Title')).not.toBeInTheDocument();
47
+ });
48
+
49
+ it('displays tracker board with items', () => {
50
+ render(<Work />);
51
+
52
+ expect(screen.getByTestId(TESTIDS.trackerBoard)).toBeInTheDocument();
53
+ });
54
+
55
+ it('displays backlog panel', () => {
56
+ render(<Work />);
57
+
58
+ expect(screen.getByTestId(TESTIDS.backlogPanel)).toBeInTheDocument();
59
+ });
60
+
61
+ it('renders all tracker lanes', () => {
62
+ render(<Work />);
63
+
64
+ // Be specific to avoid duplicate "Done" matching
65
+ // Lane headers include counts, e.g., "Proposed1", so check by prefix
66
+ const sections = screen.getAllByRole('heading', { level: 2 });
67
+ const laneTexts = sections.map((s) => s.textContent || '');
68
+
69
+ expect(laneTexts.some((t) => t.startsWith('Proposed'))).toBe(true);
70
+ expect(laneTexts.some((t) => t.startsWith('Ranked'))).toBe(true);
71
+ expect(laneTexts.some((t) => t.startsWith('In Progress'))).toBe(true);
72
+ });
73
+
74
+ it('renders backlog audit tier headers', () => {
75
+ render(<Work />);
76
+
77
+ expect(screen.getByText('Audit Backlog')).toBeInTheDocument();
78
+ // Use context to avoid matching P0 in tracker cards
79
+ const backlogPanel = screen.getByTestId(TESTIDS.backlogPanel);
80
+ expect(backlogPanel.textContent).toContain('P0');
81
+ });
82
+ });
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Work view — tracker kanban board + backlog panel + form.
3
+ * Binds TrackerBoard, TrackerCard, TrackerForm, BacklogPanel.
4
+ * Reads tracker + backlog from SSE, allows mutations.
5
+ */
6
+
7
+ import { useEffect, useState } from 'react';
8
+ import { useSSE } from '../lib/useSSE';
9
+ import { TrackerBoard } from '../components/TrackerBoard';
10
+ import { TrackerForm } from '../components/TrackerForm';
11
+ import { BacklogPanel } from '../components/BacklogPanel';
12
+ import { TESTIDS } from '../test/fixtures';
13
+ import type { TrackerItem, AuditBacklog } from '../lib/types';
14
+ import '../styles/work.css';
15
+
16
+ export function Work() {
17
+ const sse = useSSE();
18
+ const [trackerItems, setTrackerItems] = useState<TrackerItem[]>([]);
19
+ const [backlog, setBacklog] = useState<AuditBacklog | null>(null);
20
+ const [showForm, setShowForm] = useState(false);
21
+
22
+ useEffect(() => {
23
+ if (sse.tracker?.items !== undefined && sse.tracker?.items !== null) {
24
+ setTrackerItems(sse.tracker.items);
25
+ } else if (sse.tracker === null) {
26
+ // Treat null items as empty array
27
+ setTrackerItems([]);
28
+ }
29
+ }, [sse.tracker]);
30
+
31
+ useEffect(() => {
32
+ if (sse.backlog) {
33
+ setBacklog(sse.backlog);
34
+ }
35
+ }, [sse.backlog]);
36
+
37
+ const handleItemUpdate = (updated: TrackerItem) => {
38
+ setTrackerItems((prev) =>
39
+ prev.map((item) => (item.id === updated.id ? updated : item))
40
+ );
41
+ };
42
+
43
+ const handleFormSuccess = () => {
44
+ setShowForm(false);
45
+ // Tracker board will update via SSE
46
+ };
47
+
48
+ return (
49
+ <section className="work-view" data-testid={TESTIDS.viewWork} aria-label="Work view">
50
+ <div className="work-container">
51
+ <div className="work-board">
52
+ <div className="board-header">
53
+ <h2>Tracker Kanban</h2>
54
+ <button
55
+ type="button"
56
+ className="add-item-button"
57
+ onClick={() => setShowForm(!showForm)}
58
+ aria-expanded={showForm}
59
+ >
60
+ {showForm ? 'Cancel' : '+ Add Item'}
61
+ </button>
62
+ </div>
63
+
64
+ {showForm && (
65
+ <div className="form-container">
66
+ <TrackerForm onSuccess={handleFormSuccess} />
67
+ </div>
68
+ )}
69
+
70
+ <TrackerBoard items={trackerItems} onUpdate={handleItemUpdate} />
71
+ </div>
72
+
73
+ <aside className="work-sidebar">
74
+ <BacklogPanel backlog={backlog} />
75
+ </aside>
76
+ </div>
77
+ </section>
78
+ );
79
+ }
@@ -0,0 +1,10 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ interface Window {
4
+ /**
5
+ * CSRF token injected by ui/render.py's sentinel substitution in the built
6
+ * index.html; absent on the raw Vite dev server (api.ts then falls back to
7
+ * GET /api/session).
8
+ */
9
+ __AESOP_CSRF_TOKEN__?: string;
10
+ }
@@ -0,0 +1,22 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "noUnusedLocals": true,
12
+ "noUnusedParameters": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "jsx": "react-jsx",
15
+ "jsxImportSource": "react",
16
+ "moduleResolution": "bundler",
17
+ "resolveJsonModule": true,
18
+ "isolatedModules": true
19
+ },
20
+ "include": ["src"],
21
+ "exclude": ["node_modules"]
22
+ }
@@ -0,0 +1,25 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 5173,
8
+ proxy: {
9
+ '/data': 'http://localhost:8770',
10
+ '/api': 'http://localhost:8770',
11
+ '/agent': 'http://localhost:8770',
12
+ '/events': 'http://localhost:8770',
13
+ '/submit': 'http://localhost:8770',
14
+ },
15
+ },
16
+ build: {
17
+ outDir: 'dist',
18
+ sourcemap: false,
19
+ rollupOptions: {
20
+ output: {
21
+ manualChunks: undefined,
22
+ },
23
+ },
24
+ },
25
+ });
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from 'vitest/config';
2
+ import react from '@vitejs/plugin-react';
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ test: {
7
+ environment: 'jsdom',
8
+ setupFiles: ['src/test/setup.ts'],
9
+ globals: true,
10
+ include: ['src/**/*.test.{ts,tsx}'],
11
+ },
12
+ });