@opengsd/gsd-core 1.17.0 → 1.18.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 (176) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-advisor-researcher.compact.md +1 -1
  4. package/agents/gsd-advisor-researcher.md +1 -1
  5. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.md +1 -1
  7. package/agents/gsd-code-fixer.compact.md +1 -1
  8. package/agents/gsd-code-fixer.md +1 -1
  9. package/agents/gsd-code-reviewer.compact.md +1 -1
  10. package/agents/gsd-code-reviewer.md +1 -1
  11. package/agents/gsd-codebase-mapper.compact.md +1 -1
  12. package/agents/gsd-codebase-mapper.md +1 -1
  13. package/agents/gsd-debugger.md +1 -1
  14. package/agents/gsd-doc-verifier.compact.md +4 -4
  15. package/agents/gsd-doc-verifier.md +4 -3
  16. package/agents/gsd-doc-writer.compact.md +23 -5
  17. package/agents/gsd-doc-writer.md +19 -6
  18. package/agents/gsd-eval-auditor.compact.md +1 -1
  19. package/agents/gsd-eval-auditor.md +1 -1
  20. package/agents/gsd-executor.md +7 -3
  21. package/agents/gsd-integration-checker.compact.md +1 -1
  22. package/agents/gsd-integration-checker.md +1 -1
  23. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  24. package/agents/gsd-nyquist-auditor.md +1 -1
  25. package/agents/gsd-phase-researcher.md +1 -1
  26. package/agents/gsd-plan-checker.md +2 -1
  27. package/agents/gsd-planner.md +5 -4
  28. package/agents/gsd-project-researcher.compact.md +1 -1
  29. package/agents/gsd-project-researcher.md +1 -1
  30. package/agents/gsd-research-synthesizer.compact.md +5 -5
  31. package/agents/gsd-research-synthesizer.md +5 -5
  32. package/agents/gsd-roadmapper.compact.md +1 -1
  33. package/agents/gsd-roadmapper.md +1 -1
  34. package/agents/gsd-security-auditor.compact.md +1 -1
  35. package/agents/gsd-security-auditor.md +1 -1
  36. package/agents/gsd-ui-auditor.compact.md +1 -1
  37. package/agents/gsd-ui-auditor.md +1 -1
  38. package/agents/gsd-ui-checker.compact.md +1 -1
  39. package/agents/gsd-ui-checker.md +1 -1
  40. package/agents/gsd-ui-researcher.compact.md +13 -1
  41. package/agents/gsd-ui-researcher.md +28 -1
  42. package/agents/gsd-verifier.md +1 -1
  43. package/bin/install.js +38 -2
  44. package/commands/gsd/code-review.md +8 -0
  45. package/commands/gsd/pause-work.md +1 -1
  46. package/gsd-core/bin/gsd-tools.cjs +18 -11
  47. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +29 -7
  49. package/gsd-core/bin/lib/capability-registry.cjs +70 -64
  50. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  51. package/gsd-core/bin/lib/capability-state.cjs +6 -3
  52. package/gsd-core/bin/lib/capability-validator.cjs +6 -4
  53. package/gsd-core/bin/lib/commands.cjs +5 -1
  54. package/gsd-core/bin/lib/config-loader.cjs +3 -3
  55. package/gsd-core/bin/lib/decision-coverage-support.cjs +19 -0
  56. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +15 -0
  57. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +13 -0
  58. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  59. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  60. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +60 -5
  61. package/gsd-core/bin/lib/init.cjs +11 -4
  62. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  63. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  64. package/gsd-core/bin/lib/install-profiles.cjs +5 -3
  65. package/gsd-core/bin/lib/phase.cjs +120 -11
  66. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  67. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  68. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  69. package/gsd-core/bin/lib/roadmap.cjs +56 -5
  70. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +47 -2
  71. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  72. package/gsd-core/bin/lib/shell-command-projection.cjs +2 -0
  73. package/gsd-core/bin/lib/smart-entry.cjs +24 -16
  74. package/gsd-core/bin/lib/state-document.cjs +37 -3
  75. package/gsd-core/bin/lib/state-transition.cjs +31 -10
  76. package/gsd-core/bin/lib/state.cjs +90 -60
  77. package/gsd-core/bin/lib/task-command-router.cjs +2 -3
  78. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  79. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  80. package/gsd-core/bin/lib/verify.cjs +36 -1
  81. package/gsd-core/references/agent-contracts.md +1 -1
  82. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  83. package/gsd-core/references/git-integration.md +3 -3
  84. package/gsd-core/references/planner-reversibility.md +18 -4
  85. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  86. package/gsd-core/references/tdd.md +4 -3
  87. package/gsd-core/references/verifier-phase-gates.md +8 -1
  88. package/gsd-core/workflows/add-backlog.md +10 -2
  89. package/gsd-core/workflows/add-phase.md +5 -1
  90. package/gsd-core/workflows/add-tests.md +5 -1
  91. package/gsd-core/workflows/add-todo.md +5 -1
  92. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  93. package/gsd-core/workflows/audit-milestone.md +21 -9
  94. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  95. package/gsd-core/workflows/autonomous.md +52 -64
  96. package/gsd-core/workflows/check-todos.md +5 -1
  97. package/gsd-core/workflows/code-review-fix.md +5 -1
  98. package/gsd-core/workflows/code-review.md +5 -1
  99. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  100. package/gsd-core/workflows/complete-milestone.md +25 -5
  101. package/gsd-core/workflows/debug.md +5 -1
  102. package/gsd-core/workflows/diagnose-issues.md +5 -1
  103. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  104. package/gsd-core/workflows/discuss-phase-assumptions.md +11 -3
  105. package/gsd-core/workflows/discuss-phase.md +5 -1
  106. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  107. package/gsd-core/workflows/docs-update.md +27 -5
  108. package/gsd-core/workflows/edit-phase.md +5 -1
  109. package/gsd-core/workflows/eval-review.md +11 -3
  110. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  111. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +12 -6
  112. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +19 -2
  113. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  114. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +5 -1
  115. package/gsd-core/workflows/execute-phase.md +37 -6
  116. package/gsd-core/workflows/execute-plan.md +8 -4
  117. package/gsd-core/workflows/extract-learnings.md +6 -2
  118. package/gsd-core/workflows/forensics.md +5 -1
  119. package/gsd-core/workflows/import.md +5 -1
  120. package/gsd-core/workflows/insert-phase.md +5 -1
  121. package/gsd-core/workflows/manager.md +10 -2
  122. package/gsd-core/workflows/map-codebase.md +5 -1
  123. package/gsd-core/workflows/milestone-summary.md +10 -2
  124. package/gsd-core/workflows/new-milestone.md +13 -5
  125. package/gsd-core/workflows/new-project.md +9 -5
  126. package/gsd-core/workflows/next.md +8 -4
  127. package/gsd-core/workflows/pause-work.md +3 -3
  128. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  129. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +5 -1
  130. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  131. package/gsd-core/workflows/plan-phase.md +41 -73
  132. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  133. package/gsd-core/workflows/plant-seed.md +8 -4
  134. package/gsd-core/workflows/progress.md +6 -2
  135. package/gsd-core/workflows/quick-batch.md +5 -1
  136. package/gsd-core/workflows/quick.md +22 -7
  137. package/gsd-core/workflows/remove-phase.md +5 -1
  138. package/gsd-core/workflows/resume-project.md +5 -1
  139. package/gsd-core/workflows/review.md +6 -2
  140. package/gsd-core/workflows/scan.md +5 -1
  141. package/gsd-core/workflows/secure-phase.md +6 -2
  142. package/gsd-core/workflows/settings.md +3 -3
  143. package/gsd-core/workflows/ship.md +13 -10
  144. package/gsd-core/workflows/thread.md +10 -2
  145. package/gsd-core/workflows/transition.md +5 -1
  146. package/gsd-core/workflows/ui-phase.md +5 -1
  147. package/gsd-core/workflows/ui-review.md +6 -2
  148. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  149. package/gsd-core/workflows/update.md +5 -1
  150. package/gsd-core/workflows/validate-phase.md +6 -2
  151. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  152. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  153. package/gsd-core/workflows/verify-work.md +11 -3
  154. package/hooks/dist/gsd-agent-isolation-guard.js +3 -1
  155. package/hooks/dist/gsd-cursor-subagent-start.js +2 -1
  156. package/hooks/dist/gsd-secret-read-guard.js +72 -14
  157. package/hooks/dist/gsd-validate-commit.sh +27 -5
  158. package/hooks/gsd-agent-isolation-guard.js +3 -1
  159. package/hooks/gsd-cursor-subagent-start.js +2 -1
  160. package/hooks/gsd-secret-read-guard.js +72 -14
  161. package/hooks/gsd-validate-commit.sh +27 -5
  162. package/package.json +1 -1
  163. package/scripts/audit-workflow-script-paths.cjs +1 -1
  164. package/scripts/diagnose-validate-commit-in-tier.cjs +270 -0
  165. package/scripts/diagnose-validate-commit-phases.cjs +348 -0
  166. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  167. package/scripts/docs-guard-registry.cjs +2 -0
  168. package/scripts/lib/macos-conformance-tier.generated.cjs +9 -0
  169. package/scripts/lib/platform-conformance-tier.generated.cjs +16 -0
  170. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  171. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  172. package/scripts/release-tarball-smoke.cjs +1 -0
  173. package/scripts/xtrace-phase-timing.cjs +205 -0
  174. package/skills/gsd-code-review/SKILL.md +8 -0
  175. package/skills/gsd-pause-work/SKILL.md +1 -1
  176. package/vscode/package.json +1 -1
@@ -148,7 +148,11 @@ If $ARGUMENTS is a new description (no matching thread file):
148
148
 
149
149
  1. Generate slug from description:
150
150
  ```bash
151
- SLUG=$(gsd_run query generate-slug "$ARGUMENTS" --raw)
151
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
152
+ $ARGUMENTS
153
+ GSD_ARGS_END
154
+ GSD_ARGS=${GSD_ARGS%$'\n'}
155
+ SLUG=$(gsd_run query generate-slug "$GSD_ARGS" --raw)
152
156
  ```
153
157
 
154
158
  2. Create the threads directory if needed:
@@ -192,7 +196,11 @@ updated: {today ISO date}
192
196
 
193
197
  5. Commit:
194
198
  ```bash
195
- gsd_run query commit "docs: create thread — ${ARGUMENTS}" --files ".planning/threads/${SLUG}.md"
199
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
200
+ $ARGUMENTS
201
+ GSD_ARGS_END
202
+ GSD_ARGS=${GSD_ARGS%$'\n'}
203
+ gsd_run query commit "docs: create thread — ${GSD_ARGS}" --files ".planning/threads/${SLUG}.md"
196
204
  ```
197
205
 
198
206
  6. Report:
@@ -446,7 +446,11 @@ This returns all phases with goals, disk status, and completion info.
446
446
  **Section-manifest gate (#2994):** `gsd_run` is already established above (`verify_completion` step) — fetch the dedicated `init.transition` bundle for the workstream-collision-check gate below:
447
447
 
448
448
  ```bash
449
- GSD_WS=$(echo " $ARGUMENTS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
449
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
450
+ $ARGUMENTS
451
+ GSD_ARGS_END
452
+ GSD_ARGS=${GSD_ARGS%$'\n'}
453
+ GSD_WS=$(echo " $GSD_ARGS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
450
454
  INIT_TRANSITION=$(gsd_run query init.transition ${GSD_WS:+--ws=${GSD_WS##* }})
451
455
  if [[ "$INIT_TRANSITION" == @file:* ]]; then INIT_TRANSITION=$(cat "${INIT_TRANSITION#@file:}"); fi
452
456
  ```
@@ -19,8 +19,12 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
19
19
  ## 1. Initialize
20
20
 
21
21
  ```bash
22
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
23
+ $ARGUMENTS
24
+ GSD_ARGS_END
25
+ GSD_ARGS=${GSD_ARGS%$'\n'}
22
26
  _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
23
- GSD_WS=$(echo " $ARGUMENTS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
27
+ GSD_WS=$(echo " $GSD_ARGS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
24
28
  INIT=$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE")
25
29
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
26
30
  AGENT_SKILLS_UI=$(gsd_run query agent-skills gsd-ui-researcher ${GSD_WS:+--ws=${GSD_WS##* }})
@@ -16,10 +16,14 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
16
16
  ## 0. Initialize
17
17
 
18
18
  ```bash
19
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
20
+ $ARGUMENTS
21
+ GSD_ARGS_END
22
+ GSD_ARGS=${GSD_ARGS%$'\n'}
19
23
  _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
20
24
  RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "")
