@danmoisan/drm-copilot-mcp 1.1.14 → 1.1.16

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 (39) hide show
  1. package/package.json +1 -1
  2. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +4 -5
  3. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget-route.ps1 +137 -0
  4. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +95 -66
  5. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
  6. package/resources/claude-customizations/.claude/rules/powershell.md +3 -3
  7. package/resources/claude-customizations/.claude/rules/shell.md +8 -7
  8. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
  9. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
  10. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
  11. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
  12. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
  13. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
  14. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
  15. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
  16. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
  17. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
  18. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
  19. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
  20. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +3 -4
  21. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
  22. package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +6 -6
  23. package/resources/claude-customizations/pack-manifests/core.json +11 -0
  24. package/resources/claude-customizations/pack-manifests/powershell.json +1 -0
  25. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +1 -1
  26. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -2
  27. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +2 -3
  28. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +2 -3
  29. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +2 -3
  30. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +2 -3
  31. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +2 -3
  32. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -3
  33. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  34. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +285 -80
  35. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +7 -7
  36. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +8 -10
  37. package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +2 -2
  38. package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +3 -3
  39. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +6 -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,148 @@ 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
+ function ConvertFrom-PowerShellBatchBudgetCheckpoint {
54
+ <#
55
+ .SYNOPSIS
56
+ Parses orchestrator checkpoint text into a checkpoint object.
57
+ .DESCRIPTION
58
+ Returns the parsed object only when the text is a JSON object. Null or
59
+ whitespace text, text that is not parseable JSON, and JSON whose root is not
60
+ an object all return $null. Performs no file or environment access.
61
+ .PARAMETER CheckpointText
62
+ Raw text of the orchestrator checkpoint file.
63
+ #>
64
+ [CmdletBinding()]
65
+ [OutputType([psobject])]
66
+ param(
67
+ [AllowNull()]
68
+ [AllowEmptyString()]
69
+ [string] $CheckpointText
70
+ )
71
+
72
+ if ([string]::IsNullOrWhiteSpace($CheckpointText)) {
73
+ return $null
74
+ }
75
+
76
+ try {
77
+ $parsed = ConvertFrom-Json -InputObject $CheckpointText -ErrorAction Stop
78
+ } catch {
79
+ Write-Verbose "Ignoring unparseable orchestrator checkpoint text: $($_.Exception.Message)"
80
+ return $null
81
+ }
82
+
83
+ if ($parsed -is [System.Management.Automation.PSCustomObject]) {
84
+ return $parsed
85
+ }
86
+
87
+ return $null
88
+ }
89
+
90
+ function Get-PowerShellBatchBudgetSelectedRoute {
91
+ <#
92
+ .SYNOPSIS
93
+ Returns the route selected by an orchestrator checkpoint.
94
+ .DESCRIPTION
95
+ Uses route_id when the checkpoint has that key, otherwise path_selected. The
96
+ value is returned only when it is a non-blank string; every other case,
97
+ including unparseable text, returns an empty string.
98
+ .PARAMETER CheckpointText
99
+ Raw text of the orchestrator checkpoint file.
100
+ #>
101
+ [CmdletBinding()]
102
+ [OutputType([string])]
103
+ param(
104
+ [AllowNull()]
105
+ [AllowEmptyString()]
106
+ [string] $CheckpointText
107
+ )
108
+
109
+ $checkpoint = ConvertFrom-PowerShellBatchBudgetCheckpoint -CheckpointText $CheckpointText
110
+ if ($null -eq $checkpoint) {
111
+ return ''
112
+ }
113
+
114
+ $propertyNames = @($checkpoint.PSObject.Properties.Name)
115
+ $rawRoute = $null
116
+ if ($propertyNames -contains 'route_id') {
117
+ $rawRoute = $checkpoint.route_id
118
+ } elseif ($propertyNames -contains 'path_selected') {
119
+ $rawRoute = $checkpoint.path_selected
120
+ }
121
+
122
+ if ($rawRoute -is [string] -and -not [string]::IsNullOrWhiteSpace($rawRoute)) {
123
+ return $rawRoute
124
+ }
125
+
126
+ return ''
127
+ }
128
+
129
+ function Test-PowerShellBatchBudgetLargePathRoute {
130
+ <#
131
+ .SYNOPSIS
132
+ Reports whether an orchestrator checkpoint selects the uncapped large path.
133
+ .DESCRIPTION
134
+ Returns $true only when the checkpoint parses to an object, its selected route
135
+ is large, remediation, or preparation (case-sensitive), next_step is not
136
+ complete, and completed_steps does not contain S12_complete. No input causes
137
+ an exception; any failure returns $false.
138
+ .PARAMETER CheckpointText
139
+ Raw text of the orchestrator checkpoint file.
140
+ #>
141
+ [CmdletBinding()]
142
+ [OutputType([bool])]
143
+ param(
144
+ [AllowNull()]
145
+ [AllowEmptyString()]
146
+ [string] $CheckpointText
147
+ )
148
+
149
+ try {
150
+ $checkpoint = ConvertFrom-PowerShellBatchBudgetCheckpoint -CheckpointText $CheckpointText
151
+ if ($null -eq $checkpoint) {
152
+ return $false
153
+ }
154
+
155
+ $route = Get-PowerShellBatchBudgetSelectedRoute -CheckpointText $CheckpointText
156
+ if (-not (@('large', 'remediation', 'preparation') -ccontains $route)) {
157
+ return $false
158
+ }
159
+
160
+ $propertyNames = @($checkpoint.PSObject.Properties.Name)
161
+ if ($propertyNames -contains 'next_step' -and $checkpoint.next_step -is [string] -and $checkpoint.next_step -ceq 'complete') {
162
+ return $false
163
+ }
164
+
165
+ if ($propertyNames -contains 'completed_steps' -and (@($checkpoint.completed_steps) -ccontains 'S12_complete')) {
166
+ return $false
167
+ }
168
+
169
+ return $true
170
+ } catch {
171
+ Write-Verbose "Treating the orchestrator checkpoint as direct mode: $($_.Exception.Message)"
172
+ return $false
173
+ }
174
+ }
175
+
43
176
  function Get-PowerShellBatchBudgetState {
177
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'TestCap', Justification = 'Accepted and ignored for callers written against the removed test-file cap.')]
44
178
  [CmdletBinding()]
