go-agent/claude/agentic/scripts/post-edit.sh
Snider 61e01bfdf1 feat: initial go-agent — agentci + jobrunner + plugins marketplace
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>
2026-02-21 15:47:19 +00:00

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