jettypod 4.4.34 → 4.4.36

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/hooks/pre-commit CHANGED
@@ -1,30 +1,61 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { exportAll } = require('jettypod/lib/db-export');
3
+ // JettyPod pre-commit hook: Block direct commits to main branch
4
+ // This is the standalone version used for BDD testing
5
+ // The production version is in lib/git-hooks/pre-commit
6
+
4
7
  const { execSync } = require('child_process');
8
+ const fs = require('fs');
9
+ const path = require('path');
5
10
 
6
- (async () => {
11
+ // Check if direct commits to main should be blocked
12
+ function checkBranchRestriction() {
7
13
  try {
8
- // Export databases to JSON
9
- await exportAll();
10
-
11
- // Stage the JSON files
12
- try {
13
- execSync('git add .jettypod/snapshots/work.json', { stdio: 'inherit' });
14
- execSync('git add .jettypod/snapshots/database.json', { stdio: 'inherit' });
15
- } catch (gitErr) {
16
- // Log warning but don't block commit
17
- console.error('Pre-commit hook warning: Failed to stage snapshot files');
18
- console.error(` ${gitErr.message}`);
19
- console.error(' Commit will proceed but snapshots may not be included');
14
+ // Get current branch name
15
+ const branch = execSync('git symbolic-ref --short HEAD', {
16
+ encoding: 'utf-8',
17
+ stdio: ['pipe', 'pipe', 'pipe']
18
+ }).trim();
19
+
20
+ // Only restrict main/master branches
21
+ if (branch !== 'main' && branch !== 'master') {
22
+ return true; // Allow commits on feature branches
20
23
  }
21
24
 
22
- // Always exit successfully to allow commit to proceed
23
- process.exit(0);
25
+ // Check if this is a merge commit (MERGE_HEAD exists)
26
+ const gitDir = execSync('git rev-parse --git-dir', {
27
+ encoding: 'utf-8',
28
+ stdio: ['pipe', 'pipe', 'pipe']
29
+ }).trim();
30
+
31
+ if (fs.existsSync(path.join(gitDir, 'MERGE_HEAD'))) {
32
+ return true; // Allow merge commits on main
33
+ }
34
+
35
+ // Block direct commits to main
36
+ console.error('');
37
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
38
+ console.error('❌ Direct commits to main are not allowed');
39
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
40
+ console.error('');
41
+ console.error('Use the JettyPod workflow instead:');
42
+ console.error('');
43
+ console.error(' jettypod work start <work-item-id>');
44
+ console.error('');
45
+ console.error('This creates a feature branch where you can commit freely.');
46
+ console.error('When done, use \'jettypod work done\' to merge back to main.');
47
+ console.error('');
48
+ return false;
24
49
  } catch (err) {
25
- // Log warning but don't block commit
26
- console.error('Pre-commit hook warning:', err.message);
27
- console.error(' Commit will proceed but snapshots were not updated');
28
- process.exit(0);
50
+ // If we can't determine branch (detached HEAD, etc.), allow the commit
51
+ return true;
29
52
  }
30
- })();
53
+ }
54
+
55
+ // Check branch restriction first
56
+ if (!checkBranchRestriction()) {
57
+ process.exit(1);
58
+ }
59
+
60
+ // Allow commit
61
+ process.exit(0);
package/jettypod.js CHANGED
@@ -257,6 +257,11 @@ Skills auto-activate and MUST complete their full workflow:
257
257
  ❌ DO NOT manually create chores when a skill should generate them
258
258
  ✅ ALWAYS let skills complete autonomously before taking manual actions
259
259
 
260
+ ## 🔄 Session Start: Check for Interrupted Workflows
261
+ On session start, run: \`jettypod workflow resume\`
262
+ If an interrupted workflow is found, ASK the user if they want to resume it.
263
+ Do not assume - the user may want to start fresh or work on something else.
264
+
260
265
  ## Basic Commands (for non-workflow operations)
261
266
  jettypod work create epic "<title>"
262
267
  jettypod work create feature "<title>" --parent=<id>
@@ -2297,40 +2302,292 @@ Quick commands:
2297
2302
  case 'workflow': {
2298
2303
  const workflowSubcommand = args[0];
2299
2304
 
2300
- if (workflowSubcommand === 'resume') {
2301
- const { getDb } = require('./lib/database');
2302
- const { getCheckpoint, getCurrentBranch } = require('./lib/workflow-checkpoint');
2305
+ if (workflowSubcommand === 'status') {
2306
+ const { getDb, waitForMigrations } = require('./lib/database');
2307
+ const { getCurrentBranch } = require('./lib/workflow-checkpoint');
2308
+
2309
+ try {
2310
+ const db = getDb();
2311
+ await waitForMigrations();
2312
+ const currentBranch = getCurrentBranch();
2313
+
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 || []);
2321
+ }
2322
+ );
2323
+ });
2324
+
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
+ 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';
2358
+
2359
+ 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
2366
+ );
2367
+ }
2368
+
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');
2376
+ }
2377
+ } catch (err) {
2378
+ console.error(`Error: ${err.message}`);
2379
+ process.exit(1);
2380
+ }
2381
+ } else if (workflowSubcommand === 'show') {
2382
+ const workflowId = parseInt(args[1]);
2383
+
2384
+ if (!workflowId) {
2385
+ console.log('Usage: jettypod workflow show <id>');
2386
+ console.log('');
2387
+ console.log('Use "jettypod workflow status" to see available workflow IDs.');
2388
+ process.exit(1);
2389
+ }
2390
+
2391
+ const { getDb, waitForMigrations } = require('./lib/database');
2303
2392
 
2304
2393
  try {
2305
2394
  const db = getDb();
2306
- const branchName = getCurrentBranch();
2307
- const checkpoint = await getCheckpoint(db, branchName);
2395
+ await waitForMigrations();
2396
+
2397
+ const checkpoint = await new Promise((resolve, reject) => {
2398
+ db.get(
2399
+ `SELECT * FROM workflow_checkpoints WHERE id = ?`,
2400
+ [workflowId],
2401
+ (err, row) => {
2402
+ if (err) reject(err);
2403
+ else resolve(row);
2404
+ }
2405
+ );
2406
+ });
2308
2407
 
2309
2408
  if (!checkpoint) {
2310
- console.log('No interrupted workflow found');
2409
+ console.log(`Error: No workflow found with ID ${workflowId}`);
2410
+ console.log('');
2411
+ console.log('Use "jettypod workflow status" to see available workflows.');
2412
+ process.exit(1);
2413
+ }
2414
+
2415
+ const stepInfo = checkpoint.total_steps
2416
+ ? `Step ${checkpoint.current_step} of ${checkpoint.total_steps}`
2417
+ : `Step ${checkpoint.current_step}`;
2418
+
2419
+ console.log('Workflow Details');
2420
+ console.log('─'.repeat(50));
2421
+ console.log('');
2422
+ console.log(` ID: ${checkpoint.id}`);
2423
+ console.log(` Skill: ${checkpoint.skill_name}`);
2424
+ console.log(` Progress: ${stepInfo}`);
2425
+ console.log(` Work Item: ${checkpoint.work_item_id ? '#' + checkpoint.work_item_id : '-'}`);
2426
+ console.log(` Branch: ${checkpoint.branch_name}`);
2427
+ console.log(` Started: ${checkpoint.created_at}`);
2428
+ console.log(` Last Active: ${checkpoint.updated_at}`);
2429
+ console.log('');
2430
+ console.log('Actions:');
2431
+ console.log(` jettypod workflow resume ${workflowId} Continue this workflow`);
2432
+ console.log(` jettypod workflow abort ${workflowId} Cancel and clean up`);
2433
+ } catch (err) {
2434
+ console.error(`Error: ${err.message}`);
2435
+ process.exit(1);
2436
+ }
2437
+ } else if (workflowSubcommand === 'resume') {
2438
+ const { getDb, waitForMigrations } = require('./lib/database');
2439
+ const { getCheckpoint, getCurrentBranch } = require('./lib/workflow-checkpoint');
2440
+
2441
+ try {
2442
+ const db = getDb();
2443
+ await waitForMigrations();
2444
+ const currentBranch = getCurrentBranch();
2445
+ const workflowId = args[1] ? parseInt(args[1]) : null;
2446
+
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
+ });
2461
+
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
+ }
2311
2468
  } else {
2312
- const stepInfo = checkpoint.total_steps
2313
- ? `Step ${checkpoint.current_step} of ${checkpoint.total_steps}`
2314
- : `Step ${checkpoint.current_step}`;
2469
+ // No ID - check current branch
2470
+ checkpoint = await getCheckpoint(db, currentBranch);
2315
2471
 
2316
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2317
- console.log('Found interrupted workflow');
2318
- console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
2319
- console.log(`Skill: ${checkpoint.skill_name}`);
2320
- console.log(stepInfo);
2321
- if (checkpoint.work_item_id) {
2322
- console.log(`Work Item: #${checkpoint.work_item_id}`);
2472
+ if (!checkpoint) {
2473
+ console.log('No interrupted workflow on current branch.');
2474
+ 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);
2323
2478
  }
