@vibedx/vibekit 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/assets/config.yml CHANGED
@@ -12,6 +12,7 @@ tickets:
12
12
  use_status_folders: false
13
13
  status_options:
14
14
  - open
15
+ - ready
15
16
  - in_progress
16
17
  - review
17
18
  - done
package/assets/default.md CHANGED
@@ -12,19 +12,19 @@ updated_at: {date}
12
12
 
13
13
  ## Description
14
14
 
15
- <!-- Write the task details, requirements, and any other relevant information here. -->
15
+ <!-- What is being built and why? Include context on the problem being solved and the desired outcome. Fill this in before writing any code. -->
16
16
 
17
17
  ## Acceptance Criteria
18
18
 
19
- <!-- List the specific conditions that must be met for this ticket to be considered complete. -->
19
+ <!-- List specific, testable conditions for "done". Use checkboxes: - [ ] criterion. Fill this in before writing any code. -->
20
20
 
21
21
  ## Code Quality
22
22
 
23
- <!-- List the specific conditions that must be met for this ticket to be considered complete. -->
23
+ <!-- Non-functional requirements: tests, no breaking changes, shared utilities, edge cases handled. -->
24
24
 
25
25
  ## Implementation Notes
26
26
 
27
- <!-- Technical details, references, or implementation context that might be helpful. -->
27
+ <!-- Key technical decisions, files to modify, dependencies, and edge cases to handle. Fill this in before writing any code. -->
28
28
 
29
29
  ## Design / UX Considerations
30
30
 
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', 'plan', 'pr', 'swarm', 'docs'
27
+ 'status', 'stats', 'plan', 'pr', 'swarm', 'docs', 'ready'
28
28
  ];
29
29
 
30
30
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibedx/vibekit",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "A powerful CLI tool for managing development tickets and project workflows",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -22,6 +22,23 @@ Then read the ticket, plan the work, and `vibe start TKT-XXX` before touching an
22
22
 
23
23
  **Only skip the ticket for truly trivial changes**: typo fixes, single-line config tweaks, or answering a quick question.
24
24
 
25
+ ## 🔴 MANDATORY RULE: Detail the Ticket Before Writing Code
26
+
27
+ **Before writing ANY code on a ticket, the ticket must be documented.** Tickets are often created with empty sections (just HTML comment placeholders). Do not start implementing against an empty ticket.
28
+
29
+ When you `vibe start` a ticket:
30
+
31
+ 1. Read the ticket file.
32
+ 2. Check whether **Description**, **Acceptance Criteria**, and **Implementation Notes** have real content (a section that contains only whitespace or `<!-- ... -->` comments counts as empty).
33
+ 3. If any are empty, fill them in based on:
34
+ - The ticket title
35
+ - Your understanding of the codebase
36
+ - The context from the conversation
37
+ 4. Commit the updated ticket file with message `docs: add details to TKT-XXX`.
38
+ 5. Only then proceed to implementation.
39
+
40
+ This applies to every AI tool using vibekit, not just Claude. `vibe start` warns when key sections are empty; `vibe start --agent` prepends this instruction to the agent's prompt automatically. Pass `--skip-detail-check` only for intentionally minimal tickets.
41
+
25
42
  ## Why This Matters
26
43
 
27
44
  Tickets break work into scoped, focused chunks that reduce AI drift and create documentation as a side effect. They also give humans a clear trail of what was done and why — critical for collaboration between humans and agents.
@@ -58,6 +75,7 @@ vibe init # Creates .vibe/ directory with config, team, templates
58
75
  | `vibe init` | Initialize vibekit in a project |
59
76
  | `vibe new "title"` | Create a ticket |
60
77
  | `vibe list` | List all tickets |
78
+ | `vibe ready <id>` | Mark a ticket ready for agent pickup (swarm) |
61
79
  | `vibe start <id>` | Start work (creates git branch) |
62
80
  | `vibe start <id> --worktree` | Start work in a separate worktree |
63
81
  | `vibe start <id> --agent` | Spawn a Claude agent to work on the ticket |
@@ -70,7 +88,7 @@ vibe init # Creates .vibe/ directory with config, team, templates
70
88
  | `vibe lint --fix` | Auto-fix missing sections |
71
89
  | `vibe refine <id>` | AI-enhance ticket details |
72
90
  | `vibe plan to-ticket <file>` | Convert a saved plan into tickets (AI) |
73
- | `vibe swarm` | Spawn parallel agents across open tickets in worktrees |
91
+ | `vibe swarm` | Spawn parallel agents across `ready` tickets in worktrees |
74
92
  | `vibe team` | Manage team members |
75
93
 
