fix(kyberforge): bridge apm content to Claude Code's flat plugin discovery
Claude Code's (and Copilot's) native plugin installer has zero awareness of .apm/ nesting -- it convention-scans only flat skills/, agents/, commands/, hooks.json at each plugin's root. Confirmed via strings on the installed claude binary and live installs of git@holocron/gitea@holocron/kyberforge@ holocron, all reporting Skills(0) Agents(0) Hooks(0) post ADR-0015's apm conversion. Root cause (apm_cli/core/plugin_manifest.py): apm's plugin.json compiler deliberately strips skills/agents/commands keys, assuming the host already auto-discovers those convention directories -- it has no model of .apm/ being host-visible at all. Separately, apm's own bundle exporter (apm_cli/bundle/plugin_exporter.py, behind `apm pack --format plugin`) implements the correct .apm/ -> flat mapping, but only ever targeted build/<name>-<version>/, a path nothing in marketplace.json's source: points at. scripts/sync-plugin-content.sh wraps that bundle exporter and copies its agents/, skills/, commands/, instructions/, extensions/, and merged hooks.json back into each plugin's own root as a second tracked compiled-output category -- same governance status as .claude-plugin/plugin.json: generated from .apm/, never hand-edited. tests/ subdirectories are excluded from the mirror (dev fixtures, not host-visible runtime content; several hardcode a relative repo-root walk-up sized for the .apm/-nested depth, which breaks when duplicated one level shallower). Applied for real across all 6 plugins and verified two ways: `claude plugin validate --strict` passes on every real plugin directory, and a live `claude --plugin-dir <path> -p "list skills/agents"` behavioral test confirms content is now actually discovered. Also, from the same issue #90 review round: - scripts/check-manifests.sh pointed at each plugin's root-level plugin.json (checking skills/hooks/mcpServers/agents pointer fields) -- that file was a stale near-duplicate of .claude-plugin/plugin.json nothing else read or wrote, now deleted across all 6 plugins. check-manifests.sh is rewritten to validate .claude-plugin/plugin.json instead, and drops the pointer-field checks entirely (nothing to check -- those fields are correctly absent by design). Content-presence drift is now check-plugin-content-sync's job, a new pre-push hook wired in .pre-commit-config.yaml. docs/adr/0017 records the root cause and decision in full, including two rejected alternatives (patching plugin.json's path fields directly -- apm's compiler strips them on every run; pointing marketplace.json at apm pack's build/ output -- a version-suffixed non-source directory nothing can install from without an extra build step). ADR-0015 and CONTEXT.md are updated to point at it. Refs: #90
This commit is contained in:
30
plugins/core/skills/provider-adapter-author/README.md
Normal file
30
plugins/core/skills/provider-adapter-author/README.md
Normal file
@@ -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` |
|
||||
54
plugins/core/skills/provider-adapter-author/SKILL.md
Normal file
54
plugins/core/skills/provider-adapter-author/SKILL.md
Normal file
@@ -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] <adapter-file> <agents-md-file>
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -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`
|
||||
@@ -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 `<adapter-file> <agents-md-file>`, with optional `--no-import-syntax` and `--max-lines N` flags. Prints `FAIL` findings to stdout and exits non-zero on any failure.
|
||||
141
plugins/core/skills/provider-adapter-author/scripts/validate-adapter.sh
Executable file
141
plugins/core/skills/provider-adapter-author/scripts/validate-adapter.sh
Executable file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: validate-adapter.sh [--no-import-syntax] [--max-lines N] <adapter-file> <agents-md-file>
|
||||
|
||||
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
|
||||
Reference in New Issue
Block a user