@opengsd/gsd-core 1.20.0 → 1.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +265 -161
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/commands/gsd/new-milestone.md +2 -2
  11. package/commands/gsd/new-project.md +2 -2
  12. package/gsd-core/bin/gsd-tools.cjs +554 -823
  13. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  14. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  15. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  16. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  17. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  18. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  19. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  20. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  21. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  22. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  23. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  24. package/gsd-core/bin/lib/commands.cjs +99 -2
  25. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  26. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  27. package/gsd-core/bin/lib/config.cjs +139 -101
  28. package/gsd-core/bin/lib/configuration.cjs +41 -2
  29. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  30. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  31. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  32. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  33. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  34. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  35. package/gsd-core/bin/lib/graphify.cjs +19 -2
  36. package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
  37. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  38. package/gsd-core/bin/lib/init.cjs +20 -18
  39. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  40. package/gsd-core/bin/lib/install-engine.cjs +20 -28
  41. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  42. package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
  43. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +4 -2
  45. package/gsd-core/bin/lib/io.cjs +6 -0
  46. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  47. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  48. package/gsd-core/bin/lib/milestone.cjs +8 -6
  49. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  50. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  51. package/gsd-core/bin/lib/phase-id.cjs +26 -9
  52. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  53. package/gsd-core/bin/lib/phase.cjs +170 -45
  54. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  55. package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
  56. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  57. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  58. package/gsd-core/bin/lib/project-context.cjs +439 -0
  59. package/gsd-core/bin/lib/project-root.cjs +9 -230
  60. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  61. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  62. package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
  63. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  64. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  65. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
  66. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  67. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
  68. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  69. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  70. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  71. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  72. package/gsd-core/bin/lib/state-transition.cjs +103 -46
  73. package/gsd-core/bin/lib/state.cjs +448 -63
  74. package/gsd-core/bin/lib/surface.cjs +4 -7
  75. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  76. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  77. package/gsd-core/bin/lib/verify.cjs +49 -6
  78. package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
  79. package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
  80. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  81. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  82. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  83. package/gsd-core/references/checkpoints.md +1 -1
  84. package/gsd-core/references/planner-guidance.md +11 -0
  85. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  86. package/gsd-core/references/planner-reversibility.md +2 -0
  87. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  88. package/gsd-core/workflows/autonomous.md +4 -4
  89. package/gsd-core/workflows/code-review-fix.md +5 -7
  90. package/gsd-core/workflows/execute-phase.md +21 -7
  91. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  92. package/gsd-core/workflows/plan-phase.md +18 -19
  93. package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
  94. package/hooks/dist/gsd-config-reload.js +2 -2
  95. package/hooks/dist/gsd-context-monitor.js +2 -1
  96. package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
  97. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  98. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  99. package/hooks/dist/gsd-statusline.js +3 -2
  100. package/hooks/dist/gsd-workflow-guard.js +2 -1
  101. package/hooks/dist/lib/config-read.js +49 -0
  102. package/hooks/dist/lib/git-cmd.js +3 -1
  103. package/hooks/dist/lib/git-head-tree.js +474 -0
  104. package/hooks/dist/lib/isolation-sentinel.js +168 -13
  105. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  106. package/hooks/gsd-agent-isolation-guard.js +4 -3
  107. package/hooks/gsd-config-reload.js +2 -2
  108. package/hooks/gsd-context-monitor.js +2 -1
  109. package/hooks/gsd-cursor-subagent-start.js +9 -6
  110. package/hooks/gsd-read-injection-scanner.js +2 -2
  111. package/hooks/gsd-secret-read-guard.js +141 -6
  112. package/hooks/gsd-statusline.js +3 -2
  113. package/hooks/gsd-workflow-guard.js +2 -1
  114. package/hooks/lib/config-read.js +49 -0
  115. package/hooks/lib/git-cmd.js +3 -1
  116. package/hooks/lib/git-head-tree.js +474 -0
  117. package/hooks/lib/isolation-sentinel.js +168 -13
  118. package/hooks/lib/secret-read-exemptions.js +161 -0
  119. package/package.json +7 -2
  120. package/pi/package-agents/gsd-executor.md +1 -1
  121. package/pi/package-agents/gsd-plan-checker.md +1 -1
  122. package/pi/package-agents/gsd-planner.md +3 -3
  123. package/pi/package-agents/gsd-verifier.md +2 -2
  124. package/pi/package-agents/manifest.json +4 -4
  125. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  126. package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
  127. package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
  128. package/scripts/changeset/cli.cjs +2 -2
  129. package/scripts/changeset/lint.cjs +28 -3
  130. package/scripts/changeset/serialize.cjs +12 -2
  131. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  132. package/scripts/ci-fix-next-rerun.cjs +149 -0
  133. package/scripts/ci-next-health.cjs +52 -9
  134. package/scripts/ci-test-scope.cjs +77 -71
  135. package/scripts/command-contract-helpers.cjs +56 -7
  136. package/scripts/docs-guard-registry.cjs +3 -0
  137. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  138. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  139. package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
  140. package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
  141. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  142. package/scripts/lint-removed-but-needed.cjs +562 -50
  143. package/scripts/merge-commit-pr.cjs +186 -0
  144. package/scripts/release-ancestry.cjs +86 -0
  145. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  146. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  147. package/scripts/release-pipeline-pr.cjs +92 -0
  148. package/scripts/require-issue-link-policy.cjs +18 -9
  149. package/scripts/sync-next-version.cjs +11 -2
  150. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  151. package/skills/gsd-autonomous/SKILL.md +1 -1
  152. package/skills/gsd-new-milestone/SKILL.md +1 -1
  153. package/skills/gsd-new-project/SKILL.md +1 -1
  154. package/vscode/package.json +1 -1
