@danmoisan/drm-copilot-mcp 1.1.15 → 1.1.17

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 (71) hide show
  1. package/out/mcp-server.js +639 -27
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +12 -6
  4. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +4 -5
  5. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
  6. package/resources/claude-customizations/.claude/hooks/enforce-batch-budget-route.ps1 +139 -0
  7. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
  18. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +95 -66
  19. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
  20. package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
  21. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
  22. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
  23. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
  24. package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
  25. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
  26. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
  27. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
  29. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
  30. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
  31. package/resources/claude-customizations/.claude/rules/powershell.md +3 -3
  32. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +2 -0
  33. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
  34. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +7 -4
  35. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
  36. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +3 -1
  37. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +32 -18
  38. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
  39. package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +6 -6
  40. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
  41. package/resources/claude-customizations/pack-manifests/core.json +8 -0
  42. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +1 -2
  43. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
  44. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -2
  45. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
  46. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +2 -3
  47. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +2 -3
  48. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +2 -3
  49. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +2 -3
  50. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +2 -3
  51. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -3
  52. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
  53. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
  54. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
  55. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
  56. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
  57. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
  58. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
  59. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  60. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
  61. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +165 -80
  62. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
  63. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  64. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +7 -7
  65. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +8 -10
  66. package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
  67. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
  68. package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +2 -2
  69. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
  70. package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +3 -3
  71. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +12 -0
@@ -1,34 +1,44 @@
1
1
 
2
2
  <#
3
3
  .SYNOPSIS
4
- Pre-tool-use hook that enforces the PowerShell per-batch change budget.
4
+ Pre-tool-use hook that routes PowerShell changes of more than three production files to the orchestrated large path.
5
5
 
6
6
  .DESCRIPTION
7
7
  This script is invoked by the Codex PreToolUse hook before any Write or Edit
8
8
  operation. When the target file is a PowerShell source file (.ps1, .psm1, .psd1),
9
- it classifies the file as either production or test and checks the running count
10
- against the per-batch cap:
11
- - 3 production PowerShell files per batch
12
- - 3 test PowerShell files per batch
13
-
14
- A "batch" is scoped to the current Codex session. The running count is
15
- persisted under .codex/state/powershell-batch-budget.<session_id>.json. Only
16
- distinct file paths are counted; repeated edits to the same file consume one slot.
17
-
18
- Test files are those matching:
9
+ it decides whether the change may proceed in direct mode.
10
+
11
+ Direct mode counts distinct production PowerShell paths per Codex session. The
12
+ running set is persisted under .codex/state/powershell-batch-budget.<session_id>.json,
13
+ and repeated edits to the same file are counted once. The 4th distinct production
14
+ path is denied with a POWERSHELL_LARGE_PATH_REQUIRED reason that instructs the
15
+ caller to route the change through .codex/prompts/orchestrate-work.md.
16
+
17
+ The orchestrated large path is detected from
18
+ <root>/artifacts/orchestration/orchestrator-state.json. The selected route is the
19
+ route_id value when that key is present, otherwise the path_selected value, and it
20
+ is usable only as a non-blank string. When the selected route is large,
21
+ remediation, or preparation and the checkpoint is not terminal (next_step is not
22
+ complete and completed_steps does not contain S12_complete), no path is denied for
23
+ count and no state is written. Every other checkpoint outcome, including an absent,
24
+ unreadable, or malformed checkpoint, enforces direct mode.
25
+
26
+ Test files are never counted. They are those matching:
19
27
  - tests/**/*.ps1
20
28
  - *.Tests.ps1
21
29
 
22
- All other .ps1/.psm1/.psd1 files are treated as production files. Non-PowerShell
23
- paths pass through.
30
+ All other .ps1/.psm1/.psd1 files are production files. Non-PowerShell paths pass
31
+ through. The threshold of three production files is a routing constant and is not
32
+ configurable at runtime. Legacy prodCap, testCap, and testFiles keys in a persisted
33
+ state file are ignored when the state is loaded.
24
34
 
