@danmoisan/drm-copilot-mcp 1.0.1 → 1.0.8
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 +896 -202
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/atomic-executor.md +1 -0
- package/resources/claude-customizations/.claude/agents/atomic-planner.md +1 -0
- package/resources/claude-customizations/.claude/agents/commit-message.md +42 -0
- package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +128 -0
- package/resources/claude-customizations/.claude/agents/epic-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/feature-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/human-exception-runbook.md +48 -0
- package/resources/claude-customizations/.claude/agents/orchestrator.md +80 -2
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/pr-author.md +37 -21
- package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -0
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/staged-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/status-updater.md +1 -0
- package/resources/claude-customizations/.claude/agents/typescript-engineer.md +1 -0
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +103 -65
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +12 -9
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +24 -16
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +19 -13
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +304 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +304 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +237 -0
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +16 -12
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +14 -10
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +182 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +35 -8
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +17 -14
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +104 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +268 -93
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +19 -11
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +28 -6
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +17 -14
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +146 -36
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +27 -7
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +53 -0
- package/resources/claude-customizations/.claude/settings.json +38 -1
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +252 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +59 -13
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +28 -0
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +22 -18
- package/resources/claude-customizations/pack-manifests/core.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +8 -2
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +24 -3
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +65 -0
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +79 -0
- package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +66 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +15 -14
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -25
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +144 -28
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +163 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +257 -93
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +98 -0
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +24 -0
- package/resources/codex-and-agents-customizations/pack-manifests/csharp-legacy.json +11 -0
- package/resources/codex-and-agents-customizations/pack-manifests/csharp-modern.json +10 -0
- package/resources/codex-and-agents-customizations/pack-manifests/powershell.json +15 -0
- package/resources/codex-and-agents-customizations/pack-manifests/python.json +15 -0
- package/resources/codex-and-agents-customizations/pack-manifests/typescript.json +9 -0
- package/resources/config/orchestration-routing.json +92 -0
- package/resources/customizations/.github/agents/pr-author.agent.md +24 -18
- package/resources/icon.png +0 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +11 -0
- package/resources/codex-and-agents-customizations/.github/workflows/_validate-orchestrator-state.yml +0 -68
- package/resources/codex-and-agents-customizations/.github/workflows/validate-orchestrator-state.yml +0 -15
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pre-tool-use hook that gates git worktree remove behind epic checkpoint merge state.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
|
|
7
|
+
command runs. Regex-matches git worktree remove against CLAUDE_TOOL_INPUT.command,
|
|
8
|
+
extracts the target worktree path argument, reads
|
|
9
|
+
artifacts/orchestration/epic-orchestrator-state.json, and finds the features[] record
|
|
10
|
+
whose worktree_path matches. Allows removal only when that record's merge_status is
|
|
11
|
+
merged or worktree_removed. Denies with reason EPIC_WORKTREE_REMOVAL_BLOCKED when the
|
|
12
|
+
checkpoint is unreadable, no matching record exists, or merge_status is anything else -
|
|
13
|
+
fail-closed, following the enforce-orchestration-preimplementation-gate.ps1 precedent of
|
|
14
|
+
treating an unreadable/no-match checkpoint as deny.
|
|
15
|
+
|
|
16
|
+
.NOTES
|
|
17
|
+
Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
|
|
18
|
+
through an injectable wrapper function so tests can mock the boundary without writing
|
|
19
|
+
temporary files.
|
|
20
|
+
#>
|
|
21
|
+
[CmdletBinding()]
|
|
22
|
+
param()
|
|
23
|
+
|
|
24
|
+
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
25
|
+
$script:AllowedMergeStatuses = @('merged', 'worktree_removed')
|
|
26
|
+
|
|
27
|
+
function Get-EpicWorktreeGateCheckpointContent {
|
|
28
|
+
<#
|
|
29
|
+
.SYNOPSIS
|
|
30
|
+
Read the raw JSON text of the epic checkpoint. Tests mock this function
|
|
31
|
+
(read seam).
|
|
32
|
+
.OUTPUTS
|
|
33
|
+
System.String or $null
|
|
34
|
+
#>
|
|
35
|
+
[CmdletBinding()]
|
|
36
|
+
[OutputType([string])]
|
|
37
|
+
param()
|
|
38
|
+
|
|
39
|
+
if (-not (Test-Path -LiteralPath $script:EpicCheckpointPath -PathType Leaf)) {
|
|
40
|
+
return $null
|
|
41
|
+
}
|
|
42
|
+
return (Get-Content -LiteralPath $script:EpicCheckpointPath -Raw)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function Get-EpicWorktreeRemovalCommandPath {
|
|
46
|
+
<#
|
|
47
|
+
.SYNOPSIS
|
|
48
|
+
Extract the target worktree path argument from a git worktree remove command.
|
|
49
|
+
.PARAMETER CommandText
|
|
50
|
+
The Bash command text under evaluation.
|
|
51
|
+
.OUTPUTS
|
|
52
|
+
System.String or $null
|
|
53
|
+
#>
|
|
54
|
+
[CmdletBinding()]
|
|
55
|
+
[OutputType([string])]
|
|
56
|
+
param(
|
|
57
|
+
[Parameter(Mandatory)]
|
|
58
|
+
[string] $CommandText
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if ($CommandText -match '(?i)\bgit\s+worktree\s+remove\s+(?<path>\S+)') {
|
|
62
|
+
return $Matches['path'].Trim('"''')
|
|
63
|
+
}
|
|
64
|
+
return $null
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Find-EpicWorktreeFeatureRecord {
|
|
68
|
+
<#
|
|
69
|
+
.SYNOPSIS
|
|
70
|
+
Locate the features[] record whose worktree_path matches the target path.
|
|
71
|
+
.PARAMETER Checkpoint
|
|
72
|
+
Parsed epic checkpoint, or $null when absent/unreadable.
|
|
73
|
+
.PARAMETER WorktreePath
|
|
74
|
+
The target worktree path extracted from the command text.
|
|
75
|
+
.OUTPUTS
|
|
76
|
+
System.Object or $null
|
|
77
|
+
#>
|
|
78
|
+
[CmdletBinding()]
|
|
79
|
+
param(
|
|
80
|
+
[AllowNull()]
|
|
81
|
+
$Checkpoint,
|
|
82
|
+
|
|
83
|
+
[AllowNull()]
|
|
84
|
+
[string] $WorktreePath
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
|
|
88
|
+
return $null
|
|
89
|
+
}
|
|
90
|
+
$checkpointProps = @($Checkpoint.PSObject.Properties.Name)
|
|
91
|
+
if ($checkpointProps -notcontains 'features') {
|
|
92
|
+
return $null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
$normalizedTarget = ($WorktreePath -replace '\\', '/').TrimEnd('/')
|
|
96
|
+
|
|
97
|
+
# Scan every recorded feature for a worktree_path that matches the removal target;
|
|
98
|
+
# path separators are normalized so Windows- and POSIX-style paths compare equal.
|
|
99
|
+
foreach ($feature in @($Checkpoint.features)) {
|
|
100
|
+
$featureProps = @($feature.PSObject.Properties.Name)
|
|
101
|
+
if ($featureProps -notcontains 'worktree_path') {
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
$normalizedFeaturePath = (([string]$feature.worktree_path) -replace '\\', '/').TrimEnd('/')
|
|
105
|
+
if ($normalizedFeaturePath -eq $normalizedTarget) {
|
|
106
|
+
return $feature
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return $null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function Test-EpicWorktreeRemovalAllowed {
|
|
113
|
+
<#
|
|
114
|
+
.SYNOPSIS
|
|
115
|
+
Decision logic: allow only when the matching feature record's merge_status is
|
|
116
|
+
merged or worktree_removed.
|
|
117
|
+
.PARAMETER FeatureRecord
|
|
118
|
+
The matched features[] record, or $null when no match was found.
|
|
119
|
+
.OUTPUTS
|
|
120
|
+
System.Boolean
|
|
121
|
+
#>
|
|
122
|
+
[CmdletBinding()]
|
|
123
|
+
[OutputType([bool])]
|
|
124
|
+
param(
|
|
125
|
+
[AllowNull()]
|
|
126
|
+
$FeatureRecord
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
if ($null -eq $FeatureRecord) {
|
|
130
|
+
return $false
|
|
131
|
+
}
|
|
132
|
+
$props = @($FeatureRecord.PSObject.Properties.Name)
|
|
133
|
+
if ($props -notcontains 'merge_status') {
|
|
134
|
+
return $false
|
|
135
|
+
}
|
|
136
|
+
return $script:AllowedMergeStatuses -contains ([string]$FeatureRecord.merge_status)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function Get-EpicWorktreeGateAllowDecision {
|
|
140
|
+
[CmdletBinding()]
|
|
141
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
142
|
+
param()
|
|
143
|
+
|
|
144
|
+
return [ordered]@{
|
|
145
|
+
hookSpecificOutput = [ordered]@{
|
|
146
|
+
hookEventName = 'PreToolUse'
|
|
147
|
+
permissionDecision = 'allow'
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function Get-EpicWorktreeGateBlockDecision {
|
|
153
|
+
[CmdletBinding()]
|
|
154
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
155
|
+
param(
|
|
156
|
+
[Parameter(Mandatory)]
|
|
157
|
+
[string] $Reason
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return [ordered]@{
|
|
161
|
+
hookSpecificOutput = [ordered]@{
|
|
162
|
+
hookEventName = 'PreToolUse'
|
|
163
|
+
permissionDecision = 'deny'
|
|
164
|
+
permissionDecisionReason = $Reason
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function Invoke-EpicWorktreeRemovalGateDecision {
|
|
170
|
+
<#
|
|
171
|
+
.SYNOPSIS
|
|
172
|
+
Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision.
|
|
173
|
+
.PARAMETER ToolInputRaw
|
|
174
|
+
The raw JSON tool payload supplied by Claude Code.
|
|
175
|
+
.OUTPUTS
|
|
176
|
+
System.Collections.Specialized.OrderedDictionary
|
|
177
|
+
#>
|
|
178
|
+
[CmdletBinding()]
|
|
179
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
180
|
+
param(
|
|
181
|
+
[string] $ToolInputRaw
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if (-not $ToolInputRaw) {
|
|
185
|
+
return Get-EpicWorktreeGateAllowDecision
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
190
|
+
} catch {
|
|
191
|
+
throw "enforce-epic-worktree-removal-gate hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
$commandText = $toolInput.command
|
|
195
|
+
if (-not $commandText) {
|
|
196
|
+
return Get-EpicWorktreeGateAllowDecision
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if ($commandText -notmatch '(?i)\bgit\s+worktree\s+remove\b') {
|
|
200
|
+
return Get-EpicWorktreeGateAllowDecision
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
$worktreePath = Get-EpicWorktreeRemovalCommandPath -CommandText $commandText
|
|
204
|
+
|
|
205
|
+
$checkpointRaw = Get-EpicWorktreeGateCheckpointContent
|
|
206
|
+
$checkpoint = $null
|
|
207
|
+
if (-not [string]::IsNullOrWhiteSpace($checkpointRaw)) {
|
|
208
|
+
try {
|
|
209
|
+
$checkpoint = $checkpointRaw | ConvertFrom-Json -ErrorAction Stop
|
|
210
|
+
} catch {
|
|
211
|
+
$checkpoint = $null
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
$featureRecord = Find-EpicWorktreeFeatureRecord -Checkpoint $checkpoint -WorktreePath $worktreePath
|
|
216
|
+
if (Test-EpicWorktreeRemovalAllowed -FeatureRecord $featureRecord) {
|
|
217
|
+
return Get-EpicWorktreeGateAllowDecision
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return Get-EpicWorktreeGateBlockDecision -Reason "EPIC_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires a matching epic checkpoint features[] record with merge_status in {merged, worktree_removed}. The checkpoint was unreadable, no matching record was found, or merge_status was not yet safe for removal."
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
224
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
$decision = Invoke-EpicWorktreeRemovalGateDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
230
|
+
} catch {
|
|
231
|
+
Write-Error $_
|
|
232
|
+
exit 1
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
236
|
+
|
|
237
|
+
exit 0
|
|
@@ -27,10 +27,11 @@
|
|
|
27
27
|
is written to the tracked roots docs/features/<feature>/research/
|
|
28
28
|
(feature-associated) or docs/research/ (one-off).
|
|
29
29
|
|
|
30
|
-
If the file_path resolves to a forbidden prefix, the script writes a JSON
|
|
31
|
-
to stdout with
|
|
32
|
-
the reason. For allowed paths,
|
|
33
|
-
|
|
30
|
+
If the file_path resolves to a forbidden prefix, the script writes a PreToolUse JSON
|
|
31
|
+
response to stdout with hookSpecificOutput.permissionDecision = 'deny' and exits with
|
|
32
|
+
code 0 so Claude Code surfaces the reason. For allowed paths, a PreToolUse response
|
|
33
|
+
with permissionDecision = 'allow' is written to stdout and the script exits 0. On hard
|
|
34
|
+
failure (malformed JSON input), the script exits 1.
|
|
34
35
|
|
|
35
36
|
.NOTES
|
|
36
37
|
Compatible with PowerShell 7+.
|
|
@@ -83,9 +84,9 @@ function Test-EvidenceLocationForbidden {
|
|
|
83
84
|
function Get-EvidenceLocationBlockDecision {
|
|
84
85
|
<#
|
|
85
86
|
.SYNOPSIS
|
|
86
|
-
Constructs a
|
|
87
|
+
Constructs a deny-decision ordered dictionary for the supplied forbidden path.
|
|
87
88
|
.PARAMETER FilePath
|
|
88
|
-
The file path that triggered the
|
|
89
|
+
The file path that triggered the deny decision.
|
|
89
90
|
#>
|
|
90
91
|
[CmdletBinding()]
|
|
91
92
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -95,8 +96,11 @@ function Get-EvidenceLocationBlockDecision {
|
|
|
95
96
|
)
|
|
96
97
|
|
|
97
98
|
[ordered]@{
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
hookSpecificOutput = [ordered]@{
|
|
100
|
+
hookEventName = 'PreToolUse'
|
|
101
|
+
permissionDecision = 'deny'
|
|
102
|
+
permissionDecisionReason = "EVIDENCE_LOCATION_BLOCKED: '$FilePath' is not a canonical evidence location. Use <FEATURE>/evidence/<kind>/ instead. See .claude/skills/evidence-and-timestamp-conventions/SKILL.md for the canonical scheme."
|
|
103
|
+
}
|
|
100
104
|
}
|
|
101
105
|
}
|
|
102
106
|
|
|
@@ -115,7 +119,7 @@ function Invoke-EvidenceLocationDecision {
|
|
|
115
119
|
)
|
|
116
120
|
|
|
117
121
|
if (-not $ToolInputRaw) {
|
|
118
|
-
return [ordered]@{
|
|
122
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
119
123
|
}
|
|
120
124
|
|
|
121
125
|
try {
|
|
@@ -127,14 +131,14 @@ function Invoke-EvidenceLocationDecision {
|
|
|
127
131
|
|
|
128
132
|
$filePath = $toolInput.file_path
|
|
129
133
|
if (-not $filePath) {
|
|
130
|
-
return [ordered]@{
|
|
134
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
if (Test-EvidenceLocationForbidden -FilePath $filePath) {
|
|
134
138
|
return Get-EvidenceLocationBlockDecision -FilePath $filePath
|
|
135
139
|
}
|
|
136
140
|
|
|
137
|
-
return [ordered]@{
|
|
141
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
138
142
|
}
|
|
139
143
|
|
|
140
144
|
function Invoke-EvidenceLocationEntryPoint {
|
|
@@ -163,7 +167,7 @@ function Invoke-EvidenceLocationEntryPoint {
|
|
|
163
167
|
return 1
|
|
164
168
|
}
|
|
165
169
|
|
|
166
|
-
$decision | ConvertTo-Json -Compress | Write-Output
|
|
170
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
167
171
|
|
|
168
172
|
return 0
|
|
169
173
|
}
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
docs/features/(active|archive)/<folder>/plan.md, the script verifies that
|
|
11
11
|
each of issue.md, spec.md, and user-story.md exists in the same folder.
|
|
12
12
|
|
|
13
|
-
If any of the three sibling files is missing, the script emits a
|
|
14
|
-
response with
|
|
15
|
-
reason. All other paths pass through with
|
|
13
|
+
If any of the three sibling files is missing, the script emits a PreToolUse
|
|
14
|
+
JSON response with hookSpecificOutput.permissionDecision='deny' and exits 0 so
|
|
15
|
+
Claude Code surfaces the reason. All other paths pass through with
|
|
16
|
+
permissionDecision='allow'.
|
|
16
17
|
|
|
17
18
|
Filesystem reads go through Get-FeatureFolderFileExistence so tests can
|
|
18
19
|
inject a fake without touching disk.
|
|
@@ -97,7 +98,7 @@ function Invoke-FeatureFolderOrderDecision {
|
|
|
97
98
|
)
|
|
98
99
|
|
|
99
100
|
if (-not $ToolInputRaw) {
|
|
100
|
-
return [ordered]@{
|
|
101
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
try {
|
|
@@ -109,24 +110,27 @@ function Invoke-FeatureFolderOrderDecision {
|
|
|
109
110
|
|
|
110
111
|
$filePath = $toolInput.file_path
|
|
111
112
|
if (-not $filePath) {
|
|
112
|
-
return [ordered]@{
|
|
113
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
113
114
|
}
|
|
114
115
|
|
|
115
116
|
$normalized = $filePath -replace '\\', '/'
|
|
116
117
|
|
|
117
118
|
if (-not (Test-IsFeaturePlanPath -NormalizedPath $normalized)) {
|
|
118
|
-
return [ordered]@{
|
|
119
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
$missing = Get-FeatureFolderMissingFile -PlanFilePath $normalized
|
|
122
123
|
if ($missing.Count -eq 0) {
|
|
123
|
-
return [ordered]@{
|
|
124
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
124
125
|
}
|
|
125
126
|
|
|
126
127
|
$list = ($missing -join ', ')
|
|
127
128
|
return [ordered]@{
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
hookSpecificOutput = [ordered]@{
|
|
130
|
+
hookEventName = 'PreToolUse'
|
|
131
|
+
permissionDecision = 'deny'
|
|
132
|
+
permissionDecisionReason = "FEATURE_FOLDER_ORDER_BLOCKED: cannot write plan.md before producing prerequisite documents. Missing in feature folder: $list. Invoke the prd-feature subagent to generate the missing file(s) before authoring plan.md."
|
|
133
|
+
}
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
136
|
|
|
@@ -143,6 +147,6 @@ catch {
|
|
|
143
147
|
exit 1
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
$decision | ConvertTo-Json -Compress | Write-Output
|
|
150
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
147
151
|
|
|
148
152
|
exit 0
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pre-tool-use hook that blocks a delegation to a gated subagent when the
|
|
4
|
+
orchestrator checkpoint records no model-routing receipt for that agent.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
|
|
8
|
+
tool input JSON from the CLAUDE_TOOL_INPUT environment variable and the
|
|
9
|
+
orchestrator checkpoint from artifacts/orchestration/orchestrator-state.json.
|
|
10
|
+
|
|
11
|
+
The hook enforces presence only: it cannot read the delegate's chosen
|
|
12
|
+
`model` (no `model` field is exposed in the tool input), so it verifies that
|
|
13
|
+
a `model_routing_receipts[]` entry already exists for the target
|
|
14
|
+
`subagent_type`. Correctness of the recorded model stays with the
|
|
15
|
+
authoritative Python validator.
|
|
16
|
+
|
|
17
|
+
Gated subagent types are the Agent-tool delegates that participate in model
|
|
18
|
+
selection: atomic-planner, atomic-executor, feature-review, task-researcher,
|
|
19
|
+
prd-feature, pr-author. The `orchestrator` type is deliberately excluded: it
|
|
20
|
+
is the calling agent, not a subagent delegated via the Agent tool, so it is
|
|
21
|
+
never a receipt-gated `subagent_type`.
|
|
22
|
+
|
|
23
|
+
Allow-through (graceful allow) applies to a non-delegating `subagent_type`,
|
|
24
|
+
empty or absent tool input, and malformed tool-input JSON.
|
|
25
|
+
|
|
26
|
+
The checkpoint read goes through Get-ModelRoutingCheckpoint so tests can
|
|
27
|
+
inject a synthetic checkpoint without touching disk.
|
|
28
|
+
|
|
29
|
+
.NOTES
|
|
30
|
+
Compatible with PowerShell 7+. Read-only presence-gating deterrent.
|
|
31
|
+
#>
|
|
32
|
+
[CmdletBinding()]
|
|
33
|
+
param()
|
|
34
|
+
|
|
35
|
+
function Get-ModelRoutingCheckpoint {
|
|
36
|
+
<#
|
|
37
|
+
.SYNOPSIS
|
|
38
|
+
Returns the parsed orchestrator checkpoint object, or $null when the
|
|
39
|
+
file is missing or not valid JSON. Tests mock this seam.
|
|
40
|
+
#>
|
|
41
|
+
[CmdletBinding()]
|
|
42
|
+
[OutputType([object])]
|
|
43
|
+
param(
|
|
44
|
+
[string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if (-not (Test-Path -LiteralPath $CheckpointPath -PathType Leaf)) {
|
|
48
|
+
return $null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
$raw = Get-Content -LiteralPath $CheckpointPath -Raw -ErrorAction Stop
|
|
53
|
+
return $raw | ConvertFrom-Json -ErrorAction Stop
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return $null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Get-ModelRoutingGatedAgent {
|
|
61
|
+
<#
|
|
62
|
+
.SYNOPSIS
|
|
63
|
+
Returns the set of subagent types that are receipt-gated: the Agent-tool
|
|
64
|
+
delegates that participate in model selection. `orchestrator` is
|
|
65
|
+
excluded because it is the caller, not a delegated subagent.
|
|
66
|
+
#>
|
|
67
|
+
[CmdletBinding()]
|
|
68
|
+
[OutputType([string[]])]
|
|
69
|
+
param()
|
|
70
|
+
|
|
71
|
+
return [string[]] @(
|
|
72
|
+
'atomic-planner',
|
|
73
|
+
'atomic-executor',
|
|
74
|
+
'feature-review',
|
|
75
|
+
'task-researcher',
|
|
76
|
+
'prd-feature',
|
|
77
|
+
'pr-author'
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function Test-ModelRoutingReceiptPresent {
|
|
82
|
+
<#
|
|
83
|
+
.SYNOPSIS
|
|
84
|
+
Returns $true when the checkpoint carries a model_routing_receipts entry
|
|
85
|
+
whose agent equals the target subagent type.
|
|
86
|
+
#>
|
|
87
|
+
[CmdletBinding()]
|
|
88
|
+
[OutputType([bool])]
|
|
89
|
+
param(
|
|
90
|
+
[Parameter(Mandatory)]
|
|
91
|
+
[AllowNull()]
|
|
92
|
+
$Checkpoint,
|
|
93
|
+
|
|
94
|
+
[Parameter(Mandatory)]
|
|
95
|
+
[string] $Subagent
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if ($null -eq $Checkpoint) {
|
|
99
|
+
return $false
|
|
100
|
+
}
|
|
101
|
+
if ($Checkpoint.PSObject.Properties.Name -notcontains 'model_routing_receipts') {
|
|
102
|
+
return $false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Scan every receipt for one whose agent matches the delegated subagent type.
|
|
106
|
+
foreach ($receipt in @($Checkpoint.model_routing_receipts)) {
|
|
107
|
+
if ($null -eq $receipt) {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
if ($receipt.PSObject.Properties.Name -contains 'agent' -and
|
|
111
|
+
[string]$receipt.agent -eq $Subagent) {
|
|
112
|
+
return $true
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return $false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Invoke-ModelRoutingReceiptDecision {
|
|
119
|
+
<#
|
|
120
|
+
.SYNOPSIS
|
|
121
|
+
Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision object.
|
|
122
|
+
#>
|
|
123
|
+
[CmdletBinding()]
|
|
124
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
125
|
+
param(
|
|
126
|
+
[string] $ToolInputRaw
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
$allow = [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
130
|
+
|
|
131
|
+
# Empty or absent tool input is not a delegation this hook can gate.
|
|
132
|
+
if (-not $ToolInputRaw) {
|
|
133
|
+
return $allow
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Malformed tool-input JSON is allowed through gracefully; this hook is a
|
|
137
|
+
# deterrent, not the authoritative validator.
|
|
138
|
+
try {
|
|
139
|
+
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return $allow
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
$subagent = [string]$toolInput.subagent_type
|
|
146
|
+
|
|
147
|
+
# Only the gated Agent-tool delegates are receipt-checked; any other
|
|
148
|
+
# subagent_type (including orchestrator) passes through.
|
|
149
|
+
if (-not $subagent -or ((Get-ModelRoutingGatedAgent) -notcontains $subagent)) {
|
|
150
|
+
return $allow
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
$checkpoint = Get-ModelRoutingCheckpoint
|
|
154
|
+
if (Test-ModelRoutingReceiptPresent -Checkpoint $checkpoint -Subagent $subagent) {
|
|
155
|
+
return $allow
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return [ordered]@{
|
|
159
|
+
hookSpecificOutput = [ordered]@{
|
|
160
|
+
hookEventName = 'PreToolUse'
|
|
161
|
+
permissionDecision = 'deny'
|
|
162
|
+
permissionDecisionReason = "MODEL_ROUTING_RECEIPT_BLOCKED: cannot delegate to '$subagent' before a model_routing_receipts entry for it is recorded in the orchestrator checkpoint. Perform Model Selection (record the complexity assessment and routing receipt) before delegating."
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
168
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
$decision = Invoke-ModelRoutingReceiptDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
Write-Error $_
|
|
177
|
+
exit 1
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
181
|
+
|
|
182
|
+
exit 0
|
|
@@ -130,6 +130,36 @@ function Get-CheckpointContent {
|
|
|
130
130
|
return Get-Content -Raw -LiteralPath $script:CheckpointPath
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
function Get-OrchestrationPreimplementationGateAllowDecision {
|
|
134
|
+
[CmdletBinding()]
|
|
135
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
136
|
+
param()
|
|
137
|
+
|
|
138
|
+
return [ordered]@{
|
|
139
|
+
hookSpecificOutput = [ordered]@{
|
|
140
|
+
hookEventName = 'PreToolUse'
|
|
141
|
+
permissionDecision = 'allow'
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function Get-OrchestrationPreimplementationGateBlockDecision {
|
|
147
|
+
[CmdletBinding()]
|
|
148
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
149
|
+
param(
|
|
150
|
+
[Parameter(Mandatory)]
|
|
151
|
+
[string] $Reason
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
return [ordered]@{
|
|
155
|
+
hookSpecificOutput = [ordered]@{
|
|
156
|
+
hookEventName = 'PreToolUse'
|
|
157
|
+
permissionDecision = 'deny'
|
|
158
|
+
permissionDecisionReason = $Reason
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
133
163
|
function Invoke-OrchestrationPreimplementationGateDecision {
|
|
134
164
|
[CmdletBinding()]
|
|
135
165
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -139,7 +169,7 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
139
169
|
)
|
|
140
170
|
|
|
141
171
|
if (-not $ToolInputRaw) {
|
|
142
|
-
return
|
|
172
|
+
return Get-OrchestrationPreimplementationGateAllowDecision
|
|
143
173
|
}
|
|
144
174
|
try {
|
|
145
175
|
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
@@ -162,7 +192,7 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
162
192
|
}
|
|
163
193
|
|
|
164
194
|
if (-not $requiresReadyCheckpoint) {
|
|
165
|
-
return
|
|
195
|
+
return Get-OrchestrationPreimplementationGateAllowDecision
|
|
166
196
|
}
|
|
167
197
|
|
|
168
198
|
if (-not $CheckpointRaw) {
|
|
@@ -175,12 +205,9 @@ function Invoke-OrchestrationPreimplementationGateDecision {
|
|
|
175
205
|
}
|
|
176
206
|
|
|
177
207
|
if (Test-OrchestrationReady -Payload $checkpoint) {
|
|
178
|
-
return
|
|
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.'
|
|
208
|
+
return Get-OrchestrationPreimplementationGateAllowDecision
|
|
183
209
|
}
|
|
210
|
+
return Get-OrchestrationPreimplementationGateBlockDecision -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.'
|
|
184
211
|
}
|
|
185
212
|
|
|
186
213
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
@@ -194,5 +221,5 @@ try {
|
|
|
194
221
|
exit 1
|
|
195
222
|
}
|
|
196
223
|
|
|
197
|
-
$decision | ConvertTo-Json -Compress | Write-Output
|
|
224
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
198
225
|
exit 0
|