@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,312 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { spawn } from 'child_process';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { getTicketsDir, getConfig, getNextTicketId, createSlug } from '../../utils/index.js';
|
|
6
|
+
import { logger } from '../../utils/cli.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parse CLI arguments for the to-ticket subcommand
|
|
10
|
+
*/
|
|
11
|
+
function parseToTicketArgs(args) {
|
|
12
|
+
let planFile = null;
|
|
13
|
+
let dryRun = false;
|
|
14
|
+
let autoCreate = false;
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
const arg = args[i];
|
|
18
|
+
if (arg === '--dry-run') {
|
|
19
|
+
dryRun = true;
|
|
20
|
+
} else if (arg === '--auto') {
|
|
21
|
+
autoCreate = true;
|
|
22
|
+
} else if (!arg.startsWith('-')) {
|
|
23
|
+
planFile = arg;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return { planFile, dryRun, autoCreate };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Read and validate a plan file
|
|
32
|
+
*/
|
|
33
|
+
function readPlanFile(planFile) {
|
|
34
|
+
if (!planFile) {
|
|
35
|
+
throw new Error('Plan file path is required');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Resolve path relative to cwd if not absolute
|
|
39
|
+
const filePath = path.isAbsolute(planFile)
|
|
40
|
+
? planFile
|
|
41
|
+
: path.join(process.cwd(), planFile);
|
|
42
|
+
|
|
43
|
+
if (!fs.existsSync(filePath)) {
|
|
44
|
+
throw new Error(`Plan file not found: ${filePath}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
48
|
+
return { filePath, content };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Create a prompt for Claude to extract tickets from a plan
|
|
53
|
+
*/
|
|
54
|
+
function createExtractionPrompt(planContent) {
|
|
55
|
+
return `You are a senior software engineer analyzing a project plan.
|
|
56
|
+
|
|
57
|
+
Plan content:
|
|
58
|
+
${planContent}
|
|
59
|
+
|
|
60
|
+
Extract all actionable work items from this plan and convert them to a JSON list of tickets.
|
|
61
|
+
|
|
62
|
+
For each ticket, provide:
|
|
63
|
+
- title: A clear, descriptive title (max 80 chars)
|
|
64
|
+
- description: The main work to be done
|
|
65
|
+
- acceptance_criteria: An array of 2-4 specific, measurable criteria
|
|
66
|
+
- priority: One of: low, medium, high, critical
|
|
67
|
+
- estimated_hours: Rough estimate (0.5, 1, 2, 4, 8, 16, 32)
|
|
68
|
+
|
|
69
|
+
Return ONLY a valid JSON object with this structure:
|
|
70
|
+
{
|
|
71
|
+
"tickets": [
|
|
72
|
+
{
|
|
73
|
+
"title": "...",
|
|
74
|
+
"description": "...",
|
|
75
|
+
"acceptance_criteria": ["...", "..."],
|
|
76
|
+
"priority": "medium",
|
|
77
|
+
"estimated_hours": 4
|
|
78
|
+
}
|
|
79
|
+
]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Ensure each ticket is focused, actionable, and independent.`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse Claude envelope and extract text
|
|
87
|
+
*/
|
|
88
|
+
function parseClaudeEnvelope(raw) {
|
|
89
|
+
let envelope;
|
|
90
|
+
try {
|
|
91
|
+
envelope = JSON.parse(raw.trim());
|
|
92
|
+
} catch {
|
|
93
|
+
throw new Error('Claude returned non-JSON output');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (envelope.is_error) {
|
|
97
|
+
throw new Error(envelope.result || 'Claude reported an error');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const text = envelope.result ?? '';
|
|
101
|
+
if (!text.trim()) {
|
|
102
|
+
throw new Error('Claude returned an empty result');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return text;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Extract JSON from Claude's text response
|
|
110
|
+
*/
|
|
111
|
+
function extractJsonFromResponse(text) {
|
|
112
|
+
const cleaned = text.trim();
|
|
113
|
+
|
|
114
|
+
// 1. Direct parse ā Claude responded with pure JSON
|
|
115
|
+
try {
|
|
116
|
+
JSON.parse(cleaned);
|
|
117
|
+
return cleaned;
|
|
118
|
+
} catch { /* fall through */ }
|
|
119
|
+
|
|
120
|
+
// 2. Markdown code block ā ```json ... ``` or ``` ... ```
|
|
121
|
+
const codeBlockMatch = cleaned.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
122
|
+
if (codeBlockMatch) {
|
|
123
|
+
const inner = codeBlockMatch[1].trim();
|
|
124
|
+
try {
|
|
125
|
+
JSON.parse(inner);
|
|
126
|
+
return inner;
|
|
127
|
+
} catch { /* fall through */ }
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 3. Loose extraction ā grab first {...} block
|
|
131
|
+
const objMatch = cleaned.match(/\{[\s\S]*\}/);
|
|
132
|
+
if (objMatch) {
|
|
133
|
+
try {
|
|
134
|
+
JSON.parse(objMatch[0]);
|
|
135
|
+
return objMatch[0];
|
|
136
|
+
} catch { /* fall through */ }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
throw new Error('No valid JSON object found in Claude response');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Execute Claude to extract tickets from plan
|
|
144
|
+
*/
|
|
145
|
+
async function extractTicketsFromPlan(planContent) {
|
|
146
|
+
const prompt = createExtractionPrompt(planContent);
|
|
147
|
+
|
|
148
|
+
return new Promise((resolve, reject) => {
|
|
149
|
+
const env = { ...process.env };
|
|
150
|
+
delete env.ANTHROPIC_API_KEY;
|
|
151
|
+
|
|
152
|
+
const child = spawn('claude', ['--print', '--output-format', 'json'], {
|
|
153
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
154
|
+
env
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
let stdout = '';
|
|
158
|
+
let stderr = '';
|
|
159
|
+
|
|
160
|
+
child.stdout.on('data', (chunk) => {
|
|
161
|
+
stdout += chunk;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
child.stderr.on('data', (chunk) => {
|
|
165
|
+
stderr += chunk;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
child.on('error', (err) => {
|
|
169
|
+
reject(new Error(`Failed to spawn Claude: ${err.message}`));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
child.on('close', (code) => {
|
|
173
|
+
if (!stdout.trim()) {
|
|
174
|
+
reject(new Error(stderr.trim() || `Claude exited with code ${code}`));
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const text = parseClaudeEnvelope(stdout);
|
|
180
|
+
const jsonStr = extractJsonFromResponse(text);
|
|
181
|
+
const result = JSON.parse(jsonStr);
|
|
182
|
+
resolve(result.tickets || []);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
reject(new Error(`Failed to parse Claude response: ${err.message}`));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
child.stdin.write(prompt, 'utf8');
|
|
189
|
+
child.stdin.end();
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Create a ticket in .vibe/tickets/
|
|
195
|
+
*/
|
|
196
|
+
function createTicketFile(ticketData, ticketsDir, ticketId) {
|
|
197
|
+
const slug = createSlug(ticketData.title);
|
|
198
|
+
const fileName = `${ticketId}-${slug}.md`;
|
|
199
|
+
const filePath = path.join(ticketsDir, fileName);
|
|
200
|
+
|
|
201
|
+
const acceptanceCriteria = (ticketData.acceptance_criteria || [])
|
|
202
|
+
.map(criterion => `- [ ] ${criterion}`)
|
|
203
|
+
.join('\n');
|
|
204
|
+
|
|
205
|
+
const content = `---
|
|
206
|
+
id: ${ticketId}
|
|
207
|
+
title: ${ticketData.title}
|
|
208
|
+
slug: ${slug}
|
|
209
|
+
description: ${ticketData.description || ''}
|
|
210
|
+
priority: ${ticketData.priority || 'medium'}
|
|
211
|
+
status: open
|
|
212
|
+
created_at: "${new Date().toISOString()}"
|
|
213
|
+
updated_at: "${new Date().toISOString()}"
|
|
214
|
+
estimated_hours: ${ticketData.estimated_hours || 0}
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Description
|
|
218
|
+
|
|
219
|
+
${ticketData.description || 'No description provided'}
|
|
220
|
+
|
|
221
|
+
## Acceptance Criteria
|
|
222
|
+
|
|
223
|
+
${acceptanceCriteria || '- [ ] Complete the work'}
|
|
224
|
+
|
|
225
|
+
## Notes
|
|
226
|
+
|
|
227
|
+
Generated from plan via \`vibe plan to-ticket\`.
|
|
228
|
+
`;
|
|
229
|
+
|
|
230
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
231
|
+
return { fileName, filePath };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Main to-ticket subcommand
|
|
236
|
+
*/
|
|
237
|
+
async function toTicketCommand(args) {
|
|
238
|
+
try {
|
|
239
|
+
const { planFile, dryRun, autoCreate } = parseToTicketArgs(args);
|
|
240
|
+
|
|
241
|
+
// Validate plan file
|
|
242
|
+
const { filePath, content: planContent } = readPlanFile(planFile);
|
|
243
|
+
|
|
244
|
+
console.log(`š Reading plan: ${path.basename(filePath)}`);
|
|
245
|
+
console.log('');
|
|
246
|
+
|
|
247
|
+
// Extract tickets using Claude
|
|
248
|
+
console.log('š¤ Analyzing plan with Claude...');
|
|
249
|
+
const tickets = await extractTicketsFromPlan(planContent);
|
|
250
|
+
|
|
251
|
+
if (!tickets || tickets.length === 0) {
|
|
252
|
+
console.log('ā ļø No tickets extracted from plan.');
|
|
253
|
+
process.exit(0);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
console.log(`ā
Extracted ${tickets.length} ticket(s)\n`);
|
|
257
|
+
|
|
258
|
+
// Show extracted tickets
|
|
259
|
+
for (const ticket of tickets) {
|
|
260
|
+
console.log(`š ${ticket.title}`);
|
|
261
|
+
console.log(` Priority: ${ticket.priority || 'medium'}`);
|
|
262
|
+
console.log(` Est. hours: ${ticket.estimated_hours || '?'}`);
|
|
263
|
+
if (ticket.acceptance_criteria && ticket.acceptance_criteria.length > 0) {
|
|
264
|
+
console.log(` Criteria: ${ticket.acceptance_criteria.length} items`);
|
|
265
|
+
}
|
|
266
|
+
console.log('');
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (dryRun) {
|
|
270
|
+
console.log('š Dry run complete. No tickets created.');
|
|
271
|
+
process.exit(0);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (!autoCreate) {
|
|
275
|
+
console.log('š” Use --auto to create these tickets automatically.');
|
|
276
|
+
process.exit(0);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Create tickets
|
|
280
|
+
const ticketsDir = getTicketsDir();
|
|
281
|
+
if (!fs.existsSync(ticketsDir)) {
|
|
282
|
+
fs.mkdirSync(ticketsDir, { recursive: true });
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.log('⨠Creating tickets...\n');
|
|
286
|
+
|
|
287
|
+
const createdTickets = [];
|
|
288
|
+
for (const ticketData of tickets) {
|
|
289
|
+
try {
|
|
290
|
+
const ticketId = getNextTicketId(ticketsDir);
|
|
291
|
+
const result = createTicketFile(ticketData, ticketsDir, ticketId);
|
|
292
|
+
createdTickets.push({ id: ticketId, ...result });
|
|
293
|
+
console.log(` ā
${ticketId}: ${ticketData.title}`);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
console.error(` ā Failed to create ticket: ${error.message}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
console.log('');
|
|
300
|
+
console.log(`š Created ${createdTickets.length} ticket(s)`);
|
|
301
|
+
console.log('');
|
|
302
|
+
console.log('Next steps:');
|
|
303
|
+
console.log(' vibe list # See all tickets');
|
|
304
|
+
console.log(' vibe start TKT-XXX # Start working on a ticket');
|
|
305
|
+
|
|
306
|
+
} catch (error) {
|
|
307
|
+
console.error(`ā ${error.message}`);
|
|
308
|
+
process.exit(1);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export default toTicketCommand;
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { jest } from '@jest/globals';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
let ticketIdCounter = 1;
|
|
6
|
+
jest.unstable_mockModule('../../utils/index.js', () => ({
|
|
7
|
+
getTicketsDir: jest.fn(),
|
|
8
|
+
getConfig: jest.fn(() => ({})),
|
|
9
|
+
createSlug: jest.fn(title => title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')),
|
|
10
|
+
getNextTicketId: jest.fn(() => `TKT-${String(ticketIdCounter++).padStart(3, '0')}`)
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const mockChild = {
|
|
14
|
+
stdout: { on: jest.fn() },
|
|
15
|
+
stderr: { on: jest.fn() },
|
|
16
|
+
stdin: { write: jest.fn(), end: jest.fn() },
|
|
17
|
+
on: jest.fn()
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
jest.unstable_mockModule('child_process', () => ({
|
|
21
|
+
spawn: jest.fn(() => mockChild)
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
const { default: toTicketCommand } = await import('./to-ticket.js');
|
|
25
|
+
const { getTicketsDir } = await import('../../utils/index.js');
|
|
26
|
+
const childProcess = await import('child_process');
|
|
27
|
+
|
|
28
|
+
const PLAN_CONTENT = `# Feature Plan
|
|
29
|
+
|
|
30
|
+
## Goals
|
|
31
|
+
- Build a login page
|
|
32
|
+
- Add a settings page
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
const CLAUDE_TICKETS = {
|
|
36
|
+
tickets: [
|
|
37
|
+
{
|
|
38
|
+
title: 'Build login page',
|
|
39
|
+
description: 'Create the login form',
|
|
40
|
+
acceptance_criteria: ['Form renders', 'Submits credentials'],
|
|
41
|
+
priority: 'high',
|
|
42
|
+
estimated_hours: 4
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
title: 'Add settings page',
|
|
46
|
+
description: 'User settings screen',
|
|
47
|
+
acceptance_criteria: ['Settings persist'],
|
|
48
|
+
priority: 'medium',
|
|
49
|
+
estimated_hours: 2
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Drive the mocked Claude process: feed it a JSON envelope and close.
|
|
55
|
+
function primeClaude(resultObj, { isError = false } = {}) {
|
|
56
|
+
const envelope = JSON.stringify({
|
|
57
|
+
is_error: isError,
|
|
58
|
+
result: isError ? 'boom' : JSON.stringify(resultObj)
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
mockChild.stdout.on.mockImplementation((event, cb) => {
|
|
62
|
+
if (event === 'data') cb(Buffer.from(envelope));
|
|
63
|
+
});
|
|
64
|
+
mockChild.stderr.on.mockImplementation(() => {});
|
|
65
|
+
mockChild.on.mockImplementation((event, cb) => {
|
|
66
|
+
if (event === 'close') cb(0);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('plan to-ticket command', () => {
|
|
71
|
+
let tempDir;
|
|
72
|
+
let ticketsDir;
|
|
73
|
+
let planFile;
|
|
74
|
+
let mockExit;
|
|
75
|
+
let mockConsoleLog;
|
|
76
|
+
let mockConsoleError;
|
|
77
|
+
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
ticketIdCounter = 1;
|
|
80
|
+
tempDir = fs.mkdtempSync(path.join('/tmp', 'vibe-to-ticket-'));
|
|
81
|
+
ticketsDir = path.join(tempDir, '.vibe', 'tickets');
|
|
82
|
+
fs.mkdirSync(ticketsDir, { recursive: true });
|
|
83
|
+
planFile = path.join(tempDir, 'plan.md');
|
|
84
|
+
fs.writeFileSync(planFile, PLAN_CONTENT);
|
|
85
|
+
|
|
86
|
+
getTicketsDir.mockReturnValue(ticketsDir);
|
|
87
|
+
mockExit = jest.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit'); });
|
|
88
|
+
mockConsoleLog = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
89
|
+
mockConsoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
afterEach(() => {
|
|
93
|
+
mockExit.mockRestore();
|
|
94
|
+
mockConsoleLog.mockRestore();
|
|
95
|
+
mockConsoleError.mockRestore();
|
|
96
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
97
|
+
jest.clearAllMocks();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test('errors when plan file is missing', async () => {
|
|
101
|
+
await expect(toTicketCommand(['/tmp/does-not-exist.md'])).rejects.toThrow('process.exit');
|
|
102
|
+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('Plan file not found'));
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('dry-run extracts tickets without creating files', async () => {
|
|
106
|
+
primeClaude(CLAUDE_TICKETS);
|
|
107
|
+
await expect(toTicketCommand([planFile, '--dry-run'])).rejects.toThrow('process.exit');
|
|
108
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('Dry run complete'));
|
|
109
|
+
expect(fs.readdirSync(ticketsDir)).toHaveLength(0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('preview (no --auto) does not create tickets', async () => {
|
|
113
|
+
primeClaude(CLAUDE_TICKETS);
|
|
114
|
+
await expect(toTicketCommand([planFile])).rejects.toThrow('process.exit');
|
|
115
|
+
expect(mockConsoleLog).toHaveBeenCalledWith(expect.stringContaining('--auto'));
|
|
116
|
+
expect(fs.readdirSync(ticketsDir)).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('--auto creates one ticket file per extracted item', async () => {
|
|
120
|
+
primeClaude(CLAUDE_TICKETS);
|
|
121
|
+
await toTicketCommand([planFile, '--auto']);
|
|
122
|
+
const files = fs.readdirSync(ticketsDir).sort();
|
|
123
|
+
expect(files).toEqual([
|
|
124
|
+
'TKT-001-build-login-page.md',
|
|
125
|
+
'TKT-002-add-settings-page.md'
|
|
126
|
+
].sort());
|
|
127
|
+
|
|
128
|
+
const first = fs.readFileSync(path.join(ticketsDir, 'TKT-001-build-login-page.md'), 'utf-8');
|
|
129
|
+
expect(first).toContain('id: TKT-001');
|
|
130
|
+
expect(first).toContain('priority: high');
|
|
131
|
+
expect(first).toContain('- [ ] Form renders');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('surfaces Claude errors', async () => {
|
|
135
|
+
primeClaude(null, { isError: true });
|
|
136
|
+
await expect(toTicketCommand([planFile, '--auto'])).rejects.toThrow('process.exit');
|
|
137
|
+
expect(mockConsoleError).toHaveBeenCalledWith(expect.stringContaining('boom'));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('spawns claude with json output format', async () => {
|
|
141
|
+
primeClaude(CLAUDE_TICKETS);
|
|
142
|
+
await toTicketCommand([planFile, '--auto']);
|
|
143
|
+
expect(childProcess.spawn).toHaveBeenCalledWith(
|
|
144
|
+
'claude',
|
|
145
|
+
expect.arrayContaining(['--print', '--output-format', 'json']),
|
|
146
|
+
expect.any(Object)
|
|
147
|
+
);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -3,7 +3,6 @@ import path from 'path';
|
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
4
|
import { execSync, spawn } from 'child_process';
|
|
5
5
|
import { getTicketsDir, getConfig, createSlug } from '../../utils/index.js';
|
|
6
|
-
import { fileURLToPath } from 'url';
|
|
7
6
|
import {
|
|
8
7
|
isGitRepository,
|
|
9
8
|
getCurrentBranch,
|
|
@@ -19,16 +18,7 @@ import {
|
|
|
19
18
|
getRepoRoot,
|
|
20
19
|
getDefaultBaseBranch
|
|
21
20
|
} from '../../utils/git.js';
|
|
22
|
-
|
|
23
|
-
function loadSkillContext() {
|
|
24
|
-
try {
|
|
25
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
-
const skillPath = path.join(__dirname, '..', '..', '..', 'skills', 'vibekit', 'SKILL.md');
|
|
27
|
-
return fs.readFileSync(skillPath, 'utf-8');
|
|
28
|
-
} catch {
|
|
29
|
-
return '';
|
|
30
|
-
}
|
|
31
|
-
}
|
|
21
|
+
import { loadSkillContext, buildAgentPrompt, spawnAgent } from '../../utils/agent.js';
|
|
32
22
|
|
|
33
23
|
function parseTicketIds(args) {
|
|
34
24
|
const ids = [];
|
|
@@ -280,24 +270,15 @@ function startCommand(args) {
|
|
|
280
270
|
}
|
|
281
271
|
|
|
282
272
|
if (spawnAgent) {
|
|
283
|
-
const agentTimeout = config.worktree?.agent?.timeout || 900;
|
|
273
|
+
const agentTimeout = config.worktree?.agent?.timeout || config.agent?.timeout || 900;
|
|
284
274
|
const skillContext = loadSkillContext();
|
|
285
275
|
console.log('\nš¤ Spawning Claude agents...\n');
|
|
286
276
|
for (const info of worktreeInfos) {
|
|
287
|
-
const
|
|
288
|
-
const prompt = flags.prompt
|
|
289
|
-
? flags.prompt
|
|
290
|
-
: `You are working on ticket ${info.ticket.frontmatter.id}: ${info.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.${skillContext ? `\n\n--- VibeKit Skill Reference ---\n${skillContext}` : ''}`;
|
|
277
|
+
const prompt = buildAgentPrompt(info.ticket, flags.prompt, skillContext);
|
|
291
278
|
|
|
292
279
|
try {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
stdio: 'ignore',
|
|
296
|
-
detached: true
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
agentProcess.unref();
|
|
300
|
-
console.log(` š¤ ${info.ticket.frontmatter.id}: Agent spawned in ${info.worktreePath} (timeout: ${agentTimeout}s)`);
|
|
280
|
+
const pid = spawnAgent(prompt, info.worktreePath, agentTimeout);
|
|
281
|
+
console.log(` š¤ ${info.ticket.frontmatter.id}: Agent spawned in ${info.worktreePath} (PID ${pid}, timeout: ${agentTimeout}s)`);
|
|
301
282
|
} catch (error) {
|
|
302
283
|
console.error(` ā ${info.ticket.frontmatter.id}: Failed to spawn agent ā ${error.message}`);
|
|
303
284
|
}
|
|
@@ -337,23 +318,14 @@ function startCommand(args) {
|
|
|
337
318
|
|
|
338
319
|
if (spawnAgent) {
|
|
339
320
|
const ticket = tickets[0];
|
|
340
|
-
const agentTimeout = config.worktree?.agent?.timeout || 900;
|
|
321
|
+
const agentTimeout = config.worktree?.agent?.timeout || config.agent?.timeout || 900;
|
|
341
322
|
const skillContext = loadSkillContext();
|
|
342
|
-
const
|
|
343
|
-
const prompt = flags.prompt
|
|
344
|
-
? flags.prompt
|
|
345
|
-
: `You are working on ticket ${ticket.frontmatter.id}: ${ticket.frontmatter.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.${skillContext ? `\n\n--- VibeKit Skill Reference ---\n${skillContext}` : ''}`;
|
|
323
|
+
const prompt = buildAgentPrompt(ticket, flags.prompt, skillContext);
|
|
346
324
|
|
|
347
325
|
console.log('\nš¤ Spawning Claude agent...\n');
|
|
348
326
|
try {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
stdio: 'ignore',
|
|
352
|
-
detached: true
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
agentProcess.unref();
|
|
356
|
-
console.log(` š¤ ${ticket.frontmatter.id}: Agent spawned (timeout: ${agentTimeout}s)`);
|
|
327
|
+
const pid = spawnAgent(prompt, process.cwd(), agentTimeout);
|
|
328
|
+
console.log(` š¤ ${ticket.frontmatter.id}: Agent spawned (PID ${pid}, timeout: ${agentTimeout}s)`);
|
|
357
329
|
console.log('\nš Agent launched!\n');
|
|
358
330
|
console.log('Monitor progress:');
|
|
359
331
|
console.log(' vibe status # see ticket status');
|