Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions .claude/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Claude Code Hooks for Nextflow Development

This directory contains Claude Code hooks configured to improve the Nextflow development experience.

## Features

### 1. EditorConfig Enforcement (PostToolUse)
- **Trigger**: Immediately after editing any source file (`.groovy`, `.java`, `.gradle`, `.md`, `.txt`, `.yml`, `.yaml`, `.json`)
- **Action**: Applies editorconfig formatting rules using `eclint`
- **Files**: `hooks/format-editorconfig.py`

### 2. IntelliJ IDEA Formatter (PostToolUse)
- **Trigger**: Immediately after editing Groovy files
- **Action**: Applies IntelliJ IDEA code formatting to match project style
- **Files**: `hooks/format-idea.py`
- **Requirements**: IntelliJ IDEA installed (Community or Ultimate Edition)

### 3. Build Check (Stop + SubagentStop)
- **Trigger**: When Claude finishes responding or when a subagent completes
- **Action**: Runs `make compile` to verify code compiles without errors
- **Files**: `hooks/check-build.py`
- **Purpose**: Catch syntax and compilation errors immediately

### 4. Automatic Test Running (Stop)
- **Trigger**: When Claude finishes responding (main agent only)
- **Action**:
- For source files: Runs corresponding test class (e.g., `CacheDB.groovy` → runs `CacheDBTest`)
- For test files: Runs the specific test class
- **Files**: `hooks/run-tests.py`

## Hook Configuration

The hooks are configured in `.claude/settings.json` with:

### PostToolUse (runs after each edit)
- **format-editorconfig.py**: 30-second timeout
- **format-idea.py**: 60-second timeout

### Stop (runs when main agent finishes)
- **check-build.py**: 120-second timeout (runs first)
- **run-tests.py**: 300-second timeout (runs after build succeeds)

### SubagentStop (runs when subagent finishes)
- **check-build.py**: 120-second timeout

## Supported File Structure

The hooks understand Nextflow's module structure:

```
modules/
├── nextflow/src/main/groovy/nextflow/cache/CacheDB.groovy
├── nextflow/src/test/groovy/nextflow/cache/CacheDBTest.groovy
├── nf-commons/src/main/groovy/...
├── nf-lang/src/main/java/...
└── ...

plugins/
├── nf-amazon/src/main/nextflow/cloud/aws/...
├── nf-azure/src/main/nextflow/cloud/azure/...
└── ...
```

## Test Commands Generated

The hooks generate appropriate Gradle test commands:

- **Source file**: `modules/nextflow/src/main/groovy/nextflow/cache/CacheDB.groovy`
- Runs: `./gradlew :nextflow:test --tests "*CacheDBTest"`

- **Test file**: `modules/nextflow/src/test/groovy/nextflow/cache/CacheDBTest.groovy`
- Runs: `./gradlew :nextflow:test --tests "*CacheDBTest"`

- **Plugin file**: `plugins/nf-amazon/src/main/nextflow/cloud/aws/AwsPlugin.groovy`
- Runs: `./gradlew :plugins:nf-amazon:test --tests "*AwsPluginTest"`

## Error Handling

- **EditorConfig failures**: Show warnings but don't block Claude
- **Test failures**: Provide detailed feedback to Claude for potential fixes
- **Missing tests**: Silently skip if no corresponding test exists
- **Timeouts**: Cancel long-running operations gracefully

## Dependencies

The hooks may automatically install:
- `eclint` via npm for editorconfig enforcement

Optional dependencies:
- IntelliJ IDEA (Community or Ultimate Edition) for Groovy formatting
- Set `IDEA_SH` environment variable if not in standard location
- Falls back gracefully if not found

## Hook Execution Flow

1. **During editing** (PostToolUse):
```
✓ EditorConfig formatting applied to CacheDB.groovy
✓ IDEA formatter: applied to CacheDB.groovy (5.2s)
```

2. **When finishing** (Stop):
```
✓ Build check passed (12.3s)
✓ Tests passed for CacheDB.groovy
```

3. **If errors occur**:
```
Build check failed:
error: cannot find symbol
symbol: variable foo
location: class CacheDB
```

## Customization

You can modify the hooks by:
1. Editing the Python scripts in `hooks/`
2. Adjusting timeouts in `settings.json`
3. Adding or removing file extensions in the filter logic
4. Disabling specific hooks by removing them from `settings.json`

## Troubleshooting

If hooks aren't working:
1. Check that scripts are executable: `chmod +x .claude/hooks/*.py`
2. Verify Python 3 is available
3. Check Claude Code's debug output with `claude --debug`
4. Review hook execution in the transcript (Ctrl-R)
5. For IDEA formatter: verify IDEA installation or set `IDEA_SH` environment variable
124 changes: 124 additions & 0 deletions .claude/hooks/check-build.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
#!/usr/bin/env python3
"""
Build check hook for Nextflow development.
This hook runs a quick compilation check to catch syntax errors.
"""

