@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.
- package/out/mcp-server.js +542 -42
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +172 -24
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +402 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
- package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
- package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +124 -1
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
- package/resources/claude-customizations/.claude/settings.json +5 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
- package/resources/claude-customizations/config/blast-radius.json +16 -2
- package/resources/claude-customizations/pack-manifests/core.json +22 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- 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
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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 = $
|
|
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
|
-
|
|
218
|
-
|
|
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
|
-
$
|
|
222
|
-
|
|
223
|
-
$sessionId = 'default'
|
|
224
|
-
}
|
|
241
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
242
|
+
)
|
|
225
243
|
|
|
226
|
-
$
|
|
227
|
-
$
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
232
|
-
|
|
270
|
+
|
|
271
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
272
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
273
|
+
return
|
|
233
274
|
}
|
|
234
275
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
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
|
+
}
|