sqlew 2.1.4 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/CHANGELOG.md +891 -605
  2. package/LICENSE +49 -18
  3. package/README.md +337 -690
  4. package/assets/kanban-style.png +0 -0
  5. package/assets/schema.sql +532 -402
  6. package/dist/database.d.ts +9 -0
  7. package/dist/database.d.ts.map +1 -1
  8. package/dist/database.js +33 -34
  9. package/dist/database.js.map +1 -1
  10. package/dist/index.js +1050 -215
  11. package/dist/index.js.map +1 -1
  12. package/dist/migrations/add-task-tables.d.ts +47 -0
  13. package/dist/migrations/add-task-tables.d.ts.map +1 -0
  14. package/dist/migrations/add-task-tables.js +285 -0
  15. package/dist/migrations/add-task-tables.js.map +1 -0
  16. package/dist/migrations/index.d.ts +96 -0
  17. package/dist/migrations/index.d.ts.map +1 -0
  18. package/dist/migrations/index.js +239 -0
  19. package/dist/migrations/index.js.map +1 -0
  20. package/dist/migrations/migrate-decisions-to-tasks.d.ts +61 -0
  21. package/dist/migrations/migrate-decisions-to-tasks.d.ts.map +1 -0
  22. package/dist/migrations/migrate-decisions-to-tasks.js +442 -0
  23. package/dist/migrations/migrate-decisions-to-tasks.js.map +1 -0
  24. package/dist/schema.d.ts.map +1 -1
  25. package/dist/schema.js +14 -3
  26. package/dist/schema.js.map +1 -1
  27. package/dist/tools/constraints.d.ts +4 -0
  28. package/dist/tools/constraints.d.ts.map +1 -1
  29. package/dist/tools/constraints.js +6 -27
  30. package/dist/tools/constraints.js.map +1 -1
  31. package/dist/tools/context.d.ts +17 -1
  32. package/dist/tools/context.d.ts.map +1 -1
  33. package/dist/tools/context.js +195 -190
  34. package/dist/tools/context.js.map +1 -1
  35. package/dist/tools/files.d.ts.map +1 -1
  36. package/dist/tools/files.js +113 -166
  37. package/dist/tools/files.js.map +1 -1
  38. package/dist/tools/messaging.d.ts +2 -9
  39. package/dist/tools/messaging.d.ts.map +1 -1
  40. package/dist/tools/messaging.js +67 -126
  41. package/dist/tools/messaging.js.map +1 -1
  42. package/dist/tools/tasks.d.ts +90 -0
  43. package/dist/tools/tasks.d.ts.map +1 -0
  44. package/dist/tools/tasks.js +844 -0
  45. package/dist/tools/tasks.js.map +1 -0
  46. package/dist/tools/utils.d.ts +8 -1
  47. package/dist/tools/utils.d.ts.map +1 -1
  48. package/dist/tools/utils.js +50 -21
  49. package/dist/tools/utils.js.map +1 -1
  50. package/dist/types.d.ts +29 -0
  51. package/dist/types.d.ts.map +1 -1
  52. package/dist/utils/batch.d.ts +69 -0
  53. package/dist/utils/batch.d.ts.map +1 -0
  54. package/dist/utils/batch.js +148 -0
  55. package/dist/utils/batch.js.map +1 -0
  56. package/dist/utils/query-builder.d.ts +68 -0
  57. package/dist/utils/query-builder.d.ts.map +1 -0
  58. package/dist/utils/query-builder.js +116 -0
  59. package/dist/utils/query-builder.js.map +1 -0
  60. package/dist/utils/task-stale-detection.d.ts +28 -0
  61. package/dist/utils/task-stale-detection.d.ts.map +1 -0
  62. package/dist/utils/task-stale-detection.js +92 -0
  63. package/dist/utils/task-stale-detection.js.map +1 -0
  64. package/dist/utils/validators.d.ts +57 -0
  65. package/dist/utils/validators.d.ts.map +1 -0
  66. package/dist/utils/validators.js +117 -0
  67. package/dist/utils/validators.js.map +1 -0
  68. package/dist/watcher/file-watcher.d.ts +75 -0
  69. package/dist/watcher/file-watcher.d.ts.map +1 -0
  70. package/dist/watcher/file-watcher.js +374 -0
  71. package/dist/watcher/file-watcher.js.map +1 -0
  72. package/dist/watcher/index.d.ts +8 -0
  73. package/dist/watcher/index.d.ts.map +1 -0
  74. package/dist/watcher/index.js +7 -0
  75. package/dist/watcher/index.js.map +1 -0
  76. package/dist/watcher/test-executor.d.ts +23 -0
  77. package/dist/watcher/test-executor.d.ts.map +1 -0
  78. package/dist/watcher/test-executor.js +226 -0
  79. package/dist/watcher/test-executor.js.map +1 -0
  80. package/docs/ACCEPTANCE_CRITERIA.md +625 -0
  81. package/docs/AI_AGENT_GUIDE.md +1471 -648
  82. package/{ARCHITECTURE.md → docs/ARCHITECTURE.md} +636 -636
  83. package/docs/AUTO_FILE_TRACKING.md +436 -0
  84. package/docs/BEST_PRACTICES.md +481 -0
  85. package/docs/DECISION_TO_TASK_MIGRATION_GUIDE.md +457 -0
  86. package/docs/MIGRATION_CHAIN.md +280 -0
  87. package/{MIGRATION_v2.md → docs/MIGRATION_v2.md} +538 -538
  88. package/docs/SHARED_CONCEPTS.md +339 -0
  89. package/docs/TASK_ACTIONS.md +854 -0
  90. package/docs/TASK_LINKING.md +729 -0
  91. package/docs/TASK_MIGRATION.md +701 -0
  92. package/docs/TASK_OVERVIEW.md +363 -0
  93. package/docs/TASK_SYSTEM.md +1244 -0
  94. package/docs/TOOL_REFERENCE.md +471 -0
  95. package/docs/TOOL_SELECTION.md +279 -0
  96. package/docs/WORKFLOWS.md +602 -0
  97. package/package.json +65 -64
