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.
- package/CHANGELOG.md +346 -0
- package/README.md +54 -39
- package/assets/config.example.toml +93 -0
- package/assets/kanban-visualizer.png +0 -0
- package/assets/sample-agents/sqlew-architect.md +32 -13
- package/assets/sample-agents/sqlew-researcher.md +70 -17
- package/assets/sample-agents/sqlew-scrum-master.md +60 -25
- package/assets/schema.sql +2 -2
- package/dist/adapters/auth/auth-factory.d.ts +86 -0
- package/dist/adapters/auth/auth-factory.d.ts.map +1 -0
- package/dist/adapters/auth/auth-factory.js +103 -0
- package/dist/adapters/auth/auth-factory.js.map +1 -0
- package/dist/adapters/auth/auth-types.d.ts +30 -0
- package/dist/adapters/auth/auth-types.d.ts.map +1 -0
- package/dist/adapters/auth/auth-types.js +30 -0
- package/dist/adapters/auth/auth-types.js.map +1 -0
- package/dist/adapters/auth/base-auth-provider.d.ts +327 -0
- package/dist/adapters/auth/base-auth-provider.d.ts.map +1 -0
- package/dist/adapters/auth/base-auth-provider.js +111 -0
- package/dist/adapters/auth/base-auth-provider.js.map +1 -0
- package/dist/adapters/auth/direct-auth-provider.d.ts +356 -0
- package/dist/adapters/auth/direct-auth-provider.d.ts.map +1 -0
- package/dist/adapters/auth/direct-auth-provider.js +406 -0
- package/dist/adapters/auth/direct-auth-provider.js.map +1 -0
- package/dist/adapters/base-adapter.d.ts +638 -0
- package/dist/adapters/base-adapter.d.ts.map +1 -0
- package/dist/adapters/base-adapter.js +557 -0
- package/dist/adapters/base-adapter.js.map +1 -0
- package/dist/adapters/index.d.ts +13 -2
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +27 -5
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/mysql-adapter.d.ts +547 -6
- package/dist/adapters/mysql-adapter.d.ts.map +1 -1
- package/dist/adapters/mysql-adapter.js +651 -32
- package/dist/adapters/mysql-adapter.js.map +1 -1
- package/dist/adapters/postgresql-adapter.d.ts +15 -4
- package/dist/adapters/postgresql-adapter.d.ts.map +1 -1
- package/dist/adapters/postgresql-adapter.js +19 -2
- package/dist/adapters/postgresql-adapter.js.map +1 -1
- package/dist/adapters/sqlite-adapter.d.ts +35 -5
- package/dist/adapters/sqlite-adapter.d.ts.map +1 -1
- package/dist/adapters/sqlite-adapter.js +57 -18
- package/dist/adapters/sqlite-adapter.js.map +1 -1
- package/dist/cli/db-dump.d.ts +32 -0
- package/dist/cli/db-dump.d.ts.map +1 -0
- package/dist/cli/db-dump.js +409 -0
- package/dist/cli/db-dump.js.map +1 -0
- package/dist/cli.js +24 -14
- package/dist/cli.js.map +1 -1
- package/dist/config/knex/bootstrap/20251025020452_create_master_tables.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/bootstrap/20251025020452_create_master_tables.js +7 -2
- package/dist/config/knex/bootstrap/20251025020452_create_master_tables.js.map +1 -0
- package/dist/config/knex/bootstrap/20251025021152_create_transaction_tables.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/bootstrap/20251025021152_create_transaction_tables.js +49 -50
- package/dist/config/knex/bootstrap/20251025021152_create_transaction_tables.js.map +1 -0
- package/dist/config/knex/bootstrap/20251025021351_create_indexes.d.ts.map +1 -0
- package/dist/config/knex/bootstrap/20251025021351_create_indexes.js.map +1 -0
- package/dist/config/knex/bootstrap/20251025021416_seed_master_data.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/bootstrap/20251025021416_seed_master_data.js +11 -6
- package/dist/config/knex/bootstrap/20251025021416_seed_master_data.js.map +1 -0
- package/dist/config/knex/bootstrap/20251025070349_create_views.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/bootstrap/20251025070349_create_views.js +66 -14
- package/dist/config/knex/bootstrap/20251025070349_create_views.js.map +1 -0
- package/dist/config/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js +22 -0
- package/dist/config/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js.map +1 -0
- package/dist/config/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251025082220_fix_task_dependencies_columns.js.map +1 -0
- package/dist/config/knex/enhancements/20251025090000_create_help_system_tables.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251025090000_create_help_system_tables.js +6 -0
- package/dist/config/knex/enhancements/20251025090000_create_help_system_tables.js.map +1 -0
- package/dist/config/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js +6 -0
- package/dist/config/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js.map +1 -0
- package/dist/config/knex/enhancements/20251025100000_seed_help_metadata.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251025100000_seed_help_metadata.js +6 -0
- package/dist/config/knex/enhancements/20251025100000_seed_help_metadata.js.map +1 -0
- package/dist/config/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251025100100_seed_remaining_use_cases.js.map +1 -0
- package/dist/config/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js +7 -0
- package/dist/config/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js.map +1 -0
- package/dist/config/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251027000000_add_agent_reuse_system.js +62 -0
- package/dist/config/knex/enhancements/20251027000000_add_agent_reuse_system.js.map +1 -0
- package/dist/config/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js.map +1 -0
- package/dist/config/knex/enhancements/20251027020000_update_agent_reusability.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251027020000_update_agent_reusability.js +6 -0
- package/dist/config/knex/enhancements/20251027020000_update_agent_reusability.js.map +1 -0
- package/dist/config/knex/enhancements/20251028000000_simplify_agent_system.d.ts.map +1 -0
- package/dist/{migrations → config}/knex/enhancements/20251028000000_simplify_agent_system.js +6 -0
- package/dist/config/knex/enhancements/20251028000000_simplify_agent_system.js.map +1 -0
- package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.d.ts +13 -0
- package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.js +48 -0
- package/dist/config/knex/enhancements/20251031000000_drop_orphaned_message_view.js.map +1 -0
- package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.d.ts +24 -0
- package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.js +189 -0
- package/dist/config/knex/enhancements/20251104000003_rename_constraints_created_by_to_agent_id.js.map +1 -0
- package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.d.ts +16 -0
- package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.js +65 -0
- package/dist/config/knex/enhancements/20251105000000_add_token_usage_table.js.map +1 -0
- package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.d.ts +23 -0
- package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.d.ts.map +1 -0
- package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.js +118 -0
- package/dist/config/knex/enhancements/20251105000001_rename_decision_context_decided_by_to_agent_id.js.map +1 -0
- package/dist/config/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js.map +1 -0
- package/dist/config/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js.map +1 -0
- package/dist/config/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js.map +1 -0
- package/dist/config/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js.map +1 -0
- package/dist/config/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js.map +1 -0
- package/dist/config/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js.map +1 -0
- package/dist/config/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js.map +1 -0
- package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.d.ts +49 -0
- package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.d.ts.map +1 -0
- package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.js +864 -0
- package/dist/config/knex/upgrades/20251104000000_add_multi_project_v3_7_0.js.map +1 -0
- package/dist/config/loader.d.ts +19 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +149 -4
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +261 -2
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/config/writer.d.ts +65 -0
- package/dist/config/writer.d.ts.map +1 -0
- package/dist/config/writer.js +139 -0
- package/dist/config/writer.js.map +1 -0
- package/dist/database.d.ts +11 -2
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +62 -6
- package/dist/database.js.map +1 -1
- package/dist/index.js +173 -39
- package/dist/index.js.map +1 -1
- package/dist/knexfile.d.ts.map +1 -1
- package/dist/knexfile.js +88 -12
- package/dist/knexfile.js.map +1 -1
- package/dist/tests/all-features.test.js +15 -3
- package/dist/tests/all-features.test.js.map +1 -1
- package/dist/tests/config-loader.test.d.ts +6 -0
- package/dist/tests/config-loader.test.d.ts.map +1 -0
- package/dist/tests/config-loader.test.js +201 -0
- package/dist/tests/config-loader.test.js.map +1 -0
- package/dist/tests/connection-manager-integration.test.d.ts +2 -0
- package/dist/tests/connection-manager-integration.test.d.ts.map +1 -0
- package/dist/tests/connection-manager-integration.test.js +431 -0
- package/dist/tests/connection-manager-integration.test.js.map +1 -0
- package/dist/tests/connection-manager.test.d.ts +2 -0
- package/dist/tests/connection-manager.test.d.ts.map +1 -0
- package/dist/tests/connection-manager.test.js +361 -0
- package/dist/tests/connection-manager.test.js.map +1 -0
- package/dist/tests/dump-import.test.d.ts +15 -0
- package/dist/tests/dump-import.test.d.ts.map +1 -0
- package/dist/tests/dump-import.test.js +430 -0
- package/dist/tests/dump-import.test.js.map +1 -0
- package/dist/tests/migration-idempotency.test.d.ts +2 -0
- package/dist/tests/migration-idempotency.test.d.ts.map +1 -0
- package/dist/tests/migration-idempotency.test.js +330 -0
- package/dist/tests/migration-idempotency.test.js.map +1 -0
- package/dist/tests/migration-upgrade-paths.test.d.ts +2 -0
- package/dist/tests/migration-upgrade-paths.test.d.ts.map +1 -0
- package/dist/tests/migration-upgrade-paths.test.js +248 -0
- package/dist/tests/migration-upgrade-paths.test.js.map +1 -0
- package/dist/tests/migrations/test-all-versions-real.js +3 -0
- package/dist/tests/migrations/test-all-versions-real.js.map +1 -1
- package/dist/tests/multi-project-migration.test.d.ts +17 -0
- package/dist/tests/multi-project-migration.test.d.ts.map +1 -0
- package/dist/tests/multi-project-migration.test.js +399 -0
- package/dist/tests/multi-project-migration.test.js.map +1 -0
- package/dist/tests/multi-project.test.d.ts +5 -0
- package/dist/tests/multi-project.test.d.ts.map +1 -0
- package/dist/tests/multi-project.test.js +238 -0
- package/dist/tests/multi-project.test.js.map +1 -0
- package/dist/tests/schema-migration.test.d.ts +8 -0
- package/dist/tests/schema-migration.test.d.ts.map +1 -0
- package/dist/tests/schema-migration.test.js +108 -0
- package/dist/tests/schema-migration.test.js.map +1 -0
- package/dist/tests/sql-dump-converters.test.d.ts +7 -0
- package/dist/tests/sql-dump-converters.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-converters.test.js +314 -0
- package/dist/tests/sql-dump-converters.test.js.map +1 -0
- package/dist/tests/sql-dump-cross-database.test.d.ts +21 -0
- package/dist/tests/sql-dump-cross-database.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-cross-database.test.js +314 -0
- package/dist/tests/sql-dump-cross-database.test.js.map +1 -0
- package/dist/tests/sql-dump-default-conversions.test.d.ts +8 -0
- package/dist/tests/sql-dump-default-conversions.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-default-conversions.test.js +141 -0
- package/dist/tests/sql-dump-default-conversions.test.js.map +1 -0
- package/dist/tests/sql-dump-fk-constraints.test.d.ts +13 -0
- package/dist/tests/sql-dump-fk-constraints.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-fk-constraints.test.js +381 -0
- package/dist/tests/sql-dump-fk-constraints.test.js.map +1 -0
- package/dist/tests/sql-dump-indexes.test.d.ts +12 -0
- package/dist/tests/sql-dump-indexes.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-indexes.test.js +269 -0
- package/dist/tests/sql-dump-indexes.test.js.map +1 -0
- package/dist/tests/sql-dump-integration.test.d.ts +16 -0
- package/dist/tests/sql-dump-integration.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-integration.test.js +342 -0
- package/dist/tests/sql-dump-integration.test.js.map +1 -0
- package/dist/tests/sql-dump-table-ordering.test.d.ts +8 -0
- package/dist/tests/sql-dump-table-ordering.test.d.ts.map +1 -0
- package/dist/tests/sql-dump-table-ordering.test.js +253 -0
- package/dist/tests/sql-dump-table-ordering.test.js.map +1 -0
- package/dist/tests/tasks.link-file-backward-compat.test.js +11 -1
- package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
- package/dist/tests/tasks.watch-files-action.test.js +11 -1
- package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
- package/dist/tests/type-conversion.test.d.ts +8 -0
- package/dist/tests/type-conversion.test.d.ts.map +1 -0
- package/dist/tests/type-conversion.test.js +312 -0
- package/dist/tests/type-conversion.test.js.map +1 -0
- package/dist/tests/utils/test-helpers.d.ts +93 -0
- package/dist/tests/utils/test-helpers.d.ts.map +1 -0
- package/dist/tests/utils/test-helpers.js +407 -0
- package/dist/tests/utils/test-helpers.js.map +1 -0
- package/dist/tools/config.d.ts +58 -0
- package/dist/tools/config.d.ts.map +1 -0
- package/dist/tools/config.js +281 -0
- package/dist/tools/config.js.map +1 -0
- package/dist/tools/constraints.d.ts.map +1 -1
- package/dist/tools/constraints.js +138 -122
- package/dist/tools/constraints.js.map +1 -1
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +216 -109
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +123 -102
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +593 -518
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/utils.d.ts +5 -0
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +176 -122
- package/dist/tools/utils.js.map +1 -1
- package/dist/types.d.ts +9 -26
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/cleanup.d.ts +3 -0
- package/dist/utils/cleanup.d.ts.map +1 -1
- package/dist/utils/cleanup.js +14 -2
- package/dist/utils/cleanup.js.map +1 -1
- package/dist/utils/connection-manager.d.ts +59 -0
- package/dist/utils/connection-manager.d.ts.map +1 -0
- package/dist/utils/connection-manager.js +178 -0
- package/dist/utils/connection-manager.js.map +1 -0
- package/dist/utils/debug-logger.d.ts +8 -4
- package/dist/utils/debug-logger.d.ts.map +1 -1
- package/dist/utils/debug-logger.js +27 -7
- package/dist/utils/debug-logger.js.map +1 -1
- package/dist/utils/error-handler.d.ts +6 -4
- package/dist/utils/error-handler.d.ts.map +1 -1
- package/dist/utils/error-handler.js +34 -9
- package/dist/utils/error-handler.js.map +1 -1
- package/dist/utils/parameter-validator.d.ts.map +1 -1
- package/dist/utils/parameter-validator.js +50 -16
- package/dist/utils/parameter-validator.js.map +1 -1
- package/dist/utils/project-context.d.ts +111 -0
- package/dist/utils/project-context.d.ts.map +1 -0
- package/dist/utils/project-context.js +187 -0
- package/dist/utils/project-context.js.map +1 -0
- package/dist/utils/sql-dump-converters.d.ts +188 -0
- package/dist/utils/sql-dump-converters.d.ts.map +1 -0
- package/dist/utils/sql-dump-converters.js +311 -0
- package/dist/utils/sql-dump-converters.js.map +1 -0
- package/dist/utils/sql-dump.d.ts +102 -0
- package/dist/utils/sql-dump.d.ts.map +1 -0
- package/dist/utils/sql-dump.js +1550 -0
- package/dist/utils/sql-dump.js.map +1 -0
- package/dist/utils/vcs-adapter.d.ts +42 -0
- package/dist/utils/vcs-adapter.d.ts.map +1 -1
- package/dist/utils/vcs-adapter.js +154 -0
- package/dist/utils/vcs-adapter.js.map +1 -1
- package/docs/BASEADAPTER_IMPLEMENTATION.md +399 -0
- package/docs/DATABASE_AUTH.md +445 -0
- package/docs/DATABASE_MIGRATION.md +247 -0
- package/docs/MULTI_PROJECT_ARCHITECTURE.md +497 -0
- package/package.json +12 -4
- package/dist/migrations/knex/bootstrap/20251025020452_create_master_tables.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025020452_create_master_tables.js.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021152_create_transaction_tables.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021152_create_transaction_tables.js.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021351_create_indexes.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021351_create_indexes.js.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021416_seed_master_data.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025021416_seed_master_data.js.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025070349_create_views.d.ts.map +0 -1
- package/dist/migrations/knex/bootstrap/20251025070349_create_views.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js +0 -15
- package/dist/migrations/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025082220_fix_task_dependencies_columns.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025090000_create_help_system_tables.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025090000_create_help_system_tables.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025100000_seed_help_metadata.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025100000_seed_help_metadata.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025100100_seed_remaining_use_cases.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.js +0 -34
- package/dist/migrations/knex/enhancements/20251027000000_add_agent_reuse_system.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251027020000_update_agent_reusability.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251027020000_update_agent_reusability.js.map +0 -1
- package/dist/migrations/knex/enhancements/20251028000000_simplify_agent_system.d.ts.map +0 -1
- package/dist/migrations/knex/enhancements/20251028000000_simplify_agent_system.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js.map +0 -1
- package/dist/migrations/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts.map +0 -1
- package/dist/migrations/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js.map +0 -1
- /package/dist/{migrations → config}/knex/bootstrap/20251025020452_create_master_tables.d.ts +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025021152_create_transaction_tables.d.ts +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025021351_create_indexes.d.ts +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025021351_create_indexes.js +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025021416_seed_master_data.d.ts +0 -0
- /package/dist/{migrations → config}/knex/bootstrap/20251025070349_create_views.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025081221_add_link_type_to_task_decision_links.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025082220_fix_task_dependencies_columns.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025082220_fix_task_dependencies_columns.js +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025090000_create_help_system_tables.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025090100_seed_help_categories_and_use_cases.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025100000_seed_help_metadata.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025100100_seed_remaining_use_cases.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025100100_seed_remaining_use_cases.js +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251025120000_add_cascade_to_task_dependencies.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251027000000_add_agent_reuse_system.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251027010000_add_task_constraint_to_decision_context.js +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251027020000_update_agent_reusability.d.ts +0 -0
- /package/dist/{migrations → config}/knex/enhancements/20251028000000_simplify_agent_system.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024010000_upgrade_v1_0_to_v1_1.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024020000_upgrade_v2_0_to_v2_1.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024030000_upgrade_v2_1_to_v3_0.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024040000_upgrade_v3_0_to_v3_2.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024050000_upgrade_v3_2_0_to_v3_2_2.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024060000_upgrade_v3_4_to_v3_5.js +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.d.ts +0 -0
- /package/dist/{migrations → config}/knex/upgrades/20251024070000_upgrade_v3_5_to_v3_6.js +0 -0
package/dist/tools/tasks.js
CHANGED
|
@@ -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
|
|
277
|
-
return await
|
|
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
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
310
|
-
updateData
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
.
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
361
|
-
//
|
|
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
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
415
|
-
//
|
|
416
|
-
|
|
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
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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(
|
|
434
|
+
watchFilesParsed = JSON.parse(params.watch_files);
|
|
426
435
|
}
|
|
427
436
|
catch {
|
|
428
|
-
|
|
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
|
-
|
|
433
|
-
watchFilesParsed = params.watch_files;
|
|
459
|
+
throw new Error('Parameter "watch_files" must be a string or array');
|
|
434
460
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
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',
|
|
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
|
|
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',
|
|
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
|
|
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',
|
|
542
|
-
.
|
|
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
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
|
|
747
|
-
|
|
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
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
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
|
-
|
|
846
|
-
//
|
|
847
|
-
debugLog('WARN',
|
|
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
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
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
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
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
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
999
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
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
|
|
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
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
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
|
-
|
|
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
|
|
1152
|
-
return await
|
|
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
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
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
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
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
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
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
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
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
|
-
|
|
1255
|
-
|
|
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
|
-
|
|
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',
|
|
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.
|
|
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',
|
|
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.
|
|
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');
|