From 427c05b52306c16cfa79866f931810e9da3aa902 Mon Sep 17 00:00:00 2001 From: Dominik Simonik Date: Tue, 29 Sep 2026 08:43:39 +0200 Subject: [PATCH 1/2] feat: warn when FFCA layer validation prerequisites are missing Add a SessionStart hook that tells Claude when dart or jq is missing in an FFCA-shaped repo, since validate_layers.sh skips silently without them. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yaml | 2 + README.md | 6 +- hooks/hooks.json | 13 ++- hooks/warn_missing_prereqs.sh | 30 ++++++ hooks/warn_missing_prereqs_test.sh | 156 +++++++++++++++++++++++++++++ 5 files changed, 204 insertions(+), 3 deletions(-) create mode 100755 hooks/warn_missing_prereqs.sh create mode 100755 hooks/warn_missing_prereqs_test.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b46d1ce..39ba40c 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -63,6 +63,8 @@ jobs: - uses: actions/checkout@v7 - name: Check VGV CLI hook tests run: bash hooks/check_vgv_cli_test.sh + - name: Warn missing prerequisites hook tests + run: bash hooks/warn_missing_prereqs_test.sh skills-lint: name: 🔍 Skills Lint diff --git a/README.md b/README.md index a08bf19..e0c63c6 100644 --- a/README.md +++ b/README.md @@ -114,10 +114,11 @@ Skills activate automatically when Claude detects an FFCA repo or an FFCA-shaped ## Hooks -A PostToolUse hook runs on every `Edit` or `Write`. When the edited file is a `pubspec.yaml` inside an FFCA-shaped repo, it validates the package's layer dependencies. A PreToolUse hook gates every Very Good CLI MCP tool call. +A PostToolUse hook runs on every `Edit` or `Write`. When the edited file is a `pubspec.yaml` inside an FFCA-shaped repo, it validates the package's layer dependencies. A PreToolUse hook gates every Very Good CLI MCP tool call. A SessionStart hook checks up front that the validator can actually run. | Hook | Event | Behavior | | --- | --- | --- | +| **Warn missing prerequisites** (`warn_missing_prereqs.sh`) | SessionStart | In an FFCA-shaped repo (a `features/` folder with `*_domain`, `*_data`, or `*_presentation` packages), adds a warning to Claude's context when `dart` or `jq` is not on `PATH`, so Claude knows layer validation is off. Non-blocking, silent otherwise | | **Validate layers** (`validate_layers.sh`) | PostToolUse (`Edit`/`Write`) | Runs the FFCA validator incrementally on the edited package and its direct dependents. Exits 2 on a violation (blocking: Claude must fix the dependency before continuing), printing the rule and the fix. Passes silently otherwise | | **Check VGV CLI** (`check_vgv_cli.sh`) | PreToolUse (`mcp__.*very-good-cli__.*`) | Auto-approves Very Good CLI MCP tool calls when the CLI is installed at 1.3.0 or newer, so they work in every run mode. Denies with an install or upgrade message when the CLI is missing or outdated. Stands aside for any other tool, or when the CLI version cannot be read | @@ -130,12 +131,13 @@ dart run scripts/validate_layers.dart --all ### Prerequisites - **Dart SDK** must be available on your `PATH`. The validator imports only `dart:io`, so it runs with just the SDK, no `dart pub get` required. -- **jq** is used to parse the hook payload. The hooks are skipped gracefully if `jq` is not installed, and the validation hook also if `dart` is not installed. +- **jq** is used to parse the hook payload. The hooks are skipped gracefully if `jq` is not installed, and the validation hook also if `dart` is not installed. In an FFCA repo, the SessionStart hook tells Claude when validation is off for either reason. Run the hook tests with: ```bash bash hooks/check_vgv_cli_test.sh +bash hooks/warn_missing_prereqs_test.sh ``` ## MCP Integration diff --git a/hooks/hooks.json b/hooks/hooks.json index 2543f4f..fe5c3d7 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -1,6 +1,17 @@ { - "description": "VGV FFCA plugin hooks: enforces Feature-First Clean Architecture layer rules on pubspec.yaml edits and gates Very Good CLI MCP tool calls", + "description": "VGV FFCA plugin hooks: warns when layer validation prerequisites are missing, enforces Feature-First Clean Architecture layer rules on pubspec.yaml edits, and gates Very Good CLI MCP tool calls", "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"${CLAUDE_PLUGIN_ROOT}/hooks/warn_missing_prereqs.sh\"", + "timeout": 10 + } + ] + } + ], "PreToolUse": [ { "matcher": "mcp__.*very-good-cli__.*", diff --git a/hooks/warn_missing_prereqs.sh b/hooks/warn_missing_prereqs.sh new file mode 100755 index 0000000..36230dc --- /dev/null +++ b/hooks/warn_missing_prereqs.sh @@ -0,0 +1,30 @@ +#!/bin/bash +# SessionStart hook: warn when FFCA layer validation cannot run. +# validate_layers.sh skips silently (stderr only) without jq or dart, so Claude +# would never learn the PostToolUse check is off. Output is injected into +# Claude's context. Only speaks up in FFCA-shaped repos. Non-blocking, always +# exits 0. + +root="${CLAUDE_PROJECT_DIR:-$PWD}" + +# FFCA-shaped: a features/{feature}/{feature}_{domain,data,presentation} +# package somewhere under the project. Hidden, build, and node_modules folders +# are pruned so the scan stays fast on large repos. +ffca=$(find "$root" -mindepth 1 -maxdepth 6 \ + \( -name '.*' -o -name build -o -name node_modules \) -prune -o \ + \( -path '*/features/*/*_domain/pubspec.yaml' \ + -o -path '*/features/*/*_data*/pubspec.yaml' \ + -o -path '*/features/*/*_presentation/pubspec.yaml' \) \ + -print -quit 2>/dev/null) +[[ -n "$ffca" ]] || exit 0 + +missing=() +command -v dart &>/dev/null || missing+=("dart") +command -v jq &>/dev/null || missing+=("jq") +[[ ${#missing[@]} -gt 0 ]] || exit 0 + +list=$(printf '%s, ' "${missing[@]}") +list=${list%, } +echo "⚠️ FFCA layer validation is disabled this session: ${list} not found on the PATH available to hooks. The vgv-ffca-plugin PostToolUse hook will skip every pubspec.yaml edit, so layer violations will not be caught. Install the missing tools (Dart SDK: https://dart.dev/get-dart, jq: https://jqlang.org/download), make sure they are on PATH for non-interactive shells (e.g. in ~/.zprofile), and start a new session." + +exit 0 diff --git a/hooks/warn_missing_prereqs_test.sh b/hooks/warn_missing_prereqs_test.sh new file mode 100755 index 0000000..bfe2f55 --- /dev/null +++ b/hooks/warn_missing_prereqs_test.sh @@ -0,0 +1,156 @@ +#!/bin/bash +# Tests for warn_missing_prereqs.sh +# +# Usage: bash hooks/warn_missing_prereqs_test.sh +# +# The hook takes no input. In an FFCA-shaped project it prints a warning to +# stdout when dart or jq is missing from PATH, and prints nothing otherwise. It +# always exits 0. Every case runs on a PATH holding only stubbed tools plus +# find, so results do not depend on what is installed on the machine. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HOOK="$SCRIPT_DIR/warn_missing_prereqs.sh" +BASH_BIN="$(command -v bash)" + +PASSED=0 +FAILED=0 + +TMP_DIR="$(mktemp -d)" +trap 'rm -rf "$TMP_DIR"' EXIT + +BIN_DIR="$TMP_DIR/bin" +mkdir -p "$BIN_DIR" +ln -s "$(command -v find)" "$BIN_DIR/find" + +# Installs or removes stubbed tools on the hook's PATH. +# Usage: tools dart jq (installs exactly the named tools) +tools() { + rm -f "$BIN_DIR/dart" "$BIN_DIR/jq" + local t + for t in "$@"; do + printf '#!/bin/sh\nexit 0\n' > "$BIN_DIR/$t" + chmod +x "$BIN_DIR/$t" + done +} + +# Creates a project directory with pubspec.yaml files at the given paths. +# Usage: project ... +project() { + local dir="$TMP_DIR/projects/$1" + shift + rm -rf "$dir" + mkdir -p "$dir" + local pkg + for pkg in "$@"; do + mkdir -p "$dir/$pkg" + echo "name: $(basename "$pkg")" > "$dir/$pkg/pubspec.yaml" + done + PROJECT="$dir" +} + +# Runs the hook from $PROJECT. Leaves stdout in LAST_OUTPUT and the exit status +# in LAST_STATUS. Pass --env to point at the project via CLAUDE_PROJECT_DIR +# while running from elsewhere. +LAST_OUTPUT="" +LAST_STATUS=0 +run_hook() { + LAST_STATUS=0 + if [ "${1:-}" = "--env" ]; then + LAST_OUTPUT=$(cd "$TMP_DIR" && env -i PATH="$BIN_DIR" CLAUDE_PROJECT_DIR="$PROJECT" \ + "$BASH_BIN" "$HOOK" 2>/dev/null) || LAST_STATUS=$? + else + LAST_OUTPUT=$(cd "$PROJECT" && env -i PATH="$BIN_DIR" \ + "$BASH_BIN" "$HOOK" 2>/dev/null) || LAST_STATUS=$? + fi +} + +pass() { printf " \033[32mPASS\033[0m %s\n" "$1"; PASSED=$((PASSED + 1)); } +fail() { printf " \033[31mFAIL\033[0m %s\n got (exit %s): %s\n" "$1" "$LAST_STATUS" "$LAST_OUTPUT"; FAILED=$((FAILED + 1)); } + +# The hook is non-blocking: it must exit 0 whatever it finds. +assert_exit_zero() { + if [ "$LAST_STATUS" -eq 0 ]; then pass "exit 0: $1"; else fail "expected exit 0: $1"; fi +} + +assert_silent() { + local label="$1" + run_hook "${2:-}" + assert_exit_zero "$label" + if [ -z "$LAST_OUTPUT" ]; then pass "silent: $label"; else fail "expected no warning: $label"; fi +} + +assert_warns() { + local needle="$1" label="$2" + run_hook "${3:-}" + assert_exit_zero "$label" + if [[ "$LAST_OUTPUT" == *"$needle"* ]]; then + pass "warns '$needle': $label" + else + fail "expected warning containing '$needle': $label" + fi +} + +assert_not_mentions() { + local needle="$1" label="$2" + if [[ "$LAST_OUTPUT" != *"$needle"* ]]; then + pass "omits '$needle': $label" + else + fail "expected warning without '$needle': $label" + fi +} + +FFCA_PKGS=(features/cart/cart_domain features/cart/cart_data features/cart/cart_presentation) + +echo "=== warn_missing_prereqs tests ===" +echo "" +echo "--- FFCA repo, prerequisites present (no warning) ---" +project ffca "${FFCA_PKGS[@]}" +tools dart jq +assert_silent "dart and jq on PATH" + +echo "" +echo "--- FFCA repo, prerequisites missing ---" +tools jq +assert_warns "dart not found" "dart missing" +assert_not_mentions "jq," "dart missing" +tools dart +assert_warns "jq not found" "jq missing" +tools +assert_warns "dart, jq not found" "both missing" +assert_warns "dart, jq not found" "project found via CLAUDE_PROJECT_DIR" --env + +echo "" +echo "--- FFCA detection variants ---" +tools +project domain_only features/cart/cart_domain +assert_warns "FFCA layer validation is disabled" "single *_domain package" +project data_suffix features/auth/auth_data_firebase +assert_warns "FFCA layer validation is disabled" "*_data_ package" +project nested packages/app/features/cart/cart_presentation +assert_warns "FFCA layer validation is disabled" "features/ nested below the root" + +echo "" +echo "--- Not an FFCA repo (no warning even without tools) ---" +tools +project plain packages/some_pkg +assert_silent "no features/ folder" +project empty_features features +assert_silent "empty features/ folder" +project no_pubspec +mkdir -p "$PROJECT/features/cart/cart_domain" +assert_silent "layer folder without pubspec.yaml" +project wrong_names features/cart/cart_utils +assert_silent "features/ package without a layer suffix" +project hidden .dart_tool/features/cart/cart_domain +assert_silent "features/ only inside a hidden folder" +project build_dir build/features/cart/cart_domain +assert_silent "features/ only inside build/" + +echo "" +echo "=== Results: $PASSED passed, $FAILED failed ===" + +if [ "$FAILED" -gt 0 ]; then + exit 1 +fi From 13d98ccf3228a207605365b2ce4ae08f9a131632 Mon Sep 17 00:00:00 2001 From: Dominik Simonik Date: Wed, 30 Sep 2026 10:57:39 +0200 Subject: [PATCH 2/2] docs: document SessionStart hook in AGENTS, CLAUDE, and CONTRIBUTING Co-Authored-By: Claude Opus 5.5 --- AGENTS.md | 10 ++++++---- CLAUDE.md | 6 ++++-- CONTRIBUTING.md | 4 ++-- config/cspell.json | 1 + 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 06ba364..bca5b1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,11 +2,11 @@ ## Project Overview -VGV FFCA Plugin teaches Claude Code the Feature-First Clean Architecture (FFCA) for Flutter monorepos. It ships skills, one read-only auditor agent, and a PostToolUse hook that blocks pubspec edits breaking the layer rules. +VGV FFCA Plugin teaches Claude Code the Feature-First Clean Architecture (FFCA) for Flutter monorepos. It ships skills, one read-only auditor agent, a PostToolUse hook that blocks pubspec edits breaking the layer rules, and a SessionStart hook that warns when that check cannot run. The skills and the agent carry workflows only. The architecture itself lives in `references/ffca/`, which is the single source of truth. It is a byte mirror of the FFCA section of VGV Engineering, one file per page, regenerated by `scripts/sync_reference.dart`. Never edit its content to match a skill. Change the upstream page, re-sync, then fix the skills that drifted. -The only code is the Dart layer validator under `scripts/` and the bash hook wrapper that calls it. +The only code is the Dart layer validator under `scripts/` and the bash hook scripts under `hooks/`. ## Repository Structure @@ -20,10 +20,12 @@ config/ cspell.json # Spell check dictionary custom.markdownlint.jsonc # markdownlint rules hooks/ - hooks.json # Hook definitions (PreToolUse, PostToolUse) + hooks.json # Hook definitions (SessionStart, PreToolUse, PostToolUse) check_vgv_cli.sh # Checks the Very Good CLI version and auto-approves its MCP tools check_vgv_cli_test.sh # Tests for check_vgv_cli.sh validate_layers.sh # Runs the validator on edited pubspec.yaml files + warn_missing_prereqs.sh # Warns Claude when dart or jq is missing in an FFCA repo + warn_missing_prereqs_test.sh # Tests for the SessionStart hook references/ ffca/ # Byte mirror of the VGV Engineering FFCA pages, single source of truth README.md # Manifest: which upstream page each file mirrors @@ -106,7 +108,7 @@ Documentation drifts when an asset changes and the docs describing it do not. Up - **The FFCA architecture changes.** Update the VGV Engineering page first, then run `dart run scripts/sync_reference.dart` to re-sync `references/ffca/`. Check every skill, the agent, and the code templates for section names that moved or rules that changed. If a dependency rule changed, update the rules table at the top of `scripts/validate_layers.dart` and its tests. - **A skill's scope or triggers change.** Update `description` and the matching row in the `README.md` Skills table. - **The validator's rules or flags change.** Update `scripts/test/validate_layers_test.dart` and its fixtures, the **Hooks** section of `README.md`, and the `## Hooks` section of `CLAUDE.md` if the hook's behavior changes. -- **A hook changes in `hooks/hooks.json`.** Update the **Hooks** section of `README.md` and the `## Hooks` section of `CLAUDE.md`. If it is `check_vgv_cli.sh`, update `hooks/check_vgv_cli_test.sh`. +- **A hook changes in `hooks/hooks.json`.** Update the **Hooks** section of `README.md` and the `## Hooks` section of `CLAUDE.md`. If it is `check_vgv_cli.sh` or `warn_missing_prereqs.sh`, update its `_test.sh`. - **An MCP tool is added, renamed, or removed.** Check every skill's `allowed-tools` and the **MCP Integration** section of `README.md`. Nothing validates those names. ## Checks diff --git a/CLAUDE.md b/CLAUDE.md index 055faeb..0971288 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -5,7 +5,9 @@ ## Hooks -`hooks/hooks.json` defines one PostToolUse hook and one PreToolUse hook. +`hooks/hooks.json` defines one SessionStart hook, one PostToolUse hook, and one PreToolUse hook. + +- SessionStart → `warn_missing_prereqs.sh`. In an FFCA-shaped repo, meaning a `features/*/*_domain`, `*_data*`, or `*_presentation` package with a `pubspec.yaml` under the project, it prints a warning into Claude's context when `dart` or `jq` is missing from `PATH`. It never reads stdin, since `jq` may be the missing tool, and always exits 0. - `Edit|Write` matcher → `validate_layers.sh`. When the edited file is a `pubspec.yaml` inside an FFCA-shaped repo, meaning a `features/` folder exists above it, the hook runs `dart run scripts/validate_layers.dart --file `. The validator checks the edited package and its direct dependents. Exit 2 means a layer violation. The hook prints the rule and the fix to stderr and exits 2, which blocks Claude until the dependency is fixed. - Any other file, or a repo with no `features/` folder, exits 0 silently. @@ -13,6 +15,6 @@ - `mcp__.*very-good-cli__.*` matcher → `check_vgv_cli.sh`. For a Very Good CLI MCP tool, it returns `allow` when `very_good --version` is 1.3.0 or newer and `deny` with an install or upgrade message when the CLI is missing or older. It stands aside with exit 0 for any other tool, when the version cannot be read, or when `jq` is missing. `allowed-tools` grants only last for the invoking turn, so this hook is what keeps the tools approved afterwards. -`hooks/check_vgv_cli_test.sh` covers the PreToolUse hook against a stubbed `very_good` and runs in CI under the **Hook Tests** job. +`hooks/check_vgv_cli_test.sh` covers the PreToolUse hook against a stubbed `very_good` and runs in CI under the **Hook Tests** job. `hooks/warn_missing_prereqs_test.sh` covers the SessionStart hook and runs in the same job. `scripts/test/validate_layers_test.dart` covers the validator and runs in CI under the **Layer Validator** job. Add a fixture and a case there when changing a rule. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f43a338..aaef8b1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -72,7 +72,7 @@ Pushing straight to a PR only tells you the files are valid. Load your working c ### Prerequisites - **Claude Code CLI** installed (`npm install -g @anthropic-ai/claude-code`). -- **Dart SDK** and **jq** on your `PATH`. The hook needs both. +- **Dart SDK** and **jq** on your `PATH`. The hooks need both. - **Very Good CLI** (`dart pub global activate very_good_cli`) for the MCP server tools. ### Load your local copy @@ -90,7 +90,7 @@ claude --plugin-dir . | Component | How to verify | | --------- | ------------- | | **Skills** | Run `/help`. Skills appear namespaced as `/vgv-ffca-plugin:`, for example `/vgv-ffca-plugin:ffca-feature`. Invoke one to confirm it triggers | -| **Hook** | In an FFCA repo, have Claude add a forbidden dependency to a `pubspec.yaml`, for example a `_data` package to a `_presentation` package. The edit must be blocked with the rule and the fix | +| **Hooks** | In an FFCA repo, start a session with `dart` or `jq` removed from `PATH` and confirm Claude mentions that layer validation is off. Then, with both on `PATH`, have Claude add a forbidden dependency to a `pubspec.yaml`, for example a `_data` package to a `_presentation` package. The edit must be blocked with the rule and the fix | | **Agent** | Run `/agents` and confirm `ffca-layer-auditor` is listed, or run `/vgv-ffca-plugin:ffca-audit` and confirm it dispatches the agent | | **MCP server** | Run `/mcp` and confirm `plugin:vgv-ffca-plugin:very-good-cli` shows connected. Invoke `/vgv-ffca-plugin:ffca-feature` and confirm the `create` call runs without a permission prompt | diff --git a/config/cspell.json b/config/cspell.json index ec7d391..96672dd 100644 --- a/config/cspell.json +++ b/config/cspell.json @@ -12,6 +12,7 @@ "mocktail", "operationalizes", "posthog", + "prereqs", "pubspec", "pubspecs", "rebuildable",