45
179
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
46
180
  param(
47
181
  [Parameter(Mandatory)]
48
182
  [int] $ProdCap,
49
183
 
50
- [Parameter(Mandatory)]
51
- [int] $TestCap
184
+ [int] $TestCap = 0
52
185
  )
53
186
 
54
187
  [ordered]@{
55
188
  prodCap = $ProdCap
56
- testCap = $TestCap
57
189
  prodFiles = @()
58
- testFiles = @()
59
190
  }
60
191
  }
61
192
 
62
193
  function ConvertTo-PowerShellBatchBudgetState {
194
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'TestCap', Justification = 'Accepted and ignored for callers written against the removed test-file cap.')]
63
195
  [CmdletBinding()]
64
196
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
65
197
  param(
@@ -69,15 +201,13 @@ function ConvertTo-PowerShellBatchBudgetState {
69
201
  [Parameter(Mandatory)]
70
202
  [int] $ProdCap,
71
203
 
72
- [Parameter(Mandatory)]
73
- [int] $TestCap
204
+ [int] $TestCap = 0
74
205
  )
75
206
 
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 }
207
+ # Only prodFiles is carried over. Persisted prodCap, testCap, and testFiles keys
208
+ # come from the removed test-file cap and recorded overrides and are ignored.
209
+ $state = Get-PowerShellBatchBudgetState -ProdCap $ProdCap
79
210
  if ($null -ne $InputObject.prodFiles) { $state.prodFiles = @($InputObject.prodFiles) }
80
- if ($null -ne $InputObject.testFiles) { $state.testFiles = @($InputObject.testFiles) }
81
211
 
82
212
  return $state
83
213
  }
