@opengsd/gsd-core 1.20.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/bin/install.js +36 -40
- package/commands/gsd/new-milestone.md +2 -2
- package/commands/gsd/new-project.md +2 -2
- package/gsd-core/bin/gsd-tools.cjs +104 -70
- package/gsd-core/bin/lib/capability-registry.cjs +64 -64
- package/gsd-core/bin/lib/commands.cjs +19 -0
- package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
- package/gsd-core/bin/lib/init.cjs +7 -7
- package/gsd-core/bin/lib/install-engine.cjs +9 -9
- package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
- package/gsd-core/bin/lib/install-profiles.cjs +4 -2
- package/gsd-core/bin/lib/phase-id.cjs +26 -9
- package/gsd-core/bin/lib/phase.cjs +64 -15
- package/gsd-core/bin/lib/planning-snapshot.cjs +2 -2
- package/gsd-core/bin/lib/roadmap-parser.cjs +29 -0
- package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -130
- package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
- package/gsd-core/bin/lib/state-transition.cjs +12 -5
- package/gsd-core/bin/lib/state.cjs +2 -2
- package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
- package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
- package/hooks/dist/gsd-agent-isolation-guard.js +1 -1
- package/hooks/dist/gsd-cursor-subagent-start.js +1 -1
- package/hooks/dist/lib/isolation-sentinel.js +77 -2
- package/hooks/gsd-agent-isolation-guard.js +1 -1
- package/hooks/gsd-cursor-subagent-start.js +1 -1
- package/hooks/lib/isolation-sentinel.js +77 -2
- package/package.json +1 -1
- package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
- package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
- package/scripts/changeset/lint.cjs +28 -3
- package/scripts/lib/macos-conformance-tier.generated.cjs +4 -0
- package/scripts/lib/platform-conformance-tier.generated.cjs +4 -0
- package/scripts/merge-commit-pr.cjs +186 -0
- package/scripts/release-ancestry.cjs +86 -0
- package/scripts/release-pipeline-pr.cjs +92 -0
- package/scripts/require-issue-link-policy.cjs +18 -9
- package/scripts/sync-next-version.cjs +11 -2
- package/skills/gsd-new-milestone/SKILL.md +1 -1
- package/skills/gsd-new-project/SKILL.md +1 -1
- package/vscode/package.json +1 -1
|
@@ -1733,7 +1733,83 @@ function convertSlashCommandsToTraeSkillMentions(content) {
|
|
|
1733
1733
|
return `/gsd-${commandName}`;
|
|
1734
1734
|
});
|
|
1735
1735
|
}
|
|
1736
|
-
|
|
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 .
|
|
1797
|
-
|
|
1798
|
-
converted = converted.replace(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3613
|
-
*
|
|
3614
|
-
*
|
|
3615
|
-
*
|
|
3616
|
-
*
|
|
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
|
|
3622
|
-
function
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
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
|
|
3651
|
-
|
|
3652
|
-
|
|
3653
|
-
|
|
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
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
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
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
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
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
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
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
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
|
|
3722
|
-
// hardcoded
|
|
3723
|
-
|
|
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
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
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
|
-
//
|
|
3751
|
-
// rescanned
|
|
3752
|
-
//
|
|
3753
|
-
content =
|
|
3754
|
-
|
|
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
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
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
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
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
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
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
|
-
|
|
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'.
|
|
4080
|
-
//
|
|
4081
|
-
//
|
|
4082
|
-
|
|
4083
|
-
content = content.
|
|
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
|