vibecodingmachine-core 2025.12.1-534 → 2025.12.6-1702

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.
@@ -0,0 +1,388 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const crypto = require('crypto');
5
+ const EventEmitter = require('events');
6
+
7
+ /**
8
+ * SyncEngine - Handles bidirectional sync between local files and AWS DynamoDB
9
+ *
10
+ * Features:
11
+ * - Real-time change detection using file watchers
12
+ * - Conflict resolution strategies
13
+ * - Offline queue for disconnected state
14
+ * - Change history tracking
15
+ * - Delta sync for efficiency
16
+ */
17
+ class SyncEngine extends EventEmitter {
18
+ constructor(options = {}) {
19
+ super();
20
+
21
+ this.options = {
22
+ syncInterval: options.syncInterval || 30000, // 30 seconds
23
+ conflictStrategy: options.conflictStrategy || 'last-write-wins',
24
+ offlineMode: options.offlineMode || false,
25
+ ...options
26
+ };
27
+
28
+ this.computerId = this._getComputerId();
29
+ this.isOnline = !this.offlineMode;
30
+ this.isSyncing = false;
31
+ this.offlineQueue = [];
32
+ this.lastSyncTime = null;
33
+ this.changeHistory = [];
34
+
35
+ // AWS clients (will be initialized when needed)
36
+ this.dynamoClient = null;
37
+ this.apiClient = null;
38
+ this.wsClient = null;
39
+ }
40
+
41
+ /**
42
+ * Get unique computer ID
43
+ */
44
+ _getComputerId() {
45
+ const hostname = os.hostname();
46
+ return hostname;
47
+ }
48
+
49
+ /**
50
+ * Initialize AWS clients
51
+ */
52
+ async initialize() {
53
+ try {
54
+ // Initialize DynamoDB client
55
+ const { DynamoDBClient } = require('@aws-sdk/client-dynamodb');
56
+ const { DynamoDBDocumentClient } = require('@aws-sdk/lib-dynamodb');
57
+
58
+ const region = process.env.AWS_REGION || 'us-east-1';
59
+ const client = new DynamoDBClient({ region });
60
+ this.dynamoClient = DynamoDBDocumentClient.from(client);
61
+
62
+ // Initialize WebSocket client for real-time updates
63
+ await this._initializeWebSocket();
64
+
65
+ this.emit('initialized');
66
+ return true;
67
+ } catch (error) {
68
+ this.emit('error', { type: 'initialization', error });
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Initialize WebSocket connection for real-time updates
75
+ */
76
+ async _initializeWebSocket() {
77
+ // TODO: Implement WebSocket connection to AWS IoT Core or API Gateway WebSocket
78
+ // For now, we'll use polling
79
+ this.emit('websocket-status', { connected: false, reason: 'not-implemented' });
80
+ }
81
+
82
+ /**
83
+ * Start sync engine
84
+ */
85
+ async start() {
86
+ if (!this.dynamoClient) {
87
+ await this.initialize();
88
+ }
89
+
90
+ // Start periodic sync
91
+ this.syncInterval = setInterval(() => {
92
+ this.sync().catch(error => {
93
+ this.emit('error', { type: 'sync', error });
94
+ });
95
+ }, this.options.syncInterval);
96
+
97
+ // Do initial sync
98
+ await this.sync();
99
+
100
+ this.emit('started');
101
+ }
102
+
103
+ /**
104
+ * Stop sync engine
105
+ */
106
+ stop() {
107
+ if (this.syncInterval) {
108
+ clearInterval(this.syncInterval);
109
+ this.syncInterval = null;
110
+ }
111
+
112
+ this.emit('stopped');
113
+ }
114
+
115
+ /**
116
+ * Perform sync operation
117
+ */
118
+ async sync() {
119
+ if (this.isSyncing) {
120
+ return; // Already syncing
121
+ }
122
+
123
+ this.isSyncing = true;
124
+ this.emit('sync-start');
125
+
126
+ try {
127
+ // Check if online
128
+ if (!this.isOnline) {
129
+ this.emit('sync-complete', { status: 'offline', queued: this.offlineQueue.length });
130
+ return;
131
+ }
132
+
133
+ // Process offline queue first
134
+ if (this.offlineQueue.length > 0) {
135
+ await this._processOfflineQueue();
136
+ }
137
+
138
+ // Fetch remote changes
139
+ const remoteChanges = await this._fetchRemoteChanges();
140
+
141
+ // Detect local changes
142
+ const localChanges = await this._detectLocalChanges();
143
+
144
+ // Resolve conflicts
145
+ const conflicts = this._detectConflicts(localChanges, remoteChanges);
146
+ if (conflicts.length > 0) {
147
+ await this._resolveConflicts(conflicts);
148
+ }
149
+
150
+ // Apply remote changes locally
151
+ if (remoteChanges.length > 0) {
152
+ await this._applyRemoteChanges(remoteChanges);
153
+ }
154
+
155
+ // Push local changes to remote
156
+ if (localChanges.length > 0) {
157
+ await this._pushLocalChanges(localChanges);
158
+ }
159
+
160
+ this.lastSyncTime = Date.now();
161
+ this.emit('sync-complete', {
162
+ status: 'success',
163
+ remoteChanges: remoteChanges.length,
164
+ localChanges: localChanges.length,
165
+ conflicts: conflicts.length
166
+ });
167
+ } catch (error) {
168
+ this.emit('sync-complete', { status: 'error', error: error.message });
169
+ // Don't throw - just emit error event
170
+ this.emit('error', { type: 'sync', error: error.message });
171
+ } finally {
172
+ this.isSyncing = false;
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Fetch remote changes from DynamoDB
178
+ */
179
+ async _fetchRemoteChanges() {
180
+ const { ScanCommand } = require('@aws-sdk/lib-dynamodb');
181
+
182
+ const tableName = process.env.DYNAMODB_TABLE_NAME || 'vibecodingmachine-requirements';
183
+ const lastSync = this.lastSyncTime || 0;
184
+
185
+ try {
186
+ // Use Scan with filter instead of Query since we need to check all items
187
+ // In production, consider using DynamoDB Streams for real-time updates
188
+ const command = new ScanCommand({
189
+ TableName: tableName,
190
+ FilterExpression: '#ts > :lastSync',
191
+ ExpressionAttributeNames: {
192
+ '#ts': 'timestamp'
193
+ },
194
+ ExpressionAttributeValues: {
195
+ ':lastSync': lastSync
196
+ }
197
+ });
198
+
199
+ const response = await this.dynamoClient.send(command);
200
+ return response.Items || [];
201
+ } catch (error) {
202
+ this.emit('error', { type: 'fetch-remote', error });
203
+ return [];
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Detect local changes
209
+ */
210
+ async _detectLocalChanges() {
211
+ // TODO: Implement file watching and change detection
212
+ // For now, return empty array
213
+ return [];
214
+ }
215
+
216
+ /**
217
+ * Detect conflicts between local and remote changes
218
+ */
219
+ _detectConflicts(localChanges, remoteChanges) {
220
+ const conflicts = [];
221
+
222
+ for (const local of localChanges) {
223
+ for (const remote of remoteChanges) {
224
+ if (local.requirementId === remote.requirementId) {
225
+ // Same requirement modified both locally and remotely
226
+ if (local.timestamp !== remote.timestamp) {
227
+ conflicts.push({ local, remote });
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ return conflicts;
234
+ }
235
+
236
+ /**
237
+ * Resolve conflicts based on strategy
238
+ */
239
+ async _resolveConflicts(conflicts) {
240
+ for (const conflict of conflicts) {
241
+ this.emit('conflict', conflict);
242
+
243
+ let resolution;
244
+ switch (this.options.conflictStrategy) {
245
+ case 'last-write-wins':
246
+ resolution = conflict.local.timestamp > conflict.remote.timestamp
247
+ ? conflict.local
248
+ : conflict.remote;
249
+ break;
250
+
251
+ case 'manual':
252
+ // Emit event for manual resolution
253
+ resolution = await new Promise((resolve) => {
254
+ this.once('conflict-resolved', resolve);
255
+ });
256
+ break;
257
+
258
+ case 'auto-merge':
259
+ resolution = this._autoMerge(conflict.local, conflict.remote);
260
+ break;
261
+
262
+ default:
263
+ resolution = conflict.remote; // Default to remote
264
+ }
265
+
266
+ // Log conflict resolution
267
+ this.changeHistory.push({
268
+ type: 'conflict-resolution',
269
+ timestamp: Date.now(),
270
+ conflict,
271
+ resolution
272
+ });
273
+ }
274
+ }
275
+
276
+ /**
277
+ * Auto-merge non-overlapping changes
278
+ */
279
+ _autoMerge(local, remote) {
280
+ // Simple merge: combine changes from both
281
+ return {
282
+ ...remote,
283
+ ...local,
284
+ timestamp: Math.max(local.timestamp, remote.timestamp)
285
+ };
286
+ }
287
+
288
+ /**
289
+ * Apply remote changes locally
290
+ */
291
+ async _applyRemoteChanges(changes) {
292
+ for (const change of changes) {
293
+ try {
294
+ // TODO: Apply change to local requirements file
295
+ this.emit('remote-change-applied', change);
296
+ } catch (error) {
297
+ this.emit('error', { type: 'apply-remote', change, error });
298
+ }
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Push local changes to remote
304
+ */
305
+ async _pushLocalChanges(changes) {
306
+ const { PutCommand } = require('@aws-sdk/lib-dynamodb');
307
+
308
+ const tableName = process.env.DYNAMODB_TABLE_NAME || 'vibecodingmachine-requirements';
309
+
310
+ for (const change of changes) {
311
+ try {
312
+ const command = new PutCommand({
313
+ TableName: tableName,
314
+ Item: {
315
+ computerId: this.computerId,
316
+ timestamp: Date.now(),
317
+ ...change
318
+ }
319
+ });
320
+
321
+ await this.dynamoClient.send(command);
322
+ this.emit('local-change-pushed', change);
323
+ } catch (error) {
324
+ this.emit('error', { type: 'push-local', change, error });
325
+
326
+ // Add to offline queue if push fails
327
+ if (!this.isOnline) {
328
+ this.offlineQueue.push(change);
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Process offline queue
336
+ */
337
+ async _processOfflineQueue() {
338
+ const queue = [...this.offlineQueue];
339
+ this.offlineQueue = [];
340
+
341
+ for (const change of queue) {
342
+ try {
343
+ await this._pushLocalChanges([change]);
344
+ } catch (error) {
345
+ // Re-add to queue if still failing
346
+ this.offlineQueue.push(change);
347
+ }
348
+ }
349
+ }
350
+
351
+ /**
352
+ * Get sync status
353
+ */
354
+ getStatus() {
355
+ return {
356
+ computerId: this.computerId,
357
+ isOnline: this.isOnline,
358
+ isSyncing: this.isSyncing,
359
+ lastSyncTime: this.lastSyncTime,
360
+ queuedChanges: this.offlineQueue.length,
361
+ conflictStrategy: this.options.conflictStrategy
362
+ };
363
+ }
364
+
365
+ /**
366
+ * Get change history
367
+ */
368
+ getHistory(limit = 100) {
369
+ return this.changeHistory.slice(-limit);
370
+ }
371
+
372
+ /**
373
+ * Set online/offline mode
374
+ */
375
+ setOnlineMode(isOnline) {
376
+ this.isOnline = isOnline;
377
+ this.emit('online-status-changed', { isOnline });
378
+
379
+ if (isOnline && this.offlineQueue.length > 0) {
380
+ // Trigger sync to process offline queue
381
+ this.sync().catch(error => {
382
+ this.emit('error', { type: 'sync', error });
383
+ });
384
+ }
385
+ }
386
+ }
387
+
388
+ module.exports = SyncEngine;
@@ -61,67 +61,87 @@ async function promoteToVerified(reqPath, requirementTitle) {
61
61
  try {
62
62
  const content = await fs.readFile(reqPath, 'utf-8');
63
63
  const lines = content.split('\n');
64
- const updatedLines = [];
65
64
  let inVerifySection = false;
66
- let verifyCount = 0;
67
- let requirementToMove = null;
65
+ let requirementStartIndex = -1;
66
+ let requirementEndIndex = -1;
67
+ const normalizedTitle = requirementTitle.trim();
68
68
 
69
- for (const line of lines) {
70
- if (line.includes('## Verified by AI screenshot')) {
69
+ // Find the requirement in TO VERIFY section (new ### format)
70
+ for (let i = 0; i < lines.length; i++) {
71
+ const line = lines[i];
72
+ const trimmed = line.trim();
73
+
74
+ // Check if we're entering TO VERIFY section (multiple variants)
75
+ if (trimmed.startsWith('##') && !trimmed.startsWith('###') &&
76
+ (trimmed.includes('TO VERIFY') || trimmed.includes('Verified by AI screenshot'))) {
71
77
  inVerifySection = true;
72
- updatedLines.push(line);
73
78
  continue;
74
79
  }
75
80
 
76
- if (inVerifySection && line.startsWith('## ')) {
81
+ // Check if we're leaving TO VERIFY section
82
+ if (inVerifySection && trimmed.startsWith('##') && !trimmed.startsWith('###')) {
77
83
  inVerifySection = false;
78
- updatedLines.push(line);
79
- continue;
80
84
  }
81
85
 
82
- if (inVerifySection && line.startsWith('- ')) {
83
- const reqText = parseRequirementLine(line);
84
- if (reqText === requirementTitle || reqText.includes(requirementTitle)) {
85
- requirementToMove = reqText;
86
- verifyCount++;
87
- continue;
86
+ // Look for requirement in TO VERIFY section (### header format)
87
+ if (inVerifySection && trimmed.startsWith('###')) {
88
+ const title = trimmed.replace(/^###\s*/, '').trim();
89
+ if (title === normalizedTitle || title.includes(normalizedTitle) || normalizedTitle.includes(title)) {
90
+ requirementStartIndex = i;
91
+
92
+ // Find the end of this requirement block
93
+ for (let j = i + 1; j < lines.length; j++) {
94
+ const nextLine = lines[j].trim();
95
+ if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
96
+ requirementEndIndex = j;
97
+ break;
98
+ }
99
+ }
100
+ if (requirementEndIndex === -1) {
101
+ requirementEndIndex = lines.length;
102
+ }
103
+ break;
88
104
  }
89
- verifyCount++;
90
105
  }
106
+ }
91
107
 
92
- updatedLines.push(line);
108
+ if (requirementStartIndex === -1) {
109
+ return false;
93
110
  }
94
111
 
95
- if (requirementToMove) {
96
- // CHANGELOG.md should be at repository root, not in .vibecodingmachine directory
97
- const allnightDir = path.dirname(reqPath); // .vibecodingmachine directory
98
- const repoRoot = path.dirname(allnightDir); // repository root (one level up)
99
- const changelogPath = path.join(repoRoot, 'CHANGELOG.md');
100
- const timestamp = new Date().toISOString().split('T')[0];
101
- const changelogEntry = `- ${requirementToMove} (${timestamp})`;
112
+ // Extract requirement block
113
+ const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
114
+ const extractedTitle = lines[requirementStartIndex].replace(/^###\s*/, '').trim();
102
115
 
103
- let changelogContent = '';
104
- if (await fs.pathExists(changelogPath)) {
105
- changelogContent = await fs.readFile(changelogPath, 'utf-8');
106
- } else {
107
- changelogContent = '# Changelog\n\n## Verified Requirements\n\n';
108
- }
116
+ // Remove requirement from TO VERIFY section
117
+ lines.splice(requirementStartIndex, requirementEndIndex - requirementStartIndex);
109
118
 
110
- if (changelogContent.includes('## Verified Requirements')) {
111
- changelogContent = changelogContent.replace(
112
- '## Verified Requirements\n',
113
- `## Verified Requirements\n${changelogEntry}\n`
114
- );
115
- } else {
116
- changelogContent += `\n## Verified Requirements\n${changelogEntry}\n`;
117
- }
119
+ // Add to CHANGELOG.md
120
+ const allnightDir = path.dirname(reqPath);
121
+ const repoRoot = path.dirname(allnightDir);
122
+ const changelogPath = path.join(repoRoot, 'CHANGELOG.md');
123
+ const timestamp = new Date().toISOString().split('T')[0];
124
+ const changelogEntry = `- ${extractedTitle} (${timestamp})`;
125
+
126
+ let changelogContent = '';
127
+ if (await fs.pathExists(changelogPath)) {
128
+ changelogContent = await fs.readFile(changelogPath, 'utf-8');
129
+ } else {
130
+ changelogContent = '# Changelog\n\n## Verified Requirements\n\n';
131
+ }
118
132
 
119
- await fs.writeFile(changelogPath, changelogContent);
120
- await fs.writeFile(reqPath, updatedLines.join('\n'));
121
- return true;
133
+ if (changelogContent.includes('## Verified Requirements')) {
134
+ changelogContent = changelogContent.replace(
135
+ '## Verified Requirements\n',
136
+ `## Verified Requirements\n${changelogEntry}\n`
137
+ );
138
+ } else {
139
+ changelogContent += `\n## Verified Requirements\n${changelogEntry}\n`;
122
140
  }
123
141
 
124
- return false;
142
+ await fs.writeFile(changelogPath, changelogContent);
143
+ await fs.writeFile(reqPath, lines.join('\n'));
144
+ return true;
125
145
  } catch (error) {
126
146
  throw new Error(`Failed to promote requirement to verified: ${error.message}`);
127
147
  }
@@ -315,16 +335,17 @@ async function promoteTodoToVerify(reqPath, requirementTitle) {
315
335
  * Move requirement from TO VERIFY back to TODO section
316
336
  * @param {string} reqPath - Path to REQUIREMENTS file
317
337
  * @param {string} requirementTitle - Title of requirement to move
338
+ * @param {string} explanation - Optional explanation of what went wrong
318
339
  * @returns {Promise<boolean>} Success status
319
340
  */
320
- async function demoteVerifyToTodo(reqPath, requirementTitle) {
341
+ async function demoteVerifyToTodo(reqPath, requirementTitle, explanation = '') {
321
342
  try {
322
343
  const content = await fs.readFile(reqPath, 'utf-8');
323
344
  const lines = content.split('\n');
324
345
 
325
- // Find the requirement block in TO VERIFY section (### header format)
326
- let requirementStartIndex = -1;
327
- let requirementEndIndex = -1;
346
+ // Find ALL matching requirements in TO VERIFY section and remove them
347
+ // We'll collect all requirement blocks to remove, then process them
348
+ const requirementsToRemove = [];
328
349
  let inVerifySection = false;
329
350
 
330
351
  const verifySectionVariants = [
@@ -332,26 +353,56 @@ async function demoteVerifyToTodo(reqPath, requirementTitle) {
332
353
  '## 🔍 TO VERIFY',
333
354
  '## TO VERIFY',
334
355
  '## ✅ TO VERIFY',
356
+ '## ✅ Verified by AI screenshot. Needs Human to Verify and move to CHANGELOG',
335
357
  '## ✅ Verified by AI screenshot'
336
358
  ];
337
359
 
360
+ // First pass: find all matching requirements in TO VERIFY section
338
361
  for (let i = 0; i < lines.length; i++) {
339
- const line = lines[i].trim();
340
-
341
- if (verifySectionVariants.some(variant => line.includes(variant))) {
342
- inVerifySection = true;
343
- continue;
344
- }
362
+ const line = lines[i];
363
+ const trimmed = line.trim();
345
364
 
346
- if (inVerifySection && line.startsWith('##') && !line.startsWith('###')) {
347
- break;
365
+ // Check if this is a TO VERIFY section header
366
+ if (trimmed.startsWith('##') && !trimmed.startsWith('###')) {
367
+ const isToVerifyHeader = verifySectionVariants.some(variant => {
368
+ return trimmed === variant || trimmed.startsWith(variant) ||
369
+ (trimmed.includes('Verified by AI screenshot') && trimmed.includes('Needs Human to Verify'));
370
+ });
371
+
372
+ if (isToVerifyHeader) {
373
+ // Make sure it's not a VERIFIED section (without TO VERIFY)
374
+ if (!trimmed.includes('## 📝 VERIFIED') && !trimmed.match(/^##\s+VERIFIED$/i) && !trimmed.includes('📝 VERIFIED')) {
375
+ inVerifySection = true;
376
+ continue;
377
+ }
378
+ } else if (inVerifySection) {
379
+ // Check if we're leaving TO VERIFY section (hit a different section)
380
+ if (trimmed.includes('⏳ Requirements not yet completed') ||
381
+ trimmed.includes('## 📝 VERIFIED') ||
382
+ trimmed.includes('## ♻️ RECYCLED') ||
383
+ trimmed.includes('## 📦 RECYCLED') ||
384
+ trimmed.includes('## ❓ Requirements needing')) {
385
+ // We've left the TO VERIFY section
386
+ inVerifySection = false;
387
+ }
388
+ }
348
389
  }
349
390
 
350
- if (inVerifySection && line.startsWith('###')) {
351
- const title = line.replace(/^###\s*/, '').trim();
352
- if (title && (title === requirementTitle || title.includes(requirementTitle) || requirementTitle.includes(title))) {
353
- requirementStartIndex = i;
391
+ // Look for requirement in TO VERIFY section
392
+ if (inVerifySection && trimmed.startsWith('###')) {
393
+ const title = trimmed.replace(/^###\s*/, '').trim();
394
+ // Normalize titles for matching (handle TRY AGAIN prefixes)
395
+ const normalizedTitle = title.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
396
+ const normalizedRequirementTitle = requirementTitle.replace(/^TRY AGAIN \(\d+(st|nd|rd|th) time\):\s*/i, '').trim();
397
+
398
+ if (title && (title === requirementTitle ||
399
+ normalizedTitle === normalizedRequirementTitle ||
400
+ title.includes(requirementTitle) ||
401
+ requirementTitle.includes(title) ||
402
+ normalizedTitle.includes(normalizedRequirementTitle) ||
403
+ normalizedRequirementTitle.includes(normalizedTitle))) {
354
404
  // Find the end of this requirement (next ### or ## header)
405
+ let requirementEndIndex = lines.length;
355
406
  for (let j = i + 1; j < lines.length; j++) {
356
407
  const nextLine = lines[j].trim();
357
408
  if (nextLine.startsWith('###') || (nextLine.startsWith('##') && !nextLine.startsWith('###'))) {
@@ -359,31 +410,49 @@ async function demoteVerifyToTodo(reqPath, requirementTitle) {
359
410
  break;
360
411
  }
361
412
  }
362
- if (requirementEndIndex === -1) {
363
- requirementEndIndex = lines.length;
364
- }
365
- break;
413
+
414
+ // Store this requirement to remove (we'll use the first one for moving to TODO)
415
+ requirementsToRemove.push({
416
+ start: i,
417
+ end: requirementEndIndex,
418
+ block: lines.slice(i, requirementEndIndex)
419
+ });
366
420
  }
367
421
  }
368
422
  }
369
423
 
370
- if (requirementStartIndex === -1) {
424
+ if (requirementsToRemove.length === 0) {
371
425
  return false;
372
426
  }
373
427
 
374
- // Extract the requirement block
375
- const requirementBlock = lines.slice(requirementStartIndex, requirementEndIndex);
428
+ // Use the first matching requirement for moving to TODO (with TRY AGAIN prefix)
429
+ const firstRequirement = requirementsToRemove[0];
430
+ const requirementBlock = [...firstRequirement.block];
376
431
 
377
432
  // Update title with TRY AGAIN prefix
378
433
  const originalTitle = requirementBlock[0].replace(/^###\s*/, '').trim();
379
434
  const titleWithPrefix = addTryAgainPrefix(originalTitle);
380
435
  requirementBlock[0] = `### ${titleWithPrefix}`;
381
436
 
382
- // Remove the requirement from its current location
383
- const updatedLines = [
384
- ...lines.slice(0, requirementStartIndex),
385
- ...lines.slice(requirementEndIndex)
386
- ];
437
+ // Add explanation to the requirement description if provided
438
+ if (explanation && explanation.trim()) {
439
+ // Find where to insert the explanation (after the title, before any existing content)
440
+ // Insert after first line (title) with a blank line and "What went wrong:" section
441
+ const explanationLines = [
442
+ '',
443
+ '**What went wrong (from previous attempt):**',
444
+ explanation.trim(),
445
+ ''
446
+ ];
447
+ requirementBlock.splice(1, 0, ...explanationLines);
448
+ }
449
+
450
+ // Remove ALL matching requirements from TO VERIFY section (work backwards to preserve indices)
451
+ const updatedLines = [...lines];
452
+ for (let i = requirementsToRemove.length - 1; i >= 0; i--) {
453
+ const req = requirementsToRemove[i];
454
+ updatedLines.splice(req.start, req.end - req.start);
455
+ }
387
456
 
388
457
  // Find or create TODO section
389
458
  let todoIndex = -1;