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,193 @@
1
+ /**
2
+ * Merge Lock - Coordination for Concurrent Instances
3
+ *
4
+ * This module provides lock acquisition and release for merge operations
5
+ * to prevent simultaneous access to main branch across multiple Claude Code instances.
6
+ *
7
+ * Core principles:
8
+ * - Database is coordination point (simple, reliable serialization)
9
+ * - Polling mechanism with exponential backoff
10
+ * - Automatic cleanup with release() function
11
+ * - Queue position awareness for UX
12
+ */
13
+
14
+ const os = require('os');
15
+
16
+ /**
17
+ * Acquire merge lock for a work item
18
+ *
19
+ * @param {Object} db - SQLite database connection
20
+ * @param {number} workItemId - Work item ID attempting merge
21
+ * @param {string} instanceId - Optional custom instance identifier
22
+ * @param {Object} options - Optional configuration
23
+ * @param {number} options.maxWait - Maximum wait time in ms (default: 60000)
24
+ * @param {number} options.pollInterval - Poll interval in ms (default: 1500)
25
+ * @returns {Promise<Object>} Lock handle with id, locked_by, work_item_id, release()
26
+ * @throws {Error} If lock cannot be acquired within maxWait
27
+ */
28
+ async function acquireMergeLock(db, workItemId, instanceId = null, options = {}) {
29
+ if (!db) {
30
+ throw new Error('Database connection required');
31
+ }
32
+
33
+ if (!workItemId || typeof workItemId !== 'number') {
34
+ throw new Error('Invalid work item ID: must be a number');
35
+ }
36
+
37
+ const maxWait = options.maxWait || 60000; // 1 minute default
38
+ const pollInterval = options.pollInterval || 1500; // 1.5 seconds
39
+ const startTime = Date.now();
40
+
41
+ // Generate instance identifier
42
+ const lockedBy = instanceId || `${os.hostname()}-${process.pid}`;
43
+
44
+ while (Date.now() - startTime < maxWait) {
45
+ // Check if lock exists
46
+ const existingLock = await checkExistingLock(db);
47
+
48
+ if (!existingLock) {
49
+ // Try to acquire lock
50
+ try {
51
+ const lockId = await insertLock(db, workItemId, lockedBy);
52
+
53
+ // Return lock handle with release function
54
+ return createLockHandle(db, lockId, lockedBy, workItemId);
55
+ } catch (err) {
56
+ // Race condition - someone else got it first
57
+ // Continue polling
58
+ }
59
+ }
60
+
61
+ // Wait before retrying
62
+ await sleep(pollInterval);
63
+ }
64
+
65
+ throw new Error(`Lock acquisition timeout after ${maxWait}ms`);
66
+ }
67
+
68
+ /**
69
+ * Check if any merge lock currently exists
70
+ *
71
+ * @param {Object} db - SQLite database connection
72
+ * @returns {Promise<Object|null>} Existing lock or null
73
+ */
74
+ function checkExistingLock(db) {
75
+ return new Promise((resolve, reject) => {
76
+ db.get('SELECT * FROM merge_locks LIMIT 1', (err, row) => {
77
+ if (err) reject(err);
78
+ else resolve(row || null);
79
+ });
80
+ });
81
+ }
82
+
83
+ /**
84
+ * Insert lock record into database
85
+ *
86
+ * @param {Object} db - SQLite database connection
87
+ * @param {number} workItemId - Work item ID
88
+ * @param {string} lockedBy - Instance identifier
89
+ * @returns {Promise<number>} Lock ID
90
+ */
91
+ function insertLock(db, workItemId, lockedBy) {
92
+ return new Promise((resolve, reject) => {
93
+ db.run(
94
+ `INSERT INTO merge_locks (locked_by, work_item_id, operation)
95
+ VALUES (?, ?, 'merging')`,
96
+ [lockedBy, workItemId],
97
+ function(err) {
98
+ if (err) reject(err);
99
+ else resolve(this.lastID);
100
+ }
101
+ );
102
+ });
103
+ }
104
+
105
+ /**
106
+ * Create lock handle object with release function
107
+ *
108
+ * @param {Object} db - SQLite database connection
109
+ * @param {number} lockId - Lock ID
110
+ * @param {string} lockedBy - Instance identifier
111
+ * @param {number} workItemId - Work item ID
112
+ * @returns {Object} Lock handle
113
+ */
114
+ function createLockHandle(db, lockId, lockedBy, workItemId) {
115
+ return {
116
+ id: lockId,
117
+ locked_by: lockedBy,
118
+ work_item_id: workItemId,
119
+ release: async () => {
120
+ try {
121
+ if (!db || typeof db.run !== 'function') {
122
+ throw new Error('Database connection unavailable during release');
123
+ }
124
+
125
+ await new Promise((resolve, reject) => {
126
+ db.run('DELETE FROM merge_locks WHERE id = ?', [lockId], (err) => {
127
+ if (err) {
128
+ // Enhance error message for common issues
129
+ if (err.message && err.message.includes('Database is closed')) {
130
+ reject(new Error('Database connection unavailable during release'));
131
+ } else {
132
+ reject(err);
133
+ }
134
+ } else {
135
+ resolve();
136
+ }
137
+ });
138
+ });
139
+ } catch (err) {
140
+ // Log error but don't crash - lock may have already been cleaned up
141
+ // or database may be unavailable. In production, consider:
142
+ // - Logging to error tracking system
143
+ // - Alerting on repeated failures
144
+ // - Background cleanup task to handle orphaned locks
145
+ console.error(`Failed to release lock ${lockId}:`, err.message);
146
+ throw err; // Re-throw so caller knows release failed
147
+ }
148
+ }
149
+ };
150
+ }
151
+
152
+ /**
153
+ * Clean up stale locks older than threshold
154
+ *
155
+ * @param {Object} db - SQLite database connection
156
+ * @param {Object} options - Optional configuration
157
+ * @param {number} options.staleThreshold - Age threshold in ms (default: 300000 = 5 minutes)
158
+ * @returns {Promise<number>} Count of locks removed
159
+ */
160
+ async function cleanupStaleLocks(db, options = {}) {
161
+ if (!db) {
162
+ throw new Error('Database connection required');
163
+ }
164
+
165
+ const staleThreshold = options.staleThreshold || 300000; // 5 minutes default
166
+ const staleTime = new Date(Date.now() - staleThreshold).toISOString();
167
+
168
+ return new Promise((resolve, reject) => {
169
+ db.run(
170
+ 'DELETE FROM merge_locks WHERE locked_at < ?',
171
+ [staleTime],
172
+ function(err) {
173
+ if (err) reject(err);
174
+ else resolve(this.changes); // Return count of deleted locks
175
+ }
176
+ );
177
+ });
178
+ }
179
+
180
+ /**
181
+ * Sleep for specified milliseconds
182
+ *
183
+ * @param {number} ms - Milliseconds to sleep
184
+ * @returns {Promise<void>}
185
+ */
186
+ function sleep(ms) {
187
+ return new Promise(resolve => setTimeout(resolve, ms));
188
+ }
189
+
190
+ module.exports = {
191
+ acquireMergeLock,
192
+ cleanupStaleLocks
193
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Migration: Add worktree_path column to work_items table
3
+ *
4
+ * Purpose: Track the filesystem path to the git worktree for each work item.
5
+ * This enables:
6
+ * - Multiple Claude instances to work on different items in parallel
7
+ * - Proper worktree cleanup after merge
8
+ * - Worktree-based conflict detection
9
+ */
10
+
11
+ module.exports = {
12
+ id: '012-add-worktree-path',
13
+ description: 'Add worktree_path column to work_items table',
14
+
15
+ async up(db) {
16
+ return new Promise((resolve, reject) => {
17
+ db.run(`
18
+ ALTER TABLE work_items
19
+ ADD COLUMN worktree_path TEXT
20
+ `, (err) => {
21
+ if (err) {
22
+ // Column might already exist - ignore error
23
+ if (err.message.includes('duplicate column')) {
24
+ return resolve();
25
+ }
26
+ return reject(err);
27
+ }
28
+ resolve();
29
+ });
30
+ });
31
+ },
32
+
33
+ async down(db) {
34
+ // SQLite doesn't support DROP COLUMN before version 3.35.0
35
+ // We'll just leave the column - it's harmless if unused
36
+ return Promise.resolve();
37
+ }
38
+ };
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Migration: Create worktrees table
3
+ *
4
+ * Purpose: Establish database as single source of truth for worktree state.
5
+ * This table tracks all worktrees with their branch names, paths, and status.
6
+ *
7
+ * Why this is critical:
8
+ * - Never lose track of branch names (even if worktree directory deleted)
9
+ * - Enable state reconciliation between DB, git, and filesystem
10
+ * - Track worktree lifecycle with status field
11
+ * - Support multiple worktrees per work item (for retries/recovery)
12
+ */
13
+
14
+ module.exports = {
15
+ id: '013-worktrees-table',
16
+ description: 'Create worktrees table for bulletproof worktree state tracking',
17
+
18
+ async up(db) {
19
+ return new Promise((resolve, reject) => {
20
+ db.run(`
21
+ CREATE TABLE IF NOT EXISTS worktrees (
22
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ work_item_id INTEGER NOT NULL,
24
+ branch_name TEXT NOT NULL,
25
+ worktree_path TEXT NOT NULL,
26
+ status TEXT NOT NULL CHECK(status IN ('active', 'merging', 'cleanup_pending', 'corrupted')),
27
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
28
+ updated_at TEXT NOT NULL DEFAULT (datetime('now')),
29
+ FOREIGN KEY (work_item_id) REFERENCES work_items(id)
30
+ )
31
+ `, (err) => {
32
+ if (err) {
33
+ return reject(err);
34
+ }
35
+
36
+ // Create index on work_item_id for fast lookups
37
+ db.run(`
38
+ CREATE INDEX IF NOT EXISTS idx_worktrees_work_item_id
39
+ ON worktrees(work_item_id)
40
+ `, (err) => {
41
+ if (err) {
42
+ return reject(err);
43
+ }
44
+
45
+ // Create index on status for querying active/corrupted worktrees
46
+ db.run(`
47
+ CREATE INDEX IF NOT EXISTS idx_worktrees_status
48
+ ON worktrees(status)
49
+ `, (err) => {
50
+ if (err) {
51
+ return reject(err);
52
+ }
53
+
54
+ resolve();
55
+ });
56
+ });
57
+ });
58
+ });
59
+ },
60
+
61
+ async down(db) {
62
+ return new Promise((resolve, reject) => {
63
+ // Drop indexes first
64
+ db.run('DROP INDEX IF EXISTS idx_worktrees_status', (err) => {
65
+ if (err) {
66
+ return reject(err);
67
+ }
68
+
69
+ db.run('DROP INDEX IF EXISTS idx_worktrees_work_item_id', (err) => {
70
+ if (err) {
71
+ return reject(err);
72
+ }
73
+
74
+ // Drop table
75
+ db.run('DROP TABLE IF EXISTS worktrees', (err) => {
76
+ if (err) {
77
+ return reject(err);
78
+ }
79
+
80
+ resolve();
81
+ });
82
+ });
83
+ });
84
+ });
85
+ }
86
+ };
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Migration: Migrate existing worktree_path data to worktrees table
3
+ *
4
+ * Purpose: Migrate existing work_items.worktree_path data to the new worktrees table.
5
+ * This preserves existing worktree state during the transition to the new architecture.
6
+ *
7
+ * Strategy:
8
+ * - For each work item with worktree_path:
9
+ * - Check if directory exists
10
+ * - Try to discover branch name via git
11
+ * - Create worktrees entry with appropriate status (active/corrupted)
12
+ * - Handle errors gracefully - one corrupted worktree should not fail entire migration
13
+ */
14
+
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+ const { execSync } = require('child_process');
18
+
19
+ module.exports = {
20
+ id: '014-migrate-worktree-data',
21
+ description: 'Migrate existing worktree_path data from work_items to worktrees table',
22
+
23
+ async up(db) {
24
+ return new Promise((resolve, reject) => {
25
+ // Get all work items with worktree_path set
26
+ db.all('SELECT id, worktree_path FROM work_items WHERE worktree_path IS NOT NULL', async (err, workItems) => {
27
+ if (err) {
28
+ return reject(err);
29
+ }
30
+
31
+ if (!workItems || workItems.length === 0) {
32
+ // No work items with worktrees - nothing to migrate
33
+ return resolve();
34
+ }
35
+
36
+ console.log(`\nMigrating ${workItems.length} worktree(s) to new worktrees table...`);
37
+
38
+ // Process each work item
39
+ const migrations = [];
40
+ for (const workItem of workItems) {
41
+ migrations.push(migrateWorkItemWorktree(db, workItem));
42
+ }
43
+
44
+ try {
45
+ await Promise.all(migrations);
46
+ console.log('✓ Worktree data migration complete\n');
47
+ resolve();
48
+ } catch (migrateErr) {
49
+ // Even if some migrations failed, we continue (logged individual errors)
50
+ console.log('⚠️ Some worktrees could not be migrated (marked as corrupted)\n');
51
+ resolve();
52
+ }
53
+ });
54
+ });
55
+ },
56
+
57
+ async down(db) {
58
+ return new Promise((resolve, reject) => {
59
+ // Get all work items that originally had worktree_path
60
+ db.all('SELECT id FROM work_items WHERE worktree_path IS NOT NULL', (err, workItems) => {
61
+ if (err) {
62
+ return reject(err);
63
+ }
64
+
65
+ if (!workItems || workItems.length === 0) {
66
+ return resolve();
67
+ }
68
+
69
+ const workItemIds = workItems.map(w => w.id);
70
+
71
+ // Delete worktree entries for these work items
72
+ const placeholders = workItemIds.map(() => '?').join(',');
73
+ db.run(`DELETE FROM worktrees WHERE work_item_id IN (${placeholders})`, workItemIds, (err) => {
74
+ if (err) {
75
+ return reject(err);
76
+ }
77
+ resolve();
78
+ });
79
+ });
80
+ });
81
+ }
82
+ };
83
+
84
+ /**
85
+ * Migrate a single work item's worktree to the worktrees table
86
+ */
87
+ async function migrateWorkItemWorktree(db, workItem) {
88
+ const { id: workItemId, worktree_path: worktreePath } = workItem;
89
+
90
+ return new Promise(async (resolve) => {
91
+ try {
92
+ // Check if directory exists
93
+ const dirExists = fs.existsSync(worktreePath);
94
+
95
+ let branchName = 'unknown';
96
+ let status = 'corrupted';
97
+
98
+ if (dirExists) {
99
+ // Check if this is actually a git repository (has .git file or directory)
100
+ const gitPath = path.join(worktreePath, '.git');
101
+ const hasGit = fs.existsSync(gitPath);
102
+
103
+ if (!hasGit) {
104
+ console.log(` ⚠️ Work item #${workItemId}: Directory exists but no .git found (not a git worktree)`);
105
+ } else {
106
+ // Try to discover branch name
107
+ try {
108
+ const branch = execSync('git rev-parse --abbrev-ref HEAD', {
109
+ cwd: worktreePath,
110
+ encoding: 'utf8',
111
+ stdio: 'pipe'
112
+ }).trim();
113
+
114
+ if (branch && branch !== 'HEAD') {
115
+ branchName = branch;
116
+ status = 'active';
117
+ } else {
118
+ console.log(` ⚠️ Work item #${workItemId}: Could not determine branch (detached HEAD)`);
119
+ }
120
+ } catch (gitErr) {
121
+ // Git command failed - worktree is corrupted
122
+ console.log(` ⚠️ Work item #${workItemId}: Git command failed - ${gitErr.message}`);
123
+ }
124
+ }
125
+ } else {
126
+ console.log(` ⚠️ Work item #${workItemId}: Worktree directory does not exist: ${worktreePath}`);
127
+ }
128
+
129
+ // Check if entry already exists (idempotency)
130
+ db.get('SELECT id FROM worktrees WHERE work_item_id = ?', [workItemId], (err, existing) => {
131
+ if (err) {
132
+ console.error(` ❌ Work item #${workItemId}: Database error - ${err.message}`);
133
+ return resolve(); // Continue with other migrations
134
+ }
135
+
136
+ if (existing) {
137
+ // Entry already exists - skip
138
+ return resolve();
139
+ }
140
+
141
+ // Insert into worktrees table
142
+ db.run(`
143
+ INSERT INTO worktrees (work_item_id, branch_name, worktree_path, status)
144
+ VALUES (?, ?, ?, ?)
145
+ `, [workItemId, branchName, worktreePath, status], (insertErr) => {
146
+ if (insertErr) {
147
+ console.error(` ❌ Work item #${workItemId}: Failed to insert - ${insertErr.message}`);
148
+ } else if (status === 'active') {
149
+ console.log(` ✓ Work item #${workItemId}: Migrated as active (branch: ${branchName})`);
150
+ } else {
151
+ console.log(` ⚠️ Work item #${workItemId}: Migrated as corrupted`);
152
+ }
153
+ resolve();
154
+ });
155
+ });
156
+ } catch (err) {
157
+ console.error(` ❌ Work item #${workItemId}: Unexpected error - ${err.message}`);
158
+ resolve(); // Continue with other migrations
159
+ }
160
+ });
161
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Migration: Create merge_locks table
3
+ *
4
+ * Purpose: Enable coordination between multiple Claude Code instances during
5
+ * merge operations to prevent simultaneous access to main branch.
6
+ *
7
+ * Why this is critical:
8
+ * - Serialize merge operations across concurrent instances
9
+ * - Prevent git conflicts from simultaneous main branch access
10
+ * - Track which instance holds the merge lock
11
+ * - Support queue position awareness for better UX
12
+ */
13
+
14
+ module.exports = {
15
+ id: '015-merge-locks-table',
16
+ description: 'Create merge_locks table for concurrent instance coordination',
17
+
18
+ async up(db) {
19
+ return new Promise((resolve, reject) => {
20
+ db.run(`
21
+ CREATE TABLE IF NOT EXISTS merge_locks (
22
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
23
+ locked_by TEXT NOT NULL,
24
+ locked_at TEXT NOT NULL DEFAULT (datetime('now')),
25
+ operation TEXT NOT NULL DEFAULT 'merging',
26
+ work_item_id INTEGER NOT NULL
27
+ )
28
+ `, (err) => {
29
+ if (err) {
30
+ return reject(err);
31
+ }
32
+
33
+ // Create index on locked_at for queue position queries
34
+ db.run(`
35
+ CREATE INDEX IF NOT EXISTS idx_merge_locks_locked_at
36
+ ON merge_locks(locked_at)
37
+ `, (err) => {
38
+ if (err) {
39
+ return reject(err);
40
+ }
41
+
42
+ resolve();
43
+ });
44
+ });
45
+ });
46
+ },
47
+
48
+ async down(db) {
49
+ return new Promise((resolve, reject) => {
50
+ // Drop index first
51
+ db.run('DROP INDEX IF EXISTS idx_merge_locks_locked_at', (err) => {
52
+ if (err) {
53
+ return reject(err);
54
+ }
55
+
56
+ // Drop table
57
+ db.run('DROP TABLE IF EXISTS merge_locks', (err) => {
58
+ if (err) {
59
+ return reject(err);
60
+ }
61
+
62
+ resolve();
63
+ });
64
+ });
65
+ });
66
+ }
67
+ };
@@ -0,0 +1,152 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ /**
5
+ * Find similar code patterns in the codebase based on keywords
6
+ * @param {string|string[]} keywords - Keyword(s) to search for
7
+ * @param {string} baseDir - Base directory to search (defaults to process.cwd())
8
+ * @returns {Array<{file: string, description: string}>} Array of pattern matches
9
+ */
10
+ function findSimilarPatterns(keywords, baseDir = process.cwd()) {
11
+ const patterns = [];
12
+
13
+ // Normalize keywords to array
14
+ const keywordArray = Array.isArray(keywords) ? keywords : [keywords];
15
+
16
+ // Directories to search
17
+ const searchDirs = ['lib', 'features', 'src'];
18
+
19
+ // Search each directory
20
+ searchDirs.forEach(dir => {
21
+ const dirPath = path.join(baseDir, dir);
22
+
23
+ if (!fs.existsSync(dirPath)) {
24
+ return;
25
+ }
26
+
27
+ // Search for matching files
28
+ const matches = searchDirectory(dirPath, keywordArray, baseDir);
29
+ patterns.push(...matches);
30
+ });
31
+
32
+ return patterns;
33
+ }
34
+
35
+ /**
36
+ * Recursively search directory for files matching keywords
37
+ * @param {string} dirPath - Directory to search
38
+ * @param {string[]} keywords - Keywords to match
39
+ * @param {string} baseDir - Base directory for relative paths
40
+ * @returns {Array<{file: string, description: string}>} Matching files
41
+ */
42
+ function searchDirectory(dirPath, keywords, baseDir) {
43
+ const matches = [];
44
+
45
+ try {
46
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
47
+
48
+ entries.forEach(entry => {
49
+ const fullPath = path.join(dirPath, entry.name);
50
+
51
+ if (entry.isDirectory()) {
52
+ // Skip node_modules and hidden directories
53
+ if (entry.name === 'node_modules' || entry.name.startsWith('.')) {
54
+ return;
55
+ }
56
+
57
+ // Recursively search subdirectories
58
+ const subdirMatches = searchDirectory(fullPath, keywords, baseDir);
59
+ matches.push(...subdirMatches);
60
+ } else if (entry.isFile() && entry.name.endsWith('.js')) {
61
+ // Check if filename matches any keyword
62
+ const fileName = entry.name.toLowerCase();
63
+ const matchingKeywords = keywords.filter(keyword =>
64
+ fileName.includes(keyword.toLowerCase())
65
+ );
66
+
67
+ if (matchingKeywords.length > 0) {
68
+ const relativePath = path.relative(baseDir, fullPath);
69
+
70
+ // Try to read file and find relevant functions/classes
71
+ const description = analyzeFile(fullPath, keywords);
72
+
73
+ matches.push({
74
+ file: relativePath,
75
+ description: description || `Contains ${matchingKeywords.join(', ')} related code`
76
+ });
77
+ }
78
+ }
79
+ });
80
+ } catch (err) {
81
+ // Skip directories we can't read
82
+ return matches;
83
+ }
84
+
85
+ return matches;
86
+ }
87
+
88
+ /**
89
+ * Analyze a file to find relevant functions or classes
90
+ * @param {string} filePath - Path to file to analyze
91
+ * @param {string[]} keywords - Keywords to look for
92
+ * @returns {string|null} Description of what the file contains
93
+ */
94
+ function analyzeFile(filePath, keywords) {
95
+ try {
96
+ const content = fs.readFileSync(filePath, 'utf8');
97
+ const lines = content.split('\n');
98
+
99
+ const functions = [];
100
+ const classes = [];
101
+
102
+ // Look for function declarations
103
+ lines.forEach(line => {
104
+ // Match function declarations: function name() or const name = function()
105
+ const funcMatch = line.match(/(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:function|async|\())/);
106
+ if (funcMatch) {
107
+ const funcName = funcMatch[1] || funcMatch[2];
108
+
109
+ // Check if function name matches any keyword
110
+ const matchesKeyword = keywords.some(keyword =>
111
+ funcName.toLowerCase().includes(keyword.toLowerCase())
112
+ );
113
+
114
+ if (matchesKeyword) {
115
+ functions.push(funcName);
116
+ }
117
+ }
118
+
119
+ // Match class declarations: class Name
120
+ const classMatch = line.match(/class\s+(\w+)/);
121
+ if (classMatch) {
122
+ const className = classMatch[1];
123
+
124
+ // Check if class name matches any keyword
125
+ const matchesKeyword = keywords.some(keyword =>
126
+ className.toLowerCase().includes(keyword.toLowerCase())
127
+ );
128
+
129
+ if (matchesKeyword) {
130
+ classes.push(className);
131
+ }
132
+ }
133
+ });
134
+
135
+ // Build description
136
+ const parts = [];
137
+ if (functions.length > 0) {
138
+ parts.push(`Functions: ${functions.join(', ')}`);
139
+ }
140
+ if (classes.length > 0) {
141
+ parts.push(`Classes: ${classes.join(', ')}`);
142
+ }
143
+
144
+ return parts.length > 0 ? parts.join('; ') : null;
145
+ } catch (err) {
146
+ return null;
147
+ }
148
+ }
149
+
150
+ module.exports = {
151
+ findSimilarPatterns
152
+ };