clavix 2.4.3 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/cli/commands/task-complete.d.ts +27 -0
  2. package/dist/cli/commands/task-complete.js +305 -0
  3. package/dist/core/config-manager.d.ts +149 -0
  4. package/dist/core/config-manager.js +267 -0
  5. package/dist/core/git-manager.d.ts +27 -1
  6. package/dist/core/git-manager.js +114 -10
  7. package/dist/core/task-manager.d.ts +31 -0
  8. package/dist/core/task-manager.js +142 -0
  9. package/dist/templates/slash-commands/_canonical/implement.md +86 -33
  10. package/package.json +1 -1
  11. /package/dist/{core 2/adapters → core/adapters 2}/agents-md-generator.d.ts +0 -0
  12. /package/dist/{core 2/adapters → core/adapters 2}/agents-md-generator.js +0 -0
  13. /package/dist/{core 2/adapters → core/adapters 2}/amp-adapter.d.ts +0 -0
  14. /package/dist/{core 2/adapters → core/adapters 2}/amp-adapter.js +0 -0
  15. /package/dist/{core 2/adapters → core/adapters 2}/augment-adapter.d.ts +0 -0
  16. /package/dist/{core 2/adapters → core/adapters 2}/augment-adapter.js +0 -0
  17. /package/dist/{core 2/adapters → core/adapters 2}/base-adapter.d.ts +0 -0
  18. /package/dist/{core 2/adapters → core/adapters 2}/base-adapter.js +0 -0
  19. /package/dist/{core 2/adapters → core/adapters 2}/claude-code-adapter.d.ts +0 -0
  20. /package/dist/{core 2/adapters → core/adapters 2}/claude-code-adapter.js +0 -0
  21. /package/dist/{core 2/adapters → core/adapters 2}/cline-adapter.d.ts +0 -0
  22. /package/dist/{core 2/adapters → core/adapters 2}/cline-adapter.js +0 -0
  23. /package/dist/{core 2/adapters → core/adapters 2}/codebuddy-adapter.d.ts +0 -0
  24. /package/dist/{core 2/adapters → core/adapters 2}/codebuddy-adapter.js +0 -0
  25. /package/dist/{core 2/adapters → core/adapters 2}/codex-adapter.d.ts +0 -0
  26. /package/dist/{core 2/adapters → core/adapters 2}/codex-adapter.js +0 -0
  27. /package/dist/{core 2/adapters → core/adapters 2}/copilot-instructions-generator.d.ts +0 -0
  28. /package/dist/{core 2/adapters → core/adapters 2}/copilot-instructions-generator.js +0 -0
  29. /package/dist/{core 2/adapters → core/adapters 2}/crush-adapter.d.ts +0 -0
  30. /package/dist/{core 2/adapters → core/adapters 2}/crush-adapter.js +0 -0
  31. /package/dist/{core 2/adapters → core/adapters 2}/cursor-adapter.d.ts +0 -0
  32. /package/dist/{core 2/adapters → core/adapters 2}/cursor-adapter.js +0 -0
  33. /package/dist/{core 2/adapters → core/adapters 2}/droid-adapter.d.ts +0 -0
  34. /package/dist/{core 2/adapters → core/adapters 2}/droid-adapter.js +0 -0
  35. /package/dist/{core 2/adapters → core/adapters 2}/gemini-adapter.d.ts +0 -0
  36. /package/dist/{core 2/adapters → core/adapters 2}/gemini-adapter.js +0 -0
  37. /package/dist/{core 2/adapters → core/adapters 2}/kilocode-adapter.d.ts +0 -0
  38. /package/dist/{core 2/adapters → core/adapters 2}/kilocode-adapter.js +0 -0
  39. /package/dist/{core 2/adapters → core/adapters 2}/octo-md-generator.d.ts +0 -0
  40. /package/dist/{core 2/adapters → core/adapters 2}/octo-md-generator.js +0 -0
  41. /package/dist/{core 2/adapters → core/adapters 2}/opencode-adapter.d.ts +0 -0
  42. /package/dist/{core 2/adapters → core/adapters 2}/opencode-adapter.js +0 -0
  43. /package/dist/{core 2/adapters → core/adapters 2}/qwen-adapter.d.ts +0 -0
  44. /package/dist/{core 2/adapters → core/adapters 2}/qwen-adapter.js +0 -0
  45. /package/dist/{core 2/adapters → core/adapters 2}/roocode-adapter.d.ts +0 -0
  46. /package/dist/{core 2/adapters → core/adapters 2}/roocode-adapter.js +0 -0
  47. /package/dist/{core 2/adapters → core/adapters 2}/warp-md-generator.d.ts +0 -0
  48. /package/dist/{core 2/adapters → core/adapters 2}/warp-md-generator.js +0 -0
  49. /package/dist/{core 2/adapters → core/adapters 2}/windsurf-adapter.d.ts +0 -0
  50. /package/dist/{core 2/adapters → core/adapters 2}/windsurf-adapter.js +0 -0
  51. /package/dist/{core 2/agent-manager.js → core/agent-manager 2.js} +0 -0
  52. /package/dist/{core 2/agent-manager.d.ts → core/agent-manager.d 2.ts} +0 -0
  53. /package/dist/{core 2/archive-manager.js → core/archive-manager 2.js} +0 -0
  54. /package/dist/{core 2/archive-manager.d.ts → core/archive-manager.d 2.ts} +0 -0
  55. /package/dist/{core 2/conversation-analyzer.d.ts → core/conversation-analyzer.d 2.ts} +0 -0
  56. /package/dist/{core 2/doc-injector.js → core/doc-injector 2.js} +0 -0
  57. /package/dist/{core 2/doc-injector.d.ts → core/doc-injector.d 2.ts} +0 -0
  58. /package/dist/{core 2/git-manager.js → core/git-manager 2.js} +0 -0
  59. /package/dist/{core 2/git-manager.d.ts → core/git-manager.d 2.ts} +0 -0
  60. /package/dist/{core 2/prompt-optimizer.js → core/prompt-optimizer 2.js} +0 -0
  61. /package/dist/{core 2/prompt-optimizer.d.ts → core/prompt-optimizer.d 2.ts} +0 -0
  62. /package/dist/{core 2/question-engine.js → core/question-engine 2.js} +0 -0
  63. /package/dist/{core 2/question-engine.d.ts → core/question-engine.d 2.ts} +0 -0
  64. /package/dist/{core 2/session-manager.js → core/session-manager 2.js} +0 -0
  65. /package/dist/{core 2/session-manager.d.ts → core/session-manager.d 2.ts} +0 -0
  66. /package/dist/{core 2/task-manager.js → core/task-manager 2.js} +0 -0
  67. /package/dist/{core 2/task-manager.d.ts → core/task-manager.d 2.ts} +0 -0
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+ /**
3
+ * ConfigManager - Manages .clavix-implement-config.json
4
+ *
5
+ * This class handles:
6
+ * - Reading/writing implementation configuration
7
+ * - Tracking task completion state
8
+ * - Managing resume checkpoints
9
+ * - Storing git commit strategy preferences
10
+ */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
44
+ Object.defineProperty(exports, "__esModule", { value: true });
45
+ exports.ConfigManager = void 0;
46
+ const fs = __importStar(require("fs-extra"));
47
+ const path = __importStar(require("path"));
48
+ /**
49
+ * ConfigManager class
50
+ *
51
+ * Manages implementation configuration and task tracking state
52
+ */
53
+ class ConfigManager {
54
+ /**
55
+ * Read implementation config from file
56
+ * @param configPath - Path to config file (.clavix-implement-config.json)
57
+ * @returns Implementation configuration
58
+ */
59
+ async read(configPath) {
60
+ if (!(await fs.pathExists(configPath))) {
61
+ throw new Error(`Config file not found: ${configPath}\n\nHint: Run "clavix implement" first to initialize configuration`);
62
+ }
63
+ try {
64
+ const config = await fs.readJson(configPath);
65
+ // Migrate old config format if needed
66
+ return this.migrateConfig(config);
67
+ }
68
+ catch (error) {
69
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
70
+ throw new Error(`Failed to read config file: ${errorMessage}`);
71
+ }
72
+ }
73
+ /**
74
+ * Write implementation config to file
75
+ * @param configPath - Path to config file
76
+ * @param config - Configuration to write
77
+ * @param options - Update options
78
+ */
79
+ async write(configPath, config, options = {}) {
80
+ const { validate = true } = options;
81
+ if (validate) {
82
+ this.validateConfig(config);
83
+ }
84
+ try {
85
+ await fs.writeJson(configPath, config, { spaces: 2 });
86
+ }
87
+ catch (error) {
88
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
89
+ throw new Error(`Failed to write config file: ${errorMessage}`);
90
+ }
91
+ }
92
+ /**
93
+ * Update specific fields in config
94
+ * @param configPath - Path to config file
95
+ * @param updates - Partial config updates
96
+ */
97
+ async update(configPath, updates) {
98
+ const existing = await this.read(configPath);
99
+ const updated = {
100
+ ...existing,
101
+ ...updates,
102
+ timestamp: new Date().toISOString(),
103
+ };
104
+ await this.write(configPath, updated);
105
+ }
106
+ /**
107
+ * Track a task completion
108
+ * @param configPath - Path to config file
109
+ * @param taskId - ID of completed task
110
+ */
111
+ async trackCompletion(configPath, taskId) {
112
+ const config = await this.read(configPath);
113
+ // Initialize tracking arrays if not present
114
+ if (!config.completedTaskIds) {
115
+ config.completedTaskIds = [];
116
+ }
117
+ if (!config.completionTimestamps) {
118
+ config.completionTimestamps = {};
119
+ }
120
+ // Add to completed tasks (avoid duplicates)
121
+ if (!config.completedTaskIds.includes(taskId)) {
122
+ config.completedTaskIds.push(taskId);
123
+ }
124
+ // Record completion timestamp
125
+ config.completionTimestamps[taskId] = new Date().toISOString();
126
+ // Update last completed task
127
+ config.lastCompletedTaskId = taskId;
128
+ // Update timestamp
129
+ config.timestamp = new Date().toISOString();
130
+ await this.write(configPath, config);
131
+ }
132
+ /**
133
+ * Add a blocked task
134
+ * @param configPath - Path to config file
135
+ * @param taskId - ID of blocked task
136
+ * @param reason - Reason for blocking
137
+ */
138
+ async addBlockedTask(configPath, taskId, reason) {
139
+ const config = await this.read(configPath);
140
+ if (!config.blockedTasks) {
141
+ config.blockedTasks = [];
142
+ }
143
+ // Remove existing entry for this task if present
144
+ config.blockedTasks = config.blockedTasks.filter(b => b.taskId !== taskId);
145
+ // Add new blocked task
146
+ config.blockedTasks.push({
147
+ taskId,
148
+ reason,
149
+ timestamp: new Date().toISOString(),
150
+ });
151
+ config.timestamp = new Date().toISOString();
152
+ await this.write(configPath, config);
153
+ }
154
+ /**
155
+ * Remove a blocked task (unblock)
156
+ * @param configPath - Path to config file
157
+ * @param taskId - ID of task to unblock
158
+ */
159
+ async removeBlockedTask(configPath, taskId) {
160
+ const config = await this.read(configPath);
161
+ if (config.blockedTasks) {
162
+ config.blockedTasks = config.blockedTasks.filter(b => b.taskId !== taskId);
163
+ config.timestamp = new Date().toISOString();
164
+ await this.write(configPath, config);
165
+ }
166
+ }
167
+ /**
168
+ * Get current implementation state
169
+ * @param configPath - Path to config file
170
+ * @returns State summary
171
+ */
172
+ async getState(configPath) {
173
+ const config = await this.read(configPath);
174
+ return {
175
+ currentTaskId: config.currentTask.id,
176
+ completedCount: config.completedTaskIds?.length ?? 0,
177
+ remainingCount: config.stats.remaining,
178
+ blockedCount: config.blockedTasks?.length ?? 0,
179
+ lastCompletedTaskId: config.lastCompletedTaskId,
180
+ lastCompletionTime: config.lastCompletedTaskId
181
+ ? config.completionTimestamps?.[config.lastCompletedTaskId]
182
+ : undefined,
183
+ };
184
+ }
185
+ /**
186
+ * Check if a task has been completed
187
+ * @param configPath - Path to config file
188
+ * @param taskId - Task ID to check
189
+ * @returns true if task is in completed list
190
+ */
191
+ async isTaskCompleted(configPath, taskId) {
192
+ const config = await this.read(configPath);
193
+ return config.completedTaskIds?.includes(taskId) ?? false;
194
+ }
195
+ /**
196
+ * Validate config structure
197
+ * @param config - Configuration to validate
198
+ * @throws Error if config is invalid
199
+ */
200
+ validateConfig(config) {
201
+ if (!config.commitStrategy) {
202
+ throw new Error('Config validation failed: commitStrategy is required');
203
+ }
204
+ if (!config.tasksPath) {
205
+ throw new Error('Config validation failed: tasksPath is required');
206
+ }
207
+ if (!config.currentTask) {
208
+ throw new Error('Config validation failed: currentTask is required');
209
+ }
210
+ if (!config.stats) {
211
+ throw new Error('Config validation failed: stats is required');
212
+ }
213
+ const validStrategies = ['per-task', 'per-5-tasks', 'per-phase', 'none'];
214
+ if (!validStrategies.includes(config.commitStrategy)) {
215
+ throw new Error(`Config validation failed: invalid commitStrategy "${config.commitStrategy}"`);
216
+ }
217
+ }
218
+ /**
219
+ * Migrate old config format to new format
220
+ * @param config - Old config format
221
+ * @returns Migrated config
222
+ */
223
+ migrateConfig(config) {
224
+ // If already has new fields, return as-is
225
+ if (config.completedTaskIds !== undefined) {
226
+ return config;
227
+ }
228
+ // Migrate from old format
229
+ const migrated = {
230
+ commitStrategy: config.commitStrategy,
231
+ tasksPath: config.tasksPath,
232
+ currentTask: config.currentTask,
233
+ stats: config.stats,
234
+ timestamp: config.timestamp ?? new Date().toISOString(),
235
+ completedTaskIds: [],
236
+ completionTimestamps: {},
237
+ blockedTasks: [],
238
+ };
239
+ return migrated;
240
+ }
241
+ /**
242
+ * Create a resume checkpoint
243
+ * @param configPath - Path to config file
244
+ * @param currentTaskId - Current task being worked on
245
+ * @param phaseProgress - Progress by phase (phase name -> completed count)
246
+ */
247
+ async createResumeCheckpoint(configPath, currentTaskId, phaseProgress) {
248
+ const config = await this.read(configPath);
249
+ config.resumeCheckpoint = {
250
+ lastTaskId: currentTaskId,
251
+ phaseProgress,
252
+ sessionStartTime: new Date().toISOString(),
253
+ };
254
+ config.timestamp = new Date().toISOString();
255
+ await this.write(configPath, config);
256
+ }
257
+ /**
258
+ * Get the config file path for a PRD directory
259
+ * @param prdPath - Path to PRD directory
260
+ * @returns Path to config file
261
+ */
262
+ static getConfigPath(prdPath) {
263
+ return path.join(prdPath, '.clavix-implement-config.json');
264
+ }
265
+ }
266
+ exports.ConfigManager = ConfigManager;
267
+ //# sourceMappingURL=config-manager.js.map
@@ -15,7 +15,9 @@ export type CommitStrategy = 'per-phase' | 'per-5-tasks' | 'per-task' | 'none';
15
15
  * Options for creating a commit
