diff --git a/.changeset/sturdy-badgers-bark.md b/.changeset/sturdy-badgers-bark.md new file mode 100644 index 000000000..f794b56db --- /dev/null +++ b/.changeset/sturdy-badgers-bark.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4847 +--- +**OpenCode installs no longer flag all 72 GSD skills as custom files** — the installer's file manifest now records the skills it stages for OpenCode, matching every other skills-layout runtime, so clean installs report zero custom files. The first update after upgrading may show the custom-files backup prompt once while the manifest catches up; from then on updates run without it. (#4738) diff --git a/capabilities/opencode/capability.json b/capabilities/opencode/capability.json index c4502e428..b38b01bc2 100644 --- a/capabilities/opencode/capability.json +++ b/capabilities/opencode/capability.json @@ -129,8 +129,7 @@ }, "skipHomePrefixSubstitution": true, "skipSettingsUi": true, - "skipUpdateBannerCommand": true, - "skipCodexSkillsManifest": true + "skipUpdateBannerCommand": true } }, "reviewer": { diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index af443b529..4ea16da94 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -2988,8 +2988,7 @@ const capabilities = { }, "skipHomePrefixSubstitution": true, "skipSettingsUi": true, - "skipUpdateBannerCommand": true, - "skipCodexSkillsManifest": true + "skipUpdateBannerCommand": true } }, "reviewer": { @@ -7213,8 +7212,7 @@ const runtimes = { }, "skipHomePrefixSubstitution": true, "skipSettingsUi": true, - "skipUpdateBannerCommand": true, - "skipCodexSkillsManifest": true + "skipUpdateBannerCommand": true } }, "reviewer": { diff --git a/gsd-core/bin/lib/capability-validator.cjs b/gsd-core/bin/lib/capability-validator.cjs index cbc8fc617..e46a68b93 100644 --- a/gsd-core/bin/lib/capability-validator.cjs +++ b/gsd-core/bin/lib/capability-validator.cjs @@ -1947,7 +1947,6 @@ const KNOWN_HOST_BEHAVIORS = new Set([ 'skillPriorityFrontmatter', 'skillsGlobalOnboarding', 'skillsManifestPrefix', - 'skipCodexSkillsManifest', 'skipHomePrefixSubstitution', 'skipSettingsUi', 'skipSharedHooksInstall', diff --git a/tests/opencode-imperative-reference.test.cjs b/tests/opencode-imperative-reference.test.cjs index 2894aff35..e724b73b1 100644 --- a/tests/opencode-imperative-reference.test.cjs +++ b/tests/opencode-imperative-reference.test.cjs @@ -14,7 +14,7 @@ * `runtime.hostBehaviors` + the combined-family engine install path). */ -const { test } = require('node:test'); +const { test, describe, beforeEach, afterEach } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); @@ -123,7 +123,11 @@ test('opencode descriptor declares runtime.hostBehaviors (the folded-in behavior assert.equal(hb.skipHomePrefixSubstitution, true); assert.equal(hb.skipSettingsUi, true); assert.equal(hb.skipUpdateBannerCommand, true); - assert.equal(hb.skipCodexSkillsManifest, true); + // #4738: the skills-manifest exclusion is retired — opencode stages 72 + // converted skills and its manifest must record them (the flag was carried + // verbatim from a pre-skills-layout era and flagged every staged skill as + // user-added on every update). + assert.equal(hb.skipCodexSkillsManifest, undefined); assert.equal(hb.nativePlugin.file, 'gsd-core.js'); assert.equal(hb.nativePlugin.source, '.opencode/plugins/gsd-core.js'); }); @@ -139,3 +143,73 @@ test('no `runtime === "opencode"` string-equality branch remains in the install assert.deepEqual(offenders, [], `AC2: no hardcoded runtime==='opencode' branch may remain in ${rel}; found: ${offenders.join(', ')}`); } }); + +// ─── #4738: the opencode manifest must record the skills it stages ─────────── + +const { cleanup, runGsdTools, createTempDir } = require('./helpers.cjs'); +const { writeManifest } = require(path.join(__dirname, '..', 'bin', 'install.js')); +const { runMinimalInstall } = require('./helpers/install-shared.cjs'); + +describe('#4738: writeManifest records staged skills for opencode', () => { + let tmpDir; + + beforeEach(() => { + tmpDir = createTempDir('gsd-4738-manifest-'); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('manifest includes skills/gsd-xxx/SKILL.md entries for opencode (mirrors the kilo sibling)', () => { + // Stage the structure an opencode install creates: converted skills under + // skills/gsd-*/SKILL.md (convertClaudeCommandToOpencodeSkill) plus the + // gsd-core directory writeManifest always records. + const skillDir = path.join(tmpDir, 'skills', 'gsd-next'); + fs.mkdirSync(skillDir, { recursive: true }); + fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'skill content'); + const gsdDir = path.join(tmpDir, 'gsd-core'); + fs.mkdirSync(gsdDir, { recursive: true }); + fs.writeFileSync(path.join(gsdDir, 'test.md'), 'test'); + + writeManifest(tmpDir, 'opencode'); + + const manifest = JSON.parse( + fs.readFileSync(path.join(tmpDir, 'gsd-file-manifest.json'), 'utf8'), + ); + const skillEntries = Object.keys(manifest.files).filter((k) => k.startsWith('skills/')); + assert.ok(skillEntries.length > 0, 'manifest has skills/ entries for opencode'); + assert.ok( + skillEntries.includes('skills/gsd-next/SKILL.md'), + 'manifest records the staged skills/gsd-next/SKILL.md', + ); + }); +}); + +describe('#4738: a clean opencode install is invisible to detect-custom-files', () => { + test('real --opencode --global install: manifest records staged skills and the detector reports zero custom files', (t) => { + const { manifest, configDir, root } = runMinimalInstall({ runtime: 'opencode', scope: 'global' }); + t.after(() => cleanup(root)); + + const skillEntries = Object.keys(manifest.files).filter((k) => k.startsWith('skills/')); + assert.ok( + skillEntries.length > 0, + `the manifest must record the staged skills (got ${skillEntries.length} skills/ keys)`, + ); + // The on-disk side is unchanged by the fix: every manifested skill exists. + for (const rel of skillEntries) { + assert.ok(fs.existsSync(path.join(configDir, rel)), `staged file exists: ${rel}`); + } + + const result = runGsdTools(['detect-custom-files', '--config-dir', configDir], root); + assert.ok(result.success, `detector failed: ${result.error}`); + const json = JSON.parse(result.output); + assert.ok(Array.isArray(json.custom_files), 'the detector must emit a custom_files array'); + assert.deepEqual( + json.custom_files.filter((f) => f.startsWith('skills/')), + [], + `no installer-staged skill may be flagged as user-added: ${JSON.stringify(json.custom_files)}`, + ); + assert.equal(json.custom_count, 0, `a clean opencode install reports zero custom files: ${JSON.stringify(json.custom_files)}`); + }); +});