xtrm-tools 0.7.13 → 0.7.14

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.
@@ -41,6 +41,16 @@
41
41
  }
42
42
  ]
43
43
  },
44
+ {
45
+ "matcher": "Agent",
46
+ "hooks": [
47
+ {
48
+ "type": "command",
49
+ "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/specialists-agent-guard.mjs",
50
+ "timeout": 2000
51
+ }
52
+ ]
53
+ },
44
54
  {
45
55
  "matcher": "Bash",
46
56
  "hooks": [
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env node
2
+ // specialists-agent-guard — Claude Code PreToolUse hook
3
+ // Blocks raw Agent tool usage only when a specialists workflow skill is active.
4
+ // Fail-open unless the active transcript/system prompt clearly contains using-specialists.
5
+
6
+ import { readFileSync, existsSync, statSync } from 'node:fs';
7
+ import { resolve } from 'node:path';
8
+ import { logEvent } from './xtrm-logger.mjs';
9
+
10
+ function readJsonStdin() {
11
+ try {
12
+ return JSON.parse(readFileSync(0, 'utf8'));
13
+ } catch {
14
+ return null;
15
+ }
16
+ }
17
+
18
+ function tailText(filePath, maxBytes = 256 * 1024) {
19
+ try {
20
+ if (!filePath || !existsSync(filePath)) return '';
21
+ const stat = statSync(filePath);
22
+ const start = Math.max(0, stat.size - maxBytes);
23
+ const raw = readFileSync(filePath);
24
+ return raw.subarray(start).toString('utf8');
25
+ } catch {
26
+ return '';
27
+ }
28
+ }
29
+
30
+ function hasSpecialistsSkillMarker(text) {
31
+ return /<skill\s+name=["']using-specialists(?:-v2)?["']/i.test(text)
32
+ || /name:\s*using-specialists(?:-v2)?\b/i.test(text)
33
+ || /#\s*Specialists V2\b/i.test(text)
34
+ || /#\s*Specialists Usage\b/i.test(text);
35
+ }
36
+
37
+ function isSpecialistsWorkflowActive(input) {
38
+ const directText = [
39
+ input?.system_prompt,
40
+ input?.systemPrompt,
41
+ input?.prompt,
42
+ input?.message,
43
+ ].filter(Boolean).join('\n');
44
+
45
+ if (hasSpecialistsSkillMarker(directText)) return true;
46
+
47
+ const transcriptPath = input?.transcript_path ?? input?.transcriptPath;
48
+ return hasSpecialistsSkillMarker(tailText(transcriptPath));
49
+ }
50
+
51
+ const input = readJsonStdin();
52
+ if (!input) process.exit(0);
53
+
54
+ const toolName = input.tool_name ?? input.toolName ?? '';
55
+ if (toolName !== 'Agent') process.exit(0);
56
+ if (!isSpecialistsWorkflowActive(input)) process.exit(0);
57
+
58
+ const cwd = input.cwd ?? process.env.CLAUDE_PROJECT_DIR ?? process.cwd();
59
+ const sessionId = input.session_id ?? input.sessionId ?? null;
60
+ const reason = 'Use specialists CLI instead of Agent tool. Route via: specialists run <name> --bead <id>';
61
+
62
+ try {
63
+ logEvent({
64
+ cwd: resolve(cwd),
65
+ runtime: 'claude',
66
+ sessionId,
67
+ layer: 'gate',
68
+ kind: 'gate.specialists_agent.block',
69
+ outcome: 'block',
70
+ toolName: 'Agent',
71
+ message: reason,
72
+ });
73
+ } catch { /* fail closed for the Agent tool, but ignore logging failures */ }
74
+
75
+ process.stdout.write(JSON.stringify({ decision: 'block', reason }) + '\n');
76
+ process.exit(0);