mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(skills): group core skills in the skills add picker (#2412)
Add a `core-skills` entry to .claude-plugin/marketplace.json declaring the core skill set (the /hyperframes router, the hyperframes-* domain skills, and media-use). The upstream vercel-labs/skills CLI reads that entry's `skills` array for its interactive picker: the core set renders under a "Core Skills" group and everything else falls into "Other", so a human running `npx skills add heygen-com/hyperframes --full-depth` can tell the always-needed core set apart from the on-demand creation workflows — mirroring the core/on-demand tiers `hyperframes skills update` already enforces (isCoreSkill in skillsManifest.ts). The array deliberately lives on a separate marketplace entry, NOT on plugin.json or the `hyperframes` entry: Claude Code treats a manifest `skills` array as that plugin's skill allowlist (verified against claude CLI), so attaching it to the full plugin would narrow it from all skills to the core 8. As a side effect the new entry is itself a coherent Claude Code plugin — `core-skills@hyperframes` installs just the core set — while `hyperframes@hyperframes` keeps auto-discovering everything. A new pin test keeps the marketplace list in lockstep with isCoreSkill and the skills/ tree (alongside the existing FALLBACK_CORE_SKILLS pin), and asserts the full plugin carries no allowlist. Agent installs are unaffected — the upstream CLI detects agent environments and installs non-interactively, and the hyperframes CLI always passes explicit --skill flags. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6933e8acda
commit
4a4903b49a
@@ -139,6 +139,55 @@ describe("FALLBACK_CORE_SKILLS pin", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe(".claude-plugin/marketplace.json core-skills pin", () => {
|
||||
// The marketplace `core-skills` entry's `skills` array drives two surfaces:
|
||||
// the upstream `skills add` picker groups the listed skills under
|
||||
// "Core Skills" (everything unlisted falls into "Other"), and Claude Code
|
||||
// treats the array as that plugin's skill allowlist. That makes it a third
|
||||
// enumeration of core membership — pin it to isCoreSkill and the skills/
|
||||
// tree so neither surface can silently drift from the tiers `init` /
|
||||
// `skills update` actually enforce. It lives on a separate marketplace
|
||||
// entry (not plugin.json, and not the `hyperframes` entry) precisely so
|
||||
// the full `hyperframes` plugin keeps auto-discovering all skills.
|
||||
it("lists exactly the core skills present in the repo's skills/ tree", () => {
|
||||
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..", "..");
|
||||
const marketplace = JSON.parse(
|
||||
readFileSync(join(repoRoot, ".claude-plugin", "marketplace.json"), "utf-8"),
|
||||
) as { plugins?: { name?: string; skills?: string[] }[] };
|
||||
const entry = marketplace.plugins?.find((p) => p.name === "core-skills");
|
||||
if (!entry?.skills) {
|
||||
throw new Error("marketplace.json is missing the core-skills entry's `skills` array");
|
||||
}
|
||||
const declared = entry.skills.map((p) => p.replace(/^\.\/skills\//, "")).sort();
|
||||
|
||||
const skillsRoot = join(repoRoot, "skills");
|
||||
const coreOnDisk = readdirSync(skillsRoot)
|
||||
.filter((n) => existsSync(join(skillsRoot, n, "SKILL.md")))
|
||||
.filter((n) => isCoreSkill(n))
|
||||
.sort();
|
||||
|
||||
expect(declared).toEqual(coreOnDisk);
|
||||
// Upstream resolves each entry relative to the repo root — the "./skills/"
|
||||
// prefix is load-bearing (see vercel-labs/skills plugin-manifest.ts).
|
||||
for (const p of entry.skills) {
|
||||
expect(p.startsWith("./skills/")).toBe(true);
|
||||
}
|
||||
// The full plugin must NOT carry a skills allowlist: Claude Code would
|
||||
// narrow it to the listed subset instead of auto-discovering all skills.
|
||||
const full = marketplace.plugins?.find((p) => p.name === "hyperframes");
|
||||
expect(full).toBeDefined();
|
||||
expect(full?.skills).toBeUndefined();
|
||||
// Same for plugin.json (the direct-install manifest for the full plugin) —
|
||||
// and upstream lets plugin.json groupings override marketplace ones, so a
|
||||
// skills array here would also rename the picker group back to
|
||||
// "Hyperframes".
|
||||
const plugin = JSON.parse(
|
||||
readFileSync(join(repoRoot, ".claude-plugin", "plugin.json"), "utf-8"),
|
||||
) as { skills?: string[] };
|
||||
expect(plugin.skills).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("diffSkills", () => {
|
||||
const latest: SkillsManifest = {
|
||||
source: "test",
|
||||
|
||||
Reference in New Issue
Block a user