commit 0a89ac91d14f5bc28a89b9b3b798e4ac3e30cae6 Author: Snider Date: Sun Feb 1 02:36:33 2026 +0000 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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..bf8c1b3 --- /dev/null +++ b/README.md @@ -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 ` - 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 \ No newline at end of file diff --git a/commands/remember.md b/commands/remember.md new file mode 100644 index 0000000..41b8eff --- /dev/null +++ b/commands/remember.md @@ -0,0 +1,36 @@ +--- +name: remember +description: Save a fact or decision to context for persistence across compacts +args: +--- + +# 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 "" "user" +``` + +Or if running from the plugin directory: + +```bash +"${CLAUDE_PLUGIN_ROOT}/scripts/capture-context.sh" "" "user" +``` + +The fact will be: +- Stored in context.json (max 20 items) +- Included in pre-compact snapshots +- Auto-cleared after 3 hours of inactivity diff --git a/hooks/prefer-core.sh b/hooks/prefer-core.sh new file mode 100755 index 0000000..52ce773 --- /dev/null +++ b/hooks/prefer-core.sh @@ -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"}' diff --git a/plugin.json b/plugin.json new file mode 100644 index 0000000..2f79b85 --- /dev/null +++ b/plugin.json @@ -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" + } + ] + } +} diff --git a/scripts/block-docs.sh b/scripts/block-docs.sh new file mode 100755 index 0000000..dfac1da --- /dev/null +++ b/scripts/block-docs.sh @@ -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" diff --git a/scripts/capture-context.sh b/scripts/capture-context.sh new file mode 100755 index 0000000..288e9be --- /dev/null +++ b/scripts/capture-context.sh @@ -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 diff --git a/scripts/check-debug.sh b/scripts/check-debug.sh new file mode 100755 index 0000000..079cc0e --- /dev/null +++ b/scripts/check-debug.sh @@ -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" diff --git a/scripts/extract-actionables.sh b/scripts/extract-actionables.sh new file mode 100755 index 0000000..86a2bbb --- /dev/null +++ b/scripts/extract-actionables.sh @@ -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" diff --git a/scripts/go-format.sh b/scripts/go-format.sh new file mode 100755 index 0000000..8f9d322 --- /dev/null +++ b/scripts/go-format.sh @@ -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" diff --git a/scripts/php-format.sh b/scripts/php-format.sh new file mode 100755 index 0000000..e0e7ec1 --- /dev/null +++ b/scripts/php-format.sh @@ -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" diff --git a/scripts/post-commit-check.sh b/scripts/post-commit-check.sh new file mode 100755 index 0000000..42418b6 --- /dev/null +++ b/scripts/post-commit-check.sh @@ -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" diff --git a/scripts/pr-created.sh b/scripts/pr-created.sh new file mode 100755 index 0000000..82dd975 --- /dev/null +++ b/scripts/pr-created.sh @@ -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" diff --git a/scripts/pre-compact.sh b/scripts/pre-compact.sh new file mode 100755 index 0000000..bb9d841 --- /dev/null +++ b/scripts/pre-compact.sh @@ -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 diff --git a/scripts/session-start.sh b/scripts/session-start.sh new file mode 100755 index 0000000..3a44d97 --- /dev/null +++ b/scripts/session-start.sh @@ -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 diff --git a/scripts/suggest-compact.sh b/scripts/suggest-compact.sh new file mode 100755 index 0000000..e958c50 --- /dev/null +++ b/scripts/suggest-compact.sh @@ -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 diff --git a/skills/core.md b/skills/core.md new file mode 100644 index 0000000..966d7e9 --- /dev/null +++ b/skills/core.md @@ -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 ` 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 --help` for full options. diff --git a/skills/go.md b/skills/go.md new file mode 100644 index 0000000..22a2227 --- /dev/null +++ b/skills/go.md @@ -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 diff --git a/skills/php.md b/skills/php.md new file mode 100644 index 0000000..2133a20 --- /dev/null +++ b/skills/php.md @@ -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 + '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 + $user->id, + ...$data, + ]); + } +} + +// Usage: CreateThing::run($user, $validated); +``` + +## Multi-Tenant Models + +```php +