sqlew 2.1.3 → 3.0.2

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