fix(plugin): align Codex manifest with validator schema

This commit is contained in:
Wyatt Fang
2026-08-18 09:06:24 +08:00
parent ec47e8e08d
commit 19c796de59
4 changed files with 119 additions and 2 deletions
+5
View File
@@ -1,5 +1,10 @@
# Changelog
## Unreleased
- Added complete Codex plugin publisher and interface metadata, with package checks that prevent
future manifests from passing without the fields required by generic plugin validation.
## 2.0.2
- Added the owner-gated `item_progression: parallel` policy to `cs-epic`: the main workflow stays the
+20 -1
View File
@@ -2,5 +2,24 @@
"name": "codestable",
"version": "2.0.2",
"description": "CodeStable AI coding workflow skills.",
"skills": "./skills/"
"author": {
"name": "CodeStable"
},
"skills": "./skills/",
"interface": {
"displayName": "CodeStable",
"shortDescription": "Stable engineering workflows for AI coding.",
"longDescription": "Workflow skills for scoped implementation, diagnosis, review, refactoring, epics, onboarding, and durable project learning.",
"developerName": "CodeStable",
"category": "development",
"capabilities": [
"Interactive",
"Write"
],
"defaultPrompt": [
"Use CodeStable to implement this change with scoped verification.",
"Diagnose and fix this issue with red-to-green evidence.",
"Review the current diff for concrete risks and missing tests."
]
}
}
+51 -1
View File
@@ -76,7 +76,22 @@ def make_repo(tmp_path: Path) -> Path:
)
write_json(
repo / "plugins/codestable/.codex-plugin/plugin.json",
{"name": "codestable", "version": "0.1.0", "skills": "./skills/"},
{
"name": "codestable",
"version": "0.1.0",
"description": "CodeStable AI coding workflow skills.",
"author": {"name": "CodeStable"},
"skills": "./skills/",
"interface": {
"displayName": "CodeStable",
"shortDescription": "Stable engineering workflows for AI coding.",
"longDescription": "Workflow skills for scoped AI-assisted software development.",
"developerName": "CodeStable",
"category": "development",
"capabilities": ["Interactive", "Write"],
"defaultPrompt": ["Use CodeStable to handle this software task."],
},
},
)
write_json(
repo / "plugins/codestable/.claude-plugin/plugin.json",
@@ -196,6 +211,41 @@ def test_invalid_manifest_contract_fails(tmp_path: Path) -> None:
assert any("skills must equal './skills/'" in message for message in messages(findings))
def test_codex_manifest_requires_ingestion_metadata(tmp_path: Path) -> None:
repo = make_repo(tmp_path)
path = repo / "plugins/codestable/.codex-plugin/plugin.json"
manifest = json.loads(path.read_text(encoding="utf-8"))
manifest.pop("author")
manifest.pop("interface")
write_json(path, manifest)
finding_messages = messages(checker.check_repo(repo))
assert "author.name must equal 'CodeStable'" in finding_messages
assert "interface is required" in finding_messages
def test_codex_manifest_requires_complete_interface_metadata(tmp_path: Path) -> None:
repo = make_repo(tmp_path)
path = repo / "plugins/codestable/.codex-plugin/plugin.json"
manifest = json.loads(path.read_text(encoding="utf-8"))
manifest["interface"] = {}
write_json(path, manifest)
finding_messages = messages(checker.check_repo(repo))
for field in (
"displayName",
"shortDescription",
"longDescription",
"developerName",
"category",
):
assert f"interface.{field} is required" in finding_messages
assert "interface.capabilities must be an array of strings" in finding_messages
assert "interface.defaultPrompt must be an array of strings" in finding_messages
def test_codex_catalog_contract_fails(tmp_path: Path) -> None:
repo = make_repo(tmp_path)
write_json(
+43
View File
@@ -125,6 +125,7 @@ def check_catalog_contracts(root: Path, findings: list[Finding]) -> None:
(".claude-plugin/marketplace.json", ["description"], "CodeStable AI coding workflow skills."),
(".claude-plugin/marketplace.json", ["plugins", 0, "source"], "./plugins/codestable"),
("plugins/codestable/.claude-plugin/plugin.json", ["author", "name"], "CodeStable"),
("plugins/codestable/.codex-plugin/plugin.json", ["author", "name"], "CodeStable"),
("plugins/codestable/.codex-plugin/plugin.json", ["skills"], "./skills/"),
]
cache: dict[str, Any | None] = {}
@@ -150,6 +151,48 @@ def check_catalog_contracts(root: Path, findings: list[Finding]) -> None:
if not nested(codex_marketplace, ["plugins", 0, "interface", "displayName"]):
findings.append(Finding(".agents/plugins/marketplace.json", "plugins.0.interface.displayName is required"))
codex_manifest = cache.get("plugins/codestable/.codex-plugin/plugin.json")
if codex_manifest is not None:
interface = nested(codex_manifest, ["interface"])
if not isinstance(interface, dict):
findings.append(Finding("plugins/codestable/.codex-plugin/plugin.json", "interface is required"))
else:
for field in (
"displayName",
"shortDescription",
"longDescription",
"developerName",
"category",
):
value = interface.get(field)
if not isinstance(value, str) or not value.strip():
findings.append(
Finding(
"plugins/codestable/.codex-plugin/plugin.json",
f"interface.{field} is required",
)
)
capabilities = interface.get("capabilities")
if not isinstance(capabilities, list) or not all(
isinstance(value, str) and value.strip() for value in capabilities
):
findings.append(
Finding(
"plugins/codestable/.codex-plugin/plugin.json",
"interface.capabilities must be an array of strings",
)
)
default_prompt = interface.get("defaultPrompt")
if not isinstance(default_prompt, list) or not all(
isinstance(value, str) and value.strip() for value in default_prompt
):
findings.append(
Finding(
"plugins/codestable/.codex-plugin/plugin.json",
"interface.defaultPrompt must be an array of strings",
)
)
def check_skill_layout(root: Path, findings: list[Finding]) -> None:
skills_dir = root / "plugins/codestable/skills"