@opengsd/gsd-core 1.17.0 → 1.19.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 (369) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.opencode/plugins/gsd-core.js +2 -3
  4. package/agents/gsd-advisor-researcher.compact.md +1 -1
  5. package/agents/gsd-advisor-researcher.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  7. package/agents/gsd-assumptions-analyzer.md +1 -1
  8. package/agents/gsd-code-fixer.compact.md +3 -3
  9. package/agents/gsd-code-fixer.md +5 -4
  10. package/agents/gsd-code-reviewer.compact.md +1 -1
  11. package/agents/gsd-code-reviewer.md +1 -1
  12. package/agents/gsd-codebase-mapper.compact.md +1 -1
  13. package/agents/gsd-codebase-mapper.md +1 -1
  14. package/agents/gsd-debugger.md +1 -1
  15. package/agents/gsd-doc-verifier.compact.md +4 -4
  16. package/agents/gsd-doc-verifier.md +4 -3
  17. package/agents/gsd-doc-writer.compact.md +23 -5
  18. package/agents/gsd-doc-writer.md +19 -6
  19. package/agents/gsd-eval-auditor.compact.md +1 -1
  20. package/agents/gsd-eval-auditor.md +1 -1
  21. package/agents/gsd-executor.md +7 -3
  22. package/agents/gsd-integration-checker.compact.md +1 -1
  23. package/agents/gsd-integration-checker.md +1 -1
  24. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  25. package/agents/gsd-nyquist-auditor.md +1 -1
  26. package/agents/gsd-phase-researcher.md +1 -1
  27. package/agents/gsd-plan-checker.md +2 -1
  28. package/agents/gsd-planner.md +5 -4
  29. package/agents/gsd-project-researcher.compact.md +1 -1
  30. package/agents/gsd-project-researcher.md +1 -1
  31. package/agents/gsd-research-synthesizer.compact.md +5 -5
  32. package/agents/gsd-research-synthesizer.md +5 -5
  33. package/agents/gsd-roadmapper.compact.md +1 -1
  34. package/agents/gsd-roadmapper.md +1 -1
  35. package/agents/gsd-security-auditor.compact.md +1 -1
  36. package/agents/gsd-security-auditor.md +1 -1
  37. package/agents/gsd-ui-auditor.compact.md +1 -1
  38. package/agents/gsd-ui-auditor.md +1 -1
  39. package/agents/gsd-ui-checker.compact.md +1 -1
  40. package/agents/gsd-ui-checker.md +1 -1
  41. package/agents/gsd-ui-researcher.compact.md +13 -1
  42. package/agents/gsd-ui-researcher.md +28 -1
  43. package/agents/gsd-verifier.md +2 -2
  44. package/bin/install.js +460 -105
  45. package/commands/gsd/code-review.md +8 -0
  46. package/commands/gsd/pause-work.md +1 -1
  47. package/gsd-core/bin/gsd-tools.cjs +124 -18
  48. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  49. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  50. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  51. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  52. package/gsd-core/bin/lib/capability-lock.cjs +41 -8
  53. package/gsd-core/bin/lib/capability-registry.cjs +162 -74
  54. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  55. package/gsd-core/bin/lib/capability-state.cjs +20 -11
  56. package/gsd-core/bin/lib/capability-validator.cjs +23 -4
  57. package/gsd-core/bin/lib/commands.cjs +5 -1
  58. package/gsd-core/bin/lib/config-loader.cjs +13 -3
  59. package/gsd-core/bin/lib/config.cjs +256 -58
  60. package/gsd-core/bin/lib/core-utils.cjs +34 -0
  61. package/gsd-core/bin/lib/decision-coverage-support.cjs +93 -0
  62. package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
  63. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +21 -1
  64. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +19 -1
  65. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  66. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  67. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  68. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +114 -18
  69. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  70. package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
  72. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  73. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  74. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  75. package/gsd-core/bin/lib/init.cjs +76 -5
  76. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  77. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  78. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  79. package/gsd-core/bin/lib/install-profiles.cjs +14 -7
  80. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  81. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  82. package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
  83. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  84. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  85. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  86. package/gsd-core/bin/lib/milestone.cjs +43 -1
  87. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  88. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  89. package/gsd-core/bin/lib/phase-id.cjs +21 -4
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
  91. package/gsd-core/bin/lib/phase.cjs +235 -37
  92. package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
  93. package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
  94. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  95. package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
  96. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  97. package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
  98. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  99. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  100. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  101. package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
  102. package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
  103. package/gsd-core/bin/lib/roadmap.cjs +89 -30
  104. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +428 -16
  105. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  106. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  107. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  108. package/gsd-core/bin/lib/shell-command-projection.cjs +123 -16
  109. package/gsd-core/bin/lib/smart-entry.cjs +37 -16
  110. package/gsd-core/bin/lib/state-contract.cjs +29 -9
  111. package/gsd-core/bin/lib/state-document.cjs +47 -5
  112. package/gsd-core/bin/lib/state-transition.cjs +150 -34
  113. package/gsd-core/bin/lib/state.cjs +140 -88
  114. package/gsd-core/bin/lib/surface.cjs +9 -0
  115. package/gsd-core/bin/lib/task-command-router.cjs +7 -28
  116. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  117. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  118. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  119. package/gsd-core/bin/lib/verification.cjs +96 -9
  120. package/gsd-core/bin/lib/verify.cjs +38 -1
  121. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  122. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  123. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  124. package/gsd-core/references/agent-contracts.md +1 -1
  125. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  126. package/gsd-core/references/git-integration.md +14 -3
  127. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  128. package/gsd-core/references/planner-reversibility.md +18 -4
  129. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  130. package/gsd-core/references/planning-config.md +2 -0
  131. package/gsd-core/references/tdd.md +5 -4
  132. package/gsd-core/references/verifier-phase-gates.md +8 -1
  133. package/gsd-core/templates/DEBUG.md +16 -0
  134. package/gsd-core/templates/config.json +2 -1
  135. package/gsd-core/templates/verification-report.md +1 -1
  136. package/gsd-core/workflows/add-backlog.md +10 -2
  137. package/gsd-core/workflows/add-phase.md +5 -1
  138. package/gsd-core/workflows/add-tests.md +5 -1
  139. package/gsd-core/workflows/add-todo.md +5 -1
  140. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  141. package/gsd-core/workflows/audit-fix.md +2 -1
  142. package/gsd-core/workflows/audit-milestone.md +21 -9
  143. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  144. package/gsd-core/workflows/autonomous.md +52 -64
  145. package/gsd-core/workflows/check-todos.md +5 -1
  146. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  147. package/gsd-core/workflows/code-review-fix.md +5 -1
  148. package/gsd-core/workflows/code-review.md +66 -8
  149. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  150. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  151. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  152. package/gsd-core/workflows/complete-milestone.md +64 -157
  153. package/gsd-core/workflows/debug.md +5 -1
  154. package/gsd-core/workflows/diagnose-issues.md +34 -6
  155. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  156. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  157. package/gsd-core/workflows/discuss-phase-assumptions.md +44 -5
  158. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  159. package/gsd-core/workflows/discuss-phase.md +11 -11
  160. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  161. package/gsd-core/workflows/docs-update.md +27 -5
  162. package/gsd-core/workflows/edit-phase.md +5 -1
  163. package/gsd-core/workflows/eval-review.md +11 -3
  164. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  165. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  166. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +19 -8
  167. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +21 -3
  168. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  169. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +7 -3
  170. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  171. package/gsd-core/workflows/execute-phase.md +46 -11
  172. package/gsd-core/workflows/execute-plan.md +8 -4
  173. package/gsd-core/workflows/extract-learnings.md +6 -2
  174. package/gsd-core/workflows/forensics.md +5 -1
  175. package/gsd-core/workflows/health.md +2 -1
  176. package/gsd-core/workflows/import.md +6 -2
  177. package/gsd-core/workflows/insert-phase.md +5 -1
  178. package/gsd-core/workflows/manager.md +10 -2
  179. package/gsd-core/workflows/map-codebase.md +5 -1
  180. package/gsd-core/workflows/milestone-summary.md +10 -2
  181. package/gsd-core/workflows/new-milestone.md +13 -5
  182. package/gsd-core/workflows/new-project.md +9 -5
  183. package/gsd-core/workflows/next.md +8 -4
  184. package/gsd-core/workflows/pause-work.md +3 -3
  185. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  186. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +7 -3
  187. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  188. package/gsd-core/workflows/plan-phase.md +54 -86
  189. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  190. package/gsd-core/workflows/plant-seed.md +8 -4
  191. package/gsd-core/workflows/progress.md +6 -2
  192. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  193. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  194. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  195. package/gsd-core/workflows/quick-batch.md +5 -1
  196. package/gsd-core/workflows/quick.md +23 -8
  197. package/gsd-core/workflows/remove-phase.md +5 -1
  198. package/gsd-core/workflows/resume-project.md +5 -1
  199. package/gsd-core/workflows/review.md +22 -15
  200. package/gsd-core/workflows/scan.md +5 -1
  201. package/gsd-core/workflows/section-manifest.json +5 -0
  202. package/gsd-core/workflows/secure-phase.md +6 -2
  203. package/gsd-core/workflows/settings.md +4 -3
  204. package/gsd-core/workflows/ship.md +16 -14
  205. package/gsd-core/workflows/spec-phase.md +3 -6
  206. package/gsd-core/workflows/thread.md +10 -2
  207. package/gsd-core/workflows/transition.md +5 -1
  208. package/gsd-core/workflows/ui-phase.md +5 -1
  209. package/gsd-core/workflows/ui-review.md +6 -2
  210. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  211. package/gsd-core/workflows/update.md +5 -1
  212. package/gsd-core/workflows/validate-phase.md +6 -2
  213. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  214. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  215. package/gsd-core/workflows/verify-work.md +15 -6
  216. package/hooks/dist/gsd-agent-isolation-guard.js +14 -2
  217. package/hooks/dist/gsd-cursor-subagent-start.js +8 -3
  218. package/hooks/dist/gsd-path-export.js +95 -0
  219. package/hooks/dist/gsd-secret-read-guard.js +244 -53
  220. package/hooks/dist/gsd-statusline.js +35 -2
  221. package/hooks/dist/gsd-validate-commit.sh +27 -5
  222. package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
  223. package/hooks/dist/gsd-workflow-guard.js +1 -1
  224. package/hooks/dist/gsd-worktree-path-guard.js +28 -1
  225. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  226. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  227. package/hooks/gsd-agent-isolation-guard.js +14 -2
  228. package/hooks/gsd-cursor-subagent-start.js +8 -3
  229. package/hooks/gsd-path-export.js +95 -0
  230. package/hooks/gsd-secret-read-guard.js +244 -53
  231. package/hooks/gsd-statusline.js +35 -2
  232. package/hooks/gsd-validate-commit.sh +27 -5
  233. package/hooks/gsd-windsurf-pre-write.js +28 -1
  234. package/hooks/gsd-workflow-guard.js +1 -1
  235. package/hooks/gsd-worktree-path-guard.js +28 -1
  236. package/hooks/hooks.json +1 -0
  237. package/hooks/lib/isolation-sentinel.js +44 -0
  238. package/hooks/managed-hooks-registry.cjs +1 -0
  239. package/package.json +13 -6
  240. package/pi/gsd.cjs +1047 -10
  241. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  242. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  243. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  244. package/pi/package-agents/gsd-code-fixer.md +769 -0
  245. package/pi/package-agents/gsd-code-reviewer.md +402 -0
  246. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  247. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  248. package/pi/package-agents/gsd-debugger.md +1274 -0
  249. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  250. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  251. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  252. package/pi/package-agents/gsd-doc-writer.md +626 -0
  253. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  254. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  255. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  256. package/pi/package-agents/gsd-eval-planner.md +149 -0
  257. package/pi/package-agents/gsd-executor.md +894 -0
  258. package/pi/package-agents/gsd-framework-selector.md +159 -0
  259. package/pi/package-agents/gsd-integration-checker.md +476 -0
  260. package/pi/package-agents/gsd-intel-updater.md +338 -0
  261. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  262. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  263. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  264. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  265. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  266. package/pi/package-agents/gsd-planner.md +1003 -0
  267. package/pi/package-agents/gsd-project-researcher.md +611 -0
  268. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  269. package/pi/package-agents/gsd-roadmapper.md +779 -0
  270. package/pi/package-agents/gsd-security-auditor.md +171 -0
  271. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  272. package/pi/package-agents/gsd-ui-checker.md +417 -0
  273. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  274. package/pi/package-agents/gsd-user-profiler.md +174 -0
  275. package/pi/package-agents/gsd-verifier.md +963 -0
  276. package/pi/package-agents/manifest.json +39 -0
  277. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  278. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  279. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  280. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  281. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  282. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  283. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  284. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  285. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  286. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  287. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  288. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  289. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  290. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  291. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  292. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  293. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  294. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  295. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  296. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  297. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  298. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  299. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  300. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  301. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  302. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  303. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  304. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  305. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  306. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  307. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  308. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  309. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  310. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  311. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  312. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  313. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  314. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  315. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  316. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  317. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  318. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  319. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  320. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  321. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  322. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  323. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  324. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  325. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  326. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  327. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  328. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  329. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  330. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  331. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  332. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  333. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  334. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  335. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  336. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  337. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  338. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  339. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  340. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  341. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  342. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  343. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  344. package/pi/package-skills/gsd-update/SKILL.md +51 -0
  345. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  346. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  347. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  348. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  349. package/scripts/audit-workflow-script-paths.cjs +1 -1
  350. package/scripts/build-hooks.js +4 -0
  351. package/scripts/command-contract-helpers.cjs +1 -1
  352. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  353. package/scripts/docs-guard-registry.cjs +22 -7
  354. package/scripts/gen-health-docs.cjs +13 -1
  355. package/scripts/gen-pi-package-skills.cjs +217 -0
  356. package/scripts/lib/macos-conformance-tier.generated.cjs +19 -0
  357. package/scripts/lib/platform-conformance-tier.generated.cjs +41 -0
  358. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  359. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  360. package/scripts/lint-retired-runtime-name.cjs +5 -0
  361. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  362. package/scripts/npm-audit-baseline.cjs +41 -7
  363. package/scripts/release-tarball-smoke.cjs +1 -0
  364. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  365. package/scripts/select-docs-guards.cjs +1 -1
  366. package/scripts/self-test-report-policy.cjs +350 -0
  367. package/skills/gsd-code-review/SKILL.md +8 -0
  368. package/skills/gsd-pause-work/SKILL.md +1 -1
  369. package/vscode/package.json +1 -1
