@danmoisan/drm-copilot-mcp 1.0.13 → 1.0.15

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 (131) hide show
  1. package/out/mcp-server.js +3160 -458
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +24 -0
  4. package/resources/claude-customizations/.claude/agents/epic-planner.md +121 -0
  5. package/resources/claude-customizations/.claude/agents/orchestrator.md +2 -2
  6. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +246 -0
  7. package/resources/claude-customizations/.claude/settings.json +9 -2
  8. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +7 -0
  9. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +189 -0
  10. package/resources/claude-customizations/.claude/skills/epic-run/SKILL.md +38 -0
  11. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +21 -2
  12. package/resources/claude-customizations/pack-manifests/core.json +4 -0
  13. package/resources/codex-and-agents-customizations/.agents/skills/architecture-boundaries/SKILL.md +0 -1
  14. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +100 -0
  15. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/agents/openai.yaml +4 -0
  16. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +170 -0
  17. package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +230 -0
  18. package/resources/codex-and-agents-customizations/.agents/skills/epic-run/SKILL.md +39 -0
  19. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +0 -1
  20. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +0 -1
  21. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +89 -9
  22. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +39 -4
  23. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +0 -1
  24. package/resources/codex-and-agents-customizations/.agents/skills/tonality/SKILL.md +0 -1
  25. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +153 -0
  26. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +153 -0
  27. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +153 -0
  28. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +153 -0
  29. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +153 -0
  30. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +2 -0
  31. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +95 -0
  32. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +95 -0
  33. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +95 -0
  34. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +95 -0
  35. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +95 -0
  36. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +2 -0
  37. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +99 -0
  38. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +99 -0
  39. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +99 -0
  40. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +99 -0
  41. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +99 -0
  42. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +2 -0
  43. package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +96 -0
  44. package/resources/codex-and-agents-customizations/.codex/agents/epic-planner.toml +97 -0
  45. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +62 -0
  46. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +62 -0
  47. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +62 -0
  48. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +62 -0
  49. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +62 -0
  50. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +2 -0
  51. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +200 -0
  52. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +200 -0
  53. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +200 -0
  54. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +200 -0
  55. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +200 -0
  56. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +53 -3
  57. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +110 -0
  58. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +110 -0
  59. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +110 -0
  60. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +110 -0
  61. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +110 -0
  62. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +2 -0
  63. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c1.toml +28 -0
  64. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c2.toml +28 -0
  65. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c3-elevated.toml +28 -0
  66. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c3.toml +28 -0
  67. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c4.toml +28 -0
  68. package/resources/codex-and-agents-customizations/.codex/agents/pr-author.toml +2 -0
  69. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c1.toml +55 -0
  70. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c2.toml +55 -0
  71. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c3-elevated.toml +55 -0
  72. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c3.toml +55 -0
  73. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c4.toml +55 -0
  74. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature.toml +2 -0
  75. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +102 -0
  76. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +102 -0
  77. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +102 -0
  78. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +102 -0
  79. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +102 -0
  80. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +2 -0
  81. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c1.toml +105 -0
  82. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c2.toml +105 -0
  83. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c3-elevated.toml +105 -0
  84. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c3.toml +105 -0
  85. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c4.toml +105 -0
  86. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher.toml +2 -0
  87. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c1.toml +50 -0
  88. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c2.toml +50 -0
  89. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c3-elevated.toml +50 -0
  90. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c3.toml +50 -0
  91. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c4.toml +50 -0
  92. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer.toml +2 -0
  93. package/resources/codex-and-agents-customizations/.codex/config.toml +202 -37
  94. package/resources/codex-and-agents-customizations/.codex/hooks/authorize-root-epic-invocation.ps1 +236 -0
  95. package/resources/codex-and-agents-customizations/.codex/hooks/check-powershell-test-purity.ps1 +178 -69
  96. package/resources/codex-and-agents-customizations/.codex/hooks/check-python-test-purity.ps1 +92 -19
  97. package/resources/codex-and-agents-customizations/.codex/hooks/codex-agent-profile-attestation.ps1 +199 -0
  98. package/resources/codex-and-agents-customizations/.codex/hooks/codex-authority-store.ps1 +182 -0
  99. package/resources/codex-and-agents-customizations/.codex/hooks/codex-epic-child-launch-attestation.ps1 +129 -0
  100. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +148 -31
  101. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +198 -0
  102. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +28 -19
  103. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-child-worktree-binding.ps1 +325 -0
  104. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +140 -0
  105. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +292 -0
  106. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-root-invocation.ps1 +135 -0
  107. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-wave-barrier.ps1 +295 -0
  108. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +151 -0
  109. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1 +99 -34
  110. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +143 -27
  111. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +88 -43
  112. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +65 -20
  113. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +88 -42
  114. package/resources/codex-and-agents-customizations/.codex/hooks/record-subagent-routing-attestation.ps1 +414 -0
  115. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +164 -51
  116. package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +154 -0
  117. package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +58 -23
  118. package/resources/codex-and-agents-customizations/AGENTS.md +27 -0
  119. package/resources/codex-and-agents-customizations/pack-manifests/core.json +66 -1
  120. package/resources/codex-and-agents-customizations/pack-manifests/csharp-legacy.json +6 -1
  121. package/resources/codex-and-agents-customizations/pack-manifests/csharp-modern.json +6 -1
  122. package/resources/codex-and-agents-customizations/pack-manifests/powershell.json +6 -1
  123. package/resources/codex-and-agents-customizations/pack-manifests/python.json +6 -1
  124. package/resources/codex-and-agents-customizations/pack-manifests/typescript.json +6 -1
  125. package/resources/config/orchestration-routing.json +167 -0
  126. package/resources/powershell/PoshQC/PoshQC.Analyzer.psm1 +1 -1
  127. package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +125 -0
  128. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +14 -2
  129. package/resources/powershell/PoshQC/PoshQC.psd1 +0 -4
  130. package/resources/powershell/PoshQC/PoshQC.psm1 +26 -6
  131. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +34 -18
