First of all, thank you for taking the time to contribute! 🎉👍 Before you do, please carefully read this guide.
- Fork the repository and clone your fork locally.
- Create a new branch from
mainfor your work. - Open the project in your editor of choice — any text editor works.
| Contribution | Where |
|---|---|
| New skill | skills/<skill-name>/SKILL.md |
| Improve an existing skill | Edit the relevant skills/*/SKILL.md or reference.md |
| Eval cases | evals/<skill-name>/<case-name>/ |
| Hooks | hooks/ directory |
| Bug reports & feature requests | GitHub Issues |
Create skills/<skill-name>/SKILL.md. The file must begin with YAML frontmatter:
---
name: <skill-name>
description: When this skill should be triggered — be specific.
allowed-tools: Read Glob Grep
argument-hint: "[file-or-directory]" # optional
---| Field | Required | Rules |
|---|---|---|
name |
Yes | Lowercase letters, numbers, and hyphens only; no leading, trailing, or consecutive hyphen; 1-64 chars; must match the skill's directory name (enforced in CI by skills_lint) |
description |
Yes | Describes when the skill should be triggered |
allowed-tools |
No | Space-separated list of tools the skill may use; a Claude Code permission hint, not a hard cap |
argument-hint |
No | Placeholder hint shown to the user |
After the frontmatter, structure the file as:
- H1 title — human-readable skill name
- Core Standards — enforced constraints, always first
- Content sections — architecture, code examples, workflows, anti-patterns
Also create the Codex sidecar skills/<skill-name>/agents/openai.yaml with the skill-picker
metadata (see Cross-harness portability → Codex sidecar):
interface:
display_name: "Skill Name"
short_description: "One short line for the picker"Create one case directory per prompt under evals/<skill-name>/ — prompts that
prove the skill actually changes what Claude produces. Nothing has to be registered;
cases are discovered from the directory tree. See Eval Cases below.
Add relevant keywords to the keywords array in .claude-plugin/plugin.json.
Add a row to the skills table in README.md. The skill name must link to the SKILL.md file:
| [**Skill Name**](skills/<skill-name>/SKILL.md) | Short description of what the skill covers |Add the new skill directory and files to the repository structure tree in AGENTS.md.
Evals ask one question: does Claude route to the skill, and does the output follow it?
claude plugin eval runs every case
twice, once with this plugin loaded and once with no plugin at all, so a grader that
passes in both arms is measuring the model rather than the skill.
claude plugin eval . --scaffoldAdding a skill means adding a evals/<skill>/<case-name>/ directory per case, each with
a prompt.md, a case.yaml, and one file per grader under graders/. There is no
central registry to update — cases are discovered from the tree — and the new case's
fixture.sh is a symlink to evals/_fixture/fixture.sh.
evals/README.md is the single source of truth — the case format, the grader reference, prerequisites, what makes a case worth having, and what these evals deliberately do not cover. Read it before writing a case, and add new eval documentation there rather than here.
- Use clear directives — no soft language ("consider", "prefer"). Say "Use X" or "Do not use Y".
- Fence all code blocks with language identifiers (e.g.,
```dart). - Provide complete, copy-pasteable snippets — not fragments.
- Reference packages by full name (e.g.,
package:mocktail, not just "mocktail"). - Show anti-patterns alongside correct patterns when helpful, so readers understand both what to do and what to avoid.
A skill earns its tokens by carrying what the model cannot infer: the opinions this team holds. Reference material that merely restates the framework displaces those opinions and slows routing.
Every section you write gets one of three verdicts.
KEEP in SKILL.md — a VGV opinion the model cannot infer:
- Choices among valid alternatives (
mocktailovermockito;dart_packageoverflutter_packagefor data-layer packages;go()overpush()by default) - VGV-invented conventions (
pumpApp,AppMotion,AppSpacing,TestTag, the private_Mockrule, the four-layer dependency direction) - Judgement calls the model would get wrong by default (the animations decision tree; holding the implicit form when a controller is requested by name)
- Harness facts (hook-enforced tool routing, MCP tool arguments, cross-harness fallbacks)
- Safety and compliance rules
DEMOTE to references/ — correct and useful, but recallable or situational:
- API glossaries and lookup tables for framework surface
- Long code samples demonstrating framework mechanics rather than a VGV pattern
- Deep-dive setup only one workflow in the skill needs
DELETE — no new information at any load time:
- A
## Quick Referencetable that restates## Core Standardsin other words - Anti-pattern rows whose "Correct Approach" column is verbatim a Core Standard
- Reference links repeated in more than one place in the same file
Target shape: SKILL.md at or under ~150 lines — frontmatter, H1, ## Core Standards,
the VGV-specific sections, ## Additional Resources. One link per reference file, in one
place. Procedural skills that drive a tool loop (green-gate, dart-flutter-sdk-upgrade)
may run longer; the steps are the content.
Two things this rubric does not apply to:
- The frontmatter
description. It is the router and loads before the body, so every trigger phrase is load-bearing — do not trim it for length (see Cross-harness portability → Description length). - Genuinely important areas. Hard constraints stay hard. Directive density is not the defect; generic reference material is.
Skills are authored for Claude Code but target the Agent Skills open
standard, so they should degrade gracefully on
non-Claude harnesses (Codex, Gemini CLI, OpenCode) without changing Claude Code behavior.
Under that standard a skill is a static instruction set: the agent matches its
description, then reads the body — there is no argument or template substitution. The rules
below keep a skill working on both.
$ARGUMENTS/${CLAUDE_SKILL_DIR}— Claude Code conveniences, not spec features, so a body that uses them must still work unsubstituted. Always pair$ARGUMENTSwith a fallback that fires when it is empty or still shows the literal text, and ask the user (or read the conversation) instead. For bundled files prefer a relative path from the skill root (scripts/x.sh).- Frontmatter — an agent silently skips a skill with malformed frontmatter. Keep the
opening
---on line 1, close the block, and include a non-emptyname:(kebab-case, matching the directory name) anddescription:. Plugin extras (argument-hint,effort,model) are not spec fields but are ignored as unknown keys — keep them top-level. TheSkills LintCI job enforces the spec on every PR (skills_lint.yaml). - Description length —
descriptioncarries the whole trigger surface. The spec caps it at 1024 characters andskills_linthard-fails an overrun. Keep it to trigger phrases and scope; leave teaching material to the body. Do not assume a sentence is redundant because the body repeats it — routing happens before the body is read. Re-run a skill's eval cases after trimming its description. - MCP references — this plugin registers
dartandvery-good-cliin.mcp.json, the primary execution path on Claude Code; do not weaken the hook-enforced routing there. Those hooks and servers may be absent on other hosts, so every skill that drives an MCP tool must name the equivalentvery_good/dart/flutterCLI fallback and never block when the server is missing ("use the MCP tool if available; otherwise Bash"). - Subagents — not part of the standard. On a host without a subagent mechanism, the
flutter-revieweragent's preloaded standards still apply — run the review inline. AskUserQuestion/allowed-tools— Claude Code conveniences. Carry an inline fallback for structured questions (drop to plain numbered text where the host has none), and treatallowed-toolsas a permission hint, not a hard cap.- Own your references — a skill's reference files live in its own
references/directory. Do not share a reference across skills by symlink or../other-skill/…link; those do not survive every install path. - Codex sidecar (
agents/openai.yaml) — every skill ships one beside itsSKILL.mdwith the picker metadata (interface.display_name,interface.short_description). TheSKILL.mdbody stays the source of truth. Add one for every new skill. - Invocation — every skill here is model-invoked; there is no
when_to_usefield (only Claude Code read it). To make a skill user-invoked, setdisable-model-invocation: truein the frontmatter andpolicy.allow_implicit_invocation: falsein itsagents/openai.yaml— both harnesses or neither.
Editing a skill or hook and pushing straight to a PR only tells you the files are valid, not that they work correctly. Load your working copy into a real Claude Code 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.5.0 (
dart pub global activate very_good_cli) for the Very Good CLI MCP server tools.
See the README Hooks and MCP Integration sections for the full prerequisite details.
From the repository root, launch Claude Code pointed at this directory:
claude --plugin-dir .--plugin-dir loads the plugin for that session only, needs no install or
marketplace, and overrides any marketplace-installed copy of the same plugin.
${CLAUDE_PLUGIN_ROOT} (used throughout hooks/hooks.json) resolves to the
directory you pass, so the hook script paths resolve correctly.
| Component | How to verify |
|---|---|
| Skills | Run /help. Skills appear namespaced as /vgv-ai-flutter-plugin:<skill> (e.g. /vgv-ai-flutter-plugin:bloc). Invoke one to confirm it triggers. |
| MCP servers | Run /mcp. Confirm dart and very-good-cli both show connected. |
| Hooks | Have Claude Edit or Write a .dart file and confirm analyze.sh and format.sh run. Launch without Very Good CLI to see the SessionStart warning fire. |
After editing a SKILL.md, a hook script, or .mcp.json, restart the
claude --plugin-dir . session to guarantee the change is picked up. Changes
to .claude-plugin/plugin.json always require a restart. Edits to the hook
.sh scripts take effect on the next matching tool call with no restart, since
each hook runs the script fresh.
To mimic the marketplace install flow without pushing anything, register a
throwaway local marketplace. Create .claude-plugin/marketplace.json in a temp
directory with an absolute path to this repo:
Then, inside a session:
/plugin marketplace add /tmp/vgv-test-marketplace
/plugin install vgv-ai-flutter-plugin
Run the same check CI runs, from the repository root:
claude plugin validate .This validates the manifest, skill frontmatter, hook JSON, MCP config, and file references. It is static, so it confirms structure but does not replace the live checks above.
| Symptom | Likely cause | Fix |
|---|---|---|
Skill missing from /help |
Invalid frontmatter, or name doesn't match the folder |
Run claude plugin validate . and fix the reported error |
| MCP server "executable not found" | dart or very_good not on PATH |
Install the SDK / activate the CLI, then verify with which |
| Hook never fires | jq not installed, or script lacks +x / a shebang |
Install jq; chmod +x the script and add #!/bin/bash |
${CLAUDE_PLUGIN_ROOT} not resolving |
Session not launched via --plugin-dir (or restart pending) |
Restart with claude --plugin-dir . from the repo root |
| Local marketplace won't install | source.path is relative |
Use an absolute path in marketplace.json |
Every pull request runs the following checks automatically:
| Check | What it does | Config |
|---|---|---|
| Markdown quality | Lints all *.md files with markdownlint-cli2 |
config/custom.markdownlint.jsonc |
| Spelling | Runs cspell on all *.md files |
config/cspell.json |
| Skills Lint | Validates every SKILL.md's frontmatter, structure, and linked paths against the Agent Skills spec, so a malformed skill fails the build instead of silently vanishing on another host |
skills_lint.yaml |
| Plugin validation | Validates and test-installs the plugin | claude plugin validate . |
| Script tests | Runs the hook scripts' own test suites | hooks/scripts/*_test.sh |
Evals do not run on a pull request. They call real models, so they run after a merge
to main instead, scoped to the skills that changed:
| Check | What it does | Config |
|---|---|---|
| Evals (post-merge) | Runs the eval cases for the changed skills, with-plugin arm only. Advisory, never blocking | .github/workflows/evals.yaml |
That means a regression is reported after the merge rather than before it, which is a deliberate trade: a single eval run is too noisy to gate on, and running the full suite on every push to a PR would cost more than it saves. Run the cases for the skill you touched locally before opening the PR — see Eval Cases.
If the spelling check flags a legitimate word, add it to config/cspell.json in the words array.
Use Conventional Commits with the format:
type(scope): description
| Type | When to use | Example |
|---|---|---|
feat |
New skill or feature | feat: add bloc skill |
fix |
Fix an error or incorrect guidance | fix: correct GoRouter redirect example |
docs |
Documentation-only change | docs: add logo to README |
chore |
Maintenance, CI, tooling | chore: update cspell config |
refactor |
Restructure without changing behavior | refactor: reorganize testing skill sections |
ci |
CI pipeline changes | ci: add manifest validation step |
PRs are squash-merged with the PR title as the commit message, and release-please builds the changelog from those titles. The PR title must follow Conventional Commits.
- Branch from
main. - Use a Conventional Commits PR title, for example
feat: add navigation skill. - Keep PRs focused — one skill per PR for new skills.
- Fill out the PR template completely.
- Ensure all CI checks pass before requesting review.
- Link any related issues in the PR description.