@opengsd/gsd-core 1.9.1 → 1.10.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 (219) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debugger.md +12 -246
  6. package/agents/gsd-executor.md +7 -5
  7. package/agents/gsd-integration-checker.md +3 -0
  8. package/agents/gsd-plan-checker.md +9 -0
  9. package/agents/gsd-planner.md +5 -8
  10. package/agents/gsd-roadmapper.md +21 -3
  11. package/agents/gsd-verifier.md +14 -70
  12. package/bin/install.js +453 -289
  13. package/commands/gsd/mempalace-capture.md +1 -1
  14. package/commands/gsd/new-milestone.md +1 -1
  15. package/commands/gsd/plan-phase.md +1 -1
  16. package/gsd-core/bin/gsd-tools.cjs +579 -66
  17. package/gsd-core/bin/lib/active-workstream-store.cjs +25 -0
  18. package/gsd-core/bin/lib/agent-install-check.cjs +38 -6
  19. package/gsd-core/bin/lib/api-coverage.cjs +120 -0
  20. package/gsd-core/bin/lib/audit.cjs +89 -1
  21. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  22. package/gsd-core/bin/lib/capability-registry.cjs +96 -110
  23. package/gsd-core/bin/lib/capability-validator.cjs +12 -2
  24. package/gsd-core/bin/lib/check-command-router.cjs +43 -1
  25. package/gsd-core/bin/lib/command-aliases.cjs +72 -0
  26. package/gsd-core/bin/lib/commands.cjs +26 -25
  27. package/gsd-core/bin/lib/commonjs-marker.cjs +136 -0
  28. package/gsd-core/bin/lib/config-loader.cjs +1 -0
  29. package/gsd-core/bin/lib/config.cjs +12 -1
  30. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  31. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  32. package/gsd-core/bin/lib/core-utils.cjs +91 -12
  33. package/gsd-core/bin/lib/docs.cjs +3 -2
  34. package/gsd-core/bin/lib/external-job.cjs +19 -4
  35. package/gsd-core/bin/lib/frontmatter.cjs +84 -12
  36. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  37. package/gsd-core/bin/lib/git-base-branch.cjs +58 -15
  38. package/gsd-core/bin/lib/graphify.cjs +142 -27
  39. package/gsd-core/bin/lib/gsd2-import.cjs +27 -4
  40. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  41. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  42. package/gsd-core/bin/lib/init.cjs +1021 -57
  43. package/gsd-core/bin/lib/install-engine.cjs +64 -10
  44. package/gsd-core/bin/lib/install-profiles.cjs +27 -1
  45. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  46. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  47. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  48. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  49. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  50. package/gsd-core/bin/lib/installer-migrations.cjs +87 -1
  51. package/gsd-core/bin/lib/io.cjs +28 -3
  52. package/gsd-core/bin/lib/markdown-sectionizer.cjs +6 -0
  53. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  54. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  55. package/gsd-core/bin/lib/milestone.cjs +75 -47
  56. package/gsd-core/bin/lib/phase-id.cjs +63 -0
  57. package/gsd-core/bin/lib/phase-locator.cjs +138 -45
  58. package/gsd-core/bin/lib/phase.cjs +260 -28
  59. package/gsd-core/bin/lib/plan-dependency-graph.cjs +232 -0
  60. package/gsd-core/bin/lib/planning-workspace.cjs +4 -0
  61. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  62. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +80 -0
  63. package/gsd-core/bin/lib/review-lane-descriptor.cjs +99 -0
  64. package/gsd-core/bin/lib/review-lane-runner.cjs +30 -6
  65. package/gsd-core/bin/lib/roadmap-command-router.cjs +42 -9
  66. package/gsd-core/bin/lib/roadmap-parser.cjs +100 -18
  67. package/gsd-core/bin/lib/roadmap.cjs +37 -7
  68. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +195 -62
  69. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +15 -3
  70. package/gsd-core/bin/lib/runtime-homes.cjs +154 -41
  71. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +105 -41
  72. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  73. package/gsd-core/bin/lib/shell-command-projection.cjs +113 -27
  74. package/gsd-core/bin/lib/smart-entry.cjs +12 -0
  75. package/gsd-core/bin/lib/state-transition.cjs +73 -8
  76. package/gsd-core/bin/lib/state.cjs +151 -62
  77. package/gsd-core/bin/lib/surface.cjs +12 -1
  78. package/gsd-core/bin/lib/uat-predicate.cjs +11 -1
  79. package/gsd-core/bin/lib/uat.cjs +320 -21
  80. package/gsd-core/bin/lib/unusable-input.cjs +9 -0
  81. package/gsd-core/bin/lib/verification.cjs +29 -12
  82. package/gsd-core/bin/lib/verify.cjs +10 -2
  83. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  84. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +181 -18
  85. package/gsd-core/bin/lib/workstream-inventory.cjs +519 -27
  86. package/gsd-core/bin/lib/workstream.cjs +6 -0
  87. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  88. package/gsd-core/bin/lib/worktree-safety.cjs +276 -118
  89. package/gsd-core/bin/shared/config-schema.manifest.json +2 -0
  90. package/gsd-core/references/artifact-types.md +10 -3
  91. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  92. package/gsd-core/references/debugger-techniques.md +255 -0
  93. package/gsd-core/references/research-documentation-lookup.md +5 -3
  94. package/gsd-core/references/specless-probe-fallback.md +7 -6
  95. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  96. package/gsd-core/references/worktree-branch-check.md +2 -2
  97. package/gsd-core/templates/summary-complex.md +2 -0
  98. package/gsd-core/templates/summary-minimal.md +2 -0
  99. package/gsd-core/templates/summary-standard.md +2 -0
  100. package/gsd-core/templates/summary.md +2 -0
  101. package/gsd-core/workflows/audit-milestone.md +3 -0
  102. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  103. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  104. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  105. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  106. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  107. package/gsd-core/workflows/autonomous.md +32 -69
  108. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  109. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +83 -0
  110. package/gsd-core/workflows/code-review.md +42 -160
  111. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  112. package/gsd-core/workflows/complete-milestone.md +23 -81
  113. package/gsd-core/workflows/debug.md +9 -12
  114. package/gsd-core/workflows/diagnose-issues.md +22 -0
  115. package/gsd-core/workflows/discovery-phase.md +4 -4
  116. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  117. package/gsd-core/workflows/discuss-phase-assumptions.md +5 -16
  118. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  119. package/gsd-core/workflows/docs-update.md +8 -51
  120. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +34 -2
  121. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  122. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  123. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +19 -0
  124. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  125. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  126. package/gsd-core/workflows/execute-phase.md +65 -137
  127. package/gsd-core/workflows/execute-plan.md +1 -1
  128. package/gsd-core/workflows/help/modes/full.md +6 -1
  129. package/gsd-core/workflows/ingest-docs.md +2 -1
  130. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  131. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  132. package/gsd-core/workflows/new-milestone.md +21 -38
  133. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  134. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  135. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  136. package/gsd-core/workflows/new-project.md +13 -226
  137. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  138. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  139. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  140. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  141. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  142. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  143. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  144. package/gsd-core/workflows/plan-phase.md +49 -193
  145. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  146. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  147. package/gsd-core/workflows/progress.md +11 -153
  148. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  149. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  150. package/gsd-core/workflows/quick/steps/quick-verification.md +46 -0
  151. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  152. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  153. package/gsd-core/workflows/quick.md +20 -390
  154. package/gsd-core/workflows/resume-project.md +3 -0
  155. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  156. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  157. package/gsd-core/workflows/review.md +15 -8
  158. package/gsd-core/workflows/section-manifest.json +219 -0
  159. package/gsd-core/workflows/sketch.md +1 -1
  160. package/gsd-core/workflows/spec-phase.md +17 -14
  161. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  162. package/gsd-core/workflows/spike.md +50 -16
  163. package/gsd-core/workflows/sync-skills.md +49 -11
  164. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  165. package/gsd-core/workflows/transition.md +8 -21
  166. package/gsd-core/workflows/ui-phase.md +8 -7
  167. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  168. package/gsd-core/workflows/update.md +18 -7
  169. package/gsd-core/workflows/verify-phase.md +4 -7
  170. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  171. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  172. package/gsd-core/workflows/verify-work.md +8 -58
  173. package/hooks/dist/gsd-agent-isolation-guard.js +428 -0
  174. package/hooks/dist/gsd-check-update-worker.js +14 -5
  175. package/hooks/dist/gsd-cursor-subagent-start.js +532 -26
  176. package/hooks/dist/gsd-read-injection-scanner.js +7 -0
  177. package/hooks/dist/gsd-statusline.js +72 -6
  178. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  179. package/hooks/dist/gsd-write-guard.js +359 -0
  180. package/hooks/dist/lib/isolation-sentinel.js +268 -0
  181. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  182. package/hooks/gsd-agent-isolation-guard.js +428 -0
  183. package/hooks/gsd-check-update-worker.js +14 -5
  184. package/hooks/gsd-cursor-subagent-start.js +532 -26
  185. package/hooks/gsd-read-injection-scanner.js +7 -0
  186. package/hooks/gsd-statusline.js +72 -6
  187. package/hooks/gsd-worktree-path-guard.js +2 -1
  188. package/hooks/gsd-write-guard.js +359 -0
  189. package/hooks/hooks.json +12 -0
  190. package/hooks/lib/isolation-sentinel.js +268 -0
  191. package/hooks/managed-hooks-registry.cjs +2 -0
  192. package/package.json +14 -5
  193. package/pi/gsd.cjs +57 -12
  194. package/scripts/build-hooks.js +9 -0
  195. package/scripts/changeset/lint.cjs +9 -2
  196. package/scripts/changeset/serialize.cjs +5 -1
  197. package/scripts/gen-capability-matrix.cjs +1 -1
  198. package/scripts/gen-context-index.cjs +448 -0
  199. package/scripts/gen-inventory-manifest.cjs +101 -1
  200. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  201. package/scripts/gen-section-manifest.cjs +638 -0
  202. package/scripts/generate-package-identity.cjs +4 -2
  203. package/scripts/lint-allow-test-rule-refs.allowlist.json +17 -31
  204. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  205. package/scripts/lint-docs-command-form.cjs +195 -0
  206. package/scripts/lint-docs-required.cjs +9 -1
  207. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  208. package/scripts/lint-example-parser-parity.cjs +395 -0
  209. package/scripts/lint-test-file-count.allowlist.json +27 -1
  210. package/scripts/mutation-matrix.cjs +13 -0
  211. package/scripts/prompt-injection-scan.sh +27 -6
  212. package/scripts/run-tests.cjs +3 -2
  213. package/skills/gsd-autonomous/SKILL.md +1 -1
  214. package/skills/gsd-execute-phase/SKILL.md +1 -1
  215. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  216. package/skills/gsd-new-milestone/SKILL.md +1 -1
  217. package/skills/gsd-plan-phase/SKILL.md +2 -2
  218. package/vscode/package.json +1 -1
  219. package/scripts/gen-emitted-baseline.cjs +0 -145