76
94
  ## Creating Tickets (for AI agents)
@@ -41,12 +41,12 @@ function createSampleTicket(title, description, priority = "medium", status = "o
41
41
  content = content.replace(/^status: .*$/m, `status: ${status}`);
42
42
 
43
43
  // Add description
44
- content = content.replace(/## Description\\s*\\n\\s*\\n/m, `## Description\\n\\n${description}\\n\\n`);
45
-
44
+ content = content.replace(/## Description\s*\n\s*\n/m, `## Description\n\n${description}\n\n`);
45
+
46
46
  // Add AI prompt for the feature request example
47
47
  if (title.includes("AI Prompt")) {
48
- content = content.replace(/## AI Prompt\\s*\\n\\s*\\n/m,
49
- `## AI Prompt\\n\\nGenerate ideas for implementing this feature in a Node.js CLI application. Consider user experience, error handling, and performance.\\n\\n`);
48
+ content = content.replace(/## AI Prompt\s*\n\s*\n/m,
49
+ `## AI Prompt\n\nGenerate ideas for implementing this feature in a Node.js CLI application. Consider user experience, error handling, and performance.\n\n`);
50
50
  }
51
51
 
52
52
  const outputPath = path.join(ticketDir, filename);
@@ -122,6 +122,10 @@ describe('get-started command', () => {
122
122
  expect(content).toContain('status: open');
123
123
  expect(content).toContain('## Description');
124
124
  expect(content).toContain('Simple Task Example');
125
+ // The description text must be injected under the Description heading (regression: TKT-028)
126
+ expect(content).toContain('This is a simple task ticket example showing the basic structure.');
127
+ // Injected content must be real newlines, never literal backslash sequences
128
+ expect(content).not.toMatch(/\\n|\\s/);
125
129
 
126
130
  // Find the bug report example
127
131
  const bugReportFile = files.find(f => f.includes('bug-report-example'));
@@ -222,8 +226,12 @@ describe('get-started command', () => {
222
226
 
223
227
  const content = fs.readFileSync(path.join(ticketsDir, aiPromptFile), 'utf-8');
224
228
  expect(content).toContain('## AI Prompt');
225
- expect(content).toContain('## AI Prompt');
226
229
  expect(content).toContain('Feature Request with AI Prompt');
230
+ // The AI prompt body must be injected under the heading (regression: TKT-028)
231
+ expect(content).toContain('Generate ideas for implementing this feature in a Node.js CLI application.');
232
+ // And the description for this ticket too
233
+ expect(content).toContain('This example shows how to use the AI prompt section');
234
+ expect(content).not.toMatch(/\\n|\\s/);
227
235
  });
228
236
 
229
237
  it('should create tickets with proper slug formatting', () => {
@@ -0,0 +1,72 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import yaml from 'js-yaml';
4
+ import { getTicketsDir } from '../../utils/index.js';
5
+ import { checkEmptySections, KEY_TICKET_SECTIONS } from '../../utils/ticket.js';
6
+
7
+ /**
8
+ * Mark a ticket as `ready` for agent pickup (e.g. by `vibe swarm`).
9
+ *
10
+ * A ticket should only be `ready` when it is fully fleshed out, so this refuses
11
+ * to promote tickets that still have empty key sections unless --force is passed.
12
+ * @param {string[]} args Command arguments
13
+ */
14
+ function readyCommand(args) {
15
+ const ticketArg = args.find(a => !a.startsWith('-'));
16
+ const forceFlag = args.includes('--force') || args.includes('-f');
17
+
18
+ if (!ticketArg) {
19
+ console.error('❌ Usage: vibe ready <TKT-XXX> [--force]');
20
+ console.error(' Marks a ticket as ready for agent pickup (swarm).');
21
+ process.exit(1);
22
+ }
23
+
24
+ const ticketFolder = getTicketsDir();
25
+ if (!fs.existsSync(ticketFolder)) {
26
+ console.error(`❌ Tickets directory not found: ${ticketFolder}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const normalizedInput = ticketArg.startsWith('TKT-')
31
+ ? ticketArg
32
+ : `TKT-${ticketArg.padStart(3, '0')}`;
33
+
34
+ const files = fs.readdirSync(ticketFolder).filter(f => f.endsWith('.md'));
35
+
36
+ for (const file of files) {
37
+ const fullPath = path.join(ticketFolder, file);
38
+ if (fs.statSync(fullPath).isDirectory()) continue;
39
+
40
+ const content = fs.readFileSync(fullPath, 'utf-8');
41
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
42
+ if (!match) continue;
43
+
44
+ const frontmatter = yaml.load(match[1]);
45
+ if (frontmatter.id !== normalizedInput && !file.includes(normalizedInput)) continue;
46
+
47
+ const empty = checkEmptySections(content, KEY_TICKET_SECTIONS);
48
+ if (empty.length > 0 && !forceFlag) {
49
+ console.error(`❌ ${frontmatter.id} is not ready — empty section(s): ${empty.join(', ')}.`);
50
+ console.error(` Fill them in (or run \`vibe refine ${frontmatter.id}\`) before marking ready.`);
51
+ console.error(' Use --force to mark ready anyway.');
52
+ process.exit(1);
53
+ }
54
+
55
+ frontmatter.status = 'ready';
56
+ frontmatter.updated_at = new Date().toISOString();
57
+
58
+ const updated = `---\n${yaml.dump(frontmatter)}---${content.split('---').slice(2).join('---')}`;
59
+ fs.writeFileSync(fullPath, updated, 'utf-8');
60
+
61
+ console.log(`✅ Ticket ${frontmatter.id} marked as ready for pickup.`);
62
+ if (empty.length > 0) {
63
+ console.log(`⚠️ Forced despite empty section(s): ${empty.join(', ')}.`);
64
+ }
65
+ return;
66
+ }
67
+
68
+ console.log(`❌ No ticket matching '${ticketArg}' found.`);
69
+ process.exit(1);
70
+ }
71
+
72
+ export default readyCommand;
@@ -19,6 +19,7 @@ import {
19
19
  getDefaultBaseBranch
20
20
  } from '../../utils/git.js';
21
21
  import { loadSkillContext, buildAgentPrompt, spawnAgent } from '../../utils/agent.js';
22
+ import { checkEmptySections, KEY_TICKET_SECTIONS } from '../../utils/ticket.js';
22
23
 
23
24
  function parseTicketIds(args) {
24
25
  const ids = [];
@@ -35,6 +36,9 @@ function parseTicketIds(args) {
35
36
  } else if (arg === '--no-install') {
36
37
  flags.noInstall = true;
37
38
  i++;
39
+ } else if (arg === '--skip-detail-check') {
40
+ flags.skipDetailCheck = true;
41
+ i++;
38
42
  } else if (arg === '--agent') {
39
43
  flags.agent = true;
40
44
  i++;
@@ -123,6 +127,25 @@ function updateTicketStatus(ticket, worktreePath) {
123
127
  fs.writeFileSync(ticket.filePath, updatedContent, 'utf-8');
124
128
  }
125
129
 
130
+ function getEmptySections(ticket) {
131
+ try {
132
+ return checkEmptySections(fs.readFileSync(ticket.filePath, 'utf-8'));
133
+ } catch {
134
+ return [];
135
+ }
136
+ }
137
+
138
+ function buildDetailInstruction(ticketId, emptySections) {
139
+ return [
140
+ `⚠️ Ticket ${ticketId} is missing detail in: ${emptySections.join(', ')}.`,
141
+ 'BEFORE writing any code:',
142
+ `1. Fill in the empty section(s) (${emptySections.join(', ')}) based on the ticket title, the codebase, and this conversation.`,
143
+ `2. Commit the updated ticket file with message "docs: add details to ${ticketId}".`,
144
+ '3. Only then start implementation.',
145
+ ''
146
+ ].join('\n');
147
+ }
148
+
126
149
  /**
127
150
  * Start working on ticket(s) by checking out branches or creating worktrees
128
151
  * @param {string[]} args Command arguments
@@ -147,6 +170,7 @@ function startCommand(args) {
147
170
  console.error(' --base <branch> Base branch for new branches/worktrees (default: main)');
148
171
  console.error(' --dry-run Show what would happen without doing it');
149
172
  console.error(' --no-install Skip npm install in worktrees');
173
+ console.error(' --skip-detail-check Skip the empty-section detail check');
150
174
  process.exit(1);
151
175
  }
152
176
 
@@ -204,6 +228,14 @@ function startCommand(args) {
204
228
  const branchName = getBranchName(ticket, config);
205
229
  const title = ticket.frontmatter.title || 'Untitled';
206
230
 
231
+ if (!spawnAgent && !flags.skipDetailCheck) {
232
+ const empty = getEmptySections(ticket);
233
+ if (empty.length > 0) {
234
+ console.log(` ⚠️ ${ticket.frontmatter.id}: empty section(s) — ${empty.join(', ')}`);
235
+ console.log(` Fill these in before coding (or run \`vibe refine ${ticket.frontmatter.id}\`). Use --skip-detail-check to bypass.`);
236
+ }
237
+ }
238
+
207
239
  if (useWorktree) {
208
240
  const worktreePath = getWorktreePath(repoName, branchName);
209
241
  worktreeInfos.push({
@@ -274,7 +306,13 @@ function startCommand(args) {
274
306
  const skillContext = loadSkillContext();
275
307
  console.log('\n🤖 Spawning Claude agents...\n');
276
308
  for (const info of worktreeInfos) {
277
- const prompt = buildAgentPrompt(info.ticket, flags.prompt, skillContext);
309
+ let prompt = buildAgentPrompt(info.ticket, flags.prompt, skillContext);
310
+ if (!flags.prompt && !flags.skipDetailCheck) {
311
+ const empty = getEmptySections(info.ticket);
312
+ if (empty.length > 0) {
313
+ prompt = buildDetailInstruction(info.ticket.frontmatter.id, empty) + '\n' + prompt;
314
+ }
315
+ }
278
316
 
279
317
  try {
280
318
  const pid = spawnAgent(prompt, info.worktreePath, agentTimeout);
@@ -320,7 +358,13 @@ function startCommand(args) {
320
358
  const ticket = tickets[0];
321
359
  const agentTimeout = config.worktree?.agent?.timeout || config.agent?.timeout || 900;
322
360
  const skillContext = loadSkillContext();
323
- const prompt = buildAgentPrompt(ticket, flags.prompt, skillContext);
361
+ let prompt = buildAgentPrompt(ticket, flags.prompt, skillContext);
362
+ if (!flags.prompt && !flags.skipDetailCheck) {
363
+ const empty = getEmptySections(ticket);
364
+ if (empty.length > 0) {
365
+ prompt = buildDetailInstruction(ticket.frontmatter.id, empty) + '\n' + prompt;
366
+ }
367
+ }
324
368
 
325
369
  console.log('\n🤖 Spawning Claude agent...\n');
326
370
  try {
@@ -81,10 +81,14 @@ function loadTickets(ticketsDir) {
81
81
  }
82
82
 
83
83
  function applyFilters(tickets, filterStr) {
84
- if (!filterStr) return tickets.filter(t => t.frontmatter.status === 'open');
84
+ // Swarm only picks up `ready` tickets by default `open` tickets are drafts.
85
+ if (!filterStr) return tickets.filter(t => t.frontmatter.status === 'ready');
86
+
87
+ const parts = filterStr.split(',').map(s => s.trim()).filter(Boolean);
88
+ const specifiesStatus = parts.some(p => p.split(':')[0].trim().toLowerCase() === 'status');
85
89
 
86
90
  return tickets.filter(t => {
87
- const parts = filterStr.split(',').map(s => s.trim());
91
+ if (!specifiesStatus && t.frontmatter.status !== 'ready') return false;
88
92
  return parts.every(part => {
89
93
  const [key, value] = part.split(':').map(s => s.trim());
90
94
  if (!key || !value) return true;
@@ -263,7 +267,8 @@ export default function swarmCommand(args) {
263
267
  tickets = tickets.slice(0, maxAgents);
264
268
 
265
269
  if (tickets.length === 0) {
266
- console.log('No tickets match the filter. Nothing to swarm.');
270
+ console.log('No `ready` tickets to swarm. Mark tickets ready with `vibe ready TKT-XXX`.');
271
+ console.log('(Swarm only picks up tickets with status: ready. Use --filter "status:open" to override.)');
267
272
  return;
268
273
  }
269
274
 
@@ -409,15 +409,63 @@ function hasSectionContent(lines, sectionHeader) {
409
409
  if (!Array.isArray(lines)) {
410
410
  return false;
411
411
  }
412
-
412
+
413
413
  if (typeof sectionHeader !== 'string' || !sectionHeader.trim()) {
414
414
  return false;
415
415
  }
416
-
417
- const sectionIndex = lines.findIndex(line =>
416
+
417
+ const sectionIndex = lines.findIndex(line =>
418
418
  line && typeof line === 'string' && line.trim() === sectionHeader.trim()
419
419
  );
420
-
420
+
421
421
  return sectionIndex !== -1;
422
422
  }
423
423
 
424
+ // Sections an agent must fill in before writing code.
425
+ export const KEY_TICKET_SECTIONS = ['Description', 'Acceptance Criteria', 'Implementation Notes'];
426
+
427
+ /**
428
+ * Determine whether a ticket section has real content. A section is "empty" if,
429
+ * after its header, it contains only whitespace, HTML comments, or nothing at
430
+ * all before the next `##` header or end of file.
431
+ * @param {string} ticketContent - Full ticket markdown (frontmatter optional)
432
+ * @param {string} sectionName - Section name without the leading `## `
433
+ * @returns {boolean} True when the section is missing or has no real content
434
+ */
435
+ export function isTicketSectionEmpty(ticketContent, sectionName) {
436
+ if (typeof ticketContent !== 'string' || typeof sectionName !== 'string') {
437
+ return true;
438
+ }
439
+
440
+ const lines = ticketContent.split('\n');
441
+ const headerRegex = new RegExp(`^##\\s+${sectionName.trim().replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`);
442
+
443
+ let start = lines.findIndex(line => headerRegex.test(line));
444
+ if (start === -1) {
445
+ return true;
446
+ }
447
+
448
+ const body = [];
449
+ for (let i = start + 1; i < lines.length; i++) {
450
+ if (/^##\s+/.test(lines[i])) break;
451
+ body.push(lines[i]);
452
+ }
453
+
454
+ const meaningful = body
455
+ .join('\n')
456
+ .replace(/<!--[\s\S]*?-->/g, '')
457
+ .trim();
458
+
459
+ return meaningful.length === 0;
460
+ }
461
+
462
+ /**
463
+ * Return the list of key sections that are empty in a ticket.
464
+ * @param {string} ticketContent - Full ticket markdown
465
+ * @param {string[]} [sections] - Sections to check (defaults to KEY_TICKET_SECTIONS)
466
+ * @returns {string[]} Names of empty sections
467
+ */
468
+ export function checkEmptySections(ticketContent, sections = KEY_TICKET_SECTIONS) {
469
+ return sections.filter(section => isTicketSectionEmpty(ticketContent, section));
470
+ }
471
+
@@ -7,7 +7,7 @@ import {
7
7
  mockProcessCwd,
8
8
  createMockVibeProject
9
9
  } from './test-helpers.js';
10
- import { resolveTicketId } from './ticket.js';
10
+ import { resolveTicketId, isTicketSectionEmpty, checkEmptySections } from './ticket.js';
11
11
 
12
12
  describe('ticket utilities', () => {
13
13
  let tempDir;
@@ -183,6 +183,60 @@ describe('ticket utilities', () => {
183
183
  });
184
184
  });
185
185
 
186
+ describe('isTicketSectionEmpty', () => {
187
+ it('treats a missing section as empty', () => {
188
+ expect(isTicketSectionEmpty('## Other\ncontent', 'Description')).toBe(true);
189
+ });
190
+
191
+ it('treats a comment-only section as empty', () => {
192
+ const md = '## Description\n\n<!-- write here -->\n\n## Acceptance Criteria\n';
193
+ expect(isTicketSectionEmpty(md, 'Description')).toBe(true);
194
+ });
195
+
196
+ it('treats a whitespace-only section as empty', () => {
197
+ expect(isTicketSectionEmpty('## Description\n\n \n\n## Next\n', 'Description')).toBe(true);
198
+ });
199
+
200
+ it('treats a section with real content as non-empty', () => {
201
+ const md = '## Description\n\nBuild the login flow.\n\n## Next\n';
202
+ expect(isTicketSectionEmpty(md, 'Description')).toBe(false);
203
+ });
204
+
205
+ it('treats brief content as non-empty (no false positives)', () => {
206
+ expect(isTicketSectionEmpty('## Description\nFix typo.\n', 'Description')).toBe(false);
207
+ });
208
+
209
+ it('returns true for invalid input', () => {
210
+ expect(isTicketSectionEmpty(null, 'Description')).toBe(true);
211
+ expect(isTicketSectionEmpty('## Description\nx', null)).toBe(true);
212
+ });
213
+ });
214
+
215
+ describe('checkEmptySections', () => {
216
+ it('reports only the empty key sections', () => {
217
+ const md = [
218
+ '## Description',
219
+ 'A real description here.',
220
+ '',
221
+ '## Acceptance Criteria',
222
+ '<!-- todo -->',
223
+ '',
224
+ '## Implementation Notes',
225
+ ''
226
+ ].join('\n');
227
+ expect(checkEmptySections(md)).toEqual(['Acceptance Criteria', 'Implementation Notes']);
228
+ });
229
+
230
+ it('returns empty array when all key sections have content', () => {
231
+ const md = [
232
+ '## Description', 'd', '',
233
+ '## Acceptance Criteria', '- [ ] a', '',
234
+ '## Implementation Notes', 'notes'
235
+ ].join('\n');
236
+ expect(checkEmptySections(md)).toEqual([]);
237
+ });
238
+ });
239
+
186
240
  // Note: parseTicket and updateTicket have complex error handling and validation
187
241
  // that would require extensive mocking of file system operations and YAML parsing.
188
242
  // These functions are better tested through integration tests that test the