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
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ hooks/
analyze.sh # Runs dart analyze on modified .dart files
block-cli-workarounds.sh # Prevents direct CLI bypass via Bash
block-cli-workarounds_test.sh # Tests for block-cli-workarounds.sh
check-vgv-cli.sh # Validates VGV CLI installed and >= 1.3.0
check-vgv-cli.sh # Validates VGV CLI installed and >= 1.5.0
check-vgv-cli_test.sh # Tests for check-vgv-cli.sh
format.sh # Runs dart format on modified .dart files
vgv-cli-common.sh # Shared utilities for VGV CLI hook scripts
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ The `hooks/` directory contains SessionStart, PreToolUse, and PostToolUse hooks

These run **when a session begins**:

- `warn-missing-mcp.sh` β€” checks if Very Good CLI is installed and >= 1.3.0; outputs a warning to Claude's context if missing, outdated, or present but unable to run because `dart` is not on the `PATH` hooks inherit (non-blocking)
- `warn-missing-mcp.sh` β€” checks if Very Good CLI is installed and >= 1.5.0; outputs a warning to Claude's context if missing, outdated, or present but unable to run because `dart` is not on the `PATH` hooks inherit (non-blocking)

### PreToolUse Hooks

These run **before** a tool call is executed:

- `mcp__.*very-good-cli__.*` matcher β†’ `check-vgv-cli.sh` β€” reads `tool_name` from the payload and exits 0 unless the caller is a Very Good CLI tool, so a host that does not apply the matcher cannot have the decision land on an unrelated tool; for its own tools it auto-approves the call by returning a PreToolUse `allow` decision, so it is always permitted regardless of run mode (interactive, headless, or `skipAutoPermissionPrompt`) and never dead-ends when the tool isn't on `permissions.allow`; denies with an install/upgrade message if the CLI is missing or < 1.3.0, and stands aside when the CLI is present but its version cannot be read (`dart` missing from `PATH`), leaving normal permission handling to apply. The `.*` in the matcher covers both the bare `mcp__very-good-cli__*` server (repo-root `.mcp.json`) and the plugin-namespaced `mcp__plugin_<plugin>_very-good-cli__*` form used when installed from a marketplace
- `mcp__.*very-good-cli__.*` matcher β†’ `check-vgv-cli.sh` β€” reads `tool_name` from the payload and exits 0 unless the caller is a Very Good CLI tool, so a host that does not apply the matcher cannot have the decision land on an unrelated tool; for its own tools it auto-approves the call by returning a PreToolUse `allow` decision, so it is always permitted regardless of run mode (interactive, headless, or `skipAutoPermissionPrompt`) and never dead-ends when the tool isn't on `permissions.allow`; denies with an install/upgrade message if the CLI is missing or < 1.5.0, and stands aside when the CLI is present but its version cannot be read (`dart` missing from `PATH`), leaving normal permission handling to apply. The `.*` in the matcher covers both the bare `mcp__very-good-cli__*` server (repo-root `.mcp.json`) and the plugin-namespaced `mcp__plugin_<plugin>_very-good-cli__*` form used when installed from a marketplace
- `Bash` matcher β†’ `block-cli-workarounds.sh` β€” prevents direct CLI bypass of VGV CLI commands through the host's shell tool (`Bash`, or `Shell` on other hosts); exits 0 when the payload names a non-shell tool, so an unrelated tool carrying a `command` argument is never inspected; when the CLI is present but cannot run because `dart` is missing from `PATH`, the denial says so rather than redirecting to an MCP server that cannot start; every denial also says the whole shell call was refused, so the agent re-runs any command chained with the blocked one instead of assuming it ran; exits 2 on failure (blocking)

The first two PreToolUse hooks are plugin-level (defined in `hooks.json`) and share common utilities
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ session and exercise it before you commit.

