jettypod 4.4.36 → 4.4.38

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/lib/database.js CHANGED
@@ -100,10 +100,16 @@ function getDb() {
100
100
  });
101
101
 
102
102
  // Enable WAL mode for better concurrency and avoid locking issues
103
- db.run('PRAGMA journal_mode = WAL');
104
- db.run('PRAGMA synchronous = NORMAL');
103
+ db.run('PRAGMA journal_mode = WAL', (err) => {
104
+ if (err) console.warn('PRAGMA journal_mode failed:', err.message);
105
+ });
106
+ db.run('PRAGMA synchronous = NORMAL', (err) => {
107
+ if (err) console.warn('PRAGMA synchronous failed:', err.message);
108
+ });
105
109
  // Reduce busy timeout to fail faster in tests
106
- db.run('PRAGMA busy_timeout = 5000');
110
+ db.run('PRAGMA busy_timeout = 5000', (err) => {
111
+ if (err) console.warn('PRAGMA busy_timeout failed:', err.message);
112
+ });
107
113
 
108
114
  initSchema();
109
115
  } catch (err) {
@@ -141,6 +147,7 @@ function initSchema() {
141
147
  phase TEXT,
142
148
  prototype_files TEXT,
143
149
  discovery_winner TEXT,
150
+ discovery_rationale TEXT,
144
151
  scenario_file TEXT,
145
152
  completed_at TEXT,
146
153
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP
@@ -196,29 +203,30 @@ function initSchema() {
196
203
  }
197
204
  });
198
205
 
