diff --git a/.deepreview b/.deepreview index e2b21ba4..e49128a4 100644 --- a/.deepreview +++ b/.deepreview @@ -287,6 +287,52 @@ update_learning_agents_architecture: additional_context: unchanged_matching_files: true +agent_tools_fully_qualified: + description: "Claude Code agent definition files must list every tool by fully-qualified name, never by wildcard." + match: + include: + - "**/agents/*.md" + review: + strategy: individual + instructions: | + This file defines a Claude Code subagent (the YAML frontmatter at the + top of the file configures the agent's name, description, model, and + tool grants). Check the `tools:` frontmatter field. + + Rule: every tool entry MUST be a fully-qualified tool name. Wildcard + or glob patterns (any entry containing `*`, `?`, `[`, or ending in + `__*`) are non-conforming. + + Rationale: wildcard patterns in subagent `tools:` frontmatter do not + reliably match deferred MCP tools at runtime. Observed failure mode: + a frontmatter entry like `mcp__deepwork-dev__*` is accepted at parse + time, but when the subagent tries to invoke + `mcp__deepwork-dev__mark_review_as_passed` the runtime responds with + `Error: No such tool available`. Enumerating each MCP tool by its + full name (e.g., `mcp__deepwork-dev__mark_review_as_passed`) avoids + this failure mode and also documents the exact tool surface the + agent relies on. This caught us in the e2e merge-queue run that + failed PR #390. + + This rule does NOT apply to files that are not Claude Code agent + definitions — if the file has no YAML frontmatter with name/tools + fields (e.g., it is a context-injection document or a skill body + that happens to live under an `agents/` path), this review passes + vacuously. + + Check for: + - Any entry in `tools:` containing `*`, `?`, or `[...]` glob syntax. + - Any entry ending in `__*` (the common MCP-wildcard form). + - Any quoted glob pattern such as `"mcp____*"`. + + Output Format: + - PASS: `tools:` field is missing, empty, or every entry is a + fully-qualified name. + - FAIL: list each wildcard entry with its line number and the + fully-qualified tool names it should be replaced with (if + determinable from the agent's body; otherwise flag the need for + enumeration). + shell_code_review: description: "Review shell scripts for correctness, safety, and project conventions." match: diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f80a5d6..e330faea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,24 +9,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- New `PLUG-REQ-001.15: Hook Script CLI Invocation` requirement in `doc/specs/deepwork/cli_plugins/PLUG-REQ-001-claude-code-plugin.md` + ### Changed +- `claude_plugin_hook_deepwork_invocation` review rule now requires plugin hook scripts to invoke the CLI via `uvx deepwork` instead of merely providing a `uvx deepwork` fallback (PLUG-REQ-001.15) +- Plugin hook scripts (`post_commit_reminder.sh`, `deepschema_write.sh`, `post_compact.sh`) now invoke the `deepwork` CLI exclusively via `uvx deepwork ...`, matching the MCP server launch in `plugins/claude/.mcp.json` +- Flake `shellHook` no longer runs `uv tool install -e` — the editable user-level `deepwork` install is redundant now that plugin hooks go through `uvx` + ### Fixed +- Plugin hooks no longer fail when the end user has a stale user-level `deepwork` install (e.g., `uv tool install deepwork` pinned to an older release) that wins PATH lookup but lacks the hook module being requested. The 0.13.9 fallback still used PATH first; this release bypasses PATH entirely so hooks resolve to the same `uvx` cache that the MCP server populated + ### Removed ## [0.13.9] - 2026-04-16 ### Added - New `claude_plugin_hook_deepwork_invocation` review rule in `plugins/claude/.deepreview` that flags plugin hook scripts which call bare `deepwork` without a `uvx deepwork` fallback - -### Changed - -### Fixed - -- Plugin hook scripts (`post_commit_reminder.sh`, `deepschema_write.sh`, `post_compact.sh`) now fall back to `uvx deepwork` when the bare `deepwork` binary is not on PATH. End-user installs launch the MCP server via `uvx deepwork serve`, so `deepwork` is not available as a command — previously these hooks failed with exit 127 on every Bash tool use, and Claude Code reported them as failed PostToolUse hooks (regression introduced in PR #361) - -### Removed ## [0.13.8] - 2026-04-14 ### Added diff --git a/doc/platforms/claude/cli_configuration.md b/doc/platforms/claude/cli_configuration.md index be71cc3d..762ebd60 100644 --- a/doc/platforms/claude/cli_configuration.md +++ b/doc/platforms/claude/cli_configuration.md @@ -250,7 +250,10 @@ plugins/claude/ │ └── configure_reviews/SKILL.md # Set up review rules ├── hooks/ # Hook configuration │ ├── hooks.json -│ └── post_commit_reminder.sh +│ ├── deepschema_write.sh # PostToolUse: validate writes against DeepSchemas +│ ├── post_commit_reminder.sh # PostToolUse: nudge /review after git commit +│ ├── post_compact.sh # SessionStart(compact): restore workflow context +│ └── startup_context.sh # SessionStart/SubagentStart: inject session/agent IDs └── .mcp.json # MCP server config (uvx deepwork serve) ``` diff --git a/doc/specs/deepwork/cli_plugins/PLUG-REQ-001-claude-code-plugin.md b/doc/specs/deepwork/cli_plugins/PLUG-REQ-001-claude-code-plugin.md index 8a045800..4eb84f71 100644 --- a/doc/specs/deepwork/cli_plugins/PLUG-REQ-001-claude-code-plugin.md +++ b/doc/specs/deepwork/cli_plugins/PLUG-REQ-001-claude-code-plugin.md @@ -107,3 +107,8 @@ The Claude Code plugin is the primary distribution mechanism for DeepWork on the 4. The agent body MUST instruct the subagent to read the instruction file from the user prompt, perform the review against the criteria in that file, and call `mark_review_as_passed` to report results. 5. The agent body MUST instruct the subagent not to edit files and not to explore beyond what the review instructions direct. 6. When the review formatter renders tasks with no per-rule agent persona specified (`agent_name` is `None`), it MUST default to `"reviewer"` as the `subagent_type` (see REVIEW-REQ-006.3.3c). + +### PLUG-REQ-001.15: Hook Script CLI Invocation + +1. Plugin hook scripts under `plugins/claude/hooks/` MUST invoke the `deepwork` CLI via `uvx deepwork ...`, never via a bare `deepwork` lookup on `PATH` (including `uv run deepwork` or absolute paths to `PATH`-installed binaries). This matches the MCP server launch command in `plugins/claude/.mcp.json` (`uvx deepwork serve`) so that hooks and the MCP server resolve to the same `deepwork` version from the same `uvx` cache. +2. Plugin hook scripts MUST NOT rely on a `deepwork` binary resolved through `PATH`, because (a) the binary is absent from `PATH` in end-user installs that run the plugin via `uvx`, which produces exit 127, and (b) when present via a user-level install such as `uv tool install deepwork`, it can be older than the Python module the hook is asking for, which produces "Hook '...' not found" or similar errors. Both failure modes surface to Claude Code as a failed hook. diff --git a/flake.nix b/flake.nix index 576a557c..b0065c56 100644 --- a/flake.nix +++ b/flake.nix @@ -82,9 +82,6 @@ uv sync --extra dev --quiet 2>/dev/null || true export PATH="$REPO_ROOT/.venv/bin:$PATH" - # Also register as a uv tool so `uvx deepwork serve` uses local source - uv tool install -e "$REPO_ROOT" --quiet 2>/dev/null || true - # Create claude wrapper script so direnv (which can't export functions) works _claude_real=$(PATH="$(echo "$PATH" | sed "s|$REPO_ROOT/.venv/bin:||g")" command -v claude) if [ -n "$_claude_real" ]; then diff --git a/plugins/claude/.deepreview b/plugins/claude/.deepreview index 8215f224..7ea59711 100644 --- a/plugins/claude/.deepreview +++ b/plugins/claude/.deepreview @@ -1,5 +1,5 @@ claude_plugin_hook_deepwork_invocation: - description: "Plugin hook scripts must fall back to `uvx deepwork` when the bare binary is not on PATH." + description: "PLUG-REQ-001.15: Plugin hook scripts must invoke the `deepwork` CLI via `uvx deepwork`, never via a bare binary on PATH." match: include: - "hooks/*.sh" @@ -7,31 +7,34 @@ claude_plugin_hook_deepwork_invocation: strategy: individual instructions: | Plugin hook scripts run in end-user installs where the MCP server is - launched via `uvx deepwork serve` (see plugins/claude/.mcp.json). In - that environment the bare `deepwork` binary is NOT on PATH. Any hook - that calls `deepwork ...` (or `uv run deepwork ...`) directly will - exit 127, and Claude Code reports it as a failed hook on every Bash - tool use. Regression history: PR #361 reintroduced this exact bug. + launched via `uvx deepwork serve` (see plugins/claude/.mcp.json) and + the bare `deepwork` binary may be missing from PATH, or present but + stale — e.g., a user-level `uv tool install deepwork` pinned to an + older release. A stale bare binary produces errors like "Hook + '...' not found" (when the hook module did not exist in that + version) on every Bash tool use, which Claude Code surfaces as a + failed hook. - For each `deepwork` (or `uv run deepwork`) invocation in this script, - verify it uses a fallback pattern equivalent to: + To avoid both failure modes, plugin hook scripts MUST invoke the + CLI the same way the MCP server is launched: via `uvx deepwork`. + A bare `deepwork ...` (or `uv run deepwork ...`, or any invocation + that uses PATH lookup) is a FAIL. + + Expected pattern: ```bash - if command -v deepwork >/dev/null 2>&1; then - echo "${INPUT}" | deepwork hook some_hook - else - echo "${INPUT}" | uvx deepwork hook some_hook - fi + echo "${INPUT}" | uvx deepwork hook some_hook ``` - A bare `deepwork ...` call without a `command -v deepwork` guard (or - equivalent fallback to `uvx deepwork ...`) is a FAIL. + For each `deepwork` invocation in this script, verify the command + prefix is `uvx deepwork`. Shell word-splitting counts — the word + immediately before `deepwork` on the command line must be `uvx`. Output Format: - - PASS: Every `deepwork` invocation has a `uvx` fallback, or the + - PASS: Every `deepwork` invocation is prefixed with `uvx`, or the script makes no `deepwork` calls. - - FAIL: List each unguarded `deepwork` invocation with its line - number and the suggested fallback edit. + - FAIL: List each non-`uvx` `deepwork` invocation with its line + number and the required edit. claude_plugin_skill_instructions: description: "PLUG-REQ-001 & REVIEW-REQ-007: Verify skill instructions adequately convey behavioral requirements." diff --git a/plugins/claude/agents/reviewer.md b/plugins/claude/agents/reviewer.md index 90e77fe4..7170c2db 100644 --- a/plugins/claude/agents/reviewer.md +++ b/plugins/claude/agents/reviewer.md @@ -8,8 +8,8 @@ tools: - Grep - Glob - Bash - - "mcp__plugin_deepwork_deepwork__*" - - "mcp__deepwork-dev__*" + - mcp__plugin_deepwork_deepwork__mark_review_as_passed + - mcp__deepwork-dev__mark_review_as_passed --- You are a DeepWork review agent. Your only job is to execute one review task and report the result. diff --git a/plugins/claude/hooks/deepschema_write.sh b/plugins/claude/hooks/deepschema_write.sh index a8a05462..84a8148e 100755 --- a/plugins/claude/hooks/deepschema_write.sh +++ b/plugins/claude/hooks/deepschema_write.sh @@ -1,14 +1,24 @@ #!/usr/bin/env bash -# DeepSchema write hook -# PostToolUse hook for Write/Edit - validates files against applicable DeepSchemas -# Falls back to `uvx deepwork` so end-user installs (where the plugin's -# MCP server is launched via uvx and `deepwork` is not on PATH) still work. +# deepschema_write.sh - DeepSchema write hook +# +# Registered as a PostToolUse hook on Write/Edit in hooks.json. +# Delegates to the `deepwork hook deepschema_write` Python entry point, +# which validates the written file against applicable DeepSchemas. +# +# Always invokes via `uvx deepwork` to match the MCP server invocation +# in plugins/claude/.mcp.json. This avoids a class of PATH-staleness +# bugs where a user-level `deepwork` binary (e.g., `uv tool install +# deepwork`) is older than the hook module it is being asked to run, +# producing "Hook '...' not found" errors on every tool use. +# +# Input (stdin): JSON from Claude Code PostToolUse hook +# Output (stdout): JSON response for Claude Code (allow/block + context) +# Exit codes: +# 0 on success, non-zero if uvx/the hook crashes (Claude Code +# surfaces non-zero as a failed PostToolUse hook) + +set -euo pipefail INPUT=$(cat) export DEEPWORK_HOOK_PLATFORM="claude" -if command -v deepwork >/dev/null 2>&1; then - echo "${INPUT}" | deepwork hook deepschema_write -else - echo "${INPUT}" | uvx deepwork hook deepschema_write -fi -exit $? +echo "${INPUT}" | uvx deepwork hook deepschema_write diff --git a/plugins/claude/hooks/post_commit_reminder.sh b/plugins/claude/hooks/post_commit_reminder.sh index ad85b6e2..c1c00f63 100755 --- a/plugins/claude/hooks/post_commit_reminder.sh +++ b/plugins/claude/hooks/post_commit_reminder.sh @@ -1,12 +1,27 @@ #!/usr/bin/env bash -# Post-commit reminder hook — delegates to deepwork Python hook. -# Falls back to `uvx deepwork` so end-user installs (where the plugin's -# MCP server is launched via uvx and `deepwork` is not on PATH) still work. +# post_commit_reminder.sh - Post-commit review reminder hook +# +# Registered as a PostToolUse hook on Bash in hooks.json. +# Delegates to the `deepwork hook post_commit_reminder` Python entry +# point, which inspects the Bash command and (when it is a `git commit`) +# nudges the agent to run the `review` skill if matching review rules +# have not been marked as passed for the committed files. +# +# Always invokes via `uvx deepwork` to match the MCP server invocation +# in plugins/claude/.mcp.json. This avoids a class of PATH-staleness +# bugs where a user-level `deepwork` binary (e.g., `uv tool install +# deepwork`) is older than the hook module it is being asked to run, +# producing "Hook '...' not found" errors on every Bash tool use. +# +# Input (stdin): JSON from Claude Code PostToolUse hook +# Output (stdout): JSON response for Claude Code (additionalContext for +# the agent, or empty {} when nothing to add) +# Exit codes: +# 0 on success, non-zero if uvx/the hook crashes (Claude Code +# surfaces non-zero as a failed PostToolUse hook) + +set -euo pipefail + INPUT=$(cat) export DEEPWORK_HOOK_PLATFORM="claude" -if command -v deepwork >/dev/null 2>&1; then - echo "${INPUT}" | deepwork hook post_commit_reminder -else - echo "${INPUT}" | uvx deepwork hook post_commit_reminder -fi -exit $? +echo "${INPUT}" | uvx deepwork hook post_commit_reminder diff --git a/plugins/claude/hooks/post_compact.sh b/plugins/claude/hooks/post_compact.sh index fc235736..9b99081c 100755 --- a/plugins/claude/hooks/post_compact.sh +++ b/plugins/claude/hooks/post_compact.sh @@ -22,19 +22,13 @@ if [ -z "$CWD" ]; then fi # ==== Fetch active sessions ==== -# Fall back to `uvx deepwork` so end-user installs (where the plugin's -# MCP server is launched via uvx and `deepwork` is not on PATH) still work. -if command -v deepwork >/dev/null 2>&1; then - STACK_JSON=$(deepwork jobs get-stack --path "$CWD" 2>/dev/null) || { - echo '{}' - exit 0 - } -else - STACK_JSON=$(uvx deepwork jobs get-stack --path "$CWD" 2>/dev/null) || { - echo '{}' - exit 0 - } -fi +# Always invoke via `uvx deepwork` to match the MCP server invocation in +# plugins/claude/.mcp.json — avoids PATH-staleness where a user-level +# `deepwork` binary is older than the subcommand it is being asked to run. +STACK_JSON=$(uvx deepwork jobs get-stack --path "$CWD" 2>/dev/null) || { + echo '{}' + exit 0 +} # ==== Check for active sessions ==== SESSION_COUNT=$(echo "$STACK_JSON" | jq '(.active_sessions // []) | length')