fix(#4738): record opencode's staged skills in the install manifest (#4847)

* test(#4738): opencode manifest must record its staged skills (failing first)

* fix(#4738): record opencode's staged skills in the install manifest

* test(#4738): use the centralized temp-dir helper in the manifest tests

* fix(#4738): retire the dead hostBehaviors vocabulary entry, tighten detector asserts, temper changeset

* chore(#4738): backfill changeset PR number (4847)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-18 04:52:32 -04:00
committed by GitHub
parent c5629bbe74
commit 11b3091df0
5 changed files with 84 additions and 9 deletions

View File

@@ -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)

View File

@@ -129,8 +129,7 @@
},
"skipHomePrefixSubstitution": true,
"skipSettingsUi": true,
"skipUpdateBannerCommand": true,
"skipCodexSkillsManifest": true
"skipUpdateBannerCommand": true
}
},
"reviewer": {

View File

@@ -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": {

View File

@@ -1947,7 +1947,6 @@ const KNOWN_HOST_BEHAVIORS = new Set([
'skillPriorityFrontmatter',
'skillsGlobalOnboarding',
'skillsManifestPrefix',
'skipCodexSkillsManifest',
'skipHomePrefixSubstitution',
'skipSettingsUi',
'skipSharedHooksInstall',

View File

@@ -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)}`);
});
});