feat(#1682): OpenCode companion-MCP binding (mcp.gsd) — Phase 5 Slice 1a (#1929)

* docs: align PR-FLOW push gate from gsd-test-summary to gsd-test

gsd-test is the application (open-gsd/gsd-test-runner); gsd-test-summary is
the legacy local wrapper. RULESET.PR-FLOW.docker-before-push now names gsd-test
as the pre-push gate (exit 0 / verdict outcome 'passed').

* docs: drop WORKTREE.SEAM local node --test rule; align PROC dispatch to gsd-test

- Remove WORKTREE.SEAM.execution-rule (prefer local node --test) — contradicts
  CLAUDE.md 'NEVER run node --test locally'; CLAUDE.md wins.
- PROC.PARALLEL-FIX-DISPATCH: gsd-test-summary --both -> gsd-test (app; --both
  was a legacy wrapper flag).

* feat(#1682): OpenCode companion-MCP binding (mcp.gsd) — Phase 5 Slice 1a

configureOpencodePermissions registers the Phase-4 companion MCP server
(gsd-mcp-server) as opencode mcp.gsd, so OpenCode connects to GSD's command
(point 1) + state-IO (point 5) with no bespoke plugin (ADR-1239 Phase D).
Idempotent + non-clobbering (add-if-absent; respects a user-defined mcp.gsd).
Local-stdio schema per OpenCode config (packages/core/src/config/mcp.ts);
`-p @opengsd/gsd-core` resolves the bin (name != package) under npx.

Tests: registers-on-object-config; does-not-clobber-user-entry.

* docs(changeset): OpenCode companion-MCP binding (#1682)

* fix(#1682): use PACKAGE_NAME single-source (#516) + refresh opencode golden parity

- mcp.gsd command: replace hardcoded '@opengsd/gsd-core' literal with
  PACKAGE_NAME from gsd-core/bin/lib/package-identity.cjs (#516 single-source).
- opencode golden-install-parity fixture: refresh opencode.json hash for the
  added mcp.gsd block (configureOpencodePermissions output change).

* docs(changeset): add docs-exempt marker (Phase 5 slice)

* docs(changeset): backfill PR #1929
This commit is contained in:
Tom Boucher
2026-07-02 14:57:44 -04:00
committed by GitHub
parent ff6b5cb024
commit 541de6894f
5 changed files with 59 additions and 5 deletions

View File

@@ -21,6 +21,7 @@ const path = require('node:path');
const { createTempDir, cleanup } = require('./helpers.cjs');
const { configureOpencodePermissions } = require('../bin/install.js');
const { PACKAGE_NAME } = require('../gsd-core/bin/lib/package-identity.cjs');
const installSrc = fs.readFileSync(path.join(__dirname, '..', 'bin', 'install.js'), 'utf8');
@@ -78,6 +79,34 @@ describe('configureOpencodePermissions', () => {
assert.strictEqual(config.permission.external_directory[gsdPath], 'allow');
});
test('registers the companion MCP server (mcp.gsd) for object configs (#1682)', () => {
const configPath = path.join(configDir, 'opencode.json');
fs.writeFileSync(configPath, JSON.stringify({ permission: {} }, null, 2) + '\n');
process.env.OPENCODE_CONFIG_DIR = configDir;
configureOpencodePermissions(true, configDir);
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
assert.deepEqual(config.mcp.gsd, {
type: 'local',
command: ['npx', '-y', '-p', PACKAGE_NAME, 'gsd-mcp-server'],
enabled: true,
});
});
test('does not clobber a user-defined mcp.gsd entry (#1682)', () => {
const configPath = path.join(configDir, 'opencode.json');
const userMcp = { type: 'local', command: ['node', '/custom/server.js'], enabled: false };
fs.writeFileSync(configPath, JSON.stringify({ permission: {}, mcp: { gsd: userMcp } }, null, 2) + '\n');
process.env.OPENCODE_CONFIG_DIR = configDir;
configureOpencodePermissions(true, configDir);
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
// User's own mcp.gsd is preserved untouched (Hyrum's Law — non-clobbering).
assert.deepEqual(config.mcp.gsd, userMcp);
});
test('finishInstall passes the actual config dir to OpenCode permissions', () => {
assert.ok(
installSrc.includes('configureOpencodePermissions(isGlobal, configDir);'),