@danmoisan/drm-copilot-mcp 0.0.6 → 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.
Files changed (62) hide show
  1. package/out/mcp-server.js +10213 -398
  2. package/package.json +2 -2
  3. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +127 -17
  4. package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +163 -0
  5. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +0 -12
  6. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +68 -1
  7. package/resources/config/orchestration-routing.json +3 -3
  8. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +0 -5
  9. package/resources/scripts/dev_tools/__init__.py +0 -0
  10. package/resources/scripts/dev_tools/_orchestrator_state_human_interaction.py +0 -127
  11. package/resources/scripts/dev_tools/_orchestrator_state_routing.py +0 -216
  12. package/resources/scripts/dev_tools/agentic_sync.py +0 -819
  13. package/resources/scripts/dev_tools/codex_native_converter/__init__.py +0 -11
  14. package/resources/scripts/dev_tools/codex_native_converter/__main__.py +0 -6
  15. package/resources/scripts/dev_tools/codex_native_converter/cli.py +0 -11
  16. package/resources/scripts/dev_tools/new_active_feature_folder.py +0 -79
  17. package/resources/scripts/dev_tools/new_active_feature_folder_docs.py +0 -268
  18. package/resources/scripts/dev_tools/new_active_feature_folder_flow.py +0 -366
  19. package/resources/scripts/dev_tools/new_active_feature_folder_io.py +0 -306
  20. package/resources/scripts/dev_tools/new_active_feature_folder_markdown.py +0 -252
  21. package/resources/scripts/dev_tools/new_active_feature_folder_models.py +0 -136
  22. package/resources/scripts/dev_tools/new_potential_bug_entry.py +0 -465
  23. package/resources/scripts/dev_tools/potential_to_issue.py +0 -421
  24. package/resources/scripts/dev_tools/potential_to_issue_content.py +0 -212
  25. package/resources/scripts/dev_tools/pr_context/__init__.py +0 -0
  26. package/resources/scripts/dev_tools/pr_context/collector.py +0 -619
  27. package/resources/scripts/dev_tools/pr_context/feature_docs.py +0 -349
  28. package/resources/scripts/dev_tools/pr_context/git.py +0 -153
  29. package/resources/scripts/dev_tools/pr_context/github.py +0 -549
  30. package/resources/scripts/dev_tools/pr_context/models.py +0 -198
  31. package/resources/scripts/dev_tools/pr_context/render.py +0 -342
  32. package/resources/scripts/dev_tools/pr_context/render_feature_excerpts.py +0 -256
  33. package/resources/scripts/dev_tools/pr_context/render_pr_helpers.py +0 -291
  34. package/resources/scripts/dev_tools/pr_context/summary_helpers.py +0 -386
  35. package/resources/scripts/dev_tools/pr_context/verification_evidence.py +0 -171
  36. package/resources/scripts/dev_tools/prompt_mode_contract.py +0 -152
  37. package/resources/scripts/dev_tools/push_down_claude_customizations.py +0 -403
  38. package/resources/scripts/dev_tools/push_down_claude_filesystem.py +0 -472
  39. package/resources/scripts/dev_tools/push_down_claude_pack_selection.py +0 -401
  40. package/resources/scripts/dev_tools/push_down_codex_and_agents_customizations.py +0 -139
  41. package/resources/scripts/dev_tools/push_down_copilot_customizations.py +0 -504
  42. package/resources/scripts/dev_tools/push_down_copilot_customizations_filesystem.py +0 -217
  43. package/resources/scripts/dev_tools/push_down_copilot_customizations_rewrites.py +0 -293
  44. package/resources/scripts/dev_tools/resolve_file_prompt.py +0 -457
  45. package/resources/scripts/dev_tools/resolve_hard_lock_prompt.py +0 -444
  46. package/resources/scripts/dev_tools/validate_orchestration_artifacts.py +0 -246
  47. package/resources/scripts/dev_tools/validate_orchestration_review_artifacts.py +0 -107
  48. package/resources/scripts/dev_tools/validate_orchestrator_state.py +0 -505
  49. package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +0 -472
  50. package/resources/templates/codex_native_converter.py +0 -35
  51. package/resources/templates/collect_commit_context.py +0 -212
  52. package/resources/templates/collect_pr_context.py +0 -74
  53. package/resources/templates/hello_python.py +0 -11
  54. package/resources/templates/new_active_feature_folder.py +0 -67
  55. package/resources/templates/new_potential_bug_entry.py +0 -54
  56. package/resources/templates/potential_to_issue.py +0 -55
  57. package/resources/templates/push_down_claude_customizations.py +0 -233
  58. package/resources/templates/push_down_codex_and_agents_customizations.py +0 -95
  59. package/resources/templates/push_down_copilot_customizations.py +0 -124
  60. package/resources/templates/resolve_atomic_plan_prompt.py +0 -75
  61. package/resources/templates/resolve_hard_lock_prompt.py +0 -65
  62. package/resources/templates/validate_orchestration_artifacts.py +0 -55
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "0.0.6",
3
+ "version": "1.0.0",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -35,7 +35,7 @@
35
35
  "author": "Dan Moisan",
