Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ jobs:
- name: Test
run: dart test

hooks:
name: 🪝 Hook Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Check VGV CLI hook tests
run: bash hooks/check_vgv_cli_test.sh

skills-lint:
name: 🔍 Skills Lint
uses: VeryGoodOpenSource/very_good_workflows/.github/workflows/skills_lint.yml@v1.21.1
Expand Down
2 changes: 1 addition & 1 deletion .mcp.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"mcpServers": {
"very_good_cli": {
"very-good-cli": {
"command": "very_good",
"args": ["mcp"]
}
Expand Down
12 changes: 7 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ config/
cspell.json # Spell check dictionary
custom.markdownlint.jsonc # markdownlint rules
hooks/
hooks.json # Hook definitions (PostToolUse)
hooks.json # Hook definitions (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
references/
ffca/ # Byte mirror of the VGV Engineering FFCA pages, single source of truth
Expand Down Expand Up @@ -61,7 +63,7 @@ Every `SKILL.md` follows this structure:
1. **YAML frontmatter** with these fields:
- `name`: required. Must match the skill's folder name exactly, lowercase letters, numbers, and hyphens only, prefixed `ffca-`.
- `description`: required. What the skill covers, when to use it, the trigger phrases that should activate it, and the deferral to `layered-architecture` for non-FFCA repos.
- `allowed-tools`: space-separated list of tools the skill may use, for example `Read Glob Grep`. Read-only skills stop there. Skills that write code add `Write Edit`, and MCP tools use their full name, for example `mcp__very_good_cli__create`.
- `allowed-tools`: space-separated list of tools the skill may use, for example `Read Glob Grep`. Read-only skills stop there. Skills that write code add `Write Edit`, and MCP tools use their full plugin-scoped name, for example `mcp__plugin_vgv-ffca-plugin_very-good-cli__create`.
- `effort`: reasoning effort while the skill is active. Every skill here sets `high`.
2. **H1 title**, the human-readable skill name.
3. **A short purpose paragraph**, then numbered workflow steps.
Expand Down Expand Up @@ -103,9 +105,9 @@ 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 **Hook** 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 **Hook** section of `README.md` and the `## Hooks` section of `CLAUDE.md`.
- **An MCP tool is added, renamed, or removed.** Check every skill's `allowed-tools`. Nothing validates those names.
- **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`.
- **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: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@

## Hooks

`hooks/hooks.json` defines one PostToolUse hook.
`hooks/hooks.json` defines one PostToolUse hook and one PreToolUse hook.

- `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.

`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.
11 changes: 9 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,9 @@ claude --plugin-dir .
| **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 |
| **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 `very_good_cli` shows connected |
| **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 |

After editing a `SKILL.md`, an agent, or `.claude-plugin/plugin.json`, restart the session to pick up the change. Edits to `hooks/validate_layers.sh` take effect on the next matching tool call.
After editing a `SKILL.md`, an agent, or `.claude-plugin/plugin.json`, restart the session to pick up the change. Edits to the hook scripts in `hooks/` take effect on the next matching tool call.

### Run the validator tests

Expand All @@ -110,6 +110,12 @@ Run the validator against a real FFCA workspace with:
dart run scripts/validate_layers.dart --all
```

The Very Good CLI hook has a shell test suite that stubs `very_good`:

```bash
bash hooks/check_vgv_cli_test.sh
```

### Validate before you push

Run the same plugin check CI runs, from the repository root:
Expand All @@ -129,6 +135,7 @@ Every pull request runs the following checks from `.github/workflows/ci.yaml`:
| Markdown Quality | Lints all `*.md` files with markdownlint-cli2, except `CHANGELOG.md` and `references/ffca/` | `config/custom.markdownlint.jsonc` |
| Spelling Check | Runs cspell on all `*.md`, `*.yml`, and `*.yaml` files except `CHANGELOG.md` | `config/cspell.json` |
| Layer Validator | Runs `dart analyze --fatal-infos`, `dart format --set-exit-if-changed`, and `dart test` in `scripts/` | `scripts/pubspec.yaml` |
| Hook Tests | Runs `bash hooks/check_vgv_cli_test.sh` | `hooks/check_vgv_cli_test.sh` |
| Skills Lint | Validates every `SKILL.md` in `skills/` | Very Good Workflows `skills_lint` |
| Plugin Validation | Validates the plugin | `claude plugin validate .` |

Expand Down
38 changes: 31 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Developed with 💙 by [Very Good Ventures](https://verygood.ventures) 🦄
VGV FFCA Plugin teaches Claude the Feature-First Clean Architecture conventions and enforces its layer rules as you work. It ships three asset types:

- **Skills** that guide FFCA workflows: where code lives, how to scaffold a feature, how to wire routing, how to couple features, and how to audit a repo.
- **A blocking validation hook** that runs on every `pubspec.yaml` edit and stops the edit when it breaks a layer dependency rule, with the rule and the fix in the message so Claude self-corrects.
- **Hooks**: a blocking validation hook that runs on every `pubspec.yaml` edit and stops the edit when it breaks a layer dependency rule, with the rule and the fix in the message so Claude self-corrects, plus a Very Good CLI check that gates its MCP tool calls.
- **An MCP configuration** that wires the Very Good CLI server for project and package operations.

The conventions themselves live in `references/ffca/`, a byte mirror of the canonical FFCA documentation on [VGV Engineering](https://engineering.verygood.ventures/architecture/ffca/overview/), regenerated by `scripts/sync_reference.dart`. The skills never restate the conventions: they point into the mirror by file and section, so the architecture has exactly one source of truth.
Expand Down Expand Up @@ -112,13 +112,14 @@ Skills activate automatically when Claude detects an FFCA repo or an FFCA-shaped
/ffca-audit
```

## Hook
## 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 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.

| Hook | Behavior |
| --- | --- |
| **Validate layers** | 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 |
| Hook | Event | Behavior |
| --- | --- | --- |
| **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 |

The validator is also runnable directly for CI and audits, across the whole workspace:

Expand All @@ -129,7 +130,30 @@ 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 hook is skipped gracefully if `jq` or `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.

Run the hook tests with:

```bash
bash hooks/check_vgv_cli_test.sh
```

## MCP Integration

The plugin's `.mcp.json` connects Claude Code to the **Very Good CLI MCP server** (`very_good mcp`) under the server name `very-good-cli`, the same name vgv-ai-flutter-plugin uses. The `ffca-feature` skill calls it to scaffold and resolve each feature package.

| Tool | What it does |
| --- | --- |
| `create` | Scaffold packages from templates. FFCA uses `dart_package` for domain and data, `flutter_package` for presentation |
| `packages_get` | Get dependencies for a single package or recursively across the monorepo |
| `test` | Run tests with coverage enforcement |
| `packages_check_licenses` | Audit dependency licenses against an allowed list |

When installed from a marketplace, Claude Code names these tools `mcp__plugin_vgv-ffca-plugin_very-good-cli__<tool>`, and the skills' `allowed-tools` use that full name.

The plugin does not bundle the Dart MCP server (`dart mcp-server`). No FFCA skill calls it, and vgv-ai-flutter-plugin already provides it for analyze and format.

The server needs **Very Good CLI** 1.3.0 or newer. Install it with `dart pub global activate very_good_cli`.

## Agent

Expand Down
67 changes: 67 additions & 0 deletions hooks/check_vgv_cli.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/bin/bash
set -euo pipefail

# PreToolUse hook: gate Very Good CLI MCP tool calls.
# Mirrors check-vgv-cli.sh in vgv-ai-flutter-plugin. When the CLI is installed
# and new enough, auto-approve the call so it works in every run mode, since a
# skill's allowed-tools grant only lasts for the turn that invoked it.
# Otherwise deny with an install or upgrade message instead of letting the
# call fail silently.

MIN_VERSION="1.3.0"

# Read the hook payload from stdin.
input=$(cat)

# Graceful skip if jq is unavailable: normal permission handling applies.
if ! command -v jq &>/dev/null; then
echo "check_vgv_cli hook: jq not found, skipping" >&2
exit 0
fi

decide() {
jq -n --arg decision "$1" --arg reason "$2" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: $decision,
permissionDecisionReason: $reason
}
}'
exit 0
}

# The matcher is not a guarantee, so confirm the caller is a Very Good CLI
# tool. Covers mcp__very-good-cli__<tool> and the marketplace form
# mcp__plugin_<plugin>_very-good-cli__<tool>.
tool_name=$(jq -r '.tool_name // empty' <<<"$input")
if [[ "$tool_name" != mcp__*very-good-cli__* ]]; then
exit 0
fi

# Resolve very_good from PATH, falling back to the pub cache, which a hook
# subprocess does not necessarily have on its PATH.
vgv_bin=$(command -v very_good || true)
if [[ -z "$vgv_bin" ]]; then
fallback="${PUB_CACHE:-$HOME/.pub-cache}/bin/very_good"
[[ -x "$fallback" ]] && vgv_bin=$fallback
fi
if [[ -z "$vgv_bin" ]]; then
decide deny "Very Good CLI is not installed. This tool requires Very Good CLI >= ${MIN_VERSION}. Install with: dart pub global activate very_good_cli"
fi

# The very_good shim execs dart, so an unreadable version is inconclusive, not
# missing. Stand aside rather than deny a genuine call.
version=$("$vgv_bin" --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || true)
if [[ -z "$version" ]]; then
exit 0
fi

IFS='.' read -r major minor patch <<<"$version"
IFS='.' read -r min_major min_minor min_patch <<<"$MIN_VERSION"
if ((major < min_major ||
(major == min_major && minor < min_minor) ||
(major == min_major && minor == min_minor && patch < min_patch))); then
decide deny "Very Good CLI ${version} is too old. This tool requires Very Good CLI >= ${MIN_VERSION}. Update with: dart pub global activate very_good_cli"
fi

decide allow "Very Good CLI >= ${MIN_VERSION} verified; auto-approving Very Good CLI MCP tool call."
127 changes: 127 additions & 0 deletions hooks/check_vgv_cli_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
#!/bin/bash
# Tests for check_vgv_cli.sh
#
# Usage: bash hooks/check_vgv_cli_test.sh
#
# The hook reads a JSON payload from stdin and either emits a decision JSON on stdout
# (allow/deny) or exits silently, meaning it stood aside. A non-zero exit is its own
# outcome, never mistaken for standing aside. Every case runs against a
# stubbed very_good on a PATH that contains nothing else, so results do not depend on
# what is installed on the machine running the tests.

set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
HOOK="$SCRIPT_DIR/check_vgv_cli.sh"

PASSED=0
FAILED=0

STUB_DIR="$(mktemp -d)"
trap 'rm -rf "$STUB_DIR"' EXIT

BASE_PATH="$(dirname "$(command -v jq)"):/usr/bin:/bin:/usr/sbin:/sbin"

# Install a stubbed very_good. With a version argument the stub reports that version;
# with no argument it fails the way the real shim does when `dart` is missing from PATH.
# Usage: stub_cli [version] [--in-pub-cache]
stub_cli() {
local version="${1:-}"
local location="${2:-}"
local target="$STUB_DIR/very_good"
rm -rf "$STUB_DIR/pub-cache" "$STUB_DIR/very_good"
if [ "$location" = "--in-pub-cache" ]; then
mkdir -p "$STUB_DIR/pub-cache/bin"
target="$STUB_DIR/pub-cache/bin/very_good"
fi
if [ -z "$version" ]; then
printf '#!/bin/sh\necho "very_good: dart: command not found" >&2\nexit 127\n' > "$target"
else
printf '#!/bin/sh\necho "very_good %s"\n' "$version" > "$target"
fi
chmod +x "$target"
}

no_cli() { rm -rf "$STUB_DIR/pub-cache" "$STUB_DIR/very_good"; }

# Prints the permissionDecision ("allow"/"deny"), "aside" when the hook exited 0 with
# no decision, or "exit:<status>" on a non-zero exit, so a crashing hook cannot pass
# as having stood aside.
run_hook() {
local payload="$1"
local output status=0
output=$(printf '%s' "$payload" \
| env -i PATH="$STUB_DIR:$BASE_PATH" HOME="$STUB_DIR" PUB_CACHE="$STUB_DIR/pub-cache" \
bash "$HOOK" 2>/dev/null) || status=$?
if [ "$status" -ne 0 ]; then
echo "exit:$status"
elif [ -z "$output" ]; then
echo "aside"
else
echo "$output" | jq -r '.hookSpecificOutput.permissionDecision // "malformed"'
fi
}

assert_decision() {
local expected="$1" label="$2" payload="$3"
local result
result=$(run_hook "$payload")
if [ "$result" = "$expected" ]; then
printf " \033[32mPASS\033[0m %-10s %s\n" "$expected" "$label"
PASSED=$((PASSED + 1))
else
printf " \033[31mFAIL\033[0m expected %s but got %s: %s\n" "$expected" "$result" "$label"
FAILED=$((FAILED + 1))
fi
}

VGV_TOOL='{"tool_name":"mcp__very-good-cli__create","tool_input":{}}'
VGV_PLUGIN_TOOL='{"tool_name":"mcp__plugin_vgv-ffca-plugin_very-good-cli__packages_get","tool_input":{}}'

echo "=== check_vgv_cli tests ==="
echo ""
echo "--- Tools that are not this hook's business (stand aside) ---"
no_cli
assert_decision aside "server-less browser MCP call" '{"tool_name":"MCP:browser_tabs","tool_input":{"action":"list"}}'
assert_decision aside "browser MCP call, Claude naming" '{"tool_name":"mcp__cursor-ide-browser__browser_navigate","tool_input":{}}'
assert_decision aside "shell tool call" '{"tool_name":"Bash","tool_input":{"command":"ls"}}'
assert_decision aside "empty payload" '{}'
assert_decision aside "null tool_name" '{"tool_name":null}'
# A payload with no server segment is indistinguishable from any other server's `test`.
# Standing aside is the safe result.
assert_decision aside "server-less MCP name" '{"tool_name":"MCP:test","tool_input":{}}'
assert_decision aside "old underscore server name" '{"tool_name":"mcp__very_good_cli__create","tool_input":{}}'

echo ""
echo "--- Very Good CLI tools, CLI present and current ---"
stub_cli 1.5.0
assert_decision allow "version above minimum" "$VGV_TOOL"
assert_decision allow "marketplace-namespaced tool" "$VGV_PLUGIN_TOOL"
stub_cli 1.3.0
assert_decision allow "version exactly at minimum" "$VGV_TOOL"

echo ""
echo "--- Very Good CLI tools, CLI missing or outdated ---"
stub_cli 1.2.9
assert_decision deny "version below minimum" "$VGV_TOOL"
stub_cli 0.9.0
assert_decision deny "major version below minimum" "$VGV_TOOL"
no_cli
assert_decision deny "CLI not installed" "$VGV_TOOL"

echo ""
echo "--- CLI reachable only through the pub-cache fallback ---"
stub_cli 1.5.0 --in-pub-cache
assert_decision allow "resolved from PUB_CACHE, not PATH" "$VGV_TOOL"

echo ""
echo "--- Version unreadable (shim present, dart missing) ---"
stub_cli
assert_decision aside "inconclusive check does not deny" "$VGV_TOOL"

echo ""
echo "=== Results: $PASSED passed, $FAILED failed ==="

if [ "$FAILED" -gt 0 ]; then
exit 1
fi
Loading
Loading