diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json deleted file mode 100644 index 8100ea5..0000000 --- a/.agents/plugins/marketplace.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "name": "holocron", - "interface": { - "displayName": "holocron" - }, - "plugins": [ - { - "name": "kyberforge", - "source": { - "source": "local", - "path": "./plugins/kyberforge" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Developer Tools" - }, - { - "name": "bin", - "source": { - "source": "local", - "path": "./plugins/bin" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Utilities" - }, - { - "name": "git", - "source": { - "source": "local", - "path": "./plugins/git" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Version Control" - }, - { - "name": "gitea", - "source": { - "source": "local", - "path": "./plugins/gitea" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Version Control" - }, - { - "name": "core", - "source": { - "source": "local", - "path": "./plugins/core" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Productivity" - }, - { - "name": "mattpocock-skills", - "source": { - "source": "url", - "url": "mattpocock/skills", - "ref": "v1.2.3", - "sha": "835450ef244ab7335f75d95b83e7d979eae22a6d", - "tag_pattern": "v{version}" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Productivity" - }, - { - "name": "lint", - "source": { - "source": "local", - "path": "./plugins/lint" - }, - "policy": { - "installation": "AVAILABLE", - "authentication": "ON_INSTALL" - }, - "category": "Developer Tools" - } - ] -} diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 38f696d..c8c2d32 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,7 +1,7 @@ { "name": "holocron", - "description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.", - "version": "0.4.6", + "description": "AI development skills for Claude Code, and for GitHub Copilot through apm — factory, design, implement, review, and cross-cutting workflows.", + "version": "0.5.0", "owner": { "name": "Defame1297", "email": "defame1297@rkdr.net", @@ -10,56 +10,43 @@ "plugins": [ { "name": "kyberforge", - "description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.", - "version": "1.6.2", + "description": "Skills and agents for creating, maintaining, and managing an apm plugin marketplace for Claude Code and GitHub Copilot.", + "version": "2.0.0", "category": "Developer Tools", "source": "./plugins/kyberforge" }, { "name": "bin", "description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.", - "version": "1.1.7", + "version": "1.1.8", "category": "Utilities", "source": "./plugins/bin" }, { "name": "git", "description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.", - "version": "1.3.7", + "version": "1.3.8", "category": "Version Control", "source": "./plugins/git" }, { "name": "gitea", "description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.", - "version": "1.3.8", + "version": "1.3.9", "category": "Version Control", "source": "./plugins/gitea" }, { "name": "core", "description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.", - "version": "1.1.2", + "version": "1.1.3", "category": "Productivity", "source": "./plugins/core" }, - { - "name": "mattpocock-skills", - "description": "Skills for Real Engineers — planning, TDD, architecture, and debugging workflows from Matt Pocock's .claude directory.", - "version": "1.2.3", - "category": "Productivity", - "source": { - "source": "github", - "repo": "mattpocock/skills", - "ref": "v1.2.3", - "sha": "835450ef244ab7335f75d95b83e7d979eae22a6d", - "tag_pattern": "v{version}" - } - }, { "name": "lint", "description": "Skills and agents for configuring and running linters.", - "version": "1.1.7", + "version": "1.1.8", "category": "Developer Tools", "source": "./plugins/lint" } diff --git a/.claude/apm-hooks.json b/.claude/apm-hooks.json new file mode 100644 index 0000000..0c96646 --- /dev/null +++ b/.claude/apm-hooks.json @@ -0,0 +1,15 @@ +{ + "SessionStart": [ + { + "matcher": "startup", + "hooks": [ + { + "type": "command", + "command": "\"${CLAUDE_PROJECT_DIR}/.claude/hooks/kyberforge/.apm/hooks/check-apm-current.sh\"", + "timeout": 380 + } + ], + "_apm_source": "Defame1297/holocron/plugins/kyberforge" + } + ] +} diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json deleted file mode 100644 index 38f696d..0000000 --- a/.github/plugin/marketplace.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "holocron", - "description": "AI development skills for Claude Code and GitHub Copilot CLI — factory, design, implement, review, and cross-cutting workflows.", - "version": "0.4.6", - "owner": { - "name": "Defame1297", - "email": "defame1297@rkdr.net", - "url": "https://git.dev.rkdr.net/Defame1297/" - }, - "plugins": [ - { - "name": "kyberforge", - "description": "Skills and agents for creating, maintaining, and managing a Claude Code / Copilot CLI plugin marketplace.", - "version": "1.6.2", - "category": "Developer Tools", - "source": "./plugins/kyberforge" - }, - { - "name": "bin", - "description": "Skills for everyday AI-assisted development work that is not tied to a single tool, forge or language, and has not yet been split into a focused plugin.", - "version": "1.1.7", - "category": "Utilities", - "source": "./plugins/bin" - }, - { - "name": "git", - "description": "Skills and agents for working with a local Git clone over the git wire protocol, and for authoring and running the pre-commit hooks that guard it.", - "version": "1.3.7", - "category": "Version Control", - "source": "./plugins/git" - }, - { - "name": "gitea", - "description": "Skills and agents for working with a Gitea forge through its HTTP API — the forge's own objects, as distinct from the local git clone.", - "version": "1.3.8", - "category": "Version Control", - "source": "./plugins/gitea" - }, - { - "name": "core", - "description": "Skills for authoring and auditing a repo's AGENTS.md and the provider adapter files that defer to it.", - "version": "1.1.2", - "category": "Productivity", - "source": "./plugins/core" - }, - { - "name": "mattpocock-skills", - "description": "Skills for Real Engineers — planning, TDD, architecture, and debugging workflows from Matt Pocock's .claude directory.", - "version": "1.2.3", - "category": "Productivity", - "source": { - "source": "github", - "repo": "mattpocock/skills", - "ref": "v1.2.3", - "sha": "835450ef244ab7335f75d95b83e7d979eae22a6d", - "tag_pattern": "v{version}" - } - }, - { - "name": "lint", - "description": "Skills and agents for configuring and running linters.", - "version": "1.1.7", - "category": "Developer Tools", - "source": "./plugins/lint" - } - ] -} diff --git a/.gitignore b/.gitignore index 0a023db..64a54f4 100644 --- a/.gitignore +++ b/.gitignore @@ -34,12 +34,17 @@ apm_modules/ .claude/skills/ .claude/agents/ +# APM MCP deployment output — `apm install` writes the repo-root .mcp.json from +# the MCP servers its dependencies declare, and regenerates it on every install. +# It is apm's output, not repo content; nothing here is hand-authored. +/.mcp.json + # APM hook deployment output — `apm install` copies each package's referenced -# hook scripts here and tracks its own settings.json entries in the sidecar. -# Regenerated on every install; the authoring source is -# plugins//.apm/hooks/ (ADR-0019). +# hook scripts here. Regenerated on every install; the authoring source is +# plugins//.apm/hooks/ (ADR-0019). The .claude/apm-hooks.json ownership +# sidecar is committed, not ignored: without it a fresh clone's install cannot +# claim the committed settings.json entry and duplicates it (ADR-0019). .claude/hooks/ -.claude/apm-hooks.json # `apm pack` bundle output. The pre-push gate runs pack with --dry-run, so this # only appears after a bare `apm pack` during a release; it is not repo content. @@ -47,8 +52,8 @@ build/ # `apm pack`'s manifest for the *root* package. Emitted beside the marketplace # manifest by a bare `apm pack`, and never tracked on any branch — the repo's -# own paths hide it, since sync-plugin-content.sh redirects `apm pack -o` to a -# scratch tree and the apm-pack-check-clean pre-push hook runs --dry-run. Scoped +# own paths hide it, since the apm-pack-check-clean pre-push hook, the only +# thing that runs pack here, passes --dry-run. Scoped # to the file, not the directory: the sibling .claude-plugin/marketplace.json is # compiled output that IS committed and must stay tracked. /.claude-plugin/plugin.json diff --git a/.mcp.json b/.mcp.json deleted file mode 100644 index 7aa9712..0000000 --- a/.mcp.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "mcpServers": { - "obsidian": { - "args": [ - "@bitbonsai/mcpvault@0.15.0", - "docs/" - ], - "command": "npx", - "type": "stdio" - } - } -} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71b9b2a..7a912bf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,14 +32,22 @@ repos: # root-anchored and spells that path out. This was five `(^|/)` # any-depth alternatives plus one `^` root-only one -- a mixture with no # rationale, under which a fixture or vendored tree containing - # `.../.claude-plugin/plugin.json` would have been silently excluded from - # formatting while an equivalent `.../.agents/plugins/marketplace.json` - # would not. All fifteen real files (3 root marketplace manifests, 2 per - # plugin x 6 plugins) match; anything else is hand-authored and gets - # formatted. + # `.../.claude-plugin/marketplace.json` would have been silently excluded + # from formatting while an equivalent + # `.../.agents/plugins/marketplace.json` would not. Only the one root + # marketplace manifest matches now; anything else is hand-authored and + # gets formatted. The twelve per-plugin `plugin.json` alternatives were + # dropped with the plugin manifests themselves when native + # `claude plugin install` support was removed (ADR-0024) -- apm probes + # `apm.yml` and never reached them. The `.agents/plugins/` and + # `.github/plugin/` marketplace mirrors went the same way, and their + # alternations went with them: `check-useless-excludes` fails on a + # pattern that matches no file. # - # `.claude/settings.json` is the sixteenth, and it is excluded for a - # different reason: apm OWNS that file (ADR-0018, ADR-0019), and + # `.claude/settings.json` and its `.claude/apm-hooks.json` ownership + # sidecar are the last two alternations, and they are the only ones + # here for a reason other than "generated manifest": + # apm OWNS that file (ADR-0018, ADR-0019), and # `apm audit --ci` replays the install into a scratch tree and diffs # the result byte-for-byte. `pretty-format-json` sorts object keys # unless `--no-sort-keys` is passed, while apm's hook integrator emits @@ -49,8 +57,11 @@ repos: # as permanent drift on a file with no git diff -- exactly what # happened when the SessionStart hook first landed in 2e395a4. # Re-running `apm install` fixes the file; leaving it in scope here - # would re-break it on the very commit that carries the fix. - exclude: '^(\.claude-plugin/marketplace\.json|\.agents/plugins/marketplace\.json|\.github/plugin/marketplace\.json|plugins/[^/]+/\.claude-plugin/plugin\.json|plugins/[^/]+/\.github/plugin/plugin\.json|\.claude/settings\.json)$' + # would re-break it on the very commit that carries the fix. The + # sidecar is committed so a fresh clone's install can claim the + # settings entry instead of duplicating it (ADR-0019, 2026-09-16 + # correction), and it is apm output under the same byte-for-byte replay. + exclude: '^(\.claude-plugin/marketplace\.json|\.claude/(settings|apm-hooks)\.json)$' - id: check-yaml stages: ['pre-commit'] - id: trailing-whitespace @@ -75,33 +86,6 @@ repos: pass_filenames: false always_run: true - - id: check-manifests - name: Check plugin manifests - description: Validate marketplace.json and plugin.json paths - entry: bash scripts/check-manifests.sh - language: system - stages: [pre-push] - pass_filenames: false - always_run: true - - - id: check-plugin-content-sync - name: Check plugin content sync - description: Verify each plugin's flat skills/agents/commands/hooks/hooks.json mirror is in sync with .apm/ -- Claude Code has no .apm/ awareness so this compiled mirror must stay current (see issue #90) - entry: bash scripts/sync-plugin-content.sh --check --all - language: system - stages: [pre-push] - pass_filenames: false - always_run: true - - - id: check-marketplace-mirror-sync - name: Check marketplace mirror sync - description: Verify .github/plugin/marketplace.json (Copilot CLI's legacy manifest path) is byte-identical to .claude-plugin/marketplace.json -- apm has no output profile for this path, so it must be kept in sync explicitly (see issue #90) - entry: bash scripts/sync-marketplace-mirror.sh --check - language: system - stages: [pre-push] - pass_filenames: false - always_run: true - - id: check-executables-allow-sync name: Check executables allow key sync description: Verify root apm.yml's executables.allow key names kyberforge's actual version -- apm matches that key by exact "#" lookup, so a version bump on one side alone silently stops deploying kyberforge's hooks/ and bin/ and lets the apm install go stale (see ADR-0019) @@ -111,63 +95,73 @@ repos: pass_filenames: false always_run: true - - id: apm-marketplace-check - name: apm marketplace check - description: Validate every marketplace.packages[] entry resolves, including network reachability of remote refs -- catches stale/unreachable remote package references that check-manifests.sh deliberately skips (local-source checks only) - entry: apm marketplace check - language: system - stages: [pre-push] - pass_filenames: false - always_run: true - - id: apm-audit-ci name: apm audit --ci - description: Run apm's producer-side CI gate over the root manifest AND each of the six plugin packages. Verifies exactly two things per manifest -- apm.yml parses as a valid APM manifest (manifest-parse), and, if it declares dependencies, apm.lock.yaml exists and is consistent (lockfile-exists). It does NOT enforce an org policy and does NOT scan for hidden Unicode; see the comment below for why. Reference:plugins/kyberforge/.apm/skills/apm-workflow/references/audit.md + description: Run apm's producer-side CI gate over the root manifest AND each of the six plugin packages. On the root manifest it runs ten checks -- lockfile-exists, ref-consistency, deployment-ledger-owners, deployed-files-present, no-orphaned-packages, skill-subset-consistency, config-consistency, content-integrity, includes-consent, drift -- so it is both a hidden-Unicode scan and a drift gate that replays the install and diffs it. In a plugin package it runs one, lockfile-exists. It does NOT enforce an org policy; see the comment below for why. Reference:plugins/kyberforge/.apm/skills/apm-workflow/references/audit.md entry: bash -c 'for d in . plugins/*/; do (cd "$d" && apm audit --ci) || { echo "apm audit --ci failed in $d" >&2; exit 1; }; done' language: system stages: [pre-push] pass_filenames: false always_run: true - # The description above deliberately claims less than this hook's old one - # did ("lockfile/policy/hidden-content integrity"), because two of those - # three were never happening: + # What this hook actually runs, read off apm 0.28.0's own compliance + # table by invoking `apm audit --ci` at the repo root and in + # plugins/lint/. Long form in docs/spec/gates.md, "apm-audit-ci". # - # * POLICY. `apm audit --ci` discovers an org policy from the git remote, - # and apm's discovery only understands github.com and Azure DevOps. - # This repo's remote is a self-hosted Gitea, so discovery resolves - # nothing and the run prints `No org policy found at unknown; - # enforcement skipped`. apm's own message suggests - # `policy.fetch_failure_default=block` in apm.yml "to fail closed" -- - # that was tried on a scratch copy and REJECTED: it does not make the - # check meaningful, it makes it permanently red. `apm audit --ci` then - # exits 1 with `No org policy found at unknown - # (policy.fetch_failure_default=block)` on every push, because there is - # no org policy to find and no supported way for this remote to serve - # one. A gate that can never go green is not a gate. Revisit if this - # repo ever gains a policy source apm can actually reach. - # * HIDDEN CONTENT. The hidden-Unicode scan is plain `apm audit`, not - # `apm audit --ci` (the two are different modes, and --ci refuses to - # combine with --file/--strip/--dry-run/PACKAGE). Plain `apm audit` - # here reports `No apm.lock.yaml found -- nothing to scan` and exits 0, - # so adding it would buy a second vacuous check, not coverage. + # * ROOT MANIFEST -- ten checks: lockfile-exists, ref-consistency, + # deployment-ledger-owners, deployed-files-present, + # no-orphaned-packages, skill-subset-consistency, config-consistency, + # content-integrity, includes-consent, drift. It is a drift gate: it + # replays the install cache-only and diffs the scratch result against + # the working tree. Root lockfile-exists is not vacuous -- the root + # declares dependencies, so it reports `Lockfile present`. + # * PLUGIN MANIFESTS -- one check: lockfile-exists. Conditional, and + # vacuous while every plugin apm.yml declares + # `dependencies: {apm: [], mcp: []}`: it reports `No dependencies + # declared -- lockfile not required` and arms itself the moment one + # does not (verified by adding a git dependency to + # plugins/lint/apm.yml). Everything else above is root-only, because + # only the root install has a lockfile, a deployment ledger and + # deployed files to check. Running the six plugin packages is what + # makes lockfile-exists reachable for them at all -- the root-only + # invocation audits the root manifest and nothing else. + # * HIDDEN CONTENT IS COVERED. content-integrity is that scan; it + # reports `No critical hidden Unicode or hash drift detected`. An + # earlier revision of this comment said the hook does NOT scan for + # hidden Unicode and that adding the scan would buy a second vacuous + # check. Both claims were wrong. What is true is that the STANDALONE + # mode differs: plain `apm audit` (--ci refuses to combine with + # --file/--strip/--dry-run/PACKAGE) run in a plugin directory reports + # `No apm.lock.yaml found -- nothing to scan` and exits 0, because + # only the root has a lockfile. + # * MANIFEST-PARSE IS NOT A CHECK in apm 0.28.0's table, and an earlier + # revision of this comment named it as one. Parsing is still + # enforced -- a dependency entry missing its git/path/registry field + # fails with `Cannot parse apm.yml` -- but it fails the invocation + # before the table is built, so it never appears as a row. + # * POLICY IS NOT ENFORCED. `apm audit --ci` discovers an org policy + # from the git remote, and apm's discovery only understands + # github.com and Azure DevOps. This repo's remote is a self-hosted + # Gitea, so discovery resolves nothing and the run prints `No org + # policy found at unknown; enforcement skipped`. apm's own message + # suggests `policy.fetch_failure_default=block` in apm.yml "to fail + # closed" -- that was tried on a scratch copy and REJECTED: it does + # not make the check meaningful, it makes it permanently red. `apm + # audit --ci` then exits 1 with `No org policy found at unknown + # (policy.fetch_failure_default=block)` on every push, because there + # is no org policy to find and no supported way for this remote to + # serve one. A gate that can never go green is not a gate. Revisit if + # this repo ever gains a policy source apm can actually reach. # - # What IS left is worth keeping, and is now run against seven manifests - # instead of one. lockfile-exists is conditional -- it is vacuous while - # every apm.yml declares `dependencies: {apm: [], mcp: []}`, and it arms - # itself the moment one does not (verified: adding a git dependency to - # plugins/lint/apm.yml fails with `apm.yml declares dependencies but - # apm.lock.yaml is absent`). manifest-parse is unconditional and fires on - # any malformed manifest (verified: a dependency entry missing its - # git/path/registry field fails with `Cannot parse apm.yml`). Running the - # six plugin packages is what makes either reachable for them at all -- - # the root-only invocation audits the marketplace manifest and nothing - # else. Costs ~0.5s per package, needs no network (checked under - # `unshare -rn`), so this does NOT join apm-marketplace-check and - # apm-pack-check-clean on the offline SKIP= list. + # Costs ~0.5s per package. Needs no network ONCE `apm install` has + # populated apm_modules/ -- the root marketplace has no remote package + # entries, so the install replay is cache-only. On a FRESH CLONE there + # is no cache: deployed-files-present fails outright, and drift and + # config-consistency clone from the holocron remote. See README.md's + # "Offline?" section. - id: check-apm-agents-valid name: Validate real APM agent files - description: Run agent-audit's validate.sh over every plugins/*/.apm/agents/*.agent.md file in this repo -- the artifacts it governs, not fixtures + description: Run factory-audit's validate.sh over every plugins/*/.apm/agents/*.agent.md file in this repo -- the artifacts it governs, not fixtures entry: bash scripts/check-apm-agents-valid.sh language: system stages: [pre-push] @@ -192,24 +186,19 @@ repos: pass_filenames: false always_run: true - - id: check-vale-style-sync - name: Check Vale style copies are in sync - description: Diff skill-audit's Vale copy against agent-audit's canonical copy - entry: bash scripts/check-vale-style-sync.sh - language: system - stages: [pre-push] - pass_filenames: false - always_run: true - # verbose so the DOWNGRADED run is audible. This hook can pass while - # having verified strictly less than its name claims: - # CHECK_VALE_STYLE_SYNC_ALLOW_MISSING_VALE=1 skips all six glob probes - # and says so on a `passed (text-level only, vale unavailable)` line. - # pre-commit prints nothing at all for a passing hook, so without this - # the opt-out reinstated exactly the silent vacuous pass the script was - # written to kill, one level up -- the run showed a bare `Passed` and - # the documented instruction to read that summary line was impossible to - # follow in the one situation the opt-out exists for. The script's clean - # output is a single line, so this costs one line per push. + # check-vale-style-sync was removed by ADR-0025. Of its 17 assertion + # sites only 2 actually diffed skill-audit's Vale copy against + # agent-audit's, and 4 more existed solely so the script could locate the + # two copies -- a real REPO_ROOT, non-stale .apm/ paths, both copies + # present (ADR-0025:285-287). The merge into factory-audit leaves one + # copy, so all 6 are moot. The other + # 11 moved into tests/test-vale-wrap.sh (case 0, cases 28-31, its + # Vale-absent skip, and case 32 for the one-plugin narrowing guard), + # which run-tests runs here at + # pre-push, so do not re-add the hook to restore coverage. Do not + # confuse its removal with check-scope-walkup-sync below, which survives: + # that one cross-checks four hand-ported walk-up implementations, only two + # of which lived in the audit pair. - id: check-scope-walkup-sync name: Check scope walk-up implementations agree @@ -220,23 +209,21 @@ repos: pass_filenames: false always_run: true - - id: check-release-needed - name: Check a release tag covers .pre-commit-hooks.yaml's paths - description: On push to main only, fail if files exposed via .pre-commit-hooks.yaml changed since the last tag - entry: bash scripts/check-release-needed.sh - language: system - stages: [pre-push] - pass_filenames: false - always_run: true - - - id: validate-plugins - name: Validate plugins - description: Run claude plugin validate --strict on every plugin directory - entry: bash -c 'for d in plugins/*/; do claude plugin validate --strict "$d" || exit 1; done' + - id: check-skill-version-bump + name: Check changed skills bump metadata.version + description: On every push, fail if a skill directory changed (tests/ excluded) since the merge-base with main without its SKILL.md metadata.version rising above both that merge-base's and main's tip's (ADR-0022) + entry: bash scripts/check-skill-version-bump.sh language: system stages: [pre-push] pass_filenames: false always_run: true + # "Changed" is measured from the merge-base with origin/main (falling + # back to main), not the remote branch tip: readers install from main. + # The version must also beat main's tip, so two branches making the same + # bump cannot both land. Fails closed when no main ref resolves, when + # there is no merge-base, or when only local main resolves and already + # contains the pushed commit. Merges through Gitea's merge button run no + # local hook, so they bypass this. See docs/spec/gates.md. - id: validate-marketplace name: Validate marketplace manifest @@ -247,91 +234,10 @@ repos: pass_filenames: false always_run: true - - id: skill-frontmatter - stages: ['pre-commit'] - name: SKILL.md frontmatter validation - description: Ensure SKILL.md files have required frontmatter fields - entry: bash - language: system - files: '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' - # Pinned by tests/test-skill-frontmatter.sh, which drives this exact - # `bash -c