@opengsd/gsd-core 1.7.0-rc.5 → 1.7.0-rc.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-ui-checker.md +2 -0
  4. package/agents/gsd-ui-researcher.md +1 -0
  5. package/bin/install.js +970 -192
  6. package/gsd-core/bin/lib/capability-registry.cjs +501 -85
  7. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  8. package/gsd-core/bin/lib/host-integration.cjs +33 -8
  9. package/gsd-core/bin/lib/init.cjs +33 -40
  10. package/gsd-core/bin/lib/install-engine.cjs +90 -20
  11. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  12. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  13. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  14. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +235 -38
  15. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +22 -13
  16. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  17. package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
  18. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +501 -12
  19. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  20. package/gsd-core/bin/lib/security.cjs +6 -36
  21. package/gsd-core/bin/lib/shell-command-projection.cjs +115 -2
  22. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  23. package/gsd-core/bin/lib/surface.cjs +10 -6
  24. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  25. package/gsd-core/bin/shared/model-catalog.json +8 -3
  26. package/gsd-core/references/ui-consideration-probe.md +73 -0
  27. package/gsd-core/templates/UI-SPEC.md +25 -0
  28. package/gsd-core/templates/VALIDATION.md +2 -0
  29. package/gsd-core/workflows/audit-milestone.md +7 -4
  30. package/gsd-core/workflows/plan-phase.md +6 -0
  31. package/gsd-core/workflows/settings-advanced.md +7 -4
  32. package/gsd-core/workflows/ui-phase.md +146 -1
  33. package/gsd-core/workflows/validate-phase.md +2 -2
  34. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  35. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  36. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  37. package/hooks/gsd-windsurf-pre-command.js +275 -0
  38. package/hooks/gsd-windsurf-pre-write.js +132 -0
  39. package/hooks/managed-hooks-registry.cjs +2 -0
  40. package/package.json +8 -4
  41. package/pi/gsd.cjs +354 -0
  42. package/scripts/build-hooks.js +3 -0
  43. package/scripts/gen-golden-install-parity-zcode.cjs +11 -1
  44. package/scripts/gen-registry.cjs +128 -0
  45. package/scripts/lint-test-file-count.allowlist.json +2 -1
  46. package/scripts/registry-schema.cjs +565 -0
  47. package/scripts/validate-registry.cjs +117 -0
  48. package/vscode/browser.js +197 -0
  49. package/vscode/extension.js +383 -0
  50. package/vscode/host-binding.js +113 -0
  51. package/vscode/package.json +96 -0
@@ -66,6 +66,38 @@ function gsdVersion() {
66
66
  cachedVersion = resolveVersionFrom(__dirname);
67
67
  return cachedVersion;
68
68
  }
69
+ /**
70
+ * Host-specific install behaviors declared on the runtime descriptor
71
+ * (capabilities/<runtime>/capability.json -> runtime.hostBehaviors). Mirrors
72
+ * bin/install.js's / install-engine.cts's `_hostBehaviors` (ADR-1239 / #2086
73
+ * / #2092). Returns {} for runtimes that declare none, so every behavior
74
+ * branch degrades to the generic path by default. Unlike the bin/install.js
75
+ * and install-engine.cts variants, this module already imports
76
+ * `capabilityRegistry` statically (see NON_CLAUDE_RUNTIMES below), so this
77
+ * reads it directly rather than re-require()-ing inside a try/catch.
78
+ */
79
+ function _hostBehaviors(runtime) {
80
+ return ((capabilityRegistry &&
81
+ capabilityRegistry.runtimes &&
82
+ capabilityRegistry.runtimes[runtime] &&
83
+ capabilityRegistry.runtimes[runtime].runtime &&
84
+ capabilityRegistry.runtimes[runtime].runtime.hostBehaviors) ||
85
+ {});
86
+ }
87
+ /**
88
+ * Public accessor for the `hostBehaviors.agentFileExtension` descriptor field
89
+ * (ADR-1239 / #2099 / #2103). Returns the runtime's declared agent-file
90
+ * destination-suffix rename target (e.g. copilot's `.agent.md`), or
91
+ * `undefined` when the runtime declares none (the generic no-rename
92
+ * default). Exported so callers outside this module (surface.cts's
93
+ * `_syncGsdDir`) can derive the SAME rename decision as
94
+ * install-engine.cts's staged-copy loop from ONE descriptor read, instead of
95
+ * duplicating a hardcoded `runtime === 'copilot'` check (#2103 fold).
96
+ */
97
+ function agentFileExtensionFor(runtime) {
98
+ const ext = _hostBehaviors(runtime).agentFileExtension;
99
+ return typeof ext === 'string' ? ext : undefined;
100
+ }
69
101
  const colorNameToHex = {
70
102
  cyan: '#00FFFF',
71
103
  red: '#FF0000',
@@ -412,10 +444,12 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
412
444
  if (version)
413
445
  fm += `version: ${yamlQuote(version)}\n`;
414
446
  }
