feat(#2873): detect cross-scope shadowing and reach the local spec tree

4a - the detection floor. A shadowed install now reports which triggers are
shadowed and which scope wins, at install time and through a new W028
/gsd-health diagnostic. Exit codes are untouched: a shadowed install is a
warning, not a failure. Only triggers whose stem exists at BOTH scopes are
reported, so a global full profile beside a local core profile no longer
names local artifacts the user does not have.

4b - spec-root reachability, claude runtime and global scope only. The
winning global skill stops carrying a static workflow @-include and instead
resolves its spec at runtime: prefer the project-local copy, fall back to
the global one, stop if neither exists. Every other @-include stays static,
and the local emission is byte-identical. It runs after the staged-skills
rewrite pass, whose claude branch would otherwise mangle the literal tilde
path into an undocumented $HOME form.

Also fixed inline: readInstallManifest classified a top-level JSON array as
an installed v1 manifest, because typeof [] is object.

Refs #2873
This commit is contained in:
sim
2026-08-14 22:47:17 -04:00
parent 52f4ea17cc
commit 2641e6cb67
14 changed files with 1765 additions and 176 deletions

View File

@@ -60,6 +60,10 @@ const { composeWorkflow } = require('../gsd-core/bin/lib/workflow-fragments.cjs'
const { shouldCompose } = require('../gsd-core/bin/lib/mcp-catalog.cjs');
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
const { escapeRegex: escapeRegExp } = require('../gsd-core/bin/lib/pattern.cjs');
// #2873: cross-scope shadow detection — reports (never fails) when a
// GSD-owned scope shadows another on this machine (design doc:
// .gsd/phase/feat-2873-cross-scope-shadowing/40-design.md).
const { buildShadowReport, renderShadowReport } = require('../gsd-core/bin/lib/install-shadow-report.cjs');
// #2544: the CommonJS marker's single source of truth. classifyMarker() backs
// BOTH ensureCommonJsMarker() (install) and removeCommonJsMarker() (uninstall),
// so the write side can no longer clobber a package.json the remove side would
@@ -11664,6 +11668,29 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
// Report any backed-up local patches
reportLocalPatches(targetDir, runtime);
// #2873: cross-scope shadow report. Fires ONCE per install (this is the
// only writeManifest call site that gets it — the other four sites are
// sub-writes within a single install, not separate installs). A shadowed
// install is a warning, never a failure (ADR-2866 Consequences), so this
// never touches `failures` or `process.exit`, and the whole block is
// wrapped in a try/catch that swallows everything: a report failure must
// never fail an otherwise-successful install (design row C5). No options
// are injected into buildShadowReport — this is the production call shape,
// resolving the real machine via os.homedir()/process.cwd() defaults
// inside the resolver.
try {
const shadowReport = buildShadowReport(runtime);
const shadowLines = renderShadowReport(shadowReport);
if (shadowLines.length > 0) {
console.warn(`\n ${yellow}⚠${reset} ${shadowLines[0]}`);
for (const line of shadowLines.slice(1)) {
console.warn(` ${dim}${line}${reset}`);
}
}
} catch (_shadowReportErr) {
// Never fail an install over a reporting concern — see comment above.
}
// Verify no leaked .claude paths in non-Claude runtimes (manifest-scoped)
if (!_hostBehaviors(runtime).ownsClaudePaths) {
const leakedPaths = [];