s9n-devops-agent 2.0.13 → 2.0.18-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.
@@ -729,8 +729,13 @@ async function commitOnce(repoRoot, msgPath) {
729
729
  msg = rest.length > 0 ? `${msg}\n${rest.join('\n')}${infraDetails}` : `${msg}${infraDetails}`;
730
730
  }
731
731
 
732
+ const REQUIRE_AI_COMMIT = (process.env.AC_AI_COMMIT || "false").toLowerCase() === "true";
733
+
734
+ // ...
735
+
732
736
  let committed = false;
733
737
  if (REQUIRE_MSG && conventionalHeaderOK(msg)) {
738
+ // (Existing commit logic...)
734
739
  // Update infrastructure documentation before commit
735
740
  if (infraChanges.hasInfraChanges) {
736
741
  await updateInfrastructureDoc(infraChanges, msg);
@@ -753,6 +758,47 @@ async function commitOnce(repoRoot, msgPath) {
753
758
  fs.writeFileSync(tmp, msg + "\n");
754
759
  committed = (await run("git", ["commit", "-F", tmp])).ok;
755
760
  try { fs.unlinkSync(tmp); } catch {}
761
+ } else if (REQUIRE_MSG && !conventionalHeaderOK(msg) && REQUIRE_AI_COMMIT) {
762
+ // AI COMMIT GENERATION
763
+ log("generating AI commit message...");
764
+
765
+ // Look for the script in scripts/ or ../scripts/
766
+ let scriptPath = path.join(process.cwd(), 'scripts', 'generate-ai-commit.js');
767
+ if (!fs.existsSync(scriptPath)) {
768
+ // Try looking relative to __dirname
769
+ scriptPath = path.resolve(__dirname, '..', 'scripts', 'generate-ai-commit.js');
770
+ }
771
+
772
+ if (fs.existsSync(scriptPath)) {
773
+ try {
774
+ const { stdout } = await execa('node', [scriptPath], {
775
+ stdio: 'inherit',
776
+ env: { ...process.env, GROQ_API_KEY: process.env.GROQ_API_KEY }
777
+ });
778
+
779
+ // Re-read message after generation
780
+ msg = readMsgFile(msgPath);
781
+ if (conventionalHeaderOK(msg)) {
782
+ // Recursive call or just proceed? Let's proceed with commit logic duplicated or simple recursion
783
+ // To avoid recursion complexity, let's just commit here
784
+
785
+ // ... Duplicated commit logic or recursive call
786
+ // Let's recursively call commitOnce to keep it clean (busy flag handles concurrency)
787
+ busy = false; // Reset busy temporarily
788
+ await commitOnce(repoRoot, msgPath);
789
+ return;
790
+ }
791
+ } catch (err) {
792
+ log(`AI generation failed: ${err.message}`);
793
+ }
794
+ } else {
795
+ log(`AI script not found at ${scriptPath}`);
796
+ }
797
+
798
+ if (!committed) {
799
+ log("message still not ready after AI attempt; skipping commit");
800
+ return;
801
+ }
756
802
  } else if (!REQUIRE_MSG) {
757
803
  committed = (await run("git", ["commit", "-m", "chore: cs-devops-agent"])).ok;
758
804
  } else {
@@ -764,6 +810,16 @@ async function commitOnce(repoRoot, msgPath) {
764
810
  if (CLEAR_MSG_WHEN === "commit") clearMsgFile(msgPath);
765
811
 
766
812
  const sha = (await run("git", ["rev-parse", "--short", "HEAD"])).stdout.trim();
813
+
814
+ // VISIBLE NOTIFICATION FOR USER
815
+ console.log("\n" + "─".repeat(60));
816
+ console.log(`\x1b[32m✓ COMMITTED:\x1b[0m ${sha}`);
817
+ console.log(` Branch: ${await currentBranch()}`);
818
+ console.log(` Message: ${header}`);
819
+ console.log("");
820
+ console.log(`\x1b[36m💡 TO FINISH:\x1b[0m Type \x1b[1mexit\x1b[0m to close session and cleanup`);
821
+ console.log("─".repeat(60) + "\n");
822
+
767
823
  log(`committed ${sha} on ${await currentBranch()}`);
768
824
 
769
825
  if (PUSH) {
@@ -772,8 +828,13 @@ async function commitOnce(repoRoot, msgPath) {
772
828
  if (ok) {
773
829
  if (CLEAR_MSG_WHEN === "push") clearMsgFile(msgPath);
774
830
 
831
+ console.log(`\x1b[32m✓ PUSHED:\x1b[0m ${BRANCH} -> remote`);
832
+ console.log(""); // Empty line for spacing
833
+
775
834
  // Handle Docker restart if configured
776
835
  await handleDockerRestart();
836
+ } else {
837
+ console.log(`\x1b[31m✗ PUSH FAILED:\x1b[0m Check logs for details`);
777
838
  }
778
839
  }
779
840
  } finally {
@@ -1564,6 +1625,23 @@ console.log();
1564
1625
  ],
1565
1626
  })
1566
1627
  .on("all", async (evt, p) => {
1628
+ // Check if file is in local_deploy or .file-coordination (internal agent files)
1629
+ if (p.includes('local_deploy/') || p.includes('.file-coordination/')) {
1630
+ return;
1631
+ }
1632
+
1633
+ // Check if file is ignored by git (respect .gitignore)
1634
+ try {
1635
+ // git check-ignore returns 0 if ignored, 1 if not ignored
1636
+ // We use quiet mode (-q) and ignore stdio
1637
+ execSync(`git check-ignore -q "${p}"`, { stdio: 'ignore' });
1638
+ // If we get here, exit code was 0, so file IS ignored
1639
+ if (DEBUG) console.log(`[debug] Ignoring gitignored file: ${p}`);
1640
+ return;
1641
+ } catch (e) {
1642
+ // Exit code 1 means NOT ignored (or other error). Proceed.
1643
+ }
1644
+
1567
1645
  const now = Date.now();
1568
1646
  const isMsg = samePath(p, relMsg);
1569
1647
 
@@ -1618,8 +1696,10 @@ console.log();
1618
1696
  // INTERACTIVE COMMAND INTERFACE - Handle user commands during execution
1619
1697
  // ============================================================================
1620
1698
 
1621
- // Extract session ID from branch name or message file
1699
+ // Extract session ID from environment, branch name, or message file
1622
1700
  const sessionId = (() => {
1701
+ if (process.env.DEVOPS_SESSION_ID) return process.env.DEVOPS_SESSION_ID;
1702
+
1623
1703
  const branch = STATIC_BRANCH || BRANCH;
1624
1704
  const match = branch.match(/([a-z0-9]{4}-[a-z0-9]{4})/i);
1625
1705
  if (match) return match[1];
@@ -1651,7 +1731,7 @@ console.log();
1651
1731
  const rl = readline.createInterface({
1652
1732
  input: input,
1653
1733
  output: output,
1654
- prompt: '[agent] > ',
1734
+ prompt: '\\x1b[36m[agent] >\\x1b[0m ',
1655
1735
  terminal: true
1656
1736
  });
1657
1737
 
@@ -1754,7 +1834,7 @@ console.log();
1754
1834
  case 'exit':
1755
1835
  case 'quit':
1756
1836
  case 'q':
1757
- console.log("\nInitiating clean shutdown...");
1837
+ console.log("\\n\\x1b[35m[Session] Initiating clean shutdown...\\x1b[0m");
1758
1838
 
1759
1839
  // Check for uncommitted changes
1760
1840
  const uncommitted = await hasUncommittedChanges();
@@ -1995,7 +2075,7 @@ console.log();
1995
2075
  }
1996
2076
  }
1997
2077
 
1998
- console.log("\nGoodbye!");
2078
+ console.log("\n\\x1b[35m[Session] Exiting worker process...\\x1b[0m");
1999
2079
  rl.close();
2000
2080
  process.exit(0);
2001
2081
  break;
@@ -166,10 +166,10 @@ class HouseRulesManager {
166
166
 
167
167
  /**
168
168
  * Find existing house rules file
169
- * Searches across the repository, excluding the DevOps agent directories
169
+ * STRICT MODE: Only searches in the project root
170
170
  */
171
171
  findHouseRulesFile() {
172
- // First try the standard locations
172
+ // Standard locations in root only
173
173
  const possiblePaths = [
174
174
  'houserules.md',
175
175
  'HOUSERULES.md',
@@ -195,18 +195,8 @@ class HouseRulesManager {
195
195
  }
196
196
  }
197
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
-
198
+ // STRICT MODE: Do not search recursively.
199
+ // If not found in root, return null.
210
200
  return null;
211
201
  }
212
202
 
@@ -151,7 +151,15 @@ export function formatInstructions(session, options = {}) {
151
151
  lines.push('');
152
152
  }
153
153
 
154
- // Footer
154
+ // STOP instruction
155
+ lines.push('━'.repeat(70));
156
+ lines.push('');
157
+ lines.push(`${colors.red}${status.warning} IMPORTANT: STOP HERE${colors.reset}`);
158
+ lines.push('');
159
+ lines.push(`${colors.bright}Do NOT start coding or making changes yet!${colors.reset}`);
160
+ lines.push(`${colors.dim}Follow the steps above in order when instructed.${colors.reset}`);
161
+ lines.push(`${colors.dim}Wait for further instructions from the user before proceeding.${colors.reset}`);
162
+ lines.push('');
155
163
  lines.push('━'.repeat(70));
156
164
  lines.push('');
157
165
  lines.push(`${status.checkmark} ${colors.green}DevOps Agent is now monitoring this session...${colors.reset}`);
@@ -62,7 +62,7 @@ const CONFIG = {
62
62
  // SESSION COORDINATOR CLASS
63
63
  // ============================================================================
64
64
 
65
- class SessionCoordinator {
65
+ export class SessionCoordinator {
66
66
  constructor() {
67
67
  this.repoRoot = this.getRepoRoot();
68
68
  this.sessionsPath = path.join(this.repoRoot, CONFIG.sessionsDir);
@@ -265,20 +265,129 @@ class SessionCoordinator {
265
265
  }
266
266
  }
267
267
 
268
+ /**
269
+ * Initialize House Rules Contracts folder and files
270
+ */
271
+ async initializeContractsFolder() {
272
+ const contractsDir = path.join(this.repoRoot, 'House_Rules_Contracts');
273
+
274
+ // Check if contracts folder already exists
275
+ if (fs.existsSync(contractsDir)) {
276
+ console.log(`${CONFIG.colors.dim}✓ Contracts folder already exists${CONFIG.colors.reset}`);
277
+ return;
278
+ }
279
+
280
+ console.log(`\n${CONFIG.colors.blue}Creating contracts folder...${CONFIG.colors.reset}`);
281
+
282
+ try {
283
+ // Create contracts directory
284
+ fs.mkdirSync(contractsDir, { recursive: true });
285
+
286
+ // Find the npm package location to copy templates
287
+ const packageRoot = path.resolve(__dirname, '..');
288
+ const contractsTemplateDir = path.join(packageRoot, 'House_Rules_Contracts');
289
+
290
+ if (fs.existsSync(contractsTemplateDir)) {
291
+ // Copy all contract template files
292
+ const files = fs.readdirSync(contractsTemplateDir);
293
+ let copiedCount = 0;
294
+
295
+ for (const file of files) {
296
+ if (file.endsWith('.md') || file.endsWith('.json')) {
297
+ const srcPath = path.join(contractsTemplateDir, file);
298
+ const destPath = path.join(contractsDir, file);
299
+ const content = fs.readFileSync(srcPath, 'utf8');
300
+ fs.writeFileSync(destPath, content);
301
+ copiedCount++;
302
+ }
303
+ }
304
+
305
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Created contracts folder with ${copiedCount} template files`);
306
+ console.log(`${CONFIG.colors.dim} Location: ${contractsDir}${CONFIG.colors.reset}`);
307
+ console.log(`${CONFIG.colors.dim} Files: API_CONTRACT.md, DATABASE_SCHEMA_CONTRACT.md, SQL_CONTRACT.json, etc.${CONFIG.colors.reset}`);
308
+ } else {
309
+ // Fallback: create empty contracts folder with basic README
310
+ const readmeContent = `# House Rules Contracts\n\nThis folder contains contract files that document all project components.\n\nSee houserules.md for complete documentation on the Contract System.\n`;
311
+ fs.writeFileSync(path.join(contractsDir, 'README.md'), readmeContent);
312
+ console.log(`${CONFIG.colors.yellow}⚠ Created empty contracts folder (templates not found in package)${CONFIG.colors.reset}`);
313
+ console.log(`${CONFIG.colors.dim} You can manually populate contract files from the DevOps Agent repository${CONFIG.colors.reset}`);
314
+ }
315
+ } catch (err) {
316
+ console.log(`${CONFIG.colors.red}✗ Error creating contracts folder: ${err.message}${CONFIG.colors.reset}`);
317
+ console.log(`${CONFIG.colors.dim} You can manually create House_Rules_Contracts/ folder${CONFIG.colors.reset}`);
318
+ }
319
+ }
320
+
268
321
  /**
269
322
  * Ensure house rules are set up for the project
270
323
  */
271
324
  async ensureHouseRulesSetup() {
272
325
  const houseRulesManager = new HouseRulesManager(this.repoRoot);
326
+ const houseRulesPath = path.join(this.repoRoot, 'houserules.md');
273
327
 
274
328
  // Check if house rules exist
275
- if (!houseRulesManager.houseRulesPath || !fs.existsSync(houseRulesManager.houseRulesPath)) {
329
+ if (!fs.existsSync(houseRulesPath)) {
276
330
  console.log(`\n${CONFIG.colors.yellow}House rules not found - creating default house rules...${CONFIG.colors.reset}`);
277
- // Auto-create default house rules without prompting
278
- const result = await houseRulesManager.updateHouseRules({ createIfMissing: true, backupExisting: false });
279
- if (result.created) {
280
- console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} House rules created at: ${CONFIG.colors.bright}${result.path}${CONFIG.colors.reset}`);
331
+ console.log(`\n${CONFIG.colors.bright}=== Folder Organization Strategy ===${CONFIG.colors.reset}`);
332
+ console.log();
333
+ console.log(`${CONFIG.colors.bright}Option 1: STRUCTURED Organization${CONFIG.colors.reset}`);
334
+ console.log(`${CONFIG.colors.dim} NEW code follows a module-based structure:${CONFIG.colors.reset}`);
335
+ console.log(`${CONFIG.colors.dim} ModuleName/src/featurename/ (source code)${CONFIG.colors.reset}`);
336
+ console.log(`${CONFIG.colors.dim} ModuleName/test/featurename/ (tests)${CONFIG.colors.reset}`);
337
+ console.log(`${CONFIG.colors.dim} • Existing files stay where they are (no moving!)${CONFIG.colors.reset}`);
338
+ console.log(`${CONFIG.colors.dim} • Benefits: Better discoverability, clear ownership, scales well${CONFIG.colors.reset}`);
339
+ console.log(`${CONFIG.colors.dim} • Best for: Larger projects, teams, microservices${CONFIG.colors.reset}`);
340
+ console.log();
341
+ console.log(`${CONFIG.colors.bright}Option 2: FLEXIBLE Organization${CONFIG.colors.reset}`);
342
+ console.log(`${CONFIG.colors.dim} • Use any folder structure you prefer${CONFIG.colors.reset}`);
343
+ console.log(`${CONFIG.colors.dim} • No enforced patterns for new code${CONFIG.colors.reset}`);
344
+ console.log(`${CONFIG.colors.dim} • Benefits: Freedom, simplicity, less overhead${CONFIG.colors.reset}`);
345
+ console.log(`${CONFIG.colors.dim} • Best for: Small projects, prototypes, solo developers${CONFIG.colors.reset}`);
346
+ console.log();
347
+ console.log(`${CONFIG.colors.yellow}Note: Both include the full contract system for preventing duplicate work.${CONFIG.colors.reset}`);
348
+
349
+ const rl = readline.createInterface({
350
+ input: process.stdin,
351
+ output: process.stdout
352
+ });
353
+
354
+ const wantsStructure = await new Promise((resolve) => {
355
+ rl.question(`${CONFIG.colors.green}Use structured organization? (Y/n):${CONFIG.colors.reset} `, (answer) => {
356
+ rl.close();
357
+ resolve(answer.toLowerCase() !== 'n');
358
+ });
359
+ });
360
+
361
+ // Determine which template to copy
362
+ const templateName = wantsStructure ? 'houserules_structured.md' : 'houserules.md';
363
+
364
+ // Find the npm package location (where this script is running from)
365
+ // session-coordinator.js is in src/, so package root is one level up
366
+ const packageRoot = path.resolve(__dirname, '..');
367
+ const templatePath = path.join(packageRoot, templateName);
368
+
369
+ // Copy the template to the project root
370
+ try {
371
+ if (fs.existsSync(templatePath)) {
372
+ const templateContent = fs.readFileSync(templatePath, 'utf8');
373
+ fs.writeFileSync(houseRulesPath, templateContent);
374
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} House rules created at: ${CONFIG.colors.bright}${houseRulesPath}${CONFIG.colors.reset}`);
375
+ console.log(`${CONFIG.colors.dim}Using ${wantsStructure ? 'structured' : 'flexible'} organization template${CONFIG.colors.reset}`);
376
+ } else {
377
+ console.log(`${CONFIG.colors.yellow}⚠ Template not found, creating basic house rules...${CONFIG.colors.reset}`);
378
+ // Fallback to programmatic creation
379
+ const result = await houseRulesManager.updateHouseRules({ createIfMissing: true, backupExisting: false });
380
+ if (result.created) {
381
+ console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} House rules created at: ${CONFIG.colors.bright}${result.path}${CONFIG.colors.reset}`);
382
+ }
383
+ }
384
+ } catch (err) {
385
+ console.log(`${CONFIG.colors.red}✗ Error creating house rules: ${err.message}${CONFIG.colors.reset}`);
386
+ console.log(`${CONFIG.colors.dim}You can manually create houserules.md in your project root${CONFIG.colors.reset}`);
281
387
  }
388
+
389
+ // Initialize contracts folder after house rules are created
390
+ await this.initializeContractsFolder();
282
391
  } else {
283
392
  // House rules exist - check if they need updating
284
393
  const status = houseRulesManager.getStatus();
@@ -1014,7 +1123,44 @@ class SessionCoordinator {
1014
1123
 
1015
1124
  const sessionId = this.generateSessionId();
1016
1125
  const task = options.task || 'development';
1017
- const agentType = options.agent || 'claude';
1126
+
1127
+ // If agent type wasn't provided, ask for it now
1128
+ let agentType = options.agent;
1129
+ if (!agentType) {
1130
+ const rl = readline.createInterface({
1131
+ input: process.stdin,
1132
+ output: process.stdout
1133
+ });
1134
+
1135
+ console.log(`\n${CONFIG.colors.blue}Select Agent Type:${CONFIG.colors.reset}`);
1136
+ console.log(` 1) Claude (default)`);
1137
+ console.log(` 2) Cline`);
1138
+ console.log(` 3) Cursor`);
1139
+ console.log(` 4) Copilot`);
1140
+ console.log(` 5) Warp`);
1141
+ console.log(` 6) Custom\n`);
1142
+
1143
+ const agentChoice = await new Promise(resolve => {
1144
+ rl.question('Agent [1]: ', resolve);
1145
+ });
1146
+
1147
+ switch(agentChoice.trim() || '1') {
1148
+ case '1': agentType = 'claude'; break;
1149
+ case '2': agentType = 'cline'; break;
1150
+ case '3': agentType = 'cursor'; break;
1151
+ case '4': agentType = 'copilot'; break;
1152
+ case '5': agentType = 'warp'; break;
1153
+ case '6':
1154
+ agentType = await new Promise(resolve => {
1155
+ rl.question('Enter agent name: ', resolve);
1156
+ });
1157
+ agentType = agentType.trim() || 'claude';
1158
+ break;
1159
+ default: agentType = 'claude';
1160
+ }
1161
+ rl.close();
1162
+ }
1163
+
1018
1164
  const devInitials = this.getDeveloperInitials();
1019
1165
 
1020
1166
  console.log(`\n${CONFIG.colors.bgBlue}${CONFIG.colors.bright} Creating New Session ${CONFIG.colors.reset}`);
@@ -1405,6 +1551,11 @@ The DevOps agent will automatically:
1405
1551
  console.log(``);
1406
1552
  console.log(`Write commit messages to: .devops-commit-${sessionId}.msg`);
1407
1553
  console.log(`The DevOps agent will automatically commit and push changes.`);
1554
+ console.log(``);
1555
+ console.log(`⛔ IMPORTANT: STOP HERE AND WAIT`);
1556
+ console.log(`Do NOT start coding or making changes yet!`);
1557
+ console.log(`Follow the steps above in order when instructed by the user.`);
1558
+ console.log(`Wait for further instructions before proceeding.`);
1408
1559
  console.log();
1409
1560
 
1410
1561
  console.log(`${CONFIG.colors.yellow}══════════════════════════════════════════════════════════════${CONFIG.colors.reset}`);
@@ -2173,7 +2324,8 @@ async function main() {
2173
2324
  console.log(` 2) Cline`);
2174
2325
  console.log(` 3) Cursor`);
2175
2326
  console.log(` 4) Copilot`);
2176
- console.log(` 5) Custom\n`);
2327
+ console.log(` 5) Warp`);
2328
+ console.log(` 6) Custom\n`);
2177
2329
 
2178
2330
  const agentChoice = await new Promise(resolve => {
2179
2331
  rl.question('Agent [1]: ', resolve);
@@ -2185,7 +2337,8 @@ async function main() {
2185
2337
  case '2': agent = 'cline'; break;
2186
2338
  case '3': agent = 'cursor'; break;
2187
2339
  case '4': agent = 'copilot'; break;
2188
- case '5':
2340
+ case '5': agent = 'warp'; break;
2341
+ case '6':
2189
2342
  const customAgent = await new Promise(resolve => {
2190
2343
  rl.question('Enter agent name: ', resolve);
2191
2344
  });
@@ -2221,7 +2374,7 @@ async function main() {
2221
2374
 
2222
2375
  const agent = args.includes('--agent') ?
2223
2376
  args[args.indexOf('--agent') + 1] :
2224
- 'claude';
2377
+ undefined; // Pass undefined to trigger prompt in createSession
2225
2378
 
2226
2379
  await coordinator.createAndStart({ task, agent });
2227
2380
  break;
@@ -2301,8 +2454,10 @@ ${CONFIG.colors.yellow}Typical Workflow:${CONFIG.colors.reset}
2301
2454
  }
2302
2455
  }
2303
2456
 
2304
- // Run the CLI
2305
- main().catch(err => {
2306
- console.error(`${CONFIG.colors.red}Error: ${err.message}${CONFIG.colors.reset}`);
2307
- process.exit(1);
2308
- });
2457
+ // Run the CLI only if executed directly
2458
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
2459
+ main().catch(err => {
2460
+ console.error(`${CONFIG.colors.red}Error: ${err.message}${CONFIG.colors.reset}`);
2461
+ process.exit(1);
2462
+ });
2463
+ }