@vibedx/vibekit 0.11.1 → 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 +1 -0
- package/assets/default.md +4 -4
- package/index.js +1 -1
- package/package.json +1 -1
- package/skills/vibekit/SKILL.md +19 -1
- package/src/commands/ready/index.js +72 -0
- package/src/commands/start/index.js +46 -2
- package/src/commands/swarm/index.js +8 -3
- package/src/utils/ticket.js +52 -4
- package/src/utils/ticket.test.js +55 -1
package/assets/config.yml
CHANGED
package/assets/default.md
CHANGED
|
@@ -12,19 +12,19 @@ updated_at: {date}
|
|
|
12
12
|
|
|
13
13
|
## Description
|
|
14
14
|
|
|
15
|
-
<!--
|
|
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
|
|
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
|
-
<!--
|
|
23
|
+
<!-- Non-functional requirements: tests, no breaking changes, shared utilities, edge cases handled. -->
|
|
24
24
|
|
|
25
25
|
## Implementation Notes
|
|
26
26
|
|
|
27
|
-
<!--
|
|
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
package/skills/vibekit/SKILL.md
CHANGED
|
@@ -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
|
|
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)
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/src/utils/ticket.js
CHANGED
|
@@ -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
|
+
|
package/src/utils/ticket.test.js
CHANGED
|
@@ -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
|