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:
2026-08-12 18:09:37 +00:00
parent 50d5c30a3c
commit 5e232503c4
289 changed files with 741 additions and 1974 deletions

View File

@@ -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.

View File

@@ -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