sqlew 3.5.3 → 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +247 -1772
- package/README.md +70 -304
- package/assets/config.example.toml +106 -0
- 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.map +1 -1
- package/dist/config/loader.js +4 -0
- package/dist/config/loader.js.map +1 -1
- package/dist/config/types.d.ts +11 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/database.d.ts +56 -121
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +266 -414
- package/dist/database.js.map +1 -1
- package/dist/index.js +329 -245
- 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/index.d.ts +25 -12
- package/dist/migrations/index.d.ts.map +1 -1
- package/dist/migrations/index.js +147 -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/knex/20251027000000_add_agent_reuse_system.d.ts +14 -0
- package/dist/migrations/knex/20251027000000_add_agent_reuse_system.d.ts.map +1 -0
- package/dist/migrations/knex/20251027000000_add_agent_reuse_system.js +34 -0
- package/dist/migrations/knex/20251027000000_add_agent_reuse_system.js.map +1 -0
- package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.d.ts +4 -0
- package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.d.ts.map +1 -0
- package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.js +24 -0
- package/dist/migrations/knex/20251027010000_add_task_constraint_to_decision_context.js.map +1 -0
- package/dist/migrations/knex/20251027020000_update_agent_reusability.d.ts +16 -0
- package/dist/migrations/knex/20251027020000_update_agent_reusability.d.ts.map +1 -0
- package/dist/migrations/knex/20251027020000_update_agent_reusability.js +27 -0
- package/dist/migrations/knex/20251027020000_update_agent_reusability.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/agent-reuse.test.d.ts +6 -0
- package/dist/tests/agent-reuse.test.d.ts.map +1 -0
- package/dist/tests/agent-reuse.test.js +242 -0
- package/dist/tests/agent-reuse.test.js.map +1 -0
- package/dist/tests/all-features.test.d.ts +7 -0
- package/dist/tests/all-features.test.d.ts.map +1 -0
- package/dist/tests/all-features.test.js +514 -0
- package/dist/tests/all-features.test.js.map +1 -0
- package/dist/tests/git-aware-completion.test.js +89 -70
- package/dist/tests/git-aware-completion.test.js.map +1 -1
- 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.js +92 -78
- package/dist/tests/tasks.auto-pruning-decision-link.test.js.map +1 -1
- package/dist/tests/tasks.auto-pruning-partial.test.js +106 -95
- package/dist/tests/tasks.auto-pruning-partial.test.js.map +1 -1
- package/dist/tests/tasks.auto-pruning-persistence.test.js +115 -97
- package/dist/tests/tasks.auto-pruning-persistence.test.js.map +1 -1
- package/dist/tests/tasks.auto-pruning-safety.test.js +124 -103
- package/dist/tests/tasks.auto-pruning-safety.test.js.map +1 -1
- 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.js +116 -104
- package/dist/tests/tasks.link-file-backward-compat.test.js.map +1 -1
- package/dist/tests/tasks.watch-files-action.test.js +122 -101
- package/dist/tests/tasks.watch-files-action.test.js.map +1 -1
- package/dist/tests/tasks.watch-files-parameter.test.js +105 -94
- package/dist/tests/tasks.watch-files-parameter.test.js.map +1 -1
- package/dist/tests/two-step-git-completion.test.js +176 -133
- package/dist/tests/two-step-git-completion.test.js.map +1 -1
- package/dist/tests/vcs-staging.test.js +1 -1
- package/dist/tests/vcs-staging.test.js.map +1 -1
- package/dist/tools/config.d.ts +9 -6
- package/dist/tools/config.d.ts.map +1 -1
- package/dist/tools/config.js +16 -14
- package/dist/tools/config.js.map +1 -1
- package/dist/tools/constraints.d.ts +10 -7
- package/dist/tools/constraints.d.ts.map +1 -1
- package/dist/tools/constraints.js +73 -51
- package/dist/tools/constraints.js.map +1 -1
- package/dist/tools/context.d.ts +36 -33
- package/dist/tools/context.d.ts.map +1 -1
- package/dist/tools/context.js +441 -340
- package/dist/tools/context.js.map +1 -1
- package/dist/tools/files.d.ts +12 -9
- package/dist/tools/files.d.ts.map +1 -1
- package/dist/tools/files.js +173 -95
- 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 +14 -11
- package/dist/tools/messaging.d.ts.map +1 -1
- package/dist/tools/messaging.js +239 -133
- package/dist/tools/messaging.js.map +1 -1
- package/dist/tools/tasks.d.ts +18 -16
- package/dist/tools/tasks.d.ts.map +1 -1
- package/dist/tools/tasks.js +519 -442
- package/dist/tools/tasks.js.map +1 -1
- package/dist/tools/utils.d.ts +14 -11
- package/dist/tools/utils.d.ts.map +1 -1
- package/dist/tools/utils.js +90 -122
- package/dist/tools/utils.js.map +1 -1
- package/dist/types.d.ts +1 -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 +24 -14
- package/dist/utils/cleanup.d.ts.map +1 -1
- package/dist/utils/cleanup.js +37 -27
- package/dist/utils/cleanup.js.map +1 -1
- package/dist/utils/debug-logger.d.ts +99 -0
- package/dist/utils/debug-logger.d.ts.map +1 -0
- package/dist/utils/debug-logger.js +267 -0
- package/dist/utils/debug-logger.js.map +1 -0
- package/dist/utils/error-handler.d.ts +28 -0
- package/dist/utils/error-handler.d.ts.map +1 -0
- package/dist/utils/error-handler.js +121 -0
- package/dist/utils/error-handler.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/param-parser.d.ts +23 -0
- package/dist/utils/param-parser.d.ts.map +1 -0
- package/dist/utils/param-parser.js +52 -0
- package/dist/utils/param-parser.js.map +1 -0
- package/dist/utils/retention.d.ts +17 -7
- package/dist/utils/retention.d.ts.map +1 -1
- package/dist/utils/retention.js +31 -12
- package/dist/utils/retention.js.map +1 -1
- package/dist/utils/task-stale-detection.d.ts +15 -13
- package/dist/utils/task-stale-detection.d.ts.map +1 -1
- package/dist/utils/task-stale-detection.js +100 -302
- 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/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.map +1 -1
- package/dist/watcher/file-watcher.js +25 -11
- package/dist/watcher/file-watcher.js.map +1 -1
- package/docs/BEST_PRACTICES.md +56 -448
- package/docs/MIGRATION_v3.6.0.md +170 -0
- package/docs/SHARED_CONCEPTS.md +63 -208
- package/docs/TASK_OVERVIEW.md +2 -2
- package/docs/TOOL_SELECTION.md +41 -248
- package/package.json +17 -4
package/dist/tools/tasks.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
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 {
|
|
7
|
+
import { getAdapter, getOrCreateAgent, getOrCreateTag, getOrCreateContextKey, getLayerId, getOrCreateFile } from '../database.js';
|
|
6
8
|
import { detectAndTransitionStaleTasks, autoArchiveOldDoneTasks, detectAndCompleteReviewedTasks, detectAndArchiveOnCommit } from '../utils/task-stale-detection.js';
|
|
7
|
-
import { processBatch } from '../utils/batch.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';
|
|
12
|
+
import { parseStringArray } from '../utils/param-parser.js';
|
|
10
13
|
/**
|
|
11
14
|
* Task status enum (matches m_task_statuses)
|
|
12
15
|
*/
|
|
@@ -53,10 +56,12 @@ const VALID_TRANSITIONS = {
|
|
|
53
56
|
* Used by createTask (with transaction) and batchCreateTasks (manages its own transaction)
|
|
54
57
|
*
|
|
55
58
|
* @param params - Task parameters
|
|
56
|
-
* @param
|
|
59
|
+
* @param adapter - Database adapter instance
|
|
60
|
+
* @param trx - Optional transaction
|
|
57
61
|
* @returns Response with success status and task metadata
|
|
58
62
|
*/
|
|
59
|
-
function createTaskInternal(params,
|
|
63
|
+
async function createTaskInternal(params, adapter, trx) {
|
|
64
|
+
const knex = trx || adapter.getKnex();
|
|
60
65
|
// Validate priority
|
|
61
66
|
const priority = params.priority !== undefined ? params.priority : 2;
|
|
62
67
|
validatePriorityRange(priority);
|
|
@@ -69,7 +74,7 @@ function createTaskInternal(params, db) {
|
|
|
69
74
|
// Validate layer if provided
|
|
70
75
|
let layerId = null;
|
|
71
76
|
if (params.layer) {
|
|
72
|
-
layerId = getLayerId(
|
|
77
|
+
layerId = await getLayerId(adapter, params.layer, trx);
|
|
73
78
|
if (layerId === null) {
|
|
74
79
|
throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
|
|
75
80
|
}
|
|
@@ -77,19 +82,24 @@ function createTaskInternal(params, db) {
|
|
|
77
82
|
// Get or create agents
|
|
78
83
|
let assignedAgentId = null;
|
|
79
84
|
if (params.assigned_agent) {
|
|
80
|
-
assignedAgentId = getOrCreateAgent(
|
|
85
|
+
assignedAgentId = await getOrCreateAgent(adapter, params.assigned_agent, trx);
|
|
81
86
|
}
|
|
82
|
-
// Default to
|
|
83
|
-
//
|
|
84
|
-
const createdBy = params.created_by_agent || '
|
|
85
|
-
const createdByAgentId = getOrCreateAgent(
|
|
87
|
+
// Default to generic agent pool if no created_by_agent provided
|
|
88
|
+
// Empty string triggers allocation from generic-N pool
|
|
89
|
+
const createdBy = params.created_by_agent || '';
|
|
90
|
+
const createdByAgentId = await getOrCreateAgent(adapter, createdBy, trx);
|
|
86
91
|
// Insert task
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
const now = Math.floor(Date.now() / 1000);
|
|
93
|
+
const [taskId] = await knex('t_tasks').insert({
|
|
94
|
+
title: params.title,
|
|
95
|
+
status_id: statusId,
|
|
96
|
+
priority: priority,
|
|
97
|
+
assigned_agent_id: assignedAgentId,
|
|
98
|
+
created_by_agent_id: createdByAgentId,
|
|
99
|
+
layer_id: layerId,
|
|
100
|
+
created_ts: now,
|
|
101
|
+
updated_ts: now
|
|
102
|
+
});
|
|
93
103
|
// Process acceptance_criteria (can be string, JSON string, or array)
|
|
94
104
|
let acceptanceCriteriaString = null;
|
|
95
105
|
let acceptanceCriteriaJson = null;
|
|
@@ -127,23 +137,65 @@ function createTaskInternal(params, db) {
|
|
|
127
137
|
}
|
|
128
138
|
// Insert task details if provided
|
|
129
139
|
if (params.description || acceptanceCriteriaString || acceptanceCriteriaJson || params.notes) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
await knex('t_task_details').insert({
|
|
141
|
+
task_id: Number(taskId),
|
|
142
|
+
description: params.description || null,
|
|
143
|
+
acceptance_criteria: acceptanceCriteriaString,
|
|
144
|
+
acceptance_criteria_json: acceptanceCriteriaJson,
|
|
145
|
+
notes: params.notes || null
|
|
146
|
+
});
|
|
135
147
|
}
|
|
136
148
|
// Insert tags if provided
|
|
137
149
|
if (params.tags && params.tags.length > 0) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
150
|
+
// Parse tags - handle MCP SDK converting JSON string to char array
|
|
151
|
+
let tagsParsed;
|
|
152
|
+
if (typeof params.tags === 'string') {
|
|
153
|
+
// String - try to parse as JSON
|
|
154
|
+
try {
|
|
155
|
+
tagsParsed = JSON.parse(params.tags);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// If not valid JSON, treat as single tag name
|
|
159
|
+
tagsParsed = [params.tags];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else if (Array.isArray(params.tags)) {
|
|
163
|
+
// Check if it's an array of single characters (MCP SDK bug)
|
|
164
|
+
// Example: ['[', '"', 't', 'e', 's', 't', 'i', 'n', 'g', '"', ']']
|
|
165
|
+
if (params.tags.every((item) => typeof item === 'string' && item.length === 1)) {
|
|
166
|
+
// Join characters back into string and parse JSON
|
|
167
|
+
const jsonString = params.tags.join('');
|
|
168
|
+
try {
|
|
169
|
+
tagsParsed = JSON.parse(jsonString);
|
|
170
|
+
}
|
|
171
|
+
catch (e) {
|
|
172
|
+
const errMsg = e instanceof Error ? e.message : String(e);
|
|
173
|
+
throw new Error(`Invalid tags format: ${jsonString}. ${errMsg}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// Normal array of tag names
|
|
178
|
+
tagsParsed = params.tags;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
throw new Error('Parameter "tags" must be a string or array');
|
|
183
|
+
}
|
|
184
|
+
for (const tagName of tagsParsed) {
|
|
185
|
+
const tagId = await getOrCreateTag(adapter, tagName, trx);
|
|
186
|
+
await knex('t_task_tags').insert({
|
|
187
|
+
task_id: Number(taskId),
|
|
188
|
+
tag_id: tagId
|
|
189
|
+
}).onConflict(['task_id', 'tag_id']).ignore();
|
|
145
190
|
}
|
|
146
191
|
}
|
|
192
|
+
// Activity logging (replaces triggers)
|
|
193
|
+
await logTaskCreate(knex, {
|
|
194
|
+
task_id: Number(taskId),
|
|
195
|
+
title: params.title,
|
|
196
|
+
agent_id: createdByAgentId,
|
|
197
|
+
layer_id: layerId || undefined
|
|
198
|
+
});
|
|
147
199
|
// Link files and register with watcher if watch_files provided (v3.4.1)
|
|
148
200
|
if (params.watch_files && params.watch_files.length > 0) {
|
|
149
201
|
// Parse watch_files - handle MCP SDK converting JSON string to char array
|
|
@@ -180,19 +232,18 @@ function createTaskInternal(params, db) {
|
|
|
180
232
|
else {
|
|
181
233
|
throw new Error('Parameter "watch_files" must be a string or array');
|
|
182
234
|
}
|
|
183
|
-
const insertFileLinkStmt = db.prepare(`
|
|
184
|
-
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
185
|
-
VALUES (?, ?)
|
|
186
|
-
`);
|
|
187
235
|
for (const filePath of watchFilesParsed) {
|
|
188
|
-
const fileId = getOrCreateFile(
|
|
189
|
-
|
|
236
|
+
const fileId = await getOrCreateFile(adapter, filePath, trx);
|
|
237
|
+
await knex('t_task_file_links').insert({
|
|
238
|
+
task_id: Number(taskId),
|
|
239
|
+
file_id: fileId
|
|
240
|
+
}).onConflict(['task_id', 'file_id']).ignore();
|
|
190
241
|
}
|
|
191
242
|
// Register files with watcher for auto-tracking
|
|
192
243
|
try {
|
|
193
244
|
const watcher = FileWatcher.getInstance();
|
|
194
245
|
for (const filePath of watchFilesParsed) {
|
|
195
|
-
watcher.registerFile(filePath, taskId, params.title, status);
|
|
246
|
+
watcher.registerFile(filePath, Number(taskId), params.title, status);
|
|
196
247
|
}
|
|
197
248
|
}
|
|
198
249
|
catch (error) {
|
|
@@ -202,7 +253,7 @@ function createTaskInternal(params, db) {
|
|
|
202
253
|
}
|
|
203
254
|
return {
|
|
204
255
|
success: true,
|
|
205
|
-
task_id: taskId,
|
|
256
|
+
task_id: Number(taskId),
|
|
206
257
|
title: params.title,
|
|
207
258
|
status: status,
|
|
208
259
|
message: `Task "${params.title}" created successfully`
|
|
@@ -211,16 +262,16 @@ function createTaskInternal(params, db) {
|
|
|
211
262
|
/**
|
|
212
263
|
* Create a new task
|
|
213
264
|
*/
|
|
214
|
-
export function createTask(params,
|
|
215
|
-
const
|
|
265
|
+
export async function createTask(params, adapter) {
|
|
266
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
216
267
|
// Validate required parameters
|
|
217
268
|
if (!params.title || params.title.trim() === '') {
|
|
218
269
|
throw new Error('Parameter "title" is required and cannot be empty');
|
|
219
270
|
}
|
|
220
271
|
validateLength(params.title, 'Parameter "title"', 200);
|
|
221
272
|
try {
|
|
222
|
-
return transaction(
|
|
223
|
-
return createTaskInternal(params,
|
|
273
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
274
|
+
return await createTaskInternal(params, actualAdapter, trx);
|
|
224
275
|
});
|
|
225
276
|
}
|
|
226
277
|
catch (error) {
|
|
@@ -231,56 +282,50 @@ export function createTask(params, db) {
|
|
|
231
282
|
/**
|
|
232
283
|
* Update task metadata
|
|
233
284
|
*/
|
|
234
|
-
export function updateTask(params,
|
|
235
|
-
const
|
|
285
|
+
export async function updateTask(params, adapter) {
|
|
286
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
236
287
|
// Validate required parameters
|
|
237
288
|
if (!params.task_id) {
|
|
238
289
|
throw new Error('Parameter "task_id" is required');
|
|
239
290
|
}
|
|
240
291
|
try {
|
|
241
|
-
return transaction(
|
|
292
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
293
|
+
const knex = actualAdapter.getKnex();
|
|
242
294
|
// Check if task exists
|
|
243
|
-
const taskExists =
|
|
295
|
+
const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
|
|
244
296
|
if (!taskExists) {
|
|
245
297
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
246
298
|
}
|
|
247
|
-
// Build update
|
|
248
|
-
const
|
|
249
|
-
const updateParams = [];
|
|
299
|
+
// Build update data dynamically
|
|
300
|
+
const updateData = {};
|
|
250
301
|
if (params.title !== undefined) {
|
|
251
302
|
if (params.title.trim() === '') {
|
|
252
303
|
throw new Error('Parameter "title" cannot be empty');
|
|
253
304
|
}
|
|
254
305
|
validateLength(params.title, 'Parameter "title"', 200);
|
|
255
|
-
|
|
256
|
-
updateParams.push(params.title);
|
|
306
|
+
updateData.title = params.title;
|
|
257
307
|
}
|
|
258
308
|
if (params.priority !== undefined) {
|
|
259
309
|
validatePriorityRange(params.priority);
|
|
260
|
-
|
|
261
|
-
updateParams.push(params.priority);
|
|
310
|
+
updateData.priority = params.priority;
|
|
262
311
|
}
|
|
263
312
|
if (params.assigned_agent !== undefined) {
|
|
264
|
-
const agentId = getOrCreateAgent(
|
|
265
|
-
|
|
266
|
-
updateParams.push(agentId);
|
|
313
|
+
const agentId = await getOrCreateAgent(actualAdapter, params.assigned_agent, trx);
|
|
314
|
+
updateData.assigned_agent_id = agentId;
|
|
267
315
|
}
|
|
268
316
|
if (params.layer !== undefined) {
|
|
269
|
-
const layerId = getLayerId(
|
|
317
|
+
const layerId = await getLayerId(actualAdapter, params.layer, trx);
|
|
270
318
|
if (layerId === null) {
|
|
271
319
|
throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
|
|
272
320
|
}
|
|
273
|
-
|
|
274
|
-
updateParams.push(layerId);
|
|
321
|
+
updateData.layer_id = layerId;
|
|
275
322
|
}
|
|
276
323
|
// Update t_tasks if any updates
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
`);
|
|
283
|
-
updateStmt.run(...updateParams, params.task_id);
|
|
324
|
+
if (Object.keys(updateData).length > 0) {
|
|
325
|
+
await trx('t_tasks')
|
|
326
|
+
.where({ id: params.task_id })
|
|
327
|
+
.update(updateData);
|
|
328
|
+
// TODO: Add activity logging for updates if needed
|
|
284
329
|
}
|
|
285
330
|
// Update t_task_details if any detail fields provided
|
|
286
331
|
if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
|
|
@@ -322,43 +367,35 @@ export function updateTask(params, db) {
|
|
|
322
367
|
}
|
|
323
368
|
}
|
|
324
369
|
// Check if details exist
|
|
325
|
-
const detailsExist =
|
|
326
|
-
|
|
370
|
+
const detailsExist = await trx('t_task_details').where({ task_id: params.task_id }).first();
|
|
371
|
+
const detailsUpdate = {};
|
|
372
|
+
if (params.description !== undefined) {
|
|
373
|
+
detailsUpdate.description = params.description || null;
|
|
374
|
+
}
|
|
375
|
+
if (acceptanceCriteriaString !== undefined) {
|
|
376
|
+
detailsUpdate.acceptance_criteria = acceptanceCriteriaString;
|
|
377
|
+
}
|
|
378
|
+
if (acceptanceCriteriaJson !== undefined) {
|
|
379
|
+
detailsUpdate.acceptance_criteria_json = acceptanceCriteriaJson;
|
|
380
|
+
}
|
|
381
|
+
if (params.notes !== undefined) {
|
|
382
|
+
detailsUpdate.notes = params.notes || null;
|
|
383
|
+
}
|
|
384
|
+
if (detailsExist && Object.keys(detailsUpdate).length > 0) {
|
|
327
385
|
// Update existing details
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
detailUpdates.push('description = ?');
|
|
332
|
-
detailParams.push(params.description || null);
|
|
333
|
-
}
|
|
334
|
-
if (acceptanceCriteriaString !== undefined) {
|
|
335
|
-
detailUpdates.push('acceptance_criteria = ?');
|
|
336
|
-
detailParams.push(acceptanceCriteriaString);
|
|
337
|
-
}
|
|
338
|
-
if (acceptanceCriteriaJson !== undefined) {
|
|
339
|
-
detailUpdates.push('acceptance_criteria_json = ?');
|
|
340
|
-
detailParams.push(acceptanceCriteriaJson);
|
|
341
|
-
}
|
|
342
|
-
if (params.notes !== undefined) {
|
|
343
|
-
detailUpdates.push('notes = ?');
|
|
344
|
-
detailParams.push(params.notes || null);
|
|
345
|
-
}
|
|
346
|
-
if (detailUpdates.length > 0) {
|
|
347
|
-
const updateDetailsStmt = actualDb.prepare(`
|
|
348
|
-
UPDATE t_task_details
|
|
349
|
-
SET ${detailUpdates.join(', ')}
|
|
350
|
-
WHERE task_id = ?
|
|
351
|
-
`);
|
|
352
|
-
updateDetailsStmt.run(...detailParams, params.task_id);
|
|
353
|
-
}
|
|
386
|
+
await trx('t_task_details')
|
|
387
|
+
.where({ task_id: params.task_id })
|
|
388
|
+
.update(detailsUpdate);
|
|
354
389
|
}
|
|
355
|
-
else {
|
|
390
|
+
else if (!detailsExist) {
|
|
356
391
|
// Insert new details
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
392
|
+
await trx('t_task_details').insert({
|
|
393
|
+
task_id: params.task_id,
|
|
394
|
+
description: params.description || null,
|
|
395
|
+
acceptance_criteria: acceptanceCriteriaString !== undefined ? acceptanceCriteriaString : null,
|
|
396
|
+
acceptance_criteria_json: acceptanceCriteriaJson !== undefined ? acceptanceCriteriaJson : null,
|
|
397
|
+
notes: params.notes || null
|
|
398
|
+
});
|
|
362
399
|
}
|
|
363
400
|
}
|
|
364
401
|
// Handle watch_files if provided (v3.4.1)
|
|
@@ -395,22 +432,20 @@ export function updateTask(params, db) {
|
|
|
395
432
|
else {
|
|
396
433
|
throw new Error('Parameter "watch_files" must be a string or array');
|
|
397
434
|
}
|
|
398
|
-
const insertFileLinkStmt = actualDb.prepare(`
|
|
399
|
-
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
400
|
-
VALUES (?, ?)
|
|
401
|
-
`);
|
|
402
435
|
for (const filePath of watchFilesParsed) {
|
|
403
|
-
const fileId = getOrCreateFile(
|
|
404
|
-
|
|
436
|
+
const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
|
|
437
|
+
await trx('t_task_file_links').insert({
|
|
438
|
+
task_id: params.task_id,
|
|
439
|
+
file_id: fileId
|
|
440
|
+
}).onConflict(['task_id', 'file_id']).ignore();
|
|
405
441
|
}
|
|
406
442
|
// Register files with watcher for auto-tracking
|
|
407
443
|
try {
|
|
408
|
-
const taskData =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
`).get(params.task_id);
|
|
444
|
+
const taskData = await trx('t_tasks as t')
|
|
445
|
+
.join('m_task_statuses as s', 't.status_id', 's.id')
|
|
446
|
+
.where('t.id', params.task_id)
|
|
447
|
+
.select('t.title', 's.name as status')
|
|
448
|
+
.first();
|
|
414
449
|
if (taskData) {
|
|
415
450
|
const watcher = FileWatcher.getInstance();
|
|
416
451
|
for (const filePath of watchFilesParsed) {
|
|
@@ -438,89 +473,70 @@ export function updateTask(params, db) {
|
|
|
438
473
|
/**
|
|
439
474
|
* Internal helper: Query task dependencies (used by getTask and getDependencies)
|
|
440
475
|
*/
|
|
441
|
-
function queryTaskDependencies(
|
|
476
|
+
async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
|
|
477
|
+
const knex = adapter.getKnex();
|
|
442
478
|
// Build query based on include_details flag
|
|
443
|
-
|
|
479
|
+
const selectFields = includeDetails
|
|
480
|
+
? [
|
|
481
|
+
't.id',
|
|
482
|
+
't.title',
|
|
483
|
+
's.name as status',
|
|
484
|
+
't.priority',
|
|
485
|
+
'aa.name as assigned_to',
|
|
486
|
+
't.created_ts',
|
|
487
|
+
't.updated_ts',
|
|
488
|
+
'td.description'
|
|
489
|
+
]
|
|
490
|
+
: [
|
|
491
|
+
't.id',
|
|
492
|
+
't.title',
|
|
493
|
+
's.name as status',
|
|
494
|
+
't.priority'
|
|
495
|
+
];
|
|
496
|
+
// Get blockers (tasks that block this task)
|
|
497
|
+
let blockersQuery = knex('t_tasks as t')
|
|
498
|
+
.join('t_task_dependencies as d', 't.id', 'd.blocker_task_id')
|
|
499
|
+
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
500
|
+
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
501
|
+
.where('d.blocked_task_id', taskId)
|
|
502
|
+
.select(selectFields);
|
|
444
503
|
if (includeDetails) {
|
|
445
|
-
|
|
446
|
-
selectFields = `
|
|
447
|
-
t.id,
|
|
448
|
-
t.title,
|
|
449
|
-
s.name as status,
|
|
450
|
-
t.priority,
|
|
451
|
-
aa.name as assigned_to,
|
|
452
|
-
t.created_ts,
|
|
453
|
-
t.updated_ts,
|
|
454
|
-
td.description
|
|
455
|
-
`;
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
// Metadata only (token-efficient)
|
|
459
|
-
selectFields = `
|
|
460
|
-
t.id,
|
|
461
|
-
t.title,
|
|
462
|
-
s.name as status,
|
|
463
|
-
t.priority
|
|
464
|
-
`;
|
|
504
|
+
blockersQuery = blockersQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
|
|
465
505
|
}
|
|
466
|
-
|
|
467
|
-
const blockersQuery = `
|
|
468
|
-
SELECT ${selectFields}
|
|
469
|
-
FROM t_tasks t
|
|
470
|
-
JOIN t_task_dependencies d ON t.id = d.blocker_task_id
|
|
471
|
-
LEFT JOIN m_task_statuses s ON t.status_id = s.id
|
|
472
|
-
LEFT JOIN m_agents aa ON t.assigned_agent_id = aa.id
|
|
473
|
-
${includeDetails ? 'LEFT JOIN t_task_details td ON t.id = td.task_id' : ''}
|
|
474
|
-
WHERE d.blocked_task_id = ?
|
|
475
|
-
`;
|
|
476
|
-
const blockers = db.prepare(blockersQuery).all(taskId);
|
|
506
|
+
const blockers = await blockersQuery;
|
|
477
507
|
// Get blocking (tasks this task blocks)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
const blocking =
|
|
508
|
+
let blockingQuery = knex('t_tasks as t')
|
|
509
|
+
.join('t_task_dependencies as d', 't.id', 'd.blocked_task_id')
|
|
510
|
+
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
511
|
+
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
512
|
+
.where('d.blocker_task_id', taskId)
|
|
513
|
+
.select(selectFields);
|
|
514
|
+
if (includeDetails) {
|
|
515
|
+
blockingQuery = blockingQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
|
|
516
|
+
}
|
|
517
|
+
const blocking = await blockingQuery;
|
|
488
518
|
return { blockers, blocking };
|
|
489
519
|
}
|
|
490
520
|
/**
|
|
491
521
|
* Get full task details
|
|
492
522
|
*/
|
|
493
|
-
export function getTask(params,
|
|
494
|
-
const
|
|
523
|
+
export async function getTask(params, adapter) {
|
|
524
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
525
|
+
const knex = actualAdapter.getKnex();
|
|
495
526
|
if (!params.task_id) {
|
|
496
527
|
throw new Error('Parameter "task_id" is required');
|
|
497
528
|
}
|
|
498
529
|
try {
|
|
499
530
|
// Get task with details
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
t.created_ts,
|
|
510
|
-
t.updated_ts,
|
|
511
|
-
t.completed_ts,
|
|
512
|
-
td.description,
|
|
513
|
-
td.acceptance_criteria,
|
|
514
|
-
td.notes
|
|
515
|
-
FROM t_tasks t
|
|
516
|
-
LEFT JOIN m_task_statuses s ON t.status_id = s.id
|
|
517
|
-
LEFT JOIN m_agents aa ON t.assigned_agent_id = aa.id
|
|
518
|
-
LEFT JOIN m_agents ca ON t.created_by_agent_id = ca.id
|
|
519
|
-
LEFT JOIN m_layers l ON t.layer_id = l.id
|
|
520
|
-
LEFT JOIN t_task_details td ON t.id = td.task_id
|
|
521
|
-
WHERE t.id = ?
|
|
522
|
-
`);
|
|
523
|
-
const task = stmt.get(params.task_id);
|
|
531
|
+
const task = await knex('t_tasks as t')
|
|
532
|
+
.leftJoin('m_task_statuses as s', 't.status_id', 's.id')
|
|
533
|
+
.leftJoin('m_agents as aa', 't.assigned_agent_id', 'aa.id')
|
|
534
|
+
.leftJoin('m_agents as ca', 't.created_by_agent_id', 'ca.id')
|
|
535
|
+
.leftJoin('m_layers as l', 't.layer_id', 'l.id')
|
|
536
|
+
.leftJoin('t_task_details as td', 't.id', 'td.task_id')
|
|
537
|
+
.where('t.id', params.task_id)
|
|
538
|
+
.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')
|
|
539
|
+
.first();
|
|
524
540
|
if (!task) {
|
|
525
541
|
return {
|
|
526
542
|
found: false,
|
|
@@ -528,37 +544,27 @@ export function getTask(params, db) {
|
|
|
528
544
|
};
|
|
529
545
|
}
|
|
530
546
|
// Get tags
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
`);
|
|
537
|
-
const tags = tagsStmt.all(params.task_id).map((row) => row.name);
|
|
547
|
+
const tags = await knex('t_task_tags as tt')
|
|
548
|
+
.join('m_tags as tg', 'tt.tag_id', 'tg.id')
|
|
549
|
+
.where('tt.task_id', params.task_id)
|
|
550
|
+
.select('tg.name')
|
|
551
|
+
.then(rows => rows.map((row) => row.name));
|
|
538
552
|
// Get decision links
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
WHERE tdl.task_id = ?
|
|
544
|
-
`);
|
|
545
|
-
const decisions = decisionsStmt.all(params.task_id);
|
|
553
|
+
const decisions = await knex('t_task_decision_links as tdl')
|
|
554
|
+
.join('m_context_keys as ck', 'tdl.decision_key_id', 'ck.id')
|
|
555
|
+
.where('tdl.task_id', params.task_id)
|
|
556
|
+
.select('ck.key', 'tdl.link_type');
|
|
546
557
|
// Get constraint links
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
WHERE tcl.task_id = ?
|
|
552
|
-
`);
|
|
553
|
-
const constraints = constraintsStmt.all(params.task_id);
|
|
558
|
+
const constraints = await knex('t_task_constraint_links as tcl')
|
|
559
|
+
.join('t_constraints as c', 'tcl.constraint_id', 'c.id')
|
|
560
|
+
.where('tcl.task_id', params.task_id)
|
|
561
|
+
.select('c.id', 'c.constraint_text');
|
|
554
562
|
// Get file links
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
`);
|
|
561
|
-
const files = filesStmt.all(params.task_id).map((row) => row.path);
|
|
563
|
+
const files = await knex('t_task_file_links as tfl')
|
|
564
|
+
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
565
|
+
.where('tfl.task_id', params.task_id)
|
|
566
|
+
.select('f.path')
|
|
567
|
+
.then(rows => rows.map((row) => row.path));
|
|
562
568
|
// Build result
|
|
563
569
|
const result = {
|
|
564
570
|
found: true,
|
|
@@ -572,7 +578,7 @@ export function getTask(params, db) {
|
|
|
572
578
|
};
|
|
573
579
|
// Include dependencies if requested (token-efficient, metadata-only)
|
|
574
580
|
if (params.include_dependencies) {
|
|
575
|
-
const deps = queryTaskDependencies(
|
|
581
|
+
const deps = await queryTaskDependencies(actualAdapter, params.task_id, false);
|
|
576
582
|
result.task.dependencies = {
|
|
577
583
|
blockers: deps.blockers,
|
|
578
584
|
blocking: deps.blocking
|
|
@@ -588,79 +594,71 @@ export function getTask(params, db) {
|
|
|
588
594
|
/**
|
|
589
595
|
* List tasks (token-efficient, no descriptions)
|
|
590
596
|
*/
|
|
591
|
-
export async function listTasks(params = {},
|
|
592
|
-
const
|
|
597
|
+
export async function listTasks(params = {}, adapter) {
|
|
598
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
599
|
+
const knex = actualAdapter.getKnex();
|
|
593
600
|
try {
|
|
594
601
|
// Run auto-stale detection, git-aware completion, and auto-archive before listing
|
|
595
|
-
const transitionCount = detectAndTransitionStaleTasks(
|
|
596
|
-
const gitCompletedCount = await detectAndCompleteReviewedTasks(
|
|
597
|
-
const gitArchivedCount = await detectAndArchiveOnCommit(
|
|
598
|
-
const archiveCount = autoArchiveOldDoneTasks(
|
|
602
|
+
const transitionCount = await detectAndTransitionStaleTasks(actualAdapter);
|
|
603
|
+
const gitCompletedCount = await detectAndCompleteReviewedTasks(actualAdapter);
|
|
604
|
+
const gitArchivedCount = await detectAndArchiveOnCommit(actualAdapter);
|
|
605
|
+
const archiveCount = await autoArchiveOldDoneTasks(actualAdapter);
|
|
599
606
|
// Build query with optional dependency counts
|
|
600
607
|
let query;
|
|
601
608
|
if (params.include_dependency_counts) {
|
|
602
609
|
// Include dependency counts with LEFT JOINs
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
GROUP BY blocker_task_id
|
|
618
|
-
) blocking ON vt.id = blocking.blocker_task_id
|
|
619
|
-
WHERE 1=1
|
|
620
|
-
`;
|
|
610
|
+
const blockersCTE = knex('t_task_dependencies')
|
|
611
|
+
.select('blocked_task_id')
|
|
612
|
+
.count('* as blocked_by_count')
|
|
613
|
+
.groupBy('blocked_task_id')
|
|
614
|
+
.as('blockers');
|
|
615
|
+
const blockingCTE = knex('t_task_dependencies')
|
|
616
|
+
.select('blocker_task_id')
|
|
617
|
+
.count('* as blocking_count')
|
|
618
|
+
.groupBy('blocker_task_id')
|
|
619
|
+
.as('blocking');
|
|
620
|
+
query = knex('v_task_board as vt')
|
|
621
|
+
.leftJoin(blockersCTE, 'vt.id', 'blockers.blocked_task_id')
|
|
622
|
+
.leftJoin(blockingCTE, 'vt.id', 'blocking.blocker_task_id')
|
|
623
|
+
.select('vt.*', knex.raw('COALESCE(blockers.blocked_by_count, 0) as blocked_by_count'), knex.raw('COALESCE(blocking.blocking_count, 0) as blocking_count'));
|
|
621
624
|
}
|
|
622
625
|
else {
|
|
623
626
|
// Standard query without dependency counts
|
|
624
|
-
query = '
|
|
627
|
+
query = knex('v_task_board');
|
|
625
628
|
}
|
|
626
|
-
const queryParams = [];
|
|
627
629
|
// Filter by status
|
|
628
630
|
if (params.status) {
|
|
629
631
|
if (!STATUS_TO_ID[params.status]) {
|
|
630
632
|
throw new Error(`Invalid status: ${params.status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
|
|
631
633
|
}
|
|
632
|
-
query
|
|
633
|
-
queryParams.push(params.status);
|
|
634
|
+
query = query.where(params.include_dependency_counts ? 'vt.status' : 'status', params.status);
|
|
634
635
|
}
|
|
635
636
|
// Filter by assigned agent
|
|
636
637
|
if (params.assigned_agent) {
|
|
637
|
-
query
|
|
638
|
-
queryParams.push(params.assigned_agent);
|
|
638
|
+
query = query.where(params.include_dependency_counts ? 'vt.assigned_to' : 'assigned_to', params.assigned_agent);
|
|
639
639
|
}
|
|
640
640
|
// Filter by layer
|
|
641
641
|
if (params.layer) {
|
|
642
|
-
query
|
|
643
|
-
queryParams.push(params.layer);
|
|
642
|
+
query = query.where(params.include_dependency_counts ? 'vt.layer' : 'layer', params.layer);
|
|
644
643
|
}
|
|
645
644
|
// Filter by tags
|
|
646
645
|
if (params.tags && params.tags.length > 0) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
646
|
+
// Parse tags (handles both arrays and JSON strings from MCP)
|
|
647
|
+
const tags = parseStringArray(params.tags);
|
|
648
|
+
for (const tag of tags) {
|
|
649
|
+
query = query.where(params.include_dependency_counts ? 'vt.tags' : 'tags', 'like', `%${tag}%`);
|
|
650
650
|
}
|
|
651
651
|
}
|
|
652
652
|
// Order by updated timestamp (most recent first)
|
|
653
|
-
query
|
|
653
|
+
query = query.orderBy(params.include_dependency_counts ? 'vt.updated_ts' : 'updated_ts', 'desc');
|
|
654
654
|
// Pagination
|
|
655
655
|
const limit = params.limit !== undefined ? params.limit : 50;
|
|
656
656
|
const offset = params.offset || 0;
|
|
657
657
|
validateRange(limit, 'Parameter "limit"', 0, 100);
|
|
658
658
|
validateRange(offset, 'Parameter "offset"', 0, Number.MAX_SAFE_INTEGER);
|
|
659
|
-
query
|
|
660
|
-
queryParams.push(limit, offset);
|
|
659
|
+
query = query.limit(limit).offset(offset);
|
|
661
660
|
// Execute query
|
|
662
|
-
const
|
|
663
|
-
const rows = stmt.all(...queryParams);
|
|
661
|
+
const rows = await query;
|
|
664
662
|
return {
|
|
665
663
|
tasks: rows,
|
|
666
664
|
count: rows.length,
|
|
@@ -678,8 +676,9 @@ export async function listTasks(params = {}, db) {
|
|
|
678
676
|
/**
|
|
679
677
|
* Move task to different status
|
|
680
678
|
*/
|
|
681
|
-
export function moveTask(params,
|
|
682
|
-
const
|
|
679
|
+
export async function moveTask(params, adapter) {
|
|
680
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
681
|
+
const knex = actualAdapter.getKnex();
|
|
683
682
|
if (!params.task_id) {
|
|
684
683
|
throw new Error('Parameter "task_id" is required');
|
|
685
684
|
}
|
|
@@ -688,11 +687,14 @@ export function moveTask(params, db) {
|
|
|
688
687
|
}
|
|
689
688
|
try {
|
|
690
689
|
// Run auto-stale detection and auto-archive before move
|
|
691
|
-
detectAndTransitionStaleTasks(
|
|
692
|
-
autoArchiveOldDoneTasks(
|
|
693
|
-
return transaction(
|
|
690
|
+
await detectAndTransitionStaleTasks(actualAdapter);
|
|
691
|
+
await autoArchiveOldDoneTasks(actualAdapter);
|
|
692
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
694
693
|
// Get current status
|
|
695
|
-
const taskRow =
|
|
694
|
+
const taskRow = await trx('t_tasks')
|
|
695
|
+
.where({ id: params.task_id })
|
|
696
|
+
.select('status_id')
|
|
697
|
+
.first();
|
|
696
698
|
if (!taskRow) {
|
|
697
699
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
698
700
|
}
|
|
@@ -708,13 +710,26 @@ export function moveTask(params, db) {
|
|
|
708
710
|
`Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
|
|
709
711
|
}
|
|
710
712
|
// Update status
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
713
|
+
const updateData = {
|
|
714
|
+
status_id: newStatusId
|
|
715
|
+
};
|
|
716
|
+
// Set completed_ts when moving to done
|
|
717
|
+
if (newStatusId === TASK_STATUS.DONE) {
|
|
718
|
+
updateData.completed_ts = Math.floor(Date.now() / 1000);
|
|
719
|
+
}
|
|
720
|
+
await trx('t_tasks')
|
|
721
|
+
.where({ id: params.task_id })
|
|
722
|
+
.update(updateData);
|
|
723
|
+
// Activity logging (replaces trigger)
|
|
724
|
+
// Note: Using system agent (id=1) for status changes
|
|
725
|
+
// In a real implementation, you'd pass the actual agent_id who made the change
|
|
726
|
+
const systemAgentId = 1;
|
|
727
|
+
await logTaskStatusChange(knex, {
|
|
728
|
+
task_id: params.task_id,
|
|
729
|
+
old_status: currentStatusId,
|
|
730
|
+
new_status: newStatusId,
|
|
731
|
+
agent_id: systemAgentId
|
|
732
|
+
});
|
|
718
733
|
// Update watcher if moving to done or archived (stop watching)
|
|
719
734
|
if (params.new_status === 'done' || params.new_status === 'archived') {
|
|
720
735
|
try {
|
|
@@ -742,8 +757,9 @@ export function moveTask(params, db) {
|
|
|
742
757
|
/**
|
|
743
758
|
* Link task to decision/constraint/file
|
|
744
759
|
*/
|
|
745
|
-
export function linkTask(params,
|
|
746
|
-
const
|
|
760
|
+
export async function linkTask(params, adapter) {
|
|
761
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
762
|
+
const knex = actualAdapter.getKnex();
|
|
747
763
|
if (!params.task_id) {
|
|
748
764
|
throw new Error('Parameter "task_id" is required');
|
|
749
765
|
}
|
|
@@ -754,21 +770,21 @@ export function linkTask(params, db) {
|
|
|
754
770
|
throw new Error('Parameter "target_id" is required');
|
|
755
771
|
}
|
|
756
772
|
try {
|
|
757
|
-
return transaction(
|
|
773
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
758
774
|
// Check if task exists
|
|
759
|
-
const taskExists =
|
|
775
|
+
const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
|
|
760
776
|
if (!taskExists) {
|
|
761
777
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
762
778
|
}
|
|
763
779
|
if (params.link_type === 'decision') {
|
|
764
780
|
const decisionKey = String(params.target_id);
|
|
765
|
-
const keyId = getOrCreateContextKey(
|
|
781
|
+
const keyId = await getOrCreateContextKey(actualAdapter, decisionKey, trx);
|
|
766
782
|
const linkRelation = params.link_relation || 'implements';
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
783
|
+
await knex('t_task_decision_links').insert({
|
|
784
|
+
task_id: params.task_id,
|
|
785
|
+
decision_key_id: keyId,
|
|
786
|
+
link_type: linkRelation
|
|
787
|
+
}).onConflict(['task_id', 'decision_key_id']).merge();
|
|
772
788
|
return {
|
|
773
789
|
success: true,
|
|
774
790
|
task_id: params.task_id,
|
|
@@ -781,15 +797,14 @@ export function linkTask(params, db) {
|
|
|
781
797
|
else if (params.link_type === 'constraint') {
|
|
782
798
|
const constraintId = Number(params.target_id);
|
|
783
799
|
// Check if constraint exists
|
|
784
|
-
const constraintExists =
|
|
800
|
+
const constraintExists = await trx('t_constraints').where({ id: constraintId }).first();
|
|
785
801
|
if (!constraintExists) {
|
|
786
802
|
throw new Error(`Constraint with id ${constraintId} not found`);
|
|
787
803
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
stmt.run(params.task_id, constraintId);
|
|
804
|
+
await trx('t_task_constraint_links').insert({
|
|
805
|
+
task_id: params.task_id,
|
|
806
|
+
constraint_id: constraintId
|
|
807
|
+
}).onConflict(['task_id', 'constraint_id']).ignore();
|
|
793
808
|
return {
|
|
794
809
|
success: true,
|
|
795
810
|
task_id: params.task_id,
|
|
@@ -804,20 +819,18 @@ export function linkTask(params, db) {
|
|
|
804
819
|
console.warn(` Use task.create(watch_files=[...]) or task.update(watch_files=[...]) instead.`);
|
|
805
820
|
console.warn(` Or use the new watch_files action: { action: "watch_files", task_id: ${params.task_id}, file_paths: ["..."] }`);
|
|
806
821
|
const filePath = String(params.target_id);
|
|
807
|
-
const fileId = getOrCreateFile(
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
stmt.run(params.task_id, fileId);
|
|
822
|
+
const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
|
|
823
|
+
await trx('t_task_file_links').insert({
|
|
824
|
+
task_id: params.task_id,
|
|
825
|
+
file_id: fileId
|
|
826
|
+
}).onConflict(['task_id', 'file_id']).ignore();
|
|
813
827
|
// Register file with watcher for auto-tracking
|
|
814
828
|
try {
|
|
815
|
-
const taskData =
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
`).get(params.task_id);
|
|
829
|
+
const taskData = await trx('t_tasks as t')
|
|
830
|
+
.join('m_task_statuses as s', 't.status_id', 's.id')
|
|
831
|
+
.where('t.id', params.task_id)
|
|
832
|
+
.select('t.title', 's.name as status')
|
|
833
|
+
.first();
|
|
821
834
|
if (taskData) {
|
|
822
835
|
const watcher = FileWatcher.getInstance();
|
|
823
836
|
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
@@ -849,15 +862,19 @@ export function linkTask(params, db) {
|
|
|
849
862
|
/**
|
|
850
863
|
* Archive completed task
|
|
851
864
|
*/
|
|
852
|
-
export function archiveTask(params,
|
|
853
|
-
const
|
|
865
|
+
export async function archiveTask(params, adapter) {
|
|
866
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
867
|
+
const knex = actualAdapter.getKnex();
|
|
854
868
|
if (!params.task_id) {
|
|
855
869
|
throw new Error('Parameter "task_id" is required');
|
|
856
870
|
}
|
|
857
871
|
try {
|
|
858
|
-
return transaction(
|
|
872
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
859
873
|
// Check if task is in 'done' status
|
|
860
|
-
const taskRow =
|
|
874
|
+
const taskRow = await trx('t_tasks')
|
|
875
|
+
.where({ id: params.task_id })
|
|
876
|
+
.select('status_id')
|
|
877
|
+
.first();
|
|
861
878
|
if (!taskRow) {
|
|
862
879
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
863
880
|
}
|
|
@@ -865,8 +882,18 @@ export function archiveTask(params, db) {
|
|
|
865
882
|
throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
|
|
866
883
|
}
|
|
867
884
|
// Update to archived
|
|
868
|
-
|
|
869
|
-
|
|
885
|
+
await trx('t_tasks')
|
|
886
|
+
.where({ id: params.task_id })
|
|
887
|
+
.update({ status_id: TASK_STATUS.ARCHIVED });
|
|
888
|
+
// Activity logging
|
|
889
|
+
// Note: Using system agent (id=1) for status changes
|
|
890
|
+
const systemAgentId = 1;
|
|
891
|
+
await logTaskStatusChange(trx, {
|
|
892
|
+
task_id: params.task_id,
|
|
893
|
+
old_status: TASK_STATUS.DONE,
|
|
894
|
+
new_status: TASK_STATUS.ARCHIVED,
|
|
895
|
+
agent_id: systemAgentId
|
|
896
|
+
});
|
|
870
897
|
// Unregister from file watcher (archived tasks don't need tracking)
|
|
871
898
|
try {
|
|
872
899
|
const watcher = FileWatcher.getInstance();
|
|
@@ -890,8 +917,9 @@ export function archiveTask(params, db) {
|
|
|
890
917
|
/**
|
|
891
918
|
* Add dependency (blocking relationship) between tasks
|
|
892
919
|
*/
|
|
893
|
-
export function addDependency(params,
|
|
894
|
-
const
|
|
920
|
+
export async function addDependency(params, adapter) {
|
|
921
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
922
|
+
const knex = actualAdapter.getKnex();
|
|
895
923
|
if (!params.blocker_task_id) {
|
|
896
924
|
throw new Error('Parameter "blocker_task_id" is required');
|
|
897
925
|
}
|
|
@@ -899,14 +927,20 @@ export function addDependency(params, db) {
|
|
|
899
927
|
throw new Error('Parameter "blocked_task_id" is required');
|
|
900
928
|
}
|
|
901
929
|
try {
|
|
902
|
-
return transaction(
|
|
930
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
903
931
|
// Validation 1: No self-dependencies
|
|
904
932
|
if (params.blocker_task_id === params.blocked_task_id) {
|
|
905
933
|
throw new Error('Self-dependency not allowed');
|
|
906
934
|
}
|
|
907
935
|
// Validation 2: Both tasks must exist and check if archived
|
|
908
|
-
const blockerTask =
|
|
909
|
-
|
|
936
|
+
const blockerTask = await trx('t_tasks')
|
|
937
|
+
.where({ id: params.blocker_task_id })
|
|
938
|
+
.select('id', 'status_id')
|
|
939
|
+
.first();
|
|
940
|
+
const blockedTask = await trx('t_tasks')
|
|
941
|
+
.where({ id: params.blocked_task_id })
|
|
942
|
+
.select('id', 'status_id')
|
|
943
|
+
.first();
|
|
910
944
|
if (!blockerTask) {
|
|
911
945
|
throw new Error(`Blocker task #${params.blocker_task_id} not found`);
|
|
912
946
|
}
|
|
@@ -921,15 +955,17 @@ export function addDependency(params, db) {
|
|
|
921
955
|
throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
|
|
922
956
|
}
|
|
923
957
|
// Validation 4: No direct circular (reverse relationship)
|
|
924
|
-
const reverseExists =
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
958
|
+
const reverseExists = await trx('t_task_dependencies')
|
|
959
|
+
.where({
|
|
960
|
+
blocker_task_id: params.blocked_task_id,
|
|
961
|
+
blocked_task_id: params.blocker_task_id
|
|
962
|
+
})
|
|
963
|
+
.first();
|
|
928
964
|
if (reverseExists) {
|
|
929
965
|
throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
|
|
930
966
|
}
|
|
931
967
|
// Validation 5: No transitive circular (check if adding this would create a cycle)
|
|
932
|
-
const cycleCheck =
|
|
968
|
+
const cycleCheck = await trx.raw(`
|
|
933
969
|
WITH RECURSIVE dependency_chain AS (
|
|
934
970
|
-- Start from the task that would be blocked
|
|
935
971
|
SELECT blocked_task_id as task_id, 1 as depth
|
|
@@ -945,10 +981,11 @@ export function addDependency(params, db) {
|
|
|
945
981
|
WHERE dc.depth < 100
|
|
946
982
|
)
|
|
947
983
|
SELECT task_id FROM dependency_chain WHERE task_id = ?
|
|
948
|
-
|
|
984
|
+
`, [params.blocked_task_id, params.blocker_task_id])
|
|
985
|
+
.then((result) => result[0]);
|
|
949
986
|
if (cycleCheck) {
|
|
950
987
|
// Build cycle path for error message
|
|
951
|
-
const cyclePathResult =
|
|
988
|
+
const cyclePathResult = await trx.raw(`
|
|
952
989
|
WITH RECURSIVE dependency_chain AS (
|
|
953
990
|
SELECT blocked_task_id as task_id, 1 as depth,
|
|
954
991
|
CAST(blocked_task_id AS TEXT) as path
|
|
@@ -964,16 +1001,17 @@ export function addDependency(params, db) {
|
|
|
964
1001
|
WHERE dc.depth < 100
|
|
965
1002
|
)
|
|
966
1003
|
SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
|
|
967
|
-
|
|
1004
|
+
`, [params.blocked_task_id, params.blocker_task_id])
|
|
1005
|
+
.then((result) => result[0]);
|
|
968
1006
|
const cyclePath = cyclePathResult?.path || `#${params.blocked_task_id} → ... → #${params.blocker_task_id}`;
|
|
969
1007
|
throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
|
|
970
1008
|
}
|
|
971
1009
|
// All validations passed - insert dependency
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1010
|
+
await trx('t_task_dependencies').insert({
|
|
1011
|
+
blocker_task_id: params.blocker_task_id,
|
|
1012
|
+
blocked_task_id: params.blocked_task_id,
|
|
1013
|
+
created_ts: Math.floor(Date.now() / 1000)
|
|
1014
|
+
});
|
|
977
1015
|
return {
|
|
978
1016
|
success: true,
|
|
979
1017
|
message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
|
|
@@ -992,8 +1030,9 @@ export function addDependency(params, db) {
|
|
|
992
1030
|
/**
|
|
993
1031
|
* Remove dependency between tasks
|
|
994
1032
|
*/
|
|
995
|
-
export function removeDependency(params,
|
|
996
|
-
const
|
|
1033
|
+
export async function removeDependency(params, adapter) {
|
|
1034
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1035
|
+
const knex = actualAdapter.getKnex();
|
|
997
1036
|
if (!params.blocker_task_id) {
|
|
998
1037
|
throw new Error('Parameter "blocker_task_id" is required');
|
|
999
1038
|
}
|
|
@@ -1001,11 +1040,12 @@ export function removeDependency(params, db) {
|
|
|
1001
1040
|
throw new Error('Parameter "blocked_task_id" is required');
|
|
1002
1041
|
}
|
|
1003
1042
|
try {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1043
|
+
await knex('t_task_dependencies')
|
|
1044
|
+
.where({
|
|
1045
|
+
blocker_task_id: params.blocker_task_id,
|
|
1046
|
+
blocked_task_id: params.blocked_task_id
|
|
1047
|
+
})
|
|
1048
|
+
.delete();
|
|
1009
1049
|
return {
|
|
1010
1050
|
success: true,
|
|
1011
1051
|
message: `Dependency removed: Task #${params.blocker_task_id} no longer blocks Task #${params.blocked_task_id}`
|
|
@@ -1019,20 +1059,21 @@ export function removeDependency(params, db) {
|
|
|
1019
1059
|
/**
|
|
1020
1060
|
* Get dependencies for a task (bidirectional: what blocks this task, what this task blocks)
|
|
1021
1061
|
*/
|
|
1022
|
-
export function getDependencies(params,
|
|
1023
|
-
const
|
|
1062
|
+
export async function getDependencies(params, adapter) {
|
|
1063
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1064
|
+
const knex = actualAdapter.getKnex();
|
|
1024
1065
|
if (!params.task_id) {
|
|
1025
1066
|
throw new Error('Parameter "task_id" is required');
|
|
1026
1067
|
}
|
|
1027
1068
|
const includeDetails = params.include_details || false;
|
|
1028
1069
|
try {
|
|
1029
1070
|
// Check if task exists
|
|
1030
|
-
const taskExists =
|
|
1071
|
+
const taskExists = await knex('t_tasks').where({ id: params.task_id }).first();
|
|
1031
1072
|
if (!taskExists) {
|
|
1032
1073
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
1033
1074
|
}
|
|
1034
1075
|
// Use the shared helper function
|
|
1035
|
-
const deps = queryTaskDependencies(
|
|
1076
|
+
const deps = await queryTaskDependencies(actualAdapter, params.task_id, includeDetails);
|
|
1036
1077
|
return {
|
|
1037
1078
|
task_id: params.task_id,
|
|
1038
1079
|
blockers: deps.blockers,
|
|
@@ -1051,39 +1092,93 @@ export function getDependencies(params, db) {
|
|
|
1051
1092
|
/**
|
|
1052
1093
|
* Create multiple tasks atomically
|
|
1053
1094
|
*/
|
|
1054
|
-
export function batchCreateTasks(params,
|
|
1055
|
-
const
|
|
1095
|
+
export async function batchCreateTasks(params, adapter) {
|
|
1096
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1056
1097
|
if (!params.tasks || !Array.isArray(params.tasks)) {
|
|
1057
1098
|
throw new Error('Parameter "tasks" is required and must be an array');
|
|
1058
1099
|
}
|
|
1100
|
+
if (params.tasks.length > 50) {
|
|
1101
|
+
throw new Error('Parameter "tasks" must contain at most 50 items');
|
|
1102
|
+
}
|
|
1059
1103
|
const atomic = params.atomic !== undefined ? params.atomic : true;
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1104
|
+
try {
|
|
1105
|
+
if (atomic) {
|
|
1106
|
+
// Atomic mode: All or nothing
|
|
1107
|
+
const results = await actualAdapter.transaction(async (trx) => {
|
|
1108
|
+
const processedResults = [];
|
|
1109
|
+
for (const task of params.tasks) {
|
|
1110
|
+
try {
|
|
1111
|
+
const result = await createTaskInternal(task, actualAdapter, trx);
|
|
1112
|
+
processedResults.push({
|
|
1113
|
+
title: task.title,
|
|
1114
|
+
task_id: result.task_id,
|
|
1115
|
+
success: true,
|
|
1116
|
+
error: undefined
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
catch (error) {
|
|
1120
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1121
|
+
throw new Error(`Batch failed at task "${task.title}": ${errorMessage}`);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return processedResults;
|
|
1125
|
+
});
|
|
1126
|
+
return {
|
|
1127
|
+
success: true,
|
|
1128
|
+
created: results.length,
|
|
1129
|
+
failed: 0,
|
|
1130
|
+
results: results
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
else {
|
|
1134
|
+
// Non-atomic mode: Process each independently
|
|
1135
|
+
const results = [];
|
|
1136
|
+
let created = 0;
|
|
1137
|
+
let failed = 0;
|
|
1138
|
+
for (const task of params.tasks) {
|
|
1139
|
+
try {
|
|
1140
|
+
const result = await actualAdapter.transaction(async (trx) => {
|
|
1141
|
+
return await createTaskInternal(task, actualAdapter, trx);
|
|
1142
|
+
});
|
|
1143
|
+
results.push({
|
|
1144
|
+
title: task.title,
|
|
1145
|
+
task_id: result.task_id,
|
|
1146
|
+
success: true,
|
|
1147
|
+
error: undefined
|
|
1148
|
+
});
|
|
1149
|
+
created++;
|
|
1150
|
+
}
|
|
1151
|
+
catch (error) {
|
|
1152
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1153
|
+
results.push({
|
|
1154
|
+
title: task.title,
|
|
1155
|
+
task_id: undefined,
|
|
1156
|
+
success: false,
|
|
1157
|
+
error: errorMessage
|
|
1158
|
+
});
|
|
1159
|
+
failed++;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
success: failed === 0,
|
|
1164
|
+
created: created,
|
|
1165
|
+
failed: failed,
|
|
1166
|
+
results: results
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
catch (error) {
|
|
1171
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1172
|
+
throw new Error(`Failed to execute batch operation: ${message}`);
|
|
1173
|
+
}
|
|
1080
1174
|
}
|
|
1081
1175
|
/**
|
|
1082
1176
|
* Watch/unwatch files for a task (v3.4.1)
|
|
1083
1177
|
* Replaces the need to use task.link(file) for file watching
|
|
1084
1178
|
*/
|
|
1085
|
-
export function watchFiles(params,
|
|
1086
|
-
const
|
|
1179
|
+
export async function watchFiles(params, adapter) {
|
|
1180
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1181
|
+
const knex = actualAdapter.getKnex();
|
|
1087
1182
|
if (!params.task_id) {
|
|
1088
1183
|
throw new Error('Parameter "task_id" is required');
|
|
1089
1184
|
}
|
|
@@ -1091,14 +1186,13 @@ export function watchFiles(params, db) {
|
|
|
1091
1186
|
throw new Error('Parameter "action" is required (watch, unwatch, or list)');
|
|
1092
1187
|
}
|
|
1093
1188
|
try {
|
|
1094
|
-
return transaction(
|
|
1189
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
1095
1190
|
// Check if task exists
|
|
1096
|
-
const taskData =
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
`).get(params.task_id);
|
|
1191
|
+
const taskData = await trx('t_tasks as t')
|
|
1192
|
+
.join('m_task_statuses as s', 't.status_id', 's.id')
|
|
1193
|
+
.where('t.id', params.task_id)
|
|
1194
|
+
.select('t.id', 't.title', 's.name as status')
|
|
1195
|
+
.first();
|
|
1102
1196
|
if (!taskData) {
|
|
1103
1197
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
1104
1198
|
}
|
|
@@ -1106,16 +1200,18 @@ export function watchFiles(params, db) {
|
|
|
1106
1200
|
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1107
1201
|
throw new Error('Parameter "file_paths" is required for watch action');
|
|
1108
1202
|
}
|
|
1109
|
-
const insertFileLinkStmt = actualDb.prepare(`
|
|
1110
|
-
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
1111
|
-
VALUES (?, ?)
|
|
1112
|
-
`);
|
|
1113
1203
|
const addedFiles = [];
|
|
1114
1204
|
for (const filePath of params.file_paths) {
|
|
1115
|
-
const fileId = getOrCreateFile(
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1205
|
+
const fileId = await getOrCreateFile(actualAdapter, filePath, trx);
|
|
1206
|
+
// Check if already exists
|
|
1207
|
+
const existing = await trx('t_task_file_links')
|
|
1208
|
+
.where({ task_id: params.task_id, file_id: fileId })
|
|
1209
|
+
.first();
|
|
1210
|
+
if (!existing) {
|
|
1211
|
+
await trx('t_task_file_links').insert({
|
|
1212
|
+
task_id: params.task_id,
|
|
1213
|
+
file_id: fileId
|
|
1214
|
+
});
|
|
1119
1215
|
addedFiles.push(filePath);
|
|
1120
1216
|
}
|
|
1121
1217
|
}
|
|
@@ -1143,30 +1239,18 @@ export function watchFiles(params, db) {
|
|
|
1143
1239
|
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1144
1240
|
throw new Error('Parameter "file_paths" is required for unwatch action');
|
|
1145
1241
|
}
|
|
1146
|
-
const deleteFileLinkStmt = actualDb.prepare(`
|
|
1147
|
-
DELETE FROM t_task_file_links
|
|
1148
|
-
WHERE task_id = ? AND file_id = (SELECT id FROM m_files WHERE path = ?)
|
|
1149
|
-
`);
|
|
1150
1242
|
const removedFiles = [];
|
|
1151
1243
|
for (const filePath of params.file_paths) {
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
1244
|
+
const deleted = await trx('t_task_file_links')
|
|
1245
|
+
.where('task_id', params.task_id)
|
|
1246
|
+
.whereIn('file_id', function () {
|
|
1247
|
+
this.select('id').from('m_files').where({ path: filePath });
|
|
1248
|
+
})
|
|
1249
|
+
.delete();
|
|
1250
|
+
if (deleted > 0) {
|
|
1155
1251
|
removedFiles.push(filePath);
|
|
1156
1252
|
}
|
|
1157
1253
|
}
|
|
1158
|
-
// Unregister files from watcher
|
|
1159
|
-
try {
|
|
1160
|
-
const watcher = FileWatcher.getInstance();
|
|
1161
|
-
for (const filePath of removedFiles) {
|
|
1162
|
-
// Note: FileWatcher.unregisterFile doesn't exist in current API
|
|
1163
|
-
// The watcher will handle cleanup when task moves to done/archived
|
|
1164
|
-
// For now, we just remove the DB link
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
catch (error) {
|
|
1168
|
-
// Watcher may not be initialized, ignore
|
|
1169
|
-
}
|
|
1170
1254
|
return {
|
|
1171
1255
|
success: true,
|
|
1172
1256
|
task_id: params.task_id,
|
|
@@ -1177,13 +1261,11 @@ export function watchFiles(params, db) {
|
|
|
1177
1261
|
};
|
|
1178
1262
|
}
|
|
1179
1263
|
else if (params.action === 'list') {
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
`);
|
|
1186
|
-
const files = filesStmt.all(params.task_id).map((row) => row.path);
|
|
1264
|
+
const files = await trx('t_task_file_links as tfl')
|
|
1265
|
+
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
1266
|
+
.where('tfl.task_id', params.task_id)
|
|
1267
|
+
.select('f.path')
|
|
1268
|
+
.then(rows => rows.map((row) => row.path));
|
|
1187
1269
|
return {
|
|
1188
1270
|
success: true,
|
|
1189
1271
|
task_id: params.task_id,
|
|
@@ -1207,32 +1289,27 @@ export function watchFiles(params, db) {
|
|
|
1207
1289
|
* Get pruned files for a task (v3.5.0 Auto-Pruning)
|
|
1208
1290
|
* Returns audit trail of files that were auto-pruned as non-existent
|
|
1209
1291
|
*/
|
|
1210
|
-
export function getPrunedFiles(params,
|
|
1211
|
-
const
|
|
1292
|
+
export async function getPrunedFiles(params, adapter) {
|
|
1293
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1294
|
+
const knex = actualAdapter.getKnex();
|
|
1212
1295
|
try {
|
|
1213
1296
|
// Validate task_id
|
|
1214
1297
|
if (!params.task_id || typeof params.task_id !== 'number') {
|
|
1215
1298
|
throw new Error('task_id is required and must be a number');
|
|
1216
1299
|
}
|
|
1217
1300
|
// Validate task exists
|
|
1218
|
-
const task =
|
|
1301
|
+
const task = await knex('t_tasks').where({ id: params.task_id }).first();
|
|
1219
1302
|
if (!task) {
|
|
1220
1303
|
throw new Error(`Task not found: ${params.task_id}`);
|
|
1221
1304
|
}
|
|
1222
1305
|
// Get pruned files
|
|
1223
1306
|
const limit = params.limit || 100;
|
|
1224
|
-
const rows =
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
FROM t_task_pruned_files tpf
|
|
1231
|
-
LEFT JOIN m_context_keys k ON tpf.linked_decision_key_id = k.id
|
|
1232
|
-
WHERE tpf.task_id = ?
|
|
1233
|
-
ORDER BY tpf.pruned_ts DESC
|
|
1234
|
-
LIMIT ?
|
|
1235
|
-
`).all(params.task_id, limit);
|
|
1307
|
+
const rows = await knex('t_task_pruned_files as tpf')
|
|
1308
|
+
.leftJoin('m_context_keys as k', 'tpf.linked_decision_key_id', 'k.id')
|
|
1309
|
+
.where('tpf.task_id', params.task_id)
|
|
1310
|
+
.select('tpf.id', 'tpf.file_path', knex.raw(`datetime(tpf.pruned_ts, 'unixepoch') as pruned_at`), 'k.key as linked_decision')
|
|
1311
|
+
.orderBy('tpf.pruned_ts', 'desc')
|
|
1312
|
+
.limit(limit);
|
|
1236
1313
|
return {
|
|
1237
1314
|
success: true,
|
|
1238
1315
|
task_id: params.task_id,
|
|
@@ -1252,8 +1329,9 @@ export function getPrunedFiles(params, db) {
|
|
|
1252
1329
|
* Link a pruned file to a decision (v3.5.0 Auto-Pruning)
|
|
1253
1330
|
* Attaches WHY reasoning to pruned files for project archaeology
|
|
1254
1331
|
*/
|
|
1255
|
-
export function linkPrunedFile(params,
|
|
1256
|
-
const
|
|
1332
|
+
export async function linkPrunedFile(params, adapter) {
|
|
1333
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1334
|
+
const knex = actualAdapter.getKnex();
|
|
1257
1335
|
try {
|
|
1258
1336
|
// Validate pruned_file_id
|
|
1259
1337
|
if (!params.pruned_file_id || typeof params.pruned_file_id !== 'number') {
|
|
@@ -1264,30 +1342,31 @@ export function linkPrunedFile(params, db) {
|
|
|
1264
1342
|
throw new Error('decision_key is required and must be a string');
|
|
1265
1343
|
}
|
|
1266
1344
|
// Get decision key_id
|
|
1267
|
-
const decision =
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1345
|
+
const decision = await knex('m_context_keys as k')
|
|
1346
|
+
.whereExists(function () {
|
|
1347
|
+
this.select('*')
|
|
1348
|
+
.from('t_decisions as d')
|
|
1349
|
+
.whereRaw('d.key_id = k.id');
|
|
1350
|
+
})
|
|
1351
|
+
.where('k.key', params.decision_key)
|
|
1352
|
+
.select('k.id as key_id')
|
|
1353
|
+
.first();
|
|
1274
1354
|
if (!decision) {
|
|
1275
1355
|
throw new Error(`Decision not found: ${params.decision_key}`);
|
|
1276
1356
|
}
|
|
1277
1357
|
// Check if pruned file exists
|
|
1278
|
-
const prunedFile =
|
|
1279
|
-
|
|
1280
|
-
|
|
1358
|
+
const prunedFile = await knex('t_task_pruned_files')
|
|
1359
|
+
.where({ id: params.pruned_file_id })
|
|
1360
|
+
.select('id', 'task_id', 'file_path')
|
|
1361
|
+
.first();
|
|
1281
1362
|
if (!prunedFile) {
|
|
1282
1363
|
throw new Error(`Pruned file record not found: ${params.pruned_file_id}`);
|
|
1283
1364
|
}
|
|
1284
1365
|
// Update the link
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
`).run(decision.key_id, params.pruned_file_id);
|
|
1290
|
-
if (result.changes === 0) {
|
|
1366
|
+
const updated = await knex('t_task_pruned_files')
|
|
1367
|
+
.where({ id: params.pruned_file_id })
|
|
1368
|
+
.update({ linked_decision_key_id: decision.key_id });
|
|
1369
|
+
if (updated === 0) {
|
|
1291
1370
|
throw new Error(`Failed to link pruned file #${params.pruned_file_id} to decision ${params.decision_key}`);
|
|
1292
1371
|
}
|
|
1293
1372
|
return {
|
|
@@ -1565,8 +1644,9 @@ export function taskHelp() {
|
|
|
1565
1644
|
/**
|
|
1566
1645
|
* Query file watcher status and monitored files/tasks
|
|
1567
1646
|
*/
|
|
1568
|
-
export function watcherStatus(args,
|
|
1569
|
-
const
|
|
1647
|
+
export async function watcherStatus(args, adapter) {
|
|
1648
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1649
|
+
const knex = actualAdapter.getKnex();
|
|
1570
1650
|
const subaction = args.subaction || 'status';
|
|
1571
1651
|
const watcher = FileWatcher.getInstance();
|
|
1572
1652
|
if (subaction === 'help') {
|
|
@@ -1609,14 +1689,13 @@ export function watcherStatus(args, db) {
|
|
|
1609
1689
|
};
|
|
1610
1690
|
}
|
|
1611
1691
|
if (subaction === 'list_files') {
|
|
1612
|
-
const fileLinks =
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
`).all();
|
|
1692
|
+
const fileLinks = await knex('t_task_file_links as tfl')
|
|
1693
|
+
.join('t_tasks as t', 'tfl.task_id', 't.id')
|
|
1694
|
+
.join('m_task_statuses as ts', 't.status_id', 'ts.id')
|
|
1695
|
+
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
1696
|
+
.where('t.status_id', '!=', 6) // Exclude archived tasks
|
|
1697
|
+
.select('f.path as file_path', 't.id', 't.title', 'ts.name as status_name')
|
|
1698
|
+
.orderBy(['f.path', 't.id']);
|
|
1620
1699
|
// Group by file
|
|
1621
1700
|
const fileMap = new Map();
|
|
1622
1701
|
for (const link of fileLinks) {
|
|
@@ -1643,16 +1722,14 @@ export function watcherStatus(args, db) {
|
|
|
1643
1722
|
};
|
|
1644
1723
|
}
|
|
1645
1724
|
if (subaction === 'list_tasks') {
|
|
1646
|
-
const taskLinks =
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
ORDER BY t.id
|
|
1655
|
-
`).all();
|
|
1725
|
+
const taskLinks = await knex('t_tasks as t')
|
|
1726
|
+
.join('m_task_statuses as ts', 't.status_id', 'ts.id')
|
|
1727
|
+
.join('t_task_file_links as tfl', 't.id', 'tfl.task_id')
|
|
1728
|
+
.join('m_files as f', 'tfl.file_id', 'f.id')
|
|
1729
|
+
.where('t.status_id', '!=', 6) // Exclude archived tasks
|
|
1730
|
+
.groupBy('t.id', 't.title', 'ts.name')
|
|
1731
|
+
.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'))
|
|
1732
|
+
.orderBy('t.id');
|
|
1656
1733
|
const tasks = taskLinks.map(task => ({
|
|
1657
1734
|
task_id: task.id,
|
|
1658
1735
|
task_title: task.title,
|