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.
Files changed (144) hide show
  1. package/CHANGELOG.md +815 -13
  2. package/README.md +53 -2
  3. package/assets/schema.sql +6 -1
  4. package/dist/config/loader.d.ts +46 -0
  5. package/dist/config/loader.d.ts.map +1 -0
  6. package/dist/config/loader.js +151 -0
  7. package/dist/config/loader.js.map +1 -0
  8. package/dist/config/types.d.ts +77 -0
  9. package/dist/config/types.d.ts.map +1 -0
  10. package/dist/config/types.js +28 -0
  11. package/dist/config/types.js.map +1 -0
  12. package/dist/constants.d.ts +9 -0
  13. package/dist/constants.d.ts.map +1 -1
  14. package/dist/constants.js +10 -0
  15. package/dist/constants.js.map +1 -1
  16. package/dist/database.d.ts +1 -1
  17. package/dist/database.d.ts.map +1 -1
  18. package/dist/database.js +77 -10
  19. package/dist/database.js.map +1 -1
  20. package/dist/index.js +21 -3
  21. package/dist/index.js.map +1 -1
  22. package/dist/migrations/add-v3.5.0-pruned-files.d.ts +26 -0
  23. package/dist/migrations/add-v3.5.0-pruned-files.d.ts.map +1 -0
  24. package/dist/migrations/add-v3.5.0-pruned-files.js +107 -0
  25. package/dist/migrations/add-v3.5.0-pruned-files.js.map +1 -0
  26. package/dist/migrations/index.d.ts +2 -1
  27. package/dist/migrations/index.d.ts.map +1 -1
  28. package/dist/migrations/index.js +16 -1
  29. package/dist/migrations/index.js.map +1 -1
  30. package/dist/tests/git-aware-completion.test.d.ts +6 -0
  31. package/dist/tests/git-aware-completion.test.d.ts.map +1 -0
  32. package/dist/tests/git-aware-completion.test.js +141 -0
  33. package/dist/tests/git-aware-completion.test.js.map +1 -0
  34. package/dist/tests/tasks.auto-pruning-decision-link.test.d.ts +6 -0
  35. package/dist/tests/tasks.auto-pruning-decision-link.test.d.ts.map +1 -0
  36. package/dist/tests/tasks.auto-pruning-decision-link.test.js +250 -0
  37. package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -0
  38. package/dist/tests/tasks.auto-pruning-partial.test.d.ts +6 -0
  39. package/dist/tests/tasks.auto-pruning-partial.test.d.ts.map +1 -0
  40. package/dist/tests/tasks.auto-pruning-partial.test.js +274 -0
  41. package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -0
  42. package/dist/tests/tasks.auto-pruning-persistence.test.d.ts +6 -0
  43. package/dist/tests/tasks.auto-pruning-persistence.test.d.ts.map +1 -0
  44. package/dist/tests/tasks.auto-pruning-persistence.test.js +232 -0
  45. package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -0
  46. package/dist/tests/tasks.auto-pruning-safety.test.d.ts +12 -0
  47. package/dist/tests/tasks.auto-pruning-safety.test.d.ts.map +1 -0
  48. package/dist/tests/tasks.auto-pruning-safety.test.js +196 -0
  49. package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -0
  50. package/dist/tests/tasks.link-file-backward-compat.test.d.ts +6 -0
  51. package/dist/tests/tasks.link-file-backward-compat.test.d.ts.map +1 -0
  52. package/dist/tests/tasks.link-file-backward-compat.test.js +235 -0
  53. package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -0
  54. package/dist/tests/tasks.watch-files-action.test.d.ts +6 -0
  55. package/dist/tests/tasks.watch-files-action.test.d.ts.map +1 -0
  56. package/dist/tests/tasks.watch-files-action.test.js +351 -0
  57. package/dist/tests/tasks.watch-files-action.test.js.map +1 -0
  58. package/dist/tests/tasks.watch-files-parameter.test.d.ts +6 -0
  59. package/dist/tests/tasks.watch-files-parameter.test.d.ts.map +1 -0
  60. package/dist/tests/tasks.watch-files-parameter.test.js +249 -0
  61. package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -0
  62. package/dist/tests/two-step-git-completion.test.d.ts +6 -0
  63. package/dist/tests/two-step-git-completion.test.d.ts.map +1 -0
  64. package/dist/tests/two-step-git-completion.test.js +283 -0
  65. package/dist/tests/two-step-git-completion.test.js.map +1 -0
  66. package/dist/tests/vcs-staging.test.d.ts +6 -0
  67. package/dist/tests/vcs-staging.test.d.ts.map +1 -0
  68. package/dist/tests/vcs-staging.test.js +137 -0
  69. package/dist/tests/vcs-staging.test.js.map +1 -0
  70. package/dist/tools/config.d.ts +4 -2
  71. package/dist/tools/config.d.ts.map +1 -1
  72. package/dist/tools/config.js +13 -11
  73. package/dist/tools/config.js.map +1 -1
  74. package/dist/tools/constraints.d.ts +7 -4
  75. package/dist/tools/constraints.d.ts.map +1 -1
  76. package/dist/tools/constraints.js +19 -16
  77. package/dist/tools/constraints.js.map +1 -1
  78. package/dist/tools/context.d.ts +33 -17
  79. package/dist/tools/context.d.ts.map +1 -1
  80. package/dist/tools/context.js +84 -68
  81. package/dist/tools/context.js.map +1 -1
  82. package/dist/tools/files.d.ts +9 -5
  83. package/dist/tools/files.d.ts.map +1 -1
  84. package/dist/tools/files.js +19 -15
  85. package/dist/tools/files.js.map +1 -1
  86. package/dist/tools/messaging.d.ts +9 -5
  87. package/dist/tools/messaging.d.ts.map +1 -1
  88. package/dist/tools/messaging.js +20 -16
  89. package/dist/tools/messaging.js.map +1 -1
  90. package/dist/tools/tasks.d.ts +40 -12
  91. package/dist/tools/tasks.d.ts.map +1 -1
  92. package/dist/tools/tasks.js +475 -87
  93. package/dist/tools/tasks.js.map +1 -1
  94. package/dist/tools/utils.d.ts +11 -6
  95. package/dist/tools/utils.d.ts.map +1 -1
  96. package/dist/tools/utils.js +56 -44
  97. package/dist/tools/utils.js.map +1 -1
  98. package/dist/types.d.ts +4 -0
  99. package/dist/types.d.ts.map +1 -1
  100. package/dist/utils/file-pruning.d.ts +69 -0
  101. package/dist/utils/file-pruning.d.ts.map +1 -0
  102. package/dist/utils/file-pruning.js +185 -0
  103. package/dist/utils/file-pruning.js.map +1 -0
  104. package/dist/utils/quality-checks.d.ts +60 -0
  105. package/dist/utils/quality-checks.d.ts.map +1 -0
  106. package/dist/utils/quality-checks.js +228 -0
  107. package/dist/utils/quality-checks.js.map +1 -0
  108. package/dist/utils/retention.d.ts +8 -0
  109. package/dist/utils/retention.d.ts.map +1 -1
  110. package/dist/utils/retention.js +12 -0
  111. package/dist/utils/retention.js.map +1 -1
  112. package/dist/utils/task-stale-detection.d.ts +69 -1
  113. package/dist/utils/task-stale-detection.d.ts.map +1 -1
  114. package/dist/utils/task-stale-detection.js +494 -17
  115. package/dist/utils/task-stale-detection.js.map +1 -1
  116. package/dist/utils/vcs-adapter.d.ts +68 -0
  117. package/dist/utils/vcs-adapter.d.ts.map +1 -0
  118. package/dist/utils/vcs-adapter.js +187 -0
  119. package/dist/utils/vcs-adapter.js.map +1 -0
  120. package/dist/watcher/file-watcher.d.ts +54 -4
  121. package/dist/watcher/file-watcher.d.ts.map +1 -1
  122. package/dist/watcher/file-watcher.js +312 -30
  123. package/dist/watcher/file-watcher.js.map +1 -1
  124. package/dist/watcher/gitignore-parser.d.ts +70 -0
  125. package/dist/watcher/gitignore-parser.d.ts.map +1 -0
  126. package/dist/watcher/gitignore-parser.js +191 -0
  127. package/dist/watcher/gitignore-parser.js.map +1 -0
  128. package/dist/watcher/index.d.ts +1 -0
  129. package/dist/watcher/index.d.ts.map +1 -1
  130. package/dist/watcher/index.js +1 -0
  131. package/dist/watcher/index.js.map +1 -1
  132. package/docs/AI_AGENT_GUIDE.md +1 -1
  133. package/docs/ARCHITECTURE.md +12 -0
  134. package/docs/AUTO_FILE_TRACKING.md +486 -82
  135. package/docs/CONFIGURATION.md +908 -0
  136. package/docs/GIT_AWARE_AUTO_COMPLETE.md +645 -0
  137. package/docs/MIGRATION_v3.3.md +602 -0
  138. package/docs/SHARED_CONCEPTS.md +2 -1
  139. package/docs/TASK_ACTIONS.md +12 -0
  140. package/docs/TASK_OVERVIEW.md +124 -23
  141. package/docs/TASK_PRUNING.md +589 -0
  142. package/docs/TASK_SYSTEM.md +83 -13
  143. package/docs/TOOL_REFERENCE.md +94 -6
  144. package/package.json +8 -6
