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
package/lib/push-queue.js
DELETED
|
@@ -1,417 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Get path to push queue file
|
|
6
|
-
* @returns {string} Absolute path to push-queue.json
|
|
7
|
-
*/
|
|
8
|
-
function getQueueFilePath() {
|
|
9
|
-
return path.join(process.cwd(), '.jettypod', 'push-queue.json');
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Ensure .jettypod directory exists
|
|
14
|
-
*/
|
|
15
|
-
function ensureJettypodDir() {
|
|
16
|
-
const dir = path.join(process.cwd(), '.jettypod');
|
|
17
|
-
if (!fs.existsSync(dir)) {
|
|
18
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Initialize empty queue file if it doesn't exist
|
|
24
|
-
*/
|
|
25
|
-
function initializeQueueFile() {
|
|
26
|
-
const queuePath = getQueueFilePath();
|
|
27
|
-
if (!fs.existsSync(queuePath)) {
|
|
28
|
-
ensureJettypodDir();
|
|
29
|
-
fs.writeFileSync(queuePath, JSON.stringify({ queue: [] }, null, 2));
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Read queue from file
|
|
35
|
-
* @returns {Object} Queue object with queue array
|
|
36
|
-
*/
|
|
37
|
-
function readQueue() {
|
|
38
|
-
initializeQueueFile();
|
|
39
|
-
const queuePath = getQueueFilePath();
|
|
40
|
-
try {
|
|
41
|
-
const content = fs.readFileSync(queuePath, 'utf-8');
|
|
42
|
-
return JSON.parse(content);
|
|
43
|
-
} catch (err) {
|
|
44
|
-
// Handle corrupted JSON - reinitialize and continue
|
|
45
|
-
if (err instanceof SyntaxError) {
|
|
46
|
-
console.warn('⚠️ Push queue file corrupted - reinitializing with empty queue');
|
|
47
|
-
const emptyQueue = { queue: [] };
|
|
48
|
-
try {
|
|
49
|
-
fs.writeFileSync(queuePath, JSON.stringify(emptyQueue, null, 2));
|
|
50
|
-
return emptyQueue;
|
|
51
|
-
} catch (writeErr) {
|
|
52
|
-
throw new Error(`Cannot recover corrupted queue: ${writeErr.message}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
throw new Error(`Failed to read push queue: ${err.message}`);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Write queue to file
|
|
61
|
-
* @param {Object} queueData - Queue object to write
|
|
62
|
-
*/
|
|
63
|
-
function writeQueue(queueData) {
|
|
64
|
-
const queuePath = getQueueFilePath();
|
|
65
|
-
try {
|
|
66
|
-
fs.writeFileSync(queuePath, JSON.stringify(queueData, null, 2));
|
|
67
|
-
} catch (err) {
|
|
68
|
-
// Handle permission errors
|
|
69
|
-
if (err.code === 'EACCES' || err.code === 'EPERM') {
|
|
70
|
-
console.error('❌ Insufficient permissions to write push queue');
|
|
71
|
-
console.error(` File: ${queuePath}`);
|
|
72
|
-
console.error('');
|
|
73
|
-
console.error('To fix permissions:');
|
|
74
|
-
console.error(` chmod u+w ${path.dirname(queuePath)}`);
|
|
75
|
-
console.error(` chmod u+w ${queuePath}`);
|
|
76
|
-
throw new Error('Insufficient permissions - see guidance above');
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// Handle disk full errors
|
|
80
|
-
if (err.code === 'ENOSPC') {
|
|
81
|
-
console.error('❌ Disk full - cannot write push queue');
|
|
82
|
-
console.error(' Free up disk space and try again');
|
|
83
|
-
throw new Error('Disk full - cannot write queue file');
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
throw new Error(`Failed to write push queue: ${err.message}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Get current queue
|
|
92
|
-
* @returns {Array} Array of queue entries
|
|
93
|
-
*/
|
|
94
|
-
function getQueue() {
|
|
95
|
-
const data = readQueue();
|
|
96
|
-
|
|
97
|
-
// Filter out invalid entries
|
|
98
|
-
const validEntries = [];
|
|
99
|
-
const invalidEntries = [];
|
|
100
|
-
|
|
101
|
-
for (const entry of data.queue) {
|
|
102
|
-
// Check if entry has required fields
|
|
103
|
-
if (!entry.instanceId || typeof entry.instanceId !== 'string' || entry.instanceId.trim() === '') {
|
|
104
|
-
invalidEntries.push(entry);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (!entry.commitSha || typeof entry.commitSha !== 'string' || entry.commitSha.trim() === '') {
|
|
109
|
-
invalidEntries.push(entry);
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
validEntries.push(entry);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// Log warning about invalid entries
|
|
117
|
-
if (invalidEntries.length > 0) {
|
|
118
|
-
console.warn(`⚠️ Found ${invalidEntries.length} invalid queue entries - removing them`);
|
|
119
|
-
console.warn(' Invalid entries detected with missing instanceId or commitSha');
|
|
120
|
-
|
|
121
|
-
// Write back cleaned queue if we found invalid entries
|
|
122
|
-
data.queue = validEntries;
|
|
123
|
-
writeQueue(data);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return validEntries;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Check if queue is empty
|
|
131
|
-
* @returns {boolean} True if queue has no entries
|
|
132
|
-
*/
|
|
133
|
-
function isEmpty() {
|
|
134
|
-
const queue = getQueue();
|
|
135
|
-
return queue.length === 0;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Add instance to queue
|
|
140
|
-
* @param {string} instanceId - Unique identifier for Claude instance
|
|
141
|
-
* @param {string} commitSha - SHA of commit to push
|
|
142
|
-
* @returns {number} Position in queue (1-indexed)
|
|
143
|
-
*/
|
|
144
|
-
function addToQueue(instanceId, commitSha) {
|
|
145
|
-
// Validate instanceId
|
|
146
|
-
if (!instanceId || typeof instanceId !== 'string' || instanceId.trim() === '') {
|
|
147
|
-
console.error('❌ Invalid instance ID provided to queue');
|
|
148
|
-
console.error(' Instance ID must be a non-empty string');
|
|
149
|
-
throw new Error('Invalid instance ID - cannot add to queue');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Validate commitSha
|
|
153
|
-
if (!commitSha || typeof commitSha !== 'string' || commitSha.trim() === '') {
|
|
154
|
-
console.error('❌ Invalid commit SHA provided to queue');
|
|
155
|
-
console.error(' Commit SHA must be a non-empty string');
|
|
156
|
-
console.error('');
|
|
157
|
-
console.error('To fix:');
|
|
158
|
-
console.error(' Ensure you have committed your changes before pushing');
|
|
159
|
-
console.error(' Run: git rev-parse HEAD');
|
|
160
|
-
throw new Error('Invalid commit SHA - cannot add to queue');
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
const data = readQueue();
|
|
164
|
-
const position = data.queue.length + 1;
|
|
165
|
-
|
|
166
|
-
const entry = {
|
|
167
|
-
instanceId,
|
|
168
|
-
commitSha,
|
|
169
|
-
position,
|
|
170
|
-
timestamp: Date.now(),
|
|
171
|
-
status: 'waiting'
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
data.queue.push(entry);
|
|
175
|
-
writeQueue(data);
|
|
176
|
-
|
|
177
|
-
return position;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Validate queue integrity
|
|
182
|
-
* @param {Array} queue - Queue entries to validate
|
|
183
|
-
* @returns {Object} Validation result with isValid and issues array
|
|
184
|
-
*/
|
|
185
|
-
function validateQueueIntegrity(queue) {
|
|
186
|
-
const issues = [];
|
|
187
|
-
|
|
188
|
-
// Check positions are sequential and match array indices
|
|
189
|
-
const positionSet = new Set();
|
|
190
|
-
|
|
191
|
-
queue.forEach((entry, index) => {
|
|
192
|
-
const expectedPosition = index + 1;
|
|
193
|
-
|
|
194
|
-
// Check if position matches index
|
|
195
|
-
if (entry.position !== expectedPosition) {
|
|
196
|
-
issues.push({
|
|
197
|
-
type: 'position_mismatch',
|
|
198
|
-
instanceId: entry.instanceId,
|
|
199
|
-
expected: expectedPosition,
|
|
200
|
-
actual: entry.position
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Check for duplicate positions
|
|
205
|
-
if (positionSet.has(entry.position)) {
|
|
206
|
-
issues.push({
|
|
207
|
-
type: 'duplicate_position',
|
|
208
|
-
position: entry.position,
|
|
209
|
-
instanceId: entry.instanceId
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
positionSet.add(entry.position);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
// Check for gaps in positions (should be 1, 2, 3... without skips)
|
|
216
|
-
const positions = queue.map(e => e.position).sort((a, b) => a - b);
|
|
217
|
-
for (let i = 0; i < positions.length; i++) {
|
|
218
|
-
if (positions[i] !== i + 1) {
|
|
219
|
-
issues.push({
|
|
220
|
-
type: 'position_gap',
|
|
221
|
-
expected: i + 1,
|
|
222
|
-
found: positions[i]
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
isValid: issues.length === 0,
|
|
229
|
-
issues
|
|
230
|
-
};
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Detect position changes between old and new queue states
|
|
235
|
-
* @param {Array} oldQueue - Previous queue state
|
|
236
|
-
* @param {Array} newQueue - New queue state
|
|
237
|
-
* @returns {Array} List of position changes
|
|
238
|
-
*/
|
|
239
|
-
function detectPositionChanges(oldQueue, newQueue) {
|
|
240
|
-
const changes = [];
|
|
241
|
-
|
|
242
|
-
// Build maps for quick lookup
|
|
243
|
-
const oldPositions = new Map();
|
|
244
|
-
oldQueue.forEach(entry => {
|
|
245
|
-
oldPositions.set(entry.instanceId, entry.position);
|
|
246
|
-
});
|
|
247
|
-
|
|
248
|
-
const newPositions = new Map();
|
|
249
|
-
newQueue.forEach(entry => {
|
|
250
|
-
newPositions.set(entry.instanceId, entry.position);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// Check each instance in new queue for position changes
|
|
254
|
-
newQueue.forEach(entry => {
|
|
255
|
-
const oldPosition = oldPositions.get(entry.instanceId);
|
|
256
|
-
|
|
257
|
-
if (oldPosition !== undefined && oldPosition !== entry.position) {
|
|
258
|
-
changes.push({
|
|
259
|
-
instanceId: entry.instanceId,
|
|
260
|
-
oldPosition,
|
|
261
|
-
newPosition: entry.position,
|
|
262
|
-
change: oldPosition - entry.position // Positive means moved up
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
return changes;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/**
|
|
271
|
-
* Remove instance from queue
|
|
272
|
-
* @param {string} instanceId - Instance to remove
|
|
273
|
-
*/
|
|
274
|
-
function removeFromQueue(instanceId) {
|
|
275
|
-
const data = readQueue();
|
|
276
|
-
|
|
277
|
-
// Capture old queue state for position change detection
|
|
278
|
-
const oldQueue = [...data.queue];
|
|
279
|
-
|
|
280
|
-
// Remove entry
|
|
281
|
-
data.queue = data.queue.filter(entry => entry.instanceId !== instanceId);
|
|
282
|
-
|
|
283
|
-
// Recompute positions
|
|
284
|
-
data.queue.forEach((entry, index) => {
|
|
285
|
-
entry.position = index + 1;
|
|
286
|
-
});
|
|
287
|
-
|
|
288
|
-
// Detect position changes
|
|
289
|
-
const positionChanges = detectPositionChanges(oldQueue, data.queue);
|
|
290
|
-
|
|
291
|
-
// Log position changes
|
|
292
|
-
if (positionChanges.length > 0) {
|
|
293
|
-
console.log(`📊 Queue position changes after removing ${instanceId}:`);
|
|
294
|
-
positionChanges.forEach(change => {
|
|
295
|
-
console.log(` • Instance ${change.instanceId}: position ${change.oldPosition} → ${change.newPosition} (moved up ${change.change} positions)`);
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// Validate queue integrity
|
|
300
|
-
const validation = validateQueueIntegrity(data.queue);
|
|
301
|
-
if (!validation.isValid) {
|
|
302
|
-
console.error('⚠️ Queue integrity issues detected:');
|
|
303
|
-
validation.issues.forEach(issue => {
|
|
304
|
-
console.error(` • ${issue.type}:`, issue);
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
writeQueue(data);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/**
|
|
312
|
-
* Get position of instance in queue
|
|
313
|
-
* @param {string} instanceId - Instance to check
|
|
314
|
-
* @returns {number|null} Position (1-indexed) or null if not in queue
|
|
315
|
-
*/
|
|
316
|
-
function getPosition(instanceId) {
|
|
317
|
-
const queue = getQueue();
|
|
318
|
-
const entry = queue.find(e => e.instanceId === instanceId);
|
|
319
|
-
return entry ? entry.position : null;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Get number of entries ahead in queue
|
|
324
|
-
* @param {string} instanceId - Instance to check
|
|
325
|
-
* @returns {number} Number of entries ahead
|
|
326
|
-
*/
|
|
327
|
-
function getEntriesAhead(instanceId) {
|
|
328
|
-
const position = getPosition(instanceId);
|
|
329
|
-
if (!position) return 0;
|
|
330
|
-
return position - 1;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
/**
|
|
334
|
-
* Clean up stale entries from queue
|
|
335
|
-
* @param {number} timeoutMs - Timeout in milliseconds (default 1 hour)
|
|
336
|
-
* @returns {number} Number of stale entries removed
|
|
337
|
-
*/
|
|
338
|
-
function cleanupStaleEntries(timeoutMs = 3600000) {
|
|
339
|
-
const data = readQueue();
|
|
340
|
-
const now = Date.now();
|
|
341
|
-
const oldQueue = [...data.queue];
|
|
342
|
-
|
|
343
|
-
// Filter out stale entries
|
|
344
|
-
const staleEntries = [];
|
|
345
|
-
data.queue = data.queue.filter(entry => {
|
|
346
|
-
// Handle missing or invalid timestamps
|
|
347
|
-
if (!entry.timestamp || typeof entry.timestamp !== 'number') {
|
|
348
|
-
staleEntries.push(entry);
|
|
349
|
-
return false; // Remove entries with invalid timestamps
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const age = now - entry.timestamp;
|
|
353
|
-
|
|
354
|
-
// Handle negative age (clock skew)
|
|
355
|
-
if (age < 0) {
|
|
356
|
-
console.warn(`⚠️ Entry ${entry.instanceId} has future timestamp - keeping but may indicate clock skew`);
|
|
357
|
-
return true;
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
if (age > timeoutMs) {
|
|
361
|
-
staleEntries.push(entry);
|
|
362
|
-
return false; // Remove stale entry
|
|
363
|
-
}
|
|
364
|
-
return true; // Keep fresh entry
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// If entries were removed, recalculate positions and log
|
|
368
|
-
if (staleEntries.length > 0) {
|
|
369
|
-
// Recompute positions
|
|
370
|
-
data.queue.forEach((entry, index) => {
|
|
371
|
-
entry.position = index + 1;
|
|
372
|
-
});
|
|
373
|
-
|
|
374
|
-
// Detect position changes
|
|
375
|
-
const changes = detectPositionChanges(oldQueue, data.queue);
|
|
376
|
-
|
|
377
|
-
// Log stale entry removal
|
|
378
|
-
console.log(`🧹 Removed ${staleEntries.length} stale queue entries`);
|
|
379
|
-
staleEntries.forEach(entry => {
|
|
380
|
-
const waitTime = entry.timestamp ? Math.floor((now - entry.timestamp) / 60000) : 'unknown';
|
|
381
|
-
console.log(` • Instance ${entry.instanceId} (waiting ${waitTime} minutes)`);
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
// Log position changes
|
|
385
|
-
if (changes.length > 0) {
|
|
386
|
-
console.log(`📊 Queue position changes after cleanup:`);
|
|
387
|
-
changes.forEach(change => {
|
|
388
|
-
console.log(` • Instance ${change.instanceId}: position ${change.oldPosition} → ${change.newPosition} (moved up ${change.change} positions)`);
|
|
389
|
-
});
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
// Validate queue integrity
|
|
393
|
-
const validation = validateQueueIntegrity(data.queue);
|
|
394
|
-
if (!validation.isValid) {
|
|
395
|
-
console.error('⚠️ Queue integrity issues detected after cleanup:');
|
|
396
|
-
validation.issues.forEach(issue => {
|
|
397
|
-
console.error(` • ${issue.type}:`, issue);
|
|
398
|
-
});
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
writeQueue(data);
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return staleEntries.length;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
module.exports = {
|
|
408
|
-
getQueue,
|
|
409
|
-
isEmpty,
|
|
410
|
-
addToQueue,
|
|
411
|
-
removeFromQueue,
|
|
412
|
-
getPosition,
|
|
413
|
-
getEntriesAhead,
|
|
414
|
-
validateQueueIntegrity,
|
|
415
|
-
detectPositionChanges,
|
|
416
|
-
cleanupStaleEntries
|
|
417
|
-
};
|
package/lib/queue-processor.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
const { getQueue, removeFromQueue, getPosition, getEntriesAhead } = require('./push-queue');
|
|
2
|
-
const { rebaseOnMain, pushDirectlyToMain } = require('./git-coordinator');
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Check if it's this instance's turn to push
|
|
6
|
-
* @param {string} instanceId - Instance to check
|
|
7
|
-
* @returns {boolean} True if at position 1
|
|
8
|
-
*/
|
|
9
|
-
function isMyTurn(instanceId) {
|
|
10
|
-
const position = getPosition(instanceId);
|
|
11
|
-
return position === 1;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Wait for turn by polling queue
|
|
16
|
-
* @param {string} instanceId - Instance to wait for
|
|
17
|
-
* @param {number} pollInterval - Milliseconds between checks (default: 5000)
|
|
18
|
-
* @returns {Promise<void>} Resolves when it's this instance's turn
|
|
19
|
-
*/
|
|
20
|
-
async function waitForTurn(instanceId, pollInterval = 5000) {
|
|
21
|
-
return new Promise((resolve) => {
|
|
22
|
-
const checkQueue = () => {
|
|
23
|
-
if (isMyTurn(instanceId)) {
|
|
24
|
-
resolve();
|
|
25
|
-
} else {
|
|
26
|
-
const entriesAhead = getEntriesAhead(instanceId);
|
|
27
|
-
console.log(`Waiting in queue (${entriesAhead} ahead)...`);
|
|
28
|
-
setTimeout(checkQueue, pollInterval);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
checkQueue();
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Monitor current position in queue
|
|
38
|
-
* @param {string} instanceId - Instance to monitor
|
|
39
|
-
* @returns {number|null} Current position or null if not in queue
|
|
40
|
-
*/
|
|
41
|
-
function monitorPosition(instanceId) {
|
|
42
|
-
return getPosition(instanceId);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Process queue entry: wait for turn, rebase, push, remove entry
|
|
47
|
-
* @param {string} instanceId - Instance processing queue
|
|
48
|
-
* @returns {Promise<void>}
|
|
49
|
-
*/
|
|
50
|
-
async function processQueue(instanceId) {
|
|
51
|
-
const initialPosition = getPosition(instanceId);
|
|
52
|
-
console.log(`Added to push queue (position ${initialPosition})`);
|
|
53
|
-
|
|
54
|
-
// Wait until it's our turn
|
|
55
|
-
await waitForTurn(instanceId);
|
|
56
|
-
|
|
57
|
-
// Now at position 1 - rebase and push
|
|
58
|
-
console.log('Your turn! Rebasing on main...');
|
|
59
|
-
rebaseOnMain();
|
|
60
|
-
|
|
61
|
-
console.log('Pushing to main...');
|
|
62
|
-
pushDirectlyToMain();
|
|
63
|
-
|
|
64
|
-
// Remove from queue
|
|
65
|
-
removeFromQueue(instanceId);
|
|
66
|
-
console.log('Push complete, removed from queue');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
module.exports = {
|
|
70
|
-
isMyTurn,
|
|
71
|
-
waitForTurn,
|
|
72
|
-
monitorPosition,
|
|
73
|
-
processQueue
|
|
74
|
-
};
|
package/lib/test-helpers.js
DELETED
|
@@ -1,202 +0,0 @@
|
|
|
1
|
-
const { execSync } = require('child_process');
|
|
2
|
-
const fs = require('fs');
|
|
3
|
-
const path = require('path');
|
|
4
|
-
const os = require('os');
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Creates an isolated test directory for testing
|
|
8
|
-
* @returns {Object} Test context with testDir and originalCwd
|
|
9
|
-
* @throws {Error} If temporary directory cannot be created
|
|
10
|
-
*/
|
|
11
|
-
function createTestEnvironment() {
|
|
12
|
-
// Get original cwd, handling case where current directory was deleted
|
|
13
|
-
let originalCwd;
|
|
14
|
-
try {
|
|
15
|
-
originalCwd = process.cwd();
|
|
16
|
-
} catch (err) {
|
|
17
|
-
// If current directory doesn't exist, use the project root
|
|
18
|
-
// This can happen when previous tests deleted their test directory
|
|
19
|
-
originalCwd = path.resolve(__dirname, '..');
|
|
20
|
-
try {
|
|
21
|
-
process.chdir(originalCwd);
|
|
22
|
-
} catch (chdirErr) {
|
|
23
|
-
throw new Error(`Failed to restore to valid directory: ${chdirErr.message}`);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
let testDir;
|
|
28
|
-
try {
|
|
29
|
-
testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'jettypod-test-'));
|
|
30
|
-
} catch (err) {
|
|
31
|
-
throw new Error(`Failed to create test directory: ${err.message}`);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
originalCwd,
|
|
36
|
-
testDir,
|
|
37
|
-
cleanup: () => {
|
|
38
|
-
try {
|
|
39
|
-
process.chdir(originalCwd);
|
|
40
|
-
} catch (err) {
|
|
41
|
-
// Directory may have been deleted, try project root
|
|
42
|
-
try {
|
|
43
|
-
process.chdir(path.resolve(__dirname, '..'));
|
|
44
|
-
} catch (chdirErr) {
|
|
45
|
-
// Ignore if we can't change directories
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
if (fs.existsSync(testDir)) {
|
|
49
|
-
try {
|
|
50
|
-
fs.rmSync(testDir, { recursive: true, force: true });
|
|
51
|
-
} catch (err) {
|
|
52
|
-
console.warn(`Warning: Failed to cleanup test directory: ${err.message}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Copies directory recursively, excluding test files
|
|
61
|
-
* @param {string} src - Source directory
|
|
62
|
-
* @param {string} dest - Destination directory
|
|
63
|
-
* @throws {Error} If src doesn't exist, is not a directory, or copy fails
|
|
64
|
-
*/
|
|
65
|
-
function copyDirRecursive(src, dest) {
|
|
66
|
-
if (!src || typeof src !== 'string') {
|
|
67
|
-
throw new Error('Source path must be a non-empty string');
|
|
68
|
-
}
|
|
69
|
-
if (!dest || typeof dest !== 'string') {
|
|
70
|
-
throw new Error('Destination path must be a non-empty string');
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
if (!fs.existsSync(src)) {
|
|
74
|
-
throw new Error(`Source directory does not exist: ${src}`);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const srcStat = fs.statSync(src);
|
|
78
|
-
if (!srcStat.isDirectory()) {
|
|
79
|
-
throw new Error(`Source is not a directory: ${src}`);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
if (!fs.existsSync(dest)) {
|
|
84
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
fs.readdirSync(src).forEach(item => {
|
|
88
|
-
const srcPath = path.join(src, item);
|
|
89
|
-
const destPath = path.join(dest, item);
|
|
90
|
-
|
|
91
|
-
if (fs.statSync(srcPath).isDirectory()) {
|
|
92
|
-
copyDirRecursive(srcPath, destPath);
|
|
93
|
-
} else {
|
|
94
|
-
// Skip test files when copying
|
|
95
|
-
if (!item.endsWith('.test.js')) {
|
|
96
|
-
fs.copyFileSync(srcPath, destPath);
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
});
|
|
100
|
-
} catch (err) {
|
|
101
|
-
throw new Error(`Failed to copy directory from ${src} to ${dest}: ${err.message}`);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Runs a JettyPod command in test environment
|
|
107
|
-
* @param {string} cmd - Command to run (e.g., 'jettypod init')
|
|
108
|
-
* @param {Object} context - Test context with originalCwd
|
|
109
|
-
* @returns {string} Command output
|
|
110
|
-
* @throws {Error} If cmd is invalid or context is missing
|
|
111
|
-
*/
|
|
112
|
-
function runCommand(cmd, context) {
|
|
113
|
-
if (!cmd || typeof cmd !== 'string') {
|
|
114
|
-
throw new Error('Command must be a non-empty string');
|
|
115
|
-
}
|
|
116
|
-
if (!context || typeof context !== 'object') {
|
|
117
|
-
throw new Error('Context must be an object');
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
try {
|
|
121
|
-
const productionJettypodPath = context.originalCwd ?
|
|
122
|
-
path.join(context.originalCwd, 'jettypod.js') :
|
|
123
|
-
path.join(process.cwd(), '..', '..', 'jettypod.js');
|
|
124
|
-
|
|
125
|
-
if (!fs.existsSync(productionJettypodPath)) {
|
|
126
|
-
throw new Error(`JettyPod script not found at: ${productionJettypodPath}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
const fullCmd = cmd.replace(/^jettypod/, `node ${productionJettypodPath}`);
|
|
130
|
-
return execSync(fullCmd, {
|
|
131
|
-
encoding: 'utf-8',
|
|
132
|
-
stdio: 'pipe'
|
|
133
|
-
});
|
|
134
|
-
} catch (error) {
|
|
135
|
-
return error.stdout || error.message;
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Reads config.json from .jettypod directory
|
|
141
|
-
* @returns {Object|null} Config object or null if not found
|
|
142
|
-
* @throws {Error} If JSON is corrupted or file cannot be read
|
|
143
|
-
*/
|
|
144
|
-
function readConfig() {
|
|
145
|
-
const configPath = '.jettypod/config.json';
|
|
146
|
-
|
|
147
|
-
if (!fs.existsSync(configPath)) {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
try {
|
|
152
|
-
const content = fs.readFileSync(configPath, 'utf-8');
|
|
153
|
-
return JSON.parse(content);
|
|
154
|
-
} catch (err) {
|
|
155
|
-
if (err instanceof SyntaxError) {
|
|
156
|
-
throw new Error(`Config file contains invalid JSON: ${err.message}`);
|
|
157
|
-
}
|
|
158
|
-
throw new Error(`Failed to read config file: ${err.message}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Reads CLAUDE.md from current directory
|
|
164
|
-
* @returns {string} CLAUDE.md content or empty string
|
|
165
|
-
* @throws {Error} If file exists but cannot be read
|
|
166
|
-
*/
|
|
167
|
-
function readClaude() {
|
|
168
|
-
if (!fs.existsSync('CLAUDE.md')) {
|
|
169
|
-
return '';
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
try {
|
|
173
|
-
return fs.readFileSync('CLAUDE.md', 'utf-8');
|
|
174
|
-
} catch (err) {
|
|
175
|
-
throw new Error(`Failed to read CLAUDE.md: ${err.message}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Initializes tracking for created files/directories
|
|
181
|
-
* @param {Object} context - Test context to initialize
|
|
182
|
-
* @throws {Error} If context is invalid
|
|
183
|
-
*/
|
|
184
|
-
function initializeTracking(context) {
|
|
185
|
-
if (!context || typeof context !== 'object') {
|
|
186
|
-
throw new Error('Context must be an object');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
context.createdFiles = [];
|
|
190
|
-
context.createdDirs = [];
|
|
191
|
-
context.modifiedFiles = new Map();
|
|
192
|
-
context.createdTestDirs = [];
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
module.exports = {
|
|
196
|
-
createTestEnvironment,
|
|
197
|
-
copyDirRecursive,
|
|
198
|
-
runCommand,
|
|
199
|
-
readConfig,
|
|
200
|
-
readClaude,
|
|
201
|
-
initializeTracking
|
|
202
|
-
};
|