jettypod 4.4.55 → 4.4.57
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/claude-hooks/global-guardrails.js +43 -2
- package/lib/merge-lock.js +38 -0
- package/lib/work-commands/index.js +26 -1
- package/package.json +1 -1
|
@@ -11,14 +11,40 @@
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const path = require('path');
|
|
13
13
|
|
|
14
|
+
// Debug mode: set GUARDRAILS_DEBUG=1 to enable verbose logging
|
|
15
|
+
const DEBUG = process.env.GUARDRAILS_DEBUG === '1';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Structured debug logger - outputs to stderr to not interfere with JSON response
|
|
19
|
+
*/
|
|
20
|
+
function debug(category, data) {
|
|
21
|
+
if (!DEBUG) return;
|
|
22
|
+
const timestamp = new Date().toISOString();
|
|
23
|
+
const logEntry = { timestamp, category, ...data };
|
|
24
|
+
console.error(`[guardrails] ${JSON.stringify(logEntry)}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
14
27
|
// Read hook input from stdin
|
|
15
28
|
let input = '';
|
|
16
29
|
process.stdin.on('data', chunk => input += chunk);
|
|
17
30
|
process.stdin.on('end', () => {
|
|
18
31
|
try {
|
|
19
32
|
const hookInput = JSON.parse(input);
|
|
33
|
+
debug('input', {
|
|
34
|
+
tool: hookInput.tool_name,
|
|
35
|
+
cwd: hookInput.cwd,
|
|
36
|
+
active_worktree: hookInput.active_worktree_path,
|
|
37
|
+
branch: hookInput.current_branch
|
|
38
|
+
});
|
|
39
|
+
|
|
20
40
|
const result = evaluateRequest(hookInput);
|
|
21
41
|
|
|
42
|
+
debug('decision', {
|
|
43
|
+
tool: hookInput.tool_name,
|
|
44
|
+
allowed: result.allowed,
|
|
45
|
+
message: result.message || null
|
|
46
|
+
});
|
|
47
|
+
|
|
22
48
|
if (result.allowed) {
|
|
23
49
|
allow();
|
|
24
50
|
} else {
|
|
@@ -26,6 +52,7 @@ process.stdin.on('end', () => {
|
|
|
26
52
|
}
|
|
27
53
|
} catch (err) {
|
|
28
54
|
// On parse error, allow (fail open)
|
|
55
|
+
debug('error', { type: 'parse', message: err.message });
|
|
29
56
|
console.error('Hook error:', err.message);
|
|
30
57
|
allow();
|
|
31
58
|
}
|
|
@@ -159,6 +186,7 @@ function findDatabasePath(cwd) {
|
|
|
159
186
|
function getActiveWorktreePathFromDB(cwd) {
|
|
160
187
|
const dbPath = findDatabasePath(cwd);
|
|
161
188
|
if (!dbPath) {
|
|
189
|
+
debug('db_lookup', { status: 'no_db_found', cwd });
|
|
162
190
|
return null;
|
|
163
191
|
}
|
|
164
192
|
|
|
@@ -170,8 +198,11 @@ function getActiveWorktreePathFromDB(cwd) {
|
|
|
170
198
|
`SELECT worktree_path FROM worktrees WHERE status = 'active' LIMIT 1`
|
|
171
199
|
).get();
|
|
172
200
|
db.close();
|
|
173
|
-
|
|
201
|
+
const result = row ? row.worktree_path : null;
|
|
202
|
+
debug('db_lookup', { status: 'success', method: 'better-sqlite3', worktree: result });
|
|
203
|
+
return result;
|
|
174
204
|
} catch (err) {
|
|
205
|
+
debug('db_lookup', { status: 'fallback', reason: err.message });
|
|
175
206
|
// Fall back to CLI
|
|
176
207
|
const { spawnSync } = require('child_process');
|
|
177
208
|
const result = spawnSync('sqlite3', [
|
|
@@ -180,9 +211,12 @@ function getActiveWorktreePathFromDB(cwd) {
|
|
|
180
211
|
], { encoding: 'utf-8' });
|
|
181
212
|
|
|
182
213
|
if (result.error || result.status !== 0) {
|
|
214
|
+
debug('db_lookup', { status: 'cli_failed', error: result.error?.message || 'non-zero exit' });
|
|
183
215
|
return null;
|
|
184
216
|
}
|
|
185
|
-
|
|
217
|
+
const worktree = result.stdout.trim() || null;
|
|
218
|
+
debug('db_lookup', { status: 'success', method: 'sqlite3-cli', worktree });
|
|
219
|
+
return worktree;
|
|
186
220
|
}
|
|
187
221
|
}
|
|
188
222
|
|
|
@@ -197,6 +231,13 @@ function evaluateWriteOperation(filePath, inputWorktreePath, cwd) {
|
|
|
197
231
|
const normalizedPath = path.resolve(cwd || '.', filePath);
|
|
198
232
|
const normalizedWorktree = activeWorktreePath ? path.resolve(cwd || '.', activeWorktreePath) : null;
|
|
199
233
|
|
|
234
|
+
debug('write_eval', {
|
|
235
|
+
filePath,
|
|
236
|
+
normalizedPath,
|
|
237
|
+
activeWorktreePath,
|
|
238
|
+
normalizedWorktree
|
|
239
|
+
});
|
|
240
|
+
|
|
200
241
|
// BLOCKED: Protected files (skills, hooks)
|
|
201
242
|
const protectedPatterns = [
|
|
202
243
|
/\.claude\/skills\//i,
|
package/lib/merge-lock.js
CHANGED
|
@@ -43,6 +43,8 @@ async function acquireMergeLock(db, workItemId, instanceId = null, options = {})
|
|
|
43
43
|
const lockedBy = instanceId || `${os.hostname()}-${process.pid}`;
|
|
44
44
|
|
|
45
45
|
let pollCount = 0;
|
|
46
|
+
let lastStatusTime = 0;
|
|
47
|
+
const statusInterval = 10000; // Show status every 10 seconds
|
|
46
48
|
|
|
47
49
|
while (Date.now() - startTime < maxWait) {
|
|
48
50
|
// Cleanup stale locks every 5 polls (every ~10 seconds with 2s poll interval)
|
|
@@ -74,6 +76,21 @@ async function acquireMergeLock(db, workItemId, instanceId = null, options = {})
|
|
|
74
76
|
// Race condition - someone else got it first
|
|
75
77
|
// Continue polling
|
|
76
78
|
}
|
|
79
|
+
} else {
|
|
80
|
+
// Show progress when waiting for existing lock
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
if (now - lastStatusTime >= statusInterval) {
|
|
83
|
+
const waitedSeconds = Math.round((now - startTime) / 1000);
|
|
84
|
+
const lockAge = await getLockAge(db, existingLock.id);
|
|
85
|
+
const staleIn = Math.max(0, Math.round((staleThreshold - lockAge) / 1000));
|
|
86
|
+
|
|
87
|
+
if (staleIn > 0) {
|
|
88
|
+
console.log(`⏳ Waiting for merge lock... (${waitedSeconds}s elapsed, lock expires in ~${staleIn}s)`);
|
|
89
|
+
} else {
|
|
90
|
+
console.log(`⏳ Waiting for merge lock... (${waitedSeconds}s elapsed, cleaning up stale lock)`);
|
|
91
|
+
}
|
|
92
|
+
lastStatusTime = now;
|
|
93
|
+
}
|
|
77
94
|
}
|
|
78
95
|
|
|
79
96
|
// Wait before retrying
|
|
@@ -98,6 +115,27 @@ function checkExistingLock(db) {
|
|
|
98
115
|
});
|
|
99
116
|
}
|
|
100
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Get the age of a lock in milliseconds
|
|
120
|
+
*
|
|
121
|
+
* @param {Object} db - SQLite database connection
|
|
122
|
+
* @param {number} lockId - Lock ID
|
|
123
|
+
* @returns {Promise<number>} Age in milliseconds
|
|
124
|
+
*/
|
|
125
|
+
function getLockAge(db, lockId) {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
db.get(
|
|
128
|
+
`SELECT (julianday('now') - julianday(locked_at)) * 86400000 as age_ms
|
|
129
|
+
FROM merge_locks WHERE id = ?`,
|
|
130
|
+
[lockId],
|
|
131
|
+
(err, row) => {
|
|
132
|
+
if (err) reject(err);
|
|
133
|
+
else resolve(row ? row.age_ms : 0);
|
|
134
|
+
}
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
101
139
|
/**
|
|
102
140
|
* Insert lock record into database
|
|
103
141
|
*
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { execSync } = require('child_process');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
-
const { getDb, getDbPath, getJettypodDir } = require('../../lib/database');
|
|
4
|
+
const { getDb, getDbPath, getJettypodDir, waitForMigrations } = require('../../lib/database');
|
|
5
5
|
const { getCurrentWork, setCurrentWork, clearCurrentWork, getCurrentWorkPath } = require('../../lib/current-work');
|
|
6
6
|
const { VALID_STATUSES } = require('../../lib/constants');
|
|
7
7
|
const { updateCurrentWork } = require('../../lib/claudemd');
|
|
@@ -1230,6 +1230,9 @@ async function mergeWork(options = {}) {
|
|
|
1230
1230
|
const mergeLock = require('../../lib/merge-lock');
|
|
1231
1231
|
const db = getDb();
|
|
1232
1232
|
|
|
1233
|
+
// Wait for database migrations to complete before using merge_locks table
|
|
1234
|
+
await waitForMigrations();
|
|
1235
|
+
|
|
1233
1236
|
console.log('⏳ Acquiring merge lock...');
|
|
1234
1237
|
let lock;
|
|
1235
1238
|
try {
|
|
@@ -1249,6 +1252,24 @@ async function mergeWork(options = {}) {
|
|
|
1249
1252
|
return Promise.reject(new Error(`Failed to acquire merge lock: ${lockErr.message}`));
|
|
1250
1253
|
}
|
|
1251
1254
|
|
|
1255
|
+
// Set up signal handlers to release lock on interrupt (Ctrl+C, kill, etc.)
|
|
1256
|
+
// Without this, process termination leaves orphan locks that block for 90 seconds
|
|
1257
|
+
const cleanupAndExit = async (signal) => {
|
|
1258
|
+
console.log(`\n⚠️ Received ${signal}, releasing merge lock...`);
|
|
1259
|
+
if (lock) {
|
|
1260
|
+
try {
|
|
1261
|
+
await lock.release();
|
|
1262
|
+
console.log('✅ Merge lock released');
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
console.warn(`Warning: Failed to release lock: ${err.message}`);
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
process.exit(1);
|
|
1268
|
+
};
|
|
1269
|
+
|
|
1270
|
+
process.on('SIGINT', cleanupAndExit);
|
|
1271
|
+
process.on('SIGTERM', cleanupAndExit);
|
|
1272
|
+
|
|
1252
1273
|
// Wrap all merge operations in try/finally to ensure lock release
|
|
1253
1274
|
try {
|
|
1254
1275
|
// Get feature branch - either passed explicitly, from worktree DB, or from current CWD
|
|
@@ -1617,6 +1638,10 @@ async function mergeWork(options = {}) {
|
|
|
1617
1638
|
|
|
1618
1639
|
return Promise.resolve();
|
|
1619
1640
|
} finally {
|
|
1641
|
+
// Remove signal handlers to avoid interfering with other operations
|
|
1642
|
+
process.removeListener('SIGINT', cleanupAndExit);
|
|
1643
|
+
process.removeListener('SIGTERM', cleanupAndExit);
|
|
1644
|
+
|
|
1620
1645
|
// Release lock unless holding for transition
|
|
1621
1646
|
if (lock && !withTransition) {
|
|
1622
1647
|
try {
|