@@ -1733,7 +1733,83 @@ function convertSlashCommandsToTraeSkillMentions(content) {
1733
1733
  return `/gsd-${commandName}`;
1734
1734
  });
1735
1735
  }
1736
- function convertClaudeToTraeMarkdown(content) {
1736
+ /**
1737
+ * #5715: the texts of the install prefix a rewrite pass emitted, or `[]` when the prefix
1738
+ * must not be shielded. The prefix is the normalized (slash-less) form, which also covers
1739
+ * the trailing-slash form and the bare form; a `$HOME/...` prefix also yields its `~/...`
1740
+ * spelling, which the Claude tilde restore emits for `@` includes.
1741
+ *
1742
+ * A prefix that is nothing but the Claude config dir itself (`.claude`, `./.claude`, with
1743
+ * or without a trailing slash) is never shielded: it is indistinguishable from the
1744
+ * look-alike `.claude/` text the converters are meant to rewrite, so shielding it would
1745
+ * suppress every such conversion. Empty and `.` prefixes are likewise not shielded.
1746
+ */
1747
+ function shieldedPrefixForms(pathPrefix) {
1748
+ if (typeof pathPrefix !== 'string')
1749
+ return [];
1750
+ const normalized = pathPrefix.replace(/\/+$/, '');
1751
+ const bare = normalized.replace(/^(?:\.\/)+/, '');
1752
+ if (bare === '' || bare === '.' || bare === '.claude')
1753
+ return [];
1754
+ return normalized.startsWith('$HOME/') ? [normalized, '~' + normalized.slice('$HOME'.length)] : [normalized];
1755
+ }
1756
+ /** Candidate sentinel namespaces tried by shieldEmittedPrefix before it gives up. */
1757
+ const SHIELD_SENTINEL_LIMIT = 256;
1758
+ const SHIELD_SENTINEL_OPEN = '\uE000';
1759
+ const SHIELD_SENTINEL_CLOSE = '\uE001';
1760
+ /**
1761
+ * #5715: run `convert` over `content` with every exact occurrence of the install prefix
1762
+ * the rewrite pass already emitted shielded from it. The agents, the gsd-core spec tree,
1763
+ * the OpenCode-family and the command-body converters all run AFTER the prefix is
1764
+ * substituted, so a config dir that contains a source form (`.claude/`, `$&`, `/gsd:x`,
1765
+ * `Bash(`) would otherwise be converted a second time inside its own output.
1766
+ *
1767
+ * Every prefix that occurs in the content is shielded, whatever it contains. A default
1768
+ * install is unaffected: the converters have nothing to rewrite inside a prefix that
1769
+ * holds no source form, so the output is byte-identical. The sentinel namespace is chosen
1770
+ * per call and checked absent from `content`; when none of the candidates is free this
1771
+ * throws instead of converting unshielded.
1772
+ */
1773
+ function shieldEmittedPrefix(content, pathPrefix, convert) {
1774
+ const forms = shieldedPrefixForms(pathPrefix).filter((form) => content.includes(form));
1775
+ if (forms.length === 0)
1776
+ return convert(content);
1777
+ let namespace = 0;
1778
+ while (namespace < SHIELD_SENTINEL_LIMIT && content.includes(SHIELD_SENTINEL_OPEN + namespace + ':'))
1779
+ namespace += 1;
1780
+ if (namespace === SHIELD_SENTINEL_LIMIT) {
1781
+ throw new Error('shieldEmittedPrefix: no free sentinel namespace in the content; refusing to convert it unshielded');
1782
+ }
1783
+ const tokenOf = (index) => SHIELD_SENTINEL_OPEN + namespace + ':' + index + SHIELD_SENTINEL_CLOSE;
1784
+ let shielded = content;
1785
+ forms.forEach((form, index) => {
1786
+ shielded = shielded.split(form).join(tokenOf(index));
1787
+ });
1788
+ let converted = convert(shielded);
1789
+ forms.forEach((form, index) => {
1790
+ converted = converted.split(tokenOf(index)).join(form);
1791
+ });
1792
+ return converted;
1793
+ }
1794
+ /**
1795
+ * #2658 / #5715: the emitted `.trae/` prefix followed by the Claude instruction file names
1796
+ * the Trae rules file under that same prefix. The Body converter resolves it with a pattern
1797
+ * that reads the prefix text, which the shield hides from it, so the exact emitted prefix is
1798
+ * resolved here, before shielding, by literal replacement.
1799
+ */
1800
+ function resolveTraeInstructionFileAtPrefix(content, pathPrefix) {
1801
+ let resolved = content;
1802
+ for (const form of shieldedPrefixForms(pathPrefix)) {
1803
+ if (!form.endsWith('.trae'))
1804
+ continue;
1805
+ resolved = resolved.split(form + '/CLAUDE.md').join(form + '/rules/rules.md');
1806
+ }
1807
+ return resolved;
1808
+ }
1809
+ function convertClaudeToTraeMarkdown(content, pathPrefix) {
1810
+ return shieldEmittedPrefix(resolveTraeInstructionFileAtPrefix(content, pathPrefix), pathPrefix, convertClaudeToTraeMarkdownBody);
1811
+ }
1812
+ function convertClaudeToTraeMarkdownBody(content) {
1737
1813
  let converted = convertSlashCommandsToTraeSkillMentions(filterRuntimeNotesForTarget(content, 'trae'));
1738
1814
  converted = converted.replace(/\bBash\(/g, 'Shell(');
1739
1815
  converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
@@ -1793,9 +1869,10 @@ function convertClaudeToTraeMarkdown(content) {
1793
1869
  converted = converted.replace(/\.\/\.claude\//g, './.trae/');
1794
1870
  converted = converted.replace(/\.claude\//g, '.trae/');
1795
1871
  // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
1796
- // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
1797
- converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.trae');
1798
- converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.trae');
1872
+ // Use negative lookahead (?![\w-]|\.\w) to preserve .claude-plugin, .claudeignore and .claude.json.
1873
+ // #5715: an emitted prefix is shielded by convertClaudeToTraeMarkdown; the skills path runs this before the prefix pass, and the profile maps the own-dir forms.
1874
+ converted = converted.replace(/~\/\.claude(?![\w-]|\.\w)/g, '~/.trae');
1875
+ converted = converted.replace(/\$HOME\/\.claude(?![\w-]|\.\w)/g, () => '$HOME/.trae');
1799
1876
  // Environment variable name rewrite
1800
1877
  converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR');
1801
1878
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
@@ -2702,9 +2779,10 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
2702
2779
  const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
2703
2780
  const description = extractFrontmatterField(frontmatter, 'description') || '';
2704
2781
  const color = extractFrontmatterField(frontmatter, 'color');
2705
- const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
2706
- // CONV-04 + CONV-05: Map tools, deduplicate, format as JSON array
2707
- const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean);
2782
+ // CONV-04 + CONV-05: Map tools, deduplicate, format as JSON array.
2783
+ // parseFrontmatterTools reads both the inline comma list and the YAML block
2784
+ // list (#5790).
2785
+ const claudeTools = parseFrontmatterTools(frontmatter);
2708
2786
  const mappedTools = claudeTools.map(t => convertCopilotToolName(t));
2709
2787
  const uniqueTools = [...new Set(mappedTools)];
2710
2788
  const toolsArray = uniqueTools.length > 0
@@ -2794,8 +2872,8 @@ function convertClaudeAgentToAugmentAgent(content) {
2794
2872
  const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
2795
2873
  return `${cleanFrontmatter}\n${body}`;
2796
2874
  }
2797
- function convertClaudeAgentToTraeAgent(content) {
2798
- const converted = convertClaudeToTraeMarkdown(content);
2875
+ function convertClaudeAgentToTraeAgent(content, _isGlobal, meta) {
2876
+ const converted = convertClaudeToTraeMarkdown(content, meta && meta.pathPrefix);
2799
2877
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
2800
2878
  if (!frontmatter)
2801
2879
  return converted;
@@ -3110,7 +3188,9 @@ function convertClaudeAgentToCodexAgent(content) {
3110
3188
  return converted;
3111
3189
  const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
3112
3190
  const description = extractFrontmatterField(frontmatter, 'description') || '';
3113
- const tools = extractFrontmatterField(frontmatter, 'tools') || '';
3191
+ // parseFrontmatterTools reads both the inline comma list and the YAML block
3192
+ // list (#5790).
3193
+ const tools = parseFrontmatterTools(frontmatter).join(', ');
3114
3194
  const roleHeader = `<codex_agent_role>
3115
3195
  role: ${name}
3116
3196
  tools: ${tools}
@@ -3278,17 +3358,19 @@ function retargetInstalledIncludes(content, behaviors, pathPrefix) {
3278
3358
  return content;
3279
3359
  if (!behaviors)
3280
3360
  return content;
3281
- let result = content;
3361
+ // #5715: every rule is an alternative of ONE pass, so the prefix a rule emits is never
3362
+ // rescanned by another (a prefix under `$HOME/<root>/` was rewritten a second time by
3363
+ // the `@$HOME/<root>/` rule after the `@~/<root>/` rule had emitted it), and the
3364
+ // replacement is a function result: a string replacement would expand `$&` in a prefix.
3365
+ const rules = [];
3282
3366
  if (behaviors.retargetsGsdCoreProsePath === true) {
3283
- result = result.replace(/@?GSD core\/(?=[a-z])/g, `${pathPrefix}gsd-core/`);
3367
+ rules.push({ pattern: '@?GSD core\\/(?=[a-z])', replacement: `${pathPrefix}gsd-core/` });
3284
3368
  }
3285
3369
  const home = behaviors.defaultHomeIncludeRoot;
3286
3370
  if (typeof home === 'string' && home.length > 0 && pathPrefix !== `$HOME/${home}/`) {
3287
- result = result
3288
- .split(`@~/${home}/`).join(`@${pathPrefix}`)
3289
- .split(`@$HOME/${home}/`).join(`@${pathPrefix}`);
3371
+ rules.push({ pattern: '@~\\/' + (0, pattern_cjs_1.escapeRegex)(home) + '\\/', replacement: `@${pathPrefix}` }, { pattern: '@\\$HOME\\/' + (0, pattern_cjs_1.escapeRegex)(home) + '\\/', replacement: `@${pathPrefix}` });
3290
3372
  }
3291
- return result;
3373
+ return rewriteSourceFormsOnce(content, rules);
3292
3374
  }
3293
3375
  /** Return a safe project-relative prefix for a resolved install target. */
3294
3376
  function projectRelativePrefixFromProjectRoot(projectRoot, resolvedTarget) {
@@ -3609,27 +3691,102 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
3609
3691
  return _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal, attribution);
3610
3692
  }
3611
3693
  /**
3612
- * #5501: rewrite `~/.claude` and `$HOME/.claude` in one pass. `pathPrefix` (with
3613
- * its trailing slash) replaces the slash form; `normalizedPathPrefix` replaces the
3614
- * bare form, guarded so `.claude-plugin`, `.claudeignore` and `.claude.json` keep
3615
- * their bytes. A function replacement, never a string: a string replacement
3616
- * interprets `$&` in a config dir, and a single pass never rescans text it emitted.
3694
+ * #5501 / #5715: rewrite config-dir source forms (`~/.claude/`, `$HOME/.claude`,
3695
+ * `./.claude/`, ...) in ONE pass. A sequential `.replace` chain re-scans the prefix an
3696
+ * earlier pass emitted, so a config dir nested under `~/.claude` or named
3697
+ * `.claude.d` was rewritten again inside its own output. Here every rule is an
3698
+ * alternative of one regex (earlier rules win at a position), so each source
3699
+ * occurrence is rewritten exactly once and emitted text is never rescanned. The
3700
+ * replacement is a FUNCTION result, never a string: a string replacement
3701
+ * interprets `$&`, `$`` and `$'` in a config dir.
3617
3702
  */
3618
3703
  // The bare form ends at anything but a word character, `-`, or `.` + word character,
3619
3704
  // so `.claude-plugin`, `.claudeignore`, `.claude.json` and `.claude.d` are not home forms.
3620
3705
  const CLAUDE_HOME_BARE_BOUNDARY = '(?![\\w-]|\\.\\w)';
3621
- const CLAUDE_HOME_FORM_RE = new RegExp('(?:~|\\$HOME)\\/\\.claude(\\/|' + CLAUDE_HOME_BARE_BOUNDARY + ')', 'g');
3622
- function rewriteClaudeHomeForms(content, pathPrefix, normalizedPathPrefix) {
3623
- return content.replace(CLAUDE_HOME_FORM_RE, (match, tail) => {
3624
- if (tail === '/')
3625
- return pathPrefix;
3626
- // A default install: the bare form already names the target. Keep the source
3627
- // spelling (`~/.claude` stays `~/.claude`) so default installs stay byte-identical.
3628
- if (normalizedPathPrefix === '$HOME/.claude')
3629
- return match;
3630
- return normalizedPathPrefix;
3706
+ const CONFIG_DIR_REWRITE_RE_CACHE = new Map();
3707
+ function rewriteSourceFormsOnce(content, rules) {
3708
+ if (rules.length === 0)
3709
+ return content;
3710
+ const source = rules.map((rule) => '(' + rule.pattern + ')').join('|');
3711
+ let re = CONFIG_DIR_REWRITE_RE_CACHE.get(source);
3712
+ if (!re) {
3713
+ re = new RegExp(source, 'g');
3714
+ CONFIG_DIR_REWRITE_RE_CACHE.set(source, re);
3715
+ }
3716
+ return content.replace(re, (match, ...groups) => {
3717
+ for (let i = 0; i < rules.length; i++) {
3718
+ if (groups[i] !== undefined) {
3719
+ const replacement = rules[i].replacement;
3720
+ return typeof replacement === 'function' ? replacement(match) : replacement;
3721
+ }
3722
+ }
3723
+ return match;
3631
3724
  });
3632
3725
  }
3726
+ /**
3727
+ * Rule builders for one install target. `slash` rewrites `~/D/` and `$HOME/D/` to the
3728
+ * prefix; `tilde` only `~/D/`; `bare` rewrites `~/D` and `$HOME/D` (no trailing slash)
3729
+ * to the normalized prefix; `localSlash` / `localBare` rewrite `./D/` and `./D`.
3730
+ * Bare `.claude` rules (home and `./`) take CLAUDE_HOME_BARE_BOUNDARY so Claude Code's
3731
+ * own `.claude`-prefixed files (`.claude.json`, `.claude-plugin`) stay unchanged;
3732
+ * every other bare rule takes the EXACT boundary its profile used before #5715 (a
3733
+ * lookahead or `\b`). Order the rules slash forms first: most bare boundaries also
3734
+ * accept a following `/`.
3735
+ */
3736
+ function configDirRuleBuilders(pathPrefix, normalizedPathPrefix, dirName) {
3737
+ const dir = (d) => (0, pattern_cjs_1.escapeRegex)(d);
3738
+ const slashRule = (pattern) => ({ pattern, replacement: pathPrefix });
3739
+ const bareRule = (pattern, replacement) => ({
3740
+ pattern,
3741
+ replacement: replacement ?? normalizedPathPrefix,
3742
+ });
3743
+ return {
3744
+ slash: (d) => [slashRule('~\\/' + dir(d) + '\\/'), slashRule('\\$HOME\\/' + dir(d) + '\\/')],
3745
+ tilde: (d) => [slashRule('~\\/' + dir(d) + '\\/')],
3746
+ bare: (d, boundary, replacement) => [
3747
+ bareRule('~\\/' + dir(d) + boundary, replacement),
3748
+ bareRule('\\$HOME\\/' + dir(d) + boundary, replacement),
3749
+ ],
3750
+ localSlash: (d, replacement = './' + dirName + '/') => [
3751
+ { pattern: '\\.\\/' + dir(d) + '\\/', replacement },
3752
+ ],
3753
+ localBare: (d, boundary) => [
3754
+ { pattern: '\\.\\/' + dir(d) + boundary, replacement: './' + dirName },
3755
+ ],
3756
+ };
3757
+ }
3758
+ /**
3759
+ * The gsd-core/ spec-tree copy (`copyWithPathReplacement` in bin/install.js): the
3760
+ * generic Claude / Qwen / Hermes source forms to the install prefix, in one pass.
3761
+ */
3762
+ function rewriteSpecTreeConfigDirForms(content, pathPrefix, dirName) {
3763
+ const b = configDirRuleBuilders(pathPrefix, pathPrefix.replace(/\/$/, ''), dirName);
3764
+ return rewriteSourceFormsOnce(content, [
3765
+ ...b.slash('.claude'),
3766
+ ...b.localSlash('.claude'),
3767
+ ...b.slash('.qwen'),
3768
+ ...b.localSlash('.qwen'),
3769
+ ...b.slash('.hermes'),
3770
+ ...b.localSlash('.hermes'),
3771
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3772
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3773
+ ]);
3774
+ }
3775
+ /**
3776
+ * The OpenCode / Kilo commands and skills path pass: `~/.claude/`, `$HOME/.claude/` and the
3777
+ * runtimes' own `~/.opencode/` / `~/.kilo/` to the install prefix, `./.claude/` to the
3778
+ * runtime's local dir, in ONE pass with literal replacement (a prefix is never rescanned,
3779
+ * and `$&` in a config dir is not expanded).
3780
+ */
3781
+ function rewriteOpencodeFamilyConfigDirForms(content, pathPrefix, dirName) {
3782
+ const b = configDirRuleBuilders(pathPrefix, pathPrefix.replace(/\/$/, ''), dirName);
3783
+ return rewriteSourceFormsOnce(content, [
3784
+ ...b.slash('.claude'),
3785
+ ...b.localSlash('.claude'),
3786
+ ...b.tilde('.opencode'),
3787
+ ...b.tilde('.kilo'),
3788
+ ]);
3789
+ }
3633
3790
  function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
3634
3791
  const dirName = getDirName(runtime);
3635
3792
  const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
@@ -3645,95 +3802,105 @@ function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = fal
3645
3802
  if (!rewriteBehaviors.skipRuntimeDefaultsStamp) {
3646
3803
  content = _stampNonClaudeRuntimeDefaults(content, runtime);
3647
3804
  }
3805
+ const b = configDirRuleBuilders(pathPrefix, normalizedPathPrefix, dirName);
3806
+ const LOOKAHEAD = '(?![\\w-])';
3807
+ const WORD_EDGE = '\\b';
3648
3808
  switch (rewriteBehaviors.contentRewriteProfile) {
3649
3809
  case 'slash-own-tilde':
3650
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3651
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3652
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3653
- content = content.replace(/~\/\.codex\//g, pathPrefix);
3810
+ content = rewriteSourceFormsOnce(content, [
3811
+ ...b.slash('.claude'),
3812
+ ...b.localSlash('.claude'),
3813
+ ...b.tilde('.codex'),
3814
+ ]);
3654
3815
  // #1515 stamp moved to _stampNonClaudeRuntimeDefaults (#1521 generalisation).
3655
3816
  content = processAttribution(content, attribution);
3656
3817
  break;
3657
3818
  case 'slash-bare-own-tilde':
3658
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3659
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3660
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3661
- content = content.replace(/~\/\.cline\//g, pathPrefix);
3662
- content = content.replace(/\$HOME\/\.cline\//g, pathPrefix);
3663
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
3664
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
3665
- content = content.replace(/~\/\.cline\b/g, normalizedPathPrefix);
3666
- content = content.replace(/\$HOME\/\.cline\b/g, normalizedPathPrefix);
3819
+ content = rewriteSourceFormsOnce(content, [
3820
+ ...b.slash('.claude'),
3821
+ ...b.localSlash('.claude'),
3822
+ ...b.slash('.cline'),
3823
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3824
+ ...b.bare('.cline', WORD_EDGE),
3825
+ ]);
3667
3826
  content = processAttribution(content, attribution);
3668
3827
  break;
3669
3828
  case 'slash-lookahead-own-tilde':
3670
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3671
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3672
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3673
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
3674
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
3675
- content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
3676
- content = content.replace(/~\/\.cursor\//g, pathPrefix);
3829
+ content = rewriteSourceFormsOnce(content, [
3830
+ ...b.slash('.claude'),
3831
+ ...b.localSlash('.claude'),
3832
+ ...b.tilde('.cursor'),
3833
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3834
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3835
+ ]);
3677
3836
  content = processAttribution(content, attribution);
3678
3837
  break;
3679
3838
  case 'slash-lookahead-nested-own-devin': {
3680
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3681
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3682
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3683
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
3684
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
3685
- content = content.replace(/~\/\.codeium\/windsurf\//g, pathPrefix);
3686
- if (isGlobal) {
3687
- content = content.replace(/\.devin\/skills\//g, `${pathPrefix}skills/`);
3688
- content = content.replace(/\.\/\.devin\//g, pathPrefix);
3689
- content = content.replace(/~\/\.devin(?![\w-])/g, normalizedPathPrefix);
3690
- content = content.replace(/\$HOME\/\.devin(?![\w-])/g, normalizedPathPrefix);
3691
- }
3839
+ content = rewriteSourceFormsOnce(content, [
3840
+ ...b.slash('.claude'),
3841
+ ...b.localSlash('.claude'),
3842
+ ...b.tilde('.codeium/windsurf'),
3843
+ // The `~/` and `$HOME/` `.devin/skills/` forms become the prefix followed by
3844
+ // `skills/` (no stray `~/`, no doubled `$HOME/`). The `./` form keeps the text
3845
+ // before `.devin` as the pre-#5715 sequential rewrite left it. They sit before
3846
+ // the bare `.devin` rules so the longer match wins at the same position.
3847
+ ...(isGlobal
3848
+ ? [
3849
+ { pattern: '~\\/\\.devin\\/skills\\/', replacement: pathPrefix + 'skills/' },
3850
+ { pattern: '\\$HOME\\/\\.devin\\/skills\\/', replacement: pathPrefix + 'skills/' },
3851
+ { pattern: '\\.\\/\\.devin\\/skills\\/', replacement: './' + pathPrefix + 'skills/' },
3852
+ { pattern: '\\.devin\\/skills\\/', replacement: pathPrefix + 'skills/' },
3853
+ ...b.localSlash('.devin', pathPrefix),
3854
+ ]
3855
+ : []),
3856
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3857
+ ...(isGlobal ? b.bare('.devin', LOOKAHEAD) : []),
3858
+ ]);
3692
3859
  content = processAttribution(content, attribution);
3693
3860
  break;
3694
3861
  }
3695
3862
  case 'slash-lookahead-own-derived': {
3696
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3697
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3698
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3699
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
3700
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
3701
- content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
3702
3863
  // #2097: dot-dir self-references (~/.augment/…) → resolved prefix,
3703
3864
  // dirName-derived (no runtime literal). getDirName('augment') resolves
3704
3865
  // to '.augment', so this is byte-identical to the prior hardcoded regexes.
3705
- const _dd = (0, pattern_cjs_1.escapeRegex)(dirName);
3706
- content = content.replace(new RegExp('~/' + _dd + '/', 'g'), pathPrefix);
3707
- content = content.replace(new RegExp('\\$HOME/' + _dd + '/', 'g'), pathPrefix);
3708
- content = content.replace(new RegExp('~/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
3709
- content = content.replace(new RegExp('\\$HOME/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
3866
+ content = rewriteSourceFormsOnce(content, [
3867
+ ...b.slash('.claude'),
3868
+ ...b.localSlash('.claude'),
3869
+ ...b.slash(dirName),
3870
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3871
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3872
+ ...b.bare(dirName, LOOKAHEAD),
3873
+ ]);
3710
3874
  content = processAttribution(content, attribution);
3711
3875
  break;
3712
3876
  }
3713
- case 'slash-bare-own-derived':
3714
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3715
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3716
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3717
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
3718
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
3719
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
3877
+ case 'slash-bare-own-derived': {
3720
3878
  // #2094: descriptor-driven — dirName resolves to '.trae' via
3721
- // getDirName()/localConfigDir, so this regex is built rather than
3722
- // hardcoded as `/~\/\.trae\//g` (byte-identical output for trae).
3723
- content = content.replace(new RegExp('~/' + (0, pattern_cjs_1.escapeRegex)(dirName) + '/', 'g'), pathPrefix);
3879
+ // getDirName()/localConfigDir, so the own-dir rule is built rather than
3880
+ // hardcoded. #5715: own-dir slash and bare forms (both `~/` and `$HOME/`
3881
+ // spellings) map to the install prefix, so a custom or local config dir is
3882
+ // honoured; default global output is unchanged.
3883
+ const keepDefaultOwnBare = (match) => (normalizedPathPrefix === '$HOME/' + dirName ? match : normalizedPathPrefix);
3884
+ content = rewriteSourceFormsOnce(content, [
3885
+ ...b.slash('.claude'),
3886
+ ...b.localSlash('.claude'),
3887
+ ...b.slash(dirName),
3888
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3889
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3890
+ ...b.bare(dirName, CLAUDE_HOME_BARE_BOUNDARY, keepDefaultOwnBare),
3891
+ ]);
3724
3892
  content = processAttribution(content, attribution);
3725
3893
  break;
3894
+ }
3726
3895
  case 'slash-bare-own-tilde-bare':
3727
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3728
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3729
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3730
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
3731
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
3732
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
3733
- content = content.replace(/~\/\.codebuddy\//g, pathPrefix);
3734
- content = content.replace(/\$HOME\/\.codebuddy\//g, pathPrefix);
3735
- content = content.replace(/~\/\.codebuddy\b/g, normalizedPathPrefix);
3736
- content = content.replace(/\$HOME\/\.codebuddy\b/g, normalizedPathPrefix);
3896
+ content = rewriteSourceFormsOnce(content, [
3897
+ ...b.slash('.claude'),
3898
+ ...b.localSlash('.claude'),
3899
+ ...b.slash('.codebuddy'),
3900
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3901
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3902
+ ...b.bare('.codebuddy', WORD_EDGE),
3903
+ ]);
3737
3904
  content = processAttribution(content, attribution);
3738
3905
  break;
3739
3906
  case 'attribution-only':
@@ -3747,11 +3914,14 @@ function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = fal
3747
3914
  // #5501: the slash AND bare forms in ONE pass. The launcher default
3748
3915
  // `${CLAUDE_CONFIG_DIR:-$HOME/.claude}` has no trailing slash, so a slash-only
3749
3916
  // rewrite left a --config-dir install probing `~/.claude/gsd-core`. One
3750
- // combined pass (not slash then bare) so a prefix the pass emits is never
3751
- // rescanned: a config dir nested under `~/.claude` or named `.claude.d` would
3752
- // otherwise be rewritten again.
3753
- content = rewriteClaudeHomeForms(content, pathPrefix, normalizedPathPrefix);
3754
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3917
+ // pass (#5715: also `./.claude/`) so a prefix the pass emits is never
3918
+ // rescanned. A default install keeps the source spelling of the bare form
3919
+ // (`~/.claude` stays `~/.claude`) so it stays byte-identical.
3920
+ content = rewriteSourceFormsOnce(content, [
3921
+ ...b.slash('.claude'),
3922
+ ...b.localSlash('.claude'),
3923
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY, (match) => (normalizedPathPrefix === '$HOME/.claude' ? match : normalizedPathPrefix)),
3924
+ ]);
3755
3925
  // #3133 / #3544: restore @-file-reference lines to the tilde form
3756
3926
  // Claude actually expands — see restoreClaudeGlobalAtRefTilde's doc
3757
3927
  // comment above for why this must be a separate, @-anchored pass. Handed
@@ -3766,19 +3936,18 @@ function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = fal
3766
3936
  // (the gsd-surface skill) that need the negative-lookahead bare rewrites the
3767
3937
  // claude profile deliberately does not have (it would change claude's bytes).
3768
3938
  case 'slash-lookahead-tilde-restore':
3769
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3770
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3771
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3772
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
3773
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
3774
- content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
3939
+ content = rewriteSourceFormsOnce(content, [
3940
+ ...b.slash('.claude'),
3941
+ ...b.localSlash('.claude'),
3942
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3943
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3944
+ ]);
3775
3945
  content = restoreClaudeGlobalAtRefTilde(content, pathPrefix);
3776
3946
  content = processAttribution(content, attribution);
3777
3947
  break;
3778
3948
  // Descriptor-driven brand literals (ADR-1239 / #2092): the qwen/hermes
3779
3949
  // brand VALUES (CLAUDE.md/Claude Code/.claude/ replacements) now read from
3780
3950
  // runtime.hostBehaviors.brandingRewrites instead of hardcoded literals.
3781
- // EXACT regexes/order preserved — only the replacement values changed.
3782
3951
  // qwen and hermes share this profile; their own dot-dir is derived from the
3783
3952
  // descriptor's `localConfigDir` (`getDirName`) rather than spelled per host.
3784
3953
  case 'branded-lookahead-own-derived': {
@@ -3787,35 +3956,32 @@ function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = fal
3787
3956
  // brand-literal replacements, still apply the non-branding path
3788
3957
  // rewrites below) instead of throwing on `_b['CLAUDE.md']`.
3789
3958
  const _b = rewriteBehaviors.brandingRewrites;
3790
- const _own = (0, pattern_cjs_1.escapeRegex)(dirName);
3791
3959
  if (_b) {
3792
3960
  content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
3793
3961
  // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
3794
3962
  content = applyClaudeCodeBrandSwap(content, _b['Claude Code']);
3795
3963
  }
3796
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3797
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3798
- content = content.replace(new RegExp('~/' + _own + '/', 'g'), pathPrefix);
3799
- content = content.replace(new RegExp('\\$HOME/' + _own + '/', 'g'), pathPrefix);
3800
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
3801
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
3802
- content = content.replace(new RegExp('~/' + _own + '(?![\\w-])', 'g'), normalizedPathPrefix);
3803
- content = content.replace(new RegExp('\\$HOME/' + _own + '(?![\\w-])', 'g'), normalizedPathPrefix);
3804
- if (_b) {
3805
- content = content.replace(/\.claude\//g, _b['.claude/']);
3806
- }
3807
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3808
- content = content.replace(new RegExp('\\./' + _own + '/', 'g'), `./${dirName}/`);
3964
+ content = rewriteSourceFormsOnce(content, [
3965
+ ...b.slash('.claude'),
3966
+ ...b.slash(dirName),
3967
+ ...b.localSlash('.claude', _b ? './' + _b['.claude/'] : undefined),
3968
+ ...b.localSlash(dirName),
3969
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3970
+ ...b.bare(dirName, LOOKAHEAD),
3971
+ // Lowest priority: every other `.claude/` in the source takes the brand form.
3972
+ // In the SAME pass, so the prefix emitted above is never rescanned.
3973
+ ...(_b ? [{ pattern: '\\.claude\\/', replacement: () => _b['.claude/'] }] : []),
3974
+ ]);
3809
3975
  content = processAttribution(content, attribution);
3810
3976
  break;
3811
3977
  }
3812
3978
  case 'slash-bare':
3813
- content = content.replace(/~\/\.claude\//g, pathPrefix);
3814
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
3815
- content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
3816
- content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
3817
- content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
3818
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
3979
+ content = rewriteSourceFormsOnce(content, [
3980
+ ...b.slash('.claude'),
3981
+ ...b.localSlash('.claude'),
3982
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3983
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3984
+ ]);
3819
3985
  content = processAttribution(content, attribution);
3820
3986
  break;
3821
3987
  default:
@@ -3887,7 +4053,8 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
3887
4053
  // from runtime.hostBehaviors instead of a hardcoded runtime-name branch.
3888
4054
  const _cmdConv = hostBehaviorsFor(runtime).commandBodyConverter;
3889
4055
  if (_cmdConv && COMMAND_BODY_CONVERTERS[_cmdConv]) {
3890
- content = COMMAND_BODY_CONVERTERS[_cmdConv](content);
4056
+ // #5715: the converter runs after the prefix pass, so the emitted prefix is shielded.
4057
+ content = shieldEmittedPrefix(content, pathPrefix, COMMAND_BODY_CONVERTERS[_cmdConv]);
3891
4058
  }
3892
4059
  installFs().writeFileSync(node_path_1.default.join(tempDir, entry.name), content);
3893
4060
  }
@@ -4069,18 +4236,29 @@ function applyAgentPathRewrites(content, runtime, pathPrefix) {
4069
4236
  }
4070
4237
  return applyAgentPathRewritesInner(content, runtime, pathPrefix);
4071
4238
  }
4239
+ /**
4240
+ * #5715: run an agent converter over content whose config-dir forms `applyAgentPathRewrites`
4241
+ * has already rewritten, with the emitted prefix shielded from the converter. A runtime that
4242
+ * declares `noPathRewrite` emitted no prefix, so nothing is shielded.
4243
+ */
4244
+ function shieldAgentConverterPrefix(content, runtime, pathPrefix, convert) {
4245
+ if (hostBehaviorsFor(runtime).noPathRewrite === true)
4246
+ return convert(content);
4247
+ // The runtime whose own dot-dir is `.trae` resolves the instruction file at the emitted
4248
+ // prefix (see resolveTraeInstructionFileAtPrefix) before the prefix is hidden.
4249
+ const resolved = getDirName(runtime) === '.trae' ? resolveTraeInstructionFileAtPrefix(content, pathPrefix) : content;
4250
+ return shieldEmittedPrefix(resolved, pathPrefix, convert);
4251
+ }
4072
4252
  function applyAgentPathRewritesInner(content, runtime, pathPrefix) {
4073
4253
  const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
4074
- content = content.replace(/~\/\.claude\//g, pathPrefix);
4075
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
4076
4254
  // #3719 review (MAJOR): a bare `\b` is satisfied by ANY non-word character,
4077
4255
  // including '-' — for a --config-dir whose name EXTENDS '.claude' (e.g.
4078
4256
  // '.claude-work'), this re-matched the '.claude' PREFIX of the emitted
4079
- // '.claude-work' path and corrupted it to '.claude-work-work'. Guard with
4080
- // the SAME negative-lookahead convention already used at
4081
- // copyWithPathReplacement's call site (bin/install.js:7729-7730).
4082
- content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
4083
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
4257
+ // '.claude-work' path and corrupted it to '.claude-work-work'. #5715: the
4258
+ // slash and bare forms are one single-pass rewrite, so no emitted prefix is
4259
+ // ever rescanned (a nested or `.claude.`-named config dir stays intact).
4260
+ const b = configDirRuleBuilders(pathPrefix, normalizedPathPrefix, '');
4261
+ content = rewriteSourceFormsOnce(content, [...b.slash('.claude'), ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY)]);
4084
4262
  // #3719: the THIRD emit path that needed this restore. #3133 added it to the
4085
4263
  // skill/command pipeline (`_applyRuntimeRewrites` case 'claude') and #3544 to
4086
4264
  // bin/install.js's spec-tree copy; the agents pipeline never got it, so every
@@ -4379,6 +4557,11 @@ module.exports = {
4379
4557
  _localIncludeDirName: localIncludeDirName,
4380
4558
  _restoreClaudeGlobalAtRefTilde: restoreClaudeGlobalAtRefTilde,
4381
4559
  _applyRuntimeRewrites,
4560
+ _rewriteSpecTreeConfigDirForms: rewriteSpecTreeConfigDirForms,
4561
+ _rewriteOpencodeFamilyConfigDirForms: rewriteOpencodeFamilyConfigDirForms,
4562
+ _shieldAgentConverterPrefix: shieldAgentConverterPrefix,
4563
+ _resolveTraeInstructionFileAtPrefix: resolveTraeInstructionFileAtPrefix,
4564
+ _shieldEmittedPrefix: shieldEmittedPrefix,
4382
4565
  _stampNonClaudeRuntimeDefaults,
4383
4566
  // #2652: registry-resolved dispatch isolation, mirroring routeDispatchIsolation
4384
4567
  _negotiatedDispatchIsolation,