@vibedx/vibekit 0.8.8 → 0.10.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/index.js +1 -1
- package/package.json +3 -2
- package/skills/vibekit/SKILL.md +54 -0
- package/src/commands/init/index.js +2 -1
- 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/vibekit-plugin/.claude-plugin/plugin.json +20 -0
- package/vibekit-plugin/README.md +57 -0
- package/vibekit-plugin/agents/reviewer.md +47 -0
- package/vibekit-plugin/agents/ticket-worker.md +47 -0
- package/vibekit-plugin/hooks/detect-vibe.sh +15 -0
- package/vibekit-plugin/hooks/hooks.json +17 -0
- package/vibekit-plugin/settings.json +9 -0
- package/vibekit-plugin/skills/ticket-writer/SKILL.md +80 -0
- package/vibekit-plugin/skills/vibekit-workflow/SKILL.md +319 -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
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibekit",
|
|
3
|
+
"displayName": "VibeKit — Ticket-Driven Development",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "Structured ticket workflows, agent orchestration, and worktree isolation for AI-assisted development",
|
|
6
|
+
"author": "vibedx",
|
|
7
|
+
"repository": "https://github.com/vibedx/vibekit",
|
|
8
|
+
"homepage": "https://github.com/vibedx/vibekit",
|
|
9
|
+
"keywords": ["tickets", "workflow", "agents", "worktrees"],
|
|
10
|
+
"skills": [
|
|
11
|
+
"skills/vibekit-workflow",
|
|
12
|
+
"skills/ticket-writer"
|
|
13
|
+
],
|
|
14
|
+
"agents": [
|
|
15
|
+
"agents/ticket-worker.md",
|
|
16
|
+
"agents/reviewer.md"
|
|
17
|
+
],
|
|
18
|
+
"hooks": "hooks/hooks.json",
|
|
19
|
+
"settings": "settings.json"
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# VibeKit — Claude Code Plugin
|
|
2
|
+
|
|
3
|
+
Ticket-driven development workflows for Claude Code. Install once; every project with a `.vibe/` directory gets structured ticket workflows, agent orchestration, and worktree isolation.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/plugin install vibekit
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or from the community marketplace browser in Claude Code.
|
|
12
|
+
|
|
13
|
+
## What's included
|
|
14
|
+
|
|
15
|
+
**Skills**
|
|
16
|
+
- `vibekit-workflow` — teaches Claude the full ticket-driven workflow (create → start → implement → close)
|
|
17
|
+
- `ticket-writer` — helps write well-structured tickets with actionable acceptance criteria
|
|
18
|
+
|
|
19
|
+
**Agents**
|
|
20
|
+
- `ticket-worker` — autonomous agent that reads a ticket, implements it, and closes it
|
|
21
|
+
- `reviewer` — reviews a completed ticket against its acceptance criteria
|
|
22
|
+
|
|
23
|
+
**Hooks**
|
|
24
|
+
- `SessionStart` — detects `.vibe/` directory and surfaces open ticket counts on session start
|
|
25
|
+
|
|
26
|
+
## Requirements
|
|
27
|
+
|
|
28
|
+
The VibeKit CLI is optional but recommended for full functionality:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install -g @vibedx/vibekit
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Without the CLI, skills and agents still work — you just won't get the `vibe` commands for branch management and ticket status updates.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# In any project
|
|
40
|
+
vibe init # set up .vibe/ directory
|
|
41
|
+
vibe new "Add user auth" --priority high -n # create a ticket
|
|
42
|
+
vibe start TKT-001 # start working (creates branch)
|
|
43
|
+
# ... implement the work ...
|
|
44
|
+
vibe close TKT-001 # mark done
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or let an agent do it:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
vibe start TKT-001 --agent # spawns Claude agent to work on the ticket autonomously
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## Links
|
|
54
|
+
|
|
55
|
+
- [VibeKit repo](https://github.com/vibedx/vibekit)
|
|
56
|
+
- [npm package](https://www.npmjs.com/package/@vibedx/vibekit)
|
|
57
|
+
- [Claude Code plugin docs](https://code.claude.com/docs/en/discover-plugins)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Reviews a completed VibeKit ticket against its acceptance criteria. Checks out the branch, reads the ticket, inspects the diff, and reports whether all criteria are met. Use after a ticket-worker agent has finished, or before merging a PR.
|
|
4
|
+
tools:
|
|
5
|
+
- Bash
|
|
6
|
+
- Read
|
|
7
|
+
- Glob
|
|
8
|
+
- Grep
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are a senior code reviewer. Your job is to verify that a VibeKit ticket's implementation matches its acceptance criteria — not to redesign it.
|
|
12
|
+
|
|
13
|
+
## Workflow
|
|
14
|
+
|
|
15
|
+
1. **Read the ticket** — `cat .vibe/tickets/TKT-XXX-*.md`. Extract the acceptance criteria.
|
|
16
|
+
|
|
17
|
+
2. **Inspect the diff** — `git diff main...HEAD` (or the relevant base branch). Understand what changed.
|
|
18
|
+
|
|
19
|
+
3. **Check each criterion** — For every `- [ ]` item in the acceptance criteria, determine: is this met by the implementation? Be concrete — quote code or explain what's missing.
|
|
20
|
+
|
|
21
|
+
4. **Run tests if present** — `npm test` / `pytest` / whatever the project uses. A failing test suite is a blocking issue.
|
|
22
|
+
|
|
23
|
+
5. **Report** — Produce a structured review:
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
## TKT-XXX Review
|
|
27
|
+
|
|
28
|
+
### ✅ Passing criteria
|
|
29
|
+
- [ ] Criterion 1 — met: <brief evidence>
|
|
30
|
+
- [ ] Criterion 2 — met: <brief evidence>
|
|
31
|
+
|
|
32
|
+
### ❌ Failing criteria
|
|
33
|
+
- [ ] Criterion 3 — NOT met: <what's missing and where>
|
|
34
|
+
|
|
35
|
+
### Code notes
|
|
36
|
+
<Optional: any non-blocking observations about code quality, style, or potential bugs>
|
|
37
|
+
|
|
38
|
+
### Verdict
|
|
39
|
+
APPROVED / NEEDS WORK
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Rules
|
|
43
|
+
|
|
44
|
+
- Judge against the ticket's criteria, not your personal preferences. If it meets criteria, approve it even if you'd have done it differently.
|
|
45
|
+
- Be specific about failures. "It doesn't work" is not useful. "The redirect on line 42 of auth.js still points to /dashboard instead of the original destination" is useful.
|
|
46
|
+
- Don't block on style issues. Note them, but don't fail the review for them.
|
|
47
|
+
- If acceptance criteria are too vague to verify, flag that in your review — it's a ticket quality issue, not an implementation issue.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ticket-worker
|
|
3
|
+
description: Works autonomously on a single VibeKit ticket. Reads the ticket, implements the work in a branch or worktree, commits with ticket references, and closes the ticket when done. Use this agent when you need to hand off a ticket for autonomous execution.
|
|
4
|
+
tools:
|
|
5
|
+
- Bash
|
|
6
|
+
- Read
|
|
7
|
+
- Edit
|
|
8
|
+
- Write
|
|
9
|
+
- Glob
|
|
10
|
+
- Grep
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
You are a senior software engineer executing a VibeKit ticket. Your job is to read the ticket thoroughly, implement all acceptance criteria, and leave the codebase better than you found it.
|
|
14
|
+
|
|
15
|
+
## Workflow
|
|
16
|
+
|
|
17
|
+
1. **Read the ticket** — `cat .vibe/tickets/TKT-XXX-*.md`. Understand what's needed and why before touching any code.
|
|
18
|
+
|
|
19
|
+
2. **Start the ticket** — `vibe start TKT-XXX`. This creates the feature branch and marks the ticket `in_progress`.
|
|
20
|
+
|
|
21
|
+
3. **Implement** — Work through the acceptance criteria systematically. Commit after each logical unit of work:
|
|
22
|
+
```bash
|
|
23
|
+
git commit -m "TKT-XXX: descriptive message about what this commit does"
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
4. **Verify** — Check every acceptance criterion is met. Run tests if they exist. Don't close until all criteria pass.
|
|
27
|
+
|
|
28
|
+
5. **Close** — `vibe close TKT-XXX`. This marks the ticket `done`.
|
|
29
|
+
|
|
30
|
+
6. **Open a PR if requested** — `vibe pr` opens a GitHub PR with the ticket content as the description.
|
|
31
|
+
|
|
32
|
+
## Rules
|
|
33
|
+
|
|
34
|
+
- Read the full ticket before writing any code. Misunderstanding scope is the most common failure.
|
|
35
|
+
- Commit early and often with ticket references. Each commit message should start with `TKT-XXX:`.
|
|
36
|
+
- If you hit a blocker that requires a product decision, update the ticket with your finding and stop — don't guess.
|
|
37
|
+
- Do not add features beyond what the acceptance criteria require. Scope creep wastes time.
|
|
38
|
+
- Leave no commented-out code, no TODO comments, and no half-finished work in commits.
|
|
39
|
+
|
|
40
|
+
## On Ambiguity
|
|
41
|
+
|
|
42
|
+
If the ticket is unclear on a specific point, use best judgment for code-level decisions (naming, structure, patterns) but flag any product-level uncertainty in the ticket before closing:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# Append a note to the ticket
|
|
46
|
+
echo "\n## Blockers\n- [ ] Need decision on X before this can be fully closed" >> .vibe/tickets/TKT-XXX-*.md
|
|
47
|
+
```
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SessionStart hook — detect .vibe/ and emit context for Claude
|
|
3
|
+
|
|
4
|
+
if [ -d ".vibe" ]; then
|
|
5
|
+
echo "VibeKit project detected."
|
|
6
|
+
|
|
7
|
+
if command -v vibe &>/dev/null; then
|
|
8
|
+
OPEN=$(vibe list --status=open 2>/dev/null | wc -l | tr -d ' ')
|
|
9
|
+
IN_PROGRESS=$(vibe list --status=in_progress 2>/dev/null | wc -l | tr -d ' ')
|
|
10
|
+
echo "Tickets: ${IN_PROGRESS} in progress, ${OPEN} open."
|
|
11
|
+
echo "Use 'vibe list' to see tickets, 'vibe start TKT-XXX' to begin work."
|
|
12
|
+
else
|
|
13
|
+
echo "Install vibekit CLI: npm install -g @vibedx/vibekit"
|
|
14
|
+
fi
|
|
15
|
+
fi
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": [
|
|
3
|
+
{
|
|
4
|
+
"event": "SessionStart",
|
|
5
|
+
"description": "Detect .vibe/ directory and load ticket context",
|
|
6
|
+
"command": "bash hooks/detect-vibe.sh"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"event": "PostToolUse",
|
|
10
|
+
"matcher": {
|
|
11
|
+
"tool": "Bash",
|
|
12
|
+
"pattern": "git commit"
|
|
13
|
+
},
|
|
14
|
+
"description": "Remind agent to update ticket status after commits"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|