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:
5
.changeset/2094-eos-trae-imperative-adapter.md
Normal file
5
.changeset/2094-eos-trae-imperative-adapter.md
Normal 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)
|
||||
@@ -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) {
|
||||
|
||||
@@ -81,6 +81,10 @@
|
||||
"stateIO": "filesystem",
|
||||
"transport": "mcp",
|
||||
"runtime": "node"
|
||||
},
|
||||
"hostBehaviors": {
|
||||
"skipSharedHooksInstall": true,
|
||||
"soloStageMetadata": "workflow"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
144
tests/fixtures/golden-install-parity/trae.json
vendored
144
tests/fixtures/golden-install-parity/trae.json
vendored
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
153
tests/trae-imperative-reference.test.cjs
Normal file
153
tests/trae-imperative-reference.test.cjs
Normal 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(', ')}`);
|
||||
}
|
||||
});
|
||||
112
tests/trae-upgrades.test.cjs
Normal file
112
tests/trae-upgrades.test.cjs
Normal 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`);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user