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,610 @@
1
+ /**
2
+ * House Rules Version Manager
3
+ * ============================
4
+ *
5
+ * Manages versioning and updates of DevOps Agent sections in house rules.
6
+ * Allows intelligent updates while preserving user customizations.
7
+ *
8
+ * Each managed section includes:
9
+ * - Version marker: <!-- DEVOPS_AGENT_SECTION:name:version:checksum -->
10
+ * - Content checksum for change detection
11
+ * - End marker: <!-- END_DEVOPS_AGENT_SECTION:name -->
12
+ */
13
+
14
+ import fs from 'fs';
15
+ import path from 'path';
16
+ import crypto from 'crypto';
17
+ import { fileURLToPath } from 'url';
18
+ import { dirname } from 'path';
19
+ import { execSync } from 'child_process';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = dirname(__filename);
23
+
24
+ // Version of our house rules sections
25
+ const HOUSERULES_VERSION = '1.3.0';
26
+
27
+ // Managed sections with their content
28
+ const MANAGED_SECTIONS = {
29
+ 'file-coordination': {
30
+ version: '1.3.0',
31
+ title: '## 🚨 CRITICAL: File Coordination Protocol (MUST FOLLOW)',
32
+ content: `**IMPORTANT: Always check the house rules at the beginning of each session!**
33
+
34
+ To prevent conflicts with other agents editing the same files, you MUST follow this protocol:
35
+
36
+ ### Before Editing ANY Files:
37
+
38
+ 1. **DECLARE YOUR INTENT FIRST**
39
+ Create a file at \`.file-coordination/active-edits/<your-name>-<session>.json\` with:
40
+ \`\`\`json
41
+ {
42
+ "agent": "<your-name>",
43
+ "session": "<session-id>",
44
+ "files": ["list", "of", "files", "you", "will", "edit"],
45
+ "operation": "edit",
46
+ "reason": "Brief description of what you're doing",
47
+ "declaredAt": "<current-ISO-8601-timestamp>",
48
+ "estimatedDuration": 300
49
+ }
50
+ \`\`\`
51
+
52
+ 2. **CHECK FOR CONFLICTS**
53
+ - Read ALL files in \`.file-coordination/active-edits/\`
54
+ - If ANY other agent has declared the same files, you must:
55
+ - WAIT for them to finish, OR
56
+ - Choose different files to edit
57
+
58
+ 3. **ONLY EDIT DECLARED FILES**
59
+ - Never edit files you haven't declared
60
+ - Stay within your declared scope
61
+
62
+ 4. **RELEASE WHEN DONE**
63
+ - Delete your declaration file after completing edits
64
+ - Or move it to \`.file-coordination/completed-edits/\`
65
+
66
+ ### If You Detect a Conflict:
67
+ - DO NOT proceed with edits
68
+ - Report the conflict to the user
69
+ - Wait or choose alternative files
70
+
71
+ ### Helper Scripts Available:
72
+ - \`./scripts/coordination/check-file-availability.sh <files>\` - Check if files are available
73
+ - \`./scripts/coordination/declare-file-edits.sh <agent> <session> <files>\` - Declare your intent
74
+ - \`./scripts/coordination/release-file-edits.sh <agent> <session>\` - Release files after editing
75
+
76
+ **This coordination prevents wasted work and merge conflicts!**`
77
+ },
78
+ 'core-principles': {
79
+ version: '1.0.0',
80
+ title: '## Core Principles',
81
+ content: `1. **Always preserve existing functionality** - Never break working code
82
+ 2. **Follow existing patterns** - Match the codebase style and conventions
83
+ 3. **Communicate clearly** - Document your changes and reasoning
84
+ 4. **Coordinate with others** - Follow the file coordination protocol below`
85
+ },
86
+ 'project-conventions': {
87
+ version: '1.0.0',
88
+ title: '## Project Conventions',
89
+ content: `### Code Style
90
+ - Follow existing indentation and formatting patterns
91
+ - Maintain consistent naming conventions used in the project
92
+ - Keep functions small and focused
93
+ - Write clear, descriptive variable and function names
94
+
95
+ ### Git Workflow
96
+ - Write clear, descriptive commit messages
97
+ - Follow conventional commit format when applicable (feat:, fix:, docs:, etc.)
98
+ - Keep commits atomic and focused on a single change
99
+ - Never commit sensitive information or credentials
100
+
101
+ ### Testing
102
+ - Write tests for new functionality
103
+ - Ensure existing tests pass before committing
104
+ - Update tests when changing functionality
105
+
106
+ ### Documentation
107
+ - Update README files when adding new features
108
+ - Document complex logic with clear comments
109
+ - Keep API documentation up to date
110
+ - Update CHANGELOG for significant changes`
111
+ }
112
+ };
113
+
114
+ class HouseRulesManager {
115
+ constructor(projectRoot) {
116
+ const cwd = projectRoot || process.cwd();
117
+
118
+ // If we're running from within a DevOpsAgent directory, we need to find the parent project
119
+ if (cwd.includes('/DevOpsAgent') || cwd.includes('/CS_DevOpsAgent')) {
120
+ // Parse the path to find where the DevOpsAgent directory is
121
+ const pathParts = cwd.split(path.sep);
122
+
123
+ // Find the index of DevOpsAgent or CS_DevOpsAgent
124
+ let targetIndex = -1;
125
+ for (let i = pathParts.length - 1; i >= 0; i--) {
126
+ if (pathParts[i] === 'CS_DevOpsAgent' || pathParts[i] === 'DevOpsAgent') {
127
+ targetIndex = i;
128
+ break;
129
+ }
130
+ }
131
+
132
+ // If found, use the parent directory as the project root
133
+ if (targetIndex > 0) {
134
+ // Check if the parent directory is 'Scripts_Dev' or similar subdirectory
135
+ // If so, go up one more level to reach the actual project root
136
+ let parentIndex = targetIndex;
137
+ if (targetIndex > 1 && (pathParts[targetIndex - 1] === 'Scripts_Dev' ||
138
+ pathParts[targetIndex - 1] === 'scripts' ||
139
+ pathParts[targetIndex - 1] === 'tools')) {
140
+ parentIndex = targetIndex - 1;
141
+ }
142
+ this.projectRoot = pathParts.slice(0, parentIndex).join(path.sep);
143
+ } else {
144
+ // Fallback: try to use git to find the parent repo
145
+ try {
146
+ const gitRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
147
+ // Only use git root if it's not a DevOpsAgent directory
148
+ if (!gitRoot.includes('/DevOpsAgent') && !gitRoot.includes('/CS_DevOpsAgent')) {
149
+ this.projectRoot = gitRoot;
150
+ } else {
151
+ // Use parent of cwd
152
+ this.projectRoot = path.dirname(path.dirname(cwd));
153
+ }
154
+ } catch (err) {
155
+ // Default to parent of parent directory
156
+ this.projectRoot = path.dirname(path.dirname(cwd));
157
+ }
158
+ }
159
+ } else {
160
+ this.projectRoot = cwd;
161
+ }
162
+
163
+ this.houseRulesPath = null;
164
+ this.findHouseRulesFile();
165
+ }
166
+
167
+ /**
168
+ * Find existing house rules file
169
+ * Searches across the repository, excluding the DevOps agent directories
170
+ */
171
+ findHouseRulesFile() {
172
+ // First try the standard locations
173
+ const possiblePaths = [
174
+ 'houserules.md',
175
+ 'HOUSERULES.md',
176
+ '.github/HOUSERULES.md',
177
+ 'docs/houserules.md',
178
+ 'docs/HOUSERULES.md'
179
+ ];
180
+
181
+ for (const relativePath of possiblePaths) {
182
+ const fullPath = path.join(this.projectRoot, relativePath);
183
+ if (fs.existsSync(fullPath)) {
184
+ // NEVER use house rules from within DevOpsAgent directories
185
+ if (fullPath.includes('/DevOpsAgent/') || fullPath.includes('/CS_DevOpsAgent/')) {
186
+ continue;
187
+ }
188
+
189
+ this.houseRulesPath = fullPath;
190
+ // Only log if not running as CLI
191
+ if (!process.argv[1]?.endsWith('house-rules-manager.js')) {
192
+ console.log(`Found house rules at: ${fullPath}`);
193
+ }
194
+ return fullPath;
195
+ }
196
+ }
197
+
198
+ // If not found in standard locations, search the repository
199
+ // excluding DevOps agent directories
200
+ const foundPath = this.searchForHouseRules(this.projectRoot);
201
+ if (foundPath) {
202
+ this.houseRulesPath = foundPath;
203
+ // Only log if not running as CLI
204
+ if (!process.argv[1]?.endsWith('house-rules-manager.js')) {
205
+ console.log(`Found house rules at: ${foundPath}`);
206
+ }
207
+ return foundPath;
208
+ }
209
+
210
+ return null;
211
+ }
212
+
213
+ /**
214
+ * Recursively search for house rules files
215
+ * @param {string} dir Directory to search
216
+ * @param {number} depth Current depth (to prevent infinite recursion)
217
+ * @returns {string|null} Path to house rules file or null
218
+ */
219
+ searchForHouseRules(dir, depth = 0) {
220
+ // Limit search depth to prevent excessive recursion
221
+ if (depth > 5) return null;
222
+
223
+ try {
224
+ const files = fs.readdirSync(dir);
225
+
226
+ for (const file of files) {
227
+ const filePath = path.join(dir, file);
228
+ const stat = fs.statSync(filePath);
229
+
230
+ // Check if it's a house rules file
231
+ if (stat.isFile() && /^houserules\.md$/i.test(file)) {
232
+ // Skip if it's in a DevOps agent directory
233
+ if (!filePath.includes('DevOpsAgent') &&
234
+ !filePath.includes('CS_DevOpsAgent') &&
235
+ !filePath.includes('node_modules') &&
236
+ !filePath.includes('.git')) {
237
+ return filePath;
238
+ }
239
+ }
240
+
241
+ // Recursively search subdirectories
242
+ if (stat.isDirectory() &&
243
+ !file.startsWith('.') &&
244
+ file !== 'node_modules' &&
245
+ file !== 'DevOpsAgent' &&
246
+ file !== 'CS_DevOpsAgent' &&
247
+ file !== '.git') {
248
+ const found = this.searchForHouseRules(filePath, depth + 1);
249
+ if (found) return found;
250
+ }
251
+ }
252
+ } catch (err) {
253
+ // Ignore permission errors and continue searching
254
+ if (err.code !== 'EACCES' && err.code !== 'EPERM') {
255
+ console.error(`Error searching ${dir}:`, err.message);
256
+ }
257
+ }
258
+
259
+ return null;
260
+ }
261
+
262
+ /**
263
+ * Calculate checksum for content
264
+ */
265
+ calculateChecksum(content) {
266
+ return crypto.createHash('md5').update(content.trim()).digest('hex').substring(0, 8);
267
+ }
268
+
269
+ /**
270
+ * Create section marker
271
+ */
272
+ createSectionMarker(sectionName, version, checksum) {
273
+ return `<!-- DEVOPS_AGENT_SECTION:${sectionName}:${version}:${checksum} -->`;
274
+ }
275
+
276
+ /**
277
+ * Create end marker
278
+ */
279
+ createEndMarker(sectionName) {
280
+ return `<!-- END_DEVOPS_AGENT_SECTION:${sectionName} -->`;
281
+ }
282
+
283
+ /**
284
+ * Extract managed sections from existing house rules
285
+ */
286
+ extractManagedSections(content) {
287
+ const sections = {};
288
+ const pattern = /<!-- DEVOPS_AGENT_SECTION:(\w+[-\w]*):([0-9.]+):([a-f0-9]+) -->([\s\S]*?)<!-- END_DEVOPS_AGENT_SECTION:\1 -->/g;
289
+
290
+ let match;
291
+ while ((match = pattern.exec(content)) !== null) {
292
+ sections[match[1]] = {
293
+ name: match[1],
294
+ version: match[2],
295
+ checksum: match[3],
296
+ content: match[4].trim(),
297
+ startIndex: match.index,
298
+ endIndex: match.index + match[0].length
299
+ };
300
+ }
301
+
302
+ return sections;
303
+ }
304
+
305
+ /**
306
+ * Check if a section needs updating
307
+ */
308
+ needsUpdate(sectionName, existingVersion, existingChecksum) {
309
+ const currentSection = MANAGED_SECTIONS[sectionName];
310
+ if (!currentSection) return false;
311
+
312
+ // Check version
313
+ if (this.compareVersions(currentSection.version, existingVersion) > 0) {
314
+ return true;
315
+ }
316
+
317
+ // Check checksum (in case we updated content without version bump)
318
+ const currentChecksum = this.calculateChecksum(currentSection.content);
319
+ return currentChecksum !== existingChecksum;
320
+ }
321
+
322
+ /**
323
+ * Compare semantic versions
324
+ */
325
+ compareVersions(v1, v2) {
326
+ const parts1 = v1.split('.').map(Number);
327
+ const parts2 = v2.split('.').map(Number);
328
+
329
+ for (let i = 0; i < 3; i++) {
330
+ if (parts1[i] > parts2[i]) return 1;
331
+ if (parts1[i] < parts2[i]) return -1;
332
+ }
333
+
334
+ return 0;
335
+ }
336
+
337
+ /**
338
+ * Format a managed section with markers
339
+ */
340
+ formatSection(sectionName) {
341
+ const section = MANAGED_SECTIONS[sectionName];
342
+ if (!section) return '';
343
+
344
+ const checksum = this.calculateChecksum(section.content);
345
+ const marker = this.createSectionMarker(sectionName, section.version, checksum);
346
+ const endMarker = this.createEndMarker(sectionName);
347
+
348
+ return `${marker}
349
+ ${section.title}
350
+
351
+ ${section.content}
352
+ ${endMarker}`;
353
+ }
354
+
355
+ /**
356
+ * Create new house rules with all managed sections
357
+ */
358
+ createNewHouseRules() {
359
+ const sections = [];
360
+
361
+ // Header
362
+ sections.push(`# House Rules for AI Agents
363
+
364
+ **IMPORTANT: All AI agents (Claude, Cline, Copilot, etc.) must read and follow these rules at the start of each session.**
365
+ `);
366
+
367
+ // Add managed sections
368
+ sections.push(this.formatSection('file-coordination'));
369
+ sections.push('');
370
+ sections.push(this.formatSection('core-principles'));
371
+ sections.push('');
372
+ sections.push(this.formatSection('project-conventions'));
373
+
374
+ return sections.join('\n');
375
+ }
376
+
377
+ /**
378
+ * Update house rules intelligently
379
+ */
380
+ async updateHouseRules(options = {}) {
381
+ const { createIfMissing = true, backupExisting = true } = options;
382
+
383
+ // Find or create house rules file
384
+ if (!this.houseRulesPath) {
385
+ if (!createIfMissing) {
386
+ return { updated: false, reason: 'No house rules file found' };
387
+ }
388
+
389
+ this.houseRulesPath = path.join(this.projectRoot, 'houserules.md');
390
+ const content = this.createNewHouseRules();
391
+ fs.writeFileSync(this.houseRulesPath, content);
392
+
393
+ return {
394
+ updated: true,
395
+ created: true,
396
+ path: this.houseRulesPath,
397
+ sections: Object.keys(MANAGED_SECTIONS)
398
+ };
399
+ }
400
+
401
+ // Read existing content
402
+ const existingContent = fs.readFileSync(this.houseRulesPath, 'utf8');
403
+ const existingSections = this.extractManagedSections(existingContent);
404
+
405
+ // Check what needs updating
406
+ const updates = [];
407
+ const additions = [];
408
+
409
+ for (const [sectionName, sectionData] of Object.entries(MANAGED_SECTIONS)) {
410
+ if (existingSections[sectionName]) {
411
+ // Section exists - check if needs update
412
+ if (this.needsUpdate(sectionName, existingSections[sectionName].version, existingSections[sectionName].checksum)) {
413
+ updates.push(sectionName);
414
+ }
415
+ } else {
416
+ // Section doesn't exist - needs to be added
417
+ additions.push(sectionName);
418
+ }
419
+ }
420
+
421
+ // If no updates needed, return
422
+ if (updates.length === 0 && additions.length === 0) {
423
+ return { updated: false, reason: 'All sections are up to date' };
424
+ }
425
+
426
+ // Create backup if requested
427
+ if (backupExisting) {
428
+ // Create backup in DevopsAgent_Backups folder
429
+ const backupDir = path.join(this.projectRoot, 'DevopsAgent_Backups');
430
+ if (!fs.existsSync(backupDir)) {
431
+ fs.mkdirSync(backupDir, { recursive: true });
432
+ // Ensure backup folder is in gitignore
433
+ this.ensureBackupInGitignore();
434
+ }
435
+
436
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
437
+ const backupFileName = `houserules_backup_${timestamp}.md`;
438
+ const backupPath = path.join(backupDir, backupFileName);
439
+
440
+ fs.copyFileSync(this.houseRulesPath, backupPath);
441
+
442
+ // Only log if not running as CLI
443
+ if (!process.argv[1]?.endsWith('house-rules-manager.js')) {
444
+ console.log(`Backup created: ${backupPath}`);
445
+ }
446
+ }
447
+
448
+ // Build new content
449
+ let newContent = existingContent;
450
+
451
+ // Update existing sections
452
+ for (const sectionName of updates) {
453
+ const existingSection = existingSections[sectionName];
454
+ const newSection = this.formatSection(sectionName);
455
+
456
+ // Replace the old section with the new one
457
+ const beforeSection = newContent.substring(0, existingSection.startIndex);
458
+ const afterSection = newContent.substring(existingSection.endIndex);
459
+ newContent = beforeSection + newSection + afterSection;
460
+
461
+ // Recalculate positions for remaining sections
462
+ const diff = newSection.length - (existingSection.endIndex - existingSection.startIndex);
463
+ for (const section of Object.values(existingSections)) {
464
+ if (section.startIndex > existingSection.startIndex) {
465
+ section.startIndex += diff;
466
+ section.endIndex += diff;
467
+ }
468
+ }
469
+ }
470
+
471
+ // Add new sections
472
+ if (additions.length > 0) {
473
+ // Find the best place to insert new sections
474
+ let insertPosition = 0;
475
+
476
+ // Try to insert after the main header
477
+ const headerMatch = /^# .+\n/m.exec(newContent);
478
+ if (headerMatch) {
479
+ insertPosition = headerMatch.index + headerMatch[0].length;
480
+
481
+ // Skip any immediate description
482
+ const descMatch = /\*\*IMPORTANT:.+\*\*\n/m.exec(newContent.substring(insertPosition));
483
+ if (descMatch && descMatch.index === 0) {
484
+ insertPosition += descMatch[0].length;
485
+ }
486
+ }
487
+
488
+ // Add each new section
489
+ const sectionsToAdd = additions.map(name => '\n' + this.formatSection(name) + '\n').join('');
490
+ newContent = newContent.substring(0, insertPosition) + sectionsToAdd + newContent.substring(insertPosition);
491
+ }
492
+
493
+ // Write updated content
494
+ fs.writeFileSync(this.houseRulesPath, newContent);
495
+
496
+ return {
497
+ updated: true,
498
+ path: this.houseRulesPath,
499
+ updatedSections: updates,
500
+ addedSections: additions,
501
+ totalChanges: updates.length + additions.length
502
+ };
503
+ }
504
+
505
+ /**
506
+ * Ensure backup folder is in gitignore
507
+ */
508
+ ensureBackupInGitignore() {
509
+ const gitignorePath = path.join(this.projectRoot, '.gitignore');
510
+ const backupPattern = 'DevopsAgent_Backups/';
511
+
512
+ try {
513
+ if (fs.existsSync(gitignorePath)) {
514
+ const content = fs.readFileSync(gitignorePath, 'utf8');
515
+
516
+ // Check if pattern already exists
517
+ if (!content.includes(backupPattern)) {
518
+ // Add to gitignore
519
+ const updatedContent = content.trimEnd() + '\n\n# DevOps Agent backup files\n' + backupPattern + '\n';
520
+ fs.writeFileSync(gitignorePath, updatedContent);
521
+ }
522
+ } else {
523
+ // Create gitignore with the pattern
524
+ const content = '# DevOps Agent backup files\n' + backupPattern + '\n';
525
+ fs.writeFileSync(gitignorePath, content);
526
+ }
527
+ } catch (err) {
528
+ // Silently fail - not critical if gitignore can't be updated
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Get status of house rules
534
+ */
535
+ getStatus() {
536
+ if (!this.houseRulesPath) {
537
+ return {
538
+ exists: false,
539
+ path: null,
540
+ managedSections: {},
541
+ needsUpdate: true
542
+ };
543
+ }
544
+
545
+ const content = fs.readFileSync(this.houseRulesPath, 'utf8');
546
+ const existingSections = this.extractManagedSections(content);
547
+ const status = {
548
+ exists: true,
549
+ path: this.houseRulesPath,
550
+ managedSections: {},
551
+ needsUpdate: false
552
+ };
553
+
554
+ // Check each managed section
555
+ for (const [sectionName, sectionData] of Object.entries(MANAGED_SECTIONS)) {
556
+ if (existingSections[sectionName]) {
557
+ const existing = existingSections[sectionName];
558
+ const needsUpdate = this.needsUpdate(sectionName, existing.version, existing.checksum);
559
+
560
+ status.managedSections[sectionName] = {
561
+ installed: true,
562
+ installedVersion: existing.version,
563
+ currentVersion: sectionData.version,
564
+ needsUpdate
565
+ };
566
+
567
+ if (needsUpdate) {
568
+ status.needsUpdate = true;
569
+ }
570
+ } else {
571
+ status.managedSections[sectionName] = {
572
+ installed: false,
573
+ currentVersion: sectionData.version,
574
+ needsUpdate: true
575
+ };
576
+ status.needsUpdate = true;
577
+ }
578
+ }
579
+
580
+ return status;
581
+ }
582
+ }
583
+
584
+ export default HouseRulesManager;
585
+
586
+ // CLI interface when run directly
587
+ // Check if this is the main module being run
588
+ const isMainModule = import.meta.url === `file://${process.argv[1]}` ||
589
+ process.argv[1]?.endsWith('house-rules-manager.js');
590
+
591
+ if (isMainModule) {
592
+ const manager = new HouseRulesManager();
593
+ const command = process.argv[2];
594
+
595
+ switch (command) {
596
+ case 'status':
597
+ // Output only the JSON for the bash script to parse
598
+ console.log(JSON.stringify(manager.getStatus()));
599
+ break;
600
+
601
+ case 'update':
602
+ manager.updateHouseRules().then(result => {
603
+ console.log(JSON.stringify(result));
604
+ });
605
+ break;
606
+
607
+ default:
608
+ console.log('Usage: node house-rules-manager.js [status|update]');
609
+ }
610
+ }