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
|
@@ -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
|
+
};
|