s9n-devops-agent 2.0.9 → 2.0.11-dev.0

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,422 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * ============================================================================
5
+ * COMMIT MESSAGE VALIDATOR WITH CONTRACT FLAGS
6
+ * ============================================================================
7
+ *
8
+ * This script validates commit messages and checks if contract files were
9
+ * updated when the commit claims to modify contracts.
10
+ *
11
+ * New Commit Format:
12
+ *
13
+ * ```
14
+ * feat(api): add user profile endpoint
15
+ *
16
+ * Contracts: [SQL:T, API:T, DB:F, 3RD:F, FEAT:T, INFRA:F]
17
+ *
18
+ * [WHY section...]
19
+ * [WHAT section...]
20
+ * ```
21
+ *
22
+ * Contract Flags:
23
+ * SQL:T/F - SQL_CONTRACT.json modified
24
+ * API:T/F - API_CONTRACT.md modified
25
+ * DB:T/F - DATABASE_SCHEMA_CONTRACT.md modified
26
+ * 3RD:T/F - THIRD_PARTY_INTEGRATIONS.md modified
27
+ * FEAT:T/F - FEATURES_CONTRACT.md modified
28
+ * INFRA:T/F - INFRA_CONTRACT.md modified
29
+ *
30
+ * Usage:
31
+ * node scripts/contract-automation/validate-commit.js
32
+ * node scripts/contract-automation/validate-commit.js --commit-msg=.claude-commit-msg
33
+ * node scripts/contract-automation/validate-commit.js --check-staged
34
+ *
35
+ * Options:
36
+ * --commit-msg=<path> Path to commit message file (default: .claude-commit-msg)
37
+ * --check-staged Check staged files in git
38
+ * --strict Fail on warnings
39
+ * --auto-fix Suggest correct contract flags
40
+ *
41
+ * ============================================================================
42
+ */
43
+
44
+ import fs from 'fs';
45
+ import path from 'path';
46
+ import { execSync } from 'child_process';
47
+ import { fileURLToPath } from 'url';
48
+
49
+ const __filename = fileURLToPath(import.meta.url);
50
+ const __dirname = path.dirname(__filename);
51
+
52
+ // Configuration
53
+ const CONFIG = {
54
+ rootDir: process.cwd(),
55
+ commitMsgFile: getArgValue('--commit-msg') || '.claude-commit-msg',
56
+ checkStaged: process.argv.includes('--check-staged'),
57
+ strict: process.argv.includes('--strict'),
58
+ autoFix: process.argv.includes('--auto-fix'),
59
+ contractsDir: 'House_Rules_Contracts'
60
+ };
61
+
62
+ // Contract file mapping
63
+ const CONTRACT_FILES = {
64
+ SQL: 'SQL_CONTRACT.json',
65
+ API: 'API_CONTRACT.md',
66
+ DB: 'DATABASE_SCHEMA_CONTRACT.md',
67
+ '3RD': 'THIRD_PARTY_INTEGRATIONS.md',
68
+ FEAT: 'FEATURES_CONTRACT.md',
69
+ INFRA: 'INFRA_CONTRACT.md'
70
+ };
71
+
72
+ // Helper: Get command line argument value
73
+ function getArgValue(argName) {
74
+ const arg = process.argv.find(a => a.startsWith(argName + '='));
75
+ return arg ? arg.split('=')[1] : null;
76
+ }
77
+
78
+ // Helper: Log with colors
79
+ function log(message, level = 'info') {
80
+ const colors = {
81
+ info: '\x1b[36m', // Cyan
82
+ warn: '\x1b[33m', // Yellow
83
+ error: '\x1b[31m', // Red
84
+ success: '\x1b[32m', // Green
85
+ reset: '\x1b[0m'
86
+ };
87
+
88
+ const prefix = {
89
+ info: '[INFO]',
90
+ warn: '[WARN]',
91
+ error: '[ERROR]',
92
+ success: '[SUCCESS]'
93
+ }[level];
94
+
95
+ const color = colors[level] || colors.reset;
96
+ console.log(`${color}${prefix} ${message}${colors.reset}`);
97
+ }
98
+
99
+ // Helper: Read file safely
100
+ function readFileSafe(filePath) {
101
+ try {
102
+ return fs.readFileSync(filePath, 'utf8');
103
+ } catch (error) {
104
+ return '';
105
+ }
106
+ }
107
+
108
+ // ============================================================================
109
+ // COMMIT MESSAGE PARSING
110
+ // ============================================================================
111
+
112
+ function parseCommitMessage(content) {
113
+ const lines = content.split('\n');
114
+
115
+ // Extract subject line
116
+ const subject = lines[0] || '';
117
+
118
+ // Extract contract flags
119
+ const contractLine = lines.find(l => l.trim().startsWith('Contracts:'));
120
+ let contractFlags = {};
121
+
122
+ if (contractLine) {
123
+ const flagsMatch = contractLine.match(/\[(.*?)\]/);
124
+ if (flagsMatch) {
125
+ const flagsStr = flagsMatch[1];
126
+ const flags = flagsStr.split(',').map(f => f.trim());
127
+
128
+ for (const flag of flags) {
129
+ const [key, value] = flag.split(':').map(s => s.trim());
130
+ contractFlags[key] = value === 'T' || value === 'true';
131
+ }
132
+ }
133
+ }
134
+
135
+ // Extract WHY and WHAT sections
136
+ const whyIndex = content.indexOf('[WHY');
137
+ const whatIndex = content.indexOf('[WHAT');
138
+
139
+ return {
140
+ subject,
141
+ contractFlags,
142
+ hasContractLine: !!contractLine,
143
+ hasWhy: whyIndex !== -1,
144
+ hasWhat: whatIndex !== -1,
145
+ raw: content
146
+ };
147
+ }
148
+
149
+ // ============================================================================
150
+ // GIT FILE CHECKING
151
+ // ============================================================================
152
+
153
+ function getStagedFiles() {
154
+ try {
155
+ const result = execSync('git diff --cached --name-only', { encoding: 'utf8' });
156
+ return result.trim().split('\n').filter(Boolean);
157
+ } catch (error) {
158
+ log('Failed to get staged files. Not in a git repository?', 'warn');
159
+ return [];
160
+ }
161
+ }
162
+
163
+ function getModifiedContractFiles(stagedFiles) {
164
+ const modified = {};
165
+
166
+ for (const [key, filename] of Object.entries(CONTRACT_FILES)) {
167
+ const filePath = path.join(CONFIG.contractsDir, filename);
168
+ modified[key] = stagedFiles.includes(filePath);
169
+ }
170
+
171
+ return modified;
172
+ }
173
+
174
+ // ============================================================================
175
+ // VALIDATION
176
+ // ============================================================================
177
+
178
+ function validateCommitMessage(parsed) {
179
+ const issues = [];
180
+ const warnings = [];
181
+
182
+ // Check subject line format
183
+ if (!parsed.subject.match(/^(feat|fix|refactor|docs|test|chore|style)\(/)) {
184
+ issues.push('Subject line must start with type(scope): (feat|fix|refactor|docs|test|chore|style)');
185
+ }
186
+
187
+ // Check for contract flags line
188
+ if (!parsed.hasContractLine) {
189
+ warnings.push('Missing "Contracts:" line. Add contract flags to enable automatic validation.');
190
+ }
191
+
192
+ // Check for WHY section
193
+ if (!parsed.hasWhy) {
194
+ warnings.push('Missing [WHY] section explaining motivation for changes.');
195
+ }
196
+
197
+ // Check for WHAT section
198
+ if (!parsed.hasWhat) {
199
+ warnings.push('Missing [WHAT] section listing specific file changes.');
200
+ }
201
+
202
+ return { issues, warnings };
203
+ }
204
+
205
+ function validateContractFlags(claimedFlags, actualModified) {
206
+ const mismatches = [];
207
+ const suggestions = [];
208
+
209
+ // Check each contract type
210
+ for (const [key, filename] of Object.entries(CONTRACT_FILES)) {
211
+ const claimed = claimedFlags[key] || false;
212
+ const actual = actualModified[key] || false;
213
+
214
+ if (claimed && !actual) {
215
+ mismatches.push({
216
+ type: 'false_positive',
217
+ contract: key,
218
+ message: `Commit claims ${key}:T but ${filename} was NOT modified`
219
+ });
220
+ suggestions.push(`Change ${key}:T to ${key}:F`);
221
+ }
222
+
223
+ if (!claimed && actual) {
224
+ mismatches.push({
225
+ type: 'false_negative',
226
+ contract: key,
227
+ message: `${filename} was modified but commit claims ${key}:F or missing`
228
+ });
229
+ suggestions.push(`Change ${key}:F to ${key}:T (or add if missing)`);
230
+ }
231
+ }
232
+
233
+ return { mismatches, suggestions };
234
+ }
235
+
236
+ // ============================================================================
237
+ // REPORTING
238
+ // ============================================================================
239
+
240
+ function generateReport(parsed, validation, contractValidation, stagedFiles) {
241
+ log('='.repeat(80));
242
+ log('COMMIT MESSAGE VALIDATION REPORT');
243
+ log('='.repeat(80));
244
+
245
+ // Subject line
246
+ log(`Subject: ${parsed.subject}`);
247
+ log('');
248
+
249
+ // Contract flags
250
+ if (parsed.hasContractLine) {
251
+ log('Contract Flags:');
252
+ for (const [key, value] of Object.entries(parsed.contractFlags)) {
253
+ const status = value ? '✅ TRUE' : '❌ FALSE';
254
+ log(` ${key}: ${status}`);
255
+ }
256
+ } else {
257
+ log('Contract Flags: ⚠️ NOT SPECIFIED', 'warn');
258
+ }
259
+ log('');
260
+
261
+ // Validation issues
262
+ if (validation.issues.length > 0) {
263
+ log('ERRORS:', 'error');
264
+ validation.issues.forEach(issue => log(` ❌ ${issue}`, 'error'));
265
+ log('');
266
+ }
267
+
268
+ if (validation.warnings.length > 0) {
269
+ log('WARNINGS:', 'warn');
270
+ validation.warnings.forEach(warning => log(` ⚠️ ${warning}`, 'warn'));
271
+ log('');
272
+ }
273
+
274
+ // Contract flag validation
275
+ if (contractValidation.mismatches.length > 0) {
276
+ log('CONTRACT FLAG MISMATCHES:', 'error');
277
+ contractValidation.mismatches.forEach(mismatch => {
278
+ log(` ❌ ${mismatch.message}`, 'error');
279
+ });
280
+ log('');
281
+
282
+ if (CONFIG.autoFix && contractValidation.suggestions.length > 0) {
283
+ log('SUGGESTED FIXES:', 'info');
284
+ contractValidation.suggestions.forEach(suggestion => {
285
+ log(` 💡 ${suggestion}`, 'info');
286
+ });
287
+ log('');
288
+ }
289
+ }
290
+
291
+ // Staged files
292
+ if (CONFIG.checkStaged) {
293
+ log('Staged Contract Files:');
294
+ let anyStaged = false;
295
+ for (const [key, filename] of Object.entries(CONTRACT_FILES)) {
296
+ const filePath = path.join(CONFIG.contractsDir, filename);
297
+ if (stagedFiles.includes(filePath)) {
298
+ log(` ✅ ${filename}`, 'success');
299
+ anyStaged = true;
300
+ }
301
+ }
302
+ if (!anyStaged) {
303
+ log(' (none)', 'info');
304
+ }
305
+ log('');
306
+ }
307
+
308
+ // Final result
309
+ log('='.repeat(80));
310
+
311
+ const hasErrors = validation.issues.length > 0 || contractValidation.mismatches.length > 0;
312
+ const hasWarnings = validation.warnings.length > 0;
313
+
314
+ if (hasErrors) {
315
+ log('VALIDATION FAILED ❌', 'error');
316
+ log('Please fix the errors above before committing.', 'error');
317
+ return false;
318
+ } else if (hasWarnings && CONFIG.strict) {
319
+ log('VALIDATION FAILED ⚠️ (strict mode)', 'warn');
320
+ log('Fix warnings or remove --strict flag.', 'warn');
321
+ return false;
322
+ } else if (hasWarnings) {
323
+ log('VALIDATION PASSED WITH WARNINGS ⚠️', 'warn');
324
+ log('Consider addressing warnings for better documentation.', 'warn');
325
+ return true;
326
+ } else {
327
+ log('VALIDATION PASSED ✅', 'success');
328
+ return true;
329
+ }
330
+ }
331
+
332
+ // ============================================================================
333
+ // AUTO-FIX
334
+ // ============================================================================
335
+
336
+ function generateCorrectedCommitMessage(parsed, actualModified) {
337
+ const lines = parsed.raw.split('\n');
338
+
339
+ // Generate correct contract flags
340
+ const flags = [];
341
+ for (const [key, _] of Object.entries(CONTRACT_FILES)) {
342
+ const value = actualModified[key] ? 'T' : 'F';
343
+ flags.push(`${key}:${value}`);
344
+ }
345
+ const contractLine = `Contracts: [${flags.join(', ')}]`;
346
+
347
+ // Find and replace contract line, or insert after subject
348
+ const contractLineIndex = lines.findIndex(l => l.trim().startsWith('Contracts:'));
349
+
350
+ if (contractLineIndex !== -1) {
351
+ lines[contractLineIndex] = contractLine;
352
+ } else {
353
+ // Insert after subject line (index 0) and blank line
354
+ lines.splice(2, 0, contractLine);
355
+ }
356
+
357
+ return lines.join('\n');
358
+ }
359
+
360
+ // ============================================================================
361
+ // MAIN EXECUTION
362
+ // ============================================================================
363
+
364
+ function main() {
365
+ log('='.repeat(80));
366
+ log('COMMIT MESSAGE VALIDATOR');
367
+ log('='.repeat(80));
368
+ log('');
369
+
370
+ // Read commit message
371
+ const commitMsgPath = path.join(CONFIG.rootDir, CONFIG.commitMsgFile);
372
+ if (!fs.existsSync(commitMsgPath)) {
373
+ log(`Commit message file not found: ${commitMsgPath}`, 'error');
374
+ process.exit(1);
375
+ }
376
+
377
+ const commitMsg = readFileSafe(commitMsgPath);
378
+ if (!commitMsg) {
379
+ log('Commit message is empty', 'error');
380
+ process.exit(1);
381
+ }
382
+
383
+ // Parse commit message
384
+ const parsed = parseCommitMessage(commitMsg);
385
+
386
+ // Validate commit message format
387
+ const validation = validateCommitMessage(parsed);
388
+
389
+ // Get staged files and check contract modifications
390
+ const stagedFiles = CONFIG.checkStaged ? getStagedFiles() : [];
391
+ const actualModified = CONFIG.checkStaged ? getModifiedContractFiles(stagedFiles) : {};
392
+
393
+ // Validate contract flags against actual changes
394
+ const contractValidation = CONFIG.checkStaged
395
+ ? validateContractFlags(parsed.contractFlags, actualModified)
396
+ : { mismatches: [], suggestions: [] };
397
+
398
+ // Generate report
399
+ const passed = generateReport(parsed, validation, contractValidation, stagedFiles);
400
+
401
+ // Auto-fix if requested
402
+ if (CONFIG.autoFix && contractValidation.mismatches.length > 0) {
403
+ log('='.repeat(80));
404
+ log('AUTO-FIX ENABLED', 'info');
405
+ log('='.repeat(80));
406
+
407
+ const corrected = generateCorrectedCommitMessage(parsed, actualModified);
408
+ const correctedPath = commitMsgPath + '.corrected';
409
+
410
+ fs.writeFileSync(correctedPath, corrected, 'utf8');
411
+ log(`Corrected commit message saved to: ${correctedPath}`, 'success');
412
+ log('Review and replace original if correct.', 'info');
413
+ }
414
+
415
+ log('='.repeat(80));
416
+
417
+ // Exit with appropriate code
418
+ process.exit(passed ? 0 : 1);
419
+ }
420
+
421
+ // Run
422
+ main();
@@ -0,0 +1,108 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname } from 'path';
5
+
6
+ const __filename = fileURLToPath(import.meta.url);
7
+ const __dirname = dirname(__filename);
8
+ const rootDir = path.join(__dirname, '..');
9
+
10
+ const CREDENTIALS_PATH = path.join(rootDir, 'local_deploy', 'credentials.json');
11
+
12
+ // Simple obfuscation to prevent casual shoulder surfing
13
+ // NOTE: This is NOT strong encryption. In a production environment with sensitive keys,
14
+ // one should rely on system keychains or proper secret management services.
15
+ // Since this is a local dev tool, this prevents accidental plain text commits/reads.
16
+ const obfuscate = (text) => Buffer.from(text).toString('base64');
17
+ const deobfuscate = (text) => Buffer.from(text, 'base64').toString('utf8');
18
+
19
+ export class CredentialsManager {
20
+ constructor() {
21
+ this.credentials = {};
22
+ this.load();
23
+ }
24
+
25
+ load() {
26
+ if (fs.existsSync(CREDENTIALS_PATH)) {
27
+ try {
28
+ const rawData = fs.readFileSync(CREDENTIALS_PATH, 'utf8');
29
+ const data = JSON.parse(rawData);
30
+
31
+ // Deobfuscate sensitive values
32
+ if (data.groqApiKey) {
33
+ data.groqApiKey = deobfuscate(data.groqApiKey);
34
+ }
35
+
36
+ this.credentials = data;
37
+ } catch (error) {
38
+ console.error('Failed to load credentials:', error.message);
39
+ this.credentials = {};
40
+ }
41
+ }
42
+ }
43
+
44
+ save() {
45
+ try {
46
+ // Ensure local_deploy exists
47
+ const localDeployDir = path.dirname(CREDENTIALS_PATH);
48
+ if (!fs.existsSync(localDeployDir)) {
49
+ fs.mkdirSync(localDeployDir, { recursive: true });
50
+ }
51
+
52
+ // Clone and obfuscate
53
+ const dataToSave = { ...this.credentials };
54
+ if (dataToSave.groqApiKey) {
55
+ dataToSave.groqApiKey = obfuscate(dataToSave.groqApiKey);
56
+ }
57
+
58
+ fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(dataToSave, null, 2));
59
+ } catch (error) {
60
+ console.error('Failed to save credentials:', error.message);
61
+ }
62
+ }
63
+
64
+ setGroqApiKey(key) {
65
+ if (!key) return;
66
+ this.credentials.groqApiKey = key;
67
+ this.credentials.updatedAt = new Date().toISOString();
68
+ this.save();
69
+ }
70
+
71
+ getGroqApiKey() {
72
+ return this.credentials.groqApiKey || null;
73
+ }
74
+
75
+ hasGroqApiKey() {
76
+ return !!this.credentials.groqApiKey;
77
+ }
78
+
79
+ clearAll() {
80
+ this.credentials = {};
81
+ if (fs.existsSync(CREDENTIALS_PATH)) {
82
+ fs.unlinkSync(CREDENTIALS_PATH);
83
+ }
84
+ }
85
+
86
+ /**
87
+ * Injects the Groq API Key into process.env
88
+ * Sets both OPENAI_API_KEY (legacy/compat) and GROQ_API_KEY (native)
89
+ * @returns {boolean} true if key was injected or already existed
90
+ */
91
+ injectEnv() {
92
+ const key = this.getGroqApiKey();
93
+ if (!key && !process.env.OPENAI_API_KEY && !process.env.GROQ_API_KEY) {
94
+ return false;
95
+ }
96
+
97
+ if (key) {
98
+ if (!process.env.OPENAI_API_KEY) process.env.OPENAI_API_KEY = key;
99
+ if (!process.env.GROQ_API_KEY) process.env.GROQ_API_KEY = key;
100
+ return true;
101
+ }
102
+
103
+ return true; // Env vars existed
104
+ }
105
+ }
106
+
107
+ // Singleton instance
108
+ export const credentialsManager = new CredentialsManager();
@@ -21,9 +21,13 @@ import fs from 'fs';
21
21
  import path from 'path';
