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,141 @@
1
+ /**
2
+ * Analysis Job API routes for Agent Boss.
3
+ *
4
+ * Provides endpoints for checking analysis status and manually triggering
5
+ * the analysis job.
6
+ *
7
+ * @author Felix
8
+ */
9
+
10
+ const router = require('express').Router();
11
+
12
+ const { getAnalysisState, getSessionById } = require('../db/queries');
13
+ const { runAnalysisJob, analyzeAndStoreSession } = require('../analysis/job');
14
+ const { loadAdvice } = require('../llm/advice');
15
+ const { saveDb } = require('../db/connection');
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Routes
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /**
22
+ * Create the analysis router with database access.
23
+ *
24
+ * @param {object} db sql.js Database instance
25
+ * @returns {import('express').Router}
26
+ */
27
+ module.exports = function (db) {
28
+ // GET /api/analysis/status
29
+ router.get('/status', (_req, res) => {
30
+ try {
31
+ const data = getAnalysisState(db);
32
+
33
+ res.json({
34
+ ok: true,
35
+ data: data || {
36
+ status: 'idle',
37
+ current_date: null,
38
+ analyzed_count: 0,
39
+ total_count: 0,
40
+ last_analyzed_at: null,
41
+ },
42
+ meta: {
43
+ generated_at: new Date().toISOString(),
44
+ },
45
+ });
46
+ } catch (err) {
47
+ res.status(500).json({
48
+ ok: false,
49
+ error: { code: 'ANALYSIS_ERROR', message: err.message },
50
+ });
51
+ }
52
+ });
53
+
54
+ // POST /api/analysis/trigger
55
+ // Body (optional): { force: boolean, days: number, dates: string[] }
56
+ // force — reanalyze sessions that already have an analysis row
57
+ // days — how far back to scan (default 7); ignored when dates is given
58
+ // dates — explicit YYYY-MM-DD list to analyze (overrides days)
59
+ router.post('/trigger', (req, res) => {
60
+ try {
61
+ // Check if already running
62
+ const state = getAnalysisState(db);
63
+ if (state && state.status === 'running') {
64
+ return res.status(409).json({
65
+ ok: false,
66
+ error: { code: 'ALREADY_RUNNING', message: 'Analysis job is already running' },
67
+ });
68
+ }
69
+
70
+ const force = req.body?.force === true || req.body?.force === '1';
71
+ const days = Number.isFinite(Number(req.body?.days)) && Number(req.body.days) > 0
72
+ ? Math.min(Number(req.body.days), 365)
73
+ : 7;
74
+
75
+ // Optional explicit date list (YYYY-MM-DD). Filter out anything that
76
+ // doesn't match the format to keep the job loop honest.
77
+ const dateRe = /^\d{4}-\d{2}-\d{2}$/;
78
+ const dates = Array.isArray(req.body?.dates)
79
+ ? req.body.dates.filter((d) => typeof d === 'string' && dateRe.test(d))
80
+ : null;
81
+
82
+ const opts = { forceReanalyze: force, days };
83
+ if (dates && dates.length > 0) opts.dates = dates;
84
+
85
+ // Start analysis in the background (fire-and-forget)
86
+ runAnalysisJob(db, opts).catch(() => {
87
+ // Error handling is internal to the job; analysis_state will reflect
88
+ // final status regardless.
89
+ });
90
+
91
+ res.json({
92
+ ok: true,
93
+ data: { status: 'started', force, days: dates ? null : days, dates },
94
+ meta: {
95
+ generated_at: new Date().toISOString(),
96
+ },
97
+ });
98
+ } catch (err) {
99
+ res.status(500).json({
100
+ ok: false,
101
+ error: { code: 'ANALYSIS_ERROR', message: err.message },
102
+ });
103
+ }
104
+ });
105
+
106
+ // POST /api/analysis/session/:id
107
+ // Re-analyze ONE session with the combined LLM call (scores + advice),
108
+ // bypassing the analyzer cache. Returns both halves so the session
109
+ // detail page can refresh the radar and the advice in one round-trip.
110
+ router.post('/session/:id', async (req, res) => {
111
+ const session = getSessionById(db, req.params.id);
112
+ if (!session) {
113
+ return res.status(404).json({
114
+ ok: false,
115
+ error: { code: 'NOT_FOUND', message: 'Session not found' },
116
+ });
117
+ }
118
+ try {
119
+ const v2 = await analyzeAndStoreSession(db, session, { force: true });
120
+ try { saveDb(); } catch (_) { /* auto-save will catch up */ }
121
+ return res.json({
122
+ ok: true,
123
+ data: {
124
+ difficulty: v2 ? v2.difficulty.bucket : null,
125
+ scores: v2 ? v2.scores : null,
126
+ levels: v2 ? v2.levels : null,
127
+ judgeSource: v2 ? v2.judgeSource : null,
128
+ advice: loadAdvice(db, session.id) || null,
129
+ },
130
+ meta: { generated_at: new Date().toISOString() },
131
+ });
132
+ } catch (err) {
133
+ return res.status(500).json({
134
+ ok: false,
135
+ error: { code: 'ANALYSIS_ERROR', message: err.message },
136
+ });
137
+ }
138
+ });
139
+
140
+ return router;
141
+ };
@@ -0,0 +1,330 @@
1
+ /**
2
+ * Advice execution API.
3
+ *
4
+ * POST /api/execution/start { sessionId, adviceKey, executor?, ephemeral? }
5
+ * POST /api/execution/project/start { project, scope, from?, to?, adviceKey, executor?, ephemeral? }
6
+ * POST /api/execution/cancel/:runId
7
+ * GET /api/execution/:runId ?full=1 → return full stdout/stderr
8
+ * GET /api/execution/advice/:sessionId
9
+ * GET /api/execution/project/advice ?project=&scope=&from=&to=
10
+ * GET /api/execution/whitelist
11
+ * POST /api/execution/whitelist { path }
12
+ * DELETE /api/execution/whitelist { path }
13
+ *
14
+ * Spec: docs/superpowers/specs/2026-06-13-advice-execution-design.md §9
15
+ * (project-level extension: see 2026-06-15 project-advice-execution note)
16
+ *
17
+ * @author Felix
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const router = require('express').Router();
23
+ const path = require('path');
24
+
25
+ const job = require('../execution/job');
26
+ const { extractJson } = require('../llm/cli-runner');
27
+
28
+ // ---------------------------------------------------------------------------
29
+ // Constants
30
+ // ---------------------------------------------------------------------------
31
+
32
+ const LOG_PREVIEW_BYTES = 8 * 1024; // 8 KB returned by default
33
+ const VALID_EXECUTORS = ['opencode', 'claude'];
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Error mapping
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const REASON_TO_STATUS = {
40
+ 'already-running': 409,
41
+ 'no-session': 404,
42
+ 'no-project': 400,
43
+ 'no-window': 400,
44
+ 'no-advice': 404,
45
+ 'no-advice-item': 404,
46
+ 'not-actionable': 409,
47
+ 'invalid-project-path': 409,
48
+ 'not-whitelisted': 409,
49
+ 'no-cli': 409,
50
+ 'not-running': 409,
51
+ 'internal': 500,
52
+ };
53
+
54
+ const REASON_TO_MESSAGE = {
55
+ 'already-running': 'Another execution is already in flight',
56
+ 'no-session': 'Session not found',
57
+ 'no-project': 'project required',
58
+ 'no-window': 'scope=daily|weekly requires from/to',
59
+ 'no-advice': 'No cached advice for this entity — generate it first',
60
+ 'no-advice-item': 'AdviceItem not found at given adviceKey',
61
+ 'not-actionable': 'AdviceItem is not marked actionable',
62
+ 'invalid-project-path': 'Project path does not exist or is not a directory',
63
+ 'not-whitelisted': 'Project path is not in the execution whitelist',
64
+ 'no-cli': 'Requested executor CLI is not on PATH',
65
+ 'not-running': 'No active run with that id',
66
+ 'internal': 'Internal execution error',
67
+ };
68
+
69
+ function failure(res, reason, extra) {
70
+ const status = REASON_TO_STATUS[reason] || 500;
71
+ const message = (extra && extra.error) || REASON_TO_MESSAGE[reason] || reason;
72
+ const body = { ok: false, error: { code: reason.toUpperCase().replace(/-/g, '_'), message } };
73
+ if (extra && extra.extra) body.error.details = extra.extra;
74
+ return res.status(status).json(body);
75
+ }
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Row → API shape
79
+ // ---------------------------------------------------------------------------
80
+
81
+ function projectRun(row, { full = false } = {}) {
82
+ if (!row) return null;
83
+ const stdoutFull = row.stdout || '';
84
+ const stderrFull = row.stderr || '';
85
+ let stdout = stdoutFull, stderr = stderrFull;
86
+ let stdoutTruncated = false, stderrTruncated = false;
87
+ if (!full) {
88
+ if (stdoutFull.length > LOG_PREVIEW_BYTES) {
89
+ stdout = stdoutFull.slice(0, LOG_PREVIEW_BYTES);
90
+ stdoutTruncated = true;
91
+ }
92
+ if (stderrFull.length > LOG_PREVIEW_BYTES) {
93
+ stderr = stderrFull.slice(0, LOG_PREVIEW_BYTES);
94
+ stderrTruncated = true;
95
+ }
96
+ }
97
+
98
+ // Derive summary / files_changed / notes from the JSON the executor was
99
+ // asked to emit at the end of its stdout. Tolerate non-JSON / missing.
100
+ let summary = null, filesChanged = null, notes = null, ok = null;
101
+ const parsed = extractJson(stdoutFull);
102
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
103
+ if (typeof parsed.ok === 'boolean') ok = parsed.ok;
104
+ if (typeof parsed.summary === 'string') summary = parsed.summary;
105
+ if (Array.isArray(parsed.files_changed)) {
106
+ filesChanged = parsed.files_changed.filter((x) => typeof x === 'string');
107
+ }
108
+ if (typeof parsed.notes === 'string') notes = parsed.notes;
109
+ }
110
+
111
+ let adviceSnapshot = null;
112
+ if (row.advice_snapshot) {
113
+ try { adviceSnapshot = JSON.parse(row.advice_snapshot); }
114
+ catch { adviceSnapshot = null; }
115
+ }
116
+
117
+ // Scope info (new in the project-execution extension). Older rows
118
+ // default to scope='session' so the API shape stays consistent across
119
+ // pre-/post-migration data.
120
+ let scopeMeta = null;
121
+ if (row.scope_meta) {
122
+ try { scopeMeta = JSON.parse(row.scope_meta); }
123
+ catch { scopeMeta = null; }
124
+ }
125
+
126
+ return {
127
+ id: row.id,
128
+ scope: row.scope || 'session',
129
+ scopeId: row.scope_id || row.session_id,
130
+ scopeMeta,
131
+ sessionId: row.session_id || null,
132
+ adviceKey: row.advice_key,
133
+ project: row.project,
134
+ executor: row.executor,
135
+ status: row.status,
136
+ startedAt: row.started_at,
137
+ endedAt: row.ended_at,
138
+ durationMs: row.duration_ms,
139
+ exitCode: row.exit_code,
140
+ error: row.error,
141
+ stdout,
142
+ stderr,
143
+ stdoutTruncated,
144
+ stderrTruncated,
145
+ summary,
146
+ filesChanged,
147
+ notes,
148
+ reportedOk: ok,
149
+ adviceSnapshot,
150
+ };
151
+ }
152
+
153
+ // ---------------------------------------------------------------------------
154
+ // Routes
155
+ // ---------------------------------------------------------------------------
156
+
157
+ module.exports = function (db) {
158
+ // -------------------------------------------------------------------------
159
+ // Whitelist
160
+ // -------------------------------------------------------------------------
161
+
162
+ router.get('/whitelist', (_req, res) => {
163
+ res.json({ ok: true, data: { paths: job.getWhitelist(db) } });
164
+ });
165
+
166
+ router.post('/whitelist', (req, res) => {
167
+ const p = req.body?.path;
168
+ if (typeof p !== 'string' || !p.trim()) {
169
+ return res.status(400).json({
170
+ ok: false, error: { code: 'BAD_REQUEST', message: 'path required' },
171
+ });
172
+ }
173
+ const resolved = path.resolve(p.trim());
174
+ const paths = job.addToWhitelist(db, resolved);
175
+ res.json({ ok: true, data: { paths, added: resolved } });
176
+ });
177
+
178
+ router.delete('/whitelist', (req, res) => {
179
+ const p = req.body?.path;
180
+ if (typeof p !== 'string' || !p.trim()) {
181
+ return res.status(400).json({
182
+ ok: false, error: { code: 'BAD_REQUEST', message: 'path required' },
183
+ });
184
+ }
185
+ const paths = job.removeFromWhitelist(db, path.resolve(p.trim()));
186
+ res.json({ ok: true, data: { paths } });
187
+ });
188
+
189
+ // -------------------------------------------------------------------------
190
+ // Start
191
+ // -------------------------------------------------------------------------
192
+
193
+ router.post('/start', async (req, res) => {
194
+ const { sessionId, adviceKey, executor, ephemeral } = req.body || {};
195
+ if (typeof sessionId !== 'string' || typeof adviceKey !== 'string') {
196
+ return res.status(400).json({
197
+ ok: false,
198
+ error: { code: 'BAD_REQUEST', message: 'sessionId and adviceKey required' },
199
+ });
200
+ }
201
+ if (executor !== undefined && !VALID_EXECUTORS.includes(executor)) {
202
+ return res.status(400).json({
203
+ ok: false,
204
+ error: { code: 'BAD_REQUEST', message: 'executor must be opencode or claude' },
205
+ });
206
+ }
207
+ const result = await job.startExecution(db, {
208
+ sessionId,
209
+ adviceKey,
210
+ executor,
211
+ ephemeral: ephemeral === true || ephemeral === '1',
212
+ });
213
+ if (!result.ok) return failure(res, result.reason, result);
214
+ return res.status(201).json({
215
+ ok: true,
216
+ data: { runId: result.runId, run: projectRun(result.run) },
217
+ });
218
+ });
219
+
220
+ // -------------------------------------------------------------------------
221
+ // Cancel
222
+ // -------------------------------------------------------------------------
223
+
224
+ router.post('/cancel/:runId', async (req, res) => {
225
+ const runId = req.params.runId;
226
+ const result = await job.cancelRun(db, runId);
227
+ if (!result.ok) return failure(res, result.reason);
228
+ const row = job.getRun(db, runId);
229
+ res.json({ ok: true, data: { run: projectRun(row) } });
230
+ });
231
+
232
+ // -------------------------------------------------------------------------
233
+ // Project-level start + list
234
+ //
235
+ // POST /api/execution/project/start
236
+ // body: { project, scope, from?, to?, adviceKey, executor?, ephemeral? }
237
+ //
238
+ // GET /api/execution/project/advice
239
+ // query: project, scope, from?, to?
240
+ // response: { runsByAdviceKey: { 'cost.0': [run, …], … } }
241
+ // -------------------------------------------------------------------------
242
+
243
+ router.post('/project/start', async (req, res) => {
244
+ const { project, scope, from, to, adviceKey, executor, ephemeral } = req.body || {};
245
+ if (typeof project !== 'string' || !project.trim()) {
246
+ return res.status(400).json({
247
+ ok: false, error: { code: 'BAD_REQUEST', message: 'project required' },
248
+ });
249
+ }
250
+ if (typeof scope !== 'string' || !['daily', 'weekly', 'all'].includes(scope)) {
251
+ return res.status(400).json({
252
+ ok: false, error: { code: 'BAD_REQUEST', message: 'scope must be daily|weekly|all' },
253
+ });
254
+ }
255
+ if (typeof adviceKey !== 'string') {
256
+ return res.status(400).json({
257
+ ok: false, error: { code: 'BAD_REQUEST', message: 'adviceKey required' },
258
+ });
259
+ }
260
+ if (executor !== undefined && !VALID_EXECUTORS.includes(executor)) {
261
+ return res.status(400).json({
262
+ ok: false, error: { code: 'BAD_REQUEST', message: 'executor must be opencode or claude' },
263
+ });
264
+ }
265
+ const result = await job.startProjectExecution(db, {
266
+ project,
267
+ scope,
268
+ windowFrom: from || '',
269
+ windowTo: to || '',
270
+ adviceKey,
271
+ executor,
272
+ ephemeral: ephemeral === true || ephemeral === '1',
273
+ });
274
+ if (!result.ok) return failure(res, result.reason, result);
275
+ return res.status(201).json({
276
+ ok: true,
277
+ data: { runId: result.runId, run: projectRun(result.run) },
278
+ });
279
+ });
280
+
281
+ router.get('/project/advice', (req, res) => {
282
+ const project = req.query.project;
283
+ const scope = req.query.scope;
284
+ const from = req.query.from || '';
285
+ const to = req.query.to || '';
286
+ if (typeof project !== 'string' || !project.trim()) {
287
+ return res.status(400).json({
288
+ ok: false, error: { code: 'BAD_REQUEST', message: 'project required' },
289
+ });
290
+ }
291
+ if (typeof scope !== 'string' || !['daily', 'weekly', 'all'].includes(scope)) {
292
+ return res.status(400).json({
293
+ ok: false, error: { code: 'BAD_REQUEST', message: 'scope must be daily|weekly|all' },
294
+ });
295
+ }
296
+ const map = job.listRunsForProjectAdvice(db, project, scope, from, to);
297
+ const out = {};
298
+ for (const k of Object.keys(map)) {
299
+ out[k] = map[k].map((r) => projectRun(r));
300
+ }
301
+ res.json({ ok: true, data: { runsByAdviceKey: out } });
302
+ });
303
+
304
+ // -------------------------------------------------------------------------
305
+ // Read one
306
+ // -------------------------------------------------------------------------
307
+
308
+ router.get('/advice/:sessionId', (req, res) => {
309
+ const sessionId = req.params.sessionId;
310
+ const map = job.listRunsForAdvice(db, sessionId);
311
+ const out = {};
312
+ for (const k of Object.keys(map)) {
313
+ out[k] = map[k].map((r) => projectRun(r));
314
+ }
315
+ res.json({ ok: true, data: { runsByAdviceKey: out } });
316
+ });
317
+
318
+ router.get('/:runId', (req, res) => {
319
+ const full = req.query.full === '1' || req.query.full === 'true';
320
+ const row = job.getRun(db, req.params.runId);
321
+ if (!row) {
322
+ return res.status(404).json({
323
+ ok: false, error: { code: 'NOT_FOUND', message: 'Run not found' },
324
+ });
325
+ }
326
+ res.json({ ok: true, data: { run: projectRun(row, { full }) } });
327
+ });
328
+
329
+ return router;
330
+ };