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
20 changes: 20 additions & 0 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "skill-feedback-engine",
"interface": {
"displayName": "Openly Useful"
},
"plugins": [
{
"name": "skill-feedback-engine",
"source": {
"source": "local",
"path": "./"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
26 changes: 26 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"$schema": "https://json.schemastore.org/claude-code-marketplace.json",
"name": "skill-feedback-engine",
"version": "0.1.0",
"description": "Openly Useful local-first, review-gated feedback for Agent Skills.",
"owner": {
"name": "Openly Useful",
"email": "hello@openlyuseful.org",
"url": "https://openlyuseful.org"
},
"plugins": [
{
"name": "skill-feedback-engine",
"source": "./",
"description": "Capture reusable corrections and prepare sanitized, review-gated Agent Skill proposals.",
"version": "0.1.0",
"author": {
"name": "Openly Useful",
"email": "hello@openlyuseful.org",
"url": "https://openlyuseful.org"
},
"category": "productivity",
"strict": true
}
]
}
14 changes: 14 additions & 0 deletions .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "skill-feedback-engine",
"version": "0.1.0",
"description": "Local-first, review-gated feedback for improving Agent Skills without exporting raw evidence.",
"author": {
"name": "Openly Useful",
"email": "hello@openlyuseful.org",
"url": "https://openlyuseful.org"
},
"homepage": "https://github.com/Openly-Useful/skill-feedback-engine",
"repository": "https://github.com/Openly-Useful/skill-feedback-engine",
"license": "MIT",
"skills": "./skills/"
}
39 changes: 39 additions & 0 deletions .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "skill-feedback-engine",
"version": "0.1.0",
"description": "Local-first, review-gated feedback for improving Agent Skills without exporting raw evidence.",
"author": {
"name": "Openly Useful",
"email": "hello@openlyuseful.org",
"url": "https://openlyuseful.org"
},
"homepage": "https://github.com/Openly-Useful/skill-feedback-engine",
"repository": "https://github.com/Openly-Useful/skill-feedback-engine",
"license": "MIT",
"keywords": [
"agent-skills",
"feedback",
"review",
"local-first"
],
"skills": "./skills/",
"interface": {
"displayName": "Skill Feedback Engine",
"shortDescription": "Turn recurring corrections into reviewed skill proposals",
"longDescription": "A local-first feedback loop that captures reusable corrections, failures, patterns, and outcomes; keeps raw evidence private; and prepares sanitized, review-gated proposals for improving Agent Skills.",
"developerName": "Openly Useful",
"category": "Productivity",
"capabilities": [
"Local feedback",
"Review-gated proposals"
],
"websiteURL": "https://github.com/Openly-Useful/skill-feedback-engine",
"privacyPolicyURL": "https://openlyuseful.org/legal/privacy",
"termsOfServiceURL": "https://openlyuseful.org/legal/terms",
"defaultPrompt": [
"Capture this correction as reusable skill feedback after the task.",
"Review recurring skill feedback and prepare draft proposals."
],
"brandColor": "#247A4B"
}
}
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ jobs:
- run: python -m pip install -e .
- run: python -m unittest discover -s tests -v
- run: skill-feedback validate skills/skill-feedback-engine
- run: python scripts/validate_registration.py
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,14 @@ Then add `adapters/codex/AGENTS.snippet.md` to the applicable persistent instruc

Provider adapters contain activation guidance only. They all use the same CLI, database, observation policy, and export format.

## Repository-local plugin registration

The repository root is a skill-only plugin for OpenAI/Codex and Claude. `.codex-plugin/plugin.json` and `.claude-plugin/plugin.json` both expose the existing canonical `./skills/` directory, so provider registration does not create wrapper copies of `SKILL.md`. Repository-local catalogs are at `.agents/plugins/marketplace.json` and `.claude-plugin/marketplace.json`.

Skill Feedback Engine has no MCP component, and none is declared by these manifests. The registration files prepare local discovery only; they do not install, authenticate, publish, or activate anything externally.

`publisher/publisher.json` derives Openly Useful identity and public policy URLs from <https://openlyuseful.org/publisher/manifest.json>. Openly Useful LLC remains a planned, formation-pending publisher/operator/licensee, and external publication authorization remains withheld.

## Daily review on macOS

