lynkr 9.9.0 → 9.10.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 (73) hide show
  1. package/README.md +92 -23
  2. package/bin/cli.js +13 -7
  3. package/bin/lynkr-init.js +34 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +3 -3
  7. package/scripts/build-eval-set.js +256 -0
  8. package/scripts/mine-difficulty-anchors.js +288 -0
  9. package/scripts/validate-difficulty-classifier.js +144 -0
  10. package/scripts/validate-intent-anchors.js +186 -0
  11. package/src/api/providers-handler.js +0 -1
  12. package/src/api/router.js +292 -160
  13. package/src/clients/databricks.js +95 -6
  14. package/src/config/index.js +3 -43
  15. package/src/context/tool-result-compressor.js +883 -40
  16. package/src/orchestrator/index.js +117 -1235
  17. package/src/orchestrator/passthrough-stream.js +382 -0
  18. package/src/orchestrator/sse-transformer.js +408 -0
  19. package/src/routing/affinity-store.js +17 -3
  20. package/src/routing/classifier-setup.js +207 -0
  21. package/src/routing/complexity-analyzer.js +40 -4
  22. package/src/routing/difficulty-classifier.js +261 -0
  23. package/src/routing/index.js +70 -7
  24. package/src/routing/intent-score.js +132 -12
  25. package/src/routing/session-affinity.js +8 -1
  26. package/src/routing/side-channel-detector.js +103 -0
  27. package/src/server.js +20 -46
  28. package/src/agents/context-manager.js +0 -236
  29. package/src/agents/decomposition/dispatcher.js +0 -185
  30. package/src/agents/decomposition/gate.js +0 -136
  31. package/src/agents/decomposition/index.js +0 -183
  32. package/src/agents/decomposition/model-call.js +0 -75
  33. package/src/agents/decomposition/planner.js +0 -223
  34. package/src/agents/decomposition/synthesizer.js +0 -89
  35. package/src/agents/decomposition/telemetry.js +0 -55
  36. package/src/agents/definitions/loader.js +0 -653
  37. package/src/agents/executor.js +0 -457
  38. package/src/agents/index.js +0 -165
  39. package/src/agents/parallel-coordinator.js +0 -68
  40. package/src/agents/reflector.js +0 -331
  41. package/src/agents/skillbook.js +0 -331
  42. package/src/agents/store.js +0 -259
  43. package/src/edits/index.js +0 -171
  44. package/src/indexer/babel-parser.js +0 -213
  45. package/src/indexer/index.js +0 -1629
  46. package/src/indexer/navigation/index.js +0 -32
  47. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  48. package/src/indexer/parser.js +0 -443
  49. package/src/tasks/store.js +0 -349
  50. package/src/tests/coverage.js +0 -173
  51. package/src/tests/index.js +0 -171
  52. package/src/tests/store.js +0 -213
  53. package/src/tools/agent-task.js +0 -145
  54. package/src/tools/code-mode.js +0 -304
  55. package/src/tools/decompose.js +0 -91
  56. package/src/tools/edits.js +0 -94
  57. package/src/tools/execution.js +0 -171
  58. package/src/tools/git.js +0 -1346
  59. package/src/tools/index.js +0 -306
  60. package/src/tools/indexer.js +0 -360
  61. package/src/tools/lazy-loader.js +0 -366
  62. package/src/tools/mcp-remote.js +0 -88
  63. package/src/tools/mcp.js +0 -116
  64. package/src/tools/process.js +0 -167
  65. package/src/tools/smart-selection.js +0 -180
  66. package/src/tools/stubs.js +0 -55
  67. package/src/tools/tasks.js +0 -260
  68. package/src/tools/tests.js +0 -132
  69. package/src/tools/tinyfish.js +0 -358
  70. package/src/tools/truncate.js +0 -106
  71. package/src/tools/web-client.js +0 -71
  72. package/src/tools/web.js +0 -415
  73. package/src/tools/workspace.js +0 -204
