Compare commits
9 Commits
9a3f72b696
...
v1.0.0
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f523da270 | |||
| cf5de2bd87 | |||
| 76e0df6f5b | |||
| 389a4f0f7a | |||
| e62f68a1cc | |||
| 680aa4f43c | |||
| 6910f1b5a5 | |||
| 050aec4c80 | |||
| 0a41b2c7d3 |
@@ -15,5 +15,5 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"name": "gitea",
|
"name": "gitea",
|
||||||
"version": "1.3.2"
|
"version": "1.3.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,5 +20,5 @@
|
|||||||
"skills": [
|
"skills": [
|
||||||
"skills/"
|
"skills/"
|
||||||
],
|
],
|
||||||
"version": "1.3.2"
|
"version": "1.3.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Audits a Claude Code and Copilot agent definition file pair for correctness and
|
|||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
Accepts either file in a CC `.md` / Copilot `.agent.md` pair, derives the counterpart automatically, and validates both. Runs structural checks via `validate.sh` (required fields, kebab-case name, no placeholders, no CC-only fields in the Copilot file, silently-ignored fields at plugin scope), provenance chain validation via `validate-provenance.sh` (checks `source_keys` against `sources.md` at the plugin root), then qualitative checks on description phrasing and system prompt quality. Produces a compact findings report in the same format as `skill-audit`.
|
Accepts either file in a CC `.md` / Copilot `.agent.md` pair, derives the counterpart automatically, and validates both. Runs structural checks via `validate.sh` (required fields, kebab-case name, no placeholders, no CC-only fields in the Copilot file, silently-ignored fields at plugin scope), provenance chain validation via `validate-provenance.sh` (checks `source_keys` against `sources.md` at the plugin root), then qualitative checks on description phrasing and system prompt quality. Step 1 also runs a Vale-based prose sub-check via `vale-wrap.sh` against both files of the pair, using the `Kyberforge` style (both files) and `KyberforgeCopilot` style (Copilot file only) — every alert is a `FAIL`, cited by rule ID — falling back to Step 2 judgment when the `vale` binary is unavailable or reports `0 files` scanned. Produces a compact findings report in the same format as `skill-audit`.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -19,6 +19,12 @@ Pass the path to either agent file as the argument.
|
|||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `SKILL.md` | Skill instructions for agents |
|
| `SKILL.md` | Skill instructions for agents |
|
||||||
|
| `assets/vale/.vale.ini` | Vale config: scopes `Kyberforge` to `**/agents/*.md`, `Kyberforge`+`KyberforgeCopilot` to `**/*.agent.md` |
|
||||||
|
| `assets/vale/styles/Kyberforge/DescriptionOpener.yml` | Flags descriptions opening with "This skill/agent" instead of an imperative "Use when..." |
|
||||||
|
| `assets/vale/styles/Kyberforge/PaddingPhrase.yml` | Flags generic "see references/ for info" pointers instead of specific file references |
|
||||||
|
| `assets/vale/styles/Kyberforge/SentenceOpenerThereIs.yml` | Flags sentences opening with "There is/are" instead of naming the subject directly |
|
||||||
|
| `assets/vale/styles/Kyberforge/VagueWording.yml` | Flags vague capability wording ("helps with", "utilize", "assists with", "used for") in descriptions |
|
||||||
|
| `assets/vale/styles/KyberforgeCopilot/ProactivePhrase.yml` | Flags CC-specific "Use proactively" phrasing with no effect in Copilot descriptions |
|
||||||
| `references/README.md` | Directory documentation for references/ |
|
| `references/README.md` | Directory documentation for references/ |
|
||||||
| `references/description-quality.md` | Qualitative guide for borderline description findings |
|
| `references/description-quality.md` | Qualitative guide for borderline description findings |
|
||||||
| `references/field-inventory.md` | Authoritative list of valid CC and Copilot agent fields |
|
| `references/field-inventory.md` | Authoritative list of valid CC and Copilot agent fields |
|
||||||
@@ -26,6 +32,7 @@ Pass the path to either agent file as the argument.
|
|||||||
| `scripts/README.md` | Directory documentation for scripts/ |
|
| `scripts/README.md` | Directory documentation for scripts/ |
|
||||||
| `scripts/validate.sh` | Structural validation script for agent file pairs |
|
| `scripts/validate.sh` | Structural validation script for agent file pairs |
|
||||||
| `scripts/validate-provenance.sh` | Provenance chain validation script for agent pairs against `sources.md` (plugin root) |
|
| `scripts/validate-provenance.sh` | Provenance chain validation script for agent pairs against `sources.md` (plugin root) |
|
||||||
|
| `scripts/vale-wrap.sh` | Drop-in `vale` wrapper that works around a frontmatter-description NLP scope limitation |
|
||||||
| `tests/README.md` | Bats test dependency and run instructions |
|
| `tests/README.md` | Bats test dependency and run instructions |
|
||||||
| `tests/validate.bats` | Bats tests for validate.sh |
|
| `tests/validate.bats` | Bats tests for validate.sh |
|
||||||
| `tests/validate-provenance.bats` | Bats tests for validate-provenance.sh |
|
| `tests/validate-provenance.bats` | Bats tests for validate-provenance.sh |
|
||||||
|
|||||||
@@ -77,6 +77,17 @@ is_builtin_output() {
|
|||||||
*) false ;;
|
*) false ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
# Absolutizes a `--config` value against the caller's cwd. Shared by both
|
||||||
|
# argument forms below — separated (`--config X`) and joined (`--config=X`)
|
||||||
|
# — so the "already absolute vs. needs $cwd prefixed" check lives in exactly
|
||||||
|
# one place instead of being duplicated per form.
|
||||||
|
abs_config_value() {
|
||||||
|
if [[ "$1" == /* ]]; then
|
||||||
|
printf '%s' "$1"
|
||||||
|
else
|
||||||
|
printf '%s' "$cwd/$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
if [[ -n "$pending_flag" ]]; then
|
if [[ -n "$pending_flag" ]]; then
|
||||||
# Value of a separated two-argv flag. It is never a lint target, however
|
# Value of a separated two-argv flag. It is never a lint target, however
|
||||||
@@ -85,11 +96,7 @@ for arg in "$@"; do
|
|||||||
case "$pending_flag" in
|
case "$pending_flag" in
|
||||||
--config)
|
--config)
|
||||||
# Always a path, and required to exist.
|
# Always a path, and required to exist.
|
||||||
if [[ "$arg" == /* ]]; then
|
vale_args+=("$(abs_config_value "$arg")")
|
||||||
vale_args+=("$arg")
|
|
||||||
else
|
|
||||||
vale_args+=("$cwd/$arg")
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
--output|--path)
|
--output|--path)
|
||||||
# See `is_builtin_output` above for why the built-in `--output` names
|
# See `is_builtin_output` above for why the built-in `--output` names
|
||||||
@@ -117,13 +124,8 @@ for arg in "$@"; do
|
|||||||
config_given=true
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
--config=/*)
|
|
||||||
vale_args+=("$arg")
|
|
||||||
config_given=true
|
|
||||||
continue
|
|
||||||
;;
|
|
||||||
--config=*)
|
--config=*)
|
||||||
vale_args+=("--config=$cwd/${arg#--config=}")
|
vale_args+=("--config=$(abs_config_value "${arg#--config=}")")
|
||||||
config_given=true
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
@@ -204,11 +206,33 @@ abspath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flatten() {
|
flatten() {
|
||||||
python3 - "$1" "$2" <<'PYTHON'
|
# Two call shapes: `flatten src dest` (dest already resolved and inside the
|
||||||
|
# scratch tree — the per-markdown-file calls in the directory branch below)
|
||||||
|
# writes straight to `dest`. `flatten src raw_dest tmpdir` (the single-file
|
||||||
|
# branch further down) additionally resolves `raw_dest` the way a separate
|
||||||
|
# `abspath` call used to, applies the same sandbox-escape guard, and prints
|
||||||
|
# the resolved path — folding two python3 spawns per file into one.
|
||||||
|
python3 - "$@" <<'PYTHON'
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
src, dest = sys.argv[1], sys.argv[2]
|
src, dest_input = sys.argv[1], sys.argv[2]
|
||||||
|
tmpdir = sys.argv[3] if len(sys.argv) > 3 else None
|
||||||
|
|
||||||
|
if tmpdir is None:
|
||||||
|
dest = dest_input
|
||||||
|
else:
|
||||||
|
dest = os.path.abspath(dest_input)
|
||||||
|
if not dest.startswith(tmpdir + os.sep):
|
||||||
|
print(
|
||||||
|
f"vale-wrap.sh: refusing to lint '{src}': its scratch copy would "
|
||||||
|
f"land outside {tmpdir}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(2)
|
||||||
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
|
|
||||||
# surrogateescape keeps a non-UTF-8 file (reachable via a directory argument)
|
# surrogateescape keeps a non-UTF-8 file (reachable via a directory argument)
|
||||||
# a byte-for-byte round trip instead of aborting the whole run on a decode error.
|
# a byte-for-byte round trip instead of aborting the whole run on a decode error.
|
||||||
with open(src, encoding='utf-8', errors='surrogateescape') as fh:
|
with open(src, encoding='utf-8', errors='surrogateescape') as fh:
|
||||||
@@ -435,6 +459,9 @@ if header_m:
|
|||||||
|
|
||||||
with open(dest, 'w', encoding='utf-8', errors='surrogateescape') as fh:
|
with open(dest, 'w', encoding='utf-8', errors='surrogateescape') as fh:
|
||||||
fh.write(content)
|
fh.write(content)
|
||||||
|
|
||||||
|
if tmpdir is not None:
|
||||||
|
print(dest)
|
||||||
PYTHON
|
PYTHON
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,23 +476,23 @@ mkdir -p "$mirror"
|
|||||||
argv_paths=()
|
argv_paths=()
|
||||||
for arg in ${path_args[@]+"${path_args[@]}"}; do
|
for arg in ${path_args[@]+"${path_args[@]}"}; do
|
||||||
if [[ "$arg" == /* ]]; then
|
if [[ "$arg" == /* ]]; then
|
||||||
dest="$tmpdir$arg"
|
raw_dest="$tmpdir$arg"
|
||||||
else
|
else
|
||||||
dest="$mirror/$arg"
|
raw_dest="$mirror/$arg"
|
||||||
fi
|
fi
|
||||||
dest="$(abspath "$dest")"
|
|
||||||
# A path argument with enough leading `..` to climb past the mirror root would
|
|
||||||
# write outside the scratch dir. The real filesystem clamps such a path at
|
|
||||||
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
|
|
||||||
case "$dest" in
|
|
||||||
"$tmpdir"/*) ;;
|
|
||||||
*)
|
|
||||||
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
mkdir -p "$(dirname "$dest")"
|
|
||||||
if [[ -d "$arg" ]]; then
|
if [[ -d "$arg" ]]; then
|
||||||
|
dest="$(abspath "$raw_dest")"
|
||||||
|
# A path argument with enough leading `..` to climb past the mirror root would
|
||||||
|
# write outside the scratch dir. The real filesystem clamps such a path at
|
||||||
|
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
|
||||||
|
case "$dest" in
|
||||||
|
"$tmpdir"/*) ;;
|
||||||
|
*)
|
||||||
|
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
mkdir -p "$(dirname "$dest")"
|
||||||
# A directory is mirrored whole — vale applies its own format filtering to
|
# A directory is mirrored whole — vale applies its own format filtering to
|
||||||
# the tree, so any file dropped here would be silently unlinted — and then
|
# the tree, so any file dropped here would be silently unlinted — and then
|
||||||
# every markdown file in the copy is flattened in place. `.git` is pruned:
|
# every markdown file in the copy is flattened in place. `.git` is pruned:
|
||||||
@@ -484,7 +511,9 @@ for arg in ${path_args[@]+"${path_args[@]}"}; do
|
|||||||
flatten "$md" "$md"
|
flatten "$md" "$md"
|
||||||
done < <(find "$dest" -type f -name '*.md' -print0)
|
done < <(find "$dest" -type f -name '*.md' -print0)
|
||||||
else
|
else
|
||||||
flatten "$arg" "$dest"
|
# `abspath` + `flatten` folded into one python3 process — see the comment
|
||||||
|
# atop `flatten` above.
|
||||||
|
dest="$(flatten "$arg" "$raw_dest" "$tmpdir")"
|
||||||
fi
|
fi
|
||||||
if [[ "$arg" == /* ]]; then
|
if [[ "$arg" == /* ]]; then
|
||||||
argv_paths+=("$dest")
|
argv_paths+=("$dest")
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Audit a skill directory against the agentskills.io specification. Runs structura
|
|||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
1. Runs `scripts/validate.sh` and `scripts/validate-provenance.sh` for structural and provenance checks
|
1. Runs `scripts/validate.sh` and `scripts/validate-provenance.sh` for structural and provenance checks, plus `scripts/vale-wrap.sh` — a Vale prefilter that deterministically flags known-bad description openers, vague wording, padding phrases, and "There is/are" sentence openers
|
||||||
2. Reads all files in the skill directory
|
2. Reads all files in the skill directory
|
||||||
3. Applies qualitative checks across seven dimensions
|
3. Applies qualitative checks across seven dimensions
|
||||||
4. Outputs a compact findings report — findings only, grouped by dimension, each with Why and Fix — and a result block with handoff to /skill-improve
|
4. Outputs a compact findings report — findings only, grouped by dimension, each with Why and Fix — and a result block with handoff to /skill-improve
|
||||||
@@ -24,6 +24,12 @@ Provide the path to the skill directory to audit when invoking.
|
|||||||
| `SKILL.md` | Skill instructions for agents |
|
| `SKILL.md` | Skill instructions for agents |
|
||||||
| `scripts/validate.sh` | Structural validator — checks name format, name matches directory, description length, line count, placeholder detection, script executable bit, and interactive-prompt detection |
|
| `scripts/validate.sh` | Structural validator — checks name format, name matches directory, description length, line count, placeholder detection, script executable bit, and interactive-prompt detection |
|
||||||
| `scripts/validate-provenance.sh` | Provenance validator — checks sources.md completeness, source_keys/slug consistency, Contributing files existence, bidirectional linkage, Research doc: fields, and upstream research doc alignment |
|
| `scripts/validate-provenance.sh` | Provenance validator — checks sources.md completeness, source_keys/slug consistency, Contributing files existence, bidirectional linkage, Research doc: fields, and upstream research doc alignment |
|
||||||
|
| `scripts/vale-wrap.sh` | Vale prefilter wrapper — runs the bundled `Kyberforge` Vale styles against SKILL.md and reports alerts as deterministic FAILs ahead of Step 3's qualitative review |
|
||||||
|
| `assets/vale/.vale.ini` | Vale configuration — points Vale at the bundled `Kyberforge` style path, self-located relative to `vale-wrap.sh` |
|
||||||
|
| `assets/vale/styles/Kyberforge/DescriptionOpener.yml` | Vale rule — flags literal "This skill..."/"This agent..." description openers |
|
||||||
|
| `assets/vale/styles/Kyberforge/PaddingPhrase.yml` | Vale rule — flags generic "see references/" padding phrasing in conditional references |
|
||||||
|
| `assets/vale/styles/Kyberforge/SentenceOpenerThereIs.yml` | Vale rule — flags body sentences starting with "There is"/"There are" |
|
||||||
|
| `assets/vale/styles/Kyberforge/VagueWording.yml` | Vale rule — flags known filler wording (e.g. "helps with", "utilize") |
|
||||||
| `references/description-quality.md` | Spec-grounded rubric for description auditing — loaded when a finding is borderline |
|
| `references/description-quality.md` | Spec-grounded rubric for description auditing — loaded when a finding is borderline |
|
||||||
| `references/body-discipline.md` | Spec-grounded rubric for body discipline auditing — loaded when padding vs necessity is unclear |
|
| `references/body-discipline.md` | Spec-grounded rubric for body discipline auditing — loaded when padding vs necessity is unclear |
|
||||||
| `references/sources.md` | Provenance record — agentskills.io sources that informed this skill and which files each contributed to |
|
| `references/sources.md` | Provenance record — agentskills.io sources that informed this skill and which files each contributed to |
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ Work through each dimension internally. Collect findings only; report them in St
|
|||||||
|
|
||||||
Vale's `Kyberforge.DescriptionOpener` ("This skill..." openers) and `Kyberforge.VagueWording` (filler like "helps with", "utilize") alerts from Step 1 — both FAILs — cover imperative phrasing and known vague-wording filler directly; report them as findings without re-deriving by judgment. The rest is still a judgment call:
|
Vale's `Kyberforge.DescriptionOpener` ("This skill..." openers) and `Kyberforge.VagueWording` (filler like "helps with", "utilize") alerts from Step 1 — both FAILs — cover imperative phrasing and known vague-wording filler directly; report them as findings without re-deriving by judgment. The rest is still a judgment call:
|
||||||
|
|
||||||
|
- **Action-verb opening**: does the description start with a verb ("Audits...", "Reviews...", "Validates...")? Vale's `Kyberforge.DescriptionOpener` alert only catches the literal "This skill..." pattern — confirming an arbitrary opening word is genuinely a strong verb still requires judgment.
|
||||||
- **Specificity beyond the filler blocklist**: are capabilities stated precisely ("parses OpenAPI specs") or genuinely vaguely ("handles files")?
|
- **Specificity beyond the filler blocklist**: are capabilities stated precisely ("parses OpenAPI specs") or genuinely vaguely ("handles files")?
|
||||||
- **Indirect triggers**: does it cover cases where the user doesn't name the domain directly?
|
- **Indirect triggers**: does it cover cases where the user doesn't name the domain directly?
|
||||||
- **Near-miss exclusions**: are "Do not use when..." clauses present if a near-miss skill could steal activations?
|
- **Near-miss exclusions**: are "Do not use when..." clauses present if a near-miss skill could steal activations?
|
||||||
|
|||||||
@@ -77,6 +77,17 @@ is_builtin_output() {
|
|||||||
*) false ;;
|
*) false ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
# Absolutizes a `--config` value against the caller's cwd. Shared by both
|
||||||
|
# argument forms below — separated (`--config X`) and joined (`--config=X`)
|
||||||
|
# — so the "already absolute vs. needs $cwd prefixed" check lives in exactly
|
||||||
|
# one place instead of being duplicated per form.
|
||||||
|
abs_config_value() {
|
||||||
|
if [[ "$1" == /* ]]; then
|
||||||
|
printf '%s' "$1"
|
||||||
|
else
|
||||||
|
printf '%s' "$cwd/$1"
|
||||||
|
fi
|
||||||
|
}
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
if [[ -n "$pending_flag" ]]; then
|
if [[ -n "$pending_flag" ]]; then
|
||||||
# Value of a separated two-argv flag. It is never a lint target, however
|
# Value of a separated two-argv flag. It is never a lint target, however
|
||||||
@@ -85,11 +96,7 @@ for arg in "$@"; do
|
|||||||
case "$pending_flag" in
|
case "$pending_flag" in
|
||||||
--config)
|
--config)
|
||||||
# Always a path, and required to exist.
|
# Always a path, and required to exist.
|
||||||
if [[ "$arg" == /* ]]; then
|
vale_args+=("$(abs_config_value "$arg")")
|
||||||
vale_args+=("$arg")
|
|
||||||
else
|
|
||||||
vale_args+=("$cwd/$arg")
|
|
||||||
fi
|
|
||||||
;;
|
;;
|
||||||
--output|--path)
|
--output|--path)
|
||||||
# See `is_builtin_output` above for why the built-in `--output` names
|
# See `is_builtin_output` above for why the built-in `--output` names
|
||||||
@@ -117,13 +124,8 @@ for arg in "$@"; do
|
|||||||
config_given=true
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
--config=/*)
|
|
||||||
vale_args+=("$arg")
|
|
||||||
config_given=true
|
|
||||||
continue
|
|
||||||
;;
|
|
||||||
--config=*)
|
--config=*)
|
||||||
vale_args+=("--config=$cwd/${arg#--config=}")
|
vale_args+=("--config=$(abs_config_value "${arg#--config=}")")
|
||||||
config_given=true
|
config_given=true
|
||||||
continue
|
continue
|
||||||
;;
|
;;
|
||||||
@@ -204,11 +206,33 @@ abspath() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
flatten() {
|
flatten() {
|
||||||
python3 - "$1" "$2" <<'PYTHON'
|
# Two call shapes: `flatten src dest` (dest already resolved and inside the
|
||||||
|
# scratch tree — the per-markdown-file calls in the directory branch below)
|
||||||
|
# writes straight to `dest`. `flatten src raw_dest tmpdir` (the single-file
|
||||||
|
# branch further down) additionally resolves `raw_dest` the way a separate
|
||||||
|
# `abspath` call used to, applies the same sandbox-escape guard, and prints
|
||||||
|
# the resolved path — folding two python3 spawns per file into one.
|
||||||
|
python3 - "$@" <<'PYTHON'
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
src, dest = sys.argv[1], sys.argv[2]
|
src, dest_input = sys.argv[1], sys.argv[2]
|
||||||
|
tmpdir = sys.argv[3] if len(sys.argv) > 3 else None
|
||||||
|
|
||||||
|
if tmpdir is None:
|
||||||
|
dest = dest_input
|
||||||
|
else:
|
||||||
|
dest = os.path.abspath(dest_input)
|
||||||
|
if not dest.startswith(tmpdir + os.sep):
|
||||||
|
print(
|
||||||
|
f"vale-wrap.sh: refusing to lint '{src}': its scratch copy would "
|
||||||
|
f"land outside {tmpdir}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(2)
|
||||||
|
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
||||||
|
|
||||||
# surrogateescape keeps a non-UTF-8 file (reachable via a directory argument)
|
# surrogateescape keeps a non-UTF-8 file (reachable via a directory argument)
|
||||||
# a byte-for-byte round trip instead of aborting the whole run on a decode error.
|
# a byte-for-byte round trip instead of aborting the whole run on a decode error.
|
||||||
with open(src, encoding='utf-8', errors='surrogateescape') as fh:
|
with open(src, encoding='utf-8', errors='surrogateescape') as fh:
|
||||||
@@ -435,6 +459,9 @@ if header_m:
|
|||||||
|
|
||||||
with open(dest, 'w', encoding='utf-8', errors='surrogateescape') as fh:
|
with open(dest, 'w', encoding='utf-8', errors='surrogateescape') as fh:
|
||||||
fh.write(content)
|
fh.write(content)
|
||||||
|
|
||||||
|
if tmpdir is not None:
|
||||||
|
print(dest)
|
||||||
PYTHON
|
PYTHON
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,23 +476,23 @@ mkdir -p "$mirror"
|
|||||||
argv_paths=()
|
argv_paths=()
|
||||||
for arg in ${path_args[@]+"${path_args[@]}"}; do
|
for arg in ${path_args[@]+"${path_args[@]}"}; do
|
||||||
if [[ "$arg" == /* ]]; then
|
if [[ "$arg" == /* ]]; then
|
||||||
dest="$tmpdir$arg"
|
raw_dest="$tmpdir$arg"
|
||||||
else
|
else
|
||||||
dest="$mirror/$arg"
|
raw_dest="$mirror/$arg"
|
||||||
fi
|
fi
|
||||||
dest="$(abspath "$dest")"
|
|
||||||
# A path argument with enough leading `..` to climb past the mirror root would
|
|
||||||
# write outside the scratch dir. The real filesystem clamps such a path at
|
|
||||||
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
|
|
||||||
case "$dest" in
|
|
||||||
"$tmpdir"/*) ;;
|
|
||||||
*)
|
|
||||||
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
|
|
||||||
exit 2
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
mkdir -p "$(dirname "$dest")"
|
|
||||||
if [[ -d "$arg" ]]; then
|
if [[ -d "$arg" ]]; then
|
||||||
|
dest="$(abspath "$raw_dest")"
|
||||||
|
# A path argument with enough leading `..` to climb past the mirror root would
|
||||||
|
# write outside the scratch dir. The real filesystem clamps such a path at
|
||||||
|
# `/`; the mirror can't, so refuse rather than scribble outside the sandbox.
|
||||||
|
case "$dest" in
|
||||||
|
"$tmpdir"/*) ;;
|
||||||
|
*)
|
||||||
|
echo "vale-wrap.sh: refusing to lint '$arg': its scratch copy would land outside $tmpdir" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
mkdir -p "$(dirname "$dest")"
|
||||||
# A directory is mirrored whole — vale applies its own format filtering to
|
# A directory is mirrored whole — vale applies its own format filtering to
|
||||||
# the tree, so any file dropped here would be silently unlinted — and then
|
# the tree, so any file dropped here would be silently unlinted — and then
|
||||||
# every markdown file in the copy is flattened in place. `.git` is pruned:
|
# every markdown file in the copy is flattened in place. `.git` is pruned:
|
||||||
@@ -484,7 +511,9 @@ for arg in ${path_args[@]+"${path_args[@]}"}; do
|
|||||||
flatten "$md" "$md"
|
flatten "$md" "$md"
|
||||||
done < <(find "$dest" -type f -name '*.md' -print0)
|
done < <(find "$dest" -type f -name '*.md' -print0)
|
||||||
else
|
else
|
||||||
flatten "$arg" "$dest"
|
# `abspath` + `flatten` folded into one python3 process — see the comment
|
||||||
|
# atop `flatten` above.
|
||||||
|
dest="$(flatten "$arg" "$raw_dest" "$tmpdir")"
|
||||||
fi
|
fi
|
||||||
if [[ "$arg" == /* ]]; then
|
if [[ "$arg" == /* ]]; then
|
||||||
argv_paths+=("$dest")
|
argv_paths+=("$dest")
|
||||||
|
|||||||
@@ -55,7 +55,12 @@ fi
|
|||||||
# experiment tag reachable from the pushed ref must not shift the diff baseline.
|
# experiment tag reachable from the pushed ref must not shift the diff baseline.
|
||||||
# The tag is resolved from $PUSHED_REF, not HEAD, for the same reason the diff
|
# The tag is resolved from $PUSHED_REF, not HEAD, for the same reason the diff
|
||||||
# is: a tag reachable only from HEAD is not part of the history being pushed.
|
# is: a tag reachable only from HEAD is not part of the history being pushed.
|
||||||
LAST_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' "$PUSHED_REF" 2>/dev/null || true)"
|
# --match is a shell glob, not a regex: its trailing `*`s match any suffix, so
|
||||||
|
# without --exclude a pre-release/checkpoint tag like v1.2.3-checkpoint or
|
||||||
|
# v1.2.3-rc1 also satisfies 'v[0-9]*.[0-9]*.[0-9]*' and could be picked over the
|
||||||
|
# true last release tag. --exclude is glob syntax too, so '*-*' is what actually
|
||||||
|
# rules out any tag carrying a hyphenated suffix, leaving only bare vMAJOR.MINOR.PATCH.
|
||||||
|
LAST_TAG="$(git describe --tags --abbrev=0 --match 'v[0-9]*.[0-9]*.[0-9]*' --exclude '*-*' "$PUSHED_REF" 2>/dev/null || true)"
|
||||||
|
|
||||||
if [[ -z "$LAST_TAG" ]]; then
|
if [[ -z "$LAST_TAG" ]]; then
|
||||||
echo "FAIL: no release tag exists yet, but .pre-commit-hooks.yaml already exposes hooks to external consumers." >&2
|
echo "FAIL: no release tag exists yet, but .pre-commit-hooks.yaml already exposes hooks to external consumers." >&2
|
||||||
|
|||||||
@@ -10,11 +10,17 @@ set -euo pipefail
|
|||||||
# only one of the two. Run from repo root or pass REPO_ROOT as arg.
|
# only one of the two. Run from repo root or pass REPO_ROOT as arg.
|
||||||
|
|
||||||
REPO_ROOT="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
REPO_ROOT="${1:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
||||||
|
# A nonexistent REPO_ROOT must fail loudly, not fall through to the "neither
|
||||||
|
# copy present" no-op below — that guard exists for a repo that legitimately
|
||||||
|
# has no kyberforge plugin installed, not for a typo'd or stale path, and a
|
||||||
|
# clean exit 0 here would read as "checked, in sync" when nothing ran at all.
|
||||||
|
if [[ ! -d "$REPO_ROOT" ]]; then
|
||||||
|
echo "Vale style sync check failed: REPO_ROOT '$REPO_ROOT' is not a directory." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
# Absolutized because the glob probe below `cd`s into a scratch tree, where a
|
# Absolutized because the glob probe below `cd`s into a scratch tree, where a
|
||||||
# relative --config path would stop resolving.
|
# relative --config path would stop resolving.
|
||||||
if [[ -d "$REPO_ROOT" ]]; then
|
REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
|
||||||
REPO_ROOT="$(cd "$REPO_ROOT" && pwd)"
|
|
||||||
fi
|
|
||||||
FAIL=0
|
FAIL=0
|
||||||
|
|
||||||
err() { echo " FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
|
err() { echo " FAIL: $1" >&2; FAIL=$((FAIL + 1)); }
|
||||||
@@ -80,26 +86,50 @@ done
|
|||||||
# Prints the `files:` regex of every hook, in either manifest, whose entry is
|
# Prints the `files:` regex of every hook, in either manifest, whose entry is
|
||||||
# $1's vale-wrap.sh. Records are delimited by their `- id:` line, so the check
|
# $1's vale-wrap.sh. Records are delimited by their `- id:` line, so the check
|
||||||
# does not depend on `entry:` preceding `files:` within a record.
|
# does not depend on `entry:` preceding `files:` within a record.
|
||||||
|
#
|
||||||
|
# Cached per skill (parallel HOOK_REGEX_CACHE_KEYS/_VALS arrays, populated
|
||||||
|
# lazily) because the final validation loop below probes agent-audit twice —
|
||||||
|
# once for its CC agent-file shape, once for its Copilot .agent.md shape — and
|
||||||
|
# both probes need the same regex set. Without the cache, that pair of calls
|
||||||
|
# would each re-parse both manifest files from scratch for no new information.
|
||||||
|
# Plain indexed arrays, not `declare -A`: associative arrays are bash 4.0+ and
|
||||||
|
# this script must run on macOS's stock bash 3.2. Only ${#arr[@]} (always safe
|
||||||
|
# on an empty/unset array under `set -u`) and index access are used below —
|
||||||
|
# never a bare `${arr[@]}` expansion, which aborts on bash < 4.4 under nounset.
|
||||||
|
HOOK_REGEX_CACHE_KEYS=()
|
||||||
|
HOOK_REGEX_CACHE_VALS=()
|
||||||
hook_file_regexes() {
|
hook_file_regexes() {
|
||||||
local skill="$1" manifest raw
|
local skill="$1" manifest raw result idx=0
|
||||||
for manifest in "$REPO_ROOT/.pre-commit-hooks.yaml" "$REPO_ROOT/.pre-commit-config.yaml"; do
|
while [[ $idx -lt ${#HOOK_REGEX_CACHE_KEYS[@]} ]]; do
|
||||||
[[ -f "$manifest" ]] || continue
|
if [[ "${HOOK_REGEX_CACHE_KEYS[$idx]}" == "$skill" ]]; then
|
||||||
awk -v skill="$skill" '
|
printf '%s' "${HOOK_REGEX_CACHE_VALS[$idx]}"
|
||||||
function flush() {
|
return
|
||||||
if (entry ~ skill "/scripts/vale-wrap.sh" && files != "") print files
|
fi
|
||||||
entry = ""; files = ""
|
idx=$((idx + 1))
|
||||||
}
|
|
||||||
/^[ \t]*-[ \t]*id:/ { flush() }
|
|
||||||
/^[ \t]*entry:/ { entry = $0 }
|
|
||||||
/^[ \t]*files:/ { files = $0; sub(/^[ \t]*files:[ \t]*/, "", files) }
|
|
||||||
END { flush() }
|
|
||||||
' "$manifest"
|
|
||||||
done | while IFS= read -r raw; do
|
|
||||||
# Strip the surrounding YAML quotes; the regex itself never carries them.
|
|
||||||
raw="${raw%\'}"; raw="${raw#\'}"
|
|
||||||
raw="${raw%\"}"; raw="${raw#\"}"
|
|
||||||
printf '%s\n' "$raw"
|
|
||||||
done
|
done
|
||||||
|
result="$(
|
||||||
|
for manifest in "$REPO_ROOT/.pre-commit-hooks.yaml" "$REPO_ROOT/.pre-commit-config.yaml"; do
|
||||||
|
[[ -f "$manifest" ]] || continue
|
||||||
|
awk -v skill="$skill" '
|
||||||
|
function flush() {
|
||||||
|
if (entry ~ skill "/scripts/vale-wrap.sh" && files != "") print files
|
||||||
|
entry = ""; files = ""
|
||||||
|
}
|
||||||
|
/^[ \t]*-[ \t]*id:/ { flush() }
|
||||||
|
/^[ \t]*entry:/ { entry = $0 }
|
||||||
|
/^[ \t]*files:/ { files = $0; sub(/^[ \t]*files:[ \t]*/, "", files) }
|
||||||
|
END { flush() }
|
||||||
|
' "$manifest"
|
||||||
|
done | while IFS= read -r raw; do
|
||||||
|
# Strip the surrounding YAML quotes; the regex itself never carries them.
|
||||||
|
raw="${raw%\'}"; raw="${raw#\'}"
|
||||||
|
raw="${raw%\"}"; raw="${raw#\"}"
|
||||||
|
printf '%s\n' "$raw"
|
||||||
|
done
|
||||||
|
)"
|
||||||
|
HOOK_REGEX_CACHE_KEYS[${#HOOK_REGEX_CACHE_KEYS[@]}]="$skill"
|
||||||
|
HOOK_REGEX_CACHE_VALS[${#HOOK_REGEX_CACHE_VALS[@]}]="$result"
|
||||||
|
printf '%s' "$result"
|
||||||
}
|
}
|
||||||
|
|
||||||
# Asks vale — the thing that actually applies these globs — whether a config
|
# Asks vale — the thing that actually applies these globs — whether a config
|
||||||
|
|||||||
@@ -35,6 +35,13 @@ set -euo pipefail
|
|||||||
# tokenization and does not replace one. Re-measure the corpus before treating
|
# tokenization and does not replace one. Re-measure the corpus before treating
|
||||||
# any of these numbers as still current.
|
# any of these numbers as still current.
|
||||||
|
|
||||||
|
# These constants are intentionally duplicated in
|
||||||
|
# skill-audit/scripts/validate.sh (Python) rather than shared from one file:
|
||||||
|
# this script is a standalone bash pre-commit hook, that one is an in-skill
|
||||||
|
# Python validator invoked in a different context (same rationale as
|
||||||
|
# vale-wrap.sh's per-plugin duplication — see its own header comment).
|
||||||
|
# tests/test-skill-size-check.sh asserts both files agree on these values, so
|
||||||
|
# drift between them fails CI rather than silently diverging.
|
||||||
MAX_LINES=500
|
MAX_LINES=500
|
||||||
MAX_WORDS=2770
|
MAX_WORDS=2770
|
||||||
FAIL=0
|
FAIL=0
|
||||||
@@ -42,16 +49,19 @@ FAIL=0
|
|||||||
for f in "$@"; do
|
for f in "$@"; do
|
||||||
[[ -f "$f" ]] || continue
|
[[ -f "$f" ]] || continue
|
||||||
|
|
||||||
# awk's NR counts the final line even without a trailing newline, matching
|
# Single awk pass computes both line count and word count, avoiding a
|
||||||
# Python's splitlines() semantics (used by skill-audit/scripts/validate.sh
|
# second read of the file. NR counts the final line even without a
|
||||||
# for its own line count) — `wc -l` undercounts by 1 in that case.
|
# trailing newline, matching Python's splitlines() semantics (used by
|
||||||
lines=$(awk 'END{print NR}' "$f")
|
# skill-audit/scripts/validate.sh for its own line count) — `wc -l`
|
||||||
|
# undercounts by 1 in that case. Word count uses awk's default
|
||||||
|
# whitespace-splitting NF, matching `wc -w` semantics.
|
||||||
|
read -r lines words <<< "$(awk '{w += NF} END{print NR, w+0}' "$f")"
|
||||||
|
|
||||||
if (( lines > MAX_LINES )); then
|
if (( lines > MAX_LINES )); then
|
||||||
echo "ERROR: $f has $lines lines, exceeding the $MAX_LINES-line ceiling (agentskills.io skill-authoring.md)" >&2
|
echo "ERROR: $f has $lines lines, exceeding the $MAX_LINES-line ceiling (agentskills.io skill-authoring.md)" >&2
|
||||||
FAIL=1
|
FAIL=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
words=$(wc -w < "$f")
|
|
||||||
if (( words > MAX_WORDS )); then
|
if (( words > MAX_WORDS )); then
|
||||||
echo "ERROR: $f has $words words (proxy for tokens), exceeding the $MAX_WORDS-word ceiling (~5,000 tokens, agentskills.io skill-authoring.md)" >&2
|
echo "ERROR: $f has $words words (proxy for tokens), exceeding the $MAX_WORDS-word ceiling (~5,000 tokens, agentskills.io skill-authoring.md)" >&2
|
||||||
FAIL=1
|
FAIL=1
|
||||||
|
|||||||
@@ -419,6 +419,24 @@ else
|
|||||||
fail "the repo's own .pre-commit-hooks.yaml no longer satisfies the entry constraints: $OUT20"
|
fail "the repo's own .pre-commit-hooks.yaml no longer satisfies the entry constraints: $OUT20"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- 21. A vX.Y.Z-suffixed checkpoint tag must not satisfy the release gate ---
|
||||||
|
# git describe --match uses shell-glob semantics, not regex: the trailing `*` in
|
||||||
|
# 'v[0-9]*.[0-9]*.[0-9]*' matches any suffix, so a pre-release/checkpoint tag like
|
||||||
|
# v1.0.1-checkpoint also satisfies the glob and can be picked as LAST_TAG instead
|
||||||
|
# of the true last release tag — hiding a real release-relevant change that landed
|
||||||
|
# before the checkpoint tag from the diff.
|
||||||
|
echo ""
|
||||||
|
echo "--- ignores a vX.Y.Z-checkpoint tag and still flags the change since the real release tag ---"
|
||||||
|
FIXTURE21="$(make_tagged_fixture)"; track "$FIXTURE21"
|
||||||
|
echo "v2" > "$FIXTURE21/scripts/skill-size-check.sh"
|
||||||
|
(cd "$FIXTURE21" && git add -A && git commit -q -m "real release-relevant change" && git tag v1.0.1-checkpoint)
|
||||||
|
OUT21=$(run_check "$FIXTURE21" "refs/heads/main" || true)
|
||||||
|
if echo "$OUT21" | grep -q "skill-size-check.sh"; then
|
||||||
|
pass "still flags the release-relevant change since v1.0.0, ignoring the vX.Y.Z-checkpoint tag"
|
||||||
|
else
|
||||||
|
fail "a vX.Y.Z-checkpoint tag satisfied the glob and hid a real release-relevant change"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
[[ $FAIL -eq 0 ]]
|
[[ $FAIL -eq 0 ]]
|
||||||
|
|||||||
@@ -123,6 +123,19 @@ else
|
|||||||
fail "exited non-zero when skill-audit/agent-audit are simply absent"
|
fail "exited non-zero when skill-audit/agent-audit are simply absent"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- 5b. Exits 1 when REPO_ROOT does not exist ---
|
||||||
|
# A nonexistent path used to fall through to the "neither copy present" no-op
|
||||||
|
# (test 5 above) and exit 0 — indistinguishable from a real, verified in-sync
|
||||||
|
# result. That guard is for a repo legitimately missing kyberforge, not a
|
||||||
|
# typo'd or stale path.
|
||||||
|
echo ""
|
||||||
|
echo "--- exits 1 when REPO_ROOT does not exist ---"
|
||||||
|
if bash "$SCRIPT" "/nonexistent/path/$(date +%s)-$$" > /dev/null 2>&1; then
|
||||||
|
fail "exited 0 for a nonexistent REPO_ROOT — expected exit 1"
|
||||||
|
else
|
||||||
|
pass "exits non-zero for a nonexistent REPO_ROOT"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- 6. Exits 1 when only one of the two copies is present ---
|
# --- 6. Exits 1 when only one of the two copies is present ---
|
||||||
# The no-op guard used `||`, so a single missing copy also exited 0 — a deleted
|
# The no-op guard used `||`, so a single missing copy also exited 0 — a deleted
|
||||||
# or renamed copy passed the sync check silently.
|
# or renamed copy passed the sync check silently.
|
||||||
|
|||||||
@@ -447,7 +447,10 @@ fi
|
|||||||
# scripts, and the test runner AGENTS.md tells contributors to run by hand.
|
# scripts, and the test runner AGENTS.md tells contributors to run by hand.
|
||||||
# `mapfile` is checked alongside, because it is bash 4.0+ and the expansion scan
|
# `mapfile` is checked alongside, because it is bash 4.0+ and the expansion scan
|
||||||
# cannot see it — run-tests.sh carried one until it was replaced with a
|
# cannot see it — run-tests.sh carried one until it was replaced with a
|
||||||
# `while read` loop, and nothing would have caught its return.
|
# `while read` loop, and nothing would have caught its return. `declare -A`
|
||||||
|
# (bash 4.0+ associative arrays) is checked for the same reason — the
|
||||||
|
# expansion scan cannot see it, and check-vale-style-sync.sh carried a pair of
|
||||||
|
# them until they were replaced with index-scanned plain arrays.
|
||||||
echo ""
|
echo ""
|
||||||
echo "--- no unguarded array expansion remains in the macOS-facing scripts ---"
|
echo "--- no unguarded array expansion remains in the macOS-facing scripts ---"
|
||||||
unguarded_expansions() {
|
unguarded_expansions() {
|
||||||
@@ -474,6 +477,7 @@ for BASH32_SCRIPT in \
|
|||||||
"$SCRIPT" \
|
"$SCRIPT" \
|
||||||
"$REPO_ROOT/scripts/skill-size-check.sh" \
|
"$REPO_ROOT/scripts/skill-size-check.sh" \
|
||||||
"$REPO_ROOT/scripts/check-release-needed.sh" \
|
"$REPO_ROOT/scripts/check-release-needed.sh" \
|
||||||
|
"$REPO_ROOT/scripts/check-vale-style-sync.sh" \
|
||||||
"$REPO_ROOT/tests/run-tests.sh"; do
|
"$REPO_ROOT/tests/run-tests.sh"; do
|
||||||
FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")"
|
FOUND16="$(unguarded_expansions "$BASH32_SCRIPT")"
|
||||||
if [[ -n "$FOUND16" ]]; then
|
if [[ -n "$FOUND16" ]]; then
|
||||||
@@ -486,6 +490,15 @@ for BASH32_SCRIPT in \
|
|||||||
if [[ -n "$FOUND16B" ]]; then
|
if [[ -n "$FOUND16B" ]]; then
|
||||||
HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16B "
|
HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16B "
|
||||||
fi
|
fi
|
||||||
|
# `declare -A` (associative arrays) is bash 4.0+ with no 3.2 fallback. The
|
||||||
|
# flag cluster can carry other letters in any order (-Ag, -rA, ...); what
|
||||||
|
# matters is a literal uppercase A appearing in it, so match on that rather
|
||||||
|
# than the exact string "-A".
|
||||||
|
FOUND16C="$(awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$BASH32_SCRIPT" \
|
||||||
|
| grep -nE '(^|[^[:alnum:]_])declare[[:space:]]+-[a-zA-Z]*A[a-zA-Z]*([[:space:]]|$)' || true)"
|
||||||
|
if [[ -n "$FOUND16C" ]]; then
|
||||||
|
HAZARDS16+="${BASH32_SCRIPT##*/}:$FOUND16C "
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
if [[ -n "$HAZARDS16" ]]; then
|
if [[ -n "$HAZARDS16" ]]; then
|
||||||
fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')"
|
fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')"
|
||||||
|
|||||||
Reference in New Issue
Block a user