xtrm-tools 2.1.13 → 2.1.16
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 +40 -0
- package/cli/dist/index.cjs.map +1 -1
- package/cli/package.json +1 -1
- package/config/hooks.json +9 -5
- package/config/mcp_servers.json +11 -0
- package/hooks/README.md +72 -0
- package/hooks/beads-commit-gate.mjs +20 -46
- package/hooks/beads-edit-gate.mjs +19 -53
- package/hooks/beads-gate-core.mjs +209 -0
- package/hooks/beads-gate-messages.mjs +92 -0
- package/hooks/beads-memory-gate.mjs +9 -18
- package/hooks/beads-stop-gate.mjs +17 -46
- package/hooks/gitnexus/gitnexus-hook.cjs +222 -133
- package/hooks/main-guard.mjs +20 -0
- package/package.json +1 -1
- package/skills/using-xtrm/SKILL.md +271 -0
|
@@ -6,57 +6,28 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Installed by: xtrm install
|
|
8
8
|
|
|
9
|
-
import { readFileSync } from 'node:fs';
|
|
10
9
|
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
readHookInput,
|
|
11
|
+
resolveSessionContext,
|
|
12
|
+
resolveClaimAndWorkState,
|
|
13
|
+
decideStopGate,
|
|
14
|
+
} from './beads-gate-core.mjs';
|
|
15
|
+
import { withSafeBdContext } from './beads-gate-utils.mjs';
|
|
16
|
+
import { stopBlockMessage } from './beads-gate-messages.mjs';
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
input = JSON.parse(readFileSync(0, 'utf8'));
|
|
18
|
-
} catch {
|
|
19
|
-
process.exit(0);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const CLOSE_PROTOCOL =
|
|
23
|
-
' 3. bd close <id1> <id2> ... close all in_progress issues\n' +
|
|
24
|
-
' 4. git add <files> && git commit -m "..." commit your changes\n' +
|
|
25
|
-
' 5. git push -u origin <feature-branch> push feature branch\n' +
|
|
26
|
-
' 6. gh pr create --fill create PR\n' +
|
|
27
|
-
' 7. gh pr merge --squash merge PR\n' +
|
|
28
|
-
' 8. git checkout master && git reset --hard origin/master\n';
|
|
18
|
+
const input = readHookInput();
|
|
19
|
+
if (!input) process.exit(0);
|
|
29
20
|
|
|
30
21
|
withSafeBdContext(() => {
|
|
31
|
-
const
|
|
32
|
-
if (!isBeadsProject
|
|
33
|
-
|
|
34
|
-
const sessionId = input.session_id;
|
|
35
|
-
|
|
36
|
-
if (sessionId) {
|
|
37
|
-
const claimed = getSessionClaim(sessionId, cwd);
|
|
38
|
-
if (claimed === null) process.exit(0); // bd kv unavailable — fail open
|
|
39
|
-
if (!claimed) process.exit(0); // no active claim for this session
|
|
22
|
+
const ctx = resolveSessionContext(input);
|
|
23
|
+
if (!ctx || !ctx.isBeadsProject) process.exit(0);
|
|
40
24
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const summary = ip?.summary ?? ` Claimed: ${claimed}`;
|
|
25
|
+
const state = resolveClaimAndWorkState(ctx);
|
|
26
|
+
const decision = decideStopGate(ctx, state);
|
|
44
27
|
|
|
45
|
-
|
|
46
|
-
'🚫 BEADS STOP GATE: Unresolved issues — complete the session close protocol.\n\n' +
|
|
47
|
-
`Open issues:\n${summary}\n\n` +
|
|
48
|
-
'Session close protocol:\n' + CLOSE_PROTOCOL
|
|
49
|
-
);
|
|
50
|
-
process.exit(2);
|
|
51
|
-
} else {
|
|
52
|
-
const ip = getInProgress(cwd);
|
|
53
|
-
if (ip === null || ip.count === 0) process.exit(0);
|
|
28
|
+
if (decision.allow) process.exit(0);
|
|
54
29
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'Session close protocol:\n' + CLOSE_PROTOCOL
|
|
59
|
-
);
|
|
60
|
-
process.exit(2);
|
|
61
|
-
}
|
|
30
|
+
// Block with message
|
|
31
|
+
process.stderr.write(stopBlockMessage(decision.summary, decision.claimed));
|
|
32
|
+
process.exit(2);
|
|
62
33
|
});
|
|
@@ -1,133 +1,222 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* GitNexus Claude Code Hook
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GitNexus Claude Code Hook — PostToolUse enrichment
|
|
4
|
+
*
|
|
5
|
+
* Fires AFTER Read/Grep/Glob/Bash/Serena tools complete.
|
|
6
|
+
* Extracts patterns from both tool input AND output, runs
|
|
7
|
+
* `gitnexus augment <pattern>`, and injects a [GitNexus: ...]
|
|
8
|
+
* block into Claude's context — mirroring pi-gitnexus behavior.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - PostToolUse (vs old PreToolUse) — enriches actual result
|
|
12
|
+
* - Session-keyed dedup cache (/tmp) — no redundant lookups
|
|
13
|
+
* - Pattern extraction from output content (grep-style scanning)
|
|
14
|
+
* - Serena tool support — extracts symbol names from name_path
|
|
15
|
+
* - Graceful failure — always exits 0
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const os = require('os');
|
|
23
|
+
const { spawnSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const CODE_EXTS = new Set([
|
|
26
|
+
'.ts', '.tsx', '.js', '.mjs', '.cjs', '.py', '.go', '.rs',
|
|
27
|
+
'.java', '.kt', '.cpp', '.c', '.h', '.rb', '.php', '.cs',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
const SERENA_SYMBOL_TOOLS = new Set([
|
|
31
|
+
'mcp__serena__find_symbol',
|
|
32
|
+
'mcp__serena__find_referencing_symbols',
|
|
33
|
+
'mcp__serena__replace_symbol_body',
|
|
34
|
+
'mcp__serena__insert_after_symbol',
|
|
35
|
+
'mcp__serena__insert_before_symbol',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
const SERENA_FILE_TOOLS = new Set([
|
|
39
|
+
'mcp__serena__get_symbols_overview',
|
|
40
|
+
'mcp__serena__search_for_pattern',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
const SERENA_RENAME = 'mcp__serena__rename_symbol';
|
|
44
|
+
|
|
45
|
+
function readInput() {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(fs.readFileSync(0, 'utf-8'));
|
|
48
|
+
} catch {
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function findGitNexusIndex(startDir) {
|
|
54
|
+
let dir = startDir || process.cwd();
|
|
55
|
+
for (let i = 0; i < 6; i++) {
|
|
56
|
+
if (fs.existsSync(path.join(dir, '.gitnexus'))) return true;
|
|
57
|
+
const parent = path.dirname(dir);
|
|
58
|
+
if (parent === dir) break;
|
|
59
|
+
dir = parent;
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getCacheFile(sessionId) {
|
|
65
|
+
const id = sessionId ? sessionId.replace(/[^a-zA-Z0-9_-]/g, '_') : 'default';
|
|
66
|
+
return path.join(os.tmpdir(), `gitnexus-aug-${id}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function loadCache(cacheFile) {
|
|
70
|
+
try {
|
|
71
|
+
return new Set(fs.readFileSync(cacheFile, 'utf-8').split('\n').filter(Boolean));
|
|
72
|
+
} catch {
|
|
73
|
+
return new Set();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function saveToCache(cacheFile, pattern) {
|
|
78
|
+
try {
|
|
79
|
+
fs.appendFileSync(cacheFile, pattern + '\n');
|
|
80
|
+
} catch { /* graceful */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function symbolFromNamePath(namePath) {
|
|
84
|
+
if (!namePath) return null;
|
|
85
|
+
const parts = namePath.split('/').filter(Boolean);
|
|
86
|
+
const last = parts[parts.length - 1];
|
|
87
|
+
return last ? last.replace(/\[\d+\]$/, '') : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function symbolFromFilePath(filePath) {
|
|
91
|
+
if (!filePath) return null;
|
|
92
|
+
const ext = path.extname(filePath);
|
|
93
|
+
if (!CODE_EXTS.has(ext)) return null;
|
|
94
|
+
return path.basename(filePath, ext);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function extractFilePatternsFromOutput(content) {
|
|
98
|
+
if (!content || typeof content !== 'string') return [];
|
|
99
|
+
const patterns = new Set();
|
|
100
|
+
const lines = content.split('\n').slice(0, 50);
|
|
101
|
+
for (const line of lines) {
|
|
102
|
+
const m = line.match(/^([^\s:]+\.[a-z]{1,5}):\d+:/);
|
|
103
|
+
if (m) {
|
|
104
|
+
const ext = path.extname(m[1]);
|
|
105
|
+
if (CODE_EXTS.has(ext)) patterns.add(path.basename(m[1], ext));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return [...patterns];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractPatterns(toolName, toolInput, toolResponse) {
|
|
112
|
+
const patterns = [];
|
|
113
|
+
const content = typeof toolResponse === 'string'
|
|
114
|
+
? toolResponse
|
|
115
|
+
: (toolResponse?.content ?? toolResponse?.output ?? '');
|
|
116
|
+
|
|
117
|
+
if (toolName === 'Read') {
|
|
118
|
+
const sym = symbolFromFilePath(toolInput.file_path);
|
|
119
|
+
if (sym) patterns.push(sym);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (toolName === 'Grep') {
|
|
123
|
+
const raw = toolInput.pattern || '';
|
|
124
|
+
const cleaned = raw.replace(/[.*+?^${}()|[\]\\]/g, '').trim();
|
|
125
|
+
if (cleaned.length >= 3) patterns.push(cleaned);
|
|
126
|
+
patterns.push(...extractFilePatternsFromOutput(content));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (toolName === 'Glob') {
|
|
130
|
+
const raw = toolInput.pattern || '';
|
|
131
|
+
const m = raw.match(/([a-zA-Z][a-zA-Z0-9_-]{2,})/);
|
|
132
|
+
if (m) patterns.push(m[1]);
|
|
133
|
+
patterns.push(...extractFilePatternsFromOutput(
|
|
134
|
+
Array.isArray(toolResponse) ? toolResponse.join('\n') : String(toolResponse || '')
|
|
135
|
+
));
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (toolName === 'Bash') {
|
|
139
|
+
const cmd = toolInput.command || '';
|
|
140
|
+
if (!/\brg\b|\bgrep\b/.test(cmd)) return [];
|
|
141
|
+
const tokens = cmd.split(/\s+/);
|
|
142
|
+
let foundCmd = false, skipNext = false;
|
|
143
|
+
const flagsWithValues = new Set(['-e','-f','-m','-A','-B','-C','-g','--glob','-t','--type','--include','--exclude']);
|
|
144
|
+
for (const token of tokens) {
|
|
145
|
+
if (skipNext) { skipNext = false; continue; }
|
|
146
|
+
if (!foundCmd) { if (/\brg$|\bgrep$/.test(token)) foundCmd = true; continue; }
|
|
147
|
+
if (token.startsWith('-')) { if (flagsWithValues.has(token)) skipNext = true; continue; }
|
|
148
|
+
const cleaned = token.replace(/['"]/g, '').replace(/[.*+?^${}()|[\]\\]/g, '').trim();
|
|
149
|
+
if (cleaned.length >= 3) { patterns.push(cleaned); break; }
|
|
150
|
+
}
|
|
151
|
+
patterns.push(...extractFilePatternsFromOutput(content));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (SERENA_SYMBOL_TOOLS.has(toolName)) {
|
|
155
|
+
const sym = symbolFromNamePath(toolInput.name_path_pattern || toolInput.name_path);
|
|
156
|
+
if (sym) patterns.push(sym);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (toolName === SERENA_RENAME) {
|
|
160
|
+
if (toolInput.symbol_name) patterns.push(toolInput.symbol_name);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (SERENA_FILE_TOOLS.has(toolName)) {
|
|
164
|
+
const sym = symbolFromFilePath(toolInput.relative_path || toolInput.file_path);
|
|
165
|
+
if (sym) patterns.push(sym);
|
|
166
|
+
const sub = toolInput.substring || toolInput.pattern || '';
|
|
167
|
+
const cleaned = sub.replace(/[.*+?^${}()|[\]\\]/g, '').trim();
|
|
168
|
+
if (cleaned.length >= 3) patterns.push(cleaned);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return [...new Set(patterns.filter(p => p && p.length >= 3))];
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function runAugment(pattern, cwd) {
|
|
175
|
+
try {
|
|
176
|
+
const child = spawnSync('gitnexus', ['augment', pattern], {
|
|
177
|
+
encoding: 'utf-8',
|
|
178
|
+
timeout: 8000,
|
|
179
|
+
cwd,
|
|
180
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
181
|
+
});
|
|
182
|
+
return (child.stderr || '').trim();
|
|
183
|
+
} catch {
|
|
184
|
+
return '';
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function main() {
|
|
189
|
+
try {
|
|
190
|
+
const input = readInput();
|
|
191
|
+
if (input.hook_event_name !== 'PostToolUse') return;
|
|
192
|
+
|
|
193
|
+
const cwd = input.cwd || process.cwd();
|
|
194
|
+
if (!findGitNexusIndex(cwd)) return;
|
|
195
|
+
|
|
196
|
+
const toolName = input.tool_name || '';
|
|
197
|
+
const toolInput = input.tool_input || {};
|
|
198
|
+
const toolResponse = input.tool_response ?? input.tool_result ?? '';
|
|
199
|
+
|
|
200
|
+
const patterns = extractPatterns(toolName, toolInput, toolResponse);
|
|
201
|
+
if (patterns.length === 0) return;
|
|
202
|
+
|
|
203
|
+
const cacheFile = getCacheFile(input.session_id);
|
|
204
|
+
const cache = loadCache(cacheFile);
|
|
205
|
+
|
|
206
|
+
const results = [];
|
|
207
|
+
for (const pattern of patterns) {
|
|
208
|
+
if (cache.has(pattern)) continue;
|
|
209
|
+
const out = runAugment(pattern, cwd);
|
|
210
|
+
saveToCache(cacheFile, pattern);
|
|
211
|
+
if (out) results.push(out);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (results.length > 0) {
|
|
215
|
+
process.stdout.write('[GitNexus]\n' + results.join('\n\n') + '\n');
|
|
216
|
+
}
|
|
217
|
+
} catch (err) {
|
|
218
|
+
process.stderr.write('GitNexus hook error: ' + err.message + '\n');
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main();
|
package/hooks/main-guard.mjs
CHANGED
|
@@ -86,6 +86,26 @@ if (tool === 'Bash') {
|
|
|
86
86
|
process.exit(0);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Enforce squash-only PR merges for linear history
|
|
90
|
+
// Must check BEFORE the gh allowlist pattern
|
|
91
|
+
if (/^gh\s+pr\s+merge\b/.test(cmd)) {
|
|
92
|
+
if (!/--squash\b/.test(cmd)) {
|
|
93
|
+
deny(
|
|
94
|
+
`\u26D4 Only squash merges are allowed — use 'gh pr merge --squash'\n\n` +
|
|
95
|
+
'Why squash?\n' +
|
|
96
|
+
' - Keeps history linear and easy to read\n' +
|
|
97
|
+
' - One commit per PR = easy to revert\n' +
|
|
98
|
+
' - Matches the workflow documented in AGENTS.md\n\n' +
|
|
99
|
+
'Correct usage:\n' +
|
|
100
|
+
' gh pr merge --squash\n\n' +
|
|
101
|
+
'If you really need a merge commit, use:\n' +
|
|
102
|
+
' MAIN_GUARD_ALLOW_BASH=1 gh pr merge --merge\n'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
// --squash present — allow
|
|
106
|
+
process.exit(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
89
109
|
// Safe allowlist — non-mutating commands + explicit branch-exit paths.
|
|
90
110
|
// Important: do not allow generic checkout/switch forms, which include
|
|
91
111
|
// mutating variants such as `git checkout -- <path>`.
|