sqlew 3.6.10 → 3.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/CHANGELOG.md +346 -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 +173 -39
  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/migrations/test-all-versions-real.js +3 -0
  176. package/dist/tests/migrations/test-all-versions-real.js.map +1 -1
  177. package/dist/tests/multi-project-migration.test.d.ts +17 -0
  178. package/dist/tests/multi-project-migration.test.d.ts.map +1 -0
  179. package/dist/tests/multi-project-migration.test.js +399 -0
  180. package/dist/tests/multi-project-migration.test.js.map +1 -0
  181. package/dist/tests/multi-project.test.d.ts +5 -0
  182. package/dist/tests/multi-project.test.d.ts.map +1 -0
  183. package/dist/tests/multi-project.test.js +238 -0
  184. package/dist/tests/multi-project.test.js.map +1 -0
  185. package/dist/tests/schema-migration.test.d.ts +8 -0
  186. package/dist/tests/schema-migration.test.d.ts.map +1 -0
  187. package/dist/tests/schema-migration.test.js +108 -0
  188. package/dist/tests/schema-migration.test.js.map +1 -0
  189. package/dist/tests/sql-dump-converters.test.d.ts +7 -0
  190. package/dist/tests/sql-dump-converters.test.d.ts.map +1 -0
  191. package/dist/tests/sql-dump-converters.test.js +314 -0
  192. package/dist/tests/sql-dump-converters.test.js.map +1 -0
  193. package/dist/tests/sql-dump-cross-database.test.d.ts +21 -0
  194. package/dist/tests/sql-dump-cross-database.test.d.ts.map +1 -0
  195. package/dist/tests/sql-dump-cross-database.test.js +314 -0
  196. package/dist/tests/sql-dump-cross-database.test.js.map +1 -0
  197. package/dist/tests/sql-dump-default-conversions.test.d.ts +8 -0
  198. package/dist/tests/sql-dump-default-conversions.test.d.ts.map +1 -0
  199. package/dist/tests/sql-dump-default-conversions.test.js +141 -0
  200. package/dist/tests/sql-dump-default-conversions.test.js.map +1 -0
  201. package/dist/tests/sql-dump-fk-constraints.test.d.ts +13 -0
  202. package/dist/tests/sql-dump-fk-constraints.test.d.ts.map +1 -0
  203. package/dist/tests/sql-dump-fk-constraints.test.js +381 -0
  204. package/dist/tests/sql-dump-fk-constraints.test.js.map +1 -0
  205. package/dist/tests/sql-dump-indexes.test.d.ts +12 -0
  206. package/dist/tests/sql-dump-indexes.test.d.ts.map +1 -0
  207. package/dist/tests/sql-dump-indexes.test.js +269 -0
  208. package/dist/tests/sql-dump-indexes.test.js.map +1 -0
  209. package/dist/tests/sql-dump-integration.test.d.ts +16 -0
  210. package/dist/tests/sql-dump-integration.test.d.ts.map +1 -0
  211. package/dist/tests/sql-dump-integration.test.js +342 -0
  212. package/dist/tests/sql-dump-integration.test.js.map +1 -0
  213. package/dist/tests/sql-dump-table-ordering.test.d.ts +8 -0
  214. package/dist/tests/sql-dump-table-ordering.test.d.ts.map +1 -0
  215. package/dist/tests/sql-dump-table-ordering.test.js +253 -0
  216. package/dist/tests/sql-dump-table-ordering.test.js.map +1 -0
  217. package/dist/tests/tasks.link-file-backward-compat.test.js +11 -1
  218. package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
  219. package/dist/tests/tasks.watch-files-action.test.js +11 -1
  220. package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
  221. package/dist/tests/type-conversion.test.d.ts +8 -0
  222. package/dist/tests/type-conversion.test.d.ts.map +1 -0
  223. package/dist/tests/type-conversion.test.js +312 -0
  224. package/dist/tests/type-conversion.test.js.map +1 -0
  225. package/dist/tests/utils/test-helpers.d.ts +93 -0
  226. package/dist/tests/utils/test-helpers.d.ts.map +1 -0
  227. package/dist/tests/utils/test-helpers.js +407 -0
  228. package/dist/tests/utils/test-helpers.js.map +1 -0
  229. package/dist/tools/config.d.ts +58 -0
  230. package/dist/tools/config.d.ts.map +1 -0
  231. package/dist/tools/config.js +281 -0
  232. package/dist/tools/config.js.map +1 -0
  233. package/dist/tools/constraints.d.ts.map +1 -1
  234. package/dist/tools/constraints.js +138 -122
  235. package/dist/tools/constraints.js.map +1 -1
  236. package/dist/tools/context.d.ts.map +1 -1
  237. package/dist/tools/context.js +216 -109
  238. package/dist/tools/context.js.map +1 -1
  239. package/dist/tools/files.d.ts.map +1 -1
  240. package/dist/tools/files.js +123 -102
  241. package/dist/tools/files.js.map +1 -1
  242. package/dist/tools/tasks.d.ts.map +1 -1
  243. package/dist/tools/tasks.js +593 -518
  244. package/dist/tools/tasks.js.map +1 -1
  245. package/dist/tools/utils.d.ts +5 -0
  246. package/dist/tools/utils.d.ts.map +1 -1
  247. package/dist/tools/utils.js +176 -122
  248. package/dist/tools/utils.js.map +1 -1
  249. package/dist/types.d.ts +9 -26
  250. package/dist/types.d.ts.map +1 -1
  251. package/dist/utils/cleanup.d.ts +3 -0
  252. package/dist/utils/cleanup.d.ts.map +1 -1
  253. package/dist/utils/cleanup.js +14 -2
  254. package/dist/utils/cleanup.js.map +1 -1
  255. package/dist/utils/connection-manager.d.ts +59 -0
  256. package/dist/utils/connection-manager.d.ts.map +1 -0
  257. package/dist/utils/connection-manager.js +178 -0
  258. package/dist/utils/connection-manager.js.map +1 -0
  259. package/dist/utils/debug-logger.d.ts +8 -4
  260. package/dist/utils/debug-logger.d.ts.map +1 -1
  261. package/dist/utils/debug-logger.js +27 -7
  262. package/dist/utils/debug-logger.js.map +1 -1
  263. package/dist/utils/error-handler.d.ts +6 -4
  264. package/dist/utils/error-handler.d.ts.map +1 -1
  265. package/dist/utils/error-handler.js +34 -9
  266. package/dist/utils/error-handler.js.map +1 -1
  267. package/dist/utils/parameter-validator.d.ts.map +1 -1
  268. package/dist/utils/parameter-validator.js +50 -16
  269. package/dist/utils/parameter-validator.js.map +1 -1
  270. package/dist/utils/project-context.d.ts +111 -0
  271. package/dist/utils/project-context.d.ts.map +1 -0
  272. package/dist/utils/project-context.js +187 -0
  273. package/dist/utils/project-context.js.map +1 -0
  274. package/dist/utils/sql-dump-converters.d.ts +188 -0
  275. package/dist/utils/sql-dump-converters.d.ts.map +1 -0
  276. package/dist/utils/sql-dump-converters.js +311 -0
  277. package/dist/utils/sql-dump-converters.js.map +1 -0
  278. package/dist/utils/sql-dump.d.ts +102 -0
  279. package/dist/utils/sql-dump.d.ts.map +1 -0
  280. package/dist/utils/sql-dump.js +1550 -0
  281. package/dist/utils/sql-dump.js.map +1 -0
  282. package/dist/utils/vcs-adapter.d.ts +42 -0
  283. package/dist/utils/vcs-adapter.d.ts.map +1 -1
  284. package/dist/utils/vcs-adapter.js +154 -0
  285. package/dist/utils/vcs-adapter.js.map +1 -1
  286. package/docs/BASEADAPTER_IMPLEMENTATION.md +399 -0
  287. package/docs/DATABASE_AUTH.md +445 -0
  288. package/docs/DATABASE_MIGRATION.md +247 -0
  289. package/docs/MULTI_PROJECT_ARCHITECTURE.md +497 -0
  290. package/package.json +12 -4
  291. package/dist/migrations/knex/bootstrap/20251025020452_create_master_tables.d.ts.map +0 -1
  292. package/dist/migrations/knex/bootstrap/20251025020452_create_master_tables.js.map +0 -1
  293. package/dist/migrations/knex/bootstrap/20251025021152_create_transaction_tables.d.ts.map +0 -1
  294. package/dist/migrations/knex/bootstrap/20251025021152_create_transaction_tables.js.map +0 -1
  295. package/dist/migrations/knex/bootstrap/20251025021351_create_indexes.d.ts.map +0 -1
  296. package/dist/migrations/knex/bootstrap/20251025021351_create_indexes.js.map +0 -1
  297. package/dist/migrations/knex/bootstrap/20251025021416_seed_master_data.d.ts.map +0 -1
  298. package/dist/migrations/knex/bootstrap/20251025021416_seed_master_data.js.map +0 -1
  299. package/dist/migrations/knex/bootstrap/20251025070349_create_views.d.ts.map +0 -1
  300. package/dist/migrations/knex/bootstrap/20251025070349_create_views.js.map +0 -1
  301. package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts.map +0 -1
  302. package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js +0 -15
  303. package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js.map +0 -1
  304. package/dist/migrations/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts.map +0 -1
  305. package/dist/migrations/knex/enhancements/20251025082220_fix_task_dependencies_columns.js.map +0 -1
  306. package/dist/migrations/knex/enhancements/20251025090000_create_help_system_tables.d.ts.map +0 -1
  307. package/dist/migrations/knex/enhancements/20251025090000_create_help_system_tables.js.map +0 -1
  308. package/dist/migrations/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts.map +0 -1
  309. package/dist/migrations/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js.map +0 -1
  310. package/dist/migrations/knex/enhancements/20251025100000_seed_help_metadata.d.ts.map +0 -1
  311. package/dist/migrations/knex/enhancements/20251025100000_seed_help_metadata.js.map +0 -1
  312. package/dist/migrations/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts.map +0 -1
  313. package/dist/migrations/knex/enhancements/20251025100100_seed_remaining_use_cases.js.map +0 -1
  314. package/dist/migrations/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts.map +0 -1
  315. package/dist/migrations/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js.map +0 -1
  316. package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts.map +0 -1
  317. package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.js +0 -34
  318. package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.js.map +0 -1
  319. package/dist/migrations/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts.map +0 -1
  320. package/dist/migrations/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js.map +0 -1
  321. package/dist/migrations/knex/enhancements/20251027020000_update_agent_reusability.d.ts.map +0 -1
  322. package/dist/migrations/knex/enhancements/20251027020000_update_agent_reusability.js.map +0 -1
  323. package/dist/migrations/knex/enhancements/20251028000000_simplify_agent_system.d.ts.map +0 -1
  324. package/dist/migrations/knex/enhancements/20251028000000_simplify_agent_system.js.map +0 -1
  325. package/dist/migrations/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts.map +0 -1
  326. package/dist/migrations/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js.map +0 -1
  327. package/dist/migrations/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts.map +0 -1
  328. package/dist/migrations/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js.map +0 -1
  329. package/dist/migrations/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts.map +0 -1
  330. package/dist/migrations/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js.map +0 -1
  331. package/dist/migrations/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts.map +0 -1
  332. package/dist/migrations/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js.map +0 -1
  333. package/dist/migrations/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts.map +0 -1
  334. package/dist/migrations/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js.map +0 -1
  335. package/dist/migrations/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts.map +0 -1
  336. package/dist/migrations/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js.map +0 -1
  337. package/dist/migrations/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts.map +0 -1
  338. package/dist/migrations/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js.map +0 -1
  339. /package/dist/{migrations → config}/knex/bootstrap/20251025020452_create_master_tables.d.ts +0 -0
  340. /package/dist/{migrations → config}/knex/bootstrap/20251025021152_create_transaction_tables.d.ts +0 -0
  341. /package/dist/{migrations → config}/knex/bootstrap/20251025021351_create_indexes.d.ts +0 -0
  342. /package/dist/{migrations → config}/knex/bootstrap/20251025021351_create_indexes.js +0 -0
  343. /package/dist/{migrations → config}/knex/bootstrap/20251025021416_seed_master_data.d.ts +0 -0
  344. /package/dist/{migrations → config}/knex/bootstrap/20251025070349_create_views.d.ts +0 -0
  345. /package/dist/{migrations → config}/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts +0 -0
  346. /package/dist/{migrations → config}/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts +0 -0
  347. /package/dist/{migrations → config}/knex/enhancements/20251025082220_fix_task_dependencies_columns.js +0 -0
  348. /package/dist/{migrations → config}/knex/enhancements/20251025090000_create_help_system_tables.d.ts +0 -0
  349. /package/dist/{migrations → config}/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts +0 -0
  350. /package/dist/{migrations → config}/knex/enhancements/20251025100000_seed_help_metadata.d.ts +0 -0
  351. /package/dist/{migrations → config}/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts +0 -0
  352. /package/dist/{migrations → config}/knex/enhancements/20251025100100_seed_remaining_use_cases.js +0 -0
  353. /package/dist/{migrations → config}/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts +0 -0
  354. /package/dist/{migrations → config}/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts +0 -0
  355. /package/dist/{migrations → config}/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts +0 -0
  356. /package/dist/{migrations → config}/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js +0 -0
  357. /package/dist/{migrations → config}/knex/enhancements/20251027020000_update_agent_reusability.d.ts +0 -0
  358. /package/dist/{migrations → config}/knex/enhancements/20251028000000_simplify_agent_system.d.ts +0 -0
  359. /package/dist/{migrations → config}/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts +0 -0
  360. /package/dist/{migrations → config}/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js +0 -0
  361. /package/dist/{migrations → config}/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts +0 -0
  362. /package/dist/{migrations → config}/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js +0 -0
  363. /package/dist/{migrations → config}/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts +0 -0
  364. /package/dist/{migrations → config}/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js +0 -0
  365. /package/dist/{migrations → config}/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts +0 -0
  366. /package/dist/{migrations → config}/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js +0 -0
  367. /package/dist/{migrations → config}/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts +0 -0
  368. /package/dist/{migrations → config}/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js +0 -0
  369. /package/dist/{migrations → config}/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts +0 -0
  370. /package/dist/{migrations → config}/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js +0 -0
  371. /package/dist/{migrations → config}/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts +0 -0
  372. /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,12 +281,18 @@ 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) {
281
291
  const message = error instanceof Error ? error.message : String(error);
292
+ // Preserve validation errors (they already contain helpful information)
293
+ if (message.startsWith('{') && message.includes('"error"')) {
294
+ throw error;
295
+ }
282
296
  throw new Error(`Failed to create task: ${message}`);
283
297
  }