16
16
  */
17
17
  export interface CommitOptions {
18
- tasks: string[];
18
+ message?: string;
19
+ description?: string;
20
+ tasks?: string[];
19
21
  phase?: string;
20
22
  projectName?: string;
21
23
  }
@@ -41,6 +43,30 @@ export declare class GitManager {
41
43
  * Generate commit message from tasks
42
44
  */
43
45
  private generateCommitMessage;
46
+ /**
47
+ * Validate that tasks are marked before committing
48
+ * This ensures the tasks.md file matches what we expect to commit
49
+ * @param tasksPath - Path to tasks.md file
50
+ * @param completedTaskIds - Array of task IDs that should be completed
51
+ * @returns Validation result with any errors
52
+ */
53
+ validateBeforeCommit(tasksPath: string, completedTaskIds: string[]): Promise<{
54
+ valid: boolean;
55
+ errors?: string[];
56
+ }>;
57
+ /**
58
+ * Create commit with validation
59
+ * Validates that tasks are marked before committing
60
+ * @param options - Commit options
61
+ * @param tasksPath - Path to tasks.md file
62
+ * @param completedTaskIds - Array of task IDs that should be completed
63
+ * @returns Commit result with validation status
64
+ */
65
+ createCommitWithValidation(options: CommitOptions, tasksPath?: string, completedTaskIds?: string[]): Promise<{
66
+ success: boolean;
67
+ validated: boolean;
68
+ errors?: string[];
69
+ }>;
44
70
  /**
45
71
  * Escape commit message for shell
46
72
  */
@@ -8,6 +8,39 @@
8
8
  * - Generating commit messages
9
9
  * - Handling commit strategies (per task, per phase, per N tasks)
10
10
  */
11
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
+ if (k2 === undefined) k2 = k;
13
+ var desc = Object.getOwnPropertyDescriptor(m, k);
14
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
15
+ desc = { enumerable: true, get: function() { return m[k]; } };
16
+ }
17
+ Object.defineProperty(o, k2, desc);
18
+ }) : (function(o, m, k, k2) {
19
+ if (k2 === undefined) k2 = k;
20
+ o[k2] = m[k];
21
+ }));
22
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
23
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
24
+ }) : function(o, v) {
25
+ o["default"] = v;
26
+ });
27
+ var __importStar = (this && this.__importStar) || (function () {
28
+ var ownKeys = function(o) {
29
+ ownKeys = Object.getOwnPropertyNames || function (o) {
30
+ var ar = [];
31
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
32
+ return ar;
33
+ };
34
+ return ownKeys(o);
35
+ };
36
+ return function (mod) {
37
+ if (mod && mod.__esModule) return mod;
38
+ var result = {};
39
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
40
+ __setModuleDefault(result, mod);
41
+ return result;
42
+ };
43
+ })();
11
44
  Object.defineProperty(exports, "__esModule", { value: true });
