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.
Files changed (68) hide show
  1. package/README.md +17 -15
  2. package/dist/domain/agent-bridge.d.ts.map +1 -1
  3. package/dist/domain/agent-bridge.js +22 -2
  4. package/dist/domain/agent-bridge.js.map +1 -1
  5. package/dist/domain/approvals.d.ts.map +1 -1
  6. package/dist/domain/approvals.js +4 -1
  7. package/dist/domain/approvals.js.map +1 -1
  8. package/dist/domain/cleanup.d.ts +1 -0
  9. package/dist/domain/cleanup.d.ts.map +1 -1
  10. package/dist/domain/cleanup.js +36 -4
  11. package/dist/domain/cleanup.js.map +1 -1
  12. package/dist/domain/comments.d.ts +1 -0
  13. package/dist/domain/comments.d.ts.map +1 -1
  14. package/dist/domain/comments.js +10 -0
  15. package/dist/domain/comments.js.map +1 -1
  16. package/dist/domain/rules.js +11 -10
  17. package/dist/domain/rules.js.map +1 -1
  18. package/dist/domain/task-validator.d.ts +9 -0
  19. package/dist/domain/task-validator.d.ts.map +1 -0
  20. package/dist/domain/task-validator.js +70 -0
  21. package/dist/domain/task-validator.js.map +1 -0
  22. package/dist/domain/tasks.d.ts +19 -9
  23. package/dist/domain/tasks.d.ts.map +1 -1
  24. package/dist/domain/tasks.js +242 -111
  25. package/dist/domain/tasks.js.map +1 -1
  26. package/dist/index.js +4 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/storage/database.d.ts.map +1 -1
  29. package/dist/storage/database.js +11 -3
  30. package/dist/storage/database.js.map +1 -1
  31. package/dist/transport/mcp-handlers.d.ts +31 -0
  32. package/dist/transport/mcp-handlers.d.ts.map +1 -0
  33. package/dist/transport/mcp-handlers.js +426 -0
  34. package/dist/transport/mcp-handlers.js.map +1 -0
  35. package/dist/transport/mcp.d.ts.map +1 -1
  36. package/dist/transport/mcp.js +207 -656
  37. package/dist/transport/mcp.js.map +1 -1
  38. package/dist/transport/rest.d.ts.map +1 -1
  39. package/dist/transport/rest.js +23 -7
  40. package/dist/transport/rest.js.map +1 -1
  41. package/dist/transport/ws.d.ts.map +1 -1
  42. package/dist/transport/ws.js +6 -4
  43. package/dist/transport/ws.js.map +1 -1
  44. package/dist/ui/app.js +186 -1608
  45. package/dist/ui/board.js +401 -0
  46. package/dist/ui/drag.js +143 -0
  47. package/dist/ui/index.html +5 -0
  48. package/dist/ui/inline-edit.js +242 -0
  49. package/dist/ui/panel.js +574 -0
  50. package/dist/ui/styles.css +109 -0
  51. package/dist/ui/ui-utils.js +323 -0
  52. package/package.json +1 -1
  53. package/dist/db.d.ts +0 -10
  54. package/dist/db.d.ts.map +0 -1
  55. package/dist/db.js +0 -112
  56. package/dist/db.js.map +0 -1
  57. package/dist/event-bus.d.ts +0 -10
  58. package/dist/event-bus.d.ts.map +0 -1
  59. package/dist/event-bus.js +0 -38
  60. package/dist/event-bus.js.map +0 -1
  61. package/dist/session.d.ts +0 -7
  62. package/dist/session.d.ts.map +0 -1
  63. package/dist/session.js +0 -11
  64. package/dist/session.js.map +0 -1
  65. package/dist/tasks.d.ts +0 -32
  66. package/dist/tasks.d.ts.map +0 -1
  67. package/dist/tasks.js +0 -410
  68. package/dist/tasks.js.map +0 -1
@@ -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 { 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';
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
- getPipelineStages(project) {
35
- if (project) {
36
- const config = this.db.queryOne('SELECT * FROM pipeline_config WHERE project = ?', [project]);
37
- if (config) {
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
- return JSON.parse(config.stages);
60
+ gate = JSON.parse(config.gate_config);
40
61
  }
41
62
  catch {
42
- /* fall through */
63
+ // fall back to null
43
64
  }
44
65
  }
45
66
  }
46
- return [...DEFAULT_STAGES];
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
- result[c.project] = JSON.parse(c.gate_config);
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
- /* skip invalid */
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 config = this.db.queryOne('SELECT * FROM pipeline_config WHERE project = ?', [project]);
65
- if (!config?.gate_config)
66
- return null;
67
- try {
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
- this.validateProjectName(project);
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
- this.validateProjectName(project);
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
- this.validateTitle(input.title);
144
+ validateTitle(input.title);
112
145
  if (input.description !== undefined)
113
- this.validateDescription(input.description);
146
+ validateDescription(input.description);
114
147
  if (input.project !== undefined)
115
- this.validateProjectName(input.project);
148
+ validateProjectName(input.project);
116
149
  if (input.tags !== undefined)
117
- this.validateTags(input.tags);
150
+ validateTags(input.tags);
118
151
  if (input.assign_to !== undefined)
119
- this.validateAssignee(input.assign_to);
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
- this.validateTitle(updates.title);
179
+ validateTitle(updates.title);
147
180
  if (updates.description !== undefined)
148
- this.validateDescription(updates.description);
181
+ validateDescription(updates.description);
149
182
  if (updates.project !== undefined)
150
- this.validateProjectName(updates.project);
183
+ validateProjectName(updates.project);
151
184
  if (updates.tags !== undefined)
152
- this.validateTags(updates.tags);
185
+ validateTags(updates.tags);
153
186
  if (updates.assigned_to !== undefined && updates.assigned_to !== '') {
154
- this.validateAssignee(updates.assigned_to);
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
- this.validateAssignee(claimerName);
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
- this.validateResult(result);
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
- this.validateResult(result);
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
- this.validateResult(reason);
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
- this.validateResult(reason);
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 1';
422
- return this.db.queryOne(sql, params);
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
- this.validateArtifactName(name);
467
- this.validateArtifactContent(content);
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 cnt = this.db.queryOne(`SELECT COUNT(*) as cnt FROM task_comments WHERE task_id = ?`, [task.id]);
682
- if (!cnt || cnt.cnt === 0) {
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 cnt2 = this.db.queryOne(`SELECT COUNT(*) as cnt FROM task_artifacts WHERE task_id = ? AND stage = ?`, [task.id, task.stage]);
697
- if (!cnt2 || cnt2.cnt < stageGate.require_min_artifacts) {
698
- throw new ValidationError(`Stage gate [${task.stage}]: at least ${stageGate.require_min_artifacts} artifact(s) required (found ${cnt2?.cnt ?? 0}). Use task_add_artifact.`);
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 task_request_approval + task_approve.`);
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
  }