s9n-devops-agent 2.0.18-dev.1 → 2.0.18-dev.11

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.
@@ -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);
@@ -287,7 +291,16 @@ async function stagedCount() {
287
291
  return r.ok ? r.stdout.split("\n").filter(Boolean).length : 0;
288
292
  }
289
293
  async function unstageIfStaged(file) {
290
- await run("git", ["restore", "--staged", file]);
294
+ try {
295
+ // Check if file is actually staged first to avoid errors
296
+ // Use diff --cached to check without erroring on untracked files
297
+ const { ok, stdout } = await run("git", ["diff", "--name-only", "--cached", file]);
298
+ if (ok && stdout.trim().length > 0) {
299
+ await run("git", ["restore", "--staged", file]);
300
+ }
301
+ } catch (e) {
302
+ // Ignore errors
303
+ }
291
304
  }
292
305
  async function defaultRemote() {
293
306
  const r = await run("git", ["remote"]);
@@ -824,17 +837,20 @@ const REQUIRE_AI_COMMIT = (process.env.AC_AI_COMMIT || "false").toLowerCase() ==
824
837
 
825
838
  if (PUSH) {
826
839
  const ok = await pushBranch(BRANCH);
827
- log(`push ${ok ? "ok" : "failed"}`);
840
+ // Ensure this log goes to the right panel
841
+ log(`[push] pushing branch ${BRANCH} result: ${ok ? "ok" : "failed"}`);
842
+
828
843
  if (ok) {
829
844
  if (CLEAR_MSG_WHEN === "push") clearMsgFile(msgPath);
830
845
 
831
- console.log(`\x1b[32m✓ PUSHED:\x1b[0m ${BRANCH} -> remote`);
832
- console.log(""); // Empty line for spacing
846
+ // This log was going to stdout directly, bypassing TUI buffers
847
+ // Redirect to TUI log
848
+ log(`✓ PUSHED: ${BRANCH} -> remote`);
833
849
 
834
850
  // Handle Docker restart if configured
835
851
  await handleDockerRestart();
836
852
  } else {
837
- console.log(`\x1b[31m✗ PUSH FAILED:\x1b[0m Check logs for details`);
853
+ console.error(`✗ PUSH FAILED: Check logs for details`);
838
854
  }
839
855
  }
840
856
  } finally {
@@ -953,6 +969,109 @@ function detectInfrastructureChanges(changedFiles) {
953
969
  return detected;
954
970
  }
955
971
 
972
+ // ============================================================================
973
+ // AUTO-REBASE FUNCTIONALITY
974
+ // ============================================================================
975
+
976
+ /**
977
+ * Check if it's time to rebase and perform the operation
978
+ * @param {string} repoRoot - Repository root
979
+ * @returns {Promise<boolean>} - True if rebase occurred
980
+ */
981
+ async function checkAndPerformRebase(repoRoot) {
982
+ if (REBASE_INTERVAL_HOURS <= 0) return false;
983
+ if (BASE_BRANCH === 'HEAD') return false; // Can't rebase from HEAD (relative)
984
+
985
+ // Calculate interval in ms
986
+ const intervalMs = REBASE_INTERVAL_HOURS * 60 * 60 * 1000;
987
+ const now = Date.now();
988
+
989
+ // Check if enough time has passed
990
+ if (!global.lastRebaseTime) global.lastRebaseTime = now; // Initialize on first run
991
+ if (now - global.lastRebaseTime < intervalMs) return false;
992
+
993
+ // Don't rebase if we are busy
994
+ if (busy) return false;
995
+
996
+ try {
997
+ busy = true;
998
+
999
+ console.log('\n' + '━'.repeat(60));
1000
+ console.log(`\x1b[33m⚠️ AUTO-REBASE IN PROGRESS - PAUSING AGENT...\x1b[0m`);
1001
+ console.log(`Interval: ${REBASE_INTERVAL_HOURS} hours reached.`);
1002
+ console.log(`Base: ${BASE_BRANCH}`);
1003
+ console.log('━'.repeat(60) + '\n');
1004
+
1005
+ // Check for uncommitted changes
1006
+ const dirty = await hasUncommittedChanges();
1007
+ let stashed = false;
1008
+
1009
+ if (dirty) {
1010
+ log('Stashing uncommitted changes before rebase...');
1011
+ const stashRes = await run('git', ['stash', 'push', '-m', `Auto-stash before rebase ${new Date().toISOString()}`]);
1012
+ if (stashRes.ok) stashed = true;
1013
+ else {
1014
+ console.error('\x1b[31m✗ Failed to stash changes. Aborting rebase.\x1b[0m');
1015
+ busy = false;
1016
+ return false;
1017
+ }
1018
+ }
1019
+
1020
+ // Fetch latest
1021
+ log(`Fetching latest changes from origin...`);
1022
+ await run('git', ['fetch', 'origin', BASE_BRANCH]);
1023
+
1024
+ // Rebase
1025
+ log(`Rebasing onto origin/${BASE_BRANCH}...`);
1026
+ const rebaseRes = await run('git', ['pull', '--rebase', 'origin', BASE_BRANCH]);
1027
+
1028
+ if (rebaseRes.ok) {
1029
+ console.log(`\x1b[32m✓ Rebase successful!\x1b[0m`);
1030
+
1031
+ // Pop stash if needed
1032
+ if (stashed) {
1033
+ log('Restoring stashed changes...');
1034
+ const popRes = await run('git', ['stash', 'pop']);
1035
+ if (!popRes.ok) {
1036
+ console.error('\x1b[31m⚠️ Conflict during stash pop. Manual intervention required.\x1b[0m');
1037
+ console.log('\x1b[33mPlease resolve conflicts and continue.\x1b[0m');
1038
+ } else {
1039
+ console.log('\x1b[32m✓ Stash restored.\x1b[0m');
1040
+ }
1041
+ }
1042
+
1043
+ global.lastRebaseTime = Date.now();
1044
+ } else {
1045
+ console.error(`\x1b[31m✗ REBASE CONFLICT DETECTED\x1b[0m`);
1046
+ console.error(rebaseRes.stdout);
1047
+
1048
+ // Abort rebase
1049
+ log('Aborting rebase...');
1050
+ await run('git', ['rebase', '--abort']);
1051
+
1052
+ // Restore stash if we stashed
1053
+ if (stashed) {
1054
+ await run('git', ['stash', 'pop']);
1055
+ }
1056
+
1057
+ console.log('\x1b[33mRebase aborted. Please manually rebase your branch.\x1b[0m');
1058
+ // Disable auto-rebase for this session to avoid loop?
1059
+ // Or just wait for next interval.
1060
+ }
1061
+
1062
+ console.log(`\x1b[32m✓ RESUMING AGENT OPERATION\x1b[0m\n`);
1063
+ return true;
1064
+
1065
+ } catch (err) {
1066
+ console.error(`Rebase error: ${err.message}`);
1067
+ // Try to recover state
1068
+ try { await run('git', ['rebase', '--abort']); } catch (e) {}
1069
+ return false;
1070
+ } finally {
1071
+ busy = false;
1072
+ }
1073
+ }
1074
+
956
1075
  /**
957
1076
  * Update infrastructure documentation
958
1077
  * @param {object} infraChanges - Infrastructure change details
@@ -1418,8 +1537,8 @@ function saveProjectSettings(settings, settingsPath) {
1418
1537
  // Display copyright and license information immediately
1419
1538
  console.log("\n" + "=".repeat(70));
1420
1539
  console.log(" CS_DevOpsAgent - Intelligent Git Automation System");
1421
- console.log(" Version 1.4.8 | Build 20251008.1");
1422
- console.log(" \n Copyright (c) 2024 SecondBrain Labs");
1540
+ console.log(" Version 2.0.18-dev.11 | Build 20260107");
1541
+ console.log(" Copyright (c) 2026 SeKondBrain AI Labs Limited");
1423
1542
  console.log(" Author: Sachin Dev Duggal");
1424
1543
  console.log(" \n Licensed under the MIT License");
1425
1544
  console.log(" This software is provided 'as-is' without any warranty.");
@@ -1500,6 +1619,19 @@ console.log();
1500
1619
  log("watching…");
1501
1620
  }
1502
1621
 
1622
+ // Schedule auto-rebase if configured
1623
+ if (REBASE_INTERVAL_HOURS > 0) {
1624
+ log(`Auto-rebase scheduled every ${REBASE_INTERVAL_HOURS} hours (checking every 5m)`);
1625
+ // Initial check (in case we started overdue)
1626
+ // Don't await this so we don't block startup
1627
+ checkAndPerformRebase(repoRoot).catch(err => console.error(err));
1628
+
1629
+ // Periodic check
1630
+ setInterval(async () => {
1631
+ await checkAndPerformRebase(repoRoot);
1632
+ }, 5 * 60 * 1000); // Check every 5 minutes
1633
+ }
1634
+
1503
1635
  // ============================================================================
1504
1636
  // FILE WATCHER SETUP - Monitor for changes and trigger commits
1505
1637
  // ============================================================================
@@ -1625,25 +1757,30 @@ console.log();
1625
1757
  ],
1626
1758
  })
1627
1759
  .on("all", async (evt, p) => {
1760
+ // Define 'now' at the very top of the callback to avoid ReferenceError
1761
+ const now = Date.now();
1762
+
1628
1763
  // Check if file is in local_deploy or .file-coordination (internal agent files)
1629
1764
  if (p.includes('local_deploy/') || p.includes('.file-coordination/')) {
1630
1765
  return;
1631
1766
  }
1632
1767
 
1768
+ const isMsg = samePath(p, relMsg);
1769
+
1633
1770
  // 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.
1771
+ // IMPORTANT: Do NOT ignore the message file, even if it's in .gitignore
1772
+ if (!isMsg) {
1773
+ try {
1774
+ // git check-ignore returns 0 if ignored, 1 if not ignored
1775
+ // We use quiet mode (-q) and ignore stdio
1776
+ execSync(`git check-ignore -q "${p}"`, { stdio: 'ignore' });
1777
+ // If we get here, exit code was 0, so file IS ignored
1778
+ if (DEBUG) console.log(`[debug] Ignoring gitignored file: ${p}`);
1779
+ return;
1780
+ } catch (e) {
1781
+ // Exit code 1 means NOT ignored (or other error). Proceed.
1782
+ }
1643
1783
  }
1644
-
1645
- const now = Date.now();
1646
- const isMsg = samePath(p, relMsg);
1647
1784
 
1648
1785
  // Track timing of non-message changes
1649
1786
  if (!isMsg) {
@@ -1731,12 +1868,161 @@ console.log();
1731
1868
  const rl = readline.createInterface({
1732
1869
  input: input,
1733
1870
  output: output,
1734
- prompt: '\\x1b[36m[agent] >\\x1b[0m ',
1871
+ prompt: '', // We'll manage the prompt manually
1735
1872
  terminal: true
1736
1873
  });
1737
-
1874
+
1875
+ // TUI STATE
1876
+ const TUI = {
1877
+ leftLogs: [],
1878
+ rightLogs: [],
1879
+ maxLogs: 200,
1880
+ status: 'ACTIVE',
1881
+ inputBuffer: '',
1882
+ sessionId: sessionId,
1883
+ branch: await currentBranch(),
1884
+ inputMode: 'command' // 'command' or 'prompt'
1885
+ };
1886
+
1887
+ // Helper to strip ANSI codes
1888
+ const stripAnsi = (str) => str.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '');
1889
+
1890
+ // Override log output to split into two panels
1891
+ // Right Panel: [cs-devops-agent], [debug], [cmd], [err] (Background Activity)
1892
+ // Left Panel: Interactive output (status, help, etc)
1893
+ const originalConsoleLog = console.log;
1894
+ const originalConsoleError = console.error;
1895
+
1896
+ const addToBuffers = (msg, isError = false) => {
1897
+ const cleanMsg = stripAnsi(msg);
1898
+ const isBackground = cleanMsg.startsWith('[cs-devops-agent]') ||
1899
+ cleanMsg.startsWith('[debug]') ||
1900
+ cleanMsg.startsWith('[cmd]') ||
1901
+ cleanMsg.startsWith('[err]') ||
1902
+ cleanMsg.startsWith('ignore') ||
1903
+ cleanMsg.startsWith('watcher:');
1904
+
1905
+ const targetBuffer = isBackground ? TUI.rightLogs : TUI.leftLogs;
1906
+
1907
+ // Add timestamp for background logs only
1908
+ let line = msg;
1909
+ if (isBackground) {
1910
+ const time = new Date().toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute:'2-digit', second:'2-digit' });
1911
+ line = `\x1b[2m[${time}]\x1b[0m ${msg}`;
1912
+ }
1913
+
1914
+ // Split newlines and push
1915
+ const lines = line.split('\n');
1916
+ lines.forEach(l => {
1917
+ targetBuffer.push(l);
1918
+ if (targetBuffer.length > TUI.maxLogs) targetBuffer.shift();
1919
+ });
1920
+
1921
+ renderTUI();
1922
+ };
1923
+
1924
+ console.log = (...args) => {
1925
+ const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
1926
+ // Filter TUI artifacts
1927
+ if (msg.includes('\x1b[2J') || msg.includes('\x1b[0f')) return;
1928
+ addToBuffers(msg, false);
1929
+ };
1930
+
1931
+ console.error = (...args) => {
1932
+ const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ');
1933
+ const colored = msg.startsWith('\x1b') ? msg : `\x1b[31m${msg}\x1b[0m`;
1934
+ addToBuffers(colored, true);
1935
+ };
1936
+
1937
+ // Vertical Split Renderer
1938
+ function renderTUI() {
1939
+ const width = process.stdout.columns || 80;
1940
+ const height = process.stdout.rows || 24;
1941
+ const contentHeight = height - 4; // Header, Divider, Prompt, Margin
1942
+
1943
+ // Split: Left (Interactive) 40%, Right (Logs) 60%
1944
+ // Ensure minimum widths
1945
+ const leftPercent = 0.4;
1946
+ const leftWidth = Math.floor(width * leftPercent) - 2;
1947
+ const rightWidth = width - leftWidth - 3; // -3 for separator " | "
1948
+
1949
+ if (leftWidth < 10 || rightWidth < 10) {
1950
+ // Fallback for very narrow screens? Just clear and print prompt?
1951
+ // Or simple stacked? Let's just proceed, it will look messy but functional.
1952
+ }
1953
+
1954
+ // Prepare views (Bottom-Anchored)
1955
+ const leftView = TUI.leftLogs.slice(-contentHeight);
1956
+ const rightView = TUI.rightLogs.slice(-contentHeight);
1957
+
1958
+ const leftEmpty = contentHeight - leftView.length;
1959
+ const rightEmpty = contentHeight - rightView.length;
1960
+
1961
+ // Draw Frame
1962
+ process.stdout.write('\x1b[2J'); // Clear
1963
+ process.stdout.write('\x1b[H'); // Home (standard ANSI)
1964
+
1965
+ // 1. Header
1966
+ const headerText = ` DEVOPS AGENT | Session: ${TUI.sessionId || 'N/A'} | Branch: ${TUI.branch} `;
1967
+ const headPad = Math.max(0, width - headerText.length);
1968
+ process.stdout.write(`\x1b[44m\x1b[37m${headerText}${' '.repeat(headPad)}\x1b[0m\n`);
1969
+
1970
+ // 2. Columns
1971
+ for (let i = 0; i < contentHeight; i++) {
1972
+ let lLine = (i >= leftEmpty) ? leftView[i - leftEmpty] : '';
1973
+ let rLine = (i >= rightEmpty) ? rightView[i - rightEmpty] : '';
1974
+
1975
+ // Removing ANSI for length calculation is essential for alignment
1976
+ const lPlain = stripAnsi(lLine);
1977
+ const rPlain = stripAnsi(rLine);
1978
+
1979
+ // Truncate to prevent wrapping and breaking layout
1980
+ let lDisp = lLine;
1981
+ let rDisp = rLine;
1982
+
1983
+ if (lPlain.length > leftWidth) {
1984
+ // Simple truncation (note: might cut ansi codes, but prevents layout break)
1985
+ // We use the plain length to determine cut point
1986
+ // For safety, we just display the plain truncated text to avoid hanging colors
1987
+ lDisp = lPlain.substring(0, leftWidth - 3) + '...';
1988
+ }
1989
+
1990
+ if (rPlain.length > rightWidth) {
1991
+ rDisp = rPlain.substring(0, rightWidth - 3) + '...';
1992
+ }
1993
+
1994
+ // Pad using the length of the *displayed* string (re-strip to be sure)
1995
+ const lDispLen = stripAnsi(lDisp).length;
1996
+ const rDispLen = stripAnsi(rDisp).length;
1997
+
1998
+ const lPad = Math.max(0, leftWidth - lDispLen);
1999
+ const rPad = Math.max(0, rightWidth - rDispLen);
2000
+
2001
+ process.stdout.write(lDisp + ' '.repeat(lPad));
2002
+ process.stdout.write('\x1b[90m │ \x1b[0m');
2003
+ process.stdout.write(rDisp + ' '.repeat(rPad));
2004
+ process.stdout.write('\n');
2005
+ }
2006
+
2007
+ // 3. Divider
2008
+ const lDiv = '─'.repeat(leftWidth);
2009
+ const rDiv = '─'.repeat(rightWidth);
2010
+ process.stdout.write(`\x1b[90m${lDiv}─┴─${rDiv}\x1b[0m\n`);
2011
+
2012
+ // 4. Prompt
2013
+ process.stdout.write(`\x1b[36m[agent] >\x1b[0m ${TUI.inputBuffer}`);
2014
+ }
2015
+
2016
+ // Initial render
2017
+ renderTUI();
2018
+
1738
2019
  // Command handler
1739
2020
  rl.on('line', async (line) => {
2021
+ // If in prompt mode, ignore this handler (let the prompt handler take it)
2022
+ if (TUI.inputMode !== 'command') return;
2023
+
2024
+ TUI.inputBuffer = '';
2025
+ addToBuffers(`> ${line}`); // Echo command to left panel
1740
2026
  const cmd = line.trim().toLowerCase();
1741
2027
 
1742
2028
  switch (cmd) {
@@ -1802,10 +2088,22 @@ console.log();
1802
2088
  let commitMsg = readMsgFile(msgPath);
1803
2089
  if (!commitMsg) {
1804
2090
  console.log("No commit message found. Enter message (or 'cancel' to abort):");
1805
- rl.prompt();
2091
+
2092
+ // Switch to prompt mode to prevent main loop interference
2093
+ TUI.inputMode = 'prompt';
2094
+
2095
+ // Use a temporary prompt interface
1806
2096
  const msgInput = await new Promise(resolve => {
1807
- rl.once('line', resolve);
2097
+ // We need to use a one-time listener that doesn't conflict
2098
+ const handler = (input) => {
2099
+ resolve(input);
2100
+ };
2101
+ rl.once('line', handler);
1808
2102
  });
2103
+
2104
+ // Switch back to command mode
2105
+ TUI.inputMode = 'command';
2106
+
1809
2107
  if (msgInput.trim().toLowerCase() === 'cancel') {
1810
2108
  console.log("Commit cancelled.");
1811
2109
  break;
@@ -1818,7 +2116,7 @@ console.log();
1818
2116
  console.log("No changes to commit.");
1819
2117
  }
1820
2118
  break;
1821
-
2119
+
1822
2120
  case 'push':
1823
2121
  case 'p':
1824
2122
  const branchName = await currentBranch();
@@ -1840,11 +2138,18 @@ console.log();
1840
2138
  const uncommitted = await hasUncommittedChanges();
1841
2139
  if (uncommitted) {
1842
2140
  console.log("You have uncommitted changes. Commit them before exit? (y/n)");
1843
- rl.prompt();
2141
+
2142
+ TUI.inputMode = 'prompt';
2143
+
1844
2144
  const answer = await new Promise(resolve => {
1845
- rl.once('line', resolve);
2145
+ const handler = (input) => {
2146
+ resolve(input);
2147
+ };
2148
+ rl.once('line', handler);
1846
2149
  });
1847
2150
 
2151
+ TUI.inputMode = 'command';
2152
+
1848
2153
  if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
1849
2154
  console.log("Committing changes...");
1850
2155
  const exitMsg = "chore: session cleanup - final commit before exit";
@@ -1941,74 +2246,128 @@ console.log();
1941
2246
  let mergeCompleted = false;
1942
2247
  if (shouldMerge) {
1943
2248
  try {
1944
- console.log(`\n\x1b[34mMerging ${currentBranchName} into ${targetBranch}...\x1b[0m`);
1945
-
1946
- // Check if target branch exists locally
1947
- let branchExists = false;
2249
+ // Check if repoRoot is clean before attempting merge operations
2250
+ // We use --porcelain to check for modifications
2251
+ let isDirty = false;
1948
2252
  try {
1949
- execSync(`git rev-parse --verify ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1950
- branchExists = true;
1951
- } catch (err) {
1952
- // Branch doesn't exist locally
1953
- }
1954
-
1955
- if (!branchExists) {
1956
- // Check if branch exists on remote
1957
- try {
1958
- const remoteCheck = execSync(`git ls-remote --heads origin ${targetBranch}`, {
1959
- cwd: repoRoot,
1960
- encoding: 'utf8'
1961
- }).trim();
2253
+ const status = execSync(`git status --porcelain`, { cwd: repoRoot, encoding: 'utf8' });
2254
+ if (status && status.trim().length > 0) {
2255
+ isDirty = true;
2256
+ console.log(`\n\x1b[33m⚠️ The main repository has uncommitted changes:\x1b[0m`);
2257
+ const lines = status.trim().split('\n').slice(0, 5);
2258
+ lines.forEach(l => console.log(` ${l}`));
2259
+ if (status.trim().split('\n').length > 5) console.log(' ...');
2260
+
2261
+ console.log(`\nMerge cannot proceed with a dirty working tree.`);
2262
+ console.log(`You can stash these changes to proceed with the merge, or skip the merge.`);
2263
+ console.log(` y/yes - Stash changes and merge`);
2264
+ console.log(` n/no - Skip merge`);
1962
2265
 
1963
- if (remoteCheck) {
1964
- // Branch exists on remote, fetch it
1965
- console.log(`\x1b[2mTarget branch doesn't exist locally, fetching from remote...\x1b[0m`);
1966
- execSync(`git fetch origin ${targetBranch}:${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2266
+ rl.prompt();
2267
+ const stashAnswer = await new Promise(resolve => {
2268
+ rl.once('line', resolve);
2269
+ });
2270
+
2271
+ if (stashAnswer.toLowerCase() === 'y' || stashAnswer.toLowerCase() === 'yes') {
2272
+ console.log(`\nStashing changes in main repo...`);
2273
+ execSync(`git stash push -m "Auto-stash before session merge ${sessionId}"`, { cwd: repoRoot, stdio: 'pipe' });
2274
+ isDirty = false;
1967
2275
  } else {
1968
- // Branch doesn't exist on remote either, create it
1969
- console.log(`\x1b[33mTarget branch '${targetBranch}' doesn't exist. Creating it...\x1b[0m`);
1970
- execSync(`git checkout -b ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1971
- execSync(`git push -u origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1972
- console.log(`\x1b[32m✓\x1b[0m Created new branch ${targetBranch}`);
2276
+ console.log(`\nSkipping merge due to dirty working tree.`);
2277
+ shouldMerge = false;
1973
2278
  }
1974
- } catch (err) {
1975
- console.error(`\x1b[31m✗ Error checking/creating remote branch: ${err.message}\x1b[0m`);
1976
- throw err;
1977
- }
1978
- }
1979
-
1980
- // Switch to target branch
1981
- execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1982
-
1983
- // Pull latest (if branch already existed)
1984
- if (branchExists) {
1985
- try {
1986
- execSync(`git pull origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
1987
- } catch (err) {
1988
- console.log(`\x1b[2mCould not pull latest changes (may be new branch)\x1b[0m`);
1989
2279
  }
1990
- }
1991
-
1992
- // Merge the session branch
1993
- execSync(`git merge --no-ff ${currentBranchName} -m "Merge session ${sessionId}: session work"`, {
1994
- cwd: repoRoot,
1995
- stdio: 'pipe'
1996
- });
1997
-
1998
- // Push merged changes
1999
- execSync(`git push origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2000
-
2001
- console.log(`\x1b[32m✓\x1b[0m Successfully merged to ${targetBranch}`);
2002
-
2003
- // Delete remote branch after successful merge
2004
- try {
2005
- execSync(`git push origin --delete ${currentBranchName}`, { cwd: repoRoot, stdio: 'pipe' });
2006
- console.log(`\x1b[32m✓\x1b[0m Deleted remote branch ${currentBranchName}`);
2007
2280
  } catch (err) {
2008
- console.log(`\x1b[2mCould not delete remote branch\x1b[0m`);
2281
+ console.log(`\x1b[31mError checking git status: ${err.message}\x1b[0m`);
2282
+ shouldMerge = false; // Err on side of caution
2283
+ }
2284
+
2285
+ if (shouldMerge && !isDirty) {
2286
+ console.log(`\n\x1b[34mMerging ${currentBranchName} into ${targetBranch}...\x1b[0m`);
2287
+
2288
+ // Check if target branch exists locally
2289
+ let branchExists = false;
2290
+ try {
2291
+ execSync(`git rev-parse --verify ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2292
+ branchExists = true;
2293
+ } catch (err) {
2294
+ // Branch doesn't exist locally
2295
+ }
2296
+
2297
+ if (!branchExists) {
2298
+ // Check if branch exists on remote
2299
+ try {
2300
+ const remoteCheck = execSync(`git ls-remote --heads origin ${targetBranch}`, {
2301
+ cwd: repoRoot,
2302
+ encoding: 'utf8'
2303
+ }).trim();
2304
+
2305
+ if (remoteCheck) {
2306
+ // Branch exists on remote, fetch it
2307
+ console.log(`\x1b[2mTarget branch doesn't exist locally, fetching from remote...\x1b[0m`);
2308
+ execSync(`git fetch origin ${targetBranch}:${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2309
+ } else {
2310
+ // Branch doesn't exist on remote either, create it
2311
+ console.log(`\x1b[33mTarget branch '${targetBranch}' doesn't exist. Creating it...\x1b[0m`);
2312
+ execSync(`git checkout -b ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2313
+ execSync(`git push -u origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2314
+ console.log(`\x1b[32m✓\x1b[0m Created new branch ${targetBranch}`);
2315
+ }
2316
+ } catch (err) {
2317
+ console.error(`\x1b[31m✗ Error checking/creating remote branch: ${err.message}\x1b[0m`);
2318
+ throw err;
2319
+ }
2320
+ }
2321
+
2322
+ // Switch to target branch
2323
+ execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2324
+
2325
+ // Pull latest (if branch already existed)
2326
+ if (branchExists) {
2327
+ try {
2328
+ execSync(`git pull origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2329
+ } catch (err) {
2330
+ console.log(`\x1b[2mCould not pull latest changes (may be new branch)\x1b[0m`);
2331
+ }
2332
+ }
2333
+
2334
+ // Rebase session branch onto target branch
2335
+ try {
2336
+ console.log(`\x1b[34mRebasing ${currentBranchName} onto ${targetBranch}...\x1b[0m`);
2337
+ execSync(`git checkout ${currentBranchName}`, { cwd: repoRoot, stdio: 'pipe' });
2338
+ execSync(`git rebase ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2339
+ console.log(`\x1b[32m✓\x1b[0m Rebase successful`);
2340
+
2341
+ // Switch back to target for merge
2342
+ execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2343
+ } catch (err) {
2344
+ console.error(`\x1b[31m✗ Rebase failed: ${err.message}\x1b[0m`);
2345
+ console.log(`\x1b[33mAborting rebase and trying direct merge...\x1b[0m`);
2346
+ try { execSync(`git rebase --abort`, { cwd: repoRoot, stdio: 'pipe' }); } catch (e) {}
2347
+ execSync(`git checkout ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2348
+ }
2349
+
2350
+ // Merge the session branch
2351
+ execSync(`git merge --no-ff ${currentBranchName} -m "Merge session ${sessionId}: session work"`, {
2352
+ cwd: repoRoot,
2353
+ stdio: 'pipe'
2354
+ });
2355
+
2356
+ // Push merged changes
2357
+ execSync(`git push origin ${targetBranch}`, { cwd: repoRoot, stdio: 'pipe' });
2358
+
2359
+ console.log(`\x1b[32m✓\x1b[0m Successfully merged to ${targetBranch}`);
2360
+
2361
+ // Delete remote branch after successful merge
2362
+ try {
2363
+ execSync(`git push origin --delete ${currentBranchName}`, { cwd: repoRoot, stdio: 'pipe' });
2364
+ console.log(`\x1b[32m✓\x1b[0m Deleted remote branch ${currentBranchName}`);
2365
+ } catch (err) {
2366
+ console.log(`\x1b[2mCould not delete remote branch\x1b[0m`);
2367
+ }
2368
+
2369
+ mergeCompleted = true;
2009
2370
  }
2010
-
2011
- mergeCompleted = true;
2012
2371
  } catch (err) {
2013
2372
  console.error(`\x1b[31m✗ Merge failed: ${err.message}\x1b[0m`);
2014
2373
  console.log(`\x1b[33mYou may need to resolve conflicts manually\x1b[0m`);