@@ -116,8 +246,13 @@ function Invoke-PowerShellBatchBudgetDecision {
116
246
  [Parameter(Mandatory)]
117
247
  [System.Collections.IDictionary] $State,
118
248
 
119
- [Parameter(Mandatory)]
120
- [string] $StateFile
249
+ [AllowEmptyString()]
250
+ [string] $StateFile = '',
251
+
252
+ [switch] $LargePathRoute,
253
+
254
+ [AllowEmptyString()]
255
+ [string] $ObservedRoute = ''
121
256
  )
122
257
 
123
258
  $normalized = $FilePath -replace '\\', '/'
@@ -125,26 +260,32 @@ function Invoke-PowerShellBatchBudgetDecision {
125
260
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
126
261
  }
127
262
 
263
+ # The orchestrated large path has no production-file cap, so nothing is counted.
264
+ if ($LargePathRoute) {
265
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
266
+ }
267
+
268
+ # Test files never count toward the routing threshold.
128
269
  $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' }
270
+ if ($isTestFile) {
271
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
272
+ }
132
273
 
133
- if ($targetList -contains $normalized) {
274
+ $countedFiles = @($State.prodFiles)
275
+ if ($countedFiles -contains $normalized) {
134
276
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
135
277
  }
136
278
 
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."
279
+ $cap = [int]$State.prodCap
280
+ if ($countedFiles.Count -ge $cap) {
281
+ Write-Verbose "Denying production PowerShell path '$normalized' in direct mode; counted paths are recorded in '$StateFile'."
282
+ $counted = $countedFiles -join ', '
283
+ $route = if ([string]::IsNullOrWhiteSpace($ObservedRoute)) { 'none' } else { $ObservedRoute }
284
+ $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
285
  return Get-PowerShellBatchBudgetBlockDecision -Reason $reason -State $State
141
286
  }
142
287
 
143
- if ($isTestFile) {
144
- $State.testFiles = @($State.testFiles) + @($normalized)
145
- } else {
146
- $State.prodFiles = @($State.prodFiles) + @($normalized)
147
- }
288
+ $State.prodFiles = $countedFiles + @($normalized)
148
289
 
149
290
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $true }
150
291
  }
