@danmoisan/drm-copilot-mcp 1.0.19 → 1.0.21
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 +71 -8
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/atomic-executor.md +2 -2
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +14 -6
- package/resources/claude-customizations/.claude/lib/model-routing/ModelRouting.psm1 +34 -14
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +30 -18
- package/resources/claude-customizations/.claude/rules/general-code-change.md +1 -1
- package/resources/claude-customizations/.claude/rules/general-unit-test.md +2 -2
- package/resources/claude-customizations/.claude/rules/typescript.md +5 -5
- package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +4 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +4 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +4 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +4 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +4 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +4 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/check-powershell-test-purity.ps1 +9 -65
- package/resources/codex-and-agents-customizations/.codex/hooks/check-python-test-purity.ps1 +9 -65
- package/resources/codex-and-agents-customizations/.codex/hooks/codex-pretooluse-file-mapping.ps1 +474 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +20 -101
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +16 -3
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-child-worktree-binding.ps1 +14 -5
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +23 -5
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1 +30 -52
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +28 -27
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +19 -49
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +19 -49
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +19 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +0 -500
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1
CHANGED
|
@@ -44,6 +44,15 @@
|
|
|
44
44
|
[CmdletBinding()]
|
|
45
45
|
param()
|
|
46
46
|
|
|
47
|
+
# Shared Codex PreToolUse transport: stdin payload parsing and tool_input-to-file
|
|
48
|
+
# mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
|
|
49
|
+
. (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
|
|
50
|
+
|
|
51
|
+
# The only path this hook governs. On-disk reconstruction of an apply_patch
|
|
52
|
+
# Update is requested for this path alone, so a patch that merely happens to
|
|
53
|
+
# touch other files can never fail the hook.
|
|
54
|
+
$script:GovernedCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
55
|
+
|
|
47
56
|
$script:CanonicalStepPrefixes = @(
|
|
48
57
|
'S0_startup_checks',
|
|
49
58
|
'S1_change_budget_estimation',
|
|
@@ -300,112 +309,22 @@ function Invoke-CheckpointMonotonicDecision {
|
|
|
300
309
|
}
|
|
301
310
|
|
|
302
311
|
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
303
|
-
function ConvertFrom-CodexCheckpointHookPayload {
|
|
304
|
-
[CmdletBinding()]
|
|
305
|
-
param([Parameter(Mandatory)][string] $PayloadRaw)
|
|
306
|
-
|
|
307
|
-
if ([string]::IsNullOrWhiteSpace($PayloadRaw)) {
|
|
308
|
-
throw 'enforce-checkpoint-monotonic hook input is empty.'
|
|
309
|
-
}
|
|
310
|
-
try {
|
|
311
|
-
$payload = $PayloadRaw | ConvertFrom-Json -ErrorAction Stop
|
|
312
|
-
} catch {
|
|
313
|
-
throw "enforce-checkpoint-monotonic hook input is malformed JSON: $_"
|
|
314
|
-
}
|
|
315
|
-
if ($payload.PSObject.Properties.Name -notcontains 'tool_input' -or $null -eq $payload.tool_input) {
|
|
316
|
-
throw 'enforce-checkpoint-monotonic hook input is missing tool_input.'
|
|
317
|
-
}
|
|
318
|
-
if ([string]$payload.hook_event_name -ne 'PreToolUse' -or [string]$payload.tool_name -ne 'apply_patch') {
|
|
319
|
-
throw 'enforce-checkpoint-monotonic requires a PreToolUse apply_patch payload.'
|
|
320
|
-
}
|
|
321
|
-
return $payload
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function ConvertTo-CodexApplyPatchCheckpointInput {
|
|
325
|
-
[CmdletBinding()]
|
|
326
|
-
[OutputType([object[]])]
|
|
327
|
-
param([Parameter(Mandatory)] $Payload)
|
|
328
|
-
|
|
329
|
-
if ($Payload.tool_input.PSObject.Properties.Name -contains 'file_path') {
|
|
330
|
-
return , $Payload.tool_input
|
|
331
|
-
}
|
|
332
|
-
$command = [string]$Payload.tool_input.command
|
|
333
|
-
if ([string]::IsNullOrWhiteSpace($command)) {
|
|
334
|
-
throw 'enforce-checkpoint-monotonic cannot map tool_input to a file edit.'
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
$fileMatches = [regex]::Matches(
|
|
338
|
-
$command,
|
|
339
|
-
'(?ms)^\*\*\* (?<operation>Add|Update|Delete) File:\s*(?<path>.+?)\r?\n(?<body>.*?)(?=^\*\*\* (?:(?:Add|Update|Delete) File:|End Patch)\s*|\z)'
|
|
340
|
-
)
|
|
341
|
-
if ($fileMatches.Count -eq 0) {
|
|
342
|
-
throw 'enforce-checkpoint-monotonic received an unrecognized apply_patch command.'
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
$inputs = [System.Collections.Generic.List[object]]::new()
|
|
346
|
-
foreach ($match in $fileMatches) {
|
|
347
|
-
$sourcePath = ([string]$match.Groups['path'].Value).Trim()
|
|
348
|
-
$path = $sourcePath
|
|
349
|
-
$operation = [string]$match.Groups['operation'].Value
|
|
350
|
-
$body = ([string]$match.Groups['body'].Value) -replace '\r\n', "`n"
|
|
351
|
-
$moveMatch = [regex]::Match($body, '(?m)^\*\*\* Move to:\s*(?<path>.+?)\s*$')
|
|
352
|
-
if ($moveMatch.Success) {
|
|
353
|
-
$path = ([string]$moveMatch.Groups['path'].Value).Trim()
|
|
354
|
-
}
|
|
355
|
-
if ($operation -eq 'Delete') {
|
|
356
|
-
$content = ''
|
|
357
|
-
} elseif ($operation -eq 'Add') {
|
|
358
|
-
$content = (($body -split "`n") | ForEach-Object {
|
|
359
|
-
if ($_.StartsWith('+') -and -not $_.StartsWith('+++')) {
|
|
360
|
-
$_.Substring(1)
|
|
361
|
-
}
|
|
362
|
-
}) -join "`n"
|
|
363
|
-
} else {
|
|
364
|
-
if (-not (Test-Path -LiteralPath $sourcePath -PathType Leaf)) {
|
|
365
|
-
throw "enforce-checkpoint-monotonic cannot read update source '$sourcePath'."
|
|
366
|
-
}
|
|
367
|
-
$content = (Get-Content -Raw -LiteralPath $sourcePath) -replace '\r\n', "`n"
|
|
368
|
-
$hunks = @([regex]::Split($body, '(?m)^@@[^\r\n]*\r?\n') | Where-Object { $_ -match '\S' })
|
|
369
|
-
foreach ($hunk in $hunks) {
|
|
370
|
-
$oldLines = [System.Collections.Generic.List[string]]::new()
|
|
371
|
-
$newLines = [System.Collections.Generic.List[string]]::new()
|
|
372
|
-
foreach ($line in ($hunk -split "`n")) {
|
|
373
|
-
if ($line -match '^\*\*\* (?:Move to|End of File)') {
|
|
374
|
-
continue
|
|
375
|
-
}
|
|
376
|
-
if ($line.StartsWith('+') -and -not $line.StartsWith('+++')) {
|
|
377
|
-
$newLines.Add($line.Substring(1))
|
|
378
|
-
} elseif ($line.StartsWith('-') -and -not $line.StartsWith('---')) {
|
|
379
|
-
$oldLines.Add($line.Substring(1))
|
|
380
|
-
} elseif ($line.StartsWith(' ')) {
|
|
381
|
-
$oldLines.Add($line.Substring(1))
|
|
382
|
-
$newLines.Add($line.Substring(1))
|
|
383
|
-
} else {
|
|
384
|
-
$oldLines.Add($line)
|
|
385
|
-
$newLines.Add($line)
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
$oldText = $oldLines -join "`n"
|
|
389
|
-
$newText = $newLines -join "`n"
|
|
390
|
-
$index = $content.IndexOf($oldText, [System.StringComparison]::Ordinal)
|
|
391
|
-
if ($index -lt 0) {
|
|
392
|
-
throw "enforce-checkpoint-monotonic could not apply an in-memory patch hunk for '$path'."
|
|
393
|
-
}
|
|
394
|
-
$content = $content.Substring(0, $index) + $newText + $content.Substring($index + $oldText.Length)
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
$inputs.Add([pscustomobject]@{ file_path = $path; content = $content })
|
|
398
|
-
}
|
|
399
|
-
return $inputs.ToArray()
|
|
400
|
-
}
|
|
401
|
-
|
|
402
312
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
403
313
|
return
|
|
404
314
|
}
|
|
405
315
|
|
|
406
316
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
317
|
+
# Transport and mapping come from the shared module. Update reconstruction is
|
|
318
|
+
# requested for the governed checkpoint path only: an Update touching any
|
|
319
|
+
# other file yields no record and therefore allows, instead of failing the
|
|
320
|
+
# whole invocation because an unrelated source could not be read. A governed
|
|
321
|
+
# reconstruction failure yields empty content, which routes into the existing
|
|
322
|
+
# fail-closed deny below rather than exit 2.
|
|
323
|
+
$payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw ([Console]::In.ReadToEnd()) -HookName 'enforce-checkpoint-monotonic'
|
|
324
|
+
$mappedInputs = @(
|
|
325
|
+
ConvertTo-CodexFileEditInput -Payload $payload -ResolveUpdateContent -GovernedPath $script:GovernedCheckpointPath
|
|
326
|
+
)
|
|
327
|
+
foreach ($toolInput in $mappedInputs) {
|
|
409
328
|
$toolInputRaw = $toolInput | ConvertTo-Json -Compress -Depth 20
|
|
410
329
|
$decision = Invoke-CheckpointMonotonicDecision -ToolInputRaw $toolInputRaw
|
|
411
330
|
if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1
CHANGED
|
@@ -41,9 +41,16 @@
|
|
|
41
41
|
[CmdletBinding()]
|
|
42
42
|
param()
|
|
43
43
|
|
|
44
|
-
# Reuse the adjacent hook's
|
|
44
|
+
# Reuse the adjacent hook's shared checkpoint policy logic. This dot-source is by
|
|
45
|
+
# design, not a copy-paste defect, so the hook is not renamed.
|
|
45
46
|
. (Join-Path $PSScriptRoot 'enforce-checkpoint-monotonic.ps1')
|
|
46
47
|
|
|
48
|
+
# Shared Codex PreToolUse transport. Dot-sourced explicitly rather than relying on
|
|
49
|
+
# the transitive load above, so this hook's transport does not depend on its
|
|
50
|
+
# neighbour's internals. Every transport error it raises names this hook, because
|
|
51
|
+
# the shared parser is called with -HookName 'enforce-completion-consistency'.
|
|
52
|
+
. (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
|
|
53
|
+
|
|
47
54
|
# Dot-source the shared validation helpers. Guarded so a missing file produces a
|
|
48
55
|
# clear error and so dot-sourcing this hook in tests loads the helpers too.
|
|
49
56
|
$script:CompletionHelpersPath = Join-Path $PSScriptRoot 'enforce-completion-helpers.ps1'
|
|
@@ -409,8 +416,14 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
409
416
|
}
|
|
410
417
|
|
|
411
418
|
try {
|
|
412
|
-
|
|
413
|
-
|
|
419
|
+
# -HookName makes every transport error name this hook rather than the
|
|
420
|
+
# neighbour it dot-sources. Update reconstruction is scoped to the governed
|
|
421
|
+
# checkpoint path for the same reason as in enforce-checkpoint-monotonic.
|
|
422
|
+
$payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw ([Console]::In.ReadToEnd()) -HookName 'enforce-completion-consistency'
|
|
423
|
+
$mappedInputs = @(
|
|
424
|
+
ConvertTo-CodexFileEditInput -Payload $payload -ResolveUpdateContent -GovernedPath $script:GovernedCheckpointPath
|
|
425
|
+
)
|
|
426
|
+
foreach ($toolInput in $mappedInputs) {
|
|
414
427
|
$toolInputRaw = $toolInput | ConvertTo-Json -Compress -Depth 20
|
|
415
428
|
$decision = Invoke-CompletionConsistencyDecision -ToolInputRaw $toolInputRaw
|
|
416
429
|
if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
|
|
@@ -267,6 +267,18 @@ function Invoke-CodexChildGuardGit {
|
|
|
267
267
|
return & git @GitArgs 2>&1
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
function Get-CodexChildGuardLiveBranch {
|
|
271
|
+
[CmdletBinding()]
|
|
272
|
+
[OutputType([string])]
|
|
273
|
+
param([Parameter(Mandatory)][string] $RepositoryRoot)
|
|
274
|
+
|
|
275
|
+
$liveBranch = [string](Invoke-CodexChildGuardGit -GitArgs @('-C', $RepositoryRoot, 'branch', '--show-current'))
|
|
276
|
+
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($liveBranch)) {
|
|
277
|
+
return ''
|
|
278
|
+
}
|
|
279
|
+
return $liveBranch.Trim()
|
|
280
|
+
}
|
|
281
|
+
|
|
270
282
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
271
283
|
return
|
|
272
284
|
}
|
|
@@ -308,12 +320,9 @@ try {
|
|
|
308
320
|
} else {
|
|
309
321
|
''
|
|
310
322
|
}
|
|
311
|
-
$liveBranch =
|
|
312
|
-
if ($LASTEXITCODE -ne 0) {
|
|
313
|
-
$liveBranch = ''
|
|
314
|
-
}
|
|
323
|
+
$liveBranch = Get-CodexChildGuardLiveBranch -RepositoryRoot $repositoryRoot
|
|
315
324
|
$decision = Invoke-CodexEpicChildGuardDecision -PayloadRaw $payloadRaw -ReceiptRaw $receiptRaw `
|
|
316
|
-
-Attestation $attestation -HookRepositoryRoot $repositoryRoot -LiveBranch $liveBranch
|
|
325
|
+
-Attestation $attestation -HookRepositoryRoot $repositoryRoot -LiveBranch $liveBranch `
|
|
317
326
|
-ActualSpecSha256 $actualSpecSha256 -ActualProfileSha256 $actualProfileSha256
|
|
318
327
|
if ($null -ne $decision) {
|
|
319
328
|
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1
CHANGED
|
@@ -245,6 +245,28 @@ function Invoke-EpicPlanningOnlyDecision {
|
|
|
245
245
|
return Get-EpicPlanningDenyDecision -Reason "tool '$toolName' is not classified for preparation mode."
|
|
246
246
|
}
|
|
247
247
|
|
|
248
|
+
function Invoke-EpicPlanningGit {
|
|
249
|
+
[CmdletBinding()]
|
|
250
|
+
param([Parameter(Mandatory)][string[]] $GitArgs)
|
|
251
|
+
|
|
252
|
+
return & git @GitArgs 2>$null
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function Get-EpicPlanningCurrentBranch {
|
|
256
|
+
[CmdletBinding()]
|
|
257
|
+
[OutputType([string])]
|
|
258
|
+
param([Parameter(Mandatory)][string] $RepositoryRoot)
|
|
259
|
+
|
|
260
|
+
$currentBranch = [string](Invoke-EpicPlanningGit -GitArgs @('-C', $RepositoryRoot, 'branch', '--show-current'))
|
|
261
|
+
if ($LASTEXITCODE -ne 0) {
|
|
262
|
+
throw 'EPIC_PLANNING_ONLY_BLOCKED: current branch could not be resolved before push.'
|
|
263
|
+
}
|
|
264
|
+
if ([string]::IsNullOrWhiteSpace($currentBranch)) {
|
|
265
|
+
return ''
|
|
266
|
+
}
|
|
267
|
+
return $currentBranch.Trim()
|
|
268
|
+
}
|
|
269
|
+
|
|
248
270
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
249
271
|
return
|
|
250
272
|
}
|
|
@@ -270,11 +292,7 @@ try {
|
|
|
270
292
|
}
|
|
271
293
|
if ([string]$payload.tool_name -eq 'Bash' -and
|
|
272
294
|
[string]$payload.tool_input.command -match '^\s*git\s+push\b') {
|
|
273
|
-
$currentBranch =
|
|
274
|
-
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($currentBranch)) {
|
|
275
|
-
throw 'EPIC_PLANNING_ONLY_BLOCKED: current branch could not be resolved before push.'
|
|
276
|
-
}
|
|
277
|
-
$currentBranch = $currentBranch.Trim()
|
|
295
|
+
$currentBranch = Get-EpicPlanningCurrentBranch -RepositoryRoot $repositoryRoot
|
|
278
296
|
}
|
|
279
297
|
$decision = Invoke-EpicPlanningOnlyDecision `
|
|
280
298
|
-PayloadRaw $payloadRaw `
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1
CHANGED
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
|
|
31
31
|
If the file_path resolves to a forbidden prefix, the script writes a PreToolUse JSON
|
|
32
32
|
response to stdout with hookSpecificOutput.permissionDecision = 'deny' and exits with
|
|
33
|
-
code 0 so Codex surfaces the reason.
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
code 0 so Codex surfaces the reason. Allowed paths produce NO stdout at all: the
|
|
34
|
+
script allows silently and exits 0. On hard failure (empty, malformed, or
|
|
35
|
+
tool_input-less stdin) the script writes the reason to stderr and exits 2.
|
|
36
36
|
|
|
37
37
|
.NOTES
|
|
38
38
|
Compatible with PowerShell 7+.
|
|
@@ -41,6 +41,10 @@
|
|
|
41
41
|
[CmdletBinding()]
|
|
42
42
|
param()
|
|
43
43
|
|
|
44
|
+
# Shared Codex PreToolUse transport: stdin payload parsing and tool_input-to-file
|
|
45
|
+
# mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
|
|
46
|
+
. (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
|
|
47
|
+
|
|
44
48
|
function Test-EvidenceLocationForbidden {
|
|
45
49
|
<#
|
|
46
50
|
.SYNOPSIS
|
|
@@ -142,60 +146,34 @@ function Invoke-EvidenceLocationDecision {
|
|
|
142
146
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
143
147
|
}
|
|
144
148
|
|
|
145
|
-
function ConvertFrom-CodexEvidenceLocationPayload {
|
|
146
|
-
[CmdletBinding()]
|
|
147
|
-
param([Parameter(Mandatory)][string] $PayloadRaw)
|
|
148
|
-
|
|
149
|
-
if ([string]::IsNullOrWhiteSpace($PayloadRaw)) {
|
|
150
|
-
throw 'enforce-evidence-locations hook input is empty.'
|
|
151
|
-
}
|
|
152
|
-
try {
|
|
153
|
-
$payload = $PayloadRaw | ConvertFrom-Json -ErrorAction Stop
|
|
154
|
-
} catch {
|
|
155
|
-
throw "enforce-evidence-locations hook input is malformed JSON: $_"
|
|
156
|
-
}
|
|
157
|
-
if ($payload.PSObject.Properties.Name -notcontains 'tool_input' -or $null -eq $payload.tool_input) {
|
|
158
|
-
throw 'enforce-evidence-locations hook input is missing tool_input.'
|
|
159
|
-
}
|
|
160
|
-
if ([string]$payload.hook_event_name -ne 'PreToolUse' -or [string]$payload.tool_name -ne 'apply_patch') {
|
|
161
|
-
throw 'enforce-evidence-locations requires a PreToolUse apply_patch payload.'
|
|
162
|
-
}
|
|
163
|
-
return $payload
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function Get-CodexEvidenceLocationPath {
|
|
167
|
-
[CmdletBinding()]
|
|
168
|
-
[OutputType([string])]
|
|
169
|
-
param([Parameter(Mandatory)] $Payload)
|
|
170
|
-
|
|
171
|
-
if ($Payload.tool_input.PSObject.Properties.Name -contains 'file_path') {
|
|
172
|
-
return [string]$Payload.tool_input.file_path
|
|
173
|
-
}
|
|
174
|
-
$command = [string]$Payload.tool_input.command
|
|
175
|
-
if ([string]::IsNullOrWhiteSpace($command)) {
|
|
176
|
-
throw 'enforce-evidence-locations cannot map tool_input to a file edit.'
|
|
177
|
-
}
|
|
178
|
-
[string[]] $paths = @(
|
|
179
|
-
[regex]::Matches($command, '(?m)^\*\*\* (?:(?:Add|Update|Delete) File|Move to):\s*(?<path>.+?)\s*$') |
|
|
180
|
-
ForEach-Object { ([string]$_.Groups['path'].Value).Trim() }
|
|
181
|
-
)
|
|
182
|
-
$paths = @($paths | Select-Object -Unique)
|
|
183
|
-
if ($paths.Count -eq 0) {
|
|
184
|
-
throw 'enforce-evidence-locations received an unrecognized apply_patch command.'
|
|
185
|
-
}
|
|
186
|
-
foreach ($path in $paths) {
|
|
187
|
-
Write-Output ([string]$path)
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
149
|
function Invoke-EvidenceLocationEntryPoint {
|
|
150
|
+
<#
|
|
151
|
+
.SYNOPSIS
|
|
152
|
+
Reads one Codex PreToolUse payload and returns the process exit code.
|
|
153
|
+
.PARAMETER PayloadRaw
|
|
154
|
+
The raw stdin text. AllowEmptyString is required so empty input reaches
|
|
155
|
+
the shared parser and fails closed with exit 2, rather than raising a
|
|
156
|
+
parameter-binding error that would skip the exit and allow silently.
|
|
157
|
+
#>
|
|
192
158
|
[CmdletBinding()]
|
|
193
159
|
[OutputType([int])]
|
|
194
|
-
param([Parameter(Mandatory)][string] $PayloadRaw)
|
|
160
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $PayloadRaw)
|
|
195
161
|
|
|
196
162
|
try {
|
|
197
|
-
$payload = ConvertFrom-
|
|
198
|
-
|
|
163
|
+
$payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw $PayloadRaw -HookName 'enforce-evidence-locations'
|
|
164
|
+
|
|
165
|
+
# Both sides of a rename are evaluated, matching the pre-fix path scan
|
|
166
|
+
# that collected Add/Update/Delete targets and Move destinations alike.
|
|
167
|
+
# A well-formed payload that maps to no file yields no paths, so the loop
|
|
168
|
+
# body never runs and the hook allows silently.
|
|
169
|
+
$paths = @(
|
|
170
|
+
@(ConvertTo-CodexFileEditInput -Payload $payload) |
|
|
171
|
+
ForEach-Object { $_.source_path; $_.file_path } |
|
|
172
|
+
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
|
173
|
+
Select-Object -Unique
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
foreach ($path in $paths) {
|
|
199
177
|
$toolInputRaw = @{ file_path = $path } | ConvertTo-Json -Compress
|
|
200
178
|
$decision = Invoke-EvidenceLocationDecision -ToolInputRaw $toolInputRaw
|
|
201
179
|
if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
[CmdletBinding()]
|
|
7
7
|
param()
|
|
8
8
|
|
|
9
|
+
# Shared Codex PreToolUse transport: stdin payload parsing and tool_input-to-file
|
|
10
|
+
# mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
|
|
11
|
+
. (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
|
|
12
|
+
|
|
9
13
|
$script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
10
14
|
|
|
11
15
|
function ConvertFrom-CheckpointJson {
|
|
@@ -224,38 +228,35 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
224
228
|
return Get-OrchestrationPreimplementationGateBlockDecision -Reason 'PREIMPLEMENTATION_GATE_BLOCKED: Implementation operations require artifacts/orchestration/orchestrator-state.json to contain issue number, feature folder, route metadata, lifecycle readiness, and checkpoint state before implementation begins.'
|
|
225
229
|
}
|
|
226
230
|
|
|
227
|
-
function ConvertFrom-CodexPreimplementationHookPayload {
|
|
228
|
-
[CmdletBinding()]
|
|
229
|
-
param([Parameter(Mandatory)][string] $PayloadRaw)
|
|
230
|
-
|
|
231
|
-
if ([string]::IsNullOrWhiteSpace($PayloadRaw)) {
|
|
232
|
-
throw 'enforce-orchestration-preimplementation-gate hook input is empty.'
|
|
233
|
-
}
|
|
234
|
-
try {
|
|
235
|
-
$payload = $PayloadRaw | ConvertFrom-Json -ErrorAction Stop
|
|
236
|
-
} catch {
|
|
237
|
-
throw "enforce-orchestration-preimplementation-gate hook input is malformed JSON: $_"
|
|
238
|
-
}
|
|
239
|
-
if ($payload.PSObject.Properties.Name -notcontains 'tool_input' -or $null -eq $payload.tool_input) {
|
|
240
|
-
throw 'enforce-orchestration-preimplementation-gate hook input is missing tool_input.'
|
|
241
|
-
}
|
|
242
|
-
if ([string]$payload.hook_event_name -ne 'PreToolUse' -or
|
|
243
|
-
@('Bash', 'apply_patch') -notcontains [string]$payload.tool_name) {
|
|
244
|
-
throw 'enforce-orchestration-preimplementation-gate requires a supported PreToolUse payload.'
|
|
245
|
-
}
|
|
246
|
-
return $payload
|
|
247
|
-
}
|
|
248
|
-
|
|
249
231
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
250
232
|
return
|
|
251
233
|
}
|
|
252
234
|
|
|
253
235
|
try {
|
|
254
|
-
$payload = ConvertFrom-
|
|
255
|
-
$
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
236
|
+
$payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw ([Console]::In.ReadToEnd()) -HookName 'enforce-orchestration-preimplementation-gate'
|
|
237
|
+
$toolName = [string]$payload.tool_name
|
|
238
|
+
|
|
239
|
+
# Bash and apply_patch take the pre-fix path unchanged: the raw tool_input is
|
|
240
|
+
# serialized and evaluated by the untouched decision function, so every
|
|
241
|
+
# allow/deny outcome those two tool names produce today is preserved exactly.
|
|
242
|
+
if (@('Bash', 'apply_patch') -contains $toolName) {
|
|
243
|
+
$decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw ($payload.tool_input | ConvertTo-Json -Compress -Depth 20)
|
|
244
|
+
if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
|
|
245
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
246
|
+
}
|
|
247
|
+
exit 0
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
# Edit and Write map to file paths through the shared module; each mapped
|
|
251
|
+
# path enters the same untouched Test-ImplementationPath decision flow. Any
|
|
252
|
+
# other well-formed tool name maps to no records, so the hook allows.
|
|
253
|
+
foreach ($toolInput in @(ConvertTo-CodexFileEditInput -Payload $payload)) {
|
|
254
|
+
$mappedRaw = @{ file_path = [string]$toolInput.file_path } | ConvertTo-Json -Compress
|
|
255
|
+
$decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $mappedRaw
|
|
256
|
+
if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
|
|
257
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
258
|
+
exit 0
|
|
259
|
+
}
|
|
259
260
|
}
|
|
260
261
|
exit 0
|
|
261
262
|
} catch {
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1
CHANGED
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
[CmdletBinding()]
|
|
37
37
|
param()
|
|
38
38
|
|
|
39
|
+
# Shared Codex PreToolUse transport: stdin payload parsing and tool_input-to-file
|
|
40
|
+
# mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
|
|
41
|
+
. (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
|
|
42
|
+
|
|
39
43
|
function Get-PowerShellBatchBudgetState {
|
|
40
44
|
[CmdletBinding()]
|
|
41
45
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -212,65 +216,31 @@ function Invoke-PowerShellBatchBudgetHook {
|
|
|
212
216
|
return $decision
|
|
213
217
|
}
|
|
214
218
|
|
|
215
|
-
function ConvertFrom-CodexPowerShellBudgetPayload {
|
|
216
|
-
[CmdletBinding()]
|
|
217
|
-
param([Parameter(Mandatory)][string] $PayloadRaw)
|
|
218
|
-
|
|
219
|
-
if ([string]::IsNullOrWhiteSpace($PayloadRaw)) {
|
|
220
|
-
throw 'enforce-powershell-batch-budget hook input is empty.'
|
|
221
|
-
}
|
|
222
|
-
try {
|
|
223
|
-
$payload = $PayloadRaw | ConvertFrom-Json -ErrorAction Stop
|
|
224
|
-
} catch {
|
|
225
|
-
throw "enforce-powershell-batch-budget hook input is malformed JSON: $_"
|
|
226
|
-
}
|
|
227
|
-
if ($payload.PSObject.Properties.Name -notcontains 'tool_input' -or $null -eq $payload.tool_input) {
|
|
228
|
-
throw 'enforce-powershell-batch-budget hook input is missing tool_input.'
|
|
229
|
-
}
|
|
230
|
-
if ([string]$payload.hook_event_name -ne 'PreToolUse' -or [string]$payload.tool_name -ne 'apply_patch') {
|
|
231
|
-
throw 'enforce-powershell-batch-budget requires a PreToolUse apply_patch payload.'
|
|
232
|
-
}
|
|
233
|
-
if ([string]::IsNullOrWhiteSpace([string]$payload.session_id)) {
|
|
234
|
-
throw 'enforce-powershell-batch-budget hook input is missing session_id.'
|
|
235
|
-
}
|
|
236
|
-
return $payload
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
function Get-CodexPowerShellBudgetPath {
|
|
240
|
-
[CmdletBinding()]
|
|
241
|
-
[OutputType([string])]
|
|
242
|
-
param([Parameter(Mandatory)] $Payload)
|
|
243
|
-
|
|
244
|
-
if ($Payload.tool_input.PSObject.Properties.Name -contains 'file_path') {
|
|
245
|
-
return [string]$Payload.tool_input.file_path
|
|
246
|
-
}
|
|
247
|
-
$command = [string]$Payload.tool_input.command
|
|
248
|
-
if ([string]::IsNullOrWhiteSpace($command)) {
|
|
249
|
-
throw 'enforce-powershell-batch-budget cannot map tool_input to a file edit.'
|
|
250
|
-
}
|
|
251
|
-
[string[]] $paths = @(
|
|
252
|
-
[regex]::Matches($command, '(?m)^\*\*\* (?:(?:Add|Update|Delete) File|Move to):\s*(?<path>.+?)\s*$') |
|
|
253
|
-
ForEach-Object { ([string]$_.Groups['path'].Value).Trim() }
|
|
254
|
-
)
|
|
255
|
-
$paths = @($paths | Select-Object -Unique)
|
|
256
|
-
if ($paths.Count -eq 0) {
|
|
257
|
-
throw 'enforce-powershell-batch-budget received an unrecognized apply_patch command.'
|
|
258
|
-
}
|
|
259
|
-
return $paths
|
|
260
|
-
}
|
|
261
|
-
|
|
262
219
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
263
220
|
return
|
|
264
221
|
}
|
|
265
222
|
|
|
266
223
|
try {
|
|
267
|
-
|
|
224
|
+
# Transport and mapping come from the shared module. session_id is still
|
|
225
|
+
# required because the batch counter is keyed by it.
|
|
226
|
+
$payload = ConvertFrom-CodexPreToolUsePayload -PayloadRaw ([Console]::In.ReadToEnd()) -HookName 'enforce-powershell-batch-budget' -RequireSessionId
|
|
268
227
|
$sessionId = ([string]$payload.session_id) -replace '[^A-Za-z0-9._-]', '_'
|
|
269
228
|
$repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
270
229
|
$prodCap = 3
|
|
271
230
|
$testCap = 3
|
|
272
231
|
|
|
273
|
-
|
|
232
|
+
# Both sides of a rename consume budget, matching the pre-fix path scan that
|
|
233
|
+
# collected Add/Update/Delete targets and Move destinations alike. Ordering
|
|
234
|
+
# and de-duplication are preserved. A well-formed payload that maps to no
|
|
235
|
+
# file yields no paths, so no state is written and the hook allows silently.
|
|
236
|
+
$budgetPaths = @(
|
|
237
|
+
@(ConvertTo-CodexFileEditInput -Payload $payload) |
|
|
238
|
+
ForEach-Object { $_.source_path; $_.file_path } |
|
|
239
|
+
Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
|
|
240
|
+
Select-Object -Unique
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
foreach ($path in $budgetPaths) {
|
|
274
244
|
$toolInputRaw = @{ file_path = $path } | ConvertTo-Json -Compress
|
|
275
245
|
$decision = Invoke-PowerShellBatchBudgetHook -ToolInputRaw $toolInputRaw -SessionId $sessionId -Root $repositoryRoot -ProdCap $prodCap -TestCap $testCap
|
|
276
246
|
if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1
CHANGED
|
@@ -34,6 +34,10 @@
|
|
|
34
34
|
[CmdletBinding()]
|
|
35
35
|
param()
|
|
36
36
|
|
|
37
|
+
# Shared Codex PreToolUse transport: stdin payload parsing and tool_input-to-file
|
|
38
|
+
# mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
|
|
39
|
+
. (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1')
|
|
40
|
+
|
|
37
41
|
function Get-PythonBatchBudgetState {
|
|
38
42
|
[CmdletBinding()]
|
|
39
43
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -210,65 +214,31 @@ function Invoke-PythonBatchBudgetHook {
|
|
|
210
214
|
return $decision
|
|
211
215
|
}
|
|
212
216
|
|
|
213
|
-
function ConvertFrom-CodexPythonBudgetPayload {
|
|
214
|
-
[CmdletBinding()]
|
|
215
|
-
param([Parameter(Mandatory)][string] $PayloadRaw)
|
|
216
|
-
|
|
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
|
|
235
|
-
}
|
|
236
|
-
|
|
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
|
|
258
|
-
}
|
|
259
|
-
|
|
260
217
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
261
218
|
return
|
|
262
219
|
}
|
|
263
220
|
|
|
264
221
|
try {
|
|
265
|
-
|
|
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
|
|
266
225
|
$sessionId = ([string]$payload.session_id) -replace '[^A-Za-z0-9._-]', '_'
|
|
267
226
|
$repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
268
227
|
$prodCap = 3
|
|
269
228
|
$testCap = 3
|
|
270
229
|
|
|
271
|
-
|
|
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
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
foreach ($path in $budgetPaths) {
|
|
272
242
|
$toolInputRaw = @{ file_path = $path } | ConvertTo-Json -Compress
|
|
273
243
|
$decision = Invoke-PythonBatchBudgetHook -ToolInputRaw $toolInputRaw -SessionId $sessionId -Root $repositoryRoot -ProdCap $prodCap -TestCap $testCap
|
|
274
244
|
if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
".codex/hooks/codex-agent-profile-attestation.ps1",
|
|
30
30
|
".codex/hooks/codex-authority-store.ps1",
|
|
31
31
|
".codex/hooks/codex-epic-child-launch-attestation.ps1",
|
|
32
|
+
".codex/hooks/codex-pretooluse-file-mapping.ps1",
|
|
32
33
|
".codex/hooks/enforce-codex-model-routing.ps1",
|
|
33
34
|
".codex/hooks/enforce-epic-child-worktree-binding.ps1",
|
|
34
35
|
".codex/hooks/enforce-epic-merge-gate.ps1",
|
|
@@ -95,6 +95,25 @@
|
|
|
95
95
|
# entry path hosts the Pester run in the global session state; measured here so
|
|
96
96
|
# the fix produces real per-file changed-line coverage evidence.
|
|
97
97
|
'scripts/powershell/PoshQC/PoshQC.Testing.psm1'
|
|
98
|
+
# Issue #415 remediation cycle 1 (R1): the PreToolUse hook transport fix added the
|
|
99
|
+
# shared payload-parsing module and rewired seven Codex hooks to consume it. Those
|
|
100
|
+
# production files were absent from this list, so the changed surface was outside
|
|
101
|
+
# the coverage denominator. Measured here so the changed production surface is not
|
|
102
|
+
# excluded from coverage.
|
|
103
|
+
'.codex/hooks/codex-pretooluse-file-mapping.ps1'
|
|
104
|
+
'.codex/hooks/check-python-test-purity.ps1'
|
|
105
|
+
'.codex/hooks/check-powershell-test-purity.ps1'
|
|
106
|
+
'.codex/hooks/enforce-python-batch-budget.ps1'
|
|
107
|
+
'.codex/hooks/enforce-powershell-batch-budget.ps1'
|
|
108
|
+
'.codex/hooks/enforce-evidence-locations.ps1'
|
|
109
|
+
'.codex/hooks/enforce-checkpoint-monotonic.ps1'
|
|
110
|
+
'.codex/hooks/enforce-orchestration-preimplementation-gate.ps1'
|
|
111
|
+
# Issue #415 remediation cycle 2 (R-COV): the detached-HEAD null-guard fix changed
|
|
112
|
+
# these two Codex PreToolUse hooks. Both were absent from this list, so the changed
|
|
113
|
+
# production surface was outside the coverage denominator. Measured here so the
|
|
114
|
+
# changed production surface is not excluded from coverage.
|
|
115
|
+
'.codex/hooks/enforce-epic-child-worktree-binding.ps1'
|
|
116
|
+
'.codex/hooks/enforce-epic-planning-only.ps1'
|
|
98
117
|
)
|
|
99
118
|
# Optional: don't fail the run on coverage percentage
|
|
100
119
|
CoveragePercentTarget = 0
|