@danmoisan/drm-copilot-mcp 0.0.5 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/mcp-server.js +10268 -380
- package/package.json +2 -2
- package/resources/claude-customizations/.claude/agents/orchestrator.md +6 -2
- package/resources/claude-customizations/.claude/agents/pr-author.md +78 -0
- package/resources/claude-customizations/.claude/agents/task-researcher.md +10 -6
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +68 -10
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +148 -11
- package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +163 -0
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +40 -14
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +198 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +153 -10
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +68 -1
- package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +136 -0
- package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +22 -6
- package/resources/claude-customizations/.claude/settings.json +22 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +0 -1
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +76 -2
- package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +5 -3
- package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +69 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/rules/csharp.md +96 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +77 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/skills/invoke-csharp-engineer/SKILL.md +64 -0
- package/resources/claude-customizations/pack-manifests/core.json +65 -0
- package/resources/claude-customizations/pack-manifests/csharp-legacy.json +11 -0
- package/resources/claude-customizations/pack-manifests/csharp-modern.json +12 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +14 -0
- package/resources/claude-customizations/pack-manifests/python.json +14 -0
- package/resources/claude-customizations/pack-manifests/typescript.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +30 -13
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +113 -10
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +33 -8
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +12 -7
- package/resources/codex-and-agents-customizations/.codex/config.toml +65 -6
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +303 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +300 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +148 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-pr-author-skill.ps1 +336 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +83 -17
- package/resources/config/orchestration-routing.json +5 -13
- package/resources/customizations/.github/agents/pr-author.agent.md +25 -0
- package/resources/customizations/.github/agents/task-researcher.agent.md +4 -4
- package/resources/customizations/.github/prompts/fillout-prd-feature.prompt.md +1 -1
- package/resources/customizations/.github/prompts/research-issue.prompt.md +4 -3
- package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +0 -5
- package/resources/scripts/dev_tools/__init__.py +0 -0
- package/resources/scripts/dev_tools/_orchestrator_state_human_interaction.py +0 -127
- package/resources/scripts/dev_tools/_orchestrator_state_routing.py +0 -216
- package/resources/scripts/dev_tools/agentic_sync.py +0 -819
- package/resources/scripts/dev_tools/codex_native_converter/__init__.py +0 -11
- package/resources/scripts/dev_tools/codex_native_converter/__main__.py +0 -6
- package/resources/scripts/dev_tools/codex_native_converter/cli.py +0 -11
- package/resources/scripts/dev_tools/new_active_feature_folder.py +0 -79
- package/resources/scripts/dev_tools/new_active_feature_folder_docs.py +0 -268
- package/resources/scripts/dev_tools/new_active_feature_folder_flow.py +0 -366
- package/resources/scripts/dev_tools/new_active_feature_folder_io.py +0 -306
- package/resources/scripts/dev_tools/new_active_feature_folder_markdown.py +0 -252
- package/resources/scripts/dev_tools/new_active_feature_folder_models.py +0 -136
- package/resources/scripts/dev_tools/new_potential_bug_entry.py +0 -465
- package/resources/scripts/dev_tools/potential_to_issue.py +0 -421
- package/resources/scripts/dev_tools/potential_to_issue_content.py +0 -212
- package/resources/scripts/dev_tools/pr_context/__init__.py +0 -0
- package/resources/scripts/dev_tools/pr_context/collector.py +0 -619
- package/resources/scripts/dev_tools/pr_context/feature_docs.py +0 -349
- package/resources/scripts/dev_tools/pr_context/git.py +0 -153
- package/resources/scripts/dev_tools/pr_context/github.py +0 -549
- package/resources/scripts/dev_tools/pr_context/models.py +0 -198
- package/resources/scripts/dev_tools/pr_context/render.py +0 -342
- package/resources/scripts/dev_tools/pr_context/render_feature_excerpts.py +0 -256
- package/resources/scripts/dev_tools/pr_context/render_pr_helpers.py +0 -291
- package/resources/scripts/dev_tools/pr_context/summary_helpers.py +0 -386
- package/resources/scripts/dev_tools/pr_context/verification_evidence.py +0 -171
- package/resources/scripts/dev_tools/prompt_mode_contract.py +0 -152
- package/resources/scripts/dev_tools/push_down_claude_customizations.py +0 -374
- package/resources/scripts/dev_tools/push_down_codex_and_agents_customizations.py +0 -139
- package/resources/scripts/dev_tools/push_down_copilot_customizations.py +0 -504
- package/resources/scripts/dev_tools/push_down_copilot_customizations_filesystem.py +0 -217
- package/resources/scripts/dev_tools/push_down_copilot_customizations_rewrites.py +0 -293
- package/resources/scripts/dev_tools/resolve_file_prompt.py +0 -457
- package/resources/scripts/dev_tools/resolve_hard_lock_prompt.py +0 -444
- package/resources/scripts/dev_tools/validate_orchestration_artifacts.py +0 -246
- package/resources/scripts/dev_tools/validate_orchestration_review_artifacts.py +0 -107
- package/resources/scripts/dev_tools/validate_orchestrator_state.py +0 -428
- package/resources/scripts/dev_tools/validate_policy_audit_artifact.py +0 -448
- package/resources/templates/codex_native_converter.py +0 -35
- package/resources/templates/collect_commit_context.py +0 -212
- package/resources/templates/collect_pr_context.py +0 -74
- package/resources/templates/hello_python.py +0 -11
- package/resources/templates/new_active_feature_folder.py +0 -67
- package/resources/templates/new_potential_bug_entry.py +0 -54
- package/resources/templates/potential_to_issue.py +0 -55
- package/resources/templates/push_down_claude_customizations.py +0 -409
- package/resources/templates/push_down_codex_and_agents_customizations.py +0 -95
- package/resources/templates/push_down_copilot_customizations.py +0 -124
- package/resources/templates/resolve_atomic_plan_prompt.py +0 -75
- package/resources/templates/resolve_hard_lock_prompt.py +0 -65
- package/resources/templates/validate_orchestration_artifacts.py +0 -55
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",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: orchestrator
|
|
3
3
|
description: Deterministic repository orchestrator that estimates change budget, selects small or large workflow path, delegates to specialist subagents, persists checkpoint state, and enforces completion gates proactively.
|
|
4
4
|
tools:
|
|
5
|
-
- "Agent(atomic-planner,atomic-executor,feature-review,task-researcher,prd-feature,staged-review,epic-review,status-updater,python-typed-engineer,powershell-typed-engineer,csharp-typed-engineer,typescript-engineer)"
|
|
5
|
+
- "Agent(atomic-planner,atomic-executor,feature-review,task-researcher,prd-feature,staged-review,epic-review,status-updater,pr-author,python-typed-engineer,powershell-typed-engineer,csharp-typed-engineer,typescript-engineer)"
|
|
6
6
|
- Read
|
|
7
7
|
- Grep
|
|
8
8
|
- Glob
|
|
@@ -69,10 +69,14 @@ Delegate exclusively through configured subagents:
|
|
|
69
69
|
- `atomic-planner` — generates phased implementation plans (planning only)
|
|
70
70
|
- `atomic-executor` — executes approved plans task-by-task (execution only)
|
|
71
71
|
- `feature-review` — produces policy, code, and feature audit artifacts
|
|
72
|
-
- `task-researcher` — performs deep research and writes findings to `
|
|
72
|
+
- `task-researcher` — performs deep research and writes findings to the research path the orchestrator resolves before delegating: `docs/features/<feature>/research/` when an active `feature-folder` is in scope in `orchestrator-state.json`, otherwise `docs/research/` for one-off research. The orchestrator passes the resolved path in the delegation prompt.
|
|
73
73
|
|
|
74
74
|
For required delegated steps, delegation is mandatory. If a handoff cannot be started, resumed, or completed, stop execution and record blocked state. Do not perform the step locally.
|
|
75
75
|
|
|
76
|
+
## PR Creation Delegation
|
|
77
|
+
|
|
78
|
+
PR creation and PR body edits must be delegated to `Agent(pr-author)`. The orchestrator must not call `gh pr create` or `gh pr edit --body*` directly from the main thread; those commands are blocked by the `enforce-pr-author-skill.ps1` PreToolUse hook unless a valid authorization sentinel issued by the `pr-author` agent is present. The orchestrator first produces the PR-context artifact via `mcp__drm-copilot__collect_pr_context`, then delegates to `Agent(pr-author)`, which authors the PR body via the `pr-author` skill, writes and deletes the authorization sentinel around the `gh` command, and reports the resulting PR URL or PR number.
|
|
79
|
+
|
|
76
80
|
## Checkpoint Persistence
|
|
77
81
|
|
|
78
82
|
Update `artifacts/orchestration/orchestrator-state.json` after every completed step with:
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pr-author
|
|
3
|
+
description: Project-scoped agent that runs the pr-author skill to produce a GitHub-ready PR body from the canonical PR-context bundle, then opens or updates the pull request. Sole authorized caller of gh pr create and gh pr edit --body*. Writes a short-lived authorization sentinel immediately before each gh command and deletes it afterward.
|
|
4
|
+
model: sonnet
|
|
5
|
+
skills:
|
|
6
|
+
- pr-author
|
|
7
|
+
memory: project
|
|
8
|
+
tools:
|
|
9
|
+
- Read
|
|
10
|
+
- "Bash(git log *)"
|
|
11
|
+
- "Bash(git rev-parse *)"
|
|
12
|
+
- "Bash(gh pr create *)"
|
|
13
|
+
- "Bash(gh pr edit *)"
|
|
14
|
+
- "Write(/artifacts/**)"
|
|
15
|
+
hooks:
|
|
16
|
+
SubagentStop:
|
|
17
|
+
- matcher: "pr-author"
|
|
18
|
+
hooks:
|
|
19
|
+
- type: command
|
|
20
|
+
command: pwsh -NoProfile -File .claude/hooks/validate-pr-author-output.ps1
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# PR Author Agent
|
|
24
|
+
|
|
25
|
+
You are the dedicated pull-request authoring agent. Your sole responsibility is to consume the
|
|
26
|
+
canonical PR-context bundle, produce the PR body using the `pr-author` skill, and open or update the
|
|
27
|
+
pull request. You are the only agent authorized to run `gh pr create` and `gh pr edit --body*`. The
|
|
28
|
+
orchestrator and all other agents are blocked from these commands by the
|
|
29
|
+
`enforce-pr-author-skill.ps1` PreToolUse hook.
|
|
30
|
+
|
|
31
|
+
## Skill
|
|
32
|
+
|
|
33
|
+
Apply the `pr-author` skill (`.claude/skills/pr-author/SKILL.md`) as the canonical workflow for
|
|
34
|
+
authoring the PR body. The skill produces the PR body text from the PR-context bundle
|
|
35
|
+
(`artifacts/pr_context.summary.txt` and `artifacts/pr_context.appendix.txt`) and its enumerated
|
|
36
|
+
additional context files. The skill itself only authors body text; opening and editing the pull
|
|
37
|
+
request via `gh pr create` / `gh pr edit` is this agent's responsibility, not the skill's.
|
|
38
|
+
|
|
39
|
+
## Authorization Sentinel Write/Delete Protocol
|
|
40
|
+
|
|
41
|
+
Before any `gh pr create` or `gh pr edit --body*` command, you MUST perform these steps in order:
|
|
42
|
+
|
|
43
|
+
1. Run `git rev-parse HEAD` to obtain the current `head_sha`.
|
|
44
|
+
2. Write `artifacts/pr_author_authorization.json` with exactly these fields:
|
|
45
|
+
- `issued_by`: `"pr-author"`
|
|
46
|
+
- `issued_at`: the current time as a UTC ISO-8601 timestamp (for example `2026-06-24T16:00:00Z`)
|
|
47
|
+
- `head_sha`: the value from step 1
|
|
48
|
+
- `ttl_seconds`: `120`
|
|
49
|
+
3. Issue the `gh pr create` or `gh pr edit --body-file ...` command immediately, within the 120-second
|
|
50
|
+
TTL. The PreToolUse hook verifies that the sentinel is present, that `issued_by` is exactly
|
|
51
|
+
`pr-author`, and that the sentinel has not expired before allowing the command.
|
|
52
|
+
4. Delete `artifacts/pr_author_authorization.json` after the `gh` command completes, on both success
|
|
53
|
+
and failure. The TTL also expires abandoned sentinels, but explicit deletion is required.
|
|
54
|
+
|
|
55
|
+
The sentinel filename is exactly `artifacts/pr_author_authorization.json`. The PR body must be passed
|
|
56
|
+
via `--body-file`; inline `--body` is blocked by the hook (Case A).
|
|
57
|
+
|
|
58
|
+
## Final Output Requirement
|
|
59
|
+
|
|
60
|
+
After opening or updating the pull request, report the resulting PR URL
|
|
61
|
+
(`https://github.com/<owner>/<repo>/pull/<n>`) or the PR number (`PR #<n>`) in your final output. The
|
|
62
|
+
`validate-pr-author-output.ps1` SubagentStop hook blocks completion when the final output contains no
|
|
63
|
+
PR URL or PR number.
|
|
64
|
+
|
|
65
|
+
## Enforcement Strength (Honest Disclosure)
|
|
66
|
+
|
|
67
|
+
The authorization sentinel is a **policy guardrail, not a cryptographic or security control.** Any
|
|
68
|
+
actor with `Write(/artifacts/**)` access can forge `artifacts/pr_author_authorization.json`, because
|
|
69
|
+
all agents share the same filesystem and the runtime exposes no native agent-identity signal at Bash
|
|
70
|
+
PreToolUse time. The mechanism prevents accidental bypass (such as the PR #228 pattern where the
|
|
71
|
+
orchestrator wrote the body file and called `gh pr create` directly) and requires a deliberate,
|
|
72
|
+
documented act to circumvent. It is not tamper-proof and is not a security boundary.
|
|
73
|
+
|
|
74
|
+
## Standing Rules
|
|
75
|
+
|
|
76
|
+
Repository tone is defined in `CLAUDE.md` and `.claude/rules/tonality.md`. Reference only files
|
|
77
|
+
listed under "Additional context files" in the PR-context bundle; do not cite or summarize files
|
|
78
|
+
outside that enumeration. Do not invent issue or PR numbers.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: task-researcher
|
|
3
|
-
description: Research specialist that performs deep investigation and writes structured findings
|
|
3
|
+
description: Research specialist that performs deep investigation and writes structured findings to the orchestrator-supplied research path under docs/features/<feature>/research/ (feature-associated) or docs/research/ (one-off).
|
|
4
4
|
model: sonnet
|
|
5
5
|
tools:
|
|
6
6
|
- Read
|
|
7
7
|
- Grep
|
|
8
8
|
- Glob
|
|
9
9
|
- WebFetch
|
|
10
|
-
- "Write(/
|
|
10
|
+
- "Write(/docs/features/**/research/**)"
|
|
11
|
+
- "Write(/docs/research/**)"
|
|
11
12
|
- evidence-and-timestamp-conventions
|
|
12
13
|
memory: project
|
|
13
14
|
hooks:
|
|
@@ -20,13 +21,16 @@ hooks:
|
|
|
20
21
|
|
|
21
22
|
# Task Researcher Agent
|
|
22
23
|
|
|
23
|
-
You are a research-only specialist. You perform deep analysis for task planning and write structured research notes. You do not make changes to source code, configurations, or project files outside
|
|
24
|
+
You are a research-only specialist. You perform deep analysis for task planning and write structured research notes. You do not make changes to source code, configurations, or project files outside the research root the orchestrator supplies.
|
|
24
25
|
|
|
25
26
|
## Output Location
|
|
26
27
|
|
|
27
|
-
Write
|
|
28
|
+
Write each research artifact to the research path the orchestrator supplies in the delegation prompt. There are two tracked research roots:
|
|
28
29
|
|
|
29
|
-
- `
|
|
30
|
+
- Feature-associated research: `docs/features/<feature>/research/<timestamp>-<short-name>-research.md` (for example `docs/features/active/<feature>/research/<timestamp>-<short-name>-research.md`).
|
|
31
|
+
- One-off research not tied to a feature: `docs/research/<timestamp>-<short-name>-research.md`.
|
|
32
|
+
|
|
33
|
+
The orchestrator resolves which root to use from whether an active feature folder is in scope and passes the exact path in the delegation prompt; do not infer the feature folder independently. The filename convention `<timestamp>-<short-name>-research.md` is unchanged.
|
|
30
34
|
|
|
31
35
|
## Core Principles
|
|
32
36
|
|
|
@@ -67,7 +71,7 @@ Write all research artifacts to `artifacts/research/` using the filename convent
|
|
|
67
71
|
|
|
68
72
|
## Constraints
|
|
69
73
|
|
|
70
|
-
- Write only to `
|
|
74
|
+
- Write only to the orchestrator-supplied research path under `docs/features/<feature>/research/` (feature-associated) or `docs/research/` (one-off). Do not modify source code or configurations.
|
|
71
75
|
- Ground all findings in verified evidence.
|
|
72
76
|
- Keep discussion of non-selected approaches brief.
|
|
73
77
|
- Do not claim nested worker delegation.
|
|
@@ -134,6 +134,53 @@ function Get-OutOfOrderPair {
|
|
|
134
134
|
return $null
|
|
135
135
|
}
|
|
136
136
|
|
|
137
|
+
function Test-StepHasPrefix {
|
|
138
|
+
[CmdletBinding()]
|
|
139
|
+
[OutputType([bool])]
|
|
140
|
+
param(
|
|
141
|
+
[Parameter(Mandatory)]
|
|
142
|
+
[string] $StepEntry,
|
|
143
|
+
|
|
144
|
+
[Parameter(Mandatory)]
|
|
145
|
+
[string] $Prefix
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return $StepEntry -eq $Prefix -or $StepEntry.StartsWith("$Prefix" + '_') -or $StepEntry.StartsWith("$Prefix.") -or $StepEntry.StartsWith("$Prefix-")
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Get-MissingPrerequisiteForAdvancedStep {
|
|
152
|
+
[CmdletBinding()]
|
|
153
|
+
param(
|
|
154
|
+
[Parameter(Mandatory)]
|
|
155
|
+
[AllowEmptyCollection()]
|
|
156
|
+
[string[]] $CompletedSteps
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
$hasPromotion = $false
|
|
160
|
+
$hasPlanning = $false
|
|
161
|
+
foreach ($step in $CompletedSteps) {
|
|
162
|
+
if (Test-StepHasPrefix -StepEntry $step -Prefix 'S3_promotion') {
|
|
163
|
+
$hasPromotion = $true
|
|
164
|
+
}
|
|
165
|
+
if (Test-StepHasPrefix -StepEntry $step -Prefix 'S4_atomic_planning') {
|
|
166
|
+
$hasPlanning = $true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
foreach ($step in $CompletedSteps) {
|
|
171
|
+
$index = Get-CanonicalStepIndex -StepEntry $step
|
|
172
|
+
if ($index -ge 5 -and (-not $hasPromotion -or -not $hasPlanning)) {
|
|
173
|
+
return [pscustomobject]@{
|
|
174
|
+
Step = $step
|
|
175
|
+
MissingPromotion = -not $hasPromotion
|
|
176
|
+
MissingPlanning = -not $hasPlanning
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return $null
|
|
182
|
+
}
|
|
183
|
+
|
|
137
184
|
function Test-IsCheckpointPath {
|
|
138
185
|
[CmdletBinding()]
|
|
139
186
|
[OutputType([bool])]
|
|
@@ -204,10 +251,6 @@ function Invoke-CheckpointMonotonicDecision {
|
|
|
204
251
|
}
|
|
205
252
|
}
|
|
206
253
|
|
|
207
|
-
if ($steps.Count -lt 2) {
|
|
208
|
-
return [ordered]@{ decision = 'allow' }
|
|
209
|
-
}
|
|
210
|
-
|
|
211
254
|
$rollbackHistory = $null
|
|
212
255
|
if ($payload.PSObject.Properties.Name -contains 'rollback_history') {
|
|
213
256
|
$rollbackHistory = $payload.rollback_history
|
|
@@ -216,15 +259,30 @@ function Invoke-CheckpointMonotonicDecision {
|
|
|
216
259
|
return [ordered]@{ decision = 'allow' }
|
|
217
260
|
}
|
|
218
261
|
|
|
219
|
-
$pair = Get-OutOfOrderPair -CompletedSteps $steps
|
|
220
|
-
if ($null -
|
|
221
|
-
return [ordered]@{
|
|
262
|
+
$pair = if ($steps.Count -ge 2) { Get-OutOfOrderPair -CompletedSteps $steps } else { $null }
|
|
263
|
+
if ($null -ne $pair) {
|
|
264
|
+
return [ordered]@{
|
|
265
|
+
decision = 'block'
|
|
266
|
+
reason = "CHECKPOINT_ORDER_BLOCKED: completed_steps lists '$($pair.EarlierEntry)' at position $($pair.EarlierPos) before '$($pair.LaterEntry)' at position $($pair.LaterPos), but the canonical orchestrator workflow requires the later step to follow the earlier one. Reorder completed_steps or, if a rollback occurred, record it in rollback_history."
|
|
267
|
+
}
|
|
222
268
|
}
|
|
223
269
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
270
|
+
$missingPrerequisite = Get-MissingPrerequisiteForAdvancedStep -CompletedSteps $steps
|
|
271
|
+
if ($null -ne $missingPrerequisite) {
|
|
272
|
+
$missing = @()
|
|
273
|
+
if ($missingPrerequisite.MissingPromotion) {
|
|
274
|
+
$missing += 'S3_promotion'
|
|
275
|
+
}
|
|
276
|
+
if ($missingPrerequisite.MissingPlanning) {
|
|
277
|
+
$missing += 'S4_atomic_planning'
|
|
278
|
+
}
|
|
279
|
+
return [ordered]@{
|
|
280
|
+
decision = 'block'
|
|
281
|
+
reason = "CHECKPOINT_ORDER_BLOCKED: completed_steps lists '$($missingPrerequisite.Step)' before required prerequisite step(s): $($missing -join ', '). Record promotion and planning completion before implementation, review, PR, CI, or DONE steps."
|
|
282
|
+
}
|
|
227
283
|
}
|
|
284
|
+
|
|
285
|
+
return [ordered]@{ decision = 'allow' }
|
|
228
286
|
}
|
|
229
287
|
|
|
230
288
|
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
@@ -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,9 +223,92 @@ function Get-MissingCompletionEvidence {
|
|
|
188
223
|
}
|
|
189
224
|
}
|
|
190
225
|
|
|
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) {
|
|
234
|
+
$prGate = $null
|
|
235
|
+
if ($null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'pr_gate')) {
|
|
236
|
+
$prGate = $Payload.pr_gate
|
|
237
|
+
}
|
|
238
|
+
if ($null -eq $prGate -or $prGate -isnot [System.Management.Automation.PSCustomObject]) {
|
|
239
|
+
$missing += 'pr_gate (object with pr_number, pr_url, head_branch, and head_sha)'
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
foreach ($field in @('pr_number', 'pr_url', 'head_branch', 'head_sha')) {
|
|
243
|
+
if (-not (Get-CheckpointStringValue -Payload $prGate -Name $field)) {
|
|
244
|
+
$missing += "pr_gate.$field"
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
$prHeadSha = Get-CheckpointStringValue -Payload $prGate -Name 'head_sha'
|
|
248
|
+
$ciHeadSha = Get-CheckpointStringValue -Payload $ciGate -Name 'head_sha'
|
|
249
|
+
if ($prHeadSha -and $ciHeadSha -and $prHeadSha -ne $ciHeadSha) {
|
|
250
|
+
$missing += 'ci_gate.head_sha matching pr_gate.head_sha'
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
191
255
|
return [string[]]$missing
|
|
192
256
|
}
|
|
193
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
|
+
|
|
194
312
|
function Invoke-CompletionConsistencyDecision {
|
|
195
313
|
<#
|
|
196
314
|
.SYNOPSIS
|
|
@@ -200,7 +318,16 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
200
318
|
[CmdletBinding()]
|
|
201
319
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
202
320
|
param(
|
|
203
|
-
[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
|
|
204
331
|
)
|
|
205
332
|
|
|
206
333
|
if (-not $ToolInputRaw) {
|
|
@@ -224,11 +351,17 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
224
351
|
return [ordered]@{ decision = 'allow' }
|
|
225
352
|
}
|
|
226
353
|
|
|
227
|
-
# Write tool: validate the content payload. Edit tool:
|
|
228
|
-
#
|
|
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).
|
|
229
357
|
$content = $toolInput.content
|
|
230
358
|
if (-not $content) {
|
|
231
|
-
|
|
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
|
+
}
|
|
232
365
|
}
|
|
233
366
|
|
|
234
367
|
try {
|
|
@@ -244,14 +377,18 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
244
377
|
return [ordered]@{ decision = 'allow' }
|
|
245
378
|
}
|
|
246
379
|
|
|
247
|
-
$
|
|
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
|
|
248
385
|
if ($missing.Count -eq 0) {
|
|
249
386
|
return [ordered]@{ decision = 'allow' }
|
|
250
387
|
}
|
|
251
388
|
|
|
252
389
|
return [ordered]@{
|
|
253
390
|
decision = 'block'
|
|
254
|
-
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. 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."
|
|
255
392
|
}
|
|
256
393
|
}
|
|
257
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
|
+
}
|