fix(#3203): stop npm-global installs validating bundled agents against themselves (#3229)

* fix(#3203): stop npm-global installs validating bundled agents against themselves

getAgentsDir's claude branch derived the agents directory from __dirname,
which is correct for repo runs and runtime-config-dir installs (where
<root>/../agents IS the user's agents dir) but on an npm-global install
resolves to the package's own bundled agents/, so checkAgentsInstalled
validated the package against itself and agents_installed could never be
false. The new-project and new-milestone halt/warn gates were silently
dead for npm-global users.

Keep the install-relative path for the shapes where it is correct, but
when it lies inside a node_modules tree — the provably self-validating
case — resolve getGlobalConfigDir('claude')/agents like every other
runtime, honouring CLAUDE_CONFIG_DIR. GSD_AGENTS_DIR stays priority 1.
Repair the doc comment that asserted the __dirname form was correct for
both install shapes.

Regression test mirrors the published npm-global layout (package under
node_modules with a complete bundled agents/) and pins the resolved
directory plus the issue's negative control (one agent missing from the
config dir → agents_installed:false). Verified red against pre-fix code,
green post-fix; the repo-layout W010 health test stays green.

* chore(#3203): set changeset fragment pr to 3229

* docs(#3203): describe the node_modules guard as lexical, in CONTEXT.md and at the call site

The Agent Install Check Module glossary entry asserted that Claude resolves the
agents directory `__dirname`-relative unconditionally. That is the premise this
PR falsified: on an npm-global install the install-relative path resolves to the
package's own bundled `agents/`, so the check validated the package against
itself and `agents_installed` could never be false.

The inline doc comment above `getAgentsDir` was repaired with the fix; this
external predicate was left behind and has been false since. CONTRIBUTING.md's
`Fixed`-fragment docs exemption names this case explicitly — "Edit the docs
anyway if a fix corrects something the docs got wrong."

Both surfaces now describe the guard as what it is: an exact, case-sensitive
path-segment test that TARGETS those layouts rather than detecting them, so
neither claims more certainty than the predicate has. The call-site comment
carried the same conflation the glossary did. A path merely carrying a directory
of that name resolves the same way — the edge already disclosed on this PR — and
a non-empty GSD_AGENTS_DIR overrides it.

Comment-only in `src/`; no behaviour change.

`CONFIG.LOCATION.SEAM.two-families` needs no change: `GSD_AGENTS_DIR ->
getAgentsDir priority 1` is still accurate.

---------

Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
0xdhx
2026-08-12 19:45:00 -05:00
committed by GitHub
parent 3dff700aaa
commit c516c39b33
4 changed files with 88 additions and 10 deletions

View File

@@ -81,9 +81,17 @@ function truncatePostureValue(value: string): string {
*
* Priority:
* 1. GSD_AGENTS_DIR env var (explicit override, any runtime)
* 2. For claude runtime: __dirname-relative path (agents/ sibling of gsd-core/)
* This is correct for both repo runs and real installs (the runtime config dir's
* agents/ folder) because gsd-tools.cjs lives inside gsd-core/bin/ in both cases.
* 2. For claude runtime: __dirname-relative path (agents/ sibling of
* gsd-core/) — correct for repo runs and runtime-config-dir installs,
* where the sibling agents/ IS the user's agents dir — UNLESS that path
* carries an exact node_modules segment. gsd-tools.cjs lives inside
* gsd-core/bin/ in every install shape, but on an npm-global install
* gsd-core/ sits inside the package (not the runtime config dir) and the
* package ships its own agents/, so the install-relative path resolves
* to the bundled copy and the check validates the package against
* itself — agents_installed can never be false. In that case resolve
* getGlobalConfigDir('claude')/agents (honours CLAUDE_CONFIG_DIR) like
* every other runtime (#3203).
* 3. For non-claude runtimes with a manifest-backed project-local install:
* <projectRoot>/<localConfigDir>/agents (or <projectRoot>/agents when
* the runtime's local install targets the project root). Requiring the
@@ -100,7 +108,16 @@ function getAgentsDir(runtime?: string, projectRoot?: string): string {
}
const resolved = runtime ?? (process.env['GSD_RUNTIME'] || 'claude');
if (resolved === 'claude') {
return path.join(__dirname, '..', '..', '..', 'agents');
const installRelative = path.join(__dirname, '..', '..', '..', 'agents');
// #3203: a lexical guard, not an install-shape test. It targets the
// layouts where the sibling agents/ is the package's own bundled copy and
// the check would otherwise validate the package against itself; a path
// merely carrying a directory of that name resolves the same way, and a
// non-empty GSD_AGENTS_DIR overrides both.
if (installRelative.split(path.sep).includes('node_modules')) {
return path.join(getGlobalConfigDir('claude'), 'agents');
}
return installRelative;
}
if (projectRoot) {
// eslint-disable-next-line @typescript-eslint/no-require-imports