@danmoisan/drm-copilot-mcp 1.0.26 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/out/mcp-server.js +542 -42
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
  4. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
  5. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
  6. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
  7. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +172 -24
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
  12. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
  13. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
  14. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +402 -0
  15. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
  19. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
  20. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
  21. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
  22. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
  23. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
  24. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
  25. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
  26. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
  27. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
  28. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
  29. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
  30. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
  31. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
  32. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
  33. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
  34. package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
  35. package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
  36. package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
  37. package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
  38. package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
  39. package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
  40. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +124 -1
  41. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
  42. package/resources/claude-customizations/.claude/settings.json +5 -0
  43. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
  44. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  45. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
  46. package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
  47. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
  48. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
  49. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
  50. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
  51. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
  52. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
  53. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
  54. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
  55. package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
  56. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
  57. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
  58. package/resources/claude-customizations/config/blast-radius.json +16 -2
  59. package/resources/claude-customizations/pack-manifests/core.json +22 -1
  60. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  61. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  62. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
  63. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +31 -1
@@ -0,0 +1,494 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Shared PreToolUse hook payload reader: transport, envelope parse, and strict
4
+ nested tool_input extraction (issue #501).
5
+
6
+ .DESCRIPTION
7
+ Every PreToolUse hook under .claude/hooks/ acquires its payload through this
8
+ module instead of re-implementing the parse inline. The module owns three
9
+ concerns.
10
+
11
+ Transport. Claude Code delivers command-hook input on stdin as JSON.
12
+ Read-ClaudeHookRawPayload reads stdin first, then falls back to the
13
+ CLAUDE_HOOK_INPUT environment variable and then the CLAUDE_TOOL_INPUT
14
+ environment variable; the first non-whitespace source wins. Both stdin
15
+ operations sit behind injectable scriptblock seams so tests drive them without
16
+ touching a .NET static and without spawning a process.
17
+
18
+ Shape. Tool arguments are nested under the envelope's tool_input object.
19
+ Get-ClaudeHookToolInput extracts that object strictly: a parsed payload with no
20
+ tool_input key, or a tool_input that is null or not an object, is an envelope
21
+ anomaly. There is deliberately no flat-root fallback, because a permissive
22
+ dual-shape reader would keep every legacy-shaped fixture green and restore the
23
+ silent-allow failure mode on the next contract drift.
24
+
25
+ Anomaly classification. The parse and extract functions never return a silent
26
+ null for malformed input; they return a typed result whose Anomaly property
27
+ names the failure, and Get-ClaudeHookPayloadAnomalyReason maps that code to the
28
+ deny reason text a hook emits.
29
+
30
+ Fail-closed posture: an envelope-level anomaly is a deny, emitted as decision
31
+ JSON at process exit code 0. Exit code 1 is non-blocking for PreToolUse, so a
32
+ throwing hook is itself a fail-open. Property-level absence inside a well-formed
33
+ tool_input is NOT an anomaly: that is each hook's own scope filter and keeps its
34
+ existing allow early-return.
35
+
36
+ .NOTES
37
+ Compatible with PowerShell 7+. No external module dependencies, no filesystem
38
+ access, no subprocess, no network, and no wall-clock read. Mirrored
39
+ byte-identically under extensions/drm-copilot/resources/claude-customizations/.
40
+ #>
41
+
42
+ Set-StrictMode -Version Latest
43
+
44
+ # Anomaly codes. These are the only values Get-ClaudeHookPayloadAnomalyReason maps.
45
+ $script:AnomalyEmptyPayload = 'EmptyPayload'
46
+ $script:AnomalyUnparseableJson = 'UnparseableJson'
47
+ $script:AnomalyMissingToolInput = 'MissingToolInput'
48
+ $script:AnomalyNullToolInput = 'NullToolInput'
49
+ $script:AnomalyNonObjectToolInput = 'NonObjectToolInput'
50
+
51
+ # The nested key that carries the tool arguments in the documented envelope.
52
+ $script:ToolInputKey = 'tool_input'
53
+
54
+ # Deny reason text per anomaly code. Each string is distinct so an operator can
55
+ # tell which leg of the contract drifted from the emitted decision alone.
56
+ $script:AnomalyReasonText = @{
57
+ 'EmptyPayload' = 'the hook received an empty payload on stdin and on both environment-variable fallbacks'
58
+ 'UnparseableJson' = 'the hook received a payload that is not parseable JSON'
59
+ 'MissingToolInput' = 'the hook received a JSON payload with no tool_input key (the legacy flat root shape is an envelope anomaly, not a supported payload)'
60
+ 'NullToolInput' = 'the hook received a JSON payload whose tool_input is null'
61
+ 'NonObjectToolInput' = 'the hook received a JSON payload whose tool_input is not an object'
62
+ }
63
+
64
+ function Get-ClaudeHookPayloadAnomalyCode {
65
+ <#
66
+ .SYNOPSIS
67
+ Return the anomaly codes this module can emit, so callers need not
68
+ hard-code the literals in a second place.
69
+ .OUTPUTS
70
+ System.String[]
71
+ #>
72
+ [CmdletBinding()]
73
+ [OutputType([string[]])]
74
+ param()
75
+
76
+ return [string[]]@(
77
+ $script:AnomalyEmptyPayload,
78
+ $script:AnomalyUnparseableJson,
79
+ $script:AnomalyMissingToolInput,
80
+ $script:AnomalyNullToolInput,
81
+ $script:AnomalyNonObjectToolInput
82
+ )
83
+ }
84
+
85
+ function Get-ClaudeHookPayloadAnomalyReason {
86
+ <#
87
+ .SYNOPSIS
88
+ Map an anomaly code to the clause a hook puts in its deny reason.
89
+ .PARAMETER Anomaly
90
+ One of the codes returned by Get-ClaudeHookPayloadAnomalyCode.
91
+ .OUTPUTS
92
+ System.String
93
+ #>
94
+ [CmdletBinding()]
95
+ [OutputType([string])]
96
+ param(
97
+ [AllowNull()]
98
+ [AllowEmptyString()]
99
+ [string] $Anomaly
100
+ )
101
+
102
+ if ([string]::IsNullOrWhiteSpace($Anomaly)) {
103
+ return 'the hook received an unclassified payload anomaly'
104
+ }
105
+ if ($script:AnomalyReasonText.ContainsKey($Anomaly)) {
106
+ return [string]$script:AnomalyReasonText[$Anomaly]
107
+ }
108
+ return ('the hook received an unrecognized payload anomaly ({0})' -f $Anomaly)
109
+ }
110
+
111
+ function ConvertTo-ClaudeHookPayloadResult {
112
+ <#
113
+ .SYNOPSIS
114
+ Build the typed result every parse/extract function returns: IsValid, Value
115
+ (the parsed envelope or extracted tool_input), and Anomaly (null when
116
+ valid). Always an object, so no caller tests for null first.
117
+ .OUTPUTS
118
+ System.Management.Automation.PSCustomObject
119
+ #>
120
+ [CmdletBinding()]
121
+ [OutputType([pscustomobject])]
122
+ param(
123
+ [Parameter(Mandatory)]
124
+ [bool] $IsValid,
125
+
126
+ [AllowNull()]
127
+ [object] $Value,
128
+
129
+ [AllowNull()]
130
+ [AllowEmptyString()]
131
+ [string] $Anomaly
132
+ )
133
+
134
+ $normalizedAnomaly = $null
135
+ if (-not [string]::IsNullOrWhiteSpace($Anomaly)) {
136
+ $normalizedAnomaly = [string]$Anomaly
137
+ }
138
+
139
+ return [pscustomobject]@{
140
+ IsValid = $IsValid
141
+ Value = $Value
142
+ Anomaly = $normalizedAnomaly
143
+ }
144
+ }
145
+
146
+ function Read-ClaudeHookRawPayload {
147
+ <#
148
+ .SYNOPSIS
149
+ Acquire the raw hook payload text, stdin first.
150
+
151
+ .DESCRIPTION
152
+ Ordering: stdin, then the CLAUDE_HOOK_INPUT environment variable, then the
153
+ CLAUDE_TOOL_INPUT environment variable. The first non-whitespace source
154
+ wins; when every source is empty or whitespace the function returns an empty
155
+ string, which callers classify as the EmptyPayload anomaly.
156
+
157
+ The stdin read is guarded by a redirect probe. An unguarded
158
+ [Console]::In.ReadToEnd() blocks indefinitely on a non-redirected console,
159
+ which would hang the documented manual/CLI invocation of validate-bash.ps1.
160
+ The guard lives in this function body rather than inside the read seam's
161
+ default so a test can drive both polarities by injection. A stdin read that
162
+ throws falls back rather than propagating (persist-session-id.ps1 precedent).
163
+
164
+ .PARAMETER ReadStandardInput
165
+ Seam for the stdin read. Default is the bare [Console]::In.ReadToEnd().
166
+ .PARAMETER TestStandardInputRedirected
167
+ Seam for the redirect probe. Default is [Console]::IsInputRedirected. When
168
+ it evaluates falsey, stdin is treated as empty and the env fallback runs.
169
+ .PARAMETER HookInputFallback
170
+ Seam for the first environment fallback.
171
+ .PARAMETER ToolInputFallback
172
+ Seam for the second environment fallback.
173
+ .OUTPUTS
174
+ System.String
175
+ #>
176
+ [CmdletBinding()]
177
+ [OutputType([string])]
178
+ param(
179
+ [scriptblock] $ReadStandardInput = { [Console]::In.ReadToEnd() },
180
+
181
+ [scriptblock] $TestStandardInputRedirected = { [Console]::IsInputRedirected },
182
+
183
+ [AllowNull()]
184
+ [AllowEmptyString()]
185
+ [string] $HookInputFallback = $env:CLAUDE_HOOK_INPUT,
186
+
187
+ [AllowNull()]
188
+ [AllowEmptyString()]
189
+ [string] $ToolInputFallback = $env:CLAUDE_TOOL_INPUT
190
+ )
191
+
192
+ $raw = ''
193
+
194
+ $isRedirected = $false
195
+ try {
196
+ $isRedirected = [bool](& $TestStandardInputRedirected)
197
+ } catch {
198
+ $isRedirected = $false
199
+ }
200
+
201
+ if ($isRedirected) {
202
+ try {
203
+ $raw = [string](& $ReadStandardInput)
204
+ } catch {
205
+ $raw = ''
206
+ }
207
+ }
208
+
209
+ if (-not [string]::IsNullOrWhiteSpace($raw)) {
210
+ return $raw
211
+ }
212
+
213
+ if (-not [string]::IsNullOrWhiteSpace($HookInputFallback)) {
214
+ return [string]$HookInputFallback
215
+ }
216
+
217
+ if (-not [string]::IsNullOrWhiteSpace($ToolInputFallback)) {
218
+ return [string]$ToolInputFallback
219
+ }
220
+
221
+ return ''
222
+ }
223
+
224
+ function ConvertFrom-ClaudeHookEnvelope {
225
+ <#
226
+ .SYNOPSIS
227
+ Parse raw hook payload text into the envelope object.
228
+
229
+ .DESCRIPTION
230
+ Strips a UTF-8 byte-order mark and normalizes CRLF before parsing, because
231
+ stdin text arrives with either line ending and a BOM survives a piped read.
232
+ Never returns a silent null for malformed input: an empty or whitespace-only
233
+ payload yields the EmptyPayload anomaly and unparseable text yields the
234
+ UnparseableJson anomaly.
235
+
236
+ .PARAMETER Raw
237
+ The raw payload text from Read-ClaudeHookRawPayload.
238
+ .OUTPUTS
239
+ System.Management.Automation.PSCustomObject (typed result)
240
+ #>
241
+ [CmdletBinding()]
242
+ [OutputType([pscustomobject])]
243
+ param(
244
+ [AllowNull()]
245
+ [AllowEmptyString()]
246
+ [string] $Raw
247
+ )
248
+
249
+ if ([string]::IsNullOrWhiteSpace($Raw)) {
250
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyEmptyPayload
251
+ }
252
+
253
+ # A BOM decoded into the string appears as U+FEFF and makes ConvertFrom-Json fail.
254
+ $text = ([string]$Raw).TrimStart([char]0xFEFF)
255
+ $text = $text.Replace("`r`n", "`n")
256
+
257
+ if ([string]::IsNullOrWhiteSpace($text)) {
258
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyEmptyPayload
259
+ }
260
+
261
+ try {
262
+ $envelope = $text | ConvertFrom-Json -ErrorAction Stop
263
+ } catch {
264
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyUnparseableJson
265
+ }
266
+
267
+ if ($null -eq $envelope) {
268
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyUnparseableJson
269
+ }
270
+
271
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $true -Value $envelope -Anomaly $null
272
+ }
273
+
274
+ function Test-ClaudeHookEnvelopeHasKey {
275
+ <#
276
+ .SYNOPSIS
277
+ Report whether a parsed JSON object (PSCustomObject or dictionary) carries a
278
+ named key, without throwing under StrictMode.
279
+ .OUTPUTS
280
+ System.Boolean
281
+ #>
282
+ [CmdletBinding()]
283
+ [OutputType([bool])]
284
+ param(
285
+ [AllowNull()]
286
+ [object] $Envelope,
287
+
288
+ [Parameter(Mandatory)]
289
+ [string] $Name
290
+ )
291
+
292
+ if ($null -eq $Envelope) {
293
+ return $false
294
+ }
295
+ if ($Envelope -is [System.Collections.IDictionary]) {
296
+ return $Envelope.Contains($Name)
297
+ }
298
+ if ($Envelope -isnot [psobject]) {
299
+ return $false
300
+ }
301
+ return (@($Envelope.PSObject.Properties.Name) -contains $Name)
302
+ }
303
+
304
+ function Get-ClaudeHookEnvelopeValue {
305
+ <#
306
+ .SYNOPSIS
307
+ Read a named value off a parsed JSON object, returning null when the key is
308
+ absent rather than throwing under StrictMode.
309
+ .OUTPUTS
310
+ System.Object or null
311
+ #>
312
+ [CmdletBinding()]
313
+ param(
314
+ [AllowNull()]
315
+ [object] $Envelope,
316
+
317
+ [Parameter(Mandatory)]
318
+ [string] $Name
319
+ )
320
+
321
+ if (-not (Test-ClaudeHookEnvelopeHasKey -Envelope $Envelope -Name $Name)) {
322
+ return $null
323
+ }
324
+ if ($Envelope -is [System.Collections.IDictionary]) {
325
+ return $Envelope[$Name]
326
+ }
327
+ return $Envelope.PSObject.Properties[$Name].Value
328
+ }
329
+
330
+ function Test-ClaudeHookObjectValue {
331
+ <#
332
+ .SYNOPSIS
333
+ Report whether a parsed JSON value is an object rather than a scalar or an
334
+ array. ConvertFrom-Json produces a PSCustomObject for a JSON object, a
335
+ native scalar for a scalar, and an array for a JSON array; only the first is
336
+ a usable tool_input.
337
+ .OUTPUTS
338
+ System.Boolean
339
+ #>
340
+ [CmdletBinding()]
341
+ [OutputType([bool])]
342
+ param(
343
+ [AllowNull()]
344
+ [object] $Value
345
+ )
346
+
347
+ if ($null -eq $Value) {
348
+ return $false
349
+ }
350
+ if ($Value -is [System.Collections.IDictionary]) {
351
+ return $true
352
+ }
353
+ if ($Value -is [string] -or $Value -is [bool] -or $Value -is [ValueType]) {
354
+ return $false
355
+ }
356
+ if ($Value -is [System.Collections.IEnumerable]) {
357
+ return $false
358
+ }
359
+ return ($Value -is [psobject])
360
+ }
361
+
362
+ function Get-ClaudeHookToolInput {
363
+ <#
364
+ .SYNOPSIS
365
+ Extract the envelope's nested tool_input object. Strict: no flat-root
366
+ fallback.
367
+
368
+ .DESCRIPTION
369
+ A parsed payload with no tool_input key is the MissingToolInput anomaly,
370
+ which is exactly what the legacy flat root shape produces. A tool_input
371
+ present but null is NullToolInput; present but not an object is
372
+ NonObjectToolInput. Absence of a property inside a well-formed tool_input is
373
+ not handled here at all: that stays each hook's own scope filter.
374
+
375
+ .PARAMETER Envelope
376
+ The parsed envelope object from ConvertFrom-ClaudeHookEnvelope.
377
+ .OUTPUTS
378
+ System.Management.Automation.PSCustomObject (typed result)
379
+ #>
380
+ [CmdletBinding()]
381
+ [OutputType([pscustomobject])]
382
+ param(
383
+ [AllowNull()]
384
+ [object] $Envelope
385
+ )
386
+
387
+ if ($null -eq $Envelope) {
388
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyMissingToolInput
389
+ }
390
+
391
+ if (-not (Test-ClaudeHookEnvelopeHasKey -Envelope $Envelope -Name $script:ToolInputKey)) {
392
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyMissingToolInput
393
+ }
394
+
395
+ $toolInput = Get-ClaudeHookEnvelopeValue -Envelope $Envelope -Name $script:ToolInputKey
396
+
397
+ if ($null -eq $toolInput) {
398
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyNullToolInput
399
+ }
400
+
401
+ if (-not (Test-ClaudeHookObjectValue -Value $toolInput)) {
402
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $false -Value $null -Anomaly $script:AnomalyNonObjectToolInput
403
+ }
404
+
405
+ return ConvertTo-ClaudeHookPayloadResult -IsValid $true -Value $toolInput -Anomaly $null
406
+ }
407
+
408
+ function Get-ClaudeHookToolInputString {
409
+ <#
410
+ .SYNOPSIS
411
+ Read a named string property off an already-extracted tool_input object.
412
+
413
+ .DESCRIPTION
414
+ Property-level tolerance lives here: an absent property returns an empty
415
+ string, which every hook already treats as "out of my scope, allow". This
416
+ helper exists only so the hooks do not each re-implement the StrictMode-safe
417
+ property probe.
418
+
419
+ .OUTPUTS
420
+ System.String
421
+ #>
422
+ [CmdletBinding()]
423
+ [OutputType([string])]
424
+ param(
425
+ [AllowNull()]
426
+ [object] $ToolInput,
427
+
428
+ [Parameter(Mandatory)]
429
+ [string] $Name
430
+ )
431
+
432
+ $value = Get-ClaudeHookEnvelopeValue -Envelope $ToolInput -Name $Name
433
+ if ($null -eq $value) {
434
+ return ''
435
+ }
436
+ return [string]$value
437
+ }
438
+
439
+ function Resolve-ClaudeHookToolInput {
440
+ <#
441
+ .SYNOPSIS
442
+ One-call convenience: raw text in, typed tool_input result out.
443
+
444
+ .DESCRIPTION
445
+ Composes ConvertFrom-ClaudeHookEnvelope and Get-ClaudeHookToolInput and
446
+ additionally surfaces the parsed envelope on the Envelope member, because
447
+ enforce-epic-invocation-origin.ps1 needs the envelope root (agent_type)
448
+ alongside the nested tool_input. Returns the envelope-parse anomaly unchanged
449
+ when the parse fails, so a hook maps a single Anomaly value to a single deny
450
+ reason.
451
+
452
+ .PARAMETER Raw
453
+ The raw payload text from Read-ClaudeHookRawPayload.
454
+ .OUTPUTS
455
+ System.Management.Automation.PSCustomObject
456
+ #>
457
+ [CmdletBinding()]
458
+ [OutputType([pscustomobject])]
459
+ param(
460
+ [AllowNull()]
461
+ [AllowEmptyString()]
462
+ [string] $Raw
463
+ )
464
+
465
+ $parsed = ConvertFrom-ClaudeHookEnvelope -Raw $Raw
466
+ if (-not $parsed.IsValid) {
467
+ return [pscustomobject]@{
468
+ IsValid = $false
469
+ Value = $null
470
+ Envelope = $null
471
+ Anomaly = $parsed.Anomaly
472
+ }
473
+ }
474
+
475
+ $extracted = Get-ClaudeHookToolInput -Envelope $parsed.Value
476
+ return [pscustomobject]@{
477
+ IsValid = $extracted.IsValid
478
+ Value = $extracted.Value
479
+ Envelope = $parsed.Value
480
+ Anomaly = $extracted.Anomaly
481
+ }
482
+ }
483
+
484
+ Export-ModuleMember -Function `
485
+ Read-ClaudeHookRawPayload, `
486
+ ConvertFrom-ClaudeHookEnvelope, `
487
+ Get-ClaudeHookToolInput, `
488
+ Resolve-ClaudeHookToolInput, `
489
+ Get-ClaudeHookEnvelopeValue, `
490
+ Test-ClaudeHookEnvelopeHasKey, `
491
+ Test-ClaudeHookObjectValue, `
492
+ Get-ClaudeHookToolInputString, `
493
+ Get-ClaudeHookPayloadAnomalyCode, `
494
+ Get-ClaudeHookPayloadAnomalyReason