@danmoisan/drm-copilot-mcp 0.0.5 → 1.0.0

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 (96) hide show
  1. package/out/mcp-server.js +10268 -380
  2. package/package.json +2 -2
  3. package/resources/claude-customizations/.claude/agents/orchestrator.md +6 -2
  4. package/resources/claude-customizations/.claude/agents/pr-author.md +78 -0
  5. package/resources/claude-customizations/.claude/agents/task-researcher.md +10 -6
  6. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +68 -10
  7. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +148 -11
  8. package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +163 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +40 -14
  10. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +198 -0
  11. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +153 -10
  12. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +68 -1
  13. package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +136 -0
  14. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +22 -6
  15. package/resources/claude-customizations/.claude/settings.json +22 -0
  16. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +0 -1
  17. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +76 -2
  18. package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +5 -3
  19. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +69 -0
  20. package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +96 -0
  21. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +77 -0
  22. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +64 -0
  23. package/resources/claude-customizations/pack-manifests/core.json +65 -0
  24. package/resources/claude-customizations/pack-manifests/csharp-legacy.json +11 -0
  25. package/resources/claude-customizations/pack-manifests/csharp-modern.json +12 -0
  26. package/resources/claude-customizations/pack-manifests/powershell.json +14 -0
  27. package/resources/claude-customizations/pack-manifests/python.json +14 -0
  28. package/resources/claude-customizations/pack-manifests/typescript.json +9 -0
  29. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +30 -13
  30. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +113 -10
  31. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +33 -8
  32. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +12 -7
  33. package/resources/codex-and-agents-customizations/.codex/config.toml +65 -6
  34. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +303 -0
  35. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +300 -0
  36. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +148 -0
  37. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +336 -0
  38. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +83 -17
  39. package/resources/config/orchestration-routing.json +5 -13
  40. package/resources/customizations/.github/agents/pr-author.agent.md +25 -0
  41. package/resources/customizations/.github/agents/task-researcher.agent.md +4 -4
  42. package/resources/customizations/.github/prompts/fillout-prd-feature.prompt.md +1 -1
  43. package/resources/customizations/.github/prompts/research-issue.prompt.md +4 -3
  44. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +0 -5
  45. package/resources/scripts/dev_tools/__init__.py +0 -0
  46. package/resources/scripts/dev_tools/_orchestrator_state_human_interaction.py +0 -127
  47. package/resources/scripts/dev_tools/_orchestrator_state_routing.py +0 -216
  48. package/resources/scripts/dev_tools/agentic_sync.py +0 -819
  49. package/resources/scripts/dev_tools/codex_native_converter/__init__.py +0 -11
  50. package/resources/scripts/dev_tools/codex_native_converter/__main__.py +0 -6
  51. package/resources/scripts/dev_tools/codex_native_converter/cli.py +0 -11
  52. package/resources/scripts/dev_tools/new_active_feature_folder.py +0 -79
  53. package/resources/scripts/dev_tools/new_active_feature_folder_docs.py +0 -268
  54. package/resources/scripts/dev_tools/new_active_feature_folder_flow.py +0 -366
  55. package/resources/scripts/dev_tools/new_active_feature_folder_io.py +0 -306
  56. package/resources/scripts/dev_tools/new_active_feature_folder_markdown.py +0 -252
  57. package/resources/scripts/dev_tools/new_active_feature_folder_models.py +0 -136
  58. package/resources/scripts/dev_tools/new_potential_bug_entry.py +0 -465
  59. package/resources/scripts/dev_tools/potential_to_issue.py +0 -421
  60. package/resources/scripts/dev_tools/potential_to_issue_content.py +0 -212
  61. package/resources/scripts/dev_tools/pr_context/__init__.py +0 -0
  62. package/resources/scripts/dev_tools/pr_context/collector.py +0 -619
  63. package/resources/scripts/dev_tools/pr_context/feature_docs.py +0 -349
  64. package/resources/scripts/dev_tools/pr_context/git.py +0 -153
  65. package/resources/scripts/dev_tools/pr_context/github.py +0 -549
  66. package/resources/scripts/dev_tools/pr_context/models.py +0 -198
  67. package/resources/scripts/dev_tools/pr_context/render.py +0 -342
  68. package/resources/scripts/dev_tools/pr_context/render_feature_excerpts.py +0 -256
  69. package/resources/scripts/dev_tools/pr_context/render_pr_helpers.py +0 -291
  70. package/resources/scripts/dev_tools/pr_context/summary_helpers.py +0 -386
  71. package/resources/scripts/dev_tools/pr_context/verification_evidence.py +0 -171
  72. package/resources/scripts/dev_tools/prompt_mode_contract.py +0 -152
  73. package/resources/scripts/dev_tools/push_down_claude_customizations.py +0 -374
  74. package/resources/scripts/dev_tools/push_down_codex_and_agents_customizations.py +0 -139
  75. package/resources/scripts/dev_tools/push_down_copilot_customizations.py +0 -504
  76. package/resources/scripts/dev_tools/push_down_copilot_customizations_filesystem.py +0 -217
  77. package/resources/scripts/dev_tools/push_down_copilot_customizations_rewrites.py +0 -293
  78. package/resources/scripts/dev_tools/resolve_file_prompt.py +0 -457
  79. package/resources/scripts/dev_tools/resolve_hard_lock_prompt.py +0 -444
  80. package/resources/scripts/dev_tools/validate_orchestration_artifacts.py +0 -246
  81. package/resources/scripts/dev_tools/validate_orchestration_review_artifacts.py +0 -107
  82. package/resources/scripts/dev_tools/validate_orchestrator_state.py +0 -428
  83. package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +0 -448
  84. package/resources/templates/codex_native_converter.py +0 -35
  85. package/resources/templates/collect_commit_context.py +0 -212
  86. package/resources/templates/collect_pr_context.py +0 -74
  87. package/resources/templates/hello_python.py +0 -11
  88. package/resources/templates/new_active_feature_folder.py +0 -67
  89. package/resources/templates/new_potential_bug_entry.py +0 -54
  90. package/resources/templates/potential_to_issue.py +0 -55
  91. package/resources/templates/push_down_claude_customizations.py +0 -409
  92. package/resources/templates/push_down_codex_and_agents_customizations.py +0 -95
  93. package/resources/templates/push_down_copilot_customizations.py +0 -124
  94. package/resources/templates/resolve_atomic_plan_prompt.py +0 -75
  95. package/resources/templates/resolve_hard_lock_prompt.py +0 -65
  96. package/resources/templates/validate_orchestration_artifacts.py +0 -55
