* 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:
5
.changeset/jolly-lynx-run.md
Normal file
5
.changeset/jolly-lynx-run.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
type: Fixed
|
||||
pr: 3006
|
||||
---
|
||||
**Trae IDE is now detected as its own runtime** — `/gsd-new-project` and `/gsd-ingest-docs` no longer fall through to the Claude default when run inside Trae, and a `--trae` install no longer writes a malformed `.claude/.trae/rules/` or `.trae/.trae/rules/` instruction-file path; it now resolves to the concrete `.trae/rules/rules.md`. (#2658)
|
||||
@@ -2701,10 +2701,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/');
|
||||
@@ -7525,7 +7570,18 @@ const RUNTIME_CONTENT_DISPATCH = {
|
||||
return `/gsd-${commandName}`;
|
||||
});
|
||||
content = content.replace(/\.claude\/skills\//g, '.trae/skills/');
|
||||
content = content.replace(/CLAUDE\.md/g, '.trae/rules/');
|
||||
// #2658: the full dot-claude-slash-prefixed instruction-file path must
|
||||
// be replaced before the bare instruction-filename fallback, or the
|
||||
// bare regex only rewrites that filename and leaves the prefix stale
|
||||
// in place, producing a malformed doubled-prefix path (see the longer
|
||||
// note in convertClaudeToTraeMarkdown above — the instruction filename
|
||||
// and either malformed shape are deliberately never spelled out
|
||||
// contiguously here either, for the same reason: this file ships
|
||||
// verbatim). Both forms target the same concrete file (never a bare
|
||||
// directory), matching the `.md` converter (convertClaudeToTraeMarkdown)
|
||||
// so js/cjs and md content agree on one canonical path.
|
||||
content = content.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
|
||||
content = content.replace(/CLAUDE\.md/g, '.trae/rules/rules.md');
|
||||
content = content.replace(/\bClaude Code\b/g, 'Trae');
|
||||
return content;
|
||||
},
|
||||
|
||||
@@ -86,7 +86,8 @@
|
||||
},
|
||||
"hostBehaviors": {
|
||||
"skipSharedHooksInstall": true,
|
||||
"soloStageMetadata": "workflow"
|
||||
"soloStageMetadata": "workflow",
|
||||
"projectInstructionFile": ".trae/rules/rules.md"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3384,7 +3384,8 @@ const capabilities = {
|
||||
},
|
||||
"hostBehaviors": {
|
||||
"skipSharedHooksInstall": true,
|
||||
"soloStageMetadata": "workflow"
|
||||
"soloStageMetadata": "workflow",
|
||||
"projectInstructionFile": ".trae/rules/rules.md"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -6612,7 +6613,8 @@ const runtimes = {
|
||||
},
|
||||
"hostBehaviors": {
|
||||
"skipSharedHooksInstall": true,
|
||||
"soloStageMetadata": "workflow"
|
||||
"soloStageMetadata": "workflow",
|
||||
"projectInstructionFile": ".trae/rules/rules.md"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -84,9 +84,10 @@ git init
|
||||
- execution_context path `/.codex/` → `RUNTIME=codex`
|
||||
- `/.gemini/` → `RUNTIME=gemini`
|
||||
- `/.opencode/` or `/.config/opencode/` → `RUNTIME=opencode`
|
||||
- `/.trae/` → `RUNTIME=trae`
|
||||
- else → `RUNTIME=claude`
|
||||
|
||||
Fall back to env vars (`CODEX_HOME`, `GEMINI_CONFIG_DIR`, `OPENCODE_CONFIG_DIR`) if execution_context is unavailable.
|
||||
Fall back to env vars (`CODEX_HOME`, `GEMINI_CONFIG_DIR`, `OPENCODE_CONFIG_DIR`, `TRAE_CONFIG_DIR`) if execution_context is unavailable.
|
||||
|
||||
</step>
|
||||
|
||||
|
||||
@@ -101,6 +101,7 @@ Derive `RUNTIME` from the invoking prompt's `execution_context` path:
|
||||
- Path contains `/.codex/` → `RUNTIME=codex`
|
||||
- Path contains `/.gemini/` → `RUNTIME=gemini`
|
||||
- Path contains `/.config/opencode/` or `/.opencode/` → `RUNTIME=opencode`
|
||||
- Path contains `/.trae/` → `RUNTIME=trae`
|
||||
- Otherwise → `RUNTIME=claude`
|
||||
|
||||
If `execution_context` path is not available, fall back to env vars:
|
||||
@@ -108,6 +109,7 @@ If `execution_context` path is not available, fall back to env vars:
|
||||
if [ -n "$CODEX_HOME" ]; then RUNTIME="codex"
|
||||
elif [ -n "$GEMINI_CONFIG_DIR" ]; then RUNTIME="gemini"
|
||||
elif [ -n "$OPENCODE_CONFIG_DIR" ] || [ -n "$OPENCODE_CONFIG" ]; then RUNTIME="opencode"
|
||||
elif [ -n "$TRAE_CONFIG_DIR" ]; then RUNTIME="trae"
|
||||
else RUNTIME="claude"; fi
|
||||
```
|
||||
|
||||
|
||||
@@ -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/');
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"version": 1,
|
||||
"paths": {
|
||||
"gsd-core/references/checkpoints.md": "#2658: mentions CLAUDE.md in prose. convertClaudeToTraeMarkdown's CLAUDE.md replacement target changed from the bare directory '.trae/rules/' to the concrete file '.trae/rules/rules.md' for every CLAUDE.md mention (bare, ./-prefixed, backtick-wrapped, and the buggy .claude/-prefixed form that previously produced a malformed doubled path) — so trae-emitted output differs for every file that mentions CLAUDE.md, not only the ones that hit the reported bug. Content is otherwise byte-identical.",
|
||||
"gsd-core/references/debugger-bug-taxonomy.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/references/debugger-fix-acceptance.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/references/debugger-repro-hardening.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/references/debugger-sbfl.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/references/git-integration.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/references/planner-human-verify-mode.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/templates/README.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/templates/claude-md.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/templates/codebase/structure.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/execute-phase.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/execute-plan.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/help/modes/full.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/milestone-summary.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/plan-phase.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/profile-user.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/progress.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/quick.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/sketch-wrap-up.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/spike-wrap-up.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"gsd-core/workflows/update.md": "#2658: same CLAUDE.md replacement-target change as checkpoints.md above (bare directory '.trae/rules/' -> concrete file '.trae/rules/rules.md').",
|
||||
"skills/gsd-ns-project/skills/profile-user/SKILL.md": "#2658: derived (via commands/gsd/profile-user.md, which mentions CLAUDE.md) through convertClaudeToTraeMarkdown; same replacement-target change as checkpoints.md above.",
|
||||
"skills/gsd-ns-review/skills/code-review/SKILL.md": "#2658: derived (via commands/gsd/code-review.md, which mentions CLAUDE.md) through convertClaudeToTraeMarkdown; same replacement-target change as checkpoints.md above.",
|
||||
"ingest-docs.md": "#2658: runtime-detection block gained a `/.trae/` path-based line and a `TRAE_CONFIG_DIR` env-var fallback line (the same trae-detection gap found in new-project.md, fixed here too since it is the identical defect in a sibling workflow).",
|
||||
"new-project.md": "#2658: runtime-detection block gained a `/.trae/` path-based line and a `TRAE_CONFIG_DIR` env-var fallback line, so trae resolves to RUNTIME=trae instead of falling through to the claude default."
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,255 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Regression tests for #2658 — Trae runtime not detected in workflow
|
||||
* runtime-detection blocks (falls back to claude), and the install-time
|
||||
* `CLAUDE.md` path rewrite mutilates the claude fallback into a malformed
|
||||
* path instead of resolving to the Trae rules file.
|
||||
*
|
||||
* Defects collided (see
|
||||
* .gsd/bug/fix-2658-trae-runtime-not-detected-falls-back-to-/10-diagnosis.md):
|
||||
*
|
||||
* 1. `gsd-core/workflows/new-project.md` AND `gsd-core/workflows/ingest-docs.md`
|
||||
* (found during this remediation — same pattern, same gap, not just
|
||||
* new-project.md as originally reported) never recognized trae (path
|
||||
* `/.trae/` or env `TRAE_CONFIG_DIR`) in their runtime-detection blocks —
|
||||
* fell through to `RUNTIME=claude`.
|
||||
* 2. The `trae.js` entry in `bin/install.js`'s `RUNTIME_CONTENT_DISPATCH`
|
||||
* replaced bare `CLAUDE.md` first, leaving a stale `.claude/` prefix:
|
||||
* `.claude/CLAUDE.md` -> `.claude/.trae/rules/`.
|
||||
* 3. `convertClaudeToTraeMarkdown` (mirrored in `bin/install.js` and
|
||||
* `src/runtime-artifact-conversion.cts`) had the same class of bug but a
|
||||
* DIFFERENT wrong output (`.trae/.trae/rules/`), because its generic
|
||||
* `.claude/` -> `.trae/` rewrite ran after the bare `CLAUDE.md` rewrite
|
||||
* and re-mutated the leftover prefix.
|
||||
* 4. `capabilities/trae/capability.json` didn't declare
|
||||
* `hostBehaviors.projectInstructionFile`, so even a correctly-detected
|
||||
* trae runtime resolved to the generic `AGENTS.md` default via
|
||||
* `getProjectInstructionFile`.
|
||||
* 5. Found by the end-to-end install test below, one level deeper than the
|
||||
* static trace: `copyWithPathReplacement` (bin/install.js) runs a
|
||||
* GENERIC `~/.claude/` / `$HOME/.claude/` / `./.claude/` -> runtime-dir
|
||||
* rewrite on every .md file BEFORE calling `convertClaudeToTraeMarkdown`,
|
||||
* substituting a `pathPrefix` the converter is never given (it differs
|
||||
* per install: relative for a project-local install, an arbitrary
|
||||
* absolute path for a local install rooted elsewhere, `~/.trae/` for a
|
||||
* global one). The converter's `.claude/CLAUDE.md`-specific patterns
|
||||
* (defect 3's fix) never fire on that already-rewritten text, and the
|
||||
* bare fallback still doubles the prefix — a first attempt at fixing
|
||||
* this handled only the `./.trae/CLAUDE.md` shape and missed the
|
||||
* `~/.claude/` / `$HOME/.claude/` forms `gsd-core/workflows/profile-user.md`
|
||||
* actually uses, caught by row 12 (the real spawned install) below on a
|
||||
* second run. Fixed with a prefix-preserving pattern (capture whatever
|
||||
* precedes a `.trae/` tail, keep it, fix only the filename suffix)
|
||||
* instead of assuming one fixed shape.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const fc = require('fast-check');
|
||||
|
||||
process.env['GSD_TEST_MODE'] = '1';
|
||||
|
||||
const { getProjectInstructionFile } = require('../gsd-core/bin/lib/runtime-name-policy.cjs');
|
||||
const { convertClaudeToTraeMarkdown } = require('../bin/install.js');
|
||||
const runtimeArtifactConversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
|
||||
|
||||
const { runMinimalInstall, walk } = require('./helpers/install-shared.cjs');
|
||||
const { cleanup } = require('./helpers.cjs');
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, '..');
|
||||
const MALFORMED_SINGLE = '.claude/.trae/rules';
|
||||
const MALFORMED_DOUBLE = '.trae/.trae/rules';
|
||||
const EXPECTED_PATH = '.trae/rules/rules.md';
|
||||
|
||||
describe('#2658 acceptance criterion 2: getProjectInstructionFile resolves trae to a concrete file', () => {
|
||||
test('trae maps to .trae/rules/rules.md (not the generic AGENTS.md default)', () => {
|
||||
assert.strictEqual(getProjectInstructionFile('trae'), EXPECTED_PATH);
|
||||
});
|
||||
|
||||
test('capability descriptor declares the same path getProjectInstructionFile returns', () => {
|
||||
const cap = JSON.parse(
|
||||
fs.readFileSync(path.join(REPO_ROOT, 'capabilities', 'trae', 'capability.json'), 'utf8'),
|
||||
);
|
||||
assert.strictEqual(cap.runtime.hostBehaviors.projectInstructionFile, EXPECTED_PATH);
|
||||
assert.strictEqual(getProjectInstructionFile('trae'), cap.runtime.hostBehaviors.projectInstructionFile);
|
||||
});
|
||||
|
||||
test('the declared path is a concrete file, not a bare directory (acceptance criterion 2)', () => {
|
||||
assert.ok(!EXPECTED_PATH.endsWith('/'), 'must not be directory-terminated');
|
||||
assert.ok(/\.md$/.test(EXPECTED_PATH), 'must name a concrete markdown file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2658: convertClaudeToTraeMarkdown never mutilates the CLAUDE.md path (bin/install.js)', () => {
|
||||
const cases = [
|
||||
['bare CLAUDE.md', 'See CLAUDE.md for details.'],
|
||||
['./CLAUDE.md', 'Read ./CLAUDE.md before starting.'],
|
||||
['backtick-wrapped `CLAUDE.md`', 'The file `CLAUDE.md` is authoritative.'],
|
||||
['the exact reported-bug input: .claude/CLAUDE.md', 'Fallback path is .claude/CLAUDE.md by default.'],
|
||||
['backtick-wrapped .claude/CLAUDE.md', 'Fallback: `.claude/CLAUDE.md`.'],
|
||||
['./.claude/CLAUDE.md', 'From root: ./.claude/CLAUDE.md'],
|
||||
];
|
||||
for (const [label, input] of cases) {
|
||||
test(`${label} -> ${EXPECTED_PATH}, no malformed output`, () => {
|
||||
const out = convertClaudeToTraeMarkdown(input);
|
||||
assert.ok(!out.includes(MALFORMED_SINGLE), `output must not contain "${MALFORMED_SINGLE}": ${out}`);
|
||||
assert.ok(!out.includes(MALFORMED_DOUBLE), `output must not contain "${MALFORMED_DOUBLE}": ${out}`);
|
||||
assert.ok(out.includes(EXPECTED_PATH), `output must contain "${EXPECTED_PATH}": ${out}`);
|
||||
});
|
||||
}
|
||||
|
||||
test('fast-check property: any surrounding text around .claude/CLAUDE.md never yields a malformed path', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.string({ maxLength: 40 }),
|
||||
fc.string({ maxLength: 40 }),
|
||||
(prefix, suffix) => {
|
||||
const content = `${prefix}.claude/CLAUDE.md${suffix}`;
|
||||
const out = convertClaudeToTraeMarkdown(content);
|
||||
assert.ok(!out.includes(MALFORMED_SINGLE));
|
||||
assert.ok(!out.includes(MALFORMED_DOUBLE));
|
||||
},
|
||||
),
|
||||
{ numRuns: 200 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2658 defect 5: post-generic-rewrite ".trae/"-prefixed forms preserve their prefix instead of doubling it', () => {
|
||||
// These simulate the text `copyWithPathReplacement`'s generic `~/.claude/` /
|
||||
// `$HOME/.claude/` / `./.claude/` -> runtime-dir pass hands to
|
||||
// convertClaudeToTraeMarkdown — the converter never sees the original
|
||||
// `.claude/`-prefixed source in this pipeline, only these already-rewritten
|
||||
// shapes. A fixed-shape patch that only handled the local relative form
|
||||
// left the local-install-absolute-path and global tilde forms broken.
|
||||
const cases = [
|
||||
['local relative (post "./.claude/" -> "./.trae/" rewrite)', './.trae/CLAUDE.md', './.trae/rules/rules.md'],
|
||||
[
|
||||
'local install absolute path (post "./.claude/" -> "<tmp-root>/.trae/" rewrite)',
|
||||
'/private/var/folders/xx/gsd-trae-local-abc123/.trae/CLAUDE.md',
|
||||
'/private/var/folders/xx/gsd-trae-local-abc123/.trae/rules/rules.md',
|
||||
],
|
||||
['global tilde (post "~/.claude/" -> "~/.trae/" rewrite)', '~/.trae/CLAUDE.md', '~/.trae/rules/rules.md'],
|
||||
['backtick-wrapped local relative', '`./.trae/CLAUDE.md`', '`./.trae/rules/rules.md`'],
|
||||
];
|
||||
for (const [label, input, expected] of cases) {
|
||||
test(`${label} -> prefix preserved, no malformed path`, () => {
|
||||
const out = convertClaudeToTraeMarkdown(input);
|
||||
assert.ok(!out.includes(MALFORMED_SINGLE), `output must not contain "${MALFORMED_SINGLE}": ${out}`);
|
||||
assert.ok(!out.includes(MALFORMED_DOUBLE), `output must not contain "${MALFORMED_DOUBLE}": ${out}`);
|
||||
assert.strictEqual(out, expected);
|
||||
});
|
||||
}
|
||||
|
||||
test('fast-check property: any arbitrary path ending in .trae/ never yields a doubled prefix', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.string({ maxLength: 30 }).filter((s) => !s.includes('`') && !/\s/.test(s)),
|
||||
(prefix) => {
|
||||
const content = `${prefix}.trae/CLAUDE.md`;
|
||||
const out = convertClaudeToTraeMarkdown(content);
|
||||
assert.ok(!out.includes(MALFORMED_SINGLE));
|
||||
assert.ok(!out.includes(MALFORMED_DOUBLE));
|
||||
},
|
||||
),
|
||||
{ numRuns: 200 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2658 output parity: bin/install.js vs runtime-artifact-conversion.cjs convertClaudeToTraeMarkdown (#2094 mirror)', () => {
|
||||
// Parity must hold for the pre-existing reported-bug input AND for every
|
||||
// arbitrary-prefix ".trae/"-tail shape the prefix-preserving regex
|
||||
// (bin/install.js:2747-2748, mirrored byte-for-byte at
|
||||
// src/runtime-artifact-conversion.cts:1357-1358) was added to handle. A
|
||||
// change to only one copy of that regex would otherwise pass every other
|
||||
// test in this file — none of the defect-5 cases above call the mirror —
|
||||
// while silently diverging from the other copy.
|
||||
const parityCases = [
|
||||
['the reported-bug input (bare .claude/ prefix)', 'Fallback path is .claude/CLAUDE.md by default.'],
|
||||
['local relative prefix (post "./.claude/" -> "./.trae/" rewrite)', './.trae/CLAUDE.md'],
|
||||
[
|
||||
'nested project-path absolute prefix (post "./.claude/" -> "<tmp-root>/.trae/" rewrite)',
|
||||
'/private/var/folders/xx/gsd-trae-local-abc123/.trae/CLAUDE.md',
|
||||
],
|
||||
['global tilde prefix (post "~/.claude/" -> "~/.trae/" rewrite)', '~/.trae/CLAUDE.md'],
|
||||
['$HOME-variable prefix (post "$HOME/.claude/" -> "$HOME/.trae/" rewrite)', '$HOME/.trae/CLAUDE.md'],
|
||||
['backtick-wrapped local relative prefix', '`./.trae/CLAUDE.md`'],
|
||||
];
|
||||
|
||||
for (const [label, input] of parityCases) {
|
||||
test(`identical output for ${label}`, () => {
|
||||
assert.strictEqual(
|
||||
convertClaudeToTraeMarkdown(input),
|
||||
runtimeArtifactConversion.convertClaudeToTraeMarkdown(input),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test('fast-check property: any arbitrary ".trae/"-tail path produces identical output in both implementations', () => {
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.string({ maxLength: 30 }).filter((s) => !s.includes('`') && !/\s/.test(s)),
|
||||
(prefix) => {
|
||||
const content = `${prefix}.trae/CLAUDE.md`;
|
||||
assert.strictEqual(
|
||||
convertClaudeToTraeMarkdown(content),
|
||||
runtimeArtifactConversion.convertClaudeToTraeMarkdown(content),
|
||||
);
|
||||
},
|
||||
),
|
||||
{ numRuns: 200 },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2658: end-to-end --trae install never emits the malformed path (acceptance criterion 1)', () => {
|
||||
test('local install: no emitted .md/.js/.cjs file contains the malformed strings; the rules file is concrete', () => {
|
||||
const { configDir, root } = runMinimalInstall({ runtime: 'trae', scope: 'local' });
|
||||
try {
|
||||
const files = walk(configDir).filter((f) => /\.(md|js|cjs)$/.test(f));
|
||||
assert.ok(files.length > 0, 'expected at least one emitted .md/.js/.cjs file');
|
||||
for (const file of files) {
|
||||
const content = fs.readFileSync(file, 'utf8');
|
||||
assert.ok(!content.includes(MALFORMED_SINGLE), `${file} must not contain "${MALFORMED_SINGLE}"`);
|
||||
assert.ok(!content.includes(MALFORMED_DOUBLE), `${file} must not contain "${MALFORMED_DOUBLE}"`);
|
||||
}
|
||||
} finally {
|
||||
cleanup(root);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('#2658 acceptance criterion 3: new-project.md / ingest-docs.md detect trae before falling back to claude', () => {
|
||||
const workflowsDir = path.join(REPO_ROOT, 'gsd-core', 'workflows');
|
||||
|
||||
test('new-project.md recognizes /.trae/ path and TRAE_CONFIG_DIR before the claude fallback', () => {
|
||||
const content = fs.readFileSync(path.join(workflowsDir, 'new-project.md'), 'utf8');
|
||||
const pathBlock = content.match(/Derive `RUNTIME`[\s\S]*?Otherwise → `RUNTIME=claude`/);
|
||||
assert.ok(pathBlock, 'runtime-detection path block must exist');
|
||||
assert.ok(
|
||||
/Path contains `\/\.trae\/` → `RUNTIME=trae`/.test(pathBlock[0]),
|
||||
'path-based detection must recognize /.trae/ before the claude fallback',
|
||||
);
|
||||
const envBlock = content.match(/if \[ -n "\$CODEX_HOME" \][\s\S]*?else RUNTIME="claude"; fi/);
|
||||
assert.ok(envBlock, 'env-var fallback block must exist');
|
||||
assert.ok(
|
||||
/TRAE_CONFIG_DIR/.test(envBlock[0]),
|
||||
'env-var fallback must recognize TRAE_CONFIG_DIR before the claude fallback',
|
||||
);
|
||||
});
|
||||
|
||||
test('ingest-docs.md carries the same trae detection (found during this remediation, not just new-project.md)', () => {
|
||||
const content = fs.readFileSync(path.join(workflowsDir, 'ingest-docs.md'), 'utf8');
|
||||
const block = content.match(/\*\*Detect runtime\*\*[\s\S]*?else → `RUNTIME=claude`/);
|
||||
assert.ok(block, 'runtime-detection block must exist');
|
||||
assert.ok(
|
||||
/`\/\.trae\/` → `RUNTIME=trae`/.test(block[0]),
|
||||
'ingest-docs.md must also recognize /.trae/ before the claude fallback',
|
||||
);
|
||||
assert.ok(/TRAE_CONFIG_DIR/.test(content), 'env-var fallback mention must include TRAE_CONFIG_DIR');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user