sqlew 3.5.3 → 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 +247 -1772
- package/README.md +70 -304
- package/assets/config.example.toml +97 -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 +9 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/types.js.map +1 -1
- package/dist/database.d.ts +44 -122
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +145 -416
- package/dist/database.js.map +1 -1
- package/dist/index.js +215 -185
- 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/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.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 +66 -48
- 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 +374 -330
- 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 +217 -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 +513 -439
- 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 +86 -121
- package/dist/tools/utils.js.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/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/retention.d.ts +7 -7
- package/dist/utils/retention.d.ts.map +1 -1
- package/dist/utils/retention.js +12 -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 +16 -4
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 {
|
|
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';
|
|
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(
|
|
84
|
+
assignedAgentId = await getOrCreateAgent(adapter, params.assigned_agent, trx);
|
|
81
85
|
}
|
|
82
86
|
// Default to 'system' if no created_by_agent provided
|
|
83
|
-
// This ensures the activity log
|
|
87
|
+
// This ensures the activity log has a valid agent_id
|
|
84
88
|
const createdBy = params.created_by_agent || 'system';
|
|
85
|
-
const createdByAgentId = getOrCreateAgent(
|
|
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,23 +136,65 @@ 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();
|
|
145
189
|
}
|
|
146
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
|
+
});
|
|
147
198
|
// Link files and register with watcher if watch_files provided (v3.4.1)
|
|
148
199
|
if (params.watch_files && params.watch_files.length > 0) {
|
|
149
200
|
// Parse watch_files - handle MCP SDK converting JSON string to char array
|
|
@@ -180,19 +231,18 @@ function createTaskInternal(params, db) {
|
|
|
180
231
|
else {
|
|
181
232
|
throw new Error('Parameter "watch_files" must be a string or array');
|
|
182
233
|
}
|
|
183
|
-
const insertFileLinkStmt = db.prepare(`
|
|
184
|
-
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
185
|
-
VALUES (?, ?)
|
|
186
|
-
`);
|
|
187
234
|
for (const filePath of watchFilesParsed) {
|
|
188
|
-
const fileId = getOrCreateFile(
|
|
189
|
-
|
|
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();
|
|
190
240
|
}
|
|
191
241
|
// Register files with watcher for auto-tracking
|
|
192
242
|
try {
|
|
193
243
|
const watcher = FileWatcher.getInstance();
|
|
194
244
|
for (const filePath of watchFilesParsed) {
|
|
195
|
-
watcher.registerFile(filePath, taskId, params.title, status);
|
|
245
|
+
watcher.registerFile(filePath, Number(taskId), params.title, status);
|
|
196
246
|
}
|
|
197
247
|
}
|
|
198
248
|
catch (error) {
|
|
@@ -202,7 +252,7 @@ function createTaskInternal(params, db) {
|
|
|
202
252
|
}
|
|
203
253
|
return {
|
|
204
254
|
success: true,
|
|
205
|
-
task_id: taskId,
|
|
255
|
+
task_id: Number(taskId),
|
|
206
256
|
title: params.title,
|
|
207
257
|
status: status,
|
|
208
258
|
message: `Task "${params.title}" created successfully`
|
|
@@ -211,16 +261,16 @@ function createTaskInternal(params, db) {
|
|
|
211
261
|
/**
|
|
212
262
|
* Create a new task
|
|
213
263
|
*/
|
|
214
|
-
export function createTask(params,
|
|
215
|
-
const
|
|
264
|
+
export async function createTask(params, adapter) {
|
|
265
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
216
266
|
// Validate required parameters
|
|
217
267
|
if (!params.title || params.title.trim() === '') {
|
|
218
268
|
throw new Error('Parameter "title" is required and cannot be empty');
|
|
219
269
|
}
|
|
220
270
|
validateLength(params.title, 'Parameter "title"', 200);
|
|
221
271
|
try {
|
|
222
|
-
return transaction(
|
|
223
|
-
return createTaskInternal(params,
|
|
272
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
273
|
+
return await createTaskInternal(params, actualAdapter, trx);
|
|
224
274
|
});
|
|
225
275
|
}
|
|
226
276
|
catch (error) {
|
|
@@ -231,56 +281,50 @@ export function createTask(params, db) {
|
|
|
231
281
|
/**
|
|
232
282
|
* Update task metadata
|
|
233
283
|
*/
|
|
234
|
-
export function updateTask(params,
|
|
235
|
-
const
|
|
284
|
+
export async function updateTask(params, adapter) {
|
|
285
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
236
286
|
// Validate required parameters
|
|
237
287
|
if (!params.task_id) {
|
|
238
288
|
throw new Error('Parameter "task_id" is required');
|
|
239
289
|
}
|
|
240
290
|
try {
|
|
241
|
-
return transaction(
|
|
291
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
292
|
+
const knex = actualAdapter.getKnex();
|
|
242
293
|
// Check if task exists
|
|
243
|
-
const taskExists =
|
|
294
|
+
const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
|
|
244
295
|
if (!taskExists) {
|
|
245
296
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
246
297
|
}
|
|
247
|
-
// Build update
|
|
248
|
-
const
|
|
249
|
-
const updateParams = [];
|
|
298
|
+
// Build update data dynamically
|
|
299
|
+
const updateData = {};
|
|
250
300
|
if (params.title !== undefined) {
|
|
251
301
|
if (params.title.trim() === '') {
|
|
252
302
|
throw new Error('Parameter "title" cannot be empty');
|
|
253
303
|
}
|
|
254
304
|
validateLength(params.title, 'Parameter "title"', 200);
|
|
255
|
-
|
|
256
|
-
updateParams.push(params.title);
|
|
305
|
+
updateData.title = params.title;
|
|
257
306
|
}
|
|
258
307
|
if (params.priority !== undefined) {
|
|
259
308
|
validatePriorityRange(params.priority);
|
|
260
|
-
|
|
261
|
-
updateParams.push(params.priority);
|
|
309
|
+
updateData.priority = params.priority;
|
|
262
310
|
}
|
|
263
311
|
if (params.assigned_agent !== undefined) {
|
|
264
|
-
const agentId = getOrCreateAgent(
|
|
265
|
-
|
|
266
|
-
updateParams.push(agentId);
|
|
312
|
+
const agentId = await getOrCreateAgent(actualAdapter, params.assigned_agent, trx);
|
|
313
|
+
updateData.assigned_agent_id = agentId;
|
|
267
314
|
}
|
|
268
315
|
if (params.layer !== undefined) {
|
|
269
|
-
const layerId = getLayerId(
|
|
316
|
+
const layerId = await getLayerId(actualAdapter, params.layer, trx);
|
|
270
317
|
if (layerId === null) {
|
|
271
318
|
throw new Error(`Invalid layer: ${params.layer}. Must be one of: presentation, business, data, infrastructure, cross-cutting`);
|
|
272
319
|
}
|
|
273
|
-
|
|
274
|
-
updateParams.push(layerId);
|
|
320
|
+
updateData.layer_id = layerId;
|
|
275
321
|
}
|
|
276
322
|
// Update t_tasks if any updates
|
|
277
|
-
if (
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
`);
|
|
283
|
-
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
|
|
284
328
|
}
|
|
285
329
|
// Update t_task_details if any detail fields provided
|
|
286
330
|
if (params.description !== undefined || params.acceptance_criteria !== undefined || params.notes !== undefined) {
|
|
@@ -322,43 +366,35 @@ export function updateTask(params, db) {
|
|
|
322
366
|
}
|
|
323
367
|
}
|
|
324
368
|
// Check if details exist
|
|
325
|
-
const detailsExist =
|
|
326
|
-
|
|
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) {
|
|
327
384
|
// 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
|
-
}
|
|
385
|
+
await trx('t_task_details')
|
|
386
|
+
.where({ task_id: params.task_id })
|
|
387
|
+
.update(detailsUpdate);
|
|
354
388
|
}
|
|
355
|
-
else {
|
|
389
|
+
else if (!detailsExist) {
|
|
356
390
|
// Insert new details
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
+
});
|
|
362
398
|
}
|
|
363
399
|
}
|
|
364
400
|
// Handle watch_files if provided (v3.4.1)
|
|
@@ -395,22 +431,20 @@ export function updateTask(params, db) {
|
|
|
395
431
|
else {
|
|
396
432
|
throw new Error('Parameter "watch_files" must be a string or array');
|
|
397
433
|
}
|
|
398
|
-
const insertFileLinkStmt = actualDb.prepare(`
|
|
399
|
-
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
400
|
-
VALUES (?, ?)
|
|
401
|
-
`);
|
|
402
434
|
for (const filePath of watchFilesParsed) {
|
|
403
|
-
const fileId = getOrCreateFile(
|
|
404
|
-
|
|
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();
|
|
405
440
|
}
|
|
406
441
|
// Register files with watcher for auto-tracking
|
|
407
442
|
try {
|
|
408
|
-
const taskData =
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
`).get(params.task_id);
|
|
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();
|
|
414
448
|
if (taskData) {
|
|
415
449
|
const watcher = FileWatcher.getInstance();
|
|
416
450
|
for (const filePath of watchFilesParsed) {
|
|
@@ -438,89 +472,70 @@ export function updateTask(params, db) {
|
|
|
438
472
|
/**
|
|
439
473
|
* Internal helper: Query task dependencies (used by getTask and getDependencies)
|
|
440
474
|
*/
|
|
441
|
-
function queryTaskDependencies(
|
|
475
|
+
async function queryTaskDependencies(adapter, taskId, includeDetails = false) {
|
|
476
|
+
const knex = adapter.getKnex();
|
|
442
477
|
// Build query based on include_details flag
|
|
443
|
-
|
|
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);
|
|
444
502
|
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
|
-
`;
|
|
503
|
+
blockersQuery = blockersQuery.leftJoin('t_task_details as td', 't.id', 'td.task_id');
|
|
465
504
|
}
|
|
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);
|
|
505
|
+
const blockers = await blockersQuery;
|
|
477
506
|
// Get blocking (tasks this task blocks)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
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;
|
|
488
517
|
return { blockers, blocking };
|
|
489
518
|
}
|
|
490
519
|
/**
|
|
491
520
|
* Get full task details
|
|
492
521
|
*/
|
|
493
|
-
export function getTask(params,
|
|
494
|
-
const
|
|
522
|
+
export async function getTask(params, adapter) {
|
|
523
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
524
|
+
const knex = actualAdapter.getKnex();
|
|
495
525
|
if (!params.task_id) {
|
|
496
526
|
throw new Error('Parameter "task_id" is required');
|
|
497
527
|
}
|
|
498
528
|
try {
|
|
499
529
|
// 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);
|
|
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();
|
|
524
539
|
if (!task) {
|
|
525
540
|
return {
|
|
526
541
|
found: false,
|
|
@@ -528,37 +543,27 @@ export function getTask(params, db) {
|
|
|
528
543
|
};
|
|
529
544
|
}
|
|
530
545
|
// Get tags
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
`);
|
|
537
|
-
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));
|
|
538
551
|
// Get decision links
|
|
539
|
-
const
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
WHERE tdl.task_id = ?
|
|
544
|
-
`);
|
|
545
|
-
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');
|
|
546
556
|
// Get constraint links
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
WHERE tcl.task_id = ?
|
|
552
|
-
`);
|
|
553
|
-
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');
|
|
554
561
|
// Get file links
|
|
555
|
-
const
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
`);
|
|
561
|
-
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));
|
|
562
567
|
// Build result
|
|
563
568
|
const result = {
|
|
564
569
|
found: true,
|
|
@@ -572,7 +577,7 @@ export function getTask(params, db) {
|
|
|
572
577
|
};
|
|
573
578
|
// Include dependencies if requested (token-efficient, metadata-only)
|
|
574
579
|
if (params.include_dependencies) {
|
|
575
|
-
const deps = queryTaskDependencies(
|
|
580
|
+
const deps = await queryTaskDependencies(actualAdapter, params.task_id, false);
|
|
576
581
|
result.task.dependencies = {
|
|
577
582
|
blockers: deps.blockers,
|
|
578
583
|
blocking: deps.blocking
|
|
@@ -588,79 +593,69 @@ export function getTask(params, db) {
|
|
|
588
593
|
/**
|
|
589
594
|
* List tasks (token-efficient, no descriptions)
|
|
590
595
|
*/
|
|
591
|
-
export async function listTasks(params = {},
|
|
592
|
-
const
|
|
596
|
+
export async function listTasks(params = {}, adapter) {
|
|
597
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
598
|
+
const knex = actualAdapter.getKnex();
|
|
593
599
|
try {
|
|
594
600
|
// 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(
|
|
601
|
+
const transitionCount = await detectAndTransitionStaleTasks(actualAdapter);
|
|
602
|
+
const gitCompletedCount = await detectAndCompleteReviewedTasks(actualAdapter);
|
|
603
|
+
const gitArchivedCount = await detectAndArchiveOnCommit(actualAdapter);
|
|
604
|
+
const archiveCount = await autoArchiveOldDoneTasks(actualAdapter);
|
|
599
605
|
// Build query with optional dependency counts
|
|
600
606
|
let query;
|
|
601
607
|
if (params.include_dependency_counts) {
|
|
602
608
|
// 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
|
-
`;
|
|
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'));
|
|
621
623
|
}
|
|
622
624
|
else {
|
|
623
625
|
// Standard query without dependency counts
|
|
624
|
-
query = '
|
|
626
|
+
query = knex('v_task_board');
|
|
625
627
|
}
|
|
626
|
-
const queryParams = [];
|
|
627
628
|
// Filter by status
|
|
628
629
|
if (params.status) {
|
|
629
630
|
if (!STATUS_TO_ID[params.status]) {
|
|
630
631
|
throw new Error(`Invalid status: ${params.status}. Must be one of: todo, in_progress, waiting_review, blocked, done, archived`);
|
|
631
632
|
}
|
|
632
|
-
query
|
|
633
|
-
queryParams.push(params.status);
|
|
633
|
+
query = query.where(params.include_dependency_counts ? 'vt.status' : 'status', params.status);
|
|
634
634
|
}
|
|
635
635
|
// Filter by assigned agent
|
|
636
636
|
if (params.assigned_agent) {
|
|
637
|
-
query
|
|
638
|
-
queryParams.push(params.assigned_agent);
|
|
637
|
+
query = query.where(params.include_dependency_counts ? 'vt.assigned_to' : 'assigned_to', params.assigned_agent);
|
|
639
638
|
}
|
|
640
639
|
// Filter by layer
|
|
641
640
|
if (params.layer) {
|
|
642
|
-
query
|
|
643
|
-
queryParams.push(params.layer);
|
|
641
|
+
query = query.where(params.include_dependency_counts ? 'vt.layer' : 'layer', params.layer);
|
|
644
642
|
}
|
|
645
643
|
// Filter by tags
|
|
646
644
|
if (params.tags && params.tags.length > 0) {
|
|
647
645
|
for (const tag of params.tags) {
|
|
648
|
-
query
|
|
649
|
-
queryParams.push(`%${tag}%`);
|
|
646
|
+
query = query.where(params.include_dependency_counts ? 'vt.tags' : 'tags', 'like', `%${tag}%`);
|
|
650
647
|
}
|
|
651
648
|
}
|
|
652
649
|
// Order by updated timestamp (most recent first)
|
|
653
|
-
query
|
|
650
|
+
query = query.orderBy(params.include_dependency_counts ? 'vt.updated_ts' : 'updated_ts', 'desc');
|
|
654
651
|
// Pagination
|
|
655
652
|
const limit = params.limit !== undefined ? params.limit : 50;
|
|
656
653
|
const offset = params.offset || 0;
|
|
657
654
|
validateRange(limit, 'Parameter "limit"', 0, 100);
|
|
658
655
|
validateRange(offset, 'Parameter "offset"', 0, Number.MAX_SAFE_INTEGER);
|
|
659
|
-
query
|
|
660
|
-
queryParams.push(limit, offset);
|
|
656
|
+
query = query.limit(limit).offset(offset);
|
|
661
657
|
// Execute query
|
|
662
|
-
const
|
|
663
|
-
const rows = stmt.all(...queryParams);
|
|
658
|
+
const rows = await query;
|
|
664
659
|
return {
|
|
665
660
|
tasks: rows,
|
|
666
661
|
count: rows.length,
|
|
@@ -678,8 +673,9 @@ export async function listTasks(params = {}, db) {
|
|
|
678
673
|
/**
|
|
679
674
|
* Move task to different status
|
|
680
675
|
*/
|
|
681
|
-
export function moveTask(params,
|
|
682
|
-
const
|
|
676
|
+
export async function moveTask(params, adapter) {
|
|
677
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
678
|
+
const knex = actualAdapter.getKnex();
|
|
683
679
|
if (!params.task_id) {
|
|
684
680
|
throw new Error('Parameter "task_id" is required');
|
|
685
681
|
}
|
|
@@ -688,11 +684,14 @@ export function moveTask(params, db) {
|
|
|
688
684
|
}
|
|
689
685
|
try {
|
|
690
686
|
// Run auto-stale detection and auto-archive before move
|
|
691
|
-
detectAndTransitionStaleTasks(
|
|
692
|
-
autoArchiveOldDoneTasks(
|
|
693
|
-
return transaction(
|
|
687
|
+
await detectAndTransitionStaleTasks(actualAdapter);
|
|
688
|
+
await autoArchiveOldDoneTasks(actualAdapter);
|
|
689
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
694
690
|
// Get current status
|
|
695
|
-
const taskRow =
|
|
691
|
+
const taskRow = await trx('t_tasks')
|
|
692
|
+
.where({ id: params.task_id })
|
|
693
|
+
.select('status_id')
|
|
694
|
+
.first();
|
|
696
695
|
if (!taskRow) {
|
|
697
696
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
698
697
|
}
|
|
@@ -708,13 +707,26 @@ export function moveTask(params, db) {
|
|
|
708
707
|
`Valid transitions: ${validNextStatuses.map(id => ID_TO_STATUS[id]).join(', ')}`);
|
|
709
708
|
}
|
|
710
709
|
// Update status
|
|
711
|
-
const
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
+
});
|
|
718
730
|
// Update watcher if moving to done or archived (stop watching)
|
|
719
731
|
if (params.new_status === 'done' || params.new_status === 'archived') {
|
|
720
732
|
try {
|
|
@@ -742,8 +754,9 @@ export function moveTask(params, db) {
|
|
|
742
754
|
/**
|
|
743
755
|
* Link task to decision/constraint/file
|
|
744
756
|
*/
|
|
745
|
-
export function linkTask(params,
|
|
746
|
-
const
|
|
757
|
+
export async function linkTask(params, adapter) {
|
|
758
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
759
|
+
const knex = actualAdapter.getKnex();
|
|
747
760
|
if (!params.task_id) {
|
|
748
761
|
throw new Error('Parameter "task_id" is required');
|
|
749
762
|
}
|
|
@@ -754,21 +767,21 @@ export function linkTask(params, db) {
|
|
|
754
767
|
throw new Error('Parameter "target_id" is required');
|
|
755
768
|
}
|
|
756
769
|
try {
|
|
757
|
-
return transaction(
|
|
770
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
758
771
|
// Check if task exists
|
|
759
|
-
const taskExists =
|
|
772
|
+
const taskExists = await trx('t_tasks').where({ id: params.task_id }).first();
|
|
760
773
|
if (!taskExists) {
|
|
761
774
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
762
775
|
}
|
|
763
776
|
if (params.link_type === 'decision') {
|
|
764
777
|
const decisionKey = String(params.target_id);
|
|
765
|
-
const keyId = getOrCreateContextKey(
|
|
778
|
+
const keyId = await getOrCreateContextKey(actualAdapter, decisionKey, trx);
|
|
766
779
|
const linkRelation = params.link_relation || 'implements';
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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();
|
|
772
785
|
return {
|
|
773
786
|
success: true,
|
|
774
787
|
task_id: params.task_id,
|
|
@@ -781,15 +794,14 @@ export function linkTask(params, db) {
|
|
|
781
794
|
else if (params.link_type === 'constraint') {
|
|
782
795
|
const constraintId = Number(params.target_id);
|
|
783
796
|
// Check if constraint exists
|
|
784
|
-
const constraintExists =
|
|
797
|
+
const constraintExists = await trx('t_constraints').where({ id: constraintId }).first();
|
|
785
798
|
if (!constraintExists) {
|
|
786
799
|
throw new Error(`Constraint with id ${constraintId} not found`);
|
|
787
800
|
}
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
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();
|
|
793
805
|
return {
|
|
794
806
|
success: true,
|
|
795
807
|
task_id: params.task_id,
|
|
@@ -804,20 +816,18 @@ export function linkTask(params, db) {
|
|
|
804
816
|
console.warn(` Use task.create(watch_files=[...]) or task.update(watch_files=[...]) instead.`);
|
|
805
817
|
console.warn(` Or use the new watch_files action: { action: "watch_files", task_id: ${params.task_id}, file_paths: ["..."] }`);
|
|
806
818
|
const filePath = String(params.target_id);
|
|
807
|
-
const fileId = getOrCreateFile(
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
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();
|
|
813
824
|
// Register file with watcher for auto-tracking
|
|
814
825
|
try {
|
|
815
|
-
const taskData =
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
`).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();
|
|
821
831
|
if (taskData) {
|
|
822
832
|
const watcher = FileWatcher.getInstance();
|
|
823
833
|
watcher.registerFile(filePath, params.task_id, taskData.title, taskData.status);
|
|
@@ -849,15 +859,19 @@ export function linkTask(params, db) {
|
|
|
849
859
|
/**
|
|
850
860
|
* Archive completed task
|
|
851
861
|
*/
|
|
852
|
-
export function archiveTask(params,
|
|
853
|
-
const
|
|
862
|
+
export async function archiveTask(params, adapter) {
|
|
863
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
864
|
+
const knex = actualAdapter.getKnex();
|
|
854
865
|
if (!params.task_id) {
|
|
855
866
|
throw new Error('Parameter "task_id" is required');
|
|
856
867
|
}
|
|
857
868
|
try {
|
|
858
|
-
return transaction(
|
|
869
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
859
870
|
// Check if task is in 'done' status
|
|
860
|
-
const taskRow =
|
|
871
|
+
const taskRow = await trx('t_tasks')
|
|
872
|
+
.where({ id: params.task_id })
|
|
873
|
+
.select('status_id')
|
|
874
|
+
.first();
|
|
861
875
|
if (!taskRow) {
|
|
862
876
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
863
877
|
}
|
|
@@ -865,8 +879,18 @@ export function archiveTask(params, db) {
|
|
|
865
879
|
throw new Error(`Task ${params.task_id} must be in 'done' status to archive (current: ${ID_TO_STATUS[taskRow.status_id]})`);
|
|
866
880
|
}
|
|
867
881
|
// Update to archived
|
|
868
|
-
|
|
869
|
-
|
|
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
|
+
});
|
|
870
894
|
// Unregister from file watcher (archived tasks don't need tracking)
|
|
871
895
|
try {
|
|
872
896
|
const watcher = FileWatcher.getInstance();
|
|
@@ -890,8 +914,9 @@ export function archiveTask(params, db) {
|
|
|
890
914
|
/**
|
|
891
915
|
* Add dependency (blocking relationship) between tasks
|
|
892
916
|
*/
|
|
893
|
-
export function addDependency(params,
|
|
894
|
-
const
|
|
917
|
+
export async function addDependency(params, adapter) {
|
|
918
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
919
|
+
const knex = actualAdapter.getKnex();
|
|
895
920
|
if (!params.blocker_task_id) {
|
|
896
921
|
throw new Error('Parameter "blocker_task_id" is required');
|
|
897
922
|
}
|
|
@@ -899,14 +924,20 @@ export function addDependency(params, db) {
|
|
|
899
924
|
throw new Error('Parameter "blocked_task_id" is required');
|
|
900
925
|
}
|
|
901
926
|
try {
|
|
902
|
-
return transaction(
|
|
927
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
903
928
|
// Validation 1: No self-dependencies
|
|
904
929
|
if (params.blocker_task_id === params.blocked_task_id) {
|
|
905
930
|
throw new Error('Self-dependency not allowed');
|
|
906
931
|
}
|
|
907
932
|
// Validation 2: Both tasks must exist and check if archived
|
|
908
|
-
const blockerTask =
|
|
909
|
-
|
|
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();
|
|
910
941
|
if (!blockerTask) {
|
|
911
942
|
throw new Error(`Blocker task #${params.blocker_task_id} not found`);
|
|
912
943
|
}
|
|
@@ -921,15 +952,17 @@ export function addDependency(params, db) {
|
|
|
921
952
|
throw new Error(`Cannot add dependency: Task #${params.blocked_task_id} is archived`);
|
|
922
953
|
}
|
|
923
954
|
// Validation 4: No direct circular (reverse relationship)
|
|
924
|
-
const reverseExists =
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
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();
|
|
928
961
|
if (reverseExists) {
|
|
929
962
|
throw new Error(`Circular dependency detected: Task #${params.blocked_task_id} already blocks Task #${params.blocker_task_id}`);
|
|
930
963
|
}
|
|
931
964
|
// Validation 5: No transitive circular (check if adding this would create a cycle)
|
|
932
|
-
const cycleCheck =
|
|
965
|
+
const cycleCheck = await trx.raw(`
|
|
933
966
|
WITH RECURSIVE dependency_chain AS (
|
|
934
967
|
-- Start from the task that would be blocked
|
|
935
968
|
SELECT blocked_task_id as task_id, 1 as depth
|
|
@@ -945,10 +978,11 @@ export function addDependency(params, db) {
|
|
|
945
978
|
WHERE dc.depth < 100
|
|
946
979
|
)
|
|
947
980
|
SELECT task_id FROM dependency_chain WHERE task_id = ?
|
|
948
|
-
|
|
981
|
+
`, [params.blocked_task_id, params.blocker_task_id])
|
|
982
|
+
.then((result) => result[0]);
|
|
949
983
|
if (cycleCheck) {
|
|
950
984
|
// Build cycle path for error message
|
|
951
|
-
const cyclePathResult =
|
|
985
|
+
const cyclePathResult = await trx.raw(`
|
|
952
986
|
WITH RECURSIVE dependency_chain AS (
|
|
953
987
|
SELECT blocked_task_id as task_id, 1 as depth,
|
|
954
988
|
CAST(blocked_task_id AS TEXT) as path
|
|
@@ -964,16 +998,17 @@ export function addDependency(params, db) {
|
|
|
964
998
|
WHERE dc.depth < 100
|
|
965
999
|
)
|
|
966
1000
|
SELECT path FROM dependency_chain WHERE task_id = ? ORDER BY depth DESC LIMIT 1
|
|
967
|
-
|
|
1001
|
+
`, [params.blocked_task_id, params.blocker_task_id])
|
|
1002
|
+
.then((result) => result[0]);
|
|
968
1003
|
const cyclePath = cyclePathResult?.path || `#${params.blocked_task_id} → ... → #${params.blocker_task_id}`;
|
|
969
1004
|
throw new Error(`Circular dependency detected: Task #${params.blocker_task_id} → #${cyclePath} → #${params.blocker_task_id}`);
|
|
970
1005
|
}
|
|
971
1006
|
// All validations passed - insert dependency
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
+
});
|
|
977
1012
|
return {
|
|
978
1013
|
success: true,
|
|
979
1014
|
message: `Dependency added: Task #${params.blocker_task_id} blocks Task #${params.blocked_task_id}`
|
|
@@ -992,8 +1027,9 @@ export function addDependency(params, db) {
|
|
|
992
1027
|
/**
|
|
993
1028
|
* Remove dependency between tasks
|
|
994
1029
|
*/
|
|
995
|
-
export function removeDependency(params,
|
|
996
|
-
const
|
|
1030
|
+
export async function removeDependency(params, adapter) {
|
|
1031
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1032
|
+
const knex = actualAdapter.getKnex();
|
|
997
1033
|
if (!params.blocker_task_id) {
|
|
998
1034
|
throw new Error('Parameter "blocker_task_id" is required');
|
|
999
1035
|
}
|
|
@@ -1001,11 +1037,12 @@ export function removeDependency(params, db) {
|
|
|
1001
1037
|
throw new Error('Parameter "blocked_task_id" is required');
|
|
1002
1038
|
}
|
|
1003
1039
|
try {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
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();
|
|
1009
1046
|
return {
|
|
1010
1047
|
success: true,
|
|
1011
1048
|
message: `Dependency removed: Task #${params.blocker_task_id} no longer blocks Task #${params.blocked_task_id}`
|
|
@@ -1019,20 +1056,21 @@ export function removeDependency(params, db) {
|
|
|
1019
1056
|
/**
|
|
1020
1057
|
* Get dependencies for a task (bidirectional: what blocks this task, what this task blocks)
|
|
1021
1058
|
*/
|
|
1022
|
-
export function getDependencies(params,
|
|
1023
|
-
const
|
|
1059
|
+
export async function getDependencies(params, adapter) {
|
|
1060
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1061
|
+
const knex = actualAdapter.getKnex();
|
|
1024
1062
|
if (!params.task_id) {
|
|
1025
1063
|
throw new Error('Parameter "task_id" is required');
|
|
1026
1064
|
}
|
|
1027
1065
|
const includeDetails = params.include_details || false;
|
|
1028
1066
|
try {
|
|
1029
1067
|
// Check if task exists
|
|
1030
|
-
const taskExists =
|
|
1068
|
+
const taskExists = await knex('t_tasks').where({ id: params.task_id }).first();
|
|
1031
1069
|
if (!taskExists) {
|
|
1032
1070
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
1033
1071
|
}
|
|
1034
1072
|
// Use the shared helper function
|
|
1035
|
-
const deps = queryTaskDependencies(
|
|
1073
|
+
const deps = await queryTaskDependencies(actualAdapter, params.task_id, includeDetails);
|
|
1036
1074
|
return {
|
|
1037
1075
|
task_id: params.task_id,
|
|
1038
1076
|
blockers: deps.blockers,
|
|
@@ -1051,39 +1089,93 @@ export function getDependencies(params, db) {
|
|
|
1051
1089
|
/**
|
|
1052
1090
|
* Create multiple tasks atomically
|
|
1053
1091
|
*/
|
|
1054
|
-
export function batchCreateTasks(params,
|
|
1055
|
-
const
|
|
1092
|
+
export async function batchCreateTasks(params, adapter) {
|
|
1093
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1056
1094
|
if (!params.tasks || !Array.isArray(params.tasks)) {
|
|
1057
1095
|
throw new Error('Parameter "tasks" is required and must be an array');
|
|
1058
1096
|
}
|
|
1097
|
+
if (params.tasks.length > 50) {
|
|
1098
|
+
throw new Error('Parameter "tasks" must contain at most 50 items');
|
|
1099
|
+
}
|
|
1059
1100
|
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
|
-
|
|
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
|
+
}
|
|
1080
1171
|
}
|
|
1081
1172
|
/**
|
|
1082
1173
|
* Watch/unwatch files for a task (v3.4.1)
|
|
1083
1174
|
* Replaces the need to use task.link(file) for file watching
|
|
1084
1175
|
*/
|
|
1085
|
-
export function watchFiles(params,
|
|
1086
|
-
const
|
|
1176
|
+
export async function watchFiles(params, adapter) {
|
|
1177
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1178
|
+
const knex = actualAdapter.getKnex();
|
|
1087
1179
|
if (!params.task_id) {
|
|
1088
1180
|
throw new Error('Parameter "task_id" is required');
|
|
1089
1181
|
}
|
|
@@ -1091,14 +1183,13 @@ export function watchFiles(params, db) {
|
|
|
1091
1183
|
throw new Error('Parameter "action" is required (watch, unwatch, or list)');
|
|
1092
1184
|
}
|
|
1093
1185
|
try {
|
|
1094
|
-
return transaction(
|
|
1186
|
+
return await actualAdapter.transaction(async (trx) => {
|
|
1095
1187
|
// Check if task exists
|
|
1096
|
-
const taskData =
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
`).get(params.task_id);
|
|
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();
|
|
1102
1193
|
if (!taskData) {
|
|
1103
1194
|
throw new Error(`Task with id ${params.task_id} not found`);
|
|
1104
1195
|
}
|
|
@@ -1106,16 +1197,18 @@ export function watchFiles(params, db) {
|
|
|
1106
1197
|
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1107
1198
|
throw new Error('Parameter "file_paths" is required for watch action');
|
|
1108
1199
|
}
|
|
1109
|
-
const insertFileLinkStmt = actualDb.prepare(`
|
|
1110
|
-
INSERT OR IGNORE INTO t_task_file_links (task_id, file_id)
|
|
1111
|
-
VALUES (?, ?)
|
|
1112
|
-
`);
|
|
1113
1200
|
const addedFiles = [];
|
|
1114
1201
|
for (const filePath of params.file_paths) {
|
|
1115
|
-
const fileId = getOrCreateFile(
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
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
|
+
});
|
|
1119
1212
|
addedFiles.push(filePath);
|
|
1120
1213
|
}
|
|
1121
1214
|
}
|
|
@@ -1143,30 +1236,18 @@ export function watchFiles(params, db) {
|
|
|
1143
1236
|
if (!params.file_paths || params.file_paths.length === 0) {
|
|
1144
1237
|
throw new Error('Parameter "file_paths" is required for unwatch action');
|
|
1145
1238
|
}
|
|
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
1239
|
const removedFiles = [];
|
|
1151
1240
|
for (const filePath of params.file_paths) {
|
|
1152
|
-
const
|
|
1153
|
-
|
|
1154
|
-
|
|
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) {
|
|
1155
1248
|
removedFiles.push(filePath);
|
|
1156
1249
|
}
|
|
1157
1250
|
}
|
|
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
1251
|
return {
|
|
1171
1252
|
success: true,
|
|
1172
1253
|
task_id: params.task_id,
|
|
@@ -1177,13 +1258,11 @@ export function watchFiles(params, db) {
|
|
|
1177
1258
|
};
|
|
1178
1259
|
}
|
|
1179
1260
|
else if (params.action === 'list') {
|
|
1180
|
-
const
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
`);
|
|
1186
|
-
const files = filesStmt.all(params.task_id).map((row) => row.path);
|
|
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));
|
|
1187
1266
|
return {
|
|
1188
1267
|
success: true,
|
|
1189
1268
|
task_id: params.task_id,
|
|
@@ -1207,32 +1286,27 @@ export function watchFiles(params, db) {
|
|
|
1207
1286
|
* Get pruned files for a task (v3.5.0 Auto-Pruning)
|
|
1208
1287
|
* Returns audit trail of files that were auto-pruned as non-existent
|
|
1209
1288
|
*/
|
|
1210
|
-
export function getPrunedFiles(params,
|
|
1211
|
-
const
|
|
1289
|
+
export async function getPrunedFiles(params, adapter) {
|
|
1290
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1291
|
+
const knex = actualAdapter.getKnex();
|
|
1212
1292
|
try {
|
|
1213
1293
|
// Validate task_id
|
|
1214
1294
|
if (!params.task_id || typeof params.task_id !== 'number') {
|
|
1215
1295
|
throw new Error('task_id is required and must be a number');
|
|
1216
1296
|
}
|
|
1217
1297
|
// Validate task exists
|
|
1218
|
-
const task =
|
|
1298
|
+
const task = await knex('t_tasks').where({ id: params.task_id }).first();
|
|
1219
1299
|
if (!task) {
|
|
1220
1300
|
throw new Error(`Task not found: ${params.task_id}`);
|
|
1221
1301
|
}
|
|
1222
1302
|
// Get pruned files
|
|
1223
1303
|
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);
|
|
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);
|
|
1236
1310
|
return {
|
|
1237
1311
|
success: true,
|
|
1238
1312
|
task_id: params.task_id,
|
|
@@ -1252,8 +1326,9 @@ export function getPrunedFiles(params, db) {
|
|
|
1252
1326
|
* Link a pruned file to a decision (v3.5.0 Auto-Pruning)
|
|
1253
1327
|
* Attaches WHY reasoning to pruned files for project archaeology
|
|
1254
1328
|
*/
|
|
1255
|
-
export function linkPrunedFile(params,
|
|
1256
|
-
const
|
|
1329
|
+
export async function linkPrunedFile(params, adapter) {
|
|
1330
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1331
|
+
const knex = actualAdapter.getKnex();
|
|
1257
1332
|
try {
|
|
1258
1333
|
// Validate pruned_file_id
|
|
1259
1334
|
if (!params.pruned_file_id || typeof params.pruned_file_id !== 'number') {
|
|
@@ -1264,30 +1339,31 @@ export function linkPrunedFile(params, db) {
|
|
|
1264
1339
|
throw new Error('decision_key is required and must be a string');
|
|
1265
1340
|
}
|
|
1266
1341
|
// Get decision key_id
|
|
1267
|
-
const decision =
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
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();
|
|
1274
1351
|
if (!decision) {
|
|
1275
1352
|
throw new Error(`Decision not found: ${params.decision_key}`);
|
|
1276
1353
|
}
|
|
1277
1354
|
// Check if pruned file exists
|
|
1278
|
-
const prunedFile =
|
|
1279
|
-
|
|
1280
|
-
|
|
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();
|
|
1281
1359
|
if (!prunedFile) {
|
|
1282
1360
|
throw new Error(`Pruned file record not found: ${params.pruned_file_id}`);
|
|
1283
1361
|
}
|
|
1284
1362
|
// Update the link
|
|
1285
|
-
const
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
`).run(decision.key_id, params.pruned_file_id);
|
|
1290
|
-
if (result.changes === 0) {
|
|
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) {
|
|
1291
1367
|
throw new Error(`Failed to link pruned file #${params.pruned_file_id} to decision ${params.decision_key}`);
|
|
1292
1368
|
}
|
|
1293
1369
|
return {
|
|
@@ -1565,8 +1641,9 @@ export function taskHelp() {
|
|
|
1565
1641
|
/**
|
|
1566
1642
|
* Query file watcher status and monitored files/tasks
|
|
1567
1643
|
*/
|
|
1568
|
-
export function watcherStatus(args,
|
|
1569
|
-
const
|
|
1644
|
+
export async function watcherStatus(args, adapter) {
|
|
1645
|
+
const actualAdapter = adapter ?? getAdapter();
|
|
1646
|
+
const knex = actualAdapter.getKnex();
|
|
1570
1647
|
const subaction = args.subaction || 'status';
|
|
1571
1648
|
const watcher = FileWatcher.getInstance();
|
|
1572
1649
|
if (subaction === 'help') {
|
|
@@ -1609,14 +1686,13 @@ export function watcherStatus(args, db) {
|
|
|
1609
1686
|
};
|
|
1610
1687
|
}
|
|
1611
1688
|
if (subaction === 'list_files') {
|
|
1612
|
-
const fileLinks =
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
`).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']);
|
|
1620
1696
|
// Group by file
|
|
1621
1697
|
const fileMap = new Map();
|
|
1622
1698
|
for (const link of fileLinks) {
|
|
@@ -1643,16 +1719,14 @@ export function watcherStatus(args, db) {
|
|
|
1643
1719
|
};
|
|
1644
1720
|
}
|
|
1645
1721
|
if (subaction === 'list_tasks') {
|
|
1646
|
-
const taskLinks =
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
ORDER BY t.id
|
|
1655
|
-
`).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');
|
|
1656
1730
|
const tasks = taskLinks.map(task => ({
|
|
1657
1731
|
task_id: task.id,
|
|
1658
1732
|
task_title: task.title,
|