fix(#2658): detect trae runtime and resolve its instruction file to a concrete rules file (#3006)

* test(#2658): add failing-first regression for trae runtime detection and instruction path

Covers all three collided defects reported in #2658 plus a fourth
instance of defect 1 (ingest-docs.md) found while diagnosing it:
missing trae detection in workflow runtime-detection blocks, the
CLAUDE.md path-mutilation bug in both the js/cjs and md install-time
converters, and the missing projectInstructionFile capability
declaration. Fails against current source; the next commit fixes it.

* fix(#2658): detect trae runtime and resolve its instruction file to a concrete rules file

Three defects collided to produce the reported ".claude/.trae/rules/"
path:

1. new-project.md and ingest-docs.md's runtime-detection blocks only
   recognized codex/gemini/opencode and fell through to RUNTIME=claude
   for trae. Both now recognize the /.trae/ execution-context path and
   the TRAE_CONFIG_DIR env var before the claude fallback.
2. RUNTIME_CONTENT_DISPATCH.trae.js (bin/install.js) replaced bare
   "CLAUDE.md" before the ".claude/" prefix was handled, mutilating
   ".claude/CLAUDE.md" into ".claude/.trae/rules/". Now replaces the
   full ".claude/CLAUDE.md" path first, and targets a concrete file.
3. convertClaudeToTraeMarkdown (mirrored in bin/install.js and
   src/runtime-artifact-conversion.cts per the #2094 output-parity
   test) had the same class of bug with a different wrong output
   (".trae/.trae/rules/", from its generic ".claude/" rewrite firing
   after the bare CLAUDE.md rewrite). Both mirrors now match full-path
   forms before the bare/generic patterns, converging on the same
   concrete file as the js/cjs converter.
4. capabilities/trae/capability.json didn't declare
   hostBehaviors.projectInstructionFile, so getProjectInstructionFile
   fell through to the generic AGENTS.md default even when RUNTIME=trae
   was resolved correctly. Now declares ".trae/rules/rules.md",
   regenerated into gsd-core/bin/lib/capability-registry.cjs via
   npm run gen:capability-registry.

Closes #2658

* chore(#2658): add changeset

* fix(#2658): preserve arbitrary runtime-dir prefixes in the trae path rewrite

Found by the end-to-end --trae install regression test (not by static
trace) across two verification runs:

1. copyWithPathReplacement runs a generic ~/.claude/, $HOME/.claude/,
   and ./.claude/ -> runtime-dir rewrite on every .md file BEFORE
   calling convertClaudeToTraeMarkdown. The prior fix's
   .claude/CLAUDE.md-specific patterns never fire on that
   already-rewritten text, and the bare fallback still doubled
   whatever prefix the generic pass substituted. A first attempt
   handled only the fixed "./.trae/" shape and missed the
   $HOME/.claude/ and ~/.claude/ forms gsd-core/workflows/profile-user.md
   actually uses, which post-rewrite become an arbitrary absolute
   local-install-root path, not the fixed relative shape. Fixed with a
   prefix-preserving pattern that captures whatever precedes a
   ".trae/" tail and fixes only the filename suffix, instead of
   assuming one fixed shape.

2. The fix's own explanatory comments literally spelled out the
   malformed strings and the instruction filename as contiguous text.
   Since these two files ship verbatim into local --trae installs,
   where they are themselves run through the same find/replace, the
   comments got "fixed" right along with the real code, leaking the
   malformed string into the installed tree. Rewrote every comment in
   both mirror copies to never spell either the instruction filename
   or a malformed shape as one contiguous token.

Adds an emitted-drift-ack fragment: the corrected replacement target
for every CLAUDE.md mention (bare directory -> concrete file) changes
trae-emitted output for every repo file that mentions CLAUDE.md, not
only the ones that hit the originally reported bug.

* test(#2658): extend parity test with arbitrary-prefix .trae/ inputs

The bin/install.js vs runtime-artifact-conversion.cjs parity assertion
for convertClaudeToTraeMarkdown only fed the pre-existing bare
.claude/CLAUDE.md input through both implementations. Feed the
prefix-preserving cases (relative, nested-absolute, tilde, $HOME,
backtick-wrapped) plus a property-based check through both, so a
future edit to only one copy of the .trae/-tail regex fails this
test instead of silently diverging.

* chore(#2658): backfill changeset PR number to 3006

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-02 18:29:47 -04:00
committed by GitHub
parent d770365753
commit 51f32d2d40
9 changed files with 410 additions and 13 deletions

View File

@@ -1311,10 +1311,55 @@ function convertClaudeToTraeMarkdown(content) {
// Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/`');
converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/');
converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/`');
converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/');
// #2658: full-path forms (with a leading dot-claude-slash prefix) MUST be
// replaced before the bare Claude-instruction-file pattern and before the
// generic dot-claude-slash rewrite below — otherwise the bare pattern
// consumes only the instruction-filename tail, leaving that prefix stale
// in place, and the generic rewrite then mutates the stale leftover too,
// producing a doubled trae-prefix segment ahead of the rules path instead
// of a single clean one. (Deliberately never spelling the instruction
// filename as one contiguous "CLAUDE" + dot + "md" token, and never
// spelling either malformed shape out as a literal contiguous string, in
// ANY comment in this function: this file ships verbatim into local
// `--trae` installs, where it is itself run through this same class of
// find/replace — a literal instruction-filename token sitting in a
// comment gets "fixed" right along with real code, and the emitted-content
// regression test added alongside this fix asserts neither malformed
// shape appears anywhere in the installed tree, comments included; this
// bit the fix itself twice during development.) All forms converge on the
// same concrete file (never a bare directory) so this stays in parity
// with the `trae.js` RUNTIME_CONTENT_DISPATCH entry.
converted = converted.replace(/`\.\/\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
converted = converted.replace(/\.\/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
converted = converted.replace(/`\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
converted = converted.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
// #2658 (found via the end-to-end install regression test, not the static
// trace above): `copyWithPathReplacement` runs a GENERIC dot-claude-slash
// -> runtime-config-dir rewrite on every .md file before calling this
// converter — for `~/.claude/`, `$HOME/.claude/`, AND `./.claude/` alike —
// substituting a runtime-appropriate `pathPrefix` this function is never
// given and cannot itself compute (it differs per install invocation: a
// relative `./.trae/` for a project-local install, an arbitrary absolute
// path for a local install rooted elsewhere, `~/.trae/` for a global one).
// So for source using any of those prefixed forms, the patterns above
// never fire here — this converter only ever sees the ALREADY-rewritten
// "<runtime-config-dir>/" + instruction-filename shape, with whatever
// prefix the install actually used. The generic pattern below preserves
// that prefix verbatim (via the capture group) and only fixes the
// filename suffix, rather than assuming a fixed `./.trae/` shape — a
// narrower fixed-prefix version of this pattern shipped first and still
// left the doubled-prefix defect live for the `$HOME/.claude/` and
// `~/.claude/` forms specifically (found the same way, one regression-test
// run later). Scoped to a `.trae/` tail so it cannot also swallow the
// unprefixed `./CLAUDE.md` form the very next pattern handles differently
// (discarding the prefix entirely, not preserving it). Must run before
// the bare pattern for the same consume-the-full-match-first reason.
converted = converted.replace(/`([^\s`]*\.trae\/)CLAUDE\.md`/g, '`$1rules/rules.md`');
converted = converted.replace(/([^\s`]*\.trae\/)CLAUDE\.md/g, '$1rules/rules.md');
converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/rules.md');
converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/rules.md`');
converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/rules.md');
converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
converted = converted.replace(/\.\/\.claude\//g, './.trae/');
converted = converted.replace(/\.claude\//g, '.trae/');