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
@@ -0,0 +1,332 @@
1
+ /**
2
+ * Project-level AI-advice prompt builder.
3
+ *
4
+ * Where session advice asks the model to read ONE conversation, project
5
+ * advice asks the model to read N already-generated session advice
6
+ * payloads under the same project and produce a SECOND-ORDER summary:
7
+ *
8
+ * "Across these 12 sessions in C:/felix/code/AgentBoss this week,
9
+ * what patterns recur? Where is this developer systematically
10
+ * losing money / accuracy / context? Which skill / workflow change
11
+ * would compound across multiple sessions?"
12
+ *
13
+ * # Inputs
14
+ *
15
+ * The caller (server/llm/project-advice.js) gives us a `ProjectContext`:
16
+ *
17
+ * {
18
+ * project: 'C:/felix/code/AgentBoss',
19
+ * scope: 'daily' | 'weekly' | 'all',
20
+ * windowFrom: '2026-06-12',
21
+ * windowTo: '2026-06-12',
22
+ * stats: { sessionCount, totalCost, totalTokens, totalErrors, totalActiveMinutes },
23
+ * sessions: [{ id, title, model, date, cost, msgCount, advice: {…} }, …]
24
+ * }
25
+ *
26
+ * `advice` for each session is the already-cached `session_analysis.llm_advice`
27
+ * payload (categories, summary, rationale). We feed the model the
28
+ * second-pass material — NOT the raw transcripts — so the prompt stays
29
+ * manageable even for projects with 50+ sessions.
30
+ *
31
+ * # Why not raw transcripts?
32
+ *
33
+ * (a) Cost / time: even 10 sessions × 80 KB each blows past any sane
34
+ * prompt budget and would take minutes to judge.
35
+ * (b) Duplication: per-session advice already extracted the salient
36
+ * facts. Re-deriving them is just paying twice for the same insight.
37
+ * (c) Cross-session patterns are easier to spot when the model sees
38
+ * compact summaries side by side than when buried in transcripts.
39
+ *
40
+ * # Output contract (mirrors session advice + crossSessionPatterns)
41
+ *
42
+ * {
43
+ * "summary": "≤80 字总评",
44
+ * "crossSessionPatterns": ["..."], // 0-5 一句话总结的跨会话模式
45
+ * "categories": {
46
+ * "cost": [AdviceItem, …],
47
+ * "accuracy": [AdviceItem, …],
48
+ * "context": [AdviceItem, …],
49
+ * "skills": [AdviceItem, …],
50
+ * "workflow": [AdviceItem, …]
51
+ * },
52
+ * "rationale": "≤120 字综合理由"
53
+ * }
54
+ *
55
+ * AdviceItem shape matches session advice's normaliseItem(), but
56
+ * `evidence` here cites SESSION IDs / patterns rather than message
57
+ * numbers, e.g. "出现于 12 个会话中 9 个 (sess-abc, sess-def, ...)".
58
+ *
59
+ * # Sentinel + versioning
60
+ *
61
+ * Re-uses ADVICE_SENTINEL so server/etl/judge-filter.js still filters
62
+ * the helper-LLM calls out of the user's own data. Has its own
63
+ * VERSION constant so bumping the project prompt doesn't invalidate
64
+ * the session-level cache.
65
+ *
66
+ * @author Felix
67
+ */
68
+
69
+ 'use strict';
70
+
71
+ const { ADVICE_SENTINEL } = require('./advice-prompt');
72
+
73
+ /** Bump when project-advice OUTPUT contract changes. */
74
+ const PROJECT_ADVICE_PROMPT_VERSION = 1;
75
+
76
+ /** Hard categories — same enum the session prompt uses. */
77
+ const CATEGORIES = ['cost', 'accuracy', 'context', 'skills', 'workflow'];
78
+
79
+ /** Soft prompt budget. Project advice fits much more comfortably than
80
+ * session advice because per-session payloads are compact (~2–4 KB). */
81
+ const DEFAULT_MAX_BYTES = 80_000;
82
+
83
+ /** Cap per-session block when summaries are unusually verbose. */
84
+ const PER_SESSION_HARD_CAP_CHARS = 1_500;
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Truncation
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /**
91
+ * Reduce the per-session blocks so the assembled prompt stays under
92
+ * `maxBytes`. Strategy:
93
+ * 1. If total fits → no-op.
94
+ * 2. Truncate each session's serialised advice to PER_SESSION_HARD_CAP_CHARS.
95
+ * 3. Still over? Drop the oldest sessions first (keep most recent N).
96
+ * 4. Mark ctx.truncated accordingly so the prompt can disclose this.
97
+ *
98
+ * @param {object} ctx
99
+ * @param {number} [maxBytes=DEFAULT_MAX_BYTES]
100
+ * @returns {object} possibly-truncated ctx (shallow copy)
101
+ */
102
+ function truncateContext(ctx, maxBytes = DEFAULT_MAX_BYTES) {
103
+ const out = { ...ctx, truncated: false, omittedSessions: 0 };
104
+ const sessions = Array.isArray(ctx.sessions) ? ctx.sessions.slice() : [];
105
+ out.sessions = sessions;
106
+
107
+ const estimate = (list) => {
108
+ const FIXED_OVERHEAD = 6_000;
109
+ return FIXED_OVERHEAD + list.reduce((n, s) => n + serialisedLength(s), 0);
110
+ };
111
+
112
+ if (estimate(sessions) <= maxBytes) return out;
113
+
114
+ // 2. Hard-cap each session's advice serialisation.
115
+ for (const s of sessions) {
116
+ if (!s._serialised) continue;
117
+ if (s._serialised.length > PER_SESSION_HARD_CAP_CHARS) {
118
+ s._serialised = s._serialised.slice(0, PER_SESSION_HARD_CAP_CHARS) + '…[trimmed]';
119
+ }
120
+ }
121
+ if (estimate(sessions) <= maxBytes) {
122
+ out.truncated = true;
123
+ return out;
124
+ }
125
+
126
+ // 3. Drop oldest sessions until we fit (keep at least 2).
127
+ while (out.sessions.length > 2 && estimate(out.sessions) > maxBytes) {
128
+ out.sessions.shift();
129
+ out.omittedSessions++;
130
+ }
131
+ out.truncated = 'hard';
132
+ return out;
133
+ }
134
+
135
+ function serialisedLength(s) {
136
+ return s && s._serialised ? s._serialised.length : 0;
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Per-session serialisation
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /**
144
+ * Render a single session's advice payload into a compact text block
145
+ * suitable for inclusion in the prompt. Only writes fields that
146
+ * carry signal — empty categories are dropped entirely.
147
+ */
148
+ function serialiseSessionAdvice(sess) {
149
+ const lines = [];
150
+ lines.push(`## SESSION ${sess.id}`);
151
+ const meta = [];
152
+ if (sess.title) meta.push(`title="${sess.title.slice(0, 80)}"`);
153
+ if (sess.date) meta.push(`date=${sess.date}`);
154
+ if (sess.model) meta.push(`model=${sess.model}`);
155
+ if (sess.cost != null) meta.push(`cost=$${Number(sess.cost).toFixed(4)}`);
156
+ if (sess.msgCount != null) meta.push(`msgs=${sess.msgCount}`);
157
+ if (sess.errorCount) meta.push(`errors=${sess.errorCount}`);
158
+ if (meta.length) lines.push(meta.join(' · '));
159
+
160
+ const adv = sess.advice || {};
161
+ if (adv.summary) lines.push(`summary: ${adv.summary}`);
162
+
163
+ const cats = adv.categories || {};
164
+ for (const key of CATEGORIES) {
165
+ const items = Array.isArray(cats[key]) ? cats[key] : [];
166
+ if (!items.length) continue;
167
+ lines.push(`[${key}]`);
168
+ for (const it of items) {
169
+ const sev = it.severity || 'low';
170
+ const title = (it.title || '').slice(0, 80);
171
+ const why = (it.why || '').slice(0, 200);
172
+ lines.push(` - (${sev}) ${title}`);
173
+ if (why) lines.push(` why: ${why}`);
174
+ }
175
+ }
176
+ return lines.join('\n');
177
+ }
178
+
179
+ /**
180
+ * Mutate ctx in place: attach `_serialised` to each session so
181
+ * truncateContext can measure / trim.
182
+ */
183
+ function annotateContext(ctx) {
184
+ for (const s of ctx.sessions || []) {
185
+ s._serialised = serialiseSessionAdvice(s);
186
+ }
187
+ return ctx;
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Formatting helpers
192
+ // ---------------------------------------------------------------------------
193
+
194
+ function fmtNum(n) {
195
+ if (n == null || Number.isNaN(n)) return '–';
196
+ if (typeof n !== 'number') return String(n);
197
+ if (Number.isInteger(n)) return n.toLocaleString('en-US');
198
+ return n.toFixed(3);
199
+ }
200
+
201
+ function fmtSessionBlocks(sessions) {
202
+ if (!Array.isArray(sessions) || sessions.length === 0) return '(无会话)';
203
+ return sessions.map((s) => s._serialised || serialiseSessionAdvice(s)).join('\n\n');
204
+ }
205
+
206
+ function fmtWindow(ctx) {
207
+ if (ctx.scope === 'all') return '全部历史';
208
+ if (ctx.windowFrom === ctx.windowTo) return ctx.windowFrom;
209
+ return `${ctx.windowFrom} → ${ctx.windowTo}`;
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Prompt
214
+ // ---------------------------------------------------------------------------
215
+
216
+ /**
217
+ * Assemble the full project-advice prompt.
218
+ *
219
+ * @param {object} ctx
220
+ * @returns {string}
221
+ */
222
+ function buildProjectAdvicePrompt(ctx) {
223
+ const stats = ctx.stats || {};
224
+
225
+ const truncatedNote =
226
+ ctx.truncated === 'hard'
227
+ ? `(注意:会话过多,已强制丢弃最早 ${ctx.omittedSessions} 个会话。)`
228
+ : ctx.truncated
229
+ ? '(注意:部分会话摘要已截断。)'
230
+ : '';
231
+
232
+ return `${ADVICE_SENTINEL}(内部标记,忽略本行)
233
+ 你是一位 AI 协作教练。下面给你的不是单次会话,而是「同一个项目下多次会话已经被
234
+ 逐个分析后的结论列表」。你的任务:阅读这些 per-session 结论,做**跨会话二次
235
+ 汇总**,找出在这个项目上反复出现、值得系统性改进的协作模式。
236
+
237
+ # 关键原则:做总结,不要重复
238
+
239
+ 每个 SESSION 块已经写好了该次会话的具体建议。你不需要逐条复述,而是要:
240
+ 1. 找出在多个 session 之间**重复出现**的问题(例:5/12 个 session 都缺
241
+ 初始上下文)。
242
+ 2. 找出在单个 session 看似小事、但**跨会话累计起来代价巨大**的模式
243
+ (例:每次都不开缓存)。
244
+ 3. 找出**项目特定**的改进点(skill / 流程 / 配置),因为只看一次会话
245
+ 时很难判断是否值得建立 skill。
246
+ 4. 反过来,如果某个建议**只在 1 个 session 里出现一次**,不要在项目级
247
+ 再次强调它——那属于个例,不要污染项目级总览。
248
+
249
+ # 评估对象与禁区
250
+
251
+ 可以谈:
252
+ - 跨多个 session 的协作模式(开场习惯、提问粒度、上下文准备)
253
+ - 项目内的成本结构(模型选择、token 使用、缓存命中)
254
+ - 是否应建立 / 完善 opencode skill 或 subagent
255
+ - 工具使用习惯(是否反复使用低效组合)
256
+ - 项目级流程瓶颈(测试节奏、回退频率)
257
+
258
+ 不要谈:
259
+ - 任何具体业务 / 代码层面的对错
260
+ - 单次会话内的局部细节(那应该回到 session 详情看)
261
+ - 任何评分体系(H1/H2/E1/O1/Lx/子分)字眼一律禁止
262
+
263
+ # 输出契约
264
+
265
+ 只输出严格 JSON,不要 markdown 代码块,不要多余文字。结构:
266
+
267
+ {
268
+ "summary": "≤80 字一句话总评(只谈协作)",
269
+ "crossSessionPatterns": ["≤30 字的跨会话模式描述", ...], // 0-5 条
270
+ "categories": {
271
+ "cost": [AdviceItem, ...],
272
+ "accuracy": [AdviceItem, ...],
273
+ "context": [AdviceItem, ...],
274
+ "skills": [AdviceItem, ...],
275
+ "workflow": [AdviceItem, ...]
276
+ },
277
+ "rationale": "≤120 字综合理由(只谈协作)"
278
+ }
279
+
280
+ AdviceItem(项目级):
281
+ {
282
+ "severity": "high" | "medium" | "low",
283
+ "title": "≤24 字",
284
+ "why": "1-2 句话,说明这个问题在多个会话中如何重复或累积",
285
+ "action": "1 句话,项目级别可落地的改变(skill / 配置 / 流程)",
286
+ "evidence": "必须引用具体会话证据,例:出现于 7/12 个会话(sess-abc, sess-def, ...)",
287
+ "actionable": true | false,
288
+ "executor": "opencode" | "claude" | "manual",
289
+ "cwd_hint": "project_root"
290
+ }
291
+
292
+ # 硬规则
293
+
294
+ 1. 5 个 categories 键必须存在,无内容给空数组。
295
+ 2. AdviceItem 总数 ≤ 5 条,按 severity 由高到低排;**严格优先**跨多
296
+ session 重复出现的问题,单例问题不要列入。
297
+ 3. evidence 必须指向具体 session id 或可量化的统计(如 "9/12 会话
298
+ 未提供初始文件路径")。**禁止**仅引用单一会话内的某条消息。
299
+ 4. 出现「分数」「等级」「Lx」「子分」「H1」「H2」「E1」「O1」字眼一律违规。
300
+ 5. action 必须是「项目级」可落地动作(写一个 skill / 改一个配置 / 形成
301
+ 一条惯例),不是「下次开场更具体一些」这种纯口头建议。
302
+ 6. actionable / executor / cwd_hint 规则与单 session 版本相同;manual
303
+ 时 actionable 必须 false。
304
+ 7. 如果没有发现任何值得 project 级别报告的问题(全是个例),把所有
305
+ categories 设为空数组,summary 写「未发现项目级别的系统性协作问题」。
306
+
307
+ # 项目基础
308
+
309
+ 项目: ${ctx.project || '?'}
310
+ 分析范围: ${ctx.scope || '?'} · ${fmtWindow(ctx)}
311
+ 分析的会话数: ${fmtNum(stats.sessionCount)}
312
+ 活跃时长合计: ${fmtNum(stats.totalActiveMinutes)} 分钟
313
+ 总成本: $${typeof stats.totalCost === 'number' ? stats.totalCost.toFixed(4) : '–'}
314
+ 总 token: ${fmtNum(stats.totalTokens)}
315
+ 错误总数: ${fmtNum(stats.totalErrors)}
316
+
317
+ # 各会话的 per-session 结论 ${truncatedNote}
318
+
319
+ ${fmtSessionBlocks(ctx.sessions || [])}
320
+
321
+ —— 输出 JSON ——`;
322
+ }
323
+
324
+ module.exports = {
325
+ PROJECT_ADVICE_PROMPT_VERSION,
326
+ CATEGORIES,
327
+ DEFAULT_MAX_BYTES,
328
+ buildProjectAdvicePrompt,
329
+ truncateContext,
330
+ annotateContext,
331
+ serialiseSessionAdvice,
332
+ };