@danmoisan/drm-copilot-mcp 1.0.0 → 1.0.7
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 +814 -194
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/commit-message.md +42 -0
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +127 -0
- package/resources/claude-customizations/.claude/agents/human-exception-runbook.md +48 -0
- package/resources/claude-customizations/.claude/agents/orchestrator.md +67 -2
- package/resources/claude-customizations/.claude/agents/pr-author.md +37 -21
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +103 -65
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +12 -9
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +24 -16
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +19 -13
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +304 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +304 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +237 -0
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +16 -12
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +14 -10
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +35 -8
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +17 -14
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +104 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +268 -93
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +19 -11
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +28 -6
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +17 -14
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +146 -36
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +19 -7
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +36 -0
- package/resources/claude-customizations/.claude/settings.json +34 -1
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +252 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +42 -13
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +28 -0
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +22 -18
- package/resources/claude-customizations/pack-manifests/core.json +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +6 -3
- package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +65 -0
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +79 -0
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +66 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -25
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +257 -93
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +98 -0
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +24 -0
- package/resources/codex-and-agents-customizations/pack-manifests/csharp-legacy.json +11 -0
- package/resources/codex-and-agents-customizations/pack-manifests/csharp-modern.json +10 -0
- package/resources/codex-and-agents-customizations/pack-manifests/powershell.json +15 -0
- package/resources/codex-and-agents-customizations/pack-manifests/python.json +15 -0
- package/resources/codex-and-agents-customizations/pack-manifests/typescript.json +9 -0
- package/resources/config/orchestration-routing.json +92 -0
- package/resources/customizations/.github/agents/pr-author.agent.md +24 -18
- package/resources/icon.png +0 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +11 -0
- package/resources/codex-and-agents-customizations/.github/workflows/_validate-orchestrator-state.yml +0 -68
- package/resources/codex-and-agents-customizations/.github/workflows/validate-orchestrator-state.yml +0 -15
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
Required sequence:
|
|
12
12
|
1. mcp__drm-copilot__collect_pr_context writes artifacts/pr_context.summary.txt
|
|
13
|
-
2. pr-author skill
|
|
13
|
+
2. pr-author skill produces the body text; the pr-author agent writes
|
|
14
|
+
artifacts/pr_body_<N>.md and a sibling integrity receipt
|
|
15
|
+
artifacts/pr_body_<N>.receipt.json
|
|
14
16
|
3. gh pr create --body-file artifacts/pr_body_<N>.md
|
|
15
17
|
(or gh pr edit --body-file ...)
|
|
16
18
|
|
|
@@ -18,35 +20,72 @@
|
|
|
18
20
|
Case A - gh pr create or gh pr edit with --body (inline, no --body-file): blocked.
|
|
19
21
|
Case B - gh pr create with neither --body nor --body-file: blocked.
|
|
20
22
|
Case C - gh pr create or gh pr edit with --body-file but context artifact absent: blocked.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
ttl_seconds: blocked (PR_AGENT_AUTHORIZATION_EXPIRED).
|
|
30
|
-
|
|
31
|
-
Authorization sentinel decision order on the --body-file-with-context path:
|
|
32
|
-
missing -> malformed -> invalid issuer -> expired -> allow.
|
|
23
|
+
Preflight - --body-file/context present: orchestrator-state checkpoint must pass
|
|
24
|
+
--require-pr-creation-ready before receipt verification runs, else blocked.
|
|
25
|
+
Receipt - preflight passed: the SHA-256 receipt is verified in five ordered checks
|
|
26
|
+
(Section below). The first failing check blocks.
|
|
27
|
+
|
|
28
|
+
Receipt verification decision order on the --body-file-with-context path:
|
|
29
|
+
PR_BODY_PATH_NONCANONICAL -> PR_AUTHOR_RECEIPT_MISSING -> PR_AUTHOR_RECEIPT_NUMBER_MISMATCH
|
|
30
|
+
-> PR_AUTHOR_RECEIPT_HASH_MISMATCH -> PR_AUTHOR_RECEIPT_STALE -> allow.
|
|
33
31
|
|
|
34
32
|
.NOTES
|
|
35
33
|
Compatible with PowerShell 7+. No external module dependencies.
|
|
36
34
|
|
|
37
|
-
Enforcement strength: the
|
|
38
|
-
|
|
39
|
-
artifacts/
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
35
|
+
Enforcement strength: the SHA-256 receipt is a policy-level integrity check that binds the
|
|
36
|
+
PR body bytes to the receipt the pr-author agent wrote. It is not a cryptographic or security
|
|
37
|
+
boundary: any actor with Write access to artifacts/ can replace both the body file and the
|
|
38
|
+
receipt together, because all agents share the same filesystem and the runtime exposes no
|
|
39
|
+
native agent-identity signal at Bash PreToolUse time. The mechanism prevents accidental bypass
|
|
40
|
+
and requires a deliberate, documented act to circumvent. It MUST NOT be described as
|
|
41
|
+
tamper-proof or as a security boundary.
|
|
43
42
|
#>
|
|
44
43
|
[CmdletBinding()]
|
|
45
44
|
param()
|
|
46
45
|
|
|
47
46
|
$script:PrContextArtifactPath = 'artifacts/pr_context.summary.txt'
|
|
48
|
-
$script:
|
|
49
|
-
|
|
47
|
+
$script:OrchestratorStateCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
48
|
+
|
|
49
|
+
function Invoke-OrchestratorStatePreflight {
|
|
50
|
+
<#
|
|
51
|
+
.SYNOPSIS
|
|
52
|
+
Runs the orchestrator-state validator against the checkpoint and reports pass/fail.
|
|
53
|
+
.DESCRIPTION
|
|
54
|
+
Mirrors Invoke-RoutingContractValidation (.claude/hooks/validate-orchestrator-output.ps1):
|
|
55
|
+
an injectable subprocess scriptblock seam defaults to ``python -m
|
|
56
|
+
scripts.dev_tools.validate_orchestration_artifacts orchestrator-state <CheckpointPath>
|
|
57
|
+
--require-pr-creation-ready``. A missing checkpoint or --require-pr-creation-ready failure
|
|
58
|
+
both surface via the validator's non-zero exit/stderr text; no separate file-existence check
|
|
59
|
+
is made, validating pre-PR-creation readiness (steps 5-8, blocked_reason) not full completion.
|
|
60
|
+
.OUTPUTS
|
|
61
|
+
System.Collections.Hashtable with keys HasErrors (bool) and ErrorText (string).
|
|
62
|
+
#>
|
|
63
|
+
[CmdletBinding()]
|
|
64
|
+
[OutputType([hashtable])]
|
|
65
|
+
param(
|
|
66
|
+
[Parameter(Mandatory = $false)]
|
|
67
|
+
[string] $CheckpointPath = $script:OrchestratorStateCheckpointPath,
|
|
68
|
+
|
|
69
|
+
[Parameter(Mandatory = $false)]
|
|
70
|
+
[scriptblock] $Invoker = {
|
|
71
|
+
param($Path)
|
|
72
|
+
$output = & python -m scripts.dev_tools.validate_orchestration_artifacts `
|
|
73
|
+
orchestrator-state $Path --require-pr-creation-ready 2>&1
|
|
74
|
+
[pscustomobject]@{
|
|
75
|
+
ExitCode = $LASTEXITCODE
|
|
76
|
+
Output = ($output | Out-String)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
$result = & $Invoker $CheckpointPath
|
|
82
|
+
$exitCode = 0
|
|
83
|
+
if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'ExitCode')) { $exitCode = [int]$result.ExitCode }
|
|
84
|
+
$outputText = ''
|
|
85
|
+
if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Output')) { $outputText = ([string]$result.Output).Trim() }
|
|
86
|
+
|
|
87
|
+
return @{ HasErrors = ($exitCode -ne 0); ErrorText = $outputText }
|
|
88
|
+
}
|
|
50
89
|
|
|
51
90
|
function Get-PrContextArtifactExistence {
|
|
52
91
|
<#
|
|
@@ -62,108 +101,190 @@ function Get-PrContextArtifactExistence {
|
|
|
62
101
|
return [bool](Test-Path -LiteralPath $script:PrContextArtifactPath)
|
|
63
102
|
}
|
|
64
103
|
|
|
65
|
-
function Get-
|
|
104
|
+
function Get-PrBodyFileBytes {
|
|
105
|
+
<#
|
|
106
|
+
.SYNOPSIS
|
|
107
|
+
Read the raw bytes of the PR body file. Tests mock this function (read seam).
|
|
108
|
+
.DESCRIPTION
|
|
109
|
+
Returns the byte content of the supplied body-file path, or $null when the file is absent.
|
|
110
|
+
This is the injectable boundary for body-file bytes in tests; no test writes the body file
|
|
111
|
+
to disk. The bytes are hashed inline by the receipt verification function.
|
|
112
|
+
.PARAMETER BodyFilePath
|
|
113
|
+
The relative path to the PR body file (for example artifacts/pr_body_5.md).
|
|
114
|
+
.OUTPUTS
|
|
115
|
+
System.Byte[] or $null
|
|
116
|
+
#>
|
|
117
|
+
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'The plural noun names the byte-array return; the seam name is fixed by the receipt contract.')]
|
|
118
|
+
[CmdletBinding()]
|
|
119
|
+
[OutputType([byte[]])]
|
|
120
|
+
param(
|
|
121
|
+
[Parameter(Mandatory)]
|
|
122
|
+
[string] $BodyFilePath
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
if (-not (Test-Path -LiteralPath $BodyFilePath)) {
|
|
126
|
+
return $null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return [System.IO.File]::ReadAllBytes($BodyFilePath)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Get-PrAuthorReceiptContent {
|
|
66
133
|
<#
|
|
67
134
|
.SYNOPSIS
|
|
68
|
-
Read the raw text of the
|
|
135
|
+
Read the raw JSON text of the PR body receipt. Tests mock this function (read seam).
|
|
69
136
|
.DESCRIPTION
|
|
70
|
-
Returns the raw text content of artifacts/
|
|
71
|
-
|
|
72
|
-
test writes the
|
|
137
|
+
Returns the raw text content of the sibling receipt file artifacts/pr_body_<N>.receipt.json,
|
|
138
|
+
or $null when the receipt file is absent. This is the injectable boundary for receipt
|
|
139
|
+
content in tests; no test writes the receipt file to disk.
|
|
140
|
+
.PARAMETER ReceiptFilePath
|
|
141
|
+
The relative path to the receipt file (for example artifacts/pr_body_5.receipt.json).
|
|
73
142
|
.OUTPUTS
|
|
74
143
|
System.String or $null
|
|
75
144
|
#>
|
|
76
145
|
[CmdletBinding()]
|
|
77
146
|
[OutputType([string])]
|
|
78
|
-
param(
|
|
147
|
+
param(
|
|
148
|
+
[Parameter(Mandatory)]
|
|
149
|
+
[string] $ReceiptFilePath
|
|
150
|
+
)
|
|
79
151
|
|
|
80
|
-
if (-not (Test-Path -LiteralPath $
|
|
152
|
+
if (-not (Test-Path -LiteralPath $ReceiptFilePath)) {
|
|
81
153
|
return $null
|
|
82
154
|
}
|
|
83
155
|
|
|
84
|
-
return (Get-Content -LiteralPath $
|
|
156
|
+
return (Get-Content -LiteralPath $ReceiptFilePath -Raw)
|
|
85
157
|
}
|
|
86
158
|
|
|
87
|
-
function Get-
|
|
159
|
+
function Get-PrContextSummaryLastWriteUtc {
|
|
88
160
|
<#
|
|
89
161
|
.SYNOPSIS
|
|
90
|
-
Return the
|
|
162
|
+
Return the UTC last-write time of the PR context summary. Tests mock this function (seam).
|
|
163
|
+
.DESCRIPTION
|
|
164
|
+
Returns the LastWriteTimeUtc of artifacts/pr_context.summary.txt, or $null when the file is
|
|
165
|
+
absent. The staleness check compares the receipt's created_at against this value; both are
|
|
166
|
+
artifact metadata, so no wall-clock seam is required.
|
|
91
167
|
.OUTPUTS
|
|
92
|
-
System.DateTime
|
|
168
|
+
System.DateTime or $null
|
|
93
169
|
#>
|
|
94
170
|
[CmdletBinding()]
|
|
95
171
|
[OutputType([datetime])]
|
|
96
172
|
param()
|
|
97
173
|
|
|
98
|
-
|
|
174
|
+
if (-not (Test-Path -LiteralPath $script:PrContextArtifactPath)) {
|
|
175
|
+
return $null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return (Get-Item -LiteralPath $script:PrContextArtifactPath).LastWriteTimeUtc
|
|
99
179
|
}
|
|
100
180
|
|
|
101
|
-
|
|
181
|
+
. (Join-Path $PSScriptRoot 'enforce-pr-author-skill.epic-base-branch.ps1')
|
|
182
|
+
|
|
183
|
+
function Test-PrAuthorReceiptVerification {
|
|
102
184
|
<#
|
|
103
185
|
.SYNOPSIS
|
|
104
|
-
|
|
186
|
+
Verify the SHA-256 receipt and return a block-reason string, or $null when verified.
|
|
105
187
|
.DESCRIPTION
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
-
|
|
111
|
-
|
|
112
|
-
-
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
188
|
+
Runs the six ordered receipt checks on the --body-file-with-context path. Each check is its
|
|
189
|
+
own short-circuiting branch; the first failure returns its reason code:
|
|
190
|
+
1. PR_BODY_PATH_NONCANONICAL - --body-file path does not match the canonical
|
|
191
|
+
artifacts/pr_body_<N>.md pattern (case-sensitive).
|
|
192
|
+
2. PR_AUTHOR_RECEIPT_MISSING - sibling artifacts/pr_body_<N>.receipt.json absent.
|
|
193
|
+
3. PR_AUTHOR_RECEIPT_NUMBER_MISMATCH- receipt.number (integer) != <N> from the path.
|
|
194
|
+
4. PR_AUTHOR_RECEIPT_HASH_MISMATCH - inline SHA-256 (lowercase hex) of the body bytes
|
|
195
|
+
!= receipt.sha256.
|
|
196
|
+
5. PR_AUTHOR_RECEIPT_STALE - receipt.created_at (UTC) not strictly newer than the
|
|
197
|
+
context summary last-write time.
|
|
198
|
+
6. EPIC_BASE_BRANCH_MISMATCH - under epic_mode, gh pr create does not carry a
|
|
199
|
+
matching --base <epic_context.integration_branch>.
|
|
200
|
+
Returns $null when all six checks pass (allow). All disk access flows through the four
|
|
201
|
+
injectable seams (Get-PrBodyFileBytes, Get-PrAuthorReceiptContent,
|
|
202
|
+
Get-PrContextSummaryLastWriteUtc, Get-PrAuthorCheckpointContent); SHA-256 is computed
|
|
203
|
+
inline. This is a policy-level integrity check, not a cryptographic control: any actor
|
|
204
|
+
with Write access to artifacts/ can replace the body file and the receipt together.
|
|
205
|
+
.PARAMETER CommandText
|
|
206
|
+
The Bash command text containing the --body-file argument.
|
|
118
207
|
.OUTPUTS
|
|
119
208
|
System.String or $null
|
|
120
209
|
#>
|
|
121
210
|
[CmdletBinding()]
|
|
122
211
|
[OutputType([string])]
|
|
123
|
-
param(
|
|
212
|
+
param(
|
|
213
|
+
[Parameter(Mandatory)]
|
|
214
|
+
[string] $CommandText
|
|
215
|
+
)
|
|
124
216
|
|
|
125
|
-
|
|
217
|
+
# Check 1: the --body-file argument must match the canonical artifacts/pr_body_<N>.md pattern.
|
|
218
|
+
# The match is case-sensitive (-cmatch) so a non-canonical path is rejected before any read.
|
|
219
|
+
if ($CommandText -cnotmatch '--body-file\s+artifacts/pr_body_(\d+)\.md\b') {
|
|
220
|
+
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``."
|
|
221
|
+
}
|
|
126
222
|
|
|
127
|
-
|
|
128
|
-
|
|
223
|
+
$bodyNumber = [int]$Matches[1]
|
|
224
|
+
$bodyFilePath = "artifacts/pr_body_$bodyNumber.md"
|
|
225
|
+
$receiptFilePath = "artifacts/pr_body_$bodyNumber.receipt.json"
|
|
226
|
+
|
|
227
|
+
# Check 2: the sibling receipt file must exist (read via the injectable seam).
|
|
228
|
+
$receiptRaw = Get-PrAuthorReceiptContent -ReceiptFilePath $receiptFilePath
|
|
229
|
+
if ([string]::IsNullOrWhiteSpace($receiptRaw)) {
|
|
230
|
+
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``."
|
|
129
231
|
}
|
|
130
232
|
|
|
233
|
+
# A receipt that is present but not valid JSON cannot be verified; treat it as missing content.
|
|
131
234
|
try {
|
|
132
|
-
$
|
|
235
|
+
$receipt = $receiptRaw | ConvertFrom-Json -ErrorAction Stop
|
|
133
236
|
} catch {
|
|
134
|
-
return "
|
|
237
|
+
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``."
|
|
135
238
|
}
|
|
136
239
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
240
|
+
# Check 3: the receipt number must equal the <N> extracted from the canonical path.
|
|
241
|
+
$receiptNumber = $null
|
|
242
|
+
$parsedNumber = 0
|
|
243
|
+
if ([int]::TryParse([string]$receipt.number, [ref] $parsedNumber)) {
|
|
244
|
+
$receiptNumber = $parsedNumber
|
|
245
|
+
}
|
|
246
|
+
if ($receiptNumber -ne $bodyNumber) {
|
|
247
|
+
return "PR_AUTHOR_RECEIPT_NUMBER_MISMATCH: ``$receiptFilePath`` ``number`` ($($receipt.number)) does not equal the body-file number ($bodyNumber). The receipt must bind to ``$bodyFilePath``."
|
|
140
248
|
}
|
|
141
249
|
|
|
142
|
-
|
|
143
|
-
$
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
[System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
|
|
147
|
-
[ref] $issuedAt)
|
|
148
|
-
if (-not $parsed) {
|
|
149
|
-
return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` has an unparseable ``issued_at``. The pr-author agent must record a UTC ISO-8601 ``issued_at`` timestamp."
|
|
250
|
+
# Check 4: the inline SHA-256 (lowercase hex) of the body bytes must equal receipt.sha256.
|
|
251
|
+
$bodyBytes = Get-PrBodyFileBytes -BodyFilePath $bodyFilePath
|
|
252
|
+
if ($null -eq $bodyBytes) {
|
|
253
|
+
return "PR_AUTHOR_RECEIPT_HASH_MISMATCH: ``$bodyFilePath`` could not be read to verify its SHA-256 against ``$receiptFilePath``."
|
|
150
254
|
}
|
|
151
255
|
|
|
152
|
-
|
|
153
|
-
|
|
256
|
+
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
|
257
|
+
try {
|
|
258
|
+
$hashBytes = $sha256.ComputeHash($bodyBytes)
|
|
259
|
+
} finally {
|
|
260
|
+
$sha256.Dispose()
|
|
154
261
|
}
|
|
262
|
+
$computedHash = ([System.BitConverter]::ToString($hashBytes) -replace '-', '').ToLowerInvariant()
|
|
155
263
|
|
|
156
|
-
$
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
264
|
+
if ($computedHash -ne ([string]$receipt.sha256).ToLowerInvariant()) {
|
|
265
|
+
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."
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
# Check 5: receipt.created_at (UTC) must be strictly newer than the context summary last-write.
|
|
269
|
+
$createdAt = [DateTime]::MinValue
|
|
270
|
+
$createdParsed = [DateTime]::TryParse(
|
|
271
|
+
[string]$receipt.created_at,
|
|
272
|
+
[System.Globalization.CultureInfo]::InvariantCulture,
|
|
273
|
+
[System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
|
|
274
|
+
[ref] $createdAt)
|
|
275
|
+
if (-not $createdParsed) {
|
|
276
|
+
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``."
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
$contextLastWrite = Get-PrContextSummaryLastWriteUtc
|
|
280
|
+
if (($null -eq $contextLastWrite) -or ($createdAt -le $contextLastWrite)) {
|
|
281
|
+
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."
|
|
162
282
|
}
|
|
163
283
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
284
|
+
# Check 6: under epic_mode, gh pr create must carry a matching --base override.
|
|
285
|
+
$epicBaseBranchReason = Test-EpicBaseBranchOverride -CommandText $CommandText
|
|
286
|
+
if ($epicBaseBranchReason) {
|
|
287
|
+
return $epicBaseBranchReason
|
|
167
288
|
}
|
|
168
289
|
|
|
169
290
|
return $null
|
|
@@ -176,12 +297,12 @@ function Get-PrAuthorBypassReason {
|
|
|
176
297
|
.DESCRIPTION
|
|
177
298
|
Returns PR_AUTHOR_SKILL_BLOCKED when gh pr create or gh pr edit is run with --body (inline,
|
|
178
299
|
no --body-file), or when gh pr create is run with no body flag at all. Returns
|
|
179
|
-
PR_CONTEXT_MISSING when --body-file is present but the context artifact
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
(
|
|
183
|
-
Cases A, B, and C are evaluated first and unchanged;
|
|
184
|
-
previously-allowed --body-file-with-context path.
|
|
300
|
+
PR_CONTEXT_MISSING when --body-file is present but the context artifact does not exist on
|
|
301
|
+
disk. When --body-file is present and the context artifact exists, verifies the SHA-256
|
|
302
|
+
receipt via Test-PrAuthorReceiptVerification and returns its block reason
|
|
303
|
+
(PR_BODY_PATH_NONCANONICAL / PR_AUTHOR_RECEIPT_*) when verification fails. Returns $null for
|
|
304
|
+
all allowed patterns. Cases A, B, and C are evaluated first and unchanged; receipt
|
|
305
|
+
verification only extends the previously-allowed --body-file-with-context path.
|
|
185
306
|
.PARAMETER CommandText
|
|
186
307
|
The Bash command text extracted from CLAUDE_TOOL_INPUT.
|
|
187
308
|
.PARAMETER ContextExists
|
|
@@ -235,12 +356,25 @@ function Get-PrAuthorBypassReason {
|
|
|
235
356
|
return "PR_CONTEXT_MISSING: ``$script:PrContextArtifactPath`` is absent. Run ``mcp__drm-copilot__collect_pr_context`` before creating or editing the PR body."
|
|
236
357
|
}
|
|
237
358
|
|
|
238
|
-
#
|
|
239
|
-
#
|
|
359
|
+
# Orchestrator-state preflight: runs inside this same PreToolUse hook (so it cannot be
|
|
360
|
+
# bypassed by invoking gh pr create/edit directly) before receipt verification.
|
|
361
|
+
if ($hasBodyFile -and $ContextExists) {
|
|
362
|
+
$preflightResult = Invoke-OrchestratorStatePreflight
|
|
363
|
+
if ($preflightResult.HasErrors) {
|
|
364
|
+
$preflightSummary = if ([string]::IsNullOrWhiteSpace($preflightResult.ErrorText)) {
|
|
365
|
+
"checkpoint missing at $script:OrchestratorStateCheckpointPath"
|
|
366
|
+
} else {
|
|
367
|
+
$preflightResult.ErrorText
|
|
368
|
+
}
|
|
369
|
+
return "ORCHESTRATOR_STATE_PREFLIGHT_FAILED: $preflightSummary"
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
# Receipt verification: extends, and does not replace, the previously-allowed path.
|
|
240
374
|
if ($hasBodyFile -and $ContextExists) {
|
|
241
|
-
$
|
|
242
|
-
if ($
|
|
243
|
-
return $
|
|
375
|
+
$receiptReason = Test-PrAuthorReceiptVerification -CommandText $CommandText
|
|
376
|
+
if ($receiptReason) {
|
|
377
|
+
return $receiptReason
|
|
244
378
|
}
|
|
245
379
|
}
|
|
246
380
|
|
|
@@ -265,7 +399,7 @@ function Invoke-PrAuthorSkillDecision {
|
|
|
265
399
|
)
|
|
266
400
|
|
|
267
401
|
if (-not $ToolInputRaw) {
|
|
268
|
-
return
|
|
402
|
+
return Get-PrAuthorSkillAllowDecision
|
|
269
403
|
}
|
|
270
404
|
|
|
271
405
|
try {
|
|
@@ -276,20 +410,61 @@ function Invoke-PrAuthorSkillDecision {
|
|
|
276
410
|
|
|
277
411
|
$commandText = $toolInput.command
|
|
278
412
|
if (-not $commandText) {
|
|
279
|
-
return
|
|
413
|
+
return Get-PrAuthorSkillAllowDecision
|
|
280
414
|
}
|
|
281
415
|
|
|
282
416
|
$contextExists = Get-PrContextArtifactExistence
|
|
283
417
|
$reason = Get-PrAuthorBypassReason -CommandText $commandText -ContextExists $contextExists
|
|
284
418
|
|
|
285
419
|
if ($reason) {
|
|
286
|
-
return
|
|
287
|
-
|
|
288
|
-
|
|
420
|
+
return Get-PrAuthorSkillBlockDecision -Reason $reason
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return Get-PrAuthorSkillAllowDecision
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function Get-PrAuthorSkillAllowDecision {
|
|
427
|
+
<#
|
|
428
|
+
.SYNOPSIS
|
|
429
|
+
Construct the PreToolUse allow decision for a permitted Bash command.
|
|
430
|
+
.OUTPUTS
|
|
431
|
+
System.Collections.Specialized.OrderedDictionary
|
|
432
|
+
#>
|
|
433
|
+
[CmdletBinding()]
|
|
434
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
435
|
+
param()
|
|
436
|
+
|
|
437
|
+
return [ordered]@{
|
|
438
|
+
hookSpecificOutput = [ordered]@{
|
|
439
|
+
hookEventName = 'PreToolUse'
|
|
440
|
+
permissionDecision = 'allow'
|
|
289
441
|
}
|
|
290
442
|
}
|
|
443
|
+
}
|
|
291
444
|
|
|
292
|
-
|
|
445
|
+
function Get-PrAuthorSkillBlockDecision {
|
|
446
|
+
<#
|
|
447
|
+
.SYNOPSIS
|
|
448
|
+
Construct the PreToolUse deny decision for a forbidden Bash command.
|
|
449
|
+
.PARAMETER Reason
|
|
450
|
+
The specific deny reason to surface in the decision.
|
|
451
|
+
.OUTPUTS
|
|
452
|
+
System.Collections.Specialized.OrderedDictionary
|
|
453
|
+
#>
|
|
454
|
+
[CmdletBinding()]
|
|
455
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
456
|
+
param(
|
|
457
|
+
[Parameter(Mandatory)]
|
|
458
|
+
[string] $Reason
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
return [ordered]@{
|
|
462
|
+
hookSpecificOutput = [ordered]@{
|
|
463
|
+
hookEventName = 'PreToolUse'
|
|
464
|
+
permissionDecision = 'deny'
|
|
465
|
+
permissionDecisionReason = $Reason
|
|
466
|
+
}
|
|
467
|
+
}
|
|
293
468
|
}
|
|
294
469
|
|
|
295
470
|
function Test-PrAuthorBypassRequired {
|
|
@@ -328,6 +503,6 @@ try {
|
|
|
328
503
|
exit 1
|
|
329
504
|
}
|
|
330
505
|
|
|
331
|
-
$decision | ConvertTo-Json -Compress | Write-Output
|
|
506
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
332
507
|
|
|
333
508
|
exit 0
|
package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1
CHANGED
|
@@ -19,9 +19,11 @@
|
|
|
19
19
|
to reference a feature folder explicitly.
|
|
20
20
|
|
|
21
21
|
Once the folder is resolved, the hook verifies that both spec.md and
|
|
22
|
-
user-story.md exist in that folder. If either is missing, the script
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
user-story.md exist in that folder. If either is missing, the script emits a
|
|
23
|
+
PreToolUse JSON response with hookSpecificOutput.permissionDecision='deny'
|
|
24
|
+
and a reason naming the missing file(s) and instructing the orchestrator to
|
|
25
|
+
invoke prd-feature first. Allowed delegations emit
|
|
26
|
+
hookSpecificOutput.permissionDecision='allow'.
|
|
25
27
|
|
|
26
28
|
Filesystem reads and orchestrator-state lookups go through wrapper functions
|
|
27
29
|
so tests can inject fakes without touching disk.
|
|
@@ -157,7 +159,7 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
157
159
|
)
|
|
158
160
|
|
|
159
161
|
if (-not $ToolInputRaw) {
|
|
160
|
-
return [ordered]@{
|
|
162
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
try {
|
|
@@ -169,7 +171,7 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
169
171
|
|
|
170
172
|
$subagent = $toolInput.subagent_type
|
|
171
173
|
if (-not $subagent -or $subagent -ne 'atomic-planner') {
|
|
172
|
-
return [ordered]@{
|
|
174
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
173
175
|
}
|
|
174
176
|
|
|
175
177
|
$prompt = [string]$toolInput.prompt
|
|
@@ -180,21 +182,27 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
180
182
|
|
|
181
183
|
if (-not $folder) {
|
|
182
184
|
return [ordered]@{
|
|
183
|
-
|
|
184
|
-
|
|
185
|
+
hookSpecificOutput = [ordered]@{
|
|
186
|
+
hookEventName = 'PreToolUse'
|
|
187
|
+
permissionDecision = 'deny'
|
|
188
|
+
permissionDecisionReason = "PRD_FEATURE_BLOCKED: atomic-planner delegation must reference a feature folder (either in the prompt or via orchestrator-state.json) so spec.md and user-story.md prerequisites can be verified."
|
|
189
|
+
}
|
|
185
190
|
}
|
|
186
191
|
}
|
|
187
192
|
|
|
188
193
|
$folderNormalized = ($folder -replace '\\', '/').TrimEnd('/')
|
|
189
194
|
$missing = Get-PrdFeatureMissingFile -FeatureFolder $folderNormalized
|
|
190
195
|
if ($missing.Count -eq 0) {
|
|
191
|
-
return [ordered]@{
|
|
196
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
192
197
|
}
|
|
193
198
|
|
|
194
199
|
$list = ($missing -join ', ')
|
|
195
200
|
return [ordered]@{
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
hookSpecificOutput = [ordered]@{
|
|
202
|
+
hookEventName = 'PreToolUse'
|
|
203
|
+
permissionDecision = 'deny'
|
|
204
|
+
permissionDecisionReason = "PRD_FEATURE_BLOCKED: cannot delegate to atomic-planner before prd-feature outputs are present in '$folderNormalized'. Missing: $list. Invoke the prd-feature subagent first."
|
|
205
|
+
}
|
|
198
206
|
}
|
|
199
207
|
}
|
|
200
208
|
|
|
@@ -211,6 +219,6 @@ catch {
|
|
|
211
219
|
exit 1
|
|
212
220
|
}
|
|
213
221
|
|
|
214
|
-
$decision | ConvertTo-Json -Compress | Write-Output
|
|
222
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
215
223
|
|
|
216
224
|
exit 0
|
|
@@ -153,8 +153,30 @@ function Get-PromotionMcpOnlyBlockDecision {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
return [ordered]@{
|
|
156
|
-
|
|
157
|
-
|
|
156
|
+
hookSpecificOutput = [ordered]@{
|
|
157
|
+
hookEventName = 'PreToolUse'
|
|
158
|
+
permissionDecision = 'deny'
|
|
159
|
+
permissionDecisionReason = $Reason
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Get-PromotionMcpOnlyAllowDecision {
|
|
165
|
+
<#
|
|
166
|
+
.SYNOPSIS
|
|
167
|
+
Construct the structured allow decision for a permitted Bash command.
|
|
168
|
+
.OUTPUTS
|
|
169
|
+
System.Collections.Specialized.OrderedDictionary
|
|
170
|
+
#>
|
|
171
|
+
[CmdletBinding()]
|
|
172
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
173
|
+
param()
|
|
174
|
+
|
|
175
|
+
return [ordered]@{
|
|
176
|
+
hookSpecificOutput = [ordered]@{
|
|
177
|
+
hookEventName = 'PreToolUse'
|
|
178
|
+
permissionDecision = 'allow'
|
|
179
|
+
}
|
|
158
180
|
}
|
|
159
181
|
}
|
|
160
182
|
|
|
@@ -177,7 +199,7 @@ function Invoke-PromotionMcpOnlyDecision {
|
|
|
177
199
|
)
|
|
178
200
|
|
|
179
201
|
if (-not $ToolInputRaw) {
|
|
180
|
-
return
|
|
202
|
+
return Get-PromotionMcpOnlyAllowDecision
|
|
181
203
|
}
|
|
182
204
|
|
|
183
205
|
try {
|
|
@@ -188,7 +210,7 @@ function Invoke-PromotionMcpOnlyDecision {
|
|
|
188
210
|
|
|
189
211
|
$commandText = $toolInput.command
|
|
190
212
|
if (-not $commandText) {
|
|
191
|
-
return
|
|
213
|
+
return Get-PromotionMcpOnlyAllowDecision
|
|
192
214
|
}
|
|
193
215
|
|
|
194
216
|
$reason = Get-PromotionBypassReason -CommandText $commandText
|
|
@@ -196,7 +218,7 @@ function Invoke-PromotionMcpOnlyDecision {
|
|
|
196
218
|
return Get-PromotionMcpOnlyBlockDecision -Reason $reason
|
|
197
219
|
}
|
|
198
220
|
|
|
199
|
-
return
|
|
221
|
+
return Get-PromotionMcpOnlyAllowDecision
|
|
200
222
|
}
|
|
201
223
|
|
|
202
224
|
# Allow dot-sourcing in tests without executing the entrypoint.
|
|
@@ -211,6 +233,6 @@ try {
|
|
|
211
233
|
exit 1
|
|
212
234
|
}
|
|
213
235
|
|
|
214
|
-
$decision | ConvertTo-Json -Compress | Write-Output
|
|
236
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
215
237
|
|
|
216
238
|
exit 0
|