Skip to content

feat(marketplace): add unified marketplace machinery (2/3)#25

Open
sidneyswift wants to merge 1 commit into
mainfrom
feat/marketplace-machinery
Open

feat(marketplace): add unified marketplace machinery (2/3)#25
sidneyswift wants to merge 1 commit into
mainfrom
feat/marketplace-machinery

Conversation

@sidneyswift

@sidneyswift sidneyswift commented May 12, 2026

Copy link
Copy Markdown
Contributor

The problem

Recoup ships AI-agent skills and plugins, but they live in 3 separate GitHub repos. Customers have to install 3 different marketplaces. Each marketplace has 3 platform-specific manifest files (Claude Code, Codex, Cursor) — 9 files total, all hand-maintained, all drifting out of sync.

The fix

One source of truth: marketplace.source.json. A generator script produces the 3 platform marketplace files. CI catches drift on every PR.

After this lands, customers add ONE marketplace and install whichever plugins they want:

/plugin marketplace add recoupable/skills
/plugin install recoup-skills@recoup

This PR ships only the machinery + the existing 12 broad music skills as a virtual plugin. Vertical plugins (catalog) ship in #27.

Files (12)

  • marketplace.source.json — single source of truth (edit this, regenerate)
  • scripts/generate-marketplaces.py — produces the 3 platform manifests from source
  • scripts/validate-manifests.py — checks parity, paths, plugin manifests, SKILL.md frontmatter
  • .github/workflows/validate.yml — runs validator on every PR
  • .claude-plugin/marketplace.json, .agents/plugins/marketplace.json, .cursor-plugin/marketplace.jsongenerated, don't edit
  • AGENTS.md, README.md, contributing.md — describe the new pattern

What you should focus on

  1. marketplace.source.json — does the schema look right? Are the fields what we want a plugin to declare?
  2. scripts/generate-marketplaces.py — is the generation logic obvious enough that the next person can extend it?
  3. The 3 generated marketplace.json files — don't read line-by-line. Trust --check.

Test plan

  • python3 scripts/generate-marketplaces.py --check exits 0
  • python3 scripts/validate-manifests.py exits 0
  • Terminal screenshot of both validators passing
  • CI workflow run passing
  • 60-sec Loom: edit source.json → run generator → all 3 platform files update

Merge order

PR 2 of 3. Independent of #29 (which can land before or after this). #27 is stacked on this — must land before #27.

Merge preference

Please merge yourself when satisfied — I'd rather you push the button than approve.

🤖 Made with Cursor

@coderabbitai

coderabbitai Bot commented May 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

The PR consolidates marketplace configuration into a single source of truth (marketplace.source.json), introduces scripts to generate platform-specific manifests and validate consistency, removes redundant platform-specific plugin.json files, establishes CI validation via GitHub Actions, and updates documentation to reflect the new structure. All identifiers are rebranded from recoupable to recoup.

Changes

Marketplace Consolidation & Generation

Layer / File(s) Summary
Single-source marketplace definition
marketplace.source.json
Establishes marketplace.source.json as the authoritative configuration for the Recoup marketplace, defining metadata (name, owner, description), the recoup-skills plugin, its skill paths, keywords, and interface properties (capabilities, branding).
Platform-specific marketplace outputs
.agents/plugins/marketplace.json, .claude-plugin/marketplace.json, .cursor-plugin/marketplace.json
Added generated marketplace manifests for Agent, Claude Code, and Cursor, each tailored to the respective platform's JSON schema and field expectations.
Deprecated distributed plugin files
.claude-plugin/plugin.json, .codex-plugin/plugin.json (removed)
Removed old individual plugin manifests; functionality is now centralized and auto-generated from marketplace.source.json.
Marketplace generation script
scripts/generate-marketplaces.py
Python generator that loads marketplace.source.json, builds three platform-specific marketplace payloads with per-platform field mappings, serializes deterministically, and writes only on content change; --check mode enables CI to verify outputs are in sync.

Marketplace Validation & CI

Layer / File(s) Summary
Manifest validation script
scripts/validate-manifests.py
Standalone Python validator that confirms marketplace outputs match generated state, verifies all plugin source paths exist, validates virtual plugin skill paths resolve to directories with valid SKILL.md frontmatter, checks self-contained plugins have per-product manifest files, and ensures all root-level skills have required YAML frontmatter (name, description).
CI validation workflow
.github/workflows/validate.yml
New GitHub Actions workflow triggered on pushes to main and pull requests; runs marketplace sync check (scripts/generate-marketplaces.py --check) followed by full validation (scripts/validate-manifests.py).

Documentation & Contribution Workflow

