s9n-devops-agent 2.0.18-dev.2 → 2.0.18-dev.4

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 CHANGED
@@ -81,6 +81,7 @@ s9n-devops-agent start
81
81
  ### 🌲 Smart Branch Management
82
82
  - **Hierarchy:** `session/task` → `daily/date` → `main`.
83
83
  - **Auto-Merge:** Sessions automatically merge into daily branches, which roll over to main.
84
+ - **Base Branch Selection:** Choose any branch (main, develop, etc.) as the starting point for your session worktree.
84
85
 
85
86
  ### 📋 House Rules System
86
87
  - **Context Injection:** AI agents read `docs/houserules.md` to understand your coding conventions.
@@ -1,3 +1,18 @@
1
+ # Release Notes - s9n-devops-agent v2.0.18-dev.3
2
+
3
+ ## 🚀 Enhancements
4
+ - **Base Branch Selection**: You can now select a base branch (e.g., main, develop) when starting a session, allowing for cleaner feature branching from stable points.
5
+ - **Enhanced Setup Wizard**:
6
+ - Finds and merges contract files from subdirectories.
7
+ - Ensures versioning strategy is configured.
8
+ - Persists credentials in user home directory to survive package updates.
9
+
10
+ ## 🐛 Fixes
11
+ - **Update Logic**: Fixed update checker to respect dev versions.
12
+ - **Credentials**: Fixed issue where API keys were lost during updates.
13
+
14
+ ---
15
+
1
16
  # Release Notes - s9n-devops-agent v2.0.11-dev.0
2
17
 
3
18
  ## 🐛 Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s9n-devops-agent",
3
- "version": "2.0.18-dev.2",
3
+ "version": "2.0.18-dev.4",
4
4
  "description": "CS_DevOpsAgent - Intelligent Git Automation System with multi-agent support and session management",
5
5
  "type": "module",
6
6
  "main": "src/cs-devops-agent-worker.js",
@@ -3,11 +3,15 @@ import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname } from 'path';
5
5
 
6
+ import os from 'os';
7
+
6
8
  const __filename = fileURLToPath(import.meta.url);
7
9
  const __dirname = dirname(__filename);
8
- const rootDir = path.join(__dirname, '..');
9
10
 
10
- const CREDENTIALS_PATH = process.env.DEVOPS_CREDENTIALS_PATH || path.join(rootDir, 'local_deploy', 'credentials.json');
11
+ // Use home directory for persistent storage across package updates
12
+ const HOME_DIR = os.homedir();
13
+ const CONFIG_DIR = path.join(HOME_DIR, '.devops-agent');
14
+ const CREDENTIALS_PATH = process.env.DEVOPS_CREDENTIALS_PATH || path.join(CONFIG_DIR, 'credentials.json');
11
15
 
12
16
  // Simple obfuscation to prevent casual shoulder surfing
13
17
  // NOTE: This is NOT strong encryption. In a production environment with sensitive keys,
@@ -38,15 +42,33 @@ export class CredentialsManager {
38
42
  console.error('Failed to load credentials:', error.message);
39
43
  this.credentials = {};
40
44
  }
45
+ } else {
46
+ // Migration: Check for old local_deploy location
47
+ const oldPath = path.join(__dirname, '..', 'local_deploy', 'credentials.json');
48
+ if (fs.existsSync(oldPath)) {
49
+ try {
50
+ const rawData = fs.readFileSync(oldPath, 'utf8');
51
+ const data = JSON.parse(rawData);
52
+ // Deobfuscate sensitive values
53
+ if (data.groqApiKey) {
54
+ data.groqApiKey = deobfuscate(data.groqApiKey);
55
+ }
56
+ this.credentials = data;
57
+ // Save to new location immediately
58
+ this.save();
59
+ } catch (e) {
60
+ // Ignore migration errors
61
+ }
62
+ }
41
63
  }
42
64
  }
43
65
 
44
66
  save() {
45
67
  try {
46
- // Ensure local_deploy exists
47
- const localDeployDir = path.dirname(CREDENTIALS_PATH);
48
- if (!fs.existsSync(localDeployDir)) {
49
- fs.mkdirSync(localDeployDir, { recursive: true });
68
+ // Ensure config dir exists
69
+ const configDir = path.dirname(CREDENTIALS_PATH);
70
+ if (!fs.existsSync(configDir)) {
71
+ fs.mkdirSync(configDir, { recursive: true });
50
72
  }
51
73
 
52
74
  // Clone and obfuscate
@@ -165,6 +165,10 @@ const FORCE_ROLLOVER = (process.env.AC_FORCE_ROLLOVER || "false").toLowerCase(
165
165
  const VERSION_PREFIX = process.env.AC_VERSION_PREFIX || "v0.";
166
166
  const VERSION_START_MINOR = Number(process.env.AC_VERSION_START_MINOR || "20"); // Start at v0.20 for micro-revisions
167
167
  const VERSION_BASE_REF = process.env.AC_VERSION_BASE_REF || "origin/main"; // where new version branches start
168
+
169
+ // Rebase configuration
170
+ const REBASE_INTERVAL_HOURS = Number(process.env.AC_REBASE_INTERVAL || 0);
171
+ const BASE_BRANCH = process.env.AC_BASE_BRANCH || 'HEAD';
168
172
  // ------------------------------------------------
169
173
 
170
174
  const log = (...a) => console.log("[cs-devops-agent]", ...a);
@@ -953,6 +957,109 @@ function detectInfrastructureChanges(changedFiles) {
953
957
  return detected;
954
958
  }
955
959
 
960
+ // ============================================================================
961
+ // AUTO-REBASE FUNCTIONALITY
962
+ // ============================================================================
963
+
964
+ /**
965
+ * Check if it's time to rebase and perform the operation
966
+ * @param {string} repoRoot - Repository root
967
+ * @returns {Promise<boolean>} - True if rebase occurred
968
+ */
969
+ async function checkAndPerformRebase(repoRoot) {
970
+ if (REBASE_INTERVAL_HOURS <= 0) return false;
971
+ if (BASE_BRANCH === 'HEAD') return false; // Can't rebase from HEAD (relative)
972
+
973
+ // Calculate interval in ms
974
+ const intervalMs = REBASE_INTERVAL_HOURS * 60 * 60 * 1000;
975
+ const now = Date.now();
976
+
977
+ // Check if enough time has passed
978
+ if (!global.lastRebaseTime) global.lastRebaseTime = now; // Initialize on first run
979
+ if (now - global.lastRebaseTime < intervalMs) return false;
980
+
981
+ // Don't rebase if we are busy
982
+ if (busy) return false;
983
+
984
+ try {
985
+ busy = true;
986
+
987
+ console.log('\n' + '━'.repeat(60));
988
+ console.log(`\x1b[33m⚠️ AUTO-REBASE IN PROGRESS - PAUSING AGENT...\x1b[0m`);
989
+ console.log(`Interval: ${REBASE_INTERVAL_HOURS} hours reached.`);
990
+ console.log(`Base: ${BASE_BRANCH}`);
991
+ console.log('━'.repeat(60) + '\n');
992
+
993
+ // Check for uncommitted changes
994
+ const dirty = await hasUncommittedChanges();
995
+ let stashed = false;
996
+
997
+ if (dirty) {
998
+ log('Stashing uncommitted changes before rebase...');
999
+ const stashRes = await run('git', ['stash', 'push', '-m', `Auto-stash before rebase ${new Date().toISOString()}`]);
1000
+ if (stashRes.ok) stashed = true;
1001
+ else {
1002
+ console.error('\x1b[31m✗ Failed to stash changes. Aborting rebase.\x1b[0m');
1003
+ busy = false;
1004
+ return false;
1005
+ }
1006
+ }
1007
+
1008
+ // Fetch latest
1009
+ log(`Fetching latest changes from origin...`);
1010
+ await run('git', ['fetch', 'origin', BASE_BRANCH]);
1011
+
1012
+ // Rebase
1013
+ log(`Rebasing onto origin/${BASE_BRANCH}...`);
1014
+ const rebaseRes = await run('git', ['pull', '--rebase', 'origin', BASE_BRANCH]);
1015
+
1016
+ if (rebaseRes.ok) {
1017
+ console.log(`\x1b[32m✓ Rebase successful!\x1b[0m`);
1018
+
1019
+ // Pop stash if needed
1020
+ if (stashed) {
1021
+ log('Restoring stashed changes...');
1022
+ const popRes = await run('git', ['stash', 'pop']);
1023
+ if (!popRes.ok) {
1024
+ console.error('\x1b[31m⚠️ Conflict during stash pop. Manual intervention required.\x1b[0m');
1025
+ console.log('\x1b[33mPlease resolve conflicts and continue.\x1b[0m');
1026
+ } else {
1027
+ console.log('\x1b[32m✓ Stash restored.\x1b[0m');
1028
+ }
1029
+ }
1030
+
1031
+ global.lastRebaseTime = Date.now();
1032
+ } else {
1033
+ console.error(`\x1b[31m✗ REBASE CONFLICT DETECTED\x1b[0m`);
1034
+ console.error(rebaseRes.stdout);
1035
+
1036
+ // Abort rebase
1037
+ log('Aborting rebase...');
1038
+ await run('git', ['rebase', '--abort']);
1039
+
1040
+ // Restore stash if we stashed
1041
+ if (stashed) {
1042
+ await run('git', ['stash', 'pop']);
1043
+ }
1044
+
1045
+ console.log('\x1b[33mRebase aborted. Please manually rebase your branch.\x1b[0m');
1046
+ // Disable auto-rebase for this session to avoid loop?
1047
+ // Or just wait for next interval.
1048
+ }
1049
+
1050
+ console.log(`\x1b[32m✓ RESUMING AGENT OPERATION\x1b[0m\n`);
1051
+ return true;
1052
+
1053
+ } catch (err) {
1054
+ console.error(`Rebase error: ${err.message}`);
1055
+ // Try to recover state
1056
+ try { await run('git', ['rebase', '--abort']); } catch (e) {}
1057
+ return false;
1058
+ } finally {
1059
+ busy = false;
1060
+ }
1061
+ }
1062
+
956
1063
  /**
957
1064
  * Update infrastructure documentation
958
1065
  * @param {object} infraChanges - Infrastructure change details
@@ -1500,6 +1607,19 @@ console.log();
1500
1607
  log("watching…");
1501
1608
  }
1502
1609
 
1610
+ // Schedule auto-rebase if configured
1611
+ if (REBASE_INTERVAL_HOURS > 0) {
1612
+ log(`Auto-rebase scheduled every ${REBASE_INTERVAL_HOURS} hours (checking every 5m)`);
1613
+ // Initial check (in case we started overdue)
1614
+ // Don't await this so we don't block startup
1615
+ checkAndPerformRebase(repoRoot).catch(err => console.error(err));
1616
+
1617
+ // Periodic check
1618
+ setInterval(async () => {
1619
+ await checkAndPerformRebase(repoRoot);
1620
+ }, 5 * 60 * 1000); // Check every 5 minutes
1621
+ }
1622
+
1503
1623
  // ============================================================================
1504
1624
  // FILE WATCHER SETUP - Monitor for changes and trigger commits
1505
1625
  // ============================================================================
@@ -165,22 +165,42 @@ export class SessionCoordinator {
165
165
  // Show checking message
166
166
  console.log(`${CONFIG.colors.dim}🔍 Checking for DevOps Agent updates...${CONFIG.colors.reset}`);
167
167
 
168
- // Check npm for latest version
169
- const result = execSync('npm view s9n-devops-agent version', {
168
+ // Check npm for dist-tags
169
+ const distTags = JSON.parse(execSync('npm view s9n-devops-agent dist-tags --json', {
170
170
  encoding: 'utf8',
171
171
  stdio: ['ignore', 'pipe', 'ignore'],
172
172
  timeout: 5000
173
- }).trim();
173
+ }).trim());
174
+
175
+ const latest = distTags.latest;
176
+ const dev = distTags.dev;
174
177
 
175
178
  // Update last check time
176
179
  globalSettings.lastUpdateCheck = now;
177
180
  this.saveGlobalSettings(globalSettings);
178
181
 
179
- // Compare versions
180
- if (result && this.compareVersions(result, this.currentVersion) > 0) {
182
+ // Determine which version to compare against
183
+ // If current is a dev version, we check dev tag as well
184
+ const isDev = this.currentVersion.includes('dev') || this.currentVersion.includes('-');
185
+
186
+ let updateAvailable = false;
187
+ let targetVersion = latest;
188
+ let updateTag = 'latest';
189
+
190
+ if (isDev && dev && this.compareVersions(dev, this.currentVersion) > 0) {
191
+ updateAvailable = true;
192
+ targetVersion = dev;
193
+ updateTag = 'dev';
194
+ } else if (this.compareVersions(latest, this.currentVersion) > 0) {
195
+ updateAvailable = true;
196
+ targetVersion = latest;
197
+ updateTag = 'latest';
198
+ }
199
+
200
+ if (updateAvailable) {
181
201
  console.log(`\n${CONFIG.colors.yellow}▲ Update Available!${CONFIG.colors.reset}`);
182
202
  console.log(`${CONFIG.colors.dim}Current version: ${this.currentVersion}${CONFIG.colors.reset}`);
183
- console.log(`${CONFIG.colors.bright}Latest version: ${result}${CONFIG.colors.reset}`);
203
+ console.log(`${CONFIG.colors.bright}New version: ${targetVersion} (${updateTag})${CONFIG.colors.reset}`);
184
204
  console.log();
185
205
 
186
206
  // Ask if user wants to update now
@@ -199,7 +219,7 @@ export class SessionCoordinator {
199
219
  if (updateNow) {
200
220
  console.log(`\n${CONFIG.colors.blue}Updating s9n-devops-agent...${CONFIG.colors.reset}`);
201
221
  try {
202
- execSync('npm install -g s9n-devops-agent@latest', {
222
+ execSync(`npm install -g s9n-devops-agent@${updateTag}`, {
203
223
  stdio: 'inherit',
204
224
  cwd: process.cwd()
205
225
  });
@@ -207,10 +227,10 @@ export class SessionCoordinator {
207
227
  process.exit(0);
208
228
  } catch (err) {
209
229
  console.log(`\n${CONFIG.colors.red}✗ Update failed: ${err.message}${CONFIG.colors.reset}`);
210
- console.log(`${CONFIG.colors.dim}You can manually update with: npm install -g s9n-devops-agent@latest${CONFIG.colors.reset}`);
230
+ console.log(`${CONFIG.colors.dim}You can manually update with: npm install -g s9n-devops-agent@${updateTag}${CONFIG.colors.reset}`);
211
231
  }
212
232
  } else {
213
- console.log(`${CONFIG.colors.dim}You can update later with: npm install -g s9n-devops-agent@latest${CONFIG.colors.reset}`);
233
+ console.log(`${CONFIG.colors.dim}You can update later with: npm install -g s9n-devops-agent@${updateTag}${CONFIG.colors.reset}`);
214
234
  }
215
235
  console.log();
216
236
  } else {
@@ -224,19 +244,22 @@ export class SessionCoordinator {
224
244
  }
225
245
 
226
246
  /**
227
- * Compare semantic versions
247
+ * Compare semantic versions (robust to suffixes)
228
248
  * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
229
249
  */
230
250
  compareVersions(v1, v2) {
231
- const parts1 = v1.split('.').map(Number);
232
- const parts2 = v2.split('.').map(Number);
251
+ if (!v1 || !v2) return 0;
233
252
 
234
- for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
235
- const p1 = parts1[i] || 0;
236
- const p2 = parts2[i] || 0;
253
+ const normalize = v => v.replace(/^v/, '').split('.').map(p => parseInt(p, 10));
254
+ const p1 = normalize(v1);
255
+ const p2 = normalize(v2);
256
+
257
+ for (let i = 0; i < Math.max(p1.length, p2.length); i++) {
258
+ const n1 = isNaN(p1[i]) ? 0 : p1[i];
259
+ const n2 = isNaN(p2[i]) ? 0 : p2[i];
237
260
 
238
- if (p1 > p2) return 1;
239
- if (p1 < p2) return -1;
261
+ if (n1 > n2) return 1;
262
+ if (n1 < n2) return -1;
240
263
  }
241
264
 
242
265
  return 0;
@@ -427,13 +450,18 @@ export class SessionCoordinator {
427
450
  /**
428
451
  * Ensure project-specific version settings are configured
429
452
  */
430
- async ensureProjectSetup() {
453
+ async ensureProjectSetup(options = {}) {
431
454
  const projectSettings = this.loadProjectSettings();
432
455
 
433
456
  // Check if project setup is needed (version strategy)
434
- if (!projectSettings.versioningStrategy || !projectSettings.versioningStrategy.configured) {
435
- console.log(`\n${CONFIG.colors.yellow}First-time project setup for this repository!${CONFIG.colors.reset}`);
436
- console.log(`${CONFIG.colors.dim}Let's configure the versioning strategy for this project${CONFIG.colors.reset}`);
457
+ if (options.force || !projectSettings.versioningStrategy || !projectSettings.versioningStrategy.configured) {
458
+ console.log(`\n${CONFIG.colors.yellow}Project Versioning Setup${CONFIG.colors.reset}`);
459
+ if (options.force) {
460
+ console.log(`${CONFIG.colors.dim}Reconfiguring version strategy...${CONFIG.colors.reset}`);
461
+ } else {
462
+ console.log(`${CONFIG.colors.yellow}First-time project setup for this repository!${CONFIG.colors.reset}`);
463
+ console.log(`${CONFIG.colors.dim}Let's configure the versioning strategy for this project${CONFIG.colors.reset}`);
464
+ }
437
465
 
438
466
  const versionInfo = await this.promptForStartingVersion();
439
467
  projectSettings.versioningStrategy = {
@@ -797,6 +825,73 @@ export class SessionCoordinator {
797
825
  return config;
798
826
  }
799
827
 
828
+ /**
829
+ * Prompt for base branch (source)
830
+ */
831
+ async promptForBaseBranch() {
832
+ console.log(`\n${CONFIG.colors.yellow}═══ Base Branch Selection ═══${CONFIG.colors.reset}`);
833
+ console.log(`${CONFIG.colors.dim}Which branch should I use as the starting point for your work?${CONFIG.colors.reset}`);
834
+
835
+ // Get available branches
836
+ const branches = this.getAvailableBranches();
837
+ // Prioritize main/develop/master
838
+ const priorityBranches = ['main', 'master', 'develop', 'development'];
839
+
840
+ const sortedBranches = branches.sort((a, b) => {
841
+ const aP = priorityBranches.indexOf(a);
842
+ const bP = priorityBranches.indexOf(b);
843
+ if (aP !== -1 && bP !== -1) return aP - bP;
844
+ if (aP !== -1) return -1;
845
+ if (bP !== -1) return 1;
846
+ return a.localeCompare(b);
847
+ });
848
+
849
+ const uniqueBranches = [...new Set(sortedBranches)].slice(0, 10);
850
+
851
+ console.log();
852
+ uniqueBranches.forEach((branch, index) => {
853
+ const isPriority = priorityBranches.includes(branch);
854
+ const marker = isPriority ? ` ${CONFIG.colors.green}⭐${CONFIG.colors.reset}` : '';
855
+ console.log(` ${index + 1}) ${branch}${marker}`);
856
+ });
857
+ console.log(` 0) Enter a different branch name`);
858
+ console.log(` Hit Enter for default (HEAD)`);
859
+
860
+ const rl = readline.createInterface({
861
+ input: process.stdin,
862
+ output: process.stdout
863
+ });
864
+
865
+ return new Promise((resolve) => {
866
+ rl.question(`\nSelect base branch (1-${uniqueBranches.length}, 0, or Enter): `, (answer) => {
867
+ rl.close();
868
+ const choice = answer.trim();
869
+
870
+ if (choice === '') {
871
+ resolve('HEAD');
872
+ return;
873
+ }
874
+
875
+ const num = parseInt(choice);
876
+
877
+ if (num === 0) {
878
+ const rl2 = readline.createInterface({
879
+ input: process.stdin,
880
+ output: process.stdout
881
+ });
882
+ rl2.question('Enter custom branch name: ', (custom) => {
883
+ rl2.close();
884
+ resolve(custom.trim() || 'HEAD');
885
+ });
886
+ } else if (num >= 1 && num <= uniqueBranches.length) {
887
+ resolve(uniqueBranches[num - 1]);
888
+ } else {
889
+ resolve('HEAD');
890
+ }
891
+ });
892
+ });
893
+ }
894
+
800
895
  /**
801
896
  * Prompt for auto-merge configuration
802
897
  */
@@ -1172,6 +1267,12 @@ export class SessionCoordinator {
1172
1267
  // Ask for auto-merge configuration
1173
1268
  const mergeConfig = await this.promptForMergeConfig();
1174
1269
 
1270
+ // Ask for base branch (where to start work from)
1271
+ const baseBranch = await this.promptForBaseBranch();
1272
+
1273
+ // Ask for auto-rebase interval
1274
+ const rebaseInterval = await this.promptForRebaseInterval();
1275
+
1175
1276
  // Check for Docker configuration and ask about restart preference
1176
1277
  let dockerConfig = null;
1177
1278
 
@@ -1321,9 +1422,15 @@ export class SessionCoordinator {
1321
1422
 
1322
1423
  // Create worktree
1323
1424
  console.log(`\n${CONFIG.colors.yellow}Creating worktree...${CONFIG.colors.reset}`);
1324
- execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, { stdio: 'pipe' });
1425
+ const baseRef = baseBranch || 'HEAD';
1426
+ console.log(`${CONFIG.colors.dim}Branching off: ${baseRef}${CONFIG.colors.reset}`);
1427
+
1428
+ execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseRef}`, { stdio: 'pipe' });
1325
1429
  console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Worktree created at: ${worktreePath}`);
1326
1430
 
1431
+ // Store base branch in session data for rebase logic
1432
+ const sessionBaseBranch = baseRef === 'HEAD' ? await this.resolveHeadBranch() : baseRef;
1433
+
1327
1434
  // If we're in a submodule, set up the correct remote for the worktree
1328
1435
  if (isSubmodule && parentRemote) {
1329
1436
  console.log(`${CONFIG.colors.yellow}Configuring worktree to use parent repository remote...${CONFIG.colors.reset}`);
@@ -1345,11 +1452,13 @@ export class SessionCoordinator {
1345
1452
  task,
1346
1453
  worktreePath,
1347
1454
  branchName,
1455
+ baseBranch: sessionBaseBranch,
1348
1456
  created: new Date().toISOString(),
1349
1457
  status: 'active',
1350
1458
  pid: process.pid,
1351
1459
  developerInitials: devInitials,
1352
1460
  mergeConfig: mergeConfig,
1461
+ rebaseInterval: rebaseInterval,
1353
1462
  dockerConfig: dockerConfig
1354
1463
  };
1355
1464
 
@@ -1387,6 +1496,46 @@ export class SessionCoordinator {
1387
1496
  }
1388
1497
  }
1389
1498
 
1499
+ /**
1500
+ * Prompt for auto-rebase interval
1501
+ */
1502
+ async promptForRebaseInterval() {
1503
+ console.log(`\n${CONFIG.colors.yellow}═══ Auto-Rebase Configuration ═══${CONFIG.colors.reset}`);
1504
+ console.log(`${CONFIG.colors.dim}I can automatically pull updates from the base branch to keep you up-to-date.${CONFIG.colors.reset}`);
1505
+ console.log(`${CONFIG.colors.dim}This helps prevent conflicts later by rebasing your work periodically.${CONFIG.colors.reset}`);
1506
+
1507
+ const rl = readline.createInterface({
1508
+ input: process.stdin,
1509
+ output: process.stdout
1510
+ });
1511
+
1512
+ return new Promise((resolve) => {
1513
+ rl.question(`\nHow often should I rebase? (in hours, 0 to disable) [0]: `, (answer) => {
1514
+ rl.close();
1515
+ const hours = parseFloat(answer.trim());
1516
+ if (isNaN(hours) || hours <= 0) {
1517
+ console.log(`${CONFIG.colors.dim}Auto-rebase disabled. I'll let you manage updates manually.${CONFIG.colors.reset}`);
1518
+ resolve(0);
1519
+ } else {
1520
+ console.log(`${CONFIG.colors.green}✓ I'll check for updates and rebase every ${hours} hour(s).${CONFIG.colors.reset}`);
1521
+ resolve(hours);
1522
+ }
1523
+ });
1524
+ });
1525
+ }
1526
+
1527
+ /**
1528
+ * Resolve HEAD to actual branch name
1529
+ */
1530
+ async resolveHeadBranch() {
1531
+ try {
1532
+ const head = execSync('git rev-parse --abbrev-ref HEAD', { cwd: this.repoRoot, encoding: 'utf8' }).trim();
1533
+ return head;
1534
+ } catch (e) {
1535
+ return 'main';
1536
+ }
1537
+ }
1538
+
1390
1539
  /**
1391
1540
  * Generate instructions for the coding agent
1392
1541
  */
@@ -1794,7 +1943,11 @@ The DevOps agent is monitoring this worktree for changes.
1794
1943
  AC_DATE_STYLE: process.env.AC_DATE_STYLE || 'dash', // Preserve date style
1795
1944
  // Apply version configuration if set
1796
1945
  ...(projectSettings.versioningStrategy?.prefix && { AC_VERSION_PREFIX: projectSettings.versioningStrategy.prefix }),
1797
- ...(projectSettings.versioningStrategy?.startMinor && { AC_VERSION_START_MINOR: projectSettings.versioningStrategy.startMinor.toString() })
1946
+ ...(projectSettings.versioningStrategy?.startMinor && { AC_VERSION_START_MINOR: projectSettings.versioningStrategy.startMinor.toString() }),
1947
+
1948
+ // Rebase configuration
1949
+ AC_REBASE_INTERVAL: (sessionData.rebaseInterval || 0).toString(),
1950
+ AC_BASE_BRANCH: sessionData.baseBranch || 'HEAD' // We need to pass the base branch for rebasing
1798
1951
  };
1799
1952
 
1800
1953
  const agentScript = path.join(__dirname, 'cs-devops-agent-worker.js');
@@ -28,6 +28,7 @@ import { execSync } from 'child_process';
28
28
  import { fileURLToPath } from 'url';
29
29
  import { dirname } from 'path';
30
30
  import { credentialsManager } from './credentials-manager.js';
31
+ import { SessionCoordinator } from './session-coordinator.js';
31
32
  import {
32
33
  colors,
33
34
  status,
@@ -149,25 +150,9 @@ This structure is compatible with the DevOps Agent's automation tools.
149
150
  return missingFolders;
150
151
  }
151
152
 
152
- function checkContractsExist(projectRoot) {
153
- // Search recursively for contract folders
153
+ async function checkContractsExist(projectRoot) {
154
+ // Search recursively for contract folders and files
154
155
  try {
155
- // Find all directories named 'House_Rules_Contracts' or 'contracts'
156
- // Ignoring node_modules and .git
157
- const findCommand = `find "${projectRoot}" -type d \\( -name "House_Rules_Contracts" -o -name "contracts" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/local_deploy/*"`;
158
-
159
- const output = execSync(findCommand, { encoding: 'utf8' }).trim();
160
- const locations = output.split('\n').filter(Boolean);
161
-
162
- let contractsDir = null;
163
- if (locations.length > 0) {
164
- // Prefer House_Rules_Contracts if available
165
- contractsDir = locations.find(l => l.endsWith('House_Rules_Contracts')) || locations[0];
166
- log.info(`Found contracts directory at: ${contractsDir}`);
167
- }
168
-
169
- if (!contractsDir) return false;
170
-
171
156
  const requiredContracts = [
172
157
  'FEATURES_CONTRACT.md',
173
158
  'API_CONTRACT.md',
@@ -176,21 +161,122 @@ function checkContractsExist(projectRoot) {
176
161
  'THIRD_PARTY_INTEGRATIONS.md',
177
162
  'INFRA_CONTRACT.md'
178
163
  ];
164
+
165
+ // Map to hold found files for each type
166
+ const contractMap = {};
167
+ requiredContracts.forEach(c => contractMap[c] = []);
168
+
169
+ // Find all files that look like contracts
170
+ // We look for files containing "CONTRACT" in the name, excluding typical ignores
171
+ const findCommand = `find "${projectRoot}" -type f \\( -name "*CONTRACT*.md" -o -name "*CONTRACT*.json" \\) -not -path "*/node_modules/*" -not -path "*/.git/*" -not -path "*/local_deploy/*"`;
172
+
173
+ let files = [];
174
+ try {
175
+ const output = execSync(findCommand, { encoding: 'utf8' }).trim();
176
+ files = output.split('\n').filter(Boolean);
177
+ } catch (e) {
178
+ // find might fail if no matches or other issues, just treat as empty
179
+ }
180
+
181
+ // Categorize found files
182
+ for (const file of files) {
183
+ const basename = path.basename(file).toUpperCase();
184
+
185
+ // Skip files in the target directory itself (House_Rules_Contracts) to avoid self-merging if we run this multiple times
186
+ // actually we SHOULD include them to see if we have them, but valid if we are merging duplicates from elsewhere
187
+
188
+ let matched = false;
189
+
190
+ if (basename.includes('FEATURE')) contractMap['FEATURES_CONTRACT.md'].push(file);
191
+ else if (basename.includes('API')) contractMap['API_CONTRACT.md'].push(file);
192
+ else if (basename.includes('DATABASE') || basename.includes('SCHEMA')) contractMap['DATABASE_SCHEMA_CONTRACT.md'].push(file);
193
+ else if (basename.includes('SQL')) contractMap['SQL_CONTRACT.json'].push(file);
194
+ else if (basename.includes('INFRA')) contractMap['INFRA_CONTRACT.md'].push(file);
195
+ else if (basename.includes('THIRD') || basename.includes('INTEGRATION')) contractMap['THIRD_PARTY_INTEGRATIONS.md'].push(file);
196
+ else {
197
+ // Fallback or ignore
198
+ }
199
+ }
200
+
201
+ const targetDir = path.join(projectRoot, 'House_Rules_Contracts');
202
+ let hasChanges = false;
203
+
204
+ // Process each contract type
205
+ for (const [type, foundFiles] of Object.entries(contractMap)) {
206
+ // Filter out unique paths (resolve them)
207
+ const uniqueFiles = [...new Set(foundFiles.map(f => path.resolve(f)))];
208
+
209
+ if (uniqueFiles.length > 1) {
210
+ console.log();
211
+ log.info(`Found multiple files for contract type: ${colors.cyan}${type}${colors.reset}`);
212
+ uniqueFiles.forEach(f => console.log(` - ${path.relative(projectRoot, f)}`));
213
+
214
+ const shouldMerge = await confirm(`Do you want to merge these into House_Rules_Contracts/${type}?`, true);
215
+
216
+ if (shouldMerge) {
217
+ ensureDirectoryExists(targetDir);
218
+ const targetPath = path.join(targetDir, type);
219
+
220
+ let mergedContent = '';
221
+ // Handle JSON vs MD
222
+ if (type.endsWith('.json')) {
223
+ // For JSON, we try to merge arrays/objects or just list them
224
+ const mergedJson = [];
225
+ for (const file of uniqueFiles) {
226
+ try {
227
+ const content = JSON.parse(fs.readFileSync(file, 'utf8'));
228
+ mergedJson.push({ source: path.relative(projectRoot, file), content });
229
+ } catch (e) {
230
+ log.warn(`Skipping invalid JSON in ${path.basename(file)}`);
231
+ }
232
+ }
233
+ mergedContent = JSON.stringify(mergedJson, null, 2);
234
+ } else {
235
+ // Markdown
236
+ mergedContent = `# Merged ${type}\n\nGenerated on ${new Date().toISOString()}\n\n`;
237
+ for (const file of uniqueFiles) {
238
+ const content = fs.readFileSync(file, 'utf8');
239
+ mergedContent += `\n<!-- SOURCE: ${path.relative(projectRoot, file)} -->\n`;
240
+ mergedContent += `## Source: ${path.basename(file)}\n(Path: ${path.relative(projectRoot, file)})\n\n`;
241
+ mergedContent += `${content}\n\n---\n`;
242
+ }
243
+ }
244
+
245
+ fs.writeFileSync(targetPath, mergedContent);
246
+ log.success(`Merged contracts into ${path.relative(projectRoot, targetPath)}`);
247
+ hasChanges = true;
248
+ }
249
+ } else if (uniqueFiles.length === 1) {
250
+ // If single file exists but is NOT in House_Rules_Contracts, ask to move/copy
251
+ const file = uniqueFiles[0];
252
+ const targetPath = path.join(targetDir, type);
253
+
254
+ if (file !== path.resolve(targetPath)) {
255
+ console.log();
256
+ log.info(`Found ${type} at: ${path.relative(projectRoot, file)}`);
257
+ const shouldCopy = await confirm(`Copy this to central House_Rules_Contracts/${type}?`, true);
258
+ if (shouldCopy) {
259
+ ensureDirectoryExists(targetDir);
260
+ fs.copyFileSync(file, targetPath);
261
+ log.success(`Copied to ${path.relative(projectRoot, targetPath)}`);
262
+ hasChanges = true;
263
+ }
264
+ }
265
+ }
266
+ }
267
+
268
+ // Final check: Do we have all required contracts in the target directory?
269
+ const missing = requiredContracts.filter(file => !fs.existsSync(path.join(targetDir, file)));
179
270
 
180
- // Check if we have multiple similar contracts that might need merging
181
- const files = fs.readdirSync(contractsDir);
182
- const potentialDuplicates = files.filter(f =>
183
- (f.includes('FEATURE') && f !== 'FEATURES_CONTRACT.md') ||
184
- (f.includes('API') && f !== 'API_CONTRACT.md')
185
- );
186
-
187
- if (potentialDuplicates.length > 0) {
188
- log.info(`Found potential split contract files in ${contractsDir}:`);
189
- potentialDuplicates.forEach(f => console.log(` - ${f}`));
190
- console.log('You may want to merge these into single contract files per type.');
271
+ if (missing.length === 0) {
272
+ if (hasChanges) log.success('Contract files consolidated successfully.');
273
+ return true;
191
274
  }
192
275
 
193
- return requiredContracts.every(file => fs.existsSync(path.join(contractsDir, file)));
276
+ // If we are missing some, but have others, we still return false so generateContracts can run for the missing ones?
277
+ // Or we return false and generateContracts will run.
278
+ return false;
279
+
194
280
  } catch (error) {
195
281
  log.warn(`Error searching for contracts: ${error.message}`);
196
282
  return false;
@@ -866,30 +952,50 @@ async function setupEnvFile(projectRoot) {
866
952
  log.info('Creating .env file');
867
953
  }
868
954
 
869
- // Check for OPENAI_API_KEY
955
+ // Check if OPENAI_API_KEY is already present in memory (from credentials.json)
956
+ const existingKey = credentialsManager.getGroqApiKey();
957
+
958
+ // Check for OPENAI_API_KEY in .env content
870
959
  if (!envContent.includes('OPENAI_API_KEY=')) {
871
- console.log();
872
- explain(`
873
- ${colors.bright}Groq API Key Setup${colors.reset}
874
- The contract automation features use Groq LLM (via OpenAI compatibility).
875
- You can enter your API key now, or set it later in the .env file.
876
- `);
877
-
878
- const apiKey = await prompt('Enter Groq API Key (leave empty to skip)');
879
-
880
- if (apiKey) {
960
+ if (existingKey) {
961
+ log.info('Found existing Groq API Key in credentials store.');
881
962
  const newLine = envContent.endsWith('\n') || envContent === '' ? '' : '\n';
882
- envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${apiKey}\n`;
963
+ envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${existingKey}\n`;
883
964
  fs.writeFileSync(envPath, envContent);
884
- log.success('Added OPENAI_API_KEY to .env');
965
+ log.success('Restored OPENAI_API_KEY to .env');
885
966
  } else {
886
- log.warn('Skipped Groq API Key. Contract automation features may not work.');
887
- if (!fs.existsSync(envPath)) {
888
- fs.writeFileSync(envPath, '# Environment Variables\n');
967
+ console.log();
968
+ explain(`
969
+ ${colors.bright}Groq API Key Setup${colors.reset}
970
+ The contract automation features use Groq LLM (via OpenAI compatibility).
971
+ You can enter your API key now, or set it later in the .env file.
972
+ `);
973
+
974
+ const apiKey = await prompt('Enter Groq API Key (leave empty to skip)');
975
+
976
+ if (apiKey) {
977
+ const newLine = envContent.endsWith('\n') || envContent === '' ? '' : '\n';
978
+ envContent += `${newLine}# Groq API Key for Contract Automation\nOPENAI_API_KEY=${apiKey}\n`;
979
+ fs.writeFileSync(envPath, envContent);
980
+
981
+ // Also save to credentials manager for persistence across updates
982
+ credentialsManager.setGroqApiKey(apiKey);
983
+
984
+ log.success('Added OPENAI_API_KEY to .env');
985
+ } else {
986
+ log.warn('Skipped Groq API Key. Contract automation features may not work.');
987
+ if (!fs.existsSync(envPath)) {
988
+ fs.writeFileSync(envPath, '# Environment Variables\n');
989
+ }
889
990
  }
890
991
  }
891
992
  } else {
892
993
  log.info('OPENAI_API_KEY is already configured in .env');
994
+ // Ensure it's backed up in credentials manager if it exists in .env
995
+ const match = envContent.match(/OPENAI_API_KEY=(.+)/);
996
+ if (match && match[1] && !existingKey) {
997
+ credentialsManager.setGroqApiKey(match[1].trim());
998
+ }
893
999
  }
894
1000
  }
895
1001
 
@@ -1125,7 +1231,7 @@ ${colors.bright}Security:${colors.reset} Stored locally in ${colors.yellow}local
1125
1231
  }
1126
1232
 
1127
1233
  // Check for contracts
1128
- if (!checkContractsExist(projectRoot)) {
1234
+ if (!(await checkContractsExist(projectRoot))) {
1129
1235
  log.header();
1130
1236
  log.title('📜 Contract Files Missing');
1131
1237
 
@@ -1162,6 +1268,34 @@ We can scan your codebase and generate them now.
1162
1268
  log.success('Created .env file');
1163
1269
  }
1164
1270
  }
1271
+
1272
+ // Initialize SessionCoordinator for versioning setup
1273
+ const coordinator = new SessionCoordinator();
1274
+
1275
+ // Check/Setup versioning strategy
1276
+ if (!skipPrompts) {
1277
+ const settings = coordinator.loadProjectSettings();
1278
+ if (!settings.versioningStrategy?.configured) {
1279
+ log.header();
1280
+ log.title('📅 Project Versioning Strategy');
1281
+ await coordinator.ensureProjectSetup();
1282
+ } else {
1283
+ // Optional reconfigure
1284
+ log.info('Versioning strategy is already configured.');
1285
+ const reconfigure = await confirm('Do you want to reconfigure versioning?', false);
1286
+ if (reconfigure) {
1287
+ await coordinator.ensureProjectSetup({ force: true });
1288
+ }
1289
+ }
1290
+ } else {
1291
+ // In non-interactive mode, we only ensure if missing (and hope it doesn't block or has defaults)
1292
+ // Actually promptForStartingVersion is interactive-only, so we skip if missing in non-interactive
1293
+ // or we could force defaults. For now, we skip to avoid hanging.
1294
+ const settings = coordinator.loadProjectSettings();
1295
+ if (!settings.versioningStrategy?.configured) {
1296
+ log.warn('Skipping versioning setup (interactive-only). Run setup without --yes to configure.');
1297
+ }
1298
+ }
1165
1299
 
1166
1300
  // Clean up DevOpsAgent files to avoid duplicates
1167
1301
  cleanupDevOpsAgentFiles(projectRoot, agentName);