`adapters/codex/com.openlyuseful.skill-feedback-engine.plist` is a launchd template for 4:00 AM in the machine's local timezone, including daylight-saving changes.
Expand All @@ -174,6 +182,7 @@ Replace `__SKILL_FEEDBACK_EXECUTABLE__` with the absolute path returned by `comm
python3 -m unittest discover -s tests -v
python3 -m skill_feedback_engine --help
python3 -m skill_feedback_engine validate skills/skill-feedback-engine
python3 scripts/validate_registration.py
```

When running directly from a checkout without installing it, set `PYTHONPATH=src` before the `python3 -m skill_feedback_engine` commands.
46 changes: 46 additions & 0 deletions publisher/publisher.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"schemaVersion": 1,
"authorityManifest": "https://openlyuseful.org/publisher/manifest.json",
"publisher": {
"displayName": "Openly Useful",
"homepage": "https://openlyuseful.org",
"studio": "https://openlyuseful.com",
"publicContact": "hello@openlyuseful.org"
},
"plannedLegalEntity": {
"name": "Openly Useful LLC",
"status": "formation-pending",
"roles": [
"publisher",
"operator",
"licensee"
]
},
"policies": {
"privacy": "https://openlyuseful.org/legal/privacy",
"terms": "https://openlyuseful.org/legal/terms",
"security": "https://openlyuseful.org/security",
"support": "https://openlyuseful.org/support"
},
"namespaces": {
"npm": "@openly-useful",
"openSourceMcp": "org.openlyuseful",
"reservedStudioMcp": "com.openlyuseful"
},
"component": {
"name": "skill-feedback-engine",
"displayName": "Skill Feedback Engine",
"version": "0.1.0",
"repository": "https://github.com/Openly-Useful/skill-feedback-engine",
"license": "MIT",
"skillsPath": "./skills/",
"skillNames": [
"skill-feedback-engine"
],
"mcp": false
},
"externalPublication": {
"allowed": false,
"authorization": "withheld"
}
}
142 changes: 142 additions & 0 deletions scripts/validate_registration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""Validate skill-only Codex/Claude registration against publisher metadata."""

from __future__ import annotations

import json
import re
from pathlib import Path
from typing import Any, Dict, List


ROOT = Path(__file__).resolve().parents[1]
SEMVER = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$")


def read_json(path: Path, errors: List[str]) -> Dict[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
errors.append(f"{path.relative_to(ROOT)} is missing or invalid JSON")
return {}
if not isinstance(value, dict):
errors.append(f"{path.relative_to(ROOT)} must contain a JSON object")
return {}
return value


def validate_registration(root: Path = ROOT) -> List[str]:
errors: List[str] = []
publisher = read_json(root / "publisher" / "publisher.json", errors)
if publisher.get("schemaVersion") != 1:
errors.append("publisher schemaVersion must be 1")
if publisher.get("authorityManifest") != "https://openlyuseful.org/publisher/manifest.json":
errors.append("publisher authority manifest is invalid")
identity = publisher.get("publisher")
expected_identity = {
"displayName": "Openly Useful",
"homepage": "https://openlyuseful.org",
"studio": "https://openlyuseful.com",
"publicContact": "hello@openlyuseful.org",
}
if identity != expected_identity:
errors.append("publisher identity does not match Openly Useful")
legal = publisher.get("plannedLegalEntity")
if not isinstance(legal, dict) or legal.get("name") != "Openly Useful LLC" or legal.get("status") != "formation-pending":
errors.append("planned entity must be Openly Useful LLC with formation-pending status")
elif sorted(legal.get("roles", [])) != ["licensee", "operator", "publisher"]:
errors.append("planned entity roles are invalid")
if publisher.get("externalPublication") != {"allowed": False, "authorization": "withheld"}:
errors.append("external publication must remain withheld")

component = publisher.get("component")
if not isinstance(component, dict):
errors.append("publisher component metadata is required")
return errors
if component.get("name") != "skill-feedback-engine" or not SEMVER.fullmatch(str(component.get("version", ""))):
errors.append("component identity/version is invalid")
if component.get("skillNames") != ["skill-feedback-engine"]:
errors.append("component must expose exactly one canonical skill")
if component.get("mcp") is not False:
errors.append("Skill Feedback Engine must explicitly remain MCP-free")

skill_artifacts = sorted(path.relative_to(root).as_posix() for path in (root / "skills").rglob("SKILL*.md"))
if skill_artifacts != ["skills/skill-feedback-engine/SKILL.md"]:
errors.append("duplicate or unexpected SKILL artifacts: " + ", ".join(skill_artifacts))

author = {
"name": expected_identity["displayName"],
"email": expected_identity["publicContact"],
"url": expected_identity["homepage"],
}
common = {
"name": component.get("name"),
"version": component.get("version"),
"author": author,
"homepage": component.get("repository"),
"repository": component.get("repository"),
"license": component.get("license"),
"skills": component.get("skillsPath"),
}
codex = read_json(root / ".codex-plugin" / "plugin.json", errors)
claude = read_json(root / ".claude-plugin" / "plugin.json", errors)
for label, manifest in (("Codex", codex), ("Claude", claude)):
for field, expected in common.items():
if manifest.get(field) != expected:
errors.append(f"{label} plugin {field} does not derive from publisher metadata")
if "mcpServers" in manifest or (root / ".mcp.json").exists():
errors.append(f"{label} skill-only registration cannot declare MCP")
interface = codex.get("interface")
if not isinstance(interface, dict):
errors.append("Codex interface metadata is required")
else:
policies = publisher.get("policies", {})
if interface.get("displayName") != component.get("displayName"):
errors.append("Codex display name mismatch")
if interface.get("developerName") != expected_identity["displayName"]:
errors.append("Codex developer name mismatch")
if interface.get("privacyPolicyURL") != policies.get("privacy"):
errors.append("Codex privacy URL mismatch")
if interface.get("termsOfServiceURL") != policies.get("terms"):
errors.append("Codex terms URL mismatch")

codex_marketplace = read_json(root / ".agents" / "plugins" / "marketplace.json", errors)
codex_entries = codex_marketplace.get("plugins")
if codex_marketplace.get("interface") != {"displayName": "Openly Useful"}:
errors.append("Codex marketplace publisher mismatch")
if not isinstance(codex_entries, list) or len(codex_entries) != 1:
errors.append("Codex marketplace must contain exactly one plugin")
else:
entry = codex_entries[0]
if entry.get("name") != component.get("name") or entry.get("source") != {"source": "local", "path": "./"}:
errors.append("Codex marketplace root source mismatch")
if entry.get("policy") != {"installation": "AVAILABLE", "authentication": "ON_INSTALL"}:
errors.append("Codex marketplace policy mismatch")

claude_marketplace = read_json(root / ".claude-plugin" / "marketplace.json", errors)
claude_entries = claude_marketplace.get("plugins")
if claude_marketplace.get("owner") != author or claude_marketplace.get("version") != component.get("version"):
errors.append("Claude marketplace publisher/version mismatch")
if not isinstance(claude_entries, list) or len(claude_entries) != 1:
errors.append("Claude marketplace must contain exactly one plugin")
else:
entry = claude_entries[0]
if entry.get("name") != component.get("name") or entry.get("source") != "./" or entry.get("strict") is not True:
errors.append("Claude marketplace root source mismatch")
if entry.get("author") != author or entry.get("version") != component.get("version"):
errors.append("Claude marketplace component metadata mismatch")
return errors


def main() -> int:
errors = validate_registration()
if errors:
for error in errors:
print(f"ERROR: {error}")
return 1
print("Validated canonical skill plus Codex, Claude, marketplace, and publisher registration")
return 0


if __name__ == "__main__":
raise SystemExit(main())
8 changes: 8 additions & 0 deletions src/skill_feedback_engine/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ def validate_skill(path: Path) -> Dict[str, Any]:
"errors": ["SKILL.md not found"],
"warnings": warnings,
}
if path.is_dir():
duplicate_artifacts = sorted(
candidate.name for candidate in path.glob("SKILL*.md") if candidate.name != "SKILL.md"
)
if duplicate_artifacts:
errors.append(
"duplicate or unexpected SKILL artifacts: " + ", ".join(duplicate_artifacts)
)
content = skill_file.read_text(encoding="utf-8")
if not content.startswith("---\n"):
errors.append("SKILL.md must start with YAML frontmatter")
Expand Down
Loading
Loading