@@ -1,19 +1,17 @@
1
- # Converted hook
2
- # Review the generated hook behavior before enabling it.
3
1
 
4
2
  <#
5
3
  .SYNOPSIS
6
4
  Pre-tool-use hook that enforces the Python per-batch change budget.
7
5
 
8
6
  .DESCRIPTION
9
- This script is invoked by the Claude Code PreToolUse hook before any Write or Edit
7
+ This script is invoked by the Codex PreToolUse hook before any Write or Edit
10
8
  operation. When the target file is a Python file, it classifies the file as either
11
9
  production or test and checks the running count against the per-batch cap:
12
10
  - 3 production .py files per batch
13
11
  - 3 test .py files per batch
14
12
 
15
- A "batch" is scoped to the current Claude Code session. The running count is
16
- persisted under .claude/state/python-batch-budget.<session_id>.json. Only distinct
13
+ A "batch" is scoped to the current Codex session. The running count is
14
+ persisted under .codex/state/python-batch-budget.<session_id>.json. Only distinct
17
15
  file paths are counted; repeated edits to the same file consume one slot.
18
16
 
19
17
  Test files are those matching:
@@ -22,14 +20,13 @@
22
20
 
23
21
  All other .py files are treated as production files. Non-Python paths pass through.
24
22
 
25
- The cap may be overridden per session by setting the environment variable
26
- CLAUDE_PYTHON_BUDGET_PROD or CLAUDE_PYTHON_BUDGET_TEST to a positive integer before
27
- the session starts, or by writing {"prodCap": N, "testCap": M} into the state file.
23
+ An approved per-session override is recorded by writing
24
+ {"prodCap": N, "testCap": M} into the Codex state file.
28
25
 
29
- When the cap would be exceeded by a new file, the script emits a JSON response with
30
- 'decision': 'block' and exits 0. The session must explicitly reset the counter by
31
- deleting the state file before starting a new batch. Files already counted are
32
- always allowed through.
26
+ When the cap would be exceeded by a new file, the script emits a PreToolUse JSON
27
+ response with hookSpecificOutput.permissionDecision = 'deny' and exits 0. The session
28
+ must explicitly reset the counter by deleting the state file before starting a new
29
+ batch. Files already counted are always allowed through.
33
30
 
34
31
  .NOTES
35
32
  Compatible with PowerShell 7+.
@@ -90,8 +87,11 @@ function Get-PythonBatchBudgetBlockDecision {
90
87
  )
91
88
 
92
89
  $decision = [ordered]@{
93
- decision = 'block'
94
- reason = $Reason
90
+ hookSpecificOutput = [ordered]@{
91
+ hookEventName = 'PreToolUse'
92
+ permissionDecision = 'deny'
93
+ permissionDecisionReason = $Reason
94
+ }
95
95
  }
