xtrm-tools 0.5.21 → 0.5.23
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/cli/dist/index.cjs +202 -9
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/hooks/beads-claim-sync.mjs +39 -0
- package/hooks/beads-commit-gate.mjs +26 -1
- package/hooks/beads-edit-gate.mjs +40 -6
- package/hooks/beads-memory-gate.mjs +21 -1
- package/hooks/beads-stop-gate.mjs +22 -1
- package/hooks/quality-check.cjs +53 -53
- package/hooks/quality-check.py +36 -36
- package/hooks/xtrm-logger.mjs +123 -0
- package/package.json +1 -1
package/hooks/quality-check.py
CHANGED
|
@@ -26,20 +26,20 @@ class Colors:
|
|
|
26
26
|
RESET = '\x1b[0m'
|
|
27
27
|
|
|
28
28
|
def log_info(msg: str):
|
|
29
|
-
print(f"{Colors.BLUE}[INFO]{Colors.RESET} {msg}"
|
|
29
|
+
print(f"{Colors.BLUE}[INFO]{Colors.RESET} {msg}")
|
|
30
30
|
|
|
31
31
|
def log_error(msg: str):
|
|
32
|
-
print(f"{Colors.RED}[ERROR]{Colors.RESET} {msg}"
|
|
32
|
+
print(f"{Colors.RED}[ERROR]{Colors.RESET} {msg}")
|
|
33
33
|
|
|
34
34
|
def log_success(msg: str):
|
|
35
|
-
print(f"{Colors.GREEN}[OK]{Colors.RESET} {msg}"
|
|
35
|
+
print(f"{Colors.GREEN}[OK]{Colors.RESET} {msg}")
|
|
36
36
|
|
|
37
37
|
def log_warning(msg: str):
|
|
38
|
-
print(f"{Colors.YELLOW}[WARN]{Colors.RESET} {msg}"
|
|
38
|
+
print(f"{Colors.YELLOW}[WARN]{Colors.RESET} {msg}")
|
|
39
39
|
|
|
40
40
|
def log_debug(msg: str):
|
|
41
41
|
if os.environ.get('CLAUDE_HOOKS_DEBUG', 'false').lower() == 'true':
|
|
42
|
-
print(f"{Colors.CYAN}[DEBUG]{Colors.RESET} {msg}"
|
|
42
|
+
print(f"{Colors.CYAN}[DEBUG]{Colors.RESET} {msg}")
|
|
43
43
|
|
|
44
44
|
def find_project_root(file_path: str) -> str:
|
|
45
45
|
"""Find project root by looking for pyproject.toml, setup.py, or .git"""
|
|
@@ -203,20 +203,20 @@ def check_pytest_suggestions(file_path: str, project_root: str):
|
|
|
203
203
|
def print_summary(errors: list[str], autofixes: list[str]):
|
|
204
204
|
"""Print summary of errors and autofixes"""
|
|
205
205
|
if autofixes:
|
|
206
|
-
print(f'\n{Colors.BLUE}═══ Auto-fixes Applied ═══{Colors.RESET}'
|
|
206
|
+
print(f'\n{Colors.BLUE}═══ Auto-fixes Applied ═══{Colors.RESET}')
|
|
207
207
|
for fix in autofixes:
|
|
208
|
-
print(f'{Colors.GREEN}✨{Colors.RESET} {fix}'
|
|
209
|
-
print(f'{Colors.GREEN}Automatically fixed {len(autofixes)} issue(s)!{Colors.RESET}'
|
|
208
|
+
print(f'{Colors.GREEN}✨{Colors.RESET} {fix}')
|
|
209
|
+
print(f'{Colors.GREEN}Automatically fixed {len(autofixes)} issue(s)!{Colors.RESET}')
|
|
210
210
|
|
|
211
211
|
if errors:
|
|
212
|
-
print(f'\n{Colors.BLUE}═══ Quality Check Summary ═══{Colors.RESET}'
|
|
212
|
+
print(f'\n{Colors.BLUE}═══ Quality Check Summary ═══{Colors.RESET}')
|
|
213
213
|
for error in errors:
|
|
214
|
-
print(f'{Colors.RED}❌{Colors.RESET} {error}'
|
|
215
|
-
print(f'\n{Colors.RED}Found {len(errors)} issue(s) that MUST be fixed!{Colors.RESET}'
|
|
216
|
-
print(f'{Colors.RED}══════════════════════════════════════{Colors.RESET}'
|
|
217
|
-
print(f'{Colors.RED}❌ ALL ISSUES ARE BLOCKING ❌{Colors.RESET}'
|
|
218
|
-
print(f'{Colors.RED}══════════════════════════════════════{Colors.RESET}'
|
|
219
|
-
print(f'{Colors.RED}Fix EVERYTHING above until all checks are ✅ GREEN{Colors.RESET}'
|
|
214
|
+
print(f'{Colors.RED}❌{Colors.RESET} {error}')
|
|
215
|
+
print(f'\n{Colors.RED}Found {len(errors)} issue(s) that MUST be fixed!{Colors.RESET}')
|
|
216
|
+
print(f'{Colors.RED}══════════════════════════════════════{Colors.RESET}')
|
|
217
|
+
print(f'{Colors.RED}❌ ALL ISSUES ARE BLOCKING ❌{Colors.RESET}')
|
|
218
|
+
print(f'{Colors.RED}══════════════════════════════════════{Colors.RESET}')
|
|
219
|
+
print(f'{Colors.RED}Fix EVERYTHING above until all checks are ✅ GREEN{Colors.RESET}')
|
|
220
220
|
|
|
221
221
|
def parse_json_input() -> dict:
|
|
222
222
|
"""Parse JSON input from stdin"""
|
|
@@ -224,7 +224,7 @@ def parse_json_input() -> dict:
|
|
|
224
224
|
|
|
225
225
|
if not input_data:
|
|
226
226
|
log_warning('No JSON input provided.')
|
|
227
|
-
print(f'\n{Colors.YELLOW}👉 Hook executed but no input to process.{Colors.RESET}'
|
|
227
|
+
print(f'\n{Colors.YELLOW}👉 Hook executed but no input to process.{Colors.RESET}')
|
|
228
228
|
sys.exit(0)
|
|
229
229
|
|
|
230
230
|
try:
|
|
@@ -253,9 +253,9 @@ def extract_file_path(input_data: dict) -> str | None:
|
|
|
253
253
|
|
|
254
254
|
def main():
|
|
255
255
|
"""Main entry point"""
|
|
256
|
-
print(''
|
|
257
|
-
print(f'📦 Python Quality Check - Starting...'
|
|
258
|
-
print('─────────────────────────────────────'
|
|
256
|
+
print('')
|
|
257
|
+
print(f'📦 Python Quality Check - Starting...')
|
|
258
|
+
print('─────────────────────────────────────')
|
|
259
259
|
|
|
260
260
|
# Parse input
|
|
261
261
|
input_data = parse_json_input()
|
|
@@ -263,25 +263,25 @@ def main():
|
|
|
263
263
|
|
|
264
264
|
if not file_path:
|
|
265
265
|
log_warning('No file path found in JSON input.')
|
|
266
|
-
print(f'\n{Colors.YELLOW}👉 No file to check - tool may not be file-related.{Colors.RESET}'
|
|
266
|
+
print(f'\n{Colors.YELLOW}👉 No file to check - tool may not be file-related.{Colors.RESET}')
|
|
267
267
|
sys.exit(0)
|
|
268
268
|
|
|
269
269
|
# Check if file exists
|
|
270
270
|
if not Path(file_path).exists():
|
|
271
271
|
log_info(f'File does not exist: {file_path}')
|
|
272
|
-
print(f'\n{Colors.YELLOW}👉 File skipped - doesn\'t exist.{Colors.RESET}'
|
|
272
|
+
print(f'\n{Colors.YELLOW}👉 File skipped - doesn\'t exist.{Colors.RESET}')
|
|
273
273
|
sys.exit(0)
|
|
274
274
|
|
|
275
275
|
# Skip non-Python files
|
|
276
276
|
if not is_python_file(file_path):
|
|
277
277
|
log_info(f'Skipping non-Python file: {file_path}')
|
|
278
|
-
print(f'\n{Colors.GREEN}✅ No checks needed for {os.path.basename(file_path)}{Colors.RESET}'
|
|
278
|
+
print(f'\n{Colors.GREEN}✅ No checks needed for {os.path.basename(file_path)}{Colors.RESET}')
|
|
279
279
|
sys.exit(0)
|
|
280
280
|
|
|
281
281
|
# Update header
|
|
282
|
-
print(''
|
|
283
|
-
print(f'🔍 Validating: {os.path.basename(file_path)}'
|
|
284
|
-
print('─────────────────────────────────────'
|
|
282
|
+
print('')
|
|
283
|
+
print(f'🔍 Validating: {os.path.basename(file_path)}')
|
|
284
|
+
print('─────────────────────────────────────')
|
|
285
285
|
log_info(f'Checking: {file_path}')
|
|
286
286
|
|
|
287
287
|
# Find project root
|
|
@@ -290,7 +290,7 @@ def main():
|
|
|
290
290
|
|
|
291
291
|
if not has_python_project_config(project_root):
|
|
292
292
|
log_info('No pyproject.toml or .python-version found - skipping Python quality checks')
|
|
293
|
-
print(f'\n{Colors.GREEN}✅ No Python project config detected; skipping checks for {os.path.basename(file_path)}{Colors.RESET}'
|
|
293
|
+
print(f'\n{Colors.GREEN}✅ No Python project config detected; skipping checks for {os.path.basename(file_path)}{Colors.RESET}')
|
|
294
294
|
sys.exit(0)
|
|
295
295
|
|
|
296
296
|
# Get config from environment
|
|
@@ -317,20 +317,20 @@ def main():
|
|
|
317
317
|
|
|
318
318
|
# Exit with appropriate code
|
|
319
319
|
if all_errors:
|
|
320
|
-
print(f'\n{Colors.RED}🛑 FAILED - Fix issues in your edited file! 🛑{Colors.RESET}'
|
|
321
|
-
print(f'{Colors.CYAN}💡 CLAUDE.md CHECK:{Colors.RESET}'
|
|
322
|
-
print(f'{Colors.CYAN} → What CLAUDE.md pattern would have prevented this?{Colors.RESET}'
|
|
323
|
-
print(f'{Colors.YELLOW}📋 NEXT STEPS:{Colors.RESET}'
|
|
324
|
-
print(f'{Colors.YELLOW} 1. Fix the issues listed above{Colors.RESET}'
|
|
325
|
-
print(f'{Colors.YELLOW} 2. The hook will run again automatically{Colors.RESET}'
|
|
326
|
-
print(f'{Colors.YELLOW} 3. Continue once all checks pass{Colors.RESET}'
|
|
320
|
+
print(f'\n{Colors.RED}🛑 FAILED - Fix issues in your edited file! 🛑{Colors.RESET}')
|
|
321
|
+
print(f'{Colors.CYAN}💡 CLAUDE.md CHECK:{Colors.RESET}')
|
|
322
|
+
print(f'{Colors.CYAN} → What CLAUDE.md pattern would have prevented this?{Colors.RESET}')
|
|
323
|
+
print(f'{Colors.YELLOW}📋 NEXT STEPS:{Colors.RESET}')
|
|
324
|
+
print(f'{Colors.YELLOW} 1. Fix the issues listed above{Colors.RESET}')
|
|
325
|
+
print(f'{Colors.YELLOW} 2. The hook will run again automatically{Colors.RESET}')
|
|
326
|
+
print(f'{Colors.YELLOW} 3. Continue once all checks pass{Colors.RESET}')
|
|
327
327
|
sys.exit(2)
|
|
328
328
|
else:
|
|
329
|
-
print(f'\n{Colors.GREEN}✅ Quality check passed for {os.path.basename(file_path)}{Colors.RESET}'
|
|
329
|
+
print(f'\n{Colors.GREEN}✅ Quality check passed for {os.path.basename(file_path)}{Colors.RESET}')
|
|
330
330
|
if all_autofixes:
|
|
331
|
-
print(f'\n{Colors.YELLOW}👉 File quality verified. Auto-fixes applied. Continue with your task.{Colors.RESET}'
|
|
331
|
+
print(f'\n{Colors.YELLOW}👉 File quality verified. Auto-fixes applied. Continue with your task.{Colors.RESET}')
|
|
332
332
|
else:
|
|
333
|
-
print(f'\n{Colors.YELLOW}👉 File quality verified. Continue with your task.{Colors.RESET}'
|
|
333
|
+
print(f'\n{Colors.YELLOW}👉 File quality verified. Continue with your task.{Colors.RESET}')
|
|
334
334
|
|
|
335
335
|
# Suggest tests
|
|
336
336
|
check_pytest_suggestions(file_path, project_root)
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// xtrm-logger.mjs — shared event logger for xtrm hook and bd lifecycle events
|
|
3
|
+
//
|
|
4
|
+
// Writes to the xtrm_events table in the project's beads Dolt DB.
|
|
5
|
+
// Self-initializing: creates the table on first write if it doesn't exist.
|
|
6
|
+
// Fails completely silently — logging NEVER affects hook behavior.
|
|
7
|
+
//
|
|
8
|
+
// Usage (from any hook):
|
|
9
|
+
// import { logEvent } from './xtrm-logger.mjs';
|
|
10
|
+
// logEvent({ cwd, runtime: 'claude', sessionId, layer: 'gate', kind: 'hook.edit_gate.block',
|
|
11
|
+
// outcome: 'block', toolName, issueId, message, extra });
|
|
12
|
+
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
|
|
16
|
+
// ── Schema ────────────────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const CREATE_SQL = `CREATE TABLE IF NOT EXISTS xtrm_events (
|
|
19
|
+
id VARCHAR(36) NOT NULL,
|
|
20
|
+
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
21
|
+
runtime VARCHAR(16) NOT NULL,
|
|
22
|
+
session_id VARCHAR(255) NOT NULL,
|
|
23
|
+
worktree VARCHAR(255) DEFAULT NULL,
|
|
24
|
+
layer VARCHAR(16) NOT NULL,
|
|
25
|
+
kind VARCHAR(64) NOT NULL,
|
|
26
|
+
outcome VARCHAR(8) NOT NULL,
|
|
27
|
+
tool_name VARCHAR(255) DEFAULT NULL,
|
|
28
|
+
issue_id VARCHAR(255) DEFAULT NULL,
|
|
29
|
+
message TEXT DEFAULT NULL,
|
|
30
|
+
extra JSON DEFAULT NULL,
|
|
31
|
+
PRIMARY KEY (id),
|
|
32
|
+
INDEX idx_session (session_id(64)),
|
|
33
|
+
INDEX idx_kind (kind),
|
|
34
|
+
INDEX idx_created (created_at)
|
|
35
|
+
)`;
|
|
36
|
+
|
|
37
|
+
// ── SQL helpers ───────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Escape a value for use in a MySQL single-quoted string literal.
|
|
41
|
+
* Returns the quoted string, or NULL for null/undefined.
|
|
42
|
+
*/
|
|
43
|
+
function sqlEscape(val) {
|
|
44
|
+
if (val === null || val === undefined) return 'NULL';
|
|
45
|
+
const str = String(val)
|
|
46
|
+
.replace(/\\/g, '\\\\') // backslash first
|
|
47
|
+
.replace(/'/g, "''") // single-quote → doubled
|
|
48
|
+
.replace(/\0/g, ''); // strip null bytes (invalid in utf8 strings)
|
|
49
|
+
return `'${str}'`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function bdSql(sql, cwd) {
|
|
53
|
+
return spawnSync('bd', ['sql', sql], {
|
|
54
|
+
cwd,
|
|
55
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
56
|
+
encoding: 'utf8',
|
|
57
|
+
timeout: 5000,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Log an xtrm event to the beads xtrm_events table.
|
|
65
|
+
*
|
|
66
|
+
* @param {object} params
|
|
67
|
+
* @param {string} params.cwd Project working directory (required)
|
|
68
|
+
* @param {string} params.runtime 'claude' | 'pi'
|
|
69
|
+
* @param {string} params.sessionId Claude session UUID or Pi PID string
|
|
70
|
+
* @param {string} params.layer 'gate' | 'bd'
|
|
71
|
+
* @param {string} params.kind e.g. 'hook.edit_gate.block', 'bd.claimed'
|
|
72
|
+
* @param {string} params.outcome 'allow' | 'block'
|
|
73
|
+
* @param {string} [params.toolName] Tool intercepted (gates)
|
|
74
|
+
* @param {string} [params.issueId] Linked beads issue ID
|
|
75
|
+
* @param {string} [params.message] Full message sent to agent (blocks)
|
|
76
|
+
* @param {object} [params.extra] Additional structured data {file, cwd, reason_code, ...}
|
|
77
|
+
*
|
|
78
|
+
* @returns {string|null} The event UUID, or null if logging failed
|
|
79
|
+
*/
|
|
80
|
+
export function logEvent(params) {
|
|
81
|
+
try {
|
|
82
|
+
const { cwd, runtime, sessionId, layer, kind, outcome } = params;
|
|
83
|
+
if (!cwd || !runtime || !sessionId || !layer || !kind || !outcome) return null;
|
|
84
|
+
|
|
85
|
+
const { toolName, issueId, message, extra } = params;
|
|
86
|
+
|
|
87
|
+
// Derive worktree name if cwd is inside .xtrm/worktrees/<name>
|
|
88
|
+
const worktreeMatch = cwd.match(/\.xtrm\/worktrees\/([^/]+)/);
|
|
89
|
+
const worktree = worktreeMatch ? worktreeMatch[1] : null;
|
|
90
|
+
|
|
91
|
+
const id = randomUUID();
|
|
92
|
+
const extraJson = extra ? JSON.stringify(extra) : null;
|
|
93
|
+
|
|
94
|
+
const cols = 'id, runtime, session_id, worktree, layer, kind, outcome, tool_name, issue_id, message, extra';
|
|
95
|
+
const vals = [
|
|
96
|
+
sqlEscape(id),
|
|
97
|
+
sqlEscape(runtime),
|
|
98
|
+
sqlEscape(sessionId),
|
|
99
|
+
sqlEscape(worktree),
|
|
100
|
+
sqlEscape(layer),
|
|
101
|
+
sqlEscape(kind),
|
|
102
|
+
sqlEscape(outcome),
|
|
103
|
+
sqlEscape(toolName ?? null),
|
|
104
|
+
sqlEscape(issueId ?? null),
|
|
105
|
+
sqlEscape(message ?? null),
|
|
106
|
+
sqlEscape(extraJson),
|
|
107
|
+
].join(', ');
|
|
108
|
+
|
|
109
|
+
const insertSql = `INSERT INTO xtrm_events (${cols}) VALUES (${vals})`;
|
|
110
|
+
|
|
111
|
+
let result = bdSql(insertSql, cwd);
|
|
112
|
+
if (result.status !== 0) {
|
|
113
|
+
// Table may not exist yet — create it and retry once (self-initializing)
|
|
114
|
+
bdSql(CREATE_SQL, cwd);
|
|
115
|
+
result = bdSql(insertSql, cwd);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return result.status === 0 ? id : null;
|
|
119
|
+
} catch {
|
|
120
|
+
// Silently swallow all errors — logging never affects hook behavior
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
}
|