task-o-matic 0.0.13 → 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 +60 -12
- 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 +5 -4
- package/dist/commands/tasks/document/get.d.ts.map +1 -1
- package/dist/commands/tasks/document/get.js +2 -1
- package/dist/commands/tasks/list.js +2 -2
- package/dist/commands/tasks/next.js +4 -4
- 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 +4 -3
- package/dist/commands/tasks/update.d.ts.map +1 -1
- package/dist/commands/tasks/update.js +7 -1
- package/dist/lib/ai-service/ai-operations.d.ts +1 -1
- package/dist/lib/ai-service/ai-operations.d.ts.map +1 -1
- package/dist/lib/ai-service/base-operations.d.ts +22 -0
- package/dist/lib/ai-service/base-operations.d.ts.map +1 -1
- package/dist/lib/ai-service/base-operations.js +29 -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 +2 -1
- 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/better-t-stack-cli.d.ts +36 -21
- package/dist/lib/better-t-stack-cli.d.ts.map +1 -1
- package/dist/lib/better-t-stack-cli.js +212 -33
- package/dist/lib/bootstrap/cli-bootstrap.d.ts +14 -0
- package/dist/lib/bootstrap/cli-bootstrap.d.ts.map +1 -0
- package/dist/lib/bootstrap/cli-bootstrap.js +325 -0
- package/dist/lib/bootstrap/index.d.ts +4 -0
- package/dist/lib/bootstrap/index.d.ts.map +1 -0
- package/dist/lib/bootstrap/index.js +19 -0
- package/dist/lib/bootstrap/medusa-bootstrap.d.ts +14 -0
- package/dist/lib/bootstrap/medusa-bootstrap.d.ts.map +1 -0
- package/dist/lib/bootstrap/medusa-bootstrap.js +218 -0
- package/dist/lib/bootstrap/opentui-bootstrap.d.ts +11 -0
- package/dist/lib/bootstrap/opentui-bootstrap.d.ts.map +1 -0
- package/dist/lib/bootstrap/opentui-bootstrap.js +342 -0
- 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 +14 -0
- package/dist/lib/config.d.ts.map +1 -1
- package/dist/lib/config.js +37 -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 +67 -60
- package/dist/services/tasks.d.ts +317 -3
- package/dist/services/tasks.d.ts.map +1 -1
- package/dist/services/tasks.js +531 -185
- 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/types/index.d.ts +36 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/results.d.ts +60 -6
- package/dist/types/results.d.ts.map +1 -1
- 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 +70 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/error-utils.js +104 -0
- package/dist/utils/file-utils.d.ts +49 -0
- package/dist/utils/file-utils.d.ts.map +1 -0
- package/dist/utils/file-utils.js +82 -0
- package/dist/utils/id-generator.d.ts +92 -0
- package/dist/utils/id-generator.d.ts.map +1 -0
- package/dist/utils/id-generator.js +146 -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/stack-formatter.d.ts +2 -1
- package/dist/utils/stack-formatter.d.ts.map +1 -1
- package/dist/utils/stack-formatter.js +8 -2
- package/dist/utils/storage-utils.d.ts +49 -0
- package/dist/utils/storage-utils.d.ts.map +1 -0
- package/dist/utils/storage-utils.js +80 -0
- package/dist/utils/streaming-utils.d.ts +38 -0
- package/dist/utils/streaming-utils.d.ts.map +1 -0
- package/dist/utils/streaming-utils.js +56 -0
- 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/docs/agents/cli.md +58 -149
- package/package.json +2 -2
package/dist/services/tasks.js
CHANGED
|
@@ -34,37 +34,153 @@ 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");
|
|
40
41
|
const hooks_1 = require("../lib/hooks");
|
|
42
|
+
const streaming_utils_1 = require("../utils/streaming-utils");
|
|
43
|
+
const error_utils_1 = require("../utils/error-utils");
|
|
44
|
+
const id_generator_1 = require("../utils/id-generator");
|
|
45
|
+
const task_o_matic_error_1 = require("../utils/task-o-matic-error");
|
|
41
46
|
/**
|
|
42
47
|
* TaskService - Centralized business logic for all task operations
|
|
43
|
-
*
|
|
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
|
+
* ```
|
|
44
76
|
*/
|
|
45
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
|
+
}
|
|
46
96
|
// ============================================================================
|
|
47
97
|
// CORE CRUD OPERATIONS
|
|
48
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
|
+
*/
|
|
49
153
|
async createTask(input) {
|
|
50
154
|
const startTime = Date.now();
|
|
51
155
|
let content = input.content;
|
|
52
156
|
let aiMetadata;
|
|
53
157
|
if (input.aiEnhance) {
|
|
54
|
-
|
|
158
|
+
this.hooks.emit("task:progress", {
|
|
55
159
|
message: "Building context for task...",
|
|
56
160
|
type: "progress",
|
|
57
161
|
});
|
|
58
|
-
|
|
162
|
+
// Build context with error handling (Bug fix 2.2)
|
|
163
|
+
let context;
|
|
164
|
+
try {
|
|
165
|
+
context = await this.contextBuilder.buildContextForNewTask(input.title, input.content);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
// Log warning but don't fail task creation
|
|
169
|
+
console.warn("Warning: Could not build context:", (0, error_utils_1.getErrorMessage)(error));
|
|
170
|
+
// Continue with empty context
|
|
171
|
+
context = { stack: undefined, existingResearch: {} };
|
|
172
|
+
}
|
|
59
173
|
const stackInfo = (0, stack_formatter_1.formatStackInfo)(context.stack);
|
|
60
174
|
const enhancementAIConfig = (0, ai_config_builder_1.buildAIConfig)(input.aiOptions);
|
|
61
|
-
|
|
175
|
+
this.hooks.emit("task:progress", {
|
|
62
176
|
message: "Enhancing task with AI documentation...",
|
|
63
177
|
type: "progress",
|
|
64
178
|
});
|
|
65
179
|
const taskDescription = input.content ?? "";
|
|
66
|
-
|
|
67
|
-
const
|
|
180
|
+
// Generate temporary ID for AI operations (Bug fix 2.6)
|
|
181
|
+
const tempTaskId = id_generator_1.TaskIDGenerator.generate();
|
|
182
|
+
content = await this.aiOperations.enhanceTaskWithDocumentation(tempTaskId, input.title, taskDescription, stackInfo, input.streamingOptions, undefined, enhancementAIConfig, context.existingResearch);
|
|
183
|
+
const aiConfig = this.modelProvider.getAIConfig();
|
|
68
184
|
aiMetadata = {
|
|
69
185
|
taskId: "",
|
|
70
186
|
aiGenerated: true,
|
|
@@ -75,27 +191,48 @@ class TaskService {
|
|
|
75
191
|
generatedAt: Date.now(),
|
|
76
192
|
};
|
|
77
193
|
}
|
|
78
|
-
|
|
194
|
+
this.hooks.emit("task:progress", {
|
|
79
195
|
message: "Saving task...",
|
|
80
196
|
type: "progress",
|
|
81
197
|
});
|
|
82
|
-
const task = await
|
|
198
|
+
const task = await this.storage.createTask({
|
|
83
199
|
title: input.title,
|
|
84
200
|
description: input.content ?? "",
|
|
85
201
|
content,
|
|
86
202
|
parentId: input.parentId,
|
|
87
203
|
estimatedEffort: input.effort,
|
|
88
204
|
}, aiMetadata);
|
|
89
|
-
|
|
205
|
+
this.hooks.emit("task:progress", {
|
|
90
206
|
type: "completed",
|
|
91
207
|
message: "Task created successfully",
|
|
92
208
|
});
|
|
93
209
|
// Emit task:created event
|
|
94
|
-
await
|
|
210
|
+
await this.hooks.emit("task:created", { task });
|
|
95
211
|
return { success: true, task, aiMetadata };
|
|
96
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
|
+
*/
|
|
97
234
|
async listTasks(filters) {
|
|
98
|
-
const storage =
|
|
235
|
+
const storage = this.storage;
|
|
99
236
|
const topLevelTasks = await storage.getTopLevelTasks();
|
|
100
237
|
let filteredTasks = topLevelTasks;
|
|
101
238
|
if (filters.status) {
|
|
@@ -108,22 +245,22 @@ class TaskService {
|
|
|
108
245
|
return filteredTasks;
|
|
109
246
|
}
|
|
110
247
|
async getTask(id) {
|
|
111
|
-
return await
|
|
248
|
+
return await this.storage.getTask(id);
|
|
112
249
|
}
|
|
113
250
|
async getTaskContent(id) {
|
|
114
|
-
return await
|
|
251
|
+
return await this.storage.getTaskContent(id);
|
|
115
252
|
}
|
|
116
253
|
async getTaskAIMetadata(id) {
|
|
117
|
-
return await
|
|
254
|
+
return await this.storage.getTaskAIMetadata(id);
|
|
118
255
|
}
|
|
119
256
|
async getSubtasks(id) {
|
|
120
|
-
return await
|
|
257
|
+
return await this.storage.getSubtasks(id);
|
|
121
258
|
}
|
|
122
259
|
async updateTask(id, updates) {
|
|
123
|
-
const storage =
|
|
260
|
+
const storage = this.storage;
|
|
124
261
|
const existingTask = await storage.getTask(id);
|
|
125
262
|
if (!existingTask) {
|
|
126
|
-
throw
|
|
263
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(id);
|
|
127
264
|
}
|
|
128
265
|
// Validate status transitions
|
|
129
266
|
if (updates.status) {
|
|
@@ -133,7 +270,7 @@ class TaskService {
|
|
|
133
270
|
completed: ["todo", "in-progress"],
|
|
134
271
|
};
|
|
135
272
|
if (!validTransitions[existingTask.status].includes(updates.status)) {
|
|
136
|
-
throw
|
|
273
|
+
throw (0, task_o_matic_error_1.formatInvalidStatusTransitionError)(existingTask.status, updates.status);
|
|
137
274
|
}
|
|
138
275
|
}
|
|
139
276
|
const finalUpdates = { ...updates };
|
|
@@ -145,16 +282,16 @@ class TaskService {
|
|
|
145
282
|
}
|
|
146
283
|
const updatedTask = await storage.updateTask(id, finalUpdates);
|
|
147
284
|
if (!updatedTask) {
|
|
148
|
-
throw
|
|
285
|
+
throw (0, task_o_matic_error_1.formatStorageError)(`updateTask ${id}`);
|
|
149
286
|
}
|
|
150
287
|
// Emit task:updated event
|
|
151
|
-
await
|
|
288
|
+
await this.hooks.emit("task:updated", {
|
|
152
289
|
task: updatedTask,
|
|
153
290
|
changes: finalUpdates,
|
|
154
291
|
});
|
|
155
292
|
// Emit task:status-changed event if status changed
|
|
156
293
|
if (updates.status && updates.status !== existingTask.status) {
|
|
157
|
-
await
|
|
294
|
+
await this.hooks.emit("task:status-changed", {
|
|
158
295
|
task: updatedTask,
|
|
159
296
|
oldStatus: existingTask.status,
|
|
160
297
|
newStatus: updates.status,
|
|
@@ -167,10 +304,10 @@ class TaskService {
|
|
|
167
304
|
}
|
|
168
305
|
async deleteTask(id, options = {}) {
|
|
169
306
|
const { cascade, force } = options;
|
|
170
|
-
const storage =
|
|
307
|
+
const storage = this.storage;
|
|
171
308
|
const taskToDelete = await storage.getTask(id);
|
|
172
309
|
if (!taskToDelete) {
|
|
173
|
-
throw
|
|
310
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(id);
|
|
174
311
|
}
|
|
175
312
|
const deleted = [];
|
|
176
313
|
const orphanedSubtasks = [];
|
|
@@ -178,7 +315,15 @@ class TaskService {
|
|
|
178
315
|
const subtasks = await storage.getSubtasks(id);
|
|
179
316
|
if (subtasks.length > 0 && !cascade) {
|
|
180
317
|
if (!force) {
|
|
181
|
-
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
|
+
});
|
|
182
327
|
}
|
|
183
328
|
// Orphan subtasks by removing parent reference
|
|
184
329
|
for (const subtask of subtasks) {
|
|
@@ -202,7 +347,7 @@ class TaskService {
|
|
|
202
347
|
if (success) {
|
|
203
348
|
deleted.push(taskToDelete);
|
|
204
349
|
// Emit task:deleted event
|
|
205
|
-
await
|
|
350
|
+
await this.hooks.emit("task:deleted", { taskId: id });
|
|
206
351
|
}
|
|
207
352
|
return { success: true, deleted, orphanedSubtasks };
|
|
208
353
|
}
|
|
@@ -210,10 +355,10 @@ class TaskService {
|
|
|
210
355
|
// TAG OPERATIONS
|
|
211
356
|
// ============================================================================
|
|
212
357
|
async addTags(id, tags) {
|
|
213
|
-
const storage =
|
|
358
|
+
const storage = this.storage;
|
|
214
359
|
const task = await storage.getTask(id);
|
|
215
360
|
if (!task) {
|
|
216
|
-
throw
|
|
361
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(id);
|
|
217
362
|
}
|
|
218
363
|
const existingTags = task.tags || [];
|
|
219
364
|
const newTags = tags.filter((tag) => !existingTags.includes(tag));
|
|
@@ -223,15 +368,15 @@ class TaskService {
|
|
|
223
368
|
const updatedTags = [...existingTags, ...newTags];
|
|
224
369
|
const updatedTask = await storage.updateTask(id, { tags: updatedTags });
|
|
225
370
|
if (!updatedTask) {
|
|
226
|
-
throw
|
|
371
|
+
throw (0, task_o_matic_error_1.formatStorageError)(`add tags to task ${id}`);
|
|
227
372
|
}
|
|
228
373
|
return updatedTask;
|
|
229
374
|
}
|
|
230
375
|
async removeTags(id, tags) {
|
|
231
|
-
const storage =
|
|
376
|
+
const storage = this.storage;
|
|
232
377
|
const task = await storage.getTask(id);
|
|
233
378
|
if (!task) {
|
|
234
|
-
throw
|
|
379
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(id);
|
|
235
380
|
}
|
|
236
381
|
const existingTags = task.tags || [];
|
|
237
382
|
const updatedTags = existingTags.filter((tag) => !tags.includes(tag));
|
|
@@ -240,15 +385,44 @@ class TaskService {
|
|
|
240
385
|
}
|
|
241
386
|
const updatedTask = await storage.updateTask(id, { tags: updatedTags });
|
|
242
387
|
if (!updatedTask) {
|
|
243
|
-
throw
|
|
388
|
+
throw (0, task_o_matic_error_1.formatStorageError)(`remove tags from task ${id}`);
|
|
244
389
|
}
|
|
245
390
|
return updatedTask;
|
|
246
391
|
}
|
|
247
392
|
// ============================================================================
|
|
248
393
|
// TASK NAVIGATION
|
|
249
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
|
+
*/
|
|
250
424
|
async getNextTask(filters) {
|
|
251
|
-
const storage =
|
|
425
|
+
const storage = this.storage;
|
|
252
426
|
const allTasks = await storage.getTasks();
|
|
253
427
|
// Filter by status and other criteria
|
|
254
428
|
let filteredTasks = allTasks.filter((task) => {
|
|
@@ -278,12 +452,12 @@ class TaskService {
|
|
|
278
452
|
}
|
|
279
453
|
}
|
|
280
454
|
async getTaskTree(rootId) {
|
|
281
|
-
const storage =
|
|
455
|
+
const storage = this.storage;
|
|
282
456
|
if (rootId) {
|
|
283
457
|
// Return tree starting from specific task
|
|
284
458
|
const rootTask = await storage.getTask(rootId);
|
|
285
459
|
if (!rootTask) {
|
|
286
|
-
throw
|
|
460
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(rootId);
|
|
287
461
|
}
|
|
288
462
|
// Get all subtasks recursively
|
|
289
463
|
const getAllSubtasks = async (task) => {
|
|
@@ -307,71 +481,85 @@ class TaskService {
|
|
|
307
481
|
// ============================================================================
|
|
308
482
|
// AI OPERATIONS
|
|
309
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
|
+
*/
|
|
310
519
|
async enhanceTask(taskId, aiOptions, streamingOptions) {
|
|
311
520
|
const startTime = Date.now();
|
|
312
|
-
|
|
521
|
+
this.hooks.emit("task:progress", {
|
|
313
522
|
message: "Starting task enhancement...",
|
|
314
523
|
type: "started",
|
|
315
524
|
});
|
|
316
|
-
const task = await
|
|
525
|
+
const task = await this.storage.getTask(taskId);
|
|
317
526
|
if (!task) {
|
|
318
|
-
throw
|
|
527
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
319
528
|
}
|
|
320
|
-
|
|
529
|
+
this.hooks.emit("task:progress", {
|
|
321
530
|
message: "Building context...",
|
|
322
531
|
type: "progress",
|
|
323
532
|
});
|
|
324
|
-
const context = await
|
|
533
|
+
const context = await this.contextBuilder.buildContext(taskId);
|
|
325
534
|
const stackInfo = (0, stack_formatter_1.formatStackInfo)(context.stack);
|
|
326
535
|
const enhancementAIConfig = (0, ai_config_builder_1.buildAIConfig)(aiOptions);
|
|
327
|
-
|
|
536
|
+
this.hooks.emit("task:progress", {
|
|
328
537
|
message: "Calling AI for enhancement...",
|
|
329
538
|
type: "progress",
|
|
330
539
|
});
|
|
331
|
-
//
|
|
332
|
-
let tokenUsage;
|
|
333
|
-
let timeToFirstToken;
|
|
540
|
+
// Use utility to wrap streaming options and capture metrics (DRY fix 1.1)
|
|
334
541
|
const aiStartTime = Date.now();
|
|
335
|
-
|
|
336
|
-
const
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
tokenUsage = {
|
|
341
|
-
prompt: result.usage.inputTokens || result.usage.promptTokens || 0,
|
|
342
|
-
completion: result.usage.outputTokens || result.usage.completionTokens || 0,
|
|
343
|
-
total: result.usage.totalTokens || 0,
|
|
344
|
-
};
|
|
345
|
-
}
|
|
346
|
-
// Call original onFinish if provided
|
|
347
|
-
await streamingOptions?.onFinish?.(result);
|
|
348
|
-
},
|
|
349
|
-
onChunk: (chunk) => {
|
|
350
|
-
if (chunk && !timeToFirstToken) {
|
|
351
|
-
timeToFirstToken = Date.now() - aiStartTime;
|
|
352
|
-
}
|
|
353
|
-
// Call original onChunk if provided
|
|
354
|
-
streamingOptions?.onChunk?.(chunk);
|
|
355
|
-
},
|
|
356
|
-
};
|
|
357
|
-
const enhancedContent = await (0, ai_service_factory_1.getAIOperations)().enhanceTaskWithDocumentation(task.id, task.title, task.description ?? "", stackInfo, metricsStreamingOptions, undefined, enhancementAIConfig, context.existingResearch);
|
|
358
|
-
hooks_1.hooks.emit("task:progress", {
|
|
542
|
+
const { options: metricsStreamingOptions, getMetrics } = (0, streaming_utils_1.createMetricsStreamingOptions)(streamingOptions, aiStartTime);
|
|
543
|
+
const enhancedContent = await this.aiOperations.enhanceTaskWithDocumentation(task.id, task.title, task.description ?? "", stackInfo, metricsStreamingOptions, undefined, enhancementAIConfig, context.existingResearch);
|
|
544
|
+
// Extract metrics after AI call
|
|
545
|
+
const { tokenUsage, timeToFirstToken } = getMetrics();
|
|
546
|
+
this.hooks.emit("task:progress", {
|
|
359
547
|
message: "Saving enhanced content...",
|
|
360
548
|
type: "progress",
|
|
361
549
|
});
|
|
362
550
|
const originalLength = task.description?.length || 0;
|
|
363
551
|
if (enhancedContent.length > 200) {
|
|
364
|
-
const contentFile = await
|
|
365
|
-
await
|
|
552
|
+
const contentFile = await this.storage.saveEnhancedTaskContent(task.id, enhancedContent);
|
|
553
|
+
await this.storage.updateTask(task.id, {
|
|
366
554
|
contentFile,
|
|
367
555
|
description: task.description +
|
|
368
556
|
"\n\n🤖 AI-enhanced with Context7 documentation available.",
|
|
369
557
|
});
|
|
370
558
|
}
|
|
371
559
|
else {
|
|
372
|
-
await
|
|
560
|
+
await this.storage.updateTask(task.id, { description: enhancedContent });
|
|
373
561
|
}
|
|
374
|
-
const aiConfig =
|
|
562
|
+
const aiConfig = this.modelProvider.getAIConfig();
|
|
375
563
|
const aiMetadata = {
|
|
376
564
|
taskId: task.id,
|
|
377
565
|
aiGenerated: true,
|
|
@@ -381,9 +569,9 @@ class TaskService {
|
|
|
381
569
|
aiModel: aiConfig.model,
|
|
382
570
|
enhancedAt: Date.now(),
|
|
383
571
|
};
|
|
384
|
-
await
|
|
572
|
+
await this.storage.saveTaskAIMetadata(aiMetadata);
|
|
385
573
|
const duration = Date.now() - startTime;
|
|
386
|
-
|
|
574
|
+
this.hooks.emit("task:progress", {
|
|
387
575
|
message: "Task enhancement completed",
|
|
388
576
|
type: "completed",
|
|
389
577
|
});
|
|
@@ -406,72 +594,107 @@ class TaskService {
|
|
|
406
594
|
},
|
|
407
595
|
};
|
|
408
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
|
+
*/
|
|
409
643
|
async splitTask(taskId, aiOptions, promptOverride, messageOverride, streamingOptions, enableFilesystemTools) {
|
|
410
644
|
const startTime = Date.now();
|
|
411
|
-
|
|
645
|
+
this.hooks.emit("task:progress", {
|
|
412
646
|
message: "Starting task breakdown...",
|
|
413
647
|
type: "started",
|
|
414
648
|
});
|
|
415
|
-
const task = await
|
|
649
|
+
const task = await this.storage.getTask(taskId);
|
|
416
650
|
if (!task) {
|
|
417
|
-
throw
|
|
651
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
418
652
|
}
|
|
419
653
|
// Check if task already has subtasks
|
|
420
|
-
const existingSubtasks = await
|
|
654
|
+
const existingSubtasks = await this.storage.getSubtasks(taskId);
|
|
421
655
|
if (existingSubtasks.length > 0) {
|
|
422
|
-
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
|
+
});
|
|
423
665
|
}
|
|
424
|
-
|
|
666
|
+
this.hooks.emit("task:progress", {
|
|
425
667
|
message: "Building context...",
|
|
426
668
|
type: "progress",
|
|
427
669
|
});
|
|
428
670
|
// Build comprehensive context
|
|
429
|
-
const context = await
|
|
671
|
+
const context = await this.contextBuilder.buildContext(taskId);
|
|
430
672
|
const stackInfo = (0, stack_formatter_1.formatStackInfo)(context.stack);
|
|
431
673
|
// Get full task content
|
|
432
674
|
const fullContent = context.task.fullContent || task.description || "";
|
|
433
675
|
const breakdownAIConfig = (0, ai_config_builder_1.buildAIConfig)(aiOptions);
|
|
434
|
-
|
|
676
|
+
this.hooks.emit("task:progress", {
|
|
435
677
|
message: "Calling AI to break down task...",
|
|
436
678
|
type: "progress",
|
|
437
679
|
});
|
|
438
|
-
//
|
|
439
|
-
let tokenUsage;
|
|
440
|
-
let timeToFirstToken;
|
|
680
|
+
// Use utility to wrap streaming options and capture metrics (DRY fix 1.1)
|
|
441
681
|
const aiStartTime = Date.now();
|
|
442
|
-
|
|
443
|
-
const metricsStreamingOptions = {
|
|
444
|
-
...streamingOptions,
|
|
445
|
-
onFinish: async (result) => {
|
|
446
|
-
if (result.usage) {
|
|
447
|
-
tokenUsage = {
|
|
448
|
-
prompt: result.usage.inputTokens || result.usage.promptTokens || 0,
|
|
449
|
-
completion: result.usage.outputTokens || result.usage.completionTokens || 0,
|
|
450
|
-
total: result.usage.totalTokens || 0,
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
// Call original onFinish if provided
|
|
454
|
-
await streamingOptions?.onFinish?.(result);
|
|
455
|
-
},
|
|
456
|
-
onChunk: (chunk) => {
|
|
457
|
-
if (chunk && !timeToFirstToken) {
|
|
458
|
-
timeToFirstToken = Date.now() - aiStartTime;
|
|
459
|
-
}
|
|
460
|
-
// Call original onChunk if provided
|
|
461
|
-
streamingOptions?.onChunk?.(chunk);
|
|
462
|
-
},
|
|
463
|
-
};
|
|
682
|
+
const { options: metricsStreamingOptions, getMetrics } = (0, streaming_utils_1.createMetricsStreamingOptions)(streamingOptions, aiStartTime);
|
|
464
683
|
// Use AI service to break down the task with enhanced context
|
|
465
|
-
const subtaskData = await
|
|
466
|
-
|
|
684
|
+
const subtaskData = await this.aiOperations.breakdownTask(task, breakdownAIConfig, promptOverride, messageOverride, metricsStreamingOptions, undefined, fullContent, stackInfo, existingSubtasks, enableFilesystemTools);
|
|
685
|
+
// Extract metrics after AI call
|
|
686
|
+
const { tokenUsage, timeToFirstToken } = getMetrics();
|
|
687
|
+
this.hooks.emit("task:progress", {
|
|
467
688
|
message: `Creating ${subtaskData.length} subtasks...`,
|
|
468
689
|
type: "progress",
|
|
469
690
|
});
|
|
470
|
-
// Create subtasks
|
|
691
|
+
// Create subtasks and save AI metadata for each (Bug fix 2.3)
|
|
471
692
|
const createdSubtasks = [];
|
|
693
|
+
const aiConfig = this.modelProvider.getAIConfig();
|
|
694
|
+
const splitTimestamp = Date.now();
|
|
472
695
|
for (let i = 0; i < subtaskData.length; i++) {
|
|
473
696
|
const subtask = subtaskData[i];
|
|
474
|
-
|
|
697
|
+
this.hooks.emit("task:progress", {
|
|
475
698
|
message: `Creating subtask ${i + 1}/${subtaskData.length}: ${subtask.title}`,
|
|
476
699
|
type: "progress",
|
|
477
700
|
});
|
|
@@ -482,10 +705,23 @@ class TaskService {
|
|
|
482
705
|
parentId: taskId,
|
|
483
706
|
});
|
|
484
707
|
createdSubtasks.push(result.task);
|
|
708
|
+
// Save AI metadata for each subtask (Bug fix 2.3)
|
|
709
|
+
const subtaskMetadata = {
|
|
710
|
+
taskId: result.task.id,
|
|
711
|
+
aiGenerated: true,
|
|
712
|
+
aiPrompt: promptOverride ||
|
|
713
|
+
"Split task into meaningful subtasks with full context and existing subtask awareness",
|
|
714
|
+
confidence: 0.9,
|
|
715
|
+
aiProvider: aiConfig.provider,
|
|
716
|
+
aiModel: aiConfig.model,
|
|
717
|
+
splitAt: splitTimestamp,
|
|
718
|
+
parentTaskId: taskId,
|
|
719
|
+
subtaskIndex: i + 1,
|
|
720
|
+
};
|
|
721
|
+
await (0, ai_service_factory_1.getStorage)().saveTaskAIMetadata(subtaskMetadata);
|
|
485
722
|
}
|
|
486
|
-
//
|
|
487
|
-
const
|
|
488
|
-
const aiMetadata = {
|
|
723
|
+
// Save AI metadata for parent task as well
|
|
724
|
+
const parentMetadata = {
|
|
489
725
|
taskId: task.id,
|
|
490
726
|
aiGenerated: true,
|
|
491
727
|
aiPrompt: promptOverride ||
|
|
@@ -493,11 +729,12 @@ class TaskService {
|
|
|
493
729
|
confidence: 0.9,
|
|
494
730
|
aiProvider: aiConfig.provider,
|
|
495
731
|
aiModel: aiConfig.model,
|
|
496
|
-
splitAt:
|
|
732
|
+
splitAt: splitTimestamp,
|
|
733
|
+
subtasksCreated: createdSubtasks.length,
|
|
497
734
|
};
|
|
498
|
-
await (0, ai_service_factory_1.getStorage)().saveTaskAIMetadata(
|
|
735
|
+
await (0, ai_service_factory_1.getStorage)().saveTaskAIMetadata(parentMetadata);
|
|
499
736
|
const duration = Date.now() - startTime;
|
|
500
|
-
|
|
737
|
+
this.hooks.emit("task:progress", {
|
|
501
738
|
message: `Task split into ${createdSubtasks.length} subtasks`,
|
|
502
739
|
type: "completed",
|
|
503
740
|
});
|
|
@@ -519,19 +756,57 @@ class TaskService {
|
|
|
519
756
|
},
|
|
520
757
|
};
|
|
521
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
|
+
*/
|
|
522
797
|
async documentTask(taskId, force = false, aiOptions, streamingOptions) {
|
|
523
798
|
const startTime = Date.now();
|
|
524
|
-
|
|
799
|
+
this.hooks.emit("task:progress", {
|
|
525
800
|
message: "Analyzing documentation needs...",
|
|
526
801
|
type: "started",
|
|
527
802
|
});
|
|
528
|
-
const task = await
|
|
803
|
+
const task = await this.storage.getTask(taskId);
|
|
529
804
|
if (!task) {
|
|
530
|
-
throw
|
|
805
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
531
806
|
}
|
|
532
807
|
if (task.documentation && !force) {
|
|
533
|
-
if (
|
|
534
|
-
|
|
808
|
+
if (this.contextBuilder.isDocumentationFresh(task.documentation)) {
|
|
809
|
+
this.hooks.emit("task:progress", {
|
|
535
810
|
message: "Documentation is fresh, skipping analysis",
|
|
536
811
|
type: "info",
|
|
537
812
|
});
|
|
@@ -545,38 +820,45 @@ class TaskService {
|
|
|
545
820
|
};
|
|
546
821
|
}
|
|
547
822
|
}
|
|
548
|
-
|
|
823
|
+
this.hooks.emit("task:progress", {
|
|
549
824
|
message: "Building context...",
|
|
550
825
|
type: "progress",
|
|
551
826
|
});
|
|
552
|
-
const context = await
|
|
827
|
+
const context = await this.contextBuilder.buildContext(taskId);
|
|
553
828
|
const stackInfo = (0, stack_formatter_1.formatStackInfo)(context.stack);
|
|
554
829
|
const analysisAIConfig = (0, ai_config_builder_1.buildAIConfig)(aiOptions);
|
|
555
830
|
// Get full task content
|
|
556
831
|
const fullContent = context.task.fullContent || task.description;
|
|
557
832
|
if (!fullContent) {
|
|
558
|
-
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
|
+
});
|
|
559
841
|
}
|
|
560
842
|
// Get existing documentations from all tasks
|
|
561
|
-
const tasks = await
|
|
843
|
+
const tasks = await this.storage.getTasks();
|
|
562
844
|
const documentations = tasks.map((task) => task.documentation);
|
|
563
|
-
|
|
845
|
+
this.hooks.emit("task:progress", {
|
|
564
846
|
message: "Calling AI to analyze documentation needs...",
|
|
565
847
|
type: "progress",
|
|
566
848
|
});
|
|
567
849
|
// First analyze what documentation is needed
|
|
568
|
-
const analysis = await
|
|
850
|
+
const analysis = await this.aiOperations.analyzeDocumentationNeeds(task.id, task.title, fullContent, stackInfo, streamingOptions, undefined, analysisAIConfig, documentations);
|
|
569
851
|
let documentation;
|
|
570
852
|
if (analysis.libraries.length > 0) {
|
|
571
|
-
|
|
853
|
+
this.hooks.emit("task:progress", {
|
|
572
854
|
message: `Fetching documentation for ${analysis.libraries.length} libraries...`,
|
|
573
855
|
type: "progress",
|
|
574
856
|
});
|
|
575
857
|
// Build research object from actual libraries
|
|
576
858
|
const research = {};
|
|
577
859
|
for (const lib of analysis.libraries) {
|
|
578
|
-
const sanitizedLibrary =
|
|
579
|
-
const sanitizedQuery =
|
|
860
|
+
const sanitizedLibrary = this.storage.sanitizeForFilename(lib.name);
|
|
861
|
+
const sanitizedQuery = this.storage.sanitizeForFilename(lib.searchQuery);
|
|
580
862
|
const docFile = `docs/${sanitizedLibrary}/${sanitizedQuery}.md`;
|
|
581
863
|
if (!research[lib.name]) {
|
|
582
864
|
research[lib.name] = [];
|
|
@@ -586,11 +868,11 @@ class TaskService {
|
|
|
586
868
|
doc: docFile,
|
|
587
869
|
});
|
|
588
870
|
}
|
|
589
|
-
|
|
871
|
+
this.hooks.emit("task:progress", {
|
|
590
872
|
message: "Generating documentation recap...",
|
|
591
873
|
type: "progress",
|
|
592
874
|
});
|
|
593
|
-
const recap = await
|
|
875
|
+
const recap = await this.aiOperations.generateDocumentationRecap(analysis.libraries, analysis.toolResults?.map((tr) => ({
|
|
594
876
|
library: tr.toolName,
|
|
595
877
|
content: JSON.stringify(tr.output),
|
|
596
878
|
})) || [], streamingOptions);
|
|
@@ -601,14 +883,14 @@ class TaskService {
|
|
|
601
883
|
files: analysis.files || [],
|
|
602
884
|
research,
|
|
603
885
|
};
|
|
604
|
-
|
|
886
|
+
this.hooks.emit("task:progress", {
|
|
605
887
|
message: "Saving documentation...",
|
|
606
888
|
type: "progress",
|
|
607
889
|
});
|
|
608
|
-
await
|
|
890
|
+
await this.storage.updateTask(taskId, { documentation });
|
|
609
891
|
}
|
|
610
892
|
const duration = Date.now() - startTime;
|
|
611
|
-
|
|
893
|
+
this.hooks.emit("task:progress", {
|
|
612
894
|
message: "Documentation analysis completed",
|
|
613
895
|
type: "completed",
|
|
614
896
|
});
|
|
@@ -622,19 +904,59 @@ class TaskService {
|
|
|
622
904
|
},
|
|
623
905
|
};
|
|
624
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
|
+
*/
|
|
625
947
|
async planTask(taskId, aiOptions, streamingOptions) {
|
|
626
948
|
const startTime = Date.now();
|
|
627
|
-
|
|
949
|
+
this.hooks.emit("task:progress", {
|
|
628
950
|
message: "Creating implementation plan...",
|
|
629
951
|
type: "started",
|
|
630
952
|
});
|
|
631
|
-
const task = await
|
|
953
|
+
const task = await this.storage.getTask(taskId);
|
|
632
954
|
if (!task) {
|
|
633
|
-
throw
|
|
955
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
634
956
|
}
|
|
635
|
-
const aiService =
|
|
957
|
+
const aiService = this.aiOperations;
|
|
636
958
|
const planAIConfig = (0, ai_config_builder_1.buildAIConfig)(aiOptions);
|
|
637
|
-
|
|
959
|
+
this.hooks.emit("task:progress", {
|
|
638
960
|
message: "Building task context...",
|
|
639
961
|
type: "progress",
|
|
640
962
|
});
|
|
@@ -658,49 +980,28 @@ class TaskService {
|
|
|
658
980
|
taskDetails += ` ${subtask.description || "No description"}\n\n`;
|
|
659
981
|
});
|
|
660
982
|
}
|
|
661
|
-
|
|
983
|
+
this.hooks.emit("task:progress", {
|
|
662
984
|
message: "Calling AI to create plan...",
|
|
663
985
|
type: "progress",
|
|
664
986
|
});
|
|
665
|
-
//
|
|
666
|
-
let tokenUsage;
|
|
667
|
-
let timeToFirstToken;
|
|
987
|
+
// Use utility to wrap streaming options and capture metrics (DRY fix 1.1)
|
|
668
988
|
const aiStartTime = Date.now();
|
|
669
|
-
|
|
670
|
-
const metricsStreamingOptions = {
|
|
671
|
-
...streamingOptions,
|
|
672
|
-
onFinish: async (result) => {
|
|
673
|
-
if (result.usage) {
|
|
674
|
-
tokenUsage = {
|
|
675
|
-
prompt: result.usage.inputTokens || result.usage.promptTokens || 0,
|
|
676
|
-
completion: result.usage.outputTokens || result.usage.completionTokens || 0,
|
|
677
|
-
total: result.usage.totalTokens || 0,
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
// Call original onFinish if provided
|
|
681
|
-
await streamingOptions?.onFinish?.(result);
|
|
682
|
-
},
|
|
683
|
-
onChunk: (chunk) => {
|
|
684
|
-
if (chunk && !timeToFirstToken) {
|
|
685
|
-
timeToFirstToken = Date.now() - aiStartTime;
|
|
686
|
-
}
|
|
687
|
-
// Call original onChunk if provided
|
|
688
|
-
streamingOptions?.onChunk?.(chunk);
|
|
689
|
-
},
|
|
690
|
-
};
|
|
989
|
+
const { options: metricsStreamingOptions, getMetrics } = (0, streaming_utils_1.createMetricsStreamingOptions)(streamingOptions, aiStartTime);
|
|
691
990
|
const plan = await aiService.planTask(taskContext, taskDetails, planAIConfig, undefined, undefined, metricsStreamingOptions);
|
|
692
|
-
|
|
991
|
+
// Extract metrics after AI call
|
|
992
|
+
const { tokenUsage, timeToFirstToken } = getMetrics();
|
|
993
|
+
this.hooks.emit("task:progress", {
|
|
693
994
|
message: "Saving plan...",
|
|
694
995
|
type: "progress",
|
|
695
996
|
});
|
|
696
997
|
// Save the plan to storage
|
|
697
|
-
await
|
|
998
|
+
await this.storage.savePlan(taskId, plan);
|
|
698
999
|
const duration = Date.now() - startTime;
|
|
699
|
-
|
|
1000
|
+
this.hooks.emit("task:progress", {
|
|
700
1001
|
message: "Implementation plan created",
|
|
701
1002
|
type: "completed",
|
|
702
1003
|
});
|
|
703
|
-
const aiConfig =
|
|
1004
|
+
const aiConfig = this.modelProvider.getAIConfig();
|
|
704
1005
|
return {
|
|
705
1006
|
success: true,
|
|
706
1007
|
task,
|
|
@@ -721,35 +1022,48 @@ class TaskService {
|
|
|
721
1022
|
// DOCUMENTATION OPERATIONS
|
|
722
1023
|
// ============================================================================
|
|
723
1024
|
async getTaskDocumentation(taskId) {
|
|
724
|
-
return
|
|
1025
|
+
return this.storage.getTaskDocumentation(taskId);
|
|
725
1026
|
}
|
|
726
1027
|
async addTaskDocumentationFromFile(taskId, filePath) {
|
|
727
1028
|
const task = await this.getTask(taskId);
|
|
728
1029
|
if (!task) {
|
|
729
|
-
throw
|
|
1030
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
730
1031
|
}
|
|
731
1032
|
try {
|
|
732
1033
|
const { readFileSync, existsSync } = await Promise.resolve().then(() => __importStar(require("fs")));
|
|
733
1034
|
const { resolve } = await Promise.resolve().then(() => __importStar(require("path")));
|
|
734
1035
|
const resolvedPath = resolve(filePath);
|
|
735
1036
|
if (!existsSync(resolvedPath)) {
|
|
736
|
-
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
|
+
});
|
|
737
1046
|
}
|
|
738
1047
|
const content = readFileSync(resolvedPath, "utf-8");
|
|
739
|
-
const savedPath = await
|
|
1048
|
+
const savedPath = await this.storage.saveTaskDocumentation(taskId, content);
|
|
740
1049
|
return {
|
|
741
1050
|
filePath: savedPath,
|
|
742
1051
|
task,
|
|
743
1052
|
};
|
|
744
1053
|
}
|
|
745
1054
|
catch (error) {
|
|
746
|
-
throw
|
|
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);
|
|
747
1061
|
}
|
|
748
1062
|
}
|
|
749
1063
|
async setTaskPlan(taskId, planText, planFilePath) {
|
|
750
1064
|
const task = await this.getTask(taskId);
|
|
751
1065
|
if (!task) {
|
|
752
|
-
throw
|
|
1066
|
+
throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
|
|
753
1067
|
}
|
|
754
1068
|
let plan;
|
|
755
1069
|
if (planFilePath) {
|
|
@@ -758,21 +1072,41 @@ class TaskService {
|
|
|
758
1072
|
const { resolve } = await Promise.resolve().then(() => __importStar(require("path")));
|
|
759
1073
|
const resolvedPath = resolve(planFilePath);
|
|
760
1074
|
if (!existsSync(resolvedPath)) {
|
|
761
|
-
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
|
+
});
|
|
762
1084
|
}
|
|
763
1085
|
plan = readFileSync(resolvedPath, "utf-8");
|
|
764
1086
|
}
|
|
765
1087
|
catch (error) {
|
|
766
|
-
throw
|
|
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);
|
|
767
1094
|
}
|
|
768
1095
|
}
|
|
769
1096
|
else if (planText) {
|
|
770
1097
|
plan = planText;
|
|
771
1098
|
}
|
|
772
1099
|
else {
|
|
773
|
-
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
|
+
});
|
|
774
1108
|
}
|
|
775
|
-
await
|
|
1109
|
+
await this.storage.savePlan(taskId, plan);
|
|
776
1110
|
const planFile = `plans/${taskId}.md`;
|
|
777
1111
|
return {
|
|
778
1112
|
planFile,
|
|
@@ -783,15 +1117,27 @@ class TaskService {
|
|
|
783
1117
|
// PLAN OPERATIONS
|
|
784
1118
|
// ============================================================================
|
|
785
1119
|
async getTaskPlan(taskId) {
|
|
786
|
-
return
|
|
1120
|
+
return this.storage.getPlan(taskId);
|
|
787
1121
|
}
|
|
788
1122
|
async listTaskPlans() {
|
|
789
|
-
return
|
|
1123
|
+
return this.storage.listPlans();
|
|
790
1124
|
}
|
|
791
1125
|
async deleteTaskPlan(taskId) {
|
|
792
|
-
return
|
|
1126
|
+
return this.storage.deletePlan(taskId);
|
|
793
1127
|
}
|
|
794
1128
|
}
|
|
795
1129
|
exports.TaskService = TaskService;
|
|
796
|
-
//
|
|
797
|
-
|
|
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
|
+
});
|