From 58028eaf5623a1bde388795be507bd056ac12f3c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Fri, 17 Jul 2026 15:20:19 -0400 Subject: [PATCH] fix(#2341): de-dup Cursor / menu by marking skills user-invocable:false (#2386) Cursor installs both a skills and a commands surface and shows both in '/', duplicating every /gsd-*. Extend the #789 CodeBuddy de-dup to Cursor: convertClaudeCommandToCursorSkill (in both src and the live bin/install.js) now emits user-invocable:false, so the skill stays model-invocable while the commands surface is the single '/' entry point. Closes #2341. Admin-merged (self-review bypass) with full green CI. --- .changeset/brave-koalas-glide.md | 5 + bin/install.js | 8 +- docs/how-to/install-on-your-runtime.md | 2 +- src/runtime-artifact-conversion.cts | 8 +- .../golden-install-parity/cursor.json | 142 +++++++++--------- tests/install-runtime-artifacts.test.cjs | 52 +++++++ 6 files changed, 143 insertions(+), 74 deletions(-) create mode 100644 .changeset/brave-koalas-glide.md diff --git a/.changeset/brave-koalas-glide.md b/.changeset/brave-koalas-glide.md new file mode 100644 index 000000000..c074d4da4 --- /dev/null +++ b/.changeset/brave-koalas-glide.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 2386 +--- +**Cursor no longer shows every `/gsd-*` command twice** — a `--cursor` install wrote both a skill and a slash command for each action, so every GSD entry appeared twice in Cursor's `/` menu. GSD now installs Cursor skills as `user-invocable: false` (matching the existing CodeBuddy behavior), so the slash command is the single `/` entry point while skills remain model-invocable. (#2341) diff --git a/bin/install.js b/bin/install.js index a7b389a19..e22236a10 100755 --- a/bin/install.js +++ b/bin/install.js @@ -2565,7 +2565,13 @@ function convertClaudeCommandToCursorSkill(content, skillName) { const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; const adapter = getCursorSkillAdapterHeader(skillName); - return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; + // #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/' + // menu (it defaults to true). Cursor also writes a commands/ surface (#785), + // and surfacing both duplicated every /gsd-* entry. This mirrors the #789 + // CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills + // stay model-invocable background knowledge. (user-invocable:false hides from + // '/' while keeping model invocation — distinct from disable-model-invocation.) + return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`; } /** diff --git a/docs/how-to/install-on-your-runtime.md b/docs/how-to/install-on-your-runtime.md index b04ebdfb4..75409f259 100644 --- a/docs/how-to/install-on-your-runtime.md +++ b/docs/how-to/install-on-your-runtime.md @@ -276,7 +276,7 @@ COPILOT_CONFIG_DIR=~/.copilot-alt npx @opengsd/gsd-core@latest --copilot --globa npx @opengsd/gsd-core@latest --cursor --global ``` -Skills land in `~/.cursor/`. GSD installs skills, agents, and rule references. +Artifacts land in `~/.cursor/`. GSD installs slash commands (`~/.cursor/commands/gsd-*.md`), skills (`~/.cursor/skills/gsd-*/SKILL.md`), agents, and rule references. Each GSD action appears once in Cursor's `/` menu: the command surface is the single `/` entry point, and the skills are installed with `user-invocable: false` so they stay model-invocable background knowledge without duplicating the `/` entries. **Override the install directory:** diff --git a/src/runtime-artifact-conversion.cts b/src/runtime-artifact-conversion.cts index 76fadeabc..292d29d3b 100644 --- a/src/runtime-artifact-conversion.cts +++ b/src/runtime-artifact-conversion.cts @@ -954,7 +954,13 @@ function convertClaudeCommandToCursorSkill(content, skillName) { const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description; const adapter = getCursorSkillAdapterHeader(skillName); - return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`; + // #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/' + // menu (it defaults to true). Cursor also writes a commands/ surface (#785), + // and surfacing both duplicated every /gsd-* entry. This mirrors the #789 + // CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills + // stay model-invocable background knowledge. (user-invocable:false hides from + // '/' while keeping model invocation — distinct from disable-model-invocation.) + return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`; } /** diff --git a/tests/fixtures/golden-install-parity/cursor.json b/tests/fixtures/golden-install-parity/cursor.json index 647fc3139..2f4d16eae 100644 --- a/tests/fixtures/golden-install-parity/cursor.json +++ b/tests/fixtures/golden-install-parity/cursor.json @@ -402,75 +402,75 @@ "scripts/gen-loop-host-contract.cjs": "c7f15237234811a0", "scripts/lib/allowlist-ratchet.cjs": "ffaceaac3efc2660", "scripts/lib/cli-exit.cjs": "612d0c372c75b7e7", - "skills/gsd-add-tests/SKILL.md": "2cfc1922a2c0f308", - "skills/gsd-ai-integration-phase/SKILL.md": "de0a4cba5ad1651e", - "skills/gsd-audit-fix/SKILL.md": "6f9986ff00316053", - "skills/gsd-audit-milestone/SKILL.md": "44f048f405be71d6", - "skills/gsd-audit-uat/SKILL.md": "df085a31c102fb9e", - "skills/gsd-autonomous/SKILL.md": "446aadb5cfbf52c6", - "skills/gsd-capture/SKILL.md": "9f6b83a85441693c", - "skills/gsd-cleanup/SKILL.md": "03ba6edc18b933d3", - "skills/gsd-code-review/SKILL.md": "6add7a9afc1f6a97", - "skills/gsd-complete-milestone/SKILL.md": "e10a29da26ba0bd4", - "skills/gsd-config/SKILL.md": "0284ff80c1ed0898", - "skills/gsd-debug/SKILL.md": "4b43729be1dc2eaf", - "skills/gsd-discuss-phase/SKILL.md": "c985a6f4c161fb4d", - "skills/gsd-docs-update/SKILL.md": "670c98be5994cb11", - "skills/gsd-eval-review/SKILL.md": "bb375b7ebbc416af", - "skills/gsd-execute-phase/SKILL.md": "695061c3dfea5b2c", - "skills/gsd-explore/SKILL.md": "34681d40134229b5", - "skills/gsd-extract-learnings/SKILL.md": "3073d3a5fe7ebd07", - "skills/gsd-fast/SKILL.md": "83eba8ec0f933cbe", - "skills/gsd-forensics/SKILL.md": "7375ebd3a971da09", - "skills/gsd-graphify/SKILL.md": "721039d5f186982d", - "skills/gsd-health/SKILL.md": "614dc5d4f9a6bc63", - "skills/gsd-help/SKILL.md": "e346f3202ffae1a1", - "skills/gsd-import/SKILL.md": "308a5ecc47871055", - "skills/gsd-inbox/SKILL.md": "f9b4ff17883ee7e4", - "skills/gsd-ingest-docs/SKILL.md": "01ed9183a00252ee", - "skills/gsd-manager/SKILL.md": "62cd9f671a39e8a4", - "skills/gsd-map-codebase/SKILL.md": "383559aca64201e8", - "skills/gsd-mempalace-capture/SKILL.md": "f14977d3f78291c6", - "skills/gsd-mempalace-recall/SKILL.md": "56f3037091a8bd9a", - "skills/gsd-milestone-summary/SKILL.md": "c5dbbc8edb97a7f7", - "skills/gsd-mvp-phase/SKILL.md": "9d8e2999fa9f7830", - "skills/gsd-new-milestone/SKILL.md": "642d0741c89b911a", - "skills/gsd-new-project/SKILL.md": "18f587ac0b390d94", - "skills/gsd-next/SKILL.md": "75ef1905cb7425e1", - "skills/gsd-ns-context/SKILL.md": "dcf5711653d57354", - "skills/gsd-ns-ideate/SKILL.md": "0114f0bc5501c1e3", - "skills/gsd-ns-manage/SKILL.md": "f3232d05d5263f5b", - "skills/gsd-ns-project/SKILL.md": "81d802dd3ee2e1ff", - "skills/gsd-ns-review/SKILL.md": "8c686e92f293bfdd", - "skills/gsd-ns-workflow/SKILL.md": "c379192bd17d47d7", - "skills/gsd-onboard/SKILL.md": "cae1f9382469fa00", - "skills/gsd-pause-work/SKILL.md": "10e7531a2cb392ad", - "skills/gsd-phase/SKILL.md": "1a2c9b64c1af7ffc", - "skills/gsd-plan-phase/SKILL.md": "b7846866b27c6d3f", - "skills/gsd-plan-review-convergence/SKILL.md": "8e55611cdbd4715c", - "skills/gsd-pr-branch/SKILL.md": "725727e3c2b66543", - "skills/gsd-profile-user/SKILL.md": "eb6842b23f1f9256", - "skills/gsd-progress/SKILL.md": "117243aef3e64b85", - "skills/gsd-quick/SKILL.md": "0fdc1838759e2d25", - "skills/gsd-resume-work/SKILL.md": "492e402cb66b1fcf", - "skills/gsd-review-backlog/SKILL.md": "cf7ff999c96d2f76", - "skills/gsd-review/SKILL.md": "f5ebef9d2d12b2ad", - "skills/gsd-secure-phase/SKILL.md": "970f53251c7adff7", - "skills/gsd-settings/SKILL.md": "c06a8c93de64314e", - "skills/gsd-ship/SKILL.md": "c7eeb21a15c66189", - "skills/gsd-sketch/SKILL.md": "44605f8ec5808162", - "skills/gsd-spec-phase/SKILL.md": "5c48117fbb7aa595", - "skills/gsd-spike/SKILL.md": "3850674027d81c2a", - "skills/gsd-stats/SKILL.md": "ebbd0d39b5cee9a9", - "skills/gsd-surface/SKILL.md": "9b869434321867c3", - "skills/gsd-thread/SKILL.md": "621cde6272262eef", - "skills/gsd-ui-phase/SKILL.md": "505ad61ef61c9e9c", - "skills/gsd-ui-review/SKILL.md": "dc79fe0ad42f4948", - "skills/gsd-ultraplan-phase/SKILL.md": "51c2fe437b7d9e22", - "skills/gsd-undo/SKILL.md": "886857ca7dbf15a5", - "skills/gsd-update/SKILL.md": "d87ddffc97e68ec5", - "skills/gsd-validate-phase/SKILL.md": "9148179afa585dad", - "skills/gsd-verify-work/SKILL.md": "28367b353ddfb65d", - "skills/gsd-workspace/SKILL.md": "9b1cf8be726b7b3a", - "skills/gsd-workstreams/SKILL.md": "55a94c63e32f85a6" + "skills/gsd-add-tests/SKILL.md": "1719810d0f8dd79e", + "skills/gsd-ai-integration-phase/SKILL.md": "9c8242023a8bcc29", + "skills/gsd-audit-fix/SKILL.md": "2d2ee3894efbee1c", + "skills/gsd-audit-milestone/SKILL.md": "d953bd7170402fc3", + "skills/gsd-audit-uat/SKILL.md": "97794348d617f873", + "skills/gsd-autonomous/SKILL.md": "93eaad5fa3cb4d5a", + "skills/gsd-capture/SKILL.md": "0a3e8e1ec622ac20", + "skills/gsd-cleanup/SKILL.md": "1c15f1f846614fdf", + "skills/gsd-code-review/SKILL.md": "20510d2cdc0a087b", + "skills/gsd-complete-milestone/SKILL.md": "409785384c03e9a1", + "skills/gsd-config/SKILL.md": "bfb2c9a68658ea8a", + "skills/gsd-debug/SKILL.md": "9b9bd960dec805a6", + "skills/gsd-discuss-phase/SKILL.md": "528dde84ab54176b", + "skills/gsd-docs-update/SKILL.md": "be12b6b48447df38", + "skills/gsd-eval-review/SKILL.md": "892d1f5fb5665941", + "skills/gsd-execute-phase/SKILL.md": "e866ad459c9b0f5b", + "skills/gsd-explore/SKILL.md": "68c3b41f8203e7d4", + "skills/gsd-extract-learnings/SKILL.md": "9356725f248182f1", + "skills/gsd-fast/SKILL.md": "7348338dca150e4b", + "skills/gsd-forensics/SKILL.md": "dca13ed03d434edc", + "skills/gsd-graphify/SKILL.md": "1dd14bfcdb6d0f1b", + "skills/gsd-health/SKILL.md": "6b18ac9d649f63a8", + "skills/gsd-help/SKILL.md": "d159ee08c7fc95a4", + "skills/gsd-import/SKILL.md": "01ebb01767d47e8b", + "skills/gsd-inbox/SKILL.md": "8f7e1c95413968d5", + "skills/gsd-ingest-docs/SKILL.md": "1a2e87caa9011682", + "skills/gsd-manager/SKILL.md": "8a506b1b8b3841c3", + "skills/gsd-map-codebase/SKILL.md": "74a827b893d70dfa", + "skills/gsd-mempalace-capture/SKILL.md": "b5ad8d540b6320be", + "skills/gsd-mempalace-recall/SKILL.md": "04364d3012644204", + "skills/gsd-milestone-summary/SKILL.md": "1d0b245b74176236", + "skills/gsd-mvp-phase/SKILL.md": "485356f67c196015", + "skills/gsd-new-milestone/SKILL.md": "882af2f4995929b7", + "skills/gsd-new-project/SKILL.md": "daad247764b4e466", + "skills/gsd-next/SKILL.md": "ef4221025da5ed3a", + "skills/gsd-ns-context/SKILL.md": "adf3509008907983", + "skills/gsd-ns-ideate/SKILL.md": "701590ef59fefa9c", + "skills/gsd-ns-manage/SKILL.md": "2820ca89ec707808", + "skills/gsd-ns-project/SKILL.md": "799f9651be83ec24", + "skills/gsd-ns-review/SKILL.md": "1dac39c855555b0b", + "skills/gsd-ns-workflow/SKILL.md": "975c89ee2846b40c", + "skills/gsd-onboard/SKILL.md": "c7f216749ba4c09c", + "skills/gsd-pause-work/SKILL.md": "7d310e7329b7ae7d", + "skills/gsd-phase/SKILL.md": "49fca93e5f2ccc70", + "skills/gsd-plan-phase/SKILL.md": "62dc688c24e59f18", + "skills/gsd-plan-review-convergence/SKILL.md": "c2793cacf3f40d70", + "skills/gsd-pr-branch/SKILL.md": "a8f8838a6aa5e1dd", + "skills/gsd-profile-user/SKILL.md": "3be8d50399836ef2", + "skills/gsd-progress/SKILL.md": "1245ea9e6af6ae25", + "skills/gsd-quick/SKILL.md": "05670e9238811a5f", + "skills/gsd-resume-work/SKILL.md": "6c8b592295a2edc9", + "skills/gsd-review-backlog/SKILL.md": "b1070503042ddb13", + "skills/gsd-review/SKILL.md": "27bb3cfc1f54f650", + "skills/gsd-secure-phase/SKILL.md": "02828a6254eae50e", + "skills/gsd-settings/SKILL.md": "8f32c4a4085bc72a", + "skills/gsd-ship/SKILL.md": "c54eb2511f8e8417", + "skills/gsd-sketch/SKILL.md": "a1ce42548d7287bd", + "skills/gsd-spec-phase/SKILL.md": "7cfdb9fc879d9b6b", + "skills/gsd-spike/SKILL.md": "c9b2e4ab15ade118", + "skills/gsd-stats/SKILL.md": "5796e221ab9ab7ae", + "skills/gsd-surface/SKILL.md": "d219dd20380caa2f", + "skills/gsd-thread/SKILL.md": "a73159bc8269dd68", + "skills/gsd-ui-phase/SKILL.md": "c69cbdf977e2a840", + "skills/gsd-ui-review/SKILL.md": "fbed8745f1abb289", + "skills/gsd-ultraplan-phase/SKILL.md": "749373bb067636fa", + "skills/gsd-undo/SKILL.md": "6ebcaaaea575bf85", + "skills/gsd-update/SKILL.md": "df643c197bd9ad5e", + "skills/gsd-validate-phase/SKILL.md": "10a5d3f8de0d9683", + "skills/gsd-verify-work/SKILL.md": "eec11aab679a8116", + "skills/gsd-workspace/SKILL.md": "c2e42b0b5c105991", + "skills/gsd-workstreams/SKILL.md": "14f44c061fef78f8" } diff --git a/tests/install-runtime-artifacts.test.cjs b/tests/install-runtime-artifacts.test.cjs index 6b1498bb5..728c49060 100644 --- a/tests/install-runtime-artifacts.test.cjs +++ b/tests/install-runtime-artifacts.test.cjs @@ -2046,6 +2046,7 @@ const { createTempDir, cleanup } = require('./helpers.cjs'); const { convertClaudeCommandToCodebuddyCommand, convertClaudeCommandToCodebuddySkill, + convertClaudeCommandToCursorSkill, } = require('../bin/install.js'); const { @@ -2234,6 +2235,57 @@ describe('enh-789 — installRuntimeArtifacts codebuddy emits commands and skill }); }); +// ─── #2341: extend the #789 de-dup to Cursor ───────────────────────────────── +// Cursor installs BOTH a skills surface and a commands surface (#785/#803), and +// surfaces both in its '/' menu — so every /gsd-* appeared twice. The #789 fix +// (skills user-invocable:false → model-invocable but out of '/') was scoped to +// CodeBuddy only and never applied to Cursor. Cursor honors the same SKILL.md +// `user-invocable` convention (verified: user-invocable:false hides a skill from +// '/' while keeping it model-invocable, distinct from disable-model-invocation). +// Fix: emit Cursor skills with user-invocable:false so commands are the single +// '/' entry point. +describe('fix-2341 — Cursor skills marked user-invocable:false', () => { + test('convertClaudeCommandToCursorSkill emits user-invocable: false', () => { + const src = [ + '---', + 'name: gsd:help', + 'description: Show help', + '---', + '', + '# body', + '', + ].join('\n'); + const out = convertClaudeCommandToCursorSkill(src, 'gsd-help'); + assert.ok(/^user-invocable:\s*false\s*$/m.test(out), + `Cursor SKILL.md frontmatter must hide skill from '/' menu (user-invocable: false). Got:\n${out}`); + }); + + test('installed cursor skills/gsd-help/SKILL.md is hidden from the / menu', (t) => { + const configDir = createTempDir('gsd-fix2341-skillhide-'); + t.after(() => cleanup(configDir)); + + installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE); + + const skill = fs.readFileSync(path.join(configDir, 'skills', 'gsd-help', 'SKILL.md'), 'utf8'); + assert.ok(/^user-invocable:\s*false\s*$/m.test(skill), + 'installed Cursor SKILL.md must set user-invocable: false so it is not a duplicate / entry'); + }); + + test('cursor still installs the commands surface (the single / entry point)', (t) => { + const configDir = createTempDir('gsd-fix2341-cmd-'); + t.after(() => cleanup(configDir)); + + installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE); + + // The commands surface stays user-invocable — de-dup hides the skill, not the command. + assert.ok(fs.existsSync(path.join(configDir, 'commands', 'gsd-help.md')), + 'commands/gsd-help.md (the / entry point) must still be installed'); + const cmd = fs.readFileSync(path.join(configDir, 'commands', 'gsd-help.md'), 'utf8'); + assert.ok(!/^user-invocable:\s*false\s*$/m.test(cmd), + 'the command surface must remain user-invocable (only the skill is hidden)'); + }); +}); + // ─── Uninstall contract ────────────────────────────────────────────────────── describe('enh-789 — uninstallRuntimeArtifacts removes codebuddy commands', () => {