diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c154dba..c063604 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -161,6 +161,15 @@ "author": { "name": "JacobPEvans" } + }, + { + "name": "skill-guards", + "source": "./skill-guards", + "description": "Ensures fresh execution of skills on every invocation via UserPromptSubmit hook", + "version": "2.3.2", + "author": { + "name": "JacobPEvans" + } } ] } diff --git a/AGENTS.md b/AGENTS.md index b13b97e..1a731e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,6 +26,7 @@ This is a **Claude Code plugins repository** containing production-ready hooks f | **process-cleanup** | PostToolUse | — | Cleanup orphaned MCP server processes on session exit | | **project-standards** | Skill | `/agentsmd-authoring`, `/workspace-standards`, `/skills-registry` | AgentsMD authoring standards, workspace management, and skills/tools registry lookup | | **session-analytics** | Skill | `/token-breakdown` | Session token analytics via Splunk OTEL telemetry | +| **skill-guards** | UserPromptSubmit | — | Ensures fresh execution of skills on every invocation | ## Multi-Model Delegation diff --git a/cspell.json b/cspell.json index eefef18..35b0384 100644 --- a/cspell.json +++ b/cspell.json @@ -37,6 +37,7 @@ "ENDJSON", "RLENGTH", "unrecognised", - "tokenisation" + "tokenisation", + "reinvocation" ] } diff --git a/git-workflows/skills/refresh-repo/SKILL.md b/git-workflows/skills/refresh-repo/SKILL.md index df49e3a..4961b24 100644 --- a/git-workflows/skills/refresh-repo/SKILL.md +++ b/git-workflows/skills/refresh-repo/SKILL.md @@ -8,6 +8,9 @@ description: Check PR merge readiness, sync local repo, and cleanup stale worktr Check open PR merge-readiness status, sync the local repository, and cleanup stale worktrees. **Note**: Does not automatically merge PRs - only reports readiness status for each PR. +> **State warning**: Branch state, remote tracking, and PR status change between +> invocations. Re-run all git/gh commands from Step 1. + ## Steps ### 1. Identify Open PRs diff --git a/git-workflows/skills/wrap-up/SKILL.md b/git-workflows/skills/wrap-up/SKILL.md index 08d9831..bb90652 100644 --- a/git-workflows/skills/wrap-up/SKILL.md +++ b/git-workflows/skills/wrap-up/SKILL.md @@ -5,6 +5,9 @@ description: "End-of-session cleanup after PR merge: refresh repo, run quick ret # Post-Merge Wrap-Up +> **State warning**: Branch state, remote tracking, and PR status change between +> invocations. Re-run all git/gh commands from Step 1. + Run Steps 1 and 2 **in parallel** (they are independent). Step 3 starts as soon as Step 1 completes (depends on its remote prune). Step 4 runs after all prior steps finish. Provide a summary of actions taken. diff --git a/github-workflows/skills/finalize-pr/SKILL.md b/github-workflows/skills/finalize-pr/SKILL.md index 6a31588..f0d1c62 100644 --- a/github-workflows/skills/finalize-pr/SKILL.md +++ b/github-workflows/skills/finalize-pr/SKILL.md @@ -16,6 +16,10 @@ metadata: **FULLY AUTOMATIC** - Fully automates PR finalization: monitor, fix, prepare for merge. Assumes PR already exists. No manual intervention required. For manual review-focused workflows, use `/review-pr`. +> **State warning**: Automated reviewers (CodeQL, Copilot, AI reviews) post +> asynchronously. CI may have re-run. Merge conflicts may have appeared. +> Re-fetch live PR state from Step 1. + ## Critical Rules 1. **Wait for user approval to merge** - Report ready status, then pause for user merge command diff --git a/github-workflows/skills/resolve-pr-threads/SKILL.md b/github-workflows/skills/resolve-pr-threads/SKILL.md index 430d440..9f1e3cd 100644 --- a/github-workflows/skills/resolve-pr-threads/SKILL.md +++ b/github-workflows/skills/resolve-pr-threads/SKILL.md @@ -20,6 +20,9 @@ Orchestrates resolution of all unresolved PR review comments by grouping related threads, processing each group sequentially inline to implement fixes or provide explanations, then resolving threads via GitHub's GraphQL API. +> **State warning**: Thread IDs and resolution status change as reviews arrive. +> Re-fetch all open threads — cached thread lists from earlier are unreliable. + ## Usage ```text diff --git a/github-workflows/skills/ship/SKILL.md b/github-workflows/skills/ship/SKILL.md index 1fc16dc..7033d47 100644 --- a/github-workflows/skills/ship/SKILL.md +++ b/github-workflows/skills/ship/SKILL.md @@ -11,6 +11,10 @@ allowed-tools: Bash(git *), Bash(gh *), Bash(pre-commit *), Bash(npm run lint*), **Single command to commit, push, create PR(s), and auto-finalize everything.** Handles commit, push, PR creation, and `/finalize-pr` in one pipeline. Never merges. +> **State warning**: Automated reviewers (CodeQL, Copilot, AI reviews) post +> asynchronously. CI may have re-run. Merge conflicts may have appeared. +> Re-fetch live PR state from Step 1. + ## Rate Limit Awareness This skill orchestrates many downstream API calls via `/finalize-pr`, diff --git a/github-workflows/skills/squash-merge-pr/SKILL.md b/github-workflows/skills/squash-merge-pr/SKILL.md index 627f378..ea17467 100644 --- a/github-workflows/skills/squash-merge-pr/SKILL.md +++ b/github-workflows/skills/squash-merge-pr/SKILL.md @@ -14,6 +14,9 @@ Validates PR readiness and executes squash merge. If the PR is not ready, errors immediately and suggests `/finalize-pr` to fix issues. Never fixes issues — only merges. +> **State warning**: Branch state, remote tracking, and PR status change between +> invocations. Re-run all git/gh commands from Step 1. + ## Critical Rules - **Never fix issues** — only merge. If PR isn't ready, error and suggest `/finalize-pr` diff --git a/skill-guards/.claude-plugin/plugin.json b/skill-guards/.claude-plugin/plugin.json new file mode 100644 index 0000000..f472e1e --- /dev/null +++ b/skill-guards/.claude-plugin/plugin.json @@ -0,0 +1,9 @@ +{ + "name": "skill-guards", + "version": "2.3.2", + "description": "Ensures fresh execution of skills on every invocation via UserPromptSubmit hook", + "author": {"name": "JacobPEvans"}, + "homepage": "https://github.com/JacobPEvans/claude-code-plugins", + "keywords": ["skills", "execution", "guards", "hooks"], + "license": "Apache-2.0" +} diff --git a/skill-guards/README.md b/skill-guards/README.md new file mode 100644 index 0000000..75c4971 --- /dev/null +++ b/skill-guards/README.md @@ -0,0 +1,38 @@ +# skill-guards + +Ensures fresh execution of skills on every invocation via a UserPromptSubmit hook. + +## Problem + +When a skill like `/ship` is called a second time in a session, Claude may shortcut +by assuming previous results are still valid instead of re-running all commands against +live state. + +## Solution + +A UserPromptSubmit hook detects `/skill-name` patterns in user prompts and injects a +systemMessage reminding Claude to execute every step from scratch using current live state. + +## Installation + +```bash +claude plugins add jacobpevans-cc-plugins/skill-guards +``` + +## Usage + +No manual invocation required. The hook activates automatically on every user prompt. + +## Hook Behavior + +- **Fires on**: Every user prompt submission +- **Detects**: `/lowercase-with-hyphens` skill invocation patterns +- **Excludes**: Common filesystem paths (`/usr`, `/tmp`, `/etc`, `/nix`, etc.) +- **Output**: `{"systemMessage": "FRESH EXECUTION: /skill — ..."}` or `{}` +- **Exit code**: Always 0 (never blocks prompts) + +## Part of a Three-Layer System + +1. **Global rule** (`skill-execution-integrity`) — establishes the mental model +2. **This hook** — tactical trigger at moment of invocation +3. **Skill preambles** — skill-specific state warnings in SKILL.md files diff --git a/skill-guards/hooks/hooks.json b/skill-guards/hooks/hooks.json new file mode 100644 index 0000000..e327a88 --- /dev/null +++ b/skill-guards/hooks/hooks.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "UserPromptSubmit": [ + { + "matcher": "*", + "hooks": [ + { + "type": "command", + "command": "${CLAUDE_PLUGIN_ROOT}/scripts/skill-reinvocation-guard.sh", + "timeout": 3 + } + ] + } + ] + } +} diff --git a/skill-guards/scripts/skill-reinvocation-guard.sh b/skill-guards/scripts/skill-reinvocation-guard.sh new file mode 100755 index 0000000..cf3beaf --- /dev/null +++ b/skill-guards/scripts/skill-reinvocation-guard.sh @@ -0,0 +1,21 @@ +#!/bin/bash +# UserPromptSubmit hook — inject fresh-execution reminder on skill invocation. + +input=$(cat) +prompt=$(jq -r '.tool_input.prompt // .tool_input.content // .prompt // .content // empty' <<< "$input" 2>/dev/null) || { printf '{}'; exit 0; } + +[[ -z "$prompt" ]] && { printf '{}'; exit 0; } + +if [[ "$prompt" =~ (^|[[:space:]])/([a-z][a-z0-9-]+)([^a-z0-9-]|$) ]]; then + skill_name="${BASH_REMATCH[2]}" + case "$skill_name" in + usr|tmp|etc|var|bin|dev|opt|home|nix|proc|sys|run|lib|mnt|srv|boot|root|sbin) printf '{}'; exit 0 ;; + esac + jq -n --arg skill "$skill_name" '{ + systemMessage: ("FRESH EXECUTION: /" + $skill + " — Step 1 now. New invocation, new live state. All prior outputs in this session are stale. Re-run every git, gh, and API command.") + }' || printf '{"systemMessage":"FRESH EXECUTION: /%s — Step 1 now. New invocation, new live state. All prior outputs in this session are stale. Re-run every git, gh, and API command."}' "$skill_name" +else + printf '{}' +fi + +exit 0 diff --git a/tests/skill-guards/skill-reinvocation-guard/skill-reinvocation-guard.bats b/tests/skill-guards/skill-reinvocation-guard/skill-reinvocation-guard.bats new file mode 100644 index 0000000..99951f3 --- /dev/null +++ b/tests/skill-guards/skill-reinvocation-guard/skill-reinvocation-guard.bats @@ -0,0 +1,133 @@ +#!/usr/bin/env bats +# Test suite for skill-guards/scripts/skill-reinvocation-guard.sh +# +# Tests the UserPromptSubmit hook's skill detection and systemMessage injection: +# - No skill in prompt → empty JSON, exit 0 +# - /ship → systemMessage with "FRESH EXECUTION" and skill name +# - /finalize-pr 42 → systemMessage with skill name +# - Regular text → empty JSON, exit 0 +# - Filesystem path /usr/bin → empty JSON (false positive exclusion) +# - Empty prompt → empty JSON, exit 0 +# +# Run with: bats tests/skill-guards/skill-reinvocation-guard/skill-reinvocation-guard.bats + +setup() { + REPO_ROOT="$(cd "$(dirname "$BATS_TEST_FILENAME")/../../.." && pwd)" + SCRIPT="$REPO_ROOT/skill-guards/scripts/skill-reinvocation-guard.sh" + + if [[ ! -f "$SCRIPT" ]]; then + echo "ERROR: Script not found at $SCRIPT" >&2 + return 1 + fi +} + +# Run the hook with a given prompt string (uses jq for safe JSON construction) +run_hook_with_prompt() { + local prompt="$1" + run bash -c 'jq -n --arg p "$1" "{tool_input:{prompt:\$p}}" | /bin/bash "$2"' -- "$prompt" "$SCRIPT" +} + +# --------------------------------------------------------------------------- +# TC1: No skill invocation → empty JSON, exit 0 +# --------------------------------------------------------------------------- + +@test "TC1: prompt without skill outputs empty JSON" { + run_hook_with_prompt "help me fix this bug" + [ "$status" -eq 0 ] + [[ "$output" == "{}" ]] +} + +# --------------------------------------------------------------------------- +# TC2: /ship → systemMessage with FRESH EXECUTION and skill name +# --------------------------------------------------------------------------- + +@test "TC2: /ship triggers fresh execution message" { + run_hook_with_prompt "/ship" + [ "$status" -eq 0 ] + [[ "$output" =~ "systemMessage" ]] + [[ "$output" =~ "FRESH EXECUTION" ]] + [[ "$output" =~ "ship" ]] +} + +# --------------------------------------------------------------------------- +# TC3: /finalize-pr 42 → systemMessage with skill name +# --------------------------------------------------------------------------- + +@test "TC3: /finalize-pr with argument triggers fresh execution message" { + run_hook_with_prompt "/finalize-pr 42" + [ "$status" -eq 0 ] + [[ "$output" =~ "systemMessage" ]] + [[ "$output" =~ "FRESH EXECUTION" ]] + [[ "$output" =~ "finalize-pr" ]] +} + +# --------------------------------------------------------------------------- +# TC4: /resolve-pr-threads all → systemMessage with skill name +# --------------------------------------------------------------------------- + +@test "TC4: /resolve-pr-threads triggers fresh execution message" { + run_hook_with_prompt "/resolve-pr-threads all" + [ "$status" -eq 0 ] + [[ "$output" =~ "systemMessage" ]] + [[ "$output" =~ "resolve-pr-threads" ]] +} + +# --------------------------------------------------------------------------- +# TC5: Regular text without slash → empty JSON +# --------------------------------------------------------------------------- + +@test "TC5: text mentioning 'ship' without slash outputs empty JSON" { + run_hook_with_prompt "help me ship this feature" + [ "$status" -eq 0 ] + [[ "$output" == "{}" ]] +} + +# --------------------------------------------------------------------------- +# TC6: Filesystem path /usr/bin → empty JSON (false positive exclusion) +# --------------------------------------------------------------------------- + +@test "TC6: filesystem path /usr excluded as false positive" { + run_hook_with_prompt "check /usr/bin/python" + [ "$status" -eq 0 ] + [[ "$output" == "{}" ]] +} + +# --------------------------------------------------------------------------- +# TC7: Filesystem path /tmp → empty JSON (false positive exclusion) +# --------------------------------------------------------------------------- + +@test "TC7: filesystem path /tmp excluded as false positive" { + run_hook_with_prompt "read /tmp/output.log" + [ "$status" -eq 0 ] + [[ "$output" == "{}" ]] +} + +# --------------------------------------------------------------------------- +# TC8: Empty/missing prompt → empty JSON, exit 0 +# --------------------------------------------------------------------------- + +@test "TC8: empty input outputs empty JSON" { + run bash -c "echo '{}' | /bin/bash '$SCRIPT'" + [ "$status" -eq 0 ] + [[ "$output" == "{}" ]] +} + +# --------------------------------------------------------------------------- +# TC9: Malformed JSON input → empty JSON, exit 0 (fail-open) +# --------------------------------------------------------------------------- + +@test "TC9: malformed JSON input outputs empty JSON" { + run bash -c "echo 'not json at all' | /bin/bash '$SCRIPT'" + [ "$status" -eq 0 ] + [[ "$output" == "{}" ]] +} + +# --------------------------------------------------------------------------- +# TC10: /nix path excluded as false positive +# --------------------------------------------------------------------------- + +@test "TC10: filesystem path /nix excluded as false positive" { + run_hook_with_prompt "look at /nix/store/abc123" + [ "$status" -eq 0 ] + [[ "$output" == "{}" ]] +}