sqlew 3.2.5 → 3.6.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 +288 -1011
- package/README.md +80 -263
- package/assets/config.example.toml +97 -0
- package/assets/schema.sql +6 -1
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.d.ts.map +1 -0
- package/dist/adapters/index.js +21 -0
- package/dist/adapters/index.js.map +1 -0
- package/dist/adapters/mysql-adapter.d.ts +31 -0
- package/dist/adapters/mysql-adapter.d.ts.map +1 -0
- package/dist/adapters/mysql-adapter.js +63 -0
- package/dist/adapters/mysql-adapter.js.map +1 -0
- package/dist/adapters/postgresql-adapter.d.ts +31 -0
- package/dist/adapters/postgresql-adapter.d.ts.map +1 -0
- package/dist/adapters/postgresql-adapter.js +63 -0
- package/dist/adapters/postgresql-adapter.js.map +1 -0
- package/dist/adapters/sqlite-adapter.d.ts +37 -0
- package/dist/adapters/sqlite-adapter.d.ts.map +1 -0
- package/dist/adapters/sqlite-adapter.js +129 -0
- package/dist/adapters/sqlite-adapter.js.map +1 -0
- package/dist/adapters/types.d.ts +33 -0
- package/dist/adapters/types.d.ts.map +1 -0
- package/dist/adapters/types.js +2 -0
- package/dist/adapters/types.js.map +1 -0
- package/dist/cli.js +55 -54
- package/dist/cli.js.map +1 -1
- package/dist/config/example-generator.d.ts +11 -0
- package/dist/config/example-generator.d.ts.map +1 -0
- package/dist/config/example-generator.js +48 -0
- package/dist/config/example-generator.js.map +1 -0
- package/dist/config/loader.d.ts +46 -0
- package/dist/config/loader.d.ts.map +1 -0
- package/dist/config/loader.js +155 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/types.d.ts +86 -0
- package/dist/config/types.d.ts.map +1 -0
- package/dist/config/types.js +28 -0
- package/dist/config/types.js.map +1 -0
- package/dist/constants.d.ts +9 -0
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +10 -0
- package/dist/constants.js.map +1 -1
- package/dist/database.d.ts +44 -122
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +145 -349
- package/dist/database.js.map +1 -1
- package/dist/index.js +223 -175
- package/dist/index.js.map +1 -1
- package/dist/knexfile.d.ts +6 -0
- package/dist/knexfile.d.ts.map +1 -0
- package/dist/knexfile.js +85 -0
- package/dist/knexfile.js.map +1 -0
- package/dist/migrations/add-help-system-tables.d.ts +35 -0
- package/dist/migrations/add-help-system-tables.d.ts.map +1 -0
- package/dist/migrations/add-help-system-tables.js +206 -0
- package/dist/migrations/add-help-system-tables.js.map +1 -0
- package/dist/migrations/add-token-tracking.d.ts +28 -0
- package/dist/migrations/add-token-tracking.d.ts.map +1 -0
- package/dist/migrations/add-token-tracking.js +108 -0
- package/dist/migrations/add-token-tracking.js.map +1 -0
- package/dist/migrations/add-v3.5.0-pruned-files.d.ts +26 -0
- package/dist/migrations/add-v3.5.0-pruned-files.d.ts.map +1 -0
- package/dist/migrations/add-v3.5.0-pruned-files.js +107 -0
- package/dist/migrations/add-v3.5.0-pruned-files.js.map +1 -0
- package/dist/migrations/index.d.ts +26 -12
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +162 -20
- package/dist/migrations/index.js.map +1 -1
- package/dist/migrations/knex/20251025020452_create_master_tables.d.ts +4 -0
- package/dist/migrations/knex/20251025020452_create_master_tables.d.ts.map +1 -0
- package/dist/migrations/knex/20251025020452_create_master_tables.js +65 -0
- package/dist/migrations/knex/20251025020452_create_master_tables.js.map +1 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts +4 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.d.ts.map +1 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.js +235 -0
- package/dist/migrations/knex/20251025021152_create_transaction_tables.js.map +1 -0
- package/dist/migrations/knex/20251025021351_create_indexes.d.ts +4 -0
- package/dist/migrations/knex/20251025021351_create_indexes.d.ts.map +1 -0
- package/dist/migrations/knex/20251025021351_create_indexes.js +62 -0
- package/dist/migrations/knex/20251025021351_create_indexes.js.map +1 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.d.ts +4 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.d.ts.map +1 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.js +58 -0
- package/dist/migrations/knex/20251025021416_seed_master_data.js.map +1 -0
- package/dist/migrations/knex/20251025070349_create_views.d.ts +4 -0
- package/dist/migrations/knex/20251025070349_create_views.d.ts.map +1 -0
- package/dist/migrations/knex/20251025070349_create_views.js +143 -0
- package/dist/migrations/knex/20251025070349_create_views.js.map +1 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts +4 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.d.ts.map +1 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js +15 -0
- package/dist/migrations/knex/20251025081221_add_link_type_to_task_decision_links.js.map +1 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts +8 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.d.ts.map +1 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js +12 -0
- package/dist/migrations/knex/20251025082220_fix_task_dependencies_columns.js.map +1 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts +19 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.d.ts.map +1 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.js +115 -0
- package/dist/migrations/knex/20251025090000_create_help_system_tables.js.map +1 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts +13 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.d.ts.map +1 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js +377 -0
- package/dist/migrations/knex/20251025090100_seed_help_categories_and_use_cases.js.map +1 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts +15 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.d.ts.map +1 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.js +253 -0
- package/dist/migrations/knex/20251025100000_seed_help_metadata.js.map +1 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts +16 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.d.ts.map +1 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js +276 -0
- package/dist/migrations/knex/20251025100100_seed_remaining_use_cases.js.map +1 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts +8 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.d.ts.map +1 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js +64 -0
- package/dist/migrations/knex/20251025120000_add_cascade_to_task_dependencies.js.map +1 -0
- package/dist/migrations/seed-help-data.d.ts +48 -0
- package/dist/migrations/seed-help-data.d.ts.map +1 -0
- package/dist/migrations/seed-help-data.js +1466 -0
- package/dist/migrations/seed-help-data.js.map +1 -0
- package/dist/migrations/seed-tool-metadata.d.ts +24 -0
- package/dist/migrations/seed-tool-metadata.d.ts.map +1 -0
- package/dist/migrations/seed-tool-metadata.js +392 -0
- package/dist/migrations/seed-tool-metadata.js.map +1 -0
- package/dist/migrations/v3.6.0-help-system-refactor.d.ts +46 -0
- package/dist/migrations/v3.6.0-help-system-refactor.d.ts.map +1 -0
- package/dist/migrations/v3.6.0-help-system-refactor.js +223 -0
- package/dist/migrations/v3.6.0-help-system-refactor.js.map +1 -0
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +2 -0
- package/dist/schema.js.map +1 -1
- package/dist/tests/git-aware-completion.test.d.ts +6 -0
- package/dist/tests/git-aware-completion.test.d.ts.map +1 -0
- package/dist/tests/git-aware-completion.test.js +160 -0
- package/dist/tests/git-aware-completion.test.js.map +1 -0
- package/dist/tests/help-system.test.d.ts +23 -0
- package/dist/tests/help-system.test.d.ts.map +1 -0
- package/dist/tests/help-system.test.js +374 -0
- package/dist/tests/help-system.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.d.ts +6 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.d.ts.map +1 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.js +264 -0
- package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-partial.test.d.ts +6 -0
- package/dist/tests/tasks.auto-pruning-partial.test.d.ts.map +1 -0
- package/dist/tests/tasks.auto-pruning-partial.test.js +285 -0
- package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-persistence.test.d.ts +6 -0
- package/dist/tests/tasks.auto-pruning-persistence.test.d.ts.map +1 -0
- package/dist/tests/tasks.auto-pruning-persistence.test.js +250 -0
- package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -0
- package/dist/tests/tasks.auto-pruning-safety.test.d.ts +12 -0
- package/dist/tests/tasks.auto-pruning-safety.test.d.ts.map +1 -0
- package/dist/tests/tasks.auto-pruning-safety.test.js +217 -0
- package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -0
- package/dist/tests/tasks.dependencies.test.js +338 -307
- package/dist/tests/tasks.dependencies.test.js.map +1 -1
- package/dist/tests/tasks.link-file-backward-compat.test.d.ts +6 -0
- package/dist/tests/tasks.link-file-backward-compat.test.d.ts.map +1 -0
- package/dist/tests/tasks.link-file-backward-compat.test.js +247 -0
- package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -0
- package/dist/tests/tasks.watch-files-action.test.d.ts +6 -0
- package/dist/tests/tasks.watch-files-action.test.d.ts.map +1 -0
- package/dist/tests/tasks.watch-files-action.test.js +372 -0
- package/dist/tests/tasks.watch-files-action.test.js.map +1 -0
- package/dist/tests/tasks.watch-files-parameter.test.d.ts +6 -0
- package/dist/tests/tasks.watch-files-parameter.test.d.ts.map +1 -0
- package/dist/tests/tasks.watch-files-parameter.test.js +260 -0
- package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -0
- package/dist/tests/two-step-git-completion.test.d.ts +6 -0
- package/dist/tests/two-step-git-completion.test.d.ts.map +1 -0
- package/dist/tests/two-step-git-completion.test.js +326 -0
- package/dist/tests/two-step-git-completion.test.js.map +1 -0
- package/dist/tests/vcs-staging.test.d.ts +6 -0
- package/dist/tests/vcs-staging.test.d.ts.map +1 -0
- package/dist/tests/vcs-staging.test.js +137 -0
- package/dist/tests/vcs-staging.test.js.map +1 -0
- package/dist/tools/config.d.ts +9 -4
- package/dist/tools/config.d.ts.map +1 -1
- package/dist/tools/config.js +16 -12
- package/dist/tools/config.js.map +1 -1
- package/dist/tools/constraints.d.ts +9 -3
- package/dist/tools/constraints.d.ts.map +1 -1
- package/dist/tools/constraints.js +66 -45
- package/dist/tools/constraints.js.map +1 -1
- package/dist/tools/context.d.ts +35 -16
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +374 -314
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts +11 -4
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +173 -91
- package/dist/tools/files.js.map +1 -1
- package/dist/tools/help-queries.d.ts +130 -0
- package/dist/tools/help-queries.d.ts.map +1 -0
- package/dist/tools/help-queries.js +393 -0
- package/dist/tools/help-queries.js.map +1 -0
- package/dist/tools/messaging.d.ts +13 -6
- package/dist/tools/messaging.d.ts.map +1 -1
- package/dist/tools/messaging.js +217 -129
- package/dist/tools/messaging.js.map +1 -1
- package/dist/tools/tasks.d.ts +42 -12
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +809 -347
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/utils.d.ts +13 -5
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +92 -115
- package/dist/tools/utils.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/activity-logging.d.ts +114 -0
- package/dist/utils/activity-logging.d.ts.map +1 -0
- package/dist/utils/activity-logging.js +162 -0
- package/dist/utils/activity-logging.js.map +1 -0
- package/dist/utils/batch.d.ts +2 -2
- package/dist/utils/batch.d.ts.map +1 -1
- package/dist/utils/batch.js +8 -8
- package/dist/utils/batch.js.map +1 -1
- package/dist/utils/cleanup.d.ts +21 -13
- package/dist/utils/cleanup.d.ts.map +1 -1
- package/dist/utils/cleanup.js +31 -24
- package/dist/utils/cleanup.js.map +1 -1
- package/dist/utils/debug-logger.d.ts +44 -0
- package/dist/utils/debug-logger.d.ts.map +1 -0
- package/dist/utils/debug-logger.js +116 -0
- package/dist/utils/debug-logger.js.map +1 -0
- package/dist/utils/file-pruning.d.ts +69 -0
- package/dist/utils/file-pruning.d.ts.map +1 -0
- package/dist/utils/file-pruning.js +185 -0
- package/dist/utils/file-pruning.js.map +1 -0
- package/dist/utils/help-tracking.d.ts +55 -0
- package/dist/utils/help-tracking.d.ts.map +1 -0
- package/dist/utils/help-tracking.js +88 -0
- package/dist/utils/help-tracking.js.map +1 -0
- package/dist/utils/quality-checks.d.ts +60 -0
- package/dist/utils/quality-checks.d.ts.map +1 -0
- package/dist/utils/quality-checks.js +228 -0
- package/dist/utils/quality-checks.js.map +1 -0
- package/dist/utils/retention.d.ts +13 -5
- package/dist/utils/retention.d.ts.map +1 -1
- package/dist/utils/retention.js +20 -8
- package/dist/utils/retention.js.map +1 -1
- package/dist/utils/task-stale-detection.d.ts +77 -7
- package/dist/utils/task-stale-detection.d.ts.map +1 -1
- package/dist/utils/task-stale-detection.js +309 -34
- package/dist/utils/task-stale-detection.js.map +1 -1
- package/dist/utils/token-estimation.d.ts +72 -0
- package/dist/utils/token-estimation.d.ts.map +1 -0
- package/dist/utils/token-estimation.js +71 -0
- package/dist/utils/token-estimation.js.map +1 -0
- package/dist/utils/token-logging.d.ts +48 -0
- package/dist/utils/token-logging.d.ts.map +1 -0
- package/dist/utils/token-logging.js +112 -0
- package/dist/utils/token-logging.js.map +1 -0
- package/dist/utils/vcs-adapter.d.ts +68 -0
- package/dist/utils/vcs-adapter.d.ts.map +1 -0
- package/dist/utils/vcs-adapter.js +187 -0
- package/dist/utils/vcs-adapter.js.map +1 -0
- package/dist/utils/view-queries.d.ts +34 -0
- package/dist/utils/view-queries.d.ts.map +1 -0
- package/dist/utils/view-queries.js +192 -0
- package/dist/utils/view-queries.js.map +1 -0
- package/dist/watcher/file-watcher.d.ts +54 -4
- package/dist/watcher/file-watcher.d.ts.map +1 -1
- package/dist/watcher/file-watcher.js +329 -33
- package/dist/watcher/file-watcher.js.map +1 -1
- package/dist/watcher/gitignore-parser.d.ts +70 -0
- package/dist/watcher/gitignore-parser.d.ts.map +1 -0
- package/dist/watcher/gitignore-parser.js +191 -0
- package/dist/watcher/gitignore-parser.js.map +1 -0
- package/dist/watcher/index.d.ts +1 -0
- package/dist/watcher/index.d.ts.map +1 -1
- package/dist/watcher/index.js +1 -0
- package/dist/watcher/index.js.map +1 -1
- package/docs/AI_AGENT_GUIDE.md +1 -1
- package/docs/ARCHITECTURE.md +12 -0
- package/docs/AUTO_FILE_TRACKING.md +486 -82
- package/docs/BEST_PRACTICES.md +56 -448
- package/docs/CONFIGURATION.md +908 -0
- package/docs/GIT_AWARE_AUTO_COMPLETE.md +645 -0
- package/docs/MIGRATION_v3.3.md +602 -0
- package/docs/MIGRATION_v3.6.0.md +170 -0
- package/docs/SHARED_CONCEPTS.md +65 -209
- package/docs/TASK_ACTIONS.md +12 -0
- package/docs/TASK_OVERVIEW.md +125 -24
- package/docs/TASK_PRUNING.md +589 -0
- package/docs/TASK_SYSTEM.md +83 -13
- package/docs/TOOL_REFERENCE.md +94 -6
- package/docs/TOOL_SELECTION.md +41 -248
- package/package.json +21 -7
package/dist/tools/tasks.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Task management tools for Kanban Task Watcher
|
|
3
3
|
* Implements create, update, get, list, move, link, archive, batch_create actions
|
|
4
|
+
*
|
|
5
|
+
* CONVERTED: Using Knex.js with DatabaseAdapter (async/await)
|
|
4
6
|
*/
|
|
5
|
-
import {
|
|
6
|
-
import { detectAndTransitionStaleTasks } from '../utils/task-stale-detection.js';
|
|
7
|
-
import { processBatch } from '../utils/batch.js';
|
|
7
|
+
import { getAdapter, getOrCreateAgent, getOrCreateTag, getOrCreateContextKey, getLayerId, getOrCreateFile } from '../database.js';
|
|
8
|
+
import { detectAndTransitionStaleTasks, autoArchiveOldDoneTasks, detectAndCompleteReviewedTasks, detectAndArchiveOnCommit } from '../utils/task-stale-detection.js';
|
|
8
9
|
import { FileWatcher } from '../watcher/index.js';
|
|
9
10
|
import { validatePriorityRange, validateLength, validateRange } from '../utils/validators.js';
|
|
11
|
+
import { logTaskCreate, logTaskStatusChange } from '../utils/activity-logging.js';
|
|
10
12
|
/**
|
|
11
13
|
* Task status enum (matches m_task_statuses)
|
|
12
14
|
*/
|
|
@@ -53,10 +55,12 @@ const VALID_TRANSITIONS = {
|
|
|
53
55
|
* Used by createTask (with transaction) and batchCreateTasks (manages its own transaction)
|
|
54
56
|
*
|
|
55
57
|
* @param params - Task parameters
|
|
56
|
-
* @param
|
|
58
|
+
* @param adapter - Database adapter instance
|
|
59
|
+
* @param trx - Optional transaction
|
|
57
60
|
* @returns Response with success status and task metadata
|
|
58
61
|
*/
|
|
59
|
-
function createTaskInternal(params,
|
|
62
|
+
async function createTaskInternal(params, adapter, trx) {
|
|
63
|
+
const knex = trx || adapter.getKnex();
|
|
60
64
|
// Validate priority
|
|
61
65
|
const priority = params.priority !== undefined ? params.priority : 2;
|
|
62
66
|
validatePriorityRange(priority);
|
|
@@ -69,7 +73,7 @@ function createTaskInternal(params, db) {
|
|
|
69
73
|
// Validate layer if provided
|
|
70
74
|
let layerId = null;
|
|
71
75
|
if (params.layer) {
|
|
72
|
-
layerId = getLayerId(
|
|
76
|
+
layerId = await getLayerId(adapter, params.layer, trx);
|
|
73
77
|
if (layerId === null) {
|
|
74
78
|
throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
|
|
75
79
|
}
|
|
@@ -77,19 +81,24 @@ function createTaskInternal(params, db) {
|
|
|
77
81
|
// Get or create agents
|
|
78
82
|
let assignedAgentId = null;
|
|
79
83
|
if (params.assigned_agent) {
|
|
80
|
-
assignedAgentId = getOrCreateAgent(
|
|
81
|
-
}
|
|
82
|
-
let createdByAgentId = null;
|
|
83
|
-
if (params.created_by_agent) {
|
|
84
|
-
createdByAgentId = getOrCreateAgent(db, params.created_by_agent);
|
|
84
|
+
assignedAgentId = await getOrCreateAgent(adapter, params.assigned_agent, trx);
|
|
85
85
|
}
|
|
86
|
+
// Default to 'system' if no created_by_agent provided
|
|
87
|
+
// This ensures the activity log has a valid agent_id
|
|
88
|
+
const createdBy = params.created_by_agent || 'system';
|
|
89
|
+
const createdByAgentId = await getOrCreateAgent(adapter, createdBy, trx);
|
|
86
90
|
// Insert task
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
const now = Math.floor(Date.now() / 1000);
|
|
92
|
+
const [taskId] = await knex('t_tasks').insert({
|
|
93
|
+
title: params.title,
|
|
94
|
+
status_id: statusId,
|
|
95
|
+
priority: priority,
|
|
96
|
+
assigned_agent_id: assignedAgentId,
|
|
97
|
+
created_by_agent_id: createdByAgentId,
|
|
98
|
+
layer_id: layerId,
|
|
99
|
+
created_ts: now,
|
|
100
|
+
updated_ts: now
|
|
101
|
+
});
|
|
93
102
|
// Process acceptance_criteria (can be string, JSON string, or array)
|
|
94
103
|
let acceptanceCriteriaString = null;
|
|
95
104
|
let acceptanceCriteriaJson = null;
|
|
@@ -127,26 +136,123 @@ function createTaskInternal(params, db) {
|
|
|
127
136
|
}
|
|
128
137
|
// Insert task details if provided
|
|
129
138
|
if (params.description || acceptanceCriteriaString || acceptanceCriteriaJson || params.notes) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
139
|
+
await knex('t_task_details').insert({
|
|
140
|
+
task_id: Number(taskId),
|
|
141
|
+
description: params.description || null,
|
|
142
|
+
acceptance_criteria: acceptanceCriteriaString,
|
|
143
|
+
acceptance_criteria_json: acceptanceCriteriaJson,
|
|
144
|
+
notes: params.notes || null
|
|
145
|
+
});
|
|
135
146
|
}
|
|
136
147
|
// Insert tags if provided
|
|
137
148
|
if (params.tags && params.tags.length > 0) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
149
|
+
// Parse tags - handle MCP SDK converting JSON string to char array
|
|
150
|
+
let tagsParsed;
|
|
151
|
+
if (typeof params.tags === 'string') {
|
|
152
|
+
// String - try to parse as JSON
|
|
153
|
+
try {
|
|
154
|
+
tagsParsed = JSON.parse(params.tags);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
// If not valid JSON, treat as single tag name
|
|
158
|
+
tagsParsed = [params.tags];
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
else if (Array.isArray(params.tags)) {
|
|
162
|
+
// Check if it's an array of single characters (MCP SDK bug)
|
|
163
|
+
// Example: ['[', '"', 't', 'e', 's', 't', 'i', 'n', 'g', '"', ']']
|
|
164
|
+
if (params.tags.every((item) => typeof item === 'string' && item.length === 1)) {
|
|
165
|
+
// Join characters back into string and parse JSON
|
|
166
|
+
const jsonString = params.tags.join('');
|
|
167
|
+
try {
|
|
168
|
+
tagsParsed = JSON.parse(jsonString);
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
172
|
+
throw new Error(`Invalid tags format: ${jsonString}. ${errMsg}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Normal array of tag names
|
|
177
|
+
tagsParsed = params.tags;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
throw new Error('Parameter "tags" must be a string or array');
|
|
182
|
+
}
|
|
183
|
+
for (const tagName of tagsParsed) {
|
|
184
|
+
const tagId = await getOrCreateTag(adapter, tagName, trx);
|
|
185
|
+
await knex('t_task_tags').insert({
|
|
186
|
+
task_id: Number(taskId),
|
|
187
|
+
tag_id: tagId
|
|
188
|
+
}).onConflict(['task_id', 'tag_id']).ignore();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Activity logging (replaces triggers)
|
|
192
|
+
await logTaskCreate(knex, {
|
|
193
|
+
task_id: Number(taskId),
|
|
194
|
+
title: params.title,
|
|
195
|
+
agent_id: createdByAgentId,
|
|
196
|
+
layer_id: layerId || undefined
|
|
197
|
+
});
|
|
198
|
+
// Link files and register with watcher if watch_files provided (v3.4.1)
|
|
199
|
+
if (params.watch_files && params.watch_files.length > 0) {
|
|
200
|
+
// Parse watch_files - handle MCP SDK converting JSON string to char array
|
|
201
|
+
let watchFilesParsed;
|
|
202
|
+
if (typeof params.watch_files === 'string') {
|
|
203
|
+
// String - try to parse as JSON
|
|
204
|
+
try {
|
|
205
|
+
watchFilesParsed = JSON.parse(params.watch_files);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// If not valid JSON, treat as single file path
|
|
209
|
+
watchFilesParsed = [params.watch_files];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (Array.isArray(params.watch_files)) {
|
|
213
|
+
// Check if it's an array of single characters (MCP SDK bug)
|
|
214
|
+
// Example: ['[', '"', 'f', 'i', 'l', 'e', '.', 't', 'x', 't', '"', ']']
|
|
215
|
+
if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
|
|
216
|
+
// Join characters back into string and parse JSON
|
|
217
|
+
const jsonString = params.watch_files.join('');
|
|
218
|
+
try {
|
|
219
|
+
watchFilesParsed = JSON.parse(jsonString);
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
223
|
+
throw new Error(`Invalid watch_files format: ${jsonString}. ${errMsg}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
// Normal array of file paths
|
|
228
|
+
watchFilesParsed = params.watch_files;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
throw new Error('Parameter "watch_files" must be a string or array');
|
|
233
|
+
}
|
|
234
|
+
for (const filePath of watchFilesParsed) {
|
|
235
|
+
const fileId = await getOrCreateFile(adapter, filePath, trx);
|
|
236
|
+
await knex('t_task_file_links').insert({
|
|
237
|
+
task_id: Number(taskId),
|
|
238
|
+
file_id: fileId
|
|
239
|
+
}).onConflict(['task_id', 'file_id']).ignore();
|
|
240
|
+
}
|
|
241
|
+
// Register files with watcher for auto-tracking
|
|
242
|
+
try {
|
|
243
|
+
const watcher = FileWatcher.getInstance();
|
|
244
|
+
for (const filePath of watchFilesParsed) {
|
|
245
|
+
watcher.registerFile(filePath, Number(taskId), params.title, status);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
// Watcher may not be initialized yet, ignore
|
|
250
|
+
console.error('Warning: Could not register files with watcher:', error);
|
|
145
251
|
}
|
|
146
252
|
}
|
|
147
253
|
return {
|
|
148
254
|
success: true,
|
|
149
|
-
task_id: taskId,
|
|
255
|
+
task_id: Number(taskId),
|
|
150
256
|
title: params.title,
|
|
151
257
|
status: status,
|
|
152
258
|
message: `Task "${params.title}" created successfully`
|
|
@@ -155,16 +261,16 @@ function createTaskInternal(params, db) {
|
|
|
155
261
|
/**
|
|
156
262
|
* Create a new task
|
|
157
263
|
*/
|
|
158
|
-
export function createTask(params) {
|
|
159
|
-
const
|
|
264
|
+
export async function createTask(params, adapter) {
|
|
265
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
160
266
|
// Validate required parameters
|
|
161
267
|
if (!params.title || params.title.trim() === '') {
|
|
162
268
|
throw new Error('Parameter "title" is required and cannot be empty');
|
|
163
269
|
}
|
|
164
270
|
validateLength(params.title, 'Parameter "title"', 200);
|
|
165
271
|
try {
|
|
166
|
-
return transaction(
|
|
167
|
-
return createTaskInternal(params,
|
|
272
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
273
|
+
return await createTaskInternal(params, actualAdapter, trx);
|
|
168
274
|
});
|
|
169
275
|
}
|
|
170
276
|
catch (error) {
|
|
@@ -175,56 +281,50 @@ export function createTask(params) {
|
|
|
175
281
|
/**
|
|
176
282
|
* Update task metadata
|
|
177
283
|
*/
|
|
178
|
-
export function updateTask(params) {
|
|
179
|
-
const
|
|
284
|
+
export async function updateTask(params, adapter) {
|
|
285
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
180
286
|
// Validate required parameters
|
|
181
287
|
if (!params.task_id) {
|
|
182
288
|
throw new Error('Parameter "task_id" is required');
|
|
183
289
|
}
|
|
184
290
|
try {
|
|
185
|
-
return transaction(
|
|
291
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
292
|
+
const knex = actualAdapter.getKnex();
|
|
186
293
|
// Check if task exists
|
|
187
|
-
const taskExists =
|
|
294
|
+
const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
|
|
188
295
|
if (!taskExists) {
|
|
189
296
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
190
297
|
}
|
|
191
|
-
// Build update
|
|
192
|
-
const
|
|
193
|
-
const updateParams = [];
|
|
298
|
+
// Build update data dynamically
|
|
299
|
+
const updateData = {};
|
|
194
300
|
if (params.title !== undefined) {
|
|
195
301
|
if (params.title.trim() === '') {
|
|
196
302
|
throw new Error('Parameter "title" cannot be empty');
|
|
197
303
|
}
|
|
198
304
|
validateLength(params.title, 'Parameter "title"', 200);
|
|
199
|
-
|
|
200
|
-
updateParams.push(params.title);
|
|
305
|
+
updateData.title = params.title;
|
|
201
306
|
}
|
|
202
307
|
if (params.priority !== undefined) {
|
|
203
308
|
validatePriorityRange(params.priority);
|
|
204
|
-
|
|
205
|
-
updateParams.push(params.priority);
|
|
309
|
+
updateData.priority = params.priority;
|
|
206
310
|
}
|
|
207
311
|
if (params.assigned_agent !== undefined) {
|
|
208
|
-
const agentId = getOrCreateAgent(
|
|
209
|
-
|
|
210
|
-
updateParams.push(agentId);
|
|
312
|
+
const agentId = await getOrCreateAgent(actualAdapter, params.assigned_agent, trx);
|
|
313
|
+
updateData.assigned_agent_id = agentId;
|
|
211
314
|
}
|
|
212
315
|
if (params.layer !== undefined) {
|
|
213
|
-
const layerId = getLayerId(
|
|
316
|
+
const layerId = await getLayerId(actualAdapter, params.layer, trx);
|
|
214
317
|
if (layerId === null) {
|
|
215
318
|
throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
|
|
216
319
|
}
|
|
217
|
-
|
|
218
|
-
updateParams.push(layerId);
|
|
320
|
+
updateData.layer_id = layerId;
|
|
219
321
|
}
|
|
220
322
|
// Update t_tasks if any updates
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
`);
|
|
227
|
-
updateStmt.run(...updateParams, params.task_id);
|
|
323
|
+
if (Object.keys(updateData).length > 0) {
|
|
324
|
+
await trx('t_tasks')
|
|
325
|
+
.where({ id: params.task_id })
|
|
326
|
+
.update(updateData);
|
|
327
|
+
// TODO: Add activity logging for updates if needed
|
|
228
328
|
}
|
|
229
329
|
// Update t_task_details if any detail fields provided
|
|
230
330
|
if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
|
|
@@ -266,43 +366,95 @@ export function updateTask(params) {
|
|
|
266
366
|
}
|
|
267
367
|
}
|
|
268
368
|
// Check if details exist
|
|
269
|
-
const detailsExist =
|
|
270
|
-
|
|
369
|
+
const detailsExist = await trx('t_task_details').where({ task_id: params.task_id }).first();
|
|
370
|
+
const detailsUpdate = {};
|
|
371
|
+
if (params.description !== undefined) {
|
|
372
|
+
detailsUpdate.description = params.description || null;
|
|
373
|
+
}
|
|
374
|
+
if (acceptanceCriteriaString !== undefined) {
|
|
375
|
+
detailsUpdate.acceptance_criteria = acceptanceCriteriaString;
|
|
376
|
+
}
|
|
377
|
+
if (acceptanceCriteriaJson !== undefined) {
|
|
378
|
+
detailsUpdate.acceptance_criteria_json = acceptanceCriteriaJson;
|
|
379
|
+
}
|
|
380
|
+
if (params.notes !== undefined) {
|
|
381
|
+
detailsUpdate.notes = params.notes || null;
|
|
382
|
+
}
|
|
383
|
+
if (detailsExist && Object.keys(detailsUpdate).length > 0) {
|
|
271
384
|
// Update existing details
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
385
|
+
await trx('t_task_details')
|
|
386
|
+
.where({ task_id: params.task_id })
|
|
387
|
+
.update(detailsUpdate);
|
|
388
|
+
}
|
|
389
|
+
else if (!detailsExist) {
|
|
390
|
+
// Insert new details
|
|
391
|
+
await trx('t_task_details').insert({
|
|
392
|
+
task_id: params.task_id,
|
|
393
|
+
description: params.description || null,
|
|
394
|
+
acceptance_criteria: acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null,
|
|
395
|
+
acceptance_criteria_json: acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null,
|
|
396
|
+
notes: params.notes || null
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
// Handle watch_files if provided (v3.4.1)
|
|
401
|
+
if (params.watch_files && params.watch_files.length > 0) {
|
|
402
|
+
// Parse watch_files - handle MCP SDK converting JSON string to char array
|
|
403
|
+
let watchFilesParsed;
|
|
404
|
+
if (typeof params.watch_files === 'string') {
|
|
405
|
+
// String - try to parse as JSON
|
|
406
|
+
try {
|
|
407
|
+
watchFilesParsed = JSON.parse(params.watch_files);
|
|
281
408
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
409
|
+
catch {
|
|
410
|
+
// If not valid JSON, treat as single file path
|
|
411
|
+
watchFilesParsed = [params.watch_files];
|
|
285
412
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
413
|
+
}
|
|
414
|
+
else if (Array.isArray(params.watch_files)) {
|
|
415
|
+
// Check if it's an array of single characters (MCP SDK bug)
|
|
416
|
+
if (params.watch_files.every((item) => typeof item === 'string' && item.length === 1)) {
|
|
417
|
+
// Join characters back into string and parse JSON
|
|
418
|
+
const jsonString = params.watch_files.join('');
|
|
419
|
+
try {
|
|
420
|
+
watchFilesParsed = JSON.parse(jsonString);
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
throw new Error(`Invalid watch_files format: ${jsonString}`);
|
|
424
|
+
}
|
|
289
425
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
SET ${detailUpdates.join(', ')}
|
|
294
|
-
WHERE task_id = ?
|
|
295
|
-
`);
|
|
296
|
-
updateDetailsStmt.run(...detailParams, params.task_id);
|
|
426
|
+
else {
|
|
427
|
+
// Normal array of file paths
|
|
428
|
+
watchFilesParsed = params.watch_files;
|
|
297
429
|
}
|
|
298
430
|
}
|
|
299
431
|
else {
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
432
|
+
throw new Error('Parameter "watch_files" must be a string or array');
|
|
433
|
+
}
|
|
434
|
+
for (const filePath of watchFilesParsed) {
|
|
435
|
+
const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
|
|
436
|
+
await trx('t_task_file_links').insert({
|
|
437
|
+
task_id: params.task_id,
|
|
438
|
+
file_id: fileId
|
|
439
|
+
}).onConflict(['task_id', 'file_id']).ignore();
|
|
440
|
+
}
|
|
441
|
+
// Register files with watcher for auto-tracking
|
|
442
|
+
try {
|
|
443
|
+
const taskData = await trx('t_tasks as t')
|
|
444
|
+
.join('m_task_statuses as s', 't.status_id', 's.id')
|
|
445
|
+
.where('t.id', params.task_id)
|
|
446
|
+
.select('t.title', 's.name as status')
|
|
447
|
+
.first();
|
|
448
|
+
if (taskData) {
|
|
449
|
+
const watcher = FileWatcher.getInstance();
|
|
450
|
+
for (const filePath of watchFilesParsed) {
|
|
451
|
+
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
catch (error) {
|
|
456
|
+
// Watcher may not be initialized yet, ignore
|
|
457
|
+
console.error('Warning: Could not register files with watcher:', error);
|
|
306
458
|
}
|
|
307
459
|
}
|
|
308
460
|
return {
|
|
@@ -320,89 +472,70 @@ export function updateTask(params) {
|
|
|
320
472
|
/**
|
|
321
473
|
* Internal helper: Query task dependencies (used by getTask and getDependencies)
|
|
322
474
|
*/
|
|
323
|
-
function queryTaskDependencies(
|
|
475
|
+
async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
|
|
476
|
+
const knex = adapter.getKnex();
|
|
324
477
|
// Build query based on include_details flag
|
|
325
|
-
|
|
478
|
+
const selectFields = includeDetails
|
|
479
|
+
? [
|
|
480
|
+
't.id',
|
|
481
|
+
't.title',
|
|
482
|
+
's.name as status',
|
|
483
|
+
't.priority',
|
|
484
|
+
'aa.name as assigned_to',
|
|
485
|
+
't.created_ts',
|
|
486
|
+
't.updated_ts',
|
|
487
|
+
'td.description'
|
|
488
|
+
]
|
|
489
|
+
: [
|
|
490
|
+
't.id',
|
|
491
|
+
't.title',
|
|
492
|
+
's.name as status',
|
|
493
|
+
't.priority'
|
|
494
|
+
];
|
|
495
|
+
// Get blockers (tasks that block this task)
|
|
496
|
+
let blockersQuery = knex('t_tasks as t')
|
|
497
|
+
.join('t_task_dependencies as d', 't.id', 'd.blocker_task_id')
|
|
498
|
+
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
499
|
+
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
500
|
+
.where('d.blocked_task_id', taskId)
|
|
501
|
+
.select(selectFields);
|
|
326
502
|
if (includeDetails) {
|
|
327
|
-
|
|
328
|
-
selectFields = `
|
|
329
|
-
t.id,
|
|
330
|
-
t.title,
|
|
331
|
-
s.name as status,
|
|
332
|
-
t.priority,
|
|
333
|
-
aa.name as assigned_to,
|
|
334
|
-
t.created_ts,
|
|
335
|
-
t.updated_ts,
|
|
336
|
-
td.description
|
|
337
|
-
`;
|
|
338
|
-
}
|
|
339
|
-
else {
|
|
340
|
-
// Metadata only (token-efficient)
|
|
341
|
-
selectFields = `
|
|
342
|
-
t.id,
|
|
343
|
-
t.title,
|
|
344
|
-
s.name as status,
|
|
345
|
-
t.priority
|
|
346
|
-
`;
|
|
503
|
+
blockersQuery = blockersQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
|
|
347
504
|
}
|
|
348
|
-
|
|
349
|
-
const blockersQuery = `
|
|
350
|
-
SELECT ${selectFields}
|
|
351
|
-
FROM t_tasks t
|
|
352
|
-
JOIN t_task_dependencies d ON t.id = d.blocker_task_id
|
|
353
|
-
LEFT JOIN m_task_statuses s ON t.status_id = s.id
|
|
354
|
-
LEFT JOIN m_agents aa ON t.assigned_agent_id = aa.id
|
|
355
|
-
${includeDetails ? 'LEFT JOIN t_task_details td ON t.id = td.task_id' : ''}
|
|
356
|
-
WHERE d.blocked_task_id = ?
|
|
357
|
-
`;
|
|
358
|
-
const blockers = db.prepare(blockersQuery).all(taskId);
|
|
505
|
+
const blockers = await blockersQuery;
|
|
359
506
|
// Get blocking (tasks this task blocks)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const blocking =
|
|
507
|
+
let blockingQuery = knex('t_tasks as t')
|
|
508
|
+
.join('t_task_dependencies as d', 't.id', 'd.blocked_task_id')
|
|
509
|
+
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
510
|
+
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
511
|
+
.where('d.blocker_task_id', taskId)
|
|
512
|
+
.select(selectFields);
|
|
513
|
+
if (includeDetails) {
|
|
514
|
+
blockingQuery = blockingQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
|
|
515
|
+
}
|
|
516
|
+
const blocking = await blockingQuery;
|
|
370
517
|
return { blockers, blocking };
|
|
371
518
|
}
|
|
372
519
|
/**
|
|
373
520
|
* Get full task details
|
|
374
521
|
*/
|
|
375
|
-
export function getTask(params) {
|
|
376
|
-
const
|
|
522
|
+
export async function getTask(params, adapter) {
|
|
523
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
524
|
+
const knex = actualAdapter.getKnex();
|
|
377
525
|
if (!params.task_id) {
|
|
378
526
|
throw new Error('Parameter "task_id" is required');
|
|
379
527
|
}
|
|
380
528
|
try {
|
|
381
529
|
// Get task with details
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
t.created_ts,
|
|
392
|
-
t.updated_ts,
|
|
393
|
-
t.completed_ts,
|
|
394
|
-
td.description,
|
|
395
|
-
td.acceptance_criteria,
|
|
396
|
-
td.notes
|
|
397
|
-
FROM t_tasks t
|
|
398
|
-
LEFT JOIN m_task_statuses s ON t.status_id = s.id
|
|
399
|
-
LEFT JOIN m_agents aa ON t.assigned_agent_id = aa.id
|
|
400
|
-
LEFT JOIN m_agents ca ON t.created_by_agent_id = ca.id
|
|
401
|
-
LEFT JOIN m_layers l ON t.layer_id = l.id
|
|
402
|
-
LEFT JOIN t_task_details td ON t.id = td.task_id
|
|
403
|
-
WHERE t.id = ?
|
|
404
|
-
`);
|
|
405
|
-
const task = stmt.get(params.task_id);
|
|
530
|
+
const task = await knex('t_tasks as t')
|
|
531
|
+
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
532
|
+
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
533
|
+
.leftJoin('m_agents as ca', 't.created_by_agent_id', 'ca.id')
|
|
534
|
+
.leftJoin('m_layers as l', 't.layer_id', 'l.id')
|
|
535
|
+
.leftJoin('t_task_details as td', 't.id', 'td.task_id')
|
|
536
|
+
.where('t.id', params.task_id)
|
|
537
|
+
.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')
|
|
538
|
+
.first();
|
|
406
539
|
if (!task) {
|
|
407
540
|
return {
|
|
408
541
|
found: false,
|
|
@@ -410,37 +543,27 @@ export function getTask(params) {
|
|
|
410
543
|
};
|
|
411
544
|
}
|
|
412
545
|
// Get tags
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
`);
|
|
419
|
-
const tags = tagsStmt.all(params.task_id).map((row) => row.name);
|
|
546
|
+
const tags = await knex('t_task_tags as tt')
|
|
547
|
+
.join('m_tags as tg', 'tt.tag_id', 'tg.id')
|
|
548
|
+
.where('tt.task_id', params.task_id)
|
|
549
|
+
.select('tg.name')
|
|
550
|
+
.then(rows => rows.map((row) => row.name));
|
|
420
551
|
// Get decision links
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
WHERE tdl.task_id = ?
|
|
426
|
-
`);
|
|
427
|
-
const decisions = decisionsStmt.all(params.task_id);
|
|
552
|
+
const decisions = await knex('t_task_decision_links as tdl')
|
|
553
|
+
.join('m_context_keys as ck', 'tdl.decision_key_id', 'ck.id')
|
|
554
|
+
.where('tdl.task_id', params.task_id)
|
|
555
|
+
.select('ck.key', 'tdl.link_type');
|
|
428
556
|
// Get constraint links
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
WHERE tcl.task_id = ?
|
|
434
|
-
`);
|
|
435
|
-
const constraints = constraintsStmt.all(params.task_id);
|
|
557
|
+
const constraints = await knex('t_task_constraint_links as tcl')
|
|
558
|
+
.join('t_constraints as c', 'tcl.constraint_id', 'c.id')
|
|
559
|
+
.where('tcl.task_id', params.task_id)
|
|
560
|
+
.select('c.id', 'c.constraint_text');
|
|
436
561
|
// Get file links
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
`);
|
|
443
|
-
const files = filesStmt.all(params.task_id).map((row) => row.path);
|
|
562
|
+
const files = await knex('t_task_file_links as tfl')
|
|
563
|
+
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
564
|
+
.where('tfl.task_id', params.task_id)
|
|
565
|
+
.select('f.path')
|
|
566
|
+
.then(rows => rows.map((row) => row.path));
|
|
444
567
|
// Build result
|
|
445
568
|
const result = {
|
|
446
569
|
found: true,
|
|
@@ -454,7 +577,7 @@ export function getTask(params) {
|
|
|
454
577
|
};
|
|
455
578
|
// Include dependencies if requested (token-efficient, metadata-only)
|
|
456
579
|
if (params.include_dependencies) {
|
|
457
|
-
const deps = queryTaskDependencies(
|
|
580
|
+
const deps = await queryTaskDependencies(actualAdapter, params.task_id, false);
|
|
458
581
|
result.task.dependencies = {
|
|
459
582
|
blockers: deps.blockers,
|
|
460
583
|
blocking: deps.blocking
|
|
@@ -470,80 +593,76 @@ export function getTask(params) {
|
|
|
470
593
|
/**
|
|
471
594
|
* List tasks (token-efficient, no descriptions)
|
|
472
595
|
*/
|
|
473
|
-
export function listTasks(params = {}) {
|
|
474
|
-
const
|
|
596
|
+
export async function listTasks(params = {}, adapter) {
|
|
597
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
598
|
+
const knex = actualAdapter.getKnex();
|
|
475
599
|
try {
|
|
476
|
-
// Run auto-stale detection before listing
|
|
477
|
-
const transitionCount = detectAndTransitionStaleTasks(
|
|
600
|
+
// Run auto-stale detection, git-aware completion, and auto-archive before listing
|
|
601
|
+
const transitionCount = await detectAndTransitionStaleTasks(actualAdapter);
|
|
602
|
+
const gitCompletedCount = await detectAndCompleteReviewedTasks(actualAdapter);
|
|
603
|
+
const gitArchivedCount = await detectAndArchiveOnCommit(actualAdapter);
|
|
604
|
+
const archiveCount = await autoArchiveOldDoneTasks(actualAdapter);
|
|
478
605
|
// Build query with optional dependency counts
|
|
479
606
|
let query;
|
|
480
607
|
if (params.include_dependency_counts) {
|
|
481
608
|
// Include dependency counts with LEFT JOINs
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
GROUP BY blocker_task_id
|
|
497
|
-
) blocking ON vt.id = blocking.blocker_task_id
|
|
498
|
-
WHERE 1=1
|
|
499
|
-
`;
|
|
609
|
+
const blockersCTE = knex('t_task_dependencies')
|
|
610
|
+
.select('blocked_task_id')
|
|
611
|
+
.count('* as blocked_by_count')
|
|
612
|
+
.groupBy('blocked_task_id')
|
|
613
|
+
.as('blockers');
|
|
614
|
+
const blockingCTE = knex('t_task_dependencies')
|
|
615
|
+
.select('blocker_task_id')
|
|
616
|
+
.count('* as blocking_count')
|
|
617
|
+
.groupBy('blocker_task_id')
|
|
618
|
+
.as('blocking');
|
|
619
|
+
query = knex('v_task_board as vt')
|
|
620
|
+
.leftJoin(blockersCTE, 'vt.id', 'blockers.blocked_task_id')
|
|
621
|
+
.leftJoin(blockingCTE, 'vt.id', 'blocking.blocker_task_id')
|
|
622
|
+
.select('vt.*', knex.raw('COALESCE(blockers.blocked_by_count, 0) as blocked_by_count'), knex.raw('COALESCE(blocking.blocking_count, 0) as blocking_count'));
|
|
500
623
|
}
|
|
501
624
|
else {
|
|
502
625
|
// Standard query without dependency counts
|
|
503
|
-
query = '
|
|
626
|
+
query = knex('v_task_board');
|
|
504
627
|
}
|
|
505
|
-
const queryParams = [];
|
|
506
628
|
// Filter by status
|
|
507
629
|
if (params.status) {
|
|
508
630
|
if (!STATUS_TO_ID[params.status]) {
|
|
509
631
|
throw new Error(`Invalid status: ${params.status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
|
|
510
632
|
}
|
|
511
|
-
query
|
|
512
|
-
queryParams.push(params.status);
|
|
633
|
+
query = query.where(params.include_dependency_counts ? 'vt.status' : 'status', params.status);
|
|
513
634
|
}
|
|
514
635
|
// Filter by assigned agent
|
|
515
636
|
if (params.assigned_agent) {
|
|
516
|
-
query
|
|
517
|
-
queryParams.push(params.assigned_agent);
|
|
637
|
+
query = query.where(params.include_dependency_counts ? 'vt.assigned_to' : 'assigned_to', params.assigned_agent);
|
|
518
638
|
}
|
|
519
639
|
// Filter by layer
|
|
520
640
|
if (params.layer) {
|
|
521
|
-
query
|
|
522
|
-
queryParams.push(params.layer);
|
|
641
|
+
query = query.where(params.include_dependency_counts ? 'vt.layer' : 'layer', params.layer);
|
|
523
642
|
}
|
|
524
643
|
// Filter by tags
|
|
525
644
|
if (params.tags && params.tags.length > 0) {
|
|
526
645
|
for (const tag of params.tags) {
|
|
527
|
-
query
|
|
528
|
-
queryParams.push(`%${tag}%`);
|
|
646
|
+
query = query.where(params.include_dependency_counts ? 'vt.tags' : 'tags', 'like', `%${tag}%`);
|
|
529
647
|
}
|
|
530
648
|
}
|
|
531
649
|
// Order by updated timestamp (most recent first)
|
|
532
|
-
query
|
|
650
|
+
query = query.orderBy(params.include_dependency_counts ? 'vt.updated_ts' : 'updated_ts', 'desc');
|
|
533
651
|
// Pagination
|
|
534
652
|
const limit = params.limit !== undefined ? params.limit : 50;
|
|
535
653
|
const offset = params.offset || 0;
|
|
536
654
|
validateRange(limit, 'Parameter "limit"', 0, 100);
|
|
537
655
|
validateRange(offset, 'Parameter "offset"', 0, Number.MAX_SAFE_INTEGER);
|
|
538
|
-
query
|
|
539
|
-
queryParams.push(limit, offset);
|
|
656
|
+
query = query.limit(limit).offset(offset);
|
|
540
657
|
// Execute query
|
|
541
|
-
const
|
|
542
|
-
const rows = stmt.all(...queryParams);
|
|
658
|
+
const rows = await query;
|
|
543
659
|
return {
|
|
544
660
|
tasks: rows,
|
|
545
661
|
count: rows.length,
|
|
546
|
-
stale_tasks_transitioned: transitionCount
|
|
662
|
+
stale_tasks_transitioned: transitionCount,
|
|
663
|
+
git_auto_completed: gitCompletedCount,
|
|
664
|
+
git_archived: gitArchivedCount,
|
|
665
|
+
archived_tasks: archiveCount
|
|
547
666
|
};
|
|
548
667
|
}
|
|
549
668
|
catch (error) {
|
|
@@ -554,8 +673,9 @@ export function listTasks(params = {}) {
|
|
|
554
673
|
/**
|
|
555
674
|
* Move task to different status
|
|
556
675
|
*/
|
|
557
|
-
export function moveTask(params) {
|
|
558
|
-
const
|
|
676
|
+
export async function moveTask(params, adapter) {
|
|
677
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
678
|
+
const knex = actualAdapter.getKnex();
|
|
559
679
|
if (!params.task_id) {
|
|
560
680
|
throw new Error('Parameter "task_id" is required');
|
|
561
681
|
}
|
|
@@ -563,11 +683,15 @@ export function moveTask(params) {
|
|
|
563
683
|
throw new Error('Parameter "new_status" is required');
|
|
564
684
|
}
|
|
565
685
|
try {
|
|
566
|
-
// Run auto-stale detection before move
|
|
567
|
-
detectAndTransitionStaleTasks(
|
|
568
|
-
|
|
686
|
+
// Run auto-stale detection and auto-archive before move
|
|
687
|
+
await detectAndTransitionStaleTasks(actualAdapter);
|
|
688
|
+
await autoArchiveOldDoneTasks(actualAdapter);
|
|
689
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
569
690
|
// Get current status
|
|
570
|
-
const taskRow =
|
|
691
|
+
const taskRow = await trx('t_tasks')
|
|
692
|
+
.where({ id: params.task_id })
|
|
693
|
+
.select('status_id')
|
|
694
|
+
.first();
|
|
571
695
|
if (!taskRow) {
|
|
572
696
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
573
697
|
}
|
|
@@ -583,13 +707,26 @@ export function moveTask(params) {
|
|
|
583
707
|
`Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
|
|
584
708
|
}
|
|
585
709
|
// Update status
|
|
586
|
-
const
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
710
|
+
const updateData = {
|
|
711
|
+
status_id: newStatusId
|
|
712
|
+
};
|
|
713
|
+
// Set completed_ts when moving to done
|
|
714
|
+
if (newStatusId === TASK_STATUS.DONE) {
|
|
715
|
+
updateData.completed_ts = Math.floor(Date.now() / 1000);
|
|
716
|
+
}
|
|
717
|
+
await trx('t_tasks')
|
|
718
|
+
.where({ id: params.task_id })
|
|
719
|
+
.update(updateData);
|
|
720
|
+
// Activity logging (replaces trigger)
|
|
721
|
+
// Note: Using system agent (id=1) for status changes
|
|
722
|
+
// In a real implementation, you'd pass the actual agent_id who made the change
|
|
723
|
+
const systemAgentId = 1;
|
|
724
|
+
await logTaskStatusChange(knex, {
|
|
725
|
+
task_id: params.task_id,
|
|
726
|
+
old_status: currentStatusId,
|
|
727
|
+
new_status: newStatusId,
|
|
728
|
+
agent_id: systemAgentId
|
|
729
|
+
});
|
|
593
730
|
// Update watcher if moving to done or archived (stop watching)
|
|
594
731
|
if (params.new_status === 'done' || params.new_status === 'archived') {
|
|
595
732
|
try {
|
|
@@ -617,8 +754,9 @@ export function moveTask(params) {
|
|
|
617
754
|
/**
|
|
618
755
|
* Link task to decision/constraint/file
|
|
619
756
|
*/
|
|
620
|
-
export function linkTask(params) {
|
|
621
|
-
const
|
|
757
|
+
export async function linkTask(params, adapter) {
|
|
758
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
759
|
+
const knex = actualAdapter.getKnex();
|
|
622
760
|
if (!params.task_id) {
|
|
623
761
|
throw new Error('Parameter "task_id" is required');
|
|
624
762
|
}
|
|
@@ -629,21 +767,21 @@ export function linkTask(params) {
|
|
|
629
767
|
throw new Error('Parameter "target_id" is required');
|
|
630
768
|
}
|
|
631
769
|
try {
|
|
632
|
-
return transaction(
|
|
770
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
633
771
|
// Check if task exists
|
|
634
|
-
const taskExists =
|
|
772
|
+
const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
|
|
635
773
|
if (!taskExists) {
|
|
636
774
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
637
775
|
}
|
|
638
776
|
if (params.link_type === 'decision') {
|
|
639
777
|
const decisionKey = String(params.target_id);
|
|
640
|
-
const keyId = getOrCreateContextKey(
|
|
778
|
+
const keyId = await getOrCreateContextKey(actualAdapter, decisionKey, trx);
|
|
641
779
|
const linkRelation = params.link_relation || 'implements';
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
780
|
+
await knex('t_task_decision_links').insert({
|
|
781
|
+
task_id: params.task_id,
|
|
782
|
+
decision_key_id: keyId,
|
|
783
|
+
link_type: linkRelation
|
|
784
|
+
}).onConflict(['task_id', 'decision_key_id']).merge();
|
|
647
785
|
return {
|
|
648
786
|
success: true,
|
|
649
787
|
task_id: params.task_id,
|
|
@@ -656,15 +794,14 @@ export function linkTask(params) {
|
|
|
656
794
|
else if (params.link_type === 'constraint') {
|
|
657
795
|
const constraintId = Number(params.target_id);
|
|
658
796
|
// Check if constraint exists
|
|
659
|
-
const constraintExists =
|
|
797
|
+
const constraintExists = await trx('t_constraints').where({ id: constraintId }).first();
|
|
660
798
|
if (!constraintExists) {
|
|
661
799
|
throw new Error(`Constraint with id ${constraintId} not found`);
|
|
662
800
|
}
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
stmt.run(params.task_id, constraintId);
|
|
801
|
+
await trx('t_task_constraint_links').insert({
|
|
802
|
+
task_id: params.task_id,
|
|
803
|
+
constraint_id: constraintId
|
|
804
|
+
}).onConflict(['task_id', 'constraint_id']).ignore();
|
|
668
805
|
return {
|
|
669
806
|
success: true,
|
|
670
807
|
task_id: params.task_id,
|
|
@@ -674,21 +811,23 @@ export function linkTask(params) {
|
|
|
674
811
|
};
|
|
675
812
|
}
|
|
676
813
|
else if (params.link_type === 'file') {
|
|
814
|
+
// Deprecation warning (v3.4.1)
|
|
815
|
+
console.warn(`⚠️ DEPRECATION WARNING: task.link(link_type="file") is deprecated as of v3.4.1.`);
|
|
816
|
+
console.warn(` Use task.create(watch_files=[...]) or task.update(watch_files=[...]) instead.`);
|
|
817
|
+
console.warn(` Or use the new watch_files action: { action: "watch_files", task_id: ${params.task_id}, file_paths: ["..."] }`);
|
|
677
818
|
const filePath = String(params.target_id);
|
|
678
|
-
const fileId = getOrCreateFile(
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
stmt.run(params.task_id, fileId);
|
|
819
|
+
const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
|
|
820
|
+
await trx('t_task_file_links').insert({
|
|
821
|
+
task_id: params.task_id,
|
|
822
|
+
file_id: fileId
|
|
823
|
+
}).onConflict(['task_id', 'file_id']).ignore();
|
|
684
824
|
// Register file with watcher for auto-tracking
|
|
685
825
|
try {
|
|
686
|
-
const taskData =
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
`).get(params.task_id);
|
|
826
|
+
const taskData = await trx('t_tasks as t')
|
|
827
|
+
.join('m_task_statuses as s', 't.status_id', 's.id')
|
|
828
|
+
.where('t.id', params.task_id)
|
|
829
|
+
.select('t.title', 's.name as status')
|
|
830
|
+
.first();
|
|
692
831
|
if (taskData) {
|
|
693
832
|
const watcher = FileWatcher.getInstance();
|
|
694
833
|
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
@@ -703,7 +842,8 @@ export function linkTask(params) {
|
|
|
703
842
|
task_id: params.task_id,
|
|
704
843
|
linked_to: 'file',
|
|
705
844
|
target: filePath,
|
|
706
|
-
|
|
845
|
+
deprecation_warning: 'task.link(link_type="file") is deprecated. Use task.create/update(watch_files) or watch_files action instead.',
|
|
846
|
+
message: `Task ${params.task_id} linked to file "${filePath}" (DEPRECATED API - use watch_files instead)`
|
|
707
847
|
};
|
|
708
848
|
}
|
|
709
849
|
else {
|
|
@@ -719,15 +859,19 @@ export function linkTask(params) {
|
|
|
719
859
|
/**
|
|
720
860
|
* Archive completed task
|
|
721
861
|
*/
|
|
722
|
-
export function archiveTask(params) {
|
|
723
|
-
const
|
|
862
|
+
export async function archiveTask(params, adapter) {
|
|
863
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
864
|
+
const knex = actualAdapter.getKnex();
|
|
724
865
|
if (!params.task_id) {
|
|
725
866
|
throw new Error('Parameter "task_id" is required');
|
|
726
867
|
}
|
|
727
868
|
try {
|
|
728
|
-
return transaction(
|
|
869
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
729
870
|
// Check if task is in 'done' status
|
|
730
|
-
const taskRow =
|
|
871
|
+
const taskRow = await trx('t_tasks')
|
|
872
|
+
.where({ id: params.task_id })
|
|
873
|
+
.select('status_id')
|
|
874
|
+
.first();
|
|
731
875
|
if (!taskRow) {
|
|
732
876
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
733
877
|
}
|
|
@@ -735,8 +879,18 @@ export function archiveTask(params) {
|
|
|
735
879
|
throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
|
|
736
880
|
}
|
|
737
881
|
// Update to archived
|
|
738
|
-
|
|
739
|
-
|
|
882
|
+
await trx('t_tasks')
|
|
883
|
+
.where({ id: params.task_id })
|
|
884
|
+
.update({ status_id: TASK_STATUS.ARCHIVED });
|
|
885
|
+
// Activity logging
|
|
886
|
+
// Note: Using system agent (id=1) for status changes
|
|
887
|
+
const systemAgentId = 1;
|
|
888
|
+
await logTaskStatusChange(knex, {
|
|
889
|
+
task_id: params.task_id,
|
|
890
|
+
old_status: TASK_STATUS.DONE,
|
|
891
|
+
new_status: TASK_STATUS.ARCHIVED,
|
|
892
|
+
agent_id: systemAgentId
|
|
893
|
+
});
|
|
740
894
|
// Unregister from file watcher (archived tasks don't need tracking)
|
|
741
895
|
try {
|
|
742
896
|
const watcher = FileWatcher.getInstance();
|
|
@@ -760,8 +914,9 @@ export function archiveTask(params) {
|
|
|
760
914
|
/**
|
|
761
915
|
* Add dependency (blocking relationship) between tasks
|
|
762
916
|
*/
|
|
763
|
-
export function addDependency(params) {
|
|
764
|
-
const
|
|
917
|
+
export async function addDependency(params, adapter) {
|
|
918
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
919
|
+
const knex = actualAdapter.getKnex();
|
|
765
920
|
if (!params.blocker_task_id) {
|
|
766
921
|
throw new Error('Parameter "blocker_task_id" is required');
|
|
767
922
|
}
|
|
@@ -769,14 +924,20 @@ export function addDependency(params) {
|
|
|
769
924
|
throw new Error('Parameter "blocked_task_id" is required');
|
|
770
925
|
}
|
|
771
926
|
try {
|
|
772
|
-
return transaction(
|
|
927
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
773
928
|
// Validation 1: No self-dependencies
|
|
774
929
|
if (params.blocker_task_id === params.blocked_task_id) {
|
|
775
930
|
throw new Error('Self-dependency not allowed');
|
|
776
931
|
}
|
|
777
932
|
// Validation 2: Both tasks must exist and check if archived
|
|
778
|
-
const blockerTask =
|
|
779
|
-
|
|
933
|
+
const blockerTask = await trx('t_tasks')
|
|
934
|
+
.where({ id: params.blocker_task_id })
|
|
935
|
+
.select('id', 'status_id')
|
|
936
|
+
.first();
|
|
937
|
+
const blockedTask = await trx('t_tasks')
|
|
938
|
+
.where({ id: params.blocked_task_id })
|
|
939
|
+
.select('id', 'status_id')
|
|
940
|
+
.first();
|
|
780
941
|
if (!blockerTask) {
|
|
781
942
|
throw new Error(`Blocker task #${params.blocker_task_id} not found`);
|
|
782
943
|
}
|
|
@@ -791,15 +952,17 @@ export function addDependency(params) {
|
|
|
791
952
|
throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
|
|
792
953
|
}
|
|
793
954
|
// Validation 4: No direct circular (reverse relationship)
|
|
794
|
-
const reverseExists =
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
955
|
+
const reverseExists = await trx('t_task_dependencies')
|
|
956
|
+
.where({
|
|
957
|
+
blocker_task_id: params.blocked_task_id,
|
|
958
|
+
blocked_task_id: params.blocker_task_id
|
|
959
|
+
})
|
|
960
|
+
.first();
|
|
798
961
|
if (reverseExists) {
|
|
799
962
|
throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
|
|
800
963
|
}
|
|
801
964
|
// Validation 5: No transitive circular (check if adding this would create a cycle)
|
|
802
|
-
const cycleCheck =
|
|
965
|
+
const cycleCheck = await trx.raw(`
|
|
803
966
|
WITH RECURSIVE dependency_chain AS (
|
|
804
967
|
-- Start from the task that would be blocked
|
|
805
968
|
SELECT blocked_task_id as task_id, 1 as depth
|
|
@@ -815,10 +978,11 @@ export function addDependency(params) {
|
|
|
815
978
|
WHERE dc.depth < 100
|
|
816
979
|
)
|
|
817
980
|
SELECT task_id FROM dependency_chain WHERE task_id = ?
|
|
818
|
-
|
|
981
|
+
`, [params.blocked_task_id, params.blocker_task_id])
|
|
982
|
+
.then((result) => result[0]);
|
|
819
983
|
if (cycleCheck) {
|
|
820
984
|
// Build cycle path for error message
|
|
821
|
-
const cyclePathResult =
|
|
985
|
+
const cyclePathResult = await trx.raw(`
|
|
822
986
|
WITH RECURSIVE dependency_chain AS (
|
|
823
987
|
SELECT blocked_task_id as task_id, 1 as depth,
|
|
824
988
|
CAST(blocked_task_id AS TEXT) as path
|
|
@@ -834,16 +998,17 @@ export function addDependency(params) {
|
|
|
834
998
|
WHERE dc.depth < 100
|
|
835
999
|
)
|
|
836
1000
|
SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
|
|
837
|
-
|
|
1001
|
+
`, [params.blocked_task_id, params.blocker_task_id])
|
|
1002
|
+
.then((result) => result[0]);
|
|
838
1003
|
const cyclePath = cyclePathResult?.path || `#${params.blocked_task_id} → ... → #${params.blocker_task_id}`;
|
|
839
1004
|
throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
|
|
840
1005
|
}
|
|
841
1006
|
// All validations passed - insert dependency
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1007
|
+
await trx('t_task_dependencies').insert({
|
|
1008
|
+
blocker_task_id: params.blocker_task_id,
|
|
1009
|
+
blocked_task_id: params.blocked_task_id,
|
|
1010
|
+
created_ts: Math.floor(Date.now() / 1000)
|
|
1011
|
+
});
|
|
847
1012
|
return {
|
|
848
1013
|
success: true,
|
|
849
1014
|
message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
|
|
@@ -862,8 +1027,9 @@ export function addDependency(params) {
|
|
|
862
1027
|
/**
|
|
863
1028
|
* Remove dependency between tasks
|
|
864
1029
|
*/
|
|
865
|
-
export function removeDependency(params) {
|
|
866
|
-
const
|
|
1030
|
+
export async function removeDependency(params, adapter) {
|
|
1031
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1032
|
+
const knex = actualAdapter.getKnex();
|
|
867
1033
|
if (!params.blocker_task_id) {
|
|
868
1034
|
throw new Error('Parameter "blocker_task_id" is required');
|
|
869
1035
|
}
|
|
@@ -871,11 +1037,12 @@ export function removeDependency(params) {
|
|
|
871
1037
|
throw new Error('Parameter "blocked_task_id" is required');
|
|
872
1038
|
}
|
|
873
1039
|
try {
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
1040
|
+
await knex('t_task_dependencies')
|
|
1041
|
+
.where({
|
|
1042
|
+
blocker_task_id: params.blocker_task_id,
|
|
1043
|
+
blocked_task_id: params.blocked_task_id
|
|
1044
|
+
})
|
|
1045
|
+
.delete();
|
|
879
1046
|
return {
|
|
880
1047
|
success: true,
|
|
881
1048
|
message: `Dependency removed: Task #${params.blocker_task_id} no longer blocks Task #${params.blocked_task_id}`
|
|
@@ -889,20 +1056,21 @@ export function removeDependency(params) {
|
|
|
889
1056
|
/**
|
|
890
1057
|
* Get dependencies for a task (bidirectional: what blocks this task, what this task blocks)
|
|
891
1058
|
*/
|
|
892
|
-
export function getDependencies(params) {
|
|
893
|
-
const
|
|
1059
|
+
export async function getDependencies(params, adapter) {
|
|
1060
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1061
|
+
const knex = actualAdapter.getKnex();
|
|
894
1062
|
if (!params.task_id) {
|
|
895
1063
|
throw new Error('Parameter "task_id" is required');
|
|
896
1064
|
}
|
|
897
1065
|
const includeDetails = params.include_details || false;
|
|
898
1066
|
try {
|
|
899
1067
|
// Check if task exists
|
|
900
|
-
const taskExists =
|
|
1068
|
+
const taskExists = await knex('t_tasks').where({ id: params.task_id }).first();
|
|
901
1069
|
if (!taskExists) {
|
|
902
1070
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
903
1071
|
}
|
|
904
1072
|
// Use the shared helper function
|
|
905
|
-
const deps = queryTaskDependencies(
|
|
1073
|
+
const deps = await queryTaskDependencies(actualAdapter, params.task_id, includeDetails);
|
|
906
1074
|
return {
|
|
907
1075
|
task_id: params.task_id,
|
|
908
1076
|
blockers: deps.blockers,
|
|
@@ -921,32 +1089,296 @@ export function getDependencies(params) {
|
|
|
921
1089
|
/**
|
|
922
1090
|
* Create multiple tasks atomically
|
|
923
1091
|
*/
|
|
924
|
-
export function batchCreateTasks(params) {
|
|
925
|
-
const
|
|
1092
|
+
export async function batchCreateTasks(params, adapter) {
|
|
1093
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
926
1094
|
if (!params.tasks || !Array.isArray(params.tasks)) {
|
|
927
1095
|
throw new Error('Parameter "tasks" is required and must be an array');
|
|
928
1096
|
}
|
|
1097
|
+
if (params.tasks.length > 50) {
|
|
1098
|
+
throw new Error('Parameter "tasks" must contain at most 50 items');
|
|
1099
|
+
}
|
|
929
1100
|
const atomic = params.atomic !== undefined ? params.atomic : true;
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
1101
|
+
try {
|
|
1102
|
+
if (atomic) {
|
|
1103
|
+
// Atomic mode: All or nothing
|
|
1104
|
+
const results = await actualAdapter.transaction(async (trx) => {
|
|
1105
|
+
const processedResults = [];
|
|
1106
|
+
for (const task of params.tasks) {
|
|
1107
|
+
try {
|
|
1108
|
+
const result = await createTaskInternal(task, actualAdapter, trx);
|
|
1109
|
+
processedResults.push({
|
|
1110
|
+
title: task.title,
|
|
1111
|
+
task_id: result.task_id,
|
|
1112
|
+
success: true,
|
|
1113
|
+
error: undefined
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
catch (error) {
|
|
1117
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1118
|
+
throw new Error(`Batch failed at task "${task.title}": ${errorMessage}`);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
return processedResults;
|
|
1122
|
+
});
|
|
1123
|
+
return {
|
|
1124
|
+
success: true,
|
|
1125
|
+
created: results.length,
|
|
1126
|
+
failed: 0,
|
|
1127
|
+
results: results
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
else {
|
|
1131
|
+
// Non-atomic mode: Process each independently
|
|
1132
|
+
const results = [];
|
|
1133
|
+
let created = 0;
|
|
1134
|
+
let failed = 0;
|
|
1135
|
+
for (const task of params.tasks) {
|
|
1136
|
+
try {
|
|
1137
|
+
const result = await actualAdapter.transaction(async (trx) => {
|
|
1138
|
+
return await createTaskInternal(task, actualAdapter, trx);
|
|
1139
|
+
});
|
|
1140
|
+
results.push({
|
|
1141
|
+
title: task.title,
|
|
1142
|
+
task_id: result.task_id,
|
|
1143
|
+
success: true,
|
|
1144
|
+
error: undefined
|
|
1145
|
+
});
|
|
1146
|
+
created++;
|
|
1147
|
+
}
|
|
1148
|
+
catch (error) {
|
|
1149
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1150
|
+
results.push({
|
|
1151
|
+
title: task.title,
|
|
1152
|
+
task_id: undefined,
|
|
1153
|
+
success: false,
|
|
1154
|
+
error: errorMessage
|
|
1155
|
+
});
|
|
1156
|
+
failed++;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
return {
|
|
1160
|
+
success: failed === 0,
|
|
1161
|
+
created: created,
|
|
1162
|
+
failed: failed,
|
|
1163
|
+
results: results
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
catch (error) {
|
|
1168
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1169
|
+
throw new Error(`Failed to execute batch operation: ${message}`);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Watch/unwatch files for a task (v3.4.1)
|
|
1174
|
+
* Replaces the need to use task.link(file) for file watching
|
|
1175
|
+
*/
|
|
1176
|
+
export async function watchFiles(params, adapter) {
|
|
1177
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1178
|
+
const knex = actualAdapter.getKnex();
|
|
1179
|
+
if (!params.task_id) {
|
|
1180
|
+
throw new Error('Parameter "task_id" is required');
|
|
1181
|
+
}
|
|
1182
|
+
if (!params.action) {
|
|
1183
|
+
throw new Error('Parameter "action" is required (watch, unwatch, or list)');
|
|
1184
|
+
}
|
|
1185
|
+
try {
|
|
1186
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
1187
|
+
// Check if task exists
|
|
1188
|
+
const taskData = await trx('t_tasks as t')
|
|
1189
|
+
.join('m_task_statuses as s', 't.status_id', 's.id')
|
|
1190
|
+
.where('t.id', params.task_id)
|
|
1191
|
+
.select('t.id', 't.title', 's.name as status')
|
|
1192
|
+
.first();
|
|
1193
|
+
if (!taskData) {
|
|
1194
|
+
throw new Error(`Task with id ${params.task_id} not found`);
|
|
1195
|
+
}
|
|
1196
|
+
if (params.action === 'watch') {
|
|
1197
|
+
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1198
|
+
throw new Error('Parameter "file_paths" is required for watch action');
|
|
1199
|
+
}
|
|
1200
|
+
const addedFiles = [];
|
|
1201
|
+
for (const filePath of params.file_paths) {
|
|
1202
|
+
const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
|
|
1203
|
+
// Check if already exists
|
|
1204
|
+
const existing = await trx('t_task_file_links')
|
|
1205
|
+
.where({ task_id: params.task_id, file_id: fileId })
|
|
1206
|
+
.first();
|
|
1207
|
+
if (!existing) {
|
|
1208
|
+
await trx('t_task_file_links').insert({
|
|
1209
|
+
task_id: params.task_id,
|
|
1210
|
+
file_id: fileId
|
|
1211
|
+
});
|
|
1212
|
+
addedFiles.push(filePath);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
// Register files with watcher
|
|
1216
|
+
try {
|
|
1217
|
+
const watcher = FileWatcher.getInstance();
|
|
1218
|
+
for (const filePath of addedFiles) {
|
|
1219
|
+
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
catch (error) {
|
|
1223
|
+
// Watcher may not be initialized yet, ignore
|
|
1224
|
+
console.error('Warning: Could not register files with watcher:', error);
|
|
1225
|
+
}
|
|
1226
|
+
return {
|
|
1227
|
+
success: true,
|
|
1228
|
+
task_id: params.task_id,
|
|
1229
|
+
action: 'watch',
|
|
1230
|
+
files_added: addedFiles.length,
|
|
1231
|
+
files: addedFiles,
|
|
1232
|
+
message: `Watching ${addedFiles.length} file(s) for task ${params.task_id}`
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
else if (params.action === 'unwatch') {
|
|
1236
|
+
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1237
|
+
throw new Error('Parameter "file_paths" is required for unwatch action');
|
|
1238
|
+
}
|
|
1239
|
+
const removedFiles = [];
|
|
1240
|
+
for (const filePath of params.file_paths) {
|
|
1241
|
+
const deleted = await trx('t_task_file_links')
|
|
1242
|
+
.where('task_id', params.task_id)
|
|
1243
|
+
.whereIn('file_id', function () {
|
|
1244
|
+
this.select('id').from('m_files').where({ path: filePath });
|
|
1245
|
+
})
|
|
1246
|
+
.delete();
|
|
1247
|
+
if (deleted > 0) {
|
|
1248
|
+
removedFiles.push(filePath);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return {
|
|
1252
|
+
success: true,
|
|
1253
|
+
task_id: params.task_id,
|
|
1254
|
+
action: 'unwatch',
|
|
1255
|
+
files_removed: removedFiles.length,
|
|
1256
|
+
files: removedFiles,
|
|
1257
|
+
message: `Stopped watching ${removedFiles.length} file(s) for task ${params.task_id}`
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
else if (params.action === 'list') {
|
|
1261
|
+
const files = await trx('t_task_file_links as tfl')
|
|
1262
|
+
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
1263
|
+
.where('tfl.task_id', params.task_id)
|
|
1264
|
+
.select('f.path')
|
|
1265
|
+
.then(rows => rows.map((row) => row.path));
|
|
1266
|
+
return {
|
|
1267
|
+
success: true,
|
|
1268
|
+
task_id: params.task_id,
|
|
1269
|
+
action: 'list',
|
|
1270
|
+
files_count: files.length,
|
|
1271
|
+
files: files,
|
|
1272
|
+
message: `Task ${params.task_id} is watching ${files.length} file(s)`
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
else {
|
|
1276
|
+
throw new Error(`Invalid action: ${params.action}. Must be one of: watch, unwatch, list`);
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
catch (error) {
|
|
1281
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1282
|
+
throw new Error(`Failed to ${params.action} files: ${message}`);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
/**
|
|
1286
|
+
* Get pruned files for a task (v3.5.0 Auto-Pruning)
|
|
1287
|
+
* Returns audit trail of files that were auto-pruned as non-existent
|
|
1288
|
+
*/
|
|
1289
|
+
export async function getPrunedFiles(params, adapter) {
|
|
1290
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1291
|
+
const knex = actualAdapter.getKnex();
|
|
1292
|
+
try {
|
|
1293
|
+
// Validate task_id
|
|
1294
|
+
if (!params.task_id || typeof params.task_id !== 'number') {
|
|
1295
|
+
throw new Error('task_id is required and must be a number');
|
|
1296
|
+
}
|
|
1297
|
+
// Validate task exists
|
|
1298
|
+
const task = await knex('t_tasks').where({ id: params.task_id }).first();
|
|
1299
|
+
if (!task) {
|
|
1300
|
+
throw new Error(`Task not found: ${params.task_id}`);
|
|
1301
|
+
}
|
|
1302
|
+
// Get pruned files
|
|
1303
|
+
const limit = params.limit || 100;
|
|
1304
|
+
const rows = await knex('t_task_pruned_files as tpf')
|
|
1305
|
+
.leftJoin('m_context_keys as k', 'tpf.linked_decision_key_id', 'k.id')
|
|
1306
|
+
.where('tpf.task_id', params.task_id)
|
|
1307
|
+
.select('tpf.id', 'tpf.file_path', knex.raw(`datetime(tpf.pruned_ts, 'unixepoch') as pruned_at`), 'k.key as linked_decision')
|
|
1308
|
+
.orderBy('tpf.pruned_ts', 'desc')
|
|
1309
|
+
.limit(limit);
|
|
933
1310
|
return {
|
|
934
|
-
|
|
935
|
-
task_id:
|
|
1311
|
+
success: true,
|
|
1312
|
+
task_id: params.task_id,
|
|
1313
|
+
pruned_files: rows,
|
|
1314
|
+
count: rows.length,
|
|
1315
|
+
message: rows.length > 0
|
|
1316
|
+
? `Found ${rows.length} pruned file(s) for task ${params.task_id}`
|
|
1317
|
+
: `No pruned files for task ${params.task_id}`
|
|
936
1318
|
};
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1319
|
+
}
|
|
1320
|
+
catch (error) {
|
|
1321
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1322
|
+
throw new Error(`Failed to get pruned files: ${message}`);
|
|
1323
|
+
}
|
|
1324
|
+
}
|
|
1325
|
+
/**
|
|
1326
|
+
* Link a pruned file to a decision (v3.5.0 Auto-Pruning)
|
|
1327
|
+
* Attaches WHY reasoning to pruned files for project archaeology
|
|
1328
|
+
*/
|
|
1329
|
+
export async function linkPrunedFile(params, adapter) {
|
|
1330
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1331
|
+
const knex = actualAdapter.getKnex();
|
|
1332
|
+
try {
|
|
1333
|
+
// Validate pruned_file_id
|
|
1334
|
+
if (!params.pruned_file_id || typeof params.pruned_file_id !== 'number') {
|
|
1335
|
+
throw new Error('pruned_file_id is required and must be a number');
|
|
1336
|
+
}
|
|
1337
|
+
// Validate decision_key
|
|
1338
|
+
if (!params.decision_key || typeof params.decision_key !== 'string') {
|
|
1339
|
+
throw new Error('decision_key is required and must be a string');
|
|
1340
|
+
}
|
|
1341
|
+
// Get decision key_id
|
|
1342
|
+
const decision = await knex('m_context_keys as k')
|
|
1343
|
+
.whereExists(function () {
|
|
1344
|
+
this.select('*')
|
|
1345
|
+
.from('t_decisions as d')
|
|
1346
|
+
.whereRaw('d.key_id = k.id');
|
|
1347
|
+
})
|
|
1348
|
+
.where('k.key', params.decision_key)
|
|
1349
|
+
.select('k.id as key_id')
|
|
1350
|
+
.first();
|
|
1351
|
+
if (!decision) {
|
|
1352
|
+
throw new Error(`Decision not found: ${params.decision_key}`);
|
|
1353
|
+
}
|
|
1354
|
+
// Check if pruned file exists
|
|
1355
|
+
const prunedFile = await knex('t_task_pruned_files')
|
|
1356
|
+
.where({ id: params.pruned_file_id })
|
|
1357
|
+
.select('id', 'task_id', 'file_path')
|
|
1358
|
+
.first();
|
|
1359
|
+
if (!prunedFile) {
|
|
1360
|
+
throw new Error(`Pruned file record not found: ${params.pruned_file_id}`);
|
|
1361
|
+
}
|
|
1362
|
+
// Update the link
|
|
1363
|
+
const updated = await knex('t_task_pruned_files')
|
|
1364
|
+
.where({ id: params.pruned_file_id })
|
|
1365
|
+
.update({ linked_decision_key_id: decision.key_id });
|
|
1366
|
+
if (updated === 0) {
|
|
1367
|
+
throw new Error(`Failed to link pruned file #${params.pruned_file_id} to decision ${params.decision_key}`);
|
|
1368
|
+
}
|
|
1369
|
+
return {
|
|
1370
|
+
success: true,
|
|
1371
|
+
pruned_file_id: params.pruned_file_id,
|
|
1372
|
+
decision_key: params.decision_key,
|
|
1373
|
+
task_id: prunedFile.task_id,
|
|
1374
|
+
file_path: prunedFile.file_path,
|
|
1375
|
+
message: `Linked pruned file "${prunedFile.file_path}" to decision "${params.decision_key}"`
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
catch (error) {
|
|
1379
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1380
|
+
throw new Error(`Failed to link pruned file: ${message}`);
|
|
1381
|
+
}
|
|
950
1382
|
}
|
|
951
1383
|
/**
|
|
952
1384
|
* Return comprehensive help documentation
|
|
@@ -961,7 +1393,8 @@ export function taskHelp() {
|
|
|
961
1393
|
create: {
|
|
962
1394
|
description: 'Create a new task',
|
|
963
1395
|
required_params: ['title'],
|
|
964
|
-
optional_params: ['description', 'acceptance_criteria', 'notes', 'priority', 'assigned_agent', 'created_by_agent', 'layer', 'tags', 'status'],
|
|
1396
|
+
optional_params: ['description', 'acceptance_criteria', 'notes', 'priority', 'assigned_agent', 'created_by_agent', 'layer', 'tags', 'status', 'watch_files'],
|
|
1397
|
+
watch_files_param: '⭐ NEW in v3.4.1: Pass watch_files array to automatically link and watch files (replaces task.link(file))',
|
|
965
1398
|
example: {
|
|
966
1399
|
action: 'create',
|
|
967
1400
|
title: 'Implement authentication endpoint',
|
|
@@ -969,18 +1402,21 @@ export function taskHelp() {
|
|
|
969
1402
|
priority: 3,
|
|
970
1403
|
assigned_agent: 'backend-agent',
|
|
971
1404
|
layer: 'presentation',
|
|
972
|
-
tags: ['api', 'authentication']
|
|
1405
|
+
tags: ['api', 'authentication'],
|
|
1406
|
+
watch_files: ['src/api/auth.ts', 'src/middleware/jwt.ts']
|
|
973
1407
|
}
|
|
974
1408
|
},
|
|
975
1409
|
update: {
|
|
976
1410
|
description: 'Update task metadata',
|
|
977
1411
|
required_params: ['task_id'],
|
|
978
|
-
optional_params: ['title', 'priority', 'assigned_agent', 'layer', 'description', 'acceptance_criteria', 'notes'],
|
|
1412
|
+
optional_params: ['title', 'priority', 'assigned_agent', 'layer', 'description', 'acceptance_criteria', 'notes', 'watch_files'],
|
|
1413
|
+
watch_files_param: '⭐ NEW in v3.4.1: Pass watch_files array to add files to watch list',
|
|
979
1414
|
example: {
|
|
980
1415
|
action: 'update',
|
|
981
1416
|
task_id: 5,
|
|
982
1417
|
priority: 4,
|
|
983
|
-
assigned_agent: 'senior-backend-agent'
|
|
1418
|
+
assigned_agent: 'senior-backend-agent',
|
|
1419
|
+
watch_files: ['src/api/users.ts']
|
|
984
1420
|
}
|
|
985
1421
|
},
|
|
986
1422
|
get: {
|
|
@@ -1025,7 +1461,8 @@ export function taskHelp() {
|
|
|
1025
1461
|
required_params: ['task_id', 'link_type', 'target_id'],
|
|
1026
1462
|
optional_params: ['link_relation'],
|
|
1027
1463
|
link_types: ['decision', 'constraint', 'file'],
|
|
1028
|
-
file_linking_behavior: '⚠️
|
|
1464
|
+
file_linking_behavior: '⚠️ DEPRECATED in v3.4.1: link_type="file" is deprecated. Use watch_files action or watch_files parameter instead.',
|
|
1465
|
+
deprecation_note: 'For file watching, use: (1) watch_files parameter in create/update, or (2) watch_files action with watch/unwatch/list',
|
|
1029
1466
|
example: {
|
|
1030
1467
|
action: 'link',
|
|
1031
1468
|
task_id: 5,
|
|
@@ -1034,6 +1471,34 @@ export function taskHelp() {
|
|
|
1034
1471
|
link_relation: 'implements'
|
|
1035
1472
|
}
|
|
1036
1473
|
},
|
|
1474
|
+
watch_files: {
|
|
1475
|
+
description: '⭐ NEW in v3.4.1: Watch/unwatch files for a task (replaces task.link(file))',
|
|
1476
|
+
required_params: ['task_id', 'action'],
|
|
1477
|
+
optional_params: ['file_paths'],
|
|
1478
|
+
actions: ['watch', 'unwatch', 'list'],
|
|
1479
|
+
behavior: {
|
|
1480
|
+
watch: 'Add files to watch list and activate file monitoring',
|
|
1481
|
+
unwatch: 'Remove files from watch list',
|
|
1482
|
+
list: 'List all files currently watched by this task'
|
|
1483
|
+
},
|
|
1484
|
+
examples: {
|
|
1485
|
+
watch: {
|
|
1486
|
+
task_id: 5,
|
|
1487
|
+
action: 'watch',
|
|
1488
|
+
file_paths: ['src/api/auth.ts', 'src/middleware/jwt.ts']
|
|
1489
|
+
},
|
|
1490
|
+
unwatch: {
|
|
1491
|
+
task_id: 5,
|
|
1492
|
+
action: 'unwatch',
|
|
1493
|
+
file_paths: ['src/middleware/jwt.ts']
|
|
1494
|
+
},
|
|
1495
|
+
list: {
|
|
1496
|
+
task_id: 5,
|
|
1497
|
+
action: 'list'
|
|
1498
|
+
}
|
|
1499
|
+
},
|
|
1500
|
+
note: 'Preferred over task.link(file) for better clarity and batch operations'
|
|
1501
|
+
},
|
|
1037
1502
|
archive: {
|
|
1038
1503
|
description: 'Archive completed task (must be in done status)',
|
|
1039
1504
|
required_params: ['task_id'],
|
|
@@ -1176,7 +1641,9 @@ export function taskHelp() {
|
|
|
1176
1641
|
/**
|
|
1177
1642
|
* Query file watcher status and monitored files/tasks
|
|
1178
1643
|
*/
|
|
1179
|
-
export function watcherStatus(args) {
|
|
1644
|
+
export async function watcherStatus(args, adapter) {
|
|
1645
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1646
|
+
const knex = actualAdapter.getKnex();
|
|
1180
1647
|
const subaction = args.subaction || 'status';
|
|
1181
1648
|
const watcher = FileWatcher.getInstance();
|
|
1182
1649
|
if (subaction === 'help') {
|
|
@@ -1219,15 +1686,13 @@ export function watcherStatus(args) {
|
|
|
1219
1686
|
};
|
|
1220
1687
|
}
|
|
1221
1688
|
if (subaction === 'list_files') {
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
ORDER BY tfl.file_path, t.id
|
|
1230
|
-
`).all();
|
|
1689
|
+
const fileLinks = await knex('t_task_file_links as tfl')
|
|
1690
|
+
.join('t_tasks as t', 'tfl.task_id', 't.id')
|
|
1691
|
+
.join('m_task_statuses as ts', 't.status_id', 'ts.id')
|
|
1692
|
+
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
1693
|
+
.where('t.status_id', '!=', 6) // Exclude archived tasks
|
|
1694
|
+
.select('f.path as file_path', 't.id', 't.title', 'ts.name as status_name')
|
|
1695
|
+
.orderBy(['f.path', 't.id']);
|
|
1231
1696
|
// Group by file
|
|
1232
1697
|
const fileMap = new Map();
|
|
1233
1698
|
for (const link of fileLinks) {
|
|
@@ -1254,17 +1719,14 @@ export function watcherStatus(args) {
|
|
|
1254
1719
|
};
|
|
1255
1720
|
}
|
|
1256
1721
|
if (subaction === 'list_tasks') {
|
|
1257
|
-
const
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
GROUP BY t.id, t.title, ts.status_name
|
|
1266
|
-
ORDER BY t.id
|
|
1267
|
-
`).all();
|
|
1722
|
+
const taskLinks = await knex('t_tasks as t')
|
|
1723
|
+
.join('m_task_statuses as ts', 't.status_id', 'ts.id')
|
|
1724
|
+
.join('t_task_file_links as tfl', 't.id', 'tfl.task_id')
|
|
1725
|
+
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
1726
|
+
.where('t.status_id', '!=', 6) // Exclude archived tasks
|
|
1727
|
+
.groupBy('t.id', 't.title', 'ts.name')
|
|
1728
|
+
.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'))
|
|
1729
|
+
.orderBy('t.id');
|
|
1268
1730
|
const tasks = taskLinks.map(task => ({
|
|
1269
1731
|
task_id: task.id,
|
|
1270
1732
|
task_title: task.title,
|