fix(#2644): deduplicate Cursor slash menu (#2812)

* fix(#2644): deduplicate Cursor slash menu

* fix: preserve installer executable mode

* chore: add changeset for PR #2812

* test(#2644): acknowledge Cursor emission changes

* test(#2644): drop spent emitted drift acknowledgments

* fix(#2644): remove retired Cursor command converter

---------

Co-authored-by: clezcoding <clezcoding@users.noreply.github.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
𝚌𝚕𝚎𝚣𝚌𝚘𝚍𝚒𝚗𝚐
2026-08-03 18:05:45 +02:00
committed by GitHub
parent 067a4d1c6c
commit 88f6d9bd1b
29 changed files with 727 additions and 360 deletions

View File

@@ -4041,9 +4041,9 @@ describe('ADR-1016 phase 5a: closed-vocab set exports', () => {
// ─── 25. ADR-857 phase 5e: closed ConverterName enum (Part B) ─────────────────
describe('ADR-857 phase 5e: VALID_CONVERTER_NAMES closed enum', () => {
test('VALID_CONVERTER_NAMES has exactly 27 entries (17 command/skill/workflow + 10 agent converters)', () => {
test('VALID_CONVERTER_NAMES has exactly 26 entries (16 command/skill/workflow + 10 agent converters)', () => {
assert.ok(VALID_CONVERTER_NAMES instanceof Set, 'VALID_CONVERTER_NAMES must be a Set');
assert.strictEqual(VALID_CONVERTER_NAMES.size, 27, 'VALID_CONVERTER_NAMES must have exactly 27 entries, got: ' + VALID_CONVERTER_NAMES.size);
assert.strictEqual(VALID_CONVERTER_NAMES.size, 26, 'VALID_CONVERTER_NAMES must have exactly 26 entries, got: ' + VALID_CONVERTER_NAMES.size);
});
test('VALID_CONVERTER_NAMES contains all expected converter names', () => {
@@ -4057,7 +4057,6 @@ describe('ADR-857 phase 5e: VALID_CONVERTER_NAMES closed enum', () => {
'convertClaudeCommandToCodebuddySkill',
'convertClaudeCommandToCodexSkill',
'convertClaudeCommandToCopilotSkill',
'convertClaudeCommandToCursorCommand',
'convertClaudeCommandToCursorSkill',
'convertClaudeCommandToKiloSkill',
'convertClaudeCommandToKimiSkill',

View File

@@ -4,9 +4,6 @@
* Ensures Cursor frontmatter names are emitted as plain identifiers
* (without surrounding quotes), so Cursor does not treat quotes as
* literal parts of skill/subagent names.
*
* Also covers convertClaudeCommandToCursorCommand (#785 — Cursor 1.6
* slash commands via .cursor/commands/).
*/
process.env.GSD_TEST_MODE = '1';
@@ -17,7 +14,6 @@ const assert = require('node:assert/strict');
const {
convertClaudeCommandToCursorSkill,
convertClaudeAgentToCursorAgent,
convertClaudeCommandToCursorCommand,
_applyRuntimeRewrites,
} = require('../bin/install.js');
@@ -85,74 +81,6 @@ Planner body
});
});
// ─── convertClaudeCommandToCursorCommand (#785) ───────────────────────────────
describe('convertClaudeCommandToCursorCommand (#785 — Cursor 1.6 .cursor/commands/)', () => {
test('strips YAML frontmatter — output is plain markdown', () => {
const input = `---
name: help
description: Show help for GSD commands
---
# GSD Help
Use \`/gsd-help\` to see available commands.
`;
const result = convertClaudeCommandToCursorCommand(input);
assert.ok(!result.startsWith('---'), 'cursor commands must not have YAML frontmatter');
assert.ok(!result.includes('name: help'), 'name field must be stripped');
assert.ok(!result.includes('description:'), 'description field must be stripped');
assert.ok(result.includes('GSD Help'), 'body content must be preserved');
});
test('applies convertClaudeToCursorMarkdown transforms (Bash → Shell, Claude Code → Cursor)', () => {
const input = `---
name: quick
description: Quick task
---
Use Bash( to run commands.
This runs in Claude Code.
`;
const result = convertClaudeCommandToCursorCommand(input);
assert.ok(result.includes('Shell('), 'Bash( should be renamed to Shell(');
assert.ok(!result.includes('Claude Code'), 'Claude Code brand reference should be replaced');
assert.ok(result.includes('Cursor'), 'should reference Cursor instead');
});
test('normalizes gsd: colon slash commands to gsd- hyphen form', () => {
const input = `---
name: plan-phase
description: Plan a phase
---
Next step: /gsd:execute-phase 17
`;
const result = convertClaudeCommandToCursorCommand(input);
assert.ok(result.includes('/gsd-execute-phase 17'), 'colon form should become hyphen form');
assert.ok(!result.includes('/gsd:execute-phase'), 'colon form should be removed');
});
test('handles input with no frontmatter gracefully', () => {
const input = `# No Frontmatter Command
Some body content.
`;
const result = convertClaudeCommandToCursorCommand(input);
assert.ok(!result.startsWith('---'), 'output must not start with ---');
assert.ok(result.includes('No Frontmatter Command'), 'body should be preserved');
});
test('is exported from install.js', () => {
assert.strictEqual(typeof convertClaudeCommandToCursorCommand, 'function',
'convertClaudeCommandToCursorCommand must be exported from install.js');
});
});
// ─── _applyRuntimeRewrites(cursor) — bare-form regression (#1356) ────────────
//
// Prior to this fix the cursor branch only rewrote trailing-slash ~/.claude/

View File

@@ -108,7 +108,11 @@ test('cursor descriptor declares runtime.hostBehaviors (the folded-in behaviors)
assert.equal(hb.frontmatterDialect, 'cursor');
assert.equal(hb.hooksJsonSurface, true);
assert.equal(hb.skipSharedHooksInstall, true);
assert.equal(hb.reportCommandsDir, true);
assert.deepEqual(hb.retiredArtifacts, [
{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' },
]);
assert.equal(hb.reportCommandsDir, undefined,
'Cursor no longer installs a parallel commands/ surface (#2644)');
assert.ok(Array.isArray(hb.managedHookEvents) && hb.managedHookEvents.length >= 6,
'managedHookEvents must list at least 6 events (AC4a)');
});

View File

@@ -12,8 +12,9 @@
* already descriptor-driven except for two residual `isCodebuddy` branches
* in bin/install.js:
* 1. a duplicate `commands/` slash-command output report (byte-identical
* to the generic `hostBehaviors.reportCommandsDir` block already used
* by Cursor) — folded onto `hostBehaviors.reportCommandsDir` and deleted.
* to the generic `hostBehaviors.reportCommandsDir` block) — folded onto
* `hostBehaviors.reportCommandsDir` and deleted. Cursor retired that
* parallel commands surface later in #2644.
* 2. a dead legacy agent-converter dispatch arm in the inline agent-copy
* loop, unreachable because codebuddy is a member of
* `_DESCRIPTOR_AGENTS_RUNTIMES` (installRuntimeArtifacts already wrote

View File

@@ -0,0 +1,147 @@
{
"version": 1,
"paths": {
"commands/gsd-add-tests.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ai-integration-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-audit-fix.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-audit-milestone.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-audit-uat.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-autonomous.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-capture.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-cleanup.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-code-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-complete-milestone.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-config.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-debug.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-discuss-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-docs-update.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-eval-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-execute-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-explore.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-extract-learnings.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-fast.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-forensics.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-graphify.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-health.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-help.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-import.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-inbox.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ingest-docs.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-manager.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-map-codebase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-mempalace-capture.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-mempalace-recall.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-milestone-summary.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-mvp-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-new-milestone.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-new-project.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-next.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ns-context.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ns-ideate.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ns-manage.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ns-project.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ns-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ns-workflow.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-onboard.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-pause-work.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-plan-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-plan-review-convergence.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-pr-branch.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-profile-user.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-progress.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-quick.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-resume-work.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-review-backlog.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-secure-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-settings.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ship.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-sketch.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-spec-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-spike.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-stats.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-surface.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-thread.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ui-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ui-review.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-ultraplan-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-undo.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-update.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-validate-phase.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-verify-work.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-workspace.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"commands/gsd-workstreams.md": "Cursor deliberately retires its duplicate commands surface in favor of skills (#2644).",
"skills/gsd-add-tests/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ai-integration-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-audit-fix/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-audit-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-audit-uat/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-autonomous/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-capture/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-cleanup/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-code-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-complete-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-config/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-debug/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-discuss-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-docs-update/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-eval-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-execute-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-explore/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-extract-learnings/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-fast/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-forensics/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-graphify/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-health/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-help/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-import/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-inbox/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ingest-docs/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-manager/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-map-codebase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-mempalace-capture/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-mempalace-recall/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-milestone-summary/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-mvp-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-new-milestone/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-new-project/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-next/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ns-context/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ns-ideate/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ns-manage/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ns-project/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ns-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ns-workflow/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-onboard/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-pause-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-plan-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-plan-review-convergence/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-pr-branch/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-profile-user/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-progress/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-quick/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-resume-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-review-backlog/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-secure-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-settings/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ship/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-sketch/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-spec-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-spike/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-stats/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-surface/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-thread/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ui-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ui-review/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-ultraplan-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-undo/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-update/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-validate-phase/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-verify-work/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-workspace/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644).",
"skills/gsd-workstreams/SKILL.md": "Cursor skill frontmatter deliberately drops the unsupported user-invocable field while retaining model invocation (#2644)."
}
}

View File

@@ -35,77 +35,6 @@
"agents/gsd-ui-researcher.md",
"agents/gsd-user-profiler.md",
"agents/gsd-verifier.md",
"commands/gsd-add-tests.md",
"commands/gsd-ai-integration-phase.md",
"commands/gsd-audit-fix.md",
"commands/gsd-audit-milestone.md",
"commands/gsd-audit-uat.md",
"commands/gsd-autonomous.md",
"commands/gsd-capture.md",
"commands/gsd-cleanup.md",
"commands/gsd-code-review.md",
"commands/gsd-complete-milestone.md",
"commands/gsd-config.md",
"commands/gsd-debug.md",
"commands/gsd-discuss-phase.md",
"commands/gsd-docs-update.md",
"commands/gsd-eval-review.md",
"commands/gsd-execute-phase.md",
"commands/gsd-explore.md",
"commands/gsd-extract-learnings.md",
"commands/gsd-fast.md",
"commands/gsd-forensics.md",
"commands/gsd-graphify.md",
"commands/gsd-health.md",
"commands/gsd-help.md",
"commands/gsd-import.md",
"commands/gsd-inbox.md",
"commands/gsd-ingest-docs.md",
"commands/gsd-manager.md",
"commands/gsd-map-codebase.md",
"commands/gsd-mempalace-capture.md",
"commands/gsd-mempalace-recall.md",
"commands/gsd-milestone-summary.md",
"commands/gsd-mvp-phase.md",
"commands/gsd-new-milestone.md",
"commands/gsd-new-project.md",
"commands/gsd-next.md",
"commands/gsd-ns-context.md",
"commands/gsd-ns-ideate.md",
"commands/gsd-ns-manage.md",
"commands/gsd-ns-project.md",
"commands/gsd-ns-review.md",
"commands/gsd-ns-workflow.md",
"commands/gsd-onboard.md",
"commands/gsd-pause-work.md",
"commands/gsd-phase.md",
"commands/gsd-plan-phase.md",
"commands/gsd-plan-review-convergence.md",
"commands/gsd-pr-branch.md",
"commands/gsd-profile-user.md",
"commands/gsd-progress.md",
"commands/gsd-quick.md",
"commands/gsd-resume-work.md",
"commands/gsd-review-backlog.md",
"commands/gsd-review.md",
"commands/gsd-secure-phase.md",
"commands/gsd-settings.md",
"commands/gsd-ship.md",
"commands/gsd-sketch.md",
"commands/gsd-spec-phase.md",
"commands/gsd-spike.md",
"commands/gsd-stats.md",
"commands/gsd-surface.md",
"commands/gsd-thread.md",
"commands/gsd-ui-phase.md",
"commands/gsd-ui-review.md",
"commands/gsd-ultraplan-phase.md",
"commands/gsd-undo.md",
"commands/gsd-update.md",
"commands/gsd-validate-phase.md",
"commands/gsd-verify-work.md",
"commands/gsd-workspace.md",
"commands/gsd-workstreams.md",
"gsd-core/.gsd-runtime",
"gsd-core/VERSION",
"gsd-core/bin/check-latest-version.cjs",

View File

@@ -23,6 +23,7 @@ const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const crypto = require('node:crypto');
const { createTempDir, cleanup } = require('./helpers.cjs');
@@ -40,6 +41,8 @@ const {
resolveRuntimeArtifactLayout,
} = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const { applySurface } = require('../gsd-core/bin/lib/surface.cjs');
const {
loadSkillsManifest,
resolveProfile,
@@ -105,13 +108,10 @@ describe('installRuntimeArtifacts — consumes Runtime Artifact Install Plan Mod
});
t.after(restore);
// #1928: gemini (the only runtime whose 'commands' kind used a
// namespaced-by-dir 'commands/gsd' layout) was removed; cursor is the
// stand-in — it has a 'commands' kind but prefixes files 'gsd-'
// (destSubpath basename !== prefix stem), so proof.md → gsd-proof.md.
installer.installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE);
// Augment has a flat commands kind and prefixes proof.md as gsd-proof.md.
installer.installRuntimeArtifacts('augment', configDir, 'global', RESOLVED_CORE);
assert.strictEqual(planArgs.layout.runtime, 'cursor');
assert.strictEqual(planArgs.layout.runtime, 'augment');
assert.strictEqual(planArgs.layout.configDir, configDir);
assert.strictEqual(planArgs.layout.scope, 'global');
assert.strictEqual(planArgs.resolvedProfile, RESOLVED_CORE);
@@ -288,28 +288,19 @@ describe('installRuntimeArtifacts — hermes nested layout', () => {
});
});
describe('installRuntimeArtifacts — cursor commands layout (#785)', () => {
test('cursor: skills/ AND commands/ both created; commands/gsd-help.md is plain markdown', (t) => {
describe('installRuntimeArtifacts — cursor skills-only layout (#2644)', () => {
test('cursor: skills/ is created and commands/ is not materialized', (t) => {
const configDir = createTempDir('gsd-ial-cursor-cmds-');
t.after(() => cleanup(configDir));
installRuntimeArtifacts('cursor', configDir, 'global', RESOLVED_CORE);
// Existing skills kind still present
const skillsDir = path.join(configDir, 'skills');
assert.ok(fs.existsSync(skillsDir), 'skills/ must exist');
assert.ok(fs.existsSync(path.join(skillsDir, 'gsd-help', 'SKILL.md')),
'skills/gsd-help/SKILL.md must exist');
// New commands kind (#785)
const commandsDir = path.join(configDir, 'commands');
assert.ok(fs.existsSync(commandsDir), 'commands/ must exist (#785)');
assert.ok(fs.existsSync(path.join(commandsDir, 'gsd-help.md')),
'commands/gsd-help.md must exist (#785)');
// Cursor commands are plain markdown — no YAML frontmatter
const helpContent = fs.readFileSync(path.join(commandsDir, 'gsd-help.md'), 'utf8');
assert.ok(!helpContent.startsWith('---'), 'cursor commands must not start with YAML frontmatter');
assert.ok(!fs.existsSync(path.join(configDir, 'commands')),
'Cursor must not materialize commands/ because skills already populate the slash menu');
});
});
@@ -750,9 +741,9 @@ describe('uninstallRuntimeArtifacts — consumes Runtime Artifact Uninstall Plan
});
t.after(restore);
installer.uninstallRuntimeArtifacts('cursor', configDir, 'global');
installer.uninstallRuntimeArtifacts('augment', configDir, 'global');
assert.strictEqual(planLayout.runtime, 'cursor');
assert.strictEqual(planLayout.runtime, 'augment');
assert.strictEqual(planLayout.configDir, configDir);
assert.strictEqual(planLayout.scope, 'global');
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')));
@@ -2494,17 +2485,9 @@ 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', () => {
// ─── #2644: Cursor skills are the one slash + model surface ──────────────────
describe('fix-2644 — Cursor has one menu entry per GSD workflow', () => {
test('convertClaudeCommandToCursorSkill emits only supported invocation metadata', () => {
const src = [
'---',
'name: gsd:help',
@@ -2515,33 +2498,93 @@ describe('fix-2341 — Cursor skills marked user-invocable:false', () => {
'',
].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}`);
assert.ok(!/^user-invocable:/m.test(out),
`Cursor does not support user-invocable; the field must not be emitted. Got:\n${out}`);
assert.ok(!/^disable-model-invocation:/m.test(out),
'Cursor skill must remain available for contextual model invocation');
});
test('installed cursor skills/gsd-help/SKILL.md is hidden from the / menu', (t) => {
const configDir = createTempDir('gsd-fix2341-skillhide-');
test('fresh install keeps the skill slash/model surface and omits commands', (t) => {
const configDir = createTempDir('gsd-fix2644-fresh-');
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');
assert.ok(!/^user-invocable:/m.test(skill));
assert.ok(!/^disable-model-invocation:/m.test(skill));
assert.ok(!fs.existsSync(path.join(configDir, 'commands', 'gsd-help.md')));
});
test('cursor still installs the commands surface (the single / entry point)', (t) => {
const configDir = createTempDir('gsd-fix2341-cmd-');
test('reinstall removes manifest-proven legacy commands and preserves unknown files', (t) => {
const configDir = createTempDir('gsd-fix2644-upgrade-');
t.after(() => cleanup(configDir));
const commandsDir = path.join(configDir, 'commands');
fs.mkdirSync(commandsDir, { recursive: true });
const managedContent = '# old generated help\n';
fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), managedContent);
fs.writeFileSync(path.join(commandsDir, 'gsd-my-custom.md'), '# user command\n');
fs.writeFileSync(path.join(configDir, 'gsd-file-manifest.json'), JSON.stringify({
version: '1.8.0',
timestamp: '2026-07-28T00:00:00.000Z',
mode: 'full',
files: {
'commands/gsd-help.md': crypto.createHash('sha256').update(managedContent).digest('hex'),
},
}));
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)');
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')),
'manifest-proven legacy command must be retired');
assert.ok(fs.existsSync(path.join(commandsDir, 'gsd-my-custom.md')),
'unmanifested user command must be preserved');
});
test('profile surface apply also retires a manifest-proven legacy command', (t) => {
const configDir = createTempDir('gsd-fix2644-surface-');
t.after(() => cleanup(configDir));
const commandsDir = path.join(configDir, 'commands');
fs.mkdirSync(commandsDir, { recursive: true });
const content = '# old generated help\n';
fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), content);
fs.writeFileSync(path.join(configDir, 'gsd-file-manifest.json'), JSON.stringify({
version: '1.8.0', timestamp: '2026-07-28T00:00:00.000Z', mode: 'core',
files: { 'commands/gsd-help.md': crypto.createHash('sha256').update(content).digest('hex') },
}));
const layout = resolveRuntimeArtifactLayout('cursor', configDir, 'global');
applySurface(configDir, layout, MANIFEST);
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')),
'profile toggles must not leave or recreate the retired duplicate surface');
assert.ok(fs.existsSync(path.join(configDir, 'skills', 'gsd-help', 'SKILL.md')));
});
test('uninstall also retires manifest-proven legacy commands', (t) => {
const configDir = createTempDir('gsd-fix2644-uninstall-');
t.after(() => cleanup(configDir));
const commandsDir = path.join(configDir, 'commands');
fs.mkdirSync(commandsDir, { recursive: true });
const managedContent = '# old generated help\n';
fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), managedContent);
fs.writeFileSync(path.join(commandsDir, 'user-command.md'), '# user command\n');
fs.writeFileSync(path.join(configDir, 'gsd-file-manifest.json'), JSON.stringify({
version: '1.8.0', timestamp: '2026-07-28T00:00:00.000Z', mode: 'full',
files: {
'commands/gsd-help.md': crypto.createHash('sha256').update(managedContent).digest('hex'),
},
}));
uninstallRuntimeArtifacts('cursor', configDir, 'global');
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')),
'direct uninstall must remove a manifest-proven retired command');
assert.ok(fs.existsSync(path.join(commandsDir, 'user-command.md')),
'direct uninstall must preserve unknown user commands');
});
});

View File

@@ -1682,6 +1682,9 @@ test('shipped installer-migration checksums are locked to a committed baseline (
// never manifest-recorded — so the action declares its own classification.
'2026-07-28-retire-config-root-commonjs-marker':
'sha256:8f2140cbe8f2dd8f7dfd52a0f6957c5edfe966c52d7e6e4d74ec7366930e0e1d',
// Migration 008: retire Cursor's duplicate commands/ surface (#2644).
'2026-07-29-cursor-retire-commands-surface':
'sha256:d0b2b812a3f752650f2518b48280f74a5937c80ec8412bac493382dfa3db083f',
};
const { DEFAULT_MIGRATIONS_DIR, migrationChecksum: computeChecksum } = require('../gsd-core/bin/lib/installer-migrations.cjs');
@@ -1794,6 +1797,76 @@ test('reconciles a drifted applied-migration checksum into install state on appl
});
// ---------------------------------------------------------------------------
// Cursor duplicate commands-surface retirement (#2644)
// ---------------------------------------------------------------------------
{
const cursorCommandsMigration = require('../gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs');
test('migration 008 plans only manifest-managed gsd-*.md command files', (t) => {
const configDir = createTempInstall();
t.after(() => cleanup(configDir));
writeFile(configDir, 'commands/gsd-help.md', '# help\n');
writeFile(configDir, 'commands/gsd-custom.md', '# custom\n');
writeFile(configDir, 'commands/not-gsd.md', '# other\n');
const actions = cursorCommandsMigration.plan({
configDir,
classifyArtifact(relPath) {
if (relPath === 'commands/gsd-help.md') return { classification: 'managed-pristine' };
return { classification: 'unknown' };
},
});
assert.deepEqual(actions.map((action) => [action.type, action.relPath]), [
['remove-managed', 'commands/gsd-help.md'],
]);
});
test('migration 008 backs up a modified managed command and preserves an unknown neighbor', (t) => {
const configDir = createTempInstall();
t.after(() => cleanup(configDir));
writeFile(configDir, 'commands/gsd-help.md', '# locally modified help\n');
writeFile(configDir, 'commands/gsd-custom.md', '# user command\n');
writeManifest(configDir, {
'commands/gsd-help.md': sha256('# original generated help\n'),
});
const result = runInstallerMigrations({
configDir, runtime: 'cursor', scope: 'global', migrations: [cursorCommandsMigration],
now: () => '2026-07-29T00:00:00.000Z',
});
assert.equal(result.plan.actions[0].type, 'backup-and-remove');
assert.ok(!fs.existsSync(path.join(configDir, 'commands', 'gsd-help.md')));
assert.ok(fs.existsSync(path.join(configDir, 'commands', 'gsd-custom.md')),
'unmanifested user command must be preserved');
const journal = JSON.parse(fs.readFileSync(path.join(configDir, result.journalRelPath), 'utf8'));
assert.equal(
fs.readFileSync(path.join(configDir, journal.actions[0].backupRelPath), 'utf8'),
'# locally modified help\n',
);
});
test('migration 008 is scoped to Cursor for both global and local installs', (t) => {
for (const scope of ['global', 'local']) {
const configDir = createTempInstall();
t.after(() => cleanup(configDir));
writeFile(configDir, 'commands/gsd-help.md', '# help\n');
writeManifest(configDir, { 'commands/gsd-help.md': sha256('# help\n') });
const result = runInstallerMigrations({
configDir, runtime: 'cursor', scope, migrations: [cursorCommandsMigration],
now: () => `2026-07-29T00:00:0${scope === 'global' ? '0' : '1'}.000Z`,
});
assert.ok(!fs.existsSync(path.join(configDir, 'commands', 'gsd-help.md')),
`${scope} Cursor install must retire the duplicate command`);
assert.deepEqual(result.appliedMigrationIds, ['2026-07-29-cursor-retire-commands-surface']);
}
});
}
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3357-codex-legacy-hooks-json-migration.test.cjs — consolidation epic #1969 (B5 #1974)
// ────────────────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,155 @@
'use strict';
const { test } = require('node:test');
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const retiredArtifactCleanup = require('../gsd-core/bin/lib/retired-artifact-cleanup.cjs');
const capabilityRegistry = require('../gsd-core/bin/lib/capability-registry.cjs');
const cursorBehaviors = capabilityRegistry.runtimes.cursor.runtime.hostBehaviors;
function createFixture(t) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-retired-cleanup-'));
const configDir = path.join(root, 'config');
fs.mkdirSync(configDir, { recursive: true });
t.after(() => {
// eslint-disable-next-line local/no-raw-rmsync-in-tests -- isolated temp fixture cleanup
fs.rmSync(root, { recursive: true, force: true });
});
return { root, configDir };
}
function setDeclarations(t, declarations) {
const original = cursorBehaviors.retiredArtifacts;
cursorBehaviors.retiredArtifacts = declarations;
t.after(() => {
cursorBehaviors.retiredArtifacts = original;
});
}
function writeFile(root, relPath, content) {
const filePath = path.join(root, relPath);
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, content, 'utf8');
return filePath;
}
function sha256(content) {
return crypto.createHash('sha256').update(content).digest('hex');
}
function writeManifest(configDir, files) {
fs.writeFileSync(path.join(configDir, 'gsd-file-manifest.json'), JSON.stringify({
version: '1.8.0',
timestamp: '2026-07-28T00:00:00.000Z',
mode: 'full',
files,
}), 'utf8');
}
test('rejects malformed and escaping retired-artifact declarations', (t) => {
const { root, configDir } = createFixture(t);
const outsideFile = writeFile(root, 'escape/gsd-help.md', '# outside\n');
const localFile = writeFile(configDir, 'commands/gsd-help.md', '# local\n');
setDeclarations(t, [
{ destSubpath: null, prefix: 'gsd-', suffix: '.md' },
{ destSubpath: '../escape', prefix: 'gsd-', suffix: '.md' },
]);
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
assert.deepEqual(result, { removed: [], preserved: [] });
assert.ok(fs.existsSync(outsideFile), 'an escaping destination must never be touched');
assert.ok(fs.existsSync(localFile), 'a malformed declaration must not affect valid-looking files');
});
test('refuses a symlinked retired destination directory', (t) => {
const { root, configDir } = createFixture(t);
const targetDir = path.join(root, 'outside-commands');
const targetFile = writeFile(targetDir, 'gsd-help.md', '# outside\n');
const linkType = process.platform === 'win32' ? 'junction' : 'dir';
fs.symlinkSync(targetDir, path.join(configDir, 'commands'), linkType);
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
assert.deepEqual(result, { removed: [], preserved: [] });
assert.ok(fs.existsSync(targetFile), 'cleanup must not follow the destination symlink');
});
test('preserves and reports a user-modified managed file', (t) => {
const { configDir } = createFixture(t);
const relPath = 'commands/gsd-help.md';
const originalContent = '# generated\n';
const modifiedContent = '# user changed this\n';
const filePath = writeFile(configDir, relPath, modifiedContent);
writeManifest(configDir, { [relPath]: sha256(originalContent) });
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
assert.deepEqual(result, { removed: [], preserved: [relPath] });
assert.equal(fs.readFileSync(filePath, 'utf8'), modifiedContent);
assert.ok(fs.existsSync(path.dirname(filePath)), 'directory must remain while a preserved file exists');
});
test('removes and reports a managed-pristine file and then removes the empty directory', (t) => {
const { configDir } = createFixture(t);
const relPath = 'commands/gsd-help.md';
const content = '# generated\n';
const filePath = writeFile(configDir, relPath, content);
const commandsDir = path.dirname(filePath);
writeManifest(configDir, { [relPath]: sha256(content) });
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
assert.deepEqual(result, { removed: [relPath], preserved: [] });
assert.ok(!fs.existsSync(filePath));
assert.ok(!fs.existsSync(commandsDir), 'an empty retired destination should be removed');
});
test('leaves non-matching files untouched and keeps their directory', (t) => {
const { configDir } = createFixture(t);
const managedRelPath = 'commands/gsd-help.md';
const managedContent = '# generated\n';
const managedFile = writeFile(configDir, managedRelPath, managedContent);
const unrelatedFile = writeFile(configDir, 'commands/user-command.md', '# user\n');
writeManifest(configDir, { [managedRelPath]: sha256(managedContent) });
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
const result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
assert.deepEqual(result, { removed: [managedRelPath], preserved: [] });
assert.ok(!fs.existsSync(managedFile));
assert.ok(fs.existsSync(unrelatedFile));
assert.ok(fs.existsSync(path.dirname(unrelatedFile)), 'a non-empty destination must remain');
});
test('reports a managed-pristine file as preserved when unlinkSync fails', (t) => {
const { configDir } = createFixture(t);
const relPath = 'commands/gsd-help.md';
const content = '# generated\n';
const filePath = writeFile(configDir, relPath, content);
writeManifest(configDir, { [relPath]: sha256(content) });
setDeclarations(t, [{ destSubpath: 'commands', prefix: 'gsd-', suffix: '.md' }]);
const originalUnlinkSync = fs.unlinkSync;
fs.unlinkSync = (candidate) => {
if (path.resolve(candidate) === path.resolve(filePath)) throw new Error('injected unlink failure');
return originalUnlinkSync(candidate);
};
let result;
try {
result = retiredArtifactCleanup.pruneRetiredRuntimeArtifacts('cursor', configDir);
} finally {
fs.unlinkSync = originalUnlinkSync;
}
assert.deepEqual(result, { removed: [], preserved: [relPath] });
assert.ok(fs.existsSync(filePath), 'a failed unlink must leave the file in place');
});

View File

@@ -61,14 +61,13 @@ const GOLDEN = {
// Old switch: BOTH scopes returned [skills, commands] (no scope branch).
// 5b backfill: local == global.
// ADR-1235 step 1 (#1763): agents kind added.
// #2644: commands are deliberately retired so Cursor exposes a single skills surface.
'cursor/global': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'gsd-' },
],
'cursor/local': [
{ kind: 'skills', destSubpath: 'skills', prefix: 'gsd-' },
{ kind: 'commands', destSubpath: 'commands', prefix: 'gsd-' },
{ kind: 'agents', destSubpath: 'agents', prefix: 'gsd-' },
],

View File

@@ -65,11 +65,11 @@ describe('resolveRuntimeArtifactLayout — claude global', () => {
});
describe('resolveRuntimeArtifactLayout — cursor', () => {
test('returns correct layout for cursor — skills + commands + agents kinds (#785, ADR-1235)', () => {
test('returns correct layout for cursor — skills + agents only (#2644)', () => {
const layout = resolveRuntimeArtifactLayout('cursor', FAKE_DIR);
assert.strictEqual(layout.runtime, 'cursor');
assert.strictEqual(layout.configDir, FAKE_DIR);
assert.strictEqual(layout.kinds.length, 3);
assert.strictEqual(layout.kinds.length, 2);
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
assert.ok(skillsKind, 'must have a skills kind');
@@ -77,11 +77,8 @@ describe('resolveRuntimeArtifactLayout — cursor', () => {
assert.strictEqual(skillsKind.prefix, 'gsd-');
assert.strictEqual(typeof skillsKind.stage, 'function');
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
assert.ok(commandsKind, 'must have a commands kind (#785 Cursor 1.6 slash commands)');
assert.strictEqual(commandsKind.destSubpath, 'commands');
assert.strictEqual(commandsKind.prefix, 'gsd-');
assert.strictEqual(typeof commandsKind.stage, 'function');
assert.equal(layout.kinds.find(k => k.kind === 'commands'), undefined,
'Cursor skills are the sole slash-menu surface; commands would duplicate them (#2644)');
const agentsKind = layout.kinds.find(k => k.kind === 'agents');
assert.ok(agentsKind, 'must have an agents kind (ADR-1235 §1 descriptor cutover)');
@@ -416,11 +413,11 @@ describe('resolveRuntimeArtifactLayout edge-cases', () => {
assert.ok(kindNames.includes('agents'), 'should have agents kind');
});
test('cursor has both skills and commands kinds (#785)', () => {
test('cursor has a skills kind and no commands kind (#2644)', () => {
const layout = resolveRuntimeArtifactLayout('cursor', '/tmp/x');
const kindNames = layout.kinds.map(k => k.kind);
assert.ok(kindNames.includes('skills'), 'cursor must have skills kind');
assert.ok(kindNames.includes('commands'), 'cursor must have commands kind (#785 Cursor 1.6)');
assert.ok(!kindNames.includes('commands'), 'cursor commands kind would duplicate skill menu entries');
});
test('claude global has only skills kind', () => {
@@ -657,39 +654,11 @@ describe('stage — opencode/kilo skills kind (#784)', () => {
}
});
describe('stage — cursor commands kind (#785)', () => {
test('cursor commands kind stage returns directory with converted .md files', () => {
describe('stage — cursor retired commands kind (#2644)', () => {
test('cursor layout exposes no commands staging surface', () => {
const layout = resolveRuntimeArtifactLayout('cursor', FAKE_STAGE_DIR);
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
assert.ok(commandsKind, 'cursor should have a commands kind (#785)');
const stagedDir = commandsKind.stage(PROFILE_CORE);
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
const entries = fs.readdirSync(stagedDir).filter(f => f.endsWith('.md'));
assert.ok(entries.length >= 1, 'at least one command file should be staged');
// Cursor commands are plain markdown — no YAML frontmatter
for (const entry of entries) {
const content = fs.readFileSync(path.join(stagedDir, entry), 'utf8');
assert.ok(!content.startsWith('---'), `${entry}: cursor commands must not start with YAML frontmatter`);
}
});
test('cursor commands stage applies Cursor-specific content transforms', () => {
const layout = resolveRuntimeArtifactLayout('cursor', FAKE_STAGE_DIR);
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
assert.ok(commandsKind, 'cursor should have a commands kind (#785)');
const stagedDir = commandsKind.stage(PROFILE_FULL);
assert.ok(fs.existsSync(stagedDir), 'stagedDir must exist');
// Verify all staged files are .md only (no subdirectory SKILL.md layout)
const entries = fs.readdirSync(stagedDir, { withFileTypes: true });
for (const entry of entries) {
assert.ok(entry.isFile(), `${entry.name}: cursor commands dir must contain only flat files`);
assert.ok(entry.name.endsWith('.md'), `${entry.name}: must be .md file`);
}
assert.equal(commandsKind, undefined);
});
});