fix(core): strip a BOM before the adapter import check, and split usage exits
Narrowing has_reference to bool(import_lines) meant a UTF-8 BOM hid the import line, since the BOM is not \s: a CLAUDE.md whose first line is @AGENTS.md failed with 'no reference to AGENTS.md' and was told to add the line already in front of it. Decoding is now strict too, so a non-UTF-8 adapter gets an encoding diagnostic instead of being mangled and then graded on the mangling. Usage errors move to exit 2. They shared exit 1 with real findings, while the skill tells the agent to fix any non-zero exit by editing the provider file. The whole-line import rule is kept deliberately -- accepting an inline @AGENTS.md would also accept one inside backticks, which is the silent-drop failure the validator exists to catch -- and the message now says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EJJrm5YmacbwMdzZpXcoti
This commit is contained in:
@@ -31,6 +31,12 @@ Exit codes:
|
||||
0 Adapter file passes all checks
|
||||
1 One or more checks failed (empty file, no reference to AGENTS.md,
|
||||
excessive duplication, or file too long)
|
||||
2 Usage or input error — a bad or missing argument, a path that is not a
|
||||
file, or a file that is not UTF-8. Nothing was graded, so there is no
|
||||
FAIL line and no adapter edit to make: fix the invocation or the file's
|
||||
encoding and re-run. Kept distinct from 1 because the skill's own
|
||||
closeout tells the agent to fix every non-zero exit by editing the
|
||||
provider file, which for a mistyped flag edits the wrong file forever.
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -51,12 +57,12 @@ while [[ $# -gt 0 ]]; do
|
||||
--max-lines)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Error: --max-lines requires a value (a non-negative integer)." >&2
|
||||
exit 1
|
||||
exit 2
|
||||
fi
|
||||
MAX_LINES="$2"
|
||||
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2
|
||||
exit 1
|
||||
exit 2
|
||||
fi
|
||||
shift 2
|
||||
;;
|
||||
@@ -71,7 +77,7 @@ if [[ ${#ARGS[@]} -lt 2 ]]; then
|
||||
echo "Error: adapter-file and agents-md-file are required." >&2
|
||||
echo "" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
exit 2
|
||||
fi
|
||||
|
||||
python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON'
|
||||
@@ -85,15 +91,43 @@ max_lines = int(max_lines)
|
||||
|
||||
if not os.path.isfile(adapter_path):
|
||||
print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
sys.exit(2)
|
||||
if not os.path.isfile(agents_md_path):
|
||||
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
sys.exit(2)
|
||||
|
||||
with open(adapter_path, encoding="utf-8", errors="replace") as f:
|
||||
adapter_content = f.read()
|
||||
with open(agents_md_path, encoding="utf-8", errors="replace") as f:
|
||||
agents_md_content = f.read()
|
||||
|
||||
def read_text(path):
|
||||
r"""File contents as text, UTF-8, BOM stripped.
|
||||
|
||||
The BOM strip is not cosmetic. IMPORT_RE anchors on `^\s*@`, and a BOM is
|
||||
not `\s` in Python, so a CLAUDE.md saved by an editor that emits one had
|
||||
its first line — the `@AGENTS.md` import, which is the whole adapter —
|
||||
silently treated as prose. The check then said "no reference to AGENTS.md"
|
||||
told the author to add the line already sitting in front of them. Same
|
||||
class of silent BOM miss recorded in scripts/skill-size-check.sh; strip it
|
||||
at the reader so no later check has to know about it.
|
||||
|
||||
Decoding is strict, not errors="replace". Replacement mangles the file and
|
||||
the checks then grade the mangling: a UTF-16 adapter whose first line is
|
||||
`@AGENTS.md` decoded to interleaved NULs and failed as "no reference",
|
||||
which is a true FAIL for a false reason and points the fix at the wrong
|
||||
thing. A file this gate cannot read gets an encoding diagnostic and exit 2,
|
||||
the same policy the ADR-0020 validators' read_text() uses.
|
||||
"""
|
||||
try:
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
text = fh.read()
|
||||
except UnicodeDecodeError as exc:
|
||||
print(f"Error: '{path}' is not valid UTF-8 ({exc.reason} at byte "
|
||||
f"{exc.start}) — re-save it as UTF-8; this check does not guess "
|
||||
"at other encodings.", file=sys.stderr)
|
||||
sys.exit(2)
|
||||
return text[1:] if text.startswith("\ufeff") else text
|
||||
|
||||
|
||||
adapter_content = read_text(adapter_path)
|
||||
agents_md_content = read_text(agents_md_path)
|
||||
|
||||
has_fail = False
|
||||
|
||||
@@ -124,8 +158,8 @@ if not has_reference:
|
||||
print(" Why: This provider resolves no cross-file import, so the adapter must point at AGENTS.md in prose; an `@AGENTS.md` line here is inert text.")
|
||||
print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"")
|
||||
else:
|
||||
print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line; merely naming the file in prose defers nothing.")
|
||||
print(" Fix: Add an `@AGENTS.md` (or equivalent relative path) import line, or pass --no-import-syntax if this provider resolves no imports.")
|
||||
print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line of its own; naming the file mid-sentence or inside backticks is prose this check will not credit, and merely naming it defers nothing.")
|
||||
print(" Fix: Put `@AGENTS.md` (or the equivalent relative path) alone on its own line, or pass --no-import-syntax if this provider resolves no imports.")
|
||||
print()
|
||||
|
||||
# --- Duplication check ---
|
||||
|
||||
Reference in New Issue
Block a user