@vibedx/vibekit 0.8.6 → 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 +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/src/commands/get-started/index.js +6 -12
- package/src/commands/init/index.js +116 -37
- package/src/commands/init/index.test.js +38 -15
- 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/git.js +2 -2
- package/src/utils/git.test.js +1 -1
- package/src/utils/swarm.js +75 -0
package/assets/config.yml
CHANGED
|
@@ -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
|
@@ -13,26 +13,20 @@ import { getTicketsDir, getConfig, getNextTicketId, createSlug } from '../../uti
|
|
|
13
13
|
function createSampleTicket(title, description, priority = "medium", status = "open") {
|
|
14
14
|
const configPath = path.join(process.cwd(), ".vibe", "config.yml");
|
|
15
15
|
const templatePath = path.join(process.cwd(), ".vibe", ".templates", "default.md");
|
|
16
|
-
|
|
16
|
+
|
|
17
17
|
if (!fs.existsSync(configPath) || !fs.existsSync(templatePath)) {
|
|
18
18
|
console.error("❌ Missing config.yml or default.md template.");
|
|
19
19
|
return false;
|
|
20
20
|
}
|
|
21
|
-
|
|
21
|
+
|
|
22
22
|
const config = yaml.load(fs.readFileSync(configPath, "utf-8"));
|
|
23
23
|
const template = fs.readFileSync(templatePath, "utf-8");
|
|
24
24
|
const ticketDir = path.join(process.cwd(), config.tickets?.path || ".vibe/tickets");
|
|
25
|
-
|
|
26
|
-
const
|
|
27
|
-
const
|
|
28
|
-
.map(f => f.match(/^TKT-(\\d+)/))
|
|
29
|
-
.filter(Boolean)
|
|
30
|
-
.map(match => parseInt(match[1], 10));
|
|
31
|
-
const nextId = Math.max(0, ...ticketNumbers) + 1;
|
|
32
|
-
const paddedId = String(nextId).padStart(3, "0");
|
|
25
|
+
|
|
26
|
+
const ticketId = getNextTicketId();
|
|
27
|
+
const paddedId = ticketId.replace('TKT-', '');
|
|
33
28
|
const now = new Date().toISOString();
|
|
34
|
-
|
|
35
|
-
const ticketId = `TKT-${paddedId}`;
|
|
29
|
+
|
|
36
30
|
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
37
31
|
const filename = `${ticketId}-${slug}.md`;
|
|
38
32
|
|
|
@@ -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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
65
|
+
await initCommand([]);
|
|
66
66
|
|
|
67
67
|
// Assert - should skip creation for existing folder
|
|
68
|
-
expect(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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');
|
|
@@ -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
|
+
}
|
package/src/utils/git.js
CHANGED
|
@@ -49,7 +49,7 @@ function branchExistsLocally(branchName) {
|
|
|
49
49
|
*/
|
|
50
50
|
function branchExistsRemotely(branchName) {
|
|
51
51
|
try {
|
|
52
|
-
const result = execSync(`git ls-remote --heads origin ${branchName}`, { encoding: 'utf-8' });
|
|
52
|
+
const result = execSync(`git ls-remote --heads origin ${branchName}`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
53
53
|
return result.trim() !== '';
|
|
54
54
|
} catch (error) {
|
|
55
55
|
return false;
|
|
@@ -158,7 +158,7 @@ function getMainWorktreeRoot() {
|
|
|
158
158
|
|
|
159
159
|
function getRepoName() {
|
|
160
160
|
try {
|
|
161
|
-
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim();
|
|
161
|
+
const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
|
|
162
162
|
// Handle git@github.com:org/repo.git
|
|
163
163
|
let match = remoteUrl.match(/[:/]([^/]+?)(?:\.git)?$/);
|
|
164
164
|
if (match) return match[1];
|
package/src/utils/git.test.js
CHANGED
|
@@ -134,7 +134,7 @@ describe('git utilities', () => {
|
|
|
134
134
|
|
|
135
135
|
// Assert
|
|
136
136
|
expect(result).toBe(true);
|
|
137
|
-
expect(mockExecSync).toHaveBeenCalledWith('git ls-remote --heads origin feature/test', { encoding: 'utf-8' });
|
|
137
|
+
expect(mockExecSync).toHaveBeenCalledWith('git ls-remote --heads origin feature/test', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
it('should return false when branch does not exist remotely', () => {
|
|
@@ -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
|
+
}
|