Files
recoupable__skills/scripts/check_vendored.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

101 lines
3.7 KiB
Python

#!/usr/bin/env python3
"""Drift check for vendored (duplicated) files.
Portable skills are self-contained, so shared references/scripts are COPIED
into each skill rather than centralized. This check keeps every copy
byte-identical to its single canonical source, so vendoring can never silently
diverge.
Reads scripts/vendored.json. Two group kinds:
- file group: {"canonical": "<file>", "copies": ["<file>", ...]}
- dir group: {"canonical_dir": "<dir>", "copies_dirs": ["<dir>", ...]}
File groups assert byte-identity; dir groups assert the trees are identical
(same files, all byte-identical) via filecmp.dircmp.
Usage: python3 scripts/check_vendored.py
Exit 0 = all copies match canonical; 1 = drift or missing files. Stdlib only.
"""
from __future__ import annotations
import filecmp
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
MAP_FILE = REPO_ROOT / "scripts" / "vendored.json"
def main() -> int:
if not MAP_FILE.exists():
print(f"No {MAP_FILE.relative_to(REPO_ROOT)} yet — nothing vendored to check.")
return 0
try:
data = json.loads(MAP_FILE.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
print(f" ✗ scripts/vendored.json: invalid JSON: {exc}")
return 1
groups = data.get("groups", [])
problems: list[str] = []
copies_checked = 0
def tree_diff(a: Path, b: Path) -> list[str]:
"""Return human-readable differences between two directory trees."""
cmp = filecmp.dircmp(a, b)
diffs: list[str] = []
if cmp.left_only:
diffs.append(f"only in canonical: {sorted(cmp.left_only)}")
if cmp.right_only:
diffs.append(f"only in copy: {sorted(cmp.right_only)}")
if cmp.diff_files:
diffs.append(f"differing files: {sorted(cmp.diff_files)}")
for sub in cmp.common_dirs:
diffs.extend(f"{sub}/{d}" for d in tree_diff(a / sub, b / sub))
return diffs
for g in groups:
if "canonical_dir" in g:
canonical = REPO_ROOT / g["canonical_dir"]
if not canonical.is_dir():
problems.append(f"canonical dir missing: {g['canonical_dir']}")
continue
for copy_rel in g.get("copies_dirs", []):
copies_checked += 1
copy = REPO_ROOT / copy_rel
if not copy.is_dir():
problems.append(f"copy dir missing: {copy_rel} (canonical {g['canonical_dir']})")
continue
d = tree_diff(canonical, copy)
if d:
problems.append(f"DRIFT (tree): {copy_rel} != {g['canonical_dir']} -> {'; '.join(d)}")
continue
canonical = REPO_ROOT / g["canonical"]
if not canonical.exists():
problems.append(f"canonical missing: {g['canonical']}")
continue
for copy_rel in g.get("copies", []):
copies_checked += 1
copy = REPO_ROOT / copy_rel
if not copy.exists():
problems.append(f"copy missing: {copy_rel} (canonical {g['canonical']})")
continue
if not filecmp.cmp(canonical, copy, shallow=False):
problems.append(f"DRIFT: {copy_rel} != {g['canonical']}")
if problems:
print(f"VENDOR DRIFT CHECK FAILED — {len(problems)} problem(s):\n")
for p in problems:
print(f" ✗ {p}")
print("\nRe-sync copies from canonical, or update scripts/vendored.json.")
return 1
print(f"VENDOR DRIFT CHECK PASSED — {copies_checked} copy/copies match canonical across {len(groups)} group(s).")
return 0
if __name__ == "__main__":
raise SystemExit(main())