sqlew 3.6.9 → 3.7.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 (383) hide show
  1. package/CHANGELOG.md +398 -1
  2. package/README.md +58 -39
  3. package/assets/config.example.toml +93 -0
  4. package/assets/kanban-visualizer.png +0 -0
  5. package/assets/sample-agents/sqlew-architect.md +32 -13
  6. package/assets/sample-agents/sqlew-researcher.md +70 -17
  7. package/assets/sample-agents/sqlew-scrum-master.md +60 -25
  8. package/assets/schema.sql +2 -2
  9. package/dist/adapters/auth/auth-factory.d.ts +86 -0
  10. package/dist/adapters/auth/auth-factory.d.ts.map +1 -0
  11. package/dist/adapters/auth/auth-factory.js +103 -0
  12. package/dist/adapters/auth/auth-factory.js.map +1 -0
  13. package/dist/adapters/auth/auth-types.d.ts +30 -0
  14. package/dist/adapters/auth/auth-types.d.ts.map +1 -0
  15. package/dist/adapters/auth/auth-types.js +30 -0
  16. package/dist/adapters/auth/auth-types.js.map +1 -0
  17. package/dist/adapters/auth/base-auth-provider.d.ts +327 -0
  18. package/dist/adapters/auth/base-auth-provider.d.ts.map +1 -0
  19. package/dist/adapters/auth/base-auth-provider.js +111 -0
  20. package/dist/adapters/auth/base-auth-provider.js.map +1 -0
  21. package/dist/adapters/auth/direct-auth-provider.d.ts +356 -0
  22. package/dist/adapters/auth/direct-auth-provider.d.ts.map +1 -0
  23. package/dist/adapters/auth/direct-auth-provider.js +406 -0
  24. package/dist/adapters/auth/direct-auth-provider.js.map +1 -0
  25. package/dist/adapters/base-adapter.d.ts +638 -0
  26. package/dist/adapters/base-adapter.d.ts.map +1 -0
  27. package/dist/adapters/base-adapter.js +557 -0
  28. package/dist/adapters/base-adapter.js.map +1 -0
  29. package/dist/adapters/index.d.ts +13 -2
  30. package/dist/adapters/index.d.ts.map +1 -1
  31. package/dist/adapters/index.js +27 -5
  32. package/dist/adapters/index.js.map +1 -1
  33. package/dist/adapters/mysql-adapter.d.ts +547 -6
  34. package/dist/adapters/mysql-adapter.d.ts.map +1 -1
  35. package/dist/adapters/mysql-adapter.js +651 -32
  36. package/dist/adapters/mysql-adapter.js.map +1 -1
  37. package/dist/adapters/postgresql-adapter.d.ts +15 -4
  38. package/dist/adapters/postgresql-adapter.d.ts.map +1 -1
  39. package/dist/adapters/postgresql-adapter.js +19 -2
  40. package/dist/adapters/postgresql-adapter.js.map +1 -1
  41. package/dist/adapters/sqlite-adapter.d.ts +35 -5
  42. package/dist/adapters/sqlite-adapter.d.ts.map +1 -1
  43. package/dist/adapters/sqlite-adapter.js +57 -18
  44. package/dist/adapters/sqlite-adapter.js.map +1 -1
  45. package/dist/cli/db-dump.d.ts +32 -0
  46. package/dist/cli/db-dump.d.ts.map +1 -0
  47. package/dist/cli/db-dump.js +409 -0
  48. package/dist/cli/db-dump.js.map +1 -0
  49. package/dist/cli.js +24 -14
  50. package/dist/cli.js.map +1 -1
  51. package/dist/config/example-generator.d.ts.map +1 -1
  52. package/dist/config/example-generator.js +4 -3
  53. package/dist/config/example-generator.js.map +1 -1
  54. package/dist/config/knex/bootstrap/20251025020452_create_master_tables.d.ts.map +1 -0
  55. package/dist/{migrations → config}/knex/bootstrap/20251025020452_create_master_tables.js +7 -2
  56. package/dist/config/knex/bootstrap/20251025020452_create_master_tables.js.map +1 -0
  57. package/dist/config/knex/bootstrap/20251025021152_create_transaction_tables.d.ts.map +1 -0
  58. package/dist/{migrations → config}/knex/bootstrap/20251025021152_create_transaction_tables.js +49 -50
  59. package/dist/config/knex/bootstrap/20251025021152_create_transaction_tables.js.map +1 -0
  60. package/dist/config/knex/bootstrap/20251025021351_create_indexes.d.ts.map +1 -0
  61. package/dist/config/knex/bootstrap/20251025021351_create_indexes.js.map +1 -0
  62. package/dist/config/knex/bootstrap/20251025021416_seed_master_data.d.ts.map +1 -0
  63. package/dist/{migrations → config}/knex/bootstrap/20251025021416_seed_master_data.js +11 -6
  64. package/dist/config/knex/bootstrap/20251025021416_seed_master_data.js.map +1 -0
  65. package/dist/config/knex/bootstrap/20251025070349_create_views.d.ts.map +1 -0
  66. package/dist/{migrations → config}/knex/bootstrap/20251025070349_create_views.js +66 -14
  67. package/dist/config/knex/bootstrap/20251025070349_create_views.js.map +1 -0
  68. package/dist/config/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts.map +1 -0
  69. package/dist/config/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js +22 -0
  70. package/dist/config/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js.map +1 -0
  71. package/dist/config/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts.map +1 -0
  72. package/dist/config/knex/enhancements/20251025082220_fix_task_dependencies_columns.js.map +1 -0
  73. package/dist/config/knex/enhancements/20251025090000_create_help_system_tables.d.ts.map +1 -0
  74. package/dist/{migrations → config}/knex/enhancements/20251025090000_create_help_system_tables.js +6 -0
  75. package/dist/config/knex/enhancements/20251025090000_create_help_system_tables.js.map +1 -0
  76. package/dist/config/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts.map +1 -0
  77. package/dist/{migrations → config}/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js +6 -0
  78. package/dist/config/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js.map +1 -0
  79. package/dist/config/knex/enhancements/20251025100000_seed_help_metadata.d.ts.map +1 -0
  80. package/dist/{migrations → config}/knex/enhancements/20251025100000_seed_help_metadata.js +6 -0
  81. package/dist/config/knex/enhancements/20251025100000_seed_help_metadata.js.map +1 -0
  82. package/dist/config/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts.map +1 -0
  83. package/dist/config/knex/enhancements/20251025100100_seed_remaining_use_cases.js.map +1 -0
  84. package/dist/config/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts.map +1 -0
  85. package/dist/{migrations → config}/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js +7 -0
  86. package/dist/config/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js.map +1 -0
  87. package/dist/config/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts.map +1 -0
  88. package/dist/config/knex/enhancements/20251027000000_add_agent_reuse_system.js +62 -0
  89. package/dist/config/knex/enhancements/20251027000000_add_agent_reuse_system.js.map +1 -0
  90. package/dist/config/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts.map +1 -0
  91. package/dist/config/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js.map +1 -0
  92. package/dist/config/knex/enhancements/20251027020000_update_agent_reusability.d.ts.map +1 -0
  93. package/dist/{migrations → config}/knex/enhancements/20251027020000_update_agent_reusability.js +6 -0
  94. package/dist/config/knex/enhancements/20251027020000_update_agent_reusability.js.map +1 -0
  95. package/dist/config/knex/enhancements/20251028000000_simplify_agent_system.d.ts.map +1 -0
  96. package/dist/{migrations → config}/knex/enhancements/20251028000000_simplify_agent_system.js +6 -0
  97. package/dist/config/knex/enhancements/20251028000000_simplify_agent_system.js.map +1 -0
  98. package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.d.ts +13 -0
  99. package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.d.ts.map +1 -0
  100. package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.js +48 -0
  101. package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.js.map +1 -0
  102. package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.d.ts +24 -0
  103. package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.d.ts.map +1 -0
  104. package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.js +189 -0
  105. package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.js.map +1 -0
  106. package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.d.ts +16 -0
  107. package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.d.ts.map +1 -0
  108. package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.js +65 -0
  109. package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.js.map +1 -0
  110. package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.d.ts +23 -0
  111. package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.d.ts.map +1 -0
  112. package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.js +118 -0
  113. package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.js.map +1 -0
  114. package/dist/config/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts.map +1 -0
  115. package/dist/config/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js.map +1 -0
  116. package/dist/config/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts.map +1 -0
  117. package/dist/config/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js.map +1 -0
  118. package/dist/config/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts.map +1 -0
  119. package/dist/config/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js.map +1 -0
  120. package/dist/config/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts.map +1 -0
  121. package/dist/config/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js.map +1 -0
  122. package/dist/config/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts.map +1 -0
  123. package/dist/config/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js.map +1 -0
  124. package/dist/config/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts.map +1 -0
  125. package/dist/config/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js.map +1 -0
  126. package/dist/config/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts.map +1 -0
  127. package/dist/config/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js.map +1 -0
  128. package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.d.ts +49 -0
  129. package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.d.ts.map +1 -0
  130. package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.js +864 -0
  131. package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.js.map +1 -0
  132. package/dist/config/loader.d.ts +23 -3
  133. package/dist/config/loader.d.ts.map +1 -1
  134. package/dist/config/loader.js +155 -8
  135. package/dist/config/loader.js.map +1 -1
  136. package/dist/config/types.d.ts +261 -2
  137. package/dist/config/types.d.ts.map +1 -1
  138. package/dist/config/types.js.map +1 -1
  139. package/dist/config/writer.d.ts +65 -0
  140. package/dist/config/writer.d.ts.map +1 -0
  141. package/dist/config/writer.js +139 -0
  142. package/dist/config/writer.js.map +1 -0
  143. package/dist/database.d.ts +11 -2
  144. package/dist/database.d.ts.map +1 -1
  145. package/dist/database.js +65 -8
  146. package/dist/database.js.map +1 -1
  147. package/dist/index.js +186 -32
  148. package/dist/index.js.map +1 -1
  149. package/dist/knexfile.d.ts.map +1 -1
  150. package/dist/knexfile.js +88 -12
  151. package/dist/knexfile.js.map +1 -1
  152. package/dist/tests/all-features.test.js +15 -3
  153. package/dist/tests/all-features.test.js.map +1 -1
  154. package/dist/tests/config-loader.test.d.ts +6 -0
  155. package/dist/tests/config-loader.test.d.ts.map +1 -0
  156. package/dist/tests/config-loader.test.js +201 -0
  157. package/dist/tests/config-loader.test.js.map +1 -0
  158. package/dist/tests/connection-manager-integration.test.d.ts +2 -0
  159. package/dist/tests/connection-manager-integration.test.d.ts.map +1 -0
  160. package/dist/tests/connection-manager-integration.test.js +431 -0
  161. package/dist/tests/connection-manager-integration.test.js.map +1 -0
  162. package/dist/tests/connection-manager.test.d.ts +2 -0
  163. package/dist/tests/connection-manager.test.d.ts.map +1 -0
  164. package/dist/tests/connection-manager.test.js +361 -0
  165. package/dist/tests/connection-manager.test.js.map +1 -0
  166. package/dist/tests/dump-import.test.d.ts +15 -0
  167. package/dist/tests/dump-import.test.d.ts.map +1 -0
  168. package/dist/tests/dump-import.test.js +430 -0
  169. package/dist/tests/dump-import.test.js.map +1 -0
  170. package/dist/tests/migration-idempotency.test.d.ts +2 -0
  171. package/dist/tests/migration-idempotency.test.d.ts.map +1 -0
  172. package/dist/tests/migration-idempotency.test.js +330 -0
  173. package/dist/tests/migration-idempotency.test.js.map +1 -0
  174. package/dist/tests/migration-upgrade-paths.test.d.ts +2 -0
  175. package/dist/tests/migration-upgrade-paths.test.d.ts.map +1 -0
  176. package/dist/tests/migration-upgrade-paths.test.js +248 -0
  177. package/dist/tests/migration-upgrade-paths.test.js.map +1 -0
  178. package/dist/tests/multi-project-migration.test.d.ts +17 -0
  179. package/dist/tests/multi-project-migration.test.d.ts.map +1 -0
  180. package/dist/tests/multi-project-migration.test.js +399 -0
  181. package/dist/tests/multi-project-migration.test.js.map +1 -0
  182. package/dist/tests/multi-project.test.d.ts +5 -0
  183. package/dist/tests/multi-project.test.d.ts.map +1 -0
  184. package/dist/tests/multi-project.test.js +238 -0
  185. package/dist/tests/multi-project.test.js.map +1 -0
  186. package/dist/tests/schema-migration.test.d.ts +8 -0
  187. package/dist/tests/schema-migration.test.d.ts.map +1 -0
  188. package/dist/tests/schema-migration.test.js +108 -0
  189. package/dist/tests/schema-migration.test.js.map +1 -0
  190. package/dist/tests/sql-dump-converters.test.d.ts +7 -0
  191. package/dist/tests/sql-dump-converters.test.d.ts.map +1 -0
  192. package/dist/tests/sql-dump-converters.test.js +314 -0
  193. package/dist/tests/sql-dump-converters.test.js.map +1 -0
  194. package/dist/tests/sql-dump-cross-database.test.d.ts +21 -0
  195. package/dist/tests/sql-dump-cross-database.test.d.ts.map +1 -0
  196. package/dist/tests/sql-dump-cross-database.test.js +314 -0
  197. package/dist/tests/sql-dump-cross-database.test.js.map +1 -0
  198. package/dist/tests/sql-dump-default-conversions.test.d.ts +8 -0
  199. package/dist/tests/sql-dump-default-conversions.test.d.ts.map +1 -0
  200. package/dist/tests/sql-dump-default-conversions.test.js +141 -0
  201. package/dist/tests/sql-dump-default-conversions.test.js.map +1 -0
  202. package/dist/tests/sql-dump-fk-constraints.test.d.ts +13 -0
  203. package/dist/tests/sql-dump-fk-constraints.test.d.ts.map +1 -0
  204. package/dist/tests/sql-dump-fk-constraints.test.js +381 -0
  205. package/dist/tests/sql-dump-fk-constraints.test.js.map +1 -0
  206. package/dist/tests/sql-dump-indexes.test.d.ts +12 -0
  207. package/dist/tests/sql-dump-indexes.test.d.ts.map +1 -0
  208. package/dist/tests/sql-dump-indexes.test.js +269 -0
  209. package/dist/tests/sql-dump-indexes.test.js.map +1 -0
  210. package/dist/tests/sql-dump-integration.test.d.ts +16 -0
  211. package/dist/tests/sql-dump-integration.test.d.ts.map +1 -0
  212. package/dist/tests/sql-dump-integration.test.js +342 -0
  213. package/dist/tests/sql-dump-integration.test.js.map +1 -0
  214. package/dist/tests/sql-dump-table-ordering.test.d.ts +8 -0
  215. package/dist/tests/sql-dump-table-ordering.test.d.ts.map +1 -0
  216. package/dist/tests/sql-dump-table-ordering.test.js +253 -0
  217. package/dist/tests/sql-dump-table-ordering.test.js.map +1 -0
  218. package/dist/tests/tasks.link-file-backward-compat.test.js +11 -1
  219. package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
  220. package/dist/tests/tasks.watch-files-action.test.js +11 -1
  221. package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
  222. package/dist/tests/type-conversion.test.d.ts +8 -0
  223. package/dist/tests/type-conversion.test.d.ts.map +1 -0
  224. package/dist/tests/type-conversion.test.js +312 -0
  225. package/dist/tests/type-conversion.test.js.map +1 -0
  226. package/dist/tests/utils/test-helpers.d.ts +93 -0
  227. package/dist/tests/utils/test-helpers.d.ts.map +1 -0
  228. package/dist/tests/utils/test-helpers.js +407 -0
  229. package/dist/tests/utils/test-helpers.js.map +1 -0
  230. package/dist/tools/config.d.ts +58 -0
  231. package/dist/tools/config.d.ts.map +1 -0
  232. package/dist/tools/config.js +281 -0
  233. package/dist/tools/config.js.map +1 -0
  234. package/dist/tools/constraints.d.ts.map +1 -1
  235. package/dist/tools/constraints.js +138 -122
  236. package/dist/tools/constraints.js.map +1 -1
  237. package/dist/tools/context.d.ts.map +1 -1
  238. package/dist/tools/context.js +216 -109
  239. package/dist/tools/context.js.map +1 -1
  240. package/dist/tools/files.d.ts.map +1 -1
  241. package/dist/tools/files.js +123 -102
  242. package/dist/tools/files.js.map +1 -1
  243. package/dist/tools/tasks.d.ts.map +1 -1
  244. package/dist/tools/tasks.js +583 -521
  245. package/dist/tools/tasks.js.map +1 -1
  246. package/dist/tools/utils.d.ts +5 -0
  247. package/dist/tools/utils.d.ts.map +1 -1
  248. package/dist/tools/utils.js +176 -122
  249. package/dist/tools/utils.js.map +1 -1
  250. package/dist/types.d.ts +9 -26
  251. package/dist/types.d.ts.map +1 -1
  252. package/dist/utils/cleanup.d.ts +3 -0
  253. package/dist/utils/cleanup.d.ts.map +1 -1
  254. package/dist/utils/cleanup.js +14 -2
  255. package/dist/utils/cleanup.js.map +1 -1
  256. package/dist/utils/connection-manager.d.ts +59 -0
  257. package/dist/utils/connection-manager.d.ts.map +1 -0
  258. package/dist/utils/connection-manager.js +178 -0
  259. package/dist/utils/connection-manager.js.map +1 -0
  260. package/dist/utils/debug-logger.d.ts +8 -4
  261. package/dist/utils/debug-logger.d.ts.map +1 -1
  262. package/dist/utils/debug-logger.js +27 -7
  263. package/dist/utils/debug-logger.js.map +1 -1
  264. package/dist/utils/error-handler.d.ts +12 -2
  265. package/dist/utils/error-handler.d.ts.map +1 -1
  266. package/dist/utils/error-handler.js +22 -21
  267. package/dist/utils/error-handler.js.map +1 -1
  268. package/dist/utils/parameter-validator.d.ts.map +1 -1
  269. package/dist/utils/parameter-validator.js +36 -15
  270. package/dist/utils/parameter-validator.js.map +1 -1
  271. package/dist/utils/project-context.d.ts +111 -0
  272. package/dist/utils/project-context.d.ts.map +1 -0
  273. package/dist/utils/project-context.js +187 -0
  274. package/dist/utils/project-context.js.map +1 -0
  275. package/dist/utils/project-root.d.ts +79 -0
  276. package/dist/utils/project-root.d.ts.map +1 -0
  277. package/dist/utils/project-root.js +108 -0
  278. package/dist/utils/project-root.js.map +1 -0
  279. package/dist/utils/sql-dump-converters.d.ts +188 -0
  280. package/dist/utils/sql-dump-converters.d.ts.map +1 -0
  281. package/dist/utils/sql-dump-converters.js +311 -0
  282. package/dist/utils/sql-dump-converters.js.map +1 -0
  283. package/dist/utils/sql-dump.d.ts +102 -0
  284. package/dist/utils/sql-dump.d.ts.map +1 -0
  285. package/dist/utils/sql-dump.js +1550 -0
  286. package/dist/utils/sql-dump.js.map +1 -0
  287. package/dist/utils/vcs-adapter.d.ts +42 -0
  288. package/dist/utils/vcs-adapter.d.ts.map +1 -1
  289. package/dist/utils/vcs-adapter.js +154 -0
  290. package/dist/utils/vcs-adapter.js.map +1 -1
  291. package/dist/watcher/file-watcher.d.ts.map +1 -1
  292. package/dist/watcher/file-watcher.js +48 -49
  293. package/dist/watcher/file-watcher.js.map +1 -1
  294. package/dist/watcher/gitignore-parser.d.ts.map +1 -1
  295. package/dist/watcher/gitignore-parser.js +2 -1
  296. package/dist/watcher/gitignore-parser.js.map +1 -1
  297. package/docs/BASEADAPTER_IMPLEMENTATION.md +399 -0
  298. package/docs/DATABASE_AUTH.md +445 -0
  299. package/docs/DATABASE_MIGRATION.md +247 -0
  300. package/docs/MULTI_PROJECT_ARCHITECTURE.md +497 -0
  301. package/package.json +12 -4
  302. package/dist/migrations/knex/bootstrap/20251025020452_create_master_tables.d.ts.map +0 -1
  303. package/dist/migrations/knex/bootstrap/20251025020452_create_master_tables.js.map +0 -1
  304. package/dist/migrations/knex/bootstrap/20251025021152_create_transaction_tables.d.ts.map +0 -1
  305. package/dist/migrations/knex/bootstrap/20251025021152_create_transaction_tables.js.map +0 -1
  306. package/dist/migrations/knex/bootstrap/20251025021351_create_indexes.d.ts.map +0 -1
  307. package/dist/migrations/knex/bootstrap/20251025021351_create_indexes.js.map +0 -1
  308. package/dist/migrations/knex/bootstrap/20251025021416_seed_master_data.d.ts.map +0 -1
  309. package/dist/migrations/knex/bootstrap/20251025021416_seed_master_data.js.map +0 -1
  310. package/dist/migrations/knex/bootstrap/20251025070349_create_views.d.ts.map +0 -1
  311. package/dist/migrations/knex/bootstrap/20251025070349_create_views.js.map +0 -1
  312. package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts.map +0 -1
  313. package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js +0 -15
  314. package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js.map +0 -1
  315. package/dist/migrations/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts.map +0 -1
  316. package/dist/migrations/knex/enhancements/20251025082220_fix_task_dependencies_columns.js.map +0 -1
  317. package/dist/migrations/knex/enhancements/20251025090000_create_help_system_tables.d.ts.map +0 -1
  318. package/dist/migrations/knex/enhancements/20251025090000_create_help_system_tables.js.map +0 -1
  319. package/dist/migrations/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts.map +0 -1
  320. package/dist/migrations/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js.map +0 -1
  321. package/dist/migrations/knex/enhancements/20251025100000_seed_help_metadata.d.ts.map +0 -1
  322. package/dist/migrations/knex/enhancements/20251025100000_seed_help_metadata.js.map +0 -1
  323. package/dist/migrations/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts.map +0 -1
  324. package/dist/migrations/knex/enhancements/20251025100100_seed_remaining_use_cases.js.map +0 -1
  325. package/dist/migrations/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts.map +0 -1
  326. package/dist/migrations/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js.map +0 -1
  327. package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts.map +0 -1
  328. package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.js +0 -34
  329. package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.js.map +0 -1
  330. package/dist/migrations/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts.map +0 -1
  331. package/dist/migrations/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js.map +0 -1
  332. package/dist/migrations/knex/enhancements/20251027020000_update_agent_reusability.d.ts.map +0 -1
  333. package/dist/migrations/knex/enhancements/20251027020000_update_agent_reusability.js.map +0 -1
  334. package/dist/migrations/knex/enhancements/20251028000000_simplify_agent_system.d.ts.map +0 -1
  335. package/dist/migrations/knex/enhancements/20251028000000_simplify_agent_system.js.map +0 -1
  336. package/dist/migrations/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts.map +0 -1
  337. package/dist/migrations/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js.map +0 -1
  338. package/dist/migrations/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts.map +0 -1
  339. package/dist/migrations/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js.map +0 -1
  340. package/dist/migrations/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts.map +0 -1
  341. package/dist/migrations/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js.map +0 -1
  342. package/dist/migrations/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts.map +0 -1
  343. package/dist/migrations/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js.map +0 -1
  344. package/dist/migrations/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts.map +0 -1
  345. package/dist/migrations/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js.map +0 -1
  346. package/dist/migrations/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts.map +0 -1
  347. package/dist/migrations/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js.map +0 -1
  348. package/dist/migrations/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts.map +0 -1
  349. package/dist/migrations/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js.map +0 -1
  350. /package/dist/{migrations → config}/knex/bootstrap/20251025020452_create_master_tables.d.ts +0 -0
  351. /package/dist/{migrations → config}/knex/bootstrap/20251025021152_create_transaction_tables.d.ts +0 -0
  352. /package/dist/{migrations → config}/knex/bootstrap/20251025021351_create_indexes.d.ts +0 -0
  353. /package/dist/{migrations → config}/knex/bootstrap/20251025021351_create_indexes.js +0 -0
  354. /package/dist/{migrations → config}/knex/bootstrap/20251025021416_seed_master_data.d.ts +0 -0
  355. /package/dist/{migrations → config}/knex/bootstrap/20251025070349_create_views.d.ts +0 -0
  356. /package/dist/{migrations → config}/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts +0 -0
  357. /package/dist/{migrations → config}/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts +0 -0
  358. /package/dist/{migrations → config}/knex/enhancements/20251025082220_fix_task_dependencies_columns.js +0 -0
  359. /package/dist/{migrations → config}/knex/enhancements/20251025090000_create_help_system_tables.d.ts +0 -0
  360. /package/dist/{migrations → config}/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts +0 -0
  361. /package/dist/{migrations → config}/knex/enhancements/20251025100000_seed_help_metadata.d.ts +0 -0
  362. /package/dist/{migrations → config}/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts +0 -0
  363. /package/dist/{migrations → config}/knex/enhancements/20251025100100_seed_remaining_use_cases.js +0 -0
  364. /package/dist/{migrations → config}/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts +0 -0
  365. /package/dist/{migrations → config}/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts +0 -0
  366. /package/dist/{migrations → config}/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts +0 -0
  367. /package/dist/{migrations → config}/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js +0 -0
  368. /package/dist/{migrations → config}/knex/enhancements/20251027020000_update_agent_reusability.d.ts +0 -0
  369. /package/dist/{migrations → config}/knex/enhancements/20251028000000_simplify_agent_system.d.ts +0 -0
  370. /package/dist/{migrations → config}/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts +0 -0
  371. /package/dist/{migrations → config}/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js +0 -0
  372. /package/dist/{migrations → config}/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts +0 -0
  373. /package/dist/{migrations → config}/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js +0 -0
  374. /package/dist/{migrations → config}/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts +0 -0
  375. /package/dist/{migrations → config}/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js +0 -0
  376. /package/dist/{migrations → config}/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts +0 -0
  377. /package/dist/{migrations → config}/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js +0 -0
  378. /package/dist/{migrations → config}/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts +0 -0
  379. /package/dist/{migrations → config}/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js +0 -0
  380. /package/dist/{migrations → config}/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts +0 -0
  381. /package/dist/{migrations → config}/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js +0 -0
  382. /package/dist/{migrations → config}/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts +0 -0
  383. /package/dist/{migrations → config}/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js +0 -0
