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:
2026-08-31 19:46:07 +00:00
parent 00daf285ec
commit a8cd5e881d
9 changed files with 162 additions and 31 deletions

View File

@@ -23,7 +23,7 @@ Provide the path to the provider-specific file to convert (and the target repo r
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `SKILL.md` | Skill instructions for agents | | `SKILL.md` | Skill instructions for agents |
| `references/provider-matrix.md` | Loaded at Step 1 before searching, unless the target is already a known root `CLAUDE.md`: known files per provider, which ones resolve a cross-file import, and the validator flag each needs | | `references/provider-matrix.md` | Loaded at Step 1 before searching, unless the target is already a known root `CLAUDE.md`: known files per provider, which ones resolve a cross-file import, the validator flag each needs, and the rule that a standalone run and a run composed into by `agentsmd-author` behave identically |
| `references/sources.md` | Provenance record — the in-repo ADR precedent this skill's design is modeled on | | `references/sources.md` | Provenance record — the in-repo ADR precedent this skill's design is modeled on |
| `scripts/validate-adapter.sh` | Self-check gate: reference to AGENTS.md present, no excessive duplication, adapter stays thin | | `scripts/validate-adapter.sh` | Self-check gate: reference to AGENTS.md present, no excessive duplication, adapter stays thin |
| `scripts/README.md` | Directory documentation for `scripts/` | | `scripts/README.md` | Directory documentation for `scripts/` |

View File

@@ -18,6 +18,8 @@ metadata:
- Assume a provider has no cross-file import mechanism until you have confirmed it has one. Claude Code is the exception, not the rule: a `CLAUDE.md` may consist of nothing but `@path` lines, while the same `@AGENTS.md` line in a Cursor rule or a Copilot instructions file is inert text no tool resolves. Pass `--no-import-syntax` to `scripts/validate-adapter.sh` for those providers. - Assume a provider has no cross-file import mechanism until you have confirmed it has one. Claude Code is the exception, not the rule: a `CLAUDE.md` may consist of nothing but `@path` lines, while the same `@AGENTS.md` line in a Cursor rule or a Copilot instructions file is inert text no tool resolves. Pass `--no-import-syntax` to `scripts/validate-adapter.sh` for those providers.
- Works standalone or composed-into by `agentsmd-author` — behave identically either way; do not assume a caller skill exists. Detect the provider file, confirm `AGENTS.md`, and run the closeout validator yourself in both cases (`references/provider-matrix.md`).
## Step 1 — Detect ## Step 1 — Detect
Find the provider instruction file to convert. Before searching, read `references/provider-matrix.md` — skip it only when the target is already a known root `CLAUDE.md`, which is the common case. Find the provider instruction file to convert. Before searching, read `references/provider-matrix.md` — skip it only when the target is already a known root `CLAUDE.md`, which is the common case.
@@ -28,7 +30,7 @@ Then confirm `AGENTS.md` exists at the repo root. If it does not, stop and tell
Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file: Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file:
- **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import line, keep the provider-specific bucket below it. - **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import on a line of its own, keep the provider-specific bucket below it. An import folded into a sentence is not the thin-adapter shape and `scripts/validate-adapter.sh` will not credit it.
- **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket. - **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket.
The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like. The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like.
@@ -43,7 +45,7 @@ Run the bundled check before finishing — this is the skill's own closeout gate
bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file> bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file>
``` ```
Fix any `FAIL` by editing the provider file, and re-run until it exits `0`. Fix any `FAIL` by editing the provider file, and re-run until it exits `0`. Exit `2` is not a `FAIL`: it means the invocation or the input is wrong — a bad or missing argument, or a file that is not UTF-8 — so fix that, not the adapter.
## Step 4 — Report ## Step 4 — Report

View File

@@ -21,3 +21,12 @@ silently drops every rule the adapter was supposed to defer to.
Detection is a search, not a lookup: a repo may hold more than one of these, and each one converts Detection is a search, not a lookup: a repo may hold more than one of these, and each one converts
independently against the same `AGENTS.md`. independently against the same `AGENTS.md`.
## Standalone and composed runs behave identically
This skill is reached two ways: invoked directly by a user, and composed into by `agentsmd-author`
once it has written or updated the repo's `AGENTS.md`. Behave identically either way — do not
assume a caller skill exists. Detect the provider file yourself, confirm `AGENTS.md` yourself, and
run the closeout validator yourself, rather than treating any step as already done by the caller or
as something the caller will do afterwards. There is no handshake to rely on and no state passed
in beyond the file paths.

View File

@@ -31,6 +31,12 @@ Exit codes:
0 Adapter file passes all checks 0 Adapter file passes all checks
1 One or more checks failed (empty file, no reference to AGENTS.md, 1 One or more checks failed (empty file, no reference to AGENTS.md,
excessive duplication, or file too long) 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 EOF
} }
@@ -51,12 +57,12 @@ while [[ $# -gt 0 ]]; do
--max-lines) --max-lines)
if [[ $# -lt 2 ]]; then if [[ $# -lt 2 ]]; then
echo "Error: --max-lines requires a value (a non-negative integer)." >&2 echo "Error: --max-lines requires a value (a non-negative integer)." >&2
exit 1 exit 2
fi fi
MAX_LINES="$2" MAX_LINES="$2"
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2 echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2
exit 1 exit 2
fi fi
shift 2 shift 2
;; ;;
@@ -71,7 +77,7 @@ if [[ ${#ARGS[@]} -lt 2 ]]; then
echo "Error: adapter-file and agents-md-file are required." >&2 echo "Error: adapter-file and agents-md-file are required." >&2
echo "" >&2 echo "" >&2
usage >&2 usage >&2
exit 1 exit 2
fi fi
python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON' 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): if not os.path.isfile(adapter_path):
print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr) 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): if not os.path.isfile(agents_md_path):
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr) 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() def read_text(path):
with open(agents_md_path, encoding="utf-8", errors="replace") as f: r"""File contents as text, UTF-8, BOM stripped.
agents_md_content = f.read()
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 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(" 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.\"") print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"")
else: else:
print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line; merely naming the file in prose defers nothing.") 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: Add an `@AGENTS.md` (or equivalent relative path) import line, or pass --no-import-syntax if this provider resolves no imports.") 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() print()
# --- Duplication check --- # --- Duplication check ---

View File

@@ -148,7 +148,7 @@ EOF
ADAPTER="$TMPDIR/CLAUDE.md" ADAPTER="$TMPDIR/CLAUDE.md"
echo "@AGENTS.md" > "$ADAPTER" echo "@AGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" --max-lines run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" --max-lines
assert_failure assert_failure 2
assert_output --partial "--max-lines requires a value" assert_output --partial "--max-lines requires a value"
} }
@@ -156,7 +156,7 @@ EOF
ADAPTER="$TMPDIR/CLAUDE.md" ADAPTER="$TMPDIR/CLAUDE.md"
echo "@AGENTS.md" > "$ADAPTER" echo "@AGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" --max-lines abc "$ADAPTER" "$AGENTS_MD" run bash "$SCRIPT" --max-lines abc "$ADAPTER" "$AGENTS_MD"
assert_failure assert_failure 2
assert_output --partial "non-negative integer" assert_output --partial "non-negative integer"
} }
@@ -168,6 +168,47 @@ EOF
@test "fails with a clear error when the adapter file argument is missing" { @test "fails with a clear error when the adapter file argument is missing" {
run bash "$SCRIPT" run bash "$SCRIPT"
assert_failure assert_failure 2
assert_output --partial "required" assert_output --partial "required"
} }
@test "a UTF-8 BOM before the @import line does not hide it" {
ADAPTER="$TMPDIR/CLAUDE.md"
python3 -c "import sys; open(sys.argv[1], 'w', encoding='utf-8-sig').write('@AGENTS.md\n')" "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_success
}
@test "a usage error exits 2, a genuine finding exits 1" {
ADAPTER="$TMPDIR/CLAUDE.md"
echo "@AGENTS.md" > "$ADAPTER"
run bash "$SCRIPT" --max-lines -3 "$ADAPTER" "$AGENTS_MD"
assert_failure 2
refute_output --partial "FAIL"
: > "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "FAIL"
}
@test "a non-UTF-8 adapter is reported as an encoding error, not as a missing reference" {
ADAPTER="$TMPDIR/CLAUDE.md"
python3 -c "import sys; open(sys.argv[1], 'wb').write('@AGENTS.md\n'.encode('utf-16'))" "$ADAPTER"
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 2
assert_output --partial "not valid UTF-8"
refute_output --partial "no reference"
}
@test "an @AGENTS.md folded into a sentence fails, and the message says the import needs its own line" {
ADAPTER="$TMPDIR/CLAUDE.md"
cat > "$ADAPTER" <<'EOF'
See @AGENTS.md for shared conventions.
EOF
run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD"
assert_failure 1
assert_output --partial "no reference"
assert_output --partial "line of its own"
}

View File

@@ -23,7 +23,7 @@ Provide the path to the provider-specific file to convert (and the target repo r
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `SKILL.md` | Skill instructions for agents | | `SKILL.md` | Skill instructions for agents |
| `references/provider-matrix.md` | Loaded at Step 1 before searching, unless the target is already a known root `CLAUDE.md`: known files per provider, which ones resolve a cross-file import, and the validator flag each needs | | `references/provider-matrix.md` | Loaded at Step 1 before searching, unless the target is already a known root `CLAUDE.md`: known files per provider, which ones resolve a cross-file import, the validator flag each needs, and the rule that a standalone run and a run composed into by `agentsmd-author` behave identically |
| `references/sources.md` | Provenance record — the in-repo ADR precedent this skill's design is modeled on | | `references/sources.md` | Provenance record — the in-repo ADR precedent this skill's design is modeled on |
| `scripts/validate-adapter.sh` | Self-check gate: reference to AGENTS.md present, no excessive duplication, adapter stays thin | | `scripts/validate-adapter.sh` | Self-check gate: reference to AGENTS.md present, no excessive duplication, adapter stays thin |
| `scripts/README.md` | Directory documentation for `scripts/` | | `scripts/README.md` | Directory documentation for `scripts/` |

View File

@@ -18,6 +18,8 @@ metadata:
- Assume a provider has no cross-file import mechanism until you have confirmed it has one. Claude Code is the exception, not the rule: a `CLAUDE.md` may consist of nothing but `@path` lines, while the same `@AGENTS.md` line in a Cursor rule or a Copilot instructions file is inert text no tool resolves. Pass `--no-import-syntax` to `scripts/validate-adapter.sh` for those providers. - Assume a provider has no cross-file import mechanism until you have confirmed it has one. Claude Code is the exception, not the rule: a `CLAUDE.md` may consist of nothing but `@path` lines, while the same `@AGENTS.md` line in a Cursor rule or a Copilot instructions file is inert text no tool resolves. Pass `--no-import-syntax` to `scripts/validate-adapter.sh` for those providers.
- Works standalone or composed-into by `agentsmd-author` — behave identically either way; do not assume a caller skill exists. Detect the provider file, confirm `AGENTS.md`, and run the closeout validator yourself in both cases (`references/provider-matrix.md`).
## Step 1 — Detect ## Step 1 — Detect
Find the provider instruction file to convert. Before searching, read `references/provider-matrix.md` — skip it only when the target is already a known root `CLAUDE.md`, which is the common case. Find the provider instruction file to convert. Before searching, read `references/provider-matrix.md` — skip it only when the target is already a known root `CLAUDE.md`, which is the common case.
@@ -28,7 +30,7 @@ Then confirm `AGENTS.md` exists at the repo root. If it does not, stop and tell
Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file: Read the provider file and `AGENTS.md` side by side. Separate the provider file's content into two buckets: lines that restate what `AGENTS.md` already owns (universal rules, conventions, project overview) versus lines that are genuinely provider-specific (tool syntax, IDE behavior, model-specific instructions). Rewrite the provider file:
- **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import line, keep the provider-specific bucket below it. - **Providers with import syntax** (Claude Code): replace the redundant bucket with an `@AGENTS.md` (or correct relative path) import on a line of its own, keep the provider-specific bucket below it. An import folded into a sentence is not the thin-adapter shape and `scripts/validate-adapter.sh` will not credit it.
- **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket. - **Providers without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket.
The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like. The provider file is the only file this skill ever writes. Never create or edit `AGENTS.md` — not in this step, not in any step, whatever the payoff looks like.
@@ -43,7 +45,7 @@ Run the bundled check before finishing — this is the skill's own closeout gate
bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file> bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file>
``` ```
Fix any `FAIL` by editing the provider file, and re-run until it exits `0`. Fix any `FAIL` by editing the provider file, and re-run until it exits `0`. Exit `2` is not a `FAIL`: it means the invocation or the input is wrong — a bad or missing argument, or a file that is not UTF-8 — so fix that, not the adapter.
## Step 4 — Report ## Step 4 — Report

View File

@@ -21,3 +21,12 @@ silently drops every rule the adapter was supposed to defer to.
Detection is a search, not a lookup: a repo may hold more than one of these, and each one converts Detection is a search, not a lookup: a repo may hold more than one of these, and each one converts
independently against the same `AGENTS.md`. independently against the same `AGENTS.md`.
## Standalone and composed runs behave identically
This skill is reached two ways: invoked directly by a user, and composed into by `agentsmd-author`
once it has written or updated the repo's `AGENTS.md`. Behave identically either way — do not
assume a caller skill exists. Detect the provider file yourself, confirm `AGENTS.md` yourself, and
run the closeout validator yourself, rather than treating any step as already done by the caller or
as something the caller will do afterwards. There is no handshake to rely on and no state passed
in beyond the file paths.

View File

@@ -31,6 +31,12 @@ Exit codes:
0 Adapter file passes all checks 0 Adapter file passes all checks
1 One or more checks failed (empty file, no reference to AGENTS.md, 1 One or more checks failed (empty file, no reference to AGENTS.md,
excessive duplication, or file too long) 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 EOF
} }
@@ -51,12 +57,12 @@ while [[ $# -gt 0 ]]; do
--max-lines) --max-lines)
if [[ $# -lt 2 ]]; then if [[ $# -lt 2 ]]; then
echo "Error: --max-lines requires a value (a non-negative integer)." >&2 echo "Error: --max-lines requires a value (a non-negative integer)." >&2
exit 1 exit 2
fi fi
MAX_LINES="$2" MAX_LINES="$2"
if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then if [[ ! "$MAX_LINES" =~ ^[0-9]+$ ]]; then
echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2 echo "Error: --max-lines expects a non-negative integer, got '$MAX_LINES'." >&2
exit 1 exit 2
fi fi
shift 2 shift 2
;; ;;
@@ -71,7 +77,7 @@ if [[ ${#ARGS[@]} -lt 2 ]]; then
echo "Error: adapter-file and agents-md-file are required." >&2 echo "Error: adapter-file and agents-md-file are required." >&2
echo "" >&2 echo "" >&2
usage >&2 usage >&2
exit 1 exit 2
fi fi
python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON' 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): if not os.path.isfile(adapter_path):
print(f"Error: '{adapter_path}' is not a file.", file=sys.stderr) 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): if not os.path.isfile(agents_md_path):
print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr) 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() def read_text(path):
with open(agents_md_path, encoding="utf-8", errors="replace") as f: r"""File contents as text, UTF-8, BOM stripped.
agents_md_content = f.read()
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 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(" 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.\"") print(" Fix: Add a sentence like \"See AGENTS.md at the repo root for shared conventions.\"")
else: else:
print(" Why: A thin adapter must import AGENTS.md with an `@AGENTS.md` line; merely naming the file in prose defers nothing.") 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: Add an `@AGENTS.md` (or equivalent relative path) import line, or pass --no-import-syntax if this provider resolves no imports.") 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() print()
# --- Duplication check --- # --- Duplication check ---