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