@vibedx/vibekit 0.9.0 → 0.11.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.
@@ -0,0 +1,137 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { getDocsDir } from '../../utils/doc.js';
5
+
6
+ /**
7
+ * Parse `vibe docs list` filter flags.
8
+ * @param {string[]} args
9
+ * @returns {{type: string|null, status: string|null, tag: string|null}}
10
+ */
11
+ function parseFilters(args) {
12
+ let type = null;
13
+ let status = null;
14
+ let tag = null;
15
+
16
+ for (let i = 0; i < args.length; i++) {
17
+ const arg = args[i];
18
+ if (arg === '--type' && i + 1 < args.length) {
19
+ type = args[++i];
20
+ } else if (arg === '--status' && i + 1 < args.length) {
21
+ status = args[++i];
22
+ } else if (arg === '--tag' && i + 1 < args.length) {
23
+ tag = args[++i];
24
+ } else if (arg.startsWith('--type=')) {
25
+ type = arg.split('=')[1];
26
+ } else if (arg.startsWith('--status=')) {
27
+ status = arg.split('=')[1];
28
+ } else if (arg.startsWith('--tag=')) {
29
+ tag = arg.split('=')[1];
30
+ }
31
+ }
32
+
33
+ return { type, status, tag };
34
+ }
35
+
36
+ function normalizeTags(raw) {
37
+ if (Array.isArray(raw)) return raw.map(String);
38
+ if (typeof raw === 'string' && raw.trim()) {
39
+ return raw.replace(/[\[\]]/g, '').split(',').map(t => t.trim()).filter(Boolean);
40
+ }
41
+ return [];
42
+ }
43
+
44
+ /**
45
+ * `vibe docs list` — render a filtered table of docs.
46
+ * @param {string[]} args
47
+ */
48
+ function listDocs(args) {
49
+ const { type, status, tag } = parseFilters(args);
50
+ const docsDir = getDocsDir();
51
+
52
+ if (!fs.existsSync(docsDir)) {
53
+ console.log('No docs found. Create one with: vibe docs add "title"');
54
+ return;
55
+ }
56
+
57
+ const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
58
+
59
+ if (files.length === 0) {
60
+ console.log('No docs found. Create one with: vibe docs add "title"');
61
+ return;
62
+ }
63
+
64
+ let docs = [];
65
+ for (const file of files) {
66
+ try {
67
+ const content = fs.readFileSync(path.join(docsDir, file), 'utf-8');
68
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
69
+ if (match) {
70
+ const fm = yaml.load(match[1]) || {};
71
+ docs.push({
72
+ id: fm.id || 'Unknown',
73
+ title: fm.title || 'Untitled',
74
+ type: fm.type || 'guide',
75
+ status: fm.status || 'draft',
76
+ tags: normalizeTags(fm.tags),
77
+ file
78
+ });
79
+ }
80
+ } catch {
81
+ console.warn(`⚠️ Could not parse doc: ${file}`);
82
+ }
83
+ }
84
+
85
+ if (type) docs = docs.filter(d => d.type === type);
86
+ if (status) docs = docs.filter(d => d.status === status);
87
+ if (tag) docs = docs.filter(d => d.tags.includes(tag));
88
+
89
+ docs.sort((a, b) => {
90
+ const na = parseInt(a.id.replace(/\D/g, ''), 10) || 0;
91
+ const nb = parseInt(b.id.replace(/\D/g, ''), 10) || 0;
92
+ return na - nb;
93
+ });
94
+
95
+ const filters = [
96
+ type ? `type: ${type}` : '',
97
+ status ? `status: ${status}` : '',
98
+ tag ? `tag: ${tag}` : ''
99
+ ].filter(Boolean).join(', ');
100
+
101
+ if (docs.length === 0) {
102
+ console.log(filters ? `No docs found (${filters}).` : 'No docs found.');
103
+ return;
104
+ }
105
+
106
+ console.log('\n📚 VibeKit Docs 📚\n');
107
+
108
+ const idWidth = 10;
109
+ const typeWidth = 12;
110
+ const statusWidth = 11;
111
+ const titleWidth = 40;
112
+
113
+ console.log(
114
+ `${'ID'.padEnd(idWidth)}| ${'TYPE'.padEnd(typeWidth)}| ${'STATUS'.padEnd(statusWidth)}| TITLE`
115
+ );
116
+ console.log(`${'-'.repeat(idWidth)}+${'-'.repeat(typeWidth + 1)}+${'-'.repeat(statusWidth + 1)}+${'-'.repeat(titleWidth)}`);
117
+
118
+ for (const doc of docs) {
119
+ let statusColor = '\x1b[0m';
120
+ if (doc.status === 'published') statusColor = '\x1b[32m';
121
+ else if (doc.status === 'review') statusColor = '\x1b[36m';
122
+ else if (doc.status === 'draft') statusColor = '\x1b[33m';
123
+
124
+ const title = doc.title.length > titleWidth - 3
125
+ ? doc.title.substring(0, titleWidth - 3) + '...'
126
+ : doc.title;
127
+
128
+ console.log(
129
+ `${doc.id.padEnd(idWidth)}| ${doc.type.padEnd(typeWidth)}| ${statusColor}${doc.status.padEnd(statusWidth)}\x1b[0m| ${title}`
130
+ );
131
+ }
132
+
133
+ console.log(`${'-'.repeat(idWidth)}+${'-'.repeat(typeWidth + 1)}+${'-'.repeat(statusWidth + 1)}+${'-'.repeat(titleWidth)}`);
134
+ console.log(`Found ${docs.length} doc(s)${filters ? ` (${filters})` : ''}.\n`);
135
+ }
136
+
137
+ export default listDocs;
@@ -0,0 +1,138 @@
1
+ import fs from 'fs';
2
+ import { spawn } from 'child_process';
3
+ import { getConfig } from '../../utils/index.js';
4
+ import { resolveDocId, parseDoc } from '../../utils/doc.js';
5
+ import { logger, spinner } from '../../utils/cli.js';
6
+
7
+ /**
8
+ * Send a prompt to Claude via the native CLI and return the text result.
9
+ * @param {string} prompt
10
+ * @returns {Promise<string>}
11
+ */
12
+ function executeClaude(prompt) {
13
+ return new Promise((resolve, reject) => {
14
+ const env = { ...process.env };
15
+ delete env.ANTHROPIC_API_KEY;
16
+
17
+ const child = spawn('claude', ['--print', '--output-format', 'json'], {
18
+ stdio: ['pipe', 'pipe', 'pipe'],
19
+ env
20
+ });
21
+
22
+ let stdout = '';
23
+ let stderr = '';
24
+ child.stdout.on('data', (c) => { stdout += c; });
25
+ child.stderr.on('data', (c) => { stderr += c; });
26
+ child.on('error', (err) => reject(new Error(`Failed to spawn Claude: ${err.message}`)));
27
+ child.on('close', (code) => {
28
+ if (!stdout.trim()) {
29
+ reject(new Error(stderr.trim() || `Claude exited with code ${code}`));
30
+ return;
31
+ }
32
+ try {
33
+ const envelope = JSON.parse(stdout.trim());
34
+ if (envelope.is_error) {
35
+ reject(new Error(envelope.result || 'Claude reported an error'));
36
+ return;
37
+ }
38
+ const text = (envelope.result ?? '').trim();
39
+ if (!text) {
40
+ reject(new Error('Claude returned an empty result'));
41
+ return;
42
+ }
43
+ resolve(text);
44
+ } catch {
45
+ reject(new Error('Claude returned non-JSON output'));
46
+ }
47
+ });
48
+
49
+ child.stdin.write(prompt, 'utf8');
50
+ child.stdin.end();
51
+ });
52
+ }
53
+
54
+ /**
55
+ * Strip a leading markdown code fence if Claude wrapped the body.
56
+ * @param {string} text
57
+ * @returns {string}
58
+ */
59
+ function stripCodeFence(text) {
60
+ const match = text.match(/^```(?:markdown|md)?\s*\n([\s\S]*?)\n```\s*$/);
61
+ return match ? match[1] : text;
62
+ }
63
+
64
+ function buildPrompt(metadata, body) {
65
+ const title = metadata.title || 'Untitled';
66
+ const type = metadata.type || 'guide';
67
+ return `You are a senior technical writer improving a project document.
68
+
69
+ Document title: ${title}
70
+ Document type: ${type}
71
+
72
+ Current body (markdown, everything after the YAML frontmatter):
73
+ ---
74
+ ${body}
75
+ ---
76
+
77
+ INSTRUCTIONS:
78
+ 1. Improve and expand the document body. Fill in empty sections and replace HTML comment placeholders (<!-- ... -->) with real, useful content inferred from the title, type, and surrounding context.
79
+ 2. Keep the existing section headings (## ...). You may add sub-sections where helpful.
80
+ 3. Use developer-friendly formatting: \`code\`, code blocks, file paths, and lists.
81
+ 4. Respond with ONLY the improved markdown body — no frontmatter, no explanation, no surrounding code fences.`;
82
+ }
83
+
84
+ /**
85
+ * `vibe docs refine <id>` — AI pass that improves and writes back the doc body.
86
+ * @param {string[]} args
87
+ */
88
+ async function refineDoc(args) {
89
+ try {
90
+ const input = args[0];
91
+ if (!input) {
92
+ throw new Error('Please provide a doc ID. Usage: vibe docs refine <id>');
93
+ }
94
+
95
+ const config = getConfig();
96
+ if (!config.ai?.enabled) {
97
+ throw new Error('AI is not enabled. Run "vibe link" first.');
98
+ }
99
+
100
+ const docInfo = resolveDocId(input);
101
+ if (!docInfo) {
102
+ throw new Error(`Doc ${input} not found. Use "vibe docs list" to see available docs.`);
103
+ }
104
+
105
+ const doc = parseDoc(docInfo.path);
106
+ logger.info(`Refining ${docInfo.id} — ${doc.metadata.title || 'Untitled'}`);
107
+
108
+ const body = doc.contentLines.join('\n').trim();
109
+ const prompt = buildPrompt(doc.metadata, body);
110
+
111
+ const loading = spinner('🤖 Refining document...');
112
+ let result;
113
+ try {
114
+ result = await executeClaude(prompt);
115
+ loading.succeed('Refinement complete!');
116
+ } catch (error) {
117
+ loading.fail('Refinement failed');
118
+ throw error;
119
+ }
120
+
121
+ const newBody = stripCodeFence(result).trim();
122
+
123
+ const timestamp = new Date().toISOString().split('T')[0];
124
+ const yamlLines = doc.yamlLines.map(line =>
125
+ line.startsWith('updated_at:') ? `updated_at: ${timestamp}` : line
126
+ );
127
+
128
+ const reconstructed = ['---', ...yamlLines, '---', '', newBody, ''].join('\n');
129
+ fs.writeFileSync(docInfo.path, reconstructed, 'utf-8');
130
+
131
+ logger.success(`${docInfo.id} refined and updated.`);
132
+ } catch (error) {
133
+ logger.error(error.message);
134
+ process.exit(1);
135
+ }
136
+ }
137
+
138
+ export default refineDoc;
@@ -0,0 +1,28 @@
1
+ import fs from 'fs';
2
+ import { resolveDocId } from '../../utils/doc.js';
3
+ import { logger } from '../../utils/cli.js';
4
+
5
+ /**
6
+ * `vibe docs show <id>` — print a doc to stdout.
7
+ * @param {string[]} args
8
+ */
9
+ function showDoc(args) {
10
+ try {
11
+ const input = args[0];
12
+ if (!input) {
13
+ throw new Error('Please provide a doc ID. Usage: vibe docs show <id>');
14
+ }
15
+
16
+ const docInfo = resolveDocId(input);
17
+ if (!docInfo) {
18
+ throw new Error(`Doc ${input} not found. Use "vibe docs list" to see available docs.`);
19
+ }
20
+
21
+ console.log(fs.readFileSync(docInfo.path, 'utf-8'));
22
+ } catch (error) {
23
+ logger.error(error.message);
24
+ process.exit(1);
25
+ }
26
+ }
27
+
28
+ export default showDoc;
@@ -106,13 +106,25 @@ async function initCommand(args) {
106
106
  fs.mkdirSync(targetFolder, { recursive: true });
107
107
  fs.mkdirSync(path.join(targetFolder, 'tickets'), { recursive: true });
108
108
  fs.mkdirSync(path.join(targetFolder, 'plans'), { recursive: true });
109
+ fs.mkdirSync(path.join(targetFolder, 'docs'), { recursive: true });
109
110
  fs.mkdirSync(path.join(targetFolder, '.templates'), { recursive: true });
111
+ fs.mkdirSync(path.join(targetFolder, '.templates', 'docs'), { recursive: true });
110
112
 
111
113
  fs.copyFileSync(configSrc, path.join(targetFolder, 'config.yml'));
112
114
  fs.copyFileSync(templateSrc, path.join(targetFolder, '.templates', 'default.md'));
113
115
  fs.copyFileSync(teamSrc, path.join(targetFolder, 'team.yml'));
114
116
 
115
- console.log(`✅ '${targetFolder}' initialized with config, tickets/, plans/, and .templates/default.md`);
117
+ const docsTemplatesSrc = path.join(__dirname, '../../../assets', 'docs');
118
+ if (fs.existsSync(docsTemplatesSrc)) {
119
+ for (const file of fs.readdirSync(docsTemplatesSrc)) {
120
+ fs.copyFileSync(
121
+ path.join(docsTemplatesSrc, file),
122
+ path.join(targetFolder, '.templates', 'docs', file)
123
+ );
124
+ }
125
+ }
126
+
127
+ console.log(`✅ '${targetFolder}' initialized with config, tickets/, plans/, docs/, and .templates/`);
116
128
  } else {
117
129
  console.log(`⚠️ Folder '${targetFolder}' already exists. Skipping .vibe creation.`);
118
130
  }
