@opengsd/gsd-core 1.20.0 → 1.21.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 (154) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +33 -2
  4. package/agents/gsd-executor.md +1 -1
  5. package/agents/gsd-plan-checker.md +1 -1
  6. package/agents/gsd-planner.md +3 -3
  7. package/agents/gsd-verifier.md +2 -2
  8. package/bin/install.js +265 -161
  9. package/commands/gsd/autonomous.md +1 -1
  10. package/commands/gsd/new-milestone.md +2 -2
  11. package/commands/gsd/new-project.md +2 -2
  12. package/gsd-core/bin/gsd-tools.cjs +554 -823
  13. package/gsd-core/bin/lib/capability-activation.cjs +8 -2
  14. package/gsd-core/bin/lib/capability-command-router.cjs +6 -2
  15. package/gsd-core/bin/lib/capability-ledger.cjs +1 -1
  16. package/gsd-core/bin/lib/capability-lock.cjs +19 -27
  17. package/gsd-core/bin/lib/capability-registry.cjs +74 -71
  18. package/gsd-core/bin/lib/capability-validator.cjs +1 -0
  19. package/gsd-core/bin/lib/capability-writer.cjs +7 -4
  20. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +142 -62
  21. package/gsd-core/bin/lib/claude-orchestration.cjs +214 -122
  22. package/gsd-core/bin/lib/cli-surface.cjs +545 -0
  23. package/gsd-core/bin/lib/command-arg-projection.cjs +356 -103
  24. package/gsd-core/bin/lib/commands.cjs +99 -2
  25. package/gsd-core/bin/lib/config-file-read.cjs +85 -0
  26. package/gsd-core/bin/lib/config-loader.cjs +23 -15
  27. package/gsd-core/bin/lib/config.cjs +139 -101
  28. package/gsd-core/bin/lib/configuration.cjs +41 -2
  29. package/gsd-core/bin/lib/criterion-baseline.cjs +346 -0
  30. package/gsd-core/bin/lib/criterion-replay.cjs +380 -0
  31. package/gsd-core/bin/lib/estimate-cli.cjs +5 -1
  32. package/gsd-core/bin/lib/gap-checker.cjs +12 -1
  33. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +593 -27
  34. package/gsd-core/bin/lib/gate-phase-context.cjs +16 -4
  35. package/gsd-core/bin/lib/graphify.cjs +19 -2
  36. package/gsd-core/bin/lib/gsd-tool-dir.cjs +134 -0
  37. package/gsd-core/bin/lib/health-diagnostic.cjs +5 -1
  38. package/gsd-core/bin/lib/init.cjs +20 -18
  39. package/gsd-core/bin/lib/install-effort-resolver.cjs +24 -4
  40. package/gsd-core/bin/lib/install-engine.cjs +20 -28
  41. package/gsd-core/bin/lib/install-fs-adapter.cjs +5 -1
  42. package/gsd-core/bin/lib/install-manifest-record.cjs +86 -0
  43. package/gsd-core/bin/lib/install-model-override-resolver.cjs +33 -3
  44. package/gsd-core/bin/lib/install-profiles.cjs +4 -2
  45. package/gsd-core/bin/lib/io.cjs +6 -0
  46. package/gsd-core/bin/lib/markdown-sectionizer.cjs +62 -31
  47. package/gsd-core/bin/lib/markdown-table.cjs +39 -1
  48. package/gsd-core/bin/lib/milestone.cjs +8 -6
  49. package/gsd-core/bin/lib/model-resolver.cjs +8 -4
  50. package/gsd-core/bin/lib/phase-entry-grammar.cjs +14 -0
  51. package/gsd-core/bin/lib/phase-id.cjs +26 -9
  52. package/gsd-core/bin/lib/phase-locator.cjs +8 -3
  53. package/gsd-core/bin/lib/phase.cjs +170 -45
  54. package/gsd-core/bin/lib/plan-dependency-graph.cjs +46 -23
  55. package/gsd-core/bin/lib/planning-snapshot.cjs +10 -6
  56. package/gsd-core/bin/lib/planning-workspace.cjs +20 -10
  57. package/gsd-core/bin/lib/profile-output.cjs +9 -6
  58. package/gsd-core/bin/lib/project-context.cjs +439 -0
  59. package/gsd-core/bin/lib/project-root.cjs +9 -230
  60. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +1 -1
  61. package/gsd-core/bin/lib/report-parser.cjs +16 -0
  62. package/gsd-core/bin/lib/roadmap-parser.cjs +357 -50
  63. package/gsd-core/bin/lib/roadmap-upgrade.cjs +24 -5
  64. package/gsd-core/bin/lib/roadmap.cjs +2 -2
  65. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +317 -134
  66. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +81 -3
  67. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +6 -0
  68. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +279 -8
  69. package/gsd-core/bin/lib/runtime-slash.cjs +7 -2
  70. package/gsd-core/bin/lib/shell-command-projection.cjs +1010 -10
  71. package/gsd-core/bin/lib/stale-bake-guard.cjs +4 -1
  72. package/gsd-core/bin/lib/state-transition.cjs +103 -46
  73. package/gsd-core/bin/lib/state.cjs +448 -63
  74. package/gsd-core/bin/lib/surface.cjs +4 -7
  75. package/gsd-core/bin/lib/tdd-red-evidence.cjs +10 -3
  76. package/gsd-core/bin/lib/verify-command-router.cjs +6 -1
  77. package/gsd-core/bin/lib/verify.cjs +49 -6
  78. package/gsd-core/bin/lib/workstream-inventory.cjs +4 -4
  79. package/gsd-core/bin/lib/worktree-base-ref.cjs +32 -2
  80. package/gsd-core/bin/lib/worktree-safety.cjs +8 -111
  81. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  82. package/gsd-core/references/autonomous-smart-discuss.md +99 -18
  83. package/gsd-core/references/checkpoints.md +1 -1
  84. package/gsd-core/references/planner-guidance.md +11 -0
  85. package/gsd-core/references/planner-human-verify-mode.md +1 -1
  86. package/gsd-core/references/planner-reversibility.md +2 -0
  87. package/gsd-core/references/planner-verify-command-grounding.md +22 -6
  88. package/gsd-core/workflows/autonomous.md +4 -4
  89. package/gsd-core/workflows/code-review-fix.md +5 -7
  90. package/gsd-core/workflows/execute-phase.md +21 -7
  91. package/gsd-core/workflows/plan-phase/steps/plan-pre-dispatch.md +101 -0
  92. package/gsd-core/workflows/plan-phase.md +18 -19
  93. package/hooks/dist/gsd-agent-isolation-guard.js +4 -3
  94. package/hooks/dist/gsd-config-reload.js +2 -2
  95. package/hooks/dist/gsd-context-monitor.js +2 -1
  96. package/hooks/dist/gsd-cursor-subagent-start.js +9 -6
  97. package/hooks/dist/gsd-read-injection-scanner.js +2 -2
  98. package/hooks/dist/gsd-secret-read-guard.js +141 -6
  99. package/hooks/dist/gsd-statusline.js +3 -2
  100. package/hooks/dist/gsd-workflow-guard.js +2 -1
  101. package/hooks/dist/lib/config-read.js +49 -0
  102. package/hooks/dist/lib/git-cmd.js +3 -1
  103. package/hooks/dist/lib/git-head-tree.js +474 -0
  104. package/hooks/dist/lib/isolation-sentinel.js +168 -13
  105. package/hooks/dist/lib/secret-read-exemptions.js +161 -0
  106. package/hooks/gsd-agent-isolation-guard.js +4 -3
  107. package/hooks/gsd-config-reload.js +2 -2
  108. package/hooks/gsd-context-monitor.js +2 -1
  109. package/hooks/gsd-cursor-subagent-start.js +9 -6
  110. package/hooks/gsd-read-injection-scanner.js +2 -2
  111. package/hooks/gsd-secret-read-guard.js +141 -6
  112. package/hooks/gsd-statusline.js +3 -2
  113. package/hooks/gsd-workflow-guard.js +2 -1
  114. package/hooks/lib/config-read.js +49 -0
  115. package/hooks/lib/git-cmd.js +3 -1
  116. package/hooks/lib/git-head-tree.js +474 -0
  117. package/hooks/lib/isolation-sentinel.js +168 -13
  118. package/hooks/lib/secret-read-exemptions.js +161 -0
  119. package/package.json +7 -2
  120. package/pi/package-agents/gsd-executor.md +1 -1
  121. package/pi/package-agents/gsd-plan-checker.md +1 -1
  122. package/pi/package-agents/gsd-planner.md +3 -3
  123. package/pi/package-agents/gsd-verifier.md +2 -2
  124. package/pi/package-agents/manifest.json +4 -4
  125. package/pi/package-skills/gsd-autonomous/SKILL.md +1 -1
  126. package/pi/package-skills/gsd-new-milestone/SKILL.md +1 -1
  127. package/pi/package-skills/gsd-new-project/SKILL.md +1 -1
  128. package/scripts/changeset/cli.cjs +2 -2
  129. package/scripts/changeset/lint.cjs +28 -3
  130. package/scripts/changeset/serialize.cjs +12 -2
  131. package/scripts/ci-compact-v8-coverage.cjs +204 -0
  132. package/scripts/ci-fix-next-rerun.cjs +149 -0
  133. package/scripts/ci-next-health.cjs +52 -9
  134. package/scripts/ci-test-scope.cjs +77 -71
  135. package/scripts/command-contract-helpers.cjs +56 -7
  136. package/scripts/docs-guard-registry.cjs +3 -0
  137. package/scripts/gen-cli-surface-manifest.cjs +287 -0
  138. package/scripts/gen-platform-conformance-tier.cjs +258 -19
  139. package/scripts/lib/macos-conformance-tier.generated.cjs +59 -47
  140. package/scripts/lib/platform-conformance-tier.generated.cjs +23 -64
  141. package/scripts/lib/shellcheck-fetch.cjs +71 -4
  142. package/scripts/lint-removed-but-needed.cjs +562 -50
  143. package/scripts/merge-commit-pr.cjs +186 -0
  144. package/scripts/release-ancestry.cjs +86 -0
  145. package/scripts/release-notes/changelog-release-notes.cjs +345 -0
  146. package/scripts/release-notes/discord-release-summary.cjs +82 -1
  147. package/scripts/release-pipeline-pr.cjs +92 -0
  148. package/scripts/require-issue-link-policy.cjs +18 -9
  149. package/scripts/sync-next-version.cjs +11 -2
  150. package/scripts/timeout-evidence-from-ci.cjs +964 -0
  151. package/skills/gsd-autonomous/SKILL.md +1 -1
  152. package/skills/gsd-new-milestone/SKILL.md +1 -1
  153. package/skills/gsd-new-project/SKILL.md +1 -1
  154. package/vscode/package.json +1 -1
