jettypod 4.2.11 → 4.3.0
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/hooks/post-checkout +78 -3
- package/hooks/pre-push +33 -0
- package/jettypod.js +5 -1
- package/lib/claudemd.js +12 -4
- package/lib/current-work.js +97 -84
- package/lib/merge-lock.js +30 -0
- package/lib/session-writer.js +20 -0
- package/lib/worktree-facade.js +43 -0
- package/package.json +1 -1
- package/skills-templates/feature-planning/SKILL.md +155 -105
- package/skills-templates/production-mode/SKILL.md +4 -7
- package/skills-templates/speed-mode/SKILL.md +471 -463
- package/skills-templates/stable-mode/SKILL.md +319 -371
- package/lib/migrations/016-worktree-sessions-table.js +0 -84
- package/lib/worktree-sessions.js +0 -186
package/hooks/post-checkout
CHANGED
|
@@ -1,13 +1,88 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Post-Checkout Hook
|
|
5
|
+
*
|
|
6
|
+
* 1. Prevents checking out default branch in JettyPod worktrees
|
|
7
|
+
* 2. Imports database snapshots after checkout
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
const { importAll } = require('../lib/db-import');
|
|
11
|
+
const { execSync } = require('child_process');
|
|
4
12
|
|
|
5
13
|
(async () => {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
|
|
16
|
+
// FIRST: Check if we're in a JettyPod worktree and prevent default branch checkout
|
|
17
|
+
if (cwd.includes('.jettypod-work')) {
|
|
18
|
+
try {
|
|
19
|
+
// Get the branch we just checked out
|
|
20
|
+
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
21
|
+
encoding: 'utf8',
|
|
22
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
23
|
+
}).trim();
|
|
24
|
+
|
|
25
|
+
// Detect default branch
|
|
26
|
+
let defaultBranch;
|
|
27
|
+
try {
|
|
28
|
+
// Try to get the default branch from git config
|
|
29
|
+
defaultBranch = execSync('git symbolic-ref refs/remotes/origin/HEAD', {
|
|
30
|
+
encoding: 'utf8',
|
|
31
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
32
|
+
}).trim().replace('refs/remotes/origin/', '');
|
|
33
|
+
} catch {
|
|
34
|
+
// Fallback: check which common branch names exist
|
|
35
|
+
try {
|
|
36
|
+
execSync('git rev-parse --verify main', { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
37
|
+
defaultBranch = 'main';
|
|
38
|
+
} catch {
|
|
39
|
+
try {
|
|
40
|
+
execSync('git rev-parse --verify master', { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
41
|
+
defaultBranch = 'master';
|
|
42
|
+
} catch {
|
|
43
|
+
// Can't determine default branch - skip check
|
|
44
|
+
defaultBranch = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check if we checked out the default branch
|
|
50
|
+
if (defaultBranch && currentBranch === defaultBranch) {
|
|
51
|
+
console.error('');
|
|
52
|
+
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
53
|
+
console.error('❌ ERROR: Cannot checkout default branch in worktree');
|
|
54
|
+
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
55
|
+
console.error('');
|
|
56
|
+
console.error(`You are in a JettyPod worktree for isolated work.`);
|
|
57
|
+
console.error(`Checking out ${defaultBranch} in a worktree bypasses the merge workflow.`);
|
|
58
|
+
console.error('');
|
|
59
|
+
console.error('To merge your changes:');
|
|
60
|
+
console.error(' 1. Commit your changes: git add . && git commit -m "..."');
|
|
61
|
+
console.error(' 2. Use JettyPod merge: jettypod work merge');
|
|
62
|
+
console.error('');
|
|
63
|
+
console.error('This ensures proper worktree cleanup and database updates.');
|
|
64
|
+
console.error('');
|
|
65
|
+
console.error('Reverting checkout...');
|
|
66
|
+
console.error('');
|
|
67
|
+
|
|
68
|
+
// Revert to previous branch
|
|
69
|
+
try {
|
|
70
|
+
execSync('git checkout -', { stdio: 'inherit' });
|
|
71
|
+
} catch (revertErr) {
|
|
72
|
+
console.error('Failed to revert checkout. You may need to manually switch branches.');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// If we can't determine the branch, allow the checkout
|
|
79
|
+
// (better to be permissive than block legitimate operations)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// SECOND: Import database snapshots
|
|
6
84
|
try {
|
|
7
|
-
// Import JSON snapshots into databases after checkout
|
|
8
85
|
await importAll();
|
|
9
|
-
|
|
10
|
-
// Exit successfully - checkout should not be blocked
|
|
11
86
|
process.exit(0);
|
|
12
87
|
} catch (err) {
|
|
13
88
|
// Log error but don't block checkout
|
package/hooks/pre-push
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pre-Push Hook
|
|
5
|
+
*
|
|
6
|
+
* Prevents pushing directly from JettyPod worktrees.
|
|
7
|
+
* Forces use of `jettypod work merge` to ensure proper cleanup.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const cwd = process.cwd();
|
|
11
|
+
|
|
12
|
+
// Check if we're in a JettyPod worktree
|
|
13
|
+
if (cwd.includes('.jettypod-work')) {
|
|
14
|
+
console.error('');
|
|
15
|
+
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
16
|
+
console.error('❌ ERROR: Cannot push directly from worktree');
|
|
17
|
+
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
|
18
|
+
console.error('');
|
|
19
|
+
console.error('You are in a JettyPod worktree for isolated work.');
|
|
20
|
+
console.error('Pushing directly bypasses the merge workflow.');
|
|
21
|
+
console.error('');
|
|
22
|
+
console.error('To merge and push your changes:');
|
|
23
|
+
console.error(' 1. Commit your changes: git add . && git commit -m "..."');
|
|
24
|
+
console.error(' 2. Use JettyPod merge: jettypod work merge');
|
|
25
|
+
console.error('');
|
|
26
|
+
console.error('This ensures proper worktree cleanup and database updates.');
|
|
27
|
+
console.error('');
|
|
28
|
+
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Allow pushes from main repo
|
|
33
|
+
process.exit(0);
|
package/jettypod.js
CHANGED
|
@@ -1153,7 +1153,11 @@ switch (command) {
|
|
|
1153
1153
|
} else if (subcommand === 'merge') {
|
|
1154
1154
|
const workCommands = require('./features/work-commands/index.js');
|
|
1155
1155
|
try {
|
|
1156
|
-
|
|
1156
|
+
// Parse merge flags from args
|
|
1157
|
+
const withTransition = args.includes('--with-transition');
|
|
1158
|
+
const releaseLock = args.includes('--release-lock');
|
|
1159
|
+
|
|
1160
|
+
await workCommands.mergeWork({ withTransition, releaseLock });
|
|
1157
1161
|
} catch (err) {
|
|
1158
1162
|
console.error(`Error: ${err.message}`);
|
|
1159
1163
|
process.exit(1);
|
package/lib/claudemd.js
CHANGED
|
@@ -65,11 +65,19 @@ function updateCurrentWork(currentWork, mode) {
|
|
|
65
65
|
|
|
66
66
|
// Write to session file (gitignored) instead of CLAUDE.md
|
|
67
67
|
// This prevents merge conflicts and stale context on main branch
|
|
68
|
+
// Only write session file if we're in a worktree (not root directory)
|
|
68
69
|
if (currentMode !== null) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
try {
|
|
71
|
+
writeSessionFile(currentWork, currentMode, {
|
|
72
|
+
epicId: currentWork.epic_id,
|
|
73
|
+
epicTitle: currentWork.epic_title
|
|
74
|
+
});
|
|
75
|
+
} catch (err) {
|
|
76
|
+
// Skip if not in worktree - validation will throw error
|
|
77
|
+
if (!err.message.includes('Cannot create session.md in root directory')) {
|
|
78
|
+
throw err; // Re-throw unexpected errors
|
|
79
|
+
}
|
|
80
|
+
}
|
|
73
81
|
}
|
|
74
82
|
}
|
|
75
83
|
|
package/lib/current-work.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const { getDb } = require('./database');
|
|
4
|
-
const { getSessionByWorktreePath, getAllActiveSessions } = require('./worktree-sessions');
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Get path to current-work.json file
|
|
@@ -12,35 +11,62 @@ function getCurrentWorkPath() {
|
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
|
-
* Get current work item
|
|
14
|
+
* Get current work item based on status='in_progress'
|
|
15
|
+
* Extracts work item ID from worktree branch name if in a worktree,
|
|
16
|
+
* otherwise returns first in_progress item
|
|
16
17
|
* @returns {Object|null} Current work item or null if not set
|
|
17
18
|
*/
|
|
18
19
|
async function getCurrentWork() {
|
|
19
20
|
const worktreePath = process.cwd();
|
|
20
21
|
const db = getDb();
|
|
22
|
+
const { execSync } = require('child_process');
|
|
23
|
+
|
|
24
|
+
// Try to extract work item ID from branch name (worktree naming: "feature/work-123-some-title" or "123-some-title")
|
|
25
|
+
let workItemId = null;
|
|
26
|
+
try {
|
|
27
|
+
const branchName = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
28
|
+
// Match either "feature/work-123-" or "123-" at start
|
|
29
|
+
const match = branchName.match(/(?:feature\/work-|^)(\d+)-/);
|
|
30
|
+
if (match) {
|
|
31
|
+
workItemId = parseInt(match[1]);
|
|
32
|
+
}
|
|
33
|
+
} catch (err) {
|
|
34
|
+
// Not in a git repo or other git error - that's ok
|
|
35
|
+
}
|
|
21
36
|
|
|
22
|
-
// Query
|
|
37
|
+
// Query for in_progress work item
|
|
23
38
|
let row;
|
|
24
39
|
try {
|
|
25
40
|
row = await new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const query = workItemId
|
|
42
|
+
? // If we have an ID from branch name, get that specific item
|
|
43
|
+
`SELECT
|
|
44
|
+
wi.id, wi.title, wi.type, wi.status, wi.parent_id, wi.epic_id,
|
|
45
|
+
parent.title as parent_title,
|
|
46
|
+
epic.title as epic_title
|
|
47
|
+
FROM work_items wi
|
|
48
|
+
LEFT JOIN work_items parent ON wi.parent_id = parent.id
|
|
49
|
+
LEFT JOIN work_items epic ON wi.epic_id = epic.id
|
|
50
|
+
WHERE wi.id = ? AND wi.status = 'in_progress'`
|
|
51
|
+
: // Otherwise get any in_progress item (for main branch)
|
|
52
|
+
`SELECT
|
|
53
|
+
wi.id, wi.title, wi.type, wi.status, wi.parent_id, wi.epic_id,
|
|
54
|
+
parent.title as parent_title,
|
|
55
|
+
epic.title as epic_title
|
|
56
|
+
FROM work_items wi
|
|
57
|
+
LEFT JOIN work_items parent ON wi.parent_id = parent.id
|
|
58
|
+
LEFT JOIN work_items epic ON wi.epic_id = epic.id
|
|
59
|
+
WHERE wi.status = 'in_progress'
|
|
60
|
+
LIMIT 1`;
|
|
61
|
+
|
|
62
|
+
const params = workItemId ? [workItemId] : [];
|
|
63
|
+
|
|
64
|
+
db.get(query, params, (err, row) => {
|
|
65
|
+
if (err) {
|
|
66
|
+
return reject(err);
|
|
42
67
|
}
|
|
43
|
-
|
|
68
|
+
resolve(row);
|
|
69
|
+
});
|
|
44
70
|
});
|
|
45
71
|
} catch (dbErr) {
|
|
46
72
|
// Handle database errors gracefully - log but don't crash
|
|
@@ -58,53 +84,6 @@ async function getCurrentWork() {
|
|
|
58
84
|
return row;
|
|
59
85
|
}
|
|
60
86
|
|
|
61
|
-
// No session found - check if we should warn about main branch
|
|
62
|
-
try {
|
|
63
|
-
const allSessions = await getAllActiveSessions();
|
|
64
|
-
if (allSessions.length > 0) {
|
|
65
|
-
// Get all work items in ONE query using IN clause
|
|
66
|
-
const ids = allSessions.map(s => s.work_item_id);
|
|
67
|
-
const placeholders = ids.map(() => '?').join(',');
|
|
68
|
-
|
|
69
|
-
let workItems;
|
|
70
|
-
try {
|
|
71
|
-
workItems = await new Promise((resolve, reject) => {
|
|
72
|
-
db.all(
|
|
73
|
-
`SELECT id, title FROM work_items WHERE id IN (${placeholders})`,
|
|
74
|
-
ids,
|
|
75
|
-
(err, rows) => {
|
|
76
|
-
if (err) reject(err);
|
|
77
|
-
else resolve(rows || []);
|
|
78
|
-
}
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
} catch (workItemErr) {
|
|
82
|
-
// Database error getting work items - log but continue
|
|
83
|
-
console.error('Database error getting work items:', workItemErr.message);
|
|
84
|
-
workItems = [];
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Create map of id -> title
|
|
88
|
-
const titleMap = {};
|
|
89
|
-
for (const wi of workItems) {
|
|
90
|
-
titleMap[wi.id] = wi.title;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Display warnings
|
|
94
|
-
console.warn('No active work in main branch');
|
|
95
|
-
console.warn('\nActive work in other worktrees:');
|
|
96
|
-
for (const session of allSessions) {
|
|
97
|
-
const title = titleMap[session.work_item_id];
|
|
98
|
-
if (title) {
|
|
99
|
-
console.warn(` • ${title} (${session.worktree_path})`);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
} catch (listErr) {
|
|
104
|
-
// Ignore listing errors - gracefully fail
|
|
105
|
-
console.error('Error listing active sessions:', listErr.message);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
87
|
return null;
|
|
109
88
|
}
|
|
110
89
|
|
|
@@ -136,41 +115,75 @@ function validateWorkItem(workItem) {
|
|
|
136
115
|
}
|
|
137
116
|
|
|
138
117
|
/**
|
|
139
|
-
* Set current work item
|
|
118
|
+
* Set current work item by updating status to in_progress
|
|
140
119
|
* @param {Object} workItem - Work item to set as current
|
|
141
120
|
* @throws {Error} If workItem is invalid
|
|
142
121
|
*/
|
|
143
122
|
async function setCurrentWork(workItem) {
|
|
144
123
|
validateWorkItem(workItem);
|
|
145
124
|
|
|
146
|
-
const
|
|
147
|
-
const { execSync } = require('child_process');
|
|
148
|
-
const worktreePath = process.cwd();
|
|
125
|
+
const db = getDb();
|
|
149
126
|
|
|
150
|
-
//
|
|
151
|
-
let branchName;
|
|
127
|
+
// Update work item status to in_progress
|
|
152
128
|
try {
|
|
153
|
-
|
|
129
|
+
await new Promise((resolve, reject) => {
|
|
130
|
+
db.run(
|
|
131
|
+
'UPDATE work_items SET status = ? WHERE id = ?',
|
|
132
|
+
['in_progress', workItem.id],
|
|
133
|
+
(err) => {
|
|
134
|
+
if (err) {
|
|
135
|
+
return reject(new Error(`Failed to set work item status: ${err.message}`));
|
|
136
|
+
}
|
|
137
|
+
resolve();
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
});
|
|
154
141
|
} catch (err) {
|
|
155
|
-
|
|
142
|
+
throw new Error(`Failed to set current work: ${err.message}`);
|
|
156
143
|
}
|
|
157
|
-
|
|
158
|
-
// Create or update worktree session in database
|
|
159
|
-
await createOrUpdateSession(worktreePath, workItem.id, branchName);
|
|
160
144
|
}
|
|
161
145
|
|
|
162
146
|
/**
|
|
163
|
-
* Clear current work item from
|
|
164
|
-
* @throws {Error} If
|
|
147
|
+
* Clear current work item by extracting ID from branch and setting status to backlog
|
|
148
|
+
* @throws {Error} If work item cannot be found or updated
|
|
165
149
|
*/
|
|
166
150
|
async function clearCurrentWork() {
|
|
167
|
-
const {
|
|
168
|
-
const
|
|
151
|
+
const { execSync } = require('child_process');
|
|
152
|
+
const db = getDb();
|
|
169
153
|
|
|
154
|
+
// Extract work item ID from branch name
|
|
155
|
+
let workItemId = null;
|
|
170
156
|
try {
|
|
171
|
-
|
|
157
|
+
const branchName = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf-8' }).trim();
|
|
158
|
+
// Match either "feature/work-123-" or "123-" at start
|
|
159
|
+
const match = branchName.match(/(?:feature\/work-|^)(\d+)-/);
|
|
160
|
+
if (match) {
|
|
161
|
+
workItemId = parseInt(match[1]);
|
|
162
|
+
}
|
|
163
|
+
} catch (err) {
|
|
164
|
+
throw new Error('Cannot determine work item from branch name');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (!workItemId) {
|
|
168
|
+
throw new Error('Cannot clear current work: no work item ID in branch name');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Update work item status back to backlog
|
|
172
|
+
try {
|
|
173
|
+
await new Promise((resolve, reject) => {
|
|
174
|
+
db.run(
|
|
175
|
+
'UPDATE work_items SET status = ? WHERE id = ?',
|
|
176
|
+
['backlog', workItemId],
|
|
177
|
+
(err) => {
|
|
178
|
+
if (err) {
|
|
179
|
+
return reject(new Error(`Failed to clear work item status: ${err.message}`));
|
|
180
|
+
}
|
|
181
|
+
resolve();
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
});
|
|
172
185
|
} catch (err) {
|
|
173
|
-
throw new Error(`Failed to clear current work
|
|
186
|
+
throw new Error(`Failed to clear current work: ${err.message}`);
|
|
174
187
|
}
|
|
175
188
|
}
|
|
176
189
|
|
package/lib/merge-lock.js
CHANGED
|
@@ -36,11 +36,20 @@ async function acquireMergeLock(db, workItemId, instanceId = null, options = {})
|
|
|
36
36
|
|
|
37
37
|
const maxWait = options.maxWait || 60000; // 1 minute default
|
|
38
38
|
const pollInterval = options.pollInterval || 1500; // 1.5 seconds
|
|
39
|
+
const staleThreshold = options.staleThreshold || 120000; // 2 minutes (reduced from 5 minutes)
|
|
39
40
|
const startTime = Date.now();
|
|
40
41
|
|
|
41
42
|
// Generate instance identifier
|
|
42
43
|
const lockedBy = instanceId || `${os.hostname()}-${process.pid}`;
|
|
43
44
|
|
|
45
|
+
// Proactive cleanup of stale locks before attempting acquisition
|
|
46
|
+
try {
|
|
47
|
+
await cleanupStaleLocks(db, staleThreshold);
|
|
48
|
+
} catch (cleanupErr) {
|
|
49
|
+
// Log but don't fail - cleanup is best-effort
|
|
50
|
+
console.warn(`Warning: Failed to cleanup stale locks: ${cleanupErr.message}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
while (Date.now() - startTime < maxWait) {
|
|
45
54
|
// Check if lock exists
|
|
46
55
|
const existingLock = await checkExistingLock(db);
|
|
@@ -102,6 +111,27 @@ function insertLock(db, workItemId, lockedBy) {
|
|
|
102
111
|
});
|
|
103
112
|
}
|
|
104
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Clean up stale locks that are older than the threshold
|
|
116
|
+
*
|
|
117
|
+
* @param {Object} db - SQLite database connection
|
|
118
|
+
* @param {number} staleThresholdMs - Age threshold in milliseconds (default: 120000 = 2 minutes)
|
|
119
|
+
* @returns {Promise<void>}
|
|
120
|
+
*/
|
|
121
|
+
function cleanupStaleLocks(db, staleThresholdMs = 120000) {
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
db.run(
|
|
124
|
+
`DELETE FROM merge_locks
|
|
125
|
+
WHERE (julianday('now') - julianday(locked_at)) * 86400000 > ?`,
|
|
126
|
+
[staleThresholdMs],
|
|
127
|
+
(err) => {
|
|
128
|
+
if (err) return reject(err);
|
|
129
|
+
resolve();
|
|
130
|
+
}
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
105
135
|
/**
|
|
106
136
|
* Create lock handle object with release function
|
|
107
137
|
*
|
package/lib/session-writer.js
CHANGED
|
@@ -19,6 +19,23 @@ function getSessionFilePath() {
|
|
|
19
19
|
return path.join(process.cwd(), '.claude', 'session.md');
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Validate that we're in a worktree, not the root directory
|
|
24
|
+
* @throws {Error} If in root directory
|
|
25
|
+
*/
|
|
26
|
+
function validateWorktreeLocation() {
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
|
|
29
|
+
// Check if current directory is inside .jettypod-work/
|
|
30
|
+
if (!cwd.includes('.jettypod-work')) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
'Cannot create session.md in root directory.\n' +
|
|
33
|
+
'Session files should only be created in worktrees (.jettypod-work/).\n' +
|
|
34
|
+
'Use "jettypod work start <id>" to create a worktree for this work item.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
/**
|
|
23
40
|
* Ensure .claude directory exists
|
|
24
41
|
*/
|
|
@@ -69,6 +86,9 @@ function writeSessionFile(workItem, mode, options = {}) {
|
|
|
69
86
|
throw new Error('Work item must have a string status');
|
|
70
87
|
}
|
|
71
88
|
|
|
89
|
+
// Validate we're in a worktree, not root directory
|
|
90
|
+
validateWorktreeLocation();
|
|
91
|
+
|
|
72
92
|
ensureClaudeDir();
|
|
73
93
|
|
|
74
94
|
const skillName = modeToSkillName(mode);
|
package/lib/worktree-facade.js
CHANGED
|
@@ -15,6 +15,29 @@
|
|
|
15
15
|
const worktreeManager = require('./worktree-manager');
|
|
16
16
|
const fs = require('fs');
|
|
17
17
|
const path = require('path');
|
|
18
|
+
const { getDb } = require('./database');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if a worktree already exists for a work item
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} db - Database connection
|
|
24
|
+
* @param {number} workItemId - Work item ID
|
|
25
|
+
* @returns {Promise<Object|null>} Existing worktree or null
|
|
26
|
+
*/
|
|
27
|
+
function checkExistingWorktree(db, workItemId) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
db.get(
|
|
30
|
+
`SELECT * FROM worktrees
|
|
31
|
+
WHERE work_item_id = ?
|
|
32
|
+
AND status = 'active'`,
|
|
33
|
+
[workItemId],
|
|
34
|
+
(err, row) => {
|
|
35
|
+
if (err) return reject(err);
|
|
36
|
+
resolve(row || null);
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
/**
|
|
20
43
|
* Start work on a work item with graceful degradation
|
|
@@ -33,6 +56,7 @@ async function startWork(workItem, options = {}) {
|
|
|
33
56
|
}
|
|
34
57
|
|
|
35
58
|
const repoPath = options.repoPath || process.cwd();
|
|
59
|
+
const db = options.db || getDb();
|
|
36
60
|
const result = {
|
|
37
61
|
mode: null, // 'worktree' or 'main'
|
|
38
62
|
path: null, // Working directory path
|
|
@@ -41,6 +65,25 @@ async function startWork(workItem, options = {}) {
|
|
|
41
65
|
warnings: [] // Non-fatal warnings
|
|
42
66
|
};
|
|
43
67
|
|
|
68
|
+
// Explicit check for existing worktree (defensive programming)
|
|
69
|
+
try {
|
|
70
|
+
const existingWorktree = await checkExistingWorktree(db, workItem.id);
|
|
71
|
+
if (existingWorktree) {
|
|
72
|
+
// Another instance is already working on this - fall back to main
|
|
73
|
+
result.mode = 'main';
|
|
74
|
+
result.path = repoPath;
|
|
75
|
+
result.error = {
|
|
76
|
+
message: `Work item #${workItem.id} already has active worktree at ${existingWorktree.worktree_path}`,
|
|
77
|
+
reason: 'duplicate_worktree_prevented'
|
|
78
|
+
};
|
|
79
|
+
result.warnings.push('Another instance may be working on this chore');
|
|
80
|
+
return result;
|
|
81
|
+
}
|
|
82
|
+
} catch (checkErr) {
|
|
83
|
+
// If check fails, continue anyway - database constraint will catch duplicates
|
|
84
|
+
result.warnings.push(`Warning: Could not check for existing worktree: ${checkErr.message}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
44
87
|
// Attempt to create worktree
|
|
45
88
|
try {
|
|
46
89
|
const worktree = await worktreeManager.createWorktree(workItem, options);
|