@@ -31,11 +31,14 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
31
31
  // `require('../../../package.json')`. That require ran at module load on every
32
32
  // gsd-tools invocation (this module sits in the gsd-tools loader chain) and
33
33
  // threw `Cannot find module '../../../package.json'` on runtimes whose root has
34
- // no package.json — notably Codex, where the installer omits the synthetic root
35
- // package.json — taking the entire CLI down before it did anything. And even
36
- // where it resolved (Claude's synthetic `{"type":"commonjs"}`), there is no
37
- // `version` field, so the single consumer below already emitted
38
- // `version: undefined`. Resolve lazily and defensively instead:
34
+ // no package.json — originally just Codex, where the installer never wrote the
35
+ // synthetic root package.json; since #2544 that is true of EVERY runtime, as
36
+ // GSD's markers moved into `hooks/` and the native plugin dir and the config
37
+ // root is no longer written at all — taking the entire CLI down before it did
38
+ // anything. And even where it used to resolve (the synthetic
39
+ // `{"type":"commonjs"}`), there is no `version` field, so the single consumer
40
+ // below already emitted `version: undefined`. Resolve lazily and defensively
41
+ // instead:
39
42
  // 1. Installed trees carry <root>/gsd-core/VERSION (written by the installer);
40
43
  // this module lives at <root>/gsd-core/bin/lib, so VERSION is two dirs up.
