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