@@ -5,12 +5,15 @@
5
5
  * CONVERTED: Using Knex.js with DatabaseAdapter (async/await)
6
6
  */
7
7
  import { getAdapter, getOrCreateAgent, getOrCreateTag, getOrCreateContextKey, getLayerId, getOrCreateFile } from '../database.js';
8
+ import { getProjectContext } from '../utils/project-context.js';
8
9
  import { detectAndTransitionStaleTasks, autoArchiveOldDoneTasks, detectAndCompleteReviewedTasks, detectAndArchiveOnCommit } from '../utils/task-stale-detection.js';
9
10
  import { FileWatcher } from '../watcher/index.js';
10
11
  import { validatePriorityRange, validateLength, validateRange } from '../utils/validators.js';
11
12
  import { logTaskCreate, logTaskStatusChange } from '../utils/activity-logging.js';
12
13
  import { parseStringArray } from '../utils/param-parser.js';
13
14
  import { validateActionParams, validateBatchParams } from '../utils/parameter-validator.js';
15
+ import { debugLog } from '../utils/debug-logger.js';
16
+ import connectionManager from '../utils/connection-manager.js';
14
17
  /**
15
18
  * Task status enum (matches m_task_statuses)
16
19
  */
@@ -63,6 +66,8 @@ const VALID_TRANSITIONS = {
63
66
  */
64
67
  async function createTaskInternal(params, adapter, trx) {
65
68
  const knex = trx || adapter.getKnex();
69
+ // Fail-fast project_id validation (Constraint #29)
70
+ const projectId = getProjectContext().getProjectId();
66
71
  // Validate priority
67
72
  const priority = params.priority !== undefined ? params.priority : 2;
68
73
  validatePriorityRange(priority);
@@ -92,6 +97,7 @@ async function createTaskInternal(params, adapter, trx) {
92
97
  // Insert task
93
98
  const now = Math.floor(Date.now() / 1000);
94
99
  const [taskId] = await knex('t_tasks').insert({
100
+ project_id: projectId,
95
101
  title: params.title,
96
102
  status_id: statusId,
97
103
  priority: priority,
@@ -139,6 +145,7 @@ async function createTaskInternal(params, adapter, trx) {
139
145
  // Insert task details if provided
140
146
  if (params.description || acceptanceCriteriaString || acceptanceCriteriaJson || params.notes) {
141
147
  await knex('t_task_details').insert({
148
+ project_id: projectId,
142
149
  task_id: Number(taskId),
143
150
  description: params.description || null,
144
151
  acceptance_criteria: acceptanceCriteriaString,
@@ -185,9 +192,10 @@ async function createTaskInternal(params, adapter, trx) {
185
192
  for (const tagName of tagsParsed) {
186
193
  const tagId = await getOrCreateTag(adapter, tagName, trx);
187
194
  await knex('t_task_tags').insert({
195
+ project_id: projectId,
188
196
  task_id: Number(taskId),
189
197
  tag_id: tagId
190
- }).onConflict(['task_id', 'tag_id']).ignore();
198
+ }).onConflict(['project_id', 'task_id', 'tag_id']).ignore();
191
199
  }
192
200
  }
193
201
  // Activity logging (replaces triggers)
@@ -236,9 +244,10 @@ async function createTaskInternal(params, adapter, trx) {
236
244
  for (const filePath of watchFilesParsed) {
237
245
  const fileId = await getOrCreateFile(adapter, filePath, trx);
238
246
  await knex('t_task_file_links').insert({
247
+ project_id: projectId,
239
248
  task_id: Number(taskId),
240
249
  file_id: fileId
241
- }).onConflict(['task_id', 'file_id']).ignore();
250
+ }).onConflict(['project_id', 'task_id', 'file_id']).ignore();
242
251
  }
243
252
  // Register files with watcher for auto-tracking
244
253
  try {
@@ -249,7 +258,7 @@ async function createTaskInternal(params, adapter, trx) {
249
258
  }
250
259
  catch (error) {
251
260
  // Watcher may not be initialized yet, ignore
252
- console.error('Warning: Could not register files with watcher:', error);
261
+ debugLog('WARN', 'Could not register files with watcher', { error });
253
262
  }
254
263
  }
255
264
  return {
@@ -272,8 +281,10 @@ export async function createTask(params, adapter) {
272
281
  }
273
282
  validateLength(params.title, 'Parameter "title"', 200);
274
283
  try {
275
- return await actualAdapter.transaction(async (trx) => {
276
- return await createTaskInternal(params, actualAdapter, trx);
284
+ return await connectionManager.executeWithRetry(async () => {
285
+ return await actualAdapter.transaction(async (trx) => {
286
+ return await createTaskInternal(params, actualAdapter, trx);
287
+ });
277
288
  });
278
289
  }
279
290
  catch (error) {
@@ -291,181 +302,191 @@ export async function updateTask(params, adapter) {
291
302
  if (!params.task_id) {
292
303
  throw new Error('Parameter "task_id" is required');
293
304
  }
305
+ // Fail-fast project_id validation (Constraint #29)
306
+ const projectId = getProjectContext().getProjectId();
294
307
  try {
295
- return await actualAdapter.transaction(async (trx) => {
296
- const knex = actualAdapter.getKnex();
297
- // Check if task exists
298
- const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
299
- if (!taskExists) {
300
- throw new Error(`Task with id ${params.task_id} not found`);
301
- }
302
- // Build update data dynamically
303
- const updateData = {};
304
- if (params.title !== undefined) {
305
- if (params.title.trim() === '') {
306
- throw new Error('Parameter "title" cannot be empty');
308
+ return await connectionManager.executeWithRetry(async () => {
309
+ return await actualAdapter.transaction(async (trx) => {
310
+ const knex = actualAdapter.getKnex();
311
+ // Check if task exists with project_id isolation
312
+ const taskExists = await trx('t_tasks')
313
+ .where({ id: params.task_id, project_id: projectId })
314
+ .first();
315
+ if (!taskExists) {
316
+ throw new Error(`Task with id ${params.task_id} not found`);
307
317
  }
308
- validateLength(params.title, 'Parameter "title"', 200);
309
- updateData.title = params.title;
310
- }
311
- if (params.priority !== undefined) {
312
- validatePriorityRange(params.priority);
313
- updateData.priority = params.priority;
314
- }
315
- if (params.assigned_agent !== undefined) {
316
- const agentId = await getOrCreateAgent(actualAdapter, params.assigned_agent, trx);
317
- updateData.assigned_agent_id = agentId;
318
- }
319
- if (params.layer !== undefined) {
320
- const layerId = await getLayerId(actualAdapter, params.layer, trx);
321
- if (layerId === null) {
322
- throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
318
+ // Build update data dynamically
319
+ const updateData = {};
320
+ if (params.title !== undefined) {
321
+ if (params.title.trim() === '') {
322
+ throw new Error('Parameter "title" cannot be empty');
323
+ }
324
+ validateLength(params.title, 'Parameter "title"', 200);
325
+ updateData.title = params.title;
323
326
  }
324
- updateData.layer_id = layerId;
325
- }
326
- // Update t_tasks if any updates
327
- if (Object.keys(updateData).length > 0) {
328
- await trx('t_tasks')
329
- .where({ id: params.task_id })
330
- .update(updateData);
331
- // TODO: Add activity logging for updates if needed
332
- }
333
- // Update t_task_details if any detail fields provided
334
- if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
335
- // Process acceptance_criteria (can be string or array)
336
- let acceptanceCriteriaString = undefined;
337
- let acceptanceCriteriaJson = undefined;
338
- if (params.acceptance_criteria !== undefined) {
339
- if (Array.isArray(params.acceptance_criteria)) {
340
- // Array format - store as JSON in acceptance_criteria_json
341
- acceptanceCriteriaJson = JSON.stringify(params.acceptance_criteria);
342
- // Also create human-readable summary in acceptance_criteria
343
- acceptanceCriteriaString = params.acceptance_criteria
344
- .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
345
- .join('\n');
327
+ if (params.priority !== undefined) {
328
+ validatePriorityRange(params.priority);
329
+ updateData.priority = params.priority;
330
+ }
331
+ if (params.assigned_agent !== undefined) {
332
+ const agentId = await getOrCreateAgent(actualAdapter, params.assigned_agent, trx);
333
+ updateData.assigned_agent_id = agentId;
334
+ }
335
+ if (params.layer !== undefined) {
336
+ const layerId = await getLayerId(actualAdapter, params.layer, trx);
337
+ if (layerId === null) {
338
+ throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
346
339
  }
347
- else if (typeof params.acceptance_criteria === 'string') {
348
- // Try to parse as JSON first
349
- try {
350
- const parsed = JSON.parse(params.acceptance_criteria);
351
- if (Array.isArray(parsed)) {
352
- // It's a JSON array string - store in JSON field
353
- acceptanceCriteriaJson = params.acceptance_criteria;
354
- // Also create human-readable summary
355
- acceptanceCriteriaString = parsed
356
- .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
357
- .join('\n');
340
+ updateData.layer_id = layerId;
341
+ }
342
+ // Update t_tasks if any updates (with project_id isolation)
343
+ if (Object.keys(updateData).length > 0) {
344
+ await trx('t_tasks')
345
+ .where({ id: params.task_id, project_id: projectId })
346
+ .update(updateData);
347
+ // TODO: Add activity logging for updates if needed
348
+ }
349
+ // Update t_task_details if any detail fields provided
350
+ if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
351
+ // Process acceptance_criteria (can be string or array)
352
+ let acceptanceCriteriaString = undefined;
353
+ let acceptanceCriteriaJson = undefined;
354
+ if (params.acceptance_criteria !== undefined) {
355
+ if (Array.isArray(params.acceptance_criteria)) {
356
+ // Array format - store as JSON in acceptance_criteria_json
357
+ acceptanceCriteriaJson = JSON.stringify(params.acceptance_criteria);
358
+ // Also create human-readable summary in acceptance_criteria
359
+ acceptanceCriteriaString = params.acceptance_criteria
360
+ .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
361
+ .join('\n');
362
+ }
363
+ else if (typeof params.acceptance_criteria === 'string') {
364
+ // Try to parse as JSON first
365
+ try {
366
+ const parsed = JSON.parse(params.acceptance_criteria);
367
+ if (Array.isArray(parsed)) {
368
+ // It's a JSON array string - store in JSON field
369
+ acceptanceCriteriaJson = params.acceptance_criteria;
370
+ // Also create human-readable summary
371
+ acceptanceCriteriaString = parsed
372
+ .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
373
+ .join('\n');
374
+ }
375
+ else {
376
+ // Valid JSON but not an array - store as plain text
377
+ acceptanceCriteriaString = params.acceptance_criteria || null;
378
+ acceptanceCriteriaJson = null;
379
+ }
358
380
  }
359
- else {
360
- // Valid JSON but not an array - store as plain text
381
+ catch {
382
+ // Not valid JSON - store as plain text
361
383
  acceptanceCriteriaString = params.acceptance_criteria || null;
362
384
  acceptanceCriteriaJson = null;
363
385
  }
364
386
  }
365
- catch {
366
- // Not valid JSON - store as plain text
367
- acceptanceCriteriaString = params.acceptance_criteria || null;
368
- acceptanceCriteriaJson = null;
369
- }
370
387
  }
371
- }
372
- // Check if details exist
373
- const detailsExist = await trx('t_task_details').where({ task_id: params.task_id }).first();
374
- const detailsUpdate = {};
375
- if (params.description !== undefined) {
376
- detailsUpdate.description = params.description || null;
377
- }
378
- if (acceptanceCriteriaString !== undefined) {
379
- detailsUpdate.acceptance_criteria = acceptanceCriteriaString;
380
- }
381
- if (acceptanceCriteriaJson !== undefined) {
382
- detailsUpdate.acceptance_criteria_json = acceptanceCriteriaJson;
383
- }
384
- if (params.notes !== undefined) {
385
- detailsUpdate.notes = params.notes || null;
386
- }
387
- if (detailsExist && Object.keys(detailsUpdate).length > 0) {
388
- // Update existing details
389
- await trx('t_task_details')
390
- .where({ task_id: params.task_id })
391
- .update(detailsUpdate);
392
- }
393
- else if (!detailsExist) {
394
- // Insert new details
395
- await trx('t_task_details').insert({
396
- task_id: params.task_id,
397
- description: params.description || null,
398
- acceptance_criteria: acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null,
399
- acceptance_criteria_json: acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null,
400
- notes: params.notes || null
401
- });
402
- }
403
- }
404
- // Handle watch_files if provided (v3.4.1)
405
- if (params.watch_files && params.watch_files.length > 0) {
406
- // Parse watch_files - handle MCP SDK converting JSON string to char array
407
- let watchFilesParsed;
408
- if (typeof params.watch_files === 'string') {
409
- // String - try to parse as JSON
410
- try {
411
- watchFilesParsed = JSON.parse(params.watch_files);
388
+ // Check if details exist (with project_id isolation)
389
+ const detailsExist = await trx('t_task_details')
390
+ .where({ task_id: params.task_id, project_id: projectId })
391
+ .first();
392
+ const detailsUpdate = {};
393
+ if (params.description !== undefined) {
394
+ detailsUpdate.description = params.description || null;
395
+ }
396
+ if (acceptanceCriteriaString !== undefined) {
397
+ detailsUpdate.acceptance_criteria = acceptanceCriteriaString;
398
+ }
399
+ if (acceptanceCriteriaJson !== undefined) {
400
+ detailsUpdate.acceptance_criteria_json = acceptanceCriteriaJson;
401
+ }
402
+ if (params.notes !== undefined) {
403
+ detailsUpdate.notes = params.notes || null;
412
404
  }
413
- catch {
414
- // If not valid JSON, treat as single file path
415
- watchFilesParsed = [params.watch_files];
405
+ if (detailsExist && Object.keys(detailsUpdate).length > 0) {
406
+ // Update existing details (with project_id isolation)
407
+ await trx('t_task_details')
408
+ .where({ task_id: params.task_id, project_id: projectId })
409
+ .update(detailsUpdate);
410
+ }
411
+ else if (!detailsExist) {
412
+ // Insert new details
413
+ await trx('t_task_details').insert({
414
+ project_id: projectId,
415
+ task_id: params.task_id,
416
+ description: params.description || null,
417
+ acceptance_criteria: acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null,
418
+ acceptance_criteria_json: acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null,
419
+ notes: params.notes || null
420
+ });
416
421
  }
417
422
  }
418
- else if (Array.isArray(params.watch_files)) {
419
- // Check if it's an array of single characters (MCP SDK bug)
420
- if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
421
- // Join characters back into string and parse JSON
422
- const jsonString = params.watch_files.join('');
423
+ // Handle watch_files if provided (v3.4.1)
424
+ if (params.watch_files && params.watch_files.length > 0) {
425
+ // Parse watch_files - handle MCP SDK converting JSON string to char array
426
+ let watchFilesParsed;
427
+ if (typeof params.watch_files === 'string') {
428
+ // String - try to parse as JSON
423
429
  try {
424
- watchFilesParsed = JSON.parse(jsonString);
430
+ watchFilesParsed = JSON.parse(params.watch_files);
425
431
  }
426
432
  catch {
427
- throw new Error(`Invalid watch_files format: ${jsonString}`);
433
+ // If not valid JSON, treat as single file path
434
+ watchFilesParsed = [params.watch_files];
435
+ }
436
+ }
437
+ else if (Array.isArray(params.watch_files)) {
438
+ // Check if it's an array of single characters (MCP SDK bug)
439
+ if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
440
+ // Join characters back into string and parse JSON
441
+ const jsonString = params.watch_files.join('');
442
+ try {
443
+ watchFilesParsed = JSON.parse(jsonString);
444
+ }
445
+ catch {
446
+ throw new Error(`Invalid watch_files format: ${jsonString}`);
447
+ }
448
+ }
449
+ else {
450
+ // Normal array of file paths
451
+ watchFilesParsed = params.watch_files;
428
452
  }
429
453
  }
430
454
  else {
431
- // Normal array of file paths
432
- watchFilesParsed = params.watch_files;
455
+ throw new Error('Parameter "watch_files" must be a string or array');
433
456
  }
434
- }
435
- else {
436
- throw new Error('Parameter "watch_files" must be a string or array');
437
- }
438
- for (const filePath of watchFilesParsed) {
439
- const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
440
- await trx('t_task_file_links').insert({
441
- task_id: params.task_id,
442
- file_id: fileId
443
- }).onConflict(['task_id', 'file_id']).ignore();
444
- }
445
- // Register files with watcher for auto-tracking
446
- try {
447
- const taskData = await trx('t_tasks as t')
448
- .join('m_task_statuses as s', 't.status_id', 's.id')
449
- .where('t.id', params.task_id)
450
- .select('t.title', 's.name as status')
451
- .first();
452
- if (taskData) {
453
- const watcher = FileWatcher.getInstance();
454
- for (const filePath of watchFilesParsed) {
455
- watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
457
+ for (const filePath of watchFilesParsed) {
458
+ const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
459
+ await trx('t_task_file_links').insert({
460
+ project_id: projectId,
461
+ task_id: params.task_id,
462
+ file_id: fileId
463
+ }).onConflict(['project_id', 'task_id', 'file_id']).ignore();
464
+ }
465
+ // Register files with watcher for auto-tracking
466
+ try {
467
+ const taskData = await trx('t_tasks as t')
468
+ .join('m_task_statuses as s', 't.status_id', 's.id')
469
+ .where({ 't.id': params.task_id, 't.project_id': projectId })
470
+ .select('t.title', 's.name as status')
471
+ .first();
472
+ if (taskData) {
473
+ const watcher = FileWatcher.getInstance();
474
+ for (const filePath of watchFilesParsed) {
475
+ watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
476
+ }
456
477
  }
457
478
  }
479
+ catch (error) {
480
+ // Watcher may not be initialized yet, ignore
481
+ debugLog('WARN', 'Could not register files with watcher', { error });
482
+ }
458
483
  }
459
- catch (error) {
460
- // Watcher may not be initialized yet, ignore
461
- console.error('Warning: Could not register files with watcher:', error);
462
- }
463
- }
464
- return {
465
- success: true,
466
- task_id: params.task_id,
467
- message: `Task ${params.task_id} updated successfully`
468
- };
484
+ return {
485
+ success: true,
486
+ task_id: params.task_id,
487
+ message: `Task ${params.task_id} updated successfully`
488
+ };
489
+ });
469
490
  });
470
491
  }
471
492
  catch (error) {
@@ -478,6 +499,7 @@ export async function updateTask(params, adapter) {
478
499
  */
479
500
  async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
480
501
  const knex = adapter.getKnex();
502
+ const projectId = getProjectContext().getProjectId();
481
503
  // Build query based on include_details flag
482
504
  const selectFields = includeDetails
483
505
  ? [
@@ -496,26 +518,34 @@ async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
496
518
  's.name as status',
497
519
  't.priority'
498
520
  ];
499
- // Get blockers (tasks that block this task)
521
+ // Get blockers (tasks that block this task) - with project_id isolation
500
522
  let blockersQuery = knex('t_tasks as t')
501
523
  .join('t_task_dependencies as d', 't.id', 'd.blocker_task_id')
502
524
  .leftJoin('m_task_statuses as s', 't.status_id', 's.id')
503
525
  .leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
504
- .where('d.blocked_task_id', taskId)
526
+ .where({ 'd.blocked_task_id': taskId, 'd.project_id': projectId, 't.project_id': projectId })
505
527
  .select(selectFields);
506
528
  if (includeDetails) {
507
- blockersQuery = blockersQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
529
+ blockersQuery = blockersQuery
530
+ .leftJoin('t_task_details as td', function () {
531
+ this.on('t.id', '=', 'td.task_id')
532
+ .andOn('t.project_id', '=', 'td.project_id');
533
+ });
508
534
  }
509
535
  const blockers = await blockersQuery;
510
- // Get blocking (tasks this task blocks)
536
+ // Get blocking (tasks this task blocks) - with project_id isolation
511
537
  let blockingQuery = knex('t_tasks as t')
512
538
  .join('t_task_dependencies as d', 't.id', 'd.blocked_task_id')
513
539
  .leftJoin('m_task_statuses as s', 't.status_id', 's.id')
514
540
  .leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
515
- .where('d.blocker_task_id', taskId)
541
+ .where({ 'd.blocker_task_id': taskId, 'd.project_id': projectId, 't.project_id': projectId })
516
542
  .select(selectFields);
517
543
  if (includeDetails) {
518
- blockingQuery = blockingQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
544
+ blockingQuery = blockingQuery
545
+ .leftJoin('t_task_details as td', function () {
546
+ this.on('t.id', '=', 'td.task_id')
547
+ .andOn('t.project_id', '=', 'td.project_id');
548
+ });
519
549
  }
520
550
  const blocking = await blockingQuery;
521
551
  return { blockers, blocking };
@@ -530,15 +560,20 @@ export async function getTask(params, adapter) {
530
560
  if (!params.task_id) {
531
561
  throw new Error('Parameter "task_id" is required');
532
562
  }
563
+ // Fail-fast project_id validation (Constraint #29)
564
+ const projectId = getProjectContext().getProjectId();
533
565
  try {
534
- // Get task with details
566
+ // Get task with details (with project_id isolation)
535
567
  const task = await knex('t_tasks as t')
536
568
  .leftJoin('m_task_statuses as s', 't.status_id', 's.id')
537
569
  .leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
538
570
  .leftJoin('m_agents as ca', 't.created_by_agent_id', 'ca.id')
539
571
  .leftJoin('m_layers as l', 't.layer_id', 'l.id')
540
- .leftJoin('t_task_details as td', 't.id', 'td.task_id')
541
- .where('t.id', params.task_id)
572
+ .leftJoin('t_task_details as td', function () {
573
+ this.on('t.id', '=', 'td.task_id')
574
+ .andOn('t.project_id', '=', 'td.project_id');
575
+ })
576
+ .where({ 't.id': params.task_id, 't.project_id': projectId })
542
577
  .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')
543
578
  .first();
544
579
  if (!task) {
@@ -602,6 +637,8 @@ export async function listTasks(params = {}, adapter) {
602
637
  validateActionParams('task', 'list', params);
603
638
  const actualAdapter = adapter ?? getAdapter();
604
639
  const knex = actualAdapter.getKnex();
640
+ // Get current project ID for filtering (Constraint #22)
641
+ const projectId = getProjectContext().getProjectId();
605
642
  try {
606
643
  // Run auto-stale detection, git-aware completion, and auto-archive before listing
607
644
  const transitionCount = await detectAndTransitionStaleTasks(actualAdapter);
@@ -631,6 +668,8 @@ export async function listTasks(params = {}, adapter) {
631
668
  // Standard query without dependency counts
632
669
  query = knex('v_task_board');
633
670
  }
671
+ // Filter by project_id (Constraint #22: Multi-project isolation)
672
+ query = query.where(params.include_dependency_counts ? 'vt.project_id' : 'project_id', projectId);
634
673
  // Filter by status
635
674
  if (params.status) {
636
675
  if (!STATUS_TO_ID[params.status]) {
@@ -695,64 +734,66 @@ export async function moveTask(params, adapter) {
695
734
  // Run auto-stale detection and auto-archive before move
696
735
  await detectAndTransitionStaleTasks(actualAdapter);
697
736
  await autoArchiveOldDoneTasks(actualAdapter);
698
- return await actualAdapter.transaction(async (trx) => {
699
- // Get current status
700
- const taskRow = await trx('t_tasks')
701
- .where({ id: params.task_id })
702
- .select('status_id')
703
- .first();
704
- if (!taskRow) {
705
- throw new Error(`Task with id ${params.task_id} not found`);
706
- }
707
- const currentStatusId = taskRow.status_id;
708
- const newStatusId = STATUS_TO_ID[params.new_status];
709
- if (!newStatusId) {
710
- throw new Error(`Invalid new_status: ${params.new_status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
711
- }
712
- // Check if transition is valid
713
- const validNextStatuses = VALID_TRANSITIONS[currentStatusId] || [];
714
- if (!validNextStatuses.includes(newStatusId)) {
715
- throw new Error(`Invalid transition from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}. ` +
716
- `Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
717
- }
718
- // Update status
719
- const updateData = {
720
- status_id: newStatusId
721
- };
722
- // Set completed_ts when moving to done
723
- if (newStatusId === TASK_STATUS.DONE) {
724
- updateData.completed_ts = Math.floor(Date.now() / 1000);
725
- }
726
- await trx('t_tasks')
727
- .where({ id: params.task_id })
728
- .update(updateData);
729
- // Activity logging (replaces trigger)
730
- // Note: Using system agent (id=1) for status changes
731
- // In a real implementation, you'd pass the actual agent_id who made the change
732
- const systemAgentId = 1;
733
- await logTaskStatusChange(trx, {
734
- task_id: params.task_id,
735
- old_status: currentStatusId,
736
- new_status: newStatusId,
737
- agent_id: systemAgentId
738
- });
739
- // Update watcher if moving to done or archived (stop watching)
740
- if (params.new_status === 'done' || params.new_status === 'archived') {
741
- try {
742
- const watcher = FileWatcher.getInstance();
743
- watcher.unregisterTask(params.task_id);
737
+ return await connectionManager.executeWithRetry(async () => {
738
+ return await actualAdapter.transaction(async (trx) => {
739
+ // Get current status
740
+ const taskRow = await trx('t_tasks')
741
+ .where({ id: params.task_id })
742
+ .select('status_id')
743
+ .first();
744
+ if (!taskRow) {
745
+ throw new Error(`Task with id ${params.task_id} not found`);
744
746
  }
745
- catch (error) {
746
- // Watcher may not be initialized, ignore
747
+ const currentStatusId = taskRow.status_id;
748
+ const newStatusId = STATUS_TO_ID[params.new_status];
749
+ if (!newStatusId) {
750
+ throw new Error(`Invalid new_status: ${params.new_status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
747
751
  }
748
- }
749
- return {
750
- success: true,
751
- task_id: params.task_id,
752
- old_status: ID_TO_STATUS[currentStatusId],
753
- new_status: params.new_status,
754
- message: `Task ${params.task_id} moved from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}`
755
- };
752
+ // Check if transition is valid
753
+ const validNextStatuses = VALID_TRANSITIONS[currentStatusId] || [];
754
+ if (!validNextStatuses.includes(newStatusId)) {
755
+ throw new Error(`Invalid transition from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}. ` +
756
+ `Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
757
+ }
758
+ // Update status
759
+ const updateData = {
760
+ status_id: newStatusId
761
+ };
762
+ // Set completed_ts when moving to done
763
+ if (newStatusId === TASK_STATUS.DONE) {
764
+ updateData.completed_ts = Math.floor(Date.now() / 1000);
765
+ }
766
+ await trx('t_tasks')
767
+ .where({ id: params.task_id })
768
+ .update(updateData);
769
+ // Activity logging (replaces trigger)
770
+ // Note: Using system agent (id=1) for status changes
771
+ // In a real implementation, you'd pass the actual agent_id who made the change
772
+ const systemAgentId = 1;
773
+ await logTaskStatusChange(trx, {
774
+ task_id: params.task_id,
775
+ old_status: currentStatusId,
776
+ new_status: newStatusId,
777
+ agent_id: systemAgentId
778
+ });
779
+ // Update watcher if moving to done or archived (stop watching)
780
+ if (params.new_status === 'done' || params.new_status === 'archived') {
781
+ try {
782
+ const watcher = FileWatcher.getInstance();
783
+ watcher.unregisterTask(params.task_id);
784
+ }
785
+ catch (error) {
786
+ // Watcher may not be initialized, ignore
787
+ }
788
+ }
789
+ return {
790
+ success: true,
791
+ task_id: params.task_id,
792
+ old_status: ID_TO_STATUS[currentStatusId],
793
+ new_status: params.new_status,
794
+ message: `Task ${params.task_id} moved from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}`
795
+ };
796
+ });
756
797
  });
757
798
  }
758
799
  catch (error) {
@@ -777,88 +818,88 @@ export async function linkTask(params, adapter) {
777
818
  throw new Error('Parameter "target_id" is required');
778
819
  }
779
820
  try {
780
- return await actualAdapter.transaction(async (trx) => {
781
- // Check if task exists
782
- const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
783
- if (!taskExists) {
784
- throw new Error(`Task with id ${params.task_id} not found`);
785
- }
786
- if (params.link_type === 'decision') {
787
- const decisionKey = String(params.target_id);
788
- const keyId = await getOrCreateContextKey(actualAdapter, decisionKey, trx);
789
- const linkRelation = params.link_relation || 'implements';
790
- await trx('t_task_decision_links').insert({
791
- task_id: params.task_id,
792
- decision_key_id: keyId,
793
- link_type: linkRelation
794
- }).onConflict(['task_id', 'decision_key_id']).merge();
795
- return {
796
- success: true,
797
- task_id: params.task_id,
798
- linked_to: 'decision',
799
- target: decisionKey,
800
- relation: linkRelation,
801
- message: `Task ${params.task_id} linked to decision "${decisionKey}"`
802
- };
803
- }
804
- else if (params.link_type === 'constraint') {
805
- const constraintId = Number(params.target_id);
806
- // Check if constraint exists
807
- const constraintExists = await trx('t_constraints').where({ id: constraintId }).first();
808
- if (!constraintExists) {
809
- throw new Error(`Constraint with id ${constraintId} not found`);
821
+ return await connectionManager.executeWithRetry(async () => {
822
+ return await actualAdapter.transaction(async (trx) => {
823
+ // Check if task exists
824
+ const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
825
+ if (!taskExists) {
826
+ throw new Error(`Task with id ${params.task_id} not found`);
810
827
  }
811
- await trx('t_task_constraint_links').insert({
812
- task_id: params.task_id,
813
- constraint_id: constraintId
814
- }).onConflict(['task_id', 'constraint_id']).ignore();
815
- return {
816
- success: true,
817
- task_id: params.task_id,
818
- linked_to: 'constraint',
819
- target: constraintId,
820
- message: `Task ${params.task_id} linked to constraint ${constraintId}`
821
- };
822
- }
823
- else if (params.link_type === 'file') {
824
- // Deprecation warning (v3.4.1)
825
- console.warn(`⚠️ DEPRECATION WARNING: task.link(link_type="file") is deprecated as of v3.4.1.`);
826
- console.warn(` Use task.create(watch_files=[...]) or task.update(watch_files=[...]) instead.`);
827
- console.warn(` Or use the new watch_files action: { action: "watch_files", task_id: ${params.task_id}, file_paths: ["..."] }`);
828
- const filePath = String(params.target_id);
829
- const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
830
- await trx('t_task_file_links').insert({
831
- task_id: params.task_id,
832
- file_id: fileId
833
- }).onConflict(['task_id', 'file_id']).ignore();
834
- // Register file with watcher for auto-tracking
835
- try {
836
- const taskData = await trx('t_tasks as t')
837
- .join('m_task_statuses as s', 't.status_id', 's.id')
838
- .where('t.id', params.task_id)
839
- .select('t.title', 's.name as status')
840
- .first();
841
- if (taskData) {
842
- const watcher = FileWatcher.getInstance();
843
- watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
828
+ if (params.link_type === 'decision') {
829
+ const decisionKey = String(params.target_id);
830
+ const keyId = await getOrCreateContextKey(actualAdapter, decisionKey, trx);
831
+ const linkRelation = params.link_relation || 'implements';
832
+ await trx('t_task_decision_links').insert({
833
+ task_id: params.task_id,
834
+ decision_key_id: keyId,
835
+ link_type: linkRelation
836
+ }).onConflict(['task_id', 'decision_key_id']).merge();
837
+ return {
838
+ success: true,
839
+ task_id: params.task_id,
840
+ linked_to: 'decision',
841
+ target: decisionKey,
842
+ relation: linkRelation,
843
+ message: `Task ${params.task_id} linked to decision "${decisionKey}"`
844
+ };
845
+ }
846
+ else if (params.link_type === 'constraint') {
847
+ const constraintId = Number(params.target_id);
848
+ // Check if constraint exists
849
+ const constraintExists = await trx('t_constraints').where({ id: constraintId }).first();
850
+ if (!constraintExists) {
851
+ throw new Error(`Constraint with id ${constraintId} not found`);
852
+ }
853
+ await trx('t_task_constraint_links').insert({
854
+ task_id: params.task_id,
855
+ constraint_id: constraintId
856
+ }).onConflict(['task_id', 'constraint_id']).ignore();
857
+ return {
858
+ success: true,
859
+ task_id: params.task_id,
860
+ linked_to: 'constraint',
861
+ target: constraintId,
862
+ message: `Task ${params.task_id} linked to constraint ${constraintId}`
863
+ };
864
+ }
865
+ else if (params.link_type === 'file') {
866
+ // Deprecation warning (v3.4.1)
867
+ debugLog('WARN', `DEPRECATION: task.link(link_type="file") is deprecated as of v3.4.1. Use task.create(watch_files=[...]) or task.update(watch_files=[...]) instead. Or use the new watch_files action: { action: "watch_files", task_id: ${params.task_id}, file_paths: ["..."] }`);
868
+ const filePath = String(params.target_id);
869
+ const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
870
+ await trx('t_task_file_links').insert({
871
+ task_id: params.task_id,
872
+ file_id: fileId
873
+ }).onConflict(['task_id', 'file_id']).ignore();
874
+ // Register file with watcher for auto-tracking
875
+ try {
876
+ const taskData = await trx('t_tasks as t')
877
+ .join('m_task_statuses as s', 't.status_id', 's.id')
878
+ .where('t.id', params.task_id)
879
+ .select('t.title', 's.name as status')
880
+ .first();
881
+ if (taskData) {
882
+ const watcher = FileWatcher.getInstance();
883
+ watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
884
+ }
885
+ }
886
+ catch (error) {
887
+ // Watcher may not be initialized yet, ignore
888
+ debugLog('WARN', 'Could not register file with watcher', { error });
844
889
  }
890
+ return {
891
+ success: true,
892
+ task_id: params.task_id,
893
+ linked_to: 'file',
894
+ target: filePath,
895
+ deprecation_warning: 'task.link(link_type="file") is deprecated. Use task.create/update(watch_files) or watch_files action instead.',
896
+ message: `Task ${params.task_id} linked to file "${filePath}" (DEPRECATED API - use watch_files instead)`
897
+ };
845
898
  }
846
- catch (error) {
847
- // Watcher may not be initialized yet, ignore
848
- console.error('Warning: Could not register file with watcher:', error);
899
+ else {
900
+ throw new Error(`Invalid link_type: ${params.link_type}. Must be one of: decision, constraint, file`);
849
901
  }
850
- return {
851
- success: true,
852
- task_id: params.task_id,
853
- linked_to: 'file',
854
- target: filePath,
855
- deprecation_warning: 'task.link(link_type="file") is deprecated. Use task.create/update(watch_files) or watch_files action instead.',
856
- message: `Task ${params.task_id} linked to file "${filePath}" (DEPRECATED API - use watch_files instead)`
857
- };
858
- }
859
- else {
860
- throw new Error(`Invalid link_type: ${params.link_type}. Must be one of: decision, constraint, file`);
861
- }
902
+ });
862
903
  });
863
904
  }
864
905
  catch (error) {
@@ -877,44 +918,46 @@ export async function archiveTask(params, adapter) {
877
918
  throw new Error('Parameter "task_id" is required');
878
919
  }
879
920
  try {
880
- return await actualAdapter.transaction(async (trx) => {
881
- // Check if task is in 'done' status
882
- const taskRow = await trx('t_tasks')
883
- .where({ id: params.task_id })
884
- .select('status_id')
885
- .first();
886
- if (!taskRow) {
887
- throw new Error(`Task with id ${params.task_id} not found`);
888
- }
889
- if (taskRow.status_id !== TASK_STATUS.DONE) {
890
- throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
891
- }
892
- // Update to archived
893
- await trx('t_tasks')
894
- .where({ id: params.task_id })
895
- .update({ status_id: TASK_STATUS.ARCHIVED });
896
- // Activity logging
897
- // Note: Using system agent (id=1) for status changes
898
- const systemAgentId = 1;
899
- await logTaskStatusChange(trx, {
900
- task_id: params.task_id,
901
- old_status: TASK_STATUS.DONE,
902
- new_status: TASK_STATUS.ARCHIVED,
903
- agent_id: systemAgentId
921
+ return await connectionManager.executeWithRetry(async () => {
922
+ return await actualAdapter.transaction(async (trx) => {
923
+ // Check if task is in 'done' status
924
+ const taskRow = await trx('t_tasks')
925
+ .where({ id: params.task_id })
926
+ .select('status_id')
927
+ .first();
928
+ if (!taskRow) {
929
+ throw new Error(`Task with id ${params.task_id} not found`);
930
+ }
931
+ if (taskRow.status_id !== TASK_STATUS.DONE) {
932
+ throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
933
+ }
934
+ // Update to archived
935
+ await trx('t_tasks')
936
+ .where({ id: params.task_id })
937
+ .update({ status_id: TASK_STATUS.ARCHIVED });
938
+ // Activity logging
939
+ // Note: Using system agent (id=1) for status changes
940
+ const systemAgentId = 1;
941
+ await logTaskStatusChange(trx, {
942
+ task_id: params.task_id,
943
+ old_status: TASK_STATUS.DONE,
944
+ new_status: TASK_STATUS.ARCHIVED,
945
+ agent_id: systemAgentId
946
+ });
947
+ // Unregister from file watcher (archived tasks don't need tracking)
948
+ try {
949
+ const watcher = FileWatcher.getInstance();
950
+ watcher.unregisterTask(params.task_id);
951
+ }
952
+ catch (error) {
953
+ // Watcher may not be initialized, ignore
954
+ }
955
+ return {
956
+ success: true,
957
+ task_id: params.task_id,
958
+ message: `Task ${params.task_id} archived successfully`
959
+ };
904
960
  });
905
- // Unregister from file watcher (archived tasks don't need tracking)
906
- try {
907
- const watcher = FileWatcher.getInstance();
908
- watcher.unregisterTask(params.task_id);
909
- }
910
- catch (error) {
911
- // Watcher may not be initialized, ignore
912
- }
913
- return {
914
- success: true,
915
- task_id: params.task_id,
916
- message: `Task ${params.task_id} archived successfully`
917
- };
918
961
  });
919
962
  }
920
963
  catch (error) {
@@ -936,95 +979,97 @@ export async function addDependency(params, adapter) {
936
979
  throw new Error('Parameter "blocked_task_id" is required');
937
980
  }
938
981
  try {
939
- return await actualAdapter.transaction(async (trx) => {
940
- // Validation 1: No self-dependencies
941
- if (params.blocker_task_id === params.blocked_task_id) {
942
- throw new Error('Self-dependency not allowed');
943
- }
944
- // Validation 2: Both tasks must exist and check if archived
945
- const blockerTask = await trx('t_tasks')
946
- .where({ id: params.blocker_task_id })
947
- .select('id', 'status_id')
948
- .first();
949
- const blockedTask = await trx('t_tasks')
950
- .where({ id: params.blocked_task_id })
951
- .select('id', 'status_id')
952
- .first();
953
- if (!blockerTask) {
954
- throw new Error(`Blocker task #${params.blocker_task_id} not found`);
955
- }
956
- if (!blockedTask) {
957
- throw new Error(`Blocked task #${params.blocked_task_id} not found`);
958
- }
959
- // Validation 3: Neither task is archived
960
- if (blockerTask.status_id === TASK_STATUS.ARCHIVED) {
961
- throw new Error(`Cannot add dependency: Task #${params.blocker_task_id} is archived`);
962
- }
963
- if (blockedTask.status_id === TASK_STATUS.ARCHIVED) {
964
- throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
965
- }
966
- // Validation 4: No direct circular (reverse relationship)
967
- const reverseExists = await trx('t_task_dependencies')
968
- .where({
969
- blocker_task_id: params.blocked_task_id,
970
- blocked_task_id: params.blocker_task_id
971
- })
972
- .first();
973
- if (reverseExists) {
974
- throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
975
- }
976
- // Validation 5: No transitive circular (check if adding this would create a cycle)
977
- const cycleCheck = await trx.raw(`
978
- WITH RECURSIVE dependency_chain AS (
979
- -- Start from the task that would be blocked
980
- SELECT blocked_task_id as task_id, 1 as depth
981
- FROM t_task_dependencies
982
- WHERE blocker_task_id = ?
983
-
984
- UNION ALL
985
-
986
- -- Follow the chain of dependencies
987
- SELECT d.blocked_task_id, dc.depth + 1
988
- FROM t_task_dependencies d
989
- JOIN dependency_chain dc ON d.blocker_task_id = dc.task_id
990
- WHERE dc.depth < 100
991
- )
992
- SELECT task_id FROM dependency_chain WHERE task_id = ?
993
- `, [params.blocked_task_id, params.blocker_task_id])
994
- .then((result) => result[0]);
995
- if (cycleCheck) {
996
- // Build cycle path for error message
997
- const cyclePathResult = await trx.raw(`
982
+ return await connectionManager.executeWithRetry(async () => {
983
+ return await actualAdapter.transaction(async (trx) => {
984
+ // Validation 1: No self-dependencies
985
+ if (params.blocker_task_id === params.blocked_task_id) {
986
+ throw new Error('Self-dependency not allowed');
987
+ }
988
+ // Validation 2: Both tasks must exist and check if archived
989
+ const blockerTask = await trx('t_tasks')
990
+ .where({ id: params.blocker_task_id })
991
+ .select('id', 'status_id')
992
+ .first();
993
+ const blockedTask = await trx('t_tasks')
994
+ .where({ id: params.blocked_task_id })
995
+ .select('id', 'status_id')
996
+ .first();
997
+ if (!blockerTask) {
998
+ throw new Error(`Blocker task #${params.blocker_task_id} not found`);
999
+ }
1000
+ if (!blockedTask) {
1001
+ throw new Error(`Blocked task #${params.blocked_task_id} not found`);
1002
+ }
1003
+ // Validation 3: Neither task is archived
1004
+ if (blockerTask.status_id === TASK_STATUS.ARCHIVED) {
1005
+ throw new Error(`Cannot add dependency: Task #${params.blocker_task_id} is archived`);
1006
+ }
1007
+ if (blockedTask.status_id === TASK_STATUS.ARCHIVED) {
1008
+ throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
1009
+ }
1010
+ // Validation 4: No direct circular (reverse relationship)
1011
+ const reverseExists = await trx('t_task_dependencies')
1012
+ .where({
1013
+ blocker_task_id: params.blocked_task_id,
1014
+ blocked_task_id: params.blocker_task_id
1015
+ })
1016
+ .first();
1017
+ if (reverseExists) {
1018
+ throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
1019
+ }
1020
+ // Validation 5: No transitive circular (check if adding this would create a cycle)
1021
+ const cycleCheck = await trx.raw(`
998
1022
  WITH RECURSIVE dependency_chain AS (
999
- SELECT blocked_task_id as task_id, 1 as depth,
1000
- CAST(blocked_task_id AS TEXT) as path
1023
+ -- Start from the task that would be blocked
1024
+ SELECT blocked_task_id as task_id, 1 as depth
1001
1025
  FROM t_task_dependencies
1002
1026
  WHERE blocker_task_id = ?
1003
1027
 
1004
1028
  UNION ALL
1005
1029
 
1006
- SELECT d.blocked_task_id, dc.depth + 1,
1007
- dc.path || ' → ' || d.blocked_task_id
1030
+ -- Follow the chain of dependencies
1031
+ SELECT d.blocked_task_id, dc.depth + 1
1008
1032
  FROM t_task_dependencies d
1009
1033
  JOIN dependency_chain dc ON d.blocker_task_id = dc.task_id
1010
1034
  WHERE dc.depth < 100
1011
1035
  )
1012
- SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
1036
+ SELECT task_id FROM dependency_chain WHERE task_id = ?
1013
1037
  `, [params.blocked_task_id, params.blocker_task_id])
1014
1038
  .then((result) => result[0]);
1015
- const cyclePath = cyclePathResult?.path || `#${params.blocked_task_id} → ... → #${params.blocker_task_id}`;
1016
- throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
1017
- }
1018
- // All validations passed - insert dependency
1019
- await trx('t_task_dependencies').insert({
1020
- blocker_task_id: params.blocker_task_id,
1021
- blocked_task_id: params.blocked_task_id,
1022
- created_ts: Math.floor(Date.now() / 1000)
1039
+ if (cycleCheck) {
1040
+ // Build cycle path for error message
1041
+ const cyclePathResult = await trx.raw(`
1042
+ WITH RECURSIVE dependency_chain AS (
1043
+ SELECT blocked_task_id as task_id, 1 as depth,
1044
+ CAST(blocked_task_id AS TEXT) as path
1045
+ FROM t_task_dependencies
1046
+ WHERE blocker_task_id = ?
1047
+
1048
+ UNION ALL
1049
+
1050
+ SELECT d.blocked_task_id, dc.depth + 1,
1051
+ dc.path || ' → ' || d.blocked_task_id
1052
+ FROM t_task_dependencies d
1053
+ JOIN dependency_chain dc ON d.blocker_task_id = dc.task_id
1054
+ WHERE dc.depth < 100
1055
+ )
1056
+ SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
1057
+ `, [params.blocked_task_id, params.blocker_task_id])
1058
+ .then((result) => result[0]);
1059
+ const cyclePath = cyclePathResult?.path || `#${params.blocked_task_id} → ... → #${params.blocker_task_id}`;
1060
+ throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
1061
+ }
1062
+ // All validations passed - insert dependency
1063
+ await trx('t_task_dependencies').insert({
1064
+ blocker_task_id: params.blocker_task_id,
1065
+ blocked_task_id: params.blocked_task_id,
1066
+ created_ts: Math.floor(Date.now() / 1000)
1067
+ });
1068
+ return {
1069
+ success: true,
1070
+ message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
1071
+ };
1023
1072
  });
1024
- return {
1025
- success: true,
1026
- message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
1027
- };
1028
1073
  });
1029
1074
  }
1030
1075
  catch (error) {
@@ -1116,24 +1161,26 @@ export async function batchCreateTasks(params, adapter) {
1116
1161
  try {
1117
1162
  if (atomic) {
1118
1163
  // Atomic mode: All or nothing
1119
- const results = await actualAdapter.transaction(async (trx) => {
1120
- const processedResults = [];
1121
- for (const task of params.tasks) {
1122
- try {
1123
- const result = await createTaskInternal(task, actualAdapter, trx);
1124
- processedResults.push({
1125
- title: task.title,
1126
- task_id: result.task_id,
1127
- success: true,
1128
- error: undefined
1129
- });
1130
- }
1131
- catch (error) {
1132
- const errorMessage = error instanceof Error ? error.message : String(error);
1133
- throw new Error(`Batch failed at task "${task.title}": ${errorMessage}`);
1164
+ const results = await connectionManager.executeWithRetry(async () => {
1165
+ return await actualAdapter.transaction(async (trx) => {
1166
+ const processedResults = [];
1167
+ for (const task of params.tasks) {
1168
+ try {
1169
+ const result = await createTaskInternal(task, actualAdapter, trx);
1170
+ processedResults.push({
1171
+ title: task.title,
1172
+ task_id: result.task_id,
1173
+ success: true,
1174
+ error: undefined
1175
+ });
1176
+ }
1177
+ catch (error) {
1178
+ const errorMessage = error instanceof Error ? error.message : String(error);
1179
+ throw new Error(`Batch failed at task "${task.title}": ${errorMessage}`);
1180
+ }
1134
1181
  }
1135
- }
1136
- return processedResults;
1182
+ return processedResults;
1183
+ });
1137
1184
  });
1138
1185
  return {
1139
1186
  success: true,
@@ -1149,8 +1196,10 @@ export async function batchCreateTasks(params, adapter) {
1149
1196
  let failed = 0;
1150
1197
  for (const task of params.tasks) {
1151
1198
  try {
1152
- const result = await actualAdapter.transaction(async (trx) => {
1153
- return await createTaskInternal(task, actualAdapter, trx);
1199
+ const result = await connectionManager.executeWithRetry(async () => {
1200
+ return await actualAdapter.transaction(async (trx) => {
1201
+ return await createTaskInternal(task, actualAdapter, trx);
1202
+ });
1154
1203
  });
1155
1204
  results.push({
1156
1205
  title: task.title,
@@ -1192,6 +1241,7 @@ export async function watchFiles(params, adapter) {
1192
1241
  validateActionParams('task', 'watch_files', params);
1193
1242
  const actualAdapter = adapter ?? getAdapter();
1194
1243
  const knex = actualAdapter.getKnex();
1244
+ const projectId = getProjectContext().getProjectId();
1195
1245
  if (!params.task_id) {
1196
1246
  throw new Error('Parameter "task_id" is required');
1197
1247
  }
@@ -1199,98 +1249,100 @@ export async function watchFiles(params, adapter) {
1199
1249
  throw new Error('Parameter "action" is required (watch, unwatch, or list)');
1200
1250
  }
1201
1251
  try {
1202
- return await actualAdapter.transaction(async (trx) => {
1203
- // Check if task exists
1204
- const taskData = await trx('t_tasks as t')
1205
- .join('m_task_statuses as s', 't.status_id', 's.id')
1206
- .where('t.id', params.task_id)
1207
- .select('t.id', 't.title', 's.name as status')
1208
- .first();
1209
- if (!taskData) {
1210
- throw new Error(`Task with id ${params.task_id} not found`);
1211
- }
1212
- if (params.action === 'watch') {
1213
- if (!params.file_paths || params.file_paths.length === 0) {
1214
- throw new Error('Parameter "file_paths" is required for watch action');
1252
+ return await connectionManager.executeWithRetry(async () => {
1253
+ return await actualAdapter.transaction(async (trx) => {
1254
+ // Check if task exists (project-scoped)
1255
+ const taskData = await trx('t_tasks as t')
1256
+ .join('m_task_statuses as s', 't.status_id', 's.id')
1257
+ .where({ 't.id': params.task_id, 't.project_id': projectId })
1258
+ .select('t.id', 't.title', 's.name as status')
1259
+ .first();
1260
+ if (!taskData) {
1261
+ throw new Error(`Task with id ${params.task_id} not found`);
1215
1262
  }
1216
- const addedFiles = [];
1217
- for (const filePath of params.file_paths) {
1218
- const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
1219
- // Check if already exists
1220
- const existing = await trx('t_task_file_links')
1221
- .where({ task_id: params.task_id, file_id: fileId })
1222
- .first();
1223
- if (!existing) {
1224
- await trx('t_task_file_links').insert({
1225
- task_id: params.task_id,
1226
- file_id: fileId
1227
- });
1228
- addedFiles.push(filePath);
1263
+ if (params.action === 'watch') {
1264
+ if (!params.file_paths || params.file_paths.length === 0) {
1265
+ throw new Error('Parameter "file_paths" is required for watch action');
1229
1266
  }
1230
- }
1231
- // Register files with watcher
1232
- try {
1233
- const watcher = FileWatcher.getInstance();
1234
- for (const filePath of addedFiles) {
1235
- watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
1267
+ const addedFiles = [];
1268
+ for (const filePath of params.file_paths) {
1269
+ const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
1270
+ // Check if already exists
1271
+ const existing = await trx('t_task_file_links')
1272
+ .where({ task_id: params.task_id, file_id: fileId })
1273
+ .first();
1274
+ if (!existing) {
1275
+ await trx('t_task_file_links').insert({
1276
+ task_id: params.task_id,
1277
+ file_id: fileId
1278
+ });
1279
+ addedFiles.push(filePath);
1280
+ }
1236
1281
  }
1282
+ // Register files with watcher
1283
+ try {
1284
+ const watcher = FileWatcher.getInstance();
1285
+ for (const filePath of addedFiles) {
1286
+ watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
1287
+ }
1288
+ }
1289
+ catch (error) {
1290
+ // Watcher may not be initialized yet, ignore
1291
+ debugLog('WARN', 'Could not register files with watcher', { error });
1292
+ }
1293
+ return {
1294
+ success: true,
1295
+ task_id: params.task_id,
1296
+ action: 'watch',
1297
+ files_added: addedFiles.length,
1298
+ files: addedFiles,
1299
+ message: `Watching ${addedFiles.length} file(s) for task ${params.task_id}`
1300
+ };
1237
1301
  }
1238
- catch (error) {
1239
- // Watcher may not be initialized yet, ignore
1240
- console.error('Warning: Could not register files with watcher:', error);
1302
+ else if (params.action === 'unwatch') {
1303
+ if (!params.file_paths || params.file_paths.length === 0) {
1304
+ throw new Error('Parameter "file_paths" is required for unwatch action');
1305
+ }
1306
+ const removedFiles = [];
1307
+ for (const filePath of params.file_paths) {
1308
+ const deleted = await trx('t_task_file_links')
1309
+ .where('task_id', params.task_id)
1310
+ .whereIn('file_id', function () {
1311
+ this.select('id').from('m_files').where({ path: filePath });
1312
+ })
1313
+ .delete();
1314
+ if (deleted > 0) {
1315
+ removedFiles.push(filePath);
1316
+ }
1317
+ }
1318
+ return {
1319
+ success: true,
1320
+ task_id: params.task_id,
1321
+ action: 'unwatch',
1322
+ files_removed: removedFiles.length,
1323
+ files: removedFiles,
1324
+ message: `Stopped watching ${removedFiles.length} file(s) for task ${params.task_id}`
1325
+ };
1241
1326
  }
1242
- return {
1243
- success: true,
1244
- task_id: params.task_id,
1245
- action: 'watch',
1246
- files_added: addedFiles.length,
1247
- files: addedFiles,
1248
- message: `Watching ${addedFiles.length} file(s) for task ${params.task_id}`
1249
- };
1250
- }
1251
- else if (params.action === 'unwatch') {
1252
- if (!params.file_paths || params.file_paths.length === 0) {
1253
- throw new Error('Parameter "file_paths" is required for unwatch action');
1327
+ else if (params.action === 'list') {
1328
+ const files = await trx('t_task_file_links as tfl')
1329
+ .join('m_files as f', 'tfl.file_id', 'f.id')
1330
+ .where('tfl.task_id', params.task_id)
1331
+ .select('f.path')
1332
+ .then(rows => rows.map((row) => row.path));
1333
+ return {
1334
+ success: true,
1335
+ task_id: params.task_id,
1336
+ action: 'list',
1337
+ files_count: files.length,
1338
+ files: files,
1339
+ message: `Task ${params.task_id} is watching ${files.length} file(s)`
1340
+ };
1254
1341
  }
1255
- const removedFiles = [];
1256
- for (const filePath of params.file_paths) {
1257
- const deleted = await trx('t_task_file_links')
1258
- .where('task_id', params.task_id)
1259
- .whereIn('file_id', function () {
1260
- this.select('id').from('m_files').where({ path: filePath });
1261
- })
1262
- .delete();
1263
- if (deleted > 0) {
1264
- removedFiles.push(filePath);
1265
- }
1342
+ else {
1343
+ throw new Error(`Invalid action: ${params.action}. Must be one of: watch, unwatch, list`);
1266
1344
  }
1267
- return {
1268
- success: true,
1269
- task_id: params.task_id,
1270
- action: 'unwatch',
1271
- files_removed: removedFiles.length,
1272
- files: removedFiles,
1273
- message: `Stopped watching ${removedFiles.length} file(s) for task ${params.task_id}`
1274
- };
1275
- }
1276
- else if (params.action === 'list') {
1277
- const files = await trx('t_task_file_links as tfl')
1278
- .join('m_files as f', 'tfl.file_id', 'f.id')
1279
- .where('tfl.task_id', params.task_id)
1280
- .select('f.path')
1281
- .then(rows => rows.map((row) => row.path));
1282
- return {
1283
- success: true,
1284
- task_id: params.task_id,
1285
- action: 'list',
1286
- files_count: files.length,
1287
- files: files,
1288
- message: `Task ${params.task_id} is watching ${files.length} file(s)`
1289
- };
1290
- }
1291
- else {
1292
- throw new Error(`Invalid action: ${params.action}. Must be one of: watch, unwatch, list`);
1293
- }
1345
+ });
1294
1346
  });
1295
1347
  }
1296
1348
  catch (error) {
@@ -1530,6 +1582,7 @@ export function taskHelp() {
1530
1582
  limits: {
1531
1583
  max_items: 50
1532
1584
  },
1585
+ note: '⚠️ IMPORTANT: The "tasks" parameter must be a JavaScript array, not a JSON string. MCP tools require pre-parsed objects.',
1533
1586
  example: {
1534
1587
  action: 'batch_create',
1535
1588
  tasks: [
@@ -1660,6 +1713,7 @@ export function taskHelp() {
1660
1713
  export async function watcherStatus(args, adapter) {
1661
1714
  const actualAdapter = adapter ?? getAdapter();
1662
1715
  const knex = actualAdapter.getKnex();
1716
+ const projectId = getProjectContext().getProjectId();
1663
1717
  const subaction = args.subaction || 'status';
1664
1718
  const watcher = FileWatcher.getInstance();
1665
1719
  if (subaction === 'help') {
@@ -1703,10 +1757,14 @@ export async function watcherStatus(args, adapter) {
1703
1757
  }
1704
1758
  if (subaction === 'list_files') {
1705
1759
  const fileLinks = await knex('t_task_file_links as tfl')
1706
- .join('t_tasks as t', 'tfl.task_id', 't.id')
1760
+ .join('t_tasks as t', function () {
1761
+ this.on('tfl.task_id', '=', 't.id')
1762
+ .andOn('tfl.project_id', '=', 't.project_id');
1763
+ })
1707
1764
  .join('m_task_statuses as ts', 't.status_id', 'ts.id')
1708
1765
  .join('m_files as f', 'tfl.file_id', 'f.id')
1709
- .where('t.status_id', '!=', 6) // Exclude archived tasks
1766
+ .where('t.project_id', projectId)
1767
+ .whereNot('t.status_id', STATUS_TO_ID['archived']) // Exclude archived tasks
1710
1768
  .select('f.path as file_path', 't.id', 't.title', 'ts.name as status_name')
1711
1769
  .orderBy(['f.path', 't.id']);
1712
1770
  // Group by file
@@ -1737,9 +1795,13 @@ export async function watcherStatus(args, adapter) {
1737
1795
  if (subaction === 'list_tasks') {
1738
1796
  const taskLinks = await knex('t_tasks as t')
1739
1797
  .join('m_task_statuses as ts', 't.status_id', 'ts.id')
1740
- .join('t_task_file_links as tfl', 't.id', 'tfl.task_id')
1798
+ .join('t_task_file_links as tfl', function () {
1799
+ this.on('t.id', '=', 'tfl.task_id')
1800
+ .andOn('t.project_id', '=', 'tfl.project_id');
1801
+ })
1741
1802
  .join('m_files as f', 'tfl.file_id', 'f.id')
1742
- .where('t.status_id', '!=', 6) // Exclude archived tasks
1803
+ .where('t.project_id', projectId)
1804
+ .whereNot('t.status_id', STATUS_TO_ID['archived']) // Exclude archived tasks
1743
1805
  .groupBy('t.id', 't.title', 'ts.name')
1744
1806
  .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'))
1745
1807
  .orderBy('t.id');