@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
@@ -11,6 +11,7 @@ const _require = require;
11
11
  const path = _require('node:path');
12
12
  const { tryWithinRootLexical } = _require('./security.cjs');
13
13
  const { hostBehaviorsFor } = _require('./runtime-name-policy.cjs');
14
+ const { isMinimalMode } = _require('./install-profiles.cjs');
14
15
  // #2870: InstallScope is owned by install-scope.cts, not re-declared here.
15
16
  // `isGlobalScope` centralizes the `scope === 'global'` boolean projection
16
17
  // this module needs at `_computePathPrefix`'s `isGlobal: boolean` boundary
@@ -49,6 +50,64 @@ function assertDestWithinConfigHome(configDir, destSubpath) {
49
50
  }
50
51
  return contained;
51
52
  }
53
+ function resolveRuntimeArtifactDestination(configDir, kind) {
54
+ return assertDestWithinConfigHome(resolveRuntimeArtifactInstallRoot(configDir, kind), kind.destSubpath);
55
+ }
56
+ function resolveRuntimeArtifactInstallRoot(configDir, kind) {
57
+ if (kind?.home != null && typeof kind.home !== 'string') {
58
+ throw new TypeError('Artifact home must be a string, null, or undefined');
59
+ }
60
+ return (kind && typeof kind.home === 'string' && kind.home !== '') ? kind.home : configDir;
61
+ }
62
+ function shouldInstallCombinedFamily(behaviors) {
63
+ return Boolean(behaviors.combinedFamilyInstall);
64
+ }
65
+ function isLegacyFlatLocalInstall(behaviors, scope) {
66
+ return scope === 'local' && behaviors.localInstallStyle === 'legacy-flat';
67
+ }
68
+ function shouldInstallStandaloneAgents(pluginOnlyInstall, skillsRuntime) {
69
+ return !pluginOnlyInstall && !skillsRuntime;
70
+ }
71
+ function requiredRuntimeSurfaceSources(layout, scope) {
72
+ const required = new Set();
73
+ if (!(0, install_scope_cjs_1.isGlobalScope)(scope))
74
+ return required;
75
+ for (const kind of layout.kinds) {
76
+ if (kind.kind === 'commands' || kind.kind === 'skills')
77
+ required.add('commands');
78
+ if (kind.kind === 'agents' || kind.kind === 'kimi-agents')
79
+ required.add('agents');
80
+ }
81
+ return required;
82
+ }
83
+ function codexAgentRolesEnabled(installMode, resolvedProfile) {
84
+ return !isMinimalMode(installMode) || (resolvedProfile.agents instanceof Set && resolvedProfile.agents.size > 0);
85
+ }
86
+ function plannedActionsMutateCodexHooksInputs(actions) {
87
+ return actions.some((action) => (action.relPath === 'hooks.json' || action.relPath === 'config.toml') &&
88
+ ['rewrite-json', 'remove-managed', 'backup-and-remove'].includes(action.type));
89
+ }
90
+ function copyWithPathReplacementSymlinkRefusalMessage(destDir, installRoot) {
91
+ return `copyWithPathReplacement: destDir "${destDir}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
92
+ }
93
+ function installRuntimeArtifactsSymlinkRefusalMessage(dest, installRoot) {
94
+ return `installRuntimeArtifacts: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to create. If this is an intentional user-owned symlink layout (e.g. externalized skills/hooks dir, multi-account configHome, or a dotfiles-managed configHome), re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
95
+ }
96
+ function installOpencodeFamilySkillsSymlinkRefusalMessage(dest, installRoot) {
97
+ return `installOpencodeFamilySkills: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
98
+ }
99
+ function installAgentsKindStandaloneSymlinkRefusalMessage(dest, installRoot) {
100
+ return `installAgentsKindStandalone: destDir "${dest}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
101
+ }
102
+ function syncRuntimeSurfaceCorpusSymlinkRefusalMessage(destination, installRoot) {
103
+ return `syncRuntimeSurfaceCorpus: destination "${destination}" contains a symlink the install root "${installRoot}" does not trust \u2014 refusing to write.`;
104
+ }
105
+ function installCodexConfigSymlinkRefusalMessage(targetDir) {
106
+ return `installCodexConfig: a Codex config path under "${targetDir}" contains a symlink the install root does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
107
+ }
108
+ function installCodexAgentTomlSymlinkRefusalMessage(agentTomlPath) {
109
+ return `installCodexConfig: agent toml path "${agentTomlPath}" contains a symlink the install root does not trust \u2014 refusing to write. If this is an intentional user-owned symlink layout, re-run with GSD_ALLOW_SYMLINKED_DEST=1.`;
110
+ }
52
111
  function errorMessage(err) {
53
112
  if (err instanceof Error)
54
113
  return err.message;
@@ -135,7 +194,7 @@ function createRuntimeArtifactInstallPlan(args) {
135
194
  items.push({
136
195
  kind: kind.kind,
137
196
  sourceDir,
138
- destDir: assertDestWithinConfigHome(kind.home ?? layout.configDir, kind.destSubpath),
197
+ destDir: resolveRuntimeArtifactDestination(layout.configDir, kind),
139
198
  });
140
199
  }
141
200
  return { ok: true, plan: { items, cleanupDirs } };
@@ -144,8 +203,27 @@ function createRuntimeArtifactUninstallPlan(layout) {
144
203
  return {
145
204
  items: layout.kinds.map((kind) => ({
146
205
  kind: kind.kind,
147
- destDir: assertDestWithinConfigHome(kind.home ?? layout.configDir, kind.destSubpath),
206
+ destDir: resolveRuntimeArtifactDestination(layout.configDir, kind),
148
207
  })),
149
208
  };
150
209
  }
151
- module.exports = { assertDestWithinConfigHome, createRuntimeArtifactInstallPlan, createRuntimeArtifactUninstallPlan };
210
+ module.exports = {
211
+ assertDestWithinConfigHome,
212
+ resolveRuntimeArtifactDestination,
213
+ resolveRuntimeArtifactInstallRoot,
214
+ shouldInstallCombinedFamily,
215
+ isLegacyFlatLocalInstall,
216
+ shouldInstallStandaloneAgents,
217
+ requiredRuntimeSurfaceSources,
218
+ codexAgentRolesEnabled,
219
+ plannedActionsMutateCodexHooksInputs,
220
+ copyWithPathReplacementSymlinkRefusalMessage,
221
+ installRuntimeArtifactsSymlinkRefusalMessage,
222
+ installOpencodeFamilySkillsSymlinkRefusalMessage,
223
+ installAgentsKindStandaloneSymlinkRefusalMessage,
224
+ syncRuntimeSurfaceCorpusSymlinkRefusalMessage,
225
+ installCodexConfigSymlinkRefusalMessage,
226
+ installCodexAgentTomlSymlinkRefusalMessage,
227
+ createRuntimeArtifactInstallPlan,
228
+ createRuntimeArtifactUninstallPlan,
229
+ };
@@ -565,6 +565,12 @@ function convertedAgentsKind(destSubpath, prefix, converterName, configDir, sour
565
565
  // (content) ignore the extra positional arg. Mirrors skillsKind's scope
566
566
  // threading (#1173).
567
567
  converter = (content) => rawConverter(content, (0, install_scope_cjs_1.isGlobalScope)(scope));
568
+ if (converterName === 'convertClaudeAgentToTraeAgent') {
569
+ // #5715: the Trae agent converter needs the prefix the path-rewrite step
570
+ // already emitted, so it can leave it alone.
571
+ const traeConverter = rawConverter;
572
+ converter = (content, _isGlobal, meta) => traeConverter(content, (0, install_scope_cjs_1.isGlobalScope)(scope), meta);
573
+ }
568
574
  }
569
575
  // ADR-1235 §1: when agentCtx is provided (by createRuntimeArtifactInstallPlan
570
576
  // for descriptor-driven runtimes), thread it through so stageAgentsForRuntimeWithConverter
@@ -31,6 +31,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
31
31
  const node_fs_1 = __importDefault(require("node:fs"));
32
32
  const node_path_1 = __importDefault(require("node:path"));
33
33
  const node_os_1 = __importDefault(require("node:os"));
34
+ const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
34
35
  // #2544: the single source of truth for the CommonJS module-type marker. The
35
36
  // two helpers below are thin boolean-returning shims over these — see the
36
37
  // marker section for why this file no longer carries its own copy.
@@ -615,6 +616,182 @@ function resolveBashRunner(opts) {
615
616
  ? JSON.stringify(executable)
616
617
  : executable;
617
618
  }
619
+ // ---------------------------------------------------------------------------
620
+ // #5850 — PowerShell hook runner (Claude Code on Windows)
621
+ // ---------------------------------------------------------------------------
622
+ /**
623
+ * #5850 (owner ruling: always PowerShell on win32): every managed JS hook command
624
+ * Claude gets on Windows is a PowerShell command carrying an explicit
625
+ * `shell: "powershell"` (see projectPowerShellHookShell), whether or not Git Bash
626
+ * is discoverable. The POSIX `$(for ... done)` runner chain does not parse when
627
+ * Claude Code hosts the hook in PowerShell, and a blocking PreToolUse guard that
628
+ * cannot start blocks every tool call. An explicit `hookShell: 'bash'` still
629
+ * selects the chain (a test/projection seam, never set by the installer).
630
+ */
631
+ function usesPowerShellHookShell(opts) {
632
+ if (opts && opts.hookShell === 'bash')
633
+ return false;
634
+ const platform = (opts && opts.platform) || process.platform;
635
+ const runtime = (opts && opts.runtime) || 'generic';
636
+ return platform === 'win32' && (0, runtime_name_policy_cjs_1.hostBehaviorsFor)(runtime).powershellHooksOnWindows === true;
637
+ }
638
+ /**
639
+ * A PowerShell single-quoted string literal. PowerShell treats U+2018..U+201B
640
+ * as single-quote delimiters as well as the ASCII quote, and each is escaped by
641
+ * doubling itself.
642
+ */
643
+ function powerShellQuote(value) {
644
+ return `'${value.replace(/['\u2018\u2019\u201A\u201B]/g, (q) => q + q)}'`;
645
+ }
646
+ /**
647
+ * A hook script path as a PowerShell argument. A path anchored on
648
+ * `"$CLAUDE_PROJECT_DIR"/...` (local installs) or `$HOME/...` (portable installs)
649
+ * becomes an expanding PowerShell expression; any other path is a single-quoted
650
+ * literal. A path carrying some OTHER shell variable (`$FOO`) is a POSIX token
651
+ * with no PowerShell spelling here: null, and the caller leaves the entry alone.
652
+ */
653
+ function powerShellScriptArg(scriptPath, opts = {}) {
654
+ const projectDir = scriptPath.match(/^"\$CLAUDE_PROJECT_DIR"(\/.*)$/);
655
+ const homeDir = scriptPath.match(/^\$HOME(\/.*)$/);
656
+ const anchored = projectDir || homeDir;
657
+ if (anchored) {
658
+ const variable = projectDir ? '$env:CLAUDE_PROJECT_DIR' : '$HOME';
659
+ const suffix = anchored[1];
660
+ // Plain suffixes keep the readable double-quoted expansion; any character
661
+ // PowerShell treats specially inside double quotes (including the curly
662
+ // double quotes) moves the suffix into a single-quoted literal.
663
+ if (!/["`$\u201C\u201D\u201E]/.test(suffix))
664
+ return `"${variable}${suffix}"`;
665
+ return `(${variable} + ${powerShellQuote(suffix)})`;
666
+ }
667
+ // A real filesystem path (the installer's own configDir) may contain `$`; a
668
+ // token read back from settings.json may not (it would never expand).
669
+ if (!opts.literal && scriptPath.includes('$'))
670
+ return null;
671
+ return powerShellQuote(shellCmdProjection.posixNormalize(scriptPath));
672
+ }
673
+ /**
674
+ * The PowerShell counterpart of buildNodeRunnerChainToken: the baked install-time
675
+ * node first (the minimal-PATH guarantee), then node on PATH, resolved when the
676
+ * hook fires. `scriptToken` is an already-quoted PowerShell argument.
677
+ */
678
+ function buildPowerShellHookCommand(scriptToken, opts) {
679
+ const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
680
+ if (!execPath)
681
+ return null;
682
+ const baked = powerShellQuote(shellCmdProjection.posixNormalize(normalizeNodePath(execPath, opts)));
683
+ // The `exit` tail: `powershell -Command` collapses a native exit code to 0/1
684
+ // unless the script exits with it, and a blocking guard denies with exit 2. A
685
+ // runner that never started leaves $LASTEXITCODE null and exits 1, not 0.
686
+ return `& ((@(${baked}) + @(Get-Command node -CommandType Application -ErrorAction SilentlyContinue | Where-Object { $_.Source -like '*.exe' } | ForEach-Object { $_.Source })) | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1) ${scriptToken}; exit $(if ($null -ne $LASTEXITCODE) { $LASTEXITCODE } else { 1 })`;
687
+ }
688
+ /**
689
+ * Local installs anchor the script under `"$CLAUDE_PROJECT_DIR"/<dir>`; the
690
+ * PowerShell spelling of that variable is `$env:CLAUDE_PROJECT_DIR`.
691
+ */
692
+ function buildPowerShellLocalHookCommand(opts) {
693
+ if (!opts.localPrefix || !opts.hookFile)
694
+ return null;
695
+ const arg = powerShellScriptArg(`${opts.localPrefix}/hooks/${opts.hookFile}`);
696
+ return arg === null ? null : buildPowerShellHookCommand(arg, opts);
697
+ }
698
+ /**
699
+ * The statusLine spelling. Claude Code runs a statusLine command through Git Bash
700
+ * when it is installed and through PowerShell when it is not, and a statusLine has
701
+ * no `shell` key to pin that. One command line that both hosts run is
702
+ * `powershell -NoProfile -Command "..."` with a body free of `$`, backticks and
703
+ * backslashes (all special inside bash double quotes). null when a path carries
704
+ * one of those, so the statusLine is skipped rather than written broken.
705
+ */
706
+ function buildPowerShellStatusLineCommand(scriptPath, opts) {
707
+ const execPath = (opts && opts.execPath) || (typeof process.execPath === 'string' ? process.execPath : '');
708
+ if (!execPath)
709
+ return null;
710
+ const baked = shellCmdProjection.posixNormalize(normalizeNodePath(execPath, opts));
711
+ const script = shellCmdProjection.posixNormalize(scriptPath);
712
+ if ([baked, script].some((p) => /["`$\\]/.test(p)))
713
+ return null;
714
+ const b = powerShellQuote(baked);
715
+ const sc = powerShellQuote(script);
716
+ return `powershell -NoProfile -Command "if (Test-Path -LiteralPath ${b}) { & ${b} ${sc} } else { & node ${sc} }"`;
717
+ }
718
+ const POWERSHELL_STATUSLINE_COMMAND = /^powershell -NoProfile -Command "if \(Test-Path -LiteralPath '(?:[^']|'')*'\) \{ & '(?:[^']|'')*' '((?:[^']|'')*)' \} else \{ & node '((?:[^']|'')*)' \}"$/;
719
+ function unquotePowerShell(value) {
720
+ return value.replace(/(['\u2018\u2019\u201A\u201B])\1/g, '$1');
721
+ }
722
+ /**
723
+ * Read the script back out of a PowerShell runner or statusLine command, as the
724
+ * POSIX path/token pair projectLegacySettingsHookCommand takes. null when the
725
+ * command is not one of GSD's PowerShell shapes.
726
+ */
727
+ function parsePowerShellScript(command) {
728
+ // A path bash would expand or re-quote (`$`, backtick, `"`) has no faithful POSIX
729
+ // spelling here: refuse, and the entry stays as written. (A portable install under
730
+ // a directory named with one of them therefore stays PowerShell on a POSIX host.)
731
+ const posixUnsafe = /["`$]/;
732
+ const status = command.match(POWERSHELL_STATUSLINE_COMMAND);
733
+ if (status) {
734
+ const scriptPath = unquotePowerShell(status[2]);
735
+ return posixUnsafe.test(scriptPath) ? null : { scriptPath, scriptToken: JSON.stringify(scriptPath) };
736
+ }
737
+ const runner = command.replace(/^&\s+/, '').trim().match(/^\(\(@\('(?:[^']|'')*'\) \+ @\(Get-Command node [\s\S]*?Select-Object -First 1\)\s+(.+?); exit \$\(if [\s\S]*\)$/);
738
+ if (!runner)
739
+ return null;
740
+ const arg = runner[1];
741
+ let m = arg.match(/^'((?:[^']|'')*)'$/);
742
+ if (m) {
743
+ const scriptPath = unquotePowerShell(m[1]);
744
+ return posixUnsafe.test(scriptPath) ? null : { scriptPath, scriptToken: JSON.stringify(scriptPath) };
745
+ }
746
+ m = arg.match(/^"\$env:CLAUDE_PROJECT_DIR(\/[^"]*)"$/) || arg.match(/^\(\$env:CLAUDE_PROJECT_DIR \+ '((?:[^']|'')*)'\)$/);
747
+ if (m) {
748
+ const rel = unquotePowerShell(m[1]);
749
+ if (posixUnsafe.test(rel))
750
+ return null;
751
+ const token = `"$CLAUDE_PROJECT_DIR"${rel}`;
752
+ return { scriptPath: token, scriptToken: token };
753
+ }
754
+ m = arg.match(/^"\$HOME(\/[^"]*)"$/) || arg.match(/^\(\$HOME \+ '((?:[^']|'')*)'\)$/);
755
+ if (m) {
756
+ const rel = unquotePowerShell(m[1]);
757
+ if (posixUnsafe.test(rel))
758
+ return null;
759
+ return { scriptPath: `$HOME${rel}`, scriptToken: `"$HOME${rel}"` };
760
+ }
761
+ return null;
762
+ }
763
+ // Recognizes buildPowerShellHookCommand's output once the `& ` call operator is stripped.
764
+ const POWERSHELL_RUNNER_COMMAND = /^\(\(@\('(?:[^']|'')*'\) \+ @\(Get-Command node [\s\S]*?Select-Object -First 1\)\s+\S/;
765
+ function isPowerShellRunnerCommand(command) {
766
+ return typeof command === 'string' && /^&\s+/.test(command) && POWERSHELL_RUNNER_COMMAND.test(command.replace(/^&\s+/, '').trim());
767
+ }
768
+ /**
769
+ * Set the explicit `shell: "powershell"` on every hook whose command is the
770
+ * PowerShell runner, so Claude Code does not depend on its own Git Bash
771
+ * detection to pick the shell. Returns true when a hook changed. Idempotent.
772
+ */
773
+ function projectPowerShellHookShell(settings) {
774
+ if (!settings || !settings.hooks)
775
+ return false;
776
+ let changed = false;
777
+ for (const entries of Object.values(settings.hooks)) {
778
+ if (!Array.isArray(entries))
779
+ continue;
780
+ for (const entry of entries) {
781
+ if (!entry || !Array.isArray(entry.hooks))
782
+ continue;
783
+ for (const h of entry.hooks) {
784
+ if (!h || !isPowerShellRunnerCommand(h.command) || !isManagedHookCommand(h.command, { surface: 'settings-json' }))
785
+ continue;
786
+ if (h.shell === 'powershell')
787
+ continue;
788
+ h.shell = 'powershell';
789
+ changed = true;
790
+ }
791
+ }
792
+ }
793
+ return changed;
794
+ }
618
795
  // #3662 — recognize the two runtime-resolving command shapes the installer
619
796
  // emits, so the rewriter never churns (or un-does) an entry that already
620
797
  // works in every environment sharing the config root.
@@ -626,8 +803,31 @@ function rewriteLegacyManagedNodeHookCommands(settings, runnerToken, opts) {
626
803
  if (!opts)
627
804
  opts = {};
628
805
  const platform = opts.platform || process.platform;
806
+ const usePowerShell = usesPowerShellHookShell({ platform, runtime: opts.runtime });
629
807
  let changed = false;
630
- for (const entries of Object.values(settings.hooks)) {
808
+ // #5850: the managed statusLine is a command like a hook. It is converted to the
809
+ // shell-neutral PowerShell spelling on the PowerShell path, and back to the POSIX
810
+ // chain when a PowerShell statusLine reaches a POSIX host (shared config root).
811
+ const statusLine = settings.statusLine;
812
+ const statusLineGroups = statusLine && typeof statusLine.command === 'string'
813
+ && (usePowerShell || POWERSHELL_STATUSLINE_COMMAND.test(statusLine.command))
814
+ ? [{ hooks: [statusLine] }]
815
+ : [];
816
+ const toPowerShell = (h, scriptPath) => (h === statusLine
817
+ ? buildPowerShellStatusLineCommand(scriptPath, opts)
818
+ : (() => {
819
+ const arg = powerShellScriptArg(scriptPath);
820
+ return arg === null ? null : buildPowerShellHookCommand(arg, opts);
821
+ })());
822
+ const assign = (h, command) => {
823
+ if (!command || h.command === command)
824
+ return;
825
+ h.command = command;
826
+ if (h.shell === 'powershell' && !usePowerShell)
827
+ delete h.shell;
828
+ changed = true;
829
+ };
830
+ for (const entries of [...Object.values(settings.hooks), statusLineGroups]) {
631
831
  if (!Array.isArray(entries))
632
832
  continue;
633
833
  for (const entry of entries) {
@@ -638,12 +838,53 @@ function rewriteLegacyManagedNodeHookCommands(settings, runnerToken, opts) {
638
838
  continue;
639
839
  if (Array.isArray(h.args) && h.args.length > 0)
640
840
  continue;
841
+ if (!usePowerShell && platform !== 'win32') {
842
+ // #5850: a PowerShell entry on a POSIX host (a config root shared with
843
+ // Windows) is a syntax error in bash; converge it back to the POSIX chain.
844
+ const ps = parsePowerShellScript(h.command);
845
+ if (ps) {
846
+ if (!isManagedHookBasename(ps.scriptPath, { surface: 'settings-json' }))
847
+ continue;
848
+ assign(h, projectLegacySettingsHookCommand({
849
+ runnerToken,
850
+ scriptPath: ps.scriptPath,
851
+ scriptToken: ps.scriptToken,
852
+ runtime: opts.runtime || 'generic',
853
+ platform,
854
+ }));
855
+ continue;
856
+ }
857
+ }
641
858
  let trimmed = h.command.trim();
642
859
  const hadPowerShellCallOperator = platform === 'win32' && /^&\s+/.test(trimmed);
643
860
  if (hadPowerShellCallOperator) {
644
861
  trimmed = trimmed.replace(/^&\s+/, '').trim();
645
862
  }
646
- if (CHAIN_RUNNER_COMMAND.test(trimmed) || RESOLVER_RUNNER_COMMAND.test(trimmed))
863
+ if (usePowerShell) {
864
+ // #5850: already the PowerShell runner: nothing to do. A POSIX chain
865
+ // (or a legacy bare/foreign-node entry) is re-projected below.
866
+ if (POWERSHELL_RUNNER_COMMAND.test(trimmed) || POWERSHELL_STATUSLINE_COMMAND.test(trimmed))
867
+ continue;
868
+ const chain = trimmed.match(/^"\$\(for n in [\s\S]*?printf '%s' "\$n" && break; done\)"\s+(?:"([^"]+)"|'([^']+)'|(\S+))\s*$/);
869
+ if (chain) {
870
+ const chainScript = chain[1] || chain[2] || chain[3] || '';
871
+ if (!isManagedHookBasename(chainScript, { surface: 'settings-json' }))
872
+ continue;
873
+ assign(h, toPowerShell(h, chainScript));
874
+ continue;
875
+ }
876
+ // The #3662 bash resolver shape needs bash too; its script is a plain
877
+ // or `$HOME`-relative path, both spelled for PowerShell.
878
+ const resolver = trimmed.match(/^(?:"[^"]*bash(?:\.exe)?"|bash)\s+"[^"]*gsd-node-runner\.sh"\s+"[^"]*"\s+(?:"([^"]+)"|(\S+))\s*$/);
879
+ if (resolver) {
880
+ const resolverScript = resolver[1] || resolver[2] || '';
881
+ if (!isManagedHookBasename(resolverScript, { surface: 'settings-json' }))
882
+ continue;
883
+ assign(h, toPowerShell(h, resolverScript));
884
+ continue;
885
+ }
886
+ }
887
+ if (CHAIN_RUNNER_COMMAND.test(trimmed) || RESOLVER_RUNNER_COMMAND.test(trimmed) || POWERSHELL_RUNNER_COMMAND.test(trimmed))
647
888
  continue;
648
889
  const m = trimmed.match(/^node\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/) ||
649
890
  trimmed.match(/^("([^"]+)"|'([^']+)'|(\S+))\s+("([^"]+)"|'([^']+)'|(\S+))\s*$/);
@@ -666,6 +907,10 @@ function rewriteLegacyManagedNodeHookCommands(settings, runnerToken, opts) {
666
907
  }
667
908
  if (!isManagedHookBasename(scriptPath, { surface: 'settings-json' }))
668
909
  continue;
910
+ if (usePowerShell) {
911
+ assign(h, toPowerShell(h, scriptPath));
912
+ continue;
913
+ }
669
914
  const projectedCommand = projectLegacySettingsHookCommand({
670
915
  runnerToken,
671
916
  scriptPath,
@@ -831,12 +1076,9 @@ function _installEngineSymlinkGuard() {
831
1076
  const mod = require('./install-engine.cjs');
832
1077
  return mod;
833
1078
  }
834
- function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
1079
+ // Shared by install preflight and the hooks writer; resolving never writes.
1080
+ function resolveCodexHooksJsonDestination(targetDir) {
835
1081
  const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
836
- const managedCommand = typeof opts.managedCommand === 'string' ? opts.managedCommand : null;
837
- const commandWindows = typeof opts.commandWindows === 'string' ? opts.commandWindows : null;
838
- const matcher = typeof opts.matcher === 'string' ? opts.matcher : undefined;
839
- const timeout = typeof opts.timeout === 'number' ? opts.timeout : undefined;
840
1082
  // #2586 Major 2: every Codex hooks.json writer funnels through this one
841
1083
  // function, and atomicWriteFileSync's final step is a rename(2) onto
842
1084
  // `hooksJsonPath` — which, when that path is a symlink, REPLACES the
@@ -868,6 +1110,15 @@ function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
868
1110
  // is supposed to mean.
869
1111
  effectiveHooksJsonPath = node_fs_1.default.realpathSync(hooksJsonPath);
870
1112
  }
1113
+ return effectiveHooksJsonPath;
1114
+ }
1115
+ function reconcileCodexHooksJsonEvent(targetDir, eventName, opts = {}) {
1116
+ const hooksJsonPath = node_path_1.default.join(targetDir, 'hooks.json');
1117
+ const managedCommand = typeof opts.managedCommand === 'string' ? opts.managedCommand : null;
1118
+ const commandWindows = typeof opts.commandWindows === 'string' ? opts.commandWindows : null;
1119
+ const matcher = typeof opts.matcher === 'string' ? opts.matcher : undefined;
1120
+ const timeout = typeof opts.timeout === 'number' ? opts.timeout : undefined;
1121
+ const effectiveHooksJsonPath = resolveCodexHooksJsonDestination(targetDir);
871
1122
  let parsed = {};
872
1123
  let currentContent = null;
873
1124
  if (node_fs_1.default.existsSync(effectiveHooksJsonPath)) {
@@ -1280,7 +1531,8 @@ function configuredEntrypointsForHook(configDir, hookName, opts) {
1280
1531
  '/usr/local/bin/node',
1281
1532
  '/usr/bin/node',
1282
1533
  ].filter((candidate) => Boolean(candidate));
1283
- if (!opts.portableHooks) {
1534
+ // #5850: the PowerShell runner needs no bash resolver, portable or not.
1535
+ if (!opts.portableHooks || usesPowerShellHookShell({ platform, runtime, hookShell: opts.hookShell })) {
1284
1536
  return [{ ...target, interpreterCandidates: nodeCandidates }];
1285
1537
  }
1286
1538
  const runner = {
@@ -1363,6 +1615,20 @@ function buildHookCommand(configDir, hookName, opts) {
1363
1615
  // baked as a bare absolute path — an install-environment absolute path is
1364
1616
  // exactly what breaks with exit 127 when the config root is shared across
1365
1617
  // environments with different node layouts.
1618
+ // #5850: Claude on win32 always gets the PowerShell runner (owner ruling),
1619
+ // portable installs included: the `$HOME`-relative script path is spelled for
1620
+ // PowerShell and needs no bash resolver.
1621
+ if (usesPowerShellHookShell({ platform, runtime, hookShell })) {
1622
+ const scriptPath = opts.portableHooks
1623
+ ? `${projectPortableHookBaseDir({ configDir, homeDir: node_os_1.default.homedir() })}/hooks/${hookName}`
1624
+ : shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName;
1625
+ // A statusLine is not shared across hosts and has no `$`-free `$HOME` spelling:
1626
+ // it always carries the real absolute path, portable or not.
1627
+ if (opts.statusLine) {
1628
+ return track(buildPowerShellStatusLineCommand(shellCmdProjection.posixNormalize(configDir) + '/hooks/' + hookName, opts));
1629
+ }
1630
+ return track(buildPowerShellHookCommand(powerShellScriptArg(scriptPath, { literal: true }), opts));
1631
+ }
1366
1632
  if (opts.portableHooks) {
1367
1633
  // Portable installs route through the staged resolver: the baked absolute
1368
1634
  // path travels as the resolver's FIRST argument (tried first, so the
@@ -2852,6 +3118,7 @@ module.exports = {
2852
3118
  writeCopilotHookConfig,
2853
3119
  GSD_COPILOT_HOOK_FILE,
2854
3120
  // Codex hooks.json
3121
+ resolveCodexHooksJsonDestination,
2855
3122
  reconcileCodexHooksJsonEvent,
2856
3123
  reconcileCodexHooksJsonSessionStart,
2857
3124
  ensureCodexHooksJsonSessionStart,
@@ -2892,6 +3159,10 @@ module.exports = {
2892
3159
  normalizeNodePath,
2893
3160
  resolveNodeRunner,
2894
3161
  buildNodeRunnerChainToken,
3162
+ usesPowerShellHookShell,
3163
+ buildPowerShellHookCommand,
3164
+ buildPowerShellLocalHookCommand,
3165
+ projectPowerShellHookShell,
2895
3166
  resolveBashExecutable,
2896
3167
  resolveBashRunner,
2897
3168
  NODE_RUNNER_RESOLVER_HOOK,
@@ -40,6 +40,9 @@ exports.runtimeCommand = runtimeCommand;
40
40
  const node_fs_1 = __importDefault(require("node:fs"));
41
41
  const node_path_1 = __importDefault(require("node:path"));
42
42
  const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
43
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- config-file-read.cjs is an export= CommonJS module
44
+ const configFileReadMod = require("./config-file-read.cjs");
45
+ const { readConfigFileBounded } = configFileReadMod;
43
46
  function formatGsdSlash(commandName, runtime) {
44
47
  if (typeof commandName !== 'string')
45
48
  return commandName;
@@ -171,9 +174,11 @@ function readProjectConfig(projectDir) {
171
174
  return null;
172
175
  try {
173
176
  const configPath = node_path_1.default.join(projectDir, '.planning', 'config.json');
174
- if (!node_fs_1.default.existsSync(configPath))
177
+ // Bounded read (#5759): null is an absent file; a FIFO/device/oversized one throws and degrades to null.
178
+ const raw = readConfigFileBounded(configPath);
179
+ if (raw === null)
175
180
  return null;
176
- const parsed = JSON.parse(node_fs_1.default.readFileSync(configPath, 'utf-8'));
181
+ const parsed = JSON.parse(raw);
177
182
  return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
178
183
  }
179
184
  catch {