@opengsd/gsd-core 1.10.0 → 1.12.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 (544) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-code-fixer.md +1 -1
  4. package/agents/gsd-debug-session-manager.md +12 -1
  5. package/agents/gsd-debugger.md +1 -1
  6. package/agents/gsd-doc-synthesizer.md +2 -4
  7. package/agents/gsd-dom-verifier.md +169 -0
  8. package/agents/gsd-eval-auditor.md +1 -1
  9. package/agents/gsd-executor.md +22 -14
  10. package/agents/gsd-framework-selector.md +1 -3
  11. package/agents/gsd-intel-updater.md +1 -1
  12. package/agents/gsd-mempalace-curator.md +5 -3
  13. package/agents/gsd-pattern-mapper.md +11 -0
  14. package/agents/gsd-phase-researcher.md +23 -2
  15. package/agents/gsd-plan-checker.md +50 -53
  16. package/agents/gsd-planner.md +50 -50
  17. package/agents/gsd-project-researcher.md +1 -1
  18. package/agents/gsd-research-synthesizer.md +2 -2
  19. package/agents/gsd-roadmapper.md +15 -11
  20. package/agents/gsd-ui-checker.md +63 -4
  21. package/agents/gsd-ui-researcher.md +41 -3
  22. package/agents/gsd-user-profiler.md +3 -0
  23. package/agents/gsd-verifier.md +13 -4
  24. package/bin/install.js +1448 -1103
  25. package/commands/gsd/code-review.md +1 -1
  26. package/commands/gsd/discuss-phase.md +1 -1
  27. package/commands/gsd/execute-phase.md +1 -1
  28. package/commands/gsd/import.md +1 -1
  29. package/commands/gsd/map-codebase.md +1 -1
  30. package/commands/gsd/mempalace-capture.md +1 -1
  31. package/commands/gsd/mempalace-recall.md +1 -1
  32. package/commands/gsd/new-milestone.md +1 -1
  33. package/commands/gsd/quick.md +9 -5
  34. package/commands/gsd/review-backlog.md +2 -1
  35. package/commands/gsd/verify-work.md +1 -1
  36. package/gsd-core/bin/gsd-tools.cjs +1035 -138
  37. package/gsd-core/bin/lib/active-workstream-store.cjs +146 -22
  38. package/gsd-core/bin/lib/adr-parser.cjs +13 -7
  39. package/gsd-core/bin/lib/agent-install-check.cjs +392 -32
  40. package/gsd-core/bin/lib/api-coverage.cjs +33 -14
  41. package/gsd-core/bin/lib/artifacts.cjs +5 -0
  42. package/gsd-core/bin/lib/assumption-delta.cjs +32 -15
  43. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  44. package/gsd-core/bin/lib/audit.cjs +1026 -268
  45. package/gsd-core/bin/lib/broken-windows.cjs +306 -28
  46. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  47. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  48. package/gsd-core/bin/lib/capability-lock.cjs +10 -4
  49. package/gsd-core/bin/lib/capability-registry.cjs +845 -130
  50. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  51. package/gsd-core/bin/lib/capability-state.cjs +18 -3
  52. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  53. package/gsd-core/bin/lib/capability-validator.cjs +700 -40
  54. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  55. package/gsd-core/bin/lib/check-command-router.cjs +216 -42
  56. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  57. package/gsd-core/bin/lib/cli-exit.cjs +496 -10
  58. package/gsd-core/bin/lib/code-review-depth.cjs +288 -0
  59. package/gsd-core/bin/lib/codex-agent-toml.cjs +735 -0
  60. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  61. package/gsd-core/bin/lib/command-arg-projection.cjs +144 -14
  62. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  63. package/gsd-core/bin/lib/command-routing-hub.cjs +31 -2
  64. package/gsd-core/bin/lib/commands.cjs +1172 -108
  65. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  66. package/gsd-core/bin/lib/complexity-trigger.cjs +1192 -0
  67. package/gsd-core/bin/lib/config-loader.cjs +187 -23
  68. package/gsd-core/bin/lib/config.cjs +102 -3
  69. package/gsd-core/bin/lib/configuration.cjs +129 -37
  70. package/gsd-core/bin/lib/core-utils.cjs +208 -33
  71. package/gsd-core/bin/lib/decisions.cjs +23 -0
  72. package/gsd-core/bin/lib/edge-probe.cjs +9 -1
  73. package/gsd-core/bin/lib/estimate-cli.cjs +55 -11
  74. package/gsd-core/bin/lib/exit-code-registry.cjs +98 -0
  75. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  76. package/gsd-core/bin/lib/frontmatter.cjs +899 -229
  77. package/gsd-core/bin/lib/gap-checker.cjs +95 -10
  78. package/gsd-core/bin/lib/git-base-branch.cjs +276 -39
  79. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  80. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  81. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  82. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +149 -0
  83. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  84. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  85. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  86. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +268 -0
  87. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  88. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  89. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +187 -0
  90. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  91. package/gsd-core/bin/lib/health-diagnostic.cjs +451 -0
  92. package/gsd-core/bin/lib/host-integration.cjs +39 -6
  93. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  94. package/gsd-core/bin/lib/init-command-router.cjs +118 -21
  95. package/gsd-core/bin/lib/init.cjs +439 -168
  96. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  97. package/gsd-core/bin/lib/install-engine.cjs +811 -259
  98. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  99. package/gsd-core/bin/lib/install-model-override-resolver.cjs +235 -0
  100. package/gsd-core/bin/lib/install-profiles.cjs +212 -61
  101. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  102. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  103. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  104. package/gsd-core/bin/lib/installer-migration-report.cjs +3 -0
  105. package/gsd-core/bin/lib/installer-migrations/010-antigravity-retire-confighome-artifacts.cjs +169 -0
  106. package/gsd-core/bin/lib/installer-migrations.cjs +148 -38
  107. package/gsd-core/bin/lib/intel.cjs +101 -26
  108. package/gsd-core/bin/lib/io.cjs +170 -15
  109. package/gsd-core/bin/lib/learnings.cjs +85 -14
  110. package/gsd-core/bin/lib/legacy-cleanup.cjs +8 -2
  111. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  112. package/gsd-core/bin/lib/markdown-table.cjs +183 -22
  113. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  114. package/gsd-core/bin/lib/milestone.cjs +842 -73
  115. package/gsd-core/bin/lib/model-catalog.cjs +232 -16
  116. package/gsd-core/bin/lib/model-resolver.cjs +193 -68
  117. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  118. package/gsd-core/bin/lib/onboard-projection.cjs +5 -1
  119. package/gsd-core/bin/lib/pattern.cjs +122 -0
  120. package/gsd-core/bin/lib/phase-estimation.cjs +18 -9
  121. package/gsd-core/bin/lib/phase-id.cjs +514 -40
  122. package/gsd-core/bin/lib/phase-lifecycle.cjs +52 -19
  123. package/gsd-core/bin/lib/phase-locator.cjs +262 -34
  124. package/gsd-core/bin/lib/phase.cjs +1038 -214
  125. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  126. package/gsd-core/bin/lib/plan-document.cjs +263 -0
  127. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  128. package/gsd-core/bin/lib/plan-scan.cjs +98 -3
  129. package/gsd-core/bin/lib/planning-command-router.cjs +61 -0
  130. package/gsd-core/bin/lib/planning-inspect.cjs +1168 -0
  131. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  132. package/gsd-core/bin/lib/planning-snapshot.cjs +894 -0
  133. package/gsd-core/bin/lib/planning-workspace.cjs +112 -6
  134. package/gsd-core/bin/lib/probe-core.cjs +5 -2
  135. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  136. package/gsd-core/bin/lib/profile-pipeline-command-router.cjs +50 -7
  137. package/gsd-core/bin/lib/profile-pipeline.cjs +6 -3
  138. package/gsd-core/bin/lib/real-home-guard.cjs +419 -0
  139. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +766 -0
  140. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  141. package/gsd-core/bin/lib/review-lane-descriptor.cjs +22 -13
  142. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  143. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  144. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  145. package/gsd-core/bin/lib/roadmap-command-router.cjs +59 -11
  146. package/gsd-core/bin/lib/roadmap-parser.cjs +1006 -184
  147. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  148. package/gsd-core/bin/lib/roadmap.cjs +442 -96
  149. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +702 -52
  150. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  151. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +459 -55
  152. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  153. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  154. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +402 -58
  155. package/gsd-core/bin/lib/runtime-identity.cjs +234 -0
  156. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  157. package/gsd-core/bin/lib/runtime-slash.cjs +96 -8
  158. package/gsd-core/bin/lib/security.cjs +104 -5
  159. package/gsd-core/bin/lib/shell-command-projection.cjs +342 -7
  160. package/gsd-core/bin/lib/smart-entry.cjs +133 -23
  161. package/gsd-core/bin/lib/spec-section.cjs +12 -7
  162. package/gsd-core/bin/lib/state-command-router.cjs +52 -19
  163. package/gsd-core/bin/lib/state-contract.cjs +359 -0
  164. package/gsd-core/bin/lib/state-document.cjs +338 -8
  165. package/gsd-core/bin/lib/state-md-schema.cjs +221 -0
  166. package/gsd-core/bin/lib/state-transition.cjs +846 -176
  167. package/gsd-core/bin/lib/state.cjs +2589 -369
  168. package/gsd-core/bin/lib/surface.cjs +33 -11
  169. package/gsd-core/bin/lib/task-command-router.cjs +111 -1
  170. package/gsd-core/bin/lib/task-content-resolution.cjs +368 -0
  171. package/gsd-core/bin/lib/teams-status.cjs +4 -1
  172. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  173. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  174. package/gsd-core/bin/lib/uat-predicate.cjs +67 -23
  175. package/gsd-core/bin/lib/uat.cjs +1761 -167
  176. package/gsd-core/bin/lib/ui-consideration-probe.cjs +9 -1
  177. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  178. package/gsd-core/bin/lib/ui-safety-gate.cjs +51 -12
  179. package/gsd-core/bin/lib/unusable-input.cjs +37 -0
  180. package/gsd-core/bin/lib/update-context.cjs +8 -2
  181. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  182. package/gsd-core/bin/lib/validate-command-router.cjs +2 -2
  183. package/gsd-core/bin/lib/validate.cjs +20 -6
  184. package/gsd-core/bin/lib/vendor/README.md +75 -0
  185. package/gsd-core/bin/lib/vendor/js-yaml.cjs +3014 -0
  186. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  187. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  188. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  189. package/gsd-core/bin/lib/verification.cjs +272 -9
  190. package/gsd-core/bin/lib/verify-command-grounding.cjs +846 -0
  191. package/gsd-core/bin/lib/verify.cjs +453 -918
  192. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  193. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  194. package/gsd-core/bin/lib/workstream-name-policy.cjs +25 -4
  195. package/gsd-core/bin/lib/workstream.cjs +2 -2
  196. package/gsd-core/bin/lib/worktree-base-ref.cjs +66 -12
  197. package/gsd-core/bin/lib/worktree-safety.cjs +341 -18
  198. package/gsd-core/bin/shared/config-defaults.manifest.json +8 -1
  199. package/gsd-core/bin/shared/config-schema.manifest.json +12 -1
  200. package/gsd-core/bin/shared/exit-codes.json +8 -0
  201. package/gsd-core/bin/shared/exit-codes.sh +20 -0
  202. package/gsd-core/bin/shared/model-catalog.json +8 -1
  203. package/gsd-core/references/agent-contracts.md +44 -26
  204. package/gsd-core/references/api-coverage.md +24 -2
  205. package/gsd-core/references/autonomous-smart-discuss.md +3 -3
  206. package/gsd-core/references/checkpoints.md +39 -21
  207. package/gsd-core/references/context-budget.md +1 -1
  208. package/gsd-core/references/decimal-phase-calculation.md +5 -5
  209. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  210. package/gsd-core/references/doc-conflict-engine.md +1 -1
  211. package/gsd-core/references/edge-probe.md +8 -0
  212. package/gsd-core/references/execute-mvp-tdd.md +4 -6
  213. package/gsd-core/references/execute-phase-between-wave-reset.md +15 -14
  214. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  215. package/gsd-core/references/execute-phase-response-language.md +1 -1
  216. package/gsd-core/references/execute-phase-wave-guard.md +17 -11
  217. package/gsd-core/references/failing-direction.md +78 -0
  218. package/gsd-core/references/gate-prompts.md +1 -1
  219. package/gsd-core/references/git-integration.md +5 -5
  220. package/gsd-core/references/git-planning-commit.md +5 -4
  221. package/gsd-core/references/gsd-run-resolver.md +1 -1
  222. package/gsd-core/references/loop-hook-dispatch.md +61 -2
  223. package/gsd-core/references/model-profiles.md +12 -4
  224. package/gsd-core/references/mvp-concepts.md +9 -9
  225. package/gsd-core/references/nyquist-compliance.md +74 -0
  226. package/gsd-core/references/offer-next.md +3 -5
  227. package/gsd-core/references/phase-argument-parsing.md +3 -3
  228. package/gsd-core/references/planner-failing-direction.md +53 -0
  229. package/gsd-core/references/planner-guidance.md +3 -9
  230. package/gsd-core/references/planner-human-verify-mode.md +15 -1
  231. package/gsd-core/references/planner-preconditions.md +1 -1
  232. package/gsd-core/references/planner-reviews.md +1 -1
  233. package/gsd-core/references/planner-revision.md +1 -1
  234. package/gsd-core/references/planner-verify-command-grounding.md +17 -0
  235. package/gsd-core/references/planning-config.md +44 -13
  236. package/gsd-core/references/reviewer-instances.md +31 -0
  237. package/gsd-core/references/revision-loop.md +1 -1
  238. package/gsd-core/references/runtime-aware-dispatch.md +1 -1
  239. package/gsd-core/references/specless-probe-fallback.md +1 -1
  240. package/gsd-core/references/tdd.md +1 -3
  241. package/gsd-core/references/ui-brand.md +65 -21
  242. package/gsd-core/references/ui-consideration-probe.md +1 -1
  243. package/gsd-core/references/universal-anti-patterns.md +5 -5
  244. package/gsd-core/references/verifier-phase-gates.md +192 -0
  245. package/gsd-core/references/verify-command-path-resolvability.md +42 -0
  246. package/gsd-core/references/verify-mvp-mode.md +2 -2
  247. package/gsd-core/references/workstream-flag.md +33 -17
  248. package/gsd-core/templates/README.md +1 -1
  249. package/gsd-core/templates/SECURITY.md +3 -3
  250. package/gsd-core/templates/UI-SPEC.md +25 -3
  251. package/gsd-core/templates/VALIDATION.md +3 -3
  252. package/gsd-core/templates/discussion-log.md +1 -1
  253. package/gsd-core/templates/phase-prompt.md +5 -4
  254. package/gsd-core/templates/state.md +11 -4
  255. package/gsd-core/templates/verification-report.md +9 -1
  256. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  257. package/gsd-core/workflows/add-backlog.md +1 -1
  258. package/gsd-core/workflows/add-phase.md +3 -3
  259. package/gsd-core/workflows/add-tests.md +3 -8
  260. package/gsd-core/workflows/add-todo.md +1 -1
  261. package/gsd-core/workflows/ai-integration-phase.md +13 -20
  262. package/gsd-core/workflows/audit-fix.md +12 -3
  263. package/gsd-core/workflows/audit-milestone.md +9 -9
  264. package/gsd-core/workflows/audit-uat.md +17 -2
  265. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +2 -2
  266. package/gsd-core/workflows/autonomous.md +11 -27
  267. package/gsd-core/workflows/check-todos.md +1 -1
  268. package/gsd-core/workflows/cleanup.md +64 -5
  269. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +14 -4
  270. package/gsd-core/workflows/code-review-fix.md +38 -11
  271. package/gsd-core/workflows/code-review.md +159 -52
  272. package/gsd-core/workflows/complete-milestone.md +151 -23
  273. package/gsd-core/workflows/debug.md +12 -8
  274. package/gsd-core/workflows/diagnose-issues.md +47 -15
  275. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  276. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -8
  277. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  278. package/gsd-core/workflows/discuss-phase/modes/text.md +1 -1
  279. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +1 -3
  280. package/gsd-core/workflows/discuss-phase-assumptions.md +4 -3
  281. package/gsd-core/workflows/discuss-phase.md +1 -1
  282. package/gsd-core/workflows/do.md +3 -6
  283. package/gsd-core/workflows/docs-update.md +5 -4
  284. package/gsd-core/workflows/edit-phase.md +27 -2
  285. package/gsd-core/workflows/eval-review.md +7 -14
  286. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +1 -1
  287. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +142 -15
  288. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  289. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  290. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  291. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +24 -4
  292. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +2 -2
  293. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +21 -0
  294. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -2
  295. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +39 -0
  296. package/gsd-core/workflows/execute-phase.md +72 -100
  297. package/gsd-core/workflows/execute-plan.md +52 -15
  298. package/gsd-core/workflows/explore.md +131 -4
  299. package/gsd-core/workflows/extract-learnings.md +1 -1
  300. package/gsd-core/workflows/fast.md +10 -2
  301. package/gsd-core/workflows/forensics.md +1 -1
  302. package/gsd-core/workflows/graduation.md +5 -5
  303. package/gsd-core/workflows/health.md +76 -10
  304. package/gsd-core/workflows/import.md +18 -15
  305. package/gsd-core/workflows/inbox.md +4 -5
  306. package/gsd-core/workflows/ingest-docs.md +49 -16
  307. package/gsd-core/workflows/insert-phase.md +5 -5
  308. package/gsd-core/workflows/list-seeds.md +5 -3
  309. package/gsd-core/workflows/list-workspaces.md +1 -1
  310. package/gsd-core/workflows/manager.md +12 -23
  311. package/gsd-core/workflows/map-codebase.md +1 -1
  312. package/gsd-core/workflows/milestone-summary.md +1 -1
  313. package/gsd-core/workflows/mvp-phase.md +8 -5
  314. package/gsd-core/workflows/new-milestone.md +22 -29
  315. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +1 -1
  316. package/gsd-core/workflows/new-project.md +26 -40
  317. package/gsd-core/workflows/new-workspace.md +1 -1
  318. package/gsd-core/workflows/next.md +14 -2
  319. package/gsd-core/workflows/pause-work.md +1 -1
  320. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +1 -1
  321. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +1 -1
  322. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -4
  323. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +3 -3
  324. package/gsd-core/workflows/plan-phase.md +162 -59
  325. package/gsd-core/workflows/plan-review-convergence.md +96 -11
  326. package/gsd-core/workflows/plant-seed.md +2 -2
  327. package/gsd-core/workflows/pr-branch.md +187 -51
  328. package/gsd-core/workflows/profile-user.md +16 -14
  329. package/gsd-core/workflows/progress.md +61 -18
  330. package/gsd-core/workflows/quick/steps/discussion-phase.md +1 -3
  331. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +5 -7
  332. package/gsd-core/workflows/quick/steps/quick-verification.md +28 -9
  333. package/gsd-core/workflows/quick/steps/research-phase.md +4 -6
  334. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +3 -3
  335. package/gsd-core/workflows/quick.md +55 -44
  336. package/gsd-core/workflows/remove-phase.md +4 -4
  337. package/gsd-core/workflows/remove-workspace.md +2 -2
  338. package/gsd-core/workflows/resume-project.md +8 -12
  339. package/gsd-core/workflows/review.md +219 -20
  340. package/gsd-core/workflows/scan.md +1 -1
  341. package/gsd-core/workflows/secure-phase.md +3 -3
  342. package/gsd-core/workflows/session-report.md +2 -1
  343. package/gsd-core/workflows/settings-advanced.md +7 -9
  344. package/gsd-core/workflows/settings-integrations.md +64 -31
  345. package/gsd-core/workflows/settings.md +69 -7
  346. package/gsd-core/workflows/ship.md +116 -50
  347. package/gsd-core/workflows/sketch-wrap-up.md +11 -17
  348. package/gsd-core/workflows/sketch.md +12 -18
  349. package/gsd-core/workflows/smart-entry.md +3 -5
  350. package/gsd-core/workflows/spec-phase.md +53 -13
  351. package/gsd-core/workflows/spike-wrap-up.md +7 -11
  352. package/gsd-core/workflows/spike.md +20 -31
  353. package/gsd-core/workflows/stats.md +2 -2
  354. package/gsd-core/workflows/sync-skills.md +64 -9
  355. package/gsd-core/workflows/thread.md +11 -7
  356. package/gsd-core/workflows/transition.md +49 -14
  357. package/gsd-core/workflows/ui-phase.md +15 -21
  358. package/gsd-core/workflows/ui-review.md +8 -12
  359. package/gsd-core/workflows/ultraplan-phase.md +5 -13
  360. package/gsd-core/workflows/undo.md +8 -16
  361. package/gsd-core/workflows/update.md +7 -11
  362. package/gsd-core/workflows/validate-phase.md +3 -3
  363. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +25 -1
  364. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  365. package/gsd-core/workflows/verify-work.md +66 -25
  366. package/hooks/dist/gsd-agent-isolation-guard.js +158 -30
  367. package/hooks/dist/gsd-check-update-worker.js +56 -13
  368. package/hooks/dist/gsd-check-update.js +19 -1
  369. package/hooks/dist/gsd-config-reload.js +18 -12
  370. package/hooks/dist/gsd-context-monitor.js +19 -10
  371. package/hooks/dist/gsd-cursor-post-tool.js +3 -1
  372. package/hooks/dist/gsd-cursor-pre-tool.js +2 -3
  373. package/hooks/dist/gsd-cursor-session-start.js +2 -1
  374. package/hooks/dist/gsd-cursor-stop.js +2 -1
  375. package/hooks/dist/gsd-cursor-subagent-start.js +83 -3
  376. package/hooks/dist/gsd-cursor-subagent-stop.js +6 -3
  377. package/hooks/dist/gsd-ensure-canonical-path.js +2 -1
  378. package/hooks/dist/gsd-graphify-update.sh +22 -18
  379. package/hooks/dist/gsd-node-runner.sh +76 -0
  380. package/hooks/dist/gsd-phase-boundary.sh +1 -0
  381. package/hooks/dist/gsd-prompt-guard.js +37 -27
  382. package/hooks/dist/gsd-read-guard.js +16 -7
  383. package/hooks/dist/gsd-read-injection-scanner.js +55 -32
  384. package/hooks/dist/gsd-session-state.sh +1 -0
  385. package/hooks/dist/gsd-statusline.js +231 -24
  386. package/hooks/dist/gsd-update-banner.js +22 -1
  387. package/hooks/dist/gsd-validate-commit.sh +80 -6
  388. package/hooks/dist/gsd-windsurf-pre-command.js +16 -11
  389. package/hooks/dist/gsd-windsurf-pre-write.js +22 -13
  390. package/hooks/dist/gsd-workflow-guard.js +162 -46
  391. package/hooks/dist/gsd-worktree-path-guard.js +36 -21
  392. package/hooks/dist/gsd-write-guard.js +35 -25
  393. package/hooks/dist/lib/cli-exit.js +560 -0
  394. package/hooks/dist/lib/exit-code-registry.js +98 -0
  395. package/hooks/dist/lib/git-cmd.js +92 -59
  396. package/hooks/dist/lib/git-probe.js +84 -0
  397. package/hooks/dist/lib/hook-exit.js +81 -0
  398. package/hooks/dist/lib/injection-patterns.js +45 -0
  399. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  400. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  401. package/hooks/dist/managed-hooks-registry.cjs +3 -0
  402. package/hooks/gsd-agent-isolation-guard.js +158 -30
  403. package/hooks/gsd-check-update-worker.js +56 -13
  404. package/hooks/gsd-check-update.js +19 -1
  405. package/hooks/gsd-config-reload.js +18 -12
  406. package/hooks/gsd-context-monitor.js +19 -10
  407. package/hooks/gsd-cursor-post-tool.js +3 -1
  408. package/hooks/gsd-cursor-pre-tool.js +2 -3
  409. package/hooks/gsd-cursor-session-start.js +2 -1
  410. package/hooks/gsd-cursor-stop.js +2 -1
  411. package/hooks/gsd-cursor-subagent-start.js +83 -3
  412. package/hooks/gsd-cursor-subagent-stop.js +6 -3
  413. package/hooks/gsd-ensure-canonical-path.js +2 -1
  414. package/hooks/gsd-graphify-update.sh +22 -18
  415. package/hooks/gsd-node-runner.sh +76 -0
  416. package/hooks/gsd-phase-boundary.sh +1 -0
  417. package/hooks/gsd-prompt-guard.js +37 -27
  418. package/hooks/gsd-read-guard.js +16 -7
  419. package/hooks/gsd-read-injection-scanner.js +55 -32
  420. package/hooks/gsd-session-state.sh +1 -0
  421. package/hooks/gsd-statusline.js +231 -24
  422. package/hooks/gsd-update-banner.js +22 -1
  423. package/hooks/gsd-validate-commit.sh +80 -6
  424. package/hooks/gsd-windsurf-pre-command.js +16 -11
  425. package/hooks/gsd-windsurf-pre-write.js +22 -13
  426. package/hooks/gsd-workflow-guard.js +162 -46
  427. package/hooks/gsd-worktree-path-guard.js +36 -21
  428. package/hooks/gsd-write-guard.js +35 -25
  429. package/hooks/lib/cli-exit.js +560 -0
  430. package/hooks/lib/exit-code-registry.js +98 -0
  431. package/hooks/lib/git-cmd.js +92 -59
  432. package/hooks/lib/git-probe.js +84 -0
  433. package/hooks/lib/hook-exit.js +81 -0
  434. package/hooks/lib/injection-patterns.js +45 -0
  435. package/hooks/lib/isolation-deny-reason.js +39 -0
  436. package/hooks/lib/isolation-sentinel.js +9 -0
  437. package/hooks/managed-hooks-registry.cjs +3 -0
  438. package/package.json +28 -11
  439. package/pi/gsd.cjs +19 -5
  440. package/scripts/base64-scan.sh +74 -12
  441. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  442. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  443. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  444. package/scripts/build-hooks.js +5 -0
  445. package/scripts/changeset/lint.cjs +60 -5
  446. package/scripts/check-alias-drift.cjs +7 -43
  447. package/scripts/check-contract-drift.cjs +297 -0
  448. package/scripts/check-glossary-refs.cjs +77 -15
  449. package/scripts/check-mutation-score-ratchet.cjs +156 -0
  450. package/scripts/ci-check-job-near-cap.cjs +49 -0
  451. package/scripts/ci-pr-mergeability.cjs +262 -0
  452. package/scripts/ci-test-scope.cjs +64 -14
  453. package/scripts/ci-timeout-report.cjs +230 -0
  454. package/scripts/command-contract-helpers.cjs +903 -1
  455. package/scripts/docs-guard-registry.cjs +396 -0
  456. package/scripts/gen-adr-index.cjs +728 -38
  457. package/scripts/gen-capability-registry.cjs +11 -21
  458. package/scripts/gen-context-index.cjs +2 -11
  459. package/scripts/gen-exit-code-docs.cjs +318 -0
  460. package/scripts/gen-exit-code-registry.cjs +891 -0
  461. package/scripts/gen-features.cjs +836 -0
  462. package/scripts/gen-health-docs.cjs +390 -0
  463. package/scripts/gen-hooks-cli-exit.cjs +239 -0
  464. package/scripts/gen-install-tree-fixtures.cjs +2 -2
  465. package/scripts/gen-inventory-manifest.cjs +50 -4
  466. package/scripts/gen-loop-host-contract.cjs +138 -25
  467. package/scripts/gen-registry.cjs +3 -14
  468. package/scripts/gen-scripts-cli-exit.cjs +185 -0
  469. package/scripts/gen-state-md-docs.cjs +727 -0
  470. package/scripts/{test-failure-reasons.cjs → gsd-test-gate-reasons.cjs} +6 -0
  471. package/scripts/lib/alias-drift-families.cjs +46 -0
  472. package/scripts/lib/ci-job-timing.cjs +72 -0
  473. package/scripts/lib/cli-exit.cjs +546 -44
  474. package/scripts/lib/drift-scan.cjs +308 -0
  475. package/scripts/lib/exit-code-registry.cjs +98 -0
  476. package/scripts/lib/ndjson-reporter.cjs +119 -0
  477. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  478. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  479. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  480. package/scripts/lint-canary-version-leak.cjs +73 -0
  481. package/scripts/lint-command-contract.cjs +96 -13
  482. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  483. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  484. package/scripts/lint-default-flip-documentation.cjs +193 -0
  485. package/scripts/lint-docs-guard-registration.cjs +495 -0
  486. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +193 -0
  487. package/scripts/lint-eslint-glob-coverage.allowlist.json +38 -0
  488. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  489. package/scripts/{lint-fix-has-regression-test.cjs → lint-fix-has-regression-tests.cjs} +12 -6
  490. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  491. package/scripts/lint-health-diagnostic-rule-table.cjs +461 -0
  492. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  493. package/scripts/lint-milestone-window-drift.cjs +468 -0
  494. package/scripts/lint-mutation-test-derivation-drift.cjs +86 -0
  495. package/scripts/lint-phase-enumeration-drift.cjs +492 -0
  496. package/scripts/lint-plan-count-drift.cjs +318 -0
  497. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  498. package/scripts/lint-planning-prompt-drift.cjs +471 -0
  499. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  500. package/scripts/lint-regression-test-names.cjs +15 -13
  501. package/scripts/lint-removed-but-needed.cjs +488 -0
  502. package/scripts/lint-seam-enforcement.cjs +182 -0
  503. package/scripts/lint-slug-derivation-drift.cjs +921 -0
  504. package/scripts/lint-source-test-name-collision.cjs +241 -0
  505. package/scripts/lint-state-field-drift.cjs +805 -0
  506. package/scripts/lint-state-write-path-drift.cjs +950 -0
  507. package/scripts/lint-test-file-count.allowlist.json +137 -8
  508. package/scripts/lint-test-file-count.cjs +25 -3
  509. package/scripts/lint-unreachable-guard-drift.cjs +830 -0
  510. package/scripts/lint-vendored-deps.cjs +297 -0
  511. package/scripts/mutation-matrix.cjs +599 -50
  512. package/scripts/pr-changed-files.cjs +63 -0
  513. package/scripts/pr-template-policy.cjs +14 -4
  514. package/scripts/prompt-injection-scan.sh +100 -14
  515. package/scripts/require-issue-link-policy.cjs +192 -0
  516. package/scripts/secret-scan.sh +75 -13
  517. package/scripts/select-docs-guards.cjs +56 -0
  518. package/scripts/sync-runtime-launcher.cjs +24 -7
  519. package/skills/gsd-autonomous/SKILL.md +0 -1
  520. package/skills/gsd-code-review/SKILL.md +1 -1
  521. package/skills/gsd-discuss-phase/SKILL.md +1 -1
  522. package/skills/gsd-execute-phase/SKILL.md +1 -2
  523. package/skills/gsd-import/SKILL.md +1 -1
  524. package/skills/gsd-map-codebase/SKILL.md +1 -1
  525. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  526. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  527. package/skills/gsd-new-milestone/SKILL.md +1 -1
  528. package/skills/gsd-next/SKILL.md +0 -1
  529. package/skills/gsd-plan-phase/SKILL.md +0 -1
  530. package/skills/gsd-progress/SKILL.md +0 -1
  531. package/skills/gsd-quick/SKILL.md +9 -5
  532. package/skills/gsd-review-backlog/SKILL.md +2 -1
  533. package/skills/gsd-stats/SKILL.md +0 -1
  534. package/skills/gsd-verify-work/SKILL.md +1 -1
  535. package/vscode/package.json +1 -1
  536. package/bin/lib/ui-safety-gate.cjs +0 -107
  537. package/gsd-core/workflows/discovery-phase.md +0 -298
  538. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  539. package/gsd-core/workflows/verify-phase.md +0 -574
  540. package/scripts/affected-tests-lib.cjs +0 -554
  541. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  542. package/scripts/lint-emitted-drift-ack.cjs +0 -344
  543. package/scripts/run-affected-tests.cjs +0 -7
  544. package/scripts/run-tests.cjs +0 -1051
