@vibedx/vibekit 0.8.7 → 0.9.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/README.md +34 -0
- package/assets/config.yml +4 -0
- package/assets/standards/coding/default.md +15 -0
- package/assets/standards/coding/karpathy.md +58 -0
- package/assets/standards/frameworks/react.md +36 -0
- package/assets/standards/languages/node.md +34 -0
- package/assets/standards/languages/python.md +34 -0
- package/index.js +1 -1
- package/package.json +1 -1
- package/skills/vibekit/SKILL.md +54 -0
- package/src/commands/init/index.js +117 -37
- package/src/commands/init/index.test.js +38 -15
- package/src/commands/plan/index.js +283 -0
- package/src/commands/plan/index.test.js +127 -0
- package/src/commands/plan/to-ticket.js +312 -0
- package/src/commands/plan/to-ticket.test.js +149 -0
- package/src/commands/start/index.js +9 -37
- package/src/commands/swarm/index.js +375 -0
- package/src/utils/agent.js +75 -0
- package/src/utils/swarm.js +75 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import yaml from 'js-yaml';
|
|
4
|
+
import { execSync } from 'child_process';
|
|
5
|
+
import { getTicketsDir, getConfig, getProjectRoot } from '../../utils/index.js';
|
|
6
|
+
import {
|
|
7
|
+
isGitRepository,
|
|
8
|
+
branchExistsLocally,
|
|
9
|
+
branchExistsRemotely,
|
|
10
|
+
getRepoName,
|
|
11
|
+
getWorktreePath,
|
|
12
|
+
createWorktree,
|
|
13
|
+
createWorktreeExistingBranch,
|
|
14
|
+
getRepoRoot
|
|
15
|
+
} from '../../utils/git.js';
|
|
16
|
+
import {
|
|
17
|
+
loadSkillContext,
|
|
18
|
+
buildAgentPrompt,
|
|
19
|
+
spawnAgentWithLogs,
|
|
20
|
+
isProcessRunning,
|
|
21
|
+
killProcess
|
|
22
|
+
} from '../../utils/agent.js';
|
|
23
|
+
import {
|
|
24
|
+
loadSwarmState,
|
|
25
|
+
saveSwarmState,
|
|
26
|
+
createSwarmState,
|
|
27
|
+
addAgentToState,
|
|
28
|
+
updateAgentStatus,
|
|
29
|
+
getLogsDir
|
|
30
|
+
} from '../../utils/swarm.js';
|
|
31
|
+
|
|
32
|
+
function parseSwarmArgs(args) {
|
|
33
|
+
const flags = {};
|
|
34
|
+
let subcommand = null;
|
|
35
|
+
let i = 0;
|
|
36
|
+
|
|
37
|
+
while (i < args.length) {
|
|
38
|
+
const arg = args[i];
|
|
39
|
+
if (arg === 'status') {
|
|
40
|
+
subcommand = 'status';
|
|
41
|
+
i++;
|
|
42
|
+
} else if (arg === 'stop') {
|
|
43
|
+
subcommand = 'stop';
|
|
44
|
+
i++;
|
|
45
|
+
} else if (arg === '--count' && i + 1 < args.length) {
|
|
46
|
+
flags.count = parseInt(args[i + 1], 10);
|
|
47
|
+
i += 2;
|
|
48
|
+
} else if (arg === '--filter' && i + 1 < args.length) {
|
|
49
|
+
flags.filter = args[i + 1];
|
|
50
|
+
i += 2;
|
|
51
|
+
} else if (arg === '--dry-run') {
|
|
52
|
+
flags.dryRun = true;
|
|
53
|
+
i++;
|
|
54
|
+
} else if (arg === '--no-install') {
|
|
55
|
+
flags.noInstall = true;
|
|
56
|
+
i++;
|
|
57
|
+
} else if (arg === '--base' && i + 1 < args.length) {
|
|
58
|
+
flags.baseBranch = args[i + 1];
|
|
59
|
+
i += 2;
|
|
60
|
+
} else {
|
|
61
|
+
i++;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return { subcommand, flags };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function loadTickets(ticketsDir) {
|
|
69
|
+
const files = fs.readdirSync(ticketsDir).filter(f => f.endsWith('.md'));
|
|
70
|
+
const tickets = [];
|
|
71
|
+
for (const file of files) {
|
|
72
|
+
const filePath = path.join(ticketsDir, file);
|
|
73
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
74
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
75
|
+
if (match) {
|
|
76
|
+
const frontmatter = yaml.load(match[1]);
|
|
77
|
+
tickets.push({ frontmatter, content, filePath, fileName: file });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return tickets;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function applyFilters(tickets, filterStr) {
|
|
84
|
+
if (!filterStr) return tickets.filter(t => t.frontmatter.status === 'open');
|
|
85
|
+
|
|
86
|
+
return tickets.filter(t => {
|
|
87
|
+
const parts = filterStr.split(',').map(s => s.trim());
|
|
88
|
+
return parts.every(part => {
|
|
89
|
+
const [key, value] = part.split(':').map(s => s.trim());
|
|
90
|
+
if (!key || !value) return true;
|
|
91
|
+
return String(t.frontmatter[key]).toLowerCase() === value.toLowerCase();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getBranchName(ticket, config) {
|
|
97
|
+
const branchPrefix = config.git?.branch_prefix || '';
|
|
98
|
+
const slug = String(ticket.frontmatter.slug || ticket.frontmatter.id);
|
|
99
|
+
return slug.includes(ticket.frontmatter.id)
|
|
100
|
+
? `${branchPrefix}${slug}`
|
|
101
|
+
: `${branchPrefix}${ticket.frontmatter.id}-${slug}`;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function updateTicketStatus(ticket, worktreePath) {
|
|
105
|
+
const now = new Date().toISOString();
|
|
106
|
+
let updatedContent = ticket.content;
|
|
107
|
+
|
|
108
|
+
if (worktreePath) {
|
|
109
|
+
if (updatedContent.match(/^worktree_path: .+$/m)) {
|
|
110
|
+
updatedContent = updatedContent.replace(/^worktree_path: .+$/m, `worktree_path: "${worktreePath}"`);
|
|
111
|
+
} else {
|
|
112
|
+
updatedContent = updatedContent.replace(/^(updated_at: .+)$/m, `$1\nworktree_path: "${worktreePath}"`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
updatedContent = updatedContent
|
|
117
|
+
.replace(/^status: (.+)$/m, 'status: in_progress')
|
|
118
|
+
.replace(/^updated_at: (.+)$/m, `updated_at: "${now}"`);
|
|
119
|
+
|
|
120
|
+
fs.writeFileSync(ticket.filePath, updatedContent, 'utf-8');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function formatElapsed(startedAt) {
|
|
124
|
+
const ms = Date.now() - new Date(startedAt).getTime();
|
|
125
|
+
const seconds = Math.floor(ms / 1000);
|
|
126
|
+
if (seconds < 60) return `${seconds}s`;
|
|
127
|
+
const minutes = Math.floor(seconds / 60);
|
|
128
|
+
if (minutes < 60) return `${minutes}m ${seconds % 60}s`;
|
|
129
|
+
return `${Math.floor(minutes / 60)}h ${minutes % 60}m`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function showStatus() {
|
|
133
|
+
const state = loadSwarmState();
|
|
134
|
+
if (!state) {
|
|
135
|
+
console.log('No active swarm. Run `vibe swarm` to start one.');
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
console.log(`\n🐝 Swarm: ${state.id}`);
|
|
140
|
+
console.log(` Started: ${state.started}`);
|
|
141
|
+
console.log(` Config: max ${state.config.maxAgents} agents, ${state.config.timeout}s timeout\n`);
|
|
142
|
+
|
|
143
|
+
if (state.agents.length === 0) {
|
|
144
|
+
console.log(' No agents.\n');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const colId = 10;
|
|
149
|
+
const colTitle = 35;
|
|
150
|
+
const colStatus = 10;
|
|
151
|
+
const colTime = 10;
|
|
152
|
+
const colPr = 8;
|
|
153
|
+
|
|
154
|
+
console.log(
|
|
155
|
+
' ' +
|
|
156
|
+
'Ticket'.padEnd(colId) +
|
|
157
|
+
'Title'.padEnd(colTitle) +
|
|
158
|
+
'Status'.padEnd(colStatus) +
|
|
159
|
+
'Time'.padEnd(colTime) +
|
|
160
|
+
'PR'
|
|
161
|
+
);
|
|
162
|
+
console.log(' ' + '─'.repeat(colId + colTitle + colStatus + colTime + colPr));
|
|
163
|
+
|
|
164
|
+
for (const agent of state.agents) {
|
|
165
|
+
let status = agent.status;
|
|
166
|
+
if (status === 'running' && !isProcessRunning(agent.pid)) {
|
|
167
|
+
status = 'exited';
|
|
168
|
+
updateAgentStatus(state, agent.ticket, 'exited');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const statusIcon = {
|
|
172
|
+
running: '🔄',
|
|
173
|
+
done: '✅',
|
|
174
|
+
failed: '❌',
|
|
175
|
+
timeout: '⏰',
|
|
176
|
+
exited: '🏁'
|
|
177
|
+
}[status] || '❓';
|
|
178
|
+
|
|
179
|
+
const title = (agent.title || '').length > colTitle - 2
|
|
180
|
+
? (agent.title || '').slice(0, colTitle - 5) + '...'
|
|
181
|
+
: (agent.title || '');
|
|
182
|
+
|
|
183
|
+
const elapsed = agent.finishedAt
|
|
184
|
+
? formatElapsed(agent.startedAt)
|
|
185
|
+
: formatElapsed(agent.startedAt);
|
|
186
|
+
|
|
187
|
+
console.log(
|
|
188
|
+
' ' +
|
|
189
|
+
agent.ticket.padEnd(colId) +
|
|
190
|
+
title.padEnd(colTitle) +
|
|
191
|
+
`${statusIcon} ${status}`.padEnd(colStatus + 2) +
|
|
192
|
+
elapsed.padEnd(colTime) +
|
|
193
|
+
(agent.pr || '-')
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
saveSwarmState(state);
|
|
198
|
+
|
|
199
|
+
const running = state.agents.filter(a => a.status === 'running' && isProcessRunning(a.pid));
|
|
200
|
+
const done = state.agents.filter(a => a.status === 'done' || a.status === 'exited');
|
|
201
|
+
const failed = state.agents.filter(a => a.status === 'failed' || a.status === 'timeout');
|
|
202
|
+
console.log(`\n Running: ${running.length} Done: ${done.length} Failed: ${failed.length}\n`);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function stopSwarm() {
|
|
206
|
+
const state = loadSwarmState();
|
|
207
|
+
if (!state) {
|
|
208
|
+
console.log('No active swarm.');
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let killed = 0;
|
|
213
|
+
for (const agent of state.agents) {
|
|
214
|
+
if (agent.status === 'running' && isProcessRunning(agent.pid)) {
|
|
215
|
+
if (killProcess(agent.pid)) {
|
|
216
|
+
updateAgentStatus(state, agent.ticket, 'failed', { error: 'manually stopped' });
|
|
217
|
+
killed++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
saveSwarmState(state);
|
|
223
|
+
console.log(`🛑 Stopped ${killed} agent(s).`);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export default function swarmCommand(args) {
|
|
227
|
+
const { subcommand, flags } = parseSwarmArgs(args);
|
|
228
|
+
|
|
229
|
+
if (subcommand === 'status') {
|
|
230
|
+
showStatus();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (subcommand === 'stop') {
|
|
235
|
+
stopSwarm();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!isGitRepository()) {
|
|
240
|
+
console.error('❌ Not in a git repository.');
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const config = getConfig();
|
|
245
|
+
const ticketsDir = getTicketsDir();
|
|
246
|
+
const repoName = getRepoName();
|
|
247
|
+
const repoRoot = getRepoRoot();
|
|
248
|
+
|
|
249
|
+
const maxAgents = flags.count || config.swarm?.maxAgents || 3;
|
|
250
|
+
const timeout = config.swarm?.timeout || config.agent?.timeout || 900;
|
|
251
|
+
|
|
252
|
+
const allTickets = loadTickets(ticketsDir);
|
|
253
|
+
let tickets = applyFilters(allTickets, flags.filter);
|
|
254
|
+
|
|
255
|
+
// Sort by priority
|
|
256
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
257
|
+
tickets.sort((a, b) => {
|
|
258
|
+
const pa = priorityOrder[a.frontmatter.priority] ?? 2;
|
|
259
|
+
const pb = priorityOrder[b.frontmatter.priority] ?? 2;
|
|
260
|
+
return pa - pb;
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
tickets = tickets.slice(0, maxAgents);
|
|
264
|
+
|
|
265
|
+
if (tickets.length === 0) {
|
|
266
|
+
console.log('No tickets match the filter. Nothing to swarm.');
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.log(`\n🐝 Swarming ${tickets.length} ticket(s) (max: ${maxAgents}, timeout: ${timeout}s)\n`);
|
|
271
|
+
|
|
272
|
+
const worktreeInfos = [];
|
|
273
|
+
for (const ticket of tickets) {
|
|
274
|
+
const branchName = getBranchName(ticket, config);
|
|
275
|
+
const worktreePath = getWorktreePath(repoName, branchName);
|
|
276
|
+
const title = ticket.frontmatter.title || 'Untitled';
|
|
277
|
+
const exists = fs.existsSync(worktreePath);
|
|
278
|
+
|
|
279
|
+
worktreeInfos.push({ ticket, branchName, worktreePath, title, alreadyExists: exists });
|
|
280
|
+
|
|
281
|
+
const status = exists ? '(exists)' : '(new)';
|
|
282
|
+
console.log(` ${ticket.frontmatter.id} — ${title}`);
|
|
283
|
+
console.log(` 🌿 ${branchName} ${status}`);
|
|
284
|
+
console.log(` 📂 ${worktreePath}`);
|
|
285
|
+
console.log('');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (flags.dryRun) {
|
|
289
|
+
console.log('🏁 Dry run — no agents spawned.');
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Create worktrees
|
|
294
|
+
for (const info of worktreeInfos) {
|
|
295
|
+
if (info.alreadyExists) {
|
|
296
|
+
console.log(`✅ ${info.ticket.frontmatter.id}: Worktree exists`);
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
try {
|
|
301
|
+
const branchExists = branchExistsLocally(info.branchName) || branchExistsRemotely(info.branchName);
|
|
302
|
+
if (branchExists) {
|
|
303
|
+
createWorktreeExistingBranch(info.worktreePath, info.branchName);
|
|
304
|
+
} else {
|
|
305
|
+
createWorktree(info.worktreePath, info.branchName, flags.baseBranch);
|
|
306
|
+
}
|
|
307
|
+
console.log(`✅ ${info.ticket.frontmatter.id}: Worktree created`);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error(`❌ ${info.ticket.frontmatter.id}: Worktree failed — ${error.message}`);
|
|
310
|
+
info.failed = true;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
updateTicketStatus(info.ticket, info.worktreePath);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Install dependencies
|
|
318
|
+
if (!flags.noInstall) {
|
|
319
|
+
const pkgExists = fs.existsSync(path.join(repoRoot, 'package.json'));
|
|
320
|
+
if (pkgExists) {
|
|
321
|
+
console.log('\n📦 Installing dependencies...');
|
|
322
|
+
for (const info of worktreeInfos) {
|
|
323
|
+
if (info.failed) continue;
|
|
324
|
+
if (fs.existsSync(path.join(info.worktreePath, 'package.json'))) {
|
|
325
|
+
try {
|
|
326
|
+
execSync('npm install --silent', { cwd: info.worktreePath, stdio: 'ignore' });
|
|
327
|
+
console.log(` ✅ ${info.ticket.frontmatter.id}: npm install done`);
|
|
328
|
+
} catch {
|
|
329
|
+
console.warn(` ⚠️ ${info.ticket.frontmatter.id}: npm install failed`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Create swarm state
|
|
337
|
+
const state = createSwarmState({ maxAgents, timeout });
|
|
338
|
+
|
|
339
|
+
// Spawn agents
|
|
340
|
+
const skillContext = loadSkillContext();
|
|
341
|
+
const logsDir = getLogsDir();
|
|
342
|
+
console.log('\n🤖 Spawning agents...\n');
|
|
343
|
+
|
|
344
|
+
for (const info of worktreeInfos) {
|
|
345
|
+
if (info.failed) continue;
|
|
346
|
+
|
|
347
|
+
const prompt = buildAgentPrompt(info.ticket, null, skillContext);
|
|
348
|
+
const logFile = path.join(logsDir, `${info.ticket.frontmatter.id}.log`);
|
|
349
|
+
|
|
350
|
+
try {
|
|
351
|
+
const pid = spawnAgentWithLogs(prompt, info.worktreePath, timeout, logFile);
|
|
352
|
+
|
|
353
|
+
addAgentToState(state, {
|
|
354
|
+
ticket: info.ticket.frontmatter.id,
|
|
355
|
+
title: info.title,
|
|
356
|
+
pid,
|
|
357
|
+
worktree: info.worktreePath,
|
|
358
|
+
branch: info.branchName,
|
|
359
|
+
logFile
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
console.log(` 🤖 ${info.ticket.frontmatter.id}: Agent spawned (PID ${pid})`);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
console.error(` ❌ ${info.ticket.frontmatter.id}: Failed to spawn — ${error.message}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
saveSwarmState(state);
|
|
369
|
+
|
|
370
|
+
console.log(`\n🏁 Swarm launched! (${state.agents.length} agents)\n`);
|
|
371
|
+
console.log('Monitor progress:');
|
|
372
|
+
console.log(' vibe swarm status # live agent dashboard');
|
|
373
|
+
console.log(' vibe swarm stop # stop all agents');
|
|
374
|
+
console.log(' vibe status # see worktree activity');
|
|
375
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
export function loadSkillContext() {
|
|
9
|
+
try {
|
|
10
|
+
const skillPath = path.join(__dirname, '..', '..', 'skills', 'vibekit', 'SKILL.md');
|
|
11
|
+
return fs.readFileSync(skillPath, 'utf-8');
|
|
12
|
+
} catch {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function buildAgentPrompt(ticket, customPrompt, skillContext) {
|
|
18
|
+
if (customPrompt) return customPrompt;
|
|
19
|
+
|
|
20
|
+
const ticketContent = fs.readFileSync(ticket.filePath, 'utf-8');
|
|
21
|
+
const title = ticket.frontmatter.title || 'Untitled';
|
|
22
|
+
let prompt = `You are working on ticket ${ticket.frontmatter.id}: ${title}\n\nHere is the full ticket:\n\n${ticketContent}\n\nImplement the ticket requirements. Follow the acceptance criteria. Commit your work when done. Update the ticket status to done when complete.`;
|
|
23
|
+
|
|
24
|
+
if (skillContext) {
|
|
25
|
+
prompt += `\n\n--- VibeKit Skill Reference ---\n${skillContext}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return prompt;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function spawnAgent(prompt, cwd, timeoutSeconds) {
|
|
32
|
+
const agentProcess = spawn('claude', ['-p', prompt, '--timeout', String(timeoutSeconds * 1000)], {
|
|
33
|
+
cwd,
|
|
34
|
+
stdio: 'ignore',
|
|
35
|
+
detached: true
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
agentProcess.unref();
|
|
39
|
+
return agentProcess.pid;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function spawnAgentWithLogs(prompt, cwd, timeoutSeconds, logPath) {
|
|
43
|
+
const logDir = path.dirname(logPath);
|
|
44
|
+
if (!fs.existsSync(logDir)) {
|
|
45
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const logStream = fs.openSync(logPath, 'w');
|
|
49
|
+
const agentProcess = spawn('claude', ['-p', prompt, '--timeout', String(timeoutSeconds * 1000)], {
|
|
50
|
+
cwd,
|
|
51
|
+
stdio: ['ignore', logStream, logStream],
|
|
52
|
+
detached: true
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
agentProcess.unref();
|
|
56
|
+
return agentProcess.pid;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isProcessRunning(pid) {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function killProcess(pid) {
|
|
69
|
+
try {
|
|
70
|
+
process.kill(pid, 'SIGTERM');
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { getProjectRoot } from './index.js';
|
|
4
|
+
|
|
5
|
+
function getStateDir() {
|
|
6
|
+
const root = getProjectRoot();
|
|
7
|
+
return path.join(root, '.vibe', '.state');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function getSwarmPath() {
|
|
11
|
+
return path.join(getStateDir(), 'swarm.json');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function getLogsDir() {
|
|
15
|
+
return path.join(getStateDir(), 'logs');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function loadSwarmState() {
|
|
19
|
+
const swarmPath = getSwarmPath();
|
|
20
|
+
if (!fs.existsSync(swarmPath)) return null;
|
|
21
|
+
try {
|
|
22
|
+
return JSON.parse(fs.readFileSync(swarmPath, 'utf-8'));
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function saveSwarmState(state) {
|
|
29
|
+
const stateDir = getStateDir();
|
|
30
|
+
if (!fs.existsSync(stateDir)) {
|
|
31
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
fs.writeFileSync(getSwarmPath(), JSON.stringify(state, null, 2), 'utf-8');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createSwarmState(config) {
|
|
37
|
+
const now = new Date();
|
|
38
|
+
const id = `swarm-${now.toISOString().slice(0, 10).replace(/-/g, '')}-${Date.now().toString(36)}`;
|
|
39
|
+
return {
|
|
40
|
+
id,
|
|
41
|
+
started: now.toISOString(),
|
|
42
|
+
config: {
|
|
43
|
+
maxAgents: config.maxAgents || 3,
|
|
44
|
+
timeout: config.timeout || 900
|
|
45
|
+
},
|
|
46
|
+
agents: []
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function addAgentToState(state, entry) {
|
|
51
|
+
state.agents.push({
|
|
52
|
+
ticket: entry.ticket,
|
|
53
|
+
title: entry.title,
|
|
54
|
+
pid: entry.pid,
|
|
55
|
+
status: 'running',
|
|
56
|
+
worktree: entry.worktree,
|
|
57
|
+
branch: entry.branch,
|
|
58
|
+
logFile: entry.logFile || null,
|
|
59
|
+
startedAt: new Date().toISOString(),
|
|
60
|
+
finishedAt: null,
|
|
61
|
+
pr: null,
|
|
62
|
+
error: null
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function updateAgentStatus(state, ticketId, status, extra = {}) {
|
|
67
|
+
const agent = state.agents.find(a => a.ticket === ticketId);
|
|
68
|
+
if (agent) {
|
|
69
|
+
agent.status = status;
|
|
70
|
+
if (status !== 'running') {
|
|
71
|
+
agent.finishedAt = new Date().toISOString();
|
|
72
|
+
}
|
|
73
|
+
Object.assign(agent, extra);
|
|
74
|
+
}
|
|
75
|
+
}
|