agentboss 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/README.md +34 -0
  2. package/bin/aboss.js +288 -0
  3. package/client/dist/assets/index-C1wFD_Vo.css +1 -0
  4. package/client/dist/assets/index-DBj1Ujlx.js +137 -0
  5. package/client/dist/index.html +34 -0
  6. package/package.json +64 -0
  7. package/server/analysis/daily-aggregator.js +258 -0
  8. package/server/analysis/difficulty.js +129 -0
  9. package/server/analysis/dimensions/ai-knowledge.js +172 -0
  10. package/server/analysis/dimensions/ai-tools.js +161 -0
  11. package/server/analysis/dimensions/judgement.js +107 -0
  12. package/server/analysis/dimensions/llm-merge.js +57 -0
  13. package/server/analysis/dimensions/output-quality.js +167 -0
  14. package/server/analysis/dimensions/problem-definition.js +104 -0
  15. package/server/analysis/dimensions/system-thinking.js +225 -0
  16. package/server/analysis/evidence-builder.js +104 -0
  17. package/server/analysis/job.js +273 -0
  18. package/server/analysis/report-builder.js +581 -0
  19. package/server/analysis/scoring-v2.js +72 -0
  20. package/server/analysis/text-signals.js +179 -0
  21. package/server/analysis/thresholds-v2.js +358 -0
  22. package/server/api/advice.js +124 -0
  23. package/server/api/analysis.js +141 -0
  24. package/server/api/execution.js +330 -0
  25. package/server/api/metrics.js +277 -0
  26. package/server/api/overview.js +308 -0
  27. package/server/api/project.js +255 -0
  28. package/server/api/reports.js +125 -0
  29. package/server/api/sessions.js +118 -0
  30. package/server/api/settings.js +119 -0
  31. package/server/db/connection.js +175 -0
  32. package/server/db/queries.js +1051 -0
  33. package/server/db/schema.js +487 -0
  34. package/server/etl/active-time.js +150 -0
  35. package/server/etl/backfill-subagents.js +178 -0
  36. package/server/etl/claude-code.js +826 -0
  37. package/server/etl/detect.js +341 -0
  38. package/server/etl/judge-filter.js +117 -0
  39. package/server/etl/opencode.js +606 -0
  40. package/server/execution/job.js +662 -0
  41. package/server/execution/prompt.js +227 -0
  42. package/server/execution/runner.js +218 -0
  43. package/server/index.js +94 -0
  44. package/server/llm/advice-prompt.js +339 -0
  45. package/server/llm/advice.js +384 -0
  46. package/server/llm/analysis-prompt.js +162 -0
  47. package/server/llm/cli-runner.js +249 -0
  48. package/server/llm/judge-prompts.js +179 -0
  49. package/server/llm/judge.js +118 -0
  50. package/server/llm/project-advice-prompt.js +332 -0
  51. package/server/llm/project-advice.js +491 -0
  52. package/server/llm/session-analyzer.js +122 -0
  53. package/server/utils/project.js +80 -0
