From fdd5e401ebc2f4cefb956441785de4a2c1a6e84c Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 8 Jul 2026 13:15:43 -0400 Subject: [PATCH 1/5] feat(architecture): [EoS/claude] drive claude through the imperative adapter + descriptor-driven hostBehaviors (#2086) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fold Claude Code's install/uninstall onto the Embeddable Orchestration System (ADR-1239 Phase D). claude is GSD's tier-1 reference host, but its install path was still driven by 13 hardcoded `runtime === 'claude'` string-equality branches scattered across bin/install.js rather than the public Host-Integration Interface. - Route install()/uninstall() through `createImperativeAdapter({runtime})` — the adapter delegates to the SAME installRuntimeArtifacts/uninstallRuntimeArtifacts engine calls, so output is byte-identical (proven pre/post, both scopes). - Replace all 13 `runtime === 'claude'` / `runtime !== 'claude'` branches with descriptor-driven `runtime.hostBehaviors` lookups on capabilities/claude/ capability.json (attributionSource, authorsCanonicalWorkflow, localInstallStyle, permissionsSchema, settingsFileByScope, sourceMarkerFile, agentFrontmatterExtensions, ownsClaudePaths, nativeModelAliases, skillsGlobalOnboarding). Behavior is identical; the brittle string-equality coupling (the add-a-host tax) is gone. - Single-source the scattered literal 'claude' defaults/rosters behind DEFAULT_RUNTIME. - Extend golden-install-parity to assert the claude LOCAL legacy layout is byte-identical too (AC1 "both scopes"); exclude the platform-varying settings.local.json (same reason settings.json is excluded). - New tests/claude-imperative-reference.test.cjs: adapter kind, programmatic-cli profile, fail-closed negotiation on a corrupted/partial descriptor, and an AC2 source guard that no `runtime === 'claude'` branch remains. No user-visible install-output change (internal architecture only). Co-Authored-By: Claude Opus 4.8 --- .../2086-eos-claude-imperative-adapter.md | 5 + bin/install.js | 121 +++-- capabilities/claude/capability.json | 15 + .../add-or-update-a-host-integration.md | 45 ++ gsd-core/bin/lib/capability-registry.cjs | 34 ++ tests/claude-imperative-reference.test.cjs | 138 ++++++ .../golden-install-parity/claude-local.json | 418 ++++++++++++++++++ tests/golden-install-parity.test.cjs | 54 ++- 8 files changed, 799 insertions(+), 31 deletions(-) create mode 100644 .changeset/2086-eos-claude-imperative-adapter.md create mode 100644 tests/claude-imperative-reference.test.cjs create mode 100644 tests/fixtures/golden-install-parity/claude-local.json diff --git a/.changeset/2086-eos-claude-imperative-adapter.md b/.changeset/2086-eos-claude-imperative-adapter.md new file mode 100644 index 000000000..52d9e1762 --- /dev/null +++ b/.changeset/2086-eos-claude-imperative-adapter.md @@ -0,0 +1,5 @@ +--- +type: Changed +pr: 2086 +--- +**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) diff --git a/bin/install.js b/bin/install.js index 6cf14cea7..92acdb147 100755 --- a/bin/install.js +++ b/bin/install.js @@ -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,34 @@ try { } catch (_) { _capabilityRegistry = undefined; } + +/** + * Host-specific install behaviors, declared on the runtime descriptor + * (capabilities//capability.json -> runtime.hostBehaviors) instead of + * scattered `runtime === ''` string checks (ADR-1239 / #2086). Returns {} + * for runtimes that declare none, so every behavior branch degrades to the + * generic path by default. + */ +function _hostBehaviors(runtime) { + const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime]; + return (cap && cap.runtime && cap.runtime.hostBehaviors) || {}; +} + +/** + * 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 +1276,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 +6311,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 +6513,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 +6571,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 +6744,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 +7047,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 +7522,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 +7560,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 +7973,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 +8016,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 +8461,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 +8472,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 +8786,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 +8988,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 +9275,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 +9798,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 +10071,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 +10130,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 +10163,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 +10205,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 +10364,7 @@ function parseRuntimeInput(answer) { } } - return selected.length > 0 ? selected : ['claude']; + return selected.length > 0 ? selected : [DEFAULT_RUNTIME]; } function promptRuntime(callback) { @@ -10922,7 +10983,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) => { @@ -11206,7 +11267,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 +11279,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); diff --git a/capabilities/claude/capability.json b/capabilities/claude/capability.json index 7bdf6bd40..3dd1ddc0b 100644 --- a/capabilities/claude/capability.json +++ b/capabilities/claude/capability.json @@ -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 } } } diff --git a/docs/how-to/add-or-update-a-host-integration.md b/docs/how-to/add-or-update-a-host-integration.md index 5dd8a1fc5..0c279e70d 100644 --- a/docs/how-to/add-or-update-a-host-integration.md +++ b/docs/how-to/add-or-update-a-host-integration.md @@ -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 === ''` 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 === ''` / `runtime !== ''` 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 === ''` branch remains** in `bin/install.js`. + --- ## Related diff --git a/gsd-core/bin/lib/capability-registry.cjs b/gsd-core/bin/lib/capability-registry.cjs index 106f56433..82900ecaa 100644 --- a/gsd-core/bin/lib/capability-registry.cjs +++ b/gsd-core/bin/lib/capability-registry.cjs @@ -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 } } }, diff --git a/tests/claude-imperative-reference.test.cjs b/tests/claude-imperative-reference.test.cjs new file mode 100644 index 000000000..94ef54df7 --- /dev/null +++ b/tests/claude-imperative-reference.test.cjs @@ -0,0 +1,138 @@ +// 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; + +// -- 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'); + const offenders = src.match(/runtime\s*[!=]==\s*'claude'/g) || []; + assert.deepEqual( + offenders, + [], + `AC2: every hardcoded runtime==='claude'/!=='claude' branch must be descriptor-driven; found: ${offenders.join(', ')}`, + ); +}); diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json new file mode 100644 index 000000000..6b39c5300 --- /dev/null +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -0,0 +1,418 @@ +{ + ".gsd-profile": "0e716a5fef4e6dc1", + "agents/gsd-advisor-researcher.md": "4e1a8afe45ef8298", + "agents/gsd-ai-researcher.md": "1de9bb9c75e3d668", + "agents/gsd-assumptions-analyzer.md": "63c8bdab78b42128", + "agents/gsd-code-fixer.md": "341e9d4e5c081f0e", + "agents/gsd-code-reviewer.md": "65009b1743904fd3", + "agents/gsd-codebase-mapper.md": "6b58924e7ab595b6", + "agents/gsd-debug-session-manager.md": "df63cabbe4d062a0", + "agents/gsd-debugger.md": "f6874f2e4e74c6fe", + "agents/gsd-doc-classifier.md": "4f52b062005868ec", + "agents/gsd-doc-synthesizer.md": "4be3aac2095d021d", + "agents/gsd-doc-verifier.md": "4232dcf9076e3566", + "agents/gsd-doc-writer.md": "a8af8a58c28e06b2", + "agents/gsd-domain-researcher.md": "c5f1f069f3124844", + "agents/gsd-eval-auditor.md": "d8debe9e60c26574", + "agents/gsd-eval-planner.md": "8b2359a18a618b04", + "agents/gsd-executor.md": "abf1192f0ac098dc", + "agents/gsd-framework-selector.md": "a359da24df68f01c", + "agents/gsd-integration-checker.md": "1d2ca1591310bf70", + "agents/gsd-intel-updater.md": "ae95a5f8b73a465b", + "agents/gsd-mempalace-curator.md": "77b53f1b155242b4", + "agents/gsd-nyquist-auditor.md": "06e42106449d17ac", + "agents/gsd-pattern-mapper.md": "b45b5e106775bec1", + "agents/gsd-phase-researcher.md": "90f97c741075c907", + "agents/gsd-plan-checker.md": "63e7988a3d7db5cf", + "agents/gsd-planner.md": "0fb1385f2ea179f0", + "agents/gsd-project-researcher.md": "78d92594b3de6e51", + "agents/gsd-research-synthesizer.md": "635639f831464f95", + "agents/gsd-roadmapper.md": "90d11d44ac7d5121", + "agents/gsd-security-auditor.md": "c8eead1524c5a465", + "agents/gsd-ui-auditor.md": "94868774ac120a38", + "agents/gsd-ui-checker.md": "505ca13a25cb67df", + "agents/gsd-ui-researcher.md": "4596a60f979dc769", + "agents/gsd-user-profiler.md": "a5cd095688d6de25", + "agents/gsd-verifier.md": "7327f257ade6d00f", + "commands/gsd-add-tests.md": "993d9c16477b17bf", + "commands/gsd-ai-integration-phase.md": "bd8407401cc52092", + "commands/gsd-audit-fix.md": "0ce7dff55a550d96", + "commands/gsd-audit-milestone.md": "506d3d20f55d80ce", + "commands/gsd-audit-uat.md": "4aefa85d28437405", + "commands/gsd-autonomous.md": "ef5a492c9085b983", + "commands/gsd-capture.md": "61647c8baefcb116", + "commands/gsd-cleanup.md": "88e8c3d4b471a036", + "commands/gsd-code-review.md": "98a9c59ff3edb4ab", + "commands/gsd-complete-milestone.md": "7639dce1b9e755e4", + "commands/gsd-config.md": "b36340912b70c073", + "commands/gsd-debug.md": "e622240e54587e1b", + "commands/gsd-discuss-phase.md": "9eebccc3820ad37a", + "commands/gsd-docs-update.md": "a11bedfbfd0a56db", + "commands/gsd-eval-review.md": "dc602abbb7525761", + "commands/gsd-execute-phase.md": "f47b83e2110f7be8", + "commands/gsd-explore.md": "dbef734b08f7438a", + "commands/gsd-extract-learnings.md": "f3059f4e6e6b7b19", + "commands/gsd-fast.md": "be784c9f67ae3eab", + "commands/gsd-forensics.md": "36ec35a6a6f2cfd7", + "commands/gsd-graphify.md": "1b88285a888f69dd", + "commands/gsd-health.md": "f28dabf45c4ddd77", + "commands/gsd-help.md": "e48f4e1cf9ff3ad3", + "commands/gsd-import.md": "849c4a4bdfc05ebf", + "commands/gsd-inbox.md": "61eebdd7397efe7f", + "commands/gsd-ingest-docs.md": "10b1c8fee7ed7c1f", + "commands/gsd-manager.md": "0c5541f3db284fe3", + "commands/gsd-map-codebase.md": "9c32f13ade27d418", + "commands/gsd-mempalace-capture.md": "2e49397072506fd9", + "commands/gsd-mempalace-recall.md": "38716c0983a3ef9c", + "commands/gsd-milestone-summary.md": "a7fdafaaa3a665af", + "commands/gsd-mvp-phase.md": "fe9bf9df99007be1", + "commands/gsd-new-milestone.md": "d2f21e6e3d53b2e9", + "commands/gsd-new-project.md": "2129c259fa653b19", + "commands/gsd-next.md": "02d0abbd2f55a0a2", + "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": "2d98ea48454293b5", + "commands/gsd-pause-work.md": "bdfc8f46a674061a", + "commands/gsd-phase.md": "b211ba5969b4d69b", + "commands/gsd-plan-phase.md": "8113654accd5bee9", + "commands/gsd-plan-review-convergence.md": "3d496dbbba45190c", + "commands/gsd-pr-branch.md": "2346583e447a46d6", + "commands/gsd-profile-user.md": "8cc1a47fe685b60e", + "commands/gsd-progress.md": "0eddf50b039adc8e", + "commands/gsd-quick.md": "116941d841ecfb07", + "commands/gsd-resume-work.md": "330cd86844bd768b", + "commands/gsd-review-backlog.md": "434ebe7b63c107e0", + "commands/gsd-review.md": "65b5225d8405f290", + "commands/gsd-secure-phase.md": "1ab3de7d982a7b9a", + "commands/gsd-settings.md": "13a4293f7fa58276", + "commands/gsd-ship.md": "551dc7f66e14a3de", + "commands/gsd-sketch.md": "0d01a055aa4b00ee", + "commands/gsd-spec-phase.md": "b3f6c26deba9172f", + "commands/gsd-spike.md": "7c102d63239f6678", + "commands/gsd-stats.md": "dc7c484c0fc2c177", + "commands/gsd-surface.md": "81c0c0d634ae0375", + "commands/gsd-thread.md": "4d9ac2986e090f5f", + "commands/gsd-ui-phase.md": "19ab8467a3f737a6", + "commands/gsd-ui-review.md": "5e0e6a377719a125", + "commands/gsd-ultraplan-phase.md": "29426afa8f6bcef3", + "commands/gsd-undo.md": "3a8299018ada31f6", + "commands/gsd-update.md": "22f2060ab482d569", + "commands/gsd-validate-phase.md": "079a0013bd14c1bd", + "commands/gsd-verify-work.md": "a7b07cc4547e7099", + "commands/gsd-workspace.md": "24b4c632d9773c11", + "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": "08e3c3aa1f2ac11e", + "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": "338acaf52853668c", + "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": "92e6574b30f5b765", + "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": "46173caac1d469d4", + "gsd-core/references/honest-verifier.md": "6e4b8293f644b3f1", + "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": "a2fa775cac3470fa", + "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": "563d1a434304d5cc", + "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": "0126c6156a9d32a8", + "gsd-core/references/planner-mvp-mode.md": "f7fc3894770a73f8", + "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": "7612bb55f00359bb", + "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": "d22e89615c846b72", + "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": "57c67c5fd375fcb3", + "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": "b106ec2b588d2b51", + "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": "d07f52e21f15f2ab", + "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": "450f70cb4ba4bf78", + "gsd-core/workflows/add-phase.md": "500274ecffbc2b12", + "gsd-core/workflows/add-tests.md": "575110e32af4eac3", + "gsd-core/workflows/add-todo.md": "4f68274dbf475704", + "gsd-core/workflows/ai-integration-phase.md": "1be55b37af12a639", + "gsd-core/workflows/analyze-dependencies.md": "77aff48f97fa6f1c", + "gsd-core/workflows/audit-fix.md": "bab7ebaee574e486", + "gsd-core/workflows/audit-milestone.md": "3b5667e761f97774", + "gsd-core/workflows/audit-uat.md": "470c4637eb53b4a3", + "gsd-core/workflows/autonomous.md": "275e66d559d76792", + "gsd-core/workflows/check-todos.md": "dd1504bb42b17021", + "gsd-core/workflows/cleanup.md": "e06b655e5e901031", + "gsd-core/workflows/code-review-fix.md": "c6d26d40e143acca", + "gsd-core/workflows/code-review.md": "1bb99932450e9ced", + "gsd-core/workflows/complete-milestone.md": "7f0346ab9b6ab2b4", + "gsd-core/workflows/debug.md": "722e00ef94fdb5c2", + "gsd-core/workflows/diagnose-issues.md": "17fa373be09a0b82", + "gsd-core/workflows/discovery-phase.md": "b9f80db0c22d723a", + "gsd-core/workflows/discuss-phase-assumptions.md": "e3693f4e1746bc13", + "gsd-core/workflows/discuss-phase-power.md": "290c0d83d783f9f6", + "gsd-core/workflows/discuss-phase.md": "6f5c0280e386a12e", + "gsd-core/workflows/discuss-phase/modes/advisor.md": "93b4ff65a03a5487", + "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": "fe5daf7c8617e26b", + "gsd-core/workflows/discuss-phase/modes/batch.md": "6946597770e2d448", + "gsd-core/workflows/discuss-phase/modes/chain.md": "2d1f9f2396f5d0f4", + "gsd-core/workflows/discuss-phase/modes/default.md": "67d1b67f61f03966", + "gsd-core/workflows/discuss-phase/modes/power.md": "7c10e13c60b73af0", + "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": "e55fe97bac5d691f", + "gsd-core/workflows/docs-update.md": "fff626c2033d34d2", + "gsd-core/workflows/edit-phase.md": "67efb8a73213ea5a", + "gsd-core/workflows/eval-review.md": "db4b8795864f104e", + "gsd-core/workflows/execute-phase.md": "4ef4e55d34c9c605", + "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "839b0006c551f9fb", + "gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md": "7ebb7d1af6082028", + "gsd-core/workflows/execute-phase/steps/post-merge-gate.md": "cfe19fe951583f3f", + "gsd-core/workflows/execute-phase/steps/regression-gate.md": "e40c5eb516a9e052", + "gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md": "be84efbd71e1513e", + "gsd-core/workflows/execute-plan.md": "5d66d59ce03f88ba", + "gsd-core/workflows/explore.md": "eb4a623b5732303f", + "gsd-core/workflows/extract-learnings.md": "d1e0a14a9df00195", + "gsd-core/workflows/fast.md": "54fe93778b45a7eb", + "gsd-core/workflows/forensics.md": "f66bb7b50dcc918a", + "gsd-core/workflows/graduation.md": "35ebdec95d86d45b", + "gsd-core/workflows/health.md": "bcc07e9610635d49", + "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": "579577e8095f8fef", + "gsd-core/workflows/help/modes/topic.md": "6e42db16f1568be9", + "gsd-core/workflows/import.md": "88e6ef80b6d1db63", + "gsd-core/workflows/inbox.md": "91aac6360e1a8672", + "gsd-core/workflows/ingest-docs.md": "59224a43a74f0f88", + "gsd-core/workflows/insert-phase.md": "653460de68191dd7", + "gsd-core/workflows/list-phase-assumptions.md": "2a6b6a5acfb7742c", + "gsd-core/workflows/list-seeds.md": "c7026201e80f21ff", + "gsd-core/workflows/list-workspaces.md": "2c2f5e466cf97136", + "gsd-core/workflows/manager.md": "f0835592a5f4ca39", + "gsd-core/workflows/map-codebase.md": "a2c4e72aac41f477", + "gsd-core/workflows/milestone-summary.md": "4ad8a311ebf766a8", + "gsd-core/workflows/mvp-phase.md": "e459fa1351dbb663", + "gsd-core/workflows/new-milestone.md": "417bbc34f965997f", + "gsd-core/workflows/new-project.md": "4c7c381c8bc0bd98", + "gsd-core/workflows/new-workspace.md": "f907fed6831f3f32", + "gsd-core/workflows/next.md": "f2f2210290a02171", + "gsd-core/workflows/node-repair.md": "07a1628e5a1ff96b", + "gsd-core/workflows/note.md": "def2655cebe5c001", + "gsd-core/workflows/onboard.md": "9ae63d270f8fd7c0", + "gsd-core/workflows/pause-work.md": "9655ec327c5aa307", + "gsd-core/workflows/plan-milestone-gaps.md": "3be3a097114fdde6", + "gsd-core/workflows/plan-phase.md": "b1643a49267f3f01", + "gsd-core/workflows/plan-phase/steps/closed-phase-gate.md": "4099ef6d0868de60", + "gsd-core/workflows/plan-phase/steps/prd-express-path.md": "af3029d3c899f4f2", + "gsd-core/workflows/plan-phase/steps/windows-troubleshooting.md": "e9de7a96bbfff261", + "gsd-core/workflows/plan-review-convergence.md": "b09378bcc5571ca4", + "gsd-core/workflows/plant-seed.md": "4f51d8d267fd56a5", + "gsd-core/workflows/pr-branch.md": "8f6db372a275f7b1", + "gsd-core/workflows/profile-user.md": "aeb21d7386c4a41e", + "gsd-core/workflows/progress.md": "b482c9befd4af94a", + "gsd-core/workflows/quick.md": "668e7e8370ba9992", + "gsd-core/workflows/reapply-patches.md": "6466079b57163be0", + "gsd-core/workflows/remove-phase.md": "7cc144da60d95241", + "gsd-core/workflows/remove-workspace.md": "53e9575a8c4fb411", + "gsd-core/workflows/resume-project.md": "20de3d2834ee581a", + "gsd-core/workflows/review.md": "5ea70bc48614f68f", + "gsd-core/workflows/scan.md": "edba4c0bac5727a6", + "gsd-core/workflows/secure-phase.md": "265b3244a74a9bdd", + "gsd-core/workflows/session-report.md": "2e5b1205324ddefa", + "gsd-core/workflows/settings-advanced.md": "d148d5233169883f", + "gsd-core/workflows/settings-integrations.md": "b03bdf37b4f5310b", + "gsd-core/workflows/settings.md": "c24424577729f318", + "gsd-core/workflows/ship.md": "4d0fb3bb33f286f8", + "gsd-core/workflows/sketch-wrap-up.md": "47cb472ba0445c10", + "gsd-core/workflows/sketch.md": "d4de5174c8242425", + "gsd-core/workflows/smart-entry.md": "3fe82b4a6e67d0cf", + "gsd-core/workflows/spec-phase.md": "b35b7733e8ece267", + "gsd-core/workflows/spike-wrap-up.md": "496a6afc08c42a8d", + "gsd-core/workflows/spike.md": "3b2a826377c451ff", + "gsd-core/workflows/stats.md": "75618011901cebce", + "gsd-core/workflows/sync-skills.md": "452c01c7a05b238c", + "gsd-core/workflows/thread.md": "9d9cf44f7f6d788e", + "gsd-core/workflows/transition.md": "18f610382e9641f6", + "gsd-core/workflows/ui-phase.md": "4c8adbabd9f71a78", + "gsd-core/workflows/ui-review.md": "108e094e6aa085f6", + "gsd-core/workflows/ultraplan-phase.md": "e759430ce77b45db", + "gsd-core/workflows/undo.md": "e1eeffa679786c8a", + "gsd-core/workflows/update.md": "c276a4f628f4b948", + "gsd-core/workflows/validate-phase.md": "96c5ee354b72af56", + "gsd-core/workflows/verify-phase.md": "5ee79da8a9e787b3", + "gsd-core/workflows/verify-work.md": "25c8dd4c6ca6b5ae", + "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" +} diff --git a/tests/golden-install-parity.test.cjs b/tests/golden-install-parity.test.cjs index 2bf4ff1e3..c7f9f7817 100644 --- a/tests/golden-install-parity.test.cjs +++ b/tests/golden-install-parity.test.cjs @@ -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 @@ -194,3 +196,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')); + } +}); From cf159f79292ff864689f3a3083d1c2375b891e00 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 8 Jul 2026 14:38:32 -0400 Subject: [PATCH 2/5] docs(changeset): backfill pr 2106 for #2086 changeset Co-Authored-By: Claude Opus 4.8 --- .changeset/2086-eos-claude-imperative-adapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/2086-eos-claude-imperative-adapter.md b/.changeset/2086-eos-claude-imperative-adapter.md index 52d9e1762..73ab37742 100644 --- a/.changeset/2086-eos-claude-imperative-adapter.md +++ b/.changeset/2086-eos-claude-imperative-adapter.md @@ -1,5 +1,5 @@ --- type: Changed -pr: 2086 +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) From ff6e9285308400701f789bb13f2b431988864f1b Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 8 Jul 2026 14:49:58 -0400 Subject: [PATCH 3/5] fix(#2086): normalize realpath temp root in golden parity manifest (macOS /private) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The claude LOCAL install resolves its config dir via realpath, which on macOS prepends /private to the temp root and embeds it in projected agents/commands/ workflows (@ references). buildParityManifest normalized only `root` (/var/folders/…), leaving the /private prefix on macOS while Linux has none — so the mac-generated claude-local fixture failed the Linux CI leg (198 files). Normalize the realpath form too; no-op for the global fixtures (literal --config-dir, never realpath-resolved). Regenerated claude-local.json now matches the Linux hashes. Co-Authored-By: Claude Opus 4.8 --- .../golden-install-parity/claude-local.json | 396 +++++++++--------- tests/golden-install-parity.test.cjs | 16 +- 2 files changed, 213 insertions(+), 199 deletions(-) diff --git a/tests/fixtures/golden-install-parity/claude-local.json b/tests/fixtures/golden-install-parity/claude-local.json index 6b39c5300..72a5c64bb 100644 --- a/tests/fixtures/golden-install-parity/claude-local.json +++ b/tests/fixtures/golden-install-parity/claude-local.json @@ -1,109 +1,109 @@ { ".gsd-profile": "0e716a5fef4e6dc1", - "agents/gsd-advisor-researcher.md": "4e1a8afe45ef8298", - "agents/gsd-ai-researcher.md": "1de9bb9c75e3d668", - "agents/gsd-assumptions-analyzer.md": "63c8bdab78b42128", - "agents/gsd-code-fixer.md": "341e9d4e5c081f0e", - "agents/gsd-code-reviewer.md": "65009b1743904fd3", - "agents/gsd-codebase-mapper.md": "6b58924e7ab595b6", - "agents/gsd-debug-session-manager.md": "df63cabbe4d062a0", - "agents/gsd-debugger.md": "f6874f2e4e74c6fe", - "agents/gsd-doc-classifier.md": "4f52b062005868ec", - "agents/gsd-doc-synthesizer.md": "4be3aac2095d021d", + "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": "a8af8a58c28e06b2", - "agents/gsd-domain-researcher.md": "c5f1f069f3124844", - "agents/gsd-eval-auditor.md": "d8debe9e60c26574", - "agents/gsd-eval-planner.md": "8b2359a18a618b04", - "agents/gsd-executor.md": "abf1192f0ac098dc", - "agents/gsd-framework-selector.md": "a359da24df68f01c", - "agents/gsd-integration-checker.md": "1d2ca1591310bf70", - "agents/gsd-intel-updater.md": "ae95a5f8b73a465b", + "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": "06e42106449d17ac", + "agents/gsd-nyquist-auditor.md": "f86a28f5c164a0d3", "agents/gsd-pattern-mapper.md": "b45b5e106775bec1", - "agents/gsd-phase-researcher.md": "90f97c741075c907", - "agents/gsd-plan-checker.md": "63e7988a3d7db5cf", - "agents/gsd-planner.md": "0fb1385f2ea179f0", - "agents/gsd-project-researcher.md": "78d92594b3de6e51", - "agents/gsd-research-synthesizer.md": "635639f831464f95", - "agents/gsd-roadmapper.md": "90d11d44ac7d5121", - "agents/gsd-security-auditor.md": "c8eead1524c5a465", - "agents/gsd-ui-auditor.md": "94868774ac120a38", - "agents/gsd-ui-checker.md": "505ca13a25cb67df", - "agents/gsd-ui-researcher.md": "4596a60f979dc769", - "agents/gsd-user-profiler.md": "a5cd095688d6de25", - "agents/gsd-verifier.md": "7327f257ade6d00f", - "commands/gsd-add-tests.md": "993d9c16477b17bf", - "commands/gsd-ai-integration-phase.md": "bd8407401cc52092", - "commands/gsd-audit-fix.md": "0ce7dff55a550d96", - "commands/gsd-audit-milestone.md": "506d3d20f55d80ce", - "commands/gsd-audit-uat.md": "4aefa85d28437405", - "commands/gsd-autonomous.md": "ef5a492c9085b983", - "commands/gsd-capture.md": "61647c8baefcb116", - "commands/gsd-cleanup.md": "88e8c3d4b471a036", - "commands/gsd-code-review.md": "98a9c59ff3edb4ab", - "commands/gsd-complete-milestone.md": "7639dce1b9e755e4", - "commands/gsd-config.md": "b36340912b70c073", - "commands/gsd-debug.md": "e622240e54587e1b", - "commands/gsd-discuss-phase.md": "9eebccc3820ad37a", - "commands/gsd-docs-update.md": "a11bedfbfd0a56db", - "commands/gsd-eval-review.md": "dc602abbb7525761", - "commands/gsd-execute-phase.md": "f47b83e2110f7be8", - "commands/gsd-explore.md": "dbef734b08f7438a", - "commands/gsd-extract-learnings.md": "f3059f4e6e6b7b19", - "commands/gsd-fast.md": "be784c9f67ae3eab", - "commands/gsd-forensics.md": "36ec35a6a6f2cfd7", - "commands/gsd-graphify.md": "1b88285a888f69dd", - "commands/gsd-health.md": "f28dabf45c4ddd77", - "commands/gsd-help.md": "e48f4e1cf9ff3ad3", - "commands/gsd-import.md": "849c4a4bdfc05ebf", - "commands/gsd-inbox.md": "61eebdd7397efe7f", - "commands/gsd-ingest-docs.md": "10b1c8fee7ed7c1f", - "commands/gsd-manager.md": "0c5541f3db284fe3", - "commands/gsd-map-codebase.md": "9c32f13ade27d418", + "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": "a7fdafaaa3a665af", - "commands/gsd-mvp-phase.md": "fe9bf9df99007be1", - "commands/gsd-new-milestone.md": "d2f21e6e3d53b2e9", - "commands/gsd-new-project.md": "2129c259fa653b19", - "commands/gsd-next.md": "02d0abbd2f55a0a2", + "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": "2d98ea48454293b5", - "commands/gsd-pause-work.md": "bdfc8f46a674061a", - "commands/gsd-phase.md": "b211ba5969b4d69b", - "commands/gsd-plan-phase.md": "8113654accd5bee9", - "commands/gsd-plan-review-convergence.md": "3d496dbbba45190c", - "commands/gsd-pr-branch.md": "2346583e447a46d6", - "commands/gsd-profile-user.md": "8cc1a47fe685b60e", - "commands/gsd-progress.md": "0eddf50b039adc8e", - "commands/gsd-quick.md": "116941d841ecfb07", - "commands/gsd-resume-work.md": "330cd86844bd768b", + "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": "65b5225d8405f290", - "commands/gsd-secure-phase.md": "1ab3de7d982a7b9a", - "commands/gsd-settings.md": "13a4293f7fa58276", - "commands/gsd-ship.md": "551dc7f66e14a3de", - "commands/gsd-sketch.md": "0d01a055aa4b00ee", - "commands/gsd-spec-phase.md": "b3f6c26deba9172f", - "commands/gsd-spike.md": "7c102d63239f6678", - "commands/gsd-stats.md": "dc7c484c0fc2c177", - "commands/gsd-surface.md": "81c0c0d634ae0375", - "commands/gsd-thread.md": "4d9ac2986e090f5f", - "commands/gsd-ui-phase.md": "19ab8467a3f737a6", - "commands/gsd-ui-review.md": "5e0e6a377719a125", - "commands/gsd-ultraplan-phase.md": "29426afa8f6bcef3", - "commands/gsd-undo.md": "3a8299018ada31f6", - "commands/gsd-update.md": "22f2060ab482d569", - "commands/gsd-validate-phase.md": "079a0013bd14c1bd", - "commands/gsd-verify-work.md": "a7b07cc4547e7099", - "commands/gsd-workspace.md": "24b4c632d9773c11", + "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", @@ -123,11 +123,11 @@ "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": "08e3c3aa1f2ac11e", + "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": "338acaf52853668c", + "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", @@ -146,7 +146,7 @@ "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": "92e6574b30f5b765", + "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", @@ -158,12 +158,12 @@ "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": "46173caac1d469d4", - "gsd-core/references/honest-verifier.md": "6e4b8293f644b3f1", + "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": "a2fa775cac3470fa", + "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", @@ -171,11 +171,11 @@ "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": "563d1a434304d5cc", + "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": "0126c6156a9d32a8", - "gsd-core/references/planner-mvp-mode.md": "f7fc3894770a73f8", + "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", @@ -183,7 +183,7 @@ "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": "7612bb55f00359bb", + "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", @@ -198,7 +198,7 @@ "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": "d22e89615c846b72", + "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", @@ -213,7 +213,7 @@ "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": "57c67c5fd375fcb3", + "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", @@ -231,7 +231,7 @@ "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": "b106ec2b588d2b51", + "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", @@ -243,7 +243,7 @@ "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": "d07f52e21f15f2ab", + "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", @@ -265,121 +265,121 @@ "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": "450f70cb4ba4bf78", - "gsd-core/workflows/add-phase.md": "500274ecffbc2b12", - "gsd-core/workflows/add-tests.md": "575110e32af4eac3", - "gsd-core/workflows/add-todo.md": "4f68274dbf475704", - "gsd-core/workflows/ai-integration-phase.md": "1be55b37af12a639", + "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": "bab7ebaee574e486", - "gsd-core/workflows/audit-milestone.md": "3b5667e761f97774", - "gsd-core/workflows/audit-uat.md": "470c4637eb53b4a3", - "gsd-core/workflows/autonomous.md": "275e66d559d76792", - "gsd-core/workflows/check-todos.md": "dd1504bb42b17021", - "gsd-core/workflows/cleanup.md": "e06b655e5e901031", - "gsd-core/workflows/code-review-fix.md": "c6d26d40e143acca", - "gsd-core/workflows/code-review.md": "1bb99932450e9ced", - "gsd-core/workflows/complete-milestone.md": "7f0346ab9b6ab2b4", - "gsd-core/workflows/debug.md": "722e00ef94fdb5c2", - "gsd-core/workflows/diagnose-issues.md": "17fa373be09a0b82", - "gsd-core/workflows/discovery-phase.md": "b9f80db0c22d723a", - "gsd-core/workflows/discuss-phase-assumptions.md": "e3693f4e1746bc13", + "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": "6f5c0280e386a12e", - "gsd-core/workflows/discuss-phase/modes/advisor.md": "93b4ff65a03a5487", + "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": "fe5daf7c8617e26b", + "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": "2d1f9f2396f5d0f4", + "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": "7c10e13c60b73af0", + "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": "e55fe97bac5d691f", - "gsd-core/workflows/docs-update.md": "fff626c2033d34d2", - "gsd-core/workflows/edit-phase.md": "67efb8a73213ea5a", - "gsd-core/workflows/eval-review.md": "db4b8795864f104e", - "gsd-core/workflows/execute-phase.md": "4ef4e55d34c9c605", - "gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md": "839b0006c551f9fb", + "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": "cfe19fe951583f3f", - "gsd-core/workflows/execute-phase/steps/regression-gate.md": "e40c5eb516a9e052", + "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": "5d66d59ce03f88ba", - "gsd-core/workflows/explore.md": "eb4a623b5732303f", - "gsd-core/workflows/extract-learnings.md": "d1e0a14a9df00195", + "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": "f66bb7b50dcc918a", - "gsd-core/workflows/graduation.md": "35ebdec95d86d45b", - "gsd-core/workflows/health.md": "bcc07e9610635d49", + "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": "579577e8095f8fef", + "gsd-core/workflows/help/modes/full.md": "ce40e843f528e327", "gsd-core/workflows/help/modes/topic.md": "6e42db16f1568be9", - "gsd-core/workflows/import.md": "88e6ef80b6d1db63", + "gsd-core/workflows/import.md": "cc21f3da36403ed3", "gsd-core/workflows/inbox.md": "91aac6360e1a8672", - "gsd-core/workflows/ingest-docs.md": "59224a43a74f0f88", - "gsd-core/workflows/insert-phase.md": "653460de68191dd7", + "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": "c7026201e80f21ff", - "gsd-core/workflows/list-workspaces.md": "2c2f5e466cf97136", - "gsd-core/workflows/manager.md": "f0835592a5f4ca39", - "gsd-core/workflows/map-codebase.md": "a2c4e72aac41f477", - "gsd-core/workflows/milestone-summary.md": "4ad8a311ebf766a8", - "gsd-core/workflows/mvp-phase.md": "e459fa1351dbb663", - "gsd-core/workflows/new-milestone.md": "417bbc34f965997f", - "gsd-core/workflows/new-project.md": "4c7c381c8bc0bd98", - "gsd-core/workflows/new-workspace.md": "f907fed6831f3f32", - "gsd-core/workflows/next.md": "f2f2210290a02171", + "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": "def2655cebe5c001", - "gsd-core/workflows/onboard.md": "9ae63d270f8fd7c0", - "gsd-core/workflows/pause-work.md": "9655ec327c5aa307", - "gsd-core/workflows/plan-milestone-gaps.md": "3be3a097114fdde6", - "gsd-core/workflows/plan-phase.md": "b1643a49267f3f01", + "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": "af3029d3c899f4f2", + "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": "b09378bcc5571ca4", - "gsd-core/workflows/plant-seed.md": "4f51d8d267fd56a5", - "gsd-core/workflows/pr-branch.md": "8f6db372a275f7b1", - "gsd-core/workflows/profile-user.md": "aeb21d7386c4a41e", - "gsd-core/workflows/progress.md": "b482c9befd4af94a", - "gsd-core/workflows/quick.md": "668e7e8370ba9992", - "gsd-core/workflows/reapply-patches.md": "6466079b57163be0", - "gsd-core/workflows/remove-phase.md": "7cc144da60d95241", - "gsd-core/workflows/remove-workspace.md": "53e9575a8c4fb411", - "gsd-core/workflows/resume-project.md": "20de3d2834ee581a", - "gsd-core/workflows/review.md": "5ea70bc48614f68f", - "gsd-core/workflows/scan.md": "edba4c0bac5727a6", - "gsd-core/workflows/secure-phase.md": "265b3244a74a9bdd", + "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": "d148d5233169883f", - "gsd-core/workflows/settings-integrations.md": "b03bdf37b4f5310b", - "gsd-core/workflows/settings.md": "c24424577729f318", - "gsd-core/workflows/ship.md": "4d0fb3bb33f286f8", - "gsd-core/workflows/sketch-wrap-up.md": "47cb472ba0445c10", - "gsd-core/workflows/sketch.md": "d4de5174c8242425", - "gsd-core/workflows/smart-entry.md": "3fe82b4a6e67d0cf", - "gsd-core/workflows/spec-phase.md": "b35b7733e8ece267", - "gsd-core/workflows/spike-wrap-up.md": "496a6afc08c42a8d", - "gsd-core/workflows/spike.md": "3b2a826377c451ff", - "gsd-core/workflows/stats.md": "75618011901cebce", - "gsd-core/workflows/sync-skills.md": "452c01c7a05b238c", - "gsd-core/workflows/thread.md": "9d9cf44f7f6d788e", - "gsd-core/workflows/transition.md": "18f610382e9641f6", - "gsd-core/workflows/ui-phase.md": "4c8adbabd9f71a78", - "gsd-core/workflows/ui-review.md": "108e094e6aa085f6", - "gsd-core/workflows/ultraplan-phase.md": "e759430ce77b45db", - "gsd-core/workflows/undo.md": "e1eeffa679786c8a", - "gsd-core/workflows/update.md": "c276a4f628f4b948", - "gsd-core/workflows/validate-phase.md": "96c5ee354b72af56", - "gsd-core/workflows/verify-phase.md": "5ee79da8a9e787b3", - "gsd-core/workflows/verify-work.md": "25c8dd4c6ca6b5ae", + "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", diff --git a/tests/golden-install-parity.test.cjs b/tests/golden-install-parity.test.cjs index c7f9f7817..1742ab5ce 100644 --- a/tests/golden-install-parity.test.cjs +++ b/tests/golden-install-parity.test.cjs @@ -113,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 ``. 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('/'); @@ -125,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('').split(PKG_VERSION).join(''); + const normalized = content.toString('utf8') + .split(realRoot).join('') + .split(root).join('') + .split(PKG_VERSION).join(''); const hash = crypto.createHash('sha256').update(normalized).digest('hex').slice(0, 16); unsorted[rel] = hash; } From 102ffa0f9ee6a0d018ca48e3c846fcbf96c58fdc Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 8 Jul 2026 15:19:12 -0400 Subject: [PATCH 4/5] fix(#2086): #338 fail-safe floor for reference-host behaviors on registry-load failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reviewer (PR #2106, elevated): if capability-registry.cjs fails to load, _hostBehaviors('claude') returned {} — silently routing a claude LOCAL install to the repo-shared settings.json instead of the gitignored settings.local.json (#338), skipping mergeClaudePermissions + the .gsd-source marker. The migration is what introduced that registry dependency (pre-PR the path had none). Add FALLBACK_HOST_BEHAVIORS (keyed by runtime id — a data lookup, not a runtime==='claude' branch) mirroring the reference host's #338-privacy-critical keys (settingsFileByScope, permissionsSchema, sourceMarkerFile), consulted only when the registry (or the descriptor) is unavailable. Behavior degrades CLOSED, never open; the live descriptor stays the source of truth. Normal (registry-present) output is unchanged (golden parity preserved). Pinned by tests via a registry-injected _resolveHostBehaviors helper. Co-Authored-By: Claude Opus 4.8 --- bin/install.js | 39 ++++++++++++++++++++-- tests/claude-imperative-reference.test.cjs | 38 +++++++++++++++++++++ 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/bin/install.js b/bin/install.js index 92acdb147..a214124cf 100755 --- a/bin/install.js +++ b/bin/install.js @@ -315,16 +315,46 @@ try { _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 +// `runtime === 'claude'` 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//capability.json -> runtime.hostBehaviors) instead of * scattered `runtime === ''` string checks (ADR-1239 / #2086). Returns {} * for runtimes that declare none, so every behavior branch degrades to the - * generic path by default. + * 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) { - const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime]; - return (cap && cap.runtime && cap.runtime.hostBehaviors) || {}; + return _resolveHostBehaviors(runtime, _capabilityRegistry); } /** @@ -11099,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, diff --git a/tests/claude-imperative-reference.test.cjs b/tests/claude-imperative-reference.test.cjs index 94ef54df7..8b67c3dfd 100644 --- a/tests/claude-imperative-reference.test.cjs +++ b/tests/claude-imperative-reference.test.cjs @@ -36,6 +36,11 @@ const CLAUDE_CAP = JSON.parse( ); 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', () => { @@ -136,3 +141,36 @@ test('bin/install.js contains no `runtime === "claude"` / `runtime !== "claude"` `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), {}); +}); From eeec6b512e8d02adf338eb64347b11d20596bf75 Mon Sep 17 00:00:00 2001 From: Tom Boucher Date: Wed, 8 Jul 2026 15:26:19 -0400 Subject: [PATCH 5/5] fix(#2086): AC2 source-guard must ignore comments/backtick prose, not just code The #338 fail-safe commit added a comment containing the literal `runtime === 'claude'` (explaining what the data lookup is NOT), which the AC2 source-grep test matched as a false positive (the test read the whole file, prose included). Strip block/line comments + backtick spans before matching so the guard flags only LIVE code, and reword the comment. CRLF-safe line-comment strip. Co-Authored-By: Claude Opus 4.8 --- bin/install.js | 2 +- tests/claude-imperative-reference.test.cjs | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/bin/install.js b/bin/install.js index a214124cf..0a9f2dd10 100755 --- a/bin/install.js +++ b/bin/install.js @@ -321,7 +321,7 @@ try { // {} 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 -// `runtime === 'claude'` branch) so behavior degrades CLOSED (safe), never open. +// 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({ diff --git a/tests/claude-imperative-reference.test.cjs b/tests/claude-imperative-reference.test.cjs index 8b67c3dfd..3cb3245ef 100644 --- a/tests/claude-imperative-reference.test.cjs +++ b/tests/claude-imperative-reference.test.cjs @@ -134,7 +134,14 @@ test('claude descriptor declares runtime.hostBehaviors (the folded-in host behav 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'); - const offenders = src.match(/runtime\s*[!=]==\s*'claude'/g) || []; + // 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, [],