chore(#2931): cap emitted per-runtime bytes and single-source windsurf (#2984)

* fix(#2931): preserve protected regions and cap emitted per-runtime bytes

Route every runtime brand swap through applyClaudeCodeBrandSwap so
"Claude Code" survives verbatim inside <runtime_compatibility> regions
(#2284b). The fix existed only in bin/install.js's local copies; the
src/*.cts exports still used a naive replace, so binding install.js to
the single source -- as this phase does for the Windsurf family --
would have silently regressed those runtimes. A table-driven parity
guard now covers all nine brand-swapping converters.

De-duplicate the Windsurf converter family: delete the six local copies
in bin/install.js and bind the four exported ones by reference, guarded
by reference-identity assertions (the ADR-1508/#1675 pattern). The two
unexported helpers and an unused tool table go with them.

Replace the Windsurf 12,000-byte throw with description truncation,
matching the bound its sibling skill converter already applied. The
throw could only fire on an ~11.7 KB frontmatter description: the
largest emitted workflow is 311 bytes. Truncation makes the cap
unreachable by construction and leaves 12,000 in exactly one place,
eliminating the dual-surface duplication rather than testing for it.

Add the emitted-byte cap gate: buildEmittedSizes captures LF- and
<HOME>-normalized bytes from the walk buildParityManifest already
performs, and evaluateEmittedCaps asserts them against a per-runtime
cap table with dead-rule detection. buildParityManifest's return shape
is deliberately unchanged -- diffEmitted compares its values with
===, so making them objects would report all 8,529 emitted paths as
moved. A regression test pins the values as strings.

Add a deterministic trim-safety gate over composeWithinBudget's
omitted/shrunk/floored/isolatePrefix metadata, with an anti-vacuity
rule, replacing the model-graded eval gate the issue described.

* docs(#2931): correct ADR-1671 windsurf premise and trim-safety contract

* fix(#2931): bound the windsurf command name and single-source the brand swap

Review findings from the orthogonal passes, all fixed inline.

The claim that removing the 12,000-byte throw left total emission
"bounded by construction" was false. The #1615 regex constrains the
character class but not the length, and commandName is interpolated
three times into the emitted workflow: a 20,000-character name emitted
60,162 bytes silently. Add WINDSURF_COMMAND_NAME_MAX=128 as a separate,
clearly-labelled size control that THROWS -- commandName is the @-ref
path target, so truncating it would point the workflow at a file that
does not exist (DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED). The
#1615 security regex is untouched and still runs first. 128 is generous:
the longest shipped name is gsd-plan-review-convergence at 27.

Harmonize convertClaudeCommandToWindsurfSkill onto the code-point-safe
truncation helper. It still used a UTF-16 slice(0,177) -- the exact
surrogate-splitting bug the helper was written to avoid, in the very
sibling the helper's comment cites as its model. Bounds are unchanged,
so output is byte-identical for every shipped command (descriptions max
out at 99 chars).

Export applyClaudeCodeBrandSwap and bind it in bin/install.js, deleting
the local copy. Adding it to the .cts left two unlinked implementations
of identical logic -- the drift class this change exists to remove.
Verified byte-identical across eight fixtures and five sequential calls
before merging, and guarded by a reference-identity assertion.

Convert three try/finally test bodies to t.after (CONTRIBUTING.md:344),
add fast-check property coverage for the trim-safety contract, and use
fc.pre instead of a bare return in a property callback.

* test(#2931): fix three test-authoring bugs the remote matrix caught

The remote runner returned 8 unique failures on 6f15cdeb8. All three
causes were in the test files, not the modules under test -- local
harnesses exercise the modules directly, so nothing executed the test
bodies until the matrix did.

`{ __proto__: [...] }` in an object literal sets the prototype instead
of an own key, so the JSON round-trip erased it and the cap table never
saw a reserved runtime key. The production rejection was already
correct; the test could not reach it. Use a computed key.

Two cap fixtures tripped orthogonal error paths rather than the paths
they name: one declared windsurf in the cap table but omitted it from
sizes (UNKNOWN_RUNTIME), the other left the sole windsurf pattern
matching nothing (a genuine dead rule). Both now include a compliant
artifact so the intended branch is what is asserted. The dead-rule and
unknown-runtime contracts are deliberate and unchanged.

`const { root } = makeSyntheticConfig({ ... `${root}` })` referenced
`root` from inside its own initializer -- a temporal dead zone error.
makeSyntheticConfig now optionally takes a (root) => files factory.

Also raise the npm pack --dry-run bound 60s -> 120s in the shipped-
scripts packaging test. That failure is NOT from this branch: the file
is byte-identical to next, a fresh tsc measures 1.98s there vs 2.14s
here, and the run recorded 60,637ms against a 60,000ms bound -- a
timeout under 28,948-test parallel contention, not a slowdown. Fixed
rather than deferred because a bound that tight is fragile regardless
of which branch trips it.

* chore(#2931): backfill changeset pr number to 2984

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-01 16:00:14 -04:00
committed by GitHub
parent 4df6d884b3
commit 628648d63a
15 changed files with 2284 additions and 221 deletions

View File

@@ -932,8 +932,9 @@ function convertClaudeToCursorMarkdown(content) {
// Remove Claude Code-specific bug workarounds before brand replacement
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
// Replace "Claude Code" brand references with "Cursor"
converted = converted.replace(/\bClaude Code\b/g, 'Cursor');
// Replace "Claude Code" brand references with "Cursor" — #2284(b): skips
// <runtime_compatibility> comparison-table content (protected region).
converted = applyClaudeCodeBrandSwap(converted, 'Cursor');
return converted;
}
@@ -1013,6 +1014,32 @@ function convertClaudeCommandToCursorCommand(content, _commandName) {
// Windsurf uses a tool set similar to Cursor.
// Config lives in .windsurf/ (local) and ~/.codeium/windsurf/ (global).
// #2931: ported from bin/install.js's local Windsurf converter copy, which had
// picked up the #2284(b) protected-region fix that this exported source never
// received. Binding bin/install.js's Windsurf family to these exports (see
// tests/install-runtime-artifacts.test.cjs reference-identity assertions)
// without this would have silently regressed live installs: `Claude Code`
// mentions inside a `<runtime_compatibility>` comparison table would start
// getting brand-swapped again. Split `content` on the protected-block regex,
// brand-swap only the gap text between (and around) matches, then rejoin
// gap+block alternately — no placeholder/sentinel token involved.
const RUNTIME_COMPATIBILITY_BLOCK_RE = /<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g;
function applyClaudeCodeBrandSwap(content, brandName) {
if (!brandName) return content;
let result = '';
let lastIndex = 0;
RUNTIME_COMPATIBILITY_BLOCK_RE.lastIndex = 0; // reset shared global-regex state before each use
let m;
while ((m = RUNTIME_COMPATIBILITY_BLOCK_RE.exec(content))) {
const gap = content.slice(lastIndex, m.index);
result += gap.replace(/\bClaude Code\b/g, brandName);
result += m[0]; // protected block, verbatim — never brand-swapped
lastIndex = m.index + m[0].length;
}
result += content.slice(lastIndex).replace(/\bClaude Code\b/g, brandName);
return result;
}
function convertSlashCommandsToWindsurfSkillMentions(content) {
// Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
return content.replace(/gsd:/gi, 'gsd-');
@@ -1044,8 +1071,9 @@ function convertClaudeToWindsurfMarkdown(content) {
// Remove Claude Code-specific bug workarounds before brand replacement
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
// Replace "Claude Code" brand references with "Windsurf"
converted = converted.replace(/\bClaude Code\b/g, 'Windsurf');
// Replace "Claude Code" brand references with "Windsurf" — #2284(b): skips
// <runtime_compatibility> comparison-table content (protected region).
converted = applyClaudeCodeBrandSwap(converted, 'Windsurf');
return converted;
}
@@ -1086,12 +1114,50 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) {
}
}
description = toSingleLine(description);
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
// #2931: code-point-safe truncation (see truncateWindsurfWorkflowDescription
// below) — a raw UTF-16 `slice(0, 177)` can bisect a surrogate pair and
// emit a lone surrogate on re-encode. Same exact bounds as before (>180
// chars -> first 177 code points + '...'), just harmonized with the
// sibling Windsurf workflow converter's helper instead of duplicating the
// surrogate-splitting idiom here.
const shortDescription = truncateWindsurfWorkflowDescription(description);
const adapter = getWindsurfSkillAdapterHeader(skillName);
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
}
// #2931: cap on the Windsurf workflow's only unbounded input (the frontmatter
// `description`). Shared (not just mirrored) by convertClaudeCommandToWindsurfSkill
// above — both converters call truncateWindsurfWorkflowDescription below so
// there is exactly one code-point-safe truncation idiom, not two.
const WINDSURF_WORKFLOW_DESCRIPTION_MAX = 180;
function truncateWindsurfWorkflowDescription(description) {
// Multi-byte safe: slice by Unicode code points (`Array.from`), never by
// raw UTF-16 index — an index-based slice can bisect a surrogate pair and
// emit a lone surrogate / U+FFFD on re-encode. See #2931.
const codePoints = Array.from(description);
if (codePoints.length <= WINDSURF_WORKFLOW_DESCRIPTION_MAX) return description;
return `${codePoints.slice(0, WINDSURF_WORKFLOW_DESCRIPTION_MAX - 3).join('')}...`;
}
// #2931: SEPARATE size control from the #1615 security regex below — do not
// fold the two together or make either conditional on the other. The #1615
// regex constrains commandName's CHARACTER CLASS but not its LENGTH, and
// commandName is interpolated into the emitted template three times (the
// `# <commandName>` heading, the `@.../<stem>.md` @-reference target, and the
// trailing "after /<commandName>" mention) — so an unbounded commandName
// reopens the byte-cap hole the removed 12000-byte throw used to close
// (verified: commandName length 246 -> 900 bytes, 5000 -> 15,162 bytes,
// 20000 -> 60,162 bytes — all silently over the old 12000 cap). THROW rather
// than truncate: a truncated commandName would silently point the workflow's
// `@~/.claude/gsd-core/commands/gsd/<stem>.md` reference at a file that does
// not exist (see DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED) — a name
// too long to represent is a genuine error, not something to degrade. 128 is
// deliberately generous: the longest real shipped command name is
// `gsd-plan-review-convergence` at 27 characters.
const WINDSURF_COMMAND_NAME_MAX = 128;
function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
// #1615 security: commandName flows unsanitized into a markdown body that
// Windsurf loads as an LLM-readable workflow. Validate at entry to prevent
@@ -1100,6 +1166,10 @@ function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
// Pattern: optional gsd- prefix + lowercase alphanumeric + dashes; rejects
// everything else. See DEFECT.PROMPT-INJECTION-SCAN-COLLISION and the
// PR #1622 security review.
// #2931: this is a SECURITY control, not a size control — do not weaken,
// reorder, or make it conditional on the description-truncation logic
// added below. Keep the two concerns independent even though both happen
// to run in this function.
if (typeof commandName !== 'string' || !/^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(commandName)) {
const preview = typeof commandName === 'string' ? JSON.stringify(commandName.slice(0, 60)) : String(commandName);
throw new Error(
@@ -1107,16 +1177,36 @@ function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
'must match /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (no slashes, backslashes, spaces, dots, trailing dash, or control chars — prevents prompt injection and path-component injection into the workflow body)'
);
}
// #2931: SEPARATE size control — see WINDSURF_COMMAND_NAME_MAX above for
// why this exists and why it throws instead of truncating. Kept as an
// independent check from the #1615 regex above (not folded into it, not
// conditional on it).
if (commandName.length > WINDSURF_COMMAND_NAME_MAX) {
const preview = JSON.stringify(commandName.slice(0, 60));
throw new Error(
`convertClaudeCommandToWindsurfWorkflow: commandName too long (${commandName.length} chars, ` +
`preview ${preview}...); max ${WINDSURF_COMMAND_NAME_MAX} chars (see WINDSURF_COMMAND_NAME_MAX)`
);
}
const converted = convertClaudeToWindsurfMarkdown(content);
const { frontmatter } = extractFrontmatterAndBody(converted);
const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
const rawDescription = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
// #2931: a whitespace-only description is truthy (`description || fallback`
// would keep it) but toSingleLine() collapses it to ''. Treat it as absent
// so the fallback is used instead of emitting a blank line.
const singleLineDescription = rawDescription ? toSingleLine(rawDescription) : '';
const effectiveDescription = truncateWindsurfWorkflowDescription(singleLineDescription || `Run ${commandName}.`);
const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
const byteLength = Buffer.byteLength(workflow, 'utf8');
if (byteLength > 12000) {
throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
}
return workflow;
// #2931: total emission size is bounded by (fixed template text) +
// (3 x WINDSURF_COMMAND_NAME_MAX, one per commandName/stem interpolation
// above) + (WINDSURF_WORKFLOW_DESCRIPTION_MAX Unicode code points, up to 4
// UTF-8 bytes each). Both inputs are validated/truncated above — commandName
// is length-capped-and-thrown by WINDSURF_COMMAND_NAME_MAX, description is
// truncated by truncateWindsurfWorkflowDescription — so this bound holds by
// construction, not by measurement. The 12000-byte figure itself lives in
// exactly one place — the cap table in tests/helpers/emitted-caps.cjs —
// this comment only justifies why the actual emitted size stays under it.
return `# ${commandName}\n\n${effectiveDescription}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
}
// --- Augment converters ---
@@ -1147,8 +1237,9 @@ function convertClaudeToAugmentMarkdown(content) {
// Remove Claude Code-specific bug workarounds before brand replacement
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
// Replace "Claude Code" brand references with "Augment"
converted = converted.replace(/\bClaude Code\b/g, 'Augment');
// Replace "Claude Code" brand references with "Augment" — #2284(b): skips
// <runtime_compatibility> comparison-table content (protected region).
converted = applyClaudeCodeBrandSwap(converted, 'Augment');
return converted;
}
@@ -1232,7 +1323,8 @@ function convertClaudeToTraeMarkdown(content) {
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR');
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
converted = converted.replace(/\bClaude Code\b/g, 'Trae');
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
converted = applyClaudeCodeBrandSwap(converted, 'Trae');
return converted;
}
@@ -1288,7 +1380,8 @@ function convertClaudeToCodebuddyMarkdown(content) {
converted = converted.replace(/\.claude\//g, '.codebuddy/');
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
converted = converted.replace(/\bClaude Code\b/g, 'CodeBuddy');
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
converted = applyClaudeCodeBrandSwap(converted, 'CodeBuddy');
return converted;
}
@@ -1369,7 +1462,8 @@ function convertClaudeToCliineMarkdown(content) {
converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR');
converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
converted = converted.replace(/\bClaude Code\b/g, 'Cline');
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
converted = applyClaudeCodeBrandSwap(converted, 'Cline');
return converted;
}
@@ -2220,7 +2314,8 @@ function convertClaudeAgentToQwenAgent(content) {
const _b = _hostBehaviors('qwen').brandingRewrites || {};
let converted = content;
if (_b['CLAUDE.md']) converted = converted.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
if (_b['Claude Code']) converted = converted.replace(/\bClaude Code\b/g, _b['Claude Code']);
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
if (_b['Claude Code']) converted = applyClaudeCodeBrandSwap(converted, _b['Claude Code']);
if (_b['.claude/']) converted = converted.replace(/\.claude\//g, _b['.claude/']);
const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -2570,7 +2665,8 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
const _b = _hostBehaviors(runtime).brandingRewrites;
if (_b) {
content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
content = applyClaudeCodeBrandSwap(content, _b['Claude Code']);
}
content = content.replace(/~\/\.claude\//g, pathPrefix);
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2595,7 +2691,8 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
const _b = _hostBehaviors(runtime).brandingRewrites;
if (_b) {
content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
// #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
content = applyClaudeCodeBrandSwap(content, _b['Claude Code']);
}
content = content.replace(/~\/\.claude\//g, pathPrefix);
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2889,6 +2986,12 @@ export = {
convertClaudeToWindsurfMarkdown,
convertClaudeCommandToWindsurfSkill,
convertClaudeCommandToWindsurfWorkflow,
// #2931: single-sourced brand-swap helper (was duplicated verbatim in
// bin/install.js — the exact drift class this PR exists to reduce). Used
// internally by convertClaudeToWindsurfMarkdown/convertClaudeToAugmentMarkdown
// above and bound from here by the remaining bin/install.js converters
// (Cursor/Trae/CodeBuddy/Cline) that still brand-swap inline.
applyClaudeCodeBrandSwap,
convertClaudeToAugmentMarkdown,
convertClaudeCommandToAugmentSkill,
convertClaudeToTraeMarkdown,