@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.
Files changed (52) hide show
  1. package/out/mcp-server.js +814 -194
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/commit-message.md +42 -0
  4. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +127 -0
  5. package/resources/claude-customizations/.claude/agents/human-exception-runbook.md +48 -0
  6. package/resources/claude-customizations/.claude/agents/orchestrator.md +67 -2
  7. package/resources/claude-customizations/.claude/agents/pr-author.md +37 -21
  8. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +103 -65
  9. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +12 -9
  10. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +24 -16
  11. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +19 -13
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +304 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +304 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +237 -0
  15. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +16 -12
  16. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +14 -10
  17. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +35 -8
  18. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +17 -14
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +104 -0
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +268 -93
  21. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +19 -11
  22. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +28 -6
  23. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +17 -14
  24. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +146 -36
  25. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +19 -7
  26. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +36 -0
  27. package/resources/claude-customizations/.claude/settings.json +34 -1
  28. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +252 -0
  29. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +42 -13
  30. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +28 -0
  31. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +22 -18
  32. package/resources/claude-customizations/pack-manifests/core.json +8 -0
  33. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +6 -3
  34. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
  35. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +65 -0
  36. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +79 -0
  37. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +66 -0
  38. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -25
  39. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +257 -93
  40. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +98 -0
  41. package/resources/codex-and-agents-customizations/pack-manifests/core.json +24 -0
  42. package/resources/codex-and-agents-customizations/pack-manifests/csharp-legacy.json +11 -0
  43. package/resources/codex-and-agents-customizations/pack-manifests/csharp-modern.json +10 -0
  44. package/resources/codex-and-agents-customizations/pack-manifests/powershell.json +15 -0
  45. package/resources/codex-and-agents-customizations/pack-manifests/python.json +15 -0
  46. package/resources/codex-and-agents-customizations/pack-manifests/typescript.json +9 -0
  47. package/resources/config/orchestration-routing.json +92 -0
  48. package/resources/customizations/.github/agents/pr-author.agent.md +24 -18
  49. package/resources/icon.png +0 -0
  50. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +11 -0
  51. package/resources/codex-and-agents-customizations/.github/workflows/_validate-orchestrator-state.yml +0 -68
  52. 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 reads that file and writes artifacts/pr_body_<N>.md
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
- Case D - --body-file present, context artifact present, but the authorization sentinel
22
- artifacts/pr_author_authorization.json is absent or empty: blocked
23
- (PR_AGENT_AUTHORIZATION_MISSING).
24
- Malformed - sentinel present but not valid JSON, or issued_at missing/unparseable: blocked
25
- (PR_AGENT_AUTHORIZATION_MALFORMED).
26
- Case E - sentinel valid JSON but issued_by != "pr-author": blocked
27
- (PR_AGENT_AUTHORIZATION_INVALID).
28
- Case F - sentinel issued_by == "pr-author" but elapsed time since issued_at exceeds
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 authorization sentinel is a policy guardrail, not a cryptographic
38
- or security control. Any actor with Write access to artifacts/ can forge
39
- artifacts/pr_author_authorization.json, because all agents share the same filesystem and the
40
- runtime exposes no native agent-identity signal at Bash PreToolUse time. The mechanism prevents
41
- accidental bypass and requires a deliberate, documented act to circumvent. It MUST NOT be
42
- described as tamper-proof or as a security boundary.
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:PrAuthorAuthorizationPath = 'artifacts/pr_author_authorization.json'
49
- $script:PrAuthorAuthorizationTtlSeconds = 120
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-PrAuthorAuthorizationContent {
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 authorization sentinel. Tests mock this function (read seam).
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/pr_author_authorization.json, or $null when the
71
- sentinel file is absent. This is the injectable boundary for sentinel content in tests; no
72
- test writes the sentinel file to disk.
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 $script:PrAuthorAuthorizationPath)) {
152
+ if (-not (Test-Path -LiteralPath $ReceiptFilePath)) {
81
153
  return $null
82
154
  }
83
155
 
84
- return (Get-Content -LiteralPath $script:PrAuthorAuthorizationPath -Raw)
156
+ return (Get-Content -LiteralPath $ReceiptFilePath -Raw)
85
157
  }
86
158
 
