sqlew 3.5.3 → 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 (227) hide show
  1. package/CHANGELOG.md +247 -1772
  2. package/README.md +70 -304
  3. package/assets/config.example.toml +97 -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 +9 -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 +44 -122
  37. package/dist/database.d.ts.map +1 -1
  38. package/dist/database.js +145 -416
  39. package/dist/database.js.map +1 -1
  40. package/dist/index.js +215 -185
  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/seed-help-data.d.ts +48 -0
  107. package/dist/migrations/seed-help-data.d.ts.map +1 -0
  108. package/dist/migrations/seed-help-data.js +1466 -0
  109. package/dist/migrations/seed-help-data.js.map +1 -0
  110. package/dist/migrations/seed-tool-metadata.d.ts +24 -0
  111. package/dist/migrations/seed-tool-metadata.d.ts.map +1 -0
  112. package/dist/migrations/seed-tool-metadata.js +392 -0
  113. package/dist/migrations/seed-tool-metadata.js.map +1 -0
  114. package/dist/migrations/v3.6.0-help-system-refactor.d.ts +46 -0
  115. package/dist/migrations/v3.6.0-help-system-refactor.d.ts.map +1 -0
  116. package/dist/migrations/v3.6.0-help-system-refactor.js +223 -0
  117. package/dist/migrations/v3.6.0-help-system-refactor.js.map +1 -0
  118. package/dist/schema.d.ts.map +1 -1
  119. package/dist/schema.js +2 -0
  120. package/dist/schema.js.map +1 -1
  121. package/dist/tests/git-aware-completion.test.js +89 -70
  122. package/dist/tests/git-aware-completion.test.js.map +1 -1
  123. package/dist/tests/help-system.test.d.ts +23 -0
  124. package/dist/tests/help-system.test.d.ts.map +1 -0
  125. package/dist/tests/help-system.test.js +374 -0
  126. package/dist/tests/help-system.test.js.map +1 -0
  127. package/dist/tests/tasks.auto-pruning-decision-link.test.js +92 -78
  128. package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -1
  129. package/dist/tests/tasks.auto-pruning-partial.test.js +106 -95
  130. package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -1
  131. package/dist/tests/tasks.auto-pruning-persistence.test.js +115 -97
  132. package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -1
  133. package/dist/tests/tasks.auto-pruning-safety.test.js +124 -103
  134. package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -1
  135. package/dist/tests/tasks.dependencies.test.js +338 -307
  136. package/dist/tests/tasks.dependencies.test.js.map +1 -1
  137. package/dist/tests/tasks.link-file-backward-compat.test.js +116 -104
  138. package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
  139. package/dist/tests/tasks.watch-files-action.test.js +122 -101
  140. package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
  141. package/dist/tests/tasks.watch-files-parameter.test.js +105 -94
  142. package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -1
  143. package/dist/tests/two-step-git-completion.test.js +176 -133
  144. package/dist/tests/two-step-git-completion.test.js.map +1 -1
  145. package/dist/tests/vcs-staging.test.js +1 -1
  146. package/dist/tests/vcs-staging.test.js.map +1 -1
  147. package/dist/tools/config.d.ts +9 -6
  148. package/dist/tools/config.d.ts.map +1 -1
  149. package/dist/tools/config.js +16 -14
  150. package/dist/tools/config.js.map +1 -1
  151. package/dist/tools/constraints.d.ts +10 -7
  152. package/dist/tools/constraints.d.ts.map +1 -1
  153. package/dist/tools/constraints.js +66 -48
  154. package/dist/tools/constraints.js.map +1 -1
  155. package/dist/tools/context.d.ts +36 -33
  156. package/dist/tools/context.d.ts.map +1 -1
  157. package/dist/tools/context.js +374 -330
  158. package/dist/tools/context.js.map +1 -1
  159. package/dist/tools/files.d.ts +12 -9
  160. package/dist/tools/files.d.ts.map +1 -1
  161. package/dist/tools/files.js +173 -95
  162. package/dist/tools/files.js.map +1 -1
  163. package/dist/tools/help-queries.d.ts +130 -0
  164. package/dist/tools/help-queries.d.ts.map +1 -0
  165. package/dist/tools/help-queries.js +393 -0
  166. package/dist/tools/help-queries.js.map +1 -0
  167. package/dist/tools/messaging.d.ts +14 -11
  168. package/dist/tools/messaging.d.ts.map +1 -1
  169. package/dist/tools/messaging.js +217 -133
  170. package/dist/tools/messaging.js.map +1 -1
  171. package/dist/tools/tasks.d.ts +18 -16
  172. package/dist/tools/tasks.d.ts.map +1 -1
  173. package/dist/tools/tasks.js +513 -439
  174. package/dist/tools/tasks.js.map +1 -1
  175. package/dist/tools/utils.d.ts +14 -11
  176. package/dist/tools/utils.d.ts.map +1 -1
  177. package/dist/tools/utils.js +86 -121
  178. package/dist/tools/utils.js.map +1 -1
  179. package/dist/utils/activity-logging.d.ts +114 -0
  180. package/dist/utils/activity-logging.d.ts.map +1 -0
  181. package/dist/utils/activity-logging.js +162 -0
  182. package/dist/utils/activity-logging.js.map +1 -0
  183. package/dist/utils/batch.d.ts +2 -2
  184. package/dist/utils/batch.d.ts.map +1 -1
  185. package/dist/utils/batch.js +8 -8
  186. package/dist/utils/batch.js.map +1 -1
  187. package/dist/utils/cleanup.d.ts +21 -13
  188. package/dist/utils/cleanup.d.ts.map +1 -1
  189. package/dist/utils/cleanup.js +31 -24
  190. package/dist/utils/cleanup.js.map +1 -1
  191. package/dist/utils/debug-logger.d.ts +44 -0
  192. package/dist/utils/debug-logger.d.ts.map +1 -0
  193. package/dist/utils/debug-logger.js +116 -0
  194. package/dist/utils/debug-logger.js.map +1 -0
  195. package/dist/utils/help-tracking.d.ts +55 -0
  196. package/dist/utils/help-tracking.d.ts.map +1 -0
  197. package/dist/utils/help-tracking.js +88 -0
  198. package/dist/utils/help-tracking.js.map +1 -0
  199. package/dist/utils/retention.d.ts +7 -7
  200. package/dist/utils/retention.d.ts.map +1 -1
  201. package/dist/utils/retention.js +12 -12
  202. package/dist/utils/retention.js.map +1 -1
  203. package/dist/utils/task-stale-detection.d.ts +15 -13
  204. package/dist/utils/task-stale-detection.d.ts.map +1 -1
  205. package/dist/utils/task-stale-detection.js +100 -302
  206. package/dist/utils/task-stale-detection.js.map +1 -1
  207. package/dist/utils/token-estimation.d.ts +72 -0
  208. package/dist/utils/token-estimation.d.ts.map +1 -0
  209. package/dist/utils/token-estimation.js +71 -0
  210. package/dist/utils/token-estimation.js.map +1 -0
  211. package/dist/utils/token-logging.d.ts +48 -0
  212. package/dist/utils/token-logging.d.ts.map +1 -0
  213. package/dist/utils/token-logging.js +112 -0
  214. package/dist/utils/token-logging.js.map +1 -0
  215. package/dist/utils/view-queries.d.ts +34 -0
  216. package/dist/utils/view-queries.d.ts.map +1 -0
  217. package/dist/utils/view-queries.js +192 -0
  218. package/dist/utils/view-queries.js.map +1 -0
  219. package/dist/watcher/file-watcher.d.ts.map +1 -1
  220. package/dist/watcher/file-watcher.js +25 -11
  221. package/dist/watcher/file-watcher.js.map +1 -1
  222. package/docs/BEST_PRACTICES.md +56 -448
  223. package/docs/MIGRATION_v3.6.0.md +170 -0
  224. package/docs/SHARED_CONCEPTS.md +63 -208
  225. package/docs/TASK_OVERVIEW.md +2 -2
  226. package/docs/TOOL_SELECTION.md +41 -248
  227. package/package.json +16 -4
