Skip to content
Draft
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
89 changes: 89 additions & 0 deletions .github/scripts/check-frontmatter.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Frontmatter portability guard.
#
# Covers the two silent-skip gaps validate-skill@v1 leaves open:
# 1. A UTF-8 BOM before the opening `---` passes validate-skill but makes
# Gemini CLI (and any parser anchoring /^---/) skip the file silently.
# Checked for every skills/**/SKILL.md and agents/**/*.md.
# 2. agents/**/*.md frontmatter has no validator at all (validate-skill
# hard-requires the filename SKILL.md; `claude plugin validate` exits 0
# on a broken agent block). Checked here: frontmatter first, closed,
# non-empty name + description, name matches the filename stem.
#
# Skill frontmatter content is deliberately NOT re-validated here —
# Flash-Brew-Digital/validate-skill owns that in the validate-skills CI job.
#
# Parsing is plain-text matching, not YAML-aware: keys are expected as bare
# `name: value` / `description: value` lines (the only forms used in this
# repo). Quoted values would be flagged as mismatches — keep values unquoted.

set -euo pipefail

cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"

errors=0

fail() {
echo "error: $1" >&2
errors=$((errors + 1))
}

# Strip trailing CR (CRLF files) and trailing whitespace.
trim() {
sed 's/[[:space:]]*$//'
}

# --- 1. BOM guard (skills + agents) ---------------------------------------
# find -L so symlinked SKILL.md files are checked too.
while IFS= read -r file; do
if [ "$(head -c 3 "$file" | od -An -tx1 | tr -d ' \n')" = "efbbbf" ]; then
fail "$file starts with a UTF-8 BOM — Gemini CLI silently skips it"
fi
done < <(find -L skills -name SKILL.md -type f; find -L agents -name '*.md' -type f)

# --- 2. Agent frontmatter (agents only) -----------------------------------
while IFS= read -r file; do
if [ "$(head -n 1 "$file" | trim)" != "---" ]; then
fail "$file: frontmatter must start on line 1 with ---"
continue
fi
close_line=$(awk 'NR > 1 && $0 ~ /^---[[:space:]]*$/ { print NR; exit }' "$file")
if [ -z "$close_line" ]; then
fail "$file: frontmatter block is never closed with ---"
continue
fi
block=$(sed -n "2,$((close_line - 1))p" "$file" | trim)
name=$(printf '%s\n' "$block" | sed -n 's/^name:[[:space:]]*//p' | head -n 1)
if [ -z "$name" ]; then
fail "$file: frontmatter has no non-empty name"
elif [ "$name" != "$(basename "$file" .md)" ]; then
fail "$file: name '$name' does not match filename stem"
fi
if ! printf '%s\n' "$block" | grep -q '^description:'; then
fail "$file: frontmatter has no description"
else
desc=$(printf '%s\n' "$block" | sed -n 's/^description:[[:space:]]*//p' | head -n 1)
case "$desc" in
"" | "|" | "|-" | "|+" | ">" | ">-" | ">+")
# Empty or block-scalar-only value: require at least one non-empty
# indented continuation line, else the description is effectively
# empty — the exact silent-skip case on Gemini/Codex/OpenCode.
if ! printf '%s\n' "$block" | awk '
/^description:/ { in_block = 1; next }
in_block && /^[^[:space:]]/ { in_block = 0 }
in_block && /[^[:space:]]/ { found = 1 }
END { exit !found }
'; then
fail "$file: description is empty"
fi
;;
esac
fi
done < <(find -L agents -name '*.md' -type f)

if [ "$errors" -gt 0 ]; then
echo "check-frontmatter: $errors problem(s) found" >&2
exit 1
fi