87
- function Get-CurrentDateTimeUtc {
159
+ function Get-PrContextSummaryLastWriteUtc {
88
160
  <#
89
161
  .SYNOPSIS
90
- Return the current UTC time. Tests mock this function (clock seam) for time-travel scenarios.
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
- return [DateTime]::UtcNow
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
- function Test-PrAuthorAuthorization {
181
+ . (Join-Path $PSScriptRoot 'enforce-pr-author-skill.epic-base-branch.ps1')
182
+
183
+ function Test-PrAuthorReceiptVerification {
102
184
  <#
103
185
  .SYNOPSIS
104
- Validate the authorization sentinel and return a block-reason string, or $null when authorized.
186
+ Verify the SHA-256 receipt and return a block-reason string, or $null when verified.
105
187
  .DESCRIPTION
106
- Reads the sentinel via Get-PrAuthorAuthorizationContent and computes elapsed time via
107
- Get-CurrentDateTimeUtc. Applies the decision order missing -> malformed -> invalid issuer ->
108
- expired -> allow (spec FR-2 step 3):
109
- - $null/empty contents -> PR_AGENT_AUTHORIZATION_MISSING
110
- - not valid JSON, or issued_at
111
- missing/unparseable -> PR_AGENT_AUTHORIZATION_MALFORMED
112
- - issued_by != "pr-author" -> PR_AGENT_AUTHORIZATION_INVALID
113
- - elapsed seconds > ttl_seconds -> PR_AGENT_AUTHORIZATION_EXPIRED
114
- - all checks pass -> $null (allow)
115
- The ttl_seconds value defaults to the named constant when absent from the sentinel.
116
- This is a policy guardrail, not a cryptographic control; the sentinel is forgeable by any
117
- actor with Write access to artifacts/.
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
- $contents = Get-PrAuthorAuthorizationContent
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
- if ([string]::IsNullOrWhiteSpace($contents)) {
128
- return "PR_AGENT_AUTHORIZATION_MISSING: ``$script:PrAuthorAuthorizationPath`` is absent or empty. The pr-author agent must write the authorization sentinel immediately before issuing ``gh pr create``/``gh pr edit --body*``."
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
- $sentinel = $contents | ConvertFrom-Json -ErrorAction Stop
235
+ $receipt = $receiptRaw | ConvertFrom-Json -ErrorAction Stop
133
236
  } catch {
134
- return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` is not valid JSON. The pr-author agent must write a well-formed sentinel with ``issued_by``, ``issued_at``, and ``ttl_seconds``."
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
- $issuedAtRaw = $sentinel.issued_at
138
- if ([string]::IsNullOrWhiteSpace([string]$issuedAtRaw)) {
139
- return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` is missing ``issued_at``. The pr-author agent must record a UTC ISO-8601 ``issued_at`` timestamp."
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
- $issuedAt = [DateTime]::MinValue
143
- $parsed = [DateTime]::TryParse(
144
- [string]$issuedAtRaw,
145
- [System.Globalization.CultureInfo]::InvariantCulture,
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
- if ($sentinel.issued_by -ne 'pr-author') {
153
- return "PR_AGENT_AUTHORIZATION_INVALID: ``$script:PrAuthorAuthorizationPath`` ``issued_by`` is not ``pr-author``. Only the pr-author agent may authorize PR creation or body edits."
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
- $ttlSeconds = $script:PrAuthorAuthorizationTtlSeconds
157
- if ($null -ne $sentinel.ttl_seconds) {
158
- $candidateTtl = 0
159
- if ([int]::TryParse([string]$sentinel.ttl_seconds, [ref] $candidateTtl)) {
160
- $ttlSeconds = $candidateTtl
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
- $elapsedSeconds = ((Get-CurrentDateTimeUtc) - $issuedAt).TotalSeconds
165
- if ($elapsedSeconds -gt $ttlSeconds) {
166
- return "PR_AGENT_AUTHORIZATION_EXPIRED: ``$script:PrAuthorAuthorizationPath`` issued $([math]::Round($elapsedSeconds)) s ago, exceeding the ${ttlSeconds}s TTL. The pr-author agent must write a fresh sentinel immediately before the ``gh`` command."
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
- does not exist on disk. When --body-file is present and the context artifact exists, evaluates
181
- the authorization sentinel via Test-PrAuthorAuthorization and returns its block reason
182
- (PR_AGENT_AUTHORIZATION_*) when authorization fails. Returns $null for all allowed patterns.
183
- Cases A, B, and C are evaluated first and unchanged; the sentinel check only extends the
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
- # Cases D/E/F and malformed: --body-file present and context artifact exists; verify the
239
- # authorization sentinel. This extends, and does not replace, the previously-allowed path.
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
- $authorizationReason = Test-PrAuthorAuthorization
242
- if ($authorizationReason) {
243
- return $authorizationReason
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 [ordered]@{ decision = 'allow' }
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 [ordered]@{ decision = 'allow' }
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 [ordered]@{
287
- decision = 'block'
288
- reason = $reason
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
- return [ordered]@{ decision = 'allow' }
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
@@ -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 blocks
23
- with a reason naming the missing file(s) and instructing the orchestrator to
24
- invoke prd-feature first.
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]@{ decision = 'allow' }
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]@{ decision = 'allow' }
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
- decision = 'block'
184
- reason = "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."
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]@{ decision = 'allow' }
196
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
192
197
  }
193
198
 
194
199
  $list = ($missing -join ', ')
195
200
  return [ordered]@{
196
- decision = 'block'
197
- reason = "PRD_FEATURE_BLOCKED: cannot delegate to atomic-planner before prd-feature outputs are present in '$folderNormalized'. Missing: $list. Invoke the prd-feature subagent first."
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
- decision = 'block'
157
- reason = $Reason
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 [ordered]@{ decision = 'allow' }
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 [ordered]@{ decision = 'allow' }
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 [ordered]@{ decision = 'allow' }
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