diff --git a/plugins/core/skills/provider-adapter-author/README.md b/plugins/core/skills/provider-adapter-author/README.md new file mode 100644 index 0000000..ebd4fe2 --- /dev/null +++ b/plugins/core/skills/provider-adapter-author/README.md @@ -0,0 +1,30 @@ +# provider-adapter-author + +Convert a target repo's provider-specific instruction file (CLAUDE.md, .cursor/rules, copilot-instructions.md, etc.) into a thin adapter over AGENTS.md. + +## What it does + +Detects a provider-specific AI instruction file in a target repo, diffs it against the repo's `AGENTS.md`, and rewrites it down to a minimal reference — an `@AGENTS.md`-style import for providers that support one, or a text pointer for those that don't — plus only genuinely provider-specific additions. Self-validates its own output with a bundled deterministic script (no LLM judgment, no separate audit skill) before finishing. + +## Before you start + +The target repo must already have an `AGENTS.md`. If it doesn't, run `agentsmd-author` first — this skill never creates or edits `AGENTS.md` itself. + +## Usage + +``` +/provider-adapter-author +``` + +Provide the path to the provider-specific file to convert (and the target repo root, if not inferable). Can be invoked directly, or composed into by `agentsmd-author` when it detects an existing provider file with content overlapping AGENTS.md. + +## Files + +| File | Purpose | +|------|---------| +| `SKILL.md` | Skill instructions for agents | +| `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/README.md` | Directory documentation for `scripts/` | +| `tests/README.md` | Bats test dependency and run instructions | +| `tests/validate-adapter.bats` | Bats test suite for `scripts/validate-adapter.sh` | diff --git a/plugins/core/skills/provider-adapter-author/SKILL.md b/plugins/core/skills/provider-adapter-author/SKILL.md new file mode 100644 index 0000000..6629822 --- /dev/null +++ b/plugins/core/skills/provider-adapter-author/SKILL.md @@ -0,0 +1,54 @@ +--- +name: provider-adapter-author +description: > + Use when the user wants to convert a provider-specific AI instruction file + (CLAUDE.md, .cursor/rules/*.mdc, copilot-instructions.md, etc.) into a + thin adapter that defers to a repo's AGENTS.md — e.g. "reduce duplication + between CLAUDE.md and AGENTS.md", "make CLAUDE.md just import AGENTS.md" + — even if the pattern isn't named explicitly. Also invoke when + agentsmd-author detects an existing provider file overlapping with + AGENTS.md it just wrote. Detects redundant content in a provider file + relative to AGENTS.md and rewrites it down to a minimal reference (an + `@AGENTS.md`-style import where supported, or a text pointer otherwise) + plus genuinely provider-specific additions. Self-validates via a bundled + deterministic script before finishing. Do not use to write or audit + AGENTS.md itself — use agentsmd-author or agentsmd-audit. +allowed-tools: Bash Read Edit Write +metadata: + category: docs + source_keys: + - adr-0002-0003-two-tier-claude-md + version: "0.1.0" +--- + +## Gotchas + +- Not every provider supports cross-file imports. Claude Code does — a `CLAUDE.md` can consist of nothing but one or more `@path` lines (e.g. `@AGENTS.md`), with no other content required. Cursor's `.cursor/rules/*.mdc` and GitHub Copilot's `copilot-instructions.md` have no native import mechanism as of current tooling — for those, "thin" means a short text pointer to AGENTS.md plus only what that tool actually needs, not a literal import line. Pass `--no-import-syntax` to `scripts/validate-adapter.sh` for these providers. +- This skill never creates or edits `AGENTS.md` itself. If the target repo has no `AGENTS.md` yet, stop and point the user to `agentsmd-author` first — there's nothing to adapt to. +- Only strip content from the provider file that's genuinely redundant with AGENTS.md. Provider-specific material (IDE settings, tool-only syntax, model-specific instructions) stays — the goal is thin, not empty. +- Works standalone or composed-into by `agentsmd-author` — behave identically either way; don't assume a caller skill exists. + +## Step 1 — Detect + +Look for known provider instruction files in the target repo: `CLAUDE.md` (repo root, and any deployed copies), `.cursor/rules/*.mdc`, `.github/copilot-instructions.md`, and similar tool-specific files. Confirm `AGENTS.md` exists at the repo root — if not, stop and tell the user to run `agentsmd-author` first. + +## Step 2 — Diff and rewrite + +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 without import syntax** (Cursor, Copilot, etc.): replace the redundant bucket with a short pointer sentence mentioning `AGENTS.md`, keep the provider-specific bucket. + +## Step 3 — Self-validate + +Run the bundled check before finishing — this is the skill's own closeout gate; there is no separate paired audit skill for this concern: + +```bash +bash scripts/validate-adapter.sh [--no-import-syntax] [--max-lines N] +``` + +Fix any `FAIL` and re-run until it exits `0`. + +## Step 4 — Report + +State which file was converted, what was removed versus kept, and the validator's final result. diff --git a/plugins/core/skills/provider-adapter-author/references/sources.md b/plugins/core/skills/provider-adapter-author/references/sources.md new file mode 100644 index 0000000..7b7071a --- /dev/null +++ b/plugins/core/skills/provider-adapter-author/references/sources.md @@ -0,0 +1,9 @@ +# Sources + +## adr-0002-0003-two-tier-claude-md + +- **URL:** (in-repo precedent — not an external source or plugin research corpus entry) +- **Description:** This repo's own two-tier CLAUDE.md/AGENTS.md pattern: AGENTS.md is the provider-agnostic source of always-on rules; provider-specific files (CLAUDE.md) become thin adapters that import it (`@AGENTS.md` plus provider-specific additions). Grounds this skill's entire adapter-conversion design — the "thin adapter" shape, the `@`-import convention, and the size/duplication expectations enforced by `scripts/validate-adapter.sh`. +- **Research doc:** docs/adr/0002-two-tier-claude-md.md, docs/adr/0003-agents-md-provider-agnostic-entry-point.md, providers/claude-code/CLAUDE.md (in-repo ADRs and a live example, not a plugin research corpus entry; referenced here since this skill's design is modeled directly on an existing implementation rather than external research) +- **Contributing files:** SKILL.md +- **Status:** `extracted` diff --git a/plugins/core/skills/provider-adapter-author/scripts/README.md b/plugins/core/skills/provider-adapter-author/scripts/README.md new file mode 100644 index 0000000..d137d2b --- /dev/null +++ b/plugins/core/skills/provider-adapter-author/scripts/README.md @@ -0,0 +1,9 @@ +# scripts/ + +Deterministic self-check this skill shells out to instead of relying on LLM judgment for a mechanical check. + +| File | Purpose | +|------|---------| +| `validate-adapter.sh` | Checks a rewritten provider file (CLAUDE.md, etc.) has a reference to AGENTS.md, doesn't duplicate its content, and stays under a thin-file line threshold | + +Takes ` `, with optional `--no-import-syntax` and `--max-lines N` flags. Prints `FAIL` findings to stdout and exits non-zero on any failure. diff --git a/plugins/core/skills/provider-adapter-author/scripts/validate-adapter.sh b/plugins/core/skills/provider-adapter-author/scripts/validate-adapter.sh new file mode 100755 index 0000000..a94d112 --- /dev/null +++ b/plugins/core/skills/provider-adapter-author/scripts/validate-adapter.sh @@ -0,0 +1,141 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat < + +Self-check gate for provider-adapter-author. Checks that a rewritten +provider-specific instruction file (CLAUDE.md, .cursor/rules/*.mdc, +copilot-instructions.md, etc.) is actually a thin adapter over AGENTS.md, +not a duplicate copy of it. + +Arguments: + adapter-file Path to the provider-specific file to check. + agents-md-file Path to the AGENTS.md file it should defer to. + +Options: + --no-import-syntax The target provider has no native cross-file import + mechanism. Accept a plain-text pointer mention of + "AGENTS.md" instead of requiring an @import-style line. + --max-lines N Max non-blank lines allowed in the adapter file before + it's considered no longer "thin". Default: 60. + --help, -h Show this help and exit 0. + +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) +EOF +} + +NO_IMPORT_SYNTAX=0 +MAX_LINES=60 +ARGS=() + +while [[ $# -gt 0 ]]; do + case "$1" in + --help|-h) + usage + exit 0 + ;; + --no-import-syntax) + NO_IMPORT_SYNTAX=1 + shift + ;; + --max-lines) + MAX_LINES="${2:-}" + shift 2 + ;; + *) + ARGS+=("$1") + shift + ;; + esac +done + +if [[ ${#ARGS[@]} -lt 2 ]]; then + echo "Error: adapter-file and agents-md-file are required." >&2 + echo "" >&2 + usage >&2 + exit 1 +fi + +python3 -u - "${ARGS[0]}" "${ARGS[1]}" "$NO_IMPORT_SYNTAX" "$MAX_LINES" <<'PYTHON' +import sys +import os +import re + +adapter_path, agents_md_path, no_import_syntax, max_lines = sys.argv[1:5] +no_import_syntax = no_import_syntax == "1" +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) +if not os.path.isfile(agents_md_path): + print(f"Error: '{agents_md_path}' is not a file.", file=sys.stderr) + sys.exit(1) + +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() + +has_fail = False + +if not adapter_content.strip(): + print(f"FAIL Adapter file is empty — {adapter_path}") + print(" Why: An empty adapter carries no reference to AGENTS.md and no provider-specific content.") + print(" Fix: Add at least an import (or text pointer) to AGENTS.md.") + print() + sys.exit(1) + +IMPORT_RE = re.compile(r'(?m)^\s*@\S*AGENTS\.md\s*$') +lines = adapter_content.splitlines() +import_lines = [ln for ln in lines if IMPORT_RE.match(ln)] + +if no_import_syntax: + has_reference = "AGENTS.md" in adapter_content +else: + has_reference = bool(import_lines) or "AGENTS.md" in adapter_content + +if not has_reference: + has_fail = True + print(f"FAIL Adapter has no reference to AGENTS.md — {adapter_path}") + if no_import_syntax: + print(" Why: This provider has no import syntax, so the adapter must at least mention AGENTS.md as a text pointer.") + 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 (e.g. `@AGENTS.md`) rather than silently omitting it.") + print(" Fix: Add an `@AGENTS.md` (or equivalent relative path) import line.") + print() + +# --- Duplication check --- +non_import_lines = [ln for ln in lines if not IMPORT_RE.match(ln)] +adapter_lines = [ln.strip() for ln in non_import_lines if ln.strip()] +agents_lines = {ln.strip() for ln in agents_md_content.splitlines() if ln.strip()} + +if adapter_lines: + overlap = sum(1 for ln in adapter_lines if ln in agents_lines) + ratio = overlap / len(adapter_lines) + if ratio > 0.3: + has_fail = True + print(f"FAIL Adapter duplicates AGENTS.md content — {adapter_path}") + print(f" Why: {ratio:.0%} of the adapter's non-import lines already appear verbatim in AGENTS.md. A thin adapter should import shared content, not restate it.") + print(" Fix: Remove the duplicated lines and rely on the AGENTS.md import (or pointer) instead.") + print() + +# --- Size check --- +non_blank_count = len([ln for ln in lines if ln.strip()]) +if non_blank_count > max_lines: + has_fail = True + print(f"FAIL Adapter is not thin — {adapter_path}") + print(f" Why: {non_blank_count} non-blank lines exceeds the {max_lines}-line threshold for a thin adapter.") + print(" Fix: Move provider-agnostic content into AGENTS.md; keep only genuinely provider-specific additions here.") + print() + +if has_fail: + sys.exit(1) +sys.exit(0) +PYTHON diff --git a/plugins/core/skills/provider-adapter-author/tests/README.md b/plugins/core/skills/provider-adapter-author/tests/README.md new file mode 100644 index 0000000..d66b0f8 --- /dev/null +++ b/plugins/core/skills/provider-adapter-author/tests/README.md @@ -0,0 +1,28 @@ +# tests/ + +Test files for scripts bundled with this skill. + +## Dependencies + +Tests require [bats-support](https://github.com/bats-core/bats-support) and +[bats-assert](https://github.com/bats-core/bats-assert). The test files load +helpers from the repo root's `tests/test_helper/`. + +From the repo root: + +```bash +git clone https://github.com/bats-core/bats-support tests/test_helper/bats-support +git clone https://github.com/bats-core/bats-assert tests/test_helper/bats-assert +``` + +Run all tests for this skill (from the repo root): + +```bash +bats plugins/core/skills/provider-adapter-author/tests/ +``` + +## Files + +| File | Purpose | +|------|---------| +| `validate-adapter.bats` | Bats test suite for `scripts/validate-adapter.sh` | diff --git a/plugins/core/skills/provider-adapter-author/tests/validate-adapter.bats b/plugins/core/skills/provider-adapter-author/tests/validate-adapter.bats new file mode 100644 index 0000000..052d0d9 --- /dev/null +++ b/plugins/core/skills/provider-adapter-author/tests/validate-adapter.bats @@ -0,0 +1,128 @@ +#!/usr/bin/env bats + +setup() { + REPO_ROOT="$(cd "$BATS_TEST_DIRNAME/../../../../../" && pwd)" + load "$REPO_ROOT/tests/test_helper/bats-support/load" + load "$REPO_ROOT/tests/test_helper/bats-assert/load" + + SCRIPT="$(cd "$BATS_TEST_DIRNAME/../scripts" && pwd)/validate-adapter.sh" + TMPDIR="$(mktemp -d)" + AGENTS_MD="$TMPDIR/AGENTS.md" + cat > "$AGENTS_MD" <<'EOF' +# AGENTS.md + +## Setup commands +- Install deps: `pnpm install` +- Run tests: `pnpm test` + +## Code style +- TypeScript strict mode, single quotes, no semicolons. +EOF +} + +teardown() { + rm -rf "$TMPDIR" +} + +@test "fails when the adapter file is empty" { + ADAPTER="$TMPDIR/CLAUDE.md" + : > "$ADAPTER" + run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" + assert_failure + assert_output --partial "empty" +} + +@test "fails when the adapter has no reference to AGENTS.md" { + ADAPTER="$TMPDIR/CLAUDE.md" + cat > "$ADAPTER" <<'EOF' +# Claude-specific notes +Use the internal linter before committing. +EOF + run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" + assert_failure + assert_output --partial "no reference" +} + +@test "passes a thin adapter with an @import line and provider-specific additions" { + ADAPTER="$TMPDIR/CLAUDE.md" + cat > "$ADAPTER" <<'EOF' +@AGENTS.md +@core/instructions/governance.md +EOF + run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" + assert_success +} + +@test "fails when the adapter duplicates most of AGENTS.md's content" { + ADAPTER="$TMPDIR/CLAUDE.md" + cat > "$ADAPTER" <<'EOF' +@AGENTS.md + +## Setup commands +- Install deps: `pnpm install` +- Run tests: `pnpm test` + +## Code style +- TypeScript strict mode, single quotes, no semicolons. +EOF + run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" + assert_failure + assert_output --partial "duplicat" +} + +@test "fails when the adapter exceeds the max line threshold" { + ADAPTER="$TMPDIR/CLAUDE.md" + { + echo "@AGENTS.md" + for i in $(seq 1 80); do echo "Provider-specific line $i unrelated to AGENTS.md content."; done + } > "$ADAPTER" + run bash "$SCRIPT" "$ADAPTER" "$AGENTS_MD" + assert_failure + assert_output --partial "thin" +} + +@test "allows a custom --max-lines threshold" { + ADAPTER="$TMPDIR/CLAUDE.md" + { + echo "@AGENTS.md" + for i in $(seq 1 10); do echo "Provider-specific line $i unrelated to AGENTS.md content."; done + } > "$ADAPTER" + run bash "$SCRIPT" --max-lines 5 "$ADAPTER" "$AGENTS_MD" + assert_failure + assert_output --partial "thin" +} + +@test "with --no-import-syntax, a text pointer to AGENTS.md is accepted instead of an @import line" { + ADAPTER="$TMPDIR/copilot-instructions.md" + cat > "$ADAPTER" <<'EOF' +See AGENTS.md at the repo root for setup, style, and testing conventions. + +## Copilot-specific +Prefer inline suggestions over chat for one-line edits. +EOF + run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD" + assert_success +} + +@test "with --no-import-syntax, still fails if there is no mention of AGENTS.md at all" { + ADAPTER="$TMPDIR/copilot-instructions.md" + cat > "$ADAPTER" <<'EOF' +## Copilot-specific +Prefer inline suggestions over chat for one-line edits. +EOF + run bash "$SCRIPT" --no-import-syntax "$ADAPTER" "$AGENTS_MD" + assert_failure + assert_output --partial "no reference" +} + +@test "--help exits 0 and documents usage" { + run bash "$SCRIPT" --help + assert_success + assert_output --partial "Usage:" +} + +@test "fails with a clear error when the adapter file argument is missing" { + run bash "$SCRIPT" + assert_failure + assert_output --partial "required" +}