@@ -0,0 +1,830 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Prompt-layer drift guard for #3409 — shell guards that cannot observe
6
+ * their own failure arm.
7
+ *
8
+ * Design: .gsd/phase/feat-3409-unreachable-shell-guard-lint/40-design.md
9
+ * Test matrix: .gsd/phase/feat-3409-unreachable-shell-guard-lint/50-test-matrix.md
10
+ *
11
+ * RETIRED — Detector A (`--pick` + `|| echo` on one line), #3884.
12
+ * `gsd-tools.cjs`'s `--pick <field>` extractor used to coerce a missing/
13
+ * absent field to the empty string and exit **0**, which made the `|| echo D`
14
+ * arm in `$(gsd_run query V --pick F 2>/dev/null || echo D)` unreachable on
15
+ * field absence — the exact defect Detector A existed to flag (this file's
16
+ * own prior header quoted the premise verbatim: "the `|| echo D` arm can
17
+ * fire only on a typo in the verb name, never on the field absence it was
18
+ * written to handle"). ADR-3473 §8.4 ("Failure is a value") makes `--pick`
19
+ * exit **non-zero** on an absent field (see
20
+ * `.gsd/phase/feat-3884-failure-is-a-value/40-design.md` rows B6-B14), so
21
+ * that premise is now FALSE: the `|| echo D` arm is reachable, and the shape
22
+ * Detector A forbade is the CORRECT idiom going forward. Keeping Detector A
23
+ * would forbid the fix, so it is removed rather than updated — see this
24
+ * file's Guard ledger entry in 40-design.md ("net: −1 detector, 0 added").
25
+ * `docs/how-to/resolve-unreachable-guard-findings.md` Shape A was updated in
26
+ * the same change (#3884) to say the same thing. The three shell guards this
27
+ * file's Detector A shipped alongside (#3365's Walking Skeleton gate,
28
+ * `PHASE_REQ_IDS`, `complete-milestone.md`'s bare `cat <glob>`) were fixed
29
+ * under #3409 with remedies that never took the now-retired shape (a bare
30
+ * `--pick` with no fallback, a two-line `X=…`/`X="${X:-D}"` split, and an
31
+ * array expansion, respectively) — see
32
+ * `tests/unreachable-shell-guard.test.cjs`, which this file does not touch
33
+ * and which #3884 confirmed still passes unchanged.
34
+ *
35
+ * ONE detector remains, unaffected by the above — its mechanism (nullglob
36
+ * success-on-empty) has nothing to do with `--pick`'s exit code:
37
+ *
38
+ * Detector B — `cat` or `ls` invoked in COMMAND POSITION with an operand
39
+ * containing an unquoted glob metacharacter (`*` or `?`). At bottom the
40
+ * same class of bug Detector A used to catch one level up the stack: a
41
+ * fallback/guard arm that a success-on-empty case silently defeats.
42
+ * Detector B-ii below is `ls <glob> … || echo` — with `ls`'s own
43
+ * nullglob-driven success-on-empty standing in for what used to be
44
+ * `--pick`'s absence-coerced-to-''. SCOPED to exactly three fired shapes,
45
+ * per a full measurement across the four SCAN_DIRS (measured counts
46
+ * recorded in the PR description; 0 sites for B-iii today, by design — see
47
+ * KNOWN LIMITS):
48
+ *
49
+ * B-i. `cat <glob>` fires UNCONDITIONALLY. This is the stdin-hang
50
+ * shape (measured rc=137 at 3s under an unmatched glob +
51
+ * nullglob): `cat` reads from stdin the moment it gets zero
52
+ * operands, regardless of what — if anything — consumes its own
53
+ * exit code. There is no fallback arm to inspect; the hang
54
+ * happens before one could run.
55
+ * B-ii. `ls <glob>` fires when its exit code feeds a REAL fallback:
56
+ * `… || <arm>` where `<arm>` is not the no-op `true`/`:`. Under
57
+ * nullglob `ls` SUCCEEDS listing the cwd on an unmatched glob,
58
+ * so the fallback never runs and the intended message/default is
59
+ * silently replaced by a directory listing — exactly Detector
60
+ * A's shape, with `ls`'s exit code standing in for `--pick`'s
61
+ * stdout.
62
+ * B-iii. `ls <glob>` fires at the head of an `if`/`elif`/`while` test —
63
+ * the #3300 "existence guard that is always true under
64
+ * nullglob" shape the issue names directly: an unmatched glob
65
+ * makes `ls` list the CWD instead of erroring, so the guard is
66
+ * always true. Zero sites today (the #3300 fix already removed
67
+ * them); this arm exists solely so a REINTRODUCED instance of
68
+ * the shape does not ship silently.
69
+ *
70
+ * NOT fired on:
71
+ * - `ls <glob> … || true` / `… || :` — suppressing a failure is not a
72
+ * guard, and there is no fallback VALUE being defeated (the whole
73
+ * point of `true`/`:` is "do nothing, either way"). Measured: ~15
74
+ * sites in this tree, all this exact defensive idiom.
75
+ * - an `ls <glob>` whose STDOUT is what's consumed (`ls foo/*.md
76
+ * 2>/dev/null`, `X=$(ls -d …)`, `ls … | head`) — neither the stdin
77
+ * hang nor a defeated fallback nor an always-true guard. Measured: 97
78
+ * sites, explicitly out of this issue's scope ("Explicit non-goal:
79
+ * … Only the shapes above move.").
80
+ * - markdown prose describing either command, INCLUDING the specific
81
+ * shape `` `Bash(cat << 'EOF')` `` (a heredoc operator immediately
82
+ * after the command name is never a glob operand — see the heredoc
83
+ * guard below, and matrix row B8).
84
+ *
85
+ * `|| echo <default>` vs `|| true`/`|| :` is the discriminator for B-ii,
86
+ * exactly as `--pick` is Detector A's: both distinguish "a fallback VALUE
87
+ * this shape can silently defeat" from "no fallback value exists to
88
+ * defeat, so there is nothing here for nullglob's success-on-empty to
89
+ * break."
90
+ *
91
+ * Conservative BY CONSTRUCTION where it still applies (40-design.md's
92
+ * B10/B11 and "Law of Leaky Abstractions" section): whether a `nullglob`
93
+ * is in effect is not locally decidable from the line alone, so `cat`
94
+ * still fires unconditionally (B-i) and `ls`'s two exit-code-consuming
95
+ * shapes (B-ii, B-iii) still fire regardless of whether a guard already
96
+ * exists nearby. The remedy (an array expansion, or an existence test
97
+ * before the read) is correct either way, and array expansions carry no
98
+ * `*`/`?` character at all so they are never flagged — the detector does
99
+ * not punish its own fix.
100
+ *
101
+ * Regexes are small, bounded, and non-backtracking BY CONSTRUCTION —
102
+ * `npm run lint:ci` runs CodeQL js/redos over this repo, the same
103
+ * discipline `lint-planning-prompt-drift.cjs` documents in its own header:
104
+ *
105
+ * - CAT_LS_COMMAND_RE's alternation is a FIXED, non-overlapping set (a
106
+ * handful of literal command-position anchors, then a fixed
107
+ * `(cat|ls)`), with one `[ \t]*` quantifier between the anchor and the
108
+ * command name — again no nesting.
109
+ * - HEREDOC_AFTER_COMMAND_RE and FALLBACK_TOKEN_RE are each a single
110
+ * bounded quantifier over a fixed/negated class, same shape as above.
111
+ * - The B-ii/B-iii "does this clause carry a glob, and what terminates
112
+ * it" question is answered by `scanClauseAfterCommand`, a plain
113
+ * LINEAR, single left-to-right character walk — not a regex at all, and
114
+ * therefore not a ReDoS surface by construction rather than by
115
+ * argument: it inspects each character of the remainder exactly once
116
+ * and returns at the first clause-terminating token it finds.
117
+ * - MARKER_RE (the escape-marker parser) is two more `\s*` quantifiers
118
+ * over fixed literals, then a single trailing `(.*)$` — again one
119
+ * quantifier, no nesting.
120
+ *
121
+ * ESCAPE MARKER. A line carrying `# gsd-scan-ignore: <reason>` is exempt
122
+ * ONLY when `<reason>` names an issue (`#NNN`, N a positive integer) or an
123
+ * `http(s)://` URL with an actual host after the scheme — the repo's
124
+ * existing precedent from `tests/commit-files-pathspec.test.cjs`
125
+ * (CONTRIBUTING.md, "Every `commit` invocation in shipped content must
126
+ * declare `--files`"), STARTING from that precedent's predicate
127
+ * (`/#\d+|https?:\/\//`) but DELIBERATELY DIVERGING from it (see
128
+ * `ISSUE_REF_RE`'s own comment for exactly what changed and why) rather than
129
+ * copying it verbatim. The sibling file still carries the looser, unpatched
130
+ * form — this guard's escape hatch is a stricter gate than a commit-message
131
+ * pathspec check needs to be, since an accepted reason here silently
132
+ * exempts a real violation from ever being reported. A marker whose reason is free text, empty, or
133
+ * whitespace-only is reported as a DISTINCT "malformed declaration" error —
134
+ * never silently exempted (that would defeat the guard) and never folded
135
+ * into the ordinary violation list (that would tell an author who already
136
+ * explained themselves that they hadn't, the exact mangle-until-CI-shuts-up
137
+ * loop the marker exists to prevent). Simplification versus the sibling
138
+ * predicate this mirrors: that guard's marker parser tokenizes the whole
139
+ * line to rule out a marker surviving inside quoted argv text (a commit
140
+ * MESSAGE quoting the token). This guard's two detectors never process
141
+ * commit-message-shaped free text, so a plain `#\s*gsd-scan-ignore:` literal
142
+ * match is sufficient here and is not widened to match that guard's
143
+ * quote-awareness it has no corresponding hazard for.
144
+ *
145
+ * RATCHET, not an allowlist. `scripts/baselines/unreachable-guard-drift-baseline.json`
146
+ * mirrors `lint-planning-prompt-drift.cjs`'s shrink-only, count-aware
147
+ * baseline exactly (see that module's header for the full "COUNT, not
148
+ * duplicate rows" rationale) — a recorded `(file, text)` pair acknowledges
149
+ * `count` byte-identical occurrences; fewer this run is a PARTIAL migration
150
+ * (stale), more is an unacknowledged new copy (fresh), zero is a fully
151
+ * migrated pair (stale). Matched on `(file, TRIMMED text)`, never the line
152
+ * number, for the same reason: a workflow `.md` file's line numbers churn on
153
+ * every unrelated edit. Malformed declarations are NEVER ratchet-eligible —
154
+ * they are an authoring mistake in the escape hatch itself, not a
155
+ * migration-in-progress, and always hard-fail (40-design.md's Goodhart's Law
156
+ * section names "run `--update` and record the violation as acknowledged
157
+ * instead of fixing it" as the ratchet's own cheapest gaming path; a
158
+ * malformed marker is exactly the shape of a half-hearted attempt at that,
159
+ * and it is refused rather than laundered into the baseline).
160
+ *
161
+ * SHARED TREE-WALK. `scanTree` / `sanitizeForReport` are consumed from
162
+ * `scripts/lib/drift-scan.cjs`, NOT reimplemented — ADR-3180 Decision 4
163
+ * explicitly rejected "let the new drift guard copy Phase 1's tree-walk",
164
+ * and 40-design.md's Greenspun's Tenth Rule section states the binding
165
+ * consequence plainly: "the 46th guard MUST consume `drift-scan.cjs`, not
166
+ * copy it." See that module for the `toPosixRel`-equivalent rationale
167
+ * (below), the symlink-confinement contract, and the ReDoS-avoidance
168
+ * rationale for its own regex-literal reader (unused by this guard's
169
+ * regexes, which need no literal tokenizer — shared here only for the walk
170
+ * and the report sanitizer).
171
+ *
172
+ * Surfaces scanned (SCAN_DIRS): `gsd-core/workflows`, `commands`, `agents`,
173
+ * `skills` — the prompt-layer markdown that ships to every runtime.
174
+ * SCAN_EXT: `.md` only.
175
+ *
176
+ * KNOWN, ACCEPTED limits (same tradeoffs the sibling guards document):
177
+ * - Detector B's command-position anchor set (line start; `;`, `&`, `|`,
178
+ * `(`; the keywords `if`/`then`/`elif`/`while`/`do`) is what lets
179
+ * `$(cat …)` / `$(ls …)` — the dominant real invocation idiom in this
180
+ * tree — reach the glob check through the `(` anchor. The heredoc guard
181
+ * (`HEREDOC_AFTER_COMMAND_RE`) is what keeps that same `(` anchor from
182
+ * flagging the specific markdown prose shape `` `Bash(cat << 'EOF')` ``
183
+ * — measured against the real tree, it eliminates every such occurrence
184
+ * (a heredoc operator immediately after the command name is, by
185
+ * definition, never a glob operand). A prose sentence that put a real
186
+ * `*`/`?`-bearing word directly after `cat`/`ls` with NO heredoc
187
+ * operator between them (unobserved in this tree) would still be a
188
+ * residual over-flag in the same conservative-by-construction spirit as
189
+ * B10/B11 — accepted for the same reason: removing the `(` anchor
190
+ * entirely would blind the guard to most of the real `$(cat …)` sites
191
+ * it exists to catch, the strictly worse direction (silent false
192
+ * negative vs. a visible, ratchet-acknowledgeable false positive).
193
+ * - B-iii's `if`/`elif`/`while` head-position check is per-token, not a
194
+ * full parse of the conditional's grammar: `if [ -f x ] && ls
195
+ * glob; then` (a compound condition where `ls` is not literally the
196
+ * first word after `if`) is not reachable through the keyword anchor
197
+ * and falls through to B-ii's `||`-fallback check instead, which is the
198
+ * right outcome only when a `||`-fallback is present that isn't a
199
+ * no-op. A compound `if` condition ending the `ls` clause with `;`/end
200
+ * of line and no `||` arm is a genuine, unmeasured (zero observed)
201
+ * miss — left to code review, matching the design's stated per-line
202
+ * textual-scan tradeoff throughout.
203
+ * - The `|| true` / `|| :` no-op carve-out (FALLBACK_TOKEN_RE) inspects
204
+ * only the FIRST token after `||`; a real fallback dressed up as `||
205
+ * (true; echo "surprise")` would read as the no-op and miss — no such
206
+ * shape exists in this tree today (measured), and widening the
207
+ * no-op-detection is a one-line change if one ever appears.
208
+ */
209
+
210
+ const fs = require('node:fs');
211
+ const path = require('node:path');
212
+ const driftScan = require('./lib/drift-scan.cjs');
213
+ const { sanitizeForReport, scanTree } = driftScan;
214
+
215
+ // ─── Detector B — cat <glob> (B-i), ls <glob> … || <real fallback> (B-ii),
216
+ // or ls <glob> at the head of if/elif/while (B-iii) ────────────────────────
217
+ //
218
+ // Command-position anchor: start of line, a shell separator/opener
219
+ // (`;`, `&`, `|`, `(`), or one of the keywords that precede a command
220
+ // (`if`, `then`, `elif`, `while`, `do`) — each followed by optional
221
+ // horizontal whitespace and then the literal command name. Group 1 captures
222
+ // WHICH anchor matched (`''` for start-of-line, since `^` itself consumes no
223
+ // characters; the literal separator char; or the literal keyword) so
224
+ // detectGlobOperand can tell a true `if`/`elif`/`while` head position (B-iii)
225
+ // apart from `then`/`do`/a bare separator, which do not themselves test the
226
+ // following command's exit status. Group 2 captures the command name. A
227
+ // FIXED alternation with one `[ \t]*` quantifier between the anchor and the
228
+ // command name; no nesting, nothing to backtrack.
229
+ const CAT_LS_COMMAND_RE = /(^|[;&|(]|\bif\b|\bthen\b|\belif\b|\bwhile\b|\bdo\b)[ \t]*(cat|ls)\b/;
230
+
231
+ // A heredoc operator immediately after the command name (optional
232
+ // horizontal whitespace, then `<<`) is never a glob operand — matrix row B8,
233
+ // and the mechanism that keeps the markdown-prose shape `` `Bash(cat <<
234
+ // 'EOF')` `` (whose surrounding `**bold**` carries literal `*` characters
235
+ // elsewhere on the line) from ever reaching the glob scan at all. Single
236
+ // bounded quantifier, no nesting.
237
+ const HEREDOC_AFTER_COMMAND_RE = /^[ \t]*<</;
238
+
239
+ // Only `if`/`elif`/`while` test the command that follows THEM directly —
240
+ // `then` and `do` introduce what runs AFTER a test has already passed, not
241
+ // the test itself, so they do not, on their own, make `ls`'s exit code the
242
+ // thing being consumed (B-iii). `cat` (B-i) never consults this set: it
243
+ // fires unconditionally regardless of anchor.
244
+ const EXIT_TESTING_KEYWORDS = new Set(['if', 'elif', 'while']);
245
+
246
+ // The first whitespace/`;`/`)`/`|`/`&`-delimited token of the text
247
+ // immediately after a `||` — used to tell a REAL fallback (B-ii) from the
248
+ // no-op `true`/`:` idiom (~15 measured sites in this tree, all defensive
249
+ // failure-suppression with no fallback value being defeated). Single
250
+ // bounded negated-class quantifier, no nesting.
251
+ const FALLBACK_TOKEN_RE = /^[ \t]*([^\s;)|&]+)/;
252
+
253
+ function isNoopFallback(fallbackText) {
254
+ const m = FALLBACK_TOKEN_RE.exec(fallbackText);
255
+ if (!m) return true; // nothing after `||` at all — no fallback value to defeat
256
+ return m[1] === 'true' || m[1] === ':';
257
+ }
258
+
259
+ /**
260
+ * Plain LINEAR left-to-right character walk over `rest` (the line remainder
261
+ * immediately after a cat/ls command match) — not a regex, and therefore
262
+ * not a ReDoS surface by construction. Inspects each character exactly
263
+ * once and returns as soon as it finds a clause-terminating token:
264
+ * `;` -> the clause ends with no chain at all.
265
+ * `&&` -> a short-circuit "glob matched, so proceed" chain.
266
+ * `||` -> a short-circuit fallback chain; `fallback` is
267
+ * everything after the `||` (for isNoopFallback to
268
+ * classify).
269
+ * a lone `|` -> the clause's STDOUT is piped onward (informational,
270
+ * never a hazard shape this detector fires on).
271
+ * a lone `&` -> backgrounded; not a chain this detector recognizes.
272
+ * end of string -> no chain of any kind.
273
+ * `hasGlob` is tracked across the WHOLE walk regardless of where the scan
274
+ * stops, since a `*`/`?` can appear anywhere in the operand region before
275
+ * the terminator.
276
+ */
277
+ function scanClauseAfterCommand(rest) {
278
+ let hasGlob = false;
279
+ for (let i = 0; i < rest.length; i++) {
280
+ const ch = rest[i];
281
+ if (ch === '*' || ch === '?') { hasGlob = true; continue; }
282
+ if (ch === ';') return { hasGlob, terminator: ';', fallback: null };
283
+ if (ch === '&' && rest[i + 1] === '&') return { hasGlob, terminator: '&&', fallback: null };
284
+ if (ch === '|' && rest[i + 1] === '|') return { hasGlob, terminator: '||', fallback: rest.slice(i + 2) };
285
+ if (ch === '|') return { hasGlob, terminator: '|', fallback: null };
286
+ if (ch === '&') return { hasGlob, terminator: '&', fallback: null };
287
+ }
288
+ return { hasGlob, terminator: null, fallback: null };
289
+ }
290
+
291
+ /**
292
+ * Pure: does `line` carry one of Detector B's three fired shapes? Returns
293
+ * `{ command }` (`cat` or `ls`) or `null`. See the module header for the
294
+ * B-i/B-ii/B-iii scoping and what deliberately does NOT fire.
295
+ */
296
+ function detectGlobOperand(line) {
297
+ const anchor = CAT_LS_COMMAND_RE.exec(line);
298
+ if (!anchor) return null;
299
+ const anchorToken = anchor[1];
300
+ const command = anchor[2];
301
+ const rest = line.slice(anchor.index + anchor[0].length);
302
+ if (HEREDOC_AFTER_COMMAND_RE.test(rest)) return null;
303
+
304
+ const { hasGlob, terminator, fallback } = scanClauseAfterCommand(rest);
305
+ if (!hasGlob) return null;
306
+
307
+ if (command === 'cat') return { command }; // B-i: unconditional.
308
+
309
+ // command === 'ls': B-iii (head of a real conditional test) or B-ii (a
310
+ // real, non-no-op `||` fallback). Neither a lone `|` (stdout piped
311
+ // onward) nor `|| true`/`|| :` nor a bare `;`/end-of-line qualifies.
312
+ if (EXIT_TESTING_KEYWORDS.has(anchorToken)) return { command };
313
+ if (terminator === '||' && fallback !== null && !isNoopFallback(fallback)) return { command };
314
+ return null;
315
+ }
316
+
317
+ // ─── Escape marker ─────────────────────────────────────────────────────────
318
+ //
319
+ // `# gsd-scan-ignore: <reason>`. Two `\s*` quantifiers over fixed literals,
320
+ // then a single trailing `(.*)$` — one quantifier, no nesting. Lines are
321
+ // split via `/\r?\n/` (see findUnreachableGuardDrift) before this ever runs,
322
+ // so `.` never has to reason about a trailing `\r` — the pitfall the CRLF
323
+ // coverage in the test matrix (P1-P4) exists to catch.
324
+ const MARKER_RE = /#\s*gsd-scan-ignore:\s*(.*)$/;
325
+
326
+ // DELIBERATE DIVERGENCE from tests/commit-files-pathspec.test.cjs's own
327
+ // `ISSUE_REF_RE` (`/#\d+|https?:\/\//`), which this predicate started as a
328
+ // copy of. That sibling form validates FORMAT only, and two shapes satisfy
329
+ // it while naming nothing real:
330
+ // - `#0` matches `#\d+` (`\d+` allows a leading zero / an all-zero run),
331
+ // silently exempting a violation under a reason that names no positive
332
+ // issue number.
333
+ // - a bare `http://` / `https://` matches `https?:\/\/` with nothing
334
+ // after the scheme — no host, so no URL is actually named.
335
+ // Both are closed here: an issue ref requires a POSITIVE integer
336
+ // (`#[1-9]\d*` — no leading-zero/all-zero match), and a URL requires at
337
+ // least one non-whitespace character after the scheme as its host
338
+ // (`https?:\/\/[^\s]+`). This guard's escape hatch is a stricter gate than
339
+ // the sibling's commit-message pathspec check needs to be — an accepted
340
+ // reason here silently exempts a real violation from ever being reported —
341
+ // so the sibling is intentionally left at its own, looser form (not edited
342
+ // by this change) rather than tightened to match.
343
+ // Still one bounded quantifier per alternative, no nesting: `\d*` over a
344
+ // fixed digit class, `[^\s]+` over a fixed negated class. Non-backtracking,
345
+ // same as every other regex in this module (see the module header's ReDoS
346
+ // section).
347
+ const ISSUE_REF_RE = /#[1-9]\d*|https?:\/\/[^\s]+/;
348
+
349
+ // `scanTree` (scripts/lib/drift-scan.cjs) builds its repo-relative path via
350
+ // `path.relative()`, which uses NATIVE separators: on Windows that is
351
+ // `gsd-core\workflows\plan-phase.md`, while the committed baseline stores
352
+ // POSIX paths. Normalized UNCONDITIONALLY — never gated on
353
+ // `process.platform` — for the exact reason `lint-planning-prompt-drift.cjs`
354
+ // documents at its own `toPosixRel`: a platform-conditional normalizer is
355
+ // itself the bug, since it makes the POSIX path the only tested case
356
+ // (PR #3223).
357
+ function toPosixRel(relPath) {
358
+ return relPath.replace(/\\/g, '/');
359
+ }
360
+
361
+ // Prompt-layer markdown that ships to every runtime.
362
+ const SCAN_DIRS = ['gsd-core/workflows', 'commands', 'agents', 'skills'];
363
+ const SCAN_EXT = new Set(['.md']);
364
+
365
+ const BASELINE_REL_PATH = path.join('scripts', 'baselines', 'unreachable-guard-drift-baseline.json');
366
+
367
+ // The tracking issue this guard's own baseline entries are owned by, absent
368
+ // a more specific site owner named at `--update` time. #3409 is this
369
+ // guard's own issue: any Detector B site it finds that this PR does not
370
+ // convert is a "one careless line from the same class" per 40-design.md's
371
+ // Postel's Law section, tracked here until a per-site conversion lands.
372
+ // (Detector A's own entries, if any had ever existed, would have been
373
+ // tracked the same way until the upstream `--pick` contract fix landed —
374
+ // #3884 — but the baseline shipped with zero Detector A entries; see the
375
+ // retirement note at the top of this file.)
376
+ const RATCHET_OWNER_ISSUE = '#3409';
377
+
378
+ /**
379
+ * Pure: scan `text` (one file's content) for Detector B violations and
380
+ * malformed escape-marker declarations. `relPath` is the repo-relative path
381
+ * (native separators or POSIX, either accepted) — normalized via
382
+ * `toPosixRel` and attached as `file` on every result.
383
+ *
384
+ * Returns `{ violations, malformed }`:
385
+ * - `violations`: `[{ file, line, kind: 'B', found, text }]` — `text` is
386
+ * the TRIMMED source line (the baseline key), `found` names the
387
+ * discriminating command (`cat`/`ls`). `kind` is retained as a field
388
+ * (rather than dropped now that only one detector remains) so the
389
+ * baseline JSON shape and the `--json` report shape are unchanged by
390
+ * Detector A's retirement.
391
+ * - `malformed`: `[{ file, line, text, reason }]` — an ATTEMPTED
392
+ * `# gsd-scan-ignore:` declaration whose reason names no issue and no
393
+ * URL. Checked on EVERY line independent of whether that line also
394
+ * matches a detector (a comment-only malformed declaration is still a
395
+ * malformed declaration) — never ratchet-eligible.
396
+ *
397
+ * Lines are split on `/\r?\n/` so CRLF input carries no trailing `\r` into
398
+ * either the detector regexes or the baseline key (`text.trim()` would
399
+ * catch most of this anyway, per `String.prototype.trim`'s LineTerminator
400
+ * handling, but MARKER_RE's trailing `(.*)$` specifically needs the split
401
+ * to have already happened — `.` excludes `\r` from its own match).
402
+ */
403
+ function findUnreachableGuardDrift(text, relPath) {
404
+ const file = toPosixRel(relPath);
405
+ const violations = [];
406
+ const malformed = [];
407
+ const lines = text.split(/\r?\n/);
408
+ for (let i = 0; i < lines.length; i++) {
409
+ const line = lines[i];
410
+ const lineNo = i + 1;
411
+
412
+ let exempt = false;
413
+ const markerMatch = MARKER_RE.exec(line);
414
+ if (markerMatch) {
415
+ const reason = markerMatch[1];
416
+ if (ISSUE_REF_RE.test(reason)) {
417
+ exempt = true;
418
+ } else {
419
+ malformed.push({ file, line: lineNo, text: line.trim(), reason: reason.trim() });
420
+ exempt = true; // malformed declarations are reported on their own terms, never as a plain violation too (design A14 / matrix M3-M5)
421
+ }
422
+ }
423
+ if (exempt) continue;
424
+
425
+ const globInfo = detectGlobOperand(line);
426
+ if (globInfo) {
427
+ violations.push({ file, line: lineNo, kind: 'B', found: globInfo.command, text: line.trim() });
428
+ }
429
+ }
430
+ return { violations, malformed };
431
+ }
432
+
433
+ /**
434
+ * Scan the prompt-layer markdown tree and return every violation and
435
+ * malformed declaration, each annotated with the repo-relative file path
436
+ * (POSIX-normalized — see `toPosixRel`).
437
+ */
438
+ function scanRepo(root) {
439
+ const violations = [];
440
+ const malformed = [];
441
+ scanTree({
442
+ root,
443
+ scanDirs: SCAN_DIRS,
444
+ scanExt: SCAN_EXT,
445
+ onFile(rel, text) {
446
+ const found = findUnreachableGuardDrift(text, rel);
447
+ violations.push(...found.violations);
448
+ malformed.push(...found.malformed);
449
+ return []; // scanTree's own accumulator is unused; we track both lists ourselves so its single flat list is never asked to carry two shapes.
450
+ },
451
+ });
452
+ return { violations, malformed };
453
+ }
454
+
455
+ /**
456
+ * Frozen outcome-reason enum. CONTRIBUTING.md's "Prohibited: Raw Text
457
+ * Matching on Test Outputs" requires a typed structured surface wherever
458
+ * this module produces human-readable text — mirrors
459
+ * `gsd-core/bin/verify-reapply-patches.cjs`'s own `REASON` map exactly:
460
+ * `main()`'s `--json` mode and every `loadBaseline` per-error object carry
461
+ * one of these codes instead of free prose, and tests assert on the code,
462
+ * never on the rendered message. Adding a new reason requires updating this
463
+ * enum, the `--json` emission/`loadBaseline` call site that produces it, AND
464
+ * the test that locks `Object.keys(REASON).sort()` — three coordinated
465
+ * changes that keep the code surface from drifting from the test surface.
466
+ */
467
+ const REASON = Object.freeze({
468
+ // main() top-level outcomes (non---update and --update paths).
469
+ OK_NO_VIOLATIONS: 'ok_no_violations',
470
+ OK_BASELINE_UPDATED: 'ok_baseline_updated',
471
+ FAIL_FRESH_VIOLATION: 'fail_fresh_violation',
472
+ FAIL_STALE_ENTRY: 'fail_stale_entry',
473
+ FAIL_MALFORMED_MARKER: 'fail_malformed_marker',
474
+ FAIL_BASELINE_LOAD: 'fail_baseline_load',
475
+ // loadBaseline per-error outcomes — each a distinct baseline-load failure
476
+ // class (mirrors lint-planning-prompt-drift.cjs's loadBaseline validation).
477
+ FAIL_BASELINE_MISSING: 'fail_baseline_missing',
478
+ FAIL_BASELINE_EMPTY: 'fail_baseline_empty',
479
+ FAIL_BASELINE_INVALID_JSON: 'fail_baseline_invalid_json',
480
+ FAIL_BASELINE_NOT_OBJECT: 'fail_baseline_not_object',
481
+ FAIL_BASELINE_ENTRIES_NOT_ARRAY: 'fail_baseline_entries_not_array',
482
+ FAIL_BASELINE_ENTRY_NOT_OBJECT: 'fail_baseline_entry_not_object',
483
+ FAIL_BASELINE_ENTRY_FIELD_INVALID: 'fail_baseline_entry_field_invalid',
484
+ FAIL_BASELINE_ENTRY_COUNT_INVALID: 'fail_baseline_entry_count_invalid',
485
+ });
486
+
487
+ /**
488
+ * Read and parse the ratchet baseline. Returns `{ entries, errors }` —
489
+ * `entries` is `[]` and `errors` is an array of STRUCTURED error objects
490
+ * (`{ reason: REASON.*, message, ... }`) when the file is missing, empty,
491
+ * invalid JSON, or malformed. Mirrors `lint-planning-prompt-drift.cjs`'s
492
+ * `loadBaseline` validation exactly (same failure classes: missing, empty,
493
+ * invalid JSON, non-object JSON — including the `null`/array/scalar cases a
494
+ * bare `typeof === 'object'` check would miss — a non-array `entries`
495
+ * field, and per-entry validation of `file`/`text`/`count`). `message` is a
496
+ * human-readable string for the console formatter only; callers (and
497
+ * tests) must key off `reason`, never parse `message`.
498
+ */
499
+ function loadBaseline(root) {
500
+ const baselinePath = path.join(root, BASELINE_REL_PATH);
501
+ if (!fs.existsSync(baselinePath)) {
502
+ return {
503
+ entries: [],
504
+ errors: [{
505
+ reason: REASON.FAIL_BASELINE_MISSING,
506
+ message: `${BASELINE_REL_PATH} is missing — run \`node scripts/lint-unreachable-guard-drift.cjs --update\` to generate it`,
507
+ }],
508
+ };
509
+ }
510
+ const raw = fs.readFileSync(baselinePath, 'utf8');
511
+ if (raw.trim() === '') {
512
+ return {
513
+ entries: [],
514
+ errors: [{ reason: REASON.FAIL_BASELINE_EMPTY, message: `${BASELINE_REL_PATH} is present but empty` }],
515
+ };
516
+ }
517
+ let doc;
518
+ try {
519
+ doc = JSON.parse(raw);
520
+ } catch (err) {
521
+ return {
522
+ entries: [],
523
+ errors: [{
524
+ reason: REASON.FAIL_BASELINE_INVALID_JSON,
525
+ message: `${BASELINE_REL_PATH} is not valid JSON: ${err.message}`,
526
+ parseError: err.message,
527
+ }],
528
+ };
529
+ }
530
+ if (doc === null || typeof doc !== 'object' || Array.isArray(doc)) {
531
+ const gotType = Array.isArray(doc) ? 'array' : typeof doc;
532
+ return {
533
+ entries: [],
534
+ errors: [{
535
+ reason: REASON.FAIL_BASELINE_NOT_OBJECT,
536
+ message: `${BASELINE_REL_PATH} must be a JSON object, got ${gotType}`,
537
+ gotType,
538
+ }],
539
+ };
540
+ }
541
+ if (!Array.isArray(doc.entries)) {
542
+ return {
543
+ entries: [],
544
+ errors: [{
545
+ reason: REASON.FAIL_BASELINE_ENTRIES_NOT_ARRAY,
546
+ message: `${BASELINE_REL_PATH}: "entries" must be an array, got ${JSON.stringify(doc.entries)}`,
547
+ entriesValue: doc.entries,
548
+ }],
549
+ };
550
+ }
551
+ const errors = [];
552
+ const entries = [];
553
+ doc.entries.forEach((entry, i) => {
554
+ const where = `${BASELINE_REL_PATH}.entries[${i}]`;
555
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
556
+ errors.push({
557
+ reason: REASON.FAIL_BASELINE_ENTRY_NOT_OBJECT,
558
+ message: `${where} must be an object, got ${JSON.stringify(entry)}`,
559
+ index: i,
560
+ where,
561
+ });
562
+ return;
563
+ }
564
+ if (typeof entry.file !== 'string' || entry.file === '') {
565
+ errors.push({
566
+ reason: REASON.FAIL_BASELINE_ENTRY_FIELD_INVALID,
567
+ message: `${where}.file must be a non-empty string, got ${JSON.stringify(entry.file)}`,
568
+ index: i,
569
+ where,
570
+ field: 'file',
571
+ value: entry.file,
572
+ });
573
+ return;
574
+ }
575
+ if (typeof entry.text !== 'string' || entry.text === '') {
576
+ errors.push({
577
+ reason: REASON.FAIL_BASELINE_ENTRY_FIELD_INVALID,
578
+ message: `${where}.text must be a non-empty string, got ${JSON.stringify(entry.text)}`,
579
+ index: i,
580
+ where,
581
+ field: 'text',
582
+ value: entry.text,
583
+ });
584
+ return;
585
+ }
586
+ // `count` is optional on read (diffAgainstBaseline defaults an absent
587
+ // count to 1) but when present must be a positive integer.
588
+ if (entry.count !== undefined && !(Number.isInteger(entry.count) && entry.count >= 1)) {
589
+ errors.push({
590
+ reason: REASON.FAIL_BASELINE_ENTRY_COUNT_INVALID,
591
+ message: `${where}.count must be a positive integer when present, got ${JSON.stringify(entry.count)}`,
592
+ index: i,
593
+ where,
594
+ value: entry.count,
595
+ });
596
+ return;
597
+ }
598
+ entries.push(entry);
599
+ });
600
+ return { entries, errors };
601
+ }
602
+
603
+ /**
604
+ * Diff scanned `violations` against baseline `entries`, matched by the pair
605
+ * (`file`, TRIMMED `text`) — never the line number — and COUNT-aware, same
606
+ * semantics as `lint-planning-prompt-drift.cjs`'s `diffAgainstBaseline`:
607
+ * - `fresh`: violations whose `(file, text)` pair is not in the baseline
608
+ * at all, PLUS any occurrences of a KNOWN pair beyond its acknowledged
609
+ * `count`.
610
+ * - `stale`: baseline entries whose actual occurrence count this run is
611
+ * LESS than their acknowledged `count` (zero is the fully-migrated
612
+ * case; a positive-but-short count is a PARTIAL migration).
613
+ */
614
+ function diffAgainstBaseline(violations, baseline) {
615
+ const key = (file, text) => `${file} ${text}`;
616
+
617
+ const actualByKey = new Map();
618
+ for (const v of violations) {
619
+ const k = key(v.file, v.text);
620
+ let vs = actualByKey.get(k);
621
+ if (!vs) { vs = []; actualByKey.set(k, vs); }
622
+ vs.push(v);
623
+ }
624
+
625
+ const knownKeys = new Set(baseline.map((e) => key(e.file, e.text)));
626
+
627
+ const fresh = [];
628
+ const stale = [];
629
+
630
+ for (const [k, vs] of actualByKey) {
631
+ if (!knownKeys.has(k)) fresh.push(...vs);
632
+ }
633
+
634
+ for (const entry of baseline) {
635
+ const k = key(entry.file, entry.text);
636
+ const expected = entry.count ?? 1;
637
+ const vs = actualByKey.get(k) || [];
638
+ const actual = vs.length;
639
+ if (actual < expected) {
640
+ stale.push({ ...entry, count: expected, actualCount: actual });
641
+ } else if (actual > expected) {
642
+ fresh.push(...vs.slice(expected));
643
+ }
644
+ }
645
+
646
+ return { fresh, stale };
647
+ }
648
+
649
+ /** Stable sort: by `file`, then by `text`. */
650
+ function sortEntries(entries) {
651
+ return [...entries].sort((a, b) => {
652
+ if (a.file !== b.file) return a.file < b.file ? -1 : 1;
653
+ if (a.text !== b.text) return a.text < b.text ? -1 : 1;
654
+ return 0;
655
+ });
656
+ }
657
+
658
+ /**
659
+ * Collapse `violations` into one baseline row per distinct (file, text)
660
+ * pair, carrying a `count` of how many occurrences that pair has in THIS
661
+ * run. Pure; no I/O.
662
+ */
663
+ function dedupeViolationsForBaseline(violations) {
664
+ const order = [];
665
+ const byKey = new Map();
666
+ for (const v of violations) {
667
+ const k = `${v.file} ${v.text}`;
668
+ let entry = byKey.get(k);
669
+ if (!entry) {
670
+ entry = { file: v.file, text: v.text, kind: v.kind, owner_issue: RATCHET_OWNER_ISSUE, count: 0 };
671
+ byKey.set(k, entry);
672
+ order.push(entry);
673
+ }
674
+ entry.count += 1;
675
+ }
676
+ return order;
677
+ }
678
+
679
+ function writeBaseline(root, violations) {
680
+ const entries = sortEntries(dedupeViolationsForBaseline(violations));
681
+ const doc = {
682
+ $comment:
683
+ '#3409 unreachable-shell-guard ratchet. See scripts/lint-unreachable-guard-drift.cjs. '
684
+ + 'SHRINK-ONLY: entries are removed as sites migrate off the unreachable-arm shape; new or '
685
+ + 'changed entries fail lint:ci. `count` is the number of byte-identical (file, text) '
686
+ + 'occurrences acknowledged at this site — a run producing fewer fails as a partial migration, '
687
+ + 'more fails as an unacknowledged new copy.',
688
+ entries,
689
+ };
690
+ const baselinePath = path.join(root, BASELINE_REL_PATH);
691
+ fs.mkdirSync(path.dirname(baselinePath), { recursive: true });
692
+ fs.writeFileSync(baselinePath, `${JSON.stringify(doc, null, 2)}\n`, 'utf8');
693
+ return entries;
694
+ }
695
+
696
+ /**
697
+ * `--json` mode emits ONE structured JSON object to stdout in place of the
698
+ * human formatter below — the typed IR CONTRIBUTING.md's "Prohibited: Raw
699
+ * Text Matching on Test Outputs" requires. The human formatter's wording is
700
+ * untouched (operator console use only); `emitJson` is the only new output
701
+ * surface, gated on `json` so the two never interleave on the same stream.
702
+ */
703
+ function main() {
704
+ const root = path.join(__dirname, '..');
705
+ const update = process.argv.includes('--update');
706
+ const json = process.argv.includes('--json');
707
+ const { violations, malformed } = scanRepo(root);
708
+
709
+ function emitJson(report) {
710
+ if (json) process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
711
+ }
712
+
713
+ if (update) {
714
+ if (malformed.length > 0) {
715
+ if (!json) {
716
+ process.stderr.write('unreachable-guard-drift: malformed `# gsd-scan-ignore:` declaration(s) — fix these before regenerating the baseline (they are never ratchet-eligible):\n');
717
+ for (const m of malformed) {
718
+ process.stderr.write(` ${sanitizeForReport(m.file)}:${m.line} ${sanitizeForReport(m.text)}\n`);
719
+ }
720
+ process.stderr.write('\n remedy: the reason after `# gsd-scan-ignore:` must name a tracking issue (#NNN) or an http(s):// URL.\n');
721
+ }
722
+ emitJson({ reason: REASON.FAIL_MALFORMED_MARKER, violations: [], malformed, stale: [], baselineErrors: [] });
723
+ process.exitCode = 1;
724
+ return;
725
+ }
726
+ const entries = writeBaseline(root, violations);
727
+ if (!json) {
728
+ process.stdout.write(`ok unreachable-guard-drift: baseline regenerated with ${entries.length} entr${entries.length === 1 ? 'y' : 'ies'}\n`);
729
+ }
730
+ emitJson({ reason: REASON.OK_BASELINE_UPDATED, violations: [], malformed: [], stale: [], baselineErrors: [], updatedEntryCount: entries.length });
731
+ return;
732
+ }
733
+
734
+ const { entries: baseline, errors } = loadBaseline(root);
735
+ if (errors.length > 0) {
736
+ if (!json) {
737
+ process.stderr.write('unreachable-guard-drift: baseline load error(s):\n');
738
+ // OUTPUT SEAM: `loadBaseline`'s `message` strings embed
739
+ // `JSON.stringify(entry.file)` / `JSON.stringify(entry.text)` /
740
+ // `JSON.stringify(entry)` verbatim, and `JSON.stringify` escapes only
741
+ // code points below 0x20 — it passes C1 controls (0x7F-0x9F) and the
742
+ // bidi/zero-width controls (U+202E RTL override, U+2066-U+2069,
743
+ // U+2028, U+2029) through UNESCAPED. Without `sanitizeForReport` here, a
744
+ // crafted `entries[].file`/`.text` value in the baseline JSON could
745
+ // land an active bidi override straight into CI console output — the
746
+ // exact report-spoofing class every violation/malformed field below is
747
+ // already routed through `sanitizeForReport` to prevent.
748
+ for (const e of errors) process.stderr.write(` ${sanitizeForReport(e.message)}\n`);
749
+ }
750
+ emitJson({ reason: REASON.FAIL_BASELINE_LOAD, violations: [], malformed: [], stale: [], baselineErrors: errors });
751
+ process.exitCode = 1;
752
+ return;
753
+ }
754
+
755
+ const { fresh, stale } = diffAgainstBaseline(violations, baseline);
756
+
757
+ if (fresh.length === 0 && stale.length === 0 && malformed.length === 0) {
758
+ if (!json) {
759
+ process.stdout.write(`ok unreachable-guard-drift: no unacknowledged unreachable shell-guard shapes in the prompt layer (${baseline.length} known)\n`);
760
+ }
761
+ emitJson({ reason: REASON.OK_NO_VIOLATIONS, violations: [], malformed: [], stale: [], baselineErrors: [], knownCount: baseline.length });
762
+ return;
763
+ }
764
+
765
+ if (!json) {
766
+ if (fresh.length > 0) {
767
+ process.stderr.write('unreachable-guard-drift: NEW unreachable shell-guard shape(s) found in the prompt layer.\n');
768
+ process.stderr.write('Detector B (cat/ls over a glob operand): under a nullglob set elsewhere in the same shell\n');
769
+ process.stderr.write('session, an unmatched glob reads from stdin (cat) or lists the cwd (ls) — use an array\n');
770
+ process.stderr.write('expansion or an existence test instead.\n');
771
+ process.stderr.write(`Or, if this is a deliberate wrong-example, declare it with # gsd-scan-ignore: #NNN, or add an\n`);
772
+ process.stderr.write(`acknowledged entry to ${BASELINE_REL_PATH} via --update:\n`);
773
+ for (const v of fresh) {
774
+ process.stderr.write(` ${sanitizeForReport(v.file)}:${v.line} [${v.kind}] ${sanitizeForReport(v.found)} ${sanitizeForReport(v.text)}\n`);
775
+ }
776
+ }
777
+
778
+ if (stale.length > 0) {
779
+ process.stderr.write('\nunreachable-guard-drift: STALE baseline entr' + (stale.length === 1 ? 'y' : 'ies') + " (fully migrated, or a PARTIAL migration — fewer occurrences found than acknowledged; delete or re-record the row):\n");
780
+ for (const e of stale) {
781
+ process.stderr.write(` ${sanitizeForReport(e.file)} ${sanitizeForReport(e.text)} (found ${e.actualCount}/${e.count} acknowledged occurrence${e.count === 1 ? '' : 's'})\n`);
782
+ }
783
+ process.stderr.write(`\n remedy: node scripts/lint-unreachable-guard-drift.cjs --update\n`);
784
+ }
785
+
786
+ if (malformed.length > 0) {
787
+ process.stderr.write('\nunreachable-guard-drift: malformed `# gsd-scan-ignore:` declaration(s) — never ratchet-eligible, must be fixed directly:\n');
788
+ for (const m of malformed) {
789
+ process.stderr.write(` ${sanitizeForReport(m.file)}:${m.line} ${sanitizeForReport(m.text)}\n`);
790
+ }
791
+ process.stderr.write('\n remedy: the reason after `# gsd-scan-ignore:` must name a tracking issue (#NNN) or an http(s):// URL.\n');
792
+ }
793
+ }
794
+
795
+ const reason = fresh.length > 0
796
+ ? REASON.FAIL_FRESH_VIOLATION
797
+ : stale.length > 0
798
+ ? REASON.FAIL_STALE_ENTRY
799
+ : REASON.FAIL_MALFORMED_MARKER;
800
+ emitJson({ reason, violations: fresh, malformed, stale, baselineErrors: [] });
801
+
802
+ process.exitCode = 1;
803
+ }
804
+
805
+ if (require.main === module) main();
806
+
807
+ module.exports = {
808
+ findUnreachableGuardDrift,
809
+ detectGlobOperand,
810
+ scanRepo,
811
+ toPosixRel,
812
+ loadBaseline,
813
+ diffAgainstBaseline,
814
+ dedupeViolationsForBaseline,
815
+ sortEntries,
816
+ writeBaseline,
817
+ CAT_LS_COMMAND_RE,
818
+ HEREDOC_AFTER_COMMAND_RE,
819
+ EXIT_TESTING_KEYWORDS,
820
+ FALLBACK_TOKEN_RE,
821
+ isNoopFallback,
822
+ scanClauseAfterCommand,
823
+ MARKER_RE,
824
+ ISSUE_REF_RE,
825
+ SCAN_DIRS,
826
+ SCAN_EXT,
827
+ BASELINE_REL_PATH,
828
+ RATCHET_OWNER_ISSUE,
829
+ REASON,
830
+ };