@danmoisan/drm-copilot-mcp 1.1.7 → 1.1.9
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 +73 -1
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +6 -5
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +24 -7
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +8 -5
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +186 -13
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +186 -13
- package/resources/claude-customizations/.claude/hooks/persist-session-id.ps1 +16 -5
- package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +495 -0
- package/resources/claude-customizations/.claude/lib/bash/report-lane-assertion.sh +169 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +7 -5
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +2 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +5 -3
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +2 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +6 -4
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +2 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +2 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +16 -16
- package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +2 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +2 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +3 -1
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +2 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +5 -3
- package/resources/claude-customizations/.claude/lib/model-routing/ModelRouting.psm1 +2 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +11 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCheckpointValue.psm1 +2 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexModelReceipts.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexTopologyReceipts.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +9 -7
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateModelReceipts.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +3 -1
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingMatrix.psm1 +3 -1
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateUnconditional.psm1 +7 -5
- package/resources/claude-customizations/.claude/lib/requirements/GeneratedDocumentCounters.psm1 +12 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +4 -4
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +12 -9
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +3 -3
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +8 -3
- package/resources/claude-customizations/config/blast-radius.json +2 -1
- package/resources/claude-customizations/pack-manifests/core.json +2 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
|
@@ -13,6 +13,19 @@
|
|
|
13
13
|
persisted under .claude/state/python-batch-budget.<session_id>.json. Only distinct
|
|
14
14
|
file paths are counted; repeated edits to the same file consume one slot.
|
|
15
15
|
|
|
16
|
+
The session id is resolved from the first non-empty of: the CLAUDE_SESSION_ID
|
|
17
|
+
environment variable; the contents of <root>/.claude/state/current-session-id;
|
|
18
|
+
a worktree-derived identifier built from the root's leaf name and a short
|
|
19
|
+
stable hash of its normalized path. The resolved value is sanitized before it
|
|
20
|
+
is composed into a file name, so a hostile id cannot escape the state
|
|
21
|
+
directory. Resolving the id never creates the state directory.
|
|
22
|
+
|
|
23
|
+
Candidate paths are contained to the resolved root. A candidate that resolves
|
|
24
|
+
outside it is discarded: the decision is 'allow', no slot is consumed, and no
|
|
25
|
+
state is written. Persisted entries that fail the same containment test are
|
|
26
|
+
dropped when state is rehydrated, so a state file carried between worktrees
|
|
27
|
+
cannot spend this worktree's budget.
|
|
28
|
+
|
|
16
29
|
Test files are those matching:
|
|
17
30
|
- tests/**/*.py
|
|
18
31
|
- test_*.py
|
|
@@ -36,6 +49,127 @@ param()
|
|
|
36
49
|
|
|
37
50
|
|
|
38
51
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
52
|
+
|
|
53
|
+
function Test-PythonBatchBudgetPathInRoot {
|
|
54
|
+
<#
|
|
55
|
+
.SYNOPSIS
|
|
56
|
+
Reports whether a candidate path belongs to the batch-budget root.
|
|
57
|
+
.DESCRIPTION
|
|
58
|
+
Compares forward-slash-normalized forms of the candidate and the root,
|
|
59
|
+
case-insensitively. A relative candidate carries no root of its own and is
|
|
60
|
+
admitted, which is what keeps a relative path recorded by one worktree from
|
|
61
|
+
being treated as foreign by another.
|
|
62
|
+
#>
|
|
63
|
+
[CmdletBinding()]
|
|
64
|
+
[OutputType([bool])]
|
|
65
|
+
param(
|
|
66
|
+
[AllowNull()]
|
|
67
|
+
[AllowEmptyString()]
|
|
68
|
+
[string] $Path,
|
|
69
|
+
|
|
70
|
+
[AllowNull()]
|
|
71
|
+
[AllowEmptyString()]
|
|
72
|
+
[string] $Root
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if ([string]::IsNullOrWhiteSpace($Path)) {
|
|
76
|
+
return $false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
$normalizedPath = $Path -replace '\\', '/'
|
|
80
|
+
if ($normalizedPath -notmatch '^(/|[A-Za-z]:/)') {
|
|
81
|
+
return $true
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
$normalizedRoot = ($Root -replace '\\', '/').TrimEnd('/')
|
|
85
|
+
if ([string]::IsNullOrWhiteSpace($normalizedRoot)) {
|
|
86
|
+
return $true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return ([string]::Equals($normalizedPath, $normalizedRoot, [System.StringComparison]::OrdinalIgnoreCase) -or $normalizedPath.StartsWith($normalizedRoot + '/', [System.StringComparison]::OrdinalIgnoreCase))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function ConvertTo-PythonBatchBudgetSafeSegment {
|
|
93
|
+
<#
|
|
94
|
+
.SYNOPSIS
|
|
95
|
+
Reduces a session id to characters that are safe in a file name.
|
|
96
|
+
#>
|
|
97
|
+
[CmdletBinding()]
|
|
98
|
+
[OutputType([string])]
|
|
99
|
+
param(
|
|
100
|
+
[AllowNull()]
|
|
101
|
+
[AllowEmptyString()]
|
|
102
|
+
[string] $Value
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return ($Value -replace '[^A-Za-z0-9._-]', '_')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function Get-PythonBatchBudgetSessionId {
|
|
109
|
+
<#
|
|
110
|
+
.SYNOPSIS
|
|
111
|
+
Resolves the session id used to compose the batch-budget state-file name.
|
|
112
|
+
.DESCRIPTION
|
|
113
|
+
Returns the first non-empty of: the explicit SessionId argument; the
|
|
114
|
+
CLAUDE_SESSION_ID environment variable; the contents of the session-id state
|
|
115
|
+
file; a worktree-derived identifier. The result is sanitized so it cannot
|
|
116
|
+
escape the state directory. The session-id state file is read through the
|
|
117
|
+
ReadSessionIdFile seam and is never created, so resolution performs no write.
|
|
118
|
+
#>
|
|
119
|
+
[CmdletBinding()]
|
|
120
|
+
[OutputType([string])]
|
|
121
|
+
param(
|
|
122
|
+
[AllowNull()]
|
|
123
|
+
[AllowEmptyString()]
|
|
124
|
+
[string] $SessionId,
|
|
125
|
+
|
|
126
|
+
[AllowNull()]
|
|
127
|
+
[AllowEmptyString()]
|
|
128
|
+
[string] $Root,
|
|
129
|
+
|
|
130
|
+
[Parameter(Mandatory)]
|
|
131
|
+
[string] $SessionIdFilePath,
|
|
132
|
+
|
|
133
|
+
[scriptblock] $ReadSessionIdFile
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
$candidates = @(
|
|
137
|
+
$SessionId
|
|
138
|
+
$env:CLAUDE_SESSION_ID
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
foreach ($candidate in $candidates) {
|
|
142
|
+
if (-not [string]::IsNullOrWhiteSpace($candidate)) {
|
|
143
|
+
return (ConvertTo-PythonBatchBudgetSafeSegment -Value $candidate.Trim())
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$fromFile = ''
|
|
148
|
+
try {
|
|
149
|
+
$fromFile = [string](& $ReadSessionIdFile $SessionIdFilePath)
|
|
150
|
+
} catch {
|
|
151
|
+
Write-Verbose "Ignoring unreadable session-id file '$SessionIdFilePath': $($_.Exception.Message)"
|
|
152
|
+
$fromFile = ''
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (-not [string]::IsNullOrWhiteSpace($fromFile)) {
|
|
156
|
+
return (ConvertTo-PythonBatchBudgetSafeSegment -Value $fromFile.Trim())
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
$normalizedRoot = ($Root -replace '\\', '/').TrimEnd('/')
|
|
160
|
+
$leaf = ConvertTo-PythonBatchBudgetSafeSegment -Value (Split-Path -Path $normalizedRoot -Leaf)
|
|
161
|
+
|
|
162
|
+
$sha = [System.Security.Cryptography.SHA256]::Create()
|
|
163
|
+
try {
|
|
164
|
+
$hashBytes = $sha.ComputeHash([System.Text.Encoding]::UTF8.GetBytes($normalizedRoot))
|
|
165
|
+
} finally {
|
|
166
|
+
$sha.Dispose()
|
|
167
|
+
}
|
|
168
|
+
$shortHash = -join (@($hashBytes[0..3]) | ForEach-Object { $_.ToString('x2') })
|
|
169
|
+
|
|
170
|
+
return "worktree-$leaf-$shortHash"
|
|
171
|
+
}
|
|
172
|
+
|
|
39
173
|
function Get-PythonBatchBudgetState {
|
|
40
174
|
[CmdletBinding()]
|
|
41
175
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -66,14 +200,26 @@ function ConvertTo-PythonBatchBudgetState {
|
|
|
66
200
|
[int] $ProdCap,
|
|
67
201
|
|
|
68
202
|
[Parameter(Mandatory)]
|
|
69
|
-
[int] $TestCap
|
|
203
|
+
[int] $TestCap,
|
|
204
|
+
|
|
205
|
+
[AllowNull()]
|
|
206
|
+
[AllowEmptyString()]
|
|
207
|
+
[string] $Root = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent)
|
|
70
208
|
)
|
|
71
209
|
|
|
72
210
|
$state = Get-PythonBatchBudgetState -ProdCap $ProdCap -TestCap $TestCap
|
|
73
211
|
if ($null -ne $InputObject.prodCap) { $state.prodCap = [int]$InputObject.prodCap }
|
|
74
212
|
if ($null -ne $InputObject.testCap) { $state.testCap = [int]$InputObject.testCap }
|
|
75
|
-
|
|
76
|
-
|
|
213
|
+
|
|
214
|
+
# Persisted entries that resolve outside this root belong to another worktree
|
|
215
|
+
# and are dropped, so a state file carried across worktrees cannot spend this
|
|
216
|
+
# worktree's budget.
|
|
217
|
+
if ($null -ne $InputObject.prodFiles) {
|
|
218
|
+
$state.prodFiles = @(@($InputObject.prodFiles) | Where-Object { Test-PythonBatchBudgetPathInRoot -Path $_ -Root $Root })
|
|
219
|
+
}
|
|
220
|
+
if ($null -ne $InputObject.testFiles) {
|
|
221
|
+
$state.testFiles = @(@($InputObject.testFiles) | Where-Object { Test-PythonBatchBudgetPathInRoot -Path $_ -Root $Root })
|
|
222
|
+
}
|
|
77
223
|
|
|
78
224
|
return $state
|
|
79
225
|
}
|
|
@@ -113,7 +259,11 @@ function Invoke-PythonBatchBudgetDecision {
|
|
|
113
259
|
[System.Collections.IDictionary] $State,
|
|
114
260
|
|
|
115
261
|
[Parameter(Mandatory)]
|
|
116
|
-
[string] $StateFile
|
|
262
|
+
[string] $StateFile,
|
|
263
|
+
|
|
264
|
+
[AllowNull()]
|
|
265
|
+
[AllowEmptyString()]
|
|
266
|
+
[string] $Root = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent)
|
|
117
267
|
)
|
|
118
268
|
|
|
119
269
|
$normalized = $FilePath -replace '\\', '/'
|
|
@@ -121,6 +271,13 @@ function Invoke-PythonBatchBudgetDecision {
|
|
|
121
271
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
|
|
122
272
|
}
|
|
123
273
|
|
|
274
|
+
# An out-of-root candidate is discarded rather than denied: it consumes no
|
|
275
|
+
# slot and writes no state, so this hook stays deny-only for real overruns.
|
|
276
|
+
if (-not (Test-PythonBatchBudgetPathInRoot -Path $normalized -Root $Root)) {
|
|
277
|
+
Write-Verbose "Discarding batch-budget candidate '$normalized': it resolves outside the batch-budget root '$Root'."
|
|
278
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
|
|
279
|
+
}
|
|
280
|
+
|
|
124
281
|
$isTestFile = ($normalized -match '(^|/)tests/.*\.py$') -or ($normalized -match '(^|/)test_[^/]+\.py$')
|
|
125
282
|
$targetList = if ($isTestFile) { @($State.testFiles) } else { @($State.prodFiles) }
|
|
126
283
|
$cap = if ($isTestFile) { [int]$State.testCap } else { [int]$State.prodCap }
|
|
@@ -151,10 +308,17 @@ function Invoke-PythonBatchBudgetHook {
|
|
|
151
308
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
152
309
|
param(
|
|
153
310
|
[string] $ToolInputRaw,
|
|
154
|
-
[string] $SessionId = '
|
|
155
|
-
[string] $Root = (
|
|
311
|
+
[string] $SessionId = '',
|
|
312
|
+
[string] $Root = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent),
|
|
156
313
|
[int] $ProdCap = 3,
|
|
157
314
|
[int] $TestCap = 3,
|
|
315
|
+
[scriptblock] $ReadSessionIdFile = {
|
|
316
|
+
param([string] $Path)
|
|
317
|
+
if (Test-Path -LiteralPath $Path -PathType Leaf) {
|
|
318
|
+
return (Get-Content -LiteralPath $Path -Raw)
|
|
319
|
+
}
|
|
320
|
+
return ''
|
|
321
|
+
},
|
|
158
322
|
[scriptblock] $TestPathExists = { param([string] $Path) Test-Path -Path $Path },
|
|
159
323
|
[scriptblock] $EnsureDirectory = { param([string] $Path) New-Item -ItemType Directory -Path $Path -Force | Out-Null },
|
|
160
324
|
[scriptblock] $ReadState = { param([string] $Path) Get-Content -Path $Path -Raw },
|
|
@@ -183,23 +347,32 @@ function Invoke-PythonBatchBudgetHook {
|
|
|
183
347
|
}
|
|
184
348
|
|
|
185
349
|
$stateDir = Join-Path -Path $Root -ChildPath '.claude/state'
|
|
350
|
+
|
|
351
|
+
# Resolved before the directory is ensured, because reading the session-id
|
|
352
|
+
# file must never be what creates the state directory.
|
|
353
|
+
$resolvedSessionId = Get-PythonBatchBudgetSessionId `
|
|
354
|
+
-SessionId $SessionId `
|
|
355
|
+
-Root $Root `
|
|
356
|
+
-SessionIdFilePath (Join-Path -Path $stateDir -ChildPath 'current-session-id') `
|
|
357
|
+
-ReadSessionIdFile $ReadSessionIdFile
|
|
358
|
+
|
|
186
359
|
if (-not (& $TestPathExists $stateDir)) {
|
|
187
360
|
& $EnsureDirectory $stateDir
|
|
188
361
|
}
|
|
189
362
|
|
|
190
|
-
$stateFile = Join-Path -Path $stateDir -ChildPath ("python-batch-budget.$
|
|
363
|
+
$stateFile = Join-Path -Path $stateDir -ChildPath ("python-batch-budget.$resolvedSessionId.json")
|
|
191
364
|
$state = Get-PythonBatchBudgetState -ProdCap $ProdCap -TestCap $TestCap
|
|
192
365
|
|
|
193
366
|
if (& $TestPathExists $stateFile) {
|
|
194
367
|
try {
|
|
195
368
|
$loaded = & $ReadState $stateFile | ConvertFrom-Json -ErrorAction Stop
|
|
196
|
-
$state = ConvertTo-PythonBatchBudgetState -InputObject $loaded -ProdCap $ProdCap -TestCap $TestCap
|
|
369
|
+
$state = ConvertTo-PythonBatchBudgetState -InputObject $loaded -ProdCap $ProdCap -TestCap $TestCap -Root $Root
|
|
197
370
|
} catch {
|
|
198
371
|
Write-Verbose "Ignoring unreadable Python batch-budget state file '$stateFile': $($_.Exception.Message)"
|
|
199
372
|
}
|
|
200
373
|
}
|
|
201
374
|
|
|
202
|
-
$decision = Invoke-PythonBatchBudgetDecision -FilePath $filePath -State $state -StateFile $stateFile
|
|
375
|
+
$decision = Invoke-PythonBatchBudgetDecision -FilePath $filePath -State $state -StateFile $stateFile -Root $Root
|
|
203
376
|
if ($decision.shouldWriteState) {
|
|
204
377
|
try {
|
|
205
378
|
& $WriteState $stateFile $decision.state
|
|
@@ -242,10 +415,10 @@ function Invoke-PythonBatchBudgetEntryPoint {
|
|
|
242
415
|
$ToolInputRaw = [string](& $ReadPayload)
|
|
243
416
|
}
|
|
244
417
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
418
|
+
# No literal fallback here: an empty value routes through the hook's session
|
|
419
|
+
# resolution, which falls back to the session-id state file and then to a
|
|
420
|
+
# worktree-derived identifier.
|
|
421
|
+
$sessionId = [string]$env:CLAUDE_SESSION_ID
|
|
249
422
|
|
|
250
423
|
$prodCap = 3
|
|
251
424
|
$testCap = 3
|
|
@@ -8,13 +8,15 @@
|
|
|
8
8
|
falling back to the CLAUDE_HOOK_INPUT environment variable (the existing
|
|
9
9
|
SubagentStop-hook precedent), and extracts the 'session_id' field.
|
|
10
10
|
|
|
11
|
-
Persistence
|
|
12
|
-
-
|
|
13
|
-
|
|
11
|
+
Persistence channels:
|
|
12
|
+
- The id is always written to .claude/state/current-session-id, creating
|
|
13
|
+
that directory when absent. Publishing it unconditionally is what lets a
|
|
14
|
+
consumer that cannot read the env file, such as a hook running in a
|
|
15
|
+
fresh process, still resolve the session id from disk.
|
|
16
|
+
- When CLAUDE_ENV_FILE is set, the line 'CLAUDE_SESSION_ID=<id>' is
|
|
17
|
+
additionally appended to that file. Variables persisted there are
|
|
14
18
|
exported to subsequent Bash tool commands in the session, which is how
|
|
15
19
|
this hook provisions the otherwise-unset CLAUDE_SESSION_ID variable.
|
|
16
|
-
- When CLAUDE_ENV_FILE is unset, writes the id to
|
|
17
|
-
.claude/state/current-session-id instead.
|
|
18
20
|
|
|
19
21
|
On malformed or empty input (missing/blank payload, unparseable JSON, or an
|
|
20
22
|
absent/blank session_id) the hook performs no write. It always exits 0 so a
|
|
@@ -101,6 +103,15 @@ function Invoke-PersistSessionIdHook {
|
|
|
101
103
|
switch ($decision.action) {
|
|
102
104
|
'env-file' {
|
|
103
105
|
& $AppendLine $decision.path ("CLAUDE_SESSION_ID={0}" -f $decision.sessionId)
|
|
106
|
+
|
|
107
|
+
# The state file is published unconditionally, so a consumer that
|
|
108
|
+
# cannot read the env file (a hook invoked in a fresh process, for
|
|
109
|
+
# example) still resolves the session id from disk.
|
|
110
|
+
$envStateDir = Split-Path -Path $StateFilePath -Parent
|
|
111
|
+
if ($envStateDir) {
|
|
112
|
+
& $EnsureDirectory $envStateDir
|
|
113
|
+
}
|
|
114
|
+
& $WriteStateFile $StateFilePath $decision.sessionId
|
|
104
115
|
}
|
|
105
116
|
'state-file' {
|
|
106
117
|
$stateDir = Split-Path -Path $decision.path -Parent
|