@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
@@ -210,10 +210,35 @@ const SHA512_INTEGRITY_RE = /^sha512-[A-Za-z0-9+/]{86}==$/;
210
210
  // hard validation error — fail closed so the capability install/load is rejected loudly.
211
211
  const SAFE_HOOK_SCRIPT_RE = /^[A-Za-z0-9._/-]+$/;
212
212
 
213
+ // #3631 (defense-in-depth, mirrors capability-lifecycle.cts — KEEP BOTH IN SYNC): a declared script
214
+ // path must not point into the space bundleContentHash (capability-consent.cts) excludes from the
215
+ // consent-binding digest. A file whose basename ends `.pyc`/`.pyo` can contain perfectly valid
216
+ // JavaScript and would be executed by `node` regardless of extension, and a `__pycache__`/
217
+ // `.pytest_cache` segment marks a directory whose digest marker is suppressed — so a MANIFEST-DECLARED
218
+ // executable surface must never be able to reach either, or the exclusion becomes reachable from a
219
+ // path an attacker fully controls at declare-time rather than only via post-consent tamper.
220
+ // Regex asymmetry is DELIBERATE, KEEP BOTH RULES IN SYNC WITH capability-lifecycle.cts (byte-identical
221
+ // text, verified by the isSafeHookScriptPath parity test in tests/capability-registry.test.cjs):
222
+ // (i) PYCACHE_SUFFIX_RE is case-INSENSITIVE (`/i`) on purpose — a validator should be STRICTER than
223
+ // the digest it defends, so it rejects `x.PYC` too even though bundleContentHash's own suffix
224
+ // match (hasPycacheFileSuffix, capability-consent.cts) is byte-exact and would still hash it.
225
+ // (ii) The __pycache__/.pytest_cache SEGMENT match is case-SENSITIVE to match the digest's own
226
+ // byte-exact, case-sensitive directory-basename comparison (CPython always writes a lowercase
227
+ // `__pycache__`) — a validator segment match looser than the digest here would reject paths the
228
+ // digest would still hash, which is over-strict in the wrong direction for a defense-in-depth
229
+ // check layered on top of an already-correct digest.
230
+ // (iii) The `[/\\]` backslash alternations in both regexes are defensive/UNREACHABLE in practice:
231
+ // SAFE_HOOK_SCRIPT_RE (above) already rejects any backslash character outright, so a script
232
+ // string containing `\` never reaches either PYCACHE_*_RE check.
233
+ const PYCACHE_SEGMENT_RE = /(?:^|[/\\])(__pycache__|\.pytest_cache)(?:[/\\]|$)/;
234
+ const PYCACHE_SUFFIX_RE = /\.(pyc|pyo)$/i;
235
+
213
236
  /**
214
237
  * #1460 (R): true when a relative hook-script path is shell-safe (see SAFE_HOOK_SCRIPT_RE).
215
238
  * Rejects absolute paths, `..` segments, a leading `-` on any path segment, and any char
216
- * outside the allowlist (whitespace / shell metacharacters / control / NUL).
239
+ * outside the allowlist (whitespace / shell metacharacters / control / NUL). #3631: also rejects a
240
+ * path with a `__pycache__`/`.pytest_cache` segment or a `.pyc`/`.pyo` basename suffix (defense in
241
+ * depth — keeps a declared executable surface out of the digest-excluded space).
217
242
  */