@@ -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';
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';
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);
84
+ assignedAgentId = await getOrCreateAgent(adapter, params.assigned_agent, trx);
81
85
  }
82
86
  // Default to 'system' if no created_by_agent provided
83
- // This ensures the activity log trigger has a valid agent_id
87
+ // This ensures the activity log has a valid agent_id
84
88
  const createdBy = params.created_by_agent || 'system';
85
- const createdByAgentId = getOrCreateAgent(db, createdBy);
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,23 +136,65 @@ 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();
145
189
  }
146
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
+ });
147
198
  // Link files and register with watcher if watch_files provided (v3.4.1)
148
199
  if (params.watch_files && params.watch_files.length > 0) {
149
200
  // Parse watch_files - handle MCP SDK converting JSON string to char array
@@ -180,19 +231,18 @@ function createTaskInternal(params, db) {
180
231
  else {
181
232
  throw new Error('Parameter "watch_files" must be a string or array');
182
233
  }
183
- const insertFileLinkStmt = db.prepare(`
184
- INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
185
- VALUES (?, ?)
186
- `);
187
234
  for (const filePath of watchFilesParsed) {
188
- const fileId = getOrCreateFile(db, filePath);
189
- insertFileLinkStmt.run(taskId, fileId);
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();
190
240
  }
191
241
  // Register files with watcher for auto-tracking
192
242
  try {
193
243
  const watcher = FileWatcher.getInstance();
194
244
  for (const filePath of watchFilesParsed) {
195
- watcher.registerFile(filePath, taskId, params.title, status);
245
+ watcher.registerFile(filePath, Number(taskId), params.title, status);
196
246
  }
197
247
  }
198
248
  catch (error) {
@@ -202,7 +252,7 @@ function createTaskInternal(params, db) {
202
252
  }
203
253
  return {
204
254
  success: true,
205
- task_id: taskId,
255
+ task_id: Number(taskId),
206
256
  title: params.title,
207
257
  status: status,
208
258
  message: `Task "${params.title}" created successfully`
@@ -211,16 +261,16 @@ function createTaskInternal(params, db) {
211
261
  /**
212
262
  * Create a new task
213
263
  */
214
- export function createTask(params, db) {
215
- const actualDb = db ?? getDatabase();
264
+ export async function createTask(params, adapter) {
265
+ const actualAdapter = adapter ?? getAdapter();
216
266
  // Validate required parameters
217
267
  if (!params.title || params.title.trim() === '') {
218
268
  throw new Error('Parameter "title" is required and cannot be empty');
219
269
  }
220
270
  validateLength(params.title, 'Parameter "title"', 200);
221
271
  try {
222
- return transaction(actualDb, () => {
223
- return createTaskInternal(params, actualDb);
272
+ return await actualAdapter.transaction(async (trx) => {
273
+ return await createTaskInternal(params, actualAdapter, trx);
224
274
  });
225
275
  }
226
276
  catch (error) {
@@ -231,56 +281,50 @@ export function createTask(params, db) {
231
281
  /**
232
282
  * Update task metadata
233
283
  */
234
- export function updateTask(params, db) {
235
- const actualDb = db ?? getDatabase();
284
+ export async function updateTask(params, adapter) {
285
+ const actualAdapter = adapter ?? getAdapter();
236
286
  // Validate required parameters
237
287
  if (!params.task_id) {
238
288
  throw new Error('Parameter "task_id" is required');
239
289
  }
240
290
  try {
241
- return transaction(actualDb, () => {
291
+ return await actualAdapter.transaction(async (trx) => {
292
+ const knex = actualAdapter.getKnex();
242
293
  // Check if task exists
243
- const taskExists = actualDb.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();
244
295
  if (!taskExists) {
245
296
  throw new Error(`Task with id ${params.task_id} not found`);
246
297
  }
247
- // Build update query dynamically
248
- const updates = [];
249
- const updateParams = [];
298
+ // Build update data dynamically
299
+ const updateData = {};
250
300
  if (params.title !== undefined) {
251
301
  if (params.title.trim() === '') {
252
302
  throw new Error('Parameter "title" cannot be empty');
253
303
  }
254
304
  validateLength(params.title, 'Parameter "title"', 200);
255
- updates.push('title = ?');
256
- updateParams.push(params.title);
305
+ updateData.title = params.title;
257
306
  }
258
307
  if (params.priority !== undefined) {
259
308
  validatePriorityRange(params.priority);
260
- updates.push('priority = ?');
261
- updateParams.push(params.priority);
309
+ updateData.priority = params.priority;
262
310
  }
263
311
  if (params.assigned_agent !== undefined) {
264
- const agentId = getOrCreateAgent(actualDb, params.assigned_agent);
265
- updates.push('assigned_agent_id = ?');
266
- updateParams.push(agentId);
312
+ const agentId = await getOrCreateAgent(actualAdapter, params.assigned_agent, trx);
313
+ updateData.assigned_agent_id = agentId;
267
314
  }
268
315
  if (params.layer !== undefined) {
269
- const layerId = getLayerId(actualDb, params.layer);
316
+ const layerId = await getLayerId(actualAdapter, params.layer, trx);
270
317
  if (layerId === null) {
271
318
  throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
272
319
  }
273
- updates.push('layer_id = ?');
274
- updateParams.push(layerId);
320
+ updateData.layer_id = layerId;
275
321
  }
276
322
  // 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);
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
284
328
  }
285
329
  // Update t_task_details if any detail fields provided
286
330
  if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
@@ -322,43 +366,35 @@ export function updateTask(params, db) {
322
366
  }
323
367
  }
324
368
  // 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) {
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) {
327
384
  // 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
- }
385
+ await trx('t_task_details')
386
+ .where({ task_id: params.task_id })
387
+ .update(detailsUpdate);
354
388
  }
355
- else {
389
+ else if (!detailsExist) {
356
390
  // 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);
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
+ });
362
398
  }
363
399
  }
364
400
  // Handle watch_files if provided (v3.4.1)
@@ -395,22 +431,20 @@ export function updateTask(params, db) {
395
431
  else {
396
432
  throw new Error('Parameter "watch_files" must be a string or array');
397
433
  }
398
- const insertFileLinkStmt = actualDb.prepare(`
399
- INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
400
- VALUES (?, ?)
401
- `);
402
434
  for (const filePath of watchFilesParsed) {
403
- const fileId = getOrCreateFile(actualDb, filePath);
404
- insertFileLinkStmt.run(params.task_id, fileId);
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();
405
440
  }
406
441
  // Register files with watcher for auto-tracking
407
442
  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);
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();
414
448
  if (taskData) {
415
449
  const watcher = FileWatcher.getInstance();
416
450
  for (const filePath of watchFilesParsed) {
@@ -438,89 +472,70 @@ export function updateTask(params, db) {
438
472
  /**
439
473
  * Internal helper: Query task dependencies (used by getTask and getDependencies)
440
474
  */
441
- function queryTaskDependencies(db, taskId, includeDetails = false) {
475
+ async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
476
+ const knex = adapter.getKnex();
442
477
  // Build query based on include_details flag
443
- 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);
444
502
  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
- `;
503
+ blockersQuery = blockersQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
465
504
  }
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);
505
+ const blockers = await blockersQuery;
477
506
  // 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);
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;
488
517
  return { blockers, blocking };
489
518
  }
490
519
  /**
491
520
  * Get full task details
492
521
  */
493
- export function getTask(params, db) {
494
- const actualDb = db ?? getDatabase();
522
+ export async function getTask(params, adapter) {
523
+ const actualAdapter = adapter ?? getAdapter();
524
+ const knex = actualAdapter.getKnex();
495
525
  if (!params.task_id) {
496
526
  throw new Error('Parameter "task_id" is required');
497
527
  }
498
528
  try {
499
529
  // 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);
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();
524
539
  if (!task) {
525
540
  return {
526
541
  found: false,
@@ -528,37 +543,27 @@ export function getTask(params, db) {
528
543
  };
529
544
  }
530
545
  // 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);
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));
538
551
  // 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);
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');
546
556
  // 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);
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');
554
561
  // 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);
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));
562
567
  // Build result
563
568
  const result = {
564
569
  found: true,
@@ -572,7 +577,7 @@ export function getTask(params, db) {
572
577
  };
573
578
  // Include dependencies if requested (token-efficient, metadata-only)
574
579
  if (params.include_dependencies) {
575
- const deps = queryTaskDependencies(actualDb, params.task_id, false);
580
+ const deps = await queryTaskDependencies(actualAdapter, params.task_id, false);
576
581
  result.task.dependencies = {
577
582
  blockers: deps.blockers,
578
583
  blocking: deps.blocking
@@ -588,79 +593,69 @@ export function getTask(params, db) {
588
593
  /**
589
594
  * List tasks (token-efficient, no descriptions)
590
595
  */
591
- export async function listTasks(params = {}, db) {
592
- const actualDb = db ?? getDatabase();
596
+ export async function listTasks(params = {}, adapter) {
597
+ const actualAdapter = adapter ?? getAdapter();
598
+ const knex = actualAdapter.getKnex();
593
599
  try {
594
600
  // 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);
601
+ const transitionCount = await detectAndTransitionStaleTasks(actualAdapter);
602
+ const gitCompletedCount = await detectAndCompleteReviewedTasks(actualAdapter);
603
+ const gitArchivedCount = await detectAndArchiveOnCommit(actualAdapter);
604
+ const archiveCount = await autoArchiveOldDoneTasks(actualAdapter);
599
605
  // Build query with optional dependency counts
600
606
  let query;
601
607
  if (params.include_dependency_counts) {
602
608
  // 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
- `;
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'));
621
623
  }
622
624
  else {
623
625
  // Standard query without dependency counts
624
- query = 'SELECT * FROM v_task_board WHERE 1=1';
626
+ query = knex('v_task_board');
625
627
  }
626
- const queryParams = [];
627
628
  // Filter by status
628
629
  if (params.status) {
629
630
  if (!STATUS_TO_ID[params.status]) {
630
631
  throw new Error(`Invalid status: ${params.status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
631
632
  }
632
- query += params.include_dependency_counts ? ' AND vt.status = ?' : ' AND status = ?';
633
- queryParams.push(params.status);
633
+ query = query.where(params.include_dependency_counts ? 'vt.status' : 'status', params.status);
634
634
  }
635
635
  // Filter by assigned agent
636
636
  if (params.assigned_agent) {
637
- query += params.include_dependency_counts ? ' AND vt.assigned_to = ?' : ' AND assigned_to = ?';
638
- queryParams.push(params.assigned_agent);
637
+ query = query.where(params.include_dependency_counts ? 'vt.assigned_to' : 'assigned_to', params.assigned_agent);
639
638
  }
640
639
  // Filter by layer
641
640
  if (params.layer) {
642
- query += params.include_dependency_counts ? ' AND vt.layer = ?' : ' AND layer = ?';
643
- queryParams.push(params.layer);
641
+ query = query.where(params.include_dependency_counts ? 'vt.layer' : 'layer', params.layer);
644
642
  }
645
643
  // Filter by tags
646
644
  if (params.tags && params.tags.length > 0) {
647
645
  for (const tag of params.tags) {
648
- query += params.include_dependency_counts ? ' AND vt.tags LIKE ?' : ' AND tags LIKE ?';
649
- queryParams.push(`%${tag}%`);
646
+ query = query.where(params.include_dependency_counts ? 'vt.tags' : 'tags', 'like', `%${tag}%`);
650
647
  }
651
648
  }
652
649
  // Order by updated timestamp (most recent first)
653
- 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');
654
651
  // Pagination
655
652
  const limit = params.limit !== undefined ? params.limit : 50;
656
653
  const offset = params.offset || 0;
657
654
  validateRange(limit, 'Parameter "limit"', 0, 100);
658
655
  validateRange(offset, 'Parameter "offset"', 0, Number.MAX_SAFE_INTEGER);
659
- query += ' LIMIT ? OFFSET ?';
660
- queryParams.push(limit, offset);
656
+ query = query.limit(limit).offset(offset);
661
657
  // Execute query
662
- const stmt = actualDb.prepare(query);
663
- const rows = stmt.all(...queryParams);
658
+ const rows = await query;
664
659
  return {
665
660
  tasks: rows,
666
661
  count: rows.length,
@@ -678,8 +673,9 @@ export async function listTasks(params = {}, db) {
678
673
  /**
679
674
  * Move task to different status
680
675
  */
681
- export function moveTask(params, db) {
682
- const actualDb = db ?? getDatabase();
676
+ export async function moveTask(params, adapter) {
677
+ const actualAdapter = adapter ?? getAdapter();
678
+ const knex = actualAdapter.getKnex();
683
679
  if (!params.task_id) {
684
680
  throw new Error('Parameter "task_id" is required');
685
681
  }
@@ -688,11 +684,14 @@ export function moveTask(params, db) {
688
684
  }
689
685
  try {
690
686
  // Run auto-stale detection and auto-archive before move
691
- detectAndTransitionStaleTasks(actualDb);
692
- autoArchiveOldDoneTasks(actualDb);
693
- return transaction(actualDb, () => {
687
+ await detectAndTransitionStaleTasks(actualAdapter);
688
+ await autoArchiveOldDoneTasks(actualAdapter);
689
+ return await actualAdapter.transaction(async (trx) => {
694
690
  // Get current status
695
- const taskRow = actualDb.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();
696
695
  if (!taskRow) {
697
696
  throw new Error(`Task with id ${params.task_id} not found`);
698
697
  }
@@ -708,13 +707,26 @@ export function moveTask(params, db) {
708
707
  `Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
709
708
  }
710
709
  // 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);
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
+ });
718
730
  // Update watcher if moving to done or archived (stop watching)
719
731
  if (params.new_status === 'done' || params.new_status === 'archived') {
720
732
  try {
@@ -742,8 +754,9 @@ export function moveTask(params, db) {
742
754
  /**
743
755
  * Link task to decision/constraint/file
744
756
  */
745
- export function linkTask(params, db) {
746
- const actualDb = db ?? getDatabase();
757
+ export async function linkTask(params, adapter) {
758
+ const actualAdapter = adapter ?? getAdapter();
759
+ const knex = actualAdapter.getKnex();
747
760
  if (!params.task_id) {
748
761
  throw new Error('Parameter "task_id" is required');
749
762
  }
@@ -754,21 +767,21 @@ export function linkTask(params, db) {
754
767
  throw new Error('Parameter "target_id" is required');
755
768
  }
756
769
  try {
757
- return transaction(actualDb, () => {
770
+ return await actualAdapter.transaction(async (trx) => {
758
771
  // Check if task exists
759
- const taskExists = actualDb.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();
760
773
  if (!taskExists) {
761
774
  throw new Error(`Task with id ${params.task_id} not found`);
762
775
  }
763
776
  if (params.link_type === 'decision') {
764
777
  const decisionKey = String(params.target_id);
765
- const keyId = getOrCreateContextKey(actualDb, decisionKey);
778
+ const keyId = await getOrCreateContextKey(actualAdapter, decisionKey, trx);
766
779
  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);
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();
772
785
  return {
773
786
  success: true,
774
787
  task_id: params.task_id,
@@ -781,15 +794,14 @@ export function linkTask(params, db) {
781
794
  else if (params.link_type === 'constraint') {
782
795
  const constraintId = Number(params.target_id);
783
796
  // Check if constraint exists
784
- const constraintExists = actualDb.prepare('SELECT id FROM t_constraints WHERE id = ?').get(constraintId);
797
+ const constraintExists = await trx('t_constraints').where({ id: constraintId }).first();
785
798
  if (!constraintExists) {
786
799
  throw new Error(`Constraint with id ${constraintId} not found`);
787
800
  }
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);
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();
793
805
  return {
794
806
  success: true,
795
807
  task_id: params.task_id,
@@ -804,20 +816,18 @@ export function linkTask(params, db) {
804
816
  console.warn(` Use task.create(watch_files=[...]) or task.update(watch_files=[...]) instead.`);
805
817
  console.warn(` Or use the new watch_files action: { action: "watch_files", task_id: ${params.task_id}, file_paths: ["..."] }`);
806
818
  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);
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();
813
824
  // Register file with watcher for auto-tracking
814
825
  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);
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();
821
831
  if (taskData) {
822
832
  const watcher = FileWatcher.getInstance();
823
833
  watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
@@ -849,15 +859,19 @@ export function linkTask(params, db) {
849
859
  /**
850
860
  * Archive completed task
851
861
  */
852
- export function archiveTask(params, db) {
853
- const actualDb = db ?? getDatabase();
862
+ export async function archiveTask(params, adapter) {
863
+ const actualAdapter = adapter ?? getAdapter();
864
+ const knex = actualAdapter.getKnex();
854
865
  if (!params.task_id) {
855
866
  throw new Error('Parameter "task_id" is required');
856
867
  }
857
868
  try {
858
- return transaction(actualDb, () => {
869
+ return await actualAdapter.transaction(async (trx) => {
859
870
  // Check if task is in 'done' status
860
- const taskRow = actualDb.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();
861
875
  if (!taskRow) {
862
876
  throw new Error(`Task with id ${params.task_id} not found`);
863
877
  }
@@ -865,8 +879,18 @@ export function archiveTask(params, db) {
865
879
  throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
866
880
  }
867
881
  // 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);
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
+ });
870
894
  // Unregister from file watcher (archived tasks don't need tracking)
871
895
  try {
872
896
  const watcher = FileWatcher.getInstance();
@@ -890,8 +914,9 @@ export function archiveTask(params, db) {
890
914
  /**
891
915
  * Add dependency (blocking relationship) between tasks
892
916
  */
893
- export function addDependency(params, db) {
894
- const actualDb = db ?? getDatabase();
917
+ export async function addDependency(params, adapter) {
918
+ const actualAdapter = adapter ?? getAdapter();
919
+ const knex = actualAdapter.getKnex();
895
920
  if (!params.blocker_task_id) {
896
921
  throw new Error('Parameter "blocker_task_id" is required');
897
922
  }
@@ -899,14 +924,20 @@ export function addDependency(params, db) {
899
924
  throw new Error('Parameter "blocked_task_id" is required');
900
925
  }
901
926
  try {
902
- return transaction(actualDb, () => {
927
+ return await actualAdapter.transaction(async (trx) => {
903
928
  // Validation 1: No self-dependencies
904
929
  if (params.blocker_task_id === params.blocked_task_id) {
905
930
  throw new Error('Self-dependency not allowed');
906
931
  }
907
932
  // 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);
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();
910
941
  if (!blockerTask) {
911
942
  throw new Error(`Blocker task #${params.blocker_task_id} not found`);
912
943
  }
@@ -921,15 +952,17 @@ export function addDependency(params, db) {
921
952
  throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
922
953
  }
923
954
  // 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);
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();
928
961
  if (reverseExists) {
929
962
  throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
930
963
  }
931
964
  // Validation 5: No transitive circular (check if adding this would create a cycle)
932
- const cycleCheck = actualDb.prepare(`
965
+ const cycleCheck = await trx.raw(`
933
966
  WITH RECURSIVE dependency_chain AS (
934
967
  -- Start from the task that would be blocked
935
968
  SELECT blocked_task_id as task_id, 1 as depth
@@ -945,10 +978,11 @@ export function addDependency(params, db) {
945
978
  WHERE dc.depth < 100
946
979
  )
947
980
  SELECT task_id FROM dependency_chain WHERE task_id = ?
948
- `).get(params.blocked_task_id, params.blocker_task_id);
981
+ `, [params.blocked_task_id, params.blocker_task_id])
982
+ .then((result) => result[0]);
949
983
  if (cycleCheck) {
950
984
  // Build cycle path for error message
951
- const cyclePathResult = actualDb.prepare(`
985
+ const cyclePathResult = await trx.raw(`
952
986
  WITH RECURSIVE dependency_chain AS (
953
987
  SELECT blocked_task_id as task_id, 1 as depth,
954
988
  CAST(blocked_task_id AS TEXT) as path
@@ -964,16 +998,17 @@ export function addDependency(params, db) {
964
998
  WHERE dc.depth < 100
965
999
  )
966
1000
  SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
967
- `).get(params.blocked_task_id, params.blocker_task_id);
1001
+ `, [params.blocked_task_id, params.blocker_task_id])
1002
+ .then((result) => result[0]);
968
1003
  const cyclePath = cyclePathResult?.path || `#${params.blocked_task_id} → ... → #${params.blocker_task_id}`;
969
1004
  throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
970
1005
  }
971
1006
  // 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);
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
+ });
977
1012
  return {
978
1013
  success: true,
979
1014
  message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
@@ -992,8 +1027,9 @@ export function addDependency(params, db) {
992
1027
  /**
993
1028
  * Remove dependency between tasks
994
1029
  */
995
- export function removeDependency(params, db) {
996
- const actualDb = db ?? getDatabase();
1030
+ export async function removeDependency(params, adapter) {
1031
+ const actualAdapter = adapter ?? getAdapter();
1032
+ const knex = actualAdapter.getKnex();
997
1033
  if (!params.blocker_task_id) {
998
1034
  throw new Error('Parameter "blocker_task_id" is required');
999
1035
  }
@@ -1001,11 +1037,12 @@ export function removeDependency(params, db) {
1001
1037
  throw new Error('Parameter "blocked_task_id" is required');
1002
1038
  }
1003
1039
  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);
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();
1009
1046
  return {
1010
1047
  success: true,
1011
1048
  message: `Dependency removed: Task #${params.blocker_task_id} no longer blocks Task #${params.blocked_task_id}`
@@ -1019,20 +1056,21 @@ export function removeDependency(params, db) {
1019
1056
  /**
1020
1057
  * Get dependencies for a task (bidirectional: what blocks this task, what this task blocks)
1021
1058
  */
1022
- export function getDependencies(params, db) {
1023
- const actualDb = db ?? getDatabase();
1059
+ export async function getDependencies(params, adapter) {
1060
+ const actualAdapter = adapter ?? getAdapter();
1061
+ const knex = actualAdapter.getKnex();
1024
1062
  if (!params.task_id) {
1025
1063
  throw new Error('Parameter "task_id" is required');
1026
1064
  }
1027
1065
  const includeDetails = params.include_details || false;
1028
1066
  try {
1029
1067
  // Check if task exists
1030
- const taskExists = actualDb.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();
1031
1069
  if (!taskExists) {
1032
1070
  throw new Error(`Task with id ${params.task_id} not found`);
1033
1071
  }
1034
1072
  // Use the shared helper function
1035
- const deps = queryTaskDependencies(actualDb, params.task_id, includeDetails);
1073
+ const deps = await queryTaskDependencies(actualAdapter, params.task_id, includeDetails);
1036
1074
  return {
1037
1075
  task_id: params.task_id,
1038
1076
  blockers: deps.blockers,
@@ -1051,39 +1089,93 @@ export function getDependencies(params, db) {
1051
1089
  /**
1052
1090
  * Create multiple tasks atomically
1053
1091
  */
1054
- export function batchCreateTasks(params, db) {
1055
- const actualDb = db ?? getDatabase();
1092
+ export async function batchCreateTasks(params, adapter) {
1093
+ const actualAdapter = adapter ?? getAdapter();
1056
1094
  if (!params.tasks || !Array.isArray(params.tasks)) {
1057
1095
  throw new Error('Parameter "tasks" is required and must be an array');
1058
1096
  }
1097
+ if (params.tasks.length > 50) {
1098
+ throw new Error('Parameter "tasks" must contain at most 50 items');
1099
+ }
1059
1100
  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
- };
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
+ }
1080
1171
  }
1081
1172
  /**
1082
1173
  * Watch/unwatch files for a task (v3.4.1)
1083
1174
  * Replaces the need to use task.link(file) for file watching
1084
1175
  */
1085
- export function watchFiles(params, db) {
1086
- const actualDb = db ?? getDatabase();
1176
+ export async function watchFiles(params, adapter) {
1177
+ const actualAdapter = adapter ?? getAdapter();
1178
+ const knex = actualAdapter.getKnex();
1087
1179
  if (!params.task_id) {
1088
1180
  throw new Error('Parameter "task_id" is required');
1089
1181
  }
@@ -1091,14 +1183,13 @@ export function watchFiles(params, db) {
1091
1183
  throw new Error('Parameter "action" is required (watch, unwatch, or list)');
1092
1184
  }
1093
1185
  try {
1094
- return transaction(actualDb, () => {
1186
+ return await actualAdapter.transaction(async (trx) => {
1095
1187
  // 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);
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();
1102
1193
  if (!taskData) {
1103
1194
  throw new Error(`Task with id ${params.task_id} not found`);
1104
1195
  }
@@ -1106,16 +1197,18 @@ export function watchFiles(params, db) {
1106
1197
  if (!params.file_paths || params.file_paths.length === 0) {
1107
1198
  throw new Error('Parameter "file_paths" is required for watch action');
1108
1199
  }
1109
- const insertFileLinkStmt = actualDb.prepare(`
1110
- INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
1111
- VALUES (?, ?)
1112
- `);
1113
1200
  const addedFiles = [];
1114
1201
  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) {
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
+ });
1119
1212
  addedFiles.push(filePath);
1120
1213
  }
1121
1214
  }
@@ -1143,30 +1236,18 @@ export function watchFiles(params, db) {
1143
1236
  if (!params.file_paths || params.file_paths.length === 0) {
1144
1237
  throw new Error('Parameter "file_paths" is required for unwatch action');
1145
1238
  }
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
1239
  const removedFiles = [];
1151
1240
  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) {
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) {
1155
1248
  removedFiles.push(filePath);
1156
1249
  }
1157
1250
  }
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
1251
  return {
1171
1252
  success: true,
1172
1253
  task_id: params.task_id,
@@ -1177,13 +1258,11 @@ export function watchFiles(params, db) {
1177
1258
  };
1178
1259
  }
1179
1260
  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);
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));
1187
1266
  return {
1188
1267
  success: true,
1189
1268
  task_id: params.task_id,
@@ -1207,32 +1286,27 @@ export function watchFiles(params, db) {
1207
1286
  * Get pruned files for a task (v3.5.0 Auto-Pruning)
1208
1287
  * Returns audit trail of files that were auto-pruned as non-existent
1209
1288
  */
1210
- export function getPrunedFiles(params, db) {
1211
- const actualDb = db ?? getDatabase();
1289
+ export async function getPrunedFiles(params, adapter) {
1290
+ const actualAdapter = adapter ?? getAdapter();
1291
+ const knex = actualAdapter.getKnex();
1212
1292
  try {
1213
1293
  // Validate task_id
1214
1294
  if (!params.task_id || typeof params.task_id !== 'number') {
1215
1295
  throw new Error('task_id is required and must be a number');
1216
1296
  }
1217
1297
  // Validate task exists
1218
- const task = actualDb.prepare('SELECT id FROM t_tasks WHERE id = ?').get(params.task_id);
1298
+ const task = await knex('t_tasks').where({ id: params.task_id }).first();
1219
1299
  if (!task) {
1220
1300
  throw new Error(`Task not found: ${params.task_id}`);
1221
1301
  }
1222
1302
  // Get pruned files
1223
1303
  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);
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);
1236
1310
  return {
1237
1311
  success: true,
1238
1312
  task_id: params.task_id,
@@ -1252,8 +1326,9 @@ export function getPrunedFiles(params, db) {
1252
1326
  * Link a pruned file to a decision (v3.5.0 Auto-Pruning)
1253
1327
  * Attaches WHY reasoning to pruned files for project archaeology
1254
1328
  */
1255
- export function linkPrunedFile(params, db) {
1256
- const actualDb = db ?? getDatabase();
1329
+ export async function linkPrunedFile(params, adapter) {
1330
+ const actualAdapter = adapter ?? getAdapter();
1331
+ const knex = actualAdapter.getKnex();
1257
1332
  try {
1258
1333
  // Validate pruned_file_id
1259
1334
  if (!params.pruned_file_id || typeof params.pruned_file_id !== 'number') {
@@ -1264,30 +1339,31 @@ export function linkPrunedFile(params, db) {
1264
1339
  throw new Error('decision_key is required and must be a string');
1265
1340
  }
1266
1341
  // 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);
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();
1274
1351
  if (!decision) {
1275
1352
  throw new Error(`Decision not found: ${params.decision_key}`);
1276
1353
  }
1277
1354
  // 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);
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();
1281
1359
  if (!prunedFile) {
1282
1360
  throw new Error(`Pruned file record not found: ${params.pruned_file_id}`);
1283
1361
  }
1284
1362
  // 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) {
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) {
1291
1367
  throw new Error(`Failed to link pruned file #${params.pruned_file_id} to decision ${params.decision_key}`);
1292
1368
  }
1293
1369
  return {
@@ -1565,8 +1641,9 @@ export function taskHelp() {
1565
1641
  /**
1566
1642
  * Query file watcher status and monitored files/tasks
1567
1643
  */
1568
- export function watcherStatus(args, db) {
1569
- const actualDb = db ?? getDatabase();
1644
+ export async function watcherStatus(args, adapter) {
1645
+ const actualAdapter = adapter ?? getAdapter();
1646
+ const knex = actualAdapter.getKnex();
1570
1647
  const subaction = args.subaction || 'status';
1571
1648
  const watcher = FileWatcher.getInstance();
1572
1649
  if (subaction === 'help') {
@@ -1609,14 +1686,13 @@ export function watcherStatus(args, db) {
1609
1686
  };
1610
1687
  }
1611
1688
  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();
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']);
1620
1696
  // Group by file
1621
1697
  const fileMap = new Map();
1622
1698
  for (const link of fileLinks) {
@@ -1643,16 +1719,14 @@ export function watcherStatus(args, db) {
1643
1719
  };
1644
1720
  }
1645
1721
  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();
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');
1656
1730
  const tasks = taskLinks.map(task => ({
1657
1731
  task_id: task.id,
1658
1732
  task_title: task.title,