sqlew 3.6.10 → 3.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +318 -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 +165 -35
- 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/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 +581 -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 +2 -2
- package/dist/utils/error-handler.d.ts.map +1 -1
- package/dist/utils/error-handler.js +10 -7
- 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 +36 -15
- 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,8 +281,10 @@ export async function createTask(params, adapter) {
|
|
|
273
281
|
}
|
|
274
282
|
validateLength(params.title, 'Parameter "title"', 200);
|
|
275
283
|
try {
|
|
276
|
-
return await
|
|
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) {
|
|
@@ -292,181 +302,191 @@ export async function updateTask(params, adapter) {
|
|
|
292
302
|
if (!params.task_id) {
|
|
293
303
|
throw new Error('Parameter "task_id" is required');
|
|
294
304
|
}
|
|
305
|
+
// Fail-fast project_id validation (Constraint #29)
|
|
306
|
+
const projectId = getProjectContext().getProjectId();
|
|
295
307
|
try {
|
|
296
|
-
return await
|
|
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');
|
|
308
|
+
return await connectionManager.executeWithRetry(async () => {
|
|
309
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
310
|
+
const knex = actualAdapter.getKnex();
|
|
311
|
+
// Check if task exists with project_id isolation
|
|
312
|
+
const taskExists = await trx('t_tasks')
|
|
313
|
+
.where({ id: params.task_id, project_id: projectId })
|
|
314
|
+
.first();
|
|
315
|
+
if (!taskExists) {
|
|
316
|
+
throw new Error(`Task with id ${params.task_id} not found`);
|
|
308
317
|
}
|
|
309
|
-
|
|
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`);
|
|
318
|
+
// Build update data dynamically
|
|
319
|
+
const updateData = {};
|
|
320
|
+
if (params.title !== undefined) {
|
|
321
|
+
if (params.title.trim() === '') {
|
|
322
|
+
throw new Error('Parameter "title" cannot be empty');
|
|
323
|
+
}
|
|
324
|
+
validateLength(params.title, 'Parameter "title"', 200);
|
|
325
|
+
updateData.title = params.title;
|
|
324
326
|
}
|
|
325
|
-
|
|
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');
|
|
327
|
+
if (params.priority !== undefined) {
|
|
328
|
+
validatePriorityRange(params.priority);
|
|
329
|
+
updateData.priority = params.priority;
|
|
330
|
+
}
|
|
331
|
+
if (params.assigned_agent !== undefined) {
|
|
332
|
+
const agentId = await getOrCreateAgent(actualAdapter, params.assigned_agent, trx);
|
|
333
|
+
updateData.assigned_agent_id = agentId;
|
|
334
|
+
}
|
|
335
|
+
if (params.layer !== undefined) {
|
|
336
|
+
const layerId = await getLayerId(actualAdapter, params.layer, trx);
|
|
337
|
+
if (layerId === null) {
|
|
338
|
+
throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
|
|
347
339
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
340
|
+
updateData.layer_id = layerId;
|
|
341
|
+
}
|
|
342
|
+
// Update t_tasks if any updates (with project_id isolation)
|
|
343
|
+
if (Object.keys(updateData).length > 0) {
|
|
344
|
+
await trx('t_tasks')
|
|
345
|
+
.where({ id: params.task_id, project_id: projectId })
|
|
346
|
+
.update(updateData);
|
|
347
|
+
// TODO: Add activity logging for updates if needed
|
|
348
|
+
}
|
|
349
|
+
// Update t_task_details if any detail fields provided
|
|
350
|
+
if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
|
|
351
|
+
// Process acceptance_criteria (can be string or array)
|
|
352
|
+
let acceptanceCriteriaString = undefined;
|
|
353
|
+
let acceptanceCriteriaJson = undefined;
|
|
354
|
+
if (params.acceptance_criteria !== undefined) {
|
|
355
|
+
if (Array.isArray(params.acceptance_criteria)) {
|
|
356
|
+
// Array format - store as JSON in acceptance_criteria_json
|
|
357
|
+
acceptanceCriteriaJson = JSON.stringify(params.acceptance_criteria);
|
|
358
|
+
// Also create human-readable summary in acceptance_criteria
|
|
359
|
+
acceptanceCriteriaString = params.acceptance_criteria
|
|
360
|
+
.map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
|
|
361
|
+
.join('\n');
|
|
362
|
+
}
|
|
363
|
+
else if (typeof params.acceptance_criteria === 'string') {
|
|
364
|
+
// Try to parse as JSON first
|
|
365
|
+
try {
|
|
366
|
+
const parsed = JSON.parse(params.acceptance_criteria);
|
|
367
|
+
if (Array.isArray(parsed)) {
|
|
368
|
+
// It's a JSON array string - store in JSON field
|
|
369
|
+
acceptanceCriteriaJson = params.acceptance_criteria;
|
|
370
|
+
// Also create human-readable summary
|
|
371
|
+
acceptanceCriteriaString = parsed
|
|
372
|
+
.map((check, i) => `${i + 1}. ${check.type}: ${check.command || check.file || check.pattern || ''}`)
|
|
373
|
+
.join('\n');
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
// Valid JSON but not an array - store as plain text
|
|
377
|
+
acceptanceCriteriaString = params.acceptance_criteria || null;
|
|
378
|
+
acceptanceCriteriaJson = null;
|
|
379
|
+
}
|
|
359
380
|
}
|
|
360
|
-
|
|
361
|
-
//
|
|
381
|
+
catch {
|
|
382
|
+
// Not valid JSON - store as plain text
|
|
362
383
|
acceptanceCriteriaString = params.acceptance_criteria || null;
|
|
363
384
|
acceptanceCriteriaJson = null;
|
|
364
385
|
}
|
|
365
386
|
}
|
|
366
|
-
catch {
|
|
367
|
-
// Not valid JSON - store as plain text
|
|
368
|
-
acceptanceCriteriaString = params.acceptance_criteria || null;
|
|
369
|
-
acceptanceCriteriaJson = null;
|
|
370
|
-
}
|
|
371
387
|
}
|
|
372
|
-
|
|
373
|
-
|
|
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);
|
|
388
|
+
// Check if details exist (with project_id isolation)
|
|
389
|
+
const detailsExist = await trx('t_task_details')
|
|
390
|
+
.where({ task_id: params.task_id, project_id: projectId })
|
|
391
|
+
.first();
|
|
392
|
+
const detailsUpdate = {};
|
|
393
|
+
if (params.description !== undefined) {
|
|
394
|
+
detailsUpdate.description = params.description || null;
|
|
395
|
+
}
|
|
396
|
+
if (acceptanceCriteriaString !== undefined) {
|
|
397
|
+
detailsUpdate.acceptance_criteria = acceptanceCriteriaString;
|
|
398
|
+
}
|
|
399
|
+
if (acceptanceCriteriaJson !== undefined) {
|
|
400
|
+
detailsUpdate.acceptance_criteria_json = acceptanceCriteriaJson;
|
|
401
|
+
}
|
|
402
|
+
if (params.notes !== undefined) {
|
|
403
|
+
detailsUpdate.notes = params.notes || null;
|
|
413
404
|
}
|
|
414
|
-
|
|
415
|
-
//
|
|
416
|
-
|
|
405
|
+
if (detailsExist && Object.keys(detailsUpdate).length > 0) {
|
|
406
|
+
// Update existing details (with project_id isolation)
|
|
407
|
+
await trx('t_task_details')
|
|
408
|
+
.where({ task_id: params.task_id, project_id: projectId })
|
|
409
|
+
.update(detailsUpdate);
|
|
410
|
+
}
|
|
411
|
+
else if (!detailsExist) {
|
|
412
|
+
// Insert new details
|
|
413
|
+
await trx('t_task_details').insert({
|
|
414
|
+
project_id: projectId,
|
|
415
|
+
task_id: params.task_id,
|
|
416
|
+
description: params.description || null,
|
|
417
|
+
acceptance_criteria: acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null,
|
|
418
|
+
acceptance_criteria_json: acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null,
|
|
419
|
+
notes: params.notes || null
|
|
420
|
+
});
|
|
417
421
|
}
|
|
418
422
|
}
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
423
|
+
// Handle watch_files if provided (v3.4.1)
|
|
424
|
+
if (params.watch_files && params.watch_files.length > 0) {
|
|
425
|
+
// Parse watch_files - handle MCP SDK converting JSON string to char array
|
|
426
|
+
let watchFilesParsed;
|
|
427
|
+
if (typeof params.watch_files === 'string') {
|
|
428
|
+
// String - try to parse as JSON
|
|
424
429
|
try {
|
|
425
|
-
watchFilesParsed = JSON.parse(
|
|
430
|
+
watchFilesParsed = JSON.parse(params.watch_files);
|
|
426
431
|
}
|
|
427
432
|
catch {
|
|
428
|
-
|
|
433
|
+
// If not valid JSON, treat as single file path
|
|
434
|
+
watchFilesParsed = [params.watch_files];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
else if (Array.isArray(params.watch_files)) {
|
|
438
|
+
// Check if it's an array of single characters (MCP SDK bug)
|
|
439
|
+
if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
|
|
440
|
+
// Join characters back into string and parse JSON
|
|
441
|
+
const jsonString = params.watch_files.join('');
|
|
442
|
+
try {
|
|
443
|
+
watchFilesParsed = JSON.parse(jsonString);
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
throw new Error(`Invalid watch_files format: ${jsonString}`);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
// Normal array of file paths
|
|
451
|
+
watchFilesParsed = params.watch_files;
|
|
429
452
|
}
|
|
430
453
|
}
|
|
431
454
|
else {
|
|
432
|
-
|
|
433
|
-
watchFilesParsed = params.watch_files;
|
|
455
|
+
throw new Error('Parameter "watch_files" must be a string or array');
|
|
434
456
|
}
|
|
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);
|
|
457
|
+
for (const filePath of watchFilesParsed) {
|
|
458
|
+
const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
|
|
459
|
+
await trx('t_task_file_links').insert({
|
|
460
|
+
project_id: projectId,
|
|
461
|
+
task_id: params.task_id,
|
|
462
|
+
file_id: fileId
|
|
463
|
+
}).onConflict(['project_id', 'task_id', 'file_id']).ignore();
|
|
464
|
+
}
|
|
465
|
+
// Register files with watcher for auto-tracking
|
|
466
|
+
try {
|
|
467
|
+
const taskData = await trx('t_tasks as t')
|
|
468
|
+
.join('m_task_statuses as s', 't.status_id', 's.id')
|
|
469
|
+
.where({ 't.id': params.task_id, 't.project_id': projectId })
|
|
470
|
+
.select('t.title', 's.name as status')
|
|
471
|
+
.first();
|
|
472
|
+
if (taskData) {
|
|
473
|
+
const watcher = FileWatcher.getInstance();
|
|
474
|
+
for (const filePath of watchFilesParsed) {
|
|
475
|
+
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
476
|
+
}
|
|
457
477
|
}
|
|
458
478
|
}
|
|
479
|
+
catch (error) {
|
|
480
|
+
// Watcher may not be initialized yet, ignore
|
|
481
|
+
debugLog('WARN', 'Could not register files with watcher', { error });
|
|
482
|
+
}
|
|
459
483
|
}
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
success: true,
|
|
467
|
-
task_id: params.task_id,
|
|
468
|
-
message: `Task ${params.task_id} updated successfully`
|
|
469
|
-
};
|
|
484
|
+
return {
|
|
485
|
+
success: true,
|
|
486
|
+
task_id: params.task_id,
|
|
487
|
+
message: `Task ${params.task_id} updated successfully`
|
|
488
|
+
};
|
|
489
|
+
});
|
|
470
490
|
});
|
|
471
491
|
}
|
|
472
492
|
catch (error) {
|
|
@@ -479,6 +499,7 @@ export async function updateTask(params, adapter) {
|
|
|
479
499
|
*/
|
|
480
500
|
async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
|
|
481
501
|
const knex = adapter.getKnex();
|
|
502
|
+
const projectId = getProjectContext().getProjectId();
|
|
482
503
|
// Build query based on include_details flag
|
|
483
504
|
const selectFields = includeDetails
|
|
484
505
|
? [
|
|
@@ -497,26 +518,34 @@ async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
|
|
|
497
518
|
's.name as status',
|
|
498
519
|
't.priority'
|
|
499
520
|
];
|
|
500
|
-
// Get blockers (tasks that block this task)
|
|
521
|
+
// Get blockers (tasks that block this task) - with project_id isolation
|
|
501
522
|
let blockersQuery = knex('t_tasks as t')
|
|
502
523
|
.join('t_task_dependencies as d', 't.id', 'd.blocker_task_id')
|
|
503
524
|
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
504
525
|
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
505
|
-
.where('d.blocked_task_id',
|
|
526
|
+
.where({ 'd.blocked_task_id': taskId, 'd.project_id': projectId, 't.project_id': projectId })
|
|
506
527
|
.select(selectFields);
|
|
507
528
|
if (includeDetails) {
|
|
508
|
-
blockersQuery = blockersQuery
|
|
529
|
+
blockersQuery = blockersQuery
|
|
530
|
+
.leftJoin('t_task_details as td', function () {
|
|
531
|
+
this.on('t.id', '=', 'td.task_id')
|
|
532
|
+
.andOn('t.project_id', '=', 'td.project_id');
|
|
533
|
+
});
|
|
509
534
|
}
|
|
510
535
|
const blockers = await blockersQuery;
|
|
511
|
-
// Get blocking (tasks this task blocks)
|
|
536
|
+
// Get blocking (tasks this task blocks) - with project_id isolation
|
|
512
537
|
let blockingQuery = knex('t_tasks as t')
|
|
513
538
|
.join('t_task_dependencies as d', 't.id', 'd.blocked_task_id')
|
|
514
539
|
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
515
540
|
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
516
|
-
.where('d.blocker_task_id',
|
|
541
|
+
.where({ 'd.blocker_task_id': taskId, 'd.project_id': projectId, 't.project_id': projectId })
|
|
517
542
|
.select(selectFields);
|
|
518
543
|
if (includeDetails) {
|
|
519
|
-
blockingQuery = blockingQuery
|
|
544
|
+
blockingQuery = blockingQuery
|
|
545
|
+
.leftJoin('t_task_details as td', function () {
|
|
546
|
+
this.on('t.id', '=', 'td.task_id')
|
|
547
|
+
.andOn('t.project_id', '=', 'td.project_id');
|
|
548
|
+
});
|
|
520
549
|
}
|
|
521
550
|
const blocking = await blockingQuery;
|
|
522
551
|
return { blockers, blocking };
|
|
@@ -531,15 +560,20 @@ export async function getTask(params, adapter) {
|
|
|
531
560
|
if (!params.task_id) {
|
|
532
561
|
throw new Error('Parameter "task_id" is required');
|
|
533
562
|
}
|
|
563
|
+
// Fail-fast project_id validation (Constraint #29)
|
|
564
|
+
const projectId = getProjectContext().getProjectId();
|
|
534
565
|
try {
|
|
535
|
-
// Get task with details
|
|
566
|
+
// Get task with details (with project_id isolation)
|
|
536
567
|
const task = await knex('t_tasks as t')
|
|
537
568
|
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
538
569
|
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
539
570
|
.leftJoin('m_agents as ca', 't.created_by_agent_id', 'ca.id')
|
|
540
571
|
.leftJoin('m_layers as l', 't.layer_id', 'l.id')
|
|
541
|
-
.leftJoin('t_task_details as td',
|
|
542
|
-
.
|
|
572
|
+
.leftJoin('t_task_details as td', function () {
|
|
573
|
+
this.on('t.id', '=', 'td.task_id')
|
|
574
|
+
.andOn('t.project_id', '=', 'td.project_id');
|
|
575
|
+
})
|
|
576
|
+
.where({ 't.id': params.task_id, 't.project_id': projectId })
|
|
543
577
|
.select('t.id', 't.title', 's.name as status', 't.priority', 'aa.name as assigned_to', 'ca.name as created_by', 'l.name as layer', 't.created_ts', 't.updated_ts', 't.completed_ts', 'td.description', 'td.acceptance_criteria', 'td.notes')
|
|
544
578
|
.first();
|
|
545
579
|
if (!task) {
|
|
@@ -603,6 +637,8 @@ export async function listTasks(params = {}, adapter) {
|
|
|
603
637
|
validateActionParams('task', 'list', params);
|
|
604
638
|
const actualAdapter = adapter ?? getAdapter();
|
|
605
639
|
const knex = actualAdapter.getKnex();
|
|
640
|
+
// Get current project ID for filtering (Constraint #22)
|
|
641
|
+
const projectId = getProjectContext().getProjectId();
|
|
606
642
|
try {
|
|
607
643
|
// Run auto-stale detection, git-aware completion, and auto-archive before listing
|
|
608
644
|
const transitionCount = await detectAndTransitionStaleTasks(actualAdapter);
|
|
@@ -632,6 +668,8 @@ export async function listTasks(params = {}, adapter) {
|
|
|
632
668
|
// Standard query without dependency counts
|
|
633
669
|
query = knex('v_task_board');
|
|
634
670
|
}
|
|
671
|
+
// Filter by project_id (Constraint #22: Multi-project isolation)
|
|
672
|
+
query = query.where(params.include_dependency_counts ? 'vt.project_id' : 'project_id', projectId);
|
|
635
673
|
// Filter by status
|
|
636
674
|
if (params.status) {
|
|
637
675
|
if (!STATUS_TO_ID[params.status]) {
|
|
@@ -696,64 +734,66 @@ export async function moveTask(params, adapter) {
|
|
|
696
734
|
// Run auto-stale detection and auto-archive before move
|
|
697
735
|
await detectAndTransitionStaleTasks(actualAdapter);
|
|
698
736
|
await autoArchiveOldDoneTasks(actualAdapter);
|
|
699
|
-
return await
|
|
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);
|
|
737
|
+
return await connectionManager.executeWithRetry(async () => {
|
|
738
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
739
|
+
// Get current status
|
|
740
|
+
const taskRow = await trx('t_tasks')
|
|
741
|
+
.where({ id: params.task_id })
|
|
742
|
+
.select('status_id')
|
|
743
|
+
.first();
|
|
744
|
+
if (!taskRow) {
|
|
745
|
+
throw new Error(`Task with id ${params.task_id} not found`);
|
|
745
746
|
}
|
|
746
|
-
|
|
747
|
-
|
|
747
|
+
const currentStatusId = taskRow.status_id;
|
|
748
|
+
const newStatusId = STATUS_TO_ID[params.new_status];
|
|
749
|
+
if (!newStatusId) {
|
|
750
|
+
throw new Error(`Invalid new_status: ${params.new_status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
|
|
748
751
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
752
|
+
// Check if transition is valid
|
|
753
|
+
const validNextStatuses = VALID_TRANSITIONS[currentStatusId] || [];
|
|
754
|
+
if (!validNextStatuses.includes(newStatusId)) {
|
|
755
|
+
throw new Error(`Invalid transition from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}. ` +
|
|
756
|
+
`Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
|
|
757
|
+
}
|
|
758
|
+
// Update status
|
|
759
|
+
const updateData = {
|
|
760
|
+
status_id: newStatusId
|
|
761
|
+
};
|
|
762
|
+
// Set completed_ts when moving to done
|
|
763
|
+
if (newStatusId === TASK_STATUS.DONE) {
|
|
764
|
+
updateData.completed_ts = Math.floor(Date.now() / 1000);
|
|
765
|
+
}
|
|
766
|
+
await trx('t_tasks')
|
|
767
|
+
.where({ id: params.task_id })
|
|
768
|
+
.update(updateData);
|
|
769
|
+
// Activity logging (replaces trigger)
|
|
770
|
+
// Note: Using system agent (id=1) for status changes
|
|
771
|
+
// In a real implementation, you'd pass the actual agent_id who made the change
|
|
772
|
+
const systemAgentId = 1;
|
|
773
|
+
await logTaskStatusChange(trx, {
|
|
774
|
+
task_id: params.task_id,
|
|
775
|
+
old_status: currentStatusId,
|
|
776
|
+
new_status: newStatusId,
|
|
777
|
+
agent_id: systemAgentId
|
|
778
|
+
});
|
|
779
|
+
// Update watcher if moving to done or archived (stop watching)
|
|
780
|
+
if (params.new_status === 'done' || params.new_status === 'archived') {
|
|
781
|
+
try {
|
|
782
|
+
const watcher = FileWatcher.getInstance();
|
|
783
|
+
watcher.unregisterTask(params.task_id);
|
|
784
|
+
}
|
|
785
|
+
catch (error) {
|
|
786
|
+
// Watcher may not be initialized, ignore
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
return {
|
|
790
|
+
success: true,
|
|
791
|
+
task_id: params.task_id,
|
|
792
|
+
old_status: ID_TO_STATUS[currentStatusId],
|
|
793
|
+
new_status: params.new_status,
|
|
794
|
+
message: `Task ${params.task_id} moved from ${ID_TO_STATUS[currentStatusId]} to ${params.new_status}`
|
|
795
|
+
};
|
|
796
|
+
});
|
|
757
797
|
});
|
|
758
798
|
}
|
|
759
799
|
catch (error) {
|
|
@@ -778,86 +818,88 @@ export async function linkTask(params, adapter) {
|
|
|
778
818
|
throw new Error('Parameter "target_id" is required');
|
|
779
819
|
}
|
|
780
820
|
try {
|
|
781
|
-
return await
|
|
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`);
|
|
821
|
+
return await connectionManager.executeWithRetry(async () => {
|
|
822
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
823
|
+
// Check if task exists
|
|
824
|
+
const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
|
|
825
|
+
if (!taskExists) {
|
|
826
|
+
throw new Error(`Task with id ${params.task_id} not found`);
|
|
811
827
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
.
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
828
|
+
if (params.link_type === 'decision') {
|
|
829
|
+
const decisionKey = String(params.target_id);
|
|
830
|
+
const keyId = await getOrCreateContextKey(actualAdapter, decisionKey, trx);
|
|
831
|
+
const linkRelation = params.link_relation || 'implements';
|
|
832
|
+
await trx('t_task_decision_links').insert({
|
|
833
|
+
task_id: params.task_id,
|
|
834
|
+
decision_key_id: keyId,
|
|
835
|
+
link_type: linkRelation
|
|
836
|
+
}).onConflict(['task_id', 'decision_key_id']).merge();
|
|
837
|
+
return {
|
|
838
|
+
success: true,
|
|
839
|
+
task_id: params.task_id,
|
|
840
|
+
linked_to: 'decision',
|
|
841
|
+
target: decisionKey,
|
|
842
|
+
relation: linkRelation,
|
|
843
|
+
message: `Task ${params.task_id} linked to decision "${decisionKey}"`
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
else if (params.link_type === 'constraint') {
|
|
847
|
+
const constraintId = Number(params.target_id);
|
|
848
|
+
// Check if constraint exists
|
|
849
|
+
const constraintExists = await trx('t_constraints').where({ id: constraintId }).first();
|
|
850
|
+
if (!constraintExists) {
|
|
851
|
+
throw new Error(`Constraint with id ${constraintId} not found`);
|
|
852
|
+
}
|
|
853
|
+
await trx('t_task_constraint_links').insert({
|
|
854
|
+
task_id: params.task_id,
|
|
855
|
+
constraint_id: constraintId
|
|
856
|
+
}).onConflict(['task_id', 'constraint_id']).ignore();
|
|
857
|
+
return {
|
|
858
|
+
success: true,
|
|
859
|
+
task_id: params.task_id,
|
|
860
|
+
linked_to: 'constraint',
|
|
861
|
+
target: constraintId,
|
|
862
|
+
message: `Task ${params.task_id} linked to constraint ${constraintId}`
|
|
863
|
+
};
|
|
864
|
+
}
|
|
865
|
+
else if (params.link_type === 'file') {
|
|
866
|
+
// Deprecation warning (v3.4.1)
|
|
867
|
+
debugLog('WARN', `DEPRECATION: task.link(link_type="file") is deprecated as of v3.4.1. Use task.create(watch_files=[...]) or task.update(watch_files=[...]) instead. Or use the new watch_files action: { action: "watch_files", task_id: ${params.task_id}, file_paths: ["..."] }`);
|
|
868
|
+
const filePath = String(params.target_id);
|
|
869
|
+
const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
|
|
870
|
+
await trx('t_task_file_links').insert({
|
|
871
|
+
task_id: params.task_id,
|
|
872
|
+
file_id: fileId
|
|
873
|
+
}).onConflict(['task_id', 'file_id']).ignore();
|
|
874
|
+
// Register file with watcher for auto-tracking
|
|
875
|
+
try {
|
|
876
|
+
const taskData = await trx('t_tasks as t')
|
|
877
|
+
.join('m_task_statuses as s', 't.status_id', 's.id')
|
|
878
|
+
.where('t.id', params.task_id)
|
|
879
|
+
.select('t.title', 's.name as status')
|
|
880
|
+
.first();
|
|
881
|
+
if (taskData) {
|
|
882
|
+
const watcher = FileWatcher.getInstance();
|
|
883
|
+
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
// Watcher may not be initialized yet, ignore
|
|
888
|
+
debugLog('WARN', 'Could not register file with watcher', { error });
|
|
843
889
|
}
|
|
890
|
+
return {
|
|
891
|
+
success: true,
|
|
892
|
+
task_id: params.task_id,
|
|
893
|
+
linked_to: 'file',
|
|
894
|
+
target: filePath,
|
|
895
|
+
deprecation_warning: 'task.link(link_type="file") is deprecated. Use task.create/update(watch_files) or watch_files action instead.',
|
|
896
|
+
message: `Task ${params.task_id} linked to file "${filePath}" (DEPRECATED API - use watch_files instead)`
|
|
897
|
+
};
|
|
844
898
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
debugLog('WARN', 'Could not register file with watcher', { error });
|
|
899
|
+
else {
|
|
900
|
+
throw new Error(`Invalid link_type: ${params.link_type}. Must be one of: decision, constraint, file`);
|
|
848
901
|
}
|
|
849
|
-
|
|
850
|
-
success: true,
|
|
851
|
-
task_id: params.task_id,
|
|
852
|
-
linked_to: 'file',
|
|
853
|
-
target: filePath,
|
|
854
|
-
deprecation_warning: 'task.link(link_type="file") is deprecated. Use task.create/update(watch_files) or watch_files action instead.',
|
|
855
|
-
message: `Task ${params.task_id} linked to file "${filePath}" (DEPRECATED API - use watch_files instead)`
|
|
856
|
-
};
|
|
857
|
-
}
|
|
858
|
-
else {
|
|
859
|
-
throw new Error(`Invalid link_type: ${params.link_type}. Must be one of: decision, constraint, file`);
|
|
860
|
-
}
|
|
902
|
+
});
|
|
861
903
|
});
|
|
862
904
|
}
|
|
863
905
|
catch (error) {
|
|
@@ -876,44 +918,46 @@ export async function archiveTask(params, adapter) {
|
|
|
876
918
|
throw new Error('Parameter "task_id" is required');
|
|
877
919
|
}
|
|
878
920
|
try {
|
|
879
|
-
return await
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
921
|
+
return await connectionManager.executeWithRetry(async () => {
|
|
922
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
923
|
+
// Check if task is in 'done' status
|
|
924
|
+
const taskRow = await trx('t_tasks')
|
|
925
|
+
.where({ id: params.task_id })
|
|
926
|
+
.select('status_id')
|
|
927
|
+
.first();
|
|
928
|
+
if (!taskRow) {
|
|
929
|
+
throw new Error(`Task with id ${params.task_id} not found`);
|
|
930
|
+
}
|
|
931
|
+
if (taskRow.status_id !== TASK_STATUS.DONE) {
|
|
932
|
+
throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
|
|
933
|
+
}
|
|
934
|
+
// Update to archived
|
|
935
|
+
await trx('t_tasks')
|
|
936
|
+
.where({ id: params.task_id })
|
|
937
|
+
.update({ status_id: TASK_STATUS.ARCHIVED });
|
|
938
|
+
// Activity logging
|
|
939
|
+
// Note: Using system agent (id=1) for status changes
|
|
940
|
+
const systemAgentId = 1;
|
|
941
|
+
await logTaskStatusChange(trx, {
|
|
942
|
+
task_id: params.task_id,
|
|
943
|
+
old_status: TASK_STATUS.DONE,
|
|
944
|
+
new_status: TASK_STATUS.ARCHIVED,
|
|
945
|
+
agent_id: systemAgentId
|
|
946
|
+
});
|
|
947
|
+
// Unregister from file watcher (archived tasks don't need tracking)
|
|
948
|
+
try {
|
|
949
|
+
const watcher = FileWatcher.getInstance();
|
|
950
|
+
watcher.unregisterTask(params.task_id);
|
|
951
|
+
}
|
|
952
|
+
catch (error) {
|
|
953
|
+
// Watcher may not be initialized, ignore
|
|
954
|
+
}
|
|
955
|
+
return {
|
|
956
|
+
success: true,
|
|
957
|
+
task_id: params.task_id,
|
|
958
|
+
message: `Task ${params.task_id} archived successfully`
|
|
959
|
+
};
|
|
903
960
|
});
|
|
904
|
-
// Unregister from file watcher (archived tasks don't need tracking)
|
|
905
|
-
try {
|
|
906
|
-
const watcher = FileWatcher.getInstance();
|
|
907
|
-
watcher.unregisterTask(params.task_id);
|
|
908
|
-
}
|
|
909
|
-
catch (error) {
|
|
910
|
-
// Watcher may not be initialized, ignore
|
|
911
|
-
}
|
|
912
|
-
return {
|
|
913
|
-
success: true,
|
|
914
|
-
task_id: params.task_id,
|
|
915
|
-
message: `Task ${params.task_id} archived successfully`
|
|
916
|
-
};
|
|
917
961
|
});
|
|
918
962
|
}
|
|
919
963
|
catch (error) {
|
|
@@ -935,95 +979,97 @@ export async function addDependency(params, adapter) {
|
|
|
935
979
|
throw new Error('Parameter "blocked_task_id" is required');
|
|
936
980
|
}
|
|
937
981
|
try {
|
|
938
|
-
return await
|
|
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(`
|
|
982
|
+
return await connectionManager.executeWithRetry(async () => {
|
|
983
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
984
|
+
// Validation 1: No self-dependencies
|
|
985
|
+
if (params.blocker_task_id === params.blocked_task_id) {
|
|
986
|
+
throw new Error('Self-dependency not allowed');
|
|
987
|
+
}
|
|
988
|
+
// Validation 2: Both tasks must exist and check if archived
|
|
989
|
+
const blockerTask = await trx('t_tasks')
|
|
990
|
+
.where({ id: params.blocker_task_id })
|
|
991
|
+
.select('id', 'status_id')
|
|
992
|
+
.first();
|
|
993
|
+
const blockedTask = await trx('t_tasks')
|
|
994
|
+
.where({ id: params.blocked_task_id })
|
|
995
|
+
.select('id', 'status_id')
|
|
996
|
+
.first();
|
|
997
|
+
if (!blockerTask) {
|
|
998
|
+
throw new Error(`Blocker task #${params.blocker_task_id} not found`);
|
|
999
|
+
}
|
|
1000
|
+
if (!blockedTask) {
|
|
1001
|
+
throw new Error(`Blocked task #${params.blocked_task_id} not found`);
|
|
1002
|
+
}
|
|
1003
|
+
// Validation 3: Neither task is archived
|
|
1004
|
+
if (blockerTask.status_id === TASK_STATUS.ARCHIVED) {
|
|
1005
|
+
throw new Error(`Cannot add dependency: Task #${params.blocker_task_id} is archived`);
|
|
1006
|
+
}
|
|
1007
|
+
if (blockedTask.status_id === TASK_STATUS.ARCHIVED) {
|
|
1008
|
+
throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
|
|
1009
|
+
}
|
|
1010
|
+
// Validation 4: No direct circular (reverse relationship)
|
|
1011
|
+
const reverseExists = await trx('t_task_dependencies')
|
|
1012
|
+
.where({
|
|
1013
|
+
blocker_task_id: params.blocked_task_id,
|
|
1014
|
+
blocked_task_id: params.blocker_task_id
|
|
1015
|
+
})
|
|
1016
|
+
.first();
|
|
1017
|
+
if (reverseExists) {
|
|
1018
|
+
throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
|
|
1019
|
+
}
|
|
1020
|
+
// Validation 5: No transitive circular (check if adding this would create a cycle)
|
|
1021
|
+
const cycleCheck = await trx.raw(`
|
|
997
1022
|
WITH RECURSIVE dependency_chain AS (
|
|
998
|
-
|
|
999
|
-
|
|
1023
|
+
-- Start from the task that would be blocked
|
|
1024
|
+
SELECT blocked_task_id as task_id, 1 as depth
|
|
1000
1025
|
FROM t_task_dependencies
|
|
1001
1026
|
WHERE blocker_task_id = ?
|
|
1002
1027
|
|
|
1003
1028
|
UNION ALL
|
|
1004
1029
|
|
|
1005
|
-
|
|
1006
|
-
|
|
1030
|
+
-- Follow the chain of dependencies
|
|
1031
|
+
SELECT d.blocked_task_id, dc.depth + 1
|
|
1007
1032
|
FROM t_task_dependencies d
|
|
1008
1033
|
JOIN dependency_chain dc ON d.blocker_task_id = dc.task_id
|
|
1009
1034
|
WHERE dc.depth < 100
|
|
1010
1035
|
)
|
|
1011
|
-
SELECT
|
|
1036
|
+
SELECT task_id FROM dependency_chain WHERE task_id = ?
|
|
1012
1037
|
`, [params.blocked_task_id, params.blocker_task_id])
|
|
1013
1038
|
.then((result) => result[0]);
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1039
|
+
if (cycleCheck) {
|
|
1040
|
+
// Build cycle path for error message
|
|
1041
|
+
const cyclePathResult = await trx.raw(`
|
|
1042
|
+
WITH RECURSIVE dependency_chain AS (
|
|
1043
|
+
SELECT blocked_task_id as task_id, 1 as depth,
|
|
1044
|
+
CAST(blocked_task_id AS TEXT) as path
|
|
1045
|
+
FROM t_task_dependencies
|
|
1046
|
+
WHERE blocker_task_id = ?
|
|
1047
|
+
|
|
1048
|
+
UNION ALL
|
|
1049
|
+
|
|
1050
|
+
SELECT d.blocked_task_id, dc.depth + 1,
|
|
1051
|
+
dc.path || ' → ' || d.blocked_task_id
|
|
1052
|
+
FROM t_task_dependencies d
|
|
1053
|
+
JOIN dependency_chain dc ON d.blocker_task_id = dc.task_id
|
|
1054
|
+
WHERE dc.depth < 100
|
|
1055
|
+
)
|
|
1056
|
+
SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
|
|
1057
|
+
`, [params.blocked_task_id, params.blocker_task_id])
|
|
1058
|
+
.then((result) => result[0]);
|
|
1059
|
+
const cyclePath = cyclePathResult?.path || `#${params.blocked_task_id} → ... → #${params.blocker_task_id}`;
|
|
1060
|
+
throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
|
|
1061
|
+
}
|
|
1062
|
+
// All validations passed - insert dependency
|
|
1063
|
+
await trx('t_task_dependencies').insert({
|
|
1064
|
+
blocker_task_id: params.blocker_task_id,
|
|
1065
|
+
blocked_task_id: params.blocked_task_id,
|
|
1066
|
+
created_ts: Math.floor(Date.now() / 1000)
|
|
1067
|
+
});
|
|
1068
|
+
return {
|
|
1069
|
+
success: true,
|
|
1070
|
+
message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
|
|
1071
|
+
};
|
|
1022
1072
|
});
|
|
1023
|
-
return {
|
|
1024
|
-
success: true,
|
|
1025
|
-
message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
|
|
1026
|
-
};
|
|
1027
1073
|
});
|
|
1028
1074
|
}
|
|
1029
1075
|
catch (error) {
|
|
@@ -1115,24 +1161,26 @@ export async function batchCreateTasks(params, adapter) {
|
|
|
1115
1161
|
try {
|
|
1116
1162
|
if (atomic) {
|
|
1117
1163
|
// Atomic mode: All or nothing
|
|
1118
|
-
const results = await
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1164
|
+
const results = await connectionManager.executeWithRetry(async () => {
|
|
1165
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
1166
|
+
const processedResults = [];
|
|
1167
|
+
for (const task of params.tasks) {
|
|
1168
|
+
try {
|
|
1169
|
+
const result = await createTaskInternal(task, actualAdapter, trx);
|
|
1170
|
+
processedResults.push({
|
|
1171
|
+
title: task.title,
|
|
1172
|
+
task_id: result.task_id,
|
|
1173
|
+
success: true,
|
|
1174
|
+
error: undefined
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
catch (error) {
|
|
1178
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1179
|
+
throw new Error(`Batch failed at task "${task.title}": ${errorMessage}`);
|
|
1180
|
+
}
|
|
1133
1181
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1182
|
+
return processedResults;
|
|
1183
|
+
});
|
|
1136
1184
|
});
|
|
1137
1185
|
return {
|
|
1138
1186
|
success: true,
|
|
@@ -1148,8 +1196,10 @@ export async function batchCreateTasks(params, adapter) {
|
|
|
1148
1196
|
let failed = 0;
|
|
1149
1197
|
for (const task of params.tasks) {
|
|
1150
1198
|
try {
|
|
1151
|
-
const result = await
|
|
1152
|
-
return await
|
|
1199
|
+
const result = await connectionManager.executeWithRetry(async () => {
|
|
1200
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
1201
|
+
return await createTaskInternal(task, actualAdapter, trx);
|
|
1202
|
+
});
|
|
1153
1203
|
});
|
|
1154
1204
|
results.push({
|
|
1155
1205
|
title: task.title,
|
|
@@ -1191,6 +1241,7 @@ export async function watchFiles(params, adapter) {
|
|
|
1191
1241
|
validateActionParams('task', 'watch_files', params);
|
|
1192
1242
|
const actualAdapter = adapter ?? getAdapter();
|
|
1193
1243
|
const knex = actualAdapter.getKnex();
|
|
1244
|
+
const projectId = getProjectContext().getProjectId();
|
|
1194
1245
|
if (!params.task_id) {
|
|
1195
1246
|
throw new Error('Parameter "task_id" is required');
|
|
1196
1247
|
}
|
|
@@ -1198,98 +1249,100 @@ export async function watchFiles(params, adapter) {
|
|
|
1198
1249
|
throw new Error('Parameter "action" is required (watch, unwatch, or list)');
|
|
1199
1250
|
}
|
|
1200
1251
|
try {
|
|
1201
|
-
return await
|
|
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');
|
|
1252
|
+
return await connectionManager.executeWithRetry(async () => {
|
|
1253
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
1254
|
+
// Check if task exists (project-scoped)
|
|
1255
|
+
const taskData = await trx('t_tasks as t')
|
|
1256
|
+
.join('m_task_statuses as s', 't.status_id', 's.id')
|
|
1257
|
+
.where({ 't.id': params.task_id, 't.project_id': projectId })
|
|
1258
|
+
.select('t.id', 't.title', 's.name as status')
|
|
1259
|
+
.first();
|
|
1260
|
+
if (!taskData) {
|
|
1261
|
+
throw new Error(`Task with id ${params.task_id} not found`);
|
|
1214
1262
|
}
|
|
1215
|
-
|
|
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);
|
|
1263
|
+
if (params.action === 'watch') {
|
|
1264
|
+
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1265
|
+
throw new Error('Parameter "file_paths" is required for watch action');
|
|
1228
1266
|
}
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1267
|
+
const addedFiles = [];
|
|
1268
|
+
for (const filePath of params.file_paths) {
|
|
1269
|
+
const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
|
|
1270
|
+
// Check if already exists
|
|
1271
|
+
const existing = await trx('t_task_file_links')
|
|
1272
|
+
.where({ task_id: params.task_id, file_id: fileId })
|
|
1273
|
+
.first();
|
|
1274
|
+
if (!existing) {
|
|
1275
|
+
await trx('t_task_file_links').insert({
|
|
1276
|
+
task_id: params.task_id,
|
|
1277
|
+
file_id: fileId
|
|
1278
|
+
});
|
|
1279
|
+
addedFiles.push(filePath);
|
|
1280
|
+
}
|
|
1235
1281
|
}
|
|
1282
|
+
// Register files with watcher
|
|
1283
|
+
try {
|
|
1284
|
+
const watcher = FileWatcher.getInstance();
|
|
1285
|
+
for (const filePath of addedFiles) {
|
|
1286
|
+
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
catch (error) {
|
|
1290
|
+
// Watcher may not be initialized yet, ignore
|
|
1291
|
+
debugLog('WARN', 'Could not register files with watcher', { error });
|
|
1292
|
+
}
|
|
1293
|
+
return {
|
|
1294
|
+
success: true,
|
|
1295
|
+
task_id: params.task_id,
|
|
1296
|
+
action: 'watch',
|
|
1297
|
+
files_added: addedFiles.length,
|
|
1298
|
+
files: addedFiles,
|
|
1299
|
+
message: `Watching ${addedFiles.length} file(s) for task ${params.task_id}`
|
|
1300
|
+
};
|
|
1236
1301
|
}
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1302
|
+
else if (params.action === 'unwatch') {
|
|
1303
|
+
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1304
|
+
throw new Error('Parameter "file_paths" is required for unwatch action');
|
|
1305
|
+
}
|
|
1306
|
+
const removedFiles = [];
|
|
1307
|
+
for (const filePath of params.file_paths) {
|
|
1308
|
+
const deleted = await trx('t_task_file_links')
|
|
1309
|
+
.where('task_id', params.task_id)
|
|
1310
|
+
.whereIn('file_id', function () {
|
|
1311
|
+
this.select('id').from('m_files').where({ path: filePath });
|
|
1312
|
+
})
|
|
1313
|
+
.delete();
|
|
1314
|
+
if (deleted > 0) {
|
|
1315
|
+
removedFiles.push(filePath);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
return {
|
|
1319
|
+
success: true,
|
|
1320
|
+
task_id: params.task_id,
|
|
1321
|
+
action: 'unwatch',
|
|
1322
|
+
files_removed: removedFiles.length,
|
|
1323
|
+
files: removedFiles,
|
|
1324
|
+
message: `Stopped watching ${removedFiles.length} file(s) for task ${params.task_id}`
|
|
1325
|
+
};
|
|
1240
1326
|
}
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1327
|
+
else if (params.action === 'list') {
|
|
1328
|
+
const files = await trx('t_task_file_links as tfl')
|
|
1329
|
+
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
1330
|
+
.where('tfl.task_id', params.task_id)
|
|
1331
|
+
.select('f.path')
|
|
1332
|
+
.then(rows => rows.map((row) => row.path));
|
|
1333
|
+
return {
|
|
1334
|
+
success: true,
|
|
1335
|
+
task_id: params.task_id,
|
|
1336
|
+
action: 'list',
|
|
1337
|
+
files_count: files.length,
|
|
1338
|
+
files: files,
|
|
1339
|
+
message: `Task ${params.task_id} is watching ${files.length} file(s)`
|
|
1340
|
+
};
|
|
1253
1341
|
}
|
|
1254
|
-
|
|
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
|
-
}
|
|
1342
|
+
else {
|
|
1343
|
+
throw new Error(`Invalid action: ${params.action}. Must be one of: watch, unwatch, list`);
|
|
1265
1344
|
}
|
|
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
|
-
}
|
|
1345
|
+
});
|
|
1293
1346
|
});
|
|
1294
1347
|
}
|
|
1295
1348
|
catch (error) {
|
|
@@ -1529,6 +1582,7 @@ export function taskHelp() {
|
|
|
1529
1582
|
limits: {
|
|
1530
1583
|
max_items: 50
|
|
1531
1584
|
},
|
|
1585
|
+
note: '⚠️ IMPORTANT: The "tasks" parameter must be a JavaScript array, not a JSON string. MCP tools require pre-parsed objects.',
|
|
1532
1586
|
example: {
|
|
1533
1587
|
action: 'batch_create',
|
|
1534
1588
|
tasks: [
|
|
@@ -1659,6 +1713,7 @@ export function taskHelp() {
|
|
|
1659
1713
|
export async function watcherStatus(args, adapter) {
|
|
1660
1714
|
const actualAdapter = adapter ?? getAdapter();
|
|
1661
1715
|
const knex = actualAdapter.getKnex();
|
|
1716
|
+
const projectId = getProjectContext().getProjectId();
|
|
1662
1717
|
const subaction = args.subaction || 'status';
|
|
1663
1718
|
const watcher = FileWatcher.getInstance();
|
|
1664
1719
|
if (subaction === 'help') {
|
|
@@ -1702,10 +1757,14 @@ export async function watcherStatus(args, adapter) {
|
|
|
1702
1757
|
}
|
|
1703
1758
|
if (subaction === 'list_files') {
|
|
1704
1759
|
const fileLinks = await knex('t_task_file_links as tfl')
|
|
1705
|
-
.join('t_tasks as t',
|
|
1760
|
+
.join('t_tasks as t', function () {
|
|
1761
|
+
this.on('tfl.task_id', '=', 't.id')
|
|
1762
|
+
.andOn('tfl.project_id', '=', 't.project_id');
|
|
1763
|
+
})
|
|
1706
1764
|
.join('m_task_statuses as ts', 't.status_id', 'ts.id')
|
|
1707
1765
|
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
1708
|
-
.where('t.
|
|
1766
|
+
.where('t.project_id', projectId)
|
|
1767
|
+
.whereNot('t.status_id', STATUS_TO_ID['archived']) // Exclude archived tasks
|
|
1709
1768
|
.select('f.path as file_path', 't.id', 't.title', 'ts.name as status_name')
|
|
1710
1769
|
.orderBy(['f.path', 't.id']);
|
|
1711
1770
|
// Group by file
|
|
@@ -1736,9 +1795,13 @@ export async function watcherStatus(args, adapter) {
|
|
|
1736
1795
|
if (subaction === 'list_tasks') {
|
|
1737
1796
|
const taskLinks = await knex('t_tasks as t')
|
|
1738
1797
|
.join('m_task_statuses as ts', 't.status_id', 'ts.id')
|
|
1739
|
-
.join('t_task_file_links as tfl',
|
|
1798
|
+
.join('t_task_file_links as tfl', function () {
|
|
1799
|
+
this.on('t.id', '=', 'tfl.task_id')
|
|
1800
|
+
.andOn('t.project_id', '=', 'tfl.project_id');
|
|
1801
|
+
})
|
|
1740
1802
|
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
1741
|
-
.where('t.
|
|
1803
|
+
.where('t.project_id', projectId)
|
|
1804
|
+
.whereNot('t.status_id', STATUS_TO_ID['archived']) // Exclude archived tasks
|
|
1742
1805
|
.groupBy('t.id', 't.title', 'ts.name')
|
|
1743
1806
|
.select('t.id', 't.title', 'ts.name as status_name', knex.raw('COUNT(DISTINCT tfl.file_id) as file_count'), knex.raw('GROUP_CONCAT(DISTINCT f.path, \', \') as files'))
|
|
1744
1807
|
.orderBy('t.id');
|