* feat(#784): emit native skills for OpenCode + Kilo runtimes OpenCode and Kilo share a config schema and both discover on-demand skills from skills/<name>/SKILL.md. The installer previously emitted only flat commands (command/) and file-based agents (agents/) for these runtimes. Add a shared OpenCode-family skill writer that stages each GSD command as a spec-compliant SKILL.md (name matching the directory, description 1-1024 chars), wired through the runtime artifact layout so uninstall cleans skills/ automatically. Skills respect the active install profile. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#784): correct skill body paths + preserve user dev-preferences Address adversarial-review findings: - Add opencode/kilo cases to _applyRuntimeRewrites so staged SKILL.md bodies are re-pointed from the converter's hardcoded default config dir to the actual install target (fixes --local / --config-dir installs; commands/agents already did this by applying pathPrefix pre-conversion). - Preserve user-owned skills/gsd-dev-preferences across reinstall in installOpencodeFamilySkills (snapshot+restore around the gsd-* prune), matching installRuntimeArtifacts. - Export installOpencodeFamilySkills and add regression tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#784): guarantee command/skill body parity, fix kilo-alt double-rewrite Follow-up adversarial-review found the post-conversion path rewrite could double-rewrite custom Kilo dirs (kilo -> kilo-alt -> kilo-alt-alt) because the kilo pathPrefix is a $HOME (non-absolute) superset of the hardcoded default base. Restructure so OpenCode/Kilo skills mirror copyFlattenedCommands exactly: stage raw commands, apply pathPrefix BEFORE conversion via a new shared applyOpencodeFamilyPathPrefix() helper (now used by both the command and skill writers), then convert. This guarantees byte-for-byte command/ skill body parity for global, --local, and --config-dir installs and removes the prefix-overlap hazard. Drop the fragile _applyRuntimeRewrites opencode/ kilo case. Strengthen the path regression test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor(#784): derive opencode/kilo skills from the same staged command set Pass the installer's _stageSkills() output directly to installOpencodeFamilySkills instead of re-staging via the layout, so the command/ and skills/ surfaces always cover the identical profile-resolved set — including the --minimal/--core-only alias path, which stages differently from a plain --profile=core. Verified: minimal install now emits 8 commands and 8 skills. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(#784): set changeset PR number to 810 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#784): fully escape backslashes in test helper (CodeQL js/incomplete-string-escaping) Replace the dot-only escape `replace(/[.]/g, '\\.')` with a complete regex-escape pattern `replace(/[\\.*+?^${}()|[\]]/g, '\\$&')` so all regex metacharacters (including backslash itself) in `defaultBase` are safely escaped before interpolation into `new RegExp(...)`. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,8 @@ const {
|
||||
convertClaudeToOpencodeFrontmatter,
|
||||
convertClaudeToKiloFrontmatter,
|
||||
convertClaudeToGeminiAgent,
|
||||
convertClaudeCommandToOpencodeSkill,
|
||||
convertClaudeCommandToKiloSkill,
|
||||
neutralizeAgentReferences,
|
||||
} = require('../bin/install.js');
|
||||
|
||||
@@ -344,3 +346,70 @@ describe('neutralizeAgentReferences', () => {
|
||||
assert.ok(result.includes('claude-code'), 'claude-code preserved');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── OpenCode-family skill converters (SKILL.md) — #784 ──────────────────────
|
||||
|
||||
const SKILL_SAMPLE_COMMAND = `---
|
||||
description: Show available GSD commands and usage guide
|
||||
argument-hint: "[topic]"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Bash
|
||||
---
|
||||
|
||||
Run \`/gsd:help\` to see the guide. AskUserQuestion when unsure.
|
||||
`;
|
||||
|
||||
const SKILL_BETA_COMMAND = `---
|
||||
description: "[BETA] Offload plan phase to the cloud and import back."
|
||||
---
|
||||
|
||||
Body for /gsd:ultraplan-phase.
|
||||
`;
|
||||
|
||||
describe('convertClaudeCommandToOpencodeSkill / convertClaudeCommandToKiloSkill (#784)', () => {
|
||||
const cases = [
|
||||
{ label: 'opencode', convert: convertClaudeCommandToOpencodeSkill },
|
||||
{ label: 'kilo', convert: convertClaudeCommandToKiloSkill },
|
||||
];
|
||||
|
||||
for (const { label, convert } of cases) {
|
||||
describe(`${label} skill conversion`, () => {
|
||||
test('emits SKILL.md frontmatter with name matching the skill dir', () => {
|
||||
const out = convert(SKILL_SAMPLE_COMMAND, 'gsd-help');
|
||||
assert.ok(out.startsWith('---\n'), 'opens with frontmatter');
|
||||
assert.match(out, /^name: gsd-help$/m, 'name equals the skill name');
|
||||
});
|
||||
|
||||
test('preserves the description from the source command', () => {
|
||||
const out = convert(SKILL_SAMPLE_COMMAND, 'gsd-help');
|
||||
assert.match(out, /^description: "Show available GSD commands and usage guide"$/m);
|
||||
});
|
||||
|
||||
test('drops the command tools/permission block (skills inherit perms)', () => {
|
||||
const out = convert(SKILL_SAMPLE_COMMAND, 'gsd-help');
|
||||
const fmEnd = out.indexOf('\n---', 4);
|
||||
const fm = out.slice(0, fmEnd);
|
||||
assert.ok(!/tools:/.test(fm), 'no tools block in skill frontmatter');
|
||||
assert.ok(!/permission:/.test(fm), 'no permission block in skill frontmatter');
|
||||
});
|
||||
|
||||
test('rewrites /gsd: colon refs to hyphen form in the body', () => {
|
||||
const out = convert(SKILL_SAMPLE_COMMAND, 'gsd-help');
|
||||
assert.ok(!/\/gsd:/.test(out), 'no /gsd: colon refs remain');
|
||||
assert.match(out, /\/gsd-help/, 'colon ref rewritten to hyphen form');
|
||||
});
|
||||
|
||||
test('quotes descriptions with leading YAML flow indicators ([BETA])', () => {
|
||||
const out = convert(SKILL_BETA_COMMAND, 'gsd-ultraplan-phase');
|
||||
assert.match(out, /^description: "\[BETA\] /m, 'leading [BETA] safely quoted');
|
||||
});
|
||||
|
||||
test('falls back to a synthetic description when none present', () => {
|
||||
const out = convert('Body only, no frontmatter.', 'gsd-mystery');
|
||||
assert.match(out, /^name: gsd-mystery$/m);
|
||||
assert.match(out, /^description: "Run GSD workflow gsd-mystery\."$/m);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user