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