sqlew 3.2.5 → 3.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/CHANGELOG.md +288 -1011
  2. package/README.md +80 -263
  3. package/assets/config.example.toml +97 -0
  4. package/assets/schema.sql +6 -1
  5. package/dist/adapters/index.d.ts +11 -0
  6. package/dist/adapters/index.d.ts.map +1 -0
  7. package/dist/adapters/index.js +21 -0
  8. package/dist/adapters/index.js.map +1 -0
  9. package/dist/adapters/mysql-adapter.d.ts +31 -0
  10. package/dist/adapters/mysql-adapter.d.ts.map +1 -0
  11. package/dist/adapters/mysql-adapter.js +63 -0
  12. package/dist/adapters/mysql-adapter.js.map +1 -0
  13. package/dist/adapters/postgresql-adapter.d.ts +31 -0
  14. package/dist/adapters/postgresql-adapter.d.ts.map +1 -0
  15. package/dist/adapters/postgresql-adapter.js +63 -0
  16. package/dist/adapters/postgresql-adapter.js.map +1 -0
  17. package/dist/adapters/sqlite-adapter.d.ts +37 -0
  18. package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
  19. package/dist/adapters/sqlite-adapter.js +129 -0
  20. package/dist/adapters/sqlite-adapter.js.map +1 -0
  21. package/dist/adapters/types.d.ts +33 -0
  22. package/dist/adapters/types.d.ts.map +1 -0
  23. package/dist/adapters/types.js +2 -0
  24. package/dist/adapters/types.js.map +1 -0
  25. package/dist/cli.js +55 -54
  26. package/dist/cli.js.map +1 -1
  27. package/dist/config/example-generator.d.ts +11 -0
  28. package/dist/config/example-generator.d.ts.map +1 -0
  29. package/dist/config/example-generator.js +48 -0
  30. package/dist/config/example-generator.js.map +1 -0
  31. package/dist/config/loader.d.ts +46 -0
  32. package/dist/config/loader.d.ts.map +1 -0
  33. package/dist/config/loader.js +155 -0
  34. package/dist/config/loader.js.map +1 -0
  35. package/dist/config/types.d.ts +86 -0
  36. package/dist/config/types.d.ts.map +1 -0
  37. package/dist/config/types.js +28 -0
  38. package/dist/config/types.js.map +1 -0
  39. package/dist/constants.d.ts +9 -0
  40. package/dist/constants.d.ts.map +1 -1
  41. package/dist/constants.js +10 -0
  42. package/dist/constants.js.map +1 -1
  43. package/dist/database.d.ts +44 -122
  44. package/dist/database.d.ts.map +1 -1
  45. package/dist/database.js +145 -349
  46. package/dist/database.js.map +1 -1
  47. package/dist/index.js +223 -175
  48. package/dist/index.js.map +1 -1
  49. package/dist/knexfile.d.ts +6 -0
  50. package/dist/knexfile.d.ts.map +1 -0
  51. package/dist/knexfile.js +85 -0
  52. package/dist/knexfile.js.map +1 -0
  53. package/dist/migrations/add-help-system-tables.d.ts +35 -0
  54. package/dist/migrations/add-help-system-tables.d.ts.map +1 -0
  55. package/dist/migrations/add-help-system-tables.js +206 -0
  56. package/dist/migrations/add-help-system-tables.js.map +1 -0
  57. package/dist/migrations/add-token-tracking.d.ts +28 -0
  58. package/dist/migrations/add-token-tracking.d.ts.map +1 -0
  59. package/dist/migrations/add-token-tracking.js +108 -0
  60. package/dist/migrations/add-token-tracking.js.map +1 -0
  61. package/dist/migrations/add-v3.5.0-pruned-files.d.ts +26 -0
  62. package/dist/migrations/add-v3.5.0-pruned-files.d.ts.map +1 -0
  63. package/dist/migrations/add-v3.5.0-pruned-files.js +107 -0
  64. package/dist/migrations/add-v3.5.0-pruned-files.js.map +1 -0
  65. package/dist/migrations/index.d.ts +26 -12
  66. package/dist/migrations/index.d.ts.map +1 -1
  67. package/dist/migrations/index.js +162 -20
  68. package/dist/migrations/index.js.map +1 -1
  69. package/dist/migrations/knex/20251025020452_create_master_tables.d.ts +4 -0
  70. package/dist/migrations/knex/20251025020452_create_master_tables.d.ts.map +1 -0
  71. package/dist/migrations/knex/20251025020452_create_master_tables.js +65 -0
  72. package/dist/migrations/knex/20251025020452_create_master_tables.js.map +1 -0
  73. package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts +4 -0
  74. package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts.map +1 -0
  75. package/dist/migrations/knex/20251025021152_create_transaction_tables.js +235 -0
  76. package/dist/migrations/knex/20251025021152_create_transaction_tables.js.map +1 -0
  77. package/dist/migrations/knex/20251025021351_create_indexes.d.ts +4 -0
  78. package/dist/migrations/knex/20251025021351_create_indexes.d.ts.map +1 -0
  79. package/dist/migrations/knex/20251025021351_create_indexes.js +62 -0
  80. package/dist/migrations/knex/20251025021351_create_indexes.js.map +1 -0
  81. package/dist/migrations/knex/20251025021416_seed_master_data.d.ts +4 -0
  82. package/dist/migrations/knex/20251025021416_seed_master_data.d.ts.map +1 -0
  83. package/dist/migrations/knex/20251025021416_seed_master_data.js +58 -0
  84. package/dist/migrations/knex/20251025021416_seed_master_data.js.map +1 -0
  85. package/dist/migrations/knex/20251025070349_create_views.d.ts +4 -0
  86. package/dist/migrations/knex/20251025070349_create_views.d.ts.map +1 -0
  87. package/dist/migrations/knex/20251025070349_create_views.js +143 -0
  88. package/dist/migrations/knex/20251025070349_create_views.js.map +1 -0
  89. package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts +4 -0
  90. package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts.map +1 -0
  91. package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js +15 -0
  92. package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js.map +1 -0
  93. package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts +8 -0
  94. package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts.map +1 -0
  95. package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js +12 -0
  96. package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js.map +1 -0
  97. package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts +19 -0
  98. package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts.map +1 -0
  99. package/dist/migrations/knex/20251025090000_create_help_system_tables.js +115 -0
  100. package/dist/migrations/knex/20251025090000_create_help_system_tables.js.map +1 -0
  101. package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts +13 -0
  102. package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts.map +1 -0
  103. package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js +377 -0
  104. package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js.map +1 -0
  105. package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts +15 -0
  106. package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts.map +1 -0
  107. package/dist/migrations/knex/20251025100000_seed_help_metadata.js +253 -0
  108. package/dist/migrations/knex/20251025100000_seed_help_metadata.js.map +1 -0
  109. package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts +16 -0
  110. package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts.map +1 -0
  111. package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js +276 -0
  112. package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js.map +1 -0
  113. package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts +8 -0
  114. package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts.map +1 -0
  115. package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js +64 -0
  116. package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js.map +1 -0
  117. package/dist/migrations/seed-help-data.d.ts +48 -0
  118. package/dist/migrations/seed-help-data.d.ts.map +1 -0
  119. package/dist/migrations/seed-help-data.js +1466 -0
  120. package/dist/migrations/seed-help-data.js.map +1 -0
  121. package/dist/migrations/seed-tool-metadata.d.ts +24 -0
  122. package/dist/migrations/seed-tool-metadata.d.ts.map +1 -0
  123. package/dist/migrations/seed-tool-metadata.js +392 -0
  124. package/dist/migrations/seed-tool-metadata.js.map +1 -0
  125. package/dist/migrations/v3.6.0-help-system-refactor.d.ts +46 -0
  126. package/dist/migrations/v3.6.0-help-system-refactor.d.ts.map +1 -0
  127. package/dist/migrations/v3.6.0-help-system-refactor.js +223 -0
  128. package/dist/migrations/v3.6.0-help-system-refactor.js.map +1 -0
  129. package/dist/schema.d.ts.map +1 -1
  130. package/dist/schema.js +2 -0
  131. package/dist/schema.js.map +1 -1
  132. package/dist/tests/git-aware-completion.test.d.ts +6 -0
  133. package/dist/tests/git-aware-completion.test.d.ts.map +1 -0
  134. package/dist/tests/git-aware-completion.test.js +160 -0
  135. package/dist/tests/git-aware-completion.test.js.map +1 -0
  136. package/dist/tests/help-system.test.d.ts +23 -0
  137. package/dist/tests/help-system.test.d.ts.map +1 -0
  138. package/dist/tests/help-system.test.js +374 -0
  139. package/dist/tests/help-system.test.js.map +1 -0
  140. package/dist/tests/tasks.auto-pruning-decision-link.test.d.ts +6 -0
  141. package/dist/tests/tasks.auto-pruning-decision-link.test.d.ts.map +1 -0
  142. package/dist/tests/tasks.auto-pruning-decision-link.test.js +264 -0
  143. package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -0
  144. package/dist/tests/tasks.auto-pruning-partial.test.d.ts +6 -0
  145. package/dist/tests/tasks.auto-pruning-partial.test.d.ts.map +1 -0
  146. package/dist/tests/tasks.auto-pruning-partial.test.js +285 -0
  147. package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -0
  148. package/dist/tests/tasks.auto-pruning-persistence.test.d.ts +6 -0
  149. package/dist/tests/tasks.auto-pruning-persistence.test.d.ts.map +1 -0
  150. package/dist/tests/tasks.auto-pruning-persistence.test.js +250 -0
  151. package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -0
  152. package/dist/tests/tasks.auto-pruning-safety.test.d.ts +12 -0
  153. package/dist/tests/tasks.auto-pruning-safety.test.d.ts.map +1 -0
  154. package/dist/tests/tasks.auto-pruning-safety.test.js +217 -0
  155. package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -0
  156. package/dist/tests/tasks.dependencies.test.js +338 -307
  157. package/dist/tests/tasks.dependencies.test.js.map +1 -1
  158. package/dist/tests/tasks.link-file-backward-compat.test.d.ts +6 -0
  159. package/dist/tests/tasks.link-file-backward-compat.test.d.ts.map +1 -0
  160. package/dist/tests/tasks.link-file-backward-compat.test.js +247 -0
  161. package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -0
  162. package/dist/tests/tasks.watch-files-action.test.d.ts +6 -0
  163. package/dist/tests/tasks.watch-files-action.test.d.ts.map +1 -0
  164. package/dist/tests/tasks.watch-files-action.test.js +372 -0
  165. package/dist/tests/tasks.watch-files-action.test.js.map +1 -0
  166. package/dist/tests/tasks.watch-files-parameter.test.d.ts +6 -0
  167. package/dist/tests/tasks.watch-files-parameter.test.d.ts.map +1 -0
  168. package/dist/tests/tasks.watch-files-parameter.test.js +260 -0
  169. package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -0
  170. package/dist/tests/two-step-git-completion.test.d.ts +6 -0
  171. package/dist/tests/two-step-git-completion.test.d.ts.map +1 -0
  172. package/dist/tests/two-step-git-completion.test.js +326 -0
  173. package/dist/tests/two-step-git-completion.test.js.map +1 -0
  174. package/dist/tests/vcs-staging.test.d.ts +6 -0
  175. package/dist/tests/vcs-staging.test.d.ts.map +1 -0
  176. package/dist/tests/vcs-staging.test.js +137 -0
  177. package/dist/tests/vcs-staging.test.js.map +1 -0
  178. package/dist/tools/config.d.ts +9 -4
  179. package/dist/tools/config.d.ts.map +1 -1
  180. package/dist/tools/config.js +16 -12
  181. package/dist/tools/config.js.map +1 -1
  182. package/dist/tools/constraints.d.ts +9 -3
  183. package/dist/tools/constraints.d.ts.map +1 -1
  184. package/dist/tools/constraints.js +66 -45
  185. package/dist/tools/constraints.js.map +1 -1
  186. package/dist/tools/context.d.ts +35 -16
  187. package/dist/tools/context.d.ts.map +1 -1
  188. package/dist/tools/context.js +374 -314
  189. package/dist/tools/context.js.map +1 -1
  190. package/dist/tools/files.d.ts +11 -4
  191. package/dist/tools/files.d.ts.map +1 -1
  192. package/dist/tools/files.js +173 -91
  193. package/dist/tools/files.js.map +1 -1
  194. package/dist/tools/help-queries.d.ts +130 -0
  195. package/dist/tools/help-queries.d.ts.map +1 -0
  196. package/dist/tools/help-queries.js +393 -0
  197. package/dist/tools/help-queries.js.map +1 -0
  198. package/dist/tools/messaging.d.ts +13 -6
  199. package/dist/tools/messaging.d.ts.map +1 -1
  200. package/dist/tools/messaging.js +217 -129
  201. package/dist/tools/messaging.js.map +1 -1
  202. package/dist/tools/tasks.d.ts +42 -12
  203. package/dist/tools/tasks.d.ts.map +1 -1
  204. package/dist/tools/tasks.js +809 -347
  205. package/dist/tools/tasks.js.map +1 -1
  206. package/dist/tools/utils.d.ts +13 -5
  207. package/dist/tools/utils.d.ts.map +1 -1
  208. package/dist/tools/utils.js +92 -115
  209. package/dist/tools/utils.js.map +1 -1
  210. package/dist/types.d.ts +4 -0
  211. package/dist/types.d.ts.map +1 -1
  212. package/dist/utils/activity-logging.d.ts +114 -0
  213. package/dist/utils/activity-logging.d.ts.map +1 -0
  214. package/dist/utils/activity-logging.js +162 -0
  215. package/dist/utils/activity-logging.js.map +1 -0
  216. package/dist/utils/batch.d.ts +2 -2
  217. package/dist/utils/batch.d.ts.map +1 -1
  218. package/dist/utils/batch.js +8 -8
  219. package/dist/utils/batch.js.map +1 -1
  220. package/dist/utils/cleanup.d.ts +21 -13
  221. package/dist/utils/cleanup.d.ts.map +1 -1
  222. package/dist/utils/cleanup.js +31 -24
  223. package/dist/utils/cleanup.js.map +1 -1
  224. package/dist/utils/debug-logger.d.ts +44 -0
  225. package/dist/utils/debug-logger.d.ts.map +1 -0
  226. package/dist/utils/debug-logger.js +116 -0
  227. package/dist/utils/debug-logger.js.map +1 -0
  228. package/dist/utils/file-pruning.d.ts +69 -0
  229. package/dist/utils/file-pruning.d.ts.map +1 -0
  230. package/dist/utils/file-pruning.js +185 -0
  231. package/dist/utils/file-pruning.js.map +1 -0
  232. package/dist/utils/help-tracking.d.ts +55 -0
  233. package/dist/utils/help-tracking.d.ts.map +1 -0
  234. package/dist/utils/help-tracking.js +88 -0
  235. package/dist/utils/help-tracking.js.map +1 -0
  236. package/dist/utils/quality-checks.d.ts +60 -0
  237. package/dist/utils/quality-checks.d.ts.map +1 -0
  238. package/dist/utils/quality-checks.js +228 -0
  239. package/dist/utils/quality-checks.js.map +1 -0
  240. package/dist/utils/retention.d.ts +13 -5
  241. package/dist/utils/retention.d.ts.map +1 -1
  242. package/dist/utils/retention.js +20 -8
  243. package/dist/utils/retention.js.map +1 -1
  244. package/dist/utils/task-stale-detection.d.ts +77 -7
  245. package/dist/utils/task-stale-detection.d.ts.map +1 -1
  246. package/dist/utils/task-stale-detection.js +309 -34
  247. package/dist/utils/task-stale-detection.js.map +1 -1
  248. package/dist/utils/token-estimation.d.ts +72 -0
  249. package/dist/utils/token-estimation.d.ts.map +1 -0
  250. package/dist/utils/token-estimation.js +71 -0
  251. package/dist/utils/token-estimation.js.map +1 -0
  252. package/dist/utils/token-logging.d.ts +48 -0
  253. package/dist/utils/token-logging.d.ts.map +1 -0
  254. package/dist/utils/token-logging.js +112 -0
  255. package/dist/utils/token-logging.js.map +1 -0
  256. package/dist/utils/vcs-adapter.d.ts +68 -0
  257. package/dist/utils/vcs-adapter.d.ts.map +1 -0
  258. package/dist/utils/vcs-adapter.js +187 -0
  259. package/dist/utils/vcs-adapter.js.map +1 -0
  260. package/dist/utils/view-queries.d.ts +34 -0
  261. package/dist/utils/view-queries.d.ts.map +1 -0
  262. package/dist/utils/view-queries.js +192 -0
  263. package/dist/utils/view-queries.js.map +1 -0
  264. package/dist/watcher/file-watcher.d.ts +54 -4
  265. package/dist/watcher/file-watcher.d.ts.map +1 -1
  266. package/dist/watcher/file-watcher.js +329 -33
  267. package/dist/watcher/file-watcher.js.map +1 -1
  268. package/dist/watcher/gitignore-parser.d.ts +70 -0
  269. package/dist/watcher/gitignore-parser.d.ts.map +1 -0
  270. package/dist/watcher/gitignore-parser.js +191 -0
  271. package/dist/watcher/gitignore-parser.js.map +1 -0
  272. package/dist/watcher/index.d.ts +1 -0
  273. package/dist/watcher/index.d.ts.map +1 -1
  274. package/dist/watcher/index.js +1 -0
  275. package/dist/watcher/index.js.map +1 -1
  276. package/docs/AI_AGENT_GUIDE.md +1 -1
  277. package/docs/ARCHITECTURE.md +12 -0
  278. package/docs/AUTO_FILE_TRACKING.md +486 -82
  279. package/docs/BEST_PRACTICES.md +56 -448
  280. package/docs/CONFIGURATION.md +908 -0
  281. package/docs/GIT_AWARE_AUTO_COMPLETE.md +645 -0
  282. package/docs/MIGRATION_v3.3.md +602 -0
  283. package/docs/MIGRATION_v3.6.0.md +170 -0
  284. package/docs/SHARED_CONCEPTS.md +65 -209
  285. package/docs/TASK_ACTIONS.md +12 -0
  286. package/docs/TASK_OVERVIEW.md +125 -24
  287. package/docs/TASK_PRUNING.md +589 -0
  288. package/docs/TASK_SYSTEM.md +83 -13
  289. package/docs/TOOL_REFERENCE.md +94 -6
  290. package/docs/TOOL_SELECTION.md +41 -248
  291. package/package.json +21 -7
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Task management tools for Kanban Task Watcher
3
3
  * Implements create, update, get, list, move, link, archive, batch_create actions
4
+ *
5
+ * CONVERTED: Using Knex.js with DatabaseAdapter (async/await)
4
6
  */
5
- import { getDatabase, getOrCreateAgent, getOrCreateTag, getOrCreateContextKey, getLayerId, getOrCreateFile, transaction } from '../database.js';
6
- import { detectAndTransitionStaleTasks } from '../utils/task-stale-detection.js';
7
- import { processBatch } from '../utils/batch.js';
7
+ import { getAdapter, getOrCreateAgent, getOrCreateTag, getOrCreateContextKey, getLayerId, getOrCreateFile } from '../database.js';
8
+ import { detectAndTransitionStaleTasks, autoArchiveOldDoneTasks, detectAndCompleteReviewedTasks, detectAndArchiveOnCommit } from '../utils/task-stale-detection.js';
8
9
  import { FileWatcher } from '../watcher/index.js';
9
10
  import { validatePriorityRange, validateLength, validateRange } from '../utils/validators.js';
11
+ import { logTaskCreate, logTaskStatusChange } from '../utils/activity-logging.js';
10
12
  /**
11
13
  * Task status enum (matches m_task_statuses)
12
14
  */
@@ -53,10 +55,12 @@ const VALID_TRANSITIONS = {
53
55
  * Used by createTask (with transaction) and batchCreateTasks (manages its own transaction)
54
56
  *
55
57
  * @param params - Task parameters
56
- * @param db - Database instance
58
+ * @param adapter - Database adapter instance
59
+ * @param trx - Optional transaction
57
60
  * @returns Response with success status and task metadata
58
61
  */
59
- function createTaskInternal(params, db) {
62
+ async function createTaskInternal(params, adapter, trx) {
63
+ const knex = trx || adapter.getKnex();
60
64
  // Validate priority
61
65
  const priority = params.priority !== undefined ? params.priority : 2;
62
66
  validatePriorityRange(priority);
@@ -69,7 +73,7 @@ function createTaskInternal(params, db) {
69
73
  // Validate layer if provided
70
74
  let layerId = null;
71
75
  if (params.layer) {
72
- layerId = getLayerId(db, params.layer);
76
+ layerId = await getLayerId(adapter, params.layer, trx);
73
77
  if (layerId === null) {
74
78
  throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
75
79
  }
@@ -77,19 +81,24 @@ function createTaskInternal(params, db) {
77
81
  // Get or create agents
78
82
  let assignedAgentId = null;
79
83
  if (params.assigned_agent) {
80
- assignedAgentId = getOrCreateAgent(db, params.assigned_agent);
81
- }
82
- let createdByAgentId = null;
83
- if (params.created_by_agent) {
84
- createdByAgentId = getOrCreateAgent(db, params.created_by_agent);
84
+ assignedAgentId = await getOrCreateAgent(adapter, params.assigned_agent, trx);
85
85
  }
86
+ // Default to 'system' if no created_by_agent provided
87
+ // This ensures the activity log has a valid agent_id
88
+ const createdBy = params.created_by_agent || 'system';
89
+ const createdByAgentId = await getOrCreateAgent(adapter, createdBy, trx);
86
90
  // Insert task
87
- const insertTaskStmt = db.prepare(`
88
- INSERT INTO t_tasks (title, status_id, priority, assigned_agent_id, created_by_agent_id, layer_id)
89
- VALUES (?, ?, ?, ?, ?, ?)
90
- `);
91
- const taskResult = insertTaskStmt.run(params.title, statusId, priority, assignedAgentId, createdByAgentId, layerId);
92
- const taskId = taskResult.lastInsertRowid;
91
+ const now = Math.floor(Date.now() / 1000);
92
+ const [taskId] = await knex('t_tasks').insert({
93
+ title: params.title,
94
+ status_id: statusId,
95
+ priority: priority,
96
+ assigned_agent_id: assignedAgentId,
97
+ created_by_agent_id: createdByAgentId,
98
+ layer_id: layerId,
99
+ created_ts: now,
100
+ updated_ts: now
101
+ });
93
102
  // Process acceptance_criteria (can be string, JSON string, or array)
94
103
  let acceptanceCriteriaString = null;
95
104
  let acceptanceCriteriaJson = null;
@@ -127,26 +136,123 @@ function createTaskInternal(params, db) {
127
136
  }
128
137
  // Insert task details if provided
129
138
  if (params.description || acceptanceCriteriaString || acceptanceCriteriaJson || params.notes) {
130
- const insertDetailsStmt = db.prepare(`
131
- INSERT INTO t_task_details (task_id, description, acceptance_criteria, acceptance_criteria_json, notes)
132
- VALUES (?, ?, ?, ?, ?)
133
- `);
134
- insertDetailsStmt.run(taskId, params.description || null, acceptanceCriteriaString, acceptanceCriteriaJson, params.notes || null);
139
+ await knex('t_task_details').insert({
140
+ task_id: Number(taskId),
141
+ description: params.description || null,
142
+ acceptance_criteria: acceptanceCriteriaString,
143
+ acceptance_criteria_json: acceptanceCriteriaJson,
144
+ notes: params.notes || null
145
+ });
135
146
  }
136
147
  // Insert tags if provided
137
148
  if (params.tags && params.tags.length > 0) {
138
- const insertTagStmt = db.prepare(`
139
- INSERT INTO t_task_tags (task_id, tag_id)
140
- VALUES (?, ?)
141
- `);
142
- for (const tagName of params.tags) {
143
- const tagId = getOrCreateTag(db, tagName);
144
- insertTagStmt.run(taskId, tagId);
149
+ // Parse tags - handle MCP SDK converting JSON string to char array
150
+ let tagsParsed;
151
+ if (typeof params.tags === 'string') {
152
+ // String - try to parse as JSON
153
+ try {
154
+ tagsParsed = JSON.parse(params.tags);
155
+ }
156
+ catch {
157
+ // If not valid JSON, treat as single tag name
158
+ tagsParsed = [params.tags];
159
+ }
160
+ }
161
+ else if (Array.isArray(params.tags)) {
162
+ // Check if it's an array of single characters (MCP SDK bug)
163
+ // Example: ['[', '"', 't', 'e', 's', 't', 'i', 'n', 'g', '"', ']']
164
+ if (params.tags.every((item) => typeof item === 'string' && item.length === 1)) {
165
+ // Join characters back into string and parse JSON
166
+ const jsonString = params.tags.join('');
167
+ try {
168
+ tagsParsed = JSON.parse(jsonString);
169
+ }
170
+ catch (e) {
171
+ const errMsg = e instanceof Error ? e.message : String(e);
172
+ throw new Error(`Invalid tags format: ${jsonString}. ${errMsg}`);
173
+ }
174
+ }
175
+ else {
176
+ // Normal array of tag names
177
+ tagsParsed = params.tags;
178
+ }
179
+ }
180
+ else {
181
+ throw new Error('Parameter "tags" must be a string or array');
182
+ }
183
+ for (const tagName of tagsParsed) {
184
+ const tagId = await getOrCreateTag(adapter, tagName, trx);
185
+ await knex('t_task_tags').insert({
186
+ task_id: Number(taskId),
187
+ tag_id: tagId
188
+ }).onConflict(['task_id', 'tag_id']).ignore();
189
+ }
190
+ }
191
+ // Activity logging (replaces triggers)
192
+ await logTaskCreate(knex, {
193
+ task_id: Number(taskId),
194
+ title: params.title,
195
+ agent_id: createdByAgentId,
196
+ layer_id: layerId || undefined
197
+ });
198
+ // Link files and register with watcher if watch_files provided (v3.4.1)
199
+ if (params.watch_files && params.watch_files.length > 0) {
200
+ // Parse watch_files - handle MCP SDK converting JSON string to char array
201
+ let watchFilesParsed;
202
+ if (typeof params.watch_files === 'string') {
203
+ // String - try to parse as JSON
204
+ try {
205
+ watchFilesParsed = JSON.parse(params.watch_files);
206
+ }
207
+ catch {
208
+ // If not valid JSON, treat as single file path
209
+ watchFilesParsed = [params.watch_files];
210
+ }
211
+ }
212
+ else if (Array.isArray(params.watch_files)) {
213
+ // Check if it's an array of single characters (MCP SDK bug)
214
+ // Example: ['[', '"', 'f', 'i', 'l', 'e', '.', 't', 'x', 't', '"', ']']
215
+ if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
216
+ // Join characters back into string and parse JSON
217
+ const jsonString = params.watch_files.join('');
218
+ try {
219
+ watchFilesParsed = JSON.parse(jsonString);
220
+ }
221
+ catch (e) {
222
+ const errMsg = e instanceof Error ? e.message : String(e);
223
+ throw new Error(`Invalid watch_files format: ${jsonString}. ${errMsg}`);
224
+ }
225
+ }
226
+ else {
227
+ // Normal array of file paths
228
+ watchFilesParsed = params.watch_files;
229
+ }
230
+ }
231
+ else {
232
+ throw new Error('Parameter "watch_files" must be a string or array');
233
+ }
234
+ for (const filePath of watchFilesParsed) {
235
+ const fileId = await getOrCreateFile(adapter, filePath, trx);
236
+ await knex('t_task_file_links').insert({
237
+ task_id: Number(taskId),
238
+ file_id: fileId
239
+ }).onConflict(['task_id', 'file_id']).ignore();
240
+ }
241
+ // Register files with watcher for auto-tracking
242
+ try {
243
+ const watcher = FileWatcher.getInstance();
244
+ for (const filePath of watchFilesParsed) {
245
+ watcher.registerFile(filePath, Number(taskId), params.title, status);
246
+ }
247
+ }
248
+ catch (error) {
249
+ // Watcher may not be initialized yet, ignore
250
+ console.error('Warning: Could not register files with watcher:', error);
145
251
  }
146
252
  }
147
253
  return {
148
254
  success: true,
149
- task_id: taskId,
255
+ task_id: Number(taskId),
150
256
  title: params.title,
151
257
  status: status,
152
258
  message: `Task "${params.title}" created successfully`
@@ -155,16 +261,16 @@ function createTaskInternal(params, db) {
155
261
  /**
156
262
  * Create a new task
157
263
  */
158
- export function createTask(params) {
159
- const db = getDatabase();
264
+ export async function createTask(params, adapter) {
265
+ const actualAdapter = adapter ?? getAdapter();
160
266
  // Validate required parameters
161
267
  if (!params.title || params.title.trim() === '') {
162
268
  throw new Error('Parameter "title" is required and cannot be empty');
163
269
  }
164
270
  validateLength(params.title, 'Parameter "title"', 200);
165
271
  try {
166
- return transaction(db, () => {
167
- return createTaskInternal(params, db);
272
+ return await actualAdapter.transaction(async (trx) => {
273
+ return await createTaskInternal(params, actualAdapter, trx);
168
274
  });
169
275
  }
170
276
  catch (error) {
@@ -175,56 +281,50 @@ export function createTask(params) {
175
281
  /**
176
282
  * Update task metadata
177
283
  */
178
- export function updateTask(params) {
179
- const db = getDatabase();
284
+ export async function updateTask(params, adapter) {
285
+ const actualAdapter = adapter ?? getAdapter();
180
286
  // Validate required parameters
181
287
  if (!params.task_id) {
182
288
  throw new Error('Parameter "task_id" is required');
183
289
  }
184
290
  try {
185
- return transaction(db, () => {
291
+ return await actualAdapter.transaction(async (trx) => {
292
+ const knex = actualAdapter.getKnex();
186
293
  // Check if task exists
187
- const taskExists = db.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
294
+ const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
188
295
  if (!taskExists) {
189
296
  throw new Error(`Task with id ${params.task_id} not found`);
190
297
  }
191
- // Build update query dynamically
192
- const updates = [];
193
- const updateParams = [];
298
+ // Build update data dynamically
299
+ const updateData = {};
194
300
  if (params.title !== undefined) {
195
301
  if (params.title.trim() === '') {
196
302
  throw new Error('Parameter "title" cannot be empty');
197
303
  }
198
304
  validateLength(params.title, 'Parameter "title"', 200);
199
- updates.push('title = ?');
200
- updateParams.push(params.title);
305
+ updateData.title = params.title;
201
306
  }
202
307
  if (params.priority !== undefined) {
203
308
  validatePriorityRange(params.priority);
204
- updates.push('priority = ?');
205
- updateParams.push(params.priority);
309
+ updateData.priority = params.priority;
206
310
  }
207
311
  if (params.assigned_agent !== undefined) {
208
- const agentId = getOrCreateAgent(db, params.assigned_agent);
209
- updates.push('assigned_agent_id = ?');
210
- updateParams.push(agentId);
312
+ const agentId = await getOrCreateAgent(actualAdapter, params.assigned_agent, trx);
313
+ updateData.assigned_agent_id = agentId;
211
314
  }
212
315
  if (params.layer !== undefined) {
213
- const layerId = getLayerId(db, params.layer);
316
+ const layerId = await getLayerId(actualAdapter, params.layer, trx);
214
317
  if (layerId === null) {
215
318
  throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
216
319
  }
217
- updates.push('layer_id = ?');
218
- updateParams.push(layerId);
320
+ updateData.layer_id = layerId;
219
321
  }
220
322
  // Update t_tasks if any updates
221
- if (updates.length > 0) {
222
- const updateStmt = db.prepare(`
223
- UPDATE t_tasks
224
- SET ${updates.join(', ')}
225
- WHERE id = ?
226
- `);
227
- updateStmt.run(...updateParams, params.task_id);
323
+ if (Object.keys(updateData).length > 0) {
324
+ await trx('t_tasks')
325
+ .where({ id: params.task_id })
326
+ .update(updateData);
327
+ // TODO: Add activity logging for updates if needed
228
328
  }
229
329
  // Update t_task_details if any detail fields provided
230
330
  if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
@@ -266,43 +366,95 @@ export function updateTask(params) {
266
366
  }
267
367
  }
268
368
  // Check if details exist
269
- const detailsExist = db.prepare('SELECT task_id FROM t_task_details WHERE task_id = ?').get(params.task_id);
270
- if (detailsExist) {
369
+ const detailsExist = await trx('t_task_details').where({ task_id: params.task_id }).first();
370
+ const detailsUpdate = {};
371
+ if (params.description !== undefined) {
372
+ detailsUpdate.description = params.description || null;
373
+ }
374
+ if (acceptanceCriteriaString !== undefined) {
375
+ detailsUpdate.acceptance_criteria = acceptanceCriteriaString;
376
+ }
377
+ if (acceptanceCriteriaJson !== undefined) {
378
+ detailsUpdate.acceptance_criteria_json = acceptanceCriteriaJson;
379
+ }
380
+ if (params.notes !== undefined) {
381
+ detailsUpdate.notes = params.notes || null;
382
+ }
383
+ if (detailsExist && Object.keys(detailsUpdate).length > 0) {
271
384
  // Update existing details
272
- const detailUpdates = [];
273
- const detailParams = [];
274
- if (params.description !== undefined) {
275
- detailUpdates.push('description = ?');
276
- detailParams.push(params.description || null);
277
- }
278
- if (acceptanceCriteriaString !== undefined) {
279
- detailUpdates.push('acceptance_criteria = ?');
280
- detailParams.push(acceptanceCriteriaString);
385
+ await trx('t_task_details')
386
+ .where({ task_id: params.task_id })
387
+ .update(detailsUpdate);
388
+ }
389
+ else if (!detailsExist) {
390
+ // Insert new details
391
+ await trx('t_task_details').insert({
392
+ task_id: params.task_id,
393
+ description: params.description || null,
394
+ acceptance_criteria: acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null,
395
+ acceptance_criteria_json: acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null,
396
+ notes: params.notes || null
397
+ });
398
+ }
399
+ }
400
+ // Handle watch_files if provided (v3.4.1)
401
+ if (params.watch_files && params.watch_files.length > 0) {
402
+ // Parse watch_files - handle MCP SDK converting JSON string to char array
403
+ let watchFilesParsed;
404
+ if (typeof params.watch_files === 'string') {
405
+ // String - try to parse as JSON
406
+ try {
407
+ watchFilesParsed = JSON.parse(params.watch_files);
281
408
  }
282
- if (acceptanceCriteriaJson !== undefined) {
283
- detailUpdates.push('acceptance_criteria_json = ?');
284
- detailParams.push(acceptanceCriteriaJson);
409
+ catch {
410
+ // If not valid JSON, treat as single file path
411
+ watchFilesParsed = [params.watch_files];
285
412
  }
286
- if (params.notes !== undefined) {
287
- detailUpdates.push('notes = ?');
288
- detailParams.push(params.notes || null);
413
+ }
414
+ else if (Array.isArray(params.watch_files)) {
415
+ // Check if it's an array of single characters (MCP SDK bug)
416
+ if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
417
+ // Join characters back into string and parse JSON
418
+ const jsonString = params.watch_files.join('');
419
+ try {
420
+ watchFilesParsed = JSON.parse(jsonString);
421
+ }
422
+ catch {
423
+ throw new Error(`Invalid watch_files format: ${jsonString}`);
424
+ }
289
425
  }
290
- if (detailUpdates.length > 0) {
291
- const updateDetailsStmt = db.prepare(`
292
- UPDATE t_task_details
293
- SET ${detailUpdates.join(', ')}
294
- WHERE task_id = ?
295
- `);
296
- updateDetailsStmt.run(...detailParams, params.task_id);
426
+ else {
427
+ // Normal array of file paths
428
+ watchFilesParsed = params.watch_files;
297
429
  }
298
430
  }
299
431
  else {
300
- // Insert new details
301
- const insertDetailsStmt = db.prepare(`
302
- INSERT INTO t_task_details (task_id, description, acceptance_criteria, acceptance_criteria_json, notes)
303
- VALUES (?, ?, ?, ?, ?)
304
- `);
305
- insertDetailsStmt.run(params.task_id, params.description || null, acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null, acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null, params.notes || null);
432
+ throw new Error('Parameter "watch_files" must be a string or array');
433
+ }
434
+ for (const filePath of watchFilesParsed) {
435
+ const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
436
+ await trx('t_task_file_links').insert({
437
+ task_id: params.task_id,
438
+ file_id: fileId
439
+ }).onConflict(['task_id', 'file_id']).ignore();
440
+ }
441
+ // Register files with watcher for auto-tracking
442
+ try {
443
+ const taskData = await trx('t_tasks as t')
444
+ .join('m_task_statuses as s', 't.status_id', 's.id')
445
+ .where('t.id', params.task_id)
446
+ .select('t.title', 's.name as status')
447
+ .first();
448
+ if (taskData) {
449
+ const watcher = FileWatcher.getInstance();
450
+ for (const filePath of watchFilesParsed) {
451
+ watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
452
+ }
453
+ }
454
+ }
455
+ catch (error) {
456
+ // Watcher may not be initialized yet, ignore
457
+ console.error('Warning: Could not register files with watcher:', error);
306
458
  }
307
459
  }
308
460
  return {
@@ -320,89 +472,70 @@ export function updateTask(params) {
320
472
  /**
321
473
  * Internal helper: Query task dependencies (used by getTask and getDependencies)
322
474
  */
323
- function queryTaskDependencies(db, taskId, includeDetails = false) {
475
+ async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
476
+ const knex = adapter.getKnex();
324
477
  // Build query based on include_details flag
325
- let selectFields;
478
+ const selectFields = includeDetails
479
+ ? [
480
+ 't.id',
481
+ 't.title',
482
+ 's.name as status',
483
+ 't.priority',
484
+ 'aa.name as assigned_to',
485
+ 't.created_ts',
486
+ 't.updated_ts',
487
+ 'td.description'
488
+ ]
489
+ : [
490
+ 't.id',
491
+ 't.title',
492
+ 's.name as status',
493
+ 't.priority'
494
+ ];
495
+ // Get blockers (tasks that block this task)
496
+ let blockersQuery = knex('t_tasks as t')
497
+ .join('t_task_dependencies as d', 't.id', 'd.blocker_task_id')
498
+ .leftJoin('m_task_statuses as s', 't.status_id', 's.id')
499
+ .leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
500
+ .where('d.blocked_task_id', taskId)
501
+ .select(selectFields);
326
502
  if (includeDetails) {
327
- // Include description from t_task_details
328
- selectFields = `
329
- t.id,
330
- t.title,
331
- s.name as status,
332
- t.priority,
333
- aa.name as assigned_to,
334
- t.created_ts,
335
- t.updated_ts,
336
- td.description
337
- `;
338
- }
339
- else {
340
- // Metadata only (token-efficient)
341
- selectFields = `
342
- t.id,
343
- t.title,
344
- s.name as status,
345
- t.priority
346
- `;
503
+ blockersQuery = blockersQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
347
504
  }
348
- // Get blockers (tasks that block this task)
349
- const blockersQuery = `
350
- SELECT ${selectFields}
351
- FROM t_tasks t
352
- JOIN t_task_dependencies d ON t.id = d.blocker_task_id
353
- LEFT JOIN m_task_statuses s ON t.status_id = s.id
354
- LEFT JOIN m_agents aa ON t.assigned_agent_id = aa.id
355
- ${includeDetails ? 'LEFT JOIN t_task_details td ON t.id = td.task_id' : ''}
356
- WHERE d.blocked_task_id = ?
357
- `;
358
- const blockers = db.prepare(blockersQuery).all(taskId);
505
+ const blockers = await blockersQuery;
359
506
  // Get blocking (tasks this task blocks)
360
- const blockingQuery = `
361
- SELECT ${selectFields}
362
- FROM t_tasks t
363
- JOIN t_task_dependencies d ON t.id = d.blocked_task_id
364
- LEFT JOIN m_task_statuses s ON t.status_id = s.id
365
- LEFT JOIN m_agents aa ON t.assigned_agent_id = aa.id
366
- ${includeDetails ? 'LEFT JOIN t_task_details td ON t.id = td.task_id' : ''}
367
- WHERE d.blocker_task_id = ?
368
- `;
369
- const blocking = db.prepare(blockingQuery).all(taskId);
507
+ let blockingQuery = knex('t_tasks as t')
508
+ .join('t_task_dependencies as d', 't.id', 'd.blocked_task_id')
509
+ .leftJoin('m_task_statuses as s', 't.status_id', 's.id')
510
+ .leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
511
+ .where('d.blocker_task_id', taskId)
512
+ .select(selectFields);
513
+ if (includeDetails) {
514
+ blockingQuery = blockingQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
515
+ }
516
+ const blocking = await blockingQuery;
370
517
  return { blockers, blocking };
371
518
  }
372
519
  /**
373
520
  * Get full task details
374
521
  */
375
- export function getTask(params) {
376
- const db = getDatabase();
522
+ export async function getTask(params, adapter) {
523
+ const actualAdapter = adapter ?? getAdapter();
524
+ const knex = actualAdapter.getKnex();
377
525
  if (!params.task_id) {
378
526
  throw new Error('Parameter "task_id" is required');
379
527
  }
380
528
  try {
381
529
  // Get task with details
382
- const stmt = db.prepare(`
383
- SELECT
384
- t.id,
385
- t.title,
386
- s.name as status,
387
- t.priority,
388
- aa.name as assigned_to,
389
- ca.name as created_by,
390
- l.name as layer,
391
- t.created_ts,
392
- t.updated_ts,
393
- t.completed_ts,
394
- td.description,
395
- td.acceptance_criteria,
396
- td.notes
397
- FROM t_tasks t
398
- LEFT JOIN m_task_statuses s ON t.status_id = s.id
399
- LEFT JOIN m_agents aa ON t.assigned_agent_id = aa.id
400
- LEFT JOIN m_agents ca ON t.created_by_agent_id = ca.id
401
- LEFT JOIN m_layers l ON t.layer_id = l.id
402
- LEFT JOIN t_task_details td ON t.id = td.task_id
403
- WHERE t.id = ?
404
- `);
405
- const task = stmt.get(params.task_id);
530
+ const task = await knex('t_tasks as t')
531
+ .leftJoin('m_task_statuses as s', 't.status_id', 's.id')
532
+ .leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
533
+ .leftJoin('m_agents as ca', 't.created_by_agent_id', 'ca.id')
534
+ .leftJoin('m_layers as l', 't.layer_id', 'l.id')
535
+ .leftJoin('t_task_details as td', 't.id', 'td.task_id')
536
+ .where('t.id', params.task_id)
537
+ .select('t.id', 't.title', 's.name as status', 't.priority', 'aa.name as assigned_to', 'ca.name as created_by', 'l.name as layer', 't.created_ts', 't.updated_ts', 't.completed_ts', 'td.description', 'td.acceptance_criteria', 'td.notes')
538
+ .first();
406
539
  if (!task) {
407
540
  return {
408
541
  found: false,
@@ -410,37 +543,27 @@ export function getTask(params) {
410
543
  };
411
544
  }
412
545
  // Get tags
413
- const tagsStmt = db.prepare(`
414
- SELECT tg.name
415
- FROM t_task_tags tt
416
- JOIN m_tags tg ON tt.tag_id = tg.id
417
- WHERE tt.task_id = ?
418
- `);
419
- const tags = tagsStmt.all(params.task_id).map((row) => row.name);
546
+ const tags = await knex('t_task_tags as tt')
547
+ .join('m_tags as tg', 'tt.tag_id', 'tg.id')
548
+ .where('tt.task_id', params.task_id)
549
+ .select('tg.name')
550
+ .then(rows => rows.map((row) => row.name));
420
551
  // Get decision links
421
- const decisionsStmt = db.prepare(`
422
- SELECT ck.key, tdl.link_type
423
- FROM t_task_decision_links tdl
424
- JOIN m_context_keys ck ON tdl.decision_key_id = ck.id
425
- WHERE tdl.task_id = ?
426
- `);
427
- const decisions = decisionsStmt.all(params.task_id);
552
+ const decisions = await knex('t_task_decision_links as tdl')
553
+ .join('m_context_keys as ck', 'tdl.decision_key_id', 'ck.id')
554
+ .where('tdl.task_id', params.task_id)
555
+ .select('ck.key', 'tdl.link_type');
428
556
  // Get constraint links
429
- const constraintsStmt = db.prepare(`
430
- SELECT c.id, c.constraint_text
431
- FROM t_task_constraint_links tcl
432
- JOIN t_constraints c ON tcl.constraint_id = c.id
433
- WHERE tcl.task_id = ?
434
- `);
435
- const constraints = constraintsStmt.all(params.task_id);
557
+ const constraints = await knex('t_task_constraint_links as tcl')
558
+ .join('t_constraints as c', 'tcl.constraint_id', 'c.id')
559
+ .where('tcl.task_id', params.task_id)
560
+ .select('c.id', 'c.constraint_text');
436
561
  // Get file links
437
- const filesStmt = db.prepare(`
438
- SELECT f.path
439
- FROM t_task_file_links tfl
440
- JOIN m_files f ON tfl.file_id = f.id
441
- WHERE tfl.task_id = ?
442
- `);
443
- const files = filesStmt.all(params.task_id).map((row) => row.path);
562
+ const files = await knex('t_task_file_links as tfl')
563
+ .join('m_files as f', 'tfl.file_id', 'f.id')
564
+ .where('tfl.task_id', params.task_id)
565
+ .select('f.path')
566
+ .then(rows => rows.map((row) => row.path));
444
567
  // Build result
445
568
  const result = {
446
569
  found: true,
@@ -454,7 +577,7 @@ export function getTask(params) {
454
577
  };
455
578
  // Include dependencies if requested (token-efficient, metadata-only)
456
579
  if (params.include_dependencies) {
457
- const deps = queryTaskDependencies(db, params.task_id, false);
580
+ const deps = await queryTaskDependencies(actualAdapter, params.task_id, false);
458
581
  result.task.dependencies = {
459
582
  blockers: deps.blockers,
460
583
  blocking: deps.blocking
@@ -470,80 +593,76 @@ export function getTask(params) {
470
593
  /**
471
594
  * List tasks (token-efficient, no descriptions)
472
595
  */
473
- export function listTasks(params = {}) {
474
- const db = getDatabase();
596
+ export async function listTasks(params = {}, adapter) {
597
+ const actualAdapter = adapter ?? getAdapter();
598
+ const knex = actualAdapter.getKnex();
475
599
  try {
476
- // Run auto-stale detection before listing
477
- const transitionCount = detectAndTransitionStaleTasks(db);
600
+ // Run auto-stale detection, git-aware completion, and auto-archive before listing
601
+ const transitionCount = await detectAndTransitionStaleTasks(actualAdapter);
602
+ const gitCompletedCount = await detectAndCompleteReviewedTasks(actualAdapter);
603
+ const gitArchivedCount = await detectAndArchiveOnCommit(actualAdapter);
604
+ const archiveCount = await autoArchiveOldDoneTasks(actualAdapter);
478
605
  // Build query with optional dependency counts
479
606
  let query;
480
607
  if (params.include_dependency_counts) {
481
608
  // Include dependency counts with LEFT JOINs
482
- query = `
483
- SELECT
484
- vt.*,
485
- COALESCE(blockers.blocked_by_count, 0) as blocked_by_count,
486
- COALESCE(blocking.blocking_count, 0) as blocking_count
487
- FROM v_task_board vt
488
- LEFT JOIN (
489
- SELECT blocked_task_id, COUNT(*) as blocked_by_count
490
- FROM t_task_dependencies
491
- GROUP BY blocked_task_id
492
- ) blockers ON vt.id = blockers.blocked_task_id
493
- LEFT JOIN (
494
- SELECT blocker_task_id, COUNT(*) as blocking_count
495
- FROM t_task_dependencies
496
- GROUP BY blocker_task_id
497
- ) blocking ON vt.id = blocking.blocker_task_id
498
- WHERE 1=1
499
- `;
609
+ const blockersCTE = knex('t_task_dependencies')
610
+ .select('blocked_task_id')
611
+ .count('* as blocked_by_count')
612
+ .groupBy('blocked_task_id')
613
+ .as('blockers');
614
+ const blockingCTE = knex('t_task_dependencies')
615
+ .select('blocker_task_id')
616
+ .count('* as blocking_count')
617
+ .groupBy('blocker_task_id')
618
+ .as('blocking');
619
+ query = knex('v_task_board as vt')
620
+ .leftJoin(blockersCTE, 'vt.id', 'blockers.blocked_task_id')
621
+ .leftJoin(blockingCTE, 'vt.id', 'blocking.blocker_task_id')
622
+ .select('vt.*', knex.raw('COALESCE(blockers.blocked_by_count, 0) as blocked_by_count'), knex.raw('COALESCE(blocking.blocking_count, 0) as blocking_count'));
500
623
  }
501
624
  else {
502
625
  // Standard query without dependency counts
503
- query = 'SELECT * FROM v_task_board WHERE 1=1';
626
+ query = knex('v_task_board');
504
627
  }
505
- const queryParams = [];
506
628
  // Filter by status
507
629
  if (params.status) {
508
630
  if (!STATUS_TO_ID[params.status]) {
509
631
  throw new Error(`Invalid status: ${params.status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
510
632
  }
511
- query += params.include_dependency_counts ? ' AND vt.status = ?' : ' AND status = ?';
512
- queryParams.push(params.status);
633
+ query = query.where(params.include_dependency_counts ? 'vt.status' : 'status', params.status);
513
634
  }
514
635
  // Filter by assigned agent
515
636
  if (params.assigned_agent) {
516
- query += params.include_dependency_counts ? ' AND vt.assigned_to = ?' : ' AND assigned_to = ?';
517
- queryParams.push(params.assigned_agent);
637
+ query = query.where(params.include_dependency_counts ? 'vt.assigned_to' : 'assigned_to', params.assigned_agent);
518
638
  }
519
639
  // Filter by layer
520
640
  if (params.layer) {
521
- query += params.include_dependency_counts ? ' AND vt.layer = ?' : ' AND layer = ?';
522
- queryParams.push(params.layer);
641
+ query = query.where(params.include_dependency_counts ? 'vt.layer' : 'layer', params.layer);
523
642
  }
524
643
  // Filter by tags
525
644
  if (params.tags && params.tags.length > 0) {
526
645
  for (const tag of params.tags) {
527
- query += params.include_dependency_counts ? ' AND vt.tags LIKE ?' : ' AND tags LIKE ?';
528
- queryParams.push(`%${tag}%`);
646
+ query = query.where(params.include_dependency_counts ? 'vt.tags' : 'tags', 'like', `%${tag}%`);
529
647
  }
530
648
  }
531
649
  // Order by updated timestamp (most recent first)
532
- query += params.include_dependency_counts ? ' ORDER BY vt.updated_ts DESC' : ' ORDER BY updated_ts DESC';
650
+ query = query.orderBy(params.include_dependency_counts ? 'vt.updated_ts' : 'updated_ts', 'desc');
533
651
  // Pagination
534
652
  const limit = params.limit !== undefined ? params.limit : 50;
535
653
  const offset = params.offset || 0;
536
654
  validateRange(limit, 'Parameter "limit"', 0, 100);
537
655
  validateRange(offset, 'Parameter "offset"', 0, Number.MAX_SAFE_INTEGER);
538
- query += ' LIMIT ? OFFSET ?';
539
- queryParams.push(limit, offset);
656
+ query = query.limit(limit).offset(offset);
540
657
  // Execute query
541
- const stmt = db.prepare(query);
542
- const rows = stmt.all(...queryParams);
658
+ const rows = await query;
543
659
  return {
544
660
  tasks: rows,
545
661
  count: rows.length,
546
- stale_tasks_transitioned: transitionCount
662
+ stale_tasks_transitioned: transitionCount,
663
+ git_auto_completed: gitCompletedCount,
664
+ git_archived: gitArchivedCount,
665
+ archived_tasks: archiveCount
547
666
  };
548
667
  }
549
668
  catch (error) {
@@ -554,8 +673,9 @@ export function listTasks(params = {}) {
554
673
  /**
555
674
  * Move task to different status
556
675
  */
557
- export function moveTask(params) {
558
- const db = getDatabase();
676
+ export async function moveTask(params, adapter) {
677
+ const actualAdapter = adapter ?? getAdapter();
678
+ const knex = actualAdapter.getKnex();
559
679
  if (!params.task_id) {
560
680
  throw new Error('Parameter "task_id" is required');
561
681
  }
@@ -563,11 +683,15 @@ export function moveTask(params) {
563
683
  throw new Error('Parameter "new_status" is required');
564
684
  }
565
685
  try {
566
- // Run auto-stale detection before move
567
- detectAndTransitionStaleTasks(db);
568
- return transaction(db, () => {
686
+ // Run auto-stale detection and auto-archive before move
687
+ await detectAndTransitionStaleTasks(actualAdapter);
688
+ await autoArchiveOldDoneTasks(actualAdapter);
689
+ return await actualAdapter.transaction(async (trx) => {
569
690
  // Get current status
570
- const taskRow = db.prepare('SELECT status_id FROM t_tasks WHERE id = ?').get(params.task_id);
691
+ const taskRow = await trx('t_tasks')
692
+ .where({ id: params.task_id })
693
+ .select('status_id')
694
+ .first();
571
695
  if (!taskRow) {
572
696
  throw new Error(`Task with id ${params.task_id} not found`);
573
697
  }
@@ -583,13 +707,26 @@ export function moveTask(params) {
583
707
  `Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
584
708
  }
585
709
  // Update status
586
- const updateStmt = db.prepare(`
587
- UPDATE t_tasks
588
- SET status_id = ?,
589
- completed_ts = CASE WHEN ? = 5 THEN unixepoch() ELSE completed_ts END
590
- WHERE id = ?
591
- `);
592
- updateStmt.run(newStatusId, newStatusId, params.task_id);
710
+ const updateData = {
711
+ status_id: newStatusId
712
+ };
713
+ // Set completed_ts when moving to done
714
+ if (newStatusId === TASK_STATUS.DONE) {
715
+ updateData.completed_ts = Math.floor(Date.now() / 1000);
716
+ }
717
+ await trx('t_tasks')
718
+ .where({ id: params.task_id })
719
+ .update(updateData);
720
+ // Activity logging (replaces trigger)
721
+ // Note: Using system agent (id=1) for status changes
722
+ // In a real implementation, you'd pass the actual agent_id who made the change
723
+ const systemAgentId = 1;
724
+ await logTaskStatusChange(knex, {
725
+ task_id: params.task_id,
726
+ old_status: currentStatusId,
727
+ new_status: newStatusId,
728
+ agent_id: systemAgentId
729
+ });
593
730
  // Update watcher if moving to done or archived (stop watching)
594
731
  if (params.new_status === 'done' || params.new_status === 'archived') {
595
732
  try {
@@ -617,8 +754,9 @@ export function moveTask(params) {
617
754
  /**
618
755
  * Link task to decision/constraint/file
619
756
  */
620
- export function linkTask(params) {
621
- const db = getDatabase();
757
+ export async function linkTask(params, adapter) {
758
+ const actualAdapter = adapter ?? getAdapter();
759
+ const knex = actualAdapter.getKnex();
622
760
  if (!params.task_id) {
623
761
  throw new Error('Parameter "task_id" is required');
624
762
  }
@@ -629,21 +767,21 @@ export function linkTask(params) {
629
767
  throw new Error('Parameter "target_id" is required');
630
768
  }
631
769
  try {
632
- return transaction(db, () => {
770
+ return await actualAdapter.transaction(async (trx) => {
633
771
  // Check if task exists
634
- const taskExists = db.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
772
+ const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
635
773
  if (!taskExists) {
636
774
  throw new Error(`Task with id ${params.task_id} not found`);
637
775
  }
638
776
  if (params.link_type === 'decision') {
639
777
  const decisionKey = String(params.target_id);
640
- const keyId = getOrCreateContextKey(db, decisionKey);
778
+ const keyId = await getOrCreateContextKey(actualAdapter, decisionKey, trx);
641
779
  const linkRelation = params.link_relation || 'implements';
642
- const stmt = db.prepare(`
643
- INSERT OR REPLACE INTO t_task_decision_links (task_id, decision_key_id, link_type)
644
- VALUES (?, ?, ?)
645
- `);
646
- stmt.run(params.task_id, keyId, linkRelation);
780
+ await knex('t_task_decision_links').insert({
781
+ task_id: params.task_id,
782
+ decision_key_id: keyId,
783
+ link_type: linkRelation
784
+ }).onConflict(['task_id', 'decision_key_id']).merge();
647
785
  return {
648
786
  success: true,
649
787
  task_id: params.task_id,
@@ -656,15 +794,14 @@ export function linkTask(params) {
656
794
  else if (params.link_type === 'constraint') {
657
795
  const constraintId = Number(params.target_id);
658
796
  // Check if constraint exists
659
- const constraintExists = db.prepare('SELECT id FROM t_constraints WHERE id = ?').get(constraintId);
797
+ const constraintExists = await trx('t_constraints').where({ id: constraintId }).first();
660
798
  if (!constraintExists) {
661
799
  throw new Error(`Constraint with id ${constraintId} not found`);
662
800
  }
663
- const stmt = db.prepare(`
664
- INSERT OR IGNORE INTO t_task_constraint_links (task_id, constraint_id)
665
- VALUES (?, ?)
666
- `);
667
- stmt.run(params.task_id, constraintId);
801
+ await trx('t_task_constraint_links').insert({
802
+ task_id: params.task_id,
803
+ constraint_id: constraintId
804
+ }).onConflict(['task_id', 'constraint_id']).ignore();
668
805
  return {
669
806
  success: true,
670
807
  task_id: params.task_id,
@@ -674,21 +811,23 @@ export function linkTask(params) {
674
811
  };
675
812
  }
676
813
  else if (params.link_type === 'file') {
814
+ // Deprecation warning (v3.4.1)
815
+ console.warn(`⚠️ DEPRECATION WARNING: task.link(link_type="file") is deprecated as of v3.4.1.`);
816
+ console.warn(` Use task.create(watch_files=[...]) or task.update(watch_files=[...]) instead.`);
817
+ console.warn(` Or use the new watch_files action: { action: "watch_files", task_id: ${params.task_id}, file_paths: ["..."] }`);
677
818
  const filePath = String(params.target_id);
678
- const fileId = getOrCreateFile(db, filePath);
679
- const stmt = db.prepare(`
680
- INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
681
- VALUES (?, ?)
682
- `);
683
- stmt.run(params.task_id, fileId);
819
+ const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
820
+ await trx('t_task_file_links').insert({
821
+ task_id: params.task_id,
822
+ file_id: fileId
823
+ }).onConflict(['task_id', 'file_id']).ignore();
684
824
  // Register file with watcher for auto-tracking
685
825
  try {
686
- const taskData = db.prepare(`
687
- SELECT t.title, s.name as status
688
- FROM t_tasks t
689
- JOIN m_task_statuses s ON t.status_id = s.id
690
- WHERE t.id = ?
691
- `).get(params.task_id);
826
+ const taskData = await trx('t_tasks as t')
827
+ .join('m_task_statuses as s', 't.status_id', 's.id')
828
+ .where('t.id', params.task_id)
829
+ .select('t.title', 's.name as status')
830
+ .first();
692
831
  if (taskData) {
693
832
  const watcher = FileWatcher.getInstance();
694
833
  watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
@@ -703,7 +842,8 @@ export function linkTask(params) {
703
842
  task_id: params.task_id,
704
843
  linked_to: 'file',
705
844
  target: filePath,
706
- message: `Task ${params.task_id} linked to file "${filePath}"`
845
+ deprecation_warning: 'task.link(link_type="file") is deprecated. Use task.create/update(watch_files) or watch_files action instead.',
846
+ message: `Task ${params.task_id} linked to file "${filePath}" (DEPRECATED API - use watch_files instead)`
707
847
  };
708
848
  }
709
849
  else {
@@ -719,15 +859,19 @@ export function linkTask(params) {
719
859
  /**
720
860
  * Archive completed task
721
861
  */
722
- export function archiveTask(params) {
723
- const db = getDatabase();
862
+ export async function archiveTask(params, adapter) {
863
+ const actualAdapter = adapter ?? getAdapter();
864
+ const knex = actualAdapter.getKnex();
724
865
  if (!params.task_id) {
725
866
  throw new Error('Parameter "task_id" is required');
726
867
  }
727
868
  try {
728
- return transaction(db, () => {
869
+ return await actualAdapter.transaction(async (trx) => {
729
870
  // Check if task is in 'done' status
730
- const taskRow = db.prepare('SELECT status_id FROM t_tasks WHERE id = ?').get(params.task_id);
871
+ const taskRow = await trx('t_tasks')
872
+ .where({ id: params.task_id })
873
+ .select('status_id')
874
+ .first();
731
875
  if (!taskRow) {
732
876
  throw new Error(`Task with id ${params.task_id} not found`);
733
877
  }
@@ -735,8 +879,18 @@ export function archiveTask(params) {
735
879
  throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
736
880
  }
737
881
  // Update to archived
738
- const updateStmt = db.prepare('UPDATE t_tasks SET status_id = ? WHERE id = ?');
739
- updateStmt.run(TASK_STATUS.ARCHIVED, params.task_id);
882
+ await trx('t_tasks')
883
+ .where({ id: params.task_id })
884
+ .update({ status_id: TASK_STATUS.ARCHIVED });
885
+ // Activity logging
886
+ // Note: Using system agent (id=1) for status changes
887
+ const systemAgentId = 1;
888
+ await logTaskStatusChange(knex, {
889
+ task_id: params.task_id,
890
+ old_status: TASK_STATUS.DONE,
891
+ new_status: TASK_STATUS.ARCHIVED,
892
+ agent_id: systemAgentId
893
+ });
740
894
  // Unregister from file watcher (archived tasks don't need tracking)
741
895
  try {
742
896
  const watcher = FileWatcher.getInstance();
@@ -760,8 +914,9 @@ export function archiveTask(params) {
760
914
  /**
761
915
  * Add dependency (blocking relationship) between tasks
762
916
  */
763
- export function addDependency(params) {
764
- const db = getDatabase();
917
+ export async function addDependency(params, adapter) {
918
+ const actualAdapter = adapter ?? getAdapter();
919
+ const knex = actualAdapter.getKnex();
765
920
  if (!params.blocker_task_id) {
766
921
  throw new Error('Parameter "blocker_task_id" is required');
767
922
  }
@@ -769,14 +924,20 @@ export function addDependency(params) {
769
924
  throw new Error('Parameter "blocked_task_id" is required');
770
925
  }
771
926
  try {
772
- return transaction(db, () => {
927
+ return await actualAdapter.transaction(async (trx) => {
773
928
  // Validation 1: No self-dependencies
774
929
  if (params.blocker_task_id === params.blocked_task_id) {
775
930
  throw new Error('Self-dependency not allowed');
776
931
  }
777
932
  // 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);
933
+ const blockerTask = await trx('t_tasks')
934
+ .where({ id: params.blocker_task_id })
935
+ .select('id', 'status_id')
936
+ .first();
937
+ const blockedTask = await trx('t_tasks')
938
+ .where({ id: params.blocked_task_id })
939
+ .select('id', 'status_id')
940
+ .first();
780
941
  if (!blockerTask) {
781
942
  throw new Error(`Blocker task #${params.blocker_task_id} not found`);
782
943
  }
@@ -791,15 +952,17 @@ export function addDependency(params) {
791
952
  throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
792
953
  }
793
954
  // Validation 4: No direct circular (reverse relationship)
794
- const reverseExists = db.prepare(`
795
- SELECT 1 FROM t_task_dependencies
796
- WHERE blocker_task_id = ? AND blocked_task_id = ?
797
- `).get(params.blocked_task_id, params.blocker_task_id);
955
+ const reverseExists = await trx('t_task_dependencies')
956
+ .where({
957
+ blocker_task_id: params.blocked_task_id,
958
+ blocked_task_id: params.blocker_task_id
959
+ })
960
+ .first();
798
961
  if (reverseExists) {
799
962
  throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
800
963
  }
801
964
  // Validation 5: No transitive circular (check if adding this would create a cycle)
802
- const cycleCheck = db.prepare(`
965
+ const cycleCheck = await trx.raw(`
803
966
  WITH RECURSIVE dependency_chain AS (
804
967
  -- Start from the task that would be blocked
805
968
  SELECT blocked_task_id as task_id, 1 as depth
@@ -815,10 +978,11 @@ export function addDependency(params) {
815
978
  WHERE dc.depth < 100
816
979
  )
817
980
  SELECT task_id FROM dependency_chain WHERE task_id = ?
818
- `).get(params.blocked_task_id, params.blocker_task_id);
981
+ `, [params.blocked_task_id, params.blocker_task_id])
982
+ .then((result) => result[0]);
819
983
  if (cycleCheck) {
820
984
  // Build cycle path for error message
821
- const cyclePathResult = db.prepare(`
985
+ const cyclePathResult = await trx.raw(`
822
986
  WITH RECURSIVE dependency_chain AS (
823
987
  SELECT blocked_task_id as task_id, 1 as depth,
824
988
  CAST(blocked_task_id AS TEXT) as path
@@ -834,16 +998,17 @@ export function addDependency(params) {
834
998
  WHERE dc.depth < 100
835
999
  )
836
1000
  SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
837
- `).get(params.blocked_task_id, params.blocker_task_id);
1001
+ `, [params.blocked_task_id, params.blocker_task_id])
1002
+ .then((result) => result[0]);
838
1003
  const cyclePath = cyclePathResult?.path || `#${params.blocked_task_id} → ... → #${params.blocker_task_id}`;
839
1004
  throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
840
1005
  }
841
1006
  // All validations passed - insert dependency
842
- const insertStmt = db.prepare(`
843
- INSERT INTO t_task_dependencies (blocker_task_id, blocked_task_id)
844
- VALUES (?, ?)
845
- `);
846
- insertStmt.run(params.blocker_task_id, params.blocked_task_id);
1007
+ await trx('t_task_dependencies').insert({
1008
+ blocker_task_id: params.blocker_task_id,
1009
+ blocked_task_id: params.blocked_task_id,
1010
+ created_ts: Math.floor(Date.now() / 1000)
1011
+ });
847
1012
  return {
848
1013
  success: true,
849
1014
  message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
@@ -862,8 +1027,9 @@ export function addDependency(params) {
862
1027
  /**
863
1028
  * Remove dependency between tasks
864
1029
  */
865
- export function removeDependency(params) {
866
- const db = getDatabase();
1030
+ export async function removeDependency(params, adapter) {
1031
+ const actualAdapter = adapter ?? getAdapter();
1032
+ const knex = actualAdapter.getKnex();
867
1033
  if (!params.blocker_task_id) {
868
1034
  throw new Error('Parameter "blocker_task_id" is required');
869
1035
  }
@@ -871,11 +1037,12 @@ export function removeDependency(params) {
871
1037
  throw new Error('Parameter "blocked_task_id" is required');
872
1038
  }
873
1039
  try {
874
- const deleteStmt = db.prepare(`
875
- DELETE FROM t_task_dependencies
876
- WHERE blocker_task_id = ? AND blocked_task_id = ?
877
- `);
878
- deleteStmt.run(params.blocker_task_id, params.blocked_task_id);
1040
+ await knex('t_task_dependencies')
1041
+ .where({
1042
+ blocker_task_id: params.blocker_task_id,
1043
+ blocked_task_id: params.blocked_task_id
1044
+ })
1045
+ .delete();
879
1046
  return {
880
1047
  success: true,
881
1048
  message: `Dependency removed: Task #${params.blocker_task_id} no longer blocks Task #${params.blocked_task_id}`
@@ -889,20 +1056,21 @@ export function removeDependency(params) {
889
1056
  /**
890
1057
  * Get dependencies for a task (bidirectional: what blocks this task, what this task blocks)
891
1058
  */
892
- export function getDependencies(params) {
893
- const db = getDatabase();
1059
+ export async function getDependencies(params, adapter) {
1060
+ const actualAdapter = adapter ?? getAdapter();
1061
+ const knex = actualAdapter.getKnex();
894
1062
  if (!params.task_id) {
895
1063
  throw new Error('Parameter "task_id" is required');
896
1064
  }
897
1065
  const includeDetails = params.include_details || false;
898
1066
  try {
899
1067
  // Check if task exists
900
- const taskExists = db.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
1068
+ const taskExists = await knex('t_tasks').where({ id: params.task_id }).first();
901
1069
  if (!taskExists) {
902
1070
  throw new Error(`Task with id ${params.task_id} not found`);
903
1071
  }
904
1072
  // Use the shared helper function
905
- const deps = queryTaskDependencies(db, params.task_id, includeDetails);
1073
+ const deps = await queryTaskDependencies(actualAdapter, params.task_id, includeDetails);
906
1074
  return {
907
1075
  task_id: params.task_id,
908
1076
  blockers: deps.blockers,
@@ -921,32 +1089,296 @@ export function getDependencies(params) {
921
1089
  /**
922
1090
  * Create multiple tasks atomically
923
1091
  */
924
- export function batchCreateTasks(params) {
925
- const db = getDatabase();
1092
+ export async function batchCreateTasks(params, adapter) {
1093
+ const actualAdapter = adapter ?? getAdapter();
926
1094
  if (!params.tasks || !Array.isArray(params.tasks)) {
927
1095
  throw new Error('Parameter "tasks" is required and must be an array');
928
1096
  }
1097
+ if (params.tasks.length > 50) {
1098
+ throw new Error('Parameter "tasks" must contain at most 50 items');
1099
+ }
929
1100
  const atomic = params.atomic !== undefined ? params.atomic : true;
930
- // Use processBatch utility
931
- const batchResult = processBatch(db, params.tasks, (task, db) => {
932
- const result = createTaskInternal(task, db);
1101
+ try {
1102
+ if (atomic) {
1103
+ // Atomic mode: All or nothing
1104
+ const results = await actualAdapter.transaction(async (trx) => {
1105
+ const processedResults = [];
1106
+ for (const task of params.tasks) {
1107
+ try {
1108
+ const result = await createTaskInternal(task, actualAdapter, trx);
1109
+ processedResults.push({
1110
+ title: task.title,
1111
+ task_id: result.task_id,
1112
+ success: true,
1113
+ error: undefined
1114
+ });
1115
+ }
1116
+ catch (error) {
1117
+ const errorMessage = error instanceof Error ? error.message : String(error);
1118
+ throw new Error(`Batch failed at task "${task.title}": ${errorMessage}`);
1119
+ }
1120
+ }
1121
+ return processedResults;
1122
+ });
1123
+ return {
1124
+ success: true,
1125
+ created: results.length,
1126
+ failed: 0,
1127
+ results: results
1128
+ };
1129
+ }
1130
+ else {
1131
+ // Non-atomic mode: Process each independently
1132
+ const results = [];
1133
+ let created = 0;
1134
+ let failed = 0;
1135
+ for (const task of params.tasks) {
1136
+ try {
1137
+ const result = await actualAdapter.transaction(async (trx) => {
1138
+ return await createTaskInternal(task, actualAdapter, trx);
1139
+ });
1140
+ results.push({
1141
+ title: task.title,
1142
+ task_id: result.task_id,
1143
+ success: true,
1144
+ error: undefined
1145
+ });
1146
+ created++;
1147
+ }
1148
+ catch (error) {
1149
+ const errorMessage = error instanceof Error ? error.message : String(error);
1150
+ results.push({
1151
+ title: task.title,
1152
+ task_id: undefined,
1153
+ success: false,
1154
+ error: errorMessage
1155
+ });
1156
+ failed++;
1157
+ }
1158
+ }
1159
+ return {
1160
+ success: failed === 0,
1161
+ created: created,
1162
+ failed: failed,
1163
+ results: results
1164
+ };
1165
+ }
1166
+ }
1167
+ catch (error) {
1168
+ const message = error instanceof Error ? error.message : String(error);
1169
+ throw new Error(`Failed to execute batch operation: ${message}`);
1170
+ }
1171
+ }
1172
+ /**
1173
+ * Watch/unwatch files for a task (v3.4.1)
1174
+ * Replaces the need to use task.link(file) for file watching
1175
+ */
1176
+ export async function watchFiles(params, adapter) {
1177
+ const actualAdapter = adapter ?? getAdapter();
1178
+ const knex = actualAdapter.getKnex();
1179
+ if (!params.task_id) {
1180
+ throw new Error('Parameter "task_id" is required');
1181
+ }
1182
+ if (!params.action) {
1183
+ throw new Error('Parameter "action" is required (watch, unwatch, or list)');
1184
+ }
1185
+ try {
1186
+ return await actualAdapter.transaction(async (trx) => {
1187
+ // Check if task exists
1188
+ const taskData = await trx('t_tasks as t')
1189
+ .join('m_task_statuses as s', 't.status_id', 's.id')
1190
+ .where('t.id', params.task_id)
1191
+ .select('t.id', 't.title', 's.name as status')
1192
+ .first();
1193
+ if (!taskData) {
1194
+ throw new Error(`Task with id ${params.task_id} not found`);
1195
+ }
1196
+ if (params.action === 'watch') {
1197
+ if (!params.file_paths || params.file_paths.length === 0) {
1198
+ throw new Error('Parameter "file_paths" is required for watch action');
1199
+ }
1200
+ const addedFiles = [];
1201
+ for (const filePath of params.file_paths) {
1202
+ const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
1203
+ // Check if already exists
1204
+ const existing = await trx('t_task_file_links')
1205
+ .where({ task_id: params.task_id, file_id: fileId })
1206
+ .first();
1207
+ if (!existing) {
1208
+ await trx('t_task_file_links').insert({
1209
+ task_id: params.task_id,
1210
+ file_id: fileId
1211
+ });
1212
+ addedFiles.push(filePath);
1213
+ }
1214
+ }
1215
+ // Register files with watcher
1216
+ try {
1217
+ const watcher = FileWatcher.getInstance();
1218
+ for (const filePath of addedFiles) {
1219
+ watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
1220
+ }
1221
+ }
1222
+ catch (error) {
1223
+ // Watcher may not be initialized yet, ignore
1224
+ console.error('Warning: Could not register files with watcher:', error);
1225
+ }
1226
+ return {
1227
+ success: true,
1228
+ task_id: params.task_id,
1229
+ action: 'watch',
1230
+ files_added: addedFiles.length,
1231
+ files: addedFiles,
1232
+ message: `Watching ${addedFiles.length} file(s) for task ${params.task_id}`
1233
+ };
1234
+ }
1235
+ else if (params.action === 'unwatch') {
1236
+ if (!params.file_paths || params.file_paths.length === 0) {
1237
+ throw new Error('Parameter "file_paths" is required for unwatch action');
1238
+ }
1239
+ const removedFiles = [];
1240
+ for (const filePath of params.file_paths) {
1241
+ const deleted = await trx('t_task_file_links')
1242
+ .where('task_id', params.task_id)
1243
+ .whereIn('file_id', function () {
1244
+ this.select('id').from('m_files').where({ path: filePath });
1245
+ })
1246
+ .delete();
1247
+ if (deleted > 0) {
1248
+ removedFiles.push(filePath);
1249
+ }
1250
+ }
1251
+ return {
1252
+ success: true,
1253
+ task_id: params.task_id,
1254
+ action: 'unwatch',
1255
+ files_removed: removedFiles.length,
1256
+ files: removedFiles,
1257
+ message: `Stopped watching ${removedFiles.length} file(s) for task ${params.task_id}`
1258
+ };
1259
+ }
1260
+ else if (params.action === 'list') {
1261
+ const files = await trx('t_task_file_links as tfl')
1262
+ .join('m_files as f', 'tfl.file_id', 'f.id')
1263
+ .where('tfl.task_id', params.task_id)
1264
+ .select('f.path')
1265
+ .then(rows => rows.map((row) => row.path));
1266
+ return {
1267
+ success: true,
1268
+ task_id: params.task_id,
1269
+ action: 'list',
1270
+ files_count: files.length,
1271
+ files: files,
1272
+ message: `Task ${params.task_id} is watching ${files.length} file(s)`
1273
+ };
1274
+ }
1275
+ else {
1276
+ throw new Error(`Invalid action: ${params.action}. Must be one of: watch, unwatch, list`);
1277
+ }
1278
+ });
1279
+ }
1280
+ catch (error) {
1281
+ const message = error instanceof Error ? error.message : String(error);
1282
+ throw new Error(`Failed to ${params.action} files: ${message}`);
1283
+ }
1284
+ }
1285
+ /**
1286
+ * Get pruned files for a task (v3.5.0 Auto-Pruning)
1287
+ * Returns audit trail of files that were auto-pruned as non-existent
1288
+ */
1289
+ export async function getPrunedFiles(params, adapter) {
1290
+ const actualAdapter = adapter ?? getAdapter();
1291
+ const knex = actualAdapter.getKnex();
1292
+ try {
1293
+ // Validate task_id
1294
+ if (!params.task_id || typeof params.task_id !== 'number') {
1295
+ throw new Error('task_id is required and must be a number');
1296
+ }
1297
+ // Validate task exists
1298
+ const task = await knex('t_tasks').where({ id: params.task_id }).first();
1299
+ if (!task) {
1300
+ throw new Error(`Task not found: ${params.task_id}`);
1301
+ }
1302
+ // Get pruned files
1303
+ const limit = params.limit || 100;
1304
+ const rows = await knex('t_task_pruned_files as tpf')
1305
+ .leftJoin('m_context_keys as k', 'tpf.linked_decision_key_id', 'k.id')
1306
+ .where('tpf.task_id', params.task_id)
1307
+ .select('tpf.id', 'tpf.file_path', knex.raw(`datetime(tpf.pruned_ts, 'unixepoch') as pruned_at`), 'k.key as linked_decision')
1308
+ .orderBy('tpf.pruned_ts', 'desc')
1309
+ .limit(limit);
933
1310
  return {
934
- title: task.title,
935
- task_id: result.task_id
1311
+ success: true,
1312
+ task_id: params.task_id,
1313
+ pruned_files: rows,
1314
+ count: rows.length,
1315
+ message: rows.length > 0
1316
+ ? `Found ${rows.length} pruned file(s) for task ${params.task_id}`
1317
+ : `No pruned files for task ${params.task_id}`
936
1318
  };
937
- }, atomic, 50);
938
- // Map batch results to task batch response format
939
- return {
940
- success: batchResult.success,
941
- created: batchResult.processed,
942
- failed: batchResult.failed,
943
- results: batchResult.results.map(r => ({
944
- title: r.data?.title || '',
945
- task_id: r.data?.task_id,
946
- success: r.success,
947
- error: r.error
948
- }))
949
- };
1319
+ }
1320
+ catch (error) {
1321
+ const message = error instanceof Error ? error.message : String(error);
1322
+ throw new Error(`Failed to get pruned files: ${message}`);
1323
+ }
1324
+ }
1325
+ /**
1326
+ * Link a pruned file to a decision (v3.5.0 Auto-Pruning)
1327
+ * Attaches WHY reasoning to pruned files for project archaeology
1328
+ */
1329
+ export async function linkPrunedFile(params, adapter) {
1330
+ const actualAdapter = adapter ?? getAdapter();
1331
+ const knex = actualAdapter.getKnex();
1332
+ try {
1333
+ // Validate pruned_file_id
1334
+ if (!params.pruned_file_id || typeof params.pruned_file_id !== 'number') {
1335
+ throw new Error('pruned_file_id is required and must be a number');
1336
+ }
1337
+ // Validate decision_key
1338
+ if (!params.decision_key || typeof params.decision_key !== 'string') {
1339
+ throw new Error('decision_key is required and must be a string');
1340
+ }
1341
+ // Get decision key_id
1342
+ const decision = await knex('m_context_keys as k')
1343
+ .whereExists(function () {
1344
+ this.select('*')
1345
+ .from('t_decisions as d')
1346
+ .whereRaw('d.key_id = k.id');
1347
+ })
1348
+ .where('k.key', params.decision_key)
1349
+ .select('k.id as key_id')
1350
+ .first();
1351
+ if (!decision) {
1352
+ throw new Error(`Decision not found: ${params.decision_key}`);
1353
+ }
1354
+ // Check if pruned file exists
1355
+ const prunedFile = await knex('t_task_pruned_files')
1356
+ .where({ id: params.pruned_file_id })
1357
+ .select('id', 'task_id', 'file_path')
1358
+ .first();
1359
+ if (!prunedFile) {
1360
+ throw new Error(`Pruned file record not found: ${params.pruned_file_id}`);
1361
+ }
1362
+ // Update the link
1363
+ const updated = await knex('t_task_pruned_files')
1364
+ .where({ id: params.pruned_file_id })
1365
+ .update({ linked_decision_key_id: decision.key_id });
1366
+ if (updated === 0) {
1367
+ throw new Error(`Failed to link pruned file #${params.pruned_file_id} to decision ${params.decision_key}`);
1368
+ }
1369
+ return {
1370
+ success: true,
1371
+ pruned_file_id: params.pruned_file_id,
1372
+ decision_key: params.decision_key,
1373
+ task_id: prunedFile.task_id,
1374
+ file_path: prunedFile.file_path,
1375
+ message: `Linked pruned file "${prunedFile.file_path}" to decision "${params.decision_key}"`
1376
+ };
1377
+ }
1378
+ catch (error) {
1379
+ const message = error instanceof Error ? error.message : String(error);
1380
+ throw new Error(`Failed to link pruned file: ${message}`);
1381
+ }
950
1382
  }
951
1383
  /**
952
1384
  * Return comprehensive help documentation
@@ -961,7 +1393,8 @@ export function taskHelp() {
961
1393
  create: {
962
1394
  description: 'Create a new task',
963
1395
  required_params: ['title'],
964
- optional_params: ['description', 'acceptance_criteria', 'notes', 'priority', 'assigned_agent', 'created_by_agent', 'layer', 'tags', 'status'],
1396
+ optional_params: ['description', 'acceptance_criteria', 'notes', 'priority', 'assigned_agent', 'created_by_agent', 'layer', 'tags', 'status', 'watch_files'],
1397
+ watch_files_param: '⭐ NEW in v3.4.1: Pass watch_files array to automatically link and watch files (replaces task.link(file))',
965
1398
  example: {
966
1399
  action: 'create',
967
1400
  title: 'Implement authentication endpoint',
@@ -969,18 +1402,21 @@ export function taskHelp() {
969
1402
  priority: 3,
970
1403
  assigned_agent: 'backend-agent',
971
1404
  layer: 'presentation',
972
- tags: ['api', 'authentication']
1405
+ tags: ['api', 'authentication'],
1406
+ watch_files: ['src/api/auth.ts', 'src/middleware/jwt.ts']
973
1407
  }
974
1408
  },
975
1409
  update: {
976
1410
  description: 'Update task metadata',
977
1411
  required_params: ['task_id'],
978
- optional_params: ['title', 'priority', 'assigned_agent', 'layer', 'description', 'acceptance_criteria', 'notes'],
1412
+ optional_params: ['title', 'priority', 'assigned_agent', 'layer', 'description', 'acceptance_criteria', 'notes', 'watch_files'],
1413
+ watch_files_param: '⭐ NEW in v3.4.1: Pass watch_files array to add files to watch list',
979
1414
  example: {
980
1415
  action: 'update',
981
1416
  task_id: 5,
982
1417
  priority: 4,
983
- assigned_agent: 'senior-backend-agent'
1418
+ assigned_agent: 'senior-backend-agent',
1419
+ watch_files: ['src/api/users.ts']
984
1420
  }
985
1421
  },
986
1422
  get: {
@@ -1025,7 +1461,8 @@ export function taskHelp() {
1025
1461
  required_params: ['task_id', 'link_type', 'target_id'],
1026
1462
  optional_params: ['link_relation'],
1027
1463
  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.',
1464
+ file_linking_behavior: '⚠️ DEPRECATED in v3.4.1: link_type="file" is deprecated. Use watch_files action or watch_files parameter instead.',
1465
+ deprecation_note: 'For file watching, use: (1) watch_files parameter in create/update, or (2) watch_files action with watch/unwatch/list',
1029
1466
  example: {
1030
1467
  action: 'link',
1031
1468
  task_id: 5,
@@ -1034,6 +1471,34 @@ export function taskHelp() {
1034
1471
  link_relation: 'implements'
1035
1472
  }
1036
1473
  },
1474
+ watch_files: {
1475
+ description: '⭐ NEW in v3.4.1: Watch/unwatch files for a task (replaces task.link(file))',
1476
+ required_params: ['task_id', 'action'],
1477
+ optional_params: ['file_paths'],
1478
+ actions: ['watch', 'unwatch', 'list'],
1479
+ behavior: {
1480
+ watch: 'Add files to watch list and activate file monitoring',
1481
+ unwatch: 'Remove files from watch list',
1482
+ list: 'List all files currently watched by this task'
1483
+ },
1484
+ examples: {
1485
+ watch: {
1486
+ task_id: 5,
1487
+ action: 'watch',
1488
+ file_paths: ['src/api/auth.ts', 'src/middleware/jwt.ts']
1489
+ },
1490
+ unwatch: {
1491
+ task_id: 5,
1492
+ action: 'unwatch',
1493
+ file_paths: ['src/middleware/jwt.ts']
1494
+ },
1495
+ list: {
1496
+ task_id: 5,
1497
+ action: 'list'
1498
+ }
1499
+ },
1500
+ note: 'Preferred over task.link(file) for better clarity and batch operations'
1501
+ },
1037
1502
  archive: {
1038
1503
  description: 'Archive completed task (must be in done status)',
1039
1504
  required_params: ['task_id'],
@@ -1176,7 +1641,9 @@ export function taskHelp() {
1176
1641
  /**
1177
1642
  * Query file watcher status and monitored files/tasks
1178
1643
  */
1179
- export function watcherStatus(args) {
1644
+ export async function watcherStatus(args, adapter) {
1645
+ const actualAdapter = adapter ?? getAdapter();
1646
+ const knex = actualAdapter.getKnex();
1180
1647
  const subaction = args.subaction || 'status';
1181
1648
  const watcher = FileWatcher.getInstance();
1182
1649
  if (subaction === 'help') {
@@ -1219,15 +1686,13 @@ export function watcherStatus(args) {
1219
1686
  };
1220
1687
  }
1221
1688
  if (subaction === 'list_files') {
1222
- const db = getDatabase();
1223
- const fileLinks = db.prepare(`
1224
- SELECT DISTINCT tfl.file_path, t.id, t.title, ts.status_name
1225
- FROM t_task_file_links tfl
1226
- JOIN t_tasks t ON tfl.task_id = t.id
1227
- JOIN m_task_statuses ts ON t.status_id = ts.id
1228
- WHERE t.status_id != 6 -- Exclude archived tasks
1229
- ORDER BY tfl.file_path, t.id
1230
- `).all();
1689
+ const fileLinks = await knex('t_task_file_links as tfl')
1690
+ .join('t_tasks as t', 'tfl.task_id', 't.id')
1691
+ .join('m_task_statuses as ts', 't.status_id', 'ts.id')
1692
+ .join('m_files as f', 'tfl.file_id', 'f.id')
1693
+ .where('t.status_id', '!=', 6) // Exclude archived tasks
1694
+ .select('f.path as file_path', 't.id', 't.title', 'ts.name as status_name')
1695
+ .orderBy(['f.path', 't.id']);
1231
1696
  // Group by file
1232
1697
  const fileMap = new Map();
1233
1698
  for (const link of fileLinks) {
@@ -1254,17 +1719,14 @@ export function watcherStatus(args) {
1254
1719
  };
1255
1720
  }
1256
1721
  if (subaction === 'list_tasks') {
1257
- const db = getDatabase();
1258
- const taskLinks = db.prepare(`
1259
- SELECT t.id, t.title, ts.status_name, COUNT(DISTINCT tfl.file_path) as file_count,
1260
- GROUP_CONCAT(DISTINCT tfl.file_path, ', ') as files
1261
- FROM t_tasks t
1262
- JOIN m_task_statuses ts ON t.status_id = ts.id
1263
- JOIN t_task_file_links tfl ON t.id = tfl.task_id
1264
- WHERE t.status_id != 6 -- Exclude archived tasks
1265
- GROUP BY t.id, t.title, ts.status_name
1266
- ORDER BY t.id
1267
- `).all();
1722
+ const taskLinks = await knex('t_tasks as t')
1723
+ .join('m_task_statuses as ts', 't.status_id', 'ts.id')
1724
+ .join('t_task_file_links as tfl', 't.id', 'tfl.task_id')
1725
+ .join('m_files as f', 'tfl.file_id', 'f.id')
1726
+ .where('t.status_id', '!=', 6) // Exclude archived tasks
1727
+ .groupBy('t.id', 't.title', 'ts.name')
1728
+ .select('t.id', 't.title', 'ts.name as status_name', knex.raw('COUNT(DISTINCT tfl.file_id) as file_count'), knex.raw('GROUP_CONCAT(DISTINCT f.path, \', \') as files'))
1729
+ .orderBy('t.id');
1268
1730
  const tasks = taskLinks.map(task => ({
1269
1731
  task_id: task.id,
1270
1732
  task_title: task.title,