s9n-devops-agent 1.2.1 → 1.3.3

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,350 @@
1
+ /**
2
+ * Display Utilities for CS DevOps Agent
3
+ *
4
+ * Provides consistent, clean formatting for all console output
5
+ * Ensures professional appearance across all modules
6
+ */
7
+
8
+ const COLORS = {
9
+ // Basic colors
10
+ reset: '\x1b[0m',
11
+ bright: '\x1b[1m',
12
+ dim: '\x1b[2m',
13
+
14
+ // Text colors
15
+ black: '\x1b[30m',
16
+ red: '\x1b[31m',
17
+ green: '\x1b[32m',
18
+ yellow: '\x1b[33m',
19
+ blue: '\x1b[34m',
20
+ magenta: '\x1b[35m',
21
+ cyan: '\x1b[36m',
22
+ white: '\x1b[37m',
23
+ orange: '\x1b[38;5;208m',
24
+
25
+ // Background colors
26
+ bgRed: '\x1b[41m',
27
+ bgGreen: '\x1b[42m',
28
+ bgYellow: '\x1b[43m',
29
+ bgBlue: '\x1b[44m',
30
+ bgMagenta: '\x1b[45m',
31
+ bgCyan: '\x1b[46m'
32
+ };
33
+
34
+ const ICONS = {
35
+ success: '✓',
36
+ error: '✗',
37
+ warning: '⚠',
38
+ info: 'ℹ',
39
+ arrow: '→',
40
+ bullet: '•',
41
+ lock: '🔒',
42
+ unlock: '🔓',
43
+ file: '📄',
44
+ folder: '📁',
45
+ clock: '⏱',
46
+ alert: '🚨',
47
+ orange: '🟧',
48
+ red: '🔴',
49
+ green: '🟢',
50
+ save: '💾',
51
+ copy: '📋',
52
+ robot: '🤖'
53
+ };
54
+
55
+ class DisplayUtils {
56
+ constructor() {
57
+ this.colors = COLORS;
58
+ this.icons = ICONS;
59
+ this.terminalWidth = process.stdout.columns || 80;
60
+ }
61
+
62
+ /**
63
+ * Print a clean header box
64
+ */
65
+ header(title, subtitle = '') {
66
+ const width = Math.min(this.terminalWidth, 70);
67
+ const line = '═'.repeat(width);
68
+ const padding = Math.floor((width - title.length) / 2);
69
+
70
+ console.log();
71
+ console.log(this.colors.cyan + line + this.colors.reset);
72
+ console.log(this.colors.bright + ' '.repeat(padding) + title + this.colors.reset);
73
+ if (subtitle) {
74
+ const subPadding = Math.floor((width - subtitle.length) / 2);
75
+ console.log(this.colors.dim + ' '.repeat(subPadding) + subtitle + this.colors.reset);
76
+ }
77
+ console.log(this.colors.cyan + line + this.colors.reset);
78
+ console.log();
79
+ }
80
+
81
+ /**
82
+ * Print a section header
83
+ */
84
+ section(title) {
85
+ const width = Math.min(this.terminalWidth, 70);
86
+ const line = '─'.repeat(width);
87
+
88
+ console.log();
89
+ console.log(this.colors.blue + this.colors.bright + title + this.colors.reset);
90
+ console.log(this.colors.dim + line + this.colors.reset);
91
+ }
92
+
93
+ /**
94
+ * Print a subsection
95
+ */
96
+ subsection(title) {
97
+ console.log();
98
+ console.log(this.colors.cyan + '▸ ' + title + this.colors.reset);
99
+ }
100
+
101
+ /**
102
+ * Success message
103
+ */
104
+ success(message, detail = '') {
105
+ console.log(
106
+ this.colors.green + this.icons.success + this.colors.reset +
107
+ ' ' + message +
108
+ (detail ? this.colors.dim + ' (' + detail + ')' + this.colors.reset : '')
109
+ );
110
+ }
111
+
112
+ /**
113
+ * Error message
114
+ */
115
+ error(message, detail = '') {
116
+ console.log(
117
+ this.colors.red + this.icons.error + this.colors.reset +
118
+ ' ' + message +
119
+ (detail ? this.colors.dim + ' (' + detail + ')' + this.colors.reset : '')
120
+ );
121
+ }
122
+
123
+ /**
124
+ * Warning message
125
+ */
126
+ warning(message, detail = '') {
127
+ console.log(
128
+ this.colors.yellow + this.icons.warning + this.colors.reset +
129
+ ' ' + message +
130
+ (detail ? this.colors.dim + ' (' + detail + ')' + this.colors.reset : '')
131
+ );
132
+ }
133
+
134
+ /**
135
+ * Info message
136
+ */
137
+ info(message, detail = '') {
138
+ console.log(
139
+ this.colors.cyan + this.icons.info + this.colors.reset +
140
+ ' ' + message +
141
+ (detail ? this.colors.dim + ' (' + detail + ')' + this.colors.reset : '')
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Step/progress indicator
147
+ */
148
+ step(number, total, message) {
149
+ console.log(
150
+ this.colors.blue + `[${number}/${total}]` + this.colors.reset +
151
+ ' ' + message
152
+ );
153
+ }
154
+
155
+ /**
156
+ * Print a list item
157
+ */
158
+ listItem(message, indent = 2) {
159
+ console.log(' '.repeat(indent) + this.icons.bullet + ' ' + message);
160
+ }
161
+
162
+ /**
163
+ * Print key-value pair
164
+ */
165
+ keyValue(key, value, indent = 2) {
166
+ console.log(
167
+ ' '.repeat(indent) +
168
+ this.colors.dim + key + ':' + this.colors.reset + ' ' +
169
+ this.colors.bright + value + this.colors.reset
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Print a table
175
+ */
176
+ table(headers, rows) {
177
+ // Calculate column widths
178
+ const widths = headers.map((h, i) => {
179
+ const headerWidth = h.length;
180
+ const maxRowWidth = Math.max(...rows.map(r => String(r[i]).length));
181
+ return Math.max(headerWidth, maxRowWidth) + 2;
182
+ });
183
+
184
+ // Print headers
185
+ const headerRow = headers.map((h, i) =>
186
+ h.padEnd(widths[i])
187
+ ).join('│ ');
188
+
189
+ console.log();
190
+ console.log(this.colors.bright + headerRow + this.colors.reset);
191
+ console.log('─'.repeat(headerRow.length));
192
+
193
+ // Print rows
194
+ rows.forEach(row => {
195
+ const rowStr = row.map((cell, i) =>
196
+ String(cell).padEnd(widths[i])
197
+ ).join('│ ');
198
+ console.log(rowStr);
199
+ });
200
+ console.log();
201
+ }
202
+
203
+ /**
204
+ * Print emoji-based alert boxes (keeping the visual impact)
205
+ */
206
+ alertBox(type, title, message, instructions = null) {
207
+ const width = Math.min(this.terminalWidth, 70);
208
+ let borderChar, icon;
209
+
210
+ switch(type) {
211
+ case 'conflict':
212
+ borderChar = '🔴';
213
+ icon = '🔴';
214
+ break;
215
+ case 'warning':
216
+ borderChar = '🟧';
217
+ icon = '🟧';
218
+ break;
219
+ default:
220
+ borderChar = '⚠️';
221
+ icon = '⚠️';
222
+ }
223
+
224
+ // Emoji border (much more eye-catching!)
225
+ console.log();
226
+ console.log(borderChar.repeat(30));
227
+ console.log(borderChar + ' ' + title);
228
+ console.log(borderChar.repeat(30));
229
+
230
+ // Message
231
+ console.log();
232
+ if (Array.isArray(message)) {
233
+ message.forEach(line => console.log(line));
234
+ } else {
235
+ console.log(message);
236
+ }
237
+
238
+ // Instructions
239
+ if (instructions) {
240
+ console.log();
241
+ console.log('📋 COPY THIS TO YOUR AGENT:');
242
+ console.log('─'.repeat(width));
243
+ console.log(instructions);
244
+ console.log('─'.repeat(width));
245
+ }
246
+
247
+ console.log();
248
+ console.log(borderChar.repeat(30));
249
+ console.log();
250
+ }
251
+
252
+ /**
253
+ * Print a progress bar
254
+ */
255
+ progressBar(current, total, label = '') {
256
+ const width = 30;
257
+ const percentage = Math.round((current / total) * 100);
258
+ const filled = Math.round((current / total) * width);
259
+ const empty = width - filled;
260
+
261
+ const bar =
262
+ this.colors.green + '█'.repeat(filled) +
263
+ this.colors.dim + '░'.repeat(empty) + this.colors.reset;
264
+
265
+ console.log(
266
+ `${label} ${bar} ${percentage}% (${current}/${total})`
267
+ );
268
+ }
269
+
270
+ /**
271
+ * Clear line and rewrite (for updating status)
272
+ */
273
+ updateLine(message) {
274
+ process.stdout.clearLine(0);
275
+ process.stdout.cursorTo(0);
276
+ process.stdout.write(message);
277
+ }
278
+
279
+ /**
280
+ * Print session info in a clean format
281
+ */
282
+ sessionInfo(sessionData) {
283
+ this.section('Session Information');
284
+ this.keyValue('Session ID', sessionData.sessionId);
285
+ this.keyValue('Task', sessionData.task || 'General development');
286
+ this.keyValue('Branch', sessionData.branchName);
287
+ this.keyValue('Status', sessionData.status);
288
+ if (sessionData.worktreePath) {
289
+ this.keyValue('Worktree', sessionData.worktreePath);
290
+ }
291
+ if (sessionData.claimedBy) {
292
+ this.keyValue('Agent', sessionData.claimedBy);
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Print a clean menu
298
+ */
299
+ menu(title, options) {
300
+ this.section(title);
301
+ options.forEach((option, index) => {
302
+ const key = option.key || (index + 1);
303
+ const label = option.label || option;
304
+ const desc = option.description || '';
305
+
306
+ console.log(
307
+ ' ' +
308
+ this.colors.cyan + '[' + key + ']' + this.colors.reset + ' ' +
309
+ this.colors.bright + label + this.colors.reset +
310
+ (desc ? '\n ' + this.colors.dim + desc + this.colors.reset : '')
311
+ );
312
+ });
313
+ console.log();
314
+ }
315
+
316
+ /**
317
+ * Print instructions for agents
318
+ */
319
+ agentInstructions(sessionId, worktreePath, task) {
320
+ const width = Math.min(this.terminalWidth, 70);
321
+ const line = '═'.repeat(width);
322
+
323
+ console.log();
324
+ console.log(this.colors.bgCyan + this.colors.black + ' INSTRUCTIONS FOR AI AGENT ' + this.colors.reset);
325
+ console.log(this.colors.cyan + line + this.colors.reset);
326
+ console.log();
327
+ console.log('I\'m working in a DevOps-managed session with the following setup:');
328
+ console.log(`• Session ID: ${sessionId}`);
329
+ console.log(`• Working Directory: ${worktreePath}`);
330
+ console.log(`• Task: ${task || 'development'}`);
331
+ console.log();
332
+ console.log('Please switch to this directory before making any changes:');
333
+ console.log(this.colors.yellow + `cd "${worktreePath}"` + this.colors.reset);
334
+ console.log();
335
+ console.log(this.colors.bright + 'IMPORTANT: File Coordination Protocol' + this.colors.reset);
336
+ console.log('Before editing ANY files, you MUST:');
337
+ console.log('1. Declare your intent by creating .file-coordination/active-edits/<agent>-' + sessionId + '.json');
338
+ console.log('2. List all files you plan to edit in that JSON file');
339
+ console.log('3. Check for conflicts with other agents\' declarations');
340
+ console.log('4. Only proceed if no conflicts exist');
341
+ console.log('5. Release the files when done');
342
+ console.log();
343
+ console.log('Write commit messages to: ' + this.colors.yellow + `.devops-commit-${sessionId}.msg` + this.colors.reset);
344
+ console.log('The DevOps agent will automatically commit and push changes.');
345
+ console.log();
346
+ console.log(this.colors.cyan + line + this.colors.reset);
347
+ }
348
+ }
349
+
350
+ module.exports = new DisplayUtils();
@@ -0,0 +1,356 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * File Coordination System for Multi-Agent Development
5
+ *
6
+ * This module provides conflict detection and reporting for multiple agents
7
+ * editing files in the same repository. It implements an advisory lock system
8
+ * where agents declare their intent to edit files, and conflicts are reported
9
+ * to users for resolution.
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const { execSync } = require('child_process');
15
+
16
+ class FileCoordinator {
17
+ constructor(sessionId, workingDir = process.cwd()) {
18
+ this.sessionId = sessionId;
19
+ this.workingDir = workingDir;
20
+ this.coordDir = path.join(workingDir, '.file-coordination');
21
+ this.activeEditsDir = path.join(this.coordDir, 'active-edits');
22
+ this.completedEditsDir = path.join(this.coordDir, 'completed-edits');
23
+ this.conflictsDir = path.join(this.coordDir, 'conflicts');
24
+
25
+ // Ensure directories exist
26
+ this.ensureDirectories();
27
+ }
28
+
29
+ ensureDirectories() {
30
+ [this.coordDir, this.activeEditsDir, this.completedEditsDir, this.conflictsDir].forEach(dir => {
31
+ if (!fs.existsSync(dir)) {
32
+ fs.mkdirSync(dir, { recursive: true });
33
+ }
34
+ });
35
+ }
36
+
37
+ /**
38
+ * Get all active declarations from other agents
39
+ */
40
+ getActiveDeclarations() {
41
+ const declarations = {};
42
+
43
+ if (!fs.existsSync(this.activeEditsDir)) {
44
+ return declarations;
45
+ }
46
+
47
+ const files = fs.readdirSync(this.activeEditsDir);
48
+
49
+ for (const file of files) {
50
+ if (file.endsWith('.json')) {
51
+ try {
52
+ const content = fs.readFileSync(path.join(this.activeEditsDir, file), 'utf8');
53
+ const declaration = JSON.parse(content);
54
+
55
+ // Check if declaration is still valid (not expired)
56
+ const declaredAt = new Date(declaration.declaredAt);
57
+ const estimatedDuration = declaration.estimatedDuration || 300; // 5 minutes default
58
+ const expiresAt = new Date(declaredAt.getTime() + estimatedDuration * 1000);
59
+
60
+ if (new Date() < expiresAt) {
61
+ declarations[file] = declaration;
62
+ } else {
63
+ // Move expired declaration to completed
64
+ this.moveToCompleted(file);
65
+ }
66
+ } catch (err) {
67
+ console.error(`Error reading declaration ${file}:`, err.message);
68
+ }
69
+ }
70
+ }
71
+
72
+ return declarations;
73
+ }
74
+
75
+ /**
76
+ * Check if specific files are currently being edited by other agents
77
+ */
78
+ checkFilesForConflicts(filesToCheck) {
79
+ const conflicts = [];
80
+ const declarations = this.getActiveDeclarations();
81
+
82
+ for (const [declFile, declaration] of Object.entries(declarations)) {
83
+ // Skip our own declarations
84
+ if (declaration.session === this.sessionId) {
85
+ continue;
86
+ }
87
+
88
+ // Check for file overlaps
89
+ const declaredFiles = declaration.files || [];
90
+ for (const file of filesToCheck) {
91
+ if (declaredFiles.includes(file)) {
92
+ conflicts.push({
93
+ file,
94
+ conflictsWith: declaration.agent,
95
+ session: declaration.session,
96
+ reason: declaration.reason || 'No reason provided',
97
+ declaredAt: declaration.declaredAt
98
+ });
99
+ }
100
+ }
101
+ }
102
+
103
+ return conflicts;
104
+ }
105
+
106
+ /**
107
+ * Detect conflicts between actual changes and declared edits
108
+ */
109
+ async detectUndeclaredEdits() {
110
+ try {
111
+ // Get list of modified files from git
112
+ const modifiedFiles = execSync('git diff --name-only', {
113
+ cwd: this.workingDir,
114
+ encoding: 'utf8'
115
+ }).trim().split('\n').filter(f => f);
116
+
117
+ const stagedFiles = execSync('git diff --cached --name-only', {
118
+ cwd: this.workingDir,
119
+ encoding: 'utf8'
120
+ }).trim().split('\n').filter(f => f);
121
+
122
+ const allChangedFiles = [...new Set([...modifiedFiles, ...stagedFiles])];
123
+
124
+ if (allChangedFiles.length === 0) {
125
+ return { hasConflicts: false };
126
+ }
127
+
128
+ // Check if these files are declared by someone
129
+ const conflicts = this.checkFilesForConflicts(allChangedFiles);
130
+
131
+ // Check if we have our own declaration
132
+ const ourDeclarationFile = this.findOurDeclaration();
133
+ let ourDeclaredFiles = [];
134
+
135
+ if (ourDeclarationFile) {
136
+ try {
137
+ const content = fs.readFileSync(ourDeclarationFile, 'utf8');
138
+ const declaration = JSON.parse(content);
139
+ ourDeclaredFiles = declaration.files || [];
140
+ } catch (err) {
141
+ console.error('Error reading our declaration:', err.message);
142
+ }
143
+ }
144
+
145
+ // Find undeclared edits (files we changed but didn't declare)
146
+ const undeclaredEdits = allChangedFiles.filter(
147
+ file => !ourDeclaredFiles.includes(file)
148
+ );
149
+
150
+ return {
151
+ hasConflicts: conflicts.length > 0 || undeclaredEdits.length > 0,
152
+ conflicts,
153
+ undeclaredEdits,
154
+ allChangedFiles
155
+ };
156
+
157
+ } catch (err) {
158
+ console.error('Error detecting undeclared edits:', err.message);
159
+ return { hasConflicts: false, error: err.message };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Find our current declaration file
165
+ */
166
+ findOurDeclaration() {
167
+ if (!fs.existsSync(this.activeEditsDir)) {
168
+ return null;
169
+ }
170
+
171
+ const files = fs.readdirSync(this.activeEditsDir);
172
+
173
+ for (const file of files) {
174
+ if (file.includes(this.sessionId) && file.endsWith('.json')) {
175
+ return path.join(this.activeEditsDir, file);
176
+ }
177
+ }
178
+
179
+ // Try to find by session ID in content
180
+ for (const file of files) {
181
+ if (file.endsWith('.json')) {
182
+ try {
183
+ const content = fs.readFileSync(path.join(this.activeEditsDir, file), 'utf8');
184
+ const declaration = JSON.parse(content);
185
+ if (declaration.session === this.sessionId) {
186
+ return path.join(this.activeEditsDir, file);
187
+ }
188
+ } catch (err) {
189
+ // Skip invalid files
190
+ }
191
+ }
192
+ }
193
+
194
+ return null;
195
+ }
196
+
197
+ /**
198
+ * Create a conflict report for the user
199
+ */
200
+ createConflictReport(conflictData) {
201
+ const timestamp = new Date().toISOString();
202
+ const reportFile = path.join(this.conflictsDir, `conflict-${this.sessionId}-${Date.now()}.md`);
203
+
204
+ let report = `# ⚠️ FILE COORDINATION CONFLICT DETECTED\n\n`;
205
+ report += `**Time:** ${timestamp}\n`;
206
+ report += `**Session:** ${this.sessionId}\n\n`;
207
+
208
+ if (conflictData.conflicts && conflictData.conflicts.length > 0) {
209
+ report += `## 🔒 Files Being Edited by Other Agents\n\n`;
210
+ report += `The following files are currently being edited by other agents:\n\n`;
211
+
212
+ for (const conflict of conflictData.conflicts) {
213
+ report += `### ${conflict.file}\n`;
214
+ report += `- **Blocked by:** ${conflict.conflictsWith} (session: ${conflict.session})\n`;
215
+ report += `- **Reason:** ${conflict.reason}\n`;
216
+ report += `- **Since:** ${conflict.declaredAt}\n\n`;
217
+ }
218
+
219
+ report += `### ❌ ACTION REQUIRED\n\n`;
220
+ report += `You have attempted to edit files that are currently locked by other agents.\n\n`;
221
+ report += `**Options:**\n`;
222
+ report += `1. **Wait** for the other agent to complete their edits\n`;
223
+ report += `2. **Coordinate** with ${conflictData.conflicts[0].conflictsWith} to resolve the conflict\n`;
224
+ report += `3. **Choose different files** to edit\n`;
225
+ report += `4. **Force override** (not recommended - will cause merge conflicts)\n\n`;
226
+ }
227
+
228
+ if (conflictData.undeclaredEdits && conflictData.undeclaredEdits.length > 0) {
229
+ report += `## 📝 Undeclared File Edits\n\n`;
230
+ report += `The following files were edited without declaration:\n\n`;
231
+
232
+ for (const file of conflictData.undeclaredEdits) {
233
+ report += `- ${file}\n`;
234
+ }
235
+
236
+ report += `\n### ⚠️ ADVISORY WARNING\n\n`;
237
+ report += `These files were modified without following the coordination protocol.\n\n`;
238
+ report += `**To fix this:**\n`;
239
+ report += `1. Run: \`./scripts/coordination/declare-file-edits.sh ${this.sessionId.split('-')[0]} ${this.sessionId} ${conflictData.undeclaredEdits.join(' ')}\`\n`;
240
+ report += `2. Or revert these changes if they were unintentional\n\n`;
241
+ }
242
+
243
+ report += `## 📋 How to Resolve\n\n`;
244
+ report += `1. **Check current declarations:**\n`;
245
+ report += ` \`\`\`bash\n`;
246
+ report += ` ls -la .file-coordination/active-edits/\n`;
247
+ report += ` \`\`\`\n\n`;
248
+ report += `2. **Declare your intended edits:**\n`;
249
+ report += ` \`\`\`bash\n`;
250
+ report += ` ./scripts/coordination/declare-file-edits.sh <agent-name> ${this.sessionId} <files...>\n`;
251
+ report += ` \`\`\`\n\n`;
252
+ report += `3. **Release files when done:**\n`;
253
+ report += ` \`\`\`bash\n`;
254
+ report += ` ./scripts/coordination/release-file-edits.sh <agent-name> ${this.sessionId}\n`;
255
+ report += ` \`\`\`\n\n`;
256
+
257
+ report += `---\n`;
258
+ report += `*This report was generated automatically by the File Coordination System*\n`;
259
+
260
+ // Write report
261
+ fs.writeFileSync(reportFile, report);
262
+
263
+ // Also write a simplified alert to stdout
264
+ console.log('\n' + '='.repeat(60));
265
+ console.log('⚠️ FILE COORDINATION CONFLICT DETECTED');
266
+ console.log('='.repeat(60));
267
+
268
+ if (conflictData.conflicts && conflictData.conflicts.length > 0) {
269
+ console.log('\n❌ BLOCKED FILES:');
270
+ for (const conflict of conflictData.conflicts) {
271
+ console.log(` ${conflict.file} (locked by ${conflict.conflictsWith})`);
272
+ }
273
+ }
274
+
275
+ if (conflictData.undeclaredEdits && conflictData.undeclaredEdits.length > 0) {
276
+ console.log('\n📝 UNDECLARED EDITS:');
277
+ for (const file of conflictData.undeclaredEdits) {
278
+ console.log(` ${file}`);
279
+ }
280
+ }
281
+
282
+ console.log(`\n📄 Full report: ${reportFile}`);
283
+ console.log('='.repeat(60) + '\n');
284
+
285
+ return reportFile;
286
+ }
287
+
288
+ /**
289
+ * Move a declaration to completed
290
+ */
291
+ moveToCompleted(filename) {
292
+ const sourcePath = path.join(this.activeEditsDir, filename);
293
+ const destPath = path.join(this.completedEditsDir, filename);
294
+
295
+ try {
296
+ if (fs.existsSync(sourcePath)) {
297
+ fs.renameSync(sourcePath, destPath);
298
+ }
299
+ } catch (err) {
300
+ console.error(`Error moving ${filename} to completed:`, err.message);
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Clean up old/stale declarations
306
+ */
307
+ cleanupStaleDeclarations(maxAgeMinutes = 60) {
308
+ const now = new Date();
309
+ const maxAge = maxAgeMinutes * 60 * 1000;
310
+
311
+ if (!fs.existsSync(this.activeEditsDir)) {
312
+ return;
313
+ }
314
+
315
+ const files = fs.readdirSync(this.activeEditsDir);
316
+ let cleaned = 0;
317
+
318
+ for (const file of files) {
319
+ if (file.endsWith('.json')) {
320
+ const filePath = path.join(this.activeEditsDir, file);
321
+ const stats = fs.statSync(filePath);
322
+ const age = now - stats.mtime;
323
+
324
+ if (age > maxAge) {
325
+ this.moveToCompleted(file);
326
+ cleaned++;
327
+ }
328
+ }
329
+ }
330
+
331
+ if (cleaned > 0) {
332
+ console.log(`Cleaned up ${cleaned} stale declaration(s)`);
333
+ }
334
+ }
335
+ }
336
+
337
+ // Export for use in other modules
338
+ module.exports = FileCoordinator;
339
+
340
+ // If run directly, perform a conflict check
341
+ if (require.main === module) {
342
+ const sessionId = process.env.DEVOPS_SESSION_ID || 'manual-check';
343
+ const coordinator = new FileCoordinator(sessionId);
344
+
345
+ console.log('Checking for file coordination conflicts...');
346
+
347
+ coordinator.detectUndeclaredEdits().then(result => {
348
+ if (result.hasConflicts) {
349
+ coordinator.createConflictReport(result);
350
+ process.exit(1);
351
+ } else {
352
+ console.log('✅ No conflicts detected');
353
+ process.exit(0);
354
+ }
355
+ });
356
+ }