package/bin/install.js CHANGED
@@ -623,6 +623,8 @@ const {
623
623
  stageSkillsForRuntimeAsSkills,
624
624
  } = require(path.join(_gsdLibDir, 'install-profiles.cjs'));
625
625
  // ADR-857 phase 4c: load capability registry (optional; missing → falls back to undefined)
626
+ // #5759: bounded config.json reader shared with the runtime (non-blocking open, regular file, 1 MiB cap).
627
+ const { readConfigFileBounded } = require(path.join(_gsdLibDir, 'config-file-read.cjs'));
626
628
  let _capabilityRegistry;
627
629
  try {
628
630
  _capabilityRegistry = require(path.join(_gsdLibDir, 'capability-registry.cjs'));
@@ -789,7 +791,9 @@ const {
789
791
  applyInstallerMigrationPlan,
790
792
  discoverInstallerMigrations,
791
793
  MANIFEST_SCHEMA_VERSION,
794
+ planInstallerMigrations,
792
795
  readInstallManifest,
796
+ readInstallState,
793
797
  recordHostGeneration,
794
798
  runInstallerMigrations,
795
799
  } = require(path.join(_gsdLibDir, 'installer-migrations.cjs'));
@@ -810,6 +814,19 @@ const {
810
814
  } = require(path.join(_gsdLibDir, 'surface.cjs'));
811
815
  const {
812
816
  assertDestWithinConfigHome,
817
+ resolveRuntimeArtifactDestination,
818
+ resolveRuntimeArtifactInstallRoot,
819
+ plannedActionsMutateCodexHooksInputs,
820
+ shouldInstallCombinedFamily,
821
+ isLegacyFlatLocalInstall,
822
+ shouldInstallStandaloneAgents,
823
+ codexAgentRolesEnabled,
824
+ copyWithPathReplacementSymlinkRefusalMessage,
825
+ installRuntimeArtifactsSymlinkRefusalMessage,
826
+ installOpencodeFamilySkillsSymlinkRefusalMessage,
827
+ installAgentsKindStandaloneSymlinkRefusalMessage,
828
+ installCodexConfigSymlinkRefusalMessage,
829
+ installCodexAgentTomlSymlinkRefusalMessage,
813
830
  createRuntimeArtifactInstallPlan,
814
831
  createRuntimeArtifactUninstallPlan,
815
832
  } = require(path.join(_gsdLibDir, 'runtime-artifact-install-plan.cjs'));
@@ -879,6 +896,126 @@ function _resolveUserArtifactStagingRoot(configDir) {
879
896
  return stagingRoot;
880
897
  }
881
898
 
899
+ function isSkillsRuntimeInstall(runtime, scope) {
900
+ const isGlobal = scope === 'global';
901
+ const behaviors = hostBehaviorsFor(runtime);
902
+ if (isLegacyFlatLocalInstall(behaviors, scope) && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
903
+ // #2875 Part 2 defect fix: a runtime whose LOCAL commands are embedded in a
904
+ // rules file rather than materialized as files (hostBehaviors.localCommandsViaRules
905
+ // — cline is the only declarant, capabilities/cline/capability.json) must not
906
+ // flip into this skills/commands-reporting branch merely because its local
907
+ // artifactLayout now also declares an `agents` kind (#2875 Part 2 cline-local
908
+ // agents regression fix). That branch's own verification reporting expects a
909
+ // skills/ or commands/ directory this runtime never writes locally and would
910
+ // spuriously fail; the `localCommandsViaRules` branch below (unchanged
911
+ // messaging) and the unconditional agents-materialization block further down
912
+ // (installAgentsKindStandalone) already cover this runtime/scope correctly.
913
+ if (!isGlobal && behaviors.localCommandsViaRules) return false;
914
+ const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
915
+ const layout = cap && cap.runtime && cap.runtime.artifactLayout;
916
+ if (!layout) return false;
917
+ const scopeLayout = isGlobal ? layout.global : layout.local;
918
+ return Array.isArray(scopeLayout) && scopeLayout.length > 0;
919
+ }
920
+
921
+ function preflightInstallSymlinkDestinations(runtime, configDir, scope, installMode, agentProfile, installerMigrations) {
922
+ const check = (installRoot, destination, message) => {
923
+ if (hasExistingSymlinkBetween(path.resolve(installRoot), destination, { allowOptInFollow: isSymlinkedDestOptIn() })) {
924
+ throw new Error(message);
925
+ }
926
+ };
927
+
928
+ const coreDir = assertDestWithinConfigHome(configDir, path.join(configDir, 'gsd-core'));
929
+ check(configDir, coreDir, copyWithPathReplacementSymlinkRefusalMessage(coreDir, configDir));
930
+
931
+ const layout = resolveRuntimeArtifactLayout(runtime, configDir, scope, _installedCapabilityRegistry);
932
+ const behaviors = hostBehaviorsFor(runtime);
933
+ const combinedFamily = shouldInstallCombinedFamily(behaviors);
934
+ const legacyFlatLocal = isLegacyFlatLocalInstall(behaviors, scope);
935
+ const skillsRuntime = isSkillsRuntimeInstall(runtime, scope);
936
+ const standaloneAgents = shouldInstallStandaloneAgents(Boolean(behaviors.pluginOnlyInstall), skillsRuntime);
937
+
938
+ for (const kind of layout.kinds) {
939
+ // These flat writers have no write-time trust check to front-run.
940
+ // Keep their existing behavior here; a new refusal needs separate approval.
941
+ if (combinedFamily && kind.kind === 'commands') continue;
942
+ if (legacyFlatLocal && (kind.kind === 'commands' || kind.kind === 'skills')) continue;
943
+ const installRoot = resolveRuntimeArtifactInstallRoot(configDir, kind);
944
+ const dest = resolveRuntimeArtifactDestination(configDir, kind);
945
+ if (hasExistingSymlinkBetween(path.resolve(installRoot), dest, { allowOptInFollow: isSymlinkedDestOptIn() })) {
946
+ if (combinedFamily && kind.kind === 'skills') {
947
+ throw new Error(installOpencodeFamilySkillsSymlinkRefusalMessage(dest, installRoot));
948
+ }
949
+ if ((standaloneAgents || combinedFamily) && kind.kind === 'agents') {
950
+ throw new Error(installAgentsKindStandaloneSymlinkRefusalMessage(dest, installRoot));
951
+ }
952
+ throw new Error(installRuntimeArtifactsSymlinkRefusalMessage(dest, installRoot));
953
+ }
954
+ }
955
+
956
+ if (behaviors.tomlConfigInstall && codexAgentRolesEnabled(installMode, agentProfile)) {
957
+ const configPath = assertDestWithinConfigHome(configDir, 'config.toml');
958
+ const agentsTomlDir = assertDestWithinConfigHome(configDir, 'agents');
959
+ check(configDir, configPath, installCodexConfigSymlinkRefusalMessage(configDir));
960
+ check(configDir, path.resolve(agentsTomlDir), installCodexConfigSymlinkRefusalMessage(configDir));
961
+ // The layout agents pass prunes gsd-* entries (including TOML symlinks)
962
+ // before installCodexConfig. Its leaf guard sees the replacement state,
963
+ // so front-running that guard here would introduce a new refusal.
964
+ }
965
+
966
+ if (resolveInstallPlan(runtime).installSurface === 'codex-toml') {
967
+ const migrations = installerMigrations || discoverInstallerMigrations({
968
+ migrationsDir: path.join(_gsdLibDir, 'installer-migrations'),
969
+ });
970
+ // Planning is read-only. A pending id alone says nothing about whether
971
+ // this install's hooks/config inputs change (002 can itself be a no-op).
972
+ // Defer only actual mutations of those inputs; the migration executor
973
+ // and reconciler retain authority over publication and write failures.
974
+ // baselineScan intentionally stays off: migration 000 only records,
975
+ // preserves, or prompts; it cannot mutate hooks/config inputs. The real
976
+ // baseline scan and prompt resolution remain with the migration executor.
977
+ const migrationPlan = planInstallerMigrations({ configDir, runtime, scope, migrations });
978
+ if (plannedActionsMutateCodexHooksInputs(migrationPlan.actions)) return;
979
+ const configPath = path.join(configDir, 'config.toml');
980
+ let configContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : '';
981
+ if (codexAgentRolesEnabled(installMode, agentProfile)) {
982
+ // The managed config block contains no roster-dependent fields (#2406).
983
+ // If that changes, pass the writer's selected roster to both projections.
984
+ configContent = mergeCodexConfigContent(configContent, generateCodexConfigBlock([], configDir));
985
+ }
986
+ const prepared = prepareCodexHooksConfig(configContent);
987
+ // Extended-event cleanup reconciles hooks.json even when SessionStart has
988
+ // no script/runner. Inline/scalar features may leave reconciliation off.
989
+ if (hasEnabledCodexHooksFeature(prepared.content)) {
990
+ hooksSurface.resolveCodexHooksJsonDestination(configDir);
991
+ }
992
+ }
993
+ }
994
+
995
+ function prepareCodexAgentConfigContent(source, sourcePath, targetDir) {
996
+ let content = composeWorkflow(source, { sourcePath });
997
+ const codexGsdPath = `${path.resolve(targetDir, 'gsd-core').replace(/\\/g, '/')}/`;
998
+ // Replace full .claude/gsd-core prefix so path resolves to the Codex
999
+ // GSD install before generic .claude → .codex conversion rewrites it.
1000
+ // #5715: one pass, function replacement: a string replacement would expand `$&` in a
1001
+ // config dir, and a second pass would rescan the path the first emitted.
1002
+ content = content.replace(/(?:~|\$HOME)\/\.claude\/gsd-core\//g, () => codexGsdPath);
1003
+ // Route TOML emit through the same full Claude→Codex conversion pipeline
1004
+ // used on the `.md` emit path (#2639). Covers: slash-command rewrites,
1005
+ // $ARGUMENTS → {{GSD_ARGS}}, /clear removal, anchored and bare .claude/
1006
+ // paths, .claudeignore → .codexignore, and standalone "Claude" /
1007
+ // CLAUDE.md neutralization via neutralizeAgentReferences(..., 'AGENTS.md').
1008
+ // #5715: the converter runs after the path above was emitted; shield it from the converter.
1009
+ return runtimeArtifactConversion._shieldEmittedPrefix(content, codexGsdPath, convertClaudeToCodexMarkdown);
1010
+ }
1011
+
1012
+ function codexAgentTomlName(file, source) {
1013
+ const { frontmatter } = extractFrontmatterAndBody(source);
1014
+ // Resolve identity from converted frontmatter, matching the header
1015
+ // conversion in prepareCodexAgentConfigContent.
1016
+ return extractFrontmatterField(convertClaudeToCodexMarkdown(frontmatter), 'name') || file.replace(/\.md$/, '');
1017
+ }
1018
+
882
1019
  /**
883
1020
  * Degrade-not-abort wrapper over `_resolveUserArtifactStagingRoot` — mirrors
884
1021
  * install-engine.cts's own `_tryResolveUserArtifactStagingRoot` (kept local
@@ -2376,10 +2513,11 @@ function convertClaudeAgentToCopilotAgent(content, isGlobal = false) {
2376
2513
  const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
2377
2514
  const description = extractFrontmatterField(frontmatter, 'description') || '';
2378
2515
  const color = extractFrontmatterField(frontmatter, 'color');
2379
- const toolsRaw = extractFrontmatterField(frontmatter, 'tools') || '';
2380
2516
 
2381
- // CONV-04 + CONV-05: Map tools, deduplicate, format as JSON array
2382
- const claudeTools = toolsRaw.split(',').map(t => t.trim()).filter(Boolean);
2517
+ // CONV-04 + CONV-05: Map tools, deduplicate, format as JSON array.
2518
+ // parseFrontmatterTools reads both the inline comma list and the YAML block
2519
+ // list (#5790).
2520
+ const claudeTools = parseFrontmatterTools(frontmatter);
2383
2521
  const mappedTools = claudeTools.map(t => convertCopilotToolName(t));
2384
2522
  const uniqueTools = [...new Set(mappedTools)];
2385
2523
  const toolsArray = uniqueTools.length > 0
@@ -2659,7 +2797,15 @@ function convertSlashCommandsToTraeSkillMentions(content) {
2659
2797
  });
2660
2798
  }
2661
2799
 
2662
- function convertClaudeToTraeMarkdown(content) {
2800
+ function convertClaudeToTraeMarkdown(content, pathPrefix) {
2801
+ return runtimeArtifactConversion._shieldEmittedPrefix(
2802
+ runtimeArtifactConversion._resolveTraeInstructionFileAtPrefix(content, pathPrefix),
2803
+ pathPrefix,
2804
+ convertClaudeToTraeMarkdownBody,
2805
+ );
2806
+ }
2807
+
2808
+ function convertClaudeToTraeMarkdownBody(content) {
2663
2809
  let converted = convertSlashCommandsToTraeSkillMentions(filterRuntimeNotesForTarget(content, 'trae'));
2664
2810
  converted = converted.replace(/\bBash\(/g, 'Shell(');
2665
2811
  converted = converted.replace(/\bEdit\(/g, 'StrReplace(');
@@ -2719,9 +2865,10 @@ function convertClaudeToTraeMarkdown(content) {
2719
2865
  converted = converted.replace(/\.\/\.claude\//g, './.trae/');
2720
2866
  converted = converted.replace(/\.claude\//g, '.trae/');
2721
2867
  // Bare forms (no trailing slash) — after slash forms to avoid double-rewrite.
2722
- // Use negative lookahead (?![\w-]) to preserve .claude-plugin and .claudeignore.
2723
- converted = converted.replace(/~\/\.claude(?![\w-])/g, '~/.trae');
2724
- converted = converted.replace(/\$HOME\/\.claude(?![\w-])/g, '$HOME/.trae');
2868
+ // Use negative lookahead (?![\w-]|\.\w) to preserve .claude-plugin, .claudeignore and .claude.json.
2869
+ // #5715: an emitted prefix is shielded by convertClaudeToTraeMarkdown; the skills path runs this before the prefix pass, and the profile maps the own-dir forms.
2870
+ converted = converted.replace(/~\/\.claude(?![\w-]|\.\w)/g, '~/.trae');
2871
+ converted = converted.replace(/\$HOME\/\.claude(?![\w-]|\.\w)/g, () => '$HOME/.trae');
2725
2872
  // Environment variable name rewrite
2726
2873
  converted = converted.replace(/\bCLAUDE_CONFIG_DIR\b/g, 'TRAE_CONFIG_DIR');
2727
2874
  converted = converted.replace(/\*\*Known Claude Code bug \(classifyHandoffIfNeeded\):\*\*[^\n]*\n/g, '');
@@ -2765,8 +2912,8 @@ function convertClaudeCommandToTraeSkill(content, skillName) {
2765
2912
  return `${fm}\n${body}`;
2766
2913
  }
2767
2914
 
2768
- function convertClaudeAgentToTraeAgent(content) {
2769
- let converted = convertClaudeToTraeMarkdown(content);
2915
+ function convertClaudeAgentToTraeAgent(content, _isGlobal, meta) {
2916
+ let converted = convertClaudeToTraeMarkdown(content, meta && meta.pathPrefix);
2770
2917
 
2771
2918
  const { frontmatter, body } = extractFrontmatterAndBody(converted);
2772
2919
  if (!frontmatter) return converted;
@@ -4050,7 +4197,9 @@ function convertClaudeAgentToCodexAgent(content) {
4050
4197
 
4051
4198
  const name = extractFrontmatterField(frontmatter, 'name') || 'unknown';
4052
4199
  const description = extractFrontmatterField(frontmatter, 'description') || '';
4053
- const tools = extractFrontmatterField(frontmatter, 'tools') || '';
4200
+ // parseFrontmatterTools reads both the inline comma list and the YAML block
4201
+ // list (#5790).
4202
+ const tools = parseFrontmatterTools(frontmatter).join(', ');
4054
4203
 
4055
4204
  const roleHeader = `<codex_agent_role>
4056
4205
  role: ${name}
@@ -6547,15 +6696,15 @@ function resolveSymlinkWriteTarget(filePath) {
6547
6696
  }
6548
6697
 
6549
6698
  function mergeCodexConfig(configPath, gsdBlock) {
6550
- // #5301: both write sites below land on the symlink's target, never replace the link.
6699
+ // #5301: the write lands on the symlink's target, never replaces the link.
6551
6700
  configPath = resolveSymlinkWriteTarget(configPath);
6552
- // Case 1: No config.toml — create fresh
6553
- if (!fs.existsSync(configPath)) {
6554
- atomicWriteFileSync(configPath, gsdBlock + '\n');
6555
- return;
6556
- }
6701
+ const existing = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf8') : null;
6702
+ atomicWriteFileSync(configPath, mergeCodexConfigContent(existing, gsdBlock));
6703
+ }
6557
6704
 
6558
- const existing = fs.readFileSync(configPath, 'utf8');
6705
+ function mergeCodexConfigContent(existing, gsdBlock) {
6706
+ // Case 1: No config.toml — create fresh
6707
+ if (existing === null) return gsdBlock + '\n';
6559
6708
  const eol = detectLineEnding(existing);
6560
6709
  // #2088 review: the bare `[agents]` table is purged below (Case 2/3 via
6561
6710
  // stripLeakedGsdCodexSections) to keep a single managed `[agents]`. Preserve
@@ -6614,8 +6763,7 @@ function mergeCodexConfig(configPath, gsdBlock) {
6614
6763
  if (beforeSplit.rest) parts.push(beforeSplit.rest);
6615
6764
  parts.push(normalizedGsdBlock);
6616
6765
  if (afterTables) parts.push(afterTables);
6617
- atomicWriteFileSync(configPath, parts.join(eol + eol) + eol);
6618
- return;
6766
+ return parts.join(eol + eol) + eol;
6619
6767
  }
6620
6768
 
6621
6769
  // Case 3: No marker — append GSD block
@@ -6626,7 +6774,7 @@ function mergeCodexConfig(configPath, gsdBlock) {
6626
6774
  content = normalizedGsdBlock + eol;
6627
6775
  }
6628
6776
 
6629
- atomicWriteFileSync(configPath, content);
6777
+ return content;
6630
6778
  }
6631
6779
 
6632
6780
  /**
@@ -6811,6 +6959,19 @@ function ensureCodexHooksFeature(configContent) {
6811
6959
  return { content: configContent + (needsGap ? eol : '') + featuresBlock, ownership: 'section' };
6812
6960
  }
6813
6961
 
6962
+ function prepareCodexHooksConfig(configContent) {
6963
+ // Strip GSD-owned legacy blocks before migrating user hooks (#2698).
6964
+ // Share this exact projection with preflight, including inline/scalar
6965
+ // features values that ensureCodexHooksFeature deliberately preserves.
6966
+ const stripped = stripStaleGsdHookBlocks(configContent);
6967
+ const migrated = migrateCodexHooksMapFormat(stripped);
6968
+ const feature = ensureCodexHooksFeature(migrated);
6969
+ return {
6970
+ content: setManagedCodexHooksOwnership(feature.content, feature.ownership),
6971
+ migrated: migrated !== stripped,
6972
+ };
6973
+ }
6974
+
6814
6975
  function hasEnabledCodexHooksFeature(configContent) {
6815
6976
  const lineRecords = getTomlLineRecords(configContent);
6816
6977
 
@@ -7225,7 +7386,7 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
7225
7386
  hasExistingSymlinkBetween(resolvedTargetRoot, path.resolve(agentsTomlDir), { allowOptInFollow: symlinkOptIn })
7226
7387
  ) {
7227
7388
  throw new Error(
7228
- `installCodexConfig: a Codex config path under "${targetDir}" contains a symlink the install root does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
7389
+ installCodexConfigSymlinkRefusalMessage(targetDir),
7229
7390
  );
7230
7391
  }
7231
7392
  fs.mkdirSync(agentsTomlDir, { recursive: true });
@@ -7263,12 +7424,9 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
7263
7424
  const agentEntries = fs.readdirSync(agentsSrc).filter(f => f.startsWith('gsd-') && f.endsWith('.md'));
7264
7425
  const agents = [];
7265
7426
 
7266
- // Compute the Codex GSD install path (absolute, so subagents with empty $HOME work — #820)
7267
- const codexGsdPath = `${path.resolve(targetDir, 'gsd-core').replace(/\\/g, '/')}/`;
7268
-
7269
7427
  for (const file of agentEntries) {
7270
7428
  const agentTomlSourcePath = path.join(agentsSrc, file);
7271
- let content = fs.readFileSync(agentTomlSourcePath, 'utf8');
7429
+ const source = fs.readFileSync(agentTomlSourcePath, 'utf8');
7272
7430
  // #2995 (epic #1671 Phase 6.4): Codex embeds each agent's prompt into a
7273
7431
  // per-agent `.toml`, reading the source .md independently of the inline
7274
7432
  // agent loop — a separate emission path that must strip gsd:section
@@ -7276,17 +7434,7 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
7276
7434
  // Found by the exhaustive per-runtime emission sweep in
7277
7435
  // tests/agent-fragments-emission.install.test.cjs, not by call-graph
7278
7436
  // analysis, which is why that guard is behavioral rather than structural.
7279
- content = composeWorkflow(content, { sourcePath: agentTomlSourcePath });
7280
- // Replace full .claude/gsd-core prefix so path resolves to the Codex
7281
- // GSD install before generic .claude → .codex conversion rewrites it.
7282
- content = content.replace(/~\/\.claude\/gsd-core\//g, codexGsdPath);
7283
- content = content.replace(/\$HOME\/\.claude\/gsd-core\//g, codexGsdPath);
7284
- // Route TOML emit through the same full Claude→Codex conversion pipeline
7285
- // used on the `.md` emit path (#2639). Covers: slash-command rewrites,
7286
- // $ARGUMENTS → {{GSD_ARGS}}, /clear removal, anchored and bare .claude/
7287
- // paths, .claudeignore → .codexignore, and standalone "Claude" /
7288
- // CLAUDE.md neutralization via neutralizeAgentReferences(..., 'AGENTS.md').
7289
- content = convertClaudeToCodexMarkdown(content);
7437
+ const content = prepareCodexAgentConfigContent(source, agentTomlSourcePath, targetDir);
7290
7438
  const { frontmatter } = extractFrontmatterAndBody(content);
7291
7439
  // #3897 security review F1 (blocker, post-merge): this loop used to key
7292
7440
  // the sandbox/hold decision off ONLY the filename stem while the emitted
@@ -7301,7 +7449,7 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
7301
7449
  // for that call; see `deriveCodexSandboxMode`'s doc in
7302
7450
  // `codex-agent-toml.cts` for the resolution.
7303
7451
  const fileStem = file.replace(/\.md$/, '');
7304
- const name = extractFrontmatterField(frontmatter, 'name') || fileStem;
7452
+ const name = codexAgentTomlName(file, source);
7305
7453
  const description = extractFrontmatterField(frontmatter, 'description') || '';
7306
7454
 
7307
7455
  agents.push({ name, description: toSingleLine(description) });
@@ -7333,7 +7481,7 @@ function installCodexConfig(targetDir, agentsSrc, sandboxTier = 'codex-agent-san
7333
7481
  const agentTomlPath = assertDestWithinConfigHome(agentsTomlDir, `${name}.toml`);
7334
7482
  if (hasExistingSymlinkBetween(resolvedTargetRoot, agentTomlPath, { allowOptInFollow: symlinkOptIn })) {
7335
7483
  throw new Error(
7336
- `installCodexConfig: agent toml path "${agentTomlPath}" contains a symlink the install root does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
7484
+ installCodexAgentTomlSymlinkRefusalMessage(agentTomlPath),
7337
7485
  );
7338
7486
  }
7339
7487
  fs.writeFileSync(agentTomlPath, tomlContent);
@@ -7949,7 +8097,9 @@ const RUNTIME_CONTENT_DISPATCH = {
7949
8097
  },
7950
8098
  },
7951
8099
  trae: {
7952
- md: (content) => convertClaudeToTraeMarkdown(content),
8100
+ // #5715: the Trae converter shields (and resolves) the emitted prefix itself.
8101
+ mdShieldsPrefix: true,
8102
+ md: (content, ctx) => convertClaudeToTraeMarkdown(content, ctx && ctx.pathPrefix),
7953
8103
  js: (content) => {
7954
8104
  content = content.replace(/\/gsd:([a-z0-9-]+)/g, (_, commandName) => {
7955
8105
  return `/gsd-${commandName}`;
@@ -8060,7 +8210,7 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
8060
8210
  // #2393: honor GSD_ALLOW_SYMLINKED_DEST for intentional user-owned symlink layouts.
8061
8211
  if (hasExistingSymlinkBetween(resolvedConfinementRoot, resolvedDestDir, { allowOptInFollow: isSymlinkedDestOptIn() })) {
8062
8212
  throw new Error(
8063
- `copyWithPathReplacement: destDir "${destDir}" contains a symlink the install root "${confinementRoot}" does not trust — refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`,
8213
+ copyWithPathReplacementSymlinkRefusalMessage(destDir, confinementRoot),
8064
8214
  );
8065
8215
  }
8066
8216
  // Use the validated absolute path for all writes below so the gate validates
@@ -8152,33 +8302,8 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
8152
8302
  // balanced masking grammar used by this path and the rewrite engine.
8153
8303
  const rewriteGenericPaths = (body) => {
8154
8304
  content = body;
8155
- const globalClaudeRegex = /~\/\.claude\//g;
8156
- const globalClaudeHomeRegex = /\$HOME\/\.claude\//g;
8157
- const localClaudeRegex = /\.\/\.claude\//g;
8158
- content = content.replace(globalClaudeRegex, pathPrefix);
8159
- content = content.replace(globalClaudeHomeRegex, pathPrefix);
8160
- content = content.replace(localClaudeRegex, `./${dirName}/`);
8161
- // #3544 review (Finding 1 fallout): guarded with the SAME
8162
- // negative-lookahead convention already used at ~:2859-2860 below
8163
- // ("preserve .claude-plugin and .claudeignore"). A naive `\b` here
8164
- // is satisfied by ANY non-word character, including '-' — so for a
8165
- // --config-dir whose name EXTENDS '.claude' (e.g. '.claude-work',
8166
- // pathPrefix '$HOME/.claude-work/'), this pass re-matched the
8167
- // '$HOME/.claude' PREFIX of its own slash-form output (lines above)
8168
- // and re-appended the full prefix, corrupting every emitted path to
8169
- // '$HOME/.claude-work-work/...'. Harmless no-op for the literal
8170
- // default '.claude' (self-replace with an identical string), which
8171
- // is why this went undetected until a non-default config-dir name
8172
- // was exercised.
8173
- content = content.replace(/~\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
8174
- content = content.replace(/\$HOME\/\.claude(?![\w-])/g, pathPrefix.replace(/\/$/, ''));
8175
- content = content.replace(/\.\/\.claude\b/g, `./${dirName}`);
8176
- content = content.replace(/~\/\.qwen\//g, pathPrefix);
8177
- content = content.replace(/\$HOME\/\.qwen\//g, pathPrefix);
8178
- content = content.replace(/\.\/\.qwen\//g, `./${dirName}/`);
8179
- content = content.replace(/~\/\.hermes\//g, pathPrefix);
8180
- content = content.replace(/\$HOME\/\.hermes\//g, pathPrefix);
8181
- content = content.replace(/\.\/\.hermes\//g, `./${dirName}/`);
8305
+ // #5715: every generic source form in ONE pass (no rescan of the emitted prefix).
8306
+ content = runtimeArtifactConversion._rewriteSpecTreeConfigDirForms(content, pathPrefix, dirName);
8182
8307
  // #3544: restore @-file-reference lines to the tilde form Claude Code
8183
8308
  // actually expands — the SAME correction #3133 already applies to
8184
8309
  // skill/command bodies via _applyRuntimeRewrites's 'claude' case (see
@@ -8215,7 +8340,15 @@ function copyWithPathReplacement(srcDir, destDir, pathPrefix, runtime, isCommand
8215
8340
  content = normalizeAgentBodyForRuntime(content, runtime, readGsdCommandNames());
8216
8341
 
8217
8342
  // Apply per-runtime .md converter (if any)
8218
- if (dispatch.md) content = dispatch.md(content, ctx);
8343
+ // #5715: the generic pass above emitted `pathPrefix`; shield it from the converter
8344
+ // (a converter that skips the generic pass emitted nothing to shield).
8345
+ if (dispatch.md) {
8346
+ content = runtimeArtifactConversion._shieldEmittedPrefix(
8347
+ content,
8348
+ dispatch.mdSkipGenericRewrite || dispatch.mdShieldsPrefix ? undefined : pathPrefix,
8349
+ (shielded) => dispatch.md(shielded, ctx),
8350
+ );
8351
+ }
8219
8352
 
8220
8353
  // Re-run attribution after converter for runtimes that need it (copilot, antigravity)
8221
8354
  if (dispatch.mdReattributeAfter) content = processAttribution(content, getCommitAttribution(runtime));
@@ -10087,8 +10220,8 @@ const MANIFEST_NAME = 'gsd-file-manifest.json';
10087
10220
  * Compute SHA256 hash of file contents
10088
10221
  */
10089
10222
  function fileHash(filePath) {
10090
- const content = fs.readFileSync(filePath);
10091
- return crypto.createHash('sha256').update(content).digest('hex');
10223
+ // #5683: single owner of the per-file hash, shared with `effort sync`.
10224
+ return require('../gsd-core/bin/lib/install-manifest-record.cjs').fileHash(filePath);
10092
10225
  }
10093
10226
 
10094
10227
  /**
@@ -11125,26 +11258,6 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11125
11258
  warnIfForeignAgentDest(runtime, targetDir, _installScopeId, Boolean(explicitConfigDir));
11126
11259
  }
11127
11260
 
11128
- // #2875 (#1874-F19 anti-inertness, test-matrix C7): recover any user
11129
- // artifact orphaned by a PRIOR install run that died between staging and
11130
- // its own restore/discard, BEFORE this run's own preserve step stages
11131
- // anything new. This is the production entry point every install() call
11132
- // reaches — the only place this phase's durability fix is complete rather
11133
- // than merely callable (40-design.md "The inertness trap this design must
11134
- // avoid" / #1879-F15). Runs for every runtime, ahead of both the
11135
- // layout-driven path's _runLegacyInstallMigrations (site 1, inside
11136
- // installRuntimeArtifacts) and this function's own mainline gsd-core copy
11137
- // (site 4, below).
11138
- // #2875 defect fix: DEGRADE, never abort install, when the staging root
11139
- // itself cannot be resolved — skip this recovery pass rather than throw
11140
- // out of install() before it does anything.
11141
- {
11142
- const _installEntryStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
11143
- if (_installEntryStagingRoot !== null) {
11144
- recoverOrphanedUserArtifacts(_installEntryStagingRoot, targetDir);
11145
- }
11146
- }
11147
-
11148
11261
  const locationLabel = isGlobal
11149
11262
  ? targetDir.replace(os.homedir(), '~')
11150
11263
  : targetDir.replace(process.cwd(), '.');
@@ -11226,7 +11339,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11226
11339
  // #5503: Codex registers agent roles in config.toml. That used to be gated on "minimal mode"
11227
11340
  // because minimal staged no agents; gate it on whether the resolved profile stages any.
11228
11341
  const _codexAgentRolesEnabled = () =>
11229
- !isMinimalMode(_effectiveInstallMode) || (_resolvedProfile.agents instanceof Set && _resolvedProfile.agents.size > 0);
11342
+ codexAgentRolesEnabled(_effectiveInstallMode, _resolvedProfile);
11343
+ // Preserve rollback eligibility from the original profile, before preflight
11344
+ // resolves the committed surface for the later writers.
11345
+ const _codexPreConfigRollbackArmed = _codexAgentRolesEnabled();
11230
11346
  const persistActiveProfileMarker = () => {
11231
11347
  try {
11232
11348
  writeActiveProfile(targetDir, _activeProfileName);
@@ -11251,6 +11367,36 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11251
11367
  localDirName: hostBehaviorsFor(runtime).localTargetIsProjectRoot === true ? undefined : getDirName(runtime),
11252
11368
  });
11253
11369
 
11370
+ // Resolve the committed surface before preflight and keep that selection
11371
+ // for every subsequent writer, including Codex's staged agent TOMLs.
11372
+ if (isSkillsRuntimeInstall(runtime, _installScopeId)) {
11373
+ const _committedSurface = readSurface(targetDir);
11374
+ if (_committedSurface) {
11375
+ _resolvedProfile = resolveSurface(
11376
+ targetDir,
11377
+ loadSkillsManifest(_commandsDir),
11378
+ undefined,
11379
+ _installedCapabilityRegistry,
11380
+ _committedSurface,
11381
+ );
11382
+ }
11383
+ }
11384
+ // Preflight's role gate and _stageAgents use the same resolved profile;
11385
+ // core's agent roles follow #5503's profile-derived selection too.
11386
+ preflightInstallSymlinkDestinations(runtime, targetDir, _installScopeId, _effectiveInstallMode, _resolvedProfile, options.installerMigrations);
11387
+
11388
+ // #2875 (#1874-F19 anti-inertness, test-matrix C7): recover any user
11389
+ // artifact orphaned by a PRIOR install run that died between staging and
11390
+ // its own restore/discard, BEFORE this run's own preserve step stages
11391
+ // anything new. Destination preflight runs before this recovery write;
11392
+ // checks for destinations replaced before admission stay with the writers.
11393
+ {
11394
+ const _installEntryStagingRoot = _tryResolveUserArtifactStagingRoot(targetDir);
11395
+ if (_installEntryStagingRoot !== null) {
11396
+ recoverOrphanedUserArtifacts(_installEntryStagingRoot, targetDir);
11397
+ }
11398
+ }
11399
+
11254
11400
  // runtimeLabel is now the single-source getRuntimeLabel lookup (ADR-1239
11255
11401
  // Phase B / #1679) — collapses the prior 16-line assignment chain.
11256
11402
  const runtimeLabel = getRuntimeLabel(runtime);
@@ -11601,7 +11747,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11601
11747
  // The full restoreCodexSnapshot() (defined inside the config block) additionally
11602
11748
  // handles config.toml and the staged hooks/ tree, which are not yet touched
11603
11749
  // at this point in the pipeline.
11604
- const _codexPreConfigRollback = !hostBehaviorsFor(runtime).tomlConfigInstall || !_codexAgentRolesEnabled() ? null : () => {
11750
+ const _codexPreConfigRollback = !hostBehaviorsFor(runtime).tomlConfigInstall || !_codexPreConfigRollbackArmed ? null : () => {
11605
11751
  rollbackInstallerMigrations();
11606
11752
  // skills/gsd-* — pass 1: restore snapshot entries (may be absent if deleted mid-install).
11607
11753
  const _earlySkillsDir = _resolveSkillsRootDir(runtime, targetDir, _installScopeId);
@@ -11802,25 +11948,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11802
11948
  // install (ADR-1239 / #2087), replacing the bespoke inline block this comment
11803
11949
  // used to describe. Claude-local remains the one special-cased path
11804
11950
  // (copyWithPathReplacement + stale-skills cleanup).
11805
- const _isSkillsRuntime = (() => {
11806
- if (hostBehaviorsFor(runtime).localInstallStyle === 'legacy-flat' && !isGlobal) return false; // legacy flat local path (descriptor-driven; #2086)
11807
- // #2875 Part 2 defect fix: a runtime whose LOCAL commands are embedded in a
11808
- // rules file rather than materialized as files (hostBehaviors.localCommandsViaRules
11809
- // — cline is the only declarant, capabilities/cline/capability.json) must not
11810
- // flip into this skills/commands-reporting branch merely because its local
11811
- // artifactLayout now also declares an `agents` kind (#2875 Part 2 cline-local
11812
- // agents regression fix). That branch's own verification reporting expects a
11813
- // skills/ or commands/ directory this runtime never writes locally and would
11814
- // spuriously fail; the `localCommandsViaRules` branch below (unchanged
11815
- // messaging) and the unconditional agents-materialization block further down
11816
- // (installAgentsKindStandalone) already cover this runtime/scope correctly.
11817
- if (!isGlobal && hostBehaviorsFor(runtime).localCommandsViaRules) return false;
11818
- const cap = _capabilityRegistry && _capabilityRegistry.runtimes && _capabilityRegistry.runtimes[runtime];
11819
- const layout = cap && cap.runtime && cap.runtime.artifactLayout;
11820
- if (!layout) return false;
11821
- const scopeLayout = isGlobal ? layout.global : layout.local;
11822
- return Array.isArray(scopeLayout) && scopeLayout.length > 0;
11823
- })();
11951
+ const _isSkillsRuntime = isSkillsRuntimeInstall(runtime, _installScopeId);
11824
11952
 
11825
11953
  // Install the distribution-owned gsd-core tree before layout materialization.
11826
11954
  // installRuntimeArtifacts then provisions the durable Runtime Surface corpus
@@ -11889,19 +12017,6 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
11889
12017
  if (_isSkillsRuntime) {
11890
12018
  // Layout-driven install for skills-based runtimes (full and minimal modes)
11891
12019
  const scope = _installScopeId;
11892
- // Preserve an existing Runtime Surface selection during upgrades. The
11893
- // installer refreshes the source corpus first, then emits exactly the
11894
- // already-committed selection instead of widening it to the base profile.
11895
- const _committedSurface = readSurface(targetDir);
11896
- if (_committedSurface) {
11897
- _resolvedProfile = resolveSurface(
11898
- targetDir,
11899
- loadSkillsManifest(_commandsDir),
11900
- undefined,
11901
- _installedCapabilityRegistry,
11902
- _committedSurface,
11903
- );
11904
- }
11905
12020
  // ADR-1239 upgrade 3 / #2088: a kind may declare an alternate install `home`
11906
12021
  // (e.g. Codex skills -> $HOME/.agents/skills) instead of the runtime's normal
11907
12022
  // configDir. Resolve the ACTUAL on-disk skills root here, descriptor-driven
@@ -12310,7 +12425,7 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
12310
12425
  // returns `null` without writing anything.
12311
12426
  if (hostBehaviorsFor(runtime).pluginOnlyInstall) {
12312
12427
  console.log(` ${green}✓${reset} pi: no subagent files (programmatic dispatch, no named-dispatch toolkit)`);
12313
- } else if (_isSkillsRuntime) {
12428
+ } else if (!shouldInstallStandaloneAgents(Boolean(hostBehaviorsFor(runtime).pluginOnlyInstall), _isSkillsRuntime)) {
12314
12429
  console.log(` ${dim}↳${reset} Agents installed via descriptor-driven layout (${runtime})`);
12315
12430
  } else {
12316
12431
  const _standaloneProjectDir = isGlobal ? process.cwd() : targetDir;
@@ -13298,35 +13413,13 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13298
13413
  // failure (#2760 fix 3) — not to the post-agent-merge state.
13299
13414
  const preWriteBackup = codexConfigPreInstallSnapshot;
13300
13415
  try {
13301
- let configContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : '';
13302
- const eol = detectLineEnding(configContent);
13303
-
13304
- // Strip ALL prior GSD-managed hook blocks BEFORE migration so the migration
13305
- // only touches user-authored hooks, not GSD-owned stale entries. Running
13306
- // strip after migration causes Shape 1 (legacy gsd-update-check filename)
13307
- // to be converted by migration before the strip regex can match it (#2698).
13308
- //
13309
- // Historical shapes stripped, in order:
13310
- // Shape 1 — legacy gsd-update-check filename (pre-#1755): flat [[hooks]] + event
13311
- // Shape 2 — flat [[hooks]] + event = "SessionStart" (#2637 era, never correct)
13312
- // Shape 4 — correct two-block nested (strip before shape 3 to avoid orphaned header)
13313
- // Shape 3 — single-block [[hooks.SessionStart]] without nested .hooks (#2760 era)
13314
- configContent = stripStaleGsdHookBlocks(configContent);
13315
-
13316
- // Migrate legacy [hooks] map format and flat [[hooks]] AoT entries to the
13317
- // namespaced [[hooks.<EVENT>]] form after stripping GSD-managed stale blocks.
13318
- // Running migration after strip ensures only user-authored hooks are migrated
13319
- // (#2698 regression: migration before strip converts stale GSD blocks before
13320
- // the strip regexes can match their original shape).
13321
- const migratedContent = migrateCodexHooksMapFormat(configContent);
13322
- if (migratedContent !== configContent) {
13323
- configContent = migratedContent;
13416
+ const existingConfigContent = fs.existsSync(configPath) ? fs.readFileSync(configPath, 'utf-8') : '';
13417
+ const prepared = prepareCodexHooksConfig(existingConfigContent);
13418
+ const configContent = prepared.content;
13419
+ if (prepared.migrated) {
13324
13420
  console.log(` ${green}✓${reset} Migrated legacy Codex [hooks] format to two-level nested AoT`);
13325
13421
  }
13326
13422
 
13327
- const codexHooksFeature = ensureCodexHooksFeature(configContent);
13328
- configContent = setManagedCodexHooksOwnership(codexHooksFeature.content, codexHooksFeature.ownership);
13329
-
13330
13423
  // GSD-managed Codex hook payloads now live in hooks.json to avoid mixed
13331
13424
  // representation warnings when a single layer contains both hooks.json
13332
13425
  // and inline [hooks] entries. Keep config.toml focused on feature flags
@@ -13855,14 +13948,18 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13855
13948
  // skip managed-hook registration. Returning null from buildHookCommand on
13856
13949
  // global installs has the same effect. Better to skip than to emit a bare
13857
13950
  // `node` command that recreates the #2979 failure.
13951
+ // #5850: Claude on Windows always gets the PowerShell runner (owner ruling).
13952
+ const localUsesPowerShell = hooksSurface.usesPowerShellHookShell({ platform: process.platform, runtime });
13858
13953
  const localCmd = (hookFile) => localNodeRunner === null
13859
13954
  ? null
13860
- : hooksSurface.recordConfiguredHookCommand(projectShellCommandText({
13861
- runnerToken: localNodeRunner,
13862
- argTokens: [`${localPrefix}/hooks/${hookFile}`],
13863
- runtime,
13864
- platform: process.platform,
13865
- }), targetDir, hookFile, hookOpts);
13955
+ : hooksSurface.recordConfiguredHookCommand(localUsesPowerShell
13956
+ ? hooksSurface.buildPowerShellLocalHookCommand({ ...hookOpts, localPrefix, hookFile })
13957
+ : projectShellCommandText({
13958
+ runnerToken: localNodeRunner,
13959
+ argTokens: [`${localPrefix}/hooks/${hookFile}`],
13960
+ runtime,
13961
+ platform: process.platform,
13962
+ }), targetDir, hookFile, hookOpts);
13866
13963
  const localShellCmd = (hookFile) => hooksSurface.recordConfiguredHookCommand(buildLocalShellHookCommand({
13867
13964
  localPrefix,
13868
13965
  hookFile,
@@ -13871,8 +13968,10 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13871
13968
  platform: process.platform,
13872
13969
  }), targetDir, hookFile, hookOpts);
13873
13970
  const statuslineCommand = isGlobal
13874
- ? buildHookCommand(targetDir, 'gsd-statusline.js', hookOpts)
13875
- : localCmd('gsd-statusline.js');
13971
+ ? buildHookCommand(targetDir, 'gsd-statusline.js', { ...hookOpts, statusLine: true })
13972
+ // #5850: a local statusLine has no `$`-free PowerShell spelling (the project
13973
+ // dir is a shell variable); it is skipped on Windows rather than written broken.
13974
+ : (localUsesPowerShell ? null : localCmd('gsd-statusline.js'));
13876
13975
  const updateCheckCommand = isGlobal
13877
13976
  ? buildHookCommand(targetDir, 'gsd-check-update.js', hookOpts)
13878
13977
  : localCmd('gsd-check-update.js');
@@ -13993,7 +14092,9 @@ function install(isGlobal, runtime = DEFAULT_RUNTIME, options = {}) {
13993
14092
  const candidate = path.join(probeDir, '.planning', 'config.json');
13994
14093
  if (fs.existsSync(candidate)) {
13995
14094
  try {
13996
- const parsed = JSON.parse(stripJsonComments(fs.readFileSync(candidate, 'utf-8')));
14095
+ // #5759: bounded regular-file read — a FIFO/device/oversized config throws to the
14096
+ // catch below (treated as enabled) instead of blocking the installer.
14097
+ const parsed = JSON.parse(stripJsonComments(readConfigFileBounded(candidate) ?? ''));
13997
14098
  if (parsed && typeof parsed === 'object' &&
13998
14099
  parsed.workflow && parsed.workflow.use_worktrees === false) {
13999
14100
  worktreesEnabled = false;
@@ -14151,7 +14252,7 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
14151
14252
  // #3002 CR: don't write { type: 'command', command: null } — the
14152
14253
  // runtime's settings schema rejects null commands and the failure
14153
14254
  // surfaces as a confusing parse error rather than a usable diagnostic.
14154
- console.warn(` ${yellow}⚠${reset} Skipped statusline registration — Node executable path unavailable (process.execPath is empty). See #2979 / #3002.`);
14255
+ console.warn(` ${yellow}⚠${reset} Skipped statusline registration — Node executable path unavailable (process.execPath is empty), or a local Windows install (no shell-neutral statusline spelling). See #2979 / #3002 / #5850.`);
14155
14256
  } else {
14156
14257
  settings.statusLine = {
14157
14258
  type: 'command',
@@ -14213,6 +14314,9 @@ function finishInstall(settingsPath, settings, statuslineCommand, shouldInstallS
14213
14314
  // rejects at parse time. validateHookFields filters those out so the file
14214
14315
  // we write is always schema-valid.
14215
14316
  if (settingsPath && settings && plan.writesSharedSettings) {
14317
+ // #5850: pin `shell: "powershell"` on PowerShell-form hooks so Claude Code
14318
+ // does not depend on its own Git Bash detection to choose the shell.
14319
+ hooksSurface.projectPowerShellHookShell(settings);
14216
14320
  writeSettings(settingsPath, validateHookFields(settings));
14217
14321
  }
14218
14322