Layer / File(s) Summary
Agent platform and structure guide
AGENTS.md
Rewritten to describe the unified marketplace model (single source of truth + platform-specific generation), supported agent platforms (Claude Code, Codex, Cursor), on-disk directory layout, rules for editing source manifest, validation checks, and required SKILL.md YAML frontmatter schema.
Project README
README.md
Updated title and branding to "Recoup Skills," added installation instructions for Claude Code/Codex/Cursor, replaced skill listing with marketplace table using relative ./skills/... links, added Maintenance section with commands to regenerate and validate manifests.
Contribution guidance
contributing.md
Simplified to include a location table for new skills/plugins, a rule for promoting skills to plugins, and a workflow that references AGENTS.md, requires updating marketplace.source.json, and runs marketplace generation/validation scripts.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • recoupable/skills#7: Directly follows from earlier consolidation work that introduced platform-specific plugin manifests; this PR replaces those with a centralized generated-marketplace approach and establishes the validation infrastructure.

Suggested reviewers

  • sweetmantech

Poem

🐰 From scattered manifests we weave,
A single source that won't deceive,
Rebranded bright from recoupable cheer
To recoup—the name's now crystal clear!
Scripts validate, platforms align,
One marketplace source—oh, how fine! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR fully implements issue #28 requirements: marketplace and plugin identifiers renamed from recoupable to recoup; display names and paths updated; real-world resources preserved; generator and validator scripts added and tested.
Out of Scope Changes check ✅ Passed All changes are in scope: marketplace infrastructure, manifest generation/validation, docs updated for the new machinery, and superseded root manifest files removed. No unrelated modifications.
Title check ✅ Passed The title 'feat(marketplace): add unified marketplace machinery (2/3)' accurately describes the main change: introducing infrastructure for a single-source-of-truth marketplace with generator/validator scripts and source files.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/marketplace-machinery

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 13 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="README.md">

<violation number="1" location="README.md:12">
P1: Custom agent: **Flag AI Slop and Fabricated Changes**

README references `music-catalog-diligence` plugin files, install commands, and directory tree, but `plugins/music-catalog-diligence/` does not exist and the plugin is not registered in `marketplace.source.json`.</violation>
</file>

<file name="scripts/validate-manifests.py">

<violation number="1" location="scripts/validate-manifests.py:135">
P2: `source` paths are only checked for existence, not that they are directories, so invalid plugin manifests can pass validation.</violation>

<violation number="2" location="scripts/validate-manifests.py:140">
P1: Virtual plugin `skills` entries are not validated as skill directories with valid `SKILL.md` frontmatter, so malformed marketplace entries can pass CI.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread README.md Outdated
Comment thread scripts/validate-manifests.py
Comment thread scripts/validate-manifests.py
@sidneyswift sidneyswift force-pushed the feat/marketplace-machinery branch from cc7923a to afe7858 Compare May 13, 2026 03:42
@sidneyswift sidneyswift changed the title feat(marketplace): add unified marketplace machinery (1/4) feat(marketplace): add unified marketplace machinery (1/3) May 13, 2026
@sidneyswift sidneyswift force-pushed the feat/marketplace-machinery branch 3 times, most recently from ad0dffb to 972245c Compare May 13, 2026 17:28

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@scripts/generate-marketplaces.py`:
- Around line 149-154: The code currently calls path.parent.mkdir(...) before
checking the check flag, causing side effects in --check mode; change the flow
in the generate script so you only create directories and write files when not
in check mode by moving or guarding the mkdir and any file-write operations
behind a condition that ensures if check is True you do not call
path.parent.mkdir or modify the filesystem—use the existing/content/check
variables (path, existing, content, check) to first read path.exists()/content,
return True/False as appropriate, and only perform path.parent.mkdir(...) and
subsequent writes when check is False.
- Around line 81-83: The "interface" dict currently hardcodes "displayName":
"Recoup"; change this to pull the display name from the canonical source file
(marketplace.source.json) instead of a literal string: locate the code that
builds the "interface" object (the block assigning the "interface" dictionary
and key "displayName") and replace the hardcoded "Recoup" with the value read
from the loaded marketplace source data (e.g., use the parsed
marketplace.source.json entry for this marketplace, such as
source_data["displayName"] or marketplace_source["interface"]["displayName"]) so
the generator derives the displayName from the source-of-truth.

In `@scripts/validate-manifests.py`:
- Around line 134-149: The current validation allows paths like "../..." that
resolve outside the repository; update both the source check (variable target)
and the skill checks (variable resolved) to reject any resolved path that is not
inside REPO_ROOT. Concretely, after computing target = (REPO_ROOT /
source).resolve() and after computing resolved = (REPO_ROOT /
skill_path).resolve(), add a containment test against REPO_ROOT.resolve() (e.g.,
ensure REPO_ROOT.resolve() is equal to or is a parent of the resolved path) and
call report(errors, ...) with the same messages if the path is outside the repo;
keep the existing existence/is_dir checks unchanged. Ensure you reference the
same identifiers: REPO_ROOT, target, resolved, source, skill_path,
plugin.get("skills"), report, and name.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 02d10401-2243-4cb7-99ce-2b2e78ec0fe3

📥 Commits

Reviewing files that changed from the base of the PR and between 77126f9 and 972245c.

📒 Files selected for processing (12)
  • .agents/plugins/marketplace.json
  • .claude-plugin/marketplace.json
  • .claude-plugin/plugin.json
  • .codex-plugin/plugin.json
  • .cursor-plugin/marketplace.json
  • .github/workflows/validate.yml
  • AGENTS.md
  • README.md
  • contributing.md
  • marketplace.source.json
  • scripts/generate-marketplaces.py
  • scripts/validate-manifests.py
💤 Files with no reviewable changes (2)
  • .codex-plugin/plugin.json
  • .claude-plugin/plugin.json

Comment on lines +81 to +83
"interface": {
"displayName": "Recoup",
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Derive Codex displayName from source-of-truth instead of hardcoding.

"Recoup" is hardcoded here, so this file can drift from marketplace.source.json during future branding edits.

Proposed patch
-        "interface": {
-            "displayName": "Recoup",
-        },
+        "interface": {
+            "displayName": mp["name"],
+        },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-marketplaces.py` around lines 81 - 83, The "interface" dict
currently hardcodes "displayName": "Recoup"; change this to pull the display
name from the canonical source file (marketplace.source.json) instead of a
literal string: locate the code that builds the "interface" object (the block
assigning the "interface" dictionary and key "displayName") and replace the
hardcoded "Recoup" with the value read from the loaded marketplace source data
(e.g., use the parsed marketplace.source.json entry for this marketplace, such
as source_data["displayName"] or marketplace_source["interface"]["displayName"])
so the generator derives the displayName from the source-of-truth.

