lynkr 9.9.1 → 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 (67) hide show
  1. package/README.md +50 -9
  2. package/bin/cli.js +2 -0
  3. package/bin/lynkr-init.js +4 -12
  4. package/bin/lynkr-reset.js +71 -0
  5. package/config/model-tiers.json +199 -52
  6. package/package.json +2 -2
  7. package/scripts/validate-difficulty-classifier.js +27 -6
  8. package/src/api/providers-handler.js +0 -1
  9. package/src/api/router.js +275 -160
  10. package/src/clients/databricks.js +95 -6
  11. package/src/config/index.js +3 -43
  12. package/src/orchestrator/index.js +117 -1235
  13. package/src/orchestrator/passthrough-stream.js +382 -0
  14. package/src/orchestrator/sse-transformer.js +408 -0
  15. package/src/routing/affinity-store.js +17 -3
  16. package/src/routing/difficulty-classifier.js +52 -10
  17. package/src/routing/index.js +35 -3
  18. package/src/routing/intent-score.js +20 -2
  19. package/src/routing/session-affinity.js +8 -1
  20. package/src/routing/side-channel-detector.js +103 -0
  21. package/src/server.js +0 -46
  22. package/src/agents/context-manager.js +0 -236
  23. package/src/agents/decomposition/dispatcher.js +0 -185
  24. package/src/agents/decomposition/gate.js +0 -136
  25. package/src/agents/decomposition/index.js +0 -183
  26. package/src/agents/decomposition/model-call.js +0 -75
  27. package/src/agents/decomposition/planner.js +0 -223
  28. package/src/agents/decomposition/synthesizer.js +0 -89
  29. package/src/agents/decomposition/telemetry.js +0 -55
  30. package/src/agents/definitions/loader.js +0 -653
  31. package/src/agents/executor.js +0 -457
  32. package/src/agents/index.js +0 -165
  33. package/src/agents/parallel-coordinator.js +0 -68
  34. package/src/agents/reflector.js +0 -331
  35. package/src/agents/skillbook.js +0 -331
  36. package/src/agents/store.js +0 -259
  37. package/src/edits/index.js +0 -171
  38. package/src/indexer/babel-parser.js +0 -213
  39. package/src/indexer/index.js +0 -1629
  40. package/src/indexer/navigation/index.js +0 -32
  41. package/src/indexer/navigation/providers/treeSitter.js +0 -36
  42. package/src/indexer/parser.js +0 -443
  43. package/src/tasks/store.js +0 -349
  44. package/src/tests/coverage.js +0 -173
  45. package/src/tests/index.js +0 -171
  46. package/src/tests/store.js +0 -213
  47. package/src/tools/agent-task.js +0 -145
  48. package/src/tools/code-mode.js +0 -304
  49. package/src/tools/decompose.js +0 -91
  50. package/src/tools/edits.js +0 -94
  51. package/src/tools/execution.js +0 -171
  52. package/src/tools/git.js +0 -1346
  53. package/src/tools/index.js +0 -306
  54. package/src/tools/indexer.js +0 -360
  55. package/src/tools/lazy-loader.js +0 -366
  56. package/src/tools/mcp-remote.js +0 -88
  57. package/src/tools/mcp.js +0 -116
  58. package/src/tools/process.js +0 -167
  59. package/src/tools/smart-selection.js +0 -180
  60. package/src/tools/stubs.js +0 -55
  61. package/src/tools/tasks.js +0 -260
  62. package/src/tools/tests.js +0 -132
  63. package/src/tools/tinyfish.js +0 -358
  64. package/src/tools/truncate.js +0 -106
  65. package/src/tools/web-client.js +0 -71
  66. package/src/tools/web.js +0 -415
  67. 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
- };