@@ -0,0 +1,137 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { getProjectRoot } from './index.js';
5
+
6
+ const DEFAULT_DOCS_PATH = '.vibe/docs';
7
+
8
+ /**
9
+ * Get the docs directory from config or default.
10
+ * @returns {string} Absolute path to the docs directory
11
+ */
12
+ export function getDocsDir() {
13
+ const root = getProjectRoot();
14
+ const configPath = path.join(root, '.vibe', 'config.yml');
15
+ let docsPath = DEFAULT_DOCS_PATH;
16
+
17
+ try {
18
+ if (fs.existsSync(configPath)) {
19
+ const config = yaml.load(fs.readFileSync(configPath, 'utf-8')) || {};
20
+ if (config.docs?.path && typeof config.docs.path === 'string') {
21
+ docsPath = config.docs.path;
22
+ }
23
+ }
24
+ } catch (error) {
25
+ console.error(`❌ Error reading config: ${error.message}`);
26
+ }
27
+
28
+ return path.resolve(root, docsPath);
29
+ }
30
+
31
+ /**
32
+ * Generate the next doc ID (DOC-NNN), independent from ticket IDs.
33
+ * @returns {string} Next doc ID, e.g. "DOC-002"
34
+ */
35
+ export function getNextDocId() {
36
+ const docsDir = getDocsDir();
37
+
38
+ if (!fs.existsSync(docsDir)) {
39
+ return 'DOC-001';
40
+ }
41
+
42
+ try {
43
+ const numbers = fs.readdirSync(docsDir)
44
+ .filter(f => f.endsWith('.md'))
45
+ .map(f => f.match(/^DOC-(\d+)/))
46
+ .filter(Boolean)
47
+ .map(match => parseInt(match[1], 10))
48
+ .filter(num => !isNaN(num) && num > 0);
49
+
50
+ const next = Math.max(0, ...numbers) + 1;
51
+ return `DOC-${String(next).padStart(3, '0')}`;
52
+ } catch (error) {
53
+ console.error(`❌ Error scanning docs directory: ${error.message}`);
54
+ return 'DOC-001';
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Resolve a doc identifier (1, 001, DOC-1, DOC-001) to its file.
60
+ * @param {string|number} input - The doc identifier
61
+ * @returns {Object|null} { id, file, path } or null if not found
62
+ * @throws {Error} If the input format is invalid
63
+ */
64
+ export function resolveDocId(input) {
65
+ if (!input && input !== 0) {
66
+ return null;
67
+ }
68
+
69
+ const docsDir = getDocsDir();
70
+
71
+ if (!fs.existsSync(docsDir)) {
72
+ return null;
73
+ }
74
+
75
+ const files = fs.readdirSync(docsDir).filter(f => f.endsWith('.md'));
76
+
77
+ let clean = input.toString().trim().toUpperCase();
78
+ if (clean.startsWith('DOC-')) {
79
+ clean = clean.replace('DOC-', '');
80
+ }
81
+
82
+ if (!/^\d+$/.test(clean)) {
83
+ throw new Error(`Invalid doc ID format: ${input}. Expected numeric ID or DOC-XXX format.`);
84
+ }
85
+
86
+ const fullId = `DOC-${clean.padStart(3, '0')}`;
87
+ const matchingFile = files.find(file => file.startsWith(fullId));
88
+
89
+ if (matchingFile) {
90
+ return {
91
+ id: fullId,
92
+ file: matchingFile,
93
+ path: path.join(docsDir, matchingFile)
94
+ };
95
+ }
96
+
97
+ return null;
98
+ }
99
+
100
+ /**
101
+ * Parse a doc markdown file, splitting frontmatter and content.
102
+ * @param {string} filePath - Path to the doc file
103
+ * @returns {Object} { metadata, yamlLines, contentLines, fullContent, filePath }
104
+ * @throws {Error} If the file is missing or malformed
105
+ */
106
+ export function parseDoc(filePath) {
107
+ if (typeof filePath !== 'string') {
108
+ throw new Error('File path must be a string');
109
+ }
110
+
111
+ if (!fs.existsSync(filePath)) {
112
+ throw new Error(`Doc file not found: ${filePath}`);
113
+ }
114
+
115
+ const content = fs.readFileSync(filePath, 'utf8');
116
+
117
+ if (!content.trim()) {
118
+ throw new Error('Doc file is empty');
119
+ }
120
+
121
+ const lines = content.split('\n');
122
+
123
+ if (lines[0] !== '---') {
124
+ throw new Error('Invalid doc format: missing opening frontmatter delimiter (---)');
125
+ }
126
+
127
+ const yamlEndIndex = lines.findIndex((line, index) => index > 0 && line === '---');
128
+ if (yamlEndIndex === -1) {
129
+ throw new Error('Invalid doc format: missing closing frontmatter delimiter (---)');
130
+ }
131
+
132
+ const yamlLines = lines.slice(1, yamlEndIndex);
133
+ const contentLines = lines.slice(yamlEndIndex + 1);
134
+ const metadata = yaml.load(yamlLines.join('\n')) || {};
135
+
136
+ return { metadata, yamlLines, contentLines, fullContent: content, filePath };
137
+ }
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+ import {
5
+ createTempDir,
6
+ cleanupTempDir,
7
+ mockConsole,
8
+ mockProcessCwd,
9
+ createMockVibeProject
10
+ } from './test-helpers.js';
11
+ import { getNextDocId, resolveDocId, parseDoc, getDocsDir } from './doc.js';
12
+
13
+ function writeDoc(docsDir, id, slug, extra = '') {
14
+ fs.mkdirSync(docsDir, { recursive: true });
15
+ const content = `---
16
+ id: ${id}
17
+ title: ${slug}
18
+ type: guide
19
+ status: draft
20
+ tags: []
21
+ ---
22
+
23
+ ## Overview
24
+ ${extra}
25
+ `;
26
+ fs.writeFileSync(path.join(docsDir, `${id}-${slug}.md`), content, 'utf-8');
27
+ }
28
+
29
+ describe('doc utils', () => {
30
+ let tempDir;
31
+ let consoleMock;
32
+ let restoreCwd;
33
+
34
+ beforeEach(() => {
35
+ tempDir = createTempDir('doc-util-test');
36
+ consoleMock = mockConsole();
37
+ restoreCwd = mockProcessCwd(tempDir);
38
+ createMockVibeProject(tempDir);
39
+ });
40
+
41
+ afterEach(() => {
42
+ consoleMock.restore();
43
+ restoreCwd();
44
+ cleanupTempDir(tempDir);
45
+ });
46
+
47
+ describe('getDocsDir', () => {
48
+ it('defaults to .vibe/docs', () => {
49
+ expect(getDocsDir()).toBe(path.join(tempDir, '.vibe', 'docs'));
50
+ });
51
+ });
52
+
53
+ describe('getNextDocId', () => {
54
+ it('returns DOC-001 when no docs exist', () => {
55
+ expect(getNextDocId()).toBe('DOC-001');
56
+ });
57
+
58
+ it('increments based on existing docs', () => {
59
+ const docsDir = path.join(tempDir, '.vibe', 'docs');
60
+ writeDoc(docsDir, 'DOC-001', 'first');
61
+ writeDoc(docsDir, 'DOC-002', 'second');
62
+ expect(getNextDocId()).toBe('DOC-003');
63
+ });
64
+
65
+ it('is independent from ticket IDs', () => {
66
+ const ticketsDir = path.join(tempDir, '.vibe', 'tickets');
67
+ fs.writeFileSync(path.join(ticketsDir, 'TKT-042-x.md'), '---\nid: TKT-042\n---\n');
68
+ expect(getNextDocId()).toBe('DOC-001');
69
+ });
70
+ });
71
+
72
+ describe('resolveDocId', () => {
73
+ beforeEach(() => {
74
+ writeDoc(path.join(tempDir, '.vibe', 'docs'), 'DOC-001', 'first');
75
+ });
76
+
77
+ it('resolves a bare number', () => {
78
+ expect(resolveDocId('1').id).toBe('DOC-001');
79
+ });
80
+
81
+ it('resolves a full DOC- id', () => {
82
+ expect(resolveDocId('DOC-001').id).toBe('DOC-001');
83
+ });
84
+
85
+ it('returns null for a missing doc', () => {
86
+ expect(resolveDocId('999')).toBeNull();
87
+ });
88
+
89
+ it('throws on invalid format', () => {
90
+ expect(() => resolveDocId('abc')).toThrow(/Invalid doc ID/);
91
+ });
92
+ });
93
+
94
+ describe('parseDoc', () => {
95
+ it('splits frontmatter and content', () => {
96
+ const docsDir = path.join(tempDir, '.vibe', 'docs');
97
+ writeDoc(docsDir, 'DOC-001', 'first', 'Body text');
98
+ const parsed = parseDoc(path.join(docsDir, 'DOC-001-first.md'));
99
+ expect(parsed.metadata.id).toBe('DOC-001');
100
+ expect(parsed.metadata.type).toBe('guide');
101
+ expect(parsed.contentLines.join('\n')).toContain('## Overview');
102
+ });
103
+
104
+ it('throws on missing frontmatter', () => {
105
+ const docsDir = path.join(tempDir, '.vibe', 'docs');
106
+ fs.mkdirSync(docsDir, { recursive: true });
107
+ const p = path.join(docsDir, 'DOC-009-bad.md');
108
+ fs.writeFileSync(p, 'no frontmatter here', 'utf-8');
109
+ expect(() => parseDoc(p)).toThrow(/frontmatter/);
110
+ });
111
+ });
112
+ });
@@ -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)