22
22
  import { fileURLToPath } from 'url';
23
23
  import { dirname } from 'path';
24
- import { execSync, spawn, fork } from 'child_process';
25
- import crypto from 'crypto';
26
- import readline from 'readline';
24
+ import { spawn, execSync, exec } from 'child_process';
25
+ import { credentialsManager } from './credentials-manager.js';
26
+
27
+ // Inject credentials immediately
28
+ credentialsManager.injectEnv();
29
+
30
+ const __filename = fileURLToPath(import.meta.url);
27
31
  import { hasDockerConfiguration } from './docker-utils.js';
28
32
  import HouseRulesManager from './house-rules-manager.js';
29
33
 
@@ -1276,21 +1280,19 @@ The DevOps agent will automatically:
1276
1280
  console.log(`Please switch to this directory before making any changes:`);
1277
1281
  console.log(`cd "${instructions.worktreePath}"`);
1278
1282
  console.log(``);
1279
-
1280
- // Add house rules reference prominently at the top
1281
- const houseRulesExists = fs.existsSync(houseRulesPath);
1282
- if (houseRulesExists) {
1283
- console.log(`📋 IMPORTANT - READ PROJECT RULES FIRST:`);
1284
- console.log(`Before making any changes, read the house rules file at:`);
1285
- console.log(`${houseRulesPath}`);
1286
- console.log(``);
1287
- console.log(`The house rules contain:`);
1288
- console.log(`- Project coding conventions and standards`);
1289
- console.log(`- Required commit message formats`);
1290
- console.log(`- File coordination protocols`);
1291
- console.log(`- Branch naming and workflow rules`);
1292
- console.log(``);
1293
- }
1283
+ console.log(`📋 IMPORTANT - READ PROJECT RULES FIRST:`);
1284
+ console.log(`Before making ANY changes, you MUST read the project's house rules at:`);
1285
+ console.log(`${houseRulesPath}`);
1286
+ console.log(``);
1287
+ console.log(`The house rules file contains:`);
1288
+ console.log(`- Project coding conventions and standards`);
1289
+ console.log(`- Required commit message formats`);
1290
+ console.log(`- File coordination protocols`);
1291
+ console.log(`- Branch naming and workflow rules`);
1292
+ console.log(`- Testing and review requirements`);
1293
+ console.log(``);
1294
+ console.log(`You must follow ALL rules in this file. Read it carefully before proceeding.`);
1295
+ console.log(``);
1294
1296
 
1295
1297
  console.log(`⚠️ FILE COORDINATION (MANDATORY):`);
1296
1298
  console.log(`Shared coordination directory: local_deploy/.file-coordination/`);