@opengsd/gsd-core 1.19.0 → 1.20.1

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 (131) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.compact.md +5 -5
  4. package/agents/gsd-code-fixer.md +9 -4
  5. package/agents/gsd-code-reviewer.compact.md +49 -8
  6. package/agents/gsd-code-reviewer.md +53 -14
  7. package/agents/gsd-executor.md +8 -4
  8. package/bin/install.js +36 -40
  9. package/commands/gsd/new-milestone.md +2 -2
  10. package/commands/gsd/new-project.md +2 -2
  11. package/commands/gsd/update.md +5 -2
  12. package/gsd-core/bin/gsd-tools.cjs +177 -93
  13. package/gsd-core/bin/lib/capability-lifecycle.cjs +1 -12
  14. package/gsd-core/bin/lib/capability-registry.cjs +284 -93
  15. package/gsd-core/bin/lib/capability-validator.cjs +15 -0
  16. package/gsd-core/bin/lib/commands.cjs +46 -17
  17. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  18. package/gsd-core/bin/lib/core-utils.cjs +11 -0
  19. package/gsd-core/bin/lib/frontmatter.cjs +21 -3
  20. package/gsd-core/bin/lib/gate-api-coverage-verify-pre.cjs +2 -2
  21. package/gsd-core/bin/lib/gate-context-drift.cjs +1 -1
  22. package/gsd-core/bin/lib/gate-phase-context.cjs +7 -8
  23. package/gsd-core/bin/lib/gate-schema-drift.cjs +1 -1
  24. package/gsd-core/bin/lib/git-info-exclude.cjs +1 -6
  25. package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
  26. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +13 -9
  27. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +9 -7
  28. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +3 -3
  29. package/gsd-core/bin/lib/init.cjs +181 -68
  30. package/gsd-core/bin/lib/install-engine.cjs +9 -9
  31. package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
  32. package/gsd-core/bin/lib/install-profiles.cjs +4 -2
  33. package/gsd-core/bin/lib/io.cjs +6 -0
  34. package/gsd-core/bin/lib/markdown-sectionizer.cjs +34 -1
  35. package/gsd-core/bin/lib/markdown-table.cjs +23 -0
  36. package/gsd-core/bin/lib/mcp-server.cjs +8 -5
  37. package/gsd-core/bin/lib/milestone.cjs +18 -50
  38. package/gsd-core/bin/lib/phase-entry-grammar.cjs +119 -0
  39. package/gsd-core/bin/lib/phase-id-display.cjs +2 -0
  40. package/gsd-core/bin/lib/phase-id.cjs +627 -36
  41. package/gsd-core/bin/lib/phase-lifecycle.cjs +9 -74
  42. package/gsd-core/bin/lib/phase-locator.cjs +328 -19
  43. package/gsd-core/bin/lib/phase.cjs +3039 -666
  44. package/gsd-core/bin/lib/planning-document.cjs +26 -5
  45. package/gsd-core/bin/lib/planning-inspect.cjs +27 -35
  46. package/gsd-core/bin/lib/planning-snapshot.cjs +12 -7
  47. package/gsd-core/bin/lib/planning-workspace.cjs +3 -3
  48. package/gsd-core/bin/lib/read-only-git-env.cjs +23 -2
  49. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +2 -2
  50. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -0
  51. package/gsd-core/bin/lib/review-lane-invocation.cjs +125 -18
  52. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +11 -9
  53. package/gsd-core/bin/lib/roadmap-parser.cjs +759 -144
  54. package/gsd-core/bin/lib/roadmap-progress.cjs +136 -0
  55. package/gsd-core/bin/lib/roadmap-upgrade.cjs +8 -5
  56. package/gsd-core/bin/lib/roadmap.cjs +358 -106
  57. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -130
  58. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
  59. package/gsd-core/bin/lib/shell-command-projection.cjs +36 -9
  60. package/gsd-core/bin/lib/smart-entry.cjs +22 -11
  61. package/gsd-core/bin/lib/state-contract.cjs +11 -7
  62. package/gsd-core/bin/lib/state-document.cjs +332 -23
  63. package/gsd-core/bin/lib/state-transition.cjs +51 -15
  64. package/gsd-core/bin/lib/state.cjs +499 -180
  65. package/gsd-core/bin/lib/template.cjs +2 -2
  66. package/gsd-core/bin/lib/validate.cjs +3 -2
  67. package/gsd-core/bin/lib/vendor/re2js.cjs +863 -577
  68. package/gsd-core/bin/lib/vendor/re2js.d.cts +284 -219
  69. package/gsd-core/bin/lib/verify.cjs +2 -2
  70. package/gsd-core/bin/lib/workstream-inventory.cjs +27 -21
  71. package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
  72. package/gsd-core/bin/verify-reapply-patches.cjs +411 -10
  73. package/gsd-core/references/cicd-review-patterns.md +45 -0
  74. package/gsd-core/references/necessity-review.md +48 -0
  75. package/gsd-core/workflows/code-review-fix.md +46 -5
  76. package/gsd-core/workflows/code-review.md +3 -0
  77. package/gsd-core/workflows/execute-phase.md +1 -1
  78. package/gsd-core/workflows/execute-plan.md +1 -1
  79. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +13 -9
  80. package/gsd-core/workflows/plan-phase.md +1 -1
  81. package/gsd-core/workflows/pr-branch.md +2 -0
  82. package/gsd-core/workflows/reapply-patches.md +104 -0
  83. package/gsd-core/workflows/review.md +54 -141
  84. package/gsd-core/workflows/secure-phase.md +1 -1
  85. package/gsd-core/workflows/settings-advanced.md +57 -9
  86. package/gsd-core/workflows/ship.md +12 -0
  87. package/hooks/dist/gsd-agent-isolation-guard.js +1 -1
  88. package/hooks/dist/gsd-cursor-subagent-start.js +1 -1
  89. package/hooks/dist/gsd-graphify-update.sh +6 -0
  90. package/hooks/dist/gsd-statusline.js +3 -2
  91. package/hooks/dist/gsd-windsurf-pre-write.js +4 -4
  92. package/hooks/dist/gsd-workflow-guard.js +2 -1
  93. package/hooks/dist/gsd-worktree-path-guard.js +4 -4
  94. package/hooks/dist/lib/git-probe.js +16 -0
  95. package/hooks/dist/lib/isolation-sentinel.js +77 -2
  96. package/hooks/gsd-agent-isolation-guard.js +1 -1
  97. package/hooks/gsd-cursor-subagent-start.js +1 -1
  98. package/hooks/gsd-graphify-update.sh +6 -0
  99. package/hooks/gsd-statusline.js +3 -2
  100. package/hooks/gsd-windsurf-pre-write.js +4 -4
  101. package/hooks/gsd-workflow-guard.js +2 -1
  102. package/hooks/gsd-worktree-path-guard.js +4 -4
  103. package/hooks/lib/git-probe.js +16 -0
  104. package/hooks/lib/isolation-sentinel.js +77 -2
  105. package/package.json +3 -3
  106. package/pi/package-agents/gsd-code-fixer.md +9 -4
  107. package/pi/package-agents/gsd-code-reviewer.md +53 -14
  108. package/pi/package-agents/gsd-executor.md +8 -4
  109. package/pi/package-agents/manifest.json +3 -3
  110. package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
  111. package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
  112. package/pi/package-skills/gsd-update/SKILL.md +4 -1
  113. package/scripts/changeset/lint.cjs +28 -3
  114. package/scripts/ci-next-health.cjs +246 -23
  115. package/scripts/docs-guard-registry.cjs +2 -0
  116. package/scripts/gen-features.cjs +14 -2
  117. package/scripts/lib/drift-scan.cjs +27 -0
  118. package/scripts/lib/macos-conformance-tier.generated.cjs +10 -0
  119. package/scripts/lib/platform-conformance-tier.generated.cjs +15 -0
  120. package/scripts/lint-phase-enumeration-drift.cjs +8 -4
  121. package/scripts/lint-roadmap-progress-drift.cjs +421 -0
  122. package/scripts/merge-commit-pr.cjs +186 -0
  123. package/scripts/mutation-matrix.cjs +23 -1
  124. package/scripts/release-ancestry.cjs +86 -0
  125. package/scripts/release-pipeline-pr.cjs +92 -0
  126. package/scripts/require-issue-link-policy.cjs +18 -9
  127. package/scripts/sync-next-version.cjs +48 -7
  128. package/skills/gsd-new-milestone/SKILL.md +1 -1
  129. package/skills/gsd-new-project/SKILL.md +1 -1
  130. package/skills/gsd-update/SKILL.md +5 -2
  131. 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, '');
