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