* fix(#3544): restore tilde form for at-refs in the global spec tree A global claude install emitted @$HOME/.claude/gsd-core/references/*.md in its workflows and references. $HOME does not expand in a Claude Code @-import - only relative, absolute and ~ are documented, and a controlled /context test confirmed a $HOME import loads nothing - so 54 includes across 22 files silently resolved to nothing on a live install. This is a divergence, not a new bug. #3133 already applies exactly this correction to skill and command bodies through _applyRuntimeRewrites's claude case; copyWithPathReplacement, the spec-tree emit path, never had it. Both now call one exported helper, so the two surfaces cannot drift apart again. Deliberately narrower than changing computePathPrefix's return value: shipped markdown also carries double-quoted "$HOME/.claude/..." shell invocations, and ~ does not expand inside double quotes, so rewriting the prefix wholesale would regress #1284. Only @-prefixed references move. Refs #3544 * fix(#3544): derive the tilde restore from the resolved prefix Three review findings, one batch. The restore was hardcoded to the literal .claude directory, so a global install with --config-dir pointing anywhere else silently no-opped and reproduced the very defect this fixes. It now derives the tilde form from the resolved prefix, which also closes the same latent gap in #3133's original path since both call sites share the helper. The @-anchor is quote-aware, so a double-quoted shell path is never rewritten into a form the shell does not expand. Deliberately a lookbehind rather than a line-start anchor: @-references are documented to work mid-line, and anchoring would have traded a theoretical bug for a real one. Found while testing the above: the bare-form rewrites re-matched their own output whenever a config dir name extends .claude, emitting .claude-work-work. Guarded with the same negative-lookahead convention this file already uses to preserve .claude-plugin. The tests prove the emitted form, never that the host resolves it - no CI test can - and both the helper and the suite now say so, because an undocumented verification boundary is how this defect stayed green for its whole life. Refs #3544 * test(#3544): acknowledge the tilde-restore emitted drift The converter change moves 94 emitted paths that no source-file diff can explain, which is exactly the case the per-PR ack fragment exists for. Verified before acknowledging rather than after: both trees were built from real installs and every one of the 211 changed lines across all 94 paths is @$HOME becoming @~, with nothing outside that single kind. Nine spent entries were pruned from the #3151 and #2658 fragments. Those paths moved again here, and two ack sources naming one path is a hard duplicate error rather than last-wins, so the inert entries had to go before this one could land. Both fragments retain their remaining entries. Refs #3544 * chore(#3544): backfill changeset pr number --------- Co-authored-by: sim <sim@local>
This commit is contained in:
@@ -2785,6 +2785,81 @@ function _stampNonClaudeRuntimeDefaults(content: string, runtime: string): strin
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* #3544 (extending #3133's fix): restore `@$HOME<suffix>` `@`-file-reference
|
||||
* lines back to their tilde equivalent (`@~<suffix>`) in Claude-emitted
|
||||
* content whose pathPrefix is the `$HOME` form. This is a NARROW,
|
||||
* context-sensitive correction layered on top of the blanket `~/.claude/` /
|
||||
* `$HOME/.claude/` -> pathPrefix substitution every Claude emit path
|
||||
* applies: that blanket substitution MUST keep emitting `$HOME` for global
|
||||
* installs — shell commands embedded in workflow/command bodies (e.g.
|
||||
* `node "$HOME/.claude/gsd-core/bin/gsd-tools.cjs"`) need it, since `~` does
|
||||
* not expand inside double-quoted shell strings (#1284). But Claude Code's
|
||||
* own `@`-import resolver does the opposite: it documents `~` expansion and
|
||||
* does NOT expand `$HOME`. That is not merely undocumented — a controlled
|
||||
* `/context` measurement showed an `@$HOME/…` import loading nothing (see
|
||||
* .gsd/bug/fix-3544-home-expansion-spec-tree/10-diagnosis.md's ADDENDUM). No
|
||||
* automated test can verify *resolution* inside a live Claude Code session
|
||||
* (nothing in CI can spawn one and read `/context`); every test here — unit
|
||||
* and spawned-installer alike — verifies only the emitted STRING takes the
|
||||
* `~` form Claude Code documents as expanding. A single pathPrefix string
|
||||
* cannot satisfy both the shell and the `@`-import consumer, so this runs as
|
||||
* a second, `@`-anchored pass AFTER the blanket substitution.
|
||||
*
|
||||
* #3133 first applied this restore inline in `_applyRuntimeRewrites`'s
|
||||
* `case 'claude'` below (the skill/command staging pipeline). #3544 found
|
||||
* the identical defect in bin/install.js's `copyWithPathReplacement` — the
|
||||
* `gsd-core/` spec-tree emit path, which never had the restore step, so
|
||||
* every `@~/.claude/gsd-core/…` include in a global install's workflows/
|
||||
* references tree silently resolved to nothing (54 includes across 22 files
|
||||
* on a live install, per the diagnosis). Both call sites now share this one
|
||||
* implementation instead of drifting independently (DEFECT.GENERATIVE-FIX).
|
||||
*
|
||||
* No-op unless `pathPrefix` is the `$HOME` form — local installs already
|
||||
* bake an absolute, `@`-resolvable pathPrefix and are unaffected, as are
|
||||
* every non-Claude runtime (never called for them).
|
||||
*
|
||||
* #3544 review (2nd pass): the first cut of this function hardcoded the
|
||||
* literal `.claude/` segment, so it silently no-opped for any global install
|
||||
* under a non-default `--config-dir` (e.g. `~/.claude-work`) — reproducing
|
||||
* the exact defect #3544 fixes, just one directory name later. This ALSO
|
||||
* corrects the same latent gap in #3133's original path, since both call
|
||||
* sites share this one implementation. Fixed by deriving the rewrite from
|
||||
* `pathPrefix` itself rather than a hardcoded directory name: the tilde
|
||||
* equivalent of any `$HOME`-form prefix is `'~' + pathPrefix.slice(5)`
|
||||
* (`'$HOME'.length === 5`), so the transform generalizes to any config-dir
|
||||
* name with no runtime-specific literal.
|
||||
*
|
||||
* #3544 review (2nd pass), quote-awareness: the anchor is a negative
|
||||
* lookbehind for a preceding quote character, NOT a line-start anchor —
|
||||
* Claude Code documents `@`-references as valid "anywhere in your
|
||||
* CLAUDE.md" (e.g. `See @README for project overview`), so anchoring to
|
||||
* line-start would miss a legitimate mid-line reference. The lookbehind
|
||||
* instead guards the one demonstrated false-positive: a quoted shell string
|
||||
* like `echo "@$HOME/.claude/x"`, where rewriting `$HOME` to `~` inside
|
||||
* double quotes reintroduces the #1284 failure mode (`~` does not expand in
|
||||
* double-quoted shell). Deliberately NOT fenced-code-block aware (unlike
|
||||
* `resolveSpecRootReference`'s `scanFencedBlocks` use above): this pass
|
||||
* targets genuine `@`-import lines and inline shell references across the
|
||||
* whole emitted corpus, and today there are zero occurrences anywhere in the
|
||||
* tree of an `@$HOME<suffix>` sequence inside a fenced code block (the
|
||||
* quote-guard already closes the one reachable false-positive class).
|
||||
* Layering `scanFencedBlocks` on top would roughly double this function's
|
||||
* size to guard an undemonstrated case — the opposite of the brief's
|
||||
* "simpler, not more complex" direction. If a fenced example ever needs this
|
||||
* literal sequence, add fence-awareness then, with a regression test proving
|
||||
* the fence is real.
|
||||
*
|
||||
* @private — exported as `_restoreClaudeGlobalAtRefTilde` for tests and for
|
||||
* bin/install.js's `copyWithPathReplacement`.
|
||||
*/
|
||||
function restoreClaudeGlobalAtRefTilde(content, pathPrefix) {
|
||||
if (typeof pathPrefix !== 'string' || !pathPrefix.startsWith('$HOME')) return content;
|
||||
const tildeEquivalent = '~' + pathPrefix.slice('$HOME'.length);
|
||||
const atRefRe = new RegExp(`(?<!["'])@${escapeRegExp(pathPrefix)}`, 'g');
|
||||
return content.replace(atRefRe, `@${tildeEquivalent}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the per-runtime rewrite table to a single content string.
|
||||
* Relocated from bin/install.js `_applyRuntimeRewrites`.
|
||||
@@ -2916,18 +2991,10 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
|
||||
content = content.replace(/~\/\.claude\//g, pathPrefix);
|
||||
content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
|
||||
content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
|
||||
// #3133: Claude Code expands `~` and absolute paths in @-file references
|
||||
// but NOT `$HOME`. computePathPrefix returns the $HOME/.claude/ form for
|
||||
// global installs under $HOME (correct for double-quoted shell commands —
|
||||
// `~` does not expand inside double quotes, causing MODULE_NOT_FOUND,
|
||||
// #1284), so the substitutions above rewrite @~/.claude/… → @$HOME/.claude/…
|
||||
// which silently resolves to nothing and leaves skills with an empty
|
||||
// execution_context. Restore the tilde form Claude expands (the form the
|
||||
// shipped tarball uses). Local installs use an absolute pathPrefix (already
|
||||
// @-resolvable) and are unaffected — the guard fires only for the $HOME form.
|
||||
if (pathPrefix.startsWith('$HOME')) {
|
||||
content = content.replace(/@\$HOME\/\.claude\//g, '@~/.claude/');
|
||||
}
|
||||
// #3133 / #3544: restore @-file-reference lines to the tilde form
|
||||
// Claude actually expands — see restoreClaudeGlobalAtRefTilde's doc
|
||||
// comment above for why this must be a separate, @-anchored pass.
|
||||
content = restoreClaudeGlobalAtRefTilde(content, pathPrefix);
|
||||
content = processAttribution(content, attribution);
|
||||
break;
|
||||
|
||||
@@ -3384,6 +3451,7 @@ export = {
|
||||
applyAgentPathRewrites,
|
||||
normalizeAgentBodyForRuntime,
|
||||
_computePathPrefix: computePathPrefix,
|
||||
_restoreClaudeGlobalAtRefTilde: restoreClaudeGlobalAtRefTilde,
|
||||
_applyRuntimeRewrites,
|
||||
_stampNonClaudeRuntimeDefaults,
|
||||
// #2652: registry-resolved dispatch isolation, mirroring routeDispatchIsolation
|
||||
|
||||
Reference in New Issue
Block a user