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.
Files changed (144) hide show
  1. package/dist/cli/display/progress.d.ts +15 -2
  2. package/dist/cli/display/progress.d.ts.map +1 -1
  3. package/dist/cli/display/progress.js +72 -4
  4. package/dist/commands/benchmark.d.ts.map +1 -1
  5. package/dist/commands/benchmark.js +11 -3
  6. package/dist/commands/init.d.ts.map +1 -1
  7. package/dist/commands/init.js +60 -12
  8. package/dist/commands/prd.js +7 -1
  9. package/dist/commands/tasks/delete.d.ts.map +1 -1
  10. package/dist/commands/tasks/delete.js +2 -1
  11. package/dist/commands/tasks/document/add.d.ts.map +1 -1
  12. package/dist/commands/tasks/document/add.js +5 -4
  13. package/dist/commands/tasks/document/get.d.ts.map +1 -1
  14. package/dist/commands/tasks/document/get.js +2 -1
  15. package/dist/commands/tasks/list.js +2 -2
  16. package/dist/commands/tasks/next.js +4 -4
  17. package/dist/commands/tasks/plan/set.d.ts.map +1 -1
  18. package/dist/commands/tasks/plan/set.js +11 -3
  19. package/dist/commands/tasks/show.d.ts.map +1 -1
  20. package/dist/commands/tasks/show.js +2 -1
  21. package/dist/commands/tasks/status.d.ts.map +1 -1
  22. package/dist/commands/tasks/status.js +4 -3
  23. package/dist/commands/tasks/update.d.ts.map +1 -1
  24. package/dist/commands/tasks/update.js +7 -1
  25. package/dist/lib/ai-service/ai-operations.d.ts +1 -1
  26. package/dist/lib/ai-service/ai-operations.d.ts.map +1 -1
  27. package/dist/lib/ai-service/base-operations.d.ts +22 -0
  28. package/dist/lib/ai-service/base-operations.d.ts.map +1 -1
  29. package/dist/lib/ai-service/base-operations.js +29 -1
  30. package/dist/lib/ai-service/model-provider.d.ts.map +1 -1
  31. package/dist/lib/ai-service/model-provider.js +37 -6
  32. package/dist/lib/ai-service/task-operations.d.ts +2 -1
  33. package/dist/lib/ai-service/task-operations.d.ts.map +1 -1
  34. package/dist/lib/ai-service/task-operations.js +135 -173
  35. package/dist/lib/benchmark/registry.d.ts.map +1 -1
  36. package/dist/lib/benchmark/registry.js +6 -10
  37. package/dist/lib/better-t-stack-cli.d.ts +36 -21
  38. package/dist/lib/better-t-stack-cli.d.ts.map +1 -1
  39. package/dist/lib/better-t-stack-cli.js +212 -33
  40. package/dist/lib/bootstrap/cli-bootstrap.d.ts +14 -0
  41. package/dist/lib/bootstrap/cli-bootstrap.d.ts.map +1 -0
  42. package/dist/lib/bootstrap/cli-bootstrap.js +325 -0
  43. package/dist/lib/bootstrap/index.d.ts +4 -0
  44. package/dist/lib/bootstrap/index.d.ts.map +1 -0
  45. package/dist/lib/bootstrap/index.js +19 -0
  46. package/dist/lib/bootstrap/medusa-bootstrap.d.ts +14 -0
  47. package/dist/lib/bootstrap/medusa-bootstrap.d.ts.map +1 -0
  48. package/dist/lib/bootstrap/medusa-bootstrap.js +218 -0
  49. package/dist/lib/bootstrap/opentui-bootstrap.d.ts +11 -0
  50. package/dist/lib/bootstrap/opentui-bootstrap.d.ts.map +1 -0
  51. package/dist/lib/bootstrap/opentui-bootstrap.js +342 -0
  52. package/dist/lib/config-validation.d.ts +215 -0
  53. package/dist/lib/config-validation.d.ts.map +1 -0
  54. package/dist/lib/config-validation.js +246 -0
  55. package/dist/lib/config.d.ts +14 -0
  56. package/dist/lib/config.d.ts.map +1 -1
  57. package/dist/lib/config.js +37 -5
  58. package/dist/lib/storage/file-system.d.ts.map +1 -1
  59. package/dist/lib/storage/file-system.js +81 -21
  60. package/dist/lib/task-execution-core.d.ts.map +1 -1
  61. package/dist/lib/task-execution-core.js +3 -2
  62. package/dist/services/prd.d.ts +17 -0
  63. package/dist/services/prd.d.ts.map +1 -1
  64. package/dist/services/prd.js +67 -60
  65. package/dist/services/tasks.d.ts +317 -3
  66. package/dist/services/tasks.d.ts.map +1 -1
  67. package/dist/services/tasks.js +531 -185
  68. package/dist/services/workflow-ai-assistant.d.ts.map +1 -1
  69. package/dist/services/workflow-ai-assistant.js +19 -6
  70. package/dist/test/lib/ai-service/task-operations.test.d.ts +2 -0
  71. package/dist/test/lib/ai-service/task-operations.test.d.ts.map +1 -0
  72. package/dist/test/lib/ai-service/task-operations.test.js +362 -0
  73. package/dist/test/mocks/mock-ai-operations.d.ts +15 -0
  74. package/dist/test/mocks/mock-ai-operations.d.ts.map +1 -0
  75. package/dist/test/mocks/mock-ai-operations.js +107 -0
  76. package/dist/test/mocks/mock-context-builder.d.ts +10 -0
  77. package/dist/test/mocks/mock-context-builder.d.ts.map +1 -0
  78. package/dist/test/mocks/mock-context-builder.js +81 -0
  79. package/dist/test/mocks/mock-model-provider.d.ts +7 -0
  80. package/dist/test/mocks/mock-model-provider.d.ts.map +1 -0
  81. package/dist/test/mocks/mock-model-provider.js +21 -0
  82. package/dist/test/mocks/mock-service-factory.d.ts +11 -0
  83. package/dist/test/mocks/mock-service-factory.d.ts.map +1 -0
  84. package/dist/test/mocks/mock-service-factory.js +61 -0
  85. package/dist/test/mocks/mock-storage.d.ts +50 -0
  86. package/dist/test/mocks/mock-storage.d.ts.map +1 -0
  87. package/dist/test/mocks/mock-storage.js +145 -0
  88. package/dist/test/services/task-service.test.d.ts +2 -0
  89. package/dist/test/services/task-service.test.d.ts.map +1 -0
  90. package/dist/test/services/task-service.test.js +352 -0
  91. package/dist/test/test-mock-setup.d.ts +26 -0
  92. package/dist/test/test-mock-setup.d.ts.map +1 -0
  93. package/dist/test/test-mock-setup.js +41 -0
  94. package/dist/test/test-setup.d.ts +9 -0
  95. package/dist/test/test-setup.d.ts.map +1 -0
  96. package/dist/test/test-setup.js +44 -0
  97. package/dist/test/test-utils.d.ts +22 -0
  98. package/dist/test/test-utils.d.ts.map +1 -0
  99. package/dist/test/test-utils.js +37 -0
  100. package/dist/test/utils/ai-operation-utility.test.d.ts +2 -0
  101. package/dist/test/utils/ai-operation-utility.test.d.ts.map +1 -0
  102. package/dist/test/utils/ai-operation-utility.test.js +290 -0
  103. package/dist/test/utils/error-handling.test.d.ts +2 -0
  104. package/dist/test/utils/error-handling.test.d.ts.map +1 -0
  105. package/dist/test/utils/error-handling.test.js +231 -0
  106. package/dist/types/index.d.ts +36 -1
  107. package/dist/types/index.d.ts.map +1 -1
  108. package/dist/types/results.d.ts +60 -6
  109. package/dist/types/results.d.ts.map +1 -1
  110. package/dist/utils/ai-operation-utility.d.ts +142 -0
  111. package/dist/utils/ai-operation-utility.d.ts.map +1 -0
  112. package/dist/utils/ai-operation-utility.js +288 -0
  113. package/dist/utils/ai-service-factory.d.ts +10 -0
  114. package/dist/utils/ai-service-factory.d.ts.map +1 -1
  115. package/dist/utils/ai-service-factory.js +19 -1
  116. package/dist/utils/cli-validators.d.ts +2 -2
  117. package/dist/utils/cli-validators.d.ts.map +1 -1
  118. package/dist/utils/cli-validators.js +7 -6
  119. package/dist/utils/error-utils.d.ts +70 -0
  120. package/dist/utils/error-utils.d.ts.map +1 -0
  121. package/dist/utils/error-utils.js +104 -0
  122. package/dist/utils/file-utils.d.ts +49 -0
  123. package/dist/utils/file-utils.d.ts.map +1 -0
  124. package/dist/utils/file-utils.js +82 -0
  125. package/dist/utils/id-generator.d.ts +92 -0
  126. package/dist/utils/id-generator.d.ts.map +1 -0
  127. package/dist/utils/id-generator.js +146 -0
  128. package/dist/utils/model-executor-parser.d.ts +1 -1
  129. package/dist/utils/model-executor-parser.d.ts.map +1 -1
  130. package/dist/utils/model-executor-parser.js +3 -2
  131. package/dist/utils/stack-formatter.d.ts +2 -1
  132. package/dist/utils/stack-formatter.d.ts.map +1 -1
  133. package/dist/utils/stack-formatter.js +8 -2
  134. package/dist/utils/storage-utils.d.ts +49 -0
  135. package/dist/utils/storage-utils.d.ts.map +1 -0
  136. package/dist/utils/storage-utils.js +80 -0
  137. package/dist/utils/streaming-utils.d.ts +38 -0
  138. package/dist/utils/streaming-utils.d.ts.map +1 -0
  139. package/dist/utils/streaming-utils.js +56 -0
  140. package/dist/utils/task-o-matic-error.d.ts +206 -0
  141. package/dist/utils/task-o-matic-error.d.ts.map +1 -0
  142. package/dist/utils/task-o-matic-error.js +304 -0
  143. package/docs/agents/cli.md +58 -149
  144. package/package.json +2 -2
