@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,805 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Anti-divergence drift guard for the STATE.md FIELD-EXTRACTION FALLBACK
6
+ * CHAIN (epic #3180, issue #3187, ADR-3180 Decision 4, spec §7.7).
7
+ *
8
+ * The derivation this guard protects is "read a STATE.md field: prefer the
9
+ * YAML frontmatter scalar (string, trimmed-non-empty; or number/boolean
10
+ * coerced to a string), else fall back to the body field via
11
+ * `stateExtractField(body, bodyField)`". `src/state-document.cts` is
12
+ * DESIGNATED the single canonical owner of this grammar (issue #3187 Phase
13
+ * 5), but as of this guard's authorship (Phase 5's guard-first step, per
14
+ * ADR-3180 Amendment 3's standing rule) the canonical function does not yet
15
+ * exist there — this script is written to discover every existing
16
+ * re-derivation BEFORE any src/ file is touched, exactly the ordering
17
+ * `lint-plan-count-drift.cjs` and its siblings established.
18
+ *
19
+ * Per ADR-3180 Decision 4(a) this guard discovers call sites by SCANNING THE
20
+ * WHOLE `src/` TREE, not by consulting an allowlist of known files. Per
21
+ * Decision 4(d) that surface is widened further still: `src/` alone is
22
+ * itself the forbidden allowlist, one directory wide, so this guard ALSO
23
+ * scans the prompt-layer markdown (`gsd-core/workflows`, `commands`,
24
+ * `agents`, `skills`) for a PROSE re-derivation of the same chain — see the
25
+ * "PROMPT-LAYER PROSE DETECTION" section below `findStateFieldDrift`.
26
+ *
27
+ * DETECTION: FUNCTION-SCOPED CO-OCCURRENCE, not a bounded line-window.
28
+ * An earlier draft of this guard used a fixed line-distance window between
29
+ * the ladder and the fallback call. That shape was REJECTED: it produced a
30
+ * documented "known miss" on live copies inside the very function it was
31
+ * scanning (state.cts `cmdStateSnapshot`'s `Current Plan` / `Total Plans in
32
+ * Phase` / `Status` / `Progress` / `Last Activity` reads all sit further
33
+ * from the ladder than any defensible line count), which means the guard
34
+ * could report "0 re-derivations" after only the nearest few copies were
35
+ * migrated while several more of the SAME kind survived untouched in the
36
+ * SAME function — "a zero it did not earn" (ADR-3180 Decision 4(a)). A fixed
37
+ * N is also trivially gameable by reflow (Goodhart's law): moving a call one
38
+ * line further from its ladder silences the guard without changing the
39
+ * derivation at all. The window is dropped entirely; there is no magic
40
+ * number anywhere in this detection.
41
+ *
42
+ * The invariant instead: a NAMED FUNCTION that BOTH (a) contains a
43
+ * "frontmatter scalar coercion ladder" — the SAME operand compared via
44
+ * `typeof OPERAND === 'number'` and `typeof OPERAND === 'boolean'`
45
+ * (TYPEOF_TIER_CLAUSE_RE, order-independent, member/computed operands
46
+ * included, an optional `typeof OPERAND === 'string'` tier tolerated
47
+ * anywhere among them — see the ternary shape in `cmdStatePrune` below),
48
+ * within LADDER_WINDOW_LINES of each other — ANYWHERE in its own body,
49
+ * INCLUDING inside a nested closure it defines, AND (b) calls
50
+ * `stateExtractField(` anywhere in that same body, is re-deriving the
51
+ * fm-else-body fallback chain. EVERY `stateExtractField(` call line inside
52
+ * such a function is reported, not just the first.
53
+ *
54
+ * LADDER DETECTION, evasion-resistant shapes (see "LADDER DETECTION DETAIL"
55
+ * below for the full account, including the one shape still NOT caught):
56
+ * - operand may be a bare identifier (`v`), a dotted member expression
57
+ * (`fm.key`), or a computed access (`fm[key]`) — matched via
58
+ * OPERAND_SOURCE and compared as captured TEXT, not a bare-identifier
59
+ * backreference.
60
+ * - the two required tiers ('number', 'boolean') may appear in EITHER
61
+ * order; an optional third 'string' tier may sit anywhere among them.
62
+ * - the two clauses may sit on the SAME line (the classic single-line
63
+ * `||` chain) or on DIFFERENT lines within LADDER_WINDOW_LINES of each
64
+ * other — including as two entirely separate `if` statements, not just
65
+ * one expression wrapped across a line break.
66
+ *
67
+ * FUNCTION ATTRIBUTION. Two declaration shapes are recognised as opening a
68
+ * new named function scope:
69
+ * - `function NAME(...) {` (FUNCTION_DECL_RE) — top-level OR nested, at
70
+ * any indentation. `src/smart-entry.cts`'s `fmScalar` (line 144) is this
71
+ * shape, and is itself top-level.
72
+ * - `const NAME = (...): ReturnType => {` (ARROW_CONST_RE) — an arrow
73
+ * function assigned to a `const`, with a block body (`=> {`, not an
74
+ * expression body like `=> ({...})`, which never opens a new function
75
+ * frame — its `{` is an object literal, still counted toward brace
76
+ * depth, but attributes no name). `src/state.cts`'s `cmdStateSnapshot`
77
+ * defines its `fmScalar` (line 1464) this way, NESTED inside
78
+ * `cmdStateSnapshot` itself.
79
+ * `ARROW_CONST_RE`'s pattern requires `=>\s*\{` literally, so it only ever
80
+ * matches a line that already carries its opening brace — pushed
81
+ * immediately. `FUNCTION_DECL_RE` carries no such guarantee: a multi-line
82
+ * signature (parameters and/or a return-type annotation spilling onto later
83
+ * lines) matches on a line with NO `{` at all. An earlier version of this
84
+ * guard pushed such a match immediately anyway, recording `openDepth` at the
85
+ * ENCLOSING scope's depth rather than the function's own — for a top-level
86
+ * function, `openDepth: 0`, and because real code never reaches negative
87
+ * depth, a frame pushed with `openDepth: 0` could NEVER pop, sitting at the
88
+ * bottom of the stack for the rest of the file and silently misattributing
89
+ * every later line with no OTHER open frame to it. `src/state.cts`'s
90
+ * `preferNewerLastActivity` (a 4-line signature, `{` on its own line) is the
91
+ * live instance that surfaced this; it caused no observed false violation
92
+ * only because nothing ever called `stateExtractField(` at true module scope
93
+ * after it, not because the tracking was sound. `buildFunctionInfo` now
94
+ * DEFERS a `FUNCTION_DECL_RE` match (`pendingDeclName`) across lines until
95
+ * the first subsequent line whose brace count actually increases, and pushes
96
+ * the frame THERE, with `openDepth` computed from that line's `depth` —
97
+ * matching every other frame's push convention. A pending name is abandoned
98
+ * (never pushed) if a `;` terminates the statement before any `{` appears —
99
+ * a type-only declaration, `declare function`, or overload signature, none
100
+ * of which open a body.
101
+ * Function scopes NEST via a brace-depth stack: entering either shape pushes
102
+ * a frame; the frame pops once brace depth returns below the depth recorded
103
+ * when it was pushed. A line is attributed to the INNERMOST currently-open
104
+ * named frame (falling back to whatever enclosing frame IS open — typically
105
+ * the nearest enclosing top-level function — when a line sits between two
106
+ * sibling nested scopes; module-level code with no open frame is
107
+ * unattributed and therefore never a violation, matching "if you cannot
108
+ * identify one" in the design brief).
109
+ *
110
+ * TRANSITIVE ladder attribution is what makes `cmdStateSnapshot` (whose
111
+ * OWN top-level statements never spell the ladder themselves — only its
112
+ * nested `fmScalar` closure does) still register as ladder-bearing: when
113
+ * LADDER_RE matches a line, EVERY frame currently open on the stack at that
114
+ * point — not just the innermost — is marked ladder-bearing, because a
115
+ * nested closure's body is lexically part of every one of its enclosing
116
+ * functions' own bodies. `stateExtractField(` calls are attributed to the
117
+ * INNERMOST frame only (no transitivity needed there: the call already sits
118
+ * directly inside whichever frame is innermost at that point).
119
+ *
120
+ * BRACE-DEPTH COUNTING runs over `scanCode`'s per-line, cross-file output
121
+ * (see that function's own header for the full rationale and the concrete
122
+ * bug its cross-line comment/template tracking fixes) — comments and
123
+ * quoted/backtick string and template literal CONTENTS are already removed
124
+ * before a single brace is counted, escape-aware and threaded across line
125
+ * boundaries, so neither a brace inside a string (`{ label: '{' }`) nor one
126
+ * inside a multi-line block comment or template literal perturbs the depth
127
+ * count. It does NOT specially recognise regex literals (a `{` inside a
128
+ * `/.../ ` quantifier, e.g. `/x{2,3}/`, is counted as a plain character);
129
+ * see `scanCode`'s header for why every such literal actually present in
130
+ * `src/` today is harmless (balanced on its own line).
131
+ *
132
+ * MUST-NOT-FLAG case verified by running the guard (the earlier "case
133
+ * variant chain" exemption for `src/state.cts:1488` was WRONG and is
134
+ * SUPERSEDED — see the header of the guard's initial version in git history
135
+ * for the retracted reasoning; `cmdStateSnapshot` is ladder-bearing, so
136
+ * *every* `stateExtractField(` call inside it, including line 1488, is
137
+ * correctly a violation now):
138
+ * - `src/smart-entry.cts`'s `fmScalarKey` (lines 152-158): its own
139
+ * `typeof v === 'number' || typeof v === 'boolean'` ladder (line 156)
140
+ * reads a value out of a NESTED frontmatter object and never calls
141
+ * `stateExtractField(` anywhere in its own body — it is ladder-bearing
142
+ * but call-free, so it is correctly never flagged. This is the live
143
+ * control case proving the guard still distinguishes "has a ladder" from
144
+ * "re-derives the fallback chain": a ladder alone, with no body
145
+ * fallback call in the same function, is a different question (reading
146
+ * a nested frontmatter object, full stop) and stays silent.
147
+ * - Any ladder or `stateExtractField(` call appearing inside a `//` line
148
+ * comment or a `/* *\/`-style block comment (single- or multi-line).
149
+ * `scanCode` blanks comment text — cross-line-aware — before either
150
+ * regex runs, so prose describing this derivation is never mistaken for
151
+ * a copy of it (ADR-3180 Amendment 3's "trains readers to reflexively
152
+ * exempt documentation" note).
153
+ *
154
+ * FUNCTION-SCOPED EXEMPTIONS (per ADR-3180 Decision 4(a): NEVER a bare
155
+ * whole-file allowlist — Decision 4(d) records that a whole-file exemption
156
+ * on the owner is precisely how `getMilestoneInfo` stayed invisible to an
157
+ * earlier guard). `src/state-document.cts` is the owner of this grammar;
158
+ * its `stateFieldValue` (added for issue #3187 Phase 5, landed in this same
159
+ * working tree while this guard was being authored — see the guard's commit
160
+ * history / PR for the exact sequencing) IS the canonical
161
+ * frontmatter-scalar-then-body-field chain, not a copy of it, so it is the
162
+ * ONLY entry in FUNCTION_SCOPED_EXEMPTIONS. Every other function in
163
+ * `state-document.cts` — including any future re-derivation added anywhere
164
+ * else in that file — is still scanned and still flagged (mirrors
165
+ * `lint-completion-ratio-drift.cjs`'s `FUNCTION_SCOPED_EXEMPTIONS` for
166
+ * `clampPercent`/`clampPercentFromFraction`).
167
+ *
168
+ * Every regex below is small, bounded, and has no nested/overlapping
169
+ * quantifiers; the string-literal and brace scans are plain escape-aware
170
+ * character loops, not regexes, so there is nothing for a backtracking
171
+ * engine to explore. `npm run lint:ci` runs CodeQL js/redos over this repo;
172
+ * mirrors the ReDoS discipline of the sibling drift guards.
173
+ *
174
+ * The tree-walk / root-confinement / sanitizer machinery is SHARED with the
175
+ * sibling drift guards via `scripts/lib/drift-scan.cjs` (ADR-3180 Decision
176
+ * 4). This guard's detection shape needs no regex-literal extraction, so it
177
+ * does not use `readRegexLiteralAt`; the reported fragment is simply the
178
+ * trimmed source line, bounded to MAX_REGEX_LITERAL_LEN characters.
179
+ *
180
+ * LADDER DETECTION DETAIL (evasion history). An earlier version of this
181
+ * guard's ladder pattern was a single regex, `typeof (\w+) === 'number' \|\|
182
+ * typeof \1 === 'boolean'`, tested against ONE line. An isolated adversarial
183
+ * review found three live-shaped ways past it, all verified to produce ZERO
184
+ * violations against a real re-derivation:
185
+ * (a) a member-expression or computed operand — `typeof fm.key ===
186
+ * 'number' || typeof fm.key === 'boolean'` — never matched, because
187
+ * the bare-identifier backreference (`\w+` then `\1`) cannot match
188
+ * `fm.key`/`fm[key]` at all.
189
+ * (b) the tiers in the opposite order — `typeof v === 'boolean' ||
190
+ * typeof v === 'number'` — never matched, because the pattern
191
+ * hardcoded 'number' before 'boolean' with no alternative ordering.
192
+ * This one is plausible from an ordinary code-review reformat, not
193
+ * deliberate evasion.
194
+ * (c) the ladder split across two lines, or written as two separate `if`
195
+ * statements instead of one `||` chain — never matched, because
196
+ * detection ran per-line with no tolerance for the pair spanning more
197
+ * than one line.
198
+ * The fix: OPERAND_SOURCE (identifier / dotted member / computed access,
199
+ * compared as captured TEXT rather than a bare-identifier backreference),
200
+ * TYPEOF_TIER_CLAUSE_RE (one clause at a time, tier-order-independent,
201
+ * collected via a `Map<operand, Set<tier>>`), and a small bounded
202
+ * LADDER_WINDOW_LINES sliding window so the two required clauses need only
203
+ * sit within a few lines of each other, not on the identical line. All
204
+ * three evasion shapes above are now covered by
205
+ * `tests/state-field-drift.test.cjs` (D3c/D3d/D3e).
206
+ *
207
+ * KNOWN, ACCEPTED limits of this scan (honest, not exhaustive by
208
+ * construction — this is a bounded regex-based scan, not a parser):
209
+ * - Cross-function: a re-derivation whose ladder and fallback call sit in
210
+ * two DIFFERENT named functions with no shared enclosing scope (e.g. a
211
+ * ladder in one file-level helper, consumed by a caller in another
212
+ * function that itself calls `stateExtractField(` for an unrelated
213
+ * field) is not caught — this guard's unit is "one named function's own
214
+ * body, including its nested closures", not the whole call graph. That
215
+ * is left to code review, not this regex. This co-occurrence check is
216
+ * deliberately FUNCTION-SCOPED with NO line-distance window at all (see
217
+ * the module-level "DETECTION" note above) — do not confuse this with
218
+ * LADDER_WINDOW_LINES, which bounds a different, much tighter pairing
219
+ * (the ladder's own two clauses, which are always part of the SAME
220
+ * conditional expression by construction, not an arbitrary call
221
+ * anywhere later in the function).
222
+ * - Ladder clauses further apart than LADDER_WINDOW_LINES lines: a ladder
223
+ * whose 'number' and 'boolean' clauses are more than
224
+ * LADDER_WINDOW_LINES lines apart (e.g. separated by an unrelated
225
+ * intervening block of code, not just the couple of lines a single
226
+ * conditional or two adjacent `if`s span) is not caught. No such shape
227
+ * has been observed in this codebase; if one appears, raise
228
+ * LADDER_WINDOW_LINES rather than silently accepting the miss.
229
+ * - Non-`typeof`-shaped coercion checks: a ladder rewritten through a
230
+ * `switch (typeof v)`, a helper function abstracting the check (e.g.
231
+ * `isNumberOrBoolean(v)`), or any comparison operator other than
232
+ * `===` (e.g. `typeof v == 'number'`) is not recognised — the pattern
233
+ * is `typeof OPERAND === 'TIER'` literally, not "any type-coercion
234
+ * test with equivalent runtime behaviour."
235
+ * - Nested computed access: `a[b[c]]` is not modelled as a single
236
+ * operand — the computed-content class excludes `]`, so nested
237
+ * brackets truncate the captured operand at the first `]` rather than
238
+ * matching the whole expression. Not observed in this codebase's
239
+ * ladders today.
240
+ * - Whitespace-sensitive operand identity: `fm[key]` and `fm[ key ]` are
241
+ * compared as distinct operand TEXT (no normalisation), so a ladder
242
+ * whose two clauses format the same computed access differently could
243
+ * under-detect. Not observed in this codebase's ladders today.
244
+ */
245
+
246
+ const path = require('node:path');
247
+ const driftScan = require('./lib/drift-scan.cjs');
248
+ const { MAX_REGEX_LITERAL_LEN, sanitizeForReport, scanTree } = driftScan;
249
+
250
+ // A ladder operand: a bare identifier (`v`), a dotted member expression
251
+ // (`fm.key`, `fm.a.b`), or a computed/bracket access (`fm[key]`), repeated
252
+ // via a single bounded alternation. No nested/overlapping quantifiers: the
253
+ // bracket-content class (`[^\]\r\n]{1,80}`) is one bounded character class,
254
+ // not a quantifier nested inside another quantifier, so there is nothing
255
+ // for a backtracking engine to explore. The captured TEXT (not a bare-
256
+ // identifier backreference) is what two clauses are compared against for
257
+ // "same operand" — see TYPEOF_TIER_CLAUSE_RE below and buildFunctionInfo's
258
+ // ladder-window accumulation.
259
+ const OPERAND_SOURCE = String.raw`[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[^\]\r\n]{1,80}\])*`;
260
+
261
+ // One ladder clause: `typeof OPERAND === 'TIER'`, TIER being one of the
262
+ // three coercion tiers this derivation ever compares against. Run with the
263
+ // `g` flag over a single line's `detect` text so every clause that line
264
+ // carries is collected (a single-line `||` chain carries two or three; a
265
+ // clause split onto its own line, or written as a standalone `if`, carries
266
+ // one). No trailing `\b` after the closing quote: the quote itself is
267
+ // already an unambiguous, non-word boundary — see the historical note this
268
+ // replaced for why a trailing `\b` there silently matches nothing at all.
269
+ const TYPEOF_TIER_CLAUSE_RE = new RegExp(String.raw`\btypeof\s+(${OPERAND_SOURCE})\s*===\s*'(number|boolean|string)'`, 'g');
270
+
271
+ // A ladder is CONFIRMED when the SAME operand carries both a 'number' clause
272
+ // and a 'boolean' clause (in either order; an additional 'string' clause
273
+ // anywhere among them does not prevent this) within this many consecutive
274
+ // lines of each other. Small and bounded, and DELIBERATELY NOT the same
275
+ // concept as the ladder-to-`stateExtractField(` co-occurrence check, which
276
+ // stays function-scoped with no window at all (see the module header's
277
+ // "DETECTION" and "LADDER DETECTION DETAIL" notes for why these two
278
+ // pairings are not interchangeable).
279
+ const LADDER_WINDOW_LINES = 6;
280
+
281
+ // The body-fallback call whose presence, inside a ladder-bearing function,
282
+ // makes that function a re-derivation of the "frontmatter-else-body" grammar.
283
+ const STATE_EXTRACT_FIELD_CALL_RE = /\bstateExtractField\(/;
284
+
285
+ // Named function scope openers. Each requires its own opening `{` on the
286
+ // SAME line as the signature — see the header's "FUNCTION ATTRIBUTION"
287
+ // paragraph for the documented limitation and why it does not affect either
288
+ // real copy this guard was written against.
289
+ // - `function NAME(...) {` — top-level OR nested, any indentation.
290
+ const FUNCTION_DECL_RE = /\bfunction\s+([A-Za-z_$][\w$]*)\s*\(/;
291
+ // - `const NAME = (...): ReturnType => {` — block-bodied arrow assigned to
292
+ // a const. `[^)]*` and `[^=]*` are bounded, single-purpose character
293
+ // classes (no nested/overlapping quantifiers): the former stops at the
294
+ // parameter list's closing paren, the latter at the arrow itself.
295
+ const ARROW_CONST_RE = /\bconst\s+([A-Za-z_$][\w$]*)\s*=\s*\([^)]*\)\s*(?::\s*[^=]+)?=>\s*\{/;
296
+
297
+ // ADR-3180 Decision 4(d): a guard's scan surface is every AUTHORED surface
298
+ // that can EXPRESS the derivation, not `src/` — reading "whole-repo scan" as
299
+ // "the whole `src/` tree" is itself the forbidden allowlist, one directory
300
+ // wide. This derivation is expressed in TWO languages: TypeScript under
301
+ // `src/` (the ladder + `stateExtractField(` shape PASS 1/2 above detect), and
302
+ // PROSE in the workflow/command/agent/skill markdown that ships to every
303
+ // runtime — `gsd-core/workflows/smart-entry.md`'s "Extract: `status`
304
+ // (frontmatter `status:` or body `**Status:**`)" is exactly this chain,
305
+ // hand-described rather than called. `SCAN_DIRS` therefore covers both;
306
+ // `findPromptFieldDrift` (below `findStateFieldDrift`) is the markdown-side
307
+ // detector, dispatched by extension in `scanRepo`'s `onFile`. Mirrors
308
+ // `lint-planning-prompt-drift.cjs`'s own prompt-layer surface exactly
309
+ // (`gsd-core/workflows`, `commands`, `agents`, `skills`).
310
+ const SCAN_DIRS = ['src', 'gsd-core/workflows', 'commands', 'agents', 'skills'];
311
+ const SCAN_EXT = new Set(['.cts', '.ts', '.mts', '.md']);
312
+
313
+ // The designated owner (issue #3187 Phase 5, ADR-3180 §7.7).
314
+ const OWNER_FILE = path.join('src', 'state-document.cts');
315
+
316
+ // Per ADR-3180 Decision 4(a): function-scoped, NEVER a bare file allowlist —
317
+ // Decision 4(d) records that a whole-file exemption on the owner is
318
+ // precisely how `getMilestoneInfo` stayed invisible to an earlier guard.
319
+ // Only `stateFieldValue` itself is exempt: it IS the canonical
320
+ // frontmatter-scalar-then-body-field chain (holds the ladder AND calls
321
+ // `stateExtractField(` by construction, in its own body — that is its whole
322
+ // job), not a copy of it. Every OTHER function in this same file — including
323
+ // any future re-derivation added anywhere else in state-document.cts — is
324
+ // still scanned and still flagged; nothing else in this file is exempt.
325
+ const FUNCTION_SCOPED_EXEMPTIONS = new Map([[OWNER_FILE, new Set(['stateFieldValue'])]]);
326
+
327
+ /**
328
+ * Tokenize the WHOLE file into TWO parallel per-line views, from ONE
329
+ * single-pass, escape-aware character scan (not a regex — no backtracking
330
+ * cost to bound):
331
+ * - `detect[i]`: comments stripped, but string/template literal contents
332
+ * KEPT VERBATIM (quotes included). This is what the detection regexes
333
+ * (LADDER_RE / STATE_EXTRACT_FIELD_CALL_RE / FUNCTION_DECL_RE /
334
+ * ARROW_CONST_RE) run against — they need the literal quoted text
335
+ * `'number'` / `'boolean'` to still be present.
336
+ * - `braces[i]`: comments AND string/template literal CONTENTS stripped,
337
+ * used only for brace-depth counting, so a brace character written
338
+ * inside a string (e.g. `{ label: '{' }`) never perturbs the count.
339
+ *
340
+ * This REPLACES two earlier, narrower designs in turn:
341
+ * 1. Two independent per-line helpers (`stripComments` + a separate
342
+ * `stripStringLiterals`), each with no cross-line state. That design
343
+ * missed a `/* ... *\/`-style block comment whose CLOSING line also
344
+ * carries trailing real code (e.g. a `catch { /* comment` opener
345
+ * followed by ` * more comment. *\/ }` on a later line): the old
346
+ * per-line `stripComments` heuristic ("a line whose trimmed text starts
347
+ * with `*` is entirely comment") blanked that closing line WHOLESALE,
348
+ * silently dropping the real `}` it also carried, which left a
349
+ * function's brace-depth frame permanently open and made every LATER
350
+ * `stateExtractField(` call in the file — inside unrelated functions —
351
+ * inherit that stale frame's ladder-bearing status (`src/state.cts`'s
352
+ * `cmdStateValidate`, which has no ladder of its own, was falsely
353
+ * flagged this way).
354
+ * 2. A single merged output that dropped string CONTENTS unconditionally.
355
+ * That fixed (1) but broke detection outright: LADDER_RE needs the
356
+ * literal text `'number'`/`'boolean'` (with quotes) to match, and a
357
+ * merged output that strips quote contents for brace-safety also
358
+ * erases the very tokens the ladder regex looks for — every ladder
359
+ * line silently stopped matching. Two views, not one, is what lets
360
+ * each consumer see what it actually needs.
361
+ *
362
+ * `inBlockComment` and `inTemplate` are threaded ACROSS lines so a block
363
+ * comment or a multi-line backtick template literal spanning several source
364
+ * lines is tracked correctly regardless of what trails its closing
365
+ * delimiter. Regex literals are NOT specially recognised (documented,
366
+ * narrow, known limitation, same as the sibling drift guards'
367
+ * `readRegexLiteralAt`-free scans): a `/` is only ever treated as a comment
368
+ * opener when immediately followed by another `/` or `*`, so an ordinary
369
+ * regex literal's slashes pass through as plain characters and any brace
370
+ * inside one (e.g. `/x{2,3}/`) is counted like any other character —
371
+ * verified harmless for every regex literal actually present in `src/`
372
+ * today because each is balanced (equal opens/closes) on its own line, so it
373
+ * never desyncs the running depth total even though the individual
374
+ * characters are not "understood" as a literal.
375
+ */
376
+ function scanCode(lines) {
377
+ const detect = new Array(lines.length);
378
+ const braces = new Array(lines.length);
379
+ let inBlockComment = false;
380
+ let inTemplate = false;
381
+ for (let li = 0; li < lines.length; li++) {
382
+ const line = lines[li];
383
+ let outDetect = '';
384
+ let outBraces = '';
385
+ let i = 0;
386
+ if (inTemplate) {
387
+ const start = i;
388
+ while (i < line.length) {
389
+ if (line[i] === '\\') {
390
+ i += 2;
391
+ continue;
392
+ }
393
+ if (line[i] === '`') {
394
+ i++;
395
+ inTemplate = false;
396
+ break;
397
+ }
398
+ i++;
399
+ }
400
+ outDetect += line.slice(start, i); // template contents kept verbatim for detect
401
+ if (inTemplate) {
402
+ detect[li] = outDetect;
403
+ braces[li] = ''; // whole line still inside the unterminated template
404
+ continue;
405
+ }
406
+ }
407
+ while (i < line.length) {
408
+ if (inBlockComment) {
409
+ const close = line.indexOf('*/', i);
410
+ if (close === -1) {
411
+ i = line.length;
412
+ break;
413
+ }
414
+ i = close + 2;
415
+ inBlockComment = false;
416
+ continue;
417
+ }
418
+ const ch = line[i];
419
+ if (ch === '/' && line[i + 1] === '/') {
420
+ i = line.length; // rest of line is a line comment
421
+ break;
422
+ }
423
+ if (ch === '/' && line[i + 1] === '*') {
424
+ inBlockComment = true;
425
+ i += 2;
426
+ continue;
427
+ }
428
+ if (ch === "'" || ch === '"') {
429
+ const quote = ch;
430
+ const start = i;
431
+ let j = i + 1;
432
+ while (j < line.length) {
433
+ if (line[j] === '\\') {
434
+ j += 2; // escape consumes the next character, whatever it is
435
+ continue;
436
+ }
437
+ if (line[j] === quote) {
438
+ j++;
439
+ break;
440
+ }
441
+ j++;
442
+ }
443
+ outDetect += line.slice(start, j); // string kept verbatim for detect
444
+ // (nothing appended to outBraces — string contents excluded from depth counting)
445
+ i = j;
446
+ continue;
447
+ }
448
+ if (ch === '`') {
449
+ const start = i;
450
+ let j = i + 1;
451
+ let closed = false;
452
+ while (j < line.length) {
453
+ if (line[j] === '\\') {
454
+ j += 2;
455
+ continue;
456
+ }
457
+ if (line[j] === '`') {
458
+ j++;
459
+ closed = true;
460
+ break;
461
+ }
462
+ j++;
463
+ }
464
+ if (!closed) {
465
+ outDetect += line.slice(start); // rest of line kept verbatim for detect
466
+ inTemplate = true;
467
+ i = line.length;
468
+ break;
469
+ }
470
+ outDetect += line.slice(start, j); // template kept verbatim for detect
471
+ i = j;
472
+ continue;
473
+ }
474
+ outDetect += ch;
475
+ outBraces += ch;
476
+ i++;
477
+ }
478
+ detect[li] = outDetect;
479
+ braces[li] = outBraces;
480
+ }
481
+ return { detect, braces };
482
+ }
483
+
484
+ /**
485
+ * Pure: find every unsanctioned STATE.md field-extraction fallback-chain
486
+ * re-derivation in `text`. `relPath` is the repo-relative path, used both to
487
+ * report file:line and to apply the function-scoped owner exemptions above.
488
+ *
489
+ * Two passes over the same line array:
490
+ * PASS 1 walks the file once, maintaining a brace-depth stack of open
491
+ * named function frames, and records which function NAMES are
492
+ * ladder-bearing (transitively — see the header's "TRANSITIVE ladder
493
+ * attribution" paragraph) and, per source line, which frame is
494
+ * INNERMOST at that line (for call attribution) — {@link buildFunctionInfo}.
495
+ * PASS 2 walks the lines again; any `stateExtractField(` call whose
496
+ * innermost enclosing function is ladder-bearing (and not
497
+ * function-scoped-exempt) is a violation.
498
+ * Returns [{ line, found }].
499
+ */
500
+ function buildFunctionInfo(lines) {
501
+ const { detect, braces } = scanCode(lines);
502
+ const ladderBearing = new Set();
503
+ const innermostAt = new Array(lines.length).fill(null);
504
+ const stack = []; // { name, openDepth }
505
+ let depth = 0;
506
+ // A `function NAME(` match whose own line carries no `{` (a multi-line
507
+ // signature — parameters and/or a return-type annotation spilling onto
508
+ // later lines before the body opens) — awaiting the line that actually
509
+ // carries its opening brace. See the header's "FUNCTION ATTRIBUTION"
510
+ // paragraph and this function's own doc comment for why this cannot be
511
+ // pushed onto `stack` immediately: pushing it with `openDepth` recorded
512
+ // BEFORE its own `{` is counted produces a frame whose `openDepth` is the
513
+ // ENCLOSING scope's depth, not its own — for a top-level function that is
514
+ // `openDepth: 0`, and since real code never reaches negative depth, a
515
+ // frame pushed with `openDepth: 0` can NEVER pop (`depth < 0` never
516
+ // fires): it would sit at the bottom of `stack` for the rest of the file,
517
+ // and every subsequent line with no OTHER open frame would be
518
+ // misattributed to it. `ARROW_CONST_RE` never needs this deferral — its
519
+ // pattern requires `=>\s*\{` literally, so it only ever matches on a line
520
+ // that already carries the brace.
521
+ let pendingDeclName = null;
522
+ // LADDER WINDOW: a rolling buffer of every `typeof OPERAND === 'TIER'`
523
+ // clause seen in the last LADDER_WINDOW_LINES lines, oldest-first. On
524
+ // each line, new clauses from that line are appended, then entries older
525
+ // than the window are pruned from the front. A ladder is confirmed the
526
+ // moment the SAME operand has accumulated both a 'number' and a
527
+ // 'boolean' entry inside the buffer — order-independent, and regardless
528
+ // of whether the two clauses came from one `||` chain split across
529
+ // lines, one single-line chain, or two entirely separate `if`
530
+ // statements. See LADDER_WINDOW_LINES's own comment for why this is
531
+ // bounded small and is NOT the same concept as the (unbounded,
532
+ // function-scoped) ladder-to-`stateExtractField(` co-occurrence below.
533
+ const recentClauses = []; // { line, operand, tier }
534
+ for (let i = 0; i < lines.length; i++) {
535
+ const detectCode = detect[i];
536
+ const braceCode = braces[i];
537
+
538
+ let immediateName = null;
539
+ if (detectCode.trim()) {
540
+ const arrowMatch = ARROW_CONST_RE.exec(detectCode);
541
+ if (arrowMatch) {
542
+ immediateName = arrowMatch[1];
543
+ } else {
544
+ const declMatch = FUNCTION_DECL_RE.exec(detectCode);
545
+ // A NEW `function NAME(` match replaces any still-pending name
546
+ // rather than stacking deferrals — this scanner tracks at most one
547
+ // pending declaration at a time (two `function` keywords on
548
+ // consecutive lines with neither closing its signature first is not
549
+ // a shape that occurs in this codebase's style).
550
+ if (declMatch) pendingDeclName = declMatch[1];
551
+ }
552
+ }
553
+
554
+ const opens = (braceCode.match(/\{/g) || []).length;
555
+ const closes = (braceCode.match(/\}/g) || []).length;
556
+ depth += opens - closes;
557
+
558
+ if (immediateName) stack.push({ name: immediateName, openDepth: depth });
559
+
560
+ if (pendingDeclName) {
561
+ if (opens > 0) {
562
+ // First line whose brace count actually increases — its `{` is
563
+ // counted in THIS line's `opens`, so `depth` here correctly
564
+ // reflects "inside the function", matching every other frame's
565
+ // convention (push AFTER updating depth for the pushing line).
566
+ stack.push({ name: pendingDeclName, openDepth: depth });
567
+ pendingDeclName = null;
568
+ } else if (detectCode.includes(';')) {
569
+ // The statement terminated before any `{` appeared — a type-only
570
+ // declaration, an ambient `declare function`, or an overload
571
+ // signature, none of which open a function body. Abandon the
572
+ // pending name rather than stranding it to match a `{` that
573
+ // belongs to unrelated later code. (A `;` inside a string literal
574
+ // on this line would also trigger this — accepted, narrow, known
575
+ // limitation: a default-parameter string containing `;` on the
576
+ // SAME line as an unterminated multi-line signature has not been
577
+ // observed in this codebase.)
578
+ pendingDeclName = null;
579
+ }
580
+ }
581
+
582
+ while (stack.length > 0 && depth < stack[stack.length - 1].openDepth) stack.pop();
583
+
584
+ innermostAt[i] = stack.length > 0 ? stack[stack.length - 1].name : null;
585
+
586
+ if (detectCode.trim()) {
587
+ TYPEOF_TIER_CLAUSE_RE.lastIndex = 0;
588
+ let clauseMatch;
589
+ while ((clauseMatch = TYPEOF_TIER_CLAUSE_RE.exec(detectCode)) !== null) {
590
+ recentClauses.push({ line: i, operand: clauseMatch[1], tier: clauseMatch[2] });
591
+ }
592
+ }
593
+ // Prune clauses that have fallen outside the small bounded ladder window.
594
+ while (recentClauses.length > 0 && recentClauses[0].line <= i - LADDER_WINDOW_LINES) {
595
+ recentClauses.shift();
596
+ }
597
+
598
+ let ladderConfirmed = false;
599
+ if (recentClauses.length > 0) {
600
+ const tiersByOperand = new Map();
601
+ for (const clause of recentClauses) {
602
+ let tiers = tiersByOperand.get(clause.operand);
603
+ if (!tiers) {
604
+ tiers = new Set();
605
+ tiersByOperand.set(clause.operand, tiers);
606
+ }
607
+ tiers.add(clause.tier);
608
+ }
609
+ for (const tiers of tiersByOperand.values()) {
610
+ if (tiers.has('number') && tiers.has('boolean')) {
611
+ ladderConfirmed = true;
612
+ break;
613
+ }
614
+ }
615
+ }
616
+
617
+ if (ladderConfirmed) {
618
+ // Transitive: every frame currently open (not just the innermost) is
619
+ // ladder-bearing, because a nested closure's body is lexically part of
620
+ // every one of its enclosing functions' own bodies.
621
+ for (const frame of stack) ladderBearing.add(frame.name);
622
+ }
623
+ }
624
+ return { ladderBearing, innermostAt, detect };
625
+ }
626
+
627
+ // ─── PROMPT-LAYER PROSE DETECTION (ADR-3180 Decision 4(d)) ─────────────────
628
+ //
629
+ // The SAME #1760 fallback chain — "prefer the frontmatter scalar, else fall
630
+ // back to the body field" — expressed as MARKDOWN PROSE describing the
631
+ // derivation to an agent, rather than TypeScript re-deriving it. Detection is
632
+ // intentionally narrow, mirroring `lint-planning-prompt-drift.cjs`'s own
633
+ // precedent: a markdown line is a prose re-derivation only when it carries
634
+ // ALL FOUR, on the SAME line:
635
+ // (a) a backtick-quoted YAML-style frontmatter key token — a lowercase
636
+ // identifier immediately followed by a colon, inside backticks, e.g.
637
+ // `` `status:` ``. `FRONTMATTER_KEY_TOKEN_RE`.
638
+ // (b) a backtick-quoted Markdown BOLD body-field token, e.g.
639
+ // `` `**Status:**` ``. `BODY_BOLD_TOKEN_RE`.
640
+ // (c) the word "frontmatter" (case-insensitive, word-bounded) anywhere on
641
+ // the line.
642
+ // (d) the word "body" (case-insensitive, word-bounded) anywhere on the
643
+ // line.
644
+ // (c) and (d) are what distinguish a line DESCRIBING an fm-then-body
645
+ // PRECEDENCE from a line that merely happens to carry two backtick-quoted
646
+ // tokens shaped like (a) and (b) for unrelated reasons — both words are
647
+ // required so an incidental co-occurrence (e.g. a table row naming an
648
+ // unrelated frontmatter key and, several columns over, an unrelated bold
649
+ // body label) cannot false-positive on tokens alone.
650
+ // All four regexes are small, bounded, single fixed character classes with
651
+ // no nested/overlapping quantifiers — nothing for a backtracking engine to
652
+ // explore (`npm run lint:ci` runs CodeQL js/redos over this repo, the same
653
+ // discipline `findStateFieldDrift`'s own regexes document above).
654
+ const FRONTMATTER_KEY_TOKEN_RE = /`[a-z][a-z0-9_]*:`/;
655
+ const BODY_BOLD_TOKEN_RE = /`\*\*[^*`]{1,80}\*\*`/;
656
+ const FRONTMATTER_WORD_RE = /\bfrontmatter\b/i;
657
+ const BODY_WORD_RE = /\bbody\b/i;
658
+
659
+ // Per ADR-3180 Decision 4(d)/(e): `gsd-core/workflows/smart-entry.md`'s
660
+ // Fallback step 1 ("`gsd-tools` itself is broken") is a PERMANENT, by-
661
+ // construction exemption — NOT debt with an owner, and therefore NOT
662
+ // modelled as `lint-planning-prompt-drift.cjs`'s shrink-only ratchet
663
+ // baseline (which exists specifically to acknowledge sites with a removal
664
+ // issue, per Decision 4(e)). This site can never be migrated onto
665
+ // `src/state-document.cjs`'s `stateFieldValue`: the surrounding step exists
666
+ // PRECISELY for when the CLI itself cannot run (`Cannot find module ...` /
667
+ // Node crash — probed one line above this one), so by construction it cannot
668
+ // call the canonical owner it is standing in for. Fabricating a "removal
669
+ // issue" for something that can never be removed would misrepresent it as
670
+ // ratchetable debt it is not.
671
+ //
672
+ // Keyed on (file, TRIMMED source text) — never a line number, which churns
673
+ // on any unrelated edit to the same file — mirroring the ratchet's own key
674
+ // shape (`lint-planning-prompt-drift.cjs`) even though this exemption is not
675
+ // itself a ratchet. `file` uses the SAME native-separator relPath shape as
676
+ // `OWNER_FILE`/`FUNCTION_SCOPED_EXEMPTIONS` above (this guard does not
677
+ // POSIX-normalize elsewhere, so this exemption does not either, for
678
+ // consistency within the one file).
679
+ const PROMPT_LAYER_EXEMPTIONS = new Map([
680
+ [
681
+ path.join('gsd-core', 'workflows', 'smart-entry.md'),
682
+ new Map([
683
+ [
684
+ "- Read `.planning/STATE.md` (frontmatter + body) with the Read tool. Extract: `status` (frontmatter `status:` or body `**Status:**`), `Phase:` from the body, `total_phases`/`percent` from a nested `progress:` frontmatter object if present, and any `## Blockers` items.",
685
+ 'gsd-tools-down fallback (smart-entry.md Fallback step 1): runs only when gsd-tools itself cannot run, so it cannot call the canonical owner it substitutes for — permanent by construction, not removable debt.',
686
+ ],
687
+ ]),
688
+ ],
689
+ ]);
690
+
691
+ /**
692
+ * Pure: find every prose re-derivation of the STATE.md field-extraction
693
+ * fallback chain in a markdown file's `text`. Returns `[{ line, found }]` for
694
+ * every UNEXEMPTED match — `relPath` (native-separator, matching
695
+ * `PROMPT_LAYER_EXEMPTIONS`'s keys) is consulted only for the exemption
696
+ * lookup, exactly as `findStateFieldDrift` consults it for
697
+ * `FUNCTION_SCOPED_EXEMPTIONS`.
698
+ */
699
+ function findPromptFieldDrift(text, relPath) {
700
+ const out = [];
701
+ const lines = text.split('\n');
702
+ const exemptTexts = PROMPT_LAYER_EXEMPTIONS.get(relPath) || null;
703
+ for (let i = 0; i < lines.length; i++) {
704
+ const line = lines[i];
705
+ if (!FRONTMATTER_KEY_TOKEN_RE.test(line)) continue;
706
+ if (!BODY_BOLD_TOKEN_RE.test(line)) continue;
707
+ if (!FRONTMATTER_WORD_RE.test(line)) continue;
708
+ if (!BODY_WORD_RE.test(line)) continue;
709
+ const trimmed = line.trim();
710
+ if (exemptTexts && exemptTexts.has(trimmed)) continue;
711
+ out.push({ line: i + 1, found: trimmed.slice(0, MAX_REGEX_LITERAL_LEN) });
712
+ }
713
+ return out;
714
+ }
715
+
716
+ function findStateFieldDrift(text, relPath) {
717
+ const out = [];
718
+ const lines = text.split('\n');
719
+ const exemptFunctions = FUNCTION_SCOPED_EXEMPTIONS.get(relPath) || null;
720
+ const { ladderBearing, innermostAt, detect } = buildFunctionInfo(lines);
721
+
722
+ for (let i = 0; i < lines.length; i++) {
723
+ const detectCode = detect[i];
724
+ if (!detectCode.trim()) continue;
725
+ if (!STATE_EXTRACT_FIELD_CALL_RE.test(detectCode)) continue;
726
+
727
+ const fn = innermostAt[i];
728
+ if (!fn || !ladderBearing.has(fn)) continue;
729
+ if (exemptFunctions && exemptFunctions.has(fn)) continue;
730
+
731
+ out.push({ line: i + 1, found: lines[i].trim().slice(0, MAX_REGEX_LITERAL_LEN) });
732
+ }
733
+ return out;
734
+ }
735
+
736
+ /**
737
+ * Scan the authored source tree (TypeScript under `src/`, prose in the
738
+ * prompt-layer markdown — ADR-3180 Decision 4(d)) and return every
739
+ * unsanctioned re-derivation, each annotated with the repo-relative file
740
+ * path. Dispatches by extension: `.md` files run the prose detector
741
+ * (`findPromptFieldDrift`), everything else (`.cts`/`.ts`/`.mts`) runs the
742
+ * code detector (`findStateFieldDrift`) — the two derivations are expressed
743
+ * in different languages and need different detection shapes over the same
744
+ * scan surface.
745
+ */
746
+ function scanRepo(root) {
747
+ return scanTree({
748
+ root,
749
+ scanDirs: SCAN_DIRS,
750
+ scanExt: SCAN_EXT,
751
+ onFile(rel, text) {
752
+ // `rel` is already the REAL (canonical) path (scanTree resolves
753
+ // symlinks before calling onFile), so this — and
754
+ // FUNCTION_SCOPED_EXEMPTIONS/PROMPT_LAYER_EXEMPTIONS above, also keyed
755
+ // on `rel` — match consistently regardless of which symlink reached
756
+ // the file.
757
+ const finder = path.extname(rel) === '.md' ? findPromptFieldDrift : findStateFieldDrift;
758
+ return finder(text, rel).map((d) => ({ file: rel, ...d }));
759
+ },
760
+ });
761
+ }
762
+
763
+ function main() {
764
+ const root = path.join(__dirname, '..');
765
+ const violations = scanRepo(root);
766
+ if (violations.length === 0) {
767
+ process.stdout.write('ok state-field-drift: no unsanctioned STATE.md field-extraction fallback-chain re-derivations found\n');
768
+ return;
769
+ }
770
+ process.stderr.write('state-field-drift: independent re-derivation(s) of the STATE.md frontmatter-else-body field\n');
771
+ process.stderr.write('fallback chain found. Route these call sites through src/state-document.cjs\n');
772
+ process.stderr.write('`stateFieldValue` (issue #3187, ADR-3180 §7.7) instead of re-deriving the ladder:\n');
773
+ for (const d of violations) {
774
+ // `d.file` is exactly as attacker-controlled as `d.found`: a repo can
775
+ // legally track a filename containing control bytes / bidi overrides,
776
+ // and it is a fork-PR-authored value reaching a CI log the same way the
777
+ // matched line text does — sanitize it at the same reporting boundary.
778
+ process.stderr.write(` ${sanitizeForReport(d.file)}:${d.line} ${sanitizeForReport(d.found)}\n`);
779
+ }
780
+ process.exitCode = 1;
781
+ }
782
+
783
+ if (require.main === module) main();
784
+
785
+ module.exports = {
786
+ findStateFieldDrift,
787
+ findPromptFieldDrift,
788
+ buildFunctionInfo,
789
+ scanRepo,
790
+ OPERAND_SOURCE,
791
+ TYPEOF_TIER_CLAUSE_RE,
792
+ LADDER_WINDOW_LINES,
793
+ STATE_EXTRACT_FIELD_CALL_RE,
794
+ FUNCTION_DECL_RE,
795
+ ARROW_CONST_RE,
796
+ OWNER_FILE,
797
+ FUNCTION_SCOPED_EXEMPTIONS,
798
+ FRONTMATTER_KEY_TOKEN_RE,
799
+ BODY_BOLD_TOKEN_RE,
800
+ PROMPT_LAYER_EXEMPTIONS,
801
+ SCAN_DIRS,
802
+ SCAN_EXT,
803
+ scanCode,
804
+ MAX_REGEX_LITERAL_LEN,
805
+ };