@mcoda/db 0.1.4
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/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/migrations/global/GlobalMigrations.d.ts +9 -0
- package/dist/migrations/global/GlobalMigrations.d.ts.map +1 -0
- package/dist/migrations/global/GlobalMigrations.js +319 -0
- package/dist/migrations/workspace/WorkspaceMigrations.d.ts +9 -0
- package/dist/migrations/workspace/WorkspaceMigrations.d.ts.map +1 -0
- package/dist/migrations/workspace/WorkspaceMigrations.js +246 -0
- package/dist/repositories/global/GlobalRepository.d.ts +71 -0
- package/dist/repositories/global/GlobalRepository.d.ts.map +1 -0
- package/dist/repositories/global/GlobalRepository.js +281 -0
- package/dist/repositories/workspace/WorkspaceRepository.d.ts +341 -0
- package/dist/repositories/workspace/WorkspaceRepository.d.ts.map +1 -0
- package/dist/repositories/workspace/WorkspaceRepository.js +871 -0
- package/dist/sqlite/connection.d.ts +12 -0
- package/dist/sqlite/connection.d.ts.map +1 -0
- package/dist/sqlite/connection.js +32 -0
- package/dist/sqlite/pragmas.d.ts +5 -0
- package/dist/sqlite/pragmas.d.ts.map +1 -0
- package/dist/sqlite/pragmas.js +8 -0
- package/package.json +42 -0
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { Connection } from "../../sqlite/connection.js";
|
|
4
|
+
import { WorkspaceMigrations } from "../../migrations/workspace/WorkspaceMigrations.js";
|
|
5
|
+
export class WorkspaceRepository {
|
|
6
|
+
constructor(db, connection) {
|
|
7
|
+
this.db = db;
|
|
8
|
+
this.connection = connection;
|
|
9
|
+
this.workspaceKey = connection?.dbPath ?? "workspace";
|
|
10
|
+
}
|
|
11
|
+
static async create(cwd) {
|
|
12
|
+
const connection = await Connection.openWorkspace(cwd);
|
|
13
|
+
await WorkspaceMigrations.run(connection.db);
|
|
14
|
+
return new WorkspaceRepository(connection.db, connection);
|
|
15
|
+
}
|
|
16
|
+
async close() {
|
|
17
|
+
if (this.connection) {
|
|
18
|
+
await this.connection.close();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
getDb() {
|
|
22
|
+
return this.db;
|
|
23
|
+
}
|
|
24
|
+
async serialize(fn) {
|
|
25
|
+
const key = this.workspaceKey;
|
|
26
|
+
const prev = WorkspaceRepository.txLocks.get(key) ?? Promise.resolve();
|
|
27
|
+
let release;
|
|
28
|
+
const next = new Promise((resolve) => {
|
|
29
|
+
release = resolve;
|
|
30
|
+
});
|
|
31
|
+
WorkspaceRepository.txLocks.set(key, prev
|
|
32
|
+
.catch(() => {
|
|
33
|
+
/* ignore */
|
|
34
|
+
})
|
|
35
|
+
.then(() => next));
|
|
36
|
+
try {
|
|
37
|
+
const result = await fn();
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
release();
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async withTransaction(fn) {
|
|
45
|
+
const MAX_RETRIES = 5;
|
|
46
|
+
const BASE_BACKOFF_MS = 200;
|
|
47
|
+
const run = async () => {
|
|
48
|
+
await this.db.exec("BEGIN IMMEDIATE");
|
|
49
|
+
try {
|
|
50
|
+
const result = await fn();
|
|
51
|
+
await this.db.exec("COMMIT");
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
await this.db.exec("ROLLBACK");
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
60
|
+
try {
|
|
61
|
+
return await this.serialize(run);
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
const message = error.message ?? "";
|
|
65
|
+
const isBusy = message.includes("SQLITE_BUSY") || message.includes("database is locked") || message.includes("busy");
|
|
66
|
+
if (!isBusy || attempt === MAX_RETRIES) {
|
|
67
|
+
if (isBusy && attempt === MAX_RETRIES) {
|
|
68
|
+
console.warn(`Workspace DB is busy/locked after ${MAX_RETRIES} attempts for ${this.workspaceKey}. ` +
|
|
69
|
+
`If another mcoda command is running, please wait and retry.`);
|
|
70
|
+
}
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
const backoff = BASE_BACKOFF_MS * attempt;
|
|
74
|
+
await delay(backoff);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// Should never reach here
|
|
78
|
+
return this.serialize(run);
|
|
79
|
+
}
|
|
80
|
+
async getProjectByKey(key) {
|
|
81
|
+
const row = await this.db.get(`SELECT id, key, name, description, metadata_json, created_at, updated_at FROM projects WHERE key = ?`, key);
|
|
82
|
+
if (!row)
|
|
83
|
+
return undefined;
|
|
84
|
+
return {
|
|
85
|
+
id: row.id,
|
|
86
|
+
key: row.key,
|
|
87
|
+
name: row.name ?? undefined,
|
|
88
|
+
description: row.description ?? undefined,
|
|
89
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
90
|
+
createdAt: row.created_at,
|
|
91
|
+
updatedAt: row.updated_at,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async createProjectIfMissing(input) {
|
|
95
|
+
const existing = await this.getProjectByKey(input.key);
|
|
96
|
+
if (existing)
|
|
97
|
+
return existing;
|
|
98
|
+
const now = new Date().toISOString();
|
|
99
|
+
const id = randomUUID();
|
|
100
|
+
await this.db.run(`INSERT INTO projects (id, key, name, description, created_at, updated_at)
|
|
101
|
+
VALUES (?, ?, ?, ?, ?, ?)`, id, input.key, input.name ?? null, input.description ?? null, now, now);
|
|
102
|
+
return {
|
|
103
|
+
id,
|
|
104
|
+
key: input.key,
|
|
105
|
+
name: input.name,
|
|
106
|
+
description: input.description,
|
|
107
|
+
createdAt: now,
|
|
108
|
+
updatedAt: now,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async insertEpics(epics, useTransaction = true) {
|
|
112
|
+
const now = new Date().toISOString();
|
|
113
|
+
const rows = [];
|
|
114
|
+
const run = async () => {
|
|
115
|
+
for (const epic of epics) {
|
|
116
|
+
const id = randomUUID();
|
|
117
|
+
await this.db.run(`INSERT INTO epics (id, project_id, key, title, description, story_points_total, priority, metadata_json, created_at, updated_at)
|
|
118
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, epic.projectId, epic.key, epic.title, epic.description, epic.storyPointsTotal ?? null, epic.priority ?? null, epic.metadata ? JSON.stringify(epic.metadata) : null, now, now);
|
|
119
|
+
rows.push({ ...epic, id, createdAt: now, updatedAt: now });
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
if (useTransaction) {
|
|
123
|
+
await this.withTransaction(run);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
await run();
|
|
127
|
+
}
|
|
128
|
+
return rows;
|
|
129
|
+
}
|
|
130
|
+
async insertStories(stories, useTransaction = true) {
|
|
131
|
+
const now = new Date().toISOString();
|
|
132
|
+
const rows = [];
|
|
133
|
+
const run = async () => {
|
|
134
|
+
for (const story of stories) {
|
|
135
|
+
const id = randomUUID();
|
|
136
|
+
await this.db.run(`INSERT INTO user_stories (id, project_id, epic_id, key, title, description, acceptance_criteria, story_points_total, priority, metadata_json, created_at, updated_at)
|
|
137
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, story.projectId, story.epicId, story.key, story.title, story.description, story.acceptanceCriteria ?? null, story.storyPointsTotal ?? null, story.priority ?? null, story.metadata ? JSON.stringify(story.metadata) : null, now, now);
|
|
138
|
+
rows.push({ ...story, id, createdAt: now, updatedAt: now });
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
if (useTransaction) {
|
|
142
|
+
await this.withTransaction(run);
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
await run();
|
|
146
|
+
}
|
|
147
|
+
return rows;
|
|
148
|
+
}
|
|
149
|
+
async insertTasks(tasks, useTransaction = true) {
|
|
150
|
+
const now = new Date().toISOString();
|
|
151
|
+
const rows = [];
|
|
152
|
+
const run = async () => {
|
|
153
|
+
for (const task of tasks) {
|
|
154
|
+
const id = randomUUID();
|
|
155
|
+
await this.db.run(`INSERT INTO tasks (id, project_id, epic_id, user_story_id, key, title, description, type, status, story_points, priority, assigned_agent_id, assignee_human, vcs_branch, vcs_base_branch, vcs_last_commit_sha, metadata_json, openapi_version_at_creation, created_at, updated_at)
|
|
156
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, task.projectId, task.epicId, task.userStoryId, task.key, task.title, task.description, task.type ?? null, task.status, task.storyPoints ?? null, task.priority ?? null, task.assignedAgentId ?? null, task.assigneeHuman ?? null, task.vcsBranch ?? null, task.vcsBaseBranch ?? null, task.vcsLastCommitSha ?? null, task.metadata ? JSON.stringify(task.metadata) : null, task.openapiVersionAtCreation ?? null, now, now);
|
|
157
|
+
rows.push({ ...task, id, createdAt: now, updatedAt: now });
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
if (useTransaction) {
|
|
161
|
+
await this.withTransaction(run);
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
await run();
|
|
165
|
+
}
|
|
166
|
+
return rows;
|
|
167
|
+
}
|
|
168
|
+
async updateStoryPointsTotal(storyId, total) {
|
|
169
|
+
await this.db.run(`UPDATE user_stories SET story_points_total = ?, updated_at = ? WHERE id = ?`, total, new Date().toISOString(), storyId);
|
|
170
|
+
}
|
|
171
|
+
async updateEpicStoryPointsTotal(epicId, total) {
|
|
172
|
+
await this.db.run(`UPDATE epics SET story_points_total = ?, updated_at = ? WHERE id = ?`, total, new Date().toISOString(), epicId);
|
|
173
|
+
}
|
|
174
|
+
async insertTaskDependencies(deps, useTransaction = true) {
|
|
175
|
+
const now = new Date().toISOString();
|
|
176
|
+
const rows = [];
|
|
177
|
+
const run = async () => {
|
|
178
|
+
for (const dep of deps) {
|
|
179
|
+
const id = randomUUID();
|
|
180
|
+
await this.db.run(`INSERT INTO task_dependencies (id, task_id, depends_on_task_id, relation_type, created_at, updated_at)
|
|
181
|
+
VALUES (?, ?, ?, ?, ?, ?)`, id, dep.taskId, dep.dependsOnTaskId, dep.relationType, now, now);
|
|
182
|
+
rows.push({ ...dep, id, createdAt: now, updatedAt: now });
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
if (useTransaction) {
|
|
186
|
+
await this.withTransaction(run);
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
await run();
|
|
190
|
+
}
|
|
191
|
+
return rows;
|
|
192
|
+
}
|
|
193
|
+
async deleteTaskDependenciesForTask(taskId) {
|
|
194
|
+
await this.db.run(`DELETE FROM task_dependencies WHERE task_id = ? OR depends_on_task_id = ?`, taskId, taskId);
|
|
195
|
+
}
|
|
196
|
+
async deleteProjectBacklog(projectId, useTransaction = true) {
|
|
197
|
+
const run = async () => {
|
|
198
|
+
// Remove task-related rows first to satisfy foreign keys.
|
|
199
|
+
await this.db.run(`DELETE FROM task_dependencies WHERE task_id IN (SELECT id FROM tasks WHERE project_id = ?)
|
|
200
|
+
OR depends_on_task_id IN (SELECT id FROM tasks WHERE project_id = ?)`, projectId, projectId);
|
|
201
|
+
await this.db.run(`DELETE FROM task_runs WHERE task_id IN (SELECT id FROM tasks WHERE project_id = ?)`, projectId);
|
|
202
|
+
await this.db.run(`DELETE FROM task_qa_runs WHERE task_id IN (SELECT id FROM tasks WHERE project_id = ?)`, projectId);
|
|
203
|
+
await this.db.run(`DELETE FROM task_revisions WHERE task_id IN (SELECT id FROM tasks WHERE project_id = ?)`, projectId);
|
|
204
|
+
await this.db.run(`DELETE FROM task_comments WHERE task_id IN (SELECT id FROM tasks WHERE project_id = ?)`, projectId);
|
|
205
|
+
await this.db.run(`DELETE FROM task_reviews WHERE task_id IN (SELECT id FROM tasks WHERE project_id = ?)`, projectId);
|
|
206
|
+
await this.db.run(`DELETE FROM tasks WHERE project_id = ?`, projectId);
|
|
207
|
+
await this.db.run(`DELETE FROM user_stories WHERE project_id = ?`, projectId);
|
|
208
|
+
await this.db.run(`DELETE FROM epics WHERE project_id = ?`, projectId);
|
|
209
|
+
};
|
|
210
|
+
if (useTransaction) {
|
|
211
|
+
await this.withTransaction(run);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
await run();
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async updateTask(taskId, updates) {
|
|
218
|
+
const fields = [];
|
|
219
|
+
const params = [];
|
|
220
|
+
if (updates.title !== undefined) {
|
|
221
|
+
fields.push("title = ?");
|
|
222
|
+
params.push(updates.title);
|
|
223
|
+
}
|
|
224
|
+
if (updates.description !== undefined) {
|
|
225
|
+
fields.push("description = ?");
|
|
226
|
+
params.push(updates.description);
|
|
227
|
+
}
|
|
228
|
+
if (updates.type !== undefined) {
|
|
229
|
+
fields.push("type = ?");
|
|
230
|
+
params.push(updates.type);
|
|
231
|
+
}
|
|
232
|
+
if (updates.status !== undefined) {
|
|
233
|
+
fields.push("status = ?");
|
|
234
|
+
params.push(updates.status);
|
|
235
|
+
}
|
|
236
|
+
if (updates.storyPoints !== undefined) {
|
|
237
|
+
fields.push("story_points = ?");
|
|
238
|
+
params.push(updates.storyPoints);
|
|
239
|
+
}
|
|
240
|
+
if (updates.priority !== undefined) {
|
|
241
|
+
fields.push("priority = ?");
|
|
242
|
+
params.push(updates.priority);
|
|
243
|
+
}
|
|
244
|
+
if (updates.metadata !== undefined) {
|
|
245
|
+
fields.push("metadata_json = ?");
|
|
246
|
+
params.push(updates.metadata ? JSON.stringify(updates.metadata) : null);
|
|
247
|
+
}
|
|
248
|
+
if (updates.assignedAgentId !== undefined) {
|
|
249
|
+
fields.push("assigned_agent_id = ?");
|
|
250
|
+
params.push(updates.assignedAgentId);
|
|
251
|
+
}
|
|
252
|
+
if (updates.assigneeHuman !== undefined) {
|
|
253
|
+
fields.push("assignee_human = ?");
|
|
254
|
+
params.push(updates.assigneeHuman);
|
|
255
|
+
}
|
|
256
|
+
if (updates.vcsBranch !== undefined) {
|
|
257
|
+
fields.push("vcs_branch = ?");
|
|
258
|
+
params.push(updates.vcsBranch);
|
|
259
|
+
}
|
|
260
|
+
if (updates.vcsBaseBranch !== undefined) {
|
|
261
|
+
fields.push("vcs_base_branch = ?");
|
|
262
|
+
params.push(updates.vcsBaseBranch);
|
|
263
|
+
}
|
|
264
|
+
if (updates.vcsLastCommitSha !== undefined) {
|
|
265
|
+
fields.push("vcs_last_commit_sha = ?");
|
|
266
|
+
params.push(updates.vcsLastCommitSha);
|
|
267
|
+
}
|
|
268
|
+
if (fields.length === 0)
|
|
269
|
+
return;
|
|
270
|
+
fields.push("updated_at = ?");
|
|
271
|
+
params.push(new Date().toISOString());
|
|
272
|
+
params.push(taskId);
|
|
273
|
+
await this.db.run(`UPDATE tasks SET ${fields.join(", ")} WHERE id = ?`, ...params);
|
|
274
|
+
}
|
|
275
|
+
async getTaskById(taskId) {
|
|
276
|
+
const row = await this.db.get(`SELECT id, project_id, epic_id, user_story_id, key, title, description, type, status, story_points, priority, assigned_agent_id, assignee_human, vcs_branch, vcs_base_branch, vcs_last_commit_sha, metadata_json, openapi_version_at_creation, created_at, updated_at
|
|
277
|
+
FROM tasks WHERE id = ?`, taskId);
|
|
278
|
+
if (!row)
|
|
279
|
+
return undefined;
|
|
280
|
+
return {
|
|
281
|
+
id: row.id,
|
|
282
|
+
projectId: row.project_id,
|
|
283
|
+
epicId: row.epic_id,
|
|
284
|
+
userStoryId: row.user_story_id,
|
|
285
|
+
key: row.key,
|
|
286
|
+
title: row.title,
|
|
287
|
+
description: row.description ?? undefined,
|
|
288
|
+
type: row.type ?? undefined,
|
|
289
|
+
status: row.status,
|
|
290
|
+
storyPoints: row.story_points ?? undefined,
|
|
291
|
+
priority: row.priority ?? undefined,
|
|
292
|
+
assignedAgentId: row.assigned_agent_id ?? undefined,
|
|
293
|
+
assigneeHuman: row.assignee_human ?? undefined,
|
|
294
|
+
vcsBranch: row.vcs_branch ?? undefined,
|
|
295
|
+
vcsBaseBranch: row.vcs_base_branch ?? undefined,
|
|
296
|
+
vcsLastCommitSha: row.vcs_last_commit_sha ?? undefined,
|
|
297
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
298
|
+
openapiVersionAtCreation: row.openapi_version_at_creation ?? undefined,
|
|
299
|
+
createdAt: row.created_at,
|
|
300
|
+
updatedAt: row.updated_at,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async getTaskByKey(taskKey) {
|
|
304
|
+
const row = await this.db.get(`SELECT id, project_id, epic_id, user_story_id, key, title, description, type, status, story_points, priority, assigned_agent_id, assignee_human, vcs_branch, vcs_base_branch, vcs_last_commit_sha, metadata_json, openapi_version_at_creation, created_at, updated_at
|
|
305
|
+
FROM tasks WHERE key = ?`, taskKey);
|
|
306
|
+
if (!row)
|
|
307
|
+
return undefined;
|
|
308
|
+
return {
|
|
309
|
+
id: row.id,
|
|
310
|
+
projectId: row.project_id,
|
|
311
|
+
epicId: row.epic_id,
|
|
312
|
+
userStoryId: row.user_story_id,
|
|
313
|
+
key: row.key,
|
|
314
|
+
title: row.title,
|
|
315
|
+
description: row.description ?? undefined,
|
|
316
|
+
type: row.type ?? undefined,
|
|
317
|
+
status: row.status,
|
|
318
|
+
storyPoints: row.story_points ?? undefined,
|
|
319
|
+
priority: row.priority ?? undefined,
|
|
320
|
+
assignedAgentId: row.assigned_agent_id ?? undefined,
|
|
321
|
+
assigneeHuman: row.assignee_human ?? undefined,
|
|
322
|
+
vcsBranch: row.vcs_branch ?? undefined,
|
|
323
|
+
vcsBaseBranch: row.vcs_base_branch ?? undefined,
|
|
324
|
+
vcsLastCommitSha: row.vcs_last_commit_sha ?? undefined,
|
|
325
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
326
|
+
openapiVersionAtCreation: row.openapi_version_at_creation ?? undefined,
|
|
327
|
+
createdAt: row.created_at,
|
|
328
|
+
updatedAt: row.updated_at,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
async getTasksByIds(taskIds) {
|
|
332
|
+
if (!taskIds.length)
|
|
333
|
+
return [];
|
|
334
|
+
const placeholders = taskIds.map(() => "?").join(", ");
|
|
335
|
+
const rows = await this.db.all(`SELECT id, project_id, epic_id, user_story_id, key, title, description, type, status, story_points, priority, assigned_agent_id, assignee_human, vcs_branch, vcs_base_branch, vcs_last_commit_sha, metadata_json, openapi_version_at_creation, created_at, updated_at
|
|
336
|
+
FROM tasks WHERE id IN (${placeholders})`, ...taskIds);
|
|
337
|
+
return rows.map((row) => ({
|
|
338
|
+
id: row.id,
|
|
339
|
+
projectId: row.project_id,
|
|
340
|
+
epicId: row.epic_id,
|
|
341
|
+
userStoryId: row.user_story_id,
|
|
342
|
+
key: row.key,
|
|
343
|
+
title: row.title,
|
|
344
|
+
description: row.description ?? undefined,
|
|
345
|
+
type: row.type ?? undefined,
|
|
346
|
+
status: row.status,
|
|
347
|
+
storyPoints: row.story_points ?? undefined,
|
|
348
|
+
priority: row.priority ?? undefined,
|
|
349
|
+
assignedAgentId: row.assigned_agent_id ?? undefined,
|
|
350
|
+
assigneeHuman: row.assignee_human ?? undefined,
|
|
351
|
+
vcsBranch: row.vcs_branch ?? undefined,
|
|
352
|
+
vcsBaseBranch: row.vcs_base_branch ?? undefined,
|
|
353
|
+
vcsLastCommitSha: row.vcs_last_commit_sha ?? undefined,
|
|
354
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
355
|
+
openapiVersionAtCreation: row.openapi_version_at_creation ?? undefined,
|
|
356
|
+
createdAt: row.created_at,
|
|
357
|
+
updatedAt: row.updated_at,
|
|
358
|
+
}));
|
|
359
|
+
}
|
|
360
|
+
async listEpicKeys(projectId) {
|
|
361
|
+
const rows = await this.db.all(`SELECT key FROM epics WHERE project_id = ? ORDER BY key`, projectId);
|
|
362
|
+
return rows.map((r) => r.key);
|
|
363
|
+
}
|
|
364
|
+
async listStoryKeys(epicId) {
|
|
365
|
+
const rows = await this.db.all(`SELECT key FROM user_stories WHERE epic_id = ? ORDER BY key`, epicId);
|
|
366
|
+
return rows.map((r) => r.key);
|
|
367
|
+
}
|
|
368
|
+
async listTaskKeys(userStoryId) {
|
|
369
|
+
const rows = await this.db.all(`SELECT key FROM tasks WHERE user_story_id = ? ORDER BY key`, userStoryId);
|
|
370
|
+
return rows.map((r) => r.key);
|
|
371
|
+
}
|
|
372
|
+
async createJob(record) {
|
|
373
|
+
const now = new Date().toISOString();
|
|
374
|
+
const id = randomUUID();
|
|
375
|
+
await this.db.run(`INSERT INTO jobs (id, workspace_id, type, state, command_name, payload_json, total_items, processed_items, last_checkpoint, created_at, updated_at)
|
|
376
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, record.workspaceId, record.type, record.state, record.commandName ?? null, record.payload ? JSON.stringify(record.payload) : null, record.totalItems ?? null, record.processedItems ?? null, record.lastCheckpoint ?? null, now, now);
|
|
377
|
+
return {
|
|
378
|
+
id,
|
|
379
|
+
...record,
|
|
380
|
+
createdAt: now,
|
|
381
|
+
updatedAt: now,
|
|
382
|
+
completedAt: null,
|
|
383
|
+
errorSummary: null,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async listJobs() {
|
|
387
|
+
const rows = await this.db.all(`SELECT id, workspace_id, type, state, command_name, payload_json, total_items, processed_items, last_checkpoint, created_at, updated_at, completed_at, error_summary
|
|
388
|
+
FROM jobs ORDER BY updated_at DESC`);
|
|
389
|
+
return rows.map((row) => ({
|
|
390
|
+
id: row.id,
|
|
391
|
+
workspaceId: row.workspace_id,
|
|
392
|
+
type: row.type,
|
|
393
|
+
state: row.state,
|
|
394
|
+
commandName: row.command_name ?? undefined,
|
|
395
|
+
payload: row.payload_json ? JSON.parse(row.payload_json) : undefined,
|
|
396
|
+
totalItems: row.total_items ?? undefined,
|
|
397
|
+
processedItems: row.processed_items ?? undefined,
|
|
398
|
+
lastCheckpoint: row.last_checkpoint ?? undefined,
|
|
399
|
+
createdAt: row.created_at,
|
|
400
|
+
updatedAt: row.updated_at,
|
|
401
|
+
completedAt: row.completed_at ?? undefined,
|
|
402
|
+
errorSummary: row.error_summary ?? undefined,
|
|
403
|
+
}));
|
|
404
|
+
}
|
|
405
|
+
async getJob(id) {
|
|
406
|
+
const row = await this.db.get(`SELECT id, workspace_id, type, state, command_name, payload_json, total_items, processed_items, last_checkpoint, created_at, updated_at, completed_at, error_summary
|
|
407
|
+
FROM jobs WHERE id = ?`, id);
|
|
408
|
+
if (!row)
|
|
409
|
+
return undefined;
|
|
410
|
+
return {
|
|
411
|
+
id: row.id,
|
|
412
|
+
workspaceId: row.workspace_id,
|
|
413
|
+
type: row.type,
|
|
414
|
+
state: row.state,
|
|
415
|
+
commandName: row.command_name ?? undefined,
|
|
416
|
+
payload: row.payload_json ? JSON.parse(row.payload_json) : undefined,
|
|
417
|
+
totalItems: row.total_items ?? undefined,
|
|
418
|
+
processedItems: row.processed_items ?? undefined,
|
|
419
|
+
lastCheckpoint: row.last_checkpoint ?? undefined,
|
|
420
|
+
createdAt: row.created_at,
|
|
421
|
+
updatedAt: row.updated_at,
|
|
422
|
+
completedAt: row.completed_at ?? undefined,
|
|
423
|
+
errorSummary: row.error_summary ?? undefined,
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
async updateJobState(id, update) {
|
|
427
|
+
const existing = await this.db.get(`SELECT payload_json FROM jobs WHERE id = ?`, id);
|
|
428
|
+
const payload = existing?.payload_json ? JSON.parse(existing.payload_json) : undefined;
|
|
429
|
+
const mergedPayload = update.payload !== undefined ? { ...(payload ?? {}), ...(update.payload ?? {}) } : payload;
|
|
430
|
+
const fields = [];
|
|
431
|
+
const params = [];
|
|
432
|
+
if (update.state !== undefined) {
|
|
433
|
+
fields.push("state = ?");
|
|
434
|
+
params.push(update.state);
|
|
435
|
+
}
|
|
436
|
+
if (update.commandName !== undefined) {
|
|
437
|
+
fields.push("command_name = ?");
|
|
438
|
+
params.push(update.commandName ?? null);
|
|
439
|
+
}
|
|
440
|
+
if (update.totalItems !== undefined) {
|
|
441
|
+
fields.push("total_items = ?");
|
|
442
|
+
params.push(update.totalItems ?? null);
|
|
443
|
+
}
|
|
444
|
+
if (update.processedItems !== undefined) {
|
|
445
|
+
fields.push("processed_items = ?");
|
|
446
|
+
params.push(update.processedItems ?? null);
|
|
447
|
+
}
|
|
448
|
+
if (update.lastCheckpoint !== undefined) {
|
|
449
|
+
fields.push("last_checkpoint = ?");
|
|
450
|
+
params.push(update.lastCheckpoint ?? null);
|
|
451
|
+
}
|
|
452
|
+
if (update.errorSummary !== undefined) {
|
|
453
|
+
fields.push("error_summary = ?");
|
|
454
|
+
params.push(update.errorSummary ?? null);
|
|
455
|
+
}
|
|
456
|
+
if (update.completedAt !== undefined) {
|
|
457
|
+
fields.push("completed_at = ?");
|
|
458
|
+
params.push(update.completedAt ?? null);
|
|
459
|
+
}
|
|
460
|
+
if (mergedPayload !== undefined) {
|
|
461
|
+
fields.push("payload_json = ?");
|
|
462
|
+
params.push(JSON.stringify(mergedPayload));
|
|
463
|
+
}
|
|
464
|
+
fields.push("updated_at = ?");
|
|
465
|
+
params.push(new Date().toISOString());
|
|
466
|
+
params.push(id);
|
|
467
|
+
await this.db.run(`UPDATE jobs SET ${fields.join(", ")} WHERE id = ?`, ...params);
|
|
468
|
+
}
|
|
469
|
+
async createCommandRun(record) {
|
|
470
|
+
const id = randomUUID();
|
|
471
|
+
await this.db.run(`INSERT INTO command_runs (id, workspace_id, command_name, job_id, task_ids_json, git_branch, git_base_branch, started_at, status, sp_processed)
|
|
472
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, record.workspaceId, record.commandName, record.jobId ?? null, record.taskIds ? JSON.stringify(record.taskIds) : null, record.gitBranch ?? null, record.gitBaseBranch ?? null, record.startedAt, record.status, record.spProcessed ?? null);
|
|
473
|
+
return { id, ...record, completedAt: null, errorSummary: null, durationSeconds: null };
|
|
474
|
+
}
|
|
475
|
+
async setCommandRunJobId(id, jobId) {
|
|
476
|
+
await this.db.run(`UPDATE command_runs SET job_id = ? WHERE id = ?`, jobId, id);
|
|
477
|
+
}
|
|
478
|
+
async completeCommandRun(id, update) {
|
|
479
|
+
await this.db.run(`UPDATE command_runs
|
|
480
|
+
SET status = ?, completed_at = ?, error_summary = ?, duration_seconds = ?, sp_processed = ?
|
|
481
|
+
WHERE id = ?`, update.status, update.completedAt, update.errorSummary ?? null, update.durationSeconds ?? null, update.spProcessed ?? null, id);
|
|
482
|
+
}
|
|
483
|
+
async getTasksWithRelations(taskIds) {
|
|
484
|
+
if (!taskIds.length)
|
|
485
|
+
return [];
|
|
486
|
+
const placeholders = taskIds.map(() => "?").join(", ");
|
|
487
|
+
const rows = await this.db.all(`
|
|
488
|
+
SELECT
|
|
489
|
+
t.id as task_id,
|
|
490
|
+
t.project_id as project_id,
|
|
491
|
+
t.key as task_key,
|
|
492
|
+
t.status as task_status,
|
|
493
|
+
t.priority as task_priority,
|
|
494
|
+
t.story_points as task_story_points,
|
|
495
|
+
t.created_at as task_created_at,
|
|
496
|
+
t.updated_at as task_updated_at,
|
|
497
|
+
t.description as task_description,
|
|
498
|
+
t.title as task_title,
|
|
499
|
+
t.type as task_type,
|
|
500
|
+
t.metadata_json as task_metadata,
|
|
501
|
+
t.assigned_agent_id as task_assigned_agent_id,
|
|
502
|
+
t.assignee_human as task_assignee_human,
|
|
503
|
+
t.vcs_branch as task_vcs_branch,
|
|
504
|
+
t.vcs_base_branch as task_vcs_base_branch,
|
|
505
|
+
t.vcs_last_commit_sha as task_vcs_last_commit_sha,
|
|
506
|
+
e.id as epic_id,
|
|
507
|
+
e.key as epic_key,
|
|
508
|
+
e.title as epic_title,
|
|
509
|
+
e.description as epic_description,
|
|
510
|
+
us.id as story_id,
|
|
511
|
+
us.key as story_key,
|
|
512
|
+
us.title as story_title,
|
|
513
|
+
us.description as story_description,
|
|
514
|
+
us.acceptance_criteria as story_acceptance
|
|
515
|
+
FROM tasks t
|
|
516
|
+
JOIN epics e ON e.id = t.epic_id
|
|
517
|
+
JOIN user_stories us ON us.id = t.user_story_id
|
|
518
|
+
WHERE t.id IN (${placeholders})
|
|
519
|
+
`, ...taskIds);
|
|
520
|
+
return rows.map((row) => ({
|
|
521
|
+
id: row.task_id,
|
|
522
|
+
projectId: row.project_id,
|
|
523
|
+
epicId: row.epic_id,
|
|
524
|
+
userStoryId: row.story_id,
|
|
525
|
+
key: row.task_key,
|
|
526
|
+
title: row.task_title,
|
|
527
|
+
description: row.task_description ?? "",
|
|
528
|
+
type: row.task_type ?? undefined,
|
|
529
|
+
status: row.task_status,
|
|
530
|
+
storyPoints: row.task_story_points ?? undefined,
|
|
531
|
+
priority: row.task_priority ?? undefined,
|
|
532
|
+
assignedAgentId: row.task_assigned_agent_id ?? undefined,
|
|
533
|
+
assigneeHuman: row.task_assignee_human ?? undefined,
|
|
534
|
+
vcsBranch: row.task_vcs_branch ?? undefined,
|
|
535
|
+
vcsBaseBranch: row.task_vcs_base_branch ?? undefined,
|
|
536
|
+
vcsLastCommitSha: row.task_vcs_last_commit_sha ?? undefined,
|
|
537
|
+
metadata: row.task_metadata ? JSON.parse(row.task_metadata) : undefined,
|
|
538
|
+
openapiVersionAtCreation: undefined,
|
|
539
|
+
createdAt: row.task_created_at,
|
|
540
|
+
updatedAt: row.task_updated_at,
|
|
541
|
+
epicKey: row.epic_key,
|
|
542
|
+
storyKey: row.story_key,
|
|
543
|
+
epicTitle: row.epic_title ?? undefined,
|
|
544
|
+
epicDescription: row.epic_description ?? undefined,
|
|
545
|
+
storyTitle: row.story_title ?? undefined,
|
|
546
|
+
storyDescription: row.story_description ?? undefined,
|
|
547
|
+
acceptanceCriteria: row.story_acceptance
|
|
548
|
+
? row.story_acceptance
|
|
549
|
+
.split(/\r?\n/)
|
|
550
|
+
.map((s) => s.trim())
|
|
551
|
+
.filter(Boolean)
|
|
552
|
+
: undefined,
|
|
553
|
+
}));
|
|
554
|
+
}
|
|
555
|
+
async createTaskRun(record) {
|
|
556
|
+
const id = randomUUID();
|
|
557
|
+
await this.db.run(`INSERT INTO task_runs (id, task_id, command, job_id, command_run_id, agent_id, status, started_at, finished_at, story_points_at_run, sp_per_hour_effective, git_branch, git_base_branch, git_commit_sha, run_context_json)
|
|
558
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, record.taskId, record.command, record.jobId ?? null, record.commandRunId ?? null, record.agentId ?? null, record.status, record.startedAt, record.finishedAt ?? null, record.storyPointsAtRun ?? null, record.spPerHourEffective ?? null, record.gitBranch ?? null, record.gitBaseBranch ?? null, record.gitCommitSha ?? null, record.runContext ? JSON.stringify(record.runContext) : null);
|
|
559
|
+
return { id, ...record };
|
|
560
|
+
}
|
|
561
|
+
async getTaskLock(taskId) {
|
|
562
|
+
const row = await this.db.get(`SELECT task_id, task_run_id, job_id, acquired_at, expires_at FROM task_locks WHERE task_id = ?`, taskId);
|
|
563
|
+
if (!row)
|
|
564
|
+
return undefined;
|
|
565
|
+
return {
|
|
566
|
+
taskId: row.task_id,
|
|
567
|
+
taskRunId: row.task_run_id,
|
|
568
|
+
jobId: row.job_id ?? undefined,
|
|
569
|
+
acquiredAt: row.acquired_at,
|
|
570
|
+
expiresAt: row.expires_at,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
async tryAcquireTaskLock(taskId, taskRunId, jobId, ttlSeconds = 3600) {
|
|
574
|
+
const nowIso = new Date().toISOString();
|
|
575
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
576
|
+
return this.withTransaction(async () => {
|
|
577
|
+
const result = await this.db.run(`INSERT INTO task_locks (task_id, task_run_id, job_id, acquired_at, expires_at)
|
|
578
|
+
VALUES (?, ?, ?, ?, ?)
|
|
579
|
+
ON CONFLICT(task_id) DO UPDATE SET
|
|
580
|
+
task_run_id = excluded.task_run_id,
|
|
581
|
+
job_id = excluded.job_id,
|
|
582
|
+
acquired_at = excluded.acquired_at,
|
|
583
|
+
expires_at = excluded.expires_at
|
|
584
|
+
WHERE task_locks.expires_at < ?`, taskId, taskRunId, jobId ?? null, nowIso, expiresAt, nowIso);
|
|
585
|
+
if (result?.changes && result.changes > 0) {
|
|
586
|
+
return {
|
|
587
|
+
acquired: true,
|
|
588
|
+
lock: {
|
|
589
|
+
taskId,
|
|
590
|
+
taskRunId,
|
|
591
|
+
jobId: jobId ?? undefined,
|
|
592
|
+
acquiredAt: nowIso,
|
|
593
|
+
expiresAt,
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
const existing = await this.getTaskLock(taskId);
|
|
598
|
+
return { acquired: false, lock: existing };
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
async releaseTaskLock(taskId, taskRunId) {
|
|
602
|
+
await this.db.run(`DELETE FROM task_locks WHERE task_id = ? AND task_run_id = ?`, taskId, taskRunId);
|
|
603
|
+
}
|
|
604
|
+
async refreshTaskLock(taskId, taskRunId, ttlSeconds = 3600) {
|
|
605
|
+
const expiresAt = new Date(Date.now() + ttlSeconds * 1000).toISOString();
|
|
606
|
+
const result = await this.db.run(`UPDATE task_locks SET expires_at = ? WHERE task_id = ? AND task_run_id = ?`, expiresAt, taskId, taskRunId);
|
|
607
|
+
return Boolean(result?.changes && result.changes > 0);
|
|
608
|
+
}
|
|
609
|
+
async createTaskQaRun(record) {
|
|
610
|
+
const id = randomUUID();
|
|
611
|
+
const createdAt = record.createdAt ?? new Date().toISOString();
|
|
612
|
+
await this.db.run(`INSERT INTO task_qa_runs (id, task_id, task_run_id, job_id, command_run_id, agent_id, model_name, source, mode, profile_name, runner, raw_outcome, recommendation, evidence_url, artifacts_json, raw_result_json, started_at, finished_at, metadata_json, created_at)
|
|
613
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, record.taskId, record.taskRunId ?? null, record.jobId ?? null, record.commandRunId ?? null, record.agentId ?? null, record.modelName ?? null, record.source, record.mode ?? null, record.profileName ?? null, record.runner ?? null, record.rawOutcome ?? null, record.recommendation ?? null, record.evidenceUrl ?? null, record.artifacts ? JSON.stringify(record.artifacts) : null, record.rawResult ? JSON.stringify(record.rawResult) : null, record.startedAt ?? null, record.finishedAt ?? null, record.metadata ? JSON.stringify(record.metadata) : null, createdAt);
|
|
614
|
+
return { id, ...record, createdAt };
|
|
615
|
+
}
|
|
616
|
+
async listTaskQaRuns(taskId) {
|
|
617
|
+
const rows = await this.db.all(`SELECT id, task_id, task_run_id, job_id, command_run_id, agent_id, model_name, source, mode, profile_name, runner, raw_outcome, recommendation, evidence_url, artifacts_json, raw_result_json, started_at, finished_at, metadata_json, created_at
|
|
618
|
+
FROM task_qa_runs
|
|
619
|
+
WHERE task_id = ?
|
|
620
|
+
ORDER BY created_at DESC`, taskId);
|
|
621
|
+
return rows.map((row) => ({
|
|
622
|
+
id: row.id,
|
|
623
|
+
taskId: row.task_id,
|
|
624
|
+
taskRunId: row.task_run_id ?? undefined,
|
|
625
|
+
jobId: row.job_id ?? undefined,
|
|
626
|
+
commandRunId: row.command_run_id ?? undefined,
|
|
627
|
+
agentId: row.agent_id ?? undefined,
|
|
628
|
+
modelName: row.model_name ?? undefined,
|
|
629
|
+
source: row.source,
|
|
630
|
+
mode: row.mode ?? undefined,
|
|
631
|
+
profileName: row.profile_name ?? undefined,
|
|
632
|
+
runner: row.runner ?? undefined,
|
|
633
|
+
rawOutcome: row.raw_outcome ?? undefined,
|
|
634
|
+
recommendation: row.recommendation ?? undefined,
|
|
635
|
+
evidenceUrl: row.evidence_url ?? undefined,
|
|
636
|
+
artifacts: row.artifacts_json ? JSON.parse(row.artifacts_json) : undefined,
|
|
637
|
+
rawResult: row.raw_result_json ? JSON.parse(row.raw_result_json) : undefined,
|
|
638
|
+
startedAt: row.started_at ?? undefined,
|
|
639
|
+
finishedAt: row.finished_at ?? undefined,
|
|
640
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
641
|
+
createdAt: row.created_at,
|
|
642
|
+
}));
|
|
643
|
+
}
|
|
644
|
+
async listTaskQaRunsForJob(taskIds, jobId) {
|
|
645
|
+
if (!taskIds.length)
|
|
646
|
+
return [];
|
|
647
|
+
const placeholders = taskIds.map(() => '?').join(', ');
|
|
648
|
+
const rows = await this.db.all(`SELECT id, task_id, task_run_id, job_id, command_run_id, agent_id, model_name, source, mode, profile_name, runner, raw_outcome, recommendation, evidence_url, artifacts_json, raw_result_json, started_at, finished_at, metadata_json, created_at
|
|
649
|
+
FROM task_qa_runs
|
|
650
|
+
WHERE job_id = ? AND task_id IN (${placeholders})
|
|
651
|
+
ORDER BY created_at DESC`, jobId, ...taskIds);
|
|
652
|
+
return rows.map((row) => ({
|
|
653
|
+
id: row.id,
|
|
654
|
+
taskId: row.task_id,
|
|
655
|
+
taskRunId: row.task_run_id ?? undefined,
|
|
656
|
+
jobId: row.job_id ?? undefined,
|
|
657
|
+
commandRunId: row.command_run_id ?? undefined,
|
|
658
|
+
agentId: row.agent_id ?? undefined,
|
|
659
|
+
modelName: row.model_name ?? undefined,
|
|
660
|
+
source: row.source,
|
|
661
|
+
mode: row.mode ?? undefined,
|
|
662
|
+
profileName: row.profile_name ?? undefined,
|
|
663
|
+
runner: row.runner ?? undefined,
|
|
664
|
+
rawOutcome: row.raw_outcome ?? undefined,
|
|
665
|
+
recommendation: row.recommendation ?? undefined,
|
|
666
|
+
evidenceUrl: row.evidence_url ?? undefined,
|
|
667
|
+
artifacts: row.artifacts_json ? JSON.parse(row.artifacts_json) : undefined,
|
|
668
|
+
rawResult: row.raw_result_json ? JSON.parse(row.raw_result_json) : undefined,
|
|
669
|
+
startedAt: row.started_at ?? undefined,
|
|
670
|
+
finishedAt: row.finished_at ?? undefined,
|
|
671
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
672
|
+
createdAt: row.created_at,
|
|
673
|
+
}));
|
|
674
|
+
}
|
|
675
|
+
async insertTaskLog(entry) {
|
|
676
|
+
const id = randomUUID();
|
|
677
|
+
await this.db.run(`INSERT INTO task_logs (id, task_run_id, sequence, timestamp, level, source, message, details_json)
|
|
678
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, id, entry.taskRunId, entry.sequence, entry.timestamp, entry.level ?? null, entry.source ?? null, entry.message ?? null, entry.details ? JSON.stringify(entry.details) : null);
|
|
679
|
+
}
|
|
680
|
+
async updateTaskRun(id, update) {
|
|
681
|
+
const fields = [];
|
|
682
|
+
const params = [];
|
|
683
|
+
if (update.status !== undefined) {
|
|
684
|
+
fields.push("status = ?");
|
|
685
|
+
params.push(update.status);
|
|
686
|
+
}
|
|
687
|
+
if (update.finishedAt !== undefined) {
|
|
688
|
+
fields.push("finished_at = ?");
|
|
689
|
+
params.push(update.finishedAt);
|
|
690
|
+
}
|
|
691
|
+
if (update.gitBranch !== undefined) {
|
|
692
|
+
fields.push("git_branch = ?");
|
|
693
|
+
params.push(update.gitBranch);
|
|
694
|
+
}
|
|
695
|
+
if (update.gitBaseBranch !== undefined) {
|
|
696
|
+
fields.push("git_base_branch = ?");
|
|
697
|
+
params.push(update.gitBaseBranch);
|
|
698
|
+
}
|
|
699
|
+
if (update.gitCommitSha !== undefined) {
|
|
700
|
+
fields.push("git_commit_sha = ?");
|
|
701
|
+
params.push(update.gitCommitSha);
|
|
702
|
+
}
|
|
703
|
+
if (update.storyPointsAtRun !== undefined) {
|
|
704
|
+
fields.push("story_points_at_run = ?");
|
|
705
|
+
params.push(update.storyPointsAtRun);
|
|
706
|
+
}
|
|
707
|
+
if (update.spPerHourEffective !== undefined) {
|
|
708
|
+
fields.push("sp_per_hour_effective = ?");
|
|
709
|
+
params.push(update.spPerHourEffective);
|
|
710
|
+
}
|
|
711
|
+
if (update.runContext !== undefined) {
|
|
712
|
+
fields.push("run_context_json = ?");
|
|
713
|
+
params.push(update.runContext ? JSON.stringify(update.runContext) : null);
|
|
714
|
+
}
|
|
715
|
+
if (!fields.length)
|
|
716
|
+
return;
|
|
717
|
+
const clauses = fields.join(", ");
|
|
718
|
+
params.push(id);
|
|
719
|
+
await this.db.run(`UPDATE task_runs SET ${clauses} WHERE id = ?`, ...params);
|
|
720
|
+
}
|
|
721
|
+
async getTaskDependencies(taskIds) {
|
|
722
|
+
if (!taskIds.length)
|
|
723
|
+
return [];
|
|
724
|
+
const placeholders = taskIds.map(() => "?").join(", ");
|
|
725
|
+
const rows = await this.db.all(`SELECT id, task_id, depends_on_task_id, relation_type, created_at, updated_at
|
|
726
|
+
FROM task_dependencies
|
|
727
|
+
WHERE task_id IN (${placeholders})`, ...taskIds);
|
|
728
|
+
return rows.map((row) => ({
|
|
729
|
+
id: row.id,
|
|
730
|
+
taskId: row.task_id,
|
|
731
|
+
dependsOnTaskId: row.depends_on_task_id,
|
|
732
|
+
relationType: row.relation_type,
|
|
733
|
+
createdAt: row.created_at,
|
|
734
|
+
updatedAt: row.updated_at,
|
|
735
|
+
}));
|
|
736
|
+
}
|
|
737
|
+
async createTaskComment(record) {
|
|
738
|
+
const id = randomUUID();
|
|
739
|
+
await this.db.run(`INSERT INTO task_comments (id, task_id, task_run_id, job_id, source_command, author_type, author_agent_id, category, file, line, path_hint, body, metadata_json, created_at, resolved_at, resolved_by)
|
|
740
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, record.taskId, record.taskRunId ?? null, record.jobId ?? null, record.sourceCommand, record.authorType, record.authorAgentId ?? null, record.category ?? null, record.file ?? null, record.line ?? null, record.pathHint ?? null, record.body, record.metadata ? JSON.stringify(record.metadata) : null, record.createdAt, record.resolvedAt ?? null, record.resolvedBy ?? null);
|
|
741
|
+
return { ...record, id };
|
|
742
|
+
}
|
|
743
|
+
async listTaskComments(taskId, options = {}) {
|
|
744
|
+
const clauses = ["task_id = ?"];
|
|
745
|
+
const params = [taskId];
|
|
746
|
+
if (options.sourceCommands && options.sourceCommands.length) {
|
|
747
|
+
clauses.push(`source_command IN (${options.sourceCommands.map(() => "?").join(", ")})`);
|
|
748
|
+
params.push(...options.sourceCommands);
|
|
749
|
+
}
|
|
750
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
751
|
+
const limitClause = options.limit ? `LIMIT ${options.limit}` : "";
|
|
752
|
+
const rows = await this.db.all(`SELECT id, task_id, task_run_id, job_id, source_command, author_type, author_agent_id, category, file, line, path_hint, body, metadata_json, created_at, resolved_at, resolved_by
|
|
753
|
+
FROM task_comments
|
|
754
|
+
${where}
|
|
755
|
+
ORDER BY datetime(created_at) DESC
|
|
756
|
+
${limitClause}`, ...params);
|
|
757
|
+
return rows.map((row) => ({
|
|
758
|
+
id: row.id,
|
|
759
|
+
taskId: row.task_id,
|
|
760
|
+
taskRunId: row.task_run_id ?? undefined,
|
|
761
|
+
jobId: row.job_id ?? undefined,
|
|
762
|
+
sourceCommand: row.source_command,
|
|
763
|
+
authorType: row.author_type,
|
|
764
|
+
authorAgentId: row.author_agent_id ?? undefined,
|
|
765
|
+
category: row.category ?? undefined,
|
|
766
|
+
file: row.file ?? undefined,
|
|
767
|
+
line: row.line ?? undefined,
|
|
768
|
+
pathHint: row.path_hint ?? undefined,
|
|
769
|
+
body: row.body,
|
|
770
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
771
|
+
createdAt: row.created_at,
|
|
772
|
+
resolvedAt: row.resolved_at ?? undefined,
|
|
773
|
+
resolvedBy: row.resolved_by ?? undefined,
|
|
774
|
+
}));
|
|
775
|
+
}
|
|
776
|
+
async createTaskReview(record) {
|
|
777
|
+
const id = randomUUID();
|
|
778
|
+
await this.db.run(`INSERT INTO task_reviews (id, task_id, job_id, agent_id, model_name, decision, summary, findings_json, test_recommendations_json, metadata_json, created_at, created_by)
|
|
779
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, record.taskId, record.jobId ?? null, record.agentId ?? null, record.modelName ?? null, record.decision, record.summary ?? null, record.findingsJson ? JSON.stringify(record.findingsJson) : null, record.testRecommendationsJson ? JSON.stringify(record.testRecommendationsJson) : null, record.metadata ? JSON.stringify(record.metadata) : null, record.createdAt, record.createdBy ?? null);
|
|
780
|
+
return { ...record, id };
|
|
781
|
+
}
|
|
782
|
+
async getLatestTaskReview(taskId) {
|
|
783
|
+
const row = await this.db.get(`SELECT id, task_id, job_id, agent_id, model_name, decision, summary, findings_json, test_recommendations_json, metadata_json, created_at, created_by
|
|
784
|
+
FROM task_reviews
|
|
785
|
+
WHERE task_id = ?
|
|
786
|
+
ORDER BY datetime(created_at) DESC
|
|
787
|
+
LIMIT 1`, taskId);
|
|
788
|
+
if (!row)
|
|
789
|
+
return undefined;
|
|
790
|
+
return {
|
|
791
|
+
id: row.id,
|
|
792
|
+
taskId: row.task_id,
|
|
793
|
+
jobId: row.job_id ?? undefined,
|
|
794
|
+
agentId: row.agent_id ?? undefined,
|
|
795
|
+
modelName: row.model_name ?? undefined,
|
|
796
|
+
decision: row.decision,
|
|
797
|
+
summary: row.summary ?? undefined,
|
|
798
|
+
findingsJson: row.findings_json ? JSON.parse(row.findings_json) : undefined,
|
|
799
|
+
testRecommendationsJson: row.test_recommendations_json ? JSON.parse(row.test_recommendations_json) : undefined,
|
|
800
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
801
|
+
createdAt: row.created_at,
|
|
802
|
+
createdBy: row.created_by ?? undefined,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
async recordTokenUsage(entry) {
|
|
806
|
+
const id = randomUUID();
|
|
807
|
+
await this.db.run(`INSERT INTO token_usage (id, workspace_id, agent_id, model_name, job_id, command_run_id, task_run_id, task_id, project_id, epic_id, user_story_id, tokens_prompt, tokens_completion, tokens_total, cost_estimate, duration_seconds, timestamp, metadata_json)
|
|
808
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, id, entry.workspaceId, entry.agentId ?? null, entry.modelName ?? null, entry.jobId ?? null, entry.commandRunId ?? null, entry.taskRunId ?? null, entry.taskId ?? null, entry.projectId ?? null, entry.epicId ?? null, entry.userStoryId ?? null, entry.tokensPrompt ?? null, entry.tokensCompletion ?? null, entry.tokensTotal ?? null, entry.costEstimate ?? null, entry.durationSeconds ?? null, entry.timestamp, entry.metadata ? JSON.stringify(entry.metadata) : null);
|
|
809
|
+
}
|
|
810
|
+
async insertTaskRevision(record) {
|
|
811
|
+
const id = randomUUID();
|
|
812
|
+
await this.db.run(`INSERT INTO task_revisions (id, task_id, job_id, command_run_id, snapshot_before_json, snapshot_after_json, created_at)
|
|
813
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, id, record.taskId, record.jobId ?? null, record.commandRunId ?? null, record.snapshotBefore ? JSON.stringify(record.snapshotBefore) : null, record.snapshotAfter ? JSON.stringify(record.snapshotAfter) : null, record.createdAt);
|
|
814
|
+
}
|
|
815
|
+
async getEpicByKey(projectId, key) {
|
|
816
|
+
const row = await this.db.get(`SELECT id, project_id, key, title, description, story_points_total, priority, metadata_json, created_at, updated_at FROM epics WHERE project_id = ? AND key = ?`, projectId, key);
|
|
817
|
+
if (!row)
|
|
818
|
+
return undefined;
|
|
819
|
+
return {
|
|
820
|
+
id: row.id,
|
|
821
|
+
projectId: row.project_id,
|
|
822
|
+
key: row.key,
|
|
823
|
+
title: row.title,
|
|
824
|
+
description: row.description ?? undefined,
|
|
825
|
+
storyPointsTotal: row.story_points_total ?? undefined,
|
|
826
|
+
priority: row.priority ?? undefined,
|
|
827
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
828
|
+
createdAt: row.created_at,
|
|
829
|
+
updatedAt: row.updated_at,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
async getStoryByKey(epicId, key) {
|
|
833
|
+
const row = await this.db.get(`SELECT id, project_id, epic_id, key, title, description, acceptance_criteria, story_points_total, priority, metadata_json, created_at, updated_at FROM user_stories WHERE epic_id = ? AND key = ?`, epicId, key);
|
|
834
|
+
if (!row)
|
|
835
|
+
return undefined;
|
|
836
|
+
return {
|
|
837
|
+
id: row.id,
|
|
838
|
+
projectId: row.project_id,
|
|
839
|
+
epicId: row.epic_id,
|
|
840
|
+
key: row.key,
|
|
841
|
+
title: row.title,
|
|
842
|
+
description: row.description ?? undefined,
|
|
843
|
+
acceptanceCriteria: row.acceptance_criteria ?? undefined,
|
|
844
|
+
storyPointsTotal: row.story_points_total ?? undefined,
|
|
845
|
+
priority: row.priority ?? undefined,
|
|
846
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
847
|
+
createdAt: row.created_at,
|
|
848
|
+
updatedAt: row.updated_at,
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
async getStoryByProjectAndKey(projectId, key) {
|
|
852
|
+
const row = await this.db.get(`SELECT id, project_id, epic_id, key, title, description, acceptance_criteria, story_points_total, priority, metadata_json, created_at, updated_at FROM user_stories WHERE project_id = ? AND key = ?`, projectId, key);
|
|
853
|
+
if (!row)
|
|
854
|
+
return undefined;
|
|
855
|
+
return {
|
|
856
|
+
id: row.id,
|
|
857
|
+
projectId: row.project_id,
|
|
858
|
+
epicId: row.epic_id,
|
|
859
|
+
key: row.key,
|
|
860
|
+
title: row.title,
|
|
861
|
+
description: row.description ?? undefined,
|
|
862
|
+
acceptanceCriteria: row.acceptance_criteria ?? undefined,
|
|
863
|
+
storyPointsTotal: row.story_points_total ?? undefined,
|
|
864
|
+
priority: row.priority ?? undefined,
|
|
865
|
+
metadata: row.metadata_json ? JSON.parse(row.metadata_json) : undefined,
|
|
866
|
+
createdAt: row.created_at,
|
|
867
|
+
updatedAt: row.updated_at,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
WorkspaceRepository.txLocks = new Map();
|