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,341 @@
1
+ /**
2
+ * Data source auto-detection for Agent Boss
3
+ *
4
+ * Detects installed AI coding tools (OpenCode, Claude Code) by checking
5
+ * default paths and validating data source integrity, then writes results
6
+ * to the tool_config table.
7
+ *
8
+ * Design doc references: §4.1 (dual data sources), §4.4 (degradation), §11 (error handling)
9
+ *
10
+ * @author Felix
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+ const sqlite3 = require('sqlite3');
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Default path resolution
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Get the default path to opencode.db based on platform.
24
+ * All platforms: ~/.local/share/opencode/opencode.db
25
+ * @returns {string}
26
+ */
27
+ function getDefaultOpenCodePath() {
28
+ return path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
29
+ }
30
+
31
+ /**
32
+ * Get the default path to the Claude Code data directory.
33
+ * All platforms: ~/.claude/
34
+ * @returns {string}
35
+ */
36
+ function getDefaultClaudeCodePath() {
37
+ return path.join(os.homedir(), '.claude');
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Path helpers (resolve from tool_config or fall back to default)
42
+ // ---------------------------------------------------------------------------
43
+
44
+ /**
45
+ * Returns the resolved path to opencode.db.
46
+ * If the user configured a custom data_path in tool_config, that is used;
47
+ * otherwise the platform default is returned.
48
+ *
49
+ * @param {object} db - boss.db database instance
50
+ * @returns {string} absolute path to opencode.db
51
+ */
52
+ function getOpenCodeDbPath(db) {
53
+ try {
54
+ const { queryOne } = require('../db/queries');
55
+ const row = queryOne(db, 'SELECT data_path FROM tool_config WHERE tool = ?', ['opencode']);
56
+ if (row && row.data_path) {
57
+ return row.data_path;
58
+ }
59
+ } catch (_) {
60
+ // table may not exist yet; fall through to default
61
+ }
62
+ return getDefaultOpenCodePath();
63
+ }
64
+
65
+ /**
66
+ * Returns the resolved path to the Claude Code data directory (~/.claude/).
67
+ * If the user configured a custom data_path in tool_config, that is used;
68
+ * otherwise the platform default is returned.
69
+ *
70
+ * @param {object} db - boss.db database instance
71
+ * @returns {string} absolute path to ~/.claude/
72
+ */
73
+ function getClaudeCodePath(db) {
74
+ try {
75
+ const { queryOne } = require('../db/queries');
76
+ const row = queryOne(db, 'SELECT data_path FROM tool_config WHERE tool = ?', ['claude-code']);
77
+ if (row && row.data_path) {
78
+ return row.data_path;
79
+ }
80
+ } catch (_) {
81
+ // table may not exist yet; fall through to default
82
+ }
83
+ return getDefaultClaudeCodePath();
84
+ }
85
+
86
+ // ---------------------------------------------------------------------------
87
+ // Validation helpers
88
+ // ---------------------------------------------------------------------------
89
+
90
+ /** The magic bytes that every SQLite file starts with */
91
+ const SQLITE_MAGIC = 'SQLite format 3\0';
92
+
93
+ /**
94
+ * Validate an OpenCode data source.
95
+ *
96
+ * Checks:
97
+ * 1. File exists
98
+ * 2. First 16 bytes match the SQLite magic header
99
+ * 3. The database contains the required tables: session, message, part
100
+ *
101
+ * @param {string} dbPath - absolute path to opencode.db
102
+ * @returns {Promise<{status: string, detail?: string}>}
103
+ */
104
+ async function validateOpenCode(dbPath) {
105
+ // 1. File existence
106
+ if (!fs.existsSync(dbPath)) {
107
+ return { status: 'not_found', detail: `File does not exist: ${dbPath}` };
108
+ }
109
+
110
+ // 2. SQLite magic header
111
+ let fd;
112
+ try {
113
+ fd = fs.openSync(dbPath, 'r');
114
+ const buf = Buffer.alloc(16);
115
+ fs.readSync(fd, buf, 0, 16, 0);
116
+ if (buf.toString('utf8') !== SQLITE_MAGIC) {
117
+ return { status: 'invalid', detail: 'File is not a valid SQLite database (bad magic header)' };
118
+ }
119
+ } catch (err) {
120
+ return { status: 'invalid', detail: `Cannot read file: ${err.message}` };
121
+ } finally {
122
+ if (fd !== undefined) {
123
+ fs.closeSync(fd);
124
+ }
125
+ }
126
+
127
+ // 3. Open with sqlite3 (native, read-only) and verify required tables
128
+ try {
129
+ const result = await new Promise((resolve, reject) => {
130
+ const sourceDb = new sqlite3.Database(dbPath, sqlite3.OPEN_READONLY, (err) => {
131
+ if (err) return reject(err);
132
+ sourceDb.all(
133
+ "SELECT name FROM sqlite_master WHERE type='table' AND name IN ('session','message','part')",
134
+ (err2, rows) => {
135
+ sourceDb.close();
136
+ if (err2) return reject(err2);
137
+ resolve(rows);
138
+ }
139
+ );
140
+ });
141
+ });
142
+
143
+ const foundTables = result.map((row) => row.name);
144
+ const required = ['session', 'message', 'part'];
145
+ const missing = required.filter((t) => !foundTables.includes(t));
146
+
147
+ if (missing.length > 0) {
148
+ return { status: 'invalid', detail: `Missing required tables: ${missing.join(', ')}` };
149
+ }
150
+
151
+ return { status: 'available' };
152
+ } catch (err) {
153
+ return { status: 'invalid', detail: `Failed to open SQLite database: ${err.message}` };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * Validate a Claude Code data source.
159
+ *
160
+ * Checks:
161
+ * 1. Directory exists
162
+ * 2. Contains a projects/ subdirectory
163
+ * 3. At least one JSONL session file exists under projects/
164
+ * 4. First line of the first JSONL has required fields (type, timestamp, sessionId)
165
+ *
166
+ * @param {string} dirPath - absolute path to ~/.claude/
167
+ * @returns {{status: string, detail?: string}}
168
+ */
169
+ function validateClaudeCode(dirPath) {
170
+ // 1. Directory existence
171
+ if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) {
172
+ return { status: 'not_found', detail: `Directory does not exist: ${dirPath}` };
173
+ }
174
+
175
+ // 2. projects/ subdirectory
176
+ const projectsDir = path.join(dirPath, 'projects');
177
+ if (!fs.existsSync(projectsDir) || !fs.statSync(projectsDir).isDirectory()) {
178
+ return { status: 'invalid', detail: 'Missing projects/ subdirectory' };
179
+ }
180
+
181
+ // 3. Collect candidate JSONL files (one per project folder is enough)
182
+ const candidateFiles = [];
183
+ try {
184
+ const projectFolders = fs.readdirSync(projectsDir, { withFileTypes: true })
185
+ .filter((d) => d.isDirectory());
186
+
187
+ for (const folder of projectFolders) {
188
+ const folderPath = path.join(projectsDir, folder.name);
189
+ let files;
190
+ try {
191
+ files = fs.readdirSync(folderPath);
192
+ } catch (_) {
193
+ continue;
194
+ }
195
+ for (const f of files) {
196
+ if (f.endsWith('.jsonl')) {
197
+ candidateFiles.push(path.join(folderPath, f));
198
+ }
199
+ }
200
+ // Cap to avoid scanning huge installs — a handful is enough to validate
201
+ if (candidateFiles.length >= 8) break;
202
+ }
203
+ } catch (err) {
204
+ return { status: 'invalid', detail: `Cannot read projects directory: ${err.message}` };
205
+ }
206
+
207
+ if (candidateFiles.length === 0) {
208
+ return { status: 'invalid', detail: 'No JSONL session files found under projects/' };
209
+ }
210
+
211
+ // 4. Validate: across the candidate files, find at least one event row that
212
+ // looks like an actual user/assistant message. JSONL files can start
213
+ // with metadata rows (`type: "mode"`, `type: "summary"`) that lack
214
+ // `timestamp` — those are skipped by the ETL, so they must not fail
215
+ // validation either.
216
+ const REQUIRED = ['type', 'timestamp', 'sessionId'];
217
+ let lastDetail = 'No valid event rows found in any JSONL';
218
+
219
+ for (const filePath of candidateFiles) {
220
+ let fd;
221
+ try {
222
+ fd = fs.openSync(filePath, 'r');
223
+ const buf = Buffer.alloc(65536);
224
+ const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
225
+ const content = buf.toString('utf8', 0, bytesRead);
226
+ const lines = content.split('\n');
227
+
228
+ for (const rawLine of lines) {
229
+ const line = rawLine.trim();
230
+ if (!line) continue;
231
+ let parsed;
232
+ try {
233
+ parsed = JSON.parse(line);
234
+ } catch (_) {
235
+ // Truncated last line in the buffer — skip
236
+ continue;
237
+ }
238
+ const missing = REQUIRED.filter((f) => !(f in parsed));
239
+ if (missing.length === 0) {
240
+ return { status: 'available' };
241
+ }
242
+ lastDetail = `JSONL missing required fields: ${missing.join(', ')}`;
243
+ }
244
+ } catch (err) {
245
+ lastDetail = `Cannot read JSONL file: ${err.message}`;
246
+ } finally {
247
+ if (fd !== undefined) {
248
+ try { fs.closeSync(fd); } catch (_) { /* ignore */ }
249
+ }
250
+ }
251
+ }
252
+
253
+ return { status: 'invalid', detail: lastDetail };
254
+ }
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Main detection function
258
+ // ---------------------------------------------------------------------------
259
+
260
+ /**
261
+ * Detect installed data sources and update the tool_config table.
262
+ *
263
+ * For each tool (OpenCode, Claude Code):
264
+ * 1. Resolve path (custom data_path from tool_config, or platform default)
265
+ * 2. Validate the data source
266
+ * 3. Upsert the result into tool_config
267
+ *
268
+ * A detection failure for one source never affects the other.
269
+ *
270
+ * @param {object} db - boss.db database instance (better-sqlite3 compatible)
271
+ * @returns {Promise<{opencode: {status: string, path: string}, claudeCode: {status: string, path: string}}>}
272
+ */
273
+ async function detectSources(db) {
274
+ const results = {
275
+ opencode: { status: 'not_found', path: '' },
276
+ claudeCode: { status: 'not_found', path: '' },
277
+ };
278
+
279
+ /**
280
+ * Upsert a tool_config row using sql.js API.
281
+ * Preserves any existing custom data_path.
282
+ */
283
+ function upsertTool(tool, label, status, dataPath) {
284
+ db.run(
285
+ `INSERT INTO tool_config (tool, label, status, data_path, enabled)
286
+ VALUES (?, ?, ?, ?, 1)
287
+ ON CONFLICT(tool) DO UPDATE SET
288
+ status = excluded.status,
289
+ data_path = COALESCE(tool_config.data_path, excluded.data_path)`,
290
+ [tool, label, status, dataPath]
291
+ );
292
+ }
293
+
294
+ // --- OpenCode ---
295
+ try {
296
+ const ocPath = getOpenCodeDbPath(db);
297
+ results.opencode.path = ocPath;
298
+
299
+ const ocResult = await validateOpenCode(ocPath);
300
+ results.opencode.status = ocResult.status;
301
+
302
+ upsertTool('opencode', 'OpenCode', ocResult.status, ocPath);
303
+ } catch (err) {
304
+ results.opencode.status = 'invalid';
305
+ try {
306
+ upsertTool('opencode', 'OpenCode', 'invalid', results.opencode.path || getDefaultOpenCodePath());
307
+ } catch (_) {
308
+ // best-effort
309
+ }
310
+ }
311
+
312
+ // --- Claude Code ---
313
+ try {
314
+ const ccPath = getClaudeCodePath(db);
315
+ results.claudeCode.path = ccPath;
316
+
317
+ const ccResult = validateClaudeCode(ccPath);
318
+ results.claudeCode.status = ccResult.status;
319
+
320
+ upsertTool('claude-code', 'Claude Code', ccResult.status, ccPath);
321
+ } catch (err) {
322
+ results.claudeCode.status = 'invalid';
323
+ try {
324
+ upsertTool('claude-code', 'Claude Code', 'invalid', results.claudeCode.path || getDefaultClaudeCodePath());
325
+ } catch (_) {
326
+ // best-effort
327
+ }
328
+ }
329
+
330
+ return results;
331
+ }
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // Exports
335
+ // ---------------------------------------------------------------------------
336
+
337
+ module.exports = {
338
+ detectSources,
339
+ getOpenCodeDbPath,
340
+ getClaudeCodePath,
341
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Shared ETL helper — recognise sessions created by our own LLM calls.
3
+ *
4
+ * Every aboss-originated LLM invocation (judge E1/O1, per-session advice,
5
+ * anything we add later) goes through a local CLI like `opencode run` or
6
+ * `claude -p`, which logs the call as a session in its own data store.
7
+ * Importing those back would create a feedback loop: each pass spawns
8
+ * sessions, the next ETL imports them as the user's work, they get
9
+ * analyzed and spawn more, and so on.
10
+ *
11
+ * Detection covers, in order:
12
+ * • JUDGE_SENTINEL — first-line marker on judge (E1/O1) prompts
13
+ * • ADVICE_SENTINEL — first-line marker on per-session advice prompts
14
+ * • legacy signatures — prompts sent before sentinels existed
15
+ *
16
+ * New prompt families MUST register their sentinel here.
17
+ *
18
+ * @author Felix
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const { JUDGE_SENTINEL } = require('../llm/judge-prompts');
24
+ const { ADVICE_SENTINEL } = require('../llm/advice-prompt');
25
+ const { ANALYSIS_SENTINEL } = require('../llm/analysis-prompt');
26
+ const { EXECUTION_SENTINEL } = require('../execution/prompt');
27
+
28
+ // Every internal sentinel — extend when adding new prompt families.
29
+ const SENTINELS = [JUDGE_SENTINEL, ADVICE_SENTINEL, ANALYSIS_SENTINEL, EXECUTION_SENTINEL];
30
+
31
+ // Legacy signatures from older prompt versions (pre-JUDGE_SENTINEL).
32
+ // Order matters only for readability — we just OR them together.
33
+ const LEGACY_SIGNATURES = [
34
+ // v2 — Chinese audit prompts
35
+ /^你是一名严格的\s*(AI\s*协作审计员|代码与产出审计员)/,
36
+ // v1 — English five-dimension reviewer prompt (used by initial release)
37
+ /^You are an expert reviewer of human-AI coding collaboration\./,
38
+ ];
39
+
40
+ /**
41
+ * Some historical ETL paths stored the user text after JSON.stringify, so
42
+ * the payload arrives wrapped in `"..."`. Strip a single layer of
43
+ * matching outer quotes before testing the signatures; otherwise the
44
+ * leading `"` defeats the `^You are…` anchor.
45
+ *
46
+ * Edge case: server/etl/opencode.js truncates message text at 4 KB before
47
+ * filtering runs, which can chop off the closing quote — leaving us with
48
+ * `"...<no-trailing-quote>`. In that case the symmetric-quotes branch
49
+ * skips and the leading `"` defeats the anchored signatures. So we also
50
+ * strip an unpaired leading quote as a best-effort fallback.
51
+ */
52
+ function _unwrap(text) {
53
+ const s = text.trimStart();
54
+ if (s.length >= 2) {
55
+ const first = s.charAt(0);
56
+ const last = s.charAt(s.length - 1);
57
+ if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
58
+ try { return JSON.parse(s); } catch (_) { return s.slice(1, -1); }
59
+ }
60
+ // Fallback: a leading quote but no matching trailer (e.g. truncated).
61
+ if (first === '"' || first === "'") return s.slice(1);
62
+ }
63
+ return s;
64
+ }
65
+
66
+ /**
67
+ * True if the given user-message text is one of our judge prompts.
68
+ *
69
+ * @param {string|null} text
70
+ * @returns {boolean}
71
+ */
72
+ function isJudgePrompt(text) {
73
+ if (typeof text !== 'string' || !text) return false;
74
+ const t = _unwrap(text);
75
+ for (const s of SENTINELS) {
76
+ if (t.startsWith(s)) return true;
77
+ }
78
+ for (const re of LEGACY_SIGNATURES) {
79
+ if (re.test(t)) return true;
80
+ }
81
+ return false;
82
+ }
83
+
84
+ /**
85
+ * True if the given session title belongs to one of our own internal
86
+ * `opencode run` invocations (polish/analyze/etc). These sessions always
87
+ * carry an `agent-boss-internal-` prefix as their auto-title, regardless
88
+ * of prompt content, so we can filter them out without inspecting parts.
89
+ */
90
+ function isInternalAbossTitle(title) {
91
+ if (typeof title !== 'string' || !title) return false;
92
+ return title.startsWith('agent-boss-internal-');
93
+ }
94
+
95
+ /**
96
+ * True if the title is OpenCode's auto-generated placeholder for a
97
+ * session that was started programmatically without a meaningful first
98
+ * prompt — e.g. `New session - 2026-06-12T11:14:51.277Z`.
99
+ *
100
+ * These sessions carry no human intent and add only noise to reports,
101
+ * so the ETL drops them outright. Pattern is anchored on a strict
102
+ * ISO-8601 timestamp suffix to avoid swallowing real session titles
103
+ * that happen to start with the words "New session".
104
+ */
105
+ const PLACEHOLDER_TITLE_RE =
106
+ /^New session - \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
107
+
108
+ function isPlaceholderOpencodeTitle(title) {
109
+ if (typeof title !== 'string' || !title) return false;
110
+ return PLACEHOLDER_TITLE_RE.test(title);
111
+ }
112
+
113
+ module.exports = {
114
+ isJudgePrompt,
115
+ isInternalAbossTitle,
116
+ isPlaceholderOpencodeTitle,
117
+ };