@opengsd/gsd-core 1.10.0 → 1.11.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 (328) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/agents/gsd-debug-session-manager.md +11 -0
  4. package/agents/gsd-doc-synthesizer.md +2 -4
  5. package/agents/gsd-executor.md +5 -5
  6. package/agents/gsd-mempalace-curator.md +5 -2
  7. package/agents/gsd-phase-researcher.md +20 -1
  8. package/agents/gsd-plan-checker.md +37 -0
  9. package/agents/gsd-planner.md +44 -46
  10. package/agents/gsd-user-profiler.md +3 -0
  11. package/agents/gsd-verifier.md +12 -3
  12. package/bin/install.js +841 -971
  13. package/bin/lib/ui-safety-gate.cjs +2 -0
  14. package/commands/gsd/code-review.md +1 -1
  15. package/commands/gsd/execute-phase.md +1 -1
  16. package/commands/gsd/map-codebase.md +1 -1
  17. package/commands/gsd/mempalace-capture.md +1 -1
  18. package/commands/gsd/mempalace-recall.md +1 -1
  19. package/commands/gsd/new-milestone.md +1 -1
  20. package/commands/gsd/quick.md +1 -1
  21. package/commands/gsd/review-backlog.md +2 -1
  22. package/commands/gsd/verify-work.md +1 -1
  23. package/gsd-core/bin/gsd-tools.cjs +469 -88
  24. package/gsd-core/bin/lib/active-workstream-store.cjs +138 -22
  25. package/gsd-core/bin/lib/agent-install-check.cjs +230 -32
  26. package/gsd-core/bin/lib/api-coverage.cjs +3 -5
  27. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  28. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  29. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  30. package/gsd-core/bin/lib/audit.cjs +876 -240
  31. package/gsd-core/bin/lib/broken-windows.cjs +1 -1
  32. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  33. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  34. package/gsd-core/bin/lib/capability-registry.cjs +575 -101
  35. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  36. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  37. package/gsd-core/bin/lib/capability-validator.cjs +495 -22
  38. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  39. package/gsd-core/bin/lib/check-command-router.cjs +71 -37
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  41. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  42. package/gsd-core/bin/lib/command-aliases.cjs +22 -0
  43. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  44. package/gsd-core/bin/lib/commands.cjs +651 -86
  45. package/gsd-core/bin/lib/commonjs-marker.cjs +12 -6
  46. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  47. package/gsd-core/bin/lib/config-loader.cjs +75 -0
  48. package/gsd-core/bin/lib/config.cjs +10 -1
  49. package/gsd-core/bin/lib/core-utils.cjs +127 -29
  50. package/gsd-core/bin/lib/decisions.cjs +23 -0
  51. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  52. package/gsd-core/bin/lib/frontmatter.cjs +155 -20
  53. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  54. package/gsd-core/bin/lib/git-base-branch.cjs +102 -0
  55. package/gsd-core/bin/lib/gsd2-import.cjs +10 -1
  56. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  57. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  58. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  59. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  60. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  61. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  62. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  63. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  64. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  65. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  66. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  67. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  68. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  69. package/gsd-core/bin/lib/init.cjs +321 -129
  70. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  71. package/gsd-core/bin/lib/install-engine.cjs +745 -258
  72. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  73. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  74. package/gsd-core/bin/lib/install-profiles.cjs +134 -57
  75. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  76. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  77. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  78. package/gsd-core/bin/lib/installer-migrations.cjs +138 -31
  79. package/gsd-core/bin/lib/io.cjs +10 -0
  80. package/gsd-core/bin/lib/markdown-sectionizer.cjs +2 -1
  81. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  82. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  83. package/gsd-core/bin/lib/milestone.cjs +754 -70
  84. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  85. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  86. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  87. package/gsd-core/bin/lib/pattern.cjs +122 -0
  88. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  89. package/gsd-core/bin/lib/phase-id.cjs +444 -36
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  91. package/gsd-core/bin/lib/phase-locator.cjs +125 -18
  92. package/gsd-core/bin/lib/phase.cjs +646 -143
  93. package/gsd-core/bin/lib/plan-dependency-graph.cjs +72 -1
  94. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  95. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  96. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  97. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  98. package/gsd-core/bin/lib/planning-workspace.cjs +56 -6
  99. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  100. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  101. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  102. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +11 -6
  103. package/gsd-core/bin/lib/review-lane-descriptor.cjs +13 -4
  104. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  105. package/gsd-core/bin/lib/review-lane-runner.cjs +421 -66
  106. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  107. package/gsd-core/bin/lib/roadmap-command-router.cjs +34 -0
  108. package/gsd-core/bin/lib/roadmap-parser.cjs +943 -184
  109. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  110. package/gsd-core/bin/lib/roadmap.cjs +385 -94
  111. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +608 -46
  112. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  113. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +426 -55
  114. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  115. package/gsd-core/bin/lib/runtime-homes.cjs +69 -3
  116. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +115 -3
  117. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  118. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  119. package/gsd-core/bin/lib/security.cjs +104 -5
  120. package/gsd-core/bin/lib/shell-command-projection.cjs +275 -3
  121. package/gsd-core/bin/lib/smart-entry.cjs +142 -22
  122. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  123. package/gsd-core/bin/lib/state-document.cjs +152 -8
  124. package/gsd-core/bin/lib/state-transition.cjs +371 -117
  125. package/gsd-core/bin/lib/state.cjs +1794 -357
  126. package/gsd-core/bin/lib/surface.cjs +23 -9
  127. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  128. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  129. package/gsd-core/bin/lib/uat-predicate.cjs +9 -3
  130. package/gsd-core/bin/lib/uat.cjs +399 -56
  131. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  132. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  133. package/gsd-core/bin/lib/unusable-input.cjs +24 -0
  134. package/gsd-core/bin/lib/update-context.cjs +8 -2
  135. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  136. package/gsd-core/bin/lib/validate.cjs +20 -6
  137. package/gsd-core/bin/lib/vendor/README.md +37 -0
  138. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  139. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  140. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  141. package/gsd-core/bin/lib/verification.cjs +258 -8
  142. package/gsd-core/bin/lib/verify.cjs +368 -888
  143. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +53 -32
  144. package/gsd-core/bin/lib/workstream-inventory.cjs +63 -10
  145. package/gsd-core/bin/lib/workstream.cjs +2 -2
  146. package/gsd-core/bin/lib/worktree-safety.cjs +176 -9
  147. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  148. package/gsd-core/bin/shared/config-schema.manifest.json +7 -1
  149. package/gsd-core/references/agent-contracts.md +43 -26
  150. package/gsd-core/references/checkpoints.md +2 -2
  151. package/gsd-core/references/context-budget.md +1 -1
  152. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  153. package/gsd-core/references/doc-conflict-engine.md +1 -1
  154. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  155. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  156. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  157. package/gsd-core/references/execute-phase-response-language.md +1 -1
  158. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  159. package/gsd-core/references/gate-prompts.md +1 -1
  160. package/gsd-core/references/git-planning-commit.md +2 -1
  161. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  162. package/gsd-core/references/model-profiles.md +12 -4
  163. package/gsd-core/references/mvp-concepts.md +9 -9
  164. package/gsd-core/references/planner-guidance.md +3 -9
  165. package/gsd-core/references/planner-preconditions.md +1 -1
  166. package/gsd-core/references/planner-reviews.md +1 -1
  167. package/gsd-core/references/planning-config.md +8 -6
  168. package/gsd-core/references/revision-loop.md +1 -1
  169. package/gsd-core/references/specless-probe-fallback.md +1 -1
  170. package/gsd-core/references/universal-anti-patterns.md +3 -3
  171. package/gsd-core/references/verifier-phase-gates.md +192 -0
  172. package/gsd-core/references/verify-mvp-mode.md +1 -1
  173. package/gsd-core/references/workstream-flag.md +22 -6
  174. package/gsd-core/templates/discussion-log.md +1 -1
  175. package/gsd-core/templates/phase-prompt.md +2 -4
  176. package/gsd-core/templates/state.md +4 -4
  177. package/gsd-core/templates/verification-report.md +9 -1
  178. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  179. package/gsd-core/workflows/autonomous.md +1 -1
  180. package/gsd-core/workflows/cleanup.md +62 -3
  181. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +13 -3
  182. package/gsd-core/workflows/code-review-fix.md +37 -10
  183. package/gsd-core/workflows/code-review.md +38 -12
  184. package/gsd-core/workflows/complete-milestone.md +141 -18
  185. package/gsd-core/workflows/debug.md +7 -5
  186. package/gsd-core/workflows/diagnose-issues.md +35 -9
  187. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  188. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  189. package/gsd-core/workflows/discuss-phase-assumptions.md +2 -1
  190. package/gsd-core/workflows/edit-phase.md +26 -1
  191. package/gsd-core/workflows/eval-review.md +3 -5
  192. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +31 -6
  193. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  194. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +2 -0
  195. package/gsd-core/workflows/execute-phase.md +38 -50
  196. package/gsd-core/workflows/execute-plan.md +36 -4
  197. package/gsd-core/workflows/explore.md +131 -4
  198. package/gsd-core/workflows/fast.md +10 -2
  199. package/gsd-core/workflows/health.md +73 -4
  200. package/gsd-core/workflows/import.md +4 -4
  201. package/gsd-core/workflows/ingest-docs.md +5 -5
  202. package/gsd-core/workflows/mvp-phase.md +6 -3
  203. package/gsd-core/workflows/new-milestone.md +14 -9
  204. package/gsd-core/workflows/new-project.md +14 -14
  205. package/gsd-core/workflows/next.md +12 -0
  206. package/gsd-core/workflows/plan-phase.md +41 -17
  207. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  208. package/gsd-core/workflows/progress.md +34 -6
  209. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +4 -4
  210. package/gsd-core/workflows/quick/steps/quick-verification.md +27 -6
  211. package/gsd-core/workflows/quick/steps/research-phase.md +2 -2
  212. package/gsd-core/workflows/quick.md +35 -15
  213. package/gsd-core/workflows/review.md +26 -5
  214. package/gsd-core/workflows/secure-phase.md +1 -1
  215. package/gsd-core/workflows/session-report.md +2 -1
  216. package/gsd-core/workflows/settings.md +66 -2
  217. package/gsd-core/workflows/ship.md +104 -44
  218. package/gsd-core/workflows/spec-phase.md +30 -12
  219. package/gsd-core/workflows/sync-skills.md +63 -8
  220. package/gsd-core/workflows/transition.md +46 -11
  221. package/gsd-core/workflows/ui-phase.md +5 -5
  222. package/gsd-core/workflows/ui-review.md +2 -2
  223. package/gsd-core/workflows/update.md +1 -1
  224. package/gsd-core/workflows/validate-phase.md +1 -1
  225. package/gsd-core/workflows/verify-work.md +9 -7
  226. package/hooks/dist/gsd-agent-isolation-guard.js +103 -14
  227. package/hooks/dist/gsd-check-update-worker.js +56 -13
  228. package/hooks/dist/gsd-check-update.js +19 -1
  229. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  230. package/hooks/dist/gsd-cursor-subagent-start.js +77 -2
  231. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  232. package/hooks/dist/gsd-prompt-guard.js +21 -20
  233. package/hooks/dist/gsd-read-injection-scanner.js +38 -24
  234. package/hooks/dist/gsd-statusline.js +18 -0
  235. package/hooks/dist/gsd-update-banner.js +22 -1
  236. package/hooks/dist/gsd-workflow-guard.js +134 -36
  237. package/hooks/dist/lib/git-cmd.js +92 -59
  238. package/hooks/dist/lib/injection-patterns.js +45 -0
  239. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  240. package/hooks/dist/lib/isolation-sentinel.js +9 -0
  241. package/hooks/gsd-agent-isolation-guard.js +103 -14
  242. package/hooks/gsd-check-update-worker.js +56 -13
  243. package/hooks/gsd-check-update.js +19 -1
  244. package/hooks/gsd-cursor-pre-tool.js +0 -3
  245. package/hooks/gsd-cursor-subagent-start.js +77 -2
  246. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  247. package/hooks/gsd-prompt-guard.js +21 -20
  248. package/hooks/gsd-read-injection-scanner.js +38 -24
  249. package/hooks/gsd-statusline.js +18 -0
  250. package/hooks/gsd-update-banner.js +22 -1
  251. package/hooks/gsd-workflow-guard.js +134 -36
  252. package/hooks/lib/git-cmd.js +92 -59
  253. package/hooks/lib/injection-patterns.js +45 -0
  254. package/hooks/lib/isolation-deny-reason.js +39 -0
  255. package/hooks/lib/isolation-sentinel.js +9 -0
  256. package/package.json +21 -9
  257. package/pi/gsd.cjs +19 -5
  258. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  259. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  260. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  261. package/scripts/changeset/lint.cjs +60 -5
  262. package/scripts/check-alias-drift.cjs +7 -43
  263. package/scripts/check-contract-drift.cjs +297 -0
  264. package/scripts/ci-test-scope.cjs +19 -2
  265. package/scripts/command-contract-helpers.cjs +903 -1
  266. package/scripts/gen-adr-index.cjs +728 -38
  267. package/scripts/gen-capability-registry.cjs +3 -15
  268. package/scripts/gen-context-index.cjs +2 -11
  269. package/scripts/gen-health-docs.cjs +390 -0
  270. package/scripts/gen-inventory-manifest.cjs +50 -4
  271. package/scripts/gen-loop-host-contract.cjs +4 -24
  272. package/scripts/gen-registry.cjs +3 -14
  273. package/scripts/lib/alias-drift-families.cjs +46 -0
  274. package/scripts/lib/drift-scan.cjs +278 -0
  275. package/scripts/lint-allow-test-rule-refs.allowlist.json +1 -26
  276. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  277. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  278. package/scripts/lint-canary-version-leak.cjs +73 -0
  279. package/scripts/lint-command-contract.cjs +96 -13
  280. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  281. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  282. package/scripts/lint-default-flip-documentation.cjs +193 -0
  283. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  284. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  285. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  286. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  287. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  288. package/scripts/lint-milestone-window-drift.cjs +468 -0
  289. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  290. package/scripts/lint-plan-count-drift.cjs +318 -0
  291. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  292. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  293. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  294. package/scripts/lint-regression-test-names.cjs +15 -13
  295. package/scripts/lint-removed-but-needed.cjs +320 -0
  296. package/scripts/lint-state-field-drift.cjs +805 -0
  297. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  298. package/scripts/lint-test-file-count.allowlist.json +21 -10
  299. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  300. package/scripts/lint-vendored-deps.cjs +124 -0
  301. package/scripts/pr-changed-files.cjs +63 -0
  302. package/scripts/pr-template-policy.cjs +14 -4
  303. package/scripts/prompt-injection-scan.sh +25 -0
  304. package/scripts/require-issue-link-policy.cjs +192 -0
  305. package/scripts/state-write-path-drift-baseline.json +19 -0
  306. package/scripts/sync-runtime-launcher.cjs +2 -4
  307. package/skills/gsd-autonomous/SKILL.md +0 -1
  308. package/skills/gsd-code-review/SKILL.md +1 -1
  309. package/skills/gsd-execute-phase/SKILL.md +1 -2
  310. package/skills/gsd-map-codebase/SKILL.md +1 -1
  311. package/skills/gsd-mempalace-capture/SKILL.md +1 -1
  312. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  313. package/skills/gsd-new-milestone/SKILL.md +1 -1
  314. package/skills/gsd-next/SKILL.md +0 -1
  315. package/skills/gsd-plan-phase/SKILL.md +0 -1
  316. package/skills/gsd-progress/SKILL.md +0 -1
  317. package/skills/gsd-quick/SKILL.md +1 -1
  318. package/skills/gsd-review-backlog/SKILL.md +2 -1
  319. package/skills/gsd-stats/SKILL.md +0 -1
  320. package/skills/gsd-verify-work/SKILL.md +1 -1
  321. package/vscode/package.json +1 -1
  322. package/gsd-core/workflows/discovery-phase.md +0 -298
  323. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  324. package/gsd-core/workflows/verify-phase.md +0 -574
  325. package/scripts/affected-tests-lib.cjs +0 -554
  326. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  327. package/scripts/run-affected-tests.cjs +0 -7
  328. 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
 
