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.
- package/.nvmrc +1 -0
- package/docs/COMPLETE-TESTING-STRATEGY.md +970 -0
- package/docs/DECISIONS.md +10 -12
- package/docs/NODE_VERSION.md +83 -0
- package/docs/TDD-INFRASTRUCTURE-STRATEGY.md +1374 -0
- package/docs/TESTING-FOR-NON-ENGINEERS.md +1588 -0
- package/docs/TESTING-STRATEGY-AUDIT.md +698 -0
- package/hooks/post-checkout +17 -0
- package/hooks/post-merge +17 -0
- package/hooks/pre-commit +30 -0
- package/jettypod.js +259 -120
- package/lib/coverage-tracker.js +218 -0
- package/lib/database.js +2 -0
- package/lib/db-export.js +192 -0
- package/lib/db-import.js +193 -0
- package/lib/external-transition-handler.js +32 -0
- package/lib/git-hook-helpers.js +174 -0
- package/lib/git-root.js +90 -0
- package/lib/infrastructure-chore-generator.js +45 -0
- package/lib/install-hooks.js +52 -0
- package/lib/jettypod-backup.js +238 -0
- package/lib/merge-lock.js +193 -0
- package/lib/migrations/012-add-worktree-path.js +38 -0
- package/lib/migrations/013-worktrees-table.js +86 -0
- package/lib/migrations/014-migrate-worktree-data.js +161 -0
- package/lib/migrations/015-merge-locks-table.js +67 -0
- package/lib/pattern-finder.js +152 -0
- package/lib/process-manager.js +140 -0
- package/lib/production-standards-reader.js +13 -2
- package/lib/production-standards-writer.js +85 -0
- package/lib/skills/feature-planning/dry-run-validator.js +135 -0
- package/lib/skills/feature-planning/validation-formatter.js +160 -0
- package/lib/smart-conflict-detection.js +168 -0
- package/lib/smart-fetch-rebase.js +614 -0
- package/lib/step-definition-parser.js +76 -0
- package/lib/unit-test-generator.js +232 -0
- package/lib/verification-command-generator.js +66 -0
- package/lib/worktree-diagnostics.js +413 -0
- package/lib/worktree-facade.js +174 -0
- package/lib/worktree-manager.js +636 -0
- package/lib/worktree-reconciler.js +429 -0
- package/package.json +30 -3
- package/skills-templates/external-transition/SKILL.md +34 -3
- package/skills-templates/feature-planning/SKILL.md +190 -24
- package/skills-templates/production-mode/SKILL.md +127 -9
- package/skills-templates/speed-mode/SKILL.md +454 -51
- package/skills-templates/stable-mode/SKILL.md +285 -76
- package/.claude/PROTECT_SKILLS.md +0 -28
- package/.claude/settings.json +0 -24
- package/.claude/settings.local.json +0 -16
- package/.claude/skills/epic-planning/SKILL.md +0 -297
- package/.claude/skills/external-transition/SKILL.md +0 -384
- package/.claude/skills/feature-planning/SKILL.md +0 -464
- package/.claude/skills/production-mode/SKILL.md +0 -369
- package/.claude/skills/speed-mode/SKILL.md +0 -481
- package/.claude/skills/stable-mode/SKILL.md +0 -713
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-10T23-33-09-368Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T16-15-10-070Z/epic-discover/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T16-42-43-212Z/stable-mode/SKILL.md +0 -673
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/epic-planning/SKILL.md +0 -297
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/feature-planning/SKILL.md +0 -464
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/speed-mode/SKILL.md +0 -467
- package/.claude/skills.backup-2025-11-11T17-06-09-783Z/stable-mode/SKILL.md +0 -673
- package/.devpod/current-work.json +0 -10
- package/.devpod/work.db +0 -0
- package/.github/workflows/test-safety.yml +0 -85
- package/.jettypod/config.json +0 -5
- package/.jettypod/current-work.json +0 -10
- package/.jettypod/hooks/README.md +0 -77
- package/.jettypod/hooks/protect-claude-md.js +0 -338
- package/.jettypod/test-work.db +0 -0
- package/.jettypod/work.db +0 -0
- package/CLAUDE.md +0 -49
- package/SPEED-STABLE-AUDIT.md +0 -853
- package/SYSTEM-BEHAVIOR.md +0 -2199
- package/TEST_SAFETY_AUDIT.md +0 -314
- package/TEST_SAFETY_IMPLEMENTATION.md +0 -97
- package/cucumber-report.html +0 -45
- package/dist/devpod-linux +0 -0
- package/dist/devpod-macos +0 -0
- package/dist/devpod-win.exe +0 -0
- package/docs/features/jettypod-standards-explained.md +0 -543
- package/docs/features/standards-inventory.md +0 -257
- package/features/auto-generate-production-chores.feature +0 -13
- package/features/backlog-command.feature +0 -26
- package/features/backlog-filtering-production.feature +0 -10
- package/features/claude-md-protection/steps.js +0 -498
- package/features/decisions/index.js +0 -490
- package/features/decisions/index.test.js +0 -208
- package/features/fix-text-wrapping.feature +0 -42
- package/features/git-hooks/git-hooks.feature +0 -30
- package/features/git-hooks/index.js +0 -93
- package/features/git-hooks/index.test.js +0 -137
- package/features/git-hooks/post-commit +0 -56
- package/features/git-hooks/post-merge +0 -47
- package/features/git-hooks/pre-commit +0 -28
- package/features/git-hooks/simple-steps.js +0 -53
- package/features/git-hooks/simple-test.feature +0 -10
- package/features/git-hooks/steps.js +0 -196
- package/features/jettypod-update-command.feature +0 -46
- package/features/mode-prompts/index.js +0 -95
- package/features/mode-prompts/simple-steps.js +0 -44
- package/features/mode-prompts/simple-test.feature +0 -9
- package/features/mode-prompts/validation.test.js +0 -120
- package/features/multiple-claude-instances.feature +0 -121
- package/features/production-mode-skill.feature +0 -121
- package/features/refactor-mode/steps.js +0 -217
- package/features/refactor-mode.feature +0 -49
- package/features/simplify-external-transition.feature +0 -166
- package/features/skills-update/index.test.js +0 -216
- package/features/step_definitions/backlog-command.steps.js +0 -37
- package/features/step_definitions/fix-text-wrapping.steps.js +0 -271
- package/features/step_definitions/multiple-claude-instances.steps.js +0 -621
- package/features/step_definitions/production-mode-skill.steps.js +0 -862
- package/features/step_definitions/simplify-external-transition.steps.js +0 -370
- package/features/step_definitions/terminal-logo.steps.js +0 -145
- package/features/step_definitions/update-command.steps.js +0 -183
- package/features/support/hooks.js +0 -9
- package/features/terminal-logo/index.js +0 -39
- package/features/terminal-logo/terminal-logo.feature +0 -30
- package/features/update-command/index.js +0 -181
- package/features/update-command/index.test.js +0 -225
- package/features/work-commands/bug-workflow-display.feature +0 -22
- package/features/work-commands/index.js +0 -498
- package/features/work-commands/simple-steps.js +0 -69
- package/features/work-commands/stable-tests.feature +0 -57
- package/features/work-commands/steps.js +0 -1174
- package/features/work-commands/validation.test.js +0 -88
- package/features/work-commands/work-commands.feature +0 -13
- package/features/work-tracking/discovery-validation.test.js +0 -228
- package/features/work-tracking/index.js +0 -1921
- package/features/work-tracking/mode-required.feature +0 -112
- package/features/work-tracking/phase-tracking.test.js +0 -482
- package/features/work-tracking/prototype-tracking.test.js +0 -485
- package/features/work-tracking/tree-view.test.js +0 -310
- package/features/work-tracking/work-set-mode.feature +0 -71
- package/features/work-tracking/work-start-mode.feature +0 -88
- package/full-test.txt +0 -0
- package/lib/bug-workflow.test.js +0 -177
- package/lib/claudemd.test.js +0 -195
- package/lib/config.test.js +0 -511
- package/lib/constants.test.js +0 -164
- package/lib/current-work.test.js +0 -146
- package/lib/database-project-config.test.js +0 -111
- package/lib/database.test.js +0 -106
- package/lib/decisions-generator.test.js +0 -457
- package/lib/decisions-helpers.test.js +0 -310
- package/lib/git-coordinator.js +0 -167
- package/lib/git.test.js +0 -145
- package/lib/migrations/002-default-work-item-modes.test.js +0 -351
- package/lib/production-chore-generator.test.js +0 -432
- package/lib/production-context-detector.test.js +0 -277
- package/lib/production-scenario-appender.test.js +0 -235
- package/lib/production-scenario-validator.test.js +0 -246
- package/lib/production-standards-reader.test.js +0 -270
- package/lib/project-state.test.js +0 -92
- package/lib/push-queue.js +0 -417
- package/lib/queue-processor.js +0 -74
- package/lib/test-helpers.js +0 -202
- package/lib/test-helpers.test.js +0 -255
- package/prototypes/2025-01-11-production-mode-autonomous.js +0 -119
- package/prototypes/2025-01-11-production-mode-collaborative.js +0 -166
- package/prototypes/2025-01-11-production-mode-guided.js +0 -217
- package/prototypes/2025-01-11-production-mode-smart-context.js +0 -347
- package/prototypes/2025-01-11-production-standards-example.md +0 -204
- package/prototypes/2025-11-10-backlog-filtering-tree-aware.js +0 -242
- package/prototypes/test/index.html +0 -1
- package/setup-dist-repo.sh +0 -68
- package/test-production-standards-engine.js +0 -130
- package/test-results.json +0 -2195
- package/test-safety-check.sh +0 -80
- package/work-item-tracking-plan.md +0 -199
- /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
|
+
};
|