@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,279 @@
1
+ <#
2
+ .SYNOPSIS
3
+ PreToolUse hook that limits the pr-author subagent's Bash calls to an allowlist of forms.
4
+
5
+ .DESCRIPTION
6
+ Registered in the frontmatter of .claude/agents/pr-author.md on the "Bash" matcher
7
+ (issue #824, #733). Every command is read with Read-CommandLineInvocationSegment, so a
8
+ wrapper payload, a substitution body, or an xargs-injected command is a record of its own.
9
+ The command is allowed only when every record is a literal, top-level, unwrapped command
10
+ in one of these forms, matched by token position:
11
+
12
+ - git log ... and git rev-parse ... (no git global option)
13
+ - gh pr create ... and gh pr edit ... (no gh global option)
14
+ - sha256sum artifacts/pr_body_<N>.md (only as the whole command)
15
+ - date -u [+<format>] (only as the whole command)
16
+
17
+ Segments may be joined only by ';', '&&', '||', '|', or a newline. A record that is
18
+ nested, unbalanced, opaque, non-literal, wrapper-led, led by a VAR=value assignment, or
19
+ whose scanner segment carries a redirection is denied. Every deny reason begins
20
+ PR_AUTHOR_COMMAND_NOT_ALLOWED: and names the first rejected segment.
21
+
22
+ The hook is read-only: it inspects the attempted command and emits a JSON
23
+ allow-or-deny decision without mutating the command text.
24
+
25
+ .NOTES
26
+ Compatible with PowerShell 7+. Pure string logic over the shared command-line parser;
27
+ no disk, process, or network access beyond payload acquisition.
28
+ #>
29
+ [CmdletBinding()]
30
+ param()
31
+
32
+
33
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
34
+ # Shared command-line parser (issues #545 and #824).
35
+ . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
36
+ . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
37
+
38
+ $script:PrAuthorAllowlistReasonPrefix = 'PR_AUTHOR_COMMAND_NOT_ALLOWED: '
39
+ $script:PrAuthorAllowlistDelimiters = @('', ';', '&&', '||', '|', "`n")
40
+ $script:PrAuthorAllowlistWrapperWords = @(
41
+ 'sh', 'bash', 'zsh', 'dash', 'ksh', 'pwsh', 'powershell', 'eval', 'xargs',
42
+ 'env', 'timeout', 'time', 'command', 'nohup'
43
+ )
44
+ $script:PrAuthorAllowlistFormText = 'git log, git rev-parse, gh pr create, gh pr edit, sha256sum artifacts/pr_body_<N>.md, date -u [+<format>]'
45
+
46
+ function Test-PrAuthorAllowlistForm {
47
+ <#
48
+ .SYNOPSIS
49
+ Report whether one record's tokens match an allowed command form by token position.
50
+ .PARAMETER Token
51
+ The record's tokens, quotes already stripped.
52
+ .PARAMETER IsWholeCommand
53
+ Whether the command has exactly one top-level record; sha256sum and date require it.
54
+ .OUTPUTS
55
+ System.Boolean
56
+ #>
57
+ [CmdletBinding()]
58
+ [OutputType([bool])]
59
+ param(
60
+ [Parameter(Mandatory)][AllowEmptyCollection()][string[]] $Token,
61
+ [Parameter(Mandatory)][bool] $IsWholeCommand
62
+ )
63
+
64
+ if ($Token.Count -eq 0) {
65
+ return $false
66
+ }
67
+ switch -CaseSensitive ($Token[0]) {
68
+ 'git' {
69
+ return ($Token.Count -ge 2 -and @('log', 'rev-parse') -ccontains $Token[1])
70
+ }
71
+ 'gh' {
72
+ return ($Token.Count -ge 3 -and $Token[1] -ceq 'pr' -and @('create', 'edit') -ccontains $Token[2])
73
+ }
74
+ 'sha256sum' {
75
+ return ($IsWholeCommand -and $Token.Count -eq 2 -and $Token[1] -cmatch '^artifacts/pr_body_\d+\.md$')
76
+ }
77
+ 'date' {
78
+ if (-not $IsWholeCommand -or $Token.Count -lt 2 -or $Token[1] -cne '-u') {
79
+ return $false
80
+ }
81
+ return ($Token.Count -eq 2 -or ($Token.Count -eq 3 -and $Token[2].StartsWith('+')))
82
+ }
83
+ }
84
+ return $false
85
+ }
86
+
87
+ function Get-PrAuthorAllowlistRecordProblem {
88
+ <#
89
+ .SYNOPSIS
90
+ Return the reason one record is rejected, or $null when the record is allowed.
91
+ .PARAMETER Record
92
+ One Read-CommandLineInvocationSegment record.
93
+ .PARAMETER MaskedText
94
+ The masked text of the record's depth-0 scanner segment, or '' for a nested record.
95
+ .PARAMETER IsWholeCommand
96
+ Whether the command has exactly one top-level record.
97
+ .OUTPUTS
98
+ System.String or $null
99
+ #>
100
+ [CmdletBinding()]
101
+ [OutputType([string])]
102
+ param(
103
+ [Parameter(Mandatory)][pscustomobject] $Record,
104
+ [Parameter(Mandatory)][AllowEmptyString()][string] $MaskedText,
105
+ [Parameter(Mandatory)][bool] $IsWholeCommand
106
+ )
107
+
108
+ $tokens = [string[]]@($Record.Tokens)
109
+ if ($Record.Depth -gt 0) { return "it is a nested $($Record.Origin) command" }
110
+ if ($Record.Unbalanced) { return 'its quoting or grouping is unbalanced' }
111
+ if ($Record.Opaque) { return "its payload cannot be read ($($Record.OpaqueReason))" }
112
+ if (-not $Record.Literal) { return 'it carries a variable, substitution, or other non-literal token' }
113
+ if ($script:PrAuthorAllowlistWrapperWords -contains $Record.CommandWord) { return "it is led by the wrapper '$($Record.CommandWord)'" }
114
+ if ($script:PrAuthorAllowlistDelimiters -notcontains $Record.Delimiter) { return "it is joined by the delimiter '$($Record.Delimiter)'" }
115
+ if ($MaskedText.Contains('<') -or $MaskedText.Contains('>')) { return 'it carries a redirection' }
116
+ if ($tokens.Count -gt 0 -and $tokens[0] -match '^[A-Za-z_][A-Za-z0-9_]*=') { return 'it is led by a VAR=value assignment' }
117
+ if (-not (Test-PrAuthorAllowlistForm -Token $tokens -IsWholeCommand $IsWholeCommand)) { return 'it is not an allowed command form' }
118
+ return $null
119
+ }
120
+
121
+ function Get-PrAuthorCommandAllowlistReason {
122
+ <#
123
+ .SYNOPSIS
124
+ Return the deny reason for a command outside the pr-author allowlist, or $null.
125
+ .PARAMETER CommandText
126
+ The Bash command text extracted from the envelope's tool_input.
127
+ .OUTPUTS
128
+ System.String or $null
129
+ #>
130
+ [CmdletBinding()]
131
+ [OutputType([string])]
132
+ param(
133
+ [Parameter(Mandatory)]
134
+ [AllowEmptyString()]
135
+ [string] $CommandText
136
+ )
137
+
138
+ if ([string]::IsNullOrWhiteSpace($CommandText)) {
139
+ return $null
140
+ }
141
+
142
+ $records = @(Read-CommandLineInvocationSegment -CommandText $CommandText)
143
+ $segments = @(Read-CommandLineSegment -CommandText (ConvertTo-CommandLineNormalizedText -CommandText $CommandText))
144
+ $topLevel = @($records | Where-Object { $_.Depth -eq 0 })
145
+ $isWholeCommand = $topLevel.Count -eq 1
146
+ $topLevelPosition = 0
147
+
148
+ foreach ($record in $records) {
149
+ $maskedText = ''
150
+ if ($record.Depth -eq 0) {
151
+ if ($topLevelPosition -lt $segments.Count) {
152
+ $maskedText = [string]$segments[$topLevelPosition].MaskedText
153
+ }
154
+ $topLevelPosition++
155
+ }
156
+ $problem = Get-PrAuthorAllowlistRecordProblem -Record $record -MaskedText $maskedText -IsWholeCommand $isWholeCommand
157
+ if ($null -ne $problem) {
158
+ return ($script:PrAuthorAllowlistReasonPrefix +
159
+ "the segment '$($record.RawText.Trim())' is rejected because $problem. The pr-author agent may run only: $($script:PrAuthorAllowlistFormText), each without wrappers, substitutions, or redirections.")
160
+ }
161
+ }
162
+
163
+ return $null
164
+ }
165
+
166
+ function Get-PrAuthorCommandAllowlistDecision {
167
+ <#
168
+ .SYNOPSIS
169
+ Build an ordered allow or deny decision.
170
+ .PARAMETER Reason
171
+ The deny reason, or $null for an allow.
172
+ .OUTPUTS
173
+ System.Collections.Specialized.OrderedDictionary
174
+ #>
175
+ [CmdletBinding()]
176
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
177
+ param(
178
+ [AllowNull()]
179
+ [string] $Reason
180
+ )
181
+
182
+ if ([string]::IsNullOrEmpty($Reason)) {
183
+ return [ordered]@{
184
+ hookSpecificOutput = [ordered]@{
185
+ hookEventName = 'PreToolUse'
186
+ permissionDecision = 'allow'
187
+ }
188
+ }
189
+ }
190
+ return [ordered]@{
191
+ hookSpecificOutput = [ordered]@{
192
+ hookEventName = 'PreToolUse'
193
+ permissionDecision = 'deny'
194
+ permissionDecisionReason = $Reason
195
+ }
196
+ }
197
+ }
198
+
199
+ function Invoke-PrAuthorCommandAllowlistDecision {
200
+ <#
201
+ .SYNOPSIS
202
+ Return the ordered allow-or-deny decision for one command text.
203
+ .PARAMETER CommandText
204
+ The Bash command text.
205
+ .OUTPUTS
206
+ System.Collections.Specialized.OrderedDictionary
207
+ #>
208
+ [CmdletBinding()]
209
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
210
+ param(
211
+ [Parameter(Mandatory)]
212
+ [AllowEmptyString()]
213
+ [string] $CommandText
214
+ )
215
+
216
+ return Get-PrAuthorCommandAllowlistDecision -Reason (Get-PrAuthorCommandAllowlistReason -CommandText $CommandText)
217
+ }
218
+
219
+ function Invoke-PrAuthorCommandAllowlistEntryPoint {
220
+ <#
221
+ .SYNOPSIS
222
+ Runs the hook decision and returns the process exit code.
223
+ .DESCRIPTION
224
+ Acquires the payload through the shared reader unless the caller supplies one, emits
225
+ the compact decision JSON, and returns 0. It never returns 1: exit 1 is non-blocking
226
+ for PreToolUse, so every anomaly is already a deny decision by the time control
227
+ reaches here. The function does not call exit; the thin tail converts the returned
228
+ code into a process exit.
229
+ .PARAMETER ToolInputRaw
230
+ Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
231
+ .PARAMETER ReadPayload
232
+ Seam for payload acquisition, so tests can drive the empty-on-all-transports case
233
+ without touching a console.
234
+ .OUTPUTS
235
+ System.Int32
236
+ #>
237
+ [CmdletBinding()]
238
+ [OutputType([int])]
239
+ param(
240
+ [AllowNull()]
241
+ [AllowEmptyString()]
242
+ [string] $ToolInputRaw,
243
+
244
+ [scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
245
+ )
246
+
247
+ if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
248
+ $ToolInputRaw = [string](& $ReadPayload)
249
+ }
250
+
251
+ $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
252
+ if (-not $payload.IsValid) {
253
+ $decision = Get-PrAuthorCommandAllowlistDecision -Reason (
254
+ $script:PrAuthorAllowlistReasonPrefix + 'payload anomaly - ' +
255
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
256
+ '. The hook fails closed on an envelope it cannot read.')
257
+ } else {
258
+ $commandText = [string](Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'command')
259
+ $decision = Invoke-PrAuthorCommandAllowlistDecision -CommandText $commandText
260
+ }
261
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
262
+
263
+ return 0
264
+ }
265
+
266
+ # Allow dot-sourcing in tests without executing the entrypoint.
267
+ if ($MyInvocation.InvocationName -eq '.') {
268
+ return
269
+ }
270
+
271
+ # The entry point returns its [int] exit code as the last pipeline element and the
272
+ # decision JSON before it. `exit (<call>)` would capture BOTH into the exit
273
+ # expression and emit nothing, so the decision is written explicitly here first.
274
+ $entryPointResult = @(Invoke-PrAuthorCommandAllowlistEntryPoint)
275
+ if ($entryPointResult.Count -gt 1) {
276
+ $entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
277
+ }
278
+
279
+ exit ([int]$entryPointResult[-1])
@@ -17,14 +17,19 @@
17
17
 
18
18
  Both helpers read the script-scoped configuration the parent hook declares and call the
19
19
  parent hook's injectable read seams, so this file is only ever dot-sourced from that hook
20
- and never invoked on its own. $script:PrContextArtifactPath is a process-directory-relative
21
- artifact path and stays one. $script:OrchestratorStateCheckpointPath is different after
20
+ and never invoked on its own. $script:PrContextArtifactPath is the repository-relative
21
+ literal used in messages and as the composition input; every artifact read is composed
22
+ beneath the root Resolve-PrAuthorArtifactRoot selects (issue #850).
23
+ $script:OrchestratorStateCheckpointPath is different after
22
24
  issue #673: the parent declares it null, and Get-PrAuthorBypassReason assigns it the
23
25
  absolute checkpoint path of the worktree identity resolution selected, so every later
24
26
  reader receives one resolved path rather than deriving its own.
25
27
 
26
28
  .NOTES
27
- Compatible with PowerShell 7+. No external module dependencies.
29
+ Compatible with PowerShell 7+. Dependencies are in-repository only: the command parsers
30
+ and worktree-resolution modules loaded below, and the artifact-root and docs-only siblings.
31
+ No PowerShell Gallery module is required.
32
+ The docs-only sibling runs the git executable at call time (issue #901).
28
33
  #>
29
34
  [CmdletBinding()]
30
35
  param()
@@ -47,6 +52,10 @@ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemR
47
52
  # Epic scope (issue #663): the epic integration pull request is gated against the epic checkpoint.
48
53
  Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
49
54
  Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeReadiness.psm1') -Force -ErrorAction Stop
55
+ # Artifact-root resolution and the Check 1 body-path binding (issue #850).
56
+ . (Join-Path $PSScriptRoot 'enforce-pr-author-skill.artifact-root.ps1')
57
+ # Docs-only exemption from the orchestrator-state preflight (issue #901).
58
+ . (Join-Path $PSScriptRoot 'enforce-pr-author-skill.docs-only.ps1')
50
59
 
51
60
  function Resolve-PrAuthorWorktreeTarget {
52
61
  <#
@@ -98,7 +107,9 @@ function Get-PrAuthorTargetCheckpointResolution {
98
107
  refuses to answer rather than answering from unrelated state. Neither reason-code
99
108
  literal appears in this file; both come from the accessors named above.
100
109
  .OUTPUTS
101
- System.Collections.Specialized.OrderedDictionary with CheckpointPath and Reason.
110
+ System.Collections.Specialized.OrderedDictionary with CheckpointPath, Reason,
111
+ WorktreeRoot (the resolved root, or $null when unresolved), and Status (the target's
112
+ status), so the artifact root is taken from the same resolution (issue #850).
102
113
  #>
103
114
  [CmdletBinding()]
104
115
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -111,15 +122,51 @@ function Get-PrAuthorTargetCheckpointResolution {
111
122
 
112
123
  switch ($target.Status) {
113
124
  { $_ -in @('SessionRoot', 'OtherWorktree') } {
114
- return [ordered]@{ CheckpointPath = (Get-WorktreeItemCheckpointPath -WorktreeRoot $target.WorktreeRoot); Reason = $null }
125
+ return [ordered]@{ CheckpointPath = (Get-WorktreeItemCheckpointPath -WorktreeRoot $target.WorktreeRoot); Reason = $null; WorktreeRoot = $target.WorktreeRoot; Status = $target.Status }
115
126
  }
116
127
  default {
117
128
  $reason = "$($target.ReasonCode): $($target.Detail) The pr-author gate will not validate this call against the session root's checkpoint, because that checkpoint may belong to a different item. Pass --head <branch> on the gh pr create command so the call names its own target."
118
- return [ordered]@{ CheckpointPath = $null; Reason = $reason }
129
+ return [ordered]@{ CheckpointPath = $null; Reason = $reason; WorktreeRoot = $null; Status = $target.Status }
119
130
  }
120
131
  }
121
132
  }
122
133
 
134
+ function Get-PrAuthorBodyFileValue {
135
+ <#
136
+ .SYNOPSIS
137
+ Return the normalized --body-file value of the matched gh pr create or gh pr edit, or $null.
138
+ .DESCRIPTION
139
+ Reads the flag value from the first Structural gh pr create match, else gh pr edit.
140
+ Backslashes become forward slashes and one leading './' is removed (issue #824). A
141
+ rooted value is returned as written, with separators normalized, and is bound to the
142
+ resolved worktree by Get-PrAuthorBodyPathBindingReason (issue #850).
143
+ .PARAMETER CommandText
144
+ The Bash command text under evaluation.
145
+ .OUTPUTS
146
+ System.String or $null
147
+ #>
148
+ [CmdletBinding()]
149
+ [OutputType([string])]
150
+ param(
151
+ [Parameter(Mandatory)]
152
+ [string] $CommandText
153
+ )
154
+
155
+ $subcommandPath = @('pr', 'edit')
156
+ if (Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create')) {
157
+ $subcommandPath = @('pr', 'create')
158
+ }
159
+ $value = Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--body-file'
160
+ if ($null -eq $value) {
161
+ return $null
162
+ }
163
+ $value = $value.Replace('\', '/')
164
+ if ($value.StartsWith('./')) {
165
+ $value = $value.Substring(2)
166
+ }
167
+ return $value
168
+ }
169
+
123
170
  function Test-PrAuthorReceiptVerification {
124
171
  <#
125
172
  .SYNOPSIS
@@ -127,8 +174,9 @@ function Test-PrAuthorReceiptVerification {
127
174
  .DESCRIPTION
128
175
  Runs the six ordered receipt checks on the --body-file-with-context path. Each check is its
129
176
  own short-circuiting branch; the first failure returns its reason code:
130
- 1. PR_BODY_PATH_NONCANONICAL - --body-file path does not match the canonical
131
- artifacts/pr_body_<N>.md pattern (case-sensitive).
177
+ 1. PR_BODY_PATH_NONCANONICAL - --body-file path is not the canonical
178
+ artifacts/pr_body_<N>.md beneath the artifact
179
+ root (relative only for the session worktree).
132
180
  2. PR_AUTHOR_RECEIPT_MISSING - sibling artifacts/pr_body_<N>.receipt.json absent.
133
181
  3. PR_AUTHOR_RECEIPT_NUMBER_MISMATCH- receipt.number (integer) != <N> from the path.
134
182
  4. PR_AUTHOR_RECEIPT_HASH_MISMATCH - inline SHA-256 (lowercase hex) of the body bytes
@@ -139,9 +187,10 @@ function Test-PrAuthorReceiptVerification {
139
187
  matching --base <epic_context.integration_branch>.
140
188
  Returns $null when all six checks pass (allow). All disk access flows through the four
141
189
  injectable seams (Get-PrBodyFileBytes, Get-PrAuthorReceiptContent,
142
- Get-PrContextSummaryLastWriteUtc, Get-PrAuthorCheckpointContent); SHA-256 is computed
143
- inline. This is a policy-level integrity check, not a cryptographic control: any actor
144
- with Write access to artifacts/ can replace the body file and the receipt together.
190
+ Get-PrContextSummaryLastWriteUtc, Get-PrAuthorCheckpointContent), each given an absolute
191
+ path beneath the artifact root (issue #850); SHA-256 is computed inline. This is a
192
+ policy-level integrity check, not a cryptographic control: any actor with Write access
193
+ to artifacts/ can replace the body file and the receipt together.
145
194
  .PARAMETER CommandText
146
195
  The Bash command text containing the --body-file argument.
147
196
  .PARAMETER CheckpointPath
@@ -150,6 +199,10 @@ function Test-PrAuthorReceiptVerification {
150
199
  .PARAMETER EpicScope
151
200
  Optional. The epic-scope result Get-PrAuthorBypassReason resolved (issue #663), passed
152
201
  through to check 6 so the call is resolved once; $null outside epic scope.
202
+ .PARAMETER ArtifactRoot
203
+ The resolved worktree root the body, receipt, and summary are read beneath.
204
+ .PARAMETER RelativeBodyAllowed
205
+ Whether a relative --body-file is read from the resolved worktree (the session worktree).
153
206
  .OUTPUTS
154
207
  System.String or $null
155
208
  #>
@@ -163,18 +216,25 @@ function Test-PrAuthorReceiptVerification {
163
216
  [string] $CheckpointPath,
164
217
 
165
218
  [AllowNull()]
166
- [object] $EpicScope
219
+ [object] $EpicScope,
220
+
221
+ [Parameter(Mandatory)]
222
+ [string] $ArtifactRoot,
223
+
224
+ [bool] $RelativeBodyAllowed
167
225
  )
168
226
 
169
- # Check 1: the --body-file argument must match the canonical artifacts/pr_body_<N>.md pattern.
170
- # The match is case-sensitive (-cmatch) so a non-canonical path is rejected before any read.
171
- if ($CommandText -cnotmatch '--body-file\s+artifacts/pr_body_(\d+)\.md\b') {
172
- return "PR_BODY_PATH_NONCANONICAL: ``--body-file`` must reference a canonical ``artifacts/pr_body_<N>.md`` file produced by the pr-author skill. The path supplied does not match ``artifacts/pr_body_<N>.md``."
227
+ # Check 1: the --body-file value must be the canonical artifacts/pr_body_<N>.md beneath the
228
+ # artifact root (issue #850); C1a's structural read and spelling normalization are reused.
229
+ $binding = Get-PrAuthorBodyPathBindingReason -CommandText $CommandText -ArtifactRoot $ArtifactRoot -RelativeBodyAllowed $RelativeBodyAllowed
230
+ if ($binding.Reason) {
231
+ return $binding.Reason
173
232
  }
174
233
 
175
- $bodyNumber = [int]$Matches[1]
176
- $bodyFilePath = "artifacts/pr_body_$bodyNumber.md"
177
- $receiptFilePath = "artifacts/pr_body_$bodyNumber.receipt.json"
234
+ $bodyNumber = $binding.BodyNumber
235
+ $bodyFilePath = Join-WorktreeResolutionPath -WorktreeRoot $ArtifactRoot -RepoRelativePath "artifacts/pr_body_$bodyNumber.md"
236
+ $receiptFilePath = Join-WorktreeResolutionPath -WorktreeRoot $ArtifactRoot -RepoRelativePath "artifacts/pr_body_$bodyNumber.receipt.json"
237
+ $summaryPath = Join-WorktreeResolutionPath -WorktreeRoot $ArtifactRoot -RepoRelativePath $script:PrContextArtifactPath
178
238
 
179
239
  # Check 2: the sibling receipt file must exist (read via the injectable seam).
180
240
  $receiptRaw = Get-PrAuthorReceiptContent -ReceiptFilePath $receiptFilePath
@@ -228,7 +288,7 @@ function Test-PrAuthorReceiptVerification {
228
288
  return "PR_AUTHOR_RECEIPT_STALE: ``$receiptFilePath`` has a missing or unparseable ``created_at``. The pr-author agent must record a UTC ISO-8601 ``created_at`` strictly newer than ``$script:PrContextArtifactPath``."
229
289
  }
230
290
 
231
- $contextLastWrite = Get-PrContextSummaryLastWriteUtc
291
+ $contextLastWrite = Get-PrContextSummaryLastWriteUtc -Path $summaryPath
232
292
  if (($null -eq $contextLastWrite) -or ($createdAt -le $contextLastWrite)) {
233
293
  return "PR_AUTHOR_RECEIPT_STALE: ``$receiptFilePath`` ``created_at`` is not strictly newer than the last-write time of ``$script:PrContextArtifactPath``. The pr-author agent must regenerate the body and receipt after refreshing the PR context."
234
294
  }
@@ -248,17 +308,16 @@ function Get-PrAuthorBypassReason {
248
308
  Inspect the command text and return a block reason string, or $null when the command is allowed.
249
309
  .DESCRIPTION
250
310
  Returns PR_AUTHOR_SKILL_BLOCKED when gh pr create or gh pr edit is run with --body (inline,
251
- no --body-file), or when gh pr create is run with no body flag at all. Returns
252
- PR_CONTEXT_MISSING when --body-file is present but the context artifact does not exist on
253
- disk. When --body-file is present and the context artifact exists, verifies the SHA-256
254
- receipt via Test-PrAuthorReceiptVerification and returns its block reason
255
- (PR_BODY_PATH_NONCANONICAL / PR_AUTHOR_RECEIPT_*) when verification fails. Returns $null for
256
- all allowed patterns. Cases A, B, and C are evaluated first and unchanged; receipt
257
- verification only extends the previously-allowed --body-file-with-context path.
311
+ no --body-file), or when gh pr create is run with no body flag at all; these cases and the
312
+ gh pr edit no-body allow perform no resolution. For a --body-file command the artifact root
313
+ is resolved once (issue #850), and the deny precedence is: the resolution reason
314
+ (no-target or ambiguity code, or an underivable epic worktree), PR_CONTEXT_MISSING when the
315
+ context summary is absent beneath the resolved worktree, ORCHESTRATOR_STATE_PREFLIGHT_FAILED
316
+ unless Get-PrAuthorDocsOnlyExemption exempts a docs-only gh pr create (issue #901), then
317
+ receipt checks 1-6 via Test-PrAuthorReceiptVerification. Returns $null for all allowed
318
+ patterns.
258
319
  .PARAMETER CommandText
259
320
  The Bash command text extracted from the envelope's tool_input.
260
- .PARAMETER ContextExists
261
- Whether artifacts/pr_context.summary.txt currently exists on disk.
262
321
  .OUTPUTS
263
322
  System.String or $null
264
323
  #>
@@ -266,10 +325,7 @@ function Get-PrAuthorBypassReason {
266
325
  [OutputType([string])]
267
326
  param(
268
327
  [Parameter(Mandatory)]
269
- [string] $CommandText,
270
-
271
- [Parameter(Mandatory)]
272
- [bool] $ContextExists
328
+ [string] $CommandText
273
329
  )
274
330
 
275
331
  # Only act on gh pr create or gh pr edit subcommands. The test is structural: a segment
@@ -290,22 +346,21 @@ function Get-PrAuthorBypassReason {
290
346
  $hasBodyFile = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--body-file'
291
347
  $hasInlineBody = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--body'
292
348
 
293
- # Raw-scan fallback for a wrapper-led, live-substitution, or unbalanced segment. A
294
- # wrapper's quoted argument collapses into ONE token, so both flags read absent there and
295
- # the gh pr edit no-body branch below allowed bash -c "gh pr edit 42 --body 'x'". The
296
- # --body-file test runs first and wins, so a --body-file carried inside a wrapper is never
297
- # misread as an inline body. That ordering restores the pre-parser routing, in which one
298
- # whole-text --body-file match set $hasBodyFile for exactly this input.
349
+ # Raw-scan fallback (issue #824). Wrapper payloads are now read structurally, so the flags
350
+ # above already come from a payload's own tokens. The raw text is scanned only when the
351
+ # invocation could not be read structurally at all: no Structural match exists for the
352
+ # subcommand path and at least one Indeterminate match does. The --body-file test runs
353
+ # first and wins, so a --body-file is never misread as an inline body.
299
354
  if (-not $hasBodyFile -and -not $hasInlineBody) {
300
- $comparison = [System.StringComparison]::OrdinalIgnoreCase
301
- foreach ($segment in @(Read-CommandLineSegment -CommandText $CommandText)) {
302
- if (-not (Test-CommandLineSegmentRawScan -Segment $segment)) {
303
- continue
304
- }
305
- if ($segment.ScanText.IndexOf('--body-file', $comparison) -ge 0) {
306
- $hasBodyFile = $true
307
- } elseif ($segment.ScanText.IndexOf('--body', $comparison) -ge 0) {
308
- $hasInlineBody = $true
355
+ $matchList = @(Get-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath)
356
+ if (@($matchList | Where-Object { $_.Status -eq 'Structural' }).Count -eq 0) {
357
+ $comparison = [System.StringComparison]::OrdinalIgnoreCase
358
+ foreach ($match in @($matchList | Where-Object { $_.Status -eq 'Indeterminate' })) {
359
+ if ($match.Segment.RawText.IndexOf('--body-file', $comparison) -ge 0) {
360
+ $hasBodyFile = $true
361
+ } elseif ($match.Segment.RawText.IndexOf('--body', $comparison) -ge 0) {
362
+ $hasInlineBody = $true
363
+ }
309
364
  }
310
365
  }
311
366
  }
@@ -330,36 +385,34 @@ function Get-PrAuthorBypassReason {
330
385
  }
331
386
  }
332
387
 
333
- # Case C: --body-file present but context artifact is absent.
334
- if ($hasBodyFile -and -not $ContextExists) {
335
- return "PR_CONTEXT_MISSING: ``$script:PrContextArtifactPath`` is absent. Run ``mcp__drm-copilot__collect_pr_context`` before creating or editing the PR body."
388
+ # Issue #850: the worktree whose artifacts are read is resolved once, before Case C. Epic
389
+ # scope (issue #663) selects the epic checkpoint's worktree and reads no per-feature
390
+ # checkpoint; otherwise identity resolution selects the item worktree, and an unresolved
391
+ # target denies here, ahead of PR_CONTEXT_MISSING.
392
+ $artifact = Resolve-PrAuthorArtifactRoot -CommandText $CommandText
393
+ if ($artifact.Reason) {
394
+ return $artifact.Reason
336
395
  }
337
396
 
338
- # Epic scope (issue #663): when the call's --head equals the epic checkpoint's
339
- # integration_branch, the integration pull request is gated by the epic PR-creation
340
- # readiness predicate on artifacts/orchestration/epic-orchestrator-state.json, and no
341
- # per-feature checkpoint is resolved or read. Resolved once; check 6 reuses the result.
342
- $epicScope = $null
343
- if ($hasBodyFile -and $ContextExists) {
344
- $epicScope = Resolve-EpicScopeCheckpoint -Text $CommandText -SessionRoot (Get-Location).Path
397
+ # Case C: --body-file present but the context artifact is absent beneath the resolved worktree.
398
+ $summaryPath = Join-WorktreeResolutionPath -WorktreeRoot $artifact.ArtifactRoot -RepoRelativePath $script:PrContextArtifactPath
399
+ if (-not (Get-PrContextArtifactExistence -Path $summaryPath)) {
400
+ return "PR_CONTEXT_MISSING: '$script:PrContextArtifactPath' in worktree '$($artifact.ArtifactRoot)' is absent. Run ``mcp__drm-copilot__collect_pr_context`` in that worktree before creating or editing the PR body."
345
401
  }
346
402
 
347
403
  # Orchestrator-state preflight: runs inside this same PreToolUse hook (so it cannot be
348
- # bypassed by invoking gh pr create/edit directly) before receipt verification.
349
- if ($null -ne $epicScope -and $epicScope.IsEpicScope) {
404
+ # bypassed by invoking gh pr create/edit directly) before receipt verification. The
405
+ # checkpoint path is assigned once here and read by every later consumer, so the
406
+ # preflight, the receipt verifier, and the epic base-branch check gate one item.
407
+ if ($null -ne $artifact.EpicScope) {
408
+ $epicScope = $artifact.EpicScope
350
409
  $failure = Get-EpicPrCreationReadinessFailure -Checkpoint $epicScope.Checkpoint -HeadBranch $epicScope.Branch
351
410
  if ($failure) {
352
411
  return "ORCHESTRATOR_STATE_PREFLIGHT_FAILED: this epic-scope pull request was evaluated against $($epicScope.CheckpointPath), and the failed readiness predicate is '$failure'."
353
412
  }
354
- $script:OrchestratorStateCheckpointPath = $epicScope.CheckpointPath
355
- } elseif ($hasBodyFile -and $ContextExists) {
356
- $resolution = Get-PrAuthorTargetCheckpointResolution -CommandText $CommandText
357
- if ($resolution.Reason) {
358
- return $resolution.Reason
359
- }
360
- # Assigned once here and read by every later consumer, so the preflight, the receipt
361
- # verifier, and the epic base-branch check cannot disagree about which item is gated.
362
- $script:OrchestratorStateCheckpointPath = $resolution.CheckpointPath
413
+ $script:OrchestratorStateCheckpointPath = $artifact.CheckpointPath
414
+ } else {
415
+ $script:OrchestratorStateCheckpointPath = $artifact.CheckpointPath
363
416
  $checkpointPath = $script:OrchestratorStateCheckpointPath
364
417
  $preflightResult = Invoke-OrchestratorStatePreflight -CheckpointPath $checkpointPath
365
418
  if ($preflightResult.HasErrors) {
@@ -368,16 +421,17 @@ function Get-PrAuthorBypassReason {
368
421
  } else {
369
422
  $preflightResult.ErrorText
370
423
  }
371
- return "ORCHESTRATOR_STATE_PREFLIGHT_FAILED: $preflightSummary"
424
+ $exemption = Get-PrAuthorDocsOnlyExemption -CommandText $CommandText -WorktreeRoot $artifact.ArtifactRoot -CheckpointPath $checkpointPath
425
+ if (-not $exemption.Exempt) {
426
+ return "ORCHESTRATOR_STATE_PREFLIGHT_FAILED: $preflightSummary (docs-only exemption not applied: $($exemption.Reason))"
427
+ }
372
428
  }
373
429
  }
374
430
 
375
431
  # Receipt verification: extends, and does not replace, the previously-allowed path.
376
- if ($hasBodyFile -and $ContextExists) {
377
- $receiptReason = Test-PrAuthorReceiptVerification -CommandText $CommandText -CheckpointPath $script:OrchestratorStateCheckpointPath -EpicScope $epicScope
378
- if ($receiptReason) {
379
- return $receiptReason
380
- }
432
+ $receiptReason = Test-PrAuthorReceiptVerification -CommandText $CommandText -CheckpointPath $script:OrchestratorStateCheckpointPath -EpicScope $artifact.EpicScope -ArtifactRoot $artifact.ArtifactRoot -RelativeBodyAllowed $artifact.RelativeBodyAllowed
433
+ if ($receiptReason) {
434
+ return $receiptReason
381
435
  }
382
436
 
383
437
  return $null