Merge pull request #2157 from open-gsd/feat/2094-eos-trae

feat(#2094): migrate Trae onto EoS imperative adapter + SOLO stage-metadata upgrade (ADR-1239)
This commit is contained in:
Tom Boucher
2026-07-10 21:46:55 -04:00
committed by GitHub
11 changed files with 466 additions and 88 deletions

View File

@@ -0,0 +1,5 @@
---
type: Changed
pr: 2157
---
**GSD skills installed for Trae now carry SOLO stage metadata** — Trae's SOLO Agent can recognize GSD skills as workflow-stage skills for auto-invocation instead of requiring manual triggering. Several of Trae's install branches (shared-hooks gating, path rewrites) also move onto its capability descriptor. Note: the stage-metadata field is a best-effort/inferred shape — Trae publishes no formal schema. (#2094)

View File

@@ -2618,6 +2618,14 @@ function convertClaudeToTraeMarkdown(content) {
return converted;
}
// DEFECT.GENERATIVE-FIX: this body is mirrored in
// src/runtime-artifact-conversion.cts's convertClaudeCommandToTraeSkill (used
// by src/install-engine.cts's skills-install path via
// SKILLS_CONVERTER_REGISTRY). This bin/install.js copy is dead for the live
// skills-install path — kept for this file's own module-level export/test
// surface. Neither copy re-exports the other — mirror any behavior change
// into both. Guarded by the output-parity test in
// tests/runtime-converters.test.cjs (#2094).
function convertClaudeCommandToTraeSkill(content, skillName) {
const converted = convertClaudeToTraeMarkdown(content);
const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -2632,7 +2640,16 @@ function convertClaudeCommandToTraeSkill(content, skillName) {
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
// #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's
// frontmatter parser.
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`;
let fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n`;
// #2094: emit `stage:` so Trae's SOLO agent can auto-invoke GSD skills at
// the corresponding stage (docs.trae.ai/ide/agent). The field name/schema
// is not formally documented (thin SPA docs) — descriptor-driven, single
// fixed GSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/
// best-effort.
const soloStage = _hostBehaviors('trae').soloStageMetadata;
if (soloStage) fm += `stage: ${soloStage}\n`;
fm += '---';
return `${fm}\n${body}`;
}
function convertClaudeAgentToTraeAgent(content) {
@@ -6803,7 +6820,10 @@ function uninstall(isGlobal, runtime = DEFAULT_RUNTIME) {
// #2093: isKilo dropped — the Kilo permission-cleanup branch below is
// descriptor-driven (resolveInstallPlan(runtime).finishPermissionWriter),
// not gated on this flag.
const { isOpencode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
// #2094: isTrae dropped — unused in this function after the
// skipSharedHooksInstall fold (was never referenced here besides the
// destructure).
const { isOpencode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
const dirName = getDirName(runtime);
// Get the target directory based on runtime and install type. Cline local
@@ -7845,7 +7865,9 @@ function resolveInstallRelativePath(baseDir, relPath) {
*/
function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
// #2093: isKilo dropped — unused in this function.
const { isOpencode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
// #2094: isTrae dropped — was only used in the hooks-tracking conditional
// above, now covered by hostBehaviors.skipSharedHooksInstall.
const { isOpencode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
const gsdDir = path.join(configDir, 'gsd-core');
// #1367: Claude local now writes flat gsd-*.md files at commands/ (not commands/gsd/).
// Claude local uses flatCommandsDir instead for manifest recording.
@@ -7953,7 +7975,9 @@ function writeManifest(configDir, runtime = DEFAULT_RUNTIME, options = {}) {
// Hooks are only installed for runtimes that use settings.json (not Codex/Copilot/Cline)
// Descriptor-driven (ADR-1239 / #2089+#2090): cline's exclusion is via
// hostBehaviors.skipSharedHooksInstall (was hardcoded !isCline).
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi) {
// #2094: Trae's exclusion is likewise descriptor-driven (trae declares
// skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed.
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isKimi) {
const hooksDir = path.join(configDir, 'hooks');
if (fs.existsSync(hooksDir)) {
// Drive from INSTALLED_HOOK_FILES (the canonical HOOKS_TO_COPY set from
@@ -9373,8 +9397,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
// skipSharedHooksInstall:true) — the redundant `&& !isCline` was removed.
// #2093: Kilo's exclusion is likewise descriptor-driven (kilo declares
// skipSharedHooksInstall:true) — the redundant `&& !isKilo` was removed.
// #2094: Trae's exclusion is likewise descriptor-driven (trae declares
// skipSharedHooksInstall:true) — the redundant `&& !isTrae` was removed.
// ZCode still has an empty hostBehaviors, so `&& !isZcode` stays.
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi && !isZcode) {
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isKimi && !isZcode) {
// Write package.json to force CommonJS mode for GSD scripts
// Prevents "require is not defined" errors when project has "type": "module"
// Node.js walks up looking for package.json - this stops inheritance from project
@@ -9469,15 +9495,16 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
// Codex/Copilot/Cursor/Windsurf/Trae/Cline/Kilo do not use the shared hooks/lib/
// helpers (Cursor uses standalone .js hook scripts registered via hooks.json — gated
// descriptor-driven via hostBehaviors.skipSharedHooksInstall, #2089; Cline likewise
// #2090; Kilo likewise #2093; Codex uses hooks.json directly; the others skip hooks
// entirely); Kilo and ZCode also skip hooks entirely (hooksSurface:'none' with no
// plugin surface — #1821). ZCode's hostBehaviors is still empty, so `&& !isZcode`
// stays hardcoded. OpenCode is NOT excluded: its #1914 plugin adapter spawns the
// staged hooks and requires hooks/lib/ helpers. None of the excluded runtimes must
// receive the hooks/lib/ helpers — otherwise the Codex comment downstream ("we
// deliberately do *not* copy hooks/lib/ for Codex") is contradicted in practice.
// #2090; Kilo likewise #2093; Trae likewise #2094; Codex uses hooks.json directly;
// the others skip hooks entirely); Kilo and ZCode also skip hooks entirely
// (hooksSurface:'none' with no plugin surface — #1821). ZCode's hostBehaviors is
// still empty, so `&& !isZcode` stays hardcoded. OpenCode is NOT excluded: its
// #1914 plugin adapter spawns the staged hooks and requires hooks/lib/ helpers.
// None of the excluded runtimes must receive the hooks/lib/ helpers — otherwise
// the Codex comment downstream ("we deliberately do *not* copy hooks/lib/ for
// Codex") is contradicted in practice.
const hooksLibSrc = path.join(src, 'hooks', 'lib');
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isTrae && !isKimi && !isZcode && fs.existsSync(hooksLibSrc)) {
if (!isCodex && !isCopilot && _hostBehaviors(runtime).skipSharedHooksInstall !== true && !isWindsurf && !isKimi && !isZcode && fs.existsSync(hooksLibSrc)) {
const hooksLibDest = path.join(targetDir, 'hooks', 'lib');
fs.mkdirSync(hooksLibDest, { recursive: true });
copyLibDir(hooksLibSrc, hooksLibDest, GSD_HOOK_LIB_FILES);
@@ -10413,7 +10440,8 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallStatusline, runtime = DEFAULT_RUNTIME, isGlobal = true, configDir = null, bannerOpts = {}) {
// #2093: isKilo dropped — the Kilo permissions-writer call below is gated
// on plan.finishPermissionWriter === 'kilo' (descriptor-driven), not this flag.
const { isOpencode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isTrae, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
// #2094: isTrae dropped — unused in this function.
const { isOpencode, isCodex, isCopilot, isAntigravity, isCursor, isWindsurf, isAugment, isQwen, isHermes, isCodebuddy, isCline, isKimi } = runtimeFlags(runtime);
const plan = resolveInstallPlan(runtime);
if (shouldInstallStatusline && plan.writesSharedSettings && !_hostBehaviors(runtime).skipSettingsUi) {

View File

@@ -81,6 +81,10 @@
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "node"
},
"hostBehaviors": {
"skipSharedHooksInstall": true,
"soloStageMetadata": "workflow"
}
}
}

View File

@@ -538,6 +538,8 @@ Documentation gaps:
- dispatch.maxDepth — no integer depth limit documented beyond one orchestrator level.
- dispatch.subagentToolkit — docs say agents can be configured with 'callable MCP services and other capabilities' but do not state whether sub-agents receive a full vs. restricted tool set.
**EoS migration status (#2094):** Migrated onto the imperative adapter — partially. Two `runtime === 'trae'` string-equality branches folded into descriptor-driven `runtime.hostBehaviors`: `skipSharedHooksInstall:true` gates the shared-hooks install (Trae has no hook surface: `hooksSurface: "none"`), and the `case 'trae'` global-config-dir path-rewrite's self-alias regex is now built off the descriptor's `dirName` rather than a hardcoded `~/.trae/` literal (byte-identical output). Skills dispatch was already descriptor-driven before this migration (`artifactLayout.skills.converter: "convertClaudeCommandToTraeSkill"`, resolved by converter name, not a runtime check). **Still runtime-keyed** (not folded by #2094, matching the same posture as cursor/windsurf/cline, pending a future cross-runtime content-dispatch consolidation): `RUNTIME_CONTENT_DISPATCH.trae` in `bin/install.js` — its `md`/`js` bodies are regex-callback rewrites that cannot be reduced to a byte-identical descriptor map; and the `case 'trae':` switch arm itself in `src/runtime-artifact-conversion.cts` — the arm's *structure* (not just its self-alias regex) is boilerplate shared verbatim across 7 runtimes (`codex`, `cline`, `cursor`, `windsurf`, `augment`, `trae`, `codebuddy`) and remains a runtime-keyed `switch`. `trae` also remains in `RUNTIME_FLAG_IDS` (and `isTrae` remains in `bin/install.js`, gating only the agents-converter dispatch) pending the cross-runtime agents-converter dispatch migration — agents conversion is out of scope for #2094. One upgrade lands: **SOLO stage/trigger metadata** — every emitted `SKILL.md` now carries a `stage: workflow` frontmatter line (`runtime.hostBehaviors.soloStageMetadata`), so Trae's SOLO Agent can recognize GSD skills as workflow-stage skills for auto-invocation instead of requiring manual triggering; cite https://docs.trae.ai/ide/agent ("Agents in Trae can be called individually, or automatically called by SOLO Agent at the corresponding stage"). The field is a single fixed, best-effort/inferred GSD-side value — Trae's thin SPA docs don't publish a formal stage-metadata schema. The four `undocumented` dispatch sub-axes (`nested`, `maxDepth`, `subagentToolkit`, `backgroundDispatch`) keep dispatch flattened (`shouldFlattenDispatch` fails closed to inline) — fail-closed negotiation, not a regression.
---
## kimi

View File

@@ -2561,6 +2561,10 @@ const capabilities = {
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "node"
},
"hostBehaviors": {
"skipSharedHooksInstall": true,
"soloStageMetadata": "workflow"
}
}
},
@@ -5084,6 +5088,10 @@ const runtimes = {
"stateIO": "filesystem",
"transport": "mcp",
"runtime": "node"
},
"hostBehaviors": {
"skipSharedHooksInstall": true,
"soloStageMetadata": "workflow"
}
}
},

View File

@@ -1169,6 +1169,12 @@ function convertClaudeToTraeMarkdown(content) {
return converted;
}
// DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
// convertClaudeCommandToTraeSkill (dead for the live skills-install path,
// which routes here via install-engine.cts's SKILLS_CONVERTER_REGISTRY; kept
// for bin/install.js's own module-level export/test surface). Neither copy
// re-exports the other — mirror any behavior change into both. Guarded by
// the output-parity test in tests/runtime-converters.test.cjs (#2094).
function convertClaudeCommandToTraeSkill(content, skillName) {
const converted = convertClaudeToTraeMarkdown(content);
const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -1183,7 +1189,16 @@ function convertClaudeCommandToTraeSkill(content, skillName) {
const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
// #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's
// frontmatter parser.
return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`;
let fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n`;
// #2094: emit `stage:` so Trae's SOLO agent can auto-invoke GSD skills at
// the corresponding stage (docs.trae.ai/ide/agent). The field name/schema
// is not formally documented (thin SPA docs) — descriptor-driven, single
// fixed GSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/
// best-effort.
const soloStage = _hostBehaviors('trae').soloStageMetadata as string | undefined;
if (soloStage) fm += `stage: ${soloStage}\n`;
fm += '---';
return `${fm}\n${body}`;
}
function convertSlashCommandsToCodebuddySkillMentions(content) {
@@ -2431,7 +2446,10 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
content = content.replace(/~\/\.trae\//g, pathPrefix);
// #2094: descriptor-driven — dirName resolves to '.trae' via
// getDirName()/localConfigDir, so this regex is built rather than
// hardcoded as `/~\/\.trae\//g` (byte-identical output for trae).
content = content.replace(new RegExp('~/' + escapeRegExp(dirName) + '/', 'g'), pathPrefix);
content = processAttribution(content, attribution);
break;

View File

@@ -266,6 +266,11 @@ export function getGlobalConfigHomeFragment(runtime: string): string {
* function declaration block (the add-a-host tax ADR-1239 Phase B / #1679 AC2
* removes).
*/
// #2094: 'trae' stays here — bin/install.js's agents-converter dispatch
// (convertClaudeAgentToTraeAgent selection) still reads isTrae directly.
// Removing it is gated on migrating that runtime-keyed `else if` chain to a
// cross-runtime agents-dispatch table (out of scope for #2094, which only
// folds the shared-hooks-install skip).
const RUNTIME_FLAG_IDS = Object.freeze([
'opencode', 'kilo', 'codex', 'copilot', 'antigravity', 'cursor',
'windsurf', 'augment', 'trae', 'qwen', 'hermes', 'codebuddy', 'cline', 'kimi', 'zcode',

View File

@@ -324,76 +324,76 @@
"scripts/gen-loop-host-contract.cjs": "c7f15237234811a0",
"scripts/lib/allowlist-ratchet.cjs": "ffaceaac3efc2660",
"scripts/lib/cli-exit.cjs": "612d0c372c75b7e7",
"skills/gsd-ns-context/SKILL.md": "e278a50f3ecb8f56",
"skills/gsd-ns-context/skills/docs-update/SKILL.md": "52755343585b987b",
"skills/gsd-ns-context/skills/extract-learnings/SKILL.md": "d39269bea995fabc",
"skills/gsd-ns-context/skills/graphify/SKILL.md": "e0253aef14693a7e",
"skills/gsd-ns-context/skills/map-codebase/SKILL.md": "2982436d8652f172",
"skills/gsd-ns-context/skills/mempalace-capture/SKILL.md": "36c486f14abe869a",
"skills/gsd-ns-context/skills/mempalace-recall/SKILL.md": "2d95e385fee26dee",
"skills/gsd-ns-ideate/SKILL.md": "c10342345c01c91f",
"skills/gsd-ns-ideate/skills/capture/SKILL.md": "90aee26970fa7639",
"skills/gsd-ns-ideate/skills/explore/SKILL.md": "d8e26a5ed95a4ecc",
"skills/gsd-ns-ideate/skills/sketch/SKILL.md": "ede2d3ab60a3c613",
"skills/gsd-ns-ideate/skills/spec-phase/SKILL.md": "f773e17e1ae7fb35",
"skills/gsd-ns-ideate/skills/spike/SKILL.md": "da215c1fb96a70e5",
"skills/gsd-ns-manage/SKILL.md": "0f4fb6d2f96ed7e9",
"skills/gsd-ns-manage/skills/cleanup/SKILL.md": "a578d6536b9cabc3",
"skills/gsd-ns-manage/skills/config/SKILL.md": "becac686746b68a4",
"skills/gsd-ns-manage/skills/health/SKILL.md": "74a68e1c1b310bef",
"skills/gsd-ns-manage/skills/help/SKILL.md": "0f0ae57a446b4601",
"skills/gsd-ns-manage/skills/inbox/SKILL.md": "34370c6138415209",
"skills/gsd-ns-manage/skills/manager/SKILL.md": "fba2aa10f8b29d84",
"skills/gsd-ns-manage/skills/pause-work/SKILL.md": "42924a70d6a4b96f",
"skills/gsd-ns-manage/skills/pr-branch/SKILL.md": "c25b5a3e31ab6f74",
"skills/gsd-ns-manage/skills/resume-work/SKILL.md": "5f0d4d0e86b99180",
"skills/gsd-ns-manage/skills/settings/SKILL.md": "67eff2d16e17403c",
"skills/gsd-ns-manage/skills/ship/SKILL.md": "4ad9695934e069ee",
"skills/gsd-ns-manage/skills/stats/SKILL.md": "25898070fb2a4b20",
"skills/gsd-ns-manage/skills/surface/SKILL.md": "c30399ea5d11cc5d",
"skills/gsd-ns-manage/skills/thread/SKILL.md": "67b5a0451b58c50c",
"skills/gsd-ns-manage/skills/undo/SKILL.md": "2a37b9c270d4c785",
"skills/gsd-ns-manage/skills/update/SKILL.md": "2fc47bf059aa7b38",
"skills/gsd-ns-manage/skills/workspace/SKILL.md": "257d17b9d20274d8",
"skills/gsd-ns-manage/skills/workstreams/SKILL.md": "737841783c7fdd4f",
"skills/gsd-ns-project/SKILL.md": "fac77a9e770fd655",
"skills/gsd-ns-project/skills/audit-milestone/SKILL.md": "0ccabc5556d65d56",
"skills/gsd-ns-project/skills/complete-milestone/SKILL.md": "a69d534b385d6aaf",
"skills/gsd-ns-project/skills/import/SKILL.md": "a11e630bfd8ae4de",
"skills/gsd-ns-project/skills/ingest-docs/SKILL.md": "d26637cdafd1a629",
"skills/gsd-ns-project/skills/milestone-summary/SKILL.md": "9d408f56712ba948",
"skills/gsd-ns-project/skills/new-milestone/SKILL.md": "7cbc011130e8590c",
"skills/gsd-ns-project/skills/new-project/SKILL.md": "ff0d57491df30624",
"skills/gsd-ns-project/skills/onboard/SKILL.md": "ae4c7b799d9ffb09",
"skills/gsd-ns-project/skills/profile-user/SKILL.md": "4f526120cda2f6cc",
"skills/gsd-ns-project/skills/review-backlog/SKILL.md": "d8a150558cc9326a",
"skills/gsd-ns-review/SKILL.md": "c5afe33c212947dd",
"skills/gsd-ns-review/skills/audit-fix/SKILL.md": "2c1f601f2fb52585",
"skills/gsd-ns-review/skills/audit-uat/SKILL.md": "2c450432b6fd1c3e",
"skills/gsd-ns-review/skills/code-review/SKILL.md": "71f717d78fd4954d",
"skills/gsd-ns-review/skills/debug/SKILL.md": "9d814fba870c538e",
"skills/gsd-ns-review/skills/eval-review/SKILL.md": "d42b504c0ca2dc69",
"skills/gsd-ns-review/skills/forensics/SKILL.md": "250f476c4c547a0f",
"skills/gsd-ns-review/skills/review/SKILL.md": "c89722cb096066d4",
"skills/gsd-ns-review/skills/secure-phase/SKILL.md": "b35ac9da51635368",
"skills/gsd-ns-review/skills/ui-phase/SKILL.md": "89a29a2fa62b177e",
"skills/gsd-ns-review/skills/ui-review/SKILL.md": "3e23efe03dafd691",
"skills/gsd-ns-review/skills/validate-phase/SKILL.md": "fa1ab5c9a869f6cd",
"skills/gsd-ns-workflow/SKILL.md": "0cb9c23c065fe448",
"skills/gsd-ns-workflow/skills/add-tests/SKILL.md": "6d64ae85b590274f",
"skills/gsd-ns-workflow/skills/ai-integration-phase/SKILL.md": "c458b792bdf5b1f2",
"skills/gsd-ns-workflow/skills/autonomous/SKILL.md": "37dc7b78ceb74803",
"skills/gsd-ns-workflow/skills/discuss-phase/SKILL.md": "c41cf9bf50dc6be6",
"skills/gsd-ns-workflow/skills/execute-phase/SKILL.md": "eb0158de64f22a55",
"skills/gsd-ns-workflow/skills/fast/SKILL.md": "3eae6536d09c2532",
"skills/gsd-ns-workflow/skills/mvp-phase/SKILL.md": "81c98f4436888cb8",
"skills/gsd-ns-workflow/skills/next/SKILL.md": "02993a5aa046eadd",
"skills/gsd-ns-workflow/skills/phase/SKILL.md": "50f6cc4df79a26b7",
"skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": "44d42fa41a094ad9",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "fee26bc1daedbff5",
"skills/gsd-ns-workflow/skills/progress/SKILL.md": "de48301875978833",
"skills/gsd-ns-workflow/skills/quick/SKILL.md": "d81670625c884d2c",
"skills/gsd-ns-workflow/skills/spec-phase/SKILL.md": "f773e17e1ae7fb35",
"skills/gsd-ns-workflow/skills/ultraplan-phase/SKILL.md": "1cc863a090cdcc6b",
"skills/gsd-ns-workflow/skills/verify-work/SKILL.md": "aae57a48a1b966ac"
"skills/gsd-ns-context/SKILL.md": "9353c36c249db204",
"skills/gsd-ns-context/skills/docs-update/SKILL.md": "26ec00ef228c2bdb",
"skills/gsd-ns-context/skills/extract-learnings/SKILL.md": "1fc6ba92fc18eb86",
"skills/gsd-ns-context/skills/graphify/SKILL.md": "371ce22f2fe319bc",
"skills/gsd-ns-context/skills/map-codebase/SKILL.md": "cfc5310b903d594a",
"skills/gsd-ns-context/skills/mempalace-capture/SKILL.md": "8524838c851ee44f",
"skills/gsd-ns-context/skills/mempalace-recall/SKILL.md": "73b0b7a6301cb214",
"skills/gsd-ns-ideate/SKILL.md": "aa399385b735c7c1",
"skills/gsd-ns-ideate/skills/capture/SKILL.md": "3b7fc696b2107bcf",
"skills/gsd-ns-ideate/skills/explore/SKILL.md": "34495849e42be247",
"skills/gsd-ns-ideate/skills/sketch/SKILL.md": "318e76d151f6c3c1",
"skills/gsd-ns-ideate/skills/spec-phase/SKILL.md": "9291f835233fa8da",
"skills/gsd-ns-ideate/skills/spike/SKILL.md": "b84100b5c869880f",
"skills/gsd-ns-manage/SKILL.md": "d44c39486fd6b83d",
"skills/gsd-ns-manage/skills/cleanup/SKILL.md": "1aa3427198e6ee04",
"skills/gsd-ns-manage/skills/config/SKILL.md": "f67d9051f635a030",
"skills/gsd-ns-manage/skills/health/SKILL.md": "2a4f959f80995d1d",
"skills/gsd-ns-manage/skills/help/SKILL.md": "52a981e1a34dd41d",
"skills/gsd-ns-manage/skills/inbox/SKILL.md": "88b260e6166d7b8d",
"skills/gsd-ns-manage/skills/manager/SKILL.md": "ac5c418cee66ee19",
"skills/gsd-ns-manage/skills/pause-work/SKILL.md": "ba8390423acccdeb",
"skills/gsd-ns-manage/skills/pr-branch/SKILL.md": "28d58cf0bb655bbc",
"skills/gsd-ns-manage/skills/resume-work/SKILL.md": "7e1f9d4d1fc60bca",
"skills/gsd-ns-manage/skills/settings/SKILL.md": "e3590bb5e3d0fdf2",
"skills/gsd-ns-manage/skills/ship/SKILL.md": "456609cf127b36dc",
"skills/gsd-ns-manage/skills/stats/SKILL.md": "f5f5f1fbfe65cee8",
"skills/gsd-ns-manage/skills/surface/SKILL.md": "025a48d0b1129e47",
"skills/gsd-ns-manage/skills/thread/SKILL.md": "48894eaf985265b3",
"skills/gsd-ns-manage/skills/undo/SKILL.md": "0deb95bc44edf37c",
"skills/gsd-ns-manage/skills/update/SKILL.md": "ffcd813ad256769b",
"skills/gsd-ns-manage/skills/workspace/SKILL.md": "e942e86d9e284d0f",
"skills/gsd-ns-manage/skills/workstreams/SKILL.md": "2374d16c8b5653d5",
"skills/gsd-ns-project/SKILL.md": "7c5ed3164048d2fe",
"skills/gsd-ns-project/skills/audit-milestone/SKILL.md": "6d9e265675fd6440",
"skills/gsd-ns-project/skills/complete-milestone/SKILL.md": "9a36fb672ddb2334",
"skills/gsd-ns-project/skills/import/SKILL.md": "46f9c44249ddc9e3",
"skills/gsd-ns-project/skills/ingest-docs/SKILL.md": "9254dae5d5104a80",
"skills/gsd-ns-project/skills/milestone-summary/SKILL.md": "a5aa590c17da3075",
"skills/gsd-ns-project/skills/new-milestone/SKILL.md": "827a2fd21878df0b",
"skills/gsd-ns-project/skills/new-project/SKILL.md": "cbbd2ed15bd72d39",
"skills/gsd-ns-project/skills/onboard/SKILL.md": "28a81722a1083234",
"skills/gsd-ns-project/skills/profile-user/SKILL.md": "7ed27739a950999f",
"skills/gsd-ns-project/skills/review-backlog/SKILL.md": "0285f773cc4e3bfe",
"skills/gsd-ns-review/SKILL.md": "a89fa5e8afbb7c56",
"skills/gsd-ns-review/skills/audit-fix/SKILL.md": "c07aea2676e00c7b",
"skills/gsd-ns-review/skills/audit-uat/SKILL.md": "fc192f6cba1a99fd",
"skills/gsd-ns-review/skills/code-review/SKILL.md": "467fee86ac8af71d",
"skills/gsd-ns-review/skills/debug/SKILL.md": "1b9030f4a138bedc",
"skills/gsd-ns-review/skills/eval-review/SKILL.md": "919f1aaad7d9a46a",
"skills/gsd-ns-review/skills/forensics/SKILL.md": "95f2f0a901b7b31a",
"skills/gsd-ns-review/skills/review/SKILL.md": "c35628771471201e",
"skills/gsd-ns-review/skills/secure-phase/SKILL.md": "875a24903352fc0f",
"skills/gsd-ns-review/skills/ui-phase/SKILL.md": "33b2394c085b93e8",
"skills/gsd-ns-review/skills/ui-review/SKILL.md": "6d6b47e51e60afc6",
"skills/gsd-ns-review/skills/validate-phase/SKILL.md": "536619be8c40ff5e",
"skills/gsd-ns-workflow/SKILL.md": "2c85abe6d78c7cf7",
"skills/gsd-ns-workflow/skills/add-tests/SKILL.md": "6e10735c8b18de8a",
"skills/gsd-ns-workflow/skills/ai-integration-phase/SKILL.md": "b5cce2e6e801420d",
"skills/gsd-ns-workflow/skills/autonomous/SKILL.md": "a9d7374b631ae7b3",
"skills/gsd-ns-workflow/skills/discuss-phase/SKILL.md": "d2f1554b9faab0a1",
"skills/gsd-ns-workflow/skills/execute-phase/SKILL.md": "e05fc375d4b5f7b5",
"skills/gsd-ns-workflow/skills/fast/SKILL.md": "08f8ce2ba96921cc",
"skills/gsd-ns-workflow/skills/mvp-phase/SKILL.md": "fef8178c9920c2dd",
"skills/gsd-ns-workflow/skills/next/SKILL.md": "faadd9e2817e7324",
"skills/gsd-ns-workflow/skills/phase/SKILL.md": "df3efcd61f7cc796",
"skills/gsd-ns-workflow/skills/plan-phase/SKILL.md": "b6f6395312983c31",
"skills/gsd-ns-workflow/skills/plan-review-convergence/SKILL.md": "d67e16a8e28b780d",
"skills/gsd-ns-workflow/skills/progress/SKILL.md": "37a37d2cdfba46ea",
"skills/gsd-ns-workflow/skills/quick/SKILL.md": "6fd1b96274b23a9f",
"skills/gsd-ns-workflow/skills/spec-phase/SKILL.md": "9291f835233fa8da",
"skills/gsd-ns-workflow/skills/ultraplan-phase/SKILL.md": "a3cd6f98acc738f0",
"skills/gsd-ns-workflow/skills/verify-work/SKILL.md": "25dc4ab4e4a060bc"
}

View File

@@ -19,6 +19,7 @@ const {
convertClaudeAgentToAntigravityAgent,
convertClaudeCommandToOpencodeSkill,
convertClaudeCommandToKiloSkill,
convertClaudeCommandToTraeSkill,
neutralizeAgentReferences,
} = require('../bin/install.js');
@@ -267,6 +268,48 @@ describe('convertClaudeToKiloFrontmatter output parity: bin/install.js vs runtim
});
});
// ─────────────────────────────────────────────────────────────────────────────
// DEFECT.GENERATIVE-FIX output-parity guard: convertClaudeCommandToTraeSkill is
// defined TWICE — once in bin/install.js (dead for the live skills-install
// path; kept for this file's own module-level export/test surface) and once in
// src/runtime-artifact-conversion.cts, compiled to
// gsd-core/bin/lib/runtime-artifact-conversion.cjs (used by
// src/install-engine.cts's skills-install path via SKILLS_CONVERTER_REGISTRY,
// see install-engine.cts ~L754). Both copies are LIVE call surfaces — neither
// re-exports the other — so #2094's `stage:` emission had to be applied to
// bin/install.js's copy by hand to keep parity. Source-text identity can't be
// asserted (they live in different module systems: plain CJS vs a
// tsc-compiled .cts output with different surrounding comments), so this
// instead proves the two implementations still produce IDENTICAL output for
// representative command input. If a future edit changes one copy's behavior
// without mirroring it into the other, this test is the guard that catches
// the divergence.
// ─────────────────────────────────────────────────────────────────────────────
describe('convertClaudeCommandToTraeSkill output parity: bin/install.js vs runtime-artifact-conversion.cjs (#2094)', () => {
const { convertClaudeCommandToTraeSkill: convertViaConversionModule } =
require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
test('identical output for a representative command, including the #2094 stage: field', () => {
const viaInstall = convertClaudeCommandToTraeSkill(SAMPLE_COMMAND, 'gsd-execute-phase');
const viaModule = convertViaConversionModule(SAMPLE_COMMAND, 'gsd-execute-phase');
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical command output');
assert.match(viaInstall, /\nstage: workflow\n/, 'both copies must emit the #2094 stage: field');
});
test('identical output when the source has no description (falls back to generic description)', () => {
const noDescriptionCommand = `---
name: gsd-noop
allowed-tools:
- Read
---
Do nothing.`;
const viaInstall = convertClaudeCommandToTraeSkill(noDescriptionCommand, 'gsd-noop');
const viaModule = convertViaConversionModule(noDescriptionCommand, 'gsd-noop');
assert.equal(viaInstall, viaModule, 'bin/install.js and runtime-artifact-conversion.cjs must emit identical output when description is absent');
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Antigravity agent conversion — shared Gemini-backend tool mapping (#1394 / #1928)
// ─────────────────────────────────────────────────────────────────────────────

View File

@@ -0,0 +1,153 @@
// allow-test-rule: structural-regression-guard — AC2 requires asserting no `runtime === 'trae'` string-equality branch remains in bin/install.js, src/install-engine.cts, and src/runtime-artifact-conversion.cts — the descriptor-migration contract is a property of the source text, so a source-grep is the only faithful check (#2094)
'use strict';
/**
* trae imperative reference host — ADR-1239 Phase D / #2094 (EoS/trae).
*
* Proves Trae IDE is driven through the PUBLIC Host-Integration Interface
* (the imperative adapter), that its negotiated axes classify + negotiate
* correctly, that negotiation fails CLOSED on a corrupted descriptor, and
* that the migration retired the hardcoded `runtime === 'trae'` string-
* equality branches across the install engine and artifact conversion
* modules (folded into descriptor-driven `runtime.hostBehaviors`).
*
* Trae has NO hook surface at all (`hooksSurface: "none"`,
* `extendedHookEvents: []`, `installSurface: "profile-marker-only"`) — its
* `hookBus` axis is `'engine'` (VSCode-fork extension-host lifecycle, not a
* GSD-managed hook dialect), so unlike qwen/kilo this file does not assert an
* extendedHookEvents surface. Four of Trae's six dispatch sub-axes
* (`nested`, `maxDepth`, `subagentToolkit`, `backgroundDispatch`) are
* `'undocumented'` — no authoritative Trae doc states them — which is what
* drives the fail-closed `shouldFlattenDispatch` assertion below. The real
* upgrade (SOLO stage/trigger metadata on emitted skills) is covered in
* tests/trae-upgrades.test.cjs.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { createImperativeAdapter } = require('../gsd-core/bin/lib/adapter-imperative.cjs');
const {
profileOf,
negotiateHostCapabilities,
shouldFlattenDispatch,
PROFILE_BASELINES,
UNDOCUMENTED,
} = require('../gsd-core/bin/lib/host-integration.cjs');
const TRAE_CAP = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'trae', 'capability.json'), 'utf8'),
);
const TRAE_AXES = TRAE_CAP.runtime.hostIntegration;
// -- AC2: driven through the public interface (imperative adapter) -----------
test('createImperativeAdapter classifies trae as imperative + composes the registry', () => {
const adapter = createImperativeAdapter({ runtime: 'trae' });
assert.equal(adapter.kind, 'imperative');
assert.equal(adapter.runtime, 'trae');
assert.ok(adapter.registry && typeof adapter.registry === 'object');
assert.equal(typeof adapter.install, 'function');
assert.equal(typeof adapter.uninstall, 'function');
});
test('trae axes classify as the programmatic-cli reference profile', () => {
// Confirmed via `node -e` against the real descriptor before asserting:
// profileOf(TRAE_AXES) === 'programmatic-cli' (embeddingMode: 'imperative').
assert.equal(profileOf(TRAE_AXES), 'programmatic-cli');
});
// -- AC3: all axes populated + validated -------------------------------------
test('trae descriptor declares all 8 axes + 6 dispatch sub-axes with exact values', () => {
assert.equal(TRAE_AXES.embeddingMode, 'imperative');
assert.equal(TRAE_AXES.commandSurface, 'slash-file');
assert.equal(TRAE_AXES.modelMode, 'passive');
assert.equal(TRAE_AXES.hookBus, 'engine');
assert.equal(TRAE_AXES.stateIO, 'filesystem');
assert.equal(TRAE_AXES.transport, 'mcp');
assert.equal(TRAE_AXES.runtime, 'node');
const d = TRAE_AXES.dispatch;
assert.equal(d.namedDispatch, true);
assert.equal(d.nested, 'undocumented');
assert.equal(d.maxDepth, 'undocumented');
assert.equal(d.background, true);
assert.equal(d.subagentToolkit, 'undocumented');
assert.equal(d.backgroundDispatch, 'undocumented');
});
// -- AC5: negotiation fails CLOSED on a corrupted descriptor ------------------
test('negotiateHostCapabilities never throws for trae, even fully corrupted', () => {
assert.doesNotThrow(() => negotiateHostCapabilities({}));
assert.doesNotThrow(() => negotiateHostCapabilities({ ...TRAE_AXES, embeddingMode: UNDOCUMENTED }));
assert.doesNotThrow(() => negotiateHostCapabilities({ ...TRAE_AXES, embeddingMode: 'future-unknown' }));
});
test('AC-SPECIFIC: trae real dispatch axes fail CLOSED to inline (shouldFlattenDispatch === true)', () => {
// Confirmed via `node -e` against the real descriptor before asserting:
// shouldFlattenDispatch(TRAE_AXES.dispatch) === true.
//
// shouldFlattenDispatch only permits backgrounding when BOTH `background`
// AND `backgroundDispatch` are explicitly `true` (src/host-integration.cts
// shouldFlattenDispatch, `canBackground = background === true &&
// backgroundDispatch === true`). Trae's `background` is `true` but
// `backgroundDispatch` is `'undocumented'` (no authoritative Trae doc states
// whether a spawned agent can itself be backgrounded) — so canBackground is
// false and the orchestrator must run inline. Pinned here so a future
// doc-sourcing pass that fills in `backgroundDispatch` can't silently flip
// this fail-closed default without a deliberate test update.
assert.equal(shouldFlattenDispatch(TRAE_AXES.dispatch), true);
// Sanity: if backgroundDispatch WERE true (all else equal), the same shape
// would NOT flatten — proving backgroundDispatch is what flips the result.
assert.equal(shouldFlattenDispatch({ ...TRAE_AXES.dispatch, backgroundDispatch: true }), false);
});
test('a partial/empty trae descriptor degrades to the safe floor, not the programmatic-cli baseline', () => {
const result = negotiateHostCapabilities({});
assert.equal(result.effective.embeddingMode, 'declarative', 'omitted embeddingMode degrades closed');
assert.equal(result.effective.hookBus, 'none');
assert.notDeepEqual(result.effective, PROFILE_BASELINES['programmatic-cli']);
assert.ok(result.warnings.length > 0);
});
// -- AC2: the folded-in behaviors ---------------------------------------------
test('trae descriptor declares runtime.hostBehaviors (the folded-in behaviors)', () => {
const hb = TRAE_CAP.runtime.hostBehaviors;
assert.ok(hb && typeof hb === 'object');
assert.equal(hb.skipSharedHooksInstall, true);
assert.equal(hb.soloStageMetadata, 'workflow');
});
// -- AC2: the hardcoded branches are retired across all folded modules -------
test('no `runtime === "trae"` string-equality branch remains in the descriptor-migrated modules (AC2)', () => {
// NOTE: this deliberately does NOT grep for `isTrae` — `isTrae` legitimately
// remains in bin/install.js as a destructured `runtimeFlags(runtime)`
// binding used ONLY by the agents-converter dispatch chain (`else if
// (isTrae) { content = convertClaudeAgentToTraeAgent(content); }`). That
// cross-runtime agents-converter dispatch is out of scope for #2094 (a
// separate migration tracked elsewhere) — trae stays in RUNTIME_FLAG_IDS by
// design until that follow-up lands. Only a `runtime === 'trae'` /
// `runtime !== 'trae'` STRING-EQUALITY comparison is a regression here.
const strip = (src) => src
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/\/\/[^\r\n]*/g, '')
.replace(/`[^`]*`/g, '');
const repoRoot = path.join(__dirname, '..');
const files = [
path.join(repoRoot, 'bin', 'install.js'),
path.join(repoRoot, 'src', 'install-engine.cts'),
path.join(repoRoot, 'src', 'runtime-artifact-conversion.cts'),
];
for (const file of files) {
const src = fs.readFileSync(file, 'utf8');
const offenders = strip(src).match(/runtime\s*[!=]==\s*'trae'/g) || [];
assert.deepEqual(offenders, [],
`AC2: no hardcoded runtime==='trae' branch may remain in ${path.relative(repoRoot, file)}; found: ${offenders.join(', ')}`);
}
});

View File

@@ -0,0 +1,112 @@
'use strict';
/**
* trae capability UPGRADE — ADR-1239 Phase D / #2094 (EoS/trae).
*
* Drives the user-reachable surface (spawned `bin/install.js` via
* `runMinimalInstall`) to prove the one real upgrade Trae contributes beyond
* the base imperative migration (tests/trae-imperative-reference.test.cjs):
*
* UPGRADE — SOLO stage/trigger metadata: every emitted `SKILL.md` carries a
* `stage: workflow` frontmatter line (`runtime.hostBehaviors.
* soloStageMetadata`), sourced from https://docs.trae.ai/ide/agent ("Agents
* in Trae can be called individually, or automatically called by SOLO Agent
* at the corresponding stage"). This is a single fixed GSD-side value —
* best-effort/inferred, since Trae's docs don't publish a formal
* stage-metadata schema — not per-skill differentiated. It lets Trae's SOLO
* Agent auto-invoke GSD skills at the corresponding stage instead of
* requiring the user to manually trigger them.
*/
const { test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const { runMinimalInstall, walk } = require('./helpers/install-shared.cjs');
const { cleanup } = require('./helpers.cjs');
const TRAE_CAP = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'capabilities', 'trae', 'capability.json'), 'utf8'),
);
/** Extract the YAML frontmatter block (between the first pair of `---` lines), or null. */
function parseFrontmatter(content) {
const m = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
return m ? m[1] : null;
}
// -- UPGRADE: SOLO stage/trigger metadata on every emitted SKILL.md ---------
for (const scope of ['global', 'local']) {
test(`trae --${scope}: emitted SKILL.md frontmatter carries stage: workflow (UPGRADE)`, (t) => {
const { configDir, root } = runMinimalInstall({ runtime: 'trae', scope });
t.after(() => cleanup(root));
const skillsDir = path.join(configDir, 'skills');
assert.ok(fs.existsSync(skillsDir), `${skillsDir} must exist`);
// Trae uses NESTED skills (capabilities/trae/capability.json
// artifactLayout.skills.nesting === "nested"): a router SKILL.md at
// skills/gsd-ns-<namespace>/SKILL.md, and concrete skills nested under
// skills/gsd-ns-<namespace>/skills/<stem>/SKILL.md. Discover ALL of them
// by recursive walk rather than hand-listing paths, so the test doesn't
// silently stop covering skills added/renamed later.
const skillFiles = walk(skillsDir).filter((f) => f.endsWith('SKILL.md'));
assert.ok(skillFiles.length > 0, `expected at least one SKILL.md under ${skillsDir}`);
for (const filePath of skillFiles) {
const content = fs.readFileSync(filePath, 'utf8');
const fm = parseFrontmatter(content);
assert.ok(fm, `${filePath} must have YAML frontmatter`);
assert.match(fm, /^stage:\s*workflow\s*$/m,
`${filePath} frontmatter must declare stage: workflow (UPGRADE — SOLO stage metadata)`);
}
// A couple of named skills, explicitly, spanning both the top-level
// router shape and the nested concrete-skill shape — confirmed via
// `node -e` against a real install before asserting these exact paths.
const samples = [
path.join(skillsDir, 'gsd-ns-manage', 'SKILL.md'),
path.join(skillsDir, 'gsd-ns-manage', 'skills', 'cleanup', 'SKILL.md'),
path.join(skillsDir, 'gsd-ns-ideate', 'skills', 'capture', 'SKILL.md'),
];
for (const sample of samples) {
assert.ok(fs.existsSync(sample), `${sample} must exist`);
const fm = parseFrontmatter(fs.readFileSync(sample, 'utf8'));
assert.ok(fm, `${sample} must have YAML frontmatter`);
assert.match(fm, /^stage:\s*workflow\s*$/m, `${sample} frontmatter must declare stage: workflow`);
}
});
}
// -- boundary: capability.json is the single source of truth ----------------
test('capabilities/trae/capability.json runtime.hostBehaviors.soloStageMetadata === "workflow"', () => {
assert.equal(TRAE_CAP.runtime.hostBehaviors.soloStageMetadata, 'workflow');
});
// -- descriptor-gated, not global: a runtime lacking soloStageMetadata gets
// no stage: line at all -------------------------------------------------
test('qwen (no soloStageMetadata declared) does NOT get a stage: line — proves the field is descriptor-gated, not global', (t) => {
const { configDir, root } = runMinimalInstall({ runtime: 'qwen', scope: 'global' });
t.after(() => cleanup(root));
const skillsDir = path.join(configDir, 'skills');
assert.ok(fs.existsSync(skillsDir), `${skillsDir} must exist`);
// Qwen also uses the nested skill layout (router SKILL.md +
// skills/<stem>/SKILL.md) — confirmed via `node -e` against a real
// install before writing this — so walk recursively, same as the trae
// assertions above, rather than assuming flat gsd-*.md files.
const skillFiles = walk(skillsDir).filter((f) => f.endsWith('SKILL.md'));
assert.ok(skillFiles.length > 0, `expected at least one SKILL.md under ${skillsDir}`);
for (const filePath of skillFiles) {
const content = fs.readFileSync(filePath, 'utf8');
const fm = parseFrontmatter(content);
assert.ok(fm, `${filePath} must have YAML frontmatter`);
assert.doesNotMatch(fm, /^stage:/m,
`${filePath} must NOT declare stage: — qwen's descriptor has no hostBehaviors.soloStageMetadata`);
}
});