jettypod 4.4.44 → 4.4.46

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.
@@ -81,7 +81,9 @@ function KanbanCard({ item, epicTitle, showEpic = false }: KanbanCardProps) {
81
81
  {modeLabels[chore.mode].label}
82
82
  </span>
83
83
  )}
84
- <span className="text-zinc-700 dark:text-zinc-300 truncate">{chore.title}</span>
84
+ <span className="text-zinc-700 dark:text-zinc-300 truncate">
85
+ {chore.title || <span className="text-zinc-400 italic">(Untitled)</span>}
86
+ </span>
85
87
  </div>
86
88
  </Link>
87
89
  ))}
package/jettypod.js CHANGED
@@ -1303,6 +1303,23 @@ switch (command) {
1303
1303
  console.error(`Error: ${err.message}`);
1304
1304
  process.exit(1);
1305
1305
  }
1306
+ } else if (subcommand === 'tests') {
1307
+ // Create worktree for writing BDD tests
1308
+ const featureId = parseInt(args[1]);
1309
+ if (!featureId) {
1310
+ console.log('Usage: jettypod work tests <feature-id>');
1311
+ console.log('');
1312
+ console.log('Creates a worktree for writing BDD tests for a feature.');
1313
+ console.log('Tests are written in isolation, then merged with: jettypod work merge');
1314
+ process.exit(1);
1315
+ }
1316
+ const workCommands = require('./lib/work-commands/index.js');
1317
+ try {
1318
+ await workCommands.testsWork(featureId);
1319
+ } catch (err) {
1320
+ console.error(`Error: ${err.message}`);
1321
+ process.exit(1);
1322
+ }
1306
1323
  } else {
1307
1324
  // CRITICAL SAFETY CHECK: Prevent work status changes from within a worktree
1308
1325
  // This prevents cleanup failures and orphaned worktrees
@@ -2303,76 +2320,127 @@ Quick commands:
2303
2320
  const workflowSubcommand = args[0];
2304
2321
 
2305
2322
  if (workflowSubcommand === 'status') {
2323
+ // workflow status <work-item-id> - shows skill execution history for a work item
2324
+ const workItemId = args[1] ? parseInt(args[1]) : null;
2306
2325
  const { getDb, waitForMigrations } = require('./lib/database');
2307
- const { getCurrentBranch } = require('./lib/workflow-checkpoint');
2308
2326
 
2309
2327
  try {
2310
2328
  const db = getDb();
2311
2329
  await waitForMigrations();
2312
- const currentBranch = getCurrentBranch();
2313
2330
 
2314
- const checkpoints = await new Promise((resolve, reject) => {
2315
- db.all(
2316
- `SELECT * FROM workflow_checkpoints ORDER BY updated_at DESC`,
2317
- [],
2318
- (err, rows) => {
2319
- if (err) reject(err);
2320
- else resolve(rows || []);
2331
+ if (workItemId) {
2332
+ // Show skill executions for specific work item
2333
+ const executions = await new Promise((resolve, reject) => {
2334
+ db.all(
2335
+ `SELECT * FROM skill_executions WHERE work_item_id = ? ORDER BY started_at ASC`,
2336
+ [workItemId],
2337
+ (err, rows) => {
2338
+ if (err) reject(err);
2339
+ else resolve(rows || []);
2340
+ }
2341
+ );
2342
+ });
2343
+
2344
+ const gates = await new Promise((resolve, reject) => {
2345
+ db.all(
2346
+ `SELECT * FROM workflow_gates WHERE work_item_id = ?`,
2347
+ [workItemId],
2348
+ (err, rows) => {
2349
+ if (err) reject(err);
2350
+ else resolve(rows || []);
2351
+ }
2352
+ );
2353
+ });
2354
+
2355
+ console.log(`Workflow Status for #${workItemId}`);
2356
+ console.log('─'.repeat(50));
2357
+ console.log('');
2358
+ console.log('Skill Executions:');
2359
+ if (executions.length === 0) {
2360
+ console.log(' (none)');
2361
+ } else {
2362
+ for (const exec of executions) {
2363
+ console.log(` ${exec.skill_name}: ${exec.status}`);
2321
2364
  }
2322
- );
2323
- });
2365
+ }
2324
2366
 
2325
- if (checkpoints.length === 0) {
2326
- console.log('No interrupted workflows found.');
2327
- } else {
2328
- console.log('Interrupted Workflows');
2329
- console.log('─'.repeat(90));
2330
2367
  console.log('');
2331
- console.log(
2332
- 'ID'.padEnd(4) +
2333
- 'Skill'.padEnd(18) +
2334
- 'Step'.padEnd(10) +
2335
- 'Work Item'.padEnd(12) +
2336
- 'Branch'.padEnd(38) +
2337
- 'Last Active'
2338
- );
2339
- console.log('─'.repeat(90));
2340
-
2341
- for (const cp of checkpoints) {
2342
- const stepInfo = cp.total_steps
2343
- ? `${cp.current_step}/${cp.total_steps}`
2344
- : `${cp.current_step}`;
2345
- const isCurrent = cp.branch_name === currentBranch;
2346
- const branchDisplay = cp.branch_name.length > 35
2347
- ? cp.branch_name.substring(0, 32) + '...'
2348
- : cp.branch_name;
2349
- const branchWithMarker = branchDisplay + (isCurrent ? ' *' : '');
2350
-
2351
- // Format time ago
2352
- const updatedAt = new Date(cp.updated_at);
2353
- const now = new Date();
2354
- const diffMs = now - updatedAt;
2355
- const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
2356
- const diffDays = Math.floor(diffHours / 24);
2357
- const timeAgo = diffDays > 0 ? `${diffDays}d ago` : diffHours > 0 ? `${diffHours}h ago` : 'just now';
2368
+ console.log('Gates:');
2369
+ if (gates.length === 0) {
2370
+ console.log(' (none)');
2371
+ } else {
2372
+ for (const gate of gates) {
2373
+ console.log(` ${gate.gate_name}: ${gate.passed_at ? 'PASSED' : 'NOT PASSED'}`);
2374
+ }
2375
+ }
2376
+ } else {
2377
+ // No ID - show all interrupted workflows (legacy behavior)
2378
+ const { getCurrentBranch } = require('./lib/workflow-checkpoint');
2379
+ const currentBranch = getCurrentBranch();
2358
2380
 
2381
+ const checkpoints = await new Promise((resolve, reject) => {
2382
+ db.all(
2383
+ `SELECT * FROM workflow_checkpoints ORDER BY updated_at DESC`,
2384
+ [],
2385
+ (err, rows) => {
2386
+ if (err) reject(err);
2387
+ else resolve(rows || []);
2388
+ }
2389
+ );
2390
+ });
2391
+
2392
+ if (checkpoints.length === 0) {
2393
+ console.log('No interrupted workflows found.');
2394
+ } else {
2395
+ console.log('Interrupted Workflows');
2396
+ console.log('─'.repeat(90));
2397
+ console.log('');
2359
2398
  console.log(
2360
- String(cp.id).padEnd(4) +
2361
- cp.skill_name.padEnd(18) +
2362
- stepInfo.padEnd(10) +
2363
- `#${cp.work_item_id || '-'}`.padEnd(12) +
2364
- branchWithMarker.padEnd(38) +
2365
- timeAgo
2399
+ 'ID'.padEnd(4) +
2400
+ 'Skill'.padEnd(18) +
2401
+ 'Step'.padEnd(10) +
2402
+ 'Work Item'.padEnd(12) +
2403
+ 'Branch'.padEnd(38) +
2404
+ 'Last Active'
2366
2405
  );
2367
- }
2406
+ console.log('─'.repeat(90));
2407
+
2408
+ for (const cp of checkpoints) {
2409
+ const stepInfo = cp.total_steps
2410
+ ? `${cp.current_step}/${cp.total_steps}`
2411
+ : `${cp.current_step}`;
2412
+ const isCurrent = cp.branch_name === currentBranch;
2413
+ const branchDisplay = cp.branch_name.length > 35
2414
+ ? cp.branch_name.substring(0, 32) + '...'
2415
+ : cp.branch_name;
2416
+ const branchWithMarker = branchDisplay + (isCurrent ? ' *' : '');
2417
+
2418
+ // Format time ago
2419
+ const updatedAt = new Date(cp.updated_at);
2420
+ const now = new Date();
2421
+ const diffMs = now - updatedAt;
2422
+ const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
2423
+ const diffDays = Math.floor(diffHours / 24);
2424
+ const timeAgo = diffDays > 0 ? `${diffDays}d ago` : diffHours > 0 ? `${diffHours}h ago` : 'just now';
2425
+
2426
+ console.log(
2427
+ String(cp.id).padEnd(4) +
2428
+ cp.skill_name.padEnd(18) +
2429
+ stepInfo.padEnd(10) +
2430
+ `#${cp.work_item_id || '-'}`.padEnd(12) +
2431
+ branchWithMarker.padEnd(38) +
2432
+ timeAgo
2433
+ );
2434
+ }
2368
2435
 
2369
- console.log('');
2370
- console.log(`${checkpoints.length} interrupted workflow(s)`);
2371
- console.log('');
2372
- console.log('Commands:');
2373
- console.log(' jettypod workflow show <id> Show workflow details');
2374
- console.log(' jettypod workflow resume <id> Resume a workflow');
2375
- console.log(' jettypod workflow abort <id> Abort and clean up a workflow');
2436
+ console.log('');
2437
+ console.log(`${checkpoints.length} interrupted workflow(s)`);
2438
+ console.log('');
2439
+ console.log('Commands:');
2440
+ console.log(' jettypod workflow show <id> Show workflow details');
2441
+ console.log(' jettypod workflow resume <id> Resume a workflow');
2442
+ console.log(' jettypod workflow abort <id> Abort and clean up a workflow');
2443
+ }
2376
2444
  }
2377
2445
  } catch (err) {
2378
2446
  console.error(`Error: ${err.message}`);
@@ -2435,73 +2503,44 @@ Quick commands:
2435
2503
  process.exit(1);
2436
2504
  }
2437
2505
  } else if (workflowSubcommand === 'resume') {
2506
+ // workflow resume - finds interrupted workflows (in-progress skill executions)
2438
2507
  const { getDb, waitForMigrations } = require('./lib/database');
2439
- const { getCheckpoint, getCurrentBranch } = require('./lib/workflow-checkpoint');
2440
2508
 
2441
2509
  try {
2442
2510
  const db = getDb();
2443
2511
  await waitForMigrations();
2444
- const currentBranch = getCurrentBranch();
2445
- const workflowId = args[1] ? parseInt(args[1]) : null;
2446
2512
 
2447
- let checkpoint;
2448
-
2449
- if (workflowId) {
2450
- // Resume by ID - fetch specific workflow
2451
- checkpoint = await new Promise((resolve, reject) => {
2452
- db.get(
2453
- `SELECT * FROM workflow_checkpoints WHERE id = ?`,
2454
- [workflowId],
2455
- (err, row) => {
2456
- if (err) reject(err);
2457
- else resolve(row);
2458
- }
2459
- );
2460
- });
2513
+ // Find all in-progress skill executions
2514
+ const inProgressExecutions = await new Promise((resolve, reject) => {
2515
+ db.all(
2516
+ `SELECT se.*, wi.title as work_item_title
2517
+ FROM skill_executions se
2518
+ LEFT JOIN work_items wi ON se.work_item_id = wi.id
2519
+ WHERE se.status = 'in_progress'
2520
+ ORDER BY se.started_at DESC`,
2521
+ [],
2522
+ (err, rows) => {
2523
+ if (err) reject(err);
2524
+ else resolve(rows || []);
2525
+ }
2526
+ );
2527
+ });
2461
2528
 
2462
- if (!checkpoint) {
2463
- console.log(`Error: No workflow found with ID ${workflowId}`);
2464
- console.log('');
2465
- console.log('Use "jettypod workflow status" to see available workflows.');
2466
- process.exit(1);
2467
- }
2529
+ if (inProgressExecutions.length === 0) {
2530
+ console.log('No interrupted workflow found');
2468
2531
  } else {
2469
- // No ID - check current branch
2470
- checkpoint = await getCheckpoint(db, currentBranch);
2471
-
2472
- if (!checkpoint) {
2473
- console.log('No interrupted workflow on current branch.');
2532
+ for (const exec of inProgressExecutions) {
2533
+ console.log('Found interrupted workflow:');
2534
+ console.log(` Skill: ${exec.skill_name}`);
2535
+ console.log(` Work Item: #${exec.work_item_id}${exec.work_item_title ? ` - ${exec.work_item_title}` : ''}`);
2536
+ console.log(` Step ${exec.step_reached}`);
2537
+ console.log(` Started: ${exec.started_at}`);
2538
+ if (exec.context_json) {
2539
+ console.log(` Context: ${exec.context_json}`);
2540
+ }
2474
2541
  console.log('');
2475
- console.log('Use "jettypod workflow status" to see workflows on other branches,');
2476
- console.log('or specify an ID: "jettypod workflow resume <id>"');
2477
- process.exit(0);
2478
2542
  }
2479
2543
  }
2480
-
2481
- // Display resume info
2482
- const stepInfo = checkpoint.total_steps
2483
- ? `Step ${checkpoint.current_step} of ${checkpoint.total_steps}`
2484
- : `Step ${checkpoint.current_step}`;
2485
-
2486
- const needsBranchSwitch = checkpoint.branch_name !== currentBranch;
2487
-
2488
- console.log('Resuming Workflow');
2489
- console.log('─'.repeat(50));
2490
- console.log('');
2491
- console.log(` Skill: ${checkpoint.skill_name}`);
2492
- console.log(` Progress: ${stepInfo}`);
2493
- console.log(` Work Item: ${checkpoint.work_item_id ? '#' + checkpoint.work_item_id : '-'}`);
2494
- console.log(` Branch: ${checkpoint.branch_name}`);
2495
- console.log('');
2496
-
2497
- if (needsBranchSwitch) {
2498
- console.log(`Switching to branch: ${checkpoint.branch_name}`);
2499
- console.log('');
2500
- }
2501
-
2502
- console.log('[Triggering skill continuation]');
2503
- // Note: Actual skill triggering would be implemented here
2504
- // For now, this displays the intent to continue the workflow
2505
2544
  } catch (err) {
2506
2545
  console.error(`Error: ${err.message}`);
2507
2546
  process.exit(1);
@@ -2580,14 +2619,343 @@ Quick commands:
2580
2619
  console.error(`Error: ${err.message}`);
2581
2620
  process.exit(1);
2582
2621
  }
2622
+ } else if (workflowSubcommand === 'start') {
2623
+ // workflow start <skill> <work-item-id>
2624
+ // Validates gates, creates skill execution record
2625
+ const skillName = args[1];
2626
+ const workItemId = parseInt(args[2]);
2627
+
2628
+ if (!skillName || !workItemId) {
2629
+ console.log('Usage: jettypod workflow start <skill> <work-item-id>');
2630
+ console.log('');
2631
+ console.log('Starts a skill for a work item after validating prerequisite gates.');
2632
+ console.log('');
2633
+ console.log('Example: jettypod workflow start speed-mode 42');
2634
+ process.exit(1);
2635
+ }
2636
+
2637
+ const { getDb, waitForMigrations } = require('./lib/database');
2638
+
2639
+ try {
2640
+ const db = getDb();
2641
+ await waitForMigrations();
2642
+
2643
+ // Define gate requirements for each skill
2644
+ const gateRequirements = {
2645
+ 'speed-mode': ['feature_planning_complete'],
2646
+ 'stable-mode': ['speed_mode_complete'],
2647
+ 'production-mode': ['stable_mode_complete'],
2648
+ 'feature-planning': [],
2649
+ 'epic-planning': []
2650
+ };
2651
+
2652
+ const requiredGates = gateRequirements[skillName] || [];
2653
+
2654
+ // Check prerequisite gates
2655
+ const passedGates = await new Promise((resolve, reject) => {
2656
+ db.all(
2657
+ `SELECT gate_name FROM workflow_gates WHERE work_item_id = ? AND passed_at IS NOT NULL`,
2658
+ [workItemId],
2659
+ (err, rows) => {
2660
+ if (err) reject(err);
2661
+ else resolve(rows ? rows.map(r => r.gate_name) : []);
2662
+ }
2663
+ );
2664
+ });
2665
+
2666
+ const missingGates = requiredGates.filter(g => !passedGates.includes(g));
2667
+
2668
+ if (missingGates.length > 0) {
2669
+ console.log(`Gate validation failed: ${missingGates.join(', ')}`);
2670
+ process.exit(1);
2671
+ }
2672
+
2673
+ // Create skill execution record
2674
+ await new Promise((resolve, reject) => {
2675
+ db.run(
2676
+ `INSERT INTO skill_executions (work_item_id, skill_name, status, step_reached)
2677
+ VALUES (?, ?, 'in_progress', 1)`,
2678
+ [workItemId, skillName],
2679
+ (err) => {
2680
+ if (err) reject(err);
2681
+ else resolve();
2682
+ }
2683
+ );
2684
+ });
2685
+
2686
+ console.log(`Started ${skillName} for #${workItemId}`);
2687
+ } catch (err) {
2688
+ console.error(`Error: ${err.message}`);
2689
+ process.exit(1);
2690
+ }
2691
+ } else if (workflowSubcommand === 'complete') {
2692
+ // workflow complete <skill> <work-item-id>
2693
+ // Marks skill execution as completed, passes output gate
2694
+ const skillName = args[1];
2695
+ const workItemId = parseInt(args[2]);
2696
+
2697
+ if (!skillName || !workItemId) {
2698
+ console.log('Usage: jettypod workflow complete <skill> <work-item-id>');
2699
+ console.log('');
2700
+ console.log('Completes a skill and marks its output gate as passed.');
2701
+ console.log('');
2702
+ console.log('Example: jettypod workflow complete speed-mode 42');
2703
+ process.exit(1);
2704
+ }
2705
+
2706
+ const { getDb, waitForMigrations } = require('./lib/database');
2707
+
2708
+ try {
2709
+ const db = getDb();
2710
+ await waitForMigrations();
2711
+
2712
+ // Check if skill is in progress
2713
+ const execution = await new Promise((resolve, reject) => {
2714
+ db.get(
2715
+ `SELECT * FROM skill_executions WHERE work_item_id = ? AND skill_name = ? AND status = 'in_progress'`,
2716
+ [workItemId, skillName],
2717
+ (err, row) => {
2718
+ if (err) reject(err);
2719
+ else resolve(row);
2720
+ }
2721
+ );
2722
+ });
2723
+
2724
+ if (!execution) {
2725
+ console.log(`No in-progress execution found for ${skillName} on #${workItemId}`);
2726
+ process.exit(1);
2727
+ }
2728
+
2729
+ // Update skill execution to completed
2730
+ await new Promise((resolve, reject) => {
2731
+ db.run(
2732
+ `UPDATE skill_executions SET status = 'completed', completed_at = datetime('now')
2733
+ WHERE work_item_id = ? AND skill_name = ? AND status = 'in_progress'`,
2734
+ [workItemId, skillName],
2735
+ (err) => {
2736
+ if (err) reject(err);
2737
+ else resolve();
2738
+ }
2739
+ );
2740
+ });
2741
+
2742
+ // Mark output gate as passed
2743
+ const outputGate = `${skillName.replace(/-/g, '_')}_complete`;
2744
+ await new Promise((resolve, reject) => {
2745
+ db.run(
2746
+ `INSERT OR REPLACE INTO workflow_gates (work_item_id, gate_name, passed_at)
2747
+ VALUES (?, ?, datetime('now'))`,
2748
+ [workItemId, outputGate],
2749
+ (err) => {
2750
+ if (err) reject(err);
2751
+ else resolve();
2752
+ }
2753
+ );
2754
+ });
2755
+
2756
+ console.log(`Completed ${skillName} for #${workItemId}`);
2757
+ } catch (err) {
2758
+ console.error(`Error: ${err.message}`);
2759
+ process.exit(1);
2760
+ }
2761
+ } else if (workflowSubcommand === 'checkpoint') {
2762
+ // workflow checkpoint <work-item-id> --step=N [--context='{"key":"value"}']
2763
+ // Updates step progress and optionally stores context
2764
+ const workItemId = parseInt(args[1]);
2765
+ const stepArg = args.find(a => a.startsWith('--step='));
2766
+ const contextArg = args.find(a => a.startsWith('--context='));
2767
+
2768
+ if (!workItemId) {
2769
+ console.log('Usage: jettypod workflow checkpoint <work-item-id> --step=N [--context=JSON]');
2770
+ console.log('');
2771
+ console.log('Updates checkpoint progress for an in-progress skill execution.');
2772
+ console.log('');
2773
+ console.log('Example: jettypod workflow checkpoint 42 --step=3');
2774
+ console.log('Example: jettypod workflow checkpoint 42 --step=2 --context=\'{"iteration":5}\'');
2775
+ process.exit(1);
2776
+ }
2777
+
2778
+ const { getDb, waitForMigrations } = require('./lib/database');
2779
+
2780
+ try {
2781
+ const db = getDb();
2782
+ await waitForMigrations();
2783
+
2784
+ // Find in-progress skill execution
2785
+ const execution = await new Promise((resolve, reject) => {
2786
+ db.get(
2787
+ `SELECT * FROM skill_executions WHERE work_item_id = ? AND status = 'in_progress'`,
2788
+ [workItemId],
2789
+ (err, row) => {
2790
+ if (err) reject(err);
2791
+ else resolve(row);
2792
+ }
2793
+ );
2794
+ });
2795
+
2796
+ if (!execution) {
2797
+ console.log(`No in-progress skill execution found for #${workItemId}`);
2798
+ process.exit(1);
2799
+ }
2800
+
2801
+ // Build update query
2802
+ const updates = [];
2803
+ const values = [];
2804
+
2805
+ if (stepArg) {
2806
+ const step = parseInt(stepArg.split('=')[1]);
2807
+ updates.push('step_reached = ?');
2808
+ values.push(step);
2809
+ }
2810
+
2811
+ if (contextArg) {
2812
+ const contextJson = contextArg.split('=').slice(1).join('=').replace(/^'|'$/g, '');
2813
+ updates.push('context_json = ?');
2814
+ values.push(contextJson);
2815
+ }
2816
+
2817
+ if (updates.length === 0) {
2818
+ console.log('No updates specified. Use --step=N and/or --context=JSON');
2819
+ process.exit(1);
2820
+ }
2821
+
2822
+ values.push(workItemId);
2823
+
2824
+ await new Promise((resolve, reject) => {
2825
+ db.run(
2826
+ `UPDATE skill_executions SET ${updates.join(', ')} WHERE work_item_id = ? AND status = 'in_progress'`,
2827
+ values,
2828
+ (err) => {
2829
+ if (err) reject(err);
2830
+ else resolve();
2831
+ }
2832
+ );
2833
+ });
2834
+
2835
+ console.log(`Checkpoint updated for #${workItemId}`);
2836
+ } catch (err) {
2837
+ console.error(`Error: ${err.message}`);
2838
+ process.exit(1);
2839
+ }
2840
+ } else if (workflowSubcommand === 'gate') {
2841
+ // workflow gate pass <work-item-id> <gate-name>
2842
+ // workflow gate check <work-item-id>
2843
+ const gateAction = args[1];
2844
+ const workItemId = parseInt(args[2]);
2845
+
2846
+ if (!gateAction || !workItemId) {
2847
+ console.log('Usage: jettypod workflow gate <action> <work-item-id> [gate-name]');
2848
+ console.log('');
2849
+ console.log('Actions:');
2850
+ console.log(' pass <id> <gate> Mark a gate as passed');
2851
+ console.log(' check <id> Show all gates for a work item');
2852
+ console.log('');
2853
+ console.log('Example: jettypod workflow gate pass 42 feature_planning_complete');
2854
+ console.log('Example: jettypod workflow gate check 42');
2855
+ process.exit(1);
2856
+ }
2857
+
2858
+ const { getDb, waitForMigrations } = require('./lib/database');
2859
+
2860
+ try {
2861
+ const db = getDb();
2862
+ await waitForMigrations();
2863
+
2864
+ if (gateAction === 'pass') {
2865
+ const gateName = args[3];
2866
+
2867
+ if (!gateName) {
2868
+ console.log('Usage: jettypod workflow gate pass <work-item-id> <gate-name>');
2869
+ process.exit(1);
2870
+ }
2871
+
2872
+ await new Promise((resolve, reject) => {
2873
+ db.run(
2874
+ `INSERT OR REPLACE INTO workflow_gates (work_item_id, gate_name, passed_at)
2875
+ VALUES (?, ?, datetime('now'))`,
2876
+ [workItemId, gateName],
2877
+ (err) => {
2878
+ if (err) reject(err);
2879
+ else resolve();
2880
+ }
2881
+ );
2882
+ });
2883
+
2884
+ console.log(`Gate passed: ${gateName}`);
2885
+ } else if (gateAction === 'check') {
2886
+ // Get all known gates for this skill workflow
2887
+ const knownGates = [
2888
+ 'feature_planning_complete',
2889
+ 'speed_mode_complete',
2890
+ 'stable_mode_complete',
2891
+ 'production_mode_complete'
2892
+ ];
2893
+
2894
+ const passedGates = await new Promise((resolve, reject) => {
2895
+ db.all(
2896
+ `SELECT gate_name, passed_at FROM workflow_gates WHERE work_item_id = ?`,
2897
+ [workItemId],
2898
+ (err, rows) => {
2899
+ if (err) reject(err);
2900
+ else resolve(rows || []);
2901
+ }
2902
+ );
2903
+ });
2904
+
2905
+ const passedGateMap = {};
2906
+ for (const g of passedGates) {
2907
+ passedGateMap[g.gate_name] = g.passed_at;
2908
+ }
2909
+
2910
+ console.log(`Gates for #${workItemId}:`);
2911
+ console.log('');
2912
+
2913
+ for (const gateName of knownGates) {
2914
+ if (passedGateMap[gateName]) {
2915
+ console.log(`${gateName}: PASSED`);
2916
+ } else {
2917
+ console.log(`${gateName}: NOT PASSED`);
2918
+ }
2919
+ }
2920
+
2921
+ // Also show any custom gates
2922
+ const customGates = passedGates.filter(g => !knownGates.includes(g.gate_name));
2923
+ for (const g of customGates) {
2924
+ console.log(`${g.gate_name}: PASSED`);
2925
+ }
2926
+ } else {
2927
+ console.log(`Unknown gate action: ${gateAction}`);
2928
+ console.log('Use "pass" or "check"');
2929
+ process.exit(1);
2930
+ }
2931
+ } catch (err) {
2932
+ console.error(`Error: ${err.message}`);
2933
+ process.exit(1);
2934
+ }
2935
+ } else if (workflowSubcommand === '--help') {
2936
+ console.log('Usage: jettypod workflow <command>');
2937
+ console.log('');
2938
+ console.log('Commands:');
2939
+ console.log(' start <skill> <id> Start a skill for a work item (validates gates)');
2940
+ console.log(' complete <skill> <id> Complete a skill (marks gates passed)');
2941
+ console.log(' checkpoint <id> Update checkpoint progress');
2942
+ console.log(' gate <action> <id> Manage workflow gates');
2943
+ console.log(' resume Find interrupted workflows');
2944
+ console.log(' status <id> Show workflow status for a work item');
2945
+ console.log('');
2946
+ console.log('Legacy Commands:');
2947
+ console.log(' show <id> Show checkpoint details');
2948
+ console.log(' abort <id> Abort a checkpoint');
2583
2949
  } else {
2584
2950
  console.log('Usage: jettypod workflow <command>');
2585
2951
  console.log('');
2586
2952
  console.log('Commands:');
2587
- console.log(' status List all interrupted workflows');
2588
- console.log(' show <id> Show workflow details');
2589
- console.log(' resume [id] Resume a workflow');
2590
- console.log(' abort <id> Abort and clean up a workflow');
2953
+ console.log(' start <skill> <id> Start a skill for a work item (validates gates)');
2954
+ console.log(' complete <skill> <id> Complete a skill (marks gates passed)');
2955
+ console.log(' checkpoint <id> Update checkpoint progress');
2956
+ console.log(' gate <action> <id> Manage workflow gates');
2957
+ console.log(' resume Find interrupted workflows');
2958
+ console.log(' status <id> Show workflow status for a work item');
2591
2959
  }
2592
2960
  break;
2593
2961
  }
package/lib/database.js CHANGED
@@ -203,6 +203,39 @@ function initSchema() {
203
203
  }
204
204
  });
205
205
 
206
+ db.run(`
207
+ CREATE TABLE IF NOT EXISTS skill_executions (
208
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
209
+ work_item_id INTEGER NOT NULL,
210
+ skill_name TEXT NOT NULL,
211
+ status TEXT DEFAULT 'in_progress',
212
+ step_reached INTEGER DEFAULT 1,
213
+ context_json TEXT,
214
+ started_at DATETIME DEFAULT CURRENT_TIMESTAMP,
215
+ completed_at DATETIME,
216
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
217
+ )
218
+ `, (err) => {
219
+ if (err) {
220
+ throw new Error(`Failed to create skill_executions table: ${err.message}`);
221
+ }
222
+ });
223
+
224
+ db.run(`
225
+ CREATE TABLE IF NOT EXISTS workflow_gates (
226
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
227
+ work_item_id INTEGER NOT NULL,
228
+ gate_name TEXT NOT NULL,
229
+ passed_at DATETIME,
230
+ UNIQUE(work_item_id, gate_name),
231
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
232
+ )
233
+ `, (err) => {
234
+ if (err) {
235
+ throw new Error(`Failed to create workflow_gates table: ${err.message}`);
236
+ }
237
+ });
238
+
206
239
  // Migrations: Add columns if they don't exist (for databases created before these columns were in base schema)
207
240
  const handleAlterError = (columnName) => (err) => {
208
241
  if (err && !err.message.includes('duplicate column')) {
@@ -1629,10 +1629,82 @@ async function mergeWork(options = {}) {
1629
1629
  }
1630
1630
  }
1631
1631
 
1632
+ /**
1633
+ * Create a worktree for writing BDD tests for a feature
1634
+ * @param {number} featureId - Feature ID to create test worktree for
1635
+ * @returns {Promise<Object>} Worktree info with path and branch
1636
+ * @throws {Error} If feature not found or not in correct phase
1637
+ */
1638
+ async function testsWork(featureId) {
1639
+ if (!featureId || isNaN(featureId) || featureId < 1) {
1640
+ return Promise.reject(new Error('Invalid feature ID'));
1641
+ }
1642
+
1643
+ const paths = getPaths();
1644
+
1645
+ if (!fs.existsSync(paths.jettypodDir)) {
1646
+ return Promise.reject(new Error('JettyPod not initialized. Run: jettypod init'));
1647
+ }
1648
+
1649
+ if (!fs.existsSync(paths.dbPath)) {
1650
+ return Promise.reject(new Error('Work database not found. Run: jettypod init'));
1651
+ }
1652
+
1653
+ const db = getDb();
1654
+
1655
+ return new Promise((resolve, reject) => {
1656
+ // Get feature work item
1657
+ db.get(
1658
+ `SELECT * FROM work_items WHERE id = ? AND type = 'feature'`,
1659
+ [featureId],
1660
+ async (err, feature) => {
1661
+ if (err) {
1662
+ return reject(new Error(`Database error: ${err.message}`));
1663
+ }
1664
+
1665
+ if (!feature) {
1666
+ return reject(new Error(`Feature #${featureId} not found`));
1667
+ }
1668
+
1669
+ // Create worktree with tests/ prefix
1670
+ try {
1671
+ const gitRoot = getGitRoot();
1672
+ const worktreeResult = await worktreeManager.createWorktree(feature, {
1673
+ repoPath: gitRoot,
1674
+ branchPrefix: 'tests/feature',
1675
+ pathPrefix: 'tests-'
1676
+ });
1677
+
1678
+ // Set as current work
1679
+ setCurrentWork({
1680
+ id: feature.id,
1681
+ type: feature.type,
1682
+ title: feature.title,
1683
+ status: feature.status || 'in_progress',
1684
+ worktreePath: worktreeResult.worktree_path,
1685
+ branchName: worktreeResult.branch_name
1686
+ });
1687
+
1688
+ console.log(`✅ Created test worktree: ${worktreeResult.worktree_path}`);
1689
+ console.log(`📁 IMPORTANT: Use absolute paths for file operations:`);
1690
+ console.log(` ${worktreeResult.worktree_path}/features/your-feature.feature (correct)`);
1691
+ console.log(` features/your-feature.feature (wrong - creates in main repo)`);
1692
+ console.log(`\nWorking on tests for: [#${feature.id}] ${feature.title}`);
1693
+
1694
+ resolve(worktreeResult);
1695
+ } catch (worktreeErr) {
1696
+ reject(new Error(`Failed to create test worktree: ${worktreeErr.message}`));
1697
+ }
1698
+ }
1699
+ );
1700
+ });
1701
+ }
1702
+
1632
1703
  module.exports = {
1633
1704
  startWork,
1634
1705
  stopWork,
1635
1706
  getCurrentWork,
1636
1707
  cleanupWorktrees,
1637
- mergeWork
1708
+ mergeWork,
1709
+ testsWork
1638
1710
  };
@@ -29,6 +29,8 @@ const VALID_STATUSES = ['active', 'merging', 'cleanup_pending', 'corrupted'];
29
29
  * @param {Object} options - Optional configuration
30
30
  * @param {string} options.repoPath - Path to git repository (defaults to process.cwd())
31
31
  * @param {Object} options.db - Database connection (defaults to global database)
32
+ * @param {string} options.branchPrefix - Branch prefix (defaults to 'feature/work')
33
+ * @param {string} options.pathPrefix - Worktree path prefix (defaults to '')
32
34
  * @returns {Promise<Object>} Worktree record with id, work_item_id, branch_name, worktree_path, status
33
35
  */
34
36
  async function createWorktree(workItem, options = {}) {
@@ -85,9 +87,11 @@ async function createWorktree(workItem, options = {}) {
85
87
 
86
88
  // Generate branch name and worktree path
87
89
  const titleSlug = slugify(workItem.title || 'item');
88
- const branchName = `feature/work-${workItem.id}-${titleSlug}`;
90
+ const branchPrefix = options.branchPrefix || 'feature/work';
91
+ const pathPrefix = options.pathPrefix || '';
92
+ const branchName = `${branchPrefix}-${workItem.id}-${titleSlug}`;
89
93
  const worktreeBasePath = path.join(gitRoot, '.jettypod-work');
90
- const worktreePath = path.join(worktreeBasePath, `${workItem.id}-${titleSlug}`);
94
+ const worktreePath = path.join(worktreeBasePath, `${pathPrefix}${workItem.id}-${titleSlug}`);
91
95
 
92
96
  let worktreeId = null;
93
97
  let branchCreated = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.44",
3
+ "version": "4.4.46",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {
@@ -13,20 +13,23 @@ When this skill is activated, you are helping discover the best approach for a f
13
13
 
14
14
  ## 🔑 Critical Command Distinction
15
15
 
16
- **Two different commands, two different purposes:**
16
+ **Four different commands, four different purposes:**
17
17
 
18
18
  | Command | Used For | When | Phase |
19
19
  |---------|----------|------|-------|
20
- | `work implement <feature-id>` | Transition feature to implementation phase | During feature planning (Step 8B) | Feature Planning |
21
- | `work start <chore-id>` | Start implementing a specific chore | After feature planning complete (Step 8D) | Speed Mode |
20
+ | `work implement <feature-id>` | Transition feature to implementation phase | After chores created (Step 8C) | Feature Planning |
21
+ | `work tests start <feature-id>` | Create worktree for test authoring | After transition (Step 8D) | Feature Planning |
22
+ | `work tests merge <feature-id>` | Merge tests to main, cleanup worktree | After tests validated (Step 8D) | Feature Planning |
23
+ | `work start <chore-id>` | Start implementing a specific chore | After tests merged (Step 8E) | Speed Mode |
22
24
 
23
- **CRITICAL:** Both commands are run by **Claude**, not the user. The distinction is:
24
- - `work implement` = Ends feature planning, creates chores
25
- - `work start` = Creates worktree/branch for chore (does NOT auto-invoke speed-mode)
25
+ **CRITICAL:** All commands are run by **Claude**, not the user. The distinction is:
26
+ - `work implement` = Ends feature planning, transitions to implementation phase
27
+ - `work tests start/merge` = Write and land BDD tests in isolated worktree
28
+ - `work start` = Creates worktree/branch for chore implementation
26
29
 
27
- **🛑 STOP GATE:** DO NOT run `work start` until Step 8D. If you're in Steps 1-8C, use `work implement` only.
30
+ **🛑 STOP GATE:** DO NOT run `work start` until Step 8E. Tests must be merged to main first.
28
31
 
29
- **🛑 HANDOFF REQUIREMENT:** After `work start`, you MUST invoke speed-mode using the Skill tool. See Step 8D.
32
+ **🛑 HANDOFF REQUIREMENT:** After `work start`, you MUST invoke speed-mode using the Skill tool. See Step 8E.
30
33
 
31
34
  ---
32
35
 
@@ -224,9 +227,9 @@ Example:
224
227
 
225
228
  **WAIT for user to confirm or adjust the integration contract.**
226
229
 
227
- #### Step 6B: Generate BDD Scenarios AND Step Definitions
230
+ #### Step 6B: Propose BDD Scenarios (DO NOT WRITE YET)
228
231
 
229
- **CRITICAL:** BDD scenarios and step definitions must ALWAYS be created together. Never create scenarios without step definitions.
232
+ **CRITICAL:** Do NOT write files yet. Display the proposed scenarios for user confirmation. Files will be written in Step 8D inside an isolated worktree.
230
233
 
231
234
  **Feature slug:** Derive `FEATURE_SLUG` from the feature title:
232
235
  - Lowercase
@@ -234,43 +237,79 @@ Example:
234
237
  - Remove special characters
235
238
  - Examples: "Email Password Login" → "email-password-login", "User's Profile (v2)" → "users-profile-v2"
236
239
 
237
- Based on chosen approach, generate:
240
+ Based on chosen approach, **display** the proposed scenarios to the user:
238
241
 
239
- **A. Scenario file** at `features/[feature-slug].feature` using Write tool:
242
+ ```
243
+ 📋 Proposed BDD Scenarios
240
244
 
241
- 1. **Create file** at `features/[feature-slug].feature` using Write tool
242
- 2. **FIRST scenario MUST be the Integration Scenario** from Step 6A (proves reachability)
243
- 3. **Include all "make it work" scenarios**:
244
- - All success paths - required functionality AND optional features
245
- - Multiple valid workflows if the feature supports them
246
- - Success variations (different outcomes that are all correct)
247
- - NO error handling scenarios (added in stable mode)
248
- - NO validation failures (added in stable mode)
249
- - NO security/compliance scenarios (added in production mode)
245
+ **Feature file:** features/[feature-slug].feature
246
+ **Step definitions:** features/step_definitions/[feature-slug].steps.js
247
+ ```
250
248
 
251
- **B. Step definitions file** at `features/step_definitions/[feature-slug].steps.js` using Write tool:
249
+ Then show the full Gherkin content:
252
250
 
253
- 1. **Create file** at `features/step_definitions/[feature-slug].steps.js`
254
- 2. **Implement all Given/When/Then steps** from the scenarios
255
- 3. **Follow existing patterns** - check other `.steps.js` files for conventions
256
- 4. **Include**:
257
- - Test environment setup/cleanup
258
- - All Given steps (setup state)
259
- - All When steps (execute actions)
260
- - All Then steps (verify outcomes)
251
+ ```gherkin
252
+ Feature: [Feature Name]
253
+ [Brief description based on chosen UX approach]
261
254
 
262
- **C. Update database** with scenario file path (run from project root):
255
+ Epic: [Epic name if applicable]
256
+ Approach: [Chosen approach name]
263
257
 
264
- ```bash
265
- sqlite3 .jettypod/work.db "UPDATE work_items SET scenario_file = 'features/${FEATURE_SLUG}.feature' WHERE id = ${FEATURE_ID}"
258
+ # Integration Contract:
259
+ # Entry Point: [from Step 6A]
260
+ # Caller Code: [from Step 6A]
261
+
262
+ # INTEGRATION SCENARIO (REQUIRED - proves feature is reachable)
263
+ Scenario: User can reach [feature] from [entry point]
264
+ Given I am on [existing entry point users can access]
265
+ When I [navigate/click/invoke to reach the feature]
266
+ Then I [see/access the feature]
267
+
268
+ # FEATURE SCENARIOS (required + optional functionality)
269
+ Scenario: [Core functionality - required workflow]
270
+ Given [initial state]
271
+ When [user takes main action]
272
+ Then [expected successful outcome]
273
+ And [observable UI/system state change]
274
+
275
+ Scenario: [Optional feature 1 - if applicable]
276
+ Given [initial state]
277
+ When [user uses optional feature]
278
+ Then [feature works correctly]
279
+
280
+ # SPEED MODE: All success scenarios above (integration + required + optional)
281
+ # STABLE MODE: Will add error handling, validation failures, edge cases
266
282
  ```
267
283
 
268
- Replace `${FEATURE_SLUG}` and `${FEATURE_ID}` with actual values. Example:
269
- ```bash
270
- sqlite3 .jettypod/work.db "UPDATE work_items SET scenario_file = 'features/email-password-login.feature' WHERE id = 42"
284
+ Then list the step definitions that will be created:
285
+
286
+ ```
287
+ **Step definitions to implement:**
288
+ • Given I am on [entry point] - [brief implementation note]
289
+ • When I [action] - [brief implementation note]
290
+ • Then I [outcome] - [brief implementation note]
291
+ [etc.]
292
+
293
+ Does this capture the feature correctly? Any scenarios to add/change?
271
294
  ```
272
295
 
273
- **Proceed to Step 6.5.**
296
+ **Scenario content requirements:**
297
+ - **FIRST scenario MUST be the Integration Scenario** from Step 6A (proves reachability)
298
+ - **Include all "make it work" scenarios**:
299
+ - All success paths - required functionality AND optional features
300
+ - Multiple valid workflows if the feature supports them
301
+ - Success variations (different outcomes that are all correct)
302
+ - NO error handling scenarios (added in stable mode)
303
+ - NO validation failures (added in stable mode)
304
+ - NO security/compliance scenarios (added in production mode)
305
+
306
+ **WAIT for user confirmation.**
307
+
308
+ - If user confirms → Proceed to Step 7
309
+ - If user requests changes → Revise scenarios and display again
310
+ - **Store the confirmed scenarios in memory** - you'll write them in Step 8D
311
+
312
+ **Proceed to Step 7.**
274
313
 
275
314
  **Template for speed mode (make it work):**
276
315
 
@@ -395,102 +434,9 @@ Scenario: Prevent unauthorized access
395
434
 
396
435
  </details>
397
436
 
398
- ### Step 6.5: Validate BDD Infrastructure
399
-
400
- **CRITICAL:** After creating both scenario and step definition files, validate them with cucumber dry-run to catch errors immediately.
401
-
402
- **Run this command** (replace `${FEATURE_SLUG}` with actual slug):
403
-
404
- ```bash
405
- npx cucumber-js --dry-run features/${FEATURE_SLUG}.feature
406
- ```
407
-
408
- Example:
409
- ```bash
410
- npx cucumber-js --dry-run features/email-password-login.feature
411
- ```
412
-
413
- **What the output means:**
414
-
415
- ✅ **Success** - No errors, all steps have definitions:
416
- ```
417
- 0 scenarios
418
- 0 steps
419
- ```
420
- (Dry-run doesn't execute, so 0 is correct)
421
-
422
- ❌ **Undefined steps** - Missing step definitions:
423
- ```
424
- Undefined. Implement with the following snippet:
425
- Given('I am on the login page', function () {
426
- ...
427
- });
428
- ```
429
- → Add the missing step definition to your `.steps.js` file
430
-
431
- ❌ **Syntax error** - Invalid Gherkin:
432
- ```
433
- Parse error in features/foo.feature
434
- ```
435
- → Fix the feature file syntax
436
-
437
- ❌ **Duplicate steps** - Multiple definitions match:
438
- ```
439
- Multiple step definitions match
440
- ```
441
- → Remove or rename one of the duplicate step definitions
442
-
443
- **If validation fails:**
444
- 1. Read the error message carefully
445
- 2. Fix the step definitions or scenario file
446
- 3. Re-run the dry-run command
447
- 4. **Loop until validation passes** - do NOT proceed to Step 7 until green
448
-
449
- **If validation succeeds:**
450
- Display: "✅ BDD infrastructure validated - all steps have definitions"
451
-
452
- **Proceed to Step 6.6.**
453
-
454
- **Note:** Unit tests are created during speed mode implementation, not during feature planning. Feature planning focuses on BDD scenarios (user experience); speed mode handles unit tests (implementation details).
455
-
456
- ### Step 6.6: Commit BDD Files to Main
457
-
458
- **CRITICAL:** The BDD files must be committed to main BEFORE creating worktrees. If you skip this step, worktrees won't have the scenario files and speed-mode will fail.
459
-
460
- **Commit the BDD files:**
461
-
462
- ```bash
463
- git add features/${FEATURE_SLUG}.feature features/steps/${FEATURE_SLUG}.* features/step_definitions/${FEATURE_SLUG}.*
464
- git commit -m "test: Add BDD scenarios for ${FEATURE_TITLE}
465
-
466
- - Feature file: features/${FEATURE_SLUG}.feature
467
- - Step definitions for speed mode scenarios"
468
-
469
- git push
470
- ```
471
-
472
- Replace `${FEATURE_SLUG}` and `${FEATURE_TITLE}` with actual values. Example:
473
- ```bash
474
- git add features/email-password-login.feature features/steps/email-password-login.* features/step_definitions/email-password-login.*
475
- git commit -m "test: Add BDD scenarios for Email Password Login
476
-
477
- - Feature file: features/email-password-login.feature
478
- - Step definitions for speed mode scenarios"
479
-
480
- git push
481
- ```
482
-
483
- **Why this matters:**
484
- - Worktrees are created from the current branch (usually main)
485
- - If BDD files aren't committed, they won't exist in the worktree
486
- - Speed-mode will fail trying to run tests against non-existent files
487
- - **Push is required** so other assistants/machines have the latest main
488
-
489
- **Proceed to Step 7.**
490
-
491
437
  ### Step 7: Propose Speed Mode Chores
492
438
 
493
- **CRITICAL:** After BDD validation passes, analyze the codebase and propose technical implementation chores. **DO NOT CREATE CHORES YET** - the feature must transition to implementation mode first (Step 8).
439
+ **CRITICAL:** After user confirms the BDD scenarios, analyze the codebase and propose technical implementation chores. **DO NOT CREATE CHORES YET** - the feature must transition to implementation mode first (Step 8).
494
440
 
495
441
  **Your analysis should consider:**
496
442
  - The BDD scenarios (integration + all success paths)
@@ -633,6 +579,139 @@ After successful transition, display:
633
579
  ✅ Feature transitioned to implementation phase
634
580
  ✅ Created X chores for speed mode
635
581
 
582
+ Now I'll write the BDD tests in an isolated worktree...
583
+ ```
584
+
585
+ **Proceed to Step 8D.**
586
+
587
+ #### Step 8D: Write Tests in Isolated Worktree
588
+
589
+ **CRITICAL:** Tests are written in an isolated worktree, not directly on main. This ensures agent failures don't leave uncommitted garbage on main.
590
+
591
+ **Sub-step 1: Create test worktree**
592
+
593
+ ```bash
594
+ jettypod work tests start ${FEATURE_ID}
595
+ ```
596
+
597
+ This creates:
598
+ - Worktree at `.jettypod-work/tests-<id>-<slug>/`
599
+ - Branch `tests/feature-<id>-<slug>`
600
+
601
+ **Capture the worktree path from output** - you'll need it for file operations.
602
+
603
+ Example output:
604
+ ```
605
+ ✅ Created test worktree: /path/to/.jettypod-work/tests-42-email-login
606
+ Branch: tests/feature-42-email-login
607
+
608
+ Write your BDD files to:
609
+ <worktree>/features/email-login.feature
610
+ <worktree>/features/step_definitions/email-login.steps.js
611
+
612
+ When done, run: jettypod work tests merge 42
613
+ ```
614
+
615
+ **🛑 STOP AND CHECK:** Verify worktree was created successfully. If you see an error, investigate before continuing.
616
+
617
+ **Sub-step 2: Write BDD files in the worktree**
618
+
619
+ Using the scenarios you confirmed in Step 6B, write the files using **absolute paths to the worktree**:
620
+
621
+ **A. Write scenario file** using Write tool:
622
+ ```
623
+ <worktree_path>/features/${FEATURE_SLUG}.feature
624
+ ```
625
+
626
+ **B. Write step definitions file** using Write tool:
627
+ ```
628
+ <worktree_path>/features/step_definitions/${FEATURE_SLUG}.steps.js
629
+ ```
630
+
631
+ **Step definition requirements:**
632
+ 1. Implement all Given/When/Then steps from the scenarios
633
+ 2. Follow existing patterns - check other `.steps.js` files for conventions
634
+ 3. Include test environment setup/cleanup
635
+ 4. All Given steps (setup state)
636
+ 5. All When steps (execute actions)
637
+ 6. All Then steps (verify outcomes)
638
+
639
+ **Sub-step 3: Validate BDD infrastructure**
640
+
641
+ Run cucumber dry-run **from within the worktree**:
642
+
643
+ ```bash
644
+ cd <worktree_path> && npx cucumber-js --dry-run features/${FEATURE_SLUG}.feature
645
+ ```
646
+
647
+ **What the output means:**
648
+
649
+ ✅ **Success** - No errors, all steps have definitions:
650
+ ```
651
+ 0 scenarios
652
+ 0 steps
653
+ ```
654
+ (Dry-run doesn't execute, so 0 is correct)
655
+
656
+ ❌ **Undefined steps** - Missing step definitions:
657
+ ```
658
+ Undefined. Implement with the following snippet:
659
+ Given('I am on the login page', function () {
660
+ ...
661
+ });
662
+ ```
663
+ → Add the missing step definition to your `.steps.js` file
664
+
665
+ ❌ **Syntax error** - Invalid Gherkin:
666
+ ```
667
+ Parse error in features/foo.feature
668
+ ```
669
+ → Fix the feature file syntax
670
+
671
+ ❌ **Duplicate steps** - Multiple definitions match:
672
+ ```
673
+ Multiple step definitions match
674
+ ```
675
+ → Remove or rename one of the duplicate step definitions
676
+
677
+ **If validation fails:**
678
+ 1. Read the error message carefully
679
+ 2. Fix the step definitions or scenario file **in the worktree**
680
+ 3. Re-run the dry-run command
681
+ 4. **Loop until validation passes** - do NOT proceed until green
682
+
683
+ **If validation succeeds:**
684
+ Display: "✅ BDD infrastructure validated - all steps have definitions"
685
+
686
+ **Sub-step 4: Update database with scenario file path**
687
+
688
+ ```bash
689
+ sqlite3 .jettypod/work.db "UPDATE work_items SET scenario_file = 'features/${FEATURE_SLUG}.feature' WHERE id = ${FEATURE_ID}"
690
+ ```
691
+
692
+ **Sub-step 5: Merge tests to main**
693
+
694
+ ```bash
695
+ jettypod work tests merge ${FEATURE_ID}
696
+ ```
697
+
698
+ This will:
699
+ - Commit changes in the worktree
700
+ - Merge to main
701
+ - Push to remote
702
+ - Clean up the worktree
703
+
704
+ **🛑 STOP AND CHECK:** Verify merge succeeded:
705
+ - ✅ "Tests merged to main" → Proceed to Step 8E
706
+ - ❌ Error → Investigate, worktree still exists for debugging
707
+
708
+ **After successful merge, display:**
709
+
710
+ ```
711
+ ✅ Feature transitioned to implementation phase
712
+ ✅ Created X chores for speed mode
713
+ ✅ BDD tests merged to main
714
+
636
715
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
637
716
  🎯 Feature Planning Complete!
638
717
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -648,9 +727,11 @@ After successful transition, display:
648
727
  Start this one? [yes / pick different / done for now]
649
728
  ```
650
729
 
651
- #### Step 8D: Start First Chore
730
+ **Proceed to Step 8E.**
731
+
732
+ #### Step 8E: Start First Chore
652
733
 
653
- **Feature planning is now complete.** The feature has been transitioned to implementation phase and chores are ready.
734
+ **Feature planning is now complete.** The feature has been transitioned to implementation phase, tests are on main, and chores are ready.
654
735
 
655
736
  The completion message above already includes your recommendation. Now:
656
737
 
@@ -763,14 +844,15 @@ Before completing feature planning, ensure:
763
844
  - [ ] Exactly 3 approaches suggested
764
845
  - [ ] Winner chosen (with prototypes or without)
765
846
  - [ ] **Integration Contract defined** (entry point, caller code, integration scenario)
766
- - [ ] **Integration Scenario is FIRST scenario** in feature file
767
- - [ ] BDD scenarios written (integration + all success paths)
768
- - [ ] Step definitions written for ALL scenario steps
769
- - [ ] Scenarios file exists at `features/[feature-slug].feature`
770
- - [ ] Step definitions file exists at `features/step_definitions/[feature-slug].steps.js`
771
- - [ ] BDD infrastructure validated with dry-run (Step 6.5)
772
- - [ ] Feature transitioned to implementation with `work implement`
773
- - [ ] **At least one Integration Chore created** (wires feature into app)
774
- - [ ] Speed mode chores created
775
- - [ ] First chore started with `work start [chore-id]` (Step 8D)
776
- - [ ] **Speed-mode skill invoked using Skill tool** (Step 8D)
847
+ - [ ] **Integration Scenario is FIRST scenario** in proposed scenarios
848
+ - [ ] BDD scenarios **proposed and confirmed** by user (Step 6B)
849
+ - [ ] **At least one Integration Chore proposed** (wires feature into app)
850
+ - [ ] Speed mode chores proposed and confirmed (Step 7)
851
+ - [ ] Chores created in database (Step 8B)
852
+ - [ ] Feature transitioned to implementation with `work implement` (Step 8C)
853
+ - [ ] **Test worktree created** with `work tests start` (Step 8D)
854
+ - [ ] **BDD files written** in worktree (Step 8D)
855
+ - [ ] **BDD infrastructure validated** with dry-run (Step 8D)
856
+ - [ ] **Tests merged to main** with `work tests merge` (Step 8D)
857
+ - [ ] First chore started with `work start [chore-id]` (Step 8E)
858
+ - [ ] **Speed-mode skill invoked using Skill tool** (Step 8E)
@@ -627,22 +627,34 @@ After merge, you are on main branch. Ready to generate stable mode scenarios.
627
627
 
628
628
  **⚡ ASYNC BOUNDARY - Must wait for user confirmation**
629
629
 
630
- **1. Read the feature's scenario file and analyze for stable mode needs:**
630
+ **1. Create a test worktree for writing stable mode scenarios:**
631
+
632
+ ```bash
633
+ jettypod work tests <feature-id>
634
+ ```
635
+
636
+ This creates an isolated worktree at `.jettypod-work/tests-<id>-<slug>` with branch `tests/feature-<id>-<slug>`.
637
+
638
+ **🛑 CRITICAL:** All BDD file operations MUST use absolute paths in the test worktree:
639
+ - ✅ `/path/to/.jettypod-work/tests-42-login/features/login.feature`
640
+ - ❌ `features/login.feature` (this would write to main repo!)
641
+
642
+ **2. Read the feature's scenario file and analyze for stable mode needs:**
631
643
 
632
644
  ```bash
633
645
  sqlite3 .jettypod/work.db "SELECT id, title, scenario_file FROM work_items WHERE id = <feature-id>"
634
646
  ```
635
647
 
636
- **2. Generate stable mode BDD scenarios** covering:
648
+ **3. Generate stable mode BDD scenarios** covering:
637
649
  - **Error scenarios** - Invalid input, missing params, system errors, permission failures
638
650
  - **Edge cases** - Empty inputs, boundary values, special characters
639
651
  - **State consistency** - Concurrent ops, partial failures, invalid state transitions
640
652
 
641
- **3. Append scenarios to the feature file:**
653
+ **4. Append scenarios to the feature file IN THE TEST WORKTREE:**
642
654
 
643
- ```bash
644
- cat >> <scenario-file-path> << 'EOF'
655
+ Use the Edit tool to append to `<worktree-path>/features/<feature-file>`:
645
656
 
657
+ ```gherkin
646
658
  # Stable Mode Scenarios - Error Handling and Edge Cases
647
659
 
648
660
  Scenario: [Error scenario title]
@@ -654,14 +666,15 @@ Scenario: [Edge case title]
654
666
  Given [context]
655
667
  When [edge case input]
656
668
  Then [expected behavior]
657
- EOF
658
669
  ```
659
670
 
660
- **4. Create step definitions** at `features/step_definitions/<feature-slug>-stable.steps.js`
671
+ **5. Create step definitions IN THE TEST WORKTREE** at `<worktree-path>/features/step_definitions/<feature-slug>-stable.steps.js`
661
672
 
662
- **5. Commit BDD files immediately:**
673
+ **6. Commit and merge the test worktree:**
663
674
 
664
675
  ```bash
676
+ # Commit in the test worktree
677
+ cd <worktree-path>
665
678
  git add features/
666
679
  git commit -m "test: Add stable mode BDD scenarios and step definitions
667
680
 
@@ -669,13 +682,12 @@ Added error handling and edge case scenarios for stable mode.
669
682
  - [N] new stable mode scenarios
670
683
  - Step definitions for validation and error handling"
671
684
 
672
- git push
685
+ # Return to main repo and merge
686
+ cd <main-repo-path>
687
+ jettypod work merge <feature-id>
673
688
  ```
674
689
 
675
- **🛑 CRITICAL:** Commit IMMEDIATELY after generating files. Do NOT wait for user confirmation.
676
- Other assistants may be working in parallel - uncommitted files will block their merges.
677
-
678
- **6. Present proposal to user:**
690
+ **7. Present proposal to user:**
679
691
 
680
692
  ```
681
693
  📋 Stable Mode Transition
@@ -826,6 +838,12 @@ jettypod work merge --release-lock # Release held lock
826
838
  jettypod work start <chore-id> # Create worktree and start chore
827
839
  ```
828
840
 
841
+ **Create test worktree (for writing BDD scenarios):**
842
+ ```bash
843
+ jettypod work tests <feature-id> # Create worktree for writing tests
844
+ jettypod work merge <feature-id> # Merge tests back to main
845
+ ```
846
+
829
847
  **Set feature mode (BEFORE creating chores):**
830
848
  ```bash
831
849
  jettypod work set-mode <feature-id> stable