@danmoisan/drm-copilot-mcp 0.0.5 → 0.0.6

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 (47) hide show
  1. package/out/mcp-server.js +93 -20
  2. package/package.json +1 -1
  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 +28 -1
  8. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +40 -14
  9. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +210 -0
  10. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +153 -10
  11. package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +136 -0
  12. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +22 -6
  13. package/resources/claude-customizations/.claude/settings.json +22 -0
  14. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +0 -1
  15. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +76 -2
  16. package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +5 -3
  17. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +69 -0
  18. package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +96 -0
  19. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +77 -0
  20. package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +64 -0
  21. package/resources/claude-customizations/pack-manifests/core.json +65 -0
  22. package/resources/claude-customizations/pack-manifests/csharp-legacy.json +11 -0
  23. package/resources/claude-customizations/pack-manifests/csharp-modern.json +12 -0
  24. package/resources/claude-customizations/pack-manifests/powershell.json +14 -0
  25. package/resources/claude-customizations/pack-manifests/python.json +14 -0
  26. package/resources/claude-customizations/pack-manifests/typescript.json +9 -0
  27. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +30 -13
  28. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +113 -10
  29. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +33 -8
  30. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +12 -7
  31. package/resources/codex-and-agents-customizations/.codex/config.toml +65 -6
  32. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +303 -0
  33. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +300 -0
  34. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +148 -0
  35. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +336 -0
  36. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +83 -17
  37. package/resources/config/orchestration-routing.json +2 -10
  38. package/resources/customizations/.github/agents/pr-author.agent.md +25 -0
  39. package/resources/customizations/.github/agents/task-researcher.agent.md +4 -4
  40. package/resources/customizations/.github/prompts/fillout-prd-feature.prompt.md +1 -1
  41. package/resources/customizations/.github/prompts/research-issue.prompt.md +4 -3
  42. package/resources/scripts/dev_tools/push_down_claude_customizations.py +253 -224
  43. package/resources/scripts/dev_tools/push_down_claude_filesystem.py +472 -0
  44. package/resources/scripts/dev_tools/push_down_claude_pack_selection.py +401 -0
  45. package/resources/scripts/dev_tools/validate_orchestrator_state.py +106 -29
  46. package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +24 -0
  47. package/resources/templates/push_down_claude_customizations.py +128 -304
