@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
@@ -0,0 +1,140 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Gates epic child and integration PR merges behind checkpointed green state.
4
+ #>
5
+ [CmdletBinding()]
6
+ param()
7
+
8
+ function ConvertFrom-CodexMergeJson {
9
+ [CmdletBinding()]
10
+ param([AllowNull()][AllowEmptyString()][string] $Raw, [Parameter(Mandatory)][string] $Name, [switch] $Optional)
11
+
12
+ if ([string]::IsNullOrWhiteSpace($Raw)) {
13
+ if ($Optional) {
14
+ return $null
15
+ }
16
+ throw "EPIC_MERGE_GATE_BLOCKED: $Name is empty."
17
+ }
18
+ try {
19
+ return $Raw | ConvertFrom-Json -ErrorAction Stop
20
+ } catch {
21
+ if ($Optional) {
22
+ return $null
23
+ }
24
+ throw "EPIC_MERGE_GATE_BLOCKED: $Name is malformed JSON: $_"
25
+ }
26
+ }
27
+
28
+ function Get-CodexMergeCommandPrNumber {
29
+ [CmdletBinding()]
30
+ [OutputType([int])]
31
+ param([Parameter(Mandatory)][string] $Command)
32
+
33
+ if ($Command -match '(?i)\bgh\s+pr\s+merge\s+(\d+)\b') {
34
+ return [int]$Matches[1]
35
+ }
36
+ return $null
37
+ }
38
+
39
+ function Test-CodexChildMergeReady {
40
+ [CmdletBinding()]
41
+ [OutputType([bool])]
42
+ param([AllowNull()] $Checkpoint)
43
+
44
+ if ($null -eq $Checkpoint) {
45
+ return $false
46
+ }
47
+ $properties = @($Checkpoint.PSObject.Properties.Name)
48
+ return (
49
+ $properties -contains 'epic_mode' -and
50
+ $Checkpoint.epic_mode -is [bool] -and
51
+ [bool]$Checkpoint.epic_mode -and
52
+ $properties -contains 'step9_status' -and
53
+ @('passed', 'verified') -contains [string]$Checkpoint.step9_status
54
+ )
55
+ }
56
+
57
+ function Test-CodexEpicMergeReady {
58
+ [CmdletBinding()]
59
+ [OutputType([bool])]
60
+ param([AllowNull()] $Checkpoint, [AllowNull()][Nullable[int]] $CommandPrNumber)
61
+
62
+ if ($null -eq $Checkpoint -or
63
+ @($Checkpoint.PSObject.Properties.Name) -notcontains 'epic_merge_pr' -or
64
+ $null -eq $Checkpoint.epic_merge_pr) {
65
+ return $false
66
+ }
67
+ $mergePr = $Checkpoint.epic_merge_pr
68
+ if (@($mergePr.PSObject.Properties.Name) -notcontains 'ci_gate' -or
69
+ $null -eq $mergePr.ci_gate -or
70
+ [string]$mergePr.ci_gate.conclusion -ne 'success') {
71
+ return $false
72
+ }
73
+ if ($null -ne $CommandPrNumber) {
74
+ $parsed = 0
75
+ if (@($mergePr.PSObject.Properties.Name) -notcontains 'pr_number' -or
76
+ -not [int]::TryParse([string]$mergePr.pr_number, [ref]$parsed) -or
77
+ $parsed -ne $CommandPrNumber) {
78
+ return $false
79
+ }
80
+ }
81
+ return $true
82
+ }
83
+
84
+ function Invoke-CodexEpicMergeDecision {
85
+ [CmdletBinding()]
86
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
87
+ param(
88
+ [Parameter(Mandatory)][string] $PayloadRaw,
89
+ [AllowNull()][AllowEmptyString()][string] $ChildCheckpointRaw,
90
+ [AllowNull()][AllowEmptyString()][string] $EpicCheckpointRaw
91
+ )
92
+
93
+ $payload = ConvertFrom-CodexMergeJson -Raw $PayloadRaw -Name 'PreToolUse input'
94
+ if ([string]$payload.tool_name -ne 'Bash') {
95
+ return $null
96
+ }
97
+ $command = [string]$payload.tool_input.command
98
+ if ($command -notmatch '(?i)\bgh\s+pr\s+merge\b' -or $command -notmatch '(?i)--merge\b') {
99
+ return $null
100
+ }
101
+
102
+ $child = ConvertFrom-CodexMergeJson -Raw $ChildCheckpointRaw -Name 'child checkpoint' -Optional
103
+ if (Test-CodexChildMergeReady -Checkpoint $child) {
104
+ return $null
105
+ }
106
+ $epic = ConvertFrom-CodexMergeJson -Raw $EpicCheckpointRaw -Name 'epic checkpoint' -Optional
107
+ $prNumber = Get-CodexMergeCommandPrNumber -Command $command
108
+ if (Test-CodexEpicMergeReady -Checkpoint $epic -CommandPrNumber $prNumber) {
109
+ return $null
110
+ }
111
+
112
+ return [ordered]@{
113
+ hookSpecificOutput = [ordered]@{
114
+ hookEventName = 'PreToolUse'
115
+ permissionDecision = 'deny'
116
+ permissionDecisionReason = 'EPIC_MERGE_GATE_BLOCKED: gh pr merge --merge requires a safe epic child checkpoint or a successful final epic CI gate with a matching PR number.'
117
+ }
118
+ }
119
+ }
120
+
121
+ if ($MyInvocation.InvocationName -eq '.') {
122
+ return
123
+ }
124
+
125
+ try {
126
+ $payloadRaw = [Console]::In.ReadToEnd()
127
+ $repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
128
+ $childPath = Join-Path $repositoryRoot 'artifacts/orchestration/orchestrator-state.json'
129
+ $epicPath = Join-Path $repositoryRoot 'artifacts/orchestration/epic-orchestrator-state.json'
130
+ $childRaw = if (Test-Path -LiteralPath $childPath -PathType Leaf) { Get-Content -Raw -LiteralPath $childPath } else { '' }
131
+ $epicRaw = if (Test-Path -LiteralPath $epicPath -PathType Leaf) { Get-Content -Raw -LiteralPath $epicPath } else { '' }
132
+ $decision = Invoke-CodexEpicMergeDecision -PayloadRaw $payloadRaw -ChildCheckpointRaw $childRaw -EpicCheckpointRaw $epicRaw
133
+ if ($null -ne $decision) {
134
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
135
+ }
136
+ exit 0
137
+ } catch {
138
+ [Console]::Error.WriteLine([string]$_)
139
+ exit 2
140
+ }
@@ -0,0 +1,292 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Enforces the planning-only boundary for route_id preparation.
4
+
5
+ .DESCRIPTION
6
+ PreToolUse hook for Bash, apply_patch, and drm-copilot MCP tools. When the local canonical
7
+ orchestrator checkpoint selects preparation, only planning artifacts, lifecycle MCP calls,
8
+ read-only inspection, and the preparation commit sequence are permitted.
9
+ #>
10
+ [CmdletBinding()]
11
+ param()
12
+
13
+ $script:AllowedPreparationMcpTools = @(
14
+ 'mcp__drm-copilot__new_potential_entry',
15
+ 'mcp__drm-copilot__new_potential_bug_entry',
16
+ 'mcp__drm-copilot__potential_to_issue',
17
+ 'mcp__drm-copilot__new_active_feature_folder',
18
+ 'mcp__drm-copilot__resolve_atomic_plan_prompt',
19
+ 'mcp__drm-copilot__resolve_execute_hard_lock_prompt',
20
+ 'mcp__drm-copilot__validate_orchestration_artifacts'
21
+ )
22
+
23
+ function ConvertFrom-EpicPlanningJson {
24
+ [CmdletBinding()]
25
+ param(
26
+ [AllowNull()][AllowEmptyString()][string] $Raw,
27
+ [Parameter(Mandatory)][string] $Name,
28
+ [switch] $Optional
29
+ )
30
+
31
+ if ([string]::IsNullOrWhiteSpace($Raw)) {
32
+ if ($Optional) {
33
+ return $null
34
+ }
35
+ throw "EPIC_PLANNING_ONLY_BLOCKED: $Name is empty."
36
+ }
37
+ try {
38
+ return $Raw | ConvertFrom-Json -ErrorAction Stop
39
+ } catch {
40
+ throw "EPIC_PLANNING_ONLY_BLOCKED: $Name is malformed JSON: $_"
41
+ }
42
+ }
43
+
44
+ function Get-EpicPlanningDenyDecision {
45
+ [CmdletBinding()]
46
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
47
+ param([Parameter(Mandatory)][string] $Reason)
48
+
49
+ return [ordered]@{
50
+ hookSpecificOutput = [ordered]@{
51
+ hookEventName = 'PreToolUse'
52
+ permissionDecision = 'deny'
53
+ permissionDecisionReason = "EPIC_PLANNING_ONLY_BLOCKED: $Reason"
54
+ }
55
+ }
56
+ }
57
+
58
+ function Get-EpicPlanningPatchPath {
59
+ [CmdletBinding()]
60
+ [OutputType([string[]])]
61
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Patch)
62
+
63
+ $paths = [System.Collections.Generic.List[string]]::new()
64
+ foreach ($match in [regex]::Matches(
65
+ $Patch,
66
+ '(?m)^\*\*\* (?:(?:Add|Update|Delete) File:|Move to:)\s*(?<path>[^\r\n]+)\r?$'
67
+ )) {
68
+ $paths.Add((([string]$match.Groups['path'].Value) -replace '\\', '/').Trim())
69
+ }
70
+ return [string[]]$paths
71
+ }
72
+
73
+ function Test-EpicPlanningArtifactPath {
74
+ [CmdletBinding()]
75
+ [OutputType([bool])]
76
+ param([Parameter(Mandatory)][string] $Path)
77
+
78
+ $normalized = ($Path -replace '\\', '/').Trim()
79
+ if ([System.IO.Path]::IsPathRooted($Path) -or
80
+ $normalized -match '(^|/)\.\.(?:/|$)' -or
81
+ $normalized -match '^[A-Za-z]:' -or
82
+ $normalized.StartsWith('//')) {
83
+ return $false
84
+ }
85
+ $normalized = $normalized -replace '^\./', ''
86
+ if ($normalized -match '(^|/)docs/features/(?:active|potential)/') {
87
+ return $normalized -match '\.(?:md|json|txt)$'
88
+ }
89
+ if ($normalized -match '(^|/)artifacts/(?:orchestration|research)/') {
90
+ if ($normalized -match '(?i)(?:epic-root-invocation|codex-routing-attestation|codex-worktree-(?:launch|wave))') {
91
+ return $false
92
+ }
93
+ return $normalized -match '\.(?:md|json|txt)$'
94
+ }
95
+ return $false
96
+ }
97
+
98
+ function Test-EpicPlanningBashAllowed {
99
+ [CmdletBinding()]
100
+ [OutputType([bool])]
101
+ param(
102
+ [Parameter(Mandatory)][AllowEmptyString()][string] $Command,
103
+ [AllowNull()][string[]] $StagedPaths,
104
+ [AllowNull()][AllowEmptyString()][string] $CurrentBranch
105
+ )
106
+
107
+ if (-not $Command -or $Command -match '[;\r\n|&<>`]' -or $Command -match '\$\(' -or
108
+ $Command -match '(?i)(?:^|\s)--(?:output|exec|ext-diff|textconv|upload-pack|receive-pack|pre)(?:=|\s|$)') {
109
+ return $false
110
+ }
111
+ $allowedPatterns = @(
112
+ '^\s*git\s+(?:status|diff|log|show|rev-parse|fetch|ls-files)\b',
113
+ '^\s*git\s+worktree\s+list\b',
114
+ '^\s*gh\s+(?:issue|pr)\s+view\b',
115
+ '^\s*(?:rg|Get-Content|Get-ChildItem|Test-Path|Select-String)\b',
116
+ '^\s*(?:poetry\s+run\s+)?(?:python|py)\s+-m\s+scripts\.dev_tools\.(?:validate_[A-Za-z0-9_.-]+|epic_wave_computation)\b'
117
+ )
118
+ foreach ($pattern in $allowedPatterns) {
119
+ if ($Command -match $pattern) {
120
+ return $true
121
+ }
122
+ }
123
+
124
+ if ($Command -match '^\s*git\s+add(?:\s+--)?\s+(?<paths>.+?)\s*$') {
125
+ $tokens = [regex]::Matches($Matches.paths, '"[^"]+"|''[^'']+''|\S+')
126
+ if ($tokens.Count -eq 0) {
127
+ return $false
128
+ }
129
+ foreach ($token in $tokens) {
130
+ $path = ([string]$token.Value).Trim('"', "'")
131
+ if ($path.StartsWith('-') -or -not (Test-EpicPlanningArtifactPath -Path $path)) {
132
+ return $false
133
+ }
134
+ }
135
+ return $true
136
+ }
137
+ $quotedMessage = '(?:"[^"\r\n]+"|''[^''\r\n]+'')'
138
+ $commitPattern = "^\s*git\s+commit\s+(?:-m\s+$quotedMessage|--message(?:=|\s+)$quotedMessage)\s*`$"
139
+ if ($Command -match $commitPattern) {
140
+ if ($null -eq $StagedPaths -or $StagedPaths.Count -eq 0) {
141
+ return $false
142
+ }
143
+ foreach ($path in $StagedPaths) {
144
+ if (-not (Test-EpicPlanningArtifactPath -Path $path)) {
145
+ return $false
146
+ }
147
+ }
148
+ return $true
149
+ }
150
+ if (-not [string]::IsNullOrWhiteSpace($CurrentBranch)) {
151
+ $escapedBranch = [regex]::Escape($CurrentBranch)
152
+ if ($Command -match "^\s*git\s+push(?:\s+(?:-u|--set-upstream))?\s+origin\s+$escapedBranch\s*`$") {
153
+ return $true
154
+ }
155
+ }
156
+ return $false
157
+ }
158
+
159
+ function Invoke-EpicPlanningOnlyDecision {
160
+ [CmdletBinding()]
161
+ param(
162
+ [Parameter(Mandatory)][string] $PayloadRaw,
163
+ [AllowNull()][AllowEmptyString()][string] $CheckpointRaw,
164
+ [AllowNull()][AllowEmptyString()][string] $EpicExecutionContext,
165
+ [AllowNull()][string[]] $StagedPaths,
166
+ [AllowNull()][AllowEmptyString()][string] $CurrentBranch
167
+ )
168
+
169
+ $payload = ConvertFrom-EpicPlanningJson -Raw $PayloadRaw -Name 'PreToolUse input'
170
+ $attestedPreparation = $EpicExecutionContext -eq 'epic_preparation_child'
171
+ if ([string]::IsNullOrWhiteSpace($CheckpointRaw) -and -not $attestedPreparation) {
172
+ return $null
173
+ }
174
+ $checkpoint = if ([string]::IsNullOrWhiteSpace($CheckpointRaw)) {
175
+ [pscustomobject]@{ route_id = 'preparation' }
176
+ } else {
177
+ ConvertFrom-EpicPlanningJson -Raw $CheckpointRaw -Name 'orchestrator checkpoint'
178
+ }
179
+ if ([string]$checkpoint.route_id -ne 'preparation' -and $attestedPreparation) {
180
+ return Get-EpicPlanningDenyDecision -Reason 'an attested preparation child must keep route_id set to preparation.'
181
+ }
182
+ if ([string]$checkpoint.route_id -ne 'preparation') {
183
+ return $null
184
+ }
185
+
186
+ $toolName = [string]$payload.tool_name
187
+ if ($toolName -eq 'apply_patch') {
188
+ $patch = [string]$payload.tool_input.command
189
+ if ($patch -match '(?mi)^\*\*\* Delete File:\s*.*artifacts[\\/]orchestration[\\/]orchestrator-state\.json\s*$') {
190
+ return Get-EpicPlanningDenyDecision -Reason 'the canonical preparation checkpoint may not be deleted.'
191
+ }
192
+ if ($patch -match '(?mi)^\*\*\* Update File:\s*.*artifacts[\\/]orchestration[\\/]orchestrator-state\.json\s*$' -and
193
+ $patch -match '(?mi)^\*\*\* Move to:') {
194
+ return Get-EpicPlanningDenyDecision -Reason 'the canonical preparation checkpoint may not be moved.'
195
+ }
196
+ if ($patch -match '(?m)^-\s*"route_id"\s*:\s*"preparation"') {
197
+ return Get-EpicPlanningDenyDecision -Reason 'the canonical preparation route may not be removed or changed.'
198
+ }
199
+ if ($patch -match '(?mi)^\*\*\* Update File:\s*.*artifacts[\\/]orchestration[\\/]orchestrator-state\.json\s*$' -and
200
+ $patch -match '(?m)^\+[^\r\n]*"route_id"\s*:') {
201
+ return Get-EpicPlanningDenyDecision -Reason 'the canonical preparation checkpoint may not add or duplicate route_id.'
202
+ }
203
+ if ($patch -match '(?m)^\+\s*"step(?:5|6|7|8|9|10)_status"\s*:\s*"(?!not-applicable")') {
204
+ return Get-EpicPlanningDenyDecision -Reason 'execution-through-CI statuses must remain not-applicable during preparation.'
205
+ }
206
+ $paths = @(Get-EpicPlanningPatchPath -Patch $patch)
207
+ if ($paths.Count -eq 0) {
208
+ return Get-EpicPlanningDenyDecision -Reason 'preparation apply_patch input must identify planning artifact paths.'
209
+ }
210
+ foreach ($path in $paths) {
211
+ if (-not (Test-EpicPlanningArtifactPath -Path $path)) {
212
+ return Get-EpicPlanningDenyDecision -Reason "preparation may not edit '$path'; only feature planning documents and orchestration/research artifacts are writable."
213
+ }
214
+ }
215
+ return $null
216
+ }
217
+
218
+ if ($toolName -eq 'Bash') {
219
+ $command = [string]$payload.tool_input.command
220
+ if (Test-EpicPlanningBashAllowed -Command $command -StagedPaths $StagedPaths -CurrentBranch $CurrentBranch) {
221
+ return $null
222
+ }
223
+ return Get-EpicPlanningDenyDecision -Reason 'the Bash command is outside the preparation read, branch, commit, push, or validator allowlist.'
224
+ }
225
+
226
+ if ($toolName -like 'mcp__*' -and $toolName -notlike 'mcp__drm-copilot__*') {
227
+ return Get-EpicPlanningDenyDecision -Reason "MCP tool '$toolName' is not part of the repository-scoped preparation surface."
228
+ }
229
+
230
+ if ($toolName -like 'mcp__drm-copilot__*') {
231
+ if ($attestedPreparation) {
232
+ $workspaceRoot = [string]$payload.tool_input.workspace_root
233
+ if ([string]::IsNullOrWhiteSpace($workspaceRoot) -or
234
+ [string]::IsNullOrWhiteSpace([string]$payload.cwd) -or
235
+ [System.IO.Path]::GetFullPath($workspaceRoot) -ne [System.IO.Path]::GetFullPath([string]$payload.cwd)) {
236
+ return Get-EpicPlanningDenyDecision -Reason 'preparation MCP calls require workspace_root equal to the attested session cwd.'
237
+ }
238
+ }
239
+ if ($script:AllowedPreparationMcpTools -contains $toolName) {
240
+ return $null
241
+ }
242
+ return Get-EpicPlanningDenyDecision -Reason "MCP tool '$toolName' is outside the preparation lifecycle and validator allowlist."
243
+ }
244
+
245
+ return Get-EpicPlanningDenyDecision -Reason "tool '$toolName' is not classified for preparation mode."
246
+ }
247
+
248
+ if ($MyInvocation.InvocationName -eq '.') {
249
+ return
250
+ }
251
+
252
+ try {
253
+ $payloadRaw = [Console]::In.ReadToEnd()
254
+ $repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
255
+ $checkpointPath = Join-Path $repositoryRoot 'artifacts/orchestration/orchestrator-state.json'
256
+ $checkpointRaw = if (Test-Path -LiteralPath $checkpointPath -PathType Leaf) {
257
+ Get-Content -Raw -LiteralPath $checkpointPath
258
+ } else {
259
+ ''
260
+ }
261
+ $payload = ConvertFrom-EpicPlanningJson -Raw $payloadRaw -Name 'PreToolUse input'
262
+ $stagedPaths = $null
263
+ $currentBranch = ''
264
+ if ([string]$payload.tool_name -eq 'Bash' -and
265
+ [string]$payload.tool_input.command -match '^\s*git\s+commit\b') {
266
+ $stagedPaths = @(& git -C $repositoryRoot diff --cached --name-only --diff-filter=ACMR 2>$null)
267
+ if ($LASTEXITCODE -ne 0) {
268
+ throw 'EPIC_PLANNING_ONLY_BLOCKED: staged paths could not be resolved before commit.'
269
+ }
270
+ }
271
+ if ([string]$payload.tool_name -eq 'Bash' -and
272
+ [string]$payload.tool_input.command -match '^\s*git\s+push\b') {
273
+ $currentBranch = [string](& git -C $repositoryRoot branch --show-current 2>$null)
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()
278
+ }
279
+ $decision = Invoke-EpicPlanningOnlyDecision `
280
+ -PayloadRaw $payloadRaw `
281
+ -CheckpointRaw $checkpointRaw `
282
+ -EpicExecutionContext ([string]$env:CODEX_EPIC_CHILD_EXECUTION_CONTEXT) `
283
+ -StagedPaths $stagedPaths `
284
+ -CurrentBranch $currentBranch
285
+ if ($null -ne $decision) {
286
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
287
+ }
288
+ exit 0
289
+ } catch {
290
+ [Console]::Error.WriteLine([string]$_)
291
+ exit 2
292
+ }
@@ -0,0 +1,135 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Denies covered mutations by an epic persona without valid root provenance.
4
+
5
+ .DESCRIPTION
6
+ PreToolUse hook. Reads Codex stdin JSON and the SubagentStart attestation keyed by transcript
7
+ path. A matching epic attestation must carry literal provenance_valid true.
8
+ #>
9
+ [CmdletBinding()]
10
+ param()
11
+
12
+ . (Join-Path $PSScriptRoot 'codex-authority-store.ps1')
13
+
14
+ function ConvertFrom-EpicRootGatePayload {
15
+ [CmdletBinding()]
16
+ param([Parameter(Mandatory)][string] $Raw, [Parameter(Mandatory)][string] $Name)
17
+
18
+ if ([string]::IsNullOrWhiteSpace($Raw)) {
19
+ throw "EPIC_INVOCATION_ORIGIN_BLOCKED: $Name is empty."
20
+ }
21
+ try {
22
+ return $Raw | ConvertFrom-Json -ErrorAction Stop
23
+ } catch {
24
+ throw "EPIC_INVOCATION_ORIGIN_BLOCKED: $Name is malformed JSON: $_"
25
+ }
26
+ }
27
+
28
+ function Get-EpicRootGateAttestationKey {
29
+ [CmdletBinding()]
30
+ [OutputType([string])]
31
+ param([Parameter(Mandatory)][string] $TranscriptPath)
32
+
33
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($TranscriptPath)
34
+ return [Convert]::ToHexString([System.Security.Cryptography.SHA256]::HashData($bytes)).ToLowerInvariant()
35
+ }
36
+
37
+ function Get-EpicRootGateDenyDecision {
38
+ [CmdletBinding()]
39
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
40
+ param([Parameter(Mandatory)][string] $Reason)
41
+
42
+ return [ordered]@{
43
+ hookSpecificOutput = [ordered]@{
44
+ hookEventName = 'PreToolUse'
45
+ permissionDecision = 'deny'
46
+ permissionDecisionReason = $Reason
47
+ }
48
+ }
49
+ }
50
+
51
+ function Invoke-EpicRootInvocationDecision {
52
+ [CmdletBinding()]
53
+ param(
54
+ [Parameter(Mandatory)][string] $PayloadRaw,
55
+ [AllowNull()][AllowEmptyString()][string] $AttestationRaw
56
+ )
57
+
58
+ $payload = ConvertFrom-EpicRootGatePayload -Raw $PayloadRaw -Name 'PreToolUse input'
59
+ if ([string]::IsNullOrWhiteSpace($AttestationRaw)) {
60
+ if (@('epic-planner', 'epic-orchestrator') -contains [string]$payload.agent_type) {
61
+ return Get-EpicRootGateDenyDecision -Reason (
62
+ "EPIC_INVOCATION_ORIGIN_BLOCKED: $($payload.agent_type) has no matching " +
63
+ 'SubagentStart attestation. Relaunch it from a fresh root-session epic invocation.'
64
+ )
65
+ }
66
+ return $null
67
+ }
68
+
69
+ $attestation = ConvertFrom-EpicRootGatePayload -Raw $AttestationRaw -Name 'routing attestation'
70
+ if (@('epic-planner', 'epic-orchestrator') -notcontains [string]$attestation.agent_type) {
71
+ return $null
72
+ }
73
+ $properties = @($attestation.PSObject.Properties.Name)
74
+ if ($properties -contains 'provenance_valid' -and
75
+ $attestation.provenance_valid -is [bool] -and
76
+ [bool]$attestation.provenance_valid) {
77
+ return $null
78
+ }
79
+
80
+ return Get-EpicRootGateDenyDecision -Reason (
81
+ "EPIC_INVOCATION_ORIGIN_BLOCKED: $($attestation.agent_type) was not authorized by a " +
82
+ 'fresh, single-use root-session epic invocation receipt. Invoke epic-plan, epic-run, ' +
83
+ 'or epic-orchestrate from the root session.'
84
+ )
85
+ }
86
+
87
+ if ($MyInvocation.InvocationName -eq '.') {
88
+ return
89
+ }
90
+
91
+ try {
92
+ $payloadRaw = [Console]::In.ReadToEnd()
93
+ $payload = ConvertFrom-EpicRootGatePayload -Raw $payloadRaw -Name 'PreToolUse input'
94
+ $attestationRaw = ''
95
+ $repositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
96
+ $sessionId = [string]$payload.session_id
97
+ $transcriptPath = [string]$payload.transcript_path
98
+ if (-not [string]::IsNullOrWhiteSpace($transcriptPath) -and
99
+ -not [string]::IsNullOrWhiteSpace($sessionId)) {
100
+ $key = Get-EpicRootGateAttestationKey -TranscriptPath $transcriptPath
101
+ $path = Get-CodexAuthorityAttestationPath `
102
+ -RepositoryRoot $repositoryRoot `
103
+ -SessionId $sessionId `
104
+ -AttestationKey $key
105
+ if (Test-Path -LiteralPath $path -PathType Leaf) {
106
+ $attestationRaw = Get-Content -Raw -LiteralPath $path
107
+ }
108
+ }
109
+ if ([string]::IsNullOrWhiteSpace($attestationRaw) -and
110
+ -not [string]::IsNullOrWhiteSpace([string]$payload.agent_id) -and
111
+ -not [string]::IsNullOrWhiteSpace($sessionId)) {
112
+ $stateRoot = Get-CodexAuthorityStateRoot `
113
+ -RepositoryRoot $repositoryRoot `
114
+ -SessionId $sessionId
115
+ foreach ($path in Get-ChildItem -LiteralPath $stateRoot -Filter 'codex-routing-attestation.*.json' -File -ErrorAction SilentlyContinue) {
116
+ $candidate = Get-Content -Raw -LiteralPath $path.FullName
117
+ try {
118
+ if ([string]($candidate | ConvertFrom-Json -ErrorAction Stop).agent_id -eq [string]$payload.agent_id) {
119
+ $attestationRaw = $candidate
120
+ break
121
+ }
122
+ } catch {
123
+ continue
124
+ }
125
+ }
126
+ }
127
+ $decision = Invoke-EpicRootInvocationDecision -PayloadRaw $payloadRaw -AttestationRaw $attestationRaw
128
+ if ($null -ne $decision) {
129
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
130
+ }
131
+ exit 0
132
+ } catch {
133
+ [Console]::Error.WriteLine([string]$_)
134
+ exit 2
135
+ }