@@ -157,7 +298,13 @@ function Invoke-PowerShellBatchBudgetHook {
157
298
  [string] $SessionId = 'default',
158
299
  [string] $Root = (Get-Location).Path,
159
300
  [int] $ProdCap = 3,
160
- [int] $TestCap = 3,
301
+ [scriptblock] $ReadCheckpoint = {
302
+ param([string] $Path)
303
+ if (Test-Path -LiteralPath $Path -PathType Leaf) {
304
+ return (Get-Content -LiteralPath $Path -Raw)
305
+ }
306
+ return ''
307
+ },
161
308
  [scriptblock] $TestPathExists = { param([string] $Path) Test-Path -Path $Path },
162
309
  [scriptblock] $EnsureDirectory = { param([string] $Path) New-Item -ItemType Directory -Path $Path -Force | Out-Null },
163
310
  [scriptblock] $ReadState = { param([string] $Path) Get-Content -Path $Path -Raw },
@@ -188,23 +335,41 @@ function Invoke-PowerShellBatchBudgetHook {
188
335
  }
189
336
 
190
337
  $stateDir = Join-Path -Path $Root -ChildPath '.codex/state'
338
+ $stateFile = Join-Path -Path $stateDir -ChildPath ("powershell-batch-budget.$SessionId.json")
339
+
340
+ # The checkpoint is read before any state operation, so the large path neither
341
+ # creates the state directory nor reads or writes the state file.
342
+ $checkpointPath = Join-Path -Path $Root -ChildPath 'artifacts/orchestration/orchestrator-state.json'
343
+ $checkpointText = ''
344
+ try {
345
+ $checkpointText = [string](& $ReadCheckpoint $checkpointPath)
346
+ } catch {
347
+ Write-Verbose "Treating unreadable orchestrator checkpoint '$checkpointPath' as direct mode: $($_.Exception.Message)"
348
+ $checkpointText = ''
349
+ }
350
+
351
+ $isLargePath = Test-PowerShellBatchBudgetLargePathRoute -CheckpointText $checkpointText
352
+ $observedRoute = Get-PowerShellBatchBudgetSelectedRoute -CheckpointText $checkpointText
353
+ if ($isLargePath) {
354
+ return Invoke-PowerShellBatchBudgetDecision -FilePath $filePath -State (Get-PowerShellBatchBudgetState -ProdCap $ProdCap) -StateFile $stateFile -LargePathRoute
355
+ }
356
+
191
357
  if (-not (& $TestPathExists $stateDir)) {
192
358
  & $EnsureDirectory $stateDir
193
359
  }
194
360
 
195
- $stateFile = Join-Path -Path $stateDir -ChildPath ("powershell-batch-budget.$SessionId.json")
196
- $state = Get-PowerShellBatchBudgetState -ProdCap $ProdCap -TestCap $TestCap
361
+ $state = Get-PowerShellBatchBudgetState -ProdCap $ProdCap
197
362
 
198
363
  if (& $TestPathExists $stateFile) {
199
364
  try {
200
365
  $loaded = & $ReadState $stateFile | ConvertFrom-Json -ErrorAction Stop
201
- $state = ConvertTo-PowerShellBatchBudgetState -InputObject $loaded -ProdCap $ProdCap -TestCap $TestCap
366
+ $state = ConvertTo-PowerShellBatchBudgetState -InputObject $loaded -ProdCap $ProdCap
202
367
  } catch {
203
368
  Write-Verbose "Ignoring unreadable PowerShell batch-budget state file '$stateFile': $($_.Exception.Message)"
204
369
  }
205
370
  }
206
371
 
207
- $decision = Invoke-PowerShellBatchBudgetDecision -FilePath $filePath -State $state -StateFile $stateFile
372
+ $decision = Invoke-PowerShellBatchBudgetDecision -FilePath $filePath -State $state -StateFile $stateFile -ObservedRoute $observedRoute
208
373
  if ($decision.shouldWriteState) {
209
374
  try {
210
375
  & $WriteState $stateFile $decision.state
@@ -216,41 +381,81 @@ function Invoke-PowerShellBatchBudgetHook {
216
381
  return $decision
217
382
  }
218
383
 
219
- if ($MyInvocation.InvocationName -eq '.') {
220
- return
221
- }
384
+ function Invoke-PowerShellBatchBudgetCodexEntryPoint {
385
+ <#
386
+ .SYNOPSIS
387
+ Runs the Codex PowerShell batch-budget decision and returns the process exit code.
388
+ .DESCRIPTION
389
+ Parses the Codex PreToolUse payload through the shared transport, collects both
390
+ sides of every mapped file edit, and evaluates each path with the hook. The
391
+ first deny is written to the output stream as compact JSON without its state
392
+ property, followed by exit code 0. When nothing is denied the function returns
393
+ 0 and writes nothing else. Any failure writes the error text to standard error
394
+ and returns 2. The function does not call exit.
395
+ .PARAMETER PayloadRaw
396
+ Raw PreToolUse payload text.
397
+ .PARAMETER RepositoryRoot
398
+ Root used to locate the Codex state directory and the orchestrator checkpoint.
399
+ .PARAMETER HookSeams
400
+ Optional seam overrides splatted into Invoke-PowerShellBatchBudgetHook.
401
+ #>
402
+ [CmdletBinding()]
403
+ [OutputType([int])]
404
+ param(
405
+ [Parameter(Mandatory)]
406
+ [AllowNull()]
407
+ [AllowEmptyString()]
408
+ [string] $PayloadRaw,
409
+
410
+ [Parameter(Mandatory)]
411
+ [string] $RepositoryRoot,
222
412
 
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
413
+ [hashtable] $HookSeams = @{}
241
414
  )
242
415
 
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
416
+ try {
417
+ # Transport and mapping come from the shared module. session_id is still
418
+ # required because the direct-mode counter is keyed by it.
419
+ $payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw $PayloadRaw -HookName 'enforce-powershell-batch-budget' -RequireSessionId
420
+ $sessionId = ([string]$payload.session_id) -replace '[^A-Za-z0-9._-]', '_'
421
+
422
+ # Both sides of a rename are evaluated, matching the path scan that collected
423
+ # Add/Update/Delete targets and Move destinations alike. Ordering and
424
+ # de-duplication are preserved. A well-formed payload that maps to no file
425
+ # yields no paths, so no state is written and the hook allows silently.
426
+ $budgetPaths = @(
427
+ @(ConvertTo-CodexFileEditInput -Payload $payload) |
428
+ ForEach-Object { $_.source_path; $_.file_path } |
429
+ Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
430
+ Select-Object -Unique
431
+ )
432
+
433
+ foreach ($path in $budgetPaths) {
434
+ $toolInputRaw = @{ file_path = $path } | ConvertTo-Json -Compress
435
+ $decision = Invoke-PowerShellBatchBudgetHook -ToolInputRaw $toolInputRaw -SessionId $sessionId -Root $RepositoryRoot @HookSeams
436
+ if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
437
+ $decision.Remove('state')
438
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
439
+ return 0
440
+ }
250
441
  }
442
+
443
+ return 0
444
+ } catch {
445
+ [Console]::Error.WriteLine([string]$_)
446
+ return 2
251
447
  }
252
- exit 0
253
- } catch {
254
- [Console]::Error.WriteLine([string]$_)
255
- exit 2
256
448
  }
449
+
450
+ if ($MyInvocation.InvocationName -eq '.') {
451
+ return
452
+ }
453
+
454
+ # The entry point returns its [int] exit code as the last pipeline element and any
455
+ # deny JSON before it, so the JSON is written explicitly before the process exits.
456
+ $entryPointResult = @(Invoke-PowerShellBatchBudgetCodexEntryPoint -PayloadRaw ([Console]::In.ReadToEnd()) -RepositoryRoot (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent))
457
+ if ($entryPointResult.Count -gt 1) {
458
+ $entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
459
+ }
460
+
461
+ exit ([int]$entryPointResult[-1])
@@ -14,7 +14,7 @@ handoffs:
14
14
  send: true
15
15
  - label: Small-scope implementation path
16
16
  agent: powershell-typed-engineer
17
- prompt: "Estimate and confirm scope (1-2 production PowerShell files + corresponding tests). If confirmed, execute the constrained short-path implementation phase from the approved minimal-audit plan and return analyzer/test/coverage deltas."
17
+ prompt: "Estimate and confirm scope (1-3 production PowerShell files + corresponding tests). If confirmed, execute the constrained short-path implementation phase from the approved minimal-audit plan and return analyzer/test/coverage deltas."
18
18
  send: true
19
19
  - label: Validate small-path delivery and post-QC docs
20
20
  agent: atomic_executor
@@ -98,8 +98,8 @@ Use these reusable skills to avoid duplicating shared operations:
98
98
 
99
99
  3) **Single source of routing truth = change budget**
100
100
  - First action is always to estimate rough change budget by identifying likely affected production PowerShell files.
101
- - If estimate is `1-2` production PowerShell files (+ corresponding tests), use **small path**.
102
- - If estimate is `>2` production PowerShell files, use **large path**.
101
+ - If estimate is `1-3` production PowerShell files (+ corresponding tests), use **small path**.
102
+ - If estimate is more than 3 (`>3`) production PowerShell files, use **large path**. The large path has no production-file cap.
103
103
 
104
104
  4) **Deterministic variable handling**