12
45
  exports.CommitScheduler = exports.GitManager = void 0;
13
46
  const child_process_1 = require("child_process");
@@ -54,8 +87,8 @@ class GitManager {
54
87
  }
55
88
  // Stage all changes
56
89
  await execAsync('git add .');
57
- // Generate commit message
58
- const message = this.generateCommitMessage(options);
90
+ // Use provided message or generate one
91
+ const message = options.message ?? this.generateCommitMessage(options);
59
92
  // Create commit
60
93
  await execAsync(`git commit -m "${this.escapeCommitMessage(message)}"`);
61
94
  return true;
@@ -70,31 +103,102 @@ class GitManager {
70
103
  * Generate commit message from tasks
71
104
  */
72
105
  generateCommitMessage(options) {
73
- const { tasks, phase, projectName } = options;
106
+ const { description, tasks = [], phase, projectName } = options;
74
107
  let message = '';
75
108
  // Header
76
109
  if (phase) {
77
110
  message += `clavix: ${phase}\n\n`;
78
111
  }
112
+ else if (description) {
113
+ message += `clavix: ${description}\n\n`;
114
+ }
79
115
  else if (tasks.length === 1) {
80
116
  message += `clavix: ${tasks[0]}\n\n`;
81
117
  }
82
- else {
118
+ else if (tasks.length > 1) {
83
119
  message += `clavix: implement ${tasks.length} tasks\n\n`;
84
120
  }
85
- // Task list
86
- message += 'Completed tasks:\n';
87
- tasks.forEach((task) => {
88
- message += `- ${task}\n`;
89
- });
121
+ else {
122
+ message += `clavix: task completed\n\n`;
123
+ }
124
+ // Task list (if provided)
125
+ if (tasks.length > 0) {
126
+ message += 'Completed tasks:\n';
127
+ tasks.forEach((task) => {
128
+ message += `- ${task}\n`;
129
+ });
130
+ message += '\n';
131
+ }
90
132
  // Footer
91
- message += '\n';
92
133
  if (projectName) {
93
134
  message += `Project: ${projectName}\n`;
94
135
  }
95
136
  message += 'Generated by Clavix /clavix:implement';
96
137
  return message;
97
138
  }
139
+ /**
140
+ * Validate that tasks are marked before committing
141
+ * This ensures the tasks.md file matches what we expect to commit
142
+ * @param tasksPath - Path to tasks.md file
143
+ * @param completedTaskIds - Array of task IDs that should be completed
144
+ * @returns Validation result with any errors
145
+ */
146
+ async validateBeforeCommit(tasksPath, completedTaskIds) {
147
+ const errors = [];
148
+ try {
149
+ // Read tasks.md file
150
+ const fs = await Promise.resolve().then(() => __importStar(require('fs-extra')));
151
+ if (!(await fs.pathExists(tasksPath))) {
152
+ errors.push(`Tasks file not found: ${tasksPath}`);
153
+ return { valid: false, errors };
154
+ }
155
+ const content = await fs.readFile(tasksPath, 'utf-8');
156
+ // Check each task ID is marked as complete in the file
157
+ for (const taskId of completedTaskIds) {
158
+ // Look for the task in the file (should have [x] checkbox)
159
+ const hasCompleteCheckbox = content.includes(`[x]`) && content.includes(taskId);
160
+ if (!hasCompleteCheckbox) {
161
+ errors.push(`Task ${taskId} not marked as completed in tasks.md`);
162
+ }
163
+ }
164
+ return {
165
+ valid: errors.length === 0,
166
+ errors: errors.length > 0 ? errors : undefined,
167
+ };
168
+ }
169
+ catch (error) {
170
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
171
+ errors.push(`Validation failed: ${errorMessage}`);
172
+ return { valid: false, errors };
173
+ }
174
+ }
175
+ /**
176
+ * Create commit with validation
177
+ * Validates that tasks are marked before committing
178
+ * @param options - Commit options
179
+ * @param tasksPath - Path to tasks.md file
180
+ * @param completedTaskIds - Array of task IDs that should be completed
181
+ * @returns Commit result with validation status
182
+ */
183
+ async createCommitWithValidation(options, tasksPath, completedTaskIds) {
184
+ // If validation info provided, validate first
185
+ if (tasksPath && completedTaskIds) {
186
+ const validation = await this.validateBeforeCommit(tasksPath, completedTaskIds);
187
+ if (!validation.valid) {
188
+ return {
189
+ success: false,
190
+ validated: false,
191
+ errors: validation.errors,
192
+ };
193
+ }
194
+ }
195
+ // Create commit
196
+ const success = await this.createCommit(options);
197
+ return {
198
+ success,
199
+ validated: tasksPath !== undefined && completedTaskIds !== undefined,
200
+ };
201
+ }
98
202
  /**
99
203
  * Escape commit message for shell
100
204
  */
@@ -133,6 +133,37 @@ export declare class TaskManager {
133
133
  * Escape special regex characters
134
134
  */
135
135
  private escapeRegex;
136
+ /**
137
+ * Validate that a task exists in the phases
138
+ * @param phases - Array of task phases
139
+ * @param taskId - Task ID to validate
140
+ * @returns The task if found, null otherwise
141
+ */
142
+ validateTaskExists(phases: TaskPhase[], taskId: string): Task | null;
143
+ /**
144
+ * Verify that a task was successfully marked as completed in the file
145
+ * @param tasksPath - Path to tasks.md file
146
+ * @param taskId - Task ID to verify
147
+ * @returns true if task is marked completed, false otherwise
148
+ */
149
+ verifyTaskMarked(tasksPath: string, taskId: string): Promise<boolean>;
150
+ /**
151
+ * Mark a task as completed with validation and error recovery
152
+ * Enhanced version with pre/post validation
153
+ * @param tasksPath - Path to tasks.md file
154
+ * @param taskId - Task ID to mark as completed
155
+ * @param options - Optional configuration for error recovery
156
+ * @returns Object with success status and any warnings/errors
157
+ */
158
+ markTaskCompletedWithValidation(tasksPath: string, taskId: string, options?: {
159
+ retryOnFailure?: boolean;
160
+ createBackup?: boolean;
161
+ }): Promise<{
162
+ success: boolean;
163
+ alreadyCompleted?: boolean;
164
+ error?: string;
165
+ warnings?: string[];
166
+ }>;
136
167
  /**
137
168
  * Get task completion statistics
138
169
  */
@@ -605,6 +605,148 @@ class TaskManager {
605
605
  escapeRegex(str) {
606
606
  return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
607
607
  }
608
+ /**
609
+ * Validate that a task exists in the phases
610
+ * @param phases - Array of task phases
611
+ * @param taskId - Task ID to validate
612
+ * @returns The task if found, null otherwise
613
+ */
614
+ validateTaskExists(phases, taskId) {
615
+ for (const phase of phases) {
616
+ const task = phase.tasks.find(t => t.id === taskId);
617
+ if (task) {
618
+ return task;
619
+ }
620
+ }
621
+ return null;
622
+ }
623
+ /**
624
+ * Verify that a task was successfully marked as completed in the file
625
+ * @param tasksPath - Path to tasks.md file
626
+ * @param taskId - Task ID to verify
627
+ * @returns true if task is marked completed, false otherwise
628
+ */
629
+ async verifyTaskMarked(tasksPath, taskId) {
630
+ try {
631
+ const phases = await this.readTasksFile(tasksPath);
632
+ const task = this.validateTaskExists(phases, taskId);
633
+ return task ? task.completed : false;
634
+ }
635
+ catch (error) {
636
+ // If we can't read the file, verification failed
637
+ return false;
638
+ }
639
+ }
640
+ /**
641
+ * Mark a task as completed with validation and error recovery
642
+ * Enhanced version with pre/post validation
643
+ * @param tasksPath - Path to tasks.md file
644
+ * @param taskId - Task ID to mark as completed
645
+ * @param options - Optional configuration for error recovery
646
+ * @returns Object with success status and any warnings/errors
647
+ */
648
+ async markTaskCompletedWithValidation(tasksPath, taskId, options = {}) {
649
+ const { retryOnFailure = true, createBackup = true } = options;
650
+ const warnings = [];
651
+ // Pre-validation: Check if file exists
652
+ if (!(await fs.pathExists(tasksPath))) {
653
+ return {
654
+ success: false,
655
+ error: `Tasks file not found: ${tasksPath}`,
656
+ };
657
+ }
658
+ // Create backup if requested
659
+ let backupPath = null;
660
+ if (createBackup) {
661
+ backupPath = `${tasksPath}.backup`;
662
+ try {
663
+ await fs.copyFile(tasksPath, backupPath);
664
+ }
665
+ catch (error) {
666
+ warnings.push('Failed to create backup file');
667
+ }
668
+ }
669
+ try {
670
+ // Read and validate task exists
671
+ const phases = await this.readTasksFile(tasksPath);
672
+ const task = this.validateTaskExists(phases, taskId);
673
+ if (!task) {
674
+ // Task not found - provide helpful error
675
+ const allTaskIds = phases.flatMap(p => p.tasks.map(t => t.id));
676
+ return {
677
+ success: false,
678
+ error: `Task ID "${taskId}" not found. Available task IDs:\n${allTaskIds.join('\n')}`,
679
+ };
680
+ }
681
+ // Check if already completed
682
+ if (task.completed) {
683
+ return {
684
+ success: true,
685
+ alreadyCompleted: true,
686
+ warnings: [`Task "${taskId}" was already marked as completed`],
687
+ };
688
+ }
689
+ // Attempt to mark task completed
690
+ await this.markTaskCompleted(tasksPath, taskId);
691
+ // Post-validation: Verify the checkbox was actually changed
692
+ const verified = await this.verifyTaskMarked(tasksPath, taskId);
693
+ if (!verified) {
694
+ // Verification failed - attempt recovery if enabled
695
+ if (retryOnFailure && backupPath) {
696
+ warnings.push('First attempt failed verification, retrying...');
697
+ // Restore from backup and try again
698
+ await fs.copyFile(backupPath, tasksPath);
699
+ await this.markTaskCompleted(tasksPath, taskId);
700
+ // Verify again
701
+ const secondVerification = await this.verifyTaskMarked(tasksPath, taskId);
702
+ if (!secondVerification) {
703
+ // Still failed - restore backup and return error
704
+ await fs.copyFile(backupPath, tasksPath);
705
+ return {
706
+ success: false,
707
+ error: 'Failed to mark task as completed even after retry. File has been restored from backup.',
708
+ warnings,
709
+ };
710
+ }
711
+ warnings.push('Task marked successfully on retry');
712
+ }
713
+ else {
714
+ // No retry - just fail
715
+ return {
716
+ success: false,
717
+ error: 'Task completion verification failed',
718
+ warnings,
719
+ };
720
+ }
721
+ }
722
+ // Clean up backup on success
723
+ if (backupPath && await fs.pathExists(backupPath)) {
724
+ await fs.remove(backupPath);
725
+ }
726
+ return {
727
+ success: true,
728
+ warnings: warnings.length > 0 ? warnings : undefined,
729
+ };
730
+ }
731
+ catch (error) {
732
+ // Restore from backup if available
733
+ if (backupPath && await fs.pathExists(backupPath)) {
734
+ try {
735
+ await fs.copyFile(backupPath, tasksPath);
736
+ warnings.push('Restored tasks.md from backup due to error');
737
+ }
738
+ catch (restoreError) {
739
+ warnings.push('Failed to restore from backup');
740
+ }
741
+ }
742
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
743
+ return {
744
+ success: false,
745
+ error: `Error marking task as completed: ${errorMessage}`,
746
+ warnings: warnings.length > 0 ? warnings : undefined,
747
+ };
748
+ }
749
+ }
608
750
  /**
609
751
  * Get task completion statistics
610
752
  */