agent-tasks 1.6.0 → 1.6.1

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 (91) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +181 -188
  3. package/package.json +84 -84
  4. package/dist/context.d.ts +0 -21
  5. package/dist/context.d.ts.map +0 -1
  6. package/dist/context.js +0 -48
  7. package/dist/context.js.map +0 -1
  8. package/dist/db.d.ts +0 -10
  9. package/dist/db.d.ts.map +0 -1
  10. package/dist/db.js +0 -112
  11. package/dist/db.js.map +0 -1
  12. package/dist/domain/agent-bridge.d.ts +0 -13
  13. package/dist/domain/agent-bridge.d.ts.map +0 -1
  14. package/dist/domain/agent-bridge.js +0 -99
  15. package/dist/domain/agent-bridge.js.map +0 -1
  16. package/dist/domain/approvals.d.ts +0 -19
  17. package/dist/domain/approvals.d.ts.map +0 -1
  18. package/dist/domain/approvals.js +0 -99
  19. package/dist/domain/approvals.js.map +0 -1
  20. package/dist/domain/cleanup.d.ts +0 -28
  21. package/dist/domain/cleanup.d.ts.map +0 -1
  22. package/dist/domain/cleanup.js +0 -68
  23. package/dist/domain/cleanup.js.map +0 -1
  24. package/dist/domain/collaborators.d.ts +0 -15
  25. package/dist/domain/collaborators.d.ts.map +0 -1
  26. package/dist/domain/collaborators.js +0 -72
  27. package/dist/domain/collaborators.js.map +0 -1
  28. package/dist/domain/comments.d.ts +0 -14
  29. package/dist/domain/comments.d.ts.map +0 -1
  30. package/dist/domain/comments.js +0 -66
  31. package/dist/domain/comments.js.map +0 -1
  32. package/dist/domain/events.d.ts +0 -10
  33. package/dist/domain/events.d.ts.map +0 -1
  34. package/dist/domain/events.js +0 -61
  35. package/dist/domain/events.js.map +0 -1
  36. package/dist/domain/rules.d.ts +0 -2
  37. package/dist/domain/rules.d.ts.map +0 -1
  38. package/dist/domain/rules.js +0 -67
  39. package/dist/domain/rules.js.map +0 -1
  40. package/dist/domain/tasks.d.ts +0 -66
  41. package/dist/domain/tasks.d.ts.map +0 -1
  42. package/dist/domain/tasks.js +0 -655
  43. package/dist/domain/tasks.js.map +0 -1
  44. package/dist/domain/validate.d.ts +0 -16
  45. package/dist/domain/validate.d.ts.map +0 -1
  46. package/dist/domain/validate.js +0 -32
  47. package/dist/domain/validate.js.map +0 -1
  48. package/dist/event-bus.d.ts +0 -10
  49. package/dist/event-bus.d.ts.map +0 -1
  50. package/dist/event-bus.js +0 -38
  51. package/dist/event-bus.js.map +0 -1
  52. package/dist/index.d.ts +0 -3
  53. package/dist/index.d.ts.map +0 -1
  54. package/dist/index.js +0 -132
  55. package/dist/index.js.map +0 -1
  56. package/dist/server.d.ts +0 -10
  57. package/dist/server.d.ts.map +0 -1
  58. package/dist/server.js +0 -95
  59. package/dist/server.js.map +0 -1
  60. package/dist/session.d.ts +0 -7
  61. package/dist/session.d.ts.map +0 -1
  62. package/dist/session.js +0 -11
  63. package/dist/session.js.map +0 -1
  64. package/dist/storage/database.d.ts +0 -15
  65. package/dist/storage/database.d.ts.map +0 -1
  66. package/dist/storage/database.js +0 -224
  67. package/dist/storage/database.js.map +0 -1
  68. package/dist/tasks.d.ts +0 -32
  69. package/dist/tasks.d.ts.map +0 -1
  70. package/dist/tasks.js +0 -410
  71. package/dist/tasks.js.map +0 -1
  72. package/dist/transport/mcp.d.ts +0 -6
  73. package/dist/transport/mcp.d.ts.map +0 -1
  74. package/dist/transport/mcp.js +0 -731
  75. package/dist/transport/mcp.js.map +0 -1
  76. package/dist/transport/rest.d.ts +0 -4
  77. package/dist/transport/rest.d.ts.map +0 -1
  78. package/dist/transport/rest.js +0 -534
  79. package/dist/transport/rest.js.map +0 -1
  80. package/dist/transport/ws.d.ts +0 -10
  81. package/dist/transport/ws.d.ts.map +0 -1
  82. package/dist/transport/ws.js +0 -191
  83. package/dist/transport/ws.js.map +0 -1
  84. package/dist/types.d.ts +0 -147
  85. package/dist/types.d.ts.map +0 -1
  86. package/dist/types.js +0 -35
  87. package/dist/types.js.map +0 -1
  88. package/dist/ui/app.js +0 -1973
  89. package/dist/ui/index.html +0 -172
  90. package/dist/ui/morphdom.min.js +0 -1
  91. package/dist/ui/styles.css +0 -2435
