@danmoisan/drm-copilot-mcp 1.0.26 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/out/mcp-server.js +542 -42
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
  4. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
  5. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
  6. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
  7. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +172 -24
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
  12. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
  13. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
  14. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +402 -0
  15. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
  19. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
  20. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
  21. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
  22. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
  23. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
  24. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
  25. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
  26. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
  27. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
  28. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
  29. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
  30. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
  31. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
  32. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
  33. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
  34. package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
  35. package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
  36. package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
  37. package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
  38. package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
  39. package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
  40. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +124 -1
  41. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
  42. package/resources/claude-customizations/.claude/settings.json +5 -0
  43. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
  44. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  45. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
  46. package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
  47. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
  48. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
  49. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
  50. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
  51. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
  52. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
  53. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
  54. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
  55. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
  56. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
  57. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
  58. package/resources/claude-customizations/config/blast-radius.json +16 -2
  59. package/resources/claude-customizations/pack-manifests/core.json +22 -1
  60. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  61. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  62. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  63. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +31 -1
@@ -37,6 +37,8 @@
37
37
  [CmdletBinding()]
38
38
  param()
39
39
 
40
+
41
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
40
42
  function Get-PowerShellBatchBudgetState {
41
43
  [CmdletBinding()]
42
44
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -165,17 +167,15 @@ function Invoke-PowerShellBatchBudgetHook {
165
167
  }
166
168
  )
167
169
 
