@danmoisan/drm-copilot-mcp 1.1.18 → 1.1.19

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 (206) hide show
  1. package/out/mcp-server.js +989 -662
  2. package/package.json +3 -2
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +11 -0
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +10 -0
  5. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +12 -4
  6. package/resources/claude-customizations/.claude/agents/epic-planner.md +5 -4
  7. package/resources/claude-customizations/.claude/agents/feature-review.md +6 -4
  8. package/resources/claude-customizations/.claude/agents/orchestrator.md +5 -4
  9. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +31 -13
  10. package/resources/claude-customizations/.claude/agents/parallel-planner.md +9 -6
  11. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -2
  12. package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -1
  13. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +7 -1
  14. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +7 -1
  15. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
  16. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +111 -46
  17. package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +105 -0
  18. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +8 -1
  19. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +7 -1
  20. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +116 -59
  21. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +55 -23
  22. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +74 -75
  23. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +127 -35
  24. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +61 -57
  25. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +7 -1
  26. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +129 -39
  27. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +7 -1
  28. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +9 -3
  29. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +39 -51
  30. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
  31. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
  32. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
  33. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +17 -12
  34. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +9 -3
  35. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +65 -50
  36. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +49 -42
  37. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +54 -72
  38. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +8 -2
  39. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +279 -0
  40. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +129 -75
  41. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.artifact-root.ps1 +233 -0
  42. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +224 -0
  43. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +69 -24
  44. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +29 -18
  45. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +10 -4
  46. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +14 -5
  47. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +8 -2
  48. package/resources/claude-customizations/.claude/hooks/feature-folder-resolution.ps1 +373 -0
  49. package/resources/claude-customizations/.claude/hooks/feature-review-coverage-thresholds.ps1 +85 -0
  50. package/resources/claude-customizations/.claude/hooks/hook-command-heredoc.ps1 +119 -0
  51. package/resources/claude-customizations/.claude/hooks/hook-command-invocation-operands.ps1 +198 -0
  52. package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +254 -255
  53. package/resources/claude-customizations/.claude/hooks/hook-command-payload-powershell.ps1 +201 -0
  54. package/resources/claude-customizations/.claude/hooks/hook-command-payload.ps1 +484 -0
  55. package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +27 -110
  56. package/resources/claude-customizations/.claude/hooks/hook-dependency-guard.ps1 +114 -0
  57. package/resources/claude-customizations/.claude/hooks/hook-docs-only-diff.ps1 +493 -0
  58. package/resources/claude-customizations/.claude/hooks/subagentstop-envelope.ps1 +192 -0
  59. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +9 -3
  60. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +40 -42
  61. package/resources/claude-customizations/.claude/hooks/validate-executor-output.ps1 +66 -38
  62. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-metrics.ps1 +180 -0
  63. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-rules.ps1 +195 -0
  64. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage.ps1 +141 -395
  65. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-resolution.ps1 +311 -0
  66. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-routing.ps1 +170 -0
  67. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +115 -190
  68. package/resources/claude-customizations/.claude/hooks/validate-planner-output.ps1 +64 -32
  69. package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +39 -44
  70. package/resources/claude-customizations/.claude/hooks/validate-prd-feature-output.ps1 +43 -14
  71. package/resources/claude-customizations/.claude/hooks/validate-required-artifact-output.ps1 +66 -36
  72. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +31 -34
  73. package/resources/claude-customizations/.claude/lib/bash/mutate-parallel-run.sh +191 -0
  74. package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +13 -3
  75. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-cli.sh +152 -0
  76. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-ops.sh +193 -0
  77. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation.sh +322 -0
  78. package/resources/claude-customizations/.claude/lib/bash/remove-parallel-item.sh +146 -0
  79. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +23 -33
  80. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +86 -6
  81. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +91 -19
  82. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateEpicWaveBarrier.psm1 +346 -0
  83. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +24 -3
  84. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +79 -2
  85. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +3 -3
  86. package/resources/claude-customizations/.claude/rules/architecture-boundaries.md +5 -5
  87. package/resources/claude-customizations/.claude/rules/csharp.md +2 -2
  88. package/resources/claude-customizations/.claude/rules/general-code-change.md +1 -1
  89. package/resources/claude-customizations/.claude/rules/general-unit-test.md +3 -2
  90. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +37 -4
  91. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +50 -5
  92. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +47 -8
  93. package/resources/claude-customizations/.claude/rules/quality-tiers.md +11 -7
  94. package/resources/claude-customizations/.claude/rules/shell.md +10 -8
  95. package/resources/claude-customizations/.claude/rules/typescript.md +1 -1
  96. package/resources/claude-customizations/.claude/settings.json +3 -4
  97. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +1 -1
  98. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +5 -1
  99. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +1 -1
  100. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +2 -0
  101. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +2 -0
  102. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +33 -6
  103. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +1 -1
  104. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +10 -1
  105. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  106. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +4 -3
  107. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +14 -6
  108. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +52 -14
  109. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +22 -9
  110. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +79 -16
  111. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +63 -7
  112. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +35 -12
  113. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +3 -1
  114. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +1 -1
  115. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +2 -2
  116. package/resources/claude-customizations/pack-manifests/core.json +23 -0
  117. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +1 -1
  118. package/resources/codex-and-agents-customizations/.agents/skills/architecture-boundaries/SKILL.md +5 -5
  119. package/resources/codex-and-agents-customizations/.agents/skills/atomic-executor/SKILL.md +11 -0
  120. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +4 -0
  121. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +10 -0
  122. package/resources/codex-and-agents-customizations/.agents/skills/csharp/SKILL.md +2 -2
  123. package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +2 -2
  124. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +8 -0
  125. package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +17 -1
  126. package/resources/codex-and-agents-customizations/.agents/skills/epic-run/SKILL.md +2 -1
  127. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +10 -1
  128. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +13 -5
  129. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +1 -1
  130. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +3 -2
  131. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +15 -6
  132. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +1 -1
  133. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +11 -7
  134. package/resources/codex-and-agents-customizations/.agents/skills/translate-claude-to-codex/SKILL.md +1 -1
  135. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +2 -2
  136. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +2 -2
  137. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +11 -0
  138. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +11 -0
  139. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +11 -0
  140. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +11 -0
  141. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +11 -0
  142. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +11 -0
  143. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +10 -0
  144. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +10 -0
  145. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +10 -0
  146. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +10 -0
  147. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +10 -0
  148. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +10 -0
  149. package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +2 -1
  150. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +6 -6
  151. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +2 -2
  152. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +2 -2
  153. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +2 -2
  154. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +2 -2
  155. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +2 -2
  156. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +2 -2
  157. package/resources/codex-and-agents-customizations/.codex/codex-web-setup.sh +318 -273
  158. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  159. package/resources/codex-and-agents-customizations/.codex/hooks/check-powershell-test-purity.ps1 +7 -1
  160. package/resources/codex-and-agents-customizations/.codex/hooks/check-python-test-purity.ps1 +7 -1
  161. package/resources/codex-and-agents-customizations/.codex/hooks/codex-epic-child-launch-attestation.ps1 +1 -1
  162. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
  163. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +8 -2
  164. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +97 -44
  165. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +105 -0
  166. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-child-worktree-binding.ps1 +7 -1
  167. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +37 -5
  168. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +6 -0
  169. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-root-invocation.ps1 +7 -1
  170. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-wave-barrier.ps1 +7 -1
  171. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +54 -41
  172. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1 +7 -1
  173. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +35 -14
  174. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
  175. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
  176. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
  177. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +24 -11
  178. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +8 -2
  179. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +13 -4
  180. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +8 -2
  181. package/resources/codex-and-agents-customizations/.codex/hooks/feature-folder-resolution.ps1 +373 -0
  182. package/resources/codex-and-agents-customizations/.codex/hooks/feature-review-coverage-thresholds.ps1 +85 -0
  183. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-heredoc.ps1 +119 -0
  184. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation-operands.ps1 +198 -0
  185. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +254 -255
  186. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload-powershell.ps1 +201 -0
  187. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload.ps1 +484 -0
  188. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +27 -110
  189. package/resources/codex-and-agents-customizations/.codex/hooks/hook-dependency-guard.ps1 +114 -0
  190. package/resources/codex-and-agents-customizations/.codex/hooks/hook-docs-only-diff.ps1 +493 -0
  191. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +8 -2
  192. package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +7 -1
  193. package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +24 -8
  194. package/resources/codex-and-agents-customizations/pack-manifests/core.json +9 -0
  195. package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +2 -2
  196. package/resources/customizations/.github/instructions/csharp-code-change.instructions.md +4 -4
  197. package/resources/customizations/.github/instructions/csharp-unit-test.instructions.md +2 -2
  198. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +1 -1
  199. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +4 -0
  200. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +10 -1
  201. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +1 -1
  202. package/resources/powershell/PoshQC/PoshQC.Coverage.psm1 +314 -0
  203. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +25 -28
  204. package/resources/powershell/PoshQC/PoshQC.psm1 +1 -0
  205. package/resources/powershell/PoshQC/README.md +8 -2
  206. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -319
