From b65939cdff9d375ed07291c128a840b6bf62f23e Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Tue, 23 Jun 2026 15:28:54 -0400 Subject: [PATCH] fix(#1629): copy Windsurf command bodies so workflow delegation targets exist MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #1622 (issue #1615) shipped Windsurf /gsd-* workflow wrappers that delegate to command bodies at /.windsurf/gsd-core/commands/gsd/X.md via a hardcoded @~/.claude/gsd-core/commands/gsd/ path. The path-rewrite pipeline correctly substitutes ~/.claude/ to the install target. But 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 file that does not exist. The slash commands appear in the / menu but silently fail when invoked because the LLM is told to read a missing file. None of the original reviews caught this: not the security review, not Codex's adversarial orthogonal review (gpt-5.5/high), not Memtrace's graph-backed review. It was surfaced by a #1629 regression test that verifies 'every workflow @-reference target exists on disk after install' — the test failed, revealing the bug. Fix: for Windsurf local installs, copy commands/gsd/*.md into /gsd-core/commands/gsd/ via copyWithPathReplacement (applies the same path+brand rewrites as the rest of the install). Guarded on isWindsurf && !isGlobal since global Windsurf workflow install is an explicit no-op. Documented as DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED in CONTEXT.md so the pattern is locked in: any new converter emitting a wrapper that delegates to another file MUST verify the delegation target is actually installed. --- CONTEXT.md | 6 ++++++ bin/install.js | 17 +++++++++++++++++ tests/install.test.cjs | 27 +++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) diff --git a/CONTEXT.md b/CONTEXT.md index 003d3eeef..ebb9f8344 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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 /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 /.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 //workflows/, extract the @ 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 /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 @ 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` + --- diff --git a/bin/install.js b/bin/install.js index 6a4efe8d1..6c0a018fe 100755 --- a/bin/install.js +++ b/bin/install.js @@ -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 /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 diff --git a/tests/install.test.cjs b/tests/install.test.cjs index 22ab9e9d6..2c24b8cdd 100644 --- a/tests/install.test.cjs +++ b/tests/install.test.cjs @@ -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-.md → delegates to commands/gsd/.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;