41
44
  // 2. The source / npm-package tree has no gsd-core/VERSION but carries a real
@@ -365,7 +368,14 @@ function skillFrontmatterName(skillDirName) {
365
368
  return skillDirName;
366
369
  }
367
370
  function normalizeClaudeSkillEffort(effort) {
368
- return effort === 'xhigh' ? 'max' : effort;
371
+ // #3039: `max` is rejected by Anthropic models when extended thinking is
372
+ // disabled (400: output_config.effort 'max' is not supported when thinking
373
+ // is disabled). The frontmatter is static at install time and the installer
374
+ // cannot know whether thinking will be on or off at invocation. `high` is the
375
+ // maximum value that works in both states on all supported models.
376
+ if (effort === 'xhigh' || effort === 'max')
377
+ return 'high';
378
+ return effort;
369
379
  }
370
380
  /**
371
381
  * Qwen Code skills accept an optional numeric `priority` frontmatter field.
@@ -848,8 +858,9 @@ function convertClaudeToCursorMarkdown(content) {
848
858
  // Remove Claude Code-specific bug workarounds before brand replacement
849
859
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
850
860
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
851
- // Replace "Claude Code" brand references with "Cursor"
852
- converted = converted.replace(/\bClaude Code\b/g, 'Cursor');
861
+ // Replace "Claude Code" brand references with "Cursor" — #2284(b): skips
862
+ // <runtime_compatibility> comparison-table content (protected region).
863
+ converted = applyClaudeCodeBrandSwap(converted, 'Cursor');
853
864
  return converted;
854
865
  }
855
866
  function getCursorSkillAdapterHeader(skillName) {
@@ -890,39 +901,41 @@ function convertClaudeCommandToCursorSkill(content, skillName) {
890
901
  description = toSingleLine(description);
891
902
  const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
892
903
  const adapter = getCursorSkillAdapterHeader(skillName);
893
- // #2341: mark user-invocable:false so the skill is NOT shown in Cursor's '/'
894
- // menu (it defaults to true). Cursor also writes a commands/ surface (#785),
895
- // and surfacing both duplicated every /gsd-* entry. This mirrors the #789
896
- // CodeBuddy de-dup: the commands/ surface is the sole '/' entry point; skills
897
- // stay model-invocable background knowledge. (user-invocable:false hides from
898
- // '/' while keeping model invocation — distinct from disable-model-invocation.)
899
- return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\nuser-invocable: false\n---\n\n${adapter}\n\n${body.trimStart()}`;
900
- }
901
- /**
902
- * Convert a Claude Code command to a Cursor 1.6 slash command (#785).
903
- *
904
- * Cursor slash commands live in `.cursor/commands/<name>.md` and are
905
- * plain markdown — no YAML frontmatter, no adapter header. The filename
906
- * becomes the command name (e.g. `gsd-help.md` → `/gsd-help`).
907
- *
908
- * Applies the same `convertClaudeToCursorMarkdown` transforms as the skill
909
- * converter (tool renames, brand substitution, slash-command normalisation),
910
- * then strips the YAML frontmatter block so only the prose body remains.
911
- *
912
- * @param {string} content raw Claude Code command markdown (may have frontmatter)
913
- * @param {string} _commandName the target command name (unused; present for
914
- * API symmetry with other converters so the runtime-artifact-layout stage
915
- * function can call it uniformly)
916
- * @returns {string} plain markdown body, no frontmatter
917
- */
918
- function convertClaudeCommandToCursorCommand(content, _commandName) {
919
- const converted = convertClaudeToCursorMarkdown(content);
920
- const { body } = extractFrontmatterAndBody(converted);
921
- return body.trimStart();
904
+ // Cursor skills are both slash-invocable and model-invocable. Do not emit the
905
+ // unsupported `user-invocable` field: it is ignored by Cursor and previously
906
+ // hid the real cause of duplicate entries, the parallel commands/ surface
907
+ // retired in #2644.
908
+ return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
922
909
  }
