diff --git a/get-shit-done/bin/lib/drift.cjs b/get-shit-done/bin/lib/drift.cjs index 581f23879..0723eb4aa 100644 --- a/get-shit-done/bin/lib/drift.cjs +++ b/get-shit-done/bin/lib/drift.cjs @@ -190,7 +190,7 @@ function detectDrift(input) { if (action === 'auto-remap') { spawnMapper = true; } - message = buildMessage(elements, affectedPaths, action); + message = buildMessage(elements, affectedPaths, action, input.projectDir); } return { @@ -228,7 +228,7 @@ function skipped(reason) { }; } -function buildMessage(elements, affectedPaths, action) { +function buildMessage(elements, affectedPaths, action, projectDir) { const byCat = {}; for (const e of elements) { (byCat[e.category] ||= []).push(e.path); @@ -254,7 +254,7 @@ function buildMessage(elements, affectedPaths, action) { lines.push(`Auto-remap scheduled for paths: ${affectedPaths.join(', ')}`); } else { const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs'); - const mapCmd = formatGsdSlash('map-codebase', resolveRuntime(null)); + const mapCmd = formatGsdSlash('map-codebase', resolveRuntime(projectDir)); lines.push( `Run ${mapCmd} --paths ${affectedPaths.join(',')} to refresh planning context.`, ); diff --git a/get-shit-done/bin/lib/gsd2-import.cjs b/get-shit-done/bin/lib/gsd2-import.cjs index 175271538..cb7bcd5cc 100644 --- a/get-shit-done/bin/lib/gsd2-import.cjs +++ b/get-shit-done/bin/lib/gsd2-import.cjs @@ -402,7 +402,7 @@ function buildPlanningArtifacts(gsd2Data) { /** * Format a dry-run preview string for display before writing. */ -function buildPreview(gsd2Data, artifacts) { +function buildPreview(gsd2Data, artifacts, projectDir) { const lines = ['Preview — files that will be created in .planning/:']; for (const rel of artifacts.keys()) { @@ -422,7 +422,7 @@ function buildPreview(gsd2Data, artifacts) { lines.push('Cannot migrate automatically:'); lines.push(' - GSD-2 cost/token ledger (no v1 equivalent)'); const { formatGsdSlash, resolveRuntime } = require('./runtime-slash.cjs'); - lines.push(` - GSD-2 database state (rebuilt from files on first ${formatGsdSlash('health', resolveRuntime(null))})`); + lines.push(` - GSD-2 database state (rebuilt from files on first ${formatGsdSlash('health', resolveRuntime(projectDir))})`); lines.push(' - VS Code extension state'); return lines.join('\n'); @@ -472,7 +472,7 @@ function cmdFromGsd2(args, cwd, raw) { const gsd2Data = parseGsd2(gsdDir); const artifacts = buildPlanningArtifacts(gsd2Data); - const preview = buildPreview(gsd2Data, artifacts); + const preview = buildPreview(gsd2Data, artifacts, cwd); if (dryRun) { return output({ success: true, dryRun: true, preview }, raw); diff --git a/get-shit-done/bin/lib/profile-output.cjs b/get-shit-done/bin/lib/profile-output.cjs index fd9b2029a..d6918c5d0 100644 --- a/get-shit-done/bin/lib/profile-output.cjs +++ b/get-shit-done/bin/lib/profile-output.cjs @@ -829,7 +829,7 @@ function cmdGenerateDevPreferences(cwd, options, raw) { const result = { command_path: outputPath, - command_name: '/gsd-dev-preferences', + command_name: formatGsdSlash('dev-preferences', resolveRuntime(cwd)), dimensions_included: dimensionsIncluded, source: analysis.data_source || 'session_analysis', }; diff --git a/get-shit-done/bin/lib/runtime-slash.cjs b/get-shit-done/bin/lib/runtime-slash.cjs index 1a197d13c..d1ba1ecd1 100644 --- a/get-shit-done/bin/lib/runtime-slash.cjs +++ b/get-shit-done/bin/lib/runtime-slash.cjs @@ -30,14 +30,23 @@ function formatGsdSlash(commandName, runtime) { const bare = stripped === commandName ? commandName : stripped; if (bare === '') return commandName; + // Split on the first whitespace so only the command token is rewritten — + // anything after the first space is caller-supplied arguments (phase + // numbers, --flags, --paths C:\\Users\\Me, etc.) that must round-trip + // untouched. Codex lowercases only the command token; preserving the + // argument tail prevents path/flag corruption on case-sensitive systems. + const wsMatch = bare.match(/^(\S+)(\s[\s\S]*)?$/); + const token = wsMatch ? wsMatch[1] : bare; + const tail = wsMatch && wsMatch[2] ? wsMatch[2] : ''; + const rt = String(runtime || 'claude').toLowerCase(); if (rt === 'codex') { - // Codex skills are invoked as $gsd- (shell-var syntax). Lowercased - // because shell-var identifiers in the surrounding prose are conventionally + // Codex skills are invoked as $gsd- (shell-var syntax). The command + // token is lowercased because shell-var identifiers are conventionally // lowercase; matches the convertCodexSlash() projection in bin/install.js. - return `$gsd-${bare.toLowerCase()}`; + return `$gsd-${token.toLowerCase()}${tail}`; } - return `/gsd-${bare}`; + return `/gsd-${token}${tail}`; } /** diff --git a/get-shit-done/bin/lib/verify.cjs b/get-shit-done/bin/lib/verify.cjs index 30a06556e..ced96f4bd 100644 --- a/get-shit-done/bin/lib/verify.cjs +++ b/get-shit-done/bin/lib/verify.cjs @@ -1358,6 +1358,7 @@ function cmdVerifyCodebaseDrift(cwd, raw) { structureMd, threshold, action, + projectDir: cwd, }); emit({ diff --git a/tests/bug-3584-runtime-slash-emitters.test.cjs b/tests/bug-3584-runtime-slash-emitters.test.cjs index 66e1b6810..eeba857e5 100644 --- a/tests/bug-3584-runtime-slash-emitters.test.cjs +++ b/tests/bug-3584-runtime-slash-emitters.test.cjs @@ -260,7 +260,7 @@ describe('bug-3584: validate context recommendation uses hyphen form', () => { }); describe('bug-3584: validate health uses formatter for codex runtime too', () => { - test('validate health under codex emits $gsd- in fix strings', (t) => { + test('validate health under codex emits $gsd- in fix strings (positive assertion)', (t) => { const tmpDir = createTempDir(); t.after(() => cleanup(tmpDir)); @@ -281,20 +281,30 @@ describe('bug-3584: validate health uses formatter for codex runtime too', () => .concat(payload.warnings || []) .concat(payload.info || []); - const fixesWithSlash = allIssues + // Collect fixes that mention any gsd slash-command form so we can lock + // both the absence of legacy forms AND the presence of the codex shape. + const fixesWithGsdRef = allIssues .map((i) => i.fix) - .filter((f) => typeof f === 'string' && /gsd-/.test(f)); + .filter((f) => typeof f === 'string' && /(?:\$|\/)gsd[-:]/.test(f)); - // For codex, hyphen-with-slash form `/gsd-` should be replaced by - // shell-var `$gsd-`. Skip the assertion if no relevant fix was - // produced (test only asserts the contract when something is emitted). - for (const fix of fixesWithSlash) { + assert.ok( + fixesWithGsdRef.length > 0, + 'validate health on a bare tmpdir must produce at least one fix hint referencing a gsd command', + ); + + for (const fix of fixesWithGsdRef) { + assert.ok( + fix.includes('$gsd-'), + `codex validate health fix must use shell-var $gsd- form, got ${JSON.stringify(fix)}`, + ); assert.ok( !fix.includes('/gsd:'), `codex validate health fix must not contain /gsd: colon form, got ${JSON.stringify(fix)}`, ); - // Codex emits $gsd- — any /gsd- in the fix means the formatter wasn't applied. - // (We don't have a hard count guarantee — a fix can also reference a plain url etc.) + assert.ok( + !fix.includes('/gsd-'), + `codex validate health fix must not contain /gsd- (skills) form, got ${JSON.stringify(fix)}`, + ); } }); }); diff --git a/tests/bug-3584-runtime-slash-formatter.test.cjs b/tests/bug-3584-runtime-slash-formatter.test.cjs index da4ff9ac6..92c6e7076 100644 --- a/tests/bug-3584-runtime-slash-formatter.test.cjs +++ b/tests/bug-3584-runtime-slash-formatter.test.cjs @@ -148,6 +148,29 @@ describe('formatGsdSlash — runtime-aware slash command formatter', () => { '/gsd-execute-phase 03', ); }); + + test('codex form lowercases only the command token, not the argument tail', () => { + // Regression for codex review finding: a previous implementation + // lowercased the full input including arguments, which would corrupt + // Windows paths and case-sensitive flag values passed as args. + assert.strictEqual( + formatGsdSlash('Map-Codebase --paths C:\\Users\\Me\\Project', 'codex'), + '$gsd-map-codebase --paths C:\\Users\\Me\\Project', + ); + assert.strictEqual( + formatGsdSlash('execute-phase 03 --Name FooBar', 'codex'), + '$gsd-execute-phase 03 --Name FooBar', + ); + }); + + test('hyphen form preserves token case (it does not get lowercased)', () => { + // Symmetry with codex: only codex lowercases the token. Hyphen-form + // runtimes preserve whatever case the caller supplied for the token. + assert.strictEqual( + formatGsdSlash('Plan-Phase 03', 'claude'), + '/gsd-Plan-Phase 03', + ); + }); }); });