From afd95a15a951e32fdb1324b703bfe77e92ad481e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 17 Jun 2026 12:14:15 -0400 Subject: [PATCH] fix(#1384): scan live changeset fragments in the #1777 purity gate (#1385) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The product-name-purity gate scanned CHANGELOG.md only, never the .changeset/*.md fragments that render into it. An impure fragment passed PR review, sat dormant, and re-introduced a forbidden parenthetical product description at the next release — even after CHANGELOG.md had been hand-fixed. This is the recurrence vector behind the 1.5.0 back-merge (#1379) failure. - Purify the two live fragments to the already-accepted forms: - happy-finches-travel.md: "Claude Code (background dispatch …)" -> "Claude Code; background dispatch …" - 924-claude-flat-skill-layout.md: "Claude (`~/.claude/…`)" -> "Claude at `~/.claude/…`" - Extend the #1777 gate to also scan live .changeset/*.md fragments, reusing one shared detection helper. Archived fragments never re-render and are intentionally out of scope. Test-only + changeset-prose change; no production behavior change. Closes #1384 Co-authored-by: Claude Opus 4.8 --- .changeset/924-claude-flat-skill-layout.md | 2 +- .changeset/happy-finches-travel.md | 2 +- tests/product-name-purity.test.cjs | 88 +++++++++++++++++----- 3 files changed, 72 insertions(+), 20 deletions(-) diff --git a/.changeset/924-claude-flat-skill-layout.md b/.changeset/924-claude-flat-skill-layout.md index 2fcfb9d7b..8907aa172 100644 --- a/.changeset/924-claude-flat-skill-layout.md +++ b/.changeset/924-claude-flat-skill-layout.md @@ -2,4 +2,4 @@ type: Fixed pr: 924 --- -**Claude global install reverted to flat skill layout so concrete skills are discoverable.** PR #883 introduced nested skill layout for Claude (`~/.claude/skills/gsd-ns-/skills//SKILL.md`), but Claude Code's skill discovery scans only one level under `~/.claude/skills/` — nested concrete skills were never listed in the Skill-tool available-skills list and direct `Skill(skill="gsd-plan-phase")` calls stopped working. This fix reverts Claude to the flat layout (`~/.claude/skills/gsd-/SKILL.md`) so all ~61 concrete skills are top-level and immediately discoverable. The 6 other runtimes that confirmed non-recursive scanning (cline, qwen, hermes, augment, trae, antigravity) retain their nested layout. (#924) +**Claude global install reverted to flat skill layout so concrete skills are discoverable.** PR #883 introduced nested skill layout for Claude at `~/.claude/skills/gsd-ns-/skills//SKILL.md`, but Claude Code's skill discovery scans only one level under `~/.claude/skills/` — nested concrete skills were never listed in the Skill-tool available-skills list and direct `Skill(skill="gsd-plan-phase")` calls stopped working. This fix reverts Claude to the flat layout (`~/.claude/skills/gsd-/SKILL.md`) so all ~61 concrete skills are top-level and immediately discoverable. The 6 other runtimes that confirmed non-recursive scanning (cline, qwen, hermes, augment, trae, antigravity) retain their nested layout. (#924) diff --git a/.changeset/happy-finches-travel.md b/.changeset/happy-finches-travel.md index 9c974f5bf..4ec1207b5 100644 --- a/.changeset/happy-finches-travel.md +++ b/.changeset/happy-finches-travel.md @@ -2,4 +2,4 @@ type: Fixed pr: 863 --- -**`/gsd-manager` and `/gsd-autonomous --interactive` no longer silently skip worktree isolation and independent verification on Claude Code.** They dispatched plan/execute as background agents, but a backgrounded Claude Code agent has no Agent/Task tool and cannot spawn the nested executors, plan-checker, or verifier — so isolation and verification silently never ran. Both workflows now resolve the runtime and run plan/execute inline on Claude Code (background dispatch is kept on runtimes that support nested subagents). +**`/gsd-manager` and `/gsd-autonomous --interactive` no longer silently skip worktree isolation and independent verification on Claude Code.** They dispatched plan/execute as background agents, but a backgrounded Claude Code agent has no Agent/Task tool and cannot spawn the nested executors, plan-checker, or verifier — so isolation and verification silently never ran. Both workflows now resolve the runtime and run plan/execute inline on Claude Code; background dispatch is kept on runtimes that support nested subagents. diff --git a/tests/product-name-purity.test.cjs b/tests/product-name-purity.test.cjs index 8c57bbb40..44b4ce53d 100644 --- a/tests/product-name-purity.test.cjs +++ b/tests/product-name-purity.test.cjs @@ -39,7 +39,46 @@ const README_FILES = [ 'docs/README.md', ].filter(f => fs.existsSync(path.join(ROOT, f))); +// Detect "ProductName (description)" parentheticals in arbitrary prose, skipping +// version references like "Claude Code (v1.32.0)" / "Claude (1.5.0)". Returns the +// matched substrings so callers can report them. Shared by the CHANGELOG and the +// changeset-fragment scans so both apply identical rules. +function findProductParentheticals(content) { + const found = []; + for (const product of PRODUCTS) { + // Match "ProductName (something)" but not "ProductName (v1.2.3)" (version refs are ok) + const pattern = new RegExp( + product.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + + '\\s*\\([^)]*(?!v?\\d+\\.\\d)[^)]*\\)', + 'g' + ); + const matches = content.match(pattern); + if (!matches) continue; + for (const m of matches) { + // Skip version references like "Claude Code (v1.32.0)" + if (/\(v?\d+\.\d+/.test(m)) continue; + found.push(m); + } + } + return found; +} + describe('product name purity (#1777)', () => { + // Pin the shared detector's contract so neither the CHANGELOG nor the + // fragment scan can pass vacuously: a silently-broken helper that always + // returned [] would otherwise go undetected whenever the scanned files + // happen to be clean. + test('findProductParentheticals catches a real violation and allows version refs', () => { + assert.deepEqual( + findProductParentheticals('see Claude Code (the Anthropic CLI) for details'), + ['Claude Code (the Anthropic CLI)'], + ); + assert.deepEqual( + findProductParentheticals('upgraded to Claude Code (v1.32.0)'), + [], + ); + }); + test('no README install-block comments contain parenthetical descriptions', () => { const violations = []; @@ -84,24 +123,7 @@ describe('product name purity (#1777)', () => { if (!fs.existsSync(changelog)) return; const content = fs.readFileSync(changelog, 'utf-8'); - const violations = []; - - for (const product of PRODUCTS) { - // Match "ProductName (something)" but not "ProductName (v1.2.3)" (version refs are ok) - const pattern = new RegExp( - product.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + - '\\s*\\([^)]*(?!v?\\d+\\.\\d)[^)]*\\)', - 'g' - ); - const matches = content.match(pattern); - if (matches) { - for (const m of matches) { - // Skip version references like "Claude Code (v1.32.0)" - if (/\(v?\d+\.\d+/.test(m)) continue; - violations.push(m); - } - } - } + const violations = findProductParentheticals(content); assert.strictEqual( violations.length, 0, @@ -112,4 +134,34 @@ describe('product name purity (#1777)', () => { ].join('\n') ); }); + + test('live changeset fragments do not include parenthetical product descriptions', () => { + const changesetDir = path.join(ROOT, '.changeset'); + if (!fs.existsSync(changesetDir)) return; + + // Only LIVE fragments (.changeset/*.md) render into CHANGELOG.md at release + // time, so an impure fragment silently re-introduces a #1777 violation at the + // next release / back-merge even after CHANGELOG.md itself was hand-fixed. + // Archived fragments (.changeset/archived/) never re-render and are out of scope. + const fragments = fs.readdirSync(changesetDir, { withFileTypes: true }) + .filter(d => d.isFile() && d.name.endsWith('.md') && d.name !== 'README.md') + .map(d => d.name); + + const violations = []; + for (const frag of fragments) { + const content = fs.readFileSync(path.join(changesetDir, frag), 'utf-8'); + for (const m of findProductParentheticals(content)) { + violations.push(frag + ' — ' + m); + } + } + + assert.strictEqual( + violations.length, 0, + [ + 'Changeset fragments must not include parenthetical product descriptions', + '(fragment prose renders verbatim into CHANGELOG.md at release time):', + ...violations.map(v => ' ' + v), + ].join('\n') + ); + }); });