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:
Snider 2026-02-01 02:36:33 +00:00
commit 0a89ac91d1
18 changed files with 936 additions and 0 deletions

41
README.md Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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