s9n-devops-agent 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/bin/cs-devops-agent +151 -0
- package/cleanup-sessions.sh +70 -0
- package/docs/PROJECT_INFO.md +115 -0
- package/docs/RELEASE_NOTES.md +189 -0
- package/docs/SESSION_MANAGEMENT.md +120 -0
- package/docs/TESTING.md +331 -0
- package/docs/houserules.md +267 -0
- package/docs/infrastructure.md +68 -0
- package/docs/testing-guide.md +224 -0
- package/package.json +68 -0
- package/src/agent-commands.js +211 -0
- package/src/claude-session-manager.js +488 -0
- package/src/close-session.js +316 -0
- package/src/cs-devops-agent-worker.js +1660 -0
- package/src/run-with-agent.js +372 -0
- package/src/session-coordinator.js +1207 -0
- package/src/setup-cs-devops-agent.js +985 -0
- package/src/worktree-manager.js +768 -0
- package/start-devops-session.sh +299 -0
|
@@ -0,0 +1,1207 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* SESSION COORDINATOR - Foolproof Claude/Agent Handshake System
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* This coordinator ensures Claude/Cline and DevOps agents work in sync.
|
|
9
|
+
* It generates instructions for Claude and manages session allocation.
|
|
10
|
+
*
|
|
11
|
+
* WORKFLOW:
|
|
12
|
+
* 1. Start DevOps agent → generates session & instructions
|
|
13
|
+
* 2. Copy instructions to Claude/Cline
|
|
14
|
+
* 3. Claude follows instructions to use correct worktree
|
|
15
|
+
* 4. Agent monitors that worktree for changes
|
|
16
|
+
*
|
|
17
|
+
* ============================================================================
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import fs from 'fs';
|
|
21
|
+
import path from 'path';
|
|
22
|
+
import { execSync, spawn, fork } from 'child_process';
|
|
23
|
+
import { fileURLToPath } from 'url';
|
|
24
|
+
import { dirname } from 'path';
|
|
25
|
+
import crypto from 'crypto';
|
|
26
|
+
import readline from 'readline';
|
|
27
|
+
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
29
|
+
const __dirname = dirname(__filename);
|
|
30
|
+
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// CONFIGURATION
|
|
33
|
+
// ============================================================================
|
|
34
|
+
|
|
35
|
+
const CONFIG = {
|
|
36
|
+
sessionsDir: 'local_deploy/sessions',
|
|
37
|
+
locksDir: 'local_deploy/session-locks',
|
|
38
|
+
worktreesDir: 'local_deploy/worktrees',
|
|
39
|
+
instructionsDir: 'local_deploy/instructions',
|
|
40
|
+
colors: {
|
|
41
|
+
reset: '\x1b[0m',
|
|
42
|
+
bright: '\x1b[1m',
|
|
43
|
+
dim: '\x1b[2m',
|
|
44
|
+
green: '\x1b[32m',
|
|
45
|
+
yellow: '\x1b[33m',
|
|
46
|
+
blue: '\x1b[36m',
|
|
47
|
+
red: '\x1b[31m',
|
|
48
|
+
magenta: '\x1b[35m',
|
|
49
|
+
bgBlue: '\x1b[44m',
|
|
50
|
+
bgGreen: '\x1b[42m',
|
|
51
|
+
bgYellow: '\x1b[43m'
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// SESSION COORDINATOR CLASS
|
|
57
|
+
// ============================================================================
|
|
58
|
+
|
|
59
|
+
class SessionCoordinator {
|
|
60
|
+
constructor() {
|
|
61
|
+
this.repoRoot = this.getRepoRoot();
|
|
62
|
+
this.sessionsPath = path.join(this.repoRoot, CONFIG.sessionsDir);
|
|
63
|
+
this.locksPath = path.join(this.repoRoot, CONFIG.locksDir);
|
|
64
|
+
this.worktreesPath = path.join(this.repoRoot, CONFIG.worktreesDir);
|
|
65
|
+
this.instructionsPath = path.join(this.repoRoot, CONFIG.instructionsDir);
|
|
66
|
+
|
|
67
|
+
// Store user settings in home directory for cross-project usage
|
|
68
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
69
|
+
this.globalSettingsDir = path.join(homeDir, '.devops-agent');
|
|
70
|
+
this.globalSettingsPath = path.join(this.globalSettingsDir, 'settings.json');
|
|
71
|
+
|
|
72
|
+
// Store project-specific settings in local_deploy
|
|
73
|
+
this.projectSettingsPath = path.join(this.repoRoot, 'local_deploy', 'project-settings.json');
|
|
74
|
+
|
|
75
|
+
this.ensureDirectories();
|
|
76
|
+
this.cleanupStaleLocks();
|
|
77
|
+
this.ensureSettingsFile();
|
|
78
|
+
// DO NOT call ensureDeveloperInitials here - it should only be called when creating new sessions
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
getRepoRoot() {
|
|
82
|
+
try {
|
|
83
|
+
return execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim();
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error('Error: Not in a git repository');
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
ensureDirectories() {
|
|
91
|
+
// Ensure local project directories
|
|
92
|
+
[this.sessionsPath, this.locksPath, this.worktreesPath, this.instructionsPath].forEach(dir => {
|
|
93
|
+
if (!fs.existsSync(dir)) {
|
|
94
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Ensure global settings directory in home folder
|
|
99
|
+
if (!fs.existsSync(this.globalSettingsDir)) {
|
|
100
|
+
fs.mkdirSync(this.globalSettingsDir, { recursive: true });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
cleanupStaleLocks() {
|
|
105
|
+
// Clean up locks older than 1 hour
|
|
106
|
+
const oneHourAgo = Date.now() - 3600000;
|
|
107
|
+
|
|
108
|
+
if (fs.existsSync(this.locksPath)) {
|
|
109
|
+
const locks = fs.readdirSync(this.locksPath);
|
|
110
|
+
locks.forEach(lockFile => {
|
|
111
|
+
const lockPath = path.join(this.locksPath, lockFile);
|
|
112
|
+
const stats = fs.statSync(lockPath);
|
|
113
|
+
if (stats.mtimeMs < oneHourAgo) {
|
|
114
|
+
fs.unlinkSync(lockPath);
|
|
115
|
+
console.log(`${CONFIG.colors.dim}Cleaned stale lock: ${lockFile}${CONFIG.colors.reset}`);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Ensure developer initials are configured globally
|
|
123
|
+
*/
|
|
124
|
+
async ensureGlobalSetup() {
|
|
125
|
+
const globalSettings = this.loadGlobalSettings();
|
|
126
|
+
|
|
127
|
+
// Check if global setup is needed (developer initials)
|
|
128
|
+
if (!globalSettings.developerInitials || !globalSettings.configured) {
|
|
129
|
+
console.log(`\n${CONFIG.colors.yellow}First-time DevOps Agent setup!${CONFIG.colors.reset}`);
|
|
130
|
+
console.log(`${CONFIG.colors.bright}Please enter your 3-letter developer initials${CONFIG.colors.reset}`);
|
|
131
|
+
console.log(`${CONFIG.colors.dim}(These will be used in branch names across ALL projects)${CONFIG.colors.reset}`);
|
|
132
|
+
|
|
133
|
+
const initials = await this.promptForInitials();
|
|
134
|
+
globalSettings.developerInitials = initials.toLowerCase();
|
|
135
|
+
globalSettings.configured = true;
|
|
136
|
+
|
|
137
|
+
this.saveGlobalSettings(globalSettings);
|
|
138
|
+
|
|
139
|
+
console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Developer initials saved globally: ${CONFIG.colors.bright}${initials}${CONFIG.colors.reset}`);
|
|
140
|
+
console.log(`${CONFIG.colors.dim}Your initials are saved in ~/.devops-agent/settings.json${CONFIG.colors.reset}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Ensure project-specific version settings are configured
|
|
146
|
+
*/
|
|
147
|
+
async ensureProjectSetup() {
|
|
148
|
+
const projectSettings = this.loadProjectSettings();
|
|
149
|
+
|
|
150
|
+
// Check if project setup is needed (version strategy)
|
|
151
|
+
if (!projectSettings.versioningStrategy || !projectSettings.versioningStrategy.configured) {
|
|
152
|
+
console.log(`\n${CONFIG.colors.yellow}First-time project setup for this repository!${CONFIG.colors.reset}`);
|
|
153
|
+
console.log(`${CONFIG.colors.dim}Let's configure the versioning strategy for this project${CONFIG.colors.reset}`);
|
|
154
|
+
|
|
155
|
+
const versionInfo = await this.promptForStartingVersion();
|
|
156
|
+
projectSettings.versioningStrategy = {
|
|
157
|
+
prefix: versionInfo.prefix,
|
|
158
|
+
startMinor: versionInfo.startMinor,
|
|
159
|
+
dailyIncrement: versionInfo.dailyIncrement || 1,
|
|
160
|
+
configured: true
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
this.saveProjectSettings(projectSettings);
|
|
164
|
+
|
|
165
|
+
// Set environment variables for the current session
|
|
166
|
+
process.env.AC_VERSION_PREFIX = versionInfo.prefix;
|
|
167
|
+
process.env.AC_VERSION_START_MINOR = versionInfo.startMinor.toString();
|
|
168
|
+
process.env.AC_VERSION_INCREMENT = versionInfo.dailyIncrement.toString();
|
|
169
|
+
|
|
170
|
+
const incrementDisplay = (versionInfo.dailyIncrement / 100).toFixed(2);
|
|
171
|
+
console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Project versioning configured:`);
|
|
172
|
+
console.log(` Starting: ${CONFIG.colors.bright}${versionInfo.prefix}${versionInfo.startMinor}${CONFIG.colors.reset}`);
|
|
173
|
+
console.log(` Daily increment: ${CONFIG.colors.bright}${incrementDisplay}${CONFIG.colors.reset}`);
|
|
174
|
+
console.log(`${CONFIG.colors.dim}Settings saved in local_deploy/project-settings.json${CONFIG.colors.reset}`);
|
|
175
|
+
} else {
|
|
176
|
+
// Project already configured, set environment variables
|
|
177
|
+
process.env.AC_VERSION_PREFIX = projectSettings.versioningStrategy.prefix;
|
|
178
|
+
process.env.AC_VERSION_START_MINOR = projectSettings.versioningStrategy.startMinor.toString();
|
|
179
|
+
process.env.AC_VERSION_INCREMENT = (projectSettings.versioningStrategy.dailyIncrement || 1).toString();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get developer initials from settings (no prompting)
|
|
185
|
+
*/
|
|
186
|
+
getDeveloperInitials() {
|
|
187
|
+
const settings = this.loadSettings();
|
|
188
|
+
// Never prompt here, just return default if not configured
|
|
189
|
+
return settings.developerInitials || 'dev';
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Ensure settings files exist
|
|
194
|
+
*/
|
|
195
|
+
ensureSettingsFile() {
|
|
196
|
+
// Create global settings if not exists
|
|
197
|
+
if (!fs.existsSync(this.globalSettingsPath)) {
|
|
198
|
+
const defaultGlobalSettings = {
|
|
199
|
+
developerInitials: "",
|
|
200
|
+
email: "",
|
|
201
|
+
preferences: {
|
|
202
|
+
defaultTargetBranch: "main",
|
|
203
|
+
pushOnCommit: true,
|
|
204
|
+
verboseLogging: false
|
|
205
|
+
},
|
|
206
|
+
configured: false
|
|
207
|
+
};
|
|
208
|
+
fs.writeFileSync(this.globalSettingsPath, JSON.stringify(defaultGlobalSettings, null, 2));
|
|
209
|
+
console.log(`${CONFIG.colors.dim}Created global settings at ~/.devops-agent/settings.json${CONFIG.colors.reset}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Create project settings if not exists
|
|
213
|
+
if (!fs.existsSync(this.projectSettingsPath)) {
|
|
214
|
+
const defaultProjectSettings = {
|
|
215
|
+
versioningStrategy: {
|
|
216
|
+
prefix: "v0.",
|
|
217
|
+
startMinor: 20,
|
|
218
|
+
configured: false
|
|
219
|
+
},
|
|
220
|
+
autoMergeConfig: {
|
|
221
|
+
enabled: false,
|
|
222
|
+
targetBranch: "main",
|
|
223
|
+
strategy: "pull-request"
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
const projectDir = path.dirname(this.projectSettingsPath);
|
|
227
|
+
if (!fs.existsSync(projectDir)) {
|
|
228
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
229
|
+
}
|
|
230
|
+
fs.writeFileSync(this.projectSettingsPath, JSON.stringify(defaultProjectSettings, null, 2));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Load global settings (user-specific)
|
|
236
|
+
*/
|
|
237
|
+
loadGlobalSettings() {
|
|
238
|
+
if (fs.existsSync(this.globalSettingsPath)) {
|
|
239
|
+
return JSON.parse(fs.readFileSync(this.globalSettingsPath, 'utf8'));
|
|
240
|
+
}
|
|
241
|
+
return {
|
|
242
|
+
developerInitials: "",
|
|
243
|
+
email: "",
|
|
244
|
+
preferences: {},
|
|
245
|
+
configured: false
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Load project settings
|
|
251
|
+
*/
|
|
252
|
+
loadProjectSettings() {
|
|
253
|
+
if (fs.existsSync(this.projectSettingsPath)) {
|
|
254
|
+
return JSON.parse(fs.readFileSync(this.projectSettingsPath, 'utf8'));
|
|
255
|
+
}
|
|
256
|
+
return {
|
|
257
|
+
versioningStrategy: {
|
|
258
|
+
prefix: "v0.",
|
|
259
|
+
startMinor: 20,
|
|
260
|
+
configured: false
|
|
261
|
+
},
|
|
262
|
+
autoMergeConfig: {}
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Combined settings loader for compatibility
|
|
268
|
+
*/
|
|
269
|
+
loadSettings() {
|
|
270
|
+
const global = this.loadGlobalSettings();
|
|
271
|
+
const project = this.loadProjectSettings();
|
|
272
|
+
return {
|
|
273
|
+
...global,
|
|
274
|
+
...project,
|
|
275
|
+
developerInitials: global.developerInitials,
|
|
276
|
+
configured: global.configured
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Save global settings
|
|
282
|
+
*/
|
|
283
|
+
saveGlobalSettings(settings) {
|
|
284
|
+
fs.writeFileSync(this.globalSettingsPath, JSON.stringify(settings, null, 2));
|
|
285
|
+
console.log(`${CONFIG.colors.dim}Global settings saved to ~/.devops-agent/settings.json${CONFIG.colors.reset}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Save project settings
|
|
290
|
+
*/
|
|
291
|
+
saveProjectSettings(settings) {
|
|
292
|
+
const projectDir = path.dirname(this.projectSettingsPath);
|
|
293
|
+
if (!fs.existsSync(projectDir)) {
|
|
294
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
295
|
+
}
|
|
296
|
+
fs.writeFileSync(this.projectSettingsPath, JSON.stringify(settings, null, 2));
|
|
297
|
+
console.log(`${CONFIG.colors.dim}Project settings saved to local_deploy/project-settings.json${CONFIG.colors.reset}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Save settings (splits between global and project)
|
|
302
|
+
*/
|
|
303
|
+
saveSettings(settings) {
|
|
304
|
+
// Split settings into global and project
|
|
305
|
+
const globalSettings = {
|
|
306
|
+
developerInitials: settings.developerInitials,
|
|
307
|
+
email: settings.email || "",
|
|
308
|
+
preferences: settings.preferences || {},
|
|
309
|
+
configured: settings.configured
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const projectSettings = {
|
|
313
|
+
versioningStrategy: settings.versioningStrategy,
|
|
314
|
+
autoMergeConfig: settings.autoMergeConfig || {}
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
this.saveGlobalSettings(globalSettings);
|
|
318
|
+
this.saveProjectSettings(projectSettings);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Prompt for developer initials
|
|
323
|
+
*/
|
|
324
|
+
promptForInitials() {
|
|
325
|
+
const rl = readline.createInterface({
|
|
326
|
+
input: process.stdin,
|
|
327
|
+
output: process.stdout
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
return new Promise((resolve) => {
|
|
331
|
+
const askInitials = () => {
|
|
332
|
+
rl.question('Developer initials (3 letters): ', (answer) => {
|
|
333
|
+
const initials = answer.trim();
|
|
334
|
+
if (initials.length !== 3) {
|
|
335
|
+
console.log(`${CONFIG.colors.red}Please enter exactly 3 letters${CONFIG.colors.reset}`);
|
|
336
|
+
askInitials();
|
|
337
|
+
} else if (!/^[a-zA-Z]+$/.test(initials)) {
|
|
338
|
+
console.log(`${CONFIG.colors.red}Please use only letters${CONFIG.colors.reset}`);
|
|
339
|
+
askInitials();
|
|
340
|
+
} else {
|
|
341
|
+
rl.close();
|
|
342
|
+
resolve(initials);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
345
|
+
};
|
|
346
|
+
askInitials();
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Get list of available branches
|
|
352
|
+
*/
|
|
353
|
+
getAvailableBranches() {
|
|
354
|
+
try {
|
|
355
|
+
const result = execSync('git branch -a --format="%(refname:short)"', {
|
|
356
|
+
cwd: this.repoRoot,
|
|
357
|
+
encoding: 'utf8'
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
return result.split('\n')
|
|
361
|
+
.filter(branch => branch.trim())
|
|
362
|
+
.filter(branch => !branch.includes('HEAD'))
|
|
363
|
+
.map(branch => branch.replace('origin/', ''));
|
|
364
|
+
} catch (error) {
|
|
365
|
+
return ['main', 'develop', 'master'];
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Prompt for merge configuration
|
|
371
|
+
*/
|
|
372
|
+
async promptForMergeConfig() {
|
|
373
|
+
const rl = readline.createInterface({
|
|
374
|
+
input: process.stdin,
|
|
375
|
+
output: process.stdout
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
console.log(`\n${CONFIG.colors.yellow}═══ Auto-merge Configuration ═══${CONFIG.colors.reset}`);
|
|
379
|
+
console.log(`${CONFIG.colors.dim}(Automatically merge today's work into a target branch)${CONFIG.colors.reset}`);
|
|
380
|
+
|
|
381
|
+
// Ask if they want auto-merge
|
|
382
|
+
const autoMerge = await new Promise((resolve) => {
|
|
383
|
+
rl.question('\nEnable auto-merge at end of day? (y/N): ', (answer) => {
|
|
384
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
if (!autoMerge) {
|
|
389
|
+
rl.close();
|
|
390
|
+
console.log(`${CONFIG.colors.dim}Auto-merge disabled. You'll need to manually merge your work.${CONFIG.colors.reset}`);
|
|
391
|
+
return { autoMerge: false };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Get available branches
|
|
395
|
+
const branches = this.getAvailableBranches();
|
|
396
|
+
const uniqueBranches = [...new Set(branches)].slice(0, 10); // Show max 10 branches
|
|
397
|
+
|
|
398
|
+
console.log(`\n${CONFIG.colors.bright}Which branch should today's work be merged INTO?${CONFIG.colors.reset}`);
|
|
399
|
+
console.log(`${CONFIG.colors.dim}(e.g., main, develop, v2.0, feature/xyz)${CONFIG.colors.reset}\n`);
|
|
400
|
+
|
|
401
|
+
console.log(`${CONFIG.colors.bright}Available branches:${CONFIG.colors.reset}`);
|
|
402
|
+
uniqueBranches.forEach((branch, index) => {
|
|
403
|
+
const isDefault = branch === 'main' || branch === 'master' || branch === 'develop';
|
|
404
|
+
const marker = isDefault ? ` ${CONFIG.colors.green}⭐ (recommended)${CONFIG.colors.reset}` : '';
|
|
405
|
+
console.log(` ${index + 1}) ${branch}${marker}`);
|
|
406
|
+
});
|
|
407
|
+
console.log(` 0) Enter a different branch name`);
|
|
408
|
+
|
|
409
|
+
// Ask for target branch
|
|
410
|
+
const targetBranch = await new Promise((resolve) => {
|
|
411
|
+
rl.question(`\nSelect target branch to merge INTO (1-${uniqueBranches.length}, or 0): `, async (answer) => {
|
|
412
|
+
const choice = parseInt(answer);
|
|
413
|
+
if (choice === 0) {
|
|
414
|
+
rl.question('Enter custom branch name: ', (customBranch) => {
|
|
415
|
+
resolve(customBranch.trim());
|
|
416
|
+
});
|
|
417
|
+
} else if (choice >= 1 && choice <= uniqueBranches.length) {
|
|
418
|
+
resolve(uniqueBranches[choice - 1]);
|
|
419
|
+
} else {
|
|
420
|
+
resolve('main'); // Default to main if invalid choice
|
|
421
|
+
}
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Ask for merge strategy
|
|
426
|
+
console.log(`\n${CONFIG.colors.bright}Merge strategy:${CONFIG.colors.reset}`);
|
|
427
|
+
console.log(` 1) Create pull request (recommended)`);
|
|
428
|
+
console.log(` 2) Direct merge (when tests pass)`);
|
|
429
|
+
console.log(` 3) Squash and merge`);
|
|
430
|
+
|
|
431
|
+
const strategy = await new Promise((resolve) => {
|
|
432
|
+
rl.question('Select merge strategy (1-3) [1]: ', (answer) => {
|
|
433
|
+
const choice = parseInt(answer) || 1;
|
|
434
|
+
switch(choice) {
|
|
435
|
+
case 2:
|
|
436
|
+
resolve('direct');
|
|
437
|
+
break;
|
|
438
|
+
case 3:
|
|
439
|
+
resolve('squash');
|
|
440
|
+
break;
|
|
441
|
+
default:
|
|
442
|
+
resolve('pull-request');
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
rl.close();
|
|
448
|
+
|
|
449
|
+
const config = {
|
|
450
|
+
autoMerge: true,
|
|
451
|
+
targetBranch,
|
|
452
|
+
strategy,
|
|
453
|
+
requireTests: strategy !== 'pull-request'
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
console.log(`\n${CONFIG.colors.green}✓${CONFIG.colors.reset} Auto-merge configuration saved:`);
|
|
457
|
+
console.log(` ${CONFIG.colors.bright}Today's work${CONFIG.colors.reset} → ${CONFIG.colors.bright}${targetBranch}${CONFIG.colors.reset}`);
|
|
458
|
+
console.log(` Strategy: ${CONFIG.colors.bright}${strategy}${CONFIG.colors.reset}`);
|
|
459
|
+
console.log(`${CONFIG.colors.dim} (Daily branches will be merged into ${targetBranch} at end of day)${CONFIG.colors.reset}`);
|
|
460
|
+
|
|
461
|
+
return config;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Prompt for starting version configuration
|
|
466
|
+
*/
|
|
467
|
+
async promptForStartingVersion() {
|
|
468
|
+
const rl = readline.createInterface({
|
|
469
|
+
input: process.stdin,
|
|
470
|
+
output: process.stdout
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
console.log(`\n${CONFIG.colors.yellow}Version Configuration${CONFIG.colors.reset}`);
|
|
474
|
+
console.log(`${CONFIG.colors.dim}Set the starting version for this codebase${CONFIG.colors.reset}`);
|
|
475
|
+
|
|
476
|
+
// Ask if inheriting existing codebase
|
|
477
|
+
const isInherited = await new Promise((resolve) => {
|
|
478
|
+
rl.question('\nIs this an existing/inherited codebase? (y/N): ', (answer) => {
|
|
479
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
let prefix = 'v0.';
|
|
484
|
+
let startMinor = 20; // Default v0.20
|
|
485
|
+
let dailyIncrement = 1; // Default 0.01 per day
|
|
486
|
+
|
|
487
|
+
if (isInherited) {
|
|
488
|
+
console.log(`\n${CONFIG.colors.bright}Current Version Examples:${CONFIG.colors.reset}`);
|
|
489
|
+
console.log(' v1.5 → Enter: v1. and 50');
|
|
490
|
+
console.log(' v2.3 → Enter: v2. and 30');
|
|
491
|
+
console.log(' v0.8 → Enter: v0. and 80');
|
|
492
|
+
console.log(' v3.12 → Enter: v3. and 120');
|
|
493
|
+
|
|
494
|
+
// Get version prefix
|
|
495
|
+
prefix = await new Promise((resolve) => {
|
|
496
|
+
rl.question('\nEnter version prefix (e.g., v1., v2., v0.) [v0.]: ', (answer) => {
|
|
497
|
+
const cleaned = answer.trim() || 'v0.';
|
|
498
|
+
// Ensure it ends with a dot
|
|
499
|
+
resolve(cleaned.endsWith('.') ? cleaned : cleaned + '.');
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// Get starting minor version
|
|
504
|
+
const currentVersion = await new Promise((resolve) => {
|
|
505
|
+
rl.question(`Current version number (e.g., for ${prefix}5 enter 50, for ${prefix}12 enter 120) [20]: `, (answer) => {
|
|
506
|
+
const num = parseInt(answer.trim());
|
|
507
|
+
resolve(isNaN(num) ? 20 : num);
|
|
508
|
+
});
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// Next version will be current + 1
|
|
512
|
+
startMinor = currentVersion + 1;
|
|
513
|
+
|
|
514
|
+
console.log(`\n${CONFIG.colors.green}✓${CONFIG.colors.reset} Next version will be: ${CONFIG.colors.bright}${prefix}${startMinor}${CONFIG.colors.reset}`);
|
|
515
|
+
console.log(`${CONFIG.colors.dim}(This represents ${prefix}${(startMinor/100).toFixed(2)} in semantic versioning)${CONFIG.colors.reset}`);
|
|
516
|
+
} else {
|
|
517
|
+
// New project
|
|
518
|
+
console.log(`\n${CONFIG.colors.green}✓${CONFIG.colors.reset} Starting new project at: ${CONFIG.colors.bright}v0.20${CONFIG.colors.reset}`);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Ask for daily increment preference
|
|
522
|
+
console.log(`\n${CONFIG.colors.yellow}Daily Version Increment${CONFIG.colors.reset}`);
|
|
523
|
+
console.log(`${CONFIG.colors.dim}How much should the version increment each day?${CONFIG.colors.reset}`);
|
|
524
|
+
console.log(' 1) 0.01 per day (v0.20 → v0.21 → v0.22) [default]');
|
|
525
|
+
console.log(' 2) 0.1 per day (v0.20 → v0.30 → v0.40)');
|
|
526
|
+
console.log(' 3) 0.2 per day (v0.20 → v0.40 → v0.60)');
|
|
527
|
+
console.log(' 4) Custom increment');
|
|
528
|
+
|
|
529
|
+
const incrementChoice = await new Promise((resolve) => {
|
|
530
|
+
rl.question('\nSelect increment (1-4) [1]: ', (answer) => {
|
|
531
|
+
const choice = parseInt(answer.trim()) || 1;
|
|
532
|
+
resolve(choice);
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
switch (incrementChoice) {
|
|
537
|
+
case 2:
|
|
538
|
+
dailyIncrement = 10; // 0.1
|
|
539
|
+
break;
|
|
540
|
+
case 3:
|
|
541
|
+
dailyIncrement = 20; // 0.2
|
|
542
|
+
break;
|
|
543
|
+
case 4:
|
|
544
|
+
dailyIncrement = await new Promise((resolve) => {
|
|
545
|
+
rl.question('Enter increment value (e.g., 5 for 0.05, 25 for 0.25): ', (answer) => {
|
|
546
|
+
const value = parseInt(answer.trim());
|
|
547
|
+
resolve(isNaN(value) || value <= 0 ? 1 : value);
|
|
548
|
+
});
|
|
549
|
+
});
|
|
550
|
+
break;
|
|
551
|
+
default:
|
|
552
|
+
dailyIncrement = 1; // 0.01
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const incrementDisplay = (dailyIncrement / 100).toFixed(2);
|
|
556
|
+
console.log(`\n${CONFIG.colors.green}✓${CONFIG.colors.reset} Daily increment set to: ${CONFIG.colors.bright}${incrementDisplay}${CONFIG.colors.reset}`);
|
|
557
|
+
console.log(`${CONFIG.colors.dim}(${prefix}${startMinor} → ${prefix}${startMinor + dailyIncrement} → ${prefix}${startMinor + dailyIncrement * 2}...)${CONFIG.colors.reset}`);
|
|
558
|
+
|
|
559
|
+
rl.close();
|
|
560
|
+
|
|
561
|
+
return {
|
|
562
|
+
prefix,
|
|
563
|
+
startMinor,
|
|
564
|
+
dailyIncrement
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
generateSessionId() {
|
|
569
|
+
const timestamp = Date.now().toString(36).slice(-4);
|
|
570
|
+
const random = crypto.randomBytes(2).toString('hex');
|
|
571
|
+
return `${timestamp}-${random}`;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Create a new session and generate Claude instructions
|
|
576
|
+
*/
|
|
577
|
+
async createSession(options = {}) {
|
|
578
|
+
// Ensure both global and project setup are complete
|
|
579
|
+
await this.ensureGlobalSetup(); // Developer initials (once per user)
|
|
580
|
+
await this.ensureProjectSetup(); // Version strategy (once per project)
|
|
581
|
+
|
|
582
|
+
const sessionId = this.generateSessionId();
|
|
583
|
+
const task = options.task || 'development';
|
|
584
|
+
const agentType = options.agent || 'claude';
|
|
585
|
+
const devInitials = this.getDeveloperInitials();
|
|
586
|
+
|
|
587
|
+
console.log(`\n${CONFIG.colors.bgBlue}${CONFIG.colors.bright} Creating New Session ${CONFIG.colors.reset}`);
|
|
588
|
+
console.log(`${CONFIG.colors.blue}Session ID:${CONFIG.colors.reset} ${CONFIG.colors.bright}${sessionId}${CONFIG.colors.reset}`);
|
|
589
|
+
console.log(`${CONFIG.colors.blue}Task:${CONFIG.colors.reset} ${task}`);
|
|
590
|
+
console.log(`${CONFIG.colors.blue}Agent:${CONFIG.colors.reset} ${agentType}`);
|
|
591
|
+
console.log(`${CONFIG.colors.blue}Developer:${CONFIG.colors.reset} ${devInitials}`);
|
|
592
|
+
|
|
593
|
+
// Ask for auto-merge configuration
|
|
594
|
+
const mergeConfig = await this.promptForMergeConfig();
|
|
595
|
+
|
|
596
|
+
// Create worktree with developer initials in the name
|
|
597
|
+
const worktreeName = `${agentType}-${devInitials}-${sessionId}-${task.replace(/\s+/g, '-')}`;
|
|
598
|
+
const worktreePath = path.join(this.worktreesPath, worktreeName);
|
|
599
|
+
const branchName = `${agentType}/${devInitials}/${sessionId}/${task.replace(/\s+/g, '-')}`;
|
|
600
|
+
|
|
601
|
+
try {
|
|
602
|
+
// Create worktree
|
|
603
|
+
console.log(`\n${CONFIG.colors.yellow}Creating worktree...${CONFIG.colors.reset}`);
|
|
604
|
+
execSync(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, { stdio: 'pipe' });
|
|
605
|
+
console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Worktree created at: ${worktreePath}`);
|
|
606
|
+
|
|
607
|
+
// Create session lock
|
|
608
|
+
const lockData = {
|
|
609
|
+
sessionId,
|
|
610
|
+
agentType,
|
|
611
|
+
task,
|
|
612
|
+
worktreePath,
|
|
613
|
+
branchName,
|
|
614
|
+
created: new Date().toISOString(),
|
|
615
|
+
status: 'active',
|
|
616
|
+
pid: process.pid,
|
|
617
|
+
developerInitials: devInitials,
|
|
618
|
+
mergeConfig: mergeConfig
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
const lockFile = path.join(this.locksPath, `${sessionId}.lock`);
|
|
622
|
+
fs.writeFileSync(lockFile, JSON.stringify(lockData, null, 2));
|
|
623
|
+
|
|
624
|
+
// Generate Claude instructions
|
|
625
|
+
const instructions = this.generateClaudeInstructions(lockData);
|
|
626
|
+
|
|
627
|
+
// Save instructions to file
|
|
628
|
+
const instructionsFile = path.join(this.instructionsPath, `${sessionId}.md`);
|
|
629
|
+
fs.writeFileSync(instructionsFile, instructions.markdown);
|
|
630
|
+
|
|
631
|
+
// Display instructions
|
|
632
|
+
this.displayInstructions(instructions, sessionId, task);
|
|
633
|
+
|
|
634
|
+
// Create session config in worktree
|
|
635
|
+
this.createWorktreeConfig(worktreePath, lockData);
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
sessionId,
|
|
639
|
+
worktreePath,
|
|
640
|
+
branchName,
|
|
641
|
+
lockFile,
|
|
642
|
+
instructionsFile,
|
|
643
|
+
instructions: instructions.plaintext
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
} catch (error) {
|
|
647
|
+
console.error(`${CONFIG.colors.red}Failed to create session: ${error.message}${CONFIG.colors.reset}`);
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Generate instructions for Claude/Cline
|
|
654
|
+
*/
|
|
655
|
+
generateClaudeInstructions(sessionData) {
|
|
656
|
+
const { sessionId, worktreePath, branchName, task } = sessionData;
|
|
657
|
+
|
|
658
|
+
const plaintext = `
|
|
659
|
+
SESSION_ID: ${sessionId}
|
|
660
|
+
WORKTREE: ${worktreePath}
|
|
661
|
+
BRANCH: ${branchName}
|
|
662
|
+
TASK: ${task}
|
|
663
|
+
|
|
664
|
+
INSTRUCTIONS:
|
|
665
|
+
1. Change to worktree directory: cd "${worktreePath}"
|
|
666
|
+
2. Verify branch: git branch --show-current
|
|
667
|
+
3. Make your changes for: ${task}
|
|
668
|
+
4. Write commit message to: .devops-commit-${sessionId}.msg
|
|
669
|
+
5. The DevOps agent will auto-commit and push your changes
|
|
670
|
+
`;
|
|
671
|
+
|
|
672
|
+
const markdown = `# DevOps Session Instructions
|
|
673
|
+
|
|
674
|
+
## Session Information
|
|
675
|
+
- **Session ID:** \`${sessionId}\`
|
|
676
|
+
- **Task:** ${task}
|
|
677
|
+
- **Worktree Path:** \`${worktreePath}\`
|
|
678
|
+
- **Branch:** \`${branchName}\`
|
|
679
|
+
|
|
680
|
+
## Instructions for Claude/Cline
|
|
681
|
+
|
|
682
|
+
### Step 1: Navigate to Your Worktree
|
|
683
|
+
\`\`\`bash
|
|
684
|
+
cd "${worktreePath}"
|
|
685
|
+
\`\`\`
|
|
686
|
+
|
|
687
|
+
### Step 2: Verify You're on the Correct Branch
|
|
688
|
+
\`\`\`bash
|
|
689
|
+
git branch --show-current
|
|
690
|
+
# Should output: ${branchName}
|
|
691
|
+
\`\`\`
|
|
692
|
+
|
|
693
|
+
### Step 3: Work on Your Task
|
|
694
|
+
Make changes for: **${task}**
|
|
695
|
+
|
|
696
|
+
### Step 4: Commit Your Changes
|
|
697
|
+
Write your commit message to the session-specific file:
|
|
698
|
+
\`\`\`bash
|
|
699
|
+
echo "feat: your commit message here" > .devops-commit-${sessionId}.msg
|
|
700
|
+
\`\`\`
|
|
701
|
+
|
|
702
|
+
### Step 5: Automatic Processing
|
|
703
|
+
The DevOps agent will automatically:
|
|
704
|
+
- Detect your changes
|
|
705
|
+
- Read your commit message
|
|
706
|
+
- Commit and push to the remote repository
|
|
707
|
+
- Clear the message file
|
|
708
|
+
|
|
709
|
+
## Session Status
|
|
710
|
+
- Created: ${new Date().toISOString()}
|
|
711
|
+
- Status: Active
|
|
712
|
+
- Agent: Monitoring
|
|
713
|
+
|
|
714
|
+
## Important Notes
|
|
715
|
+
- All changes should be made in the worktree directory
|
|
716
|
+
- Do not switch branches manually
|
|
717
|
+
- The agent is watching for changes in this specific worktree
|
|
718
|
+
`;
|
|
719
|
+
|
|
720
|
+
const shellCommand = `cd "${worktreePath}" && echo "Session ${sessionId} ready"`;
|
|
721
|
+
|
|
722
|
+
return {
|
|
723
|
+
plaintext,
|
|
724
|
+
markdown,
|
|
725
|
+
shellCommand,
|
|
726
|
+
worktreePath,
|
|
727
|
+
sessionId
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Display instructions in a user-friendly format
|
|
733
|
+
*/
|
|
734
|
+
displayInstructions(instructions, sessionId, task) {
|
|
735
|
+
console.log(`\n${CONFIG.colors.bgGreen}${CONFIG.colors.bright} Instructions for Claude/Cline ${CONFIG.colors.reset}\n`);
|
|
736
|
+
|
|
737
|
+
// Clean separator
|
|
738
|
+
console.log(`${CONFIG.colors.yellow}══════════════════════════════════════════════════════════════${CONFIG.colors.reset}`);
|
|
739
|
+
console.log(`${CONFIG.colors.bright}COPY AND PASTE THIS ENTIRE BLOCK INTO CLAUDE BEFORE YOUR PROMPT:${CONFIG.colors.reset}`);
|
|
740
|
+
console.log(`${CONFIG.colors.yellow}──────────────────────────────────────────────────────────────${CONFIG.colors.reset}`);
|
|
741
|
+
console.log();
|
|
742
|
+
|
|
743
|
+
// The actual copyable content - no colors inside
|
|
744
|
+
console.log(`I'm working in a DevOps-managed session with the following setup:`);
|
|
745
|
+
console.log(`- Session ID: ${sessionId}`);
|
|
746
|
+
console.log(`- Working Directory: ${instructions.worktreePath}`);
|
|
747
|
+
console.log(`- Task: ${task || 'development'}`);
|
|
748
|
+
console.log(``);
|
|
749
|
+
console.log(`Please switch to this directory before making any changes:`);
|
|
750
|
+
console.log(`cd "${instructions.worktreePath}"`);
|
|
751
|
+
console.log(``);
|
|
752
|
+
console.log(`Write commit messages to: .devops-commit-${sessionId}.msg`);
|
|
753
|
+
console.log(`The DevOps agent will automatically commit and push changes.`);
|
|
754
|
+
console.log();
|
|
755
|
+
|
|
756
|
+
console.log(`${CONFIG.colors.yellow}══════════════════════════════════════════════════════════════${CONFIG.colors.reset}`);
|
|
757
|
+
|
|
758
|
+
// Status info
|
|
759
|
+
console.log(`\n${CONFIG.colors.green}✓ DevOps agent is starting...${CONFIG.colors.reset}`);
|
|
760
|
+
console.log(`${CONFIG.colors.dim}Full instructions saved to: ${CONFIG.instructionsDir}/${sessionId}.md${CONFIG.colors.reset}`);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* Create configuration in the worktree
|
|
765
|
+
*/
|
|
766
|
+
createWorktreeConfig(worktreePath, sessionData) {
|
|
767
|
+
// Session config file
|
|
768
|
+
const configPath = path.join(worktreePath, '.devops-session.json');
|
|
769
|
+
fs.writeFileSync(configPath, JSON.stringify(sessionData, null, 2));
|
|
770
|
+
|
|
771
|
+
// Commit message file
|
|
772
|
+
const msgFile = path.join(worktreePath, `.devops-commit-${sessionData.sessionId}.msg`);
|
|
773
|
+
fs.writeFileSync(msgFile, '');
|
|
774
|
+
|
|
775
|
+
// VS Code settings
|
|
776
|
+
const vscodeDir = path.join(worktreePath, '.vscode');
|
|
777
|
+
if (!fs.existsSync(vscodeDir)) {
|
|
778
|
+
fs.mkdirSync(vscodeDir, { recursive: true });
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const settings = {
|
|
782
|
+
'window.title': `${sessionData.agentType.toUpperCase()} Session ${sessionData.sessionId} - ${sessionData.task}`,
|
|
783
|
+
'terminal.integrated.env.osx': {
|
|
784
|
+
'DEVOPS_SESSION_ID': sessionData.sessionId,
|
|
785
|
+
'DEVOPS_WORKTREE': path.basename(worktreePath),
|
|
786
|
+
'DEVOPS_BRANCH': sessionData.branchName,
|
|
787
|
+
'AC_MSG_FILE': `.devops-commit-${sessionData.sessionId}.msg`,
|
|
788
|
+
'AC_BRANCH_PREFIX': `${sessionData.agentType}_${sessionData.sessionId}_`
|
|
789
|
+
}
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
fs.writeFileSync(
|
|
793
|
+
path.join(vscodeDir, 'settings.json'),
|
|
794
|
+
JSON.stringify(settings, null, 2)
|
|
795
|
+
);
|
|
796
|
+
|
|
797
|
+
// Create a README for the session
|
|
798
|
+
const readme = `# DevOps Session: ${sessionData.sessionId}
|
|
799
|
+
|
|
800
|
+
## Task
|
|
801
|
+
${sessionData.task}
|
|
802
|
+
|
|
803
|
+
## Session Details
|
|
804
|
+
- **Session ID:** ${sessionData.sessionId}
|
|
805
|
+
- **Branch:** ${sessionData.branchName}
|
|
806
|
+
- **Created:** ${sessionData.created}
|
|
807
|
+
- **Agent Type:** ${sessionData.agentType}
|
|
808
|
+
|
|
809
|
+
## How to Use
|
|
810
|
+
1. Make your changes in this directory
|
|
811
|
+
2. Write commit message to: \`.devops-commit-${sessionData.sessionId}.msg\`
|
|
812
|
+
3. The DevOps agent will handle the rest
|
|
813
|
+
|
|
814
|
+
## Status
|
|
815
|
+
The DevOps agent is monitoring this worktree for changes.
|
|
816
|
+
`;
|
|
817
|
+
|
|
818
|
+
fs.writeFileSync(path.join(worktreePath, 'SESSION_README.md'), readme);
|
|
819
|
+
|
|
820
|
+
// Update .gitignore in the worktree to exclude session files
|
|
821
|
+
const gitignorePath = path.join(worktreePath, '.gitignore');
|
|
822
|
+
let gitignoreContent = '';
|
|
823
|
+
|
|
824
|
+
// Read existing gitignore if it exists
|
|
825
|
+
if (fs.existsSync(gitignorePath)) {
|
|
826
|
+
gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Session file patterns to ignore
|
|
830
|
+
const sessionPatterns = [
|
|
831
|
+
'# DevOps session management files',
|
|
832
|
+
'.devops-commit-*.msg',
|
|
833
|
+
'.devops-session.json',
|
|
834
|
+
'SESSION_README.md',
|
|
835
|
+
'.session-cleanup-requested',
|
|
836
|
+
'.worktree-session',
|
|
837
|
+
'.agent-config',
|
|
838
|
+
'.session-*',
|
|
839
|
+
'.devops-command-*'
|
|
840
|
+
];
|
|
841
|
+
|
|
842
|
+
// Check if we need to add patterns
|
|
843
|
+
let needsUpdate = false;
|
|
844
|
+
for (const pattern of sessionPatterns) {
|
|
845
|
+
if (!gitignoreContent.includes(pattern)) {
|
|
846
|
+
needsUpdate = true;
|
|
847
|
+
break;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
if (needsUpdate) {
|
|
852
|
+
// Add session patterns to gitignore
|
|
853
|
+
if (!gitignoreContent.endsWith('\n') && gitignoreContent.length > 0) {
|
|
854
|
+
gitignoreContent += '\n';
|
|
855
|
+
}
|
|
856
|
+
gitignoreContent += '\n' + sessionPatterns.join('\n') + '\n';
|
|
857
|
+
fs.writeFileSync(gitignorePath, gitignoreContent);
|
|
858
|
+
console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Updated .gitignore to exclude session files`);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
console.log(`${CONFIG.colors.dim}Session files created but not committed (they are gitignored)${CONFIG.colors.reset}`);
|
|
862
|
+
|
|
863
|
+
// Note: We do NOT commit these files - they're for session management only
|
|
864
|
+
// This prevents the "uncommitted changes" issue when starting sessions
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Request a session (for Claude to call)
|
|
869
|
+
*/
|
|
870
|
+
async requestSession(agentName = 'claude') {
|
|
871
|
+
console.log(`\n${CONFIG.colors.magenta}[${agentName.toUpperCase()}]${CONFIG.colors.reset} Requesting session...`);
|
|
872
|
+
|
|
873
|
+
// Check for available unlocked sessions
|
|
874
|
+
const availableSession = this.findAvailableSession();
|
|
875
|
+
|
|
876
|
+
if (availableSession) {
|
|
877
|
+
console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} Found available session: ${availableSession.sessionId}`);
|
|
878
|
+
return this.claimSession(availableSession, agentName);
|
|
879
|
+
} else {
|
|
880
|
+
console.log(`${CONFIG.colors.yellow}No available sessions. Creating new one...${CONFIG.colors.reset}`);
|
|
881
|
+
const task = await this.promptForTask();
|
|
882
|
+
return this.createSession({ task, agent: agentName });
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Find an available unclaimed session
|
|
888
|
+
*/
|
|
889
|
+
findAvailableSession() {
|
|
890
|
+
if (!fs.existsSync(this.locksPath)) {
|
|
891
|
+
return null;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const locks = fs.readdirSync(this.locksPath);
|
|
895
|
+
|
|
896
|
+
for (const lockFile of locks) {
|
|
897
|
+
const lockPath = path.join(this.locksPath, lockFile);
|
|
898
|
+
const lockData = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
899
|
+
|
|
900
|
+
// Check if session is available (not claimed)
|
|
901
|
+
if (lockData.status === 'waiting' && !lockData.claimedBy) {
|
|
902
|
+
return lockData;
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return null;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/**
|
|
910
|
+
* Claim a session for an agent
|
|
911
|
+
*/
|
|
912
|
+
claimSession(session, agentName) {
|
|
913
|
+
session.claimedBy = agentName;
|
|
914
|
+
session.claimedAt = new Date().toISOString();
|
|
915
|
+
session.status = 'active';
|
|
916
|
+
|
|
917
|
+
const lockFile = path.join(this.locksPath, `${session.sessionId}.lock`);
|
|
918
|
+
fs.writeFileSync(lockFile, JSON.stringify(session, null, 2));
|
|
919
|
+
|
|
920
|
+
const instructions = this.generateClaudeInstructions(session);
|
|
921
|
+
this.displayInstructions(instructions, session.sessionId, session.task);
|
|
922
|
+
|
|
923
|
+
return {
|
|
924
|
+
...session,
|
|
925
|
+
instructions: instructions.plaintext
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* Start the DevOps agent for a session
|
|
931
|
+
*/
|
|
932
|
+
async startAgent(sessionId, options = {}) {
|
|
933
|
+
const lockFile = path.join(this.locksPath, `${sessionId}.lock`);
|
|
934
|
+
|
|
935
|
+
if (!fs.existsSync(lockFile)) {
|
|
936
|
+
console.error(`${CONFIG.colors.red}Session not found: ${sessionId}${CONFIG.colors.reset}`);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const sessionData = JSON.parse(fs.readFileSync(lockFile, 'utf8'));
|
|
941
|
+
|
|
942
|
+
console.log(`\n${CONFIG.colors.bgYellow}${CONFIG.colors.bright} Starting DevOps Agent ${CONFIG.colors.reset}`);
|
|
943
|
+
console.log(`${CONFIG.colors.blue}Session:${CONFIG.colors.reset} ${sessionId}`);
|
|
944
|
+
console.log(`${CONFIG.colors.blue}Worktree:${CONFIG.colors.reset} ${sessionData.worktreePath}`);
|
|
945
|
+
console.log(`${CONFIG.colors.blue}Branch:${CONFIG.colors.reset} ${sessionData.branchName}`);
|
|
946
|
+
|
|
947
|
+
// Update session status
|
|
948
|
+
sessionData.agentStarted = new Date().toISOString();
|
|
949
|
+
sessionData.agentPid = process.pid;
|
|
950
|
+
fs.writeFileSync(lockFile, JSON.stringify(sessionData, null, 2));
|
|
951
|
+
|
|
952
|
+
// Get developer initials from session data or settings (NO PROMPTING HERE)
|
|
953
|
+
const devInitials = sessionData.developerInitials || this.getDeveloperInitials() || 'dev';
|
|
954
|
+
const settings = this.loadSettings();
|
|
955
|
+
const projectSettings = this.loadProjectSettings();
|
|
956
|
+
|
|
957
|
+
// Start the agent
|
|
958
|
+
const env = {
|
|
959
|
+
...process.env,
|
|
960
|
+
DEVOPS_SESSION_ID: sessionId,
|
|
961
|
+
AC_MSG_FILE: `.devops-commit-${sessionId}.msg`,
|
|
962
|
+
AC_BRANCH_PREFIX: `${sessionData.agentType}_${devInitials}_${sessionId}_`,
|
|
963
|
+
AC_WORKING_DIR: sessionData.worktreePath,
|
|
964
|
+
// Don't set AC_BRANCH - let the agent create daily branches within the worktree
|
|
965
|
+
// AC_BRANCH would force a static branch, preventing daily/weekly rollover
|
|
966
|
+
AC_PUSH: 'true', // Enable auto-push for session branches
|
|
967
|
+
AC_DAILY_PREFIX: `${sessionData.agentType}_${devInitials}_${sessionId}_`, // Daily branches with dev initials
|
|
968
|
+
AC_TZ: process.env.AC_TZ || 'Asia/Dubai', // Preserve timezone for daily branches
|
|
969
|
+
AC_DATE_STYLE: process.env.AC_DATE_STYLE || 'dash', // Preserve date style
|
|
970
|
+
// Apply version configuration if set
|
|
971
|
+
...(projectSettings.versioningStrategy?.prefix && { AC_VERSION_PREFIX: projectSettings.versioningStrategy.prefix }),
|
|
972
|
+
...(projectSettings.versioningStrategy?.startMinor && { AC_VERSION_START_MINOR: projectSettings.versioningStrategy.startMinor.toString() })
|
|
973
|
+
};
|
|
974
|
+
|
|
975
|
+
const agentScript = path.join(__dirname, 'cs-devops-agent-worker.js');
|
|
976
|
+
|
|
977
|
+
console.log(`\n${CONFIG.colors.green}Agent starting...${CONFIG.colors.reset}`);
|
|
978
|
+
console.log(`${CONFIG.colors.dim}Monitoring: ${sessionData.worktreePath}${CONFIG.colors.reset}`);
|
|
979
|
+
console.log(`${CONFIG.colors.dim}Message file: .devops-commit-${sessionId}.msg${CONFIG.colors.reset}`);
|
|
980
|
+
|
|
981
|
+
// Use fork for better Node.js script handling
|
|
982
|
+
// Fork automatically uses the same node executable and handles paths better
|
|
983
|
+
const child = fork(agentScript, [], {
|
|
984
|
+
cwd: sessionData.worktreePath,
|
|
985
|
+
env,
|
|
986
|
+
stdio: 'inherit',
|
|
987
|
+
silent: false
|
|
988
|
+
});
|
|
989
|
+
|
|
990
|
+
child.on('exit', (code) => {
|
|
991
|
+
console.log(`${CONFIG.colors.yellow}Agent exited with code: ${code}${CONFIG.colors.reset}`);
|
|
992
|
+
|
|
993
|
+
// Update session status
|
|
994
|
+
sessionData.agentStopped = new Date().toISOString();
|
|
995
|
+
sessionData.status = 'stopped';
|
|
996
|
+
fs.writeFileSync(lockFile, JSON.stringify(sessionData, null, 2));
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// Handle graceful shutdown
|
|
1000
|
+
process.on('SIGINT', () => {
|
|
1001
|
+
console.log(`\n${CONFIG.colors.yellow}Stopping agent...${CONFIG.colors.reset}`);
|
|
1002
|
+
child.kill('SIGINT');
|
|
1003
|
+
setTimeout(() => process.exit(0), 1000);
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
/**
|
|
1008
|
+
* List all sessions
|
|
1009
|
+
*/
|
|
1010
|
+
listSessions() {
|
|
1011
|
+
console.log(`\n${CONFIG.colors.bright}Active Sessions:${CONFIG.colors.reset}`);
|
|
1012
|
+
|
|
1013
|
+
if (!fs.existsSync(this.locksPath)) {
|
|
1014
|
+
console.log('No active sessions');
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
const locks = fs.readdirSync(this.locksPath);
|
|
1019
|
+
|
|
1020
|
+
if (locks.length === 0) {
|
|
1021
|
+
console.log('No active sessions');
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
locks.forEach(lockFile => {
|
|
1026
|
+
const lockPath = path.join(this.locksPath, lockFile);
|
|
1027
|
+
const session = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
1028
|
+
|
|
1029
|
+
const status = session.status === 'active' ?
|
|
1030
|
+
`${CONFIG.colors.green}●${CONFIG.colors.reset}` :
|
|
1031
|
+
`${CONFIG.colors.yellow}○${CONFIG.colors.reset}`;
|
|
1032
|
+
|
|
1033
|
+
console.log(`\n${status} ${CONFIG.colors.bright}${session.sessionId}${CONFIG.colors.reset}`);
|
|
1034
|
+
console.log(` Task: ${session.task}`);
|
|
1035
|
+
console.log(` Agent: ${session.agentType}`);
|
|
1036
|
+
console.log(` Branch: ${session.branchName}`);
|
|
1037
|
+
console.log(` Status: ${session.status}`);
|
|
1038
|
+
|
|
1039
|
+
if (session.claimedBy) {
|
|
1040
|
+
console.log(` Claimed by: ${session.claimedBy}`);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
if (session.agentPid) {
|
|
1044
|
+
console.log(` Agent PID: ${session.agentPid}`);
|
|
1045
|
+
}
|
|
1046
|
+
});
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Prompt for task name
|
|
1051
|
+
*/
|
|
1052
|
+
promptForTask() {
|
|
1053
|
+
const rl = readline.createInterface({
|
|
1054
|
+
input: process.stdin,
|
|
1055
|
+
output: process.stdout
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
return new Promise((resolve) => {
|
|
1059
|
+
rl.question('Enter task name: ', (answer) => {
|
|
1060
|
+
rl.close();
|
|
1061
|
+
resolve(answer || 'development');
|
|
1062
|
+
});
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
/**
|
|
1067
|
+
* Create a combined session (both create and start agent)
|
|
1068
|
+
*/
|
|
1069
|
+
async createAndStart(options = {}) {
|
|
1070
|
+
const session = await this.createSession(options);
|
|
1071
|
+
|
|
1072
|
+
console.log(`\n${CONFIG.colors.yellow}Starting agent for session ${session.sessionId}...${CONFIG.colors.reset}`);
|
|
1073
|
+
|
|
1074
|
+
// Wait a moment for user to see instructions
|
|
1075
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
1076
|
+
|
|
1077
|
+
await this.startAgent(session.sessionId);
|
|
1078
|
+
|
|
1079
|
+
return session;
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// ============================================================================
|
|
1084
|
+
// CLI INTERFACE
|
|
1085
|
+
// ============================================================================
|
|
1086
|
+
|
|
1087
|
+
async function main() {
|
|
1088
|
+
// Display copyright and license information immediately
|
|
1089
|
+
console.log();
|
|
1090
|
+
console.log("=".repeat(70));
|
|
1091
|
+
console.log();
|
|
1092
|
+
console.log(" CS_DevOpsAgent - Intelligent Git Automation System");
|
|
1093
|
+
console.log(" Version 2.4.0 | Build 20240930.1");
|
|
1094
|
+
console.log(" ");
|
|
1095
|
+
console.log(" Copyright (c) 2024 SecondBrain Labs");
|
|
1096
|
+
console.log(" Author: Sachin Dev Duggal");
|
|
1097
|
+
console.log(" ");
|
|
1098
|
+
console.log(" Licensed under the MIT License");
|
|
1099
|
+
console.log(" This software is provided 'as-is' without any warranty.");
|
|
1100
|
+
console.log(" See LICENSE file for full license text.");
|
|
1101
|
+
console.log("=".repeat(70));
|
|
1102
|
+
console.log();
|
|
1103
|
+
|
|
1104
|
+
const args = process.argv.slice(2);
|
|
1105
|
+
const command = args[0] || 'help';
|
|
1106
|
+
|
|
1107
|
+
const coordinator = new SessionCoordinator();
|
|
1108
|
+
|
|
1109
|
+
switch (command) {
|
|
1110
|
+
case 'create': {
|
|
1111
|
+
// Create a new session
|
|
1112
|
+
const task = args.includes('--task') ?
|
|
1113
|
+
args[args.indexOf('--task') + 1] :
|
|
1114
|
+
await coordinator.promptForTask();
|
|
1115
|
+
|
|
1116
|
+
const agent = args.includes('--agent') ?
|
|
1117
|
+
args[args.indexOf('--agent') + 1] :
|
|
1118
|
+
'claude';
|
|
1119
|
+
|
|
1120
|
+
await coordinator.createSession({ task, agent });
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
case 'start': {
|
|
1125
|
+
// Start agent for a session
|
|
1126
|
+
const sessionId = args[1];
|
|
1127
|
+
if (!sessionId) {
|
|
1128
|
+
console.error('Usage: start <session-id>');
|
|
1129
|
+
process.exit(1);
|
|
1130
|
+
}
|
|
1131
|
+
await coordinator.startAgent(sessionId);
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
case 'create-and-start': {
|
|
1136
|
+
// Create session and immediately start agent
|
|
1137
|
+
const task = args.includes('--task') ?
|
|
1138
|
+
args[args.indexOf('--task') + 1] :
|
|
1139
|
+
await coordinator.promptForTask();
|
|
1140
|
+
|
|
1141
|
+
const agent = args.includes('--agent') ?
|
|
1142
|
+
args[args.indexOf('--agent') + 1] :
|
|
1143
|
+
'claude';
|
|
1144
|
+
|
|
1145
|
+
await coordinator.createAndStart({ task, agent });
|
|
1146
|
+
break;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
case 'request': {
|
|
1150
|
+
// Request a session (for Claude to call)
|
|
1151
|
+
const agent = args[1] || 'claude';
|
|
1152
|
+
await coordinator.requestSession(agent);
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
case 'list': {
|
|
1157
|
+
coordinator.listSessions();
|
|
1158
|
+
break;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
case 'help':
|
|
1162
|
+
default: {
|
|
1163
|
+
console.log(`
|
|
1164
|
+
${CONFIG.colors.bright}DevOps Session Coordinator${CONFIG.colors.reset}
|
|
1165
|
+
|
|
1166
|
+
${CONFIG.colors.blue}Usage:${CONFIG.colors.reset}
|
|
1167
|
+
node session-coordinator.js <command> [options]
|
|
1168
|
+
|
|
1169
|
+
${CONFIG.colors.blue}Commands:${CONFIG.colors.reset}
|
|
1170
|
+
${CONFIG.colors.green}create${CONFIG.colors.reset} Create a new session and show instructions
|
|
1171
|
+
${CONFIG.colors.green}start <id>${CONFIG.colors.reset} Start DevOps agent for a session
|
|
1172
|
+
${CONFIG.colors.green}create-and-start${CONFIG.colors.reset} Create session and start agent (all-in-one)
|
|
1173
|
+
${CONFIG.colors.green}request [agent]${CONFIG.colors.reset} Request a session (for Claude to call)
|
|
1174
|
+
${CONFIG.colors.green}list${CONFIG.colors.reset} List all active sessions
|
|
1175
|
+
${CONFIG.colors.green}help${CONFIG.colors.reset} Show this help
|
|
1176
|
+
|
|
1177
|
+
${CONFIG.colors.blue}Options:${CONFIG.colors.reset}
|
|
1178
|
+
--task <name> Task or feature name
|
|
1179
|
+
--agent <type> Agent type (claude, cline, copilot, etc.)
|
|
1180
|
+
|
|
1181
|
+
${CONFIG.colors.blue}Examples:${CONFIG.colors.reset}
|
|
1182
|
+
${CONFIG.colors.dim}# Workflow 1: Manual coordination${CONFIG.colors.reset}
|
|
1183
|
+
node session-coordinator.js create --task "auth-feature"
|
|
1184
|
+
${CONFIG.colors.dim}# Copy instructions to Claude${CONFIG.colors.reset}
|
|
1185
|
+
node session-coordinator.js start <session-id>
|
|
1186
|
+
|
|
1187
|
+
${CONFIG.colors.dim}# Workflow 2: All-in-one${CONFIG.colors.reset}
|
|
1188
|
+
node session-coordinator.js create-and-start --task "api-endpoints"
|
|
1189
|
+
|
|
1190
|
+
${CONFIG.colors.dim}# Workflow 3: Claude requests a session${CONFIG.colors.reset}
|
|
1191
|
+
node session-coordinator.js request claude
|
|
1192
|
+
|
|
1193
|
+
${CONFIG.colors.yellow}Typical Workflow:${CONFIG.colors.reset}
|
|
1194
|
+
1. Run: ${CONFIG.colors.green}node session-coordinator.js create-and-start${CONFIG.colors.reset}
|
|
1195
|
+
2. Copy the displayed instructions to Claude/Cline
|
|
1196
|
+
3. Claude navigates to the worktree and starts working
|
|
1197
|
+
4. Agent automatically commits and pushes changes
|
|
1198
|
+
`);
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// Run the CLI
|
|
1204
|
+
main().catch(err => {
|
|
1205
|
+
console.error(`${CONFIG.colors.red}Error: ${err.message}${CONFIG.colors.reset}`);
|
|
1206
|
+
process.exit(1);
|
|
1207
|
+
});
|