@@ -2794,8 +2871,8 @@ function convertClaudeAgentToAugmentAgent(content) {
2794
2871
  const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
2795
2872
  return `${cleanFrontmatter}\n${body}`;
2796
2873
  }
2797
- function convertClaudeAgentToTraeAgent(content) {
2798
- const converted = convertClaudeToTraeMarkdown(content);
2874
+ function convertClaudeAgentToTraeAgent(content, _isGlobal, meta) {
2875
+ const converted = convertClaudeToTraeMarkdown(content, meta && meta.pathPrefix);
2799
2876
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
2800
2877
  if (!frontmatter)
2801
2878
  return converted;
@@ -3278,17 +3355,19 @@ function retargetInstalledIncludes(content, behaviors, pathPrefix) {
3278
3355
  return content;
3279
3356
  if (!behaviors)
3280
3357
  return content;
3281
- let result = content;
3358
+ // #5715: every rule is an alternative of ONE pass, so the prefix a rule emits is never
3359
+ // rescanned by another (a prefix under `$HOME/<root>/` was rewritten a second time by
3360
+ // the `@$HOME/<root>/` rule after the `@~/<root>/` rule had emitted it), and the
3361
+ // replacement is a function result: a string replacement would expand `$&` in a prefix.
3362
+ const rules = [];
3282
3363
  if (behaviors.retargetsGsdCoreProsePath === true) {
3283
- result = result.replace(/@?GSD core\/(?=[a-z])/g, `${pathPrefix}gsd-core/`);
3364
+ rules.push({ pattern: '@?GSD core\\/(?=[a-z])', replacement: `${pathPrefix}gsd-core/` });
3284
3365
  }
3285
3366
  const home = behaviors.defaultHomeIncludeRoot;
3286
3367
  if (typeof home === 'string' && home.length > 0 && pathPrefix !== `$HOME/${home}/`) {
3287
- result = result
3288
- .split(`@~/${home}/`).join(`@${pathPrefix}`)
3289
- .split(`@$HOME/${home}/`).join(`@${pathPrefix}`);
3368
+ rules.push({ pattern: '@~\\/' + (0, pattern_cjs_1.escapeRegex)(home) + '\\/', replacement: `@${pathPrefix}` }, { pattern: '@\\$HOME\\/' + (0, pattern_cjs_1.escapeRegex)(home) + '\\/', replacement: `@${pathPrefix}` });
3290
3369
  }
3291
- return result;
3370
+ return rewriteSourceFormsOnce(content, rules);
3292
3371
  }
3293
3372
  /** Return a safe project-relative prefix for a resolved install target. */
3294
3373
  function projectRelativePrefixFromProjectRoot(projectRoot, resolvedTarget) {
@@ -3609,26 +3688,101 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
3609
3688
  return _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal, attribution);
3610
3689
  }
3611
3690
  /**
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.
3691
+ * #5501 / #5715: rewrite config-dir source forms (`~/.claude/`, `$HOME/.claude`,
3692
+ * `./.claude/`, ...) in ONE pass. A sequential `.replace` chain re-scans the prefix an
3693
+ * earlier pass emitted, so a config dir nested under `~/.claude` or named
3694
+ * `.claude.d` was rewritten again inside its own output. Here every rule is an
3695
+ * alternative of one regex (earlier rules win at a position), so each source
3696
+ * occurrence is rewritten exactly once and emitted text is never rescanned. The
3697
+ * replacement is a FUNCTION result, never a string: a string replacement
3698
+ * interprets `$&`, `$`` and `$'` in a config dir.
3617
3699
  */
3618
3700
  // The bare form ends at anything but a word character, `-`, or `.` + word character,
3619
3701
  // so `.claude-plugin`, `.claudeignore`, `.claude.json` and `.claude.d` are not home forms.
3620
3702
  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;
3703
+ const CONFIG_DIR_REWRITE_RE_CACHE = new Map();
3704
+ function rewriteSourceFormsOnce(content, rules) {
3705
+ if (rules.length === 0)
3706
+ return content;
3707
+ const source = rules.map((rule) => '(' + rule.pattern + ')').join('|');
3708
+ let re = CONFIG_DIR_REWRITE_RE_CACHE.get(source);
3709
+ if (!re) {
3710
+ re = new RegExp(source, 'g');
3711
+ CONFIG_DIR_REWRITE_RE_CACHE.set(source, re);
3712
+ }
3713
+ return content.replace(re, (match, ...groups) => {
3714
+ for (let i = 0; i < rules.length; i++) {
3715
+ if (groups[i] !== undefined) {
3716
+ const replacement = rules[i].replacement;
3717
+ return typeof replacement === 'function' ? replacement(match) : replacement;
3718
+ }
3719
+ }
3720
+ return match;
3721
+ });
3722
+ }
3723
+ /**
3724
+ * Rule builders for one install target. `slash` rewrites `~/D/` and `$HOME/D/` to the
3725
+ * prefix; `tilde` only `~/D/`; `bare` rewrites `~/D` and `$HOME/D` (no trailing slash)
3726
+ * to the normalized prefix; `localSlash` / `localBare` rewrite `./D/` and `./D`.
3727
+ * Bare `.claude` rules (home and `./`) take CLAUDE_HOME_BARE_BOUNDARY so Claude Code's
3728
+ * own `.claude`-prefixed files (`.claude.json`, `.claude-plugin`) stay unchanged;
3729
+ * every other bare rule takes the EXACT boundary its profile used before #5715 (a
3730
+ * lookahead or `\b`). Order the rules slash forms first: most bare boundaries also
3731
+ * accept a following `/`.
3732
+ */
3733
+ function configDirRuleBuilders(pathPrefix, normalizedPathPrefix, dirName) {
3734
+ const dir = (d) => (0, pattern_cjs_1.escapeRegex)(d);
3735
+ const slashRule = (pattern) => ({ pattern, replacement: pathPrefix });
3736
+ const bareRule = (pattern, replacement) => ({
3737
+ pattern,
3738
+ replacement: replacement ?? normalizedPathPrefix,
3631
3739
  });
3740
+ return {
3741
+ slash: (d) => [slashRule('~\\/' + dir(d) + '\\/'), slashRule('\\$HOME\\/' + dir(d) + '\\/')],
3742
+ tilde: (d) => [slashRule('~\\/' + dir(d) + '\\/')],
3743
+ bare: (d, boundary, replacement) => [
3744
+ bareRule('~\\/' + dir(d) + boundary, replacement),
3745
+ bareRule('\\$HOME\\/' + dir(d) + boundary, replacement),
3746
+ ],
3747
+ localSlash: (d, replacement = './' + dirName + '/') => [
3748
+ { pattern: '\\.\\/' + dir(d) + '\\/', replacement },
3749
+ ],
3750
+ localBare: (d, boundary) => [
3751
+ { pattern: '\\.\\/' + dir(d) + boundary, replacement: './' + dirName },
3752
+ ],
3753
+ };
3754
+ }
3755
+ /**
3756
+ * The gsd-core/ spec-tree copy (`copyWithPathReplacement` in bin/install.js): the
3757
+ * generic Claude / Qwen / Hermes source forms to the install prefix, in one pass.
3758
+ */
3759
+ function rewriteSpecTreeConfigDirForms(content, pathPrefix, dirName) {
3760
+ const b = configDirRuleBuilders(pathPrefix, pathPrefix.replace(/\/$/, ''), dirName);
3761
+ return rewriteSourceFormsOnce(content, [
3762
+ ...b.slash('.claude'),
3763
+ ...b.localSlash('.claude'),
3764
+ ...b.slash('.qwen'),
3765
+ ...b.localSlash('.qwen'),
3766
+ ...b.slash('.hermes'),
3767
+ ...b.localSlash('.hermes'),
3768
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3769
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3770
+ ]);
3771
+ }
3772
+ /**
3773
+ * The OpenCode / Kilo commands and skills path pass: `~/.claude/`, `$HOME/.claude/` and the
3774
+ * runtimes' own `~/.opencode/` / `~/.kilo/` to the install prefix, `./.claude/` to the
3775
+ * runtime's local dir, in ONE pass with literal replacement (a prefix is never rescanned,
3776
+ * and `$&` in a config dir is not expanded).
3777
+ */
3778
+ function rewriteOpencodeFamilyConfigDirForms(content, pathPrefix, dirName) {
3779
+ const b = configDirRuleBuilders(pathPrefix, pathPrefix.replace(/\/$/, ''), dirName);
3780
+ return rewriteSourceFormsOnce(content, [
3781
+ ...b.slash('.claude'),
3782
+ ...b.localSlash('.claude'),
3783
+ ...b.tilde('.opencode'),
3784
+ ...b.tilde('.kilo'),
3785
+ ]);
3632
3786
  }
3633
3787
  function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = false, attribution = undefined) {
3634
3788
  const dirName = getDirName(runtime);
@@ -3645,95 +3799,105 @@ function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = fal
3645
3799
  if (!rewriteBehaviors.skipRuntimeDefaultsStamp) {
3646
3800
  content = _stampNonClaudeRuntimeDefaults(content, runtime);
3647
3801
  }
3802
+ const b = configDirRuleBuilders(pathPrefix, normalizedPathPrefix, dirName);
3803
+ const LOOKAHEAD = '(?![\\w-])';
3804
+ const WORD_EDGE = '\\b';
3648
3805
  switch (rewriteBehaviors.contentRewriteProfile) {
3649
3806
  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);
3807
+ content = rewriteSourceFormsOnce(content, [
3808
+ ...b.slash('.claude'),
3809
+ ...b.localSlash('.claude'),
3810
+ ...b.tilde('.codex'),
3811
+ ]);
3654
3812
  // #1515 stamp moved to _stampNonClaudeRuntimeDefaults (#1521 generalisation).
3655
3813
  content = processAttribution(content, attribution);
3656
3814
  break;
3657
3815
  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);
