@vibedx/vibekit 0.8.7 → 0.8.8

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/assets/config.yml CHANGED
@@ -27,6 +27,10 @@ team:
27
27
  agent:
28
28
  timeout: 900
29
29
 
30
+ swarm:
31
+ maxAgents: 3
32
+ timeout: 900
33
+
30
34
  ai:
31
35
  enabled: false
32
36
 
@@ -0,0 +1,15 @@
1
+ # Project Guidelines
2
+
3
+ ## Code Style
4
+ - Follow the existing codebase conventions
5
+ - Keep functions small and focused
6
+ - Use descriptive variable and function names
7
+
8
+ ## Git Workflow
9
+ - Write clear, concise commit messages
10
+ - One logical change per commit
11
+ - Keep PRs focused and reviewable
12
+
13
+ ## Testing
14
+ - Write tests for new functionality
15
+ - Ensure existing tests pass before committing
@@ -0,0 +1,58 @@
1
+ # CLAUDE.md
2
+
3
+ Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
4
+
5
+ **Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
6
+
7
+ ## 1. Think Before Coding
8
+
9
+ **Don't assume. Don't hide confusion. Surface tradeoffs.**
10
+
11
+ Before implementing:
12
+ - State your assumptions explicitly. If uncertain, ask.
13
+ - If multiple interpretations exist, present them - don't pick silently.
14
+ - If a simpler approach exists, say so. Push back when warranted.
15
+ - If something is unclear, stop. Name what's confusing. Ask.
16
+
17
+ ## 2. Simplicity First
18
+
19
+ **Minimum code that solves the problem. Nothing speculative.**
20
+
21
+ - No features beyond what was asked.
22
+ - No abstractions for single-use code.
23
+ - No "flexibility" or "configurability" that wasn't requested.
24
+ - No error handling for impossible scenarios.
25
+ - If you write 200 lines and it could be 50, rewrite it.
26
+
27
+ Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
28
+
29
+ ## 3. Surgical Changes
30
+
31
+ **Touch only what you must. Clean up only your own mess.**
32
+
33
+ When editing existing code:
34
+ - Don't "improve" adjacent code, comments, or formatting.
35
+ - Don't refactor things that aren't broken.
36
+ - Match existing style, even if you'd do it differently.
37
+ - If you notice unrelated dead code, mention it - don't delete it.
38
+
39
+ When your changes create orphans:
40
+ - Remove imports/variables/functions that YOUR changes made unused.
41
+ - Don't remove pre-existing dead code unless asked.
42
+
43
+ The test: Every changed line should trace directly to the user's request.
44
+
45
+ ## 4. Goal-Driven Execution
46
+
47
+ **Define success criteria. Loop until verified.**
48
+
49
+ Transform tasks into verifiable goals:
50
+ - "Add validation" -> "Write tests for invalid inputs, then make them pass"
51
+ - "Fix the bug" -> "Write a test that reproduces it, then make it pass"
52
+ - "Refactor X" -> "Ensure tests pass before and after"
53
+
54
+ For multi-step tasks, state a brief plan with steps and verification checkpoints.
55
+
56
+ ---
57
+
58
+ **These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
@@ -0,0 +1,36 @@
1
+ # React Project Guidelines
2
+
3
+ ## Architecture
4
+ - Use functional components with hooks
5
+ - Keep components small and single-responsibility
6
+ - Colocate related files (component, styles, tests, types)
7
+
8
+ ## State Management
9
+ - Use local state (useState) for component-specific state
10
+ - Lift state up when shared between siblings
11
+ - Use context sparingly — prefer prop drilling for shallow trees
12
+
13
+ ## Patterns
14
+ - Extract custom hooks for reusable logic
15
+ - Use composition over inheritance
16
+ - Prefer controlled components for forms
17
+ - Memoize expensive computations with useMemo, not every render
18
+
19
+ ## Styling
20
+ - Use CSS modules or the project's styling solution consistently
21
+ - Follow mobile-first responsive design
22
+ - Keep styles colocated with components
23
+
24
+ ## Testing
25
+ - Test behavior, not implementation details
26
+ - Use React Testing Library
27
+ - Write integration tests for user flows, unit tests for utilities
28
+
29
+ ## Performance
30
+ - Lazy load routes and heavy components
31
+ - Avoid unnecessary re-renders — profile before optimizing
32
+ - Use React.memo only when measured improvement exists
33
+
34
+ ## Git Workflow
35
+ - One component/feature per PR
36
+ - Write clear commit messages describing the user-facing change
@@ -0,0 +1,34 @@
1
+ # Node.js Project Guidelines
2
+
3
+ ## Architecture
4
+ - Separate concerns: routes, controllers, services, data access
5
+ - Use dependency injection for testability
6
+ - Keep middleware thin — delegate to service layer
7
+
8
+ ## Error Handling
9
+ - Use typed errors with meaningful messages
10
+ - Let errors bubble up to a centralized handler
11
+ - Never swallow errors silently
12
+ - Return appropriate HTTP status codes
13
+
14
+ ## Security
15
+ - Validate all external input at the boundary
16
+ - Use parameterized queries — never interpolate user input into SQL/NoSQL
17
+ - Keep dependencies updated; audit regularly
18
+ - Never log sensitive data (tokens, passwords, PII)
19
+
20
+ ## Performance
21
+ - Use streaming for large payloads
22
+ - Cache expensive operations with TTLs
23
+ - Use connection pooling for databases
24
+ - Profile before optimizing — measure, don't guess
25
+
26
+ ## Testing
27
+ - Unit test business logic in isolation
28
+ - Integration test API endpoints with real database
29
+ - Use fixtures, not mocks, for data layer tests
30
+
31
+ ## Git Workflow
32
+ - One logical change per commit
33
+ - Write commit messages that explain why, not what
34
+ - Keep PRs reviewable — under 400 lines when possible
@@ -0,0 +1,34 @@
1
+ # Python Project Guidelines
2
+
3
+ ## Code Style
4
+ - Follow PEP 8 — use a formatter (black, ruff) to enforce
5
+ - Use type hints for function signatures and class attributes
6
+ - Prefer f-strings over format() or % formatting
7
+ - Use pathlib over os.path for file operations
8
+
9
+ ## Architecture
10
+ - Keep modules focused — one responsibility per module
11
+ - Use dataclasses or Pydantic for structured data
12
+ - Prefer composition over deep inheritance hierarchies
13
+ - Use context managers for resource management
14
+
15
+ ## Error Handling
16
+ - Use specific exception types, not bare except
17
+ - Let exceptions propagate — catch only when you can handle them
18
+ - Use logging, not print, for operational output
19
+
20
+ ## Testing
21
+ - Use pytest with fixtures
22
+ - Test behavior, not implementation
23
+ - Use parametrize for testing multiple inputs
24
+ - Aim for integration tests over excessive mocking
25
+
26
+ ## Dependencies
27
+ - Pin versions in requirements.txt or pyproject.toml
28
+ - Use virtual environments — never install globally
29
+ - Prefer standard library when it's sufficient
30
+
31
+ ## Git Workflow
32
+ - One logical change per commit
33
+ - Clear commit messages explaining the why
34
+ - Keep PRs focused and reviewable
package/index.js CHANGED
@@ -24,7 +24,7 @@ const __dirname = dirname(__filename);
24
24
  const AVAILABLE_COMMANDS = [
25
25
  'init', 'new', 'close', 'list', 'get-started',
26
26
  'start', 'link', 'unlink', 'refine', 'lint', 'review', 'team', 'skills',
27
- 'status', 'stats', 'pr'
27
+ 'status', 'stats', 'pr', 'swarm'
28
28
  ];
