From d72f23c3ddec4fbaac7204cada4785211a2be6c4 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 15 Jul 2026 11:22:21 -0400 Subject: [PATCH 1/3] =?UTF-8?q?fix(agents):=20review=20fixes=20=E2=80=94?= =?UTF-8?q?=20declared=20framework=20identity,=20strict=20flow=20key,=20pu?= =?UTF-8?q?re=20variants=5Ffrom?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-ups from the review of #181: - skill-menu.json entries now carry `group` (hyphenated skill-id prefix), `framework` (the detection id a variant serves), and `default` (the variant a bare framework id resolves to when a family has several). Declared in the integration group's variant matrix and copied across by `variants_from`, so consumers resolve variants by exact match instead of reverse-engineering ids with alias tables and prefix guessing. Pairs with the wizard-side resolver change (PostHog/wizard PR on experiment/orchestrator-pi-runtask). - agent prompts must declare `flow:` — the build now rejects a missing key instead of only a contradicting one, since consumers filter prompts by it and a missing key built green here but silently dropped the prompt from the runtime registry. - `resolveVariantsFrom` is pure: it returns a resolved copy instead of mutating the config in place, which removes the `_variantsResolved` marker that existed to dodge the "variants or variants_from, not both" validation on re-expansion. Generated-By: PostHog Code Task-Id: fafc230d-6f14-4e4d-9462-0e7f18a1eec1 --- context/skills/integration/config.yaml | 45 ++++++++++++ scripts/lib/agent-generator.js | 14 +++- scripts/lib/build-phases.js | 13 +++- scripts/lib/skill-generator.js | 32 ++++++--- .../lib/tests/agent-generator-flow.test.js | 68 +++++++++++++++++++ scripts/lib/tests/skill-variants-from.test.js | 17 +++++ 6 files changed, 175 insertions(+), 14 deletions(-) create mode 100644 scripts/lib/tests/agent-generator-flow.test.js diff --git a/context/skills/integration/config.yaml b/context/skills/integration/config.yaml index 1dae71c4..71820372 100644 --- a/context/skills/integration/config.yaml +++ b/context/skills/integration/config.yaml @@ -1,4 +1,12 @@ # Integration skills - example-based (code + docs) +# +# `framework` names the detection id a variant serves (the id the wizard's +# detector emits, e.g. `rails`, `react-router`). It is emitted into +# skill-menu.json so consumers resolve a framework to a variant by exact +# match. When a family has several variants (app vs pages router), exactly +# one carries `default: true` — the one a bare framework id resolves to. +# Variants with no `framework` (e.g. react-vite, php) are only reachable by +# their full id. type: skill template: description.md description: PostHog integration for {display_name} applications @@ -6,6 +14,8 @@ shared_docs: - https://posthog.com/docs/getting-started/identify-users.md variants: - id: nextjs-app-router + framework: nextjs + default: true example_paths: example-apps/next-app-router display_name: Next.js App Router tags: [nextjs, react, ssr, app-router, javascript, javascript_web, javascript_node] @@ -13,6 +23,7 @@ variants: - https://posthog.com/docs/libraries/next-js.md - id: nextjs-pages-router + framework: nextjs example_paths: example-apps/next-pages-router display_name: Next.js Pages Router tags: [nextjs, react, ssr, pages-router, javascript, javascript_web, javascript_node] @@ -20,6 +31,8 @@ variants: - https://posthog.com/docs/libraries/next-js.md - id: react-react-router-6 + framework: react-router + default: true example_paths: example-apps/react-react-router-6 display_name: React Router v6 tags: [react, react-router, v6, spa, javascript, javascript_web] @@ -27,6 +40,7 @@ variants: - https://posthog.com/docs/libraries/react-router/react-router-v6.md - id: react-react-router-7-framework + framework: react-router example_paths: example-apps/react-react-router-7-framework display_name: React Router v7 - Framework mode tags: [react, react-router, v7, framework, ssr, javascript, javascript_node, javascript_web] @@ -34,6 +48,7 @@ variants: - https://posthog.com/docs/libraries/react-router/react-router-v7-framework-mode.md - id: react-react-router-7-data + framework: react-router example_paths: example-apps/react-react-router-7-data display_name: React Router v7 - Data mode tags: [react, react-router, v7, data, spa, javascript, javascript_web] @@ -41,6 +56,7 @@ variants: - https://posthog.com/docs/libraries/react-router/react-router-v7-data-mode.md - id: react-react-router-7-declarative + framework: react-router example_paths: example-apps/react-react-router-7-declarative display_name: React Router v7 - Declarative mode tags: [react, react-router, v7, declarative, spa, javascript, javascript_web] @@ -56,6 +72,8 @@ variants: - https://posthog.com/docs/libraries/react.md - id: nuxt-3-6 + framework: nuxt + default: true example_paths: example-apps/nuxt-3-6 display_name: Nuxt 3.6 description: PostHog integration for Nuxt versions 3.0 to 3.6 @@ -64,6 +82,7 @@ variants: - https://posthog.com/docs/libraries/nuxt-js-3-6.md - id: nuxt-4 + framework: nuxt example_paths: example-apps/nuxt-4 display_name: Nuxt 4 description: PostHog integration for Nuxt 4 applications @@ -72,6 +91,7 @@ variants: - https://posthog.com/docs/libraries/nuxt-js.md - id: vue-3 + framework: vue example_paths: example-apps/vue-3 display_name: Vue 3 description: PostHog integration for Vue 3 applications @@ -80,6 +100,7 @@ variants: - https://posthog.com/docs/libraries/vue-js.md - id: django + framework: django example_paths: example-apps/django display_name: Django tags: [django, python] @@ -87,6 +108,7 @@ variants: - https://posthog.com/docs/libraries/django.md - id: flask + framework: flask example_paths: example-apps/flask display_name: Flask tags: [flask, python] @@ -94,6 +116,7 @@ variants: - https://posthog.com/docs/libraries/flask.md - id: fastapi + framework: fastapi example_paths: example-apps/fastapi display_name: FastAPI description: PostHog integration for FastAPI applications @@ -102,6 +125,7 @@ variants: - https://posthog.com/docs/libraries/python.md - id: react-tanstack-router-file-based + framework: tanstack-router example_paths: example-apps/react-tanstack-router-file-based display_name: React with TanStack Router (file-based) description: PostHog integration for React applications using TanStack Router with file-based routing @@ -110,6 +134,8 @@ variants: - https://posthog.com/docs/libraries/tanstack-start.md - id: react-tanstack-router-code-based + framework: tanstack-router + default: true example_paths: example-apps/react-tanstack-router-code-based display_name: React with TanStack Router (code-based) description: PostHog integration for React applications using TanStack Router with code-based routing @@ -118,6 +144,7 @@ variants: - https://posthog.com/docs/libraries/tanstack-start.md - id: tanstack-start + framework: tanstack-start example_paths: example-apps/tanstack-start display_name: TanStack Start description: PostHog integration for TanStack Start full-stack applications @@ -126,6 +153,7 @@ variants: - https://posthog.com/docs/libraries/tanstack-start.md - id: laravel + framework: laravel example_paths: example-apps/laravel display_name: Laravel tags: [laravel, php] @@ -141,6 +169,7 @@ variants: - https://posthog.com/docs/libraries/php.md - id: ruby-on-rails + framework: rails example_paths: example-apps/ruby-on-rails display_name: Ruby on Rails tags: [ruby-on-rails, ruby] @@ -149,6 +178,7 @@ variants: - https://posthog.com/docs/libraries/ruby.md - id: android + framework: android example_paths: example-apps/android display_name: Android description: PostHog integration for Android applications @@ -157,6 +187,7 @@ variants: - https://posthog.com/docs/libraries/android.md - id: sveltekit + framework: sveltekit example_paths: example-apps/sveltekit display_name: SvelteKit description: PostHog integration for SvelteKit applications @@ -166,6 +197,7 @@ variants: # Language fallback skills, used when no specific framework is detected - id: python + framework: python example_paths: example-apps/python display_name: Python description: PostHog integration for any Python application using the Python SDK @@ -175,6 +207,7 @@ variants: - https://posthog.com/docs/references/posthog-python.md - id: javascript_node + framework: javascript_node example_path: example-apps/javascript-node display_name: JavaScript Node description: PostHog integration for server-side Node.js applications using posthog-node @@ -184,6 +217,7 @@ variants: - https://posthog.com/docs/references/posthog-node.md - id: javascript_web + framework: javascript_web example_path: example-apps/javascript-web display_name: JavaScript Web description: PostHog integration for client-side web JavaScript applications using posthog-js @@ -193,6 +227,7 @@ variants: - https://posthog.com/docs/references/posthog-js.md - id: ruby + framework: ruby example_paths: example-apps/ruby display_name: Ruby description: PostHog integration for any Ruby application using the Ruby SDK @@ -217,6 +252,7 @@ variants: - https://posthog.com/docs/libraries/go.md - id: swift + framework: swift example_paths: - example-apps/swift - example-apps/swift-xcodegen @@ -237,6 +273,8 @@ variants: - https://posthog.com/docs/libraries/flutter.md - id: react-native + framework: react-native + default: true example_paths: example-apps/react-native display_name: React Native description: PostHog integration for React Native applications @@ -245,6 +283,7 @@ variants: - https://posthog.com/docs/libraries/react-native.md - id: expo + framework: react-native example_paths: example-apps/expo display_name: Expo description: PostHog integration for Expo applications @@ -253,6 +292,7 @@ variants: - https://posthog.com/docs/libraries/react-native.md - id: astro-static + framework: astro example_paths: example-apps/astro-static display_name: Astro (Static) description: PostHog integration for static Astro sites using SSG @@ -261,6 +301,7 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: astro-view-transitions + framework: astro example_paths: example-apps/astro-view-transitions display_name: Astro (View Transitions) description: PostHog integration for Astro with ClientRouter view transitions @@ -269,6 +310,7 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: astro-ssr + framework: astro example_paths: example-apps/astro-ssr display_name: Astro (SSR) description: PostHog integration for server-rendered Astro applications with API routes @@ -277,6 +319,8 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: astro-hybrid + framework: astro + default: true example_paths: example-apps/astro-hybrid display_name: Astro (Hybrid) description: PostHog integration for Astro hybrid rendering with both static and server-rendered pages @@ -285,6 +329,7 @@ variants: - https://posthog.com/docs/libraries/astro.md - id: angular + framework: angular example_paths: example-apps/angular display_name: Angular description: PostHog integration for Angular applications diff --git a/scripts/lib/agent-generator.js b/scripts/lib/agent-generator.js index 10689301..0de836e3 100644 --- a/scripts/lib/agent-generator.js +++ b/scripts/lib/agent-generator.js @@ -46,14 +46,22 @@ export function loadAgentEntries(agentsSourceDir) { } /** - * A prompt's frontmatter `flow:` must match its folder — the folder is the - * registry scope, the frontmatter keeps the file self-describing on disk. + * A prompt's frontmatter `flow:` must be present and match its folder — the + * folder is the registry scope, the frontmatter keeps the file + * self-describing on disk. Absence is an error, not a pass: consumers filter + * prompts by the frontmatter flow, so a missing key would build fine here and + * then silently drop the prompt from the registry at runtime. */ function assertFlowMatches(sourcePath, flow) { const text = fs.readFileSync(sourcePath, 'utf8'); const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---/); const declared = match?.[1].match(/^flow:\s*(.+?)\s*$/m)?.[1]; - if (declared && declared !== flow) { + if (!declared) { + throw new Error( + `Agent prompt ${sourcePath} is missing the "flow:" frontmatter key — declare flow: ${flow}`, + ); + } + if (declared !== flow) { throw new Error( `Agent prompt ${sourcePath} declares flow "${declared}" but lives in agents/${flow}/`, ); diff --git a/scripts/lib/build-phases.js b/scripts/lib/build-phases.js index a52e08d8..30e59dd2 100644 --- a/scripts/lib/build-phases.js +++ b/scripts/lib/build-phases.js @@ -190,11 +190,20 @@ function writeManifestAndMenu({ allSkills, docContents, distDir, configDir, vers for (const skill of allSkills) { const cat = skill.group; if (!skillsByCategory[cat]) skillsByCategory[cat] = []; - skillsByCategory[cat].push({ + // `group` is the hyphenated skill-id prefix and `framework` the + // detection id a variant serves, so a consumer resolves a bare skill + // id + framework to a menu id by exact match instead of guessing from + // id prefixes. `default: true` marks the variant a bare framework id + // resolves to when a family has several (e.g. app vs pages router). + const entry = { id: skill.id, name: skill.name, + group: skill.group.replace(/\//g, '-'), downloadUrl: manifest.resources.find(r => r.id === skill.id)?.downloadUrl, - }); + }; + if (skill.framework) entry.framework = skill.framework; + if (skill.default) entry.default = true; + skillsByCategory[cat].push(entry); } // The CLI entries are the lookup table the wizard's runtime resolver uses diff --git a/scripts/lib/skill-generator.js b/scripts/lib/skill-generator.js index e67998b6..a06e9021 100644 --- a/scripts/lib/skill-generator.js +++ b/scripts/lib/skill-generator.js @@ -228,15 +228,20 @@ function normalizeExamplePaths(value) { /** * Resolve `variants_from` references: a group may borrow another group's * variant matrix instead of duplicating it. Only the framework identity comes - * across — id, display_name, tags, docs_urls — never example paths, templates, - * cli blocks, or shared docs, which stay the borrowing group's own concern. - * One level only; a source group must declare its variants literally. + * across — id, display_name, tags, docs_urls, framework, default — never + * example paths, templates, cli blocks, or shared docs, which stay the + * borrowing group's own concern. One level only; a source group must declare + * its variants literally. Returns a new config; the input is never mutated, + * so repeated expansion of the same config is naturally stable. */ function resolveVariantsFrom(config) { + const resolved = {}; for (const [key, group] of Object.entries(config)) { - if (!group.variants_from) continue; + if (!group.variants_from) { + resolved[key] = group; + continue; + } if (group.variants) { - if (group._variantsResolved) continue; throw new Error(`Skill group "${key}": declare either variants or variants_from, not both`); } const source = config[group.variants_from]; @@ -246,14 +251,17 @@ function resolveVariantsFrom(config) { if (source.variants_from) { throw new Error(`Skill group "${key}": variants_from cannot chain ("${group.variants_from}" also uses variants_from)`); } - group.variants = source.variants.map(v => { + const variants = source.variants.map(v => { const variant = { id: v.id, display_name: v.display_name }; if (v.tags) variant.tags = [...v.tags]; if (v.docs_urls) variant.docs_urls = [...v.docs_urls]; + if (v.framework) variant.framework = v.framework; + if (v.default) variant.default = v.default; return variant; }); - group._variantsResolved = true; + resolved[key] = { ...group, variants }; } + return resolved; } /** @@ -263,9 +271,9 @@ function resolveVariantsFrom(config) { */ function expandSkillGroups(config, configDir) { const skills = []; - resolveVariantsFrom(config); + const resolvedConfig = resolveVariantsFrom(config); - for (const [key, group] of Object.entries(config)) { + for (const [key, group] of Object.entries(resolvedConfig)) { if (key === 'shared_docs') continue; if (!group.variants) continue; @@ -796,6 +804,12 @@ function serializeSkill(s) { description: s.description, tags: s.tags || [], }; + if (s.framework) { + result.framework = s.framework; + } + if (s.default) { + result.default = true; + } if (s._cli) { result.cli = s._cli; } diff --git a/scripts/lib/tests/agent-generator-flow.test.js b/scripts/lib/tests/agent-generator-flow.test.js new file mode 100644 index 00000000..8e4e54d1 --- /dev/null +++ b/scripts/lib/tests/agent-generator-flow.test.js @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { mkdirSync, writeFileSync, mkdtempSync, rmSync, readFileSync, existsSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; + +import { buildAgents } from '../agent-generator.js'; + +const prompt = (frontmatter) => `---\n${frontmatter}\n---\n\n## Goal\n\nDo the thing.\n`; + +describe('buildAgents flow frontmatter', () => { + let tmpDir; + let configDir; + let distDir; + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), 'agents-test-')); + configDir = join(tmpDir, 'context'); + distDir = join(tmpDir, 'dist'); + mkdirSync(join(configDir, 'agents', 'my-flow'), { recursive: true }); + }); + + afterEach(() => rmSync(tmpDir, { recursive: true, force: true })); + + it('builds a prompt whose flow matches its folder', () => { + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: my-flow'), + ); + const { count } = buildAgents({ configDir, distDir, baseUrl: 'http://x' }); + expect(count).toBe(1); + const menu = JSON.parse(readFileSync(join(distDir, 'agents', 'agent-menu.json'), 'utf8')); + expect(menu.agents).toEqual([ + { id: 'task', flow: 'my-flow', downloadUrl: 'http://x/my-flow/task.md' }, + ]); + }); + + it('rejects a prompt missing the flow key — consumers filter by it', () => { + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task'), + ); + expect(() => buildAgents({ configDir, distDir, baseUrl: 'http://x' })).toThrow( + /missing the "flow:" frontmatter key/, + ); + }); + + it('rejects a prompt whose flow contradicts its folder', () => { + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: other-flow'), + ); + expect(() => buildAgents({ configDir, distDir, baseUrl: 'http://x' })).toThrow( + /declares flow "other-flow"/, + ); + }); + + it('still ignores README.md files at both levels', () => { + writeFileSync(join(configDir, 'agents', 'README.md'), '# docs'); + writeFileSync(join(configDir, 'agents', 'my-flow', 'README.md'), '# docs'); + writeFileSync( + join(configDir, 'agents', 'my-flow', 'task.md'), + prompt('type: task\nflow: my-flow'), + ); + const { count } = buildAgents({ configDir, distDir, baseUrl: 'http://x' }); + expect(count).toBe(1); + expect(existsSync(join(distDir, 'agents', 'my-flow', 'README.md'))).toBe(false); + }); +}); diff --git a/scripts/lib/tests/skill-variants-from.test.js b/scripts/lib/tests/skill-variants-from.test.js index f3bcbd79..acf5d833 100644 --- a/scripts/lib/tests/skill-variants-from.test.js +++ b/scripts/lib/tests/skill-variants-from.test.js @@ -40,6 +40,8 @@ describe('variants_from', () => { { id: 'django', display_name: 'Django', + framework: 'django', + default: true, tags: ['django', 'python'], docs_urls: ['https://posthog.com/docs/libraries/django.md'], example_paths: 'example-apps/django', @@ -77,6 +79,21 @@ describe('variants_from', () => { expect(step._examplePaths).toEqual([]); }); + it('borrows framework and default so variant steps resolve like the source', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + const skills = expandSkillGroups(config, tmpDir); + const step = skills.find(s => s.id === 'flow-x-install-django'); + expect(step.framework).toBe('django'); + expect(step.default).toBe(true); + }); + + it('does not mutate the input config', () => { + const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; + expandSkillGroups(config, tmpDir); + expect(config['flow-x/install'].variants).toBeUndefined(); + expect(config['flow-x/install']._variantsResolved).toBeUndefined(); + }); + it('leaves the source group untouched', () => { const config = { integration: integrationGroup(), 'flow-x/install': borrowingGroup() }; const skills = expandSkillGroups(config, tmpDir); From 1d6c63b4b658f46b0623fc1beaae602261a9cf48 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 15 Jul 2026 11:34:17 -0400 Subject: [PATCH 2/3] chore(agents): trim review-fix comments to one-liners Generated-By: PostHog Code Task-Id: fafc230d-6f14-4e4d-9462-0e7f18a1eec1 --- context/skills/integration/config.yaml | 9 +-------- scripts/lib/agent-generator.js | 7 ++----- scripts/lib/build-phases.js | 6 +----- scripts/lib/skill-generator.js | 3 +-- 4 files changed, 5 insertions(+), 20 deletions(-) diff --git a/context/skills/integration/config.yaml b/context/skills/integration/config.yaml index 71820372..d676a1fd 100644 --- a/context/skills/integration/config.yaml +++ b/context/skills/integration/config.yaml @@ -1,12 +1,5 @@ # Integration skills - example-based (code + docs) -# -# `framework` names the detection id a variant serves (the id the wizard's -# detector emits, e.g. `rails`, `react-router`). It is emitted into -# skill-menu.json so consumers resolve a framework to a variant by exact -# match. When a family has several variants (app vs pages router), exactly -# one carries `default: true` — the one a bare framework id resolves to. -# Variants with no `framework` (e.g. react-vite, php) are only reachable by -# their full id. +# `framework` = the detection id a variant serves; `default: true` marks the family default. Both are emitted into skill-menu.json. type: skill template: description.md description: PostHog integration for {display_name} applications diff --git a/scripts/lib/agent-generator.js b/scripts/lib/agent-generator.js index 0de836e3..93e8aac3 100644 --- a/scripts/lib/agent-generator.js +++ b/scripts/lib/agent-generator.js @@ -46,11 +46,8 @@ export function loadAgentEntries(agentsSourceDir) { } /** - * A prompt's frontmatter `flow:` must be present and match its folder — the - * folder is the registry scope, the frontmatter keeps the file - * self-describing on disk. Absence is an error, not a pass: consumers filter - * prompts by the frontmatter flow, so a missing key would build fine here and - * then silently drop the prompt from the registry at runtime. + * A prompt's frontmatter `flow:` must be present and match its folder — + * consumers filter by it, so a missing key would silently drop the prompt. */ function assertFlowMatches(sourcePath, flow) { const text = fs.readFileSync(sourcePath, 'utf8'); diff --git a/scripts/lib/build-phases.js b/scripts/lib/build-phases.js index 30e59dd2..d65370de 100644 --- a/scripts/lib/build-phases.js +++ b/scripts/lib/build-phases.js @@ -190,11 +190,7 @@ function writeManifestAndMenu({ allSkills, docContents, distDir, configDir, vers for (const skill of allSkills) { const cat = skill.group; if (!skillsByCategory[cat]) skillsByCategory[cat] = []; - // `group` is the hyphenated skill-id prefix and `framework` the - // detection id a variant serves, so a consumer resolves a bare skill - // id + framework to a menu id by exact match instead of guessing from - // id prefixes. `default: true` marks the variant a bare framework id - // resolves to when a family has several (e.g. app vs pages router). + // group/framework/default let consumers resolve a bare skill id + framework by exact match. const entry = { id: skill.id, name: skill.name, diff --git a/scripts/lib/skill-generator.js b/scripts/lib/skill-generator.js index a06e9021..5cf53d13 100644 --- a/scripts/lib/skill-generator.js +++ b/scripts/lib/skill-generator.js @@ -231,8 +231,7 @@ function normalizeExamplePaths(value) { * across — id, display_name, tags, docs_urls, framework, default — never * example paths, templates, cli blocks, or shared docs, which stay the * borrowing group's own concern. One level only; a source group must declare - * its variants literally. Returns a new config; the input is never mutated, - * so repeated expansion of the same config is naturally stable. + * its variants literally. Pure — returns a new config, never mutates the input. */ function resolveVariantsFrom(config) { const resolved = {}; From 6eeba8eb8cc559ef2f86d9674ed97328eaa85341 Mon Sep 17 00:00:00 2001 From: "Vincent (Wen Yu) Ge" Date: Wed, 15 Jul 2026 12:43:48 -0400 Subject: [PATCH 3/3] refactor: drop the stale variants_from key from resolved groups (/simplify) Generated-By: PostHog Code Task-Id: fafc230d-6f14-4e4d-9462-0e7f18a1eec1 --- scripts/lib/skill-generator.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/lib/skill-generator.js b/scripts/lib/skill-generator.js index 5cf53d13..86b8b825 100644 --- a/scripts/lib/skill-generator.js +++ b/scripts/lib/skill-generator.js @@ -258,7 +258,9 @@ function resolveVariantsFrom(config) { if (v.default) variant.default = v.default; return variant; }); - resolved[key] = { ...group, variants }; + // Drop the reference key — the resolved copy declares variants literally. + const { variants_from: _from, ...rest } = group; + resolved[key] = { ...rest, variants }; } return resolved; }