25
- An approved per-session override is recorded by writing
26
- {"prodCap": N, "testCap": M} into the Codex state file.
35
+ When a production path is denied, the script emits a PreToolUse JSON response with
36
+ hookSpecificOutput.permissionDecision = 'deny' and exits 0. Files already counted
37
+ are always allowed through.
27
38
 
28
- When the cap would be exceeded by a new file, the script emits a PreToolUse JSON
29
- response with hookSpecificOutput.permissionDecision = 'deny' and exits 0. The session
30
- must explicitly reset the counter by deleting the state file before starting a new
31
- batch. Files already counted are always allowed through.
39
+ Known limitation: a stale non-terminal large-path checkpoint left at the root
40
+ exempts a later direct-mode session at that root. Orchestrator checkpoint hygiene
41
+ (issue #673) moves foreign checkpoints aside before a new run.
32
42
 
33
43
  .NOTES
34
44
  Compatible with PowerShell 7+.
@@ -40,26 +50,28 @@ param()
40
50
  # mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
41
51
  . (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
42
52
 
53
+ # Shared route helpers; this file is byte-identical to the Claude runtime copy.
54
+ . (Join-Path $PSScriptRoot 'enforce-batch-budget-route.ps1')
55
+
43
56
  function Get-PowerShellBatchBudgetState {
57
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'TestCap', Justification = 'Accepted and ignored for callers written against the removed test-file cap.')]
44
58
  [CmdletBinding()]
45
59
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
46
60
  param(
47
61
  [Parameter(Mandatory)]
48
62
  [int] $ProdCap,
49
63
 
50
- [Parameter(Mandatory)]
51
- [int] $TestCap
64
+ [int] $TestCap = 0
52
65
  )
53
66
 
54
67
  [ordered]@{
55
68
  prodCap = $ProdCap
56
- testCap = $TestCap
57
69
  prodFiles = @()
58
- testFiles = @()
59
70
  }
60
71
  }
61
72
 
62
73
  function ConvertTo-PowerShellBatchBudgetState {
74
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'TestCap', Justification = 'Accepted and ignored for callers written against the removed test-file cap.')]
63
75
  [CmdletBinding()]
64
76
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
65
77
  param(
@@ -69,15 +81,13 @@ function ConvertTo-PowerShellBatchBudgetState {
69
81
  [Parameter(Mandatory)]
70
82
  [int] $ProdCap,
71
83
 
72
- [Parameter(Mandatory)]
73
- [int] $TestCap
84
+ [int] $TestCap = 0
74
85
  )
75
86
 
76
- $state = Get-PowerShellBatchBudgetState -ProdCap $ProdCap -TestCap $TestCap
77
- if ($null -ne $InputObject.prodCap) { $state.prodCap = [int]$InputObject.prodCap }
78
- if ($null -ne $InputObject.testCap) { $state.testCap = [int]$InputObject.testCap }
87
+ # Only prodFiles is carried over. Persisted prodCap, testCap, and testFiles keys
88
+ # come from the removed test-file cap and recorded overrides and are ignored.
89
+ $state = Get-PowerShellBatchBudgetState -ProdCap $ProdCap
79
90
  if ($null -ne $InputObject.prodFiles) { $state.prodFiles = @($InputObject.prodFiles) }
80
- if ($null -ne $InputObject.testFiles) { $state.testFiles = @($InputObject.testFiles) }
81
91
 
82
92
  return $state
83
93
  }
@@ -116,8 +126,13 @@ function Invoke-PowerShellBatchBudgetDecision {
116
126
  [Parameter(Mandatory)]
117
127
  [System.Collections.IDictionary] $State,
118
128
 
119
- [Parameter(Mandatory)]
120
- [string] $StateFile
129
+ [AllowEmptyString()]
130
+ [string] $StateFile = '',
131
+
132
+ [switch] $LargePathRoute,
133
+
134
+ [AllowEmptyString()]
135
+ [string] $ObservedRoute = ''
121
136
  )
122
137
 
123
138
  $normalized = $FilePath -replace '\\', '/'
