jettypod 4.1.2 → 4.1.4

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.
Files changed (179) hide show
  1. package/.nvmrc +1 -0
  2. package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
  3. package/docs/DECISIONS.md +10 -12
  4. package/docs/NODE_VERSION.md +83 -0
  5. package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
  6. package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
  7. package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
  8. package/hooks/post-checkout +17 -0
  9. package/hooks/post-merge +17 -0
  10. package/hooks/pre-commit +30 -0
  11. package/jettypod.js +259 -120
  12. package/lib/coverage-tracker.js +218 -0
  13. package/lib/database.js +2 -0
  14. package/lib/db-export.js +192 -0
  15. package/lib/db-import.js +193 -0
  16. package/lib/external-transition-handler.js +32 -0
  17. package/lib/git-hook-helpers.js +174 -0
  18. package/lib/git-root.js +90 -0
  19. package/lib/infrastructure-chore-generator.js +45 -0
  20. package/lib/install-hooks.js +52 -0
  21. package/lib/jettypod-backup.js +238 -0
  22. package/lib/merge-lock.js +193 -0
  23. package/lib/migrations/012-add-worktree-path.js +38 -0
  24. package/lib/migrations/013-worktrees-table.js +86 -0
  25. package/lib/migrations/014-migrate-worktree-data.js +161 -0
  26. package/lib/migrations/015-merge-locks-table.js +67 -0
  27. package/lib/pattern-finder.js +152 -0
  28. package/lib/process-manager.js +140 -0
  29. package/lib/production-standards-reader.js +13 -2
  30. package/lib/production-standards-writer.js +85 -0
  31. package/lib/skills/feature-planning/dry-run-validator.js +135 -0
  32. package/lib/skills/feature-planning/validation-formatter.js +160 -0
  33. package/lib/smart-conflict-detection.js +168 -0
  34. package/lib/smart-fetch-rebase.js +614 -0
  35. package/lib/step-definition-parser.js +76 -0
  36. package/lib/unit-test-generator.js +232 -0
  37. package/lib/verification-command-generator.js +66 -0
  38. package/lib/worktree-diagnostics.js +413 -0
  39. package/lib/worktree-facade.js +174 -0
  40. package/lib/worktree-manager.js +636 -0
  41. package/lib/worktree-reconciler.js +429 -0
  42. package/package.json +30 -3
  43. package/skills-templates/external-transition/SKILL.md +34 -3
  44. package/skills-templates/feature-planning/SKILL.md +190 -24
  45. package/skills-templates/production-mode/SKILL.md +127 -9
  46. package/skills-templates/speed-mode/SKILL.md +454 -51
  47. package/skills-templates/stable-mode/SKILL.md +285 -76
  48. package/.claude/PROTECT_SKILLS.md +0 -28
  49. package/.claude/settings.json +0 -24
  50. package/.claude/settings.local.json +0 -16
  51. package/.claude/skills/epic-planning/SKILL.md +0 -297
  52. package/.claude/skills/external-transition/SKILL.md +0 -384
  53. package/.claude/skills/feature-planning/SKILL.md +0 -464
  54. package/.claude/skills/production-mode/SKILL.md +0 -369
  55. package/.claude/skills/speed-mode/SKILL.md +0 -481
  56. package/.claude/skills/stable-mode/SKILL.md +0 -713
  57. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
  58. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
  59. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
  60. package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
  61. package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
  62. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
  63. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
  64. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
  65. package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
  66. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
  67. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
  68. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
  69. package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
  70. package/.devpod/current-work.json +0 -10
  71. package/.devpod/work.db +0 -0
  72. package/.github/workflows/test-safety.yml +0 -85
  73. package/.jettypod/config.json +0 -5
  74. package/.jettypod/current-work.json +0 -10
  75. package/.jettypod/hooks/README.md +0 -77
  76. package/.jettypod/hooks/protect-claude-md.js +0 -338
  77. package/.jettypod/test-work.db +0 -0
  78. package/.jettypod/work.db +0 -0
  79. package/CLAUDE.md +0 -49
  80. package/SPEED-STABLE-AUDIT.md +0 -853
  81. package/SYSTEM-BEHAVIOR.md +0 -2199
  82. package/TEST_SAFETY_AUDIT.md +0 -314
  83. package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
  84. package/cucumber-report.html +0 -45
  85. package/dist/devpod-linux +0 -0
  86. package/dist/devpod-macos +0 -0
  87. package/dist/devpod-win.exe +0 -0
  88. package/docs/features/jettypod-standards-explained.md +0 -543
  89. package/docs/features/standards-inventory.md +0 -257
  90. package/features/auto-generate-production-chores.feature +0 -13
  91. package/features/backlog-command.feature +0 -26
  92. package/features/backlog-filtering-production.feature +0 -10
  93. package/features/claude-md-protection/steps.js +0 -498
  94. package/features/decisions/index.js +0 -490
  95. package/features/decisions/index.test.js +0 -208
  96. package/features/fix-text-wrapping.feature +0 -42
  97. package/features/git-hooks/git-hooks.feature +0 -30
  98. package/features/git-hooks/index.js +0 -93
  99. package/features/git-hooks/index.test.js +0 -137
  100. package/features/git-hooks/post-commit +0 -56
  101. package/features/git-hooks/post-merge +0 -47
  102. package/features/git-hooks/pre-commit +0 -28
  103. package/features/git-hooks/simple-steps.js +0 -53
  104. package/features/git-hooks/simple-test.feature +0 -10
  105. package/features/git-hooks/steps.js +0 -196
  106. package/features/jettypod-update-command.feature +0 -46
  107. package/features/mode-prompts/index.js +0 -95
  108. package/features/mode-prompts/simple-steps.js +0 -44
  109. package/features/mode-prompts/simple-test.feature +0 -9
  110. package/features/mode-prompts/validation.test.js +0 -120
  111. package/features/multiple-claude-instances.feature +0 -121
  112. package/features/production-mode-skill.feature +0 -121
  113. package/features/refactor-mode/steps.js +0 -217
  114. package/features/refactor-mode.feature +0 -49
  115. package/features/simplify-external-transition.feature +0 -166
  116. package/features/skills-update/index.test.js +0 -216
  117. package/features/step_definitions/backlog-command.steps.js +0 -37
  118. package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
  119. package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
  120. package/features/step_definitions/production-mode-skill.steps.js +0 -862
  121. package/features/step_definitions/simplify-external-transition.steps.js +0 -370
  122. package/features/step_definitions/terminal-logo.steps.js +0 -145
  123. package/features/step_definitions/update-command.steps.js +0 -183
  124. package/features/support/hooks.js +0 -9
  125. package/features/terminal-logo/index.js +0 -39
  126. package/features/terminal-logo/terminal-logo.feature +0 -30
  127. package/features/update-command/index.js +0 -181
  128. package/features/update-command/index.test.js +0 -225
  129. package/features/work-commands/bug-workflow-display.feature +0 -22
  130. package/features/work-commands/index.js +0 -498
  131. package/features/work-commands/simple-steps.js +0 -69
  132. package/features/work-commands/stable-tests.feature +0 -57
  133. package/features/work-commands/steps.js +0 -1174
  134. package/features/work-commands/validation.test.js +0 -88
  135. package/features/work-commands/work-commands.feature +0 -13
  136. package/features/work-tracking/discovery-validation.test.js +0 -228
  137. package/features/work-tracking/index.js +0 -1921
  138. package/features/work-tracking/mode-required.feature +0 -112
  139. package/features/work-tracking/phase-tracking.test.js +0 -482
  140. package/features/work-tracking/prototype-tracking.test.js +0 -485
  141. package/features/work-tracking/tree-view.test.js +0 -310
  142. package/features/work-tracking/work-set-mode.feature +0 -71
  143. package/features/work-tracking/work-start-mode.feature +0 -88
  144. package/full-test.txt +0 -0
  145. package/lib/bug-workflow.test.js +0 -177
  146. package/lib/claudemd.test.js +0 -195
  147. package/lib/config.test.js +0 -511
  148. package/lib/constants.test.js +0 -164
  149. package/lib/current-work.test.js +0 -146
  150. package/lib/database-project-config.test.js +0 -111
  151. package/lib/database.test.js +0 -106
  152. package/lib/decisions-generator.test.js +0 -457
  153. package/lib/decisions-helpers.test.js +0 -310
  154. package/lib/git-coordinator.js +0 -167
  155. package/lib/git.test.js +0 -145
  156. package/lib/migrations/002-default-work-item-modes.test.js +0 -351
  157. package/lib/production-chore-generator.test.js +0 -432
  158. package/lib/production-context-detector.test.js +0 -277
  159. package/lib/production-scenario-appender.test.js +0 -235
  160. package/lib/production-scenario-validator.test.js +0 -246
  161. package/lib/production-standards-reader.test.js +0 -270
  162. package/lib/project-state.test.js +0 -92
  163. package/lib/push-queue.js +0 -417
  164. package/lib/queue-processor.js +0 -74
  165. package/lib/test-helpers.js +0 -202
  166. package/lib/test-helpers.test.js +0 -255
  167. package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
  168. package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
  169. package/prototypes/2025-01-11-production-mode-guided.js +0 -217
  170. package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
  171. package/prototypes/2025-01-11-production-standards-example.md +0 -204
  172. package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
  173. package/prototypes/test/index.html +0 -1
  174. package/setup-dist-repo.sh +0 -68
  175. package/test-production-standards-engine.js +0 -130
  176. package/test-results.json +0 -2195
  177. package/test-safety-check.sh +0 -80
  178. package/work-item-tracking-plan.md +0 -199
  179. /package/{.jettypod/devpod.db → jettypod.db} +0 -0