3816
+ content = rewriteSourceFormsOnce(content, [
3817
+ ...b.slash('.claude'),
3818
+ ...b.localSlash('.claude'),
3819
+ ...b.slash('.cline'),
3820
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3821
+ ...b.bare('.cline', WORD_EDGE),
3822
+ ]);
3667
3823
  content = processAttribution(content, attribution);
3668
3824
  break;
3669
3825
  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);
3826
+ content = rewriteSourceFormsOnce(content, [
3827
+ ...b.slash('.claude'),
3828
+ ...b.localSlash('.claude'),
3829
+ ...b.tilde('.cursor'),
3830
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3831
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3832
+ ]);
3677
3833
  content = processAttribution(content, attribution);
3678
3834
  break;
3679
3835
  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
- }
3836
+ content = rewriteSourceFormsOnce(content, [
3837
+ ...b.slash('.claude'),
3838
+ ...b.localSlash('.claude'),
3839
+ ...b.tilde('.codeium/windsurf'),
3840
+ // The `~/` and `$HOME/` `.devin/skills/` forms become the prefix followed by
3841
+ // `skills/` (no stray `~/`, no doubled `$HOME/`). The `./` form keeps the text
3842
+ // before `.devin` as the pre-#5715 sequential rewrite left it. They sit before
3843
+ // the bare `.devin` rules so the longer match wins at the same position.
3844
+ ...(isGlobal
3845
+ ? [
3846
+ { pattern: '~\\/\\.devin\\/skills\\/', replacement: pathPrefix + 'skills/' },
3847
+ { pattern: '\\$HOME\\/\\.devin\\/skills\\/', replacement: pathPrefix + 'skills/' },
3848
+ { pattern: '\\.\\/\\.devin\\/skills\\/', replacement: './' + pathPrefix + 'skills/' },
3849
+ { pattern: '\\.devin\\/skills\\/', replacement: pathPrefix + 'skills/' },
3850
+ ...b.localSlash('.devin', pathPrefix),
3851
+ ]
3852
+ : []),
3853
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3854
+ ...(isGlobal ? b.bare('.devin', LOOKAHEAD) : []),
3855
+ ]);
3692
3856
  content = processAttribution(content, attribution);
