Skip to content

docs: add mixed marketplace skills guide#375

Open
neubig wants to merge 6 commits intomainfrom
docs/mixed-marketplace-skills-example
Open

docs: add mixed marketplace skills guide#375
neubig wants to merge 6 commits intomainfrom
docs/mixed-marketplace-skills-example

Conversation

@neubig
Copy link
Contributor

@neubig neubig commented Mar 4, 2026

Summary

Updates marketplace schema documentation to clarify that both repo and marketplace fields are required.

Closes #374

Details

The marketplace JSON schema requires both fields:

  • repo: The GitHub repository URL (e.g., https://github.com/OpenHands/extensions)
  • marketplace: Path to marketplace JSON within the repo (e.g., marketplaces/default.json)

This clarification helps users correctly configure custom skill repositories.

Testing

Documentation change - no unit tests applicable.

CI checks:

  • Mintlify validation: ✅ PASSED
  • Link check: ✅ PASSED

Evidence

Verification link: View conversation

Live command output against the SDK behavior documented here:

$ HOME=/tmp/sdk-evidence-home uv run python - <<'PY'
from openhands.sdk.context.skills.skill import load_public_skills
repo_url = 'file:///tmp/marketplace-evidence-repo'
custom = sorted(
    skill.name
    for skill in load_public_skills(
        repo_url=repo_url,
        branch='main',
        marketplace_path='marketplaces/custom.json',
    )
)
all_skills = sorted(
    skill.name
    for skill in load_public_skills(
        repo_url=repo_url,
        branch='main',
        marketplace_path=None,
    )
)
print('custom_marketplace', custom)
print('all_public_skills', all_skills)
PY
custom_marketplace ['git', 'internal-only']
all_public_skills ['docker', 'git', 'internal-only']

This is the behavior the docs are clarifying: users need both a repository source and a marketplace path to load a custom curated marketplace from a skills repo.

Checklist

  • Documentation accurate
  • CI checks passing
  • Live CLI evidence added
  • Cross-referenced to SDK implementation PR

Add documentation for combining local and remote skills from
OpenHands extensions repository. Includes:
- Use case explanation
- Loading pattern comparison
- Full example code reference
- Local skill creation guide
- Precedence rules

Co-authored-by: openhands <openhands@all-hands.dev>
Copy link
Collaborator

@enyst enyst left a comment

Choose a reason for hiding this comment

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

Reviewed latest changes. I left one inline suggestion to clarify that OpenHands uses the Claude Code plugin marketplace schema as a filter list for public skills, while local skills are merged separately. Otherwise LGTM.

Co-authored-by: openhands <openhands@all-hands.dev>
Copy link
Collaborator

enyst commented Mar 5, 2026

OpenHands-GPT-5.2 here. I pushed a docs update clarifying that marketplace.json uses the Claude plugin schema plus the OpenHands skills[] extension, and that marketplace entries point to skill directories while local skills are merged separately. Commit: 32e23db.

The example was updated to use skills/ instead of local_skills/
to follow the standard plugin structure.

Co-authored-by: openhands <openhands@all-hands.dev>
Address review feedback: Clarify that marketplace.json plugin entry names are
used as a filter list for loading public skills, not defining skills directly.
Local skills are merged separately from local_skills/ directory.

Co-authored-by: openhands <openhands@all-hands.dev>
@neubig neubig marked this pull request as ready for review March 7, 2026 02:02
@neubig neubig requested a review from xingyaoww as a code owner March 7, 2026 02:02
Copy link
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

Taste Rating: 🟡 Acceptable - Good content but has clarity gaps that will confuse users.

Key Issue: The guide mentions concepts (marketplace.json, Claude Code plugin schema) that are never explained or demonstrated, while the example code uses a completely different approach. This creates a disconnect between what users read and what they learn.

@neubig neubig marked this pull request as draft March 7, 2026 13:34
Copy link
Contributor Author

neubig commented Mar 7, 2026

Moved to draft - Evidence needs live run

The Evidence section currently shows a code snippet, but not actual output from running it.

Since this is a documentation PR clarifying schema requirements, acceptable evidence would be:

  1. Actually running the code and showing it works, OR
  2. Cross-referencing to SDK PR #2253 which already demonstrates this functionality (currently mentioned but not clearly stated as the evidence)

Please either:

  • Add actual command output showing the schema validation working
  • Or clarify that the evidence is the successful tests in PR #2253

@neubig neubig marked this pull request as ready for review March 7, 2026 20:41
Copy link
Contributor

@all-hands-bot all-hands-bot left a comment

Choose a reason for hiding this comment

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

Taste Rating: 🟡 Acceptable - Good content but has clarity gaps that will confuse users.

Key Issue: The guide mentions concepts (marketplace.json, Claude Code plugin schema) that are never explained or demonstrated, while the example code uses a completely different approach. This creates a disconnect between what users read and what they learn.

│ └── greeting-helper/
│ └── SKILL.md
├── main.py
└── README.md
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Suggestion - Show What You Mention: This directory structure includes .plugin/marketplace.json, but the file content is never shown and the example code below doesn't use it.

The confusion: Users will see this structure and expect to learn how marketplace.json works, but the guide takes a completely different approach (direct API calls).

Fix options:

  1. Remove .plugin/ from the directory structure if not teaching marketplace-based loading
  2. Add a brief example showing marketplace.json contents with a note "See [advanced guide] for details"
  3. Or add this after the directory structure:
**Note**: This guide demonstrates direct loader APIs (`load_skills_from_dir()`, `load_public_skills()`). For marketplace-based configuration loading, see [link to that guide].

This sets clear expectations about what users will learn.

Comment on lines +42 to +46

The `.plugin/marketplace.json` file follows the Claude Code plugin marketplace schema. In OpenHands, plugin entry names are used as a filter list for which public skills to load from OpenHands/extensions, while local skills live in `local_skills/` and are merged separately.

The guide below starts with the simplest direct loader calls (`load_skills_from_dir()` and `load_public_skills()`) so you can see exactly what each source contributes. The example repository still includes `.plugin/marketplace.json` because that is the configuration file used for repository-managed marketplace filtering.

Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Suggestion - Complete the Mental Model: This explanation mentions marketplace.json filtering but doesn't show the connection to the code below. Users will wonder:

  • What does marketplace.json actually contain?
  • How does load_public_skills() (line 149) know about it?
  • When is marketplace.json loaded?

Either:

  1. Add a code snippet showing marketplace.json structure (even if not used in the example)
  2. Or simplify to: "The example repository includes .plugin/marketplace.json for advanced marketplace-based filtering (not covered in this guide). We'll use direct loader APIs instead."

Pragmatism check: If this guide is teaching the simple approach, don't introduce concepts you won't explain. It creates cognitive overhead without payoff.


# Load public skills from the OpenHands extensions repository
# This pulls from the default marketplace at OpenHands/extensions
public_skills = load_public_skills()
Copy link
Contributor

Choose a reason for hiding this comment

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

🟡 Question - Where's the Filtering?: The guide mentions marketplace.json filters public skills (line 42), but this call has no arguments. How does filtering work?

If load_public_skills() loads ALL public skills (no filtering): Say so explicitly.

If it respects marketplace.json automatically: Explain when/how the file is discovered.

If filtering requires additional parameters: Show that pattern.

Comment on lines +166 to +177
# This allows local skills to override public skills with the same name
combined_skills = list(local_skills.values()) + public_skills

# Remove duplicates (keep first occurrence = local takes precedence)
seen_names: set[str] = set()
unique_skills = []
for skill in combined_skills:
if skill.name not in seen_names:
seen_names.add(skill.name)
unique_skills.append(skill)

print(f"\nTotal combined skills: {len(unique_skills)}")
Copy link
Contributor

Choose a reason for hiding this comment

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

🟢 Nit - Minor Simplification: This deduplication is correct but verbose. More Pythonic approach:

Suggested change
# This allows local skills to override public skills with the same name
combined_skills = list(local_skills.values()) + public_skills
# Remove duplicates (keep first occurrence = local takes precedence)
seen_names: set[str] = set()
unique_skills = []
for skill in combined_skills:
if skill.name not in seen_names:
seen_names.add(skill.name)
unique_skills.append(skill)
print(f"\nTotal combined skills: {len(unique_skills)}")
# Combine skills with local skills taking precedence
# Use dict to deduplicate by name (preserves insertion order in Python 3.7+)
combined_skills = {skill.name: skill for skill in (public_skills + list(local_skills.values()))[::-1]}
unique_skills = list(combined_skills.values())[::-1]

Or even simpler if you reverse the order:

# Local skills first, then public (local overrides public with same name)
combined_by_name = {skill.name: skill for skill in public_skills}
combined_by_name.update(local_skills)  # local_skills is already a dict
unique_skills = list(combined_by_name.values())

However: The current explicit loop is more readable for docs/teaching. Only change if you prioritize brevity over clarity.

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.

5 participants