@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.
- package/out/mcp-server.js +10213 -398
- package/package.json +2 -2
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +127 -17
- package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +163 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +0 -12
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +68 -1
- package/resources/config/orchestration-routing.json +3 -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 -403
- package/resources/scripts/dev_tools/push_down_claude_filesystem.py +0 -472
- package/resources/scripts/dev_tools/push_down_claude_pack_selection.py +0 -401
- 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 -505
- package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +0 -472
- 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 -233
- 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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danmoisan/drm-copilot-mcp",
|
|
3
|
-
"version": "0.0
|
|
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
|
|
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
|
-
|
|
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 +=
|
|
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
|
-
|
|
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:
|
|
250
|
-
#
|
|
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
|
-
|
|
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
|
-
$
|
|
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 ', ')
|
|
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-
|
|
36
|
-
"
|
|
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
|
]
|
|
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
|