Consolidates three codebases into a single agent orchestration repo: - agentci (from go-scm): Clotho dual-run verification, agent config, SSH security (sanitisation, secure commands, token masking) - jobrunner (from go-scm): Poll-dispatch-report pipeline with 7 handlers (dispatch, completion, auto-merge, publish draft, dismiss reviews, send fix command, tick parent epic) - plugins marketplace (from agentic/plugins): 27 Claude/Codex/Gemini plugins with shared MCP server All 150+ tests passing across 6 packages. Co-Authored-By: Virgil <virgil@lethean.io>
286 lines
10 KiB
Bash
Executable file
286 lines
10 KiB
Bash
Executable file
#!/bin/bash
|
|
# PostToolUse:Edit Hook - Suggest lint/format/test commands after file edits
|
|
# Detects file type from edited path and suggests appropriate commands
|
|
#
|
|
# Input: JSON with tool_input.file_path containing the edited file
|
|
# Output: JSON with systemMessage containing suggested commands
|
|
|
|
set -euo pipefail
|
|
|
|
# Read input from stdin
|
|
input=$(cat)
|
|
|
|
# Extract the edited file path from tool_input
|
|
file_path=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
|
|
|
# If no file path, silently succeed
|
|
if [[ -z "$file_path" ]]; then
|
|
echo '{"continue": true}'
|
|
exit 0
|
|
fi
|
|
|
|
# Get the file extension (lowercase)
|
|
extension="${file_path##*.}"
|
|
extension=$(echo "$extension" | tr '[:upper:]' '[:lower:]')
|
|
|
|
# Detect project type and available tools
|
|
PROJECT_DIR="${CLAUDE_PROJECT_DIR:-$(pwd)}"
|
|
HAS_CORE_CLI="false"
|
|
if command -v core &>/dev/null; then
|
|
HAS_CORE_CLI="true"
|
|
fi
|
|
|
|
# Build suggestions based on file type
|
|
SUGGESTIONS=""
|
|
QUICK_CMD=""
|
|
|
|
case "$extension" in
|
|
go)
|
|
if [[ "$HAS_CORE_CLI" == "true" ]]; then
|
|
QUICK_CMD="core go qa quick"
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
SUGGESTIONS+="| Quick check | \`core go qa quick\` |\\n"
|
|
SUGGESTIONS+="| Fix all issues | \`core go qa --fix\` |\\n"
|
|
SUGGESTIONS+="| Run tests | \`core go qa --only=test\` |\\n"
|
|
SUGGESTIONS+="| Full QA with coverage | \`core go qa --coverage\` |\\n"
|
|
else
|
|
QUICK_CMD="go fmt \"$file_path\" && go vet \"$file_path\""
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
SUGGESTIONS+="| Format file | \`go fmt \"$file_path\"\` |\\n"
|
|
SUGGESTIONS+="| Vet file | \`go vet \"$file_path\"\` |\\n"
|
|
SUGGESTIONS+="| Run tests | \`go test ./...\` |\\n"
|
|
SUGGESTIONS+="| All checks | \`go fmt ./... && go vet ./... && go test ./...\` |\\n"
|
|
fi
|
|
;;
|
|
|
|
ts|tsx)
|
|
# Check for package.json in project
|
|
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
|
# Check what scripts are available
|
|
HAS_LINT=$(jq -r '.scripts.lint // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "")
|
|
HAS_TEST=$(jq -r '.scripts.test // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "")
|
|
HAS_FORMAT=$(jq -r '.scripts.format // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "")
|
|
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
|
|
if [[ -n "$HAS_LINT" ]]; then
|
|
QUICK_CMD="npm run lint"
|
|
SUGGESTIONS+="| Lint | \`npm run lint\` |\\n"
|
|
fi
|
|
if [[ -n "$HAS_FORMAT" ]]; then
|
|
SUGGESTIONS+="| Format | \`npm run format\` |\\n"
|
|
fi
|
|
if [[ -n "$HAS_TEST" ]]; then
|
|
SUGGESTIONS+="| Test | \`npm test\` |\\n"
|
|
fi
|
|
|
|
# TypeScript specific
|
|
SUGGESTIONS+="| Type check | \`npx tsc --noEmit\` |\\n"
|
|
else
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
SUGGESTIONS+="| Type check | \`npx tsc --noEmit\` |\\n"
|
|
QUICK_CMD="npx tsc --noEmit"
|
|
fi
|
|
;;
|
|
|
|
js|jsx|mjs|cjs)
|
|
# Check for package.json in project
|
|
if [[ -f "$PROJECT_DIR/package.json" ]]; then
|
|
HAS_LINT=$(jq -r '.scripts.lint // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "")
|
|
HAS_TEST=$(jq -r '.scripts.test // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "")
|
|
HAS_FORMAT=$(jq -r '.scripts.format // empty' "$PROJECT_DIR/package.json" 2>/dev/null || echo "")
|
|
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
|
|
if [[ -n "$HAS_LINT" ]]; then
|
|
QUICK_CMD="npm run lint"
|
|
SUGGESTIONS+="| Lint | \`npm run lint\` |\\n"
|
|
fi
|
|
if [[ -n "$HAS_FORMAT" ]]; then
|
|
SUGGESTIONS+="| Format | \`npm run format\` |\\n"
|
|
fi
|
|
if [[ -n "$HAS_TEST" ]]; then
|
|
SUGGESTIONS+="| Test | \`npm test\` |\\n"
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
py)
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
|
|
# Check for common Python tools
|
|
if command -v ruff &>/dev/null; then
|
|
QUICK_CMD="ruff check \"$file_path\""
|
|
SUGGESTIONS+="| Lint | \`ruff check \"$file_path\"\` |\\n"
|
|
SUGGESTIONS+="| Fix issues | \`ruff check --fix \"$file_path\"\` |\\n"
|
|
SUGGESTIONS+="| Format | \`ruff format \"$file_path\"\` |\\n"
|
|
elif command -v flake8 &>/dev/null; then
|
|
QUICK_CMD="flake8 \"$file_path\""
|
|
SUGGESTIONS+="| Lint | \`flake8 \"$file_path\"\` |\\n"
|
|
elif command -v pylint &>/dev/null; then
|
|
QUICK_CMD="pylint \"$file_path\""
|
|
SUGGESTIONS+="| Lint | \`pylint \"$file_path\"\` |\\n"
|
|
fi
|
|
|
|
# Check for pytest
|
|
if command -v pytest &>/dev/null; then
|
|
SUGGESTIONS+="| Test | \`pytest\` |\\n"
|
|
elif [[ -f "$PROJECT_DIR/setup.py" ]] || [[ -f "$PROJECT_DIR/pyproject.toml" ]]; then
|
|
SUGGESTIONS+="| Test | \`python -m pytest\` |\\n"
|
|
fi
|
|
|
|
# Type checking
|
|
if command -v mypy &>/dev/null; then
|
|
SUGGESTIONS+="| Type check | \`mypy \"$file_path\"\` |\\n"
|
|
fi
|
|
;;
|
|
|
|
php)
|
|
if [[ "$HAS_CORE_CLI" == "true" ]]; then
|
|
QUICK_CMD="core php qa --quick"
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
SUGGESTIONS+="| Quick check | \`core php qa --quick\` |\\n"
|
|
SUGGESTIONS+="| Fix all issues | \`core php qa --fix\` |\\n"
|
|
SUGGESTIONS+="| Run tests | \`core php test\` |\\n"
|
|
SUGGESTIONS+="| Static analysis | \`core php stan\` |\\n"
|
|
elif [[ -f "$PROJECT_DIR/composer.json" ]]; then
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
|
|
# Check for Laravel Pint
|
|
if [[ -f "$PROJECT_DIR/vendor/bin/pint" ]]; then
|
|
QUICK_CMD="./vendor/bin/pint \"$file_path\""
|
|
SUGGESTIONS+="| Format | \`./vendor/bin/pint \"$file_path\"\` |\\n"
|
|
fi
|
|
|
|
# Check for PHPStan
|
|
if [[ -f "$PROJECT_DIR/vendor/bin/phpstan" ]]; then
|
|
SUGGESTIONS+="| Static analysis | \`./vendor/bin/phpstan analyse \"$file_path\"\` |\\n"
|
|
fi
|
|
|
|
# Check for PHPUnit or Pest
|
|
if [[ -f "$PROJECT_DIR/vendor/bin/phpunit" ]]; then
|
|
SUGGESTIONS+="| Test | \`./vendor/bin/phpunit\` |\\n"
|
|
elif [[ -f "$PROJECT_DIR/vendor/bin/pest" ]]; then
|
|
SUGGESTIONS+="| Test | \`./vendor/bin/pest\` |\\n"
|
|
fi
|
|
|
|
# Check for composer scripts
|
|
HAS_LINT=$(jq -r '.scripts.lint // empty' "$PROJECT_DIR/composer.json" 2>/dev/null || echo "")
|
|
HAS_TEST=$(jq -r '.scripts.test // empty' "$PROJECT_DIR/composer.json" 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$HAS_LINT" ]]; then
|
|
SUGGESTIONS+="| Lint (composer) | \`composer lint\` |\\n"
|
|
fi
|
|
if [[ -n "$HAS_TEST" ]]; then
|
|
SUGGESTIONS+="| Test (composer) | \`composer test\` |\\n"
|
|
fi
|
|
fi
|
|
;;
|
|
|
|
sh|bash)
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
|
|
if command -v shellcheck &>/dev/null; then
|
|
QUICK_CMD="shellcheck \"$file_path\""
|
|
SUGGESTIONS+="| Check script | \`shellcheck \"$file_path\"\` |\\n"
|
|
SUGGESTIONS+="| Check (verbose) | \`shellcheck -x \"$file_path\"\` |\\n"
|
|
else
|
|
SUGGESTIONS+="| Syntax check | \`bash -n \"$file_path\"\` |\\n"
|
|
QUICK_CMD="bash -n \"$file_path\""
|
|
fi
|
|
;;
|
|
|
|
json)
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
|
|
if command -v jq &>/dev/null; then
|
|
QUICK_CMD="jq . \"$file_path\" > /dev/null"
|
|
SUGGESTIONS+="| Validate JSON | \`jq . \"$file_path\" > /dev/null\` |\\n"
|
|
SUGGESTIONS+="| Pretty print | \`jq . \"$file_path\"\` |\\n"
|
|
fi
|
|
|
|
# Check if it's package.json
|
|
if [[ "$(basename "$file_path")" == "package.json" ]]; then
|
|
SUGGESTIONS+="| Install deps | \`npm install\` |\\n"
|
|
fi
|
|
;;
|
|
|
|
yaml|yml)
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
|
|
if command -v yamllint &>/dev/null; then
|
|
QUICK_CMD="yamllint \"$file_path\""
|
|
SUGGESTIONS+="| Validate YAML | \`yamllint \"$file_path\"\` |\\n"
|
|
elif command -v yq &>/dev/null; then
|
|
QUICK_CMD="yq . \"$file_path\" > /dev/null"
|
|
SUGGESTIONS+="| Validate YAML | \`yq . \"$file_path\" > /dev/null\` |\\n"
|
|
fi
|
|
;;
|
|
|
|
md|markdown)
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
|
|
if command -v markdownlint &>/dev/null; then
|
|
QUICK_CMD="markdownlint \"$file_path\""
|
|
SUGGESTIONS+="| Lint markdown | \`markdownlint \"$file_path\"\` |\\n"
|
|
fi
|
|
;;
|
|
|
|
rs)
|
|
SUGGESTIONS="| Task | Command |\\n"
|
|
SUGGESTIONS+="|------|---------|\\n"
|
|
QUICK_CMD="cargo fmt -- --check"
|
|
SUGGESTIONS+="| Format check | \`cargo fmt -- --check\` |\\n"
|
|
SUGGESTIONS+="| Format | \`cargo fmt\` |\\n"
|
|
SUGGESTIONS+="| Lint | \`cargo clippy\` |\\n"
|
|
SUGGESTIONS+="| Test | \`cargo test\` |\\n"
|
|
SUGGESTIONS+="| Check | \`cargo check\` |\\n"
|
|
;;
|
|
|
|
*)
|
|
# Unknown file type - no suggestions
|
|
echo '{"continue": true}'
|
|
exit 0
|
|
;;
|
|
esac
|
|
|
|
# If no suggestions were built, exit silently
|
|
if [[ -z "$SUGGESTIONS" ]]; then
|
|
echo '{"continue": true}'
|
|
exit 0
|
|
fi
|
|
|
|
# Build the message
|
|
MSG="**Post-Edit Suggestions** for \`$(basename "$file_path")\`\\n\\n"
|
|
MSG+="$SUGGESTIONS"
|
|
|
|
# Add recommended quick command if available
|
|
if [[ -n "$QUICK_CMD" ]]; then
|
|
MSG+="\\n**Recommended:** \`$QUICK_CMD\`"
|
|
fi
|
|
|
|
# Escape for JSON
|
|
ESCAPED_MSG=$(echo -e "$MSG" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | tr '\n' ' ' | sed 's/ */ /g')
|
|
|
|
# Output JSON response with additionalContext for Claude
|
|
cat << EOF
|
|
{
|
|
"continue": true,
|
|
"hookSpecificOutput": {
|
|
"hookEventName": "PostToolUse",
|
|
"additionalContext": "$ESCAPED_MSG"
|
|
}
|
|
}
|
|
EOF
|