feat(architecture): [EoS/claude] drive claude through the imperative adapter + descriptor-driven hostBehaviors (#2086)

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 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-07-08 13:15:43 -04:00
parent 3852c318c4
commit fdd5e401eb
8 changed files with 799 additions and 31 deletions

View File

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

View File

@@ -43,6 +43,7 @@ const {
readBaseRefFromSettings,
} = require('../gsd-core/bin/lib/worktree-base-ref.cjs');
const { resolveInstallPlan } = require('../gsd-core/bin/lib/runtime-config-adapter-registry.cjs');
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
// Canonical set of hook files shipped to users. Imported here so writeManifest()
// records exactly the same set that build-hooks.js copies to hooks/dist/, making
@@ -126,6 +127,9 @@ function isCodexHooksFeatureKey(key) {
//
// Merge policy: additive, non-destructive \u2014 existing user entries are preserved;
// GSD entries are appended only when not already present (idempotent).
// The reference/default runtime (ADR-1239 reference host). Single-sourced here
// instead of scattered literal 'claude' defaults/rosters (#2086).
const DEFAULT_RUNTIME = 'claude';
const GSD_CLAUDE_ALLOW_PERMISSIONS = Object.freeze([
'Bash(npx gsd-core *)',
'Read(.planning/*)',
@@ -310,6 +314,34 @@ try {
} catch (_) {
_capabilityRegistry = undefined;
}
/**
* Host-specific install behaviors, declared on the runtime descriptor
* (capabilities/<runtime>/capability.json -> runtime.hostBehaviors) instead of
* scattered `runtime === '<id>'` string checks (ADR-1239 / #2086). Returns {}
* for runtimes that declare none, so every behavior branch degrades to the
* generic path by default.
*/
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);

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,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(', ')}`,
);
});

View File

@@ -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"
}

View File

@@ -80,7 +80,9 @@ const PKG_VERSION = require('../package.json').version;
// (install-minimal-hooks, sh-hook-paths, codex-config, etc.). Matched by basename.
// settings.json = Claude/Antigravity/Augment/etc. hook surface; hooks.json =
// Codex/Cursor hook surface — both embed the platform-varying node-runner command.
const HOOK_CONFIG_FILES = new Set(['settings.json', 'hooks.json']);
// settings.local.json = Claude LOCAL hook surface (#338): same platform-varying
// node-runner command as settings.json, so excluded for the same reason (#2086).
const HOOK_CONFIG_FILES = new Set(['settings.json', 'settings.local.json', 'hooks.json']);
// Path prefixes excluded from the parity manifest. `gsd-core/bin/lib/` holds the
// tsc-built runtime artifacts (compiled from src/*.cts) that the install COPIES
@@ -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'));
}
});