Comment on lines +149 to +154
path.parent.mkdir(parents=True, exist_ok=True)
existing = path.read_text() if path.exists() else None
if existing == content:
return True
if check:
return False

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Keep --check mode read-only.

This path creates directories even in check mode, which introduces side effects in validation/CI runs.

Proposed patch
 def write_if_changed(path: Path, content: str, *, check: bool) -> bool:
     """Return True when path matches content; False when it differs."""
-    path.parent.mkdir(parents=True, exist_ok=True)
+    if not check:
+        path.parent.mkdir(parents=True, exist_ok=True)
     existing = path.read_text() if path.exists() else None
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
path.parent.mkdir(parents=True, exist_ok=True)
existing = path.read_text() if path.exists() else None
if existing == content:
return True
if check:
return False
if not check:
path.parent.mkdir(parents=True, exist_ok=True)
existing = path.read_text() if path.exists() else None
if existing == content:
return True
if check:
return False
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/generate-marketplaces.py` around lines 149 - 154, The code currently
calls path.parent.mkdir(...) before checking the check flag, causing side
effects in --check mode; change the flow in the generate script so you only
create directories and write files when not in check mode by moving or guarding
the mkdir and any file-write operations behind a condition that ensures if check
is True you do not call path.parent.mkdir or modify the filesystem—use the
existing/content/check variables (path, existing, content, check) to first read
path.exists()/content, return True/False as appropriate, and only perform
path.parent.mkdir(...) and subsequent writes when check is False.

Comment on lines +134 to +149
target = (REPO_ROOT / source).resolve()
if not target.exists():
report(errors, f"plugin `{name}`: source path `{source}` does not exist")
continue
if not target.is_dir():
report(errors, f"plugin `{name}`: source path `{source}` is not a directory")
continue
if plugin.get("type") == "virtual":
for skill_path in plugin.get("skills", []):
resolved = (REPO_ROOT / skill_path).resolve()
if not resolved.exists():
report(errors, f"plugin `{name}`: skill path `{skill_path}` does not exist")
continue
if not resolved.is_dir():
report(errors, f"plugin `{name}`: skill path `{skill_path}` is not a directory")
continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reject plugin/skill paths that resolve outside the repository.

Current checks allow ../... paths as long as they exist, so external directories can pass validation and leak out of repo scope.

Proposed patch
 def check_source_paths(errors: list[str]) -> None:
     print("→ checking marketplace.source.json plugin paths")
     with SOURCE_FILE.open() as fh:
         data = json.load(fh)
+    repo_root_resolved = REPO_ROOT.resolve()
+
+    def _is_within_repo(path: Path) -> bool:
+        try:
+            path.relative_to(repo_root_resolved)
+            return True
+        except ValueError:
+            return False
+
     for plugin in data.get("plugins", []):
         name = plugin["name"]
         source = plugin["source"]
         target = (REPO_ROOT / source).resolve()
+        if not _is_within_repo(target):
+            report(errors, f"plugin `{name}`: source path `{source}` resolves outside repository")
+            continue
         if not target.exists():
             report(errors, f"plugin `{name}`: source path `{source}` does not exist")
             continue
         if not target.is_dir():
             report(errors, f"plugin `{name}`: source path `{source}` is not a directory")
             continue
         if plugin.get("type") == "virtual":
             for skill_path in plugin.get("skills", []):
                 resolved = (REPO_ROOT / skill_path).resolve()
+                if not _is_within_repo(resolved):
+                    report(errors, f"plugin `{name}`: skill path `{skill_path}` resolves outside repository")
+                    continue
                 if not resolved.exists():
                     report(errors, f"plugin `{name}`: skill path `{skill_path}` does not exist")
                     continue
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/validate-manifests.py` around lines 134 - 149, The current validation
allows paths like "../..." that resolve outside the repository; update both the
source check (variable target) and the skill checks (variable resolved) to
reject any resolved path that is not inside REPO_ROOT. Concretely, after
computing target = (REPO_ROOT / source).resolve() and after computing resolved =
(REPO_ROOT / skill_path).resolve(), add a containment test against
REPO_ROOT.resolve() (e.g., ensure REPO_ROOT.resolve() is equal to or is a parent
of the resolved path) and call report(errors, ...) with the same messages if the
path is outside the repo; keep the existing existence/is_dir checks unchanged.
Ensure you reference the same identifiers: REPO_ROOT, target, resolved, source,
skill_path, plugin.get("skills"), report, and name.

