@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,950 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * Anti-divergence drift guard for the STATE.md WRITE PATH — epic #3408, issue
6
+ * #3468, ADR-3408 Decision 5, contract §8.1/§8.2/§8.3
7
+ * (`docs/adr/3408-state-write-path-preservation.md` is the contract this
8
+ * guard enforces; read it first) — SHRUNK per ADR-3473 §8.6 (issue #3871).
9
+ *
10
+ * WHAT MOVED INTO THE TYPE SYSTEM (ADR-3473 §8.6): `writeStateMd`'s third
11
+ * parameter now REQUIRES a `StateTransaction` of `kind: 'rebuild'`, produced
12
+ * only by `openStateTransaction()` / `rebuildStateTransaction()`
13
+ * (`src/state-transition.cts`) — `writeStateMd` itself throws
14
+ * `STATE_TRANSACTION_KIND_INVALID` for anything else. The two exceptions this
15
+ * guard used to track as a RATCHETED STRING MATCH against
16
+ * `scripts/state-write-path-drift-baseline.json` — `cmdStateSync`
17
+ * (`src/state.cts`, #905's "let the body win") and `REGENERATE_STATE`
18
+ * (`src/health-diagnostic.cts`, `/gsd-health --repair`'s factory reset) — are
19
+ * NO LONGER TRACKED HERE. Both are now a constructor the type system names
20
+ * (`rebuildStateTransaction`), not an entry a human had to remember to keep
21
+ * acknowledging in a baseline file. That baseline file, and the whole
22
+ * ratchet machinery that existed ONLY to support it (loading it, the
23
+ * STALE-entry check, `--baseline` regeneration), is retired along with it —
24
+ * the `writeStateMd(` ARM of the old `findSeamBypasses` axis is gone for
25
+ * good, because the type system now names both of its exceptions.
26
+ *
27
+ * WHAT STAYED, RETITLED, AND MADE TERMINAL (issue #3871 review): the OTHER
28
+ * half of `findSeamBypasses` — every direct `syncStateFrontmatter(` /
29
+ * `applyPostSyncPreservation(` call outside their owner
30
+ * (`syncAndPreserveStateMd`, `src/state.cts`) — is NOT redundant. The type
31
+ * system gates `writeStateMd`'s THIRD PARAMETER; it says nothing about a call
32
+ * site that re-assembles `syncStateFrontmatter` + `applyPostSyncPreservation`
33
+ * itself instead of calling the one write-seam composition. ADR-3408 §8.3:
34
+ * "Assembling the stages at a call site is a re-derivation even when every
35
+ * step calls the owner." #3469 found exactly that shape live in
36
+ * `cmdPhaseComplete`'s atomic-commit adapter (`src/phase.cts`) — every step
37
+ * called an owner, so an owner-level test and this guard's OLD, narrower
38
+ * scan both stayed green while the composition itself drifted from
39
+ * `readModifyWriteStateMd`'s. See `findCompositionBypasses` below. Unlike
40
+ * the retired `writeStateMd(` arm, this one ships TERMINAL, not ratcheted —
41
+ * mirrors `findPromptSeamUses`'s own conversion in this same shrink: no
42
+ * legitimate call site outside the owner exists today, so any occurrence is
43
+ * a violation, not an entry to acknowledge.
44
+ *
45
+ * WHAT THIS GUARD STILL OWNS, because the type system cannot make it
46
+ * unrepresentable:
47
+ *
48
+ * AXIS 1 — POLICY DISPATCH (§8.1). `applyStatePreservation` must select
49
+ * its branch from a `FIELD_CLASSIFICATION` row's `preservation` value,
50
+ * never from a field NAME. Every `getFieldClassification('<string
51
+ * literal>')` inside `src/state-transition.cts` is a field-name-keyed
52
+ * branch — the shape that let four declared rows go unimplemented until
53
+ * #3258, and that leaves `derive` and `clear` with no executor today. A
54
+ * VARIABLE argument (`getFieldClassification(field)`) is the CORRECT
55
+ * table-driven shape and is deliberately NOT matched. Also matched: a
56
+ * direct `field === '<literal>'` / `field !== '<literal>'` / `'<literal>'
57
+ * === field` comparison of the dispatch loop's own `field` variable — the
58
+ * same prohibited shape routed AROUND `getFieldClassification` instead of
59
+ * through it (#3468 found this exact form live in `applyPreserveIfPlaceholder`,
60
+ * undetected by the call-shape check alone). Scoped to the identifier
61
+ * `field` only; see `FIELD_VAR_EQ_LITERAL_RE`'s own comment for why.
62
+ *
63
+ * AXIS 2 — RAW STATE WRITE (§8.6, RETAINED). A direct `fs.writeFileSync(`
64
+ * call whose target argument is the state path (`statePath`, or a literal
65
+ * containing `STATE.md`) is a write that skips BOTH the write seam
66
+ * (`writeStateMd`/`syncAndPreserveStateMd`) AND the OS Shell Projection
67
+ * seam (`platformWriteSync`, `src/shell-command-projection.cts`) entirely.
68
+ * No constructor or type can make this unrepresentable — Node's `fs`
69
+ * module is always one `import` away — so this axis stays a plain
70
+ * string-match scan, unratcheted: any occurrence is a violation, because no
71
+ * legitimate call site in this codebase writes STATE.md this way (every
72
+ * real writer goes through `platformWriteSync`).
73
+ *
74
+ * AXIS 3 — FRONTMATTER-SHAPED WRITE (§8.3(b), closed Phase 2 / #3469).
75
+ * `findUnstrippedContentWrites` below flags a `stateReplaceField(` call
76
+ * only when BOTH (a) its field-name argument is a VARIABLE, not a fixed
77
+ * string literal, and (b) its content argument has not been run through
78
+ * `stripFrontmatter` first (a narrow backward-scan approximation, not full
79
+ * dataflow — see the function's own docstring).
80
+ *
81
+ * AXIS 4 — PROMPT-LAYER WRITE (§8.3, Decision 4(d)). Prose in the prompt
82
+ * layer instructing an agent to shell out to a write-side `gsd-tools`
83
+ * subcommand is the same write seam, expressed as markdown rather than
84
+ * TypeScript — a check the type system cannot reach at all, since markdown
85
+ * is never compiled. Any occurrence is a violation.
86
+ *
87
+ * AXIS 5 — COMPOSITION BYPASS (§8.3, RETAINED, issue #3871). A direct
88
+ * `syncStateFrontmatter(` or `applyPostSyncPreservation(` call outside
89
+ * their owner (`syncAndPreserveStateMd`) is a re-assembly of the write-seam
90
+ * composition — the exact shape #3469 found live in `cmdPhaseComplete`.
91
+ * `writeStateMd(` is deliberately NOT scanned here (that arm is retired,
92
+ * §8.6) — `writeStateMd`'s own legitimate direct `syncStateFrontmatter(`
93
+ * call (the sanctioned #905 exception) is instead exempted by function
94
+ * name, same as the composition owner itself; see
95
+ * `SEAM_OWNER_EXEMPT_FUNCTIONS`. Terminal: any occurrence is a violation.
96
+ *
97
+ * DESIGN CONSTRAINTS (ADR-3180 Decision 4, adopted verbatim by ADR-3408):
98
+ * - 4(a) whole-repo scan, never an allowlist. ADR-3180's own phases found
99
+ * 26/5/54 copies where their epics scoped 3/3/4 — a scoped guard earns
100
+ * nothing.
101
+ * - 4(d) the scan surface is DECLARED and is NOT just `src/` — `src/`
102
+ * alone is itself an allowlist one directory wide; #1762 traced a wrong
103
+ * count to a shell snippet in `gsd-core/workflows/progress.md`.
104
+ *
105
+ * GOODHART, PER ADR-3408 DECISION 5: "0 violations" is a LAGGING metric — a
106
+ * measure about to become a target. This guard's own `_comment` and its
107
+ * human-readable success message both say so: the zero this guard reports
108
+ * must NEVER be quoted alone; it is only meaningful beside the behavioral
109
+ * identity test's result (the consumer-output assertion Decision 5's gaming
110
+ * table names as the actual defense).
111
+ *
112
+ * String literals are matched, never parsed as an AST — deliberately, per
113
+ * `scripts/lint-state-field-drift.cjs`'s own precedent: over-reporting
114
+ * (flagging a comment or a string that merely looks like a call) is safe;
115
+ * under-reporting (missing a real bypass) is the failure this guard exists
116
+ * to prevent. `stripComments` does not track quoted strings for exactly
117
+ * this reason — see its own header.
118
+ *
119
+ * CORRECTION TO ADR-3473 §8.6's TEXT, RECORDED HERE SO A FUTURE READER
120
+ * COMPARING THE ADR TO THIS FILE DOES NOT CONCLUDE THE FILE DRIFTED:
121
+ * §8.6 says this guard "keeps only its raw-write check (`fs.writeFileSync`
122
+ * against the state path), which the type cannot make unrepresentable." That
123
+ * sentence is wrong on both halves. First, no such check existed anywhere in
124
+ * this file before this shrink — `findRawStateWrites` (Axis 2, below) is
125
+ * NET-NEW, written for this shrink, not retained from a prior version.
126
+ * Second, this file did not (and does not) drop to "only" one check: besides
127
+ * the retired `writeStateMd(` arm of the old `findSeamBypasses` axis (the
128
+ * half §8.6 correctly names for removal, since the type system now names
129
+ * both of its exceptions), `findPolicyDispatchDrift`, `findUnimplementedPolicies`,
130
+ * `findUnstrippedContentWrites`, `findPromptSeamUses`, and the RETAINED
131
+ * `syncStateFrontmatter`/`applyPostSyncPreservation` composition-bypass half
132
+ * of `findSeamBypasses` (now `findCompositionBypasses`, terminal — issue
133
+ * #3871 review) all remain, because §8.6 names neither them nor anything
134
+ * that makes what they check unrepresentable — a field-name-keyed dispatch
135
+ * branch, an unimplemented `FieldPreservation` policy, an unstripped
136
+ * frontmatter write, prompt-layer prose shelling out to `gsd-tools`, and a
137
+ * re-assembled write-seam composition are all still exactly as representable
138
+ * in TypeScript (or in markdown, for the prompt-layer one) after the
139
+ * state-transaction constructor as they were before it — the constructor
140
+ * gates `writeStateMd`'s third parameter, nothing about a call site that
141
+ * never goes through `writeStateMd` at all. Do not edit the ADR to match
142
+ * this file; this paragraph is the correction of record.
143
+ */
144
+
145
+ const path = require('node:path');
146
+ const { scanTree, sanitizeForReport } = require('./lib/drift-scan.cjs');
147
+ const { escapeRegex } = require('../gsd-core/bin/lib/pattern.cjs');
148
+
149
+ const REPO_ROOT = path.resolve(__dirname, '..');
150
+
151
+ // Frozen REASON enum — mirrors `lint-state-field-drift.cjs`'s house style of
152
+ // naming every failure shape explicitly rather than reusing one generic
153
+ // "violation" string, so a reader can `grep` a reason string straight back
154
+ // to the paragraph of this header (or of the ADR) that explains it.
155
+ const REASON = Object.freeze({
156
+ FIELD_NAME_DISPATCH: 'field_name_dispatch',
157
+ UNIMPLEMENTED_POLICY: 'unimplemented_policy',
158
+ // Axis 3 (§8.3(b), closed Phase 2 / #3469): a `stateReplaceField(` call
159
+ // with a variable field-name argument whose content argument was not run
160
+ // through `stripFrontmatter` first — see `findUnstrippedContentWrites`.
161
+ UNSTRIPPED_CONTENT_WRITE: 'unstripped_content_write',
162
+ // Axis 2 (§8.6, retained): a raw `fs.writeFileSync(` call targeting the
163
+ // state path — see `findRawStateWrites`.
164
+ RAW_STATE_WRITE: 'raw_state_write',
165
+ // Axis 4 (§8.3, Decision 4(d)): prompt-layer prose shelling out to a
166
+ // write-side `gsd-tools` subcommand — see `findPromptSeamUses`.
167
+ PROMPT_LAYER_STATE_WRITE: 'prompt_layer_state_write',
168
+ // Axis 5 (§8.3, RETAINED, issue #3871): a direct `syncStateFrontmatter(` or
169
+ // `applyPostSyncPreservation(` call outside their owner
170
+ // (`syncAndPreserveStateMd`) — see `findCompositionBypasses`.
171
+ COMPOSITION_BYPASS: 'composition_bypass',
172
+ });
173
+
174
+ // Scan surface — declared, per Decision 4(d), never inferred from `src/`
175
+ // alone. `src/` covers the executor; the prompt layer covers markdown that
176
+ // can shell out to `state.patch` / `phase.complete` and post-process the
177
+ // result outside any TypeScript this guard could see.
178
+ const SRC_DIRS = ['src'];
179
+ const SRC_EXT = new Set(['.cts']);
180
+ const PROMPT_DIRS = ['gsd-core/workflows', 'commands', 'agents', 'skills'];
181
+ const PROMPT_EXT = new Set(['.md']);
182
+
183
+ // The executor (Axis 1 / Axis 3). Forward-slash literal: every `rel` this
184
+ // guard compares against it is unconditionally POSIX-normalized first
185
+ // (`toPosixRel` below) — never gated on `process.platform`.
186
+ const EXECUTOR_FILE = 'src/state-transition.cts';
187
+
188
+ // The write-seam composition owner (Axis 5). Forward-slash literal, same
189
+ // POSIX-normalization rule as `EXECUTOR_FILE` above.
190
+ const SEAM_OWNER_FILE = 'src/state.cts';
191
+
192
+ // Per Decision 4(d)'s "owner FILE is not exempt, only its named canonical
193
+ // FUNCTIONS are": a `syncStateFrontmatter(`/`applyPostSyncPreservation(` call
194
+ // inside one of these two functions, in `SEAM_OWNER_FILE` only, is the
195
+ // seam's own internal plumbing, not a bypass. `writeStateMd` is the
196
+ // `cmdStateSync`/`REGENERATE_STATE` path's own I/O wrapper calling
197
+ // `syncStateFrontmatter` directly (no preservation, by design — §8.3's
198
+ // closed exception list; ADR-3473 §8.6 gates ITS third parameter, which is
199
+ // an orthogonal, type-level check — this guard's exemption is about which
200
+ // FUNCTION BODY a raw call to the two seam stages is allowed to live in).
201
+ // `syncAndPreserveStateMd` is the ONE write-seam composition — every OTHER
202
+ // caller needing a non-standard I/O envelope routes through it. Every OTHER
203
+ // function in `state.cts` — and every function in every OTHER file — is
204
+ // still scanned and still flagged; in particular `readModifyWriteStateMd` is
205
+ // NOT exempt: it calls `syncAndPreserveStateMd` like everyone else, so if a
206
+ // direct `syncStateFrontmatter(`/`applyPostSyncPreservation(` call
207
+ // reappeared there it would be exactly the re-assembly shape this axis
208
+ // exists to catch.
209
+ const SEAM_OWNER_EXEMPT_FUNCTIONS = ['writeStateMd', 'syncAndPreserveStateMd'];
210
+
211
+ // Unconditional path-separator normalization (never gated on
212
+ // `process.platform` — a Windows-authored fork PR must be judged by the
213
+ // same POSIX-relative rule as everything else this guard reads).
214
+ function toPosixRel(rel) {
215
+ return rel.split(path.sep).join('/');
216
+ }
217
+
218
+ /**
219
+ * Strip `//` line comments and `/* ... *\/` block comments from `text`,
220
+ * returning one entry PER INPUT LINE so line numbers computed against the
221
+ * result stay correct against the original file. Block comments are tracked
222
+ * across lines (`inBlock`); line comments only ever affect their own line.
223
+ *
224
+ * Deliberately does NOT parse string/template literal contents — a `//` or
225
+ * `/*` embedded inside a quoted string is treated exactly like real source,
226
+ * which can occasionally UNDER-strip (leaving a would-be-comment's text
227
+ * live) but never OVER-strips real code into invisibility. Per this guard's
228
+ * header and `lint-state-field-drift.cjs`'s own precedent: over-reporting a
229
+ * documentation paragraph that merely DESCRIBES a call (ADR-3180 Amendment
230
+ * 3's exact false positive) is the failure this exists to prevent; a rare
231
+ * miss on an adversarial one-line string is an accepted, narrower risk in
232
+ * the opposite (safe) direction — under-reporting, never over-reporting.
233
+ */
234
+ function stripComments(text) {
235
+ const lines = text.split('\n');
236
+ const out = new Array(lines.length);
237
+ let inBlock = false;
238
+ for (let i = 0; i < lines.length; i++) {
239
+ const line = lines[i];
240
+ let result = '';
241
+ let j = 0;
242
+ while (j < line.length) {
243
+ if (inBlock) {
244
+ const close = line.indexOf('*/', j);
245
+ if (close === -1) {
246
+ j = line.length;
247
+ break;
248
+ }
249
+ j = close + 2;
250
+ inBlock = false;
251
+ continue;
252
+ }
253
+ if (line[j] === '/' && line[j + 1] === '/') {
254
+ j = line.length; // rest of line is a line comment
255
+ break;
256
+ }
257
+ if (line[j] === '/' && line[j + 1] === '*') {
258
+ inBlock = true;
259
+ j += 2;
260
+ continue;
261
+ }
262
+ result += line[j];
263
+ j++;
264
+ }
265
+ out[i] = result;
266
+ }
267
+ return out;
268
+ }
269
+
270
+ // A named function declaration, tolerating `export`/`async` prefixes — the
271
+ // SAME shape `nearestPrecedingAssignment` uses as its backward-scan boundary,
272
+ // and `enclosingFunction` below uses to recognise (and skip) the seam
273
+ // functions' own definitions.
274
+ const FUNCTION_DECL_LINE_RE = /^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/;
275
+
276
+ /**
277
+ * Nearest preceding named-function declaration, scanning `lines` BACKWARD
278
+ * from `index`. Scopes an exemption to a FUNCTION, never a FILE — a whole-
279
+ * file owner exemption is precisely how `getMilestoneInfo` stayed invisible
280
+ * to an earlier drift guard (ADR-3180 Decision 4(d)'s own cautionary case,
281
+ * cross-referenced by this guard's header).
282
+ */
283
+ function enclosingFunction(lines, index) {
284
+ for (let i = index; i >= 0; i--) {
285
+ const m = FUNCTION_DECL_LINE_RE.exec(lines[i]);
286
+ if (m) return m[1];
287
+ }
288
+ return null;
289
+ }
290
+
291
+ // One member of the `FieldPreservation` union, e.g. `'preserve-always'` —
292
+ // lowercase-with-dashes, single-quoted.
293
+ const POLICY_UNION_START_RE = /export\s+type\s+FieldPreservation\s*=/;
294
+ const POLICY_UNION_MEMBER_RE = /'([a-z][a-z-]*)'/g;
295
+
296
+ /**
297
+ * Parse the members of `export type FieldPreservation = 'a' | 'b' | ...;`
298
+ * straight out of the executor's own source, so this guard cannot drift
299
+ * from the type it polices (a hardcoded copy of the union would be exactly
300
+ * the "declared here, enforced somewhere else" shape ADR-3408 exists to
301
+ * remove — this time inside the GUARD). In `src/state-transition.cts` the
302
+ * union is declared across several lines, each shaped like
303
+ * ` | 'clear'; // remove the field entirely`. Scans from the declaration
304
+ * line forward, collecting every quoted token on each line, and stops at
305
+ * the first line whose (comment-INCLUDING) text still carries a `;` — the
306
+ * statement terminator ends the union regardless of any trailing comment.
307
+ */
308
+ function readPolicyUnion(text) {
309
+ const lines = text.split('\n');
310
+ const members = [];
311
+ let inUnion = false;
312
+ for (const line of lines) {
313
+ if (!inUnion) {
314
+ if (!POLICY_UNION_START_RE.test(line)) continue;
315
+ inUnion = true;
316
+ }
317
+ POLICY_UNION_MEMBER_RE.lastIndex = 0;
318
+ let m;
319
+ while ((m = POLICY_UNION_MEMBER_RE.exec(line)) !== null) {
320
+ members.push(m[1]);
321
+ }
322
+ if (line.includes(';')) break;
323
+ }
324
+ return members;
325
+ }
326
+
327
+ // `getFieldClassification(` called with a quoted string-literal argument —
328
+ // the field-name-keyed dispatch shape. `getFieldClassification(variable)`
329
+ // (a bare identifier, no quote) never matches this pattern, by construction
330
+ // (the quote-character backreference requires an opening quote immediately
331
+ // inside the parens) — the correct, table-driven shape is silent here.
332
+ const FIELD_NAME_DISPATCH_RE = /getFieldClassification\s*\(\s*(['"`])([^'"`]+)\1\s*\)/g;
333
+
334
+ // `field === '<literal>'` / `field !== '<literal>'`, and the reversed
335
+ // `'<literal>' === field` — the field-name-keyed BRANCH shape (as opposed to
336
+ // `FIELD_NAME_DISPATCH_RE`'s field-name-keyed CALL shape above; both report
337
+ // the same `REASON.FIELD_NAME_DISPATCH`, since both are "a branch selected
338
+ // by field name", ADR-3408 §8.1's exact prohibition). This is the shape a
339
+ // bypass takes when it routes AROUND `getFieldClassification` entirely
340
+ // rather than through it — ADR-3408 Decision 5's "route the bypass through
341
+ // a wrapper or a differently-named local" gaming route.
342
+ //
343
+ // Deliberately scoped to ONLY an identifier literally named `field` — the
344
+ // dispatch loop's own loop variable declared at
345
+ // `for (const field of Object.keys(FIELD_CLASSIFICATION))` a few dozen lines
346
+ // below in this same file. This is a DECLARED, narrow limitation, not a
347
+ // silent one: a rename of the loop variable would evade this detector
348
+ // entirely, and an unrelated local elsewhere in this file that happens to
349
+ // also be named `field` would false-positive. Both risks are accepted
350
+ // in trade for avoiding a name-agnostic match, which would flag every
351
+ // unrelated `===`/`!==` string comparison in the file (there are many —
352
+ // e.g. `derivedName !== MILESTONE_PLACEHOLDER`-shaped guards) and bury the
353
+ // real signal in noise; per this guard's own header, over-reporting a
354
+ // comment is an accepted risk but over-reporting live code this broadly is
355
+ // not.
356
+ //
357
+ // The reversed `!==` form (`'<literal>' !== field`) is deliberately NOT
358
+ // matched — not observed anywhere in this codebase, and left out rather
359
+ // than speculatively widened past what was found in practice.
360
+ const FIELD_VAR_EQ_LITERAL_RE = /\bfield\s*(?:===|!==)\s*(['"`])([^'"`]+)\1/g;
361
+ const LITERAL_EQ_FIELD_VAR_RE = /(['"`])([^'"`]+)\1\s*===\s*\bfield\b/g;
362
+
363
+ /**
364
+ * AXIS 1a: every `getFieldClassification('<literal>')` CALL, and every
365
+ * `field === '<literal>'` / `field !== '<literal>'` / `'<literal>' ===
366
+ * field` BRANCH, inside the executor is a field-name-keyed branch (§8.1).
367
+ * Only ever called against `EXECUTOR_FILE` — `collect()` gates the call
368
+ * site, mirroring `findPolicyDispatchDrift`'s own "only when rel ===
369
+ * EXECUTOR_FILE" rule from the spec this guard was authored against.
370
+ * `preservation === '<member>'` comparisons — the CORRECT policy-dispatch
371
+ * shape `findUnimplementedPolicies` requires to exist — are unaffected: the
372
+ * identifier compared there is `preservation`, never `field`, so
373
+ * `FIELD_VAR_EQ_LITERAL_RE`'s `\bfield\b` anchor does not reach them.
374
+ */
375
+ function findPolicyDispatchDrift(rel, text) {
376
+ const out = [];
377
+ const stripped = stripComments(text);
378
+ for (let i = 0; i < stripped.length; i++) {
379
+ const line = stripped[i];
380
+ if (!line.trim()) continue;
381
+ // `file` is sanitized here, at construction, not just at the human
382
+ // formatter: `rel` is exactly as attacker-controlled as `source` on a
383
+ // fork PR (a tracked filename can legally carry C1 bytes or bidi
384
+ // overrides), and it reaches `--json` stdout unfiltered otherwise — see
385
+ // `sanitizeForReport`'s own header.
386
+ FIELD_NAME_DISPATCH_RE.lastIndex = 0;
387
+ let m;
388
+ while ((m = FIELD_NAME_DISPATCH_RE.exec(line)) !== null) {
389
+ out.push({
390
+ reason: REASON.FIELD_NAME_DISPATCH,
391
+ axis: 'policy-dispatch',
392
+ file: sanitizeForReport(rel),
393
+ line: i + 1,
394
+ // `field` is captured straight out of a quoted string literal in
395
+ // repo source — attacker-controlled on the same fork-PR basis as
396
+ // `file`/`source`, so sanitize it too rather than let it reach
397
+ // `--json` stdout.
398
+ field: sanitizeForReport(m[2]),
399
+ source: sanitizeForReport(line.trim()),
400
+ });
401
+ }
402
+ FIELD_VAR_EQ_LITERAL_RE.lastIndex = 0;
403
+ while ((m = FIELD_VAR_EQ_LITERAL_RE.exec(line)) !== null) {
404
+ out.push({
405
+ reason: REASON.FIELD_NAME_DISPATCH,
406
+ axis: 'policy-dispatch',
407
+ file: sanitizeForReport(rel),
408
+ line: i + 1,
409
+ field: sanitizeForReport(m[2]),
410
+ source: sanitizeForReport(line.trim()),
411
+ });
412
+ }
413
+ LITERAL_EQ_FIELD_VAR_RE.lastIndex = 0;
414
+ while ((m = LITERAL_EQ_FIELD_VAR_RE.exec(line)) !== null) {
415
+ out.push({
416
+ reason: REASON.FIELD_NAME_DISPATCH,
417
+ axis: 'policy-dispatch',
418
+ file: sanitizeForReport(rel),
419
+ line: i + 1,
420
+ field: sanitizeForReport(m[2]),
421
+ source: sanitizeForReport(line.trim()),
422
+ });
423
+ }
424
+ }
425
+ return out;
426
+ }
427
+
428
+ /**
429
+ * AXIS 1b: every `FieldPreservation` member (read from `text` via
430
+ * `readPolicyUnion`, so the check cannot itself drift from the union) that
431
+ * has no `preservation === '<member>'` comparison anywhere in the
432
+ * comment-stripped executor source is a declared policy with no executor —
433
+ * §8.1's mirror defect, one level up (a whole MEMBER unimplemented, not just
434
+ * one dispatch call keyed on a field name). `derive` and `clear` are the
435
+ * live instances ADR-3408 §8.6 names.
436
+ */
437
+ function findUnimplementedPolicies(text, rel) {
438
+ const members = readPolicyUnion(text);
439
+ const strippedText = stripComments(text).join('\n');
440
+ const out = [];
441
+ for (const member of members) {
442
+ const memberRe = new RegExp(`preservation\\s*===\\s*'${escapeRegex(member)}'`);
443
+ if (memberRe.test(strippedText)) continue;
444
+ // `file` and `policy` are sanitized here for the same reason as
445
+ // `findPolicyDispatchDrift` above: both `rel` and a `FieldPreservation`
446
+ // union member are attacker-controlled on a fork PR, exactly like
447
+ // `source`.
448
+ out.push({
449
+ reason: REASON.UNIMPLEMENTED_POLICY,
450
+ axis: 'policy-dispatch',
451
+ file: sanitizeForReport(rel),
452
+ line: 0,
453
+ policy: sanitizeForReport(member),
454
+ source: sanitizeForReport(`FieldPreservation member '${member}' has no executor`),
455
+ });
456
+ }
457
+ return out;
458
+ }
459
+
460
+ // AXIS 3 (§8.3(b), closed Phase 2 / #3469): `stateReplaceField(<contentArg>,
461
+ // <fieldArg>, ...)` on a single line, capturing both argument expressions.
462
+ // `contentArg` must be a bare identifier (a call expression or property
463
+ // access as the first argument is not matched — silently out of scope, per
464
+ // this axis's own narrow-limitation note below) so its assignments can be
465
+ // tracked; `fieldArg` is everything up to the next comma, trimmed, so its
466
+ // literal-vs-variable shape can be read off directly.
467
+ const STATE_REPLACE_FIELD_CALL_RE = /\bstateReplaceField\s*\(\s*([A-Za-z_$][\w$]*)\s*,\s*([^,()]+),/g;
468
+
469
+ // True when `arg` (already trimmed) is a fixed string/template literal —
470
+ // the safe shape, since every literal field name this codebase actually
471
+ // uses is a Title-Case body label that cannot collide with a lowercase/
472
+ // snake_case YAML frontmatter key.
473
+ function isQuotedLiteralArg(arg) {
474
+ const t = arg.trim();
475
+ return t.startsWith("'") || t.startsWith('"') || t.startsWith('`');
476
+ }
477
+
478
+ /**
479
+ * The nearest assignment to `varName` (`varName = <expr>` or
480
+ * `const|let|var varName = <expr>`), scanning `lines` BACKWARD from `index`
481
+ * (inclusive) and stopping at the nearest preceding named-function
482
+ * declaration. Returns the assigned expression's trimmed text, or `null`
483
+ * when no such assignment is found before the boundary — meaning `varName`
484
+ * is the enclosing function's own untouched parameter.
485
+ *
486
+ * Deliberately single-hop: this reports whatever the NEAREST assignment's
487
+ * right-hand side literally is, and does not itself follow a further alias
488
+ * (`let body = someOtherVar;` is reported as `"someOtherVar"`, not resolved
489
+ * further). Every real call site in this file assigns its body variable
490
+ * directly from `stripFrontmatter(content)` with no intermediate alias
491
+ * (`updateCore`, `patchCore`, `beginPhaseCore`'s `tryField` helper) — a
492
+ * future call site that introduces one extra hop of aliasing would evade
493
+ * this check. A declared, narrow limitation, not a silent one — mirrors
494
+ * this file's existing precedent (`FIELD_VAR_EQ_LITERAL_RE`'s own
495
+ * documented scope) of accepting a bounded risk in trade for not chasing
496
+ * full dataflow, which is exactly what made the Phase 1 approximation
497
+ * unusable (29 false positives to 1 true positive).
498
+ */
499
+ function nearestPrecedingAssignment(lines, index, varName) {
500
+ const assignRe = new RegExp(`(?:^|[^.\\w$])(?:const|let|var)?\\s*${escapeRegex(varName)}\\s*=\\s*([^=].*)$`);
501
+ for (let i = index; i >= 0; i--) {
502
+ if (FUNCTION_DECL_LINE_RE.test(lines[i])) return null;
503
+ const m = assignRe.exec(lines[i]);
504
+ if (m) return m[1].trim();
505
+ }
506
+ return null;
507
+ }
508
+
509
+ /**
510
+ * AXIS 3: every `stateReplaceField(` call in `EXECUTOR_FILE` whose field-name
511
+ * argument is a VARIABLE (not a quoted literal) — the only shape that can
512
+ * ever rewrite YAML frontmatter, since `stateReplaceField`'s `^field:` line
513
+ * pattern is case-insensitive and matches any line starting with that name,
514
+ * literal or not — AND whose content argument was not assigned from
515
+ * `stripFrontmatter(` at the nearest preceding assignment. A literal
516
+ * field-name argument is never flagged regardless of stripping: every fixed
517
+ * string this file's `stateReplaceField` calls use is a Title-Case body
518
+ * label (`'Phase'`, `'Total Plans in Phase'`, ...) that cannot collide with
519
+ * a lowercase/snake_case frontmatter key by construction, so checking its
520
+ * content argument would only add false positives on the ~20 already-safe
521
+ * `sectionBody`-scoped calls this axis must NOT report (mirrors
522
+ * `updateCore`'s strip-then-replace shape, and `beginPhaseCore`'s
523
+ * `stateReplaceField(body, name, value)`, both legitimately unflagged).
524
+ */
525
+ function findUnstrippedContentWrites(rel, text) {
526
+ const rawLines = text.split('\n');
527
+ const stripped = stripComments(text);
528
+ const out = [];
529
+ for (let i = 0; i < stripped.length; i++) {
530
+ const line = stripped[i];
531
+ if (!line.trim()) continue;
532
+ STATE_REPLACE_FIELD_CALL_RE.lastIndex = 0;
533
+ let m;
534
+ while ((m = STATE_REPLACE_FIELD_CALL_RE.exec(line)) !== null) {
535
+ const contentArg = m[1];
536
+ const fieldArg = m[2];
537
+ if (isQuotedLiteralArg(fieldArg)) continue;
538
+ const assignment = nearestPrecedingAssignment(stripped, i - 1, contentArg);
539
+ const isStripped = assignment !== null && /^stripFrontmatter\s*\(/.test(assignment);
540
+ if (isStripped) continue;
541
+ // `file`/`source` sanitized for the same fork-PR reason as every other
542
+ // finding in this guard; `contentArg` is captured out of repo source
543
+ // (an identifier name), attacker-controlled on the same basis.
544
+ out.push({
545
+ reason: REASON.UNSTRIPPED_CONTENT_WRITE,
546
+ axis: 'frontmatter-write',
547
+ file: sanitizeForReport(rel),
548
+ line: i + 1,
549
+ field: sanitizeForReport(contentArg),
550
+ source: sanitizeForReport(rawLines[i].trim()),
551
+ });
552
+ }
553
+ }
554
+ return out;
555
+ }
556
+
557
+ // AXIS 2 (§8.6, retained): `fs.writeFileSync(<targetArg>, ...)`, capturing
558
+ // the target-path argument up to the next comma. The type system (ADR-3473
559
+ // §8.6's `StateTransaction` constructors) closes the `writeStateMd`/
560
+ // `syncStateFrontmatter`/`applyPostSyncPreservation` bypass shape this guard
561
+ // used to scan for by function name; it cannot close a call site that skips
562
+ // those functions ENTIRELY and reaches for Node's raw `fs` module directly —
563
+ // that residual risk is what this axis stays alive to catch.
564
+ const RAW_WRITE_CALL_START_RE = /\bfs\.writeFileSync\s*\(/g;
565
+
566
+ /**
567
+ * Capture `fs.writeFileSync`'s first-argument text starting at `startIdx`
568
+ * (the offset right after its opening `(`), stopping at the first TOP-LEVEL
569
+ * comma or the call's own closing paren — bracket/paren/brace depth and
570
+ * string-literal spans are tracked so a nested call in the target expression
571
+ * (e.g. `path.join(cwd, 'STATE.md')`) does not stop the scan at ITS internal
572
+ * comma. A naive `[^,]+` capture (the guard's prior encoding) stopped at
573
+ * `path.join(cwd` for exactly that shape, silently missing every
574
+ * `fs.writeFileSync(path.join(cwd, 'STATE.md'), …)` call in the wild
575
+ * (found via `tests/state-write-path-drift-guard.test.cjs` F1: "guard:
576
+ * fs.writeFileSync against a STATE.md literal is reported").
577
+ */
578
+ function captureFirstArg(line, startIdx) {
579
+ let depth = 0;
580
+ let inStr = null;
581
+ let i = startIdx;
582
+ for (; i < line.length; i++) {
583
+ const c = line[i];
584
+ if (inStr) {
585
+ if (c === '\\') { i++; continue; }
586
+ if (c === inStr) inStr = null;
587
+ continue;
588
+ }
589
+ if (c === '\'' || c === '"' || c === '`') { inStr = c; continue; }
590
+ if (c === '(' || c === '[' || c === '{') { depth++; continue; }
591
+ if (c === ')' || c === ']' || c === '}') {
592
+ if (depth === 0) break; // the call's own closing paren — no comma found
593
+ depth--;
594
+ continue;
595
+ }
596
+ if (c === ',' && depth === 0) break;
597
+ }
598
+ return line.slice(startIdx, i);
599
+ }
600
+
601
+ // True when the captured target-path expression plausibly names the STATE.md
602
+ // path: either the canonical `statePath` identifier this codebase uses at
603
+ // every real write site (see `src/state.cts`'s `writeStateMd`,
604
+ // `readModifyWriteStateMd`, `cmdStateMilestoneSwitch`), or a literal/template
605
+ // segment containing `STATE.md` outright.
606
+ function targetsStatePath(arg) {
607
+ return /\bstatePath\b/.test(arg) || /STATE\.md/.test(arg);
608
+ }
609
+
610
+ /**
611
+ * AXIS 2: every `fs.writeFileSync(` call in `text` whose target argument
612
+ * names the state path is a raw write that bypasses BOTH the write seam
613
+ * (`writeStateMd` / `syncAndPreserveStateMd`) and the OS Shell Projection
614
+ * seam (`platformWriteSync`, `src/shell-command-projection.cts`) — no
615
+ * legitimate call site in this codebase writes STATE.md this way today
616
+ * (every real writer goes through `platformWriteSync`, whose OWN internal
617
+ * `fs.writeFileSync` calls take a generic `filePath`/`tmpPath` argument, not
618
+ * `statePath`, and are therefore never matched by `targetsStatePath` above).
619
+ * Unratcheted, unexempted: any occurrence is a violation.
620
+ */
621
+ function findRawStateWrites(rel, text) {
622
+ const rawLines = text.split('\n');
623
+ const stripped = stripComments(text);
624
+ const out = [];
625
+ for (let i = 0; i < stripped.length; i++) {
626
+ const line = stripped[i];
627
+ if (!line.trim()) continue;
628
+ RAW_WRITE_CALL_START_RE.lastIndex = 0;
629
+ let m;
630
+ while ((m = RAW_WRITE_CALL_START_RE.exec(line)) !== null) {
631
+ const argStart = m.index + m[0].length;
632
+ const targetArg = captureFirstArg(line, argStart).trim();
633
+ if (!targetsStatePath(targetArg)) continue;
634
+ // `file`/`source` sanitized for the same fork-PR reason as every other
635
+ // finding in this guard.
636
+ out.push({
637
+ reason: REASON.RAW_STATE_WRITE,
638
+ axis: 'raw-write',
639
+ file: sanitizeForReport(rel),
640
+ line: i + 1,
641
+ source: sanitizeForReport(rawLines[i].trim()),
642
+ });
643
+ }
644
+ }
645
+ return out;
646
+ }
647
+
648
+ // The two write-seam STAGE functions, matched only as CALLS (`\(`
649
+ // immediately after, modulo whitespace) — never as bare mentions of the
650
+ // name. `writeStateMd(` is deliberately NOT included here (that arm is
651
+ // retired — ADR-3473 §8.6 gates it at the type level instead).
652
+ const SEAM_CALL_RE = /\b(syncStateFrontmatter|applyPostSyncPreservation)\s*\(/g;
653
+ // A line that IS one of the two seam stage functions' own definitions —
654
+ // skipped outright, never counted as a call to itself.
655
+ const SEAM_DEF_LINE_RE = /^\s*(?:export\s+)?(?:async\s+)?function\s+(?:syncStateFrontmatter|applyPostSyncPreservation)\b/;
656
+
657
+ /**
658
+ * AXIS 5 (§8.3, RETAINED, issue #3871): every direct `syncStateFrontmatter(`/
659
+ * `applyPostSyncPreservation(` call in `text`, outside the two functions' own
660
+ * definitions and (only inside `SEAM_OWNER_FILE`) outside
661
+ * `SEAM_OWNER_EXEMPT_FUNCTIONS`'s own bodies. Terminal: unlike the old
662
+ * `findSeamBypasses` this axis descends from, there is no ratchet — any
663
+ * occurrence is `REASON.COMPOSITION_BYPASS` directly.
664
+ */
665
+ function findCompositionBypasses(rel, text) {
666
+ const rawLines = text.split('\n');
667
+ const stripped = stripComments(text);
668
+ const out = [];
669
+ for (let i = 0; i < stripped.length; i++) {
670
+ const line = stripped[i];
671
+ if (!line.trim()) continue;
672
+ if (SEAM_DEF_LINE_RE.test(line)) continue;
673
+ SEAM_CALL_RE.lastIndex = 0;
674
+ let m;
675
+ while ((m = SEAM_CALL_RE.exec(line)) !== null) {
676
+ if (rel === SEAM_OWNER_FILE) {
677
+ const fn = enclosingFunction(stripped, i);
678
+ if (fn && SEAM_OWNER_EXEMPT_FUNCTIONS.includes(fn)) continue;
679
+ }
680
+ // `file`/`source` sanitized for the same fork-PR reason as every other
681
+ // finding in this guard.
682
+ out.push({
683
+ reason: REASON.COMPOSITION_BYPASS,
684
+ axis: 'composition-bypass',
685
+ file: sanitizeForReport(rel),
686
+ line: i + 1,
687
+ symbol: m[1],
688
+ source: sanitizeForReport(rawLines[i].trim()),
689
+ });
690
+ }
691
+ }
692
+ return out;
693
+ }
694
+
695
+ // Prose in the prompt layer shelling out to a write-side `gsd-tools`
696
+ // subcommand — the SAME write seam, expressed as markdown instructing an
697
+ // agent to run a command, rather than TypeScript calling a function
698
+ // directly (Decision 4(d)'s "the scan surface is declared, and is not just
699
+ // `src/`"). Matched on RAW lines — no comment stripping — because markdown
700
+ // carries no comment syntax this guard should be stripping in the first
701
+ // place. `g`-flagged so multiple candidate occurrences on one line are all
702
+ // checked against backtick spans below, rather than only the first.
703
+ const PROMPT_SEAM_RE = /gsd[-_]?tools[^\n]*\b(state\.patch|state\.planned-phase|state\.sync|phase\.complete)\b/g;
704
+
705
+ /**
706
+ * Every `` `...` `` inline-code span on `line`, as `[start, end)` character
707
+ * ranges (end exclusive). Handles multiple spans on one line correctly by
708
+ * repeated `exec` over a global, non-overlapping backtick-pair pattern —
709
+ * unlike a naive "count backticks before the match" parity check, this does
710
+ * not get confused by a line that mixes code spans with unrelated literal
711
+ * backticks (e.g. an unmatched one in prose).
712
+ */
713
+ const CODE_SPAN_RE = /`[^`\n]+`/g;
714
+ function codeSpanRanges(line) {
715
+ const ranges = [];
716
+ CODE_SPAN_RE.lastIndex = 0;
717
+ let m;
718
+ while ((m = CODE_SPAN_RE.exec(line)) !== null) {
719
+ ranges.push([m.index, m.index + m[0].length]);
720
+ }
721
+ return ranges;
722
+ }
723
+
724
+ /**
725
+ * True when character offset `index` of `line` falls inside one of `line`'s
726
+ * inline-code spans.
727
+ */
728
+ function isInsideCodeSpan(line, index) {
729
+ return codeSpanRanges(line).some(([start, end]) => index >= start && index < end);
730
+ }
731
+
732
+ /**
733
+ * AXIS 4: every prompt-layer line instructing an agent to shell out to a
734
+ * write-side `gsd-tools` subcommand. Terminal (unratcheted): any occurrence
735
+ * is a violation — this baseline was always empty for the prompt layer (no
736
+ * prompt-layer entry was ever acknowledged), so removing the ratchet changes
737
+ * nothing observable here.
738
+ *
739
+ * A candidate occurrence enclosed in backticks is a MENTION, not an
740
+ * invocation, and is deliberately not reported — CONTRIBUTING.md's "Every
741
+ * `commit` invocation in shipped content must declare `--files`" section
742
+ * states the repo's settled convention verbatim: "Write the command
743
+ * reference in backticks — the repo's own convention — and it is correctly
744
+ * read as a mention." ADR-3180 Amendment 3 records the cost of getting this
745
+ * wrong: the first `lint-phase-enumeration-drift.cjs` flagged JSDoc that
746
+ * merely documented the canonical owner as drift, which trains readers to
747
+ * reflexively exempt documentation instead of trusting the guard — the
748
+ * opposite of Decision 4(a)'s intent.
749
+ */
750
+ function findPromptSeamUses(rel, text) {
751
+ const lines = text.split('\n');
752
+ const out = [];
753
+ for (let i = 0; i < lines.length; i++) {
754
+ const line = lines[i];
755
+ PROMPT_SEAM_RE.lastIndex = 0;
756
+ let m;
757
+ while ((m = PROMPT_SEAM_RE.exec(line)) !== null) {
758
+ if (isInsideCodeSpan(line, m.index)) continue;
759
+ // `file` is sanitized here for the same reason as every other finding
760
+ // in this guard: `rel` is attacker-controlled on a fork PR, exactly
761
+ // like `source`.
762
+ out.push({
763
+ reason: REASON.PROMPT_LAYER_STATE_WRITE,
764
+ axis: 'write-seam',
765
+ file: sanitizeForReport(rel),
766
+ line: i + 1,
767
+ symbol: 'prompt-layer-state-write',
768
+ source: sanitizeForReport(line.trim()),
769
+ });
770
+ }
771
+ }
772
+ return out;
773
+ }
774
+
775
+ /**
776
+ * Run both scan passes (the `src/` tree for Axis 1 + Axis 2 + Axis 3 + Axis 5,
777
+ * the prompt layer for Axis 4) and return the flat, already-terminal finding
778
+ * list — every finding this guard produces carries its own `reason`; there
779
+ * is no longer a ratcheted axis needing a second pass against a baseline.
780
+ *
781
+ * `root` defaults to `REPO_ROOT` (this repo) so every existing caller —
782
+ * `npm run lint:ci`, a bare `node scripts/lint-state-write-path-drift.cjs`,
783
+ * every other module that `require`s `collect` with no argument — is
784
+ * byte-identical to before this parameter existed. It is overridable so a
785
+ * test can exercise the guard's real scanning/reporting logic against a
786
+ * throwaway synthetic tree instead of mutating this repository's own `src/`
787
+ * to prove the guard can fail (see `--root` on the CLI, and F2 in
788
+ * `tests/state-write-path-drift-guard.test.cjs`).
789
+ */
790
+ function collect(root = REPO_ROOT) {
791
+ const srcFindings = scanTree({
792
+ root,
793
+ scanDirs: SRC_DIRS,
794
+ scanExt: SRC_EXT,
795
+ onFile(rel, text) {
796
+ const relPosix = toPosixRel(rel);
797
+ const found = [];
798
+ if (relPosix === EXECUTOR_FILE) {
799
+ found.push(...findPolicyDispatchDrift(relPosix, text));
800
+ found.push(...findUnimplementedPolicies(text, relPosix));
801
+ found.push(...findUnstrippedContentWrites(relPosix, text));
802
+ }
803
+ found.push(...findRawStateWrites(relPosix, text));
804
+ found.push(...findCompositionBypasses(relPosix, text));
805
+ return found;
806
+ },
807
+ });
808
+
809
+ const promptFindings = scanTree({
810
+ root,
811
+ scanDirs: PROMPT_DIRS,
812
+ scanExt: PROMPT_EXT,
813
+ onFile(rel, text) {
814
+ return findPromptSeamUses(toPosixRel(rel), text);
815
+ },
816
+ });
817
+
818
+ return { findings: [...srcFindings, ...promptFindings] };
819
+ }
820
+
821
+ const GOODHART_NOTE =
822
+ 'Goodhart note (ADR-3408 Decision 5): this "0 write-path violations" is a LAGGING metric — report ' +
823
+ 'it only alongside the behavioral identity test\'s result (asserted at the consumer\'s output), ' +
824
+ 'never alone.';
825
+
826
+ function printFindings(findings) {
827
+ for (const f of findings) {
828
+ process.stderr.write(`[${f.reason}] ${sanitizeForReport(f.file)}:${f.line}\n`);
829
+ process.stderr.write(` ${sanitizeForReport(f.source)}\n`);
830
+ }
831
+ }
832
+
833
+ /**
834
+ * Parse `argv` into `{ root, wantJson, unknown }`. `--root <dir>` overrides
835
+ * the scan root (default `REPO_ROOT`, resolved relative to `process.cwd()`
836
+ * when given); `--json` is a bare flag. Anything else lands in `unknown` so
837
+ * `main` can report a usage error rather than silently ignoring a typo.
838
+ */
839
+ function parseArgs(argv) {
840
+ const args = argv || [];
841
+ let root = REPO_ROOT;
842
+ let wantJson = false;
843
+ const unknown = [];
844
+ for (let i = 0; i < args.length; i++) {
845
+ const a = args[i];
846
+ if (a === '--json') {
847
+ wantJson = true;
848
+ continue;
849
+ }
850
+ if (a === '--root') {
851
+ const value = args[i + 1];
852
+ if (typeof value !== 'string' || value.length === 0) {
853
+ return { error: '--root requires a directory argument' };
854
+ }
855
+ root = path.resolve(value);
856
+ i += 1;
857
+ continue;
858
+ }
859
+ unknown.push(a);
860
+ }
861
+ return { root, wantJson, unknown };
862
+ }
863
+
864
+ /**
865
+ * `argv`: `--json` prints the machine-readable finding set instead of the
866
+ * human-readable report; `--root <dir>` overrides the scan root (defaults to
867
+ * this repo — see `parseArgs`'s own docstring). Exit codes: 0 clean, 1 a
868
+ * finding was reported, 2 usage error.
869
+ */
870
+ function main(argv) {
871
+ const parsed = parseArgs(argv);
872
+ if (parsed.error) {
873
+ process.stderr.write(`lint-state-write-path-drift: ${parsed.error}\n`);
874
+ process.exitCode = 2;
875
+ return;
876
+ }
877
+ const { root, wantJson, unknown } = parsed;
878
+ if (unknown.length > 0) {
879
+ process.stderr.write(
880
+ `lint-state-write-path-drift: unrecognized argument(s): ${unknown.map((a) => sanitizeForReport(a)).join(', ')} ` +
881
+ '(expected --json and/or --root <dir>)\n',
882
+ );
883
+ process.exitCode = 2;
884
+ return;
885
+ }
886
+
887
+ const { findings } = collect(root);
888
+ const summary = {
889
+ policyDispatchViolations: findings.filter((f) => f.axis === 'policy-dispatch').length,
890
+ frontmatterWriteViolations: findings.filter((f) => f.axis === 'frontmatter-write').length,
891
+ rawWriteViolations: findings.filter((f) => f.axis === 'raw-write').length,
892
+ writeSeamViolations: findings.filter((f) => f.axis === 'write-seam').length,
893
+ compositionBypassViolations: findings.filter((f) => f.axis === 'composition-bypass').length,
894
+ };
895
+
896
+ if (wantJson) {
897
+ process.stdout.write(`${JSON.stringify({ ok: findings.length === 0, findings, summary }, null, 2)}\n`);
898
+ process.exitCode = findings.length === 0 ? 0 : 1;
899
+ return;
900
+ }
901
+
902
+ if (findings.length === 0) {
903
+ process.stdout.write(
904
+ 'ok state-write-path-drift: no policy-dispatch, frontmatter-write, raw-write, prompt-layer, or ' +
905
+ 'composition-bypass violations found\n',
906
+ );
907
+ process.stdout.write(`${GOODHART_NOTE}\n`);
908
+ process.exitCode = 0;
909
+ return;
910
+ }
911
+
912
+ process.stderr.write(
913
+ 'state-write-path-drift: violation(s) found (ADR-3408 §8.1/§8.3, ADR-3473 §8.6). See ' +
914
+ 'docs/adr/3408-state-write-path-preservation.md and docs/adr/3473-enforcement-by-construction.md ' +
915
+ 'for the contract:\n',
916
+ );
917
+ printFindings(findings);
918
+ process.exitCode = 1;
919
+ }
920
+
921
+ if (require.main === module) main(process.argv.slice(2));
922
+
923
+ module.exports = {
924
+ REASON,
925
+ REPO_ROOT,
926
+ SRC_DIRS,
927
+ SRC_EXT,
928
+ PROMPT_DIRS,
929
+ PROMPT_EXT,
930
+ EXECUTOR_FILE,
931
+ SEAM_OWNER_FILE,
932
+ SEAM_OWNER_EXEMPT_FUNCTIONS,
933
+ toPosixRel,
934
+ stripComments,
935
+ enclosingFunction,
936
+ readPolicyUnion,
937
+ findPolicyDispatchDrift,
938
+ findUnimplementedPolicies,
939
+ findUnstrippedContentWrites,
940
+ isQuotedLiteralArg,
941
+ nearestPrecedingAssignment,
942
+ findRawStateWrites,
943
+ targetsStatePath,
944
+ findCompositionBypasses,
945
+ findPromptSeamUses,
946
+ isInsideCodeSpan,
947
+ parseArgs,
948
+ collect,
949
+ main,
950
+ };