package/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # agentboss
2
+
3
+ > AI Agent 协作分析工具 —— 做 AI 的老板,而不是它的保姆。
4
+
5
+ `agentboss` 是一个本地优先的命令行工具,把 OpenCode / Claude Code 的会话数据 ETL 进本地 SQLite,提供一个 Web Dashboard,帮你回顾 AI agent 的工作表现:在哪些事上做得好、哪些事上踩了坑、时间花在了哪、应该怎么改进 prompt 和工作流。
6
+
7
+ 所有数据都在本地(`~/.agent-boss/boss.db`),不上传任何东西。
8
+
9
+ ## 安装
10
+
11
+ ```bash
12
+ npm install -g agentboss
13
+ ```
14
+
15
+ 需要 Node.js >= 18。
16
+
17
+ ## 使用
18
+
19
+ ```bash
20
+ aboss # 启动服务并自动打开浏览器(默认 http://localhost:3141)
21
+ aboss -p 4000 # 指定端口
22
+ aboss --no-open # 启动但不自动打开浏览器
23
+ ```
24
+
25
+ 首次启动时会自动扫描本地 OpenCode / Claude Code 的会话数据库,做一次同步,然后在后台跑分析任务。
26
+
27
+ ## 依赖
28
+
29
+ - 至少安装了 [OpenCode](https://opencode.ai) 或 [Claude Code](https://docs.anthropic.com/claude/docs/claude-code) 中的一个,并产生过会话数据
30
+ - 分析功能会调用你本地的 `opencode` 或 `claude` 命令作为 LLM judge(不需要额外配置 API key)
31
+
32
+ ## License
33
+
34
+ MIT © Felix
package/bin/aboss.js ADDED
@@ -0,0 +1,288 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Boss CLI entry point.
4
+ * Usage: aboss [--port PORT] [--no-open]
5
+ *
6
+ * Startup sequence (design doc §12.3):
7
+ * 1. Parse CLI args
8
+ * 2. Print banner
9
+ * 3. Initialise database
10
+ * 4. Detect data sources
11
+ * 5. Run ETL sync (incremental)
12
+ * 6. Calculate active times
13
+ * 7. Start analysis job (background)
14
+ * 8. Start Express server (with port auto-increment)
15
+ * 9. Open browser (unless --no-open)
16
+ * 10. Handle graceful shutdown
17
+ *
18
+ * @author Felix
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const { getDb, closeDb } = require('../server/db/connection');
24
+ const { detectSources } = require('../server/etl/detect');
25
+ const { backfillSubagents } = require('../server/etl/backfill-subagents');
26
+ const { collectOpenCode } = require('../server/etl/opencode');
27
+ const { collectClaudeCode } = require('../server/etl/claude-code');
28
+ const { calculateActiveTime } = require('../server/etl/active-time');
29
+ const { runAnalysisJob } = require('../server/analysis/job');
30
+ const { startServer } = require('../server/index');
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // CLI arg parsing
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Parse process.argv into { port, noOpen }.
38
+ * @returns {{ port: number, noOpen: boolean }}
39
+ */
40
+ function parseArgs() {
41
+ const args = process.argv.slice(2);
42
+ let port = 3141;
43
+ let noOpen = false;
44
+
45
+ for (let i = 0; i < args.length; i++) {
46
+ if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
47
+ const parsed = Number(args[i + 1]);
48
+ if (!Number.isNaN(parsed) && parsed > 0 && parsed < 65536) {
49
+ port = parsed;
50
+ }
51
+ i++; // skip next arg (the port value)
52
+ } else if (args[i] === '--no-open') {
53
+ noOpen = true;
54
+ }
55
+ }
56
+
57
+ return { port, noOpen };
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Banner
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function printBanner() {
65
+ console.log('');
66
+ console.log(' ___ _ ____');
67
+ console.log(' / _ \\ __ _ ___ _ __ | |_ | __ ) ___ ___ ___');
68
+ console.log("| |_| |/ _` |/ _ \\ '_ \\| __| | _ \\ / _ \\/ __/ __|");
69
+ console.log('| ___ | (_| | __/ | | | |_ | |_) | (_) \\__ \\__ \\');
70
+ console.log('|_| |_|\\__, |\\___|_| |_|\\__| |____/ \\___/|___/___/');
71
+ console.log(' |___/');
72
+ console.log('');
73
+ console.log(' Agent Boss v0.1.0');
74
+ console.log(' Be your AI agent\'s boss, not its babysitter.');
75
+ console.log('');
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Port auto-increment helper
80
+ // ---------------------------------------------------------------------------
81
+
82
+ /**
83
+ * Try starting the server on `startPort`, auto-incrementing on EADDRINUSE.
84
+ * @param {object} db
85
+ * @param {number} startPort
86
+ * @param {number} maxAttempts
87
+ * @returns {Promise<number>} the actual port used
88
+ */
89
+ async function startWithPortRetry(db, startPort, maxAttempts = 10) {
90
+ for (let i = 0; i < maxAttempts; i++) {
91
+ const port = startPort + i;
92
+ try {
93
+ await startServer(db, port);
94
+ if (port !== startPort) {
95
+ console.log(`[server] Port ${startPort} is busy, using ${port} instead`);
96
+ }
97
+ return port;
98
+ } catch (err) {
99
+ if (err.code === 'EADDRINUSE' && i < maxAttempts - 1) {
100
+ continue; // try next port
101
+ }
102
+ throw err;
103
+ }
104
+ }
105
+ }
106
+
107
+ // ---------------------------------------------------------------------------
108
+ // Main
109
+ // ---------------------------------------------------------------------------
110
+
111
+ async function main() {
112
+ const { port: requestedPort, noOpen } = parseArgs();
113
+
114
+ // 1. Banner
115
+ printBanner();
116
+
117
+ // 2. Initialise database
118
+ let db;
119
+ try {
120
+ db = await getDb();
121
+ console.log('[db] Database initialised');
122
+ } catch (err) {
123
+ console.error('[db] Failed to initialise database:', err.message);
124
+ process.exit(1);
125
+ }
126
+
127
+ // 3. Detect data sources
128
+ let sources;
129
+ try {
130
+ sources = await detectSources(db);
131
+ } catch (err) {
132
+ console.error('[detect] Source detection failed:', err.message);
133
+ sources = {
134
+ opencode: { status: 'not_found', path: '' },
135
+ claudeCode: { status: 'not_found', path: '' },
136
+ };
137
+ }
138
+
139
+ const ocAvailable = sources.opencode.status === 'available';
140
+ const ccAvailable = sources.claudeCode.status === 'available';
141
+
142
+ console.log(
143
+ `[detect] OpenCode: ${ocAvailable ? 'available' : 'not found'}` +
144
+ (ocAvailable ? ` (${sources.opencode.path})` : '')
145
+ );
146
+ console.log(
147
+ `[detect] Claude Code: ${ccAvailable ? 'available' : 'not found'}` +
148
+ (ccAvailable ? ` (${sources.claudeCode.path})` : '')
149
+ );
150
+
151
+ // 4. Run ETL sync (incremental)
152
+ if (ocAvailable) {
153
+ try {
154
+ console.log('[etl] Starting OpenCode ETL sync...');
155
+ const stats = await collectOpenCode(db, sources.opencode.path, {
156
+ onProgress: (msg) => console.log(`[etl] ${msg}`),
157
+ });
158
+ console.log(
159
+ `[etl] OpenCode ETL done: ${stats.sessionCount} sessions, ` +
160
+ `${stats.messageCount} messages, ${stats.toolCallCount} tool calls` +
161
+ (stats.errorSessionCount ? `, ${stats.errorSessionCount} failed` : '')
162
+ );
163
+ } catch (err) {
164
+ console.error('[etl] OpenCode ETL sync failed:', err.message);
165
+ }
166
+ }
167
+
168
+ if (ccAvailable) {
169
+ try {
170
+ console.log('[etl] Starting Claude Code ETL sync...');
171
+ const stats = await collectClaudeCode(db, sources.claudeCode.path, {
172
+ onProgress: (msg) => console.log(`[etl] ${msg}`),
173
+ });
174
+ console.log(
175
+ `[etl] Claude Code ETL done: ${stats.sessionCount} sessions, ` +
176
+ `${stats.messageCount} messages, ${stats.toolCallCount} tool calls` +
177
+ (stats.errorSessionCount ? `, ${stats.errorSessionCount} failed` : '')
178
+ );
179
+ } catch (err) {
180
+ console.error('[etl] Claude Code ETL sync failed:', err.message);
181
+ }
182
+ }
183
+
184
+ if (!ocAvailable && !ccAvailable) {
185
+ console.log('[etl] Skipping ETL (no data sources available)');
186
+ }
187
+
188
+ // 5. Calculate active times
189
+ try {
190
+ const updated = calculateActiveTime(db);
191
+ if (updated > 0) {
192
+ console.log(`[active] Updated active_minutes for ${updated} session(s)`);
193
+ }
194
+ } catch (err) {
195
+ console.error('[active] Active-time calculation failed:', err.message);
196
+ }
197
+
198
+ // 5.5 Backfill parent_session_id / agent_type for legacy rows imported
199
+ // before the subagent linkage columns existed. Idempotent + cheap
200
+ // after the first run. Failures are non-fatal.
201
+ if (ocAvailable) {
202
+ try {
203
+ const r = await backfillSubagents(db);
204
+ if (r.updated > 0) {
205
+ console.log(
206
+ `[backfill] Marked ${r.updated} subagent session(s) ` +
207
+ `(scanned ${r.scanned})`
208
+ );
209
+ } else if (r.reason) {
210
+ console.log(`[backfill] Skipped: ${r.reason}`);
211
+ }
212
+ } catch (err) {
213
+ console.error('[backfill] Subagent backfill failed:', err.message);
214
+ }
215
+ }
216
+
217
+ // 6. Start analysis job in background (don't await)
218
+ runAnalysisJob(db, {
219
+ onProgress: (p) => {
220
+ if (p.error) {
221
+ console.log(`[analysis] Error on ${p.date} session ${p.sessionId}: ${p.error}`);
222
+ } else if (p.aggregationError) {
223
+ console.log(`[analysis] Aggregation error for ${p.date}: ${p.aggregationError}`);
224
+ } else {
225
+ console.log(`[analysis] ${p.analyzed}/${p.total} sessions (${p.date})`);
226
+ }
227
+ },
228
+ })
229
+ .then((result) => {
230
+ console.log(
231
+ `[analysis] Background job complete: ${result.analyzed || 0} sessions analyzed`
232
+ );
233
+ })
234
+ .catch((err) => {
235
+ console.error('[analysis] Background job failed:', err.message);
236
+ });
237
+ console.log('[analysis] Analysis job started in background...');
238
+
239
+ // 7. Start Express server (with port auto-increment on EADDRINUSE)
240
+ let actualPort;
241
+ try {
242
+ actualPort = await startWithPortRetry(db, requestedPort);
243
+ } catch (err) {
244
+ console.error('[server] Failed to start server:', err.message);
245
+ process.exit(1);
246
+ }
247
+
248
+ // 8. Open browser (unless --no-open)
249
+ if (!noOpen) {
250
+ const url = `http://localhost:${actualPort}`;
251
+ import('open')
252
+ .then((m) => m.default(url))
253
+ .catch(() => {
254
+ // 'open' is optional — if it fails, just print the URL
255
+ console.log(`[browser] Could not open browser. Visit: ${url}`);
256
+ });
257
+ }
258
+
259
+ // 9. Graceful shutdown
260
+ process.on('SIGINT', async () => {
261
+ console.log('\n[shutdown] Shutting down gracefully...');
262
+ try {
263
+ await closeDb();
264
+ } catch (_) {
265
+ // best-effort
266
+ }
267
+ process.exit(0);
268
+ });
269
+
270
+ process.on('SIGTERM', async () => {
271
+ console.log('\n[shutdown] Received SIGTERM, shutting down...');
272
+ try {
273
+ await closeDb();
274
+ } catch (_) {
275
+ // best-effort
276
+ }
277
+ process.exit(0);
278
+ });
279
+ }
280
+
281
+ // ---------------------------------------------------------------------------
282
+ // Run
283
+ // ---------------------------------------------------------------------------
284
+
285
+ main().catch((err) => {
286
+ console.error('Fatal error:', err);
287
+ process.exit(1);
288
+ });
@@ -0,0 +1 @@
1
+ :root{--bg: #F5F7FA;--surface: #FFFFFF;--ink: #1F2937;--ink-dim: #374151;--ink-mute: #6B7280;--ink-faint: #9CA3AF;--line: #E5E7EB;--line-soft: #F1F3F5;--accent: #1677FF;--accent-deep: #0958D9;--accent-soft: #E8F1FF;--ok: #00B42A;--ok-soft: #EAFBEE;--warn: #FAAD14;--warn-soft: #FFF8E6;--bad: #F53F3F;--bad-soft: #FEECEC;--hero-bg: linear-gradient(135deg, #1677FF 0%, #00C0FA 100%);--hero-line: rgba(255, 255, 255, .18);--hero-mute: rgba(255, 255, 255, .78);--bg-raised: var(--surface);--bg-inset: var(--line-soft);--rule: var(--line);--rule-soft: var(--line-soft);--phosphor: var(--accent);--phosphor-2: var(--accent);--lime: var(--ok);--magenta: var(--bad);--plum: var(--accent);--amber: var(--warn);--danger: var(--bad);--success: var(--ok);--glow-cyan: none;--glow-lime: none;--glow-mag: none;--font-display: "Space Grotesk", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;--font-body: "Space Grotesk", "PingFang SC", "Microsoft YaHei", system-ui, sans-serif;--font-mono: "Geist Mono", ui-monospace, "JetBrains Mono", monospace;--max-w: 1180px;--r-panel: 12px;--r-ctl: 8px;--shadow-panel: 0 2px 8px rgba(17, 24, 39, .06);--shadow-pop: 0 12px 32px rgba(17, 24, 39, .14);--sidebar-w: 220px}:root[data-theme=hack]{--bg: #0a0e0f;--surface: #0f1518;--ink: #d6ffe8;--ink-dim: #9fe6bd;--ink-mute: #5f9079;--ink-faint: #3e5d4c;--line: #1c2c23;--line-soft: #15201a;--accent: #00ff9c;--accent-deep: #00d683;--accent-soft: rgba(0, 255, 156, .13);--ok: #00ff9c;--ok-soft: rgba(0, 255, 156, .13);--warn: #ffcc33;--warn-soft: rgba(255, 204, 51, .13);--bad: #ff5c6c;--bad-soft: rgba(255, 92, 108, .13);--hero-bg: linear-gradient(135deg, #00231a 0%, #001016 60%, #001a26 100%);--hero-line: rgba(0, 255, 156, .2);--hero-mute: rgba(190, 255, 222, .62);--shadow-panel: 0 0 0 1px rgba(0, 255, 156, .05), 0 6px 18px rgba(0, 0, 0, .45);--shadow-pop: 0 12px 36px rgba(0, 0, 0, .65);--font-display: "Geist Mono", ui-monospace, "JetBrains Mono", monospace;--font-body: "Geist Mono", ui-monospace, "JetBrains Mono", monospace}:root[data-theme=hack] .topbar{background:#0a0e0fd1}:root[data-theme=hack] .btn{background:var(--accent);border-color:var(--accent);color:#00130c}:root[data-theme=hack] .btn:hover{background:var(--accent-deep)}:root[data-theme=hack] ::selection{background:var(--accent);color:#00130c}*{margin:0;padding:0;box-sizing:border-box}html,body,#root{min-height:100%}body{background:var(--bg);color:var(--ink);font-family:var(--font-body);font-size:14px;line-height:1.6;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}::selection{background:var(--accent);color:#fff}a{color:inherit;text-decoration:none}button{font:inherit;cursor:pointer}.shell{display:flex;align-items:stretch;min-height:100vh}.shell__main{flex:1;min-width:0;display:flex;flex-direction:column}.sidebar{width:var(--sidebar-w);flex-shrink:0;background:var(--surface);border-right:1px solid var(--line);position:sticky;top:0;height:100vh;display:flex;flex-direction:column;z-index:20}.sidebar__brand{display:flex;align-items:center;gap:10px;height:60px;padding:0 20px;border-bottom:1px solid var(--line)}.sidebar__logo{width:30px;height:30px;border-radius:8px;background:linear-gradient(135deg,var(--accent),#00C0FA);color:#fff;display:flex;align-items:center;justify-content:center;font-family:var(--font-display);font-weight:700;font-size:13px;letter-spacing:-.02em}.sidebar__name{font-family:var(--font-display);font-weight:600;font-size:15px;letter-spacing:-.02em;white-space:nowrap}.sidebar__name b{color:var(--accent);font-weight:700}.sidebar__nav{flex:1;display:flex;flex-direction:column;gap:4px;padding:14px 12px;overflow-y:auto}.sidebar__nav a{display:flex;align-items:center;gap:12px;padding:10px 14px;border-radius:var(--r-ctl);font-size:14px;font-weight:500;color:var(--ink-mute);white-space:nowrap;transition:background .15s,color .15s}.sidebar__nav a .i{font-size:17px;width:20px;text-align:center;flex-shrink:0}.sidebar__nav a:hover{background:var(--line-soft);color:var(--ink)}.sidebar__nav a.active{background:var(--accent-soft);color:var(--accent-deep);font-weight:600}.sidebar__foot{padding:16px 20px;border-top:1px solid var(--line);font-family:var(--font-mono);font-size:11px;color:var(--ink-faint)}.theme-toggle{display:inline-flex;align-items:center;justify-content:center;width:36px;height:36px;border:1px solid var(--line);border-radius:var(--r-ctl);background:var(--surface);font-size:17px;line-height:1;transition:background .15s,border-color .15s,box-shadow .15s,transform .1s}.theme-toggle:hover{background:var(--line-soft);border-color:var(--ink-faint)}.theme-toggle:active{transform:scale(.94)}:root[data-theme=hack] .theme-toggle{border-color:var(--accent);box-shadow:0 0 0 1px #00ff9c26,0 0 12px #00ff9c1f}:root[data-theme=hack] .theme-toggle:hover{background:var(--accent-soft)}.topbar{position:sticky;top:0;z-index:10;background:#ffffffeb;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);border-bottom:1px solid var(--line)}.topbar__in{padding:0 32px;height:60px;display:flex;align-items:center;justify-content:flex-end;gap:24px}.topbar__status{display:flex;align-items:center;gap:10px;font-family:var(--font-mono);font-size:12px;color:var(--ink-mute);white-space:nowrap}.topbar__status .bar{width:72px;height:4px;border-radius:999px;background:var(--line);overflow:hidden}.topbar__status .bar i{display:block;height:100%;background:var(--accent);border-radius:999px;transform-origin:left;transition:transform .4s ease}.topbar__status .err{color:var(--bad)}.page{flex:1;max-width:var(--max-w);width:100%;margin:0 auto;padding:36px 32px 64px}@media (max-width: 768px){.page{padding:24px 16px 48px}}@media (max-width: 860px){.shell{flex-direction:column}.sidebar{width:100%;height:auto;flex-direction:row;align-items:center;border-right:0;border-bottom:1px solid var(--line)}.sidebar__brand{border-bottom:0;border-right:1px solid var(--line)}.sidebar__nav{flex-direction:row;align-items:center;padding:8px 12px;overflow-x:auto;overflow-y:hidden}.sidebar__nav a{padding:8px 12px}.sidebar__nav a .t,.sidebar__foot{display:none}}@media (prefers-reduced-motion: no-preference){.page>article>*{animation:rise .45s cubic-bezier(.16,1,.3,1) both}.page>article>*:nth-child(2){animation-delay:.05s}.page>article>*:nth-child(3){animation-delay:.1s}.page>article>*:nth-child(4){animation-delay:.15s}@keyframes rise{0%{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}}.phead{display:flex;align-items:flex-end;justify-content:space-between;gap:24px;margin-bottom:28px}.phead__title{font-family:var(--font-display);font-weight:700;font-size:clamp(26px,4vw,34px);letter-spacing:-.02em;line-height:1.15}.phead__meta{font-family:var(--font-mono);font-size:12.5px;color:var(--ink-mute);text-align:right;line-height:1.7;white-space:nowrap}.phead__meta b{color:var(--ink);font-weight:600}@media (max-width: 640px){.phead{flex-direction:column;align-items:flex-start;gap:8px}.phead__meta{text-align:left}}.hero{display:grid;grid-template-columns:1.7fr 1fr;background:var(--hero-bg);color:#fff;border-radius:var(--r-panel);margin-bottom:32px;overflow:hidden}.hero__main{padding:20px 24px}.hero__side{padding:16px 20px;border-left:1px solid var(--hero-line);display:flex;flex-direction:column;justify-content:center;gap:2px}@media (max-width: 820px){.hero{grid-template-columns:1fr}.hero__side{border-left:0;border-top:1px solid var(--hero-line);flex-direction:row;flex-wrap:wrap;gap:24px}}.hero__label{font-size:12px;font-weight:500;color:var(--hero-mute)}.hero__big{font-family:var(--font-display);font-weight:700;font-size:clamp(36px,4.8vw,56px);line-height:1.05;letter-spacing:-.03em;font-variant-numeric:lining-nums tabular-nums;margin:4px 0 6px;display:flex;align-items:baseline;gap:10px}.hero__big .u{font-family:var(--font-mono);font-size:14px;font-weight:500;letter-spacing:.06em;color:var(--hero-mute)}.hero__sub{font-size:12px;color:var(--hero-mute)}.hero__sub b{color:#fff;font-weight:600;font-variant-numeric:tabular-nums}.dial{padding:6px 0}.dial+.dial{border-top:1px solid var(--hero-line)}@media (max-width: 820px){.dial+.dial{border-top:0}}.dial__name{font-size:11px;color:var(--hero-mute)}.dial__val{font-family:var(--font-display);font-size:18px;font-weight:600;letter-spacing:-.02em;font-variant-numeric:tabular-nums;color:#fff;margin-top:2px}.dial__hint{font-family:var(--font-mono);font-size:11px;color:#ffffff61;margin-top:2px}.stats{display:grid;grid-template-columns:repeat(4,1fr);background:var(--surface);border:1px solid var(--line);border-radius:var(--r-panel);box-shadow:var(--shadow-panel);margin-bottom:40px;overflow:hidden}.stats--5{grid-template-columns:repeat(5,1fr)}.stats--6{grid-template-columns:repeat(6,1fr)}@media (max-width: 900px){.stats,.stats--5,.stats--6{grid-template-columns:repeat(2,1fr)}}.stat{padding:20px 24px;border-left:1px solid var(--line-soft)}.stat:first-child{border-left:0}@media (max-width: 900px){.stat{border-top:1px solid var(--line-soft)}.stat:nth-child(-n+2){border-top:0}.stat:nth-child(odd){border-left:0}}.stat__label{font-size:12.5px;color:var(--ink-mute)}.stat__val{font-family:var(--font-display);font-weight:600;font-size:28px;line-height:1.2;letter-spacing:-.02em;font-variant-numeric:lining-nums tabular-nums;margin-top:4px}.stat__val .u{font-family:var(--font-mono);font-size:12px;font-weight:500;color:var(--ink-faint);margin-left:5px}.stat__delta{font-family:var(--font-mono);font-size:11.5px;color:var(--ink-faint);margin-top:4px}.section{margin-bottom:44px}.section__head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;margin-bottom:14px}.section__head--toggle{cursor:pointer;-webkit-user-select:none;user-select:none}.section__title{font-family:var(--font-display);font-weight:600;font-size:18px;letter-spacing:-.01em}.section__caret{display:inline-block;width:1em;margin-right:6px;color:var(--ink-faint);font-size:14px}.section__meta{font-family:var(--font-mono);font-size:12px;color:var(--ink-faint);display:flex;align-items:center;gap:14px}.panel{background:var(--surface);border:1px solid var(--line);border-radius:var(--r-panel);box-shadow:var(--shadow-panel);padding:24px}.panel--flush{padding:0;overflow:hidden}.panel__head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:16px}.panel__title{font-weight:600;font-size:15px}.panel__tag{font-family:var(--font-mono);font-size:11px;color:var(--ink-faint)}.col-2{display:grid;grid-template-columns:repeat(2,1fr);gap:16px}.col-3{display:grid;grid-template-columns:repeat(3,1fr);gap:16px}@media (max-width: 900px){.col-2,.col-3{grid-template-columns:1fr}}.chip{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;font-family:var(--font-mono);font-size:11px;font-weight:500;background:var(--line-soft);color:var(--ink-dim)}.chip--accent{background:var(--accent-soft);color:var(--accent-deep)}.chip--good{background:var(--ok-soft);color:var(--ok)}.chip--warn{background:var(--warn-soft);color:var(--warn)}.chip--bad{background:var(--bad-soft);color:var(--bad)}.chip--info{background:var(--accent-soft);color:var(--accent-deep)}.score{display:inline-block;min-width:36px;text-align:center;font-family:var(--font-mono);font-size:12px;font-weight:600;font-variant-numeric:tabular-nums;padding:2px 8px;border-radius:999px;background:var(--line-soft);color:var(--ink-mute)}.score.s-hi{background:var(--ok-soft);color:var(--ok)}.score.s-md{background:var(--accent-soft);color:var(--accent-deep)}.score.s-lo{background:var(--bad-soft);color:var(--bad)}.tbl{width:100%;border-collapse:collapse;font-size:13.5px}.tbl thead th{font-family:var(--font-mono);font-size:11px;font-weight:500;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-faint);text-align:left;padding:12px 20px;border-bottom:1px solid var(--line);background:var(--surface)}.tbl thead th.num{text-align:right}.tbl tbody td{padding:14px 20px;border-bottom:1px solid var(--line-soft);vertical-align:middle}.tbl tbody tr:last-child td{border-bottom:0}.tbl tbody td.num{text-align:right;font-family:var(--font-mono);font-size:13px;font-variant-numeric:tabular-nums}.tbl tbody tr{transition:background .12s}.tbl tbody tr.click{cursor:pointer}.tbl tbody tr.click:hover{background:var(--bg)}.tbl .title{font-weight:600;font-size:14px}.tbl .title small{display:block;font-family:var(--font-mono);font-weight:400;font-size:11px;color:var(--ink-faint);margin-top:3px}.btn{display:inline-flex;align-items:center;gap:8px;background:var(--ink);border:1px solid var(--ink);color:#fff;padding:9px 18px;border-radius:var(--r-ctl);font-size:13.5px;font-weight:600;transition:background .15s,transform .1s}.btn:hover{background:#2d2d31}.btn:active{transform:scale(.98)}.btn:disabled{opacity:.5;cursor:default}.btn--ghost{background:var(--surface);border-color:var(--line);color:var(--ink)}.btn--ghost:hover{background:var(--bg)}.back{display:inline-flex;align-items:center;gap:6px;font-size:13px;font-weight:500;color:var(--ink-mute);background:none;border:0;padding:0;margin-bottom:20px;transition:color .15s}.back:before{content:"←"}.back:hover{color:var(--ink)}.alert{display:flex;align-items:center;gap:10px;background:var(--warn-soft);border:1px solid #FDE68A;color:var(--warn);border-radius:var(--r-ctl);padding:11px 16px;font-size:13px;margin-bottom:24px}.state{padding:96px 0;text-align:center;font-size:14px;color:var(--ink-mute)}.state.err{color:var(--bad)}@media (prefers-reduced-motion: no-preference){.state-blink:after{content:"…";display:inline-block;animation:pulse 1.2s ease-in-out infinite}@keyframes pulse{50%{opacity:.25}}}.field{display:flex;flex-direction:column;gap:6px}.field__label{font-size:13.5px;font-weight:600}.field__desc{font-size:12.5px;color:var(--ink-mute)}.input{width:100%;padding:10px 14px;font-family:var(--font-mono);font-size:13.5px;background:var(--surface);border:1px solid var(--line);border-radius:var(--r-ctl);color:var(--ink);outline:none;transition:border-color .15s,box-shadow .15s}.input:focus{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}.input::placeholder{color:var(--ink-faint)}.seg{display:inline-flex;background:var(--line-soft);border-radius:999px;padding:3px}.seg button{border:0;background:transparent;color:var(--ink-mute);font-size:12px;font-weight:500;padding:4px 14px;border-radius:999px;transition:background .15s,color .15s}.seg button[aria-selected=true]{background:var(--surface);color:var(--ink);box-shadow:var(--shadow-panel)}.footer{margin-top:56px;padding-top:20px;border-top:1px solid var(--line);display:flex;justify-content:space-between;align-items:baseline;font-size:12.5px;color:var(--ink-faint)}.footer .mark{font-weight:600;color:var(--ink-mute)}.recharts-default-tooltip{background:var(--surface)!important;border:1px solid var(--line)!important;border-radius:10px!important;box-shadow:var(--shadow-pop)!important;color:var(--ink)!important;font-family:var(--font-mono)!important;font-size:12px!important}.recharts-tooltip-label{color:var(--ink-mute)!important}.recharts-cartesian-grid-horizontal line,.recharts-cartesian-grid-vertical line{stroke:var(--line-soft)!important}.recharts-cartesian-axis-line{stroke:var(--line)!important;stroke-width:1!important}.recharts-cartesian-axis-tick-line{stroke:var(--line)!important}.recharts-text{fill:var(--ink-mute)!important;font-family:Geist Mono,monospace!important;font-size:11px!important}.recharts-polar-grid-angle line,.recharts-polar-grid-concentric circle,.recharts-polar-grid-concentric polygon{stroke:var(--line)!important;stroke-width:1!important}.recharts-polar-angle-axis-tick-value{fill:var(--ink-dim)!important}:root{--positive: var(--ok);--negative: var(--bad)}.subgrid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:16px}@media (max-width: 900px){.subgrid{grid-template-columns:1fr}}.dim-card{background:var(--surface);border:1px solid var(--line);border-radius:14px;padding:18px 18px 14px;display:flex;flex-direction:column;gap:14px}.dim-card__head{display:grid;grid-template-columns:1fr auto;gap:12px;align-items:flex-start;padding-bottom:12px;border-bottom:1px solid var(--line-soft)}.dim-card__code{font-family:var(--font-mono, ui-monospace, monospace);font-size:11px;letter-spacing:.18em;color:var(--ink-faint);text-transform:uppercase}.dim-card__name{font-size:17px;font-weight:600;color:var(--ink);margin-top:2px;line-height:1.2}.dim-card__blurb{font-family:var(--font-mono, ui-monospace, monospace);font-size:11px;color:var(--ink-faint);margin-top:4px}.dim-card__avg{text-align:right;display:flex;flex-direction:column;align-items:flex-end;gap:2px}.dim-card__avg-num{font-family:var(--font-mono, ui-monospace, monospace);font-size:28px;font-weight:600;line-height:1;font-variant-numeric:tabular-nums}.dim-card__avg-tier{font-family:var(--font-mono, ui-monospace, monospace);font-size:10px;letter-spacing:.14em;text-transform:uppercase}.dim-card__avg-empty{font-family:var(--font-mono, ui-monospace, monospace);font-size:20px;color:var(--ink-faint)}.dim-card__list{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:12px}.sub-row{display:flex;flex-direction:column;gap:6px}.sub-row__head{display:grid;grid-template-columns:1fr auto auto;align-items:center;gap:10px}.sub-row__label{display:flex;flex-direction:column;gap:1px;min-width:0}.sub-row__name{font-size:13px;color:var(--ink);font-weight:500}.sub-row__key{font-family:var(--font-mono, ui-monospace, monospace);font-size:10px;color:var(--ink-faint);letter-spacing:.06em}.sub-row__score{font-family:var(--font-mono, ui-monospace, monospace);font-size:16px;font-weight:600;font-variant-numeric:tabular-nums;min-width:32px;text-align:right}.sub-row__badge{font-family:var(--font-mono, ui-monospace, monospace);font-size:10.5px;font-weight:600;letter-spacing:.06em;padding:3px 8px;border:1px solid;border-radius:999px;background:transparent;line-height:1.2;transition:background-color .15s ease}.sub-row__badge:hover,.sub-row__badge:focus-visible{background:var(--line-soft);outline:none}.sub-row__bar{height:4px;background:var(--line-soft);border-radius:999px;overflow:hidden}.sub-row__bar-fill{height:100%;border-radius:999px;transition:width .4s cubic-bezier(.16,1,.3,1)}.session-groups{display:flex;flex-direction:column;gap:20px}.session-group{display:flex;flex-direction:column;gap:10px}.session-group__head{display:flex;align-items:baseline;justify-content:space-between;gap:16px;padding:4px 6px 8px;border-bottom:1px solid var(--line);flex-wrap:wrap}.session-group__date{font-family:var(--font-mono, ui-monospace, monospace);font-size:13px;font-weight:600;color:var(--ink);letter-spacing:.02em}.session-group__totals{display:inline-flex;align-items:baseline;gap:8px;font-family:var(--font-mono, ui-monospace, monospace);font-size:12px;color:var(--ink-mute);font-variant-numeric:tabular-nums}.session-group__sep{color:var(--ink-faint)}@media (max-width: 640px){.session-group__head{flex-direction:column;align-items:flex-start;gap:4px}}.day-tabs{display:flex;flex-direction:column;gap:14px}.day-tabs__bar{display:flex;align-items:stretch;gap:6px;overflow-x:auto;scrollbar-width:thin;border-bottom:1px solid var(--line);padding-bottom:2px;-webkit-overflow-scrolling:touch}.day-tab{flex:0 0 auto;display:inline-flex;align-items:center;gap:8px;padding:8px 14px;background:transparent;border:1px solid transparent;border-bottom:2px solid transparent;border-radius:10px 10px 0 0;font-family:inherit;font-size:13px;color:var(--ink-mute);cursor:pointer;transition:color .15s ease,background-color .15s ease,border-color .15s ease;white-space:nowrap}.day-tab:hover{color:var(--ink);background:var(--line-soft)}.day-tab:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}.day-tab--active{color:var(--ink);font-weight:600;border-bottom-color:var(--accent);background:var(--surface)}.day-tab__date{font-family:var(--font-mono, ui-monospace, monospace);font-size:12px;letter-spacing:.02em}.day-tab__count{display:inline-flex;align-items:center;justify-content:center;min-width:22px;height:18px;padding:0 6px;border-radius:999px;background:var(--line-soft);color:var(--ink-mute);font-family:var(--font-mono, ui-monospace, monospace);font-size:11px;font-variant-numeric:tabular-nums}.day-tab--active .day-tab__count{background:var(--accent-soft);color:var(--accent-deep)}.day-tabs__panel{display:flex;flex-direction:column;gap:10px}.day-tabs__meta{display:inline-flex;align-items:baseline;flex-wrap:wrap;gap:8px;padding:0 4px;font-family:var(--font-mono, ui-monospace, monospace);font-size:12px;color:var(--ink-mute);font-variant-numeric:tabular-nums}.day-tabs__meta-strong{color:var(--ink);font-weight:600}.day-tabs__meta-sep{color:var(--ink-faint)}@media (max-width: 640px){.day-tabs__meta{flex-direction:column;align-items:flex-start;gap:2px}.day-tabs__meta-sep{display:none}}.btn-ghost{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;background:transparent;border:1px solid var(--line);border-radius:8px;font-family:inherit;font-size:12px;color:var(--ink-mute);cursor:pointer;transition:color .15s ease,border-color .15s ease,background-color .15s ease}.btn-ghost:hover{color:var(--ink);border-color:var(--ink-mute);background:var(--line-soft)}.btn-ghost:focus-visible{outline:2px solid var(--accent);outline-offset:2px}.transcript{--tx-bg: #0b0f14;--tx-bg-soft: #131922;--tx-line: #1f2937;--tx-ink: #e5e7eb;--tx-ink-dim: #94a3b8;--tx-ink-mute: #64748b;--tx-user: #60a5fa;--tx-ai: #34d399;--tx-tool: #fbbf24;--tx-err: #f87171;background:var(--tx-bg);color:var(--tx-ink);border:1px solid var(--line);border-radius:14px;overflow:hidden;font-family:var(--font-mono, ui-monospace, "JetBrains Mono", "Geist Mono", Menlo, monospace);font-size:12.5px;line-height:1.55}.transcript__header{display:flex;align-items:center;gap:8px;padding:10px 14px;background:var(--tx-bg-soft);border-bottom:1px solid var(--tx-line)}.transcript__dot{width:10px;height:10px;border-radius:999px;display:inline-block}.transcript__dot--r{background:#ef4444}.transcript__dot--y{background:#f59e0b}.transcript__dot--g{background:#10b981}.transcript__title{margin-left:8px;font-size:11px;color:var(--tx-ink-mute);letter-spacing:.02em}.transcript__body{list-style:none;margin:0;padding:8px 0;max-height:70vh;overflow:auto;scrollbar-width:thin;scrollbar-color:var(--tx-line) var(--tx-bg)}.transcript__body::-webkit-scrollbar{width:10px;height:10px}.transcript__body::-webkit-scrollbar-thumb{background:var(--tx-line);border-radius:999px}.tx-msg,.tx-tool{display:grid;grid-template-columns:44px 70px 64px 1fr;gap:10px;padding:8px 16px;align-items:start;border-left:3px solid transparent}.tx-msg+.tx-msg,.tx-tool+.tx-tool,.tx-msg+.tx-tool,.tx-tool+.tx-msg{border-top:1px solid var(--tx-line)}.tx-msg--user{border-left-color:var(--tx-user)}.tx-msg--assistant{border-left-color:var(--tx-ai)}.tx-msg--err{border-left-color:var(--tx-err);background:#f871710d}.tx-tool{border-left-color:var(--tx-tool);background:#fbbf2408}.tx-tool--err{border-left-color:var(--tx-err);background:#f8717112}.tx-gutter{color:var(--tx-ink-mute);font-size:10.5px;letter-spacing:.04em;-webkit-user-select:none;user-select:none;text-align:right}.tx-time{color:var(--tx-ink-mute);font-size:11px;font-variant-numeric:tabular-nums}.tx-tag{font-size:10.5px;font-weight:600;letter-spacing:.08em;text-transform:uppercase;padding:1px 6px;border-radius:4px;text-align:center;white-space:nowrap;height:fit-content}.tx-tag--user{color:var(--tx-bg);background:var(--tx-user)}.tx-tag--assistant{color:var(--tx-bg);background:var(--tx-ai)}.tx-tag--tool{color:var(--tx-bg);background:var(--tx-tool)}.tx-payload{min-width:0;display:flex;flex-direction:column;gap:4px}.tx-text{margin:0;font-family:inherit;font-size:12.5px;color:var(--tx-ink);white-space:pre-wrap;word-break:break-word}.tx-empty{color:var(--tx-ink-mute);font-style:italic;font-size:11.5px}.tx-meta{font-size:10.5px;color:var(--tx-ink-mute);letter-spacing:.02em}.tx-tool__name{color:var(--tx-tool);font-weight:600}.tx-tool__status{display:inline-block;margin-left:8px;padding:0 6px;border-radius:999px;font-size:10px;letter-spacing:.06em;text-transform:uppercase;border:1px solid var(--tx-line);color:var(--tx-ink-dim)}.tx-tool__status--completed{color:var(--tx-ai);border-color:#34d39966}.tx-tool__status--error{color:var(--tx-err);border-color:#f8717166}.tx-tool__status--running,.tx-tool__status--pending{color:var(--tx-tool);border-color:#fbbf2466}.tx-tool__desc{display:block;margin-top:4px;color:var(--tx-ink-dim);font-size:11.5px;word-break:break-word}@media (max-width: 640px){.tx-msg,.tx-tool{grid-template-columns:28px 1fr;grid-template-rows:auto auto;gap:4px 8px}.tx-time,.tx-tag{grid-column:2;justify-self:start;margin-right:6px;display:inline-block}.tx-payload{grid-column:1 / -1}}.transcript--full .transcript__body{max-height:none}.tx-page-head{margin-bottom:20px}.tx-page-head__top{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.tx-page-head__meta{font-family:var(--font-mono, ui-monospace, monospace);font-size:11px;color:var(--ink-mute)}.tx-page-head__title{margin:0 0 8px;font-size:22px;font-weight:600;color:var(--ink);letter-spacing:-.005em}.tx-page-head__sub{display:inline-flex;flex-wrap:wrap;align-items:baseline;gap:8px;font-family:var(--font-mono, ui-monospace, monospace);font-size:12px;color:var(--ink-mute)}.tx-page-head__sub .sep{color:var(--ink-faint)}