96
96
  if ($State) {
97
97
  $decision.state = $State
@@ -116,7 +116,7 @@ function Invoke-PythonBatchBudgetDecision {
116
116
 
117
117
  $normalized = $FilePath -replace '\\', '/'
118
118
  if ($normalized -notmatch '\.py$') {
119
- return [ordered]@{ decision = 'allow'; state = $State; shouldWriteState = $false }
119
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
120
120
  }
121
121
 
122
122
  $isTestFile = ($normalized -match '(^|/)tests/.*\.py$') -or ($normalized -match '(^|/)test_[^/]+\.py$')
@@ -125,13 +125,12 @@ function Invoke-PythonBatchBudgetDecision {
125
125
  $kind = if ($isTestFile) { 'test' } else { 'production' }
126
126
 
127
127
  if ($targetList -contains $normalized) {
128
- return [ordered]@{ decision = 'allow'; state = $State; shouldWriteState = $false }
128
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
129
129
  }
130
130
 
131
131
  if ($targetList.Count -ge $cap) {
132
132
  $currentFiles = ($targetList -join ', ')
133
- $kindUpper = $kind.ToUpperInvariant()
134
- $reason = "Python per-batch budget exceeded: $kind file cap is $cap and is already full ($currentFiles). Requested new file: $normalized. Split the work into a new batch, raise the cap via CLAUDE_PYTHON_BUDGET_$kindUpper environment variable with approved scope, or reset the batch by deleting $StateFile."
133
+ $reason = "Python per-batch budget exceeded: $kind file cap is $cap and is already full ($currentFiles). Requested new file: $normalized. Split the work into a new batch, record an approved cap in $StateFile, or reset the batch by deleting that state file."
135
134
  return Get-PythonBatchBudgetBlockDecision -Reason $reason -State $State
136
135
  }
137
136
 
@@ -141,7 +140,7 @@ function Invoke-PythonBatchBudgetDecision {
141
140
  $State.prodFiles = @($State.prodFiles) + @($normalized)
142
141
  }
143
142
 
144
- return [ordered]@{ decision = 'allow'; state = $State; shouldWriteState = $true }
143
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $true }
145
144
  }
146
145
 
147
146
  function Invoke-PythonBatchBudgetHook {
@@ -163,26 +162,26 @@ function Invoke-PythonBatchBudgetHook {
163
162
  )
164
163
 
165
164
  if (-not $ToolInputRaw) {
166
- return [ordered]@{ decision = 'allow' }
165
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
167
166
  }
168
167
 
169
168
  try {
170
169
  $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
171
170
  } catch {
172
- return Get-PythonBatchBudgetBlockDecision -Reason 'Python batch-budget hook received malformed JSON in CLAUDE_TOOL_INPUT.'
171
+ return Get-PythonBatchBudgetBlockDecision -Reason 'Python batch-budget hook received malformed JSON in Codex tool_input.'
173
172
  }
174
173
 
175
174
  $filePath = $toolInput.file_path
176
175
  if (-not $filePath) {
177
- return [ordered]@{ decision = 'allow' }
176
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
178
177
  }
179
178
 
180
179
  $normalized = $filePath -replace '\\', '/'
181
180
  if ($normalized -notmatch '\.py$') {
182
- return [ordered]@{ decision = 'allow' }
181
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
183
182
  }
184
183
 
185
- $stateDir = Join-Path -Path $Root -ChildPath '.claude/state'
184
+ $stateDir = Join-Path -Path $Root -ChildPath '.codex/state'
186
185
  if (-not (& $TestPathExists $stateDir)) {
187
186
  & $EnsureDirectory $stateDir
188
187
  }
@@ -211,28 +210,75 @@ function Invoke-PythonBatchBudgetHook {
211
210
  return $decision
212
211
  }
213
212
 
214
- if ($MyInvocation.InvocationName -eq '.') {
215
- return
216
- }
213
+ function ConvertFrom-CodexPythonBudgetPayload {
214
+ [CmdletBinding()]
215
+ param([Parameter(Mandatory)][string] $PayloadRaw)
217
216
 
218
- $sessionId = $env:CLAUDE_SESSION_ID
219
- if (-not $sessionId) {
220
- $sessionId = 'default'
217
+ if ([string]::IsNullOrWhiteSpace($PayloadRaw)) {
218
+ throw 'enforce-python-batch-budget hook input is empty.'
219
+ }
220
+ try {
221
+ $payload = $PayloadRaw | ConvertFrom-Json -ErrorAction Stop
222
+ } catch {
223
+ throw "enforce-python-batch-budget hook input is malformed JSON: $_"
224
+ }
225
+ if ($payload.PSObject.Properties.Name -notcontains 'tool_input' -or $null -eq $payload.tool_input) {
226
+ throw 'enforce-python-batch-budget hook input is missing tool_input.'
227
+ }
228
+ if ([string]$payload.hook_event_name -ne 'PreToolUse' -or [string]$payload.tool_name -ne 'apply_patch') {
229
+ throw 'enforce-python-batch-budget requires a PreToolUse apply_patch payload.'
230
+ }
231
+ if ([string]::IsNullOrWhiteSpace([string]$payload.session_id)) {
232
+ throw 'enforce-python-batch-budget hook input is missing session_id.'
233
+ }
234
+ return $payload
221
235
  }
222
236
 
223
- $prodCap = 3
224
- $testCap = 3
225
- if ($env:CLAUDE_PYTHON_BUDGET_PROD -match '^\d+$') {
226
- $prodCap = [int]$env:CLAUDE_PYTHON_BUDGET_PROD
227
- }
228
- if ($env:CLAUDE_PYTHON_BUDGET_TEST -match '^\d+$') {
229
- $testCap = [int]$env:CLAUDE_PYTHON_BUDGET_TEST
237
+ function Get-CodexPythonBudgetPath {
238
+ [CmdletBinding()]
239
+ [OutputType([string])]
240
+ param([Parameter(Mandatory)] $Payload)
241
+
242
+ if ($Payload.tool_input.PSObject.Properties.Name -contains 'file_path') {
243
+ return [string]$Payload.tool_input.file_path
244
+ }
245
+ $command = [string]$Payload.tool_input.command
246
+ if ([string]::IsNullOrWhiteSpace($command)) {
247
+ throw 'enforce-python-batch-budget cannot map tool_input to a file edit.'
248
+ }
249
+ [string[]] $paths = @(
250
+ [regex]::Matches($command, '(?m)^\*\*\* (?:(?:Add|Update|Delete) File|Move to):\s*(?<path>.+?)\s*$') |
251
+ ForEach-Object { ([string]$_.Groups['path'].Value).Trim() }
252
+ )
253
+ $paths = @($paths | Select-Object -Unique)
254
+ if ($paths.Count -eq 0) {
255
+ throw 'enforce-python-batch-budget received an unrecognized apply_patch command.'
256
+ }
257
+ return $paths
230
258
  }
231
259
 
232
- $decision = Invoke-PythonBatchBudgetHook -ToolInputRaw $env:CLAUDE_TOOL_INPUT -SessionId $sessionId -ProdCap $prodCap -TestCap $testCap
233
- if ($decision.decision -eq 'block') {
234
- $decision.Remove('state')
235
- $decision | ConvertTo-Json -Compress | Write-Output
260
+ if ($MyInvocation.InvocationName -eq '.') {
261
+ return
236
262
  }
237
263
 
238
- exit 0
264
+ try {
265
+ $payload = ConvertFrom-CodexPythonBudgetPayload -PayloadRaw ([Console]::In.ReadToEnd())
266
+ $sessionId = ([string]$payload.session_id) -replace '[^A-Za-z0-9._-]', '_'
267
+ $repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
268
+ $prodCap = 3
269
+ $testCap = 3
270
+
271
+ foreach ($path in @(Get-CodexPythonBudgetPath -Payload $payload)) {
272
+ $toolInputRaw = @{ file_path = $path } | ConvertTo-Json -Compress
273
+ $decision = Invoke-PythonBatchBudgetHook -ToolInputRaw $toolInputRaw -SessionId $sessionId -Root $repositoryRoot -ProdCap $prodCap -TestCap $testCap
274
+ if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
275
+ $decision.Remove('state')
276
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
277
+ exit 0
278
+ }
279
+ }
280
+ exit 0
281
+ } catch {
282
+ [Console]::Error.WriteLine([string]$_)
283
+ exit 2
284
+ }
@@ -0,0 +1,414 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Records actual Codex subagent identity, model, routing, and epic-root provenance.
4
+
5
+ .DESCRIPTION
6
+ SubagentStart hook. The hook cannot prevent startup, so it writes an attestation consumed by
7
+ mutation and stop hooks. Epic personas require a matching single-use root receipt. Routed
8
+ worker profiles require a matching model-routing receipt.
9
+ #>
10
+ [CmdletBinding()]
11
+ param()
12
+
13
+ . (Join-Path $PSScriptRoot 'codex-authority-store.ps1')
14
+ . (Join-Path $PSScriptRoot 'codex-agent-profile-attestation.ps1')
15
+ . (Join-Path $PSScriptRoot 'codex-epic-child-launch-attestation.ps1')
16
+ $script:EpicPersonas = @('epic-planner', 'epic-orchestrator')
17
+
18
+ function ConvertFrom-SubagentAttestationJson {
19
+ [CmdletBinding()]
20
+ param(
21
+ [AllowNull()][AllowEmptyString()][string] $Raw,
22
+ [Parameter(Mandatory)][string] $Name,
23
+ [switch] $Optional
24
+ )
25
+
26
+ if ([string]::IsNullOrWhiteSpace($Raw)) {
27
+ if ($Optional) {
28
+ return $null
29
+ }
30
+ throw "MODEL_ROUTING_ATTESTATION_BLOCKED: $Name is empty."
31
+ }
32
+ try {
33
+ return $Raw | ConvertFrom-Json -ErrorAction Stop
34
+ } catch {
35
+ if ($Optional) {
36
+ return $null
37
+ }
38
+ throw "MODEL_ROUTING_ATTESTATION_BLOCKED: $Name is malformed JSON: $_"
39
+ }
40
+ }
41
+
42
+ function Get-SubagentAttestationKey {
43
+ [CmdletBinding()]
44
+ [OutputType([string])]
45
+ param(
46
+ [AllowNull()][string] $TranscriptPath,
47
+ [Parameter(Mandatory)][string] $AgentId
48
+ )
49
+
50
+ $identity = if ([string]::IsNullOrWhiteSpace($TranscriptPath)) { $AgentId } else { $TranscriptPath }
51
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($identity)
52
+ return [Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant()
53
+ }
54
+
55
+ function Test-RootEpicReceipt {
56
+ [CmdletBinding()]
57
+ [OutputType([bool])]
58
+ param(
59
+ [AllowNull()] $Receipt,
60
+ [Parameter(Mandatory)] $Payload,
61
+ [Parameter(Mandatory)][string] $RepositoryRoot,
62
+ [Parameter(Mandatory)][string] $CurrentHeadSha,
63
+ [Parameter(Mandatory)][datetimeoffset] $Now
64
+ )
65
+
66
+ if ($null -eq $Receipt) {
67
+ return $false
68
+ }
69
+ $properties = @($Receipt.PSObject.Properties.Name)
70
+ $expectedProperties = @(
71
+ 'schema_version', 'repository_root', 'repository_sha256', 'repository_head_sha',
72
+ 'session_id', 'turn_id', 'prompt_sha256', 'requested_persona', 'entry_kind',
73
+ 'epic_reference', 'kickoff_path', 'created_at', 'expires_at', 'consumed',
74
+ 'consumed_by', 'consumed_at'
75
+ )
76
+ if (@($properties | Where-Object { $expectedProperties -notcontains $_ }).Count -gt 0 -or
77
+ @($expectedProperties | Where-Object { $properties -notcontains $_ }).Count -gt 0) {
78
+ return $false
79
+ }
80
+ $canonicalRoot = Get-CodexCanonicalAuthorityPath -Path $RepositoryRoot
81
+ $repositoryKey = Get-CodexAuthorityRepositoryKey -RepositoryRoot $RepositoryRoot
82
+ if ($Receipt.schema_version -isnot [ValueType] -or [int]$Receipt.schema_version -ne 1 -or
83
+ [string]$Receipt.repository_root -cne $canonicalRoot -or
84
+ [string]$Receipt.repository_sha256 -ne $repositoryKey -or
85
+ [string]$Receipt.repository_head_sha -ne $CurrentHeadSha -or
86
+ [string]$Receipt.repository_head_sha -notmatch '^[0-9a-fA-F]{40,64}$' -or
87
+ [string]$Receipt.session_id -ne [string]$Payload.session_id -or
88
+ [string]$Receipt.turn_id -ne [string]$Payload.turn_id -or
89
+ [string]$Receipt.requested_persona -ne [string]$Payload.agent_type -or
90
+ [string]$Receipt.prompt_sha256 -notmatch '^[0-9a-f]{64}$' -or
91
+ $Receipt.consumed -isnot [bool] -or [bool]$Receipt.consumed) {
92
+ return $false
93
+ }
94
+ if ($null -ne $Receipt.consumed_by -or $null -ne $Receipt.consumed_at) {
95
+ return $false
96
+ }
97
+ $entryKind = [string]$Receipt.entry_kind
98
+ $entryMatchesPersona = switch ([string]$Receipt.requested_persona) {
99
+ 'epic-planner' { $entryKind -in @('epic-plan', 'direct') }
100
+ 'epic-orchestrator' { $entryKind -in @('epic-run', 'epic-orchestrate', 'direct') }
101
+ default { $false }
102
+ }
103
+ if (-not $entryMatchesPersona) {
104
+ return $false
105
+ }
106
+ $reference = [string]$Receipt.epic_reference
107
+ $kickoffPath = [string]$Receipt.kickoff_path
108
+ if ($entryKind -in @('epic-plan', 'epic-run', 'epic-orchestrate') -and
109
+ [string]::IsNullOrWhiteSpace($reference)) {
110
+ return $false
111
+ }
112
+ if ($entryKind -eq 'epic-run') {
113
+ $referenceParts = @(
114
+ ($reference -replace '\\', '/').Trim('/').Split('/') |
115
+ Where-Object { $_ }
116
+ )
117
+ $slug = if ($referenceParts.Count -ge 2 -and
118
+ $referenceParts[-1] -in @('epic.md', 'epic-kickoff.md')) {
119
+ [string]$referenceParts[-2]
120
+ } elseif ($referenceParts.Count -gt 0) {
121
+ [string]$referenceParts[-1]
122
+ } else {
123
+ ''
124
+ }
125
+ if (-not $slug -or $kickoffPath -ne "docs/features/epics/$slug/epic-kickoff.md") {
126
+ return $false
127
+ }
128
+ } elseif (-not [string]::IsNullOrWhiteSpace($kickoffPath)) {
129
+ return $false
130
+ }
131
+ $created = [datetimeoffset]::MinValue
132
+ $expiry = [datetimeoffset]::MinValue
133
+ if (-not [datetimeoffset]::TryParse([string]$Receipt.created_at, [ref]$created) -or
134
+ -not [datetimeoffset]::TryParse([string]$Receipt.expires_at, [ref]$expiry) -or
135
+ $created -gt $Now.AddMinutes(1) -or
136
+ $expiry -le $created -or
137
+ $expiry -gt $created.AddMinutes(60)) {
138
+ return $false
139
+ }
140
+ return $expiry -gt $Now
141
+ }
142
+
143
+ function Get-CodexSubagentAttestation {
144
+ [CmdletBinding()]
145
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
146
+ param(
147
+ [Parameter(Mandatory)] $Payload,
148
+ [AllowNull()] $RootReceipt,
149
+ [Parameter(Mandatory)][AllowEmptyCollection()][object[]] $Checkpoints,
150
+ [Parameter(Mandatory)][string] $RepositoryRoot,
151
+ [Parameter(Mandatory)][string] $CurrentHeadSha,
152
+ [Parameter(Mandatory)][datetimeoffset] $Now
153
+ )
154
+
155
+ $agentId = [string]$Payload.agent_id
156
+ $agentType = [string]$Payload.agent_type
157
+ $model = [string]$Payload.model
158
+ if (-not $agentId -or -not $agentType -or -not $model) {
159
+ throw 'MODEL_ROUTING_ATTESTATION_BLOCKED: SubagentStart requires agent_id, agent_type, and model.'
160
+ }
161
+ $isEpic = $script:EpicPersonas -contains $agentType
162
+ $rootAuthorized = if ($isEpic) {
163
+ Test-RootEpicReceipt `
164
+ -Receipt $RootReceipt `
165
+ -Payload $Payload `
166
+ -RepositoryRoot $RepositoryRoot `
167
+ -CurrentHeadSha $CurrentHeadSha `
168
+ -Now $Now
169
+ } else {
170
+ $null
171
+ }
172
+
173
+ $routingReceipt = $null
174
+ $expectedModel = ''
175
+ $expectedReasoningEffort = ''
176
+ $agentProfile = $null
177
+ $profileValidationError = $null
178
+ $routingValid = $true
179
+ if ($isEpic) {
180
+ $expectedModel = 'gpt-5.6-sol'
181
+ $expectedReasoningEffort = 'ultra'
182
+ } elseif (Test-CodexRoutedAgentType -AgentType $agentType) {
183
+ $routingReceipt = Find-CodexModelRoutingReceipt -AgentType $agentType -Checkpoints $Checkpoints
184
+ if ($null -eq $routingReceipt) {
185
+ $routingValid = $false
186
+ } else {
187
+ $receiptProperties = @($routingReceipt.PSObject.Properties.Name)
188
+ if ($receiptProperties -contains 'model' -and
189
+ $receiptProperties -contains 'model_reasoning_effort') {
190
+ $expectedModel = [string]$routingReceipt.model
191
+ $expectedReasoningEffort = [string]$routingReceipt.model_reasoning_effort
192
+ }
193
+ }
194
+ }
195
+ if ($isEpic -or (Test-CodexRoutedAgentType -AgentType $agentType)) {
196
+ try {
197
+ $agentProfile = Get-CodexAgentProfileAttestation `
198
+ -RepositoryRoot $RepositoryRoot `
199
+ -AgentType $agentType
200
+ $routingValid = $routingValid -and (Test-CodexAgentProfileBinding `
201
+ -AgentProfile $agentProfile `
202
+ -AgentType $agentType `
203
+ -ActualModel $model `
204
+ -ExpectedModel $expectedModel `
205
+ -ExpectedReasoningEffort $expectedReasoningEffort)
206
+ } catch {
207
+ $routingValid = $false
208
+ $profileValidationError = [string]$_
209
+ }
210
+ }
211
+ $launchAuthorityValid = Test-CodexEpicChildRoutingLaunchAuthority -RoutingReceipt $routingReceipt -Payload $Payload -RepositoryRoot $RepositoryRoot
212
+ $routingValid = $routingValid -and $launchAuthorityValid
213
+
214
+ $provenanceValid = -not $isEpic -or [bool]$rootAuthorized
215
+ return [ordered]@{
216
+ schema_version = 2
217
+ session_id = [string]$Payload.session_id
218
+ turn_id = [string]$Payload.turn_id
219
+ agent_id = $agentId
220
+ agent_type = $agentType
221
+ transcript_path = [string]$Payload.transcript_path
222
+ attestation_key = Get-SubagentAttestationKey -TranscriptPath ([string]$Payload.transcript_path) -AgentId $agentId
223
+ actual_model = $model
224
+ expected_model = $expectedModel
225
+ actual_reasoning_effort = if ($null -ne $agentProfile) { [string]$agentProfile.profile_reasoning_effort } else { $null }
226
+ expected_reasoning_effort = $expectedReasoningEffort
227
+ profile_name = if ($null -ne $agentProfile) { [string]$agentProfile.profile_name } else { $null }
228
+ profile_model = if ($null -ne $agentProfile) { [string]$agentProfile.profile_model } else { $null }
229
+ profile_path = if ($null -ne $agentProfile) { [string]$agentProfile.profile_path } else { $null }
230
+ profile_sha256 = if ($null -ne $agentProfile) { [string]$agentProfile.profile_sha256 } else { $null }
231
+ profile_validation_error = $profileValidationError
232
+ routing_valid = $routingValid
233
+ launch_authority_valid = $launchAuthorityValid
234
+ root_authorized = $rootAuthorized
235
+ root_entry_kind = if ($rootAuthorized) { [string]$RootReceipt.entry_kind } else { $null }
236
+ root_epic_reference = if ($rootAuthorized) { [string]$RootReceipt.epic_reference } else { $null }
237
+ root_kickoff_path = if ($rootAuthorized) { [string]$RootReceipt.kickoff_path } else { $null }
238
+ authority_repository_sha256 = Get-CodexAuthorityRepositoryKey -RepositoryRoot $RepositoryRoot
239
+ provenance_valid = $provenanceValid
240
+ enforcement_marker = if ($provenanceValid) { $null } else { 'EPIC_INVOCATION_ORIGIN_BLOCKED' }
241
+ recorded_at = $Now.ToUniversalTime().ToString('o')
242
+ }
243
+ }
244
+
245
+ function Write-CodexSubagentAttestation {
246
+ [CmdletBinding()]
247
+ param(
248
+ [Parameter(Mandatory)][string] $RepositoryRoot,
249
+ [Parameter(Mandatory)][System.Collections.IDictionary] $Attestation
250
+ )
251
+
252
+ $directory = Get-CodexAuthorityStateRoot `
253
+ -RepositoryRoot $RepositoryRoot `
254
+ -SessionId ([string]$Attestation.session_id)
255
+ [System.IO.Directory]::CreateDirectory($directory) | Out-Null
256
+ $path = Get-CodexAuthorityAttestationPath `
257
+ -RepositoryRoot $RepositoryRoot `
258
+ -SessionId ([string]$Attestation.session_id) `
259
+ -AttestationKey ([string]$Attestation.attestation_key)
260
+ $stream = [System.IO.File]::Open(
261
+ $path,
262
+ [System.IO.FileMode]::CreateNew,
263
+ [System.IO.FileAccess]::Write,
264
+ [System.IO.FileShare]::None
265
+ )
266
+ try {
267
+ $writer = [System.IO.StreamWriter]::new(
268
+ $stream,
269
+ [System.Text.UTF8Encoding]::new($false)
270
+ )
271
+ try {
272
+ $writer.Write(($Attestation | ConvertTo-Json -Depth 8))
273
+ $writer.Flush()
274
+ } finally {
275
+ $writer.Dispose()
276
+ }
277
+ } finally {
278
+ $stream.Dispose()
279
+ }
280
+ return $path
281
+ }
282
+
283
+ function Get-CodexSubagentAttestationFromAuthority {
284
+ [CmdletBinding()]
285
+ param(
286
+ [Parameter(Mandatory)] $Payload,
287
+ [Parameter(Mandatory)][string] $ReceiptPath,
288
+ [Parameter(Mandatory)][AllowEmptyCollection()][object[]] $Checkpoints,
289
+ [Parameter(Mandatory)][string] $RepositoryRoot,
290
+ [Parameter(Mandatory)][string] $CurrentHeadSha,
291
+ [Parameter(Mandatory)][datetimeoffset] $Now
292
+ )
293
+
294
+ $isEpic = $script:EpicPersonas -contains [string]$Payload.agent_type
295
+ if (-not $isEpic -or -not (Test-Path -LiteralPath $ReceiptPath -PathType Leaf)) {
296
+ return Get-CodexSubagentAttestation `
297
+ -Payload $Payload `
298
+ -RootReceipt $null `
299
+ -Checkpoints $Checkpoints `
300
+ -RepositoryRoot $RepositoryRoot `
301
+ -CurrentHeadSha $CurrentHeadSha `
302
+ -Now $Now
303
+ }
304
+
305
+ $stream = [System.IO.File]::Open(
306
+ $ReceiptPath,
307
+ [System.IO.FileMode]::Open,
308
+ [System.IO.FileAccess]::ReadWrite,
309
+ [System.IO.FileShare]::None
310
+ )
311
+ try {
312
+ $reader = [System.IO.StreamReader]::new(
313
+ $stream,
314
+ [System.Text.Encoding]::UTF8,
315
+ $true,
316
+ 1024,
317
+ $true
318
+ )
319
+ try {
320
+ $raw = $reader.ReadToEnd()
321
+ } finally {
322
+ $reader.Dispose()
323
+ }
324
+ $receipt = ConvertFrom-SubagentAttestationJson -Raw $raw -Name 'root receipt'
325
+ $attestation = Get-CodexSubagentAttestation `
326
+ -Payload $Payload `
327
+ -RootReceipt $receipt `
328
+ -Checkpoints $Checkpoints `
329
+ -RepositoryRoot $RepositoryRoot `
330
+ -CurrentHeadSha $CurrentHeadSha `
331
+ -Now $Now
332
+ if ($attestation.root_authorized -eq $true) {
333
+ $receipt.consumed = $true
334
+ $receipt.consumed_by = [string]$Payload.agent_id
335
+ $receipt.consumed_at = $Now.ToUniversalTime().ToString('o')
336
+ $stream.Position = 0
337
+ $stream.SetLength(0)
338
+ $writer = [System.IO.StreamWriter]::new(
339
+ $stream,
340
+ [System.Text.UTF8Encoding]::new($false),
341
+ 1024,
342
+ $true
343
+ )
344
+ try {
345
+ $writer.Write(($receipt | ConvertTo-Json -Depth 8))
346
+ $writer.Flush()
347
+ $stream.Flush($true)
348
+ } finally {
349
+ $writer.Dispose()
350
+ }
351
+ }
352
+ return $attestation
353
+ } finally {
354
+ $stream.Dispose()
355
+ }
356
+ }
357
+
358
+ if ($MyInvocation.InvocationName -eq '.') {
359
+ return
360
+ }
361
+
362
+ try {
363
+ $payload = ConvertFrom-SubagentAttestationJson -Raw ([Console]::In.ReadToEnd()) -Name 'SubagentStart input'
364
+ $repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
365
+ $stateRoot = Join-Path $repositoryRoot 'artifacts/orchestration'
366
+ $receiptPath = Get-CodexAuthorityReceiptPath `
367
+ -RepositoryRoot $repositoryRoot `
368
+ -SessionId ([string]$payload.session_id) `
369
+ -TurnId ([string]$payload.turn_id)
370
+ $checkpoints = @()
371
+ foreach ($name in @('epic-planner-state.json', 'epic-orchestrator-state.json', 'orchestrator-state.json')) {
372
+ $path = Join-Path $stateRoot $name
373
+ if (Test-Path -LiteralPath $path -PathType Leaf) {
374
+ $parsed = ConvertFrom-SubagentAttestationJson -Raw (Get-Content -Raw -LiteralPath $path) -Name $name -Optional
375
+ if ($null -ne $parsed) {
376
+ $checkpoints += $parsed
377
+ }
378
+ }
379
+ }
380
+ $now = [datetimeoffset]::UtcNow
381
+ $headSha = [string](& git -C $repositoryRoot rev-parse HEAD 2>$null)
382
+ if ($LASTEXITCODE -ne 0 -or $headSha -notmatch '^[0-9a-fA-F]{40,64}$') {
383
+ throw 'EPIC_INVOCATION_ORIGIN_BLOCKED: repository HEAD could not be resolved.'
384
+ }
385
+ $attestation = Get-CodexSubagentAttestationFromAuthority `
386
+ -Payload $payload `
387
+ -ReceiptPath $receiptPath `
388
+ -Checkpoints $checkpoints `
389
+ -RepositoryRoot $repositoryRoot `
390
+ -CurrentHeadSha $headSha `
391
+ -Now $now
392
+ $attestationPath = Write-CodexSubagentAttestation -RepositoryRoot $repositoryRoot -Attestation $attestation
393
+ if (-not $attestation.provenance_valid) {
394
+ [ordered]@{
395
+ systemMessage = "EPIC_INVOCATION_ORIGIN_BLOCKED: $($payload.agent_type) requires a fresh root-session epic invocation receipt."
396
+ hookSpecificOutput = [ordered]@{
397
+ hookEventName = 'SubagentStart'
398
+ additionalContext = 'Do not mutate state. Report EPIC_INVOCATION_ORIGIN_BLOCKED and stop.'
399
+ }
400
+ } | ConvertTo-Json -Compress -Depth 5 | Write-Output
401
+ } elseif (-not $attestation.routing_valid) {
402
+ [ordered]@{
403
+ systemMessage = "MODEL_ROUTING_ATTESTATION_BLOCKED: actual model/profile does not match the persisted routing receipt. Attestation: $attestationPath"
404
+ hookSpecificOutput = [ordered]@{
405
+ hookEventName = 'SubagentStart'
406
+ additionalContext = 'Do not mutate state until the parent corrects the deployment profile and routing receipt.'
407
+ }
408
+ } | ConvertTo-Json -Compress -Depth 5 | Write-Output
409
+ }
410
+ exit 0
411
+ } catch {
412
+ [Console]::Error.WriteLine([string]$_)
413
+ exit 2
414
+ }