@@ -125,26 +140,32 @@ function Invoke-PowerShellBatchBudgetDecision {
125
140
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
126
141
  }
127
142
 
143
+ # The orchestrated large path has no production-file cap, so nothing is counted.
144
+ if ($LargePathRoute) {
145
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
146
+ }
147
+
148
+ # Test files never count toward the routing threshold.
128
149
  $isTestFile = ($normalized -match '(^|/)tests/.*\.ps1$') -or ($normalized -match '\.Tests\.ps1$')
129
- $targetList = if ($isTestFile) { @($State.testFiles) } else { @($State.prodFiles) }
130
- $cap = if ($isTestFile) { [int]$State.testCap } else { [int]$State.prodCap }
131
- $kind = if ($isTestFile) { 'test' } else { 'production' }
150
+ if ($isTestFile) {
151
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
152
+ }
132
153
 
133
- if ($targetList -contains $normalized) {
154
+ $countedFiles = @($State.prodFiles)
155
+ if ($countedFiles -contains $normalized) {
134
156
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
135
157
  }
136
158
 
137
- if ($targetList.Count -ge $cap) {
138
- $currentFiles = ($targetList -join ', ')
139
- $reason = "PowerShell per-batch budget exceeded: $kind file cap is $cap and is already full ($currentFiles). Requested new file: $normalized. Split the work into a new batch, record an approved cap in $StateFile, or reset the batch by deleting that state file."
159
+ $cap = [int]$State.prodCap
160
+ if ($countedFiles.Count -ge $cap) {
161
+ Write-Verbose "Denying production PowerShell path '$normalized' in direct mode; counted paths are recorded in '$StateFile'."
162
+ $counted = $countedFiles -join ', '
163
+ $route = if ([string]::IsNullOrWhiteSpace($ObservedRoute)) { 'none' } else { $ObservedRoute }
164
+ $reason = "POWERSHELL_LARGE_PATH_REQUIRED: this change touches more than $cap production PowerShell files (already counted: $counted; requested: $normalized). A change of this size belongs on the orchestrated large path, which has no production-file cap. Route the change through .codex/prompts/orchestrate-work.md. Checkpoint route observed: $route."
140
165
  return Get-PowerShellBatchBudgetBlockDecision -Reason $reason -State $State
141
166
  }
142
167
 
143
- if ($isTestFile) {
144
- $State.testFiles = @($State.testFiles) + @($normalized)
145
- } else {
146
- $State.prodFiles = @($State.prodFiles) + @($normalized)
147
- }
168
+ $State.prodFiles = $countedFiles + @($normalized)
148
169
 
149
170
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $true }
150
171
  }