105
105
  - Persist and reuse these variables exactly as names:
@@ -123,12 +123,12 @@ Use these reusable skills to avoid duplicating shared operations:
123
123
  2. Estimate rough change budget.
124
124
  3. Write/update orchestration checkpoint.
125
125
  4. Route to one of two paths:
126
- - **Small path**: budget `1-2`
127
- - **Large path**: budget `>2`
126
+ - **Small path**: budget `1-3`
127
+ - **Large path**: budget `>3`
128
128
 
129
129
  ---
130
130
 
131
- ## Small path (budget 1-2 production PowerShell files)
131
+ ## Small path (budget 1-3 production PowerShell files)
132
132
 
133
133
  Follow this exact sequence.
134
134
 
@@ -245,7 +245,7 @@ Hard enforcement for S8:
245
245
 
246
246
  ---
247
247
 
248
- ## Large path (budget >2 production PowerShell files)
248
+ ## Large path (budget >3 production PowerShell files)
249
249
 
250
250
  Follow this exact sequence.
251
251
 
@@ -49,8 +49,8 @@ Use these reusable skills to avoid duplicating shared operations:
49
49
 
50
50
  - **Direct mode (default)**
51
51
  - Trigger: no directive line present.
52
- - Intended scope: up to 2 production PowerShell files (+ corresponding tests).
53
- - If estimated scope is >2 production files: stop and instruct caller to use `powershell-orchestrator` (or `.github/prompts/orchestrate-powershell-work.prompt.md`).
52
+ - Intended scope: 1-3 production PowerShell files (+ corresponding tests).
53
+ - If estimated scope is more than 3 production files: stop and instruct caller to use `powershell-orchestrator` (or `.github/prompts/orchestrate-powershell-work.prompt.md`).
54
54
 