923
910
  // --- Windsurf converters ---
924
911
  // Windsurf uses a tool set similar to Cursor.
925
912
  // Config lives in .windsurf/ (local) and ~/.codeium/windsurf/ (global).
913
+ // #2931: ported from bin/install.js's local Windsurf converter copy, which had
914
+ // picked up the #2284(b) protected-region fix that this exported source never
915
+ // received. Binding bin/install.js's Windsurf family to these exports (see
916
+ // tests/install-runtime-artifacts.test.cjs reference-identity assertions)
917
+ // without this would have silently regressed live installs: `Claude Code`
918
+ // mentions inside a `<runtime_compatibility>` comparison table would start
919
+ // getting brand-swapped again. Split `content` on the protected-block regex,
920
+ // brand-swap only the gap text between (and around) matches, then rejoin
921
+ // gap+block alternately — no placeholder/sentinel token involved.
922
+ const RUNTIME_COMPATIBILITY_BLOCK_RE = /<runtime_compatibility>[\s\S]*?<\/runtime_compatibility>/g;
923
+ function applyClaudeCodeBrandSwap(content, brandName) {
924
+ if (!brandName)
925
+ return content;
926
+ let result = '';
927
+ let lastIndex = 0;
928
+ RUNTIME_COMPATIBILITY_BLOCK_RE.lastIndex = 0; // reset shared global-regex state before each use
929
+ let m;
930
+ while ((m = RUNTIME_COMPATIBILITY_BLOCK_RE.exec(content))) {
931
+ const gap = content.slice(lastIndex, m.index);
932
+ result += gap.replace(/\bClaude Code\b/g, brandName);
933
+ result += m[0]; // protected block, verbatim — never brand-swapped
934
+ lastIndex = m.index + m[0].length;
935
+ }
936
+ result += content.slice(lastIndex).replace(/\bClaude Code\b/g, brandName);
937
+ return result;
938
+ }
926
939
  function convertSlashCommandsToWindsurfSkillMentions(content) {
927
940
  // Keep leading "/" for slash commands; only normalize gsd: -> gsd-.
928
941
  return content.replace(/gsd:/gi, 'gsd-');
@@ -953,8 +966,9 @@ function convertClaudeToWindsurfMarkdown(content) {
953
966
  // Remove Claude Code-specific bug workarounds before brand replacement
954
967
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
955
968
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
956
- // Replace "Claude Code" brand references with "Windsurf"
957
- converted = converted.replace(/\bClaude Code\b/g, 'Windsurf');
969
+ // Replace "Claude Code" brand references with "Windsurf" — #2284(b): skips
970
+ // <runtime_compatibility> comparison-table content (protected region).
971
+ converted = applyClaudeCodeBrandSwap(converted, 'Windsurf');
958
972
  return converted;
959
973
  }
960
974
  function getWindsurfSkillAdapterHeader(skillName) {
@@ -993,10 +1007,46 @@ function convertClaudeCommandToWindsurfSkill(content, skillName) {
993
1007
  }
994
1008
  }
995
1009
  description = toSingleLine(description);
996
- const shortDescription = description.length > 180 ? `${description.slice(0, 177)}...` : description;
1010
+ // #2931: code-point-safe truncation (see truncateWindsurfWorkflowDescription
1011
+ // below) — a raw UTF-16 `slice(0, 177)` can bisect a surrogate pair and
1012
+ // emit a lone surrogate on re-encode. Same exact bounds as before (>180
1013
+ // chars -> first 177 code points + '...'), just harmonized with the
1014
+ // sibling Windsurf workflow converter's helper instead of duplicating the
1015
+ // surrogate-splitting idiom here.
1016
+ const shortDescription = truncateWindsurfWorkflowDescription(description);
997
1017
  const adapter = getWindsurfSkillAdapterHeader(skillName);
998
1018
  return `---\nname: ${yamlIdentifier(skillName)}\ndescription: ${yamlQuote(shortDescription)}\n---\n\n${adapter}\n\n${body.trimStart()}`;
999
1019
  }
