Files
recoupable__skills/scripts/validate_manifests.py
Sidney Swift aaf9c06c42 Unify skills, plugins, and marketplace into one cross-harness source of truth (#33)
* feat: consolidate skills + plugins into single recoupable/skills repo

Unify the open skills library, rich plugins, and the marketplace registry
into one repo to kill the drift between recoupable/marketplace (live) and
recoupable/skills (stale), and the Claude/Codex registry mismatches.

Structure:
- skills/                     generic, install-anywhere library skills
- plugins/{research,catalog,content}/   rich plugins (commands, agents, evals, refs)
- .claude-plugin/marketplace.json + .agents/plugins/marketplace.json
  unified registries listing the library (source ".") + 3 plugins

Content reconciliation (single-source the duplicated short-video):
- Promote the newer short-video to the library and bundle its references
  (short-video-manual.md, song-sourcing.md) inside skills/short-video/
  so the library skill is self-contained.
- Remove the duplicated short-video + content-creation from plugins/content.
- Rewrite recoup-content-create to inline the async pipeline and reference
  library skills (short-video, content-creation, create-artist) by name,
  decoupling it from in-tree relative paths.
- Refresh plugins/content README + legacy command to drop dead in-tree refs.

Validated e2e: all manifest sources resolve, every SKILL.md has valid
frontmatter, no duplicate skill names (42 distinct), JSON parses clean.

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: make all 42 skills cross-harness portable (self-contained)

Reject the Claude-only ${CLAUDE_PLUGIN_ROOT} pattern (verified: doesn't
expand in markdown bodies, runtime CWD is the user project) in favor of
the compound-engineering self-containment model so every skill runs on
any harness (Claude Code, Codex, Cursor, bare npx skills), not just as a
Claude marketplace plugin.

- Portable Skill Contract added to AGENTS.md + contributing.md
- scripts/: portability_lint, check_vendored (file + recursive-dir drift),
  validate_manifests (+ dual-manifest parity), vendored.json (27 groups)
- research (10): vendor the used shared refs per skill; md-links -> backtick
- catalog (17): vendor plugin-root refs + deal-workspace template +
  demo-data-room fixtures + full 17-script set into 8 script-bearing skills;
  kill cross-skill dep (royalty-audit<-catalog-analysis); rephrase prose
- library (14) + content (1): fix mir links, de-path cross-skill prose,
  replace dead content-strategy.md ref with inline guidance
- ci: .github/workflows/validate.yml runs lint + drift + manifests

Verified: lint 42/42 portable, drift 174 copies/27 groups, manifests
13 files + parity OK. Vendored catalog scripts execute from skill dir.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: vendor catalog scripts as per-skill dependency closures

The first portability pass copied the full 17-script catalog toolkit into
all 8 script-bearing skills (136 copies). Most skills invoke far fewer:
dashboard 1, kickoff 1, report 2. Replace the superset with each skill's
true closure — scripts named in SKILL.md plus their transitive deps:
run-deal-checks's 5 subprocess validators, auto-column-map's importlib
normalize target, and the shared _helpers import.

Drops 80 dead copies (174 -> 94 tracked). Verified: lint 42/42, drift
94/27 green, and the dynamic subprocess/importlib chains execute from
each tightened skill dir.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(deals): fold 14 deal skills into 6 workflow stages

Collapse the deal skills into start / ingest / analysis / dashboard /
report / demo so the slash menu reads as a natural workflow. Most users
only run the orchestrator, so granular skills became modes, params, and
bundled reference playbooks. Nothing dropped.

- start: absorbs workspace-setup + carries deal-type modes
  (buy-side / seller-prep / financing / post-close)
- analysis: absorbs rights-reviewer, royalties-auditor,
  financing-underwriter, seller-prep, post-close-admin as references/*.md
- dashboard: absorbs quality-checker pre-share QC gate
- report: absorbs memo-maker (memo assembly + packages)
- vendored.json slug-remapped + resynced; hooks.json Gate B fixed;
  plugin README rewritten to the true 6

Gates green: portability_lint (31 portable), check_vendored (85 copies /
25 groups), validate_manifests (13 files + parity). Repo-wide sweep:
zero lingering old deal slugs.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address cubic review findings on deal validators + docs

Resolves 9 review comments from cubic-dev-ai on PR #33.

P1:
- protect-source-files.sh: also match relative `deals/*/source/*` paths
  (Write/Edit tools pass repo-relative paths; the leading-segment glob alone
  missed them, leaving immutable source files writable).
- validate-evidence-ledger.py: guard non-dict top-level JSON (array/primitive
  no longer crashes with AttributeError; emits a clean error).
- deal-workspace.md: workpapers list now `nps-bridge.json` + `nls-bridge.json`
  (two files, matching the dashboard validator) instead of a combined file.

P2:
- validate-deal-workspace.py: catch OSError/UnicodeDecodeError, not just
  JSONDecodeError, so unreadable/non-UTF8 files report cleanly.
- validate-normalized-ledger.py: report empty `ledger_line_id` values as a
  data-integrity error instead of silently accepting them.
- validate-findings-evidence.py: handle malformed JSON in both loaders;
  emit a structured `invalid_findings_file` result instead of a traceback.
- content plugin manifests: author email -> agent@recoupable.com to match
  marketplace.json + branding.
- deal-workspace.md: `valuation-scenarios.md` -> `valuation-summary.json`
  (matches the workspace + dashboard validators).

P3:
- AGENTS.md: align Skill Format (templates/, fixtures/) with the Portable
  Contract; drop the contradictory assets/ reference.

Vendored copies resynced (20). Gates green: portability 31, vendored 85, manifests 13.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: rename root plugin recoupable-skills -> recoup-skills

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: drop -plugin suffix from plugin names (recoup-deals/research/content)

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: rebrand root library to "Recoup Skills" + add CLAUDE.md symlink

- README.md / AGENTS.md titles: "Recoupable Skills" -> "Recoup Skills"
- displayName fields (marketplace + codex manifest) -> "Recoup Skills"
- README skill-dir tree: assets/ -> templates/ + fixtures/ (matches AGENTS.md contract)
- CLAUDE.md -> AGENTS.md symlink so Claude Code reads the same instructions

Co-authored-by: Cursor <cursoragent@cursor.com>

* docs: add Plugins section to README; drop ' Plugin' from research/content titles

Co-authored-by: Cursor <cursoragent@cursor.com>

* feat: add recoup-song-* skills for Music Flamingo audio analysis

Add a P0 router skill (recoup-song-analyzer) plus five focused P1
workflow skills built on the Music Flamingo /api/songs/analyze endpoint:

- recoup-song-analyzer: entry point — resolves audio URL, picks preset,
  routes to focused workflows
- recoup-song-metadata-tagger: structured catalog metadata
- recoup-song-lyrics-transcriber: sectioned lyric sheet (draft)
- recoup-song-playlist-pitch-maker: DSP/editorial pitch + positioning
- recoup-song-sync-brief-maker: supervisor sync brief + clearance risk
- recoup-song-mix-feedback-reviewer: technical mix critique

Canonical endpoint reference lives in recoup-song-analyzer and is
vendored byte-identical into the other five (registered in
scripts/vendored.json). README skill table updated. All three gates
pass (portability, vendor drift, manifests).

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: promote recoup-song-* skills into recoup-songs plugin

Convert the six loose recoup-song-* skills into a cohesive recoup-songs
plugin, matching the structure of recoup-deals/research/content.

- git mv skills/recoup-song-* -> plugins/songs/skills/ (history preserved)
- promote canonical flamingo-api.md to plugins/songs/references/ and vendor
  it byte-identical into all 6 skills (analyzer included) — fixes the prior
  smell where recoup-song-analyzer owned the reference the others depended on
- add plugin manifests (.claude-plugin + .cursor-plugin), README, LICENSE
- register recoup-songs in both marketplace files (.claude-plugin + .agents)
- root README: drop the analyzer skills-table row, add Recoup Songs to the
  Plugins table

All 3 gates pass: portability (37), vendor drift (91 copies / 26 groups),
manifests (15 files + dual-manifest parity).

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: shorten recoup-song-* P1 skill names (drop action suffixes)

Drop the action-noun suffix from the five P1 skills so the names read as
the thing they produce, not the actor:

- recoup-song-metadata-tagger      -> recoup-song-metadata
- recoup-song-lyrics-transcriber   -> recoup-song-lyrics
- recoup-song-playlist-pitch-maker -> recoup-song-playlist-pitch
- recoup-song-sync-brief-maker     -> recoup-song-sync-brief
- recoup-song-mix-feedback-reviewer-> recoup-song-mix-feedback

git mv preserves history. Updated SKILL.md name frontmatter, analyzer
decision-tree handoffs, sibling cross-references, plugin README skill
table, and scripts/vendored.json paths. recoup-song-analyzer (router)
unchanged. All 3 gates pass.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: address cubic review comments on PR #33

- validate-findings-evidence.py: a present-but-malformed evidence-ledger.json
  is now a hard error (invalid_ledger_file) instead of being silently
  downgraded to an empty set, which could report status: ok without
  validating any evidence references. Missing ledger stays tolerable.
- validate-normalized-ledger.py: only inspect ledger_line_id values when the
  column exists, so empty strings no longer leak into the dedup sets and
  produce a confusing "duplicate empty id" error on top of the real
  missing-column error.
- Standardize all plugin author emails on agent@recoupable.com (documented
  support email) across marketplace + plugin.json manifests for consistency.
- Re-vendored 9 byte-identical script copies; all 3 gates green.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 03:10:19 -04:00

124 lines
4.5 KiB
Python

#!/usr/bin/env python3
"""Validate plugin/marketplace manifests + connector configs.
Checks:
- every marketplace.json / plugin.json / .mcp.json is valid JSON
(catches the invalid-JSON class of bug, e.g. a missing comma in .mcp.json);
- marketplace plugins have name + source + semver version, source dir exists,
names are unique;
- dual-manifest PARITY: .claude-plugin/marketplace.json and
.agents/plugins/marketplace.json list the same plugins (name/source/version);
the .agents copy may add an `interface{}` block, which is ignored.
Usage: python3 scripts/validate_manifests.py
Exit 0 = valid; 1 = problems (printed). Standard library only.
"""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
SEMVER = re.compile(r"^\d+\.\d+\.\d+([-+].+)?$")
CLAUDE_MARKET = REPO_ROOT / ".claude-plugin" / "marketplace.json"
AGENTS_MARKET = REPO_ROOT / ".agents" / "plugins" / "marketplace.json"
def load_json(path: Path) -> tuple[dict | list | None, str | None]:
try:
return json.loads(path.read_text(encoding="utf-8")), None
except json.JSONDecodeError as exc:
return None, f"invalid JSON: {exc}"
except OSError as exc:
return None, f"cannot read: {exc}"
def discover(name: str) -> list[Path]:
return sorted(p for p in REPO_ROOT.rglob(name) if "node_modules" not in p.parts)
def validate_marketplace(path: Path, problems: list[str]) -> list[dict]:
data, err = load_json(path)
rel = path.relative_to(REPO_ROOT)
if err:
problems.append(f"{rel}: {err}")
return []
if not isinstance(data, dict) or not isinstance(data.get("plugins"), list):
problems.append(f"{rel}: missing top-level 'plugins' array")
return []
plugins = data["plugins"]
seen_names: set[str] = set()
for i, plugin in enumerate(plugins):
loc = f"{rel} plugins[{i}]"
for field in ("name", "source", "version"):
if field not in plugin:
problems.append(f"{loc}: missing required field '{field}'")
name = plugin.get("name", "")
if name in seen_names:
problems.append(f"{loc}: duplicate plugin name '{name}'")
seen_names.add(name)
version = plugin.get("version", "")
if version and not SEMVER.match(str(version)):
problems.append(f"{loc}: version '{version}' is not semver")
source = plugin.get("source")
if source:
src_dir = (REPO_ROOT / source).resolve()
if not src_dir.is_dir():
problems.append(f"{loc}: source '{source}' is not a directory")
return plugins
def parity(claude: list[dict], agents: list[dict], problems: list[str]) -> None:
def key_set(plugins: list[dict]) -> set[tuple[str, str, str]]:
return {(p.get("name", ""), p.get("source", ""), str(p.get("version", ""))) for p in plugins}
c, a = key_set(claude), key_set(agents)
only_claude = c - a
only_agents = a - c
for name, source, version in sorted(only_claude):
problems.append(f"PARITY: in .claude-plugin only: {name} ({source} @ {version})")
for name, source, version in sorted(only_agents):
problems.append(f"PARITY: in .agents only: {name} ({source} @ {version})")
def main() -> int:
problems: list[str] = []
# 1. JSON validity for every manifest/connector file.
checked = 0
for fname in ("marketplace.json", "plugin.json", ".mcp.json"):
for path in discover(fname):
checked += 1
_, err = load_json(path)
if err:
problems.append(f"{path.relative_to(REPO_ROOT)}: {err}")
# 2. Marketplace structure.
claude_plugins = validate_marketplace(CLAUDE_MARKET, problems) if CLAUDE_MARKET.exists() else []
if not CLAUDE_MARKET.exists():
problems.append(f"missing {CLAUDE_MARKET.relative_to(REPO_ROOT)}")
agents_plugins = validate_marketplace(AGENTS_MARKET, problems) if AGENTS_MARKET.exists() else []
if not AGENTS_MARKET.exists():
problems.append(f"missing {AGENTS_MARKET.relative_to(REPO_ROOT)}")
# 3. Dual-manifest parity.
if claude_plugins and agents_plugins:
parity(claude_plugins, agents_plugins, problems)
if problems:
print(f"MANIFEST VALIDATION FAILED — {len(problems)} problem(s):\n")
for p in problems:
print(f" ✗ {p}")
return 1
print(f"MANIFEST VALIDATION PASSED — {checked} manifest/connector file(s) valid; dual-manifest parity OK.")
return 0
if __name__ == "__main__":
raise SystemExit(main())