Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 6 additions & 4 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@

## 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 <pubspec>`. 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.
- The hook skips with exit 0 when `jq` or `dart` is missing from `PATH`. A validator failure other than exit 2 is reported to stderr but never blocks.

- `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.
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -90,7 +90,7 @@ claude --plugin-dir .
| Component | How to verify |
| --------- | ------------- |
| **Skills** | Run `/help`. Skills appear namespaced as `/vgv-ffca-plugin:<skill>`, 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 |

Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |

Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions config/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"mocktail",
"operationalizes",
"posthog",
"prereqs",
"pubspec",
"pubspecs",
"rebuildable",
Expand Down
13 changes: 12 additions & 1 deletion hooks/hooks.json
Original file line number Diff line number Diff line change
@@ -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__.*",
Expand Down
30 changes: 30 additions & 0 deletions hooks/warn_missing_prereqs.sh
Original file line number Diff line number Diff line change
@@ -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
156 changes: 156 additions & 0 deletions hooks/warn_missing_prereqs_test.sh
Original file line number Diff line number Diff line change
@@ -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 <name> <relative package dir>...
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_<suffix> 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
Loading