@@ -0,0 +1,493 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Docs-only diff classification and the merge-side docs-only exemption (issue #927).
4
+
5
+ .DESCRIPTION
6
+ Shared by enforce-pr-author-skill.docs-only.ps1 (create side) and by the Claude and Codex
7
+ copies of enforce-epic-merge-gate.ps1 (merge side). The file defines functions only: it
8
+ imports nothing, dot-sources nothing, and has no entry point. At call time
9
+ Get-DocsOnlyMergeExemption uses Read-CommandLineSegment, Test-CommandLineSegmentRawScan,
10
+ Get-CommandLineStructuralMatch, Test-CommandLineFlag, and Get-CommandLineFlagValue from
11
+ the command parsers every consumer already loads.
12
+
13
+ Docs-only merge branch: a merge gate calls Get-DocsOnlyMergeExemption only for an explicit
14
+ pull request number that no checkpoint records. The exemption fails closed; conditions
15
+ are evaluated in order and the first failure returns its code:
16
+
17
+ M1 DOCS_ONLY_EXEMPTION_PR_UNRESOLVED - a positive number; exactly one segment that is
18
+ not wrapper-led, has no live substitution, and
19
+ is balanced; a structural gh pr merge; no inline
20
+ VAR= prefix token (GH_REPO=, GH_HOST=); no
21
+ single-dash token containing R; no --repo or
22
+ --repo= token; no --auto token.
23
+ M2 DOCS_ONLY_EXEMPTION_PR_UNRESOLVED - the worktree root is an existing directory.
24
+ M3 DOCS_ONLY_EXEMPTION_PR_UNRESOLVED - gh pr view <N> --json baseRefName,headRefOid
25
+ exits 0 and returns a JSON object; headRefOid
26
+ is 40 or 64 lowercase hex; a --match-head-commit
27
+ value, when present, equals headRefOid (OD-2).
28
+ DOCS_ONLY_EXEMPTION_BASE_UNRESOLVED - baseRefName is a conservative branch spelling
29
+ without '..'.
30
+ M4 DOCS_ONLY_EXEMPTION_BASE_UNRESOLVED - refs/remotes/origin/<base> resolves to a commit.
31
+ M5 DOCS_ONLY_EXEMPTION_DIFF_FAILED - headRefOid is present in the local repository.
32
+ M6 DOCS_ONLY_EXEMPTION_DIFF_FAILED - git diff --name-only -z --no-renames --no-color
33
+ --no-ext-diff --no-relative <base>...<head>
34
+ exits 0.
35
+ M7 DOCS_ONLY_EXEMPTION_DIFF_FAILED - non-empty diff output ends with NUL.
36
+ M8 DOCS_ONLY_EXEMPTION_EMPTY_DIFF - the diff lists at least one path.
37
+ M9 DOCS_ONLY_EXEMPTION_NON_DOCS_PATH - every listed path starts with docs/ (ordinal).
38
+
39
+ No-fetch policy: no function here fetches or writes a ref. A missing origin/<base> ref or
40
+ a head commit that is not present locally denies, and the M5 reason names the command
41
+ the operator can run before retrying.
42
+
43
+ Residual limits: a GH_REPO or GH_HOST value exported by an earlier shell command or
44
+ inherited from the session environment is not visible in the command text; gh and git
45
+ run in the WorktreeRoot the gate passes (the Claude session worktree root or the Codex
46
+ repository root), so a working directory set by an earlier shell call is not visible
47
+ either; a push between the gh pr view lookup and the merge can change the head, which
48
+ only --match-head-commit closes; the control is policy-level, not cryptographic.
49
+
50
+ Time limit: Claude Code lets a tool call continue through the permission flow when a
51
+ PreToolUse hook exceeds its timeout, and the repository records no Codex behavior for
52
+ that case, so this file bounds every gh and git call itself. The default invokers run
53
+ the executable through Invoke-DocsOnlyBoundedNative: at most 7000 ms for gh and 3000 ms
54
+ for each git call, plus at most 1000 ms for the output. An expired wait stops the
55
+ process tree and reports exit code 124, which denies. The worst case for one gh and
56
+ three git calls is 20 s, below the 30 s Codex registration timeout.
57
+
58
+ .NOTES
59
+ PowerShell 7+. Byte-identical at .claude/hooks, .codex/hooks, and both bundle mirrors.
60
+ git and gh run only through Invoke-DocsOnlyGitExe and Invoke-DocsOnlyGhExe, which never
61
+ throw; unit tests pass a test-local scriptblock through -GitInvoker and -GhInvoker so
62
+ the seam bodies run without starting a process, and pass a test-local process starter
63
+ through the -ProcessStarter parameter of Invoke-DocsOnlyBoundedNative.
64
+ #>
65
+
66
+ function ConvertFrom-DocsOnlyNameList {
67
+ <#
68
+ .SYNOPSIS
69
+ Split NUL-separated git output into paths, discarding empty elements.
70
+ .DESCRIPTION
71
+ Callers wrap the result in @() so that an empty listing is an empty array. A path that
72
+ contains a line feed stays one element, because only NUL separates entries.
73
+ .PARAMETER Text
74
+ The captured git output.
75
+ .OUTPUTS
76
+ System.String, one per path
77
+ #>
78
+ [CmdletBinding()]
79
+ [OutputType([string])]
80
+ param(
81
+ [AllowNull()][AllowEmptyString()][string] $Text
82
+ )
83
+
84
+ if ([string]::IsNullOrEmpty($Text)) {
85
+ return
86
+ }
87
+ foreach ($entry in $Text.Split([char]0)) {
88
+ if ($entry.Length -gt 0) {
89
+ $entry
90
+ }
91
+ }
92
+ }
93
+
94
+ function Get-DocsOnlyPathViolation {
95
+ <#
96
+ .SYNOPSIS
97
+ Return $null when every path is under docs/, else the denial sub-code text.
98
+ .DESCRIPTION
99
+ An empty set returns DOCS_ONLY_EXEMPTION_EMPTY_DIFF. Otherwise the first path that does
100
+ not start with docs/ under ordinal comparison, or that is not longer than docs/ itself,
101
+ returns DOCS_ONLY_EXEMPTION_NON_DOCS_PATH naming that path.
102
+ .PARAMETER Path
103
+ The changed paths, repository-relative, as git reports them.
104
+ .OUTPUTS
105
+ System.String or $null
106
+ #>
107
+ [CmdletBinding()]
108
+ [OutputType([string])]
109
+ param(
110
+ [AllowNull()][AllowEmptyCollection()][string[]] $Path
111
+ )
112
+
113
+ $paths = @($Path | Where-Object { $null -ne $_ })
114
+ if ($paths.Count -eq 0) {
115
+ return 'DOCS_ONLY_EXEMPTION_EMPTY_DIFF: the branch diff lists no path.'
116
+ }
117
+ foreach ($entry in $paths) {
118
+ if ($entry.Length -le 5 -or -not $entry.StartsWith('docs/', [System.StringComparison]::Ordinal)) {
119
+ return "DOCS_ONLY_EXEMPTION_NON_DOCS_PATH: '$entry' is outside docs/."
120
+ }
121
+ }
122
+ return $null
123
+ }
124
+
125
+ function Get-DocsOnlyDiffTextViolation {
126
+ <#
127
+ .SYNOPSIS
128
+ Return $null when NUL-terminated diff output lists only docs/ paths, else the sub-code text.
129
+ .DESCRIPTION
130
+ Pure. Non-empty text that does not end with NUL returns DOCS_ONLY_EXEMPTION_DIFF_FAILED,
131
+ because entries could otherwise run together into one path that starts with docs/.
132
+ Otherwise the text is split on NUL and classified by Get-DocsOnlyPathViolation, so empty
133
+ text reaches DOCS_ONLY_EXEMPTION_EMPTY_DIFF.
134
+ .PARAMETER DiffText
135
+ The captured output of git diff --name-only -z.
136
+ .PARAMETER BaseName
137
+ The base branch named in the denial text.
138
+ .OUTPUTS
139
+ System.String or $null
140
+ #>
141
+ [CmdletBinding()]
142
+ [OutputType([string])]
143
+ param(
144
+ [AllowNull()][AllowEmptyString()][string] $DiffText,
145
+ [AllowNull()][AllowEmptyString()][string] $BaseName
146
+ )
147
+
148
+ $text = [string]$DiffText
149
+ if ($text.Length -gt 0 -and -not $text.EndsWith([char]0)) {
150
+ return "DOCS_ONLY_EXEMPTION_DIFF_FAILED: git diff against origin/$BaseName did not return NUL-terminated output."
151
+ }
152
+ return (Get-DocsOnlyPathViolation -Path @(ConvertFrom-DocsOnlyNameList -Text $text))
153
+ }
154
+
155
+ function Invoke-DocsOnlyBoundedNative {
156
+ <#
157
+ .SYNOPSIS
158
+ Run git or gh with a fail-closed time limit; the body of the default seam invokers.
159
+ .DESCRIPTION
160
+ Starts FileName in the current file-system location with each argument passed
161
+ unchanged and standard output and standard error redirected, then waits at most
162
+ TimeoutMilliseconds for the exit and at most 1000 ms more for the output. When a
163
+ wait expires, the process tree is stopped, nothing is emitted, and $LASTEXITCODE is
164
+ set to 124, so the calling seam reports a non-zero exit and the exemption denies.
165
+ Otherwise the output lines are emitted and $LASTEXITCODE is set to the exit code.
166
+ Standard error is drained and discarded. A start failure throws to the calling
167
+ seam, which returns its failure object. $LASTEXITCODE is set in the global scope
168
+ because the seams read it after the invoker, as they do after a native command.
169
+ The process object is disposed on every return path once it has started.
170
+ After an expired wait, each read is checked once without waiting before disposal.
171
+ .PARAMETER FileName
172
+ The executable, git or gh, resolved from PATH.
173
+ .PARAMETER ArgumentList
174
+ The arguments, one element per argument.
175
+ .PARAMETER TimeoutMilliseconds
176
+ The longest wait for the process to exit.
177
+ .PARAMETER ProcessStarter
178
+ The scriptblock that starts the process from its start description; a unit test
179
+ passes a test-local stand-in that returns a fake process object.
180
+ .OUTPUTS
181
+ System.String, one per output line
182
+ #>
183
+ [CmdletBinding()]
184
+ [OutputType([string])]
185
+ param(
186
+ [Parameter(Mandatory)][string] $FileName,
187
+ [AllowEmptyCollection()][string[]] $ArgumentList = @(),
188
+ [ValidateRange(1, 10000)][int] $TimeoutMilliseconds = 3000,
189
+ [ValidateNotNull()][scriptblock] $ProcessStarter = { param($StartInfo) [System.Diagnostics.Process]::Start($StartInfo) }
190
+ )
191
+
192
+ $startInfo = [System.Diagnostics.ProcessStartInfo]::new($FileName)
193
+ foreach ($argument in $ArgumentList) {
194
+ $startInfo.ArgumentList.Add($argument)
195
+ }
196
+ $startInfo.WorkingDirectory = $ExecutionContext.SessionState.Path.CurrentFileSystemLocation.ProviderPath
197
+ $startInfo.UseShellExecute = $false
198
+ $startInfo.RedirectStandardOutput = $true
199
+ $startInfo.RedirectStandardError = $true
200
+ $startInfo.StandardOutputEncoding = [System.Text.Encoding]::UTF8
201
+ $process = & $ProcessStarter $startInfo
202
+ try {
203
+ $output = $process.StandardOutput.ReadToEndAsync()
204
+ $errorRead = $process.StandardError.ReadToEndAsync()
205
+ if (-not $process.WaitForExit($TimeoutMilliseconds) -or -not $output.Wait(1000)) {
206
+ try {
207
+ $process.Kill($true)
208
+ }
209
+ catch {
210
+ $null = $_
211
+ }
212
+ foreach ($pendingRead in @($output, $errorRead)) {
213
+ try {
214
+ $null = $pendingRead.Wait(0)
215
+ }
216
+ catch {
217
+ $null = $_
218
+ }
219
+ }
220
+ Set-Variable -Name LASTEXITCODE -Value 124 -Scope Global
221
+ return
222
+ }
223
+ Set-Variable -Name LASTEXITCODE -Value ([int]$process.ExitCode) -Scope Global
224
+ $text = [string]$output.Result
225
+ if ($text.EndsWith("`n")) {
226
+ $text = $text.Substring(0, $text.Length - 1)
227
+ }
228
+ if ($text.Length -eq 0) {
229
+ return
230
+ }
231
+ foreach ($line in $text.Split("`n")) {
232
+ $line.TrimEnd("`r")
233
+ }
234
+ }
235
+ finally {
236
+ $process.Dispose()
237
+ }
238
+ }
239
+
240
+ function Invoke-DocsOnlyGitExe {
241
+ <#
242
+ .SYNOPSIS
243
+ Run git -C <root> with the given arguments; the git seam of the merge exemption.
244
+ .DESCRIPTION
245
+ Never throws. A root that is not an existing directory returns ExitCode -1 without
246
+ running the command, and so does a command that cannot start. Standard error is
247
+ discarded so that it is never read as path data. Output lines are joined with a line
248
+ feed.
249
+ .PARAMETER WorktreeRoot
250
+ The directory git runs in.
251
+ .PARAMETER GitArgs
252
+ The git arguments that follow -C <root>.
253
+ .PARAMETER GitInvoker
254
+ The scriptblock that runs git; a unit test passes a test-local stand-in instead.
255
+ .OUTPUTS
256
+ System.Management.Automation.PSCustomObject with ExitCode and Output.
257
+ #>
258
+ [CmdletBinding()]
259
+ [OutputType([pscustomobject])]
260
+ param(
261
+ [AllowNull()][AllowEmptyString()][string] $WorktreeRoot,
262
+ [Parameter(Mandatory)][string[]] $GitArgs,
263
+ [ValidateNotNull()][scriptblock] $GitInvoker = { Invoke-DocsOnlyBoundedNative -FileName 'git' -ArgumentList $args -TimeoutMilliseconds 3000 }
264
+ )
265
+
266
+ $failure = [pscustomobject]@{ ExitCode = -1; Output = '' }
267
+ if ([string]::IsNullOrWhiteSpace($WorktreeRoot) -or -not (Test-Path -LiteralPath $WorktreeRoot -PathType Container)) {
268
+ return $failure
269
+ }
270
+ try {
271
+ $lines = @(& $GitInvoker -C $WorktreeRoot @GitArgs 2>$null)
272
+ $code = $LASTEXITCODE
273
+ }
274
+ catch {
275
+ return $failure
276
+ }
277
+ return [pscustomobject]@{ ExitCode = [int]$code; Output = ($lines -join "`n") }
278
+ }
279
+
280
+ function Invoke-DocsOnlyGhExe {
281
+ <#
282
+ .SYNOPSIS
283
+ Run gh with the given arguments inside a working root; the gh seam.
284
+ .DESCRIPTION
285
+ Never throws. gh has no -C option, so the seam pushes the working root and pops it in
286
+ finally, restoring the caller's location after success and after a throw. A root that
287
+ is not an existing directory returns ExitCode -1 without running the command.
288
+ Standard error is discarded. Output lines are joined with a line feed.
289
+ .PARAMETER WorkingRoot
290
+ The directory gh runs in.
291
+ .PARAMETER GhArgs
292
+ The gh arguments.
293
+ .PARAMETER GhInvoker
294
+ The scriptblock that runs gh; a unit test passes a test-local stand-in instead.
295
+ .OUTPUTS
296
+ System.Management.Automation.PSCustomObject with ExitCode and Output.
297
+ #>
298
+ [CmdletBinding()]
299
+ [OutputType([pscustomobject])]
300
+ param(
301
+ [AllowNull()][AllowEmptyString()][string] $WorkingRoot,
302
+ [Parameter(Mandatory)][string[]] $GhArgs,
303
+ [ValidateNotNull()][scriptblock] $GhInvoker = { Invoke-DocsOnlyBoundedNative -FileName 'gh' -ArgumentList $args -TimeoutMilliseconds 7000 }
304
+ )
305
+
306
+ $failure = [pscustomobject]@{ ExitCode = -1; Output = '' }
307
+ if ([string]::IsNullOrWhiteSpace($WorkingRoot) -or -not (Test-Path -LiteralPath $WorkingRoot -PathType Container)) {
308
+ return $failure
309
+ }
310
+ try {
311
+ Push-Location -LiteralPath $WorkingRoot -ErrorAction Stop
312
+ }
313
+ catch {
314
+ return $failure
315
+ }
316
+ try {
317
+ $lines = @(& $GhInvoker @GhArgs 2>$null)
318
+ $code = $LASTEXITCODE
319
+ }
320
+ catch {
321
+ return $failure
322
+ }
323
+ finally {
324
+ Pop-Location
325
+ }
326
+ return [pscustomobject]@{ ExitCode = [int]$code; Output = ($lines -join "`n") }
327
+ }
328
+
329
+ function Get-DocsOnlyMergeShapeViolation {
330
+ <#
331
+ .SYNOPSIS
332
+ Return $null when the merge command has the M1 shape, else the denial text.
333
+ .DESCRIPTION
334
+ Pure; runs before any process. See M1 in the file header.
335
+ .PARAMETER CommandText
336
+ The Bash command text under evaluation.
337
+ .PARAMETER PrNumber
338
+ The explicit pull request number the gate read, or $null.
339
+ .OUTPUTS
340
+ System.String or $null
341
+ #>
342
+ [CmdletBinding()]
343
+ [OutputType([string])]
344
+ param(
345
+ [AllowNull()][AllowEmptyString()][string] $CommandText,
346
+ [AllowNull()][Nullable[int]] $PrNumber
347
+ )
348
+
349
+ $prefix = 'DOCS_ONLY_EXEMPTION_PR_UNRESOLVED: '
350
+ if ($null -eq $PrNumber -or $PrNumber -le 0) {
351
+ return $prefix + 'the command names no positive pull request number.'
352
+ }
353
+ $segments = @(Read-CommandLineSegment -CommandText $CommandText)
354
+ if ($segments.Count -ne 1 -or (Test-CommandLineSegmentRawScan -Segment $segments[0])) {
355
+ return $prefix + 'the command must be one gh pr merge segment with no chain, wrapper, or substitution.'
356
+ }
357
+ if ($null -eq (Get-CommandLineStructuralMatch -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge'))) {
358
+ return $prefix + 'the command is not a structural gh pr merge.'
359
+ }
360
+ foreach ($token in @($segments[0].Tokens)) {
361
+ $text = [string]$token
362
+ if ($text -cmatch '^[A-Za-z_][A-Za-z0-9_]*=') {
363
+ return $prefix + "the inline environment prefix '$text' is not accepted."
364
+ }
365
+ if ($text.Length -ge 2 -and $text[0] -eq '-' -and $text[1] -ne '-' -and $text.Substring(1).Contains('R')) {
366
+ return $prefix + "the repository override '$text' is not accepted."
367
+ }
368
+ if ($text -eq '--repo' -or $text.StartsWith('--repo=', [System.StringComparison]::OrdinalIgnoreCase)) {
369
+ return $prefix + "the repository override '$text' is not accepted."
370
+ }
371
+ if ($text -eq '--auto' -or $text.StartsWith('--auto=', [System.StringComparison]::OrdinalIgnoreCase)) {
372
+ return $prefix + 'auto-merge (--auto) is not eligible.'
373
+ }
374
+ }
375
+ return $null
376
+ }
377
+
378
+ function Get-DocsOnlyPullRequestTarget {
379
+ <#
380
+ .SYNOPSIS
381
+ Read the base branch and head commit of a pull request through the gh seam (M3).
382
+ .DESCRIPTION
383
+ Returns an object with Reason, Base, and Head. Reason is $null when the lookup
384
+ succeeded and every M3 check passed; otherwise it carries the denial text.
385
+ .PARAMETER CommandText
386
+ The Bash command text, read for an optional --match-head-commit value.
387
+ .PARAMETER PrNumber
388
+ The pull request number.
389
+ .PARAMETER WorktreeRoot
390
+ The directory gh runs in.
391
+ .OUTPUTS
392
+ System.Management.Automation.PSCustomObject with Reason, Base, and Head.
393
+ #>
394
+ [CmdletBinding()]
395
+ [OutputType([pscustomobject])]
396
+ param(
397
+ [AllowNull()][AllowEmptyString()][string] $CommandText,
398
+ [Parameter(Mandatory)][int] $PrNumber,
399
+ [Parameter(Mandatory)][string] $WorktreeRoot
400
+ )
401
+
402
+ $unresolved = 'DOCS_ONLY_EXEMPTION_PR_UNRESOLVED: '
403
+ $lookup = Invoke-DocsOnlyGhExe -WorkingRoot $WorktreeRoot -GhArgs @('pr', 'view', [string]$PrNumber, '--json', 'baseRefName,headRefOid')
404
+ $pr = $null
405
+ if ($null -ne $lookup -and $lookup.ExitCode -eq 0) {
406
+ try {
407
+ $pr = ConvertFrom-Json -InputObject ([string]$lookup.Output) -ErrorAction Stop
408
+ }
409
+ catch {
410
+ $pr = $null
411
+ }
412
+ }
413
+ if ($pr -isnot [System.Management.Automation.PSCustomObject]) {
414
+ return [pscustomobject]@{ Reason = $unresolved + "gh pr view $PrNumber did not return a readable pull request."; Base = $null; Head = $null }
415
+ }
416
+ $base = [string]$pr.baseRefName
417
+ if ($base -cnotmatch '^[A-Za-z0-9][A-Za-z0-9._/-]*$' -or $base.Contains('..')) {
418
+ return [pscustomobject]@{ Reason = "DOCS_ONLY_EXEMPTION_BASE_UNRESOLVED: the base branch '$base' of pull request $PrNumber is not a conservative branch name."; Base = $null; Head = $null }
419
+ }
420
+ $head = [string]$pr.headRefOid
421
+ if ($head -cnotmatch '^[0-9a-f]{40}([0-9a-f]{24})?$') {
422
+ return [pscustomobject]@{ Reason = $unresolved + "the head commit of pull request $PrNumber is not a full lowercase object id."; Base = $null; Head = $null }
423
+ }
424
+ $subcommandPath = @('pr', 'merge')
425
+ if (Test-CommandLineFlag -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--match-head-commit') {
426
+ $pinned = Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--match-head-commit'
427
+ if (-not [string]::Equals($pinned, $head, [System.StringComparison]::Ordinal)) {
428
+ return [pscustomobject]@{ Reason = $unresolved + "--match-head-commit does not equal the head commit $head of pull request $PrNumber."; Base = $null; Head = $null }
429
+ }
430
+ }
431
+ return [pscustomobject]@{ Reason = $null; Base = $base; Head = $head }
432
+ }
433
+
434
+ function Get-DocsOnlyMergeExemption {
435
+ <#
436
+ .SYNOPSIS
437
+ Decide whether gh pr merge <N> --merge is exempt because the pull request changes only docs/.
438
+ .DESCRIPTION
439
+ Evaluates M1 to M9 of the file header in order and returns at the first failure. M1
440
+ and M2 run before any process. Every seam failure denies.
441
+ .PARAMETER CommandText
442
+ The Bash command text under evaluation.
443
+ .PARAMETER PrNumber
444
+ The explicit pull request number the gate read from the command, or $null.
445
+ .PARAMETER WorktreeRoot
446
+ The directory gh and git run in.
447
+ .OUTPUTS
448
+ System.Collections.Specialized.OrderedDictionary with Exempt and Reason.
449
+ #>
450
+ [CmdletBinding()]
451
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
452
+ param(
453
+ [AllowNull()][AllowEmptyString()][string] $CommandText,
454
+ [AllowNull()][Nullable[int]] $PrNumber,
455
+ [AllowNull()][AllowEmptyString()][string] $WorktreeRoot
456
+ )
457
+
458
+ $shape = Get-DocsOnlyMergeShapeViolation -CommandText $CommandText -PrNumber $PrNumber
459
+ if ($shape) {
460
+ return [ordered]@{ Exempt = $false; Reason = $shape }
461
+ }
462
+ if ([string]::IsNullOrWhiteSpace($WorktreeRoot) -or -not (Test-Path -LiteralPath $WorktreeRoot -PathType Container)) {
463
+ return [ordered]@{ Exempt = $false; Reason = 'DOCS_ONLY_EXEMPTION_PR_UNRESOLVED: the worktree root for the lookup is not an existing directory.' }
464
+ }
465
+ $target = Get-DocsOnlyPullRequestTarget -CommandText $CommandText -PrNumber ([int]$PrNumber) -WorktreeRoot $WorktreeRoot
466
+ if ($target.Reason) {
467
+ return [ordered]@{ Exempt = $false; Reason = $target.Reason }
468
+ }
469
+ $base = $target.Base
470
+ $head = $target.Head
471
+
472
+ $baseRev = Invoke-DocsOnlyGitExe -WorktreeRoot $WorktreeRoot -GitArgs @('rev-parse', '--verify', '--quiet', ('refs/remotes/origin/' + $base + '^{commit}'))
473
+ $baseCommit = if ($null -ne $baseRev) { ([string]$baseRev.Output).Trim() } else { '' }
474
+ if ($null -eq $baseRev -or $baseRev.ExitCode -ne 0 -or $baseCommit -cnotmatch '^[0-9a-f]{40}([0-9a-f]{24})?$') {
475
+ return [ordered]@{ Exempt = $false; Reason = "DOCS_ONLY_EXEMPTION_BASE_UNRESOLVED: refs/remotes/origin/$base does not resolve to a commit in the worktree." }
476
+ }
477
+
478
+ $headRev = Invoke-DocsOnlyGitExe -WorktreeRoot $WorktreeRoot -GitArgs @('rev-parse', '--verify', '--quiet', ($head + '^{commit}'))
479
+ $headCommit = if ($null -ne $headRev) { ([string]$headRev.Output).Trim() } else { '' }
480
+ if ($null -eq $headRev -or $headRev.ExitCode -ne 0 -or -not [string]::Equals($headCommit, $head, [System.StringComparison]::Ordinal)) {
481
+ return [ordered]@{ Exempt = $false; Reason = "DOCS_ONLY_EXEMPTION_DIFF_FAILED: the head commit $head of pull request $PrNumber is not present locally; run git fetch origin pull/$PrNumber/head, then retry. This hook does not fetch." }
482
+ }
483
+
484
+ $diff = Invoke-DocsOnlyGitExe -WorktreeRoot $WorktreeRoot -GitArgs @('diff', '--name-only', '-z', '--no-renames', '--no-color', '--no-ext-diff', '--no-relative', ($baseCommit + '...' + $head))
485
+ if ($null -eq $diff -or $diff.ExitCode -ne 0) {
486
+ return [ordered]@{ Exempt = $false; Reason = "DOCS_ONLY_EXEMPTION_DIFF_FAILED: git diff against origin/$base could not be computed." }
487
+ }
488
+ $violation = Get-DocsOnlyDiffTextViolation -DiffText ([string]$diff.Output) -BaseName $base
489
+ if ($violation) {
490
+ return [ordered]@{ Exempt = $false; Reason = $violation }
491
+ }
492
+ return [ordered]@{ Exempt = $true; Reason = $null }
493
+ }
@@ -0,0 +1,192 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Shared SubagentStop envelope reader and exit-2 block helper (issue #897, epic subagentstop-envelope).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by the SubagentStop validators under .claude/hooks. It is not registered as a hook in
7
+ either settings.json copy. It imports nothing (no Import-Module, no using module, no dot-source),
8
+ reads no environment variable, and writes nothing to the success or warning streams; the only
9
+ stream it ever writes is standard error, from Exit-SubagentStopBlock.
10
+
11
+ The governing contract is the section "## Authoritative Helper Contract" of
12
+ docs/features/archive/subagentstop-envelope-core-897/spec.md (issue #897; at
13
+ docs/features/active/subagentstop-envelope-core-897/ until the archive move). Where this file
14
+ and any other document differ, that section governs.
15
+
16
+ .NOTES
17
+ PowerShell 7.5 or later (ConvertFrom-Json -DateKind). Byte-identical bundled mirror at
18
+ extensions/drm-copilot/resources/claude-customizations/.claude/hooks/subagentstop-envelope.ps1.
19
+ #>
20
+
21
+ function Get-SubagentStopEnvelopeAnomalyReason {
22
+ <#
23
+ .SYNOPSIS
24
+ Returns the reason clause for one SubagentStop envelope anomaly code.
25
+ .DESCRIPTION
26
+ Consumers write '<PREFIX> payload anomaly - <clause>', where <PREFIX> is the validator's own
27
+ reason prefix. Any value that is not one of the six codes (including $null, an empty string, and
28
+ a case variant) is a consumer programming error and throws System.ArgumentException naming the
29
+ rejected value. No [ValidateSet()] is used, so the exception is an ArgumentException rather than a
30
+ parameter-binding exception.
31
+ .PARAMETER Anomaly
32
+ One of EmptyPayload, UnparseableJson, NonObjectEnvelope, MissingAssistantMessage,
33
+ NullAssistantMessage or NonStringAssistantMessage (case-sensitive).
34
+ #>
35
+ [CmdletBinding()]
36
+ [OutputType([string])]
37
+ param(
38
+ [Parameter()]
39
+ [AllowNull()]
40
+ [AllowEmptyString()]
41
+ [string] $Anomaly
42
+ )
43
+
44
+ switch -CaseSensitive ($Anomaly) {
45
+ 'EmptyPayload' { return 'the SubagentStop payload on stdin was empty' }
46
+ 'UnparseableJson' { return 'the SubagentStop payload is not valid JSON' }
47
+ 'NonObjectEnvelope' { return 'the SubagentStop payload root is not a JSON object' }
48
+ 'MissingAssistantMessage' { return 'the SubagentStop payload has no last_assistant_message field' }
49
+ 'NullAssistantMessage' { return 'the SubagentStop payload last_assistant_message is null' }
50
+ 'NonStringAssistantMessage' { return 'the SubagentStop payload last_assistant_message is not a string' }
51
+ default { throw [System.ArgumentException]::new("Unknown SubagentStop envelope anomaly code: '$Anomaly'.", 'Anomaly') }
52
+ }
53
+ }
54
+
55
+ function Read-SubagentStopEnvelope {
56
+ <#
57
+ .SYNOPSIS
58
+ Reads and classifies a SubagentStop hook envelope from -InputText or from standard input.
59
+ .DESCRIPTION
60
+ When -InputText is bound (including an empty string or $null) the function parses that text and
61
+ does not read standard input. When -InputText is not bound the function reads standard input to
62
+ end; a manual run without redirected stdin therefore waits for end-of-input (Ctrl+Z then Enter on
63
+ Windows, Ctrl+D elsewhere). A read failure is treated as empty input. One leading BOM (U+FEFF) is
64
+ stripped before any other evaluation. The function never throws and never exits; every failure
65
+ maps to one of six anomaly codes, and the derived fields are read from any object root regardless
66
+ of validity.
67
+ .PARAMETER InputText
68
+ The raw envelope text. Omit the parameter to read standard input.
69
+ .OUTPUTS
70
+ Exactly one PSCustomObject with the properties IsValid, Anomaly, Envelope, AssistantText,
71
+ StopHookActive, AgentType and HookEventName, in that order.
72
+ #>
73
+ [CmdletBinding()]
74
+ [OutputType([pscustomobject])]
75
+ param(
76
+ [Parameter()]
77
+ [AllowNull()]
78
+ [AllowEmptyString()]
79
+ [string] $InputText
80
+ )
81
+
82
+ if ($PSBoundParameters.ContainsKey('InputText')) {
83
+ $text = $InputText
84
+ }
85
+ else {
86
+ try {
87
+ $text = [Console]::In.ReadToEnd()
88
+ }
89
+ catch {
90
+ $text = ''
91
+ }
92
+ }
93
+
94
+ if ($null -ne $text -and $text.Length -gt 0 -and $text[0] -eq [char]0xFEFF) {
95
+ $text = $text.Substring(1)
96
+ }
97
+
98
+ $isValid = $false
99
+ $anomaly = $null
100
+ $envelope = $null
101
+ $assistantText = ''
102
+ $stopHookActive = $false
103
+ $agentType = ''
104
+ $hookEventName = ''
105
+
106
+ if ([string]::IsNullOrWhiteSpace($text)) {
107
+ $anomaly = 'EmptyPayload'
108
+ }
109
+ else {
110
+ $parsed = $null
111
+ $parseFailed = $false
112
+ try {
113
+ $parsed = ConvertFrom-Json -InputObject $text -NoEnumerate -DateKind String -ErrorAction Stop
114
+ }
115
+ catch {
116
+ $parseFailed = $true
117
+ }
118
+
119
+ if ($parseFailed) {
120
+ $anomaly = 'UnparseableJson'
121
+ }
122
+ elseif (-not ($parsed -is [System.Management.Automation.PSCustomObject])) {
123
+ # The [pscustomobject] accelerator resolves to PSObject and would match non-objects; the full type name does not.
124
+ $anomaly = 'NonObjectEnvelope'
125
+ }
126
+ else {
127
+ $envelope = $parsed
128
+
129
+ $messageProperty = @($parsed.PSObject.Properties | Where-Object { $_.Name -ceq 'last_assistant_message' })
130
+ if ($messageProperty.Count -eq 0) {
131
+ $anomaly = 'MissingAssistantMessage'
132
+ }
133
+ elseif ($null -eq $messageProperty[0].Value) {
134
+ $anomaly = 'NullAssistantMessage'
135
+ }
136
+ elseif ($messageProperty[0].Value -is [string]) {
137
+ $isValid = $true
138
+ $assistantText = $messageProperty[0].Value
139
+ }
140
+ else {
141
+ $anomaly = 'NonStringAssistantMessage'
142
+ }
143
+
144
+ $stopHookProperty = @($parsed.PSObject.Properties | Where-Object { $_.Name -ceq 'stop_hook_active' })
145
+ if ($stopHookProperty.Count -gt 0 -and $stopHookProperty[0].Value -is [bool]) {
146
+ $stopHookActive = $stopHookProperty[0].Value
147
+ }
148
+
149
+ $agentTypeProperty = @($parsed.PSObject.Properties | Where-Object { $_.Name -ceq 'agent_type' })
150
+ if ($agentTypeProperty.Count -gt 0 -and $agentTypeProperty[0].Value -is [string]) {
151
+ $agentType = $agentTypeProperty[0].Value
152
+ }
153
+
154
+ $hookEventProperty = @($parsed.PSObject.Properties | Where-Object { $_.Name -ceq 'hook_event_name' })
155
+ if ($hookEventProperty.Count -gt 0 -and $hookEventProperty[0].Value -is [string]) {
156
+ $hookEventName = $hookEventProperty[0].Value
157
+ }
158
+ }
159
+ }
160
+
161
+ return [pscustomobject][ordered]@{
162
+ IsValid = $isValid
163
+ Anomaly = $anomaly
164
+ Envelope = $envelope
165
+ AssistantText = $assistantText
166
+ StopHookActive = $stopHookActive
167
+ AgentType = $agentType
168
+ HookEventName = $hookEventName
169
+ }
170
+ }
171
+
172
+ function Exit-SubagentStopBlock {
173
+ <#
174
+ .SYNOPSIS
175
+ Blocks a SubagentStop by writing the reason to standard error and exiting with code 2.
176
+ .DESCRIPTION
177
+ Exit code 2 is the only exit code that blocks a SubagentStop; the stderr text is delivered to the
178
+ subagent as its next instruction. A null or empty Message fails parameter binding before the
179
+ body runs, so no exit occurs in that case.
180
+ .PARAMETER Message
181
+ The block reason written to standard error.
182
+ #>
183
+ [CmdletBinding()]
184
+ param(
185
+ [Parameter(Mandatory)]
186
+ [ValidateNotNullOrEmpty()]
187
+ [string] $Message
188
+ )
189
+
190
+ [Console]::Error.WriteLine($Message)
191
+ exit 2
192
+ }