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,768 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ============================================================================
|
|
5
|
+
* WORKTREE MANAGER FOR MULTI-AGENT DEVELOPMENT
|
|
6
|
+
* ============================================================================
|
|
7
|
+
*
|
|
8
|
+
* This module manages Git worktrees to enable multiple AI agents to work
|
|
9
|
+
* on the same codebase simultaneously without conflicts.
|
|
10
|
+
*
|
|
11
|
+
* Key Features:
|
|
12
|
+
* - Creates isolated worktrees for each AI agent
|
|
13
|
+
* - Manages agent-specific branches
|
|
14
|
+
* - Coordinates merges between agent work
|
|
15
|
+
* - Prevents conflicts through branch isolation
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node worktree-manager.js create --agent claude --task feature-x
|
|
19
|
+
* node worktree-manager.js list
|
|
20
|
+
* node worktree-manager.js merge --agent claude
|
|
21
|
+
* node worktree-manager.js cleanup --agent claude
|
|
22
|
+
* ============================================================================
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import fs from 'fs';
|
|
26
|
+
import path from 'path';
|
|
27
|
+
import { execSync, spawn } from 'child_process';
|
|
28
|
+
import { fileURLToPath } from 'url';
|
|
29
|
+
import { dirname } from 'path';
|
|
30
|
+
import readline from 'readline';
|
|
31
|
+
|
|
32
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
33
|
+
const __dirname = dirname(__filename);
|
|
34
|
+
|
|
35
|
+
// ============================================================================
|
|
36
|
+
// CONFIGURATION
|
|
37
|
+
// ============================================================================
|
|
38
|
+
|
|
39
|
+
const CONFIG = {
|
|
40
|
+
// Base directory for worktrees (relative to main repo)
|
|
41
|
+
worktreesDir: '.worktrees',
|
|
42
|
+
|
|
43
|
+
// Agent naming patterns
|
|
44
|
+
agentPrefix: 'agent',
|
|
45
|
+
|
|
46
|
+
// Branch naming patterns
|
|
47
|
+
branchPatterns: {
|
|
48
|
+
agent: 'agent/${agentName}/${taskName}',
|
|
49
|
+
daily: 'agent/${agentName}/daily/${date}',
|
|
50
|
+
feature: 'agent/${agentName}/feature/${feature}'
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// Supported AI agents
|
|
54
|
+
knownAgents: ['claude', 'copilot', 'cursor', 'aider', 'custom'],
|
|
55
|
+
|
|
56
|
+
// Colors for console output
|
|
57
|
+
colors: {
|
|
58
|
+
reset: '\x1b[0m',
|
|
59
|
+
bright: '\x1b[1m',
|
|
60
|
+
green: '\x1b[32m',
|
|
61
|
+
yellow: '\x1b[33m',
|
|
62
|
+
blue: '\x1b[36m',
|
|
63
|
+
red: '\x1b[31m',
|
|
64
|
+
magenta: '\x1b[35m',
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// ============================================================================
|
|
69
|
+
// UTILITY FUNCTIONS
|
|
70
|
+
// ============================================================================
|
|
71
|
+
|
|
72
|
+
const log = {
|
|
73
|
+
info: (msg) => console.log(`${CONFIG.colors.blue}ℹ${CONFIG.colors.reset} ${msg}`),
|
|
74
|
+
success: (msg) => console.log(`${CONFIG.colors.green}✓${CONFIG.colors.reset} ${msg}`),
|
|
75
|
+
warn: (msg) => console.log(`${CONFIG.colors.yellow}⚠${CONFIG.colors.reset} ${msg}`),
|
|
76
|
+
error: (msg) => console.log(`${CONFIG.colors.red}✗${CONFIG.colors.reset} ${msg}`),
|
|
77
|
+
agent: (agent, msg) => console.log(`${CONFIG.colors.magenta}[${agent}]${CONFIG.colors.reset} ${msg}`),
|
|
78
|
+
header: (msg) => {
|
|
79
|
+
console.log(`\n${CONFIG.colors.bright}${CONFIG.colors.blue}${'='.repeat(60)}${CONFIG.colors.reset}`);
|
|
80
|
+
console.log(`${CONFIG.colors.bright}${msg}${CONFIG.colors.reset}`);
|
|
81
|
+
console.log(`${CONFIG.colors.bright}${CONFIG.colors.blue}${'='.repeat(60)}${CONFIG.colors.reset}\n`);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Execute a shell command and return the output
|
|
87
|
+
*/
|
|
88
|
+
function execCommand(command, options = {}) {
|
|
89
|
+
try {
|
|
90
|
+
const result = execSync(command, {
|
|
91
|
+
encoding: 'utf8',
|
|
92
|
+
stdio: options.silent ? 'pipe' : 'inherit',
|
|
93
|
+
...options
|
|
94
|
+
});
|
|
95
|
+
return result ? result.trim() : '';
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (!options.ignoreError) {
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Check if we're in a git repository
|
|
106
|
+
*/
|
|
107
|
+
function checkGitRepo() {
|
|
108
|
+
try {
|
|
109
|
+
execSync('git rev-parse --git-dir', { stdio: 'pipe' });
|
|
110
|
+
return true;
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Get the root directory of the git repository
|
|
118
|
+
*/
|
|
119
|
+
function getRepoRoot() {
|
|
120
|
+
return execCommand('git rev-parse --show-toplevel', { silent: true });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Get current branch name
|
|
125
|
+
*/
|
|
126
|
+
function getCurrentBranch() {
|
|
127
|
+
return execCommand('git branch --show-current', { silent: true });
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Create a directory if it doesn't exist
|
|
132
|
+
*/
|
|
133
|
+
function ensureDir(dirPath) {
|
|
134
|
+
if (!fs.existsSync(dirPath)) {
|
|
135
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Format date as YYYY-MM-DD
|
|
141
|
+
*/
|
|
142
|
+
function getDateString() {
|
|
143
|
+
const now = new Date();
|
|
144
|
+
return now.toISOString().split('T')[0];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Generate branch name based on pattern
|
|
149
|
+
*/
|
|
150
|
+
function generateBranchName(pattern, vars) {
|
|
151
|
+
let branch = pattern;
|
|
152
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
153
|
+
branch = branch.replace(`\${${key}}`, value);
|
|
154
|
+
}
|
|
155
|
+
return branch;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ============================================================================
|
|
159
|
+
// WORKTREE MANAGEMENT CLASS
|
|
160
|
+
// ============================================================================
|
|
161
|
+
|
|
162
|
+
class WorktreeManager {
|
|
163
|
+
constructor() {
|
|
164
|
+
this.repoRoot = getRepoRoot();
|
|
165
|
+
this.worktreesPath = path.join(this.repoRoot, CONFIG.worktreesDir);
|
|
166
|
+
this.configFile = path.join(this.worktreesPath, 'agents.json');
|
|
167
|
+
this.loadConfig();
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Load or initialize agent configuration
|
|
172
|
+
*/
|
|
173
|
+
loadConfig() {
|
|
174
|
+
ensureDir(this.worktreesPath);
|
|
175
|
+
|
|
176
|
+
if (fs.existsSync(this.configFile)) {
|
|
177
|
+
const data = fs.readFileSync(this.configFile, 'utf8');
|
|
178
|
+
this.agents = JSON.parse(data);
|
|
179
|
+
} else {
|
|
180
|
+
this.agents = {};
|
|
181
|
+
this.saveConfig();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Save agent configuration
|
|
187
|
+
*/
|
|
188
|
+
saveConfig() {
|
|
189
|
+
fs.writeFileSync(this.configFile, JSON.stringify(this.agents, null, 2));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Create a new worktree for an AI agent
|
|
194
|
+
*/
|
|
195
|
+
createWorktree(agentName, taskName, options = {}) {
|
|
196
|
+
log.header(`Creating Worktree for Agent: ${agentName}`);
|
|
197
|
+
|
|
198
|
+
// Validate agent name
|
|
199
|
+
if (!agentName || agentName.length < 2) {
|
|
200
|
+
throw new Error('Agent name must be at least 2 characters');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Generate paths and names
|
|
204
|
+
const worktreeName = `${agentName}-${taskName || getDateString()}`;
|
|
205
|
+
const worktreePath = path.join(this.worktreesPath, worktreeName);
|
|
206
|
+
const branchName = generateBranchName(
|
|
207
|
+
options.branchPattern || CONFIG.branchPatterns.agent,
|
|
208
|
+
{
|
|
209
|
+
agentName,
|
|
210
|
+
taskName: taskName || 'main',
|
|
211
|
+
date: getDateString(),
|
|
212
|
+
feature: options.feature || taskName
|
|
213
|
+
}
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Check if worktree already exists
|
|
217
|
+
if (fs.existsSync(worktreePath)) {
|
|
218
|
+
log.warn(`Worktree already exists at: ${worktreePath}`);
|
|
219
|
+
return { path: worktreePath, branch: branchName, exists: true };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Create the worktree
|
|
223
|
+
log.info(`Creating worktree at: ${worktreePath}`);
|
|
224
|
+
log.info(`Branch: ${branchName}`);
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
// Create new branch and worktree
|
|
228
|
+
execCommand(`git worktree add -b ${branchName} "${worktreePath}" HEAD`, { silent: false });
|
|
229
|
+
|
|
230
|
+
// Update agent configuration
|
|
231
|
+
this.agents[agentName] = {
|
|
232
|
+
name: agentName,
|
|
233
|
+
worktrees: [
|
|
234
|
+
...(this.agents[agentName]?.worktrees || []),
|
|
235
|
+
{
|
|
236
|
+
name: worktreeName,
|
|
237
|
+
path: worktreePath,
|
|
238
|
+
branch: branchName,
|
|
239
|
+
task: taskName,
|
|
240
|
+
created: new Date().toISOString(),
|
|
241
|
+
status: 'active'
|
|
242
|
+
}
|
|
243
|
+
]
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
this.saveConfig();
|
|
247
|
+
|
|
248
|
+
// Setup agent-specific configuration
|
|
249
|
+
this.setupAgentConfig(agentName, worktreePath, options);
|
|
250
|
+
|
|
251
|
+
log.success(`Worktree created successfully!`);
|
|
252
|
+
log.info(`Agent ${agentName} can now work in: ${worktreePath}`);
|
|
253
|
+
|
|
254
|
+
return { path: worktreePath, branch: branchName, exists: false };
|
|
255
|
+
|
|
256
|
+
} catch (error) {
|
|
257
|
+
log.error(`Failed to create worktree: ${error.message}`);
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Setup agent-specific configuration in the worktree
|
|
264
|
+
*/
|
|
265
|
+
setupAgentConfig(agentName, worktreePath, options) {
|
|
266
|
+
log.info('Setting up agent-specific configuration...');
|
|
267
|
+
|
|
268
|
+
// Create .agent-config file
|
|
269
|
+
const agentConfig = {
|
|
270
|
+
agent: agentName,
|
|
271
|
+
worktree: path.basename(worktreePath),
|
|
272
|
+
created: new Date().toISOString(),
|
|
273
|
+
task: options.task || 'general',
|
|
274
|
+
autoCommit: {
|
|
275
|
+
enabled: true,
|
|
276
|
+
prefix: `agent_${agentName}_`,
|
|
277
|
+
messagePrefix: `[${agentName.toUpperCase()}]`,
|
|
278
|
+
pushOnCommit: options.autoPush !== false
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
fs.writeFileSync(
|
|
283
|
+
path.join(worktreePath, '.agent-config'),
|
|
284
|
+
JSON.stringify(agentConfig, null, 2)
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// Create agent-specific commit message file
|
|
288
|
+
fs.writeFileSync(
|
|
289
|
+
path.join(worktreePath, `.${agentName}-commit-msg`),
|
|
290
|
+
''
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
// Create agent-specific VS Code settings
|
|
294
|
+
const vscodeDir = path.join(worktreePath, '.vscode');
|
|
295
|
+
ensureDir(vscodeDir);
|
|
296
|
+
|
|
297
|
+
const settings = {
|
|
298
|
+
'window.title': `${agentName.toUpperCase()} - ${options.task || 'Workspace'}`,
|
|
299
|
+
'terminal.integrated.env.osx': {
|
|
300
|
+
'AGENT_NAME': agentName,
|
|
301
|
+
'AGENT_WORKTREE': path.basename(worktreePath),
|
|
302
|
+
'AC_BRANCH_PREFIX': `agent_${agentName}_`,
|
|
303
|
+
'AC_MSG_FILE': `.${agentName}-commit-msg`
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
fs.writeFileSync(
|
|
308
|
+
path.join(vscodeDir, 'settings.json'),
|
|
309
|
+
JSON.stringify(settings, null, 2)
|
|
310
|
+
);
|
|
311
|
+
|
|
312
|
+
log.success('Agent configuration created');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* List all worktrees and their agents
|
|
317
|
+
*/
|
|
318
|
+
listWorktrees() {
|
|
319
|
+
log.header('Active Worktrees');
|
|
320
|
+
|
|
321
|
+
// Get git worktree list
|
|
322
|
+
const worktrees = execCommand('git worktree list --porcelain', { silent: true })
|
|
323
|
+
.split('\n\n')
|
|
324
|
+
.filter(w => w)
|
|
325
|
+
.map(w => {
|
|
326
|
+
const lines = w.split('\n');
|
|
327
|
+
const worktreePath = lines[0].replace('worktree ', '');
|
|
328
|
+
const branch = lines[2]?.replace('branch refs/heads/', '') || 'detached';
|
|
329
|
+
return { path: worktreePath, branch };
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
// Display main repository
|
|
333
|
+
const mainWorktree = worktrees.find(w => w.path === this.repoRoot);
|
|
334
|
+
if (mainWorktree) {
|
|
335
|
+
console.log(`${CONFIG.colors.bright}Main Repository:${CONFIG.colors.reset}`);
|
|
336
|
+
console.log(` Path: ${mainWorktree.path}`);
|
|
337
|
+
console.log(` Branch: ${mainWorktree.branch}`);
|
|
338
|
+
console.log('');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Display agent worktrees
|
|
342
|
+
console.log(`${CONFIG.colors.bright}Agent Worktrees:${CONFIG.colors.reset}`);
|
|
343
|
+
|
|
344
|
+
let agentWorktreeCount = 0;
|
|
345
|
+
for (const [agentName, agentData] of Object.entries(this.agents)) {
|
|
346
|
+
const activeWorktrees = (agentData.worktrees || []).filter(w => w.status === 'active');
|
|
347
|
+
|
|
348
|
+
if (activeWorktrees.length > 0) {
|
|
349
|
+
console.log(`\n${CONFIG.colors.magenta}[${agentName}]${CONFIG.colors.reset}`);
|
|
350
|
+
|
|
351
|
+
for (const wt of activeWorktrees) {
|
|
352
|
+
const exists = fs.existsSync(wt.path);
|
|
353
|
+
const status = exists ? CONFIG.colors.green + '✓' : CONFIG.colors.red + '✗';
|
|
354
|
+
|
|
355
|
+
console.log(` ${status}${CONFIG.colors.reset} ${wt.name}`);
|
|
356
|
+
console.log(` Branch: ${wt.branch}`);
|
|
357
|
+
console.log(` Task: ${wt.task || 'N/A'}`);
|
|
358
|
+
console.log(` Created: ${new Date(wt.created).toLocaleDateString()}`);
|
|
359
|
+
|
|
360
|
+
if (!exists) {
|
|
361
|
+
console.log(` ${CONFIG.colors.yellow}(Worktree missing - may need cleanup)${CONFIG.colors.reset}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
agentWorktreeCount++;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (agentWorktreeCount === 0) {
|
|
370
|
+
console.log(' No agent worktrees found');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
console.log(`\nTotal worktrees: ${worktrees.length}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Merge agent's work back to main branch
|
|
378
|
+
*/
|
|
379
|
+
async mergeAgentWork(agentName, options = {}) {
|
|
380
|
+
log.header(`Merging ${agentName}'s Work`);
|
|
381
|
+
|
|
382
|
+
const agentData = this.agents[agentName];
|
|
383
|
+
if (!agentData) {
|
|
384
|
+
log.error(`No worktrees found for agent: ${agentName}`);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const activeWorktrees = (agentData.worktrees || []).filter(w => w.status === 'active');
|
|
389
|
+
if (activeWorktrees.length === 0) {
|
|
390
|
+
log.warn(`No active worktrees for agent: ${agentName}`);
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Let user select which worktree to merge
|
|
395
|
+
console.log('Select worktree to merge:');
|
|
396
|
+
activeWorktrees.forEach((wt, idx) => {
|
|
397
|
+
console.log(` ${idx + 1}. ${wt.name} (${wt.branch})`);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const selection = await this.promptUser('Enter number: ');
|
|
401
|
+
const selectedIdx = parseInt(selection) - 1;
|
|
402
|
+
|
|
403
|
+
if (selectedIdx < 0 || selectedIdx >= activeWorktrees.length) {
|
|
404
|
+
log.error('Invalid selection');
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const worktree = activeWorktrees[selectedIdx];
|
|
409
|
+
const targetBranch = options.target || 'main';
|
|
410
|
+
|
|
411
|
+
log.info(`Merging ${worktree.branch} into ${targetBranch}...`);
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
// Save current branch
|
|
415
|
+
const currentBranch = getCurrentBranch();
|
|
416
|
+
|
|
417
|
+
// Checkout target branch
|
|
418
|
+
execCommand(`git checkout ${targetBranch}`);
|
|
419
|
+
|
|
420
|
+
// Merge agent's branch
|
|
421
|
+
const mergeMessage = `Merge ${agentName}'s work: ${worktree.task || 'updates'}`;
|
|
422
|
+
execCommand(`git merge ${worktree.branch} -m "${mergeMessage}"`);
|
|
423
|
+
|
|
424
|
+
log.success(`Successfully merged ${worktree.branch} into ${targetBranch}`);
|
|
425
|
+
|
|
426
|
+
// Ask if should delete the branch
|
|
427
|
+
const shouldDelete = await this.promptUser('Delete merged branch? (y/n): ');
|
|
428
|
+
if (shouldDelete.toLowerCase() === 'y') {
|
|
429
|
+
execCommand(`git branch -d ${worktree.branch}`);
|
|
430
|
+
worktree.status = 'merged';
|
|
431
|
+
this.saveConfig();
|
|
432
|
+
log.success('Branch deleted');
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Return to original branch
|
|
436
|
+
if (currentBranch) {
|
|
437
|
+
execCommand(`git checkout ${currentBranch}`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
} catch (error) {
|
|
441
|
+
log.error(`Merge failed: ${error.message}`);
|
|
442
|
+
log.info('You may need to resolve conflicts manually');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Clean up worktrees for an agent
|
|
448
|
+
*/
|
|
449
|
+
async cleanupWorktrees(agentName, options = {}) {
|
|
450
|
+
log.header(`Cleaning Up ${agentName}'s Worktrees`);
|
|
451
|
+
|
|
452
|
+
const agentData = this.agents[agentName];
|
|
453
|
+
if (!agentData) {
|
|
454
|
+
log.warn(`No worktrees found for agent: ${agentName}`);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const worktrees = agentData.worktrees || [];
|
|
459
|
+
let cleaned = 0;
|
|
460
|
+
|
|
461
|
+
for (const wt of worktrees) {
|
|
462
|
+
const exists = fs.existsSync(wt.path);
|
|
463
|
+
|
|
464
|
+
if (!exists && !options.force) {
|
|
465
|
+
log.info(`Worktree already removed: ${wt.name}`);
|
|
466
|
+
wt.status = 'removed';
|
|
467
|
+
cleaned++;
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (options.all || wt.status === 'merged' || !exists) {
|
|
472
|
+
try {
|
|
473
|
+
// Remove worktree
|
|
474
|
+
if (exists) {
|
|
475
|
+
log.info(`Removing worktree: ${wt.name}`);
|
|
476
|
+
execCommand(`git worktree remove "${wt.path}" --force`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Delete branch if requested
|
|
480
|
+
if (options.deleteBranches) {
|
|
481
|
+
execCommand(`git branch -D ${wt.branch}`, { ignoreError: true });
|
|
482
|
+
log.info(`Deleted branch: ${wt.branch}`);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
wt.status = 'removed';
|
|
486
|
+
cleaned++;
|
|
487
|
+
|
|
488
|
+
} catch (error) {
|
|
489
|
+
log.error(`Failed to remove ${wt.name}: ${error.message}`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// Clean up agent data if all worktrees removed
|
|
495
|
+
if (worktrees.every(wt => wt.status === 'removed')) {
|
|
496
|
+
delete this.agents[agentName];
|
|
497
|
+
log.info(`Removed agent configuration for: ${agentName}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
this.saveConfig();
|
|
501
|
+
log.success(`Cleaned up ${cleaned} worktree(s)`);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Run cs-devops-agent worker in a specific worktree
|
|
506
|
+
*/
|
|
507
|
+
runCS_DevOpsAgent(agentName, worktreeName) {
|
|
508
|
+
const agentData = this.agents[agentName];
|
|
509
|
+
if (!agentData) {
|
|
510
|
+
log.error(`Agent not found: ${agentName}`);
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const worktree = agentData.worktrees.find(w => w.name === worktreeName);
|
|
515
|
+
if (!worktree) {
|
|
516
|
+
log.error(`Worktree not found: ${worktreeName}`);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
log.header(`Starting DevOps Agent for ${agentName}`);
|
|
521
|
+
log.info(`Worktree: ${worktree.path}`);
|
|
522
|
+
log.info(`Branch: ${worktree.branch}`);
|
|
523
|
+
|
|
524
|
+
// Set up environment variables
|
|
525
|
+
const env = {
|
|
526
|
+
...process.env,
|
|
527
|
+
AGENT_NAME: agentName,
|
|
528
|
+
AGENT_WORKTREE: worktreeName,
|
|
529
|
+
AC_BRANCH_PREFIX: `agent_${agentName}_`,
|
|
530
|
+
AC_MSG_FILE: `.${agentName}-commit-msg`,
|
|
531
|
+
AC_WORKING_DIR: worktree.path
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
// Start cs-devops-agent worker
|
|
535
|
+
const autoCommitPath = path.join(this.repoRoot, 'cs-devops-agent-worker.js');
|
|
536
|
+
const child = spawn('node', [autoCommitPath], {
|
|
537
|
+
cwd: worktree.path,
|
|
538
|
+
env,
|
|
539
|
+
stdio: 'inherit'
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
child.on('exit', (code) => {
|
|
543
|
+
log.info(`Auto-commit worker exited with code: ${code}`);
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Handle graceful shutdown
|
|
547
|
+
process.on('SIGINT', () => {
|
|
548
|
+
log.info('Stopping cs-devops-agent worker...');
|
|
549
|
+
child.kill('SIGINT');
|
|
550
|
+
process.exit(0);
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Prompt user for input
|
|
556
|
+
*/
|
|
557
|
+
promptUser(question) {
|
|
558
|
+
const rl = readline.createInterface({
|
|
559
|
+
input: process.stdin,
|
|
560
|
+
output: process.stdout
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
return new Promise((resolve) => {
|
|
564
|
+
rl.question(question, (answer) => {
|
|
565
|
+
rl.close();
|
|
566
|
+
resolve(answer);
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Create parallel workspace for multiple agents
|
|
573
|
+
*/
|
|
574
|
+
async createParallelWorkspace(agents, task) {
|
|
575
|
+
log.header('Creating Parallel Workspace for Multiple Agents');
|
|
576
|
+
|
|
577
|
+
const workspace = {
|
|
578
|
+
id: `parallel-${Date.now()}`,
|
|
579
|
+
task,
|
|
580
|
+
agents: [],
|
|
581
|
+
created: new Date().toISOString()
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
for (const agent of agents) {
|
|
585
|
+
log.info(`Setting up workspace for: ${agent}`);
|
|
586
|
+
|
|
587
|
+
const result = this.createWorktree(agent, task, {
|
|
588
|
+
feature: task,
|
|
589
|
+
autoPush: false
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
workspace.agents.push({
|
|
593
|
+
name: agent,
|
|
594
|
+
...result
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// Save workspace configuration
|
|
599
|
+
const workspaceFile = path.join(this.worktreesPath, 'workspaces.json');
|
|
600
|
+
let workspaces = {};
|
|
601
|
+
|
|
602
|
+
if (fs.existsSync(workspaceFile)) {
|
|
603
|
+
workspaces = JSON.parse(fs.readFileSync(workspaceFile, 'utf8'));
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
workspaces[workspace.id] = workspace;
|
|
607
|
+
fs.writeFileSync(workspaceFile, JSON.stringify(workspaces, null, 2));
|
|
608
|
+
|
|
609
|
+
log.success(`Parallel workspace created: ${workspace.id}`);
|
|
610
|
+
log.info(`Agents ready: ${agents.join(', ')}`);
|
|
611
|
+
|
|
612
|
+
return workspace;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ============================================================================
|
|
617
|
+
// CLI INTERFACE
|
|
618
|
+
// ============================================================================
|
|
619
|
+
|
|
620
|
+
async function main() {
|
|
621
|
+
const args = process.argv.slice(2);
|
|
622
|
+
const command = args[0];
|
|
623
|
+
|
|
624
|
+
if (!checkGitRepo()) {
|
|
625
|
+
log.error('Not in a git repository!');
|
|
626
|
+
process.exit(1);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
const manager = new WorktreeManager();
|
|
630
|
+
|
|
631
|
+
switch (command) {
|
|
632
|
+
case 'create': {
|
|
633
|
+
const agentIdx = args.indexOf('--agent');
|
|
634
|
+
const taskIdx = args.indexOf('--task');
|
|
635
|
+
|
|
636
|
+
if (agentIdx === -1 || !args[agentIdx + 1]) {
|
|
637
|
+
log.error('Usage: worktree-manager create --agent <name> --task <task>');
|
|
638
|
+
process.exit(1);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
const agentName = args[agentIdx + 1];
|
|
642
|
+
const taskName = taskIdx !== -1 ? args[taskIdx + 1] : null;
|
|
643
|
+
|
|
644
|
+
manager.createWorktree(agentName, taskName);
|
|
645
|
+
break;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
case 'list': {
|
|
649
|
+
manager.listWorktrees();
|
|
650
|
+
break;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
case 'merge': {
|
|
654
|
+
const agentIdx = args.indexOf('--agent');
|
|
655
|
+
|
|
656
|
+
if (agentIdx === -1 || !args[agentIdx + 1]) {
|
|
657
|
+
log.error('Usage: worktree-manager merge --agent <name>');
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
const agentName = args[agentIdx + 1];
|
|
662
|
+
await manager.mergeAgentWork(agentName);
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
case 'cleanup': {
|
|
667
|
+
const agentIdx = args.indexOf('--agent');
|
|
668
|
+
|
|
669
|
+
if (agentIdx === -1 || !args[agentIdx + 1]) {
|
|
670
|
+
log.error('Usage: worktree-manager cleanup --agent <name> [--all] [--delete-branches]');
|
|
671
|
+
process.exit(1);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const agentName = args[agentIdx + 1];
|
|
675
|
+
const options = {
|
|
676
|
+
all: args.includes('--all'),
|
|
677
|
+
deleteBranches: args.includes('--delete-branches'),
|
|
678
|
+
force: args.includes('--force')
|
|
679
|
+
};
|
|
680
|
+
|
|
681
|
+
await manager.cleanupWorktrees(agentName, options);
|
|
682
|
+
break;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
case 'cs-devops-agent': {
|
|
686
|
+
const agentIdx = args.indexOf('--agent');
|
|
687
|
+
const worktreeIdx = args.indexOf('--worktree');
|
|
688
|
+
|
|
689
|
+
if (agentIdx === -1 || !args[agentIdx + 1] || worktreeIdx === -1 || !args[worktreeIdx + 1]) {
|
|
690
|
+
log.error('Usage: worktree-manager cs-devops-agent --agent <name> --worktree <name>');
|
|
691
|
+
process.exit(1);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
const agentName = args[agentIdx + 1];
|
|
695
|
+
const worktreeName = args[worktreeIdx + 1];
|
|
696
|
+
|
|
697
|
+
manager.runCS_DevOpsAgent(agentName, worktreeName);
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
case 'parallel': {
|
|
702
|
+
const agentsIdx = args.indexOf('--agents');
|
|
703
|
+
const taskIdx = args.indexOf('--task');
|
|
704
|
+
|
|
705
|
+
if (agentsIdx === -1 || taskIdx === -1) {
|
|
706
|
+
log.error('Usage: worktree-manager parallel --agents agent1,agent2,agent3 --task <task>');
|
|
707
|
+
process.exit(1);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const agents = args[agentsIdx + 1].split(',');
|
|
711
|
+
const task = args[taskIdx + 1];
|
|
712
|
+
|
|
713
|
+
await manager.createParallelWorkspace(agents, task);
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
default: {
|
|
718
|
+
console.log(`
|
|
719
|
+
${CONFIG.colors.bright}Worktree Manager - Multi-Agent Development System${CONFIG.colors.reset}
|
|
720
|
+
|
|
721
|
+
Commands:
|
|
722
|
+
create Create a new worktree for an AI agent
|
|
723
|
+
worktree-manager create --agent <name> --task <task>
|
|
724
|
+
|
|
725
|
+
list List all active worktrees and agents
|
|
726
|
+
worktree-manager list
|
|
727
|
+
|
|
728
|
+
merge Merge an agent's work back to main
|
|
729
|
+
worktree-manager merge --agent <name>
|
|
730
|
+
|
|
731
|
+
cleanup Clean up worktrees for an agent
|
|
732
|
+
worktree-manager cleanup --agent <name> [--all] [--delete-branches]
|
|
733
|
+
|
|
734
|
+
cs-devops-agent Run cs-devops-agent in an agent's worktree
|
|
735
|
+
worktree-manager cs-devops-agent --agent <name> --worktree <name>
|
|
736
|
+
|
|
737
|
+
parallel Create parallel workspace for multiple agents
|
|
738
|
+
worktree-manager parallel --agents agent1,agent2 --task <task>
|
|
739
|
+
|
|
740
|
+
Examples:
|
|
741
|
+
# Create worktree for Claude to work on authentication
|
|
742
|
+
worktree-manager create --agent claude --task auth-feature
|
|
743
|
+
|
|
744
|
+
# Create parallel workspace for 3 agents
|
|
745
|
+
worktree-manager parallel --agents claude,copilot,cursor --task refactor-api
|
|
746
|
+
|
|
747
|
+
# List all worktrees
|
|
748
|
+
worktree-manager list
|
|
749
|
+
|
|
750
|
+
# Merge Claude's work
|
|
751
|
+
worktree-manager merge --agent claude
|
|
752
|
+
|
|
753
|
+
# Clean up merged worktrees
|
|
754
|
+
worktree-manager cleanup --agent claude --delete-branches
|
|
755
|
+
`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
// Run if called directly
|
|
761
|
+
if (import.meta.url === `file://${__filename}`) {
|
|
762
|
+
main().catch(error => {
|
|
763
|
+
log.error(`Fatal error: ${error.message}`);
|
|
764
|
+
process.exit(1);
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export default WorktreeManager;
|