@@ -0,0 +1,844 @@
1
+ /**
2
+ * Task management tools for Kanban Task Watcher
3
+ * Implements create, update, get, list, move, link, archive, batch_create actions
4
+ */
5
+ import { getDatabase, getOrCreateAgent, getOrCreateTag, getOrCreateContextKey, getLayerId, getOrCreateFile, transaction } from '../database.js';
6
+ import { detectAndTransitionStaleTasks } from '../utils/task-stale-detection.js';
7
+ import { processBatch } from '../utils/batch.js';
8
+ import { FileWatcher } from '../watcher/index.js';
9
+ import { validatePriorityRange, validateLength, validateRange } from '../utils/validators.js';
10
+ /**
11
+ * Task status enum (matches m_task_statuses)
12
+ */
13
+ const TASK_STATUS = {
14
+ TODO: 1,
15
+ IN_PROGRESS: 2,
16
+ WAITING_REVIEW: 3,
17
+ BLOCKED: 4,
18
+ DONE: 5,
19
+ ARCHIVED: 6,
20
+ };
21
+ /**
22
+ * Task status name mapping
23
+ */
24
+ const STATUS_TO_ID = {
25
+ 'todo': TASK_STATUS.TODO,
26
+ 'in_progress': TASK_STATUS.IN_PROGRESS,
27
+ 'waiting_review': TASK_STATUS.WAITING_REVIEW,
28
+ 'blocked': TASK_STATUS.BLOCKED,
29
+ 'done': TASK_STATUS.DONE,
30
+ 'archived': TASK_STATUS.ARCHIVED,
31
+ };
32
+ const ID_TO_STATUS = {
33
+ [TASK_STATUS.TODO]: 'todo',
34
+ [TASK_STATUS.IN_PROGRESS]: 'in_progress',
35
+ [TASK_STATUS.WAITING_REVIEW]: 'waiting_review',
36
+ [TASK_STATUS.BLOCKED]: 'blocked',
37
+ [TASK_STATUS.DONE]: 'done',
38
+ [TASK_STATUS.ARCHIVED]: 'archived',
39
+ };
40
+ /**
41
+ * Valid status transitions
42
+ */
43
+ const VALID_TRANSITIONS = {
44
+ [TASK_STATUS.TODO]: [TASK_STATUS.IN_PROGRESS, TASK_STATUS.BLOCKED],
45
+ [TASK_STATUS.IN_PROGRESS]: [TASK_STATUS.WAITING_REVIEW, TASK_STATUS.BLOCKED, TASK_STATUS.DONE],
46
+ [TASK_STATUS.WAITING_REVIEW]: [TASK_STATUS.IN_PROGRESS, TASK_STATUS.TODO, TASK_STATUS.DONE],
47
+ [TASK_STATUS.BLOCKED]: [TASK_STATUS.TODO, TASK_STATUS.IN_PROGRESS],
48
+ [TASK_STATUS.DONE]: [TASK_STATUS.ARCHIVED],
49
+ [TASK_STATUS.ARCHIVED]: [], // No transitions from archived
50
+ };
51
+ /**
52
+ * Internal helper: Create task without wrapping in transaction
53
+ * Used by createTask (with transaction) and batchCreateTasks (manages its own transaction)
54
+ *
55
+ * @param params - Task parameters
56
+ * @param db - Database instance
57
+ * @returns Response with success status and task metadata
58
+ */
59
+ function createTaskInternal(params, db) {
60
+ // Validate priority
61
+ const priority = params.priority !== undefined ? params.priority : 2;
62
+ validatePriorityRange(priority);
63
+ // Get status_id
64
+ const status = params.status || 'todo';
65
+ const statusId = STATUS_TO_ID[status];
66
+ if (!statusId) {
67
+ throw new Error(`Invalid status: ${status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
68
+ }
69
+ // Validate layer if provided
70
+ let layerId = null;
71
+ if (params.layer) {
72
+ layerId = getLayerId(db, params.layer);
73
+ if (layerId === null) {
74
+ throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
75
+ }
76
+ }
77
+ // Get or create agents
78
+ let assignedAgentId = null;
79
+ if (params.assigned_agent) {
80
+ assignedAgentId = getOrCreateAgent(db, params.assigned_agent);
81
+ }
82
+ let createdByAgentId = null;
83
+ if (params.created_by_agent) {
84
+ createdByAgentId = getOrCreateAgent(db, params.created_by_agent);
85
+ }
86
+ // Insert task
87
+ const insertTaskStmt = db.prepare(`
88
+ INSERT INTO t_tasks (title, status_id, priority, assigned_agent_id, created_by_agent_id, layer_id)
89
+ VALUES (?, ?, ?, ?, ?, ?)
90
+ `);
91
+ const taskResult = insertTaskStmt.run(params.title, statusId, priority, assignedAgentId, createdByAgentId, layerId);
92
+ const taskId = taskResult.lastInsertRowid;
93
+ // Process acceptance_criteria (can be string, JSON string, or array)
94
+ let acceptanceCriteriaString = null;
95
+ let acceptanceCriteriaJson = null;
96
+ if (params.acceptance_criteria) {
97
+ if (Array.isArray(params.acceptance_criteria)) {
98
+ // Array format - store as JSON in acceptance_criteria_json
99
+ acceptanceCriteriaJson = JSON.stringify(params.acceptance_criteria);
100
+ // Also create human-readable summary in acceptance_criteria
101
+ acceptanceCriteriaString = params.acceptance_criteria
102
+ .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
103
+ .join('\n');
104
+ }
105
+ else if (typeof params.acceptance_criteria === 'string') {
106
+ // Try to parse as JSON first
107
+ try {
108
+ const parsed = JSON.parse(params.acceptance_criteria);
109
+ if (Array.isArray(parsed)) {
110
+ // It's a JSON array string - store in JSON field
111
+ acceptanceCriteriaJson = params.acceptance_criteria;
112
+ // Also create human-readable summary
113
+ acceptanceCriteriaString = parsed
114
+ .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
115
+ .join('\n');
116
+ }
117
+ else {
118
+ // Valid JSON but not an array - store as plain text
119
+ acceptanceCriteriaString = params.acceptance_criteria;
120
+ }
121
+ }
122
+ catch {
123
+ // Not valid JSON - store as plain text
124
+ acceptanceCriteriaString = params.acceptance_criteria;
125
+ }
126
+ }
127
+ }
128
+ // Insert task details if provided
129
+ if (params.description || acceptanceCriteriaString || acceptanceCriteriaJson || params.notes) {
130
+ const insertDetailsStmt = db.prepare(`
131
+ INSERT INTO t_task_details (task_id, description, acceptance_criteria, acceptance_criteria_json, notes)
132
+ VALUES (?, ?, ?, ?, ?)
133
+ `);
134
+ insertDetailsStmt.run(taskId, params.description || null, acceptanceCriteriaString, acceptanceCriteriaJson, params.notes || null);
135
+ }
136
+ // Insert tags if provided
137
+ if (params.tags && params.tags.length > 0) {
138
+ const insertTagStmt = db.prepare(`
139
+ INSERT INTO t_task_tags (task_id, tag_id)
140
+ VALUES (?, ?)
141
+ `);
142
+ for (const tagName of params.tags) {
143
+ const tagId = getOrCreateTag(db, tagName);
144
+ insertTagStmt.run(taskId, tagId);
145
+ }
146
+ }
147
+ return {
148
+ success: true,
149
+ task_id: taskId,
150
+ title: params.title,
151
+ status: status,
152
+ message: `Task "${params.title}" created successfully`
153
+ };
154
+ }
155
+ /**
156
+ * Create a new task
157
+ */
158
+ export function createTask(params) {
159
+ const db = getDatabase();
160
+ // Validate required parameters
161
+ if (!params.title || params.title.trim() === '') {
162
+ throw new Error('Parameter "title" is required and cannot be empty');
163
+ }
164
+ validateLength(params.title, 'Parameter "title"', 200);
165
+ try {
166
+ return transaction(db, () => {
167
+ return createTaskInternal(params, db);
168
+ });
169
+ }
170
+ catch (error) {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ throw new Error(`Failed to create task: ${message}`);
173
+ }
174
+ }
175
+ /**
176
+ * Update task metadata
177
+ */
178
+ export function updateTask(params) {
179
+ const db = getDatabase();
180
+ // Validate required parameters
181
+ if (!params.task_id) {
182
+ throw new Error('Parameter "task_id" is required');
183
+ }
184
+ try {
185
+ return transaction(db, () => {
186
+ // Check if task exists
187
+ const taskExists = db.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
188
+ if (!taskExists) {
189
+ throw new Error(`Task with id ${params.task_id} not found`);
190
+ }
191
+ // Build update query dynamically
192
+ const updates = [];
193
+ const updateParams = [];
194
+ if (params.title !== undefined) {
195
+ if (params.title.trim() === '') {
196
+ throw new Error('Parameter "title" cannot be empty');
197
+ }
198
+ validateLength(params.title, 'Parameter "title"', 200);
199
+ updates.push('title = ?');
200
+ updateParams.push(params.title);
201
+ }
202
+ if (params.priority !== undefined) {
203
+ validatePriorityRange(params.priority);
204
+ updates.push('priority = ?');
205
+ updateParams.push(params.priority);
206
+ }
207
+ if (params.assigned_agent !== undefined) {
208
+ const agentId = getOrCreateAgent(db, params.assigned_agent);
209
+ updates.push('assigned_agent_id = ?');
210
+ updateParams.push(agentId);
211
+ }
212
+ if (params.layer !== undefined) {
213
+ const layerId = getLayerId(db, params.layer);
214
+ if (layerId === null) {
215
+ throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
216
+ }
217
+ updates.push('layer_id = ?');
218
+ updateParams.push(layerId);
219
+ }
220
+ // Update t_tasks if any updates
221
+ if (updates.length > 0) {
222
+ const updateStmt = db.prepare(`
223
+ UPDATE t_tasks
224
+ SET ${updates.join(', ')}
225
+ WHERE id = ?
226
+ `);
227
+ updateStmt.run(...updateParams, params.task_id);
228
+ }
229
+ // Update t_task_details if any detail fields provided
230
+ if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
231
+ // Process acceptance_criteria (can be string or array)
232
+ let acceptanceCriteriaString = undefined;
233
+ let acceptanceCriteriaJson = undefined;
234
+ if (params.acceptance_criteria !== undefined) {
235
+ if (Array.isArray(params.acceptance_criteria)) {
236
+ // Array format - store as JSON in acceptance_criteria_json
237
+ acceptanceCriteriaJson = JSON.stringify(params.acceptance_criteria);
238
+ // Also create human-readable summary in acceptance_criteria
239
+ acceptanceCriteriaString = params.acceptance_criteria
240
+ .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
241
+ .join('\n');
242
+ }
243
+ else if (typeof params.acceptance_criteria === 'string') {
244
+ // Try to parse as JSON first
245
+ try {
246
+ const parsed = JSON.parse(params.acceptance_criteria);
247
+ if (Array.isArray(parsed)) {
248
+ // It's a JSON array string - store in JSON field
249
+ acceptanceCriteriaJson = params.acceptance_criteria;
250
+ // Also create human-readable summary
251
+ acceptanceCriteriaString = parsed
252
+ .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
253
+ .join('\n');
254
+ }
255
+ else {
256
+ // Valid JSON but not an array - store as plain text
257
+ acceptanceCriteriaString = params.acceptance_criteria || null;
258
+ acceptanceCriteriaJson = null;
259
+ }
260
+ }
261
+ catch {
262
+ // Not valid JSON - store as plain text
263
+ acceptanceCriteriaString = params.acceptance_criteria || null;
264
+ acceptanceCriteriaJson = null;
265
+ }
266
+ }
267
+ }
268
+ // Check if details exist
269
+ const detailsExist = db.prepare('SELECT task_id FROM t_task_details WHERE task_id = ?').get(params.task_id);
270
+ if (detailsExist) {
271
+ // Update existing details
272
+ const detailUpdates = [];
273
+ const detailParams = [];
274
+ if (params.description !== undefined) {
275
+ detailUpdates.push('description = ?');
276
+ detailParams.push(params.description || null);
277
+ }
278
+ if (acceptanceCriteriaString !== undefined) {
279
+ detailUpdates.push('acceptance_criteria = ?');
280
+ detailParams.push(acceptanceCriteriaString);
281
+ }
282
+ if (acceptanceCriteriaJson !== undefined) {
283
+ detailUpdates.push('acceptance_criteria_json = ?');
284
+ detailParams.push(acceptanceCriteriaJson);
285
+ }
286
+ if (params.notes !== undefined) {
287
+ detailUpdates.push('notes = ?');
288
+ detailParams.push(params.notes || null);
289
+ }
290
+ if (detailUpdates.length > 0) {
291
+ const updateDetailsStmt = db.prepare(`
292
+ UPDATE t_task_details
293
+ SET ${detailUpdates.join(', ')}
294
+ WHERE task_id = ?
295
+ `);
296
+ updateDetailsStmt.run(...detailParams, params.task_id);
297
+ }
298
+ }
299
+ else {
300
+ // Insert new details
301
+ const insertDetailsStmt = db.prepare(`
302
+ INSERT INTO t_task_details (task_id, description, acceptance_criteria, acceptance_criteria_json, notes)
303
+ VALUES (?, ?, ?, ?, ?)
304
+ `);
305
+ insertDetailsStmt.run(params.task_id, params.description || null, acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null, acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null, params.notes || null);
306
+ }
307
+ }
308
+ return {
309
+ success: true,
310
+ task_id: params.task_id,
311
+ message: `Task ${params.task_id} updated successfully`
312
+ };
313
+ });
314
+ }
315
+ catch (error) {
316
+ const message = error instanceof Error ? error.message : String(error);
317
+ throw new Error(`Failed to update task: ${message}`);
318
+ }
319
+ }
320
+ /**
321
+ * Get full task details
322
+ */
323
+ export function getTask(params) {
324
+ const db = getDatabase();
325
+ if (!params.task_id) {
326
+ throw new Error('Parameter "task_id" is required');
327
+ }
328
+ try {
329
+ // Get task with details
330
+ const stmt = db.prepare(`
331
+ SELECT
332
+ t.id,
333
+ t.title,
334
+ s.name as status,
335
+ t.priority,
336
+ aa.name as assigned_to,
337
+ ca.name as created_by,
338
+ l.name as layer,
339
+ t.created_ts,
340
+ t.updated_ts,
341
+ t.completed_ts,
342
+ td.description,
343
+ td.acceptance_criteria,
344
+ td.notes
345
+ FROM t_tasks t
346
+ LEFT JOIN m_task_statuses s ON t.status_id = s.id
347
+ LEFT JOIN m_agents aa ON t.assigned_agent_id = aa.id
348
+ LEFT JOIN m_agents ca ON t.created_by_agent_id = ca.id
349
+ LEFT JOIN m_layers l ON t.layer_id = l.id
350
+ LEFT JOIN t_task_details td ON t.id = td.task_id
351
+ WHERE t.id = ?
352
+ `);
353
+ const task = stmt.get(params.task_id);
354
+ if (!task) {
355
+ return {
356
+ found: false,
357
+ task_id: params.task_id
358
+ };
359
+ }
360
+ // Get tags
361
+ const tagsStmt = db.prepare(`
362
+ SELECT tg.name
363
+ FROM t_task_tags tt
364
+ JOIN m_tags tg ON tt.tag_id = tg.id
365
+ WHERE tt.task_id = ?
366
+ `);
367
+ const tags = tagsStmt.all(params.task_id).map((row) => row.name);
368
+ // Get decision links
369
+ const decisionsStmt = db.prepare(`
370
+ SELECT ck.key, tdl.link_type
371
+ FROM t_task_decision_links tdl
372
+ JOIN m_context_keys ck ON tdl.decision_key_id = ck.id
373
+ WHERE tdl.task_id = ?
374
+ `);
375
+ const decisions = decisionsStmt.all(params.task_id);
376
+ // Get constraint links
377
+ const constraintsStmt = db.prepare(`
378
+ SELECT c.id, c.constraint_text
379
+ FROM t_task_constraint_links tcl
380
+ JOIN t_constraints c ON tcl.constraint_id = c.id
381
+ WHERE tcl.task_id = ?
382
+ `);
383
+ const constraints = constraintsStmt.all(params.task_id);
384
+ // Get file links
385
+ const filesStmt = db.prepare(`
386
+ SELECT f.path
387
+ FROM t_task_file_links tfl
388
+ JOIN m_files f ON tfl.file_id = f.id
389
+ WHERE tfl.task_id = ?
390
+ `);
391
+ const files = filesStmt.all(params.task_id).map((row) => row.path);
392
+ return {
393
+ found: true,
394
+ task: {
395
+ ...task,
396
+ tags: tags,
397
+ linked_decisions: decisions,
398
+ linked_constraints: constraints,
399
+ linked_files: files
400
+ }
401
+ };
402
+ }
403
+ catch (error) {
404
+ const message = error instanceof Error ? error.message : String(error);
405
+ throw new Error(`Failed to get task: ${message}`);
406
+ }
407
+ }
408
+ /**
409
+ * List tasks (token-efficient, no descriptions)
410
+ */
411
+ export function listTasks(params = {}) {
412
+ const db = getDatabase();
413
+ try {
414
+ // Run auto-stale detection before listing
415
+ const transitionCount = detectAndTransitionStaleTasks(db);
416
+ // Build query
417
+ let query = 'SELECT * FROM v_task_board WHERE 1=1';
418
+ const queryParams = [];
419
+ // Filter by status
420
+ if (params.status) {
421
+ if (!STATUS_TO_ID[params.status]) {
422
+ throw new Error(`Invalid status: ${params.status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
423
+ }
424
+ query += ' AND status = ?';
425
+ queryParams.push(params.status);
426
+ }
427
+ // Filter by assigned agent
428
+ if (params.assigned_agent) {
429
+ query += ' AND assigned_to = ?';
430
+ queryParams.push(params.assigned_agent);
431
+ }
432
+ // Filter by layer
433
+ if (params.layer) {
434
+ query += ' AND layer = ?';
435
+ queryParams.push(params.layer);
436
+ }
437
+ // Filter by tags
438
+ if (params.tags && params.tags.length > 0) {
439
+ for (const tag of params.tags) {
440
+ query += ' AND tags LIKE ?';
441
+ queryParams.push(`%${tag}%`);
442
+ }
443
+ }
444
+ // Order by updated timestamp (most recent first)
445
+ query += ' ORDER BY updated_ts DESC';
446
+ // Pagination
447
+ const limit = params.limit !== undefined ? params.limit : 50;
448
+ const offset = params.offset || 0;
449
+ validateRange(limit, 'Parameter "limit"', 0, 100);
450
+ validateRange(offset, 'Parameter "offset"', 0, Number.MAX_SAFE_INTEGER);
451
+ query += ' LIMIT ? OFFSET ?';
452
+ queryParams.push(limit, offset);
453
+ // Execute query
454
+ const stmt = db.prepare(query);
455
+ const rows = stmt.all(...queryParams);
456
+ return {
457
+ tasks: rows,
458
+ count: rows.length,
459
+ stale_tasks_transitioned: transitionCount
460
+ };
461
+ }
462
+ catch (error) {
463
+ const message = error instanceof Error ? error.message : String(error);
464
+ throw new Error(`Failed to list tasks: ${message}`);
465
+ }
466
+ }
467
+ /**
468
+ * Move task to different status
469
+ */
470
+ export function moveTask(params) {
471
+ const db = getDatabase();
472
+ if (!params.task_id) {
473
+ throw new Error('Parameter "task_id" is required');
474
+ }
475
+ if (!params.new_status) {
476
+ throw new Error('Parameter "new_status" is required');
477
+ }
478
+ try {
479
+ // Run auto-stale detection before move
480
+ detectAndTransitionStaleTasks(db);
481
+ return transaction(db, () => {
482
+ // Get current status
483
+ const taskRow = db.prepare('SELECT status_id FROM t_tasks WHERE id = ?').get(params.task_id);
484
+ if (!taskRow) {
485
+ throw new Error(`Task with id ${params.task_id} not found`);
486
+ }
487
+ const currentStatusId = taskRow.status_id;
488
+ const newStatusId = STATUS_TO_ID[params.new_status];
489
+ if (!newStatusId) {
490
+ throw new Error(`Invalid new_status: ${params.new_status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
491
+ }
492
+ // Check if transition is valid
493
+ const validNextStatuses = VALID_TRANSITIONS[currentStatusId] || [];
494
+ if (!validNextStatuses.includes(newStatusId)) {
495
+ throw new Error(`Invalid transition from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}. ` +
496
+ `Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
497
+ }
498
+ // Update status
499
+ const updateStmt = db.prepare(`
500
+ UPDATE t_tasks
501
+ SET status_id = ?,
502
+ completed_ts = CASE WHEN ? = 5 THEN unixepoch() ELSE completed_ts END
503
+ WHERE id = ?
504
+ `);
505
+ updateStmt.run(newStatusId, newStatusId, params.task_id);
506
+ // Update watcher if moving to done or archived (stop watching)
507
+ if (params.new_status === 'done' || params.new_status === 'archived') {
508
+ try {
509
+ const watcher = FileWatcher.getInstance();
510
+ watcher.unregisterTask(params.task_id);
511
+ }
512
+ catch (error) {
513
+ // Watcher may not be initialized, ignore
514
+ }
515
+ }
516
+ return {
517
+ success: true,
518
+ task_id: params.task_id,
519
+ old_status: ID_TO_STATUS[currentStatusId],
520
+ new_status: params.new_status,
521
+ message: `Task ${params.task_id} moved from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}`
522
+ };
523
+ });
524
+ }
525
+ catch (error) {
526
+ const message = error instanceof Error ? error.message : String(error);
527
+ throw new Error(`Failed to move task: ${message}`);
528
+ }
529
+ }
530
+ /**
531
+ * Link task to decision/constraint/file
532
+ */
533
+ export function linkTask(params) {
534
+ const db = getDatabase();
535
+ if (!params.task_id) {
536
+ throw new Error('Parameter "task_id" is required');
537
+ }
538
+ if (!params.link_type) {
539
+ throw new Error('Parameter "link_type" is required');
540
+ }
541
+ if (params.target_id === undefined || params.target_id === null) {
542
+ throw new Error('Parameter "target_id" is required');
543
+ }
544
+ try {
545
+ return transaction(db, () => {
546
+ // Check if task exists
547
+ const taskExists = db.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
548
+ if (!taskExists) {
549
+ throw new Error(`Task with id ${params.task_id} not found`);
550
+ }
551
+ if (params.link_type === 'decision') {
552
+ const decisionKey = String(params.target_id);
553
+ const keyId = getOrCreateContextKey(db, decisionKey);
554
+ const linkRelation = params.link_relation || 'implements';
555
+ const stmt = db.prepare(`
556
+ INSERT OR REPLACE INTO t_task_decision_links (task_id, decision_key_id, link_type)
557
+ VALUES (?, ?, ?)
558
+ `);
559
+ stmt.run(params.task_id, keyId, linkRelation);
560
+ return {
561
+ success: true,
562
+ task_id: params.task_id,
563
+ linked_to: 'decision',
564
+ target: decisionKey,
565
+ relation: linkRelation,
566
+ message: `Task ${params.task_id} linked to decision "${decisionKey}"`
567
+ };
568
+ }
569
+ else if (params.link_type === 'constraint') {
570
+ const constraintId = Number(params.target_id);
571
+ // Check if constraint exists
572
+ const constraintExists = db.prepare('SELECT id FROM t_constraints WHERE id = ?').get(constraintId);
573
+ if (!constraintExists) {
574
+ throw new Error(`Constraint with id ${constraintId} not found`);
575
+ }
576
+ const stmt = db.prepare(`
577
+ INSERT OR IGNORE INTO t_task_constraint_links (task_id, constraint_id)
578
+ VALUES (?, ?)
579
+ `);
580
+ stmt.run(params.task_id, constraintId);
581
+ return {
582
+ success: true,
583
+ task_id: params.task_id,
584
+ linked_to: 'constraint',
585
+ target: constraintId,
586
+ message: `Task ${params.task_id} linked to constraint ${constraintId}`
587
+ };
588
+ }
589
+ else if (params.link_type === 'file') {
590
+ const filePath = String(params.target_id);
591
+ const fileId = getOrCreateFile(db, filePath);
592
+ const stmt = db.prepare(`
593
+ INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
594
+ VALUES (?, ?)
595
+ `);
596
+ stmt.run(params.task_id, fileId);
597
+ // Register file with watcher for auto-tracking
598
+ try {
599
+ const taskData = db.prepare(`
600
+ SELECT t.title, s.name as status
601
+ FROM t_tasks t
602
+ JOIN m_task_statuses s ON t.status_id = s.id
603
+ WHERE t.id = ?
604
+ `).get(params.task_id);
605
+ if (taskData) {
606
+ const watcher = FileWatcher.getInstance();
607
+ watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
608
+ }
609
+ }
610
+ catch (error) {
611
+ // Watcher may not be initialized yet, ignore
612
+ console.error('Warning: Could not register file with watcher:', error);
613
+ }
614
+ return {
615
+ success: true,
616
+ task_id: params.task_id,
617
+ linked_to: 'file',
618
+ target: filePath,
619
+ message: `Task ${params.task_id} linked to file "${filePath}"`
620
+ };
621
+ }
622
+ else {
623
+ throw new Error(`Invalid link_type: ${params.link_type}. Must be one of: decision, constraint, file`);
624
+ }
625
+ });
626
+ }
627
+ catch (error) {
628
+ const message = error instanceof Error ? error.message : String(error);
629
+ throw new Error(`Failed to link task: ${message}`);
630
+ }
631
+ }
632
+ /**
633
+ * Archive completed task
634
+ */
635
+ export function archiveTask(params) {
636
+ const db = getDatabase();
637
+ if (!params.task_id) {
638
+ throw new Error('Parameter "task_id" is required');
639
+ }
640
+ try {
641
+ return transaction(db, () => {
642
+ // Check if task is in 'done' status
643
+ const taskRow = db.prepare('SELECT status_id FROM t_tasks WHERE id = ?').get(params.task_id);
644
+ if (!taskRow) {
645
+ throw new Error(`Task with id ${params.task_id} not found`);
646
+ }
647
+ if (taskRow.status_id !== TASK_STATUS.DONE) {
648
+ throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
649
+ }
650
+ // Update to archived
651
+ const updateStmt = db.prepare('UPDATE t_tasks SET status_id = ? WHERE id = ?');
652
+ updateStmt.run(TASK_STATUS.ARCHIVED, params.task_id);
653
+ // Unregister from file watcher (archived tasks don't need tracking)
654
+ try {
655
+ const watcher = FileWatcher.getInstance();
656
+ watcher.unregisterTask(params.task_id);
657
+ }
658
+ catch (error) {
659
+ // Watcher may not be initialized, ignore
660
+ }
661
+ return {
662
+ success: true,
663
+ task_id: params.task_id,
664
+ message: `Task ${params.task_id} archived successfully`
665
+ };
666
+ });
667
+ }
668
+ catch (error) {
669
+ const message = error instanceof Error ? error.message : String(error);
670
+ throw new Error(`Failed to archive task: ${message}`);
671
+ }
672
+ }
673
+ /**
674
+ * Create multiple tasks atomically
675
+ */
676
+ export function batchCreateTasks(params) {
677
+ const db = getDatabase();
678
+ if (!params.tasks || !Array.isArray(params.tasks)) {
679
+ throw new Error('Parameter "tasks" is required and must be an array');
680
+ }
681
+ const atomic = params.atomic !== undefined ? params.atomic : true;
682
+ // Use processBatch utility
683
+ const batchResult = processBatch(db, params.tasks, (task, db) => {
684
+ const result = createTaskInternal(task, db);
685
+ return {
686
+ title: task.title,
687
+ task_id: result.task_id
688
+ };
689
+ }, atomic, 50);
690
+ // Map batch results to task batch response format
691
+ return {
692
+ success: batchResult.success,
693
+ created: batchResult.processed,
694
+ failed: batchResult.failed,
695
+ results: batchResult.results.map(r => ({
696
+ title: r.data?.title || '',
697
+ task_id: r.data?.task_id,
698
+ success: r.success,
699
+ error: r.error
700
+ }))
701
+ };
702
+ }
703
+ /**
704
+ * Return comprehensive help documentation
705
+ */
706
+ export function taskHelp() {
707
+ return {
708
+ tool: 'task',
709
+ description: 'Kanban Task Watcher for managing tasks with AI-optimized lifecycle states',
710
+ actions: {
711
+ create: {
712
+ description: 'Create a new task',
713
+ required_params: ['title'],
714
+ optional_params: ['description', 'acceptance_criteria', 'notes', 'priority', 'assigned_agent', 'created_by_agent', 'layer', 'tags', 'status'],
715
+ example: {
716
+ action: 'create',
717
+ title: 'Implement authentication endpoint',
718
+ description: 'Add JWT-based authentication to /api/login',
719
+ priority: 3,
720
+ assigned_agent: 'backend-agent',
721
+ layer: 'presentation',
722
+ tags: ['api', 'authentication']
723
+ }
724
+ },
725
+ update: {
726
+ description: 'Update task metadata',
727
+ required_params: ['task_id'],
728
+ optional_params: ['title', 'priority', 'assigned_agent', 'layer', 'description', 'acceptance_criteria', 'notes'],
729
+ example: {
730
+ action: 'update',
731
+ task_id: 5,
732
+ priority: 4,
733
+ assigned_agent: 'senior-backend-agent'
734
+ }
735
+ },
736
+ get: {
737
+ description: 'Get full task details including descriptions and links',
738
+ required_params: ['task_id'],
739
+ example: {
740
+ action: 'get',
741
+ task_id: 5
742
+ }
743
+ },
744
+ list: {
745
+ description: 'List tasks (token-efficient, no descriptions)',
746
+ required_params: [],
747
+ optional_params: ['status', 'assigned_agent', 'layer', 'tags', 'limit', 'offset'],
748
+ example: {
749
+ action: 'list',
750
+ status: 'in_progress',
751
+ assigned_agent: 'backend-agent',
752
+ limit: 20
753
+ }
754
+ },
755
+ move: {
756
+ description: 'Move task to different status with validation',
757
+ required_params: ['task_id', 'new_status'],
758
+ valid_statuses: ['todo', 'in_progress', 'waiting_review', 'blocked', 'done', 'archived'],
759
+ transitions: {
760
+ todo: ['in_progress', 'blocked'],
761
+ in_progress: ['waiting_review', 'blocked', 'done'],
762
+ waiting_review: ['in_progress', 'todo', 'done'],
763
+ blocked: ['todo', 'in_progress'],
764
+ done: ['archived'],
765
+ archived: []
766
+ },
767
+ example: {
768
+ action: 'move',
769
+ task_id: 5,
770
+ new_status: 'in_progress'
771
+ }
772
+ },
773
+ link: {
774
+ description: 'Link task to decision/constraint/file',
775
+ required_params: ['task_id', 'link_type', 'target_id'],
776
+ optional_params: ['link_relation'],
777
+ link_types: ['decision', 'constraint', 'file'],
778
+ example: {
779
+ action: 'link',
780
+ task_id: 5,
781
+ link_type: 'decision',
782
+ target_id: 'auth_method',
783
+ link_relation: 'implements'
784
+ }
785
+ },
786
+ archive: {
787
+ description: 'Archive completed task (must be in done status)',
788
+ required_params: ['task_id'],
789
+ example: {
790
+ action: 'archive',
791
+ task_id: 5
792
+ }
793
+ },
794
+ batch_create: {
795
+ description: 'Create multiple tasks atomically',
796
+ required_params: ['tasks'],
797
+ optional_params: ['atomic'],
798
+ limits: {
799
+ max_items: 50
800
+ },
801
+ example: {
802
+ action: 'batch_create',
803
+ tasks: [
804
+ { title: 'Task 1', priority: 2 },
805
+ { title: 'Task 2', priority: 3, layer: 'business' }
806
+ ],
807
+ atomic: true
808
+ }
809
+ },
810
+ help: {
811
+ description: 'Return this help documentation',
812
+ example: { action: 'help' }
813
+ }
814
+ },
815
+ auto_stale_detection: {
816
+ description: 'Tasks automatically transition when abandoned',
817
+ behavior: {
818
+ in_progress: 'Untouched for >2 hours → waiting_review',
819
+ waiting_review: 'Untouched for >24 hours → todo'
820
+ },
821
+ config_keys: {
822
+ task_stale_hours_in_progress: 'Hours before in_progress tasks go stale (default: 2)',
823
+ task_stale_hours_waiting_review: 'Hours before waiting_review tasks go stale (default: 24)',
824
+ task_auto_stale_enabled: 'Enable/disable auto-stale detection (default: true)'
825
+ }
826
+ },
827
+ priority_levels: {
828
+ 1: 'low',
829
+ 2: 'medium (default)',
830
+ 3: 'high',
831
+ 4: 'critical'
832
+ },
833
+ documentation: {
834
+ task_overview: 'docs/TASK_OVERVIEW.md - Lifecycle, status transitions, auto-stale detection (363 lines, ~10k tokens)',
835
+ task_actions: 'docs/TASK_ACTIONS.md - All action references with examples (854 lines, ~21k tokens)',
836
+ task_linking: 'docs/TASK_LINKING.md - Link tasks to decisions/constraints/files (729 lines, ~18k tokens)',
837
+ task_migration: 'docs/TASK_MIGRATION.md - Migrate from decision-based tracking (701 lines, ~18k tokens)',
838
+ tool_selection: 'docs/TOOL_SELECTION.md - Task vs decision vs constraint comparison (236 lines, ~12k tokens)',
839
+ workflows: 'docs/WORKFLOWS.md - Multi-agent task coordination workflows (602 lines, ~30k tokens)',
840
+ shared_concepts: 'docs/SHARED_CONCEPTS.md - Layer definitions, enum values (status/priority), atomic mode (339 lines, ~17k tokens)'
841
+ }
842
+ };
843
+ }
844
+ //# sourceMappingURL=tasks.js.map