feat(#789): elevate CodeBuddy — slash commands (#830)

* feat(#789): elevate CodeBuddy — emit slash commands (+ document subagent/MCP scope)

Emit a CodeBuddy slash-command surface so GSD workflows appear in the
'/' menu, reaching parity with other elevated runtimes.

- Add convertClaudeCommandToCodebuddyCommand and register a commands/
  artifact kind for the codebuddy runtime (commands/gsd-<name>.md),
  consistent with the Cursor (#785) and Augment (#790) commands surfaces.
- Mark emitted skills user-invocable:false so the commands surface is the
  sole '/' entry point (no duplicate /gsd-* entries); skills stay
  model-invocable. CodeBuddy's SKILL.md supports this field.
- Normalize $HOME/.codebuddy (bare + slash) path forms in runtime
  rewrites so --config-dir/local installs don't leak the default home.
- Report installed commands/ count on install; uninstall prunes gsd-*
  commands while preserving user-owned commands.

Scope: subagents (~/.codebuddy/agents/) are already emitted by the
generic agents block (unchanged); no mcp.json is written (gsd ships no
MCP server, and CodeBuddy's mcp.json registers only external servers).

Closes #789

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#789): set changeset pr number to 830

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-07 18:56:47 -04:00
committed by GitHub
parent 28c8d524fe
commit cb284962bf
9 changed files with 357 additions and 9 deletions

View File

@@ -0,0 +1,7 @@
---
type: Added
pr: 830
---
**CodeBuddy (Tencent) installs now emit `/gsd-*` slash commands.** A `--codebuddy` install writes `commands/gsd-<name>.md` files to `~/.codebuddy/commands/` so GSD workflows are invokable from CodeBuddy's `/` menu (`/gsd-phase`, `/gsd-ship`, etc.), matching the integration depth of other fully-elevated runtimes (#789). The existing `skills/gsd-<name>/SKILL.md` files are now emitted with `user-invocable: false` so they stay out of the `/` menu — the commands surface is the single `/` entry point (no duplicate entries) and skills remain available for model invocation. Subagents (`~/.codebuddy/agents/`) were already emitted and are unchanged. Uninstall removes the `gsd-*` command files while preserving user-owned commands. No `mcp.json` is written — gsd ships no MCP server and CodeBuddy's `mcp.json` only registers external MCP servers.
<!-- docs-exempt-not-used: user-facing install behaviour is documented in docs/USER-GUIDE.md and docs/how-to/install-on-your-runtime.md -->

View File

@@ -2843,7 +2843,47 @@ function convertClaudeCommandToCodebuddySkill(content, skillName) {
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
// #2876: quote so YAML flow indicators (`[BETA] …`) don't break
// CodeBuddy's frontmatter parser.
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`;
//
// #789: mark user-invocable:false so the skill is NOT shown in CodeBuddy's
// '/' menu (it defaults to true). The commands/ surface (#789) is the sole
// '/' entry point; skills remain model-invocable background knowledge,
// avoiding a duplicated /gsd-* entry per workflow.
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n${body}`;
}
/**
* Convert a Claude Code slash-command (.md) to a CodeBuddy slash-command (.md).
*
* CodeBuddy reads user-level slash commands from ~/.codebuddy/commands/<name>.md
* (https://www.codebuddy.ai/docs/cli/slash-commands). The filename determines the
* command name (gsd-help.md → /gsd-help), so the Claude-specific `name: gsd:<x>`
* frontmatter field is dropped. CodeBuddy command frontmatter supports
* `description` and `argument-hint`; both are preserved when present. The body is
* brand/path-converted via convertClaudeToCodebuddyMarkdown.
*
* @param {string} content raw Claude command markdown
* @param {string} commandName installed command name (e.g. 'gsd-help')
* @returns {string}
*/
function convertClaudeCommandToCodebuddyCommand(content, commandName) {
const converted = convertClaudeToCodebuddyMarkdown(content);
const { frontmatter, body } = extractFrontmatterAndBody(converted);
let description = `Run GSD workflow ${commandName}.`;
let argumentHint = '';
if (frontmatter) {
const maybeDescription = extractFrontmatterField(frontmatter, 'description');
if (maybeDescription) description = maybeDescription;
const maybeArgHint = extractFrontmatterField(frontmatter, 'argument-hint');
if (maybeArgHint) argumentHint = maybeArgHint;
}
description = toSingleLine(description);
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
// #2876: quote values so YAML flow indicators (`[BETA] …`, `[name]`) don't
// break CodeBuddy's frontmatter parser.
const lines = ['---', `description: ${yamlQuote(shortDescription)}`];
if (argumentHint) lines.push(`argument-hint: ${yamlQuote(toSingleLine(argumentHint))}`);
lines.push('---', body.trimStart());
return lines.join('\n');
}
function convertClaudeAgentToCodebuddyAgent(content) {
@@ -6934,7 +6974,14 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix) {
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
// The codebuddy converter rewrites `.claude/` → `.codebuddy/` at stage
// time, so `$HOME/.claude/...` arrives here as `$HOME/.codebuddy/...`.
// Normalize BOTH the `~/` and `$HOME/` forms (slash + bare) to the install
// target so `--config-dir`/local installs don't leak the default home.
content = content.replace(/~\/\.codebuddy\//g, pathPrefix);
content = content.replace(/\$HOME\/\.codebuddy\//g, pathPrefix);
content = content.replace(/~\/\.codebuddy\b/g, normalizedPathPrefix);
content = content.replace(/\$HOME\/\.codebuddy\b/g, normalizedPathPrefix);
content = processAttribution(content, getCommitAttribution(runtime));
break;
@@ -9664,6 +9711,22 @@ function install(isGlobal, runtime = 'claude', options = {}) {
failures.push('commands/gsd-*');
}
}
// CodeBuddy only: also report the commands/ output (#789 — slash commands)
if (isCodebuddy) {
const commandsDir = path.join(targetDir, 'commands');
if (fs.existsSync(commandsDir)) {
const cmdCount = fs.readdirSync(commandsDir)
.filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
if (cmdCount > 0) {
console.log(` ${green}✓${reset} Installed ${cmdCount} slash commands to commands/`);
} else {
failures.push('commands/gsd-*');
}
} else {
failures.push('commands/gsd-*');
}
}
}
} else if (isOpencode || isKilo) {
// OpenCode/Kilo: flat structure in command/ directory
@@ -12122,6 +12185,7 @@ module.exports = {
convertClaudeAgentToTraeAgent,
convertClaudeToCodebuddyMarkdown,
convertClaudeCommandToCodebuddySkill,
convertClaudeCommandToCodebuddyCommand,
convertClaudeAgentToCodebuddyAgent,
convertClaudeToCliineMarkdown,
convertClaudeCommandToClineSkill,

View File

@@ -808,7 +808,7 @@ The migration-specific ownership and source snapshots live in
| Trae | `~/.trae` | `./.trae` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Rule references under `rules/`; no GSD hooks |
| Qwen Code | `~/.qwen` | `./.qwen` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Common GSD settings and hook entries where supported |
| Hermes Agent | `~/.hermes` | `./.hermes` | `skills/gsd/DESCRIPTION.md` plus `skills/gsd/gsd-*/SKILL.md` | `agents/gsd-*.md` | Common GSD settings and hook entries where supported |
| CodeBuddy | `~/.codebuddy` | `./.codebuddy` | `skills/gsd-*/SKILL.md` | `agents/gsd-*.md` | Common GSD settings and hook entries where supported |
| CodeBuddy | `~/.codebuddy` | `./.codebuddy` | `skills/gsd-*/SKILL.md` (`user-invocable: false`) | `agents/gsd-*.md` | `/gsd-*` slash commands under `commands/`; common GSD settings and hook entries where supported |
| Cline | `~/.cline` | project root | `.clinerules` | Rules only | No GSD hooks or statusline |
### Upstream Contract Sources

View File

@@ -2290,7 +2290,7 @@ Test suite that scans all agent, workflow, and command files for embedded inject
**Requirements:**
- REQ-CLINE-02: Cline install MUST write `.clinerules` to `~/.cline/` (global) or `./.cline/` (local). No custom slash commands — rules-based integration only. Flag: `--cline`.
- REQ-CODEBUDDY-01: CodeBuddy install MUST deploy skills to `~/.codebuddy/skills/gsd-*/SKILL.md`. Flag: `--codebuddy`.
- REQ-CODEBUDDY-01: CodeBuddy install MUST deploy skills to `~/.codebuddy/skills/gsd-*/SKILL.md` (emitted `user-invocable: false`), `/gsd-*` slash commands to `~/.codebuddy/commands/gsd-*.md`, and subagents to `~/.codebuddy/agents/gsd-*.md`. The commands surface is the sole `/` menu entry point. No `mcp.json` is written (gsd ships no MCP server). Flag: `--codebuddy`.
- REQ-QWEN-01: Qwen Code install MUST deploy skills to `~/.qwen/skills/gsd-*/SKILL.md`, following the open standard used by Claude Code 2.1.88+. `QWEN_CONFIG_DIR` env var overrides the default path. Flag: `--qwen`.
**Runtime summary:**

View File

@@ -746,6 +746,8 @@ npx @opengsd/gsd-core --cline --local # this project only
npx @opengsd/gsd-core --codebuddy --global
```
GSD installs four surfaces for CodeBuddy: `/gsd-*` slash commands in `~/.codebuddy/commands/`, subagents in `~/.codebuddy/agents/`, model-invocable skills in `~/.codebuddy/skills/`, and `settings.json` hooks. The skills are emitted with `user-invocable: false` so the slash commands are the single `/` menu surface (no duplicate entries).
### Installing for Qwen Code
```bash

View File

@@ -281,7 +281,7 @@ directory is created for local scope.
npx @opengsd/gsd-core@latest --codebuddy --global
```
Skills land in `~/.codebuddy/skills/gsd-*/SKILL.md`.
GSD installs four surfaces. Slash command definitions land in `~/.codebuddy/commands/gsd-*.md` and appear as `/gsd-help`, `/gsd-phase`, `/gsd-ship`, etc. in the `/` menu. Subagents land in `~/.codebuddy/agents/gsd-*.md`. Skills land in `~/.codebuddy/skills/gsd-*/SKILL.md` — emitted with `user-invocable: false` so they stay out of the `/` menu (the commands surface is the sole `/` entry point) and remain available for model invocation. CodeBuddy hooks are written to `settings.json`. No `mcp.json` is written: GSD ships no MCP server.
---

View File

@@ -345,7 +345,17 @@ function resolveRuntimeArtifactLayout(runtime: string, configDir: string, scope:
break;
case 'codebuddy':
kinds = [skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodebuddySkill', 'codebuddy', configDir)];
// CodeBuddy (Tencent) reads two user-level surfaces (codebuddy.ai/docs/cli):
// 1. commands/gsd-<name>.md — slash commands shown in the '/' menu (#789)
// 2. skills/gsd-<name>/SKILL.md — model-invocable skills, emitted with
// user-invocable:false so they stay OUT of '/' (the commands surface is
// the sole '/' entry point) — avoids a duplicated /gsd-* per workflow.
// Subagents (~/.codebuddy/agents/) are already emitted by the generic agents
// block in bin/install.js; MCP is excluded (gsd ships no MCP server).
kinds = [
convertedCommandsKind('commands', 'gsd-', 'convertClaudeCommandToCodebuddyCommand', configDir),
skillsKind('skills', 'gsd-', 'convertClaudeCommandToCodebuddySkill', 'codebuddy', configDir),
];
break;
case 'cline':

View File

@@ -0,0 +1,259 @@
// allow-test-rule: source-text-is-the-product
// Workflow .md / command .md / SKILL.md files — their text IS what the runtime
// loads. Testing emitted text tests the deployed contract.
// Per CONTRIBUTING.md exception matrix.
/**
* Regression guard — enh(#789): elevate CodeBuddy slash-command surface.
*
* CodeBuddy (Tencent, @tencent-ai/codebuddy-code) reads user-level surfaces
* (https://www.codebuddy.ai/docs/cli/slash-commands, /skills):
* - commands/gsd-<stem>.md — slash commands shown in the '/' menu
* - skills/gsd-<stem>/SKILL.md — model-invocable skills
*
* Before #789 gsd emitted only skills/. Because CodeBuddy skills default to
* user-invocable:true (appear in '/'), emitting a commands/ surface AND leaving
* skills user-invocable would duplicate every /gsd-* entry. #789 therefore:
* 1. emits commands/gsd-<stem>.md (the '/' surface, peer-consistent with
* Cursor #785 and Augment #790),
* 2. marks skills user-invocable:false so they become model-invocable
* background knowledge and the commands/ surface is the sole '/' surface.
*
* Subagents are already emitted via the generic agents block + convertClaude
* AgentToCodebuddyAgent (~/.codebuddy/agents/), so #789 adds no agents change.
*
* mcp.json is intentionally NOT written: gsd ships no MCP server, and CodeBuddy's
* mcp.json holds an `mcpServers` map of *external* servers to connect to —
* there is nothing for gsd to register. Same exclusion as #784/#785/#790.
*/
'use strict';
process.env.GSD_TEST_MODE = '1';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createTempDir, cleanup } = require('./helpers.cjs');
const {
installRuntimeArtifacts,
uninstallRuntimeArtifacts,
convertClaudeCommandToCodebuddyCommand,
convertClaudeCommandToCodebuddySkill,
} = require('../bin/install.js');
const { resolveRuntimeArtifactLayout } = require('../gsd-core/bin/lib/runtime-artifact-layout.cjs');
const { loadSkillsManifest, resolveProfile } = require('../gsd-core/bin/lib/install-profiles.cjs');
const REAL_COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
const MANIFEST = loadSkillsManifest(REAL_COMMANDS_DIR);
const RESOLVED_CORE = resolveProfile({ modes: ['core'], manifest: MANIFEST });
// ─── Layout contract ─────────────────────────────────────────────────────────
describe('enh-789 — codebuddy layout has commands + skills kinds', () => {
test('resolveRuntimeArtifactLayout codebuddy returns 2 kinds', () => {
const layout = resolveRuntimeArtifactLayout('codebuddy', '/tmp/fake-codebuddy-dir');
assert.strictEqual(layout.kinds.length, 2, 'codebuddy must have exactly 2 artifact kinds');
const kindNames = layout.kinds.map(k => k.kind).sort();
assert.deepStrictEqual(kindNames, ['commands', 'skills']);
});
test('codebuddy commands kind targets commands/ with gsd- prefix', () => {
const layout = resolveRuntimeArtifactLayout('codebuddy', '/tmp/fake-codebuddy-dir');
const commandsKind = layout.kinds.find(k => k.kind === 'commands');
assert.ok(commandsKind, 'must have commands kind');
assert.strictEqual(commandsKind.destSubpath, 'commands');
assert.strictEqual(commandsKind.prefix, 'gsd-');
assert.strictEqual(typeof commandsKind.stage, 'function');
});
test('codebuddy skills kind targets skills/ with gsd- prefix', () => {
const layout = resolveRuntimeArtifactLayout('codebuddy', '/tmp/fake-codebuddy-dir');
const skillsKind = layout.kinds.find(k => k.kind === 'skills');
assert.ok(skillsKind, 'must have skills kind');
assert.strictEqual(skillsKind.destSubpath, 'skills');
assert.strictEqual(skillsKind.prefix, 'gsd-');
});
});
// ─── Command converter contract ──────────────────────────────────────────────
describe('enh-789 — convertClaudeCommandToCodebuddyCommand', () => {
const SRC = [
'---',
'name: gsd:new-project',
'description: Initialize a project',
'argument-hint: "[name]"',
'allowed-tools:',
' - Read',
'---',
'',
'Use .claude/skills/ and run /gsd:help. Claude Code reads CLAUDE.md.',
'',
].join('\n');
test('emits a description-only frontmatter (no Claude-specific name: gsd:)', () => {
const out = convertClaudeCommandToCodebuddyCommand(SRC, 'gsd-new-project');
assert.ok(out.startsWith('---\n'), 'must begin with frontmatter');
assert.ok(/^description:/m.test(out), 'must carry a description field');
assert.ok(!out.includes('name: gsd:new-project'), 'must drop Claude colon-form name field');
});
test('preserves a present argument-hint (CodeBuddy supports it)', () => {
const out = convertClaudeCommandToCodebuddyCommand(SRC, 'gsd-new-project');
assert.ok(/^argument-hint:\s*["']?\[name\]["']?\s*$/m.test(out),
`argument-hint must be carried through when present in source. Got:\n${out}`);
});
test('converts body Claude-isms to CodeBuddy equivalents', () => {
const out = convertClaudeCommandToCodebuddyCommand(SRC, 'gsd-new-project');
assert.ok(out.includes('.codebuddy/skills/'), out);
assert.ok(out.includes('/gsd-help'), out);
assert.ok(out.includes('CODEBUDDY.md'), out);
assert.ok(!/\bClaude Code\b/.test(out), 'must rebrand "Claude Code"');
});
});
describe('enh-789 — skills marked user-invocable:false', () => {
test('convertClaudeCommandToCodebuddySkill emits user-invocable: false', () => {
const src = [
'---',
'name: gsd:help',
'description: Show help',
'---',
'',
'# body',
'',
].join('\n');
const out = convertClaudeCommandToCodebuddySkill(src, 'gsd-help');
assert.ok(/^user-invocable:\s*false\s*$/m.test(out),
`SKILL.md frontmatter must hide skill from '/' menu (user-invocable: false). Got:\n${out}`);
});
});
// ─── Install contract ────────────────────────────────────────────────────────
describe('enh-789 — installRuntimeArtifacts codebuddy emits commands and skills', () => {
test('global codebuddy install: commands/gsd-help.md and skills/gsd-help/SKILL.md exist', (t) => {
const configDir = createTempDir('gsd-enh789-codebuddy-');
t.after(() => cleanup(configDir));
installRuntimeArtifacts('codebuddy', configDir, 'global', RESOLVED_CORE);
const commandsDir = path.join(configDir, 'commands');
assert.ok(fs.existsSync(commandsDir), 'commands/ dir must exist');
const cmdFiles = fs.readdirSync(commandsDir).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
assert.ok(cmdFiles.length > 0, 'at least one gsd-*.md command file must be installed');
assert.ok(fs.existsSync(path.join(commandsDir, 'gsd-help.md')), 'commands/gsd-help.md must exist');
const skillsDir = path.join(configDir, 'skills');
assert.ok(fs.existsSync(skillsDir), 'skills/ dir must exist');
assert.ok(fs.existsSync(path.join(skillsDir, 'gsd-help', 'SKILL.md')), 'skills/gsd-help/SKILL.md must exist');
});
test('installed commands/gsd-help.md is CodeBuddy-compatible (no raw ~/.claude/, rebranded)', (t) => {
const configDir = createTempDir('gsd-enh789-content-');
t.after(() => cleanup(configDir));
installRuntimeArtifacts('codebuddy', configDir, 'global', RESOLVED_CORE);
const helpCmd = path.join(configDir, 'commands', 'gsd-help.md');
const content = fs.readFileSync(helpCmd, 'utf8');
assert.ok(!content.includes('~/.claude/'), 'commands must not contain raw ~/.claude/ refs');
assert.ok(content.startsWith('---'), 'commands must carry frontmatter');
});
test('installed skills/gsd-help/SKILL.md is hidden from the / menu', (t) => {
const configDir = createTempDir('gsd-enh789-skillhide-');
t.after(() => cleanup(configDir));
installRuntimeArtifacts('codebuddy', configDir, 'global', RESOLVED_CORE);
const skill = fs.readFileSync(path.join(configDir, 'skills', 'gsd-help', 'SKILL.md'), 'utf8');
assert.ok(/^user-invocable:\s*false\s*$/m.test(skill),
'installed SKILL.md must set user-invocable: false');
});
test('command count matches skill count (profile parity)', (t) => {
const configDir = createTempDir('gsd-enh789-parity-');
t.after(() => cleanup(configDir));
installRuntimeArtifacts('codebuddy', configDir, 'global', RESOLVED_CORE);
const cmdCount = fs.readdirSync(path.join(configDir, 'commands'))
.filter(f => f.startsWith('gsd-') && f.endsWith('.md')).length;
const skillCount = fs.readdirSync(path.join(configDir, 'skills'), { withFileTypes: true })
.filter(e => e.isDirectory() && e.name.startsWith('gsd-')).length;
assert.strictEqual(cmdCount, skillCount, 'command count must equal skill count for same profile');
});
test('full profile install: no $HOME/.codebuddy or ~/.codebuddy leak in any command', (t) => {
// The codebuddy converter rewrites `.claude/` → `.codebuddy/`, so source
// refs like `@$HOME/.claude/gsd-core/...` (e.g. plan-review-convergence.md)
// must be normalized to the install target — not left as $HOME/.codebuddy.
const RESOLVED_FULL = resolveProfile({ modes: ['full'], manifest: MANIFEST });
const configDir = createTempDir('gsd-enh789-noleak-');
t.after(() => cleanup(configDir));
installRuntimeArtifacts('codebuddy', configDir, 'global', RESOLVED_FULL);
const commandsDir = path.join(configDir, 'commands');
for (const f of fs.readdirSync(commandsDir).filter(n => n.endsWith('.md'))) {
const content = fs.readFileSync(path.join(commandsDir, f), 'utf8');
assert.ok(!content.includes('$HOME/.codebuddy'), `${f} must not leak $HOME/.codebuddy`);
assert.ok(!content.includes('~/.codebuddy'), `${f} must not leak ~/.codebuddy`);
assert.ok(!content.includes('.claude/'), `${f} must not retain raw .claude/ refs`);
}
});
test('full profile install does NOT mutate source commands/gsd/ files', (t) => {
const RESOLVED_FULL = resolveProfile({ modes: ['full'], manifest: MANIFEST });
assert.strictEqual(RESOLVED_FULL.skills, '*', 'full profile must have skills === "*"');
const configDir = createTempDir('gsd-enh789-full-');
t.after(() => cleanup(configDir));
const srcHelpPath = path.join(REAL_COMMANDS_DIR, 'help.md');
const before = fs.readFileSync(srcHelpPath, 'utf8');
installRuntimeArtifacts('codebuddy', configDir, 'global', RESOLVED_FULL);
const after = fs.readFileSync(srcHelpPath, 'utf8');
assert.strictEqual(before, after, 'source commands/gsd/help.md must not be mutated by the install');
});
});
// ─── Uninstall contract ──────────────────────────────────────────────────────
describe('enh-789 — uninstallRuntimeArtifacts removes codebuddy commands', () => {
test('uninstall removes gsd-* commands but preserves user commands', (t) => {
const configDir = createTempDir('gsd-enh789-uninstall-');
t.after(() => cleanup(configDir));
const commandsDir = path.join(configDir, 'commands');
fs.mkdirSync(commandsDir, { recursive: true });
fs.writeFileSync(path.join(commandsDir, 'gsd-help.md'), '# help\n');
fs.writeFileSync(path.join(commandsDir, 'user-custom.md'), '# user\n');
uninstallRuntimeArtifacts('codebuddy', configDir, 'global');
assert.ok(!fs.existsSync(path.join(commandsDir, 'gsd-help.md')), 'gsd-help.md must be removed');
assert.ok(fs.existsSync(path.join(commandsDir, 'user-custom.md')), 'user-custom.md must be preserved');
});
});
// ─── mcp.json exclusion ──────────────────────────────────────────────────────
describe('enh-789 — mcp.json excluded (gsd ships no MCP server)', () => {
test('codebuddy install does not write mcp.json / .mcp.json', (t) => {
const configDir = createTempDir('gsd-enh789-mcp-excluded-');
t.after(() => cleanup(configDir));
installRuntimeArtifacts('codebuddy', configDir, 'global', RESOLVED_CORE);
assert.ok(!fs.existsSync(path.join(configDir, 'mcp.json')), 'must not write mcp.json');
assert.ok(!fs.existsSync(path.join(configDir, '.mcp.json')), 'must not write .mcp.json');
});
});

View File

@@ -203,15 +203,21 @@ describe('resolveRuntimeArtifactLayout — hermes', () => {
});
describe('resolveRuntimeArtifactLayout — codebuddy', () => {
test('returns correct layout for codebuddy', () => {
test('returns correct layout for codebuddy (commands + skills — #789)', () => {
const layout = resolveRuntimeArtifactLayout('codebuddy', FAKE_DIR);
assert.strictEqual(layout.runtime, 'codebuddy');
assert.strictEqual(layout.configDir, FAKE_DIR);
assert.strictEqual(layout.kinds.length, 1);
assert.strictEqual(layout.kinds[0].kind, 'skills');
assert.strictEqual(layout.kinds[0].destSubpath, 'skills');
assert.strictEqual(layout.kinds.length, 2);
// commands kind first
assert.strictEqual(layout.kinds[0].kind, 'commands');
assert.strictEqual(layout.kinds[0].destSubpath, 'commands');
assert.strictEqual(layout.kinds[0].prefix, 'gsd-');
assert.strictEqual(typeof layout.kinds[0].stage, 'function');
// skills kind second
assert.strictEqual(layout.kinds[1].kind, 'skills');
assert.strictEqual(layout.kinds[1].destSubpath, 'skills');
assert.strictEqual(layout.kinds[1].prefix, 'gsd-');
assert.strictEqual(typeof layout.kinds[1].stage, 'function');
});
});