import json
import os
import subprocess
import sys
import time
from pathlib import Path


def run_build_check():
"""Run a quick build check"""
start_time = time.time()

try:
# Run make compile for quick syntax checking
result = subprocess.run(
['make', 'compile'],
capture_output=True,
text=True,
timeout=120 # 2 minute timeout
)

execution_time = time.time() - start_time

if result.returncode == 0:
return {
'success': True,
'execution_time': execution_time,
'message': f"✓ Build check passed ({execution_time:.1f}s)"
}
else:
# Extract error information
error_lines = []
for line in result.stdout.split('\n') + result.stderr.split('\n'):
if any(keyword in line.lower() for keyword in ['error', 'failed', 'exception']):
error_lines.append(line)

error_summary = '\n'.join(error_lines[-10:]) if error_lines else result.stderr[-500:]

return {
'success': False,
'execution_time': execution_time,
'error': f"Build check failed:\n{error_summary}",
'suggestions': [
'Check for syntax errors in modified files',
'Review compilation errors above',
'Run `make compile` manually for full output'
]
}

except subprocess.TimeoutExpired:
execution_time = time.time() - start_time
return {
'success': False,
'execution_time': execution_time,
'error': 'Build check timed out after 2 minutes',
'suggestions': [
'Check if build is hung',
'Try running `make compile` manually'
]
}
except Exception as e:
execution_time = time.time() - start_time
return {
'success': False,
'execution_time': execution_time,
'error': f'Exception during build check: {str(e)}',
'suggestions': [
'Verify make is installed',
'Check build system configuration'
]
}


def main():
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
error_output = {
"decision": "block",
"reason": f"Build check hook received invalid JSON input: {e}",
"suggestions": ["Check Claude Code hook configuration"]
}
print(json.dumps(error_output))
sys.exit(0)

hook_event = input_data.get("hook_event_name", "")

# Only run on Stop and SubagentStop events
if hook_event not in ["stop-hook", "subagent-stop-hook"]:
sys.exit(0)

# Run build check
result = run_build_check()

if result['success']:
output = {
"suppressOutput": True,
"systemMessage": result['message']
}
print(json.dumps(output))
sys.exit(0)
else:
# Block with error information
output = {
"decision": "block",
"reason": result['error'],
"stopReason": "Build compilation failed",
"buildError": {
"executionTime": result['execution_time'],
"suggestions": result.get('suggestions', [])
}
}
print(json.dumps(output))
sys.exit(0)


if __name__ == "__main__":
main()
95 changes: 95 additions & 0 deletions .claude/hooks/format-editorconfig.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
#!/usr/bin/env python3
"""
EditorConfig enforcement hook for Nextflow development.
This hook applies editorconfig formatting rules after file edits.
"""

import json
import os
import subprocess
import sys
from pathlib import Path


def is_source_file(file_path):
"""Check if the file should be formatted"""
if not file_path:
return False

# Only format source code files
extensions = {'.groovy', '.java', '.gradle', '.md', '.txt', '.yml', '.yaml', '.json'}
path = Path(file_path)

# Skip build directories, .git, etc.
if any(part.startswith('.') or part == 'build' for part in path.parts):
return False

return path.suffix.lower() in extensions


def format_with_editorconfig(file_path):
"""Apply editorconfig formatting to a file"""
try:
# Check if eclint is available
result = subprocess.run(['which', 'eclint'], capture_output=True, text=True)
if result.returncode != 0:
print("eclint not found. Installing via npm...", file=sys.stderr)
install_result = subprocess.run(['npm', 'install', '-g', 'eclint'],
capture_output=True, text=True)
if install_result.returncode != 0:
return False, "Failed to install eclint"

# Apply editorconfig formatting
format_result = subprocess.run(['eclint', 'fix', file_path],
capture_output=True, text=True)

if format_result.returncode == 0:
return True, f"Applied editorconfig formatting to {file_path}"
else:
return False, f"eclint failed: {format_result.stderr}"

except Exception as e:
return False, f"Error formatting file: {str(e)}"


def main():
try:
input_data = json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error: Invalid JSON input: {e}", file=sys.stderr)
sys.exit(1)

hook_event = input_data.get("hook_event_name", "")
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})

# Only process Edit, Write, MultiEdit tools
if tool_name not in ["Edit", "Write", "MultiEdit"]:
sys.exit(0)

file_path = tool_input.get("file_path", "")
if not file_path or not is_source_file(file_path):
sys.exit(0)

# Check if file exists after the edit
if not os.path.exists(file_path):
sys.exit(0)

success, message = format_with_editorconfig(file_path)

if success:
# Use JSON output to suppress the normal stdout display
output = {
"suppressOutput": True,
"systemMessage": f"✓ EditorConfig formatting applied to {os.path.basename(file_path)}"
}
print(json.dumps(output))
sys.exit(0)
else:
# Non-blocking error - show message but don't fail
print(f"Warning: {message}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
main()
Loading