- **Claude Code CLI** installed (`npm install -g @anthropic-ai/claude-code`).
- **Dart SDK** and **jq** on your `PATH` β€” the hooks need both.
- **Very Good CLI** β‰₯ 1.3.0 (`dart pub global activate very_good_cli`) for the
- **Very Good CLI** β‰₯ 1.5.0 (`dart pub global activate very_good_cli`) for the
Very Good CLI MCP server tools.

See the README [Hooks](README.md#hooks) and [MCP Integration](README.md#mcp-integration)
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ This plugin includes SessionStart, PreToolUse, and PostToolUse hooks that valida

| Hook | Trigger | Behavior |
| ---- | ------- | -------- |
| **Warn Missing MCP** (`warn-missing-mcp.sh`) | SessionStart | Warns if the Very Good CLI is missing, older than 1.3.0, or present but unable to run because `dart` is not on the `PATH` hooks inherit; non-blocking |
| **Check VGV CLI** (`check-vgv-cli.sh`) | PreToolUse (`mcp__.*very-good-cli__.*`) | Verifies from the payload that the caller is a Very Good CLI tool and stands aside otherwise, so the decision never lands on an unrelated tool; for its own tools, auto-approves the call in every run mode via a PreToolUse `allow` decision, so they never dead-end when the tool isn't on `permissions.allow` (including under `skipAutoPermissionPrompt`); denies with an install/upgrade message if the CLI is missing or < 1.3.0 |
| **Warn Missing MCP** (`warn-missing-mcp.sh`) | SessionStart | Warns if the Very Good CLI is missing, older than 1.5.0, or present but unable to run because `dart` is not on the `PATH` hooks inherit; non-blocking |
| **Check VGV CLI** (`check-vgv-cli.sh`) | PreToolUse (`mcp__.*very-good-cli__.*`) | Verifies from the payload that the caller is a Very Good CLI tool and stands aside otherwise, so the decision never lands on an unrelated tool; for its own tools, auto-approves the call in every run mode via a PreToolUse `allow` decision, so they never dead-end when the tool isn't on `permissions.allow` (including under `skipAutoPermissionPrompt`); denies with an install/upgrade message if the CLI is missing or < 1.5.0 |
| **Block CLI Workarounds** (`block-cli-workarounds.sh`) | PreToolUse (`Bash`) | Blocks direct CLI bypass of Very Good CLI commands through the host's shell tool; inspects the command only when the payload identifies a shell call, so an unrelated tool carrying a `command` argument is left alone; when the CLI is present but cannot run because `dart` is missing from `PATH`, the denial says so instead of redirecting to an MCP server that cannot start; exits 2 on failure (blocking) |
| **Allow Read-only Git** (`allow-readonly-git.sh`) | PreToolUse (`Bash`, `flutter-reviewer` agent only) | Restricts the `flutter-reviewer` agent's Bash to single-line `git diff`/`git status`, without the `--output` or `--ext-diff` options, and denies anything else (blocking). Scoped via the agent's frontmatter, not `hooks.json` |
| **Analyze** (`analyze.sh`) | PostToolUse (`Edit`/`Write`) | Runs `dart analyze` on the modified `.dart` file; exits 2 on failure (blocking β€” Claude must fix issues before continuing) |
Expand Down Expand Up @@ -186,7 +186,7 @@ The Very Good CLI MCP server exposes Very Good CLI commands to Claude.

**Prerequisites:**

- Very Good CLI v1.3.0+ installed: `dart pub global activate very_good_cli`
- Very Good CLI v1.5.0+ installed: `dart pub global activate very_good_cli`
- `very_good` must be on your PATH

**How it works:**
Expand Down
8 changes: 4 additions & 4 deletions hooks/scripts/check-vgv-cli_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,15 @@ assert_decision aside "server-less MCP name" '{"tool_name":"MCP:tes

echo ""
echo "--- Very Good CLI tools, CLI present and current ---"
stub_cli 1.5.0
stub_cli 1.6.0
assert_decision allow "version above minimum" "$VGV_TOOL"
assert_decision allow "marketplace-namespaced tool" "$VGV_PLUGIN_TOOL"
stub_cli 1.3.0
stub_cli 1.5.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
stub_cli 1.4.9
assert_decision deny "version below minimum" "$VGV_TOOL"
stub_cli 0.9.0
assert_decision deny "major version below minimum" "$VGV_TOOL"
Expand All @@ -110,7 +110,7 @@ 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
stub_cli 1.6.0 --in-pub-cache
assert_decision allow "resolved from PUB_CACHE, not PATH" "$VGV_TOOL"

echo ""
Expand Down
4 changes: 2 additions & 2 deletions hooks/scripts/vgv-cli-common.sh
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/bin/bash
# Shared helpers for Very Good CLI version checks and hook deny responses.

MIN_VERSION="1.3.0"
MIN_VERSION="1.5.0"
MIN_MAJOR=1
MIN_MINOR=3
MIN_MINOR=5
MIN_PATCH=0

deny() {
Expand Down
10 changes: 5 additions & 5 deletions hooks/scripts/warn-missing-mcp_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -80,19 +80,19 @@ assert_warns() {
echo "=== warn-missing-mcp tests ==="
echo ""
echo "--- CLI present and current (no warning) ---"
stub_cli 1.5.0
stub_cli 1.6.0
assert_silent "version above minimum"
stub_cli 1.3.0
stub_cli 1.5.0
assert_silent "version exactly at minimum"
stub_cli 1.5.0 --in-pub-cache
stub_cli 1.6.0 --in-pub-cache
assert_silent "resolved from PUB_CACHE, not PATH"

echo ""
echo "--- CLI missing or outdated ---"
no_cli
assert_warns "not installed" "CLI not installed"
stub_cli 1.2.9
assert_warns "1.2.9 is too old" "version below minimum"
stub_cli 1.4.9
assert_warns "1.4.9 is too old" "version below minimum"

echo ""
echo "--- Version unreadable (shim present, dart missing) ---"
Expand Down
75 changes: 46 additions & 29 deletions skills/green-gate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ Apply these to all green-gate work:
through a shell command. The Bash test path (`very_good test`, `flutter test`,
`dart test`) is hook-blocked by `block-cli-workarounds.sh` and will be denied, and
`dart analyze` / `dart format` via Bash are redundant with the MCP tools.
**Bash is reserved for parsing `coverage/lcov.info` β€” nothing else.** This MCP-only
**The only Bash the loop runs is the fallback `coverage/lcov.info` parse**, and
only when the test result carries no `Lines not covered:` block. This MCP-only
rule is a Claude Code constraint enforced by that hook; the hook does not run on
other hosts. **Cross-harness fallback:** on a host without the hook and without the
MCP servers connected, run the equivalent `dart analyze`, `dart format`, and
Expand All @@ -49,7 +50,7 @@ Apply these to all green-gate work:
tools, which arguments, or what order the gates run in and does not want a run
yet, answer from this skill: the same tool calls (`mcp__dart__analyze_files`
with `applyFixes: true`, `mcp__dart__dart_format`, `mcp__very-good-cli__test`
with the coverage triple), in gate order, with the precedence rule that makes
with the coverage parameters), in gate order, with the precedence rule that makes
the order matter. Never substitute an improvised shell plan of `flutter
analyze` / `flutter test --coverage` for the tools the loop actually runs.
- **Never cache green** β€” re-evaluate every gate every round. Fixing analyze or
Expand All @@ -58,10 +59,13 @@ analyze` / `flutter test --coverage` for the tools the loop actually runs.
- **Exit only on observed numbers** β€” the loop terminates only after a single
final iteration in which analyze is clean, format reports zero changes, all
tests pass, and `min_coverage` is satisfied, all observed in the same round.
- **Pass `coverage: true`, `min_coverage`, and `check_ignore: true` together** β€”
omitting `coverage: true` silently produces no `lcov.info` (mimics a
misconfiguration); omitting `check_ignore: true` makes the `// coverage:ignore`
remedy a no-op.
- **Pass `coverage: true`, `min_coverage`, `check_ignore: true`, and
`show_uncovered: true` together** β€” omitting `coverage: true` silently produces
no `lcov.info` (mimics a misconfiguration); omitting `check_ignore: true` makes
the `// coverage:ignore` remedy a no-op; omitting `show_uncovered: true` drops
the per-file uncovered lines from the result and sends the loop to the lcov file
for what the tool would have handed back. `show_uncovered` arrived in
`very_good_cli` 1.5.0, the plugin's minimum.
- **Defer test-writing to the `testing` skill** β€” when a fix requires authoring
tests, follow that skill for structure, mocking, and naming.
- **Fix root causes, not symptoms** β€” never weaken a gate to pass it (do not
Expand Down Expand Up @@ -89,9 +93,10 @@ For each package root (see **Recursive / Monorepo**), run this algorithm:
with the coverage parameters from **Test Gate**. If tests fail, fix, record
the fingerprint, go to step 7.
5. **Coverage** β€” only if tests pass this round. The MCP `min_coverage` result is
authoritative pass/fail. Parse `coverage/lcov.info` for the displayed
percentage and per-file fix targets (advisory). If below target, author tests
for the ranked under-covered files (via the `testing` skill), go to step 7.
authoritative pass/fail, and its `Lines not covered:` block gives the per-file
fix targets; parse `coverage/lcov.info` only when that block is absent. If
below target, author tests for the ranked under-covered files (via the
`testing` skill), go to step 7.
6. **Exit** β€” if all four gates are green in _this same iteration_, confirm
success with the observed numbers and stop. This is the only exit-green path.
7. **Re-verify** β€” increment the iteration counter, recompute the failure
Expand Down Expand Up @@ -123,7 +128,7 @@ Fingerprint keys per gate:
| **Analyze** | Sorted set of `diagnosticCode @ file:line` |
| **Format** | Set of files the format gate rewrote (empty = green) |
| **Test** | Set of failing test IDs / names |
| **Coverage** | Observed percentage + sorted set of under-covered `SF` files |
| **Coverage** | Observed percentage + sorted set of under-covered file paths |

Definitions:

Expand All @@ -141,7 +146,7 @@ Fixed order: **analyze β†’ format β†’ test β†’ coverage**. Two rules govern it:

1. **A downstream gate is not assessed until the upstream gate is green this
round.** A red analyzer can make tests fail to compile; coverage is
meaningless when tests do not pass β€” never parse `lcov.info` after a compile
meaningless when tests do not pass β€” never diagnose coverage after a compile
failure. Format runs after analyze so its reformatting does not churn over
code that analyze's `applyFixes` is about to rewrite.
2. **Every gate is re-evaluated every round; green is never cached.** Exit
Expand Down Expand Up @@ -188,7 +193,8 @@ Fixed order: **analyze β†’ format β†’ test β†’ coverage**. Two rules govern it:
## Test Gate

- Run `mcp__very-good-cli__test` with `coverage: true`, `min_coverage: <target>`,
`check_ignore: true`, and the `exclude_coverage` glob (below).
`check_ignore: true`, `show_uncovered: true`, and the `exclude_coverage` glob
(below).
- **Dart vs Flutter** β€” omit `dart` to let the tool auto-detect (Flutter is run
when a Flutter project is detected). Pass `dart: true` only for a pure Dart
package the tool would otherwise misclassify.
Expand All @@ -206,9 +212,18 @@ Fixed order: **analyze β†’ format β†’ test β†’ coverage**. Two rules govern it:

## Coverage Gate

The MCP `min_coverage` result is **authoritative** for pass/fail. The
`coverage/lcov.info` parse is **advisory** β€” it supplies the displayed
percentage and per-file fix targets.
The MCP `min_coverage` result is **authoritative** for pass/fail, and with
`show_uncovered: true` it also carries the per-file fix targets:

```text
Expected coverage >= 100.00% but actual is 33.33%.
Lines not covered:
- lib/src/weather_repository.dart: 4, 5
```

Rank the files from that block. The `coverage/lcov.info` parse is the
**fallback**, for a result that has no such block: lcov is left behind by a run
that may not be the one being diagnosed, while the result cannot be stale.

- **Default target is `100`** (VGV's house standard), **overridable per
invocation** (e.g. `80`) for legacy or non-template packages. Generated files
Expand All @@ -217,23 +232,24 @@ percentage and per-file fix targets.
- **Default `exclude_coverage`** β€” `**/*.{g,freezed,gen}.dart` plus generated and
l10n directories. One glob string (brace expansion; fall back to `**/*.g.dart`
if a CLI build does not honor it).
- **Pass `coverage: true`, `min_coverage`, and `check_ignore: true` together** β€”
see **Core Standards**.
- **When `coverage/lcov.info` is missing or below target**, follow the decision
tree and parsing rules in [`references/coverage.md`](references/coverage.md) β€”
it covers the three absence causes, stale-lcov handling, the lcov record fields
(`SF`/`LF`/`LH`/`DA`), the advisory-mirrors-the-gate rule, and the
`check_ignore` Dart-only limitation.
- **Pass `coverage: true`, `min_coverage`, `check_ignore: true`, and
`show_uncovered: true` together** β€” see **Core Standards**.
- **When the result carries no `Lines not covered:` block**, fall back to
`coverage/lcov.info` per [`references/coverage.md`](references/coverage.md) β€”
it covers `show_uncovered` itself, the three lcov absence causes, stale-lcov
handling, the lcov record fields (`SF`/`LF`/`LH`/`DA`), the
fallback-mirrors-the-gate rule, and the `check_ignore` Dart-only limitation.

---

## Fixing

- **Fix only failing items** β€” address the diagnostics, tests, or under-covered
files surfaced this round. Do not refactor unrelated code (YAGNI).
- **Coverage fixes = author tests** β€” for each ranked under-covered `SF` file,
write tests following the `testing` skill. Prioritize files by uncovered
line count (`LF - LH`).
- **Coverage fixes = author tests** β€” for each ranked under-covered file, write
tests following the `testing` skill. Prioritize files by uncovered line count β€”
the line numbers listed per file in the result, or `LF - LH` from the lcov
fallback.
- **Bound files per round** β€” fix a coherent batch, then re-verify. Re-running
the gates after each batch is what makes "no progress" detectable and prevents
fixing one gate while silently breaking another.
Expand All @@ -257,7 +273,7 @@ Stop and surface to the user when:

When escalating, name the gate that is red β€” analyze, format, test, or coverage β€”
then give its fingerprint entries verbatim (`diagnosticCode @ file:line` for
analyze, failing test names for test, under-covered `SF` paths for coverage),
analyze, failing test names for test, under-covered file paths for coverage),
the files touched, the iteration count, and the one decision the user must make.
A summary count ("4 errors remain") is not a report; the user cannot act on it.

Expand All @@ -277,7 +293,8 @@ report, and wait.
- **Shared package-root discovery** β€” walk for `pubspec.yaml` files. The
`analyze_files` `roots` set must match the package set
`mcp__very-good-cli__test --recursive` (`recursive: true`) covers.
- **lcov path is `<package>/coverage/lcov.info`** β€” resolved per discovered root.
- **Fallback lcov path is `<package>/coverage/lcov.info`** β€” resolved per
discovered root.
- **Single `min_coverage` applies to all packages** β€” documented limitation: the
tool schema has no per-package coverage override. State the shared target when
confirming coverage.
Expand All @@ -287,7 +304,7 @@ report, and wait.
## Additional Resources

- [`references/coverage.md`](references/coverage.md) β€” green-gate's coverage-gate
detail (default target, exclude globs, lcov fields, decision tree,
`check_ignore`, stale lcov).
detail (default target, exclude globs, `show_uncovered`, the lcov fallback and
its fields, decision tree, `check_ignore`, stale lcov).
- The `testing` skill β€” test conventions, plus its coverage reference for closing
per-file gaps.
Loading
Loading