B1: check-skill-version-bump.sh resolves every merge-base with `git merge-base
--all` instead of the single base git happens to pick. A criss-cross history has
two, so the verdict turned on that choice: a skill byte-identical to main's tip
could still be reported "not above merge-base" / "not above main tip" and fail a
push that should pass. A skill now counts as changed only when it differs from
EVERY base, and its version must exceed the version at every base it exists at
as well as at the main tip; with more than one base the failure names which one.
Case 40 in tests/test-skill-version-bump.sh builds the criss-cross fixture and
pins both directions.
B2: check-apm-current.sh no longer assumes the remote default branch is `main`
when origin/HEAD is unset. A checkout whose default is `master` was standing on
its default branch and being told "this is a feature branch, so discard it" --
to throw away a real lock update. With origin/HEAD unset nothing is asserted and
the neutral advice stands. tests/test-apm-current-hook.sh covers the unset case
on both `main` and `master`.
#4: the required-frontmatter checks folded into skill-size-check.sh by c8a7c9e
were untested apart from the leading-zero shape -- mutating the missing-version
ERROR into a no-op left every suite green. tests/test-adr0020-frontmatter.sh now
pins name presence and non-emptiness, metadata.version presence and semver
shape, and the four grep defects the deleted test-skill-frontmatter.sh named.
#5: nothing asked whether a Vale rule still MATCHES anything -- rewriting
CompositionNote.yml's tokens to match nothing left test-vale-wrap.sh at 63/63.
Case 35 enumerates the rule files under the Kyberforge* style directories at run
time, requires an alert from each on its own fixture, and fails when a
discovered rule has no fixture row. The stale comment at case 31 is corrected.
#6: tests/run-tests.sh --strict exited 0 when discovery found no test-*.sh at
all; strictness only ever acted on skips, and with no suites there were none. It
now cross-checks the git index the way run-bats.sh does and fails
unconditionally on an empty set, naming the search root.
N9: the skill-size-check hook description in .pre-commit-config.yaml covered
only the size, context-budget and boundary-target gates. It now also names the
required frontmatter fields, matching docs/spec/gates.md.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NwD8Egs5r4ndqeFLmhusX2
276 lines
16 KiB
YAML
276 lines
16 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 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`) -- consistent with every other pre-push hook: none of
|
|
# them need the network (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. Only 6 of its 17
|
|
# assertions diffed skill-audit's Vale copy against agent-audit's; the
|
|
# merge into factory-audit leaves one copy, so those 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. A skill-directory
|
|
# README is consumer-facing prose that no agent ever loads, 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
|