36
36
  "scripts": {
37
37
  "build": "node esbuild-mcp-server.cjs",
38
- "prepack": "node -e \"const{cpSync}=require('fs');cpSync('../../extensions/drm-copilot/resources','./resources',{recursive:true,force:true})\""
38
+ "prepack": "node prepack.cjs"
39
39
  },
40
40
  "overrides": {
41
41
  "fast-uri": "^3.1.2",
@@ -39,6 +39,11 @@
39
39
  [CmdletBinding()]
40
40
  param()
41
41
 
42
+ # Dot-source the shared validation helpers. Guarded so a missing file produces a
43
+ # clear error and so dot-sourcing this hook in tests loads the helpers too.
44
+ $script:CompletionHelpersPath = Join-Path $PSScriptRoot 'enforce-completion-helpers.ps1'
45
+ . $script:CompletionHelpersPath
46
+
42
47
  function ConvertFrom-CheckpointJson {
43
48
  <#
44
49
  .SYNOPSIS
@@ -53,6 +58,29 @@ function ConvertFrom-CheckpointJson {
53
58
  return $Json | ConvertFrom-Json -ErrorAction Stop
54
59
  }
55
60
 
61
+ function Get-CheckpointFileContent {
62
+ <#
63
+ .SYNOPSIS
64
+ Reads the on-disk checkpoint content for the read-then-validate Edit path.
65
+ .DESCRIPTION
66
+ Returns the full file text when the path resolves to a file on disk, or
67
+ $null when the file does not exist. Tests inject a CheckpointReader
68
+ scriptblock instead of mocking this function so no temporary files are
69
+ required.
70
+ #>
71
+ [CmdletBinding()]
72
+ [OutputType([string])]
73
+ param(
74
+ [Parameter(Mandatory)]
75
+ [string] $Path
76
+ )
77
+
78
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
79
+ return $null
80
+ }
81
+ return Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
82
+ }
83
+
56
84
  function Test-IsCheckpointPath {
57
85
  [CmdletBinding()]
58
86
  [OutputType([bool])]
@@ -149,7 +177,13 @@ function Get-MissingCompletionEvidence {
149
177
  param(
150
178
  [Parameter(Mandatory)]
151
179
  [AllowNull()]
152
- $Payload
180
+ $Payload,
181
+
182
+ [Parameter(Mandatory = $false)]
183
+ [scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container },
184
+
185
+ [Parameter(Mandatory = $false)]
186
+ [scriptblock] $RoutingMatrixReader
153
187
  )
154
188
 
155
189
  $missing = @()
@@ -158,16 +192,17 @@ function Get-MissingCompletionEvidence {
158
192
  if (-not $issueNum -and $null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'variables')) {
159
193
  $issueNum = Get-CheckpointStringValue -Payload $Payload.variables -Name 'issue-num'
160
194
  }
161
- if (-not $issueNum) {
162
- $missing += 'issue-num'
195
+ if (-not (Test-IsValidIssueNum -Value $issueNum)) {
196
+ # Name the offending value so sentinel/placeholder inputs are explicit.
197
+ $missing += "issue-num value '$issueNum' is not a valid issue number (must be digits-only)"
163
198
  }
164
199
 
165
200
  $featureFolder = Get-CheckpointStringValue -Payload $Payload -Name 'feature-folder'
166
201
  if (-not $featureFolder -and $null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'variables')) {
167
202
  $featureFolder = Get-CheckpointStringValue -Payload $Payload.variables -Name 'feature-folder'
168
203
  }
169
- if (-not $featureFolder) {
170
- $missing += 'feature-folder'
204
+ if (-not (Test-IsValidFeatureFolder -Value $featureFolder -FolderExistsCheck $FolderExistsCheck)) {
205
+ $missing += "feature-folder value '$featureFolder' is not a valid feature folder (must be under docs/features/active/ and exist)"
171
206
  }
172
207
 
173
208
  $ciGate = $null
@@ -188,7 +223,14 @@ function Get-MissingCompletionEvidence {
188
223
  }
189
224
  }
190
225
 
191
- if ($issueNum -eq '232') {
226
+ # PR-gate evidence is required only when the checkpoint's selected route
227
+ # opts into it via requires_pr_gate in the routing matrix. This replaces the
228
+ # former issue-number special-casing with route-driven enforcement.
229
+ $prGateArgs = @{ Payload = $Payload }
230
+ if ($PSBoundParameters.ContainsKey('RoutingMatrixReader') -and $null -ne $RoutingMatrixReader) {
231
+ $prGateArgs['RoutingMatrixReader'] = $RoutingMatrixReader
232
+ }
233
+ if (Test-RouteRequiresPrGate @prGateArgs) {
192
234
  $prGate = $null
193
235
  if ($null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'pr_gate')) {
194
236
  $prGate = $Payload.pr_gate
@@ -213,6 +255,60 @@ function Get-MissingCompletionEvidence {
213
255
  return [string[]]$missing
214
256
  }
215
257
 
258
+ function Resolve-EditedCheckpointContent {
259
+ <#
260
+ .SYNOPSIS
261
+ Returns the patched checkpoint content for an Edit-tool call, or $null
262
+ when the patch cannot be applied against the on-disk checkpoint.
263
+ .DESCRIPTION
264
+ Implements the read-then-validate Edit path. When the tool input carries
265
+ an old_string (an Edit patch), the on-disk checkpoint is read through the
266
+ injectable CheckpointReader seam and the old_string -> new_string
267
+ replacement is applied in memory (no on-disk mutation). Returns $null
268
+ when there is no old_string, the on-disk file does not exist, or the
269
+ old_string is not present in the on-disk content, signalling the caller
270
+ to allow (defer).
271
+ #>
272
+ [CmdletBinding()]
273
+ [OutputType([string])]
274
+ param(
275
+ [Parameter(Mandatory)]
276
+ [AllowNull()]
277
+ $ToolInput,
278
+
279
+ [Parameter(Mandatory)]
280
+ [scriptblock] $CheckpointReader
281
+ )
282
+
283
+ $oldString = $null
284
+ if ($null -ne $ToolInput -and ($ToolInput.PSObject.Properties.Name -contains 'old_string')) {
285
+ $oldString = [string]$ToolInput.old_string
286
+ }
287
+ if ([string]::IsNullOrEmpty($oldString)) {
288
+ return $null
289
+ }
290
+
291
+ $newString = ''
292
+ if ($ToolInput.PSObject.Properties.Name -contains 'new_string') {
293
+ $newString = [string]$ToolInput.new_string
294
+ }
295
+
296
+ $onDisk = & $CheckpointReader 'artifacts/orchestration/orchestrator-state.json'
297
+ if ([string]::IsNullOrEmpty([string]$onDisk)) {
298
+ # The on-disk checkpoint does not exist (or is empty); cannot patch.
299
+ return $null
300
+ }
301
+
302
+ $onDiskText = [string]$onDisk
303
+ if (-not $onDiskText.Contains($oldString)) {
304
+ # The old_string is not present, so the patch does not apply here.
305
+ return $null
306
+ }
307
+
308
+ # Apply the patch in memory using a literal (non-regex) replacement.
309
+ return $onDiskText.Replace($oldString, $newString)
310
+ }
311
+
216
312
  function Invoke-CompletionConsistencyDecision {
217
313
  <#
218
314
  .SYNOPSIS
@@ -222,7 +318,16 @@ function Invoke-CompletionConsistencyDecision {
222
318
  [CmdletBinding()]
223
319
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
224
320
  param(
225
- [string] $ToolInputRaw
321
+ [string] $ToolInputRaw,
322
+
323
+ [Parameter(Mandatory = $false)]
324
+ [scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container },
325
+
326
+ [Parameter(Mandatory = $false)]
327
+ [scriptblock] $CheckpointReader = { param($Path) Get-CheckpointFileContent -Path $Path },
328
+
329
+ [Parameter(Mandatory = $false)]
330
+ [scriptblock] $RoutingMatrixReader
226
331
  )
227
332
 
228
333
  if (-not $ToolInputRaw) {
@@ -246,11 +351,17 @@ function Invoke-CompletionConsistencyDecision {
246
351
  return [ordered]@{ decision = 'allow' }
247
352
  }
248
353
 
249
- # Write tool: validate the content payload. Edit tool: partial new_string is
250
- # not reliable without the full target file content, so allow.
354
+ # Write tool: validate the content payload directly. Edit tool: no content is
355
+ # supplied, so read the on-disk checkpoint through the injectable seam and
356
+ # apply the old_string -> new_string patch in memory (read-then-validate).
251
357
  $content = $toolInput.content
252
358
  if (-not $content) {
253
- return [ordered]@{ decision = 'allow' }
359
+ $content = Resolve-EditedCheckpointContent -ToolInput $toolInput -CheckpointReader $CheckpointReader
360
+ if (-not $content) {
361
+ # No content, and the Edit could not be resolved against on-disk
362
+ # state (missing file or non-matching patch): defer and allow.
363
+ return [ordered]@{ decision = 'allow' }
364
+ }
254
365
  }
255
366
 
256
367
  try {
@@ -266,19 +377,18 @@ function Invoke-CompletionConsistencyDecision {
266
377
  return [ordered]@{ decision = 'allow' }
267
378
  }
268
379
 
269
- $missing = Get-MissingCompletionEvidence -Payload $payload
380
+ $missingArgs = @{ Payload = $payload; FolderExistsCheck = $FolderExistsCheck }
381
+ if ($PSBoundParameters.ContainsKey('RoutingMatrixReader') -and $null -ne $RoutingMatrixReader) {
382
+ $missingArgs['RoutingMatrixReader'] = $RoutingMatrixReader
383
+ }
384
+ $missing = Get-MissingCompletionEvidence @missingArgs
270
385
  if ($missing.Count -eq 0) {
271
386
  return [ordered]@{ decision = 'allow' }
272
387
  }
273
388
 
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.'
277
- }
278
-
279
389
  return [ordered]@{
280
390
  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."
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."
282
392
  }
283
393
  }
284
394
 
@@ -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
+ }
@@ -6,7 +6,6 @@
6
6
  param()
7
7
 
8
8
  $script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
9
- $script:Issue232FeatureFolder = 'docs/features/active/2026-06-24-harden-orchestrate-skill-232'
10
9
 
11
10
  function ConvertFrom-CheckpointJson {
12
11
  [CmdletBinding()]
@@ -113,10 +112,6 @@ function Test-OrchestrationReady {
113
112
  return $false
114
113
  }
115
114
 
116
- if ($issueNum -eq '232' -and $featureFolder -ne $script:Issue232FeatureFolder) {
117
- return $false
118
- }
119
-
120
115
  return (
121
116
  $featureFolder.StartsWith('docs/features/active/') -and
122
117
  $routeId -and
@@ -182,13 +177,6 @@ function Invoke-OrchestrationPreimplementationGateDecision {
182
177
  if (Test-OrchestrationReady -Payload $checkpoint) {
183
178
  return [ordered]@{ decision = 'allow' }
184
179
  }
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
180
  return [ordered]@{
193
181
  decision = 'block'
194
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.'
@@ -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
 
@@ -27,13 +27,14 @@
27
27
  },
28
28
  "large": {
29
29
  "description": "Full feature or bug path for work outside small-path budget.",
30
+ "requires_pr_gate": true,
30
31
  "required_agents": [
31
32
  "task-researcher",
32
33
  "prd-feature",
33
34
  "atomic-planner",
34
35
  "atomic-executor",
35
- "feature-reviewer",
36
- "commit-steward"
36
+ "feature-review",
37
+ "pr-author"
37
38
  ],
38
39
  "required_skills": [
39
40
  "orchestrate",
@@ -49,7 +50,6 @@
49
50
  "new_potential_entry",
50
51
  "potential_to_issue",
51
52
  "new_active_feature_folder",
52
- "collect_commit_context",
53
53
  "collect_pr_context",
54
54
  "validate_orchestration_artifacts"
55
55
  ]
@@ -1,5 +0,0 @@
1
- [CmdletBinding()]
2
- param()
3
-
4
- Set-StrictMode -Version Latest
5
- $ErrorActionPreference = 'Stop'
File without changes
@@ -1,127 +0,0 @@
1
- """Human-interaction invariants for orchestrator-state checkpoints.
2
-
3
- Purpose:
4
- Hold the optional ``human_interaction`` block constants and the
5
- ``_validate_human_interaction`` helper so the primary validator module
6
- (`scripts.dev_tools.validate_orchestrator_state`) can stay within the
7
- repository's 500-line file limit while preserving the existing validator
8
- contract.
9
-
10
- Usage:
11
- Import ``HUMAN_INTERACTION_KEY`` and ``_validate_human_interaction`` from
12
- this module. The primary validator re-exports both symbols so existing
13
- callers and tests continue to resolve them from
14
- ``scripts.dev_tools.validate_orchestrator_state`` unchanged.
15
-
16
- Invariants / Constraints:
17
- - The three permitted ``response`` values are ``scope_change``,
18
- ``exception``, and ``halt``.
19
- - An ``exception`` response requires a non-empty ``runbook_path`` string.
20
- - The validator never imports ``schemas/orchestrator-state.schema.json``;
21
- the invariants are expressed directly here per
22
- `.claude/rules/orchestrator-state.md`.
23
-
24
- Side Effects:
25
- None.
26
- """
27
-
28
- from __future__ import annotations
29
-
30
- from typing import Any, cast
31
-
32
- # Declare the module's intended exported surface. Listing ``_validate_human_
33
- # interaction`` here marks it as a deliberate re-export consumed by
34
- # ``validate_orchestrator_state``, so static analysis does not flag the helper
35
- # as unused locally or as private-usage when imported across the module boundary.
36
- __all__ = [
37
- "HUMAN_INTERACTION_KEY",
38
- "HUMAN_INTERACTION_REQUIREMENTS_KEY",
39
- "HUMAN_INTERACTION_RESPONSE_ENUM",
40
- "HUMAN_INTERACTION_EXCEPTION_RESPONSE",
41
- "_validate_human_interaction",
42
- ]
43
-
44
- HUMAN_INTERACTION_KEY = "human_interaction"
45
- HUMAN_INTERACTION_REQUIREMENTS_KEY = "requirements"
46
- # The three permitted responses for an unautomatable requirement under the
47
- # autonomous-execution mandate (see `.claude/skills/orchestrate/SKILL.md`).
48
- HUMAN_INTERACTION_RESPONSE_ENUM = {"scope_change", "exception", "halt"}
49
- HUMAN_INTERACTION_EXCEPTION_RESPONSE = "exception"
50
-
51
-
52
- def _validate_human_interaction(human_interaction: object) -> list[str]:
53
- """Validate the optional ``human_interaction`` block invariants.
54
-
55
- Purpose:
56
- Apply the autonomous-execution mandate invariants to the checkpoint's
57
- optional top-level ``human_interaction`` object, mirroring the schema
58
- invariants documented in `.claude/rules/orchestrator-state.md`. The
59
- validator never imports `schemas/orchestrator-state.schema.json`; the
60
- invariants are expressed directly here in the existing helper-plus-
61
- error-list style.
62
-
63
- Args:
64
- human_interaction (object): The raw value of the checkpoint's top-level
65
- ``human_interaction`` key. Callers invoke this helper only when the
66
- key is present, so a non-object value is itself a malformed block.
67
-
68
- Returns:
69
- list[str]: One error string per violated invariant; an empty list when
70
- the block is well-formed. The three invariants are: ``requirements`` is
71
- present and is a list; each requirement is an object whose ``response``
72
- is within the permitted enum; a requirement whose ``response`` is
73
- ``exception`` carries a non-empty ``runbook_path`` string.
74
-
75
- Raises:
76
- None.
77
-
78
- Side Effects:
79
- None.
80
- """
81
-
82
- errors: list[str] = []
83
-
84
- # A non-object human_interaction cannot carry a requirements list; the key
85
- # was present, so this is a malformed block rather than an absent one.
86
- if not isinstance(human_interaction, dict):
87
- errors.append("Checkpoint human_interaction must be an object when present.")
88
- return errors
89
- human_interaction_map = cast("dict[str, Any]", human_interaction)
90
-
91
- # Invariant 1: requirements must be present and a list.
92
- requirements = human_interaction_map.get(HUMAN_INTERACTION_REQUIREMENTS_KEY)
93
- if not isinstance(requirements, list):
94
- errors.append("Checkpoint human_interaction.requirements must be a list.")
95
- return errors
96
- requirement_list = cast("list[object]", requirements)
97
-
98
- # Validate each requirement independently so callers receive a complete
99
- # error list instead of stopping at the first malformed requirement.
100
- for index, requirement in enumerate(requirement_list):
101
- if not isinstance(requirement, dict):
102
- errors.append(
103
- f"Checkpoint human_interaction.requirements #{index} must be an "
104
- "object."
105
- )
106
- continue
107
- requirement_map = cast("dict[str, Any]", requirement)
108
-
109
- # Invariant 2: response must be within the permitted enum.
110
- response = requirement_map.get("response")
111
- if response not in HUMAN_INTERACTION_RESPONSE_ENUM:
112
- errors.append(
113
- f"Checkpoint human_interaction.requirements #{index} response "
114
- f"must be one of scope_change, exception, halt; got: {response}"
115
- )
116
- continue
117
-
118
- # Invariant 3: an exception response requires a non-empty runbook_path.
119
- if response == HUMAN_INTERACTION_EXCEPTION_RESPONSE:
120
- runbook_path = requirement_map.get("runbook_path")
121
- if not isinstance(runbook_path, str) or not runbook_path.strip():
122
- errors.append(
123
- f"Checkpoint human_interaction.requirements #{index} "
124
- "response is exception but runbook_path is missing or empty."
125
- )
126
-
127
- return errors