Merge pull request #1630 from open-gsd/fix/1629a-install-order

fix(#1629): copy Windsurf command bodies so workflow delegation targets resolve
This commit is contained in:
Tom Boucher
2026-06-23 15:43:17 -04:00
committed by GitHub
4 changed files with 55 additions and 0 deletions

View File

@@ -0,0 +1,5 @@
---
type: Fixed
pr: 1630
---
**`/gsd-*` commands in Windsurf Cascade resolve their command bodies** — Windsurf slash-command workflows delegate to canonical command bodies at gsd-core/commands/gsd/X.md, but the install never copied those files. Commands appeared in the `/` menu yet silently failed when invoked because the LLM was told to read a missing file. Installs now copy commands/gsd/*.md into the workflow delegation target.

View File

@@ -736,6 +736,12 @@ A legal deferred state of an Execute step (`external_job_waiting`): the executor
`DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS.fix-forward=ADD the test file to scripts/prompt-injection-scan.sh ALLOWLIST array with a comment citing this defect class; for large fixture sets, move them to tests/fixtures/adversarial/security/ (auto-allowlisted dir) and load via readFileSync; never weaken or fragment the payload to evade the scanner — that defeats the test's purpose; ALSO when documenting this defect in CONTEXT.md, do NOT quote the literal pattern — describe it generically (the scanner scans CONTEXT.md too)`
`DEFECT.PROMPT-INJECTION-SCAN-COLLISION-WITH-TESTS.prevention=when writing a security regression test that uses real injection payloads as fixtures, immediately add the test file path to scripts/prompt-injection-scan.sh ALLOWLIST in the same commit; when documenting this defect class anywhere under scanner scope (CONTEXT.md, docs/, agent .md), use descriptive references like 'scanner-matching payload' rather than quoting the literal pattern; ref DEFECT.PROMPT-INJECTION-SCAN-COLLISION (the older XML-tag-collision variant)`
`DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.symptom=workflow wrapper file (e.g. Windsurf convertClaudeCommandToWindsurfWorkflow) delegates to a command body at <targetDir>/gsd-core/commands/gsd/X.md via a hardcoded @~/.claude/gsd-core/commands/gsd/ path that _applyRuntimeRewrites rewrites to the install target; the source gsd-core/ dir ships without commands/ (it lives at package-root commands/gsd/); install completes successfully, workflow files appear in the / menu, but invocation tells the LLM to read a file that does not exist; the slash commands silently fail`
`DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.examples=PR #1622 (issue #1615) shipped Windsurf /gsd-* workflow wrappers that all reference <targetDir>/.windsurf/gsd-core/commands/gsd/X.md; that directory was never populated; none of the reviews (security, Codex adversarial, Memtrace) caught it; a #1629 regression test verifying 'every workflow @- reference target exists on disk' surfaced it post-merge`
`DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.detect=after install, for every workflow .md file under <targetDir>/<runtime-config-dir>/workflows/, extract the @<path> reference from the body and assert fs.existsSync(path); if any reference target is absent, this defect is present`
`DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.fix-forward=copy the canonical command source (commands/gsd/*.md) into <targetDir>/gsd-core/commands/gsd/ during install, gated on the runtime that uses workflow delegation (currently Windsurf local only); use copyWithPathReplacement to apply the same path+brand rewrites as the rest of the install; verify with a regression test that every workflow's @-reference resolves`
`DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED.prevention=any new converter that emits a wrapper file delegating to another file MUST verify the delegation target is actually written by the same install; add a post-install invariant test: for every @<path> reference in every generated wrapper, assert the target exists; the workflow converter's hardcoded path was copy-pasted from Claude's skill pattern without verifying the target exists for the new runtime`
---

View File

@@ -9905,6 +9905,23 @@ function install(isGlobal, runtime = 'claude', options = {}) {
failures.push('gsd-core');
}
// #1629 critical fix: Windsurf workflow wrappers (convertClaudeCommandToWindsurfWorkflow)
// delegate to command bodies at <targetDir>/gsd-core/commands/gsd/${stem}.md via a
// hardcoded @~/.claude/gsd-core/commands/gsd/ path that _applyRuntimeRewrites rewrites
// to the install target. The source gsd-core/ dir does NOT ship with commands/ —
// the canonical command source lives at the package root (commands/gsd/). Without
// this copy, every /gsd-* workflow in Cascade references a missing file and the LLM
// cannot execute the command body. Surfaced by the #1629 regression test after the
// original adversarial review of #1622 missed it.
if (isWindsurf && !isGlobal) {
const commandsSrc = path.join(src, 'commands', 'gsd');
const commandsDest = path.join(skillDest, 'commands', 'gsd');
if (fs.existsSync(commandsSrc)) {
copyWithPathReplacement(commandsSrc, commandsDest, pathPrefix, runtime, true, isGlobal);
console.log(` ${green}✓${reset} Installed command bodies to gsd-core/commands/gsd/ (workflow delegation targets)`);
}
}
// Copy shared manifests into the gsd-core payload
// at the co-located path that CJS modules resolve first:
// gsd-core/bin/shared/*.json

View File

@@ -1330,6 +1330,33 @@ describe('windsurf local install writes workflow slash commands (#1615)', () =>
}
});
// #1629 Finding A: every workflow's @-reference target must exist on disk
// after install. Pre-fix, gsd-core/ was copied AFTER workflows were written;
// a throw or kill in that window left workflows pointing at missing files.
// Post-fix, gsd-core/ is copied first. This behavioral invariant catches
// any ordering regression that leaves a workflow target absent.
test('every workflow @-reference target exists on disk after install (#1629 Finding A)', () => {
install(false, 'windsurf');
const workflowsDir = path.join(tmpDir, '.windsurf', 'workflows');
const workflowEntries = fs.readdirSync(workflowsDir, { withFileTypes: true })
.filter(e => e.isFile() && e.name.startsWith('gsd-') && e.name.endsWith('.md'));
assert.ok(workflowEntries.length > 0, 'pre-condition: at least one gsd-* workflow must be installed');
const commandsGsdDir = path.join(tmpDir, '.windsurf', 'gsd-core', 'commands', 'gsd');
assert.ok(fs.existsSync(commandsGsdDir),
`gsd-core/commands/gsd/ must exist at ${commandsGsdDir} so workflows can delegate to it`);
for (const workflowEntry of workflowEntries) {
// Workflow naming convention: gsd-<stem>.md → delegates to commands/gsd/<stem>.md
const stem = workflowEntry.name.replace(/^gsd-/, '').replace(/\.md$/, '');
const targetFile = path.join(commandsGsdDir, `${stem}.md`);
assert.ok(
fs.existsSync(targetFile),
`${workflowEntry.name} delegates to commands/gsd/${stem}.md, but that file does not exist at ${targetFile}`,
);
}
});
test('global windsurf install does not write unsupported workflows or skills', () => {
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-ws-global-'));
const savedHome = process.env.HOME;