@@ -1,655 +0,0 @@
1
- // =============================================================================
2
- // agent-tasks — Task domain service
3
- //
4
- // Core pipeline logic: CRUD, stage advancement, dependencies, artifacts.
5
- // All mutations emit events. All inputs are validated.
6
- // =============================================================================
7
- import { NotFoundError, ConflictError, ValidationError } from '../types.js';
8
- import { MAX_TITLE_LENGTH, MAX_DESCRIPTION_LENGTH, MAX_RESULT_LENGTH, MAX_ARTIFACT_CONTENT_LENGTH, MAX_ARTIFACT_NAME_LENGTH, MAX_PROJECT_NAME_LENGTH, MAX_TAG_LENGTH, MAX_TAGS_COUNT, MAX_STAGE_NAME_LENGTH, MAX_STAGES_COUNT, MAX_LIST_LIMIT, rejectNullBytes, rejectControlChars, } from './validate.js';
9
- export const DEFAULT_STAGES = [
10
- 'backlog',
11
- 'spec',
12
- 'plan',
13
- 'implement',
14
- 'test',
15
- 'review',
16
- 'done',
17
- 'cancelled',
18
- ];
19
- const VALID_STATUSES = [
20
- 'pending',
21
- 'in_progress',
22
- 'completed',
23
- 'failed',
24
- 'cancelled',
25
- ];
26
- export class TaskService {
27
- db;
28
- events;
29
- constructor(db, events) {
30
- this.db = db;
31
- this.events = events;
32
- }
33
- // ---- Pipeline Config ----
34
- getPipelineStages(project) {
35
- if (project) {
36
- const config = this.db.queryOne('SELECT * FROM pipeline_config WHERE project = ?', [project]);
37
- if (config) {
38
- try {
39
- return JSON.parse(config.stages);
40
- }
41
- catch {
42
- /* fall through */
43
- }
44
- }
45
- }
46
- return [...DEFAULT_STAGES];
47
- }
48
- setPipelineConfig(project, stages) {
49
- this.validateProjectName(project);
50
- if (!stages.length)
51
- throw new ValidationError('Stages array cannot be empty.');
52
- if (stages.length > MAX_STAGES_COUNT) {
53
- throw new ValidationError(`Too many stages (max ${MAX_STAGES_COUNT}).`);
54
- }
55
- const seen = new Set();
56
- for (const s of stages) {
57
- if (s.length > MAX_STAGE_NAME_LENGTH) {
58
- throw new ValidationError(`Stage name too long: "${s}" (max ${MAX_STAGE_NAME_LENGTH}).`);
59
- }
60
- rejectControlChars(s, 'stage name');
61
- rejectNullBytes(s, 'stage name');
62
- if (seen.has(s))
63
- throw new ConflictError(`Duplicate stage: ${s}`);
64
- seen.add(s);
65
- }
66
- const json = JSON.stringify(stages);
67
- this.db.run(`INSERT INTO pipeline_config (project, stages, updated_at) VALUES (?, ?, datetime('now'))
68
- ON CONFLICT(project) DO UPDATE SET stages = ?, updated_at = datetime('now')`, [project, json, json]);
69
- this.events.emit('pipeline:configured', { project, stages });
70
- return this.db.queryOne('SELECT * FROM pipeline_config WHERE project = ?', [
71
- project,
72
- ]);
73
- }
74
- // ---- CRUD ----
75
- create(input, createdBy) {
76
- this.validateTitle(input.title);
77
- if (input.description !== undefined)
78
- this.validateDescription(input.description);
79
- if (input.project !== undefined)
80
- this.validateProjectName(input.project);
81
- if (input.tags !== undefined)
82
- this.validateTags(input.tags);
83
- if (input.assign_to !== undefined)
84
- this.validateAssignee(input.assign_to);
85
- if (input.parent_id !== undefined)
86
- this.requireTask(input.parent_id);
87
- const stages = this.getPipelineStages(input.project);
88
- const effectiveStage = input.stage || stages[0];
89
- this.validateStage(effectiveStage, stages);
90
- const status = syncStatusForStage(effectiveStage, stages);
91
- const result = this.db.run(`INSERT INTO tasks (title, description, created_by, assigned_to, status, stage, priority, project, tags, parent_id)
92
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
93
- input.title.trim(),
94
- input.description?.trim() ?? null,
95
- createdBy,
96
- input.assign_to ?? null,
97
- status,
98
- effectiveStage,
99
- input.priority ?? 0,
100
- input.project ?? null,
101
- input.tags ? JSON.stringify(input.tags) : null,
102
- input.parent_id ?? null,
103
- ]);
104
- const task = this.getById(Number(result.lastInsertRowid));
105
- this.events.emit('task:created', { task });
106
- return task;
107
- }
108
- update(taskId, updates) {
109
- const task = this.requireTask(taskId);
110
- if (updates.title !== undefined)
111
- this.validateTitle(updates.title);
112
- if (updates.description !== undefined)
113
- this.validateDescription(updates.description);
114
- if (updates.project !== undefined)
115
- this.validateProjectName(updates.project);
116
- if (updates.tags !== undefined)
117
- this.validateTags(updates.tags);
118
- if (updates.assigned_to !== undefined && updates.assigned_to !== '') {
119
- this.validateAssignee(updates.assigned_to);
120
- }
121
- const sets = [];
122
- const params = [];
123
- if (updates.title !== undefined) {
124
- sets.push('title = ?');
125
- params.push(updates.title.trim());
126
- }
127
- if (updates.description !== undefined) {
128
- sets.push('description = ?');
129
- params.push(updates.description.trim());
130
- }
131
- if (updates.priority !== undefined) {
132
- sets.push('priority = ?');
133
- params.push(updates.priority);
134
- }
135
- if (updates.project !== undefined) {
136
- sets.push('project = ?');
137
- params.push(updates.project);
138
- }
139
- if (updates.tags !== undefined) {
140
- sets.push('tags = ?');
141
- params.push(JSON.stringify(updates.tags));
142
- }
143
- if (updates.assigned_to !== undefined) {
144
- sets.push('assigned_to = ?');
145
- params.push(updates.assigned_to || null);
146
- }
147
- if (!sets.length)
148
- throw new ValidationError('No fields to update.');
149
- sets.push("updated_at = datetime('now')");
150
- params.push(taskId);
151
- this.db.run(`UPDATE tasks SET ${sets.join(', ')} WHERE id = ?`, params);
152
- const updated = this.getById(taskId);
153
- this.events.emit('task:updated', { task: updated, previous: task });
154
- return updated;
155
- }
156
- list(filter = {}) {
157
- if (filter.status && !VALID_STATUSES.includes(filter.status)) {
158
- throw new ValidationError(`Invalid status: ${filter.status}. Valid: ${VALID_STATUSES.join(', ')}`);
159
- }
160
- let sql = 'SELECT DISTINCT t.* FROM tasks t';
161
- const params = [];
162
- if (filter.collaborator) {
163
- sql += ' JOIN task_collaborators tc ON tc.task_id = t.id';
164
- }
165
- sql += ' WHERE 1=1';
166
- if (filter.status) {
167
- sql += ' AND t.status = ?';
168
- params.push(filter.status);
169
- }
170
- if (filter.assigned_to) {
171
- sql += ' AND t.assigned_to = ?';
172
- params.push(filter.assigned_to);
173
- }
174
- if (filter.stage) {
175
- sql += ' AND t.stage = ?';
176
- params.push(filter.stage);
177
- }
178
- if (filter.project) {
179
- sql += ' AND t.project = ?';
180
- params.push(filter.project);
181
- }
182
- if (filter.parent_id !== undefined) {
183
- sql += ' AND t.parent_id = ?';
184
- params.push(filter.parent_id);
185
- }
186
- if (filter.root_only) {
187
- sql += ' AND t.parent_id IS NULL';
188
- }
189
- if (filter.collaborator) {
190
- sql += ' AND tc.agent_id = ?';
191
- params.push(filter.collaborator);
192
- }
193
- sql += ' ORDER BY t.priority DESC, t.created_at DESC';
194
- const limit = Math.min(filter.limit ?? MAX_LIST_LIMIT, MAX_LIST_LIMIT);
195
- sql += ' LIMIT ?';
196
- params.push(limit);
197
- if (filter.offset && filter.offset > 0) {
198
- sql += ' OFFSET ?';
199
- params.push(filter.offset);
200
- }
201
- return this.db.queryAll(sql, params);
202
- }
203
- getById(id) {
204
- return this.db.queryOne('SELECT * FROM tasks WHERE id = ?', [id]);
205
- }
206
- count(filter) {
207
- let sql = 'SELECT COUNT(*) as cnt FROM tasks';
208
- const conditions = [];
209
- const params = [];
210
- if (filter?.status) {
211
- conditions.push('status = ?');
212
- params.push(filter.status);
213
- }
214
- if (filter?.project) {
215
- conditions.push('project = ?');
216
- params.push(filter.project);
217
- }
218
- if (filter?.stage) {
219
- conditions.push('stage = ?');
220
- params.push(filter.stage);
221
- }
222
- if (conditions.length)
223
- sql += ' WHERE ' + conditions.join(' AND ');
224
- const row = this.db.queryOne(sql, params);
225
- return row?.cnt ?? 0;
226
- }
227
- // ---- Claiming ----
228
- claim(taskId, claimerName) {
229
- this.validateAssignee(claimerName);
230
- return this.db.transaction(() => {
231
- const task = this.requireTask(taskId);
232
- if (task.status !== 'pending') {
233
- throw new ConflictError(`Task ${taskId} is not pending (status: ${task.status}).`);
234
- }
235
- const stages = this.getPipelineStages(task.project ?? undefined);
236
- const firstStage = stages[0];
237
- const nextStage = stages.length > 1 ? stages[1] : firstStage;
238
- const newStage = task.stage === firstStage ? nextStage : task.stage;
239
- const newStatus = syncStatusForStage(newStage, stages);
240
- this.db.run(`UPDATE tasks SET status = ?, stage = ?, assigned_to = ?, updated_at = datetime('now') WHERE id = ?`, [newStatus, newStage, claimerName, taskId]);
241
- const claimed = this.getById(taskId);
242
- this.events.emit('task:claimed', { task: claimed, claimer: claimerName });
243
- return claimed;
244
- });
245
- }
246
- // ---- Completion / Failure / Cancellation ----
247
- complete(taskId, result) {
248
- this.validateResult(result);
249
- return this.db.transaction(() => {
250
- const task = this.requireTask(taskId);
251
- if (task.status !== 'in_progress') {
252
- throw new ConflictError(`Task ${taskId} not in progress (status: ${task.status}).`);
253
- }
254
- const stages = this.getPipelineStages(task.project ?? undefined);
255
- const doneStage = stages.filter((s) => s !== 'cancelled').pop() ?? 'done';
256
- this.db.run(`UPDATE tasks SET status = 'completed', stage = ?, result = ?, updated_at = datetime('now') WHERE id = ?`, [doneStage, result, taskId]);
257
- const completed = this.getById(taskId);
258
- this.events.emit('task:completed', { task: completed });
259
- return completed;
260
- });
261
- }
262
- fail(taskId, result) {
263
- this.validateResult(result);
264
- return this.db.transaction(() => {
265
- const task = this.requireTask(taskId);
266
- if (task.status !== 'in_progress') {
267
- throw new ConflictError(`Task ${taskId} not in progress (status: ${task.status}).`);
268
- }
269
- this.db.run(`UPDATE tasks SET status = 'failed', result = ?, updated_at = datetime('now') WHERE id = ?`, [result, taskId]);
270
- const failed = this.getById(taskId);
271
- this.events.emit('task:failed', { task: failed });
272
- return failed;
273
- });
274
- }
275
- cancel(taskId, reason) {
276
- this.validateResult(reason);
277
- return this.db.transaction(() => {
278
- const task = this.requireTask(taskId);
279
- if (task.status === 'completed' || task.status === 'cancelled') {
280
- throw new ConflictError(`Task ${taskId} is already ${task.status}.`);
281
- }
282
- this.db.run(`UPDATE tasks SET status = 'cancelled', stage = 'cancelled', result = ?, updated_at = datetime('now') WHERE id = ?`, [reason, taskId]);
283
- const cancelled = this.getById(taskId);
284
- this.events.emit('task:cancelled', { task: cancelled });
285
- return cancelled;
286
- });
287
- }
288
- // ---- Pipeline Advancement ----
289
- advance(taskId, toStage) {
290
- return this.db.transaction(() => {
291
- const task = this.requireTask(taskId);
292
- if (task.status === 'completed' || task.status === 'failed' || task.status === 'cancelled') {
293
- throw new ConflictError(`Task ${taskId} is ${task.status} — cannot advance.`);
294
- }
295
- const stages = this.getPipelineStages(task.project ?? undefined);
296
- const activeStages = stages.filter((s) => s !== 'cancelled');
297
- const currentIdx = activeStages.indexOf(task.stage);
298
- if (currentIdx === -1) {
299
- throw new ValidationError(`Task stage '${task.stage}' not in pipeline.`);
300
- }
301
- let targetIdx;
302
- if (toStage) {
303
- if (toStage === 'cancelled') {
304
- throw new ValidationError('Use task_cancel to cancel a task.');
305
- }
306
- targetIdx = activeStages.indexOf(toStage);
307
- if (targetIdx === -1) {
308
- throw new ValidationError(`Invalid target stage: ${toStage}. Valid: ${activeStages.join(', ')}`);
309
- }
310
- if (targetIdx <= currentIdx) {
311
- throw new ValidationError(`Target stage '${toStage}' is not ahead of current stage '${task.stage}'. Use task_regress to move backward.`);
312
- }
313
- }
314
- else {
315
- targetIdx = currentIdx + 1;
316
- if (targetIdx >= activeStages.length) {
317
- throw new ConflictError(`Task is already at the final stage: ${task.stage}.`);
318
- }
319
- }
320
- this.checkDependencies(taskId);
321
- const newStage = activeStages[targetIdx];
322
- const newStatus = syncStatusForStage(newStage, activeStages);
323
- const autoAssignee = this.getAutoAssignee(newStage, task.project ?? undefined);
324
- this.db.run(`UPDATE tasks SET stage = ?, status = ?, ${autoAssignee ? 'assigned_to = ?, ' : ''}updated_at = datetime('now') WHERE id = ?`, autoAssignee ? [newStage, newStatus, autoAssignee, taskId] : [newStage, newStatus, taskId]);
325
- const advanced = this.getById(taskId);
326
- this.events.emit('task:advanced', {
327
- task: advanced,
328
- from_stage: task.stage,
329
- to_stage: newStage,
330
- });
331
- return advanced;
332
- });
333
- }
334
- regress(taskId, toStage, reason) {
335
- return this.db.transaction(() => {
336
- const task = this.requireTask(taskId);
337
- const stages = this.getPipelineStages(task.project ?? undefined);
338
- const currentIdx = stages.indexOf(task.stage);
339
- const targetIdx = stages.indexOf(toStage);
340
- if (targetIdx === -1) {
341
- throw new ValidationError(`Invalid target stage: ${toStage}. Valid: ${stages.join(', ')}`);
342
- }
343
- if (targetIdx >= currentIdx) {
344
- throw new ValidationError(`Target stage '${toStage}' is not before current stage '${task.stage}'.`);
345
- }
346
- const newStatus = syncStatusForStage(toStage, stages);
347
- this.db.run(`UPDATE tasks SET stage = ?, status = ?, updated_at = datetime('now') WHERE id = ?`, [toStage, newStatus, taskId]);
348
- if (reason) {
349
- this.validateResult(reason);
350
- this.db.run(`INSERT INTO task_artifacts (task_id, stage, name, content, created_by) VALUES (?, ?, ?, ?, ?)`, [
351
- taskId,
352
- task.stage,
353
- 'rejection',
354
- `Regressed from ${task.stage} to ${toStage}: ${reason}`,
355
- 'system',
356
- ]);
357
- }
358
- const regressed = this.getById(taskId);
359
- this.events.emit('task:regressed', {
360
- task: regressed,
361
- from_stage: task.stage,
362
- to_stage: toStage,
363
- reason,
364
- });
365
- return regressed;
366
- });
367
- }
368
- // ---- Next Task ----
369
- next(project, stage) {
370
- let sql = `SELECT t.* FROM tasks t WHERE t.status IN ('pending', 'in_progress') AND t.assigned_to IS NULL`;
371
- const params = [];
372
- if (project) {
373
- sql += ' AND t.project = ?';
374
- params.push(project);
375
- }
376
- if (stage) {
377
- sql += ' AND t.stage = ?';
378
- params.push(stage);
379
- }
380
- sql += ` AND NOT EXISTS (
381
- SELECT 1 FROM task_dependencies d
382
- JOIN tasks dep ON dep.id = d.depends_on
383
- WHERE d.task_id = t.id AND d.relationship = 'blocks' AND dep.status NOT IN ('completed', 'cancelled', 'failed')
384
- )`;
385
- sql += ' ORDER BY t.priority DESC, t.created_at ASC LIMIT 1';
386
- return this.db.queryOne(sql, params);
387
- }
388
- // ---- Dependencies ----
389
- addDependency(taskId, dependsOn, relationship = 'blocks') {
390
- if (taskId === dependsOn) {
391
- throw new ValidationError('A task cannot depend on itself.');
392
- }
393
- const validRelationships = ['blocks', 'related', 'duplicate'];
394
- if (!validRelationships.includes(relationship)) {
395
- throw new ValidationError(`Invalid relationship type: ${relationship}. Valid: ${validRelationships.join(', ')}`);
396
- }
397
- this.requireTask(taskId);
398
- this.requireTask(dependsOn);
399
- if (relationship === 'blocks' && this.wouldCreateCycle(taskId, dependsOn)) {
400
- throw new ConflictError(`Adding dependency ${taskId} → ${dependsOn} would create a cycle.`);
401
- }
402
- try {
403
- this.db.run('INSERT INTO task_dependencies (task_id, depends_on, relationship) VALUES (?, ?, ?)', [taskId, dependsOn, relationship]);
404
- }
405
- catch {
406
- throw new ConflictError(`Dependency ${taskId} → ${dependsOn} already exists.`);
407
- }
408
- this.events.emit('dependency:added', { task_id: taskId, depends_on: dependsOn, relationship });
409
- }
410
- removeDependency(taskId, dependsOn) {
411
- const result = this.db.run('DELETE FROM task_dependencies WHERE task_id = ? AND depends_on = ?', [taskId, dependsOn]);
412
- if (result.changes === 0) {
413
- throw new NotFoundError('Dependency', `${taskId} → ${dependsOn}`);
414
- }
415
- this.events.emit('dependency:removed', { task_id: taskId, depends_on: dependsOn });
416
- }
417
- getDependencies(taskId) {
418
- this.requireTask(taskId);
419
- return {
420
- blockers: this.db.queryAll('SELECT t.* FROM tasks t JOIN task_dependencies d ON t.id = d.depends_on WHERE d.task_id = ?', [taskId]),
421
- blocking: this.db.queryAll('SELECT t.* FROM tasks t JOIN task_dependencies d ON t.id = d.task_id WHERE d.depends_on = ?', [taskId]),
422
- };
423
- }
424
- getAllDependencies() {
425
- return this.db.queryAll('SELECT * FROM task_dependencies');
426
- }
427
- // ---- Artifacts ----
428
- addArtifact(taskId, name, content, createdBy, stage) {
429
- const task = this.requireTask(taskId);
430
- this.validateArtifactName(name);
431
- this.validateArtifactContent(content);
432
- const effectiveStage = stage && stage !== '_current_' ? stage : task.stage;
433
- const existing = this.db.queryOne('SELECT * FROM task_artifacts WHERE task_id = ? AND stage = ? AND name = ? ORDER BY version DESC LIMIT 1', [taskId, effectiveStage, name]);
434
- const version = existing ? existing.version + 1 : 1;
435
- const previousId = existing?.id ?? null;
436
- const result = this.db.run(`INSERT INTO task_artifacts (task_id, stage, name, content, created_by, version, previous_id) VALUES (?, ?, ?, ?, ?, ?, ?)`, [taskId, effectiveStage, name, content, createdBy, version, previousId]);
437
- const artifact = this.db.queryOne('SELECT * FROM task_artifacts WHERE id = ?', [
438
- Number(result.lastInsertRowid),
439
- ]);
440
- this.events.emit('artifact:created', { artifact });
441
- return artifact;
442
- }
443
- getArtifacts(taskId, stage) {
444
- this.requireTask(taskId);
445
- if (stage) {
446
- return this.db.queryAll('SELECT * FROM task_artifacts WHERE task_id = ? AND stage = ? ORDER BY created_at ASC', [taskId, stage]);
447
- }
448
- return this.db.queryAll('SELECT * FROM task_artifacts WHERE task_id = ? ORDER BY created_at ASC', [taskId]);
449
- }
450
- getArtifactCounts() {
451
- const rows = this.db.queryAll('SELECT task_id, COUNT(*) as cnt FROM task_artifacts GROUP BY task_id');
452
- const counts = {};
453
- for (const r of rows)
454
- counts[r.task_id] = r.cnt;
455
- return counts;
456
- }
457
- // ---- Subtasks ----
458
- getSubtasks(taskId) {
459
- this.requireTask(taskId);
460
- return this.db.queryAll('SELECT * FROM tasks WHERE parent_id = ? ORDER BY priority DESC, created_at ASC', [taskId]);
461
- }
462
- getSubtaskProgress(taskId) {
463
- const rows = this.db.queryAll(`SELECT status, COUNT(*) as cnt FROM tasks WHERE parent_id = ? GROUP BY status`, [taskId]);
464
- let total = 0;
465
- let done = 0;
466
- for (const r of rows) {
467
- total += r.cnt;
468
- if (r.status === 'completed')
469
- done += r.cnt;
470
- }
471
- return { total, done };
472
- }
473
- getAllSubtaskProgress() {
474
- const rows = this.db.queryAll(`SELECT parent_id, status, COUNT(*) as cnt FROM tasks WHERE parent_id IS NOT NULL GROUP BY parent_id, status`);
475
- const progress = {};
476
- for (const r of rows) {
477
- if (!progress[r.parent_id])
478
- progress[r.parent_id] = { total: 0, done: 0 };
479
- progress[r.parent_id].total += r.cnt;
480
- if (r.status === 'completed')
481
- progress[r.parent_id].done += r.cnt;
482
- }
483
- return progress;
484
- }
485
- // ---- Search ----
486
- search(query, options) {
487
- if (!query.trim())
488
- return [];
489
- const sanitized = this.sanitizeFtsQuery(query);
490
- if (!sanitized)
491
- return [];
492
- let sql = `
493
- SELECT t.*, snippet(tasks_fts, 0, '<mark>', '</mark>', '...', 32) as snippet,
494
- rank
495
- FROM tasks_fts
496
- JOIN tasks t ON t.id = tasks_fts.rowid
497
- WHERE tasks_fts MATCH ?`;
498
- const params = [sanitized];
499
- if (options?.project) {
500
- sql += ' AND t.project = ?';
501
- params.push(options.project);
502
- }
503
- sql += ' ORDER BY rank LIMIT ?';
504
- params.push(Math.min(options?.limit ?? 50, 200));
505
- const rows = this.db.queryAll(sql, params);
506
- return rows.map((r) => ({
507
- task: r,
508
- snippet: r.snippet,
509
- rank: r.rank,
510
- }));
511
- }
512
- sanitizeFtsQuery(query) {
513
- const cleaned = query
514
- .replace(/["*^{}[\]:()\\/]/g, '')
515
- .replace(/\b(AND|OR|NOT|NEAR)\b/gi, '')
516
- .trim();
517
- if (!cleaned)
518
- return '';
519
- return cleaned
520
- .split(/\s+/)
521
- .filter((w) => w.length > 0)
522
- .map((w) => `"${w}"`)
523
- .join(' ');
524
- }
525
- // ---- Delete ----
526
- delete(taskId) {
527
- const task = this.requireTask(taskId);
528
- this.db.run('DELETE FROM tasks WHERE id = ?', [taskId]);
529
- this.events.emit('task:deleted', { task });
530
- }
531
- // ---- Internal ----
532
- requireTask(id) {
533
- const task = this.getById(id);
534
- if (!task)
535
- throw new NotFoundError('Task', id);
536
- return task;
537
- }
538
- validateStage(stage, stages) {
539
- if (!stages.includes(stage)) {
540
- throw new ValidationError(`Invalid stage: ${stage}. Valid: ${stages.join(', ')}`);
541
- }
542
- }
543
- getAutoAssignee(stage, project) {
544
- if (!project)
545
- return null;
546
- const config = this.db.queryOne('SELECT * FROM pipeline_config WHERE project = ?', [project]);
547
- if (!config?.assignment_config)
548
- return null;
549
- try {
550
- const assignmentConfig = JSON.parse(config.assignment_config);
551
- return assignmentConfig[stage]?.auto_assign ?? null;
552
- }
553
- catch {
554
- return null;
555
- }
556
- }
557
- validateTitle(title) {
558
- rejectNullBytes(title, 'title');
559
- rejectControlChars(title, 'title');
560
- const trimmed = title.trim();
561
- if (!trimmed)
562
- throw new ValidationError('Title must not be empty.');
563
- if (trimmed.length > MAX_TITLE_LENGTH) {
564
- throw new ValidationError(`Title too long (max ${MAX_TITLE_LENGTH} chars).`);
565
- }
566
- }
567
- validateDescription(desc) {
568
- rejectNullBytes(desc, 'description');
569
- if (desc.length > MAX_DESCRIPTION_LENGTH) {
570
- throw new ValidationError(`Description too long (max ${MAX_DESCRIPTION_LENGTH} chars).`);
571
- }
572
- }
573
- validateResult(result) {
574
- rejectNullBytes(result, 'result');
575
- if (result.length > MAX_RESULT_LENGTH) {
576
- throw new ValidationError(`Result too long (max ${MAX_RESULT_LENGTH} chars).`);
577
- }
578
- }
579
- validateProjectName(project) {
580
- rejectNullBytes(project, 'project');
581
- rejectControlChars(project, 'project');
582
- if (project.length > MAX_PROJECT_NAME_LENGTH) {
583
- throw new ValidationError(`Project name too long (max ${MAX_PROJECT_NAME_LENGTH} chars).`);
584
- }
585
- }
586
- validateAssignee(name) {
587
- rejectNullBytes(name, 'assign_to');
588
- rejectControlChars(name, 'assign_to');
589
- if (!name.trim())
590
- throw new ValidationError('Assignee name must not be empty.');
591
- }
592
- validateTags(tags) {
593
- if (tags.length > MAX_TAGS_COUNT) {
594
- throw new ValidationError(`Too many tags (max ${MAX_TAGS_COUNT}).`);
595
- }
596
- for (const tag of tags) {
597
- rejectNullBytes(tag, 'tag');
598
- rejectControlChars(tag, 'tag');
599
- if (tag.length > MAX_TAG_LENGTH) {
600
- throw new ValidationError(`Tag too long: "${tag}" (max ${MAX_TAG_LENGTH} chars).`);
601
- }
602
- }
603
- }
604
- validateArtifactName(name) {
605
- rejectNullBytes(name, 'artifact name');
606
- rejectControlChars(name, 'artifact name');
607
- if (!name.trim())
608
- throw new ValidationError('Artifact name must not be empty.');
609
- if (name.length > MAX_ARTIFACT_NAME_LENGTH) {
610
- throw new ValidationError(`Artifact name too long (max ${MAX_ARTIFACT_NAME_LENGTH} chars).`);
611
- }
612
- }
613
- validateArtifactContent(content) {
614
- rejectNullBytes(content, 'artifact content');
615
- if (content.length > MAX_ARTIFACT_CONTENT_LENGTH) {
616
- throw new ValidationError(`Artifact content too long (max ${MAX_ARTIFACT_CONTENT_LENGTH} chars).`);
617
- }
618
- }
619
- checkDependencies(taskId) {
620
- const blockers = this.db.queryAll(`SELECT t.* FROM tasks t JOIN task_dependencies d ON t.id = d.depends_on
621
- WHERE d.task_id = ? AND d.relationship = 'blocks' AND t.status NOT IN ('completed', 'cancelled', 'failed')`, [taskId]);
622
- if (blockers.length > 0) {
623
- const names = blockers.map((b) => `#${b.id} "${b.title}" (${b.stage})`).join(', ');
624
- throw new ConflictError(`Blocked by incomplete dependencies: ${names}`);
625
- }
626
- }
627
- wouldCreateCycle(taskId, dependsOn) {
628
- const visited = new Set();
629
- const stack = [dependsOn];
630
- while (stack.length) {
631
- const current = stack.pop();
632
- if (current === taskId)
633
- return true;
634
- if (visited.has(current))
635
- continue;
636
- visited.add(current);
637
- const deps = this.db.queryAll(`SELECT * FROM task_dependencies WHERE task_id = ? AND relationship = 'blocks'`, [current]);
638
- for (const d of deps)
639
- stack.push(d.depends_on);
640
- }
641
- return false;
642
- }
643
- }
644
- // ---- Helpers ----
645
- function syncStatusForStage(stage, stages) {
646
- if (stage === 'cancelled')
647
- return 'cancelled';
648
- const activeStages = stages.filter((s) => s !== 'cancelled');
649
- if (stage === activeStages[activeStages.length - 1])
650
- return 'completed';
651
- if (stage === stages[0])
652
- return 'pending';
653
- return 'in_progress';
654
- }
655
- //# sourceMappingURL=tasks.js.map