jettypod 3.0.2 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,37 @@
1
+ const { Given, Then } = require('@cucumber/cucumber');
2
+
3
+ // Test data setup
4
+ Given('I have work items in various statuses', async function () {
5
+ // Speed mode will implement: Create test work items with different statuses
6
+ // (backlog, todo, in_progress, done, cancelled)
7
+ this.testWorkItems = null; // Placeholder for speed mode
8
+ });
9
+
10
+ // Output assertions
11
+ Then('I see only active work items \\(backlog, todo, in_progress)', function () {
12
+ // Speed mode will implement: Parse output and verify only active items shown
13
+ // Check output contains items with status: backlog, todo, in_progress
14
+ // Check output does NOT contain items with status: done, cancelled
15
+ });
16
+
17
+ Then('I see all work items regardless of status', function () {
18
+ // Speed mode will implement: Parse output and verify all items shown
19
+ // Check output contains items with all statuses
20
+ });
21
+
22
+ Then('I see only completed work items \\(done, cancelled)', function () {
23
+ // Speed mode will implement: Parse output and verify only completed items shown
24
+ // Check output contains items with status: done, cancelled
25
+ // Check output does NOT contain items with status: backlog, todo, in_progress
26
+ });
27
+
28
+ Then('I see the items in a tree hierarchy', function () {
29
+ // Speed mode will implement: Verify tree structure in output
30
+ // Check for indentation or tree symbols (├──, └──, etc.)
31
+ // Check parent-child relationships are preserved
32
+ });
33
+
34
+ Then('completed items are hidden', function () {
35
+ // Speed mode will implement: Verify no completed items in output
36
+ // Check output does NOT contain items with status: done, cancelled
37
+ });
@@ -163,7 +163,7 @@ function startWork(id) {
163
163
  console.log('');
164
164
  console.log('💬 Claude Code is ready to guide you through feature discovery.');
165
165
  console.log('');
166
- console.log('📋 The feature-discover skill will automatically activate.');
166
+ console.log('📋 The feature-planning skill will automatically activate.');
167
167
  console.log('');
168
168
  }
169
169
 
@@ -279,14 +279,21 @@ function stopWork(newStatus = null) {
279
279
  return reject(new Error(`Database error: ${err.message}`));
280
280
  }
281
281
 
282
- try {
283
- clearCurrentWork();
284
- } catch (unlinkErr) {
285
- return reject(new Error(`Failed to remove current work file: ${unlinkErr.message}`));
286
- }
282
+ // Check if this completes all stable mode chores for the parent feature
283
+ checkStableModeCompletion(db, currentWork, newStatus, (completionErr) => {
284
+ if (completionErr) {
285
+ console.warn(`Warning: ${completionErr.message}`);
286
+ }
287
+
288
+ try {
289
+ clearCurrentWork();
290
+ } catch (unlinkErr) {
291
+ return reject(new Error(`Failed to remove current work file: ${unlinkErr.message}`));
292
+ }
287
293
 
288
- console.log(`Stopped work on #${currentWork.id}, status set to ${newStatus}`);
289
- resolve({ id: currentWork.id, status: newStatus });
294
+ console.log(`Stopped work on #${currentWork.id}, status set to ${newStatus}`);
295
+ resolve({ id: currentWork.id, status: newStatus });
296
+ });
290
297
  });
291
298
  } else {
292
299
  try {
@@ -301,6 +308,183 @@ function stopWork(newStatus = null) {
301
308
  });
302
309
  }
303
310
 