199
- // Migrations: Add columns if they don't exist
200
- // Note: ALTER TABLE errors are expected if column exists, so we silently ignore them
201
- db.run(`ALTER TABLE work_items ADD COLUMN branch_name TEXT`, () => {});
202
- db.run(`ALTER TABLE work_items ADD COLUMN file_paths TEXT`, () => {});
203
- db.run(`ALTER TABLE work_items ADD COLUMN commit_sha TEXT`, () => {});
204
- db.run(`ALTER TABLE work_items ADD COLUMN mode TEXT`, () => {});
205
- db.run(`ALTER TABLE work_items ADD COLUMN current INTEGER DEFAULT 0`, () => {});
206
- db.run(`ALTER TABLE work_items ADD COLUMN needs_discovery INTEGER DEFAULT 0`, () => {});
207
- // NOTE: phase column is handled by migration 006-feature-phase-field.js
208
- // Do NOT add it here - the migration includes important data migration logic
209
- // NOTE: completed_at column is handled by migration 011-add-completed-at.js
210
- // Do NOT add it here - the migration system handles it properly
211
- // NOTE: worktree_path column is handled by migration 012-add-worktree-path.js
212
- db.run(`ALTER TABLE work_items ADD COLUMN worktree_path TEXT`, () => {});
213
- db.run(`ALTER TABLE work_items ADD COLUMN architectural_decision TEXT`, async () => {
206
+ // Migrations: Add columns if they don't exist (for databases created before these columns were in base schema)
207
+ const handleAlterError = (columnName) => (err) => {
208
+ if (err && !err.message.includes('duplicate column')) {
209
+ console.warn(`ALTER TABLE ADD COLUMN ${columnName} failed:`, err.message);
210
+ }
211
+ };
212
+ db.run(`ALTER TABLE work_items ADD COLUMN branch_name TEXT`, handleAlterError('branch_name'));
213
+ db.run(`ALTER TABLE work_items ADD COLUMN file_paths TEXT`, handleAlterError('file_paths'));
214
+ db.run(`ALTER TABLE work_items ADD COLUMN commit_sha TEXT`, handleAlterError('commit_sha'));
215
+ db.run(`ALTER TABLE work_items ADD COLUMN mode TEXT`, handleAlterError('mode'));
216
+ db.run(`ALTER TABLE work_items ADD COLUMN current INTEGER DEFAULT 0`, handleAlterError('current'));
217
+ db.run(`ALTER TABLE work_items ADD COLUMN needs_discovery INTEGER DEFAULT 0`, handleAlterError('needs_discovery'));
218
+ db.run(`ALTER TABLE work_items ADD COLUMN worktree_path TEXT`, handleAlterError('worktree_path'));
219
+ db.run(`ALTER TABLE work_items ADD COLUMN discovery_rationale TEXT`, handleAlterError('discovery_rationale'));
220
+ db.run(`ALTER TABLE work_items ADD COLUMN architectural_decision TEXT`, async (err) => {
221
+ handleAlterError('architectural_decision')(err);
214
222
  // Run data migrations after all schema operations complete (skip in test environments)
215
223
  if (process.env.NODE_ENV !== 'test') {
216
224
  try {
217
225
  await runMigrations(db);
218
226
  resolve();
219
227
  } catch (err) {
220
- // Silently ignore migration errors but still resolve - don't block startup
221
- resolve();
228
+ console.error('Migration failed:', err.message);
229
+ reject(err);
222
230
  }
223
231
  } else {
224
232
  resolve();
@@ -228,6 +236,38 @@ function initSchema() {
228
236
  });
229
237
  }
230
238
 
239
+ /**
240
+ * Validate that all expected columns exist in work_items table
241
+ * @param {sqlite3.Database} database - Database connection to validate
242
+ * @returns {Promise<void>} Resolves if valid, rejects with missing columns
243
+ */
244
+ function validateSchema(database) {
245
+ const expectedColumns = [
246
+ 'id', 'type', 'title', 'description', 'status', 'parent_id', 'epic_id',
247
+ 'branch_name', 'file_paths', 'commit_sha', 'mode', 'current', 'phase',
248
+ 'prototype_files', 'discovery_winner', 'discovery_rationale', 'scenario_file',
249
+ 'completed_at', 'created_at', 'needs_discovery', 'worktree_path', 'architectural_decision'
250
+ ];
251
+
252
+ return new Promise((resolve, reject) => {
253
+ database.all(`PRAGMA table_info(work_items)`, [], (err, rows) => {
254
+ if (err) {
255
+ return reject(new Error(`Failed to get schema info: ${err.message}`));
256
+ }
257
+
258
+ const existingColumns = rows.map(r => r.name);
259
+ const missingColumns = expectedColumns.filter(col => !existingColumns.includes(col));
260
+
261
+ if (missingColumns.length > 0) {
262
+ console.error('Missing columns in work_items table:', missingColumns.join(', '));
263
+ return reject(new Error(`Schema validation failed: missing columns: ${missingColumns.join(', ')}`));
264
+ }
265
+
266
+ resolve();
267
+ });
268
+ });
269
+ }
270
+
231
271
  /**
232
272
  * Close database connection
233
273
  * @returns {Promise<void>} Resolves when database is closed
@@ -292,6 +332,7 @@ module.exports = {
292
332
  getDbPath,
293
333
  getJettypodDir,
294
334
  waitForMigrations,
335
+ validateSchema,
295
336
  dbPath, // Deprecated: use getDbPath() for dynamic path
296
337
  jettypodDir // Deprecated: use getJettypodDir() for dynamic path
297
338
  };
package/lib/merge-lock.js CHANGED
@@ -115,18 +115,30 @@ function insertLock(db, workItemId, lockedBy) {
115
115
  * Clean up stale locks that are older than the threshold
116
116
  *
117
117
  * @param {Object} db - SQLite database connection
118
- * @param {number} staleThresholdMs - Age threshold in milliseconds (default: 120000 = 2 minutes)
119
- * @returns {Promise<void>}
118
+ * @param {number|Object} staleThresholdOrOptions - Age threshold in milliseconds (default: 120000), or options object with staleThreshold
119
+ * @returns {Promise<number>} Count of locks removed
120
120
  */
121
- function cleanupStaleLocks(db, staleThresholdMs = 120000) {
121
+ function cleanupStaleLocks(db, staleThresholdOrOptions = 120000) {
122
+ if (!db) {
123
+ return Promise.reject(new Error('Database connection required'));
124
+ }
125
+
126
+ // Support both number (old API) and options object (new API)
127
+ let staleThresholdMs;
128
+ if (typeof staleThresholdOrOptions === 'object') {
129
+ staleThresholdMs = staleThresholdOrOptions.staleThreshold || 120000;
130
+ } else {
131
+ staleThresholdMs = staleThresholdOrOptions;
132
+ }
133
+
122
134
  return new Promise((resolve, reject) => {
123
135
  db.run(
124
136
  `DELETE FROM merge_locks
125
137
  WHERE (julianday('now') - julianday(locked_at)) * 86400000 > ?`,
126
138
  [staleThresholdMs],
127
- (err) => {
139
+ function(err) {
128
140
  if (err) return reject(err);
129
- resolve();
141
+ resolve(this.changes); // Return count of deleted locks
130
142
  }
131
143
  );
132
144
  });
@@ -179,33 +191,6 @@ function createLockHandle(db, lockId, lockedBy, workItemId) {
179
191
  };
180
192
  }
181
193
 
182
- /**
183
- * Clean up stale locks older than threshold
184
- *
185
- * @param {Object} db - SQLite database connection
186
- * @param {Object} options - Optional configuration
187
- * @param {number} options.staleThreshold - Age threshold in ms (default: 300000 = 5 minutes)
188
- * @returns {Promise<number>} Count of locks removed
189
- */
190
- async function cleanupStaleLocks(db, options = {}) {
191
- if (!db) {
192
- throw new Error('Database connection required');
193
- }
194
-
195
- const staleThreshold = options.staleThreshold || 300000; // 5 minutes default
196
- const staleTime = new Date(Date.now() - staleThreshold).toISOString();
197
-
198
- return new Promise((resolve, reject) => {
199
- db.run(
200
- 'DELETE FROM merge_locks WHERE locked_at < ?',
201
- [staleTime],
202
- function(err) {
203
- if (err) reject(err);
204
- else resolve(this.changes); // Return count of deleted locks
205
- }
206
- );
207
- });
208
- }
209
194
 
210
195
  /**
211
196
  * Sleep for specified milliseconds
@@ -0,0 +1,66 @@
1
+ // Preflight Validator - validates prerequisites before workflow transitions
2
+ // Speed mode implementation - checks scenario_file and chores exist
3
+
4
+ const { getDb } = require('./database');
5
+
6
+ /**
7
+ * Validate feature prerequisites before transitioning to implementation
8
+ * @param {number} featureId - Feature ID to validate
9
+ * @returns {Promise<{success: boolean, failures: Array<{check: string, remediation: string}>}>}
10
+ */
11
+ function validateFeatureToImplement(featureId) {
12
+ return new Promise((resolve, reject) => {
13
+ const db = getDb();
14
+ const failures = [];
15
+
16
+ // Get feature details
17
+ db.get(
18
+ 'SELECT id, title, type, phase, scenario_file FROM work_items WHERE id = ?',
19
+ [featureId],
20
+ (err, feature) => {
21
+ if (err) {
22
+ return reject(new Error(`Failed to get feature: ${err.message}`));
23
+ }
24
+
25
+ if (!feature) {
26
+ return reject(new Error(`Feature #${featureId} not found`));
27
+ }
28
+
29
+ // Check 1: scenario_file must be set
30
+ if (!feature.scenario_file) {
31
+ failures.push({
32
+ check: 'scenario_file',
33
+ remediation: 'Use the feature-planning skill to generate BDD scenarios first.\n Invoke skill: feature-planning'
34
+ });
35
+ }
36
+
37
+ // Check 2: chores must exist
38
+ db.all(
39
+ 'SELECT id, title FROM work_items WHERE parent_id = ? AND type = ?',
40
+ [featureId, 'chore'],
41
+ (err, chores) => {
42
+ if (err) {
43
+ return reject(new Error(`Failed to get chores: ${err.message}`));
44
+ }
45
+
46
+ if (!chores || chores.length === 0) {
47
+ failures.push({
48
+ check: 'chores_exist',
49
+ remediation: 'Complete feature planning to generate speed mode chores.\n Invoke skill: feature-planning'
50
+ });
51
+ }
52
+
53
+ resolve({
54
+ success: failures.length === 0,
55
+ failures: failures
56
+ });
57
+ }
58
+ );
59
+ }
60
+ );
61
+ });
62
+ }
63
+
64
+ module.exports = {
65
+ validateFeatureToImplement
66
+ };
@@ -11,7 +11,7 @@ async function readStandards() {
11
11
 
12
12
  // Check file exists
13
13
  if (!fs.existsSync(standardsPath)) {
14
- throw new Error('Production standards not found');
14
+ throw new Error('Production standards file not found');
15
15
  }
16
16
 
17
17
  // Read file with specific error handling
@@ -59,7 +59,7 @@ async function readStandards() {
59
59
  function validateStandardsFormat(standards) {
60
60
  // Validate standards object exists
61
61
  if (!standards) {
62
- throw new Error('Invalid standards format: Standards object is null or undefined');
62
+ throw new Error('Standards object is null or undefined');
63
63
  }
64
64
 
65
65
  // Validate required top-level fields
@@ -98,7 +98,7 @@ function validateStandardsFormat(standards) {
98
98
  }
99
99
 
100
100
  if (!standard.acceptance) {
101
- throw new Error(`Invalid standard format: missing acceptance field`);
101
+ throw new Error(`Standard ${standard.id} missing required field: acceptance`);
102
102
  }
103
103
 
104
104
  // Validate infrastructure-scoped standards have all required fields
@@ -55,7 +55,7 @@ async function startWork(id) {
55
55
  // Get work item
56
56
  db.get(`
57
57
  SELECT w.*,
58
- p.title as parent_title, p.id as parent_id, p.scenario_file as parent_scenario_file, p.mode as parent_mode,
58
+ p.title as parent_title, p.id as parent_id, p.scenario_file as parent_scenario_file, p.mode as parent_mode, p.phase as parent_phase, p.type as parent_type,
59
59
  e.title as epic_title, e.id as epic_id
60
60
  FROM work_items w
61
61
  LEFT JOIN work_items p ON w.parent_id = p.id
@@ -101,11 +101,31 @@ async function startWork(id) {
101
101
  errorMsg += ` jettypod work start ${c.id} # ${c.title}\n`;
102
102
  });
103
103
  } else if (inProgressChores.length === 0) {
104
- errorMsg += `No chores available. Create chores first or check if feature is complete.`;
104
+ const doneChores = chores.filter(c => c.status === 'done');
105
+ if (doneChores.length === chores.length) {
106
+ errorMsg += `All chores are complete. This feature is done.\n`;
107
+ errorMsg += `Use: jettypod work status ${id} done # to mark the feature as done`;
108
+ } else {
109
+ errorMsg += `No chores available to start.\n`;
110
+ errorMsg += `Use: jettypod backlog # to see work item status`;
111
+ }
105
112
  }
106
113
  } else {
107
- errorMsg += `This feature has no chores yet. Use the feature-planning skill to plan it:\n`;
108
- errorMsg += ` Tell Claude: "Help me plan feature #${id}"`;
114
+ // No chores - check feature phase to give accurate guidance
115
+ if (workItem.status === 'done') {
116
+ errorMsg += `This feature is already done. No action needed.`;
117
+ } else if (workItem.phase === 'implementation') {
118
+ // Feature is in implementation but has no chores - unusual state
119
+ errorMsg += `This feature is in implementation phase but has no chores.\n`;
120
+ errorMsg += `The feature-planning skill should have generated chores.\n\n`;
121
+ errorMsg += `To regenerate chores, use the feature-planning skill:\n`;
122
+ errorMsg += ` Invoke skill: feature-planning`;
123
+ } else {
124
+ // Feature needs planning
125
+ errorMsg += `This feature needs planning first.\n\n`;
126
+ errorMsg += `Use the feature-planning skill to plan it:\n`;
127
+ errorMsg += ` Invoke skill: feature-planning`;
128
+ }
109
129
  }
110
130
 
111
131
  return reject(new Error(errorMsg));
@@ -116,7 +136,19 @@ async function startWork(id) {
116
136
 
117
137
  // Prevent starting epics directly
118
138
  if (workItem.type === 'epic') {
119
- return reject(new Error(`Cannot start epic #${id} directly. Start a feature or chore instead.\n\nUse: jettypod backlog # to see available work items`));
139
+ return reject(new Error(`Cannot start epic #${id} directly.\n\nTo plan this epic's features, use the epic-planning skill:\n Invoke skill: epic-planning\n\nOr start an existing feature/chore:\n jettypod backlog # to see available work items`));
140
+ }
141
+
142
+ // Validate chores have their parent feature in implementation phase
143
+ if (workItem.type === 'chore' && workItem.parent_type === 'feature' && workItem.parent_phase !== 'implementation') {
144
+ return reject(new Error(
145
+ `Cannot start chore - feature not in implementation phase\n\n` +
146
+ `Chore #${id}: ${workItem.title}\n` +
147
+ `Parent Feature #${workItem.parent_id}: ${workItem.parent_title}\n` +
148
+ ` Phase: ${workItem.parent_phase || 'discovery'}\n\n` +
149
+ `To transition the feature to implementation phase:\n` +
150
+ ` jettypod work implement ${workItem.parent_id} --winner="..." --rationale="..."`
151
+ ));
120
152
  }
121
153
 
122
154
  // Update status to in_progress if currently todo
@@ -76,8 +76,8 @@ function create(type, title, description = '', parentId = null, mode = null, nee
76
76
  return continueWithValidatedParent(null);
77
77
  }
78
78
 
79
- // Check parent's mode and phase
80
- db.get('SELECT id, title, type, mode, phase, epic_id FROM work_items WHERE id = ?', [parentId], (err, parent) => {
79
+ // Check parent's mode, phase, and scenario_file
80
+ db.get('SELECT id, title, type, mode, phase, scenario_file, epic_id FROM work_items WHERE id = ?', [parentId], (err, parent) => {
81
81
  if (err) {
82
82
  return reject(new Error(`Failed to validate parent: ${err.message}`));
83
83
  }
@@ -90,19 +90,19 @@ function create(type, title, description = '', parentId = null, mode = null, nee
90
90
  return reject(new Error(`Chores can only be created under features or epics. Parent #${parentId} is a ${parent.type}`));
91
91
  }
92
92
 
93
- // For chores under features, validate mode exists
94
- // Chores under epics are standalone and don't require mode
95
- if (parent.type === 'feature' && !parent.mode) {
93
+ // For chores under features, validate prerequisites exist
94
+ // Allow chores in discovery phase if scenario_file is set (feature-planning creates chores before work implement)
95
+ // Otherwise require mode to be set
96
+ if (parent.type === 'feature' && !parent.mode && !parent.scenario_file) {
96
97
  return reject(new Error(
97
- `Cannot create chore for feature without a mode.\n\n` +
98
+ `Cannot create chore for feature without BDD scenarios.\n\n` +
98
99
  `Feature #${parent.id} "${parent.title}"\n` +
99
100
  ` Phase: ${parent.phase || 'not set'}\n` +
100
101
  ` Mode: ${parent.mode || 'not set'}\n` +
101
102
  ` Scenario file: ${parent.scenario_file || 'not set'}\n\n` +
102
- `Chores can only be created after the feature has transitioned to implementation\n` +
103
- `with a mode (speed/stable/production).\n\n` +
104
- `To fix this, run:\n` +
105
- ` jettypod work implement ${parent.id} --scenario-file="features/[feature].feature" --winner="..." --rationale="..."`
103
+ `Chores can only be created after BDD scenarios are generated.\n\n` +
104
+ `Use the feature-planning skill to generate scenarios and chores:\n` +
105
+ ` Invoke skill: feature-planning`
106
106
  ));
107
107
  }
108
108
 
@@ -1077,16 +1077,14 @@ async function main() {
1077
1077
  console.log('🎯 Plan this epic now?');
1078
1078
  console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
1079
1079
  console.log('');
1080
- console.log('Ask Claude Code:');
1081
- console.log(` "Help me plan epic #${newId}"`);
1080
+ console.log('Use the epic-planning skill:');
1081
+ console.log(' Invoke skill: epic-planning');
1082
1082
  console.log('');
1083
- console.log('Claude will help you:');
1083
+ console.log('The skill will guide you to:');
1084
1084
  console.log(' • Brainstorm features for this epic');
1085
1085
  console.log(' • Identify architectural decisions (if needed)');
1086
1086
  console.log(' • Create features automatically');
1087
1087
  console.log('');
1088
- console.log('Or run: jettypod work epic-planning ' + newId);
1089
- console.log('');
1090
1088
  console.log('💡 You can also plan later when ready');
1091
1089
  }
1092
1090
 
@@ -1101,10 +1099,8 @@ async function main() {
1101
1099
  console.log('');
1102
1100
  console.log('💡 Tip: Consider planning this epic first');
1103
1101
  console.log('');
1104
- console.log('Ask Claude Code:');
1105
- console.log(` "Help me plan epic #${parentId}"`);
1106
- console.log('');
1107
- console.log(`Or run: jettypod work epic-planning ${parentId}`);
1102
+ console.log('Use the epic-planning skill:');
1103
+ console.log(' Invoke skill: epic-planning');
1108
1104
  }
1109
1105
  });
1110
1106
  }
@@ -1831,16 +1827,14 @@ async function main() {
1831
1827
  console.log(`Description: ${epic.description || 'Not provided'}`);
1832
1828
  console.log('Needs Discovery: true');
1833
1829
  console.log('');
1834
- console.log('💬 Now ask Claude Code:');
1835
- console.log(` "Help me with epic discovery for #${epicId}"`);
1830
+ console.log('Use the epic-planning skill:');
1831
+ console.log(' Invoke skill: epic-planning');
1836
1832
  console.log('');
1837
- console.log('Claude will use the epic-planning skill to guide you through:');
1833
+ console.log('The skill will guide you through:');
1838
1834
  console.log(' 1. Feature brainstorming');
1839
1835
  console.log(' 2. Architectural decisions (if needed)');
1840
1836
  console.log(' 3. Prototype validation (optional)');
1841
1837
  console.log(' 4. Feature creation');
1842
- console.log('');
1843
- console.log('📋 The skill is at: .claude/skills/epic-planning/SKILL.md');
1844
1838
  }
1845
1839
  );
1846
1840
  });
@@ -2012,6 +2006,9 @@ async function main() {
2012
2006
  }
2013
2007
 
2014
2008
  case 'implement': {
2009
+ // Ensure migrations are complete before accessing database
2010
+ await waitForMigrations();
2011
+
2015
2012
  const featureId = parseInt(args[0]);
2016
2013
 
2017
2014
  if (!featureId || isNaN(featureId)) {
@@ -2063,14 +2060,15 @@ async function main() {
2063
2060
 
2064
2061
  // Validate that BDD scenarios exist (either from flag or database)
2065
2062
  if (!scenarioFile) {
2066
- console.error(`Error: Feature #${featureId} has no BDD scenarios`);
2063
+ console.error(`Error: Cannot implement - no BDD scenarios`);
2067
2064
  console.log('');
2068
2065
  console.log('Discovery is not complete without BDD scenarios.');
2069
2066
  console.log('');
2070
- console.log('To complete discovery, provide the scenario file:');
2071
- console.log(` jettypod work implement ${featureId} --scenario-file="features/[feature-name].feature" --winner="..." --rationale="..."`);
2067
+ console.log('Use the feature-planning skill to generate BDD scenarios:');
2068
+ console.log(' Invoke skill: feature-planning');
2072
2069
  console.log('');
2073
- console.log('Or generate scenarios using the feature-planning skill first.');
2070
+ console.log('Or provide the scenario file directly:');
2071
+ console.log(` jettypod work implement ${featureId} --scenario-file="features/[feature-name].feature" --winner="..." --rationale="..."`);
2074
2072
  process.exit(1);
2075
2073
  }
2076
2074
 
@@ -2090,79 +2088,126 @@ async function main() {
2090
2088
  process.exit(1);
2091
2089
  }
2092
2090
 
2093
- // Determine if this is a transition or an update
2094
- // Also handle the case where phase is implementation but mode is not set (partial state)
2095
- const isTransition = feature.phase === 'discovery' || !feature.mode;
2096
- const isUpdate = feature.phase === 'implementation' && feature.mode;
2097
-
2098
- // Prepare values
2099
- const prototypeFilesValue = prototypes.length > 0 ? JSON.stringify(prototypes) : null;
2100
- const winnerValue = winner || null;
2101
- const rationaleValue = rationale || null;
2102
- const scenarioFileValue = scenarioFile; // Already validated above
2103
-
2104
- // Update query: if transitioning, set phase, mode, and scenario_file; if updating, just update decision fields
2105
- let updateSql;
2106
- let updateParams;
2107
-
2108
- if (isTransition) {
2109
- // When transitioning to implementation, ensure status is 'todo' (ready for work)
2110
- // This prevents status from being left as NULL or in a discovery-related state
2111
- updateSql = `UPDATE work_items SET phase = 'implementation', mode = 'speed', status = 'todo', scenario_file = ?, prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
2112
- updateParams = [scenarioFileValue, prototypeFilesValue, winnerValue, rationaleValue, featureId];
2113
- } else {
2114
- updateSql = `UPDATE work_items SET prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
2115
- updateParams = [prototypeFilesValue, winnerValue, rationaleValue, featureId];
2116
- }
2117
-
2118
- db.run(
2119
- updateSql,
2120
- updateParams,
2121
- (err) => {
2091
+ // Check if chores exist - required before transitioning to implementation
2092
+ db.all(
2093
+ 'SELECT id, title, status FROM work_items WHERE parent_id = ? AND type = ? ORDER BY created_at ASC',
2094
+ [featureId, 'chore'],
2095
+ (err, chores) => {
2122
2096
  if (err) {
2123
- console.error(`Error: ${err.message}`);
2097
+ console.error(`Error checking chores: ${err.message}`);
2124
2098
  process.exit(1);
2125
2099
  }
2126
2100
 
2127
- console.log('');
2128
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2101
+ if (!chores || chores.length === 0) {
2102
+ console.error(`Error: Cannot implement - no chores exist`);
2103
+ console.log('');
2104
+ console.log('Feature planning must generate speed mode chores before transitioning to implementation.');
2105
+ console.log('');
2106
+ console.log('Complete feature planning first:');
2107
+ console.log(' Invoke skill: feature-planning');
2108
+ process.exit(1);
2109
+ }
2110
+
2111
+ // Get first chore for auto-start
2112
+ const firstChore = chores[0];
2113
+
2114
+ // Determine if this is a transition or an update
2115
+ // Also handle the case where phase is implementation but mode is not set (partial state)
2116
+ const isTransition = feature.phase === 'discovery' || !feature.mode;
2117
+ const isUpdate = feature.phase === 'implementation' && feature.mode;
2118
+
2119
+ // Prepare values
2120
+ const prototypeFilesValue = prototypes.length > 0 ? JSON.stringify(prototypes) : null;
2121
+ const winnerValue = winner || null;
2122
+ const rationaleValue = rationale || null;
2123
+ const scenarioFileValue = scenarioFile; // Already validated above
2124
+
2125
+ // Update query: if transitioning, set phase, mode, and scenario_file; if updating, just update decision fields
2126
+ let updateSql;
2127
+ let updateParams;
2128
+
2129
2129
  if (isTransition) {
2130
- console.log(`✅ Feature #${featureId} transitioned to Implementation Phase`);
2130
+ // When transitioning to implementation, ensure status is 'todo' (ready for work)
2131
+ // This prevents status from being left as NULL or in a discovery-related state
2132
+ updateSql = `UPDATE work_items SET phase = 'implementation', mode = 'speed', status = 'todo', scenario_file = ?, prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
2133
+ updateParams = [scenarioFileValue, prototypeFilesValue, winnerValue, rationaleValue, featureId];
2131
2134
  } else {
2132
- console.log(`✅ Feature #${featureId} discovery decision updated`);
2135
+ updateSql = `UPDATE work_items SET prototype_files = ?, discovery_winner = ?, discovery_rationale = ? WHERE id = ?`;
2136
+ updateParams = [prototypeFilesValue, winnerValue, rationaleValue, featureId];
2133
2137
  }
2134
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2135
- console.log('');
2136
- console.log(`Title: ${feature.title}`);
2137
- console.log('Phase: Implementation');
2138
- console.log('Mode: Speed');
2139
- console.log(`Scenarios: ${scenarioFileValue}`);
2140
- if (prototypes.length > 0) {
2141
- console.log(`Prototypes: ${prototypes.join(', ')}`);
2142
- }
2143
- if (winner) {
2144
- console.log(`Winner: ${winner}`);
2145
- }
2146
- if (rationale) {
2147
- console.log(`Rationale: ${rationale}`);
2148
- }
2149
- console.log('');
2150
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2151
- console.log('🚀 Speed Mode: Prove It Works');
2152
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2153
- console.log('');
2154
- console.log('Build code that passes happy path scenarios.');
2155
- console.log('Focus:');
2156
- console.log(' • Happy path only');
2157
- console.log(' • Single file when possible');
2158
- console.log(' localStorage for data');
2159
- console.log(' • Basic try/catch');
2160
- console.log('');
2161
- console.log('⚠️ Speed Mode is a checkpoint - pass through quickly!');
2162
- console.log('');
2163
- console.log('Next: Elevate to Stable Mode');
2164
- console.log(` jettypod work elevate ${featureId} stable`);
2165
- console.log('');
2138
+
2139
+ db.run(
2140
+ updateSql,
2141
+ updateParams,
2142
+ (err) => {
2143
+ if (err) {
2144
+ console.error(`Error: ${err.message}`);
2145
+ process.exit(1);
2146
+ }
2147
+
2148
+ console.log('');
2149
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2150
+ if (isTransition) {
2151
+ console.log(`✅ Feature #${featureId} transitioned to Implementation Phase`);
2152
+ } else {
2153
+ console.log(`✅ Feature #${featureId} discovery decision updated`);
2154
+ }
2155
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2156
+ console.log('');
2157
+ console.log(`Title: ${feature.title}`);
2158
+ console.log('Phase: Implementation');
2159
+ console.log('Mode: Speed');
2160
+ console.log(`Scenarios: ${scenarioFileValue}`);
2161
+ if (prototypes.length > 0) {
2162
+ console.log(`Prototypes: ${prototypes.join(', ')}`);
2163
+ }
2164
+ if (winner) {
2165
+ console.log(`Winner: ${winner}`);
2166
+ }
2167
+ if (rationale) {
2168
+ console.log(`Rationale: ${rationale}`);
2169
+ }
2170
+
2171
+ // Auto-start first chore if transitioning
2172
+ if (isTransition && firstChore) {
2173
+ console.log('');
2174
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2175
+ console.log('🚀 Auto-starting first chore...');
2176
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2177
+
2178
+ // Import and call startWork
2179
+ const { startWork } = require('../work-commands');
2180
+ startWork(firstChore.id).then(() => {
2181
+ console.log('');
2182
+ console.log('✅ First chore started! Invoke skill: speed-mode');
2183
+ console.log('');
2184
+ }).catch((startErr) => {
2185
+ console.log('');
2186
+ console.log(`⚠️ Feature transitioned but failed to auto-start chore: ${startErr.message}`);
2187
+ console.log(` Manually start with: jettypod work start ${firstChore.id}`);
2188
+ console.log('');
2189
+ });
2190
+ } else {
2191
+ console.log('');
2192
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2193
+ console.log('🚀 Speed Mode: Prove It Works');
2194
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2195
+ console.log('');
2196
+ console.log('Build code that passes happy path scenarios.');
2197
+ console.log('Focus:');
2198
+ console.log(' • Happy path only');
2199
+ console.log(' • Single file when possible');
2200
+ console.log(' • localStorage for data');
2201
+ console.log(' • Basic try/catch');
2202
+ console.log('');
2203
+ console.log('⚠️ Speed Mode is a checkpoint - pass through quickly!');
2204
+ console.log('');
2205
+ console.log('Next: Elevate to Stable Mode');
2206
+ console.log(` jettypod work elevate ${featureId} stable`);
2207
+ console.log('');
2208
+ }
2209
+ }
2210
+ );
2166
2211
  }
2167
2212
  );
2168
2213
  });
@@ -2236,17 +2281,15 @@ async function main() {
2236
2281
  }
2237
2282
  }
2238
2283
  console.log('');
2239
- console.log('💬 Now ask Claude Code:');
2240
- console.log(` "Help me with feature discovery for #${featureId}"`);
2284
+ console.log('Use the feature-planning skill:');
2285
+ console.log(' Invoke skill: feature-planning');
2241
2286
  console.log('');
2242
- console.log('Claude will use the feature-planning skill to guide you through:');
2287
+ console.log('The skill will guide you through:');
2243
2288
  console.log(' 1. Suggesting 3 UX approaches');
2244
2289
  console.log(' 2. Optional prototyping');
2245
2290
  console.log(' 3. Choosing the winner');
2246
2291
  console.log(' 4. Generating BDD scenarios');
2247
2292
  console.log(' 5. Transitioning to implementation');
2248
- console.log('');
2249
- console.log('📋 The skill is at: .claude/skills/feature-planning/SKILL.md');
2250
2293
  }
2251
2294
  );
2252
2295
  } else {
@@ -2261,17 +2304,15 @@ async function main() {
2261
2304
  console.log(`Title: ${feature.title}`);
2262
2305
  console.log(`Description: ${feature.description || 'Not provided'}`);
2263
2306
  console.log('');
2264
- console.log('💬 Now ask Claude Code:');
2265
- console.log(` "Help me with feature discovery for #${featureId}"`);
2307
+ console.log('Use the feature-planning skill:');
2308
+ console.log(' Invoke skill: feature-planning');
2266
2309
  console.log('');
2267
- console.log('Claude will use the feature-planning skill to guide you through:');
2310
+ console.log('The skill will guide you through:');
2268
2311
  console.log(' 1. Suggesting 3 UX approaches');
2269
2312
  console.log(' 2. Optional prototyping');
2270
2313
  console.log(' 3. Choosing the winner');
2271
2314
  console.log(' 4. Generating BDD scenarios');
2272
2315
  console.log(' 5. Transitioning to implementation');
2273
- console.log('');
2274
- console.log('📋 The skill is at: .claude/skills/feature-planning/SKILL.md');
2275
2316
  }
2276
2317
  });
2277
2318
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.36",
3
+ "version": "4.4.38",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {