The #113 sweep rested on CLAUDE.md's premise that rtk either filters or passes through unchanged, so prefixing is always safe. Measured against rtk 0.42.4, that premise is false for several of the commands the sweep prefixed, and two skills were left giving wrong answers silently. Why: - `rtk git worktree list --porcelain -z` discards both flags and renders its own format. The `locked`/`lock_reason` fields git-worktrees Step 2 must emit are absent entirely, and paths under $HOME are abbreviated to `~/`. - `rtk git branch --list <name>` prints a phantom `* ` line even when nothing matches, so git-branches' stated ambiguity test — "output from both means the name is ambiguous" — reported every name as ambiguous. `tag --list` is a clean passthrough, so only one half broke. - `rtk git diff --name-only`/`--name-status` append a `Changes:` trailer to output documented as "one per line"; `--word-diff` emits none of the `[-removed-] {+added+}` markers its table describes; `rtk git log -L` truncates each line at ~72 chars, on the one command whose purpose is showing line content. - `rtk git stash pop` prints only `FAILED: git stash pop`, swallowing the conflict diagnostic and retained-entry message the surrounding prose tells the agent to rely on. Implementation notes: - Eleven sites reverted to bare `git`, each carrying its reason inline so the next sweep does not undo it. `mergetool` and `rebase -i` are reverted on clause 3's interactive limb only: the TTY defect does not reproduce — rtk filters exactly twelve subcommands and execs the rest — and ADR-0023 records that measurement rather than a convenient one. - ADR-0023 states the rule repo-wide with a third clause: a command whose output the skill parses, or which is interactive, stays bare. `plugins/git/README.md` is reduced to a pointer; its claim that gitea skills "contain no git/rtk mentions at all" was false, and its citation of `hard-rules.md` pointed at a file containing no occurrence of "rtk". - Eight gitea sites swept, all verified byte-identical passthroughs first. - `scripts/check-rtk-prefix.sh` gates clause 1. Run against main's pre-sweep corpus it reports 99 findings including every gitea site, so it would have caught the drift #113 was filed about. Impact: the gate covers clause 1 only, in shell-tagged fences and the opening span of Run cells. Clause 2 is not gateable — "Run `git switch`" and "`git switch` refuses" are the same tokens — and prose bullets are invisible to it. Both limits are recorded in gates.md rather than left implied. Refs: #113 ADR: 0023 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EeH8SCbcrCAQrtymkNuhKP
386 lines
20 KiB
YAML
386 lines
20 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/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-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 "<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-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
|
|
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-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 <script> <arg0> <files...>` call shape rather than a copy of
|
|
# the script -- the bug below was invisible to any test that did not.
|
|
args:
|
|
- -c
|
|
- |
|
|
# Every check reads the FRONTMATTER only, never the whole file. A
|
|
# `metadata:` / `name:` / `description:` line inside a body code
|
|
# fence is documentation (skill-author quotes exactly such a block)
|
|
# and used to satisfy these greps.
|
|
for f in "$@"; do
|
|
[[ -f "$f" ]] || continue
|
|
|
|
fm="$(awk '
|
|
{ sub(/\r$/, "") }
|
|
NR == 1 { sub(/^\357\273\277/, "") }
|
|
!opened && /^[[:blank:]]*$/ { next }
|
|
!opened {
|
|
if ($0 ~ /^---[[:blank:]]*$/) { opened = 1; next }
|
|
exit
|
|
}
|
|
/^---[[:blank:]]*$/ { closed = 1; exit }
|
|
{ print }
|
|
END { if (!opened || !closed) exit 3 }
|
|
' "$f")" || {
|
|
echo "ERROR: $f has no closing YAML frontmatter block (expected --- ... --- at the top of the file)"
|
|
exit 1
|
|
}
|
|
|
|
missing=""
|
|
printf '%s\n' "$fm" | grep -q "^name:" || missing="${missing}name: "
|
|
printf '%s\n' "$fm" | grep -q "^description:" || missing="${missing}description: "
|
|
|
|
# Scoped to the `metadata:` block and stopped at the next
|
|
# top-level key, so a `version:` under a following `source:` list
|
|
# cannot stand in for it; the `^ version:` anchor is exact, so a
|
|
# deeper-nested ` version:` cannot either. No line budget, so a
|
|
# long `metadata:` block does not hide the key.
|
|
ver="$(printf '%s\n' "$fm" | awk '
|
|
/^metadata:/ { inm = 1; next }
|
|
inm && /^[A-Za-z]/ { exit }
|
|
inm && /^ version:/ {
|
|
v = $0
|
|
sub(/^ version:[[:blank:]]*/, "", v)
|
|
sub(/[[:blank:]]+#.*$/, "", v)
|
|
sub(/[[:blank:]]+$/, "", v)
|
|
print "found:" v
|
|
exit
|
|
}
|
|
')"
|
|
[[ -n "$ver" ]] || missing="${missing}metadata.version "
|
|
|
|
if [[ -n "$missing" ]]; then
|
|
echo "ERROR: $f is missing required frontmatter fields (${missing})"
|
|
exit 1
|
|
fi
|
|
|
|
raw="${ver#found:}"
|
|
v="$raw"
|
|
case "$v" in
|
|
\"*\") v="${v#\"}"; v="${v%\"}" ;;
|
|
\'*\') v="${v#\'}"; v="${v%\'}" ;;
|
|
esac
|
|
if [[ ! "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
echo "ERROR: $f has a malformed frontmatter metadata.version (${raw:-<empty>}) -- expected a three-part semver, e.g. \"1.0.0\""
|
|
exit 1
|
|
fi
|
|
done
|
|
# arg0 for `bash -c`. WITHOUT it pre-commit's first filename lands in
|
|
# $0 and is dropped from "$@" -- so a single-file commit, the normal
|
|
# case, ran the loop zero times and reported Passed having checked
|
|
# nothing. Do not remove; tests/test-skill-frontmatter.sh pins it.
|
|
- skill-frontmatter
|
|
|
|
- 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
|