jettypod 4.4.66 → 4.4.68

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/jettypod.js CHANGED
@@ -270,6 +270,7 @@ jettypod work create feature "<title>" --parent=<id>
270
270
  jettypod work create chore "<title>" --parent=<id>
271
271
  jettypod work start <id> # Creates worktree branch
272
272
  jettypod work merge # Merges worktree back to main
273
+ jettypod work cleanup <id> # Cleanup worktree after merge
273
274
  jettypod work tests <feature-id> # Create test worktree for BDD
274
275
  jettypod work tests merge <id> # Merge tests to main
275
276
  jettypod work status <id> cancelled
@@ -1294,18 +1295,26 @@ switch (command) {
1294
1295
  }
1295
1296
  } else if (subcommand === 'cleanup') {
1296
1297
  const workCommands = require('./lib/work-commands/index.js');
1297
- const dryRun = args[0] === '--dry-run';
1298
+ const dryRun = args.includes('--dry-run');
1299
+ // Find numeric arg for specific work item cleanup
1300
+ const workItemId = args.find(a => /^\d+$/.test(a)) ? parseInt(args.find(a => /^\d+$/.test(a))) : null;
1298
1301
 
1299
1302
  try {
1300
- const results = await workCommands.cleanupWorktrees({ dryRun });
1301
-
1302
- if (results.cleaned === 0 && results.failed === 0) {
1303
- console.log(`\n${results.message || 'No worktrees needed cleanup'}`);
1303
+ if (workItemId) {
1304
+ // Clean up specific work item's worktree
1305
+ await workCommands.cleanupWorkItem(workItemId);
1304
1306
  } else {
1305
- console.log(`\n✓ Cleanup complete:`);
1306
- console.log(` Cleaned: ${results.cleaned}`);
1307
- if (results.failed > 0) {
1308
- console.log(` Failed: ${results.failed}`);
1307
+ // Batch cleanup of all orphaned worktrees
1308
+ const results = await workCommands.cleanupWorktrees({ dryRun });
1309
+
1310
+ if (results.cleaned === 0 && results.failed === 0) {
1311
+ console.log(`\n${results.message || 'No worktrees needed cleanup'}`);
1312
+ } else {
1313
+ console.log(`\n✓ Cleanup complete:`);
1314
+ console.log(` Cleaned: ${results.cleaned}`);
1315
+ if (results.failed > 0) {
1316
+ console.log(` Failed: ${results.failed}`);
1317
+ }
1309
1318
  }
1310
1319
  }
1311
1320
  } catch (err) {
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Migration: Add 'merged' status to worktrees table
3
+ *
4
+ * Purpose: Support separating merge from cleanup - worktrees stay after merge
5
+ * until explicitly cleaned up. This prevents shell CWD corruption when merge
6
+ * is run from inside the worktree.
7
+ *
8
+ * SQLite doesn't support ALTER CONSTRAINT, so we need to recreate the table.
9
+ */
10
+
11
+ module.exports = {
12
+ id: '018-worktrees-merged-status',
13
+ description: 'Add merged status to worktrees table for separate cleanup step',
14
+
15
+ async up(db) {
16
+ return new Promise((resolve, reject) => {
17
+ db.serialize(() => {
18
+ // 1. Create new table with updated constraint
19
+ db.run(`
20
+ CREATE TABLE IF NOT EXISTS worktrees_new (
21
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
22
+ work_item_id INTEGER NOT NULL,
23
+ branch_name TEXT NOT NULL,
24
+ worktree_path TEXT NOT NULL,
25
+ status TEXT NOT NULL CHECK(status IN ('active', 'merging', 'merged', 'cleanup_pending', 'corrupted')),
26
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
27
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
28
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
29
+ )
30
+ `, (err) => {
31
+ if (err) return reject(err);
32
+ });
33
+
34
+ // 2. Copy data from old table
35
+ db.run(`
36
+ INSERT INTO worktrees_new (id, work_item_id, branch_name, worktree_path, status, created_at, updated_at)
37
+ SELECT id, work_item_id, branch_name, worktree_path, status, created_at, updated_at
38
+ FROM worktrees
39
+ `, (err) => {
40
+ if (err) return reject(err);
41
+ });
42
+
43
+ // 3. Drop old indexes
44
+ db.run('DROP INDEX IF EXISTS idx_worktrees_work_item_id', (err) => {
45
+ if (err) return reject(err);
46
+ });
47
+
48
+ db.run('DROP INDEX IF EXISTS idx_worktrees_status', (err) => {
49
+ if (err) return reject(err);
50
+ });
51
+
52
+ // 4. Drop old table
53
+ db.run('DROP TABLE worktrees', (err) => {
54
+ if (err) return reject(err);
55
+ });
56
+
57
+ // 5. Rename new table
58
+ db.run('ALTER TABLE worktrees_new RENAME TO worktrees', (err) => {
59
+ if (err) return reject(err);
60
+ });
61
+
62
+ // 6. Recreate indexes
63
+ db.run(`
64
+ CREATE INDEX idx_worktrees_work_item_id
65
+ ON worktrees(work_item_id)
66
+ `, (err) => {
67
+ if (err) return reject(err);
68
+ });
69
+
70
+ db.run(`
71
+ CREATE INDEX idx_worktrees_status
72
+ ON worktrees(status)
73
+ `, (err) => {
74
+ if (err) return reject(err);
75
+ resolve();
76
+ });
77
+ });
78
+ });
79
+ },
80
+
81
+ async down(db) {
82
+ // Reverse migration - remove 'merged' status
83
+ // Any rows with 'merged' status will fail the constraint, so convert them first
84
+ return new Promise((resolve, reject) => {
85
+ db.serialize(() => {
86
+ // Convert any 'merged' status to 'cleanup_pending'
87
+ db.run(`UPDATE worktrees SET status = 'cleanup_pending' WHERE status = 'merged'`, (err) => {
88
+ if (err) return reject(err);
89
+ });
90
+
91
+ // Create table without 'merged' in constraint
92
+ db.run(`
93
+ CREATE TABLE IF NOT EXISTS worktrees_new (
94
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
95
+ work_item_id INTEGER NOT NULL,
96
+ branch_name TEXT NOT NULL,
97
+ worktree_path TEXT NOT NULL,
98
+ status TEXT NOT NULL CHECK(status IN ('active', 'merging', 'cleanup_pending', 'corrupted')),
99
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
100
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
101
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
102
+ )
103
+ `, (err) => {
104
+ if (err) return reject(err);
105
+ });
106
+
107
+ db.run(`
108
+ INSERT INTO worktrees_new (id, work_item_id, branch_name, worktree_path, status, created_at, updated_at)
109
+ SELECT id, work_item_id, branch_name, worktree_path, status, created_at, updated_at
110
+ FROM worktrees
111
+ `, (err) => {
112
+ if (err) return reject(err);
113
+ });
114
+
115
+ db.run('DROP INDEX IF EXISTS idx_worktrees_work_item_id', (err) => {
116
+ if (err) return reject(err);
117
+ });
118
+
119
+ db.run('DROP INDEX IF EXISTS idx_worktrees_status', (err) => {
120
+ if (err) return reject(err);
121
+ });
122
+
123
+ db.run('DROP TABLE worktrees', (err) => {
124
+ if (err) return reject(err);
125
+ });
126
+
127
+ db.run('ALTER TABLE worktrees_new RENAME TO worktrees', (err) => {
128
+ if (err) return reject(err);
129
+ });
130
+
131
+ db.run(`
132
+ CREATE INDEX idx_worktrees_work_item_id
133
+ ON worktrees(work_item_id)
134
+ `, (err) => {
135
+ if (err) return reject(err);
136
+ });
137
+
138
+ db.run(`
139
+ CREATE INDEX idx_worktrees_status
140
+ ON worktrees(status)
141
+ `, (err) => {
142
+ if (err) return reject(err);
143
+ resolve();
144
+ });
145
+ });
146
+ });
147
+ }
148
+ };
@@ -1130,6 +1130,106 @@ async function cleanupWorktrees(options = {}) {
1130
1130
  return results;
1131
1131
  }
1132
1132
 
1133
+ /**
1134
+ * Clean up a specific worktree after merge
1135
+ * Should be run from main repo after cd'ing out of the worktree
1136
+ * @param {number} workItemId - The work item ID to clean up
1137
+ * @returns {Promise<Object>} Result with success status
1138
+ */
1139
+ async function cleanupWorkItem(workItemId) {
1140
+ const db = getDb();
1141
+ const gitRoot = getGitRoot();
1142
+
1143
+ // Check we're not inside the worktree being deleted
1144
+ const cwd = process.cwd();
1145
+ if (cwd.includes('.jettypod-work')) {
1146
+ return Promise.reject(new Error(
1147
+ `Cannot cleanup from inside a worktree.\n\n` +
1148
+ `Run this first: cd ${gitRoot}`
1149
+ ));
1150
+ }
1151
+
1152
+ // Find the worktree for this work item
1153
+ const worktree = await new Promise((resolve, reject) => {
1154
+ db.get(
1155
+ `SELECT w.id, w.worktree_path, w.branch_name, w.status, wi.title
1156
+ FROM worktrees w
1157
+ JOIN work_items wi ON w.work_item_id = wi.id
1158
+ WHERE w.work_item_id = ?`,
1159
+ [workItemId],
1160
+ (err, row) => {
1161
+ if (err) return reject(err);
1162
+ resolve(row);
1163
+ }
1164
+ );
1165
+ });
1166
+
1167
+ if (!worktree) {
1168
+ console.log(`No worktree found for work item #${workItemId}`);
1169
+ return { success: true, message: 'No worktree to clean up' };
1170
+ }
1171
+
1172
+ if (worktree.status === 'active') {
1173
+ return Promise.reject(new Error(
1174
+ `Worktree for #${workItemId} is still active.\n` +
1175
+ `Run 'jettypod work merge ${workItemId}' first.`
1176
+ ));
1177
+ }
1178
+
1179
+ console.log(`Cleaning up worktree for #${workItemId}: ${worktree.title}`);
1180
+
1181
+ // Remove git worktree if it exists
1182
+ if (worktree.worktree_path && fs.existsSync(worktree.worktree_path)) {
1183
+ try {
1184
+ execSync(`git worktree remove "${worktree.worktree_path}" --force`, {
1185
+ cwd: gitRoot,
1186
+ stdio: 'pipe'
1187
+ });
1188
+ console.log('✅ Removed worktree directory');
1189
+ } catch (err) {
1190
+ console.log(`⚠️ Failed to remove worktree directory: ${err.message}`);
1191
+ }
1192
+ }
1193
+
1194
+ // Delete the branch if it exists
1195
+ if (worktree.branch_name) {
1196
+ try {
1197
+ execSync(`git branch -d "${worktree.branch_name}"`, {
1198
+ cwd: gitRoot,
1199
+ stdio: 'pipe'
1200
+ });
1201
+ console.log('✅ Deleted branch');
1202
+ } catch (err) {
1203
+ // Branch might not exist or might not be fully merged
1204
+ // Try force delete if regular delete failed
1205
+ try {
1206
+ execSync(`git branch -D "${worktree.branch_name}"`, {
1207
+ cwd: gitRoot,
1208
+ stdio: 'pipe'
1209
+ });
1210
+ console.log('✅ Deleted branch (force)');
1211
+ } catch {
1212
+ console.log(`⚠️ Could not delete branch: ${err.message}`);
1213
+ }
1214
+ }
1215
+ }
1216
+
1217
+ // Delete worktree record from database
1218
+ await new Promise((resolve, reject) => {
1219
+ db.run(
1220
+ `DELETE FROM worktrees WHERE id = ?`,
1221
+ [worktree.id],
1222
+ (err) => {
1223
+ if (err) return reject(err);
1224
+ resolve();
1225
+ }
1226
+ );
1227
+ });
1228
+
1229
+ console.log('✅ Worktree cleaned up');
1230
+ return { success: true };
1231
+ }
1232
+
1133
1233
  // Re-export getCurrentWork from shared module for backwards compatibility
1134
1234
  // (used by jettypod.js)
1135
1235
 
@@ -1590,49 +1690,32 @@ async function mergeWork(options = {}) {
1590
1690
  );
1591
1691
  });
1592
1692
 
1593
- // Clean up worktree if it exists
1594
- if (worktree && worktree.worktree_path && fs.existsSync(worktree.worktree_path)) {
1595
- // Check if shell CWD is inside the worktree being deleted
1596
- const shellCwd = process.cwd();
1597
- const worktreePath = path.resolve(worktree.worktree_path);
1598
- const cwdWillBeInvalid = shellCwd.startsWith(worktreePath);
1599
-
1600
- console.log('Cleaning up worktree...');
1601
- try {
1602
- // Remove the git worktree
1603
- execSync(`git worktree remove "${worktree.worktree_path}" --force`, {
1604
- cwd: gitRoot,
1605
- stdio: 'pipe'
1606
- });
1607
-
1608
- // Delete worktree record from database
1609
- await new Promise((resolve, reject) => {
1610
- db.run(
1611
- `DELETE FROM worktrees WHERE id = ?`,
1612
- [worktree.id],
1613
- (err) => {
1614
- if (err) return reject(err);
1615
- resolve();
1616
- }
1617
- );
1618
- });
1619
-
1620
- console.log('✅ Worktree cleaned up');
1621
-
1622
- // Warn if shell CWD was inside deleted worktree
1623
- if (cwdWillBeInvalid) {
1624
- console.log('');
1625
- console.log('⚠️ Your shell was inside the deleted worktree.');
1626
- console.log(` Run this to fix: cd ${gitRoot}`);
1627
- }
1628
- } catch (worktreeErr) {
1629
- console.warn(`Warning: Failed to clean up worktree: ${worktreeErr.message}`);
1630
- // Non-fatal - continue with merge success
1631
- }
1693
+ // Mark worktree as merged but DON'T delete it yet
1694
+ // This prevents shell CWD corruption when merge is run from inside the worktree
1695
+ // User must run `jettypod work cleanup` separately after cd'ing to main repo
1696
+ if (worktree && worktree.worktree_path) {
1697
+ await new Promise((resolve, reject) => {
1698
+ db.run(
1699
+ `UPDATE worktrees SET status = 'merged' WHERE id = ?`,
1700
+ [worktree.id],
1701
+ (err) => {
1702
+ if (err) return reject(err);
1703
+ resolve();
1704
+ }
1705
+ );
1706
+ });
1632
1707
  }
1633
1708
 
1634
1709
  console.log(`✅ Work item #${currentWork.id} marked as done`);
1635
1710
 
1711
+ // Instruct user to cleanup worktree separately
1712
+ if (worktree && worktree.worktree_path && fs.existsSync(worktree.worktree_path)) {
1713
+ console.log('');
1714
+ console.log('📁 Worktree preserved. To clean up:');
1715
+ console.log(` cd ${gitRoot}`);
1716
+ console.log(` jettypod work cleanup ${currentWork.id}`);
1717
+ }
1718
+
1636
1719
  if (withTransition) {
1637
1720
  // Hold lock for transition phase (BDD generation)
1638
1721
  // Work is done and worktree is cleaned up, but lock is held so no other merges
@@ -1897,46 +1980,11 @@ async function testsMerge(featureId) {
1897
1980
  console.log('⚠️ Failed to push (non-fatal):', err.message);
1898
1981
  }
1899
1982
 
1900
- // Clean up the worktree
1901
- // Check if shell CWD is inside the worktree being deleted
1902
- const shellCwd = process.cwd();
1903
- const resolvedWorktreePath = path.resolve(worktreePath);
1904
- const cwdWillBeInvalid = shellCwd.startsWith(resolvedWorktreePath);
1905
-
1906
- try {
1907
- execSync(`git worktree remove "${worktreePath}" --force`, {
1908
- cwd: gitRoot,
1909
- encoding: 'utf8',
1910
- stdio: 'pipe'
1911
- });
1912
- console.log('✅ Removed worktree directory');
1913
-
1914
- // Warn if shell CWD was inside deleted worktree
1915
- if (cwdWillBeInvalid) {
1916
- console.log('');
1917
- console.log('⚠️ Your shell was inside the deleted worktree.');
1918
- console.log(` Run this to fix: cd ${gitRoot}`);
1919
- }
1920
- } catch (err) {
1921
- console.log('⚠️ Failed to remove worktree (non-fatal):', err.message);
1922
- }
1923
-
1924
- // Delete the test branch
1925
- try {
1926
- execSync(`git branch -d ${branchName}`, {
1927
- cwd: gitRoot,
1928
- encoding: 'utf8',
1929
- stdio: 'pipe'
1930
- });
1931
- console.log('✅ Deleted test branch');
1932
- } catch (err) {
1933
- console.log('⚠️ Failed to delete branch (non-fatal):', err.message);
1934
- }
1935
-
1936
- // Delete worktree record from database (cleanup complete)
1983
+ // Mark worktree as merged but DON'T delete it yet
1984
+ // This prevents shell CWD corruption when merge is run from inside the worktree
1937
1985
  await new Promise((resolve, reject) => {
1938
1986
  db.run(
1939
- `DELETE FROM worktrees WHERE id = ?`,
1987
+ `UPDATE worktrees SET status = 'merged' WHERE id = ?`,
1940
1988
  [worktree.id],
1941
1989
  (err) => {
1942
1990
  if (err) return reject(err);
@@ -1954,6 +2002,14 @@ async function testsMerge(featureId) {
1954
2002
  console.log('');
1955
2003
  console.log(`✅ Test worktree for feature #${featureId} merged successfully`);
1956
2004
 
2005
+ // Instruct user to cleanup worktree separately
2006
+ if (fs.existsSync(worktreePath)) {
2007
+ console.log('');
2008
+ console.log('📁 Worktree preserved. To clean up:');
2009
+ console.log(` cd ${gitRoot}`);
2010
+ console.log(` jettypod work cleanup ${featureId}`);
2011
+ }
2012
+
1957
2013
  return Promise.resolve();
1958
2014
  }
1959
2015
 
@@ -1962,6 +2018,7 @@ module.exports = {
1962
2018
  stopWork,
1963
2019
  getCurrentWork,
1964
2020
  cleanupWorktrees,
2021
+ cleanupWorkItem,
1965
2022
  mergeWork,
1966
2023
  testsWork,
1967
2024
  testsMerge
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jettypod",
3
- "version": "4.4.66",
3
+ "version": "4.4.68",
4
4
  "description": "AI-powered development workflow manager with TDD, BDD, and automatic test generation",
5
5
  "main": "jettypod.js",
6
6
  "bin": {
@@ -373,22 +373,23 @@ git commit -m "chore: [brief description]"
373
373
  git push
374
374
  ```
375
375
 
376
- **🚨 CRITICAL: Shell CWD Corruption Prevention**
376
+ **Merge and cleanup (3 steps):**
377
377
 
378
- The merge will delete the worktree. You must be in the main repo BEFORE merging.
378
+ ```bash
379
+ # Step 1: Merge (can run from worktree - it won't delete it)
380
+ jettypod work merge [chore-id]
381
+ ```
379
382
 
380
383
  ```bash
381
- # First, cd to the main repo (worktrees are in .jettypod-work/ inside main repo)
384
+ # Step 2: cd to main repo
382
385
  cd /path/to/main/repo
383
-
384
- # Verify you're in the main repo
385
- pwd && ls .jettypod
386
-
387
- # Then merge (pass the work item ID explicitly)
388
- jettypod work merge [chore-id]
386
+ pwd && ls .jettypod # verify
389
387
  ```
390
388
 
391
- **Why not `cd $(git rev-parse --show-toplevel)/..`?** Inside a worktree, `--show-toplevel` returns the worktree path, not the main repo. Going `..` from there doesn't reach the main repo.
389
+ ```bash
390
+ # Step 3: Clean up the worktree (now safe since shell is in main repo)
391
+ jettypod work cleanup [chore-id]
392
+ ```
392
393
 
393
394
  **Display:**
394
395
 
@@ -19,7 +19,7 @@ When this skill is activated, you are helping discover the best approach for a f
19
19
  |---------|----------|------|-------|
20
20
  | `work implement <feature-id>` | Transition feature to implementation phase | After chores created (Step 8C) | Feature Planning |
21
21
  | `work tests start <feature-id>` | Create worktree for test authoring | After transition (Step 8D) | Feature Planning |
22
- | `cd <main-repo> && work tests merge <feature-id>` | Merge tests to main, cleanup worktree | After tests validated (Step 8D) | Feature Planning |
22
+ | `work tests merge` `cd` → `work cleanup` | Merge tests to main, then cleanup worktree | After tests validated (Step 8D) | Feature Planning |
23
23
  | `work start <chore-id>` | Start implementing a specific chore | After tests merged (Step 8E) | Speed Mode |
24
24
 
25
25
  **CRITICAL:** All commands are run by **Claude**, not the user. The distinction is:
@@ -653,8 +653,10 @@ Write your BDD files to:
653
653
  <worktree>/features/email-login.feature
654
654
  <worktree>/features/step_definitions/email-login.steps.js
655
655
 
656
- When done, cd to main repo and merge:
657
- cd /path/to/main/repo && jettypod work tests merge 42
656
+ When done, merge then cleanup:
657
+ jettypod work tests merge 42
658
+ cd /path/to/main/repo
659
+ jettypod work cleanup 42
658
660
  ```
659
661
 
660
662
  **🛑 STOP AND CHECK:** Verify worktree was created successfully. If you see an error, investigate before continuing.
@@ -736,21 +738,29 @@ sqlite3 .jettypod/work.db "UPDATE work_items SET scenario_file = 'features/${FEA
736
738
 
737
739
  **Sub-step 5: Merge tests to main**
738
740
 
739
- **⚠️ CRITICAL: The merge will delete the worktree.** Your shell is currently inside it. Chain commands to ensure shell is in main repo BEFORE deletion:
741
+ **Merge and cleanup (3 steps):**
740
742
 
741
743
  ```bash
742
- # CRITICAL: cd to main repo AND merge in SAME command
743
- # This ensures shell is in main repo BEFORE worktree deletion
744
- cd <main-repo-path> && jettypod work tests merge ${FEATURE_ID}
744
+ # Step 1: Merge tests (can run from worktree - it won't delete it)
745
+ jettypod work tests merge ${FEATURE_ID}
745
746
  ```
746
747
 
747
- If you run the merge while your shell is in the worktree, subsequent commands will fail with "No such file or directory".
748
+ ```bash
749
+ # Step 2: cd to main repo
750
+ cd <main-repo-path>
751
+ pwd && ls .jettypod # verify
752
+ ```
753
+
754
+ ```bash
755
+ # Step 3: Clean up the worktree (now safe since shell is in main repo)
756
+ jettypod work cleanup ${FEATURE_ID}
757
+ ```
748
758
 
749
759
  This will:
750
760
  - Commit changes in the worktree
751
761
  - Merge to main
752
762
  - Push to remote
753
- - Clean up the worktree
763
+ - Mark worktree as merged (cleanup is separate)
754
764
 
755
765
  **🛑 STOP AND CHECK:** Verify merge succeeded:
756
766
  - ✅ "Tests merged to main" → Proceed to Step 8E
@@ -920,6 +930,6 @@ Before completing feature planning, ensure:
920
930
  - [ ] **Test worktree created** with `work tests start` (Step 8D)
921
931
  - [ ] **BDD files written** in worktree (Step 8D)
922
932
  - [ ] **BDD infrastructure validated** with dry-run (Step 8D)
923
- - [ ] **Tests merged to main** with `cd <main-repo> && work tests merge` (Step 8D)
933
+ - [ ] **Tests merged to main** with `work tests merge` → `cd` → `work cleanup` (Step 8D)
924
934
  - [ ] First chore started with `work start [chore-id]` (Step 8E)
925
935
  - [ ] **Speed-mode skill invoked using Skill tool** (Step 8E)
@@ -613,28 +613,25 @@ More speed mode chores remain. Starting next chore:
613
613
 
614
614
  **Merge and start next:**
615
615
 
616
- **🚨 CRITICAL: Shell CWD Corruption Prevention**
617
-
618
- The merge will delete the worktree. You must be in the main repo BEFORE merging.
619
-
620
616
  ```bash
621
617
  # Commit changes in the worktree
622
618
  git add . && git commit -m "feat: [brief description of what was implemented]"
623
619
  ```
624
620
 
625
621
  ```bash
626
- # cd to the main repo first (worktrees are in .jettypod-work/ inside main repo)
627
- cd /path/to/main/repo
628
-
629
- # Verify you're in the main repo
630
- pwd && ls .jettypod
631
-
632
- # Then merge
622
+ # Step 1: Merge (can run from worktree - it won't delete it)
633
623
  jettypod work merge [current-chore-id]
634
624
  ```
635
625
 
636
626
  ```bash
637
- # Start next chore
627
+ # Step 2: cd to main repo
628
+ cd /path/to/main/repo
629
+ pwd && ls .jettypod # verify
630
+ ```
631
+
632
+ ```bash
633
+ # Step 3: Clean up the worktree, then start next chore
634
+ jettypod work cleanup [current-chore-id]
638
635
  jettypod work start [next-chore-id]
639
636
  ```
640
637
 
@@ -674,29 +671,28 @@ npx cucumber-js <scenario-file-path> --name "User can reach" --format progress
674
671
 
675
672
  #### Step 7B: Merge Final Speed Chore
676
673
 
677
- **🚨 CRITICAL: Shell CWD Corruption Prevention**
678
-
679
- The merge will delete the worktree. You must be in the main repo BEFORE merging.
680
-
681
674
  ```bash
682
675
  # Commit changes in the worktree
683
676
  git add . && git commit -m "feat: [brief description of what was implemented]"
684
677
  ```
685
678
 
686
679
  ```bash
687
- # cd to the main repo first (worktrees are in .jettypod-work/ inside main repo)
688
- cd /path/to/main/repo
689
-
690
- # Verify you're in the main repo
691
- pwd && ls .jettypod
692
-
693
- # Then merge with transition flag
680
+ # Step 1: Merge with transition flag (can run from worktree - it won't delete it)
694
681
  jettypod work merge [current-chore-id] --with-transition
695
682
  ```
696
683
 
697
- **Why not `cd $(git rev-parse --show-toplevel)/..`?** Inside a worktree, `--show-toplevel` returns the worktree path, not the main repo. Going `..` from there doesn't reach the main repo.
684
+ ```bash
685
+ # Step 2: cd to main repo
686
+ cd /path/to/main/repo
687
+ pwd && ls .jettypod # verify
688
+ ```
689
+
690
+ ```bash
691
+ # Step 3: Clean up the worktree
692
+ jettypod work cleanup [current-chore-id]
693
+ ```
698
694
 
699
- After merge, you are on main branch. Ready to generate stable mode scenarios.
695
+ After cleanup, you are on main branch. Ready to generate stable mode scenarios.
700
696
 
701
697
  #### Step 7C: Generate and Propose Stable Mode Chores
702
698
 
@@ -770,20 +766,22 @@ Added error handling and edge case scenarios for stable mode.
770
766
  - Step definitions for validation and error handling"
771
767
  ```
772
768
 
769
+ **Merge and cleanup (3 steps):**
770
+
773
771
  ```bash
774
- # CRITICAL: cd to main repo AND merge in SAME command
775
- # This ensures shell is in main repo BEFORE worktree deletion
776
- cd <main-repo-path> && jettypod work tests merge <feature-id>
772
+ # Step 1: Merge tests (can run from worktree - it won't delete it)
773
+ jettypod work tests merge <feature-id>
777
774
  ```
778
775
 
779
776
  ```bash
780
- # MANDATORY: Verify shell is in main repo (run this immediately after merge)
781
- pwd && ls .jettypod
777
+ # Step 2: cd to main repo
778
+ cd <main-repo-path>
779
+ pwd && ls .jettypod # verify
782
780
  ```
783
781
 
784
- **If you see "No such file or directory" errors:** Your shell CWD was corrupted. Run:
785
782
  ```bash
786
- cd <main-repo-path>
783
+ # Step 3: Clean up the worktree (now safe since shell is in main repo)
784
+ jettypod work cleanup <feature-id>
787
785
  ```
788
786
 
789
787
  **7. Present proposal to user:**
@@ -948,8 +946,9 @@ jettypod work start <chore-id> # Create worktree and start chore
948
946
  **Create test worktree (for writing BDD scenarios):**
949
947
  ```bash
950
948
  jettypod work tests <feature-id> # Create worktree for writing tests
951
- # CRITICAL: cd to main repo BEFORE merge (worktree will be deleted)
952
- cd <main-repo> && jettypod work tests merge <feature-id>
949
+ jettypod work tests merge <feature-id> # Merge tests (preserves worktree)
950
+ cd <main-repo> # cd to main repo
951
+ jettypod work cleanup <feature-id> # Clean up worktree
953
952
  ```
954
953
 
955
954
  **Set feature mode (BEFORE creating chores):**
@@ -572,28 +572,25 @@ More stable mode chores remain. Starting next chore:
572
572
 
573
573
  **Merge and start next:**
574
574
 
575
- **🚨 CRITICAL: Shell CWD Corruption Prevention**
576
-
577
- The merge will delete the worktree. You must be in the main repo BEFORE merging.
578
-
579
575
  ```bash
580
576
  # Commit changes in the worktree
581
577
  git add . && git commit -m "feat: [brief description of error handling added]"
582
578
  ```
583
579
 
584
580
  ```bash
585
- # cd to the main repo first (worktrees are in .jettypod-work/ inside main repo)
586
- cd /path/to/main/repo
587
-
588
- # Verify you're in the main repo
589
- pwd && ls .jettypod
590
-
591
- # Then merge
581
+ # Step 1: Merge (can run from worktree - it won't delete it)
592
582
  jettypod work merge [current-chore-id]
593
583
  ```
594
584
 
595
585
  ```bash
596
- # Start next chore
586
+ # Step 2: cd to main repo
587
+ cd /path/to/main/repo
588
+ pwd && ls .jettypod # verify
589
+ ```
590
+
591
+ ```bash
592
+ # Step 3: Clean up the worktree, then start next chore
593
+ jettypod work cleanup [current-chore-id]
597
594
  jettypod work start [next-chore-id]
598
595
  ```
599
596
 
@@ -613,21 +610,24 @@ If the query returns no remaining chores, proceed to Step 7.
613
610
 
614
611
  **First, merge the final stable chore:**
615
612
 
616
- **🚨 CRITICAL: Shell CWD Corruption Prevention**
617
-
618
613
  ```bash
619
614
  git add . && git commit -m "feat: [brief description of error handling added]"
620
615
  ```
621
616
 
622
617
  ```bash
623
- # cd to the main repo first (worktrees are in .jettypod-work/ inside main repo)
624
- cd /path/to/main/repo
618
+ # Step 1: Merge (can run from worktree - it won't delete it)
619
+ jettypod work merge [current-chore-id]
620
+ ```
625
621
 
626
- # Verify you're in the main repo
627
- pwd && ls .jettypod
622
+ ```bash
623
+ # Step 2: cd to main repo
624
+ cd /path/to/main/repo
625
+ pwd && ls .jettypod # verify
626
+ ```
628
627
 
629
- # Then merge
630
- jettypod work merge [current-chore-id]
628
+ ```bash
629
+ # Step 3: Clean up the worktree
630
+ jettypod work cleanup [current-chore-id]
631
631
  ```
632
632
 
633
633
  **Then check project state:**