@opengsd/gsd-core 1.13.0 → 1.15.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 (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
@@ -26,12 +26,24 @@
26
26
  // .env, .secrets, and .env.<suffix> — EXCEPT .env.example / .env.sample /
27
27
  // .env.template / .env.dist, which are the non-secret templates GSD's own
28
28
  // phase prompt tells executors to read.
29
- // Stated cost: this is narrower than the retired `Read(.env.*)` rule — a
30
- // real secret stored in `.env.example` is not protected.
29
+ // Stated cost (#4580): the exemption matches the token's FINAL EXTENSION,
30
+ // not the whole name, so the trusted set is `.env.<anything>.example` /
31
+ // `.sample` / `.template` / `.dist` — an unbounded family, not four fixed
32
+ // names. A real secret named `.env.prod-real-secrets.example` is NOT
33
+ // protected, and renaming any secret to end in one of those four
34
+ // extensions bypasses the guard across Read, Grep and Bash alike. This is
35
+ // the deliberate cost of #4580, which fixed the prior whole-name
36
+ // comparison wrongly refusing committed, secret-free templates like
37
+ // `.env.local.example`.
31
38
  // A token containing `:` is also tested on the part after its LAST `:`,
32
39
  // so `git show HEAD:.env`, `origin/main:config/.env` and `C:\proj\.env`
33
- // are caught without git-specific parsing. No whitespace trimming: the
34
- // commit message `fix: .env parsing` yields ` .env parsing`, not a name.
40
+ // are caught without git-specific parsing. Leading/interior whitespace is
41
+ // still NOT trimmed: the commit message `fix: .env parsing` yields
42
+ // ` .env parsing`, which is prose, not a name. TRAILING dots and spaces ARE
43
+ // stripped from the basename before classification (`.env.`, `.env..`,
44
+ // `.env `, `.env. ` all normalize to `.env`), because Win32 strips trailing
45
+ // dots and spaces from each path component, so these are aliases for the
46
+ // same on-disk file, not distinct names.
35
47
  //
36
48
  // Bash analysis is a two-pass token scan, not a shell:
37
49
  // pass 1 tokenizes with quote state, comments, redirect operators (with fd
@@ -74,7 +86,12 @@
74
86
  // on plain commands, without arming the compound-`cd` prompt". Writes to
75
87
  // secret files are out of scope (Write/Edit were never gated). Commands
76
88
  // over 1 MiB are denied outright (`command-too-large`) rather than
77
- // scanned partially or waved through.
89
+ // scanned partially or waved through. (#4639 adds one more, by design: the
90
+ // value of `--env-file` under a container runtime is exempt, so the
91
+ // container's own command can print the interpolated environment
92
+ // (`alpine printenv`, `docker compose config`) — the same exposure class
93
+ // as the pre-existing volume-mount gap (`-v .env:/s`); the flag's value
94
+ // itself is a name, never contents.)
78
95
  //
79
96
  // Triggers on: Read, Grep, Bash tool calls (Kimi: ReadFile, Grep, Shell)
80
97
  // Action: BLOCK (decision: 'block', exit 2) — codes secret-read |
@@ -86,6 +103,7 @@
86
103
  'use strict';
87
104
 
88
105
  const { HOOK_ON_CRASH, allow, deny, crash } = require('./lib/hook-exit.js');
106
+ const { finalExtension, normalizeWindowsBasename, lastSegment } = require('./lib/filename-classification.js');
89
107
 
90
108
  // Fail open on a hook-internal error (see header). Declared ONCE so the
91
109
  // outer catch states its policy explicitly (#3911).
@@ -114,6 +132,15 @@ const NON_READING_COMMANDS = new Set([
114
132
  'basename', 'dirname', 'realpath', 'file', 'echo', 'printf',
115
133
  ]);
116
134
 
135
+ // #4639: container runtimes take `--env-file <file>` — the runtime opens the
136
+ // file itself, interpolates it into the container environment, and returns
137
+ // nothing to the agent, so the flag's VALUE is a name, never contents. The
138
+ // same category NON_READING_COMMANDS encodes, expressed as a flag value. Only
139
+ // the flag's value under these runtimes is exempt; every other operand in the
140
+ // segment is still checked, so the carve-out cannot launder a read.
141
+ const CONTAINER_RUNTIMES = new Set(['docker', 'docker-compose', 'podman', 'nerdctl']);
142
+ const ENV_FILE_FLAG_RE = /^--env-file(=|$)/;
143
+
117
144
  // Shell interpreters that run a script from `-c`, a file operand, or stdin
118
145
  // (heredoc / here-string / piped `echo`|`printf`). `su` is here for its `-c`
119
146
  // form (`su [user] -c 'cmd'`); a bare `su user` resolves to file mode, which
@@ -148,21 +175,18 @@ const GLOB_PROBES = [
148
175
  // ---------------------------------------------------------------------------
149
176
 
150
177
  function isSecretBasename(name) {
151
- if (name === '.env' || name === '.secrets') return true;
152
- if (name.startsWith('.env.')) {
153
- const suffix = name.slice('.env.'.length);
154
- return suffix !== '' && !NON_SECRET_ENV_SUFFIXES.has(suffix.toLowerCase());
178
+ // Win32 strips trailing dots/spaces per path component, so `.env.`,
179
+ // `.env ` etc. resolve to the real `.env` on Windows — normalize FIRST so
180
+ // those aliases can't bypass classification.
181
+ const n = normalizeWindowsBasename(name);
182
+ if (n === '.env' || n === '.secrets') return true;
183
+ if (n.startsWith('.env.')) {
184
+ const suffix = n.slice('.env.'.length);
185
+ return suffix !== '' && !NON_SECRET_ENV_SUFFIXES.has(finalExtension(suffix).toLowerCase());
155
186
  }
156
187
  return false;
157
188
  }
158
189
 
159
- // Last `/`- or `\`-separated segment, ignoring trailing separators.
160
- function lastSegment(tok) {
161
- const s = tok.replace(/[\\/]+$/, '');
162
- const i = Math.max(s.lastIndexOf('/'), s.lastIndexOf('\\'));
163
- return i === -1 ? s : s.slice(i + 1);
164
- }
165
-
166
190
  // True when the token's basename — or the basename of the part after its
167
191
  // last `:` (git `<ref>:<path>`, Windows drive) — is a secret name. Folded to
168
192
  // lower case once at the top so `.ENV` / `.Secrets` match on the
@@ -247,7 +271,19 @@ function globAltSelectsSecret(alt) {
247
271
  if (/^[*?]+$/.test(alt)) return false; // pure wildcard: equivalent to no glob
248
272
  const wild = alt.search(/[*?[]/);
249
273
  const lit = wild === -1 ? alt : alt.slice(0, wild);
250
- if (lit.startsWith('.env.')) return true;
274
+ // #4580: when there is no wildcard, `alt` (== `lit`) is a WHOLE literal
275
+ // filename, so classify it exactly the same way Read/Bash do (by its
276
+ // FINAL extension, via isSecretBasename) rather than by a `.env.`-prefix
277
+ // heuristic — that heuristic mis-blocked multi-dot templates like
278
+ // `.env.local.example`. When a wildcard IS present, `lit` is only a
279
+ // PARTIAL literal prefix (`.env.local.exam*` can still select
280
+ // `.env.local`), which cannot be classified exactly, so the original
281
+ // conservative prefix rule stays.
282
+ if (wild === -1) {
283
+ if (isSecretBasename(lit)) return true;
284
+ } else if (lit.startsWith('.env.')) {
285
+ return true;
286
+ }
251
287
  if (lit !== '' && ('.env.'.startsWith(lit) || '.secrets'.startsWith(lit))) return true;
252
288
  let re;
253
289
  try {
@@ -260,10 +296,14 @@ function globAltSelectsSecret(alt) {
260
296
 
261
297
  // Returns null (allowed), 'secret-read', or 'glob-too-complex'.
262
298
  function classifyGrepGlob(glob) {
263
- const segIdx = glob.replace(/\/+$/, '').lastIndexOf('/');
299
+ // Segment via the SAME `lastSegment` helper Read/Bash use (namesSecret),
300
+ // rather than a hand-rolled forward-slash-only split — the two used to
301
+ // diverge on a backslash-bearing glob (`config\.env`), which `lastSegment`
302
+ // reduces to `.env` but a `/`-only split left untouched, letting it escape
303
+ // this arm's predicate while Read/Bash still blocked it.
264
304
  // Case-fold the last segment (GLOB_PROBES are lower case) so `.ENV*` and
265
305
  // `*.ENV` select the secret namespace on case-insensitive filesystems.
266
- const segment = (segIdx === -1 ? glob : glob.slice(segIdx + 1)).toLowerCase();
306
+ const segment = lastSegment(glob).toLowerCase();
267
307
  const alts = expandBraces(segment);
268
308
  if (alts === null) return 'glob-too-complex';
269
309
  return alts.some(globAltSelectsSecret) ? 'secret-read' : null;
@@ -814,7 +854,19 @@ function findSecretRead(command, depth) {
814
854
 
815
855
  if (NON_READING_COMMANDS.has(base)) continue;
816
856
 
857
+ // #4639: exempt ONLY the value of `--env-file`, and only when the
858
+ // segment's command word is a container runtime. A bare `--env-file`
859
+ // consumes the next operand; `--env-file=<value>` is a single word.
860
+ const envFileExempt = CONTAINER_RUNTIMES.has(base);
861
+ let skipNext = false;
817
862
  for (const w of operands) {
863
+ if (envFileExempt) {
864
+ if (skipNext) { skipNext = false; continue; }
865
+ if (ENV_FILE_FLAG_RE.test(w.text)) {
866
+ if (!w.text.includes('=')) skipNext = true; // bare flag consumes the next operand
867
+ continue;
868
+ }
869
+ }
818
870
  if (namesSecret(normalizeOperand(w.text))) return w.text;
819
871
  }
820
872
  }
@@ -42,6 +42,10 @@ if (require.main === module) {
42
42
  const { isSemverNewer } = require('../gsd-core/bin/lib/semver-compare.cjs');
43
43
  const { PACKAGE_NAME, updateCacheFileName } = require('../gsd-core/bin/lib/package-identity.cjs');
44
44
  const { normalizeStateStatus } = require('../gsd-core/bin/lib/state-document.cjs');
45
+ const {
46
+ renderBracketPhaseDisplay,
47
+ renderBracketMilestoneDisplay,
48
+ } = require('../gsd-core/bin/lib/phase-id-display.cjs');
45
49
  // #2850: reuse the existing workstream resolution seams rather than
46
50
  // re-implementing CLI>env>store precedence or path construction inline.
47
51
  // peekActiveWorkstream is the read-only sibling of the store-tier lookup
@@ -359,16 +363,26 @@ function renderProgressBar(percent) {
359
363
  * Progress bar is opt-in: appended to the milestone segment only when
360
364
  * progress.percent is present in frontmatter; absent → empty string.
361
365
  */
362
- function formatGsdState(s) {
366
+ function formatGsdState(s, opts = {}) {
363
367
  // #2850: workstream mode with nothing resolvable — an observable signal,
364
368
  // never silent emptiness (distinguishes from "GSD isn't installed here").
365
369
  if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
366
370
 
367
371
  const parts = [];
372
+ const bracket = opts.convention === 'bracket';
373
+ const bracketMilestone = bracket
374
+ ? renderBracketMilestoneDisplay(s.milestone, opts.projectCode)
375
+ : null;
376
+ const phaseLabel = (phase) => {
377
+ const display = bracket
378
+ ? renderBracketPhaseDisplay(s.milestone, phase, opts.projectCode)
379
+ : null;
380
+ return display ?? `Phase ${phase}`;
381
+ };
368
382
 
369
383
  // Milestone segment: version + name + (opt-in) progress bar
370
384
  if (s.milestone || s.milestoneName) {
371
- const ver = s.milestone || '';
385
+ const ver = bracketMilestone ?? s.milestone ?? '';
372
386
  const name = (s.milestoneName && s.milestoneName !== 'milestone') ? s.milestoneName : '';
373
387
  const bar = renderProgressBar(s.percent);
374
388
  const pieces = [ver, name, bar].filter(Boolean);
@@ -384,7 +398,8 @@ function formatGsdState(s) {
384
398
  // stage = whichever lifecycle status was written by the orchestrator
385
399
  // (discussing / planning / executing / verifying)
386
400
  const stage = s.status || '';
387
- parts.push(stage ? `Phase ${s.activePhase} ${stage}` : `Phase ${s.activePhase}`);
401
+ const phase = phaseLabel(s.activePhase);
402
+ parts.push(stage ? `${phase} ${stage}` : phase);
388
403
  } else if (s.nextAction && phasesStr) {
389
404
  // Scene 2: idle + a recommended next command is visible to the user.
390
405
  // Surfaces "what to run next" without the user opening STATE.md.
@@ -402,9 +417,16 @@ function formatGsdState(s) {
402
417
  // earlier so no existing project's status-line changes shape.
403
418
  if (s.status) parts.push(s.status);
404
419
  if (s.phaseNum && s.phaseTotal) {
420
+ const bracketPhase = bracket
421
+ ? renderBracketPhaseDisplay(s.milestone, s.phaseNum, opts.projectCode)
422
+ : null;
405
423
  const phase = s.phaseName
406
- ? `${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
407
- : `ph ${s.phaseNum}/${s.phaseTotal}`;
424
+ ? bracketPhase
425
+ ? `${bracketPhase} ${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
426
+ : `${s.phaseName} (${s.phaseNum}/${s.phaseTotal})`
427
+ : bracketPhase
428
+ ? `${bracketPhase} (${s.phaseNum}/${s.phaseTotal})`
429
+ : `ph ${s.phaseNum}/${s.phaseTotal}`;
408
430
  parts.push(phase);
409
431
  }
410
432
  }
@@ -449,13 +471,17 @@ function contextTokenSuffix(currentUsage) {
449
471
  // --- Compact state format (opt-in) ---------------------------------------------
450
472
 
451
473
  /**
452
- * Collapse GSD's free-text status (often a multi-sentence narrative) to a
453
- * single keyword, built on the canonical normalizer (#2162 approval
454
- * condition): normalizeStateStatus() in state-document.cjs owns the status
455
- * vocabulary (discussing / planning / executing / verifying / completed /
456
- * paused) so the two can't drift. "paused" — the canonical stuck state — is
457
- * uppercased to PAUSED, the one state worth shouting about. Statuses the
458
- * normalizer passes through unrecognized fall back to their first word,
474
+ * Collapse GSD's status value to a single keyword, built on the canonical
475
+ * normalizer (#2162 approval condition): normalizeStateStatus() in
476
+ * state-document.cjs owns the status vocabulary (discussing / planning /
477
+ * executing / verifying / completed / paused) so the two can't drift.
478
+ * #4186: the normalizer recognizes the DECLARED vocabulary by anchored
479
+ * whole-field match — vocabulary values (the state writer persists tokens)
480
+ * collapse to their keyword; free-text narratives are no longer
481
+ * keyword-guessed from substrings (a `.planning/` mention in non-English
482
+ * prose used to render `planning`), and pass through unrecognized to the
483
+ * first-word fallback below. "paused" — the canonical stuck state — is
484
+ * uppercased to PAUSED, the one state worth shouting about. The fallback is
459
485
  * capped at 16 chars so a rogue STATE.md can't blow up the line.
460
486
  * Returns null for empty input.
461
487
  */
@@ -486,17 +512,24 @@ function shortGsdStatus(status) {
486
512
  * default format — and collapses narrative statuses via shortGsdStatus().
487
513
  * The default "full" format is untouched.
488
514
  */
489
- function formatGsdStateCompact(s) {
515
+ function formatGsdStateCompact(s, opts = {}) {
490
516
  // #2850: mirrors formatGsdState's observable "nothing resolvable" signal.
491
517
  if (s.noActiveWorkstream) return NO_ACTIVE_WORKSTREAM_LABEL;
492
518
 
493
519
  const parts = [];
520
+ const bracket = opts.convention === 'bracket';
521
+ const bracketMilestone = bracket
522
+ ? renderBracketMilestoneDisplay(s.milestone, opts.projectCode)
523
+ : null;
494
524
 
495
- if (s.milestone) parts.push(s.milestone);
525
+ if (s.milestone) parts.push(bracketMilestone ?? s.milestone);
496
526
 
497
527
  const phaseId = s.activePhase || s.phaseNum;
498
528
  if (phaseId) {
499
- parts.push(s.phaseTotal ? `P${phaseId}/${s.phaseTotal}` : `P${phaseId}`);
529
+ const bracketPhase = bracket
530
+ ? renderBracketPhaseDisplay(s.milestone, phaseId, opts.projectCode)
531
+ : null;
532
+ parts.push(bracketPhase ?? (s.phaseTotal ? `P${phaseId}/${s.phaseTotal}` : `P${phaseId}`));
500
533
  }
501
534
 
502
535
  // Scene exclusivity mirrors formatGsdState's if/else chain: an in-flight
@@ -743,7 +776,7 @@ function formatStateFreshness(fresh) {
743
776
  * entry points — this collapses both onto one resolver.
744
777
  *
745
778
  * @param {object} cfg — parsed .planning/config.json (readGsdConfig())
746
- * @returns {{ showLastCommand: boolean, position: 'end'|'front', stateFormat: 'full'|'compact', showGit: boolean, showStateFreshness: boolean }}
779
+ * @returns {{ showLastCommand: boolean, position: 'end'|'front', stateFormat: 'full'|'compact', showGit: boolean, showStateFreshness: boolean, convention: string|null, projectCode: string|null }}
747
780
  */
748
781
  function resolveStatuslineOptions(cfg) {
749
782
  const showLastCommand = getConfigValue(cfg, 'statusline.show_last_command') === true;
@@ -755,7 +788,22 @@ function resolveStatuslineOptions(cfg) {
755
788
  const stateFormat = getConfigValue(cfg, 'statusline.state_format') === 'compact' ? 'compact' : 'full';
756
789
  const showGit = getConfigValue(cfg, 'statusline.show_git') === true;
757
790
  const showStateFreshness = getConfigValue(cfg, 'statusline.show_state_freshness') === true;
758
- return { showLastCommand, position, stateFormat, showGit, showStateFreshness };
791
+ const convention = getConfigValue(cfg, 'phase_id_convention') === 'bracket'
792
+ ? 'bracket'
793
+ : null;
794
+ const projectCodeValue = getConfigValue(cfg, 'project_code');
795
+ const projectCode = typeof projectCodeValue === 'string' && projectCodeValue !== ''
796
+ ? projectCodeValue
797
+ : null;
798
+ return {
799
+ showLastCommand,
800
+ position,
801
+ stateFormat,
802
+ showGit,
803
+ showStateFreshness,
804
+ convention,
805
+ projectCode,
806
+ };
759
807
  }
760
808
 
761
809
  // --- stdin ------------------------------------------------------------------
@@ -930,7 +978,9 @@ function runStatusline() {
930
978
  // freshness git spawn here would spend a subprocess on discarded output.
931
979
  if (!task) {
932
980
  const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
933
- gsdStateStr = options.stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
981
+ gsdStateStr = options.stateFormat === 'compact'
982
+ ? formatGsdStateCompact(state, options)
983
+ : formatGsdState(state, options);
934
984
  }
935
985
 
936
986
  // Output
@@ -1038,6 +1088,7 @@ module.exports = {
1038
1088
  STATE_HEAD_ADVISORY_COMMITS, isValidStateHeadStamp,
1039
1089
  readStateHeadCommits, parseRevListCounts, deriveStateFreshness,
1040
1090
  formatStateFreshness, resolveStatuslineOptions,
1091
+ renderBracketPhaseDisplay, renderBracketMilestoneDisplay,
1041
1092
  };
1042
1093
 
1043
1094
  /**
@@ -1055,7 +1106,15 @@ function renderStatusline(data) {
1055
1106
  // key from reaching only one of them.
1056
1107
  let lastCmdSuffix = '';
1057
1108
  let gitSuffix = '';
1058
- let options = { showLastCommand: false, position: 'end', stateFormat: 'full', showGit: false, showStateFreshness: false };
1109
+ let options = {
1110
+ showLastCommand: false,
1111
+ position: 'end',
1112
+ stateFormat: 'full',
1113
+ showGit: false,
1114
+ showStateFreshness: false,
1115
+ convention: null,
1116
+ projectCode: null,
1117
+ };
1059
1118
  try {
1060
1119
  const cfg = readGsdConfig(dir);
1061
1120
  options = resolveStatuslineOptions(cfg);
@@ -1071,7 +1130,9 @@ function renderStatusline(data) {
1071
1130
  } catch (e) { /* swallow */ }
1072
1131
 
1073
1132
  const state = readGsdState(dir, { stateFreshness: options.showStateFreshness }) || {};
1074
- const gsdStateStr = options.stateFormat === 'compact' ? formatGsdStateCompact(state) : formatGsdState(state);
1133
+ const gsdStateStr = options.stateFormat === 'compact'
1134
+ ? formatGsdStateCompact(state, options)
1135
+ : formatGsdState(state, options);
1075
1136
  const middle = gsdStateStr ? `\x1b[2m${gsdStateStr}\x1b[0m` : null;
1076
1137
  return composeStatusline({ model, ctx: '', middle, dirname, lastCmdSuffix, gitSuffix, position: options.position });
1077
1138
  }
@@ -16,6 +16,21 @@ set -euo pipefail
16
16
  # Idempotent and failure-proof by construction: unset vars expand to "" (a
17
17
  # no-op rm -f target), and `|| true` guarantees the trap itself never changes
18
18
  # the script's exit status.
19
+ # Subprocess exit statuses, pre-initialised so they can never be inherited from
20
+ # the ambient environment. Each is captured as `... || VAR=$?`, which assigns
21
+ # ONLY on the failure branch; on success the variable keeps whatever it already
22
+ # held, and `${VAR:-0}` defaults only when unset or empty. So an EXPORTED
23
+ # CONFIG_STATUS / CMD_STATUS / CLASSIFY_STATUS — from a CI wrapper, a .envrc, or
24
+ # another hook — survived into the success path and was read as "the subprocess
25
+ # failed". Measured: `CLASSIFY_STATUS=3 git commit -m "nope: bad"` printed
26
+ # "validator disabled for this call" and exited 0, silently accepting a
27
+ # non-conforming commit. Same for CONFIG_STATUS and CMD_STATUS. Found by the
28
+ # security review of #4429; the gate is fail-open by design on a genuine
29
+ # subprocess failure (#3838), which is exactly what made this bypass quiet.
30
+ CONFIG_STATUS=0
31
+ CMD_STATUS=0
32
+ CLASSIFY_STATUS=0
33
+
19
34
  ENABLED_ERR=""
20
35
  CMD_ERR=""
21
36
  CLASSIFY_ERR=""
@@ -61,6 +76,12 @@ if [ -f .planning/config.json ]; then
61
76
  process.exit(3);
62
77
  }
63
78
  " 2>"$ENABLED_ERR") || CONFIG_STATUS=$?
79
+ # Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
80
+ # assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
81
+ # already held — and an EXPORTED variable of this name is inherited from the
82
+ # ambient environment. `${VAR:-0}` defaults only when unset/empty, so
83
+ # `CONFIG_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
84
+ # silently accepting a non-conforming commit. Found by review of #4429.
64
85
  CONFIG_STATUS=${CONFIG_STATUS:-0}
65
86
  if [ "$CONFIG_STATUS" != "0" ]; then
66
87
  # Could not determine the opt-in flag at all (node missing, JSON parse
@@ -69,7 +90,11 @@ if [ -f .planning/config.json ]; then
69
90
  echo "gsd-validate-commit.sh: could not read .planning/config.json (opt-in check) — validator disabled for this call. $(cat "$ENABLED_ERR")" >&2
70
91
  exit 0
71
92
  fi
72
- ENABLED=$(printf '%s\n' "$CONFIG_OUT" | head -1)
93
+ # Pure parameter expansion, not `printf ... | head -1`: same SIGPIPE race
94
+ # class as the SUBJECT extraction below (`echo "$MSG" | head -1`) — CONFIG_OUT
95
+ # is multi-line whenever extra commit types are configured, and `head -1`
96
+ # closing early can SIGPIPE `printf` under `set -euo pipefail`.
97
+ ENABLED="${CONFIG_OUT%%$'\n'*}"
73
98
  if [ "$ENABLED" != "1" ]; then exit 0; fi
74
99
  # Remaining lines (if any) are the sanitized, deduped configured commit
75
100
  # types beyond the 10 built-ins (#3811). Read into a bash-3.2-safe array —
@@ -99,6 +124,12 @@ CMD=$(echo "$INPUT" | node -e "
99
124
  }
100
125
  });
101
126
  " 2>"$CMD_ERR") || CMD_STATUS=$?
127
+ # Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
128
+ # assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
129
+ # already held — and an EXPORTED variable of this name is inherited from the
130
+ # ambient environment. `${VAR:-0}` defaults only when unset/empty, so
131
+ # `CMD_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
132
+ # silently accepting a non-conforming commit. Found by review of #4429.
102
133
  CMD_STATUS=${CMD_STATUS:-0}
103
134
  if [ "$CMD_STATUS" != "0" ]; then
104
135
  # Could not extract tool_input.command at all (node missing, malformed
@@ -123,6 +154,12 @@ GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" node -e "
123
154
  process.exit(3);
124
155
  }
125
156
  " "$CMD" 2>"$CLASSIFY_ERR" || CLASSIFY_STATUS=$?
157
+ # Pre-initialised, NOT left to `${...:-0}` alone: the capture below only
158
+ # assigns on the `||` branch, so on SUCCESS the variable keeps whatever it
159
+ # already held — and an EXPORTED variable of this name is inherited from the
160
+ # ambient environment. `${VAR:-0}` defaults only when unset/empty, so
161
+ # `CLASSIFY_STATUS=3 git commit …` made this hook print "validator disabled" and exit 0,
162
+ # silently accepting a non-conforming commit. Found by review of #4429.
126
163
  CLASSIFY_STATUS=${CLASSIFY_STATUS:-0}
127
164
  if [ "$CLASSIFY_STATUS" != "0" ] && [ "$CLASSIFY_STATUS" != "1" ]; then
128
165
  # 0 = is a git commit (validate below); 1 = genuinely not a git commit
@@ -214,7 +251,24 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
214
251
  # the message — the window a guard must use when the token it scans for
215
252
  # is also legal English inside a commit message, but may legally appear
216
253
  # on EITHER side of the message on the command line.
217
- MSG_SUFFIX="${CMD#*"$MSG_MATCH"}"
254
+ # Indexed, not searched (#4492). `${CMD#*"$MSG_MATCH"}` is quadratic in
255
+ # the message: bash walks every prefix length and compares the whole
256
+ # matched literal at each one, and MSG_MATCH is BASH_REMATCH[0] — the
257
+ # entire `-m "..."` — so the cost grows with the thing being scanned.
258
+ # Measured on the path EVERY commit takes (conforming and non-conforming
259
+ # cost the same): 10.0 s at a 64 KB message, 22.0 s at 96 KB, 30.2 s at
260
+ # 112 KB. Sizes stop there deliberately — a single argument above Linux's
261
+ # MAX_ARG_STRLEN (131072 on a 4 KB-page kernel) never reaches this code
262
+ # at all, because execve fails and the hook fails open, so a larger
263
+ # "measurement" would be timing the wrong thing.
264
+ #
265
+ # MSG_PREFIX above has already located the match, so the suffix is
266
+ # arithmetic rather than a search: skip the prefix and the match. This
267
+ # removes the quadratic SEARCH; the expansion still counts characters and
268
+ # materialises a substring, so it is linear in the command, not O(1).
269
+ # Same first-occurrence assumption both expansions here always made —
270
+ # MSG_MATCH is a literal substring of CMD by construction.
271
+ MSG_SUFFIX="${CMD:$(( ${#MSG_PREFIX} + ${#MSG_MATCH} ))}"
218
272
  # LINE CONTINUATIONS ARE NOT SEPARATORS (review of #3816, rounds 8 and 9).
219
273
  # `git commit \` newline ` -m "$(cat <<'EOF' …` is an ordinary way to
220
274
  # spread an invocation over lines, and every guard below reads a newline in
@@ -521,13 +575,22 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
521
575
  SUBJECT=$(GIT_CMD_LIB="$HOOK_DIR/lib/git-cmd.js" MSG="$MSG" node -e "
522
576
  const {resolveCommitSubject}=require(process.env.GIT_CMD_LIB);
523
577
  process.stdout.write(resolveCommitSubject(process.env.MSG));
524
- " 2>/dev/null) || SUBJECT=$(echo "$MSG" | head -1)
578
+ " 2>/dev/null) || SUBJECT="${MSG%%$'\n'*}"
525
579
  else
526
- SUBJECT=$(echo "$MSG" | head -1)
580
+ # Pure parameter expansion, not `echo "$MSG" | head -1`: that pipeline
581
+ # raced a SIGPIPE under `set -euo pipefail` whenever $MSG had a body
582
+ # (the common case) — `head -1` can close its read end as soon as it
583
+ # has the first line, and if `echo`'s write lands after that close,
584
+ # `echo` dies with signal 13 (exit 141), which is NOT suppressed by
585
+ # `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
588
+ # `head -1` for single-line, multi-line, and trailing-newline input.
589
+ SUBJECT="${MSG%%$'\n'*}"
527
590
  fi
528
591
  # Single source of truth for the accepted commit-type list (#3811): the
529
592
  # 10 built-ins plus whatever passed the safe-token filter above. Both the
530
- # regex alternation and the human-readable error text below are derived
593
+ # membership test and the human-readable error text below are derived
531
594
  # from this ONE array — no hand-synced second copy.
532
595
  #
533
596
  # The `"${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}"` form (not
@@ -538,7 +601,6 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
538
601
  # /bin/bash 3.2.57 on macOS. The `${arr[@]+word}` form is the
539
602
  # nounset-safe idiom for "expand if set, empty otherwise" on empty arrays.
540
603
  COMMIT_TYPES=("${BUILTIN_COMMIT_TYPES[@]}" "${EXTRA_COMMIT_TYPES[@]+"${EXTRA_COMMIT_TYPES[@]}"}")
541
- COMMIT_TYPE_ALT=$(IFS='|'; echo "${COMMIT_TYPES[*]}")
542
604
  COMMIT_TYPE_LIST=$(printf '%s, ' "${COMMIT_TYPES[@]}")
543
605
  COMMIT_TYPE_LIST="${COMMIT_TYPE_LIST%, }"
544
606
  # Typed `valid_types` array (#3811 review finding): CONTRIBUTING.md bans
@@ -550,8 +612,35 @@ if [ "$CLASSIFY_STATUS" = "0" ]; then
550
612
  # or `\`.
551
613
  COMMIT_TYPES_JSON=$(printf '"%s",' "${COMMIT_TYPES[@]}")
552
614
  COMMIT_TYPES_JSON="[${COMMIT_TYPES_JSON%,}]"
553
- # Validate Conventional Commits format
554
- if ! [[ "$SUBJECT" =~ ^($COMMIT_TYPE_ALT)(\(.+\))?:[[:space:]].+ ]]; then
615
+ # Validate Conventional Commits format.
616
+ #
617
+ # #4429: do NOT build `^(type1|type2|...)` out of COMMIT_TYPES. That
618
+ # alternation grows with the CONFIGURED list, and how large a pattern can be
619
+ # compiled is a property of the platform's regex engine. bash 3.2.57 / BSD
620
+ # libc (macOS, this file's stated target) caps it at 64 KiB - bisected: a
621
+ # 65504-byte alternation compiles, 65515 fails. bash 5.2 / glibc has no
622
+ # reachable cap, so this half never bit Linux. Past a cap `[[ =~ ]]`
623
+ # returns 2, and `if !` cannot tell a COMPILE ERROR from "the subject does
624
+ # not conform" - so a valid `feat(auth): ...` was blocked with
625
+ # CONVENTIONAL_COMMITS_VIOLATION while `feat` sat in its own valid_types.
626
+ #
627
+ # Match the SHAPE with a fixed-size pattern, then test membership against
628
+ # the array. The regex no longer depends on how many types are configured,
629
+ # and the loop adds no subprocess or pipe (the #4429 hazard this file
630
+ # already avoids elsewhere). The character class is exactly the safe-token
631
+ # filter `^[a-z][a-z0-9-]*$` applied above, so it captures every type that
632
+ # can legally reach COMMIT_TYPES and no token that cannot.
633
+ SUBJECT_TYPE=''
634
+ if [[ "$SUBJECT" =~ ^([a-z][a-z0-9-]*)(\(.+\))?:[[:space:]].+ ]]; then
635
+ SUBJECT_TYPE="${BASH_REMATCH[1]}"
636
+ fi
637
+ COMMIT_TYPE_OK=0
638
+ if [ -n "$SUBJECT_TYPE" ]; then
639
+ for _known_type in "${COMMIT_TYPES[@]}"; do
640
+ if [ "$_known_type" = "$SUBJECT_TYPE" ]; then COMMIT_TYPE_OK=1; break; fi
641
+ done
642
+ fi
643
+ if [ "$COMMIT_TYPE_OK" -ne 1 ]; then
555
644
  # Emit typed `code` and `valid_types` fields alongside `reason` (#2974,
556
645
  # #3811). Tests assert on the stable code string and the typed array;
557
646
  # the reason is the human-readable copy, never grepped by tests.
@@ -163,17 +163,37 @@ process.stdin.on('end', () => {
163
163
  // returns a path containing .git/worktrees/ as a component.
164
164
  // In the main repo or a submodule it returns .git (or a path without /worktrees/).
165
165
  // This approach works even when cwd is a subdirectory of the worktree.
166
- const gitDirResult = git(['rev-parse', '--git-dir'], cwd);
166
+ // Combined into one spawn — git rev-parse accepts multiple query flags in
167
+ // one invocation and prints one line of output per flag, in the exact
168
+ // order given, reducing this guard's worst-case subprocess count under
169
+ // CI/load contention (three spawns collapse into one). `--abbrev-ref HEAD`
170
+ // is used instead of `symbolic-ref --short HEAD` because it is combinable
171
+ // (a single `rev-parse` call) and behaviorally equivalent for this guard's
172
+ // branch-acceptance check, including on detached HEAD: `--abbrev-ref`
173
+ // returns the literal string `HEAD` there (exit 0), which the acceptance
174
+ // regex below also rejects — the same guard outcome as symbolic-ref's
175
+ // exit-128/empty-stdout failure. Do not change any timeout value as part
176
+ // of this change, only the spawn count.
177
+ const combinedResult = git(['rev-parse', '--git-dir', '--abbrev-ref', 'HEAD', '--show-toplevel'], cwd);
167
178
  // #3911: a timeout/spawn-failure result is indistinguishable from a clean
168
179
  // "not a git repo" answer by status/stdout alone — reportIfUndetermined
169
180
  // is a no-op on a genuine negative and only fires the diagnostic when the
170
181
  // probe itself could not run. The allow() below is UNCHANGED either way.
171
- reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --git-dir', gitDirResult);
172
- if (gitDirResult.status !== 0 || !gitDirResult.stdout) {
182
+ reportIfUndetermined(
183
+ 'gsd-worktree-path-guard',
184
+ 'git rev-parse --git-dir --abbrev-ref HEAD --show-toplevel',
185
+ combinedResult
186
+ );
187
+ if (combinedResult.status !== 0 || !combinedResult.stdout) {
173
188
  allow(undefined); // not a git repo — pass through
174
189
  }
175
190
 
176
- const gitDir = gitDirResult.stdout.trim();
191
+ const combinedLines = combinedResult.stdout.split('\n').map((l) => l.trim()).filter((l) => l.length > 0);
192
+ if (combinedLines.length < 3) {
193
+ allow(undefined); // malformed/short output — can't determine root, fail open
194
+ }
195
+ const [gitDir, branch, wtTopRaw] = combinedLines;
196
+
177
197
  // A linked worktree's --git-dir contains .git/worktrees/ as a path component
178
198
  const isLinkedWorktree = /[/\\]\.git[/\\]worktrees[/\\]/.test(gitDir);
179
199
  if (!isLinkedWorktree) {
@@ -186,23 +206,14 @@ process.stdin.on('end', () => {
186
206
  // created linked worktree (plain non-GSD work, e.g. Claude Code plan-mode) is
187
207
  // on the user's own branch, so the guard must be a no-op there. Detached HEAD
188
208
  // / error → not GSD-managed → no-op.
189
- const branchResult = git(['symbolic-ref', '--short', 'HEAD'], cwd);
190
- reportIfUndetermined('gsd-worktree-path-guard', 'git symbolic-ref --short HEAD', branchResult);
191
- const branch = branchResult.status === 0 && branchResult.stdout ? branchResult.stdout.trim() : '';
192
209
  // #3021: accept worktree-wf_<runid>-<n> branches (Workflow backend's naming).
193
210
  if (!/^((worktree-)?agent-|worktree-wf_)[A-Za-z0-9._/-]+$/.test(branch)) {
194
211
  allow(undefined); // not a GSD-managed executor worktree — no-op
195
212
  }
196
213
 
197
- // Get the raw --show-toplevel output for the worktree (cwd).
214
+ // wtTopRaw: the raw --show-toplevel output for the worktree (cwd).
198
215
  // We keep it raw (not path.resolve'd) to compare directly with the
199
216
  // file's toplevel — same git binary, same format, no normalization needed.
200
- const wtTopResult = git(['rev-parse', '--show-toplevel'], cwd);
201
- reportIfUndetermined('gsd-worktree-path-guard', 'git rev-parse --show-toplevel (worktree cwd)', wtTopResult);
202
- if (wtTopResult.status !== 0 || !wtTopResult.stdout) {
203
- allow(undefined); // can't determine root — fail open
204
- }
205
- const wtTopRaw = wtTopResult.stdout.trim();
206
217
 
207
218
  // #2595 (review Major 3): read the field TYPED. `?.file_path || ''` let a
208
219
  // non-string through — `[]` and `{}` are truthy, so they survived the