task-o-matic 0.0.14 → 0.0.16
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/dist/cli/display/progress.d.ts +15 -2
- package/dist/cli/display/progress.d.ts.map +1 -1
- package/dist/cli/display/progress.js +72 -4
- package/dist/commands/benchmark.d.ts.map +1 -1
- package/dist/commands/benchmark.js +11 -3
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +19 -4
- package/dist/commands/prd.js +7 -1
- package/dist/commands/tasks/delete.d.ts.map +1 -1
- package/dist/commands/tasks/delete.js +2 -1
- package/dist/commands/tasks/document/add.d.ts.map +1 -1
- package/dist/commands/tasks/document/add.js +2 -1
- package/dist/commands/tasks/document/get.d.ts.map +1 -1
- package/dist/commands/tasks/document/get.js +2 -1
- package/dist/commands/tasks/plan/set.d.ts.map +1 -1
- package/dist/commands/tasks/plan/set.js +11 -3
- package/dist/commands/tasks/show.d.ts.map +1 -1
- package/dist/commands/tasks/show.js +2 -1
- package/dist/commands/tasks/status.d.ts.map +1 -1
- package/dist/commands/tasks/status.js +2 -1
- package/dist/commands/tasks/update.d.ts.map +1 -1
- package/dist/commands/tasks/update.js +7 -1
- package/dist/commands/workflow.d.ts.map +1 -1
- package/dist/commands/workflow.js +15 -2
- package/dist/lib/ai-service/base-operations.d.ts +8 -0
- package/dist/lib/ai-service/base-operations.d.ts.map +1 -1
- package/dist/lib/ai-service/base-operations.js +23 -10
- package/dist/lib/ai-service/model-provider.d.ts.map +1 -1
- package/dist/lib/ai-service/model-provider.js +37 -6
- package/dist/lib/ai-service/prd-operations.d.ts.map +1 -1
- package/dist/lib/ai-service/prd-operations.js +50 -7
- package/dist/lib/ai-service/task-operations.d.ts +1 -0
- package/dist/lib/ai-service/task-operations.d.ts.map +1 -1
- package/dist/lib/ai-service/task-operations.js +158 -171
- package/dist/lib/benchmark/registry.d.ts.map +1 -1
- package/dist/lib/benchmark/registry.js +6 -10
- package/dist/lib/config-validation.d.ts +215 -0
- package/dist/lib/config-validation.d.ts.map +1 -0
- package/dist/lib/config-validation.js +246 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +30 -7
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +2 -1
- package/dist/lib/storage/file-system.d.ts.map +1 -1
- package/dist/lib/storage/file-system.js +81 -21
- package/dist/lib/task-execution-core.d.ts.map +1 -1
- package/dist/lib/task-execution-core.js +3 -2
- package/dist/services/prd.d.ts +17 -0
- package/dist/services/prd.d.ts.map +1 -1
- package/dist/services/prd.js +69 -84
- package/dist/services/tasks.d.ts +315 -1
- package/dist/services/tasks.d.ts.map +1 -1
- package/dist/services/tasks.js +486 -121
- package/dist/services/workflow-ai-assistant.d.ts.map +1 -1
- package/dist/services/workflow-ai-assistant.js +19 -6
- package/dist/services/workflow.d.ts.map +1 -1
- package/dist/services/workflow.js +7 -1
- package/dist/test/lib/ai-service/task-operations.test.d.ts +2 -0
- package/dist/test/lib/ai-service/task-operations.test.d.ts.map +1 -0
- package/dist/test/lib/ai-service/task-operations.test.js +362 -0
- package/dist/test/mocks/mock-ai-operations.d.ts +15 -0
- package/dist/test/mocks/mock-ai-operations.d.ts.map +1 -0
- package/dist/test/mocks/mock-ai-operations.js +107 -0
- package/dist/test/mocks/mock-context-builder.d.ts +10 -0
- package/dist/test/mocks/mock-context-builder.d.ts.map +1 -0
- package/dist/test/mocks/mock-context-builder.js +81 -0
- package/dist/test/mocks/mock-model-provider.d.ts +7 -0
- package/dist/test/mocks/mock-model-provider.d.ts.map +1 -0
- package/dist/test/mocks/mock-model-provider.js +21 -0
- package/dist/test/mocks/mock-service-factory.d.ts +11 -0
- package/dist/test/mocks/mock-service-factory.d.ts.map +1 -0
- package/dist/test/mocks/mock-service-factory.js +61 -0
- package/dist/test/mocks/mock-storage.d.ts +50 -0
- package/dist/test/mocks/mock-storage.d.ts.map +1 -0
- package/dist/test/mocks/mock-storage.js +145 -0
- package/dist/test/services/task-service.test.d.ts +2 -0
- package/dist/test/services/task-service.test.d.ts.map +1 -0
- package/dist/test/services/task-service.test.js +459 -0
- package/dist/test/test-mock-setup.d.ts +26 -0
- package/dist/test/test-mock-setup.d.ts.map +1 -0
- package/dist/test/test-mock-setup.js +41 -0
- package/dist/test/test-setup.d.ts +9 -0
- package/dist/test/test-setup.d.ts.map +1 -0
- package/dist/test/test-setup.js +44 -0
- package/dist/test/test-utils.d.ts +22 -0
- package/dist/test/test-utils.d.ts.map +1 -0
- package/dist/test/test-utils.js +37 -0
- package/dist/test/utils/ai-operation-utility.test.d.ts +2 -0
- package/dist/test/utils/ai-operation-utility.test.d.ts.map +1 -0
- package/dist/test/utils/ai-operation-utility.test.js +290 -0
- package/dist/test/utils/error-handling.test.d.ts +2 -0
- package/dist/test/utils/error-handling.test.d.ts.map +1 -0
- package/dist/test/utils/error-handling.test.js +231 -0
- package/dist/utils/ai-operation-utility.d.ts +142 -0
- package/dist/utils/ai-operation-utility.d.ts.map +1 -0
- package/dist/utils/ai-operation-utility.js +279 -0
- package/dist/utils/ai-service-factory.d.ts +10 -0
- package/dist/utils/ai-service-factory.d.ts.map +1 -1
- package/dist/utils/ai-service-factory.js +19 -1
- package/dist/utils/cli-validators.d.ts +2 -2
- package/dist/utils/cli-validators.d.ts.map +1 -1
- package/dist/utils/cli-validators.js +7 -6
- package/dist/utils/error-utils.d.ts +3 -3
- package/dist/utils/error-utils.d.ts.map +1 -1
- package/dist/utils/error-utils.js +5 -4
- package/dist/utils/file-utils.d.ts +27 -4
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +46 -6
- package/dist/utils/id-generator.d.ts +1 -1
- package/dist/utils/id-generator.d.ts.map +1 -1
- package/dist/utils/id-generator.js +8 -2
- package/dist/utils/metadata-utils.d.ts +40 -0
- package/dist/utils/metadata-utils.d.ts.map +1 -0
- package/dist/utils/metadata-utils.js +43 -0
- package/dist/utils/model-executor-parser.d.ts +1 -1
- package/dist/utils/model-executor-parser.d.ts.map +1 -1
- package/dist/utils/model-executor-parser.js +3 -2
- package/dist/utils/storage-utils.d.ts +3 -3
- package/dist/utils/storage-utils.d.ts.map +1 -1
- package/dist/utils/storage-utils.js +7 -6
- package/dist/utils/task-o-matic-error.d.ts +206 -0
- package/dist/utils/task-o-matic-error.d.ts.map +1 -0
- package/dist/utils/task-o-matic-error.js +304 -0
- package/package.json +7 -2
package/dist/services/tasks.js
CHANGED
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.taskService = exports.TaskService = void 0;
|
|
37
|
+
exports.getTaskService = getTaskService;
|
|
37
38
|
const ai_service_factory_1 = require("../utils/ai-service-factory");
|
|
38
39
|
const stack_formatter_1 = require("../utils/stack-formatter");
|
|
39
40
|
const ai_config_builder_1 = require("../utils/ai-config-builder");
|
|
@@ -41,27 +42,128 @@ const hooks_1 = require("../lib/hooks");
|
|
|
41
42
|
const streaming_utils_1 = require("../utils/streaming-utils");
|
|
42
43
|
const error_utils_1 = require("../utils/error-utils");
|
|
43
44
|
const id_generator_1 = require("../utils/id-generator");
|
|
45
|
+
const task_o_matic_error_1 = require("../utils/task-o-matic-error");
|
|
46
|
+
const metadata_utils_1 = require("../utils/metadata-utils");
|
|
44
47
|
/**
|
|
45
48
|
* TaskService - Centralized business logic for all task operations
|
|
46
|
-
*
|
|
49
|
+
*
|
|
50
|
+
* This service provides a comprehensive API for task management with AI-powered features.
|
|
51
|
+
* It's framework-agnostic and can be used by CLI, TUI, or Web applications.
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* import { TaskService } from "task-o-matic";
|
|
56
|
+
*
|
|
57
|
+
* // Initialize with default configuration
|
|
58
|
+
* const taskService = new TaskService();
|
|
59
|
+
*
|
|
60
|
+
* // Create a task with AI enhancement
|
|
61
|
+
* const result = await taskService.createTask({
|
|
62
|
+
* title: "Implement feature",
|
|
63
|
+
* content: "Feature description",
|
|
64
|
+
* aiEnhance: true,
|
|
65
|
+
* aiOptions: {
|
|
66
|
+
* provider: "anthropic",
|
|
67
|
+
* model: "claude-3-5-sonnet"
|
|
68
|
+
* }
|
|
69
|
+
* });
|
|
70
|
+
*
|
|
71
|
+
* // Or inject dependencies for testing
|
|
72
|
+
* const taskService = new TaskService({
|
|
73
|
+
* storage: mockStorage,
|
|
74
|
+
* aiOperations: mockAI,
|
|
75
|
+
* });
|
|
76
|
+
* ```
|
|
47
77
|
*/
|
|
48
78
|
class TaskService {
|
|
79
|
+
storage;
|
|
80
|
+
aiOperations;
|
|
81
|
+
modelProvider;
|
|
82
|
+
contextBuilder;
|
|
83
|
+
hooks;
|
|
84
|
+
/**
|
|
85
|
+
* Create a new TaskService
|
|
86
|
+
*
|
|
87
|
+
* @param dependencies - Optional dependencies to inject (for testing)
|
|
88
|
+
*/
|
|
89
|
+
constructor(dependencies = {}) {
|
|
90
|
+
// Use injected dependencies or fall back to singletons
|
|
91
|
+
this.storage = dependencies.storage ?? (0, ai_service_factory_1.getStorage)();
|
|
92
|
+
this.aiOperations = dependencies.aiOperations ?? (0, ai_service_factory_1.getAIOperations)();
|
|
93
|
+
this.modelProvider = dependencies.modelProvider ?? (0, ai_service_factory_1.getModelProvider)();
|
|
94
|
+
this.contextBuilder = dependencies.contextBuilder ?? (0, ai_service_factory_1.getContextBuilder)();
|
|
95
|
+
this.hooks = dependencies.hooks ?? hooks_1.hooks;
|
|
96
|
+
}
|
|
49
97
|
// ============================================================================
|
|
50
98
|
// CORE CRUD OPERATIONS
|
|
51
99
|
// ============================================================================
|
|
100
|
+
/**
|
|
101
|
+
* Creates a new task with optional AI enhancement
|
|
102
|
+
*
|
|
103
|
+
* @param input - Task creation parameters
|
|
104
|
+
* @param input.title - Task title (required, 1-255 characters)
|
|
105
|
+
* @param input.content - Task content/description (optional)
|
|
106
|
+
* @param input.parentId - Parent task ID for creating subtasks
|
|
107
|
+
* @param input.effort - Estimated effort ("small" | "medium" | "large")
|
|
108
|
+
* @param input.aiEnhance - Enable AI enhancement with Context7 documentation
|
|
109
|
+
* @param input.aiOptions - AI configuration override
|
|
110
|
+
* @param input.streamingOptions - Real-time streaming options
|
|
111
|
+
*
|
|
112
|
+
* @returns Promise resolving to task creation result
|
|
113
|
+
*
|
|
114
|
+
* @throws {TaskOMaticError} If task creation fails (e.g., AI operation errors, storage errors)
|
|
115
|
+
* @throws {Error} If input validation fails
|
|
116
|
+
*
|
|
117
|
+
* @example Basic task creation
|
|
118
|
+
* ```typescript
|
|
119
|
+
* const task = await taskService.createTask({
|
|
120
|
+
* title: "Fix authentication bug",
|
|
121
|
+
* content: "Users cannot login with valid credentials",
|
|
122
|
+
* aiEnhance: false
|
|
123
|
+
* });
|
|
124
|
+
* ```
|
|
125
|
+
*
|
|
126
|
+
* @example Task with AI enhancement
|
|
127
|
+
* ```typescript
|
|
128
|
+
* try {
|
|
129
|
+
* const enhancedTask = await taskService.createTask({
|
|
130
|
+
* title: "Design authentication system",
|
|
131
|
+
* content: "Implement OAuth2 + JWT authentication",
|
|
132
|
+
* aiEnhance: true,
|
|
133
|
+
* streamingOptions: {
|
|
134
|
+
* onChunk: (chunk) => console.log("AI:", chunk)
|
|
135
|
+
* }
|
|
136
|
+
* });
|
|
137
|
+
* console.log("Enhanced content:", enhancedTask.task.content);
|
|
138
|
+
* } catch (error) {
|
|
139
|
+
* if (error instanceof TaskOMaticError) {
|
|
140
|
+
* console.error("AI enhancement failed:", error.getDetails());
|
|
141
|
+
* }
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @example Creating subtasks
|
|
146
|
+
* ```typescript
|
|
147
|
+
* const subtask = await taskService.createTask({
|
|
148
|
+
* title: "Implement OAuth2 flow",
|
|
149
|
+
* parentId: "1",
|
|
150
|
+
* effort: "medium"
|
|
151
|
+
* });
|
|
152
|
+
* ```
|
|
153
|
+
*/
|
|
52
154
|
async createTask(input) {
|
|
53
155
|
const startTime = Date.now();
|
|
54
156
|
let content = input.content;
|
|
55
157
|
let aiMetadata;
|
|
56
158
|
if (input.aiEnhance) {
|
|
57
|
-
|
|
159
|
+
this.hooks.emit("task:progress", {
|
|
58
160
|
message: "Building context for task...",
|
|
59
161
|
type: "progress",
|
|
60
162
|
});
|
|
61
163
|
// Build context with error handling (Bug fix 2.2)
|
|
62
164
|
let context;
|
|
63
165
|
try {
|
|
64
|
-
context = await
|
|
166
|
+
context = await this.contextBuilder.buildContextForNewTask(input.title, input.content);
|
|
65
167
|
}
|
|
66
168
|
catch (error) {
|
|
67
169
|
// Log warning but don't fail task creation
|
|
@@ -71,15 +173,15 @@ class TaskService {
|
|
|
71
173
|
}
|
|
72
174
|
const stackInfo = (0, stack_formatter_1.formatStackInfo)(context.stack);
|
|
73
175
|
const enhancementAIConfig = (0, ai_config_builder_1.buildAIConfig)(input.aiOptions);
|
|
74
|
-
|
|
176
|
+
this.hooks.emit("task:progress", {
|
|
75
177
|
message: "Enhancing task with AI documentation...",
|
|
76
178
|
type: "progress",
|
|
77
179
|
});
|
|
78
180
|
const taskDescription = input.content ?? "";
|
|
79
181
|
// Generate temporary ID for AI operations (Bug fix 2.6)
|
|
80
182
|
const tempTaskId = id_generator_1.TaskIDGenerator.generate();
|
|
81
|
-
content = await
|
|
82
|
-
const aiConfig =
|
|
183
|
+
content = await this.aiOperations.enhanceTaskWithDocumentation(tempTaskId, input.title, taskDescription, stackInfo, input.streamingOptions, undefined, enhancementAIConfig, context.existingResearch);
|
|
184
|
+
const aiConfig = this.modelProvider.getAIConfig();
|
|
83
185
|
aiMetadata = {
|
|
84
186
|
taskId: "",
|
|
85
187
|
aiGenerated: true,
|
|
@@ -90,27 +192,48 @@ class TaskService {
|
|
|
90
192
|
generatedAt: Date.now(),
|
|
91
193
|
};
|
|
92
194
|
}
|
|
93
|
-
|
|
195
|
+
this.hooks.emit("task:progress", {
|
|
94
196
|
message: "Saving task...",
|
|
95
197
|
type: "progress",
|
|
96
198
|
});
|
|
97
|
-
const task = await
|
|
199
|
+
const task = await this.storage.createTask({
|
|
98
200
|
title: input.title,
|
|
99
201
|
description: input.content ?? "",
|
|
100
202
|
content,
|
|
101
203
|
parentId: input.parentId,
|
|
102
204
|
estimatedEffort: input.effort,
|
|
103
205
|
}, aiMetadata);
|
|
104
|
-
|
|
206
|
+
this.hooks.emit("task:progress", {
|
|
105
207
|
type: "completed",
|
|
106
208
|
message: "Task created successfully",
|
|
107
209
|
});
|
|
108
210
|
// Emit task:created event
|
|
109
|
-
await
|
|
211
|
+
await this.hooks.emit("task:created", { task });
|
|
110
212
|
return { success: true, task, aiMetadata };
|
|
111
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* List tasks with optional filtering
|
|
216
|
+
*
|
|
217
|
+
* @param filters - Filter criteria
|
|
218
|
+
* @param filters.status - Filter by task status ("todo", "in-progress", "completed")
|
|
219
|
+
* @param filters.tag - Filter by task tag
|
|
220
|
+
*
|
|
221
|
+
* @returns Promise resolving to array of matching tasks
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```typescript
|
|
225
|
+
* // List all tasks
|
|
226
|
+
* const allTasks = await taskService.listTasks({});
|
|
227
|
+
*
|
|
228
|
+
* // List only completed tasks
|
|
229
|
+
* const completedTasks = await taskService.listTasks({ status: "completed" });
|
|
230
|
+
*
|
|
231
|
+
* // List tasks with specific tag
|
|
232
|
+
* const frontendTasks = await taskService.listTasks({ tag: "frontend" });
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
112
235
|
async listTasks(filters) {
|
|
113
|
-
const storage =
|
|
236
|
+
const storage = this.storage;
|
|
114
237
|
const topLevelTasks = await storage.getTopLevelTasks();
|
|
115
238
|
let filteredTasks = topLevelTasks;
|
|
116
239
|
if (filters.status) {
|
|
@@ -123,22 +246,22 @@ class TaskService {
|
|
|
123
246
|
return filteredTasks;
|
|
124
247
|
}
|
|
125
248
|
async getTask(id) {
|
|
126
|
-
return await
|
|
249
|
+
return await this.storage.getTask(id);
|
|
127
250
|
}
|
|
128
251
|
async getTaskContent(id) {
|
|
129
|
-
return await
|
|
252
|
+
return await this.storage.getTaskContent(id);
|
|
130
253
|
}
|
|
131
254
|
async getTaskAIMetadata(id) {
|
|
132
|
-
return await
|
|
255
|
+
return await this.storage.getTaskAIMetadata(id);
|
|
133
256
|
}
|
|
134
257
|
async getSubtasks(id) {
|
|
135
|
-
return await
|
|
258
|
+
return await this.storage.getSubtasks(id);
|
|
136
259
|
}
|
|
137
260
|
async updateTask(id, updates) {
|
|
138
|
-
const storage =
|
|
261
|
+
const storage = this.storage;
|
|
139
262
|
const existingTask = await storage.getTask(id);
|
|
140
263
|
if (!existingTask) {
|
|
141
|
-
throw
|
|
264
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(id);
|
|
142
265
|
}
|
|
143
266
|
// Validate status transitions
|
|
144
267
|
if (updates.status) {
|
|
@@ -148,7 +271,7 @@ class TaskService {
|
|
|
148
271
|
completed: ["todo", "in-progress"],
|
|
149
272
|
};
|
|
150
273
|
if (!validTransitions[existingTask.status].includes(updates.status)) {
|
|
151
|
-
throw
|
|
274
|
+
throw (0, task_o_matic_error_1.formatInvalidStatusTransitionError)(existingTask.status, updates.status);
|
|
152
275
|
}
|
|
153
276
|
}
|
|
154
277
|
const finalUpdates = { ...updates };
|
|
@@ -160,16 +283,16 @@ class TaskService {
|
|
|
160
283
|
}
|
|
161
284
|
const updatedTask = await storage.updateTask(id, finalUpdates);
|
|
162
285
|
if (!updatedTask) {
|
|
163
|
-
throw
|
|
286
|
+
throw (0, task_o_matic_error_1.formatStorageError)(`updateTask ${id}`);
|
|
164
287
|
}
|
|
165
288
|
// Emit task:updated event
|
|
166
|
-
await
|
|
289
|
+
await this.hooks.emit("task:updated", {
|
|
167
290
|
task: updatedTask,
|
|
168
291
|
changes: finalUpdates,
|
|
169
292
|
});
|
|
170
293
|
// Emit task:status-changed event if status changed
|
|
171
294
|
if (updates.status && updates.status !== existingTask.status) {
|
|
172
|
-
await
|
|
295
|
+
await this.hooks.emit("task:status-changed", {
|
|
173
296
|
task: updatedTask,
|
|
174
297
|
oldStatus: existingTask.status,
|
|
175
298
|
newStatus: updates.status,
|
|
@@ -182,10 +305,10 @@ class TaskService {
|
|
|
182
305
|
}
|
|
183
306
|
async deleteTask(id, options = {}) {
|
|
184
307
|
const { cascade, force } = options;
|
|
185
|
-
const storage =
|
|
308
|
+
const storage = this.storage;
|
|
186
309
|
const taskToDelete = await storage.getTask(id);
|
|
187
310
|
if (!taskToDelete) {
|
|
188
|
-
throw
|
|
311
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(id);
|
|
189
312
|
}
|
|
190
313
|
const deleted = [];
|
|
191
314
|
const orphanedSubtasks = [];
|
|
@@ -193,7 +316,15 @@ class TaskService {
|
|
|
193
316
|
const subtasks = await storage.getSubtasks(id);
|
|
194
317
|
if (subtasks.length > 0 && !cascade) {
|
|
195
318
|
if (!force) {
|
|
196
|
-
throw
|
|
319
|
+
throw (0, task_o_matic_error_1.createStandardError)(task_o_matic_error_1.TaskOMaticErrorCodes.TASK_OPERATION_FAILED, `Cannot delete task with ${subtasks.length} subtasks`, {
|
|
320
|
+
context: `Task ${id} has ${subtasks.length} subtasks`,
|
|
321
|
+
suggestions: [
|
|
322
|
+
"Use --cascade flag to delete task and all subtasks",
|
|
323
|
+
"Use --force flag to delete task and orphan subtasks",
|
|
324
|
+
"Delete subtasks first, then delete parent task",
|
|
325
|
+
],
|
|
326
|
+
metadata: { taskId: id, subtaskCount: subtasks.length },
|
|
327
|
+
});
|
|
197
328
|
}
|
|
198
329
|
// Orphan subtasks by removing parent reference
|
|
199
330
|
for (const subtask of subtasks) {
|
|
@@ -217,7 +348,7 @@ class TaskService {
|
|
|
217
348
|
if (success) {
|
|
218
349
|
deleted.push(taskToDelete);
|
|
219
350
|
// Emit task:deleted event
|
|
220
|
-
await
|
|
351
|
+
await this.hooks.emit("task:deleted", { taskId: id });
|
|
221
352
|
}
|
|
222
353
|
return { success: true, deleted, orphanedSubtasks };
|
|
223
354
|
}
|
|
@@ -225,10 +356,10 @@ class TaskService {
|
|
|
225
356
|
// TAG OPERATIONS
|
|
226
357
|
// ============================================================================
|
|
227
358
|
async addTags(id, tags) {
|
|
228
|
-
const storage =
|
|
359
|
+
const storage = this.storage;
|
|
229
360
|
const task = await storage.getTask(id);
|
|
230
361
|
if (!task) {
|
|
231
|
-
throw
|
|
362
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(id);
|
|
232
363
|
}
|
|
233
364
|
const existingTags = task.tags || [];
|
|
234
365
|
const newTags = tags.filter((tag) => !existingTags.includes(tag));
|
|
@@ -238,15 +369,15 @@ class TaskService {
|
|
|
238
369
|
const updatedTags = [...existingTags, ...newTags];
|
|
239
370
|
const updatedTask = await storage.updateTask(id, { tags: updatedTags });
|
|
240
371
|
if (!updatedTask) {
|
|
241
|
-
throw
|
|
372
|
+
throw (0, task_o_matic_error_1.formatStorageError)(`add tags to task ${id}`);
|
|
242
373
|
}
|
|
243
374
|
return updatedTask;
|
|
244
375
|
}
|
|
245
376
|
async removeTags(id, tags) {
|
|
246
|
-
const storage =
|
|
377
|
+
const storage = this.storage;
|
|
247
378
|
const task = await storage.getTask(id);
|
|
248
379
|
if (!task) {
|
|
249
|
-
throw
|
|
380
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(id);
|
|
250
381
|
}
|
|
251
382
|
const existingTags = task.tags || [];
|
|
252
383
|
const updatedTags = existingTags.filter((tag) => !tags.includes(tag));
|
|
@@ -255,15 +386,44 @@ class TaskService {
|
|
|
255
386
|
}
|
|
256
387
|
const updatedTask = await storage.updateTask(id, { tags: updatedTags });
|
|
257
388
|
if (!updatedTask) {
|
|
258
|
-
throw
|
|
389
|
+
throw (0, task_o_matic_error_1.formatStorageError)(`remove tags from task ${id}`);
|
|
259
390
|
}
|
|
260
391
|
return updatedTask;
|
|
261
392
|
}
|
|
262
393
|
// ============================================================================
|
|
263
394
|
// TASK NAVIGATION
|
|
264
395
|
// ============================================================================
|
|
396
|
+
/**
|
|
397
|
+
* Get the next task based on priority and filtering criteria
|
|
398
|
+
*
|
|
399
|
+
* @param filters - Filter and priority criteria
|
|
400
|
+
* @param filters.status - Filter by task status
|
|
401
|
+
* @param filters.tag - Filter by task tag
|
|
402
|
+
* @param filters.effort - Filter by estimated effort
|
|
403
|
+
* @param filters.priority - Priority strategy ("newest", "oldest", "effort", or default)
|
|
404
|
+
*
|
|
405
|
+
* @returns Promise resolving to the highest priority task or null if none found
|
|
406
|
+
*
|
|
407
|
+
* @example
|
|
408
|
+
* ```typescript
|
|
409
|
+
* // Get the next task by default priority (task ID order)
|
|
410
|
+
* const nextTask = await taskService.getNextTask({});
|
|
411
|
+
*
|
|
412
|
+
* // Get the newest task with "todo" status
|
|
413
|
+
* const newestTodo = await taskService.getNextTask({
|
|
414
|
+
* status: "todo",
|
|
415
|
+
* priority: "newest"
|
|
416
|
+
* });
|
|
417
|
+
*
|
|
418
|
+
* // Get the highest effort task with specific tag
|
|
419
|
+
* const highEffortTask = await taskService.getNextTask({
|
|
420
|
+
* tag: "backend",
|
|
421
|
+
* priority: "effort"
|
|
422
|
+
* });
|
|
423
|
+
* ```
|
|
424
|
+
*/
|
|
265
425
|
async getNextTask(filters) {
|
|
266
|
-
const storage =
|
|
426
|
+
const storage = this.storage;
|
|
267
427
|
const allTasks = await storage.getTasks();
|
|
268
428
|
// Filter by status and other criteria
|
|
269
429
|
let filteredTasks = allTasks.filter((task) => {
|
|
@@ -293,12 +453,12 @@ class TaskService {
|
|
|
293
453
|
}
|
|
294
454
|
}
|
|
295
455
|
async getTaskTree(rootId) {
|
|
296
|
-
const storage =
|
|
456
|
+
const storage = this.storage;
|
|
297
457
|
if (rootId) {
|
|
298
458
|
// Return tree starting from specific task
|
|
299
459
|
const rootTask = await storage.getTask(rootId);
|
|
300
460
|
if (!rootTask) {
|
|
301
|
-
throw
|
|
461
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(rootId);
|
|
302
462
|
}
|
|
303
463
|
// Get all subtasks recursively
|
|
304
464
|
const getAllSubtasks = async (task) => {
|
|
@@ -322,50 +482,85 @@ class TaskService {
|
|
|
322
482
|
// ============================================================================
|
|
323
483
|
// AI OPERATIONS
|
|
324
484
|
// ============================================================================
|
|
485
|
+
/**
|
|
486
|
+
* Enhance a task with AI-generated documentation using Context7
|
|
487
|
+
*
|
|
488
|
+
* Uses AI to enrich the task description with relevant documentation,
|
|
489
|
+
* code examples, and best practices from Context7 documentation sources.
|
|
490
|
+
*
|
|
491
|
+
* @param taskId - ID of the task to enhance
|
|
492
|
+
* @param aiOptions - Optional AI configuration overrides
|
|
493
|
+
* @param streamingOptions - Optional streaming callbacks for real-time feedback
|
|
494
|
+
* @returns Promise resolving to enhancement result with metrics
|
|
495
|
+
*
|
|
496
|
+
* @throws {Error} If task not found
|
|
497
|
+
* @throws {TaskOMaticError} If AI enhancement fails
|
|
498
|
+
*
|
|
499
|
+
* @example Basic enhancement
|
|
500
|
+
* ```typescript
|
|
501
|
+
* const result = await taskService.enhanceTask("1");
|
|
502
|
+
* console.log("Enhanced content:", result.enhancedContent);
|
|
503
|
+
* console.log("Took:", result.stats.duration, "ms");
|
|
504
|
+
* ```
|
|
505
|
+
*
|
|
506
|
+
* @example With streaming
|
|
507
|
+
* ```typescript
|
|
508
|
+
* try {
|
|
509
|
+
* const result = await taskService.enhanceTask("1", undefined, {
|
|
510
|
+
* onChunk: (chunk) => process.stdout.write(chunk)
|
|
511
|
+
* });
|
|
512
|
+
* console.log("\nEnhancement complete!");
|
|
513
|
+
* } catch (error) {
|
|
514
|
+
* if (error instanceof TaskOMaticError) {
|
|
515
|
+
* console.error("Enhancement failed:", error.getDetails());
|
|
516
|
+
* }
|
|
517
|
+
* }
|
|
518
|
+
* ```
|
|
519
|
+
*/
|
|
325
520
|
async enhanceTask(taskId, aiOptions, streamingOptions) {
|
|
326
521
|
const startTime = Date.now();
|
|
327
|
-
|
|
522
|
+
this.hooks.emit("task:progress", {
|
|
328
523
|
message: "Starting task enhancement...",
|
|
329
524
|
type: "started",
|
|
330
525
|
});
|
|
331
|
-
const task = await
|
|
526
|
+
const task = await this.storage.getTask(taskId);
|
|
332
527
|
if (!task) {
|
|
333
|
-
throw
|
|
528
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
334
529
|
}
|
|
335
|
-
|
|
530
|
+
this.hooks.emit("task:progress", {
|
|
336
531
|
message: "Building context...",
|
|
337
532
|
type: "progress",
|
|
338
533
|
});
|
|
339
|
-
const context = await
|
|
534
|
+
const context = await this.contextBuilder.buildContext(taskId);
|
|
340
535
|
const stackInfo = (0, stack_formatter_1.formatStackInfo)(context.stack);
|
|
341
536
|
const enhancementAIConfig = (0, ai_config_builder_1.buildAIConfig)(aiOptions);
|
|
342
|
-
|
|
537
|
+
this.hooks.emit("task:progress", {
|
|
343
538
|
message: "Calling AI for enhancement...",
|
|
344
539
|
type: "progress",
|
|
345
540
|
});
|
|
346
541
|
// Use utility to wrap streaming options and capture metrics (DRY fix 1.1)
|
|
347
542
|
const aiStartTime = Date.now();
|
|
348
543
|
const { options: metricsStreamingOptions, getMetrics } = (0, streaming_utils_1.createMetricsStreamingOptions)(streamingOptions, aiStartTime);
|
|
349
|
-
const enhancedContent = await
|
|
544
|
+
const enhancedContent = await this.aiOperations.enhanceTaskWithDocumentation(task.id, task.title, task.description ?? "", stackInfo, metricsStreamingOptions, undefined, enhancementAIConfig, context.existingResearch);
|
|
350
545
|
// Extract metrics after AI call
|
|
351
546
|
const { tokenUsage, timeToFirstToken } = getMetrics();
|
|
352
|
-
|
|
547
|
+
this.hooks.emit("task:progress", {
|
|
353
548
|
message: "Saving enhanced content...",
|
|
354
549
|
type: "progress",
|
|
355
550
|
});
|
|
356
551
|
const originalLength = task.description?.length || 0;
|
|
357
552
|
if (enhancedContent.length > 200) {
|
|
358
|
-
const contentFile = await
|
|
359
|
-
await
|
|
553
|
+
const contentFile = await this.storage.saveEnhancedTaskContent(task.id, enhancedContent);
|
|
554
|
+
await this.storage.updateTask(task.id, {
|
|
360
555
|
contentFile,
|
|
361
556
|
description: task.description +
|
|
362
557
|
"\n\n🤖 AI-enhanced with Context7 documentation available.",
|
|
363
558
|
});
|
|
364
559
|
}
|
|
365
560
|
else {
|
|
366
|
-
await
|
|
561
|
+
await this.storage.updateTask(task.id, { description: enhancedContent });
|
|
367
562
|
}
|
|
368
|
-
const aiConfig =
|
|
563
|
+
const aiConfig = this.modelProvider.getAIConfig();
|
|
369
564
|
const aiMetadata = {
|
|
370
565
|
taskId: task.id,
|
|
371
566
|
aiGenerated: true,
|
|
@@ -375,9 +570,9 @@ class TaskService {
|
|
|
375
570
|
aiModel: aiConfig.model,
|
|
376
571
|
enhancedAt: Date.now(),
|
|
377
572
|
};
|
|
378
|
-
await
|
|
573
|
+
await this.storage.saveTaskAIMetadata(aiMetadata);
|
|
379
574
|
const duration = Date.now() - startTime;
|
|
380
|
-
|
|
575
|
+
this.hooks.emit("task:progress", {
|
|
381
576
|
message: "Task enhancement completed",
|
|
382
577
|
type: "completed",
|
|
383
578
|
});
|
|
@@ -400,32 +595,86 @@ class TaskService {
|
|
|
400
595
|
},
|
|
401
596
|
};
|
|
402
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* Split a task into subtasks using AI
|
|
600
|
+
*
|
|
601
|
+
* Analyzes the task and breaks it down into smaller, actionable subtasks
|
|
602
|
+
* with estimated effort. Can optionally use filesystem tools to understand
|
|
603
|
+
* project structure when creating subtasks.
|
|
604
|
+
*
|
|
605
|
+
* @param taskId - ID of the task to split
|
|
606
|
+
* @param aiOptions - Optional AI configuration overrides
|
|
607
|
+
* @param promptOverride - Optional custom prompt
|
|
608
|
+
* @param messageOverride - Optional custom message
|
|
609
|
+
* @param streamingOptions - Optional streaming callbacks
|
|
610
|
+
* @param enableFilesystemTools - Enable filesystem analysis for context
|
|
611
|
+
* @returns Promise resolving to split result with created subtasks
|
|
612
|
+
*
|
|
613
|
+
* @throws {Error} If task not found or already has subtasks
|
|
614
|
+
* @throws {TaskOMaticError} If AI operation fails
|
|
615
|
+
*
|
|
616
|
+
* @example Basic task splitting
|
|
617
|
+
* ```typescript
|
|
618
|
+
* const result = await taskService.splitTask("1");
|
|
619
|
+
* console.log(`Created ${result.subtasks.length} subtasks`);
|
|
620
|
+
* result.subtasks.forEach(subtask => {
|
|
621
|
+
* console.log(`- ${subtask.title} (${subtask.estimatedEffort})`);
|
|
622
|
+
* });
|
|
623
|
+
* ```
|
|
624
|
+
*
|
|
625
|
+
* @example With filesystem tools for code analysis
|
|
626
|
+
* ```typescript
|
|
627
|
+
* try {
|
|
628
|
+
* const result = await taskService.splitTask(
|
|
629
|
+
* "1",
|
|
630
|
+
* undefined,
|
|
631
|
+
* undefined,
|
|
632
|
+
* undefined,
|
|
633
|
+
* { onChunk: (chunk) => console.log(chunk) },
|
|
634
|
+
* true // Enable filesystem tools
|
|
635
|
+
* );
|
|
636
|
+
* console.log("AI analyzed codebase to create subtasks");
|
|
637
|
+
* } catch (error) {
|
|
638
|
+
* if (error instanceof TaskOMaticError) {
|
|
639
|
+
* console.error("Split failed:", error.suggestions);
|
|
640
|
+
* }
|
|
641
|
+
* }
|
|
642
|
+
* ```
|
|
643
|
+
*/
|
|
403
644
|
async splitTask(taskId, aiOptions, promptOverride, messageOverride, streamingOptions, enableFilesystemTools) {
|
|
404
645
|
const startTime = Date.now();
|
|
405
|
-
|
|
646
|
+
this.hooks.emit("task:progress", {
|
|
406
647
|
message: "Starting task breakdown...",
|
|
407
648
|
type: "started",
|
|
408
649
|
});
|
|
409
|
-
const task = await
|
|
650
|
+
const task = await this.storage.getTask(taskId);
|
|
410
651
|
if (!task) {
|
|
411
|
-
throw
|
|
652
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
412
653
|
}
|
|
413
654
|
// Check if task already has subtasks
|
|
414
|
-
const existingSubtasks = await
|
|
655
|
+
const existingSubtasks = await this.storage.getSubtasks(taskId);
|
|
415
656
|
if (existingSubtasks.length > 0) {
|
|
416
|
-
throw
|
|
657
|
+
throw (0, task_o_matic_error_1.createStandardError)(task_o_matic_error_1.TaskOMaticErrorCodes.TASK_OPERATION_FAILED, `Task already has ${existingSubtasks.length} subtasks`, {
|
|
658
|
+
context: `Task "${task.title}" (${taskId}) already has subtasks`,
|
|
659
|
+
suggestions: [
|
|
660
|
+
"Use existing subtasks instead of splitting again",
|
|
661
|
+
"Delete existing subtasks first if you want to re-split",
|
|
662
|
+
"Consider editing existing subtasks instead",
|
|
663
|
+
],
|
|
664
|
+
metadata: { taskId, subtaskCount: existingSubtasks.length },
|
|
665
|
+
});
|
|
417
666
|
}
|
|
418
|
-
|
|
667
|
+
this.hooks.emit("task:progress", {
|
|
419
668
|
message: "Building context...",
|
|
420
669
|
type: "progress",
|
|
421
670
|
});
|
|
422
671
|
// Build comprehensive context
|
|
423
|
-
const context = await
|
|
672
|
+
const context = await this.contextBuilder.buildContext(taskId);
|
|
424
673
|
const stackInfo = (0, stack_formatter_1.formatStackInfo)(context.stack);
|
|
425
674
|
// Get full task content
|
|
426
675
|
const fullContent = context.task.fullContent || task.description || "";
|
|
427
676
|
const breakdownAIConfig = (0, ai_config_builder_1.buildAIConfig)(aiOptions);
|
|
428
|
-
|
|
677
|
+
this.hooks.emit("task:progress", {
|
|
429
678
|
message: "Calling AI to break down task...",
|
|
430
679
|
type: "progress",
|
|
431
680
|
});
|
|
@@ -433,20 +682,20 @@ class TaskService {
|
|
|
433
682
|
const aiStartTime = Date.now();
|
|
434
683
|
const { options: metricsStreamingOptions, getMetrics } = (0, streaming_utils_1.createMetricsStreamingOptions)(streamingOptions, aiStartTime);
|
|
435
684
|
// Use AI service to break down the task with enhanced context
|
|
436
|
-
const subtaskData = await
|
|
685
|
+
const subtaskData = await this.aiOperations.breakdownTask(task, breakdownAIConfig, promptOverride, messageOverride, metricsStreamingOptions, undefined, fullContent, stackInfo, existingSubtasks, enableFilesystemTools);
|
|
437
686
|
// Extract metrics after AI call
|
|
438
687
|
const { tokenUsage, timeToFirstToken } = getMetrics();
|
|
439
|
-
|
|
688
|
+
this.hooks.emit("task:progress", {
|
|
440
689
|
message: `Creating ${subtaskData.length} subtasks...`,
|
|
441
690
|
type: "progress",
|
|
442
691
|
});
|
|
443
692
|
// Create subtasks and save AI metadata for each (Bug fix 2.3)
|
|
444
693
|
const createdSubtasks = [];
|
|
445
|
-
const aiConfig =
|
|
694
|
+
const aiConfig = this.modelProvider.getAIConfig();
|
|
446
695
|
const splitTimestamp = Date.now();
|
|
447
696
|
for (let i = 0; i < subtaskData.length; i++) {
|
|
448
697
|
const subtask = subtaskData[i];
|
|
449
|
-
|
|
698
|
+
this.hooks.emit("task:progress", {
|
|
450
699
|
message: `Creating subtask ${i + 1}/${subtaskData.length}: ${subtask.title}`,
|
|
451
700
|
type: "progress",
|
|
452
701
|
});
|
|
@@ -459,13 +708,7 @@ class TaskService {
|
|
|
459
708
|
createdSubtasks.push(result.task);
|
|
460
709
|
// Save AI metadata for each subtask (Bug fix 2.3)
|
|
461
710
|
const subtaskMetadata = {
|
|
462
|
-
|
|
463
|
-
aiGenerated: true,
|
|
464
|
-
aiPrompt: promptOverride ||
|
|
465
|
-
"Split task into meaningful subtasks with full context and existing subtask awareness",
|
|
466
|
-
confidence: 0.9,
|
|
467
|
-
aiProvider: aiConfig.provider,
|
|
468
|
-
aiModel: aiConfig.model,
|
|
711
|
+
...(0, metadata_utils_1.createBaseAIMetadata)(result.task.id, aiConfig, promptOverride, "Split task into meaningful subtasks with full context and existing subtask awareness", 0.9),
|
|
469
712
|
splitAt: splitTimestamp,
|
|
470
713
|
parentTaskId: taskId,
|
|
471
714
|
subtaskIndex: i + 1,
|
|
@@ -474,19 +717,13 @@ class TaskService {
|
|
|
474
717
|
}
|
|
475
718
|
// Save AI metadata for parent task as well
|
|
476
719
|
const parentMetadata = {
|
|
477
|
-
|
|
478
|
-
aiGenerated: true,
|
|
479
|
-
aiPrompt: promptOverride ||
|
|
480
|
-
"Split task into meaningful subtasks with full context and existing subtask awareness",
|
|
481
|
-
confidence: 0.9,
|
|
482
|
-
aiProvider: aiConfig.provider,
|
|
483
|
-
aiModel: aiConfig.model,
|
|
720
|
+
...(0, metadata_utils_1.createBaseAIMetadata)(task.id, aiConfig, promptOverride, "Split task into meaningful subtasks with full context and existing subtask awareness", 0.9),
|
|
484
721
|
splitAt: splitTimestamp,
|
|
485
722
|
subtasksCreated: createdSubtasks.length,
|
|
486
723
|
};
|
|
487
724
|
await (0, ai_service_factory_1.getStorage)().saveTaskAIMetadata(parentMetadata);
|
|
488
725
|
const duration = Date.now() - startTime;
|
|
489
|
-
|
|
726
|
+
this.hooks.emit("task:progress", {
|
|
490
727
|
message: `Task split into ${createdSubtasks.length} subtasks`,
|
|
491
728
|
type: "completed",
|
|
492
729
|
});
|
|
@@ -508,19 +745,57 @@ class TaskService {
|
|
|
508
745
|
},
|
|
509
746
|
};
|
|
510
747
|
}
|
|
748
|
+
/**
|
|
749
|
+
* Analyze and fetch documentation for a task using Context7
|
|
750
|
+
*
|
|
751
|
+
* Analyzes the task content to identify required libraries and documentation,
|
|
752
|
+
* then fetches relevant documentation from Context7. Caches documentation
|
|
753
|
+
* for future use.
|
|
754
|
+
*
|
|
755
|
+
* @param taskId - ID of the task to document
|
|
756
|
+
* @param force - Force re-fetch even if documentation exists
|
|
757
|
+
* @param aiOptions - Optional AI configuration overrides
|
|
758
|
+
* @param streamingOptions - Optional streaming callbacks
|
|
759
|
+
* @returns Promise resolving to documentation analysis result
|
|
760
|
+
*
|
|
761
|
+
* @throws {Error} If task not found or content is empty
|
|
762
|
+
* @throws {TaskOMaticError} If AI operation fails
|
|
763
|
+
*
|
|
764
|
+
* @example Analyze documentation needs
|
|
765
|
+
* ```typescript
|
|
766
|
+
* const result = await taskService.documentTask("1");
|
|
767
|
+
* if (result.documentation) {
|
|
768
|
+
* console.log("Documentation fetched:");
|
|
769
|
+
* console.log(result.documentation.recap);
|
|
770
|
+
* console.log("Libraries:", result.documentation.libraries);
|
|
771
|
+
* }
|
|
772
|
+
* ```
|
|
773
|
+
*
|
|
774
|
+
* @example Force refresh documentation
|
|
775
|
+
* ```typescript
|
|
776
|
+
* try {
|
|
777
|
+
* const result = await taskService.documentTask("1", true);
|
|
778
|
+
* console.log(`Analyzed ${result.analysis.libraries.length} libraries`);
|
|
779
|
+
* } catch (error) {
|
|
780
|
+
* if (error instanceof TaskOMaticError) {
|
|
781
|
+
* console.error("Documentation fetch failed:", error.getDetails());
|
|
782
|
+
* }
|
|
783
|
+
* }
|
|
784
|
+
* ```
|
|
785
|
+
*/
|
|
511
786
|
async documentTask(taskId, force = false, aiOptions, streamingOptions) {
|
|
512
787
|
const startTime = Date.now();
|
|
513
|
-
|
|
788
|
+
this.hooks.emit("task:progress", {
|
|
514
789
|
message: "Analyzing documentation needs...",
|
|
515
790
|
type: "started",
|
|
516
791
|
});
|
|
517
|
-
const task = await
|
|
792
|
+
const task = await this.storage.getTask(taskId);
|
|
518
793
|
if (!task) {
|
|
519
|
-
throw
|
|
794
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
520
795
|
}
|
|
521
796
|
if (task.documentation && !force) {
|
|
522
|
-
if (
|
|
523
|
-
|
|
797
|
+
if (this.contextBuilder.isDocumentationFresh(task.documentation)) {
|
|
798
|
+
this.hooks.emit("task:progress", {
|
|
524
799
|
message: "Documentation is fresh, skipping analysis",
|
|
525
800
|
type: "info",
|
|
526
801
|
});
|
|
@@ -534,38 +809,45 @@ class TaskService {
|
|
|
534
809
|
};
|
|
535
810
|
}
|
|
536
811
|
}
|
|
537
|
-
|
|
812
|
+
this.hooks.emit("task:progress", {
|
|
538
813
|
message: "Building context...",
|
|
539
814
|
type: "progress",
|
|
540
815
|
});
|
|
541
|
-
const context = await
|
|
816
|
+
const context = await this.contextBuilder.buildContext(taskId);
|
|
542
817
|
const stackInfo = (0, stack_formatter_1.formatStackInfo)(context.stack);
|
|
543
818
|
const analysisAIConfig = (0, ai_config_builder_1.buildAIConfig)(aiOptions);
|
|
544
819
|
// Get full task content
|
|
545
820
|
const fullContent = context.task.fullContent || task.description;
|
|
546
821
|
if (!fullContent) {
|
|
547
|
-
throw
|
|
822
|
+
throw (0, task_o_matic_error_1.createStandardError)(task_o_matic_error_1.TaskOMaticErrorCodes.INVALID_INPUT, "Task content is empty", {
|
|
823
|
+
context: `Task ${taskId} has no content to enhance`,
|
|
824
|
+
suggestions: [
|
|
825
|
+
"Add content to the task before enhancing",
|
|
826
|
+
"Provide task description or details",
|
|
827
|
+
],
|
|
828
|
+
metadata: { taskId },
|
|
829
|
+
});
|
|
548
830
|
}
|
|
549
831
|
// Get existing documentations from all tasks
|
|
550
|
-
const tasks = await
|
|
832
|
+
const tasks = await this.storage.getTasks();
|
|
551
833
|
const documentations = tasks.map((task) => task.documentation);
|
|
552
|
-
|
|
834
|
+
this.hooks.emit("task:progress", {
|
|
553
835
|
message: "Calling AI to analyze documentation needs...",
|
|
554
836
|
type: "progress",
|
|
555
837
|
});
|
|
556
838
|
// First analyze what documentation is needed
|
|
557
|
-
const analysis = await
|
|
839
|
+
const analysis = await this.aiOperations.analyzeDocumentationNeeds(task.id, task.title, fullContent, stackInfo, streamingOptions, undefined, analysisAIConfig, documentations);
|
|
558
840
|
let documentation;
|
|
559
841
|
if (analysis.libraries.length > 0) {
|
|
560
|
-
|
|
842
|
+
this.hooks.emit("task:progress", {
|
|
561
843
|
message: `Fetching documentation for ${analysis.libraries.length} libraries...`,
|
|
562
844
|
type: "progress",
|
|
563
845
|
});
|
|
564
846
|
// Build research object from actual libraries
|
|
565
847
|
const research = {};
|
|
566
848
|
for (const lib of analysis.libraries) {
|
|
567
|
-
const sanitizedLibrary =
|
|
568
|
-
const sanitizedQuery =
|
|
849
|
+
const sanitizedLibrary = this.storage.sanitizeForFilename(lib.name);
|
|
850
|
+
const sanitizedQuery = this.storage.sanitizeForFilename(lib.searchQuery);
|
|
569
851
|
const docFile = `docs/${sanitizedLibrary}/${sanitizedQuery}.md`;
|
|
570
852
|
if (!research[lib.name]) {
|
|
571
853
|
research[lib.name] = [];
|
|
@@ -575,11 +857,11 @@ class TaskService {
|
|
|
575
857
|
doc: docFile,
|
|
576
858
|
});
|
|
577
859
|
}
|
|
578
|
-
|
|
860
|
+
this.hooks.emit("task:progress", {
|
|
579
861
|
message: "Generating documentation recap...",
|
|
580
862
|
type: "progress",
|
|
581
863
|
});
|
|
582
|
-
const recap = await
|
|
864
|
+
const recap = await this.aiOperations.generateDocumentationRecap(analysis.libraries, analysis.toolResults?.map((tr) => ({
|
|
583
865
|
library: tr.toolName,
|
|
584
866
|
content: JSON.stringify(tr.output),
|
|
585
867
|
})) || [], streamingOptions);
|
|
@@ -590,14 +872,14 @@ class TaskService {
|
|
|
590
872
|
files: analysis.files || [],
|
|
591
873
|
research,
|
|
592
874
|
};
|
|
593
|
-
|
|
875
|
+
this.hooks.emit("task:progress", {
|
|
594
876
|
message: "Saving documentation...",
|
|
595
877
|
type: "progress",
|
|
596
878
|
});
|
|
597
|
-
await
|
|
879
|
+
await this.storage.updateTask(taskId, { documentation });
|
|
598
880
|
}
|
|
599
881
|
const duration = Date.now() - startTime;
|
|
600
|
-
|
|
882
|
+
this.hooks.emit("task:progress", {
|
|
601
883
|
message: "Documentation analysis completed",
|
|
602
884
|
type: "completed",
|
|
603
885
|
});
|
|
@@ -611,19 +893,59 @@ class TaskService {
|
|
|
611
893
|
},
|
|
612
894
|
};
|
|
613
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Generate an implementation plan for a task using AI
|
|
898
|
+
*
|
|
899
|
+
* Creates a detailed implementation plan with steps, considerations,
|
|
900
|
+
* and technical approach. Uses filesystem and Context7 tools to understand
|
|
901
|
+
* the project context and provide relevant suggestions.
|
|
902
|
+
*
|
|
903
|
+
* @param taskId - ID of the task to plan
|
|
904
|
+
* @param aiOptions - Optional AI configuration overrides
|
|
905
|
+
* @param streamingOptions - Optional streaming callbacks
|
|
906
|
+
* @returns Promise resolving to plan result with generated plan text
|
|
907
|
+
*
|
|
908
|
+
* @throws {Error} If task not found
|
|
909
|
+
* @throws {TaskOMaticError} If AI operation fails
|
|
910
|
+
*
|
|
911
|
+
* @example Basic implementation planning
|
|
912
|
+
* ```typescript
|
|
913
|
+
* const result = await taskService.planTask("1");
|
|
914
|
+
* console.log("Implementation Plan:");
|
|
915
|
+
* console.log(result.plan);
|
|
916
|
+
* console.log(`Generated in ${result.stats.duration}ms`);
|
|
917
|
+
* ```
|
|
918
|
+
*
|
|
919
|
+
* @example With streaming for real-time plan generation
|
|
920
|
+
* ```typescript
|
|
921
|
+
* try {
|
|
922
|
+
* const result = await taskService.planTask("1", undefined, {
|
|
923
|
+
* onChunk: (chunk) => {
|
|
924
|
+
* // Display plan as it's generated
|
|
925
|
+
* process.stdout.write(chunk);
|
|
926
|
+
* }
|
|
927
|
+
* });
|
|
928
|
+
* console.log("\n\nPlan saved to:", `plans/${result.task.id}.md`);
|
|
929
|
+
* } catch (error) {
|
|
930
|
+
* if (error instanceof TaskOMaticError) {
|
|
931
|
+
* console.error("Planning failed:", error.getDetails());
|
|
932
|
+
* }
|
|
933
|
+
* }
|
|
934
|
+
* ```
|
|
935
|
+
*/
|
|
614
936
|
async planTask(taskId, aiOptions, streamingOptions) {
|
|
615
937
|
const startTime = Date.now();
|
|
616
|
-
|
|
938
|
+
this.hooks.emit("task:progress", {
|
|
617
939
|
message: "Creating implementation plan...",
|
|
618
940
|
type: "started",
|
|
619
941
|
});
|
|
620
|
-
const task = await
|
|
942
|
+
const task = await this.storage.getTask(taskId);
|
|
621
943
|
if (!task) {
|
|
622
|
-
throw
|
|
944
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
623
945
|
}
|
|
624
|
-
const aiService =
|
|
946
|
+
const aiService = this.aiOperations;
|
|
625
947
|
const planAIConfig = (0, ai_config_builder_1.buildAIConfig)(aiOptions);
|
|
626
|
-
|
|
948
|
+
this.hooks.emit("task:progress", {
|
|
627
949
|
message: "Building task context...",
|
|
628
950
|
type: "progress",
|
|
629
951
|
});
|
|
@@ -647,7 +969,7 @@ class TaskService {
|
|
|
647
969
|
taskDetails += ` ${subtask.description || "No description"}\n\n`;
|
|
648
970
|
});
|
|
649
971
|
}
|
|
650
|
-
|
|
972
|
+
this.hooks.emit("task:progress", {
|
|
651
973
|
message: "Calling AI to create plan...",
|
|
652
974
|
type: "progress",
|
|
653
975
|
});
|
|
@@ -657,18 +979,18 @@ class TaskService {
|
|
|
657
979
|
const plan = await aiService.planTask(taskContext, taskDetails, planAIConfig, undefined, undefined, metricsStreamingOptions);
|
|
658
980
|
// Extract metrics after AI call
|
|
659
981
|
const { tokenUsage, timeToFirstToken } = getMetrics();
|
|
660
|
-
|
|
982
|
+
this.hooks.emit("task:progress", {
|
|
661
983
|
message: "Saving plan...",
|
|
662
984
|
type: "progress",
|
|
663
985
|
});
|
|
664
986
|
// Save the plan to storage
|
|
665
|
-
await
|
|
987
|
+
await this.storage.savePlan(taskId, plan);
|
|
666
988
|
const duration = Date.now() - startTime;
|
|
667
|
-
|
|
989
|
+
this.hooks.emit("task:progress", {
|
|
668
990
|
message: "Implementation plan created",
|
|
669
991
|
type: "completed",
|
|
670
992
|
});
|
|
671
|
-
const aiConfig =
|
|
993
|
+
const aiConfig = this.modelProvider.getAIConfig();
|
|
672
994
|
return {
|
|
673
995
|
success: true,
|
|
674
996
|
task,
|
|
@@ -689,36 +1011,48 @@ class TaskService {
|
|
|
689
1011
|
// DOCUMENTATION OPERATIONS
|
|
690
1012
|
// ============================================================================
|
|
691
1013
|
async getTaskDocumentation(taskId) {
|
|
692
|
-
return
|
|
1014
|
+
return this.storage.getTaskDocumentation(taskId);
|
|
693
1015
|
}
|
|
694
1016
|
async addTaskDocumentationFromFile(taskId, filePath) {
|
|
695
1017
|
const task = await this.getTask(taskId);
|
|
696
1018
|
if (!task) {
|
|
697
|
-
throw
|
|
1019
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
698
1020
|
}
|
|
699
1021
|
try {
|
|
700
1022
|
const { readFileSync, existsSync } = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
701
1023
|
const { resolve } = await Promise.resolve().then(() => __importStar(require("path")));
|
|
702
1024
|
const resolvedPath = resolve(filePath);
|
|
703
1025
|
if (!existsSync(resolvedPath)) {
|
|
704
|
-
throw
|
|
1026
|
+
throw (0, task_o_matic_error_1.createStandardError)(task_o_matic_error_1.TaskOMaticErrorCodes.STORAGE_ERROR, `Documentation file not found: ${filePath}`, {
|
|
1027
|
+
context: `Tried to load documentation from ${resolvedPath}`,
|
|
1028
|
+
suggestions: [
|
|
1029
|
+
"Check that the file path is correct",
|
|
1030
|
+
"Ensure the file exists",
|
|
1031
|
+
"Use an absolute path or path relative to current directory",
|
|
1032
|
+
],
|
|
1033
|
+
metadata: { filePath, resolvedPath },
|
|
1034
|
+
});
|
|
705
1035
|
}
|
|
706
1036
|
const content = readFileSync(resolvedPath, "utf-8");
|
|
707
|
-
const savedPath = await
|
|
1037
|
+
const savedPath = await this.storage.saveTaskDocumentation(taskId, content);
|
|
708
1038
|
return {
|
|
709
1039
|
filePath: savedPath,
|
|
710
1040
|
task,
|
|
711
1041
|
};
|
|
712
1042
|
}
|
|
713
1043
|
catch (error) {
|
|
714
|
-
//
|
|
715
|
-
|
|
1044
|
+
// Re-throw if already a TaskOMaticError
|
|
1045
|
+
if (error instanceof task_o_matic_error_1.TaskOMaticError) {
|
|
1046
|
+
throw error;
|
|
1047
|
+
}
|
|
1048
|
+
// Wrap other errors
|
|
1049
|
+
throw (0, task_o_matic_error_1.formatStorageError)("saveTaskDocumentation", error instanceof Error ? error : undefined);
|
|
716
1050
|
}
|
|
717
1051
|
}
|
|
718
1052
|
async setTaskPlan(taskId, planText, planFilePath) {
|
|
719
1053
|
const task = await this.getTask(taskId);
|
|
720
1054
|
if (!task) {
|
|
721
|
-
throw
|
|
1055
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
722
1056
|
}
|
|
723
1057
|
let plan;
|
|
724
1058
|
if (planFilePath) {
|
|
@@ -727,22 +1061,41 @@ class TaskService {
|
|
|
727
1061
|
const { resolve } = await Promise.resolve().then(() => __importStar(require("path")));
|
|
728
1062
|
const resolvedPath = resolve(planFilePath);
|
|
729
1063
|
if (!existsSync(resolvedPath)) {
|
|
730
|
-
throw
|
|
1064
|
+
throw (0, task_o_matic_error_1.createStandardError)(task_o_matic_error_1.TaskOMaticErrorCodes.STORAGE_ERROR, `Plan file not found: ${planFilePath}`, {
|
|
1065
|
+
context: `Tried to load plan from ${resolvedPath}`,
|
|
1066
|
+
suggestions: [
|
|
1067
|
+
"Check that the file path is correct",
|
|
1068
|
+
"Ensure the file exists",
|
|
1069
|
+
"Use an absolute path or path relative to current directory",
|
|
1070
|
+
],
|
|
1071
|
+
metadata: { planFilePath, resolvedPath },
|
|
1072
|
+
});
|
|
731
1073
|
}
|
|
732
1074
|
plan = readFileSync(resolvedPath, "utf-8");
|
|
733
1075
|
}
|
|
734
1076
|
catch (error) {
|
|
735
|
-
//
|
|
736
|
-
|
|
1077
|
+
// Re-throw if already a TaskOMaticError
|
|
1078
|
+
if (error instanceof task_o_matic_error_1.TaskOMaticError) {
|
|
1079
|
+
throw error;
|
|
1080
|
+
}
|
|
1081
|
+
// Wrap other errors
|
|
1082
|
+
throw (0, task_o_matic_error_1.formatStorageError)("readFileSync", error instanceof Error ? error : undefined);
|
|
737
1083
|
}
|
|
738
1084
|
}
|
|
739
1085
|
else if (planText) {
|
|
740
1086
|
plan = planText;
|
|
741
1087
|
}
|
|
742
1088
|
else {
|
|
743
|
-
throw
|
|
1089
|
+
throw (0, task_o_matic_error_1.createStandardError)(task_o_matic_error_1.TaskOMaticErrorCodes.INVALID_INPUT, "Either planText or planFilePath must be provided", {
|
|
1090
|
+
context: "setTaskPlan requires either planText or planFilePath parameter",
|
|
1091
|
+
suggestions: [
|
|
1092
|
+
"Provide planText parameter with the plan content",
|
|
1093
|
+
"Provide planFilePath parameter with path to plan file",
|
|
1094
|
+
],
|
|
1095
|
+
metadata: { taskId },
|
|
1096
|
+
});
|
|
744
1097
|
}
|
|
745
|
-
await
|
|
1098
|
+
await this.storage.savePlan(taskId, plan);
|
|
746
1099
|
const planFile = `plans/${taskId}.md`;
|
|
747
1100
|
return {
|
|
748
1101
|
planFile,
|
|
@@ -753,15 +1106,27 @@ class TaskService {
|
|
|
753
1106
|
// PLAN OPERATIONS
|
|
754
1107
|
// ============================================================================
|
|
755
1108
|
async getTaskPlan(taskId) {
|
|
756
|
-
return
|
|
1109
|
+
return this.storage.getPlan(taskId);
|
|
757
1110
|
}
|
|
758
1111
|
async listTaskPlans() {
|
|
759
|
-
return
|
|
1112
|
+
return this.storage.listPlans();
|
|
760
1113
|
}
|
|
761
1114
|
async deleteTaskPlan(taskId) {
|
|
762
|
-
return
|
|
1115
|
+
return this.storage.deletePlan(taskId);
|
|
763
1116
|
}
|
|
764
1117
|
}
|
|
765
1118
|
exports.TaskService = TaskService;
|
|
766
|
-
//
|
|
767
|
-
|
|
1119
|
+
// Lazy singleton instance - only created when first accessed
|
|
1120
|
+
let taskServiceInstance;
|
|
1121
|
+
function getTaskService() {
|
|
1122
|
+
if (!taskServiceInstance) {
|
|
1123
|
+
taskServiceInstance = new TaskService();
|
|
1124
|
+
}
|
|
1125
|
+
return taskServiceInstance;
|
|
1126
|
+
}
|
|
1127
|
+
// Backward compatibility: export as const but use getter
|
|
1128
|
+
exports.taskService = new Proxy({}, {
|
|
1129
|
+
get(target, prop) {
|
|
1130
|
+
return getTaskService()[prop];
|
|
1131
|
+
},
|
|
1132
|
+
});
|