55
55
  - **Orchestrator handoff mode**
56
56
  - Trigger: request includes exact line `DIRECTIVE: ORCHESTRATOR HANDOFF MODE`.
@@ -115,9 +115,9 @@ Blocking rules in orchestrator mode:
115
115
 
116
116
  ## 1) Scope control (NO scope creep)
117
117
 
118
- - **Direct mode** default scope is one small feature/bug slice (typically **1–2 production PowerShell files**) plus corresponding test file(s).
119
- - In **Direct mode**, if estimated scope exceeds **2 production PowerShell files**, do not continue implementation; instruct the user to invoke `powershell-orchestrator` (or `.github/prompts/orchestrate-powershell-work.prompt.md`) and stop.
120
- - In **Orchestrator handoff mode**, overall scope may exceed 2 production files when supported by provided context package and approved plan artifacts.
118
+ - **Direct mode** default scope is one small feature/bug slice (typically **1-3 production PowerShell files**) plus corresponding test file(s).
119
+ - In **Direct mode**, if estimated scope exceeds **3 production PowerShell files**, do not continue implementation; instruct the user to invoke `powershell-orchestrator` (or `.github/prompts/orchestrate-powershell-work.prompt.md`) and stop.
120
+ - In **Orchestrator handoff mode**, overall scope may exceed 3 production files when supported by provided context package and approved plan artifacts.
121
121
  - In all modes, avoid unrelated files and preserve minimal, targeted changes.
122
122
  - If scope expansion is required, STOP and provide:
123
123
  - a one-paragraph justification,
@@ -127,11 +127,9 @@ Blocking rules in orchestrator mode:
127
127
 
128
128
  ## 2) Change budget (hard gate)
129
129
 
130
- - **Direct mode** overall budget: up to **2 production PowerShell files** (+ corresponding tests).
130
+ - **Direct mode** overall budget: **1-3 production PowerShell files** (+ corresponding tests).
131
131
  - **Orchestrator handoff mode**: no strict overall production-file budget, provided required documentation package is present.
132
- - In all modes, per-batch budget remains: at most **3 production files** and **3 test files** unless explicit override is approved.
133
- - If no override is provided, the 3/3 per-batch limit applies.
134
- - If a batch would exceed budget, split it into smaller batches.
132
+ - Test files are not counted toward the routing threshold in any mode, and the large path has no production-file cap.
135
133
 
136
134
  ## 3) Deterministic unit tests only (no external dependency coupling)
137
135
 
@@ -195,7 +193,7 @@ If tools cannot run in the environment, STOP implementation and provide plan + p
195
193
  - if `DIRECTIVE: ORCHESTRATOR HANDOFF MODE` is present, use orchestrator handoff mode and validate full context package.
196
194
  - otherwise use direct mode.
