jettypod 4.1.2 → 4.1.4

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.
Files changed (179) hide show
  1. package/.nvmrc +1 -0
  2. package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
  3. package/docs/DECISIONS.md +10 -12
  4. package/docs/NODE_VERSION.md +83 -0
  5. package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
  6. package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
  7. package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
  8. package/hooks/post-checkout +17 -0
  9. package/hooks/post-merge +17 -0
  10. package/hooks/pre-commit +30 -0
  11. package/jettypod.js +259 -120
  12. package/lib/coverage-tracker.js +218 -0
  13. package/lib/database.js +2 -0
  14. package/lib/db-export.js +192 -0
  15. package/lib/db-import.js +193 -0
  16. package/lib/external-transition-handler.js +32 -0
  17. package/lib/git-hook-helpers.js +174 -0
  18. package/lib/git-root.js +90 -0
  19. package/lib/infrastructure-chore-generator.js +45 -0
  20. package/lib/install-hooks.js +52 -0
  21. package/lib/jettypod-backup.js +238 -0
  22. package/lib/merge-lock.js +193 -0
  23. package/lib/migrations/012-add-worktree-path.js +38 -0
  24. package/lib/migrations/013-worktrees-table.js +86 -0
  25. package/lib/migrations/014-migrate-worktree-data.js +161 -0
  26. package/lib/migrations/015-merge-locks-table.js +67 -0
  27. package/lib/pattern-finder.js +152 -0
  28. package/lib/process-manager.js +140 -0
  29. package/lib/production-standards-reader.js +13 -2
  30. package/lib/production-standards-writer.js +85 -0
  31. package/lib/skills/feature-planning/dry-run-validator.js +135 -0
  32. package/lib/skills/feature-planning/validation-formatter.js +160 -0
  33. package/lib/smart-conflict-detection.js +168 -0
  34. package/lib/smart-fetch-rebase.js +614 -0
  35. package/lib/step-definition-parser.js +76 -0
  36. package/lib/unit-test-generator.js +232 -0
  37. package/lib/verification-command-generator.js +66 -0
  38. package/lib/worktree-diagnostics.js +413 -0
  39. package/lib/worktree-facade.js +174 -0
  40. package/lib/worktree-manager.js +636 -0
  41. package/lib/worktree-reconciler.js +429 -0
  42. package/package.json +30 -3
  43. package/skills-templates/external-transition/SKILL.md +34 -3
  44. package/skills-templates/feature-planning/SKILL.md +190 -24
  45. package/skills-templates/production-mode/SKILL.md +127 -9
  46. package/skills-templates/speed-mode/SKILL.md +454 -51
  47. package/skills-templates/stable-mode/SKILL.md +285 -76
  48. package/.claude/PROTECT_SKILLS.md +0 -28
  49. package/.claude/settings.json +0 -24
  50. package/.claude/settings.local.json +0 -16
  51. package/.claude/skills/epic-planning/SKILL.md +0 -297
  52. package/.claude/skills/external-transition/SKILL.md +0 -384
  53. package/.claude/skills/feature-planning/SKILL.md +0 -464
  54. package/.claude/skills/production-mode/SKILL.md +0 -369
  55. package/.claude/skills/speed-mode/SKILL.md +0 -481
  56. package/.claude/skills/stable-mode/SKILL.md +0 -713
  57. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
  58. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
  59. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
  60. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
  61. package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
  62. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
  63. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
  64. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
  65. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
  66. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
  67. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
  68. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
  69. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
  70. package/.devpod/current-work.json +0 -10
  71. package/.devpod/work.db +0 -0
  72. package/.github/workflows/test-safety.yml +0 -85
  73. package/.jettypod/config.json +0 -5
  74. package/.jettypod/current-work.json +0 -10
  75. package/.jettypod/hooks/README.md +0 -77
  76. package/.jettypod/hooks/protect-claude-md.js +0 -338
  77. package/.jettypod/test-work.db +0 -0
  78. package/.jettypod/work.db +0 -0
  79. package/CLAUDE.md +0 -49
  80. package/SPEED-STABLE-AUDIT.md +0 -853
  81. package/SYSTEM-BEHAVIOR.md +0 -2199
  82. package/TEST_SAFETY_AUDIT.md +0 -314
  83. package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
  84. package/cucumber-report.html +0 -45
  85. package/dist/devpod-linux +0 -0
  86. package/dist/devpod-macos +0 -0
  87. package/dist/devpod-win.exe +0 -0
  88. package/docs/features/jettypod-standards-explained.md +0 -543
  89. package/docs/features/standards-inventory.md +0 -257
  90. package/features/auto-generate-production-chores.feature +0 -13
  91. package/features/backlog-command.feature +0 -26
  92. package/features/backlog-filtering-production.feature +0 -10
  93. package/features/claude-md-protection/steps.js +0 -498
  94. package/features/decisions/index.js +0 -490
  95. package/features/decisions/index.test.js +0 -208
  96. package/features/fix-text-wrapping.feature +0 -42
  97. package/features/git-hooks/git-hooks.feature +0 -30
  98. package/features/git-hooks/index.js +0 -93
  99. package/features/git-hooks/index.test.js +0 -137
  100. package/features/git-hooks/post-commit +0 -56
  101. package/features/git-hooks/post-merge +0 -47
  102. package/features/git-hooks/pre-commit +0 -28
  103. package/features/git-hooks/simple-steps.js +0 -53
  104. package/features/git-hooks/simple-test.feature +0 -10
  105. package/features/git-hooks/steps.js +0 -196
  106. package/features/jettypod-update-command.feature +0 -46
  107. package/features/mode-prompts/index.js +0 -95
  108. package/features/mode-prompts/simple-steps.js +0 -44
  109. package/features/mode-prompts/simple-test.feature +0 -9
  110. package/features/mode-prompts/validation.test.js +0 -120
  111. package/features/multiple-claude-instances.feature +0 -121
  112. package/features/production-mode-skill.feature +0 -121
  113. package/features/refactor-mode/steps.js +0 -217
  114. package/features/refactor-mode.feature +0 -49
  115. package/features/simplify-external-transition.feature +0 -166
  116. package/features/skills-update/index.test.js +0 -216
  117. package/features/step_definitions/backlog-command.steps.js +0 -37
  118. package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
  119. package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
  120. package/features/step_definitions/production-mode-skill.steps.js +0 -862
  121. package/features/step_definitions/simplify-external-transition.steps.js +0 -370
  122. package/features/step_definitions/terminal-logo.steps.js +0 -145
  123. package/features/step_definitions/update-command.steps.js +0 -183
  124. package/features/support/hooks.js +0 -9
  125. package/features/terminal-logo/index.js +0 -39
  126. package/features/terminal-logo/terminal-logo.feature +0 -30
  127. package/features/update-command/index.js +0 -181
  128. package/features/update-command/index.test.js +0 -225
  129. package/features/work-commands/bug-workflow-display.feature +0 -22
  130. package/features/work-commands/index.js +0 -498
  131. package/features/work-commands/simple-steps.js +0 -69
  132. package/features/work-commands/stable-tests.feature +0 -57
  133. package/features/work-commands/steps.js +0 -1174
  134. package/features/work-commands/validation.test.js +0 -88
  135. package/features/work-commands/work-commands.feature +0 -13
  136. package/features/work-tracking/discovery-validation.test.js +0 -228
  137. package/features/work-tracking/index.js +0 -1921
  138. package/features/work-tracking/mode-required.feature +0 -112
  139. package/features/work-tracking/phase-tracking.test.js +0 -482
  140. package/features/work-tracking/prototype-tracking.test.js +0 -485
  141. package/features/work-tracking/tree-view.test.js +0 -310
  142. package/features/work-tracking/work-set-mode.feature +0 -71
  143. package/features/work-tracking/work-start-mode.feature +0 -88
  144. package/full-test.txt +0 -0
  145. package/lib/bug-workflow.test.js +0 -177
  146. package/lib/claudemd.test.js +0 -195
  147. package/lib/config.test.js +0 -511
  148. package/lib/constants.test.js +0 -164
  149. package/lib/current-work.test.js +0 -146
  150. package/lib/database-project-config.test.js +0 -111
  151. package/lib/database.test.js +0 -106
  152. package/lib/decisions-generator.test.js +0 -457
  153. package/lib/decisions-helpers.test.js +0 -310
  154. package/lib/git-coordinator.js +0 -167
  155. package/lib/git.test.js +0 -145
  156. package/lib/migrations/002-default-work-item-modes.test.js +0 -351
  157. package/lib/production-chore-generator.test.js +0 -432
  158. package/lib/production-context-detector.test.js +0 -277
  159. package/lib/production-scenario-appender.test.js +0 -235
  160. package/lib/production-scenario-validator.test.js +0 -246
  161. package/lib/production-standards-reader.test.js +0 -270
  162. package/lib/project-state.test.js +0 -92
  163. package/lib/push-queue.js +0 -417
  164. package/lib/queue-processor.js +0 -74
  165. package/lib/test-helpers.js +0 -202
  166. package/lib/test-helpers.test.js +0 -255
  167. package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
  168. package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
  169. package/prototypes/2025-01-11-production-mode-guided.js +0 -217
  170. package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
  171. package/prototypes/2025-01-11-production-standards-example.md +0 -204
  172. package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
  173. package/prototypes/test/index.html +0 -1
  174. package/setup-dist-repo.sh +0 -68
  175. package/test-production-standards-engine.js +0 -130
  176. package/test-results.json +0 -2195
  177. package/test-safety-check.sh +0 -80
  178. package/work-item-tracking-plan.md +0 -199
  179. /package/{.jettypod/devpod.db → jettypod.db} +0 -0