@@ -1,349 +0,0 @@
1
- const db = require("../db");
2
- const logger = require("../logger");
3
-
4
- const ORDER_FIELDS = {
5
- default: "status ASC, priority DESC, updated_at DESC",
6
- status: "status ASC, priority DESC, updated_at DESC",
7
- priority: "priority DESC, updated_at DESC",
8
- updated: "updated_at DESC",
9
- created: "created_at DESC",
10
- };
11
-
12
- const insertTaskStmt = db.prepare(
13
- `INSERT INTO tasks (
14
- title,
15
- status,
16
- priority,
17
- tags,
18
- linked_file,
19
- created_at,
20
- updated_at,
21
- created_by,
22
- updated_by,
23
- metadata
24
- ) VALUES (
25
- @title,
26
- @status,
27
- @priority,
28
- @tags,
29
- @linked_file,
30
- @created_at,
31
- @updated_at,
32
- @created_by,
33
- @updated_by,
34
- @metadata
35
- )`,
36
- );
37
-
38
- const selectTaskByIdStmt = db.prepare(
39
- `SELECT
40
- id,
41
- title,
42
- status,
43
- priority,
44
- tags,
45
- linked_file,
46
- created_at,
47
- updated_at,
48
- created_by,
49
- updated_by,
50
- metadata
51
- FROM tasks
52
- WHERE id = ?`,
53
- );
54
-
55
- const deleteTaskStmt = db.prepare("DELETE FROM tasks WHERE id = ?");
56
- const countTasksByStatusStmt = db.prepare(
57
- `SELECT status, COUNT(1) AS total
58
- FROM tasks
59
- GROUP BY status`,
60
- );
61
- const listRecentTasksStmt = db.prepare(
62
- `SELECT
63
- id,
64
- title,
65
- status,
66
- priority,
67
- tags,
68
- linked_file,
69
- created_at,
70
- updated_at,
71
- created_by,
72
- updated_by,
73
- metadata
74
- FROM tasks
75
- ORDER BY updated_at DESC
76
- LIMIT ?`,
77
- );
78
-
79
- function resolveOrder(orderBy) {
80
- if (typeof orderBy === "string") {
81
- const key = orderBy.toLowerCase().trim();
82
- if (ORDER_FIELDS[key]) {
83
- return ORDER_FIELDS[key];
84
- }
85
- }
86
- return ORDER_FIELDS.default;
87
- }
88
-
89
- function buildListQuery({ status, linkedFile, search, limit, orderBy }) {
90
- let sql = `SELECT
91
- id,
92
- title,
93
- status,
94
- priority,
95
- tags,
96
- linked_file,
97
- created_at,
98
- updated_at,
99
- created_by,
100
- updated_by,
101
- metadata
102
- FROM tasks`;
103
- const conditions = [];
104
- const params = [];
105
-
106
- if (status) {
107
- conditions.push("status = ?");
108
- params.push(status);
109
- }
110
- if (linkedFile) {
111
- conditions.push("linked_file = ?");
112
- params.push(linkedFile);
113
- }
114
- if (search) {
115
- conditions.push("LOWER(title) LIKE ?");
116
- params.push(`%${search.toLowerCase()}%`);
117
- }
118
- if (conditions.length) {
119
- sql += ` WHERE ${conditions.join(" AND ")}`;
120
- }
121
-
122
- sql += ` ORDER BY ${resolveOrder(orderBy)}`;
123
-
124
- if (Number.isInteger(limit) && limit > 0) {
125
- sql += " LIMIT ?";
126
- params.push(limit);
127
- }
128
-
129
- return { sql, params };
130
- }
131
-
132
- function normaliseJson(value, fallback) {
133
- if (value === null || value === undefined) return fallback;
134
- if (typeof value === "string") {
135
- try {
136
- return JSON.parse(value);
137
- } catch (err) {
138
- logger.debug({ err }, "Failed to parse JSON column");
139
- return fallback;
140
- }
141
- }
142
- return value;
143
- }
144
-
145
- function normaliseTaskRow(row) {
146
- if (!row) return null;
147
- return {
148
- id: row.id,
149
- title: row.title,
150
- status: row.status,
151
- priority: typeof row.priority === "number" ? row.priority : Number(row.priority ?? 0),
152
- tags: normaliseJson(row.tags, []),
153
- linkedFile: row.linked_file ?? null,
154
- createdAt: row.created_at,
155
- updatedAt: row.updated_at,
156
- createdBy: row.created_by ?? null,
157
- updatedBy: row.updated_by ?? null,
158
- metadata: normaliseJson(row.metadata, null),
159
- };
160
- }
161
-
162
- function serializeTags(tags) {
163
- if (!tags) return null;
164
- if (Array.isArray(tags) && tags.length === 0) return "[]";
165
- try {
166
- return JSON.stringify(tags);
167
- } catch (err) {
168
- logger.debug({ err }, "Failed to serialise tags");
169
- return null;
170
- }
171
- }
172
-
173
- function serializeMetadata(metadata) {
174
- if (!metadata) return null;
175
- try {
176
- return JSON.stringify(metadata);
177
- } catch (err) {
178
- logger.debug({ err }, "Failed to serialise metadata");
179
- return null;
180
- }
181
- }
182
-
183
- function getTaskById(id) {
184
- const row = selectTaskByIdStmt.get(id);
185
- return normaliseTaskRow(row);
186
- }
187
-
188
- function createTask({
189
- title,
190
- status = "todo",
191
- priority = 0,
192
- tags = [],
193
- linkedFile = null,
194
- createdBy = null,
195
- metadata = null,
196
- }) {
197
- if (typeof title !== "string" || title.trim().length === 0) {
198
- throw new Error("Task title is required.");
199
- }
200
-
201
- const now = Date.now();
202
- const params = {
203
- title: title.trim(),
204
- status: typeof status === "string" && status.trim().length ? status.trim() : "todo",
205
- priority: Number.isFinite(priority) ? Math.trunc(priority) : 0,
206
- tags: serializeTags(tags),
207
- linked_file: linkedFile ?? null,
208
- created_at: now,
209
- updated_at: now,
210
- created_by: createdBy ?? null,
211
- updated_by: createdBy ?? null,
212
- metadata: serializeMetadata(metadata),
213
- };
214
- const result = insertTaskStmt.run(params);
215
- return getTaskById(result.lastInsertRowid);
216
- }
217
-
218
- function listTasks(options = {}) {
219
- const { sql, params } = buildListQuery({
220
- status: options.status,
221
- linkedFile: options.linkedFile ?? options.path ?? options.file,
222
- search: options.search ?? options.query ?? null,
223
- limit: options.limit,
224
- orderBy: options.orderBy,
225
- });
226
- return db
227
- .prepare(sql)
228
- .all(...params)
229
- .map(normaliseTaskRow);
230
- }
231
-
232
- function updateTask(id, updates = {}) {
233
- if (!id) {
234
- throw new Error("Task id is required for update.");
235
- }
236
- const fields = [];
237
- const params = {};
238
-
239
- if (updates.title !== undefined) {
240
- if (typeof updates.title !== "string" || !updates.title.trim()) {
241
- throw new Error("Task title must be a non-empty string.");
242
- }
243
- fields.push("title = @title");
244
- params.title = updates.title.trim();
245
- }
246
- if (updates.status !== undefined) {
247
- if (typeof updates.status !== "string" || !updates.status.trim()) {
248
- throw new Error("Task status must be a non-empty string.");
249
- }
250
- fields.push("status = @status");
251
- params.status = updates.status.trim();
252
- }
253
- if (updates.priority !== undefined) {
254
- fields.push("priority = @priority");
255
- params.priority = Number.isFinite(updates.priority)
256
- ? Math.trunc(updates.priority)
257
- : 0;
258
- }
259
- if (updates.tags !== undefined) {
260
- fields.push("tags = @tags");
261
- params.tags = serializeTags(updates.tags);
262
- }
263
- if (updates.linkedFile !== undefined) {
264
- fields.push("linked_file = @linked_file");
265
- params.linked_file =
266
- updates.linkedFile === null || updates.linkedFile === undefined
267
- ? null
268
- : String(updates.linkedFile);
269
- }
270
- if (updates.metadata !== undefined) {
271
- fields.push("metadata = @metadata");
272
- params.metadata = serializeMetadata(updates.metadata);
273
- }
274
- if (updates.updatedBy !== undefined) {
275
- fields.push("updated_by = @updated_by");
276
- params.updated_by =
277
- updates.updatedBy === null || updates.updatedBy === undefined
278
- ? null
279
- : String(updates.updatedBy);
280
- }
281
-
282
- if (!fields.length) {
283
- return getTaskById(id);
284
- }
285
-
286
- params.updated_at = Date.now();
287
- params.id = id;
288
- fields.push("updated_at = @updated_at");
289
-
290
- const sql = `UPDATE tasks SET ${fields.join(", ")} WHERE id = @id`;
291
- const stmt = db.prepare(sql);
292
- const result = stmt.run(params);
293
- if (result.changes === 0) {
294
- throw new Error(`Task ${id} not found.`);
295
- }
296
- return getTaskById(id);
297
- }
298
-
299
- function setTaskStatus(id, status, updatedBy) {
300
- return updateTask(id, { status, updatedBy });
301
- }
302
-
303
- function deleteTask(id) {
304
- if (!id) {
305
- throw new Error("Task id is required for deletion.");
306
- }
307
- const result = deleteTaskStmt.run(id);
308
- return result.changes > 0;
309
- }
310
-
311
- function normaliseLimit(limit, fallback = 5) {
312
- if (limit === undefined || limit === null) return fallback;
313
- const num = Number(limit);
314
- if (!Number.isFinite(num)) return fallback;
315
- const clamped = Math.trunc(num);
316
- if (clamped <= 0) return 0;
317
- return Math.min(clamped, 50);
318
- }
319
-
320
- function getTaskSummary(options = {}) {
321
- const counts = countTasksByStatusStmt.all();
322
- const byStatus = counts.map((row) => ({
323
- status: row.status,
324
- total: row.total,
325
- }));
326
- const total = byStatus.reduce((acc, item) => acc + item.total, 0);
327
- const limit = normaliseLimit(options.limit, 5);
328
- let recent = [];
329
- if (limit > 0) {
330
- recent = listRecentTasksStmt
331
- .all(limit)
332
- .map(normaliseTaskRow);
333
- }
334
- return {
335
- total,
336
- byStatus,
337
- recent,
338
- };
339
- }
340
-
341
- module.exports = {
342
- createTask,
343
- listTasks,
344
- getTaskById,
345
- updateTask,
346
- setTaskStatus,
347
- deleteTask,
348
- getTaskSummary,
349
- };
@@ -1,173 +0,0 @@
1
- const fsp = require("fs/promises");
2
- const path = require("path");
3
- const fg = require("fast-glob");
4
- const logger = require("../logger");
5
- const { workspaceRoot } = require("../workspace");
6
-
7
- const COVERAGE_METRICS = ["lines", "statements", "branches", "functions"];
8
- const OUTPUT_LIMIT = 256;
9
-
10
- function hasWildcard(pattern) {
11
- return /[*?[\]{}]/.test(pattern);
12
- }
13
-
14
- function ensureWorkspaceAbsolute(targetPath) {
15
- const absolute = path.isAbsolute(targetPath)
16
- ? targetPath
17
- : path.resolve(workspaceRoot, targetPath);
18
- if (!absolute.startsWith(workspaceRoot)) {
19
- logger.debug(
20
- { targetPath },
21
- "Ignoring coverage path outside workspace",
22
- );
23
- return null;
24
- }
25
- return absolute;
26
- }
27
-
28
- function expandCoveragePatterns(patterns) {
29
- if (!Array.isArray(patterns) || patterns.length === 0) return [];
30
- const results = new Set();
31
- for (const rawPattern of patterns) {
32
- if (typeof rawPattern !== "string") continue;
33
- const pattern = rawPattern.trim();
34
- if (!pattern) continue;
35
- if (hasWildcard(pattern)) {
36
- const matches = fg.sync(pattern, {
37
- cwd: workspaceRoot,
38
- dot: true,
39
- absolute: true,
40
- onlyFiles: true,
41
- unique: true,
42
- });
43
- matches.forEach((match) => {
44
- const absolute = ensureWorkspaceAbsolute(match);
45
- if (absolute) results.add(absolute);
46
- });
47
- } else {
48
- const absolute = ensureWorkspaceAbsolute(pattern);
49
- if (absolute) results.add(absolute);
50
- }
51
- }
52
- return Array.from(results);
53
- }
54
-
55
- function normaliseMetric(rawMetric) {
56
- if (!rawMetric || typeof rawMetric !== "object") return null;
57
- const total = Number(rawMetric.total ?? rawMetric.statements ?? rawMetric.lines ?? rawMetric.functions ?? 0);
58
- const covered = Number(rawMetric.covered ?? rawMetric.hit ?? 0);
59
- const skipped = Number(rawMetric.skipped ?? 0);
60
- let pct = rawMetric.pct;
61
- if (!Number.isFinite(pct)) {
62
- pct = total > 0 ? (covered / total) * 100 : 0;
63
- }
64
- return {
65
- total: Number.isFinite(total) ? total : null,
66
- covered: Number.isFinite(covered) ? covered : null,
67
- skipped: Number.isFinite(skipped) ? skipped : null,
68
- pct: Number.isFinite(pct) ? Number(pct.toFixed(2)) : null,
69
- };
70
- }
71
-
72
- async function parseIstanbulCoverage(filePath) {
73
- try {
74
- const stats = await fsp.stat(filePath);
75
- if (!stats.isFile()) return null;
76
- const raw = await fsp.readFile(filePath, "utf8");
77
- const data = JSON.parse(raw);
78
- const totals = data.total ?? data.totals ?? null;
79
- if (!totals) return null;
80
- const summary = {};
81
- let hasMetric = false;
82
- for (const metric of COVERAGE_METRICS) {
83
- const value = normaliseMetric(totals[metric]);
84
- if (value) {
85
- summary[metric] = value;
86
- hasMetric = true;
87
- }
88
- }
89
- if (!hasMetric) return null;
90
- return {
91
- path: path.relative(workspaceRoot, filePath) || path.basename(filePath),
92
- absolutePath: filePath,
93
- type: "istanbul",
94
- totals: summary,
95
- generatedAt: new Date(stats.mtimeMs).toISOString(),
96
- };
97
- } catch (err) {
98
- const sample =
99
- typeof err?.message === "string" && err.message.length > OUTPUT_LIMIT
100
- ? `${err.message.slice(0, OUTPUT_LIMIT)}…`
101
- : err?.message ?? String(err);
102
- logger.debug({ err: sample, filePath }, "Failed to parse coverage file");
103
- return null;
104
- }
105
- }
106
-
107
- async function parseCoverageFile(filePath) {
108
- const extension = path.extname(filePath).toLowerCase();
109
- if (extension === ".json") {
110
- return parseIstanbulCoverage(filePath);
111
- }
112
- return null;
113
- }
114
-
115
- function aggregateMetric(values) {
116
- if (!values.length) return null;
117
- let total = 0;
118
- let covered = 0;
119
- let skipped = 0;
120
- for (const value of values) {
121
- total += Number(value.total ?? 0);
122
- covered += Number(value.covered ?? 0);
123
- skipped += Number(value.skipped ?? 0);
124
- }
125
- const pct = total > 0 ? Number(((covered / total) * 100).toFixed(2)) : null;
126
- return {
127
- total: total || null,
128
- covered,
129
- skipped,
130
- pct,
131
- };
132
- }
133
-
134
- function synthesiseSummary(entries) {
135
- if (!Array.isArray(entries) || entries.length === 0) return null;
136
- const summary = {};
137
- let hasMetric = false;
138
- for (const metric of COVERAGE_METRICS) {
139
- const values = entries
140
- .map((entry) => entry?.totals?.[metric])
141
- .filter(Boolean);
142
- if (values.length === 0) continue;
143
- const aggregated = aggregateMetric(values);
144
- if (aggregated) {
145
- summary[metric] = aggregated;
146
- hasMetric = true;
147
- }
148
- }
149
- return hasMetric ? summary : null;
150
- }
151
-
152
- async function collectCoverageSummary(patterns = []) {
153
- if (!Array.isArray(patterns) || patterns.length === 0) {
154
- return { sources: [], summary: null };
155
- }
156
- const files = expandCoveragePatterns(patterns);
157
- if (files.length === 0) {
158
- return { sources: [], summary: null };
159
- }
160
- const sources = [];
161
- for (const filePath of files) {
162
- const parsed = await parseCoverageFile(filePath);
163
- if (parsed) {
164
- sources.push(parsed);
165
- }
166
- }
167
- const summary = synthesiseSummary(sources);
168
- return { sources, summary };
169
- }
170
-
171
- module.exports = {
172
- collectCoverageSummary,
173
- };
@@ -1,171 +0,0 @@
1
- const config = require("../config");
2
- const logger = require("../logger");
3
- const { workspaceRoot } = require("../workspace");
4
- const { runProcess, MAX_TIMEOUT_MS } = require("../tools/process");
5
- const { collectCoverageSummary } = require("./coverage");
6
- const { createTestRun, listTestRuns, getTestSummary } = require("./store");
7
-
8
- function resolveProfile(name) {
9
- if (!name || typeof name !== "string") return null;
10
- const profiles = config.tests?.profiles;
11
- if (!Array.isArray(profiles) || profiles.length === 0) return null;
12
- const normalized = name.trim().toLowerCase();
13
- return (
14
- profiles.find(
15
- (profile) =>
16
- typeof profile?.name === "string" && profile.name.trim().toLowerCase() === normalized,
17
- ) ?? null
18
- );
19
- }
20
-
21
- function deriveCommand({ profile, args }) {
22
- if (profile && typeof profile.command === "string" && profile.command.trim().length > 0) {
23
- const command = profile.command.trim();
24
- const commandArgs = Array.isArray(profile.args) ? profile.args.map(String) : [];
25
- const cwd =
26
- typeof profile.cwd === "string" && profile.cwd.trim().length > 0
27
- ? profile.cwd.trim()
28
- : ".";
29
- const sandbox = typeof profile.sandbox === "string" ? profile.sandbox.toLowerCase() : null;
30
- return { command, args: commandArgs, cwd, sandbox };
31
- }
32
-
33
- const defaultCommand = config.tests?.defaultCommand;
34
- if (typeof defaultCommand === "string" && defaultCommand.trim().length > 0) {
35
- return {
36
- command: defaultCommand.trim(),
37
- args: Array.isArray(args) && args.length ? args.map(String) : config.tests?.defaultArgs ?? [],
38
- cwd: ".",
39
- sandbox: config.tests?.sandbox ?? "auto",
40
- };
41
- }
42
-
43
- throw new Error(
44
- "No default test command configured. Set WORKSPACE_TEST_COMMAND or define profiles.",
45
- );
46
- }
47
-
48
- async function executeTestCommand({
49
- profile,
50
- command,
51
- args = [],
52
- cwd = ".",
53
- env = {},
54
- timeoutMs,
55
- sandbox,
56
- sessionId,
57
- }) {
58
- const timeout =
59
- Number.isFinite(timeoutMs) && timeoutMs > 0
60
- ? Math.min(timeoutMs, MAX_TIMEOUT_MS)
61
- : config.tests?.timeoutMs ?? 600000;
62
-
63
- const runEnv = {
64
- ...env,
65
- WORKSPACE_TEST_PROFILE: profile ?? "",
66
- };
67
-
68
- return runProcess({
69
- command,
70
- args,
71
- cwd,
72
- env: runEnv,
73
- timeoutMs: timeout,
74
- sandbox: sandbox ?? config.tests?.sandbox ?? "auto",
75
- sessionId,
76
- });
77
- }
78
-
79
- async function runWorkspaceTests({
80
- profileName,
81
- args,
82
- cwd,
83
- env,
84
- timeoutMs,
85
- sandbox,
86
- sessionId,
87
- collectCoverage = true,
88
- }) {
89
- const profile = resolveProfile(profileName);
90
- const derived = deriveCommand({ profile, args });
91
-
92
- const command = typeof derived.command === "string" ? derived.command : null;
93
- const commandArgs = Array.isArray(args) && args.length ? args.map(String) : derived.args ?? [];
94
- const effectiveCwd = typeof cwd === "string" ? cwd : derived.cwd ?? ".";
95
- const effectiveSandbox = sandbox ?? derived.sandbox ?? config.tests?.sandbox ?? "auto";
96
-
97
- const start = Date.now();
98
- const result = await executeTestCommand({
99
- profile: profile?.name ?? profileName ?? null,
100
- command,
101
- args: commandArgs,
102
- cwd: effectiveCwd,
103
- env,
104
- timeoutMs,
105
- sandbox: effectiveSandbox,
106
- sessionId,
107
- });
108
-
109
- const durationMs = Date.now() - start;
110
- const status =
111
- result.exitCode === 0 && !result.timedOut
112
- ? "passed"
113
- : result.timedOut
114
- ? "timed_out"
115
- : "failed";
116
-
117
- let coverage = null;
118
- if (collectCoverage) {
119
- try {
120
- const patterns =
121
- Array.isArray(config.tests?.coverage?.files) && config.tests.coverage.files.length > 0
122
- ? config.tests.coverage.files
123
- : [];
124
- if (patterns.length > 0) {
125
- const summary = await collectCoverageSummary(patterns);
126
- coverage = summary;
127
- }
128
- } catch (err) {
129
- logger.warn({ err }, "Failed to collect coverage summary");
130
- }
131
- }
132
-
133
- const record = createTestRun({
134
- profile: profile?.name ?? profileName ?? null,
135
- status,
136
- command,
137
- args: commandArgs,
138
- cwd: effectiveCwd === "." ? workspaceRoot : effectiveCwd,
139
- exitCode: result.exitCode,
140
- timedOut: result.timedOut,
141
- durationMs,
142
- sandbox: effectiveSandbox,
143
- stdout: result.stdout,
144
- stderr: result.stderr,
145
- coverage,
146
- createdAt: Date.now(),
147
- });
148
-
149
- return {
150
- run: {
151
- id: record.id,
152
- status,
153
- exitCode: result.exitCode,
154
- timedOut: result.timedOut,
155
- durationMs,
156
- sandbox: effectiveSandbox,
157
- profile: record.profile,
158
- stdout: record.stdout,
159
- stdoutTruncated: record.stdoutTruncated,
160
- stderr: record.stderr,
161
- stderrTruncated: record.stderrTruncated,
162
- },
163
- coverage,
164
- };
165
- }
166
-
167
- module.exports = {
168
- runWorkspaceTests,
169
- listTestRuns,
170
- getTestSummary,
171
- };