284
298
  }
@@ -292,185 +306,199 @@ export async function updateTask(params, adapter) {
292
306
  if (!params.task_id) {
293
307
  throw new Error('Parameter "task_id" is required');
294
308
  }
309
+ // Fail-fast project_id validation (Constraint #29)
310
+ const projectId = getProjectContext().getProjectId();
295
311
  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');
312
+ return await connectionManager.executeWithRetry(async () => {
313
+ return await actualAdapter.transaction(async (trx) => {
314
+ const knex = actualAdapter.getKnex();
315
+ // Check if task exists with project_id isolation
316
+ const taskExists = await trx('t_tasks')
317
+ .where({ id: params.task_id, project_id: projectId })
318
+ .first();
319
+ if (!taskExists) {
320
+ throw new Error(`Task with id ${params.task_id} not found`);
308
321
  }
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`);
322
+ // Build update data dynamically
323
+ const updateData = {};
324
+ if (params.title !== undefined) {
325
+ if (params.title.trim() === '') {
326
+ throw new Error('Parameter "title" cannot be empty');
327
+ }
328
+ validateLength(params.title, 'Parameter "title"', 200);
329
+ updateData.title = params.title;
324
330
  }
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');
331
+ if (params.priority !== undefined) {
332
+ validatePriorityRange(params.priority);
333
+ updateData.priority = params.priority;
334
+ }
335
+ if (params.assigned_agent !== undefined) {
336
+ const agentId = await getOrCreateAgent(actualAdapter, params.assigned_agent, trx);
337
+ updateData.assigned_agent_id = agentId;
338
+ }
339
+ if (params.layer !== undefined) {
340
+ const layerId = await getLayerId(actualAdapter, params.layer, trx);
341
+ if (layerId === null) {
342
+ throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
347
343
  }
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');
344
+ updateData.layer_id = layerId;
345
+ }
346
+ // Update t_tasks if any updates (with project_id isolation)
347
+ if (Object.keys(updateData).length > 0) {
348
+ await trx('t_tasks')
349
+ .where({ id: params.task_id, project_id: projectId })
350
+ .update(updateData);
351
+ // TODO: Add activity logging for updates if needed
352
+ }
353
+ // Update t_task_details if any detail fields provided
354
+ if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
355
+ // Process acceptance_criteria (can be string or array)
356
+ let acceptanceCriteriaString = undefined;
357
+ let acceptanceCriteriaJson = undefined;
358
+ if (params.acceptance_criteria !== undefined) {
359
+ if (Array.isArray(params.acceptance_criteria)) {
360
+ // Array format - store as JSON in acceptance_criteria_json
361
+ acceptanceCriteriaJson = JSON.stringify(params.acceptance_criteria);
362
+ // Also create human-readable summary in acceptance_criteria
363
+ acceptanceCriteriaString = params.acceptance_criteria
364
+ .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
365
+ .join('\n');
366
+ }
367
+ else if (typeof params.acceptance_criteria === 'string') {
368
+ // Try to parse as JSON first
369
+ try {
370
+ const parsed = JSON.parse(params.acceptance_criteria);
371
+ if (Array.isArray(parsed)) {
372
+ // It's a JSON array string - store in JSON field
373
+ acceptanceCriteriaJson = params.acceptance_criteria;
374
+ // Also create human-readable summary
375
+ acceptanceCriteriaString = parsed
376
+ .map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
377
+ .join('\n');
378
+ }
379
+ else {
380
+ // Valid JSON but not an array - store as plain text
381
+ acceptanceCriteriaString = params.acceptance_criteria || null;
382
+ acceptanceCriteriaJson = null;
383
+ }
359
384
  }
360
- else {
361
- // Valid JSON but not an array - store as plain text
385
+ catch {
386
+ // Not valid JSON - store as plain text
362
387
  acceptanceCriteriaString = params.acceptance_criteria || null;
363
388
  acceptanceCriteriaJson = null;
364
389
  }
365
390
  }
366
- catch {
367
- // Not valid JSON - store as plain text
368
- acceptanceCriteriaString = params.acceptance_criteria || null;
369
- acceptanceCriteriaJson = null;
370
- }
371
391
  }
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);
392
+ // Check if details exist (with project_id isolation)
393
+ const detailsExist = await trx('t_task_details')
394
+ .where({ task_id: params.task_id, project_id: projectId })
395
+ .first();
396
+ const detailsUpdate = {};
397
+ if (params.description !== undefined) {
398
+ detailsUpdate.description = params.description || null;
399
+ }
400
+ if (acceptanceCriteriaString !== undefined) {
401
+ detailsUpdate.acceptance_criteria = acceptanceCriteriaString;
402
+ }
403
+ if (acceptanceCriteriaJson !== undefined) {
404
+ detailsUpdate.acceptance_criteria_json = acceptanceCriteriaJson;
405
+ }
406
+ if (params.notes !== undefined) {
407
+ detailsUpdate.notes = params.notes || null;
413
408
  }
414
- catch {
415
- // If not valid JSON, treat as single file path
416
- watchFilesParsed = [params.watch_files];
409
+ if (detailsExist && Object.keys(detailsUpdate).length > 0) {
410
+ // Update existing details (with project_id isolation)
411
+ await trx('t_task_details')
412
+ .where({ task_id: params.task_id, project_id: projectId })
413
+ .update(detailsUpdate);
414
+ }
415
+ else if (!detailsExist) {
416
+ // Insert new details
417
+ await trx('t_task_details').insert({
418
+ project_id: projectId,
419
+ task_id: params.task_id,
420
+ description: params.description || null,
421
+ acceptance_criteria: acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null,
422
+ acceptance_criteria_json: acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null,
423
+ notes: params.notes || null
424
+ });
417
425
  }
418
426
  }
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('');
427
+ // Handle watch_files if provided (v3.4.1)
428
+ if (params.watch_files && params.watch_files.length > 0) {
429
+ // Parse watch_files - handle MCP SDK converting JSON string to char array
430
+ let watchFilesParsed;
431
+ if (typeof params.watch_files === 'string') {
432
+ // String - try to parse as JSON
424
433
  try {
425
- watchFilesParsed = JSON.parse(jsonString);
434
+ watchFilesParsed = JSON.parse(params.watch_files);
426
435
  }
427
436
  catch {
428
- throw new Error(`Invalid watch_files format: ${jsonString}`);
437
+ // If not valid JSON, treat as single file path
438
+ watchFilesParsed = [params.watch_files];
439
+ }
440
+ }
441
+ else if (Array.isArray(params.watch_files)) {
442
+ // Check if it's an array of single characters (MCP SDK bug)
443
+ if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
444
+ // Join characters back into string and parse JSON
445
+ const jsonString = params.watch_files.join('');
446
+ try {
447
+ watchFilesParsed = JSON.parse(jsonString);
448
+ }
449
+ catch {
450
+ throw new Error(`Invalid watch_files format: ${jsonString}`);
451
+ }
452
+ }
453
+ else {
454
+ // Normal array of file paths
455
+ watchFilesParsed = params.watch_files;
429
456
  }
430
457
  }
431
458
  else {
432
- // Normal array of file paths
433
- watchFilesParsed = params.watch_files;
459
+ throw new Error('Parameter "watch_files" must be a string or array');
434
460
  }
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);
461
+ for (const filePath of watchFilesParsed) {
462
+ const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
463
+ await trx('t_task_file_links').insert({
464
+ project_id: projectId,
465
+ task_id: params.task_id,
466
+ file_id: fileId
467
+ }).onConflict(['project_id', 'task_id', 'file_id']).ignore();
468
+ }
469
+ // Register files with watcher for auto-tracking
470
+ try {
471
+ const taskData = await trx('t_tasks as t')
472
+ .join('m_task_statuses as s', 't.status_id', 's.id')
473
+ .where({ 't.id': params.task_id, 't.project_id': projectId })
474
+ .select('t.title', 's.name as status')
475
+ .first();
476
+ if (taskData) {
477
+ const watcher = FileWatcher.getInstance();
478
+ for (const filePath of watchFilesParsed) {
479
+ watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
480
+ }
457
481
  }
458
482
  }
483
+ catch (error) {
484
+ // Watcher may not be initialized yet, ignore
485
+ debugLog('WARN', 'Could not register files with watcher', { error });
486
+ }
459
487
  }
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
- };
488
+ return {
489
+ success: true,
490
+ task_id: params.task_id,
491
+ message: `Task ${params.task_id} updated successfully`
492
+ };
493
+ });
470
494
  });
471
495
  }
472
496
  catch (error) {
473
497
  const message = error instanceof Error ? error.message : String(error);
498
+ // Preserve validation errors (they already contain helpful information)
499
+ if (message.startsWith('{') && message.includes('"error"')) {
500
+ throw error; // Re-throw validation error as-is
501
+ }
474
502
  throw new Error(`Failed to update task: ${message}`);
475
503
  }
476
504
  }
@@ -479,6 +507,7 @@ export async function updateTask(params, adapter) {
479
507
  */
480
508
  async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
481
509
  const knex = adapter.getKnex();
510
+ const projectId = getProjectContext().getProjectId();
482
511
  // Build query based on include_details flag
483
512
  const selectFields = includeDetails
484
513
  ? [
@@ -497,26 +526,34 @@ async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
497
526
  's.name as status',
498
527
  't.priority'
499
528
  ];
500
- // Get blockers (tasks that block this task)
529
+ // Get blockers (tasks that block this task) - with project_id isolation
501
530
  let blockersQuery = knex('t_tasks as t')
502
531
  .join('t_task_dependencies as d', 't.id', 'd.blocker_task_id')
503
532
  .leftJoin('m_task_statuses as s', 't.status_id', 's.id')
504
533
  .leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
505
- .where('d.blocked_task_id', taskId)
534
+ .where({ 'd.blocked_task_id': taskId, 'd.project_id': projectId, 't.project_id': projectId })
506
535
  .select(selectFields);
507
536
  if (includeDetails) {
508
- blockersQuery = blockersQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
537
+ blockersQuery = blockersQuery
538
+ .leftJoin('t_task_details as td', function () {
539
+ this.on('t.id', '=', 'td.task_id')
540
+ .andOn('t.project_id', '=', 'td.project_id');
541
+ });
509
542
  }
510
543
  const blockers = await blockersQuery;
511
- // Get blocking (tasks this task blocks)
544
+ // Get blocking (tasks this task blocks) - with project_id isolation
512
545
  let blockingQuery = knex('t_tasks as t')
513
546
  .join('t_task_dependencies as d', 't.id', 'd.blocked_task_id')
514
547
  .leftJoin('m_task_statuses as s', 't.status_id', 's.id')
515
548
  .leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
516
- .where('d.blocker_task_id', taskId)
549
+ .where({ 'd.blocker_task_id': taskId, 'd.project_id': projectId, 't.project_id': projectId })
517
550
  .select(selectFields);
518
551
  if (includeDetails) {
519
- blockingQuery = blockingQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
552
+ blockingQuery = blockingQuery
553
+ .leftJoin('t_task_details as td', function () {
554
+ this.on('t.id', '=', 'td.task_id')
555
+ .andOn('t.project_id', '=', 'td.project_id');
556
+ });
520
557
  }
521
558
  const blocking = await blockingQuery;
522
559
  return { blockers, blocking };
@@ -531,15 +568,20 @@ export async function getTask(params, adapter) {
531
568
  if (!params.task_id) {
532
569
  throw new Error('Parameter "task_id" is required');
533
570
  }
571
+ // Fail-fast project_id validation (Constraint #29)
572
+ const projectId = getProjectContext().getProjectId();
534
573
  try {
535
- // Get task with details
574
+ // Get task with details (with project_id isolation)
536
575
  const task = await knex('t_tasks as t')
537
576
  .leftJoin('m_task_statuses as s', 't.status_id', 's.id')
538
577
  .leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
539
578
  .leftJoin('m_agents as ca', 't.created_by_agent_id', 'ca.id')
540
579
  .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)
580
+ .leftJoin('t_task_details as td', function () {
581
+ this.on('t.id', '=', 'td.task_id')
582
+ .andOn('t.project_id', '=', 'td.project_id');
583
+ })
584
+ .where({ 't.id': params.task_id, 't.project_id': projectId })
543
585
  .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
586
  .first();
545
587
  if (!task) {
@@ -603,6 +645,8 @@ export async function listTasks(params = {}, adapter) {
603
645
  validateActionParams('task', 'list', params);
604
646
  const actualAdapter = adapter ?? getAdapter();
605
647
  const knex = actualAdapter.getKnex();
648
+ // Get current project ID for filtering (Constraint #22)
649
+ const projectId = getProjectContext().getProjectId();
606
650
  try {
607
651
  // Run auto-stale detection, git-aware completion, and auto-archive before listing
608
652
  const transitionCount = await detectAndTransitionStaleTasks(actualAdapter);
@@ -632,6 +676,8 @@ export async function listTasks(params = {}, adapter) {
632
676
  // Standard query without dependency counts
633
677
  query = knex('v_task_board');
634
678
  }
679
+ // Filter by project_id (Constraint #22: Multi-project isolation)
680
+ query = query.where(params.include_dependency_counts ? 'vt.project_id' : 'project_id', projectId);
635
681
  // Filter by status
636
682
  if (params.status) {
637
683
  if (!STATUS_TO_ID[params.status]) {
@@ -696,68 +742,74 @@ export async function moveTask(params, adapter) {
696
742
  // Run auto-stale detection and auto-archive before move
697
743
  await detectAndTransitionStaleTasks(actualAdapter);
698
744
  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);
745
+ return await connectionManager.executeWithRetry(async () => {
746
+ return await actualAdapter.transaction(async (trx) => {
747
+ // Get current status
748
+ const taskRow = await trx('t_tasks')
749
+ .where({ id: params.task_id })
750
+ .select('status_id')
751
+ .first();
752
+ if (!taskRow) {
753
+ throw new Error(`Task with id ${params.task_id} not found`);
745
754
  }
746
- catch (error) {
747
- // Watcher may not be initialized, ignore
755
+ const currentStatusId = taskRow.status_id;
756
+ const newStatusId = STATUS_TO_ID[params.new_status];
757
+ if (!newStatusId) {
758
+ throw new Error(`Invalid new_status: ${params.new_status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
748
759
  }
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
- };
760
+ // Check if transition is valid
761
+ const validNextStatuses = VALID_TRANSITIONS[currentStatusId] || [];
762
+ if (!validNextStatuses.includes(newStatusId)) {
763
+ throw new Error(`Invalid transition from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}. ` +
764
+ `Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
765
+ }
766
+ // Update status
767
+ const updateData = {
768
+ status_id: newStatusId
769
+ };
770
+ // Set completed_ts when moving to done
771
+ if (newStatusId === TASK_STATUS.DONE) {
772
+ updateData.completed_ts = Math.floor(Date.now() / 1000);
773
+ }
774
+ await trx('t_tasks')
775
+ .where({ id: params.task_id })
776
+ .update(updateData);
777
+ // Activity logging (replaces trigger)
778
+ // Note: Using system agent (id=1) for status changes
779
+ // In a real implementation, you'd pass the actual agent_id who made the change
780
+ const systemAgentId = 1;
781
+ await logTaskStatusChange(trx, {
782
+ task_id: params.task_id,
783
+ old_status: currentStatusId,
784
+ new_status: newStatusId,
785
+ agent_id: systemAgentId
786
+ });
787
+ // Update watcher if moving to done or archived (stop watching)
788
+ if (params.new_status === 'done' || params.new_status === 'archived') {
789
+ try {
790
+ const watcher = FileWatcher.getInstance();
791
+ watcher.unregisterTask(params.task_id);
792
+ }
793
+ catch (error) {
794
+ // Watcher may not be initialized, ignore
795
+ }
796
+ }
797
+ return {
798
+ success: true,
799
+ task_id: params.task_id,
800
+ old_status: ID_TO_STATUS[currentStatusId],
801
+ new_status: params.new_status,
802
+ message: `Task ${params.task_id} moved from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}`
803
+ };
804
+ });
757
805
  });
758
806
  }
759
807
  catch (error) {
760
808
  const message = error instanceof Error ? error.message : String(error);
809
+ // Preserve validation errors (they already contain helpful information)
810
+ if (message.startsWith('{') && message.includes('"error"')) {
811
+ throw error;
812
+ }
761
813
  throw new Error(`Failed to move task: ${message}`);
762
814
  }
763
815
  }
@@ -778,86 +830,88 @@ export async function linkTask(params, adapter) {
778
830
  throw new Error('Parameter "target_id" is required');
779
831
  }
780
832
  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`);
833
+ return await connectionManager.executeWithRetry(async () => {
834
+ return await actualAdapter.transaction(async (trx) => {
835
+ // Check if task exists
836
+ const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
837
+ if (!taskExists) {
838
+ throw new Error(`Task with id ${params.task_id} not found`);
811
839
  }
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);
840
+ if (params.link_type === 'decision') {
841
+ const decisionKey = String(params.target_id);
842
+ const keyId = await getOrCreateContextKey(actualAdapter, decisionKey, trx);
843
+ const linkRelation = params.link_relation || 'implements';
844
+ await trx('t_task_decision_links').insert({
845
+ task_id: params.task_id,
846
+ decision_key_id: keyId,
847
+ link_type: linkRelation
848
+ }).onConflict(['task_id', 'decision_key_id']).merge();
849
+ return {
850
+ success: true,
851
+ task_id: params.task_id,
852
+ linked_to: 'decision',
853
+ target: decisionKey,
854
+ relation: linkRelation,
855
+ message: `Task ${params.task_id} linked to decision "${decisionKey}"`
856
+ };
857
+ }
858
+ else if (params.link_type === 'constraint') {
859
+ const constraintId = Number(params.target_id);
860
+ // Check if constraint exists
861
+ const constraintExists = await trx('t_constraints').where({ id: constraintId }).first();
862
+ if (!constraintExists) {
863
+ throw new Error(`Constraint with id ${constraintId} not found`);
843
864
  }
865
+ await trx('t_task_constraint_links').insert({
866
+ task_id: params.task_id,
867
+ constraint_id: constraintId
868
+ }).onConflict(['task_id', 'constraint_id']).ignore();
869
+ return {
870
+ success: true,
871
+ task_id: params.task_id,
872
+ linked_to: 'constraint',
873
+ target: constraintId,
874
+ message: `Task ${params.task_id} linked to constraint ${constraintId}`
875
+ };
844
876
  }
845
- catch (error) {
846
- // Watcher may not be initialized yet, ignore
847
- debugLog('WARN', 'Could not register file with watcher', { error });
877
+ else if (params.link_type === 'file') {
878
+ // Deprecation warning (v3.4.1)
879
+ 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: ["..."] }`);
880
+ const filePath = String(params.target_id);
881
+ const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
882
+ await trx('t_task_file_links').insert({
883
+ task_id: params.task_id,
884
+ file_id: fileId
885
+ }).onConflict(['task_id', 'file_id']).ignore();
886
+ // Register file with watcher for auto-tracking
887
+ try {
888
+ const taskData = await trx('t_tasks as t')
889
+ .join('m_task_statuses as s', 't.status_id', 's.id')
890
+ .where('t.id', params.task_id)
891
+ .select('t.title', 's.name as status')
892
+ .first();
893
+ if (taskData) {
894
+ const watcher = FileWatcher.getInstance();
895
+ watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
896
+ }
897
+ }
898
+ catch (error) {
899
+ // Watcher may not be initialized yet, ignore
900
+ debugLog('WARN', 'Could not register file with watcher', { error });
901
+ }
902
+ return {
903
+ success: true,
904
+ task_id: params.task_id,
905
+ linked_to: 'file',
906
+ target: filePath,
907
+ deprecation_warning: 'task.link(link_type="file") is deprecated. Use task.create/update(watch_files) or watch_files action instead.',
908
+ message: `Task ${params.task_id} linked to file "${filePath}" (DEPRECATED API - use watch_files instead)`
909
+ };
848
910
  }
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
- }
911
+ else {
912
+ throw new Error(`Invalid link_type: ${params.link_type}. Must be one of: decision, constraint, file`);
913
+ }
914
+ });
861
915
  });
862
916
  }
863
917
  catch (error) {
@@ -876,44 +930,46 @@ export async function archiveTask(params, adapter) {
876
930
  throw new Error('Parameter "task_id" is required');
877
931
  }
878
932
  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
933
+ return await connectionManager.executeWithRetry(async () => {
934
+ return await actualAdapter.transaction(async (trx) => {
935
+ // Check if task is in 'done' status
936
+ const taskRow = await trx('t_tasks')
937
+ .where({ id: params.task_id })
938
+ .select('status_id')
939
+ .first();
940
+ if (!taskRow) {
941
+ throw new Error(`Task with id ${params.task_id} not found`);
942
+ }
943
+ if (taskRow.status_id !== TASK_STATUS.DONE) {
944
+ throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
945
+ }
946
+ // Update to archived
947
+ await trx('t_tasks')
948
+ .where({ id: params.task_id })
949
+ .update({ status_id: TASK_STATUS.ARCHIVED });
950
+ // Activity logging
951
+ // Note: Using system agent (id=1) for status changes
952
+ const systemAgentId = 1;
953
+ await logTaskStatusChange(trx, {
954
+ task_id: params.task_id,
955
+ old_status: TASK_STATUS.DONE,
956
+ new_status: TASK_STATUS.ARCHIVED,
957
+ agent_id: systemAgentId
958
+ });
959
+ // Unregister from file watcher (archived tasks don't need tracking)
960
+ try {
961
+ const watcher = FileWatcher.getInstance();
962
+ watcher.unregisterTask(params.task_id);
963
+ }
964
+ catch (error) {
965
+ // Watcher may not be initialized, ignore
966
+ }
967
+ return {
968
+ success: true,
969
+ task_id: params.task_id,
970
+ message: `Task ${params.task_id} archived successfully`
971
+ };
903
972
  });
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
973
  });
918
974
  }
919
975
  catch (error) {
@@ -935,95 +991,97 @@ export async function addDependency(params, adapter) {
935
991
  throw new Error('Parameter "blocked_task_id" is required');
936
992
  }
937
993
  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(`
994
+ return await connectionManager.executeWithRetry(async () => {
995
+ return await actualAdapter.transaction(async (trx) => {
996
+ // Validation 1: No self-dependencies
997
+ if (params.blocker_task_id === params.blocked_task_id) {
998
+ throw new Error('Self-dependency not allowed');
999
+ }
1000
+ // Validation 2: Both tasks must exist and check if archived
1001
+ const blockerTask = await trx('t_tasks')
1002
+ .where({ id: params.blocker_task_id })
1003
+ .select('id', 'status_id')
1004
+ .first();
1005
+ const blockedTask = await trx('t_tasks')
1006
+ .where({ id: params.blocked_task_id })
1007
+ .select('id', 'status_id')
1008
+ .first();
1009
+ if (!blockerTask) {
1010
+ throw new Error(`Blocker task #${params.blocker_task_id} not found`);
1011
+ }
1012
+ if (!blockedTask) {
1013
+ throw new Error(`Blocked task #${params.blocked_task_id} not found`);
1014
+ }
1015
+ // Validation 3: Neither task is archived
1016
+ if (blockerTask.status_id === TASK_STATUS.ARCHIVED) {
1017
+ throw new Error(`Cannot add dependency: Task #${params.blocker_task_id} is archived`);
1018
+ }
1019
+ if (blockedTask.status_id === TASK_STATUS.ARCHIVED) {
1020
+ throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
1021
+ }
1022
+ // Validation 4: No direct circular (reverse relationship)
1023
+ const reverseExists = await trx('t_task_dependencies')
1024
+ .where({
1025
+ blocker_task_id: params.blocked_task_id,
1026
+ blocked_task_id: params.blocker_task_id
1027
+ })
1028
+ .first();
1029
+ if (reverseExists) {
1030
+ throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
1031
+ }
1032
+ // Validation 5: No transitive circular (check if adding this would create a cycle)
1033
+ const cycleCheck = await trx.raw(`
997
1034
  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
1035
+ -- Start from the task that would be blocked
1036
+ SELECT blocked_task_id as task_id, 1 as depth
1000
1037
  FROM t_task_dependencies
1001
1038
  WHERE blocker_task_id = ?
1002
1039
 
1003
1040
  UNION ALL
1004
1041
 
1005
- SELECT d.blocked_task_id, dc.depth + 1,
1006
- dc.path || ' → ' || d.blocked_task_id
1042
+ -- Follow the chain of dependencies
1043
+ SELECT d.blocked_task_id, dc.depth + 1
1007
1044
  FROM t_task_dependencies d
1008
1045
  JOIN dependency_chain dc ON d.blocker_task_id = dc.task_id
1009
1046
  WHERE dc.depth < 100
1010
1047
  )
1011
- SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
1048
+ SELECT task_id FROM dependency_chain WHERE task_id = ?
1012
1049
  `, [params.blocked_task_id, params.blocker_task_id])
1013
1050
  .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)
1051
+ if (cycleCheck) {
1052
+ // Build cycle path for error message
1053
+ const cyclePathResult = await trx.raw(`
1054
+ WITH RECURSIVE dependency_chain AS (
1055
+ SELECT blocked_task_id as task_id, 1 as depth,
1056
+ CAST(blocked_task_id AS TEXT) as path
1057
+ FROM t_task_dependencies
1058
+ WHERE blocker_task_id = ?
1059
+
1060
+ UNION ALL
1061
+
1062
+ SELECT d.blocked_task_id, dc.depth + 1,
1063
+ dc.path || ' → ' || d.blocked_task_id
1064
+ FROM t_task_dependencies d
1065
+ JOIN dependency_chain dc ON d.blocker_task_id = dc.task_id
1066
+ WHERE dc.depth < 100
1067
+ )
1068
+ SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
1069
+ `, [params.blocked_task_id, params.blocker_task_id])
1070
+ .then((result) => result[0]);
1071
+ const cyclePath = cyclePathResult?.path || `#${params.blocked_task_id} → ... → #${params.blocker_task_id}`;
1072
+ throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
1073
+ }
1074
+ // All validations passed - insert dependency
1075
+ await trx('t_task_dependencies').insert({
1076
+ blocker_task_id: params.blocker_task_id,
1077
+ blocked_task_id: params.blocked_task_id,
1078
+ created_ts: Math.floor(Date.now() / 1000)
1079
+ });
1080
+ return {
1081
+ success: true,
1082
+ message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
1083
+ };
1022
1084
  });
1023
- return {
1024
- success: true,
1025
- message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
1026
- };
1027
1085
  });
1028
1086
  }
1029
1087
  catch (error) {
@@ -1115,24 +1173,26 @@ export async function batchCreateTasks(params, adapter) {
1115
1173
  try {
1116
1174
  if (atomic) {
1117
1175
  // 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}`);
1176
+ const results = await connectionManager.executeWithRetry(async () => {
1177
+ return await actualAdapter.transaction(async (trx) => {
1178
+ const processedResults = [];
1179
+ for (const task of params.tasks) {
1180
+ try {
1181
+ const result = await createTaskInternal(task, actualAdapter, trx);
1182
+ processedResults.push({
1183
+ title: task.title,
1184
+ task_id: result.task_id,
1185
+ success: true,
1186
+ error: undefined
1187
+ });
1188
+ }
1189
+ catch (error) {
1190
+ const errorMessage = error instanceof Error ? error.message : String(error);
1191
+ throw new Error(`Batch failed at task "${task.title}": ${errorMessage}`);
1192
+ }
1133
1193
  }
1134
- }
1135
- return processedResults;
1194
+ return processedResults;
1195
+ });
1136
1196
  });
1137
1197
  return {
1138
1198
  success: true,
@@ -1148,8 +1208,10 @@ export async function batchCreateTasks(params, adapter) {
1148
1208
  let failed = 0;
1149
1209
  for (const task of params.tasks) {
1150
1210
  try {
1151
- const result = await actualAdapter.transaction(async (trx) => {
1152
- return await createTaskInternal(task, actualAdapter, trx);
1211
+ const result = await connectionManager.executeWithRetry(async () => {
1212
+ return await actualAdapter.transaction(async (trx) => {
1213
+ return await createTaskInternal(task, actualAdapter, trx);
1214
+ });
1153
1215
  });
1154
1216
  results.push({
1155
1217
  title: task.title,
@@ -1191,6 +1253,7 @@ export async function watchFiles(params, adapter) {
1191
1253
  validateActionParams('task', 'watch_files', params);
1192
1254
  const actualAdapter = adapter ?? getAdapter();
1193
1255
  const knex = actualAdapter.getKnex();
1256
+ const projectId = getProjectContext().getProjectId();
1194
1257
  if (!params.task_id) {
1195
1258
  throw new Error('Parameter "task_id" is required');
1196
1259
  }
@@ -1198,98 +1261,100 @@ export async function watchFiles(params, adapter) {
1198
1261
  throw new Error('Parameter "action" is required (watch, unwatch, or list)');
1199
1262
  }
1200
1263
  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');
1264
+ return await connectionManager.executeWithRetry(async () => {
1265
+ return await actualAdapter.transaction(async (trx) => {
1266
+ // Check if task exists (project-scoped)
1267
+ const taskData = await trx('t_tasks as t')
1268
+ .join('m_task_statuses as s', 't.status_id', 's.id')
1269
+ .where({ 't.id': params.task_id, 't.project_id': projectId })
1270
+ .select('t.id', 't.title', 's.name as status')
1271
+ .first();
1272
+ if (!taskData) {
1273
+ throw new Error(`Task with id ${params.task_id} not found`);
1214
1274
  }
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);
1275
+ if (params.action === 'watch') {
1276
+ if (!params.file_paths || params.file_paths.length === 0) {
1277
+ throw new Error('Parameter "file_paths" is required for watch action');
1228
1278
  }
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);
1279
+ const addedFiles = [];
1280
+ for (const filePath of params.file_paths) {
1281
+ const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
1282
+ // Check if already exists
1283
+ const existing = await trx('t_task_file_links')
1284
+ .where({ task_id: params.task_id, file_id: fileId })
1285
+ .first();
1286
+ if (!existing) {
1287
+ await trx('t_task_file_links').insert({
1288
+ task_id: params.task_id,
1289
+ file_id: fileId
1290
+ });
1291
+ addedFiles.push(filePath);
1292
+ }
1235
1293
  }
1294
+ // Register files with watcher
1295
+ try {
1296
+ const watcher = FileWatcher.getInstance();
1297
+ for (const filePath of addedFiles) {
1298
+ watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
1299
+ }
1300
+ }
1301
+ catch (error) {
1302
+ // Watcher may not be initialized yet, ignore
1303
+ debugLog('WARN', 'Could not register files with watcher', { error });
1304
+ }
1305
+ return {
1306
+ success: true,
1307
+ task_id: params.task_id,
1308
+ action: 'watch',
1309
+ files_added: addedFiles.length,
1310
+ files: addedFiles,
1311
+ message: `Watching ${addedFiles.length} file(s) for task ${params.task_id}`
1312
+ };
1236
1313
  }
1237
- catch (error) {
1238
- // Watcher may not be initialized yet, ignore
1239
- debugLog('WARN', 'Could not register files with watcher', { error });
1314
+ else if (params.action === 'unwatch') {
1315
+ if (!params.file_paths || params.file_paths.length === 0) {
1316
+ throw new Error('Parameter "file_paths" is required for unwatch action');
1317
+ }
1318
+ const removedFiles = [];
1319
+ for (const filePath of params.file_paths) {
1320
+ const deleted = await trx('t_task_file_links')
1321
+ .where('task_id', params.task_id)
1322
+ .whereIn('file_id', function () {
1323
+ this.select('id').from('m_files').where({ path: filePath });
1324
+ })
1325
+ .delete();
1326
+ if (deleted > 0) {
1327
+ removedFiles.push(filePath);
1328
+ }
1329
+ }
1330
+ return {
1331
+ success: true,
1332
+ task_id: params.task_id,
1333
+ action: 'unwatch',
1334
+ files_removed: removedFiles.length,
1335
+ files: removedFiles,
1336
+ message: `Stopped watching ${removedFiles.length} file(s) for task ${params.task_id}`
1337
+ };
1240
1338
  }
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');
1339
+ else if (params.action === 'list') {
1340
+ const files = await trx('t_task_file_links as tfl')
1341
+ .join('m_files as f', 'tfl.file_id', 'f.id')
1342
+ .where('tfl.task_id', params.task_id)
1343
+ .select('f.path')
1344
+ .then(rows => rows.map((row) => row.path));
1345
+ return {
1346
+ success: true,
1347
+ task_id: params.task_id,
1348
+ action: 'list',
1349
+ files_count: files.length,
1350
+ files: files,
1351
+ message: `Task ${params.task_id} is watching ${files.length} file(s)`
1352
+ };
1253
1353
  }
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
- }
1354
+ else {
1355
+ throw new Error(`Invalid action: ${params.action}. Must be one of: watch, unwatch, list`);
1265
1356
  }
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
- }
1357
+ });
1293
1358
  });
1294
1359
  }
1295
1360
  catch (error) {
@@ -1529,6 +1594,7 @@ export function taskHelp() {
1529
1594
  limits: {
1530
1595
  max_items: 50
1531
1596
  },
1597
+ note: '⚠️ IMPORTANT: The "tasks" parameter must be a JavaScript array, not a JSON string. MCP tools require pre-parsed objects.',
1532
1598
  example: {
1533
1599
  action: 'batch_create',
1534
1600
  tasks: [
@@ -1659,6 +1725,7 @@ export function taskHelp() {
1659
1725
  export async function watcherStatus(args, adapter) {
1660
1726
  const actualAdapter = adapter ?? getAdapter();
1661
1727
  const knex = actualAdapter.getKnex();
1728
+ const projectId = getProjectContext().getProjectId();
1662
1729
  const subaction = args.subaction || 'status';
1663
1730
  const watcher = FileWatcher.getInstance();
1664
1731
  if (subaction === 'help') {
@@ -1702,10 +1769,14 @@ export async function watcherStatus(args, adapter) {
1702
1769
  }
1703
1770
  if (subaction === 'list_files') {
1704
1771
  const fileLinks = await knex('t_task_file_links as tfl')
1705
- .join('t_tasks as t', 'tfl.task_id', 't.id')
1772
+ .join('t_tasks as t', function () {
1773
+ this.on('tfl.task_id', '=', 't.id')
1774
+ .andOn('tfl.project_id', '=', 't.project_id');
1775
+ })
1706
1776
  .join('m_task_statuses as ts', 't.status_id', 'ts.id')
1707
1777
  .join('m_files as f', 'tfl.file_id', 'f.id')
1708
- .where('t.status_id', '!=', 6) // Exclude archived tasks
1778
+ .where('t.project_id', projectId)
1779
+ .whereNot('t.status_id', STATUS_TO_ID['archived']) // Exclude archived tasks
1709
1780
  .select('f.path as file_path', 't.id', 't.title', 'ts.name as status_name')
1710
1781
  .orderBy(['f.path', 't.id']);
1711
1782
  // Group by file
@@ -1736,9 +1807,13 @@ export async function watcherStatus(args, adapter) {
1736
1807
  if (subaction === 'list_tasks') {
1737
1808
  const taskLinks = await knex('t_tasks as t')
1738
1809
  .join('m_task_statuses as ts', 't.status_id', 'ts.id')
1739
- .join('t_task_file_links as tfl', 't.id', 'tfl.task_id')
1810
+ .join('t_task_file_links as tfl', function () {
1811
+ this.on('t.id', '=', 'tfl.task_id')
1812
+ .andOn('t.project_id', '=', 'tfl.project_id');
1813
+ })
1740
1814
  .join('m_files as f', 'tfl.file_id', 'f.id')
1741
- .where('t.status_id', '!=', 6) // Exclude archived tasks
1815
+ .where('t.project_id', projectId)
1816
+ .whereNot('t.status_id', STATUS_TO_ID['archived']) // Exclude archived tasks
1742
1817
  .groupBy('t.id', 't.title', 'ts.name')
1743
1818
  .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
1819
  .orderBy('t.id');