@@ -1,1921 +0,0 @@
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
- mode = null; // Explicitly set to null for chores
74
- }
75
-
76
- // Only features have modes - they start with mode=NULL (no mode until implementation starts)
77
- // Epics, chores, and bugs don't have modes (always NULL)
78
- if (type === 'feature' && !mode) {
79
- mode = null;
80
- }
81
-
82
- // Bugs should never have modes - always NULL
83
- if (type === 'bug') {
84
- if (mode) {
85
- return reject(new Error('Bugs cannot have modes. Only features have modes (speed/stable/production).'));
86
- }
87
- mode = null;
88
- }
89
-
90
- // Validate mode value if provided (only for features)
91
- if (mode && type === 'feature') {
92
- const validModes = ['speed', 'stable', 'production'];
93
- if (!validModes.includes(mode)) {
94
- return reject(new Error(`Invalid mode: ${mode}. Must be one of: ${validModes.join(', ')}`));
95
- }
96
- }
97
-
98
- // Set phase for features (discovery when mode=NULL, implementation when mode is set, NULL for everything else)
99
- const phase = type === 'feature' ? (mode ? 'implementation' : 'discovery') : null;
100
-
101
- const sql = `INSERT INTO work_items (type, title, description, parent_id, mode, needs_discovery, phase) VALUES (?, ?, ?, ?, ?, ?, ?)`;
102
- db.run(sql, [type, title, description, parentId, mode, needsDiscovery ? 1 : 0, phase], function(err) {
103
- if (err) {
104
- return reject(err);
105
- }
106
- const newId = this.lastID;
107
- const discoveryIndicator = needsDiscovery ? ' (needs discovery)' : '';
108
- console.log(`Created ${type} #${newId}: ${title}${discoveryIndicator}`);
109
- resolve(newId);
110
- });
111
- }
112
- });
113
- }
114
-
115
- /**
116
- * Determine if an item should be hidden in internal projects
117
- * Rules:
118
- * - Features with mode='production' are hidden
119
- * - Chores whose parent feature has mode='production' are hidden (handled by tree filtering)
120
- * - Epics are never hidden (they're containers)
121
- *
122
- * @param {Object} item - Work item to check
123
- * @returns {boolean} True if item should be hidden
124
- */
125
- function isProductionModeItem(item) {
126
- // Validate input
127
- if (!item || typeof item !== 'object') {
128
- return false;
129
- }
130
-
131
- // Only features can have production mode
132
- if (item.type === 'feature' && item.mode === 'production') {
133
- return true;
134
- }
135
-
136
- // Epics are containers - never hide them
137
- // Chores don't have modes - their visibility is determined by their parent
138
- return false;
139
- }
140
-
141
- /**
142
- * Recursively filter out production mode features and their children
143
- * Smart filtering: only removes items if they're production mode features
144
- * Maintains tree integrity: removes entire branches when feature is production
145
- *
146
- * @param {Array} items - Items to filter
147
- * @param {Object} itemsById - Lookup map of all items
148
- * @returns {Array} Filtered items
149
- */
150
- function filterProductionItems(items, itemsById) {
151
- // Validate input
152
- if (!Array.isArray(items)) {
153
- return [];
154
- }
155
-
156
- return items.filter(item => {
157
- // Skip invalid items
158
- if (!item || typeof item !== 'object') {
159
- return false;
160
- }
161
-
162
- // Check if this item should be hidden
163
- const shouldHide = isProductionModeItem(item);
164
-
165
- if (shouldHide) {
166
- return false; // Hide this item and all its children
167
- }
168
-
169
- // Item is not production - keep it and recursively filter its children
170
- if (item.children && Array.isArray(item.children) && item.children.length > 0) {
171
- item.children = filterProductionItems(item.children, itemsById);
172
- }
173
-
174
- return true;
175
- });
176
- }
177
-
178
- /**
179
- * Get all work items as hierarchical tree structure
180
- * @param {boolean} includeCompleted - Include done/cancelled items (default: false)
181
- * @param {boolean} showAll - Show all items including production (bypasses internal/external filtering)
182
- * @returns {Promise<Array>} Root work items with nested children
183
- * @throws {Error} If database query fails
184
- */
185
- function getTree(includeCompleted = false, showAll = false) {
186
- return new Promise((resolve, reject) => {
187
- const whereClause = includeCompleted ? '' : "WHERE status NOT IN ('done', 'cancelled')";
188
- db.all(`SELECT * FROM work_items ${whereClause} ORDER BY parent_id, id`, [], (err, rows) => {
189
- if (err) {
190
- return reject(new Error(`Failed to fetch work items: ${err.message}`));
191
- }
192
-
193
- if (!rows || rows.length === 0) {
194
- return resolve([]);
195
- }
196
-
197
- const itemsById = {};
198
- const rootItems = [];
199
-
200
- try {
201
- // Build lookup
202
- rows.forEach(item => {
203
- itemsById[item.id] = item;
204
- item.children = [];
205
- });
206
-
207
- // Build tree
208
- rows.forEach(item => {
209
- if (item.parent_id && itemsById[item.parent_id]) {
210
- itemsById[item.parent_id].children.push(item);
211
- } else if (!item.parent_id) {
212
- rootItems.push(item);
213
- }
214
- });
215
-
216
- // Apply production filtering if internal project and not showing all
217
- let filteredItems = rootItems;
218
- try {
219
- const config = require('../../lib/config');
220
- const projectConfig = config.read();
221
- const isInternal = projectConfig.project_state === 'internal';
222
-
223
- if (isInternal && !showAll) {
224
- filteredItems = filterProductionItems(rootItems, itemsById);
225
- }
226
- } catch (err) {
227
- // If config reading fails, don't filter (safer to show everything)
228
- // Log warning but continue
229
- console.warn(`Warning: Failed to read config for production filtering: ${err.message}`);
230
- }
231
-
232
- resolve(filteredItems);
233
- } catch (err) {
234
- reject(new Error(`Failed to build backlog: ${err.message}`));
235
- }
236
- });
237
- });
238
- }
239
-
240
- /**
241
- * Print work items tree to console with visual hierarchy
242
- * @param {Array} items - Work items to print
243
- * @param {string} prefix - Indentation prefix for nested items
244
- * @param {boolean} isRootLevel - Whether we're at the root level
245
- * @param {Set} expandedIds - Set of item IDs to show expanded (null = all collapsed, 'all' = all expanded)
246
- * @throws {Error} If items is not an array
247
- */
248
- function printTree(items, prefix = '', isRootLevel = true, expandedIds = null) {
249
- if (!Array.isArray(items)) {
250
- throw new Error('Items must be an array');
251
- }
252
-
253
- items.forEach((item, index) => {
254
- if (!item || typeof item !== 'object') {
255
- console.warn(`Skipping invalid item at index ${index}`);
256
- return;
257
- }
258
-
259
- const isLast = index === items.length - 1;
260
- const emoji = TYPE_EMOJIS[item.type] || '📋';
261
-
262
- // Show mode indicator only when mode is set
263
- let modeIndicator = '';
264
- if (item.mode) {
265
- modeIndicator = ` [${item.mode}]`;
266
- }
267
-
268
- // Determine if this item should be expanded
269
- const isExpanded = expandedIds === 'all' || (expandedIds && expandedIds.has(item.id));
270
- const hasChildren = item.children && item.children.length > 0;
271
- const expandIndicator = hasChildren ? (isExpanded ? ' ⊖' : ' ⊕') : '';
272
- const childCount = hasChildren ? ` (${item.children.length} ${item.type === 'epic' ? 'features' : item.type === 'feature' ? 'chores' : 'items'})` : '';
273
-
274
- // Root level items have no connectors
275
- if (isRootLevel) {
276
- const titlePrefix = `${emoji} [#${item.id}] `;
277
- const titleText = `${item.title}${modeIndicator}${childCount}${expandIndicator}`;
278
- // Calculate actual visual width of emoji + space to find bracket position
279
- const visualBracketPos = getVisualWidth(emoji + ' ');
280
- const termWidth = getTerminalWidth();
281
- const prefixVisualWidth = getVisualWidth(titlePrefix);
282
- const maxTextWidth = termWidth - prefixVisualWidth;
283
- const wrappedTitle = wrapText(titleText, maxTextWidth, 0);
284
-
285
- console.log(titlePrefix + wrappedTitle[0]);
286
- // Continuation lines align with the opening bracket
287
- const continuationPrefix = (hasChildren ? '│' : ' ') + ' '.repeat(visualBracketPos - 1);
288
- for (let i = 1; i < wrappedTitle.length; i++) {
289
- console.log(continuationPrefix + wrappedTitle[i]);
290
- }
291
-
292
- if (isExpanded && item.description) {
293
- const descLabel = 'Description: "';
294
- const descText = item.description + '"';
295
- const maxDescWidth = termWidth - getVisualWidth(continuationPrefix) - getVisualWidth(descLabel);
296
- const wrappedDesc = wrapText(descText, maxDescWidth, 0);
297
-
298
- console.log(continuationPrefix + descLabel + wrappedDesc[0]);
299
- for (let i = 1; i < wrappedDesc.length; i++) {
300
- console.log(continuationPrefix + wrappedDesc[i]);
301
- }
302
- }
303
- if (isExpanded && item.status && item.type !== 'epic') {
304
- console.log(`${continuationPrefix}Status: ${item.status}${item.mode ? ` | Mode: ${item.mode}` : ''}`);
305
- }
306
- } else {
307
- // Nested items get tree connectors
308
- const connector = isLast ? '└── ' : '├── ';
309
- const treePrefix = prefix + (isLast ? ' ' : '│ ');
310
- const titlePrefix = `${prefix}${connector}${emoji} [#${item.id}] `;
311
- const titleText = `${item.title}${modeIndicator}${childCount}${expandIndicator}`;
312
- const termWidth = getTerminalWidth();
313
-
314
- // Calculate actual visual width to find bracket position
315
- const visualBracketPos = getVisualWidth(`${prefix}${connector}${emoji} `);
316
- const prefixVisualWidth = getVisualWidth(titlePrefix);
317
- const maxTextWidth = termWidth - prefixVisualWidth;
318
- const wrappedTitle = wrapText(titleText, maxTextWidth, 0);
319
-
320
- console.log(titlePrefix + wrappedTitle[0]);
321
- // Continuation lines align with the opening bracket
322
- const continuationPrefix = (treePrefix.substring(0, 1)) + ' '.repeat(visualBracketPos - 1);
323
- for (let i = 1; i < wrappedTitle.length; i++) {
324
- console.log(continuationPrefix + wrappedTitle[i]);
325
- }
326
-
327
- if (isExpanded && item.description) {
328
- const descLabel = 'Description: "';
329
- const descText = item.description + '"';
330
- const maxDescWidth = termWidth - getVisualWidth(continuationPrefix) - getVisualWidth(descLabel);
331
- const wrappedDesc = wrapText(descText, maxDescWidth, 0);
332
-
333
- console.log(continuationPrefix + descLabel + wrappedDesc[0]);
334
- for (let i = 1; i < wrappedDesc.length; i++) {
335
- console.log(continuationPrefix + wrappedDesc[i]);
336
- }
337
- }
338
- if (isExpanded && item.status && item.type !== 'epic') {
339
- console.log(`${continuationPrefix}Status: ${item.status}${item.mode ? ` | Mode: ${item.mode}` : ''}`);
340
- }
341
- }
342
-
343
- // Always show children - expansion only controls extra details
344
- if (hasChildren) {
345
- const newPrefix = isRootLevel ? '' : (prefix + (isLast ? ' ' : '│ '));
346
- printTree(item.children, newPrefix, false, expandedIds);
347
- }
348
- });
349
- }
350
-
351
- // Update status
352
- function updateStatus(id, status) {
353
- return new Promise((resolve, reject) => {
354
- // First get the work item to check its parent
355
- db.get('SELECT id, parent_id FROM work_items WHERE id = ?', [id], (err, item) => {
356
- if (err) {
357
- console.error(`Error: ${err.message}`);
358
- return reject(err);
359
- }
360
-
361
- if (!item) {
362
- console.log(`Work item #${id} not found`);
363
- return resolve();
364
- }
365
-
366
- // Update the status and completed_at if marking as done
367
- const completedAt = status === 'done' ? new Date().toISOString() : null;
368
- const sql = status === 'done'
369
- ? `UPDATE work_items SET status = ?, completed_at = ? WHERE id = ?`
370
- : `UPDATE work_items SET status = ? WHERE id = ?`;
371
- const params = status === 'done' ? [status, completedAt, id] : [status, id];
372
-
373
- db.run(sql, params, (err) => {
374
- if (err) {
375
- console.error(`Error: ${err.message}`);
376
- return reject(err);
377
- }
378
-
379
- console.log(`Updated #${id} status to ${status}`);
380
-
381
- // If status is 'done' and item has a parent, check if we should auto-close the parent epic
382
- if (status === 'done' && item.parent_id) {
383
- db.get('SELECT id, type FROM work_items WHERE id = ?', [item.parent_id], (err, parent) => {
384
- if (err || !parent) {
385
- return resolve();
386
- }
387
-
388
- // Only auto-close epics
389
- if (parent.type === 'epic') {
390
- // Check if all children of this epic are done
391
- db.all(
392
- 'SELECT id, status FROM work_items WHERE parent_id = ?',
393
- [parent.id],
394
- (err, children) => {
395
- if (err) {
396
- return resolve();
397
- }
398
-
399
- const allDone = children.every(child => child.status === 'done');
400
- if (allDone) {
401
- const epicCompletedAt = new Date().toISOString();
402
- db.run('UPDATE work_items SET status = ?, completed_at = ? WHERE id = ?', ['done', epicCompletedAt, parent.id], (err) => {
403
- if (err) {
404
- console.error(`Failed to auto-close epic: ${err.message}`);
405
- } else {
406
- console.log(`✓ Epic #${parent.id} also completed (all children done)`);
407
- }
408
- resolve();
409
- });
410
- } else {
411
- resolve();
412
- }
413
- }
414
- );
415
- } else {
416
- resolve();
417
- }
418
- });
419
- } else {
420
- resolve();
421
- }
422
- });
423
- });
424
- });
425
- }
426
-
427
- // Set branch
428
- function setBranch(id, branch) {
429
- db.run(`UPDATE work_items SET branch_name = ? WHERE id = ?`, [branch, id], () => {
430
- console.log(`Set #${id} branch to ${branch}`);
431
- });
432
- }
433
-
434
- // Set mode
435
- function setMode(id, mode) {
436
- return new Promise((resolve, reject) => {
437
- const { getCurrentWork } = require('../../lib/current-work');
438
- const { updateCurrentWork } = require('../../lib/claudemd');
439
-
440
- // Validate mode
441
- const validModes = ['speed', 'stable', 'production'];
442
- if (mode && !validModes.includes(mode)) {
443
- const error = new Error(`Invalid mode: ${mode}. Must be one of: ${validModes.join(', ')}`);
444
- console.error(`Error: ${error.message}`);
445
- reject(error);
446
- return;
447
- }
448
-
449
- // Check if work item is an epic - epics don't have modes
450
- db.get(`SELECT type FROM work_items WHERE id = ?`, [id], (err, row) => {
451
- if (err) {
452
- console.error(`Error: ${err.message}`);
453
- reject(err);
454
- return;
455
- }
456
-
457
- if (!row) {
458
- console.error(`Error: Work item #${id} not found`);
459
- resolve();
460
- return;
461
- }
462
-
463
- if (row.type === 'epic') {
464
- const error = new Error('Epics do not have modes. Only features have modes.');
465
- console.error(`Error: ${error.message}`);
466
- reject(error);
467
- return;
468
- }
469
-
470
- if (row.type === 'chore') {
471
- const error = new Error('Chores do not have modes. Chores inherit the workflow context from their parent feature.');
472
- console.error(`Error: ${error.message}`);
473
- reject(error);
474
- return;
475
- }
476
-
477
- if (row.type === 'bug') {
478
- const error = new Error('Bugs do not have modes. Only features have modes (speed/stable/production).');
479
- console.error(`Error: ${error.message}`);
480
- reject(error);
481
- return;
482
- }
483
-
484
- // Update database
485
- db.serialize(() => {
486
- db.run(`UPDATE work_items SET mode = ? WHERE id = ?`, [mode, id], (err) => {
487
- if (err) {
488
- console.error(`Error: ${err.message}`);
489
- reject(err);
490
- return;
491
- }
492
-
493
- console.log(`Set #${id} mode to ${mode}`);
494
-
495
- // If this is the current work item, update CLAUDE.md
496
- const currentWork = getCurrentWork();
497
- if (currentWork && currentWork.id === id) {
498
- // Get updated work item to pass to updateCurrentWork
499
- db.get(`
500
- SELECT w.*,
501
- p.title as parent_title, p.id as parent_id
502
- FROM work_items w
503
- LEFT JOIN work_items p ON w.parent_id = p.id
504
- WHERE w.id = ?
505
- `, [id], async (err, workItem) => {
506
- if (err || !workItem) {
507
- resolve();
508
- return;
509
- }
510
-
511
- // Add epic info
512
- const epic = await findEpic(workItem.id);
513
- workItem.epic_id = epic ? epic.id : null;
514
- workItem.epic_title = epic ? epic.title : null;
515
-
516
- // Update CLAUDE.md with new mode
517
- // Epics don't have mode lines in CLAUDE.md, pass null
518
- const modeForClaudeMd = workItem.type === 'epic' ? null : mode;
519
- updateCurrentWork(workItem, modeForClaudeMd);
520
- console.log('📝 CLAUDE.md updated');
521
- resolve();
522
- });
523
- } else {
524
- resolve();
525
- }
526
- });
527
- });
528
- }); // Close outer db.get for epic check
529
- });
530
- }
531
-
532
- // Set current work item
533
- function setCurrent(id) {
534
- const { setCurrentWork } = require('../../lib/current-work');
535
- const { updateCurrentWork } = require('../../lib/claudemd');
536
-
537
- db.run(`UPDATE work_items SET current = 0`, [], () => {
538
- db.get(`
539
- SELECT w.*,
540
- p.title as parent_title, p.id as parent_id, p.type as parent_type
541
- FROM work_items w
542
- LEFT JOIN work_items p ON w.parent_id = p.id
543
- WHERE w.id = ?
544
- `, [id], async (err, workItem) => {
545
- if (err || !workItem) {
546
- console.log(`Work item #${id} not found`);
547
- return;
548
- }
549
-
550
- // Find epic by traversing parent chain
551
- const epic = await findEpic(workItem.id);
552
-
553
- db.run(`UPDATE work_items SET current = 1 WHERE id = ?`, [id], () => {
554
- // Create current work file
555
- const currentWork = {
556
- id: workItem.id,
557
- title: workItem.title,
558
- type: workItem.type,
559
- status: workItem.status,
560
- mode: workItem.mode,
561
- parent_id: workItem.parent_id,
562
- parent_title: workItem.parent_title,
563
- parent_type: workItem.parent_type,
564
- epic_id: epic ? epic.id : null,
565
- epic_title: epic ? epic.title : null,
566
- description: workItem.description
567
- };
568
-
569
- setCurrentWork(currentWork);
570
- updateCurrentWork(currentWork, workItem.mode);
571
-
572
- console.log(`Set #${id} as current work`);
573
- });
574
- });
575
- });
576
- }
577
-
578
- // Re-export getCurrentWork from shared module for backwards compatibility
579
- // (used by jettypod.js)
580
-
581
- // Show single work item details
582
- function showItem(id) {
583
- return new Promise(async (resolve) => {
584
- db.get(`
585
- SELECT w.*,
586
- p.title as parent_title
587
- FROM work_items w
588
- LEFT JOIN work_items p ON w.parent_id = p.id
589
- WHERE w.id = ?
590
- `, [id], async (err, row) => {
591
- if (err || !row) {
592
- console.log(`Work item #${id} not found`);
593
- resolve(null);
594
- } else {
595
- const epic = await findEpic(row.id);
596
-
597
- console.log(`\n#${row.id} ${row.title}`);
598
- console.log(`Type: ${row.type}`);
599
- console.log(`Status: ${row.status}`);
600
- if (row.mode) console.log(`Mode: ${row.mode}`);
601
- if (row.branch_name) console.log(`Branch: ${row.branch_name}`);
602
- if (row.parent_title) console.log(`Parent: #${row.parent_id} ${row.parent_title}`);
603
- if (epic) console.log(`Epic: #${epic.id} ${epic.title}`);
604
-
605
- // Display prototype tracking information
606
- if (row.prototype_files) {
607
- try {
608
- const prototypeFiles = JSON.parse(row.prototype_files);
609
- if (prototypeFiles && prototypeFiles.length > 0) {
610
- console.log(`\n🔬 Prototypes: ${prototypeFiles.join(', ')}`);
611
- }
612
- } catch (e) {
613
- // Invalid JSON, skip
614
- }
615
- }
616
- if (row.discovery_winner) {
617
- console.log(`✅ Winner: ${row.discovery_winner}`);
618
- }
619
-
620
- // Display epic discovery information
621
- if (row.type === 'epic' && row.needs_discovery) {
622
- // Query all discovery decisions for this epic
623
- db.all(
624
- `SELECT * FROM discovery_decisions WHERE work_item_id = ? ORDER BY created_at`,
625
- [row.id],
626
- (err, decisions) => {
627
- if (err) {
628
- console.error(`Error fetching decisions: ${err.message}`);
629
- }
630
-
631
- if (decisions && decisions.length > 0) {
632
- console.log(`\n🏛 Discovery Decisions:`);
633
- decisions.forEach((d) => {
634
- console.log(`\n ${d.aspect}:`);
635
- console.log(` Decision: ${d.decision}`);
636
- console.log(` Rationale: ${d.rationale}`);
637
- });
638
- console.log(`\n 💡 Add more decisions: jettypod work epic-implement ${row.id} --aspect="<type>" --decision="<approach>" --rationale="<why>"`);
639
- } else {
640
- console.log(`\n⚠️ DISCOVERY REQUIRED: This epic needs architectural decisions`);
641
- console.log(``);
642
- console.log(` 💬 Talk to Claude Code: "Let's do epic discovery for #${row.id}"`);
643
- console.log(` Or run: jettypod work epic-planning ${row.id}`);
644
- console.log(``);
645
- console.log(` Claude Code will guide you through:`);
646
- console.log(` • Suggesting 3 architectural options`);
647
- console.log(` • Building prototypes`);
648
- console.log(` • Recording your decisions`);
649
- }
650
-
651
- if (row.description) console.log(`\nDescription: ${row.description}`);
652
- resolve(row);
653
- }
654
- );
655
- } else {
656
- if (row.description) console.log(`\nDescription: ${row.description}`);
657
- resolve(row);
658
- }
659
- }
660
- });
661
- });
662
- }
663
-
664
- // Epic overview
665
- function epicOverview(epicId) {
666
- return new Promise(async (resolve) => {
667
- db.all('SELECT * FROM work_items', [], async (err, rows) => {
668
- if (err) {
669
- console.error(err);
670
- return resolve();
671
- }
672
-
673
- // Filter items that belong to this epic
674
- const epicItems = [];
675
- for (const row of rows) {
676
- const epic = await findEpic(row.id);
677
- if ((epic && epic.id === epicId) || row.id === epicId) {
678
- epicItems.push(row);
679
- }
680
- }
681
-
682
- console.log(`\nEpic #${epicId} Overview:`);
683
-
684
- const byType = {};
685
- const byStatus = {};
686
-
687
- epicItems.forEach(row => {
688
- if (!byType[row.type]) byType[row.type] = 0;
689
- byType[row.type]++;
690
-
691
- if (!byStatus[row.status]) byStatus[row.status] = 0;
692
- byStatus[row.status]++;
693
- });
694
-
695
- console.log('\nBy Type:');
696
- Object.entries(byType).forEach(([type, count]) => {
697
- console.log(` ${type}: ${count}`);
698
- });
699
-
700
- console.log('\nBy Status:');
701
- Object.entries(byStatus).forEach(([status, count]) => {
702
- console.log(` ${status}: ${count}`);
703
- });
704
-
705
- resolve();
706
- });
707
- });
708
- }
709
-
710
- // Main CLI handler
711
- async function main() {
712
- const command = process.argv[2];
713
- const args = process.argv.slice(3);
714
-
715
- switch(command) {
716
- case 'create': {
717
- const type = args[0];
718
- const title = args[1];
719
- const desc = args[2] || '';
720
-
721
- let parentId = null;
722
- let mode = null;
723
- let needsDiscovery = false;
724
-
725
- args.forEach(arg => {
726
- if (arg.startsWith('--parent=')) {
727
- parentId = parseInt(arg.split('=')[1]);
728
- }
729
- if (arg.startsWith('--mode=')) {
730
- mode = arg.split('=')[1];
731
- }
732
- if (arg === '--needs-discovery') {
733
- needsDiscovery = true;
734
- }
735
- });
736
-
737
- try {
738
- const newId = await create(type, title, desc, parentId, mode, needsDiscovery);
739
-
740
- // Prompt for epic discovery after epic creation
741
- if (type === 'epic') {
742
- console.log('');
743
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
744
- console.log('🎯 Plan this epic now?');
745
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
746
- console.log('');
747
- console.log('Ask Claude Code:');
748
- console.log(` "Help me plan epic #${newId}"`);
749
- console.log('');
750
- console.log('Claude will help you:');
751
- console.log(' • Brainstorm features for this epic');
752
- console.log(' • Identify architectural decisions (if needed)');
753
- console.log(' • Create features automatically');
754
- console.log('');
755
- console.log('Or run: jettypod work epic-planning ' + newId);
756
- console.log('');
757
- console.log('💡 You can also plan later when ready');
758
- }
759
-
760
- // Check if creating feature under an unplanned epic
761
- if (type === 'feature' && parentId) {
762
- db.get(`SELECT * FROM work_items WHERE id = ?`, [parentId], (err, parent) => {
763
- if (!err && parent && parent.type === 'epic') {
764
- // Check if epic has any features already
765
- db.get(`SELECT COUNT(*) as count FROM work_items WHERE parent_id = ? AND type = 'feature'`, [parentId], (err, result) => {
766
- if (!err && result.count === 1) {
767
- // This is the first feature - suggest planning the epic
768
- console.log('');
769
- console.log('💡 Tip: Consider planning this epic first');
770
- console.log('');
771
- console.log('Ask Claude Code:');
772
- console.log(` "Help me plan epic #${parentId}"`);
773
- console.log('');
774
- console.log(`Or run: jettypod work epic-planning ${parentId}`);
775
- }
776
- });
777
- }
778
- });
779
- }
780
- } catch (err) {
781
- console.error(`Error: ${err.message}`);
782
- process.exit(1);
783
- }
784
- break;
785
- }
786
-
787
- case 'tree': {
788
- try {
789
- // Wait for migrations to complete before running queries
790
- await waitForMigrations();
791
-
792
- // Parse expand flags
793
- let expandedIds = null; // null = collapsed by default
794
- const expandArg = process.argv.find(arg => arg.startsWith('--expand'));
795
-
796
- if (expandArg === '--expand-all') {
797
- expandedIds = 'all';
798
- } else if (expandArg && expandArg.includes('=')) {
799
- const idsStr = expandArg.split('=')[1];
800
- const ids = idsStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
801
- expandedIds = new Set(ids);
802
- }
803
-
804
- const currentWork = getCurrentWork();
805
-
806
- // Show active work at top if exists
807
- if (currentWork) {
808
- const emoji = TYPE_EMOJIS[currentWork.type] || '📋';
809
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
810
- console.log('🎯 ACTIVE WORK');
811
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
812
- console.log(`${emoji} [#${currentWork.id}] ${currentWork.title}`);
813
- // Show epic if parent is the epic, otherwise show parent
814
- if (currentWork.epic_title && currentWork.epic_id !== currentWork.id && currentWork.parent_id === currentWork.epic_id) {
815
- console.log(`└─ Epic: 🎯 #${currentWork.epic_id} ${currentWork.epic_title}`);
816
- } else if (currentWork.parent_title) {
817
- const parentEmoji = TYPE_EMOJIS[currentWork.parent_type] || '📋';
818
- console.log(`└─ Part of: ${parentEmoji} #${currentWork.parent_id} ${currentWork.parent_title}`);
819
- } else if (currentWork.epic_title && currentWork.epic_id !== currentWork.id) {
820
- console.log(`└─ Epic: 🎯 #${currentWork.epic_id} ${currentWork.epic_title}`);
821
- }
822
- console.log('');
823
- }
824
-
825
- // Show recently completed items
826
- const recentlyCompleted = await new Promise((resolve, reject) => {
827
- db.all(`
828
- SELECT w.id, w.title, w.type, w.mode,
829
- p.title as parent_title, p.id as parent_id, p.type as parent_type
830
- FROM work_items w
831
- LEFT JOIN work_items p ON w.parent_id = p.id
832
- WHERE w.status = 'done'
833
- ORDER BY w.id DESC
834
- LIMIT 3
835
- `, [], async (err, rows) => {
836
- if (err) reject(err);
837
- else {
838
- // Add epic info to each item
839
- for (const row of rows || []) {
840
- const epic = await findEpic(row.id);
841
- row.epic_id = epic ? epic.id : null;
842
- row.epic_title = epic ? epic.title : null;
843
- }
844
- resolve(rows || []);
845
- }
846
- });
847
- });
848
-
849
- if (recentlyCompleted.length > 0) {
850
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
851
- console.log('✅ RECENTLY COMPLETED');
852
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
853
- recentlyCompleted.forEach(item => {
854
- const emoji = TYPE_EMOJIS[item.type] || '📋';
855
- let modeIndicator = '';
856
- if (item.mode) {
857
- modeIndicator = ` [${item.mode}]`;
858
- }
859
-
860
- // Wrap the title text properly
861
- const titlePrefix = `${emoji} [${item.id}] `;
862
- const titleText = `${item.title}${modeIndicator}`;
863
- const visualBracketPos = getVisualWidth(emoji + ' ');
864
- const termWidth = getTerminalWidth();
865
- const prefixVisualWidth = getVisualWidth(titlePrefix);
866
- const maxTextWidth = termWidth - prefixVisualWidth;
867
- const wrappedTitle = wrapText(titleText, maxTextWidth, 0);
868
-
869
- console.log(titlePrefix + wrappedTitle[0]);
870
- // Continuation lines align with the opening bracket
871
- const continuationPrefix = ' '.repeat(visualBracketPos);
872
- for (let i = 1; i < wrappedTitle.length; i++) {
873
- console.log(continuationPrefix + wrappedTitle[i]);
874
- }
875
-
876
- // Wrap parent/epic lines too
877
- if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
878
- const parentPrefix = ' └─ ';
879
- const parentText = `Epic: 🎯 #${item.epic_id} ${item.epic_title}`;
880
- const parentPrefixWidth = getVisualWidth(parentPrefix);
881
- const maxParentWidth = termWidth - parentPrefixWidth;
882
- const wrappedParent = wrapText(parentText, maxParentWidth, 0);
883
- console.log(parentPrefix + wrappedParent[0]);
884
- const parentContinuation = ' ';
885
- for (let i = 1; i < wrappedParent.length; i++) {
886
- console.log(parentContinuation + wrappedParent[i]);
887
- }
888
- } else if (item.parent_title) {
889
- const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
890
- const parentPrefix = ' └─ ';
891
- const parentText = `Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`;
892
- const parentPrefixWidth = getVisualWidth(parentPrefix);
893
- const maxParentWidth = termWidth - parentPrefixWidth;
894
- const wrappedParent = wrapText(parentText, maxParentWidth, 0);
895
- console.log(parentPrefix + wrappedParent[0]);
896
- const parentContinuation = ' ';
897
- for (let i = 1; i < wrappedParent.length; i++) {
898
- console.log(parentContinuation + wrappedParent[i]);
899
- }
900
- } else if (item.epic_title && item.epic_id !== item.id) {
901
- const parentPrefix = ' └─ ';
902
- const parentText = `Epic: 🎯 #${item.epic_id} ${item.epic_title}`;
903
- const parentPrefixWidth = getVisualWidth(parentPrefix);
904
- const maxParentWidth = termWidth - parentPrefixWidth;
905
- const wrappedParent = wrapText(parentText, maxParentWidth, 0);
906
- console.log(parentPrefix + wrappedParent[0]);
907
- const parentContinuation = ' ';
908
- for (let i = 1; i < wrappedParent.length; i++) {
909
- console.log(parentContinuation + wrappedParent[i]);
910
- }
911
- }
912
- });
913
- console.log('');
914
- }
915
-
916
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
917
- console.log('📋 BACKLOG');
918
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
919
- const items = await getTree();
920
-
921
- // Default: expand all epics unless user specified expand flags
922
- if (expandedIds === null) {
923
- expandedIds = new Set();
924
- items.forEach(item => {
925
- if (item.type === 'epic') {
926
- expandedIds.add(item.id);
927
- }
928
- });
929
- }
930
-
931
- printTree(items, '', true, expandedIds);
932
-
933
- // Show legend and commands
934
- console.log('');
935
- console.log('Legend: ⊕ = collapsed ⊖ = expanded');
936
- console.log('');
937
- console.log('Commands:');
938
- console.log(' jettypod backlog --expand=1 Show details for item #1');
939
- console.log(' jettypod backlog --expand=1,2,3 Show details for multiple items');
940
- console.log(' jettypod backlog --expand-all Show all details');
941
- console.log(' jettypod backlog Collapse all (default)');
942
- console.log('');
943
- } catch (err) {
944
- console.error(`Error displaying backlog: ${err.message}`);
945
- process.exit(1);
946
- }
947
- break;
948
- }
949
-
950
- case 'backlog': {
951
- try {
952
- // Wait for migrations to complete before running queries
953
- await waitForMigrations();
954
-
955
- const filter = args[0]; // undefined, 'all', or 'completed'
956
-
957
- // Check for --all flag (bypasses production filtering)
958
- const showAll = args.includes('--all');
959
-
960
- // For 'all' and 'completed' filters, use old simple display
961
- if (filter === 'all' || filter === 'completed') {
962
- let items;
963
- if (filter === 'all') {
964
- items = await getTree(true, true); // includeCompleted=true, showAll=true
965
- } else {
966
- items = await new Promise((resolve, reject) => {
967
- db.all(`SELECT * FROM work_items WHERE status IN ('done', 'cancelled') ORDER BY parent_id, id`, [], (err, rows) => {
968
- if (err) {
969
- return reject(new Error(`Failed to fetch completed items: ${err.message}`));
970
- }
971
-
972
- if (!rows || rows.length === 0) {
973
- return resolve([]);
974
- }
975
-
976
- const itemsById = {};
977
- const rootItems = [];
978
-
979
- rows.forEach(item => {
980
- itemsById[item.id] = item;
981
- item.children = [];
982
- });
983
-
984
- rows.forEach(item => {
985
- if (item.parent_id && itemsById[item.parent_id]) {
986
- itemsById[item.parent_id].children.push(item);
987
- } else if (!item.parent_id) {
988
- rootItems.push(item);
989
- }
990
- });
991
-
992
- resolve(rootItems);
993
- });
994
- });
995
- }
996
-
997
- if (items.length === 0) {
998
- console.log('No items found.');
999
- } else {
1000
- const expandedIds = new Set();
1001
- items.forEach(item => {
1002
- if (item.type === 'epic') {
1003
- expandedIds.add(item.id);
1004
- }
1005
- });
1006
- printTree(items, '', true, expandedIds);
1007
- }
1008
- console.log('');
1009
- } else {
1010
- // Default: show three-section view (active work, recently completed, backlog)
1011
-
1012
- // Parse expand flags
1013
- let expandedIds = null; // null = collapsed by default
1014
- const expandArg = process.argv.find(arg => arg.startsWith('--expand'));
1015
-
1016
- if (expandArg === '--expand-all') {
1017
- expandedIds = 'all';
1018
- } else if (expandArg && expandArg.includes('=')) {
1019
- const idsStr = expandArg.split('=')[1];
1020
- const ids = idsStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
1021
- expandedIds = new Set(ids);
1022
- }
1023
-
1024
- const currentWork = getCurrentWork();
1025
-
1026
- // Show active work at top if exists
1027
- if (currentWork) {
1028
- const emoji = TYPE_EMOJIS[currentWork.type] || '📋';
1029
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1030
- console.log('🎯 ACTIVE WORK');
1031
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1032
- console.log(`${emoji} [#${currentWork.id}] ${currentWork.title}`);
1033
- if (currentWork.epic_title && currentWork.epic_id !== currentWork.id && currentWork.parent_id === currentWork.epic_id) {
1034
- console.log(`└─ Epic: 🎯 #${currentWork.epic_id} ${currentWork.epic_title}`);
1035
- } else if (currentWork.parent_title) {
1036
- const parentEmoji = TYPE_EMOJIS[currentWork.parent_type] || '📋';
1037
- console.log(`└─ Part of: ${parentEmoji} #${currentWork.parent_id} ${currentWork.parent_title}`);
1038
- } else if (currentWork.epic_title && currentWork.epic_id !== currentWork.id) {
1039
- console.log(`└─ Epic: 🎯 #${currentWork.epic_id} ${currentWork.epic_title}`);
1040
- }
1041
- console.log('');
1042
- }
1043
-
1044
- // Show recently completed items
1045
- const recentlyCompleted = await new Promise((resolve, reject) => {
1046
- db.all(`
1047
- SELECT w.id, w.title, w.type, w.mode,
1048
- p.title as parent_title, p.id as parent_id, p.type as parent_type
1049
- FROM work_items w
1050
- LEFT JOIN work_items p ON w.parent_id = p.id
1051
- WHERE w.status = 'done'
1052
- ORDER BY w.completed_at DESC, w.id DESC
1053
- LIMIT 3
1054
- `, [], async (err, rows) => {
1055
- if (err) reject(err);
1056
- else {
1057
- // Add epic info to each item
1058
- for (const row of rows || []) {
1059
- const epic = await findEpic(row.id);
1060
- if (epic) {
1061
- row.epic_id = epic.id;
1062
- row.epic_title = epic.title;
1063
- }
1064
- }
1065
- resolve(rows || []);
1066
- }
1067
- });
1068
- });
1069
-
1070
- if (recentlyCompleted.length > 0) {
1071
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1072
- console.log('✅ RECENTLY COMPLETED');
1073
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1074
- recentlyCompleted.forEach(item => {
1075
- const emoji = TYPE_EMOJIS[item.type] || '📋';
1076
- let modeIndicator = '';
1077
- if (item.mode) {
1078
- modeIndicator = ` [${item.mode}]`;
1079
- }
1080
-
1081
- // Wrap the title text properly
1082
- const titlePrefix = `${emoji} [${item.id}] `;
1083
- const titleText = `${item.title}${modeIndicator}`;
1084
- const visualBracketPos = getVisualWidth(emoji + ' ');
1085
- const termWidth = getTerminalWidth();
1086
- const prefixVisualWidth = getVisualWidth(titlePrefix);
1087
- const maxTextWidth = termWidth - prefixVisualWidth;
1088
- const wrappedTitle = wrapText(titleText, maxTextWidth, 0);
1089
-
1090
- console.log(titlePrefix + wrappedTitle[0]);
1091
- // Continuation lines align with the opening bracket
1092
- const continuationPrefix = ' '.repeat(visualBracketPos);
1093
- for (let i = 1; i < wrappedTitle.length; i++) {
1094
- console.log(continuationPrefix + wrappedTitle[i]);
1095
- }
1096
-
1097
- // Wrap parent/epic lines too
1098
- if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1099
- const parentPrefix = ' └─ ';
1100
- const parentText = `Epic: 🎯 #${item.epic_id} ${item.epic_title}`;
1101
- const parentPrefixWidth = getVisualWidth(parentPrefix);
1102
- const maxParentWidth = termWidth - parentPrefixWidth;
1103
- const wrappedParent = wrapText(parentText, maxParentWidth, 0);
1104
- console.log(parentPrefix + wrappedParent[0]);
1105
- const parentContinuation = ' ';
1106
- for (let i = 1; i < wrappedParent.length; i++) {
1107
- console.log(parentContinuation + wrappedParent[i]);
1108
- }
1109
- } else if (item.parent_title) {
1110
- const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1111
- const parentPrefix = ' └─ ';
1112
- const parentText = `Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`;
1113
- const parentPrefixWidth = getVisualWidth(parentPrefix);
1114
- const maxParentWidth = termWidth - parentPrefixWidth;
1115
- const wrappedParent = wrapText(parentText, maxParentWidth, 0);
1116
- console.log(parentPrefix + wrappedParent[0]);
1117
- const parentContinuation = ' ';
1118
- for (let i = 1; i < wrappedParent.length; i++) {
1119
- console.log(parentContinuation + wrappedParent[i]);
1120
- }
1121
- } else if (item.epic_title && item.epic_id !== item.id) {
1122
- const parentPrefix = ' └─ ';
1123
- const parentText = `Epic: 🎯 #${item.epic_id} ${item.epic_title}`;
1124
- const parentPrefixWidth = getVisualWidth(parentPrefix);
1125
- const maxParentWidth = termWidth - parentPrefixWidth;
1126
- const wrappedParent = wrapText(parentText, maxParentWidth, 0);
1127
- console.log(parentPrefix + wrappedParent[0]);
1128
- const parentContinuation = ' ';
1129
- for (let i = 1; i < wrappedParent.length; i++) {
1130
- console.log(parentContinuation + wrappedParent[i]);
1131
- }
1132
- }
1133
- });
1134
- console.log('');
1135
- }
1136
-
1137
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1138
- console.log('📋 BACKLOG');
1139
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1140
- const items = await getTree(false, showAll);
1141
-
1142
- printTree(items, '', true, expandedIds);
1143
-
1144
- // Show legend and commands
1145
- console.log('');
1146
- console.log('Legend: ⊕ = collapsed ⊖ = expanded');
1147
- console.log('');
1148
- console.log('Commands:');
1149
- console.log(' jettypod backlog --expand=1 Show details for item #1');
1150
- console.log(' jettypod backlog --expand=1,2,3 Show details for multiple items');
1151
- console.log(' jettypod backlog --expand-all Show all details');
1152
- console.log(' jettypod backlog Collapse all (default)');
1153
- console.log('');
1154
- }
1155
- } catch (err) {
1156
- console.error(`Error displaying backlog: ${err.message}`);
1157
- process.exit(1);
1158
- }
1159
- break;
1160
- }
1161
-
1162
- case 'status': {
1163
- const id = parseInt(args[0]);
1164
- const status = args[1];
1165
- try {
1166
- await updateStatus(id, status);
1167
- } catch (err) {
1168
- console.error(`Error: ${err.message}`);
1169
- process.exit(1);
1170
- }
1171
- break;
1172
- }
1173
-
1174
- case 'set-branch': {
1175
- const id = parseInt(args[0]);
1176
- const branch = args[1];
1177
- setBranch(id, branch);
1178
- break;
1179
- }
1180
-
1181
- case 'set-mode': {
1182
- const id = parseInt(args[0]);
1183
- const mode = args[1];
1184
- await setMode(id, mode);
1185
- break;
1186
- }
1187
-
1188
- case 'current': {
1189
- const id = parseInt(args[0]);
1190
- setCurrent(id);
1191
- break;
1192
- }
1193
-
1194
- case 'show': {
1195
- if (!args[0]) {
1196
- console.error('Error: Work item ID is required');
1197
- console.log('');
1198
- console.log('Usage: jettypod work show <id>');
1199
- console.log('');
1200
- console.log('Example:');
1201
- console.log(' jettypod work show 5');
1202
- console.log('');
1203
- console.log('💡 Tip: Use `jettypod backlog` to see all work items');
1204
- process.exit(1);
1205
- }
1206
-
1207
- const id = parseInt(args[0]);
1208
- if (isNaN(id)) {
1209
- console.error(`Error: Invalid work item ID: ${args[0]}`);
1210
- console.log('ID must be a number');
1211
- process.exit(1);
1212
- }
1213
-
1214
- await showItem(id);
1215
- break;
1216
- }
1217
-
1218
- case 'describe': {
1219
- const id = parseInt(args[0]);
1220
- const description = args.slice(1).join(' ');
1221
-
1222
- db.run(`UPDATE work_items SET description = ? WHERE id = ?`, [description, id], (err) => {
1223
- if (err) {
1224
- console.error(`Error: ${err.message}`);
1225
- process.exit(1);
1226
- }
1227
- console.log(`Updated #${id} description`);
1228
- });
1229
- break;
1230
- }
1231
-
1232
- case 'epic': {
1233
- const epicId = parseInt(args[0]);
1234
- await epicOverview(epicId);
1235
- break;
1236
- }
1237
-
1238
- case 'completed': {
1239
- db.all(`
1240
- SELECT w.*,
1241
- p.title as parent_title, p.type as parent_type
1242
- FROM work_items w
1243
- LEFT JOIN work_items p ON w.parent_id = p.id
1244
- WHERE w.status = 'done'
1245
- ORDER BY w.id DESC
1246
- `, [], async (err, rows) => {
1247
- if (err) {
1248
- console.error(`Error fetching completed items: ${err.message}`);
1249
- process.exit(1);
1250
- }
1251
-
1252
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1253
- console.log('✅ COMPLETED WORK');
1254
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1255
-
1256
- if (!rows || rows.length === 0) {
1257
- console.log('No completed work items');
1258
- console.log('');
1259
- } else {
1260
- // Add epic info to each item
1261
- for (const row of rows) {
1262
- const epic = await findEpic(row.id);
1263
- row.epic_id = epic ? epic.id : null;
1264
- row.epic_title = epic ? epic.title : null;
1265
- }
1266
-
1267
- // Group by mode
1268
- const byMode = {
1269
- speed: [],
1270
- stable: [],
1271
- production: [],
1272
- other: []
1273
- };
1274
-
1275
- rows.forEach(item => {
1276
- if (item.mode === 'speed') byMode.speed.push(item);
1277
- else if (item.mode === 'stable') byMode.stable.push(item);
1278
- else if (item.mode === 'production') byMode.production.push(item);
1279
- else byMode.other.push(item);
1280
- });
1281
-
1282
- // Display Speed Mode section
1283
- if (byMode.speed.length > 0) {
1284
- console.log('\n⚡ SPEED MODE');
1285
- byMode.speed.forEach(item => {
1286
- const emoji = TYPE_EMOJIS[item.type] || '📋';
1287
- console.log(`${emoji} [#${item.id}] ${item.title}`);
1288
- // Show epic if parent is the epic, otherwise show parent
1289
- if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1290
- console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1291
- } else if (item.parent_title) {
1292
- const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1293
- console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1294
- } else if (item.epic_title && item.epic_id !== item.id) {
1295
- console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1296
- }
1297
- });
1298
- }
1299
-
1300
- // Display Stable Mode section
1301
- if (byMode.stable.length > 0) {
1302
- console.log('\n🔒 STABLE MODE');
1303
- byMode.stable.forEach(item => {
1304
- const emoji = TYPE_EMOJIS[item.type] || '📋';
1305
- console.log(`${emoji} [#${item.id}] ${item.title}`);
1306
- // Show epic if parent is the epic, otherwise show parent
1307
- if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1308
- console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1309
- } else if (item.parent_title) {
1310
- const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1311
- console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1312
- } else if (item.epic_title && item.epic_id !== item.id) {
1313
- console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1314
- }
1315
- });
1316
- }
1317
-
1318
- // Display Production Mode section
1319
- if (byMode.production.length > 0) {
1320
- console.log('\n🚀 PRODUCTION MODE');
1321
- byMode.production.forEach(item => {
1322
- const emoji = TYPE_EMOJIS[item.type] || '📋';
1323
- console.log(`${emoji} [#${item.id}] ${item.title}`);
1324
- // Show epic if parent is the epic, otherwise show parent
1325
- if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1326
- console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1327
- } else if (item.parent_title) {
1328
- const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1329
- console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1330
- } else if (item.epic_title && item.epic_id !== item.id) {
1331
- console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1332
- }
1333
- });
1334
- }
1335
-
1336
- // Display Other/Untagged section
1337
- if (byMode.other.length > 0) {
1338
- console.log('\n📦 OTHER');
1339
- byMode.other.forEach(item => {
1340
- const emoji = TYPE_EMOJIS[item.type] || '📋';
1341
- const modeIndicator = item.mode ? ` [${item.mode}]` : '';
1342
- console.log(`${emoji} [#${item.id}] ${item.title}${modeIndicator}`);
1343
- // Show epic if parent is the epic, otherwise show parent
1344
- if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
1345
- console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1346
- } else if (item.parent_title) {
1347
- const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
1348
- console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
1349
- } else if (item.epic_title && item.epic_id !== item.id) {
1350
- console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
1351
- }
1352
- });
1353
- }
1354
-
1355
- console.log('');
1356
- }
1357
- });
1358
- break;
1359
- }
1360
-
1361
- case 'epic-planning': {
1362
- const epicId = parseInt(args[0]);
1363
-
1364
- if (!epicId || isNaN(epicId)) {
1365
- console.error('Error: Epic ID is required');
1366
- console.log('Usage: jettypod work epic-planning <epic-id>');
1367
- process.exit(1);
1368
- }
1369
-
1370
- db.get(`SELECT * FROM work_items WHERE id = ?`, [epicId], (err, epic) => {
1371
- if (err) {
1372
- console.error(`Error: ${err.message}`);
1373
- process.exit(1);
1374
- }
1375
-
1376
- if (!epic) {
1377
- console.error(`Error: Epic #${epicId} not found`);
1378
- process.exit(1);
1379
- }
1380
-
1381
- if (epic.type !== 'epic') {
1382
- console.error(`Error: Work item #${epicId} is not an epic (type: ${epic.type})`);
1383
- process.exit(1);
1384
- }
1385
-
1386
- if (!epic.needs_discovery) {
1387
- console.error(`Error: Epic #${epicId} does not need discovery (needs_discovery=false)`);
1388
- console.log('');
1389
- console.log('Create epics with --needs-discovery flag if architectural decisions needed:');
1390
- console.log(' jettypod work create epic "Epic Title" "Description" --needs-discovery');
1391
- process.exit(1);
1392
- }
1393
-
1394
- // Check if any decisions have been recorded
1395
- db.all(
1396
- `SELECT * FROM discovery_decisions WHERE work_item_id = ?`,
1397
- [epicId],
1398
- (err, decisions) => {
1399
- if (err) {
1400
- console.error(`Error: ${err.message}`);
1401
- process.exit(1);
1402
- }
1403
-
1404
- if (decisions && decisions.length > 0) {
1405
- console.log(`📋 Epic #${epicId} discovery decisions:`);
1406
- console.log('');
1407
- decisions.forEach((d) => {
1408
- console.log(` ${d.aspect}: ${d.decision}`);
1409
- console.log(` Rationale: ${d.rationale}`);
1410
- console.log('');
1411
- });
1412
- console.log('💡 You can add more decisions for different aspects:');
1413
- console.log(` jettypod work epic-implement ${epicId} --aspect="<type>" --decision="<approach>" --rationale="<why>"`);
1414
- return;
1415
- }
1416
-
1417
- console.log(`🔍 Starting epic discovery for #${epicId}: ${epic.title}`);
1418
- console.log('');
1419
- console.log(`Description: ${epic.description || 'No description'}`);
1420
- console.log('');
1421
- console.log('────────────────────────────────────────────────────');
1422
- console.log('Epic Discovery Context (for Claude Code):');
1423
- console.log('────────────────────────────────────────────────────');
1424
- console.log(`Epic ID: ${epicId}`);
1425
- console.log(`Title: ${epic.title}`);
1426
- console.log(`Description: ${epic.description || 'Not provided'}`);
1427
- console.log('Needs Discovery: true');
1428
- console.log('');
1429
- console.log('💬 Now ask Claude Code:');
1430
- console.log(` "Help me with epic discovery for #${epicId}"`);
1431
- console.log('');
1432
- console.log('Claude will use the epic-planning skill to guide you through:');
1433
- console.log(' 1. Feature brainstorming');
1434
- console.log(' 2. Architectural decisions (if needed)');
1435
- console.log(' 3. Prototype validation (optional)');
1436
- console.log(' 4. Feature creation');
1437
- console.log('');
1438
- console.log('📋 The skill is at: .claude/skills/epic-planning/SKILL.md');
1439
- }
1440
- );
1441
- });
1442
- break;
1443
- }
1444
-
1445
- case 'epic-implement': {
1446
- const epicId = parseInt(args[0]);
1447
-
1448
- if (!epicId || isNaN(epicId)) {
1449
- console.error('Error: Epic ID is required');
1450
- console.log('Usage: jettypod work epic-implement <epic-id> --aspect="<type>" --decision="<approach>" --rationale="<why>"');
1451
- process.exit(1);
1452
- }
1453
-
1454
- // Parse --aspect, --decision, --rationale, and --prototypes flags
1455
- const aspectIndex = args.findIndex(a => a.startsWith('--aspect='));
1456
- const decisionIndex = args.findIndex(a => a.startsWith('--decision='));
1457
- const rationaleIndex = args.findIndex(a => a.startsWith('--rationale='));
1458
- const prototypesIndex = args.findIndex(a => a.startsWith('--prototypes='));
1459
-
1460
- if (aspectIndex === -1 || decisionIndex === -1 || rationaleIndex === -1) {
1461
- console.error('Error: --aspect, --decision, and --rationale are all required');
1462
- console.log('');
1463
- console.log('Usage: jettypod work epic-implement <epic-id> --aspect="<type>" --decision="<approach>" --rationale="<why>" [--prototypes="file1,file2"]');
1464
- console.log('');
1465
- console.log('Example:');
1466
- console.log(' jettypod work epic-implement 5 \\');
1467
- console.log(' --aspect="Architecture" \\');
1468
- console.log(' --decision="WebSockets with Socket.io" \\');
1469
- console.log(' --rationale="Bidirectional real-time updates needed, Socket.io provides fallbacks" \\');
1470
- console.log(' --prototypes="prototypes/websockets.js,prototypes/sse.js"');
1471
- console.log('');
1472
- console.log('Common aspects: Architecture, Design Pattern, State Management, API Design, Testing Strategy');
1473
- process.exit(1);
1474
- }
1475
-
1476
- const aspect = args[aspectIndex].split('=')[1].replace(/^["']|["']$/g, '');
1477
- const decision = args[decisionIndex].split('=')[1].replace(/^["']|["']$/g, '');
1478
- const rationale = args[rationaleIndex].split('=').slice(1).join('=').replace(/^["']|["']$/g, '');
1479
- const prototypes = prototypesIndex !== -1
1480
- ? args[prototypesIndex].split('=')[1].replace(/^["']|["']$/g, '').split(',').map(p => p.trim())
1481
- : [];
1482
-
1483
- // Validate inputs
1484
- if (!aspect || !decision || !rationale) {
1485
- console.error('Error: --aspect, --decision, and --rationale must all have values');
1486
- process.exit(1);
1487
- }
1488
-
1489
- if (aspect.trim().length === 0 || rationale.trim().length === 0) {
1490
- console.error('Error: --aspect and --rationale cannot be empty or whitespace only');
1491
- console.log('');
1492
- console.log('Provide meaningful values for the decision aspect and rationale.');
1493
- process.exit(1);
1494
- }
1495
-
1496
- // Get and validate epic
1497
- db.get(`SELECT * FROM work_items WHERE id = ?`, [epicId], (err, epic) => {
1498
- if (err) {
1499
- console.error(`Error: ${err.message}`);
1500
- process.exit(1);
1501
- }
1502
-
1503
- if (!epic) {
1504
- console.error(`Error: Epic #${epicId} not found`);
1505
- process.exit(1);
1506
- }
1507
-
1508
- if (epic.type !== 'epic') {
1509
- console.error(`Error: Work item #${epicId} is not an epic (type: ${epic.type})`);
1510
- process.exit(1);
1511
- }
1512
-
1513
- if (!epic.needs_discovery) {
1514
- console.error(`Error: Epic #${epicId} does not need discovery (needs_discovery=false)`);
1515
- process.exit(1);
1516
- }
1517
-
1518
- // Check if this aspect already has a decision
1519
- db.get(
1520
- `SELECT * FROM discovery_decisions WHERE work_item_id = ? AND aspect = ?`,
1521
- [epicId, aspect],
1522
- (err, existingDecision) => {
1523
- if (err) {
1524
- console.error(`Error: ${err.message}`);
1525
- process.exit(1);
1526
- }
1527
-
1528
- if (existingDecision) {
1529
- console.error(`Error: Epic #${epicId} already has a decision for aspect "${aspect}"`);
1530
- console.log('');
1531
- console.log(`Current decision: ${existingDecision.decision}`);
1532
- console.log(`Rationale: ${existingDecision.rationale}`);
1533
- console.log('');
1534
- console.log('Use a different aspect name or update the database directly to change this decision.');
1535
- process.exit(1);
1536
- }
1537
-
1538
- // Insert new discovery decision
1539
- db.run(
1540
- `INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale)
1541
- VALUES (?, ?, ?, ?)`,
1542
- [epicId, aspect, decision, rationale],
1543
- (err) => {
1544
- if (err) {
1545
- console.error(`Error: ${err.message}`);
1546
- process.exit(1);
1547
- }
1548
-
1549
- // Update work_items with prototype tracking if provided
1550
- if (prototypes.length > 0) {
1551
- db.run(
1552
- `UPDATE work_items SET prototype_files = ?, discovery_winner = ? WHERE id = ?`,
1553
- [JSON.stringify(prototypes), decision, epicId],
1554
- (err) => {
1555
- if (err) {
1556
- console.warn(`⚠️ Could not save prototypes: ${err.message}`);
1557
- }
1558
- }
1559
- );
1560
- }
1561
-
1562
- console.log(`✅ Epic #${epicId} discovery decision recorded!`);
1563
- console.log('');
1564
- console.log(`Aspect: ${aspect}`);
1565
- console.log(`Decision: ${decision}`);
1566
- console.log(`Rationale: ${rationale}`);
1567
- if (prototypes.length > 0) {
1568
- console.log(`Prototypes: ${prototypes.join(', ')}`);
1569
- }
1570
- console.log('');
1571
- console.log('📝 Architectural decision recorded');
1572
- console.log('');
1573
-
1574
- // Generate DECISIONS.md
1575
- (async () => {
1576
- try {
1577
- const { generateDecisionsFile } = require('../../lib/decisions-generator');
1578
- await generateDecisionsFile();
1579
- console.log('📋 DECISIONS.md updated');
1580
- console.log('');
1581
- } catch (err) {
1582
- console.warn('⚠️ Could not generate DECISIONS.md:', err.message);
1583
- console.log('');
1584
- }
1585
-
1586
- console.log('You can add more decisions for other aspects:');
1587
- console.log(` jettypod work epic-implement ${epicId} --aspect="<type>" --decision="<approach>" --rationale="<why>"`);
1588
- console.log('');
1589
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1590
- console.log('✨ Next Step: Create Features');
1591
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1592
- console.log('');
1593
- console.log('Create features for this epic:');
1594
- console.log(` jettypod work create feature "Feature Title" "Description" --parent=${epicId}`);
1595
- console.log('');
1596
- console.log('Then plan each feature:');
1597
- console.log(' jettypod work discover <feature-id>');
1598
- console.log('');
1599
- console.log('💡 Tip: Claude Code will suggest UX approaches and generate BDD scenarios');
1600
- })();
1601
- }
1602
- );
1603
- }
1604
- );
1605
- });
1606
- break;
1607
- }
1608
-
1609
- case 'implement': {
1610
- const featureId = parseInt(args[0]);
1611
-
1612
- if (!featureId || isNaN(featureId)) {
1613
- console.error('Error: Feature ID is required');
1614
- console.log('Usage: jettypod work implement <feature-id> [--prototypes="file1,file2"] [--winner="file.js"] [--rationale="why this approach"]');
1615
- process.exit(1);
1616
- }
1617
-
1618
- // Parse optional --prototypes, --winner, and --rationale flags
1619
- const prototypesIndex = args.findIndex(a => a.startsWith('--prototypes='));
1620
- const winnerIndex = args.findIndex(a => a.startsWith('--winner='));
1621
- const rationaleIndex = args.findIndex(a => a.startsWith('--rationale='));
1622
-
1623
- const prototypes = prototypesIndex !== -1
1624
- ? args[prototypesIndex].split('=')[1].replace(/^["']|["']$/g, '').split(',').map(p => p.trim())
1625
- : [];
1626
- const winner = winnerIndex !== -1
1627
- ? args[winnerIndex].split('=')[1].replace(/^["']|["']$/g, '')
1628
- : null;
1629
- const rationale = rationaleIndex !== -1
1630
- ? args[rationaleIndex].split('=')[1].replace(/^["']|["']$/g, '')
1631
- : null;
1632
-
1633
- db.get(`SELECT * FROM work_items WHERE id = ?`, [featureId], (err, feature) => {
1634
- if (err) {
1635
- console.error(`Error: ${err.message}`);
1636
- process.exit(1);
1637
- }
1638
-
1639
- if (!feature) {
1640
- console.error(`Error: Feature #${featureId} not found`);
1641
- process.exit(1);
1642
- }
1643
-
1644
- if (feature.type !== 'feature') {
1645
- console.error(`Error: Work item #${featureId} is not a feature (type: ${feature.type})`);
1646
- console.log('');
1647
- console.log('The implement command transitions features from Discovery to Implementation phase.');
1648
- console.log('Only features have phases.');
1649
- process.exit(1);
1650
- }
1651
-
1652
- // Validate that BDD scenarios exist
1653
- if (!feature.scenario_file) {
1654
- console.error(`Error: Feature #${featureId} has no BDD scenarios`);
1655
- console.log('');
1656
- console.log('Discovery is not complete without BDD scenarios.');
1657
- console.log('');
1658
- console.log('To complete discovery:');
1659
- console.log(' 1. Generate scenarios using feature-planning skill');
1660
- console.log(' 2. Or manually create scenarios at features/[feature-name].feature');
1661
- console.log(' 3. Then run: jettypod work implement');
1662
- console.log('');
1663
- console.log('💡 Tip: Talk to Claude Code to complete feature discovery');
1664
- process.exit(1);
1665
- }
1666
-
1667
- // Check if scenario file actually exists on disk
1668
- const fs = require('fs');
1669
- const path = require('path');
1670
- const scenarioPath = path.join(process.cwd(), feature.scenario_file);
1671
-
1672
- if (!fs.existsSync(scenarioPath)) {
1673
- console.error(`Error: Scenario file not found: ${feature.scenario_file}`);
1674
- console.log('');
1675
- console.log('The scenario file path is in the database, but the file doesn\'t exist.');
1676
- console.log('');
1677
- console.log('To fix this:');
1678
- console.log(` 1. Create the file at: ${feature.scenario_file}`);
1679
- console.log(' 2. Or run feature discovery again to regenerate scenarios');
1680
- console.log('');
1681
- console.log('💡 Tip: Talk to Claude Code to complete feature discovery');
1682
- process.exit(1);
1683
- }
1684
-
1685
- // Determine if this is a transition or an update
1686
- const isTransition = feature.phase === 'discovery';
1687
- const isUpdate = feature.phase === 'implementation';
1688
-
1689
- if (!isTransition && !isUpdate) {
1690
- console.error(`Error: Feature #${featureId} has invalid phase: ${feature.phase}`);
1691
- console.log('');
1692
- console.log('Expected phase to be either "discovery" or "implementation"');
1693
- process.exit(1);
1694
- }
1695
-
1696
- // Prepare values
1697
- const prototypeFilesValue = prototypes.length > 0 ? JSON.stringify(prototypes) : null;
1698
- const winnerValue = winner || null;
1699
- const rationaleValue = rationale || null;
1700
-
1701
- // Update query: if transitioning, set phase and mode; if updating, just update decision fields
1702
- const updateSql = isTransition
1703
- ? `UPDATE work_items SET phase = 'implementation', mode = 'speed', prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`
1704
- : `UPDATE work_items SET prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
1705
-
1706
- db.run(
1707
- updateSql,
1708
- [prototypeFilesValue, winnerValue, rationaleValue, featureId],
1709
- (err) => {
1710
- if (err) {
1711
- console.error(`Error: ${err.message}`);
1712
- process.exit(1);
1713
- }
1714
-
1715
- console.log('');
1716
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1717
- if (isTransition) {
1718
- console.log(`✅ Feature #${featureId} transitioned to Implementation Phase`);
1719
- } else {
1720
- console.log(`✅ Feature #${featureId} discovery decision updated`);
1721
- }
1722
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1723
- console.log('');
1724
- console.log(`Title: ${feature.title}`);
1725
- console.log('Phase: Implementation');
1726
- console.log('Mode: Speed');
1727
- if (prototypes.length > 0) {
1728
- console.log(`Prototypes: ${prototypes.join(', ')}`);
1729
- }
1730
- if (winner) {
1731
- console.log(`Winner: ${winner}`);
1732
- }
1733
- if (rationale) {
1734
- console.log(`Rationale: ${rationale}`);
1735
- }
1736
- console.log('');
1737
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1738
- console.log('🚀 Speed Mode: Prove It Works');
1739
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1740
- console.log('');
1741
- console.log('Build code that passes happy path scenarios.');
1742
- console.log('Focus:');
1743
- console.log(' • Happy path only');
1744
- console.log(' • Single file when possible');
1745
- console.log(' • localStorage for data');
1746
- console.log(' • Basic try/catch');
1747
- console.log('');
1748
- console.log('⚠️ Speed Mode is a checkpoint - pass through quickly!');
1749
- console.log('');
1750
- console.log('Next: Elevate to Stable Mode');
1751
- console.log(` jettypod work elevate ${featureId} stable`);
1752
- console.log('');
1753
- }
1754
- );
1755
- });
1756
- break;
1757
- }
1758
-
1759
- case 'discover': {
1760
- const featureId = parseInt(args[0]);
1761
-
1762
- if (!featureId || isNaN(featureId)) {
1763
- console.error('Error: Feature ID is required');
1764
- console.log('Usage: jettypod work discover <feature-id>');
1765
- process.exit(1);
1766
- }
1767
-
1768
- db.get(`SELECT * FROM work_items WHERE id = ?`, [featureId], (err, feature) => {
1769
- if (err) {
1770
- console.error(`Error: ${err.message}`);
1771
- process.exit(1);
1772
- }
1773
-
1774
- if (!feature) {
1775
- console.error(`Error: Feature #${featureId} not found`);
1776
- process.exit(1);
1777
- }
1778
-
1779
- if (feature.type !== 'feature') {
1780
- console.error(`Error: Work item #${featureId} is not a feature (type: ${feature.type})`);
1781
- process.exit(1);
1782
- }
1783
-
1784
- // Get parent epic if exists
1785
- db.get(`SELECT * FROM work_items WHERE id = ?`, [feature.parent_id || feature.epic_id], (err, epic) => {
1786
- // Get epic's architectural decisions if epic exists
1787
- if (epic && epic.type === 'epic') {
1788
- db.all(
1789
- `SELECT * FROM discovery_decisions WHERE work_item_id = ?`,
1790
- [epic.id],
1791
- (err, decisions) => {
1792
- console.log(`✨ Starting feature discovery for #${featureId}: ${feature.title}`);
1793
- console.log('');
1794
- console.log(`Description: ${feature.description || 'No description'}`);
1795
- console.log('');
1796
-
1797
- if (epic) {
1798
- console.log(`Epic: #${epic.id} ${epic.title}`);
1799
-
1800
- if (decisions && decisions.length > 0) {
1801
- console.log('');
1802
- console.log('📋 Epic architectural decisions:');
1803
- decisions.forEach((d) => {
1804
- console.log(` • ${d.aspect}: ${d.decision}`);
1805
- });
1806
- }
1807
- }
1808
-
1809
- console.log('');
1810
- console.log('────────────────────────────────────────────────────');
1811
- console.log('Feature Discovery Context (for Claude Code):');
1812
- console.log('────────────────────────────────────────────────────');
1813
- console.log(`Feature ID: ${featureId}`);
1814
- console.log(`Title: ${feature.title}`);
1815
- console.log(`Description: ${feature.description || 'Not provided'}`);
1816
- if (epic) {
1817
- console.log(`Epic: #${epic.id} ${epic.title}`);
1818
- if (decisions && decisions.length > 0) {
1819
- console.log('Architectural Constraints:');
1820
- decisions.forEach((d) => {
1821
- console.log(` ${d.aspect}: ${d.decision}`);
1822
- });
1823
- }
1824
- }
1825
- console.log('');
1826
- console.log('💬 Now ask Claude Code:');
1827
- console.log(` "Help me with feature discovery for #${featureId}"`);
1828
- console.log('');
1829
- console.log('Claude will use the feature-planning skill to guide you through:');
1830
- console.log(' 1. Suggesting 3 UX approaches');
1831
- console.log(' 2. Optional prototyping');
1832
- console.log(' 3. Choosing the winner');
1833
- console.log(' 4. Generating BDD scenarios');
1834
- console.log(' 5. Transitioning to implementation');
1835
- console.log('');
1836
- console.log('📋 The skill is at: .claude/skills/feature-planning/SKILL.md');
1837
- }
1838
- );
1839
- } else {
1840
- console.log(`✨ Starting feature discovery for #${featureId}: ${feature.title}`);
1841
- console.log('');
1842
- console.log(`Description: ${feature.description || 'No description'}`);
1843
- console.log('');
1844
- console.log('────────────────────────────────────────────────────');
1845
- console.log('Feature Discovery Context (for Claude Code):');
1846
- console.log('────────────────────────────────────────────────────');
1847
- console.log(`Feature ID: ${featureId}`);
1848
- console.log(`Title: ${feature.title}`);
1849
- console.log(`Description: ${feature.description || 'Not provided'}`);
1850
- console.log('');
1851
- console.log('💬 Now ask Claude Code:');
1852
- console.log(` "Help me with feature discovery for #${featureId}"`);
1853
- console.log('');
1854
- console.log('Claude will use the feature-planning skill to guide you through:');
1855
- console.log(' 1. Suggesting 3 UX approaches');
1856
- console.log(' 2. Optional prototyping');
1857
- console.log(' 3. Choosing the winner');
1858
- console.log(' 4. Generating BDD scenarios');
1859
- console.log(' 5. Transitioning to implementation');
1860
- console.log('');
1861
- console.log('📋 The skill is at: .claude/skills/feature-planning/SKILL.md');
1862
- }
1863
- });
1864
- });
1865
- break;
1866
- }
1867
-
1868
- default:
1869
- console.log(`
1870
- JettyPod Work Tracking
1871
-
1872
- Commands:
1873
- jettypod work create <type> <title> [desc] [--parent=ID]
1874
- Types: epic, feature, bug, chore
1875
-
1876
- jettypod backlog [filter]
1877
- Show work items (active by default)
1878
- Filters: all, completed
1879
-
1880
- jettypod work show <id>
1881
- Show work item details
1882
-
1883
- jettypod work status <id> <status>
1884
- Statuses: backlog, todo, in_progress, done, cancelled
1885
-
1886
- jettypod work set-branch <id> <branch-name>
1887
- Set branch for work item
1888
-
1889
- jettypod work set-mode <id> <mode>
1890
- Set mode (speed/discovery/stable/production)
1891
-
1892
- jettypod work current <id>
1893
- Set as current work item
1894
-
1895
- jettypod work epic <id>
1896
- Show epic overview
1897
-
1898
- Examples:
1899
- jettypod work create epic "Q1 Roadmap"
1900
- jettypod work create feature "Auth" "" --parent=1
1901
- jettypod work set-branch 2 feature/auth
1902
- jettypod work set-mode 2 speed
1903
- jettypod work current 2
1904
- jettypod work show 2
1905
- `);
1906
- }
1907
-
1908
- // Don't explicitly close DB in test environments - let Node.js handle cleanup
1909
- // This prevents FATAL errors when async operations are still pending
1910
- if (process.env.NODE_ENV !== 'test') {
1911
- await closeDb();
1912
- }
1913
- }
1914
-
1915
- // Export for use in jettypod.js and other modules
1916
- module.exports = { main, getCurrentWork, updateStatus, create };
1917
-
1918
- // Run if called directly
1919
- if (require.main === module) {
1920
- main();
1921
- }