218
243
  function isSafeHookScriptPath(script) {
219
244
  if (typeof script !== 'string' || script.length === 0) return false;
@@ -225,6 +250,8 @@ function isSafeHookScriptPath(script) {
225
250
  for (const seg of segments) {
226
251
  if (seg.startsWith('-')) return false;
227
252
  }
253
+ if (PYCACHE_SEGMENT_RE.test(script)) return false;
254
+ if (PYCACHE_SUFFIX_RE.test(path.basename(script))) return false;
228
255
  return true;
229
256
  }
230
257
 
@@ -369,6 +396,11 @@ function validateCapability(cap, folderId) {
369
396
  errors.push(...validateRuntimeBody(cap));
370
397
  // A host that is ALSO a reviewer keeps exactly one manifest (ADR-2782 D1).
371
398
  errors.push(...validateReviewerBody(cap));
399
+ // ADR-3646: a runtime capability installs a host CLI, it does not resolve
400
+ // task content — taskContentResolver is feature-only.
401
+ if (cap.taskContentResolver !== undefined) {
402
+ errors.push('role:runtime capability must not have a "taskContentResolver" body (feature-only field)');
403
+ }
372
404
  } else if (cap.role === 'reviewer') {
373
405
  // ADR-2782 D3 — a lane that is not an install target. No runtime body, no
374
406
  // install surface, no runtimeCompat (it surfaces through no host runtime).
@@ -691,6 +723,118 @@ function validateFeatureBody(cap) {
691
723
  }
692
724
  }
693
725
 
726
+ // ADR-3646: optional per-task external-tracker content-resolution seam.
727
+ errors.push(...validateTaskContentResolver(cap));
728
+
729
+ return errors;
730
+ }
731
+
732
+ /**
733
+ * ADR-3646 — validate an OPTIONAL `taskContentResolver` body on a `role:
734
+ * "feature"` capability. Absence is never an error (most manifests won't
735
+ * have one); presence is strictly validated.
736
+ *
737
+ * Wrapped in try/catch to degrade any unexpected throw (a hostile Proxy, a
738
+ * throwing getter, etc.) to a single validation error rather than crashing
739
+ * every consumer of loadRegistry, per the #1461 OVL-1 discipline that
740
+ * `validateReviewerBody` follows.
741
+ *
742
+ * @param {object} cap The parsed capability manifest.
743
+ * @returns {string[]} Array of error strings; empty = valid or absent.
744
+ */
745
+ function validateTaskContentResolver(cap) {
746
+ try {
747
+ return validateTaskContentResolverFields(cap);
748
+ } catch (err) {
749
+ return ['capability taskContentResolver body could not be validated: ' + safeErrorMessage(err)];
750
+ }
751
+ }
752
+
753
+ /**
754
+ * Upper bound for `taskContentResolver.invoke.timeoutMs`, specific to this
755
+ * field only. `isPositiveIntegerMs()` has no ceiling and stays that way — it
756
+ * is shared with the reviewer lane's `timeoutFloorMs` and probe `timeoutMs`,
757
+ * which may legitimately need a longer or unbounded value. Without a ceiling
758
+ * here, a manifest could declare `Number.MAX_SAFE_INTEGER` and let
759
+ * `resolve-content` hang near-indefinitely on a stuck/malicious resolver,
760
+ * defeating the feature's "bounded subprocess" design intent.
761
+ */
762
+ const TASK_CONTENT_RESOLVER_TIMEOUT_CEILING_MS = 120000;
763
+
764
+ function validateTaskContentResolverFields(cap) {
765
+ const errors = [];
766
+ if (typeof cap !== 'object' || cap === null || Array.isArray(cap)) return errors;
767
+
768
+ const tcr = cap.taskContentResolver;
769
+ if (tcr === undefined) return errors; // optional — never an error to omit
770
+
771
+ const ctx = 'capability "' + (typeof cap.id === 'string' ? cap.id : '(unknown)') + '"';
772
+
773
+ if (typeof tcr !== 'object' || tcr === null || Array.isArray(tcr)) {
774
+ const got = tcr === null ? 'null' : Array.isArray(tcr) ? 'array' : typeof tcr;
775
+ errors.push(
776
+ ctx + ' taskContentResolver must be an object (got: ' + got + '). ' +
777
+ 'Omit the key entirely to declare no resolver — an explicit null is not an omission.',
778
+ );
779
+ return errors; // cannot validate fields of a non-object
780
+ }
781
+
782
+ // ── trackerPrefix — same grammar as a capability id ─────────────────────
783
+ if (typeof tcr.trackerPrefix !== 'string' || tcr.trackerPrefix.length === 0 || !KEBAB_RE.test(tcr.trackerPrefix)) {
784
+ errors.push(
785
+ ctx + ' taskContentResolver.trackerPrefix must be a non-empty kebab-case string matching ' +
786
+ String(KEBAB_RE) + ' (got: ' + describeValue(tcr.trackerPrefix) + ')',
787
+ );
788
+ }
789
+
790
+ // ── invoke ────────────────────────────────────────────────────────────
791
+ const inv = tcr.invoke;
792
+ if (typeof inv !== 'object' || inv === null || Array.isArray(inv)) {
793
+ const got = inv === null ? 'null' : Array.isArray(inv) ? 'array' : typeof inv;
794
+ errors.push(ctx + ' taskContentResolver.invoke must be an object (got: ' + got + ')');
795
+ return errors; // cannot validate sub-fields of a non-object
796
+ }
797
+
798
+ if (typeof inv.binary !== 'string' || inv.binary.length === 0) {
799
+ errors.push(ctx + ' taskContentResolver.invoke.binary must be a non-empty string');
800
+ }
801
+
802
+ if (!Array.isArray(inv.args)) {
803
+ errors.push(ctx + ' taskContentResolver.invoke.args must be an array of strings');
804
+ } else {
805
+ let hasPlaceholder = false;
806
+ for (const a of inv.args) {
807
+ if (typeof a !== 'string') {
808
+ errors.push(ctx + ' taskContentResolver.invoke.args entries must be strings (got: ' + describeValue(a) + ')');
809
+ } else if (a === '{{id}}') {
810
+ hasPlaceholder = true;
811
+ }
812
+ }
813
+ if (!hasPlaceholder) {
814
+ errors.push(
815
+ ctx + ' taskContentResolver.invoke.args must contain a "{{id}}" placeholder — ' +
816
+ 'without it the resolved tracker id could never reach the resolver subprocess',
817
+ );
818
+ }
819
+ }
820
+
821
+ if (!isPositiveIntegerMs(inv.timeoutMs)) {
822
+ errors.push(
823
+ ctx + ' taskContentResolver.invoke.timeoutMs must be a positive integer of milliseconds — ' +
824
+ 'an unbounded resolver call could hang task execution indefinitely ' +
825
+ '(got: ' + describeValue(inv.timeoutMs) + ')',
826
+ );
827
+ } else if (inv.timeoutMs > TASK_CONTENT_RESOLVER_TIMEOUT_CEILING_MS) {
828
+ // Ceiling specific to this field — `isPositiveIntegerMs()` itself stays
829
+ // unbounded because it is shared with the reviewer lane's
830
+ // `timeoutFloorMs`/probe `timeoutMs`, which have no such ceiling.
831
+ errors.push(
832
+ ctx + ' taskContentResolver.invoke.timeoutMs must not exceed ' +
833
+ TASK_CONTENT_RESOLVER_TIMEOUT_CEILING_MS + 'ms — an unbounded-in-practice value defeats the ' +
834
+ '"bounded subprocess" design intent (got: ' + inv.timeoutMs + ')',
835
+ );
836
+ }
837
+
694
838
  return errors;
695
839
  }
696
840
 
@@ -727,6 +871,16 @@ const VALID_CONVERTER_NAMES = new Set([
727
871
  'convertClaudeAgentToCodexAgent',
728
872
  // ADR-1239 / #2092 Phase B Upgrade 1 — native .qwen/agents/*.md subagent projection.
729
873
  'convertClaudeAgentToQwenAgent',
874
+ // #3384 — ZCode agents are Claude-shaped but its dispatcher treats mcp__* tools
875
+ // grants as required MCP servers; this converter strips them at install time.
876
+ 'convertClaudeAgentToZcodeAgent',
877
+ // #2875 Part 2 (the agents-bypass closure) — data-driven Hermes branding
878
+ // converter (reads hostBehaviors.brandingRewrites rather than a hardcode),
879
+ // and the kilo/opencode agent converters (shared with those runtimes'
880
+ // commands-kind entries, options-bag signature `(content, {isAgent, modelOverride})`).
881
+ 'convertClaudeAgentToHermesAgent',
882
+ 'convertClaudeToKiloFrontmatter',
883
+ 'convertClaudeToOpencodeFrontmatter',
730
884
  ]);
731
885
 
732
886
  // C3: Validate role:runtime body
@@ -745,6 +899,23 @@ const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'kilo', 'non
745
899
  const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
746
900
  const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
747
901
  const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);
902
+ // #2871 Phase 2 — only `commands` and `skills` are trigger-bearing (a `/gsd-<name>`
903
+ // the USER types). `agents` and `kimi-agents` are a separate dispatch interface
904
+ // point (subagent invocation via `subagent_type`/named dispatch), never a trigger —
905
+ // see 40-design.md's "agents are not trigger-bearing" correction. A narrower set
906
+ // than VALID_ARTIFACT_KIND_NAMES on purpose: an artifact KIND can be agents; a
907
+ // trigger-precedence MEMBER never can.
908
+ const VALID_TRIGGER_PRECEDENCE_KINDS = new Set(['commands', 'skills']);
909
+ // The default runtime.triggerPrecedence (highest priority first) applied when a
910
+ // descriptor omits the axis (see validateRuntimeBody's required-with-default
911
+ // handling below). Not invented: `['skills', 'commands']` is the ordering every
912
+ // in-tree runtime that emits both kinds (from the same trigger stems) wants —
913
+ // claude's local/global collision and the same-scope collision (codebuddy, kilo,
914
+ // opencode, zcode) all resolve to skills winning. claude's shipped descriptor
915
+ // declares this SAME value explicitly, and
916
+ // tests/runtime-artifact-layout-trigger-surface.test.cjs asserts the two agree
917
+ // (a parity assertion — two surfaces reading one rule must not silently drift).
918
+ const DEFAULT_TRIGGER_PRECEDENCE = Object.freeze(['skills', 'commands']);
748
919
  const FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey'];
749
920
  // 'none' added #2103 — Marketplace/VSIX-distributed hosts (e.g. VS Code) that
750
921
  // are never CLI-installed (no allRuntimes membership, no install flag).
