plugins/onedev is the first plugin package to declare a real dependency, and that arms a check every previous plugin left vacuous. apm treats any directory holding both apm.yml and apm.lock.yaml as an install root; a package is not one, so there is no green state for it. Without a package lockfile, lockfile-exists fails outright. With one, it passes and thereby arms the other nine checks, where drift then demands the dependency's skills be deployed inside the package and apm lock leaves an apm_modules/ tree behind. scripts/apm-audit-ci.sh replaces the inline bash -c loop and waives that single check for a non-root manifest. It fails closed on three axes: the root is never waived; the failing check must be lockfile-exists and no other, asserted by matching "1 of 1 check(s) failed"; and unrecognised output fails. Dropping --ci for package directories was the smaller change and is wrong. Verified on apm 0.28.0 against a scratch package whose dependency entry carried no git/path/registry field: apm audit --ci exits 1 naming it, while plain apm audit exits 0 and says nothing. Malformed-dependency detection is the reason gates.md gives for auditing packages at all, and a package with dependencies is the only kind that can carry a malformed dependency entry. The waiver matches on apm's stdout, so an apm upgrade rewording either line turns it off. That fails the push rather than hiding a defect. Also records the onedev entry in apm.lock.yaml, which PR #136 could not carry because the plugin was not yet resolvable from the remote's main. ADR: 0026 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
311 lines
18 KiB
YAML
311 lines
18 KiB
YAML
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/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` 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
|
|
# 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. 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
|
|
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-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 "<package>#<version>" 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-audit-ci
|
|
name: apm audit --ci
|
|
description: Run apm's producer-side CI gate over the root manifest AND each plugin package, via scripts/apm-audit-ci.sh. 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, which the script waives when that package declares dependencies, because a package is not an install root (ADR-0026). The waiver never applies to the root and never covers a second failing check. It does NOT enforce an org policy; see the comment below for why. Reference:plugins/kyberforge/.apm/skills/apm-workflow/references/audit.md
|
|
entry: scripts/apm-audit-ci.sh
|
|
language: system
|
|
stages: [pre-push]
|
|
pass_filenames: false
|
|
always_run: true
|
|
# 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".
|
|
#
|
|
# * 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 every plugin package is what
|
|
# makes lockfile-exists reachable for them at all -- the root-only
|
|
# invocation audits the root manifest and nothing else.
|
|
# THAT ARMING NOW HAPPENS: plugins/onedev declares a real dependency,
|
|
# and there is no green state for it -- without a package lockfile
|
|
# lockfile-exists fails, and with one it passes and arms the other
|
|
# nine, where drift then demands the dependency's skills be deployed
|
|
# INSIDE the package. A package is not an install root, so
|
|
# scripts/apm-audit-ci.sh waives that single check for a package and
|
|
# nothing else (ADR-0026). Dropping --ci for packages would have been
|
|
# smaller and is wrong: verified on apm 0.28.0, plain `apm audit`
|
|
# exits 0 on a dependency entry missing its git/path/registry field
|
|
# while --ci exits 1 naming it, and malformed-dependency detection is
|
|
# the whole reason packages are audited.
|
|
# * 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.
|
|
#
|
|
# 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 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]
|
|
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
|
|
|
|
# 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
|
|
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-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
|
|
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/ -- plus the required frontmatter fields folded in from the former skill-frontmatter hook, namely name, a non-empty description, and a metadata.version matching three-part semver (1.0.0)
|
|
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. The 12 README.md
|
|
# files still in scope sit in a skill's scripts/, tests/ and assets/
|
|
# subdirectories -- consumer-facing prose that no agent ever loads (the
|
|
# skill-directory READMEs this was first written for are deleted) -- and
|
|
# the `git clone` lines in the six 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 factory-audit's skill flow, via factory-audit's own bundled copy
|
|
entry: plugins/kyberforge/.apm/skills/factory-audit/scripts/vale-wrap.sh
|
|
language: script
|
|
files: '^plugins/[^/]+/\.apm/skills/[^/]+/SKILL\.md$'
|
|
pass_filenames: true
|
|
|
|
# Two hook IDs pointing at ONE vale-wrap.sh is deliberate, not leftover
|
|
# duplication. ADR-0014 measured a single hook entry silently scanning 0
|
|
# files of the other type, and ADR-0025 carried that finding across the
|
|
# merge: the two `files:` regexes are what keep the SKILL.md scope and the
|
|
# agent-file scope independently addressable. The script self-locates its
|
|
# config via ${BASH_SOURCE[0]}, so one copy serves both.
|
|
- 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 factory-audit's agent flow, via factory-audit's own bundled copy
|
|
entry: plugins/kyberforge/.apm/skills/factory-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
|