1020
+ // #2931: cap on the Windsurf workflow's only unbounded input (the frontmatter
1021
+ // `description`). Shared (not just mirrored) by convertClaudeCommandToWindsurfSkill
1022
+ // above — both converters call truncateWindsurfWorkflowDescription below so
1023
+ // there is exactly one code-point-safe truncation idiom, not two.
1024
+ const WINDSURF_WORKFLOW_DESCRIPTION_MAX = 180;
1025
+ function truncateWindsurfWorkflowDescription(description) {
1026
+ // Multi-byte safe: slice by Unicode code points (`Array.from`), never by
1027
+ // raw UTF-16 index — an index-based slice can bisect a surrogate pair and
1028
+ // emit a lone surrogate / U+FFFD on re-encode. See #2931.
1029
+ const codePoints = Array.from(description);
1030
+ if (codePoints.length <= WINDSURF_WORKFLOW_DESCRIPTION_MAX)
1031
+ return description;
1032
+ return `${codePoints.slice(0, WINDSURF_WORKFLOW_DESCRIPTION_MAX - 3).join('')}...`;
1033
+ }
1034
+ // #2931: SEPARATE size control from the #1615 security regex below — do not
1035
+ // fold the two together or make either conditional on the other. The #1615
1036
+ // regex constrains commandName's CHARACTER CLASS but not its LENGTH, and
1037
+ // commandName is interpolated into the emitted template three times (the
1038
+ // `# <commandName>` heading, the `@.../<stem>.md` @-reference target, and the
1039
+ // trailing "after /<commandName>" mention) — so an unbounded commandName
1040
+ // reopens the byte-cap hole the removed 12000-byte throw used to close
1041
+ // (verified: commandName length 246 -> 900 bytes, 5000 -> 15,162 bytes,
1042
+ // 20000 -> 60,162 bytes — all silently over the old 12000 cap). THROW rather
1043
+ // than truncate: a truncated commandName would silently point the workflow's
1044
+ // `@~/.claude/gsd-core/commands/gsd/<stem>.md` reference at a file that does
1045
+ // not exist (see DEFECT.WORKFLOW-DELEGATION-TARGET-NOT-INSTALLED) — a name
1046
+ // too long to represent is a genuine error, not something to degrade. 128 is
1047
+ // deliberately generous: the longest real shipped command name is
1048
+ // `gsd-plan-review-convergence` at 27 characters.
1049
+ const WINDSURF_COMMAND_NAME_MAX = 128;
1000
1050
  function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
1001
1051
  // #1615 security: commandName flows unsanitized into a markdown body that
1002
1052
  // Windsurf loads as an LLM-readable workflow. Validate at entry to prevent
@@ -1005,21 +1055,43 @@ function convertClaudeCommandToWindsurfWorkflow(content, commandName) {
1005
1055
  // Pattern: optional gsd- prefix + lowercase alphanumeric + dashes; rejects
1006
1056
  // everything else. See DEFECT.PROMPT-INJECTION-SCAN-COLLISION and the
1007
1057
  // PR #1622 security review.
1058
+ // #2931: this is a SECURITY control, not a size control — do not weaken,
1059
+ // reorder, or make it conditional on the description-truncation logic
1060
+ // added below. Keep the two concerns independent even though both happen
1061
+ // to run in this function.
1008
1062
  if (typeof commandName !== 'string' || !/^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(commandName)) {
1009
1063
  const preview = typeof commandName === 'string' ? JSON.stringify(commandName.slice(0, 60)) : String(commandName);
1010
1064
  throw new Error(`convertClaudeCommandToWindsurfWorkflow: rejected commandName ${preview}; ` +
1011
1065
  'must match /^(?:gsd-)?[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/ (no slashes, backslashes, spaces, dots, trailing dash, or control chars — prevents prompt injection and path-component injection into the workflow body)');
1012
1066
  }
1067
+ // #2931: SEPARATE size control — see WINDSURF_COMMAND_NAME_MAX above for
1068
+ // why this exists and why it throws instead of truncating. Kept as an
1069
+ // independent check from the #1615 regex above (not folded into it, not
1070
+ // conditional on it).
1071
+ if (commandName.length > WINDSURF_COMMAND_NAME_MAX) {
1072
+ const preview = JSON.stringify(commandName.slice(0, 60));
1073
+ throw new Error(`convertClaudeCommandToWindsurfWorkflow: commandName too long (${commandName.length} chars, ` +
1074
+ `preview ${preview}...); max ${WINDSURF_COMMAND_NAME_MAX} chars (see WINDSURF_COMMAND_NAME_MAX)`);
1075
+ }
1013
1076
  const converted = convertClaudeToWindsurfMarkdown(content);
1014
1077
  const { frontmatter } = extractFrontmatterAndBody(converted);
1015
- const description = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
1078
+ const rawDescription = frontmatter ? extractFrontmatterField(frontmatter, 'description') : '';
1079
+ // #2931: a whitespace-only description is truthy (`description || fallback`
1080
+ // would keep it) but toSingleLine() collapses it to ''. Treat it as absent
1081
+ // so the fallback is used instead of emitting a blank line.
1082
+ const singleLineDescription = rawDescription ? toSingleLine(rawDescription) : '';
1083
+ const effectiveDescription = truncateWindsurfWorkflowDescription(singleLineDescription || `Run ${commandName}.`);
1016
1084
  const stem = commandName.startsWith('gsd-') ? commandName.slice(4) : commandName;
1017
- const workflow = `# ${commandName}\n\n${toSingleLine(description || `Run ${commandName}.`)}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
1018
- const byteLength = Buffer.byteLength(workflow, 'utf8');
1019
- if (byteLength > 12000) {
1020
- throw new Error(`Windsurf workflow ${commandName} exceeds 12000 bytes (${byteLength}); extract references before installing`);
1021
- }
1022
- return workflow;
1085
+ // #2931: total emission size is bounded by (fixed template text) +
1086
+ // (3 x WINDSURF_COMMAND_NAME_MAX, one per commandName/stem interpolation
1087
+ // above) + (WINDSURF_WORKFLOW_DESCRIPTION_MAX Unicode code points, up to 4
1088
+ // UTF-8 bytes each). Both inputs are validated/truncated above — commandName
1089
+ // is length-capped-and-thrown by WINDSURF_COMMAND_NAME_MAX, description is
1090
+ // truncated by truncateWindsurfWorkflowDescription — so this bound holds by
1091
+ // construction, not by measurement. The 12000-byte figure itself lives in
1092
+ // exactly one place — the cap table in tests/helpers/emitted-caps.cjs —
1093
+ // this comment only justifies why the actual emitted size stays under it.
1094
+ return `# ${commandName}\n\n${effectiveDescription}\n\nRead and execute the GSD command at @~/.claude/gsd-core/commands/gsd/${stem}.md end-to-end. Treat the user's message after /${commandName} as the command arguments.`;
1023
1095
  }