@@ -851,7 +1022,36 @@ const VALID_LANE_HANDLERS = new Set(['antigravity', 'openai-compatible', 'op
851
1022
  // BOTH sub-shapes, or from NEITHER, has undefined meaning and fails validation.
852
1023
  // The discriminator is explicit rather than inferred from field presence, which
853
1024
  // is precisely the ambiguity these two sets exist to detect.
854
- const SPAWN_ONLY_INVOKE_FIELDS = ['binary', 'args', 'promptChannel', 'outputChannel', 'outputArg', 'modelArg'];
1025
+ // `env` added by #2483. It belongs in THIS set and not merely in validateSpawnInvoke: an environment
1026
+ // pair has no meaning for a transport that issues an HTTP POST, so a manifest declaring it alongside
1027
+ // `openai-http` fields is the undefined-meaning case the comment above describes. Registering it here
1028
+ // is what makes that case reportable — the openai-http arm rejects exactly the members of this list,
1029
+ // so a field absent from it is silently accepted on the wrong transport. Note `effortChannel` is
1030
+ // deliberately in NEITHER set: D2 defines it for both transports, so it is shared, not spawn-only.
1031
+ const SPAWN_ONLY_INVOKE_FIELDS = ['binary', 'args', 'promptChannel', 'outputChannel', 'outputArg', 'modelArg', 'env'];
1032
+ // Environment names refused outright on a reviewer lane (#2483): each one turns a declared pair into
1033
+ // arbitrary code execution in the spawned child. DEFENCE IN DEPTH, NOT THE BOUNDARY — say so plainly,
1034
+ // because a future reader who mistakes this for the control will under-invest in the one that is.
1035
+ // The boundary is install-time consent: `capability-trust` discloses every declared env pair, shows
1036
+ // its value, and binds it to the consent signature, so an unlisted name is still SEEN before it runs.
1037
+ //
1038
+ // Deliberately incomplete, and it cannot be otherwise: the spawned binary is arbitrary third-party
1039
+ // code, so the exhaustive set is every interpreter's injection variables. What this list buys is that
1040
+ // the highest-confidence, lowest-legitimacy routes cannot be taken quietly. `PATH` is included — it is
1041
+ // the most complete primitive of the set (repoint it at a directory holding a fake binary) and no
1042
+ // shipped reviewer manifest declares it; a lane that needs a specific executable declares an absolute
1043
+ // `invoke.binary` rather than reshaping the child's `PATH`.
1044
+ //
1045
+ // Matched CASE-INSENSITIVELY (members stored uppercase) — Windows environment lookup is
1046
+ // case-insensitive, so an exact-case set is bypassed by declaring `Path` or `node_options`.
1047
+ const DENIED_LANE_ENV_KEYS = new Set([
1048
+ 'PATH', 'NODE_OPTIONS', 'NODE_REPL_EXTERNAL_MODULE', 'NODE_PATH',
1049
+ 'LD_PRELOAD', 'LD_AUDIT', 'LD_LIBRARY_PATH',
1050
+ 'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH',
1051
+ 'PYTHONSTARTUP', 'PYTHONPATH', 'BASH_ENV', 'ENV',
1052
+ 'PERL5OPT', 'RUBYOPT', 'JAVA_TOOL_OPTIONS', '_JAVA_OPTIONS', 'CLASSPATH',
1053
+ 'GIT_SSH_COMMAND', 'GIT_EXTERNAL_DIFF',
1054
+ ]);
855
1055
  // `defaultHost` / `fallbackModel` added by Phase 5b (#2799). Phase 4 federated every
856
1056
  // `review.*_host` key with a default of `""`, so the REAL fallback destination and model
857
1057
  // (`http://localhost:11434` / `llama3` and friends) existed only inside the bash leg. Once the
@@ -860,7 +1060,7 @@ const HTTP_ONLY_INVOKE_FIELDS = ['hostConfigKey', 'defaultHost', 'path', 'model
860
1060
 
861
1061
  // Feature-only fields are as forbidden on a lane-only capability as on a runtime
862
1062
  // one; a `role: "reviewer"` capability owns no artefacts and wires no loop point.
863
- const FEATURE_FIELDS_FORBIDDEN_ON_REVIEWER = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey'];
1063
+ const FEATURE_FIELDS_FORBIDDEN_ON_REVIEWER = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey', 'taskContentResolver'];
864
1064
 
865
1065
  // GATE A: installSurface → allowed hooksSurface values (DEFECT.GENERATIVE-FIX: parity invariant)
866
1066
  // Derived from the actual pairings in the 16 real runtime descriptors.
@@ -1505,6 +1705,16 @@ function validateRuntimeBody(cap) {
1505
1705
  'runtime.orchestratorExec.promptFlag must be a string or null (got: ' + JSON.stringify(oe.promptFlag) + ')',
1506
1706
  );
1507
1707
  }
1708
+
1709
+ // modelFlag — optional; string or null (#3714). `null`/absent means the
1710
+ // host offers no per-invocation model override on this exec path; a
1711
+ // string names the flag that pins the executor's model (codex: --model).
1712
+ // Deliberately asymmetric: only codex declares this today.
1713
+ if (oe.modelFlag !== undefined && oe.modelFlag !== null && typeof oe.modelFlag !== 'string') {
1714
+ errors.push(
1715
+ 'runtime.orchestratorExec.modelFlag must be a string or null (got: ' + JSON.stringify(oe.modelFlag) + ')',
1716
+ );
1717
+ }
1508
1718
  }
1509
1719
  }
1510
1720
 
@@ -1557,6 +1767,45 @@ function validateRuntimeBody(cap) {
1557
1767
  }
1558
1768
  }
1559
1769
 
1770
+ // triggerPrecedence — #2871 Phase 2 amendment to ADR-1016's runtime body.
1771
+ // REQUIRED-WITH-DEFAULT: no other axis in this validator uses this shape —
1772
+ // every axis above either hard-requires the field (throws when absent) or
1773
+ // treats absence as fully unconstrained (effortSurface/isolation: "nothing to
1774
+ // validate" when undefined). This axis does neither: an ABSENT value is
1775
+ // substituted with DEFAULT_TRIGGER_PRECEDENCE and then validated exactly as
1776
+ // if it HAD been supplied, so a third-party capability.json authored before
1777
+ // this phase keeps validating (ADR-894 additive-only / Hyrum's Law, ADR-1244)
1778
+ // while a PRESENT-but-malformed value still fails loudly instead of silently
1779
+ // passing through unchecked.
1780
+ const triggerPrecedenceValue = Object.prototype.hasOwnProperty.call(r, 'triggerPrecedence')
1781
+ ? r.triggerPrecedence
1782
+ : DEFAULT_TRIGGER_PRECEDENCE;
1783
+ if (!Array.isArray(triggerPrecedenceValue)) {
1784
+ errors.push(
1785
+ 'runtime.triggerPrecedence must be an array of trigger-bearing kind names (' +
1786
+ [...VALID_TRIGGER_PRECEDENCE_KINDS].join(', ') + ') (got: ' + JSON.stringify(triggerPrecedenceValue) + ')',
1787
+ );
1788
+ } else if (triggerPrecedenceValue.length === 0) {
1789
+ errors.push('runtime.triggerPrecedence must not be empty');
1790
+ } else {
1791
+ const seenKinds = new Set();
1792
+ for (let i = 0; i < triggerPrecedenceValue.length; i++) {
1793
+ const k = triggerPrecedenceValue[i];
1794
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') {
1795
+ errors.push('runtime.triggerPrecedence[' + i + '] "' + k + '" is a reserved name');
1796
+ } else if (typeof k !== 'string' || !VALID_TRIGGER_PRECEDENCE_KINDS.has(k)) {
1797
+ errors.push(
1798
+ 'runtime.triggerPrecedence[' + i + '] must be one of: ' + [...VALID_TRIGGER_PRECEDENCE_KINDS].join(', ') +
1799
+ ' (got: ' + JSON.stringify(k) + ')',
1800
+ );
1801
+ } else if (seenKinds.has(k)) {
1802
+ errors.push('runtime.triggerPrecedence contains a duplicate kind: ' + JSON.stringify(k));
1803
+ } else {
1804
+ seenKinds.add(k);
1805
+ }
1806
+ }
1807
+ }
1808
+
1560
1809
  return errors;
