jettypod 4.4.19 → 4.4.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2396 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Speed Mode implementation - ship in 2 hours!
4
+ // Just the essentials from our prototype
5
+
6
+ const sqlite3 = require('sqlite3').verbose();
7
+ const { getDb, closeDb, dbPath, waitForMigrations } = require('../../lib/database');
8
+ const { getCurrentWork } = require('../../lib/current-work');
9
+ const { TYPE_EMOJIS, STATUS_EMOJIS } = require('../../lib/constants');
10
+ const { wrapText, getTerminalWidth, getVisualWidth } = require('../../lib/text-wrapper');
11
+
12
+ const db = getDb();
13
+
14
+ /**
15
+ * Find epic by traversing parent_id chain
16
+ * @param {number} itemId - Work item ID to start from
17
+ * @returns {Promise<Object|null>} Epic work item or null
18
+ */
19
+ function findEpic(itemId) {
20
+ return new Promise((resolve, reject) => {
21
+ function traverse(currentId) {
22
+ if (!currentId) {
23
+ return resolve(null);
24
+ }
25
+
26
+ db.get('SELECT id, type, title, parent_id FROM work_items WHERE id = ?', [currentId], (err, item) => {
27
+ if (err) {
28
+ return reject(new Error(`Failed to find epic: ${err.message}`));
29
+ }
30
+ if (!item) {
31
+ return resolve(null);
32
+ }
33
+ if (item.type === 'epic') {
34
+ return resolve({ id: item.id, title: item.title });
35
+ }
36
+ traverse(item.parent_id);
37
+ });
38
+ }
39
+
40
+ traverse(itemId);
41
+ });
42
+ }
43
+
44
+ // Create work item
45
+ function create(type, title, description = '', parentId = null, mode = null, needsDiscovery = false) {
46
+ return new Promise((resolve, reject) => {
47
+ // Validate project discovery is not blocking work creation
48
+ if (type === 'epic' || type === 'feature') {
49
+ const config = require('../../lib/config');
50
+ const currentConfig = config.read();
51
+ const discovery = currentConfig.project_discovery;
52
+
53
+ if (discovery && discovery.status === 'in_progress') {
54
+ return reject(new Error(`Cannot create ${type}s while project discovery is in progress.\n\nProject discovery must be completed first.\n\nComplete discovery with:\n jettypod project discover complete --winner=<path> --rationale="<reason>"\n\nOr talk to Claude Code to continue the discovery conversation.`));
55
+ }
56
+ }
57
+
58
+ // Validate needs_discovery flag is only used for epics
59
+ if (needsDiscovery && type !== 'epic') {
60
+ return reject(new Error(`The --needs-discovery flag can only be used with epic work items`));
61
+ }
62
+
63
+ // Continue with creation (no blocking validation for epic discovery)
64
+ // The needs_discovery flag is informational only - trust users to decide when ready
65
+ continueCreate();
66
+
67
+ function continueCreate() {
68
+ // Chores don't have modes - they inherit context from their parent feature
69
+ if (type === 'chore') {
70
+ if (mode) {
71
+ return reject(new Error(`Chores do not have modes. Chores inherit the workflow context from their parent feature.\n\nTo create a chore: jettypod work create chore "title" "description" --parent=<feature-id>`));
72
+ }
73
+
74
+ // Standalone chores (no parent) are allowed - skip parent validation
75
+ if (!parentId) {
76
+ return continueWithValidatedParent(null);
77
+ }
78
+
79
+ // Check parent's mode and phase
80
+ db.get('SELECT id, title, type, mode, phase, epic_id FROM work_items WHERE id = ?', [parentId], (err, parent) => {
81
+ if (err) {
82
+ return reject(new Error(`Failed to validate parent: ${err.message}`));
83
+ }
84
+
85
+ if (!parent) {
86
+ return reject(new Error(`Parent feature #${parentId} not found`));
87
+ }
88
+
89
+ if (parent.type !== 'feature' && parent.type !== 'epic') {
90
+ return reject(new Error(`Chores can only be created under features or epics. Parent #${parentId} is a ${parent.type}`));
91
+ }
92
+
93
+ // For chores under features, validate mode exists
94
+ // Chores under epics are standalone and don't require mode
95
+ if (parent.type === 'feature' && !parent.mode) {
96
+ return reject(new Error(
97
+ `Cannot create chore for feature without a mode.\n\n` +
98
+ `Feature #${parent.id} "${parent.title}"\n` +
99
+ ` Phase: ${parent.phase || 'not set'}\n` +
100
+ ` Mode: ${parent.mode || 'not set'}\n` +
101
+ ` Scenario file: ${parent.scenario_file || 'not set'}\n\n` +
102
+ `Chores can only be created after the feature has transitioned to implementation\n` +
103
+ `with a mode (speed/stable/production).\n\n` +
104
+ `To fix this, run:\n` +
105
+ ` jettypod work implement ${parent.id} --scenario-file="features/[feature].feature" --winner="..." --rationale="..."`
106
+ ));
107
+ }
108
+
109
+ // ═══════════════════════════════════════════════════════════════════
110
+ // SPEED-MODE WORKFLOW GUARD
111
+ // ═══════════════════════════════════════════════════════════════════
112
+ // Block manual chore creation during active speed-mode workflow.
113
+ // Speed-mode auto-generates stable chores - manual creation bypasses the skill.
114
+ //
115
+ // Guard Logic: ONLY block when ALL conditions are true:
116
+ // 1. mode === 'speed' (feature is in speed mode)
117
+ // 2. phase === 'implementation' (skill is actively running)
118
+ // 3. chores already exist (initial chores have been created)
119
+ //
120
+ // Edge Case Matrix:
121
+ // ┌─────────────┬─────────────────┬─────────────────┬─────────────────┐
122
+ // │ mode │ phase=impl │ phase=other │ phase=null │
123
+ // ├─────────────┼─────────────────┼─────────────────┼─────────────────┤
124
+ // │ speed │ BLOCKED* │ ALLOWED │ ALLOWED │
125
+ // │ stable │ ALLOWED │ ALLOWED │ ALLOWED │
126
+ // │ production │ ALLOWED │ ALLOWED │ ALLOWED │
127
+ // │ null │ (caught earlier by "no mode" check) │
128
+ // └─────────────┴─────────────────┴─────────────────┴─────────────────┘
129
+ // * Only blocked if a chore has been STARTED (status != 'todo')
130
+ // This allows feature-planning to create ALL chores after scenarios
131
+ //
132
+ // Why this logic:
133
+ // - speed + implementation + chore started = active skill workflow
134
+ // - speed + implementation + all chores todo = initial setup from feature-planning
135
+ // - speed + other phase = skill paused or not started (manual ok)
136
+ // - stable/production = designed for manual chore creation
137
+ // ═══════════════════════════════════════════════════════════════════
138
+ if (parent.mode === 'speed' && parent.phase === 'implementation') {
139
+ // Check if any chore has been STARTED (status = 'in_progress' or 'done')
140
+ // This allows feature-planning to create ALL chores after scenarios
141
+ // Note: Chores start with status 'backlog' or 'todo', which are NOT "started"
142
+ db.get(
143
+ `SELECT COUNT(*) as count FROM work_items WHERE parent_id = ? AND type = ? AND status IN ('in_progress', 'done')`,
144
+ [parent.id, 'chore'],
145
+ (err, result) => {
146
+ if (err) {
147
+ return reject(new Error(`Failed to check started chores: ${err.message}`));
148
+ }
149
+
150
+ const startedChores = result ? result.count : 0;
151
+
152
+ if (startedChores > 0) {
153
+ // A chore has been started - block manual creation
154
+ return reject(new Error(
155
+ `Cannot manually create chores while speed-mode implementation is in progress.\n\n` +
156
+ `Feature #${parent.id} "${parent.title}" has chores being worked on.\n` +
157
+ `Speed-mode will auto-generate stable chores when the happy path is complete.\n\n` +
158
+ `Next steps:\n` +
159
+ ` 1. Complete the current speed-mode chores\n` +
160
+ ` 2. Run: jettypod work status ${parent.id} to see progress\n` +
161
+ ` 3. Speed-mode will transition to stable and generate chores automatically\n\n` +
162
+ `If you need to bypass this (not recommended):\n` +
163
+ ` jettypod work elevate ${parent.id} stable`
164
+ ));
165
+ }
166
+
167
+ // No chores started yet - allow creation (feature-planning phase)
168
+ mode = null;
169
+ // Derive epic_id from parent feature
170
+ const derivedEpicId = parent.epic_id;
171
+ continueWithValidatedParent(derivedEpicId);
172
+ }
173
+ );
174
+ return; // Wait for async chore check
175
+ }
176
+
177
+ // Parent is valid, continue with creation
178
+ mode = null; // Explicitly set to null for chores
179
+ // Derive epic_id: if parent is epic, use parent.id; if parent is feature, use parent.epic_id
180
+ const derivedEpicId = parent.type === 'epic' ? parent.id : parent.epic_id;
181
+ continueWithValidatedParent(derivedEpicId);
182
+ });
183
+
184
+ return; // Wait for async validation
185
+ }
186
+
187
+ // For non-chores (features, epics), derive epic_id if there's a parent
188
+ if (parentId && (type === 'feature' || type === 'bug')) {
189
+ // Features/bugs with parent - look up parent to derive epic_id
190
+ db.get('SELECT id, type, epic_id FROM work_items WHERE id = ?', [parentId], (err, parent) => {
191
+ if (err) {
192
+ return reject(new Error(`Failed to look up parent: ${err.message}`));
193
+ }
194
+ if (!parent) {
195
+ return reject(new Error(`Parent #${parentId} not found`));
196
+ }
197
+ // Parent should be an epic for features
198
+ const derivedEpicId = parent.type === 'epic' ? parent.id : parent.epic_id;
199
+ continueWithValidatedParent(derivedEpicId);
200
+ });
201
+ return;
202
+ }
203
+
204
+ // For epics or items without parent, no epic_id
205
+ continueWithValidatedParent(null);
206
+ }
207
+
208
+ function continueWithValidatedParent(epicId) {
209
+
210
+ // Only features have modes - they start with mode=NULL (no mode until implementation starts)
211
+ // Epics, chores, and bugs don't have modes (always NULL)
212
+ if (type === 'feature' && !mode) {
213
+ mode = null;
214
+ }
215
+
216
+ // Bugs should never have modes - always NULL
217
+ if (type === 'bug') {
218
+ if (mode) {
219
+ return reject(new Error('Bugs cannot have modes. Only features have modes (speed/stable/production).'));
220
+ }
221
+ mode = null;
222
+ }
223
+
224
+ // Validate mode value if provided (only for features)
225
+ if (mode && type === 'feature') {
226
+ const validModes = ['speed', 'stable', 'production'];
227
+ if (!validModes.includes(mode)) {
228
+ return reject(new Error(`Invalid mode: ${mode}. Must be one of: ${validModes.join(', ')}`));
229
+ }
230
+ }
231
+
232
+ // Set phase for features (discovery when mode=NULL, implementation when mode is set, NULL for everything else)
233
+ const phase = type === 'feature' ? (mode ? 'implementation' : 'discovery') : null;
234
+
235
+ const sql = `INSERT INTO work_items (type, title, description, parent_id, epic_id, mode, needs_discovery, phase, status) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`;
236
+ db.run(sql, [type, title, description, parentId, epicId, mode, needsDiscovery ? 1 : 0, phase, 'backlog'], function(err) {
237
+ if (err) {
238
+ return reject(err);
239
+ }
240
+ const newId = this.lastID;
241
+ const discoveryIndicator = needsDiscovery ? ' (needs discovery)' : '';
242
+ console.log(`Created ${type} #${newId}: ${title}${discoveryIndicator}`);
243
+ resolve(newId);
244
+ });
245
+ }
246
+ });
247
+ }
248
+
249
+ /**
250
+ * Determine if an item should be hidden in internal projects
251
+ * Rules:
252
+ * - Features with mode='production' are hidden
253
+ * - Chores whose parent feature has mode='production' are hidden (handled by tree filtering)
254
+ * - Epics are never hidden (they're containers)
255
+ *
256
+ * @param {Object} item - Work item to check
257
+ * @returns {boolean} True if item should be hidden
258
+ */
259
+ function isProductionModeItem(item) {
260
+ // Validate input
261
+ if (!item || typeof item !== 'object') {
262
+ return false;
263
+ }
264
+
265
+ // Only features can have production mode
266
+ if (item.type === 'feature' && item.mode === 'production') {
267
+ return true;
268
+ }
269
+
270
+ // Epics are containers - never hide them
271
+ // Chores don't have modes - their visibility is determined by their parent
272
+ return false;
273
+ }
274
+
275
+ /**
276
+ * Recursively filter out production mode features and their children
277
+ * Smart filtering: only removes items if they're production mode features
278
+ * Maintains tree integrity: removes entire branches when feature is production
279
+ *
280
+ * @param {Array} items - Items to filter
281
+ * @param {Object} itemsById - Lookup map of all items
282
+ * @returns {Array} Filtered items
283
+ */
284
+ function filterProductionItems(items, itemsById) {
285
+ // Validate input
286
+ if (!Array.isArray(items)) {
287
+ return [];
288
+ }
289
+
290
+ return items.filter(item => {
291
+ // Skip invalid items
292
+ if (!item || typeof item !== 'object') {
293
+ return false;
294
+ }
295
+
296
+ // Check if this item should be hidden
297
+ const shouldHide = isProductionModeItem(item);
298
+
299
+ if (shouldHide) {
300
+ return false; // Hide this item and all its children
301
+ }
302
+
303
+ // Item is not production - keep it and recursively filter its children
304
+ if (item.children && Array.isArray(item.children) && item.children.length > 0) {
305
+ item.children = filterProductionItems(item.children, itemsById);
306
+ }
307
+
308
+ return true;
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Get all work items as hierarchical tree structure
314
+ * @param {boolean} includeCompleted - Include done/cancelled items (default: false)
315
+ * @param {boolean} showAll - Show all items including production (bypasses internal/external filtering)
316
+ * @returns {Promise<Array>} Root work items with nested children
317
+ * @throws {Error} If database query fails
318
+ */
319
+ function getTree(includeCompleted = false, showAll = false) {
320
+ return new Promise((resolve, reject) => {
321
+ const whereClause = includeCompleted ? '' : "WHERE (status NOT IN ('done', 'cancelled') OR status IS NULL)";
322
+ db.all(`SELECT * FROM work_items ${whereClause} ORDER BY parent_id, id`, [], (err, rows) => {
323
+ if (err) {
324
+ return reject(new Error(`Failed to fetch work items: ${err.message}`));
325
+ }
326
+
327
+ if (!rows || rows.length === 0) {
328
+ return resolve([]);
329
+ }
330
+
331
+ const itemsById = {};
332
+ const rootItems = [];
333
+
334
+ try {
335
+ // Build lookup
336
+ rows.forEach(item => {
337
+ itemsById[item.id] = item;
338
+ item.children = [];
339
+ });
340
+
341
+ // Build tree
342
+ rows.forEach(item => {
343
+ if (item.parent_id && itemsById[item.parent_id]) {
344
+ itemsById[item.parent_id].children.push(item);
345
+ } else if (!item.parent_id) {
346
+ rootItems.push(item);
347
+ }
348
+ });
349
+
350
+ // Apply production filtering if internal project and not showing all
351
+ let filteredItems = rootItems;
352
+ try {
353
+ const config = require('../../lib/config');
354
+ const projectConfig = config.read();
355
+ const isInternal = projectConfig.project_state === 'internal';
356
+
357
+ if (isInternal && !showAll) {
358
+ filteredItems = filterProductionItems(rootItems, itemsById);
359
+ }
360
+ } catch (err) {
361
+ // If config reading fails, don't filter (safer to show everything)
362
+ // Log warning but continue
363
+ console.warn(`Warning: Failed to read config for production filtering: ${err.message}`);
364
+ }
365
+
366
+ resolve(filteredItems);
367
+ } catch (err) {
368
+ reject(new Error(`Failed to build backlog: ${err.message}`));
369
+ }
370
+ });
371
+ });
372
+ }
373
+
374
+ /**
375
+ * Print work items tree to console with visual hierarchy
376
+ * @param {Array} items - Work items to print
377
+ * @param {string} prefix - Indentation prefix for nested items
378
+ * @param {boolean} isRootLevel - Whether we're at the root level
379
+ * @param {Set} expandedIds - Set of item IDs to show expanded (null = all collapsed, 'all' = all expanded)
380
+ * @throws {Error} If items is not an array
381
+ */
382
+ function printTree(items, prefix = '', isRootLevel = true, expandedIds = null) {
383
+ if (!Array.isArray(items)) {
384
+ throw new Error('Items must be an array');
385
+ }
386
+
387
+ items.forEach((item, index) => {
388
+ if (!item || typeof item !== 'object') {
389
+ console.warn(`Skipping invalid item at index ${index}`);
390
+ return;
391
+ }
392
+
393
+ const isLast = index === items.length - 1;
394
+ const emoji = TYPE_EMOJIS[item.type] || '📋';
395
+
396
+ // Show mode indicator only when mode is set
397
+ let modeIndicator = '';
398
+ if (item.mode) {
399
+ modeIndicator = ` [${item.mode}]`;
400
+ }
401
+
402
+ // Determine if this item should be expanded
403
+ const isExpanded = expandedIds === 'all' || (expandedIds && expandedIds.has(item.id));
404
+ const hasChildren = item.children && item.children.length > 0;
405
+ const expandIndicator = hasChildren ? (isExpanded ? ' ⊖' : ' ⊕') : '';
406
+ const childCount = hasChildren ? ` (${item.children.length} ${item.type === 'epic' ? 'features' : item.type === 'feature' ? 'chores' : 'items'})` : '';
407
+
408
+ // Root level items have no connectors
409
+ if (isRootLevel) {
410
+ const titlePrefix = `${emoji} [#${item.id}] `;
411
+ const titleText = `${item.title}${modeIndicator}${childCount}${expandIndicator}`;
412
+ // Calculate actual visual width of emoji + space to find bracket position
413
+ const visualBracketPos = getVisualWidth(emoji + ' ');
414
+ const termWidth = getTerminalWidth();
415
+ const prefixVisualWidth = getVisualWidth(titlePrefix);
416
+ const maxTextWidth = termWidth - prefixVisualWidth;
417
+ const wrappedTitle = wrapText(titleText, maxTextWidth, 0);
418
+
419
+ console.log(titlePrefix + wrappedTitle[0]);
420
+ // Continuation lines align with the opening bracket
421
+ const continuationPrefix = (hasChildren ? '│' : ' ') + ' '.repeat(visualBracketPos - 1);
422
+ for (let i = 1; i < wrappedTitle.length; i++) {
423
+ console.log(continuationPrefix + wrappedTitle[i]);
424
+ }
425
+
426
+ if (isExpanded && item.description) {
427
+ const descLabel = 'Description: "';
428
+ const descText = item.description + '"';
429
+ const maxDescWidth = termWidth - getVisualWidth(continuationPrefix) - getVisualWidth(descLabel);
430
+ const wrappedDesc = wrapText(descText, maxDescWidth, 0);
431
+
432
+ console.log(continuationPrefix + descLabel + wrappedDesc[0]);
433
+ for (let i = 1; i < wrappedDesc.length; i++) {
434
+ console.log(continuationPrefix + wrappedDesc[i]);
435
+ }
436
+ }
437
+ if (isExpanded && item.status && item.type !== 'epic') {
438
+ console.log(`${continuationPrefix}Status: ${item.status}${item.mode ? ` | Mode: ${item.mode}` : ''}`);
439
+ }
440
+ } else {
441
+ // Nested items get tree connectors
442
+ const connector = isLast ? '└── ' : '├── ';
443
+ const treePrefix = prefix + (isLast ? ' ' : '│ ');
444
+ const titlePrefix = `${prefix}${connector}${emoji} [#${item.id}] `;
445
+ const titleText = `${item.title}${modeIndicator}${childCount}${expandIndicator}`;
446
+ const termWidth = getTerminalWidth();
447
+
448
+ // Calculate actual visual width to find bracket position
449
+ const visualBracketPos = getVisualWidth(`${prefix}${connector}${emoji} `);
450
+ const prefixVisualWidth = getVisualWidth(titlePrefix);
451
+ const maxTextWidth = termWidth - prefixVisualWidth;
452
+ const wrappedTitle = wrapText(titleText, maxTextWidth, 0);
453
+
454
+ console.log(titlePrefix + wrappedTitle[0]);
455
+ // Continuation lines align with the opening bracket
456
+ const continuationPrefix = (treePrefix.substring(0, 1)) + ' '.repeat(visualBracketPos - 1);
457
+ for (let i = 1; i < wrappedTitle.length; i++) {
458
+ console.log(continuationPrefix + wrappedTitle[i]);
459
+ }
460
+
461
+ if (isExpanded && item.description) {
462
+ const descLabel = 'Description: "';
463
+ const descText = item.description + '"';
464
+ const maxDescWidth = termWidth - getVisualWidth(continuationPrefix) - getVisualWidth(descLabel);
465
+ const wrappedDesc = wrapText(descText, maxDescWidth, 0);
466
+
467
+ console.log(continuationPrefix + descLabel + wrappedDesc[0]);
468
+ for (let i = 1; i < wrappedDesc.length; i++) {
469
+ console.log(continuationPrefix + wrappedDesc[i]);
470
+ }
471
+ }
472
+ if (isExpanded && item.status && item.type !== 'epic') {
473
+ console.log(`${continuationPrefix}Status: ${item.status}${item.mode ? ` | Mode: ${item.mode}` : ''}`);
474
+ }
475
+ }
476
+
477
+ // Always show children - expansion only controls extra details
478
+ if (hasChildren) {
479
+ const newPrefix = isRootLevel ? '' : (prefix + (isLast ? ' ' : '│ '));
480
+ printTree(item.children, newPrefix, false, expandedIds);
481
+ }
482
+ });
483
+ }
484
+
485
+ // Update status
486
+ function updateStatus(id, status) {
487
+ return new Promise((resolve, reject) => {
488
+ // First get the work item to check its parent
489
+ db.get('SELECT id, parent_id FROM work_items WHERE id = ?', [id], (err, item) => {
490
+ if (err) {
491
+ console.error(`Error: ${err.message}`);
492
+ return reject(err);
493
+ }
494
+
495
+ if (!item) {
496
+ console.log(`Work item #${id} not found`);
497
+ return resolve();
498
+ }
499
+
500
+ // Update the status and completed_at if marking as done
501
+ const completedAt = status === 'done' ? new Date().toISOString() : null;
502
+ const sql = status === 'done'
503
+ ? `UPDATE work_items SET status = ?, completed_at = ? WHERE id = ?`
504
+ : `UPDATE work_items SET status = ? WHERE id = ?`;
505
+ const params = status === 'done' ? [status, completedAt, id] : [status, id];
506
+
507
+ db.run(sql, params, async (err) => {
508
+ if (err) {
509
+ console.error(`Error: ${err.message}`);
510
+ return reject(err);
511
+ }
512
+
513
+ console.log(`Updated #${id} status to ${status}`);
514
+
515
+ // If status is 'done', merge and cleanup associated worktree
516
+ if (status === 'done') {
517
+ try {
518
+ const worktree = await new Promise((res, rej) => {
519
+ db.get('SELECT id, worktree_path, branch_name FROM worktrees WHERE work_item_id = ? AND status = ?', [id, 'active'], (err, row) => {
520
+ if (err) rej(err);
521
+ else res(row);
522
+ });
523
+ });
524
+
525
+ if (worktree) {
526
+ const { execSync } = require('child_process');
527
+ const { getGitRoot } = require('../../lib/git-root');
528
+ const { isGitRepo } = require('../../lib/git');
529
+ const gitRoot = getGitRoot();
530
+
531
+ // CRITICAL: Merge to main BEFORE cleanup
532
+ if (isGitRepo()) {
533
+ try {
534
+ // Test merge to detect conflicts (without actually merging)
535
+ const mergeResult = execSync(`git merge --no-commit --no-ff ${worktree.branch_name}`, {
536
+ cwd: gitRoot,
537
+ encoding: 'utf8',
538
+ stdio: 'pipe'
539
+ });
540
+
541
+ // Use smart conflict detection to check if we have true conflicts
542
+ const { checkForConflicts } = require('../../lib/smart-conflict-detection');
543
+ const conflictCheck = checkForConflicts(gitRoot);
544
+
545
+ if (conflictCheck.hasConflicts) {
546
+ // True conflicts detected - abort and preserve
547
+ try {
548
+ execSync('git merge --abort', { cwd: gitRoot, stdio: 'pipe' });
549
+ } catch (abortErr) {
550
+ // Ignore abort errors
551
+ }
552
+ console.warn(`⚠️ Cannot merge worktree branch "${worktree.branch_name}" - conflicts detected`);
553
+ console.warn(` Conflicts in: ${conflictCheck.conflictFiles.join(', ')}`);
554
+ console.warn(` Worktree preserved for manual resolution`);
555
+ throw new Error('Merge conflicts - worktree preserved');
556
+ }
557
+
558
+ // No conflicts - auto-commit if there are uncommitted changes
559
+ if (conflictCheck.status.trim() !== '') {
560
+ try {
561
+ execSync('git add .', { cwd: gitRoot, stdio: 'pipe' });
562
+ execSync(`git commit -m "Merge work item #${id}"`, {
563
+ cwd: gitRoot,
564
+ stdio: 'pipe'
565
+ });
566
+ console.log(`✓ Merged branch "${worktree.branch_name}" to main`);
567
+ } catch (commitErr) {
568
+ // Failed to commit - abort the merge
569
+ console.warn(`Warning: Failed to commit merge: ${commitErr.message}`);
570
+ try {
571
+ execSync('git merge --abort', { cwd: gitRoot, stdio: 'pipe' });
572
+ } catch (abortErr) {
573
+ // Ignore abort errors
574
+ }
575
+ throw new Error('Merge commit failed - worktree preserved');
576
+ }
577
+ } else {
578
+ // Merge completed with no changes (fast-forward or already merged)
579
+ console.log(`✓ Merged branch "${worktree.branch_name}" to main (no changes)`);
580
+ }
581
+ } catch (mergeErr) {
582
+ // Check if this is our controlled error or an unexpected git error
583
+ if (mergeErr.message === 'Merge conflicts - worktree preserved' ||
584
+ mergeErr.message === 'Merge commit failed - worktree preserved') {
585
+ throw mergeErr;
586
+ }
587
+
588
+ // Unexpected merge error - abort and preserve worktree
589
+ try {
590
+ execSync('git merge --abort', { cwd: gitRoot, stdio: 'pipe' });
591
+ } catch (abortErr) {
592
+ // Ignore abort errors
593
+ }
594
+ console.warn(`⚠️ Cannot merge worktree branch "${worktree.branch_name}" - merge failed`);
595
+ console.warn(` Error: ${mergeErr.message}`);
596
+ console.warn(` Worktree preserved for manual resolution`);
597
+ throw new Error('Merge failed - worktree preserved');
598
+ }
599
+ }
600
+
601
+ // Only cleanup after successful merge
602
+ const worktreeFacade = require('../../lib/worktree-facade');
603
+ const cleanupResult = await worktreeFacade.stopWork(worktree.id, {
604
+ repoPath: gitRoot,
605
+ db: db,
606
+ deleteBranch: true
607
+ });
608
+
609
+ if (cleanupResult.success) {
610
+ console.log(`✓ Cleaned up worktree for #${id}`);
611
+ } else {
612
+ // CRITICAL: Cleanup failures must be fatal - do not swallow errors
613
+ // Orphaned worktrees prevent recreation and cause confusion
614
+ const userMsg = cleanupResult.error?.userMessage || cleanupResult.error?.message;
615
+ console.error(`\n❌ Failed to cleanup worktree for #${id}: ${userMsg || 'Unknown error'}`);
616
+ console.error('This may leave orphaned worktree files.');
617
+ console.error('Please manually cleanup before continuing.\n');
618
+ throw new Error(`Cleanup failed: ${userMsg || 'Unknown error'}`);
619
+ }
620
+ }
621
+ } catch (cleanupErr) {
622
+ // CRITICAL: Cleanup failures must be fatal - do not swallow errors
623
+ console.error(`\n❌ Failed to cleanup worktree for #${id}: ${cleanupErr.message}`);
624
+ console.error('This may leave orphaned worktree files.');
625
+ console.error('Please manually cleanup before continuing.\n');
626
+ throw cleanupErr;
627
+ }
628
+ }
629
+
630
+ // If status is 'done', check if this is the current work item and cleanup current work state
631
+ if (status === 'done') {
632
+ const { getCurrentWork, clearCurrentWork } = require('../../lib/current-work');
633
+ const currentWork = getCurrentWork();
634
+
635
+ if (currentWork && currentWork.id === id) {
636
+ try {
637
+ clearCurrentWork();
638
+ console.log('✓ Cleared current work state');
639
+ } catch (clearErr) {
640
+ console.warn(`Warning: Failed to clear current work: ${clearErr.message}`);
641
+ }
642
+
643
+ // Also remove <current_work> block from CLAUDE.md
644
+ const fs = require('fs');
645
+ const path = require('path');
646
+ const claudeMdPath = path.join(process.cwd(), 'CLAUDE.md');
647
+
648
+ if (fs.existsSync(claudeMdPath)) {
649
+ try {
650
+ let content = fs.readFileSync(claudeMdPath, 'utf-8');
651
+ content = content.replace(/<current_work>[\s\S]*?<\/current_work>\n*/g, '');
652
+ fs.writeFileSync(claudeMdPath, content);
653
+ console.log('✓ Removed current work from CLAUDE.md');
654
+ } catch (claudeErr) {
655
+ console.warn(`Warning: Failed to update CLAUDE.md: ${claudeErr.message}`);
656
+ }
657
+ }
658
+ }
659
+ }
660
+
661
+ // If status is 'done' and item has a parent, check if we should auto-close the parent
662
+ if (status === 'done' && item.parent_id) {
663
+ db.get('SELECT id, type, mode FROM work_items WHERE id = ?', [item.parent_id], (err, parent) => {
664
+ if (err || !parent) {
665
+ return resolve();
666
+ }
667
+
668
+ // Auto-close epics when all children are done
669
+ if (parent.type === 'epic') {
670
+ // Check if all children of this epic are done
671
+ db.all(
672
+ 'SELECT id, status FROM work_items WHERE parent_id = ?',
673
+ [parent.id],
674
+ (err, children) => {
675
+ if (err) {
676
+ return resolve();
677
+ }
678
+
679
+ const allDone = children.every(child => child.status === 'done');
680
+ if (allDone) {
681
+ const epicCompletedAt = new Date().toISOString();
682
+ db.run('UPDATE work_items SET status = ?, completed_at = ? WHERE id = ?', ['done', epicCompletedAt, parent.id], (err) => {
683
+ if (err) {
684
+ console.error(`Failed to auto-close epic: ${err.message}`);
685
+ } else {
686
+ console.log(`✓ Epic #${parent.id} also completed (all children done)`);
687
+ }
688
+ resolve();
689
+ });
690
+ } else {
691
+ resolve();
692
+ }
693
+ }
694
+ );
695
+ }
696
+ // Auto-close features when all chores are done AND mode progression is complete
697
+ else if (parent.type === 'feature') {
698
+ // Check if all chores of this feature are done
699
+ db.all(
700
+ 'SELECT id, status FROM work_items WHERE parent_id = ? AND type = ?',
701
+ [parent.id, 'chore'],
702
+ (err, chores) => {
703
+ if (err || !chores || chores.length === 0) {
704
+ return resolve();
705
+ }
706
+
707
+ const allChoresDone = chores.every(chore => chore.status === 'done');
708
+ if (!allChoresDone) {
709
+ return resolve();
710
+ }
711
+
712
+ // All chores done - check if feature should be marked done based on mode progression
713
+ // Get project state to determine required modes
714
+ db.get('SELECT project_state FROM project_config WHERE id = 1', [], (err, config) => {
715
+ const projectState = (config && config.project_state) || 'internal';
716
+ const featureMode = parent.mode || 'speed';
717
+
718
+ // Determine if feature is complete based on project state and mode
719
+ let featureComplete = false;
720
+
721
+ if (projectState === 'internal') {
722
+ // Internal: complete after stable mode
723
+ featureComplete = (featureMode === 'stable');
724
+ } else if (projectState === 'external') {
725
+ // External: complete after production mode
726
+ featureComplete = (featureMode === 'production');
727
+ }
728
+
729
+ if (featureComplete) {
730
+ const featureCompletedAt = new Date().toISOString();
731
+ db.run('UPDATE work_items SET status = ?, completed_at = ? WHERE id = ?', ['done', featureCompletedAt, parent.id], (err) => {
732
+ if (err) {
733
+ console.error(`Failed to auto-close feature: ${err.message}`);
734
+ } else {
735
+ console.log(`✓ Feature #${parent.id} completed (all ${featureMode} mode chores done)`);
736
+ }
737
+ resolve();
738
+ });
739
+ } else {
740
+ // Feature not complete yet - needs more modes
741
+ const nextMode = featureMode === 'speed' ? 'stable' : 'production';
742
+ console.log(`✓ All ${featureMode} mode chores done. Feature #${parent.id} ready for ${nextMode} mode.`);
743
+ resolve();
744
+ }
745
+ });
746
+ }
747
+ );
748
+ } else {
749
+ resolve();
750
+ }
751
+ });
752
+ } else {
753
+ resolve();
754
+ }
755
+ });
756
+ });
757
+ });
758
+ }
759
+
760
+ // Set branch
761
+ function setBranch(id, branch) {
762
+ db.run(`UPDATE work_items SET branch_name = ? WHERE id = ?`, [branch, id], () => {
763
+ console.log(`Set #${id} branch to ${branch}`);
764
+ });
765
+ }
766
+
767
+ // Set mode
768
+ function setMode(id, mode) {
769
+ return new Promise((resolve, reject) => {
770
+ const { getCurrentWork } = require('../../lib/current-work');
771
+ const { updateCurrentWork } = require('../../lib/claudemd');
772
+
773
+ // Validate mode
774
+ const validModes = ['speed', 'stable', 'production'];
775
+ if (mode && !validModes.includes(mode)) {
776
+ const error = new Error(`Invalid mode: ${mode}. Must be one of: ${validModes.join(', ')}`);
777
+ console.error(`Error: ${error.message}`);
778
+ reject(error);
779
+ return;
780
+ }
781
+
782
+ // Check if work item is an epic - epics don't have modes
783
+ db.get(`SELECT type FROM work_items WHERE id = ?`, [id], (err, row) => {
784
+ if (err) {
785
+ console.error(`Error: ${err.message}`);
786
+ reject(err);
787
+ return;
788
+ }
789
+
790
+ if (!row) {
791
+ console.error(`Error: Work item #${id} not found`);
792
+ resolve();
793
+ return;
794
+ }
795
+
796
+ if (row.type === 'epic') {
797
+ const error = new Error('Epics do not have modes. Only features have modes.');
798
+ console.error(`Error: ${error.message}`);
799
+ reject(error);
800
+ return;
801
+ }
802
+
803
+ if (row.type === 'chore') {
804
+ const error = new Error('Chores do not have modes. Chores inherit the workflow context from their parent feature.');
805
+ console.error(`Error: ${error.message}`);
806
+ reject(error);
807
+ return;
808
+ }
809
+
810
+ if (row.type === 'bug') {
811
+ const error = new Error('Bugs do not have modes. Only features have modes (speed/stable/production).');
812
+ console.error(`Error: ${error.message}`);
813
+ reject(error);
814
+ return;
815
+ }
816
+
817
+ // Update database
818
+ db.serialize(() => {
819
+ db.run(`UPDATE work_items SET mode = ? WHERE id = ?`, [mode, id], async (err) => {
820
+ if (err) {
821
+ console.error(`Error: ${err.message}`);
822
+ reject(err);
823
+ return;
824
+ }
825
+
826
+ console.log(`Set #${id} mode to ${mode}`);
827
+
828
+ // If this is the current work item, update session file
829
+ const currentWork = await getCurrentWork();
830
+ if (currentWork && currentWork.id === id) {
831
+ // Get updated work item to pass to updateCurrentWork
832
+ db.get(`
833
+ SELECT w.*,
834
+ p.title as parent_title, p.id as parent_id
835
+ FROM work_items w
836
+ LEFT JOIN work_items p ON w.parent_id = p.id
837
+ WHERE w.id = ?
838
+ `, [id], async (err, workItem) => {
839
+ if (err || !workItem) {
840
+ resolve();
841
+ return;
842
+ }
843
+
844
+ // Add epic info
845
+ const epic = await findEpic(workItem.id);
846
+ workItem.epic_id = epic ? epic.id : null;
847
+ workItem.epic_title = epic ? epic.title : null;
848
+
849
+ // Update CLAUDE.md with new mode
850
+ // Epics don't have mode lines in CLAUDE.md, pass null
851
+ const modeForClaudeMd = workItem.type === 'epic' ? null : mode;
852
+ updateCurrentWork(workItem, modeForClaudeMd);
853
+ console.log('📝 CLAUDE.md updated');
854
+ resolve();
855
+ });
856
+ } else {
857
+ resolve();
858
+ }
859
+ });
860
+ });
861
+ }); // Close outer db.get for epic check
862
+ });
863
+ }
864
+
865
+ // Set current work item
866
+ function setCurrent(id) {
867
+ const { setCurrentWork } = require('../../lib/current-work');
868
+ const { updateCurrentWork } = require('../../lib/claudemd');
869
+
870
+ db.run(`UPDATE work_items SET current = 0`, [], () => {
871
+ db.get(`
872
+ SELECT w.*,
873
+ p.title as parent_title, p.id as parent_id, p.type as parent_type
874
+ FROM work_items w
875
+ LEFT JOIN work_items p ON w.parent_id = p.id
876
+ WHERE w.id = ?
877
+ `, [id], async (err, workItem) => {
878
+ if (err || !workItem) {
879
+ console.log(`Work item #${id} not found`);
880
+ return;
881
+ }
882
+
883
+ // Find epic by traversing parent chain
884
+ const epic = await findEpic(workItem.id);
885
+
886
+ db.run(`UPDATE work_items SET current = 1 WHERE id = ?`, [id], () => {
887
+ // Create current work file
888
+ const currentWork = {
889
+ id: workItem.id,
890
+ title: workItem.title,
891
+ type: workItem.type,
892
+ status: workItem.status,
893
+ mode: workItem.mode,
894
+ parent_id: workItem.parent_id,
895
+ parent_title: workItem.parent_title,
896
+ parent_type: workItem.parent_type,
897
+ epic_id: epic ? epic.id : null,
898
+ epic_title: epic ? epic.title : null,
899
+ description: workItem.description
900
+ };
901
+
902
+ setCurrentWork(currentWork);
903
+ updateCurrentWork(currentWork, workItem.mode);
904
+
905
+ console.log(`Set #${id} as current work`);
906
+ });
907
+ });
908
+ });
909
+ }
910
+
911
+ // Re-export getCurrentWork from shared module for backwards compatibility
912
+ // (used by jettypod.js)
913
+
914
+ // Show single work item details
915
+ function showItem(id) {
916
+ return new Promise(async (resolve) => {
917
+ db.get(`
918
+ SELECT w.*,
919
+ p.title as parent_title
920
+ FROM work_items w
921
+ LEFT JOIN work_items p ON w.parent_id = p.id
922
+ WHERE w.id = ?
923
+ `, [id], async (err, row) => {
924
+ if (err || !row) {
925
+ console.log(`Work item #${id} not found`);
926
+ resolve(null);
927
+ } else {
928
+ const epic = await findEpic(row.id);
929
+
930
+ console.log(`\n#${row.id} ${row.title}`);
931
+ console.log(`Type: ${row.type}`);
932
+ console.log(`Status: ${row.status}`);
933
+ if (row.mode) console.log(`Mode: ${row.mode}`);
934
+ if (row.branch_name) console.log(`Branch: ${row.branch_name}`);
935
+ if (row.parent_title) console.log(`Parent: #${row.parent_id} ${row.parent_title}`);
936
+ if (epic) console.log(`Epic: #${epic.id} ${epic.title}`);
937
+
938
+ // Display prototype tracking information
939
+ if (row.prototype_files) {
940
+ try {
941
+ const prototypeFiles = JSON.parse(row.prototype_files);
942
+ if (prototypeFiles && prototypeFiles.length > 0) {
943
+ console.log(`\n🔬 Prototypes: ${prototypeFiles.join(', ')}`);
944
+ }
945
+ } catch (e) {
946
+ // Invalid JSON, skip
947
+ }
948
+ }
949
+ if (row.discovery_winner) {
950
+ console.log(`✅ Winner: ${row.discovery_winner}`);
951
+ }
952
+
953
+ // Display epic discovery information
954
+ if (row.type === 'epic' && row.needs_discovery) {
955
+ // Query all discovery decisions for this epic
956
+ db.all(
957
+ `SELECT * FROM discovery_decisions WHERE work_item_id = ? ORDER BY created_at`,
958
+ [row.id],
959
+ (err, decisions) => {
960
+ if (err) {
961
+ console.error(`Error fetching decisions: ${err.message}`);
962
+ }
963
+
964
+ if (decisions && decisions.length > 0) {
965
+ console.log(`\n🏛 Discovery Decisions:`);
966
+ decisions.forEach((d) => {
967
+ console.log(`\n ${d.aspect}:`);
968
+ console.log(` Decision: ${d.decision}`);
969
+ console.log(` Rationale: ${d.rationale}`);
970
+ });
971
+ console.log(`\n 💡 Add more decisions: jettypod work epic-implement ${row.id} --aspect="<type>" --decision="<approach>" --rationale="<why>"`);
972
+ } else {
973
+ console.log(`\n⚠️ DISCOVERY REQUIRED: This epic needs architectural decisions`);
974
+ console.log(``);
975
+ console.log(` 💬 Talk to Claude Code: "Let's do epic discovery for #${row.id}"`);
976
+ console.log(` Or run: jettypod work epic-planning ${row.id}`);
977
+ console.log(``);
978
+ console.log(` Claude Code will guide you through:`);
979
+ console.log(` • Suggesting 3 architectural options`);
980
+ console.log(` • Building prototypes`);
981
+ console.log(` • Recording your decisions`);
982
+ }
983
+
984
+ if (row.description) console.log(`\nDescription: ${row.description}`);
985
+ resolve(row);
986
+ }
987
+ );
988
+ } else {
989
+ if (row.description) console.log(`\nDescription: ${row.description}`);
990
+ resolve(row);
991
+ }
992
+ }
993
+ });
994
+ });
995
+ }
996
+
997
+ // Epic overview
998
+ function epicOverview(epicId) {
999
+ return new Promise(async (resolve) => {
1000
+ db.all('SELECT * FROM work_items', [], async (err, rows) => {
1001
+ if (err) {
1002
+ console.error(err);
1003
+ return resolve();
1004
+ }
1005
+
1006
+ // Filter items that belong to this epic
1007
+ const epicItems = [];
1008
+ for (const row of rows) {
1009
+ const epic = await findEpic(row.id);
1010
+ if ((epic && epic.id === epicId) || row.id === epicId) {
1011
+ epicItems.push(row);
1012
+ }
1013
+ }
1014
+
1015
+ console.log(`\nEpic #${epicId} Overview:`);
1016
+
1017
+ const byType = {};
1018
+ const byStatus = {};
1019
+
1020
+ epicItems.forEach(row => {
1021
+ if (!byType[row.type]) byType[row.type] = 0;
1022
+ byType[row.type]++;
1023
+
1024
+ if (!byStatus[row.status]) byStatus[row.status] = 0;
1025
+ byStatus[row.status]++;
1026
+ });
1027
+
1028
+ console.log('\nBy Type:');
1029
+ Object.entries(byType).forEach(([type, count]) => {
1030
+ console.log(` ${type}: ${count}`);
1031
+ });
1032
+
1033
+ console.log('\nBy Status:');
1034
+ Object.entries(byStatus).forEach(([status, count]) => {
1035
+ console.log(` ${status}: ${count}`);
1036
+ });
1037
+
1038
+ resolve();
1039
+ });
1040
+ });
1041
+ }
1042
+
1043
+ // Main CLI handler
1044
+ async function main() {
1045
+ const command = process.argv[2];
1046
+ const args = process.argv.slice(3);
1047
+
1048
+ switch(command) {
1049
+ case 'create': {
1050
+ const type = args[0];
1051
+ const title = args[1];
1052
+ const desc = args[2] || '';
1053
+
1054
+ let parentId = null;
1055
+ let mode = null;
1056
+ let needsDiscovery = false;
1057
+
1058
+ args.forEach(arg => {
1059
+ if (arg.startsWith('--parent=')) {
1060
+ parentId = parseInt(arg.split('=')[1]);
1061
+ }
1062
+ if (arg.startsWith('--mode=')) {
1063
+ mode = arg.split('=')[1];
1064
+ }
1065
+ if (arg === '--needs-discovery') {
1066
+ needsDiscovery = true;
1067
+ }
1068
+ });
1069
+
1070
+ try {
1071
+ const newId = await create(type, title, desc, parentId, mode, needsDiscovery);
1072
+
1073
+ // Prompt for epic discovery after epic creation
1074
+ if (type === 'epic') {
1075
+ console.log('');
1076
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1077
+ console.log('🎯 Plan this epic now?');
1078
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1079
+ console.log('');
1080
+ console.log('Ask Claude Code:');
1081
+ console.log(` "Help me plan epic #${newId}"`);
1082
+ console.log('');
1083
+ console.log('Claude will help you:');
1084
+ console.log(' • Brainstorm features for this epic');
1085
+ console.log(' • Identify architectural decisions (if needed)');
1086
+ console.log(' • Create features automatically');
1087
+ console.log('');
1088
+ console.log('Or run: jettypod work epic-planning ' + newId);
1089
+ console.log('');
1090
+ console.log('💡 You can also plan later when ready');
1091
+ }
1092
+
1093
+ // Check if creating feature under an unplanned epic
1094
+ if (type === 'feature' && parentId) {
1095
+ db.get(`SELECT * FROM work_items WHERE id = ?`, [parentId], (err, parent) => {
1096
+ if (!err && parent && parent.type === 'epic') {
1097
+ // Check if epic has any features already
1098
+ db.get(`SELECT COUNT(*) as count FROM work_items WHERE parent_id = ? AND type = 'feature'`, [parentId], (err, result) => {
1099
+ if (!err && result.count === 1) {
1100
+ // This is the first feature - suggest planning the epic
1101
+ console.log('');
1102
+ console.log('💡 Tip: Consider planning this epic first');
1103
+ console.log('');
1104
+ console.log('Ask Claude Code:');
1105
+ console.log(` "Help me plan epic #${parentId}"`);
1106
+ console.log('');
1107
+ console.log(`Or run: jettypod work epic-planning ${parentId}`);
1108
+ }
1109
+ });
1110
+ }
1111
+ });
1112
+ }
1113
+ } catch (err) {
1114
+ console.error(`Error: ${err.message}`);
1115
+ process.exit(1);
1116
+ }
1117
+ break;
1118
+ }
1119
+
1120
+ case 'tree': {
1121
+ try {
1122
+ // Wait for migrations to complete before running queries
1123
+ await waitForMigrations();
1124
+
1125
+ // Parse expand flags
1126
+ let expandedIds = null; // null = collapsed by default
1127
+ const expandArg = process.argv.find(arg => arg.startsWith('--expand'));
1128
+
1129
+ if (expandArg === '--expand-all') {
1130
+ expandedIds = 'all';
1131
+ } else if (expandArg && expandArg.includes('=')) {
1132
+ const idsStr = expandArg.split('=')[1];
1133
+ const ids = idsStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
1134
+ expandedIds = new Set(ids);
1135
+ }
1136
+
1137
+ // Query for ALL in_progress items
1138
+ const activeItems = await new Promise((resolve, reject) => {
1139
+ db.all(`
1140
+ SELECT w.id, w.title, w.type, w.status,
1141
+ p.title as parent_title, p.id as parent_id, p.type as parent_type,
1142
+ e.title as epic_title, e.id as epic_id
1143
+ FROM work_items w
1144
+ LEFT JOIN work_items p ON w.parent_id = p.id
1145
+ LEFT JOIN work_items e ON w.epic_id = e.id
1146
+ WHERE w.status = 'in_progress'
1147
+ ORDER BY w.id ASC
1148
+ `, [], (err, rows) => {
1149
+ if (err) return reject(err);
1150
+ resolve(rows || []);
1151
+ });
1152
+ });
1153
+
1154
+ // Show active work at top if exists
1155
+ if (activeItems.length > 0) {
1156
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1157
+ console.log('🎯 ACTIVE WORK');
1158
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1159
+
1160
+ activeItems.forEach((item, index) => {
1161
+ const emoji = TYPE_EMOJIS[item.type] || '📋';
1162
+ console.log(`${emoji} [#${item.id}] ${item.title}`);
1163
+ // Show epic if parent is the epic, otherwise show parent
1164
+ if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1165
+ console.log(`└─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1166
+ } else if (item.parent_title) {
1167
+ const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1168
+ console.log(`└─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1169
+ } else if (item.epic_title && item.epic_id !== item.id) {
1170
+ console.log(`└─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1171
+ }
1172
+
1173
+ // Add spacing between items except after last one
1174
+ if (index < activeItems.length - 1) {
1175
+ console.log('');
1176
+ }
1177
+ });
1178
+
1179
+ console.log('');
1180
+ }
1181
+
1182
+ // Show recently completed items
1183
+ const recentlyCompleted = await new Promise((resolve, reject) => {
1184
+ db.all(`
1185
+ SELECT w.id, w.title, w.type, w.mode,
1186
+ p.title as parent_title, p.id as parent_id, p.type as parent_type
1187
+ FROM work_items w
1188
+ LEFT JOIN work_items p ON w.parent_id = p.id
1189
+ WHERE w.status = 'done'
1190
+ ORDER BY w.id DESC
1191
+ LIMIT 3
1192
+ `, [], async (err, rows) => {
1193
+ if (err) reject(err);
1194
+ else {
1195
+ // Add epic info to each item
1196
+ for (const row of rows || []) {
1197
+ const epic = await findEpic(row.id);
1198
+ row.epic_id = epic ? epic.id : null;
1199
+ row.epic_title = epic ? epic.title : null;
1200
+ }
1201
+ resolve(rows || []);
1202
+ }
1203
+ });
1204
+ });
1205
+
1206
+ if (recentlyCompleted.length > 0) {
1207
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1208
+ console.log('✅ RECENTLY COMPLETED');
1209
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1210
+ recentlyCompleted.forEach(item => {
1211
+ const emoji = TYPE_EMOJIS[item.type] || '📋';
1212
+ let modeIndicator = '';
1213
+ if (item.mode) {
1214
+ modeIndicator = ` [${item.mode}]`;
1215
+ }
1216
+
1217
+ // Wrap the title text properly
1218
+ const titlePrefix = `${emoji} [${item.id}] `;
1219
+ const titleText = `${item.title}${modeIndicator}`;
1220
+ const visualBracketPos = getVisualWidth(emoji + ' ');
1221
+ const termWidth = getTerminalWidth();
1222
+ const prefixVisualWidth = getVisualWidth(titlePrefix);
1223
+ const maxTextWidth = termWidth - prefixVisualWidth;
1224
+ const wrappedTitle = wrapText(titleText, maxTextWidth, 0);
1225
+
1226
+ console.log(titlePrefix + wrappedTitle[0]);
1227
+ // Continuation lines align with the opening bracket
1228
+ const continuationPrefix = ' '.repeat(visualBracketPos);
1229
+ for (let i = 1; i < wrappedTitle.length; i++) {
1230
+ console.log(continuationPrefix + wrappedTitle[i]);
1231
+ }
1232
+
1233
+ // Wrap parent/epic lines too
1234
+ if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1235
+ const parentPrefix = ' └─ ';
1236
+ const parentText = `Epic: 🎯 #${item.epic_id} ${item.epic_title}`;
1237
+ const parentPrefixWidth = getVisualWidth(parentPrefix);
1238
+ const maxParentWidth = termWidth - parentPrefixWidth;
1239
+ const wrappedParent = wrapText(parentText, maxParentWidth, 0);
1240
+ console.log(parentPrefix + wrappedParent[0]);
1241
+ const parentContinuation = ' ';
1242
+ for (let i = 1; i < wrappedParent.length; i++) {
1243
+ console.log(parentContinuation + wrappedParent[i]);
1244
+ }
1245
+ } else if (item.parent_title) {
1246
+ const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1247
+ const parentPrefix = ' └─ ';
1248
+ const parentText = `Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`;
1249
+ const parentPrefixWidth = getVisualWidth(parentPrefix);
1250
+ const maxParentWidth = termWidth - parentPrefixWidth;
1251
+ const wrappedParent = wrapText(parentText, maxParentWidth, 0);
1252
+ console.log(parentPrefix + wrappedParent[0]);
1253
+ const parentContinuation = ' ';
1254
+ for (let i = 1; i < wrappedParent.length; i++) {
1255
+ console.log(parentContinuation + wrappedParent[i]);
1256
+ }
1257
+ } else if (item.epic_title && item.epic_id !== item.id) {
1258
+ const parentPrefix = ' └─ ';
1259
+ const parentText = `Epic: 🎯 #${item.epic_id} ${item.epic_title}`;
1260
+ const parentPrefixWidth = getVisualWidth(parentPrefix);
1261
+ const maxParentWidth = termWidth - parentPrefixWidth;
1262
+ const wrappedParent = wrapText(parentText, maxParentWidth, 0);
1263
+ console.log(parentPrefix + wrappedParent[0]);
1264
+ const parentContinuation = ' ';
1265
+ for (let i = 1; i < wrappedParent.length; i++) {
1266
+ console.log(parentContinuation + wrappedParent[i]);
1267
+ }
1268
+ }
1269
+ });
1270
+ console.log('');
1271
+ }
1272
+
1273
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1274
+ console.log('📋 BACKLOG');
1275
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1276
+ const items = await getTree();
1277
+
1278
+ // Default: expand all epics unless user specified expand flags
1279
+ if (expandedIds === null) {
1280
+ expandedIds = new Set();
1281
+ items.forEach(item => {
1282
+ if (item.type === 'epic') {
1283
+ expandedIds.add(item.id);
1284
+ }
1285
+ });
1286
+ }
1287
+
1288
+ printTree(items, '', true, expandedIds);
1289
+
1290
+ // Show legend and commands
1291
+ console.log('');
1292
+ console.log('Legend: ⊕ = collapsed ⊖ = expanded');
1293
+ console.log('');
1294
+ console.log('Commands:');
1295
+ console.log(' jettypod backlog --expand=1 Show details for item #1');
1296
+ console.log(' jettypod backlog --expand=1,2,3 Show details for multiple items');
1297
+ console.log(' jettypod backlog --expand-all Show all details');
1298
+ console.log(' jettypod backlog Collapse all (default)');
1299
+ console.log('');
1300
+ } catch (err) {
1301
+ console.error(`Error displaying backlog: ${err.message}`);
1302
+ process.exit(1);
1303
+ }
1304
+ break;
1305
+ }
1306
+
1307
+ case 'backlog': {
1308
+ try {
1309
+ // Wait for migrations to complete before running queries
1310
+ await waitForMigrations();
1311
+
1312
+ const filter = args[0]; // undefined, 'all', or 'completed'
1313
+
1314
+ // Check for --all flag (bypasses production filtering)
1315
+ const showAll = args.includes('--all');
1316
+
1317
+ // For 'all' and 'completed' filters, use old simple display
1318
+ if (filter === 'all' || filter === 'completed') {
1319
+ let items;
1320
+ if (filter === 'all') {
1321
+ items = await getTree(true, true); // includeCompleted=true, showAll=true
1322
+ } else {
1323
+ items = await new Promise((resolve, reject) => {
1324
+ db.all(`SELECT * FROM work_items WHERE status IN ('done', 'cancelled') ORDER BY parent_id, id`, [], (err, rows) => {
1325
+ if (err) {
1326
+ return reject(new Error(`Failed to fetch completed items: ${err.message}`));
1327
+ }
1328
+
1329
+ if (!rows || rows.length === 0) {
1330
+ return resolve([]);
1331
+ }
1332
+
1333
+ const itemsById = {};
1334
+ const rootItems = [];
1335
+
1336
+ rows.forEach(item => {
1337
+ itemsById[item.id] = item;
1338
+ item.children = [];
1339
+ });
1340
+
1341
+ rows.forEach(item => {
1342
+ if (item.parent_id && itemsById[item.parent_id]) {
1343
+ itemsById[item.parent_id].children.push(item);
1344
+ } else if (!item.parent_id) {
1345
+ rootItems.push(item);
1346
+ }
1347
+ });
1348
+
1349
+ resolve(rootItems);
1350
+ });
1351
+ });
1352
+ }
1353
+
1354
+ if (items.length === 0) {
1355
+ console.log('No items found.');
1356
+ } else {
1357
+ const expandedIds = new Set();
1358
+ items.forEach(item => {
1359
+ if (item.type === 'epic') {
1360
+ expandedIds.add(item.id);
1361
+ }
1362
+ });
1363
+ printTree(items, '', true, expandedIds);
1364
+ }
1365
+ console.log('');
1366
+ } else {
1367
+ // Default: show three-section view (active work, recently completed, backlog)
1368
+
1369
+ // Parse expand flags
1370
+ let expandedIds = null; // null = collapsed by default
1371
+ const expandArg = process.argv.find(arg => arg.startsWith('--expand'));
1372
+
1373
+ if (expandArg === '--expand-all') {
1374
+ expandedIds = 'all';
1375
+ } else if (expandArg && expandArg.includes('=')) {
1376
+ const idsStr = expandArg.split('=')[1];
1377
+ const ids = idsStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
1378
+ expandedIds = new Set(ids);
1379
+ }
1380
+
1381
+ // Query for ALL in_progress items
1382
+ const activeItems = await new Promise((resolve, reject) => {
1383
+ db.all(`
1384
+ SELECT w.id, w.title, w.type, w.status,
1385
+ p.title as parent_title, p.id as parent_id, p.type as parent_type,
1386
+ e.title as epic_title, e.id as epic_id
1387
+ FROM work_items w
1388
+ LEFT JOIN work_items p ON w.parent_id = p.id
1389
+ LEFT JOIN work_items e ON w.epic_id = e.id
1390
+ WHERE w.status = 'in_progress'
1391
+ ORDER BY w.id ASC
1392
+ `, [], (err, rows) => {
1393
+ if (err) return reject(err);
1394
+ resolve(rows || []);
1395
+ });
1396
+ });
1397
+
1398
+ // Show active work at top if exists
1399
+ if (activeItems.length > 0) {
1400
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1401
+ console.log('🎯 ACTIVE WORK');
1402
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1403
+
1404
+ activeItems.forEach((item, index) => {
1405
+ const emoji = TYPE_EMOJIS[item.type] || '📋';
1406
+ console.log(`${emoji} [#${item.id}] ${item.title}`);
1407
+ // Show epic if parent is the epic, otherwise show parent
1408
+ if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1409
+ console.log(`└─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1410
+ } else if (item.parent_title) {
1411
+ const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1412
+ console.log(`└─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1413
+ } else if (item.epic_title && item.epic_id !== item.epic_id) {
1414
+ console.log(`└─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1415
+ }
1416
+
1417
+ // Add spacing between items except after last one
1418
+ if (index < activeItems.length - 1) {
1419
+ console.log('');
1420
+ }
1421
+ });
1422
+
1423
+ console.log('');
1424
+ }
1425
+
1426
+ // Show recently completed items
1427
+ const recentlyCompleted = await new Promise((resolve, reject) => {
1428
+ db.all(`
1429
+ SELECT w.id, w.title, w.type, w.mode,
1430
+ p.title as parent_title, p.id as parent_id, p.type as parent_type
1431
+ FROM work_items w
1432
+ LEFT JOIN work_items p ON w.parent_id = p.id
1433
+ WHERE w.status = 'done'
1434
+ ORDER BY w.completed_at DESC, w.id DESC
1435
+ LIMIT 3
1436
+ `, [], async (err, rows) => {
1437
+ if (err) reject(err);
1438
+ else {
1439
+ // Add epic info to each item
1440
+ for (const row of rows || []) {
1441
+ const epic = await findEpic(row.id);
1442
+ if (epic) {
1443
+ row.epic_id = epic.id;
1444
+ row.epic_title = epic.title;
1445
+ }
1446
+ }
1447
+ resolve(rows || []);
1448
+ }
1449
+ });
1450
+ });
1451
+
1452
+ if (recentlyCompleted.length > 0) {
1453
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1454
+ console.log('✅ RECENTLY COMPLETED');
1455
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1456
+ recentlyCompleted.forEach(item => {
1457
+ const emoji = TYPE_EMOJIS[item.type] || '📋';
1458
+ let modeIndicator = '';
1459
+ if (item.mode) {
1460
+ modeIndicator = ` [${item.mode}]`;
1461
+ }
1462
+
1463
+ // Wrap the title text properly
1464
+ const titlePrefix = `${emoji} [${item.id}] `;
1465
+ const titleText = `${item.title}${modeIndicator}`;
1466
+ const visualBracketPos = getVisualWidth(emoji + ' ');
1467
+ const termWidth = getTerminalWidth();
1468
+ const prefixVisualWidth = getVisualWidth(titlePrefix);
1469
+ const maxTextWidth = termWidth - prefixVisualWidth;
1470
+ const wrappedTitle = wrapText(titleText, maxTextWidth, 0);
1471
+
1472
+ console.log(titlePrefix + wrappedTitle[0]);
1473
+ // Continuation lines align with the opening bracket
1474
+ const continuationPrefix = ' '.repeat(visualBracketPos);
1475
+ for (let i = 1; i < wrappedTitle.length; i++) {
1476
+ console.log(continuationPrefix + wrappedTitle[i]);
1477
+ }
1478
+
1479
+ // Wrap parent/epic lines too
1480
+ if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1481
+ const parentPrefix = ' └─ ';
1482
+ const parentText = `Epic: 🎯 #${item.epic_id} ${item.epic_title}`;
1483
+ const parentPrefixWidth = getVisualWidth(parentPrefix);
1484
+ const maxParentWidth = termWidth - parentPrefixWidth;
1485
+ const wrappedParent = wrapText(parentText, maxParentWidth, 0);
1486
+ console.log(parentPrefix + wrappedParent[0]);
1487
+ const parentContinuation = ' ';
1488
+ for (let i = 1; i < wrappedParent.length; i++) {
1489
+ console.log(parentContinuation + wrappedParent[i]);
1490
+ }
1491
+ } else if (item.parent_title) {
1492
+ const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1493
+ const parentPrefix = ' └─ ';
1494
+ const parentText = `Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`;
1495
+ const parentPrefixWidth = getVisualWidth(parentPrefix);
1496
+ const maxParentWidth = termWidth - parentPrefixWidth;
1497
+ const wrappedParent = wrapText(parentText, maxParentWidth, 0);
1498
+ console.log(parentPrefix + wrappedParent[0]);
1499
+ const parentContinuation = ' ';
1500
+ for (let i = 1; i < wrappedParent.length; i++) {
1501
+ console.log(parentContinuation + wrappedParent[i]);
1502
+ }
1503
+ } else if (item.epic_title && item.epic_id !== item.id) {
1504
+ const parentPrefix = ' └─ ';
1505
+ const parentText = `Epic: 🎯 #${item.epic_id} ${item.epic_title}`;
1506
+ const parentPrefixWidth = getVisualWidth(parentPrefix);
1507
+ const maxParentWidth = termWidth - parentPrefixWidth;
1508
+ const wrappedParent = wrapText(parentText, maxParentWidth, 0);
1509
+ console.log(parentPrefix + wrappedParent[0]);
1510
+ const parentContinuation = ' ';
1511
+ for (let i = 1; i < wrappedParent.length; i++) {
1512
+ console.log(parentContinuation + wrappedParent[i]);
1513
+ }
1514
+ }
1515
+ });
1516
+ console.log('');
1517
+ }
1518
+
1519
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1520
+ console.log('📋 BACKLOG');
1521
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1522
+ const items = await getTree(false, showAll);
1523
+
1524
+ printTree(items, '', true, expandedIds);
1525
+
1526
+ // Show legend and commands
1527
+ console.log('');
1528
+ console.log('Legend: ⊕ = collapsed ⊖ = expanded');
1529
+ console.log('');
1530
+ console.log('Commands:');
1531
+ console.log(' jettypod backlog --expand=1 Show details for item #1');
1532
+ console.log(' jettypod backlog --expand=1,2,3 Show details for multiple items');
1533
+ console.log(' jettypod backlog --expand-all Show all details');
1534
+ console.log(' jettypod backlog Collapse all (default)');
1535
+ console.log('');
1536
+ }
1537
+ } catch (err) {
1538
+ console.error(`Error displaying backlog: ${err.message}`);
1539
+ process.exit(1);
1540
+ }
1541
+ break;
1542
+ }
1543
+
1544
+ case 'status': {
1545
+ const id = parseInt(args[0]);
1546
+ const status = args[1];
1547
+ try {
1548
+ await updateStatus(id, status);
1549
+ } catch (err) {
1550
+ console.error(`Error: ${err.message}`);
1551
+ process.exit(1);
1552
+ }
1553
+ break;
1554
+ }
1555
+
1556
+ case 'set-branch': {
1557
+ const id = parseInt(args[0]);
1558
+ const branch = args[1];
1559
+ setBranch(id, branch);
1560
+ break;
1561
+ }
1562
+
1563
+ case 'set-mode': {
1564
+ const id = parseInt(args[0]);
1565
+ const mode = args[1];
1566
+ await setMode(id, mode);
1567
+ break;
1568
+ }
1569
+
1570
+ case 'current': {
1571
+ if (!args[0]) {
1572
+ // No ID provided - show current work
1573
+ const currentWork = await getCurrentWork();
1574
+ if (!currentWork) {
1575
+ console.log('No active work');
1576
+ console.log('');
1577
+ console.log('Start work on an item: jettypod work start <id>');
1578
+ } else {
1579
+ const emoji = TYPE_EMOJIS[currentWork.type] || '📋';
1580
+ console.log(`${emoji} Current work: [#${currentWork.id}] ${currentWork.title}`);
1581
+ console.log(` Type: ${currentWork.type}`);
1582
+ console.log(` Status: ${currentWork.status}`);
1583
+ if (currentWork.parent_title) {
1584
+ const parentEmoji = TYPE_EMOJIS['feature'] || '✨';
1585
+ console.log(` Parent: ${parentEmoji} [#${currentWork.parent_id}] ${currentWork.parent_title}`);
1586
+ }
1587
+ if (currentWork.epic_title) {
1588
+ console.log(` Epic: 🎯 [#${currentWork.epic_id}] ${currentWork.epic_title}`);
1589
+ }
1590
+ }
1591
+ } else {
1592
+ // ID provided - set current work
1593
+ const id = parseInt(args[0]);
1594
+ setCurrent(id);
1595
+ }
1596
+ break;
1597
+ }
1598
+
1599
+ case 'show': {
1600
+ if (!args[0]) {
1601
+ console.error('Error: Work item ID is required');
1602
+ console.log('');
1603
+ console.log('Usage: jettypod work show <id>');
1604
+ console.log('');
1605
+ console.log('Example:');
1606
+ console.log(' jettypod work show 5');
1607
+ console.log('');
1608
+ console.log('💡 Tip: Use `jettypod backlog` to see all work items');
1609
+ process.exit(1);
1610
+ }
1611
+
1612
+ const id = parseInt(args[0]);
1613
+ if (isNaN(id)) {
1614
+ console.error(`Error: Invalid work item ID: ${args[0]}`);
1615
+ console.log('ID must be a number');
1616
+ process.exit(1);
1617
+ }
1618
+
1619
+ await showItem(id);
1620
+ break;
1621
+ }
1622
+
1623
+ case 'describe': {
1624
+ const id = parseInt(args[0]);
1625
+ const description = args.slice(1).join(' ');
1626
+
1627
+ db.run(`UPDATE work_items SET description = ? WHERE id = ?`, [description, id], (err) => {
1628
+ if (err) {
1629
+ console.error(`Error: ${err.message}`);
1630
+ process.exit(1);
1631
+ }
1632
+ console.log(`Updated #${id} description`);
1633
+ });
1634
+ break;
1635
+ }
1636
+
1637
+ case 'epic': {
1638
+ const epicId = parseInt(args[0]);
1639
+ await epicOverview(epicId);
1640
+ break;
1641
+ }
1642
+
1643
+ case 'completed': {
1644
+ db.all(`
1645
+ SELECT w.*,
1646
+ p.title as parent_title, p.type as parent_type
1647
+ FROM work_items w
1648
+ LEFT JOIN work_items p ON w.parent_id = p.id
1649
+ WHERE w.status = 'done'
1650
+ ORDER BY w.id DESC
1651
+ `, [], async (err, rows) => {
1652
+ if (err) {
1653
+ console.error(`Error fetching completed items: ${err.message}`);
1654
+ process.exit(1);
1655
+ }
1656
+
1657
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1658
+ console.log('✅ COMPLETED WORK');
1659
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1660
+
1661
+ if (!rows || rows.length === 0) {
1662
+ console.log('No completed work items');
1663
+ console.log('');
1664
+ } else {
1665
+ // Add epic info to each item
1666
+ for (const row of rows) {
1667
+ const epic = await findEpic(row.id);
1668
+ row.epic_id = epic ? epic.id : null;
1669
+ row.epic_title = epic ? epic.title : null;
1670
+ }
1671
+
1672
+ // Group by mode
1673
+ const byMode = {
1674
+ speed: [],
1675
+ stable: [],
1676
+ production: [],
1677
+ other: []
1678
+ };
1679
+
1680
+ rows.forEach(item => {
1681
+ if (item.mode === 'speed') byMode.speed.push(item);
1682
+ else if (item.mode === 'stable') byMode.stable.push(item);
1683
+ else if (item.mode === 'production') byMode.production.push(item);
1684
+ else byMode.other.push(item);
1685
+ });
1686
+
1687
+ // Display Speed Mode section
1688
+ if (byMode.speed.length > 0) {
1689
+ console.log('\n⚡ SPEED MODE');
1690
+ byMode.speed.forEach(item => {
1691
+ const emoji = TYPE_EMOJIS[item.type] || '📋';
1692
+ console.log(`${emoji} [#${item.id}] ${item.title}`);
1693
+ // Show epic if parent is the epic, otherwise show parent
1694
+ if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1695
+ console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1696
+ } else if (item.parent_title) {
1697
+ const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1698
+ console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1699
+ } else if (item.epic_title && item.epic_id !== item.id) {
1700
+ console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1701
+ }
1702
+ });
1703
+ }
1704
+
1705
+ // Display Stable Mode section
1706
+ if (byMode.stable.length > 0) {
1707
+ console.log('\n🔒 STABLE MODE');
1708
+ byMode.stable.forEach(item => {
1709
+ const emoji = TYPE_EMOJIS[item.type] || '📋';
1710
+ console.log(`${emoji} [#${item.id}] ${item.title}`);
1711
+ // Show epic if parent is the epic, otherwise show parent
1712
+ if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1713
+ console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1714
+ } else if (item.parent_title) {
1715
+ const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1716
+ console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1717
+ } else if (item.epic_title && item.epic_id !== item.id) {
1718
+ console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1719
+ }
1720
+ });
1721
+ }
1722
+
1723
+ // Display Production Mode section
1724
+ if (byMode.production.length > 0) {
1725
+ console.log('\n🚀 PRODUCTION MODE');
1726
+ byMode.production.forEach(item => {
1727
+ const emoji = TYPE_EMOJIS[item.type] || '📋';
1728
+ console.log(`${emoji} [#${item.id}] ${item.title}`);
1729
+ // Show epic if parent is the epic, otherwise show parent
1730
+ if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1731
+ console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1732
+ } else if (item.parent_title) {
1733
+ const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1734
+ console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1735
+ } else if (item.epic_title && item.epic_id !== item.id) {
1736
+ console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1737
+ }
1738
+ });
1739
+ }
1740
+
1741
+ // Display Other/Untagged section
1742
+ if (byMode.other.length > 0) {
1743
+ console.log('\n📦 OTHER');
1744
+ byMode.other.forEach(item => {
1745
+ const emoji = TYPE_EMOJIS[item.type] || '📋';
1746
+ const modeIndicator = item.mode ? ` [${item.mode}]` : '';
1747
+ console.log(`${emoji} [#${item.id}] ${item.title}${modeIndicator}`);
1748
+ // Show epic if parent is the epic, otherwise show parent
1749
+ if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1750
+ console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1751
+ } else if (item.parent_title) {
1752
+ const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1753
+ console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1754
+ } else if (item.epic_title && item.epic_id !== item.id) {
1755
+ console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1756
+ }
1757
+ });
1758
+ }
1759
+
1760
+ console.log('');
1761
+ }
1762
+ });
1763
+ break;
1764
+ }
1765
+
1766
+ case 'epic-planning': {
1767
+ const epicId = parseInt(args[0]);
1768
+
1769
+ if (!epicId || isNaN(epicId)) {
1770
+ console.error('Error: Epic ID is required');
1771
+ console.log('Usage: jettypod work epic-planning <epic-id>');
1772
+ process.exit(1);
1773
+ }
1774
+
1775
+ db.get(`SELECT * FROM work_items WHERE id = ?`, [epicId], (err, epic) => {
1776
+ if (err) {
1777
+ console.error(`Error: ${err.message}`);
1778
+ process.exit(1);
1779
+ }
1780
+
1781
+ if (!epic) {
1782
+ console.error(`Error: Epic #${epicId} not found`);
1783
+ process.exit(1);
1784
+ }
1785
+
1786
+ if (epic.type !== 'epic') {
1787
+ console.error(`Error: Work item #${epicId} is not an epic (type: ${epic.type})`);
1788
+ process.exit(1);
1789
+ }
1790
+
1791
+ if (!epic.needs_discovery) {
1792
+ console.error(`Error: Epic #${epicId} does not need discovery (needs_discovery=false)`);
1793
+ console.log('');
1794
+ console.log('Create epics with --needs-discovery flag if architectural decisions needed:');
1795
+ console.log(' jettypod work create epic "Epic Title" "Description" --needs-discovery');
1796
+ process.exit(1);
1797
+ }
1798
+
1799
+ // Check if any decisions have been recorded
1800
+ db.all(
1801
+ `SELECT * FROM discovery_decisions WHERE work_item_id = ?`,
1802
+ [epicId],
1803
+ (err, decisions) => {
1804
+ if (err) {
1805
+ console.error(`Error: ${err.message}`);
1806
+ process.exit(1);
1807
+ }
1808
+
1809
+ if (decisions && decisions.length > 0) {
1810
+ console.log(`📋 Epic #${epicId} discovery decisions:`);
1811
+ console.log('');
1812
+ decisions.forEach((d) => {
1813
+ console.log(` ${d.aspect}: ${d.decision}`);
1814
+ console.log(` Rationale: ${d.rationale}`);
1815
+ console.log('');
1816
+ });
1817
+ console.log('💡 You can add more decisions for different aspects:');
1818
+ console.log(` jettypod work epic-implement ${epicId} --aspect="<type>" --decision="<approach>" --rationale="<why>"`);
1819
+ return;
1820
+ }
1821
+
1822
+ console.log(`🔍 Starting epic discovery for #${epicId}: ${epic.title}`);
1823
+ console.log('');
1824
+ console.log(`Description: ${epic.description || 'No description'}`);
1825
+ console.log('');
1826
+ console.log('────────────────────────────────────────────────────');
1827
+ console.log('Epic Discovery Context (for Claude Code):');
1828
+ console.log('────────────────────────────────────────────────────');
1829
+ console.log(`Epic ID: ${epicId}`);
1830
+ console.log(`Title: ${epic.title}`);
1831
+ console.log(`Description: ${epic.description || 'Not provided'}`);
1832
+ console.log('Needs Discovery: true');
1833
+ console.log('');
1834
+ console.log('💬 Now ask Claude Code:');
1835
+ console.log(` "Help me with epic discovery for #${epicId}"`);
1836
+ console.log('');
1837
+ console.log('Claude will use the epic-planning skill to guide you through:');
1838
+ console.log(' 1. Feature brainstorming');
1839
+ console.log(' 2. Architectural decisions (if needed)');
1840
+ console.log(' 3. Prototype validation (optional)');
1841
+ console.log(' 4. Feature creation');
1842
+ console.log('');
1843
+ console.log('📋 The skill is at: .claude/skills/epic-planning/SKILL.md');
1844
+ }
1845
+ );
1846
+ });
1847
+ break;
1848
+ }
1849
+
1850
+ case 'epic-implement': {
1851
+ const epicId = parseInt(args[0]);
1852
+
1853
+ if (!epicId || isNaN(epicId)) {
1854
+ console.error('Error: Epic ID is required');
1855
+ console.log('Usage: jettypod work epic-implement <epic-id> --aspect="<type>" --decision="<approach>" --rationale="<why>"');
1856
+ process.exit(1);
1857
+ }
1858
+
1859
+ // Parse --aspect, --decision, --rationale, and --prototypes flags
1860
+ const aspectIndex = args.findIndex(a => a.startsWith('--aspect='));
1861
+ const decisionIndex = args.findIndex(a => a.startsWith('--decision='));
1862
+ const rationaleIndex = args.findIndex(a => a.startsWith('--rationale='));
1863
+ const prototypesIndex = args.findIndex(a => a.startsWith('--prototypes='));
1864
+
1865
+ if (aspectIndex === -1 || decisionIndex === -1 || rationaleIndex === -1) {
1866
+ console.error('Error: --aspect, --decision, and --rationale are all required');
1867
+ console.log('');
1868
+ console.log('Usage: jettypod work epic-implement <epic-id> --aspect="<type>" --decision="<approach>" --rationale="<why>" [--prototypes="file1,file2"]');
1869
+ console.log('');
1870
+ console.log('Example:');
1871
+ console.log(' jettypod work epic-implement 5 \\');
1872
+ console.log(' --aspect="Architecture" \\');
1873
+ console.log(' --decision="WebSockets with Socket.io" \\');
1874
+ console.log(' --rationale="Bidirectional real-time updates needed, Socket.io provides fallbacks" \\');
1875
+ console.log(' --prototypes="prototypes/websockets.js,prototypes/sse.js"');
1876
+ console.log('');
1877
+ console.log('Common aspects: Architecture, Design Pattern, State Management, API Design, Testing Strategy');
1878
+ process.exit(1);
1879
+ }
1880
+
1881
+ const aspect = args[aspectIndex].split('=')[1].replace(/^["']|["']$/g, '');
1882
+ const decision = args[decisionIndex].split('=')[1].replace(/^["']|["']$/g, '');
1883
+ const rationale = args[rationaleIndex].split('=').slice(1).join('=').replace(/^["']|["']$/g, '');
1884
+ const prototypes = prototypesIndex !== -1
1885
+ ? args[prototypesIndex].split('=')[1].replace(/^["']|["']$/g, '').split(',').map(p => p.trim())
1886
+ : [];
1887
+
1888
+ // Validate inputs
1889
+ if (!aspect || !decision || !rationale) {
1890
+ console.error('Error: --aspect, --decision, and --rationale must all have values');
1891
+ process.exit(1);
1892
+ }
1893
+
1894
+ if (aspect.trim().length === 0 || rationale.trim().length === 0) {
1895
+ console.error('Error: --aspect and --rationale cannot be empty or whitespace only');
1896
+ console.log('');
1897
+ console.log('Provide meaningful values for the decision aspect and rationale.');
1898
+ process.exit(1);
1899
+ }
1900
+
1901
+ // Get and validate epic
1902
+ db.get(`SELECT * FROM work_items WHERE id = ?`, [epicId], (err, epic) => {
1903
+ if (err) {
1904
+ console.error(`Error: ${err.message}`);
1905
+ process.exit(1);
1906
+ }
1907
+
1908
+ if (!epic) {
1909
+ console.error(`Error: Epic #${epicId} not found`);
1910
+ process.exit(1);
1911
+ }
1912
+
1913
+ if (epic.type !== 'epic') {
1914
+ console.error(`Error: Work item #${epicId} is not an epic (type: ${epic.type})`);
1915
+ process.exit(1);
1916
+ }
1917
+
1918
+ if (!epic.needs_discovery) {
1919
+ console.error(`Error: Epic #${epicId} does not need discovery (needs_discovery=false)`);
1920
+ process.exit(1);
1921
+ }
1922
+
1923
+ // Check if this aspect already has a decision
1924
+ db.get(
1925
+ `SELECT * FROM discovery_decisions WHERE work_item_id = ? AND aspect = ?`,
1926
+ [epicId, aspect],
1927
+ (err, existingDecision) => {
1928
+ if (err) {
1929
+ console.error(`Error: ${err.message}`);
1930
+ process.exit(1);
1931
+ }
1932
+
1933
+ if (existingDecision) {
1934
+ console.error(`Error: Epic #${epicId} already has a decision for aspect "${aspect}"`);
1935
+ console.log('');
1936
+ console.log(`Current decision: ${existingDecision.decision}`);
1937
+ console.log(`Rationale: ${existingDecision.rationale}`);
1938
+ console.log('');
1939
+ console.log('Use a different aspect name or update the database directly to change this decision.');
1940
+ process.exit(1);
1941
+ }
1942
+
1943
+ // Insert new discovery decision
1944
+ db.run(
1945
+ `INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale)
1946
+ VALUES (?, ?, ?, ?)`,
1947
+ [epicId, aspect, decision, rationale],
1948
+ (err) => {
1949
+ if (err) {
1950
+ console.error(`Error: ${err.message}`);
1951
+ process.exit(1);
1952
+ }
1953
+
1954
+ // Update work_items with prototype tracking if provided
1955
+ if (prototypes.length > 0) {
1956
+ db.run(
1957
+ `UPDATE work_items SET prototype_files = ?, discovery_winner = ? WHERE id = ?`,
1958
+ [JSON.stringify(prototypes), decision, epicId],
1959
+ (err) => {
1960
+ if (err) {
1961
+ console.warn(`⚠️ Could not save prototypes: ${err.message}`);
1962
+ }
1963
+ }
1964
+ );
1965
+ }
1966
+
1967
+ console.log(`✅ Epic #${epicId} discovery decision recorded!`);
1968
+ console.log('');
1969
+ console.log(`Aspect: ${aspect}`);
1970
+ console.log(`Decision: ${decision}`);
1971
+ console.log(`Rationale: ${rationale}`);
1972
+ if (prototypes.length > 0) {
1973
+ console.log(`Prototypes: ${prototypes.join(', ')}`);
1974
+ }
1975
+ console.log('');
1976
+ console.log('📝 Architectural decision recorded');
1977
+ console.log('');
1978
+
1979
+ // Generate DECISIONS.md
1980
+ (async () => {
1981
+ try {
1982
+ const { generateDecisionsFile } = require('../../lib/decisions-generator');
1983
+ await generateDecisionsFile();
1984
+ console.log('📋 DECISIONS.md updated');
1985
+ console.log('');
1986
+ } catch (err) {
1987
+ console.warn('⚠️ Could not generate DECISIONS.md:', err.message);
1988
+ console.log('');
1989
+ }
1990
+
1991
+ console.log('You can add more decisions for other aspects:');
1992
+ console.log(` jettypod work epic-implement ${epicId} --aspect="<type>" --decision="<approach>" --rationale="<why>"`);
1993
+ console.log('');
1994
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1995
+ console.log('✨ Next Step: Create Features');
1996
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1997
+ console.log('');
1998
+ console.log('Create features for this epic:');
1999
+ console.log(` jettypod work create feature "Feature Title" "Description" --parent=${epicId}`);
2000
+ console.log('');
2001
+ console.log('Then plan each feature:');
2002
+ console.log(' jettypod work discover <feature-id>');
2003
+ console.log('');
2004
+ console.log('💡 Tip: Claude Code will suggest UX approaches and generate BDD scenarios');
2005
+ })();
2006
+ }
2007
+ );
2008
+ }
2009
+ );
2010
+ });
2011
+ break;
2012
+ }
2013
+
2014
+ case 'implement': {
2015
+ const featureId = parseInt(args[0]);
2016
+
2017
+ if (!featureId || isNaN(featureId)) {
2018
+ console.error('Error: Feature ID is required');
2019
+ console.log('Usage: jettypod work implement <feature-id> [--scenario-file="path"] [--prototypes="file1,file2"] [--winner="file.js"] [--rationale="why this approach"]');
2020
+ process.exit(1);
2021
+ }
2022
+
2023
+ // Parse optional flags
2024
+ const scenarioFileIndex = args.findIndex(a => a.startsWith('--scenario-file='));
2025
+ const prototypesIndex = args.findIndex(a => a.startsWith('--prototypes='));
2026
+ const winnerIndex = args.findIndex(a => a.startsWith('--winner='));
2027
+ const rationaleIndex = args.findIndex(a => a.startsWith('--rationale='));
2028
+
2029
+ const scenarioFileArg = scenarioFileIndex !== -1
2030
+ ? args[scenarioFileIndex].split('=')[1].replace(/^["']|["']$/g, '')
2031
+ : null;
2032
+ const prototypes = prototypesIndex !== -1
2033
+ ? args[prototypesIndex].split('=')[1].replace(/^["']|["']$/g, '').split(',').map(p => p.trim())
2034
+ : [];
2035
+ const winner = winnerIndex !== -1
2036
+ ? args[winnerIndex].split('=')[1].replace(/^["']|["']$/g, '')
2037
+ : null;
2038
+ const rationale = rationaleIndex !== -1
2039
+ ? args[rationaleIndex].split('=')[1].replace(/^["']|["']$/g, '')
2040
+ : null;
2041
+
2042
+ db.get(`SELECT * FROM work_items WHERE id = ?`, [featureId], (err, feature) => {
2043
+ if (err) {
2044
+ console.error(`Error: ${err.message}`);
2045
+ process.exit(1);
2046
+ }
2047
+
2048
+ if (!feature) {
2049
+ console.error(`Error: Feature #${featureId} not found`);
2050
+ process.exit(1);
2051
+ }
2052
+
2053
+ if (feature.type !== 'feature') {
2054
+ console.error(`Error: Work item #${featureId} is not a feature (type: ${feature.type})`);
2055
+ console.log('');
2056
+ console.log('The implement command transitions features from Discovery to Implementation phase.');
2057
+ console.log('Only features have phases.');
2058
+ process.exit(1);
2059
+ }
2060
+
2061
+ // Use scenario_file from flag if provided, otherwise from database
2062
+ const scenarioFile = scenarioFileArg || feature.scenario_file;
2063
+
2064
+ // Validate that BDD scenarios exist (either from flag or database)
2065
+ if (!scenarioFile) {
2066
+ console.error(`Error: Feature #${featureId} has no BDD scenarios`);
2067
+ console.log('');
2068
+ console.log('Discovery is not complete without BDD scenarios.');
2069
+ console.log('');
2070
+ console.log('To complete discovery, provide the scenario file:');
2071
+ console.log(` jettypod work implement ${featureId} --scenario-file="features/[feature-name].feature" --winner="..." --rationale="..."`);
2072
+ console.log('');
2073
+ console.log('Or generate scenarios using the feature-planning skill first.');
2074
+ process.exit(1);
2075
+ }
2076
+
2077
+ // Check if scenario file actually exists on disk
2078
+ const fs = require('fs');
2079
+ const path = require('path');
2080
+ const scenarioPath = path.join(process.cwd(), scenarioFile);
2081
+
2082
+ if (!fs.existsSync(scenarioPath)) {
2083
+ console.error(`Error: Scenario file not found: ${scenarioFile}`);
2084
+ console.log('');
2085
+ console.log('The scenario file must exist on disk before transitioning to implementation.');
2086
+ console.log('');
2087
+ console.log('To fix this:');
2088
+ console.log(` 1. Create the file at: ${scenarioFile}`);
2089
+ console.log(' 2. Or check the path is correct');
2090
+ process.exit(1);
2091
+ }
2092
+
2093
+ // Determine if this is a transition or an update
2094
+ // Also handle the case where phase is implementation but mode is not set (partial state)
2095
+ const isTransition = feature.phase === 'discovery' || !feature.mode;
2096
+ const isUpdate = feature.phase === 'implementation' && feature.mode;
2097
+
2098
+ // Prepare values
2099
+ const prototypeFilesValue = prototypes.length > 0 ? JSON.stringify(prototypes) : null;
2100
+ const winnerValue = winner || null;
2101
+ const rationaleValue = rationale || null;
2102
+ const scenarioFileValue = scenarioFile; // Already validated above
2103
+
2104
+ // Update query: if transitioning, set phase, mode, and scenario_file; if updating, just update decision fields
2105
+ let updateSql;
2106
+ let updateParams;
2107
+
2108
+ if (isTransition) {
2109
+ // When transitioning to implementation, ensure status is 'todo' (ready for work)
2110
+ // This prevents status from being left as NULL or in a discovery-related state
2111
+ updateSql = `UPDATE work_items SET phase = 'implementation', mode = 'speed', status = 'todo', scenario_file = ?, prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
2112
+ updateParams = [scenarioFileValue, prototypeFilesValue, winnerValue, rationaleValue, featureId];
2113
+ } else {
2114
+ updateSql = `UPDATE work_items SET prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
2115
+ updateParams = [prototypeFilesValue, winnerValue, rationaleValue, featureId];
2116
+ }
2117
+
2118
+ db.run(
2119
+ updateSql,
2120
+ updateParams,
2121
+ (err) => {
2122
+ if (err) {
2123
+ console.error(`Error: ${err.message}`);
2124
+ process.exit(1);
2125
+ }
2126
+
2127
+ console.log('');
2128
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2129
+ if (isTransition) {
2130
+ console.log(`✅ Feature #${featureId} transitioned to Implementation Phase`);
2131
+ } else {
2132
+ console.log(`✅ Feature #${featureId} discovery decision updated`);
2133
+ }
2134
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2135
+ console.log('');
2136
+ console.log(`Title: ${feature.title}`);
2137
+ console.log('Phase: Implementation');
2138
+ console.log('Mode: Speed');
2139
+ console.log(`Scenarios: ${scenarioFileValue}`);
2140
+ if (prototypes.length > 0) {
2141
+ console.log(`Prototypes: ${prototypes.join(', ')}`);
2142
+ }
2143
+ if (winner) {
2144
+ console.log(`Winner: ${winner}`);
2145
+ }
2146
+ if (rationale) {
2147
+ console.log(`Rationale: ${rationale}`);
2148
+ }
2149
+ console.log('');
2150
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2151
+ console.log('🚀 Speed Mode: Prove It Works');
2152
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2153
+ console.log('');
2154
+ console.log('Build code that passes happy path scenarios.');
2155
+ console.log('Focus:');
2156
+ console.log(' • Happy path only');
2157
+ console.log(' • Single file when possible');
2158
+ console.log(' • localStorage for data');
2159
+ console.log(' • Basic try/catch');
2160
+ console.log('');
2161
+ console.log('⚠️ Speed Mode is a checkpoint - pass through quickly!');
2162
+ console.log('');
2163
+ console.log('Next: Elevate to Stable Mode');
2164
+ console.log(` jettypod work elevate ${featureId} stable`);
2165
+ console.log('');
2166
+ }
2167
+ );
2168
+ });
2169
+ break;
2170
+ }
2171
+
2172
+ case 'discover': {
2173
+ const featureId = parseInt(args[0]);
2174
+
2175
+ if (!featureId || isNaN(featureId)) {
2176
+ console.error('Error: Feature ID is required');
2177
+ console.log('Usage: jettypod work discover <feature-id>');
2178
+ process.exit(1);
2179
+ }
2180
+
2181
+ db.get(`SELECT * FROM work_items WHERE id = ?`, [featureId], (err, feature) => {
2182
+ if (err) {
2183
+ console.error(`Error: ${err.message}`);
2184
+ process.exit(1);
2185
+ }
2186
+
2187
+ if (!feature) {
2188
+ console.error(`Error: Feature #${featureId} not found`);
2189
+ process.exit(1);
2190
+ }
2191
+
2192
+ if (feature.type !== 'feature') {
2193
+ console.error(`Error: Work item #${featureId} is not a feature (type: ${feature.type})`);
2194
+ process.exit(1);
2195
+ }
2196
+
2197
+ // Get parent epic if exists
2198
+ db.get(`SELECT * FROM work_items WHERE id = ?`, [feature.parent_id || feature.epic_id], (err, epic) => {
2199
+ // Get epic's architectural decisions if epic exists
2200
+ if (epic && epic.type === 'epic') {
2201
+ db.all(
2202
+ `SELECT * FROM discovery_decisions WHERE work_item_id = ?`,
2203
+ [epic.id],
2204
+ (err, decisions) => {
2205
+ console.log(`✨ Starting feature discovery for #${featureId}: ${feature.title}`);
2206
+ console.log('');
2207
+ console.log(`Description: ${feature.description || 'No description'}`);
2208
+ console.log('');
2209
+
2210
+ if (epic) {
2211
+ console.log(`Epic: #${epic.id} ${epic.title}`);
2212
+
2213
+ if (decisions && decisions.length > 0) {
2214
+ console.log('');
2215
+ console.log('📋 Epic architectural decisions:');
2216
+ decisions.forEach((d) => {
2217
+ console.log(` • ${d.aspect}: ${d.decision}`);
2218
+ });
2219
+ }
2220
+ }
2221
+
2222
+ console.log('');
2223
+ console.log('────────────────────────────────────────────────────');
2224
+ console.log('Feature Discovery Context (for Claude Code):');
2225
+ console.log('────────────────────────────────────────────────────');
2226
+ console.log(`Feature ID: ${featureId}`);
2227
+ console.log(`Title: ${feature.title}`);
2228
+ console.log(`Description: ${feature.description || 'Not provided'}`);
2229
+ if (epic) {
2230
+ console.log(`Epic: #${epic.id} ${epic.title}`);
2231
+ if (decisions && decisions.length > 0) {
2232
+ console.log('Architectural Constraints:');
2233
+ decisions.forEach((d) => {
2234
+ console.log(` ${d.aspect}: ${d.decision}`);
2235
+ });
2236
+ }
2237
+ }
2238
+ console.log('');
2239
+ console.log('💬 Now ask Claude Code:');
2240
+ console.log(` "Help me with feature discovery for #${featureId}"`);
2241
+ console.log('');
2242
+ console.log('Claude will use the feature-planning skill to guide you through:');
2243
+ console.log(' 1. Suggesting 3 UX approaches');
2244
+ console.log(' 2. Optional prototyping');
2245
+ console.log(' 3. Choosing the winner');
2246
+ console.log(' 4. Generating BDD scenarios');
2247
+ console.log(' 5. Transitioning to implementation');
2248
+ console.log('');
2249
+ console.log('📋 The skill is at: .claude/skills/feature-planning/SKILL.md');
2250
+ }
2251
+ );
2252
+ } else {
2253
+ console.log(`✨ Starting feature discovery for #${featureId}: ${feature.title}`);
2254
+ console.log('');
2255
+ console.log(`Description: ${feature.description || 'No description'}`);
2256
+ console.log('');
2257
+ console.log('────────────────────────────────────────────────────');
2258
+ console.log('Feature Discovery Context (for Claude Code):');
2259
+ console.log('────────────────────────────────────────────────────');
2260
+ console.log(`Feature ID: ${featureId}`);
2261
+ console.log(`Title: ${feature.title}`);
2262
+ console.log(`Description: ${feature.description || 'Not provided'}`);
2263
+ console.log('');
2264
+ console.log('💬 Now ask Claude Code:');
2265
+ console.log(` "Help me with feature discovery for #${featureId}"`);
2266
+ console.log('');
2267
+ console.log('Claude will use the feature-planning skill to guide you through:');
2268
+ console.log(' 1. Suggesting 3 UX approaches');
2269
+ console.log(' 2. Optional prototyping');
2270
+ console.log(' 3. Choosing the winner');
2271
+ console.log(' 4. Generating BDD scenarios');
2272
+ console.log(' 5. Transitioning to implementation');
2273
+ console.log('');
2274
+ console.log('📋 The skill is at: .claude/skills/feature-planning/SKILL.md');
2275
+ }
2276
+ });
2277
+ });
2278
+ break;
2279
+ }
2280
+
2281
+ case 'children': {
2282
+ const parentId = parseInt(args[0]);
2283
+
2284
+ if (!parentId || isNaN(parentId)) {
2285
+ console.log('Usage: jettypod work children <parent-id>');
2286
+ console.log('');
2287
+ console.log('Lists all direct children of a work item.');
2288
+ console.log('');
2289
+ console.log('Example:');
2290
+ console.log(' jettypod work children 42 # List children of epic/feature #42');
2291
+ process.exit(1);
2292
+ }
2293
+
2294
+ try {
2295
+ await waitForMigrations();
2296
+
2297
+ // First verify the parent exists
2298
+ const parent = await new Promise((resolve, reject) => {
2299
+ db.get('SELECT id, title, type FROM work_items WHERE id = ?', [parentId], (err, row) => {
2300
+ if (err) reject(err);
2301
+ else resolve(row);
2302
+ });
2303
+ });
2304
+
2305
+ if (!parent) {
2306
+ console.error(`Error: Work item #${parentId} not found`);
2307
+ process.exit(1);
2308
+ }
2309
+
2310
+ // Get children
2311
+ const children = await new Promise((resolve, reject) => {
2312
+ db.all(
2313
+ 'SELECT id, title, type, status, mode FROM work_items WHERE parent_id = ? ORDER BY id',
2314
+ [parentId],
2315
+ (err, rows) => {
2316
+ if (err) reject(err);
2317
+ else resolve(rows || []);
2318
+ }
2319
+ );
2320
+ });
2321
+
2322
+ console.log(`Children of ${TYPE_EMOJIS[parent.type] || '📦'} #${parent.id} "${parent.title}":`);
2323
+ console.log('');
2324
+
2325
+ if (children.length === 0) {
2326
+ console.log(' (no children)');
2327
+ } else {
2328
+ children.forEach(child => {
2329
+ const emoji = TYPE_EMOJIS[child.type] || '📦';
2330
+ const statusEmoji = STATUS_EMOJIS[child.status] || '';
2331
+ const modeStr = child.mode ? ` [${child.mode}]` : '';
2332
+ console.log(` ${emoji} #${child.id}: ${child.title}${modeStr} ${statusEmoji}`);
2333
+ });
2334
+ }
2335
+ console.log('');
2336
+ } catch (err) {
2337
+ console.error(`Error: ${err.message}`);
2338
+ process.exit(1);
2339
+ }
2340
+ break;
2341
+ }
2342
+
2343
+ default:
2344
+ console.log(`
2345
+ JettyPod Work Tracking
2346
+
2347
+ Commands:
2348
+ jettypod work create <type> <title> [desc] [--parent=ID]
2349
+ Types: epic, feature, bug, chore
2350
+
2351
+ jettypod backlog [filter]
2352
+ Show work items (active by default)
2353
+ Filters: all, completed
2354
+
2355
+ jettypod work show <id>
2356
+ Show work item details
2357
+
2358
+ jettypod work status <id> <status>
2359
+ Statuses: backlog, todo, in_progress, done, cancelled
2360
+
2361
+ jettypod work set-branch <id> <branch-name>
2362
+ Set branch for work item
2363
+
2364
+ jettypod work set-mode <id> <mode>
2365
+ Set mode (speed/discovery/stable/production)
2366
+
2367
+ jettypod work current <id>
2368
+ Set as current work item
2369
+
2370
+ jettypod work epic <id>
2371
+ Show epic overview
2372
+
2373
+ Examples:
2374
+ jettypod work create epic "Q1 Roadmap"
2375
+ jettypod work create feature "Auth" "" --parent=1
2376
+ jettypod work set-branch 2 feature/auth
2377
+ jettypod work set-mode 2 speed
2378
+ jettypod work current 2
2379
+ jettypod work show 2
2380
+ `);
2381
+ }
2382
+
2383
+ // Don't explicitly close DB in test environments - let Node.js handle cleanup
2384
+ // This prevents FATAL errors when async operations are still pending
2385
+ if (process.env.NODE_ENV !== 'test') {
2386
+ await closeDb();
2387
+ }
2388
+ }
2389
+
2390
+ // Export for use in jettypod.js and other modules
2391
+ module.exports = { main, getCurrentWork, updateStatus, create };
2392
+
2393
+ // Run if called directly
2394
+ if (require.main === module) {
2395
+ main();
2396
+ }