feat(kyberforge): execute plugin-to-apm marketplace conversion
Why: ADR-0015 established that Microsoft APM (apm.yml + .apm/) should replace this repo's hand-authored plugin.json/marketplace.json model, with those files becoming compiled output of `apm pack` instead of files edited by hand via the (now-retired) plugin-author/marketplace-author skills. Issue #90 was the deferred execution of that decision, gated on #88 (apm tooling) and #89 (apm-native agent-author/skill-author routing). Implementation notes: - All six plugins (bin, core, git, gitea, kyberforge, lint) now carry apm.yml + .apm/{skills,agents,hooks} as their authoring source. Skills moved with a plain git mv (content-identical across targets). Agents were re-authored, not moved: per ADR-0016, .apm/agents/*.agent.md compiles verbatim to both Claude and Copilot, so plugin-scope agents now carry only name/description/model/source_keys -- no tools: field, no Claude-only knobs (isolation, maxTurns, effort, memory, permissionMode). - Root apm.yml registers all 7 marketplace packages (6 local plus mattpocock-skills as a remote entry) under versioning: per_package, matching this repo's existing independent-plugin-versioning practice. - .claude-plugin/marketplace.json and every plugin's plugin.json are now apm-pack-compiled output, verified against the prior hand-maintained content: same names/descriptions/versions/licenses/authors, only cosmetic serialization differences (JSON key order, owner email vs. url, Unicode escaping). - plugin-author and marketplace-author are retired now that apm-based authoring fully replaces their job; kyberforge bumped 1.3.1 -> 1.4.0 for that removal, and the root marketplace catalog bumped 0.3.1 -> 0.3.2 to match, per the version-bump convention now documented in apm-workflow's reference docs instead of a dedicated script (apm has no native version-bump automation). - Fixed hardcoded pre-.apm/ path assumptions across .pre-commit-config.yaml, .pre-commit-hooks.yaml, scripts/check-scope-walkup-sync.sh, scripts/sync-vale-styles.sh, scripts/check-vale-style-sync.sh, six plugins' root plugin.json (stale skills/hooks/agents pointer fields that check-manifests.sh validates), and several tests/*.bats and tests/*.sh fixtures -- including a bats REPO_ROOT relative-path depth bug (10 files, one extra .apm/ directory level to walk up) and a vale probe-path isolation regression introduced mid-fix. - Corrected empirically-wrong assumptions surfaced this session in apm-workflow/apm-install's own reference docs: `apm marketplace package add` does not accept local paths (only owner/repo remote shorthand -- local packages are registered by editing apm.yml's marketplace.packages[] directly); `apm compile` is a consumer-side AGENTS.md/CLAUDE.md generator, not the plugin.json producer, and hard-fails on skill/agent-only packages without --clean; `apm plugin init <name>` nests a stray subdirectory when run with a positional name arg from inside a same-named directory; no native Copilot marketplace output profile exists; .mcp.json is merged into the compiled plugin.json content-aware and target-scoped, with no dependencies.mcp entry needed for simple passthrough; pipx is the correct pip fallback on externally-managed Python environments. - Renamed agent-author's copilot.agent.md template asset to copilot.agent.md.template so apm compile's recursive *.agent.md glob stops misparsing the placeholder template as a real agent primitive. Impact: plugin.json and marketplace.json are compiled artifacts from here on -- editing them by hand is no longer the workflow; edit apm.yml/.apm/ and run apm pack. CONTEXT.md's Plugin/Plugin marketplace glossary entries reflect this. ADR-0001 is marked superseded, ADR-0006 moot, and ADR-0010 updated for the new .apm/agents/ path (project/user scope unaffected, per ADR-0016). Full local verification: claude plugin validate --strict on all 6 plugins, apm audit --ci, apm marketplace check, check-manifests.sh, and the full test suite (165/165 bats, 13/13 shell scripts) all pass clean. Fixes: #90 Refs: #88, #89 ADR: 0015 ADR: 0016 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ub96PyaSRD9BHPktotj1pC
This commit is contained in:
@@ -1,9 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,141 +0,0 @@
|
||||
#!/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