jettypod 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/PROTECT_SKILLS.md +28 -0
- package/.claude/settings.json +24 -0
- package/.claude/settings.local.json +16 -0
- package/.claude/skills/epic-discover/SKILL.md +262 -0
- package/.claude/skills/feature-discover/SKILL.md +393 -0
- package/.claude/skills/speed-mode/SKILL.md +364 -0
- package/.claude/skills/stable-mode/SKILL.md +591 -0
- package/.github/workflows/test-safety.yml +85 -0
- package/README.md +25 -0
- package/SPEED-STABLE-AUDIT.md +853 -0
- package/SYSTEM-BEHAVIOR.md +1241 -0
- package/TEST_SAFETY_AUDIT.md +314 -0
- package/TEST_SAFETY_IMPLEMENTATION.md +97 -0
- package/cucumber.js +8 -0
- package/docs/COMMAND_REFERENCE.md +903 -0
- package/docs/DECISIONS.md +68 -0
- package/docs/README.md +48 -0
- package/docs/STANDARDS-SYSTEM-DOCUMENTATION.md +374 -0
- package/docs/TEST-REWRITE-PLAN.md +261 -0
- package/docs/ai-test-writing-requirements.md +219 -0
- package/docs/claude-code-skills.md +607 -0
- package/docs/core-jettypod-methodology/comprehensive-jettypod-methodology.md +582 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-comprehensive-standards.md +1222 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-operating-guide.md +3399 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-technical-checklist.md +1325 -0
- package/docs/core-jettypod-methodology/deprecated/jettypod-vibe-coding-framework.md +1544 -0
- package/docs/core-jettypod-methodology/deprecated/prompt-engineering-guide.md +320 -0
- package/docs/core-jettypod-methodology/deprecated/vibe-coding-cheatsheet (1).md +516 -0
- package/docs/core-jettypod-methodology/deprecated/vibe-coding-framework.md +1544 -0
- package/docs/features/jettypod-standards-explained.md +543 -0
- package/docs/features/standards-inventory.md +257 -0
- package/docs/gap-analysis-current-vs-comprehensive-methodology.md +939 -0
- package/docs/jettypod-system-overview.md +409 -0
- package/features/auto-generate-production-chores.feature +14 -0
- package/features/claude-md-protection/steps.js +487 -0
- package/features/decisions/index.js +490 -0
- package/features/decisions/index.test.js +208 -0
- package/features/git-hooks/git-hooks.feature +30 -0
- package/features/git-hooks/index.js +93 -0
- package/features/git-hooks/index.test.js +137 -0
- package/features/git-hooks/post-commit +56 -0
- package/features/git-hooks/post-merge +47 -0
- package/features/git-hooks/pre-commit +28 -0
- package/features/git-hooks/simple-steps.js +53 -0
- package/features/git-hooks/simple-test.feature +10 -0
- package/features/git-hooks/steps.js +196 -0
- package/features/jettypod-update-command.feature +46 -0
- package/features/mode-prompts/index.js +95 -0
- package/features/mode-prompts/simple-steps.js +44 -0
- package/features/mode-prompts/simple-test.feature +9 -0
- package/features/mode-prompts/validation.test.js +120 -0
- package/features/refactor-mode/steps.js +217 -0
- package/features/refactor-mode.feature +49 -0
- package/features/skills-update/index.test.js +216 -0
- package/features/step_definitions/auto-generate-production-chores.steps.js +162 -0
- package/features/step_definitions/terminal-logo.steps.js +145 -0
- package/features/step_definitions/update-command.steps.js +183 -0
- package/features/terminal-logo/index.js +39 -0
- package/features/terminal-logo/terminal-logo.feature +30 -0
- package/features/update-command/index.js +181 -0
- package/features/update-command/index.test.js +225 -0
- package/features/work-commands/bug-workflow-display.feature +22 -0
- package/features/work-commands/index.js +311 -0
- package/features/work-commands/simple-steps.js +69 -0
- package/features/work-commands/stable-tests.feature +57 -0
- package/features/work-commands/steps.js +1120 -0
- package/features/work-commands/validation.test.js +88 -0
- package/features/work-commands/work-commands.feature +13 -0
- package/features/work-tracking/discovery-validation.test.js +228 -0
- package/features/work-tracking/index.js +1511 -0
- package/features/work-tracking/mode-required.feature +112 -0
- package/features/work-tracking/phase-tracking.test.js +482 -0
- package/features/work-tracking/prototype-tracking.test.js +485 -0
- package/features/work-tracking/tree-view.test.js +310 -0
- package/features/work-tracking/work-set-mode.feature +71 -0
- package/features/work-tracking/work-start-mode.feature +88 -0
- package/full-test.txt +0 -0
- package/install.sh +89 -0
- package/jettypod.js +1640 -0
- package/lib/bug-workflow.js +94 -0
- package/lib/bug-workflow.test.js +177 -0
- package/lib/claudemd.js +130 -0
- package/lib/claudemd.test.js +195 -0
- package/lib/comprehensive-standards-full.json +1778 -0
- package/lib/config.js +181 -0
- package/lib/config.test.js +511 -0
- package/lib/constants.js +107 -0
- package/lib/constants.test.js +164 -0
- package/lib/current-work.js +130 -0
- package/lib/current-work.test.js +146 -0
- package/lib/database-project-config.test.js +107 -0
- package/lib/database.js +256 -0
- package/lib/database.test.js +106 -0
- package/lib/decisions-generator.js +102 -0
- package/lib/decisions-generator.test.js +457 -0
- package/lib/decisions-helpers.js +119 -0
- package/lib/decisions-helpers.test.js +310 -0
- package/lib/discovery-checkpoint.js +83 -0
- package/lib/docs-generator.js +280 -0
- package/lib/external-checklist.js +177 -0
- package/lib/git.js +142 -0
- package/lib/git.test.js +145 -0
- package/lib/logo.js +3 -0
- package/lib/migrations/001-epic-to-parent.js +24 -0
- package/lib/migrations/002-default-work-item-modes.js +37 -0
- package/lib/migrations/002-default-work-item-modes.test.js +351 -0
- package/lib/migrations/003-epic-discovery-fields.js +52 -0
- package/lib/migrations/004-discovery-decisions-table.js +32 -0
- package/lib/migrations/005-migrate-decision-data.js +62 -0
- package/lib/migrations/006-feature-phase-field.js +61 -0
- package/lib/migrations/007-prototype-tracking.js +38 -0
- package/lib/migrations/008-scenario-file-field.js +24 -0
- package/lib/migrations/index.js +74 -0
- package/lib/production-helpers.js +69 -0
- package/lib/project-state.test.js +92 -0
- package/lib/test-helpers.js +184 -0
- package/lib/test-helpers.test.js +255 -0
- package/package.json +36 -0
- package/prototypes/test/index.html +1 -0
- package/setup-dist-repo.sh +68 -0
- package/test-safety-check.sh +80 -0
- package/work-item-tracking-plan.md +199 -0
|
@@ -0,0 +1,1511 @@
|
|
|
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 } = require('../../lib/database');
|
|
8
|
+
const { getCurrentWork } = require('../../lib/current-work');
|
|
9
|
+
const { TYPE_EMOJIS, STATUS_EMOJIS } = require('../../lib/constants');
|
|
10
|
+
|
|
11
|
+
const db = getDb();
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Find epic by traversing parent_id chain
|
|
15
|
+
* @param {number} itemId - Work item ID to start from
|
|
16
|
+
* @returns {Promise<Object|null>} Epic work item or null
|
|
17
|
+
*/
|
|
18
|
+
function findEpic(itemId) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
function traverse(currentId) {
|
|
21
|
+
if (!currentId) {
|
|
22
|
+
return resolve(null);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
db.get('SELECT id, type, title, parent_id FROM work_items WHERE id = ?', [currentId], (err, item) => {
|
|
26
|
+
if (err) {
|
|
27
|
+
return reject(new Error(`Failed to find epic: ${err.message}`));
|
|
28
|
+
}
|
|
29
|
+
if (!item) {
|
|
30
|
+
return resolve(null);
|
|
31
|
+
}
|
|
32
|
+
if (item.type === 'epic') {
|
|
33
|
+
return resolve({ id: item.id, title: item.title });
|
|
34
|
+
}
|
|
35
|
+
traverse(item.parent_id);
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
traverse(itemId);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Create work item
|
|
44
|
+
function create(type, title, description = '', parentId = null, mode = null, needsDiscovery = false) {
|
|
45
|
+
return new Promise((resolve, reject) => {
|
|
46
|
+
// Validate project discovery is not blocking work creation
|
|
47
|
+
if (type === 'epic' || type === 'feature') {
|
|
48
|
+
const config = require('../../lib/config');
|
|
49
|
+
const currentConfig = config.read();
|
|
50
|
+
const discovery = currentConfig.project_discovery;
|
|
51
|
+
|
|
52
|
+
if (discovery && discovery.status === 'in_progress') {
|
|
53
|
+
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.`));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Validate needs_discovery flag is only used for epics
|
|
58
|
+
if (needsDiscovery && type !== 'epic') {
|
|
59
|
+
return reject(new Error(`The --needs-discovery flag can only be used with epic work items`));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Continue with creation (no blocking validation for epic discovery)
|
|
63
|
+
// The needs_discovery flag is informational only - trust users to decide when ready
|
|
64
|
+
continueCreate();
|
|
65
|
+
|
|
66
|
+
function continueCreate() {
|
|
67
|
+
// Chores don't have modes - they inherit context from their parent feature
|
|
68
|
+
if (type === 'chore') {
|
|
69
|
+
if (mode) {
|
|
70
|
+
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>`));
|
|
71
|
+
}
|
|
72
|
+
mode = null; // Explicitly set to null for chores
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Default mode to 'discovery' for features and bugs when not specified
|
|
76
|
+
// Epics and chores don't have modes (NULL)
|
|
77
|
+
if ((type === 'feature' || type === 'bug') && !mode) {
|
|
78
|
+
mode = 'discovery';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Validate mode value if provided (only for features and bugs)
|
|
82
|
+
if (mode) {
|
|
83
|
+
const validModes = ['speed', 'discovery', 'stable', 'production'];
|
|
84
|
+
if (!validModes.includes(mode)) {
|
|
85
|
+
return reject(new Error(`Invalid mode: ${mode}. Must be one of: ${validModes.join(', ')}`));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Set phase for features (discovery by default, implementation if mode specified, NULL for everything else)
|
|
90
|
+
const phase = type === 'feature' ? (mode ? 'implementation' : 'discovery') : null;
|
|
91
|
+
|
|
92
|
+
const sql = `INSERT INTO work_items (type, title, description, parent_id, mode, needs_discovery, phase) VALUES (?, ?, ?, ?, ?, ?, ?)`;
|
|
93
|
+
db.run(sql, [type, title, description, parentId, mode, needsDiscovery ? 1 : 0, phase], function(err) {
|
|
94
|
+
if (err) {
|
|
95
|
+
return reject(err);
|
|
96
|
+
}
|
|
97
|
+
const newId = this.lastID;
|
|
98
|
+
const discoveryIndicator = needsDiscovery ? ' (needs discovery)' : '';
|
|
99
|
+
console.log(`Created ${type} #${newId}: ${title}${discoveryIndicator}`);
|
|
100
|
+
resolve(newId);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get all work items as hierarchical tree structure
|
|
108
|
+
* @param {boolean} includeCompleted - Include done/cancelled items (default: false)
|
|
109
|
+
* @returns {Promise<Array>} Root work items with nested children
|
|
110
|
+
* @throws {Error} If database query fails
|
|
111
|
+
*/
|
|
112
|
+
function getTree(includeCompleted = false) {
|
|
113
|
+
return new Promise((resolve, reject) => {
|
|
114
|
+
const whereClause = includeCompleted ? '' : "WHERE status NOT IN ('done', 'cancelled')";
|
|
115
|
+
db.all(`SELECT * FROM work_items ${whereClause} ORDER BY parent_id, id`, [], (err, rows) => {
|
|
116
|
+
if (err) {
|
|
117
|
+
return reject(new Error(`Failed to fetch work items: ${err.message}`));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!rows || rows.length === 0) {
|
|
121
|
+
return resolve([]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const itemsById = {};
|
|
125
|
+
const rootItems = [];
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
// Build lookup
|
|
129
|
+
rows.forEach(item => {
|
|
130
|
+
itemsById[item.id] = item;
|
|
131
|
+
item.children = [];
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Build tree
|
|
135
|
+
rows.forEach(item => {
|
|
136
|
+
if (item.parent_id && itemsById[item.parent_id]) {
|
|
137
|
+
itemsById[item.parent_id].children.push(item);
|
|
138
|
+
} else if (!item.parent_id) {
|
|
139
|
+
rootItems.push(item);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
resolve(rootItems);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
reject(new Error(`Failed to build work tree: ${err.message}`));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Print work items tree to console with visual hierarchy
|
|
153
|
+
* @param {Array} items - Work items to print
|
|
154
|
+
* @param {string} prefix - Indentation prefix for nested items
|
|
155
|
+
* @param {boolean} isRootLevel - Whether we're at the root level
|
|
156
|
+
* @param {Set} expandedIds - Set of item IDs to show expanded (null = all collapsed, 'all' = all expanded)
|
|
157
|
+
* @throws {Error} If items is not an array
|
|
158
|
+
*/
|
|
159
|
+
function printTree(items, prefix = '', isRootLevel = true, expandedIds = null) {
|
|
160
|
+
if (!Array.isArray(items)) {
|
|
161
|
+
throw new Error('Items must be an array');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
items.forEach((item, index) => {
|
|
165
|
+
if (!item || typeof item !== 'object') {
|
|
166
|
+
console.warn(`Skipping invalid item at index ${index}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const isLast = index === items.length - 1;
|
|
171
|
+
const emoji = TYPE_EMOJIS[item.type] || '📋';
|
|
172
|
+
|
|
173
|
+
// Show phase/mode indicator for features
|
|
174
|
+
let modeIndicator = '';
|
|
175
|
+
if (item.type === 'feature') {
|
|
176
|
+
if (item.phase === 'discovery') {
|
|
177
|
+
modeIndicator = ' [🔍 discovery]';
|
|
178
|
+
} else if (item.mode) {
|
|
179
|
+
// Implementation phase - show mode
|
|
180
|
+
modeIndicator = ` [${item.mode}]`;
|
|
181
|
+
}
|
|
182
|
+
} else if (item.type !== 'epic' && item.mode) {
|
|
183
|
+
// Non-feature, non-epic items (chores, bugs) - show mode only
|
|
184
|
+
modeIndicator = ` [${item.mode}]`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Determine if this item should be expanded
|
|
188
|
+
const isExpanded = expandedIds === 'all' || (expandedIds && expandedIds.has(item.id));
|
|
189
|
+
const hasChildren = item.children && item.children.length > 0;
|
|
190
|
+
const expandIndicator = hasChildren ? (isExpanded ? ' ⊖' : ' ⊕') : '';
|
|
191
|
+
const childCount = hasChildren ? ` (${item.children.length} ${item.type === 'epic' ? 'features' : item.type === 'feature' ? 'chores' : 'items'})` : '';
|
|
192
|
+
|
|
193
|
+
// Root level items have no connectors
|
|
194
|
+
if (isRootLevel) {
|
|
195
|
+
console.log(`${emoji} [#${item.id}] ${item.title}${modeIndicator}${childCount}${expandIndicator}`);
|
|
196
|
+
if (isExpanded && item.description) {
|
|
197
|
+
console.log(` Description: "${item.description}"`);
|
|
198
|
+
}
|
|
199
|
+
if (isExpanded && item.status) {
|
|
200
|
+
console.log(` Status: ${item.status}${item.mode ? ` | Mode: ${item.mode}` : ''}`);
|
|
201
|
+
}
|
|
202
|
+
} else {
|
|
203
|
+
// Nested items get tree connectors
|
|
204
|
+
const connector = isLast ? '└── ' : '├── ';
|
|
205
|
+
console.log(`${prefix}${connector}${emoji} [#${item.id}] ${item.title}${modeIndicator}${childCount}${expandIndicator}`);
|
|
206
|
+
if (isExpanded && item.description) {
|
|
207
|
+
const descPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
208
|
+
console.log(`${descPrefix}Description: "${item.description}"`);
|
|
209
|
+
}
|
|
210
|
+
if (isExpanded && item.status) {
|
|
211
|
+
const descPrefix = prefix + (isLast ? ' ' : '│ ');
|
|
212
|
+
console.log(`${descPrefix}Status: ${item.status}${item.mode ? ` | Mode: ${item.mode}` : ''}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Only show children if expanded
|
|
217
|
+
if (hasChildren && isExpanded) {
|
|
218
|
+
const newPrefix = isRootLevel ? '' : (prefix + (isLast ? ' ' : '│ '));
|
|
219
|
+
printTree(item.children, newPrefix, false, expandedIds);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Update status
|
|
225
|
+
function updateStatus(id, status) {
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
// First get the work item to check its parent
|
|
228
|
+
db.get('SELECT id, parent_id FROM work_items WHERE id = ?', [id], (err, item) => {
|
|
229
|
+
if (err) {
|
|
230
|
+
console.error(`Error: ${err.message}`);
|
|
231
|
+
return reject(err);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (!item) {
|
|
235
|
+
console.log(`Work item #${id} not found`);
|
|
236
|
+
return resolve();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Update the status
|
|
240
|
+
db.run(`UPDATE work_items SET status = ? WHERE id = ?`, [status, id], (err) => {
|
|
241
|
+
if (err) {
|
|
242
|
+
console.error(`Error: ${err.message}`);
|
|
243
|
+
return reject(err);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
console.log(`Updated #${id} status to ${status}`);
|
|
247
|
+
|
|
248
|
+
// If status is 'done' and item has a parent, check if we should auto-close the parent epic
|
|
249
|
+
if (status === 'done' && item.parent_id) {
|
|
250
|
+
db.get('SELECT id, type FROM work_items WHERE id = ?', [item.parent_id], (err, parent) => {
|
|
251
|
+
if (err || !parent) {
|
|
252
|
+
return resolve();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Only auto-close epics
|
|
256
|
+
if (parent.type === 'epic') {
|
|
257
|
+
// Check if all children of this epic are done
|
|
258
|
+
db.all(
|
|
259
|
+
'SELECT id, status FROM work_items WHERE parent_id = ?',
|
|
260
|
+
[parent.id],
|
|
261
|
+
(err, children) => {
|
|
262
|
+
if (err) {
|
|
263
|
+
return resolve();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const allDone = children.every(child => child.status === 'done');
|
|
267
|
+
if (allDone) {
|
|
268
|
+
db.run('UPDATE work_items SET status = ? WHERE id = ?', ['done', parent.id], (err) => {
|
|
269
|
+
if (err) {
|
|
270
|
+
console.error(`Failed to auto-close epic: ${err.message}`);
|
|
271
|
+
} else {
|
|
272
|
+
console.log(`✓ Epic #${parent.id} also completed (all children done)`);
|
|
273
|
+
}
|
|
274
|
+
resolve();
|
|
275
|
+
});
|
|
276
|
+
} else {
|
|
277
|
+
resolve();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
} else {
|
|
282
|
+
resolve();
|
|
283
|
+
}
|
|
284
|
+
});
|
|
285
|
+
} else {
|
|
286
|
+
resolve();
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Set branch
|
|
294
|
+
function setBranch(id, branch) {
|
|
295
|
+
db.run(`UPDATE work_items SET branch_name = ? WHERE id = ?`, [branch, id], () => {
|
|
296
|
+
console.log(`Set #${id} branch to ${branch}`);
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Set mode
|
|
301
|
+
function setMode(id, mode) {
|
|
302
|
+
return new Promise((resolve, reject) => {
|
|
303
|
+
const { getCurrentWork } = require('../../lib/current-work');
|
|
304
|
+
const { updateCurrentWork } = require('../../lib/claudemd');
|
|
305
|
+
|
|
306
|
+
// Validate mode
|
|
307
|
+
const validModes = ['speed', 'discovery', 'stable', 'production'];
|
|
308
|
+
if (mode && !validModes.includes(mode)) {
|
|
309
|
+
const error = new Error(`Invalid mode: ${mode}. Must be one of: ${validModes.join(', ')}`);
|
|
310
|
+
console.error(`Error: ${error.message}`);
|
|
311
|
+
reject(error);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Check if work item is an epic - epics don't have modes
|
|
316
|
+
db.get(`SELECT type FROM work_items WHERE id = ?`, [id], (err, row) => {
|
|
317
|
+
if (err) {
|
|
318
|
+
console.error(`Error: ${err.message}`);
|
|
319
|
+
reject(err);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (!row) {
|
|
324
|
+
console.error(`Error: Work item #${id} not found`);
|
|
325
|
+
resolve();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (row.type === 'epic') {
|
|
330
|
+
const error = new Error('Epics do not have modes. Only features and bugs have modes.');
|
|
331
|
+
console.error(`Error: ${error.message}`);
|
|
332
|
+
reject(error);
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (row.type === 'chore') {
|
|
337
|
+
const error = new Error('Chores do not have modes. Chores inherit the workflow context from their parent feature.');
|
|
338
|
+
console.error(`Error: ${error.message}`);
|
|
339
|
+
reject(error);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Update database
|
|
344
|
+
db.serialize(() => {
|
|
345
|
+
db.run(`UPDATE work_items SET mode = ? WHERE id = ?`, [mode, id], (err) => {
|
|
346
|
+
if (err) {
|
|
347
|
+
console.error(`Error: ${err.message}`);
|
|
348
|
+
reject(err);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
console.log(`Set #${id} mode to ${mode}`);
|
|
353
|
+
|
|
354
|
+
// If this is the current work item, update CLAUDE.md
|
|
355
|
+
const currentWork = getCurrentWork();
|
|
356
|
+
if (currentWork && currentWork.id === id) {
|
|
357
|
+
// Get updated work item to pass to updateCurrentWork
|
|
358
|
+
db.get(`
|
|
359
|
+
SELECT w.*,
|
|
360
|
+
p.title as parent_title, p.id as parent_id
|
|
361
|
+
FROM work_items w
|
|
362
|
+
LEFT JOIN work_items p ON w.parent_id = p.id
|
|
363
|
+
WHERE w.id = ?
|
|
364
|
+
`, [id], async (err, workItem) => {
|
|
365
|
+
if (err || !workItem) {
|
|
366
|
+
resolve();
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Add epic info
|
|
371
|
+
const epic = await findEpic(workItem.id);
|
|
372
|
+
workItem.epic_id = epic ? epic.id : null;
|
|
373
|
+
workItem.epic_title = epic ? epic.title : null;
|
|
374
|
+
|
|
375
|
+
// Update CLAUDE.md with new mode
|
|
376
|
+
// Epics don't have mode lines in CLAUDE.md, pass null
|
|
377
|
+
const modeForClaudeMd = workItem.type === 'epic' ? null : mode;
|
|
378
|
+
updateCurrentWork(workItem, modeForClaudeMd);
|
|
379
|
+
console.log('📝 CLAUDE.md updated');
|
|
380
|
+
resolve();
|
|
381
|
+
});
|
|
382
|
+
} else {
|
|
383
|
+
resolve();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
}); // Close outer db.get for epic check
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Set current work item
|
|
392
|
+
function setCurrent(id) {
|
|
393
|
+
const { setCurrentWork } = require('../../lib/current-work');
|
|
394
|
+
const { updateCurrentWork } = require('../../lib/claudemd');
|
|
395
|
+
|
|
396
|
+
db.run(`UPDATE work_items SET current = 0`, [], () => {
|
|
397
|
+
db.get(`
|
|
398
|
+
SELECT w.*,
|
|
399
|
+
p.title as parent_title, p.id as parent_id, p.type as parent_type
|
|
400
|
+
FROM work_items w
|
|
401
|
+
LEFT JOIN work_items p ON w.parent_id = p.id
|
|
402
|
+
WHERE w.id = ?
|
|
403
|
+
`, [id], async (err, workItem) => {
|
|
404
|
+
if (err || !workItem) {
|
|
405
|
+
console.log(`Work item #${id} not found`);
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Find epic by traversing parent chain
|
|
410
|
+
const epic = await findEpic(workItem.id);
|
|
411
|
+
|
|
412
|
+
db.run(`UPDATE work_items SET current = 1 WHERE id = ?`, [id], () => {
|
|
413
|
+
// Create current work file
|
|
414
|
+
const currentWork = {
|
|
415
|
+
id: workItem.id,
|
|
416
|
+
title: workItem.title,
|
|
417
|
+
type: workItem.type,
|
|
418
|
+
status: workItem.status,
|
|
419
|
+
mode: workItem.mode,
|
|
420
|
+
parent_id: workItem.parent_id,
|
|
421
|
+
parent_title: workItem.parent_title,
|
|
422
|
+
parent_type: workItem.parent_type,
|
|
423
|
+
epic_id: epic ? epic.id : null,
|
|
424
|
+
epic_title: epic ? epic.title : null,
|
|
425
|
+
description: workItem.description
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
setCurrentWork(currentWork);
|
|
429
|
+
updateCurrentWork(currentWork, workItem.mode);
|
|
430
|
+
|
|
431
|
+
console.log(`Set #${id} as current work`);
|
|
432
|
+
});
|
|
433
|
+
});
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Re-export getCurrentWork from shared module for backwards compatibility
|
|
438
|
+
// (used by jettypod.js)
|
|
439
|
+
|
|
440
|
+
// Show single work item details
|
|
441
|
+
function showItem(id) {
|
|
442
|
+
return new Promise(async (resolve) => {
|
|
443
|
+
db.get(`
|
|
444
|
+
SELECT w.*,
|
|
445
|
+
p.title as parent_title
|
|
446
|
+
FROM work_items w
|
|
447
|
+
LEFT JOIN work_items p ON w.parent_id = p.id
|
|
448
|
+
WHERE w.id = ?
|
|
449
|
+
`, [id], async (err, row) => {
|
|
450
|
+
if (err || !row) {
|
|
451
|
+
console.log(`Work item #${id} not found`);
|
|
452
|
+
resolve(null);
|
|
453
|
+
} else {
|
|
454
|
+
const epic = await findEpic(row.id);
|
|
455
|
+
|
|
456
|
+
console.log(`\n#${row.id} ${row.title}`);
|
|
457
|
+
console.log(`Type: ${row.type}`);
|
|
458
|
+
console.log(`Status: ${row.status}`);
|
|
459
|
+
if (row.mode) console.log(`Mode: ${row.mode}`);
|
|
460
|
+
if (row.branch_name) console.log(`Branch: ${row.branch_name}`);
|
|
461
|
+
if (row.parent_title) console.log(`Parent: #${row.parent_id} ${row.parent_title}`);
|
|
462
|
+
if (epic) console.log(`Epic: #${epic.id} ${epic.title}`);
|
|
463
|
+
|
|
464
|
+
// Display prototype tracking information
|
|
465
|
+
if (row.prototype_files) {
|
|
466
|
+
try {
|
|
467
|
+
const prototypeFiles = JSON.parse(row.prototype_files);
|
|
468
|
+
if (prototypeFiles && prototypeFiles.length > 0) {
|
|
469
|
+
console.log(`\n🔬 Prototypes: ${prototypeFiles.join(', ')}`);
|
|
470
|
+
}
|
|
471
|
+
} catch (e) {
|
|
472
|
+
// Invalid JSON, skip
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (row.discovery_winner) {
|
|
476
|
+
console.log(`✅ Winner: ${row.discovery_winner}`);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Display epic discovery information
|
|
480
|
+
if (row.type === 'epic' && row.needs_discovery) {
|
|
481
|
+
// Query all discovery decisions for this epic
|
|
482
|
+
db.all(
|
|
483
|
+
`SELECT * FROM discovery_decisions WHERE work_item_id = ? ORDER BY created_at`,
|
|
484
|
+
[row.id],
|
|
485
|
+
(err, decisions) => {
|
|
486
|
+
if (err) {
|
|
487
|
+
console.error(`Error fetching decisions: ${err.message}`);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (decisions && decisions.length > 0) {
|
|
491
|
+
console.log(`\n🏛 Discovery Decisions:`);
|
|
492
|
+
decisions.forEach((d) => {
|
|
493
|
+
console.log(`\n ${d.aspect}:`);
|
|
494
|
+
console.log(` Decision: ${d.decision}`);
|
|
495
|
+
console.log(` Rationale: ${d.rationale}`);
|
|
496
|
+
});
|
|
497
|
+
console.log(`\n 💡 Add more decisions: jettypod work epic-implement ${row.id} --aspect="<type>" --decision="<approach>" --rationale="<why>"`);
|
|
498
|
+
} else {
|
|
499
|
+
console.log(`\n⚠️ DISCOVERY REQUIRED: This epic needs architectural decisions`);
|
|
500
|
+
console.log(``);
|
|
501
|
+
console.log(` 💬 Talk to Claude Code: "Let's do epic discovery for #${row.id}"`);
|
|
502
|
+
console.log(` Or run: jettypod work epic-discover ${row.id}`);
|
|
503
|
+
console.log(``);
|
|
504
|
+
console.log(` Claude Code will guide you through:`);
|
|
505
|
+
console.log(` • Suggesting 3 architectural options`);
|
|
506
|
+
console.log(` • Building prototypes`);
|
|
507
|
+
console.log(` • Recording your decisions`);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (row.description) console.log(`\nDescription: ${row.description}`);
|
|
511
|
+
resolve(row);
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
} else {
|
|
515
|
+
if (row.description) console.log(`\nDescription: ${row.description}`);
|
|
516
|
+
resolve(row);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Epic overview
|
|
524
|
+
function epicOverview(epicId) {
|
|
525
|
+
return new Promise(async (resolve) => {
|
|
526
|
+
db.all('SELECT * FROM work_items', [], async (err, rows) => {
|
|
527
|
+
if (err) {
|
|
528
|
+
console.error(err);
|
|
529
|
+
return resolve();
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// Filter items that belong to this epic
|
|
533
|
+
const epicItems = [];
|
|
534
|
+
for (const row of rows) {
|
|
535
|
+
const epic = await findEpic(row.id);
|
|
536
|
+
if ((epic && epic.id === epicId) || row.id === epicId) {
|
|
537
|
+
epicItems.push(row);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
console.log(`\nEpic #${epicId} Overview:`);
|
|
542
|
+
|
|
543
|
+
const byType = {};
|
|
544
|
+
const byStatus = {};
|
|
545
|
+
|
|
546
|
+
epicItems.forEach(row => {
|
|
547
|
+
if (!byType[row.type]) byType[row.type] = 0;
|
|
548
|
+
byType[row.type]++;
|
|
549
|
+
|
|
550
|
+
if (!byStatus[row.status]) byStatus[row.status] = 0;
|
|
551
|
+
byStatus[row.status]++;
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
console.log('\nBy Type:');
|
|
555
|
+
Object.entries(byType).forEach(([type, count]) => {
|
|
556
|
+
console.log(` ${type}: ${count}`);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
console.log('\nBy Status:');
|
|
560
|
+
Object.entries(byStatus).forEach(([status, count]) => {
|
|
561
|
+
console.log(` ${status}: ${count}`);
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
resolve();
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Main CLI handler
|
|
570
|
+
async function main() {
|
|
571
|
+
const command = process.argv[2];
|
|
572
|
+
const args = process.argv.slice(3);
|
|
573
|
+
|
|
574
|
+
switch(command) {
|
|
575
|
+
case 'create': {
|
|
576
|
+
const type = args[0];
|
|
577
|
+
const title = args[1];
|
|
578
|
+
const desc = args[2] || '';
|
|
579
|
+
|
|
580
|
+
let parentId = null;
|
|
581
|
+
let mode = null;
|
|
582
|
+
let needsDiscovery = false;
|
|
583
|
+
|
|
584
|
+
args.forEach(arg => {
|
|
585
|
+
if (arg.startsWith('--parent=')) {
|
|
586
|
+
parentId = parseInt(arg.split('=')[1]);
|
|
587
|
+
}
|
|
588
|
+
if (arg.startsWith('--mode=')) {
|
|
589
|
+
mode = arg.split('=')[1];
|
|
590
|
+
}
|
|
591
|
+
if (arg === '--needs-discovery') {
|
|
592
|
+
needsDiscovery = true;
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
const newId = await create(type, title, desc, parentId, mode, needsDiscovery);
|
|
598
|
+
|
|
599
|
+
// Prompt for epic discovery after epic creation
|
|
600
|
+
if (type === 'epic') {
|
|
601
|
+
console.log('');
|
|
602
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
603
|
+
console.log('🎯 Plan this epic now?');
|
|
604
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
605
|
+
console.log('');
|
|
606
|
+
console.log('Ask Claude Code:');
|
|
607
|
+
console.log(` "Help me plan epic #${newId}"`);
|
|
608
|
+
console.log('');
|
|
609
|
+
console.log('Claude will help you:');
|
|
610
|
+
console.log(' • Brainstorm features for this epic');
|
|
611
|
+
console.log(' • Identify architectural decisions (if needed)');
|
|
612
|
+
console.log(' • Create features automatically');
|
|
613
|
+
console.log('');
|
|
614
|
+
console.log('Or run: jettypod work epic-discover ' + newId);
|
|
615
|
+
console.log('');
|
|
616
|
+
console.log('💡 You can also plan later when ready');
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Check if creating feature under an unplanned epic
|
|
620
|
+
if (type === 'feature' && parentId) {
|
|
621
|
+
db.get(`SELECT * FROM work_items WHERE id = ?`, [parentId], (err, parent) => {
|
|
622
|
+
if (!err && parent && parent.type === 'epic') {
|
|
623
|
+
// Check if epic has any features already
|
|
624
|
+
db.get(`SELECT COUNT(*) as count FROM work_items WHERE parent_id = ? AND type = 'feature'`, [parentId], (err, result) => {
|
|
625
|
+
if (!err && result.count === 1) {
|
|
626
|
+
// This is the first feature - suggest planning the epic
|
|
627
|
+
console.log('');
|
|
628
|
+
console.log('💡 Tip: Consider planning this epic first');
|
|
629
|
+
console.log('');
|
|
630
|
+
console.log('Ask Claude Code:');
|
|
631
|
+
console.log(` "Help me plan epic #${parentId}"`);
|
|
632
|
+
console.log('');
|
|
633
|
+
console.log(`Or run: jettypod work epic-discover ${parentId}`);
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.error(`Error: ${err.message}`);
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
case 'tree': {
|
|
647
|
+
try {
|
|
648
|
+
// Parse expand flags
|
|
649
|
+
let expandedIds = null; // null = collapsed by default
|
|
650
|
+
const expandArg = process.argv.find(arg => arg.startsWith('--expand'));
|
|
651
|
+
|
|
652
|
+
if (expandArg === '--expand-all') {
|
|
653
|
+
expandedIds = 'all';
|
|
654
|
+
} else if (expandArg && expandArg.includes('=')) {
|
|
655
|
+
const idsStr = expandArg.split('=')[1];
|
|
656
|
+
const ids = idsStr.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
|
|
657
|
+
expandedIds = new Set(ids);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const currentWork = getCurrentWork();
|
|
661
|
+
|
|
662
|
+
// Show active work at top if exists
|
|
663
|
+
if (currentWork) {
|
|
664
|
+
const emoji = TYPE_EMOJIS[currentWork.type] || '📋';
|
|
665
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
666
|
+
console.log('🎯 ACTIVE WORK');
|
|
667
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
668
|
+
console.log(`${emoji} [#${currentWork.id}] ${currentWork.title}`);
|
|
669
|
+
// Show epic if parent is the epic, otherwise show parent
|
|
670
|
+
if (currentWork.epic_title && currentWork.epic_id !== currentWork.id && currentWork.parent_id === currentWork.epic_id) {
|
|
671
|
+
console.log(`└─ Epic: 🎯 #${currentWork.epic_id} ${currentWork.epic_title}`);
|
|
672
|
+
} else if (currentWork.parent_title) {
|
|
673
|
+
const parentEmoji = TYPE_EMOJIS[currentWork.parent_type] || '📋';
|
|
674
|
+
console.log(`└─ Part of: ${parentEmoji} #${currentWork.parent_id} ${currentWork.parent_title}`);
|
|
675
|
+
} else if (currentWork.epic_title && currentWork.epic_id !== currentWork.id) {
|
|
676
|
+
console.log(`└─ Epic: 🎯 #${currentWork.epic_id} ${currentWork.epic_title}`);
|
|
677
|
+
}
|
|
678
|
+
console.log('');
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Show recently completed items
|
|
682
|
+
const recentlyCompleted = await new Promise((resolve, reject) => {
|
|
683
|
+
db.all(`
|
|
684
|
+
SELECT w.id, w.title, w.type, w.mode,
|
|
685
|
+
p.title as parent_title, p.id as parent_id, p.type as parent_type
|
|
686
|
+
FROM work_items w
|
|
687
|
+
LEFT JOIN work_items p ON w.parent_id = p.id
|
|
688
|
+
WHERE w.status = 'done'
|
|
689
|
+
ORDER BY w.id DESC
|
|
690
|
+
LIMIT 3
|
|
691
|
+
`, [], async (err, rows) => {
|
|
692
|
+
if (err) reject(err);
|
|
693
|
+
else {
|
|
694
|
+
// Add epic info to each item
|
|
695
|
+
for (const row of rows || []) {
|
|
696
|
+
const epic = await findEpic(row.id);
|
|
697
|
+
row.epic_id = epic ? epic.id : null;
|
|
698
|
+
row.epic_title = epic ? epic.title : null;
|
|
699
|
+
}
|
|
700
|
+
resolve(rows || []);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
});
|
|
704
|
+
|
|
705
|
+
if (recentlyCompleted.length > 0) {
|
|
706
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
707
|
+
console.log('✅ RECENTLY COMPLETED');
|
|
708
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
709
|
+
recentlyCompleted.forEach(item => {
|
|
710
|
+
const emoji = TYPE_EMOJIS[item.type] || '📋';
|
|
711
|
+
|
|
712
|
+
// Show phase/mode indicator
|
|
713
|
+
let modeIndicator = '';
|
|
714
|
+
if (item.type === 'feature') {
|
|
715
|
+
if (item.phase === 'discovery') {
|
|
716
|
+
modeIndicator = ' [🔍 discovery]';
|
|
717
|
+
} else if (item.mode) {
|
|
718
|
+
modeIndicator = ` [${item.mode}]`;
|
|
719
|
+
}
|
|
720
|
+
} else if (item.mode) {
|
|
721
|
+
modeIndicator = ` [${item.mode}]`;
|
|
722
|
+
}
|
|
723
|
+
console.log(`${emoji} [${item.id}] ${item.title}${modeIndicator}`);
|
|
724
|
+
// Show epic if parent is the epic, otherwise show parent
|
|
725
|
+
if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
|
|
726
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
727
|
+
} else if (item.parent_title) {
|
|
728
|
+
const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
|
|
729
|
+
console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
|
|
730
|
+
} else if (item.epic_title && item.epic_id !== item.id) {
|
|
731
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
console.log('');
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
738
|
+
console.log('📋 BACKLOG');
|
|
739
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
740
|
+
const items = await getTree();
|
|
741
|
+
|
|
742
|
+
// Default: expand all epics unless user specified expand flags
|
|
743
|
+
if (expandedIds === null) {
|
|
744
|
+
expandedIds = new Set();
|
|
745
|
+
items.forEach(item => {
|
|
746
|
+
if (item.type === 'epic') {
|
|
747
|
+
expandedIds.add(item.id);
|
|
748
|
+
}
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
printTree(items, '', true, expandedIds);
|
|
753
|
+
|
|
754
|
+
// Show legend and commands
|
|
755
|
+
console.log('');
|
|
756
|
+
console.log('Legend: ⊕ = collapsed ⊖ = expanded');
|
|
757
|
+
console.log('');
|
|
758
|
+
console.log('Commands:');
|
|
759
|
+
console.log(' jettypod work tree --expand=1 Show details for item #1');
|
|
760
|
+
console.log(' jettypod work tree --expand=1,2,3 Show details for multiple items');
|
|
761
|
+
console.log(' jettypod work tree --expand-all Show all details');
|
|
762
|
+
console.log(' jettypod work tree Collapse all (default)');
|
|
763
|
+
console.log('');
|
|
764
|
+
} catch (err) {
|
|
765
|
+
console.error(`Error displaying work tree: ${err.message}`);
|
|
766
|
+
process.exit(1);
|
|
767
|
+
}
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
case 'status': {
|
|
772
|
+
const id = parseInt(args[0]);
|
|
773
|
+
const status = args[1];
|
|
774
|
+
try {
|
|
775
|
+
await updateStatus(id, status);
|
|
776
|
+
} catch (err) {
|
|
777
|
+
console.error(`Error: ${err.message}`);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
break;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
case 'set-branch': {
|
|
784
|
+
const id = parseInt(args[0]);
|
|
785
|
+
const branch = args[1];
|
|
786
|
+
setBranch(id, branch);
|
|
787
|
+
break;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
case 'set-mode': {
|
|
791
|
+
const id = parseInt(args[0]);
|
|
792
|
+
const mode = args[1];
|
|
793
|
+
await setMode(id, mode);
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
case 'current': {
|
|
798
|
+
const id = parseInt(args[0]);
|
|
799
|
+
setCurrent(id);
|
|
800
|
+
break;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
case 'show': {
|
|
804
|
+
if (!args[0]) {
|
|
805
|
+
console.error('Error: Work item ID is required');
|
|
806
|
+
console.log('');
|
|
807
|
+
console.log('Usage: jettypod work show <id>');
|
|
808
|
+
console.log('');
|
|
809
|
+
console.log('Example:');
|
|
810
|
+
console.log(' jettypod work show 5');
|
|
811
|
+
console.log('');
|
|
812
|
+
console.log('💡 Tip: Use `jettypod work tree` to see all work items');
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
const id = parseInt(args[0]);
|
|
817
|
+
if (isNaN(id)) {
|
|
818
|
+
console.error(`Error: Invalid work item ID: ${args[0]}`);
|
|
819
|
+
console.log('ID must be a number');
|
|
820
|
+
process.exit(1);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
await showItem(id);
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
case 'describe': {
|
|
828
|
+
const id = parseInt(args[0]);
|
|
829
|
+
const description = args.slice(1).join(' ');
|
|
830
|
+
|
|
831
|
+
db.run(`UPDATE work_items SET description = ? WHERE id = ?`, [description, id], (err) => {
|
|
832
|
+
if (err) {
|
|
833
|
+
console.error(`Error: ${err.message}`);
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
console.log(`Updated #${id} description`);
|
|
837
|
+
});
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
case 'epic': {
|
|
842
|
+
const epicId = parseInt(args[0]);
|
|
843
|
+
await epicOverview(epicId);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
case 'completed': {
|
|
848
|
+
db.all(`
|
|
849
|
+
SELECT w.*,
|
|
850
|
+
p.title as parent_title, p.type as parent_type
|
|
851
|
+
FROM work_items w
|
|
852
|
+
LEFT JOIN work_items p ON w.parent_id = p.id
|
|
853
|
+
WHERE w.status = 'done'
|
|
854
|
+
ORDER BY w.id DESC
|
|
855
|
+
`, [], async (err, rows) => {
|
|
856
|
+
if (err) {
|
|
857
|
+
console.error(`Error fetching completed items: ${err.message}`);
|
|
858
|
+
process.exit(1);
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
862
|
+
console.log('✅ COMPLETED WORK');
|
|
863
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
864
|
+
|
|
865
|
+
if (!rows || rows.length === 0) {
|
|
866
|
+
console.log('No completed work items');
|
|
867
|
+
console.log('');
|
|
868
|
+
} else {
|
|
869
|
+
// Add epic info to each item
|
|
870
|
+
for (const row of rows) {
|
|
871
|
+
const epic = await findEpic(row.id);
|
|
872
|
+
row.epic_id = epic ? epic.id : null;
|
|
873
|
+
row.epic_title = epic ? epic.title : null;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
// Group by mode
|
|
877
|
+
const byMode = {
|
|
878
|
+
speed: [],
|
|
879
|
+
stable: [],
|
|
880
|
+
production: [],
|
|
881
|
+
other: []
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
rows.forEach(item => {
|
|
885
|
+
if (item.mode === 'speed') byMode.speed.push(item);
|
|
886
|
+
else if (item.mode === 'stable') byMode.stable.push(item);
|
|
887
|
+
else if (item.mode === 'production') byMode.production.push(item);
|
|
888
|
+
else byMode.other.push(item);
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
// Display Speed Mode section
|
|
892
|
+
if (byMode.speed.length > 0) {
|
|
893
|
+
console.log('\n⚡ SPEED MODE');
|
|
894
|
+
byMode.speed.forEach(item => {
|
|
895
|
+
const emoji = TYPE_EMOJIS[item.type] || '📋';
|
|
896
|
+
console.log(`${emoji} [#${item.id}] ${item.title}`);
|
|
897
|
+
// Show epic if parent is the epic, otherwise show parent
|
|
898
|
+
if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
|
|
899
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
900
|
+
} else if (item.parent_title) {
|
|
901
|
+
const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
|
|
902
|
+
console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
|
|
903
|
+
} else if (item.epic_title && item.epic_id !== item.id) {
|
|
904
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
905
|
+
}
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Display Stable Mode section
|
|
910
|
+
if (byMode.stable.length > 0) {
|
|
911
|
+
console.log('\n🔒 STABLE MODE');
|
|
912
|
+
byMode.stable.forEach(item => {
|
|
913
|
+
const emoji = TYPE_EMOJIS[item.type] || '📋';
|
|
914
|
+
console.log(`${emoji} [#${item.id}] ${item.title}`);
|
|
915
|
+
// Show epic if parent is the epic, otherwise show parent
|
|
916
|
+
if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
|
|
917
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
918
|
+
} else if (item.parent_title) {
|
|
919
|
+
const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
|
|
920
|
+
console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
|
|
921
|
+
} else if (item.epic_title && item.epic_id !== item.id) {
|
|
922
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
923
|
+
}
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Display Production Mode section
|
|
928
|
+
if (byMode.production.length > 0) {
|
|
929
|
+
console.log('\n🚀 PRODUCTION MODE');
|
|
930
|
+
byMode.production.forEach(item => {
|
|
931
|
+
const emoji = TYPE_EMOJIS[item.type] || '📋';
|
|
932
|
+
console.log(`${emoji} [#${item.id}] ${item.title}`);
|
|
933
|
+
// Show epic if parent is the epic, otherwise show parent
|
|
934
|
+
if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
|
|
935
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
936
|
+
} else if (item.parent_title) {
|
|
937
|
+
const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
|
|
938
|
+
console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
|
|
939
|
+
} else if (item.epic_title && item.epic_id !== item.id) {
|
|
940
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
941
|
+
}
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// Display Other/Untagged section
|
|
946
|
+
if (byMode.other.length > 0) {
|
|
947
|
+
console.log('\n📦 OTHER');
|
|
948
|
+
byMode.other.forEach(item => {
|
|
949
|
+
const emoji = TYPE_EMOJIS[item.type] || '📋';
|
|
950
|
+
const modeIndicator = item.mode ? ` [${item.mode}]` : '';
|
|
951
|
+
console.log(`${emoji} [#${item.id}] ${item.title}${modeIndicator}`);
|
|
952
|
+
// Show epic if parent is the epic, otherwise show parent
|
|
953
|
+
if (item.epic_title && item.epic_id !== item.id && item.parent_id === item.epic_id) {
|
|
954
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
955
|
+
} else if (item.parent_title) {
|
|
956
|
+
const parentEmoji = TYPE_EMOJIS[item.parent_type] || '📋';
|
|
957
|
+
console.log(` └─ Part of: ${parentEmoji} #${item.parent_id} ${item.parent_title}`);
|
|
958
|
+
} else if (item.epic_title && item.epic_id !== item.id) {
|
|
959
|
+
console.log(` └─ Epic: 🎯 #${item.epic_id} ${item.epic_title}`);
|
|
960
|
+
}
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
console.log('');
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
break;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
case 'epic-discover': {
|
|
971
|
+
const epicId = parseInt(args[0]);
|
|
972
|
+
|
|
973
|
+
if (!epicId || isNaN(epicId)) {
|
|
974
|
+
console.error('Error: Epic ID is required');
|
|
975
|
+
console.log('Usage: jettypod work epic-discover <epic-id>');
|
|
976
|
+
process.exit(1);
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
db.get(`SELECT * FROM work_items WHERE id = ?`, [epicId], (err, epic) => {
|
|
980
|
+
if (err) {
|
|
981
|
+
console.error(`Error: ${err.message}`);
|
|
982
|
+
process.exit(1);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (!epic) {
|
|
986
|
+
console.error(`Error: Epic #${epicId} not found`);
|
|
987
|
+
process.exit(1);
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (epic.type !== 'epic') {
|
|
991
|
+
console.error(`Error: Work item #${epicId} is not an epic (type: ${epic.type})`);
|
|
992
|
+
process.exit(1);
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
if (!epic.needs_discovery) {
|
|
996
|
+
console.error(`Error: Epic #${epicId} does not need discovery (needs_discovery=false)`);
|
|
997
|
+
console.log('');
|
|
998
|
+
console.log('Create epics with --needs-discovery flag if architectural decisions needed:');
|
|
999
|
+
console.log(' jettypod work create epic "Epic Title" "Description" --needs-discovery');
|
|
1000
|
+
process.exit(1);
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// Check if any decisions have been recorded
|
|
1004
|
+
db.all(
|
|
1005
|
+
`SELECT * FROM discovery_decisions WHERE work_item_id = ?`,
|
|
1006
|
+
[epicId],
|
|
1007
|
+
(err, decisions) => {
|
|
1008
|
+
if (err) {
|
|
1009
|
+
console.error(`Error: ${err.message}`);
|
|
1010
|
+
process.exit(1);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
if (decisions && decisions.length > 0) {
|
|
1014
|
+
console.log(`📋 Epic #${epicId} discovery decisions:`);
|
|
1015
|
+
console.log('');
|
|
1016
|
+
decisions.forEach((d) => {
|
|
1017
|
+
console.log(` ${d.aspect}: ${d.decision}`);
|
|
1018
|
+
console.log(` Rationale: ${d.rationale}`);
|
|
1019
|
+
console.log('');
|
|
1020
|
+
});
|
|
1021
|
+
console.log('💡 You can add more decisions for different aspects:');
|
|
1022
|
+
console.log(` jettypod work epic-implement ${epicId} --aspect="<type>" --decision="<approach>" --rationale="<why>"`);
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
console.log(`🔍 Starting epic discovery for #${epicId}: ${epic.title}`);
|
|
1027
|
+
console.log('');
|
|
1028
|
+
console.log(`Description: ${epic.description || 'No description'}`);
|
|
1029
|
+
console.log('');
|
|
1030
|
+
console.log('────────────────────────────────────────────────────');
|
|
1031
|
+
console.log('Epic Discovery Context (for Claude Code):');
|
|
1032
|
+
console.log('────────────────────────────────────────────────────');
|
|
1033
|
+
console.log(`Epic ID: ${epicId}`);
|
|
1034
|
+
console.log(`Title: ${epic.title}`);
|
|
1035
|
+
console.log(`Description: ${epic.description || 'Not provided'}`);
|
|
1036
|
+
console.log('Needs Discovery: true');
|
|
1037
|
+
console.log('');
|
|
1038
|
+
console.log('💬 Now ask Claude Code:');
|
|
1039
|
+
console.log(` "Help me with epic discovery for #${epicId}"`);
|
|
1040
|
+
console.log('');
|
|
1041
|
+
console.log('Claude will use the epic-discover skill to guide you through:');
|
|
1042
|
+
console.log(' 1. Feature brainstorming');
|
|
1043
|
+
console.log(' 2. Architectural decisions (if needed)');
|
|
1044
|
+
console.log(' 3. Prototype validation (optional)');
|
|
1045
|
+
console.log(' 4. Feature creation');
|
|
1046
|
+
console.log('');
|
|
1047
|
+
console.log('📋 The skill is at: .claude/skills/epic-discover/SKILL.md');
|
|
1048
|
+
}
|
|
1049
|
+
);
|
|
1050
|
+
});
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
case 'epic-implement': {
|
|
1055
|
+
const epicId = parseInt(args[0]);
|
|
1056
|
+
|
|
1057
|
+
if (!epicId || isNaN(epicId)) {
|
|
1058
|
+
console.error('Error: Epic ID is required');
|
|
1059
|
+
console.log('Usage: jettypod work epic-implement <epic-id> --aspect="<type>" --decision="<approach>" --rationale="<why>"');
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Parse --aspect, --decision, --rationale, and --prototypes flags
|
|
1064
|
+
const aspectIndex = args.findIndex(a => a.startsWith('--aspect='));
|
|
1065
|
+
const decisionIndex = args.findIndex(a => a.startsWith('--decision='));
|
|
1066
|
+
const rationaleIndex = args.findIndex(a => a.startsWith('--rationale='));
|
|
1067
|
+
const prototypesIndex = args.findIndex(a => a.startsWith('--prototypes='));
|
|
1068
|
+
|
|
1069
|
+
if (aspectIndex === -1 || decisionIndex === -1 || rationaleIndex === -1) {
|
|
1070
|
+
console.error('Error: --aspect, --decision, and --rationale are all required');
|
|
1071
|
+
console.log('');
|
|
1072
|
+
console.log('Usage: jettypod work epic-implement <epic-id> --aspect="<type>" --decision="<approach>" --rationale="<why>" [--prototypes="file1,file2"]');
|
|
1073
|
+
console.log('');
|
|
1074
|
+
console.log('Example:');
|
|
1075
|
+
console.log(' jettypod work epic-implement 5 \\');
|
|
1076
|
+
console.log(' --aspect="Architecture" \\');
|
|
1077
|
+
console.log(' --decision="WebSockets with Socket.io" \\');
|
|
1078
|
+
console.log(' --rationale="Bidirectional real-time updates needed, Socket.io provides fallbacks" \\');
|
|
1079
|
+
console.log(' --prototypes="prototypes/websockets.js,prototypes/sse.js"');
|
|
1080
|
+
console.log('');
|
|
1081
|
+
console.log('Common aspects: Architecture, Design Pattern, State Management, API Design, Testing Strategy');
|
|
1082
|
+
process.exit(1);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
const aspect = args[aspectIndex].split('=')[1].replace(/^["']|["']$/g, '');
|
|
1086
|
+
const decision = args[decisionIndex].split('=')[1].replace(/^["']|["']$/g, '');
|
|
1087
|
+
const rationale = args[rationaleIndex].split('=').slice(1).join('=').replace(/^["']|["']$/g, '');
|
|
1088
|
+
const prototypes = prototypesIndex !== -1
|
|
1089
|
+
? args[prototypesIndex].split('=')[1].replace(/^["']|["']$/g, '').split(',').map(p => p.trim())
|
|
1090
|
+
: [];
|
|
1091
|
+
|
|
1092
|
+
// Validate inputs
|
|
1093
|
+
if (!aspect || !decision || !rationale) {
|
|
1094
|
+
console.error('Error: --aspect, --decision, and --rationale must all have values');
|
|
1095
|
+
process.exit(1);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
if (aspect.trim().length === 0 || rationale.trim().length === 0) {
|
|
1099
|
+
console.error('Error: --aspect and --rationale cannot be empty or whitespace only');
|
|
1100
|
+
console.log('');
|
|
1101
|
+
console.log('Provide meaningful values for the decision aspect and rationale.');
|
|
1102
|
+
process.exit(1);
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Get and validate epic
|
|
1106
|
+
db.get(`SELECT * FROM work_items WHERE id = ?`, [epicId], (err, epic) => {
|
|
1107
|
+
if (err) {
|
|
1108
|
+
console.error(`Error: ${err.message}`);
|
|
1109
|
+
process.exit(1);
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
if (!epic) {
|
|
1113
|
+
console.error(`Error: Epic #${epicId} not found`);
|
|
1114
|
+
process.exit(1);
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
if (epic.type !== 'epic') {
|
|
1118
|
+
console.error(`Error: Work item #${epicId} is not an epic (type: ${epic.type})`);
|
|
1119
|
+
process.exit(1);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (!epic.needs_discovery) {
|
|
1123
|
+
console.error(`Error: Epic #${epicId} does not need discovery (needs_discovery=false)`);
|
|
1124
|
+
process.exit(1);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// Check if this aspect already has a decision
|
|
1128
|
+
db.get(
|
|
1129
|
+
`SELECT * FROM discovery_decisions WHERE work_item_id = ? AND aspect = ?`,
|
|
1130
|
+
[epicId, aspect],
|
|
1131
|
+
(err, existingDecision) => {
|
|
1132
|
+
if (err) {
|
|
1133
|
+
console.error(`Error: ${err.message}`);
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
if (existingDecision) {
|
|
1138
|
+
console.error(`Error: Epic #${epicId} already has a decision for aspect "${aspect}"`);
|
|
1139
|
+
console.log('');
|
|
1140
|
+
console.log(`Current decision: ${existingDecision.decision}`);
|
|
1141
|
+
console.log(`Rationale: ${existingDecision.rationale}`);
|
|
1142
|
+
console.log('');
|
|
1143
|
+
console.log('Use a different aspect name or update the database directly to change this decision.');
|
|
1144
|
+
process.exit(1);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1147
|
+
// Insert new discovery decision
|
|
1148
|
+
db.run(
|
|
1149
|
+
`INSERT INTO discovery_decisions (work_item_id, aspect, decision, rationale)
|
|
1150
|
+
VALUES (?, ?, ?, ?)`,
|
|
1151
|
+
[epicId, aspect, decision, rationale],
|
|
1152
|
+
(err) => {
|
|
1153
|
+
if (err) {
|
|
1154
|
+
console.error(`Error: ${err.message}`);
|
|
1155
|
+
process.exit(1);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// Update work_items with prototype tracking if provided
|
|
1159
|
+
if (prototypes.length > 0) {
|
|
1160
|
+
db.run(
|
|
1161
|
+
`UPDATE work_items SET prototype_files = ?, discovery_winner = ? WHERE id = ?`,
|
|
1162
|
+
[JSON.stringify(prototypes), decision, epicId],
|
|
1163
|
+
(err) => {
|
|
1164
|
+
if (err) {
|
|
1165
|
+
console.warn(`⚠️ Could not save prototypes: ${err.message}`);
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
console.log(`✅ Epic #${epicId} discovery decision recorded!`);
|
|
1172
|
+
console.log('');
|
|
1173
|
+
console.log(`Aspect: ${aspect}`);
|
|
1174
|
+
console.log(`Decision: ${decision}`);
|
|
1175
|
+
console.log(`Rationale: ${rationale}`);
|
|
1176
|
+
if (prototypes.length > 0) {
|
|
1177
|
+
console.log(`Prototypes: ${prototypes.join(', ')}`);
|
|
1178
|
+
}
|
|
1179
|
+
console.log('');
|
|
1180
|
+
console.log('📝 Architectural decision recorded');
|
|
1181
|
+
console.log('');
|
|
1182
|
+
|
|
1183
|
+
// Generate DECISIONS.md
|
|
1184
|
+
(async () => {
|
|
1185
|
+
try {
|
|
1186
|
+
const { generateDecisionsFile } = require('../../lib/decisions-generator');
|
|
1187
|
+
await generateDecisionsFile();
|
|
1188
|
+
console.log('📋 DECISIONS.md updated');
|
|
1189
|
+
console.log('');
|
|
1190
|
+
} catch (err) {
|
|
1191
|
+
console.warn('⚠️ Could not generate DECISIONS.md:', err.message);
|
|
1192
|
+
console.log('');
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
console.log('You can add more decisions for other aspects:');
|
|
1196
|
+
console.log(` jettypod work epic-implement ${epicId} --aspect="<type>" --decision="<approach>" --rationale="<why>"`);
|
|
1197
|
+
console.log('');
|
|
1198
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
1199
|
+
console.log('✨ Next Step: Create Features');
|
|
1200
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
1201
|
+
console.log('');
|
|
1202
|
+
console.log('Create features for this epic:');
|
|
1203
|
+
console.log(` jettypod work create feature "Feature Title" "Description" --parent=${epicId}`);
|
|
1204
|
+
console.log('');
|
|
1205
|
+
console.log('Then plan each feature:');
|
|
1206
|
+
console.log(' jettypod work discover <feature-id>');
|
|
1207
|
+
console.log('');
|
|
1208
|
+
console.log('💡 Tip: Claude Code will suggest UX approaches and generate BDD scenarios');
|
|
1209
|
+
})();
|
|
1210
|
+
}
|
|
1211
|
+
);
|
|
1212
|
+
}
|
|
1213
|
+
);
|
|
1214
|
+
});
|
|
1215
|
+
break;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
case 'implement': {
|
|
1219
|
+
const featureId = parseInt(args[0]);
|
|
1220
|
+
|
|
1221
|
+
if (!featureId || isNaN(featureId)) {
|
|
1222
|
+
console.error('Error: Feature ID is required');
|
|
1223
|
+
console.log('Usage: jettypod work implement <feature-id> [--prototypes="file1,file2"] [--winner="file.js"]');
|
|
1224
|
+
process.exit(1);
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
// Parse optional --prototypes and --winner flags
|
|
1228
|
+
const prototypesIndex = args.findIndex(a => a.startsWith('--prototypes='));
|
|
1229
|
+
const winnerIndex = args.findIndex(a => a.startsWith('--winner='));
|
|
1230
|
+
|
|
1231
|
+
const prototypes = prototypesIndex !== -1
|
|
1232
|
+
? args[prototypesIndex].split('=')[1].replace(/^["']|["']$/g, '').split(',').map(p => p.trim())
|
|
1233
|
+
: [];
|
|
1234
|
+
const winner = winnerIndex !== -1
|
|
1235
|
+
? args[winnerIndex].split('=')[1].replace(/^["']|["']$/g, '')
|
|
1236
|
+
: null;
|
|
1237
|
+
|
|
1238
|
+
db.get(`SELECT * FROM work_items WHERE id = ?`, [featureId], (err, feature) => {
|
|
1239
|
+
if (err) {
|
|
1240
|
+
console.error(`Error: ${err.message}`);
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
if (!feature) {
|
|
1245
|
+
console.error(`Error: Feature #${featureId} not found`);
|
|
1246
|
+
process.exit(1);
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (feature.type !== 'feature') {
|
|
1250
|
+
console.error(`Error: Work item #${featureId} is not a feature (type: ${feature.type})`);
|
|
1251
|
+
console.log('');
|
|
1252
|
+
console.log('The implement command transitions features from Discovery to Implementation phase.');
|
|
1253
|
+
console.log('Only features have phases.');
|
|
1254
|
+
process.exit(1);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (feature.phase !== 'discovery') {
|
|
1258
|
+
console.error(`Error: Feature #${featureId} is not in discovery phase (current phase: ${feature.phase || 'implementation'})`);
|
|
1259
|
+
console.log('');
|
|
1260
|
+
console.log('Features can only be transitioned to implementation from discovery phase.');
|
|
1261
|
+
process.exit(1);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
// Validate that BDD scenarios exist before transitioning
|
|
1265
|
+
if (!feature.scenario_file) {
|
|
1266
|
+
console.error(`Error: Feature #${featureId} has no BDD scenarios`);
|
|
1267
|
+
console.log('');
|
|
1268
|
+
console.log('Discovery is not complete without BDD scenarios.');
|
|
1269
|
+
console.log('');
|
|
1270
|
+
console.log('To complete discovery:');
|
|
1271
|
+
console.log(' 1. Generate scenarios using feature-discover skill');
|
|
1272
|
+
console.log(' 2. Or manually create scenarios at features/[feature-name].feature');
|
|
1273
|
+
console.log(' 3. Then run: jettypod work implement');
|
|
1274
|
+
console.log('');
|
|
1275
|
+
console.log('💡 Tip: Talk to Claude Code to complete feature discovery');
|
|
1276
|
+
process.exit(1);
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
// Check if scenario file actually exists on disk
|
|
1280
|
+
const fs = require('fs');
|
|
1281
|
+
const path = require('path');
|
|
1282
|
+
const scenarioPath = path.join(process.cwd(), feature.scenario_file);
|
|
1283
|
+
|
|
1284
|
+
if (!fs.existsSync(scenarioPath)) {
|
|
1285
|
+
console.error(`Error: Scenario file not found: ${feature.scenario_file}`);
|
|
1286
|
+
console.log('');
|
|
1287
|
+
console.log('The scenario file path is in the database, but the file doesn\'t exist.');
|
|
1288
|
+
console.log('');
|
|
1289
|
+
console.log('To fix this:');
|
|
1290
|
+
console.log(` 1. Create the file at: ${feature.scenario_file}`);
|
|
1291
|
+
console.log(' 2. Or run feature discovery again to regenerate scenarios');
|
|
1292
|
+
console.log('');
|
|
1293
|
+
console.log('💡 Tip: Talk to Claude Code to complete feature discovery');
|
|
1294
|
+
process.exit(1);
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
// Transition to implementation phase, set mode to speed
|
|
1298
|
+
const prototypeFilesValue = prototypes.length > 0 ? JSON.stringify(prototypes) : null;
|
|
1299
|
+
const winnerValue = winner || null;
|
|
1300
|
+
|
|
1301
|
+
db.run(
|
|
1302
|
+
`UPDATE work_items SET phase = 'implementation', mode = 'speed', prototype_files = ?, discovery_winner = ? WHERE id = ?`,
|
|
1303
|
+
[prototypeFilesValue, winnerValue, featureId],
|
|
1304
|
+
(err) => {
|
|
1305
|
+
if (err) {
|
|
1306
|
+
console.error(`Error: ${err.message}`);
|
|
1307
|
+
process.exit(1);
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
console.log('');
|
|
1311
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
1312
|
+
console.log(`✅ Feature #${featureId} transitioned to Implementation Phase`);
|
|
1313
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
1314
|
+
console.log('');
|
|
1315
|
+
console.log(`Title: ${feature.title}`);
|
|
1316
|
+
console.log('Phase: Implementation');
|
|
1317
|
+
console.log('Mode: Speed');
|
|
1318
|
+
if (prototypes.length > 0) {
|
|
1319
|
+
console.log(`Prototypes: ${prototypes.join(', ')}`);
|
|
1320
|
+
}
|
|
1321
|
+
if (winner) {
|
|
1322
|
+
console.log(`Winner: ${winner}`);
|
|
1323
|
+
}
|
|
1324
|
+
console.log('');
|
|
1325
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
1326
|
+
console.log('🚀 Speed Mode: Prove It Works');
|
|
1327
|
+
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
1328
|
+
console.log('');
|
|
1329
|
+
console.log('Build code that passes happy path scenarios.');
|
|
1330
|
+
console.log('Focus:');
|
|
1331
|
+
console.log(' • Happy path only');
|
|
1332
|
+
console.log(' • Single file when possible');
|
|
1333
|
+
console.log(' • localStorage for data');
|
|
1334
|
+
console.log(' • Basic try/catch');
|
|
1335
|
+
console.log('');
|
|
1336
|
+
console.log('⚠️ Speed Mode is a checkpoint - pass through quickly!');
|
|
1337
|
+
console.log('');
|
|
1338
|
+
console.log('Next: Elevate to Stable Mode');
|
|
1339
|
+
console.log(` jettypod work elevate ${featureId} stable`);
|
|
1340
|
+
console.log('');
|
|
1341
|
+
}
|
|
1342
|
+
);
|
|
1343
|
+
});
|
|
1344
|
+
break;
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
case 'discover': {
|
|
1348
|
+
const featureId = parseInt(args[0]);
|
|
1349
|
+
|
|
1350
|
+
if (!featureId || isNaN(featureId)) {
|
|
1351
|
+
console.error('Error: Feature ID is required');
|
|
1352
|
+
console.log('Usage: jettypod work discover <feature-id>');
|
|
1353
|
+
process.exit(1);
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
db.get(`SELECT * FROM work_items WHERE id = ?`, [featureId], (err, feature) => {
|
|
1357
|
+
if (err) {
|
|
1358
|
+
console.error(`Error: ${err.message}`);
|
|
1359
|
+
process.exit(1);
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if (!feature) {
|
|
1363
|
+
console.error(`Error: Feature #${featureId} not found`);
|
|
1364
|
+
process.exit(1);
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
if (feature.type !== 'feature') {
|
|
1368
|
+
console.error(`Error: Work item #${featureId} is not a feature (type: ${feature.type})`);
|
|
1369
|
+
process.exit(1);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// Get parent epic if exists
|
|
1373
|
+
db.get(`SELECT * FROM work_items WHERE id = ?`, [feature.parent_id || feature.epic_id], (err, epic) => {
|
|
1374
|
+
// Get epic's architectural decisions if epic exists
|
|
1375
|
+
if (epic && epic.type === 'epic') {
|
|
1376
|
+
db.all(
|
|
1377
|
+
`SELECT * FROM discovery_decisions WHERE work_item_id = ?`,
|
|
1378
|
+
[epic.id],
|
|
1379
|
+
(err, decisions) => {
|
|
1380
|
+
console.log(`✨ Starting feature discovery for #${featureId}: ${feature.title}`);
|
|
1381
|
+
console.log('');
|
|
1382
|
+
console.log(`Description: ${feature.description || 'No description'}`);
|
|
1383
|
+
console.log('');
|
|
1384
|
+
|
|
1385
|
+
if (epic) {
|
|
1386
|
+
console.log(`Epic: #${epic.id} ${epic.title}`);
|
|
1387
|
+
|
|
1388
|
+
if (decisions && decisions.length > 0) {
|
|
1389
|
+
console.log('');
|
|
1390
|
+
console.log('📋 Epic architectural decisions:');
|
|
1391
|
+
decisions.forEach((d) => {
|
|
1392
|
+
console.log(` • ${d.aspect}: ${d.decision}`);
|
|
1393
|
+
});
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
console.log('');
|
|
1398
|
+
console.log('────────────────────────────────────────────────────');
|
|
1399
|
+
console.log('Feature Discovery Context (for Claude Code):');
|
|
1400
|
+
console.log('────────────────────────────────────────────────────');
|
|
1401
|
+
console.log(`Feature ID: ${featureId}`);
|
|
1402
|
+
console.log(`Title: ${feature.title}`);
|
|
1403
|
+
console.log(`Description: ${feature.description || 'Not provided'}`);
|
|
1404
|
+
if (epic) {
|
|
1405
|
+
console.log(`Epic: #${epic.id} ${epic.title}`);
|
|
1406
|
+
if (decisions && decisions.length > 0) {
|
|
1407
|
+
console.log('Architectural Constraints:');
|
|
1408
|
+
decisions.forEach((d) => {
|
|
1409
|
+
console.log(` ${d.aspect}: ${d.decision}`);
|
|
1410
|
+
});
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
console.log('');
|
|
1414
|
+
console.log('💬 Now ask Claude Code:');
|
|
1415
|
+
console.log(` "Help me with feature discovery for #${featureId}"`);
|
|
1416
|
+
console.log('');
|
|
1417
|
+
console.log('Claude will use the feature-discover skill to guide you through:');
|
|
1418
|
+
console.log(' 1. Suggesting 3 UX approaches');
|
|
1419
|
+
console.log(' 2. Optional prototyping');
|
|
1420
|
+
console.log(' 3. Choosing the winner');
|
|
1421
|
+
console.log(' 4. Generating BDD scenarios');
|
|
1422
|
+
console.log(' 5. Transitioning to implementation');
|
|
1423
|
+
console.log('');
|
|
1424
|
+
console.log('📋 The skill is at: .claude/skills/feature-discover/SKILL.md');
|
|
1425
|
+
}
|
|
1426
|
+
);
|
|
1427
|
+
} else {
|
|
1428
|
+
console.log(`✨ Starting feature discovery for #${featureId}: ${feature.title}`);
|
|
1429
|
+
console.log('');
|
|
1430
|
+
console.log(`Description: ${feature.description || 'No description'}`);
|
|
1431
|
+
console.log('');
|
|
1432
|
+
console.log('────────────────────────────────────────────────────');
|
|
1433
|
+
console.log('Feature Discovery Context (for Claude Code):');
|
|
1434
|
+
console.log('────────────────────────────────────────────────────');
|
|
1435
|
+
console.log(`Feature ID: ${featureId}`);
|
|
1436
|
+
console.log(`Title: ${feature.title}`);
|
|
1437
|
+
console.log(`Description: ${feature.description || 'Not provided'}`);
|
|
1438
|
+
console.log('');
|
|
1439
|
+
console.log('💬 Now ask Claude Code:');
|
|
1440
|
+
console.log(` "Help me with feature discovery for #${featureId}"`);
|
|
1441
|
+
console.log('');
|
|
1442
|
+
console.log('Claude will use the feature-discover skill to guide you through:');
|
|
1443
|
+
console.log(' 1. Suggesting 3 UX approaches');
|
|
1444
|
+
console.log(' 2. Optional prototyping');
|
|
1445
|
+
console.log(' 3. Choosing the winner');
|
|
1446
|
+
console.log(' 4. Generating BDD scenarios');
|
|
1447
|
+
console.log(' 5. Transitioning to implementation');
|
|
1448
|
+
console.log('');
|
|
1449
|
+
console.log('📋 The skill is at: .claude/skills/feature-discover/SKILL.md');
|
|
1450
|
+
}
|
|
1451
|
+
});
|
|
1452
|
+
});
|
|
1453
|
+
break;
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
default:
|
|
1457
|
+
console.log(`
|
|
1458
|
+
JettyPod Work Tracking
|
|
1459
|
+
|
|
1460
|
+
Commands:
|
|
1461
|
+
jettypod work create <type> <title> [desc] [--parent=ID]
|
|
1462
|
+
Types: epic, feature, bug, chore
|
|
1463
|
+
|
|
1464
|
+
jettypod work tree
|
|
1465
|
+
Show hierarchical view (active items only)
|
|
1466
|
+
|
|
1467
|
+
jettypod work completed
|
|
1468
|
+
Show all completed work items
|
|
1469
|
+
|
|
1470
|
+
jettypod work show <id>
|
|
1471
|
+
Show work item details
|
|
1472
|
+
|
|
1473
|
+
jettypod work status <id> <status>
|
|
1474
|
+
Statuses: backlog, todo, in_progress, done, cancelled
|
|
1475
|
+
|
|
1476
|
+
jettypod work set-branch <id> <branch-name>
|
|
1477
|
+
Set branch for work item
|
|
1478
|
+
|
|
1479
|
+
jettypod work set-mode <id> <mode>
|
|
1480
|
+
Set mode (speed/discovery/stable/production)
|
|
1481
|
+
|
|
1482
|
+
jettypod work current <id>
|
|
1483
|
+
Set as current work item
|
|
1484
|
+
|
|
1485
|
+
jettypod work epic <id>
|
|
1486
|
+
Show epic overview
|
|
1487
|
+
|
|
1488
|
+
Examples:
|
|
1489
|
+
jettypod work create epic "Q1 Roadmap"
|
|
1490
|
+
jettypod work create feature "Auth" "" --parent=1
|
|
1491
|
+
jettypod work set-branch 2 feature/auth
|
|
1492
|
+
jettypod work set-mode 2 speed
|
|
1493
|
+
jettypod work current 2
|
|
1494
|
+
jettypod work show 2
|
|
1495
|
+
`);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// Don't explicitly close DB in test environments - let Node.js handle cleanup
|
|
1499
|
+
// This prevents FATAL errors when async operations are still pending
|
|
1500
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
1501
|
+
await closeDb();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
// Export for use in jettypod.js and other modules
|
|
1506
|
+
module.exports = { main, getCurrentWork, updateStatus, create };
|
|
1507
|
+
|
|
1508
|
+
// Run if called directly
|
|
1509
|
+
if (require.main === module) {
|
|
1510
|
+
main();
|
|
1511
|
+
}
|