diff --git a/.changeset/steady-pumas-click.md b/.changeset/steady-pumas-click.md new file mode 100644 index 000000000..eb662d0e4 --- /dev/null +++ b/.changeset/steady-pumas-click.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 3252 +--- +**The 1.4.0 changelog entry for Cursor slash commands now credits the PR that shipped it** — the entry describing `gsd install --cursor` writing `.cursor/commands/` cited #803 (the Cline PR, which the adjacent entry cites correctly) instead of #805, so anyone tracing the Cursor commands surface landed in an unrelated change. (#2359) diff --git a/CHANGELOG.md b/CHANGELOG.md index 062a4896e..af4edbe08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1140,7 +1140,7 @@ Added: register newly-available Claude Code lifecycle hooks — SubagentStop, St - Gemini installs now register three additional hook events — `BeforeAgent`, `AfterAgent`, and `BeforeModel` — wired to `gsd-context-monitor.js` for per-turn context headroom tracking. Previously only `SessionStart`, `BeforeTool`, and `AfterTool` were registered. The installer also detects `hooksConfig.enabled: false` in the user's Gemini `settings.json` and emits a clear warning, surfacing the silent failure mode where all hooks are registered but never execute. (#776) (#829) - Cross-runtime command enrichment in the installer. Gemini CLI commands now use native `{{args}}` interpolation (translated from Claude's `$ARGUMENTS`) so typed arguments interpolate into the prompt body, and `/gsd:progress` injects live project state via a fixed, injection-safe `!{cat .planning/STATE.md 2>/dev/null}` shell block. Qwen Code skills now carry a numeric `priority` field so the most-used main-loop workflows (`new-project`, `plan-phase`, `execute-phase`, …) surface first in the `/skills` list. The OpenCode per-command `model`/`agent`/`subtask` enrichment was evaluated and intentionally not implemented — `model` would reintroduce the ProviderModelNotFoundError regression that the converter deliberately guards against for non-Anthropic providers (#1156), `subtask`/`agent` change execution semantics for GSD's interactive commands, and `variant` is not in the OpenCode command schema. (#778) (#825) - Emit native on-demand skills (`skills//SKILL.md`) for the OpenCode-family runtimes (OpenCode and Kilo) at install time, in addition to the existing flat `command/` and file-based `agents/` surfaces. OpenCode and Kilo share a config schema and both discover skills from `skills//SKILL.md`; the installer now stages each GSD command as a skill with minimal, spec-compliant frontmatter (`name` matching the directory, `description` 1–1024 chars) via a shared OpenCode-family skill writer. Skills respect the active install profile (core/minimal stage only their subset) and are removed on uninstall. (#784) (#810) -- `gsd install --cursor` now writes `.cursor/commands/gsd-.md` in addition to the existing `.cursor/skills/` surface. Cursor 1.6 introduced plain-markdown slash commands (no frontmatter) in `.cursor/commands/`; they appear in the `/` menu in the Agent input. Each command file is generated from the same source as the skill but with frontmatter stripped and Cursor-specific content transforms applied (`convertClaudeCommandToCursorCommand`). The skills surface is unchanged — both surfaces are written on every install. (#803) +- `gsd install --cursor` now writes `.cursor/commands/gsd-.md` in addition to the existing `.cursor/skills/` surface. Cursor 1.6 introduced plain-markdown slash commands (no frontmatter) in `.cursor/commands/`; they appear in the `/` menu in the Agent input. Each command file is generated from the same source as the skill but with frontmatter stripped and Cursor-specific content transforms applied (`convertClaudeCommandToCursorCommand`). The skills surface is unchanged — both surfaces are written on every install. (#805) - The GitHub Copilot installer now reaches lifecycle-hook and instruction parity with other first-class runtimes. It emits a self-contained `sessionStart` hook config (`.github/hooks/gsd-session.json` for local installs, `~/.copilot/hooks/gsd-session.json` for global) and writes `AGENTS.md` at the repository root (which Copilot CLI reads as primary instructions) alongside `copilot-instructions.md`. The hook is an inline `command` hook with no separate script file, so it cannot dangle. Both artifacts are removed — with user-authored content preserved — on `--uninstall`. (#786) (#804) - Elevate the Cline runtime to hook parity. The installer now emits the Cline `.clinerules/` directory form (`.clinerules/gsd.md`) instead of a single `.clinerules` file, adds a `.clinerules/hooks/PreToolUse` lifecycle hook (Cline v3.36+ JSON stdin → `{cancel,errorMessage,contextModification}` protocol; guards `.planning/` artifacts and fails open), and merges GSD instructions into the cross-tool global `~/.agents/AGENTS.md` target on global installs. A legacy single-file `.clinerules` is migrated to the directory form in place, and `--uninstall` removes the new artifacts and strips the GSD block from `~/.agents/AGENTS.md`. (#787) (#803) - Qwen Code installs now register three additional hook events that Qwen Code supports beyond Claude Code: `SubagentStop`, `Stop`, and `PreCompact` — all wired to `gsd-context-monitor.js` for context headroom tracking at subagent completion, model stop, and pre-compaction. These events are Qwen-only; Claude Code installs are unchanged. `UserPromptSubmit` is deferred: `gsd-prompt-guard` exits unless `tool_name` is `Write|Edit`, making it a no-op for that payload shape. (#788) (#807) diff --git a/tests/changeset-serialize.test.cjs b/tests/changeset-serialize.test.cjs index b9428ff23..d0094515f 100644 --- a/tests/changeset-serialize.test.cjs +++ b/tests/changeset-serialize.test.cjs @@ -3,6 +3,7 @@ process.env.GSD_TEST_MODE = '1'; const { test, describe } = require('node:test'); const assert = require('node:assert/strict'); +const fs = require('node:fs'); const path = require('node:path'); const { serializeChangelog, parseChangelog } = require(path.join(__dirname, '..', 'scripts', 'changeset', 'serialize.cjs')); @@ -181,3 +182,64 @@ describe('#3001: multi-paragraph changeset body round-trips without truncation', `second paragraph must survive; got: ${JSON.stringify(bullet.body)}`); }); }); + +describe('changeset serialize: shipped CHANGELOG citation integrity (#2359)', () => { + // Regression: the `## [1.4.0]` Cursor commands-surface entry carried the + // trailing reference (#803) — the *Cline* PR, cited correctly by the entry two + // lines below it. The Cursor surface actually shipped in #805. Two adjacent + // bullets therefore claimed one PR, and only the second was right. + // + // This is not cosmetic: the trailing (#NNNN) is machine-parsed here and + // consumed by `cmdExtract`, so a wrong number propagates into extracted + // release-note ranges. It had already propagated to a human reporter (#2341, + // quoted there as "#785 / #803"). + // + // The obvious bad fix is a global s/(#803)/(#805)/, which repairs the Cursor + // entry and silently corrupts the Cline one. The Cline assertion below is the + // guard for exactly that, and is expected to pass both before and after. + const CHANGELOG = fs.readFileSync(path.join(__dirname, '..', 'CHANGELOG.md'), 'utf8'); + const CURSOR_MARKER = /`\.cursor\/commands\/gsd-\.md`/; + const CLINE_MARKER = /Elevate the Cline runtime to hook parity/; + + function bulletsOf(version) { + const release = parseChangelog(CHANGELOG).releases.find((r) => r.version === version); + assert.ok(release, `CHANGELOG.md must still carry a \`## [${version}]\` release section`); + return release.sections.flatMap((s) => s.bullets); + } + + // Asserts cardinality before returning, so a reworded or deleted entry fails + // with a readable message instead of a TypeError on an undefined bullet. + function locateBullet(version, marker, label) { + const matches = bulletsOf(version).filter((b) => marker.test(b.body)); + assert.equal(matches.length, 1, + `expected exactly one ${label} bullet in the ${version} section, found ${matches.length}`); + return matches[0]; + } + + test('the 1.4.0 Cursor commands-surface bullet cites PR 805', () => { + const cursor = locateBullet('1.4.0', CURSOR_MARKER, 'Cursor commands-surface'); + assert.equal(cursor.pr, 805, + 'the .cursor/commands/ surface shipped in PR #805 (feat(#785)), not #803 (the Cline PR)'); + }); + + test('the adjacent 1.4.0 Cline bullet still cites PR 803', () => { + const cline = locateBullet('1.4.0', CLINE_MARKER, 'Cline hook-parity'); + assert.equal(cline.pr, 803, + "the Cline entry's #803 is correct and must survive the Cursor correction"); + }); + + test('both 1.4.0 entries survive the correction exactly once each', () => { + const bullets = bulletsOf('1.4.0'); + assert.equal(bullets.filter((b) => CURSOR_MARKER.test(b.body)).length, 1, + 'the Cursor entry must be corrected in place — neither dropped nor duplicated'); + assert.equal(bullets.filter((b) => CLINE_MARKER.test(b.body)).length, 1, + 'the Cline entry must be left alone — neither dropped nor duplicated'); + }); + + test('the adjacent Cursor and Cline bullets do not cite the same PR', () => { + const cursor = locateBullet('1.4.0', CURSOR_MARKER, 'Cursor commands-surface'); + const cline = locateBullet('1.4.0', CLINE_MARKER, 'Cline hook-parity'); + assert.notEqual(cursor.pr, cline.pr, + 'two adjacent entries describing different runtimes cannot share one PR'); + }); +});