echo "check-frontmatter: all skills and agents pass"
35 changes: 20 additions & 15 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,36 +30,31 @@ jobs:
**/*.md
!CHANGELOG.md
config: 'config/cspell.json'
detect-changed-skills:
name: 🔎 Detect Changed Skills
list-skills:
name: 🔎 List Skills
runs-on: ubuntu-latest
outputs:
skills: ${{ steps.changed-skills.outputs.skills }}
skills: ${{ steps.list-skills.outputs.skills }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Get changed skills
id: changed-skills
- name: List all skills
id: list-skills
run: |
BASE=${{ github.event.pull_request.base.sha }}
HEAD=${{ github.event.pull_request.head.sha }}
SKILLS=$(git diff --name-only $BASE $HEAD | \
{ grep 'SKILL.md' || true; } | \
SKILLS=$(find -L skills -name SKILL.md -type f | \
xargs -I {} dirname {} | \
sort -u | \
jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "skills=$SKILLS" >> "$GITHUB_OUTPUT"
echo "Changed skills: $SKILLS"
echo "Skills: $SKILLS"
validate-skills:
name: 🔍 Validate Skills
needs: detect-changed-skills
if: needs.detect-changed-skills.outputs.skills != '[]'
needs: list-skills
if: needs.list-skills.outputs.skills != '[]'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
skill: ${{ fromJson(needs.detect-changed-skills.outputs.skills) }}
skill: ${{ fromJson(needs.list-skills.outputs.skills) }}
steps:
- uses: actions/checkout@v7
- name: Validate ${{ matrix.skill }}
Expand All @@ -76,6 +71,16 @@ jobs:
echo "Valid: ${{ steps.validate.outputs.valid }}"
echo "Errors: ${{ steps.validate.outputs.errors }}"
echo "Warnings: ${{ steps.validate.outputs.warnings }}"
frontmatter-guard:
name: 🛡️ Frontmatter Guard
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# Covers what validate-skills cannot: UTF-8 BOM detection (Gemini-fatal,
# passes validate-skill) and agents/**/*.md frontmatter (validate-skill
# only accepts files named SKILL.md).
- name: Check frontmatter portability
run: bash .github/scripts/check-frontmatter.sh
plugin-validate:
name: 📦 Plugin Validation
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ skills/
bloc/SKILL.md
bloc/references/
create-project/SKILL.md
create-project/references/interaction-fallbacks.md # symlink → skills/shared/references/
dart-flutter-sdk-upgrade/SKILL.md
green-gate/SKILL.md
green-gate/references/
Expand All @@ -48,6 +49,9 @@ skills/
license-compliance/SKILL.md
material-theming/SKILL.md
navigation/SKILL.md
shared/
references/
interaction-fallbacks.md # AskUserQuestion + allowed-tools fallbacks; symlinked into consuming skills
static-security/SKILL.md
static-security/references/
testing/SKILL.md
Expand Down
95 changes: 87 additions & 8 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ argument-hint: "[file-or-directory]" # optional

| Field | Required | Rules |
| ----- | -------- | ----- |
| `name` | Yes | Must match the skill's folder name exactly; lowercase letters, numbers, and hyphens only |
| `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 `validate-skill`) |
| `description` | Yes | Describes when the skill should be triggered |
| `allowed-tools` | Yes | Comma-separated list of tools the skill may use |
| `argument-hint` | No | Placeholder hint shown to the user |
Expand Down Expand Up @@ -69,6 +69,78 @@ Add the new skill directory and files to the repository structure tree in `CLAUD
- **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.

## Cross-harness portability

Skills are authored for Claude Code but target the [Agent Skills open
standard](https://agentskills.io/specification) (the `npx skills` format, supported by
many agents), so they should degrade gracefully on non-Claude harnesses such as Codex,
Gemini CLI, and OpenCode without changing Claude Code behavior. Under that standard a skill
is a **static instruction set**: the agent loads it by matching its `description`, then reads
the body — there is no argument or template substitution. `$ARGUMENTS` and
`${CLAUDE_SKILL_DIR}` are Claude Code conveniences, not spec features, so a body that uses
them must still work when they arrive unsubstituted.

**`$ARGUMENTS`** — not a spec concept; on a plain Agent Skill it is never substituted and
stays literal. Always pair it with a fallback that fires when it is empty *or still shows
the literal text* `$ARGUMENTS`:

```markdown
<feature_description>$ARGUMENTS</feature_description>

**If the feature description above is empty or still shows the literal text
`$ARGUMENTS` (the host did not substitute it), ask the user** for it (or read it
from the conversation).
```

**`${CLAUDE_SKILL_DIR}`** — no skill here uses it today (the hooks use
`${CLAUDE_PLUGIN_ROOT}`, resolved by Claude Code, not by skill bodies). If a future skill
references a bundled file, prefer the spec form — a **relative path from the skill root**
(`scripts/x.sh`) — and add a fallback for hosts that do not substitute the absolute form.

**Frontmatter** — an agent silently skips a skill whose frontmatter is malformed. Keep the
opening `---` on line 1, close the block with `---`, and include a non-empty `name:`
(kebab-case, **matching the directory name**) and `description:`. The spec also allows
`license`, `compatibility`, `metadata`, and `allowed-tools`. This plugin's Claude Code
extras (`when_to_use`, `argument-hint`, `effort`, `model`) are not spec fields, but
`npx skills` and other agents ignore unknown frontmatter keys — keep them top-level so
Claude Code reads them and nothing else breaks. (The spec's optional `skills-ref` linter is
stricter, rejecting any top-level field outside the six it allows; `npx skills` does not run
it, and nesting these extras under `metadata:` is the escape hatch if strict conformance is
ever needed.) The `Skill validation` CI job (`Flash-Brew-Digital/validate-skill@v1`) enforces
the spec (including name-matches-directory) across every skill on each pull request, and
`.github/scripts/check-frontmatter.sh` guards the gaps it leaves — a UTF-8 BOM (Gemini-fatal but
passes `validate-skill`) and `agents/**/*.md` frontmatter, which no other check covers.

**MCP references** — this plugin registers two MCP servers in `.mcp.json`: `dart` (Dart and
Flutter actions) and `very-good-cli` (scaffolding, tests, license checks). On Claude Code
they are the primary execution path, and the `check-vgv-cli.sh` / `block-cli-workarounds.sh`
hooks deliberately steer the quality gates through the MCP tools instead of the raw CLI — do
not weaken that on Claude Code. Those hooks do not run on other hosts and the MCP servers may
not be connected there, so every skill that drives an MCP tool must name the equivalent
`very_good` / `dart` / `flutter` CLI command as a fallback and never block when the server is
absent. The `dart-flutter-sdk-upgrade` and `very-good-analysis-upgrade` skills already phrase
this as "use the MCP tool if available; otherwise Bash" — match that.

**Subagents** — subagents are not part of the Agent Skills standard, and no skill in this
plugin dispatches one. The `flutter-reviewer` agent (`agents/flutter-reviewer.md`) is a
Claude Code construct; on a host without a subagent mechanism its four preloaded standards
(`bloc`, `testing`, `static-security`, `accessibility`) still apply — run the review inline
against those skills instead of dispatching the agent.

**`AskUserQuestion` and `allowed-tools`** — both are Claude Code conveniences. A skill that
asks the user a structured question or declares a narrow `allowed-tools` list carries the
cross-harness fallbacks in [`skills/shared/references/interaction-fallbacks.md`](skills/shared/references/interaction-fallbacks.md):
ask the same question as plain numbered text where `AskUserQuestion` is absent, and treat
`allowed-tools` as a permission hint rather than a hard cap.

**Shared content via symlinks** — references shared across skills live once under
`skills/shared/references/` and are symlinked into each consuming skill's `references/`
directory (for example `interaction-fallbacks.md`). `npx skills` dereferences symlinks when
it copies a cloned or local skill, so those install paths work as-is. Before relying on a
server-side snapshot install, verify with a real `npx skills add` that the shared files
arrive intact; if they do not, materialize them (dereference the symlinks into real files at
publish time, or vendor real copies).

## Testing Locally

Editing a skill or hook and pushing straight to a PR only tells you the files
Expand Down Expand Up @@ -144,15 +216,20 @@ Then, inside a session:

### Validate before you push

Run the same check CI runs, from the repository root:
Run the same checks CI runs, from the repository root:

```bash
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.
```bash
bash .github/scripts/check-frontmatter.sh
```

The first validates the manifest, skill frontmatter, hook JSON, MCP config, and file
references. The second is the frontmatter guard (UTF-8 BOM detection plus `agents/**/*.md`
frontmatter). Both are static, so they confirm structure but do not replace the live checks
above.

### Troubleshooting

Expand All @@ -170,10 +247,12 @@ Every pull request runs the following checks automatically:

| Check | What it does | Config |
| ----- | ------------ | ------ |
| Markdown lint | Lints all `*.md` files (except `CHANGELOG.md`) | `config/custom.markdownlint.jsonc` |
| Spelling | Runs cspell on all `*.md` files | `config/cspell.json` |
| File size | Ensures no file exceeds 50 KB | `scripts/check_large_files.sh` |
| Skill validation | Validates `SKILL.md` frontmatter and structure | `scripts/validate_skills.sh` |
| Plugin validation | Validates and test-installs the plugin | `claude plugin validate .` |
| Skill validation | Validates **every** `SKILL.md`'s frontmatter and structure against the Agent Skills spec, so a malformed skill fails the build instead of silently vanishing on another host | `Flash-Brew-Digital/validate-skill@v1` |
| Frontmatter guard | Fails on a UTF-8 BOM in any `SKILL.md` or agent file (Gemini-fatal, passes validate-skill) and validates `agents/**/*.md` frontmatter, which no other check covers | `.github/scripts/check-frontmatter.sh` |
| Plugin validation | Validates the plugin manifests via the Claude Code CLI | `claude plugin validate .` |
| Script tests | Runs the hook script unit tests | `hooks/scripts/*_test.sh` |

If the spelling check flags a legitimate word, add it to `config/cspell.json` in the `words` array.

Expand Down
5 changes: 3 additions & 2 deletions agents/flutter-reviewer.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ Determine the change set adaptively, from the repository root:
per affected package.

Read the changed files with `Read`/`Grep` to review their full context, not just the diff hunks.
You may use `mcp__dart__analyze_files` to corroborate a skill-based judgment, but analyzer output is
not itself a findings source (see "What not to report").
When the Dart MCP server is connected, you may use `mcp__dart__analyze_files` to corroborate a
skill-based judgment, but analyzer output is not itself a findings source (see "What not to
report"). If it is unavailable, rely on the four preloaded standards alone.

### When scoping fails

Expand Down
3 changes: 3 additions & 0 deletions config/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
"words": [
"activatable",
"adversarial",
"agentskills",
"antipattern",
"Automator",
"Bidirectionality",
"Bienvenido",
"bypassable",
"Codex",
"dartdoc",
"CSPRNG",
"Cupertino",
Expand Down Expand Up @@ -37,6 +39,7 @@
"mocktail",
"monorepo",
"Mundo",
"opencode",
"pasteable",
"prefs",
"pubspec",
Expand Down
2 changes: 2 additions & 0 deletions skills/accessibility/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ Apply these standards to all accessibility work:

Every accessibility engagement follows four phases in sequence. Do not skip Phase 1 or Phase 2.

> **Cross-harness note.** Phases 1, 2, and 4 below use `AskUserQuestion`. On a host without it, ask the same question as plain numbered text and wait for the reply before proceeding. See [interaction fallbacks](references/interaction-fallbacks.md).

### Phase 1: Conformance Level Selection

Use `AskUserQuestion` to ask:
Expand Down
1 change: 1 addition & 0 deletions skills/accessibility/references/interaction-fallbacks.md
2 changes: 2 additions & 0 deletions skills/create-project/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ model: haiku

Scaffold a new Dart or Flutter project using Very Good CLI templates.

> **Cross-harness fallbacks.** This skill drives the Very Good CLI MCP server and asks the user structured questions. On a host without that MCP server connected, run the equivalent `very_good create …` and `very_good packages get` commands directly. On a host without `AskUserQuestion`, ask the same questions as plain numbered text. See [interaction fallbacks](references/interaction-fallbacks.md).

---

## Core Standards
Expand Down
7 changes: 4 additions & 3 deletions skills/dart-flutter-sdk-upgrade/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ for the target Flutter release before editing any files.
3. Note the Dart version listed alongside it

The target version comes from `$ARGUMENTS` (e.g. `3.41.0`) when the user supplied one. If
`$ARGUMENTS` is empty, look up the latest Flutter stable release from that same page. For pure
Dart packages (no Flutter dependency), the Dart version is whatever `$ARGUMENTS` specifies or
the latest stable — no Flutter mapping needed.
`$ARGUMENTS` is empty or still shows the literal text `$ARGUMENTS` (the host did not substitute
it), look up the latest Flutter stable release from that same page. For pure Dart packages (no
Flutter dependency), the Dart version is whatever `$ARGUMENTS` specifies or the latest stable —
no Flutter mapping needed.

Confirm both resolved versions with the user before editing files.

Expand Down
6 changes: 5 additions & 1 deletion skills/green-gate/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ Apply these to ALL green-gate work:
coverage via `mcp__very-good-cli__test`. The Bash test path
(`very_good test`, `flutter test`, `dart test`) is hook-blocked by
`block-cli-workarounds.sh`; `dart analyze` via Bash is redundant with the MCP
tool. **Bash is reserved for parsing `coverage/lcov.info` only.**
tool. **Bash is reserved for parsing `coverage/lcov.info` only.** This MCP-only
rule is a Claude Code constraint enforced by that hook; it 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
`very_good test` CLI commands instead — never block on a missing MCP server.
- **Never cache green** — re-evaluate every gate every round. Fixing analyze or
test failures and writing new test files shifts both formatting and the
coverage denominator, so a previously green gate can regress.
Expand Down
2 changes: 2 additions & 0 deletions skills/layered-architecture/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ Apply these standards to ALL layered architecture work:
- **Repositories accept data layer dependencies via constructor injection** — never instantiate clients internally
- **App bootstrap wires all layers** — `main_<flavor>.dart` creates clients and repositories, provides them via `RepositoryProvider`

> **Cross-harness fallback.** This skill scaffolds and tests packages via the Very Good CLI MCP server. On a host without that MCP server connected, run the equivalent `very_good create dart_package …`, `very_good packages get`, and `very_good test` commands directly.

## Architecture Overview

| Layer | Responsibility | Location | Depends On | Example |
Expand Down
Loading
Loading