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.
@@ -0,0 +1,443 @@
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
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ // Version of our house rules sections
24
+ const HOUSERULES_VERSION = '1.3.0';
25
+
26
+ // Managed sections with their content
27
+ const MANAGED_SECTIONS = {
28
+ 'file-coordination': {
29
+ version: '1.3.0',
30
+ title: '## 🚨 CRITICAL: File Coordination Protocol (MUST FOLLOW)',
31
+ content: `**IMPORTANT: Always check the house rules at the beginning of each session!**
32
+
33
+ To prevent conflicts with other agents editing the same files, you MUST follow this protocol:
34
+
35
+ ### Before Editing ANY Files:
36
+
37
+ 1. **DECLARE YOUR INTENT FIRST**
38
+ Create a file at \`.file-coordination/active-edits/<your-name>-<session>.json\` with:
39
+ \`\`\`json
40
+ {
41
+ "agent": "<your-name>",
42
+ "session": "<session-id>",
43
+ "files": ["list", "of", "files", "you", "will", "edit"],
44
+ "operation": "edit",
45
+ "reason": "Brief description of what you're doing",
46
+ "declaredAt": "<current-ISO-8601-timestamp>",
47
+ "estimatedDuration": 300
48
+ }
49
+ \`\`\`
50
+
51
+ 2. **CHECK FOR CONFLICTS**
52
+ - Read ALL files in \`.file-coordination/active-edits/\`
53
+ - If ANY other agent has declared the same files, you must:
54
+ - WAIT for them to finish, OR
55
+ - Choose different files to edit
56
+
57
+ 3. **ONLY EDIT DECLARED FILES**
58
+ - Never edit files you haven't declared
59
+ - Stay within your declared scope
60
+
61
+ 4. **RELEASE WHEN DONE**
62
+ - Delete your declaration file after completing edits
63
+ - Or move it to \`.file-coordination/completed-edits/\`
64
+
65
+ ### If You Detect a Conflict:
66
+ - DO NOT proceed with edits
67
+ - Report the conflict to the user
68
+ - Wait or choose alternative files
69
+
70
+ ### Helper Scripts Available:
71
+ - \`./scripts/coordination/check-file-availability.sh <files>\` - Check if files are available
72
+ - \`./scripts/coordination/declare-file-edits.sh <agent> <session> <files>\` - Declare your intent
73
+ - \`./scripts/coordination/release-file-edits.sh <agent> <session>\` - Release files after editing
74
+
75
+ **This coordination prevents wasted work and merge conflicts!**`
76
+ },
77
+ 'core-principles': {
78
+ version: '1.0.0',
79
+ title: '## Core Principles',
80
+ content: `1. **Always preserve existing functionality** - Never break working code
81
+ 2. **Follow existing patterns** - Match the codebase style and conventions
82
+ 3. **Communicate clearly** - Document your changes and reasoning
83
+ 4. **Coordinate with others** - Follow the file coordination protocol below`
84
+ },
85
+ 'project-conventions': {
86
+ version: '1.0.0',
87
+ title: '## Project Conventions',
88
+ content: `### Code Style
89
+ - Follow existing indentation and formatting patterns
90
+ - Maintain consistent naming conventions used in the project
91
+ - Keep functions small and focused
92
+ - Write clear, descriptive variable and function names
93
+
94
+ ### Git Workflow
95
+ - Write clear, descriptive commit messages
96
+ - Follow conventional commit format when applicable (feat:, fix:, docs:, etc.)
97
+ - Keep commits atomic and focused on a single change
98
+ - Never commit sensitive information or credentials
99
+
100
+ ### Testing
101
+ - Write tests for new functionality
102
+ - Ensure existing tests pass before committing
103
+ - Update tests when changing functionality
104
+
105
+ ### Documentation
106
+ - Update README files when adding new features
107
+ - Document complex logic with clear comments
108
+ - Keep API documentation up to date
109
+ - Update CHANGELOG for significant changes`
110
+ }
111
+ };
112
+
113
+ class HouseRulesManager {
114
+ constructor(projectRoot) {
115
+ this.projectRoot = projectRoot || process.cwd();
116
+ this.houseRulesPath = null;
117
+ this.findHouseRulesFile();
118
+ }
119
+
120
+ /**
121
+ * Find existing house rules file
122
+ */
123
+ findHouseRulesFile() {
124
+ const possiblePaths = [
125
+ 'houserules.md',
126
+ 'HOUSERULES.md',
127
+ '.github/HOUSERULES.md',
128
+ 'docs/houserules.md',
129
+ 'docs/HOUSERULES.md'
130
+ ];
131
+
132
+ for (const relativePath of possiblePaths) {
133
+ const fullPath = path.join(this.projectRoot, relativePath);
134
+ if (fs.existsSync(fullPath)) {
135
+ this.houseRulesPath = fullPath;
136
+ return fullPath;
137
+ }
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ /**
144
+ * Calculate checksum for content
145
+ */
146
+ calculateChecksum(content) {
147
+ return crypto.createHash('md5').update(content.trim()).digest('hex').substring(0, 8);
148
+ }
149
+
150
+ /**
151
+ * Create section marker
152
+ */
153
+ createSectionMarker(sectionName, version, checksum) {
154
+ return `<!-- DEVOPS_AGENT_SECTION:${sectionName}:${version}:${checksum} -->`;
155
+ }
156
+
157
+ /**
158
+ * Create end marker
159
+ */
160
+ createEndMarker(sectionName) {
161
+ return `<!-- END_DEVOPS_AGENT_SECTION:${sectionName} -->`;
162
+ }
163
+
164
+ /**
165
+ * Extract managed sections from existing house rules
166
+ */
167
+ extractManagedSections(content) {
168
+ const sections = {};
169
+ const pattern = /<!-- DEVOPS_AGENT_SECTION:(\w+[-\w]*):([0-9.]+):([a-f0-9]+) -->([\s\S]*?)<!-- END_DEVOPS_AGENT_SECTION:\1 -->/g;
170
+
171
+ let match;
172
+ while ((match = pattern.exec(content)) !== null) {
173
+ sections[match[1]] = {
174
+ name: match[1],
175
+ version: match[2],
176
+ checksum: match[3],
177
+ content: match[4].trim(),
178
+ startIndex: match.index,
179
+ endIndex: match.index + match[0].length
180
+ };
181
+ }
182
+
183
+ return sections;
184
+ }
185
+
186
+ /**
187
+ * Check if a section needs updating
188
+ */
189
+ needsUpdate(sectionName, existingVersion, existingChecksum) {
190
+ const currentSection = MANAGED_SECTIONS[sectionName];
191
+ if (!currentSection) return false;
192
+
193
+ // Check version
194
+ if (this.compareVersions(currentSection.version, existingVersion) > 0) {
195
+ return true;
196
+ }
197
+
198
+ // Check checksum (in case we updated content without version bump)
199
+ const currentChecksum = this.calculateChecksum(currentSection.content);
200
+ return currentChecksum !== existingChecksum;
201
+ }
202
+
203
+ /**
204
+ * Compare semantic versions
205
+ */
206
+ compareVersions(v1, v2) {
207
+ const parts1 = v1.split('.').map(Number);
208
+ const parts2 = v2.split('.').map(Number);
209
+
210
+ for (let i = 0; i < 3; i++) {
211
+ if (parts1[i] > parts2[i]) return 1;
212
+ if (parts1[i] < parts2[i]) return -1;
213
+ }
214
+
215
+ return 0;
216
+ }
217
+
218
+ /**
219
+ * Format a managed section with markers
220
+ */
221
+ formatSection(sectionName) {
222
+ const section = MANAGED_SECTIONS[sectionName];
223
+ if (!section) return '';
224
+
225
+ const checksum = this.calculateChecksum(section.content);
226
+ const marker = this.createSectionMarker(sectionName, section.version, checksum);
227
+ const endMarker = this.createEndMarker(sectionName);
228
+
229
+ return `${marker}
230
+ ${section.title}
231
+
232
+ ${section.content}
233
+ ${endMarker}`;
234
+ }
235
+
236
+ /**
237
+ * Create new house rules with all managed sections
238
+ */
239
+ createNewHouseRules() {
240
+ const sections = [];
241
+
242
+ // Header
243
+ sections.push(`# House Rules for AI Agents
244
+
245
+ **IMPORTANT: All AI agents (Claude, Cline, Copilot, etc.) must read and follow these rules at the start of each session.**
246
+ `);
247
+
248
+ // Add managed sections
249
+ sections.push(this.formatSection('file-coordination'));
250
+ sections.push('');
251
+ sections.push(this.formatSection('core-principles'));
252
+ sections.push('');
253
+ sections.push(this.formatSection('project-conventions'));
254
+
255
+ return sections.join('\n');
256
+ }
257
+
258
+ /**
259
+ * Update house rules intelligently
260
+ */
261
+ async updateHouseRules(options = {}) {
262
+ const { createIfMissing = true, backupExisting = true } = options;
263
+
264
+ // Find or create house rules file
265
+ if (!this.houseRulesPath) {
266
+ if (!createIfMissing) {
267
+ return { updated: false, reason: 'No house rules file found' };
268
+ }
269
+
270
+ this.houseRulesPath = path.join(this.projectRoot, 'houserules.md');
271
+ const content = this.createNewHouseRules();
272
+ fs.writeFileSync(this.houseRulesPath, content);
273
+
274
+ return {
275
+ updated: true,
276
+ created: true,
277
+ path: this.houseRulesPath,
278
+ sections: Object.keys(MANAGED_SECTIONS)
279
+ };
280
+ }
281
+
282
+ // Read existing content
283
+ const existingContent = fs.readFileSync(this.houseRulesPath, 'utf8');
284
+ const existingSections = this.extractManagedSections(existingContent);
285
+
286
+ // Check what needs updating
287
+ const updates = [];
288
+ const additions = [];
289
+
290
+ for (const [sectionName, sectionData] of Object.entries(MANAGED_SECTIONS)) {
291
+ if (existingSections[sectionName]) {
292
+ // Section exists - check if needs update
293
+ if (this.needsUpdate(sectionName, existingSections[sectionName].version, existingSections[sectionName].checksum)) {
294
+ updates.push(sectionName);
295
+ }
296
+ } else {
297
+ // Section doesn't exist - needs to be added
298
+ additions.push(sectionName);
299
+ }
300
+ }
301
+
302
+ // If no updates needed, return
303
+ if (updates.length === 0 && additions.length === 0) {
304
+ return { updated: false, reason: 'All sections are up to date' };
305
+ }
306
+
307
+ // Create backup if requested
308
+ if (backupExisting) {
309
+ const backupPath = `${this.houseRulesPath}.backup.${Date.now()}`;
310
+ fs.copyFileSync(this.houseRulesPath, backupPath);
311
+ }
312
+
313
+ // Build new content
314
+ let newContent = existingContent;
315
+
316
+ // Update existing sections
317
+ for (const sectionName of updates) {
318
+ const existingSection = existingSections[sectionName];
319
+ const newSection = this.formatSection(sectionName);
320
+
321
+ // Replace the old section with the new one
322
+ const beforeSection = newContent.substring(0, existingSection.startIndex);
323
+ const afterSection = newContent.substring(existingSection.endIndex);
324
+ newContent = beforeSection + newSection + afterSection;
325
+
326
+ // Recalculate positions for remaining sections
327
+ const diff = newSection.length - (existingSection.endIndex - existingSection.startIndex);
328
+ for (const section of Object.values(existingSections)) {
329
+ if (section.startIndex > existingSection.startIndex) {
330
+ section.startIndex += diff;
331
+ section.endIndex += diff;
332
+ }
333
+ }
334
+ }
335
+
336
+ // Add new sections
337
+ if (additions.length > 0) {
338
+ // Find the best place to insert new sections
339
+ let insertPosition = 0;
340
+
341
+ // Try to insert after the main header
342
+ const headerMatch = /^# .+\n/m.exec(newContent);
343
+ if (headerMatch) {
344
+ insertPosition = headerMatch.index + headerMatch[0].length;
345
+
346
+ // Skip any immediate description
347
+ const descMatch = /\*\*IMPORTANT:.+\*\*\n/m.exec(newContent.substring(insertPosition));
348
+ if (descMatch && descMatch.index === 0) {
349
+ insertPosition += descMatch[0].length;
350
+ }
351
+ }
352
+
353
+ // Add each new section
354
+ const sectionsToAdd = additions.map(name => '\n' + this.formatSection(name) + '\n').join('');
355
+ newContent = newContent.substring(0, insertPosition) + sectionsToAdd + newContent.substring(insertPosition);
356
+ }
357
+
358
+ // Write updated content
359
+ fs.writeFileSync(this.houseRulesPath, newContent);
360
+
361
+ return {
362
+ updated: true,
363
+ path: this.houseRulesPath,
364
+ updatedSections: updates,
365
+ addedSections: additions,
366
+ totalChanges: updates.length + additions.length
367
+ };
368
+ }
369
+
370
+ /**
371
+ * Get status of house rules
372
+ */
373
+ getStatus() {
374
+ if (!this.houseRulesPath) {
375
+ return {
376
+ exists: false,
377
+ path: null,
378
+ managedSections: {},
379
+ needsUpdate: true
380
+ };
381
+ }
382
+
383
+ const content = fs.readFileSync(this.houseRulesPath, 'utf8');
384
+ const existingSections = this.extractManagedSections(content);
385
+ const status = {
386
+ exists: true,
387
+ path: this.houseRulesPath,
388
+ managedSections: {},
389
+ needsUpdate: false
390
+ };
391
+
392
+ // Check each managed section
393
+ for (const [sectionName, sectionData] of Object.entries(MANAGED_SECTIONS)) {
394
+ if (existingSections[sectionName]) {
395
+ const existing = existingSections[sectionName];
396
+ const needsUpdate = this.needsUpdate(sectionName, existing.version, existing.checksum);
397
+
398
+ status.managedSections[sectionName] = {
399
+ installed: true,
400
+ installedVersion: existing.version,
401
+ currentVersion: sectionData.version,
402
+ needsUpdate
403
+ };
404
+
405
+ if (needsUpdate) {
406
+ status.needsUpdate = true;
407
+ }
408
+ } else {
409
+ status.managedSections[sectionName] = {
410
+ installed: false,
411
+ currentVersion: sectionData.version,
412
+ needsUpdate: true
413
+ };
414
+ status.needsUpdate = true;
415
+ }
416
+ }
417
+
418
+ return status;
419
+ }
420
+ }
421
+
422
+ export default HouseRulesManager;
423
+
424
+ // CLI interface when run directly
425
+ if (import.meta.url === `file://${process.argv[1]}`) {
426
+ const manager = new HouseRulesManager();
427
+ const command = process.argv[2];
428
+
429
+ switch (command) {
430
+ case 'status':
431
+ console.log('House Rules Status:', JSON.stringify(manager.getStatus(), null, 2));
432
+ break;
433
+
434
+ case 'update':
435
+ manager.updateHouseRules().then(result => {
436
+ console.log('Update Result:', JSON.stringify(result, null, 2));
437
+ });
438
+ break;
439
+
440
+ default:
441
+ console.log('Usage: node house-rules-manager.js [status|update]');
442
+ }
443
+ }