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.
@@ -133,17 +133,22 @@ export class SessionCoordinator {
133
133
  }
134
134
 
135
135
  cleanupStaleLocks() {
136
- // Clean up locks older than 1 hour
137
- const oneHourAgo = Date.now() - 3600000;
136
+ // Clean up locks older than 24 hours (increased from 1 hour to allow resuming next day)
137
+ const staleThreshold = Date.now() - 86400000;
138
138
 
139
139
  if (fs.existsSync(this.locksPath)) {
140
140
  const locks = fs.readdirSync(this.locksPath);
141
141
  locks.forEach(lockFile => {
142
142
  const lockPath = path.join(this.locksPath, lockFile);
143
- const stats = fs.statSync(lockPath);
144
- if (stats.mtimeMs < oneHourAgo) {
145
- fs.unlinkSync(lockPath);
146
- console.log(`${CONFIG.colors.dim}Cleaned stale lock: ${lockFile}${CONFIG.colors.reset}`);
143
+ try {
144
+ const stats = fs.statSync(lockPath);
145
+ // Only cleanup if VERY old
146
+ if (stats.mtimeMs < staleThreshold) {
147
+ fs.unlinkSync(lockPath);
148
+ console.log(`${CONFIG.colors.dim}Cleaned stale lock: ${lockFile}${CONFIG.colors.reset}`);
149
+ }
150
+ } catch (e) {
151
+ // Ignore errors
147
152
  }
148
153
  });
149
154
  }
@@ -152,7 +157,9 @@ export class SessionCoordinator {
152
157
  /**
153
158
  * Check for newer version on npm registry
154
159
  */
155
- async checkForUpdates() {
160
+ async checkForUpdates(skip = false) {
161
+ if (skip) return;
162
+
156
163
  const globalSettings = this.loadGlobalSettings();
157
164
  const now = Date.now();
158
165
 
@@ -165,22 +172,42 @@ export class SessionCoordinator {
165
172
  // Show checking message
166
173
  console.log(`${CONFIG.colors.dim}🔍 Checking for DevOps Agent updates...${CONFIG.colors.reset}`);
167
174
 
168
- // Check npm for latest version
169
- const result = execSync('npm view s9n-devops-agent version', {
175
+ // Check npm for dist-tags
176
+ const distTags = JSON.parse(execSync('npm view s9n-devops-agent dist-tags --json', {
170
177
  encoding: 'utf8',
171
178
  stdio: ['ignore', 'pipe', 'ignore'],
172
179
  timeout: 5000
173
- }).trim();
180
+ }).trim());
181
+
182
+ const latest = distTags.latest;
183
+ const dev = distTags.dev;
174
184
 
175
185
  // Update last check time
176
186
  globalSettings.lastUpdateCheck = now;
177
187
  this.saveGlobalSettings(globalSettings);
178
188
 
179
- // Compare versions
180
- if (result && this.compareVersions(result, this.currentVersion) > 0) {
189
+ // Determine which version to compare against
190
+ // If current is a dev version, we check dev tag as well
191
+ const isDev = this.currentVersion.includes('dev') || this.currentVersion.includes('-');
192
+
193
+ let updateAvailable = false;
194
+ let targetVersion = latest;
195
+ let updateTag = 'latest';
196
+
197
+ if (isDev && dev && this.compareVersions(dev, this.currentVersion) > 0) {
198
+ updateAvailable = true;
199
+ targetVersion = dev;
200
+ updateTag = 'dev';
201
+ } else if (this.compareVersions(latest, this.currentVersion) > 0) {
202
+ updateAvailable = true;
203
+ targetVersion = latest;
204
+ updateTag = 'latest';
205
+ }
206
+
207
+ if (updateAvailable) {
181
208
  console.log(`\n${CONFIG.colors.yellow}▲ Update Available!${CONFIG.colors.reset}`);
182
209
  console.log(`${CONFIG.colors.dim}Current version: ${this.currentVersion}${CONFIG.colors.reset}`);
183
- console.log(`${CONFIG.colors.bright}Latest version: ${result}${CONFIG.colors.reset}`);
210
+ console.log(`${CONFIG.colors.bright}New version: ${targetVersion} (${updateTag})${CONFIG.colors.reset}`);
184
211
  console.log();
185
212
 
186
213
  // Ask if user wants to update now
@@ -199,7 +226,7 @@ export class SessionCoordinator {
199
226
  if (updateNow) {
200
227
  console.log(`\n${CONFIG.colors.blue}Updating s9n-devops-agent...${CONFIG.colors.reset}`);
201
228
  try {
202
- execSync('npm install -g s9n-devops-agent@latest', {
229
+ execSync(`npm install -g s9n-devops-agent@${updateTag}`, {
203
230
  stdio: 'inherit',
204
231
  cwd: process.cwd()
205
232
  });
@@ -207,10 +234,10 @@ export class SessionCoordinator {
207
234
  process.exit(0);
208
235
  } catch (err) {
209
236
  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}`);
237
+ console.log(`${CONFIG.colors.dim}You can manually update with: npm install -g s9n-devops-agent@${updateTag}${CONFIG.colors.reset}`);
211
238
  }
212
239
  } else {
213
- console.log(`${CONFIG.colors.dim}You can update later with: npm install -g s9n-devops-agent@latest${CONFIG.colors.reset}`);
240
+ console.log(`${CONFIG.colors.dim}You can update later with: npm install -g s9n-devops-agent@${updateTag}${CONFIG.colors.reset}`);
214
241
  }
215
242
  console.log();
216
243
  } else {
@@ -224,19 +251,22 @@ export class SessionCoordinator {
224
251
  }
225
252
 
226
253
  /**
227
- * Compare semantic versions
254
+ * Compare semantic versions (robust to suffixes)
228
255
  * Returns: 1 if v1 > v2, -1 if v1 < v2, 0 if equal
229
256
  */
230
257
  compareVersions(v1, v2) {
231
- const parts1 = v1.split('.').map(Number);
232
- const parts2 = v2.split('.').map(Number);
258
+ if (!v1 || !v2) return 0;
233
259
 
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;
260
+ const normalize = v => v.replace(/^v/, '').split('.').map(p => parseInt(p, 10));
261
+ const p1 = normalize(v1);
262
+ const p2 = normalize(v2);
263
+
264
+ for (let i = 0; i < Math.max(p1.length, p2.length); i++) {
265
+ const n1 = isNaN(p1[i]) ? 0 : p1[i];
266
+ const n2 = isNaN(p2[i]) ? 0 : p2[i];
237
267
 
238
- if (p1 > p2) return 1;
239
- if (p1 < p2) return -1;
268
+ if (n1 > n2) return 1;
269
+ if (n1 < n2) return -1;
240
270
  }
241
271
 
242
272
  return 0;
@@ -245,7 +275,9 @@ export class SessionCoordinator {
245
275
  /**
246
276
  * Ensure developer initials are configured globally
247
277
  */
248
- async ensureGlobalSetup() {
278
+ async ensureGlobalSetup(skip = false) {
279
+ if (skip) return;
280
+
249
281
  const globalSettings = this.loadGlobalSettings();
250
282
 
251
283
  // Check if global setup is needed (developer initials)
@@ -321,7 +353,9 @@ export class SessionCoordinator {
321
353
  /**
322
354
  * Ensure house rules are set up for the project
323
355
  */
324
- async ensureHouseRulesSetup() {
356
+ async ensureHouseRulesSetup(skip = false) {
357
+ if (skip) return;
358
+
325
359
  const houseRulesManager = new HouseRulesManager(this.repoRoot);
326
360
  const houseRulesPath = path.join(this.repoRoot, 'houserules.md');
327
361
 
@@ -427,13 +461,20 @@ export class SessionCoordinator {
427
461
  /**
428
462
  * Ensure project-specific version settings are configured
429
463
  */
430
- async ensureProjectSetup() {
464
+ async ensureProjectSetup(options = {}) {
465
+ if (options.skip) return;
466
+
431
467
  const projectSettings = this.loadProjectSettings();
432
468
 
433
469
  // 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}`);
470
+ if (options.force || !projectSettings.versioningStrategy || !projectSettings.versioningStrategy.configured) {
471
+ console.log(`\n${CONFIG.colors.yellow}Project Versioning Setup${CONFIG.colors.reset}`);
472
+ if (options.force) {
473
+ console.log(`${CONFIG.colors.dim}Reconfiguring version strategy...${CONFIG.colors.reset}`);
474
+ } else {
475
+ console.log(`${CONFIG.colors.yellow}First-time project setup for this repository!${CONFIG.colors.reset}`);
476
+ console.log(`${CONFIG.colors.dim}Let's configure the versioning strategy for this project${CONFIG.colors.reset}`);
477
+ }
437
478
 
438
479
  const versionInfo = await this.promptForStartingVersion();
439
480
  projectSettings.versioningStrategy = {
@@ -555,7 +596,7 @@ export class SessionCoordinator {
555
596
  return {
556
597
  ...global,
557
598
  ...project,
558
- developerInitials: global.developerInitials,
599
+ developerInitials: project.developerInitials || global.developerInitials,
559
600
  configured: global.configured
560
601
  };
561
602
  }
@@ -657,6 +698,8 @@ export class SessionCoordinator {
657
698
  const projectSettings = this.loadProjectSettings();
658
699
  if (projectSettings.dockerConfig && projectSettings.dockerConfig.neverAsk === true) {
659
700
  // User selected 'Never' - skip Docker configuration
701
+ // Show a subtle message so they know why it's skipped
702
+ console.log(`${CONFIG.colors.dim}Skipping Docker config (User preference: Never ask). Edit local_deploy/project-settings.json to enable.${CONFIG.colors.reset}`);
660
703
  return { enabled: false, neverAsk: true };
661
704
  }
662
705
 
@@ -797,6 +840,73 @@ export class SessionCoordinator {
797
840
  return config;
798
841
  }
799
842
 
843
+ /**
844
+ * Prompt for base branch (source)
845
+ */
846
+ async promptForBaseBranch() {
847
+ console.log(`\n${CONFIG.colors.yellow}═══ Base Branch Selection ═══${CONFIG.colors.reset}`);
848
+ console.log(`${CONFIG.colors.dim}Which branch should I use as the starting point for your work?${CONFIG.colors.reset}`);
849
+
850
+ // Get available branches
851
+ const branches = this.getAvailableBranches();
852
+ // Prioritize main/develop/master
853
+ const priorityBranches = ['main', 'master', 'develop', 'development'];
854
+
855
+ const sortedBranches = branches.sort((a, b) => {
856
+ const aP = priorityBranches.indexOf(a);
857
+ const bP = priorityBranches.indexOf(b);
858
+ if (aP !== -1 && bP !== -1) return aP - bP;
859
+ if (aP !== -1) return -1;
860
+ if (bP !== -1) return 1;
861
+ return a.localeCompare(b);
862
+ });
863
+
864
+ const uniqueBranches = [...new Set(sortedBranches)].slice(0, 10);
865
+
866
+ console.log();
867
+ uniqueBranches.forEach((branch, index) => {
868
+ const isPriority = priorityBranches.includes(branch);
869
+ const marker = isPriority ? ` ${CONFIG.colors.green}⭐${CONFIG.colors.reset}` : '';
870
+ console.log(` ${index + 1}) ${branch}${marker}`);
871
+ });
872
+ console.log(` 0) Enter a different branch name`);
873
+ console.log(` Hit Enter for default (HEAD)`);
874
+
875
+ const rl = readline.createInterface({
876
+ input: process.stdin,
877
+ output: process.stdout
878
+ });
879
+
880
+ return new Promise((resolve) => {
881
+ rl.question(`\nSelect base branch (1-${uniqueBranches.length}, 0, or Enter): `, (answer) => {
882
+ rl.close();
883
+ const choice = answer.trim();
884
+
885
+ if (choice === '') {
886
+ resolve('HEAD');
887
+ return;
888
+ }
889
+
890
+ const num = parseInt(choice);
891
+
892
+ if (num === 0) {
893
+ const rl2 = readline.createInterface({
894
+ input: process.stdin,
895
+ output: process.stdout
896
+ });
897
+ rl2.question('Enter custom branch name: ', (custom) => {
898
+ rl2.close();
899
+ resolve(custom.trim() || 'HEAD');
900
+ });
901
+ } else if (num >= 1 && num <= uniqueBranches.length) {
902
+ resolve(uniqueBranches[num - 1]);
903
+ } else {
904
+ resolve('HEAD');
905
+ }
906
+ });
907
+ });
908
+ }
909
+
800
910
  /**
801
911
  * Prompt for auto-merge configuration
802
912
  */
@@ -1112,15 +1222,50 @@ export class SessionCoordinator {
1112
1222
  * Create a new session and generate Claude instructions
1113
1223
  */
1114
1224
  async createSession(options = {}) {
1115
- // Check for updates (once per day)
1116
- await this.checkForUpdates();
1225
+ // Check for updates (once per day) - skip if requested (e.g. called from Kora)
1226
+ await this.checkForUpdates(options.skipUpdate);
1117
1227
 
1118
1228
  // Ensure both global and project setup are complete
1119
- await this.ensureGlobalSetup(); // Developer initials (once per user)
1120
- await this.ensureProjectSetup(); // Version strategy (once per project)
1121
- await this.ensureHouseRulesSetup(); // House rules setup (once per project)
1229
+ await this.ensureGlobalSetup(options.skipSetup); // Developer initials (once per user)
1230
+ await this.ensureProjectSetup({ force: false, skip: options.skipSetup }); // Version strategy (once per project)
1231
+ await this.ensureHouseRulesSetup(options.skipSetup); // House rules setup (once per project)
1122
1232
  await this.ensureGroqApiKey(); // GROQ API key for AI commits (once per user)
1123
1233
 
1234
+ // Resume Check: If task provided, check for existing sessions with similar task names
1235
+ if (options.task && options.task !== 'development') {
1236
+ const matchingSession = this.findSessionByTask(options.task);
1237
+ if (matchingSession) {
1238
+ console.log(`\\n${CONFIG.colors.yellow}Found existing session for '${options.task}'${CONFIG.colors.reset}`);
1239
+ console.log(`Session ID: ${CONFIG.colors.bright}${matchingSession.sessionId}${CONFIG.colors.reset}`);
1240
+ console.log(`Status: ${matchingSession.status}`);
1241
+ console.log(`Branch: ${matchingSession.branchName}`);
1242
+
1243
+ const rlResume = readline.createInterface({ input: process.stdin, output: process.stdout });
1244
+ const resume = await new Promise(resolve => {
1245
+ rlResume.question(`\\nDo you want to resume this session instead? (Y/n): `, ans => {
1246
+ rlResume.close();
1247
+ resolve(ans.trim().toLowerCase() !== 'n');
1248
+ });
1249
+ });
1250
+
1251
+ if (resume) {
1252
+ // Return existing session info structure similar to createSession
1253
+ // but we might need to "claim" it if it's inactive
1254
+ if (matchingSession.status !== 'active') {
1255
+ // Claim/Restart it
1256
+ return this.claimSession(matchingSession, options.agent || 'claude');
1257
+ } else {
1258
+ // It's already active, just return info so startAgent can pick it up
1259
+ // But startAgent requires lock file integrity.
1260
+ // If it's active, we might be double-attaching unless we check PID.
1261
+ // findAvailableSession logic already handles dead PIDs.
1262
+ // If PID is alive, we probably shouldn't interfere, but here we assume user knows best.
1263
+ return matchingSession;
1264
+ }
1265
+ }
1266
+ }
1267
+ }
1268
+
1124
1269
  const sessionId = this.generateSessionId();
1125
1270
  const task = options.task || 'development';
1126
1271
 
@@ -1172,6 +1317,12 @@ export class SessionCoordinator {
1172
1317
  // Ask for auto-merge configuration
1173
1318
  const mergeConfig = await this.promptForMergeConfig();
1174
1319
 
1320
+ // Ask for base branch (where to start work from)
1321
+ const baseBranch = await this.promptForBaseBranch();
1322
+
1323
+ // Ask for auto-rebase interval
1324
+ const rebaseInterval = await this.promptForRebaseInterval();
1325
+
1175
1326
  // Check for Docker configuration and ask about restart preference
1176
1327
  let dockerConfig = null;
1177
1328
 
@@ -1321,9 +1472,15 @@ export class SessionCoordinator {
1321
1472
 
1322
1473
  // Create worktree
1323
1474
  console.log(`\n${CONFIG.colors.yellow}Creating worktree...${CONFIG.colors.reset}`);
1324
- execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, { stdio: 'pipe' });
1475
+ const baseRef = baseBranch || 'HEAD';
1476
+ console.log(`${CONFIG.colors.dim}Branching off: ${baseRef}${CONFIG.colors.reset}`);
1477
+
1478
+ execSync(`git worktree add -b ${branchName} "${worktreePath}" ${baseRef}`, { stdio: 'pipe' });
1325
1479
  console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Worktree created at: ${worktreePath}`);
1326
1480
 
1481
+ // Store base branch in session data for rebase logic
1482
+ const sessionBaseBranch = baseRef === 'HEAD' ? await this.resolveHeadBranch() : baseRef;
1483
+
1327
1484
  // If we're in a submodule, set up the correct remote for the worktree
1328
1485
  if (isSubmodule && parentRemote) {
1329
1486
  console.log(`${CONFIG.colors.yellow}Configuring worktree to use parent repository remote...${CONFIG.colors.reset}`);
@@ -1345,16 +1502,17 @@ export class SessionCoordinator {
1345
1502
  task,
1346
1503
  worktreePath,
1347
1504
  branchName,
1505
+ baseBranch: sessionBaseBranch,
1348
1506
  created: new Date().toISOString(),
1349
1507
  status: 'active',
1350
1508
  pid: process.pid,
1351
1509
  developerInitials: devInitials,
1352
1510
  mergeConfig: mergeConfig,
1511
+ rebaseInterval: rebaseInterval,
1353
1512
  dockerConfig: dockerConfig
1354
1513
  };
1355
1514
 
1356
1515
  const lockFile = path.join(this.locksPath, `${sessionId}.lock`);
1357
- fs.writeFileSync(lockFile, JSON.stringify(lockData, null, 2));
1358
1516
 
1359
1517
  // Generate Claude instructions
1360
1518
  const instructions = this.generateClaudeInstructions(lockData);
@@ -1372,6 +1530,9 @@ export class SessionCoordinator {
1372
1530
  // Store instructions in lockData so createAndStart can access them
1373
1531
  lockData.instructions = instructions;
1374
1532
 
1533
+ // Write lock file with instructions included
1534
+ fs.writeFileSync(lockFile, JSON.stringify(lockData, null, 2));
1535
+
1375
1536
  return {
1376
1537
  sessionId,
1377
1538
  worktreePath,
@@ -1387,6 +1548,46 @@ export class SessionCoordinator {
1387
1548
  }
1388
1549
  }
1389
1550
 
1551
+ /**
1552
+ * Prompt for auto-rebase interval
1553
+ */
1554
+ async promptForRebaseInterval() {
1555
+ console.log(`\n${CONFIG.colors.yellow}═══ Auto-Rebase Configuration ═══${CONFIG.colors.reset}`);
1556
+ console.log(`${CONFIG.colors.dim}I can automatically pull updates from the base branch to keep you up-to-date.${CONFIG.colors.reset}`);
1557
+ console.log(`${CONFIG.colors.dim}This helps prevent conflicts later by rebasing your work periodically.${CONFIG.colors.reset}`);
1558
+
1559
+ const rl = readline.createInterface({
1560
+ input: process.stdin,
1561
+ output: process.stdout
1562
+ });
1563
+
1564
+ return new Promise((resolve) => {
1565
+ rl.question(`\nHow often should I rebase? (in hours, 0 to disable) [0]: `, (answer) => {
1566
+ rl.close();
1567
+ const hours = parseFloat(answer.trim());
1568
+ if (isNaN(hours) || hours <= 0) {
1569
+ console.log(`${CONFIG.colors.dim}Auto-rebase disabled. I'll let you manage updates manually.${CONFIG.colors.reset}`);
1570
+ resolve(0);
1571
+ } else {
1572
+ console.log(`${CONFIG.colors.green}✓ I'll check for updates and rebase every ${hours} hour(s).${CONFIG.colors.reset}`);
1573
+ resolve(hours);
1574
+ }
1575
+ });
1576
+ });
1577
+ }
1578
+
1579
+ /**
1580
+ * Resolve HEAD to actual branch name
1581
+ */
1582
+ async resolveHeadBranch() {
1583
+ try {
1584
+ const head = execSync('git rev-parse --abbrev-ref HEAD', { cwd: this.repoRoot, encoding: 'utf8' }).trim();
1585
+ return head;
1586
+ } catch (e) {
1587
+ return 'main';
1588
+ }
1589
+ }
1590
+
1390
1591
  /**
1391
1592
  * Generate instructions for the coding agent
1392
1593
  */
@@ -1403,7 +1604,7 @@ INSTRUCTIONS:
1403
1604
  1. Change to worktree directory: cd "${worktreePath}"
1404
1605
  2. Verify branch: git branch --show-current
1405
1606
  3. Make your changes for: ${task}
1406
- 4. Write commit message to: .devops-commit-${sessionId}.msg
1607
+ 4. Write commit message to: .devops-commit-${sessionId}.msg (use >> to append)
1407
1608
  5. The DevOps agent will auto-commit and push your changes
1408
1609
  `;
1409
1610
 
@@ -1459,7 +1660,7 @@ Make changes for: **${task}**
1459
1660
  ### Step 5: Commit Your Changes
1460
1661
  Write your commit message to the session-specific file:
1461
1662
  \`\`\`bash
1462
- echo "feat: your commit message here" > .devops-commit-${sessionId}.msg
1663
+ echo "feat: your commit message here" >> .devops-commit-${sessionId}.msg
1463
1664
  \`\`\`
1464
1665
 
1465
1666
  ### Step 6: Release Your File Locks
@@ -1550,6 +1751,7 @@ The DevOps agent will automatically:
1550
1751
  console.log(`}`);
1551
1752
  console.log(``);
1552
1753
  console.log(`Write commit messages to: .devops-commit-${sessionId}.msg`);
1754
+ console.log(`(Use '>>' to append if you want to add to an existing message)`);
1553
1755
  console.log(`The DevOps agent will automatically commit and push changes.`);
1554
1756
  console.log(``);
1555
1757
  console.log(`⛔ IMPORTANT: STOP HERE AND WAIT`);
@@ -1709,7 +1911,33 @@ The DevOps agent is monitoring this worktree for changes.
1709
1911
  }
1710
1912
 
1711
1913
  /**
1712
- * Find an available unclaimed session
1914
+ * Find a session by task name (fuzzy match)
1915
+ */
1916
+ findSessionByTask(taskName) {
1917
+ if (!fs.existsSync(this.locksPath)) return null;
1918
+
1919
+ const locks = fs.readdirSync(this.locksPath).filter(f => f.endsWith('.lock'));
1920
+ const search = taskName.toLowerCase().replace(/[^a-z0-9]/g, '');
1921
+
1922
+ for (const lockFile of locks) {
1923
+ try {
1924
+ const lockPath = path.join(this.locksPath, lockFile);
1925
+ const session = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
1926
+ const task = (session.task || '').toLowerCase().replace(/[^a-z0-9]/g, '');
1927
+
1928
+ // Exact match or significant partial match
1929
+ if (task === search || (task.length > 4 && task.includes(search)) || (search.length > 4 && search.includes(task))) {
1930
+ return session;
1931
+ }
1932
+ } catch (e) {
1933
+ // Ignore invalid locks
1934
+ }
1935
+ }
1936
+ return null;
1937
+ }
1938
+
1939
+ /**
1940
+ * Find an available unclaimed session or orphaned session
1713
1941
  */
1714
1942
  findAvailableSession() {
1715
1943
  if (!fs.existsSync(this.locksPath)) {
@@ -1720,11 +1948,33 @@ The DevOps agent is monitoring this worktree for changes.
1720
1948
 
1721
1949
  for (const lockFile of locks) {
1722
1950
  const lockPath = path.join(this.locksPath, lockFile);
1723
- const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
1951
+ try {
1952
+ const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
1724
1953
 
1725
- // Check if session is available (not claimed)
1726
- if (lockData.status === 'waiting' && !lockData.claimedBy) {
1727
- return lockData;
1954
+ // Check if session is available (not claimed)
1955
+ if (lockData.status === 'waiting' && !lockData.claimedBy) {
1956
+ return lockData;
1957
+ }
1958
+
1959
+ // Check if session is orphaned/stopped but not cleaned up
1960
+ // If the PID is no longer running, it might be orphaned
1961
+ if (lockData.status === 'active' && lockData.agentPid) {
1962
+ try {
1963
+ // Check if process exists
1964
+ process.kill(lockData.agentPid, 0);
1965
+ } catch (e) {
1966
+ // Process doesn't exist - it's orphaned!
1967
+ console.log(`${CONFIG.colors.yellow}Found orphaned session: ${lockData.sessionId} (PID ${lockData.agentPid} dead)${CONFIG.colors.reset}`);
1968
+ // Mark as stopped so it can be reclaimed
1969
+ lockData.status = 'stopped';
1970
+ lockData.agentPid = null;
1971
+ lockData.agentStopped = new Date().toISOString();
1972
+ fs.writeFileSync(lockPath, JSON.stringify(lockData, null, 2));
1973
+ return lockData;
1974
+ }
1975
+ }
1976
+ } catch (e) {
1977
+ // Invalid lock file
1728
1978
  }
1729
1979
  }
1730
1980
 
@@ -1745,10 +1995,11 @@ The DevOps agent is monitoring this worktree for changes.
1745
1995
  const instructions = this.generateClaudeInstructions(session);
1746
1996
  // Don't display instructions here - they'll be shown after agent starts
1747
1997
 
1748
- return {
1749
- ...session,
1750
- instructions: instructions
1751
- };
1998
+ // Add instructions to session object and save to lock file
1999
+ session.instructions = instructions;
2000
+ fs.writeFileSync(lockFile, JSON.stringify(session, null, 2));
2001
+
2002
+ return session;
1752
2003
  }
1753
2004
 
1754
2005
  /**
@@ -1759,7 +2010,7 @@ The DevOps agent is monitoring this worktree for changes.
1759
2010
 
1760
2011
  if (!fs.existsSync(lockFile)) {
1761
2012
  console.error(`${CONFIG.colors.red}Session not found: ${sessionId}${CONFIG.colors.reset}`);
1762
- return;
2013
+ return false;
1763
2014
  }
1764
2015
 
1765
2016
  const sessionData = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
@@ -1794,7 +2045,11 @@ The DevOps agent is monitoring this worktree for changes.
1794
2045
  AC_DATE_STYLE: process.env.AC_DATE_STYLE || 'dash', // Preserve date style
1795
2046
  // Apply version configuration if set
1796
2047
  ...(projectSettings.versioningStrategy?.prefix && { AC_VERSION_PREFIX: projectSettings.versioningStrategy.prefix }),
1797
- ...(projectSettings.versioningStrategy?.startMinor && { AC_VERSION_START_MINOR: projectSettings.versioningStrategy.startMinor.toString() })
2048
+ ...(projectSettings.versioningStrategy?.startMinor && { AC_VERSION_START_MINOR: projectSettings.versioningStrategy.startMinor.toString() }),
2049
+
2050
+ // Rebase configuration
2051
+ AC_REBASE_INTERVAL: (sessionData.rebaseInterval || 0).toString(),
2052
+ AC_BASE_BRANCH: sessionData.baseBranch || 'HEAD' // We need to pass the base branch for rebasing
1798
2053
  };
1799
2054
 
1800
2055
  const agentScript = path.join(__dirname, 'cs-devops-agent-worker.js');
@@ -1812,16 +2067,6 @@ The DevOps agent is monitoring this worktree for changes.
1812
2067
  silent: false
1813
2068
  });
1814
2069
 
1815
- // Wait for agent to initialize and display its interactive commands
1816
- // Then show the copy-paste instructions
1817
- setTimeout(async () => {
1818
- console.log('\n'); // Add spacing
1819
-
1820
- // Generate and display instructions
1821
- const instructions = this.generateClaudeInstructions(sessionData);
1822
- this.displayInstructions(instructions, sessionId, sessionData.task);
1823
- }, 3000); // Wait 3 seconds for agent to show interactive commands
1824
-
1825
2070
  child.on('exit', (code) => {
1826
2071
  console.log(`${CONFIG.colors.yellow}Agent exited with code: ${code}${CONFIG.colors.reset}`);
1827
2072
 
@@ -1837,6 +2082,8 @@ The DevOps agent is monitoring this worktree for changes.
1837
2082
  child.kill('SIGINT');
1838
2083
  setTimeout(() => process.exit(0), 1000);
1839
2084
  });
2085
+
2086
+ return true;
1840
2087
  }
1841
2088
 
1842
2089
  /**
@@ -2171,8 +2418,114 @@ The DevOps agent is monitoring this worktree for changes.
2171
2418
  }
2172
2419
 
2173
2420
  /**
2174
- * Clean up all stale sessions and worktrees
2421
+ * Recover sessions from existing worktrees that are missing lock files
2175
2422
  */
2423
+ async recoverSessions() {
2424
+ console.log(`\n${CONFIG.colors.yellow}Scanning for recoverable sessions...${CONFIG.colors.reset}`);
2425
+
2426
+ if (!fs.existsSync(this.worktreesPath)) {
2427
+ console.log('No worktrees directory found.');
2428
+ return 0;
2429
+ }
2430
+
2431
+ const worktrees = fs.readdirSync(this.worktreesPath);
2432
+ let recovered = 0;
2433
+
2434
+ for (const dir of worktrees) {
2435
+ // Skip .DS_Store and other system files
2436
+ if (dir.startsWith('.')) continue;
2437
+
2438
+ const worktreePath = path.join(this.worktreesPath, dir);
2439
+
2440
+ // Ensure it's a directory
2441
+ try {
2442
+ if (!fs.statSync(worktreePath).isDirectory()) continue;
2443
+ } catch (e) { continue; }
2444
+
2445
+ const configPath = path.join(worktreePath, '.devops-session.json');
2446
+
2447
+ if (fs.existsSync(configPath)) {
2448
+ try {
2449
+ const sessionData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
2450
+
2451
+ if (!sessionData.sessionId) continue;
2452
+
2453
+ const lockFile = path.join(this.locksPath, `${sessionData.sessionId}.lock`);
2454
+
2455
+ if (!fs.existsSync(lockFile)) {
2456
+ // Restore lock file
2457
+ // Reset status to 'stopped' so it can be resumed/claimed
2458
+ sessionData.status = 'stopped';
2459
+ sessionData.agentPid = null;
2460
+ sessionData.agentStopped = new Date().toISOString();
2461
+ sessionData.recoveredAt = new Date().toISOString();
2462
+
2463
+ fs.writeFileSync(lockFile, JSON.stringify(sessionData, null, 2));
2464
+ console.log(`${CONFIG.colors.green}✓ Recovered session ${sessionData.sessionId} (${sessionData.task})${CONFIG.colors.reset}`);
2465
+ recovered++;
2466
+
2467
+ // Check for uncommitted changes in the recovered session
2468
+ try {
2469
+ const status = execSync(`git -C "${worktreePath}" status --porcelain`, { encoding: 'utf8' });
2470
+ if (status.trim()) {
2471
+ console.log(`\n${CONFIG.colors.yellow}Uncommitted changes found in recovered session ${sessionData.sessionId}${CONFIG.colors.reset}`);
2472
+
2473
+ const rl = readline.createInterface({
2474
+ input: process.stdin,
2475
+ output: process.stdout
2476
+ });
2477
+
2478
+ const commitNow = await new Promise(resolve => {
2479
+ rl.question('Would you like to commit these changes now? (Y/n): ', answer => {
2480
+ rl.close();
2481
+ resolve(answer.toLowerCase() !== 'n' && answer.toLowerCase() !== 'no');
2482
+ });
2483
+ });
2484
+
2485
+ if (commitNow) {
2486
+ const timestamp = new Date().toISOString();
2487
+ execSync(`git -C "${worktreePath}" add -A`, { stdio: 'ignore' });
2488
+ execSync(`git -C "${worktreePath}" commit -m "chore: recovered session auto-commit at ${timestamp}"`, { stdio: 'ignore' });
2489
+ console.log(`${CONFIG.colors.green}✓ Changes committed.${CONFIG.colors.reset}`);
2490
+
2491
+ // Ask to push
2492
+ const rlPush = readline.createInterface({ input: process.stdin, output: process.stdout });
2493
+ const pushNow = await new Promise(resolve => {
2494
+ rlPush.question('Push changes to remote? (Y/n): ', answer => {
2495
+ rlPush.close();
2496
+ resolve(answer.toLowerCase() !== 'n' && answer.toLowerCase() !== 'no');
2497
+ });
2498
+ });
2499
+
2500
+ if (pushNow) {
2501
+ try {
2502
+ execSync(`git -C "${worktreePath}" push origin ${sessionData.branchName}`, { stdio: 'ignore' });
2503
+ console.log(`${CONFIG.colors.green}✓ Changes pushed to ${sessionData.branchName}.${CONFIG.colors.reset}`);
2504
+ } catch (e) {
2505
+ console.log(`${CONFIG.colors.red}✗ Push failed. You may need to pull first or check remote.${CONFIG.colors.reset}`);
2506
+ }
2507
+ }
2508
+ }
2509
+ }
2510
+ } catch (e) {
2511
+ // Ignore git errors during recovery scan
2512
+ }
2513
+ }
2514
+ } catch (err) {
2515
+ console.error(`Failed to recover ${dir}: ${err.message}`);
2516
+ }
2517
+ }
2518
+ }
2519
+
2520
+ if (recovered === 0) {
2521
+ console.log('No orphaned sessions found to recover.');
2522
+ } else {
2523
+ console.log(`\n${CONFIG.colors.green}Recovered ${recovered} sessions. You can now resume them.${CONFIG.colors.reset}`);
2524
+ }
2525
+
2526
+ return recovered;
2527
+ }
2528
+
2176
2529
  async cleanupAll() {
2177
2530
  console.log(`\n${CONFIG.colors.yellow}Cleaning up stale sessions and worktrees...${CONFIG.colors.reset}`);
2178
2531
 
@@ -2248,7 +2601,7 @@ async function main() {
2248
2601
  console.log(" CS_DevOpsAgent - Intelligent Git Automation System");
2249
2602
  console.log(` Version ${packageJson.version} | Build ${new Date().toISOString().split('T')[0].replace(/-/g, '')}`);
2250
2603
  console.log(" ");
2251
- console.log(" Copyright (c) 2024 SecondBrain Labs");
2604
+ console.log(" Copyright (c) 2026 SeKondBrain AI Labs Limited");
2252
2605
  console.log(" Author: Sachin Dev Duggal");
2253
2606
  console.log(" ");
2254
2607
  console.log(" Licensed under the MIT License");
@@ -2281,8 +2634,36 @@ async function main() {
2281
2634
  // Start agent for a session
2282
2635
  const sessionId = args[1];
2283
2636
  if (!sessionId) {
2637
+ // Ask if user wants Kora assistance
2638
+ const koraRl = readline.createInterface({
2639
+ input: process.stdin,
2640
+ output: process.stdout
2641
+ });
2642
+
2643
+ console.log(`\n${CONFIG.colors.magenta}🤖 Kora AI Assistant Available${CONFIG.colors.reset}`);
2644
+ const useKora = await new Promise(resolve => {
2645
+ koraRl.question(`Would you like Kora to guide you? (Y/n): `, answer => {
2646
+ koraRl.close();
2647
+ resolve(answer.toLowerCase() !== 'n' && answer.toLowerCase() !== 'no');
2648
+ });
2649
+ });
2650
+
2651
+ if (useKora) {
2652
+ console.log(`\n${CONFIG.colors.magenta}Launching Kora...${CONFIG.colors.reset}`);
2653
+ const chatScript = path.join(__dirname, 'agent-chat.js');
2654
+ const child = spawn('node', [chatScript], {
2655
+ stdio: 'inherit',
2656
+ env: process.env
2657
+ });
2658
+
2659
+ child.on('exit', (code) => {
2660
+ process.exit(code);
2661
+ });
2662
+ return; // Hand off to Kora
2663
+ }
2664
+
2284
2665
  // No session ID provided - show interactive menu
2285
- console.log(`${CONFIG.colors.bright}DevOps Agent Session Manager${CONFIG.colors.reset}\n`);
2666
+ console.log(`\n${CONFIG.colors.bright}DevOps Agent Session Manager${CONFIG.colors.reset}\n`);
2286
2667
 
2287
2668
  // Show existing sessions first
2288
2669
  const locks = fs.existsSync(coordinator.locksPath) ?
@@ -2376,7 +2757,10 @@ async function main() {
2376
2757
  args[args.indexOf('--agent') + 1] :
2377
2758
  undefined; // Pass undefined to trigger prompt in createSession
2378
2759
 
2379
- await coordinator.createAndStart({ task, agent });
2760
+ const skipSetup = args.includes('--skip-setup');
2761
+ const skipUpdate = args.includes('--skip-update');
2762
+
2763
+ await coordinator.createAndStart({ task, agent, skipSetup, skipUpdate });
2380
2764
  break;
2381
2765
  }
2382
2766
 
@@ -2386,6 +2770,39 @@ async function main() {
2386
2770
  await coordinator.requestSession(agent);
2387
2771
  break;
2388
2772
  }
2773
+
2774
+ case 'resume': {
2775
+ // Resume an existing session by ID or Task
2776
+ const sessionId = args.includes('--session-id') ?
2777
+ args[args.indexOf('--session-id') + 1] :
2778
+ undefined;
2779
+
2780
+ const task = args.includes('--task') ?
2781
+ args[args.indexOf('--task') + 1] :
2782
+ undefined;
2783
+
2784
+ let targetSessionId = sessionId;
2785
+
2786
+ if (!targetSessionId && task) {
2787
+ const session = coordinator.findSessionByTask(task);
2788
+ if (session) {
2789
+ targetSessionId = session.sessionId;
2790
+ }
2791
+ }
2792
+
2793
+ if (targetSessionId) {
2794
+ // Check if session is already active/claimed?
2795
+ // startAgent checks existence.
2796
+ const success = await coordinator.startAgent(targetSessionId);
2797
+ if (!success) process.exit(1);
2798
+ } else {
2799
+ console.error(`${CONFIG.colors.red}Error: Could not find session to resume.${CONFIG.colors.reset}`);
2800
+ if (task) console.error(`No session found matching task: ${task}`);
2801
+ else console.error(`Please provide --session-id or --task`);
2802
+ process.exit(1);
2803
+ }
2804
+ break;
2805
+ }
2389
2806
 
2390
2807
  case 'list': {
2391
2808
  coordinator.listSessions();
@@ -2404,11 +2821,23 @@ async function main() {
2404
2821
  break;
2405
2822
  }
2406
2823
 
2407
- case 'cleanup': {
2408
- // Clean up stale sessions and worktrees
2409
- await coordinator.cleanupAll();
2410
- break;
2411
- }
2824
+ case 'cleanup': {
2825
+ // Clean up stale sessions and worktrees
2826
+ await coordinator.cleanupAll();
2827
+ break;
2828
+ }
2829
+
2830
+ case 'recover': {
2831
+ // Recover orphaned sessions from worktrees
2832
+ await coordinator.recoverSessions();
2833
+ break;
2834
+ }
2835
+
2836
+ case 'recover': {
2837
+ // Recover orphaned sessions from worktrees
2838
+ await coordinator.recoverSessions();
2839
+ break;
2840
+ }
2412
2841
 
2413
2842
  case 'help':
2414
2843
  default: {