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

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 (112) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-executor.md +2 -1
  4. package/agents/gsd-security-auditor.md +13 -15
  5. package/agents/gsd-ui-checker.md +2 -0
  6. package/agents/gsd-ui-researcher.md +1 -0
  7. package/bin/install.js +975 -196
  8. package/commands/gsd/mempalace-capture.md +27 -1
  9. package/commands/gsd/surface.md +6 -6
  10. package/gsd-core/bin/gsd-tools.cjs +63 -2
  11. package/gsd-core/bin/lib/api-coverage.cjs +3 -4
  12. package/gsd-core/bin/lib/audit.cjs +7 -6
  13. package/gsd-core/bin/lib/capability-registry.cjs +503 -87
  14. package/gsd-core/bin/lib/capability-validator.cjs +56 -18
  15. package/gsd-core/bin/lib/check-command-router.cjs +1 -1
  16. package/gsd-core/bin/lib/clock.cjs +19 -0
  17. package/gsd-core/bin/lib/commands.cjs +48 -9
  18. package/gsd-core/bin/lib/config-loader.cjs +6 -2
  19. package/gsd-core/bin/lib/config.cjs +12 -0
  20. package/gsd-core/bin/lib/core-utils.cjs +8 -2
  21. package/gsd-core/bin/lib/drift.cjs +4 -4
  22. package/gsd-core/bin/lib/frontmatter.cjs +22 -0
  23. package/gsd-core/bin/lib/gsd2-import.cjs +2 -1
  24. package/gsd-core/bin/lib/host-integration.cjs +33 -8
  25. package/gsd-core/bin/lib/init.cjs +60 -53
  26. package/gsd-core/bin/lib/install-engine.cjs +93 -22
  27. package/gsd-core/bin/lib/installer-migration-authoring.cjs +2 -1
  28. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  29. package/gsd-core/bin/lib/installer-migrations.cjs +1 -1
  30. package/gsd-core/bin/lib/markdown-sectionizer.cjs +342 -0
  31. package/gsd-core/bin/lib/markdown-table.cjs +698 -0
  32. package/gsd-core/bin/lib/mcp-server.cjs +18 -7
  33. package/gsd-core/bin/lib/milestone.cjs +217 -31
  34. package/gsd-core/bin/lib/phase-command-router.cjs +50 -2
  35. package/gsd-core/bin/lib/phase-lifecycle.cjs +62 -36
  36. package/gsd-core/bin/lib/phase-locator.cjs +23 -2
  37. package/gsd-core/bin/lib/phase.cjs +436 -61
  38. package/gsd-core/bin/lib/plan-scan.cjs +3 -0
  39. package/gsd-core/bin/lib/review-reviewer-selection.cjs +24 -7
  40. package/gsd-core/bin/lib/roadmap-parser.cjs +218 -13
  41. package/gsd-core/bin/lib/roadmap.cjs +100 -49
  42. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +242 -44
  43. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +3 -2
  44. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +24 -14
  45. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +19 -5
  46. package/gsd-core/bin/lib/runtime-homes.cjs +22 -0
  47. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +526 -29
  48. package/gsd-core/bin/lib/runtime-name-policy.cjs +63 -5
  49. package/gsd-core/bin/lib/schema-detect.cjs +2 -1
  50. package/gsd-core/bin/lib/security.cjs +7 -37
  51. package/gsd-core/bin/lib/shell-command-projection.cjs +176 -27
  52. package/gsd-core/bin/lib/smart-entry.cjs +4 -3
  53. package/gsd-core/bin/lib/stale-bake-guard.cjs +30 -10
  54. package/gsd-core/bin/lib/state-transition.cjs +100 -45
  55. package/gsd-core/bin/lib/state.cjs +391 -126
  56. package/gsd-core/bin/lib/surface.cjs +12 -8
  57. package/gsd-core/bin/lib/template.cjs +2 -1
  58. package/gsd-core/bin/lib/uat.cjs +54 -8
  59. package/gsd-core/bin/lib/ui-consideration-probe.cjs +249 -0
  60. package/gsd-core/bin/lib/ui-safety-gate.cjs +23 -1
  61. package/gsd-core/bin/lib/verify.cjs +4 -3
  62. package/gsd-core/bin/lib/workstream.cjs +3 -2
  63. package/gsd-core/bin/lib/worktree-safety.cjs +1 -1
  64. package/gsd-core/bin/lib/write-set.cjs +38 -0
  65. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  66. package/gsd-core/bin/shared/model-catalog.json +8 -3
  67. package/gsd-core/references/checkpoints.md +12 -0
  68. package/gsd-core/references/ui-consideration-probe.md +73 -0
  69. package/gsd-core/templates/UI-SPEC.md +25 -0
  70. package/gsd-core/templates/VALIDATION.md +2 -0
  71. package/gsd-core/workflows/add-tests.md +1 -1
  72. package/gsd-core/workflows/audit-milestone.md +7 -4
  73. package/gsd-core/workflows/debug.md +2 -0
  74. package/gsd-core/workflows/execute-phase.md +5 -3
  75. package/gsd-core/workflows/fast.md +8 -22
  76. package/gsd-core/workflows/plan-phase.md +6 -0
  77. package/gsd-core/workflows/progress.md +2 -2
  78. package/gsd-core/workflows/quick.md +2 -0
  79. package/gsd-core/workflows/review.md +42 -3
  80. package/gsd-core/workflows/secure-phase.md +1 -1
  81. package/gsd-core/workflows/settings-advanced.md +7 -4
  82. package/gsd-core/workflows/ship.md +8 -2
  83. package/gsd-core/workflows/spec-phase.md +1 -1
  84. package/gsd-core/workflows/transition.md +1 -1
  85. package/gsd-core/workflows/ui-phase.md +146 -1
  86. package/gsd-core/workflows/validate-phase.md +2 -2
  87. package/hooks/dist/gsd-statusline.js +164 -14
  88. package/hooks/dist/gsd-windsurf-pre-command.js +275 -0
  89. package/hooks/dist/gsd-windsurf-pre-write.js +132 -0
  90. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  91. package/hooks/gsd-statusline.js +164 -14
  92. package/hooks/gsd-windsurf-pre-command.js +275 -0
  93. package/hooks/gsd-windsurf-pre-write.js +132 -0
  94. package/hooks/managed-hooks-registry.cjs +2 -0
  95. package/package.json +10 -4
  96. package/pi/gsd.cjs +354 -0
  97. package/scripts/build-hooks.js +3 -0
  98. package/scripts/ci-test-scope.cjs +39 -1
  99. package/scripts/gen-golden-install-parity-zcode.cjs +35 -35
  100. package/scripts/gen-install-tree-fixtures.cjs +75 -0
  101. package/scripts/gen-registry.cjs +128 -0
  102. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -1
  103. package/scripts/lint-table-schema-drift.cjs +157 -0
  104. package/scripts/lint-test-file-count.allowlist.json +2 -1
  105. package/scripts/registry-schema.cjs +565 -0
  106. package/scripts/validate-registry.cjs +117 -0
  107. package/skills/gsd-mempalace-capture/SKILL.md +27 -1
  108. package/skills/gsd-surface/SKILL.md +6 -6
  109. package/vscode/browser.js +197 -0
  110. package/vscode/extension.js +383 -0
  111. package/vscode/host-binding.js +113 -0
  112. package/vscode/package.json +96 -0
