@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.
- package/out/mcp-server.js +93 -20
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/orchestrator.md +6 -2
- package/resources/claude-customizations/.claude/agents/pr-author.md +78 -0
- package/resources/claude-customizations/.claude/agents/task-researcher.md +10 -6
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +68 -10
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +28 -1
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +40 -14
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +210 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +153 -10
- package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +136 -0
- package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +22 -6
- package/resources/claude-customizations/.claude/settings.json +22 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +0 -1
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +76 -2
- package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +5 -3
- package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +69 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +96 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +77 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +64 -0
- package/resources/claude-customizations/pack-manifests/core.json +65 -0
- package/resources/claude-customizations/pack-manifests/csharp-legacy.json +11 -0
- package/resources/claude-customizations/pack-manifests/csharp-modern.json +12 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +14 -0
- package/resources/claude-customizations/pack-manifests/python.json +14 -0
- package/resources/claude-customizations/pack-manifests/typescript.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +30 -13
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +113 -10
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +33 -8
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +12 -7
- package/resources/codex-and-agents-customizations/.codex/config.toml +65 -6
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +303 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +300 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +148 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +336 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +83 -17
- package/resources/config/orchestration-routing.json +2 -10
- package/resources/customizations/.github/agents/pr-author.agent.md +25 -0
- package/resources/customizations/.github/agents/task-researcher.agent.md +4 -4
- package/resources/customizations/.github/prompts/fillout-prd-feature.prompt.md +1 -1
- package/resources/customizations/.github/prompts/research-issue.prompt.md +4 -3
- package/resources/scripts/dev_tools/push_down_claude_customizations.py +253 -224
- package/resources/scripts/dev_tools/push_down_claude_filesystem.py +472 -0
- package/resources/scripts/dev_tools/push_down_claude_pack_selection.py +401 -0
- package/resources/scripts/dev_tools/validate_orchestrator_state.py +106 -29
- package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +24 -0
- 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
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
9
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
},
|
package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md
CHANGED
|
@@ -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
|
|