2324
2479
  }
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
+ } catch (err) {
2506
+ console.error(`Error: ${err.message}`);
2507
+ process.exit(1);
2508
+ }
2509
+ } else if (workflowSubcommand === 'abort') {
2510
+ const workflowId = parseInt(args[1]);
2511
+
2512
+ if (!workflowId) {
2513
+ console.log('Usage: jettypod workflow abort <id>');
2514
+ console.log('');
2515
+ console.log('Use "jettypod workflow status" to see workflow IDs.');
2516
+ process.exit(1);
2517
+ }
2518
+
2519
+ const { getDb, waitForMigrations } = require('./lib/database');
2520
+
2521
+ try {
2522
+ const db = getDb();
2523
+ await waitForMigrations();
2524
+
2525
+ // First fetch the checkpoint to display info
2526
+ const checkpoint = await new Promise((resolve, reject) => {
2527
+ db.get(
2528
+ `SELECT * FROM workflow_checkpoints WHERE id = ?`,
2529
+ [workflowId],
2530
+ (err, row) => {
2531
+ if (err) reject(err);
2532
+ else resolve(row);
2533
+ }
2534
+ );
2535
+ });
2536
+
2537
+ if (!checkpoint) {
2538
+ console.log(`Error: No workflow found with ID ${workflowId}`);
2539
+ console.log('');
2540
+ console.log('Use "jettypod workflow status" to see available workflows.');
2541
+ process.exit(1);
2542
+ }
2543
+
2544
+ // Delete the checkpoint
2545
+ await new Promise((resolve, reject) => {
2546
+ db.run(
2547
+ `DELETE FROM workflow_checkpoints WHERE id = ?`,
2548
+ [workflowId],
2549
+ (err) => {
2550
+ if (err) reject(err);
2551
+ else resolve();
2552
+ }
2553
+ );
2554
+ });
2555
+
2556
+ // Display abort confirmation
2557
+ const stepInfo = checkpoint.total_steps
2558
+ ? `Step ${checkpoint.current_step} of ${checkpoint.total_steps}`
2559
+ : `Step ${checkpoint.current_step}`;
2560
+
2561
+ console.log('Abort Workflow');
2562
+ console.log('─'.repeat(50));
2563
+ console.log('');
2564
+ console.log(` Skill: ${checkpoint.skill_name}`);
2565
+ console.log(` Progress: ${stepInfo}`);
2566
+ console.log(` Work Item: ${checkpoint.work_item_id ? '#' + checkpoint.work_item_id : '-'}`);
2567
+ console.log(` Branch: ${checkpoint.branch_name}`);
2568
+ console.log('');
2569
+ console.log('This will:');
2570
+ console.log(' - Delete the checkpoint from the database');
2571
+ console.log(' - NOT delete the branch or any code changes');
2572
+ console.log(' - Allow you to restart the skill fresh');
2573
+ console.log('');
2574
+ console.log('Checkpoint deleted. Workflow aborted.');
2575
+ console.log('');
2576
+ if (checkpoint.work_item_id) {
2577
+ console.log(`To restart: jettypod work start ${checkpoint.work_item_id}`);
2578
+ }
2325
2579
  } catch (err) {
2326
2580
  console.error(`Error: ${err.message}`);
2327
2581
  process.exit(1);
2328
2582
  }
2329
2583
  } else {
2330
- console.log('Usage: jettypod workflow resume');
2584
+ console.log('Usage: jettypod workflow <command>');
2331
2585
  console.log('');
2332
2586
  console.log('Commands:');
2333
- console.log(' resume Check for and resume interrupted workflows');
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');
2334
2591
  }
