@danmoisan/drm-copilot-mcp 1.0.7 → 1.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/out/mcp-server.js +82 -8
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +1 -0
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +1 -0
  5. package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -0
  6. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -0
  7. package/resources/claude-customizations/.claude/agents/epic-review.md +1 -0
  8. package/resources/claude-customizations/.claude/agents/feature-review.md +1 -0
  9. package/resources/claude-customizations/.claude/agents/orchestrator.md +13 -0
  10. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -0
  11. package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -0
  12. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -0
  13. package/resources/claude-customizations/.claude/agents/staged-review.md +1 -0
  14. package/resources/claude-customizations/.claude/agents/status-updater.md +1 -0
  15. package/resources/claude-customizations/.claude/agents/typescript-engineer.md +1 -0
  16. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +182 -0
  17. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +9 -1
  18. package/resources/claude-customizations/.claude/lib/model-routing/ModelRouting.psm1 +209 -0
  19. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +17 -0
  20. package/resources/claude-customizations/.claude/settings.json +4 -0
  21. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +3 -3
  22. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +22 -3
  23. package/resources/claude-customizations/pack-manifests/core.json +3 -1
  24. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +8 -2
  25. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +18 -0
  26. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +13 -0
  27. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +15 -14
  28. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +144 -28
  29. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +163 -0
@@ -23,9 +23,11 @@
23
23
  variables.feature-folder);
24
24
  - a ci_gate object with conclusion == "success" and a non-empty head_sha.
25
25
 
26
- The block reason names the specific missing evidence so the caller can
27
- remediate. When completion is not asserted, the write is allowed
28
- (backward compatibility).
26
+ When completion evidence is missing the hook emits a PreToolUse JSON
27
+ response with hookSpecificOutput.permissionDecision='deny' and a reason that
28
+ names the specific missing evidence so the caller can remediate. When
29
+ completion is not asserted, the write is allowed via
30
+ hookSpecificOutput.permissionDecision='allow' (backward compatibility).
29
31
 
30
32
  Edit tool calls supply only old_string/new_string (a partial patch) and
31
33
  cannot be reliably validated without the full target file content, so they
@@ -39,6 +41,11 @@
39
41
  [CmdletBinding()]
40
42
  param()
41
43
 
