@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.
- package/out/mcp-server.js +10268 -380
- package/package.json +2 -2
- 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 +148 -11
- package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +163 -0
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +40 -14
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +198 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +153 -10
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +68 -1
- 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 +5 -13
- 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/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +0 -5
- package/resources/scripts/dev_tools/__init__.py +0 -0
- package/resources/scripts/dev_tools/_orchestrator_state_human_interaction.py +0 -127
- package/resources/scripts/dev_tools/_orchestrator_state_routing.py +0 -216
- package/resources/scripts/dev_tools/agentic_sync.py +0 -819
- package/resources/scripts/dev_tools/codex_native_converter/__init__.py +0 -11
- package/resources/scripts/dev_tools/codex_native_converter/__main__.py +0 -6
- package/resources/scripts/dev_tools/codex_native_converter/cli.py +0 -11
- package/resources/scripts/dev_tools/new_active_feature_folder.py +0 -79
- package/resources/scripts/dev_tools/new_active_feature_folder_docs.py +0 -268
- package/resources/scripts/dev_tools/new_active_feature_folder_flow.py +0 -366
- package/resources/scripts/dev_tools/new_active_feature_folder_io.py +0 -306
- package/resources/scripts/dev_tools/new_active_feature_folder_markdown.py +0 -252
- package/resources/scripts/dev_tools/new_active_feature_folder_models.py +0 -136
- package/resources/scripts/dev_tools/new_potential_bug_entry.py +0 -465
- package/resources/scripts/dev_tools/potential_to_issue.py +0 -421
- package/resources/scripts/dev_tools/potential_to_issue_content.py +0 -212
- package/resources/scripts/dev_tools/pr_context/__init__.py +0 -0
- package/resources/scripts/dev_tools/pr_context/collector.py +0 -619
- package/resources/scripts/dev_tools/pr_context/feature_docs.py +0 -349
- package/resources/scripts/dev_tools/pr_context/git.py +0 -153
- package/resources/scripts/dev_tools/pr_context/github.py +0 -549
- package/resources/scripts/dev_tools/pr_context/models.py +0 -198
- package/resources/scripts/dev_tools/pr_context/render.py +0 -342
- package/resources/scripts/dev_tools/pr_context/render_feature_excerpts.py +0 -256
- package/resources/scripts/dev_tools/pr_context/render_pr_helpers.py +0 -291
- package/resources/scripts/dev_tools/pr_context/summary_helpers.py +0 -386
- package/resources/scripts/dev_tools/pr_context/verification_evidence.py +0 -171
- package/resources/scripts/dev_tools/prompt_mode_contract.py +0 -152
- package/resources/scripts/dev_tools/push_down_claude_customizations.py +0 -374
- package/resources/scripts/dev_tools/push_down_codex_and_agents_customizations.py +0 -139
- package/resources/scripts/dev_tools/push_down_copilot_customizations.py +0 -504
- package/resources/scripts/dev_tools/push_down_copilot_customizations_filesystem.py +0 -217
- package/resources/scripts/dev_tools/push_down_copilot_customizations_rewrites.py +0 -293
- package/resources/scripts/dev_tools/resolve_file_prompt.py +0 -457
- package/resources/scripts/dev_tools/resolve_hard_lock_prompt.py +0 -444
- package/resources/scripts/dev_tools/validate_orchestration_artifacts.py +0 -246
- package/resources/scripts/dev_tools/validate_orchestration_review_artifacts.py +0 -107
- package/resources/scripts/dev_tools/validate_orchestrator_state.py +0 -428
- package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +0 -448
- package/resources/templates/codex_native_converter.py +0 -35
- package/resources/templates/collect_commit_context.py +0 -212
- package/resources/templates/collect_pr_context.py +0 -74
- package/resources/templates/hello_python.py +0 -11
- package/resources/templates/new_active_feature_folder.py +0 -67
- package/resources/templates/new_potential_bug_entry.py +0 -54
- package/resources/templates/potential_to_issue.py +0 -55
- package/resources/templates/push_down_claude_customizations.py +0 -409
- package/resources/templates/push_down_codex_and_agents_customizations.py +0 -95
- package/resources/templates/push_down_copilot_customizations.py +0 -124
- package/resources/templates/resolve_atomic_plan_prompt.py +0 -75
- package/resources/templates/resolve_hard_lock_prompt.py +0 -65
- package/resources/templates/validate_orchestration_artifacts.py +0 -55
|
@@ -17,12 +17,15 @@
|
|
|
17
17
|
- artifacts/evidence/
|
|
18
18
|
- artifacts/regression-testing/
|
|
19
19
|
- artifacts/post-change/
|
|
20
|
+
- artifacts/research/
|
|
20
21
|
|
|
21
22
|
All other paths pass through, including canonical evidence paths of the form
|
|
22
23
|
<FEATURE>/evidence/<kind>/ and permitted artifacts/ sub-paths such as
|
|
23
|
-
artifacts/orchestration/, artifacts/
|
|
24
|
-
artifacts/
|
|
25
|
-
|
|
24
|
+
artifacts/orchestration/, artifacts/pr_context, artifacts/reviews/,
|
|
25
|
+
artifacts/status/, artifacts/python/, artifacts/pester/, and
|
|
26
|
+
artifacts/csharp/. Research output is no longer an artifacts/ sub-path; it
|
|
27
|
+
is written to the tracked roots docs/features/<feature>/research/
|
|
28
|
+
(feature-associated) or docs/research/ (one-off).
|
|
26
29
|
|
|
27
30
|
If the file_path resolves to a forbidden prefix, the script writes a JSON response
|
|
28
31
|
to stdout with 'decision': 'block' and exits with code 0 so Claude Code surfaces
|
|
@@ -61,7 +64,8 @@ function Test-EvidenceLocationForbidden {
|
|
|
61
64
|
'artifacts/coverage/',
|
|
62
65
|
'artifacts/evidence/',
|
|
63
66
|
'artifacts/regression-testing/',
|
|
64
|
-
'artifacts/post-change/'
|
|
67
|
+
'artifacts/post-change/',
|
|
68
|
+
'artifacts/research/'
|
|
65
69
|
)
|
|
66
70
|
|
|
67
71
|
# Match the prefix either at the start of the string or after any directory separator,
|
|
@@ -133,18 +137,40 @@ function Invoke-EvidenceLocationDecision {
|
|
|
133
137
|
return [ordered]@{ decision = 'allow' }
|
|
134
138
|
}
|
|
135
139
|
|
|
140
|
+
function Invoke-EvidenceLocationEntryPoint {
|
|
141
|
+
<#
|
|
142
|
+
.SYNOPSIS
|
|
143
|
+
Runs the evidence-location decision and returns the process exit code.
|
|
144
|
+
.DESCRIPTION
|
|
145
|
+
Wraps the dispatch logic that the hook entry point performs so it can be
|
|
146
|
+
exercised by unit tests. On success it writes the compact JSON decision to
|
|
147
|
+
the output stream and returns 0. On a hard failure (malformed JSON) it
|
|
148
|
+
writes the error record and returns 1. This function does not call exit;
|
|
149
|
+
the thin entry-point wiring converts the returned code into a process exit.
|
|
150
|
+
.PARAMETER ToolInputRaw
|
|
151
|
+
The raw JSON string from $env:CLAUDE_TOOL_INPUT.
|
|
152
|
+
#>
|
|
153
|
+
[CmdletBinding()]
|
|
154
|
+
[OutputType([int])]
|
|
155
|
+
param(
|
|
156
|
+
[string] $ToolInputRaw = $env:CLAUDE_TOOL_INPUT
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
$decision = Invoke-EvidenceLocationDecision -ToolInputRaw $ToolInputRaw
|
|
161
|
+
} catch {
|
|
162
|
+
Write-Error $_
|
|
163
|
+
return 1
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
$decision | ConvertTo-Json -Compress | Write-Output
|
|
167
|
+
|
|
168
|
+
return 0
|
|
169
|
+
}
|
|
170
|
+
|
|
136
171
|
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
137
172
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
138
173
|
return
|
|
139
174
|
}
|
|
140
175
|
|
|
141
|
-
|
|
142
|
-
$decision = Invoke-EvidenceLocationDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
143
|
-
} catch {
|
|
144
|
-
Write-Error $_
|
|
145
|
-
exit 1
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
$decision | ConvertTo-Json -Compress | Write-Output
|
|
149
|
-
|
|
150
|
-
exit 0
|
|
176
|
+
exit (Invoke-EvidenceLocationEntryPoint)
|
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
|
|
10
|
+
function ConvertFrom-CheckpointJson {
|
|
11
|
+
[CmdletBinding()]
|
|
12
|
+
param([Parameter(Mandatory)][string] $Json)
|
|
13
|
+
|
|
14
|
+
return $Json | ConvertFrom-Json -ErrorAction Stop
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function Get-StringProperty {
|
|
18
|
+
[CmdletBinding()]
|
|
19
|
+
[OutputType([string])]
|
|
20
|
+
param(
|
|
21
|
+
[Parameter(Mandatory)][AllowNull()] $Value,
|
|
22
|
+
[Parameter(Mandatory)][string] $Name
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
if ($null -eq $Value -or -not ($Value.PSObject.Properties.Name -contains $Name)) {
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
return ([string]$Value.$Name).Trim()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Test-FeatureDocumentationOrEvidencePath {
|
|
32
|
+
[CmdletBinding()]
|
|
33
|
+
[OutputType([bool])]
|
|
34
|
+
param([Parameter(Mandatory)][string] $NormalizedPath)
|
|
35
|
+
|
|
36
|
+
return $NormalizedPath.StartsWith('docs/features/active/')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function Test-ImplementationPath {
|
|
40
|
+
[CmdletBinding()]
|
|
41
|
+
[OutputType([bool])]
|
|
42
|
+
param([Parameter(Mandatory)][string] $NormalizedPath)
|
|
43
|
+
|
|
44
|
+
if (Test-FeatureDocumentationOrEvidencePath -NormalizedPath $NormalizedPath) {
|
|
45
|
+
return $false
|
|
46
|
+
}
|
|
47
|
+
if ($NormalizedPath -eq $script:CheckpointPath) {
|
|
48
|
+
return $false
|
|
49
|
+
}
|
|
50
|
+
return $NormalizedPath -match '\.(py|ps1|psm1|ts|tsx|js|jsx|cs|json|yml|yaml)$'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function Test-ImplementationCommand {
|
|
54
|
+
[CmdletBinding()]
|
|
55
|
+
[OutputType([bool])]
|
|
56
|
+
param([Parameter(Mandatory)][string] $Command)
|
|
57
|
+
|
|
58
|
+
$normalizedCommand = $Command.Trim()
|
|
59
|
+
if (-not $normalizedCommand) {
|
|
60
|
+
return $false
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
$implementationCommandPatterns = @(
|
|
64
|
+
'(^|\s)git\s+(add|commit)\b',
|
|
65
|
+
'(^|\s)(poetry\s+run\s+)?(black|ruff|pyright|pytest)\b',
|
|
66
|
+
'(^|\s)npm\s+.*\s+(prettier|lint|typecheck|test:unit)\b',
|
|
67
|
+
'(^|\s)npx\s+(prettier|eslint|tsc|jest)\b',
|
|
68
|
+
'(^|\s)pwsh\s+.*(Invoke-Pester|tests/scripts/)'
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
foreach ($pattern in $implementationCommandPatterns) {
|
|
72
|
+
if ($normalizedCommand -match $pattern) {
|
|
73
|
+
return $true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return $false
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function Test-ImplementationDelegation {
|
|
80
|
+
[CmdletBinding()]
|
|
81
|
+
[OutputType([bool])]
|
|
82
|
+
param([Parameter(Mandatory)][AllowNull()] $ToolInput)
|
|
83
|
+
|
|
84
|
+
if ($null -eq $ToolInput) {
|
|
85
|
+
return $false
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
$payloadText = ($ToolInput | ConvertTo-Json -Depth 20 -Compress)
|
|
89
|
+
return $payloadText -match '(python-typed-engineer|powershell-typed-engineer|typescript-engineer|csharp-typed-engineer|atomic-executor|implementation|execute)'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function Test-OrchestrationReady {
|
|
93
|
+
[CmdletBinding()]
|
|
94
|
+
[OutputType([bool])]
|
|
95
|
+
param([Parameter(Mandatory)][AllowNull()] $Payload)
|
|
96
|
+
|
|
97
|
+
if ($null -eq $Payload) {
|
|
98
|
+
return $false
|
|
99
|
+
}
|
|
100
|
+
$issueNum = Get-StringProperty -Value $Payload -Name 'issue-num'
|
|
101
|
+
$featureFolder = Get-StringProperty -Value $Payload -Name 'feature-folder'
|
|
102
|
+
$routeId = Get-StringProperty -Value $Payload -Name 'route_id'
|
|
103
|
+
if (-not $routeId) {
|
|
104
|
+
$routeId = Get-StringProperty -Value $Payload -Name 'path_selected'
|
|
105
|
+
}
|
|
106
|
+
$lifecycleReady = $false
|
|
107
|
+
if ($Payload.PSObject.Properties.Name -contains 'lifecycle_ready') {
|
|
108
|
+
$lifecycleReady = [bool]$Payload.lifecycle_ready
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (-not $issueNum -or -not $featureFolder -or -not $routeId -or -not $lifecycleReady) {
|
|
112
|
+
return $false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
$featureFolder.StartsWith('docs/features/active/') -and
|
|
117
|
+
$routeId -and
|
|
118
|
+
$lifecycleReady
|
|
119
|
+
)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function Get-CheckpointContent {
|
|
123
|
+
[CmdletBinding()]
|
|
124
|
+
[OutputType([string])]
|
|
125
|
+
param()
|
|
126
|
+
|
|
127
|
+
if (-not (Test-Path -LiteralPath $script:CheckpointPath)) {
|
|
128
|
+
return ''
|
|
129
|
+
}
|
|
130
|
+
return Get-Content -Raw -LiteralPath $script:CheckpointPath
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function Invoke-OrchestrationPreimplementationGateDecision {
|
|
134
|
+
[CmdletBinding()]
|
|
135
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
136
|
+
param(
|
|
137
|
+
[string] $ToolInputRaw,
|
|
138
|
+
[string] $CheckpointRaw
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if (-not $ToolInputRaw) {
|
|
142
|
+
return [ordered]@{ decision = 'allow' }
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
146
|
+
} catch {
|
|
147
|
+
throw "enforce-orchestration-preimplementation-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
$requiresReadyCheckpoint = $false
|
|
151
|
+
$filePath = Get-StringProperty -Value $toolInput -Name 'file_path'
|
|
152
|
+
if ($filePath) {
|
|
153
|
+
$normalized = ([string]$filePath) -replace '\\', '/'
|
|
154
|
+
$requiresReadyCheckpoint = Test-ImplementationPath -NormalizedPath $normalized
|
|
155
|
+
} else {
|
|
156
|
+
$command = Get-StringProperty -Value $toolInput -Name 'command'
|
|
157
|
+
if ($command) {
|
|
158
|
+
$requiresReadyCheckpoint = Test-ImplementationCommand -Command $command
|
|
159
|
+
} else {
|
|
160
|
+
$requiresReadyCheckpoint = Test-ImplementationDelegation -ToolInput $toolInput
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (-not $requiresReadyCheckpoint) {
|
|
165
|
+
return [ordered]@{ decision = 'allow' }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (-not $CheckpointRaw) {
|
|
169
|
+
$CheckpointRaw = Get-CheckpointContent
|
|
170
|
+
}
|
|
171
|
+
try {
|
|
172
|
+
$checkpoint = ConvertFrom-CheckpointJson -Json $CheckpointRaw
|
|
173
|
+
} catch {
|
|
174
|
+
$checkpoint = $null
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (Test-OrchestrationReady -Payload $checkpoint) {
|
|
178
|
+
return [ordered]@{ decision = 'allow' }
|
|
179
|
+
}
|
|
180
|
+
return [ordered]@{
|
|
181
|
+
decision = 'block'
|
|
182
|
+
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.'
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
$decision = Invoke-OrchestrationPreimplementationGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
192
|
+
} catch {
|
|
193
|
+
Write-Error $_
|
|
194
|
+
exit 1
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
$decision | ConvertTo-Json -Compress | Write-Output
|
|
198
|
+
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
|
|
|
@@ -141,6 +141,58 @@ function Test-HumanInteractionShape {
|
|
|
141
141
|
return @{ Ok = $true; Message = $null }
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
function Invoke-RoutingContractValidation {
|
|
145
|
+
<#
|
|
146
|
+
.SYNOPSIS
|
|
147
|
+
Runs the authoritative Python routing-contract validator against the
|
|
148
|
+
on-disk checkpoint and reports whether it emitted errors.
|
|
149
|
+
.DESCRIPTION
|
|
150
|
+
Invokes the validator through an injectable subprocess scriptblock seam.
|
|
151
|
+
The default Invoker runs the authoritative Python CLI:
|
|
152
|
+
python -m scripts.dev_tools.validate_orchestration_artifacts \
|
|
153
|
+
orchestrator-state <CheckpointPath> --require-complete
|
|
154
|
+
Tests inject a mock scriptblock so no Python process runs. The function
|
|
155
|
+
does not reimplement routing logic; it delegates to the Python validator.
|
|
156
|
+
|
|
157
|
+
Returns a hashtable with keys:
|
|
158
|
+
- HasErrors: $true when the validator reported a non-zero exit or
|
|
159
|
+
produced any error text; $false when clean.
|
|
160
|
+
- ErrorText: the validator's combined output text (empty on success).
|
|
161
|
+
#>
|
|
162
|
+
[CmdletBinding()]
|
|
163
|
+
[OutputType([hashtable])]
|
|
164
|
+
param(
|
|
165
|
+
[Parameter(Mandatory = $true)]
|
|
166
|
+
[string] $CheckpointPath,
|
|
167
|
+
|
|
168
|
+
[Parameter(Mandatory = $false)]
|
|
169
|
+
[scriptblock] $Invoker = {
|
|
170
|
+
param($Path)
|
|
171
|
+
$output = & python -m scripts.dev_tools.validate_orchestration_artifacts `
|
|
172
|
+
orchestrator-state $Path --require-complete 2>&1
|
|
173
|
+
[pscustomobject]@{
|
|
174
|
+
ExitCode = $LASTEXITCODE
|
|
175
|
+
Output = ($output | Out-String)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
$result = & $Invoker $CheckpointPath
|
|
181
|
+
$exitCode = 0
|
|
182
|
+
if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'ExitCode')) {
|
|
183
|
+
$exitCode = [int]$result.ExitCode
|
|
184
|
+
}
|
|
185
|
+
$outputText = ''
|
|
186
|
+
if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Output')) {
|
|
187
|
+
$outputText = ([string]$result.Output).Trim()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# The validator signals a routing-contract failure either through a non-zero
|
|
191
|
+
# exit code or through emitted error text; either condition blocks DONE.
|
|
192
|
+
$hasErrors = ($exitCode -ne 0) -or (-not [string]::IsNullOrWhiteSpace($outputText))
|
|
193
|
+
return @{ HasErrors = $hasErrors; ErrorText = $outputText }
|
|
194
|
+
}
|
|
195
|
+
|
|
144
196
|
function Invoke-OrchestratorOutputValidation {
|
|
145
197
|
<#
|
|
146
198
|
.SYNOPSIS
|
|
@@ -154,7 +206,10 @@ function Invoke-OrchestratorOutputValidation {
|
|
|
154
206
|
[OutputType([hashtable])]
|
|
155
207
|
param(
|
|
156
208
|
[string] $RawPayload,
|
|
157
|
-
[string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
209
|
+
[string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json',
|
|
210
|
+
|
|
211
|
+
[Parameter(Mandatory = $false)]
|
|
212
|
+
[scriptblock] $RoutingInvoker
|
|
158
213
|
)
|
|
159
214
|
|
|
160
215
|
if ([string]::IsNullOrWhiteSpace($RawPayload)) {
|
|
@@ -217,6 +272,18 @@ function Invoke-OrchestratorOutputValidation {
|
|
|
217
272
|
return @{ Ok = $false; Message = $hiResult.Message }
|
|
218
273
|
}
|
|
219
274
|
|
|
275
|
+
# Delegate to the authoritative Python routing-contract validator. The
|
|
276
|
+
# optional RoutingInvoker seam lets tests inject a mock; the default seam
|
|
277
|
+
# produces the real subprocess call.
|
|
278
|
+
$routingArgs = @{ CheckpointPath = $CheckpointPath }
|
|
279
|
+
if ($PSBoundParameters.ContainsKey('RoutingInvoker') -and $null -ne $RoutingInvoker) {
|
|
280
|
+
$routingArgs['Invoker'] = $RoutingInvoker
|
|
281
|
+
}
|
|
282
|
+
$routingResult = Invoke-RoutingContractValidation @routingArgs
|
|
283
|
+
if ($routingResult.HasErrors) {
|
|
284
|
+
return @{ Ok = $false; Message = "ROUTING_CONTRACT_BLOCKED: $($routingResult.ErrorText)" }
|
|
285
|
+
}
|
|
286
|
+
|
|
220
287
|
return @{ Ok = $true; Message = $null }
|
|
221
288
|
}
|
|
222
289
|
|