1561
1810
  }
1562
1811
 
@@ -1578,6 +1827,122 @@ const KNOWN_REVIEWER_FIELDS = new Set([
1578
1827
  'handler',
1579
1828
  ]);
1580
1829
 
1830
+ /**
1831
+ * The closed `runtime.hostBehaviors` vocabulary (ADR-1016, closed via #2801).
1832
+ *
1833
+ * ADR-1016's core principle is that every per-runtime difference is a value over
1834
+ * a closed primitive vocabulary, and that a host needing a new shape gets a
1835
+ * reviewed first-party primitive rather than "an open escape hatch in the
1836
+ * descriptor" (§Alternatives #2 rejects exactly that). `hostBehaviors` was the
1837
+ * one hole left in that closure: 59 keys across 18 manifests, 39 of them set by
1838
+ * a single capability, validated by nothing. It was described in the reference
1839
+ * docs as a deliberate open seam sanctioned by ADR-1016 — ADR-1016 does not
1840
+ * mention `hostBehaviors` at all, and its stated principle is the opposite.
1841
+ *
1842
+ * Adding a key here is deliberate and reviewed. That friction IS the decision
1843
+ * (ADR-1016 §Consequences: "the closed vocabulary must grow (reviewed) when a
1844
+ * genuinely new host shape appears — intentional friction, the trust boundary").
1845
+ *
1846
+ * WARNING, never error. Two reasons:
1847
+ * 1. It matches the ADR-2782 D4.3 treatment of an unknown `reviewer` field, so
1848
+ * a manifest built against a newer GSD degrades visibly instead of failing
1849
+ * the build of a repo that merely reads it.
1850
+ * 2. An error would hard-break an out-of-tree descriptor carrying a bespoke
1851
+ * key, with no deprecation window — the exact mistake #2801's own alias
1852
+ * removal spent a full release avoiding. Escalating to an error is a later
1853
+ * step and needs its own window.
1854
+ *
1855
+ * Kept in sorted order, and `tests/reviewer-manifest-body.test.cjs` asserts this
1856
+ * set equals the keys the shipped manifests actually declare, so the list cannot
1857
+ * silently rot away from reality (DEFECT.GENERATIVE-FIX).
1858
+ */
1859
+ const KNOWN_HOST_BEHAVIORS = new Set([
1860
+ 'agentFileExtension',
1861
+ 'agentFrontmatterExtensions',
1862
+ 'agentManifestStyle',
1863
+ 'agentTomlFiles',
1864
+ 'attributionConfigResolver',
1865
+ 'attributionSource',
1866
+ 'authorsCanonicalWorkflow',
1867
+ 'brandingRewrites',
1868
+ 'cleanupSkillSidecars',
1869
+ 'clineRulesSurface',
1870
+ 'combinedFamilyInstall',
1871
+ 'commandBodyConverter',
1872
+ 'doneBannerStyle',
1873
+ 'flatCommandDir',
1874
+ 'frontmatterDialect',
1875
+ 'globalDirResolver',
1876
+ 'hookPathStyle',
1877
+ 'hooksJsonSurface',
1878
+ 'hyphenNameAgentBody',
1879
+ 'installsCommandBodiesForWorkflowDelegation',
1880
+ 'legacyCommandsGsdCleanup',
1881
+ 'legacyCommandsGsdInstallMigration',
1882
+ 'legacyCommandsGsdUninstall',
1883
+ 'legacyDevinSkillsCleanup',
1884
+ 'localCommandsViaRules',
1885
+ 'localInstallDeferred',
1886
+ 'localInstallStyle',
1887
+ 'localTargetIsProjectRoot',
1888
+ 'managedHookEvents',
1889
+ 'mcpCompanion',
1890
+ 'namedSubagentsSupported',
1891
+ 'nativeModelAliases',
1892
+ 'nativePlugin',
1893
+ 'noPathRewrite',
1894
+ 'ownsClaudePaths',
1895
+ 'permissionsSchema',
1896
+ 'pluginOnlyInstall',
1897
+ 'projectInstructionFile',
1898
+ 'reapplyCommand',
1899
+ 'reportCommandsDir',
1900
+ 'reportSkillsCount',
1901
+ 'retiredArtifacts',
1902
+ 'settingsFileByScope',
1903
+ 'sharedHooksDirName',
1904
+ 'skillFrontmatterVersion',
1905
+ 'skillPriorityFrontmatter',
1906
+ 'skillsGlobalOnboarding',
1907
+ 'skillsManifestPrefix',
1908
+ 'skipCodexSkillsManifest',
1909
+ 'skipHomePrefixSubstitution',
1910
+ 'skipSettingsUi',
1911
+ 'skipSharedHooksInstall',
1912
+ 'skipUpdateBannerCommand',
1913
+ 'soloStageMetadata',
1914
+ 'sourceMarkerFile',
1915
+ 'tomlConfigInstall',
1916
+ 'trackCategoryDescription',
1917
+ 'verificationStyle',
1918
+ 'writeCategoryDescription',
1919
+ ]);
1920
+
1921
+ /**
1922
+ * Frozen reason codes for the non-fatal reviewer diagnostics (ADR-2782 D4.3).
1923
+ *
1924
+ * The IR behind `collectReviewerWarnings`' rendered strings. Tests assert on
1925
+ * these codes; the rendered `message` is operator console output and tests must
1926
+ * not depend on it (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test
1927
+ * Outputs"), which is the same split `bin/verify-reapply-patches.cjs` uses.
1928
+ *
1929
+ * Adding a code is THREE coordinated changes: this enum, the emitting site in
1930
+ * `collectReviewerWarningRecordFields`, and the test that locks
1931
+ * `Object.keys(REVIEWER_WARNING).sort()`. That coupling is the point — it stops
1932
+ * the code surface drifting from the test surface.
1933
+ */
1934
+ const REVIEWER_WARNING = Object.freeze({
1935
+ /** A key inside a `reviewer` body that this GSD version does not know. */
1936
+ UNKNOWN_REVIEWER_FIELD: 'unknown_reviewer_field',
1937
+ /** A `runtime.hostBehaviors` key that was removed from the vocabulary. */
1938
+ REMOVED_HOST_BEHAVIOR: 'removed_host_behavior',
1939
+ /** A `runtime.hostBehaviors` key outside the closed vocabulary. */
1940
+ UNKNOWN_HOST_BEHAVIOR: 'unknown_host_behavior',
1941
+ });
1942
+
1943
+ /** Dotted path of the field removed by ADR-2782 D9 / #2801. */
1944
+ const REMOVED_REVIEWER_CLI_FIELD = 'runtime.hostBehaviors.reviewerCli';
1945
+
1581
1946
  const KNOWN_PROBE_FIELDS = new Set(['kind', 'binary', 'needle', 'timeoutMs', 'hostConfigKey', 'path']);
