sqlew 2.1.4 → 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.
- package/CHANGELOG.md +891 -605
- package/README.md +302 -690
- package/assets/kanban-style.png +0 -0
- package/assets/schema.sql +531 -402
- package/dist/database.d.ts +9 -0
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +33 -34
- package/dist/database.js.map +1 -1
- package/dist/index.js +1024 -213
- package/dist/index.js.map +1 -1
- package/dist/migrations/add-task-tables.d.ts +47 -0
- package/dist/migrations/add-task-tables.d.ts.map +1 -0
- package/dist/migrations/add-task-tables.js +285 -0
- package/dist/migrations/add-task-tables.js.map +1 -0
- package/dist/migrations/index.d.ts +96 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +239 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/migrations/migrate-decisions-to-tasks.d.ts +61 -0
- package/dist/migrations/migrate-decisions-to-tasks.d.ts.map +1 -0
- package/dist/migrations/migrate-decisions-to-tasks.js +442 -0
- package/dist/migrations/migrate-decisions-to-tasks.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +14 -3
- package/dist/schema.js.map +1 -1
- package/dist/tools/constraints.d.ts +4 -0
- package/dist/tools/constraints.d.ts.map +1 -1
- package/dist/tools/constraints.js +6 -27
- package/dist/tools/constraints.js.map +1 -1
- package/dist/tools/context.d.ts +17 -1
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +195 -190
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +113 -166
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/messaging.d.ts +2 -9
- package/dist/tools/messaging.d.ts.map +1 -1
- package/dist/tools/messaging.js +67 -126
- package/dist/tools/messaging.js.map +1 -1
- package/dist/tools/tasks.d.ts +90 -0
- package/dist/tools/tasks.d.ts.map +1 -0
- package/dist/tools/tasks.js +732 -0
- package/dist/tools/tasks.js.map +1 -0
- package/dist/tools/utils.d.ts +8 -1
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +50 -21
- package/dist/tools/utils.js.map +1 -1
- package/dist/types.d.ts +14 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/batch.d.ts +69 -0
- package/dist/utils/batch.d.ts.map +1 -0
- package/dist/utils/batch.js +148 -0
- package/dist/utils/batch.js.map +1 -0
- package/dist/utils/query-builder.d.ts +68 -0
- package/dist/utils/query-builder.d.ts.map +1 -0
- package/dist/utils/query-builder.js +116 -0
- package/dist/utils/query-builder.js.map +1 -0
- package/dist/utils/task-stale-detection.d.ts +28 -0
- package/dist/utils/task-stale-detection.d.ts.map +1 -0
- package/dist/utils/task-stale-detection.js +92 -0
- package/dist/utils/task-stale-detection.js.map +1 -0
- package/dist/utils/validators.d.ts +57 -0
- package/dist/utils/validators.d.ts.map +1 -0
- package/dist/utils/validators.js +117 -0
- package/dist/utils/validators.js.map +1 -0
- package/docs/AI_AGENT_GUIDE.md +1471 -648
- package/{ARCHITECTURE.md → docs/ARCHITECTURE.md} +636 -636
- package/docs/BEST_PRACTICES.md +481 -0
- package/docs/DECISION_TO_TASK_MIGRATION_GUIDE.md +457 -0
- package/docs/MIGRATION_CHAIN.md +280 -0
- package/{MIGRATION_v2.md → docs/MIGRATION_v2.md} +538 -538
- package/docs/SHARED_CONCEPTS.md +339 -0
- package/docs/TASK_ACTIONS.md +854 -0
- package/docs/TASK_LINKING.md +729 -0
- package/docs/TASK_MIGRATION.md +701 -0
- package/docs/TASK_OVERVIEW.md +363 -0
- package/docs/TASK_SYSTEM.md +1244 -0
- package/docs/TOOL_REFERENCE.md +471 -0
- package/docs/TOOL_SELECTION.md +279 -0
- package/docs/WORKFLOWS.md +602 -0
- package/docs/refactoring-summary-2025-10-17.md +365 -0
- package/docs/requirement-2025-10-17.md +508 -0
- package/package.json +64 -64
|
@@ -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
|