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.
- package/package.json +4 -1
- package/src/auth/shared-auth-storage.js +43 -6
- package/src/ide-integration/applescript-manager.cjs +126 -18
- package/src/ide-integration/applescript-manager.js +172 -84
- package/src/index.cjs +2 -0
- package/src/index.js +2 -0
- package/src/sync/aws-setup.js +445 -0
- package/src/sync/sync-engine.js +388 -0
- package/src/utils/requirement-helpers.js +139 -70
- package/src/utils/requirements-parser.js +310 -0
|
@@ -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
|
|
67
|
-
let
|
|
65
|
+
let requirementStartIndex = -1;
|
|
66
|
+
let requirementEndIndex = -1;
|
|
67
|
+
const normalizedTitle = requirementTitle.trim();
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
108
|
+
if (requirementStartIndex === -1) {
|
|
109
|
+
return false;
|
|
93
110
|
}
|
|
94
111
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
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
|
|
326
|
-
|
|
327
|
-
|
|
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]
|
|
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
|
|
347
|
-
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
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
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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 (
|
|
424
|
+
if (requirementsToRemove.length === 0) {
|
|
371
425
|
return false;
|
|
372
426
|
}
|
|
373
427
|
|
|
374
|
-
//
|
|
375
|
-
const
|
|
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
|
-
//
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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;
|