3693
3857
  break;
3694
3858
  }
3695
3859
  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
3860
  // #2097: dot-dir self-references (~/.augment/…) → resolved prefix,
3703
3861
  // dirName-derived (no runtime literal). getDirName('augment') resolves
3704
3862
  // 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);
3863
+ content = rewriteSourceFormsOnce(content, [
3864
+ ...b.slash('.claude'),
3865
+ ...b.localSlash('.claude'),
3866
+ ...b.slash(dirName),
3867
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3868
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3869
+ ...b.bare(dirName, LOOKAHEAD),
3870
+ ]);
3710
3871
  content = processAttribution(content, attribution);
3711
3872
  break;
3712
3873
  }
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}`);
3874
+ case 'slash-bare-own-derived': {
3720
3875
  // #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);
3876
+ // getDirName()/localConfigDir, so the own-dir rule is built rather than
3877
+ // hardcoded. #5715: own-dir slash and bare forms (both `~/` and `$HOME/`
3878
+ // spellings) map to the install prefix, so a custom or local config dir is
3879
+ // honoured; default global output is unchanged.
3880
+ const keepDefaultOwnBare = (match) => (normalizedPathPrefix === '$HOME/' + dirName ? match : normalizedPathPrefix);
3881
+ content = rewriteSourceFormsOnce(content, [
3882
+ ...b.slash('.claude'),
3883
+ ...b.localSlash('.claude'),
3884
+ ...b.slash(dirName),
3885
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3886
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3887
+ ...b.bare(dirName, CLAUDE_HOME_BARE_BOUNDARY, keepDefaultOwnBare),
3888
+ ]);
3724
3889
  content = processAttribution(content, attribution);
3725
3890
  break;
3891
+ }
3726
3892
  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);
3893
+ content = rewriteSourceFormsOnce(content, [
3894
+ ...b.slash('.claude'),
3895
+ ...b.localSlash('.claude'),
3896
+ ...b.slash('.codebuddy'),
3897
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3898
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3899
+ ...b.bare('.codebuddy', WORD_EDGE),
3900
+ ]);
3737
3901
  content = processAttribution(content, attribution);
3738
3902
  break;
3739
3903
  case 'attribution-only':
@@ -3747,11 +3911,14 @@ function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = fal
3747
3911
  // #5501: the slash AND bare forms in ONE pass. The launcher default
3748
3912
  // `${CLAUDE_CONFIG_DIR:-$HOME/.claude}` has no trailing slash, so a slash-only
3749
3913
  // 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}/`);
