* test(#2359): failing-first regression for the 1.4.0 Cursor commands citation The `## [1.4.0]` CHANGELOG entry for the `.cursor/commands/` surface carries the trailing reference (#803) — the Cline PR, cited correctly by the entry two lines below. The Cursor surface shipped in #805 (feat(#785)). Adds four behavioral cases to the owning module's test file, asserting through parseChangelog (the same parser cmdExtract/cmdVerify/cmdRender use) rather than raw text matching: 1. the Cursor bullet cites 805 - FAILS before the fix 2. the adjacent Cline bullet still cites 803 - guards a global s/803/805/ 3. exactly one Cursor bullet exists - guards drop/duplicate 4. the two bullets cite different PRs - the defect as an invariant Folded into tests/changeset-serialize.test.cjs rather than a new bug-2359-*.test.cjs file, per scripts/lint-regression-test-names.cjs. Refs #2359 * fix(#2359): CHANGELOG 1.4.0 Cursor commands entry cites #805, not #803 The `## [1.4.0]` entry for `gsd install --cursor` writing `.cursor/commands/gsd-<name>.md` carried the trailing reference (#803). That PR is `feat(#787): elevate Cline` — cited correctly by the entry two lines below. The Cursor slash-command surface shipped in #805 (`feat(#785): write .cursor/commands/ Cursor 1.6 slash-command surface`). Two adjacent bullets therefore claimed one PR, and only the second was right. The trailing (#NNNN) is machine-parsed by parseChangelog and consumed by `changeset extract`, so the wrong number is live data, not only prose — and it had already propagated to a human reporter (#2341, quoted there as "#785 / #803"). Single-occurrence, line-anchored edit. The Cline entry's (#803) is correct and is deliberately untouched; the regression test added in the preceding commit asserts both, so a global s/803/805/ fails. Prose is unchanged, including the "both surfaces are written on every install" clause the issue explicitly certifies as accurate. Fixes #2359 * chore(#2359): changeset fragment for the Cursor commands citation fix * test(#2359): assert bullet cardinality before reading it Review findings, one root cause: each of the four cases re-derived the bullet list, re-filtered by marker, then destructured `const [x] = ...` and read `.pr` unguarded. A reworded or deleted entry therefore died with TypeError: Cannot read properties of undefined (reading 'pr') instead of naming what was missing. The repetition was also duplicated logic across all four cases. Adds locateBullet(version, marker, label), which asserts exactly one match before returning, and folds the two cardinality guards into one case covering both entries. Failure mode verified: a marker matching nothing now raises AssertionError "expected exactly one bogus bullet in the 1.4.0 section, found 0". Refs #2359 * chore(#2359): backfill changeset pr number (#3252) --------- Co-authored-by: sim <sim@local>
This commit is contained in:
5
.changeset/steady-pumas-click.md
Normal file
5
.changeset/steady-pumas-click.md
Normal file
@@ -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)
|
||||||
@@ -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)
|
- 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)
|
- 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/<name>/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/<name>/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)
|
- Emit native on-demand skills (`skills/<name>/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/<name>/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-<name>.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-<name>.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)
|
- 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)
|
- 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)
|
- 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)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ process.env.GSD_TEST_MODE = '1';
|
|||||||
|
|
||||||
const { test, describe } = require('node:test');
|
const { test, describe } = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
|
|
||||||
const { serializeChangelog, parseChangelog } = require(path.join(__dirname, '..', 'scripts', 'changeset', 'serialize.cjs'));
|
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)}`);
|
`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-<name>\.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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user