repos: - repo: https://github.com/compilerla/conventional-pre-commit rev: v2.4.0 hooks: - id: conventional-pre-commit stages: [commit-msg] - repo: https://github.com/gitleaks/gitleaks rev: v8.21.2 hooks: - id: gitleaks stages: ['pre-commit'] - repo: https://github.com/jumanjihouse/pre-commit-hooks rev: 3.0.0 hooks: - id: shellcheck args: [--severity=warning] stages: ['pre-commit'] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: end-of-file-fixer stages: ['pre-commit'] - id: check-json stages: ['pre-commit'] - id: pretty-format-json stages: ['pre-commit'] args: [--autofix] # Every generated manifest lives at a KNOWN path, so every alternative is # 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/settings.json` is the sixteenth, and it is excluded for a # different reason: 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 # insertion order (`matcher` before `hooks`, `type` before `command`). # Formatting the file therefore rewrites apm's output into a form apm # would never produce, and the `apm-audit-ci` pre-push hook reports it # 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)$' - id: check-yaml stages: ['pre-commit'] - id: trailing-whitespace stages: ['pre-commit'] - id: check-merge-conflict stages: ['pre-commit'] - id: detect-private-key stages: ['pre-commit'] - id: check-toml stages: ['pre-commit'] - id: check-ast stages: ['pre-commit'] - repo: local hooks: - id: run-tests name: Run test suite description: Run all test-*.sh files and bats suite. --strict because a suite that exits 77 (SKIPPED) at pre-push means a documented dependency is missing on this machine, and pre-commit prints nothing for a passing hook -- without it the gate went green having verified 15 of 17 suites on a vale-less PATH, with the skip list swallowed. Ad-hoc `bash tests/run-tests.sh` still skips gracefully. entry: bash tests/run-tests.sh --strict 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) entry: bash scripts/check-executables-allow-sync.sh language: system stages: [pre-push] 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 -- the only hook that checks remote package references rather than local-source paths 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 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: # # * 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. # # 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. - 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 entry: bash scripts/check-apm-agents-valid.sh language: system stages: [pre-push] pass_filenames: false always_run: true # validate.sh was previously exercised only by check-scope-walkup-sync, # and only against synthetic mktemp fixtures -- it had never run against # the four agent files it governs. That is how ADR-0016 could be amended # to bless a `disallowedTools` frontmatter field while validate.sh's # allowlist still rejected it: the spec and its enforcer disagreed and # every gate stayed green. The expected file set is derived from # `git ls-files` (the pattern tests/run-bats.sh established) rather than # a hardcoded count, and discovering zero files is an error, not a pass. # Needs no network. - id: apm-pack-check-clean name: apm pack --check-clean description: Release gate -- verify .claude-plugin/marketplace.json still matches what apm.yml + .apm/ would currently generate, and that per-package versions agree with the per_package versioning strategy. Closes issue #90's deferred item 3 (a check-clean-equivalent gate) using apm's own flag instead of custom drift logic. entry: apm pack --check-versions --check-clean --dry-run language: system stages: [pre-push] 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. - id: check-scope-walkup-sync name: Check scope walk-up implementations agree description: Behaviorally cross-check validate.sh, validate-provenance.sh, new-agent.sh, and new-skill.sh's independent $HOME/.git/apm.yml walk-up ports against each other entry: bash scripts/check-scope-walkup-sync.sh language: system stages: [pre-push] 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' language: system stages: [pre-push] pass_filenames: false always_run: true - id: validate-marketplace name: Validate marketplace manifest description: Run claude plugin validate --strict on the root marketplace manifest entry: claude plugin validate --strict .claude-plugin/marketplace.json language: system stages: [pre-push] pass_filenames: false always_run: true - id: skill-size-check stages: ['pre-commit'] name: SKILL.md size and context-budget ceilings description: Enforce agentskills.io's 500-line/2,770-whole-file-word spec ceilings AND ADR-0020's context budget -- description 250 chars SUGGESTION / 400 FAIL, body-only 600 words SUGGESTION / 900 FAIL, and every boundary-clause routing target resolving to a real skill or agent under plugins/*/.apm/ entry: scripts/skill-size-check.sh language: script files: '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' pass_filenames: true verbose: true # verbose so the SUGGESTION tier is audible. ADR-0020 depends on it: # "A ceiling does not produce an average ... The halving depends # entirely on the 250-character SUGGESTION tier being visible and # respected." pre-commit prints nothing at all for a passing hook, and # a SUGGESTION deliberately does not fail, so without verbose every # suggestion would be swallowed -- the exact invisibility ADR-0013 # records for Vale warnings. Costs nothing on a clean file: the script # prints only findings. - id: check-rtk-prefix stages: ['pre-commit'] name: ADR-0023 rtk prefix on executable git commands description: Enforce ADR-0023 clause 1 -- an executable, instructed git command in a shell code fence or a dispatch-table Run cell is written `rtk git`. Clauses 2 and 3 are not machine-decidable; a deliberately bare command opts out with the literal string ADR-0023 on its own line entry: scripts/check-rtk-prefix.sh language: script files: '^plugins/[^/]+/\.apm/(skills/.*\.md|agents/.*\.agent\.md)$' # README.md is excluded on purpose, not by oversight. A skill-directory # README is consumer-facing prose that no agent ever loads, and the # `git clone` lines in the seven tests/README.md files are setup # instructions for a third party who has no rtk installed. Prefixing # those would be actively wrong -- see ADR-0023's consumer section. exclude: '(^|/)README\.md$' pass_filenames: true - id: vale-audit-prefilter-skill stages: ['pre-commit'] name: Vale audit prefilter (SKILL.md) description: Run Vale against SKILL.md files as a deterministic prefilter for skill-audit, via skill-audit's own bundled copy entry: plugins/kyberforge/.apm/skills/skill-audit/scripts/vale-wrap.sh language: script files: '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$' pass_filenames: true - id: vale-audit-prefilter-agent stages: ['pre-commit'] name: Vale audit prefilter (agent files) description: Run Vale against agent markdown files as a deterministic prefilter for agent-audit, via agent-audit's own bundled copy entry: plugins/kyberforge/.apm/skills/agent-audit/scripts/vale-wrap.sh language: script files: '^plugins/[^/]+/\.apm/agents/[^/]+\.agent\.md$' pass_filenames: true - repo: meta hooks: - id: check-hooks-apply - id: check-useless-excludes