@@ -0,0 +1,210 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Blocks implementation operations before 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-FeatureDocumentationOrEvidencePath {
33
+ [CmdletBinding()]
34
+ [OutputType([bool])]
35
+ param([Parameter(Mandatory)][string] $NormalizedPath)
36
+
37
+ return $NormalizedPath.StartsWith('docs/features/active/')
38
+ }
39
+
40
+ function Test-ImplementationPath {
41
+ [CmdletBinding()]
42
+ [OutputType([bool])]
43
+ param([Parameter(Mandatory)][string] $NormalizedPath)
44
+
45
+ if (Test-FeatureDocumentationOrEvidencePath -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-ImplementationCommand {
55
+ [CmdletBinding()]
56
+ [OutputType([bool])]
57
+ param([Parameter(Mandatory)][string] $Command)
58
+
59
+ $normalizedCommand = $Command.Trim()
60
+ if (-not $normalizedCommand) {
61
+ return $false
62
+ }
63
+
64
+ $implementationCommandPatterns = @(
65
+ '(^|\s)git\s+(add|commit)\b',
66
+ '(^|\s)(poetry\s+run\s+)?(black|ruff|pyright|pytest)\b',
67
+ '(^|\s)npm\s+.*\s+(prettier|lint|typecheck|test:unit)\b',
68
+ '(^|\s)npx\s+(prettier|eslint|tsc|jest)\b',
69
+ '(^|\s)pwsh\s+.*(Invoke-Pester|tests/scripts/)'
70
+ )
71
+
72
+ foreach ($pattern in $implementationCommandPatterns) {
73
+ if ($normalizedCommand -match $pattern) {
74
+ return $true
75
+ }
76
+ }
77
+ return $false
78
+ }
79
+
80
+ function Test-ImplementationDelegation {
81
+ [CmdletBinding()]
82
+ [OutputType([bool])]
83
+ param([Parameter(Mandatory)][AllowNull()] $ToolInput)
84
+
85
+ if ($null -eq $ToolInput) {
86
+ return $false
87
+ }
88
+
89
+ $payloadText = ($ToolInput | ConvertTo-Json -Depth 20 -Compress)
90
+ return $payloadText -match '(python-typed-engineer|powershell-typed-engineer|typescript-engineer|csharp-typed-engineer|atomic-executor|implementation|execute)'
91
+ }
92
+
93
+ function Test-OrchestrationReady {
94
+ [CmdletBinding()]
95
+ [OutputType([bool])]
96
+ param([Parameter(Mandatory)][AllowNull()] $Payload)
97
+
98
+ if ($null -eq $Payload) {
99
+ return $false
100
+ }
101
+ $issueNum = Get-StringProperty -Value $Payload -Name 'issue-num'
102
+ $featureFolder = Get-StringProperty -Value $Payload -Name 'feature-folder'
103
+ $routeId = Get-StringProperty -Value $Payload -Name 'route_id'
104
+ if (-not $routeId) {
105
+ $routeId = Get-StringProperty -Value $Payload -Name 'path_selected'
106
+ }
107
+ $lifecycleReady = $false
108
+ if ($Payload.PSObject.Properties.Name -contains 'lifecycle_ready') {
109
+ $lifecycleReady = [bool]$Payload.lifecycle_ready
110
+ }
111
+
112
+ if (-not $issueNum -or -not $featureFolder -or -not $routeId -or -not $lifecycleReady) {
113
+ return $false
114
+ }
115
+
116
+ if ($issueNum -eq '232' -and $featureFolder -ne $script:Issue232FeatureFolder) {
117
+ return $false
118
+ }
119
+
120
+ return (
121
+ $featureFolder.StartsWith('docs/features/active/') -and
122
+ $routeId -and
123
+ $lifecycleReady
124
+ )
125
+ }
126
+
127
+ function Get-CheckpointContent {
128
+ [CmdletBinding()]
129
+ [OutputType([string])]
130
+ param()
131
+
132
+ if (-not (Test-Path -LiteralPath $script:CheckpointPath)) {
133
+ return ''
134
+ }
135
+ return Get-Content -Raw -LiteralPath $script:CheckpointPath
136
+ }
137
+
138
+ function Invoke-OrchestrationPreimplementationGateDecision {
139
+ [CmdletBinding()]
140
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
141
+ param(
142
+ [string] $ToolInputRaw,
143
+ [string] $CheckpointRaw
144
+ )
145
+
146
+ if (-not $ToolInputRaw) {
147
+ return [ordered]@{ decision = 'allow' }
148
+ }
149
+ try {
150
+ $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
151
+ } catch {
152
+ throw "enforce-orchestration-preimplementation-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
153
+ }
154
+
155
+ $requiresReadyCheckpoint = $false
156
+ $filePath = Get-StringProperty -Value $toolInput -Name 'file_path'
157
+ if ($filePath) {
158
+ $normalized = ([string]$filePath) -replace '\\', '/'
159
+ $requiresReadyCheckpoint = Test-ImplementationPath -NormalizedPath $normalized
160
+ } else {
161
+ $command = Get-StringProperty -Value $toolInput -Name 'command'
162
+ if ($command) {
163
+ $requiresReadyCheckpoint = Test-ImplementationCommand -Command $command
164
+ } else {
165
+ $requiresReadyCheckpoint = Test-ImplementationDelegation -ToolInput $toolInput
166
+ }
167
+ }
168
+
169
+ if (-not $requiresReadyCheckpoint) {
170
+ return [ordered]@{ decision = 'allow' }
171
+ }
172
+
173
+ if (-not $CheckpointRaw) {
174
+ $CheckpointRaw = Get-CheckpointContent
175
+ }
176
+ try {
177
+ $checkpoint = ConvertFrom-CheckpointJson -Json $CheckpointRaw
178
+ } catch {
179
+ $checkpoint = $null
180
+ }
181
+
182
+ if (Test-OrchestrationReady -Payload $checkpoint) {
183
+ return [ordered]@{ decision = 'allow' }
184
+ }
185
+ $issueNum = Get-StringProperty -Value $checkpoint -Name 'issue-num'
186
+ if ($issueNum -eq '232') {
187
+ return [ordered]@{
188
+ decision = 'block'
189
+ reason = 'PREIMPLEMENTATION_GATE_BLOCKED: Issue #232 implementation operations require artifacts/orchestration/orchestrator-state.json to contain route metadata, lifecycle readiness, and checkpoint state before implementation begins.'
190
+ }
191
+ }
192
+ return [ordered]@{
193
+ decision = 'block'
194
+ 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.'
195
+ }
196
+ }
197
+
198
+ if ($MyInvocation.InvocationName -eq '.') {
199
+ return
200
+ }
201
+
202
+ try {
203
+ $decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
204
+ } catch {
205
+ Write-Error $_
206
+ exit 1
207
+ }
208
+
209
+ $decision | ConvertTo-Json -Compress | Write-Output
210
+ exit 0
@@ -14,18 +14,39 @@
14
14
  3. gh pr create --body-file artifacts/pr_body_<N>.md
15
15
  (or gh pr edit --body-file ...)
16
16
 
17
- Three block cases:
18
- Case A - gh pr create with --body (inline, no --body-file): blocked.
17
+ Block cases:
18
+ Case A - gh pr create or gh pr edit with --body (inline, no --body-file): blocked.
19
19
  Case B - gh pr create with neither --body nor --body-file: blocked.
20
20
  Case C - gh pr create or gh pr edit with --body-file but context artifact absent: blocked.
21
+ Case D - --body-file present, context artifact present, but the authorization sentinel
22
+ artifacts/pr_author_authorization.json is absent or empty: blocked
23
+ (PR_AGENT_AUTHORIZATION_MISSING).
24
+ Malformed - sentinel present but not valid JSON, or issued_at missing/unparseable: blocked
25
+ (PR_AGENT_AUTHORIZATION_MALFORMED).
26
+ Case E - sentinel valid JSON but issued_by != "pr-author": blocked
27
+ (PR_AGENT_AUTHORIZATION_INVALID).
28
+ Case F - sentinel issued_by == "pr-author" but elapsed time since issued_at exceeds
29
+ ttl_seconds: blocked (PR_AGENT_AUTHORIZATION_EXPIRED).
30
+
31
+ Authorization sentinel decision order on the --body-file-with-context path:
32
+ missing -> malformed -> invalid issuer -> expired -> allow.
21
33
 
22
34
  .NOTES
23
35
  Compatible with PowerShell 7+. No external module dependencies.
36
+
37
+ Enforcement strength: the authorization sentinel is a policy guardrail, not a cryptographic
38
+ or security control. Any actor with Write access to artifacts/ can forge
39
+ artifacts/pr_author_authorization.json, because all agents share the same filesystem and the
40
+ runtime exposes no native agent-identity signal at Bash PreToolUse time. The mechanism prevents
41
+ accidental bypass and requires a deliberate, documented act to circumvent. It MUST NOT be
42
+ described as tamper-proof or as a security boundary.
24
43
  #>
25
44
  [CmdletBinding()]
26
45
  param()
27
46
 
28
47
  $script:PrContextArtifactPath = 'artifacts/pr_context.summary.txt'
48
+ $script:PrAuthorAuthorizationPath = 'artifacts/pr_author_authorization.json'
49
+ $script:PrAuthorAuthorizationTtlSeconds = 120
29
50
 
30
51
  function Get-PrContextArtifactExistence {
31
52
  <#
@@ -41,14 +62,126 @@ function Get-PrContextArtifactExistence {
41
62
  return [bool](Test-Path -LiteralPath $script:PrContextArtifactPath)
42
63
  }
43
64
 
65
+ function Get-PrAuthorAuthorizationContent {
66
+ <#
67
+ .SYNOPSIS
68
+ Read the raw text of the authorization sentinel. Tests mock this function (read seam).
69
+ .DESCRIPTION
70
+ Returns the raw text content of artifacts/pr_author_authorization.json, or $null when the
71
+ sentinel file is absent. This is the injectable boundary for sentinel content in tests; no
72
+ test writes the sentinel file to disk.
73
+ .OUTPUTS
74
+ System.String or $null
75
+ #>
76
+ [CmdletBinding()]
77
+ [OutputType([string])]
78
+ param()
79
+
80
+ if (-not (Test-Path -LiteralPath $script:PrAuthorAuthorizationPath)) {
81
+ return $null
82
+ }
83
+
84
+ return (Get-Content -LiteralPath $script:PrAuthorAuthorizationPath -Raw)
85
+ }
86
+
87
+ function Get-CurrentDateTimeUtc {
88
+ <#
89
+ .SYNOPSIS
90
+ Return the current UTC time. Tests mock this function (clock seam) for time-travel scenarios.
91
+ .OUTPUTS
92
+ System.DateTime
93
+ #>
94
+ [CmdletBinding()]
95
+ [OutputType([datetime])]
96
+ param()
97
+
98
+ return [DateTime]::UtcNow
99
+ }
100
+
101
+ function Test-PrAuthorAuthorization {
102
+ <#
103
+ .SYNOPSIS
104
+ Validate the authorization sentinel and return a block-reason string, or $null when authorized.
105
+ .DESCRIPTION
106
+ Reads the sentinel via Get-PrAuthorAuthorizationContent and computes elapsed time via
107
+ Get-CurrentDateTimeUtc. Applies the decision order missing -> malformed -> invalid issuer ->
108
+ expired -> allow (spec FR-2 step 3):
109
+ - $null/empty contents -> PR_AGENT_AUTHORIZATION_MISSING
110
+ - not valid JSON, or issued_at
111
+ missing/unparseable -> PR_AGENT_AUTHORIZATION_MALFORMED
112
+ - issued_by != "pr-author" -> PR_AGENT_AUTHORIZATION_INVALID
113
+ - elapsed seconds > ttl_seconds -> PR_AGENT_AUTHORIZATION_EXPIRED
114
+ - all checks pass -> $null (allow)
115
+ The ttl_seconds value defaults to the named constant when absent from the sentinel.
116
+ This is a policy guardrail, not a cryptographic control; the sentinel is forgeable by any
117
+ actor with Write access to artifacts/.
118
+ .OUTPUTS
119
+ System.String or $null
120
+ #>
121
+ [CmdletBinding()]
122
+ [OutputType([string])]
123
+ param()
124
+
125
+ $contents = Get-PrAuthorAuthorizationContent
126
+
127
+ if ([string]::IsNullOrWhiteSpace($contents)) {
128
+ 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*``."
129
+ }
130
+
131
+ try {
132
+ $sentinel = $contents | ConvertFrom-Json -ErrorAction Stop
133
+ } catch {
134
+ 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``."
135
+ }
136
+
137
+ $issuedAtRaw = $sentinel.issued_at
138
+ if ([string]::IsNullOrWhiteSpace([string]$issuedAtRaw)) {
139
+ return "PR_AGENT_AUTHORIZATION_MALFORMED: ``$script:PrAuthorAuthorizationPath`` is missing ``issued_at``. The pr-author agent must record a UTC ISO-8601 ``issued_at`` timestamp."
140
+ }
141
+
142
+ $issuedAt = [DateTime]::MinValue
143
+ $parsed = [DateTime]::TryParse(
144
+ [string]$issuedAtRaw,
145
+ [System.Globalization.CultureInfo]::InvariantCulture,
146
+ [System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
147
+ [ref] $issuedAt)
148
+ if (-not $parsed) {
149
+ 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."
150
+ }
151
+
152
+ if ($sentinel.issued_by -ne 'pr-author') {
153
+ 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."
154
+ }
155
+
156
+ $ttlSeconds = $script:PrAuthorAuthorizationTtlSeconds
157
+ if ($null -ne $sentinel.ttl_seconds) {
158
+ $candidateTtl = 0
159
+ if ([int]::TryParse([string]$sentinel.ttl_seconds, [ref] $candidateTtl)) {
160
+ $ttlSeconds = $candidateTtl
161
+ }
162
+ }
163
+
164
+ $elapsedSeconds = ((Get-CurrentDateTimeUtc) - $issuedAt).TotalSeconds
165
+ if ($elapsedSeconds -gt $ttlSeconds) {
166
+ 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."
167
+ }
168
+
169
+ return $null
170
+ }
171
+
44
172
  function Get-PrAuthorBypassReason {
45
173
  <#
46
174
  .SYNOPSIS
47
175
  Inspect the command text and return a block reason string, or $null when the command is allowed.
48
176
  .DESCRIPTION
49
- Returns PR_AUTHOR_SKILL_BLOCKED when gh pr create is run with --body (inline) or with no body
50
- flag at all. Returns PR_CONTEXT_MISSING when --body-file is present but the context artifact
51
- does not exist on disk. Returns $null for all allowed patterns.
177
+ Returns PR_AUTHOR_SKILL_BLOCKED when gh pr create or gh pr edit is run with --body (inline,
178
+ no --body-file), or when gh pr create is run with no body flag at all. Returns
179
+ PR_CONTEXT_MISSING when --body-file is present but the context artifact
180
+ does not exist on disk. When --body-file is present and the context artifact exists, evaluates
181
+ the authorization sentinel via Test-PrAuthorAuthorization and returns its block reason
182
+ (PR_AGENT_AUTHORIZATION_*) when authorization fails. Returns $null for all allowed patterns.
183
+ Cases A, B, and C are evaluated first and unchanged; the sentinel check only extends the
184
+ previously-allowed --body-file-with-context path.
52
185
  .PARAMETER CommandText
53
186
  The Bash command text extracted from CLAUDE_TOOL_INPUT.
54
187
  .PARAMETER ContextExists
@@ -77,12 +210,13 @@ function Get-PrAuthorBypassReason {
77
210
  $hasBodyFile = $CommandText -match '(?i)--body-file\b'
78
211
  $hasInlineBody = $CommandText -match '(?i)--body(?!-file)\b'
79
212
 
80
- if ($isPrCreate) {
81
- # Case A: gh pr create with inline --body (not --body-file).
82
- if ($hasInlineBody -and -not $hasBodyFile) {
83
- return "PR_AUTHOR_SKILL_BLOCKED: ``gh pr create`` 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``."
84
- }
213
+ # Case A: gh pr create OR gh pr edit with inline --body (not --body-file). Evaluated before the
214
+ # gh pr edit no-body allow short-circuit so inline-body edits are blocked, not allowed.
215
+ if (($isPrCreate -or $isPrEdit) -and $hasInlineBody -and -not $hasBodyFile) {
216
+ 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``."
217
+ }
85
218
 
219
+ if ($isPrCreate) {
86
220
  # Case B: gh pr create with no body flag at all.
87
221
  if (-not $hasInlineBody -and -not $hasBodyFile) {
88
222
  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``."
@@ -101,6 +235,15 @@ function Get-PrAuthorBypassReason {
101
235
  return "PR_CONTEXT_MISSING: ``$script:PrContextArtifactPath`` is absent. Run ``mcp__drm-copilot__collect_pr_context`` before creating or editing the PR body."
102
236
  }
103
237
 
238
+ # Cases D/E/F and malformed: --body-file present and context artifact exists; verify the
239
+ # authorization sentinel. This extends, and does not replace, the previously-allowed path.
240
+ if ($hasBodyFile -and $ContextExists) {
241
+ $authorizationReason = Test-PrAuthorAuthorization
242
+ if ($authorizationReason) {
243
+ return $authorizationReason
244
+ }
245
+ }
246
+
104
247
  return $null
105
248
  }
106
249
 
@@ -0,0 +1,136 @@
1
+ <#
2
+ .SYNOPSIS
3
+ SubagentStop hook that verifies the pr-author agent reported a created or updated PR.
4
+
5
+ .DESCRIPTION
6
+ Invoked by the Claude Code SubagentStop hook for the pr-author matcher. Reads the agent
7
+ transcript JSON from the CLAUDE_HOOK_INPUT environment variable, extracts the .output field,
8
+ and confirms the output reports that a pull request was created or updated. The hook allows
9
+ (exit 0) when the output contains:
10
+ - a GitHub PR URL (github.com/<owner>/<repo>/pull/<n>), or
11
+ - a PR reference of the form "PR #<n>", or
12
+ - a "gh pr create"/"gh pr edit" confirmation that includes a PR number.
13
+ The hook blocks (exit 1) when:
14
+ - CLAUDE_HOOK_INPUT is empty,
15
+ - the input is not valid JSON,
16
+ - the .output field is empty, or
17
+ - the output contains no PR URL or PR number.
18
+
19
+ .NOTES
20
+ Compatible with PowerShell 7+. No external module dependencies.
21
+
22
+ Enforcement strength: this validator is a policy guardrail, not a cryptographic or security
23
+ control. It confirms that the pr-author agent's final output references a PR; it cannot verify
24
+ that the referenced PR actually exists on GitHub, and the output text is forgeable by any actor
25
+ that controls the agent transcript. It MUST NOT be described as tamper-proof or as a security
26
+ boundary.
27
+ #>
28
+ [CmdletBinding()]
29
+ param()
30
+
31
+ function Test-PrAuthorOutputReportsPr {
32
+ <#
33
+ .SYNOPSIS
34
+ Return $true when the supplied output text reports a created or updated PR.
35
+ .DESCRIPTION
36
+ Detection helper (injectable boundary for tests). Matches a GitHub PR URL, a "PR #<n>"
37
+ reference, or a "gh pr create"/"gh pr edit" confirmation that contains a PR number.
38
+ .PARAMETER OutputText
39
+ The pr-author agent's final output text extracted from CLAUDE_HOOK_INPUT.output.
40
+ .OUTPUTS
41
+ System.Boolean
42
+ #>
43
+ [CmdletBinding()]
44
+ [OutputType([bool])]
45
+ param(
46
+ [string] $OutputText
47
+ )
48
+
49
+ if ([string]::IsNullOrWhiteSpace($OutputText)) {
50
+ return $false
51
+ }
52
+
53
+ # GitHub PR URL, e.g. https://github.com/owner/repo/pull/123
54
+ if ($OutputText -match '(?i)github\.com/[^\s/]+/[^\s/]+/pull/\d+') {
55
+ return $true
56
+ }
57
+
58
+ # Explicit PR number reference, e.g. "PR #123".
59
+ if ($OutputText -match '(?i)\bPR\s*#\d+') {
60
+ return $true
61
+ }
62
+
63
+ # gh pr create / gh pr edit confirmation that includes a PR number anywhere in the output.
64
+ if (($OutputText -match '(?i)\bgh\s+pr\s+(create|edit)\b') -and ($OutputText -match '#\d+')) {
65
+ return $true
66
+ }
67
+
68
+ return $false
69
+ }
70
+
71
+ function Get-PrAuthorOutputDecision {
72
+ <#
73
+ .SYNOPSIS
74
+ Parse CLAUDE_HOOK_INPUT and return an allow-or-block decision with exit code semantics.
75
+ .DESCRIPTION
76
+ Returns an ordered dictionary with 'allowed' (bool) and 'reason' (string). Allowed is $true
77
+ only when the parsed .output reports a PR per Test-PrAuthorOutputReportsPr. Empty input,
78
+ malformed JSON, empty output, and PR-less output all yield allowed = $false.
79
+ .PARAMETER HookInputRaw
80
+ The raw JSON transcript payload supplied by Claude Code via CLAUDE_HOOK_INPUT.
81
+ .OUTPUTS
82
+ System.Collections.Specialized.OrderedDictionary
83
+ #>
84
+ [CmdletBinding()]
85
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
86
+ param(
87
+ [string] $HookInputRaw
88
+ )
89
+
90
+ if ([string]::IsNullOrWhiteSpace($HookInputRaw)) {
91
+ return [ordered]@{
92
+ allowed = $false
93
+ reason = 'PR_AUTHOR_OUTPUT_MISSING: CLAUDE_HOOK_INPUT is empty; the pr-author agent produced no transcript to validate.'
94
+ }
95
+ }
96
+
97
+ try {
98
+ $parsed = $HookInputRaw | ConvertFrom-Json -ErrorAction Stop
99
+ } catch {
100
+ return [ordered]@{
101
+ allowed = $false
102
+ reason = 'PR_AUTHOR_OUTPUT_MALFORMED: CLAUDE_HOOK_INPUT is not valid JSON.'
103
+ }
104
+ }
105
+
106
+ $outputText = [string]$parsed.output
107
+ if ([string]::IsNullOrWhiteSpace($outputText)) {
108
+ return [ordered]@{
109
+ allowed = $false
110
+ reason = 'PR_AUTHOR_OUTPUT_EMPTY: the pr-author agent output is empty; it must report the created or updated PR URL or number.'
111
+ }
112
+ }
113
+
114
+ if (Test-PrAuthorOutputReportsPr -OutputText $outputText) {
115
+ return [ordered]@{ allowed = $true }
116
+ }
117
+
118
+ return [ordered]@{
119
+ allowed = $false
120
+ reason = 'PR_AUTHOR_OUTPUT_NO_PR: the pr-author agent output does not reference a PR URL or PR number; it must report the created or updated pull request.'
121
+ }
122
+ }
123
+
124
+ # Allow dot-sourcing in tests without executing the entrypoint.
125
+ if ($MyInvocation.InvocationName -eq '.') {
126
+ return
127
+ }
128
+
129
+ $decision = Get-PrAuthorOutputDecision -HookInputRaw $env:CLAUDE_HOOK_INPUT
130
+
131
+ if (-not $decision['allowed']) {
132
+ Write-Error $decision['reason']
133
+ exit 1
134
+ }
135
+
136
+ exit 0
@@ -5,8 +5,10 @@
5
5
  .DESCRIPTION
6
6
  Blocks termination of the task-researcher subagent unless the agent's
7
7
  final output advertises a `research-path` token, the path is rooted
8
- under artifacts/research/, matches the documented filename convention,
9
- and the file exists on disk.
8
+ under one of the two tracked research roots
9
+ (docs/features/<feature>/research/ for feature-associated research, or
10
+ docs/research/ for one-off research), matches the documented filename
11
+ convention, and the file exists on disk.
10
12
 
11
13
  .NOTES
12
14
  Reads the hook payload from CLAUDE_HOOK_INPUT as JSON. Exits 0 to allow
@@ -64,7 +66,20 @@ function Test-IsUnderResearchRoot {
64
66
  )
65
67
 
66
68
  $normalized = $Path -replace '\\', '/'
67
- return $normalized.StartsWith('artifacts/research/', [System.StringComparison]::OrdinalIgnoreCase)
69
+
70
+ # Accept either of the two tracked research roots:
71
+ # - feature-associated research: docs/features/<...>/research/<file>
72
+ # (a feature-folder path that contains a /research/ segment), or
73
+ # - one-off research: docs/research/<file>.
74
+ # The /research/ segment requirement distinguishes feature research from
75
+ # other files under docs/features/ (for example spec.md or plan files).
76
+ $isFeatureResearch =
77
+ $normalized.StartsWith('docs/features/', [System.StringComparison]::OrdinalIgnoreCase) -and
78
+ $normalized.IndexOf('/research/', [System.StringComparison]::OrdinalIgnoreCase) -ge 0
79
+ $isOneOffResearch =
80
+ $normalized.StartsWith('docs/research/', [System.StringComparison]::OrdinalIgnoreCase)
81
+
82
+ return ($isFeatureResearch -or $isOneOffResearch)
68
83
  }
69
84
 
70
85
  function Test-IsValidResearchFileName {
@@ -174,15 +189,15 @@ function Invoke-TaskResearcherOutputValidation {
174
189
 
175
190
  $researchPath = Get-ResearchPathFromOutput -AgentOutput $agentOutput
176
191
  if ($null -eq $researchPath) {
177
- return @{ Ok = $false; Message = 'task-researcher hook: agent output does not advertise a research-path. Researcher must report `research-path: <path>` pointing to artifacts/research/.' }
192
+ return @{ Ok = $false; Message = 'task-researcher hook: agent output does not advertise a research-path. Researcher must report `research-path: <path>` pointing to docs/features/<feature>/research/ (feature-associated) or docs/research/ (one-off).' }
178
193
  }
179
194
 
180
195
  if (-not (Test-IsUnderResearchRoot -Path $researchPath)) {
181
- return @{ Ok = $false; Message = "task-researcher hook: research-path '$researchPath' is not under artifacts/research/. All research artifacts must be written to artifacts/research/." }
196
+ return @{ Ok = $false; Message = "task-researcher hook: research-path '$researchPath' is not under a tracked research root. All research artifacts must be written to docs/features/<feature>/research/ (feature-associated) or docs/research/ (one-off)." }
182
197
  }
183
198
 
184
199
  if (-not (Test-IsValidResearchFileName -Path $researchPath)) {
185
- return @{ Ok = $false; Message = "task-researcher hook: research-path '$researchPath' does not match the required filename convention `artifacts/research/<timestamp>-<short-name>-research.md`." }
200
+ return @{ Ok = $false; Message = "task-researcher hook: research-path '$researchPath' does not match the required filename convention `<timestamp>-<short-name>-research.md` under docs/features/<feature>/research/ or docs/research/." }
186
201
  }
187
202
 
188
203
  if (-not (Test-ResearchFile -Path $researchPath)) {
@@ -208,3 +223,4 @@ if (-not $result.Ok) {
208
223
  }
209
224
 
210
225
  exit 0
226
+
@@ -30,6 +30,7 @@
30
30
  "Agent(staged-review)",
31
31
  "Agent(epic-review)",
32
32
  "Agent(status-updater)",
33
+ "Agent(pr-author)",
33
34
  "Agent(python-typed-engineer)",
34
35
  "Agent(powershell-typed-engineer)",
35
36
  "Agent(csharp-typed-engineer)",
@@ -82,6 +83,10 @@
82
83
  {
83
84
  "type": "command",
84
85
  "command": "pwsh -NoProfile -File .claude/hooks/enforce-pr-author-skill.ps1"
86
+ },
87
+ {
88
+ "type": "command",
89
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-orchestration-preimplementation-gate.ps1"
85
90
  }
86
91
  ]
87
92
  },
@@ -108,6 +113,10 @@
108
113
  "type": "command",
109
114
  "command": "pwsh -NoProfile -File .claude/hooks/enforce-evidence-locations.ps1"
110
115
  },
116
+ {
117
+ "type": "command",
118
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-orchestration-preimplementation-gate.ps1"
119
+ },
111
120
  {
112
121
  "type": "command",
113
122
  "command": "pwsh -NoProfile -File .claude/hooks/enforce-feature-folder-order.ps1"
@@ -128,6 +137,10 @@
128
137
  {
129
138
  "type": "command",
130
139
  "command": "pwsh -NoProfile -File .claude/hooks/enforce-prd-feature-before-planner.ps1"
140
+ },
141
+ {
142
+ "type": "command",
143
+ "command": "pwsh -NoProfile -File .claude/hooks/enforce-orchestration-preimplementation-gate.ps1"
131
144
  }
132
145
  ]
133
146
  }
@@ -159,6 +172,15 @@
159
172
  "command": "pwsh -NoProfile -File .claude/hooks/validate-planner-output.ps1"
160
173
  }
161
174
  ]
175
+ },
176
+ {
177
+ "matcher": "pr-author",
178
+ "hooks": [
179
+ {
180
+ "type": "command",
181
+ "command": "pwsh -NoProfile -File .claude/hooks/validate-pr-author-output.ps1"
182
+ }
183
+ ]
162
184
  }
163
185
  ]
164
186
  },
@@ -31,7 +31,6 @@ The following sub-paths under `artifacts/` are FORBIDDEN for evidence output:
31
31
 
32
32
  Allowed `artifacts/` sub-paths (non-evidence orchestration use only):
33
33
  - `artifacts/orchestration/`
34
- - `artifacts/research/`
35
34
 
36
35
  No delegation prompt, plan task, or upstream agent instruction may override this scheme. If a caller supplies a non-canonical path, the receiving agent MUST reject it, substitute the canonical path, and record `EVIDENCE_LOCATION_OVERRIDE_REJECTED: <supplied path> replaced with <canonical path>`.
37
36