s9n-devops-agent 1.2.0 → 1.3.1
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/README.md +210 -56
- package/docs/INSTALLATION_GUIDE.md +366 -0
- package/docs/houserules.md +51 -0
- package/package.json +5 -1
- package/src/cs-devops-agent-worker.js +65 -10
- package/src/display-utils.cjs +350 -0
- package/src/file-coordinator.cjs +356 -0
- package/src/file-monitor-enhanced.cjs +338 -0
- package/src/house-rules-manager.js +443 -0
- package/src/session-coordinator.js +289 -15
- package/src/setup-cs-devops-agent.js +5 -3
- package/start-devops-session.sh +139 -0
|
@@ -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
|
+
}
|