From d01f9431b5fd868bab74f408c82fab4d3608415f Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 18:39:12 +0200 Subject: [PATCH 01/12] feat(hooks): add is_design_artifact glob matcher --- hooks/lib/common.sh | 21 ++++++++++++++++++ tests/design-review.bats | 47 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 tests/design-review.bats diff --git a/hooks/lib/common.sh b/hooks/lib/common.sh index bb2b792..42e2527 100644 --- a/hooks/lib/common.sh +++ b/hooks/lib/common.sh @@ -84,3 +84,24 @@ is_destructive_command() { local cmd="$1" echo "$cmd" | grep -qE '\brm\s+-[a-zA-Z]*[rRf]|\bgit\s+reset\s+--hard\b|\bgit\s+push\s+[^|;]*--force\b|\bDROP\s+(TABLE|DATABASE|SCHEMA)\b|\bTRUNCATE\s+TABLE\b|\bdd\s+if=|>\s*/dev/sd[a-z]' } + +# Default design-artifact globs (colon-separated). Overridable via +# CLAUDE_PLUGIN_GEMINI_DESIGN_GLOBS. Globs use a leading */ so they match a +# path whether it is absolute or repo-relative. A path matches if it matches +# ANY glob. In [[ $x == $glob ]], * matches across slashes. +DEFAULT_DESIGN_GLOBS="*/superpowers/specs/*-design.md:*/superpowers/plans/*.md:*-plan.md:*/specs/*.md:*/plans/*.md:*/DESIGN.md:*/PLAN.md" + +is_design_artifact() { + local path="$1" + local globs="${CLAUDE_PLUGIN_GEMINI_DESIGN_GLOBS:-$DEFAULT_DESIGN_GLOBS}" + local IFS=':' + local g + for g in $globs; do + [ -z "$g" ] && continue + # shellcheck disable=SC2053 + if [[ "$path" == $g ]]; then + return 0 + fi + done + return 1 +} diff --git a/tests/design-review.bats b/tests/design-review.bats new file mode 100644 index 0000000..d2fa4f0 --- /dev/null +++ b/tests/design-review.bats @@ -0,0 +1,47 @@ +#!/usr/bin/env bats + +setup() { + export CLAUDE_PLUGIN_DATA="$BATS_TMPDIR/test-data-$$" + export CLAUDE_PLUGIN_OPTION_GEMINI_API_KEY="test-key" + export CLAUDE_PLUGIN_GEMINI_DISABLE_HOOKS=0 + mkdir -p "$CLAUDE_PLUGIN_DATA" +} +teardown() { + rm -rf "$CLAUDE_PLUGIN_DATA" +} + +# --- is_design_artifact --- +@test "is_design_artifact: matches superpowers spec (relative)" { + run bash -c 'source hooks/lib/common.sh; is_design_artifact "docs/superpowers/specs/2026-06-02-foo-design.md"' + [ "$status" -eq 0 ] +} +@test "is_design_artifact: matches absolute spec path" { + run bash -c 'source hooks/lib/common.sh; is_design_artifact "/Users/me/proj/docs/superpowers/specs/x-design.md"' + [ "$status" -eq 0 ] +} +@test "is_design_artifact: matches a *-plan.md" { + run bash -c 'source hooks/lib/common.sh; is_design_artifact "docs/superpowers/plans/2026-06-02-foo-plan.md"' + [ "$status" -eq 0 ] +} +@test "is_design_artifact: matches generic specs/ dir" { + run bash -c 'source hooks/lib/common.sh; is_design_artifact "myproj/specs/auth.md"' + [ "$status" -eq 0 ] +} +@test "is_design_artifact: matches DESIGN.md" { + run bash -c 'source hooks/lib/common.sh; is_design_artifact "subsystem/DESIGN.md"' + [ "$status" -eq 0 ] +} +@test "is_design_artifact: rejects a non-design markdown" { + run bash -c 'source hooks/lib/common.sh; is_design_artifact "docs/notes.md"' + [ "$status" -ne 0 ] +} +@test "is_design_artifact: rejects a source file" { + run bash -c 'source hooks/lib/common.sh; is_design_artifact "src/foo.ts"' + [ "$status" -ne 0 ] +} +@test "is_design_artifact: respects CLAUDE_PLUGIN_GEMINI_DESIGN_GLOBS override" { + run bash -c 'source hooks/lib/common.sh; CLAUDE_PLUGIN_GEMINI_DESIGN_GLOBS="*/rfc/*.md" is_design_artifact "x/rfc/1.md"' + [ "$status" -eq 0 ] + run bash -c 'source hooks/lib/common.sh; CLAUDE_PLUGIN_GEMINI_DESIGN_GLOBS="*/rfc/*.md" is_design_artifact "docs/superpowers/specs/x-design.md"' + [ "$status" -ne 0 ] +} From 56aed29dc58d3274640470296ded1dd9840fec44 Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 18:39:37 +0200 Subject: [PATCH 02/12] feat(hooks): add pending-mode marker helpers --- hooks/lib/common.sh | 23 +++++++++++++++++++++++ tests/design-review.bats | 18 ++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/hooks/lib/common.sh b/hooks/lib/common.sh index 42e2527..bfd0eb0 100644 --- a/hooks/lib/common.sh +++ b/hooks/lib/common.sh @@ -105,3 +105,26 @@ is_design_artifact() { done return 1 } + +# Record the intended verdict-handling mode ("advisory"|"blocking") for an +# agent about to be dispatched. The verdict-handler consumes it on SubagentStop. +write_pending_mode() { + local agent="$1" + local mode="$2" + mkdir -p "$(data_dir)/pending" + echo "$mode" > "$(data_dir)/pending/${agent}.mode" +} + +# Print and delete the pending mode for an agent. Prints "blocking" if no +# marker exists, which preserves the original blocking plan/done-claim gates. +read_consume_pending_mode() { + local agent="$1" + local f + f="$(data_dir)/pending/${agent}.mode" + if [ -f "$f" ]; then + cat "$f" + rm -f "$f" + else + echo "blocking" + fi +} diff --git a/tests/design-review.bats b/tests/design-review.bats index d2fa4f0..c762d4e 100644 --- a/tests/design-review.bats +++ b/tests/design-review.bats @@ -45,3 +45,21 @@ teardown() { run bash -c 'source hooks/lib/common.sh; CLAUDE_PLUGIN_GEMINI_DESIGN_GLOBS="*/rfc/*.md" is_design_artifact "docs/superpowers/specs/x-design.md"' [ "$status" -ne 0 ] } + +# --- pending-mode markers --- +@test "pending mode: write creates a marker file" { + bash -c 'source hooks/lib/common.sh; write_pending_mode gemini-challenger advisory' + [ -f "$CLAUDE_PLUGIN_DATA/pending/gemini-challenger.mode" ] + [ "$(cat "$CLAUDE_PLUGIN_DATA/pending/gemini-challenger.mode")" = "advisory" ] +} +@test "pending mode: read returns the mode and consumes the marker" { + bash -c 'source hooks/lib/common.sh; write_pending_mode gemini-validator advisory' + run bash -c 'source hooks/lib/common.sh; read_consume_pending_mode gemini-validator' + [ "$status" -eq 0 ] + [ "$output" = "advisory" ] + [ ! -f "$CLAUDE_PLUGIN_DATA/pending/gemini-validator.mode" ] +} +@test "pending mode: default is blocking when no marker exists" { + run bash -c 'source hooks/lib/common.sh; read_consume_pending_mode gemini-validator' + [ "$output" = "blocking" ] +} From 46a5ff51cf3b46ae71a66a26483d685b623bb015 Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 18:40:02 +0200 Subject: [PATCH 03/12] feat(hooks): add design seen-hash helpers --- hooks/lib/common.sh | 13 +++++++++++++ tests/design-review.bats | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/hooks/lib/common.sh b/hooks/lib/common.sh index bfd0eb0..9f9f68f 100644 --- a/hooks/lib/common.sh +++ b/hooks/lib/common.sh @@ -128,3 +128,16 @@ read_consume_pending_mode() { echo "blocking" fi } + +# SHA-256 of a file's contents (first field only). Empty if unreadable. +file_content_hash() { + shasum -a 256 "$1" 2>/dev/null | cut -d' ' -f1 +} + +# Path-keyed file storing the last-reviewed content hash for a design artifact. +design_seen_file() { + local path="$1" + local pathhash + pathhash=$(echo -n "$path" | shasum -a 256 | cut -c1-12) + echo "$(data_dir)/design-review-seen/${pathhash}.sha" +} diff --git a/tests/design-review.bats b/tests/design-review.bats index c762d4e..6074f3e 100644 --- a/tests/design-review.bats +++ b/tests/design-review.bats @@ -63,3 +63,26 @@ teardown() { run bash -c 'source hooks/lib/common.sh; read_consume_pending_mode gemini-validator' [ "$output" = "blocking" ] } + +# --- design seen-hash --- +@test "file_content_hash: returns a hash for an existing file" { + F="$BATS_TMPDIR/h-$$.txt" + echo "hello" > "$F" + run bash -c "source hooks/lib/common.sh; file_content_hash '$F'" + [ "$status" -eq 0 ] + [ -n "$output" ] +} +@test "file_content_hash: differs when content changes" { + F="$BATS_TMPDIR/h2-$$.txt" + echo "v1" > "$F" + H1=$(bash -c "source hooks/lib/common.sh; file_content_hash '$F'") + echo "v2" > "$F" + H2=$(bash -c "source hooks/lib/common.sh; file_content_hash '$F'") + [ "$H1" != "$H2" ] +} +@test "design_seen_file: path-keyed, stable for same path" { + S1=$(bash -c 'source hooks/lib/common.sh; design_seen_file "docs/superpowers/specs/x-design.md"') + S2=$(bash -c 'source hooks/lib/common.sh; design_seen_file "docs/superpowers/specs/x-design.md"') + [ "$S1" = "$S2" ] + [[ "$S1" == *"/design-review-seen/"* ]] +} From 1cfa6e435f87ea67e9cc3a760f05989dd3bae63d Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 18:40:36 +0200 Subject: [PATCH 04/12] feat(hooks): add design-review and plan-challenge directives --- hooks/lib/prompt-builder.sh | 38 +++++++++++++++++++++++++++++++++++++ tests/design-review.bats | 18 ++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/hooks/lib/prompt-builder.sh b/hooks/lib/prompt-builder.sh index fe7e91d..99dd9d4 100644 --- a/hooks/lib/prompt-builder.sh +++ b/hooks/lib/prompt-builder.sh @@ -97,3 +97,41 @@ ${final_claim} Diff summary: ${diff_summary}" } + +# Build a combined directive asking Claude to dispatch BOTH the validator and +# the challenger on a design artifact as an ADVISORY pass. +build_design_review_directive() { + local file_path="$1" + local history="$2" + + cat < Date: Tue, 2 Jun 2026 18:46:10 +0200 Subject: [PATCH 05/12] test+docs: address Batch A review (run-guard, isolate plan glob, collision test, comment precision) --- hooks/lib/common.sh | 7 ++++--- hooks/lib/prompt-builder.sh | 2 ++ tests/design-review.bats | 12 +++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/hooks/lib/common.sh b/hooks/lib/common.sh index 9f9f68f..edefcbb 100644 --- a/hooks/lib/common.sh +++ b/hooks/lib/common.sh @@ -86,9 +86,10 @@ is_destructive_command() { } # Default design-artifact globs (colon-separated). Overridable via -# CLAUDE_PLUGIN_GEMINI_DESIGN_GLOBS. Globs use a leading */ so they match a -# path whether it is absolute or repo-relative. A path matches if it matches -# ANY glob. In [[ $x == $glob ]], * matches across slashes. +# CLAUDE_PLUGIN_GEMINI_DESIGN_GLOBS. Most globs start with */ so they match a +# path whether it is absolute or repo-relative; the bare *-plan.md entry matches +# a file ending in -plan.md at any depth (including the repo root). A path +# matches if it matches ANY glob. In [[ $x == $glob ]], * matches across slashes. DEFAULT_DESIGN_GLOBS="*/superpowers/specs/*-design.md:*/superpowers/plans/*.md:*-plan.md:*/specs/*.md:*/plans/*.md:*/DESIGN.md:*/PLAN.md" is_design_artifact() { diff --git a/hooks/lib/prompt-builder.sh b/hooks/lib/prompt-builder.sh index 99dd9d4..2e9e884 100644 --- a/hooks/lib/prompt-builder.sh +++ b/hooks/lib/prompt-builder.sh @@ -98,6 +98,8 @@ Diff summary: ${diff_summary}" } +# NOTE: intentionally does not use build_directive: this dispatches TWO agents +# and is advisory, so it must omit build_directive's single-agent blocking footer. # Build a combined directive asking Claude to dispatch BOTH the validator and # the challenger on a design artifact as an ADVISORY pass. build_design_review_directive() { diff --git a/tests/design-review.bats b/tests/design-review.bats index 42b836f..0c4d00f 100644 --- a/tests/design-review.bats +++ b/tests/design-review.bats @@ -23,6 +23,10 @@ teardown() { run bash -c 'source hooks/lib/common.sh; is_design_artifact "docs/superpowers/plans/2026-06-02-foo-plan.md"' [ "$status" -eq 0 ] } +@test "is_design_artifact: matches a bare *-plan.md at any depth" { + run bash -c 'source hooks/lib/common.sh; is_design_artifact "subdir/my-plan.md"' + [ "$status" -eq 0 ] +} @test "is_design_artifact: matches generic specs/ dir" { run bash -c 'source hooks/lib/common.sh; is_design_artifact "myproj/specs/auth.md"' [ "$status" -eq 0 ] @@ -48,7 +52,8 @@ teardown() { # --- pending-mode markers --- @test "pending mode: write creates a marker file" { - bash -c 'source hooks/lib/common.sh; write_pending_mode gemini-challenger advisory' + run bash -c 'source hooks/lib/common.sh; write_pending_mode gemini-challenger advisory' + [ "$status" -eq 0 ] [ -f "$CLAUDE_PLUGIN_DATA/pending/gemini-challenger.mode" ] [ "$(cat "$CLAUDE_PLUGIN_DATA/pending/gemini-challenger.mode")" = "advisory" ] } @@ -86,6 +91,11 @@ teardown() { [ "$S1" = "$S2" ] [[ "$S1" == *"/design-review-seen/"* ]] } +@test "design_seen_file: distinct paths produce distinct seen files" { + S1=$(bash -c 'source hooks/lib/common.sh; design_seen_file "docs/superpowers/specs/a-design.md"') + S2=$(bash -c 'source hooks/lib/common.sh; design_seen_file "docs/superpowers/specs/b-design.md"') + [ "$S1" != "$S2" ] +} # --- directives --- @test "build_design_review_directive: names both agents and both tasks" { From 58c008e45e91dfd131b4528be91dcea010a97da7 Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 18:49:00 +0200 Subject: [PATCH 06/12] feat(hooks): add design-review.sh PostToolUse hook --- hooks/design-review.sh | 48 ++++++++++++++++++++++++++++++ tests/design-review.bats | 63 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100755 hooks/design-review.sh diff --git a/hooks/design-review.sh b/hooks/design-review.sh new file mode 100755 index 0000000..b939f15 --- /dev/null +++ b/hooks/design-review.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# PostToolUse(Write|Edit) hook: when a design/plan artifact is written, ask +# Claude to dispatch BOTH gemini-validator (VALIDATE_DESIGN) and +# gemini-challenger (CHALLENGE_DESIGN) as an ADVISORY pass. Deduped by content +# hash so cosmetic re-writes do not re-fire. +# +# Pattern: exit 0 + JSON hookSpecificOutput.additionalContext. PostToolUse runs +# after the write has happened, so it never denies; it only injects context. +set -euo pipefail +trap 'echo "[gemini-plugin] design-review crashed at line ${LINENO} (last command: ${BASH_COMMAND})" >&2; exit 0' ERR + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" +source "${SCRIPT_DIR}/lib/prompt-builder.sh" + +check_plugin_enabled +check_gemini_available +ensure_data_dir + +INPUT=$(cat) +FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') + +[ -z "$FILE_PATH" ] && exit 0 +is_design_artifact "$FILE_PATH" || exit 0 + +# Material-change dedup: skip if content hash matches the last reviewed hash. +SEEN_FILE=$(design_seen_file "$FILE_PATH") +NEW_HASH=$(file_content_hash "$FILE_PATH") +[ -z "$NEW_HASH" ] && exit 0 +if [ -f "$SEEN_FILE" ] && [ "$(cat "$SEEN_FILE")" = "$NEW_HASH" ]; then + exit 0 +fi + +HISTORY=$(get_plan_history "VALIDATE_DESIGN" 3) +write_pending_mode "gemini-validator" "advisory" +write_pending_mode "gemini-challenger" "advisory" +DIRECTIVE=$(build_design_review_directive "$FILE_PATH" "$HISTORY") + +mkdir -p "$(dirname "$SEEN_FILE")" +echo "$NEW_HASH" > "$SEEN_FILE" + +jq -n --arg ctx "$DIRECTIVE" '{ + hookSpecificOutput: { + hookEventName: "PostToolUse", + additionalContext: $ctx + } +}' +exit 0 diff --git a/tests/design-review.bats b/tests/design-review.bats index 0c4d00f..334aa6c 100644 --- a/tests/design-review.bats +++ b/tests/design-review.bats @@ -114,3 +114,66 @@ teardown() { [[ "$output" == *"gemini-challenger"* ]] [[ "$output" == *"CHALLENGE_PLAN"* ]] } + +# --- design-review.sh hook --- +@test "design-review: non-design path exits 0 with no output" { + run bash -c 'echo "{\"tool_input\":{\"file_path\":\"src/foo.ts\"}}" | ./hooks/design-review.sh' + [ "$status" -eq 0 ] + [ -z "$output" ] +} +@test "design-review: empty file_path exits 0 with no output" { + run bash -c 'echo "{\"tool_input\":{\"file_path\":\"\"}}" | ./hooks/design-review.sh' + [ "$status" -eq 0 ] + [ -z "$output" ] +} +@test "design-review: first write of a design file dispatches both agents" { + SPEC="$BATS_TMPDIR/sd1-$$/docs/superpowers/specs/x-design.md" + mkdir -p "$(dirname "$SPEC")" + echo "# design v1" > "$SPEC" + run bash -c "echo '{\"tool_input\":{\"file_path\":\"${SPEC}\"}}' | ./hooks/design-review.sh" + [ "$status" -eq 0 ] + echo "$output" | jq -e '.hookSpecificOutput.hookEventName == "PostToolUse"' + echo "$output" | jq -e '.hookSpecificOutput.additionalContext | contains("gemini-validator")' + echo "$output" | jq -e '.hookSpecificOutput.additionalContext | contains("VALIDATE_DESIGN")' + echo "$output" | jq -e '.hookSpecificOutput.additionalContext | contains("gemini-challenger")' + echo "$output" | jq -e '.hookSpecificOutput.additionalContext | contains("CHALLENGE_DESIGN")' +} +@test "design-review: writes advisory pending markers for both agents" { + SPEC="$BATS_TMPDIR/sd2-$$/docs/superpowers/specs/y-design.md" + mkdir -p "$(dirname "$SPEC")" + echo "# design" > "$SPEC" + run bash -c "echo '{\"tool_input\":{\"file_path\":\"${SPEC}\"}}' | ./hooks/design-review.sh" + [ "$status" -eq 0 ] + [ "$(cat "$CLAUDE_PLUGIN_DATA/pending/gemini-validator.mode")" = "advisory" ] + [ "$(cat "$CLAUDE_PLUGIN_DATA/pending/gemini-challenger.mode")" = "advisory" ] +} +@test "design-review: unchanged content does not re-fire (hash dedup)" { + SPEC="$BATS_TMPDIR/sd3-$$/docs/superpowers/specs/z-design.md" + mkdir -p "$(dirname "$SPEC")" + echo "# stable design" > "$SPEC" + run bash -c "echo '{\"tool_input\":{\"file_path\":\"${SPEC}\"}}' | ./hooks/design-review.sh" + [ "$status" -eq 0 ] + [ -n "$output" ] + run bash -c "echo '{\"tool_input\":{\"file_path\":\"${SPEC}\"}}' | ./hooks/design-review.sh" + [ "$status" -eq 0 ] + [ -z "$output" ] +} +@test "design-review: materially changed content re-fires" { + SPEC="$BATS_TMPDIR/sd4-$$/docs/superpowers/specs/w-design.md" + mkdir -p "$(dirname "$SPEC")" + echo "# v1" > "$SPEC" + run bash -c "echo '{\"tool_input\":{\"file_path\":\"${SPEC}\"}}' | ./hooks/design-review.sh" + [ -n "$output" ] + echo "# v2 substantially different content here" > "$SPEC" + run bash -c "echo '{\"tool_input\":{\"file_path\":\"${SPEC}\"}}' | ./hooks/design-review.sh" + [ -n "$output" ] +} +@test "design-review: exits 0 silently when disabled" { + export CLAUDE_PLUGIN_GEMINI_DISABLE_HOOKS=1 + SPEC="$BATS_TMPDIR/sd5-$$/docs/superpowers/specs/d-design.md" + mkdir -p "$(dirname "$SPEC")" + echo "# design" > "$SPEC" + run bash -c "echo '{\"tool_input\":{\"file_path\":\"${SPEC}\"}}' | ./hooks/design-review.sh" + [ "$status" -eq 0 ] + [ -z "$output" ] +} From aab49855b10d61de2ac964e8d5aaf117475b4766 Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 18:49:50 +0200 Subject: [PATCH 07/12] feat(hooks): route verdict blocking vs advisory via pending marker --- hooks/subagent-verdict-handler.sh | 6 +++++- tests/hooks-triggers.bats | 24 ++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/hooks/subagent-verdict-handler.sh b/hooks/subagent-verdict-handler.sh index 9dda8e0..e6f7a98 100755 --- a/hooks/subagent-verdict-handler.sh +++ b/hooks/subagent-verdict-handler.sh @@ -10,6 +10,7 @@ DATA="$(data_dir)" INPUT=$(cat) AGENT=$(echo "$INPUT" | jq -r '.agent_type') +MODE=$(read_consume_pending_mode "$AGENT") TRANSCRIPT=$(echo "$INPUT" | jq -r '.transcript_path // empty') if [ -z "$TRANSCRIPT" ] || [ ! -f "$TRANSCRIPT" ]; then @@ -43,10 +44,13 @@ echo "{\"task\":\"$(echo "$INPUT" | jq -r '.task // "unknown"')\",\"agent\":\"${ if [ "$VERDICT" = "fail" ] || [ "$VERDICT" = "block" ]; then cat >&2 < "$TRANSCRIPT" + mkdir -p "$CLAUDE_PLUGIN_DATA/pending" + echo "advisory" > "$CLAUDE_PLUGIN_DATA/pending/gemini-validator.mode" + run bash -c "echo '{\"agent_type\":\"gemini-validator\",\"transcript_path\":\"${TRANSCRIPT}\"}' | ./hooks/subagent-verdict-handler.sh" + [ "$status" -eq 0 ] + [[ "$output" == *"a gap"* ]] + [ ! -f "$CLAUDE_PLUGIN_DATA/pending/gemini-validator.mode" ] +} +@test "verdict-handler: blocking marker keeps fail blocking (exit 2)" { + TRANSCRIPT="$BATS_TMPDIR/transcript-blk-$$.jsonl" + echo '{"type":"assistant","message":{"content":[{"text":"{\"verdict\":\"fail\",\"gaps\":[\"a gap\"]}"}]}}' > "$TRANSCRIPT" + mkdir -p "$CLAUDE_PLUGIN_DATA/pending" + echo "blocking" > "$CLAUDE_PLUGIN_DATA/pending/gemini-validator.mode" + run bash -c "echo '{\"agent_type\":\"gemini-validator\",\"transcript_path\":\"${TRANSCRIPT}\"}' | ./hooks/subagent-verdict-handler.sh" + [ "$status" -eq 2 ] +} +@test "verdict-handler: no marker defaults to blocking (exit 2), no regression" { + TRANSCRIPT="$BATS_TMPDIR/transcript-def-$$.jsonl" + echo '{"type":"assistant","message":{"content":[{"text":"{\"verdict\":\"fail\",\"gaps\":[\"a gap\"]}"}]}}' > "$TRANSCRIPT" + run bash -c "echo '{\"agent_type\":\"gemini-validator\",\"transcript_path\":\"${TRANSCRIPT}\"}' | ./hooks/subagent-verdict-handler.sh" + [ "$status" -eq 2 ] +} # --- plugin disable --- From 08c3f04a1980efb6a15747e91c8d51f12651abfc Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 18:50:25 +0200 Subject: [PATCH 08/12] feat(hooks): add advisory challenger at ExitPlanMode --- hooks/plan-complete.sh | 11 ++++++++++- tests/hooks-triggers.bats | 13 +++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/hooks/plan-complete.sh b/hooks/plan-complete.sh index d8c894d..8c9a9c2 100755 --- a/hooks/plan-complete.sh +++ b/hooks/plan-complete.sh @@ -29,8 +29,17 @@ fi HISTORY=$(get_plan_history "VALIDATE_PLAN" 3) DIRECTIVE=$(build_plan_validation_directive "$PLAN_TEXT" "$HISTORY") +CHALLENGE=$(build_plan_challenge_directive "$PLAN_TEXT") -jq -n --arg ctx "$DIRECTIVE" '{ +# Validator keeps its blocking gate; challenger runs advisory alongside it. +write_pending_mode "gemini-validator" "blocking" +write_pending_mode "gemini-challenger" "advisory" + +COMBINED="${DIRECTIVE} + +${CHALLENGE}" + +jq -n --arg ctx "$COMBINED" '{ hookSpecificOutput: { hookEventName: "PreToolUse", additionalContext: $ctx diff --git a/tests/hooks-triggers.bats b/tests/hooks-triggers.bats index 7ecbb51..d813bb1 100644 --- a/tests/hooks-triggers.bats +++ b/tests/hooks-triggers.bats @@ -170,6 +170,19 @@ teardown() { [ "$status" -eq 0 ] [ -z "$output" ] } +@test "plan-complete: also dispatches advisory challenger alongside blocking validator" { + run bash -c 'echo "{\"tool_input\":{\"plan\":\"Step 1: do X.\"}}" | ./hooks/plan-complete.sh' + [ "$status" -eq 0 ] + echo "$output" | jq -e '.hookSpecificOutput.additionalContext | contains("VALIDATE_PLAN")' + echo "$output" | jq -e '.hookSpecificOutput.additionalContext | contains("gemini-challenger")' + echo "$output" | jq -e '.hookSpecificOutput.additionalContext | contains("CHALLENGE_PLAN")' +} +@test "plan-complete: marks challenger advisory and validator blocking" { + run bash -c 'echo "{\"tool_input\":{\"plan\":\"Step 1: do X.\"}}" | ./hooks/plan-complete.sh' + [ "$status" -eq 0 ] + [ "$(cat "$CLAUDE_PLUGIN_DATA/pending/gemini-challenger.mode")" = "advisory" ] + [ "$(cat "$CLAUDE_PLUGIN_DATA/pending/gemini-validator.mode")" = "blocking" ] +} # --- stop-done-claim --- # Uses exit 0 + JSON with decision=block + additionalContext. From d14aa8b0c7cc4f7cc1242599c576df36636fa78d Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 18:50:49 +0200 Subject: [PATCH 09/12] feat(hooks): register PostToolUse(Write|Edit) design-review hook --- hooks/hooks.json | 8 ++++++++ tests/manifests.bats | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/hooks/hooks.json b/hooks/hooks.json index 0ab2079..473e3d5 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -29,6 +29,14 @@ ] } ], + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { "type": "command", "command": "${CLAUDE_PLUGIN_ROOT}/hooks/design-review.sh", "async": false } + ] + } + ], "PreCompact": [ { "hooks": [ diff --git a/tests/manifests.bats b/tests/manifests.bats index 8f7736e..e042065 100644 --- a/tests/manifests.bats +++ b/tests/manifests.bats @@ -23,3 +23,16 @@ @test "marketplace.json is not present (distributed via SynthForge)" { [ ! -f .claude-plugin/marketplace.json ] } +@test "hooks.json is valid JSON" { + jq empty hooks/hooks.json +} +@test "hooks.json declares PostToolUse(Write|Edit) -> design-review.sh" { + jq -e '.hooks.PostToolUse[0].matcher == "Write|Edit"' hooks/hooks.json + jq -e '.hooks.PostToolUse[0].hooks[0].command | endswith("design-review.sh")' hooks/hooks.json +} +@test "hooks.json SubagentStop matcher includes all five agents" { + M=$(jq -r '.hooks.SubagentStop[0].matcher' hooks/hooks.json) + for a in gemini-validator gemini-challenger gemini-researcher gemini-summarizer gemini-reviewer; do + [[ "$M" == *"$a"* ]] + done +} From 2fd2543eebb33ed7f583a4d2dfc1fc4101536f38 Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 19:00:23 +0200 Subject: [PATCH 10/12] fix: address Batch B review (verdict-handler ordering, advisory directive footer, seen-hash ordering, test guards) --- hooks/design-review.sh | 6 +++--- hooks/lib/prompt-builder.sh | 20 +++++++++++++++++++- hooks/subagent-verdict-handler.sh | 3 ++- tests/design-review.bats | 7 +++++++ 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/hooks/design-review.sh b/hooks/design-review.sh index b939f15..f21b7ca 100755 --- a/hooks/design-review.sh +++ b/hooks/design-review.sh @@ -36,13 +36,13 @@ write_pending_mode "gemini-validator" "advisory" write_pending_mode "gemini-challenger" "advisory" DIRECTIVE=$(build_design_review_directive "$FILE_PATH" "$HISTORY") -mkdir -p "$(dirname "$SEEN_FILE")" -echo "$NEW_HASH" > "$SEEN_FILE" - jq -n --arg ctx "$DIRECTIVE" '{ hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: $ctx } }' + +mkdir -p "$(dirname "$SEEN_FILE")" +echo "$NEW_HASH" > "$SEEN_FILE" exit 0 diff --git a/hooks/lib/prompt-builder.sh b/hooks/lib/prompt-builder.sh index 2e9e884..d6d8157 100644 --- a/hooks/lib/prompt-builder.sh +++ b/hooks/lib/prompt-builder.sh @@ -18,6 +18,24 @@ IMPORTANT: Block until the subagent returns its structured JSON verdict. If verd EOF } +# Like build_directive, but for an ADVISORY consult: surface findings without +# blocking, instead of build_directive's "Block until ... address the gaps" +# footer. Use for hook-dispatched agents whose verdict must not halt the flow. +build_advisory_directive() { + local agent="$1" + local task="$2" + local context="$3" + + cat < "$SPEC" run bash -c "echo '{\"tool_input\":{\"file_path\":\"${SPEC}\"}}' | ./hooks/design-review.sh" + [ "$status" -eq 0 ] [ -n "$output" ] echo "# v2 substantially different content here" > "$SPEC" run bash -c "echo '{\"tool_input\":{\"file_path\":\"${SPEC}\"}}' | ./hooks/design-review.sh" From 3dd3ec437041ed617b7821a52b56b3ae79ed1703 Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 19:02:28 +0200 Subject: [PATCH 11/12] docs: document the automatic design-review pass --- rules/using-gemini.md | 1 + skills/gemini-consult/SKILL.md | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/rules/using-gemini.md b/rules/using-gemini.md index 92e882e..c78ca36 100644 --- a/rules/using-gemini.md +++ b/rules/using-gemini.md @@ -9,6 +9,7 @@ The gemini-plugin is loaded. Five subagents assist you: | gemini-researcher | Search-grounded facts with citations; never opines without a URL | Hook (UserPromptSubmit) or /gemini-plugin:gemini-research | | gemini-summarizer | Compresses session state; writes risk maps at SessionStart | Hook (SessionStart, PreCompact) | | gemini-reviewer | Generalist diff/PR review: security, threading, version drift, docs, dead code | /gemini-plugin:gemini-consult rule (manual dispatch) | +| **design-review pass** | **Advisory:** when a design/plan artifact is written or native plan mode exits, gemini-validator and gemini-challenger review it. Never blocks; silenced by `CLAUDE_PLUGIN_GEMINI_DISABLE_HOOKS=1`. | Hook (PostToolUse Write\|Edit, ExitPlanMode) | ### Always reach for Gemini when diff --git a/skills/gemini-consult/SKILL.md b/skills/gemini-consult/SKILL.md index cd07f0a..3d9fe19 100644 --- a/skills/gemini-consult/SKILL.md +++ b/skills/gemini-consult/SKILL.md @@ -53,6 +53,19 @@ missing Gemini tool, do NOT assume the server is down: first run `/gemini-plugin:gemini-doctor`. If doctor's check 2 passes but check 3 fails, the session is stale (restart Claude Code); if both pass, retry the dispatch. +## The automatic design-review pass + +Separately from the manual consults this rule governs, the plugin runs an +AUTOMATIC, advisory design-review pass via hooks: whenever a design/plan +artifact is written (a `*-design.md` spec, a `*-plan.md`, or a file under a +`specs/`/`plans/` dir) or native plan mode exits, the plugin asks you to +dispatch gemini-validator and gemini-challenger over it. That pass is part of +the uncounted hook channel: it does NOT count against the one-consult-per-turn +cap, and it is advisory (findings surface but never block). It is silenced by +the same `CLAUDE_PLUGIN_GEMINI_DISABLE_HOOKS=1` / `brainstorm.off` kill switch +as every other hook. The blocking validator at native plan-mode exit is +unchanged; only the added challenger and the file-artifact pass are advisory. + ## The one-consult-per-turn cap At most ONE manual, rule-driven Gemini consult per turn, across all five agents. From 037f7a79b00b134ff3bcee0bd0c58595d5afeda0 Mon Sep 17 00:00:00 2001 From: azmym Date: Tue, 2 Jun 2026 19:03:22 +0200 Subject: [PATCH 12/12] chore: bump to 0.6.0 for design-review pass --- .claude-plugin/plugin.json | 2 +- CHANGELOG.md | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index 65ee941..0a774a2 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "gemini-plugin", "displayName": "Gemini Plugin", - "version": "0.5.1", + "version": "0.6.0", "description": "Wraps gemini-mcp into a Claude Code plugin so Gemini acts as a second opinion: validator/challenger/researcher/summarizer/reviewer subagents, auto-trigger hooks, and 9 task-oriented skills.", "author": { "name": "azmym" }, "homepage": "https://github.com/azmym/gemini-plugin", diff --git a/CHANGELOG.md b/CHANGELOG.md index ff6e667..b76d7ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,17 @@ All notable changes to gemini-plugin are documented here. The format follows [Ke ## [Unreleased] +## [0.6.0] - 2026-06-02 + +### Added + +- **Automatic design-review pass.** Whenever a design/plan artifact is written (a `*-design.md` spec, a `*-plan.md`, or a file under a `specs/`/`plans/` directory) via a new `PostToolUse(Write|Edit)` hook, or when native plan mode exits, the plugin asks Claude to dispatch gemini-validator (VALIDATE_DESIGN) and gemini-challenger (CHALLENGE_DESIGN) over it. The pass is advisory (findings surface but never block), deduped by file content hash so cosmetic re-edits do not re-fire, exempt from the manual one-consult-per-turn cap (it is part of the uncounted hook channel), and silenced by the existing `CLAUDE_PLUGIN_GEMINI_DISABLE_HOOKS` / `brainstorm.off` kill switch. The artifact globs are overridable via `CLAUDE_PLUGIN_GEMINI_DESIGN_GLOBS`. + +### Changed + +- **Verdict handling is now per-dispatch advisory-or-blocking.** `subagent-verdict-handler.sh` reads and consumes a per-agent "pending mode" marker written by the dispatching hook. A `fail`/`block` verdict blocks (exit 2) only when the marker is `blocking` (the default when no marker exists, which preserves the plan-validator and done-claim gates); the design-review pass writes `advisory` markers so its findings never halt the flow. +- **Native plan-mode exit now also runs an advisory challenger** alongside the existing blocking plan-validator (the validator gate is unchanged). + ## [0.5.1] - 2026-06-01 ### Fixed