2335
2592
  break;
2336
2593
  }
@@ -1,11 +1,60 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // Pre-commit hook: Run tests before allowing commit
3
+ // Pre-commit hook: Block direct commits to main + run tests
4
4
 
5
5
  const { execSync } = require('child_process');
6
6
  const fs = require('fs');
7
7
  const path = require('path');
8
8
 
9
+ // Check if direct commits to main should be blocked
10
+ function checkBranchRestriction() {
11
+ try {
12
+ // Get current branch name
13
+ const branch = execSync('git symbolic-ref --short HEAD', {
14
+ encoding: 'utf-8',
15
+ stdio: ['pipe', 'pipe', 'pipe']
16
+ }).trim();
17
+
18
+ // Only restrict main/master branches
19
+ if (branch !== 'main' && branch !== 'master') {
20
+ return true; // Allow commits on feature branches
21
+ }
22
+
23
+ // Check if this is a merge commit (MERGE_HEAD exists)
24
+ const gitDir = execSync('git rev-parse --git-dir', {
25
+ encoding: 'utf-8',
26
+ stdio: ['pipe', 'pipe', 'pipe']
27
+ }).trim();
28
+
29
+ if (fs.existsSync(path.join(gitDir, 'MERGE_HEAD'))) {
30
+ return true; // Allow merge commits on main
31
+ }
32
+
33
+ // Block direct commits to main
34
+ console.error('');
35
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
36
+ console.error('❌ Direct commits to main are not allowed');
37
+ console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
38
+ console.error('');
39
+ console.error('Use the JettyPod workflow instead:');
40
+ console.error('');
41
+ console.error(' jettypod work start <work-item-id>');
42
+ console.error('');
43
+ console.error('This creates a feature branch where you can commit freely.');
44
+ console.error('When done, use \'jettypod work done\' to merge back to main.');
45
+ console.error('');
46
+ return false;
47
+ } catch (err) {
48
+ // If we can't determine branch (detached HEAD, etc.), allow the commit
49
+ return true;
50
+ }
51
+ }
52
+
53
+ // First check branch restriction
54
+ if (!checkBranchRestriction()) {
55
+ process.exit(1);
56
+ }
57
+
9
58
  // Check if we're in a real project (not a test directory)
10
59
  const packageJsonPath = path.join(process.cwd(), 'package.json');
11
60
  if (!fs.existsSync(packageJsonPath)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.34",
3
+ "version": "4.4.36",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {