s9n-devops-agent 2.0.18-dev.3 β†’ 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "s9n-devops-agent",
3
- "version": "2.0.18-dev.3",
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",
@@ -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
  // ============================================================================
@@ -830,7 +830,7 @@ export class SessionCoordinator {
830
830
  */
831
831
  async promptForBaseBranch() {
832
832
  console.log(`\n${CONFIG.colors.yellow}═══ Base Branch Selection ═══${CONFIG.colors.reset}`);
833
- console.log(`${CONFIG.colors.dim}Select the branch you want to start your work FROM.${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
834
 
835
835
  // Get available branches
836
836
  const branches = this.getAvailableBranches();
@@ -1270,6 +1270,9 @@ export class SessionCoordinator {
1270
1270
  // Ask for base branch (where to start work from)
1271
1271
  const baseBranch = await this.promptForBaseBranch();
1272
1272
 
1273
+ // Ask for auto-rebase interval
1274
+ const rebaseInterval = await this.promptForRebaseInterval();
1275
+
1273
1276
  // Check for Docker configuration and ask about restart preference
1274
1277
  let dockerConfig = null;
1275
1278
 
@@ -1425,6 +1428,9 @@ export class SessionCoordinator {
1425
1428
  execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseRef}`, { stdio: 'pipe' });
1426
1429
  console.log(`${CONFIG.colors.green}βœ“${CONFIG.colors.reset} Worktree created at: ${worktreePath}`);
1427
1430
 
1431
+ // Store base branch in session data for rebase logic
1432
+ const sessionBaseBranch = baseRef === 'HEAD' ? await this.resolveHeadBranch() : baseRef;
1433
+
1428
1434
  // If we're in a submodule, set up the correct remote for the worktree
1429
1435
  if (isSubmodule && parentRemote) {
1430
1436
  console.log(`${CONFIG.colors.yellow}Configuring worktree to use parent repository remote...${CONFIG.colors.reset}`);
@@ -1446,11 +1452,13 @@ export class SessionCoordinator {
1446
1452
  task,
1447
1453
  worktreePath,
1448
1454
  branchName,
1455
+ baseBranch: sessionBaseBranch,
1449
1456
  created: new Date().toISOString(),
1450
1457
  status: 'active',
1451
1458
  pid: process.pid,
1452
1459
  developerInitials: devInitials,
1453
1460
  mergeConfig: mergeConfig,
1461
+ rebaseInterval: rebaseInterval,
1454
1462
  dockerConfig: dockerConfig
1455
1463
  };
1456
1464
 
@@ -1488,6 +1496,46 @@ export class SessionCoordinator {
1488
1496
  }
1489
1497
  }
1490
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
+
1491
1539
  /**
1492
1540
  * Generate instructions for the coding agent
1493
1541
  */
@@ -1895,7 +1943,11 @@ The DevOps agent is monitoring this worktree for changes.
1895
1943
  AC_DATE_STYLE: process.env.AC_DATE_STYLE || 'dash', // Preserve date style
1896
1944
  // Apply version configuration if set
1897
1945
  ...(projectSettings.versioningStrategy?.prefix && { AC_VERSION_PREFIX: projectSettings.versioningStrategy.prefix }),
1898
- ...(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
1899
1951
  };
1900
1952
 
1901
1953
  const agentScript = path.join(__dirname, 'cs-devops-agent-worker.js');