xtrm-tools 2.1.4 → 2.1.6
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/README.md +10 -4
- package/cli/dist/index.cjs +1562 -1392
- package/cli/dist/index.cjs.map +1 -1
- package/config/hooks.json +4 -4
- package/hooks/README.md +3 -3
- package/hooks/main-guard.mjs +10 -1
- package/package.json +8 -3
- package/project-skills/py-quality-gate/.claude/hooks/quality-check.py +15 -2
- package/project-skills/py-quality-gate/.claude/settings.json +1 -1
- package/project-skills/service-skills-set/.claude/settings.json +2 -2
- package/project-skills/service-skills-set/install-service-skills.py +43 -13
- package/project-skills/tdd-guard/.claude/hooks/tdd-guard-pretool-bridge.cjs +87 -0
- package/project-skills/tdd-guard/.claude/settings.json +2 -2
- package/project-skills/tdd-guard/README.md +6 -4
- package/project-skills/tdd-guard/docs/linting.md +2 -2
- package/project-skills/tdd-guard/reporters/jest/src/JestReporter.test-data.ts +199 -0
- package/project-skills/tdd-guard/reporters/jest/src/JestReporter.test.ts +302 -0
- package/project-skills/tdd-guard/reporters/jest/src/JestReporter.ts +201 -0
- package/project-skills/tdd-guard/reporters/jest/src/index.ts +4 -0
- package/project-skills/tdd-guard/reporters/jest/src/types.ts +42 -0
- package/project-skills/tdd-guard/reporters/jest/tsconfig.json +11 -0
- package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.test-data.ts +85 -0
- package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.test.ts +446 -0
- package/project-skills/tdd-guard/reporters/vitest/src/VitestReporter.ts +110 -0
- package/project-skills/tdd-guard/reporters/vitest/src/index.ts +4 -0
- package/project-skills/tdd-guard/reporters/vitest/src/types.ts +39 -0
- package/project-skills/tdd-guard/reporters/vitest/tsconfig.json +11 -0
- package/project-skills/ts-quality-gate/.claude/hooks/quality-check.cjs +36 -1
- package/project-skills/ts-quality-gate/.claude/settings.json +1 -1
package/config/hooks.json
CHANGED
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
],
|
|
14
14
|
"PreToolUse": [
|
|
15
15
|
{
|
|
16
|
-
"matcher": "Write|Edit|MultiEdit",
|
|
16
|
+
"matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
17
17
|
"script": "main-guard.mjs",
|
|
18
18
|
"timeout": 5000
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
|
-
"matcher": "Read|Edit",
|
|
21
|
+
"matcher": "Read|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
22
22
|
"script": "serena-workflow-reminder.py"
|
|
23
23
|
},
|
|
24
24
|
{
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"timeout": 30000
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
|
-
"matcher": "Edit|Write",
|
|
30
|
+
"matcher": "Edit|Write|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
31
31
|
"script": "type-safety-enforcement.py"
|
|
32
32
|
},
|
|
33
33
|
{
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"timeout": 8000
|
|
37
37
|
},
|
|
38
38
|
{
|
|
39
|
-
"matcher": "Edit|Write|MultiEdit|NotebookEdit|
|
|
39
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
40
40
|
"script": "beads-edit-gate.mjs",
|
|
41
41
|
"timeout": 5000
|
|
42
42
|
},
|
package/hooks/README.md
CHANGED
|
@@ -147,7 +147,7 @@ Installed globally to `~/.claude/hooks/` by `xtrm install`. Require Node.js.
|
|
|
147
147
|
|
|
148
148
|
**Purpose**: Blocks direct file edits and dangerous git operations on protected branches (`main`/`master`). Enforces the feature-branch → PR workflow.
|
|
149
149
|
|
|
150
|
-
**Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|Bash`)
|
|
150
|
+
**Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|Bash`)
|
|
151
151
|
|
|
152
152
|
**Blocks**:
|
|
153
153
|
- Write/Edit/MultiEdit/NotebookEdit on protected branches
|
|
@@ -159,7 +159,7 @@ Installed globally to `~/.claude/hooks/` by `xtrm install`. Require Node.js.
|
|
|
159
159
|
{
|
|
160
160
|
"hooks": {
|
|
161
161
|
"PreToolUse": [{
|
|
162
|
-
"matcher": "Edit|Write|MultiEdit|NotebookEdit|Bash",
|
|
162
|
+
"matcher": "Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol|Bash",
|
|
163
163
|
"hooks": [{ "type": "command", "command": "node \"~/.claude/hooks/main-guard.mjs\"", "timeout": 5000 }]
|
|
164
164
|
}]
|
|
165
165
|
}
|
|
@@ -182,7 +182,7 @@ Installed globally to `~/.claude/hooks/` by `xtrm install`. Require Node.js.
|
|
|
182
182
|
|
|
183
183
|
**Purpose**: Blocks file edits when the current session has not claimed a beads issue via `bd kv`. Prevents free-riding in multi-agent and multi-session scenarios.
|
|
184
184
|
|
|
185
|
-
**Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|
|
|
185
|
+
**Trigger**: PreToolUse (`Edit|Write|MultiEdit|NotebookEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol`)
|
|
186
186
|
|
|
187
187
|
**Behavior**:
|
|
188
188
|
- Session has claim (`bd kv get "claimed:<session_id>"`) → allow
|
package/hooks/main-guard.mjs
CHANGED
|
@@ -43,7 +43,16 @@ function deny(reason) {
|
|
|
43
43
|
process.exit(2);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
const WRITE_TOOLS = new Set([
|
|
46
|
+
const WRITE_TOOLS = new Set([
|
|
47
|
+
'Edit',
|
|
48
|
+
'Write',
|
|
49
|
+
'MultiEdit',
|
|
50
|
+
'NotebookEdit',
|
|
51
|
+
'mcp__serena__rename_symbol',
|
|
52
|
+
'mcp__serena__replace_symbol_body',
|
|
53
|
+
'mcp__serena__insert_after_symbol',
|
|
54
|
+
'mcp__serena__insert_before_symbol',
|
|
55
|
+
]);
|
|
47
56
|
|
|
48
57
|
if (WRITE_TOOLS.has(tool)) {
|
|
49
58
|
deny(
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xtrm-tools",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.6",
|
|
4
4
|
"description": "Claude Code tools installer (skills, hooks, MCP servers)",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
|
+
"workspaces": [
|
|
8
|
+
"cli"
|
|
9
|
+
],
|
|
7
10
|
"bin": {
|
|
8
11
|
"xtrm": "cli/dist/index.cjs"
|
|
9
12
|
},
|
|
@@ -29,10 +32,12 @@
|
|
|
29
32
|
"url": "https://github.com/Jaggerxtrm/xtrm-tools/issues"
|
|
30
33
|
},
|
|
31
34
|
"scripts": {
|
|
32
|
-
"build": "npm run build --
|
|
35
|
+
"build": "npm run build --workspace cli",
|
|
36
|
+
"typecheck": "npm run typecheck --workspace cli",
|
|
33
37
|
"start": "node cli/dist/index.cjs",
|
|
34
38
|
"lint": "echo 'No linting configured'",
|
|
35
|
-
"test": "
|
|
39
|
+
"test": "npm test --workspace cli",
|
|
40
|
+
"prepublishOnly": "npm run build"
|
|
36
41
|
},
|
|
37
42
|
"engines": {
|
|
38
43
|
"node": ">=18.0.0"
|
|
@@ -218,9 +218,22 @@ def parse_json_input() -> dict:
|
|
|
218
218
|
sys.exit(1)
|
|
219
219
|
|
|
220
220
|
def extract_file_path(input_data: dict) -> str | None:
|
|
221
|
-
"""Extract file path from tool input"""
|
|
221
|
+
"""Extract file path from tool input, including Serena relative_path."""
|
|
222
222
|
tool_input = input_data.get('tool_input', {})
|
|
223
|
-
|
|
223
|
+
file_path = (
|
|
224
|
+
tool_input.get('file_path')
|
|
225
|
+
or tool_input.get('path')
|
|
226
|
+
or tool_input.get('relative_path')
|
|
227
|
+
)
|
|
228
|
+
if not file_path:
|
|
229
|
+
return None
|
|
230
|
+
|
|
231
|
+
# Serena tools pass relative_path relative to the project root.
|
|
232
|
+
if not os.path.isabs(file_path):
|
|
233
|
+
project_root = os.environ.get('CLAUDE_PROJECT_DIR') or os.getcwd()
|
|
234
|
+
return str(Path(project_root) / file_path)
|
|
235
|
+
|
|
236
|
+
return file_path
|
|
224
237
|
|
|
225
238
|
def main():
|
|
226
239
|
"""Main entry point"""
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"hooks": {
|
|
3
3
|
"PostToolUse": [
|
|
4
4
|
{
|
|
5
|
-
"matcher": "Write|Edit|MultiEdit",
|
|
5
|
+
"matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
],
|
|
13
13
|
"PreToolUse": [
|
|
14
14
|
{
|
|
15
|
-
"matcher": "Read|Write|Edit|Glob|Grep|Bash",
|
|
15
|
+
"matcher": "Read|Write|Edit|Glob|Grep|Bash|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
16
16
|
"hooks": [
|
|
17
17
|
{
|
|
18
18
|
"type": "command",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
],
|
|
24
24
|
"PostToolUse": [
|
|
25
25
|
{
|
|
26
|
-
"matcher": "Write|Edit",
|
|
26
|
+
"matcher": "Write|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
27
27
|
"hooks": [
|
|
28
28
|
{
|
|
29
29
|
"type": "command",
|
|
@@ -41,12 +41,12 @@ SETTINGS_HOOKS = {
|
|
|
41
41
|
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/skills/using-service-skills/scripts/cataloger.py\""}]}
|
|
42
42
|
],
|
|
43
43
|
"PreToolUse": [
|
|
44
|
-
{"matcher": "Read|Write|Edit|Glob|Grep|Bash",
|
|
44
|
+
{"matcher": "Read|Write|Edit|Glob|Grep|Bash|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
45
45
|
"hooks": [{"type": "command",
|
|
46
46
|
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/skills/using-service-skills/scripts/skill_activator.py\""}]}
|
|
47
47
|
],
|
|
48
48
|
"PostToolUse": [
|
|
49
|
-
{"matcher": "Write|Edit",
|
|
49
|
+
{"matcher": "Write|Edit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
50
50
|
"hooks": [{"type": "command",
|
|
51
51
|
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/skills/updating-service-skills/scripts/drift_detector.py\" check-hook",
|
|
52
52
|
"timeout": 10}]}
|
|
@@ -55,6 +55,7 @@ SETTINGS_HOOKS = {
|
|
|
55
55
|
|
|
56
56
|
MARKER_DOC = "# [jaggers] doc-reminder"
|
|
57
57
|
MARKER_STALENESS = "# [jaggers] skill-staleness"
|
|
58
|
+
MARKER_CHAIN = "# [jaggers] chain-githooks"
|
|
58
59
|
|
|
59
60
|
|
|
60
61
|
def get_project_root() -> Path:
|
|
@@ -126,25 +127,54 @@ def install_git_hooks(project_root: Path) -> None:
|
|
|
126
127
|
f"\n{MARKER_STALENESS}\nif command -v python3 &>/dev/null && [ -f \"{staleness_script}\" ]; then\n python3 \"{staleness_script}\" || true\nfi\n"),
|
|
127
128
|
]
|
|
128
129
|
|
|
129
|
-
changed = False
|
|
130
130
|
for hook_path, marker, snippet in snippets:
|
|
131
131
|
content = hook_path.read_text(encoding="utf-8")
|
|
132
132
|
if marker not in content:
|
|
133
133
|
hook_path.write_text(content + snippet, encoding="utf-8")
|
|
134
134
|
print(f"{GREEN} ✓{NC} {hook_path.relative_to(project_root)}")
|
|
135
|
-
changed = True
|
|
136
135
|
else:
|
|
137
136
|
print(f"{YELLOW} ○{NC} already installed: {hook_path.name}")
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
138
|
+
hooks_path = ""
|
|
139
|
+
try:
|
|
140
|
+
r = subprocess.run(
|
|
141
|
+
["git", "config", "--get", "core.hooksPath"],
|
|
142
|
+
cwd=project_root,
|
|
143
|
+
capture_output=True,
|
|
144
|
+
text=True,
|
|
145
|
+
timeout=5,
|
|
146
|
+
check=False,
|
|
147
|
+
)
|
|
148
|
+
if r.returncode == 0:
|
|
149
|
+
hooks_path = r.stdout.strip()
|
|
150
|
+
except Exception:
|
|
151
|
+
hooks_path = ""
|
|
152
|
+
|
|
153
|
+
active_hooks_dir = (Path(hooks_path) if Path(hooks_path).is_absolute() else project_root / hooks_path) if hooks_path else (project_root / ".git" / "hooks")
|
|
154
|
+
activation_targets = {project_root / ".git" / "hooks", active_hooks_dir}
|
|
155
|
+
|
|
156
|
+
for hooks_dir in activation_targets:
|
|
157
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
158
|
+
for name, source_hook in (("pre-commit", pre_commit), ("pre-push", pre_push)):
|
|
159
|
+
target_hook = hooks_dir / name
|
|
160
|
+
if not target_hook.exists():
|
|
161
|
+
target_hook.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
|
|
162
|
+
target_hook.chmod(0o755)
|
|
163
|
+
|
|
164
|
+
if target_hook.resolve() == source_hook.resolve():
|
|
165
|
+
continue
|
|
166
|
+
|
|
167
|
+
chain_snippet = (
|
|
168
|
+
f"\n{MARKER_CHAIN}\n"
|
|
169
|
+
f"if [ -x \"{source_hook}\" ]; then\n"
|
|
170
|
+
f" \"{source_hook}\" \"$@\"\n"
|
|
171
|
+
"fi\n"
|
|
172
|
+
)
|
|
173
|
+
target_content = target_hook.read_text(encoding="utf-8")
|
|
174
|
+
if MARKER_CHAIN not in target_content:
|
|
175
|
+
target_hook.write_text(target_content + chain_snippet, encoding="utf-8")
|
|
176
|
+
|
|
177
|
+
print(f"{GREEN} ✓{NC} activated in .git/hooks/")
|
|
148
178
|
|
|
149
179
|
|
|
150
180
|
def main() -> None:
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { readFileSync } = require('node:fs');
|
|
3
|
+
const { spawnSync } = require('node:child_process');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
|
|
6
|
+
const CODE_EXTENSIONS = new Set([
|
|
7
|
+
'.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs',
|
|
8
|
+
'.py', '.php', '.go', '.rs', '.java', '.kt', '.kts',
|
|
9
|
+
'.c', '.cc', '.cpp', '.cxx', '.h', '.hh', '.hpp',
|
|
10
|
+
'.cs', '.rb', '.swift', '.scala', '.lua', '.sh', '.zsh', '.bash',
|
|
11
|
+
]);
|
|
12
|
+
|
|
13
|
+
const SERENA_EDIT_TOOLS = new Set([
|
|
14
|
+
'mcp__serena__rename_symbol',
|
|
15
|
+
'mcp__serena__replace_symbol_body',
|
|
16
|
+
'mcp__serena__insert_after_symbol',
|
|
17
|
+
'mcp__serena__insert_before_symbol',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
function isEditLikeTool(toolName) {
|
|
21
|
+
if (!toolName) return false;
|
|
22
|
+
if (toolName === 'Edit' || toolName === 'Write' || toolName === 'MultiEdit' || toolName === 'TodoWrite') {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
return SERENA_EDIT_TOOLS.has(toolName);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function pickFilePath(toolInput) {
|
|
29
|
+
if (!toolInput || typeof toolInput !== 'object') return null;
|
|
30
|
+
|
|
31
|
+
const direct = toolInput.file_path || toolInput.filePath || toolInput.path ||
|
|
32
|
+
toolInput.relative_path || toolInput.relativePath;
|
|
33
|
+
if (typeof direct === 'string' && direct.trim()) return direct;
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(toolInput.edits)) {
|
|
36
|
+
for (const edit of toolInput.edits) {
|
|
37
|
+
const p = edit?.file_path || edit?.filePath || edit?.path || edit?.relative_path || edit?.relativePath;
|
|
38
|
+
if (typeof p === 'string' && p.trim()) return p;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isCodeFile(filePath) {
|
|
46
|
+
if (!filePath) return true; // fail-open when no path is available
|
|
47
|
+
return CODE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let payloadText = '';
|
|
51
|
+
try {
|
|
52
|
+
payloadText = readFileSync(0, 'utf8');
|
|
53
|
+
} catch {
|
|
54
|
+
process.exit(0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let payload;
|
|
58
|
+
try {
|
|
59
|
+
payload = JSON.parse(payloadText);
|
|
60
|
+
} catch {
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const toolName = payload.tool_name || '';
|
|
65
|
+
if (!isEditLikeTool(toolName)) {
|
|
66
|
+
process.exit(0);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const filePath = pickFilePath(payload.tool_input);
|
|
70
|
+
if (!isCodeFile(filePath)) {
|
|
71
|
+
process.exit(0);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const result = spawnSync('tdd-guard', {
|
|
75
|
+
input: payloadText,
|
|
76
|
+
encoding: 'utf8',
|
|
77
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
81
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
82
|
+
|
|
83
|
+
if (result.error) {
|
|
84
|
+
process.exit(0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
process.exit(result.status ?? 0);
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"hooks": {
|
|
3
3
|
"PreToolUse": [
|
|
4
4
|
{
|
|
5
|
-
"matcher": "Write|Edit|MultiEdit|TodoWrite",
|
|
5
|
+
"matcher": "Write|Edit|MultiEdit|TodoWrite|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
6
6
|
"hooks": [
|
|
7
7
|
{
|
|
8
8
|
"type": "command",
|
|
9
|
-
"command": "tdd-guard",
|
|
9
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/tdd-guard-pretool-bridge.cjs\"",
|
|
10
10
|
"timeout": 30
|
|
11
11
|
}
|
|
12
12
|
]
|
|
@@ -276,8 +276,8 @@ Type `/hooks` in Claude Code to open the hooks menu, then configure each hook. U
|
|
|
276
276
|
**PreToolUse Hook**
|
|
277
277
|
|
|
278
278
|
1. Select `PreToolUse - Before tool execution`
|
|
279
|
-
2. Choose `+ Add new matcher...` and enter: `Write|Edit|MultiEdit|TodoWrite`
|
|
280
|
-
3. Select `+ Add new hook...` and enter: `tdd-guard`
|
|
279
|
+
2. Choose `+ Add new matcher...` and enter: `Write|Edit|MultiEdit|TodoWrite|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol`
|
|
280
|
+
3. Select `+ Add new hook...` and enter: `node "$CLAUDE_PROJECT_DIR/.claude/hooks/tdd-guard-pretool-bridge.cjs"`
|
|
281
281
|
4. Choose where to save
|
|
282
282
|
|
|
283
283
|
**UserPromptSubmit Hook**
|
|
@@ -305,11 +305,11 @@ If you prefer to edit settings files directly, add all three hooks to your chose
|
|
|
305
305
|
"hooks": {
|
|
306
306
|
"PreToolUse": [
|
|
307
307
|
{
|
|
308
|
-
"matcher": "Write|Edit|MultiEdit|TodoWrite",
|
|
308
|
+
"matcher": "Write|Edit|MultiEdit|TodoWrite|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
309
309
|
"hooks": [
|
|
310
310
|
{
|
|
311
311
|
"type": "command",
|
|
312
|
-
"command": "tdd-guard"
|
|
312
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/tdd-guard-pretool-bridge.cjs\""
|
|
313
313
|
}
|
|
314
314
|
]
|
|
315
315
|
}
|
|
@@ -341,6 +341,8 @@ If you prefer to edit settings files directly, add all three hooks to your chose
|
|
|
341
341
|
|
|
342
342
|
</details>
|
|
343
343
|
|
|
344
|
+
**Note**: The pretool bridge skips non-code files (for example `.md`) and forwards code edits to `tdd-guard`, which avoids false positives on documentation-only changes.
|
|
345
|
+
|
|
344
346
|
## Additional Configuration
|
|
345
347
|
|
|
346
348
|
- [Custom instructions](docs/custom-instructions.md) - Customize TDD validation rules
|
|
@@ -42,7 +42,7 @@ The refactoring support helps by:
|
|
|
42
42
|
1. Type `/hooks` in Claude Code
|
|
43
43
|
2. Select `PostToolUse - After tool execution`
|
|
44
44
|
3. Choose `+ Add new matcher...`
|
|
45
|
-
4. Enter: `Write|Edit|MultiEdit`
|
|
45
|
+
4. Enter: `Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol`
|
|
46
46
|
5. Select `+ Add new hook...`
|
|
47
47
|
6. Enter command: `tdd-guard`
|
|
48
48
|
7. Choose where to save
|
|
@@ -56,7 +56,7 @@ The refactoring support helps by:
|
|
|
56
56
|
"hooks": {
|
|
57
57
|
"PostToolUse": [
|
|
58
58
|
{
|
|
59
|
-
"matcher": "Write|Edit|MultiEdit",
|
|
59
|
+
"matcher": "Write|Edit|MultiEdit|mcp__serena__rename_symbol|mcp__serena__replace_symbol_body|mcp__serena__insert_after_symbol|mcp__serena__insert_before_symbol",
|
|
60
60
|
"hooks": [
|
|
61
61
|
{
|
|
62
62
|
"type": "command",
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import type { Test, TestResult, AggregatedResult } from '@jest/reporters'
|
|
2
|
+
import type { Config } from '@jest/types'
|
|
3
|
+
|
|
4
|
+
// Create a minimal snapshot object that satisfies the type requirements
|
|
5
|
+
const createSnapshot = (): TestResult['snapshot'] =>
|
|
6
|
+
({
|
|
7
|
+
added: 0,
|
|
8
|
+
didUpdate: false,
|
|
9
|
+
failure: false,
|
|
10
|
+
filesAdded: 0,
|
|
11
|
+
filesRemoved: 0,
|
|
12
|
+
filesRemovedList: [],
|
|
13
|
+
filesUnmatched: 0,
|
|
14
|
+
filesUpdated: 0,
|
|
15
|
+
matched: 0,
|
|
16
|
+
total: 0,
|
|
17
|
+
unchecked: 0,
|
|
18
|
+
uncheckedKeysByFile: [],
|
|
19
|
+
unmatched: 0,
|
|
20
|
+
updated: 0,
|
|
21
|
+
// Additional properties that might be required by different versions
|
|
22
|
+
fileDeleted: false,
|
|
23
|
+
uncheckedKeys: [],
|
|
24
|
+
}) as TestResult['snapshot']
|
|
25
|
+
|
|
26
|
+
// Create a minimal snapshot summary for AggregatedResult
|
|
27
|
+
const createSnapshotSummary = (): AggregatedResult['snapshot'] =>
|
|
28
|
+
({
|
|
29
|
+
added: 0,
|
|
30
|
+
didUpdate: false,
|
|
31
|
+
failure: false,
|
|
32
|
+
filesAdded: 0,
|
|
33
|
+
filesRemoved: 0,
|
|
34
|
+
filesRemovedList: [],
|
|
35
|
+
filesUnmatched: 0,
|
|
36
|
+
filesUpdated: 0,
|
|
37
|
+
matched: 0,
|
|
38
|
+
total: 0,
|
|
39
|
+
unchecked: 0,
|
|
40
|
+
uncheckedKeysByFile: [],
|
|
41
|
+
unmatched: 0,
|
|
42
|
+
updated: 0,
|
|
43
|
+
}) as AggregatedResult['snapshot']
|
|
44
|
+
|
|
45
|
+
// Create a minimal Test object
|
|
46
|
+
export function createTest(overrides?: Partial<Test>): Test {
|
|
47
|
+
// For test purposes, we create minimal mock implementations
|
|
48
|
+
const mockContext = {
|
|
49
|
+
config: {} as Config.ProjectConfig,
|
|
50
|
+
hasteFS: {} as never, // Using never since we don't access these properties
|
|
51
|
+
moduleMap: {} as never, // Using never since we don't access these properties
|
|
52
|
+
resolver: {} as never, // Using never since we don't access these properties
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
context: mockContext,
|
|
57
|
+
duration: 100,
|
|
58
|
+
path: '/test/example.test.ts',
|
|
59
|
+
...overrides,
|
|
60
|
+
} as Test
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Create a minimal TestResult object
|
|
64
|
+
export function createTestResult(overrides?: Partial<TestResult>): TestResult {
|
|
65
|
+
const base: TestResult = {
|
|
66
|
+
leaks: false,
|
|
67
|
+
numFailingTests: 0,
|
|
68
|
+
numPassingTests: 1,
|
|
69
|
+
numPendingTests: 0,
|
|
70
|
+
numTodoTests: 0,
|
|
71
|
+
openHandles: [],
|
|
72
|
+
perfStats: {
|
|
73
|
+
end: 1000,
|
|
74
|
+
runtime: 100,
|
|
75
|
+
slow: false,
|
|
76
|
+
start: 900,
|
|
77
|
+
loadTestEnvironmentEnd: 950,
|
|
78
|
+
loadTestEnvironmentStart: 920,
|
|
79
|
+
setupAfterEnvEnd: 980,
|
|
80
|
+
setupAfterEnvStart: 960,
|
|
81
|
+
setupFilesEnd: 940,
|
|
82
|
+
setupFilesStart: 930,
|
|
83
|
+
},
|
|
84
|
+
skipped: false,
|
|
85
|
+
snapshot: createSnapshot(),
|
|
86
|
+
testExecError: undefined,
|
|
87
|
+
testFilePath: '/test/example.test.ts',
|
|
88
|
+
testResults: [
|
|
89
|
+
{
|
|
90
|
+
ancestorTitles: ['Example Suite'],
|
|
91
|
+
duration: 5,
|
|
92
|
+
failureDetails: [],
|
|
93
|
+
failureMessages: [],
|
|
94
|
+
fullName: 'Example Suite should pass',
|
|
95
|
+
invocations: 1,
|
|
96
|
+
location: undefined,
|
|
97
|
+
numPassingAsserts: 0,
|
|
98
|
+
retryReasons: [],
|
|
99
|
+
status: 'passed',
|
|
100
|
+
title: 'should pass',
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
...overrides,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// If test is failing, update the test results
|
|
107
|
+
if (overrides?.numFailingTests && overrides.numFailingTests > 0) {
|
|
108
|
+
base.testResults = [
|
|
109
|
+
{
|
|
110
|
+
ancestorTitles: ['Example Suite'],
|
|
111
|
+
duration: 5,
|
|
112
|
+
failureDetails: [{}],
|
|
113
|
+
failureMessages: ['expected 2 to be 3'],
|
|
114
|
+
fullName: 'Example Suite should fail',
|
|
115
|
+
invocations: 1,
|
|
116
|
+
location: undefined,
|
|
117
|
+
numPassingAsserts: 0,
|
|
118
|
+
retryReasons: [],
|
|
119
|
+
status: 'failed',
|
|
120
|
+
title: 'should fail',
|
|
121
|
+
},
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return base
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Create a minimal AggregatedResult object
|
|
129
|
+
export function createAggregatedResult(
|
|
130
|
+
overrides?: Partial<AggregatedResult>
|
|
131
|
+
): AggregatedResult {
|
|
132
|
+
return {
|
|
133
|
+
numFailedTestSuites: 0,
|
|
134
|
+
numFailedTests: 0,
|
|
135
|
+
numPassedTestSuites: 1,
|
|
136
|
+
numPassedTests: 1,
|
|
137
|
+
numPendingTestSuites: 0,
|
|
138
|
+
numPendingTests: 0,
|
|
139
|
+
numRuntimeErrorTestSuites: 0,
|
|
140
|
+
numTodoTests: 0,
|
|
141
|
+
numTotalTestSuites: 1,
|
|
142
|
+
numTotalTests: 1,
|
|
143
|
+
openHandles: [],
|
|
144
|
+
runExecError: undefined,
|
|
145
|
+
snapshot: createSnapshotSummary(),
|
|
146
|
+
startTime: Date.now(),
|
|
147
|
+
success: true,
|
|
148
|
+
testResults: [],
|
|
149
|
+
wasInterrupted: false,
|
|
150
|
+
...overrides,
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function createUnhandledError(
|
|
155
|
+
overrides: Partial<{ name: string; message: string; stack: string }> = {}
|
|
156
|
+
): AggregatedResult['runExecError'] {
|
|
157
|
+
return {
|
|
158
|
+
message: overrides.message ?? 'Cannot find module "./helpers"',
|
|
159
|
+
stack:
|
|
160
|
+
overrides.stack ??
|
|
161
|
+
"Error: Cannot find module './helpers' imported from '/src/example.test.ts'",
|
|
162
|
+
...(overrides.name && { name: overrides.name }),
|
|
163
|
+
// SerializableError might have additional properties but these are the required ones
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Create a module error (testExecError) for import failures
|
|
168
|
+
export function createModuleError(
|
|
169
|
+
overrides: Partial<{
|
|
170
|
+
name: string
|
|
171
|
+
message: string
|
|
172
|
+
stack: string
|
|
173
|
+
type: string
|
|
174
|
+
code: string
|
|
175
|
+
}> = {}
|
|
176
|
+
): TestResult['testExecError'] {
|
|
177
|
+
return {
|
|
178
|
+
message: overrides.message ?? "Cannot find module './non-existent-module'",
|
|
179
|
+
stack:
|
|
180
|
+
overrides.stack ??
|
|
181
|
+
"Error: Cannot find module './non-existent-module'\n at Resolver.resolveModule",
|
|
182
|
+
...(overrides.name && { name: overrides.name }),
|
|
183
|
+
...(overrides.type && { type: overrides.type }),
|
|
184
|
+
...(overrides.code && { code: overrides.code }),
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Create a TestResult with module import error
|
|
189
|
+
export function createTestResultWithModuleError(
|
|
190
|
+
overrides?: Partial<TestResult>
|
|
191
|
+
): TestResult {
|
|
192
|
+
return createTestResult({
|
|
193
|
+
testExecError: createModuleError(),
|
|
194
|
+
testResults: [], // No test results when module fails to load
|
|
195
|
+
numFailingTests: 0,
|
|
196
|
+
numPassingTests: 0,
|
|
197
|
+
...overrides,
|
|
198
|
+
})
|
|
199
|
+
}
|