Merge pull request #2106 from open-gsd/feat/2086-eos-claude-imperative-adapter

feat(#2086): [EoS/claude] Migrate Claude Code onto the Embeddable Orchestration System (ADR-1239)
This commit is contained in:
Tom Boucher
2026-07-08 15:47:47 -04:00
committed by GitHub
8 changed files with 892 additions and 32 deletions

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 2106
---
**Internal: Claude Code's installer is now driven through the public Host-Integration Interface (ADR-1239 / EoS).** `bin/install.js` routes `claude` install/uninstall through the imperative adapter (`createImperativeAdapter`) instead of calling the engine directly, and its 13 hardcoded `runtime === 'claude'` / `runtime !== 'claude'` branches are folded into descriptor-driven `runtime.hostBehaviors` on `capabilities/claude/capability.json` (permission schema, `settings.local.json` scope routing, `.gsd-source` marker, effort frontmatter, canonical-workflow authorship, and more). Install/uninstall output is **byte-identical** for both the global skills layout and the local legacy layout (golden-parity asserted for both scopes); no other runtime changes. Removes the "add-a-host tax" of scattered string-equality checks for the tier-1 reference host. No user-facing change. (#2086)

View File

@@ -43,6 +43,7 @@ const {
readBaseRefFromSettings,
} = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
// Canonical set of hook files shipped to users. Imported here so writeManifest()
// records exactly the same set that build-hooks.js copies to hooks/dist/, making
@@ -126,6 +127,9 @@ function isCodexHooksFeatureKey(key) {
//
// Merge policy: additive, non-destructive \u2014 existing user entries are preserved;
// GSD entries are appended only when not already present (idempotent).
// The reference/default runtime (ADR-1239 reference host). Single-sourced here
// instead of scattered literal 'claude' defaults/rosters (#2086).
const DEFAULT_RUNTIME = 'claude';
const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
'Bash(npx gsd-core *)',
'Read(.planning/*)',
@@ -310,6 +314,64 @@ try {
} catch (_) {
_capabilityRegistry = undefined;
}
// Fail-safe floor for the reference host's #338-privacy-critical behaviors, used
// ONLY when the first-party capability registry cannot be loaded (a broken bundle).
// Without it, a registry-load failure would make `_hostBehaviors('claude')` return
// {} and silently route a claude LOCAL install to the repo-shared, committed
// `settings.json` instead of the gitignored `settings.local.json` (#338) — leaking
// engineer-specific absolute paths. Keyed by runtime id (a DATA lookup, not a
// hardcoded string-equality branch) so behavior degrades CLOSED (safe), never open.
// The live descriptor (capabilities/claude/capability.json) remains the source of
// truth; this mirrors only the privacy-load-bearing subset. (ADR-1239 / #2086)
const FALLBACK_HOST_BEHAVIORS = Object.freeze({
claude: Object.freeze({
settingsFileByScope: Object.freeze({ local: 'settings.local.json', global: 'settings.json' }),
permissionsSchema: 'claude',
sourceMarkerFile: '.gsd-source',
}),
});
/**
* Resolve a runtime's host behaviors from a capability registry, with the
* #338-privacy fail-safe floor when the registry (or the runtime's descriptor)
* is unavailable. Registry is passed in so this is unit-testable under a
* simulated registry-load failure. (ADR-1239 / #2086)
*/
function _resolveHostBehaviors(runtime, registry) {
const cap = registry && registry.runtimes && registry.runtimes[runtime];
const declared = cap && cap.runtime && cap.runtime.hostBehaviors;
if (declared) return declared;
return FALLBACK_HOST_BEHAVIORS[runtime] || {};
}
/**
* Host-specific install behaviors, declared on the runtime descriptor
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors) instead of
* scattered `runtime === '<id>'` string checks (ADR-1239 / #2086). Returns {}
* for runtimes that declare none, so every behavior branch degrades to the
* generic path by default — EXCEPT the reference host's #338-critical keys, which
* fall back to FALLBACK_HOST_BEHAVIORS if the registry failed to load.
*/
function _hostBehaviors(runtime) {
return _resolveHostBehaviors(runtime, _capabilityRegistry);
}
/**
* Construct the imperative Host-Integration adapter (ADR-1239 / #2086), FAIL-OPEN.
* `createImperativeAdapter` composes the capability registry via
* `loadRegistry({includeInstalled:true})`, which require()s several capability
* modules. If any is unavailable (e.g. a packaging regression), return null so
* the caller degrades to the engine directly rather than hard-crashing install/
* uninstall — matching the optional `capability-registry.cjs` load posture above.
*/
function _runtimeAdapter(runtime) {
try {
return createImperativeAdapter({ runtime });
} catch {
return null;
}
}
const {
applyInstallerMigrationPlan,
discoverInstallerMigrations,
@@ -1244,9 +1306,9 @@ function getCommitAttribution(runtime) {
: resolveKiloConfigPath;
const config = readSettings(resolveConfigPath(getGlobalConfigDir(runtime, null)));
result = (config && config.disable_ai_attribution === true) ? null : undefined;
} else if (runtime === 'claude') {
} else if (_hostBehaviors(runtime).attributionSource === 'settings-json-commit') {
// Claude Code
const settings = readSettings(path.join(getGlobalConfigDir('claude', explicitConfigDir), 'settings.json'));
const settings = readSettings(path.join(getGlobalConfigDir(runtime, explicitConfigDir), 'settings.json'));
if (!settings || !settings.attribution || settings.attribution.commit === undefined) {
result = undefined;
} else if (settings.attribution.commit === '') {
@@ -6279,7 +6341,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
// copyWithPathReplacement is the emit path for gsd-core/workflows/*.md;
// _applyRuntimeRewrites is NOT invoked here, so this is what makes the fix
// live in real installs (it is a no-op for files without those lines).
if (runtime !== 'claude') {
if (!_hostBehaviors(runtime).authorsCanonicalWorkflow) {
content = _stampNonClaudeRuntimeDefaults(content, runtime);
}
@@ -6481,7 +6543,7 @@ const GSD_UNINSTALL_HOOKS = [
* @param {boolean} isGlobal - Whether to uninstall from global or local
* @param {string} runtime - Target runtime ('claude', 'opencode', 'codex', 'copilot')
*/
function uninstall(isGlobal, runtime = 'claude') {
function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
const dirName = getDirName(runtime);
@@ -6539,7 +6601,14 @@ function uninstall(isGlobal, runtime = 'claude') {
// 1. Remove GSD commands/skills (layout-driven)
const scope = isGlobal ? 'global' : 'local';
uninstallRuntimeArtifacts(runtime, targetDir, scope);
// ADR-1239 / #2086: drive uninstall through the public Host-Integration Interface.
// Fail-open to the engine directly if the composed-registry adapter can't load.
const _uninstallAdapter = _runtimeAdapter(runtime);
if (_uninstallAdapter) {
_uninstallAdapter.uninstall({ configDir: targetDir, scope });
} else {
uninstallRuntimeArtifacts(runtime, targetDir, scope);
}
removedCount++;
// 1a. Non-layout Codex side-effects: agent .toml files, config.toml sections, hooks.json
@@ -6705,7 +6774,7 @@ function uninstall(isGlobal, runtime = 'claude') {
// 1c. Claude local: remove flat gsd-*.md commands from commands/ (current layout,
// #1367 fix). Also remove legacy commands/gsd/ subdirectory from prior installs.
if (!isGlobal && runtime === 'claude') {
if (!isGlobal && _hostBehaviors(runtime).localInstallStyle === 'legacy-flat') {
const commandsDir = path.join(targetDir, 'commands');
// Remove flat gsd-*.md files (current layout after #1367 fix)
if (fs.existsSync(commandsDir)) {
@@ -7008,7 +7077,7 @@ function uninstall(isGlobal, runtime = 'claude') {
// to preserve any user-added allow/deny entries.
// Uses a local flag to avoid the shared `settingsModified` producing a false
// "Removed GSD permissions" message when only hooks/statusline changed.
if (runtime === 'claude' && settings.permissions) {
if (_hostBehaviors(runtime).permissionsSchema === 'claude' && settings.permissions) {
let permissionsModified = false;
if (Array.isArray(settings.permissions.allow)) {
const before = settings.permissions.allow.length;
@@ -7483,7 +7552,7 @@ function resolveInstallRelativePath(baseDir, relPath) {
/**
* Write file manifest after installation for future modification detection
*/
function writeManifest(configDir, runtime = 'claude', options = {}) {
function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
const gsdDir = path.join(configDir, 'gsd-core');
// #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
@@ -7521,7 +7590,7 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
// Claude local (#1367): flat gsd-*.md files at commands/ level.
// Only claude local writes gsd-*.md here; global installs don't emit commands,
// so this branch is a no-op for global (no matching files to find).
if (runtime === 'claude' && fs.existsSync(flatCommandsDir)) {
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && fs.existsSync(flatCommandsDir)) {
for (const file of fs.readdirSync(flatCommandsDir)) {
if (file.startsWith('gsd-') && file.endsWith('.md')) {
manifest.files['commands/' + file] = fileHash(path.join(flatCommandsDir, file));
@@ -7934,7 +8003,7 @@ function saveLocalPatches(configDir, pristineCtx) {
/**
* After install, report backed-up patches for user to reapply.
*/
function reportLocalPatches(configDir, runtime = 'claude') {
function reportLocalPatches(configDir, runtime = DEFAULT_RUNTIME) {
const patchesDir = path.join(configDir, PATCHES_DIR_NAME);
const metaPath = path.join(patchesDir, 'backup-meta.json');
if (!fs.existsSync(metaPath)) return [];
@@ -7977,7 +8046,7 @@ function reportInstallerMigrationResult(result) {
}
}
function install(isGlobal, runtime = 'claude', options = {}) {
function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
const { isOpencode, isKilo, isZcode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
const plan = resolveInstallPlan(runtime);
const dirName = getDirName(runtime);
@@ -8422,7 +8491,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
// (copyWithPathReplacement + stale-skills cleanup).
const _isSkillsRuntime = (() => {
if (isOpencode || isKilo) return false; // specialized combined path
if (runtime === 'claude' && !isGlobal) return false; // claude-local legacy path
if (_hostBehaviors(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
const layout = cap && cap.runtime && cap.runtime.artifactLayout;
if (!layout) return false;
@@ -8433,7 +8502,21 @@ function install(isGlobal, runtime = 'claude', options = {}) {
if (_isSkillsRuntime) {
// Layout-driven install for skills-based runtimes (full and minimal modes)
const scope = isGlobal ? 'global' : 'local';
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile, getCommitAttribution);
// ADR-1239 / #2086: drive install through the public Host-Integration Interface
// (imperative adapter). The adapter delegates to the SAME installRuntimeArtifacts
// engine call -> byte-identical output (gated by golden-install-parity). Fail-open
// to the engine directly if the composed-registry adapter can't load.
const _adapter = _runtimeAdapter(runtime);
if (_adapter) {
_adapter.install({
configDir: targetDir,
scope,
resolvedProfile: _resolvedProfile,
resolveAttribution: getCommitAttribution,
});
} else {
installRuntimeArtifacts(runtime, targetDir, scope, _resolvedProfile, getCommitAttribution);
}
// #1326 — Codex only: remove stale agents/openai.yaml sidecars from managed
// gsd-* skill dirs. Prior installs wrote these files so Codex would show a
@@ -8733,11 +8816,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
// other runtime/scope deploys commands/gsd, so its walk-up already resolves
// and needs no marker. Guarded on source presence so a half-published
// package never writes a dangling marker.
if (runtime === 'claude' && isGlobal) {
if (_hostBehaviors(runtime).sourceMarkerFile && isGlobal) {
const gsdSourceCommands = path.join(src, 'commands', 'gsd');
if (fs.existsSync(gsdSourceCommands)) {
try {
fs.writeFileSync(path.join(targetDir, '.gsd-source'), gsdSourceCommands + '\n', 'utf8');
// ADR-1239 Phase B write-confinement: the descriptor-sourced marker filename
// must resolve under targetDir (parity with the other descriptor-driven writes).
const _markerPath = assertDestWithinConfigHome(targetDir, _hostBehaviors(runtime).sourceMarkerFile);
fs.writeFileSync(_markerPath, gsdSourceCommands + '\n', 'utf8');
} catch (err) {
// Non-fatal: install proceeds. But on the Claude-global layout walk-up
// also fails (no commands/gsd source tree), so a silent write failure
@@ -8932,11 +9018,11 @@ function install(isGlobal, runtime = 'claude', options = {}) {
// Claude Code reads per-subagent `effort:` frontmatter (anthropics/claude-code #31536).
// Injection is per-runtime at install time because the canonical source
// agents/*.md must stay runtime-safe (no effort: key in source).
if (runtime === 'claude') {
if ((_hostBehaviors(runtime).agentFrontmatterExtensions || []).includes('effort')) {
const _effortCfg = readGsdEffectiveEffortConfig(targetDir);
const _agentName = entry.name.replace(/\.md$/, '');
const _universalEffort = resolveInstallTimeEffort(_effortCfg, _agentName);
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime('claude', _universalEffort).value;
const _renderedEffort = _getGsdEffortCatalog().renderEffortForRuntime(runtime, _universalEffort).value;
content = injectEffortFrontmatter(content, _renderedEffort);
const _disallowedTools = READONLY_AGENT_DISALLOWED_TOOLS[_agentName];
if (_disallowedTools) content = injectDisallowedToolsFrontmatter(content, _disallowedTools);
@@ -9219,7 +9305,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
reportLocalPatches(targetDir, runtime);
// Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
if (runtime !== 'claude') {
if (!_hostBehaviors(runtime).ownsClaudePaths) {
const leakedPaths = [];
// Only scan files that were written by this install (manifest-tracked).
// Scanning the entire targetDir can match user-authored content that
@@ -9742,9 +9828,14 @@ function install(isGlobal, runtime = 'claude', options = {}) {
// #338: local Claude installs write to settings.local.json (Claude Code's per-user/gitignored slot)
// so engineer-specific absolute paths (Node binary, home dir) never land in the repo-shared
// settings.json. Global installs and all other runtimes continue to use settings.json.
const isLocalClaude = (runtime === 'claude' && !isGlobal);
const settingsFileName = isLocalClaude ? 'settings.local.json' : 'settings.json';
const settingsPath = path.join(targetDir, settingsFileName);
const _scopedSettings = _hostBehaviors(runtime).settingsFileByScope || null;
const isLocalClaude = (!isGlobal && !!(_scopedSettings && _scopedSettings.local));
const settingsFileName = isLocalClaude
? _scopedSettings.local
: ((_scopedSettings && _scopedSettings.global) || 'settings.json');
// ADR-1239 Phase B write-confinement: the descriptor-sourced settings filename
// must resolve under targetDir (this path also drives a recursive mkdirSync).
const settingsPath = assertDestWithinConfigHome(targetDir, settingsFileName);
// #338 migration: if a prior local Claude install wrote GSD-shaped entries to settings.json,
// relocate them to settings.local.json and clear them from the shared file in the same run.
@@ -10010,7 +10101,7 @@ function install(isGlobal, runtime = 'claude', options = {}) {
/**
* Apply statusline config, then print completion message
*/
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = 'claude', isGlobal = true, configDir = null, bannerOpts = {}) {
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
const { isOpencode, isKilo, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
const plan = resolveInstallPlan(runtime);
@@ -10069,7 +10160,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
// Merges GSD-owned entries non-destructively (preserves existing user permissions).
// Scoped to Claude only: antigravity/qwen/hermes/codebuddy also write
// settings.json but use different runtimes and do not use these permission strings.
if (runtime === 'claude') {
if (_hostBehaviors(runtime).permissionsSchema === 'claude') {
mergeClaudePermissions(settings);
}
@@ -10102,7 +10193,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
// chat model instead of pinning the resolved model. See #1156 (default-to-omit
// intent) and #1569 (preserve explicit true). Guard matches the #130-class pattern
// on configureOpencodePermissions above.
if (runtime !== 'claude' && !process.env.GSD_TEST_MODE) {
if (!_hostBehaviors(runtime).nativeModelAliases && !process.env.GSD_TEST_MODE) {
const gsdDir = path.join(os.homedir(), '.gsd');
const defaultsPath = path.join(gsdDir, 'defaults.json');
try {
@@ -10144,7 +10235,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
// Restart is required for CC to pick up newly-installed skills, and the
// slash-menu surface depends on CC version — so the instruction needs to
// cover both invocation paths to avoid #2957-style "no commands appear".
if (runtime === 'claude' && isGlobal) {
if (_hostBehaviors(runtime).skillsGlobalOnboarding && isGlobal) {
console.log(`
${green}Done!${reset} Restart ${program}, then in any directory either type ${cyan}${command}${reset} or ask Claude to run the ${cyan}gsd-new-project${reset} skill.
@@ -10303,7 +10394,7 @@ function parseRuntimeInput(answer) {
}
}
return selected.length > 0 ? selected : ['claude'];
return selected.length > 0 ? selected : [DEFAULT_RUNTIME];
}
function promptRuntime(callback) {
@@ -10922,7 +11013,7 @@ function installAllRuntimes(runtimes, isGlobal, isInteractive) {
throw error;
}
const statuslineRuntimes = ['claude'];
const statuslineRuntimes = [DEFAULT_RUNTIME];
const primaryStatuslineResult = results.find(r => statuslineRuntimes.includes(r.runtime));
const finalize = (shouldInstallStatusline, shouldInstallBanner) => {
@@ -11038,6 +11129,9 @@ module.exports = {
install,
installAllRuntimes,
uninstall,
// #2086 — host-behavior resolution + the #338 privacy fail-safe floor (exported for tests)
_resolveHostBehaviors,
FALLBACK_HOST_BEHAVIORS,
convertSlashCommandsToCodexSkillMentions,
convertClaudeCommandToCodexSkill,
convertClaudeCommandToKimiSkill,
@@ -11206,7 +11300,7 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
console.error(` ${yellow}--uninstall requires --global or --local${reset}`);
process.exit(1);
}
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : ['claude'];
const runtimes = selectedRuntimes.length > 0 ? selectedRuntimes : [DEFAULT_RUNTIME];
for (const runtime of runtimes) {
uninstall(hasGlobal, runtime);
}
@@ -11218,12 +11312,12 @@ if (require.main === module && !process.env.GSD_TEST_MODE) {
}
} else if (hasGlobal || hasLocal) {
// Default to Claude if no runtime specified but location is
installAllRuntimes(['claude'], hasGlobal, false);
installAllRuntimes([DEFAULT_RUNTIME], hasGlobal, false);
} else {
// Interactive
if (!process.stdin.isTTY) {
console.log(` ${yellow}Non-interactive terminal detected, defaulting to Claude Code global install${reset}\n`);
installAllRuntimes(['claude'], true, false);
installAllRuntimes([DEFAULT_RUNTIME], true, false);
} else {
promptRuntime((runtimes) => {
promptLocation(runtimes);

View File

@@ -79,6 +79,21 @@
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "node"
},
"hostBehaviors": {
"attributionSource": "settings-json-commit",
"authorsCanonicalWorkflow": true,
"localInstallStyle": "legacy-flat",
"permissionsSchema": "claude",
"settingsFileByScope": {
"local": "settings.local.json",
"global": "settings.json"
},
"sourceMarkerFile": ".gsd-source",
"agentFrontmatterExtensions": ["effort"],
"ownsClaudePaths": true,
"nativeModelAliases": true,
"skillsGlobalOnboarding": true
}
}
}

View File

@@ -102,6 +102,51 @@ a first-party primitive, reviewed. To add one (e.g. a new `runtime` kind):
The parity guard (`tests/host-integration-validator-parity.test.cjs`) fails if these two drift, so
they must be updated together. Document the new value's meaning in the matrix legend.
## 7. Fold an already-hardcoded host into the interface (worked example: `claude`)
Sections 1–6 cover a *green-field* host (`pi`, `antigravity` — a fresh descriptor + reference
binding). This section covers the other case: a host that already has a **real production install**
driven by scattered `runtime === '<id>'` string-equality branches in `bin/install.js`, which you want
to move onto the Host-Integration Interface **without changing a single installed byte**. `claude`
(the tier-1 reference host, #2086) is the worked example.
The pattern is byte-parity-safe by construction — each string check becomes a **descriptor lookup that
yields the same truth value**, so behavior is unchanged and only the brittle coupling is removed:
1. **Inventory the branches.** Find every `runtime === '<id>'` / `runtime !== '<id>'` in `bin/install.js`
for the host (`grep -nE "runtime\s*[!=]==\s*'claude'"`). Each is a host behavior encoded as a string
comparison rather than a declared capability.
2. **Declare the behaviors on the descriptor.** Add a `runtime.hostBehaviors` object to the host's
`capability.json`. Each key names one behavior the branches gated on — e.g. for `claude`:
`permissionsSchema: "claude"`, `settingsFileByScope: { local: "settings.local.json", global: "settings.json" }`,
`sourceMarkerFile: ".gsd-source"`, `agentFrontmatterExtensions: ["effort"]`, `localInstallStyle: "legacy-flat"`,
`authorsCanonicalWorkflow: true`, `ownsClaudePaths: true`, `nativeModelAliases: true`,
`skillsGlobalOnboarding: true`, `attributionSource: "settings-json-commit"`. The validator
(`validateRuntimeBody`) is lenient toward these host-behavior keys; they carry install policy, not the
closed negotiated axes.
3. **Replace each branch with a descriptor read.** `bin/install.js` exposes a `_hostBehaviors(runtime)`
helper (reads `_capabilityRegistry.runtimes[runtime].runtime.hostBehaviors`, `{}` if absent). Rewrite
`if (runtime === 'claude')` → `if (_hostBehaviors(runtime).permissionsSchema === 'claude')`, and
`if (runtime !== 'claude')` → `if (!_hostBehaviors(runtime).authorsCanonicalWorkflow)`. Only the host
declares the key, so every other runtime keeps the generic path.
4. **Route install/uninstall through the public adapter.** Replace the direct
`installRuntimeArtifacts(...)` / `uninstallRuntimeArtifacts(...)` calls with
`createImperativeAdapter({ runtime }).install({...})` / `.uninstall({...})`. The imperative adapter
delegates to the *same* engine functions, so the output is byte-identical — that is the point: the
host is now driven **through** the interface, not around it.
5. **Prove parity, both scopes.** `tests/golden-install-parity.test.cjs` captures a byte-stable manifest
of every emitted file. Assert the host's install is unchanged for **global and local** scopes
(regenerate a baseline from `origin/next` first, then confirm the migrated tree matches it). Exclude
only genuinely volatile / platform-varying files (`settings.json`, `settings.local.json`, `.gsd-source`).
6. **Guard against regression.** Add a `*-imperative-reference.test.cjs` asserting the adapter classifies
the host correctly, negotiation fails closed on a corrupted descriptor, and — with a source-grep behind
an `// allow-test-rule:` exemption — that **no `runtime === '<id>'` branch remains** in `bin/install.js`.
---
## Related

View File

@@ -453,6 +453,23 @@ const capabilities = {
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "node"
},
"hostBehaviors": {
"attributionSource": "settings-json-commit",
"authorsCanonicalWorkflow": true,
"localInstallStyle": "legacy-flat",
"permissionsSchema": "claude",
"settingsFileByScope": {
"local": "settings.local.json",
"global": "settings.json"
},
"sourceMarkerFile": ".gsd-source",
"agentFrontmatterExtensions": [
"effort"
],
"ownsClaudePaths": true,
"nativeModelAliases": true,
"skillsGlobalOnboarding": true
}
}
},
@@ -3877,6 +3894,23 @@ const runtimes = {
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "node"
},
"hostBehaviors": {
"attributionSource": "settings-json-commit",
"authorsCanonicalWorkflow": true,
"localInstallStyle": "legacy-flat",
"permissionsSchema": "claude",
"settingsFileByScope": {
"local": "settings.local.json",
"global": "settings.json"
},
"sourceMarkerFile": ".gsd-source",
"agentFrontmatterExtensions": [
"effort"
],
"ownsClaudePaths": true,
"nativeModelAliases": true,
"skillsGlobalOnboarding": true
}
}
},

View File

@@ -0,0 +1,183 @@
// allow-test-rule: AC2 requires asserting no `runtime === 'claude'` string-equality branch remains in bin/install.js — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2086)
'use strict';
/**
* claude imperative reference host — ADR-1239 Phase D / #2086 (EoS/claude).
*
* claude is GSD's tier-1 reference host — "golden parity vs. the Claude reference
* host" (ADR-1239 line 146). This proves claude is driven through the PUBLIC
* Host-Integration Interface (the imperative adapter), that its negotiated axes
* classify + negotiate correctly, that negotiation FAILS CLOSED on a corrupted
* descriptor, and that the migration retired the hardcoded `runtime === 'claude'`
* string-equality branches in bin/install.js (folded into descriptor-driven
* `runtime.hostBehaviors`).
*
* Mirrors tests/pi-imperative-reference.test.cjs + tests/vscode-ide-reference.test.cjs
* but binds against the REAL descriptor + the REAL installer source, since claude
* (unlike pi/vscode) has a real production install being folded in.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
const {
profileOf,
negotiateHostCapabilities,
PROTOCOL_VERSION,
PROFILE_BASELINES,
UNDOCUMENTED,
} = require('../gsd-core/bin/lib/host-integration.cjs');
const CLAUDE_CAP = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'claude', 'capability.json'), 'utf8'),
);
const CLAUDE_AXES = CLAUDE_CAP.runtime.hostIntegration;
// Requiring the installer (not as main) never runs the CLI; GSD_TEST_MODE is set
// defensively to match the install-test convention.
process.env.GSD_TEST_MODE = process.env.GSD_TEST_MODE || '1';
const installMod = require('../bin/install.js');
// -- AC2: driven through the public interface (imperative adapter) -----------
test('createImperativeAdapter classifies claude as imperative + composes the registry', () => {
const adapter = createImperativeAdapter({ runtime: 'claude' });
assert.equal(adapter.kind, 'imperative', "claude embeddingMode is imperative -> adapter kind must be 'imperative'");
assert.equal(adapter.runtime, 'claude');
assert.ok(adapter.registry && typeof adapter.registry === 'object', 'imperative adapter exposes the composed capability registry');
assert.equal(typeof adapter.install, 'function');
assert.equal(typeof adapter.uninstall, 'function');
});
test('claude descriptor embeddingMode agrees with the imperative adapter kind', () => {
assert.equal(CLAUDE_AXES.embeddingMode, 'imperative', 'capability.json must declare embeddingMode: imperative for the imperative binding');
});
test('claude axes classify as the programmatic-cli reference profile', () => {
assert.equal(profileOf(CLAUDE_AXES), 'programmatic-cli');
assert.notEqual(profileOf(CLAUDE_AXES), 'ide');
});
// -- AC3: every negotiated axis populated, negotiation is clean ---------------
test('claude negotiates its declared axes verbatim (no degradation of documented values)', () => {
const result = negotiateHostCapabilities({ ...CLAUDE_AXES, protocolVersion: PROTOCOL_VERSION });
assert.equal(result.protocolVersion, PROTOCOL_VERSION);
// Every declared scalar axis survives negotiation unchanged (all are known+documented).
assert.equal(result.effective.embeddingMode, 'imperative');
assert.equal(result.effective.commandSurface, CLAUDE_AXES.commandSurface);
assert.equal(result.effective.modelMode, CLAUDE_AXES.modelMode);
assert.equal(result.effective.hookBus, CLAUDE_AXES.hookBus);
assert.equal(result.effective.stateIO, CLAUDE_AXES.stateIO);
assert.equal(result.effective.transport, CLAUDE_AXES.transport);
assert.equal(result.effective.runtime, CLAUDE_AXES.runtime);
// No `undocumented` sentinel anywhere in claude's declared axes.
assert.ok(
!JSON.stringify(CLAUDE_AXES).includes(UNDOCUMENTED),
'claude descriptor must carry no `undocumented` sentinel (fully doc-sourced)',
);
});
// -- AC5: negotiation fails CLOSED on a corrupted / partial descriptor --------
test('negotiateHostCapabilities never throws for claude — even fully corrupted input', () => {
assert.doesNotThrow(() => negotiateHostCapabilities({}));
assert.doesNotThrow(() => negotiateHostCapabilities({ embeddingMode: UNDOCUMENTED }));
assert.doesNotThrow(() => negotiateHostCapabilities({ embeddingMode: 'wildly-unknown-future-value' }));
});
test('a partial/empty claude descriptor degrades to the safe floor — NOT the full programmatic-cli baseline', () => {
const result = negotiateHostCapabilities({});
// Fail-closed floor (SAFE_DEFAULTS), not the rich profile baseline.
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed to declarative');
assert.equal(result.effective.hookBus, 'none', 'omitted hookBus degrades closed to none');
assert.equal(result.effective.commandSurface, 'prose-only', 'omitted commandSurface degrades closed to prose-only');
assert.equal(result.effective.dispatch.namedDispatch, false, 'omitted dispatch degrades closed (no named dispatch)');
assert.notDeepEqual(
result.effective,
PROFILE_BASELINES['programmatic-cli'],
'a corrupted descriptor MUST NOT silently reuse the full programmatic-cli baseline',
);
assert.ok(result.warnings.length > 0, 'degrade-closed must surface warnings');
});
test('an undocumented/unknown claude axis value is not trusted (degraded closed per-axis)', () => {
const corrupted = { ...CLAUDE_AXES, embeddingMode: UNDOCUMENTED, hookBus: 'unknown-bus-kind' };
const result = negotiateHostCapabilities(corrupted);
assert.equal(result.effective.embeddingMode, 'declarative', 'undocumented embeddingMode -> safe floor');
assert.equal(result.effective.hookBus, 'none', 'unknown hookBus value -> safe floor');
// Untouched axes still negotiate to their declared (documented) values.
assert.equal(result.effective.stateIO, CLAUDE_AXES.stateIO);
});
// -- AC2: the hardcoded string-equality branches are retired ------------------
test('claude descriptor declares runtime.hostBehaviors (the folded-in host behaviors)', () => {
const hb = CLAUDE_CAP.runtime.hostBehaviors;
assert.ok(hb && typeof hb === 'object', 'capabilities/claude/capability.json must declare runtime.hostBehaviors');
// The behaviors that replaced the 13 `runtime === 'claude'` branches.
assert.equal(hb.permissionsSchema, 'claude');
assert.equal(hb.localInstallStyle, 'legacy-flat');
assert.equal(hb.sourceMarkerFile, '.gsd-source');
assert.equal(hb.authorsCanonicalWorkflow, true);
assert.equal(hb.ownsClaudePaths, true);
assert.equal(hb.nativeModelAliases, true);
assert.equal(hb.skillsGlobalOnboarding, true);
assert.equal(hb.attributionSource, 'settings-json-commit');
assert.deepEqual(hb.agentFrontmatterExtensions, ['effort']);
assert.equal(hb.settingsFileByScope.local, 'settings.local.json');
assert.equal(hb.settingsFileByScope.global, 'settings.json');
});
test('bin/install.js contains no `runtime === "claude"` / `runtime !== "claude"` string-equality branches (AC2)', () => {
const src = fs.readFileSync(path.join(__dirname, '..', 'bin', 'install.js'), 'utf8');
// Strip comments + backtick/inline-code spans so PROSE mentions of the old
// pattern (a comment explaining "not a string-equality branch") do not
// false-positive — only LIVE code counts.
const codeOnly = src
.replace(/\/\*[\s\S]*?\*\//g, '') // block comments
.replace(/\/\/[^\r\n]*/g, '') // line comments (CRLF-safe)
.replace(/`[^`]*`/g, ''); // backtick / inline-code spans
const offenders = codeOnly.match(/runtime\s*[!=]==\s*'claude'/g) || [];
assert.deepEqual(
offenders,
[],
`AC2: every hardcoded runtime==='claude'/!=='claude' branch must be descriptor-driven; found: ${offenders.join(', ')}`,
);
});
// -- Reviewer #2106 (elevated): #338 privacy fail-safe on registry-load failure --
// If the first-party capability registry fails to load, `_hostBehaviors('claude')`
// would return {} and route a claude LOCAL install to the repo-shared settings.json
// instead of the gitignored settings.local.json — silently reintroducing #338. The
// reference host must degrade CLOSED (safe) for its privacy-critical keys.
test('claude #338-critical host behaviors degrade CLOSED when the capability registry cannot load', () => {
// Simulate a broken bundle: registry is undefined.
const degraded = installMod._resolveHostBehaviors('claude', undefined);
assert.equal(degraded.settingsFileByScope.local, 'settings.local.json',
'#338: a claude LOCAL install must still route to the gitignored settings.local.json');
assert.equal(degraded.settingsFileByScope.global, 'settings.json');
assert.equal(degraded.permissionsSchema, 'claude', 'permission cleanup/merge must still apply');
assert.equal(degraded.sourceMarkerFile, '.gsd-source');
});
test('with the registry present, claude host behaviors come from the live descriptor (superset of the fail-safe floor)', () => {
const reg = require('../gsd-core/bin/lib/capability-registry.cjs');
const declared = installMod._resolveHostBehaviors('claude', reg);
assert.equal(declared.settingsFileByScope.local, 'settings.local.json');
assert.equal(declared.localInstallStyle, 'legacy-flat');
assert.equal(declared.authorsCanonicalWorkflow, true);
// The fail-safe floor is a strict subset of what the descriptor declares.
for (const k of Object.keys(installMod.FALLBACK_HOST_BEHAVIORS.claude)) {
assert.ok(k in declared, `descriptor must still declare the #338-critical key '${k}'`);
}
});
test('a non-reference runtime has no fail-safe fallback (degrades to the generic path)', () => {
assert.deepEqual(installMod._resolveHostBehaviors('opencode', undefined), {});
assert.deepEqual(installMod._resolveHostBehaviors('codex', undefined), {});
});

View File

@@ -0,0 +1,418 @@
{
".gsd-profile": "0e716a5fef4e6dc1",
"agents/gsd-advisor-researcher.md": "c81b55d567dcd99e",
"agents/gsd-ai-researcher.md": "27c7df941b01cc13",
"agents/gsd-assumptions-analyzer.md": "d796f8245bbe05d2",
"agents/gsd-code-fixer.md": "c6148e5511d02459",
"agents/gsd-code-reviewer.md": "e2c45baa8c0b5f6d",
"agents/gsd-codebase-mapper.md": "f96958e5f85b93fb",
"agents/gsd-debug-session-manager.md": "ec9ca0011a1aab75",
"agents/gsd-debugger.md": "9f35a91f8b3a918e",
"agents/gsd-doc-classifier.md": "a76778bdde1c7f72",
"agents/gsd-doc-synthesizer.md": "8b0b6fc187c9d353",
"agents/gsd-doc-verifier.md": "4232dcf9076e3566",
"agents/gsd-doc-writer.md": "22264239fa0ee611",
"agents/gsd-domain-researcher.md": "f1e03df842ddfb95",
"agents/gsd-eval-auditor.md": "d0f45fff7370bb0b",
"agents/gsd-eval-planner.md": "9cc049b82897daa4",
"agents/gsd-executor.md": "bf1de739df0c9245",
"agents/gsd-framework-selector.md": "85005d716f9d98f7",
"agents/gsd-integration-checker.md": "17a8ee731986564d",
"agents/gsd-intel-updater.md": "4953a465db9dadc1",
"agents/gsd-mempalace-curator.md": "77b53f1b155242b4",
"agents/gsd-nyquist-auditor.md": "f86a28f5c164a0d3",
"agents/gsd-pattern-mapper.md": "b45b5e106775bec1",
"agents/gsd-phase-researcher.md": "4772d9eada32e8bd",
"agents/gsd-plan-checker.md": "75851b147f35354a",
"agents/gsd-planner.md": "9c9ffc56275b8ca2",
"agents/gsd-project-researcher.md": "d7f355894519f9fe",
"agents/gsd-research-synthesizer.md": "1c738df9932d325a",
"agents/gsd-roadmapper.md": "453e9471ad27c7ea",
"agents/gsd-security-auditor.md": "45bd98918cd3a004",
"agents/gsd-ui-auditor.md": "a0b09cc8e4645956",
"agents/gsd-ui-checker.md": "3a7be21f4daa1c05",
"agents/gsd-ui-researcher.md": "a739b0ded9c3ae23",
"agents/gsd-user-profiler.md": "d40584599906f3b7",
"agents/gsd-verifier.md": "628ef3a944a7a6eb",
"commands/gsd-add-tests.md": "057e3e440989e681",
"commands/gsd-ai-integration-phase.md": "998dc23188e131fb",
"commands/gsd-audit-fix.md": "ebdcc267a2bebaab",
"commands/gsd-audit-milestone.md": "319caced85c69ff3",
"commands/gsd-audit-uat.md": "99c9af1b2a2c600a",
"commands/gsd-autonomous.md": "c256989da0d3e736",
"commands/gsd-capture.md": "d3f4353483df3637",
"commands/gsd-cleanup.md": "110473269dabd608",
"commands/gsd-code-review.md": "d2f3160f3a1a790a",
"commands/gsd-complete-milestone.md": "561b8e6bf35ee6e9",
"commands/gsd-config.md": "24d7e8cb8237456e",
"commands/gsd-debug.md": "488ec45a06eb7afa",
"commands/gsd-discuss-phase.md": "e50613dc10e1bab5",
"commands/gsd-docs-update.md": "e314a24fd3926ace",
"commands/gsd-eval-review.md": "c636c832d9a7f5c0",
"commands/gsd-execute-phase.md": "a2c4ad3e635c280a",
"commands/gsd-explore.md": "6d5e1f5f14b2d380",
"commands/gsd-extract-learnings.md": "e43d320cb3ed1aaa",
"commands/gsd-fast.md": "5357a1cf3f363dd2",
"commands/gsd-forensics.md": "b816a6cd5ea304f8",
"commands/gsd-graphify.md": "a07ffe1827c512e1",
"commands/gsd-health.md": "253680291c74b8ed",
"commands/gsd-help.md": "eb2c387f97c59e66",
"commands/gsd-import.md": "bcd7c5018b6c96b2",
"commands/gsd-inbox.md": "e829f45bfe8b4ca5",
"commands/gsd-ingest-docs.md": "ded9013d0de7e77b",
"commands/gsd-manager.md": "72d5b31b88f77703",
"commands/gsd-map-codebase.md": "ecd69887996ae561",
"commands/gsd-mempalace-capture.md": "2e49397072506fd9",
"commands/gsd-mempalace-recall.md": "38716c0983a3ef9c",
"commands/gsd-milestone-summary.md": "908509042caf5beb",
"commands/gsd-mvp-phase.md": "1ef0d7c2871be49a",
"commands/gsd-new-milestone.md": "8ce4861325cd3862",
"commands/gsd-new-project.md": "d68b36481d09bfd4",
"commands/gsd-next.md": "e976ddf80b6cb425",
"commands/gsd-ns-context.md": "011c44e7aa46e64a",
"commands/gsd-ns-ideate.md": "edc5e543512dd48a",
"commands/gsd-ns-manage.md": "0409d810e499357f",
"commands/gsd-ns-project.md": "ff67e85bc6f7fc5a",
"commands/gsd-ns-review.md": "3766ed10827882a0",
"commands/gsd-ns-workflow.md": "c3b3c046a74ec0ee",
"commands/gsd-onboard.md": "d0d9405bc73899bd",
"commands/gsd-pause-work.md": "01dbaebfefacd252",
"commands/gsd-phase.md": "e8c226d2694692a5",
"commands/gsd-plan-phase.md": "518357828182dca7",
"commands/gsd-plan-review-convergence.md": "d5a85a50dcff2dd1",
"commands/gsd-pr-branch.md": "382c23a6a644c0e4",
"commands/gsd-profile-user.md": "adbc5b025b30e836",
"commands/gsd-progress.md": "75a6dc71b74c54c2",
"commands/gsd-quick.md": "fe2f6655e67a4223",
"commands/gsd-resume-work.md": "cb393bc9f46dcc15",
"commands/gsd-review-backlog.md": "434ebe7b63c107e0",
"commands/gsd-review.md": "e990b7adb72472f9",
"commands/gsd-secure-phase.md": "ca4c23a332ca6204",
"commands/gsd-settings.md": "ed3fe2f6c845226d",
"commands/gsd-ship.md": "35a93b86512a7dff",
"commands/gsd-sketch.md": "c35f09bf0698c0c9",
"commands/gsd-spec-phase.md": "383b4928c4df4bf1",
"commands/gsd-spike.md": "98d789db7ca53873",
"commands/gsd-stats.md": "a9c0e3f338bd61a5",
"commands/gsd-surface.md": "998bf327c3f4a001",
"commands/gsd-thread.md": "1b78c7b75b53ad4e",
"commands/gsd-ui-phase.md": "67a759b3973f961f",
"commands/gsd-ui-review.md": "9b4ecf2d40bdc476",
"commands/gsd-ultraplan-phase.md": "4b42890e7d7a0c30",
"commands/gsd-undo.md": "e3cf64bb9562a0d8",
"commands/gsd-update.md": "23342d312e49a125",
"commands/gsd-validate-phase.md": "36c1a57cf7e1e724",
"commands/gsd-verify-work.md": "246ac304c4dfde48",
"commands/gsd-workspace.md": "5928e93b8ab475ac",
"commands/gsd-workstreams.md": "52ab9c585d3a00f3",
"gsd-core/VERSION": "ef0deccd81a6723c",
"gsd-core/bin/check-latest-version.cjs": "e4a224058c8f4d74",
"gsd-core/bin/ensure-runtime-build.cjs": "51bc64467ab30f62",
"gsd-core/bin/gsd-tools.cjs": "6a7616125440c8b1",
"gsd-core/bin/gsd_run": "62d9b647ede212e6",
"gsd-core/bin/shared/config-defaults.manifest.json": "517e6a7c1e9f4f16",
"gsd-core/bin/shared/config-schema.manifest.json": "6bba2b9c9fa47cb8",
"gsd-core/bin/shared/model-catalog.json": "e554a288fcbc1b2e",
"gsd-core/bin/shared/runtime-aliases.manifest.json": "2df2c5ac1957911a",
"gsd-core/bin/verify-reapply-patches.cjs": "caec5dbce11e3904",
"gsd-core/contexts/dev.md": "dcb0de9dce33cf41",
"gsd-core/contexts/research.md": "b3285d8e7209cc3b",
"gsd-core/contexts/review.md": "dc578fdd74bbea11",
"gsd-core/references/agent-contracts.md": "ff65e633c656c0d2",
"gsd-core/references/agent-skills-bootstrap.md": "5ab875054b1adda9",
"gsd-core/references/ai-evals.md": "b5afa786b938671e",
"gsd-core/references/ai-frameworks.md": "f827de93dde124eb",
"gsd-core/references/api-coverage.md": "205a43c5fa7c221c",
"gsd-core/references/artifact-types.md": "251040866a3a1818",
"gsd-core/references/autonomous-smart-discuss.md": "2fc710cde0ec7785",
"gsd-core/references/checkpoints.md": "6aa620c6ca38bdf0",
"gsd-core/references/common-bug-patterns.md": "780145be56352626",
"gsd-core/references/context-budget.md": "6bfac08025ea3106",
"gsd-core/references/continuation-format.md": "580287399ad3ba68",
"gsd-core/references/debugger-philosophy.md": "0466f95a3d6bfcd3",
"gsd-core/references/decimal-phase-calculation.md": "46b5ba045852c474",
"gsd-core/references/doc-conflict-engine.md": "883d0a1b9d9ff96e",
"gsd-core/references/domain-probes.md": "762b965e84035b72",
"gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json": "72d1e29cedc854ec",
"gsd-core/references/edge-probe-fixtures/01-round-half-even/requirements.json": "fbc1b355d8625eeb",
"gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json": "fad67dcc8294f6da",
"gsd-core/references/edge-probe-fixtures/02-merge-intervals/requirements.json": "30a78ee9ce3473ea",
"gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json": "66dd60957fee45f0",
"gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/requirements.json": "47fca61f076835fa",
"gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json": "72d1e29cedc854ec",
"gsd-core/references/edge-probe-fixtures/04-money-rounding/requirements.json": "80f04f5c04fb24cf",
"gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json": "fad67dcc8294f6da",
"gsd-core/references/edge-probe-fixtures/05-list-dedupe/requirements.json": "d38147adb0e5b342",
"gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json": "bc552c01939bf4f8",
"gsd-core/references/edge-probe-fixtures/06-resolved-mixed/requirements.json": "30a78ee9ce3473ea",
"gsd-core/references/edge-probe-fixtures/06-resolved-mixed/resolutions.json": "688ec62c13e08afe",
"gsd-core/references/edge-probe.md": "36f44960a5c1dc45",
"gsd-core/references/execute-mvp-tdd.md": "a98a270a7ab126bc",
"gsd-core/references/execute-phase-between-wave-reset.md": "3ad96ca0f7fee37e",
"gsd-core/references/execute-phase-context-guard.md": "a5a1058d35806a8e",
"gsd-core/references/execute-phase-wave-guard.md": "de9ac22cead4cfd8",
"gsd-core/references/executor-examples.md": "ba59243ed45c8ab1",
"gsd-core/references/few-shot-examples/plan-checker.md": "2574808188ac9de4",
"gsd-core/references/few-shot-examples/verifier.md": "5badee4560b14ae8",
"gsd-core/references/gate-prompts.md": "099c8d52e3562336",
"gsd-core/references/gates.md": "7dc9fd3a3d6217c6",
"gsd-core/references/git-integration.md": "5c70ef3203b7c9ce",
"gsd-core/references/git-planning-commit.md": "f897a15ebfc3f5a7",
"gsd-core/references/gsd-run-resolver.md": "1541604e8301ff6d",
"gsd-core/references/honest-verifier.md": "809f3488bc5a79d0",
"gsd-core/references/ios-scaffold.md": "5ef0cb7e0fac891f",
"gsd-core/references/loop-hook-dispatch.md": "32e5dfb4dba76987",
"gsd-core/references/mandatory-initial-read.md": "fe59abce693717cf",
"gsd-core/references/model-profile-resolution.md": "18e7cfd4ba0ca9bc",
"gsd-core/references/model-profiles.md": "c249163663bbea53",
"gsd-core/references/mvp-concepts.md": "3464783eaaef5c10",
"gsd-core/references/phase-argument-parsing.md": "e5bbb985f3bc3e34",
"gsd-core/references/planner-antipatterns.md": "7ed54ec1e2cc54ac",
"gsd-core/references/planner-chunked.md": "79fe674221e738e6",
"gsd-core/references/planner-gap-closure.md": "76bee257911413e7",
"gsd-core/references/planner-graphify-auto-update.md": "1ed614dfba72f2a3",
"gsd-core/references/planner-guidance.md": "782fa05092be3ffa",
"gsd-core/references/planner-human-verify-mode.md": "56d05e841630b3f4",
"gsd-core/references/planner-interface-context.md": "b28fa3da6ae739a8",
"gsd-core/references/planner-load-graph-context.md": "add55e135dd968da",
"gsd-core/references/planner-mvp-mode.md": "ffd7b9d0e402714e",
"gsd-core/references/planner-reviews.md": "da39eace09a10743",
"gsd-core/references/planner-revision.md": "86ba8a511f081f05",
"gsd-core/references/planner-source-audit.md": "7de5bdb07232ce0b",
"gsd-core/references/planning-config.md": "b025429fc72f9285",
"gsd-core/references/prohibition-probe-fixtures/01-streak-reminder/expected.json": "f10df472f2846cc6",
"gsd-core/references/prohibition-probe-fixtures/02-clean-utility/expected.json": "31e8a781eeffe020",
"gsd-core/references/prohibition-probe-fixtures/03-multi-prohibition/expected.json": "70a532a7cc1b6ae8",
"gsd-core/references/prohibition-probe.md": "2d3b728f8ef5cf6e",
"gsd-core/references/project-skills-discovery.md": "c155e03dce8dc3c2",
"gsd-core/references/questioning.md": "a8c988cab05f4651",
"gsd-core/references/research-documentation-lookup.md": "c070007d1d72ab71",
"gsd-core/references/research-philosophy.md": "62930e66cc979c1a",
"gsd-core/references/research-verification-protocol.md": "9c38c9d9a687e679",
"gsd-core/references/reviewer-instances.md": "1412472f858b8f41",
"gsd-core/references/revision-loop.md": "e55ff32dd98c63df",
"gsd-core/references/scout-codebase.md": "ba266ecc18fbf172",
"gsd-core/references/security-asvs-levels.md": "4774fac3b94b6ca8",
"gsd-core/references/skeleton-template.md": "528691d1f0efa878",
"gsd-core/references/sketch-interactivity.md": "7d982fe877e1e1cc",
"gsd-core/references/sketch-theme-system.md": "33e2e96e450456f8",
"gsd-core/references/sketch-tooling.md": "df6c4f24c1c27611",
"gsd-core/references/sketch-variant-patterns.md": "66c197aa4fb52810",
"gsd-core/references/specless-probe-fallback.md": "c0331cc8b7df24a8",
"gsd-core/references/spidr-splitting.md": "074ac154c0e4f906",
"gsd-core/references/tdd.md": "e4708ede157478b6",
"gsd-core/references/thinking-models-debug.md": "2da61022b16c4e7c",
"gsd-core/references/thinking-models-execution.md": "dcc650a8b5f3e049",
"gsd-core/references/thinking-models-planning.md": "7e19462313fa028f",
"gsd-core/references/thinking-models-research.md": "5f6bf3f3b889c6e4",
"gsd-core/references/thinking-models-verification.md": "a71a933d51ca3d8d",
"gsd-core/references/thinking-partner.md": "827c1badf3e6df41",
"gsd-core/references/ui-brand.md": "48717bcfcd63bd27",
"gsd-core/references/universal-anti-patterns.md": "6a1245050b21df01",
"gsd-core/references/untrusted-input-boundary.md": "d33b80d4d348599a",
"gsd-core/references/user-profiling.md": "b50416fe57c1b321",
"gsd-core/references/user-story-template.md": "0cc50e06a144ff8a",
"gsd-core/references/verification-overrides.md": "a3e2d5166d16a37b",
"gsd-core/references/verification-patterns.md": "cfaf338f42b1111e",
"gsd-core/references/verify-mvp-mode.md": "534bdc7f2432903a",
"gsd-core/references/workstream-flag.md": "ca99ca79e716f0f5",
"gsd-core/references/worktree-branch-check.md": "21d9c31bf6542b93",
"gsd-core/references/worktree-path-safety.md": "3c8d74756f9b16a8",
"gsd-core/templates/AI-SPEC.md": "24df5fe5ba34e367",
"gsd-core/templates/DEBUG.md": "57bd61bfd1d98e7e",
"gsd-core/templates/README.md": "90d2617778373147",
"gsd-core/templates/SECURITY.md": "b628f7f1c6d2328f",
"gsd-core/templates/UAT.md": "68d32d1fea14e184",
"gsd-core/templates/UI-SPEC.md": "20ca56a4e3e21f01",
"gsd-core/templates/VALIDATION.md": "6144951011cdca57",
"gsd-core/templates/claude-md.md": "d8f0fe8dba3bb28a",
"gsd-core/templates/codebase/architecture.md": "6be88214162fdd89",
"gsd-core/templates/codebase/concerns.md": "efa26d1fb5132f25",
"gsd-core/templates/codebase/conventions.md": "c2e07698dad6b364",
"gsd-core/templates/codebase/integrations.md": "39bd23c71eedd564",
"gsd-core/templates/codebase/stack.md": "116e7e67dd87ddec",
"gsd-core/templates/codebase/structure.md": "d34c1d0eb4f15ed6",
"gsd-core/templates/codebase/testing.md": "76abff7f2050c9ea",
"gsd-core/templates/config.json": "a4b783ef759a0f37",
"gsd-core/templates/context.md": "69b01e7909ea3f66",
"gsd-core/templates/continue-here.md": "f522a51b6895fba8",
"gsd-core/templates/copilot-instructions.md": "aea34bc52ff548ea",
"gsd-core/templates/debug-subagent-prompt.md": "8c18a89e25929d8e",
"gsd-core/templates/dev-preferences.md": "95048a71063d980b",
"gsd-core/templates/discovery.md": "e4ab738326eb70e0",
"gsd-core/templates/discussion-log.md": "cac1b48ec0f4dcb8",
"gsd-core/templates/milestone-archive.md": "591b6decdc0c0e51",
"gsd-core/templates/milestone.md": "74d2f750ae9f4a9c",
"gsd-core/templates/phase-prompt.md": "974daa528c2ce3f1",
"gsd-core/templates/planner-subagent-prompt.md": "6c9f1b23ee3dc05f",
"gsd-core/templates/project.md": "ae1f68db042c2522",
"gsd-core/templates/requirements.md": "a44de4c2f146e473",
"gsd-core/templates/research-project/ARCHITECTURE.md": "746b9ef791d758b0",
"gsd-core/templates/research-project/FEATURES.md": "f2b800de5df91b0f",
"gsd-core/templates/research-project/PITFALLS.md": "3ef75fa768422eec",
"gsd-core/templates/research-project/STACK.md": "82c85799ac4dd344",
"gsd-core/templates/research-project/SUMMARY.md": "dceb2f346388839d",
"gsd-core/templates/research.md": "fa6dfb2ff2e8d273",
"gsd-core/templates/retrospective.md": "03981e30dd760103",
"gsd-core/templates/roadmap.md": "e4e35a9eb5dd4d4f",
"gsd-core/templates/spec.md": "7dc900c355098d8b",
"gsd-core/templates/state.md": "73e424b8c70b765c",
"gsd-core/templates/summary-complex.md": "a5e40574fd8894dc",
"gsd-core/templates/summary-minimal.md": "7d09b5e709e2e67c",
"gsd-core/templates/summary-standard.md": "e8d9cf4a8377cdff",
"gsd-core/templates/summary.md": "23c40f6503b3ea98",
"gsd-core/templates/user-profile.md": "20749f23e4c413fc",
"gsd-core/templates/user-setup.md": "78b7d718b6e8d67c",
"gsd-core/templates/verification-report.md": "dd5faa6254183731",
"gsd-core/workflows/_runtime-launcher.snippet.sh": "bf2dd5d1debd5335",
"gsd-core/workflows/add-backlog.md": "8d05775f367d1e48",
"gsd-core/workflows/add-phase.md": "7285e6e8894a41c5",
"gsd-core/workflows/add-tests.md": "8012263b2d27b83e",
"gsd-core/workflows/add-todo.md": "1fc850476cd8340d",
"gsd-core/workflows/ai-integration-phase.md": "3503f52a7356caf0",
"gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c",
"gsd-core/workflows/audit-fix.md": "816c71b0ef2d8c1d",
"gsd-core/workflows/audit-milestone.md": "a35795246b955bdb",
"gsd-core/workflows/audit-uat.md": "1c4a02a8c1ab930f",
"gsd-core/workflows/autonomous.md": "6adf957e64518d15",
"gsd-core/workflows/check-todos.md": "8f2c6b27f18cc5e2",
"gsd-core/workflows/cleanup.md": "bfbab4b981d39544",
"gsd-core/workflows/code-review-fix.md": "78c716068ccdf820",
"gsd-core/workflows/code-review.md": "2d21452eb0449fdd",
"gsd-core/workflows/complete-milestone.md": "9962377cddee50d7",
"gsd-core/workflows/debug.md": "3354c726abbfd75b",
"gsd-core/workflows/diagnose-issues.md": "db6a599674efbc4d",
"gsd-core/workflows/discovery-phase.md": "a20dfb32adec51de",
"gsd-core/workflows/discuss-phase-assumptions.md": "35a3b2d1285565d8",
"gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6",
"gsd-core/workflows/discuss-phase.md": "ff2563dc378c5e5c",
"gsd-core/workflows/discuss-phase/modes/advisor.md": "f64ece6d53b6d432",
"gsd-core/workflows/discuss-phase/modes/all.md": "fa70d79066562e54",
"gsd-core/workflows/discuss-phase/modes/analyze.md": "da0788f3be7f8105",
"gsd-core/workflows/discuss-phase/modes/auto.md": "d0d68b06bcd43bcf",
"gsd-core/workflows/discuss-phase/modes/batch.md": "6946597770e2d448",
"gsd-core/workflows/discuss-phase/modes/chain.md": "94548620da9708dd",
"gsd-core/workflows/discuss-phase/modes/default.md": "67d1b67f61f03966",
"gsd-core/workflows/discuss-phase/modes/power.md": "dfcf239382e9bd67",
"gsd-core/workflows/discuss-phase/modes/text.md": "b62c9085d4dc2963",
"gsd-core/workflows/discuss-phase/templates/checkpoint.json": "e3bc3dca49db59eb",
"gsd-core/workflows/discuss-phase/templates/context.md": "6cd929e989fe2b0f",
"gsd-core/workflows/discuss-phase/templates/discussion-log.md": "1bbd7703f11128e1",
"gsd-core/workflows/do.md": "149084d893a23024",
"gsd-core/workflows/docs-update.md": "cd753783ab95da00",
"gsd-core/workflows/edit-phase.md": "dbbb6191f5a8b65e",
"gsd-core/workflows/eval-review.md": "086a1f2b3c11462c",
"gsd-core/workflows/execute-phase.md": "d7d8ac751aa2915e",
"gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "6d38bfd540030da4",
"gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028",
"gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "611b2be3bd133eb1",
"gsd-core/workflows/execute-phase/steps/regression-gate.md": "016ac9c7c02b1438",
"gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md": "be84efbd71e1513e",
"gsd-core/workflows/execute-plan.md": "f17623fd47e795dd",
"gsd-core/workflows/explore.md": "95e463d4bdd6dadd",
"gsd-core/workflows/extract-learnings.md": "fd75072c339b58bd",
"gsd-core/workflows/fast.md": "54fe93778b45a7eb",
"gsd-core/workflows/forensics.md": "857d7b064f4cca21",
"gsd-core/workflows/graduation.md": "ecf8da93e094fd2e",
"gsd-core/workflows/health.md": "551e63aa6f3df711",
"gsd-core/workflows/help.md": "5d040504b9ab35e3",
"gsd-core/workflows/help/modes/brief.md": "fa2675516b40e2e3",
"gsd-core/workflows/help/modes/default.md": "be05e56b2c5ee2c0",
"gsd-core/workflows/help/modes/full.md": "ce40e843f528e327",
"gsd-core/workflows/help/modes/topic.md": "6e42db16f1568be9",
"gsd-core/workflows/import.md": "cc21f3da36403ed3",
"gsd-core/workflows/inbox.md": "91aac6360e1a8672",
"gsd-core/workflows/ingest-docs.md": "1d42ea1be30becc4",
"gsd-core/workflows/insert-phase.md": "ae977afd1509dc86",
"gsd-core/workflows/list-phase-assumptions.md": "2a6b6a5acfb7742c",
"gsd-core/workflows/list-seeds.md": "7576156b91e9abee",
"gsd-core/workflows/list-workspaces.md": "90caeeb11cec0128",
"gsd-core/workflows/manager.md": "36559ff897e55c09",
"gsd-core/workflows/map-codebase.md": "b11ca99a885e93ef",
"gsd-core/workflows/milestone-summary.md": "99636900c216c8d2",
"gsd-core/workflows/mvp-phase.md": "254baac57e85dca7",
"gsd-core/workflows/new-milestone.md": "fcb63a8b13c02d6e",
"gsd-core/workflows/new-project.md": "26907d3cf3630ed0",
"gsd-core/workflows/new-workspace.md": "26615bf710f0a324",
"gsd-core/workflows/next.md": "1193222c5618d3db",
"gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b",
"gsd-core/workflows/note.md": "a2cc926854a5666c",
"gsd-core/workflows/onboard.md": "b86d78eef6c77e5b",
"gsd-core/workflows/pause-work.md": "da902807d2213204",
"gsd-core/workflows/plan-milestone-gaps.md": "7679fac068d1009d",
"gsd-core/workflows/plan-phase.md": "8e8331ca99bc8680",
"gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60",
"gsd-core/workflows/plan-phase/steps/prd-express-path.md": "b810f9f2374e23a5",
"gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "e9de7a96bbfff261",
"gsd-core/workflows/plan-review-convergence.md": "1cabea77790fc16c",
"gsd-core/workflows/plant-seed.md": "fbe964fcdb244802",
"gsd-core/workflows/pr-branch.md": "513f6cff722eff2d",
"gsd-core/workflows/profile-user.md": "3b34dcb337d50f4b",
"gsd-core/workflows/progress.md": "2be3a57916eccf87",
"gsd-core/workflows/quick.md": "20f9dbfcd20b4b4b",
"gsd-core/workflows/reapply-patches.md": "44a96b52b975e9bb",
"gsd-core/workflows/remove-phase.md": "8effc8742d58a11a",
"gsd-core/workflows/remove-workspace.md": "10882656198d9075",
"gsd-core/workflows/resume-project.md": "af9761bcec0f6fe9",
"gsd-core/workflows/review.md": "34cb7ab671eb8684",
"gsd-core/workflows/scan.md": "75c670d08cee8680",
"gsd-core/workflows/secure-phase.md": "64ec4d06ca85720a",
"gsd-core/workflows/session-report.md": "2e5b1205324ddefa",
"gsd-core/workflows/settings-advanced.md": "2fc2738442f4f9de",
"gsd-core/workflows/settings-integrations.md": "dfe3672c4fabf139",
"gsd-core/workflows/settings.md": "ba138b058d91fd38",
"gsd-core/workflows/ship.md": "44af1c72d86e153b",
"gsd-core/workflows/sketch-wrap-up.md": "d52a5462bafda830",
"gsd-core/workflows/sketch.md": "dbe6acc4d976060c",
"gsd-core/workflows/smart-entry.md": "1850447c045f36d8",
"gsd-core/workflows/spec-phase.md": "e03fa9f1a44613dc",
"gsd-core/workflows/spike-wrap-up.md": "4bdfaf9d05c63e7c",
"gsd-core/workflows/spike.md": "1571a05457beea8d",
"gsd-core/workflows/stats.md": "3953356f476b5053",
"gsd-core/workflows/sync-skills.md": "5624d529dae1ad79",
"gsd-core/workflows/thread.md": "14a9d195572a198f",
"gsd-core/workflows/transition.md": "78a91b0154a93cf5",
"gsd-core/workflows/ui-phase.md": "57664a12509b455d",
"gsd-core/workflows/ui-review.md": "9c6005236e2067b5",
"gsd-core/workflows/ultraplan-phase.md": "b926ba7e4de0c76d",
"gsd-core/workflows/undo.md": "d759702f84e308fa",
"gsd-core/workflows/update.md": "2a59b4edf4a3c8c7",
"gsd-core/workflows/validate-phase.md": "83eeefeeca31c2b5",
"gsd-core/workflows/verify-phase.md": "6ae6f159be75dcdd",
"gsd-core/workflows/verify-work.md": "de14acdc8e925338",
"hooks/gsd-check-update-worker.js": "a530efdb5fdc0da3",
"hooks/gsd-check-update.js": "25cde66a12d6b886",
"hooks/gsd-config-reload.js": "96546e0e8bb47904",
"hooks/gsd-context-monitor.js": "ecbe9747e4a442e0",
"hooks/gsd-cursor-post-tool.js": "8a8a249c0642cc71",
"hooks/gsd-cursor-session-start.js": "05a14e903c5edafa",
"hooks/gsd-ensure-canonical-path.js": "b4b3b88a0e493b16",
"hooks/gsd-graphify-update.sh": "e4c6e14fe6ad64ff",
"hooks/gsd-phase-boundary.sh": "32739d5fbe0d0a1c",
"hooks/gsd-prompt-guard.js": "a749b8cb2c5248de",
"hooks/gsd-read-guard.js": "9e423cd03e2d1b16",
"hooks/gsd-read-injection-scanner.js": "00d2449afefd2e5f",
"hooks/gsd-session-state.sh": "e54379ba86bf1b6d",
"hooks/gsd-statusline.js": "7c315416ffc99a9a",
"hooks/gsd-update-banner.js": "b457746cb76c1957",
"hooks/gsd-validate-commit.sh": "bf5dd61d33cb3a38",
"hooks/gsd-workflow-guard.js": "59b46a74d19d58d3",
"hooks/gsd-worktree-path-guard.js": "02be1bb504b22eb5",
"hooks/lib/git-cmd.js": "268ba15992ca0b23",
"hooks/lib/gsd-graphify-rebuild.sh": "66af89601074d2a9",
"hooks/managed-hooks-registry.cjs": "ea876b1ec185173e",
"package.json": "dbf8353f77358bc1",
"scripts/changeset/README.md": "86ff89331dfd94b2",
"scripts/changeset/cli.cjs": "68f92a344b199271",
"scripts/changeset/github-release-notes.cjs": "795677f0c009b132",
"scripts/changeset/lint.cjs": "0066faed159154f0",
"scripts/changeset/new.cjs": "4991e21fd17f5541",
"scripts/changeset/parse.cjs": "f9a949cbcab56445",
"scripts/changeset/render.cjs": "e47bc3e1587c3cae",
"scripts/changeset/serialize.cjs": "ac0b8fe6f87cdb0e",
"scripts/fix-slash-commands.cjs": "0519742531ff3529",
"scripts/gen-capability-registry.cjs": "c52201ff4d1c2cd7",
"scripts/gen-loop-host-contract.cjs": "c7f15237234811a0",
"scripts/lib/allowlist-ratchet.cjs": "ffaceaac3efc2660",
"scripts/lib/cli-exit.cjs": "612d0c372c75b7e7"
}

View File

@@ -80,7 +80,9 @@ const PKG_VERSION = require('../package.json').version;
// (install-minimal-hooks, sh-hook-paths, codex-config, etc.). Matched by basename.
// settings.json = Claude/Antigravity/Augment/etc. hook surface; hooks.json =
// Codex/Cursor hook surface — both embed the platform-varying node-runner command.
const HOOK_CONFIG_FILES = new Set(['settings.json', 'hooks.json']);
// settings.local.json = Claude LOCAL hook surface (#338): same platform-varying
// node-runner command as settings.json, so excluded for the same reason (#2086).
const HOOK_CONFIG_FILES = new Set(['settings.json', 'settings.local.json', 'hooks.json']);
// Path prefixes excluded from the parity manifest. `gsd-core/bin/lib/` holds the
// tsc-built runtime artifacts (compiled from src/*.cts) that the install COPIES
@@ -111,6 +113,17 @@ function buildParityManifest(configDir, root) {
const allFiles = walk(configDir);
const unsorted = {};
// The claude LOCAL install resolves its config dir via realpath, which on macOS
// prepends `/private` to the temp root (`/var/folders/…` -> `/private/var/folders/…`)
// and embeds that resolved path in the projected agents/commands/workflows (`@…`
// references). On Linux the temp root has no `/private` symlink, so normalizing
// ONLY `root` left the `/private` prefix on macOS and produced platform-divergent
// hashes (#2086). Normalize the realpath form FIRST (it is the longer, `/private`-
// prefixed string) so both platforms collapse to `<HOME>`. No-op for the global
// fixtures (global install uses the literal `--config-dir`, never realpath-resolved).
let realRoot = root;
try { realRoot = fs.realpathSync(root); } catch { /* root already gone / not resolvable */ }
for (const full of allFiles) {
// Build POSIX-style relative path for cross-platform stability
const rel = path.relative(configDir, full).split(path.sep).join('/');
@@ -123,7 +136,10 @@ function buildParityManifest(configDir, root) {
// Normalize every occurrence of the temp root so hashes are stable across runs.
// Also normalize the package version so the golden survives `npm version` bumps
// (the rc release step bakes the new version into hook files before running tests).
const normalized = content.toString('utf8').split(root).join('<HOME>').split(PKG_VERSION).join('<VERSION>');
const normalized = content.toString('utf8')
.split(realRoot).join('<HOME>')
.split(root).join('<HOME>')
.split(PKG_VERSION).join('<VERSION>');
const hash = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16);
unsorted[rel] = hash;
}
@@ -194,3 +210,53 @@ for (const runtime of runtimes) {
}
});
}
// #2086 (EoS/claude): claude is the reference host and the ONLY runtime with a
// distinct LOCAL "legacy flat-commands" layout (commands/gsd-*.md + agents/gsd-*.md).
// The loop above asserts the GLOBAL skills layout; this asserts the LOCAL
// commands/agents layout is byte-identical too, so folding claude's
// `runtime === 'claude'` branches into descriptor-driven hostBehaviors cannot
// silently change the local install output (AC1: "both scopes"). NOTE: the
// settings.local.json ROUTING itself is excluded here (platform-varying node-runner
// path) — that dimension is covered directly by install.test.cjs's #338 suite.
test('golden parity — claude (local legacy layout)', async (t) => {
if (process.platform === 'win32') {
t.skip('install output is platform-specific on Windows (backslash paths); parity is asserted on macOS + Linux');
return;
}
const { configDir, root } = runMinimalInstall({ runtime: 'claude', scope: 'local' });
let actual;
try {
actual = buildParityManifest(configDir, root);
} finally {
cleanup(root);
}
const fixturePath = path.join(FIXTURE_DIR, 'claude-local.json');
if (UPDATE) {
fs.writeFileSync(fixturePath, JSON.stringify(actual, null, 2) + '\n', 'utf8');
process.stdout.write(` [UPDATE] claude-local: wrote ${Object.keys(actual).length} file hashes → ${fixturePath}\n`);
return;
}
if (!fs.existsSync(fixturePath)) {
assert.fail(
`Golden fixture missing for claude-local: ${fixturePath}\n` +
'Run UPDATE_GOLDEN=1 node --test tests/golden-install-parity.test.cjs to capture.',
);
}
const golden = JSON.parse(fs.readFileSync(fixturePath, 'utf8'));
const added = Object.keys(actual).filter(k => !(k in golden));
const removed = Object.keys(golden).filter(k => !(k in actual));
const changed = Object.keys(actual).filter(k => k in golden && actual[k] !== golden[k]);
if (added.length || removed.length || changed.length) {
const lines = ['Parity mismatch for claude-local:'];
if (added.length) lines.push(` added (${added.length}): ${added.join(', ')}`);
if (removed.length) lines.push(` removed (${removed.length}): ${removed.join(', ')}`);
if (changed.length) lines.push(` changed (${changed.length}): ${changed.join(', ')}`);
lines.push('Run UPDATE_GOLDEN=1 to recapture if the change is intentional.');
assert.deepEqual(actual, golden, lines.join('\n'));
}
});