44
+ # Dot-source the shared validation helpers. Guarded so a missing file produces a
45
+ # clear error and so dot-sourcing this hook in tests loads the helpers too.
46
+ $script:CompletionHelpersPath = Join-Path $PSScriptRoot 'enforce-completion-helpers.ps1'
47
+ . $script:CompletionHelpersPath
48
+
42
49
  function ConvertFrom-CheckpointJson {
43
50
  <#
44
51
  .SYNOPSIS
@@ -53,6 +60,29 @@ function ConvertFrom-CheckpointJson {
53
60
  return $Json | ConvertFrom-Json -ErrorAction Stop
54
61
  }
55
62
 
63
+ function Get-CheckpointFileContent {
64
+ <#
65
+ .SYNOPSIS
66
+ Reads the on-disk checkpoint content for the read-then-validate Edit path.
67
+ .DESCRIPTION
68
+ Returns the full file text when the path resolves to a file on disk, or
69
+ $null when the file does not exist. Tests inject a CheckpointReader
70
+ scriptblock instead of mocking this function so no temporary files are
71
+ required.
72
+ #>
73
+ [CmdletBinding()]
74
+ [OutputType([string])]
75
+ param(
76
+ [Parameter(Mandatory)]
77
+ [string] $Path
78
+ )
79
+
80
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
81
+ return $null
82
+ }
83
+ return Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
84
+ }
85
+
56
86
  function Test-IsCheckpointPath {
57
87
  [CmdletBinding()]
58
88
  [OutputType([bool])]
@@ -149,7 +179,13 @@ function Get-MissingCompletionEvidence {
149
179
  param(
150
180
  [Parameter(Mandatory)]
151
181
  [AllowNull()]
152
- $Payload
182
+ $Payload,
183
+
184
+ [Parameter(Mandatory = $false)]
185
+ [scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container },
186
+
187
+ [Parameter(Mandatory = $false)]
188
+ [scriptblock] $RoutingMatrixReader
153
189
  )
154
190
 
155
191
  $missing = @()
@@ -158,16 +194,17 @@ function Get-MissingCompletionEvidence {
158
194
  if (-not $issueNum -and $null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'variables')) {
159
195
  $issueNum = Get-CheckpointStringValue -Payload $Payload.variables -Name 'issue-num'
160
196
  }
161
- if (-not $issueNum) {
162
- $missing += 'issue-num'
197
+ if (-not (Test-IsValidIssueNum -Value $issueNum)) {
198
+ # Name the offending value so sentinel/placeholder inputs are explicit.
199
+ $missing += "issue-num value '$issueNum' is not a valid issue number (must be digits-only)"
163
200
  }
164
201
 
165
202
  $featureFolder = Get-CheckpointStringValue -Payload $Payload -Name 'feature-folder'
166
203
  if (-not $featureFolder -and $null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'variables')) {
167
204
  $featureFolder = Get-CheckpointStringValue -Payload $Payload.variables -Name 'feature-folder'
168
205
  }
169
- if (-not $featureFolder) {
170
- $missing += 'feature-folder'
206
+ if (-not (Test-IsValidFeatureFolder -Value $featureFolder -FolderExistsCheck $FolderExistsCheck)) {
207
+ $missing += "feature-folder value '$featureFolder' is not a valid feature folder (must be under docs/features/active/ and exist)"
171
208
  }
172
209
 
173
210
  $ciGate = $null
@@ -188,7 +225,14 @@ function Get-MissingCompletionEvidence {
188
225
  }
189
226
  }
190
227
 
191
- if ($issueNum -eq '232') {
228
+ # PR-gate evidence is required only when the checkpoint's selected route
229
+ # opts into it via requires_pr_gate in the routing matrix. This replaces the
230
+ # former issue-number special-casing with route-driven enforcement.
231
+ $prGateArgs = @{ Payload = $Payload }
232
+ if ($PSBoundParameters.ContainsKey('RoutingMatrixReader') -and $null -ne $RoutingMatrixReader) {
233
+ $prGateArgs['RoutingMatrixReader'] = $RoutingMatrixReader
234
+ }
235
+ if (Test-RouteRequiresPrGate @prGateArgs) {
192
236
  $prGate = $null
193
237
  if ($null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'pr_gate')) {
194
238
  $prGate = $Payload.pr_gate
@@ -213,6 +257,60 @@ function Get-MissingCompletionEvidence {
213
257
  return [string[]]$missing
214
258
  }
215
259
 
260
+ function Resolve-EditedCheckpointContent {
261
+ <#
262
+ .SYNOPSIS
263
+ Returns the patched checkpoint content for an Edit-tool call, or $null
264
+ when the patch cannot be applied against the on-disk checkpoint.
265
+ .DESCRIPTION
266
+ Implements the read-then-validate Edit path. When the tool input carries
267
+ an old_string (an Edit patch), the on-disk checkpoint is read through the
268
+ injectable CheckpointReader seam and the old_string -> new_string
269
+ replacement is applied in memory (no on-disk mutation). Returns $null
270
+ when there is no old_string, the on-disk file does not exist, or the
271
+ old_string is not present in the on-disk content, signalling the caller
272
+ to allow (defer).
273
+ #>
274
+ [CmdletBinding()]
275
+ [OutputType([string])]
276
+ param(
277
+ [Parameter(Mandatory)]
278
+ [AllowNull()]
279
+ $ToolInput,
280
+
281
+ [Parameter(Mandatory)]
282
+ [scriptblock] $CheckpointReader
283
+ )
284
+
285
+ $oldString = $null
286
+ if ($null -ne $ToolInput -and ($ToolInput.PSObject.Properties.Name -contains 'old_string')) {
287
+ $oldString = [string]$ToolInput.old_string
288
+ }
289
+ if ([string]::IsNullOrEmpty($oldString)) {
290
+ return $null
291
+ }
292
+
293
+ $newString = ''
294
+ if ($ToolInput.PSObject.Properties.Name -contains 'new_string') {
295
+ $newString = [string]$ToolInput.new_string
296
+ }
297
+
298
+ $onDisk = & $CheckpointReader 'artifacts/orchestration/orchestrator-state.json'
299
+ if ([string]::IsNullOrEmpty([string]$onDisk)) {
300
+ # The on-disk checkpoint does not exist (or is empty); cannot patch.
301
+ return $null
302
+ }
303
+
304
+ $onDiskText = [string]$onDisk
305
+ if (-not $onDiskText.Contains($oldString)) {
306
+ # The old_string is not present, so the patch does not apply here.
307
+ return $null
308
+ }
309
+
310
+ # Apply the patch in memory using a literal (non-regex) replacement.
311
+ return $onDiskText.Replace($oldString, $newString)
312
+ }
313
+
216
314
  function Invoke-CompletionConsistencyDecision {
217
315
  <#
218
316
  .SYNOPSIS
@@ -222,11 +320,20 @@ function Invoke-CompletionConsistencyDecision {
222
320
  [CmdletBinding()]
223
321
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
224
322
  param(
225
- [string] $ToolInputRaw
323
+ [string] $ToolInputRaw,
324
+
325
+ [Parameter(Mandatory = $false)]
326
+ [scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container },
327
+
328
+ [Parameter(Mandatory = $false)]
329
+ [scriptblock] $CheckpointReader = { param($Path) Get-CheckpointFileContent -Path $Path },
330
+
331
+ [Parameter(Mandatory = $false)]
332
+ [scriptblock] $RoutingMatrixReader
226
333
  )
227
334
 
228
335
  if (-not $ToolInputRaw) {
229
- return [ordered]@{ decision = 'allow' }
336
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
230
337
  }
231
338
 
232
339
  try {
@@ -238,19 +345,25 @@ function Invoke-CompletionConsistencyDecision {
238
345
 
239
346
  $filePath = $toolInput.file_path
240
347
  if (-not $filePath) {
241
- return [ordered]@{ decision = 'allow' }
348
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
242
349
  }
243
350
 
244
351
  $normalized = $filePath -replace '\\', '/'
245
352
  if (-not (Test-IsCheckpointPath -NormalizedPath $normalized)) {
246
- return [ordered]@{ decision = 'allow' }
353
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
247
354
  }
248
355
 
249
- # Write tool: validate the content payload. Edit tool: partial new_string is
250
- # not reliable without the full target file content, so allow.
356
+ # Write tool: validate the content payload directly. Edit tool: no content is
357
+ # supplied, so read the on-disk checkpoint through the injectable seam and
358
+ # apply the old_string -> new_string patch in memory (read-then-validate).
251
359
  $content = $toolInput.content
252
360
  if (-not $content) {
253
- return [ordered]@{ decision = 'allow' }
361
+ $content = Resolve-EditedCheckpointContent -ToolInput $toolInput -CheckpointReader $CheckpointReader
362
+ if (-not $content) {
363
+ # No content, and the Edit could not be resolved against on-disk
364
+ # state (missing file or non-matching patch): defer and allow.
365
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
366
+ }
254
367
  }
255
368
 
256
369
  try {
@@ -259,26 +372,29 @@ function Invoke-CompletionConsistencyDecision {
259
372
  catch {
260
373
  # The content itself is not valid JSON. Let downstream tools surface the
261
374
  # error rather than blocking with a misleading reason here.
262
- return [ordered]@{ decision = 'allow' }
375
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
263
376
  }
264
377
 
265
378
  if (-not (Test-CompletionAsserted -Payload $payload)) {
266
- return [ordered]@{ decision = 'allow' }
379
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
267
380
  }
268
381
 
269
- $missing = Get-MissingCompletionEvidence -Payload $payload
270
- if ($missing.Count -eq 0) {
271
- return [ordered]@{ decision = 'allow' }
382
+ $missingArgs = @{ Payload = $payload; FolderExistsCheck = $FolderExistsCheck }
383
+ if ($PSBoundParameters.ContainsKey('RoutingMatrixReader') -and $null -ne $RoutingMatrixReader) {
384
+ $missingArgs['RoutingMatrixReader'] = $RoutingMatrixReader
272
385
  }
273
-
274
- $issueContext = ''
275
- if ((Get-CheckpointStringValue -Payload $payload -Name 'issue-num') -eq '232') {
276
- $issueContext = ' Issue #232 requires pr_gate evidence and current-head ci_gate evidence.'
386
+ $missing = Get-MissingCompletionEvidence @missingArgs
387
+ if ($missing.Count -eq 0) {
388
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
277
389
  }
278
390
 
391
+ $reason = "COMPLETION_CONSISTENCY_BLOCKED: the checkpoint asserts completion but is missing required completion evidence: $($missing -join ', '). A completion-asserting checkpoint must include a non-empty issue-num, a non-empty feature-folder, and a ci_gate object with conclusion == 'success' and a non-empty head_sha; routes whose requires_pr_gate is true must also include pr_gate evidence with a matching head_sha. Supply the missing evidence or remove the completion assertion."
279
392
  return [ordered]@{
280
- decision = 'block'
281
- reason = "COMPLETION_CONSISTENCY_BLOCKED: the checkpoint asserts completion but is missing required completion evidence: $($missing -join ', ').$issueContext A completion-asserting checkpoint must include a non-empty issue-num, a non-empty feature-folder, and a ci_gate object with conclusion == 'success' and a non-empty head_sha. Supply the missing evidence or remove the completion assertion."
393
+ hookSpecificOutput = [ordered]@{
394
+ hookEventName = 'PreToolUse'
395
+ permissionDecision = 'deny'
396
+ permissionDecisionReason = $reason
397
+ }
282
398
  }
283
399
  }
284
400
 
@@ -295,6 +411,6 @@ catch {
295
411
  exit 1
296
412
  }
297
413
 
298
- $decision | ConvertTo-Json -Compress | Write-Output
414
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
299
415
 
300
416
  exit 0
@@ -0,0 +1,163 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Dot-sourced helper functions for enforce-completion-consistency.ps1.
4
+
5
+ .DESCRIPTION
6
+ Provides testable validation helpers used by the completion-consistency
7
+ PreToolUse hook:
8
+
9
+ - Test-IsValidIssueNum: rejects sentinel/placeholder and non-digit issue
10
+ numbers; accepts digits-only strings.
11
+ - Test-IsValidFeatureFolder: rejects sentinel/placeholder feature folders
12
+ and folders not anchored under docs/features/active/<segment>; optionally
13
+ verifies on-disk existence through an injectable scriptblock seam.
14
+
15
+ This script is dot-sourced by enforce-completion-consistency.ps1. It contains
16
+ no entrypoint logic, so dot-sourcing it in tests has no side effects.
17
+
18
+ .NOTES
19
+ Compatible with PowerShell 7+.
20
+ #>
21
+ [CmdletBinding()]
22
+ param()
23
+
24
+ # Sentinel/placeholder values that must never satisfy a presence check.
25
+ $script:CompletionEvidenceSentinels = @('n/a', 'none', 'tbd')
26
+
27
+ function Test-IsValidIssueNum {
28
+ <#
29
+ .SYNOPSIS
30
+ Returns $true only for a digits-only issue number.
31
+ .DESCRIPTION
32
+ Returns $false when the value is empty, whitespace-only, a sentinel
33
+ (n/a, none, tbd; case-insensitive), or contains any non-digit character.
34
+ Returns $true only when the trimmed value matches ^\d+$.
35
+ #>
36
+ [CmdletBinding()]
37
+ [OutputType([bool])]
38
+ param(
39
+ [Parameter(Mandatory = $true)]
40
+ [AllowNull()]
41
+ [AllowEmptyString()]
42
+ [string] $Value
43
+ )
44
+
45
+ if ([string]::IsNullOrWhiteSpace($Value)) {
46
+ return $false
47
+ }
48
+ $trimmed = $Value.Trim()
49
+ # Sentinel placeholders are explicitly rejected even though they are
50
+ # non-empty strings; the comparison is case-insensitive.
51
+ if ($script:CompletionEvidenceSentinels -contains $trimmed.ToLowerInvariant()) {
52
+ return $false
53
+ }
54
+ return $trimmed -match '^\d+$'
55
+ }
56
+
57
+ function Test-IsValidFeatureFolder {
58
+ <#
59
+ .SYNOPSIS
60
+ Returns $true only for a sentinel-free feature folder anchored under
61
+ docs/features/active/ with a non-empty trailing segment that exists.
62
+ .DESCRIPTION
63
+ Returns $false when the value is empty, whitespace-only, or a sentinel
64
+ (n/a, none, tbd; case-insensitive). Requires the value to start with
65
+ 'docs/features/active/' and to carry at least one additional non-empty
66
+ path segment after that prefix. Invokes the injectable FolderExistsCheck
67
+ scriptblock (default Test-Path -PathType Container) and returns $false
68
+ when it reports the folder does not exist.
69
+ #>
70
+ [CmdletBinding()]
71
+ [OutputType([bool])]
72
+ param(
73
+ [Parameter(Mandatory = $true)]
74
+ [AllowNull()]
75
+ [AllowEmptyString()]
76
+ [string] $Value,
77
+
78
+ [Parameter(Mandatory = $false)]
79
+ [scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container }
80
+ )
81
+
82
+ if ([string]::IsNullOrWhiteSpace($Value)) {
83
+ return $false
84
+ }
85
+ $trimmed = $Value.Trim()
86
+ if ($script:CompletionEvidenceSentinels -contains $trimmed.ToLowerInvariant()) {
87
+ return $false
88
+ }
89
+
90
+ $prefix = 'docs/features/active/'
91
+ $normalized = $trimmed -replace '\\', '/'
92
+ if (-not $normalized.StartsWith($prefix)) {
93
+ return $false
94
+ }
95
+
96
+ # Require a non-empty segment after the active/ prefix so the bare prefix is
97
+ # not accepted as a valid folder.
98
+ $suffix = $normalized.Substring($prefix.Length).TrimEnd('/')
99
+ if ([string]::IsNullOrWhiteSpace($suffix)) {
100
+ return $false
101
+ }
102
+
103
+ return [bool](& $FolderExistsCheck $normalized)
104
+ }
105
+
106
+ function Test-RouteRequiresPrGate {
107
+ <#
108
+ .SYNOPSIS
109
+ Returns $true when the payload's selected route opts into the PR gate.
110
+ .DESCRIPTION
111
+ Resolves the route id from the payload (route_id, falling back to
112
+ path_selected), looks it up in the routing matrix returned by the
113
+ injectable RoutingMatrixReader seam, and returns $true only when that
114
+ route's requires_pr_gate value is the boolean $true. A missing route id,
115
+ an unknown route, a matrix without routes, or a missing/false
116
+ requires_pr_gate returns $false. This generalizes the former issue-232
117
+ special-casing into a route-driven check.
118
+ #>
119
+ [CmdletBinding()]
120
+ [OutputType([bool])]
121
+ param(
122
+ [Parameter(Mandatory)]
123
+ [AllowNull()]
124
+ $Payload,
125
+
126
+ [Parameter(Mandatory = $false)]
127
+ [scriptblock] $RoutingMatrixReader = {
128
+ $configPath = Join-Path $PSScriptRoot '../../config/orchestration-routing.json'
129
+ if (-not (Test-Path -LiteralPath $configPath)) { return $null }
130
+ Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
131
+ }
132
+ )
133
+
134
+ if ($null -eq $Payload) {
135
+ return $false
136
+ }
137
+
138
+ # Resolve the selected route id, preferring route_id over path_selected.
139
+ $routeId = ''
140
+ if ($Payload.PSObject.Properties.Name -contains 'route_id') {
141
+ $routeId = ([string]$Payload.route_id).Trim()
142
+ }
143
+ if (-not $routeId -and ($Payload.PSObject.Properties.Name -contains 'path_selected')) {
144
+ $routeId = ([string]$Payload.path_selected).Trim()
145
+ }
146
+ if (-not $routeId) {
147
+ return $false
148
+ }
149
+
150
+ $matrix = & $RoutingMatrixReader
151
+ if ($null -eq $matrix -or -not ($matrix.PSObject.Properties.Name -contains 'routes')) {
152
+ return $false
153
+ }
154
+ $routes = $matrix.routes
155
+ if ($null -eq $routes -or -not ($routes.PSObject.Properties.Name -contains $routeId)) {
156
+ return $false
157
+ }
158
+ $route = $routes.$routeId
159
+ if ($null -eq $route -or -not ($route.PSObject.Properties.Name -contains 'requires_pr_gate')) {
160
+ return $false
161
+ }
162
+ return ([bool]$route.requires_pr_gate -eq $true)
163
+ }