sqlew 3.2.5 → 3.5.3
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 +815 -13
- package/README.md +53 -2
- package/assets/schema.sql +6 -1
- package/dist/config/loader.d.ts +46 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +151 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/types.d.ts +77 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +28 -0
- package/dist/config/types.js.map +1 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +10 -0
- package/dist/constants.js.map +1 -1
- package/dist/database.d.ts +1 -1
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +77 -10
- package/dist/database.js.map +1 -1
- package/dist/index.js +21 -3
- package/dist/index.js.map +1 -1
- package/dist/migrations/add-v3.5.0-pruned-files.d.ts +26 -0
- package/dist/migrations/add-v3.5.0-pruned-files.d.ts.map +1 -0
- package/dist/migrations/add-v3.5.0-pruned-files.js +107 -0
- package/dist/migrations/add-v3.5.0-pruned-files.js.map +1 -0
- package/dist/migrations/index.d.ts +2 -1
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +16 -1
- package/dist/migrations/index.js.map +1 -1
- package/dist/tests/git-aware-completion.test.d.ts +6 -0
- package/dist/tests/git-aware-completion.test.d.ts.map +1 -0
- package/dist/tests/git-aware-completion.test.js +141 -0
- package/dist/tests/git-aware-completion.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.d.ts +6 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.d.ts.map +1 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.js +250 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-partial.test.d.ts +6 -0
- package/dist/tests/tasks.auto-pruning-partial.test.d.ts.map +1 -0
- package/dist/tests/tasks.auto-pruning-partial.test.js +274 -0
- package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-persistence.test.d.ts +6 -0
- package/dist/tests/tasks.auto-pruning-persistence.test.d.ts.map +1 -0
- package/dist/tests/tasks.auto-pruning-persistence.test.js +232 -0
- package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-safety.test.d.ts +12 -0
- package/dist/tests/tasks.auto-pruning-safety.test.d.ts.map +1 -0
- package/dist/tests/tasks.auto-pruning-safety.test.js +196 -0
- package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -0
- package/dist/tests/tasks.link-file-backward-compat.test.d.ts +6 -0
- package/dist/tests/tasks.link-file-backward-compat.test.d.ts.map +1 -0
- package/dist/tests/tasks.link-file-backward-compat.test.js +235 -0
- package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -0
- package/dist/tests/tasks.watch-files-action.test.d.ts +6 -0
- package/dist/tests/tasks.watch-files-action.test.d.ts.map +1 -0
- package/dist/tests/tasks.watch-files-action.test.js +351 -0
- package/dist/tests/tasks.watch-files-action.test.js.map +1 -0
- package/dist/tests/tasks.watch-files-parameter.test.d.ts +6 -0
- package/dist/tests/tasks.watch-files-parameter.test.d.ts.map +1 -0
- package/dist/tests/tasks.watch-files-parameter.test.js +249 -0
- package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -0
- package/dist/tests/two-step-git-completion.test.d.ts +6 -0
- package/dist/tests/two-step-git-completion.test.d.ts.map +1 -0
- package/dist/tests/two-step-git-completion.test.js +283 -0
- package/dist/tests/two-step-git-completion.test.js.map +1 -0
- package/dist/tests/vcs-staging.test.d.ts +6 -0
- package/dist/tests/vcs-staging.test.d.ts.map +1 -0
- package/dist/tests/vcs-staging.test.js +137 -0
- package/dist/tests/vcs-staging.test.js.map +1 -0
- package/dist/tools/config.d.ts +4 -2
- package/dist/tools/config.d.ts.map +1 -1
- package/dist/tools/config.js +13 -11
- package/dist/tools/config.js.map +1 -1
- package/dist/tools/constraints.d.ts +7 -4
- package/dist/tools/constraints.d.ts.map +1 -1
- package/dist/tools/constraints.js +19 -16
- package/dist/tools/constraints.js.map +1 -1
- package/dist/tools/context.d.ts +33 -17
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +84 -68
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts +9 -5
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +19 -15
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/messaging.d.ts +9 -5
- package/dist/tools/messaging.d.ts.map +1 -1
- package/dist/tools/messaging.js +20 -16
- package/dist/tools/messaging.js.map +1 -1
- package/dist/tools/tasks.d.ts +40 -12
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +475 -87
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/utils.d.ts +11 -6
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +56 -44
- package/dist/tools/utils.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/file-pruning.d.ts +69 -0
- package/dist/utils/file-pruning.d.ts.map +1 -0
- package/dist/utils/file-pruning.js +185 -0
- package/dist/utils/file-pruning.js.map +1 -0
- package/dist/utils/quality-checks.d.ts +60 -0
- package/dist/utils/quality-checks.d.ts.map +1 -0
- package/dist/utils/quality-checks.js +228 -0
- package/dist/utils/quality-checks.js.map +1 -0
- package/dist/utils/retention.d.ts +8 -0
- package/dist/utils/retention.d.ts.map +1 -1
- package/dist/utils/retention.js +12 -0
- package/dist/utils/retention.js.map +1 -1
- package/dist/utils/task-stale-detection.d.ts +69 -1
- package/dist/utils/task-stale-detection.d.ts.map +1 -1
- package/dist/utils/task-stale-detection.js +494 -17
- package/dist/utils/task-stale-detection.js.map +1 -1
- package/dist/utils/vcs-adapter.d.ts +68 -0
- package/dist/utils/vcs-adapter.d.ts.map +1 -0
- package/dist/utils/vcs-adapter.js +187 -0
- package/dist/utils/vcs-adapter.js.map +1 -0
- package/dist/watcher/file-watcher.d.ts +54 -4
- package/dist/watcher/file-watcher.d.ts.map +1 -1
- package/dist/watcher/file-watcher.js +312 -30
- package/dist/watcher/file-watcher.js.map +1 -1
- package/dist/watcher/gitignore-parser.d.ts +70 -0
- package/dist/watcher/gitignore-parser.d.ts.map +1 -0
- package/dist/watcher/gitignore-parser.js +191 -0
- package/dist/watcher/gitignore-parser.js.map +1 -0
- package/dist/watcher/index.d.ts +1 -0
- package/dist/watcher/index.d.ts.map +1 -1
- package/dist/watcher/index.js +1 -0
- package/dist/watcher/index.js.map +1 -1
- package/docs/AI_AGENT_GUIDE.md +1 -1
- package/docs/ARCHITECTURE.md +12 -0
- package/docs/AUTO_FILE_TRACKING.md +486 -82
- package/docs/CONFIGURATION.md +908 -0
- package/docs/GIT_AWARE_AUTO_COMPLETE.md +645 -0
- package/docs/MIGRATION_v3.3.md +602 -0
- package/docs/SHARED_CONCEPTS.md +2 -1
- package/docs/TASK_ACTIONS.md +12 -0
- package/docs/TASK_OVERVIEW.md +124 -23
- package/docs/TASK_PRUNING.md +589 -0
- package/docs/TASK_SYSTEM.md +83 -13
- package/docs/TOOL_REFERENCE.md +94 -6
- package/package.json +8 -6
package/dist/tools/tasks.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Implements create, update, get, list, move, link, archive, batch_create actions
|
|
4
4
|
*/
|
|
5
5
|
import { getDatabase, getOrCreateAgent, getOrCreateTag, getOrCreateContextKey, getLayerId, getOrCreateFile, transaction } from '../database.js';
|
|
6
|
-
import { detectAndTransitionStaleTasks } from '../utils/task-stale-detection.js';
|
|
6
|
+
import { detectAndTransitionStaleTasks, autoArchiveOldDoneTasks, detectAndCompleteReviewedTasks, detectAndArchiveOnCommit } from '../utils/task-stale-detection.js';
|
|
7
7
|
import { processBatch } from '../utils/batch.js';
|
|
8
8
|
import { FileWatcher } from '../watcher/index.js';
|
|
9
9
|
import { validatePriorityRange, validateLength, validateRange } from '../utils/validators.js';
|
|
@@ -79,10 +79,10 @@ function createTaskInternal(params, db) {
|
|
|
79
79
|
if (params.assigned_agent) {
|
|
80
80
|
assignedAgentId = getOrCreateAgent(db, params.assigned_agent);
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
82
|
+
// Default to 'system' if no created_by_agent provided
|
|
83
|
+
// This ensures the activity log trigger has a valid agent_id
|
|
84
|
+
const createdBy = params.created_by_agent || 'system';
|
|
85
|
+
const createdByAgentId = getOrCreateAgent(db, createdBy);
|
|
86
86
|
// Insert task
|
|
87
87
|
const insertTaskStmt = db.prepare(`
|
|
88
88
|
INSERT INTO t_tasks (title, status_id, priority, assigned_agent_id, created_by_agent_id, layer_id)
|
|
@@ -144,6 +144,62 @@ function createTaskInternal(params, db) {
|
|
|
144
144
|
insertTagStmt.run(taskId, tagId);
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
+
// Link files and register with watcher if watch_files provided (v3.4.1)
|
|
148
|
+
if (params.watch_files && params.watch_files.length > 0) {
|
|
149
|
+
// Parse watch_files - handle MCP SDK converting JSON string to char array
|
|
150
|
+
let watchFilesParsed;
|
|
151
|
+
if (typeof params.watch_files === 'string') {
|
|
152
|
+
// String - try to parse as JSON
|
|
153
|
+
try {
|
|
154
|
+
watchFilesParsed = JSON.parse(params.watch_files);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// If not valid JSON, treat as single file path
|
|
158
|
+
watchFilesParsed = [params.watch_files];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (Array.isArray(params.watch_files)) {
|
|
162
|
+
// Check if it's an array of single characters (MCP SDK bug)
|
|
163
|
+
// Example: ['[', '"', 'f', 'i', 'l', 'e', '.', 't', 'x', 't', '"', ']']
|
|
164
|
+
if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
|
|
165
|
+
// Join characters back into string and parse JSON
|
|
166
|
+
const jsonString = params.watch_files.join('');
|
|
167
|
+
try {
|
|
168
|
+
watchFilesParsed = JSON.parse(jsonString);
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
172
|
+
throw new Error(`Invalid watch_files format: ${jsonString}. ${errMsg}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Normal array of file paths
|
|
177
|
+
watchFilesParsed = params.watch_files;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
throw new Error('Parameter "watch_files" must be a string or array');
|
|
182
|
+
}
|
|
183
|
+
const insertFileLinkStmt = db.prepare(`
|
|
184
|
+
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
185
|
+
VALUES (?, ?)
|
|
186
|
+
`);
|
|
187
|
+
for (const filePath of watchFilesParsed) {
|
|
188
|
+
const fileId = getOrCreateFile(db, filePath);
|
|
189
|
+
insertFileLinkStmt.run(taskId, fileId);
|
|
190
|
+
}
|
|
191
|
+
// Register files with watcher for auto-tracking
|
|
192
|
+
try {
|
|
193
|
+
const watcher = FileWatcher.getInstance();
|
|
194
|
+
for (const filePath of watchFilesParsed) {
|
|
195
|
+
watcher.registerFile(filePath, taskId, params.title, status);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
// Watcher may not be initialized yet, ignore
|
|
200
|
+
console.error('Warning: Could not register files with watcher:', error);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
147
203
|
return {
|
|
148
204
|
success: true,
|
|
149
205
|
task_id: taskId,
|
|
@@ -155,16 +211,16 @@ function createTaskInternal(params, db) {
|
|
|
155
211
|
/**
|
|
156
212
|
* Create a new task
|
|
157
213
|
*/
|
|
158
|
-
export function createTask(params) {
|
|
159
|
-
const
|
|
214
|
+
export function createTask(params, db) {
|
|
215
|
+
const actualDb = db ?? getDatabase();
|
|
160
216
|
// Validate required parameters
|
|
161
217
|
if (!params.title || params.title.trim() === '') {
|
|
162
218
|
throw new Error('Parameter "title" is required and cannot be empty');
|
|
163
219
|
}
|
|
164
220
|
validateLength(params.title, 'Parameter "title"', 200);
|
|
165
221
|
try {
|
|
166
|
-
return transaction(
|
|
167
|
-
return createTaskInternal(params,
|
|
222
|
+
return transaction(actualDb, () => {
|
|
223
|
+
return createTaskInternal(params, actualDb);
|
|
168
224
|
});
|
|
169
225
|
}
|
|
170
226
|
catch (error) {
|
|
@@ -175,16 +231,16 @@ export function createTask(params) {
|
|
|
175
231
|
/**
|
|
176
232
|
* Update task metadata
|
|
177
233
|
*/
|
|
178
|
-
export function updateTask(params) {
|
|
179
|
-
const
|
|
234
|
+
export function updateTask(params, db) {
|
|
235
|
+
const actualDb = db ?? getDatabase();
|
|
180
236
|
// Validate required parameters
|
|
181
237
|
if (!params.task_id) {
|
|
182
238
|
throw new Error('Parameter "task_id" is required');
|
|
183
239
|
}
|
|
184
240
|
try {
|
|
185
|
-
return transaction(
|
|
241
|
+
return transaction(actualDb, () => {
|
|
186
242
|
// Check if task exists
|
|
187
|
-
const taskExists =
|
|
243
|
+
const taskExists = actualDb.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
|
|
188
244
|
if (!taskExists) {
|
|
189
245
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
190
246
|
}
|
|
@@ -205,12 +261,12 @@ export function updateTask(params) {
|
|
|
205
261
|
updateParams.push(params.priority);
|
|
206
262
|
}
|
|
207
263
|
if (params.assigned_agent !== undefined) {
|
|
208
|
-
const agentId = getOrCreateAgent(
|
|
264
|
+
const agentId = getOrCreateAgent(actualDb, params.assigned_agent);
|
|
209
265
|
updates.push('assigned_agent_id = ?');
|
|
210
266
|
updateParams.push(agentId);
|
|
211
267
|
}
|
|
212
268
|
if (params.layer !== undefined) {
|
|
213
|
-
const layerId = getLayerId(
|
|
269
|
+
const layerId = getLayerId(actualDb, params.layer);
|
|
214
270
|
if (layerId === null) {
|
|
215
271
|
throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
|
|
216
272
|
}
|
|
@@ -219,7 +275,7 @@ export function updateTask(params) {
|
|
|
219
275
|
}
|
|
220
276
|
// Update t_tasks if any updates
|
|
221
277
|
if (updates.length > 0) {
|
|
222
|
-
const updateStmt =
|
|
278
|
+
const updateStmt = actualDb.prepare(`
|
|
223
279
|
UPDATE t_tasks
|
|
224
280
|
SET ${updates.join(', ')}
|
|
225
281
|
WHERE id = ?
|
|
@@ -266,7 +322,7 @@ export function updateTask(params) {
|
|
|
266
322
|
}
|
|
267
323
|
}
|
|
268
324
|
// Check if details exist
|
|
269
|
-
const detailsExist =
|
|
325
|
+
const detailsExist = actualDb.prepare('SELECT task_id FROM t_task_details WHERE task_id = ?').get(params.task_id);
|
|
270
326
|
if (detailsExist) {
|
|
271
327
|
// Update existing details
|
|
272
328
|
const detailUpdates = [];
|
|
@@ -288,7 +344,7 @@ export function updateTask(params) {
|
|
|
288
344
|
detailParams.push(params.notes || null);
|
|
289
345
|
}
|
|
290
346
|
if (detailUpdates.length > 0) {
|
|
291
|
-
const updateDetailsStmt =
|
|
347
|
+
const updateDetailsStmt = actualDb.prepare(`
|
|
292
348
|
UPDATE t_task_details
|
|
293
349
|
SET ${detailUpdates.join(', ')}
|
|
294
350
|
WHERE task_id = ?
|
|
@@ -298,13 +354,75 @@ export function updateTask(params) {
|
|
|
298
354
|
}
|
|
299
355
|
else {
|
|
300
356
|
// Insert new details
|
|
301
|
-
const insertDetailsStmt =
|
|
357
|
+
const insertDetailsStmt = actualDb.prepare(`
|
|
302
358
|
INSERT INTO t_task_details (task_id, description, acceptance_criteria, acceptance_criteria_json, notes)
|
|
303
359
|
VALUES (?, ?, ?, ?, ?)
|
|
304
360
|
`);
|
|
305
361
|
insertDetailsStmt.run(params.task_id, params.description || null, acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null, acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null, params.notes || null);
|
|
306
362
|
}
|
|
307
363
|
}
|
|
364
|
+
// Handle watch_files if provided (v3.4.1)
|
|
365
|
+
if (params.watch_files && params.watch_files.length > 0) {
|
|
366
|
+
// Parse watch_files - handle MCP SDK converting JSON string to char array
|
|
367
|
+
let watchFilesParsed;
|
|
368
|
+
if (typeof params.watch_files === 'string') {
|
|
369
|
+
// String - try to parse as JSON
|
|
370
|
+
try {
|
|
371
|
+
watchFilesParsed = JSON.parse(params.watch_files);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
// If not valid JSON, treat as single file path
|
|
375
|
+
watchFilesParsed = [params.watch_files];
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else if (Array.isArray(params.watch_files)) {
|
|
379
|
+
// Check if it's an array of single characters (MCP SDK bug)
|
|
380
|
+
if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
|
|
381
|
+
// Join characters back into string and parse JSON
|
|
382
|
+
const jsonString = params.watch_files.join('');
|
|
383
|
+
try {
|
|
384
|
+
watchFilesParsed = JSON.parse(jsonString);
|
|
385
|
+
}
|
|
386
|
+
catch {
|
|
387
|
+
throw new Error(`Invalid watch_files format: ${jsonString}`);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
// Normal array of file paths
|
|
392
|
+
watchFilesParsed = params.watch_files;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
throw new Error('Parameter "watch_files" must be a string or array');
|
|
397
|
+
}
|
|
398
|
+
const insertFileLinkStmt = actualDb.prepare(`
|
|
399
|
+
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
400
|
+
VALUES (?, ?)
|
|
401
|
+
`);
|
|
402
|
+
for (const filePath of watchFilesParsed) {
|
|
403
|
+
const fileId = getOrCreateFile(actualDb, filePath);
|
|
404
|
+
insertFileLinkStmt.run(params.task_id, fileId);
|
|
405
|
+
}
|
|
406
|
+
// Register files with watcher for auto-tracking
|
|
407
|
+
try {
|
|
408
|
+
const taskData = actualDb.prepare(`
|
|
409
|
+
SELECT t.title, s.name as status
|
|
410
|
+
FROM t_tasks t
|
|
411
|
+
JOIN m_task_statuses s ON t.status_id = s.id
|
|
412
|
+
WHERE t.id = ?
|
|
413
|
+
`).get(params.task_id);
|
|
414
|
+
if (taskData) {
|
|
415
|
+
const watcher = FileWatcher.getInstance();
|
|
416
|
+
for (const filePath of watchFilesParsed) {
|
|
417
|
+
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
// Watcher may not be initialized yet, ignore
|
|
423
|
+
console.error('Warning: Could not register files with watcher:', error);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
308
426
|
return {
|
|
309
427
|
success: true,
|
|
310
428
|
task_id: params.task_id,
|
|
@@ -372,14 +490,14 @@ function queryTaskDependencies(db, taskId, includeDetails = false) {
|
|
|
372
490
|
/**
|
|
373
491
|
* Get full task details
|
|
374
492
|
*/
|
|
375
|
-
export function getTask(params) {
|
|
376
|
-
const
|
|
493
|
+
export function getTask(params, db) {
|
|
494
|
+
const actualDb = db ?? getDatabase();
|
|
377
495
|
if (!params.task_id) {
|
|
378
496
|
throw new Error('Parameter "task_id" is required');
|
|
379
497
|
}
|
|
380
498
|
try {
|
|
381
499
|
// Get task with details
|
|
382
|
-
const stmt =
|
|
500
|
+
const stmt = actualDb.prepare(`
|
|
383
501
|
SELECT
|
|
384
502
|
t.id,
|
|
385
503
|
t.title,
|
|
@@ -410,7 +528,7 @@ export function getTask(params) {
|
|
|
410
528
|
};
|
|
411
529
|
}
|
|
412
530
|
// Get tags
|
|
413
|
-
const tagsStmt =
|
|
531
|
+
const tagsStmt = actualDb.prepare(`
|
|
414
532
|
SELECT tg.name
|
|
415
533
|
FROM t_task_tags tt
|
|
416
534
|
JOIN m_tags tg ON tt.tag_id = tg.id
|
|
@@ -418,7 +536,7 @@ export function getTask(params) {
|
|
|
418
536
|
`);
|
|
419
537
|
const tags = tagsStmt.all(params.task_id).map((row) => row.name);
|
|
420
538
|
// Get decision links
|
|
421
|
-
const decisionsStmt =
|
|
539
|
+
const decisionsStmt = actualDb.prepare(`
|
|
422
540
|
SELECT ck.key, tdl.link_type
|
|
423
541
|
FROM t_task_decision_links tdl
|
|
424
542
|
JOIN m_context_keys ck ON tdl.decision_key_id = ck.id
|
|
@@ -426,7 +544,7 @@ export function getTask(params) {
|
|
|
426
544
|
`);
|
|
427
545
|
const decisions = decisionsStmt.all(params.task_id);
|
|
428
546
|
// Get constraint links
|
|
429
|
-
const constraintsStmt =
|
|
547
|
+
const constraintsStmt = actualDb.prepare(`
|
|
430
548
|
SELECT c.id, c.constraint_text
|
|
431
549
|
FROM t_task_constraint_links tcl
|
|
432
550
|
JOIN t_constraints c ON tcl.constraint_id = c.id
|
|
@@ -434,7 +552,7 @@ export function getTask(params) {
|
|
|
434
552
|
`);
|
|
435
553
|
const constraints = constraintsStmt.all(params.task_id);
|
|
436
554
|
// Get file links
|
|
437
|
-
const filesStmt =
|
|
555
|
+
const filesStmt = actualDb.prepare(`
|
|
438
556
|
SELECT f.path
|
|
439
557
|
FROM t_task_file_links tfl
|
|
440
558
|
JOIN m_files f ON tfl.file_id = f.id
|
|
@@ -454,7 +572,7 @@ export function getTask(params) {
|
|
|
454
572
|
};
|
|
455
573
|
// Include dependencies if requested (token-efficient, metadata-only)
|
|
456
574
|
if (params.include_dependencies) {
|
|
457
|
-
const deps = queryTaskDependencies(
|
|
575
|
+
const deps = queryTaskDependencies(actualDb, params.task_id, false);
|
|
458
576
|
result.task.dependencies = {
|
|
459
577
|
blockers: deps.blockers,
|
|
460
578
|
blocking: deps.blocking
|
|
@@ -470,11 +588,14 @@ export function getTask(params) {
|
|
|
470
588
|
/**
|
|
471
589
|
* List tasks (token-efficient, no descriptions)
|
|
472
590
|
*/
|
|
473
|
-
export function listTasks(params = {}) {
|
|
474
|
-
const
|
|
591
|
+
export async function listTasks(params = {}, db) {
|
|
592
|
+
const actualDb = db ?? getDatabase();
|
|
475
593
|
try {
|
|
476
|
-
// Run auto-stale detection before listing
|
|
477
|
-
const transitionCount = detectAndTransitionStaleTasks(
|
|
594
|
+
// Run auto-stale detection, git-aware completion, and auto-archive before listing
|
|
595
|
+
const transitionCount = detectAndTransitionStaleTasks(actualDb);
|
|
596
|
+
const gitCompletedCount = await detectAndCompleteReviewedTasks(actualDb);
|
|
597
|
+
const gitArchivedCount = await detectAndArchiveOnCommit(actualDb);
|
|
598
|
+
const archiveCount = autoArchiveOldDoneTasks(actualDb);
|
|
478
599
|
// Build query with optional dependency counts
|
|
479
600
|
let query;
|
|
480
601
|
if (params.include_dependency_counts) {
|
|
@@ -538,12 +659,15 @@ export function listTasks(params = {}) {
|
|
|
538
659
|
query += ' LIMIT ? OFFSET ?';
|
|
539
660
|
queryParams.push(limit, offset);
|
|
540
661
|
// Execute query
|
|
541
|
-
const stmt =
|
|
662
|
+
const stmt = actualDb.prepare(query);
|
|
542
663
|
const rows = stmt.all(...queryParams);
|
|
543
664
|
return {
|
|
544
665
|
tasks: rows,
|
|
545
666
|
count: rows.length,
|
|
546
|
-
stale_tasks_transitioned: transitionCount
|
|
667
|
+
stale_tasks_transitioned: transitionCount,
|
|
668
|
+
git_auto_completed: gitCompletedCount,
|
|
669
|
+
git_archived: gitArchivedCount,
|
|
670
|
+
archived_tasks: archiveCount
|
|
547
671
|
};
|
|
548
672
|
}
|
|
549
673
|
catch (error) {
|
|
@@ -554,8 +678,8 @@ export function listTasks(params = {}) {
|
|
|
554
678
|
/**
|
|
555
679
|
* Move task to different status
|
|
556
680
|
*/
|
|
557
|
-
export function moveTask(params) {
|
|
558
|
-
const
|
|
681
|
+
export function moveTask(params, db) {
|
|
682
|
+
const actualDb = db ?? getDatabase();
|
|
559
683
|
if (!params.task_id) {
|
|
560
684
|
throw new Error('Parameter "task_id" is required');
|
|
561
685
|
}
|
|
@@ -563,11 +687,12 @@ export function moveTask(params) {
|
|
|
563
687
|
throw new Error('Parameter "new_status" is required');
|
|
564
688
|
}
|
|
565
689
|
try {
|
|
566
|
-
// Run auto-stale detection before move
|
|
567
|
-
detectAndTransitionStaleTasks(
|
|
568
|
-
|
|
690
|
+
// Run auto-stale detection and auto-archive before move
|
|
691
|
+
detectAndTransitionStaleTasks(actualDb);
|
|
692
|
+
autoArchiveOldDoneTasks(actualDb);
|
|
693
|
+
return transaction(actualDb, () => {
|
|
569
694
|
// Get current status
|
|
570
|
-
const taskRow =
|
|
695
|
+
const taskRow = actualDb.prepare('SELECT status_id FROM t_tasks WHERE id = ?').get(params.task_id);
|
|
571
696
|
if (!taskRow) {
|
|
572
697
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
573
698
|
}
|
|
@@ -583,7 +708,7 @@ export function moveTask(params) {
|
|
|
583
708
|
`Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
|
|
584
709
|
}
|
|
585
710
|
// Update status
|
|
586
|
-
const updateStmt =
|
|
711
|
+
const updateStmt = actualDb.prepare(`
|
|
587
712
|
UPDATE t_tasks
|
|
588
713
|
SET status_id = ?,
|
|
589
714
|
completed_ts = CASE WHEN ? = 5 THEN unixepoch() ELSE completed_ts END
|
|
@@ -617,8 +742,8 @@ export function moveTask(params) {
|
|
|
617
742
|
/**
|
|
618
743
|
* Link task to decision/constraint/file
|
|
619
744
|
*/
|
|
620
|
-
export function linkTask(params) {
|
|
621
|
-
const
|
|
745
|
+
export function linkTask(params, db) {
|
|
746
|
+
const actualDb = db ?? getDatabase();
|
|
622
747
|
if (!params.task_id) {
|
|
623
748
|
throw new Error('Parameter "task_id" is required');
|
|
624
749
|
}
|
|
@@ -629,17 +754,17 @@ export function linkTask(params) {
|
|
|
629
754
|
throw new Error('Parameter "target_id" is required');
|
|
630
755
|
}
|
|
631
756
|
try {
|
|
632
|
-
return transaction(
|
|
757
|
+
return transaction(actualDb, () => {
|
|
633
758
|
// Check if task exists
|
|
634
|
-
const taskExists =
|
|
759
|
+
const taskExists = actualDb.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
|
|
635
760
|
if (!taskExists) {
|
|
636
761
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
637
762
|
}
|
|
638
763
|
if (params.link_type === 'decision') {
|
|
639
764
|
const decisionKey = String(params.target_id);
|
|
640
|
-
const keyId = getOrCreateContextKey(
|
|
765
|
+
const keyId = getOrCreateContextKey(actualDb, decisionKey);
|
|
641
766
|
const linkRelation = params.link_relation || 'implements';
|
|
642
|
-
const stmt =
|
|
767
|
+
const stmt = actualDb.prepare(`
|
|
643
768
|
INSERT OR REPLACE INTO t_task_decision_links (task_id, decision_key_id, link_type)
|
|
644
769
|
VALUES (?, ?, ?)
|
|
645
770
|
`);
|
|
@@ -656,11 +781,11 @@ export function linkTask(params) {
|
|
|
656
781
|
else if (params.link_type === 'constraint') {
|
|
657
782
|
const constraintId = Number(params.target_id);
|
|
658
783
|
// Check if constraint exists
|
|
659
|
-
const constraintExists =
|
|
784
|
+
const constraintExists = actualDb.prepare('SELECT id FROM t_constraints WHERE id = ?').get(constraintId);
|
|
660
785
|
if (!constraintExists) {
|
|
661
786
|
throw new Error(`Constraint with id ${constraintId} not found`);
|
|
662
787
|
}
|
|
663
|
-
const stmt =
|
|
788
|
+
const stmt = actualDb.prepare(`
|
|
664
789
|
INSERT OR IGNORE INTO t_task_constraint_links (task_id, constraint_id)
|
|
665
790
|
VALUES (?, ?)
|
|
666
791
|
`);
|
|
@@ -674,16 +799,20 @@ export function linkTask(params) {
|
|
|
674
799
|
};
|
|
675
800
|
}
|
|
676
801
|
else if (params.link_type === 'file') {
|
|
802
|
+
// Deprecation warning (v3.4.1)
|
|
803
|
+
console.warn(`⚠️ DEPRECATION WARNING: task.link(link_type="file") is deprecated as of v3.4.1.`);
|
|
804
|
+
console.warn(` Use task.create(watch_files=[...]) or task.update(watch_files=[...]) instead.`);
|
|
805
|
+
console.warn(` Or use the new watch_files action: { action: "watch_files", task_id: ${params.task_id}, file_paths: ["..."] }`);
|
|
677
806
|
const filePath = String(params.target_id);
|
|
678
|
-
const fileId = getOrCreateFile(
|
|
679
|
-
const stmt =
|
|
807
|
+
const fileId = getOrCreateFile(actualDb, filePath);
|
|
808
|
+
const stmt = actualDb.prepare(`
|
|
680
809
|
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
681
810
|
VALUES (?, ?)
|
|
682
811
|
`);
|
|
683
812
|
stmt.run(params.task_id, fileId);
|
|
684
813
|
// Register file with watcher for auto-tracking
|
|
685
814
|
try {
|
|
686
|
-
const taskData =
|
|
815
|
+
const taskData = actualDb.prepare(`
|
|
687
816
|
SELECT t.title, s.name as status
|
|
688
817
|
FROM t_tasks t
|
|
689
818
|
JOIN m_task_statuses s ON t.status_id = s.id
|
|
@@ -703,7 +832,8 @@ export function linkTask(params) {
|
|
|
703
832
|
task_id: params.task_id,
|
|
704
833
|
linked_to: 'file',
|
|
705
834
|
target: filePath,
|
|
706
|
-
|
|
835
|
+
deprecation_warning: 'task.link(link_type="file") is deprecated. Use task.create/update(watch_files) or watch_files action instead.',
|
|
836
|
+
message: `Task ${params.task_id} linked to file "${filePath}" (DEPRECATED API - use watch_files instead)`
|
|
707
837
|
};
|
|
708
838
|
}
|
|
709
839
|
else {
|
|
@@ -719,15 +849,15 @@ export function linkTask(params) {
|
|
|
719
849
|
/**
|
|
720
850
|
* Archive completed task
|
|
721
851
|
*/
|
|
722
|
-
export function archiveTask(params) {
|
|
723
|
-
const
|
|
852
|
+
export function archiveTask(params, db) {
|
|
853
|
+
const actualDb = db ?? getDatabase();
|
|
724
854
|
if (!params.task_id) {
|
|
725
855
|
throw new Error('Parameter "task_id" is required');
|
|
726
856
|
}
|
|
727
857
|
try {
|
|
728
|
-
return transaction(
|
|
858
|
+
return transaction(actualDb, () => {
|
|
729
859
|
// Check if task is in 'done' status
|
|
730
|
-
const taskRow =
|
|
860
|
+
const taskRow = actualDb.prepare('SELECT status_id FROM t_tasks WHERE id = ?').get(params.task_id);
|
|
731
861
|
if (!taskRow) {
|
|
732
862
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
733
863
|
}
|
|
@@ -735,7 +865,7 @@ export function archiveTask(params) {
|
|
|
735
865
|
throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
|
|
736
866
|
}
|
|
737
867
|
// Update to archived
|
|
738
|
-
const updateStmt =
|
|
868
|
+
const updateStmt = actualDb.prepare('UPDATE t_tasks SET status_id = ? WHERE id = ?');
|
|
739
869
|
updateStmt.run(TASK_STATUS.ARCHIVED, params.task_id);
|
|
740
870
|
// Unregister from file watcher (archived tasks don't need tracking)
|
|
741
871
|
try {
|
|
@@ -760,8 +890,8 @@ export function archiveTask(params) {
|
|
|
760
890
|
/**
|
|
761
891
|
* Add dependency (blocking relationship) between tasks
|
|
762
892
|
*/
|
|
763
|
-
export function addDependency(params) {
|
|
764
|
-
const
|
|
893
|
+
export function addDependency(params, db) {
|
|
894
|
+
const actualDb = db ?? getDatabase();
|
|
765
895
|
if (!params.blocker_task_id) {
|
|
766
896
|
throw new Error('Parameter "blocker_task_id" is required');
|
|
767
897
|
}
|
|
@@ -769,14 +899,14 @@ export function addDependency(params) {
|
|
|
769
899
|
throw new Error('Parameter "blocked_task_id" is required');
|
|
770
900
|
}
|
|
771
901
|
try {
|
|
772
|
-
return transaction(
|
|
902
|
+
return transaction(actualDb, () => {
|
|
773
903
|
// Validation 1: No self-dependencies
|
|
774
904
|
if (params.blocker_task_id === params.blocked_task_id) {
|
|
775
905
|
throw new Error('Self-dependency not allowed');
|
|
776
906
|
}
|
|
777
907
|
// Validation 2: Both tasks must exist and check if archived
|
|
778
|
-
const blockerTask =
|
|
779
|
-
const blockedTask =
|
|
908
|
+
const blockerTask = actualDb.prepare('SELECT id, status_id FROM t_tasks WHERE id = ?').get(params.blocker_task_id);
|
|
909
|
+
const blockedTask = actualDb.prepare('SELECT id, status_id FROM t_tasks WHERE id = ?').get(params.blocked_task_id);
|
|
780
910
|
if (!blockerTask) {
|
|
781
911
|
throw new Error(`Blocker task #${params.blocker_task_id} not found`);
|
|
782
912
|
}
|
|
@@ -791,7 +921,7 @@ export function addDependency(params) {
|
|
|
791
921
|
throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
|
|
792
922
|
}
|
|
793
923
|
// Validation 4: No direct circular (reverse relationship)
|
|
794
|
-
const reverseExists =
|
|
924
|
+
const reverseExists = actualDb.prepare(`
|
|
795
925
|
SELECT 1 FROM t_task_dependencies
|
|
796
926
|
WHERE blocker_task_id = ? AND blocked_task_id = ?
|
|
797
927
|
`).get(params.blocked_task_id, params.blocker_task_id);
|
|
@@ -799,7 +929,7 @@ export function addDependency(params) {
|
|
|
799
929
|
throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
|
|
800
930
|
}
|
|
801
931
|
// Validation 5: No transitive circular (check if adding this would create a cycle)
|
|
802
|
-
const cycleCheck =
|
|
932
|
+
const cycleCheck = actualDb.prepare(`
|
|
803
933
|
WITH RECURSIVE dependency_chain AS (
|
|
804
934
|
-- Start from the task that would be blocked
|
|
805
935
|
SELECT blocked_task_id as task_id, 1 as depth
|
|
@@ -818,7 +948,7 @@ export function addDependency(params) {
|
|
|
818
948
|
`).get(params.blocked_task_id, params.blocker_task_id);
|
|
819
949
|
if (cycleCheck) {
|
|
820
950
|
// Build cycle path for error message
|
|
821
|
-
const cyclePathResult =
|
|
951
|
+
const cyclePathResult = actualDb.prepare(`
|
|
822
952
|
WITH RECURSIVE dependency_chain AS (
|
|
823
953
|
SELECT blocked_task_id as task_id, 1 as depth,
|
|
824
954
|
CAST(blocked_task_id AS TEXT) as path
|
|
@@ -839,7 +969,7 @@ export function addDependency(params) {
|
|
|
839
969
|
throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
|
|
840
970
|
}
|
|
841
971
|
// All validations passed - insert dependency
|
|
842
|
-
const insertStmt =
|
|
972
|
+
const insertStmt = actualDb.prepare(`
|
|
843
973
|
INSERT INTO t_task_dependencies (blocker_task_id, blocked_task_id)
|
|
844
974
|
VALUES (?, ?)
|
|
845
975
|
`);
|
|
@@ -862,8 +992,8 @@ export function addDependency(params) {
|
|
|
862
992
|
/**
|
|
863
993
|
* Remove dependency between tasks
|
|
864
994
|
*/
|
|
865
|
-
export function removeDependency(params) {
|
|
866
|
-
const
|
|
995
|
+
export function removeDependency(params, db) {
|
|
996
|
+
const actualDb = db ?? getDatabase();
|
|
867
997
|
if (!params.blocker_task_id) {
|
|
868
998
|
throw new Error('Parameter "blocker_task_id" is required');
|
|
869
999
|
}
|
|
@@ -871,7 +1001,7 @@ export function removeDependency(params) {
|
|
|
871
1001
|
throw new Error('Parameter "blocked_task_id" is required');
|
|
872
1002
|
}
|
|
873
1003
|
try {
|
|
874
|
-
const deleteStmt =
|
|
1004
|
+
const deleteStmt = actualDb.prepare(`
|
|
875
1005
|
DELETE FROM t_task_dependencies
|
|
876
1006
|
WHERE blocker_task_id = ? AND blocked_task_id = ?
|
|
877
1007
|
`);
|
|
@@ -889,20 +1019,20 @@ export function removeDependency(params) {
|
|
|
889
1019
|
/**
|
|
890
1020
|
* Get dependencies for a task (bidirectional: what blocks this task, what this task blocks)
|
|
891
1021
|
*/
|
|
892
|
-
export function getDependencies(params) {
|
|
893
|
-
const
|
|
1022
|
+
export function getDependencies(params, db) {
|
|
1023
|
+
const actualDb = db ?? getDatabase();
|
|
894
1024
|
if (!params.task_id) {
|
|
895
1025
|
throw new Error('Parameter "task_id" is required');
|
|
896
1026
|
}
|
|
897
1027
|
const includeDetails = params.include_details || false;
|
|
898
1028
|
try {
|
|
899
1029
|
// Check if task exists
|
|
900
|
-
const taskExists =
|
|
1030
|
+
const taskExists = actualDb.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
|
|
901
1031
|
if (!taskExists) {
|
|
902
1032
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
903
1033
|
}
|
|
904
1034
|
// Use the shared helper function
|
|
905
|
-
const deps = queryTaskDependencies(
|
|
1035
|
+
const deps = queryTaskDependencies(actualDb, params.task_id, includeDetails);
|
|
906
1036
|
return {
|
|
907
1037
|
task_id: params.task_id,
|
|
908
1038
|
blockers: deps.blockers,
|
|
@@ -921,15 +1051,15 @@ export function getDependencies(params) {
|
|
|
921
1051
|
/**
|
|
922
1052
|
* Create multiple tasks atomically
|
|
923
1053
|
*/
|
|
924
|
-
export function batchCreateTasks(params) {
|
|
925
|
-
const
|
|
1054
|
+
export function batchCreateTasks(params, db) {
|
|
1055
|
+
const actualDb = db ?? getDatabase();
|
|
926
1056
|
if (!params.tasks || !Array.isArray(params.tasks)) {
|
|
927
1057
|
throw new Error('Parameter "tasks" is required and must be an array');
|
|
928
1058
|
}
|
|
929
1059
|
const atomic = params.atomic !== undefined ? params.atomic : true;
|
|
930
1060
|
// Use processBatch utility
|
|
931
|
-
const batchResult = processBatch(
|
|
932
|
-
const result = createTaskInternal(task,
|
|
1061
|
+
const batchResult = processBatch(actualDb, params.tasks, (task, actualDb) => {
|
|
1062
|
+
const result = createTaskInternal(task, actualDb);
|
|
933
1063
|
return {
|
|
934
1064
|
title: task.title,
|
|
935
1065
|
task_id: result.task_id
|
|
@@ -948,6 +1078,232 @@ export function batchCreateTasks(params) {
|
|
|
948
1078
|
}))
|
|
949
1079
|
};
|
|
950
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Watch/unwatch files for a task (v3.4.1)
|
|
1083
|
+
* Replaces the need to use task.link(file) for file watching
|
|
1084
|
+
*/
|
|
1085
|
+
export function watchFiles(params, db) {
|
|
1086
|
+
const actualDb = db ?? getDatabase();
|
|
1087
|
+
if (!params.task_id) {
|
|
1088
|
+
throw new Error('Parameter "task_id" is required');
|
|
1089
|
+
}
|
|
1090
|
+
if (!params.action) {
|
|
1091
|
+
throw new Error('Parameter "action" is required (watch, unwatch, or list)');
|
|
1092
|
+
}
|
|
1093
|
+
try {
|
|
1094
|
+
return transaction(actualDb, () => {
|
|
1095
|
+
// Check if task exists
|
|
1096
|
+
const taskData = actualDb.prepare(`
|
|
1097
|
+
SELECT t.id, t.title, s.name as status
|
|
1098
|
+
FROM t_tasks t
|
|
1099
|
+
JOIN m_task_statuses s ON t.status_id = s.id
|
|
1100
|
+
WHERE t.id = ?
|
|
1101
|
+
`).get(params.task_id);
|
|
1102
|
+
if (!taskData) {
|
|
1103
|
+
throw new Error(`Task with id ${params.task_id} not found`);
|
|
1104
|
+
}
|
|
1105
|
+
if (params.action === 'watch') {
|
|
1106
|
+
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1107
|
+
throw new Error('Parameter "file_paths" is required for watch action');
|
|
1108
|
+
}
|
|
1109
|
+
const insertFileLinkStmt = actualDb.prepare(`
|
|
1110
|
+
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
1111
|
+
VALUES (?, ?)
|
|
1112
|
+
`);
|
|
1113
|
+
const addedFiles = [];
|
|
1114
|
+
for (const filePath of params.file_paths) {
|
|
1115
|
+
const fileId = getOrCreateFile(actualDb, filePath);
|
|
1116
|
+
const result = insertFileLinkStmt.run(params.task_id, fileId);
|
|
1117
|
+
// Check if row was actually inserted (changes > 0)
|
|
1118
|
+
if (result.changes > 0) {
|
|
1119
|
+
addedFiles.push(filePath);
|
|
1120
|
+
}
|
|
1121
|
+
}
|
|
1122
|
+
// Register files with watcher
|
|
1123
|
+
try {
|
|
1124
|
+
const watcher = FileWatcher.getInstance();
|
|
1125
|
+
for (const filePath of addedFiles) {
|
|
1126
|
+
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
catch (error) {
|
|
1130
|
+
// Watcher may not be initialized yet, ignore
|
|
1131
|
+
console.error('Warning: Could not register files with watcher:', error);
|
|
1132
|
+
}
|
|
1133
|
+
return {
|
|
1134
|
+
success: true,
|
|
1135
|
+
task_id: params.task_id,
|
|
1136
|
+
action: 'watch',
|
|
1137
|
+
files_added: addedFiles.length,
|
|
1138
|
+
files: addedFiles,
|
|
1139
|
+
message: `Watching ${addedFiles.length} file(s) for task ${params.task_id}`
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
else if (params.action === 'unwatch') {
|
|
1143
|
+
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1144
|
+
throw new Error('Parameter "file_paths" is required for unwatch action');
|
|
1145
|
+
}
|
|
1146
|
+
const deleteFileLinkStmt = actualDb.prepare(`
|
|
1147
|
+
DELETE FROM t_task_file_links
|
|
1148
|
+
WHERE task_id = ? AND file_id = (SELECT id FROM m_files WHERE path = ?)
|
|
1149
|
+
`);
|
|
1150
|
+
const removedFiles = [];
|
|
1151
|
+
for (const filePath of params.file_paths) {
|
|
1152
|
+
const result = deleteFileLinkStmt.run(params.task_id, filePath);
|
|
1153
|
+
// Check if row was actually deleted (changes > 0)
|
|
1154
|
+
if (result.changes > 0) {
|
|
1155
|
+
removedFiles.push(filePath);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
// Unregister files from watcher
|
|
1159
|
+
try {
|
|
1160
|
+
const watcher = FileWatcher.getInstance();
|
|
1161
|
+
for (const filePath of removedFiles) {
|
|
1162
|
+
// Note: FileWatcher.unregisterFile doesn't exist in current API
|
|
1163
|
+
// The watcher will handle cleanup when task moves to done/archived
|
|
1164
|
+
// For now, we just remove the DB link
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
catch (error) {
|
|
1168
|
+
// Watcher may not be initialized, ignore
|
|
1169
|
+
}
|
|
1170
|
+
return {
|
|
1171
|
+
success: true,
|
|
1172
|
+
task_id: params.task_id,
|
|
1173
|
+
action: 'unwatch',
|
|
1174
|
+
files_removed: removedFiles.length,
|
|
1175
|
+
files: removedFiles,
|
|
1176
|
+
message: `Stopped watching ${removedFiles.length} file(s) for task ${params.task_id}`
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
else if (params.action === 'list') {
|
|
1180
|
+
const filesStmt = actualDb.prepare(`
|
|
1181
|
+
SELECT f.path
|
|
1182
|
+
FROM t_task_file_links tfl
|
|
1183
|
+
JOIN m_files f ON tfl.file_id = f.id
|
|
1184
|
+
WHERE tfl.task_id = ?
|
|
1185
|
+
`);
|
|
1186
|
+
const files = filesStmt.all(params.task_id).map((row) => row.path);
|
|
1187
|
+
return {
|
|
1188
|
+
success: true,
|
|
1189
|
+
task_id: params.task_id,
|
|
1190
|
+
action: 'list',
|
|
1191
|
+
files_count: files.length,
|
|
1192
|
+
files: files,
|
|
1193
|
+
message: `Task ${params.task_id} is watching ${files.length} file(s)`
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
throw new Error(`Invalid action: ${params.action}. Must be one of: watch, unwatch, list`);
|
|
1198
|
+
}
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
catch (error) {
|
|
1202
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1203
|
+
throw new Error(`Failed to ${params.action} files: ${message}`);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Get pruned files for a task (v3.5.0 Auto-Pruning)
|
|
1208
|
+
* Returns audit trail of files that were auto-pruned as non-existent
|
|
1209
|
+
*/
|
|
1210
|
+
export function getPrunedFiles(params, db) {
|
|
1211
|
+
const actualDb = db ?? getDatabase();
|
|
1212
|
+
try {
|
|
1213
|
+
// Validate task_id
|
|
1214
|
+
if (!params.task_id || typeof params.task_id !== 'number') {
|
|
1215
|
+
throw new Error('task_id is required and must be a number');
|
|
1216
|
+
}
|
|
1217
|
+
// Validate task exists
|
|
1218
|
+
const task = actualDb.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
|
|
1219
|
+
if (!task) {
|
|
1220
|
+
throw new Error(`Task not found: ${params.task_id}`);
|
|
1221
|
+
}
|
|
1222
|
+
// Get pruned files
|
|
1223
|
+
const limit = params.limit || 100;
|
|
1224
|
+
const rows = actualDb.prepare(`
|
|
1225
|
+
SELECT
|
|
1226
|
+
tpf.id,
|
|
1227
|
+
tpf.file_path,
|
|
1228
|
+
datetime(tpf.pruned_ts, 'unixepoch') as pruned_at,
|
|
1229
|
+
k.key as linked_decision
|
|
1230
|
+
FROM t_task_pruned_files tpf
|
|
1231
|
+
LEFT JOIN m_context_keys k ON tpf.linked_decision_key_id = k.id
|
|
1232
|
+
WHERE tpf.task_id = ?
|
|
1233
|
+
ORDER BY tpf.pruned_ts DESC
|
|
1234
|
+
LIMIT ?
|
|
1235
|
+
`).all(params.task_id, limit);
|
|
1236
|
+
return {
|
|
1237
|
+
success: true,
|
|
1238
|
+
task_id: params.task_id,
|
|
1239
|
+
pruned_files: rows,
|
|
1240
|
+
count: rows.length,
|
|
1241
|
+
message: rows.length > 0
|
|
1242
|
+
? `Found ${rows.length} pruned file(s) for task ${params.task_id}`
|
|
1243
|
+
: `No pruned files for task ${params.task_id}`
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
catch (error) {
|
|
1247
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1248
|
+
throw new Error(`Failed to get pruned files: ${message}`);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
/**
|
|
1252
|
+
* Link a pruned file to a decision (v3.5.0 Auto-Pruning)
|
|
1253
|
+
* Attaches WHY reasoning to pruned files for project archaeology
|
|
1254
|
+
*/
|
|
1255
|
+
export function linkPrunedFile(params, db) {
|
|
1256
|
+
const actualDb = db ?? getDatabase();
|
|
1257
|
+
try {
|
|
1258
|
+
// Validate pruned_file_id
|
|
1259
|
+
if (!params.pruned_file_id || typeof params.pruned_file_id !== 'number') {
|
|
1260
|
+
throw new Error('pruned_file_id is required and must be a number');
|
|
1261
|
+
}
|
|
1262
|
+
// Validate decision_key
|
|
1263
|
+
if (!params.decision_key || typeof params.decision_key !== 'string') {
|
|
1264
|
+
throw new Error('decision_key is required and must be a string');
|
|
1265
|
+
}
|
|
1266
|
+
// Get decision key_id
|
|
1267
|
+
const decision = actualDb.prepare(`
|
|
1268
|
+
SELECT k.id as key_id
|
|
1269
|
+
FROM m_context_keys k
|
|
1270
|
+
WHERE k.key = ? AND EXISTS (
|
|
1271
|
+
SELECT 1 FROM t_decisions d WHERE d.key_id = k.id
|
|
1272
|
+
)
|
|
1273
|
+
`).get(params.decision_key);
|
|
1274
|
+
if (!decision) {
|
|
1275
|
+
throw new Error(`Decision not found: ${params.decision_key}`);
|
|
1276
|
+
}
|
|
1277
|
+
// Check if pruned file exists
|
|
1278
|
+
const prunedFile = actualDb.prepare(`
|
|
1279
|
+
SELECT id, task_id, file_path FROM t_task_pruned_files WHERE id = ?
|
|
1280
|
+
`).get(params.pruned_file_id);
|
|
1281
|
+
if (!prunedFile) {
|
|
1282
|
+
throw new Error(`Pruned file record not found: ${params.pruned_file_id}`);
|
|
1283
|
+
}
|
|
1284
|
+
// Update the link
|
|
1285
|
+
const result = actualDb.prepare(`
|
|
1286
|
+
UPDATE t_task_pruned_files
|
|
1287
|
+
SET linked_decision_key_id = ?
|
|
1288
|
+
WHERE id = ?
|
|
1289
|
+
`).run(decision.key_id, params.pruned_file_id);
|
|
1290
|
+
if (result.changes === 0) {
|
|
1291
|
+
throw new Error(`Failed to link pruned file #${params.pruned_file_id} to decision ${params.decision_key}`);
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
success: true,
|
|
1295
|
+
pruned_file_id: params.pruned_file_id,
|
|
1296
|
+
decision_key: params.decision_key,
|
|
1297
|
+
task_id: prunedFile.task_id,
|
|
1298
|
+
file_path: prunedFile.file_path,
|
|
1299
|
+
message: `Linked pruned file "${prunedFile.file_path}" to decision "${params.decision_key}"`
|
|
1300
|
+
};
|
|
1301
|
+
}
|
|
1302
|
+
catch (error) {
|
|
1303
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1304
|
+
throw new Error(`Failed to link pruned file: ${message}`);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
951
1307
|
/**
|
|
952
1308
|
* Return comprehensive help documentation
|
|
953
1309
|
*/
|
|
@@ -961,7 +1317,8 @@ export function taskHelp() {
|
|
|
961
1317
|
create: {
|
|
962
1318
|
description: 'Create a new task',
|
|
963
1319
|
required_params: ['title'],
|
|
964
|
-
optional_params: ['description', 'acceptance_criteria', 'notes', 'priority', 'assigned_agent', 'created_by_agent', 'layer', 'tags', 'status'],
|
|
1320
|
+
optional_params: ['description', 'acceptance_criteria', 'notes', 'priority', 'assigned_agent', 'created_by_agent', 'layer', 'tags', 'status', 'watch_files'],
|
|
1321
|
+
watch_files_param: '⭐ NEW in v3.4.1: Pass watch_files array to automatically link and watch files (replaces task.link(file))',
|
|
965
1322
|
example: {
|
|
966
1323
|
action: 'create',
|
|
967
1324
|
title: 'Implement authentication endpoint',
|
|
@@ -969,18 +1326,21 @@ export function taskHelp() {
|
|
|
969
1326
|
priority: 3,
|
|
970
1327
|
assigned_agent: 'backend-agent',
|
|
971
1328
|
layer: 'presentation',
|
|
972
|
-
tags: ['api', 'authentication']
|
|
1329
|
+
tags: ['api', 'authentication'],
|
|
1330
|
+
watch_files: ['src/api/auth.ts', 'src/middleware/jwt.ts']
|
|
973
1331
|
}
|
|
974
1332
|
},
|
|
975
1333
|
update: {
|
|
976
1334
|
description: 'Update task metadata',
|
|
977
1335
|
required_params: ['task_id'],
|
|
978
|
-
optional_params: ['title', 'priority', 'assigned_agent', 'layer', 'description', 'acceptance_criteria', 'notes'],
|
|
1336
|
+
optional_params: ['title', 'priority', 'assigned_agent', 'layer', 'description', 'acceptance_criteria', 'notes', 'watch_files'],
|
|
1337
|
+
watch_files_param: '⭐ NEW in v3.4.1: Pass watch_files array to add files to watch list',
|
|
979
1338
|
example: {
|
|
980
1339
|
action: 'update',
|
|
981
1340
|
task_id: 5,
|
|
982
1341
|
priority: 4,
|
|
983
|
-
assigned_agent: 'senior-backend-agent'
|
|
1342
|
+
assigned_agent: 'senior-backend-agent',
|
|
1343
|
+
watch_files: ['src/api/users.ts']
|
|
984
1344
|
}
|
|
985
1345
|
},
|
|
986
1346
|
get: {
|
|
@@ -1025,7 +1385,8 @@ export function taskHelp() {
|
|
|
1025
1385
|
required_params: ['task_id', 'link_type', 'target_id'],
|
|
1026
1386
|
optional_params: ['link_relation'],
|
|
1027
1387
|
link_types: ['decision', 'constraint', 'file'],
|
|
1028
|
-
file_linking_behavior: '⚠️
|
|
1388
|
+
file_linking_behavior: '⚠️ DEPRECATED in v3.4.1: link_type="file" is deprecated. Use watch_files action or watch_files parameter instead.',
|
|
1389
|
+
deprecation_note: 'For file watching, use: (1) watch_files parameter in create/update, or (2) watch_files action with watch/unwatch/list',
|
|
1029
1390
|
example: {
|
|
1030
1391
|
action: 'link',
|
|
1031
1392
|
task_id: 5,
|
|
@@ -1034,6 +1395,34 @@ export function taskHelp() {
|
|
|
1034
1395
|
link_relation: 'implements'
|
|
1035
1396
|
}
|
|
1036
1397
|
},
|
|
1398
|
+
watch_files: {
|
|
1399
|
+
description: '⭐ NEW in v3.4.1: Watch/unwatch files for a task (replaces task.link(file))',
|
|
1400
|
+
required_params: ['task_id', 'action'],
|
|
1401
|
+
optional_params: ['file_paths'],
|
|
1402
|
+
actions: ['watch', 'unwatch', 'list'],
|
|
1403
|
+
behavior: {
|
|
1404
|
+
watch: 'Add files to watch list and activate file monitoring',
|
|
1405
|
+
unwatch: 'Remove files from watch list',
|
|
1406
|
+
list: 'List all files currently watched by this task'
|
|
1407
|
+
},
|
|
1408
|
+
examples: {
|
|
1409
|
+
watch: {
|
|
1410
|
+
task_id: 5,
|
|
1411
|
+
action: 'watch',
|
|
1412
|
+
file_paths: ['src/api/auth.ts', 'src/middleware/jwt.ts']
|
|
1413
|
+
},
|
|
1414
|
+
unwatch: {
|
|
1415
|
+
task_id: 5,
|
|
1416
|
+
action: 'unwatch',
|
|
1417
|
+
file_paths: ['src/middleware/jwt.ts']
|
|
1418
|
+
},
|
|
1419
|
+
list: {
|
|
1420
|
+
task_id: 5,
|
|
1421
|
+
action: 'list'
|
|
1422
|
+
}
|
|
1423
|
+
},
|
|
1424
|
+
note: 'Preferred over task.link(file) for better clarity and batch operations'
|
|
1425
|
+
},
|
|
1037
1426
|
archive: {
|
|
1038
1427
|
description: 'Archive completed task (must be in done status)',
|
|
1039
1428
|
required_params: ['task_id'],
|
|
@@ -1176,7 +1565,8 @@ export function taskHelp() {
|
|
|
1176
1565
|
/**
|
|
1177
1566
|
* Query file watcher status and monitored files/tasks
|
|
1178
1567
|
*/
|
|
1179
|
-
export function watcherStatus(args) {
|
|
1568
|
+
export function watcherStatus(args, db) {
|
|
1569
|
+
const actualDb = db ?? getDatabase();
|
|
1180
1570
|
const subaction = args.subaction || 'status';
|
|
1181
1571
|
const watcher = FileWatcher.getInstance();
|
|
1182
1572
|
if (subaction === 'help') {
|
|
@@ -1219,8 +1609,7 @@ export function watcherStatus(args) {
|
|
|
1219
1609
|
};
|
|
1220
1610
|
}
|
|
1221
1611
|
if (subaction === 'list_files') {
|
|
1222
|
-
const
|
|
1223
|
-
const fileLinks = db.prepare(`
|
|
1612
|
+
const fileLinks = actualDb.prepare(`
|
|
1224
1613
|
SELECT DISTINCT tfl.file_path, t.id, t.title, ts.status_name
|
|
1225
1614
|
FROM t_task_file_links tfl
|
|
1226
1615
|
JOIN t_tasks t ON tfl.task_id = t.id
|
|
@@ -1254,8 +1643,7 @@ export function watcherStatus(args) {
|
|
|
1254
1643
|
};
|
|
1255
1644
|
}
|
|
1256
1645
|
if (subaction === 'list_tasks') {
|
|
1257
|
-
const
|
|
1258
|
-
const taskLinks = db.prepare(`
|
|
1646
|
+
const taskLinks = actualDb.prepare(`
|
|
1259
1647
|
SELECT t.id, t.title, ts.status_name, COUNT(DISTINCT tfl.file_path) as file_count,
|
|
1260
1648
|
GROUP_CONCAT(DISTINCT tfl.file_path, ', ') as files
|
|
1261
1649
|
FROM t_tasks t
|