197
195
  4) Enforce budget routing:
198
- - in direct mode, if estimated scope is >2 production PowerShell files, STOP and instruct user to invoke `powershell-orchestrator` for orchestration.
196
+ - in direct mode, if estimated scope is more than 3 production PowerShell files, STOP and instruct user to invoke `powershell-orchestrator` for orchestration.
199
197
  5) Summarize root cause/design constraint in one paragraph.
200
198
 
201
199
  ## Phase B — Design + plan (no edits)
@@ -21,9 +21,9 @@ Coordinate the user request from intake to completion using the correct path bas
21
21
  ## Required orchestration behavior
22
22
 
23
23
  1. Estimate rough change budget first (production PowerShell files).
24
- 2. If budget is **1–2 production PowerShell files** (+ corresponding tests):
24
+ 2. If budget is **1-3 production PowerShell files** (+ corresponding tests):
25
25
  - Delegate directly to `powershell-typed-engineer` for plan + implementation + QA closure.
26
- 3. If budget is **>2 production PowerShell files**:
26
+ 3. If budget is **more than 3 production PowerShell files**:
27
27
  - Execute full large-path lifecycle:
28
28
  1) Scope and create potential entry (`feature` vs `bug`, short-name creation)
29
29
  2) Promote to GitHub issue and capture issue metadata
@@ -18,8 +18,8 @@ Use this skill when:
18
18
 
19
19
  1) Estimate rough change budget first based on likely **production PowerShell files** touched.
20
20
  2) Route:
21
- - `1-2` production files (+ corresponding tests) → **small path** (`powershell-typed-engineer` direct mode).
22
- - `>2` production files → **large path** (`powershell-orchestrator`).
21
+ - `1-3` production files (+ corresponding tests) → **small path** (`powershell-typed-engineer` direct mode).
22
+ - More than 3 production files → **large path** (`powershell-orchestrator`). The large path has no production-file cap.
23
23
 
24
24
  ## Orchestrated Small-Path Requirements
25
25
 
@@ -36,7 +36,7 @@ Direct invocation of `powershell-typed-engineer` remains implementation-focused
36
36
 
37
37
  ## Direct-Mode Rejection Rule
38
38
 
39
- If `powershell-typed-engineer` is invoked directly and estimated scope is `>2` production files:
39
+ If `powershell-typed-engineer` is invoked directly and estimated scope is more than 3 production files:
40
40
  - Stop before implementation.
41
41
  - Return explicit routing instruction to invoke `powershell-orchestrator` (or `.github/prompts/orchestrate-powershell-work.prompt.md`).
42
42
 
@@ -29,6 +29,7 @@
29
29
  '.claude/hooks/check-powershell-test-purity.ps1'
30
30
  '.claude/hooks/enforce-python-batch-budget.ps1'
31
31
  '.claude/hooks/enforce-powershell-batch-budget.ps1'
32
+ '.claude/hooks/enforce-powershell-batch-budget-route.ps1'
32
33
  # Issue #272 hardened this PreToolUse hook with a new orchestrator-state preflight
33
34
  # check; measured here so the change produces real per-file coverage evidence.
34
35
  '.claude/hooks/enforce-pr-author-skill.ps1'
@@ -317,6 +318,11 @@
317
318
  # Issue #663 added the epic-scope resolver and its pure readiness predicates.
318
319
  '.claude/lib/worktree-resolution/EpicScopeResolution.psm1'
319
320
  '.claude/lib/worktree-resolution/EpicScopeReadiness.psm1'
321
+ # Issue #762 relocated the CI gate parser into the bundled .claude/lib tree so
322
+ # push-down carries it with the orchestrate and epic-orchestrate skills.
323
+ # CodeCoverage.Path is an explicit per-file allow-list, so the relocated production
324
+ # file is registered here to stay in the coverage denominator.
325
+ '.claude/lib/ci-gate/Invoke-CiGateParser.ps1'
320
326
  )
321
327
  # Optional: don't fail the run on coverage percentage
322
328
  CoveragePercentTarget = 0