29
29
 
30
30
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedx/vibekit",
3
- "version": "0.8.7",
3
+ "version": "0.8.8",
4
4
  "description": "A powerful CLI tool for managing development tickets and project workflows",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -3,50 +3,129 @@ import path from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { dirname } from 'path';
5
5
  import getStartedCommand from '../get-started/index.js';
6
+ import { arrowSelect } from '../../utils/arrow-select.js';
6
7
 
7
- // ESM replacement for __dirname
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = dirname(__filename);
10
10
 
11
- /**
12
- * Initialize a new VibeKit project
13
- * @param {string[]} args Command arguments
14
- */
15
- function initCommand(args) {
16
- const targetFolder = ".vibe";
17
-
18
- if (fs.existsSync(targetFolder)) {
19
- console.log(`⚠️ Folder '${targetFolder}' already exists. Skipping creation.`);
20
- process.exit(0);
21
- }
22
-
23
- // Use real files instead of hardcoded template strings
24
- const templateSrc = path.join(__dirname, "../../../assets", "default.md");
25
- const configSrc = path.join(__dirname, "../../../assets", "config.yml");
26
- const teamSrc = path.join(__dirname, "../../../assets", "team.yml");
27
-
28
- fs.mkdirSync(targetFolder, { recursive: true });
29
- fs.mkdirSync(path.join(targetFolder, "tickets"), { recursive: true });
30
- fs.mkdirSync(path.join(targetFolder, ".templates"), { recursive: true });
31
-
32
- // Copy files from assets directory instead of using hardcoded templates
33
- fs.copyFileSync(configSrc, path.join(targetFolder, "config.yml"));
34
- fs.copyFileSync(templateSrc, path.join(targetFolder, ".templates", "default.md"));
35
- fs.copyFileSync(teamSrc, path.join(targetFolder, "team.yml"));
36
-
37
- console.log(`✅ '${targetFolder}' initialized with config, tickets/, and .templates/default.md`);
38
-
39
- // Ask if the user wants to run get-started
40
- console.log("\nWould you like to create sample tickets and documentation? (y/n)");
41
-
42
- // Since we can't get user input directly in this environment, we'll check for a flag
43
- const runGetStarted = args.includes("--with-samples") || args.includes("-s");
44
-
11
+ const TEMPLATES = [
12
+ { name: 'default', value: 'default', category: 'coding', description: 'Minimal project conventions' },
13
+ { name: 'karpathy', value: 'karpathy', category: 'coding', description: 'Karpathy-style dev philosophy' },
14
+ { name: 'react', value: 'react', category: 'frameworks', description: 'React/frontend best practices' },
15
+ { name: 'node', value: 'node', category: 'languages', description: 'Node.js backend guidelines' },
16
+ { name: 'python', value: 'python', category: 'languages', description: 'Python project conventions' },
17
+ ];
18
+
19
+ function getTemplatePath(templateName) {
20
+ const template = TEMPLATES.find(t => t.value === templateName);
21
+ if (!template) return null;
22
+ return path.join(__dirname, '../../../assets/standards', template.category, `${templateName}.md`);
23
+ }
24
+
25
+ function listTemplates() {
26
+ console.log('\n📋 Available templates:\n');
27
+ const categories = [...new Set(TEMPLATES.map(t => t.category))];
28
+ for (const cat of categories) {
29
+ console.log(` ${cat}/`);
30
+ for (const t of TEMPLATES.filter(t => t.category === cat)) {
31
+ console.log(` ${t.value.padEnd(18)} ${t.description}`);
32
+ }
33
+ }
34
+ console.log('\nUsage: vibe init --template <name>');
35
+ console.log(' vibe init --template (interactive picker)\n');
36
+ }
37
+
38
+ function applyTemplate(templateName, targetDir) {
39
+ const templatePath = getTemplatePath(templateName);
40
+ if (!templatePath || !fs.existsSync(templatePath)) {
41
+ console.error(`❌ Template "${templateName}" not found.`);
42
+ process.exit(1);
43
+ }
44
+
45
+ const templateContent = fs.readFileSync(templatePath, 'utf-8');
46
+ const destPath = path.join(targetDir, 'CLAUDE.md');
47
+
48
+ if (fs.existsSync(destPath)) {
49
+ const existing = fs.readFileSync(destPath, 'utf-8');
50
+ const separator = `\n\n<!-- vibekit:template:${templateName} -->\n`;
51
+ if (existing.includes(`vibekit:template:${templateName}`)) {
52
+ console.log(`⚠️ Template "${templateName}" already injected in CLAUDE.md — skipping`);
53
+ } else {
54
+ fs.writeFileSync(destPath, existing.trimEnd() + separator + templateContent);
55
+ console.log(`✅ Template "${templateName}" injected into existing CLAUDE.md`);
56
+ }
57
+ } else {
58
+ fs.writeFileSync(destPath, templateContent);
59
+ console.log(`✅ CLAUDE.md created from "${templateName}" template`);
60
+ }
61
+ }
62
+
63
+ async function initCommand(args) {
64
+ const targetFolder = '.vibe';
65
+
66
+ const hasTemplate = args.includes('--template') || args.includes('-t');
67
+ const templateIdx = args.indexOf('--template') !== -1 ? args.indexOf('--template') : args.indexOf('-t');
68
+ const listOnly = args.includes('--list-templates');
69
+
70
+ if (listOnly) {
71
+ listTemplates();
72
+ return;
73
+ }
74
+
75
+ let templateName = null;
76
+
77
+ if (hasTemplate) {
78
+ const nextArg = args[templateIdx + 1];
79
+ if (nextArg && !nextArg.startsWith('-')) {
80
+ templateName = nextArg;
81
+ const valid = TEMPLATES.find(t => t.value === templateName);
82
+ if (!valid) {
83
+ console.error(`❌ Unknown template: "${templateName}"`);
84
+ listTemplates();
85
+ process.exit(1);
86
+ }
87
+ } else {
88
+ try {
89
+ const choices = TEMPLATES.map(t => ({
90
+ name: `${t.category}/${t.value} — ${t.description}`,
91
+ value: t.value
92
+ }));
93
+ templateName = await arrowSelect('Select a template:', choices);
94
+ } catch {
95
+ console.log('\nNo template selected.');
96
+ return;
97
+ }
98
+ }
99
+ }
100
+
101
+ if (!fs.existsSync(targetFolder)) {
102
+ const templateSrc = path.join(__dirname, '../../../assets', 'default.md');
103
+ const configSrc = path.join(__dirname, '../../../assets', 'config.yml');
104
+ const teamSrc = path.join(__dirname, '../../../assets', 'team.yml');
105
+
106
+ fs.mkdirSync(targetFolder, { recursive: true });
107
+ fs.mkdirSync(path.join(targetFolder, 'tickets'), { recursive: true });
108
+ fs.mkdirSync(path.join(targetFolder, '.templates'), { recursive: true });
109
+
110
+ fs.copyFileSync(configSrc, path.join(targetFolder, 'config.yml'));
111
+ fs.copyFileSync(templateSrc, path.join(targetFolder, '.templates', 'default.md'));
112
+ fs.copyFileSync(teamSrc, path.join(targetFolder, 'team.yml'));
113
+
114
+ console.log(`✅ '${targetFolder}' initialized with config, tickets/, and .templates/default.md`);
115
+ } else {
116
+ console.log(`⚠️ Folder '${targetFolder}' already exists. Skipping .vibe creation.`);
117
+ }
118
+
119
+ if (templateName) {
120
+ applyTemplate(templateName, process.cwd());
121
+ }
122
+
123
+ const runGetStarted = args.includes('--with-samples') || args.includes('-s');
45
124
  if (runGetStarted) {
46
- // Run the get-started command to create sample tickets and documentation
47
125
  getStartedCommand([]);
48
- } else {
126
+ } else if (!templateName) {
49
127
  console.log("\nTip: Run 'vibe get-started' anytime to create sample tickets and documentation.");
128
+ console.log(" Run 'vibe init --template' to set up CLAUDE.md from a curated template.");
50
129
  }
51
130
  }
52
131
 
@@ -56,22 +56,21 @@ describe('init command', () => {
56
56
  expect(['--with-samples']).toContain('--with-samples'); // Flag args
57
57
  });
58
58
 
59
- it('should handle folder existence check in temp', () => {
59
+ it('should handle folder existence check in temp', async () => {
60
60
  // Arrange - create existing folder in temp
61
61
  fs.mkdirSync(path.join(tempDir, '.vibe'), { recursive: true });
62
62
  setupMockAssets(tempDir);
63
63
 
64
64
  // Act
65
- expect(() => initCommand([])).toThrow('process.exit(0)');
65
+ await initCommand([]);
66
66
 
67
67
  // Assert - should skip creation for existing folder
68
- expect(exitMock.exitCalls).toContain(0);
69
- expect(consoleMock.logs.log).toContain("⚠️ Folder '.vibe' already exists. Skipping creation.");
68
+ expect(consoleMock.logs.log).toContain("⚠️ Folder '.vibe' already exists. Skipping .vibe creation.");
70
69
  });
71
70
 
72
- it('should show tip about get-started command when no flags', () => {
71
+ it('should show tip about get-started command when no flags', async () => {
73
72
  // Act
74
- expect(() => initCommand([])).toThrow();
73
+ await initCommand([]);
75
74
 
76
75
  // Assert - should show either tip or error (both are valid test outcomes)
77
76
  const hasMessageOrError = consoleMock.logs.log.length > 0 || consoleMock.logs.error.length > 0;
@@ -80,17 +79,16 @@ describe('init command', () => {
80
79
  });
81
80
 
82
81
  describe('existing folder handling', () => {
83
- it('should skip creation when folder already exists', () => {
82
+ it('should skip creation when folder already exists', async () => {
84
83
  // Arrange - create the folder first in temp directory
85
84
  fs.mkdirSync(path.join(tempDir, '.vibe'), { recursive: true });
86
85
 
87
86
  // Act
88
- expect(() => initCommand([])).toThrow('process.exit(0)');
87
+ await initCommand([]);
89
88
 
90
89
  // Assert
91
- expect(exitMock.exitCalls).toContain(0);
92
90
  expect(consoleMock.logs.log).toContain(
93
- "⚠️ Folder '.vibe' already exists. Skipping creation."
91
+ "⚠️ Folder '.vibe' already exists. Skipping .vibe creation."
94
92
  );
95
93
  });
96
94
 
@@ -141,19 +139,44 @@ describe('init command', () => {
141
139
  });
142
140
 
143
141
  it('should validate template structure', () => {
144
- // Restore original cwd temporarily to read template
145
142
  restoreCwd();
146
143
  const originalCwd = process.cwd();
147
-
144
+
148
145
  const templateSrc = path.resolve(originalCwd, 'assets', 'default.md');
149
146
  const templateContent = fs.readFileSync(templateSrc, 'utf-8');
150
-
147
+
151
148
  expect(templateContent).toContain('---');
152
149
  expect(templateContent).toContain('id: TKT-{id}');
153
150
  expect(templateContent).toContain('title: {title}');
154
-
155
- // Restore mock
151
+
152
+ const standardsSrc = path.resolve(originalCwd, 'assets', 'standards');
153
+ expect(fs.existsSync(path.join(standardsSrc, 'coding', 'default.md'))).toBe(true);
154
+ expect(fs.existsSync(path.join(standardsSrc, 'coding', 'karpathy.md'))).toBe(true);
155
+ expect(fs.existsSync(path.join(standardsSrc, 'frameworks', 'react.md'))).toBe(true);
156
+ expect(fs.existsSync(path.join(standardsSrc, 'languages', 'node.md'))).toBe(true);
157
+ expect(fs.existsSync(path.join(standardsSrc, 'languages', 'python.md'))).toBe(true);
158
+
156
159
  restoreCwd = mockProcessCwd(tempDir);
157
160
  });
161
+
162
+ it('should inject template into existing CLAUDE.md', async () => {
163
+ restoreCwd();
164
+ const originalCwd = process.cwd();
165
+ restoreCwd = mockProcessCwd(tempDir);
166
+
167
+ fs.mkdirSync(path.join(tempDir, '.vibe'), { recursive: true });
168
+ setupMockAssets(tempDir);
169
+ fs.writeFileSync(path.join(tempDir, 'CLAUDE.md'), '# My Project\n\nExisting content.');
170
+
171
+ const initMod = await import('./index.js');
172
+ const { applyTemplate: apply } = await import('./index.js').catch(() => null) || {};
173
+
174
+ await initCommand(['--template', 'default']);
175
+
176
+ const result = fs.readFileSync(path.join(tempDir, 'CLAUDE.md'), 'utf-8');
177
+ expect(result).toContain('# My Project');
178
+ expect(result).toContain('Existing content.');
179
+ expect(result).toContain('vibekit:template:default');
180
+ });
158
181
  });
159
182
  });
@@ -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 ticketContent = fs.readFileSync(info.ticket.filePath, 'utf-8');
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 agentProcess = spawn('claude', ['-p', prompt, '--timeout', String(agentTimeout * 1000)], {
294
- cwd: info.worktreePath,
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 ticketContent = fs.readFileSync(ticket.filePath, 'utf-8');
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 agentProcess = spawn('claude', ['-p', prompt, '--timeout', String(agentTimeout * 1000)], {
350
- cwd: process.cwd(),
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');
@@ -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
+ }