…nding)

Sets up the single-source-of-truth marketplace pattern. After this lands,
adding a new plugin is one entry in marketplace.source.json + a regen.
No end-user behavior change yet — this PR ships the infrastructure only.

What this PR adds
- marketplace.source.json — single source of truth for the marketplace
  + the recoup-skills virtual plugin (the existing 12 broad music skills).
- scripts/generate-marketplaces.py — emits the three platform manifests:
  .claude-plugin/marketplace.json, .agents/plugins/marketplace.json,
  .cursor-plugin/marketplace.json. --check mode detects drift.
- scripts/validate-manifests.py — checks generator parity, plugin source
  paths, .claude/.codex/.cursor plugin manifests on each plugins/ folder,
  and SKILL.md frontmatter (name + description) on every skill.
- .github/workflows/validate.yml — runs both scripts on every PR.
- AGENTS.md, README.md, contributing.md, CHANGELOG.md — describe the
  source-of-truth + generated-files pattern (with recoup branding).
- Removes the now-superseded root .claude-plugin/plugin.json and
  .codex-plugin/plugin.json (the recoup-skills virtual plugin entry
  in the marketplace replaces them).

Branding
- Marketplace name: recoup
- Plugin name: recoup-skills
- Display names: Recoup Skills, developerName: Recoup
- Owner/author: Recoup
- Install path: @recoup
- recoupable.com URLs, agent@recoupable.com, recoupable/skills repo
  path, @recoupable/cli npm package, and LICENSE entity stay unchanged.

After this lands
  /plugin marketplace add recoupable/skills
  /plugin install recoup-skills@recoup

Why this is small enough to review
- 13 files, +961 / -137. All-new infrastructure, zero behavior change
  for end users (no skill paths under skills/ moved).
- Generated files are mechanically derivable from marketplace.source.json.
  Spot-check the source file; trust the generator.

What's intentionally NOT in this PR
- No catalog plugin (separate PR — adds plugins/music-catalog-diligence/
  directory and registers it in marketplace.source.json).
- No getting-started orchestrator rewrite (separate PR).

Test plan
- python3 scripts/generate-marketplaces.py --check → exit 0
- python3 scripts/validate-manifests.py            → exit 0

Replaces #23 (split into 3 PRs; rename baked into each).

Co-authored-by: Cursor <cursoragent@cursor.com>
@sidneyswift sidneyswift force-pushed the feat/marketplace-machinery branch from 972245c to 6794f24 Compare May 13, 2026 17:43
@sidneyswift sidneyswift changed the title feat(marketplace): add unified marketplace machinery (1/3) feat(marketplace): add unified marketplace machinery (2/3) May 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant