@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.
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +4 -5
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget-route.ps1 +137 -0
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +95 -66
- package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
- package/resources/claude-customizations/.claude/rules/powershell.md +3 -3
- package/resources/claude-customizations/.claude/rules/shell.md +8 -7
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +3 -4
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +6 -6
- package/resources/claude-customizations/pack-manifests/core.json +11 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +1 -0
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -2
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +285 -80
- package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +7 -7
- package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +8 -10
- package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +2 -2
- package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +3 -3
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +6 -0
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1
CHANGED
|
@@ -1,34 +1,44 @@
|
|
|
1
1
|
|
|
2
2
|
<#
|
|
3
3
|
.SYNOPSIS
|
|
4
|
-
Pre-tool-use hook that
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
23
|
-
|
|
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
|
-
|
|
26
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
[
|
|
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
|
-
[
|
|
73
|
-
[int] $TestCap
|
|
204
|
+
[int] $TestCap = 0
|
|
74
205
|
)
|
|
75
206
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
[
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
270
|
+
if ($isTestFile) {
|
|
271
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
|
|
272
|
+
}
|
|
132
273
|
|
|
133
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
-
|
|
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
|
-
[
|
|
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
|
-
$
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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-
|
|
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-
|
|
102
|
-
- If estimate is `>
|
|
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-
|
|
127
|
-
- **Large path**: budget `>
|
|
126
|
+
- **Small path**: budget `1-3`
|
|
127
|
+
- **Large path**: budget `>3`
|
|
128
128
|
|
|
129
129
|
---
|
|
130
130
|
|
|
131
|
-
## Small path (budget 1-
|
|
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 >
|
|
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:
|
|
53
|
-
- If estimated scope is
|
|
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
|
|
119
|
-
- In **Direct mode**, if estimated scope exceeds **
|
|
120
|
-
- In **Orchestrator handoff mode**, overall scope may exceed
|
|
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:
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
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-
|
|
22
|
-
-
|
|
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
|
|
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
|