agent-tasks 1.7.1 → 1.9.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.
- package/README.md +17 -15
- package/dist/domain/agent-bridge.d.ts.map +1 -1
- package/dist/domain/agent-bridge.js +22 -2
- package/dist/domain/agent-bridge.js.map +1 -1
- package/dist/domain/approvals.d.ts.map +1 -1
- package/dist/domain/approvals.js +4 -1
- package/dist/domain/approvals.js.map +1 -1
- package/dist/domain/cleanup.d.ts +1 -0
- package/dist/domain/cleanup.d.ts.map +1 -1
- package/dist/domain/cleanup.js +36 -4
- package/dist/domain/cleanup.js.map +1 -1
- package/dist/domain/comments.d.ts +1 -0
- package/dist/domain/comments.d.ts.map +1 -1
- package/dist/domain/comments.js +10 -0
- package/dist/domain/comments.js.map +1 -1
- package/dist/domain/rules.js +11 -10
- package/dist/domain/rules.js.map +1 -1
- package/dist/domain/task-validator.d.ts +9 -0
- package/dist/domain/task-validator.d.ts.map +1 -0
- package/dist/domain/task-validator.js +70 -0
- package/dist/domain/task-validator.js.map +1 -0
- package/dist/domain/tasks.d.ts +19 -9
- package/dist/domain/tasks.d.ts.map +1 -1
- package/dist/domain/tasks.js +242 -111
- package/dist/domain/tasks.js.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/storage/database.d.ts.map +1 -1
- package/dist/storage/database.js +11 -3
- package/dist/storage/database.js.map +1 -1
- package/dist/transport/mcp-handlers.d.ts +31 -0
- package/dist/transport/mcp-handlers.d.ts.map +1 -0
- package/dist/transport/mcp-handlers.js +426 -0
- package/dist/transport/mcp-handlers.js.map +1 -0
- package/dist/transport/mcp.d.ts.map +1 -1
- package/dist/transport/mcp.js +207 -656
- package/dist/transport/mcp.js.map +1 -1
- package/dist/transport/rest.d.ts.map +1 -1
- package/dist/transport/rest.js +23 -7
- package/dist/transport/rest.js.map +1 -1
- package/dist/transport/ws.d.ts.map +1 -1
- package/dist/transport/ws.js +6 -4
- package/dist/transport/ws.js.map +1 -1
- package/dist/ui/app.js +186 -1608
- package/dist/ui/board.js +401 -0
- package/dist/ui/drag.js +143 -0
- package/dist/ui/index.html +5 -0
- package/dist/ui/inline-edit.js +242 -0
- package/dist/ui/panel.js +574 -0
- package/dist/ui/styles.css +109 -0
- package/dist/ui/ui-utils.js +323 -0
- package/package.json +1 -1
- package/dist/db.d.ts +0 -10
- package/dist/db.d.ts.map +0 -1
- package/dist/db.js +0 -112
- package/dist/db.js.map +0 -1
- package/dist/event-bus.d.ts +0 -10
- package/dist/event-bus.d.ts.map +0 -1
- package/dist/event-bus.js +0 -38
- package/dist/event-bus.js.map +0 -1
- package/dist/session.d.ts +0 -7
- package/dist/session.d.ts.map +0 -1
- package/dist/session.js +0 -11
- package/dist/session.js.map +0 -1
- package/dist/tasks.d.ts +0 -32
- package/dist/tasks.d.ts.map +0 -1
- package/dist/tasks.js +0 -410
- package/dist/tasks.js.map +0 -1
package/dist/domain/tasks.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// All mutations emit events. All inputs are validated.
|
|
6
6
|
// =============================================================================
|
|
7
7
|
import { NotFoundError, ConflictError, ValidationError } from '../types.js';
|
|
8
|
-
import {
|
|
8
|
+
import { MAX_STAGE_NAME_LENGTH, MAX_STAGES_COUNT, MAX_LIST_LIMIT, rejectNullBytes, rejectControlChars, } from './validate.js';
|
|
9
|
+
import { validateTitle, validateDescription, validateResult, validateProjectName, validateAssignee, validateTags, validateArtifactName, validateArtifactContent, } from './task-validator.js';
|
|
9
10
|
export const DEFAULT_STAGES = [
|
|
10
11
|
'backlog',
|
|
11
12
|
'spec',
|
|
@@ -26,34 +27,69 @@ const VALID_STATUSES = [
|
|
|
26
27
|
export class TaskService {
|
|
27
28
|
db;
|
|
28
29
|
events;
|
|
30
|
+
configCache = new Map();
|
|
31
|
+
static CONFIG_CACHE_TTL = 30_000;
|
|
29
32
|
constructor(db, events) {
|
|
30
33
|
this.db = db;
|
|
31
34
|
this.events = events;
|
|
32
35
|
}
|
|
33
|
-
// ---- Pipeline Config ----
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
// ---- Pipeline Config (cached) ----
|
|
37
|
+
getCachedConfig(project) {
|
|
38
|
+
const entry = this.configCache.get(project);
|
|
39
|
+
if (entry && Date.now() - entry.at < TaskService.CONFIG_CACHE_TTL) {
|
|
40
|
+
return { stages: entry.stages, gate: entry.gate };
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
invalidateConfigCache(project) {
|
|
45
|
+
this.configCache.delete(project);
|
|
46
|
+
}
|
|
47
|
+
loadAndCacheConfig(project) {
|
|
48
|
+
const config = this.db.queryOne('SELECT * FROM pipeline_config WHERE project = ?', [project]);
|
|
49
|
+
let stages = [...DEFAULT_STAGES];
|
|
50
|
+
let gate = null;
|
|
51
|
+
if (config) {
|
|
52
|
+
try {
|
|
53
|
+
stages = JSON.parse(config.stages);
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// fall back to defaults
|
|
57
|
+
}
|
|
58
|
+
if (config.gate_config) {
|
|
38
59
|
try {
|
|
39
|
-
|
|
60
|
+
gate = JSON.parse(config.gate_config);
|
|
40
61
|
}
|
|
41
62
|
catch {
|
|
42
|
-
|
|
63
|
+
// fall back to null
|
|
43
64
|
}
|
|
44
65
|
}
|
|
45
66
|
}
|
|
46
|
-
|
|
67
|
+
this.configCache.set(project, { stages, gate, at: Date.now() });
|
|
68
|
+
return { stages, gate };
|
|
69
|
+
}
|
|
70
|
+
getPipelineStages(project) {
|
|
71
|
+
if (!project)
|
|
72
|
+
return [...DEFAULT_STAGES];
|
|
73
|
+
const cached = this.getCachedConfig(project);
|
|
74
|
+
if (cached)
|
|
75
|
+
return cached.stages;
|
|
76
|
+
return this.loadAndCacheConfig(project).stages;
|
|
47
77
|
}
|
|
48
78
|
getAllGateConfigs() {
|
|
49
79
|
const configs = this.db.queryAll('SELECT * FROM pipeline_config WHERE gate_config IS NOT NULL');
|
|
50
80
|
const result = {};
|
|
51
81
|
for (const c of configs) {
|
|
52
82
|
try {
|
|
53
|
-
|
|
83
|
+
const gate = JSON.parse(c.gate_config);
|
|
84
|
+
result[c.project] = gate;
|
|
85
|
+
const cached = this.configCache.get(c.project);
|
|
86
|
+
if (cached) {
|
|
87
|
+
cached.gate = gate;
|
|
88
|
+
cached.at = Date.now();
|
|
89
|
+
}
|
|
54
90
|
}
|
|
55
91
|
catch {
|
|
56
|
-
|
|
92
|
+
// skip corrupt entries
|
|
57
93
|
}
|
|
58
94
|
}
|
|
59
95
|
return result;
|
|
@@ -61,27 +97,23 @@ export class TaskService {
|
|
|
61
97
|
getGateConfig(project) {
|
|
62
98
|
if (!project)
|
|
63
99
|
return null;
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
return JSON.parse(config.gate_config);
|
|
69
|
-
}
|
|
70
|
-
catch {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
100
|
+
const cached = this.getCachedConfig(project);
|
|
101
|
+
if (cached)
|
|
102
|
+
return cached.gate;
|
|
103
|
+
return this.loadAndCacheConfig(project).gate;
|
|
73
104
|
}
|
|
74
105
|
setGateConfig(project, gateConfig) {
|
|
75
|
-
|
|
106
|
+
validateProjectName(project);
|
|
76
107
|
const json = JSON.stringify(gateConfig);
|
|
77
108
|
this.db.run(`INSERT INTO pipeline_config (project, stages, gate_config, updated_at) VALUES (?, ?, ?, datetime('now'))
|
|
78
109
|
ON CONFLICT(project) DO UPDATE SET gate_config = ?, updated_at = datetime('now')`, [project, JSON.stringify(this.getPipelineStages(project)), json, json]);
|
|
110
|
+
this.invalidateConfigCache(project);
|
|
79
111
|
return this.db.queryOne('SELECT * FROM pipeline_config WHERE project = ?', [
|
|
80
112
|
project,
|
|
81
113
|
]);
|
|
82
114
|
}
|
|
83
115
|
setPipelineConfig(project, stages) {
|
|
84
|
-
|
|
116
|
+
validateProjectName(project);
|
|
85
117
|
if (!stages.length)
|
|
86
118
|
throw new ValidationError('Stages array cannot be empty.');
|
|
87
119
|
if (stages.length > MAX_STAGES_COUNT) {
|
|
@@ -101,6 +133,7 @@ export class TaskService {
|
|
|
101
133
|
const json = JSON.stringify(stages);
|
|
102
134
|
this.db.run(`INSERT INTO pipeline_config (project, stages, updated_at) VALUES (?, ?, datetime('now'))
|
|
103
135
|
ON CONFLICT(project) DO UPDATE SET stages = ?, updated_at = datetime('now')`, [project, json, json]);
|
|
136
|
+
this.invalidateConfigCache(project);
|
|
104
137
|
this.events.emit('pipeline:configured', { project, stages });
|
|
105
138
|
return this.db.queryOne('SELECT * FROM pipeline_config WHERE project = ?', [
|
|
106
139
|
project,
|
|
@@ -108,15 +141,15 @@ export class TaskService {
|
|
|
108
141
|
}
|
|
109
142
|
// ---- CRUD ----
|
|
110
143
|
create(input, createdBy) {
|
|
111
|
-
|
|
144
|
+
validateTitle(input.title);
|
|
112
145
|
if (input.description !== undefined)
|
|
113
|
-
|
|
146
|
+
validateDescription(input.description);
|
|
114
147
|
if (input.project !== undefined)
|
|
115
|
-
|
|
148
|
+
validateProjectName(input.project);
|
|
116
149
|
if (input.tags !== undefined)
|
|
117
|
-
|
|
150
|
+
validateTags(input.tags);
|
|
118
151
|
if (input.assign_to !== undefined)
|
|
119
|
-
|
|
152
|
+
validateAssignee(input.assign_to);
|
|
120
153
|
if (input.parent_id !== undefined)
|
|
121
154
|
this.requireTask(input.parent_id);
|
|
122
155
|
const stages = this.getPipelineStages(input.project);
|
|
@@ -143,15 +176,15 @@ export class TaskService {
|
|
|
143
176
|
update(taskId, updates) {
|
|
144
177
|
const task = this.requireTask(taskId);
|
|
145
178
|
if (updates.title !== undefined)
|
|
146
|
-
|
|
179
|
+
validateTitle(updates.title);
|
|
147
180
|
if (updates.description !== undefined)
|
|
148
|
-
|
|
181
|
+
validateDescription(updates.description);
|
|
149
182
|
if (updates.project !== undefined)
|
|
150
|
-
|
|
183
|
+
validateProjectName(updates.project);
|
|
151
184
|
if (updates.tags !== undefined)
|
|
152
|
-
|
|
185
|
+
validateTags(updates.tags);
|
|
153
186
|
if (updates.assigned_to !== undefined && updates.assigned_to !== '') {
|
|
154
|
-
|
|
187
|
+
validateAssignee(updates.assigned_to);
|
|
155
188
|
}
|
|
156
189
|
const sets = [];
|
|
157
190
|
const params = [];
|
|
@@ -261,7 +294,7 @@ export class TaskService {
|
|
|
261
294
|
}
|
|
262
295
|
// ---- Claiming ----
|
|
263
296
|
claim(taskId, claimerName) {
|
|
264
|
-
|
|
297
|
+
validateAssignee(claimerName);
|
|
265
298
|
return this.db.transaction(() => {
|
|
266
299
|
const task = this.requireTask(taskId);
|
|
267
300
|
if (task.status !== 'pending') {
|
|
@@ -278,9 +311,38 @@ export class TaskService {
|
|
|
278
311
|
return claimed;
|
|
279
312
|
});
|
|
280
313
|
}
|
|
314
|
+
// ---- Learnings ----
|
|
315
|
+
learn(taskId, content, category = 'technique', createdBy = 'system') {
|
|
316
|
+
const validCategories = ['technique', 'pitfall', 'decision', 'pattern'];
|
|
317
|
+
if (!validCategories.includes(category)) {
|
|
318
|
+
throw new ValidationError(`Invalid learning category: ${category}. Valid: ${validCategories.join(', ')}`);
|
|
319
|
+
}
|
|
320
|
+
validateArtifactContent(content);
|
|
321
|
+
const task = this.requireTask(taskId);
|
|
322
|
+
const prefixedContent = `[${category}] ${content}`;
|
|
323
|
+
return this.addArtifact(taskId, 'learning', prefixedContent, createdBy, task.stage);
|
|
324
|
+
}
|
|
325
|
+
propagateLearnings(task) {
|
|
326
|
+
if (!task.parent_id)
|
|
327
|
+
return;
|
|
328
|
+
const learnings = this.db.queryAll(`SELECT * FROM task_artifacts WHERE task_id = ? AND name = 'learning' ORDER BY created_at ASC`, [task.id]);
|
|
329
|
+
if (learnings.length === 0)
|
|
330
|
+
return;
|
|
331
|
+
for (const learning of learnings) {
|
|
332
|
+
const parentContent = `Learning from subtask #${task.id}: ${learning.content}`;
|
|
333
|
+
this.addArtifact(task.parent_id, 'learning', parentContent, 'system');
|
|
334
|
+
}
|
|
335
|
+
const siblings = this.db.queryAll(`SELECT * FROM tasks WHERE parent_id = ? AND id != ? AND status = 'in_progress'`, [task.parent_id, task.id]);
|
|
336
|
+
for (const sibling of siblings) {
|
|
337
|
+
for (const learning of learnings) {
|
|
338
|
+
const siblingContent = `Learning from sibling #${task.id}: ${learning.content}`;
|
|
339
|
+
this.addArtifact(sibling.id, 'learning', siblingContent, 'system');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
281
343
|
// ---- Completion / Failure / Cancellation ----
|
|
282
344
|
complete(taskId, result) {
|
|
283
|
-
|
|
345
|
+
validateResult(result);
|
|
284
346
|
return this.db.transaction(() => {
|
|
285
347
|
const task = this.requireTask(taskId);
|
|
286
348
|
if (task.status !== 'in_progress') {
|
|
@@ -289,13 +351,14 @@ export class TaskService {
|
|
|
289
351
|
const stages = this.getPipelineStages(task.project ?? undefined);
|
|
290
352
|
const doneStage = stages.filter((s) => s !== 'cancelled').pop() ?? 'done';
|
|
291
353
|
this.db.run(`UPDATE tasks SET status = 'completed', stage = ?, result = ?, updated_at = datetime('now') WHERE id = ?`, [doneStage, result, taskId]);
|
|
354
|
+
this.propagateLearnings(task);
|
|
292
355
|
const completed = this.getById(taskId);
|
|
293
356
|
this.events.emit('task:completed', { task: completed });
|
|
294
357
|
return completed;
|
|
295
358
|
});
|
|
296
359
|
}
|
|
297
360
|
fail(taskId, result) {
|
|
298
|
-
|
|
361
|
+
validateResult(result);
|
|
299
362
|
return this.db.transaction(() => {
|
|
300
363
|
const task = this.requireTask(taskId);
|
|
301
364
|
if (task.status !== 'in_progress') {
|
|
@@ -308,7 +371,7 @@ export class TaskService {
|
|
|
308
371
|
});
|
|
309
372
|
}
|
|
310
373
|
cancel(taskId, reason) {
|
|
311
|
-
|
|
374
|
+
validateResult(reason);
|
|
312
375
|
return this.db.transaction(() => {
|
|
313
376
|
const task = this.requireTask(taskId);
|
|
314
377
|
if (task.status === 'completed' || task.status === 'cancelled') {
|
|
@@ -382,7 +445,7 @@ export class TaskService {
|
|
|
382
445
|
const newStatus = syncStatusForStage(toStage, stages);
|
|
383
446
|
this.db.run(`UPDATE tasks SET stage = ?, status = ?, updated_at = datetime('now') WHERE id = ?`, [toStage, newStatus, taskId]);
|
|
384
447
|
if (reason) {
|
|
385
|
-
|
|
448
|
+
validateResult(reason);
|
|
386
449
|
this.db.run(`INSERT INTO task_artifacts (task_id, stage, name, content, created_by) VALUES (?, ?, ?, ?, ?)`, [
|
|
387
450
|
taskId,
|
|
388
451
|
task.stage,
|
|
@@ -402,7 +465,7 @@ export class TaskService {
|
|
|
402
465
|
});
|
|
403
466
|
}
|
|
404
467
|
// ---- Next Task ----
|
|
405
|
-
next(project, stage) {
|
|
468
|
+
next(project, stage, agent) {
|
|
406
469
|
let sql = `SELECT t.* FROM tasks t WHERE t.status IN ('pending', 'in_progress') AND t.assigned_to IS NULL`;
|
|
407
470
|
const params = [];
|
|
408
471
|
if (project) {
|
|
@@ -418,8 +481,104 @@ export class TaskService {
|
|
|
418
481
|
JOIN tasks dep ON dep.id = d.depends_on
|
|
419
482
|
WHERE d.task_id = t.id AND d.relationship = 'blocks' AND dep.status NOT IN ('completed', 'cancelled', 'failed')
|
|
420
483
|
)`;
|
|
421
|
-
sql += ' ORDER BY t.priority DESC, t.created_at ASC LIMIT
|
|
422
|
-
|
|
484
|
+
sql += ' ORDER BY t.priority DESC, t.created_at ASC LIMIT 50';
|
|
485
|
+
const candidates = this.db.queryAll(sql, params);
|
|
486
|
+
if (candidates.length === 0)
|
|
487
|
+
return null;
|
|
488
|
+
if (!agent || candidates.length === 1) {
|
|
489
|
+
return { task: candidates[0], affinity_score: 0, affinity_reasons: [] };
|
|
490
|
+
}
|
|
491
|
+
const topPriority = candidates[0].priority;
|
|
492
|
+
const topCandidates = candidates.filter((t) => t.priority === topPriority);
|
|
493
|
+
if (topCandidates.length <= 1) {
|
|
494
|
+
return { task: candidates[0], affinity_score: 0, affinity_reasons: [] };
|
|
495
|
+
}
|
|
496
|
+
const scored = this.computeAffinityBatch(topCandidates, agent);
|
|
497
|
+
let bestTask = topCandidates[0];
|
|
498
|
+
let bestScore = 0;
|
|
499
|
+
let bestReasons = [];
|
|
500
|
+
for (const { task: t, score, reasons } of scored) {
|
|
501
|
+
if (score > bestScore) {
|
|
502
|
+
bestScore = score;
|
|
503
|
+
bestReasons = reasons;
|
|
504
|
+
bestTask = t;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
return { task: bestTask, affinity_score: bestScore, affinity_reasons: bestReasons };
|
|
508
|
+
}
|
|
509
|
+
computeAffinityBatch(tasks, agent) {
|
|
510
|
+
const parentIds = [...new Set(tasks.map((t) => t.parent_id).filter((id) => id !== null))];
|
|
511
|
+
const parentMap = new Map();
|
|
512
|
+
if (parentIds.length > 0) {
|
|
513
|
+
const placeholders = parentIds.map(() => '?').join(',');
|
|
514
|
+
const parents = this.db.queryAll(`SELECT * FROM tasks WHERE id IN (${placeholders})`, parentIds);
|
|
515
|
+
for (const p of parents)
|
|
516
|
+
parentMap.set(p.id, p);
|
|
517
|
+
}
|
|
518
|
+
const taskIds = tasks.map((t) => t.id);
|
|
519
|
+
const depsMap = new Map();
|
|
520
|
+
if (taskIds.length > 0) {
|
|
521
|
+
const placeholders = taskIds.map(() => '?').join(',');
|
|
522
|
+
const allDeps = this.db.queryAll(`SELECT * FROM task_dependencies WHERE task_id IN (${placeholders}) AND relationship = 'blocks'`, taskIds);
|
|
523
|
+
for (const d of allDeps) {
|
|
524
|
+
let arr = depsMap.get(d.task_id);
|
|
525
|
+
if (!arr) {
|
|
526
|
+
arr = [];
|
|
527
|
+
depsMap.set(d.task_id, arr);
|
|
528
|
+
}
|
|
529
|
+
arr.push(d);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const depTargetIds = new Set();
|
|
533
|
+
for (const deps of depsMap.values()) {
|
|
534
|
+
for (const d of deps)
|
|
535
|
+
depTargetIds.add(d.depends_on);
|
|
536
|
+
}
|
|
537
|
+
const depTaskMap = new Map();
|
|
538
|
+
if (depTargetIds.size > 0) {
|
|
539
|
+
const placeholders = [...depTargetIds].map(() => '?').join(',');
|
|
540
|
+
const depTasks = this.db.queryAll(`SELECT * FROM tasks WHERE id IN (${placeholders})`, [
|
|
541
|
+
...depTargetIds,
|
|
542
|
+
]);
|
|
543
|
+
for (const t of depTasks)
|
|
544
|
+
depTaskMap.set(t.id, t);
|
|
545
|
+
}
|
|
546
|
+
const projects = [...new Set(tasks.map((t) => t.project).filter((p) => p !== null))];
|
|
547
|
+
const projectCounts = new Map();
|
|
548
|
+
if (projects.length > 0) {
|
|
549
|
+
const placeholders = projects.map(() => '?').join(',');
|
|
550
|
+
const rows = this.db.queryAll(`SELECT project, COUNT(*) as cnt FROM tasks WHERE project IN (${placeholders}) AND assigned_to = ? AND status IN ('completed', 'in_progress') GROUP BY project`, [...projects, agent]);
|
|
551
|
+
for (const r of rows)
|
|
552
|
+
projectCounts.set(r.project, r.cnt);
|
|
553
|
+
}
|
|
554
|
+
return tasks.map((task) => {
|
|
555
|
+
let score = 0;
|
|
556
|
+
const reasons = [];
|
|
557
|
+
if (task.parent_id) {
|
|
558
|
+
const parent = parentMap.get(task.parent_id);
|
|
559
|
+
if (parent?.assigned_to === agent) {
|
|
560
|
+
score += 3;
|
|
561
|
+
reasons.push('worked on parent task');
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
const deps = depsMap.get(task.id) ?? [];
|
|
565
|
+
for (const dep of deps) {
|
|
566
|
+
const depTask = depTaskMap.get(dep.depends_on);
|
|
567
|
+
if (depTask?.assigned_to === agent) {
|
|
568
|
+
score += 2;
|
|
569
|
+
reasons.push('worked on dependency #' + dep.depends_on);
|
|
570
|
+
break;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (task.project) {
|
|
574
|
+
const cnt = projectCounts.get(task.project) ?? 0;
|
|
575
|
+
if (cnt > 0) {
|
|
576
|
+
score += 1;
|
|
577
|
+
reasons.push('worked on project ' + task.project);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return { task, score, reasons };
|
|
581
|
+
});
|
|
423
582
|
}
|
|
424
583
|
// ---- Dependencies ----
|
|
425
584
|
addDependency(taskId, dependsOn, relationship = 'blocks') {
|
|
@@ -460,11 +619,17 @@ export class TaskService {
|
|
|
460
619
|
getAllDependencies() {
|
|
461
620
|
return this.db.queryAll('SELECT * FROM task_dependencies');
|
|
462
621
|
}
|
|
622
|
+
getDependenciesForTasks(taskIds) {
|
|
623
|
+
if (taskIds.length === 0)
|
|
624
|
+
return [];
|
|
625
|
+
const placeholders = taskIds.map(() => '?').join(',');
|
|
626
|
+
return this.db.queryAll(`SELECT * FROM task_dependencies WHERE task_id IN (${placeholders}) OR depends_on IN (${placeholders})`, [...taskIds, ...taskIds]);
|
|
627
|
+
}
|
|
463
628
|
// ---- Artifacts ----
|
|
464
629
|
addArtifact(taskId, name, content, createdBy, stage) {
|
|
465
630
|
const task = this.requireTask(taskId);
|
|
466
|
-
|
|
467
|
-
|
|
631
|
+
validateArtifactName(name);
|
|
632
|
+
validateArtifactContent(content);
|
|
468
633
|
const effectiveStage = stage && stage !== '_current_' ? stage : task.stage;
|
|
469
634
|
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]);
|
|
470
635
|
const version = existing ? existing.version + 1 : 1;
|
|
@@ -490,6 +655,16 @@ export class TaskService {
|
|
|
490
655
|
counts[r.task_id] = r.cnt;
|
|
491
656
|
return counts;
|
|
492
657
|
}
|
|
658
|
+
getArtifactCountsForTasks(taskIds) {
|
|
659
|
+
if (taskIds.length === 0)
|
|
660
|
+
return {};
|
|
661
|
+
const placeholders = taskIds.map(() => '?').join(',');
|
|
662
|
+
const rows = this.db.queryAll(`SELECT task_id, COUNT(*) as cnt FROM task_artifacts WHERE task_id IN (${placeholders}) GROUP BY task_id`, taskIds);
|
|
663
|
+
const counts = {};
|
|
664
|
+
for (const r of rows)
|
|
665
|
+
counts[r.task_id] = r.cnt;
|
|
666
|
+
return counts;
|
|
667
|
+
}
|
|
493
668
|
// ---- Subtasks ----
|
|
494
669
|
getSubtasks(taskId) {
|
|
495
670
|
this.requireTask(taskId);
|
|
@@ -506,6 +681,21 @@ export class TaskService {
|
|
|
506
681
|
}
|
|
507
682
|
return { total, done };
|
|
508
683
|
}
|
|
684
|
+
getSubtaskProgressForTasks(taskIds) {
|
|
685
|
+
if (taskIds.length === 0)
|
|
686
|
+
return {};
|
|
687
|
+
const placeholders = taskIds.map(() => '?').join(',');
|
|
688
|
+
const rows = this.db.queryAll(`SELECT parent_id, status, COUNT(*) as cnt FROM tasks WHERE parent_id IN (${placeholders}) GROUP BY parent_id, status`, taskIds);
|
|
689
|
+
const progress = {};
|
|
690
|
+
for (const r of rows) {
|
|
691
|
+
if (!progress[r.parent_id])
|
|
692
|
+
progress[r.parent_id] = { total: 0, done: 0 };
|
|
693
|
+
progress[r.parent_id].total += r.cnt;
|
|
694
|
+
if (r.status === 'completed')
|
|
695
|
+
progress[r.parent_id].done += r.cnt;
|
|
696
|
+
}
|
|
697
|
+
return progress;
|
|
698
|
+
}
|
|
509
699
|
getAllSubtaskProgress() {
|
|
510
700
|
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`);
|
|
511
701
|
const progress = {};
|
|
@@ -586,72 +776,13 @@ export class TaskService {
|
|
|
586
776
|
const assignmentConfig = JSON.parse(config.assignment_config);
|
|
587
777
|
return assignmentConfig[stage]?.auto_assign ?? null;
|
|
588
778
|
}
|
|
589
|
-
catch {
|
|
779
|
+
catch (err) {
|
|
780
|
+
process.stderr.write('[agent-tasks] getAutoAssignee JSON parse: ' +
|
|
781
|
+
(err instanceof Error ? err.message : String(err)) +
|
|
782
|
+
'\n');
|
|
590
783
|
return null;
|
|
591
784
|
}
|
|
592
785
|
}
|
|
593
|
-
validateTitle(title) {
|
|
594
|
-
rejectNullBytes(title, 'title');
|
|
595
|
-
rejectControlChars(title, 'title');
|
|
596
|
-
const trimmed = title.trim();
|
|
597
|
-
if (!trimmed)
|
|
598
|
-
throw new ValidationError('Title must not be empty.');
|
|
599
|
-
if (trimmed.length > MAX_TITLE_LENGTH) {
|
|
600
|
-
throw new ValidationError(`Title too long (max ${MAX_TITLE_LENGTH} chars).`);
|
|
601
|
-
}
|
|
602
|
-
}
|
|
603
|
-
validateDescription(desc) {
|
|
604
|
-
rejectNullBytes(desc, 'description');
|
|
605
|
-
if (desc.length > MAX_DESCRIPTION_LENGTH) {
|
|
606
|
-
throw new ValidationError(`Description too long (max ${MAX_DESCRIPTION_LENGTH} chars).`);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
validateResult(result) {
|
|
610
|
-
rejectNullBytes(result, 'result');
|
|
611
|
-
if (result.length > MAX_RESULT_LENGTH) {
|
|
612
|
-
throw new ValidationError(`Result too long (max ${MAX_RESULT_LENGTH} chars).`);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
validateProjectName(project) {
|
|
616
|
-
rejectNullBytes(project, 'project');
|
|
617
|
-
rejectControlChars(project, 'project');
|
|
618
|
-
if (project.length > MAX_PROJECT_NAME_LENGTH) {
|
|
619
|
-
throw new ValidationError(`Project name too long (max ${MAX_PROJECT_NAME_LENGTH} chars).`);
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
validateAssignee(name) {
|
|
623
|
-
rejectNullBytes(name, 'assign_to');
|
|
624
|
-
rejectControlChars(name, 'assign_to');
|
|
625
|
-
if (!name.trim())
|
|
626
|
-
throw new ValidationError('Assignee name must not be empty.');
|
|
627
|
-
}
|
|
628
|
-
validateTags(tags) {
|
|
629
|
-
if (tags.length > MAX_TAGS_COUNT) {
|
|
630
|
-
throw new ValidationError(`Too many tags (max ${MAX_TAGS_COUNT}).`);
|
|
631
|
-
}
|
|
632
|
-
for (const tag of tags) {
|
|
633
|
-
rejectNullBytes(tag, 'tag');
|
|
634
|
-
rejectControlChars(tag, 'tag');
|
|
635
|
-
if (tag.length > MAX_TAG_LENGTH) {
|
|
636
|
-
throw new ValidationError(`Tag too long: "${tag}" (max ${MAX_TAG_LENGTH} chars).`);
|
|
637
|
-
}
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
validateArtifactName(name) {
|
|
641
|
-
rejectNullBytes(name, 'artifact name');
|
|
642
|
-
rejectControlChars(name, 'artifact name');
|
|
643
|
-
if (!name.trim())
|
|
644
|
-
throw new ValidationError('Artifact name must not be empty.');
|
|
645
|
-
if (name.length > MAX_ARTIFACT_NAME_LENGTH) {
|
|
646
|
-
throw new ValidationError(`Artifact name too long (max ${MAX_ARTIFACT_NAME_LENGTH} chars).`);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
validateArtifactContent(content) {
|
|
650
|
-
rejectNullBytes(content, 'artifact content');
|
|
651
|
-
if (content.length > MAX_ARTIFACT_CONTENT_LENGTH) {
|
|
652
|
-
throw new ValidationError(`Artifact content too long (max ${MAX_ARTIFACT_CONTENT_LENGTH} chars).`);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
786
|
checkStageGate(task, inlineComment) {
|
|
656
787
|
const gate = this.getGateConfig(task.project ?? undefined);
|
|
657
788
|
if (!gate)
|
|
@@ -678,8 +809,8 @@ export class TaskService {
|
|
|
678
809
|
return;
|
|
679
810
|
if (stageGate.require_comment) {
|
|
680
811
|
if (!inlineComment) {
|
|
681
|
-
const
|
|
682
|
-
if (!
|
|
812
|
+
const commentCount = this.db.queryOne(`SELECT COUNT(*) as cnt FROM task_comments WHERE task_id = ?`, [task.id]);
|
|
813
|
+
if (!commentCount || commentCount.cnt === 0) {
|
|
683
814
|
throw new ValidationError(`Stage gate [${task.stage}]: comment required before advancing. Use task_comment or pass comment param to task_advance.`);
|
|
684
815
|
}
|
|
685
816
|
}
|
|
@@ -693,15 +824,15 @@ export class TaskService {
|
|
|
693
824
|
}
|
|
694
825
|
}
|
|
695
826
|
if (stageGate.require_min_artifacts !== undefined && stageGate.require_min_artifacts > 0) {
|
|
696
|
-
const
|
|
697
|
-
if (!
|
|
698
|
-
throw new ValidationError(`Stage gate [${task.stage}]: at least ${stageGate.require_min_artifacts} artifact(s) required (found ${
|
|
827
|
+
const artifactCount = this.db.queryOne(`SELECT COUNT(*) as cnt FROM task_artifacts WHERE task_id = ? AND stage = ?`, [task.id, task.stage]);
|
|
828
|
+
if (!artifactCount || artifactCount.cnt < stageGate.require_min_artifacts) {
|
|
829
|
+
throw new ValidationError(`Stage gate [${task.stage}]: at least ${stageGate.require_min_artifacts} artifact(s) required (found ${artifactCount?.cnt ?? 0}). Use task_add_artifact.`);
|
|
699
830
|
}
|
|
700
831
|
}
|
|
701
832
|
if (stageGate.require_approval) {
|
|
702
833
|
const approved = this.db.queryOne(`SELECT COUNT(*) as cnt FROM task_approvals WHERE task_id = ? AND stage = ? AND status = 'approved'`, [task.id, task.stage]);
|
|
703
834
|
if (!approved || approved.cnt === 0) {
|
|
704
|
-
throw new ValidationError(`Stage gate [${task.stage}]: approval required before advancing. Use
|
|
835
|
+
throw new ValidationError(`Stage gate [${task.stage}]: approval required before advancing. Use task_approval(action: "request") + task_approval(action: "approve").`);
|
|
705
836
|
}
|
|
706
837
|
}
|
|
707
838
|
}
|