3914
+ // pass (#5715: also `./.claude/`) so a prefix the pass emits is never
3915
+ // rescanned. A default install keeps the source spelling of the bare form
3916
+ // (`~/.claude` stays `~/.claude`) so it stays byte-identical.
3917
+ content = rewriteSourceFormsOnce(content, [
3918
+ ...b.slash('.claude'),
3919
+ ...b.localSlash('.claude'),
3920
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY, (match) => (normalizedPathPrefix === '$HOME/.claude' ? match : normalizedPathPrefix)),
3921
+ ]);
3755
3922
  // #3133 / #3544: restore @-file-reference lines to the tilde form
3756
3923
  // Claude actually expands — see restoreClaudeGlobalAtRefTilde's doc
3757
3924
  // comment above for why this must be a separate, @-anchored pass. Handed
@@ -3766,19 +3933,18 @@ function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = fal
3766
3933
  // (the gsd-surface skill) that need the negative-lookahead bare rewrites the
3767
3934
  // claude profile deliberately does not have (it would change claude's bytes).
3768
3935
  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}`);
3936
+ content = rewriteSourceFormsOnce(content, [
3937
+ ...b.slash('.claude'),
3938
+ ...b.localSlash('.claude'),
3939
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3940
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3941
+ ]);
3775
3942
  content = restoreClaudeGlobalAtRefTilde(content, pathPrefix);
3776
3943
  content = processAttribution(content, attribution);
3777
3944
  break;
3778
3945
  // Descriptor-driven brand literals (ADR-1239 / #2092): the qwen/hermes
3779
3946
  // brand VALUES (CLAUDE.md/Claude Code/.claude/ replacements) now read from
3780
3947
  // runtime.hostBehaviors.brandingRewrites instead of hardcoded literals.
3781
- // EXACT regexes/order preserved — only the replacement values changed.
3782
3948
  // qwen and hermes share this profile; their own dot-dir is derived from the
3783
3949
  // descriptor's `localConfigDir` (`getDirName`) rather than spelled per host.
3784
3950
  case 'branded-lookahead-own-derived': {
@@ -3787,35 +3953,32 @@ function _applyRuntimeRewritesInner(content, runtime, pathPrefix, isGlobal = fal
3787
3953
  // brand-literal replacements, still apply the non-branding path
3788
3954
  // rewrites below) instead of throwing on `_b['CLAUDE.md']`.
3789
3955
  const _b = rewriteBehaviors.brandingRewrites;
3790
- const _own = (0, pattern_cjs_1.escapeRegex)(dirName);
3791
3956
  if (_b) {
3792
3957
  content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
3793
3958
  // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
3794
3959
  content = applyClaudeCodeBrandSwap(content, _b['Claude Code']);
3795
3960
  }
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}/`);
3961
+ content = rewriteSourceFormsOnce(content, [
3962
+ ...b.slash('.claude'),
3963
+ ...b.slash(dirName),
3964
+ ...b.localSlash('.claude', _b ? './' + _b['.claude/'] : undefined),
3965
+ ...b.localSlash(dirName),
3966
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3967
+ ...b.bare(dirName, LOOKAHEAD),
3968
+ // Lowest priority: every other `.claude/` in the source takes the brand form.
3969
+ // In the SAME pass, so the prefix emitted above is never rescanned.
3970
+ ...(_b ? [{ pattern: '\\.claude\\/', replacement: () => _b['.claude/'] }] : []),
3971
+ ]);
3809
3972
  content = processAttribution(content, attribution);