@@ -727,6 +754,16 @@ const VALID_CONVERTER_NAMES = new Set([
727
754
  'convertClaudeAgentToCodexAgent',
728
755
  // ADR-1239 / #2092 Phase B Upgrade 1 — native .qwen/agents/*.md subagent projection.
729
756
  'convertClaudeAgentToQwenAgent',
757
+ // #3384 — ZCode agents are Claude-shaped but its dispatcher treats mcp__* tools
758
+ // grants as required MCP servers; this converter strips them at install time.
759
+ 'convertClaudeAgentToZcodeAgent',
760
+ // #2875 Part 2 (the agents-bypass closure) — data-driven Hermes branding
761
+ // converter (reads hostBehaviors.brandingRewrites rather than a hardcode),
762
+ // and the kilo/opencode agent converters (shared with those runtimes'
763
+ // commands-kind entries, options-bag signature `(content, {isAgent, modelOverride})`).
764
+ 'convertClaudeAgentToHermesAgent',
765
+ 'convertClaudeToKiloFrontmatter',
766
+ 'convertClaudeToOpencodeFrontmatter',
730
767
  ]);
731
768
 
732
769
  // C3: Validate role:runtime body
@@ -745,6 +782,23 @@ const VALID_EXTENSION_EVENTS = new Set(['opencode', 'pi', 'hermes', 'kilo', 'non
745
782
  const VALID_SANDBOX_TIERS = new Set(['none', 'codex-agent-sandbox']);
746
783
  const VALID_ARTIFACT_KIND_NAMES = new Set(['commands', 'agents', 'skills', 'kimi-agents']);
747
784
  const VALID_ARTIFACT_NESTINGS = new Set(['flat', 'nested']);
785
+ // #2871 Phase 2 — only `commands` and `skills` are trigger-bearing (a `/gsd-<name>`
786
+ // the USER types). `agents` and `kimi-agents` are a separate dispatch interface
787
+ // point (subagent invocation via `subagent_type`/named dispatch), never a trigger —
788
+ // see 40-design.md's "agents are not trigger-bearing" correction. A narrower set
789
+ // than VALID_ARTIFACT_KIND_NAMES on purpose: an artifact KIND can be agents; a
790
+ // trigger-precedence MEMBER never can.
791
+ const VALID_TRIGGER_PRECEDENCE_KINDS = new Set(['commands', 'skills']);
792
+ // The default runtime.triggerPrecedence (highest priority first) applied when a
793
+ // descriptor omits the axis (see validateRuntimeBody's required-with-default
794
+ // handling below). Not invented: `['skills', 'commands']` is the ordering every
795
+ // in-tree runtime that emits both kinds (from the same trigger stems) wants —
796
+ // claude's local/global collision and the same-scope collision (codebuddy, kilo,
797
+ // opencode, zcode) all resolve to skills winning. claude's shipped descriptor
798
+ // declares this SAME value explicitly, and
799
+ // tests/runtime-artifact-layout-trigger-surface.test.cjs asserts the two agree
800
+ // (a parity assertion — two surfaces reading one rule must not silently drift).
801
+ const DEFAULT_TRIGGER_PRECEDENCE = Object.freeze(['skills', 'commands']);
748
802
  const FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME = ['skills', 'agents', 'steps', 'contributions', 'gates', 'hooks', 'activationKey'];
749
803
  // 'none' added #2103 — Marketplace/VSIX-distributed hosts (e.g. VS Code) that
750
804
  // are never CLI-installed (no allRuntimes membership, no install flag).
@@ -851,7 +905,36 @@ const VALID_LANE_HANDLERS = new Set(['antigravity', 'openai-compatible', 'op
851
905
  // BOTH sub-shapes, or from NEITHER, has undefined meaning and fails validation.
852
906
  // The discriminator is explicit rather than inferred from field presence, which
853
907
  // is precisely the ambiguity these two sets exist to detect.
854
- const SPAWN_ONLY_INVOKE_FIELDS = ['binary', 'args', 'promptChannel', 'outputChannel', 'outputArg', 'modelArg'];
908
+ // `env` added by #2483. It belongs in THIS set and not merely in validateSpawnInvoke: an environment
909
+ // pair has no meaning for a transport that issues an HTTP POST, so a manifest declaring it alongside
910
+ // `openai-http` fields is the undefined-meaning case the comment above describes. Registering it here
911
+ // is what makes that case reportable — the openai-http arm rejects exactly the members of this list,
912
+ // so a field absent from it is silently accepted on the wrong transport. Note `effortChannel` is
913
+ // deliberately in NEITHER set: D2 defines it for both transports, so it is shared, not spawn-only.
914
+ const SPAWN_ONLY_INVOKE_FIELDS = ['binary', 'args', 'promptChannel', 'outputChannel', 'outputArg', 'modelArg', 'env'];
915
+ // Environment names refused outright on a reviewer lane (#2483): each one turns a declared pair into
916
+ // arbitrary code execution in the spawned child. DEFENCE IN DEPTH, NOT THE BOUNDARY — say so plainly,
917
+ // because a future reader who mistakes this for the control will under-invest in the one that is.
918
+ // The boundary is install-time consent: `capability-trust` discloses every declared env pair, shows
919
+ // its value, and binds it to the consent signature, so an unlisted name is still SEEN before it runs.
920
+ //
921
+ // Deliberately incomplete, and it cannot be otherwise: the spawned binary is arbitrary third-party
922
+ // code, so the exhaustive set is every interpreter's injection variables. What this list buys is that
923
+ // the highest-confidence, lowest-legitimacy routes cannot be taken quietly. `PATH` is included — it is
924
+ // the most complete primitive of the set (repoint it at a directory holding a fake binary) and no
925
+ // shipped reviewer manifest declares it; a lane that needs a specific executable declares an absolute
926
+ // `invoke.binary` rather than reshaping the child's `PATH`.
927
+ //
928
+ // Matched CASE-INSENSITIVELY (members stored uppercase) — Windows environment lookup is
929
+ // case-insensitive, so an exact-case set is bypassed by declaring `Path` or `node_options`.
930
+ const DENIED_LANE_ENV_KEYS = new Set([
931
+ 'PATH', 'NODE_OPTIONS', 'NODE_REPL_EXTERNAL_MODULE', 'NODE_PATH',
932
+ 'LD_PRELOAD', 'LD_AUDIT', 'LD_LIBRARY_PATH',
933
+ 'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH',
934
+ 'PYTHONSTARTUP', 'PYTHONPATH', 'BASH_ENV', 'ENV',
935
+ 'PERL5OPT', 'RUBYOPT', 'JAVA_TOOL_OPTIONS', '_JAVA_OPTIONS', 'CLASSPATH',
936
+ 'GIT_SSH_COMMAND', 'GIT_EXTERNAL_DIFF',
937
+ ]);
855
938
  // `defaultHost` / `fallbackModel` added by Phase 5b (#2799). Phase 4 federated every
856
939
  // `review.*_host` key with a default of `""`, so the REAL fallback destination and model
857
940
  // (`http://localhost:11434` / `llama3` and friends) existed only inside the bash leg. Once the
@@ -1557,6 +1640,45 @@ function validateRuntimeBody(cap) {
1557
1640
  }
1558
1641
  }
1559
1642
 
1643
+ // triggerPrecedence — #2871 Phase 2 amendment to ADR-1016's runtime body.
1644
+ // REQUIRED-WITH-DEFAULT: no other axis in this validator uses this shape —
1645
+ // every axis above either hard-requires the field (throws when absent) or
1646
+ // treats absence as fully unconstrained (effortSurface/isolation: "nothing to
1647
+ // validate" when undefined). This axis does neither: an ABSENT value is
1648
+ // substituted with DEFAULT_TRIGGER_PRECEDENCE and then validated exactly as
1649
+ // if it HAD been supplied, so a third-party capability.json authored before
1650
+ // this phase keeps validating (ADR-894 additive-only / Hyrum's Law, ADR-1244)
1651
+ // while a PRESENT-but-malformed value still fails loudly instead of silently
1652
+ // passing through unchecked.
1653
+ const triggerPrecedenceValue = Object.prototype.hasOwnProperty.call(r, 'triggerPrecedence')
1654
+ ? r.triggerPrecedence
1655
+ : DEFAULT_TRIGGER_PRECEDENCE;
1656
+ if (!Array.isArray(triggerPrecedenceValue)) {
1657
+ errors.push(
1658
+ 'runtime.triggerPrecedence must be an array of trigger-bearing kind names (' +
1659
+ [...VALID_TRIGGER_PRECEDENCE_KINDS].join(', ') + ') (got: ' + JSON.stringify(triggerPrecedenceValue) + ')',
1660
+ );
1661
+ } else if (triggerPrecedenceValue.length === 0) {
1662
+ errors.push('runtime.triggerPrecedence must not be empty');
1663
+ } else {
1664
+ const seenKinds = new Set();
1665
+ for (let i = 0; i < triggerPrecedenceValue.length; i++) {
1666
+ const k = triggerPrecedenceValue[i];
1667
+ if (k === '__proto__' || k === 'constructor' || k === 'prototype') {
1668
+ errors.push('runtime.triggerPrecedence[' + i + '] "' + k + '" is a reserved name');
1669
+ } else if (typeof k !== 'string' || !VALID_TRIGGER_PRECEDENCE_KINDS.has(k)) {
1670
+ errors.push(
1671
+ 'runtime.triggerPrecedence[' + i + '] must be one of: ' + [...VALID_TRIGGER_PRECEDENCE_KINDS].join(', ') +
1672
+ ' (got: ' + JSON.stringify(k) + ')',
1673
+ );
1674
+ } else if (seenKinds.has(k)) {
1675
+ errors.push('runtime.triggerPrecedence contains a duplicate kind: ' + JSON.stringify(k));
1676
+ } else {
1677
+ seenKinds.add(k);
1678
+ }
1679
+ }
1680
+ }
1681
+
1560
1682
  return errors;
1561
1683
  }
1562
1684
 
@@ -1578,6 +1700,122 @@ const KNOWN_REVIEWER_FIELDS = new Set([
1578
1700
  'handler',
1579
1701
  ]);
1580
1702
 
1703
+ /**
1704
+ * The closed `runtime.hostBehaviors` vocabulary (ADR-1016, closed via #2801).
1705
+ *
1706
+ * ADR-1016's core principle is that every per-runtime difference is a value over
1707
+ * a closed primitive vocabulary, and that a host needing a new shape gets a
1708
+ * reviewed first-party primitive rather than "an open escape hatch in the
1709
+ * descriptor" (§Alternatives #2 rejects exactly that). `hostBehaviors` was the
1710
+ * one hole left in that closure: 59 keys across 18 manifests, 39 of them set by
1711
+ * a single capability, validated by nothing. It was described in the reference
1712
+ * docs as a deliberate open seam sanctioned by ADR-1016 — ADR-1016 does not
1713
+ * mention `hostBehaviors` at all, and its stated principle is the opposite.
1714
+ *
1715
+ * Adding a key here is deliberate and reviewed. That friction IS the decision
1716
+ * (ADR-1016 §Consequences: "the closed vocabulary must grow (reviewed) when a
1717
+ * genuinely new host shape appears — intentional friction, the trust boundary").
1718
+ *
1719
+ * WARNING, never error. Two reasons:
1720
+ * 1. It matches the ADR-2782 D4.3 treatment of an unknown `reviewer` field, so
1721
+ * a manifest built against a newer GSD degrades visibly instead of failing
1722
+ * the build of a repo that merely reads it.
1723
+ * 2. An error would hard-break an out-of-tree descriptor carrying a bespoke
1724
+ * key, with no deprecation window — the exact mistake #2801's own alias
1725
+ * removal spent a full release avoiding. Escalating to an error is a later
1726
+ * step and needs its own window.
1727
+ *
1728
+ * Kept in sorted order, and `tests/reviewer-manifest-body.test.cjs` asserts this
1729
+ * set equals the keys the shipped manifests actually declare, so the list cannot
1730
+ * silently rot away from reality (DEFECT.GENERATIVE-FIX).
1731
+ */
1732
+ const KNOWN_HOST_BEHAVIORS = new Set([
1733
+ 'agentFileExtension',
1734
+ 'agentFrontmatterExtensions',
1735
+ 'agentManifestStyle',
1736
+ 'agentTomlFiles',
1737
+ 'attributionConfigResolver',
1738
+ 'attributionSource',
1739
+ 'authorsCanonicalWorkflow',
1740
+ 'brandingRewrites',
1741
+ 'cleanupSkillSidecars',
1742
+ 'clineRulesSurface',
1743
+ 'combinedFamilyInstall',
1744
+ 'commandBodyConverter',
1745
+ 'doneBannerStyle',
1746
+ 'flatCommandDir',
1747
+ 'frontmatterDialect',
1748
+ 'globalDirResolver',
1749
+ 'hookPathStyle',
1750
+ 'hooksJsonSurface',
1751
+ 'hyphenNameAgentBody',
1752
+ 'installsCommandBodiesForWorkflowDelegation',
1753
+ 'legacyCommandsGsdCleanup',
1754
+ 'legacyCommandsGsdInstallMigration',
1755
+ 'legacyCommandsGsdUninstall',
1756
+ 'legacyDevinSkillsCleanup',
1757
+ 'localCommandsViaRules',
1758
+ 'localInstallDeferred',
1759
+ 'localInstallStyle',
1760
+ 'localTargetIsProjectRoot',
1761
+ 'managedHookEvents',
1762
+ 'mcpCompanion',
1763
+ 'namedSubagentsSupported',
1764
+ 'nativeModelAliases',
1765
+ 'nativePlugin',
1766
+ 'noPathRewrite',
1767
+ 'ownsClaudePaths',
1768
+ 'permissionsSchema',
1769
+ 'pluginOnlyInstall',
1770
+ 'projectInstructionFile',
1771
+ 'reapplyCommand',
1772
+ 'reportCommandsDir',
1773
+ 'reportSkillsCount',
1774
+ 'retiredArtifacts',
1775
+ 'settingsFileByScope',
1776
+ 'sharedHooksDirName',
1777
+ 'skillFrontmatterVersion',
1778
+ 'skillPriorityFrontmatter',
1779
+ 'skillsGlobalOnboarding',
1780
+ 'skillsManifestPrefix',
1781
+ 'skipCodexSkillsManifest',
1782
+ 'skipHomePrefixSubstitution',
1783
+ 'skipSettingsUi',
1784
+ 'skipSharedHooksInstall',
1785
+ 'skipUpdateBannerCommand',
1786
+ 'soloStageMetadata',
1787
+ 'sourceMarkerFile',
1788
+ 'tomlConfigInstall',
1789
+ 'trackCategoryDescription',
1790
+ 'verificationStyle',
1791
+ 'writeCategoryDescription',
1792
+ ]);
1793
+
1794
+ /**
1795
+ * Frozen reason codes for the non-fatal reviewer diagnostics (ADR-2782 D4.3).
1796
+ *
1797
+ * The IR behind `collectReviewerWarnings`' rendered strings. Tests assert on
1798
+ * these codes; the rendered `message` is operator console output and tests must
1799
+ * not depend on it (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test
1800
+ * Outputs"), which is the same split `bin/verify-reapply-patches.cjs` uses.
1801
+ *
1802
+ * Adding a code is THREE coordinated changes: this enum, the emitting site in
1803
+ * `collectReviewerWarningRecordFields`, and the test that locks
1804
+ * `Object.keys(REVIEWER_WARNING).sort()`. That coupling is the point — it stops
1805
+ * the code surface drifting from the test surface.
1806
+ */
1807
+ const REVIEWER_WARNING = Object.freeze({
1808
+ /** A key inside a `reviewer` body that this GSD version does not know. */
1809
+ UNKNOWN_REVIEWER_FIELD: 'unknown_reviewer_field',
1810
+ /** A `runtime.hostBehaviors` key that was removed from the vocabulary. */
1811
+ REMOVED_HOST_BEHAVIOR: 'removed_host_behavior',
1812
+ /** A `runtime.hostBehaviors` key outside the closed vocabulary. */
1813
+ UNKNOWN_HOST_BEHAVIOR: 'unknown_host_behavior',
1814
+ });
1815
+
1816
+ /** Dotted path of the field removed by ADR-2782 D9 / #2801. */
1817
+ const REMOVED_REVIEWER_CLI_FIELD = 'runtime.hostBehaviors.reviewerCli';
1818
+
1581
1819
  const KNOWN_PROBE_FIELDS = new Set(['kind', 'binary', 'needle', 'timeoutMs', 'hostConfigKey', 'path']);
1582
1820
 
1583
1821
  /** A bounded probe timeout must be a finite, positive INTEGER of milliseconds. */
@@ -1615,6 +1853,41 @@ function describeValue(v) {
1615
1853
  }
1616
1854
  }
1617
1855
 
1856
+ /**
1857
+ * Ceiling on how many undeclared-key diagnostics one capability may produce.
1858
+ *
1859
+ * The loops below iterate MANIFEST-SUPPLIED keys, and an installed third-party
1860
+ * manifest is bounded only by MANIFEST_MAX_BYTES (8MB). Unbounded, one manifest
1861
+ * of 800k keys yields 800k records and ~139MB of message text, retained for the
1862
+ * registry's lifetime in OverlayMeta.diagnostics. Ten is enough to act on; the
1863
+ * rest are summarized. Mirrors the existing truncation idiom in
1864
+ * `capability-loader.cts` (`crossErrs.slice(0, 3)`).
1865
+ */
1866
+ const MAX_REPORTED_UNKNOWN_KEYS = 10;
1867
+
1868
+ /** Ceiling on how much of one manifest-supplied key name a diagnostic repeats. */
1869
+ const MAX_REPORTED_KEY_CHARS = 80;
1870
+
1871
+ /**
1872
+ * Render a manifest-supplied KEY for a diagnostic: control-safe and bounded.
1873
+ *
1874
+ * Key names carry no grammar anywhere — unlike `cap.id`, which `validateCapability`
1875
+ * gates on KEBAB_RE before these diagnostics run — so a key is fully
1876
+ * attacker-controlled text heading for stderr and OverlayMeta.warnings. C0/C1
1877
+ * controls (ESC, CR, LF) become U+FFFD so a key cannot emit terminal escapes or
1878
+ * forge a log line, and the result is clipped so one key cannot carry megabytes
1879
+ * into a retained diagnostic.
1880
+ *
1881
+ * @param {*} key
1882
+ * @returns {string}
1883
+ */
1884
+ function describeKey(key) {
1885
+ const raw = typeof key === 'string' ? key : String(key);
1886
+ // eslint-disable-next-line no-control-regex
1887
+ const safe = raw.replace(/[\x00-\x1f\x7f-\x9f]/g, '�');
1888
+ return safe.length > MAX_REPORTED_KEY_CHARS ? safe.slice(0, MAX_REPORTED_KEY_CHARS) + '…' : safe;
1889
+ }
1890
+
1618
1891
  /** Extract a message from an unknown thrown value without throwing again. */
1619
1892
  function safeErrorMessage(err) {
1620
1893
  try {
@@ -1661,7 +1934,39 @@ function validateEnumField(ctx, label, value, validSet) {
1661
1934
  }
1662
1935
 
1663
1936
  /**
1664
- * Collect NON-FATAL diagnostics for a reviewer body (ADR-2782 D4.3).
1937
+ * Collect NON-FATAL reviewer diagnostics for a capability manifest as TYPED
1938
+ * RECORDS (ADR-2782 D4.3, plus the D9 `hostBehaviors.reviewerCli` removal
1939
+ * notice, #2801).
1940
+ *
1941
+ * This is the IR. `collectReviewerWarnings` below renders it to strings for the
1942
+ * two production consumers; tests assert on these records instead of matching
1943
+ * the rendered prose (CONTRIBUTING.md, "Prohibited: Raw Text Matching on Test
1944
+ * Outputs").
1945
+ *
1946
+ * Record shape — `code` and `capId` are always present; the rest is per-code:
1947
+ * { code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
1948
+ * capId, field: 'reviewer.<key>', knownFields: string[], message }
1949
+ * { code: REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR,
1950
+ * capId, field: 'runtime.hostBehaviors.reviewerCli',
1951
+ * replacement: 'reviewer', docs: '<how-to path>', message }
1952
+ *
1953
+ * TOTAL: returns an array for ANY input and never throws. It runs on
1954
+ * loadRegistry's ACCEPT path, so a diagnostic that throws would cost the user a
1955
+ * lane that is otherwise perfectly valid (#1461 OVL-1).
1956
+ *
1957
+ * @param {object} cap A capability manifest.
1958
+ * @returns {Array<object>} Warning records; empty when there is nothing to say.
1959
+ */
1960
+ function collectReviewerWarningRecords(cap) {
1961
+ try {
1962
+ return collectReviewerWarningRecordFields(cap);
1963
+ } catch {
1964
+ return [];
1965
+ }
1966
+ }
1967
+
1968
+ /**
1969
+ * Render the IR for operator console output.
1665
1970
  *
1666
1971
  * Kept separate from validateReviewerBody so validateCapability's contract
1667
1972
  * (`=> string[]` of ERRORS) is unchanged for its two existing callers. The
@@ -1669,35 +1974,135 @@ function validateEnumField(ctx, label, value, validSet) {
1669
1974
  * through OverlayMeta.warnings. A warning written only to a build log nobody
1670
1975
  * reads is not a warning (ADR-2782 D4, "Where warnings surface").
1671
1976
  *
1977
+ * The `string[]` shape is load-bearing for both consumers and does not change.
1978
+ *
1672
1979
  * @param {object} cap A capability manifest.
1673
1980
  * @returns {string[]} Warning strings; empty when there is nothing to say.
1674
1981
  */
1675
1982
  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
- }
1983
+ return collectReviewerWarningRecords(cap).map((record) => record.message);
1684
1984
  }
1685
1985
 
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;
1986
+ function collectReviewerWarningRecordFields(cap) {
1987
+ const records = [];
1988
+ if (typeof cap !== 'object' || cap === null || Array.isArray(cap)) return records;
1691
1989
 
1692
1990
  const capId = typeof cap.id === 'string' ? cap.id : '(unknown)';
1991
+
1992
+ // ADR-2782 D9 / #2801 — the REMOVED `runtime.hostBehaviors.reviewerCli` alias.
1993
+ //
1994
+ // Emitted BEFORE the `reviewer`-body early-return below, deliberately. The
1995
+ // manifest this notice exists for is the ALIAS-ONLY one, which by definition
1996
+ // carries no body; after that guard the check would fire only for
1997
+ // capabilities that already declare a lane — exactly the set that does not
1998
+ // need telling.
1999
+ //
2000
+ // Presence-based, not `=== true`: the key is unknown at ANY value now, which
2001
+ // is the same rule the unknown-`reviewer.*`-field loop below applies. Own-key
2002
+ // read, so a polluted prototype cannot manufacture this warning on every
2003
+ // otherwise-innocent manifest. This is ONE keyed removal notice, not general
2004
+ // `hostBehaviors` validation — the closed vocabulary below handles that (#2801).
2005
+ const runtimeBody = cap.runtime;
2006
+ if (typeof runtimeBody === 'object' && runtimeBody !== null && !Array.isArray(runtimeBody)) {
2007
+ const hostBehaviors = runtimeBody.hostBehaviors;
2008
+ if (
2009
+ typeof hostBehaviors === 'object'
2010
+ && hostBehaviors !== null
2011
+ && !Array.isArray(hostBehaviors)
2012
+ && Object.prototype.hasOwnProperty.call(hostBehaviors, 'reviewerCli')
2013
+ ) {
2014
+ records.push({
2015
+ code: REVIEWER_WARNING.REMOVED_HOST_BEHAVIOR,
2016
+ capId,
2017
+ field: REMOVED_REVIEWER_CLI_FIELD,
2018
+ replacement: 'reviewer',
2019
+ docs: 'docs/how-to/ship-a-reviewer-lane.md',
2020
+ message:
2021
+ '⚠ capability "' + capId + '" ' + REMOVED_REVIEWER_CLI_FIELD + ' was removed (ADR-2782 D9) ' +
2022
+ '— ignored, and it contributes no reviewer lane. Declare a `reviewer` body instead; see ' +
2023
+ 'docs/how-to/ship-a-reviewer-lane.md',
2024
+ });
2025
+ }
2026
+
2027
+ // #2801 — the closed `hostBehaviors` vocabulary (ADR-1016). `reviewerCli` is
2028
+ // excluded here: it already drew its own removal notice above, and a second,
2029
+ // generic "unknown key" record for the same key would be noise, not signal.
2030
+ if (typeof hostBehaviors === 'object' && hostBehaviors !== null && !Array.isArray(hostBehaviors)) {
2031
+ let reported = 0;
2032
+ let omitted = 0;
2033
+ for (const key of Object.keys(hostBehaviors)) {
2034
+ if (isReservedName(key) || key === 'reviewerCli' || KNOWN_HOST_BEHAVIORS.has(key)) continue;
2035
+ if (reported >= MAX_REPORTED_UNKNOWN_KEYS) {
2036
+ omitted += 1;
2037
+ continue;
2038
+ }
2039
+ reported += 1;
2040
+ const safeKey = describeKey(key);
2041
+ records.push({
2042
+ code: REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR,
2043
+ capId,
2044
+ field: 'runtime.hostBehaviors.' + safeKey,
2045
+ message:
2046
+ '⚠ capability "' + capId + '" runtime.hostBehaviors.' + safeKey + ' is not a known host behavior ' +
2047
+ 'in this GSD version — ignored. Adding one is a reviewed first-party change (ADR-1016).',
2048
+ });
2049
+ }
2050
+ if (omitted > 0) {
2051
+ records.push({
2052
+ code: REVIEWER_WARNING.UNKNOWN_HOST_BEHAVIOR,
2053
+ capId,
2054
+ field: 'runtime.hostBehaviors',
2055
+ truncated: true,
2056
+ omittedCount: omitted,
2057
+ message:
2058
+ '⚠ capability "' + capId + '" declares ' + omitted + ' further unknown runtime.hostBehaviors ' +
2059
+ 'key(s), not listed. A manifest this far outside the vocabulary is likely built for a ' +
2060
+ 'different GSD version.',
2061
+ });
2062
+ }
2063
+ }
2064
+ }
2065
+
2066
+ const r = cap.reviewer;
2067
+ if (typeof r !== 'object' || r === null || Array.isArray(r)) return records;
2068
+
2069
+ // Same ceiling and the same key sanitization as the hostBehaviors sweep above.
2070
+ // This loop predates #2801 and carried both defects; fixing only the new copy
2071
+ // would leave the identical defect one screen away from its own fix.
2072
+ let reportedFields = 0;
2073
+ let omittedFields = 0;
1693
2074
  for (const key of Object.keys(r)) {
1694
2075
  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;
2076
+ if (reportedFields >= MAX_REPORTED_UNKNOWN_KEYS) {
2077
+ omittedFields += 1;
2078
+ continue;
2079
+ }
2080
+ reportedFields += 1;
2081
+ const safeKey = describeKey(key);
2082
+ records.push({
2083
+ code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
2084
+ capId,
2085
+ field: 'reviewer.' + safeKey,
2086
+ knownFields: [...KNOWN_REVIEWER_FIELDS],
2087
+ message:
2088
+ '⚠ capability "' + capId + '" reviewer.' + safeKey + ' is not a known reviewer field ' +
2089
+ 'in this GSD version — ignored. Known fields: ' + [...KNOWN_REVIEWER_FIELDS].join(', '),
2090
+ });
2091
+ }
2092
+ if (omittedFields > 0) {
2093
+ records.push({
2094
+ code: REVIEWER_WARNING.UNKNOWN_REVIEWER_FIELD,
2095
+ capId,
2096
+ field: 'reviewer',
2097
+ knownFields: [...KNOWN_REVIEWER_FIELDS],
2098
+ truncated: true,
2099
+ omittedCount: omittedFields,
2100
+ message:
2101
+ '⚠ capability "' + capId + '" declares ' + omittedFields + ' further unknown reviewer field(s), ' +
2102
+ 'not listed.',
2103
+ });
2104
+ }
2105
+ return records;
1701
2106
  }
1702
2107
 
1703
2108
  /**
@@ -2046,6 +2451,66 @@ function validateSpawnInvoke(ctx, invoke) {
2046
2451
 
2047
2452
  errors.push(...validateEnumField(ctx, 'reviewer.invoke.effortChannel', invoke.effortChannel, VALID_LANE_EFFORT_CHANNELS));
2048
2453
 
2454
+ // `env` — per-invocation environment pairs (#2483). OPTIONAL, unlike every field above: only a lane
2455
+ // that needs to shape its child's environment declares it, and absent is the common case. Validated
2456
+ // when present, because `resolveLanePlan` DROPS a non-string value rather than coercing it — so an
2457
+ // unvalidated manifest declares a pair that silently never reaches the spawn, which is the failure
2458
+ // mode a shipped env-guard can least afford. Keys are held to the portable POSIX
2459
+ // environment-name grammar, which is a POLICY — not a claim about what an environment can physically
2460
+ // hold. Measured: of the names refused below only NUL is actually rejected by `spawnSync`; `=`, a
2461
+ // leading digit, a dash and a space are all carried through to the child (an `A=B` key arrives as
2462
+ // the raw entry `A=B=value`, and reads back via `process.env['A=B']`). They are refused because a
2463
+ // name outside the grammar is not portably addressable by the program meant to read it.
2464
+ if (invoke.env !== undefined) {
2465
+ if (typeof invoke.env !== 'object' || invoke.env === null || Array.isArray(invoke.env)) {
2466
+ errors.push(
2467
+ ctx + ' reviewer.invoke.env must be an object of environment name/value pairs ' +
2468
+ '(got: ' + describeValue(invoke.env) + ')',
2469
+ );
2470
+ } else {
2471
+ for (const key of Object.keys(invoke.env)) {
2472
+ // `__proto__` passes the grammar below and IS a real own key once a manifest is JSON-parsed,
2473
+ // but assigning it onto a plain accumulator goes through the inherited `__proto__` SETTER
2474
+ // instead of creating an own property. For the string values this field permits the setter is
2475
+ // a no-op — the prototype is not even changed — so the pair would validate and then simply
2476
+ // vanish before the spawn: the declared-but-never-delivered failure this block exists to catch.
2477
+ // Refused by name, because the grammar cannot see it.
2478
+ //
2479
+ // Only `__proto__` needs this. Sibling reserved-name guards in this file reject
2480
+ // `constructor`/`prototype` alongside it, but those guard bracket LOOKUPS that resolve
2481
+ // prototype members; here the read is `Object.keys` + an own-value read, and `constructor`
2482
+ // assigns as an ordinary own key. Refusing it too would reject a name the spawn could carry.
2483
+ // CASE-INSENSITIVE, and that is not pedantry: Windows environment lookups are
2484
+ // case-insensitive, so `Path` / `node_options` reach the child as `PATH` / `NODE_OPTIONS`
2485
+ // and an exact-case set is bypassed by changing one letter. The grammar above already
2486
+ // constrains keys to ASCII, so a plain uppercase fold is sufficient here.
2487
+ if (DENIED_LANE_ENV_KEYS.has(key.toUpperCase())) {
2488
+ errors.push(
2489
+ ctx + ' reviewer.invoke.env key "' + key + '" is not permitted ' +
2490
+ '(it makes the spawned reviewer run code of the manifest\'s choosing; ' +
2491
+ 'declare an absolute `invoke.binary` instead of reshaping the child\'s environment)',
2492
+ );
2493
+ } else if (key === '__proto__') {
2494
+ errors.push(
2495
+ ctx + ' reviewer.invoke.env key "__proto__" is not permitted ' +
2496
+ '(it is silently dropped when the spawn plan is assembled, so it would never reach the child)',
2497
+ );
2498
+ } else if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
2499
+ errors.push(
2500
+ ctx + ' reviewer.invoke.env key ' + describeValue(key) +
2501
+ ' is not a valid environment variable name',
2502
+ );
2503
+ }
2504
+ if (typeof invoke.env[key] !== 'string') {
2505
+ errors.push(
2506
+ ctx + ' reviewer.invoke.env.' + key + ' must be a string ' +
2507
+ '(got: ' + describeValue(invoke.env[key]) + ')',
2508
+ );
2509
+ }
2510
+ }
2511
+ }
2512
+ }
2513
+
2049
2514
  return errors;
2050
2515
  }
2051
2516
 
@@ -3155,6 +3620,8 @@ module.exports = {
3155
3620
  VALID_SANDBOX_TIERS,
3156
3621
  VALID_ARTIFACT_KIND_NAMES,
3157
3622
  VALID_ARTIFACT_NESTINGS,
3623
+ VALID_TRIGGER_PRECEDENCE_KINDS,
3624
+ DEFAULT_TRIGGER_PRECEDENCE,
3158
3625
  FEATURE_FIELDS_FORBIDDEN_ON_RUNTIME,
3159
3626
  // ADR-2782 D1/D2/D3/D6/D7/D8 — reviewer lane body
3160
3627
  FEATURE_FIELDS_FORBIDDEN_ON_REVIEWER,
@@ -3170,8 +3637,14 @@ module.exports = {
3170
3637
  VALID_EVIDENCE_CLASSES,
3171
3638
  VALID_LANE_HANDLERS,
3172
3639
  KNOWN_REVIEWER_FIELDS,
3640
+ KNOWN_HOST_BEHAVIORS,
3641
+ MAX_REPORTED_UNKNOWN_KEYS,
3642
+ MAX_REPORTED_KEY_CHARS,
3173
3643
  validateReviewerBody,
3174
3644
  collectReviewerWarnings,
3645
+ collectReviewerWarningRecords,
3646
+ REVIEWER_WARNING,
3647
+ REMOVED_REVIEWER_CLI_FIELD,
3175
3648
  VALID_INSTALL_SURFACES,
3176
3649
  VALID_PERMISSION_WRITERS,
3177
3650
  VALID_EXTENDED_HOOK_EVENTS,
@@ -270,8 +270,9 @@ function setCapabilityState(cwd, runtimeConfigDir, desired, opts) {
270
270
  // access to getCommitAttribution (which lives in bin/install.js). Until that
271
271
  // is refactored into a shared module, surface-path agents for descriptor-
272
272
  // driven runtimes will lack the Co-Authored-By trailer that install adds.
273
- // Parity is proven when resolveAttribution IS provided (see
274
- // tests/issue-1575-agent-descriptor-parity.test.cjs).
273
+ // Parity is proven when resolveAttribution IS provided (see the
274
+ // folded:issue-1575-agent-descriptor-parity describe block in
275
+ // tests/golden-parity-single-source.test.cjs).
275
276
  // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
276
277
  applySurface(resolvedConfigDir, layout, manifest, undefined, registry, opts?.materialize?.resolveAttribution
277
278
  ? { resolveAttribution: opts.materialize.resolveAttribution }