1582
1947
 
1583
1948
  /** A bounded probe timeout must be a finite, positive INTEGER of milliseconds. */
@@ -1615,6 +1980,41 @@ function describeValue(v) {
1615
1980
  }
1616
1981
  }
1617
1982
 
1983
+ /**
1984
+ * Ceiling on how many undeclared-key diagnostics one capability may produce.
1985
+ *
1986
+ * The loops below iterate MANIFEST-SUPPLIED keys, and an installed third-party
1987
+ * manifest is bounded only by MANIFEST_MAX_BYTES (8MB). Unbounded, one manifest
1988
+ * of 800k keys yields 800k records and ~139MB of message text, retained for the
1989
+ * registry's lifetime in OverlayMeta.diagnostics. Ten is enough to act on; the
1990
+ * rest are summarized. Mirrors the existing truncation idiom in
1991
+ * `capability-loader.cts` (`crossErrs.slice(0, 3)`).
1992
+ */
1993
+ const MAX_REPORTED_UNKNOWN_KEYS = 10;
1994
+
1995
+ /** Ceiling on how much of one manifest-supplied key name a diagnostic repeats. */
1996
+ const MAX_REPORTED_KEY_CHARS = 80;
1997
+
1998
+ /**
1999
+ * Render a manifest-supplied KEY for a diagnostic: control-safe and bounded.
2000
+ *
2001
+ * Key names carry no grammar anywhere — unlike `cap.id`, which `validateCapability`
2002
+ * gates on KEBAB_RE before these diagnostics run — so a key is fully
2003
+ * attacker-controlled text heading for stderr and OverlayMeta.warnings. C0/C1
2004
+ * controls (ESC, CR, LF) become U+FFFD so a key cannot emit terminal escapes or
2005
+ * forge a log line, and the result is clipped so one key cannot carry megabytes
2006
+ * into a retained diagnostic.
2007
+ *
2008
+ * @param {*} key
2009
+ * @returns {string}
2010
+ */
2011
+ function describeKey(key) {
2012
+ const raw = typeof key === 'string' ? key : String(key);
2013
+ // eslint-disable-next-line no-control-regex
2014
+ const safe = raw.replace(/[\x00-\x1f\x7f-\x9f]/g, '�');
2015
+ return safe.length > MAX_REPORTED_KEY_CHARS ? safe.slice(0, MAX_REPORTED_KEY_CHARS) + '…' : safe;
2016
+ }
2017
+
1618
2018
  /** Extract a message from an unknown thrown value without throwing again. */