311
+ /**
312
+ * Check if stable mode is complete for a feature and trigger production chore generation
313
+ * @param {Object} db - Database connection
314
+ * @param {Object} currentWork - Current work item that was just completed
315
+ * @param {string} newStatus - Status that was just set
316
+ * @param {Function} callback - Callback function
317
+ */
318
+ async function checkStableModeCompletion(db, currentWork, newStatus, callback) {
319
+ // Only check if:
320
+ // 1. Current work is a chore
321
+ // 2. Status was just set to 'done'
322
+ // 3. Current work has a parent (feature)
323
+ if (currentWork.type !== 'chore' || newStatus !== 'done' || !currentWork.parent_id) {
324
+ return callback(null);
325
+ }
326
+
327
+ // Get parent feature details and check its mode
328
+ db.get(`
329
+ SELECT id, title, mode, type
330
+ FROM work_items
331
+ WHERE id = ?
332
+ `, [currentWork.parent_id], async (err, parent) => {
333
+ if (err) {
334
+ return callback(new Error(`Failed to get parent feature: ${err.message}`));
335
+ }
336
+
337
+ if (!parent) {
338
+ return callback(new Error(`Parent feature #${currentWork.parent_id} not found`));
339
+ }
340
+
341
+ // Only proceed if parent is a feature in stable mode
342
+ if (parent.type !== 'feature' || parent.mode !== 'stable') {
343
+ return callback(null);
344
+ }
345
+
346
+ // Check if all stable chores for this feature are done
347
+ db.get(`
348
+ SELECT COUNT(*) as incomplete_count
349
+ FROM work_items
350
+ WHERE parent_id = ?
351
+ AND type = 'chore'
352
+ AND mode = 'stable'
353
+ AND status != 'done'
354
+ `, [parent.id], async (countErr, result) => {
355
+ if (countErr) {
356
+ return callback(new Error(`Failed to check stable chore completion: ${countErr.message}`));
357
+ }
358
+
359
+ // If all stable chores are done, stable mode is complete
360
+ if (result.incomplete_count === 0) {
361
+ console.log('');
362
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
363
+ console.log('✅ STABLE MODE COMPLETE');
364
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
365
+ console.log('');
366
+ console.log(`All stable chores for feature #${parent.id} "${parent.title}" are done.`);
367
+ console.log('');
368
+
369
+ // Trigger production chore generation
370
+ try {
371
+ await generateAndConfirmProductionChores(parent);
372
+ } catch (genErr) {
373
+ console.error('Warning: Production chore generation failed:', genErr.message);
374
+ }
375
+ }
376
+
377
+ callback(null);
378
+ });
379
+ });
380
+ }
381
+
382
+ /**
383
+ * Generate production chores and get user confirmation
384
+ * @param {Object} feature - Feature object with id and title
385
+ */
386
+ async function generateAndConfirmProductionChores(feature) {
387
+ const { analyzeImplementation, generateProductionChores } = require('../../lib/production-chore-generator');
388
+ const readline = require('readline');
389
+
390
+ console.log('🔍 Analyzing implementation for production gaps...');
391
+ console.log('');
392
+
393
+ // Analyze implementation
394
+ let analysisResult;
395
+ try {
396
+ analysisResult = await analyzeImplementation(feature.id);
397
+ } catch (analysisErr) {
398
+ console.error(`Failed to analyze implementation: ${analysisErr.message}`);
399
+ return;
400
+ }
401
+
402
+ // Display warning if no git commits found
403
+ if (analysisResult.warning) {
404
+ console.log(`⚠️ ${analysisResult.warning}`);
405
+ console.log('');
406
+ }
407
+
408
+ console.log(`✅ Analyzed ${analysisResult.filesAnalyzed.length} implementation files`);
409
+ console.log('');
410
+
411
+ // Generate production chore proposals
412
+ const proposedChores = generateProductionChores(analysisResult, feature.title);
413
+
414
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
415
+ console.log('📋 PROPOSED PRODUCTION CHORES');
416
+ console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
417
+ console.log('');
418
+ console.log('These production chores will be created:');
419
+ console.log('');
420
+
421
+ proposedChores.forEach((chore, index) => {
422
+ console.log(`${index + 1}. ${chore.title}`);
423
+ console.log(` ${chore.description.split('\n')[0]}`);
424
+ console.log('');
425
+ });
426
+
427
+ // Get user confirmation
428
+ return new Promise((resolve) => {
429
+ const rl = readline.createInterface({
430
+ input: process.stdin,
431
+ output: process.stdout
432
+ });
433
+
434
+ rl.question('Create these production chores? (yes/skip): ', async (answer) => {
435
+ rl.close();
436
+
437
+ const response = answer.trim().toLowerCase();
438
+
439
+ if (response === 'yes' || response === 'y') {
440
+ console.log('');
441
+ console.log('Creating production chores...');
442
+
443
+ try {
444
+ await createProductionChoresAndElevate(feature, proposedChores);
445
+ console.log('');
446
+ console.log(`✅ Created ${proposedChores.length} production chores. Feature elevated to production mode.`);
447
+ console.log('');
448
+ } catch (createErr) {
449
+ console.error(`Failed to create production chores: ${createErr.message}`);
450
+ }
451
+ } else {
452
+ console.log('');
453
+ console.log('⏭️ Skipped production chore creation');
454
+ console.log(' You can generate them later by talking to Claude Code');
455
+ console.log('');
456
+ }
457
+
458
+ resolve();
459
+ });
460
+ });
461
+ }
462
+
463
+ /**
464
+ * Create production chores and elevate feature to production mode (if external)
465
+ * @param {Object} feature - Feature object
466
+ * @param {Array} proposedChores - Array of chore proposals
467
+ */
468
+ async function createProductionChoresAndElevate(feature, proposedChores) {
469
+ const { create } = require('../work-tracking');
470
+ const projectConfig = config.read();
471
+
472
+ // Create production chores
473
+ for (const chore of proposedChores) {
474
+ await create('chore', chore.title, chore.description, feature.id, 'production', false);
475
+ }
476
+
477
+ // Always elevate feature to production mode
478
+ // (Feature #611 controls visibility in backlog, not mode elevation)
479
+ const db = getDb();
480
+ await new Promise((resolve, reject) => {
481
+ db.run('UPDATE work_items SET mode = ? WHERE id = ?', ['production', feature.id], (err) => {
482
+ if (err) return reject(err);
483
+ resolve();
484
+ });
485
+ });
486
+ }
487
+
304
488
  // Re-export getCurrentWork from shared module for backwards compatibility
