feat: initial Claude Code plugin for host-uk monorepo
- Skills for core CLI, PHP, and Go patterns - PreToolUse hooks to block destructive commands and enforce core CLI - PreCompact/SessionStart hooks for context preservation - PostToolUse hooks for auto-formatting and debug warnings - /core:remember command for manual context capture Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
commit
0a89ac91d1
18 changed files with 936 additions and 0 deletions
41
README.md
Normal file
41
README.md
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# core-claude
|
||||||
|
|
||||||
|
Claude Code plugin for the Host UK federated monorepo.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/plugin marketplace add host-uk/core-claude
|
||||||
|
/plugin install core@core-claude
|
||||||
|
```
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Skills
|
||||||
|
- **core** - Core CLI command reference for multi-repo management
|
||||||
|
- **core-php** - PHP module patterns for Laravel packages
|
||||||
|
- **core-go** - Go package patterns for the CLI
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
- `/core:remember <fact>` - Save context facts that persist across compaction
|
||||||
|
|
||||||
|
### Hooks
|
||||||
|
|
||||||
|
**Safety hooks:**
|
||||||
|
- Blocks destructive commands (`rm -rf`, `sed -i`, mass operations)
|
||||||
|
- Enforces `core` CLI over raw `go`/`php` commands
|
||||||
|
- Prevents random .md file creation
|
||||||
|
|
||||||
|
**Context preservation:**
|
||||||
|
- Saves state before auto-compact (prevents "amnesia")
|
||||||
|
- Restores recent session context on startup
|
||||||
|
- Extracts actionables from tool output
|
||||||
|
|
||||||
|
**Auto-formatting:**
|
||||||
|
- PHP files via Pint after edits
|
||||||
|
- Go files via gofmt after edits
|
||||||
|
- Warns about debug statements
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- [superpowers](https://github.com/anthropics/claude-plugins-official) from claude-plugins-official
|
||||||
36
commands/remember.md
Normal file
36
commands/remember.md
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
---
|
||||||
|
name: remember
|
||||||
|
description: Save a fact or decision to context for persistence across compacts
|
||||||
|
args: <fact to remember>
|
||||||
|
---
|
||||||
|
|
||||||
|
# Remember Context
|
||||||
|
|
||||||
|
Save the provided fact to `~/.claude/sessions/context.json`.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/core:remember Use Action pattern not Service
|
||||||
|
/core:remember User prefers UK English
|
||||||
|
/core:remember RFC: minimal state in pre-compact hook
|
||||||
|
```
|
||||||
|
|
||||||
|
## Action
|
||||||
|
|
||||||
|
Run this command to save the fact:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
~/.claude/plugins/cache/core/scripts/capture-context.sh "<fact>" "user"
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if running from the plugin directory:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"${CLAUDE_PLUGIN_ROOT}/scripts/capture-context.sh" "<fact>" "user"
|
||||||
|
```
|
||||||
|
|
||||||
|
The fact will be:
|
||||||
|
- Stored in context.json (max 20 items)
|
||||||
|
- Included in pre-compact snapshots
|
||||||
|
- Auto-cleared after 3 hours of inactivity
|
||||||
102
hooks/prefer-core.sh
Executable file
102
hooks/prefer-core.sh
Executable file
|
|
@ -0,0 +1,102 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# PreToolUse hook: Block dangerous commands, enforce core CLI
|
||||||
|
#
|
||||||
|
# BLOCKS:
|
||||||
|
# - Raw go commands (use core go *)
|
||||||
|
# - Destructive grep patterns (sed -i, xargs rm, etc.)
|
||||||
|
# - Mass file operations (rm -rf, mv/cp with wildcards)
|
||||||
|
# - Any sed outside of safe patterns
|
||||||
|
#
|
||||||
|
# This prevents "efficient shortcuts" that nuke codebases
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
command=$(echo "$input" | jq -r '.tool_input.command // empty')
|
||||||
|
|
||||||
|
# === HARD BLOCKS - Never allow these ===
|
||||||
|
|
||||||
|
# Block rm -rf, rm -r (except for known safe paths like node_modules, vendor, .cache)
|
||||||
|
if echo "$command" | grep -qE 'rm\s+(-[a-zA-Z]*r[a-zA-Z]*|-[a-zA-Z]*f[a-zA-Z]*r|--recursive)'; then
|
||||||
|
# Allow only specific safe directories
|
||||||
|
if ! echo "$command" | grep -qE 'rm\s+(-rf|-r)\s+(node_modules|vendor|\.cache|dist|build|__pycache__|\.pytest_cache|/tmp/)'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: Recursive delete is not allowed. Delete files individually or ask the user to run this command."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block mv/cp with wildcards (mass file moves)
|
||||||
|
if echo "$command" | grep -qE '(mv|cp)\s+.*\*'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: Mass file move/copy with wildcards is not allowed. Move files individually."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block xargs with rm, mv, cp (mass operations)
|
||||||
|
if echo "$command" | grep -qE 'xargs\s+.*(rm|mv|cp)'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: xargs with file operations is not allowed. Too risky for mass changes."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block find -exec with rm, mv, cp
|
||||||
|
if echo "$command" | grep -qE 'find\s+.*-exec\s+.*(rm|mv|cp)'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: find -exec with file operations is not allowed. Too risky for mass changes."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block ALL sed -i (in-place editing)
|
||||||
|
if echo "$command" | grep -qE 'sed\s+(-[a-zA-Z]*i|--in-place)'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: sed -i (in-place edit) is never allowed. Use the Edit tool for file changes."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block sed piped to file operations
|
||||||
|
if echo "$command" | grep -qE 'sed.*\|.*tee|sed.*>'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: sed with file output is not allowed. Use the Edit tool for file changes."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block grep with -l piped to xargs/rm/sed (the classic codebase nuke pattern)
|
||||||
|
if echo "$command" | grep -qE 'grep\s+.*-l.*\|'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: grep -l piped to other commands is the classic codebase nuke pattern. Not allowed."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Block perl -i, awk with file redirection (sed alternatives)
|
||||||
|
if echo "$command" | grep -qE 'perl\s+-[a-zA-Z]*i|awk.*>'; then
|
||||||
|
echo '{"decision": "block", "message": "BLOCKED: In-place file editing with perl/awk is not allowed. Use the Edit tool."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === REQUIRE CORE CLI ===
|
||||||
|
|
||||||
|
# Block raw go commands
|
||||||
|
case "$command" in
|
||||||
|
"go test"*|"go build"*|"go fmt"*|"go mod tidy"*|"go vet"*|"go run"*)
|
||||||
|
echo '{"decision": "block", "message": "Use `core go test`, `core build`, `core go fmt --fix`, etc. Raw go commands are not allowed."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"go "*)
|
||||||
|
# Other go commands - warn but allow
|
||||||
|
echo '{"decision": "block", "message": "Prefer `core go *` commands. If core does not have this command, ask the user."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Block raw php commands
|
||||||
|
case "$command" in
|
||||||
|
"php artisan serve"*|"./vendor/bin/pest"*|"./vendor/bin/pint"*|"./vendor/bin/phpstan"*)
|
||||||
|
echo '{"decision": "block", "message": "Use `core php dev`, `core php test`, `core php fmt`, `core php analyse`. Raw php commands are not allowed."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
"composer test"*|"composer lint"*)
|
||||||
|
echo '{"decision": "block", "message": "Use `core php test` or `core php fmt`. Raw composer commands are not allowed."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Block golangci-lint directly
|
||||||
|
if echo "$command" | grep -qE '^golangci-lint'; then
|
||||||
|
echo '{"decision": "block", "message": "Use `core go lint` instead of golangci-lint directly."}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# === APPROVED ===
|
||||||
|
echo '{"decision": "approve"}'
|
||||||
102
plugin.json
Normal file
102
plugin.json
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
{
|
||||||
|
"name": "core",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Host UK unified framework - Go CLI, PHP framework, multi-repo management",
|
||||||
|
"dependencies": [
|
||||||
|
"superpowers@claude-plugins-official"
|
||||||
|
],
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"name": "core",
|
||||||
|
"path": "skills/core.md",
|
||||||
|
"description": "Use when working in host-uk repositories. Provides core CLI command reference."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "core-php",
|
||||||
|
"path": "skills/php.md",
|
||||||
|
"description": "Use when creating PHP modules, services, or actions in core-* packages."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "core-go",
|
||||||
|
"path": "skills/go.md",
|
||||||
|
"description": "Use when creating Go packages or extending the core CLI."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": "remember",
|
||||||
|
"path": "commands/remember.md",
|
||||||
|
"description": "Save a fact or decision to context"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"hooks": {
|
||||||
|
"SessionStart": [
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"script": "scripts/session-start.sh",
|
||||||
|
"description": "Check for recent session state on startup"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PreCompact": [
|
||||||
|
{
|
||||||
|
"matcher": "*",
|
||||||
|
"script": "scripts/pre-compact.sh",
|
||||||
|
"description": "Save state before auto-compact to prevent amnesia"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"script": "hooks/prefer-core.sh",
|
||||||
|
"description": "Suggest core CLI instead of raw go/php commands"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Write",
|
||||||
|
"script": "scripts/block-docs.sh",
|
||||||
|
"description": "Block random .md files, keep docs consolidated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"script": "scripts/suggest-compact.sh",
|
||||||
|
"description": "Suggest /compact at logical intervals"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Write",
|
||||||
|
"script": "scripts/suggest-compact.sh",
|
||||||
|
"description": "Suggest /compact at logical intervals"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"PostToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"script": "scripts/php-format.sh",
|
||||||
|
"description": "Auto-format PHP files after edits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"script": "scripts/go-format.sh",
|
||||||
|
"description": "Auto-format Go files after edits"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Edit",
|
||||||
|
"script": "scripts/check-debug.sh",
|
||||||
|
"description": "Warn about debug statements (dd, dump, fmt.Println)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"script": "scripts/pr-created.sh",
|
||||||
|
"description": "Log PR URL after creation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"script": "scripts/extract-actionables.sh",
|
||||||
|
"description": "Extract actionables from core CLI output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"matcher": "Bash",
|
||||||
|
"script": "scripts/post-commit-check.sh",
|
||||||
|
"description": "Warn about uncommitted work after git commit"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
27
scripts/block-docs.sh
Executable file
27
scripts/block-docs.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Block creation of random .md files - keeps docs consolidated
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_PATH" ]]; then
|
||||||
|
# Allow known documentation files
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
*README.md|*CLAUDE.md|*AGENTS.md|*CONTRIBUTING.md|*CHANGELOG.md|*LICENSE.md)
|
||||||
|
echo "$input"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
# Allow docs/ directory
|
||||||
|
*/docs/*.md|*/docs/**/*.md)
|
||||||
|
echo "$input"
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
# Block other .md files
|
||||||
|
*.md)
|
||||||
|
echo '{"decision": "block", "message": "Use README.md or docs/ for documentation. Random .md files clutter the repo."}'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$input"
|
||||||
44
scripts/capture-context.sh
Executable file
44
scripts/capture-context.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Capture context facts from tool output or conversation
|
||||||
|
# Called by PostToolUse hooks to extract actionable items
|
||||||
|
#
|
||||||
|
# Stores in ~/.claude/sessions/context.json as:
|
||||||
|
# [{"fact": "...", "source": "core go qa", "ts": 1234567890}, ...]
|
||||||
|
|
||||||
|
CONTEXT_FILE="${HOME}/.claude/sessions/context.json"
|
||||||
|
TIMESTAMP=$(date '+%s')
|
||||||
|
THREE_HOURS=10800
|
||||||
|
|
||||||
|
mkdir -p "${HOME}/.claude/sessions"
|
||||||
|
|
||||||
|
# Initialize if missing or stale
|
||||||
|
if [[ -f "$CONTEXT_FILE" ]]; then
|
||||||
|
FIRST_TS=$(jq -r '.[0].ts // 0' "$CONTEXT_FILE" 2>/dev/null)
|
||||||
|
NOW=$(date '+%s')
|
||||||
|
AGE=$((NOW - FIRST_TS))
|
||||||
|
if [[ $AGE -gt $THREE_HOURS ]]; then
|
||||||
|
echo "[]" > "$CONTEXT_FILE"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "[]" > "$CONTEXT_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Read input (fact and source passed as args or stdin)
|
||||||
|
FACT="${1:-}"
|
||||||
|
SOURCE="${2:-manual}"
|
||||||
|
|
||||||
|
if [[ -z "$FACT" ]]; then
|
||||||
|
# Try reading from stdin
|
||||||
|
read -r FACT
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$FACT" ]]; then
|
||||||
|
# Append to context (keep last 20 items)
|
||||||
|
jq --arg fact "$FACT" --arg source "$SOURCE" --argjson ts "$TIMESTAMP" \
|
||||||
|
'. + [{"fact": $fact, "source": $source, "ts": $ts}] | .[-20:]' \
|
||||||
|
"$CONTEXT_FILE" > "${CONTEXT_FILE}.tmp" && mv "${CONTEXT_FILE}.tmp" "$CONTEXT_FILE"
|
||||||
|
|
||||||
|
echo "[Context] Saved: $FACT" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
27
scripts/check-debug.sh
Executable file
27
scripts/check-debug.sh
Executable file
|
|
@ -0,0 +1,27 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Warn about debug statements left in code after edits
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
|
||||||
|
case "$FILE_PATH" in
|
||||||
|
*.go)
|
||||||
|
# Check for fmt.Println, log.Println debug statements
|
||||||
|
if grep -n "fmt\.Println\|log\.Println" "$FILE_PATH" 2>/dev/null | head -3 | grep -q .; then
|
||||||
|
echo "[Hook] WARNING: Debug prints found in $FILE_PATH" >&2
|
||||||
|
grep -n "fmt\.Println\|log\.Println" "$FILE_PATH" 2>/dev/null | head -3 >&2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*.php)
|
||||||
|
# Check for dd(), dump(), var_dump(), print_r()
|
||||||
|
if grep -n "dd(\|dump(\|var_dump(\|print_r(" "$FILE_PATH" 2>/dev/null | head -3 | grep -q .; then
|
||||||
|
echo "[Hook] WARNING: Debug statements found in $FILE_PATH" >&2
|
||||||
|
grep -n "dd(\|dump(\|var_dump(\|print_r(" "$FILE_PATH" 2>/dev/null | head -3 >&2
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pass through the input
|
||||||
|
echo "$input"
|
||||||
34
scripts/extract-actionables.sh
Executable file
34
scripts/extract-actionables.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Extract actionable items from core CLI output
|
||||||
|
# Called PostToolUse on Bash commands that run core
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
COMMAND=$(echo "$input" | jq -r '.tool_input.command // empty')
|
||||||
|
OUTPUT=$(echo "$input" | jq -r '.tool_output.output // empty')
|
||||||
|
|
||||||
|
CONTEXT_SCRIPT="$(dirname "$0")/capture-context.sh"
|
||||||
|
|
||||||
|
# Extract actionables from specific core commands
|
||||||
|
case "$COMMAND" in
|
||||||
|
"core go qa"*|"core go test"*|"core go lint"*)
|
||||||
|
# Extract error/warning lines
|
||||||
|
echo "$OUTPUT" | grep -E "^(ERROR|WARN|FAIL|---)" | head -5 | while read -r line; do
|
||||||
|
"$CONTEXT_SCRIPT" "$line" "core go"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
"core php test"*|"core php analyse"*)
|
||||||
|
# Extract PHP errors
|
||||||
|
echo "$OUTPUT" | grep -E "^(FAIL|Error|×)" | head -5 | while read -r line; do
|
||||||
|
"$CONTEXT_SCRIPT" "$line" "core php"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
"core build"*)
|
||||||
|
# Extract build errors
|
||||||
|
echo "$OUTPUT" | grep -E "^(error|cannot|undefined)" | head -5 | while read -r line; do
|
||||||
|
"$CONTEXT_SCRIPT" "$line" "core build"
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Pass through
|
||||||
|
echo "$input"
|
||||||
19
scripts/go-format.sh
Executable file
19
scripts/go-format.sh
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Auto-format Go files after edits using core go fmt
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
|
||||||
|
# Run gofmt/goimports on the file silently
|
||||||
|
if command -v core &> /dev/null; then
|
||||||
|
core go fmt --fix "$FILE_PATH" 2>/dev/null || true
|
||||||
|
elif command -v goimports &> /dev/null; then
|
||||||
|
goimports -w "$FILE_PATH" 2>/dev/null || true
|
||||||
|
elif command -v gofmt &> /dev/null; then
|
||||||
|
gofmt -w "$FILE_PATH" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pass through the input
|
||||||
|
echo "$input"
|
||||||
17
scripts/php-format.sh
Executable file
17
scripts/php-format.sh
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Auto-format PHP files after edits using core php fmt
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
FILE_PATH=$(echo "$input" | jq -r '.tool_input.file_path // empty')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_PATH" && -f "$FILE_PATH" ]]; then
|
||||||
|
# Run Pint on the file silently
|
||||||
|
if command -v core &> /dev/null; then
|
||||||
|
core php fmt --fix "$FILE_PATH" 2>/dev/null || true
|
||||||
|
elif [[ -f "./vendor/bin/pint" ]]; then
|
||||||
|
./vendor/bin/pint "$FILE_PATH" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pass through the input
|
||||||
|
echo "$input"
|
||||||
51
scripts/post-commit-check.sh
Executable file
51
scripts/post-commit-check.sh
Executable file
|
|
@ -0,0 +1,51 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Post-commit hook: Check for uncommitted work that might get lost
|
||||||
|
#
|
||||||
|
# After committing task-specific files, check if there's other work
|
||||||
|
# in the repo that should be committed or stashed
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
COMMAND=$(echo "$input" | jq -r '.tool_input.command // empty')
|
||||||
|
|
||||||
|
# Only run after git commit
|
||||||
|
if ! echo "$COMMAND" | grep -qE '^git commit'; then
|
||||||
|
echo "$input"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check for remaining uncommitted changes
|
||||||
|
UNSTAGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
UNTRACKED=$(git ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
|
||||||
|
|
||||||
|
TOTAL=$((UNSTAGED + STAGED + UNTRACKED))
|
||||||
|
|
||||||
|
if [[ $TOTAL -gt 0 ]]; then
|
||||||
|
echo "" >&2
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
||||||
|
echo "[PostCommit] WARNING: Uncommitted work remains" >&2
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
||||||
|
|
||||||
|
if [[ $UNSTAGED -gt 0 ]]; then
|
||||||
|
echo " Modified (unstaged): $UNSTAGED files" >&2
|
||||||
|
git diff --name-only 2>/dev/null | head -5 | sed 's/^/ /' >&2
|
||||||
|
[[ $UNSTAGED -gt 5 ]] && echo " ... and $((UNSTAGED - 5)) more" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $STAGED -gt 0 ]]; then
|
||||||
|
echo " Staged (not committed): $STAGED files" >&2
|
||||||
|
git diff --cached --name-only 2>/dev/null | head -5 | sed 's/^/ /' >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $UNTRACKED -gt 0 ]]; then
|
||||||
|
echo " Untracked: $UNTRACKED files" >&2
|
||||||
|
git ls-files --others --exclude-standard 2>/dev/null | head -5 | sed 's/^/ /' >&2
|
||||||
|
[[ $UNTRACKED -gt 5 ]] && echo " ... and $((UNTRACKED - 5)) more" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "" >&2
|
||||||
|
echo "Consider: commit these, stash them, or confirm they're intentionally left" >&2
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$input"
|
||||||
18
scripts/pr-created.sh
Executable file
18
scripts/pr-created.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Log PR URL and provide review command after PR creation
|
||||||
|
|
||||||
|
read -r input
|
||||||
|
COMMAND=$(echo "$input" | jq -r '.tool_input.command // empty')
|
||||||
|
OUTPUT=$(echo "$input" | jq -r '.tool_output.output // empty')
|
||||||
|
|
||||||
|
if [[ "$COMMAND" == *"gh pr create"* ]]; then
|
||||||
|
PR_URL=$(echo "$OUTPUT" | grep -oE 'https://github.com/[^/]+/[^/]+/pull/[0-9]+' | head -1)
|
||||||
|
if [[ -n "$PR_URL" ]]; then
|
||||||
|
REPO=$(echo "$PR_URL" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/[0-9]+|\1|')
|
||||||
|
PR_NUM=$(echo "$PR_URL" | sed -E 's|.*/pull/([0-9]+)|\1|')
|
||||||
|
echo "[Hook] PR created: $PR_URL" >&2
|
||||||
|
echo "[Hook] To review: gh pr review $PR_NUM --repo $REPO" >&2
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$input"
|
||||||
69
scripts/pre-compact.sh
Executable file
69
scripts/pre-compact.sh
Executable file
|
|
@ -0,0 +1,69 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Pre-compact: Save minimal state for Claude to resume after auto-compact
|
||||||
|
#
|
||||||
|
# Captures:
|
||||||
|
# - Working directory + branch
|
||||||
|
# - Git status (files touched)
|
||||||
|
# - Todo state (in_progress items)
|
||||||
|
# - Context facts (decisions, actionables)
|
||||||
|
|
||||||
|
STATE_FILE="${HOME}/.claude/sessions/scratchpad.md"
|
||||||
|
CONTEXT_FILE="${HOME}/.claude/sessions/context.json"
|
||||||
|
TIMESTAMP=$(date '+%s')
|
||||||
|
CWD=$(pwd)
|
||||||
|
|
||||||
|
mkdir -p "${HOME}/.claude/sessions"
|
||||||
|
|
||||||
|
# Get todo state
|
||||||
|
TODOS=""
|
||||||
|
if [[ -f "${HOME}/.claude/todos/current.json" ]]; then
|
||||||
|
TODOS=$(cat "${HOME}/.claude/todos/current.json" 2>/dev/null | head -50)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get git status
|
||||||
|
GIT_STATUS=""
|
||||||
|
BRANCH=""
|
||||||
|
if git rev-parse --git-dir > /dev/null 2>&1; then
|
||||||
|
GIT_STATUS=$(git status --short 2>/dev/null | head -15)
|
||||||
|
BRANCH=$(git branch --show-current 2>/dev/null)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get context facts
|
||||||
|
CONTEXT=""
|
||||||
|
if [[ -f "$CONTEXT_FILE" ]]; then
|
||||||
|
CONTEXT=$(jq -r '.[] | "- [\(.source)] \(.fact)"' "$CONTEXT_FILE" 2>/dev/null | tail -10)
|
||||||
|
fi
|
||||||
|
|
||||||
|
cat > "$STATE_FILE" << EOF
|
||||||
|
---
|
||||||
|
timestamp: ${TIMESTAMP}
|
||||||
|
cwd: ${CWD}
|
||||||
|
branch: ${BRANCH:-none}
|
||||||
|
---
|
||||||
|
|
||||||
|
# Resume After Compact
|
||||||
|
|
||||||
|
You were mid-task. Do NOT assume work is complete.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
\`${CWD}\` on \`${BRANCH:-no branch}\`
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
\`\`\`
|
||||||
|
${GIT_STATUS:-none}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Todos (in_progress = NOT done)
|
||||||
|
\`\`\`json
|
||||||
|
${TODOS:-check /todos}
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Context (decisions & actionables)
|
||||||
|
${CONTEXT:-none captured}
|
||||||
|
|
||||||
|
## Next
|
||||||
|
Continue the in_progress todo.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "[PreCompact] Snapshot saved" >&2
|
||||||
|
exit 0
|
||||||
34
scripts/session-start.sh
Executable file
34
scripts/session-start.sh
Executable file
|
|
@ -0,0 +1,34 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Session start: Read scratchpad if recent, otherwise start fresh
|
||||||
|
# 3 hour window - if older, you've moved on mentally
|
||||||
|
|
||||||
|
STATE_FILE="${HOME}/.claude/sessions/scratchpad.md"
|
||||||
|
THREE_HOURS=10800 # seconds
|
||||||
|
|
||||||
|
if [[ -f "$STATE_FILE" ]]; then
|
||||||
|
# Get timestamp from file
|
||||||
|
FILE_TS=$(grep -E '^timestamp:' "$STATE_FILE" 2>/dev/null | cut -d' ' -f2)
|
||||||
|
NOW=$(date '+%s')
|
||||||
|
|
||||||
|
if [[ -n "$FILE_TS" ]]; then
|
||||||
|
AGE=$((NOW - FILE_TS))
|
||||||
|
|
||||||
|
if [[ $AGE -lt $THREE_HOURS ]]; then
|
||||||
|
# Recent - read it back
|
||||||
|
echo "[SessionStart] Found recent scratchpad ($(($AGE / 60)) min ago)" >&2
|
||||||
|
echo "[SessionStart] Reading previous state..." >&2
|
||||||
|
echo "" >&2
|
||||||
|
cat "$STATE_FILE" >&2
|
||||||
|
echo "" >&2
|
||||||
|
else
|
||||||
|
# Stale - delete and start fresh
|
||||||
|
rm -f "$STATE_FILE"
|
||||||
|
echo "[SessionStart] Previous session >3h old - starting fresh" >&2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
# No timestamp, delete it
|
||||||
|
rm -f "$STATE_FILE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
28
scripts/suggest-compact.sh
Executable file
28
scripts/suggest-compact.sh
Executable file
|
|
@ -0,0 +1,28 @@
|
||||||
|
#!/bin/bash
|
||||||
|
# Suggest /compact at logical intervals to manage context window
|
||||||
|
# Tracks tool calls per session, suggests compaction every 50 calls
|
||||||
|
|
||||||
|
SESSION_ID="${CLAUDE_SESSION_ID:-$$}"
|
||||||
|
COUNTER_FILE="/tmp/claude-tool-count-${SESSION_ID}"
|
||||||
|
THRESHOLD="${COMPACT_THRESHOLD:-50}"
|
||||||
|
|
||||||
|
# Read or initialize counter
|
||||||
|
if [[ -f "$COUNTER_FILE" ]]; then
|
||||||
|
COUNT=$(($(cat "$COUNTER_FILE") + 1))
|
||||||
|
else
|
||||||
|
COUNT=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$COUNT" > "$COUNTER_FILE"
|
||||||
|
|
||||||
|
# Suggest compact at threshold
|
||||||
|
if [[ $COUNT -eq $THRESHOLD ]]; then
|
||||||
|
echo "[Compact] ${THRESHOLD} tool calls - consider /compact if transitioning phases" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Suggest at intervals after threshold
|
||||||
|
if [[ $COUNT -gt $THRESHOLD ]] && [[ $((COUNT % 25)) -eq 0 ]]; then
|
||||||
|
echo "[Compact] ${COUNT} tool calls - good checkpoint for /compact" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
|
exit 0
|
||||||
60
skills/core.md
Normal file
60
skills/core.md
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
---
|
||||||
|
name: core
|
||||||
|
description: Use when working in host-uk repositories, running tests, building, releasing, or managing multi-repo workflows. Provides the core CLI command reference.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Core CLI
|
||||||
|
|
||||||
|
The `core` command provides a unified interface for Go/PHP development and multi-repo management.
|
||||||
|
|
||||||
|
**Rule:** Always prefer `core <command>` over raw commands.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Go tests | `core go test` |
|
||||||
|
| Go coverage | `core go cov` |
|
||||||
|
| Go format | `core go fmt --fix` |
|
||||||
|
| Go lint | `core go lint` |
|
||||||
|
| PHP dev server | `core php dev` |
|
||||||
|
| PHP tests | `core php test` |
|
||||||
|
| PHP format | `core php fmt --fix` |
|
||||||
|
| Build | `core build` |
|
||||||
|
| Preview release | `core ci` |
|
||||||
|
| Publish | `core ci --were-go-for-launch` |
|
||||||
|
| Multi-repo status | `core dev health` |
|
||||||
|
| Commit dirty repos | `core dev commit` |
|
||||||
|
| Push repos | `core dev push` |
|
||||||
|
|
||||||
|
## Decision Tree
|
||||||
|
|
||||||
|
```
|
||||||
|
Go project?
|
||||||
|
tests: core go test
|
||||||
|
format: core go fmt --fix
|
||||||
|
build: core build
|
||||||
|
|
||||||
|
PHP project?
|
||||||
|
dev: core php dev
|
||||||
|
tests: core php test
|
||||||
|
format: core php fmt --fix
|
||||||
|
deploy: core php deploy
|
||||||
|
|
||||||
|
Multiple repos?
|
||||||
|
status: core dev health
|
||||||
|
commit: core dev commit
|
||||||
|
push: core dev push
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
| Wrong | Right |
|
||||||
|
|-------|-------|
|
||||||
|
| `go test ./...` | `core go test` |
|
||||||
|
| `go build` | `core build` |
|
||||||
|
| `php artisan serve` | `core php dev` |
|
||||||
|
| `./vendor/bin/pest` | `core php test` |
|
||||||
|
| `git status` per repo | `core dev health` |
|
||||||
|
|
||||||
|
Run `core --help` or `core <cmd> --help` for full options.
|
||||||
107
skills/go.md
Normal file
107
skills/go.md
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
---
|
||||||
|
name: core-go
|
||||||
|
description: Use when creating Go packages or extending the core CLI.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Go Framework Patterns
|
||||||
|
|
||||||
|
Core CLI uses `pkg/` for reusable packages. Use `core go` commands.
|
||||||
|
|
||||||
|
## Package Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
core/
|
||||||
|
├── main.go # CLI entry point
|
||||||
|
├── pkg/
|
||||||
|
│ ├── cli/ # CLI framework, output, errors
|
||||||
|
│ ├── {domain}/ # Domain package
|
||||||
|
│ │ ├── cmd_{name}.go # Cobra command definitions
|
||||||
|
│ │ ├── service.go # Business logic
|
||||||
|
│ │ └── *_test.go # Tests
|
||||||
|
│ └── ...
|
||||||
|
└── internal/ # Private packages
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a CLI Command
|
||||||
|
|
||||||
|
1. Create `pkg/{domain}/cmd_{name}.go`:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package domain
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/host-uk/core/pkg/cli"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewNameCmd() *cobra.Command {
|
||||||
|
cmd := &cobra.Command{
|
||||||
|
Use: "name",
|
||||||
|
Short: cli.T("domain.name.short"),
|
||||||
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
|
// Implementation
|
||||||
|
cli.Success("Done")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return cmd
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Register in parent command.
|
||||||
|
|
||||||
|
## CLI Output Helpers
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "github.com/host-uk/core/pkg/cli"
|
||||||
|
|
||||||
|
cli.Success("Operation completed") // Green check
|
||||||
|
cli.Warning("Something to note") // Yellow warning
|
||||||
|
cli.Error("Something failed") // Red error
|
||||||
|
cli.Info("Informational message") // Blue info
|
||||||
|
cli.Fatal(err) // Print error and exit 1
|
||||||
|
|
||||||
|
// Structured output
|
||||||
|
cli.Table(headers, rows)
|
||||||
|
cli.JSON(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
## i18n Pattern
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use cli.T() for translatable strings
|
||||||
|
cli.T("domain.action.success")
|
||||||
|
cli.T("domain.action.error", "details", value)
|
||||||
|
|
||||||
|
// Define in pkg/i18n/locales/en.yaml:
|
||||||
|
domain:
|
||||||
|
action:
|
||||||
|
success: "Operation completed successfully"
|
||||||
|
error: "Failed: {{.details}}"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Test Naming
|
||||||
|
|
||||||
|
```go
|
||||||
|
func TestFeature_Good(t *testing.T) { /* happy path */ }
|
||||||
|
func TestFeature_Bad(t *testing.T) { /* expected errors */ }
|
||||||
|
func TestFeature_Ugly(t *testing.T) { /* panics, edge cases */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Run tests | `core go test` |
|
||||||
|
| Coverage | `core go cov` |
|
||||||
|
| Format | `core go fmt --fix` |
|
||||||
|
| Lint | `core go lint` |
|
||||||
|
| Build | `core build` |
|
||||||
|
| Install | `core go install` |
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- `CGO_ENABLED=0` for all builds
|
||||||
|
- UK English in user-facing strings
|
||||||
|
- All errors via `cli.E("context", "message", err)`
|
||||||
|
- Table-driven tests preferred
|
||||||
120
skills/php.md
Normal file
120
skills/php.md
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
---
|
||||||
|
name: core-php
|
||||||
|
description: Use when creating PHP modules, services, or actions in core-* packages.
|
||||||
|
---
|
||||||
|
|
||||||
|
# PHP Framework Patterns
|
||||||
|
|
||||||
|
Host UK PHP modules follow strict conventions. Use `core php` commands.
|
||||||
|
|
||||||
|
## Module Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
core-{name}/
|
||||||
|
├── src/
|
||||||
|
│ ├── Core/ # Namespace: Core\{Name}
|
||||||
|
│ │ ├── Boot.php # Module bootstrap (listens to lifecycle events)
|
||||||
|
│ │ ├── Actions/ # Single-purpose business logic
|
||||||
|
│ │ └── Models/ # Eloquent models
|
||||||
|
│ └── Mod/ # Namespace: Core\Mod\{Name} (optional extensions)
|
||||||
|
├── resources/views/ # Blade templates
|
||||||
|
├── routes/ # Route definitions
|
||||||
|
├── database/migrations/ # Migrations
|
||||||
|
├── tests/ # Pest tests
|
||||||
|
└── composer.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Boot Class Pattern
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\{Name};
|
||||||
|
|
||||||
|
use Core\Php\Events\WebRoutesRegistering;
|
||||||
|
use Core\Php\Events\AdminPanelBooting;
|
||||||
|
|
||||||
|
class Boot
|
||||||
|
{
|
||||||
|
public static array $listens = [
|
||||||
|
WebRoutesRegistering::class => 'onWebRoutes',
|
||||||
|
AdminPanelBooting::class => ['onAdmin', 10], // With priority
|
||||||
|
];
|
||||||
|
|
||||||
|
public function onWebRoutes(WebRoutesRegistering $event): void
|
||||||
|
{
|
||||||
|
$event->router->middleware('web')->group(__DIR__ . '/../routes/web.php');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAdmin(AdminPanelBooting $event): void
|
||||||
|
{
|
||||||
|
$event->panel->resources([...]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Action Pattern
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\{Name}\Actions;
|
||||||
|
|
||||||
|
use Core\Php\Action;
|
||||||
|
|
||||||
|
class CreateThing
|
||||||
|
{
|
||||||
|
use Action;
|
||||||
|
|
||||||
|
public function handle(User $user, array $data): Thing
|
||||||
|
{
|
||||||
|
return Thing::create([
|
||||||
|
'user_id' => $user->id,
|
||||||
|
...$data,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage: CreateThing::run($user, $validated);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Multi-Tenant Models
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Core\{Name}\Models;
|
||||||
|
|
||||||
|
use Core\Tenant\Concerns\BelongsToWorkspace;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
|
class Thing extends Model
|
||||||
|
{
|
||||||
|
use BelongsToWorkspace; // Auto-scopes queries, sets workspace_id
|
||||||
|
|
||||||
|
protected $fillable = ['name', 'workspace_id'];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
| Task | Command |
|
||||||
|
|------|---------|
|
||||||
|
| Run tests | `core php test` |
|
||||||
|
| Format | `core php fmt --fix` |
|
||||||
|
| Analyse | `core php analyse` |
|
||||||
|
| Dev server | `core php dev` |
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Always `declare(strict_types=1);`
|
||||||
|
- UK English: colour, organisation, centre
|
||||||
|
- Type hints on all parameters and returns
|
||||||
|
- Pest for tests, not PHPUnit
|
||||||
|
- Flux Pro for UI, not vanilla Alpine
|
||||||
Reference in a new issue