1619
2019
  function safeErrorMessage(err) {
1620
2020
  try {
@@ -1661,7 +2061,39 @@ function validateEnumField(ctx, label, value, validSet) {
1661
2061
  }
1662
2062
 
1663
2063
  /**
1664
- * Collect NON-FATAL diagnostics for a reviewer body (ADR-2782 D4.3).
2064
+ * Collect NON-FATAL reviewer diagnostics for a capability manifest as TYPED
2065
+ * RECORDS (ADR-2782 D4.3, plus the D9 `hostBehaviors.reviewerCli` removal
2066
+ * notice, #2801).
2067
+ *
2068
+ * This is the IR. `collectReviewerWarnings` below renders it to strings for the
2069
+ * two production consumers; tests assert on these records instead of matching
2070
+ * the rendered prose (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test
2071
+ * Outputs").
2072
+ *
2073
+ * Record shape — `code` and `capId` are always present; the rest is per-code:
2074
+ * { code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
2075
+ * capId, field: 'reviewer.<key>', knownFields: string[], message }
2076
+ * { code: REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR,
2077
+ * capId, field: 'runtime.hostBehaviors.reviewerCli',
2078
+ * replacement: 'reviewer', docs: '<how-to path>', message }
2079
+ *
2080
+ * TOTAL: returns an array for ANY input and never throws. It runs on
2081
+ * loadRegistry's ACCEPT path, so a diagnostic that throws would cost the user a
2082
+ * lane that is otherwise perfectly valid (#1461 OVL-1).
2083
+ *
2084
+ * @param {object} cap A capability manifest.
2085
+ * @returns {Array<object>} Warning records; empty when there is nothing to say.
2086
+ */
2087
+ function collectReviewerWarningRecords(cap) {
2088
+ try {
2089
+ return collectReviewerWarningRecordFields(cap);
2090
+ } catch {
2091
+ return [];
2092
+ }
2093
+ }
2094
+
2095
+ /**
2096
+ * Render the IR for operator console output.
1665
2097
  *
1666
2098
  * Kept separate from validateReviewerBody so validateCapability's contract
1667
2099
  * (`=> string[]` of ERRORS) is unchanged for its two existing callers. The
@@ -1669,35 +2101,135 @@ function validateEnumField(ctx, label, value, validSet) {
1669
2101
  * through OverlayMeta.warnings. A warning written only to a build log nobody
1670
2102
  * reads is not a warning (ADR-2782 D4, "Where warnings surface").
1671
2103
  *
2104
+ * The `string[]` shape is load-bearing for both consumers and does not change.
2105
+ *
1672
2106
  * @param {object} cap A capability manifest.
1673
2107
  * @returns {string[]} Warning strings; empty when there is nothing to say.
1674
2108
  */
1675
2109
  function collectReviewerWarnings(cap) {
1676
- // Same totality contract as validateReviewerBody, for the same reason: this is
1677
- // called from loadRegistry's accept path, and a diagnostic that throws must
1678
- // never cost the user a working lane.
1679
- try {
1680
- return collectReviewerWarningFields(cap);
1681
- } catch {
1682
- return [];
1683
- }
2110
+ return collectReviewerWarningRecords(cap).map((record) => record.message);
1684
2111
  }
1685
2112
 
1686
- function collectReviewerWarningFields(cap) {
1687
- const warnings = [];
1688
- if (typeof cap !== 'object' || cap === null || Array.isArray(cap)) return warnings;
1689
- const r = cap.reviewer;
1690
- if (typeof r !== 'object' || r === null || Array.isArray(r)) return warnings;
2113
+ function collectReviewerWarningRecordFields(cap) {
2114
+ const records = [];
2115
+ if (typeof cap !== 'object' || cap === null || Array.isArray(cap)) return records;
1691
2116
 
1692
2117
  const capId = typeof cap.id === 'string' ? cap.id : '(unknown)';
2118
+
2119
+ // ADR-2782 D9 / #2801 — the REMOVED `runtime.hostBehaviors.reviewerCli` alias.
2120
+ //
2121
+ // Emitted BEFORE the `reviewer`-body early-return below, deliberately. The
2122
+ // manifest this notice exists for is the ALIAS-ONLY one, which by definition
2123
+ // carries no body; after that guard the check would fire only for
2124
+ // capabilities that already declare a lane — exactly the set that does not
2125
+ // need telling.
2126
+ //
2127
+ // Presence-based, not `=== true`: the key is unknown at ANY value now, which
2128
+ // is the same rule the unknown-`reviewer.*`-field loop below applies. Own-key
2129
+ // read, so a polluted prototype cannot manufacture this warning on every
2130
+ // otherwise-innocent manifest. This is ONE keyed removal notice, not general
2131
+ // `hostBehaviors` validation — the closed vocabulary below handles that (#2801).
2132
+ const runtimeBody = cap.runtime;
2133
+ if (typeof runtimeBody === 'object' && runtimeBody !== null && !Array.isArray(runtimeBody)) {
2134
+ const hostBehaviors = runtimeBody.hostBehaviors;
2135
+ if (
2136
+ typeof hostBehaviors === 'object'
2137
+ && hostBehaviors !== null
2138
+ && !Array.isArray(hostBehaviors)
2139
+ && Object.prototype.hasOwnProperty.call(hostBehaviors, 'reviewerCli')
2140
+ ) {
2141
+ records.push({
2142
+ code: REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR,
2143
+ capId,
2144
+ field: REMOVED_REVIEWER_CLI_FIELD,
2145
+ replacement: 'reviewer',
2146
+ docs: 'docs/how-to/ship-a-reviewer-lane.md',
2147
+ message:
2148
+ '⚠ capability "' + capId + '" ' + REMOVED_REVIEWER_CLI_FIELD + ' was removed (ADR-2782 D9) ' +
2149
+ '— ignored, and it contributes no reviewer lane. Declare a `reviewer` body instead; see ' +
2150
+ 'docs/how-to/ship-a-reviewer-lane.md',
2151
+ });
2152
+ }
2153
+
2154
+ // #2801 — the closed `hostBehaviors` vocabulary (ADR-1016). `reviewerCli` is
2155
+ // excluded here: it already drew its own removal notice above, and a second,
2156
+ // generic "unknown key" record for the same key would be noise, not signal.
2157
+ if (typeof hostBehaviors === 'object' && hostBehaviors !== null && !Array.isArray(hostBehaviors)) {
2158
+ let reported = 0;
2159
+ let omitted = 0;
2160
+ for (const key of Object.keys(hostBehaviors)) {
2161
+ if (isReservedName(key) || key === 'reviewerCli' || KNOWN_HOST_BEHAVIORS.has(key)) continue;
2162
+ if (reported >= MAX_REPORTED_UNKNOWN_KEYS) {
2163
+ omitted += 1;
2164
+ continue;
2165
+ }
2166
+ reported += 1;
2167
+ const safeKey = describeKey(key);
2168
+ records.push({
2169
+ code: REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR,
2170
+ capId,
2171
+ field: 'runtime.hostBehaviors.' + safeKey,
2172
+ message:
2173
+ '⚠ capability "' + capId + '" runtime.hostBehaviors.' + safeKey + ' is not a known host behavior ' +
2174
+ 'in this GSD version — ignored. Adding one is a reviewed first-party change (ADR-1016).',
2175
+ });
2176
+ }
2177
+ if (omitted > 0) {
2178
+ records.push({
2179
+ code: REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR,
2180
+ capId,
2181
+ field: 'runtime.hostBehaviors',
2182
+ truncated: true,
2183
+ omittedCount: omitted,
2184
+ message:
2185
+ '⚠ capability "' + capId + '" declares ' + omitted + ' further unknown runtime.hostBehaviors ' +
2186
+ 'key(s), not listed. A manifest this far outside the vocabulary is likely built for a ' +
2187
+ 'different GSD version.',
2188
+ });
2189
+ }
2190
+ }
2191
+ }
2192
+
2193
+ const r = cap.reviewer;
2194
+ if (typeof r !== 'object' || r === null || Array.isArray(r)) return records;
2195
+
2196
+ // Same ceiling and the same key sanitization as the hostBehaviors sweep above.
2197
+ // This loop predates #2801 and carried both defects; fixing only the new copy
2198
+ // would leave the identical defect one screen away from its own fix.
2199
+ let reportedFields = 0;
2200
+ let omittedFields = 0;
1693
2201
  for (const key of Object.keys(r)) {
1694
2202
  if (isReservedName(key) || KNOWN_REVIEWER_FIELDS.has(key)) continue;
1695
- warnings.push(
1696
- '⚠ capability "' + capId + '" reviewer.' + key + ' is not a known reviewer field ' +
1697
- 'in this GSD version — ignored. Known fields: ' + [...KNOWN_REVIEWER_FIELDS].join(', '),
1698
- );
1699
- }
1700
- return warnings;
2203
+ if (reportedFields >= MAX_REPORTED_UNKNOWN_KEYS) {
2204
+ omittedFields += 1;
2205
+ continue;
2206
+ }
2207
+ reportedFields += 1;
2208
+ const safeKey = describeKey(key);
2209
+ records.push({
2210
+ code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
2211
+ capId,
2212
+ field: 'reviewer.' + safeKey,
2213
+ knownFields: [...KNOWN_REVIEWER_FIELDS],
2214
+ message:
2215
+ '⚠ capability "' + capId + '" reviewer.' + safeKey + ' is not a known reviewer field ' +
2216
+ 'in this GSD version — ignored. Known fields: ' + [...KNOWN_REVIEWER_FIELDS].join(', '),
2217
+ });
2218
+ }
2219
+ if (omittedFields > 0) {
2220
+ records.push({
2221
+ code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
2222
+ capId,
2223
+ field: 'reviewer',
2224
+ knownFields: [...KNOWN_REVIEWER_FIELDS],
2225
+ truncated: true,
2226
+ omittedCount: omittedFields,
2227
+ message:
2228
+ '⚠ capability "' + capId + '" declares ' + omittedFields + ' further unknown reviewer field(s), ' +
2229
+ 'not listed.',
2230
+ });
2231
+ }
2232
+ return records;
1701
2233
  }
1702
2234
 
1703
2235
  /**
@@ -2046,6 +2578,66 @@ function validateSpawnInvoke(ctx, invoke) {
2046
2578
 
2047
2579
  errors.push(...validateEnumField(ctx, 'reviewer.invoke.effortChannel', invoke.effortChannel, VALID_LANE_EFFORT_CHANNELS));
2048
2580
 
2581
+ // `env` — per-invocation environment pairs (#2483). OPTIONAL, unlike every field above: only a lane
2582
+ // that needs to shape its child's environment declares it, and absent is the common case. Validated
2583
+ // when present, because `resolveLanePlan` DROPS a non-string value rather than coercing it — so an
2584
+ // unvalidated manifest declares a pair that silently never reaches the spawn, which is the failure
2585
+ // mode a shipped env-guard can least afford. Keys are held to the portable POSIX
2586
+ // environment-name grammar, which is a POLICY — not a claim about what an environment can physically
2587
+ // hold. Measured: of the names refused below only NUL is actually rejected by `spawnSync`; `=`, a
2588
+ // leading digit, a dash and a space are all carried through to the child (an `A=B` key arrives as
2589
+ // the raw entry `A=B=value`, and reads back via `process.env['A=B']`). They are refused because a
2590
+ // name outside the grammar is not portably addressable by the program meant to read it.
2591
+ if (invoke.env !== undefined) {
2592
+ if (typeof invoke.env !== 'object' || invoke.env === null || Array.isArray(invoke.env)) {
2593
+ errors.push(
2594
+ ctx + ' reviewer.invoke.env must be an object of environment name/value pairs ' +
2595
+ '(got: ' + describeValue(invoke.env) + ')',
2596
+ );
2597
+ } else {
2598
+ for (const key of Object.keys(invoke.env)) {
2599
+ // `__proto__` passes the grammar below and IS a real own key once a manifest is JSON-parsed,
2600
+ // but assigning it onto a plain accumulator goes through the inherited `__proto__` SETTER
2601
+ // instead of creating an own property. For the string values this field permits the setter is
2602
+ // a no-op — the prototype is not even changed — so the pair would validate and then simply
2603
+ // vanish before the spawn: the declared-but-never-delivered failure this block exists to catch.
2604
+ // Refused by name, because the grammar cannot see it.
2605
+ //
2606
+ // Only `__proto__` needs this. Sibling reserved-name guards in this file reject
2607
+ // `constructor`/`prototype` alongside it, but those guard bracket LOOKUPS that resolve
2608
+ // prototype members; here the read is `Object.keys` + an own-value read, and `constructor`
2609
+ // assigns as an ordinary own key. Refusing it too would reject a name the spawn could carry.
2610
+ // CASE-INSENSITIVE, and that is not pedantry: Windows environment lookups are
2611
+ // case-insensitive, so `Path` / `node_options` reach the child as `PATH` / `NODE_OPTIONS`
2612
+ // and an exact-case set is bypassed by changing one letter. The grammar above already
2613
+ // constrains keys to ASCII, so a plain uppercase fold is sufficient here.
2614
+ if (DENIED_LANE_ENV_KEYS.has(key.toUpperCase())) {
2615
+ errors.push(
2616
+ ctx + ' reviewer.invoke.env key "' + key + '" is not permitted ' +
2617
+ '(it makes the spawned reviewer run code of the manifest\'s choosing; ' +
2618
+ 'declare an absolute `invoke.binary` instead of reshaping the child\'s environment)',
2619
+ );
2620
+ } else if (key === '__proto__') {
2621
+ errors.push(
2622
+ ctx + ' reviewer.invoke.env key "__proto__" is not permitted ' +
2623
+ '(it is silently dropped when the spawn plan is assembled, so it would never reach the child)',
2624
+ );
2625
+ } else if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
2626
+ errors.push(
2627
+ ctx + ' reviewer.invoke.env key ' + describeValue(key) +
2628
+ ' is not a valid environment variable name',
2629
+ );
2630
+ }
2631
+ if (typeof invoke.env[key] !== 'string') {
2632
+ errors.push(
2633
+ ctx + ' reviewer.invoke.env.' + key + ' must be a string ' +
2634
+ '(got: ' + describeValue(invoke.env[key]) + ')',
2635
+ );
2636
+ }
2637
+ }
2638
+ }
2639
+ }
2640
+
2049
2641
  return errors;
2050
2642
  }
2051
2643
 
@@ -2709,6 +3301,11 @@ function validateCrossCapability(capMap, centralKeys, centralPatterns = []) {
2709
3301
  const laneSlugClaims = new Map(); // slug → capId[]
2710
3302
  const laneFlagClaims = new Map(); // flag → capId[]
2711
3303
  const laneSectionClaims = new Map(); // reviewsSection → capId[]
3304
+ // ADR-3646: task-content resolver tracker-prefix uniqueness across the
3305
+ // MERGED first-party ∪ overlay set, mirroring the reviewer-lane collision
3306
+ // pattern above — two resolvers claiming the same prefix would make
3307
+ // `execute:task` dispatch ambiguous (which capability's resolver runs?).
3308
+ const trackerPrefixClaims = new Map(); // trackerPrefix → capId[]
2712
3309
 
2713
3310
  // Claims are ACCUMULATED and reported after the sweep, never reported on the
2714
3311
  // second claimant. Reporting pairwise-on-collision looks equivalent and is not:
@@ -2729,6 +3326,15 @@ function validateCrossCapability(capMap, centralKeys, centralPatterns = []) {
2729
3326
  };
2730
3327
 
2731
3328
  for (const [capId, cap] of capMap) {
3329
+ // ADR-3646: a MALFORMED taskContentResolver body was already reported by
3330
+ // validateCapability — do not double-report; only claim well-shaped
3331
+ // bodies. Independent of the reviewer-lane checks below, so it runs even
3332
+ // for capabilities that carry no `reviewer` body at all.
3333
+ const tcr = cap.taskContentResolver;
3334
+ if (typeof tcr === 'object' && tcr !== null && !Array.isArray(tcr)) {
3335
+ claim(trackerPrefixClaims, tcr.trackerPrefix, capId);
3336
+ }
3337
+
2732
3338
  const r = cap.reviewer;
2733
3339
  // A capability with no lane contributes to no uniqueness set. A MALFORMED
2734
3340
  // body was already reported by validateCapability — do not double-report.
@@ -2750,6 +3356,7 @@ function validateCrossCapability(capMap, centralKeys, centralPatterns = []) {
2750
3356
  [laneSlugClaims, 'slug'],
2751
3357
  [laneFlagClaims, 'flag'],
2752
3358
  [laneSectionClaims, 'reviewsSection'],
3359
+ [trackerPrefixClaims, 'taskContentResolver.trackerPrefix'],
2753
3360
  ]) {
2754
3361
  for (const [key, claimants] of claims) {
2755
3362
  if (claimants.length < 2) continue;
@@ -2970,26 +3577,55 @@ function topoSortContributions(entries) {
2970
3577
 
2971
3578
  // ─── Gen-time wired guard ─────────────────────────────────────────────────────
2972
3579
 
3580
+ /**
3581
+ * Hook group (capability.json array name) → hook kind (dispatch discriminator).
3582
+ * Single source of truth for both validateHooksWired and the scanner-parity
3583
+ * test in tests/capability-registry.test.cjs (#3606).
3584
+ */
3585
+ const HOOK_GROUP_KINDS = Object.freeze({
3586
+ steps: 'step',
3587
+ contributions: 'contribution',
3588
+ gates: 'gate',
3589
+ });
3590
+
2973
3591
  /**
2974
3592
  * Validate that every hook point declared by a capability has a corresponding
2975
- * `loop render-hooks <point>` call site in one of the host-loop workflow files.
3593
+ * `loop render-hooks <point>` call site in one of the host-loop workflow files,
3594
+ * AND — #3606 — that the call site's dispatch text covers the hook's KIND.
3595
+ *
3596
+ * A call site proves hooks are rendered, not dispatched: a consumer that
3597
+ * iterates only `kind == "gate"` (or narrows `kind == "step"` to one
3598
+ * `ref.skill`) silently drops every other registered kind — a capability can
3599
+ * be wired, enabled, resolved active, and never run. See
3600
+ * gsd-core/references/loop-hook-dispatch.md ("A point whose workflow
3601
+ * hand-rolls one kind does not implement this contract").
2976
3602
  *
2977
3603
  * Only valid loop points (in VALID_LOOP_POINTS) are checked here. Invalid points
2978
3604
  * are already caught by validateStep/validateContribution/validateGate — do not
2979
3605
  * double-report.
2980
3606
  *
3607
+ * KNOWN LIMITATION (#3606): coverage is the UNION across all call sites for a
3608
+ * point in the five STEP_WORKFLOWS host files. Consumers outside that universe
3609
+ * (quick.md, autonomous.md, code-review*.md, audit-milestone.md,
3610
+ * secure-phase.md, validate-phase.md) are not per-file checked — a narrowed
3611
+ * consumer there passes as long as one host file covers the point. Per-file
3612
+ * coverage maps are the tightening path.
3613
+ *
2981
3614
  * @param {object} cap Validated capability object.
2982
- * @param {Set<string>} wiredSet Set of points that have call sites in host workflows.
2983
- * @returns {string[]} Array of error strings; empty means all points are wired.
3615
+ * @param {Map<string, Set<string>>} wiredKinds Per point, the hook kinds the
3616
+ * host workflows' call-site dispatch text covers (getWiredKinds). A point
3617
+ * absent from the map is unwired.
3618
+ * @returns {string[]} Array of error strings; empty means all points
3619
+ * are wired and every registered kind is covered.
2984
3620
  */
2985
- function validateHooksWired(cap, wiredSet) {
3621
+ function validateHooksWired(cap, wiredKinds) {
2986
3622
  const errors = [];
2987
3623
  const capId = cap.id || '(unknown)';
2988
3624
 
2989
- function checkPoint(point, groupName, idx) {
3625
+ function checkPoint(point, groupName, kind, idx) {
2990
3626
  // Only flag valid points that are unwired — invalid points are schema-validator's job.
2991
3627
  if (!VALID_LOOP_POINTS.has(point)) return;
2992
- if (!wiredSet.has(point)) {
3628
+ if (!wiredKinds.has(point)) {
2993
3629
  errors.push(
2994
3630
  'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
2995
3631
  '" is declared but not wired in any host-loop workflow ' +
@@ -2997,20 +3633,34 @@ function validateHooksWired(cap, wiredSet) {
2997
3633
  'Wire the call site in the host workflow ' +
2998
3634
  '(see scripts/gen-loop-host-contract.cjs STEP_WORKFLOWS) or remove the hook.',
2999
3635
  );
3636
+ return;
3637
+ }
3638
+ const covered = wiredKinds.get(point);
3639
+ if (covered.size === 0) {
3640
+ errors.push(
3641
+ 'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
3642
+ '" has `loop render-hooks ' + point + '` call site(s), but their dispatch text covers NO ' +
3643
+ 'hook kind — every consumer is narrowed to specific hooks. Dispatch every registered kind ' +
3644
+ 'per gsd-core/references/loop-hook-dispatch.md.',
3645
+ );
3646
+ return;
3647
+ }
3648
+ if (!covered.has(kind)) {
3649
+ errors.push(
3650
+ 'capability "' + capId + '" ' + groupName + '[' + idx + '].point "' + point +
3651
+ '" registers a ' + kind + ' hook, but the host call site\'s dispatch text never ' +
3652
+ 'covers `kind == "' + kind + '"` (it covers: ' + [...covered].sort().join(', ') + '). ' +
3653
+ 'A hand-rolled single-kind consumer silently never dispatches the other kinds — ' +
3654
+ 'dispatch every registered kind per gsd-core/references/loop-hook-dispatch.md.',
3655
+ );
3000
3656
  }
3001
3657
  }
3002
3658
 
3003
- for (let i = 0; i < (cap.steps || []).length; i++) {
3004
- const hook = cap.steps[i];
3005
- if (hook.point !== undefined) checkPoint(hook.point, 'steps', i);
3006
- }
3007
- for (let i = 0; i < (cap.contributions || []).length; i++) {
3008
- const hook = cap.contributions[i];
3009
- if (hook.point !== undefined) checkPoint(hook.point, 'contributions', i);
3010
- }
3011
- for (let i = 0; i < (cap.gates || []).length; i++) {
3012
- const hook = cap.gates[i];
3013
- if (hook.point !== undefined) checkPoint(hook.point, 'gates', i);
3659
+ for (const [group, kind] of Object.entries(HOOK_GROUP_KINDS)) {
3660
+ for (let i = 0; i < (cap[group] || []).length; i++) {
3661
+ const hook = cap[group][i];
3662
+ if (hook.point !== undefined) checkPoint(hook.point, group, kind, i);
3663
+ }
3014
3664
  }
3015
3665
 
3016
3666
  return errors;
@@ -3155,6 +3805,8 @@ module.exports = {
3155
3805
  VALID_SANDBOX_TIERS,
3156
3806
  VALID_ARTIFACT_KIND_NAMES,
3157
3807
  VALID_ARTIFACT_NESTINGS,
3808
+ VALID_TRIGGER_PRECEDENCE_KINDS,
3809
+ DEFAULT_TRIGGER_PRECEDENCE,
3158
3810
  FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME,
3159
3811
  // ADR-2782 D1/D2/D3/D6/D7/D8 — reviewer lane body
3160
3812
  FEATURE_FIELDS_FORBIDDEN_ON_REVIEWER,
@@ -3170,8 +3822,14 @@ module.exports = {
3170
3822
  VALID_EVIDENCE_CLASSES,
3171
3823
  VALID_LANE_HANDLERS,
3172
3824
  KNOWN_REVIEWER_FIELDS,
3825
+ KNOWN_HOST_BEHAVIORS,
3826
+ MAX_REPORTED_UNKNOWN_KEYS,
3827
+ MAX_REPORTED_KEY_CHARS,
3173
3828
  validateReviewerBody,
3174
3829
  collectReviewerWarnings,
3830
+ collectReviewerWarningRecords,
3831
+ REVIEWER_WARNING,
3832
+ REMOVED_REVIEWER_CLI_FIELD,
3175
3833
  VALID_INSTALL_SURFACES,
3176
3834
  VALID_PERMISSION_WRITERS,
3177
3835
  VALID_EXTENDED_HOOK_EVENTS,
@@ -3208,6 +3866,7 @@ module.exports = {
3208
3866
  validateCommandEntry,
3209
3867
  validateRuntimeCompat,
3210
3868
  validateFeatureBody,
3869
+ validateTaskContentResolver,
3211
3870
  validateConfigHome,
3212
3871
  validateArtifactKindEntry,
3213
3872
  validateArtifactLayout,
@@ -3226,6 +3885,7 @@ module.exports = {
3226
3885
  topoSortSteps,
3227
3886
  topoSortContributions,
3228
3887
  validateHooksWired,
3888
+ HOOK_GROUP_KINDS,
3229
3889
  validateConfigSliceEntry,
3230
3890
  classifyCrossErrors,
3231
3891
  runConfigFormatParityGate,