@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.
Files changed (45) hide show
  1. package/out/mcp-server.js +73 -1
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +6 -5
  4. package/resources/claude-customizations/.claude/agents/parallel-planner.md +24 -7
  5. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +8 -5
  6. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +186 -13
  7. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +186 -13
  8. package/resources/claude-customizations/.claude/hooks/persist-session-id.ps1 +16 -5
  9. package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +495 -0
  10. package/resources/claude-customizations/.claude/lib/bash/report-lane-assertion.sh +169 -0
  11. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +7 -5
  12. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +4 -2
  13. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +4 -2
  14. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +2 -0
  15. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +5 -3
  16. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +2 -0
  17. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +6 -4
  18. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +2 -0
  19. package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +2 -0
  20. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +16 -16
  21. package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +2 -0
  22. package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +2 -0
  23. package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +3 -1
  24. package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +2 -0
  25. package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +5 -3
  26. package/resources/claude-customizations/.claude/lib/model-routing/ModelRouting.psm1 +2 -0
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +11 -0
  28. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCheckpointValue.psm1 +2 -0
  29. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexModelReceipts.psm1 +4 -2
  30. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCodexTopologyReceipts.psm1 +4 -2
  31. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1 +9 -7
  32. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateCompletionChecks.psm1 +4 -2
  33. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateModelReceipts.psm1 +4 -2
  34. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +3 -1
  35. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +4 -2
  36. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingMatrix.psm1 +3 -1
  37. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateUnconditional.psm1 +7 -5
  38. package/resources/claude-customizations/.claude/lib/requirements/GeneratedDocumentCounters.psm1 +12 -0
  39. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +4 -4
  40. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +12 -9
  41. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +3 -3
  42. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +8 -3
  43. package/resources/claude-customizations/config/blast-radius.json +2 -1
  44. package/resources/claude-customizations/pack-manifests/core.json +2 -0
  45. 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
- if ($null -ne $InputObject.prodFiles) { $state.prodFiles = @($InputObject.prodFiles) }
76
- if ($null -ne $InputObject.testFiles) { $state.testFiles = @($InputObject.testFiles) }
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 = 'default',
155
- [string] $Root = (Get-Location).Path,
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.$SessionId.json")
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
- $sessionId = $env:CLAUDE_SESSION_ID
246
- if (-not $sessionId) {
247
- $sessionId = 'default'
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 channel:
12
- - When CLAUDE_ENV_FILE is set, appends the line
13
- 'CLAUDE_SESSION_ID=<id>' to that file. Variables persisted there are
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