@@ -157,7 +178,13 @@ function Invoke-PowerShellBatchBudgetHook {
157
178
  [string] $SessionId = 'default',
158
179
  [string] $Root = (Get-Location).Path,
159
180
  [int] $ProdCap = 3,
160
- [int] $TestCap = 3,
181
+ [scriptblock] $ReadCheckpoint = {
182
+ param([string] $Path)
183
+ if (Test-Path -LiteralPath $Path -PathType Leaf) {
184
+ return (Get-Content -LiteralPath $Path -Raw)
185
+ }
186
+ return ''
187
+ },
161
188
  [scriptblock] $TestPathExists = { param([string] $Path) Test-Path -Path $Path },
162
189
  [scriptblock] $EnsureDirectory = { param([string] $Path) New-Item -ItemType Directory -Path $Path -Force | Out-Null },
163
190
  [scriptblock] $ReadState = { param([string] $Path) Get-Content -Path $Path -Raw },
@@ -188,23 +215,41 @@ function Invoke-PowerShellBatchBudgetHook {
188
215
  }
189
216
 
190
217
  $stateDir = Join-Path -Path $Root -ChildPath '.codex/state'
218
+ $stateFile = Join-Path -Path $stateDir -ChildPath ("powershell-batch-budget.$SessionId.json")
219
+
220
+ # The checkpoint is read before any state operation, so the large path neither
221
+ # creates the state directory nor reads or writes the state file.
222
+ $checkpointPath = Join-Path -Path $Root -ChildPath 'artifacts/orchestration/orchestrator-state.json'
223
+ $checkpointText = ''
224
+ try {
225
+ $checkpointText = [string](& $ReadCheckpoint $checkpointPath)
226
+ } catch {
227
+ Write-Verbose "Treating unreadable orchestrator checkpoint '$checkpointPath' as direct mode: $($_.Exception.Message)"
228
+ $checkpointText = ''
229
+ }
230
+
231
+ $isLargePath = Test-BatchBudgetLargePathRoute -CheckpointText $checkpointText
232
+ $observedRoute = Get-BatchBudgetSelectedRoute -CheckpointText $checkpointText
233
+ if ($isLargePath) {
234
+ return Invoke-PowerShellBatchBudgetDecision -FilePath $filePath -State (Get-PowerShellBatchBudgetState -ProdCap $ProdCap) -StateFile $stateFile -LargePathRoute
235
+ }
236
+
191
237
  if (-not (& $TestPathExists $stateDir)) {
192
238
  & $EnsureDirectory $stateDir
193
239
  }
194
240
 
195
- $stateFile = Join-Path -Path $stateDir -ChildPath ("powershell-batch-budget.$SessionId.json")
196
- $state = Get-PowerShellBatchBudgetState -ProdCap $ProdCap -TestCap $TestCap
241
+ $state = Get-PowerShellBatchBudgetState -ProdCap $ProdCap
197
242
 
198
243
  if (& $TestPathExists $stateFile) {
199
244
  try {
200
245
  $loaded = & $ReadState $stateFile | ConvertFrom-Json -ErrorAction Stop
201
- $state = ConvertTo-PowerShellBatchBudgetState -InputObject $loaded -ProdCap $ProdCap -TestCap $TestCap
246
+ $state = ConvertTo-PowerShellBatchBudgetState -InputObject $loaded -ProdCap $ProdCap
202
247
  } catch {
203
248
  Write-Verbose "Ignoring unreadable PowerShell batch-budget state file '$stateFile': $($_.Exception.Message)"
204
249
  }
205
250
  }
206
251
 
207
- $decision = Invoke-PowerShellBatchBudgetDecision -FilePath $filePath -State $state -StateFile $stateFile
252
+ $decision = Invoke-PowerShellBatchBudgetDecision -FilePath $filePath -State $state -StateFile $stateFile -ObservedRoute $observedRoute
208
253
  if ($decision.shouldWriteState) {
209
254
  try {
210
255
  & $WriteState $stateFile $decision.state
@@ -216,41 +261,81 @@ function Invoke-PowerShellBatchBudgetHook {
216
261
  return $decision
217
262
  }
218
263
 
219
- if ($MyInvocation.InvocationName -eq '.') {
220
- return
221
- }
264
+ function Invoke-PowerShellBatchBudgetCodexEntryPoint {
265
+ <#
266
+ .SYNOPSIS
267
+ Runs the Codex PowerShell batch-budget decision and returns the process exit code.
268
+ .DESCRIPTION
269
+ Parses the Codex PreToolUse payload through the shared transport, collects both
270
+ sides of every mapped file edit, and evaluates each path with the hook. The
271
+ first deny is written to the output stream as compact JSON without its state
272
+ property, followed by exit code 0. When nothing is denied the function returns
273
+ 0 and writes nothing else. Any failure writes the error text to standard error
274
+ and returns 2. The function does not call exit.
275
+ .PARAMETER PayloadRaw
276
+ Raw PreToolUse payload text.
277
+ .PARAMETER RepositoryRoot
278
+ Root used to locate the Codex state directory and the orchestrator checkpoint.
279
+ .PARAMETER HookSeams
280
+ Optional seam overrides splatted into Invoke-PowerShellBatchBudgetHook.
281
+ #>
282
+ [CmdletBinding()]
283
+ [OutputType([int])]
284
+ param(
285
+ [Parameter(Mandatory)]
286
+ [AllowNull()]
287
+ [AllowEmptyString()]
288
+ [string] $PayloadRaw,
289
+
290
+ [Parameter(Mandatory)]
291
+ [string] $RepositoryRoot,
222
292
 
223
- try {
224
- # Transport and mapping come from the shared module. session_id is still
225
- # required because the batch counter is keyed by it.
226
- $payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw ([Console]::In.ReadToEnd()) -HookName 'enforce-powershell-batch-budget' -RequireSessionId
227
- $sessionId = ([string]$payload.session_id) -replace '[^A-Za-z0-9._-]', '_'
228
- $repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
229
- $prodCap = 3
230
- $testCap = 3
231
-
232
- # Both sides of a rename consume budget, matching the pre-fix path scan that
233
- # collected Add/Update/Delete targets and Move destinations alike. Ordering
234
- # and de-duplication are preserved. A well-formed payload that maps to no
235
- # file yields no paths, so no state is written and the hook allows silently.
236
- $budgetPaths = @(
237
- @(ConvertTo-CodexFileEditInput -Payload $payload) |
238
- ForEach-Object { $_.source_path; $_.file_path } |
239
- Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
240
- Select-Object -Unique
293
+ [hashtable] $HookSeams = @{}
241
294
  )
242
295
 
243
- foreach ($path in $budgetPaths) {
244
- $toolInputRaw = @{ file_path = $path } | ConvertTo-Json -Compress
245
- $decision = Invoke-PowerShellBatchBudgetHook -ToolInputRaw $toolInputRaw -SessionId $sessionId -Root $repositoryRoot -ProdCap $prodCap -TestCap $testCap
246
- if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
247
- $decision.Remove('state')
248
- $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
249
- exit 0
296
+ try {
297
+ # Transport and mapping come from the shared module. session_id is still
298
+ # required because the direct-mode counter is keyed by it.
299
+ $payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw $PayloadRaw -HookName 'enforce-powershell-batch-budget' -RequireSessionId
300
+ $sessionId = ([string]$payload.session_id) -replace '[^A-Za-z0-9._-]', '_'
301
+
302
+ # Both sides of a rename are evaluated, matching the path scan that collected
303
+ # Add/Update/Delete targets and Move destinations alike. Ordering and
304
+ # de-duplication are preserved. A well-formed payload that maps to no file
305
+ # yields no paths, so no state is written and the hook allows silently.
306
+ $budgetPaths = @(
307
+ @(ConvertTo-CodexFileEditInput -Payload $payload) |
308
+ ForEach-Object { $_.source_path; $_.file_path } |
309
+ Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
310
+ Select-Object -Unique
311
+ )
312
+
313
+ foreach ($path in $budgetPaths) {
314
+ $toolInputRaw = @{ file_path = $path } | ConvertTo-Json -Compress
315
+ $decision = Invoke-PowerShellBatchBudgetHook -ToolInputRaw $toolInputRaw -SessionId $sessionId -Root $RepositoryRoot @HookSeams
316
+ if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
317
+ $decision.Remove('state')
318
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
319
+ return 0
320
+ }
250
321
  }
322
+
323
+ return 0
324
+ } catch {
325
+ [Console]::Error.WriteLine([string]$_)
326
+ return 2
251
327
  }
252
- exit 0
253
- } catch {
254
- [Console]::Error.WriteLine([string]$_)
255
- exit 2
256
328
  }
329
+
330
+ if ($MyInvocation.InvocationName -eq '.') {
331
+ return
332
+ }
333
+
334
+ # The entry point returns its [int] exit code as the last pipeline element and any
335
+ # deny JSON before it, so the JSON is written explicitly before the process exits.
336
+ $entryPointResult = @(Invoke-PowerShellBatchBudgetCodexEntryPoint -PayloadRaw ([Console]::In.ReadToEnd()) -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent))
337
+ if ($entryPointResult.Count -gt 1) {
338
+ $entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
339
+ }
340
+
341
+ exit ([int]$entryPointResult[-1])