Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"source": "git-subdir",
"url": "https://github.com/underpass-ai/kmp.git",
"path": "plugins/kmp",
"ref": "af452279507d8575d308ffdb9ab7ee01205b9c60"
"ref": "v0.5.0"
}
},
{
Expand Down
15 changes: 9 additions & 6 deletions .github/workflows/kmp-contract.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,15 @@ jobs:
shell: bash
run: echo "ref=$(python3 scripts/ci/kmp-contract.py --print-source-ref)" >> "${GITHUB_OUTPUT}"

- name: Checkout immutable KMP source
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
repository: underpass-ai/kmp
ref: ${{ steps.source.outputs.ref }}
path: kmp-source
# This is the operation Claude Code's git-subdir installer performs.
# Keeping it literal prevents a commit SHA (fetchable by actions/checkout
# but invalid for git clone --branch) from passing the marketplace gate.
- name: Clone the KMP source exactly as Claude Code does
run: >-
git clone --depth 1
--branch "${{ steps.source.outputs.ref }}"
https://github.com/underpass-ai/kmp.git
kmp-source

- name: Verify catalogs, copy and exact plugin tree
run: >-
Expand Down
72 changes: 66 additions & 6 deletions scripts/ci/kmp-contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import json
import pathlib
import re
import subprocess
import sys


Expand All @@ -21,6 +22,8 @@
"path": "plugins/kmp",
}
EXPECTED_CODEX_SOURCE = {"source": "local", "path": "./plugins/kmp"}
KMP_REMOTE = "https://github.com/underpass-ai/kmp.git"
COMMIT_SHA = re.compile(r"[0-9a-f]{40}")
RETIRED_COUNT = re.compile(
r"\b(?:ten|10)(?:\s+kmp)?\s+(?:mcp\s+)?(?:moves|tools)\b",
re.I,
Expand Down Expand Up @@ -59,7 +62,50 @@ def kmp_entry(path: pathlib.Path) -> dict[str, object]:
return matches[0]


def claude_source() -> tuple[dict[str, object], str]:
def mirrored_version() -> str:
versions = set()
for relative in (".claude-plugin/plugin.json", ".codex-plugin/plugin.json"):
version = read_json(PLUGIN / relative).get("version")
if not isinstance(version, str) or not version:
raise SystemExit(f"{display_path(PLUGIN / relative)} has no version")
versions.add(version.split("+", 1)[0])
if len(versions) != 1:
raise SystemExit(f"mirrored KMP host manifests disagree on version: {sorted(versions)}")
return versions.pop()


def annotated_tag_commit(ref: str) -> str:
result = subprocess.run(
[
"git",
"ls-remote",
"--tags",
KMP_REMOTE,
f"refs/tags/{ref}",
f"refs/tags/{ref}^{{}}",
],
check=False,
capture_output=True,
text=True,
)
if result.returncode != 0:
detail = result.stderr.strip() or "tag lookup failed"
raise SystemExit(f"could not resolve KMP source tag {ref}: {detail}")
refs = {}
for line in result.stdout.splitlines():
fields = line.split()
if len(fields) == 2 and COMMIT_SHA.fullmatch(fields[0]):
refs[fields[1]] = fields[0]
tag_ref = f"refs/tags/{ref}"
if tag_ref not in refs:
raise SystemExit(f"KMP source tag {ref} does not exist")
peeled = refs.get(f"{tag_ref}^{{}}")
if peeled is None:
raise SystemExit(f"KMP source tag {ref} must be annotated")
return peeled


def claude_source() -> tuple[dict[str, object], str, str]:
entry = kmp_entry(CLAUDE_LISTING)
source = entry.get("source")
if not isinstance(source, dict):
Expand All @@ -68,10 +114,11 @@ def claude_source() -> tuple[dict[str, object], str]:
if stable != EXPECTED_CLAUDE_SOURCE:
raise SystemExit("Claude kmp entry no longer resolves underpass-ai/kmp/plugins/kmp")
ref = source.get("ref")
if not isinstance(ref, str) or not re.fullmatch(r"[0-9a-f]{40}", ref):
raise SystemExit("Claude kmp source must pin an immutable 40-character commit SHA")
expected_ref = f"v{mirrored_version()}"
if ref != expected_ref:
raise SystemExit(f"Claude kmp source must pin clonable immutable release tag {expected_ref}")
verify_description("Claude marketplace entry", entry.get("description"))
return entry, ref
return entry, ref, annotated_tag_commit(ref)


def verify_description(label: str, value: object) -> None:
Expand Down Expand Up @@ -113,13 +160,26 @@ def verify_tree(source_root: pathlib.Path) -> None:


def verify_contract(source_root: pathlib.Path) -> None:
_, ref = claude_source()
_, ref, expected_commit = claude_source()
codex = kmp_entry(CODEX_LISTING)
if codex.get("source") != EXPECTED_CODEX_SOURCE:
raise SystemExit("Codex kmp entry no longer resolves the reviewed plugins/kmp snapshot")

verify_tree(source_root)

checked_out_commit = subprocess.run(
["git", "rev-parse", "HEAD"],
cwd=source_root,
check=False,
capture_output=True,
text=True,
).stdout.strip()
if checked_out_commit != expected_commit:
raise SystemExit(
f"Claude clone checked out {checked_out_commit or 'no commit'}, "
f"not annotated tag commit {expected_commit}"
)

versions = set()
for root in (PLUGIN, source_root):
for relative in (".claude-plugin/plugin.json", ".codex-plugin/plugin.json"):
Expand Down Expand Up @@ -159,7 +219,7 @@ def main() -> None:
parser.add_argument("--source-root", type=pathlib.Path)
parser.add_argument("--print-source-ref", action="store_true")
args = parser.parse_args()
_, ref = claude_source()
_, ref, _ = claude_source()
if args.print_source_ref:
print(ref)
return
Expand Down