@@ -26,6 +26,7 @@ const { readGsdCommandNames, transformContentToHyphen } = commandRoster;
26
26
  const runtimeNamePolicy = require("./runtime-name-policy.cjs");
27
27
  const { getDirName } = runtimeNamePolicy;
28
28
  const capabilityRegistry = require("./capability-registry.cjs");
29
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
29
30
  // #1383: resolve GSD's version WITHOUT a top-level
30
31
  // `require('../../../package.json')`. That require ran at module load on every
31
32
  // gsd-tools invocation (this module sits in the gsd-tools loader chain) and
@@ -66,6 +67,38 @@ function gsdVersion() {
66
67
  cachedVersion = resolveVersionFrom(__dirname);
67
68
  return cachedVersion;
68
69
  }
70
+ /**
71
+ * Host-specific install behaviors declared on the runtime descriptor
72
+ * (capabilities/<runtime>/capability.json -> runtime.hostBehaviors). Mirrors
73
+ * bin/install.js's / install-engine.cts's `_hostBehaviors` (ADR-1239 / #2086
74
+ * / #2092). Returns {} for runtimes that declare none, so every behavior
75
+ * branch degrades to the generic path by default. Unlike the bin/install.js
76
+ * and install-engine.cts variants, this module already imports
77
+ * `capabilityRegistry` statically (see NON_CLAUDE_RUNTIMES below), so this
78
+ * reads it directly rather than re-require()-ing inside a try/catch.
79
+ */
80
+ function _hostBehaviors(runtime) {
81
+ return ((capabilityRegistry &&
82
+ capabilityRegistry.runtimes &&
83
+ capabilityRegistry.runtimes[runtime] &&
84
+ capabilityRegistry.runtimes[runtime].runtime &&
85
+ capabilityRegistry.runtimes[runtime].runtime.hostBehaviors) ||
86
+ {});
87
+ }
88
+ /**
89
+ * Public accessor for the `hostBehaviors.agentFileExtension` descriptor field
90
+ * (ADR-1239 / #2099 / #2103). Returns the runtime's declared agent-file
91
+ * destination-suffix rename target (e.g. copilot's `.agent.md`), or
92
+ * `undefined` when the runtime declares none (the generic no-rename
93
+ * default). Exported so callers outside this module (surface.cts's
94
+ * `_syncGsdDir`) can derive the SAME rename decision as
95
+ * install-engine.cts's staged-copy loop from ONE descriptor read, instead of
96
+ * duplicating a hardcoded `runtime === 'copilot'` check (#2103 fold).
97
+ */
98
+ function agentFileExtensionFor(runtime) {
99
+ const ext = _hostBehaviors(runtime).agentFileExtension;
100
+ return typeof ext === 'string' ? ext : undefined;
101
+ }
69
102
  const colorNameToHex = {
70
103
  cyan: '#00FFFF',
71
104
  red: '#FF0000',
@@ -412,10 +445,12 @@ function convertClaudeCommandToClaudeSkill(content, skillName, runtime = null, c
412
445
  if (version)
413
446
  fm += `version: ${yamlQuote(version)}\n`;
414
447
  }
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') {
448
+ // #778 (b) — numeric priority for /skills ordering, declared on the runtime
449
+ // descriptor (runtime.hostBehaviors.skillPriorityFrontmatter). Scoped to
450
+ // runtimes that declare the flag so Claude/Hermes skill frontmatter is
451
+ // unchanged (they ignore the field, but we keep their output byte-stable).
452
+ // skillName is the `gsd-<stem>` dir name. (ADR-1239 / #2092)
453
+ if (_hostBehaviors(runtime).skillPriorityFrontmatter) {
419
454
  const stem = typeof skillName === 'string' && skillName.startsWith('gsd-')
420
455
  ? skillName.slice(4)
421
456
  : skillName;
@@ -465,6 +500,14 @@ function convertGsdCommandReferencesToKimiSkillInvocations(content, cmdNames) {
465
500
  .replace(colonPattern, (_, cmd) => `/skill:gsd-${cmd}`)
466
501
  .replace(hyphenPattern, (_, cmd) => `/skill:gsd-${cmd}`);
467
502
  }
503
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
504
+ // convertClaudeCommandToKimiSkill (kept for bin/install.js's own
505
+ // module-level export/test surface; dead for the live skills-install path,
506
+ // which routes here via install-engine.cts's SKILLS_CONVERTER_REGISTRY
507
+ // through the kimi capability descriptor's artifactLayout
508
+ // `converter: "convertClaudeCommandToKimiSkill"`). Neither copy re-exports
509
+ // the other — mirror any behavior change into both. Guarded by the
510
+ // output-parity test in tests/runtime-converters.test.cjs (#2095).
468
511
  function convertClaudeCommandToKimiSkill(content, skillName, _runtime = null, cmdNames = null) {
469
512
  const { frontmatter, body } = extractFrontmatterAndBody(content);
470
513
  const kimiSkillName = normalizeKimiSkillName(skillName);
@@ -592,6 +635,15 @@ function buildKimiSubagentYaml({ name, description, tools }) {
592
635
  pushKimiToolsYaml(lines, 2, tools);
593
636
  return `${lines.join('\n')}\n`;
594
637
  }
638
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
639
+ // buildKimiAgentArtifacts (kept for bin/install.js's own module-level
640
+ // export/test surface; dead for the live install path, which routes here via
641
+ // runtime-artifact-layout.cts's kimiAgentsKind through a dynamic
642
+ // `conversionExports['buildKimiAgentArtifacts']` lookup against this
643
+ // compiled module). Neither copy re-exports the other — mirror any behavior
644
+ // change into both, including the kimi_cli.tools.agent:Agent grant that
645
+ // enables background dispatch (#2095 Upgrade 2). Guarded by the
646
+ // output-parity test in tests/runtime-converters.test.cjs (#2095).
595
647
  function buildKimiAgentArtifacts({ rootAgent = '', subagents = [], requestedSubagents = null, } = {}) {
596
648
  const diagnostics = [];
597
649
  const rootSource = parseKimiAgentSource(rootAgent) || { path: null, content: '' };
@@ -971,6 +1023,10 @@ function convertClaudeToAugmentMarkdown(content) {
971
1023
  converted = converted.replace(/\bClaude Code\b/g, 'Augment');
972
1024
  return converted;
973
1025
  }
1026
+ // #2097 (ADR-1239): command-body converters selected by descriptor
1027
+ // (runtime.hostBehaviors.commandBodyConverter) instead of a runtime-name
1028
+ // branch. Degrade-closed: unknown/absent name → no conversion.
1029
+ const COMMAND_BODY_CONVERTERS = { convertClaudeToAugmentMarkdown };
974
1030
  function getAugmentSkillAdapterHeader(skillName) {
975
1031
  return `<augment_skill_adapter>
976
1032
  ## A. Skill Invocation
@@ -1045,6 +1101,12 @@ function convertClaudeToTraeMarkdown(content) {
1045
1101
  converted = converted.replace(/\bClaude Code\b/g, 'Trae');
1046
1102
  return converted;
1047
1103
  }
1104
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
1105
+ // convertClaudeCommandToTraeSkill (dead for the live skills-install path,
1106
+ // which routes here via install-engine.cts's SKILLS_CONVERTER_REGISTRY; kept
1107
+ // for bin/install.js's own module-level export/test surface). Neither copy
1108
+ // re-exports the other — mirror any behavior change into both. Guarded by
1109
+ // the output-parity test in tests/runtime-converters.test.cjs (#2094).
1048
1110
  function convertClaudeCommandToTraeSkill(content, skillName) {
1049
1111
  const converted = convertClaudeToTraeMarkdown(content);
1050
1112
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -1059,7 +1121,17 @@ function convertClaudeCommandToTraeSkill(content, skillName) {
1059
1121
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1060
1122
  // #2876: quote so YAML flow indicators (`[BETA] …`) don't break Trae's
1061
1123
  // frontmatter parser.
1062
- return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n${body}`;
1124
+ let fm = `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n`;
1125
+ // #2094: emit `stage:` so Trae's SOLO agent can auto-invoke GSD skills at
1126
+ // the corresponding stage (docs.trae.ai/ide/agent). The field name/schema
1127
+ // is not formally documented (thin SPA docs) — descriptor-driven, single
1128
+ // fixed GSD-side value (runtime.hostBehaviors.soloStageMetadata), inferred/
1129
+ // best-effort.
1130
+ const soloStage = _hostBehaviors('trae').soloStageMetadata;
1131
+ if (soloStage)
1132
+ fm += `stage: ${soloStage}\n`;
1133
+ fm += '---';
1134
+ return `${fm}\n${body}`;
1063
1135
  }
1064
1136
  function convertSlashCommandsToCodebuddySkillMentions(content) {
1065
1137
  return content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
@@ -1520,7 +1592,12 @@ function convertClaudeToOpencodeFrontmatter(content, { isAgent = false, modelOve
1520
1592
  return `---\n${newFrontmatter}\n---${body}`;
1521
1593
  }
1522
1594
  // Kilo CLI — same conversion logic as OpenCode, different config paths.
1523
- function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
1595
+ // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
1596
+ // convertClaudeToKiloFrontmatter (used by bin/install.js's own legacy install
1597
+ // path). Neither copy re-exports the other — mirror any behavior change into
1598
+ // both. Guarded by the output-parity test in tests/runtime-converters.test.cjs
1599
+ // (#2093).
1600
+ function convertClaudeToKiloFrontmatter(content, { isAgent = false, modelOverride = null } = {}) {
1524
1601
  // Replace tool name references in content (applies to all files)
1525
1602
  let convertedContent = content;
1526
1603
  convertedContent = convertedContent.replace(/\bAskUserQuestion\b/g, 'question');
@@ -1666,6 +1743,14 @@ function convertClaudeToKiloFrontmatter(content, { isAgent = false } = {}) {
1666
1743
  // For agents: add required Kilo agent fields
1667
1744
  if (isAgent) {
1668
1745
  newLines.push('mode: subagent');
1746
+ // Embed model override from ~/.gsd/defaults.json so model_overrides is
1747
+ // respected on Kilo (which uses static agent frontmatter, not inline
1748
+ // Task() model parameters) — mirrors convertClaudeToOpencodeFrontmatter's
1749
+ // model emission exactly (#2093 UPGRADE 2 / ADR-1239; Kilo is an OpenCode
1750
+ // fork with the same static-frontmatter model constraint). See #2256.
1751
+ if (modelOverride) {
1752
+ newLines.push(['model:', modelOverride].join(' '));
1753
+ }
1669
1754
  newLines.push(...buildKiloAgentPermissionBlock(agentTools));
1670
1755
  }
1671
1756
  // For commands: add tools object if we had allowed-tools or tools
@@ -1875,6 +1960,76 @@ function convertClaudeAgentToTraeAgent(content) {
1875
1960
  const cleanFrontmatter = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n---`;
1876
1961
  return `${cleanFrontmatter}\n${body}`;
1877
1962
  }
1963
+ /**
1964
+ * Convert a Claude agent (.md) to a native Qwen Code subagent file
1965
+ * (`.qwen/agents/gsd-*.md` / `<qwenhome>/agents/gsd-*.md`, ADR-1239 / #2092
1966
+ * Phase B Upgrade 1). Qwen Code is a Claude-dialect host: its docs' "Claude
1967
+ * Code Compatibility Fields" section confirms CC agent files parse under
1968
+ * `.qwen/agents/` (https://qwenlm.github.io/qwen-code-docs/en/users/features/sub-agents),
1969
+ * so — unlike Cursor/Trae/Copilot/Antigravity — tool names pass through
1970
+ * UNCHANGED (no remapping table).
1971
+ *
1972
+ * Emits DETERMINISTIC frontmatter: `name:` + `description:` (mirrors
1973
+ * convertClaudeAgentToCursorAgent), plus `tools:` as a YAML block list when the
1974
+ * source declares one. Qwen's documented `tools:` schema is a YAML array
1975
+ * (`tools:\n- tool1\n- tool2`), not Claude's single-line comma-separated string
1976
+ * — passing the raw single-line string through unchanged would parse as one
1977
+ * malformed tool name and be silently dropped ("Optional fields with invalid
1978
+ * values are silently dropped at parse time" — same docs page). Reuses
1979
+ * `parseFrontmatterTools` (already relied on by the Kimi agent path), which
1980
+ * tolerates BOTH source formats Claude's own agents/*.md files use — the
1981
+ * single-line comma list (most agents) and the YAML block list (e.g.
1982
+ * agents/gsd-nyquist-auditor.md, agents/gsd-security-auditor.md) — so no tools
1983
+ * are lost regardless of which the source agent uses.
1984
+ *
1985
+ * `color` IS preserved: Qwen's docs list `color` under "Claude Code
1986
+ * Compatibility Fields" as a supported optional field, so it is passed
1987
+ * through as a plain scalar (unlike the cursor/trae/augment/windsurf
1988
+ * reduced-frontmatter converters, which drop it — those hosts have no such
1989
+ * compatibility field). `model:` and `approvalMode:` are intentionally NOT
1990
+ * emitted: both are optional per the docs and out of scope for #2092 (model:
1991
+ * would couple to the model catalog and introduce nondeterminism;
1992
+ * approvalMode is a deliberate follow-on).
1993
+ *
1994
+ * Body: preserved verbatim after the qwen branding rewrite (CLAUDE.md /
1995
+ * Claude Code / .claude/ literal-substring values — descriptor-driven via
1996
+ * runtime.hostBehaviors.brandingRewrites, mirrors the qwen case in
1997
+ * _applyRuntimeRewrites). The anchored `~/.claude/` / `$HOME/.claude/` forms
1998
+ * are already rewritten upstream by applyAgentPathRewrites (agentCtx Step 1 in
1999
+ * stageAgentsForRuntimeWithConverter) before this converter runs, so only the
2000
+ * bare/non-anchored forms are handled here — mirrors how
2001
+ * convertClaudeToTraeMarkdown orders its bare-form rewrites after the slash
2002
+ * forms to avoid double-rewriting the same substring.
2003
+ */
2004
+ function convertClaudeAgentToQwenAgent(content) {
2005
+ const _b = _hostBehaviors('qwen').brandingRewrites || {};
2006
+ let converted = content;
2007
+ if (_b['CLAUDE.md'])
2008
+ converted = converted.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2009
+ if (_b['Claude Code'])
2010
+ converted = converted.replace(/\bClaude Code\b/g, _b['Claude Code']);
2011
+ if (_b['.claude/'])
2012
+ converted = converted.replace(/\.claude\//g, _b['.claude/']);
2013
+ const { frontmatter, body } = extractFrontmatterAndBody(converted);
2014
+ if (!frontmatter)
2015
+ return converted;
2016
+ const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
2017
+ const description = extractFrontmatterField(frontmatter, 'description') || '';
2018
+ const tools = parseFrontmatterTools(frontmatter);
2019
+ const color = extractFrontmatterField(frontmatter, 'color');
2020
+ let fm = `---\nname: ${yamlIdentifier(name)}\ndescription: ${yamlQuote(toSingleLine(description))}\n`;
2021
+ if (tools.length > 0) {
2022
+ fm += 'tools:\n';
2023
+ for (const tool of tools) {
2024
+ fm += ` - ${yamlIdentifier(tool)}\n`;
2025
+ }
2026
+ }
2027
+ if (color) {
2028
+ fm += `color: ${yamlIdentifier(color)}\n`;
2029
+ }
2030
+ fm += '---';
2031
+ return `${fm}\n${body}`;
2032
+ }
1878
2033
  function convertClaudeAgentToCodebuddyAgent(content) {
1879
2034
  const converted = convertClaudeToCodebuddyMarkdown(content);
1880
2035
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -1989,8 +2144,8 @@ function computePathPrefix({ isGlobal, isOpencode, isWindowsHost: _isWindowsHost
1989
2144
  // Without this, path.join on Windows produces a backslash prefix that
1990
2145
  // leaks into markdown content and breaks cross-platform substring checks.
1991
2146
  // See DEFECT.WINDOWS-PATH-LEAK-IN-MARKDOWN-CONTENT in CONTEXT.md.
1992
- const posixTarget = String(resolvedTarget).replace(/\\/g, '/');
1993
- const posixHome = homeDir ? String(homeDir).replace(/\\/g, '/') : homeDir;
2147
+ const posixTarget = (0, shell_command_projection_cjs_1.posixNormalize)(String(resolvedTarget));
2148
+ const posixHome = homeDir ? (0, shell_command_projection_cjs_1.posixNormalize)(String(homeDir)) : homeDir;
1994
2149
  if (isGlobal && posixTarget.startsWith(posixHome) && !isOpencode) {
1995
2150
  return '$HOME' + posixTarget.slice(posixHome.length) + '/';
1996
2151
  }
@@ -2087,19 +2242,24 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2087
2242
  content = processAttribution(content, attribution);
2088
2243
  break;
2089
2244
  }
2090
- case 'augment':
2245
+ case 'augment': {
2091
2246
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2092
2247
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2093
2248
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2094
2249
  content = content.replace(/~\/\.claude(?![\w-])/g, normalizedPathPrefix);
2095
2250
  content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2096
2251
  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);
2252
+ // #2097: dot-dir self-references (~/.augment/…) → resolved prefix,
2253
+ // dirName-derived (no runtime literal). getDirName('augment') resolves
2254
+ // to '.augment', so this is byte-identical to the prior hardcoded regexes.
2255
+ const _dd = escapeRegExp(dirName);
2256
+ content = content.replace(new RegExp('~/' + _dd + '/', 'g'), pathPrefix);
2257
+ content = content.replace(new RegExp('\\$HOME/' + _dd + '/', 'g'), pathPrefix);
2258
+ content = content.replace(new RegExp('~/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
2259
+ content = content.replace(new RegExp('\\$HOME/' + _dd + '(?![\\w-])', 'g'), normalizedPathPrefix);
2101
2260
  content = processAttribution(content, attribution);
2102
2261
  break;
2262
+ }
2103
2263
  case 'trae':
2104
2264
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2105
2265
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2107,7 +2267,10 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2107
2267
  content = content.replace(/~\/\.claude\b/g, normalizedPathPrefix);
2108
2268
  content = content.replace(/\$HOME\/\.claude\b/g, normalizedPathPrefix);
2109
2269
  content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
2110
- content = content.replace(/~\/\.trae\//g, pathPrefix);
2270
+ // #2094: descriptor-driven — dirName resolves to '.trae' via
2271
+ // getDirName()/localConfigDir, so this regex is built rather than
2272
+ // hardcoded as `/~\/\.trae\//g` (byte-identical output for trae).
2273
+ content = content.replace(new RegExp('~/' + escapeRegExp(dirName) + '/', 'g'), pathPrefix);
2111
2274
  content = processAttribution(content, attribution);
2112
2275
  break;
2113
2276
  case 'codebuddy':
@@ -2135,9 +2298,20 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2135
2298
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2136
2299
  content = processAttribution(content, attribution);
2137
2300
  break;
2138
- case 'qwen':
2139
- content = content.replace(/CLAUDE\.md/g, 'QWEN.md');
2140
- content = content.replace(/\bClaude Code\b/g, 'Qwen Code');
2301
+ // Descriptor-driven brand literals (ADR-1239 / #2092): the qwen/hermes
2302
+ // brand VALUES (CLAUDE.md/Claude Code/.claude/ replacements) now read from
2303
+ // runtime.hostBehaviors.brandingRewrites instead of hardcoded literals.
2304
+ // EXACT regexes/order preserved — only the replacement values changed.
2305
+ case 'qwen': {
2306
+ // Guarded (post-review #2092): brandingRewrites is undefined if the
2307
+ // capability registry fails to load — degrade closed (skip the
2308
+ // brand-literal replacements, still apply the non-branding path
2309
+ // rewrites below) instead of throwing on `_b['CLAUDE.md']`.
2310
+ const _b = _hostBehaviors(runtime).brandingRewrites;
2311
+ if (_b) {
2312
+ content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2313
+ content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
2314
+ }
2141
2315
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2142
2316
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2143
2317
  content = content.replace(/~\/\.qwen\//g, pathPrefix);
@@ -2146,14 +2320,22 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2146
2320
  content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2147
2321
  content = content.replace(/~\/\.qwen(?![\w-])/g, normalizedPathPrefix);
2148
2322
  content = content.replace(/\$HOME\/\.qwen(?![\w-])/g, normalizedPathPrefix);
2149
- content = content.replace(/\.claude\//g, '.qwen/');
2323
+ if (_b) {
2324
+ content = content.replace(/\.claude\//g, _b['.claude/']);
2325
+ }
2150
2326
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2151
2327
  content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
2152
2328
  content = processAttribution(content, attribution);
2153
2329
  break;
2154
- case 'hermes':
2155
- content = content.replace(/CLAUDE\.md/g, 'HERMES.md');
2156
- content = content.replace(/\bClaude Code\b/g, 'Hermes Agent');
2330
+ }
2331
+ case 'hermes': {
2332
+ // Guarded (post-review #2092): see qwen case above — same degrade-closed
2333
+ // rationale.
2334
+ const _b = _hostBehaviors(runtime).brandingRewrites;
2335
+ if (_b) {
2336
+ content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2337
+ content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
2338
+ }
2157
2339
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2158
2340
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
2159
2341
  content = content.replace(/~\/\.hermes\//g, pathPrefix);
@@ -2162,11 +2344,14 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2162
2344
  content = content.replace(/\$HOME\/\.claude(?![\w-])/g, normalizedPathPrefix);
2163
2345
  content = content.replace(/~\/\.hermes(?![\w-])/g, normalizedPathPrefix);
2164
2346
  content = content.replace(/\$HOME\/\.hermes(?![\w-])/g, normalizedPathPrefix);
2165
- content = content.replace(/\.claude\//g, '.hermes/');
2347
+ if (_b) {
2348
+ content = content.replace(/\.claude\//g, _b['.claude/']);
2349
+ }
2166
2350
  content = content.replace(/\.\/\.claude\//g, `./${dirName}/`);
2167
2351
  content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
2168
2352
  content = processAttribution(content, attribution);
2169
2353
  break;
2354
+ }
2170
2355
  case 'kimi':
2171
2356
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2172
2357
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2241,8 +2426,11 @@ function applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathP
2241
2426
  continue;
2242
2427
  let content = node_fs_1.default.readFileSync(node_path_1.default.join(stagedDir, entry.name), 'utf8');
2243
2428
  content = _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal, attribution);
2244
- if (runtime === 'augment') {
2245
- content = convertClaudeToAugmentMarkdown(content);
2429
+ // #2097 (ADR-1239): descriptor-driven — commandBodyConverter name comes
2430
+ // from runtime.hostBehaviors instead of a hardcoded runtime-name branch.
2431
+ const _cmdConv = _hostBehaviors(runtime).commandBodyConverter;
2432
+ if (_cmdConv && COMMAND_BODY_CONVERTERS[_cmdConv]) {
2433
+ content = COMMAND_BODY_CONVERTERS[_cmdConv](content);
2246
2434
  }
2247
2435
  node_fs_1.default.writeFileSync(node_path_1.default.join(tempDir, entry.name), content);
2248
2436
  }
@@ -2274,8 +2462,8 @@ function rewriteStagedSkillBodies(stagedDir, opts) {
2274
2462
  const { runtime, configDir, scope = 'global', homedir = () => node_os_1.default.homedir(), platform = process.platform, resolveAttribution, } = opts;
2275
2463
  if (!node_fs_1.default.existsSync(stagedDir))
2276
2464
  return;
2277
- const resolvedTarget = node_path_1.default.resolve(configDir).replace(/\\/g, '/');
2278
- const homeDir = homedir().replace(/\\/g, '/');
2465
+ const resolvedTarget = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(configDir));
2466
+ const homeDir = (0, shell_command_projection_cjs_1.posixNormalize)(homedir());
2279
2467
  const isGlobal = scope === 'global';
2280
2468
  const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite
2281
2469
  const isWindowsHost = platform === 'win32';
@@ -2303,8 +2491,8 @@ function rewriteStagedCommandBodies(stagedDir, opts) {
2303
2491
  const { runtime, configDir, scope = 'global', homedir = () => node_os_1.default.homedir(), platform = process.platform, resolveAttribution, } = opts;
2304
2492
  if (!node_fs_1.default.existsSync(stagedDir))
2305
2493
  return stagedDir;
2306
- const resolvedTarget = node_path_1.default.resolve(configDir).replace(/\\/g, '/');
2307
- const homeDir = homedir().replace(/\\/g, '/');
2494
+ const resolvedTarget = (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.resolve(configDir));
2495
+ const homeDir = (0, shell_command_projection_cjs_1.posixNormalize)(homedir());
2308
2496
  const isGlobal = scope === 'global';
2309
2497
  const isOpencode = false; // #2087: opencode installs via the combined-family engine path, never through the generic rewrite
2310
2498
  const isWindowsHost = platform === 'win32';
@@ -2312,25 +2500,21 @@ function rewriteStagedCommandBodies(stagedDir, opts) {
2312
2500
  const attribution = resolveAttribution ? resolveAttribution(runtime) : undefined;
2313
2501
  return applyRuntimeContentRewritesForCommandsInPlace(stagedDir, runtime, pathPrefix, isGlobal, attribution);
2314
2502
  }
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
2503
  /**
2324
2504
  * 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.
2505
+ * runtimes that declare `runtime.hostBehaviors.hyphenNameAgentBody` on their
2506
+ * descriptor (claude / qwen / hermes use hyphen-`name:` frontmatter;
2507
+ * cursor/windsurf/etc self-convert and don't declare the flag). Descriptor-
2508
+ * driven (ADR-1239 / #2092) — folded from the hardcoded
2509
+ * `HYPHEN_NAME_AGENT_RUNTIMES` allow-list set. Mirrors the per-file call in
2510
+ * bin/install.js line 9370 / `shouldNormalizeHyphenNamespaceInAgentBody`.
2327
2511
  *
2328
2512
  * @param content raw agent file content (post-converter)
2329
2513
  * @param runtime canonical runtime ID
2330
2514
  * @param cmdNames gsd command names from readGsdCommandNames()
2331
2515
  */
2332
2516
  function normalizeAgentBodyForRuntime(content, runtime, cmdNames) {
2333
- if (!HYPHEN_NAME_AGENT_RUNTIMES.has(runtime))
2517
+ if (_hostBehaviors(runtime).hyphenNameAgentBody !== true)
2334
2518
  return content;
2335
2519
  return transformContentToHyphen(content, cmdNames);
2336
2520
  }
@@ -2342,9 +2526,13 @@ function normalizeAgentBodyForRuntime(content, runtime, cmdNames) {
2342
2526
  * ~/\.claude\b → normalizedPathPrefix
2343
2527
  * $HOME/\.claude\b → normalizedPathPrefix
2344
2528
  *
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.
2529
+ * Skipped for any runtime that declares `hostBehaviors.noPathRewrite`
2530
+ * (descriptor-driven, ADR-1239 / #2096 — folds the prior hardcoded
2531
+ * `runtime === 'antigravity'` literal; Antigravity does NOT do path rewrites
2532
+ * in the inline loop / #2103 — folds the prior hardcoded
2533
+ * `runtime === 'copilot'` literal onto the same descriptor field, since
2534
+ * copilot also skips these rewrites). NO stamp
2535
+ * (_stampNonClaudeRuntimeDefaults) — agents are NOT stamped in the inline loop.
2348
2536
  *
2349
2537
  * ADR-1235 §1: pre-converter cross-cutting for descriptor-driven agent pipeline.
2350
2538
  * Exported as `applyAgentPathRewrites` for testing and for injection into
@@ -2353,10 +2541,10 @@ function normalizeAgentBodyForRuntime(content, runtime, cmdNames) {
2353
2541
  * @param content raw agent file content
2354
2542
  * @param runtime canonical runtime ID
2355
2543
  * @param pathPrefix trailing-slash path prefix (e.g. '$HOME/.cursor/')
2356
- * @returns content with path-prefix rewrites applied (or unchanged for copilot/antigravity)
2544
+ * @returns content with path-prefix rewrites applied (or unchanged for noPathRewrite runtimes, e.g. copilot)
2357
2545
  */
2358
2546
  function applyAgentPathRewrites(content, runtime, pathPrefix) {
2359
- if (runtime === 'copilot' || runtime === 'antigravity')
2547
+ if (_hostBehaviors(runtime).noPathRewrite === true)
2360
2548
  return content;
2361
2549
  const normalizedPathPrefix = pathPrefix.replace(/\/$/, '');
2362
2550
  content = content.replace(/~\/\.claude\//g, pathPrefix);
@@ -2392,6 +2580,11 @@ function processAttribution(content, attribution) {
2392
2580
  }
2393
2581
  module.exports = {
2394
2582
  processAttribution,
2583
+ // #2103: public accessor for hostBehaviors.agentFileExtension, exported so
2584
+ // surface.cts's _syncGsdDir can derive the .agent.md rename from the SAME
2585
+ // descriptor read as install-engine.cts (folds a duplicated hardcoded
2586
+ // `runtime === 'copilot'` literal).
2587
+ agentFileExtensionFor,
2395
2588
  yamlIdentifier,
2396
2589
  yamlQuote,
2397
2590
  toSingleLine,
@@ -2451,6 +2644,11 @@ module.exports = {
2451
2644
  convertClaudeAgentToCodebuddyAgent,
2452
2645
  convertClaudeAgentToClineAgent,
2453
2646
  convertClaudeAgentToCodexAgent,
2647
+ // ADR-1239 / #2092 Phase B Upgrade 1: native .qwen/agents/*.md subagent
2648
+ // projection — registered by name so convertedAgentsKind's
2649
+ // conversionExports[converterName] dispatch (runtime-artifact-layout.cts)
2650
+ // can resolve it from capabilities/qwen/capability.json's agents kind.
2651
+ convertClaudeAgentToQwenAgent,
2454
2652
  // #1511 ADR-1508 Phase 2: rewrite engine deep seam
2455
2653
  // Low-level walkers (pathPrefix + attribution pre-resolved by caller):
2456
2654
  applyRuntimeContentRewritesInPlace,
@@ -67,9 +67,10 @@ function createRuntimeArtifactInstallPlan(args) {
67
67
  // to kind.stage() for agents kind entries with a converter (convertedAgentsKind).
68
68
  // NO _stampNonClaudeRuntimeDefaults — agents are NOT stamped in the inline loop.
69
69
  const os = _require('node:os');
70
+ const { posixNormalize } = _require('./shell-command-projection.cjs');
70
71
  const homedirFn = homedir ?? (() => os.homedir());
71
- const resolvedTarget = path.resolve(layout.configDir).replace(/\\/g, '/');
72
- const homeDir = homedirFn().replace(/\\/g, '/');
72
+ const resolvedTarget = posixNormalize(path.resolve(layout.configDir));
73
+ const homeDir = posixNormalize(homedirFn());
73
74
  const isGlobal = scope === 'global';
74
75
  const isOpencode = layout.runtime === 'opencode';
75
76
  const isWindowsHost = (platform ?? process.platform) === 'win32';
@@ -23,6 +23,7 @@ const { stageSkillsForProfile, stageAgentsForProfile, stageAgentsForRuntimeWithC
23
23
  // eslint-disable-next-line @typescript-eslint/no-require-imports
24
24
  const runtimeArtifactConversion = require("./runtime-artifact-conversion.cjs");
25
25
  const conversionExports = runtimeArtifactConversion;
26
+ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
26
27
  // In .cts (CommonJS output) files, `require` is available as a global.
27
28
  const _require = require;
28
29
  // ---------------------------------------------------------------------------
@@ -130,19 +131,27 @@ function agentsKind(destSubpath, prefix, configDir) {
130
131
  * Agent filenames are preserved verbatim (the prefix is already embedded in the
131
132
  * agent stem — e.g. `gsd-planner.md`).
132
133
  *
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.
134
+ * #1173 SCOPE — plumbing only (real install still elsewhere): this provides
135
+ * the converter dispatch + `isGlobal` scope threading for the descriptor's
136
+ * `agents` kind. As of #2092, 8 non-Claude runtimes DO declare a converted
137
+ * `agents` kind in their `capability.json` — qwen (`convertClaudeAgentToQwenAgent`)
138
+ * plus the 7 that already declared one before it (antigravity, augment,
139
+ * codebuddy, copilot, cursor, trae, windsurf) — so the descriptor-level
140
+ * declaration is no longer deferred. What IS still deferred is wiring
141
+ * `resolveRuntimeArtifactLayout`'s `agents` kind into the REAL install:
142
+ * `bin/install.js`'s agent-staging loop does not consume this module's
143
+ * `convertedAgentsKind` resolution at all — it dispatches the very same
144
+ * converter functions directly via `_hostBehaviors(runtime)` checks
145
+ * (`frontmatterDialect`, `brandingRewrites`, `isCopilot`/`isAntigravity`/…),
146
+ * duplicating the mapping declared here. That duplication is deliberate until
147
+ * the second `layout.kinds` consumer — `applySurface` / `/gsd:surface` /
148
+ * `--materialize` (`src/surface.cts`) — mirrors the legacy agent pipeline
149
+ * (Copilot's `.agent.md` filename rename, the cross-cutting path-prefix
150
+ * rewrite + attribution, stale-file cleanup, config-reading steps); declaring
151
+ * `bin/install.js` itself against this resolver before then would risk
152
+ * regressing the surface path. Until that follow-up lands, `bin/install.js`
153
+ * remains authoritative for the real install, and this `convertedAgentsKind`
154
+ * is exercised only by `/gsd:surface` and synthetic-descriptor seam tests.
146
155
  *
147
156
  * Mirrors the `convertedCommandsKind` pattern (#785).
148
157
  *
@@ -184,7 +193,7 @@ function kimiAgentsKind(destSubpath, prefix, configDir) {
184
193
  continue;
185
194
  const agentPath = node_path_1.default.join(stagedAgents, entry.name);
186
195
  subagents.push({
187
- path: node_path_1.default.join('agents', entry.name).replace(/\\/g, '/'),
196
+ path: (0, shell_command_projection_cjs_1.posixNormalize)(node_path_1.default.join('agents', entry.name)),
188
197
  content: node_fs_1.default.readFileSync(agentPath, 'utf8'),
189
198
  });
190
199
  }
@@ -224,6 +233,7 @@ function skillsKind(destSubpath, prefix, converterName, runtime, configDir, nest
224
233
  kind: 'skills',
225
234
  destSubpath,
226
235
  prefix,
236
+ converter: converterName,
227
237
  stage: (resolved) => {
228
238
  const realConverter = conversionExports[converterName];
229
239
  // 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.