@@ -0,0 +1,636 @@
1
+ /**
2
+ * Worktree Manager - Database as Single Source of Truth
3
+ *
4
+ * This module manages all worktree operations with the database as the single
5
+ * source of truth. All state is persisted BEFORE any git/filesystem operations.
6
+ *
7
+ * Core principles:
8
+ * - Database is the source of truth (not git or filesystem)
9
+ * - All operations are transactional (rollback on failure)
10
+ * - Branch names are ALWAYS stored in DB (never lost)
11
+ * - Operations are idempotent (safe to retry)
12
+ */
13
+
14
+ // Database will be required lazily when implementing actual functions
15
+ // const getDb = require('./database');
16
+
17
+ /**
18
+ * Valid worktree statuses
19
+ */
20
+ const VALID_STATUSES = ['active', 'merging', 'cleanup_pending', 'corrupted'];
21
+
22
+ /**
23
+ * Create a worktree for a work item
24
+ *
25
+ * @param {Object} workItem - Work item from database (must have id and title)
26
+ * @param {Object} options - Optional configuration
27
+ * @param {string} options.repoPath - Path to git repository (defaults to process.cwd())
28
+ * @param {Object} options.db - Database connection (defaults to global database)
29
+ * @returns {Promise<Object>} Worktree record with id, work_item_id, branch_name, worktree_path, status
30
+ */
31
+ async function createWorktree(workItem, options = {}) {
32
+ if (!workItem || !workItem.id) {
33
+ return Promise.reject(new Error('Invalid work item: must have id'));
34
+ }
35
+
36
+ const fs = require('fs');
37
+ const path = require('path');
38
+ const { execSync } = require('child_process');
39
+ const { slugify } = require('./git');
40
+
41
+ // CRITICAL SAFETY: repoPath MUST be provided - never auto-detect from cwd
42
+ // Auto-detection from worktrees causes catastrophic repo corruption
43
+ if (!options.repoPath) {
44
+ return Promise.reject(new Error('SAFETY: repoPath must be explicitly provided to createWorktree'));
45
+ }
46
+
47
+ const gitRoot = options.repoPath;
48
+
49
+ // SAFETY CHECK: Verify gitRoot is actually the main repository
50
+ try {
51
+ const gitRootCheck = execSync('git rev-parse --show-toplevel', {
52
+ cwd: gitRoot,
53
+ encoding: 'utf8'
54
+ }).trim();
55
+
56
+ // Resolve both paths to handle /var vs /private/var symlink on macOS
57
+ const resolvedGitRoot = fs.realpathSync(gitRoot);
58
+ const resolvedGitRootCheck = fs.realpathSync(gitRootCheck);
59
+
60
+ if (resolvedGitRootCheck !== resolvedGitRoot) {
61
+ return Promise.reject(new Error(`SAFETY: Provided repoPath ${gitRoot} does not match git root ${gitRootCheck}`));
62
+ }
63
+
64
+ // Verify .jettypod exists in the git root
65
+ const jettypodPath = path.join(gitRoot, '.jettypod');
66
+ if (!fs.existsSync(jettypodPath)) {
67
+ return Promise.reject(new Error(`SAFETY: .jettypod directory not found in ${gitRoot} - refusing to proceed`));
68
+ }
69
+ } catch (err) {
70
+ return Promise.reject(new Error(`SAFETY: Failed to verify git root: ${err.message}`));
71
+ }
72
+
73
+ const { getDb } = require('./database');
74
+ const db = options.db || getDb();
75
+
76
+ // Generate branch name and worktree path
77
+ const titleSlug = slugify(workItem.title || 'item');
78
+ const branchName = `feature/work-${workItem.id}-${titleSlug}`;
79
+ const worktreeBasePath = path.join(gitRoot, '.jettypod-work');
80
+ const worktreePath = path.join(worktreeBasePath, `${workItem.id}-${titleSlug}`);
81
+
82
+ let worktreeId = null;
83
+ let branchCreated = false;
84
+ let worktreeCreated = false;
85
+
86
+ try {
87
+ // Step 1: Create database entry with status='active'
88
+ // Note: We use 'active' from the start because if anything fails,
89
+ // we'll delete the entire entry during rollback
90
+ worktreeId = await new Promise((resolve, reject) => {
91
+ db.run(
92
+ `INSERT INTO worktrees (work_item_id, branch_name, worktree_path, status)
93
+ VALUES (?, ?, ?, 'active')`,
94
+ [workItem.id, branchName, worktreePath],
95
+ function(err) {
96
+ if (err) reject(err);
97
+ else resolve(this.lastID);
98
+ }
99
+ );
100
+ });
101
+
102
+ // Step 2: Create git worktree
103
+ // Always branch from 'main' to avoid nested worktrees
104
+ try {
105
+ execSync(`git worktree add -b "${branchName}" "${worktreePath}" main`, {
106
+ cwd: gitRoot,
107
+ stdio: 'pipe'
108
+ });
109
+ worktreeCreated = true;
110
+ branchCreated = true;
111
+ } catch (gitErr) {
112
+ throw new Error(`Failed to create git worktree: ${gitErr.message}`);
113
+ }
114
+
115
+ // Step 3: Verify directory exists
116
+ if (!fs.existsSync(worktreePath)) {
117
+ throw new Error(`Worktree directory was not created: ${worktreePath}`);
118
+ }
119
+
120
+ // Step 4: Create .jettypod symlink
121
+ // CRITICAL: Git creates .jettypod directory because snapshots are tracked
122
+ // We must remove it first to create the symlink to the shared database
123
+ const jettypodLink = path.join(worktreePath, '.jettypod');
124
+ const jettypodTarget = path.join(gitRoot, '.jettypod');
125
+ try {
126
+ // Remove the git-created .jettypod directory if it exists
127
+ if (fs.existsSync(jettypodLink)) {
128
+ const stats = fs.lstatSync(jettypodLink);
129
+ if (stats.isSymbolicLink()) {
130
+ // Already a symlink - verify it points to the right place
131
+ const linkTarget = fs.readlinkSync(jettypodLink);
132
+ const resolvedLink = path.resolve(path.dirname(jettypodLink), linkTarget);
133
+ const resolvedTarget = path.resolve(jettypodTarget);
134
+ if (resolvedLink === resolvedTarget) {
135
+ // Symlink already correct - nothing to do
136
+ } else {
137
+ // Wrong target - remove and recreate
138
+ fs.unlinkSync(jettypodLink);
139
+ fs.symlinkSync(jettypodTarget, jettypodLink, 'dir');
140
+ }
141
+ } else {
142
+ // It's a directory - remove it and create symlink
143
+ fs.rmSync(jettypodLink, { recursive: true, force: true });
144
+ fs.symlinkSync(jettypodTarget, jettypodLink, 'dir');
145
+ }
146
+ } else {
147
+ // Doesn't exist - create symlink
148
+ fs.symlinkSync(jettypodTarget, jettypodLink, 'dir');
149
+ }
150
+ } catch (symlinkErr) {
151
+ // Symlink failure is CRITICAL - the worktree won't have access to the database
152
+ throw new Error(`Failed to create .jettypod symlink: ${symlinkErr.message}`);
153
+ }
154
+
155
+ // Step 5: Return the created worktree record
156
+ const worktree = await new Promise((resolve, reject) => {
157
+ db.get('SELECT * FROM worktrees WHERE id = ?', [worktreeId], (err, row) => {
158
+ if (err) reject(err);
159
+ else resolve(row);
160
+ });
161
+ });
162
+
163
+ return worktree;
164
+
165
+ } catch (err) {
166
+ // Transaction rollback
167
+ console.error(`Worktree creation failed, rolling back: ${err.message}`);
168
+
169
+ // Cleanup database entry
170
+ if (worktreeId) {
171
+ try {
172
+ await new Promise((resolve, reject) => {
173
+ db.run('DELETE FROM worktrees WHERE id = ?', [worktreeId], (err) => {
174
+ if (err) reject(err);
175
+ else resolve();
176
+ });
177
+ });
178
+ } catch (dbErr) {
179
+ console.error(`Failed to rollback database entry: ${dbErr.message}`);
180
+ }
181
+ }
182
+
183
+ // Cleanup git worktree
184
+ if (worktreeCreated) {
185
+ try {
186
+ execSync(`git worktree remove --force "${worktreePath}"`, {
187
+ cwd: gitRoot,
188
+ stdio: 'pipe'
189
+ });
190
+ } catch (gitErr) {
191
+ console.error(`Failed to cleanup git worktree: ${gitErr.message}`);
192
+ }
193
+ }
194
+
195
+ // Cleanup branch (only if we created it)
196
+ if (branchCreated && !worktreeCreated) {
197
+ try {
198
+ execSync(`git branch -D "${branchName}"`, {
199
+ cwd: gitRoot,
200
+ stdio: 'pipe'
201
+ });
202
+ } catch (gitErr) {
203
+ console.error(`Failed to cleanup branch: ${gitErr.message}`);
204
+ }
205
+ }
206
+
207
+ // Cleanup directory
208
+ if (fs.existsSync(worktreePath)) {
209
+ try {
210
+ fs.rmSync(worktreePath, { recursive: true, force: true });
211
+ } catch (fsErr) {
212
+ console.error(`Failed to cleanup worktree directory: ${fsErr.message}`);
213
+ }
214
+ }
215
+
216
+ throw new Error(`Worktree creation failed: ${err.message}`);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Get the worktree for a work item
222
+ *
223
+ * @param {number} workItemId - Work item ID
224
+ * @param {Object} options - Optional configuration
225
+ * @param {Object} options.db - Database connection (defaults to global database)
226
+ * @returns {Promise<Object|null>} Worktree record or null if not found
227
+ */
228
+ async function getWorktreeForWorkItem(workItemId, options = {}) {
229
+ if (!workItemId || typeof workItemId !== 'number') {
230
+ return Promise.reject(new Error('Invalid work item ID: must be a number'));
231
+ }
232
+
233
+ const { getDb } = require('./database');
234
+ const db = options.db || getDb();
235
+
236
+ return new Promise((resolve, reject) => {
237
+ db.get(
238
+ 'SELECT * FROM worktrees WHERE work_item_id = ? ORDER BY created_at DESC LIMIT 1',
239
+ [workItemId],
240
+ (err, row) => {
241
+ if (err) reject(err);
242
+ else resolve(row || null);
243
+ }
244
+ );
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Get all active worktrees
250
+ *
251
+ * @param {Object} options - Optional configuration
252
+ * @param {Object} options.db - Database connection (defaults to global database)
253
+ * @returns {Promise<Array>} Array of worktree records with status='active'
254
+ */
255
+ async function getAllActiveWorktrees(options = {}) {
256
+ const { getDb } = require('./database');
257
+ const db = options.db || getDb();
258
+
259
+ return new Promise((resolve, reject) => {
260
+ db.all(
261
+ 'SELECT * FROM worktrees WHERE status = ? ORDER BY created_at DESC',
262
+ ['active'],
263
+ (err, rows) => {
264
+ if (err) reject(err);
265
+ else resolve(rows || []);
266
+ }
267
+ );
268
+ });
269
+ }
270
+
271
+ /**
272
+ * Mark worktree status
273
+ *
274
+ * @param {number} worktreeId - Worktree ID
275
+ * @param {string} status - New status (active|merging|cleanup_pending|corrupted)
276
+ * @param {Object} options - Optional configuration
277
+ * @param {Object} options.db - Database connection (defaults to global database)
278
+ * @returns {Promise<void>}
279
+ */
280
+ async function markWorktreeStatus(worktreeId, status, options = {}) {
281
+ if (!worktreeId || typeof worktreeId !== 'number') {
282
+ return Promise.reject(new Error('Invalid worktree ID: must be a number'));
283
+ }
284
+
285
+ if (!VALID_STATUSES.includes(status)) {
286
+ return Promise.reject(new Error(`Invalid status: must be one of ${VALID_STATUSES.join(', ')}`));
287
+ }
288
+
289
+ const { getDb } = require('./database');
290
+ const db = options.db || getDb();
291
+
292
+ return new Promise((resolve, reject) => {
293
+ db.run(
294
+ 'UPDATE worktrees SET status = ?, updated_at = datetime(\'now\') WHERE id = ?',
295
+ [status, worktreeId],
296
+ function(err) {
297
+ if (err) reject(err);
298
+ else if (this.changes === 0) reject(new Error(`Worktree not found: ${worktreeId}`));
299
+ else resolve();
300
+ }
301
+ );
302
+ });
303
+ }
304
+
305
+ /**
306
+ * Manual recursive directory deletion (last resort fallback)
307
+ * Walks directory tree and deletes files individually
308
+ *
309
+ * @param {string} dirPath - Directory to delete
310
+ * @returns {Promise<void>}
311
+ */
312
+ async function manualRecursiveDelete(dirPath) {
313
+ const fs = require('fs');
314
+ const path = require('path');
315
+
316
+ if (!fs.existsSync(dirPath)) {
317
+ return;
318
+ }
319
+
320
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
321
+
322
+ for (const entry of entries) {
323
+ const fullPath = path.join(dirPath, entry.name);
324
+
325
+ if (entry.isDirectory()) {
326
+ await manualRecursiveDelete(fullPath);
327
+ } else {
328
+ try {
329
+ fs.unlinkSync(fullPath);
330
+ } catch (err) {
331
+ // Try changing permissions and retry
332
+ try {
333
+ fs.chmodSync(fullPath, 0o666);
334
+ fs.unlinkSync(fullPath);
335
+ } catch (retryErr) {
336
+ throw new Error(`Cannot delete file ${fullPath}: ${retryErr.message}`);
337
+ }
338
+ }
339
+ }
340
+ }
341
+
342
+ // Delete the now-empty directory
343
+ fs.rmdirSync(dirPath);
344
+ }
345
+
346
+ /**
347
+ * Resilient directory removal with multi-stage escalation strategy
348
+ * Implements conditional verbosity: silent on success, detailed on escalation
349
+ *
350
+ * Stage 1: Standard git worktree remove
351
+ * Stage 2: Forced git worktree remove --force
352
+ * Stage 3: Filesystem fs.rmSync() with force
353
+ * Stage 4: Manual recursive directory deletion (fallback)
354
+ *
355
+ * @param {string} dirPath - Absolute path to directory to remove
356
+ * @param {string} gitRoot - Path to git repository root
357
+ * @returns {Promise<void>}
358
+ */
359
+ async function removeDirectoryResilient(dirPath, gitRoot) {
360
+ const fs = require('fs');
361
+ const { execSync } = require('child_process');
362
+
363
+ // Stage 1: Try standard git worktree remove (silent)
364
+ try {
365
+ execSync(`git worktree remove "${dirPath}"`, {
366
+ cwd: gitRoot,
367
+ stdio: 'pipe'
368
+ });
369
+ return; // Success - exit silently
370
+ } catch (stage1Err) {
371
+ // Stage 1 failed - escalate with logging
372
+ console.log(`⚠️ Standard removal failed, escalating to forced removal...`);
373
+ }
374
+
375
+ // Stage 2: Try forced git worktree remove
376
+ try {
377
+ execSync(`git worktree remove --force "${dirPath}"`, {
378
+ cwd: gitRoot,
379
+ stdio: 'pipe'
380
+ });
381
+ console.log(`✓ Forced git removal succeeded`);
382
+ return;
383
+ } catch (stage2Err) {
384
+ console.log(`⚠️ Forced git removal failed, escalating to filesystem removal...`);
385
+ }
386
+
387
+ // Stage 3: Try filesystem removal with force
388
+ if (fs.existsSync(dirPath)) {
389
+ try {
390
+ fs.rmSync(dirPath, { recursive: true, force: true });
391
+ console.log(`✓ Filesystem removal succeeded`);
392
+
393
+ // Clean up git metadata
394
+ try {
395
+ execSync('git worktree prune', { cwd: gitRoot, stdio: 'pipe' });
396
+ } catch (pruneErr) {
397
+ // Non-fatal
398
+ }
399
+ return;
400
+ } catch (stage3Err) {
401
+ console.log(`⚠️ Filesystem removal failed, escalating to manual deletion...`);
402
+ }
403
+ }
404
+
405
+ // Stage 4: Manual recursive deletion (last resort)
406
+ try {
407
+ await manualRecursiveDelete(dirPath);
408
+ console.log(`✓ Manual recursive deletion succeeded`);
409
+
410
+ // Clean up git metadata
411
+ try {
412
+ execSync('git worktree prune', { cwd: gitRoot, stdio: 'pipe' });
413
+ } catch (pruneErr) {
414
+ // Non-fatal
415
+ }
416
+ } catch (stage4Err) {
417
+ throw new Error(`All removal strategies failed: ${stage4Err.message}`);
418
+ }
419
+ }
420
+
421
+ /**
422
+ * Cleanup a worktree (remove from filesystem and update DB)
423
+ *
424
+ * @param {number} worktreeId - Worktree ID
425
+ * @param {Object} options - Optional configuration
426
+ * @param {string} options.repoPath - Path to git repository (defaults to process.cwd())
427
+ * @param {Object} options.db - Database connection (defaults to global database)
428
+ * @param {boolean} options.deleteBranch - Whether to delete the branch (defaults to false)
429
+ * @returns {Promise<void>}
430
+ */
431
+ async function cleanupWorktree(worktreeId, options = {}) {
432
+ if (!worktreeId || typeof worktreeId !== 'number') {
433
+ return Promise.reject(new Error('Invalid worktree ID: must be a number'));
434
+ }
435
+
436
+ const fs = require('fs');
437
+ const path = require('path');
438
+ const { execSync } = require('child_process');
439
+
440
+ // CRITICAL SAFETY: repoPath MUST be provided - never auto-detect from cwd
441
+ // Auto-detection from worktrees causes catastrophic repo corruption
442
+ if (!options.repoPath) {
443
+ return Promise.reject(new Error('SAFETY: repoPath must be explicitly provided to cleanupWorktree'));
444
+ }
445
+
446
+ const gitRoot = options.repoPath;
447
+ const { getDb } = require('./database');
448
+ const db = options.db || getDb();
449
+ const deleteBranch = options.deleteBranch || false;
450
+
451
+ // SAFETY CHECK 1: Verify we're not running from within a worktree
452
+ try {
453
+ const currentGitDir = execSync('git rev-parse --git-dir', {
454
+ cwd: process.cwd(),
455
+ encoding: 'utf8'
456
+ }).trim();
457
+
458
+ if (currentGitDir.includes('.jettypod-work') || currentGitDir !== '.git') {
459
+ return Promise.reject(new Error('SAFETY: Cannot run cleanup from within a worktree'));
460
+ }
461
+ } catch (err) {
462
+ // Ignore - might not be in a git repo
463
+ }
464
+
465
+ // SAFETY CHECK 2: Verify gitRoot is actually the main repository
466
+ try {
467
+ const gitRootCheck = execSync('git rev-parse --show-toplevel', {
468
+ cwd: gitRoot,
469
+ encoding: 'utf8'
470
+ }).trim();
471
+
472
+ // Resolve both paths to handle /var vs /private/var symlink on macOS
473
+ const resolvedGitRoot = fs.realpathSync(gitRoot);
474
+ const resolvedGitRootCheck = fs.realpathSync(gitRootCheck);
475
+
476
+ if (resolvedGitRootCheck !== resolvedGitRoot) {
477
+ return Promise.reject(new Error(`SAFETY: Provided repoPath ${gitRoot} does not match git root ${gitRootCheck}`));
478
+ }
479
+
480
+ // Verify .jettypod exists in the git root
481
+ const jettypodPath = path.join(gitRoot, '.jettypod');
482
+ if (!fs.existsSync(jettypodPath)) {
483
+ return Promise.reject(new Error(`SAFETY: .jettypod directory not found in ${gitRoot} - refusing to proceed`));
484
+ }
485
+ } catch (err) {
486
+ return Promise.reject(new Error(`SAFETY: Failed to verify git root: ${err.message}`));
487
+ }
488
+
489
+ // CRITICAL SAFETY: Create automatic backup of .jettypod before cleanup
490
+ const { createBackup } = require('./jettypod-backup');
491
+ const backupResult = await createBackup(gitRoot, `cleanup-worktree-${worktreeId}`);
492
+ if (!backupResult.success) {
493
+ console.warn(`⚠️ Warning: Could not create backup: ${backupResult.error}`);
494
+ console.warn(` Proceeding anyway, but .jettypod is not backed up!`);
495
+ } else {
496
+ console.log(`💾 Created backup: ${path.basename(backupResult.backupPath)}`);
497
+ }
498
+
499
+ try {
500
+ // Step 1: Get worktree details from database
501
+ const worktree = await new Promise((resolve, reject) => {
502
+ db.get('SELECT * FROM worktrees WHERE id = ?', [worktreeId], (err, row) => {
503
+ if (err) reject(err);
504
+ else if (!row) reject(new Error(`Worktree not found: ${worktreeId}`));
505
+ else resolve(row);
506
+ });
507
+ });
508
+
509
+ // Step 2: Mark as cleanup_pending
510
+ await new Promise((resolve, reject) => {
511
+ db.run(
512
+ 'UPDATE worktrees SET status = ?, updated_at = datetime(\'now\') WHERE id = ?',
513
+ ['cleanup_pending', worktreeId],
514
+ (err) => {
515
+ if (err) reject(err);
516
+ else resolve();
517
+ }
518
+ );
519
+ });
520
+
521
+ // Step 2.5: Smart conflict detection and auto-commit
522
+ const { checkForConflicts } = require('./smart-conflict-detection');
523
+
524
+ try {
525
+ const conflictCheck = checkForConflicts(gitRoot);
526
+
527
+ // Check for recoverable errors
528
+ if (conflictCheck.error) {
529
+ if (conflictCheck.error.category === 'DETACHED_HEAD') {
530
+ console.warn(`⚠️ ${conflictCheck.error.message} - skipping auto-commit`);
531
+ } else {
532
+ console.warn(`⚠️ Conflict detection issue: ${conflictCheck.error.message}`);
533
+ }
534
+ // Continue with cleanup even if conflict detection had issues
535
+ }
536
+ // If no conflicts but uncommitted changes exist, auto-commit
537
+ else if (!conflictCheck.hasConflicts && conflictCheck.status.trim() !== '') {
538
+ try {
539
+ execSync('git add .', { cwd: gitRoot, stdio: 'pipe' });
540
+ execSync(`git commit -m "Merge work item #${worktree.work_item_id}"`, {
541
+ cwd: gitRoot,
542
+ stdio: 'pipe'
543
+ });
544
+ console.log(`✅ Auto-committed clean merge for work item #${worktree.work_item_id}`);
545
+ } catch (commitErr) {
546
+ // Non-fatal - log warning but continue
547
+ console.warn(`Warning: Could not auto-commit: ${commitErr.message}`);
548
+ }
549
+ } else if (conflictCheck.hasConflicts) {
550
+ console.warn(`⚠️ Conflicts detected in ${conflictCheck.conflictFiles.length} file(s)`);
551
+ console.warn(` Files: ${conflictCheck.conflictFiles.join(', ')}`);
552
+ }
553
+ } catch (conflictErr) {
554
+ // Fatal errors from checkForConflicts (invalid repo, permission denied)
555
+ // These should stop the cleanup process
556
+ console.error(`❌ Fatal error during conflict detection: ${conflictErr.message}`);
557
+ throw conflictErr;
558
+ }
559
+
560
+ // Step 2.5: Terminate background processes in worktree directory
561
+ const { findProcessesInDirectory, terminateProcessGracefully } = require('./process-manager');
562
+
563
+ try {
564
+ const processes = await findProcessesInDirectory(worktree.worktree_path);
565
+
566
+ if (processes.length > 0) {
567
+ console.log(`🔄 Found ${processes.length} background process(es) in worktree...`);
568
+
569
+ for (const proc of processes) {
570
+ const result = await terminateProcessGracefully(proc.pid, 2000);
571
+ console.log(` Terminated ${result.name} (PID ${result.pid})... ${result.method}`);
572
+ }
573
+ }
574
+ } catch (processErr) {
575
+ // Non-fatal - log warning but continue with cleanup
576
+ console.warn(`⚠️ Warning: Process termination failed: ${processErr.message}`);
577
+ }
578
+
579
+ // Step 3: Remove directory with resilient multi-stage strategy
580
+ await removeDirectoryResilient(worktree.worktree_path, gitRoot);
581
+
582
+ // Step 4: Optionally delete branch
583
+ if (deleteBranch) {
584
+ try {
585
+ execSync(`git branch -D "${worktree.branch_name}"`, {
586
+ cwd: gitRoot,
587
+ stdio: 'pipe'
588
+ });
589
+ } catch (branchErr) {
590
+ // Non-fatal - log warning but continue
591
+ console.warn(`Warning: Could not delete branch: ${branchErr.message}`);
592
+ }
593
+ }
594
+
595
+ // Step 5: Mark as corrupted (we never delete DB entries)
596
+ // Using 'corrupted' as the final state for cleaned up worktrees
597
+ // This preserves the audit trail
598
+ await new Promise((resolve, reject) => {
599
+ db.run(
600
+ 'UPDATE worktrees SET status = ?, updated_at = datetime(\'now\') WHERE id = ?',
601
+ ['corrupted', worktreeId],
602
+ (err) => {
603
+ if (err) reject(err);
604
+ else resolve();
605
+ }
606
+ );
607
+ });
608
+
609
+ } catch (err) {
610
+ // On any failure, mark as corrupted
611
+ try {
612
+ await new Promise((resolve, reject) => {
613
+ db.run(
614
+ 'UPDATE worktrees SET status = ?, updated_at = datetime(\'now\') WHERE id = ?',
615
+ ['corrupted', worktreeId],
616
+ (err) => {
617
+ if (err) reject(err);
618
+ else resolve();
619
+ }
620
+ );
621
+ });
622
+ } catch (dbErr) {
623
+ console.error(`Failed to mark worktree as corrupted: ${dbErr.message}`);
624
+ }
625
+
626
+ throw new Error(`Worktree cleanup failed: ${err.message}`);
627
+ }
628
+ }
629
+
630
+ module.exports = {
631
+ createWorktree,
632
+ getWorktreeForWorkItem,
633
+ getAllActiveWorktrees,
634
+ markWorktreeStatus,
635
+ cleanupWorktree
636
+ };