sqlew 3.5.3 → 3.6.1

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