@@ -0,0 +1,148 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Blocks implementation writes before Issue #232 orchestration readiness exists.
4
+ #>
5
+ [CmdletBinding()]
6
+ param()
7
+
8
+ $script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
9
+ $script:Issue232FeatureFolder = 'docs/features/active/2026-06-24-harden-orchestrate-skill-232'
10
+
11
+ function ConvertFrom-CheckpointJson {
12
+ [CmdletBinding()]
13
+ param([Parameter(Mandatory)][string] $Json)
14
+
15
+ return $Json | ConvertFrom-Json -ErrorAction Stop
16
+ }
17
+
18
+ function Get-StringProperty {
19
+ [CmdletBinding()]
20
+ [OutputType([string])]
21
+ param(
22
+ [Parameter(Mandatory)][AllowNull()] $Value,
23
+ [Parameter(Mandatory)][string] $Name
24
+ )
25
+
26
+ if ($null -eq $Value -or -not ($Value.PSObject.Properties.Name -contains $Name)) {
27
+ return ''
28
+ }
29
+ return ([string]$Value.$Name).Trim()
30
+ }
31
+
32
+ function Test-DocumentationOrEvidencePath {
33
+ [CmdletBinding()]
34
+ [OutputType([bool])]
35
+ param([Parameter(Mandatory)][string] $NormalizedPath)
36
+
37
+ return $NormalizedPath.StartsWith($script:Issue232FeatureFolder + '/')
38
+ }
39
+
40
+ function Test-ImplementationPath {
41
+ [CmdletBinding()]
42
+ [OutputType([bool])]
43
+ param([Parameter(Mandatory)][string] $NormalizedPath)
44
+
45
+ if (Test-DocumentationOrEvidencePath -NormalizedPath $NormalizedPath) {
46
+ return $false
47
+ }
48
+ if ($NormalizedPath -eq $script:CheckpointPath) {
49
+ return $false
50
+ }
51
+ return $NormalizedPath -match '\.(py|ps1|psm1|ts|tsx|js|jsx|cs|json|yml|yaml)$'
52
+ }
53
+
54
+ function Test-Issue232OrchestrationReady {
55
+ [CmdletBinding()]
56
+ [OutputType([bool])]
57
+ param([Parameter(Mandatory)][AllowNull()] $Payload)
58
+
59
+ if ($null -eq $Payload) {
60
+ return $false
61
+ }
62
+ $issueNum = Get-StringProperty -Value $Payload -Name 'issue-num'
63
+ $featureFolder = Get-StringProperty -Value $Payload -Name 'feature-folder'
64
+ $routeId = Get-StringProperty -Value $Payload -Name 'route_id'
65
+ if (-not $routeId) {
66
+ $routeId = Get-StringProperty -Value $Payload -Name 'path_selected'
67
+ }
68
+ $lifecycleReady = $false
69
+ if ($Payload.PSObject.Properties.Name -contains 'lifecycle_ready') {
70
+ $lifecycleReady = [bool]$Payload.lifecycle_ready
71
+ }
72
+
73
+ return (
74
+ $issueNum -eq '232' -and
75
+ $featureFolder -eq $script:Issue232FeatureFolder -and
76
+ $routeId -and
77
+ $lifecycleReady
78
+ )
79
+ }
80
+
81
+ function Get-CheckpointContent {
82
+ [CmdletBinding()]
83
+ [OutputType([string])]
84
+ param()
85
+
86
+ if (-not (Test-Path -LiteralPath $script:CheckpointPath)) {
87
+ return ''
88
+ }
89
+ return Get-Content -Raw -LiteralPath $script:CheckpointPath
90
+ }
91
+
92
+ function Invoke-OrchestrationPreimplementationGateDecision {
93
+ [CmdletBinding()]
94
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
95
+ param(
96
+ [string] $ToolInputRaw,
97
+ [string] $CheckpointRaw
98
+ )
99
+
100
+ if (-not $ToolInputRaw) {
101
+ return [ordered]@{ decision = 'allow' }
102
+ }
103
+ try {
104
+ $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
105
+ } catch {
106
+ throw "enforce-orchestration-preimplementation-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
107
+ }
108
+
109
+ $filePath = $toolInput.file_path
110
+ if (-not $filePath) {
111
+ return [ordered]@{ decision = 'allow' }
112
+ }
113
+ $normalized = ([string]$filePath) -replace '\\', '/'
114
+ if (-not (Test-ImplementationPath -NormalizedPath $normalized)) {
115
+ return [ordered]@{ decision = 'allow' }
116
+ }
117
+
118
+ if (-not $CheckpointRaw) {
119
+ $CheckpointRaw = Get-CheckpointContent
120
+ }
121
+ try {
122
+ $checkpoint = ConvertFrom-CheckpointJson -Json $CheckpointRaw
123
+ } catch {
124
+ $checkpoint = $null
125
+ }
126
+
127
+ if (Test-Issue232OrchestrationReady -Payload $checkpoint) {
128
+ return [ordered]@{ decision = 'allow' }
129
+ }
130
+ return [ordered]@{
131
+ decision = 'block'
132
+ reason = 'PREIMPLEMENTATION_GATE_BLOCKED: Issue #232 implementation writes require artifacts/orchestration/orchestrator-state.json to contain route metadata, lifecycle readiness, and checkpoint state before implementation begins.'
133
+ }
134
+ }
135
+
136
+ if ($MyInvocation.InvocationName -eq '.') {
137
+ return
138
+ }
139
+
140
+ try {
141
+ $decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
142
+ } catch {
143
+ Write-Error $_
144
+ exit 1
145
+ }
146
+
147
+ $decision | ConvertTo-Json -Compress | Write-Output
148
+ exit 0
@@ -0,0 +1,336 @@
1
+ # Converted hook
2
+ # Review the generated hook behavior before enabling it.
3
+
4
+ <#
5
+ .SYNOPSIS
6
+ Pre-tool-use hook that enforces the pr-author skill is used before gh pr create or gh pr edit.
7
+
8
+ .DESCRIPTION
9
+ Invoked by the Claude Code PreToolUse hook before any Bash command runs. Reads
10
+ tool input JSON from the CLAUDE_TOOL_INPUT environment variable, inspects the
11
+ attempted command, and blocks gh pr create / gh pr edit commands that bypass the
12
+ pr-author skill workflow.
13
+
14
+ Required sequence:
15
+ 1. mcp__drm-copilot__collect_pr_context writes artifacts/pr_context.summary.txt
16
+ 2. pr-author skill reads that file and writes artifacts/pr_body_<N>.md
17
+ 3. gh pr create --body-file artifacts/pr_body_<N>.md
18
+ (or gh pr edit --body-file ...)
19
+
20
+ Block cases:
21
+ Case A - gh pr create or gh pr edit with --body (inline, no --body-file): blocked.
22
+ Case B - gh pr create with neither --body nor --body-file: blocked.
23
+ Case C - gh pr create or gh pr edit with --body-file but context artifact absent: blocked.
24
+ Case D - --body-file present, context artifact present, but the authorization sentinel
25
+ artifacts/pr_author_authorization.json is absent or empty: blocked
26
+ (PR_AGENT_AUTHORIZATION_MISSING).
27
+ Malformed - sentinel present but not valid JSON, or issued_at missing/unparseable: blocked
28
+ (PR_AGENT_AUTHORIZATION_MALFORMED).
29
+ Case E - sentinel valid JSON but issued_by != "pr-author": blocked
30
+ (PR_AGENT_AUTHORIZATION_INVALID).
31
+ Case F - sentinel issued_by == "pr-author" but elapsed time since issued_at exceeds
32
+ ttl_seconds: blocked (PR_AGENT_AUTHORIZATION_EXPIRED).
33
+
34
+ Authorization sentinel decision order on the --body-file-with-context path:
35
+ missing -> malformed -> invalid issuer -> expired -> allow.
36
+
37
+ .NOTES
38
+ Compatible with PowerShell 7+. No external module dependencies.
39
+
40
+ Enforcement strength: the authorization sentinel is a policy guardrail, not a cryptographic
41
+ or security control. Any actor with Write access to artifacts/ can forge
42
+ artifacts/pr_author_authorization.json, because all agents share the same filesystem and the
43
+ runtime exposes no native agent-identity signal at Bash PreToolUse time. The mechanism prevents
44
+ accidental bypass and requires a deliberate, documented act to circumvent. It MUST NOT be
45
+ described as tamper-proof or as a security boundary.
46
+ #>
47
+ [CmdletBinding()]
48
+ param()
49
+
50
+ $script:PrContextArtifactPath = 'artifacts/pr_context.summary.txt'
51
+ $script:PrAuthorAuthorizationPath = 'artifacts/pr_author_authorization.json'
52
+ $script:PrAuthorAuthorizationTtlSeconds = 120
53
+
54
+ function Get-PrContextArtifactExistence {
55
+ <#
56
+ .SYNOPSIS
57
+ Wrapper around Test-Path for the PR context artifact. Tests mock this function.
58
+ .OUTPUTS
59
+ System.Boolean
60
+ #>
61
+ [CmdletBinding()]
62
+ [OutputType([bool])]
63
+ param()
64
+
65
+ return [bool](Test-Path -LiteralPath $script:PrContextArtifactPath)
66
+ }
67
+
68
+ function Get-PrAuthorAuthorizationContent {
69
+ <#
70
+ .SYNOPSIS
71
+ Read the raw text of the authorization sentinel. Tests mock this function (read seam).
72
+ .DESCRIPTION
73
+ Returns the raw text content of artifacts/pr_author_authorization.json, or $null when the
74
+ sentinel file is absent. This is the injectable boundary for sentinel content in tests; no
75
+ test writes the sentinel file to disk.
76
+ .OUTPUTS
77
+ System.String or $null
78
+ #>
79
+ [CmdletBinding()]
80
+ [OutputType([string])]
81
+ param()
82
+
83
+ if (-not (Test-Path -LiteralPath $script:PrAuthorAuthorizationPath)) {
84
+ return $null
85
+ }
86
+
87
+ return (Get-Content -LiteralPath $script:PrAuthorAuthorizationPath -Raw)
88
+ }
89
+
90
+ function Get-CurrentDateTimeUtc {
91
+ <#
92
+ .SYNOPSIS
93
+ Return the current UTC time. Tests mock this function (clock seam) for time-travel scenarios.
94
+ .OUTPUTS
95
+ System.DateTime
96
+ #>
97
+ [CmdletBinding()]
98
+ [OutputType([datetime])]
99
+ param()
100
+
101
+ return [DateTime]::UtcNow
102
+ }
103
+
104
+ function Test-PrAuthorAuthorization {
105
+ <#
106
+ .SYNOPSIS
107
+ Validate the authorization sentinel and return a block-reason string, or $null when authorized.
108
+ .DESCRIPTION
109
+ Reads the sentinel via Get-PrAuthorAuthorizationContent and computes elapsed time via
110
+ Get-CurrentDateTimeUtc. Applies the decision order missing -> malformed -> invalid issuer ->
111
+ expired -> allow (spec FR-2 step 3):
112
+ - $null/empty contents -> PR_AGENT_AUTHORIZATION_MISSING
113
+ - not valid JSON, or issued_at
114
+ missing/unparseable -> PR_AGENT_AUTHORIZATION_MALFORMED
115
+ - issued_by != "pr-author" -> PR_AGENT_AUTHORIZATION_INVALID
116
+ - elapsed seconds > ttl_seconds -> PR_AGENT_AUTHORIZATION_EXPIRED
117
+ - all checks pass -> $null (allow)
118
+ The ttl_seconds value defaults to the named constant when absent from the sentinel.
119
+ This is a policy guardrail, not a cryptographic control; the sentinel is forgeable by any
120
+ actor with Write access to artifacts/.
121
+ .OUTPUTS
122
+ System.String or $null
123
+ #>
124
+ [CmdletBinding()]
125
+ [OutputType([string])]
126
+ param()
127
+
128
+ $contents = Get-PrAuthorAuthorizationContent
129
+
130
+ if ([string]::IsNullOrWhiteSpace($contents)) {
131
+ return "PR_AGENT_AUTHORIZATION_MISSING: ``$script:PrAuthorAuthorizationPath`` is absent or empty. The pr-author agent must write the authorization sentinel immediately before issuing ``gh pr create``/``gh pr edit --body*``."
132
+ }
133
+
134
+ try {
135
+ $sentinel = $contents | ConvertFrom-Json -ErrorAction Stop
136
+ } catch {
137
+ return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` is not valid JSON. The pr-author agent must write a well-formed sentinel with ``issued_by``, ``issued_at``, and ``ttl_seconds``."
138
+ }
139
+
140
+ $issuedAtRaw = $sentinel.issued_at
141
+ if ([string]::IsNullOrWhiteSpace([string]$issuedAtRaw)) {
142
+ return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` is missing ``issued_at``. The pr-author agent must record a UTC ISO-8601 ``issued_at`` timestamp."
143
+ }
144
+
145
+ $issuedAt = [DateTime]::MinValue
146
+ $parsed = [DateTime]::TryParse(
147
+ [string]$issuedAtRaw,
148
+ [System.Globalization.CultureInfo]::InvariantCulture,
149
+ [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
150
+ [ref] $issuedAt)
151
+ if (-not $parsed) {
152
+ return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` has an unparseable ``issued_at``. The pr-author agent must record a UTC ISO-8601 ``issued_at`` timestamp."
153
+ }
154
+
155
+ if ($sentinel.issued_by -ne 'pr-author') {
156
+ return "PR_AGENT_AUTHORIZATION_INVALID: ``$script:PrAuthorAuthorizationPath`` ``issued_by`` is not ``pr-author``. Only the pr-author agent may authorize PR creation or body edits."
157
+ }
158
+
159
+ $ttlSeconds = $script:PrAuthorAuthorizationTtlSeconds
160
+ if ($null -ne $sentinel.ttl_seconds) {
161
+ $candidateTtl = 0
162
+ if ([int]::TryParse([string]$sentinel.ttl_seconds, [ref] $candidateTtl)) {
163
+ $ttlSeconds = $candidateTtl
164
+ }
165
+ }
166
+
167
+ $elapsedSeconds = ((Get-CurrentDateTimeUtc) - $issuedAt).TotalSeconds
168
+ if ($elapsedSeconds -gt $ttlSeconds) {
169
+ return "PR_AGENT_AUTHORIZATION_EXPIRED: ``$script:PrAuthorAuthorizationPath`` issued $([math]::Round($elapsedSeconds)) s ago, exceeding the ${ttlSeconds}s TTL. The pr-author agent must write a fresh sentinel immediately before the ``gh`` command."
170
+ }
171
+
172
+ return $null
173
+ }
174
+
175
+ function Get-PrAuthorBypassReason {
176
+ <#
177
+ .SYNOPSIS
178
+ Inspect the command text and return a block reason string, or $null when the command is allowed.
179
+ .DESCRIPTION
180
+ Returns PR_AUTHOR_SKILL_BLOCKED when gh pr create or gh pr edit is run with --body (inline,
181
+ no --body-file), or when gh pr create is run with no body flag at all. Returns
182
+ PR_CONTEXT_MISSING when --body-file is present but the context artifact
183
+ does not exist on disk. When --body-file is present and the context artifact exists, evaluates
184
+ the authorization sentinel via Test-PrAuthorAuthorization and returns its block reason
185
+ (PR_AGENT_AUTHORIZATION_*) when authorization fails. Returns $null for all allowed patterns.
186
+ Cases A, B, and C are evaluated first and unchanged; the sentinel check only extends the
187
+ previously-allowed --body-file-with-context path.
188
+ .PARAMETER CommandText
189
+ The Bash command text extracted from CLAUDE_TOOL_INPUT.
190
+ .PARAMETER ContextExists
191
+ Whether artifacts/pr_context.summary.txt currently exists on disk.
192
+ .OUTPUTS
193
+ System.String or $null
194
+ #>
195
+ [CmdletBinding()]
196
+ [OutputType([string])]
197
+ param(
198
+ [Parameter(Mandatory)]
199
+ [string] $CommandText,
200
+
201
+ [Parameter(Mandatory)]
202
+ [bool] $ContextExists
203
+ )
204
+
205
+ # Only act on gh pr create or gh pr edit subcommands.
206
+ $isPrCreate = $CommandText -match '(?i)\bgh\s+pr\s+create\b'
207
+ $isPrEdit = $CommandText -match '(?i)\bgh\s+pr\s+edit\b'
208
+
209
+ if (-not $isPrCreate -and -not $isPrEdit) {
210
+ return $null
211
+ }
212
+
213
+ $hasBodyFile = $CommandText -match '(?i)--body-file\b'
214
+ $hasInlineBody = $CommandText -match '(?i)--body(?!-file)\b'
215
+
216
+ # Case A: gh pr create OR gh pr edit with inline --body (not --body-file). Evaluated before the
217
+ # gh pr edit no-body allow short-circuit so inline-body edits are blocked, not allowed.
218
+ if (($isPrCreate -or $isPrEdit) -and $hasInlineBody -and -not $hasBodyFile) {
219
+ return "PR_AUTHOR_SKILL_BLOCKED: ``gh pr create`` and ``gh pr edit`` must use ``--body-file`` with a file produced by the pr-author skill from ``$script:PrContextArtifactPath``. Run ``mcp__drm-copilot__collect_pr_context`` to generate the context file, apply the pr-author skill to produce ``artifacts/pr_body_<N>.md``, then pass that file via ``--body-file``."
220
+ }
221
+
222
+ if ($isPrCreate) {
223
+ # Case B: gh pr create with no body flag at all.
224
+ if (-not $hasInlineBody -and -not $hasBodyFile) {
225
+ return "PR_AUTHOR_SKILL_BLOCKED: New PRs require ``--body-file``. Run ``mcp__drm-copilot__collect_pr_context`` to generate ``$script:PrContextArtifactPath``, apply the pr-author skill to produce ``artifacts/pr_body_<N>.md``, then pass that file via ``--body-file``."
226
+ }
227
+ }
228
+
229
+ if ($isPrEdit) {
230
+ # gh pr edit with no --body or --body-file (e.g., --title, --add-label, --reviewer) is allowed.
231
+ if (-not $hasInlineBody -and -not $hasBodyFile) {
232
+ return $null
233
+ }
234
+ }
235
+
236
+ # Case C: --body-file present but context artifact is absent.
237
+ if ($hasBodyFile -and -not $ContextExists) {
238
+ return "PR_CONTEXT_MISSING: ``$script:PrContextArtifactPath`` is absent. Run ``mcp__drm-copilot__collect_pr_context`` before creating or editing the PR body."
239
+ }
240
+
241
+ # Cases D/E/F and malformed: --body-file present and context artifact exists; verify the
242
+ # authorization sentinel. This extends, and does not replace, the previously-allowed path.
243
+ if ($hasBodyFile -and $ContextExists) {
244
+ $authorizationReason = Test-PrAuthorAuthorization
245
+ if ($authorizationReason) {
246
+ return $authorizationReason
247
+ }
248
+ }
249
+
250
+ return $null
251
+ }
252
+
253
+ function Invoke-PrAuthorSkillDecision {
254
+ <#
255
+ .SYNOPSIS
256
+ Parse CLAUDE_TOOL_INPUT and return an allow-or-block decision.
257
+ .PARAMETER ToolInputRaw
258
+ The raw JSON tool payload supplied by Claude Code.
259
+ .OUTPUTS
260
+ System.Collections.Specialized.OrderedDictionary
261
+ .NOTES
262
+ Missing tool input or missing command text is treated as allow.
263
+ #>
264
+ [CmdletBinding()]
265
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
266
+ param(
267
+ [string] $ToolInputRaw
268
+ )
269
+
270
+ if (-not $ToolInputRaw) {
271
+ return [ordered]@{ decision = 'allow' }
272
+ }
273
+
274
+ try {
275
+ $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
276
+ } catch {
277
+ throw "enforce-pr-author-skill hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
278
+ }
279
+
280
+ $commandText = $toolInput.command
281
+ if (-not $commandText) {
282
+ return [ordered]@{ decision = 'allow' }
283
+ }
284
+
285
+ $contextExists = Get-PrContextArtifactExistence
286
+ $reason = Get-PrAuthorBypassReason -CommandText $commandText -ContextExists $contextExists
287
+
288
+ if ($reason) {
289
+ return [ordered]@{
290
+ decision = 'block'
291
+ reason = $reason
292
+ }
293
+ }
294
+
295
+ return [ordered]@{ decision = 'allow' }
296
+ }
297
+
298
+ function Test-PrAuthorBypassRequired {
299
+ <#
300
+ .SYNOPSIS
301
+ Return $true when a Bash command requires the pr-author skill to run first.
302
+ .PARAMETER CommandText
303
+ The Bash command text extracted from CLAUDE_TOOL_INPUT.
304
+ .PARAMETER ContextExists
305
+ Whether artifacts/pr_context.summary.txt currently exists on disk.
306
+ .OUTPUTS
307
+ System.Boolean
308
+ #>
309
+ [CmdletBinding()]
310
+ [OutputType([bool])]
311
+ param(
312
+ [Parameter(Mandatory)]
313
+ [string] $CommandText,
314
+
315
+ [Parameter(Mandatory)]
316
+ [bool] $ContextExists
317
+ )
318
+
319
+ return ($null -ne (Get-PrAuthorBypassReason -CommandText $CommandText -ContextExists $ContextExists))
320
+ }
321
+
322
+ # Allow dot-sourcing in tests without executing the entrypoint.
323
+ if ($MyInvocation.InvocationName -eq '.') {
324
+ return
325
+ }
326
+
327
+ try {
328
+ $decision = Invoke-PrAuthorSkillDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
329
+ } catch {
330
+ Write-Error $_
331
+ exit 1
332
+ }
333
+
334
+ $decision | ConvertTo-Json -Compress | Write-Output
335
+
336
+ exit 0
@@ -1,6 +1,3 @@
1
- # Converted hook
2
- # Review the generated hook behavior before enabling it.
3
-
4
1
  <#
5
2
  .SYNOPSIS
6
3
  Pre-tool-use hook that blocks Bash promotion-script bypass attempts.
@@ -11,12 +8,18 @@
11
8
  variable, inspects the attempted command text, and blocks direct promotion
12
9
  script execution that would bypass the repository's MCP-only promotion path.
13
10
 
14
- Forbidden command tokens:
11
+ Forbidden command tokens (legacy promotion-script bypass):
15
12
  - new-potential-entry.ps1
16
13
  - new_potential_bug_entry
17
14
  - potential_to_issue
18
15
  - new_active_feature_folder
19
16
 
17
+ Forbidden gh-CLI patterns (raw GitHub issue creation bypass):
18
+ - gh issue create (with any flag suffix)
19
+ - gh issue new
20
+ - gh api against repos/<owner>/<repo>/issues with explicit POST method
21
+ (-X POST or --method POST)
22
+
20
23
  The hook is read-only: it inspects the attempted command and emits a JSON
21
24
  allow-or-block decision without mutating the command text.
22
25
 
@@ -28,10 +31,12 @@ param()
28
31
 
29
32
  $script:PromotionMcpOnlyBlockedReason = 'PROMOTION_MCP_ONLY_BLOCKED: Direct Bash promotion-script execution is not allowed in agent sessions. Use the drm-copilot MCP promotion tools instead.'
30
33
 
34
+ $script:PromotionMcpOnlyGhIssueBlockedReason = 'PROMOTION_MCP_ONLY_BLOCKED: Direct GitHub issue creation via `gh` bypasses the approved drm-copilot MCP promotion path (`mcp__drm-copilot__new_potential_entry` -> `mcp__drm-copilot__potential_to_issue` -> `mcp__drm-copilot__new_active_feature_folder`). Use those MCP tools instead.'
35
+
31
36
  function Get-PromotionMcpOnlyBlockedReason {
32
37
  <#
33
38
  .SYNOPSIS
34
- Return the canonical deny message for promotion-script bypass attempts.
39
+ Return the canonical deny message for legacy promotion-script bypass attempts.
35
40
  .OUTPUTS
36
41
  System.String
37
42
  #>
@@ -42,24 +47,40 @@ function Get-PromotionMcpOnlyBlockedReason {
42
47
  return $script:PromotionMcpOnlyBlockedReason
43
48
  }
44
49
 
45
- function Test-PromotionBypassToken {
50
+ function Get-PromotionMcpOnlyGhIssueBlockedReason {
51
+ <#
52
+ .SYNOPSIS
53
+ Return the deny message for raw gh-CLI issue creation bypass attempts.
54
+ .OUTPUTS
55
+ System.String
56
+ #>
57
+ [CmdletBinding()]
58
+ [OutputType([string])]
59
+ param()
60
+
61
+ return $script:PromotionMcpOnlyGhIssueBlockedReason
62
+ }
63
+
64
+ function Get-PromotionBypassReason {
46
65
  <#
47
66
  .SYNOPSIS
48
- Return $true when a Bash command contains a forbidden promotion token.
67
+ Inspect the command text and return the specific deny reason, or $null when allowed.
68
+ .DESCRIPTION
69
+ Returns the legacy promotion-script reason when any forbidden token is present.
70
+ Returns the gh-CLI issue creation reason when a forbidden gh pattern is matched.
71
+ Returns $null when the command is allowed.
49
72
  .PARAMETER CommandText
50
73
  The Bash command text extracted from CLAUDE_TOOL_INPUT.
51
74
  .OUTPUTS
52
- System.Boolean
75
+ System.String or $null.
53
76
  #>
54
77
  [CmdletBinding()]
55
- [OutputType([bool])]
78
+ [OutputType([string])]
56
79
  param(
57
80
  [Parameter(Mandatory)]
58
81
  [string] $CommandText
59
82
  )
60
83
 
61
- # Inspect only the command text so the hook remains a narrow, non-mutating
62
- # policy gate for direct promotion-script bypass attempts.
63
84
  $forbiddenTokens = @(
64
85
  'new-potential-entry.ps1',
65
86
  'new_potential_bug_entry',
@@ -69,27 +90,71 @@ function Test-PromotionBypassToken {
69
90
 
70
91
  foreach ($token in $forbiddenTokens) {
71
92
  if ($CommandText.IndexOf($token, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
72
- return $true
93
+ return (Get-PromotionMcpOnlyBlockedReason)
73
94
  }
74
95
  }
75
96
 
76
- return $false
97
+ # `gh issue create` and `gh issue new` are direct bypasses of the MCP
98
+ # promotion path. Tolerate any flags after the subcommand.
99
+ if ($CommandText -match '(?i)\bgh\s+issue\s+(?:create|new)\b') {
100
+ return (Get-PromotionMcpOnlyGhIssueBlockedReason)
101
+ }
102
+
103
+ # `gh api repos/<owner>/<repo>/issues` is a write surface only when an
104
+ # explicit POST method is supplied. `gh api` defaults to GET, so we only
105
+ # block when -X POST or --method POST is present, to avoid false positives
106
+ # on issue read operations. Use a single regex with lookaheads against the
107
+ # whole command string.
108
+ $ghApiIssuesPostPattern = '(?i)(?=.*\bgh\s+api\b)(?=.*repos/[^/\s]+/[^/\s]+/issues(?:\b|/[^/\s]*$))(?=.*(?:-X\s+POST|--method\s+POST))'
109
+ if ($CommandText -match $ghApiIssuesPostPattern) {
110
+ return (Get-PromotionMcpOnlyGhIssueBlockedReason)
111
+ }
112
+
113
+ return $null
114
+ }
115
+
116
+ function Test-PromotionBypassToken {
117
+ <#
118
+ .SYNOPSIS
119
+ Return $true when a Bash command contains a forbidden promotion bypass pattern.
120
+ .PARAMETER CommandText
121
+ The Bash command text extracted from CLAUDE_TOOL_INPUT.
122
+ .OUTPUTS
123
+ System.Boolean
124
+ #>
125
+ [CmdletBinding()]
126
+ [OutputType([bool])]
127
+ param(
128
+ [Parameter(Mandatory)]
129
+ [string] $CommandText
130
+ )
131
+
132
+ return ($null -ne (Get-PromotionBypassReason -CommandText $CommandText))
77
133
  }
78
134
 
79
135
  function Get-PromotionMcpOnlyBlockDecision {
80
136
  <#
81
137
  .SYNOPSIS
82
138
  Construct the structured block decision for a forbidden Bash command.
139
+ .PARAMETER Reason
140
+ The specific deny reason to surface in the block decision. Defaults to the
141
+ legacy promotion-script reason for backward compatibility.
83
142
  .OUTPUTS
84
143
  System.Collections.Specialized.OrderedDictionary
85
144
  #>
86
145
  [CmdletBinding()]
87
146
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
88
- param()
147
+ param(
148
+ [string] $Reason
149
+ )
150
+
151
+ if (-not $Reason) {
152
+ $Reason = Get-PromotionMcpOnlyBlockedReason
153
+ }
89
154
 
90
155
  return [ordered]@{
91
156
  decision = 'block'
92
- reason = (Get-PromotionMcpOnlyBlockedReason)
157
+ reason = $Reason
93
158
  }
94
159
  }
95
160
 
@@ -126,8 +191,9 @@ function Invoke-PromotionMcpOnlyDecision {
126
191
  return [ordered]@{ decision = 'allow' }
127
192
  }
128
193
 
129
- if (Test-PromotionBypassToken -CommandText $commandText) {
130
- return Get-PromotionMcpOnlyBlockDecision
194
+ $reason = Get-PromotionBypassReason -CommandText $commandText
195
+ if ($reason) {
196
+ return Get-PromotionMcpOnlyBlockDecision -Reason $reason
131
197
  }
132
198
 
133
199
  return [ordered]@{ decision = 'allow' }