415
- // #778 (b) — Qwen-only numeric priority for /skills ordering. Scoped to qwen
416
- // so Claude/Hermes skill frontmatter is unchanged (they ignore the field, but
417
- // we keep their output byte-stable). skillName is the `gsd-<stem>` dir name.
418
- if (runtime === 'qwen') {
447
+ // #778 (b) — numeric priority for /skills ordering, declared on the runtime
448
+ // descriptor (runtime.hostBehaviors.skillPriorityFrontmatter). Scoped to
449
+ // runtimes that declare the flag so Claude/Hermes skill frontmatter is
450
+ // unchanged (they ignore the field, but we keep their output byte-stable).
451
+ // skillName is the `gsd-<stem>` dir name. (ADR-1239 / #2092)
452
+ if (_hostBehaviors(runtime).skillPriorityFrontmatter) {
419
453
  const stem = typeof skillName === 'string' && skillName.startsWith('gsd-')
420
454
  ? skillName.slice(4)
421
455
  : skillName;
@@ -465,6 +499,14 @@ function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) {
465
499
  .replace(colonPattern, (_, cmd) => `/skill:gsd-${cmd}`)
466
500
  .replace(hyphenPattern, (_, cmd) => `/skill:gsd-${cmd}`);
467
501
  }
502
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
503
+ // convertClaudeCommandToKimiSkill (kept for bin/install.js's own
504
+ // module-level export/test surface; dead for the live skills-install path,
505
+ // which routes here via install-engine.cts's SKILLS_CONVERTER_REGISTRY
506
+ // through the kimi capability descriptor's artifactLayout
507
+ // `converter: "convertClaudeCommandToKimiSkill"`). Neither copy re-exports
508
+ // the other — mirror any behavior change into both. Guarded by the
509
+ // output-parity test in tests/runtime-converters.test.cjs (#2095).
468
510
  function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) {
469
511
  const { frontmatter, body } = extractFrontmatterAndBody(content);
470
512
  const kimiSkillName = normalizeKimiSkillName(skillName);
@@ -592,6 +634,15 @@ function buildKimiSubagentYaml({ name, description, tools }) {
592
634
  pushKimiToolsYaml(lines, 2, tools);
593
635
  return `${lines.join('\n')}\n`;
594
636
  }
637
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
638
+ // buildKimiAgentArtifacts (kept for bin/install.js's own module-level
639
+ // export/test surface; dead for the live install path, which routes here via
640
+ // runtime-artifact-layout.cts's kimiAgentsKind through a dynamic
641
+ // `conversionExports['buildKimiAgentArtifacts']` lookup against this
642
+ // compiled module). Neither copy re-exports the other — mirror any behavior
643
+ // change into both, including the kimi_cli.tools.agent:Agent grant that
644
+ // enables background dispatch (#2095 Upgrade 2). Guarded by the
645
+ // output-parity test in tests/runtime-converters.test.cjs (#2095).
595
646
  function buildKimiAgentArtifacts({ rootAgent = '', subagents = [], requestedSubagents = null, } = {}) {
596
647
  const diagnostics = [];
597
648
  const rootSource = parseKimiAgentSource(rootAgent) || { path: null, content: '' };
@@ -971,6 +1022,10 @@ function convertClaudeToAugmentMarkdown(content) {
971
1022
  converted = converted.replace(/\bClaude Code\b/g, 'Augment');
972
1023
  return converted;
973
1024
  }
1025
+ // #2097 (ADR-1239): command-body converters selected by descriptor
1026
+ // (runtime.hostBehaviors.commandBodyConverter) instead of a runtime-name
1027
+ // branch. Degrade-closed: unknown/absent name → no conversion.
1028
+ const COMMAND_BODY_CONVERTERS = { convertClaudeToAugmentMarkdown };
974
1029
  function getAugmentSkillAdapterHeader(skillName) {
975
1030
  return `<augment_skill_adapter>
976
1031
  ## A. Skill Invocation
@@ -1045,6 +1100,12 @@ function convertClaudeToTraeMarkdown(content) {
1045
1100
  converted = converted.replace(/\bClaude Code\b/g, 'Trae');
1046
1101
  return converted;
1047
1102
  }
1103
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
1104
+ // convertClaudeCommandToTraeSkill (dead for the live skills-install path,
1105
+ // which routes here via install-engine.cts's SKILLS_CONVERTER_REGISTRY; kept
1106
+ // for bin/install.js's own module-level export/test surface). Neither copy
1107
+ // re-exports the other — mirror any behavior change into both. Guarded by
1108
+ // the output-parity test in tests/runtime-converters.test.cjs (#2094).
1048
1109
  function convertClaudeCommandToTraeSkill(content, skillName) {
1049
1110
  const converted = convertClaudeToTraeMarkdown(content);
1050
1111
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -1059,7 +1120,17 @@ function convertClaudeCommandToTraeSkill(content, skillName) {
1059
1120
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1060
1121
  // #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's
1061
1122
  // frontmatter parser.
1062
- return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`;
1123
+ let fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n`;
1124
+ // #2094: emit `stage:` so Trae's SOLO agent can auto-invoke GSD skills at
1125
+ // the corresponding stage (docs.trae.ai/ide/agent). The field name/schema
1126
+ // is not formally documented (thin SPA docs) — descriptor-driven, single
1127
+ // fixed GSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/
1128
+ // best-effort.
1129
+ const soloStage = _hostBehaviors('trae').soloStageMetadata;
1130
+ if (soloStage)
1131
+ fm += `stage: ${soloStage}\n`;
1132
+ fm += '---';
1133
+ return `${fm}\n${body}`;
1063
1134
  }
1064
1135
  function convertSlashCommandsToCodebuddySkillMentions(content) {
1065
1136
  return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
@@ -1520,7 +1591,12 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
1520
1591
  return `---\n${newFrontmatter}\n---${body}`;
1521
1592
  }
1522
1593
  // Kilo CLI — same conversion logic as OpenCode, different config paths.
1523
- function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
1594
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
1595
+ // convertClaudeToKiloFrontmatter (used by bin/install.js's own legacy install
1596
+ // path). Neither copy re-exports the other — mirror any behavior change into
1597
+ // both. Guarded by the output-parity test in tests/runtime-converters.test.cjs
1598
+ // (#2093).
1599
+ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
1524
1600
  // Replace tool name references in content (applies to all files)
1525
1601
  let convertedContent = content;
1526
1602
  convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
@@ -1666,6 +1742,14 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
1666
1742
  // For agents: add required Kilo agent fields
1667
1743
  if (isAgent) {
1668
1744
  newLines.push('mode: subagent');
1745
+ // Embed model override from ~/.gsd/defaults.json so model_overrides is
1746
+ // respected on Kilo (which uses static agent frontmatter, not inline
1747
+ // Task() model parameters) — mirrors convertClaudeToOpencodeFrontmatter's
1748
+ // model emission exactly (#2093 UPGRADE 2 / ADR-1239; Kilo is an OpenCode
1749
+ // fork with the same static-frontmatter model constraint). See #2256.
1750
+ if (modelOverride) {
1751
+ newLines.push(['model:', modelOverride].join(' '));
1752
+ }
1669
1753
  newLines.push(...buildKiloAgentPermissionBlock(agentTools));
1670
1754
  }
1671
1755
  // For commands: add tools object if we had allowed-tools or tools
@@ -1875,6 +1959,76 @@ function convertClaudeAgentToTraeAgent(content) {
1875
1959
  const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
1876
1960
  return `${cleanFrontmatter}\n${body}`;
1877
1961
  }
1962
+ /**
1963
+ * Convert a Claude agent (.md) to a native Qwen Code subagent file
1964
+ * (`.qwen/agents/gsd-*.md` / `<qwenhome>/agents/gsd-*.md`, ADR-1239 / #2092
1965
+ * Phase B Upgrade 1). Qwen Code is a Claude-dialect host: its docs' "Claude
1966
+ * Code Compatibility Fields" section confirms CC agent files parse under
1967
+ * `.qwen/agents/` (https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents),
1968
+ * so — unlike Cursor/Trae/Copilot/Antigravity — tool names pass through
1969
+ * UNCHANGED (no remapping table).
1970
+ *
1971
+ * Emits DETERMINISTIC frontmatter: `name:` + `description:` (mirrors
1972
+ * convertClaudeAgentToCursorAgent), plus `tools:` as a YAML block list when the
1973
+ * source declares one. Qwen's documented `tools:` schema is a YAML array
1974
+ * (`tools:\n- tool1\n- tool2`), not Claude's single-line comma-separated string
1975
+ * — passing the raw single-line string through unchanged would parse as one
1976
+ * malformed tool name and be silently dropped ("Optional fields with invalid
1977
+ * values are silently dropped at parse time" — same docs page). Reuses
1978
+ * `parseFrontmatterTools` (already relied on by the Kimi agent path), which
1979
+ * tolerates BOTH source formats Claude's own agents/*.md files use — the
1980
+ * single-line comma list (most agents) and the YAML block list (e.g.
1981
+ * agents/gsd-nyquist-auditor.md, agents/gsd-security-auditor.md) — so no tools
1982
+ * are lost regardless of which the source agent uses.
1983
+ *
1984
+ * `color` IS preserved: Qwen's docs list `color` under "Claude Code
1985
+ * Compatibility Fields" as a supported optional field, so it is passed
1986
+ * through as a plain scalar (unlike the cursor/trae/augment/windsurf
1987
+ * reduced-frontmatter converters, which drop it — those hosts have no such
1988
+ * compatibility field). `model:` and `approvalMode:` are intentionally NOT
1989
+ * emitted: both are optional per the docs and out of scope for #2092 (model:
1990
+ * would couple to the model catalog and introduce nondeterminism;
1991
+ * approvalMode is a deliberate follow-on).
1992
+ *
1993
+ * Body: preserved verbatim after the qwen branding rewrite (CLAUDE.md /
1994
+ * Claude Code / .claude/ literal-substring values — descriptor-driven via
1995
+ * runtime.hostBehaviors.brandingRewrites, mirrors the qwen case in
1996
+ * _applyRuntimeRewrites). The anchored `~/.claude/` / `$HOME/.claude/` forms
1997
+ * are already rewritten upstream by applyAgentPathRewrites (agentCtx Step 1 in
1998
+ * stageAgentsForRuntimeWithConverter) before this converter runs, so only the
1999
+ * bare/non-anchored forms are handled here — mirrors how
2000
+ * convertClaudeToTraeMarkdown orders its bare-form rewrites after the slash
2001
+ * forms to avoid double-rewriting the same substring.
2002
+ */
2003
+ function convertClaudeAgentToQwenAgent(content) {
2004
+ const _b = _hostBehaviors('qwen').brandingRewrites || {};
2005
+ let converted = content;
2006
+ if (_b['CLAUDE.md'])
2007
+ converted = converted.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2008
+ if (_b['Claude Code'])
2009
+ converted = converted.replace(/\bClaude Code\b/g, _b['Claude Code']);
2010
+ if (_b['.claude/'])
2011
+ converted = converted.replace(/\.claude\//g, _b['.claude/']);
2012
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
2013
+ if (!frontmatter)
2014
+ return converted;
2015
+ const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
2016
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
2017
+ const tools = parseFrontmatterTools(frontmatter);
2018
+ const color = extractFrontmatterField(frontmatter, 'color');
2019
+ let fm = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n`;
2020
+ if (tools.length > 0) {
2021
+ fm += 'tools:\n';
2022
+ for (const tool of tools) {
2023
+ fm += ` - ${yamlIdentifier(tool)}\n`;
2024
+ }
2025
+ }
2026
+ if (color) {
2027
+ fm += `color: ${yamlIdentifier(color)}\n`;
2028
+ }
2029
+ fm += '---';
2030
+ return `${fm}\n${body}`;
2031
+ }
1878
2032
  function convertClaudeAgentToCodebuddyAgent(content) {
1879
2033
  const converted = convertClaudeToCodebuddyMarkdown(content);
1880
2034
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -2087,19 +2241,24 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2087
2241
  content = processAttribution(content, attribution);
2088
2242
  break;
2089
2243
  }
2090
- case 'augment':
2244
+ case 'augment': {
2091
2245
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2092
2246
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2093
2247
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2094
2248
  content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
2095
2249
  content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2096
2250
  content = content.replace(/\.\/\.claude(?![\w-])/g, `./${dirName}`);
2097
- content = content.replace(/~\/\.augment\//g, pathPrefix);
2098
- content = content.replace(/\$HOME\/\.augment\//g, pathPrefix);
2099
- content = content.replace(/~\/\.augment(?![\w-])/g, normalizedPathPrefix);
2100
- content = content.replace(/\$HOME\/\.augment(?![\w-])/g, normalizedPathPrefix);
2251
+ // #2097: dot-dir self-references (~/.augment/…) → resolved prefix,
2252
+ // dirName-derived (no runtime literal). getDirName('augment') resolves
2253
+ // to '.augment', so this is byte-identical to the prior hardcoded regexes.
2254
+ const _dd = escapeRegExp(dirName);
2255
+ content = content.replace(new RegExp('~/' + _dd + '/', 'g'), pathPrefix);
2256
+ content = content.replace(new RegExp('\\$HOME/' + _dd + '/', 'g'), pathPrefix);
2257
+ content = content.replace(new RegExp('~/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
2258
+ content = content.replace(new RegExp('\\$HOME/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
2101
2259
  content = processAttribution(content, attribution);
2102
2260
  break;
2261
+ }
2103
2262
  case 'trae':
2104
2263
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2105
2264
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2107,7 +2266,10 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2107
2266
  content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
2108
2267
  content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
2109
2268
  content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
2110
- content = content.replace(/~\/\.trae\//g, pathPrefix);
2269
+ // #2094: descriptor-driven — dirName resolves to '.trae' via
2270
+ // getDirName()/localConfigDir, so this regex is built rather than
2271
+ // hardcoded as `/~\/\.trae\//g` (byte-identical output for trae).
2272
+ content = content.replace(new RegExp('~/' + escapeRegExp(dirName) + '/', 'g'), pathPrefix);
2111
2273
  content = processAttribution(content, attribution);
2112
2274
  break;
2113
2275
  case 'codebuddy':
@@ -2135,9 +2297,20 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2135
2297
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2136
2298
  content = processAttribution(content, attribution);
2137
2299
  break;
2138
- case 'qwen':
2139
- content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
2140
- content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
2300
+ // Descriptor-driven brand literals (ADR-1239 / #2092): the qwen/hermes
2301
+ // brand VALUES (CLAUDE.md/Claude Code/.claude/ replacements) now read from
2302
+ // runtime.hostBehaviors.brandingRewrites instead of hardcoded literals.
2303
+ // EXACT regexes/order preserved — only the replacement values changed.
2304
+ case 'qwen': {
2305
+ // Guarded (post-review #2092): brandingRewrites is undefined if the
2306
+ // capability registry fails to load — degrade closed (skip the
2307
+ // brand-literal replacements, still apply the non-branding path
2308
+ // rewrites below) instead of throwing on `_b['CLAUDE.md']`.
2309
+ const _b = _hostBehaviors(runtime).brandingRewrites;
2310
+ if (_b) {
2311
+ content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2312
+ content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
2313
+ }
2141
2314
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2142
2315
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2143
2316
  content = content.replace(/~\/\.qwen\//g, pathPrefix);
@@ -2146,14 +2319,22 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2146
2319
  content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2147
2320
  content = content.replace(/~\/\.qwen(?![\w-])/g, normalizedPathPrefix);
2148
2321
  content = content.replace(/\$HOME\/\.qwen(?![\w-])/g, normalizedPathPrefix);
2149
- content = content.replace(/\.claude\//g, '.qwen/');
2322
+ if (_b) {
2323
+ content = content.replace(/\.claude\//g, _b['.claude/']);
2324
+ }
2150
2325
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2151
2326
  content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
2152
2327
  content = processAttribution(content, attribution);
2153
2328
  break;
2154
- case 'hermes':
2155
- content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
2156
- content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
2329
+ }
2330
+ case 'hermes': {
2331
+ // Guarded (post-review #2092): see qwen case above — same degrade-closed
2332
+ // rationale.
2333
+ const _b = _hostBehaviors(runtime).brandingRewrites;
2334
+ if (_b) {
2335
+ content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2336
+ content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
2337
+ }
2157
2338
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2158
2339
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2159
2340
  content = content.replace(/~\/\.hermes\//g, pathPrefix);
@@ -2162,11 +2343,14 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2162
2343
  content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2163
2344
  content = content.replace(/~\/\.hermes(?![\w-])/g, normalizedPathPrefix);
2164
2345
  content = content.replace(/\$HOME\/\.hermes(?![\w-])/g, normalizedPathPrefix);
2165
- content = content.replace(/\.claude\//g, '.hermes/');
2346
+ if (_b) {
2347
+ content = content.replace(/\.claude\//g, _b['.claude/']);
2348
+ }
2166
2349
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2167
2350
  content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
2168
2351
  content = processAttribution(content, attribution);
2169
2352
  break;
2353
+ }
2170
2354
  case 'kimi':
2171
2355
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2172
2356
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2241,8 +2425,11 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
2241
2425
  continue;
2242
2426
  let content = node_fs_1.default.readFileSync(node_path_1.default.join(stagedDir, entry.name), 'utf8');
2243
2427
  content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
2244
- if (runtime === 'augment') {
2245
- content = convertClaudeToAugmentMarkdown(content);
2428
+ // #2097 (ADR-1239): descriptor-driven — commandBodyConverter name comes
2429
+ // from runtime.hostBehaviors instead of a hardcoded runtime-name branch.
2430
+ const _cmdConv = _hostBehaviors(runtime).commandBodyConverter;
2431
+ if (_cmdConv && COMMAND_BODY_CONVERTERS[_cmdConv]) {
2432
+ content = COMMAND_BODY_CONVERTERS[_cmdConv](content);
2246
2433
  }
2247
2434
  node_fs_1.default.writeFileSync(node_path_1.default.join(tempDir, entry.name), content);
2248
2435
  }
@@ -2312,25 +2499,21 @@ function rewriteStagedCommandBodies(stagedDir, opts) {
2312
2499
  const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
2313
2500
  return applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal, attribution);
2314
2501
  }
2315
- /**
2316
- * Runtimes that use the hyphen-namespace form `/gsd-<cmd>` in agent bodies.
2317
- * claude/qwen/hermes use hyphen-name:`...` frontmatter; cursor/windsurf/etc
2318
- * self-convert. Mirrors the `HYPHEN_NAME_AGENT_RUNTIMES` set in bin/install.js.
2319
- *
2320
- * @private — export normalizeAgentBodyForRuntime for callers.
2321
- */
2322
- const HYPHEN_NAME_AGENT_RUNTIMES = new Set(['claude', 'qwen', 'hermes']);
2323
2502
  /**
2324
2503
  * Normalize `/gsd:<cmd>` colon refs in the agent body to `/gsd-<cmd>` for
2325
- * hyphen-`name:` runtimes (claude / qwen / hermes). No-op for all other
2326
- * runtimes. Mirrors the per-file call in bin/install.js line 9400.
2504
+ * runtimes that declare `runtime.hostBehaviors.hyphenNameAgentBody` on their
2505
+ * descriptor (claude / qwen / hermes use hyphen-`name:` frontmatter;
2506
+ * cursor/windsurf/etc self-convert and don't declare the flag). Descriptor-
2507
+ * driven (ADR-1239 / #2092) — folded from the hardcoded
2508
+ * `HYPHEN_NAME_AGENT_RUNTIMES` allow-list set. Mirrors the per-file call in
2509
+ * bin/install.js line 9370 / `shouldNormalizeHyphenNamespaceInAgentBody`.
2327
2510
  *
2328
2511
  * @param content raw agent file content (post-converter)
2329
2512
  * @param runtime canonical runtime ID
2330
2513
  * @param cmdNames gsd command names from readGsdCommandNames()
2331
2514
  */
2332
2515
  function normalizeAgentBodyForRuntime(content, runtime, cmdNames) {
2333
- if (!HYPHEN_NAME_AGENT_RUNTIMES.has(runtime))
2516
+ if (_hostBehaviors(runtime).hyphenNameAgentBody !== true)
2334
2517
  return content;
2335
2518
  return transformContentToHyphen(content, cmdNames);
2336
2519
  }
@@ -2342,9 +2525,13 @@ function normalizeAgentBodyForRuntime(content, runtime, cmdNames) {
2342
2525
  * ~/\.claude\b → normalizedPathPrefix
2343
2526
  * $HOME/\.claude\b → normalizedPathPrefix
2344
2527
  *
2345
- * Skipped for copilot and antigravity (which do NOT do path rewrites in the
2346
- * inline loop). NO stamp (_stampNonClaudeRuntimeDefaults) — agents are NOT
2347
- * stamped in the inline loop.
2528
+ * Skipped for any runtime that declares `hostBehaviors.noPathRewrite`
2529
+ * (descriptor-driven, ADR-1239 / #2096 — folds the prior hardcoded
2530
+ * `runtime === 'antigravity'` literal; Antigravity does NOT do path rewrites
2531
+ * in the inline loop / #2103 — folds the prior hardcoded
2532
+ * `runtime === 'copilot'` literal onto the same descriptor field, since
2533
+ * copilot also skips these rewrites). NO stamp
2534
+ * (_stampNonClaudeRuntimeDefaults) — agents are NOT stamped in the inline loop.
2348
2535
  *
2349
2536
  * ADR-1235 §1: pre-converter cross-cutting for descriptor-driven agent pipeline.
2350
2537
  * Exported as `applyAgentPathRewrites` for testing and for injection into
@@ -2353,10 +2540,10 @@ function normalizeAgentBodyForRuntime(content, runtime, cmdNames) {
2353
2540
  * @param content raw agent file content
2354
2541
  * @param runtime canonical runtime ID
2355
2542
  * @param pathPrefix trailing-slash path prefix (e.g. '$HOME/.cursor/')
2356
- * @returns content with path-prefix rewrites applied (or unchanged for copilot/antigravity)
2543
+ * @returns content with path-prefix rewrites applied (or unchanged for noPathRewrite runtimes, e.g. copilot)
2357
2544
  */
2358
2545
  function applyAgentPathRewrites(content, runtime, pathPrefix) {
2359
- if (runtime === 'copilot' || runtime === 'antigravity')
2546
+ if (_hostBehaviors(runtime).noPathRewrite === true)
2360
2547
  return content;
2361
2548
  const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
2362
2549
  content = content.replace(/~\/\.claude\//g, pathPrefix);
@@ -2392,6 +2579,11 @@ function processAttribution(content, attribution) {
2392
2579
  }
2393
2580
  module.exports = {
2394
2581
  processAttribution,
2582
+ // #2103: public accessor for hostBehaviors.agentFileExtension, exported so
2583
+ // surface.cts's _syncGsdDir can derive the .agent.md rename from the SAME
2584
+ // descriptor read as install-engine.cts (folds a duplicated hardcoded
2585
+ // `runtime === 'copilot'` literal).
2586
+ agentFileExtensionFor,
2395
2587
  yamlIdentifier,
2396
2588
  yamlQuote,
2397
2589
  toSingleLine,
@@ -2451,6 +2643,11 @@ module.exports = {
2451
2643
  convertClaudeAgentToCodebuddyAgent,
2452
2644
  convertClaudeAgentToClineAgent,
2453
2645
  convertClaudeAgentToCodexAgent,
2646
+ // ADR-1239 / #2092 Phase B Upgrade 1: native .qwen/agents/*.md subagent
2647
+ // projection — registered by name so convertedAgentsKind's
2648
+ // conversionExports[converterName] dispatch (runtime-artifact-layout.cts)
2649
+ // can resolve it from capabilities/qwen/capability.json's agents kind.
2650
+ convertClaudeAgentToQwenAgent,
2454
2651
  // #1511 ADR-1508 Phase 2: rewrite engine deep seam
2455
2652
  // Low-level walkers (pathPrefix + attribution pre-resolved by caller):
2456
2653
  applyRuntimeContentRewritesInPlace,
@@ -130,19 +130,27 @@ function agentsKind(destSubpath, prefix, configDir) {
130
130
  * Agent filenames are preserved verbatim (the prefix is already embedded in the
131
131
  * agent stem — e.g. `gsd-planner.md`).
132
132
  *
133
- * #1173 SCOPE — plumbing only (declarations deferred): this provides the
134
- * converter dispatch + `isGlobal` scope threading for the descriptor's `agents`
135
- * kind, but NO runtime currently declares a converted `agents` kind in its
136
- * `capability.json`. The descriptor declarations for the 8 non-Claude runtimes
137
- * (copilot/antigravity/cursor/windsurf/augment/trae/codebuddy/cline) are
138
- * DEFERRED to a follow-up that first ships the ADR-1235 §0 byte-for-byte parity
139
- * harness, because the second `layout.kinds` consumer — `applySurface` /
140
- * `/gsd:surface` / `--materialize` (`src/surface.cts`) — does not yet mirror the
141
- * legacy agent pipeline (Copilot's `.agent.md` filename rename, the cross-cutting
142
- * path-prefix rewrite + attribution, stale-file cleanup, config-reading steps),
143
- * so declaring the kind now would regress the surface path. Until then the legacy
144
- * `bin/install.js` agent loop remains authoritative for the real install, and
145
- * this `convertedAgentsKind` is exercised only by synthetic-descriptor seam tests.
133
+ * #1173 SCOPE — plumbing only (real install still elsewhere): this provides
134
+ * the converter dispatch + `isGlobal` scope threading for the descriptor's
135
+ * `agents` kind. As of #2092, 8 non-Claude runtimes DO declare a converted
136
+ * `agents` kind in their `capability.json` — qwen (`convertClaudeAgentToQwenAgent`)
137
+ * plus the 7 that already declared one before it (antigravity, augment,
138
+ * codebuddy, copilot, cursor, trae, windsurf) — so the descriptor-level
139
+ * declaration is no longer deferred. What IS still deferred is wiring
140
+ * `resolveRuntimeArtifactLayout`'s `agents` kind into the REAL install:
141
+ * `bin/install.js`'s agent-staging loop does not consume this module's
142
+ * `convertedAgentsKind` resolution at all — it dispatches the very same
143
+ * converter functions directly via `_hostBehaviors(runtime)` checks
144
+ * (`frontmatterDialect`, `brandingRewrites`, `isCopilot`/`isAntigravity`/…),
145
+ * duplicating the mapping declared here. That duplication is deliberate until
146
+ * the second `layout.kinds` consumer — `applySurface` / `/gsd:surface` /
147
+ * `--materialize` (`src/surface.cts`) — mirrors the legacy agent pipeline
148
+ * (Copilot's `.agent.md` filename rename, the cross-cutting path-prefix
149
+ * rewrite + attribution, stale-file cleanup, config-reading steps); declaring
150
+ * `bin/install.js` itself against this resolver before then would risk
151
+ * regressing the surface path. Until that follow-up lands, `bin/install.js`
152
+ * remains authoritative for the real install, and this `convertedAgentsKind`
153
+ * is exercised only by `/gsd:surface` and synthetic-descriptor seam tests.
146
154
  *
147
155
  * Mirrors the `convertedCommandsKind` pattern (#785).
148
156
  *
@@ -224,6 +232,7 @@ function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nest
224
232
  kind: 'skills',
225
233
  destSubpath,
226
234
  prefix,
235
+ converter: converterName,
227
236
  stage: (resolved) => {
228
237
  const realConverter = conversionExports[converterName];
229
238
  // Compute cmdNames once per stage call for performance (#3583).
@@ -21,17 +21,30 @@
21
21
  * false for codex / copilot / kilo / cursor / windsurf / trae / cline / kimi (legacy exclusion list).
22
22
  * true for all other runtimes.
23
23
  * - `finishPermissionWriter` names the finishInstall-phase dedicated config writer:
24
- * 'opencode' → writes BOTH shared settings AND its own permissions file.
25
- * 'kilo' → writes only its own permissions file.
26
- * null → no dedicated permission writer.
24
+ * 'opencode' → writes BOTH shared settings AND its own permissions file.
25
+ * 'kilo' → writes only its own permissions file.
26
+ * 'antigravity' → writes BOTH shared settings.json permissions.allow AND a
27
+ * standalone mcp_config.json MCP companion profile (#2096
28
+ * Phase B Upgrades 1+2).
29
+ * null → no dedicated permission writer.
27
30
  */
28
31
  // eslint-disable-next-line @typescript-eslint/no-require-imports
29
32
  const { runtimes } = require('./capability-registry.cjs');
30
33
  /** Valid sandboxTier enum values — mirrors the gen-capability-registry validator vocabulary. */
31
34
  const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
32
- /** The complete set of 16 supported runtimes for config-adapter dispatch. */
35
+ /**
36
+ * The complete set of 16 supported runtimes for config-adapter dispatch.
37
+ *
38
+ * Excludes runtimes whose installSurface is 'none' (#2103 — e.g. VS Code): a
39
+ * 'none' installSurface means the runtime has NO CLI install surface at all
40
+ * (Marketplace/VSIX-distributed, never dispatched through
41
+ * install()/finishInstall()), so it is not a "config-adapter runtime" by
42
+ * definition. This keeps this set in lockstep with bin/install.js's
43
+ * `allRuntimes` (see tests/issue-57-runtime-install-no-drift.test.cjs) without
44
+ * needing a separate hand-kept exclusion list.
45
+ */
33
46
  const ALLOWED_CONFIG_RUNTIMES = new Set(Object.entries(runtimes)
34
- .filter(([, cap]) => cap && cap.runtime && typeof cap.runtime['installSurface'] === 'string')
47
+ .filter(([, cap]) => cap && cap.runtime && typeof cap.runtime['installSurface'] === 'string' && cap.runtime['installSurface'] !== 'none')
35
48
  .map(([id]) => id));
36
49
  /** All valid installSurface values. */
37
50
  const INSTALL_SURFACES = Object.freeze([
@@ -41,6 +54,7 @@ const INSTALL_SURFACES = Object.freeze([
41
54
  'cline-rules',
42
55
  'cursor-hooks-json',
43
56
  'profile-marker-only',
57
+ 'none',
44
58
  ]);
45
59
  /**
46
60
  * Resolve the config adapter intent for a given runtime.
@@ -38,6 +38,7 @@ exports.resolveConfigHomeFromDescriptor = resolveConfigHomeFromDescriptor;
38
38
  exports.resolveAntigravityGlobalDir = resolveAntigravityGlobalDir;
39
39
  exports.detectAntigravityDirAmbiguity = detectAntigravityDirAmbiguity;
40
40
  exports.resolveKimiGlobalDir = resolveKimiGlobalDir;
41
+ exports.resolveKimiHooksTomlDir = resolveKimiHooksTomlDir;
41
42
  exports.getGlobalConfigDir = getGlobalConfigDir;
42
43
  exports.resolveSkillsBaseFromDescriptor = resolveSkillsBaseFromDescriptor;
43
44
  exports.getGlobalSkillsBase = getGlobalSkillsBase;
@@ -255,6 +256,27 @@ function resolveKimiGlobalDir(opts = {}) {
255
256
  probeExists: 'skills',
256
257
  }, { env, home, existsSync: existsSyncFn });
257
258
  }
259
+ /**
260
+ * Resolve the directory holding Kimi CLI's OWN native config.toml (the file
261
+ * Kimi itself reads for providers/models/hooks/etc — see
262
+ * moonshotai.github.io/kimi-cli/en/configuration/data-locations.html and
263
+ * .../reference/kimi-command.html). Default `~/.kimi`, overridden by
264
+ * `KIMI_SHARE_DIR` per Kimi's own upstream env-var (NOT `KIMI_CONFIG_DIR`,
265
+ * which is a GSD-installer write-location override for the unrelated generic
266
+ * Agent-Skills root resolved by resolveKimiGlobalDir above).
267
+ *
268
+ * This is deliberately a SEPARATE directory from GSD's kimi configHome
269
+ * (~/.config/agents): Kimi's own docs confirm the Agent-Skills search path is
270
+ * independent of KIMI_SHARE_DIR ("This variable does not affect Agent Skills
271
+ * search paths, which are handled separately"). #2095 Upgrade 1 writes GSD's
272
+ * native [[hooks]] entries into `<this dir>/config.toml`, never into the
273
+ * skills configDir.
274
+ */
275
+ function resolveKimiHooksTomlDir(opts = {}) {
276
+ const env = opts.env ?? process.env;
277
+ const home = opts.home ?? node_os_1.default.homedir();
278
+ return resolveConfigHomeFromDescriptor({ kind: 'dot-home', name: '.kimi', env: ['KIMI_SHARE_DIR'] }, { env, home });
279
+ }
258
280
  /**
259
281
  * Return the global config base directory for the given runtime.
260
282
  * Respects the same env-var overrides as bin/install.js getGlobalDir().