168
- if (-not $ToolInputRaw) {
169
- return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
170
- }
171
-
172
- try {
173
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
174
- } catch {
175
- return Get-PowerShellBatchBudgetBlockDecision -Reason 'PowerShell batch-budget hook received malformed JSON in CLAUDE_TOOL_INPUT.'
170
+ $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
171
+ if (-not $payload.IsValid) {
172
+ return Get-PowerShellBatchBudgetBlockDecision -Reason (
173
+ 'PowerShell batch-budget hook received an unreadable PreToolUse envelope: ' +
174
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
175
+ '. The gate fails closed on an envelope it cannot read.')
176
176
  }
177
177
 
178
- $filePath = $toolInput.file_path
178
+ $filePath = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'file_path'
179
179
  if (-not $filePath) {
180
180
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
181
181
  }
@@ -214,28 +214,71 @@ function Invoke-PowerShellBatchBudgetHook {
214
214
  return $decision
215
215
  }
216
216
 
217
- if ($MyInvocation.InvocationName -eq '.') {
218
- return
219
- }
217
+ function Invoke-PowerShellBatchBudgetEntryPoint {
218
+ <#
219
+ .SYNOPSIS
220
+ Runs the PowerShell batch-budget decision and returns the process exit code.
221
+ .DESCRIPTION
222
+ Wraps the dispatch logic that the hook entry point performs so it can be
223
+ exercised by unit tests. It acquires the payload through the shared reader
224
+ unless the caller supplies one, writes the compact JSON decision to the output
225
+ stream only when the decision is a deny (this hook is deny-only: an allow
226
+ decision emits nothing), and returns 0. This function does not call exit; the
227
+ thin entry-point wiring converts the returned code into a process exit.
228
+ .PARAMETER ToolInputRaw
229
+ Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
230
+ .PARAMETER ReadPayload
231
+ Seam for payload acquisition, so tests can drive the empty-on-all-transports
232
+ case without touching a console.
233
+ #>
234
+ [CmdletBinding()]
235
+ [OutputType([int])]
236
+ param(
237
+ [AllowNull()]
238
+ [AllowEmptyString()]
239
+ [string] $ToolInputRaw,
220
240
 
221
- $sessionId = $env:CLAUDE_SESSION_ID
222
- if (-not $sessionId) {
223
- $sessionId = 'default'
224
- }
241
+ [scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
242
+ )
225
243
 
226
- $prodCap = 3
227
- $testCap = 3
228
- if ($env:CLAUDE_POWERSHELL_BUDGET_PROD -match '^\d+$') {
229
- $prodCap = [int]$env:CLAUDE_POWERSHELL_BUDGET_PROD
244
+ if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
245
+ $ToolInputRaw = [string](& $ReadPayload)
246
+ }
247
+
248
+ $sessionId = $env:CLAUDE_SESSION_ID
249
+ if (-not $sessionId) {
250
+ $sessionId = 'default'
251
+ }
252
+
253
+ $prodCap = 3
254
+ $testCap = 3
255
+ if ($env:CLAUDE_POWERSHELL_BUDGET_PROD -match '^\d+$') {
256
+ $prodCap = [int]$env:CLAUDE_POWERSHELL_BUDGET_PROD
257
+ }
258
+ if ($env:CLAUDE_POWERSHELL_BUDGET_TEST -match '^\d+$') {
259
+ $testCap = [int]$env:CLAUDE_POWERSHELL_BUDGET_TEST
260
+ }
261
+
262
+ $decision = Invoke-PowerShellBatchBudgetHook -ToolInputRaw $ToolInputRaw -SessionId $sessionId -ProdCap $prodCap -TestCap $testCap
263
+ if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
264
+ $decision.Remove('state')
265
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
266
+ }
267
+
268
+ return 0
230
269
  }
231
- if ($env:CLAUDE_POWERSHELL_BUDGET_TEST -match '^\d+$') {
232
- $testCap = [int]$env:CLAUDE_POWERSHELL_BUDGET_TEST
270
+
271
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
272
+ if ($MyInvocation.InvocationName -eq '.') {
273
+ return
233
274
  }
234
275
 
235
- $decision = Invoke-PowerShellBatchBudgetHook -ToolInputRaw $env:CLAUDE_TOOL_INPUT -SessionId $sessionId -ProdCap $prodCap -TestCap $testCap
236
- if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
237
- $decision.Remove('state')
238
- $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
276
+ # The entry point returns its [int] exit code as the last pipeline element and the
277
+ # decision JSON before it. `exit (<call>)` would capture BOTH into the exit
278
+ # expression and emit nothing, so the decision is written explicitly here first.
279
+ $entryPointResult = @(Invoke-PowerShellBatchBudgetEntryPoint)
280
+ if ($entryPointResult.Count -gt 1) {
281
+ $entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
239
282
  }
240
283
 
241
- exit 0
284
+ exit ([int]$entryPointResult[-1])
@@ -0,0 +1,228 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Dot-sourced receipt-verification and bypass-reason helpers for enforce-pr-author-skill.ps1.
4
+
5
+ .DESCRIPTION
6
+ Holds the two decision helpers the pr-author gate uses once a Bash command has been
7
+ identified as a gh pr create or gh pr edit invocation:
8
+
9
+ - Test-PrAuthorReceiptVerification: the six ordered SHA-256 receipt checks.
10
+ - Get-PrAuthorBypassReason: cases A, B, and C plus the orchestrator-state preflight
11
+ and the receipt-verification call.
12
+
13
+ They are split out of the parent hook, .claude/hooks/enforce-pr-author-skill.ps1,
14
+ because that file had no headroom left under the 500-line limit once the shared
15
+ payload-reader migration (issue #501) added the module import, the envelope-anomaly
16
+ mapping, and the entry-point seam. The split is a pure move with no behaviour change.
17
+
18
+ Both helpers read the script-scoped configuration variables the parent hook assigns
19
+ ($script:PrContextArtifactPath, $script:OrchestratorStateCheckpointPath) and call the
20
+ parent hook's injectable read seams, so this file is only ever dot-sourced from that
21
+ hook and never invoked on its own.
22
+
23
+ .NOTES
24
+ Compatible with PowerShell 7+. No external module dependencies.
25
+ #>
26
+ [CmdletBinding()]
27
+ param()
28
+
29
+ function Test-PrAuthorReceiptVerification {
30
+ <#
31
+ .SYNOPSIS
32
+ Verify the SHA-256 receipt and return a block-reason string, or $null when verified.
33
+ .DESCRIPTION
34
+ Runs the six ordered receipt checks on the --body-file-with-context path. Each check is its
35
+ own short-circuiting branch; the first failure returns its reason code:
36
+ 1. PR_BODY_PATH_NONCANONICAL - --body-file path does not match the canonical
37
+ artifacts/pr_body_<N>.md pattern (case-sensitive).
38
+ 2. PR_AUTHOR_RECEIPT_MISSING - sibling artifacts/pr_body_<N>.receipt.json absent.
39
+ 3. PR_AUTHOR_RECEIPT_NUMBER_MISMATCH- receipt.number (integer) != <N> from the path.
40
+ 4. PR_AUTHOR_RECEIPT_HASH_MISMATCH - inline SHA-256 (lowercase hex) of the body bytes
41
+ != receipt.sha256.
42
+ 5. PR_AUTHOR_RECEIPT_STALE - receipt.created_at (UTC) not strictly newer than the
43
+ context summary last-write time.
44
+ 6. EPIC_BASE_BRANCH_MISMATCH - under epic_mode, gh pr create does not carry a
45
+ matching --base <epic_context.integration_branch>.
46
+ Returns $null when all six checks pass (allow). All disk access flows through the four
47
+ injectable seams (Get-PrBodyFileBytes, Get-PrAuthorReceiptContent,
48
+ Get-PrContextSummaryLastWriteUtc, Get-PrAuthorCheckpointContent); SHA-256 is computed
49
+ inline. This is a policy-level integrity check, not a cryptographic control: any actor
50
+ with Write access to artifacts/ can replace the body file and the receipt together.
51
+ .PARAMETER CommandText
52
+ The Bash command text containing the --body-file argument.
53
+ .OUTPUTS
54
+ System.String or $null
55
+ #>
56
+ [CmdletBinding()]
57
+ [OutputType([string])]
58
+ param(
59
+ [Parameter(Mandatory)]
60
+ [string] $CommandText
61
+ )
62
+
63
+ # Check 1: the --body-file argument must match the canonical artifacts/pr_body_<N>.md pattern.
64
+ # The match is case-sensitive (-cmatch) so a non-canonical path is rejected before any read.
65
+ if ($CommandText -cnotmatch '--body-file\s+artifacts/pr_body_(\d+)\.md\b') {
66
+ 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``."
67
+ }
68
+
69
+ $bodyNumber = [int]$Matches[1]
70
+ $bodyFilePath = "artifacts/pr_body_$bodyNumber.md"
71
+ $receiptFilePath = "artifacts/pr_body_$bodyNumber.receipt.json"
72
+
73
+ # Check 2: the sibling receipt file must exist (read via the injectable seam).
74
+ $receiptRaw = Get-PrAuthorReceiptContent -ReceiptFilePath $receiptFilePath
75
+ if ([string]::IsNullOrWhiteSpace($receiptRaw)) {
76
+ return "PR_AUTHOR_RECEIPT_MISSING: ``$receiptFilePath`` is absent. The pr-author agent must write the SHA-256 receipt alongside ``$bodyFilePath`` before issuing ``gh pr create``/``gh pr edit --body-file``."
77
+ }
78
+
79
+ # A receipt that is present but not valid JSON cannot be verified; treat it as missing content.
80
+ try {
81
+ $receipt = $receiptRaw | ConvertFrom-Json -ErrorAction Stop
82
+ } catch {
83
+ return "PR_AUTHOR_RECEIPT_MISSING: ``$receiptFilePath`` is not valid JSON. The pr-author agent must write a well-formed receipt with ``number``, ``sha256``, and ``created_at``."
84
+ }
85
+
86
+ # Check 3: the receipt number must equal the <N> extracted from the canonical path.
87
+ $receiptNumber = $null
88
+ $parsedNumber = 0
89
+ if ([int]::TryParse([string]$receipt.number, [ref] $parsedNumber)) {
90
+ $receiptNumber = $parsedNumber
91
+ }
92
+ if ($receiptNumber -ne $bodyNumber) {
93
+ return "PR_AUTHOR_RECEIPT_NUMBER_MISMATCH: ``$receiptFilePath`` ``number`` ($($receipt.number)) does not equal the body-file number ($bodyNumber). The receipt must bind to ``$bodyFilePath``."
94
+ }
95
+
96
+ # Check 4: the inline SHA-256 (lowercase hex) of the body bytes must equal receipt.sha256.
97
+ $bodyBytes = Get-PrBodyFileBytes -BodyFilePath $bodyFilePath
98
+ if ($null -eq $bodyBytes) {
99
+ return "PR_AUTHOR_RECEIPT_HASH_MISMATCH: ``$bodyFilePath`` could not be read to verify its SHA-256 against ``$receiptFilePath``."
100
+ }
101
+
102
+ $sha256 = [System.Security.Cryptography.SHA256]::Create()
103
+ try {
104
+ $hashBytes = $sha256.ComputeHash($bodyBytes)
105
+ } finally {
106
+ $sha256.Dispose()
107
+ }
108
+ $computedHash = ([System.BitConverter]::ToString($hashBytes) -replace '-', '').ToLowerInvariant()
109
+
110
+ if ($computedHash -ne ([string]$receipt.sha256).ToLowerInvariant()) {
111
+ return "PR_AUTHOR_RECEIPT_HASH_MISMATCH: the SHA-256 of ``$bodyFilePath`` does not equal ``sha256`` in ``$receiptFilePath``. The body file was modified after the receipt was written."
112
+ }
113
+
114
+ # Check 5: receipt.created_at (UTC) must be strictly newer than the context summary last-write.
115
+ $createdAt = [DateTime]::MinValue
116
+ $createdParsed = [DateTime]::TryParse(
117
+ [string]$receipt.created_at,
118
+ [System.Globalization.CultureInfo]::InvariantCulture,
119
+ [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
120
+ [ref] $createdAt)
121
+ if (-not $createdParsed) {
122
+ 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``."
123
+ }
124
+
125
+ $contextLastWrite = Get-PrContextSummaryLastWriteUtc
126
+ if (($null -eq $contextLastWrite) -or ($createdAt -le $contextLastWrite)) {
127
+ 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."
128
+ }
129
+
130
+ # Check 6: under epic_mode, gh pr create must carry a matching --base override.
131
+ $epicBaseBranchReason = Test-EpicBaseBranchOverride -CommandText $CommandText
132
+ if ($epicBaseBranchReason) {
133
+ return $epicBaseBranchReason
134
+ }
135
+
136
+ return $null
137
+ }
138
+
139
+ function Get-PrAuthorBypassReason {
140
+ <#
141
+ .SYNOPSIS
142
+ Inspect the command text and return a block reason string, or $null when the command is allowed.
143
+ .DESCRIPTION
144
+ Returns PR_AUTHOR_SKILL_BLOCKED when gh pr create or gh pr edit is run with --body (inline,
145
+ no --body-file), or when gh pr create is run with no body flag at all. Returns
146
+ PR_CONTEXT_MISSING when --body-file is present but the context artifact does not exist on
147
+ disk. When --body-file is present and the context artifact exists, verifies the SHA-256
148
+ receipt via Test-PrAuthorReceiptVerification and returns its block reason
149
+ (PR_BODY_PATH_NONCANONICAL / PR_AUTHOR_RECEIPT_*) when verification fails. Returns $null for
150
+ all allowed patterns. Cases A, B, and C are evaluated first and unchanged; receipt
151
+ verification only extends the previously-allowed --body-file-with-context path.
152
+ .PARAMETER CommandText
153
+ The Bash command text extracted from the envelope's tool_input.
154
+ .PARAMETER ContextExists
155
+ Whether artifacts/pr_context.summary.txt currently exists on disk.
156
+ .OUTPUTS
157
+ System.String or $null
158
+ #>
159
+ [CmdletBinding()]
160
+ [OutputType([string])]
161
+ param(
162
+ [Parameter(Mandatory)]
163
+ [string] $CommandText,
164
+
165
+ [Parameter(Mandatory)]
166
+ [bool] $ContextExists
167
+ )
168
+
169
+ # Only act on gh pr create or gh pr edit subcommands.
170
+ $isPrCreate = $CommandText -match '(?i)\bgh\s+pr\s+create\b'
171
+ $isPrEdit = $CommandText -match '(?i)\bgh\s+pr\s+edit\b'
172
+
173
+ if (-not $isPrCreate -and -not $isPrEdit) {
174
+ return $null
175
+ }
176
+
177
+ $hasBodyFile = $CommandText -match '(?i)--body-file\b'
178
+ $hasInlineBody = $CommandText -match '(?i)--body(?!-file)\b'
179
+
180
+ # Case A: gh pr create OR gh pr edit with inline --body (not --body-file). Evaluated before the
181
+ # gh pr edit no-body allow short-circuit so inline-body edits are blocked, not allowed.
182
+ if (($isPrCreate -or $isPrEdit) -and $hasInlineBody -and -not $hasBodyFile) {
183
+ return "PR_AUTHOR_SKILL_BLOCKED: ``gh pr create`` and ``gh pr edit`` must use ``--body-file`` with a file produced by the pr-author skill from ``$script:PrContextArtifactPath``. Run ``mcp__drm-copilot__collect_pr_context`` to generate the context file, apply the pr-author skill to produce ``artifacts/pr_body_<N>.md``, then pass that file via ``--body-file``."
184
+ }
185
+
186
+ if ($isPrCreate) {
187
+ # Case B: gh pr create with no body flag at all.
188
+ if (-not $hasInlineBody -and -not $hasBodyFile) {
189
+ return "PR_AUTHOR_SKILL_BLOCKED: New PRs require ``--body-file``. Run ``mcp__drm-copilot__collect_pr_context`` to generate ``$script:PrContextArtifactPath``, apply the pr-author skill to produce ``artifacts/pr_body_<N>.md``, then pass that file via ``--body-file``."
190
+ }
191
+ }
192
+
193
+ if ($isPrEdit) {
194
+ # gh pr edit with no --body or --body-file (e.g., --title, --add-label, --reviewer) is allowed.
195
+ if (-not $hasInlineBody -and -not $hasBodyFile) {
196
+ return $null
197
+ }
198
+ }
199
+
200
+ # Case C: --body-file present but context artifact is absent.
201
+ if ($hasBodyFile -and -not $ContextExists) {
202
+ return "PR_CONTEXT_MISSING: ``$script:PrContextArtifactPath`` is absent. Run ``mcp__drm-copilot__collect_pr_context`` before creating or editing the PR body."
203
+ }
204
+
205
+ # Orchestrator-state preflight: runs inside this same PreToolUse hook (so it cannot be
206
+ # bypassed by invoking gh pr create/edit directly) before receipt verification.
207
+ if ($hasBodyFile -and $ContextExists) {
208
+ $preflightResult = Invoke-OrchestratorStatePreflight -CheckpointPath $script:OrchestratorStateCheckpointPath
209
+ if ($preflightResult.HasErrors) {
210
+ $preflightSummary = if ([string]::IsNullOrWhiteSpace($preflightResult.ErrorText)) {
211
+ "checkpoint missing at $script:OrchestratorStateCheckpointPath"
212
+ } else {
213
+ $preflightResult.ErrorText
214
+ }
215
+ return "ORCHESTRATOR_STATE_PREFLIGHT_FAILED: $preflightSummary"
216
+ }
217
+ }
218
+
219
+ # Receipt verification: extends, and does not replace, the previously-allowed path.
220
+ if ($hasBodyFile -and $ContextExists) {
221
+ $receiptReason = Test-PrAuthorReceiptVerification -CommandText $CommandText
222
+ if ($receiptReason) {
223
+ return $receiptReason
224
+ }
225
+ }
226
+
227
+ return $null
228
+ }