1024
1096
  // --- Augment converters ---
1025
1097
  // Augment uses a tool set similar to Cursor/Windsurf.
@@ -1047,8 +1119,9 @@ function convertClaudeToAugmentMarkdown(content) {
1047
1119
  // Remove Claude Code-specific bug workarounds before brand replacement
1048
1120
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1049
1121
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1050
- // Replace "Claude Code" brand references with "Augment"
1051
- converted = converted.replace(/\bClaude Code\b/g, 'Augment');
1122
+ // Replace "Claude Code" brand references with "Augment" — #2284(b): skips
1123
+ // <runtime_compatibility> comparison-table content (protected region).
1124
+ converted = applyClaudeCodeBrandSwap(converted, 'Augment');
1052
1125
  return converted;
1053
1126
  }
1054
1127
  // #2097 (ADR-1239): command-body converters selected by descriptor
@@ -1111,10 +1184,55 @@ function convertClaudeToTraeMarkdown(content) {
1111
1184
  // Replace general-purpose subagent type with Trae's equivalent "general_purpose_task"
1112
1185
  converted = converted.replace(/subagent_type="general-purpose"/g, 'subagent_type="general_purpose_task"');
1113
1186
  converted = converted.replace(/\$ARGUMENTS\b/g, '{{GSD_ARGS}}');
1114
- converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/`');
1115
- converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/');
1116
- converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/`');
1117
- converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/');
1187
+ // #2658: full-path forms (with a leading dot-claude-slash prefix) MUST be
1188
+ // replaced before the bare Claude-instruction-file pattern and before the
1189
+ // generic dot-claude-slash rewrite below — otherwise the bare pattern
1190
+ // consumes only the instruction-filename tail, leaving that prefix stale
1191
+ // in place, and the generic rewrite then mutates the stale leftover too,
1192
+ // producing a doubled trae-prefix segment ahead of the rules path instead
1193
+ // of a single clean one. (Deliberately never spelling the instruction
1194
+ // filename as one contiguous "CLAUDE" + dot + "md" token, and never
1195
+ // spelling either malformed shape out as a literal contiguous string, in
1196
+ // ANY comment in this function: this file ships verbatim into local
1197
+ // `--trae` installs, where it is itself run through this same class of
1198
+ // find/replace — a literal instruction-filename token sitting in a
1199
+ // comment gets "fixed" right along with real code, and the emitted-content
1200
+ // regression test added alongside this fix asserts neither malformed
1201
+ // shape appears anywhere in the installed tree, comments included; this
1202
+ // bit the fix itself twice during development.) All forms converge on the
1203
+ // same concrete file (never a bare directory) so this stays in parity
1204
+ // with the `trae.js` RUNTIME_CONTENT_DISPATCH entry.
1205
+ converted = converted.replace(/`\.\/\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
1206
+ converted = converted.replace(/\.\/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
1207
+ converted = converted.replace(/`\.claude\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
1208
+ converted = converted.replace(/\.claude\/CLAUDE\.md/g, '.trae/rules/rules.md');
1209
+ // #2658 (found via the end-to-end install regression test, not the static
1210
+ // trace above): `copyWithPathReplacement` runs a GENERIC dot-claude-slash
1211
+ // -> runtime-config-dir rewrite on every .md file before calling this
1212
+ // converter — for `~/.claude/`, `$HOME/.claude/`, AND `./.claude/` alike —
1213
+ // substituting a runtime-appropriate `pathPrefix` this function is never
1214
+ // given and cannot itself compute (it differs per install invocation: a
1215
+ // relative `./.trae/` for a project-local install, an arbitrary absolute
1216
+ // path for a local install rooted elsewhere, `~/.trae/` for a global one).
1217
+ // So for source using any of those prefixed forms, the patterns above
1218
+ // never fire here — this converter only ever sees the ALREADY-rewritten
1219
+ // "<runtime-config-dir>/" + instruction-filename shape, with whatever
1220
+ // prefix the install actually used. The generic pattern below preserves
1221
+ // that prefix verbatim (via the capture group) and only fixes the
1222
+ // filename suffix, rather than assuming a fixed `./.trae/` shape — a
1223
+ // narrower fixed-prefix version of this pattern shipped first and still
1224
+ // left the doubled-prefix defect live for the `$HOME/.claude/` and
1225
+ // `~/.claude/` forms specifically (found the same way, one regression-test
1226
+ // run later). Scoped to a `.trae/` tail so it cannot also swallow the
1227
+ // unprefixed `./CLAUDE.md` form the very next pattern handles differently
1228
+ // (discarding the prefix entirely, not preserving it). Must run before
1229
+ // the bare pattern for the same consume-the-full-match-first reason.
1230
+ converted = converted.replace(/`([^\s`]*\.trae\/)CLAUDE\.md`/g, '`$1rules/rules.md`');
1231
+ converted = converted.replace(/([^\s`]*\.trae\/)CLAUDE\.md/g, '$1rules/rules.md');
1232
+ converted = converted.replace(/`\.\/CLAUDE\.md`/g, '`.trae/rules/rules.md`');
1233
+ converted = converted.replace(/\.\/CLAUDE\.md/g, '.trae/rules/rules.md');
1234
+ converted = converted.replace(/`CLAUDE\.md`/g, '`.trae/rules/rules.md`');
1235
+ converted = converted.replace(/\bCLAUDE\.md\b/g, '.trae/rules/rules.md');
1118
1236
  converted = converted.replace(/\.claude\/skills\//g, '.trae/skills/');
1119
1237
  converted = converted.replace(/\.\/\.claude\//g, './.trae/');
1120
1238
  converted = converted.replace(/\.claude\//g, '.trae/');
@@ -1126,7 +1244,8 @@ function convertClaudeToTraeMarkdown(content) {
1126
1244
  converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR');
1127
1245
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1128
1246
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1129
- converted = converted.replace(/\bClaude Code\b/g, 'Trae');
1247
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
1248
+ converted = applyClaudeCodeBrandSwap(converted, 'Trae');
1130
1249
  return converted;
1131
1250
  }
1132
1251
  // DEFECT.GENERATIVE-FIX: this body is mirrored in bin/install.js's
@@ -1180,7 +1299,8 @@ function convertClaudeToCodebuddyMarkdown(content) {
1180
1299
  converted = converted.replace(/\.claude\//g, '.codebuddy/');
1181
1300
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1182
1301
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1183
- converted = converted.replace(/\bClaude Code\b/g, 'CodeBuddy');
1302
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
1303
+ converted = applyClaudeCodeBrandSwap(converted, 'CodeBuddy');
1184
1304
  return converted;
1185
1305
  }
1186
1306
  function convertClaudeCommandToCodebuddySkill(content, skillName) {
@@ -1260,7 +1380,8 @@ function convertClaudeToCliineMarkdown(content) {
1260
1380
  converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'CLINE_CONFIG_DIR');
1261
1381
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
1262
1382
  converted = converted.replace(/- \*\*classifyHandoffIfNeeded false failure:\*\*[^\n]*\n/g, '');
1263
- converted = converted.replace(/\bClaude Code\b/g, 'Cline');
1383
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
1384
+ converted = applyClaudeCodeBrandSwap(converted, 'Cline');
1264
1385
  return converted;
1265
1386
  }
1266
1387
  /**
@@ -1413,6 +1534,8 @@ Typed mapping (agent_type-capable schema only):
1413
1534
  inherited, or unsupported values; do not invent one-off effort literals in
1414
1535
  workflow prose.
1415
1536
  - \`fork_context: false\` by default — GSD agents load their own context via \`<files_to_read>\` blocks
1537
+ - \`task_name\` — required by the collaboration schema; provide a descriptive name for each spawned task
1538
+ - \`fork_turns\` — optional parameter controlling turn-forking depth; coexists with \`fork_context\` (not a replacement)
1416
1539
  - \`Task(isolation="worktree")\` / \`Agent(isolation="worktree")\` → no direct \`spawn_agent\` mapping,
1417
1540
  but Codex declares \`dispatch.isolation: orchestrator-worktree\` (#2584). Codex
1418
1541
  \`spawn_agent\` still does not create or bind a git worktree; instead GSD itself
@@ -1449,11 +1572,13 @@ Spawn restriction:
1449
1572
  defaulting to inline execution.
1450
1573
 
1451
1574
  Parallel fan-out:
1452
- - Spawn multiple agents → collect agent IDs → \`wait(ids)\` for all to complete
1575
+ - Spawn multiple agents → collect agent IDs → \`collaboration.wait_agent(timeout_ms=...)\` for each to complete
1576
+ - Do NOT use \`functions.wait(cell_id=...)\` — that is an unrelated exec-cell tool, not the collaboration wait
1453
1577
 
1454
1578
  Result parsing:
1455
1579
  - Look for structured markers in agent output: \`CHECKPOINT\`, \`PLAN COMPLETE\`, \`SUMMARY\`, etc.
1456
- - \`close_agent(id)\` after collecting results from each agent
1580
+ - \`close_agent(id)\` after collecting results — but only if \`close_agent\` is visible in the current
1581
+ tool schema (check via \`tool_search\` first, same schema-detection gate as \`spawn_agent\` above)
1457
1582
  </codex_skill_adapter>`;
1458
1583
  }
1459
1584
  function convertClaudeCommandToCodexSkill(content, skillName) {
@@ -2038,8 +2163,9 @@ function convertClaudeAgentToQwenAgent(content) {
2038
2163
  let converted = content;
2039
2164
  if (_b['CLAUDE.md'])
2040
2165
  converted = converted.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2166
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
2041
2167
  if (_b['Claude Code'])
2042
- converted = converted.replace(/\bClaude Code\b/g, _b['Claude Code']);
2168
+ converted = applyClaudeCodeBrandSwap(converted, _b['Claude Code']);
2043
2169
  if (_b['.claude/'])
2044
2170
  converted = converted.replace(/\.claude\//g, _b['.claude/']);
2045
2171
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
@@ -2342,7 +2468,8 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2342
2468
  const _b = _hostBehaviors(runtime).brandingRewrites;
2343
2469
  if (_b) {
2344
2470
  content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2345
- content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
2471
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
2472
+ content = applyClaudeCodeBrandSwap(content, _b['Claude Code']);
2346
2473
  }
2347
2474
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2348
2475
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2366,7 +2493,8 @@ function _applyRuntimeRewrites(content, runtime, pathPrefix, isGlobal = false, a
2366
2493
  const _b = _hostBehaviors(runtime).brandingRewrites;
2367
2494
  if (_b) {
2368
2495
  content = content.replace(/CLAUDE\.md/g, _b['CLAUDE.md']);
2369
- content = content.replace(/\bClaude Code\b/g, _b['Claude Code']);
2496
+ // #2284(b): skips <runtime_compatibility> comparison-table content (protected region).
2497
+ content = applyClaudeCodeBrandSwap(content, _b['Claude Code']);
2370
2498
  }
2371
2499
  content = content.replace(/~\/\.claude\//g, pathPrefix);
2372
2500
  content = content.replace(/\$HOME\/\.claude\//g, pathPrefix);
@@ -2633,10 +2761,15 @@ module.exports = {
2633
2761
  buildKimiAgentArtifacts,
2634
2762
  convertClaudeToCursorMarkdown,
2635
2763
  convertClaudeCommandToCursorSkill,
2636
- convertClaudeCommandToCursorCommand,
2637
2764
  convertClaudeToWindsurfMarkdown,
2638
2765
  convertClaudeCommandToWindsurfSkill,
2639
2766
  convertClaudeCommandToWindsurfWorkflow,
2767
+ // #2931: single-sourced brand-swap helper (was duplicated verbatim in
2768
+ // bin/install.js — the exact drift class this PR exists to reduce). Used
2769
+ // internally by convertClaudeToWindsurfMarkdown/convertClaudeToAugmentMarkdown
2770
+ // above and bound from here by the remaining bin/install.js converters
2771
+ // (Cursor/Trae/CodeBuddy/Cline) that still brand-swap inline.
2772
+ applyClaudeCodeBrandSwap,
2640
2773
  convertClaudeToAugmentMarkdown,
2641
2774
  convertClaudeCommandToAugmentSkill,
2642
2775
  convertClaudeToTraeMarkdown,
@@ -19,7 +19,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
19
19
  const node_os_1 = __importDefault(require("node:os"));
20
20
  // eslint-disable-next-line @typescript-eslint/no-require-imports
21
21
  const installProfiles = require("./install-profiles.cjs");
22
- const { stageSkillsForProfile, stageAgentsForProfile, stageAgentsForRuntimeWithConverter, stageSkillsForRuntimeAsSkills, stageCommandsForRuntimeFlat, } = installProfiles;
22
+ const { stageSkillsForProfile, stageAgentsForRuntimeWithConverter, stageSkillsForRuntimeAsSkills, stageCommandsForRuntimeFlat, } = installProfiles;
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;
@@ -117,7 +117,17 @@ function agentsKind(destSubpath, prefix, configDir) {
117
117
  kind: 'agents',
118
118
  destSubpath,
119
119
  prefix,
120
- stage: (resolved) => stageAgentsForProfile(findAgentsSourceRoot(configDir), resolved),
120
+ // #2995: a `converter: null` agents entry (claude local, zcode) previously
121
+ // staged via stageAgentsForProfile — a RAW byte copy that never reads content
122
+ // into JS, so gsd:section markers shipped verbatim. Route through the
123
+ // composing stager with an identity converter instead: same output as the raw
124
+ // copy for an unmarked agent, markers stripped for a marked one. Routing both
125
+ // agent kinds through the stager collapses what were five independent agent
126
+ // read points down to three compose call sites: this stager, bin/install.js's
127
+ // inline agent loop, and installCodexConfig's per-agent .toml writer. The
128
+ // exhaustive per-runtime sweep in tests/agent-fragments-emission.install.test.cjs
129
+ // is what keeps a fourth from appearing uncomposed.
130
+ stage: (resolved) => stageAgentsForRuntimeWithConverter(findAgentsSourceRoot(configDir), resolved, (content) => content),
121
131
  };
122
132
  }
123
133
  /**
@@ -185,7 +195,9 @@ function kimiAgentsKind(destSubpath, prefix, configDir) {
185
195
  prefix,
186
196
  stage: (resolved) => {
187
197
  const buildKimiAgentArtifacts = conversionExports['buildKimiAgentArtifacts'];
188
- const stagedAgents = stageAgentsForProfile(findAgentsSourceRoot(configDir), resolved);
198
+ // #2995: compose at staging (identity converter) so the readFileSync below
199
+ // sees marker-free content — same single composing stager as agentsKind.
200
+ const stagedAgents = stageAgentsForRuntimeWithConverter(findAgentsSourceRoot(configDir), resolved, (content) => content);
189
201
  const subagents = [];
190
202
  if (node_fs_1.default.existsSync(stagedAgents)) {
191
203
  for (const entry of node_fs_1.default.readdirSync(stagedAgents, { withFileTypes: true })) {