@cardor/agent-harness-kit 1.8.1 → 1.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.
- package/LICENSE +80 -58
- package/README.md +126 -44
- package/dist/agent-templates/builder.md +13 -0
- package/dist/{chunk-OEPZRC7J.js → chunk-ADV7OPU2.js} +4 -1
- package/dist/chunk-ADV7OPU2.js.map +1 -0
- package/dist/chunk-DNFFWQWR.js +821 -0
- package/dist/chunk-DNFFWQWR.js.map +1 -0
- package/dist/cli.js +952 -905
- package/dist/cli.js.map +1 -1
- package/dist/dashboard-dist/assets/index-CyU-X1yO.js +9 -0
- package/dist/dashboard-dist/assets/index-CzEB2a6I.css +1 -0
- package/dist/dashboard-dist/index.html +2 -2
- package/dist/db-QQ7BR5K7.js +23 -0
- package/dist/db-QQ7BR5K7.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +1 -1
- package/dist/skills/ahk-review/SKILL.md +45 -0
- package/dist/{sqlite-KWYK4IJW.js → sqlite-TR4D324R.js} +5 -5
- package/dist/{sqlite-KWYK4IJW.js.map → sqlite-TR4D324R.js.map} +1 -1
- package/package.json +3 -2
- package/dist/chunk-OEPZRC7J.js.map +0 -1
- package/dist/dashboard-dist/assets/index-6UCLKb-M.css +0 -1
- package/dist/dashboard-dist/assets/index-CoqlHfTu.js +0 -9
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
// src/core/db.ts
|
|
2
|
+
import { randomUUID } from "crypto";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
+
import { homedir } from "os";
|
|
5
|
+
import { dirname, join, resolve } from "path";
|
|
6
|
+
|
|
7
|
+
// src/core/repositories/ActionRepository.ts
|
|
8
|
+
var ActionRepository = class {
|
|
9
|
+
constructor(driver) {
|
|
10
|
+
this.driver = driver;
|
|
11
|
+
}
|
|
12
|
+
driver;
|
|
13
|
+
async create(id, taskId, agent, now) {
|
|
14
|
+
await this.driver.exec(
|
|
15
|
+
`INSERT INTO actions (id, task_id, agent, status, created_at) VALUES (?, ?, ?, 'in_progress', ?)`,
|
|
16
|
+
[id, taskId, agent, now]
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
async complete(actionId, summary, now) {
|
|
20
|
+
await this.driver.exec(
|
|
21
|
+
`UPDATE actions SET status = 'completed', completed_at = ?, summary = ? WHERE id = ?`,
|
|
22
|
+
[now, summary, actionId]
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
async closeOrphaned(taskId, now) {
|
|
26
|
+
return this.driver.exec(
|
|
27
|
+
`UPDATE actions SET status = 'completed', completed_at = ?, summary = 'Auto-closed: task marked done' WHERE task_id = ? AND status = 'in_progress'`,
|
|
28
|
+
[now, taskId]
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
async getById(actionId) {
|
|
32
|
+
return this.driver.queryOne(`SELECT * FROM actions WHERE id = ?`, [actionId]);
|
|
33
|
+
}
|
|
34
|
+
async getForTask(taskId) {
|
|
35
|
+
return this.driver.query(
|
|
36
|
+
`SELECT * FROM actions WHERE task_id = ? ORDER BY created_at`,
|
|
37
|
+
[taskId]
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
async getAll() {
|
|
41
|
+
return this.driver.query(`SELECT * FROM actions ORDER BY created_at`);
|
|
42
|
+
}
|
|
43
|
+
async getWithDetails(taskId) {
|
|
44
|
+
const actions = await this.getForTask(taskId);
|
|
45
|
+
return Promise.all(
|
|
46
|
+
actions.map(async (action) => ({
|
|
47
|
+
...action,
|
|
48
|
+
sections: await this.getSections(action.id),
|
|
49
|
+
files: await this.getFiles(action.id),
|
|
50
|
+
tools: await this.getTools(action.id)
|
|
51
|
+
}))
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
// ─── Sections ─────────────────────────────────────────────────────────────
|
|
55
|
+
async addSection(actionId, sectionType, content, now) {
|
|
56
|
+
await this.driver.exec(
|
|
57
|
+
`INSERT INTO action_sections (action_id, section_type, content, created_at) VALUES (?, ?, ?, ?)`,
|
|
58
|
+
[actionId, sectionType, content, now]
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
async getSections(actionId) {
|
|
62
|
+
return this.driver.query(
|
|
63
|
+
`SELECT * FROM action_sections WHERE action_id = ? ORDER BY created_at`,
|
|
64
|
+
[actionId]
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
async getAllSections() {
|
|
68
|
+
return this.driver.query(`SELECT * FROM action_sections ORDER BY created_at`);
|
|
69
|
+
}
|
|
70
|
+
// ─── Files ────────────────────────────────────────────────────────────────
|
|
71
|
+
async addFile(actionId, filePath, operation, notes) {
|
|
72
|
+
await this.driver.exec(
|
|
73
|
+
`INSERT INTO action_files (action_id, file_path, operation, notes) VALUES (?, ?, ?, ?)`,
|
|
74
|
+
[actionId, filePath, operation, notes]
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
async getFiles(actionId) {
|
|
78
|
+
return this.driver.query(
|
|
79
|
+
`SELECT * FROM action_files WHERE action_id = ?`,
|
|
80
|
+
[actionId]
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
async getFilesForTask(taskId) {
|
|
84
|
+
return this.driver.query(
|
|
85
|
+
`SELECT af.*, a.agent FROM action_files af JOIN actions a ON af.action_id = a.id WHERE a.task_id = ? ORDER BY a.agent, af.operation`,
|
|
86
|
+
[taskId]
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
/** Returns ALL action_files rows regardless of action — used by full DB
|
|
90
|
+
* exports (e.g. `ahk migrate storage`) so file-touch records aren't lost. */
|
|
91
|
+
async getAllFiles() {
|
|
92
|
+
return this.driver.query(`SELECT * FROM action_files ORDER BY id`);
|
|
93
|
+
}
|
|
94
|
+
// ─── Tools ────────────────────────────────────────────────────────────────
|
|
95
|
+
async addTool(actionId, toolName, argsJson, resultSummary, now) {
|
|
96
|
+
await this.driver.exec(
|
|
97
|
+
`INSERT INTO action_tools (action_id, tool_name, args_json, result_summary, called_at) VALUES (?, ?, ?, ?, ?)`,
|
|
98
|
+
[actionId, toolName, argsJson, resultSummary, now]
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
async getTools(actionId) {
|
|
102
|
+
return this.driver.query(
|
|
103
|
+
`SELECT * FROM action_tools WHERE action_id = ? ORDER BY called_at`,
|
|
104
|
+
[actionId]
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
/** Returns ALL action_tools rows regardless of action — used by full DB
|
|
108
|
+
* exports (e.g. `ahk migrate storage`) so tool-call records aren't lost. */
|
|
109
|
+
async getAllTools() {
|
|
110
|
+
return this.driver.query(`SELECT * FROM action_tools ORDER BY id`);
|
|
111
|
+
}
|
|
112
|
+
async getTopTools(limit) {
|
|
113
|
+
return this.driver.query(
|
|
114
|
+
`SELECT tool_name, COUNT(*) as uses FROM action_tools GROUP BY tool_name ORDER BY uses DESC LIMIT ?`,
|
|
115
|
+
[limit]
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// src/core/repositories/StatsRepository.ts
|
|
121
|
+
var AGENT_ORDER = ["lead", "explorer", "builder", "reviewer"];
|
|
122
|
+
var StatsRepository = class {
|
|
123
|
+
constructor(driver) {
|
|
124
|
+
this.driver = driver;
|
|
125
|
+
}
|
|
126
|
+
driver;
|
|
127
|
+
async getCounts() {
|
|
128
|
+
const [{ total: totalActions }] = await this.driver.query(
|
|
129
|
+
`SELECT COUNT(*) as total FROM actions`
|
|
130
|
+
);
|
|
131
|
+
const [{ total: totalFiles }] = await this.driver.query(
|
|
132
|
+
`SELECT COUNT(*) as total FROM action_files`
|
|
133
|
+
);
|
|
134
|
+
const [{ total: uniqueTools }] = await this.driver.query(
|
|
135
|
+
`SELECT COUNT(DISTINCT tool_name) as total FROM action_tools`
|
|
136
|
+
);
|
|
137
|
+
const [{ total: activeAgents }] = await this.driver.query(
|
|
138
|
+
`SELECT COUNT(DISTINCT agent) as total FROM actions WHERE status = 'in_progress'`
|
|
139
|
+
);
|
|
140
|
+
return { totalActions, totalFiles, uniqueTools, activeAgents };
|
|
141
|
+
}
|
|
142
|
+
async getRecentTools(limit) {
|
|
143
|
+
return this.driver.query(
|
|
144
|
+
`SELECT at.*, t.id as task_id, t.title as task_title, t.slug as task_slug, a.agent
|
|
145
|
+
FROM action_tools at
|
|
146
|
+
JOIN actions a ON at.action_id = a.id
|
|
147
|
+
JOIN tasks t ON a.task_id = t.id
|
|
148
|
+
ORDER BY at.called_at DESC
|
|
149
|
+
LIMIT ?`,
|
|
150
|
+
[limit]
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
async getTopFiles(limit) {
|
|
154
|
+
return this.driver.query(
|
|
155
|
+
`SELECT
|
|
156
|
+
file_path,
|
|
157
|
+
COUNT(*) as total,
|
|
158
|
+
SUM(CASE WHEN operation='read' THEN 1 ELSE 0 END) as read,
|
|
159
|
+
SUM(CASE WHEN operation='created' THEN 1 ELSE 0 END) as created,
|
|
160
|
+
SUM(CASE WHEN operation='modified' THEN 1 ELSE 0 END) as modified,
|
|
161
|
+
SUM(CASE WHEN operation='deleted' THEN 1 ELSE 0 END) as deleted
|
|
162
|
+
FROM action_files
|
|
163
|
+
GROUP BY file_path
|
|
164
|
+
ORDER BY total DESC
|
|
165
|
+
LIMIT ?`,
|
|
166
|
+
[limit]
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
async getRecentFiles(limit) {
|
|
170
|
+
return this.driver.query(
|
|
171
|
+
`SELECT af.*, t.id as task_id, t.title as task_title, t.slug as task_slug,
|
|
172
|
+
a.agent, a.created_at as called_at
|
|
173
|
+
FROM action_files af
|
|
174
|
+
JOIN actions a ON af.action_id = a.id
|
|
175
|
+
JOIN tasks t ON a.task_id = t.id
|
|
176
|
+
ORDER BY a.created_at DESC
|
|
177
|
+
LIMIT ?`,
|
|
178
|
+
[limit]
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
async getAgentStats() {
|
|
182
|
+
const rows = await this.driver.query(
|
|
183
|
+
`SELECT
|
|
184
|
+
a.agent,
|
|
185
|
+
COUNT(*) as actions_total,
|
|
186
|
+
SUM(CASE WHEN a.status='completed' THEN 1 ELSE 0 END) as actions_done,
|
|
187
|
+
SUM(CASE WHEN a.status='blocked' THEN 1 ELSE 0 END) as actions_blocked,
|
|
188
|
+
COUNT(DISTINCT a.task_id) as tasks_worked,
|
|
189
|
+
COUNT(DISTINCT af.file_path) as files_touched
|
|
190
|
+
FROM actions a
|
|
191
|
+
LEFT JOIN action_files af ON af.action_id = a.id
|
|
192
|
+
GROUP BY a.agent
|
|
193
|
+
ORDER BY actions_total DESC`
|
|
194
|
+
);
|
|
195
|
+
return rows.sort((a, b) => {
|
|
196
|
+
const ai = AGENT_ORDER.indexOf(a.agent);
|
|
197
|
+
const bi = AGENT_ORDER.indexOf(b.agent);
|
|
198
|
+
if (ai === -1 && bi === -1) return 0;
|
|
199
|
+
if (ai === -1) return 1;
|
|
200
|
+
if (bi === -1) return -1;
|
|
201
|
+
return ai - bi;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
async getTimeline(limit) {
|
|
205
|
+
return this.driver.query(
|
|
206
|
+
`SELECT a.*, t.title as task_title, t.slug as task_slug, t.status as task_status
|
|
207
|
+
FROM actions a
|
|
208
|
+
JOIN tasks t ON a.task_id = t.id
|
|
209
|
+
ORDER BY a.created_at DESC
|
|
210
|
+
LIMIT ?`,
|
|
211
|
+
[limit]
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/core/repositories/TaskRepository.ts
|
|
217
|
+
var TaskRepository = class {
|
|
218
|
+
constructor(driver) {
|
|
219
|
+
this.driver = driver;
|
|
220
|
+
}
|
|
221
|
+
driver;
|
|
222
|
+
async add(params) {
|
|
223
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
224
|
+
return this.driver.insert(
|
|
225
|
+
`INSERT INTO tasks (slug, title, description, status, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
226
|
+
[params.slug, params.title, params.description ?? null, params.status ?? "pending", now, now]
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
async addAcceptance(taskId, criteria) {
|
|
230
|
+
for (const criterion of criteria) {
|
|
231
|
+
await this.driver.exec(
|
|
232
|
+
`INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,
|
|
233
|
+
[taskId, criterion]
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async getAll(status, includeArchived = false) {
|
|
238
|
+
let sql = `SELECT * FROM tasks`;
|
|
239
|
+
const params = [];
|
|
240
|
+
const conditions = [];
|
|
241
|
+
if (!includeArchived) {
|
|
242
|
+
conditions.push(`archived_at IS NULL`);
|
|
243
|
+
}
|
|
244
|
+
if (status) {
|
|
245
|
+
conditions.push(`status = ?`);
|
|
246
|
+
params.push(status);
|
|
247
|
+
}
|
|
248
|
+
if (conditions.length > 0) {
|
|
249
|
+
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
250
|
+
}
|
|
251
|
+
sql += ` ORDER BY CASE status WHEN 'pending' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 WHEN 'done' THEN 4 ELSE 5 END, updated_at DESC`;
|
|
252
|
+
return this.driver.query(sql, params);
|
|
253
|
+
}
|
|
254
|
+
async getAllWithAcceptanceCounts(includeArchived = false) {
|
|
255
|
+
let sql = `
|
|
256
|
+
SELECT t.*,
|
|
257
|
+
COUNT(ta.id) as acceptance_total,
|
|
258
|
+
COALESCE(SUM(ta.met), 0) as acceptance_met
|
|
259
|
+
FROM tasks t
|
|
260
|
+
LEFT JOIN task_acceptance ta ON ta.task_id = t.id
|
|
261
|
+
`;
|
|
262
|
+
if (!includeArchived) {
|
|
263
|
+
sql += ` WHERE t.archived_at IS NULL`;
|
|
264
|
+
}
|
|
265
|
+
sql += ` GROUP BY t.id ORDER BY CASE t.status WHEN 'pending' THEN 1 WHEN 'in_progress' THEN 2 WHEN 'blocked' THEN 3 WHEN 'done' THEN 4 ELSE 5 END, t.updated_at DESC`;
|
|
266
|
+
return this.driver.query(sql);
|
|
267
|
+
}
|
|
268
|
+
async getById(id) {
|
|
269
|
+
return this.driver.queryOne(`SELECT * FROM tasks WHERE id = ?`, [id]);
|
|
270
|
+
}
|
|
271
|
+
async getBySlug(slug) {
|
|
272
|
+
return this.driver.queryOne(`SELECT * FROM tasks WHERE slug = ?`, [slug]);
|
|
273
|
+
}
|
|
274
|
+
async getAcceptance(taskId) {
|
|
275
|
+
return this.driver.query(
|
|
276
|
+
`SELECT * FROM task_acceptance WHERE task_id = ?`,
|
|
277
|
+
[taskId]
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
/** Returns ALL task_acceptance rows regardless of task — used by full DB
|
|
281
|
+
* exports (e.g. `ahk migrate storage`) so criteria aren't silently dropped. */
|
|
282
|
+
async getAllAcceptance() {
|
|
283
|
+
return this.driver.query(`SELECT * FROM task_acceptance ORDER BY id`);
|
|
284
|
+
}
|
|
285
|
+
async setStatus(id, status, extra) {
|
|
286
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
287
|
+
if (extra?.started_at) {
|
|
288
|
+
await this.driver.exec(
|
|
289
|
+
`UPDATE tasks SET status = ?, started_at = ?, updated_at = ? WHERE id = ?`,
|
|
290
|
+
[status, extra.started_at, now, id]
|
|
291
|
+
);
|
|
292
|
+
} else if (extra?.completed_at) {
|
|
293
|
+
await this.driver.exec(
|
|
294
|
+
`UPDATE tasks SET status = ?, completed_at = ?, updated_at = ? WHERE id = ?`,
|
|
295
|
+
[status, extra.completed_at, now, id]
|
|
296
|
+
);
|
|
297
|
+
} else {
|
|
298
|
+
await this.driver.exec(`UPDATE tasks SET status = ?, updated_at = ? WHERE id = ?`, [status, now, id]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
async update(id, params) {
|
|
302
|
+
const sets = [];
|
|
303
|
+
const vals = [];
|
|
304
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
305
|
+
if (params.title !== void 0) {
|
|
306
|
+
sets.push("title = ?");
|
|
307
|
+
vals.push(params.title);
|
|
308
|
+
}
|
|
309
|
+
if (params.description !== void 0) {
|
|
310
|
+
sets.push("description = ?");
|
|
311
|
+
vals.push(params.description);
|
|
312
|
+
}
|
|
313
|
+
if (params.slug !== void 0) {
|
|
314
|
+
sets.push("slug = ?");
|
|
315
|
+
vals.push(params.slug);
|
|
316
|
+
}
|
|
317
|
+
if (sets.length === 0) return;
|
|
318
|
+
sets.push("updated_at = ?");
|
|
319
|
+
vals.push(now);
|
|
320
|
+
vals.push(id);
|
|
321
|
+
await this.driver.exec(`UPDATE tasks SET ${sets.join(", ")} WHERE id = ?`, vals);
|
|
322
|
+
}
|
|
323
|
+
async replaceAcceptance(taskId, criteria) {
|
|
324
|
+
await this.driver.exec(`DELETE FROM task_acceptance WHERE task_id = ?`, [taskId]);
|
|
325
|
+
for (const criterion of criteria) {
|
|
326
|
+
await this.driver.exec(
|
|
327
|
+
`INSERT INTO task_acceptance (task_id, criterion) VALUES (?, ?)`,
|
|
328
|
+
[taskId, criterion]
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async archive(id) {
|
|
333
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
334
|
+
await this.driver.exec(`UPDATE tasks SET archived_at = ?, updated_at = ? WHERE id = ?`, [now, now, id]);
|
|
335
|
+
}
|
|
336
|
+
async unarchive(id) {
|
|
337
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
338
|
+
await this.driver.exec(`UPDATE tasks SET archived_at = NULL, updated_at = ? WHERE id = ?`, [now, id]);
|
|
339
|
+
}
|
|
340
|
+
async getArchived() {
|
|
341
|
+
return this.driver.query(
|
|
342
|
+
`SELECT * FROM tasks WHERE archived_at IS NOT NULL ORDER BY archived_at DESC`
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
async claim(id, agent, now) {
|
|
346
|
+
return this.driver.exec(
|
|
347
|
+
`UPDATE tasks SET status = 'in_progress', assigned_to = ?, started_at = ?, updated_at = ? WHERE id = ? AND status = 'pending'`,
|
|
348
|
+
[agent, now, now, id]
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
async markAcceptanceMet(criterionId) {
|
|
352
|
+
await this.driver.exec(`UPDATE task_acceptance SET met = 1 WHERE id = ?`, [criterionId]);
|
|
353
|
+
}
|
|
354
|
+
async getStatusSummary() {
|
|
355
|
+
return this.driver.query(
|
|
356
|
+
`SELECT status, COUNT(*) as total FROM tasks WHERE archived_at IS NULL GROUP BY status`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
// src/core/db.ts
|
|
362
|
+
var AUTOINCREMENT_TABLES = ["tasks", "task_acceptance", "action_sections", "action_files", "action_tools"];
|
|
363
|
+
var TABLE_INSERT_ORDER = ["tasks", "task_acceptance", "actions", "action_sections", "action_files", "action_tools"];
|
|
364
|
+
var TABLE_DELETE_ORDER = [...TABLE_INSERT_ORDER].reverse();
|
|
365
|
+
function resolveGlobalStorageDir(config, homeDir = homedir()) {
|
|
366
|
+
return join(homeDir, ".harness", "dbs", config.storage.projectId);
|
|
367
|
+
}
|
|
368
|
+
var HarnessDB = class {
|
|
369
|
+
tasks;
|
|
370
|
+
actions;
|
|
371
|
+
stats;
|
|
372
|
+
driver;
|
|
373
|
+
config;
|
|
374
|
+
/** Overridable home directory, used to keep 'global' scope tests off the real $HOME. */
|
|
375
|
+
homeDir;
|
|
376
|
+
constructor(driver, config, homeDir = homedir()) {
|
|
377
|
+
this.driver = driver;
|
|
378
|
+
this.config = config;
|
|
379
|
+
this.homeDir = homeDir;
|
|
380
|
+
this.tasks = new TaskRepository(driver);
|
|
381
|
+
this.actions = new ActionRepository(driver);
|
|
382
|
+
this.stats = new StatsRepository(driver);
|
|
383
|
+
}
|
|
384
|
+
// ─── Tasks (public facade — delegates to TaskRepository) ──────────────────
|
|
385
|
+
async addTask(params) {
|
|
386
|
+
const taskId = await this.tasks.add({
|
|
387
|
+
slug: params.slug,
|
|
388
|
+
title: params.title,
|
|
389
|
+
description: params.description
|
|
390
|
+
});
|
|
391
|
+
if (params.acceptance?.length) {
|
|
392
|
+
await this.tasks.addAcceptance(taskId, params.acceptance);
|
|
393
|
+
}
|
|
394
|
+
await this.regenerateCurrentMd();
|
|
395
|
+
return await this.tasks.getById(taskId);
|
|
396
|
+
}
|
|
397
|
+
async getTasks(status, includeArchived = false) {
|
|
398
|
+
return this.tasks.getAll(status, includeArchived);
|
|
399
|
+
}
|
|
400
|
+
async getTaskById(id) {
|
|
401
|
+
return this.tasks.getById(id);
|
|
402
|
+
}
|
|
403
|
+
async getTaskBySlug(slug) {
|
|
404
|
+
return this.tasks.getBySlug(slug);
|
|
405
|
+
}
|
|
406
|
+
async getTaskAcceptance(taskId) {
|
|
407
|
+
return this.tasks.getAcceptance(taskId);
|
|
408
|
+
}
|
|
409
|
+
async updateTaskStatus(idOrSlug, status) {
|
|
410
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
411
|
+
const task = typeof idOrSlug === "number" ? await this.tasks.getById(idOrSlug) : await this.tasks.getBySlug(idOrSlug);
|
|
412
|
+
if (!task) throw new Error(`Task not found: ${idOrSlug}`);
|
|
413
|
+
if (status === "in_progress" && !task.started_at) {
|
|
414
|
+
await this.tasks.setStatus(task.id, status, { started_at: now });
|
|
415
|
+
} else if (status === "done") {
|
|
416
|
+
await this.tasks.setStatus(task.id, status, { completed_at: now });
|
|
417
|
+
} else {
|
|
418
|
+
await this.tasks.setStatus(task.id, status);
|
|
419
|
+
}
|
|
420
|
+
await this.regenerateCurrentMd();
|
|
421
|
+
return await this.tasks.getById(task.id);
|
|
422
|
+
}
|
|
423
|
+
async claimTask(id, agent) {
|
|
424
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
425
|
+
return this.driver.transaction(async (tx) => {
|
|
426
|
+
const txTasks = new TaskRepository(tx);
|
|
427
|
+
const changed = await txTasks.claim(id, agent, now);
|
|
428
|
+
if (!changed) return null;
|
|
429
|
+
const task = await txTasks.getById(id);
|
|
430
|
+
if (!task || task.status !== "in_progress" || task.assigned_to !== agent) return null;
|
|
431
|
+
await this.regenerateCurrentMd();
|
|
432
|
+
return task;
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
async markAcceptanceMet(criterionId) {
|
|
436
|
+
return this.tasks.markAcceptanceMet(criterionId);
|
|
437
|
+
}
|
|
438
|
+
async updateTask(id, params) {
|
|
439
|
+
await this.tasks.update(id, params);
|
|
440
|
+
await this.regenerateCurrentMd();
|
|
441
|
+
return await this.tasks.getById(id);
|
|
442
|
+
}
|
|
443
|
+
async updateTaskAcceptance(taskId, criteria) {
|
|
444
|
+
await this.tasks.replaceAcceptance(taskId, criteria);
|
|
445
|
+
await this.regenerateCurrentMd();
|
|
446
|
+
}
|
|
447
|
+
async archiveTask(id) {
|
|
448
|
+
await this.tasks.archive(id);
|
|
449
|
+
await this.regenerateCurrentMd();
|
|
450
|
+
return await this.tasks.getById(id);
|
|
451
|
+
}
|
|
452
|
+
async unarchiveTask(id) {
|
|
453
|
+
await this.tasks.unarchive(id);
|
|
454
|
+
await this.regenerateCurrentMd();
|
|
455
|
+
return await this.tasks.getById(id);
|
|
456
|
+
}
|
|
457
|
+
async getArchivedTasks() {
|
|
458
|
+
return this.tasks.getArchived();
|
|
459
|
+
}
|
|
460
|
+
async getStatusSummary() {
|
|
461
|
+
return this.tasks.getStatusSummary();
|
|
462
|
+
}
|
|
463
|
+
// ─── Actions (public facade — delegates to ActionRepository) ──────────────
|
|
464
|
+
async startAction(taskId, agent) {
|
|
465
|
+
const id = randomUUID();
|
|
466
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
467
|
+
await this.actions.create(id, taskId, agent, now);
|
|
468
|
+
await this.regenerateCurrentMd();
|
|
469
|
+
return await this.actions.getById(id);
|
|
470
|
+
}
|
|
471
|
+
async writeSection(actionId, sectionType, content) {
|
|
472
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
473
|
+
await this.actions.addSection(actionId, sectionType, content, now);
|
|
474
|
+
await this.regenerateCurrentMd();
|
|
475
|
+
}
|
|
476
|
+
async completeAction(actionId, summary) {
|
|
477
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
478
|
+
await this.actions.complete(actionId, summary, now);
|
|
479
|
+
await this.regenerateCurrentMd();
|
|
480
|
+
return await this.actions.getById(actionId);
|
|
481
|
+
}
|
|
482
|
+
async closeOrphanedActions(taskId) {
|
|
483
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
484
|
+
return this.actions.closeOrphaned(taskId, now);
|
|
485
|
+
}
|
|
486
|
+
async getAction(actionId) {
|
|
487
|
+
return this.actions.getById(actionId);
|
|
488
|
+
}
|
|
489
|
+
async getActionsForTask(taskId) {
|
|
490
|
+
return this.actions.getForTask(taskId);
|
|
491
|
+
}
|
|
492
|
+
async getActionSections(actionId) {
|
|
493
|
+
return this.actions.getSections(actionId);
|
|
494
|
+
}
|
|
495
|
+
async recordFile(actionId, filePath, operation, notes) {
|
|
496
|
+
return this.actions.addFile(actionId, filePath, operation, notes ?? null);
|
|
497
|
+
}
|
|
498
|
+
async recordTool(actionId, toolName, argsJson, resultSummary) {
|
|
499
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
500
|
+
return this.actions.addTool(actionId, toolName, argsJson ?? null, resultSummary ?? null, now);
|
|
501
|
+
}
|
|
502
|
+
async getFilesForTask(taskId) {
|
|
503
|
+
return this.actions.getFilesForTask(taskId);
|
|
504
|
+
}
|
|
505
|
+
async getTopTools(limit = 10) {
|
|
506
|
+
return this.actions.getTopTools(limit);
|
|
507
|
+
}
|
|
508
|
+
// ─── current.md fallback ──────────────────────────────────────────────────
|
|
509
|
+
async regenerateCurrentMd() {
|
|
510
|
+
if (!this.config.storage.markdownFallback.enabled) return;
|
|
511
|
+
const mdPath = this.config.storage.scope === "global" ? join(resolveGlobalStorageDir(this.config, this.homeDir), "current.md") : resolve(this.config.storage.markdownFallback.path);
|
|
512
|
+
mkdirSync(dirname(mdPath), { recursive: true });
|
|
513
|
+
const inProgress = await this.tasks.getAll("in_progress");
|
|
514
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
515
|
+
let md = `<!-- AUTO-GENERATED by agent-harness-kit \u2014 DO NOT EDIT MANUALLY -->
|
|
516
|
+
`;
|
|
517
|
+
md += `<!-- Last updated: ${now} -->
|
|
518
|
+
|
|
519
|
+
`;
|
|
520
|
+
md += `# Current Session
|
|
521
|
+
|
|
522
|
+
`;
|
|
523
|
+
if (inProgress.length === 0) {
|
|
524
|
+
md += `## No tasks in progress
|
|
525
|
+
|
|
526
|
+
`;
|
|
527
|
+
const pending = await this.tasks.getAll("pending");
|
|
528
|
+
if (pending.length > 0) {
|
|
529
|
+
md += `### Next pending tasks
|
|
530
|
+
`;
|
|
531
|
+
for (const t of pending.slice(0, 5)) {
|
|
532
|
+
md += `- **#${t.id}** ${t.title} (\`${t.slug}\`)
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
} else {
|
|
537
|
+
for (const task of inProgress) {
|
|
538
|
+
md += `## Active Task
|
|
539
|
+
`;
|
|
540
|
+
md += `- **ID:** ${task.id}
|
|
541
|
+
`;
|
|
542
|
+
md += `- **Slug:** ${task.slug}
|
|
543
|
+
`;
|
|
544
|
+
md += `- **Status:** ${task.status}
|
|
545
|
+
`;
|
|
546
|
+
md += `- **Started:** ${task.started_at ?? "unknown"}
|
|
547
|
+
|
|
548
|
+
`;
|
|
549
|
+
const taskActions = await this.actions.getForTask(task.id);
|
|
550
|
+
if (taskActions.length > 0) {
|
|
551
|
+
md += `## Actions this session
|
|
552
|
+
`;
|
|
553
|
+
md += `| Agent | Status | Summary | Started |
|
|
554
|
+
`;
|
|
555
|
+
md += `|----------|-------------|----------------------------------|-------------|
|
|
556
|
+
`;
|
|
557
|
+
for (const a of taskActions) {
|
|
558
|
+
const started = a.created_at.slice(11, 16);
|
|
559
|
+
const summary = (a.summary ?? "").slice(0, 34).padEnd(34);
|
|
560
|
+
md += `| ${a.agent.padEnd(8)} | ${a.status.padEnd(11)} | ${summary} | ${started} |
|
|
561
|
+
`;
|
|
562
|
+
}
|
|
563
|
+
md += `
|
|
564
|
+
`;
|
|
565
|
+
}
|
|
566
|
+
const acceptance = await this.tasks.getAcceptance(task.id);
|
|
567
|
+
if (acceptance.length > 0) {
|
|
568
|
+
md += `## Acceptance Criteria
|
|
569
|
+
`;
|
|
570
|
+
for (const a of acceptance) {
|
|
571
|
+
md += `- [${a.met ? "x" : " "}] ${a.criterion}
|
|
572
|
+
`;
|
|
573
|
+
}
|
|
574
|
+
md += `
|
|
575
|
+
`;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
writeFileSync(mdPath, md, "utf8");
|
|
580
|
+
}
|
|
581
|
+
// ─── Raw query escape hatch ───────────────────────────────────────────────
|
|
582
|
+
async queryRaw(sql, ...params) {
|
|
583
|
+
return this.driver.query(sql, params);
|
|
584
|
+
}
|
|
585
|
+
// ─── Export helpers ───────────────────────────────────────────────────────
|
|
586
|
+
/** Full relational export of ALL 6 tables (tasks, task_acceptance, actions,
|
|
587
|
+
* action_sections, action_files, action_tools). Extended for task #47 —
|
|
588
|
+
* the previous version (tasks/actions/sections only) silently dropped
|
|
589
|
+
* acceptance criteria and file/tool records on export/migrate. */
|
|
590
|
+
async exportJson() {
|
|
591
|
+
return {
|
|
592
|
+
tasks: await this.tasks.getAll(void 0, true),
|
|
593
|
+
taskAcceptance: await this.tasks.getAllAcceptance(),
|
|
594
|
+
actions: await this.actions.getAll(),
|
|
595
|
+
sections: await this.actions.getAllSections(),
|
|
596
|
+
actionFiles: await this.actions.getAllFiles(),
|
|
597
|
+
actionTools: await this.actions.getAllTools()
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
/** Row counts for all 6 tables against THIS db's driver — used to decide
|
|
601
|
+
* whether a destination is "empty" (safe to import into directly) before
|
|
602
|
+
* a migration. Counts are queried directly (COUNT(*)), never inferred
|
|
603
|
+
* from storage-state.json. */
|
|
604
|
+
async getRowCounts() {
|
|
605
|
+
return getRowCounts(this.driver);
|
|
606
|
+
}
|
|
607
|
+
/** Imports a full export into THIS db's driver — see standalone
|
|
608
|
+
* `importFullExport()` for the transactional/rollback/sequence-reset
|
|
609
|
+
* guarantees. `dbType` must match `this.config.database.type`. */
|
|
610
|
+
async importFullExport(data, dbType, opts) {
|
|
611
|
+
return importFullExport(this.driver, data, dbType, opts);
|
|
612
|
+
}
|
|
613
|
+
async reconnect() {
|
|
614
|
+
await this.driver.reconnect();
|
|
615
|
+
}
|
|
616
|
+
async close() {
|
|
617
|
+
await this.driver.close();
|
|
618
|
+
}
|
|
619
|
+
// ─── feature_list.json sync ───────────────────────────────────────────────
|
|
620
|
+
async syncFromFeatureList(seeds) {
|
|
621
|
+
let added = 0;
|
|
622
|
+
let skipped = 0;
|
|
623
|
+
for (const t of seeds) {
|
|
624
|
+
if (await this.tasks.getBySlug(t.slug)) {
|
|
625
|
+
skipped++;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
await this.addTask(t);
|
|
629
|
+
added++;
|
|
630
|
+
}
|
|
631
|
+
return { added, skipped };
|
|
632
|
+
}
|
|
633
|
+
async writeFeatureList(cwd) {
|
|
634
|
+
const allTasks = await this.tasks.getAll(void 0, true);
|
|
635
|
+
const list = await Promise.all(
|
|
636
|
+
allTasks.map(async (t) => ({
|
|
637
|
+
slug: t.slug,
|
|
638
|
+
title: t.title,
|
|
639
|
+
description: t.description ?? void 0,
|
|
640
|
+
acceptance: (await this.tasks.getAcceptance(t.id)).map((a) => a.criterion),
|
|
641
|
+
status: t.status
|
|
642
|
+
}))
|
|
643
|
+
);
|
|
644
|
+
const path = join(resolve(cwd), this.config.storage.dir, "feature_list.json");
|
|
645
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
646
|
+
writeFileSync(path, JSON.stringify(list, null, 2) + "\n", "utf8");
|
|
647
|
+
}
|
|
648
|
+
// ─── storage-state.json (real storage state, for `ahk migrate storage`) ──
|
|
649
|
+
/** Writes .harness/storage-state.json, ALWAYS project-local regardless of
|
|
650
|
+
* scope. Reflects the REAL current storage state (scope/projectId/dbType
|
|
651
|
+
* actually in use right now), as opposed to agent-harness-kit.config.ts
|
|
652
|
+
* which reflects the DESIRED state. Format is stable — task #47 (ahk
|
|
653
|
+
* migrate storage) depends on it; do not change field names/shape. */
|
|
654
|
+
async writeStorageState(cwd) {
|
|
655
|
+
writeStorageStateFile(cwd, this.config.storage.dir, {
|
|
656
|
+
scope: this.config.storage.scope,
|
|
657
|
+
projectId: this.config.storage.projectId,
|
|
658
|
+
dbType: this.config.database.type,
|
|
659
|
+
migratedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
};
|
|
663
|
+
async function getRowCounts(driver) {
|
|
664
|
+
const counts = {};
|
|
665
|
+
for (const table of TABLE_INSERT_ORDER) {
|
|
666
|
+
const row = await driver.queryOne(`SELECT COUNT(*) as n FROM ${table}`);
|
|
667
|
+
counts[table] = Number(row?.n ?? 0);
|
|
668
|
+
}
|
|
669
|
+
return counts;
|
|
670
|
+
}
|
|
671
|
+
async function isEmptyDatabase(driver) {
|
|
672
|
+
const counts = await getRowCounts(driver);
|
|
673
|
+
return Object.values(counts).every((n) => n === 0);
|
|
674
|
+
}
|
|
675
|
+
async function truncateAllTables(tx) {
|
|
676
|
+
for (const table of TABLE_DELETE_ORDER) {
|
|
677
|
+
await tx.exec(`DELETE FROM ${table}`);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
async function resetAutoincrementSequences(tx, dbType) {
|
|
681
|
+
if (dbType === "mysql") return;
|
|
682
|
+
for (const table of AUTOINCREMENT_TABLES) {
|
|
683
|
+
const row = await tx.queryOne(`SELECT MAX(id) as max FROM ${table}`);
|
|
684
|
+
const max = row?.max;
|
|
685
|
+
if (!max) continue;
|
|
686
|
+
if (dbType === "postgres") {
|
|
687
|
+
await tx.execRaw(`SELECT setval(pg_get_serial_sequence('${table}','id'), ${max}, true)`);
|
|
688
|
+
} else {
|
|
689
|
+
await tx.execRaw(
|
|
690
|
+
`INSERT INTO sqlite_sequence (name, seq) SELECT '${table}', ${max} WHERE NOT EXISTS (SELECT 1 FROM sqlite_sequence WHERE name = '${table}')`
|
|
691
|
+
);
|
|
692
|
+
await tx.execRaw(`UPDATE sqlite_sequence SET seq = ${max} WHERE name = '${table}'`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
async function importFullExport(destDriver, data, destDbType, opts = { truncateFirst: false }) {
|
|
697
|
+
await destDriver.transaction(async (tx) => {
|
|
698
|
+
if (opts.truncateFirst) {
|
|
699
|
+
await truncateAllTables(tx);
|
|
700
|
+
}
|
|
701
|
+
for (const task of data.tasks) {
|
|
702
|
+
await tx.exec(
|
|
703
|
+
`INSERT INTO tasks (id, slug, title, description, status, assigned_to, created_at, started_at, completed_at, archived_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
704
|
+
[
|
|
705
|
+
task.id,
|
|
706
|
+
task.slug,
|
|
707
|
+
task.title,
|
|
708
|
+
task.description,
|
|
709
|
+
task.status,
|
|
710
|
+
task.assigned_to,
|
|
711
|
+
task.created_at,
|
|
712
|
+
task.started_at,
|
|
713
|
+
task.completed_at,
|
|
714
|
+
task.archived_at,
|
|
715
|
+
task.updated_at
|
|
716
|
+
]
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
for (const ta of data.taskAcceptance) {
|
|
720
|
+
await tx.exec(
|
|
721
|
+
`INSERT INTO task_acceptance (id, task_id, criterion, met) VALUES (?, ?, ?, ?)`,
|
|
722
|
+
[ta.id, ta.task_id, ta.criterion, ta.met]
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
for (const action of data.actions) {
|
|
726
|
+
await tx.exec(
|
|
727
|
+
`INSERT INTO actions (id, task_id, agent, status, created_at, completed_at, summary) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
728
|
+
[action.id, action.task_id, action.agent, action.status, action.created_at, action.completed_at, action.summary]
|
|
729
|
+
);
|
|
730
|
+
}
|
|
731
|
+
for (const section of data.sections) {
|
|
732
|
+
await tx.exec(
|
|
733
|
+
`INSERT INTO action_sections (id, action_id, section_type, content, created_at) VALUES (?, ?, ?, ?, ?)`,
|
|
734
|
+
[section.id, section.action_id, section.section_type, section.content, section.created_at]
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
for (const file of data.actionFiles) {
|
|
738
|
+
await tx.exec(
|
|
739
|
+
`INSERT INTO action_files (id, action_id, file_path, operation, notes) VALUES (?, ?, ?, ?, ?)`,
|
|
740
|
+
[file.id, file.action_id, file.file_path, file.operation, file.notes]
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
for (const tool of data.actionTools) {
|
|
744
|
+
await tx.exec(
|
|
745
|
+
`INSERT INTO action_tools (id, action_id, tool_name, args_json, result_summary, called_at) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
746
|
+
[tool.id, tool.action_id, tool.tool_name, tool.args_json, tool.result_summary, tool.called_at]
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
await resetAutoincrementSequences(tx, destDbType);
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
function resolveSqlitePathForScope(scope, sqlitePath, cwd, config, homeDir) {
|
|
753
|
+
return scope === "global" ? join(resolveGlobalStorageDir(config, homeDir), "harness.db") : resolve(cwd, sqlitePath);
|
|
754
|
+
}
|
|
755
|
+
function writeStorageStateFile(cwd, storageDir, state) {
|
|
756
|
+
const path = join(resolve(cwd), storageDir, "storage-state.json");
|
|
757
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
758
|
+
writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
759
|
+
}
|
|
760
|
+
function readStorageStateFile(cwd, storageDir) {
|
|
761
|
+
try {
|
|
762
|
+
const path = join(resolve(cwd), storageDir, "storage-state.json");
|
|
763
|
+
if (!existsSync(path)) return null;
|
|
764
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
765
|
+
} catch {
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
async function openDB(config, cwd, homeDir = homedir()) {
|
|
770
|
+
const dbConfig = config.database;
|
|
771
|
+
let driver;
|
|
772
|
+
if (dbConfig.type === "postgres") {
|
|
773
|
+
const { PostgresDriver } = await import("./postgres-IOQE32DM.js");
|
|
774
|
+
driver = new PostgresDriver(dbConfig);
|
|
775
|
+
} else if (dbConfig.type === "mysql") {
|
|
776
|
+
const { MySQLDriver } = await import("./mysql-THKQOXIS.js");
|
|
777
|
+
driver = new MySQLDriver(dbConfig);
|
|
778
|
+
} else {
|
|
779
|
+
const { SQLiteDriver } = await import("./sqlite-TR4D324R.js");
|
|
780
|
+
if (dbConfig.type !== "sqlite") {
|
|
781
|
+
throw new Error("Invalid database type");
|
|
782
|
+
}
|
|
783
|
+
let dbPath;
|
|
784
|
+
if (config.storage.scope === "global") {
|
|
785
|
+
const globalDir = resolveGlobalStorageDir(config, homeDir);
|
|
786
|
+
const existingStatePath = join(globalDir, "storage-state.json");
|
|
787
|
+
if (existsSync(existingStatePath)) {
|
|
788
|
+
try {
|
|
789
|
+
const existingState = JSON.parse(readFileSync(existingStatePath, "utf8"));
|
|
790
|
+
if (existingState.projectId !== config.storage.projectId) {
|
|
791
|
+
throw new Error(
|
|
792
|
+
`Global storage dir ${globalDir} already holds a different project (projectId: ${existingState.projectId}). Refusing to reuse it.`
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
} catch (err) {
|
|
796
|
+
if (err instanceof Error && err.message.includes("already holds a different project")) throw err;
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
mkdirSync(globalDir, { recursive: true });
|
|
800
|
+
dbPath = join(globalDir, "harness.db");
|
|
801
|
+
} else {
|
|
802
|
+
dbPath = resolve(cwd, dbConfig.path);
|
|
803
|
+
}
|
|
804
|
+
driver = new SQLiteDriver(dbPath);
|
|
805
|
+
}
|
|
806
|
+
await driver.ensureSchema();
|
|
807
|
+
return new HarnessDB(driver, config, homeDir);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
export {
|
|
811
|
+
resolveGlobalStorageDir,
|
|
812
|
+
HarnessDB,
|
|
813
|
+
getRowCounts,
|
|
814
|
+
isEmptyDatabase,
|
|
815
|
+
importFullExport,
|
|
816
|
+
resolveSqlitePathForScope,
|
|
817
|
+
writeStorageStateFile,
|
|
818
|
+
readStorageStateFile,
|
|
819
|
+
openDB
|
|
820
|
+
};
|
|
821
|
+
//# sourceMappingURL=chunk-DNFFWQWR.js.map
|