@@ -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
- let createdByAgentId = null;
83
- if (params.created_by_agent) {
84
- createdByAgentId = getOrCreateAgent(db, params.created_by_agent);
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 db = getDatabase();
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(db, () => {
167
- return createTaskInternal(params, db);
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 db = getDatabase();
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(db, () => {
241
+ return transaction(actualDb, () => {
186
242
  // Check if task exists
187
- const taskExists = db.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
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(db, params.assigned_agent);
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(db, params.layer);
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 = db.prepare(`
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 = db.prepare('SELECT task_id FROM t_task_details WHERE task_id = ?').get(params.task_id);
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 = db.prepare(`
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 = db.prepare(`
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 db = getDatabase();
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 = db.prepare(`
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 = db.prepare(`
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 = db.prepare(`
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 = db.prepare(`
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 = db.prepare(`
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(db, params.task_id, false);
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 db = getDatabase();
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(db);
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 = db.prepare(query);
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 db = getDatabase();
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(db);
568
- return transaction(db, () => {
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 = db.prepare('SELECT status_id FROM t_tasks WHERE id = ?').get(params.task_id);
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 = db.prepare(`
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 db = getDatabase();
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(db, () => {
757
+ return transaction(actualDb, () => {
633
758
  // Check if task exists
634
- const taskExists = db.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
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(db, decisionKey);
765
+ const keyId = getOrCreateContextKey(actualDb, decisionKey);
641
766
  const linkRelation = params.link_relation || 'implements';
642
- const stmt = db.prepare(`
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 = db.prepare('SELECT id FROM t_constraints WHERE id = ?').get(constraintId);
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 = db.prepare(`
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(db, filePath);
679
- const stmt = db.prepare(`
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 = db.prepare(`
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
- message: `Task ${params.task_id} linked to file "${filePath}"`
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 db = getDatabase();
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(db, () => {
858
+ return transaction(actualDb, () => {
729
859
  // Check if task is in 'done' status
730
- const taskRow = db.prepare('SELECT status_id FROM t_tasks WHERE id = ?').get(params.task_id);
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 = db.prepare('UPDATE t_tasks SET status_id = ? WHERE id = ?');
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 db = getDatabase();
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(db, () => {
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 = db.prepare('SELECT id, status_id FROM t_tasks WHERE id = ?').get(params.blocker_task_id);
779
- const blockedTask = db.prepare('SELECT id, status_id FROM t_tasks WHERE id = ?').get(params.blocked_task_id);
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 = db.prepare(`
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 = db.prepare(`
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 = db.prepare(`
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 = db.prepare(`
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 db = getDatabase();
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 = db.prepare(`
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 db = getDatabase();
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 = db.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
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(db, params.task_id, includeDetails);
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 db = getDatabase();
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(db, params.tasks, (task, db) => {
932
- const result = createTaskInternal(task, db);
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: '⚠️ IMPORTANT: When link_type="file", this action ACTIVATES AUTOMATIC FILE WATCHING. The file watcher monitors linked files for changes and validates acceptance criteria when files are saved. You can save 300 tokens per file compared to registering watchers manually.',
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 db = getDatabase();
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 db = getDatabase();
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