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.
- package/README.md +34 -0
- package/bin/aboss.js +288 -0
- package/client/dist/assets/index-C1wFD_Vo.css +1 -0
- package/client/dist/assets/index-DBj1Ujlx.js +137 -0
- package/client/dist/index.html +34 -0
- package/package.json +64 -0
- package/server/analysis/daily-aggregator.js +258 -0
- package/server/analysis/difficulty.js +129 -0
- package/server/analysis/dimensions/ai-knowledge.js +172 -0
- package/server/analysis/dimensions/ai-tools.js +161 -0
- package/server/analysis/dimensions/judgement.js +107 -0
- package/server/analysis/dimensions/llm-merge.js +57 -0
- package/server/analysis/dimensions/output-quality.js +167 -0
- package/server/analysis/dimensions/problem-definition.js +104 -0
- package/server/analysis/dimensions/system-thinking.js +225 -0
- package/server/analysis/evidence-builder.js +104 -0
- package/server/analysis/job.js +273 -0
- package/server/analysis/report-builder.js +581 -0
- package/server/analysis/scoring-v2.js +72 -0
- package/server/analysis/text-signals.js +179 -0
- package/server/analysis/thresholds-v2.js +358 -0
- package/server/api/advice.js +124 -0
- package/server/api/analysis.js +141 -0
- package/server/api/execution.js +330 -0
- package/server/api/metrics.js +277 -0
- package/server/api/overview.js +308 -0
- package/server/api/project.js +255 -0
- package/server/api/reports.js +125 -0
- package/server/api/sessions.js +118 -0
- package/server/api/settings.js +119 -0
- package/server/db/connection.js +175 -0
- package/server/db/queries.js +1051 -0
- package/server/db/schema.js +487 -0
- package/server/etl/active-time.js +150 -0
- package/server/etl/backfill-subagents.js +178 -0
- package/server/etl/claude-code.js +826 -0
- package/server/etl/detect.js +341 -0
- package/server/etl/judge-filter.js +117 -0
- package/server/etl/opencode.js +606 -0
- package/server/execution/job.js +662 -0
- package/server/execution/prompt.js +227 -0
- package/server/execution/runner.js +218 -0
- package/server/index.js +94 -0
- package/server/llm/advice-prompt.js +339 -0
- package/server/llm/advice.js +384 -0
- package/server/llm/analysis-prompt.js +162 -0
- package/server/llm/cli-runner.js +249 -0
- package/server/llm/judge-prompts.js +179 -0
- package/server/llm/judge.js +118 -0
- package/server/llm/project-advice-prompt.js +332 -0
- package/server/llm/project-advice.js +491 -0
- package/server/llm/session-analyzer.js +122 -0
- 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)}
|