3810
3973
  break;
3811
3974
  }
3812
3975
  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}`);
3976
+ content = rewriteSourceFormsOnce(content, [
3977
+ ...b.slash('.claude'),
3978
+ ...b.localSlash('.claude'),
3979
+ ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3980
+ ...b.localBare('.claude', CLAUDE_HOME_BARE_BOUNDARY),
3981
+ ]);
3819
3982
  content = processAttribution(content, attribution);
3820
3983
  break;
3821
3984
  default:
@@ -3887,7 +4050,8 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
3887
4050
  // from runtime.hostBehaviors instead of a hardcoded runtime-name branch.
3888
4051
  const _cmdConv = hostBehaviorsFor(runtime).commandBodyConverter;
3889
4052
  if (_cmdConv && COMMAND_BODY_CONVERTERS[_cmdConv]) {
3890
- content = COMMAND_BODY_CONVERTERS[_cmdConv](content);
4053
+ // #5715: the converter runs after the prefix pass, so the emitted prefix is shielded.
4054
+ content = shieldEmittedPrefix(content, pathPrefix, COMMAND_BODY_CONVERTERS[_cmdConv]);
3891
4055
  }
3892
4056
  installFs().writeFileSync(node_path_1.default.join(tempDir, entry.name), content);
3893
4057
  }
@@ -4069,18 +4233,29 @@ function applyAgentPathRewrites(content, runtime, pathPrefix) {
4069
4233
  }
4070
4234
  return applyAgentPathRewritesInner(content, runtime, pathPrefix);
4071
4235
  }
4236
+ /**
4237
+ * #5715: run an agent converter over content whose config-dir forms `applyAgentPathRewrites`
4238
+ * has already rewritten, with the emitted prefix shielded from the converter. A runtime that
4239
+ * declares `noPathRewrite` emitted no prefix, so nothing is shielded.
4240
+ */
4241
+ function shieldAgentConverterPrefix(content, runtime, pathPrefix, convert) {
4242
+ if (hostBehaviorsFor(runtime).noPathRewrite === true)
4243
+ return convert(content);
4244
+ // The runtime whose own dot-dir is `.trae` resolves the instruction file at the emitted
4245
+ // prefix (see resolveTraeInstructionFileAtPrefix) before the prefix is hidden.
4246
+ const resolved = getDirName(runtime) === '.trae' ? resolveTraeInstructionFileAtPrefix(content, pathPrefix) : content;
4247
+ return shieldEmittedPrefix(resolved, pathPrefix, convert);
4248
+ }
4072
4249
  function applyAgentPathRewritesInner(content, runtime, pathPrefix) {
4073
4250
  const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
4074
- content = content.replace(/~\/\.claude\//g, pathPrefix);
4075
- content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
4076
4251
  // #3719 review (MAJOR): a bare `\b` is satisfied by ANY non-word character,
4077
4252
  // including '-' — for a --config-dir whose name EXTENDS '.claude' (e.g.
4078
4253
  // '.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);
4254
+ // '.claude-work' path and corrupted it to '.claude-work-work'. #5715: the
4255
+ // slash and bare forms are one single-pass rewrite, so no emitted prefix is
4256
+ // ever rescanned (a nested or `.claude.`-named config dir stays intact).
4257
+ const b = configDirRuleBuilders(pathPrefix, normalizedPathPrefix, '');
4258
+ content = rewriteSourceFormsOnce(content, [...b.slash('.claude'), ...b.bare('.claude', CLAUDE_HOME_BARE_BOUNDARY)]);
4084
4259
  // #3719: the THIRD emit path that needed this restore. #3133 added it to the
4085
4260
  // skill/command pipeline (`_applyRuntimeRewrites` case 'claude') and #3544 to
4086
4261
  // bin/install.js's spec-tree copy; the agents pipeline never got it, so every
@@ -4379,6 +4554,11 @@ module.exports = {
4379
4554
  _localIncludeDirName: localIncludeDirName,
4380
4555
  _restoreClaudeGlobalAtRefTilde: restoreClaudeGlobalAtRefTilde,
4381
4556
  _applyRuntimeRewrites,
4557
+ _rewriteSpecTreeConfigDirForms: rewriteSpecTreeConfigDirForms,
4558
+ _rewriteOpencodeFamilyConfigDirForms: rewriteOpencodeFamilyConfigDirForms,
4559
+ _shieldAgentConverterPrefix: shieldAgentConverterPrefix,
4560
+ _resolveTraeInstructionFileAtPrefix: resolveTraeInstructionFileAtPrefix,
4561
+ _shieldEmittedPrefix: shieldEmittedPrefix,
4382
4562
  _stampNonClaudeRuntimeDefaults,
4383
4563
  // #2652: registry-resolved dispatch isolation, mirroring routeDispatchIsolation
4384
4564
  _negotiatedDispatchIsolation,
@@ -565,6 +565,12 @@ function convertedAgentsKind(destSubpath, prefix, converterName, configDir, sour
565
565
  // (content) ignore the extra positional arg. Mirrors skillsKind's scope
566
566
  // threading (#1173).
567
567
  converter = (content) => rawConverter(content, (0, install_scope_cjs_1.isGlobalScope)(scope));
568
+ if (converterName === 'convertClaudeAgentToTraeAgent') {
569
+ // #5715: the Trae agent converter needs the prefix the path-rewrite step
570
+ // already emitted, so it can leave it alone.
571
+ const traeConverter = rawConverter;
572
+ converter = (content, _isGlobal, meta) => traeConverter(content, (0, install_scope_cjs_1.isGlobalScope)(scope), meta);
573
+ }
568
574
  }
569
575
  // ADR-1235 §1: when agentCtx is provided (by createRuntimeArtifactInstallPlan
570
576
  // for descriptor-driven runtimes), thread it through so stageAgentsForRuntimeWithConverter