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