@@ -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
- * This service is framework-agnostic and can be used by CLI, TUI, or Web
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
- hooks_1.hooks.emit("task:progress", {
158
+ this.hooks.emit("task:progress", {
55
159
  message: "Building context for task...",
56
160
  type: "progress",
57
161
  });
58
- const context = await (0, ai_service_factory_1.getContextBuilder)().buildContextForNewTask(input.title, input.content);
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
- hooks_1.hooks.emit("task:progress", {
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
- content = await (0, ai_service_factory_1.getAIOperations)().enhanceTaskWithDocumentation(`task-${Date.now()}`, input.title, taskDescription, stackInfo, input.streamingOptions, undefined, enhancementAIConfig, context.existingResearch);
67
- const aiConfig = (0, ai_service_factory_1.getModelProvider)().getAIConfig();
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
- hooks_1.hooks.emit("task:progress", {
194
+ this.hooks.emit("task:progress", {
79
195
  message: "Saving task...",
80
196
  type: "progress",
81
197
  });
82
- const task = await (0, ai_service_factory_1.getStorage)().createTask({
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
- hooks_1.hooks.emit("task:progress", {
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 hooks_1.hooks.emit("task:created", { task });
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 = (0, ai_service_factory_1.getStorage)();
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 (0, ai_service_factory_1.getStorage)().getTask(id);
248
+ return await this.storage.getTask(id);
112
249
  }
113
250
  async getTaskContent(id) {
114
- return await (0, ai_service_factory_1.getStorage)().getTaskContent(id);
251
+ return await this.storage.getTaskContent(id);
115
252
  }
116
253
  async getTaskAIMetadata(id) {
117
- return await (0, ai_service_factory_1.getStorage)().getTaskAIMetadata(id);
254
+ return await this.storage.getTaskAIMetadata(id);
118
255
  }
119
256
  async getSubtasks(id) {
120
- return await (0, ai_service_factory_1.getStorage)().getSubtasks(id);
257
+ return await this.storage.getSubtasks(id);
121
258
  }
122
259
  async updateTask(id, updates) {
123
- const storage = (0, ai_service_factory_1.getStorage)();
260
+ const storage = this.storage;
124
261
  const existingTask = await storage.getTask(id);
125
262
  if (!existingTask) {
126
- throw new Error(`Task with ID ${id} not found`);
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 new Error(`Invalid status transition from ${existingTask.status} to ${updates.status}`);
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 new Error(`Failed to update task ${id}`);
285
+ throw (0, task_o_matic_error_1.formatStorageError)(`updateTask ${id}`);
149
286
  }
150
287
  // Emit task:updated event
151
- await hooks_1.hooks.emit("task:updated", {
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 hooks_1.hooks.emit("task:status-changed", {
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 = (0, ai_service_factory_1.getStorage)();
307
+ const storage = this.storage;
171
308
  const taskToDelete = await storage.getTask(id);
172
309
  if (!taskToDelete) {
173
- throw new Error(`Task with ID ${id} not found`);
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 new Error(`Task has ${subtasks.length} subtasks. Use cascade to delete them or force to orphan them`);
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 hooks_1.hooks.emit("task:deleted", { taskId: id });
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 = (0, ai_service_factory_1.getStorage)();
358
+ const storage = this.storage;
214
359
  const task = await storage.getTask(id);
215
360
  if (!task) {
216
- throw new Error(`Task with ID ${id} not found`);
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 new Error(`Failed to add tags to task ${id}`);
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 = (0, ai_service_factory_1.getStorage)();
376
+ const storage = this.storage;
232
377
  const task = await storage.getTask(id);
233
378
  if (!task) {
234
- throw new Error(`Task with ID ${id} not found`);
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 new Error(`Failed to remove tags from task ${id}`);
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 = (0, ai_service_factory_1.getStorage)();
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 = (0, ai_service_factory_1.getStorage)();
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 new Error(`Task with ID ${rootId} not found`);
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
- hooks_1.hooks.emit("task:progress", {
521
+ this.hooks.emit("task:progress", {
313
522
  message: "Starting task enhancement...",
314
523
  type: "started",
315
524
  });
316
- const task = await (0, ai_service_factory_1.getStorage)().getTask(taskId);
525
+ const task = await this.storage.getTask(taskId);
317
526
  if (!task) {
318
- throw new Error(`Task with ID ${taskId} not found`);
527
+ throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
319
528
  }
320
- hooks_1.hooks.emit("task:progress", {
529
+ this.hooks.emit("task:progress", {
321
530
  message: "Building context...",
322
531
  type: "progress",
323
532
  });
324
- const context = await (0, ai_service_factory_1.getContextBuilder)().buildContext(taskId);
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
- hooks_1.hooks.emit("task:progress", {
536
+ this.hooks.emit("task:progress", {
328
537
  message: "Calling AI for enhancement...",
329
538
  type: "progress",
330
539
  });
331
- // Capture metrics
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
- // Wrap streaming options to capture metrics
336
- const metricsStreamingOptions = {
337
- ...streamingOptions,
338
- onFinish: async (result) => {
339
- if (result.usage) {
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 (0, ai_service_factory_1.getStorage)().saveEnhancedTaskContent(task.id, enhancedContent);
365
- await (0, ai_service_factory_1.getStorage)().updateTask(task.id, {
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 (0, ai_service_factory_1.getStorage)().updateTask(task.id, { description: enhancedContent });
560
+ await this.storage.updateTask(task.id, { description: enhancedContent });
373
561
  }
374
- const aiConfig = (0, ai_service_factory_1.getModelProvider)().getAIConfig();
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 (0, ai_service_factory_1.getStorage)().saveTaskAIMetadata(aiMetadata);
572
+ await this.storage.saveTaskAIMetadata(aiMetadata);
385
573
  const duration = Date.now() - startTime;
386
- hooks_1.hooks.emit("task:progress", {
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
- hooks_1.hooks.emit("task:progress", {
645
+ this.hooks.emit("task:progress", {
412
646
  message: "Starting task breakdown...",
413
647
  type: "started",
414
648
  });
415
- const task = await (0, ai_service_factory_1.getStorage)().getTask(taskId);
649
+ const task = await this.storage.getTask(taskId);
416
650
  if (!task) {
417
- throw new Error(`Task with ID ${taskId} not found`);
651
+ throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
418
652
  }
419
653
  // Check if task already has subtasks
420
- const existingSubtasks = await (0, ai_service_factory_1.getStorage)().getSubtasks(taskId);
654
+ const existingSubtasks = await this.storage.getSubtasks(taskId);
421
655
  if (existingSubtasks.length > 0) {
422
- throw new Error(`Task ${task.title} already has ${existingSubtasks.length} subtasks. Use existing subtasks or delete them first.`);
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
- hooks_1.hooks.emit("task:progress", {
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 (0, ai_service_factory_1.getContextBuilder)().buildContext(taskId);
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
- hooks_1.hooks.emit("task:progress", {
676
+ this.hooks.emit("task:progress", {
435
677
  message: "Calling AI to break down task...",
436
678
  type: "progress",
437
679
  });
438
- // Capture metrics
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
- // Wrap streaming options to capture metrics
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 (0, ai_service_factory_1.getAIOperations)().breakdownTask(task, breakdownAIConfig, promptOverride, messageOverride, metricsStreamingOptions, undefined, fullContent, stackInfo, existingSubtasks, enableFilesystemTools);
466
- hooks_1.hooks.emit("task:progress", {
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
- hooks_1.hooks.emit("task:progress", {
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
- // Create AI metadata for tracking using actual AI config
487
- const aiConfig = (0, ai_service_factory_1.getModelProvider)().getAIConfig();
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: Date.now(),
732
+ splitAt: splitTimestamp,
733
+ subtasksCreated: createdSubtasks.length,
497
734
  };
498
- await (0, ai_service_factory_1.getStorage)().saveTaskAIMetadata(aiMetadata);
735
+ await (0, ai_service_factory_1.getStorage)().saveTaskAIMetadata(parentMetadata);
499
736
  const duration = Date.now() - startTime;
500
- hooks_1.hooks.emit("task:progress", {
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
- hooks_1.hooks.emit("task:progress", {
799
+ this.hooks.emit("task:progress", {
525
800
  message: "Analyzing documentation needs...",
526
801
  type: "started",
527
802
  });
528
- const task = await (0, ai_service_factory_1.getStorage)().getTask(taskId);
803
+ const task = await this.storage.getTask(taskId);
529
804
  if (!task) {
530
- throw new Error(`Task with ID ${taskId} not found`);
805
+ throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
531
806
  }
532
807
  if (task.documentation && !force) {
533
- if ((0, ai_service_factory_1.getContextBuilder)().isDocumentationFresh(task.documentation)) {
534
- hooks_1.hooks.emit("task:progress", {
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
- hooks_1.hooks.emit("task:progress", {
823
+ this.hooks.emit("task:progress", {
549
824
  message: "Building context...",
550
825
  type: "progress",
551
826
  });
552
- const context = await (0, ai_service_factory_1.getContextBuilder)().buildContext(taskId);
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 new Error("Task content is empty");
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 (0, ai_service_factory_1.getStorage)().getTasks();
843
+ const tasks = await this.storage.getTasks();
562
844
  const documentations = tasks.map((task) => task.documentation);
563
- hooks_1.hooks.emit("task:progress", {
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 (0, ai_service_factory_1.getAIOperations)().analyzeDocumentationNeeds(task.id, task.title, fullContent, stackInfo, streamingOptions, undefined, analysisAIConfig, documentations);
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
- hooks_1.hooks.emit("task:progress", {
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 = (0, ai_service_factory_1.getStorage)().sanitizeForFilename(lib.name);
579
- const sanitizedQuery = (0, ai_service_factory_1.getStorage)().sanitizeForFilename(lib.searchQuery);
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
- hooks_1.hooks.emit("task:progress", {
871
+ this.hooks.emit("task:progress", {
590
872
  message: "Generating documentation recap...",
591
873
  type: "progress",
592
874
  });
593
- const recap = await (0, ai_service_factory_1.getAIOperations)().generateDocumentationRecap(analysis.libraries, analysis.toolResults?.map((tr) => ({
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
- hooks_1.hooks.emit("task:progress", {
886
+ this.hooks.emit("task:progress", {
605
887
  message: "Saving documentation...",
606
888
  type: "progress",
607
889
  });
608
- await (0, ai_service_factory_1.getStorage)().updateTask(taskId, { documentation });
890
+ await this.storage.updateTask(taskId, { documentation });
609
891
  }
610
892
  const duration = Date.now() - startTime;
611
- hooks_1.hooks.emit("task:progress", {
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
- hooks_1.hooks.emit("task:progress", {
949
+ this.hooks.emit("task:progress", {
628
950
  message: "Creating implementation plan...",
629
951
  type: "started",
630
952
  });
631
- const task = await (0, ai_service_factory_1.getStorage)().getTask(taskId);
953
+ const task = await this.storage.getTask(taskId);
632
954
  if (!task) {
633
- throw new Error(`Task with ID ${taskId} not found`);
955
+ throw (0, task_o_matic_error_1.formatTaskNotFoundError)(taskId);
634
956
  }
635
- const aiService = (0, ai_service_factory_1.getAIOperations)();
957
+ const aiService = this.aiOperations;
636
958
  const planAIConfig = (0, ai_config_builder_1.buildAIConfig)(aiOptions);
637
- hooks_1.hooks.emit("task:progress", {
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
- hooks_1.hooks.emit("task:progress", {
983
+ this.hooks.emit("task:progress", {
662
984
  message: "Calling AI to create plan...",
663
985
  type: "progress",
664
986
  });
665
- // Capture metrics
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
- // Wrap streaming options to capture metrics
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
- hooks_1.hooks.emit("task:progress", {
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 (0, ai_service_factory_1.getStorage)().savePlan(taskId, plan);
998
+ await this.storage.savePlan(taskId, plan);
698
999
  const duration = Date.now() - startTime;
699
- hooks_1.hooks.emit("task:progress", {
1000
+ this.hooks.emit("task:progress", {
700
1001
  message: "Implementation plan created",
701
1002
  type: "completed",
702
1003
  });
703
- const aiConfig = (0, ai_service_factory_1.getModelProvider)().getAIConfig();
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 (0, ai_service_factory_1.getStorage)().getTaskDocumentation(taskId);
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 new Error(`Task with ID ${taskId} not found`);
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 new Error(`Documentation file not found: ${filePath}`);
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 (0, ai_service_factory_1.getStorage)().saveTaskDocumentation(taskId, content);
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 new Error(`Failed to add documentation from file: ${error instanceof Error ? error.message : "Unknown error"}`);
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 new Error(`Task with ID ${taskId} not found`);
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 new Error(`Plan file not found: ${planFilePath}`);
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 new Error(`Failed to read plan file: ${error instanceof Error ? error.message : "Unknown error"}`);
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 new Error("Either planText or planFilePath must be provided");
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 (0, ai_service_factory_1.getStorage)().savePlan(taskId, plan);
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 (0, ai_service_factory_1.getStorage)().getPlan(taskId);
1120
+ return this.storage.getPlan(taskId);
787
1121
  }
788
1122
  async listTaskPlans() {
789
- return (0, ai_service_factory_1.getStorage)().listPlans();
1123
+ return this.storage.listPlans();
790
1124
  }
791
1125
  async deleteTaskPlan(taskId) {
792
- return (0, ai_service_factory_1.getStorage)().deletePlan(taskId);
1126
+ return this.storage.deletePlan(taskId);
793
1127
  }
794
1128
  }
795
1129
  exports.TaskService = TaskService;
796
- // Export singleton instance
797
- exports.taskService = new TaskService();
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
+ });