@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
@@ -61,4 +61,906 @@ function executionContextRefs(content) {
61
61
  return refs;
62
62
  }
63
63
 
64
- module.exports = { CANONICAL_TOOLS, parseFrontmatter, executionContextRefs };
64
+ /**
65
+ * workflowPathRefs(content)
66
+ *
67
+ * Locates every gsd-core-relative workflow path referenced in a markdown
68
+ * string, whether the reference is an eager @-include (already covered by
69
+ * executionContextRefs) or a *lazy* path mentioned only in prose/code — a
70
+ * path a command reads on demand via Read/Bash rather than an @-inclusion
71
+ * the harness inlines automatically. Both kinds are load-bearing: the
72
+ * progressive-disclosure split (#717) deliberately keeps most workflow
73
+ * content out of the eager path so the common case stays cheap, but that
74
+ * means a command naming a workflow only in prose is invisible to
75
+ * executionContextRefs even though the runtime still needs the file to
76
+ * exist. Recognizes three reference shapes:
77
+ *
78
+ * A. Any path whose segments include `workflows/`, optionally preceded by
79
+ * an eager `@`, a home-dir prefix (`~/` or `$HOME/`), `.claude/`, and/or
80
+ * `gsd-core/` — e.g. `@~/.claude/gsd-core/workflows/scan.md`,
81
+ * `gsd-core/workflows/x.md`, or a bare `workflows/x.md`.
82
+ * B. Same as A but without the eager `@` — a lazy reference read on
83
+ * demand rather than inlined at load time.
84
+ * C. Parent-relative sub-file paths with no `workflows/` prefix at all —
85
+ * `execute-phase/steps/post-merge-gate.md` — implicitly rooted under
86
+ * `workflows/` because that's the only place `steps/`, `modes/`, and
87
+ * `templates/` subdirectories live.
88
+ *
89
+ * Traversal segments (`..`) are dropped rather than surfaced: this resolver
90
+ * only ever reports paths under `workflows/`, never something a `..` could
91
+ * walk outside of it. Results are de-duplicated, first-seen order preserved.
92
+ *
93
+ * Both regexes anchor `\.md` with a trailing `(?![A-Za-z0-9_])` negative
94
+ * lookahead so a longer extension (`.mdx`, `.md5`) is rejected outright
95
+ * rather than silently truncated into a plausible-looking `.md` path.
96
+ */
97
+ function workflowPathRefs(content) {
98
+ const refs = [];
99
+ const seen = new Set();
100
+
101
+ function addRef(normalized) {
102
+ if (normalized.split('/').includes('..')) return;
103
+ if (seen.has(normalized)) return;
104
+ seen.add(normalized);
105
+ refs.push(normalized);
106
+ }
107
+
108
+ const shapeARe = /@?(?:(?:~|\$HOME)\/)?(?:\.claude\/)?(?:gsd-core\/)?workflows\/[A-Za-z0-9._/-]+\.md(?![A-Za-z0-9_])/g;
109
+ let m;
110
+ while ((m = shapeARe.exec(content)) !== null) {
111
+ const normalized = m[0]
112
+ .replace(/^@/, '')
113
+ .replace(/^(?:~|\$HOME)\//, '')
114
+ .replace(/^\.claude\//, '')
115
+ .replace(/^gsd-core\//, '');
116
+ addRef(normalized);
117
+ }
118
+
119
+ const shapeCRe = /(?:^|[\s`("'>])([A-Za-z0-9._-]+\/(?:steps|modes|templates)\/[A-Za-z0-9._-]+\.md(?![A-Za-z0-9_]))/gm;
120
+ while ((m = shapeCRe.exec(content)) !== null) {
121
+ addRef('workflows/' + m[1]);
122
+ }
123
+
124
+ return refs;
125
+ }
126
+
127
+ /**
128
+ * unreachableWorkflows(loaderContents, gsdFiles, workflowPaths)
129
+ *
130
+ * Computes reachability over the gsd-core file graph and reports which
131
+ * `workflowPaths` are never reached, starting only from `loaderContents`
132
+ * (commands/agents/skills — the files a runtime actually loads) and walking
133
+ * `workflowPathRefs` edges transitively through `gsdFiles`.
134
+ *
135
+ * The seed set is deliberately restricted to loaders and never includes a
136
+ * workflow's own content. Seeding from workflows too would let two failure
137
+ * modes hide: a workflow that only references itself would satisfy its own
138
+ * reachability, and a pair of workflows that reference only each other would
139
+ * form an island that looks connected from the inside but that no command,
140
+ * agent, or skill ever actually opens. Both are orphans in every sense that
141
+ * matters — nothing external can reach them — and both must be reported.
142
+ * Requiring every path to originate at a loader is what makes "reachable"
143
+ * mean "a runtime can actually get here," not merely "something points to
144
+ * it."
145
+ *
146
+ * `gsdFiles` covers all of `gsd-core/**`, not just `workflows/`, because a
147
+ * `references/` or `templates/` file can itself name a workflow path and
148
+ * needs to be walked through to propagate reachability — restricting the map
149
+ * to `workflows/` would silently break any chain that passes through a
150
+ * non-workflow file.
151
+ *
152
+ * `visited` guards the walk against reference cycles (including the
153
+ * mutual/self cases above) so traversal always terminates.
154
+ */
155
+ function unreachableWorkflows(loaderContents, gsdFiles, workflowPaths) {
156
+ const visited = new Set();
157
+ const queue = [];
158
+
159
+ for (const content of loaderContents) {
160
+ for (const ref of workflowPathRefs(content)) queue.push(ref);
161
+ }
162
+
163
+ while (queue.length > 0) {
164
+ const p = queue.pop();
165
+ if (visited.has(p)) continue;
166
+ visited.add(p);
167
+ if (gsdFiles.has(p)) {
168
+ for (const ref of workflowPathRefs(gsdFiles.get(p))) queue.push(ref);
169
+ }
170
+ }
171
+
172
+ return workflowPaths.filter(p => !visited.has(p));
173
+ }
174
+
175
+ /**
176
+ * splitOutsideDelimiter(str, delimiter)
177
+ *
178
+ * Splits `str` on `delimiter` characters that fall outside any backtick
179
+ * span AND outside any parenthetical span. A backtick toggles an "inside a
180
+ * code span" flag; `(` / `)` track nesting depth. Delimiters seen while
181
+ * inside either construct are treated as literal content rather than split
182
+ * points. Used both for comma-splitting a completion-markers cell and for
183
+ * pipe-splitting a table row, so a marker annotation
184
+ * (`(unconsumed: draft presentation, approved interactively)`) or a path
185
+ * that happens to contain the delimiter inside backticks or parens is never
186
+ * torn in half.
187
+ */
188
+ function splitOutsideDelimiter(str, delimiter) {
189
+ const parts = [];
190
+ let cur = '';
191
+ let inBacktick = false;
192
+ let parenDepth = 0;
193
+ for (const ch of str) {
194
+ if (ch === '`') inBacktick = !inBacktick;
195
+ if (!inBacktick) {
196
+ if (ch === '(') parenDepth += 1;
197
+ else if (ch === ')' && parenDepth > 0) parenDepth -= 1;
198
+ }
199
+ if (ch === delimiter && !inBacktick && parenDepth === 0) {
200
+ parts.push(cur);
201
+ cur = '';
202
+ } else {
203
+ cur += ch;
204
+ }
205
+ }
206
+ parts.push(cur);
207
+ return parts;
208
+ }
209
+
210
+ function splitTableRow(line) {
211
+ let s = line.trim();
212
+ if (s.startsWith('|')) s = s.slice(1);
213
+ if (s.endsWith('|')) s = s.slice(0, -1);
214
+ return splitOutsideDelimiter(s, '|').map(c => c.trim());
215
+ }
216
+
217
+ function isSeparatorRow(line) {
218
+ const t = line.trim();
219
+ return t.length > 0 && /^[|:\- ]+$/.test(t) && t.includes('-');
220
+ }
221
+
222
+ /**
223
+ * parseCompletionMarkersCell(value)
224
+ *
225
+ * Parses one `Completion Markers` table cell into `{markers, notes}`.
226
+ * Entries are comma-separated OUTSIDE backticks (so a marker's own text
227
+ * never leaks a split point). Each entry is stripped of a trailing
228
+ * parenthetical annotation (`(title case)`, `(non-standard)`, ...) — that
229
+ * annotation is captured into `notes` rather than the marker text — then
230
+ * stripped of surrounding backticks and a leading `## ` heading prefix. A
231
+ * bare `No marker` entry (post-annotation-strip) contributes nothing to
232
+ * `markers`; its parenthetical still lands in `notes`.
233
+ */
234
+ function parseCompletionMarkersCell(value) {
235
+ const markers = [];
236
+ const notes = [];
237
+ const unconsumedMarkers = [];
238
+ if (!value) return { markers, notes, unconsumedMarkers };
239
+ const entries = splitOutsideDelimiter(value, ',');
240
+ for (const rawEntry of entries) {
241
+ const entry = rawEntry.trim();
242
+ if (entry === '') continue;
243
+ let markerPortion = entry;
244
+ let annotation = null;
245
+ const noteMatch = entry.match(/^(.*?)\s*\(([^)]*)\)\s*$/);
246
+ if (noteMatch) {
247
+ markerPortion = noteMatch[1].trim();
248
+ annotation = noteMatch[2].trim();
249
+ notes.push(annotation);
250
+ }
251
+ markerPortion = markerPortion.replace(/`/g, '').trim();
252
+ markerPortion = markerPortion.replace(/^#{1,6}\s+/, '').trim();
253
+ if (markerPortion === '' || markerPortion === 'No marker') continue;
254
+ // An `(unconsumed: <reason>)` annotation declares a marker that is
255
+ // emitted deliberately but matched by no consumer (e.g. a user-facing
256
+ // presentation format). It stays in the declared+emitted contract and in
257
+ // case-collision scope; only the consumer requirement is waived.
258
+ if (annotation !== null && /^unconsumed\b/i.test(annotation)) {
259
+ unconsumedMarkers.push(markerPortion);
260
+ continue;
261
+ }
262
+ markers.push(markerPortion);
263
+ }
264
+ return { markers, notes, unconsumedMarkers };
265
+ }
266
+
267
+ /**
268
+ * extractMarkers(content, knownMarkers)
269
+ *
270
+ * Scans a markdown string line by line for H1-H6 headings, and reports
271
+ * whether each heading sits inside a fenced code block. A heading counts as
272
+ * a marker only when its captured text EXACTLY matches (case-sensitively)
273
+ * one of `knownMarkers` — the declared vocabulary from the agent-contracts
274
+ * registry, supplied by the caller. There is deliberately no shape
275
+ * heuristic (e.g. "ends in COMPLETE") here: the registry already declares
276
+ * every marker string, several of which do NOT end in COMPLETE (`##
277
+ * ISSUES FOUND`, `## ROADMAP CREATED`, `## CHECKPOINT REACHED`, `##
278
+ * ESCALATE`, ...), so guessing the shape both over-matches ordinary prose
279
+ * headings that happen to look marker-shaped and under-matches every real
280
+ * marker that doesn't end in the word COMPLETE. When `knownMarkers` is
281
+ * omitted or empty, `markers` is always `[]` — this function never falls
282
+ * back to guessing.
283
+ *
284
+ * `candidates` separately reports in-fence headings that are NOT in
285
+ * `knownMarkers` but still look marker-shaped (`/^[A-Z][A-Z0-9 _:'\/-]*$/`,
286
+ * no backticks, <= 60 chars) — these feed the `emitted_marker_not_declared`
287
+ * check. They are reported as candidates, never silently treated as
288
+ * markers.
289
+ *
290
+ * Fence tracking is a single boolean, not a stack: a line whose trimmed
291
+ * form starts with a run of 3+ backticks or tildes toggles it, but only a
292
+ * closing run of the SAME character and AT LEAST the same length actually
293
+ * closes an open fence — a shorter or differently-charred run nested inside
294
+ * (e.g. a 3-backtick span shown inside a 4-backtick outer fence) is inert
295
+ * and leaves the outer fence open. `unclosedFence` reports when the file
296
+ * ends without ever closing the last-opened fence, so callers can tell a
297
+ * truncated/malformed file from one that's actually well-formed.
298
+ */
299
+ function extractMarkers(content, knownMarkers) {
300
+ const markers = [];
301
+ const candidates = [];
302
+ if (content == null || typeof content !== 'string' || content.trim() === '') {
303
+ return { markers, candidates, unclosedFence: false };
304
+ }
305
+
306
+ const known = new Set(knownMarkers || []);
307
+ const lines = content.split(/\r?\n/);
308
+ const headingRe = /^(#{1,6})\s+(\S.*?)\s*$/;
309
+ const candidateShapeRe = /^[A-Z][A-Z0-9 _:'/-]*$/;
310
+
311
+ let inFence = false;
312
+ let fenceChar = null;
313
+ let fenceLen = 0;
314
+
315
+ for (let i = 0; i < lines.length; i++) {
316
+ const line = lines[i];
317
+ const trimmed = line.trim();
318
+ const stateForThisLine = inFence;
319
+
320
+ const fenceMatch = trimmed.match(/^(`{3,}|~{3,})/);
321
+ if (fenceMatch) {
322
+ const runChar = fenceMatch[1][0];
323
+ const runLen = fenceMatch[1].length;
324
+ if (!inFence) {
325
+ inFence = true;
326
+ fenceChar = runChar;
327
+ fenceLen = runLen;
328
+ } else if (runChar === fenceChar && runLen >= fenceLen) {
329
+ inFence = false;
330
+ fenceChar = null;
331
+ fenceLen = 0;
332
+ }
333
+ // else: nested/mismatched run — does not close the outer fence.
334
+ }
335
+
336
+ const hm = line.match(headingRe);
337
+ if (hm) {
338
+ const text = hm[2].trim();
339
+ if (known.has(text)) {
340
+ markers.push({ marker: text, inFence: stateForThisLine, line: i + 1 });
341
+ } else if (stateForThisLine && text.length <= 60 && candidateShapeRe.test(text)) {
342
+ candidates.push({ marker: text, inFence: stateForThisLine, line: i + 1 });
343
+ }
344
+ }
345
+ }
346
+
347
+ return { markers, candidates, unclosedFence: inFence };
348
+ }
349
+
350
+ /**
351
+ * parseAgentContracts(markdown)
352
+ *
353
+ * Parses the `## Agent Registry` pipe table in
354
+ * gsd-core/references/agent-contracts.md into structured rows, keyed by
355
+ * lowercased, underscore-joined column headers (`Completion Markers` ->
356
+ * `completion_markers`). Column set is read from the header row itself, so
357
+ * the table can grow a `Consumed By` / `Kind` column later without this
358
+ * parser needing an update — rows simply gain the corresponding key.
359
+ *
360
+ * The `completion_markers` column gets special handling via
361
+ * parseCompletionMarkersCell: it is never a plain string on the returned
362
+ * row, always the parsed `{markers}` array (with any parenthetical
363
+ * annotation split out into a row-level `notes` array instead).
364
+ *
365
+ * Malformed rows (wrong cell count for the header, or an empty agent name)
366
+ * are reported in `errors` as `{line, reason}` and excluded from `rows`
367
+ * rather than thrown.
368
+ */
369
+ function parseAgentContracts(markdown) {
370
+ const rows = [];
371
+ const errors = [];
372
+ if (markdown == null || typeof markdown !== 'string') return { rows, errors };
373
+
374
+ const lines = markdown.split(/\r?\n/);
375
+ const registryIdx = lines.findIndex(l => /^##\s+Agent Registry\s*$/.test(l.trim()));
376
+ if (registryIdx === -1) return { rows, errors };
377
+
378
+ let i = registryIdx + 1;
379
+ while (i < lines.length && lines[i].trim() === '') i++;
380
+ if (i >= lines.length || !lines[i].trim().startsWith('|')) return { rows, errors };
381
+
382
+ const columnKeys = splitTableRow(lines[i]).map(h =>
383
+ h.trim().toLowerCase().replace(/\s+/g, '_')
384
+ );
385
+ i++;
386
+ if (i < lines.length && isSeparatorRow(lines[i])) i++;
387
+
388
+ for (; i < lines.length; i++) {
389
+ const raw = lines[i];
390
+ if (!raw.trim().startsWith('|')) break;
391
+
392
+ const lineNum = i + 1;
393
+ const cells = splitTableRow(raw);
394
+ if (cells.length !== columnKeys.length) {
395
+ errors.push({
396
+ line: lineNum,
397
+ reason: `column count mismatch: expected ${columnKeys.length}, got ${cells.length}`,
398
+ });
399
+ continue;
400
+ }
401
+
402
+ const row = {};
403
+ for (let j = 0; j < columnKeys.length; j++) {
404
+ const key = columnKeys[j];
405
+ const value = cells[j];
406
+ if (key === 'completion_markers') {
407
+ const parsed = parseCompletionMarkersCell(value);
408
+ row.completion_markers = parsed.markers;
409
+ row.notes = parsed.notes;
410
+ row.unconsumed_markers = parsed.unconsumedMarkers;
411
+ } else {
412
+ row[key] = value;
413
+ }
414
+ }
415
+
416
+ if (!row.agent) {
417
+ errors.push({ line: lineNum, reason: 'empty agent name' });
418
+ continue;
419
+ }
420
+
421
+ rows.push(row);
422
+ }
423
+
424
+ return { rows, errors };
425
+ }
426
+
427
+ const CONTRACT_KINDS = new Set(['sentinel-match', 'artifact+query', 'structured-return']);
428
+
429
+ /**
430
+ * contractViolations({registry, producerMarkers, candidateMarkers, consumerTexts})
431
+ *
432
+ * Cross-checks the agent-contracts registry (from parseAgentContracts)
433
+ * against what agent files actually emit in-fence (from extractMarkers) and
434
+ * what every workflow/command/agent file actually consumes, exact-case.
435
+ * Rows are evaluated independently; `registry`, `producerMarkers`,
436
+ * `candidateMarkers`, and `consumerTexts` default to empty when omitted so a
437
+ * partial call never throws.
438
+ *
439
+ * `emitted_marker_not_declared` is driven by `candidateMarkers` (agent ->
440
+ * [in-fence heading text]) rather than `producerMarkers` -- a heading only
441
+ * lands in `producerMarkers` once it already matches a KNOWN registry
442
+ * marker (see extractMarkers), so it can never be "emitted but undeclared";
443
+ * `candidateMarkers` is exactly the set of marker-shaped in-fence headings
444
+ * extractMarkers could NOT resolve against the known vocabulary.
445
+ *
446
+ * A row declaring `kind: 'artifact+query'` or `'structured-return'` opts
447
+ * out of the sentinel-marker contract entirely — its only possible
448
+ * violation is `vestigial_marker` (it still emits an in-fence marker
449
+ * despite the row saying completion isn't detected that way). Every other
450
+ * row (kind `sentinel-match`, or the column simply absent/empty) is held to
451
+ * the full contract: declared markers must be emitted, emitted markers must
452
+ * be declared, and — for sentinel-match rows specifically — every declared
453
+ * marker must have at least one EXACT-CASE consumer somewhere in
454
+ * `consumerTexts`, excluding the producing agent's own file (an agent can
455
+ * never satisfy its own marker, even when the same map entry also serves as
456
+ * a consumer of some OTHER agent's marker). A case-insensitive-only match
457
+ * is reported (`case_only_match`), never treated as satisfying the
458
+ * contract. `case_collision` is computed once across the whole registry,
459
+ * independent of kind, since two markers differing only by case is a
460
+ * registry-wide authoring hazard.
461
+ */
462
+ function contractViolations({ registry, producerMarkers, candidateMarkers, consumerTexts } = {}) {
463
+ const violations = [];
464
+ try {
465
+ const rows = Array.isArray(registry) ? registry : [];
466
+ const producers = producerMarkers instanceof Map ? producerMarkers : new Map();
467
+ const candidatesByAgent = candidateMarkers instanceof Map ? candidateMarkers : new Map();
468
+ const consumers = consumerTexts instanceof Map ? consumerTexts : new Map();
469
+
470
+ // case_collision — global, across every declared marker in the registry,
471
+ // including `(unconsumed: …)` entries: a case-variant of an unconsumed
472
+ // marker is still an authoring hazard even though no consumer matches it.
473
+ const byLower = new Map();
474
+ for (const row of rows) {
475
+ for (const m of [...(row.completion_markers || []), ...(row.unconsumed_markers || [])]) {
476
+ const lower = m.toLowerCase();
477
+ if (!byLower.has(lower)) byLower.set(lower, new Set());
478
+ byLower.get(lower).add(m);
479
+ }
480
+ }
481
+ for (const row of rows) {
482
+ for (const m of [...(row.completion_markers || []), ...(row.unconsumed_markers || [])]) {
483
+ const variants = byLower.get(m.toLowerCase());
484
+ if (variants && variants.size > 1) {
485
+ const others = [...variants].filter(v => v !== m);
486
+ violations.push({
487
+ kind: 'case_collision',
488
+ agent: row.agent,
489
+ marker: m,
490
+ detail: `case-insensitive collision with: ${others.join(', ')}`,
491
+ });
492
+ }
493
+ }
494
+ }
495
+
496
+ // roster coverage — the registry is the declaration of record for every
497
+ // agents/*.md file: an agent with no row, two rows for one agent, or a
498
+ // row naming an agent that does not exist are all contract drift. The
499
+ // roster is the producer map's key set; guards on map size keep empty
500
+ // inputs inert rather than spuriously "missing".
501
+ const seenAgents = new Set();
502
+ for (const row of rows) {
503
+ const agent = row.agent;
504
+ if (seenAgents.has(agent)) {
505
+ violations.push({
506
+ kind: 'duplicate_registry_row',
507
+ agent,
508
+ marker: null,
509
+ detail: `agent "${agent}" has more than one registry row`,
510
+ });
511
+ }
512
+ seenAgents.add(agent);
513
+ if (producers.size > 0 && !producers.has(agent)) {
514
+ violations.push({
515
+ kind: 'unknown_producer',
516
+ agent,
517
+ marker: null,
518
+ detail: `registry row for "${agent}" but no agents/${agent}.md exists`,
519
+ });
520
+ }
521
+ }
522
+ for (const agent of producers.keys()) {
523
+ if (!seenAgents.has(agent)) {
524
+ violations.push({
525
+ kind: 'agent_without_contract',
526
+ agent,
527
+ marker: null,
528
+ detail: `agents/${agent}.md exists but has no registry row in agent-contracts.md`,
529
+ });
530
+ }
531
+ }
532
+
533
+ for (const row of rows) {
534
+ const agent = row.agent;
535
+ const declared = row.completion_markers || [];
536
+ const unconsumed = row.unconsumed_markers || [];
537
+ const declaredAll = [...declared, ...unconsumed];
538
+ // Deduped: the same marker emitted on several lines/files is one
539
+ // emission fact, never several violations.
540
+ const produced = [...new Set(producers.get(agent) || [])];
541
+ const kind = row.kind;
542
+ const kindProvided = kind !== undefined && kind !== null && kind !== '';
543
+
544
+ // A row naming a nonexistent agent file: unknown_producer (above) is
545
+ // the violation; per-marker noise on top of it hides nothing.
546
+ if (producers.size > 0 && !producers.has(agent)) continue;
547
+
548
+ // unknown_consumer — the `Consumed by` column feeds readTagViolations,
549
+ // so a file-shaped entry that resolves to nothing would make that arm a
550
+ // lint over fiction. Only checked when a consumer corpus was supplied.
551
+ for (const p of parseConsumedByCell(row.consumed_by)) {
552
+ if (consumers.size > 0 && !consumers.has(p)) {
553
+ violations.push({
554
+ kind: 'unknown_consumer',
555
+ agent,
556
+ marker: null,
557
+ detail: `Consumed by names "${p}" but no such file exists`,
558
+ });
559
+ }
560
+ }
561
+
562
+ if (kindProvided && !CONTRACT_KINDS.has(kind)) {
563
+ violations.push({
564
+ kind: 'unknown_kind',
565
+ agent,
566
+ marker: null,
567
+ detail: `row declares unknown kind "${kind}"`,
568
+ });
569
+ }
570
+
571
+ const isArtifactOrStructured =
572
+ kindProvided && (kind === 'artifact+query' || kind === 'structured-return');
573
+
574
+ // declaredAll includes unconsumed entries: an `(unconsumed: …)` marker
575
+ // must still be emitted — the exemption waives the consumer
576
+ // requirement only, never the declared↔emitted agreement (Marker
577
+ // Rule 7). This runs for EVERY row kind.
578
+ for (const m of declaredAll) {
579
+ if (!produced.includes(m)) {
580
+ violations.push({
581
+ kind: 'declared_marker_not_emitted',
582
+ agent,
583
+ marker: m,
584
+ detail: `declared in registry but not emitted in-fence by ${agent}`,
585
+ });
586
+ }
587
+ }
588
+
589
+ if (isArtifactOrStructured) {
590
+ // An `(unconsumed: …)` annotation on an artifact/structured row is a
591
+ // recorded decision to keep emitting a marker the completion route
592
+ // doesn't need (Marker Rule 2's intentional title-case markers) —
593
+ // exempt from vestigial_marker. Every OTHER emitted marker on such a
594
+ // row is reported ONCE, as vestigial_marker: the candidate check
595
+ // below is skipped for these rows because emitted_marker_not_declared
596
+ // on top of vestigial_marker double-reports the same fact, and the
597
+ // consumer contract cannot apply to a row whose completion is not
598
+ // detected by marker matching at all.
599
+ const unconsumedSet = new Set(unconsumed);
600
+ for (const m of produced) {
601
+ if (unconsumedSet.has(m)) continue;
602
+ violations.push({
603
+ kind: 'vestigial_marker',
604
+ agent,
605
+ marker: m,
606
+ detail: `row kind "${kind}" expects no marker, but agent still emits "${m}" in-fence`,
607
+ });
608
+ }
609
+ continue;
610
+ }
611
+
612
+ // Deduped per marker: "emitted but undeclared" is a fact about the
613
+ // marker, and a caller-supplied list carrying the same marker twice
614
+ // (two template lines, or agent file + @-included reference) is one
615
+ // violation, not two.
616
+ const candidates = [...new Set(candidatesByAgent.get(agent) || [])];
617
+ for (const m of candidates) {
618
+ if (!declaredAll.includes(m)) {
619
+ violations.push({
620
+ kind: 'emitted_marker_not_declared',
621
+ agent,
622
+ marker: m,
623
+ detail: `emitted in-fence by ${agent} but absent from its registry row`,
624
+ });
625
+ }
626
+ }
627
+
628
+ const isSentinelMatchKind = !kindProvided || kind === 'sentinel-match';
629
+ if (isSentinelMatchKind) {
630
+ // Only markers the agent actually emits participate in the consumer
631
+ // contract — a declared_marker_not_emitted finding already covers a
632
+ // phantom declaration, and stacking no_consumer on top of it is
633
+ // double-reporting the same dead row.
634
+ const declaredConsumerPaths = parseConsumedByCell(row.consumed_by);
635
+ for (const m of declared) {
636
+ if (!produced.includes(m)) continue;
637
+ let exactMatch = false;
638
+ let caseInsensitiveMatch = false;
639
+ let declaredConsumerMatch = false;
640
+ for (const [consumerId, text] of consumers.entries()) {
641
+ // consumerTexts is keyed by repo-relative path — an agent's own
642
+ // file is 'agents/<name>.md'. The producer's own template can
643
+ // never satisfy its marker.
644
+ if (consumerId === `agents/${agent}.md`) continue;
645
+ if (typeof text !== 'string') continue;
646
+ if (text.includes(m)) {
647
+ exactMatch = true;
648
+ if (declaredConsumerPaths.includes(consumerId)) declaredConsumerMatch = true;
649
+ } else if (text.toLowerCase().includes(m.toLowerCase())) {
650
+ caseInsensitiveMatch = true;
651
+ }
652
+ }
653
+ if (!exactMatch) {
654
+ if (caseInsensitiveMatch) {
655
+ violations.push({
656
+ kind: 'case_only_match',
657
+ agent,
658
+ marker: m,
659
+ detail: `no exact-case consumer for "${m}"; case-insensitive match found only`,
660
+ });
661
+ } else {
662
+ violations.push({
663
+ kind: 'no_consumer',
664
+ agent,
665
+ marker: m,
666
+ detail: `no consumer text contains "${m}" (exact case)`,
667
+ });
668
+ }
669
+ } else if (
670
+ declaredConsumerPaths.length > 0 &&
671
+ !declaredConsumerMatch
672
+ ) {
673
+ // The marker IS consumed somewhere, but by none of the consumers
674
+ // the row cites — the `Consumed by` cell is documentation the
675
+ // read-tag arm and humans both navigate by, so a cell naming
676
+ // files that never match is registry drift even when the corpus
677
+ // at large happens to contain the token.
678
+ violations.push({
679
+ kind: 'declared_consumer_no_match',
680
+ agent,
681
+ marker: m,
682
+ detail: `consumed somewhere, but none of the row's Consumed by entries ( ${declaredConsumerPaths.join(', ')} ) contain "${m}"`,
683
+ });
684
+ }
685
+ }
686
+ }
687
+ }
688
+ } catch (e) {
689
+ // A drift gate must fail RED, never green: an internal error here must
690
+ // surface, not be swallowed into '0 violations'.
691
+ e.message = `contractViolations (internal failure — failing red): ${e.message}`;
692
+ throw e;
693
+ }
694
+ return violations;
695
+ }
696
+
697
+ /**
698
+ * parseConsumedByCell(value)
699
+ *
700
+ * Extracts every file-shaped path from a `Consumed by` table cell. Cell
701
+ * entries are comma-separated outside backticks, and an entry may carry
702
+ * several backtick-wrapped tokens (a path, a command, a marker string) in
703
+ * prose — so every backtick span is inspected and only tokens that look like
704
+ * repo-relative markdown paths (`^[\w][\w./-]*\.md$` — no spaces, no glob
705
+ * stars) are returned. A glob (`*-VERIFICATION.md`) or a command
706
+ * (`gsd_run query verification.status`) describes the consumption mechanism
707
+ * in prose; it is not a file the check can open, and is ignored.
708
+ */
709
+ function parseConsumedByCell(value) {
710
+ if (!value || typeof value !== 'string') return [];
711
+ const paths = [];
712
+ const re = /`([^`]+)`/g;
713
+ let m;
714
+ while ((m = re.exec(value)) !== null) {
715
+ const token = m[1].trim();
716
+ if (!/^[\w][\w./-]*\.md$/.test(token) || token.includes('*')) continue;
717
+ // Traversal segments are dropped, never surfaced — the same rule
718
+ // workflowPathRefs applies to its refs: this resolver only ever reports
719
+ // paths inside the repo, never something a `..` could walk outside of
720
+ // (the driver reads these paths under --root, so an interior `..` would
721
+ // be an out-of-root file read).
722
+ if (token.split('/').includes('..')) continue;
723
+ paths.push(token);
724
+ }
725
+ return paths;
726
+ }
727
+
728
+ /**
729
+ * readTagViolations({registry, agentTexts, consumerTexts})
730
+ *
731
+ * The read-tag arm — F8 one layer up. Two invariants:
732
+ *
733
+ * legacy_read_tag — no `<files_to_read>` may survive anywhere in the
734
+ * consumer corpus (workflows/, commands/, agents/). #3423 standardized on
735
+ * `<required_reading>` and retired the old vocabulary via a test; this
736
+ * folds that assertion into the gate itself.
737
+ *
738
+ * read_tag_gate_missing — for every registry row, every file-shaped
739
+ * `Consumed by` consumer that itself emits `<required_reading>` requires
740
+ * the producing agent's file to reference `<required_reading>`. A spawner
741
+ * that sends a required-reading block to an agent whose instructions never
742
+ * mention the gate is exactly the F8 shape: the emit side is correct, the
743
+ * gate side never fires — and #3423's per-agent test cannot see it,
744
+ * because an agent carrying no reading instruction at all is skipped by
745
+ * it. The registry supplies the producer↔consumer pair, so this check
746
+ * needs no spawn-site parsing heuristics.
747
+ */
748
+ function readTagViolations({ registry, agentTexts, consumerTexts } = {}) {
749
+ const violations = [];
750
+ try {
751
+ const rows = Array.isArray(registry) ? registry : [];
752
+ const agents = agentTexts instanceof Map ? agentTexts : new Map();
753
+ const consumers = consumerTexts instanceof Map ? consumerTexts : new Map();
754
+
755
+ for (const [file, text] of consumers.entries()) {
756
+ if (typeof text === 'string' && text.includes('<files_to_read>')) {
757
+ violations.push({
758
+ kind: 'legacy_read_tag',
759
+ agent: null,
760
+ marker: null,
761
+ detail: `${file} still contains the retired <files_to_read> tag`,
762
+ });
763
+ }
764
+ }
765
+
766
+ for (const row of rows) {
767
+ const agent = row.agent;
768
+ const agentText = agents.get(agent);
769
+ if (typeof agentText !== 'string') continue; // unknown_producer reports this
770
+ if (agentText.includes('<required_reading>')) continue;
771
+ for (const p of parseConsumedByCell(row.consumed_by)) {
772
+ const text = consumers.get(p);
773
+ if (typeof text === 'string' && text.includes('<required_reading>')) {
774
+ violations.push({
775
+ kind: 'read_tag_gate_missing',
776
+ agent,
777
+ marker: null,
778
+ detail: `consumer ${p} emits <required_reading> but agents/${agent}.md never references the gate`,
779
+ });
780
+ break; // one violation per agent is the fact; more is noise
781
+ }
782
+ }
783
+ }
784
+ } catch (e) {
785
+ // A drift gate must fail RED, never green: an internal error here must
786
+ // surface, not be swallowed into '0 violations'.
787
+ e.message = `readTagViolations (internal failure — failing red): ${e.message}`;
788
+ throw e;
789
+ }
790
+ return violations;
791
+ }
792
+
793
+ /**
794
+ * Tokens that look like completion markers but are NOT agent contracts, and
795
+ * therefore exempt from the reverse-direction check. Each entry carries its
796
+ * justification inline; a test locks the set's membership so it cannot grow
797
+ * silently.
798
+ */
799
+ const NON_AGENT_TOKENS = new Set([
800
+ // Display headings the /gsd:graphify command itself renders from
801
+ // `gsd_run graphify build` CLI output — tool output shown to the user,
802
+ // not an agent return any spawner dispatches on.
803
+ 'GRAPHIFY BUILD COMPLETE',
804
+ 'GRAPHIFY BUILD FAILED',
805
+ ]);
806
+
807
+ /**
808
+ * unmatchedConsumerTokens({consumerTexts, vocabulary})
809
+ *
810
+ * The reverse direction of the registry contract: a consumer that matches a
811
+ * `## TOKEN` string no producer emits is dispatch-on-phantom — F9's shape
812
+ * (a sanctioned return that no dispatch branch ever keyed on), seen from
813
+ * the consumer side. Scans ONLY workflow/command files (the dispatch
814
+ * surfaces); `agents/` is excluded because an agent mentioning a token in
815
+ * its own file is a self-description, not a spawner's match instruction —
816
+ * the forward direction already validates agent-to-agent consumption.
817
+ *
818
+ * A token is extracted when it appears quoted (`## X`, "## X", '## X') —
819
+ * the shape a match/dispatch instruction actually writes — is ALL-CAPS
820
+ * marker-shaped (the same candidate shape extractMarkers uses, ≤60 chars),
821
+ * is absent from the declared vocabulary, and is not exempt via
822
+ * NON_AGENT_TOKENS.
823
+ */
824
+ function unmatchedConsumerTokens({ consumerTexts, vocabulary } = {}) {
825
+ const violations = [];
826
+ try {
827
+ const consumers = consumerTexts instanceof Map ? consumerTexts : new Map();
828
+ const known = vocabulary instanceof Set ? vocabulary : new Set(knownMarkersArray(vocabulary));
829
+ const shapeRe = /^[A-Z][A-Z0-9 _:'/-]*$/;
830
+ const tokenRe = /[`'"]## ([^`'"\n]+)[`'"]/g;
831
+
832
+ const reported = new Set();
833
+ for (const [file, text] of consumers.entries()) {
834
+ if (!file.startsWith('gsd-core/workflows/') && !file.startsWith('commands/')) continue;
835
+ if (typeof text !== 'string') continue;
836
+ tokenRe.lastIndex = 0;
837
+ let m;
838
+ while ((m = tokenRe.exec(text)) !== null) {
839
+ const token = m[1].trim();
840
+ if (known.has(token)) continue;
841
+ if (NON_AGENT_TOKENS.has(token)) continue;
842
+ if (token.length > 60 || !shapeRe.test(token)) continue;
843
+ const key = `${file}\0${token}`;
844
+ if (reported.has(key)) continue;
845
+ reported.add(key);
846
+ violations.push({
847
+ kind: 'unmatched_consumer_token',
848
+ agent: null,
849
+ marker: token,
850
+ detail: `${file} matches \`## ${token}\` but no agent declares or emits it`,
851
+ });
852
+ }
853
+ }
854
+ } catch (e) {
855
+ // A drift gate must fail RED, never green: an internal error here must
856
+ // surface, not be swallowed into '0 violations'.
857
+ e.message = `unmatchedConsumerTokens (internal failure — failing red): ${e.message}`;
858
+ throw e;
859
+ }
860
+ return violations;
861
+ }
862
+
863
+ function knownMarkersArray(vocabulary) {
864
+ // vocabulary may arrive as an array (tests) — normalize; the Set branch
865
+ // above handles the driver's Set.
866
+ return Array.isArray(vocabulary) ? vocabulary : [];
867
+ }
868
+
869
+ /**
870
+ * The frozen violation-kind vocabulary — one entry per finding the check
871
+ * can emit. Tests lock this set and the driver's REMEDIES parity, so a new
872
+ * kind is three coordinated changes: enum + remedy + test, the same
873
+ * discipline verify-reapply-patches' REASON enum follows.
874
+ */
875
+ const VIOLATION_KINDS = Object.freeze({
876
+ PARSE_ERROR: 'parse_error',
877
+ UNCLOSED_FENCE: 'unclosed_fence',
878
+ DECLARED_MARKER_NOT_EMITTED: 'declared_marker_not_emitted',
879
+ EMITTED_MARKER_NOT_DECLARED: 'emitted_marker_not_declared',
880
+ CASE_COLLISION: 'case_collision',
881
+ CASE_ONLY_MATCH: 'case_only_match',
882
+ NO_CONSUMER: 'no_consumer',
883
+ DECLARED_CONSUMER_NO_MATCH: 'declared_consumer_no_match',
884
+ UNKNOWN_KIND: 'unknown_kind',
885
+ VESTIGIAL_MARKER: 'vestigial_marker',
886
+ AGENT_WITHOUT_CONTRACT: 'agent_without_contract',
887
+ DUPLICATE_REGISTRY_ROW: 'duplicate_registry_row',
888
+ UNKNOWN_PRODUCER: 'unknown_producer',
889
+ UNKNOWN_CONSUMER: 'unknown_consumer',
890
+ LEGACY_READ_TAG: 'legacy_read_tag',
891
+ READ_TAG_GATE_MISSING: 'read_tag_gate_missing',
892
+ UNMATCHED_CONSUMER_TOKEN: 'unmatched_consumer_token',
893
+ });
894
+
895
+ /**
896
+ * The remedy shown per violation kind — one entry per VIOLATION_KINDS value.
897
+ * The parity test locks REMEDIES keys against VIOLATION_KINDS values, so a
898
+ * new kind cannot ship without its remedy.
899
+ */
900
+ const REMEDIES = Object.freeze({
901
+ parse_error: 'fix the malformed row in gsd-core/references/agent-contracts.md',
902
+ unclosed_fence: 'close the unterminated code fence in this agent file',
903
+ declared_marker_not_emitted:
904
+ 'remove the marker from the registry row, or emit it as an in-fence example in the agent file',
905
+ emitted_marker_not_declared:
906
+ "add this marker to the agent's Completion Markers cell in the registry table",
907
+ case_collision: 'rename one of the colliding markers so they no longer differ only by case',
908
+ case_only_match:
909
+ 'fix the consumer file to match this marker exact-case, or update the marker/registry to match the consumer',
910
+ no_consumer:
911
+ 'add an exact-case consumer for this marker, or reclassify the row Kind to artifact+query/structured-return',
912
+ declared_consumer_no_match:
913
+ "the marker is consumed, but not by any file the row's Consumed by cell names — fix the cell to name a real consumer",
914
+ unknown_kind: 'set the Kind column to one of sentinel-match, artifact+query, structured-return',
915
+ vestigial_marker:
916
+ "remove this marker heading from the agent file, since the row's Kind says no marker is expected",
917
+ agent_without_contract: 'add a registry row for this agent in agent-contracts.md',
918
+ duplicate_registry_row: 'merge the two rows for this agent into one',
919
+ unknown_producer: 'remove the row, or create the agents/<name>.md file it names',
920
+ unknown_consumer:
921
+ 'fix the path in the Consumed by cell, or drop it — only real files the check can open belong there',
922
+ legacy_read_tag:
923
+ 'replace <files_to_read> with <required_reading> (#3423 standardized on the gate tag)',
924
+ read_tag_gate_missing:
925
+ "add the <required_reading> MUST-Read gate clause to this agent's instructions (see docs/AGENTS.md)",
926
+ unmatched_consumer_token:
927
+ 'declare the producing agent and marker in the registry, or remove the dispatch — nothing emits this token',
928
+ });
929
+
930
+ /**
931
+ * sanitizeEcho(text)
932
+ *
933
+ * File-derived strings (registry cells, marker tokens, workflow text) are
934
+ * echoed into lint output that AI agents read as trusted instructions —
935
+ * every interpolated field is control-char-stripped and length-capped
936
+ * before it is written, so a malicious repo file cannot make the lint emit
937
+ * instruction-shaped text. Implemented as a code-point filter rather than a
938
+ * regex so no control-character regex literal exists to lint.
939
+ */
940
+ function sanitizeEcho(text) {
941
+ let out = '';
942
+ for (const ch of String(text)) {
943
+ const code = ch.codePointAt(0);
944
+ if (code <= 0x1f || code === 0x7f) continue;
945
+ out += ch;
946
+ }
947
+ return out.length > 200 ? out.slice(0, 200) : out;
948
+ }
949
+
950
+ module.exports = {
951
+ CANONICAL_TOOLS,
952
+ parseFrontmatter,
953
+ executionContextRefs,
954
+ workflowPathRefs,
955
+ unreachableWorkflows,
956
+ extractMarkers,
957
+ parseAgentContracts,
958
+ contractViolations,
959
+ parseConsumedByCell,
960
+ readTagViolations,
961
+ unmatchedConsumerTokens,
962
+ NON_AGENT_TOKENS,
963
+ VIOLATION_KINDS,
964
+ REMEDIES,
965
+ sanitizeEcho,
966
+ };