305
489
  // (used by jettypod.js)
306
490
 
@@ -22,19 +22,19 @@ Given('I have initialized jettypod', function () {
22
22
  execSync('git init', { stdio: 'pipe' });
23
23
  execSync('git config user.email "test@test.com"', { stdio: 'pipe' });
24
24
  execSync('git config user.name "Test"', { stdio: 'pipe' });
25
- execSync(`node ${path.join(originalDir, 'jettypod.js')} init`, { stdio: 'pipe' });
25
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} init`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
26
26
  });
27
27
 
28
28
  Given('I create an epic with id {int}', function (id) {
29
- execSync(`node ${path.join(originalDir, 'jettypod.js')} work create epic "Test Epic"`, { stdio: 'pipe' });
29
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} work create epic "Test Epic"`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
30
30
  });
31
31
 
32
32
  Given('I create a feature with id {int} under epic {int}', function (featureId, epicId) {
33
- execSync(`node ${path.join(originalDir, 'jettypod.js')} work create feature "Test Feature" "" --parent=${epicId}`, { stdio: 'pipe' });
33
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} work create feature "Test Feature" "" --parent=${epicId}`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
34
34
  });
35
35
 
36
36
  When('I start work on item {int}', function (id) {
37
- execSync(`node ${path.join(originalDir, 'jettypod.js')} work start ${id}`, { stdio: 'pipe' });
37
+ execSync(`node ${path.join(originalDir, 'jettypod.js')} work start ${id}`, { stdio: 'pipe', env: { ...process.env, NODE_ENV: 'test' } });
38
38
  });
39
39
 
40
40
  Then('the current work file exists', function () {
@@ -51,7 +51,7 @@ Then('the current work contains item {int}', function (id) {
51
51
  Then('item {int} status is in_progress or backlog', function (id) {
52
52
  // Use CLI to check status to avoid database connection issues
53
53
  try {
54
- const output = execSync(`node ${path.join(originalDir, 'jettypod.js')} work show ${id}`, { encoding: 'utf-8' });
54
+ const output = execSync(`node ${path.join(originalDir, 'jettypod.js')} work show ${id}`, { encoding: 'utf-8', env: { ...process.env, NODE_ENV: 'test' } });
55
55
  const statusMatch = output.match(/Status:\s+(\w+)/);
56
56
  if (!statusMatch) {
57
57
  throw new Error('Could not find status in output');
@@ -601,9 +601,9 @@ Given('I create a feature {string} with mode {string} and parent epic', function
601
601
  }
602
602
  });
603
603
 
604
- When('I view the work tree', function() {
604
+ When('I view the backlog', function() {
605
605
  testContext.lastOutput = execSync(
606
- `node ${path.join(__dirname, '../../jettypod.js')} work tree`,
606
+ `node ${path.join(__dirname, '../../jettypod.js')} backlog`,
607
607
  { cwd: testDir, encoding: 'utf-8' }
608
608
  );
609
609
  });