21
- PHASE_ARG=$(echo "$ARGUMENTS" | sed -E 's/--ws[[:space:]]+[A-Za-z0-9._-]+//g' | xargs)
22
- GSD_WS=$(echo " $ARGUMENTS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
25
+ PHASE_ARG=$(echo "$GSD_ARGS" | sed -E 's/--ws[[:space:]]+[A-Za-z0-9._-]+//g' | xargs)
26
+ GSD_WS=$(echo " $GSD_ARGS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
23
27
  INIT=$(gsd_run query init.phase-op ${GSD_WS:+--ws=${GSD_WS##* }} "${PHASE_ARG}")
24
28
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
25
29
  AGENT_SKILLS_UI_REVIEWER=$(gsd_run query agent-skills gsd-ui-auditor ${GSD_WS:+--ws=${GSD_WS##* }})
@@ -64,8 +64,12 @@ unplanned phase from the roadmap (same logic as /gsd:plan-phase).
64
64
  Load GSD phase context:
65
65
 
66
66
  ```bash
67
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
68
+ $ARGUMENTS
69
+ GSD_ARGS_END
70
+ GSD_ARGS=${GSD_ARGS%$'\n'}
67
71
  _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
68
- GSD_WS=$(echo " $ARGUMENTS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
72
+ GSD_WS=$(echo " $GSD_ARGS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
69
73
  INIT=$(gsd_run query init.plan-phase ${GSD_WS:+--ws=${GSD_WS##* }} "$PHASE")
70
74
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
71
75
  ```
@@ -99,7 +99,11 @@ Exit.
99
99
  Determine the release channel from `$ARGUMENTS`. This selects which npm dist-tag the entire update flow targets — `latest` (stable) by default, or `next` (the RC channel established by ADR #660) when the user opts in with `--next`/`--rc`:
100
100
 
101
101
  ```bash
102
- case " $ARGUMENTS " in
102
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
103
+ $ARGUMENTS
104
+ GSD_ARGS_END
105
+ GSD_ARGS=${GSD_ARGS%$'\n'}
106
+ case " $GSD_ARGS " in
103
107
  *" --next "*|*" --rc "*)
104
108
  TAG="next"
105
109
  CHANNEL_LABEL="next (RC)"
@@ -16,10 +16,14 @@ Valid GSD subagent types (use exact names — do not fall back to 'general-purpo
16
16
  ## 0. Initialize
17
17
 
18
18
  ```bash
19
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
20
+ $ARGUMENTS
21
+ GSD_ARGS_END
22
+ GSD_ARGS=${GSD_ARGS%$'\n'}
19
23
  _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
20
24
  RESPONSE_LANGUAGE=$(gsd_run query config-get response_language --raw --default "" 2>/dev/null || echo "")
21
- PHASE_ARG=$(echo "$ARGUMENTS" | sed -E 's/--ws[[:space:]]+[A-Za-z0-9._-]+//g' | xargs)
22
- GSD_WS=$(echo " $ARGUMENTS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
25
+ PHASE_ARG=$(echo "$GSD_ARGS" | sed -E 's/--ws[[:space:]]+[A-Za-z0-9._-]+//g' | xargs)
26
+ GSD_WS=$(echo " $GSD_ARGS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
23
27
  INIT=$(gsd_run query init.phase-op ${GSD_WS:+--ws=${GSD_WS##* }} "${PHASE_ARG}")
24
28
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
25
29
  AGENT_SKILLS_AUDITOR=$(gsd_run query agent-skills gsd-nyquist-auditor ${GSD_WS:+--ws=${GSD_WS##* }})
@@ -267,4 +267,4 @@ alongside `pending_count`/`blocked_count`/`skipped_no_reason`, and its issues-ro
267
267
  (`diagnose_issues` vs. proceeding toward promotion) reads `unresolved_issues`, not the raw
268
268
  `issues` count from `## Summary`. The raw `issues` count is unchanged and still drives the
269
269
  presented summary numbers.
270
- ```
270
+ ```
@@ -8,9 +8,13 @@
8
8
  **User-story format guard.** When `MVP_MODE=true`, also verify the phase's goal is in User Story format via the centralized validator:
9
9
 
10
10
  ```bash
11
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
12
+ $ARGUMENTS
13
+ GSD_ARGS_END
14
+ GSD_ARGS=${GSD_ARGS%$'\n'}
11
15
  _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
12
16
  # #4772: this file is Read, not textually substituted - write the parent command's original argument string in place of $ARGUMENTS so --ws reaches the query.
13
- GSD_WS=$(echo " $ARGUMENTS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
17
+ GSD_WS=$(echo " $GSD_ARGS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
14
18
  PHASE_GOAL=$(gsd_run query roadmap.get-phase "${phase_number}" ${GSD_WS:+--ws=${GSD_WS##* }} --pick goal)
15
19
  USER_STORY_VALID=$(gsd_run query user-story.validate --story "$PHASE_GOAL" --pick valid)
16
20
  if [ "$USER_STORY_VALID" != "true" ]; then
@@ -39,10 +39,14 @@ No Pass/Fail buttons. No severity questions. Just: "Here's what should happen. D
39
39
  If $ARGUMENTS contains a phase number, load context:
40
40
 
41
41
  ```bash
42
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
43
+ $ARGUMENTS
44
+ GSD_ARGS_END
45
+ GSD_ARGS=${GSD_ARGS%$'\n'}
42
46
  _GSD_SHIM_NAME="gsd-tools.cjs"; _GSD_RUNTIME_ROOT="${RUNTIME_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"; GSD_TOOLS="${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}"; _gsd_at() { for _p; do if [ -f "$_p" ]; then GSD_TOOLS="$_p"; return 0; fi; done; return 1; }; _gsd_id_ok() { case "$("$1" runtime-identity --raw 2>/dev/null || true)" in '{"packageName":"@opengsd/gsd-core"'*'}') return 0;; *) return 1;; esac; }; _gsd_homes() { set -- "${CLAUDE_CONFIG_DIR:-$HOME/.claude}" "${ANTIGRAVITY_CONFIG_DIR:-$HOME/.gemini/antigravity}" "$HOME/.gemini/antigravity-ide" "$HOME/.gemini/antigravity-cli" "${AUGMENT_CONFIG_DIR:-$HOME/.augment}" "${CLINE_CONFIG_DIR:-$HOME/.cline}" "${CODEBUDDY_CONFIG_DIR:-$HOME/.codebuddy}" "${CODEX_HOME:-$HOME/.codex}" "${COPILOT_CONFIG_DIR:-${COPILOT_HOME:-$HOME/.copilot}}" "${CURSOR_CONFIG_DIR:-$HOME/.cursor}" "${HERMES_HOME:-$HOME/.hermes}" "${KILO_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/kilo}" "${KIMI_CONFIG_DIR:-$HOME/.config/agents}" "$HOME/.agents" "${KIMI_CODE_HOME:-$HOME/.kimi-code}" "${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}" "${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}" "${QWEN_CONFIG_DIR:-$HOME/.qwen}" "${TRAE_CONFIG_DIR:-$HOME/.trae}" "${WINDSURF_CONFIG_DIR:-$HOME/.codeium/windsurf}" "${ZCODE_CONFIG_DIR:-$HOME/.zcode}" "${GROK_AGENTS_HOME:-$HOME/.agents}"; for _h; do _gsd_at "$_h/gsd-core/bin/${_GSD_SHIM_NAME}" && return 0; done; return 1; }; if _gsd_at "${_GSD_RUNTIME_ROOT}/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.claude/gsd-core/bin/${_GSD_SHIM_NAME}" "${_GSD_RUNTIME_ROOT}/.codex/gsd-core/bin/${_GSD_SHIM_NAME}"; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif _gsd_homes; then gsd_run() { node "$GSD_TOOLS" "$@"; }; elif unset -f gsd_run; _G="$(command -v gsd_run)"; [ -n "$_G" ] && _gsd_id_ok "$_G"; then GSD_TOOLS="$_G"; gsd_run() { "$GSD_TOOLS" "$@"; }; else echo "ERROR: gsd-tools.cjs not found at $GSD_TOOLS and no identity-proving gsd_run is on PATH. Run: npx -y @opengsd/gsd-core@latest --claude --local" >&2; exit 1; fi; GSD_IDENTITY_STATUS=unverified; _gsd_id_ok gsd_run && GSD_IDENTITY_STATUS=ok; export GSD_IDENTITY_STATUS; [ "$GSD_IDENTITY_STATUS" = ok ] || echo "WARNING: \"$GSD_TOOLS\" did not prove it is @opengsd/gsd-core - it is either a different package or an @opengsd/gsd-core older than the runtime-identity verb. See docs/how-to/diagnose-a-foreign-gsd-tools.md" >&2; if [ -n "${CLAUDE_ENV_FILE:-}" ] && [ -n "${GSD_TOOLS:-}" ]; then printf "export PATH='%s':\"\$PATH\"\n" "${GSD_TOOLS%/*}" >> "$CLAUDE_ENV_FILE" 2>/dev/null || true; fi
43
47
  GSD_WS=""
44
- echo "$ARGUMENTS" | grep -qE -- '--ws[[:space:]]+[A-Za-z0-9._-]+' && GSD_WS=$(echo "$ARGUMENTS" | grep -oE -- '--ws[[:space:]]+[A-Za-z0-9._-]+')
45
- PHASE_ARG=$(echo "$ARGUMENTS" | sed -E 's/--ws[[:space:]]+[A-Za-z0-9._-]+//g' | xargs)
48
+ echo "$GSD_ARGS" | grep -qE -- '--ws[[:space:]]+[A-Za-z0-9._-]+' && GSD_WS=$(echo "$GSD_ARGS" | grep -oE -- '--ws[[:space:]]+[A-Za-z0-9._-]+')
49
+ PHASE_ARG=$(echo "$GSD_ARGS" | sed -E 's/--ws[[:space:]]+[A-Za-z0-9._-]+//g' | xargs)
46
50
 
47
51
  INIT=$(gsd_run query init.verify-work "${PHASE_ARG}" ${GSD_WS:+--ws=${GSD_WS##* }})
48
52
  if [[ "$INIT" == @file:* ]]; then INIT=$(cat "${INIT#@file:}"); fi
@@ -678,7 +682,11 @@ itself (its only write is the canonicalization, #4663).
678
682
  Load the step's inputs through the SAME bundle execute-phase loads, then include the step:
679
683
 
680
684
  ```bash
681
- GSD_WS=$(echo " $ARGUMENTS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
685
+ IFS= read -rd '' GSD_ARGS <<'GSD_ARGS_END' || true
686
+ $ARGUMENTS
687
+ GSD_ARGS_END
688
+ GSD_ARGS=${GSD_ARGS%$'\n'}
689
+ GSD_WS=$(echo " $GSD_ARGS" | sed -nE 's/.* --ws +([A-Za-z0-9][A-Za-z0-9._-]*).*/--ws \1/p' | head -n 1)
682
690
  EXECUTE_INIT=$(gsd_run query init.execute-phase "{phase}" ${GSD_WS:+--ws=${GSD_WS##* }})
683
691
  if [[ "$EXECUTE_INIT" == @file:* ]]; then EXECUTE_INIT=$(cat "${EXECUTE_INIT#@file:}"); fi
684
692
  for _k in phase_dir phase_number verifier_model phase_req_ids requirements_path section_manifest response_language; do
@@ -227,7 +227,9 @@ function resolveRuntimeIdentity(cwd, configPath, resolveRuntimeNameFromCandidate
227
227
  // never a resolution failure (this function only ever throws for the
228
228
  // config.json read above, which the caller's catch already handles).
229
229
  try {
230
- const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json');
230
+ // #5017: the store root is GSD_HOME when set and non-blank, else the home
231
+ // directory — the config loader's rule (src/runtime-homes.cts resolveGsdHome).
232
+ const defaultsPath = path.join(process.env.GSD_HOME || os.homedir(), '.gsd', 'defaults.json');
231
233
  const defaultsRaw = fs.readFileSync(defaultsPath, 'utf-8');
232
234
  const defaultsParsed = JSON.parse(defaultsRaw);
233
235
  if (defaultsParsed && typeof defaultsParsed === 'object' && 'runtime' in defaultsParsed) {
@@ -435,7 +435,8 @@ function resolveFallbackIsolation(root, configPath) {
435
435
  }
436
436
  if (!runtimeId) {
437
437
  try {
438
- const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json');
438
+ // #5017: GSD_HOME when set and non-blank, else the home directory — the loader's rule.
439
+ const defaultsPath = path.join(process.env.GSD_HOME || os.homedir(), '.gsd', 'defaults.json');
439
440
  const defaultsParsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf-8'));
440
441
  if (defaultsParsed && typeof defaultsParsed === 'object' && 'runtime' in defaultsParsed) {
441
442
  runtimeId = resolveRuntimeNameFromCandidates(defaultsParsed.runtime) || null;
@@ -67,6 +67,8 @@
67
67
  // `git check-ignore` / `git ls-files` / `git rm --cached`, and the
68
68
  // destination of a one-source `cp`/`mv` (#4856). The #4856 carve-outs
69
69
  // fail closed: an option they do not list withdraws them from the segment.
70
+ // A git global option the walker does not know also keeps the #5045 `-c`
71
+ // scan running over every later word (see walkGitGlobals).
70
72
  // A shell interpreter (bash/sh/zsh/dash/ksh/su) has its script scanned
71
73
  // whether it arrives via `-c '…'`, a `<( )` file operand, a heredoc /
72
74
  // here-string, or a pipe from a knowable `echo`/`printf` source
@@ -192,14 +194,29 @@ const GIT_PATHSPEC_SUBCOMMANDS = new Map([
192
194
  }],
193
195
  ]);
194
196
 
195
- // git's global options (its own usage line, git 2.49), needed to locate the
196
- // subcommand. A value option consumes the next word unless written
197
- // `--opt=value`; an option in neither set fails closed, so a value is never
198
- // read as the subcommand (`git -C ls-files show HEAD:.env` runs `show`).
199
- const GIT_GLOBAL_VALUE_OPTIONS = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env']);
197
+ // git's global options (git.c handle_options, git 2.49) — the ONE vocabulary
198
+ // both the #4856 pathspec exemption and the #5045 -c scan walk, via
199
+ // walkGitGlobals. A value option consumes the next word; the long options in
200
+ // GIT_GLOBAL_EQUALS_FORM_OPTIONS also take it attached as `--opt=value`.
201
+ // `-C`, `-c` and the hidden `--shallow-file` accept only the separate word
202
+ // (git rejects `--shallow-file=x` and `-c<name>=<value>`), and
203
+ // `--exec-path` runs a subcommand only as `--exec-path=<path>` (bare, it
204
+ // prints the path and exits). Any other option fails closed for both
205
+ // consumers: its arity is unknown, so its value could be read as the
206
+ // subcommand (`git -C ls-files show HEAD:.env` runs `show`). The pathspec
207
+ // exemption is withdrawn, and the -c scan keeps collecting every later
208
+ // `-c <value>` pair instead of stopping.
209
+ const GIT_GLOBAL_VALUE_OPTIONS = new Set([
210
+ '-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source',
211
+ '--shallow-file',
212
+ ]);
213
+ const GIT_GLOBAL_EQUALS_FORM_OPTIONS = new Set([
214
+ '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--exec-path',
215
+ ]);
200
216
  const GIT_GLOBAL_FLAGS = new Set([
201
217
  '-p', '--paginate', '-P', '--no-pager', '--no-replace-objects', '--no-lazy-fetch',
202
218
  '--no-optional-locks', '--no-advice', '--bare',
219
+ '--literal-pathspecs', '--glob-pathspecs', '--noglob-pathspecs', '--icase-pathspecs',
203
220
  ]);
204
221
 
205
222
  // #4856: `cp`/`mv` write their destination and never print it, so a secret
@@ -789,6 +806,29 @@ function resolveCommand(words) {
789
806
  return { base: lastSegment(words[idx].text).toLowerCase(), operands: words.slice(idx + 1) };
790
807
  }
791
808
 
809
+ /**
810
+ * Rescan only the command-valued -c keys selected by #5045. This deliberately
811
+ * does not resolve config files, --config-env/GIT_CONFIG_* values, or other
812
+ * executing keys (diff drivers, filters, credential helpers, ssh, gpg).
813
+ * Those are documented gaps, not a promise of complete Git config analysis.
814
+ * Global option values and subcommand arguments are data, not more -c flags.
815
+ */
816
+ function scanGitConfig(operands, depth) {
817
+ for (const config of walkGitGlobals(operands).configs) {
818
+ const eq = config.indexOf('=');
819
+ if (eq === -1) continue;
820
+ const key = config.slice(0, eq).toLowerCase();
821
+ const value = config.slice(eq + 1);
822
+ let script;
823
+ if (key.startsWith('alias.') && value.startsWith('!')) script = value.slice(1);
824
+ else if (key === 'diff.external') script = value;
825
+ else continue;
826
+ const hit = findSecretRead(script, depth + 1);
827
+ if (hit) return hit;
828
+ }
829
+ return null;
830
+ }
831
+
792
832
  // Operand indices the segment's command consumes as a NAME, never as
793
833
  // CONTENTS (#4639, #4856). Only these positions skip the operand check;
794
834
  // every other operand is still checked, so a carve-out cannot launder a read
@@ -820,26 +860,38 @@ function isListedFlag(text, flags) {
820
860
  return /^-[A-Za-z]+$/.test(text) && [...text.slice(1)].every((ch) => flags.has(`-${ch}`));
821
861
  }
822
862
 
823
- // Index of git's subcommand past its global options, or -1 when an option
824
- // outside GIT_GLOBAL_VALUE_OPTIONS / GIT_GLOBAL_FLAGS precedes it.
825
- function gitSubcommandIndex(operands) {
863
+ // Walks git's global options (see GIT_GLOBAL_VALUE_OPTIONS). `sub` is the
864
+ // subcommand's index, or -1 when an unlisted option (or the end of the words)
865
+ // comes first; `configs` holds every `-c` value seen on the way, in order.
866
+ // Past an unlisted option the subcommand cannot be located, so `configs`
867
+ // conservatively takes the word after every later `-c` as well: a read
868
+ // still needs a command-valued key, so a harmless value stays allowed.
869
+ function walkGitGlobals(operands) {
870
+ const configs = [];
826
871
  for (let k = 0; k < operands.length; k++) {
827
872
  const t = operands[k].text;
828
- if (!t.startsWith('-')) return k;
829
- if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) { k++; continue; }
873
+ if (!t.startsWith('-')) return { sub: k, configs };
874
+ if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) {
875
+ if (t === '-c' && k + 1 < operands.length) configs.push(operands[k + 1].text);
876
+ k++;
877
+ continue;
878
+ }
830
879
  if (GIT_GLOBAL_FLAGS.has(t)) continue;
831
880
  const eq = t.indexOf('=');
832
- const isLongValueForm = t.startsWith('--') && eq !== -1 && GIT_GLOBAL_VALUE_OPTIONS.has(t.slice(0, eq));
833
- if (!isLongValueForm) return -1;
881
+ if (t.startsWith('--') && eq !== -1 && GIT_GLOBAL_EQUALS_FORM_OPTIONS.has(t.slice(0, eq))) continue;
882
+ for (let j = k + 1; j + 1 < operands.length; j++) {
883
+ if (operands[j].text === '-c') configs.push(operands[++j].text);
884
+ }
885
+ return { sub: -1, configs };
834
886
  }
835
- return -1;
887
+ return { sub: -1, configs };
836
888
  }
837
889
 
838
890
  // #4856: the pathspec operands of a GIT_PATHSPEC_SUBCOMMANDS invocation. git
839
891
  // permutes options, so one may follow a pathspec; after `--` every word is a
840
892
  // pathspec. The subcommand is matched case-sensitively, as git looks it up.
841
893
  function gitPathspecIndices(operands) {
842
- const sub = gitSubcommandIndex(operands);
894
+ const { sub } = walkGitGlobals(operands);
843
895
  const spec = sub === -1 ? undefined : GIT_PATHSPEC_SUBCOMMANDS.get(operands[sub].text);
844
896
  if (!spec) return new Set();
845
897
  const pathspecs = new Set();
@@ -1007,6 +1059,12 @@ function findSecretRead(command, depth) {
1007
1059
  // `bash .env` (file mode) is caught by the operand check below.
1008
1060
  }
1009
1061
 
1062
+ // Git -c can carry a shell script instead of a file-name operand (#5045).
1063
+ if ((base === 'git' || base === 'git.exe') && depth < MAX_NESTING_DEPTH) {
1064
+ const hit = scanGitConfig(operands, depth);
1065
+ if (hit) return hit;
1066
+ }
1067
+
1010
1068
  if (NON_READING_COMMANDS.has(base)) continue;
1011
1069
 
1012
1070
  const nameOnly = nameOnlyOperandIndices(base, operands);
@@ -39,6 +39,27 @@ cleanup_temp_files() {
39
39
  }
40
40
  trap cleanup_temp_files EXIT
41
41
 
42
+ # The first line of $1 (everything before the first newline), matched BYTE-wise.
43
+ #
44
+ # `${1%%$'\n'*}` is a longest-suffix glob match, and on bash 3.2 (macOS's
45
+ # /bin/bash) in a multibyte locale such as en_US.UTF-8 it costs ~3.5 s on a
46
+ # 112KB value (#5473, measured inside the macOS conformance job: the same hook
47
+ # on the same 114750-byte command took ~5.6 s under LC_ALL=en_US.UTF-8 and
48
+ # ~0.55 s under LC_ALL=C, and a trace put 3.5 s of it on this expansion). A
49
+ # newline is the same single byte in every locale, so matching it byte-wise
50
+ # changes no result.
51
+ #
52
+ # ONLY call this inside a command substitution, `SUBJECT=$(gsd_first_line "$MSG")`:
53
+ # the substitution's subshell is what keeps LC_ALL=C from leaking into the rest
54
+ # of the hook, whose regexes and length arithmetic keep the caller's locale. A
55
+ # substitution is a fork with no pipe between two processes, so it keeps the
56
+ # property the pure-expansion form was chosen for (no `head -1` SIGPIPE race
57
+ # under `set -euo pipefail`, #4429); printf is a builtin, so no exec either.
58
+ gsd_first_line() {
59
+ LC_ALL=C
60
+ printf '%s' "${1%%$'\n'*}"
61
+ }
62
+
42
63
  # The 10 built-in Conventional Commits types — the SINGLE declaration (#3811
43
64
  # review finding: this was previously hand-typed a second time inside the
44
65
  # node -e script below, a generative-fix-divergence risk per CLAUDE.md's
@@ -575,18 +596,19 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
575
596
  SUBJECT=$(GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" MSG="$MSG" node -e "
576
597
  const {resolveCommitSubject}=require(process.env.GIT_CMD_LIB);
577
598
  process.stdout.write(resolveCommitSubject(process.env.MSG));
578
- " 2>/dev/null) || SUBJECT="${MSG%%$'\n'*}"
599
+ " 2>/dev/null) || SUBJECT=$(gsd_first_line "$MSG")
579
600
  else
580
- # Pure parameter expansion, not `echo "$MSG" | head -1`: that pipeline
601
+ # Pure parameter expansion (in gsd_first_line), not
602
+ # `echo "$MSG" | head -1`: that pipeline
581
603
  # raced a SIGPIPE under `set -euo pipefail` whenever $MSG had a body
582
604
  # (the common case) — `head -1` can close its read end as soon as it
583
605
  # has the first line, and if `echo`'s write lands after that close,
584
606
  # `echo` dies with signal 13 (exit 141), which is NOT suppressed by
585
607
  # `set -e` and aborted the whole hook intermittently (observed in
586
- # tests/hooks-opt-in.test.cjs's --fixup=HEAD "round 7" case). Zero
587
- # subprocesses here means zero pipe/race surface. Equivalent to
608
+ # tests/hooks-opt-in.test.cjs's --fixup=HEAD "round 7" case). No pipe
609
+ # means no race surface. Equivalent to
588
610
  # `head -1` for single-line, multi-line, and trailing-newline input.
589
- SUBJECT="${MSG%%$'\n'*}"
611
+ SUBJECT=$(gsd_first_line "$MSG")
590
612
  fi
591
613
  # Single source of truth for the accepted commit-type list (#3811): the
592
614
  # 10 built-ins plus whatever passed the safe-token filter above. Both the
@@ -227,7 +227,9 @@ function resolveRuntimeIdentity(cwd, configPath, resolveRuntimeNameFromCandidate
227
227
  // never a resolution failure (this function only ever throws for the
228
228
  // config.json read above, which the caller's catch already handles).
229
229
  try {
230
- const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json');
230
+ // #5017: the store root is GSD_HOME when set and non-blank, else the home
231
+ // directory — the config loader's rule (src/runtime-homes.cts resolveGsdHome).
232
+ const defaultsPath = path.join(process.env.GSD_HOME || os.homedir(), '.gsd', 'defaults.json');
231
233
  const defaultsRaw = fs.readFileSync(defaultsPath, 'utf-8');
232
234
  const defaultsParsed = JSON.parse(defaultsRaw);
233
235
  if (defaultsParsed && typeof defaultsParsed === 'object' && 'runtime' in defaultsParsed) {
@@ -435,7 +435,8 @@ function resolveFallbackIsolation(root, configPath) {
435
435
  }
436
436
  if (!runtimeId) {
437
437
  try {
438
- const defaultsPath = path.join(os.homedir(), '.gsd', 'defaults.json');
438
+ // #5017: GSD_HOME when set and non-blank, else the home directory — the loader's rule.
439
+ const defaultsPath = path.join(process.env.GSD_HOME || os.homedir(), '.gsd', 'defaults.json');
439
440
  const defaultsParsed = JSON.parse(fs.readFileSync(defaultsPath, 'utf-8'));
440
441
  if (defaultsParsed && typeof defaultsParsed === 'object' && 'runtime' in defaultsParsed) {
441
442
  runtimeId = resolveRuntimeNameFromCandidates(defaultsParsed.runtime) || null;
@@ -67,6 +67,8 @@
67
67
  // `git check-ignore` / `git ls-files` / `git rm --cached`, and the
68
68
  // destination of a one-source `cp`/`mv` (#4856). The #4856 carve-outs
69
69
  // fail closed: an option they do not list withdraws them from the segment.
70
+ // A git global option the walker does not know also keeps the #5045 `-c`
71
+ // scan running over every later word (see walkGitGlobals).
70
72
  // A shell interpreter (bash/sh/zsh/dash/ksh/su) has its script scanned
71
73
  // whether it arrives via `-c '…'`, a `<( )` file operand, a heredoc /
72
74
  // here-string, or a pipe from a knowable `echo`/`printf` source
@@ -192,14 +194,29 @@ const GIT_PATHSPEC_SUBCOMMANDS = new Map([
192
194
  }],
193
195
  ]);
194
196
 
195
- // git's global options (its own usage line, git 2.49), needed to locate the
196
- // subcommand. A value option consumes the next word unless written
197
- // `--opt=value`; an option in neither set fails closed, so a value is never
198
- // read as the subcommand (`git -C ls-files show HEAD:.env` runs `show`).
199
- const GIT_GLOBAL_VALUE_OPTIONS = new Set(['-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env']);
197
+ // git's global options (git.c handle_options, git 2.49) — the ONE vocabulary
198
+ // both the #4856 pathspec exemption and the #5045 -c scan walk, via
199
+ // walkGitGlobals. A value option consumes the next word; the long options in
200
+ // GIT_GLOBAL_EQUALS_FORM_OPTIONS also take it attached as `--opt=value`.
201
+ // `-C`, `-c` and the hidden `--shallow-file` accept only the separate word
202
+ // (git rejects `--shallow-file=x` and `-c<name>=<value>`), and
203
+ // `--exec-path` runs a subcommand only as `--exec-path=<path>` (bare, it
204
+ // prints the path and exits). Any other option fails closed for both
205
+ // consumers: its arity is unknown, so its value could be read as the
206
+ // subcommand (`git -C ls-files show HEAD:.env` runs `show`). The pathspec
207
+ // exemption is withdrawn, and the -c scan keeps collecting every later
208
+ // `-c <value>` pair instead of stopping.
209
+ const GIT_GLOBAL_VALUE_OPTIONS = new Set([
210
+ '-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source',
211
+ '--shallow-file',
212
+ ]);
213
+ const GIT_GLOBAL_EQUALS_FORM_OPTIONS = new Set([
214
+ '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--exec-path',
215
+ ]);
200
216
  const GIT_GLOBAL_FLAGS = new Set([
201
217
  '-p', '--paginate', '-P', '--no-pager', '--no-replace-objects', '--no-lazy-fetch',
202
218
  '--no-optional-locks', '--no-advice', '--bare',
219
+ '--literal-pathspecs', '--glob-pathspecs', '--noglob-pathspecs', '--icase-pathspecs',
203
220
  ]);
204
221
 
205
222
  // #4856: `cp`/`mv` write their destination and never print it, so a secret
@@ -789,6 +806,29 @@ function resolveCommand(words) {
789
806
  return { base: lastSegment(words[idx].text).toLowerCase(), operands: words.slice(idx + 1) };
790
807
  }
791
808
 
809
+ /**
810
+ * Rescan only the command-valued -c keys selected by #5045. This deliberately
811
+ * does not resolve config files, --config-env/GIT_CONFIG_* values, or other
812
+ * executing keys (diff drivers, filters, credential helpers, ssh, gpg).
813
+ * Those are documented gaps, not a promise of complete Git config analysis.
814
+ * Global option values and subcommand arguments are data, not more -c flags.
815
+ */
816
+ function scanGitConfig(operands, depth) {
817
+ for (const config of walkGitGlobals(operands).configs) {
818
+ const eq = config.indexOf('=');
819
+ if (eq === -1) continue;
820
+ const key = config.slice(0, eq).toLowerCase();
821
+ const value = config.slice(eq + 1);
822
+ let script;
823
+ if (key.startsWith('alias.') && value.startsWith('!')) script = value.slice(1);
824
+ else if (key === 'diff.external') script = value;
825
+ else continue;
826
+ const hit = findSecretRead(script, depth + 1);
827
+ if (hit) return hit;
828
+ }
829
+ return null;
830
+ }
831
+
792
832
  // Operand indices the segment's command consumes as a NAME, never as
793
833
  // CONTENTS (#4639, #4856). Only these positions skip the operand check;
794
834
  // every other operand is still checked, so a carve-out cannot launder a read
@@ -820,26 +860,38 @@ function isListedFlag(text, flags) {
820
860
  return /^-[A-Za-z]+$/.test(text) && [...text.slice(1)].every((ch) => flags.has(`-${ch}`));
821
861
  }
822
862
 
823
- // Index of git's subcommand past its global options, or -1 when an option
824
- // outside GIT_GLOBAL_VALUE_OPTIONS / GIT_GLOBAL_FLAGS precedes it.
825
- function gitSubcommandIndex(operands) {
863
+ // Walks git's global options (see GIT_GLOBAL_VALUE_OPTIONS). `sub` is the
864
+ // subcommand's index, or -1 when an unlisted option (or the end of the words)
865
+ // comes first; `configs` holds every `-c` value seen on the way, in order.
866
+ // Past an unlisted option the subcommand cannot be located, so `configs`
867
+ // conservatively takes the word after every later `-c` as well: a read
868
+ // still needs a command-valued key, so a harmless value stays allowed.
869
+ function walkGitGlobals(operands) {
870
+ const configs = [];
826
871
  for (let k = 0; k < operands.length; k++) {
827
872
  const t = operands[k].text;
828
- if (!t.startsWith('-')) return k;
829
- if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) { k++; continue; }
873
+ if (!t.startsWith('-')) return { sub: k, configs };
874
+ if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) {
875
+ if (t === '-c' && k + 1 < operands.length) configs.push(operands[k + 1].text);
876
+ k++;
877
+ continue;
878
+ }
830
879
  if (GIT_GLOBAL_FLAGS.has(t)) continue;
831
880
  const eq = t.indexOf('=');
832
- const isLongValueForm = t.startsWith('--') && eq !== -1 && GIT_GLOBAL_VALUE_OPTIONS.has(t.slice(0, eq));
833
- if (!isLongValueForm) return -1;
881
+ if (t.startsWith('--') && eq !== -1 && GIT_GLOBAL_EQUALS_FORM_OPTIONS.has(t.slice(0, eq))) continue;
882
+ for (let j = k + 1; j + 1 < operands.length; j++) {
883
+ if (operands[j].text === '-c') configs.push(operands[++j].text);
884
+ }
885
+ return { sub: -1, configs };
834
886
  }
835
- return -1;
887
+ return { sub: -1, configs };
836
888
  }
837
889
 
838
890
  // #4856: the pathspec operands of a GIT_PATHSPEC_SUBCOMMANDS invocation. git
839
891
  // permutes options, so one may follow a pathspec; after `--` every word is a
840
892
  // pathspec. The subcommand is matched case-sensitively, as git looks it up.
841
893
  function gitPathspecIndices(operands) {
842
- const sub = gitSubcommandIndex(operands);
894
+ const { sub } = walkGitGlobals(operands);
843
895
  const spec = sub === -1 ? undefined : GIT_PATHSPEC_SUBCOMMANDS.get(operands[sub].text);
844
896
  if (!spec) return new Set();
845
897
  const pathspecs = new Set();
@@ -1007,6 +1059,12 @@ function findSecretRead(command, depth) {
1007
1059
  // `bash .env` (file mode) is caught by the operand check below.
1008
1060
  }
1009
1061
 
1062
+ // Git -c can carry a shell script instead of a file-name operand (#5045).
1063
+ if ((base === 'git' || base === 'git.exe') && depth < MAX_NESTING_DEPTH) {
1064
+ const hit = scanGitConfig(operands, depth);
1065
+ if (hit) return hit;
1066
+ }
1067
+
1010
1068
  if (NON_READING_COMMANDS.has(base)) continue;
1011
1069
 
1012
1070
  const nameOnly = nameOnlyOperandIndices(base, operands);