@@ -70,7 +70,7 @@ const { allow } = require('./lib/hook-exit.js');
70
70
  // hooks/lib/cursor-workspace.js. Staged next to these scripts by
71
71
  // writeCursorHooksJson so the require always resolves post-install.
72
72
  const { resolveStatePath } = require('./lib/cursor-workspace.js');
73
- const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
73
+ const { readSentinel, VALID_ISOLATION, extractDispatchIdentifiers, sentinelAppliesToDispatch, agedNoneGovernsDispatch, buildSentinelDiscard } = require('./lib/isolation-sentinel.js');
74
74
  const { REASON_CODE, describeSentinelDiscard } = require('./lib/isolation-deny-reason.js');
75
75
  // #3582: gsd-core/bin/lib/*.cjs (runtime-homes.cjs, worktree-safety.cjs,
76
76
  // runtime-name-policy.cjs, capability-registry.cjs — required below, inside
@@ -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;
@@ -517,9 +518,13 @@ function evaluateRootIsolation(root, subagentType, { clock = Date, dispatchIds =
517
518
 
518
519
  let declaredIsolation;
519
520
  try {
521
+ // #5479: an aged recorded `none` for this dispatch's phase keeps governing it — the same predicate,
522
+ // and so the same verdicts, as the Claude guard (hooks/gsd-agent-isolation-guard.js).
520
523
  declaredIsolation = (sentinel.present && !sentinel.stale && applies)
521
524
  ? sentinel.isolation
522
- : resolveFallbackIsolation(root, configPath);
525
+ : (sentinel.present && sentinel.stale && agedNoneGovernsDispatch(sentinel, dispatchIds, { clock }))
526
+ ? 'none'
527
+ : resolveFallbackIsolation(root, configPath);
523
528
  } catch {
524
529
  return {
525
530
  action: 'deny',
@@ -0,0 +1,95 @@
1
+ #!/usr/bin/env node
2
+ // gsd-hook-version: {{GSD_VERSION}}
3
+ // SessionStart hook: put the installed gsd-core `bin/` directory (which holds `gsd_run`) on the
4
+ // PATH of every later Bash tool call in this Claude Code session (#5465).
5
+ //
6
+ // Why a hook: the resolver snippet (gsd-core/workflows/_runtime-launcher.snippet.sh) persists its
7
+ // PATH export through $CLAUDE_ENV_FILE, but Claude Code provides that variable only to SessionStart,
8
+ // Setup, CwdChanged and FileChanged hooks, never to a workflow Bash block. So the write never fired
9
+ // and a later block that called `gsd_run` without re-including the resolver got "command not found"
10
+ // (hidden by `2>/dev/null || echo <fallback>`). This hook writes the same line from the one place
11
+ // the variable exists. The snippet's in-block write stays for users who export the variable
12
+ // themselves.
13
+ //
14
+ // Fail-open and side-effect-minimal by design: it only ever appends one line to the file Claude Code
15
+ // hands to SessionStart hooks, only for a directory that contains an installed `gsd_run`, and any
16
+ // problem (no variable, no gsd_run, unwritable file) exits 0 without output.
17
+
18
+ 'use strict';
19
+
20
+ const fs = require('fs');
21
+ const path = require('path');
22
+ const { allow } = require('./lib/hook-exit.js');
23
+
24
+ /**
25
+ * The `bin/` directory next to this hook's install root: hooks live in `<root>/hooks/` and the
26
+ * runtime in `<root>/gsd-core/` for global, project-local and plugin layouts alike. Returns the
27
+ * real path, or null when `gsd_run` is not there.
28
+ */
29
+ function resolveBinDir(hookDir) {
30
+ try {
31
+ const bin = fs.realpathSync(path.join(hookDir, '..', 'gsd-core', 'bin'));
32
+ return fs.statSync(path.join(bin, 'gsd_run')).isFile() ? bin : null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * The directory as the bash that sources $CLAUDE_ENV_FILE spells it. On Windows the env file is
40
+ * sourced by Git Bash, where `:` separates PATH entries and a drive letter is `/c/...`, which is
41
+ * also what the resolver snippet writes (it derives the directory from bash's `$GSD_TOOLS`). A
42
+ * native `C:\Users\x\...` would split at the colon and never resolve.
43
+ */
44
+ function shellPath(dir, platform = process.platform) {
45
+ if (platform !== 'win32') return dir;
46
+ const m = /^([A-Za-z]):[\\/](.*)$/.exec(dir);
47
+ const rest = (m ? m[2] : dir.replace(/^[\\/]+/, '')).replace(/\\/g, '/');
48
+ return m ? `/${m[1].toLowerCase()}/${rest}` : dir.replace(/\\/g, '/');
49
+ }
50
+
51
+ /** The exact line the resolver snippet writes: `export PATH='<dir>':"$PATH"` with `'` escaped. */
52
+ function exportLine(binDir, platform = process.platform) {
53
+ return `export PATH='${shellPath(binDir, platform).replace(/'/g, "'\\''")}':"$PATH"`;
54
+ }
55
+
56
+ /**
57
+ * Append the export to `envFile` unless it is already there. Returns true when it wrote.
58
+ * Never throws.
59
+ */
60
+ function persistPath(envFile, binDir) {
61
+ try {
62
+ const line = exportLine(binDir);
63
+ let existing = '';
64
+ try {
65
+ existing = fs.readFileSync(envFile, 'utf8');
66
+ } catch (e) {
67
+ if (e && e.code !== 'ENOENT') return false;
68
+ }
69
+ if (existing.split(/\r?\n/).includes(line)) return false;
70
+ const prefix = existing === '' || existing.endsWith('\n') ? '' : '\n';
71
+ fs.appendFileSync(envFile, `${prefix}${line}\n`, 'utf8');
72
+ return true;
73
+ } catch {
74
+ return false;
75
+ }
76
+ }
77
+
78
+ function main() {
79
+ const envFile = process.env.CLAUDE_ENV_FILE;
80
+ if (typeof envFile !== 'string' || envFile.trim() === '') return;
81
+ const binDir = resolveBinDir(__dirname);
82
+ if (binDir === null) return;
83
+ persistPath(envFile, binDir);
84
+ }
85
+
86
+ if (require.main === module) {
87
+ try {
88
+ main();
89
+ } catch {
90
+ // A PATH convenience must never fail session start.
91
+ }
92
+ allow();
93
+ }
94
+
95
+ module.exports = { resolveBinDir, shellPath, exportLine, persistPath };
@@ -56,7 +56,9 @@
56
56
  // EOF)"` — is never operand-checked, so prose mentioning `.env` is safe.
57
57
  // pass 2 groups tokens by segment and evaluates each on its own:
58
58
  // input redirect targets (`<`, `N<`) are always checked; the command word is
59
- // located past `sudo`/`env VAR=x`/`nohup`-style prefixes; a closed set of
59
+ // located past wrapper prefixes AND their own options (`sudo -u U`,
60
+ // `env -i VAR=x`, `timeout -s KILL 5`, `nice -n 5`, `stdbuf -oL`, `exec`;
61
+ // see WRAPPERS, #5627); a closed set of
60
62
  // NON-READING commands (test/[/ls/stat/rm/touch/echo/…) exempts that
61
63
  // segment's operands — `[ -f .env ]` and `ls .env*` are existence checks
62
64
  // GSD's own agents run — while `cp`/`mv`/`ln`/`git` are deliberately NOT
@@ -67,6 +69,8 @@
67
69
  // `git check-ignore` / `git ls-files` / `git rm --cached`, and the
68
70
  // destination of a one-source `cp`/`mv` (#4856). The #4856 carve-outs
69
71
  // fail closed: an option they do not list withdraws them from the segment.
72
+ // A git global option the walker does not know also keeps the #5045 `-c`
73
+ // scan running over every later word (see walkGitGlobals).
70
74
  // A shell interpreter (bash/sh/zsh/dash/ksh/su) has its script scanned
71
75
  // whether it arrives via `-c '…'`, a `<( )` file operand, a heredoc /
72
76
  // here-string, or a pipe from a knowable `echo`/`printf` source
@@ -88,7 +92,9 @@
88
92
  // (`cat .e*`), interpreter one-liners (`python -c "open('.env')"`), a piped
89
93
  // script from a non-echo source (`cat gen.sh | bash`, `curl … | sh`), reads
90
94
  // inside scripts the agent executes, and `glob: '*'` reaching a
91
- // NON-gitignored `.env`. The promise is "no looser than the retired rules
95
+ // NON-gitignored `.env`, and a wrapper (or wrapper option taking a
96
+ // separate value) the WRAPPERS table does not list, or `env -S 'script'` (the split string is skipped as
97
+ // the option's value). The promise is "no looser than the retired rules
92
98
  // on plain commands, without arming the compound-`cd` prompt". Writes to
93
99
  // secret files are out of scope (Write/Edit were never gated). Commands
94
100
  // over 1 MiB are denied outright (`command-too-large`) rather than
@@ -146,8 +152,29 @@ const MAX_GLOB_ALTERNATIVES = 64;
146
152
  const NON_SECRET_ENV_SUFFIXES = new Set(['example', 'sample', 'template', 'dist']);
147
153
 
148
154
  // Command-prefix wrappers to look through when locating the command word at
149
- // the head of a segment (same set as hooks/gsd-windsurf-pre-command.js).
150
- const CMD_PREFIXES = new Set(['sudo', 'env', 'command', 'nice', 'nohup', 'time', 'doas']);
155
+ // the head of a segment (#5627). Each wrapper runs the words that follow its
156
+ // own options, so the table records what resolveCommand must step over:
157
+ // short letters of short options that take a value (`-n 5`, `-n5`)
158
+ // long long options that take a SEPARATE value word (`--adjustment 5`);
159
+ // `--opt=value` is one word and needs no entry
160
+ // assigns leading `VAR=value` words belong to the wrapper (env)
161
+ // operands positional words after the options (timeout's DURATION)
162
+ // Taken from sudo(8), doas(1), env(1), nice(1), time(1), timeout(1),
163
+ // stdbuf(1), setsid(1) and bash's `command`/`exec`. A wrong entry fails open
164
+ // (the option's value becomes the command word), so keep it to the man pages.
165
+ const WRAPPERS = new Map([
166
+ ['sudo', { short: 'CDghpRrTtUu', long: ['--close-from', '--chdir', '--group', '--host', '--prompt', '--chroot', '--role', '--command-timeout', '--type', '--other-user', '--user'] }],
167
+ ['doas', { short: 'Cu' }],
168
+ ['env', { short: 'uCSaP', long: ['--unset', '--chdir', '--split-string', '--argv0'], assigns: true }],
169
+ ['nice', { short: 'n', long: ['--adjustment'] }],
170
+ ['nohup', {}],
171
+ ['command', {}],
172
+ ['time', { short: 'fo', long: ['--format', '--output'] }],
173
+ ['timeout', { short: 'sk', long: ['--signal', '--kill-after'], operands: 1 }],
174
+ ['stdbuf', { short: 'ioe', long: ['--input', '--output', '--error'] }],
175
+ ['setsid', {}],
176
+ ['exec', { short: 'a' }],
177
+ ]);
151
178
 
152
179
  // Commands whose ordinary operands are file NAMES, never file CONTENTS. A
153
180
  // closed set on purpose: anything not listed is assumed to read.
@@ -192,14 +219,29 @@ const GIT_PATHSPEC_SUBCOMMANDS = new Map([
192
219
  }],
193
220
  ]);
194
221
 
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']);
222
+ // git's global options (git.c handle_options, git 2.49) — the ONE vocabulary
223
+ // both the #4856 pathspec exemption and the #5045 -c scan walk, via
224
+ // walkGitGlobals. A value option consumes the next word; the long options in
225
+ // GIT_GLOBAL_EQUALS_FORM_OPTIONS also take it attached as `--opt=value`.
226
+ // `-C`, `-c` and the hidden `--shallow-file` accept only the separate word
227
+ // (git rejects `--shallow-file=x` and `-c<name>=<value>`), and
228
+ // `--exec-path` runs a subcommand only as `--exec-path=<path>` (bare, it
229
+ // prints the path and exits). Any other option fails closed for both
230
+ // consumers: its arity is unknown, so its value could be read as the
231
+ // subcommand (`git -C ls-files show HEAD:.env` runs `show`). The pathspec
232
+ // exemption is withdrawn, and the -c scan keeps collecting every later
233
+ // `-c <value>` pair instead of stopping.
234
+ const GIT_GLOBAL_VALUE_OPTIONS = new Set([
235
+ '-C', '-c', '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source',
236
+ '--shallow-file',
237
+ ]);
238
+ const GIT_GLOBAL_EQUALS_FORM_OPTIONS = new Set([
239
+ '--git-dir', '--work-tree', '--namespace', '--config-env', '--attr-source', '--exec-path',
240
+ ]);
200
241
  const GIT_GLOBAL_FLAGS = new Set([
201
242
  '-p', '--paginate', '-P', '--no-pager', '--no-replace-objects', '--no-lazy-fetch',
202
243
  '--no-optional-locks', '--no-advice', '--bare',
244
+ '--literal-pathspecs', '--glob-pathspecs', '--noglob-pathspecs', '--icase-pathspecs',
203
245
  ]);
204
246
 
205
247
  // #4856: `cp`/`mv` write their destination and never print it, so a secret
@@ -388,6 +430,20 @@ function classifyGrepGlob(glob) {
388
430
  // Bash command scan — pass 1: tokenizer
389
431
  // ---------------------------------------------------------------------------
390
432
 
433
+ // Which reading of a heredoc the scan is making (#5513). The guard scans a command that
434
+ // opens a heredoc under every reading below and denies if any of them finds a secret read
435
+ // (see findSecretReadUnderOtherReadings), so a change of reading can add a denial but
436
+ // never remove one.
437
+ // - heredocBodiesSkippedInSpans: bash 5 skips the bodies of heredocs opened inside a
438
+ // `$( )` span when it looks for the closing `)`; bash 3.2.57 (still `/bin/bash` on
439
+ // macOS) does not, so a `)` on a body line closes the substitution and the rest of
440
+ // that line runs as the OUTER command.
441
+ // - heredocTagsAsWords: an unquoted tag is a bash word (a CR and a substitution belong
442
+ // to it). false is the earlier reading, where the tag stopped at any whitespace and at
443
+ // `(`: kept as a reading so nothing the guard denied before is allowed now.
444
+ let heredocBodiesSkippedInSpans = true;
445
+ let heredocTagsAsWords = true;
446
+
391
447
  // Index of the `)` closing a `$(` / `<(` / `>(` opened just before `i`, or
392
448
  // str.length when unterminated. Quote- and heredoc-aware so a `)` inside a
393
449
  // quoted string or a heredoc body never closes the span early.
@@ -422,7 +478,7 @@ function findParenClose(str, i) {
422
478
  i = j === -1 ? str.length : j + 1;
423
479
  continue;
424
480
  }
425
- if (ch === '<' && str[i + 1] === '<' && str[i + 2] !== '<') {
481
+ if (heredocBodiesSkippedInSpans && ch === '<' && str[i + 1] === '<' && str[i + 2] !== '<') {
426
482
  const tag = readHeredocTag(str, i + 2);
427
483
  heredocTags.push(tag);
428
484
  i = tag.end;
@@ -458,7 +514,34 @@ function readHeredocTag(str, i) {
458
514
  i = j === -1 ? str.length : j + 1;
459
515
  } else {
460
516
  if (str[i] === '\\') { quoted = true; i++; }
461
- while (i < str.length && !/[\s;&|<>()]/.test(str[i])) tag += str[i++];
517
+ // #5513: an unquoted tag is a bash WORD. It ends only at a metacharacter (space, tab,
518
+ // newline, `;&|<>()`): a carriage return is not one, so `EOF` + CR is the tag and the
519
+ // CRLF terminator line matches it; and a substitution inside the word belongs to it,
520
+ // taken literally (`E$(x)F` waits for the line `E$(x)F`).
521
+ while (i < str.length && !(heredocTagsAsWords ? /[ \t\n;&|<>()]/ : /[\s;&|<>()]/).test(str[i])) {
522
+ if (!heredocTagsAsWords) { tag += str[i++]; continue; }
523
+ if (str[i] === '$' && str[i + 1] === '(') {
524
+ const e = findParenClose(str, i + 2);
525
+ tag += str.slice(i, e + 1);
526
+ i = e + 1;
527
+ continue;
528
+ }
529
+ if (str[i] === '$' && str[i + 1] === '{') {
530
+ const j = str.indexOf('}', i);
531
+ const e = j === -1 ? str.length - 1 : j;
532
+ tag += str.slice(i, e + 1);
533
+ i = e + 1;
534
+ continue;
535
+ }
536
+ if (str[i] === '`') {
537
+ const j = str.indexOf('`', i + 1);
538
+ const e = j === -1 ? str.length - 1 : j;
539
+ tag += str.slice(i, e + 1);
540
+ i = e + 1;
541
+ continue;
542
+ }
543
+ tag += str[i++];
544
+ }
462
545
  }
463
546
  return { tag, quoted, stripTabs, end: i };
464
547
  }
@@ -604,6 +687,7 @@ function tokenize(str) {
604
687
  hasWord = true;
605
688
  const e = findParenClose(str, i + 2);
606
689
  nested.push(str.slice(i + 2, e));
690
+ if (expectTag && heredocTagsAsWords) buf += str.slice(i, e + 1); // #5513: the tag is taken literally
607
691
  i = e + 1;
608
692
  continue;
609
693
  }
@@ -622,6 +706,7 @@ function tokenize(str) {
622
706
  const j = str.indexOf('`', i + 1);
623
707
  const e = j === -1 ? str.length : j;
624
708
  nested.push(str.slice(i + 1, e));
709
+ if (expectTag && heredocTagsAsWords) buf += str.slice(i, e + 1); // #5513: the tag is taken literally
625
710
  i = e + 1;
626
711
  continue;
627
712
  }
@@ -656,6 +741,14 @@ function tokenize(str) {
656
741
  continue;
657
742
  }
658
743
 
744
+ // #5513: a CR is not a bash metacharacter, so inside a heredoc tag it is part of the word
745
+ // (a CRLF command's terminator line is `EOF` + CR).
746
+ if (ch === '\r' && expectTag && hasWord && heredocTagsAsWords) {
747
+ buf += ch;
748
+ i++;
749
+ continue;
750
+ }
751
+
659
752
  if (ch === ' ' || ch === '\t' || ch === '\r') {
660
753
  flush();
661
754
  i++;
@@ -752,17 +845,33 @@ function tokenize(str) {
752
845
  // Bash command scan — pass 2: per-segment evaluation
753
846
  // ---------------------------------------------------------------------------
754
847
 
755
- // `@file` (curl -d), `--flag=value`, `-Xvalue` → the operand that names the file.
756
- function normalizeOperand(text) {
848
+ // Option characters of a bundled short cluster (`-2nif`).
849
+ const CLUSTER_FLAG_CHAR_RE = /[A-Za-z0-9]/;
850
+
851
+ // True when an operand word names a secret file. `@file` (curl -d) and
852
+ // `--flag=value` are stripped to the value. In a single-dash word
853
+ // (`/^-[^-]./`, so `-2if.env` counts too) the value of a bundled cluster
854
+ // starts right after its run of option characters (#5046): `-if.env` holds
855
+ // `.env`, `-nf=.env` holds `.env` after the `=`. That value is checked as
856
+ // well as the historical two-character strip, so the verdict is never looser
857
+ // than base and costs a fixed number of `namesSecret` calls (at most three,
858
+ // whatever the word's length). The option run also swallows a value's leading
859
+ // letters and digits, so `-fapp.env` is denied although `cat app.env` is not:
860
+ // a fail-safe false positive, pinned in tests.
861
+ function operandNamesSecret(text) {
757
862
  let v = text;
758
863
  if (v.startsWith('@')) v = v.slice(1);
759
864
  if (v.startsWith('--')) {
760
865
  const eq = v.indexOf('=');
761
866
  if (eq !== -1) v = v.slice(eq + 1);
762
- } else if (/^-[A-Za-z]./.test(v)) {
763
- v = v.slice(2);
867
+ return namesSecret(v);
764
868
  }
765
- return v;
869
+ if (!/^-[^-]./.test(v)) return namesSecret(v);
870
+ if (namesSecret(v.slice(2))) return true;
871
+ let end = 1;
872
+ while (end < v.length && CLUSTER_FLAG_CHAR_RE.test(v[end])) end++;
873
+ if (end > 2 && namesSecret(v.slice(end))) return true;
874
+ return v[end] === '=' && namesSecret(v.slice(end + 1));
766
875
  }
767
876
 
768
877
  const ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
@@ -772,23 +881,83 @@ const DASH_C_RE = /^-[A-Za-z]*c$/;
772
881
 
773
882
  const GROUPING_SEPS = new Set(['(', ')', '{', '}']);
774
883
 
884
+ // How many words the option `word` occupies for wrapper `spec`: 2 when it
885
+ // takes a separate value word, else 1.
886
+ function wrapperOptionWidth(word, spec) {
887
+ if (word.startsWith('--')) {
888
+ if (word.includes('=')) return 1;
889
+ // getopt_long accepts any unambiguous prefix (`--adj 5`).
890
+ return word.length > 2 && (spec.long || []).some((l) => l.startsWith(word)) ? 2 : 1;
891
+ }
892
+ const valued = spec.short || '';
893
+ for (let i = 1; i < word.length; i++) {
894
+ if (valued.includes(word[i])) return i === word.length - 1 ? 2 : 1; // `-n5` carries its value
895
+ }
896
+ return 1;
897
+ }
898
+
899
+ // Index of the first word after a wrapper's own options, assignments and
900
+ // positional operands (`-n 5`, `-i A=1`, `-s KILL 5`, `--`).
901
+ function skipWrapperArgs(words, start, spec) {
902
+ let idx = start;
903
+ let optionsEnded = false;
904
+ while (idx < words.length) {
905
+ const text = words[idx].text;
906
+ if (spec.assigns && ASSIGNMENT_RE.test(text)) {
907
+ idx++;
908
+ } else if (!optionsEnded && text === '--') {
909
+ optionsEnded = true;
910
+ idx++;
911
+ } else if (!optionsEnded && text.length > 1 && text.startsWith('-')) {
912
+ idx += wrapperOptionWidth(text, spec);
913
+ } else {
914
+ break;
915
+ }
916
+ }
917
+ if (!spec.operands) return idx;
918
+ idx = Math.min(idx + spec.operands, words.length);
919
+ // `timeout 5 -- cmd`: some getopt builds still accept `--` after DURATION.
920
+ return idx < words.length && words[idx].text === '--' ? idx + 1 : idx;
921
+ }
922
+
775
923
  // Command base + operands after leading `VAR=val` assignments and prefix
776
- // wrappers (`sudo`, `env VAR=x`, …), or null when nothing but prefixes remain.
924
+ // wrappers with their own options (`sudo -u root`, `env -i VAR=x`,
925
+ // `timeout -s KILL 5`, `nice -n 5`, …), or null when nothing but prefixes remain.
777
926
  function resolveCommand(words) {
778
927
  let idx = 0;
779
928
  while (idx < words.length && ASSIGNMENT_RE.test(words[idx].text)) idx++;
780
929
  while (idx < words.length) {
781
- const base = lastSegment(words[idx].text).toLowerCase();
782
- if (!CMD_PREFIXES.has(base)) break;
783
- idx++;
784
- if (base === 'env') {
785
- while (idx < words.length && ASSIGNMENT_RE.test(words[idx].text)) idx++;
786
- }
930
+ const spec = WRAPPERS.get(lastSegment(words[idx].text).toLowerCase());
931
+ if (!spec) break;
932
+ idx = skipWrapperArgs(words, idx + 1, spec);
787
933
  }
788
934
  if (idx >= words.length) return null;
789
935
  return { base: lastSegment(words[idx].text).toLowerCase(), operands: words.slice(idx + 1) };
790
936
  }
791
937
 
938
+ /**
939
+ * Rescan only the command-valued -c keys selected by #5045. This deliberately
940
+ * does not resolve config files, --config-env/GIT_CONFIG_* values, or other
941
+ * executing keys (diff drivers, filters, credential helpers, ssh, gpg).
942
+ * Those are documented gaps, not a promise of complete Git config analysis.
943
+ * Global option values and subcommand arguments are data, not more -c flags.
944
+ */
945
+ function scanGitConfig(operands, depth) {
946
+ for (const config of walkGitGlobals(operands).configs) {
947
+ const eq = config.indexOf('=');
948
+ if (eq === -1) continue;
949
+ const key = config.slice(0, eq).toLowerCase();
950
+ const value = config.slice(eq + 1);
951
+ let script;
952
+ if (key.startsWith('alias.') && value.startsWith('!')) script = value.slice(1);
953
+ else if (key === 'diff.external') script = value;
954
+ else continue;
955
+ const hit = findSecretRead(script, depth + 1);
956
+ if (hit) return hit;
957
+ }
958
+ return null;
959
+ }
960
+
792
961
  // Operand indices the segment's command consumes as a NAME, never as
793
962
  // CONTENTS (#4639, #4856). Only these positions skip the operand check;
794
963
  // every other operand is still checked, so a carve-out cannot launder a read
@@ -820,26 +989,38 @@ function isListedFlag(text, flags) {
820
989
  return /^-[A-Za-z]+$/.test(text) && [...text.slice(1)].every((ch) => flags.has(`-${ch}`));
821
990
  }
822
991
 
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) {
992
+ // Walks git's global options (see GIT_GLOBAL_VALUE_OPTIONS). `sub` is the
993
+ // subcommand's index, or -1 when an unlisted option (or the end of the words)
994
+ // comes first; `configs` holds every `-c` value seen on the way, in order.
995
+ // Past an unlisted option the subcommand cannot be located, so `configs`
996
+ // conservatively takes the word after every later `-c` as well: a read
997
+ // still needs a command-valued key, so a harmless value stays allowed.
998
+ function walkGitGlobals(operands) {
999
+ const configs = [];
826
1000
  for (let k = 0; k < operands.length; k++) {
827
1001
  const t = operands[k].text;
828
- if (!t.startsWith('-')) return k;
829
- if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) { k++; continue; }
1002
+ if (!t.startsWith('-')) return { sub: k, configs };
1003
+ if (GIT_GLOBAL_VALUE_OPTIONS.has(t)) {
1004
+ if (t === '-c' && k + 1 < operands.length) configs.push(operands[k + 1].text);
1005
+ k++;
1006
+ continue;
1007
+ }
830
1008
  if (GIT_GLOBAL_FLAGS.has(t)) continue;
831
1009
  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;
1010
+ if (t.startsWith('--') && eq !== -1 && GIT_GLOBAL_EQUALS_FORM_OPTIONS.has(t.slice(0, eq))) continue;
1011
+ for (let j = k + 1; j + 1 < operands.length; j++) {
1012
+ if (operands[j].text === '-c') configs.push(operands[++j].text);
1013
+ }
1014
+ return { sub: -1, configs };
834
1015
  }
835
- return -1;
1016
+ return { sub: -1, configs };
836
1017
  }
837
1018
 
838
1019
  // #4856: the pathspec operands of a GIT_PATHSPEC_SUBCOMMANDS invocation. git
839
1020
  // permutes options, so one may follow a pathspec; after `--` every word is a
840
1021
  // pathspec. The subcommand is matched case-sensitively, as git looks it up.
841
1022
  function gitPathspecIndices(operands) {
842
- const sub = gitSubcommandIndex(operands);
1023
+ const { sub } = walkGitGlobals(operands);
843
1024
  const spec = sub === -1 ? undefined : GIT_PATHSPEC_SUBCOMMANDS.get(operands[sub].text);
844
1025
  if (!spec) return new Set();
845
1026
  const pathspecs = new Set();
@@ -910,6 +1091,26 @@ function precedingOp(s, bySeg, sepAfter) {
910
1091
  return { op, prevSeg: p };
911
1092
  }
912
1093
 
1094
+ // #5513: the same scan under the other readings of a heredoc: bash 3.2.57's `$( )` span
1095
+ // (bodies not skipped) and the earlier tag reading (tag ends at any whitespace). Only a
1096
+ // command that opens a heredoc can read differently, so the extra passes are skipped for
1097
+ // every other command.
1098
+ function findSecretReadUnderOtherReadings(command) {
1099
+ if (!command.includes('<<')) return null;
1100
+ for (const [bodiesSkipped, tagsAsWords] of [[false, true], [true, false]]) {
1101
+ heredocBodiesSkippedInSpans = bodiesSkipped;
1102
+ heredocTagsAsWords = tagsAsWords;
1103
+ try {
1104
+ const hit = findSecretRead(command, 0);
1105
+ if (hit) return hit;
1106
+ } finally {
1107
+ heredocBodiesSkippedInSpans = true;
1108
+ heredocTagsAsWords = true;
1109
+ }
1110
+ }
1111
+ return null;
1112
+ }
1113
+
913
1114
  // Returns the offending token text, or null.
914
1115
  function findSecretRead(command, depth) {
915
1116
  const { tokens, nested } = tokenize(command);
@@ -988,7 +1189,7 @@ function findSecretRead(command, depth) {
988
1189
  const hit = findSecretRead(src, depth + 1);
989
1190
  if (hit) return hit;
990
1191
  }
991
- } else if (namesSecret(normalizeOperand(w.text))) return w.text;
1192
+ } else if (operandNamesSecret(w.text)) return w.text;
992
1193
  }
993
1194
  continue;
994
1195
  }
@@ -1007,12 +1208,18 @@ function findSecretRead(command, depth) {
1007
1208
  // `bash .env` (file mode) is caught by the operand check below.
1008
1209
  }
1009
1210
 
1211
+ // Git -c can carry a shell script instead of a file-name operand (#5045).
1212
+ if ((base === 'git' || base === 'git.exe') && depth < MAX_NESTING_DEPTH) {
1213
+ const hit = scanGitConfig(operands, depth);
1214
+ if (hit) return hit;
1215
+ }
1216
+
1010
1217
  if (NON_READING_COMMANDS.has(base)) continue;
1011
1218
 
1012
1219
  const nameOnly = nameOnlyOperandIndices(base, operands);
1013
1220
  for (let k = 0; k < operands.length; k++) {
1014
1221
  if (nameOnly.has(k)) continue;
1015
- if (namesSecret(normalizeOperand(operands[k].text))) return operands[k].text;
1222
+ if (operandNamesSecret(operands[k].text)) return operands[k].text;
1016
1223
  }
1017
1224
  }
1018
1225
  return null;
@@ -1030,21 +1237,6 @@ function isDebuggerAgent(value) {
1030
1237
  return value.slice(value.lastIndexOf(':') + 1) === DEBUGGER_AGENT_NAME;
1031
1238
  }
1032
1239
 
1033
- // `timeout [flags] DURATION cmd …` is not in CMD_PREFIXES because its first
1034
- // operand is a duration, not the command. Returns the command that follows it.
1035
- function unwrapTimeout(cmd) {
1036
- let { base, operands } = cmd;
1037
- while (base === 'timeout') {
1038
- let k = 0;
1039
- while (k < operands.length && operands[k].text.startsWith('-')) k++;
1040
- k++; // the DURATION operand
1041
- const rest = resolveCommand(operands.slice(k));
1042
- if (!rest) return null;
1043
- ({ base, operands } = rest);
1044
- }
1045
- return { base, operands };
1046
- }
1047
-
1048
1240
  // Returns the `--agent` value text of a debugger launch, or null.
1049
1241
  function findDebuggerCliSpawn(command, depth) {
1050
1242
  const { tokens, nested } = tokenize(command);
@@ -1076,8 +1268,7 @@ function findDebuggerCliSpawn(command, depth) {
1076
1268
  }
1077
1269
  if (!words.length) continue;
1078
1270
 
1079
- const first = resolveCommand(words);
1080
- const cmd = first && unwrapTimeout(first);
1271
+ const cmd = resolveCommand(words);
1081
1272
  if (!cmd) continue;
1082
1273
  const { base, operands } = cmd;
1083
1274
 
@@ -1190,7 +1381,7 @@ function scanXargsPipe(operands, s, bySeg, sepAfter, depth) {
1190
1381
  const prevCmd = resolveCommand((bySeg.get(prevSeg) || []).filter((t) => t.kind === 'word'));
1191
1382
  if (!prevCmd) return null;
1192
1383
  for (const w of prevCmd.operands) {
1193
- if (namesSecret(normalizeOperand(w.text))) return w.text;
1384
+ if (operandNamesSecret(w.text)) return w.text;
1194
1385
  }
1195
1386
  return null;
1196
1387
  }
@@ -1365,7 +1556,7 @@ process.stdin.on('end', () => {
1365
1556
  const command = typeof data.tool_input.command === 'string' ? data.tool_input.command : '';
1366
1557
  if (command === '') allow(undefined);
1367
1558
  if (command.length > MAX_COMMAND_LENGTH) emitBlock('command-too-large', tool, '');
1368
- const hit = findSecretRead(command, 0);
1559
+ const hit = findSecretRead(command, 0) || findSecretReadUnderOtherReadings(command);
1369
1560
  if (hit !== null) emitBlock('secret-read', tool, hit);
1370
1561
  const spawn = findDebuggerCliSpawn(command, 0);
1371
1562
  if (spawn !== null) emitBlock('debugger-cli-spawn', tool, spawn);