@danmoisan/drm-copilot-mcp 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/mcp-server.js +82 -8
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/atomic-executor.md +1 -0
- package/resources/claude-customizations/.claude/agents/atomic-planner.md +1 -0
- package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -0
- package/resources/claude-customizations/.claude/agents/epic-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/feature-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/orchestrator.md +13 -0
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -0
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -0
- package/resources/claude-customizations/.claude/agents/staged-review.md +1 -0
- package/resources/claude-customizations/.claude/agents/status-updater.md +1 -0
- package/resources/claude-customizations/.claude/agents/typescript-engineer.md +1 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +182 -0
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +9 -1
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +17 -0
- package/resources/claude-customizations/.claude/settings.json +4 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +18 -1
- package/resources/claude-customizations/pack-manifests/core.json +1 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +8 -2
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +18 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +15 -14
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +144 -28
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +163 -0
package/out/mcp-server.js
CHANGED
|
@@ -16208,6 +16208,10 @@ var REPO_AUTOMATION_TOOL_DEFINITIONS = [
|
|
|
16208
16208
|
require_complete: {
|
|
16209
16209
|
type: "boolean",
|
|
16210
16210
|
description: "When true and artifact_type is 'orchestrator-state', require all phases to be complete."
|
|
16211
|
+
},
|
|
16212
|
+
require_model_routing: {
|
|
16213
|
+
type: "boolean",
|
|
16214
|
+
description: "When true and artifact_type is 'orchestrator-state', require a model_routing_receipts entry per delegated agent once a delegation is recorded. The TypeScript side performs the existence check only; the Python validator is authoritative for full per-receipt correctness."
|
|
16211
16215
|
}
|
|
16212
16216
|
},
|
|
16213
16217
|
required: ["artifact_type", "artifact_path"],
|
|
@@ -16597,6 +16601,7 @@ function resolveValidateOrchestrationArtifactsToolInput(rawInput, fallbackWorksp
|
|
|
16597
16601
|
);
|
|
16598
16602
|
}
|
|
16599
16603
|
const requireComplete = args["require_complete"];
|
|
16604
|
+
const requireModelRouting = args["require_model_routing"];
|
|
16600
16605
|
return {
|
|
16601
16606
|
workspaceRoot: normalizeWorkspaceRoot(
|
|
16602
16607
|
args["workspace_root"],
|
|
@@ -16604,7 +16609,8 @@ function resolveValidateOrchestrationArtifactsToolInput(rawInput, fallbackWorksp
|
|
|
16604
16609
|
),
|
|
16605
16610
|
artifactType,
|
|
16606
16611
|
artifactPath: normalizeRequiredText(args["artifact_path"], "artifact_path"),
|
|
16607
|
-
...requireComplete === true ? { requireComplete: true } : {}
|
|
16612
|
+
...requireComplete === true ? { requireComplete: true } : {},
|
|
16613
|
+
...requireModelRouting === true ? { requireModelRouting: true } : {}
|
|
16608
16614
|
};
|
|
16609
16615
|
}
|
|
16610
16616
|
|
|
@@ -22519,6 +22525,14 @@ var STEP_STATUS_KEYS = [
|
|
|
22519
22525
|
"step9_status",
|
|
22520
22526
|
"step10_status"
|
|
22521
22527
|
];
|
|
22528
|
+
var DELEGATING_AGENTS = /* @__PURE__ */ new Set([
|
|
22529
|
+
"atomic-planner",
|
|
22530
|
+
"atomic-executor",
|
|
22531
|
+
"feature-review",
|
|
22532
|
+
"task-researcher",
|
|
22533
|
+
"prd-feature",
|
|
22534
|
+
"pr-author"
|
|
22535
|
+
]);
|
|
22522
22536
|
function isObject7(value) {
|
|
22523
22537
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
22524
22538
|
}
|
|
@@ -22582,6 +22596,53 @@ function resolveRoutingMatrix(options) {
|
|
|
22582
22596
|
}
|
|
22583
22597
|
return void 0;
|
|
22584
22598
|
}
|
|
22599
|
+
function delegatedAgents(stateMap) {
|
|
22600
|
+
const agents = /* @__PURE__ */ new Set();
|
|
22601
|
+
const receipts = stateMap["delegation_receipts"];
|
|
22602
|
+
if (Array.isArray(receipts)) {
|
|
22603
|
+
receipts.forEach((receipt) => {
|
|
22604
|
+
if (!isObject7(receipt)) {
|
|
22605
|
+
return;
|
|
22606
|
+
}
|
|
22607
|
+
const agentName = receipt["agent_name"];
|
|
22608
|
+
if (typeof agentName === "string" && agentName.trim() !== "") {
|
|
22609
|
+
agents.add(agentName);
|
|
22610
|
+
}
|
|
22611
|
+
});
|
|
22612
|
+
}
|
|
22613
|
+
const nextStep = stateMap["next_step"];
|
|
22614
|
+
if (typeof nextStep === "string" && DELEGATING_AGENTS.has(nextStep)) {
|
|
22615
|
+
agents.add(nextStep);
|
|
22616
|
+
}
|
|
22617
|
+
return agents;
|
|
22618
|
+
}
|
|
22619
|
+
function validateModelRoutingExistence(stateMap) {
|
|
22620
|
+
const errors = [];
|
|
22621
|
+
const delegated = delegatedAgents(stateMap);
|
|
22622
|
+
if (delegated.size === 0) {
|
|
22623
|
+
return errors;
|
|
22624
|
+
}
|
|
22625
|
+
const receiptAgents2 = /* @__PURE__ */ new Set();
|
|
22626
|
+
const receipts = stateMap["model_routing_receipts"];
|
|
22627
|
+
if (Array.isArray(receipts)) {
|
|
22628
|
+
receipts.forEach((receipt) => {
|
|
22629
|
+
if (!isObject7(receipt)) {
|
|
22630
|
+
return;
|
|
22631
|
+
}
|
|
22632
|
+
const agent = receipt["agent"];
|
|
22633
|
+
if (typeof agent === "string" && agent.trim() !== "") {
|
|
22634
|
+
receiptAgents2.add(agent);
|
|
22635
|
+
}
|
|
22636
|
+
});
|
|
22637
|
+
}
|
|
22638
|
+
const missing = [...delegated].filter((a) => !receiptAgents2.has(a)).sort();
|
|
22639
|
+
for (const agent of missing) {
|
|
22640
|
+
errors.push(
|
|
22641
|
+
`Checkpoint model_routing_receipts is missing a receipt for delegated agent: ${agent}.`
|
|
22642
|
+
);
|
|
22643
|
+
}
|
|
22644
|
+
return errors;
|
|
22645
|
+
}
|
|
22585
22646
|
function validateOrchestratorStateText(text, options = {}) {
|
|
22586
22647
|
const errors = [];
|
|
22587
22648
|
let state;
|
|
@@ -22655,6 +22716,9 @@ function validateOrchestratorStateText(text, options = {}) {
|
|
|
22655
22716
|
})
|
|
22656
22717
|
);
|
|
22657
22718
|
}
|
|
22719
|
+
if (options.requireModelRouting === true) {
|
|
22720
|
+
errors.push(...validateModelRoutingExistence(stateMap));
|
|
22721
|
+
}
|
|
22658
22722
|
return errors;
|
|
22659
22723
|
}
|
|
22660
22724
|
|
|
@@ -23029,6 +23093,7 @@ function validateArtifact(input) {
|
|
|
23029
23093
|
case "orchestrator-state": {
|
|
23030
23094
|
const options = {
|
|
23031
23095
|
...input.requireComplete === void 0 ? {} : { requireComplete: input.requireComplete },
|
|
23096
|
+
...input.requireModelRouting === void 0 ? {} : { requireModelRouting: input.requireModelRouting },
|
|
23032
23097
|
...input.fs === void 0 ? {} : { fs: input.fs },
|
|
23033
23098
|
...input.root === void 0 ? {} : { root: input.root },
|
|
23034
23099
|
...input.routingMatrix === void 0 ? {} : { routingMatrix: input.routingMatrix }
|
|
@@ -23056,6 +23121,7 @@ function validateOrchestrationServiceCall(input) {
|
|
|
23056
23121
|
artifactType: input.artifactType,
|
|
23057
23122
|
text,
|
|
23058
23123
|
...input.requireComplete === void 0 ? {} : { requireComplete: input.requireComplete },
|
|
23124
|
+
...input.requireModelRouting === void 0 ? {} : { requireModelRouting: input.requireModelRouting },
|
|
23059
23125
|
fs: input.fileSystem,
|
|
23060
23126
|
root: input.workspaceRoot
|
|
23061
23127
|
});
|
|
@@ -23072,6 +23138,18 @@ ${errors.join("\n")}`
|
|
|
23072
23138
|
};
|
|
23073
23139
|
}
|
|
23074
23140
|
|
|
23141
|
+
// ../../extensions/drm-copilot/src/lib/validate/build-validate-orchestration-service-call-input.ts
|
|
23142
|
+
function buildValidateOrchestrationServiceCallInput(fileSystem, input) {
|
|
23143
|
+
return {
|
|
23144
|
+
fileSystem,
|
|
23145
|
+
workspaceRoot: input.workspaceRoot,
|
|
23146
|
+
artifactType: input.artifactType,
|
|
23147
|
+
artifactPath: input.artifactPath,
|
|
23148
|
+
...input.requireComplete === void 0 ? {} : { requireComplete: input.requireComplete },
|
|
23149
|
+
...input.requireModelRouting === void 0 ? {} : { requireModelRouting: input.requireModelRouting }
|
|
23150
|
+
};
|
|
23151
|
+
}
|
|
23152
|
+
|
|
23075
23153
|
// ../../extensions/drm-copilot/src/lib/new-potential-bug-entry.ts
|
|
23076
23154
|
var fs5 = __toESM(require("node:fs"));
|
|
23077
23155
|
var nodePath4 = __toESM(require("node:path"));
|
|
@@ -27713,13 +27791,9 @@ var DefaultRepoAutomationService = class {
|
|
|
27713
27791
|
});
|
|
27714
27792
|
}
|
|
27715
27793
|
async validateOrchestrationArtifacts(input) {
|
|
27716
|
-
return validateOrchestrationServiceCall(
|
|
27717
|
-
|
|
27718
|
-
|
|
27719
|
-
artifactType: input.artifactType,
|
|
27720
|
-
artifactPath: input.artifactPath,
|
|
27721
|
-
...input.requireComplete === void 0 ? {} : { requireComplete: input.requireComplete }
|
|
27722
|
-
});
|
|
27794
|
+
return validateOrchestrationServiceCall(
|
|
27795
|
+
buildValidateOrchestrationServiceCallInput(this.fileSystem, input)
|
|
27796
|
+
);
|
|
27723
27797
|
}
|
|
27724
27798
|
async executeScript(options) {
|
|
27725
27799
|
const execution = await executeBundledScriptFromExtensionRoot(this.output, {
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: csharp-typed-engineer
|
|
3
|
+
model: sonnet
|
|
3
4
|
description: Project-scoped worker that implements and verifies C# changes within typed repository boundaries. Applies the CSharpier -> .NET Analyzers -> Nullable Analysis -> xUnit toolchain, the 1-3 production-file small-path budget, and zero-regression quality gates.
|
|
4
5
|
tools:
|
|
5
6
|
- Read
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: epic-orchestrator
|
|
3
|
+
model: opus
|
|
3
4
|
description: Deterministic epic-scale orchestrator that schedules a dependency graph of child features across parallel, isolated git worktrees, fans results back into a shared integration branch, and drives the final integration-to-main PR. Distinct from orchestrator; only this agent is authorized to delegate to Agent(orchestrator).
|
|
4
5
|
tools:
|
|
5
6
|
- "Agent(orchestrator)"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: orchestrator
|
|
3
|
+
model: opus
|
|
3
4
|
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
5
|
tools:
|
|
5
6
|
- "Agent(atomic-planner,atomic-executor,feature-review,task-researcher,prd-feature,staged-review,epic-review,status-updater,pr-author,commit-message,human-exception-runbook,python-typed-engineer,powershell-typed-engineer,csharp-typed-engineer,typescript-engineer)"
|
|
@@ -56,6 +57,18 @@ On every invocation:
|
|
|
56
57
|
4. If a valid checkpoint exists with a matching objective, resume from the recorded `next_step`.
|
|
57
58
|
5. If no checkpoint exists or the objective is new, begin from change-budget estimation.
|
|
58
59
|
|
|
60
|
+
### Model-choice reconciliation on resume
|
|
61
|
+
|
|
62
|
+
When the resumed `next_step` is a delegating step, repair a missing model choice deterministically before delegating (this mirrors `## Checkpoint Handling` in `.claude/skills/orchestrate/SKILL.md`):
|
|
63
|
+
|
|
64
|
+
a. Run the orchestrator-state validator with `--require-model-routing` before the first delegation and record a `model_routing_preflight` block `{ status ("pass"|"fail"), checked_at, validator_command, output_summary }`.
|
|
65
|
+
b. Recompute the upcoming phase's floor with `compute_complexity_floor(signals_present)` (no reimplementation).
|
|
66
|
+
c. Record a `complexity_assessments[]` entry `{ phase, band, floor, signals_present[], rationale, assessed_at }` with `floor` equal to the recomputed value and `band >= floor`.
|
|
67
|
+
d. Resolve the model with `resolve_delegation_model(agent, complexity_band, fable_policy)` and record a `model_routing_receipts[]` entry `{ agent, phase, complexity_band, fable_policy, table_model, clamped_from | null, model }`.
|
|
68
|
+
e. Persist the checkpoint, then delegate with `model` equal to the receipt's `model`.
|
|
69
|
+
|
|
70
|
+
The orchestrator MUST NOT delegate at a delegating `next_step` while `model_routing_preflight` status is `fail`; it repairs the missing choice (steps b-e) and re-preflights until the status is `pass`.
|
|
71
|
+
|
|
59
72
|
## Change Budget Routing
|
|
60
73
|
|
|
61
74
|
The first action is always to estimate the change budget by identifying likely affected production files and tests:
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: powershell-typed-engineer
|
|
3
|
+
model: sonnet
|
|
3
4
|
description: Project-scoped worker that implements and verifies PowerShell changes within typed repository boundaries. Applies PoshQC format -> PSScriptAnalyzer -> Pester toolchain, the 1-2 production-file direct-mode budget, the 3-production + 3-test per-batch cap, and zero-regression quality gates.
|
|
4
5
|
tools:
|
|
5
6
|
- Read
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: python-typed-engineer
|
|
3
|
+
model: sonnet
|
|
3
4
|
description: Project-scoped worker that implements and verifies Python changes within typed repository boundaries. Applies the Black -> Ruff -> Pyright -> Pytest toolchain, the 3-production + 3-test per-batch budget, and zero-regression quality gates.
|
|
4
5
|
tools:
|
|
5
6
|
- Read
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pre-tool-use hook that blocks a delegation to a gated subagent when the
|
|
4
|
+
orchestrator checkpoint records no model-routing receipt for that agent.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
|
|
8
|
+
tool input JSON from the CLAUDE_TOOL_INPUT environment variable and the
|
|
9
|
+
orchestrator checkpoint from artifacts/orchestration/orchestrator-state.json.
|
|
10
|
+
|
|
11
|
+
The hook enforces presence only: it cannot read the delegate's chosen
|
|
12
|
+
`model` (no `model` field is exposed in the tool input), so it verifies that
|
|
13
|
+
a `model_routing_receipts[]` entry already exists for the target
|
|
14
|
+
`subagent_type`. Correctness of the recorded model stays with the
|
|
15
|
+
authoritative Python validator.
|
|
16
|
+
|
|
17
|
+
Gated subagent types are the Agent-tool delegates that participate in model
|
|
18
|
+
selection: atomic-planner, atomic-executor, feature-review, task-researcher,
|
|
19
|
+
prd-feature, pr-author. The `orchestrator` type is deliberately excluded: it
|
|
20
|
+
is the calling agent, not a subagent delegated via the Agent tool, so it is
|
|
21
|
+
never a receipt-gated `subagent_type`.
|
|
22
|
+
|
|
23
|
+
Allow-through (graceful allow) applies to a non-delegating `subagent_type`,
|
|
24
|
+
empty or absent tool input, and malformed tool-input JSON.
|
|
25
|
+
|
|
26
|
+
The checkpoint read goes through Get-ModelRoutingCheckpoint so tests can
|
|
27
|
+
inject a synthetic checkpoint without touching disk.
|
|
28
|
+
|
|
29
|
+
.NOTES
|
|
30
|
+
Compatible with PowerShell 7+. Read-only presence-gating deterrent.
|
|
31
|
+
#>
|
|
32
|
+
[CmdletBinding()]
|
|
33
|
+
param()
|
|
34
|
+
|
|
35
|
+
function Get-ModelRoutingCheckpoint {
|
|
36
|
+
<#
|
|
37
|
+
.SYNOPSIS
|
|
38
|
+
Returns the parsed orchestrator checkpoint object, or $null when the
|
|
39
|
+
file is missing or not valid JSON. Tests mock this seam.
|
|
40
|
+
#>
|
|
41
|
+
[CmdletBinding()]
|
|
42
|
+
[OutputType([object])]
|
|
43
|
+
param(
|
|
44
|
+
[string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if (-not (Test-Path -LiteralPath $CheckpointPath -PathType Leaf)) {
|
|
48
|
+
return $null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
$raw = Get-Content -LiteralPath $CheckpointPath -Raw -ErrorAction Stop
|
|
53
|
+
return $raw | ConvertFrom-Json -ErrorAction Stop
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return $null
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Get-ModelRoutingGatedAgent {
|
|
61
|
+
<#
|
|
62
|
+
.SYNOPSIS
|
|
63
|
+
Returns the set of subagent types that are receipt-gated: the Agent-tool
|
|
64
|
+
delegates that participate in model selection. `orchestrator` is
|
|
65
|
+
excluded because it is the caller, not a delegated subagent.
|
|
66
|
+
#>
|
|
67
|
+
[CmdletBinding()]
|
|
68
|
+
[OutputType([string[]])]
|
|
69
|
+
param()
|
|
70
|
+
|
|
71
|
+
return [string[]] @(
|
|
72
|
+
'atomic-planner',
|
|
73
|
+
'atomic-executor',
|
|
74
|
+
'feature-review',
|
|
75
|
+
'task-researcher',
|
|
76
|
+
'prd-feature',
|
|
77
|
+
'pr-author'
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function Test-ModelRoutingReceiptPresent {
|
|
82
|
+
<#
|
|
83
|
+
.SYNOPSIS
|
|
84
|
+
Returns $true when the checkpoint carries a model_routing_receipts entry
|
|
85
|
+
whose agent equals the target subagent type.
|
|
86
|
+
#>
|
|
87
|
+
[CmdletBinding()]
|
|
88
|
+
[OutputType([bool])]
|
|
89
|
+
param(
|
|
90
|
+
[Parameter(Mandatory)]
|
|
91
|
+
[AllowNull()]
|
|
92
|
+
$Checkpoint,
|
|
93
|
+
|
|
94
|
+
[Parameter(Mandatory)]
|
|
95
|
+
[string] $Subagent
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if ($null -eq $Checkpoint) {
|
|
99
|
+
return $false
|
|
100
|
+
}
|
|
101
|
+
if ($Checkpoint.PSObject.Properties.Name -notcontains 'model_routing_receipts') {
|
|
102
|
+
return $false
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
# Scan every receipt for one whose agent matches the delegated subagent type.
|
|
106
|
+
foreach ($receipt in @($Checkpoint.model_routing_receipts)) {
|
|
107
|
+
if ($null -eq $receipt) {
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
if ($receipt.PSObject.Properties.Name -contains 'agent' -and
|
|
111
|
+
[string]$receipt.agent -eq $Subagent) {
|
|
112
|
+
return $true
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return $false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function Invoke-ModelRoutingReceiptDecision {
|
|
119
|
+
<#
|
|
120
|
+
.SYNOPSIS
|
|
121
|
+
Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision object.
|
|
122
|
+
#>
|
|
123
|
+
[CmdletBinding()]
|
|
124
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
125
|
+
param(
|
|
126
|
+
[string] $ToolInputRaw
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
$allow = [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
130
|
+
|
|
131
|
+
# Empty or absent tool input is not a delegation this hook can gate.
|
|
132
|
+
if (-not $ToolInputRaw) {
|
|
133
|
+
return $allow
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
# Malformed tool-input JSON is allowed through gracefully; this hook is a
|
|
137
|
+
# deterrent, not the authoritative validator.
|
|
138
|
+
try {
|
|
139
|
+
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return $allow
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
$subagent = [string]$toolInput.subagent_type
|
|
146
|
+
|
|
147
|
+
# Only the gated Agent-tool delegates are receipt-checked; any other
|
|
148
|
+
# subagent_type (including orchestrator) passes through.
|
|
149
|
+
if (-not $subagent -or ((Get-ModelRoutingGatedAgent) -notcontains $subagent)) {
|
|
150
|
+
return $allow
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
$checkpoint = Get-ModelRoutingCheckpoint
|
|
154
|
+
if (Test-ModelRoutingReceiptPresent -Checkpoint $checkpoint -Subagent $subagent) {
|
|
155
|
+
return $allow
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return [ordered]@{
|
|
159
|
+
hookSpecificOutput = [ordered]@{
|
|
160
|
+
hookEventName = 'PreToolUse'
|
|
161
|
+
permissionDecision = 'deny'
|
|
162
|
+
permissionDecisionReason = "MODEL_ROUTING_RECEIPT_BLOCKED: cannot delegate to '$subagent' before a model_routing_receipts entry for it is recorded in the orchestrator checkpoint. Perform Model Selection (record the complexity assessment and routing receipt) before delegating."
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
168
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
$decision = Invoke-ModelRoutingReceiptDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
Write-Error $_
|
|
177
|
+
exit 1
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
181
|
+
|
|
182
|
+
exit 0
|
|
@@ -180,7 +180,7 @@ function Invoke-RoutingContractValidation {
|
|
|
180
180
|
[scriptblock] $Invoker = {
|
|
181
181
|
param($Path, $Type)
|
|
182
182
|
$output = & python -m scripts.dev_tools.validate_orchestration_artifacts `
|
|
183
|
-
$Type $Path --require-complete 2>&1
|
|
183
|
+
$Type $Path --require-complete --require-model-routing 2>&1
|
|
184
184
|
[pscustomobject]@{
|
|
185
185
|
ExitCode = $LASTEXITCODE
|
|
186
186
|
Output = ($output | Out-String)
|
|
@@ -293,6 +293,14 @@ function Invoke-OrchestratorOutputValidation {
|
|
|
293
293
|
}
|
|
294
294
|
$routingResult = Invoke-RoutingContractValidation @routingArgs
|
|
295
295
|
if ($routingResult.HasErrors) {
|
|
296
|
+
# One subprocess call now covers both --require-complete and
|
|
297
|
+
# --require-model-routing. Surface a model-routing gate failure under its
|
|
298
|
+
# own block reason (its errors name model_routing_receipts or
|
|
299
|
+
# complexity_assessments); otherwise fall back to the routing-contract
|
|
300
|
+
# block reason for a generic completion/routing failure.
|
|
301
|
+
if ($routingResult.ErrorText -match 'model_routing_receipts|complexity_assessments') {
|
|
302
|
+
return @{ Ok = $false; Message = "MODEL_ROUTING_BLOCKED: $($routingResult.ErrorText)" }
|
|
303
|
+
}
|
|
296
304
|
return @{ Ok = $false; Message = "ROUTING_CONTRACT_BLOCKED: $($routingResult.ErrorText)" }
|
|
297
305
|
}
|
|
298
306
|
|
|
@@ -66,10 +66,27 @@ Each entry records one delegation with the shape `{ agent, phase, complexity_ban
|
|
|
66
66
|
|
|
67
67
|
The session `model_budget.fable_policy` switch is a three-way enum `disabled | available | preferred` defined in `config/orchestration-routing.json`, defaulting to `disabled`. It governs only the delegation model tier and is not a route input. `disabled` removes `fable` from the consideration set and clamps `fable` cells to `opus`; `available` applies the base `complexity_to_model` table as-is; `preferred` applies the `preferred_overlay` (which redirects only the C3 cell to `fable` for the overlay agents `atomic-planner`, `prd-feature`, `feature-review`, `task-researcher`) and leaves `atomic-executor` and `pr-author` C3 cells at `opus`. `route` is never an input to model selection.
|
|
68
68
|
|
|
69
|
+
## Require-Model-Routing Mode Scope and Backward Compatibility
|
|
70
|
+
|
|
71
|
+
The complexity-assessment and model-routing-receipt invariants above are key-gated: they run only when their key is present, so a checkpoint that omits both arrays passes at every stage. The `require_model_routing` mode adds an existence gate that closes that gap without changing the default behavior. It is an opt-in keyword on `validate_orchestrator_state_text(..., require_model_routing=False)` (CLI flag `--require-model-routing`; MCP parameter `require_model_routing`), defaulting off. Plain, `require_complete`, and `require_pr_creation_ready` calls are unaffected and produce byte-identical results.
|
|
72
|
+
|
|
73
|
+
## Invariants (require_model_routing mode)
|
|
74
|
+
|
|
75
|
+
These invariants apply only when a caller passes `require_model_routing=True` and the checkpoint records at least one delegation. A checkpoint with zero delegations (no well-formed `delegation_receipts[]` entry and a `next_step` that names no delegating agent) imposes no requirement, so genuinely old, delegation-free checkpoints stay valid.
|
|
76
|
+
|
|
77
|
+
1. **Required routing receipt once delegated.** Once the checkpoint records a delegation, the set of `model_routing_receipts[].agent` must be a superset of the delegated-agent set (each well-formed `delegation_receipts[].agent_name` plus a `next_step` that names a delegating agent). A delegated agent with no matching receipt is a violation. The delegating agent set excludes `orchestrator` (the caller, not a delegated subagent).
|
|
78
|
+
|
|
79
|
+
2. **Required complexity assessment per matched phase.** Each phase named by a routing receipt whose agent is in the delegated-agent set must have a `complexity_assessments[]` entry for that phase.
|
|
80
|
+
|
|
81
|
+
3. **Per-entry consistency reused, not reimplemented.** Present receipts and assessments must satisfy the model-routing-receipt and complexity-assessment invariants above; the gate reuses `_validate_model_routing_receipts` and `_validate_complexity_assessments` and never reimplements `compute_complexity_floor` or `resolve_delegation_model`. The gate logic lives in `scripts/dev_tools/_orchestrator_state_model_routing_gate.py`; enforcement is the Python validator, not an imported schema.
|
|
82
|
+
|
|
83
|
+
The completion hook (`.claude/hooks/validate-orchestrator-output.ps1`) passes `--require-model-routing` alongside `--require-complete` and surfaces a gate failure as the `MODEL_ROUTING_BLOCKED:` block reason. The PreToolUse deterrent (`.claude/hooks/enforce-model-routing-receipt.ps1`) performs presence-only gating before a delegation. The MCP TypeScript surface performs the existence check only (delegated-agent set ⊆ routing-receipt-agent set); the Python validator remains authoritative for per-receipt correctness.
|
|
84
|
+
|
|
69
85
|
## Enforcement
|
|
70
86
|
|
|
71
87
|
- `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated invariant when a `remediation_loop` is present, using the existing validator message style (literal, checkpoint-context prefixed). The validator returns a list of error strings and does not mutate its input.
|
|
72
88
|
- `scripts/dev_tools/validate_orchestrator_state.py` likewise appends one error per violated `human_interaction` invariant when a `human_interaction` key is present, using the same literal, checkpoint-context-prefixed message style. The check does not import or read any schema file.
|
|
73
89
|
- `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated `complexity_assessments` invariant when a `complexity_assessments` key is present, delegating to `scripts/dev_tools/_orchestrator_state_complexity.py`, which recomputes the floor via `compute_complexity_floor`. The check does not import or read any schema file.
|
|
74
90
|
- `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated `model_routing_receipts` invariant when a `model_routing_receipts` key is present, delegating to `scripts/dev_tools/_orchestrator_state_model_routing.py`, which recomputes the resolved model via `resolve_delegation_model`. The check does not import or read any schema file.
|
|
91
|
+
- `scripts/dev_tools/validate_orchestrator_state.py` appends one error per violated `require_model_routing` invariant only when the caller passes `require_model_routing=True`, delegating to `scripts/dev_tools/_orchestrator_state_model_routing_gate.py`, which reuses the complexity and model-routing per-entry validators. When the flag is not passed the gate does not run, so existing calls are byte-identical.
|
|
75
92
|
- The validator is consumed by the MCP tool `validate_orchestration_artifacts`; backward compatibility for existing step-based checkpoints is preserved.
|
|
@@ -156,6 +156,10 @@
|
|
|
156
156
|
{
|
|
157
157
|
"type": "command",
|
|
158
158
|
"command": "pwsh -NoProfile -File .claude/hooks/enforce-epic-wave-barrier.ps1"
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
"type": "command",
|
|
162
|
+
"command": "pwsh -NoProfile -File .claude/hooks/enforce-model-routing-receipt.ps1"
|
|
159
163
|
}
|
|
160
164
|
]
|
|
161
165
|
}
|
|
@@ -24,6 +24,18 @@ On every invocation, the main session must:
|
|
|
24
24
|
2. If a valid checkpoint exists with a matching objective, resume from the recorded `next_step`.
|
|
25
25
|
3. If no checkpoint exists or the objective is new, begin the orchestration lifecycle from the start.
|
|
26
26
|
|
|
27
|
+
### Model-choice reconciliation on resume
|
|
28
|
+
|
|
29
|
+
Because model selection is required once delegation occurs (see `## Model Selection`), a resuming orchestrator must repair a missing model choice deterministically before delegating at a delegating `next_step`. When the resumed `next_step` is a delegating step:
|
|
30
|
+
|
|
31
|
+
a. **Preflight the checkpoint.** Run the orchestrator-state validator with `--require-model-routing` (via `mcp__drm-copilot__validate_orchestration_artifacts` or the local CLI) against `artifacts/orchestration/orchestrator-state.json` before the first delegation. Record the result in a `model_routing_preflight` block `{ status ("pass"|"fail"), checked_at (ISO-8601 UTC), validator_command, output_summary }`.
|
|
32
|
+
b. **Recompute the floor.** For the upcoming phase, recompute the complexity floor with `compute_complexity_floor(signals_present)` (`scripts/dev_tools/compute_complexity_floor.py`); do not reimplement the formula.
|
|
33
|
+
c. **Record the assessment.** Write a `complexity_assessments[]` entry `{ phase, band, floor, signals_present[], rationale, assessed_at }` with `floor` equal to the recomputed value and `band >= floor`.
|
|
34
|
+
d. **Resolve and record the receipt.** Resolve the model with `resolve_delegation_model(agent, complexity_band, fable_policy)` (`scripts/dev_tools/resolve_delegation_model.py`) and write a `model_routing_receipts[]` entry `{ agent, phase, complexity_band, fable_policy, table_model, clamped_from | null, model }`.
|
|
35
|
+
e. **Persist and delegate.** Persist the checkpoint, then delegate with `model` equal to the receipt's `model`.
|
|
36
|
+
|
|
37
|
+
The orchestrator MUST NOT delegate at a delegating `next_step` while `model_routing_preflight` status is `fail`; it repairs the missing choice (steps b-e) and re-preflights until the status is `pass`.
|
|
38
|
+
|
|
27
39
|
## Autonomous-Execution Mandate
|
|
28
40
|
|
|
29
41
|
The orchestrator must achieve all actions agentically with no human interaction; full autonomy is a hard requirement. A silent manual blocker discovered at the end of a workflow is a defect, not an acceptable outcome. Every unautomatable (human-interaction) requirement must be detected early, resolved by exactly one of three permitted responses, and recorded in orchestrator state.
|
|
@@ -81,7 +93,11 @@ End-to-end procedure:
|
|
|
81
93
|
3. **Run the per-delegation selection order.** For each delegation, resolve the model as `resolve_delegation_model(agent, complexity_band, fable_policy)`: the `table_model` is the `preferred` overlay value when (`fable_policy == "preferred"` and the agent is in the overlay set `{atomic-planner, prd-feature, feature-review, task-researcher}` and `band == "C3"`), otherwise the base `complexity_to_model[band]`. Under `fable_policy == "disabled"`, a `fable` `table_model` clamps to `model = "opus"` with `clamped_from = "fable"`. `atomic-executor` and `pr-author` C3 cells stay `opus` under every policy.
|
|
82
94
|
4. **Emit a routing receipt.** Record a `model_routing_receipts[]` entry `{ agent, phase, complexity_band, fable_policy, table_model, clamped_from | null, model }`. `table_model` is the pre-clamp lookup; `model` is the post-clamp result.
|
|
83
95
|
|
|
84
|
-
The `complexity_assessments[]` and `model_routing_receipts[]` invariants are enforced by `scripts/dev_tools/validate_orchestrator_state.py` per `.claude/rules/orchestrator-state.md`; both arrays
|
|
96
|
+
The `complexity_assessments[]` and `model_routing_receipts[]` invariants are enforced by `scripts/dev_tools/validate_orchestrator_state.py` per `.claude/rules/orchestrator-state.md`; both arrays remain additive (a checkpoint that predates model routing stays valid).
|
|
97
|
+
|
|
98
|
+
### Required-once-delegated invariant (`require_model_routing` mode)
|
|
99
|
+
|
|
100
|
+
The `validate_orchestrator_state_text(...)` validator accepts a `require_model_routing` mode (CLI flag `--require-model-routing`; MCP parameter `require_model_routing`). Under this mode the arrays stop being merely optional: once the checkpoint records at least one delegation (a well-formed `delegation_receipts[]` entry, or a `next_step` that names a delegating agent), every delegated agent must have a matching `model_routing_receipts[]` entry, each matched receipt's phase must have a `complexity_assessments[]` entry, and every present receipt/assessment must be consistent with the reference formulas. A delegation-free checkpoint imposes no requirement, so old checkpoints stay valid. The gate is implemented in `scripts/dev_tools/_orchestrator_state_model_routing_gate.py`; it reuses the per-entry validators and never reimplements `compute_complexity_floor` or `resolve_delegation_model`. Two enforcement layers consume it: the completion gate (`.claude/hooks/validate-orchestrator-output.ps1` passes `--require-model-routing` and surfaces failures as `MODEL_ROUTING_BLOCKED:`), and the pre-delegation deterrent (`.claude/hooks/enforce-model-routing-receipt.ps1`, presence-only). The MCP TypeScript surface performs the existence check only (delegated-agent set ⊆ routing-receipt-agent set); the Python validator is authoritative for per-receipt correctness.
|
|
85
101
|
|
|
86
102
|
**`fork` caveat.** A skill whose frontmatter `context` field holds the value `fork` inherits the parent model and ignores a model override. Model selection therefore applies to agent delegations, not to fork-routed skill invocations.
|
|
87
103
|
|
|
@@ -126,6 +142,7 @@ The orchestrator must not report completion until:
|
|
|
126
142
|
1. All required artifacts for the selected workflow path are present on disk.
|
|
127
143
|
2. All validation gates (toolchain, acceptance criteria, audit artifacts) have passed.
|
|
128
144
|
3. The checkpoint file at `artifacts/orchestration/orchestrator-state.json` reflects the completed state.
|
|
145
|
+
4. The model-routing gate passes: `.claude/hooks/validate-orchestrator-output.ps1` runs the validator with `--require-model-routing` alongside `--require-complete` and refuses DONE with `MODEL_ROUTING_BLOCKED:` when a recorded delegation lacks a matching `model_routing_receipts[]` / `complexity_assessments[]` entry (see the required-once-delegated invariant under `## Model Selection`).
|
|
129
146
|
|
|
130
147
|
## Pre-Feature-Review Commit
|
|
131
148
|
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
".claude/hooks/enforce-epic-worktree-removal-gate.ps1",
|
|
23
23
|
".claude/hooks/enforce-evidence-locations.ps1",
|
|
24
24
|
".claude/hooks/enforce-feature-folder-order.ps1",
|
|
25
|
+
".claude/hooks/enforce-model-routing-receipt.ps1",
|
|
25
26
|
".claude/hooks/enforce-orchestration-preimplementation-gate.ps1",
|
|
26
27
|
".claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1",
|
|
27
28
|
".claude/hooks/enforce-pr-author-skill.ps1",
|
|
@@ -101,8 +101,14 @@ Lifecycle guardrails:
|
|
|
101
101
|
- `DIRECTIVE: MINIMAL-AUDIT PLAN REQUIRED`
|
|
102
102
|
|
|
103
103
|
8a) Resolve and persist `${plan-path}` before delegation:
|
|
104
|
-
-
|
|
105
|
-
-
|
|
104
|
+
- enumerate `${feature-folder}/plan*.md` files in deterministic filename order
|
|
105
|
+
- reuse the first existing `plan*.md` in `${feature-folder}` when present
|
|
106
|
+
- otherwise create exactly one canonical plan file path using the repository's
|
|
107
|
+
feature-folder plan naming convention and reuse it for all revisions
|
|
108
|
+
- never default to `${feature-folder}/plan.md` when a timestamped scaffolded
|
|
109
|
+
plan already exists
|
|
110
|
+
- if checkpoint state already contains a different `${plan-path}`, correct the
|
|
111
|
+
checkpoint before planner delegation instead of creating another plan file
|
|
106
112
|
|
|
107
113
|
9) Require preflight validation via `atomic_executor` until:
|
|
108
114
|
- `PREFLIGHT: ALL CLEAR`
|
|
@@ -109,6 +109,24 @@ staging, commits, or implementation delegation. This gate covers edits, formatte
|
|
|
109
109
|
If any required item is missing, implementation is blocked until the checkpoint
|
|
110
110
|
and lifecycle state are corrected.
|
|
111
111
|
|
|
112
|
+
## Plan-Path Resolution Gate
|
|
113
|
+
|
|
114
|
+
After active feature folder creation and before any planning delegation, the
|
|
115
|
+
main session must resolve `${plan-path}` from the active feature folder:
|
|
116
|
+
|
|
117
|
+
1. Enumerate existing `${feature-folder}/plan*.md` files in deterministic
|
|
118
|
+
filename order.
|
|
119
|
+
2. If one or more files exist, persist `${plan-path}` as the first existing
|
|
120
|
+
file and require every planner and executor handoff to use that exact path.
|
|
121
|
+
3. If no `plan*.md` file exists, create exactly one canonical target path using
|
|
122
|
+
the repository's feature-folder plan naming convention, persist that path,
|
|
123
|
+
and reuse it for all revisions.
|
|
124
|
+
4. Do not default to `${feature-folder}/plan.md` when a timestamped scaffolded
|
|
125
|
+
plan already exists.
|
|
126
|
+
5. If checkpoint state names a different plan path than the resolved existing
|
|
127
|
+
plan file, correct the checkpoint before planner delegation. Do not create a
|
|
128
|
+
second plan artifact to satisfy an incorrect checkpoint value.
|
|
129
|
+
|
|
112
130
|
## Pre-Implementation Violation Handling
|
|
113
131
|
|
|
114
132
|
If an implementation action is attempted before a required orchestration gate
|
package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md
CHANGED
|
@@ -101,6 +101,16 @@ Persist and reuse these fields exactly:
|
|
|
101
101
|
- `lifecycle_operations`
|
|
102
102
|
- `pre-implementation-violation`
|
|
103
103
|
|
|
104
|
+
Plan-path invariant:
|
|
105
|
+
- `${plan-path}` is resolved only after `${feature-folder}` exists.
|
|
106
|
+
- The orchestrator MUST enumerate existing `${feature-folder}/plan*.md` files
|
|
107
|
+
in deterministic filename order before planner delegation.
|
|
108
|
+
- If any existing plan file is present, `${plan-path}` MUST be that first
|
|
109
|
+
existing file. Do not persist or delegate against `${feature-folder}/plan.md`
|
|
110
|
+
when a timestamped scaffolded plan already exists.
|
|
111
|
+
- If checkpoint state conflicts with the resolved existing plan file, correct
|
|
112
|
+
checkpoint state before delegation and do not create a duplicate plan.
|
|
113
|
+
|
|
104
114
|
For small-path runs, also persist:
|
|
105
115
|
- `bootstrap_mode`
|
|
106
116
|
- `phase0_execution_summary`
|
|
@@ -310,6 +320,9 @@ Required behavior:
|
|
|
310
320
|
7. When those specialists are not yet migrated, perform the authoring steps directly without changing template headings.
|
|
311
321
|
8. Spawn `atomic-planner` to finalize `${plan-path}` and require `PREFLIGHT: ALL CLEAR`.
|
|
312
322
|
Hard enforcement for Step 7:
|
|
323
|
+
- Before spawning `atomic-planner`, resolve `${plan-path}` by enumerating
|
|
324
|
+
existing `${feature-folder}/plan*.md` files. Reuse the first existing file
|
|
325
|
+
in deterministic filename order, including timestamped scaffolded plans.
|
|
313
326
|
- The planning route MUST be `atomic-planner -> atomic-executor` for preflight validation.
|
|
314
327
|
- The planner MUST update `${plan-path}` in place and MUST NOT create additional `plan.*.md` files for revisions.
|
|
315
328
|
- The approved plan MUST include explicit Phase 0 baseline evidence tasks and explicit final-QA evidence or coverage tasks for each language in scope where policy requires them.
|
|
@@ -76,6 +76,8 @@ Every agent named above must exist as a native Codex agent under `.codex/agents/
|
|
|
76
76
|
- Do not rename, back up, or create sidecar checkpoint files.
|
|
77
77
|
- Do not create or edit `${feature-folder}/issue.md`, `${feature-folder}/spec.md`, `${feature-folder}/user-story.md`, or `plan*.md` until lifecycle setup succeeds.
|
|
78
78
|
- Do not persist placeholder lifecycle values such as `NONE`, `TBD`, or empty strings once lifecycle setup begins.
|
|
79
|
+
- After active folder creation and before planner delegation, resolve `${plan-path}` by enumerating existing `${feature-folder}/plan*.md` files in deterministic filename order. If a scaffolded timestamped plan exists, persist and delegate against that exact file. Do not default to `${feature-folder}/plan.md` and do not create a second plan artifact when a plan already exists.
|
|
80
|
+
- For issue #306, the canonical existing plan path is `docs/features/active/2026-07-04-codex-agent-role-config-306/plan.2026-07-04T13-47.md`; reuse that exact file and do not create `docs/features/active/2026-07-04-codex-agent-role-config-306/plan.md`.
|
|
79
81
|
|
|
80
82
|
## Checkpoint Persistence
|
|
81
83
|
|
|
@@ -133,17 +135,16 @@ The MCP validator and required CI checks are the hard completion boundary. There
|
|
|
133
135
|
Do not claim mission completion unless all required delegations completed with receipts and the required orchestration artifacts exist on disk.
|
|
134
136
|
'''
|
|
135
137
|
|
|
136
|
-
[
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
pr-base-branch-merge-base = true
|
|
138
|
+
[skills]
|
|
139
|
+
config = [
|
|
140
|
+
{ name = "policy-compliance-order", enabled = true },
|
|
141
|
+
{ name = "orchestrate", enabled = true },
|
|
142
|
+
{ name = "orchestrator-workflow", enabled = true },
|
|
143
|
+
{ name = "feature-promotion-lifecycle", enabled = true },
|
|
144
|
+
{ name = "repo-automation-adapter", enabled = true },
|
|
145
|
+
{ name = "atomic-plan-contract", enabled = true },
|
|
146
|
+
{ name = "acceptance-criteria-tracking", enabled = true },
|
|
147
|
+
{ name = "evidence-and-timestamp-conventions", enabled = true },
|
|
148
|
+
{ name = "pr-context-artifacts", enabled = true },
|
|
149
|
+
{ name = "pr-base-branch-merge-base", enabled = true },
|
|
150
|
+
]
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1
CHANGED
|
@@ -23,9 +23,11 @@
|
|
|
23
23
|
variables.feature-folder);
|
|
24
24
|
- a ci_gate object with conclusion == "success" and a non-empty head_sha.
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
When completion evidence is missing the hook emits a PreToolUse JSON
|
|
27
|
+
response with hookSpecificOutput.permissionDecision='deny' and a reason that
|
|
28
|
+
names the specific missing evidence so the caller can remediate. When
|
|
29
|
+
completion is not asserted, the write is allowed via
|
|
30
|
+
hookSpecificOutput.permissionDecision='allow' (backward compatibility).
|
|
29
31
|
|
|
30
32
|
Edit tool calls supply only old_string/new_string (a partial patch) and
|
|
31
33
|
cannot be reliably validated without the full target file content, so they
|
|
@@ -39,6 +41,11 @@
|
|
|
39
41
|
[CmdletBinding()]
|
|
40
42
|
param()
|
|
41
43
|
|
|
44
|
+
# Dot-source the shared validation helpers. Guarded so a missing file produces a
|
|
45
|
+
# clear error and so dot-sourcing this hook in tests loads the helpers too.
|
|
46
|
+
$script:CompletionHelpersPath = Join-Path $PSScriptRoot 'enforce-completion-helpers.ps1'
|
|
47
|
+
. $script:CompletionHelpersPath
|
|
48
|
+
|
|
42
49
|
function ConvertFrom-CheckpointJson {
|
|
43
50
|
<#
|
|
44
51
|
.SYNOPSIS
|
|
@@ -53,6 +60,29 @@ function ConvertFrom-CheckpointJson {
|
|
|
53
60
|
return $Json | ConvertFrom-Json -ErrorAction Stop
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
function Get-CheckpointFileContent {
|
|
64
|
+
<#
|
|
65
|
+
.SYNOPSIS
|
|
66
|
+
Reads the on-disk checkpoint content for the read-then-validate Edit path.
|
|
67
|
+
.DESCRIPTION
|
|
68
|
+
Returns the full file text when the path resolves to a file on disk, or
|
|
69
|
+
$null when the file does not exist. Tests inject a CheckpointReader
|
|
70
|
+
scriptblock instead of mocking this function so no temporary files are
|
|
71
|
+
required.
|
|
72
|
+
#>
|
|
73
|
+
[CmdletBinding()]
|
|
74
|
+
[OutputType([string])]
|
|
75
|
+
param(
|
|
76
|
+
[Parameter(Mandatory)]
|
|
77
|
+
[string] $Path
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
|
81
|
+
return $null
|
|
82
|
+
}
|
|
83
|
+
return Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
|
|
84
|
+
}
|
|
85
|
+
|
|
56
86
|
function Test-IsCheckpointPath {
|
|
57
87
|
[CmdletBinding()]
|
|
58
88
|
[OutputType([bool])]
|
|
@@ -149,7 +179,13 @@ function Get-MissingCompletionEvidence {
|
|
|
149
179
|
param(
|
|
150
180
|
[Parameter(Mandatory)]
|
|
151
181
|
[AllowNull()]
|
|
152
|
-
$Payload
|
|
182
|
+
$Payload,
|
|
183
|
+
|
|
184
|
+
[Parameter(Mandatory = $false)]
|
|
185
|
+
[scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container },
|
|
186
|
+
|
|
187
|
+
[Parameter(Mandatory = $false)]
|
|
188
|
+
[scriptblock] $RoutingMatrixReader
|
|
153
189
|
)
|
|
154
190
|
|
|
155
191
|
$missing = @()
|
|
@@ -158,16 +194,17 @@ function Get-MissingCompletionEvidence {
|
|
|
158
194
|
if (-not $issueNum -and $null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'variables')) {
|
|
159
195
|
$issueNum = Get-CheckpointStringValue -Payload $Payload.variables -Name 'issue-num'
|
|
160
196
|
}
|
|
161
|
-
if (-not $issueNum) {
|
|
162
|
-
|
|
197
|
+
if (-not (Test-IsValidIssueNum -Value $issueNum)) {
|
|
198
|
+
# Name the offending value so sentinel/placeholder inputs are explicit.
|
|
199
|
+
$missing += "issue-num value '$issueNum' is not a valid issue number (must be digits-only)"
|
|
163
200
|
}
|
|
164
201
|
|
|
165
202
|
$featureFolder = Get-CheckpointStringValue -Payload $Payload -Name 'feature-folder'
|
|
166
203
|
if (-not $featureFolder -and $null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'variables')) {
|
|
167
204
|
$featureFolder = Get-CheckpointStringValue -Payload $Payload.variables -Name 'feature-folder'
|
|
168
205
|
}
|
|
169
|
-
if (-not $featureFolder) {
|
|
170
|
-
$missing +=
|
|
206
|
+
if (-not (Test-IsValidFeatureFolder -Value $featureFolder -FolderExistsCheck $FolderExistsCheck)) {
|
|
207
|
+
$missing += "feature-folder value '$featureFolder' is not a valid feature folder (must be under docs/features/active/ and exist)"
|
|
171
208
|
}
|
|
172
209
|
|
|
173
210
|
$ciGate = $null
|
|
@@ -188,7 +225,14 @@ function Get-MissingCompletionEvidence {
|
|
|
188
225
|
}
|
|
189
226
|
}
|
|
190
227
|
|
|
191
|
-
|
|
228
|
+
# PR-gate evidence is required only when the checkpoint's selected route
|
|
229
|
+
# opts into it via requires_pr_gate in the routing matrix. This replaces the
|
|
230
|
+
# former issue-number special-casing with route-driven enforcement.
|
|
231
|
+
$prGateArgs = @{ Payload = $Payload }
|
|
232
|
+
if ($PSBoundParameters.ContainsKey('RoutingMatrixReader') -and $null -ne $RoutingMatrixReader) {
|
|
233
|
+
$prGateArgs['RoutingMatrixReader'] = $RoutingMatrixReader
|
|
234
|
+
}
|
|
235
|
+
if (Test-RouteRequiresPrGate @prGateArgs) {
|
|
192
236
|
$prGate = $null
|
|
193
237
|
if ($null -ne $Payload -and ($Payload.PSObject.Properties.Name -contains 'pr_gate')) {
|
|
194
238
|
$prGate = $Payload.pr_gate
|
|
@@ -213,6 +257,60 @@ function Get-MissingCompletionEvidence {
|
|
|
213
257
|
return [string[]]$missing
|
|
214
258
|
}
|
|
215
259
|
|
|
260
|
+
function Resolve-EditedCheckpointContent {
|
|
261
|
+
<#
|
|
262
|
+
.SYNOPSIS
|
|
263
|
+
Returns the patched checkpoint content for an Edit-tool call, or $null
|
|
264
|
+
when the patch cannot be applied against the on-disk checkpoint.
|
|
265
|
+
.DESCRIPTION
|
|
266
|
+
Implements the read-then-validate Edit path. When the tool input carries
|
|
267
|
+
an old_string (an Edit patch), the on-disk checkpoint is read through the
|
|
268
|
+
injectable CheckpointReader seam and the old_string -> new_string
|
|
269
|
+
replacement is applied in memory (no on-disk mutation). Returns $null
|
|
270
|
+
when there is no old_string, the on-disk file does not exist, or the
|
|
271
|
+
old_string is not present in the on-disk content, signalling the caller
|
|
272
|
+
to allow (defer).
|
|
273
|
+
#>
|
|
274
|
+
[CmdletBinding()]
|
|
275
|
+
[OutputType([string])]
|
|
276
|
+
param(
|
|
277
|
+
[Parameter(Mandatory)]
|
|
278
|
+
[AllowNull()]
|
|
279
|
+
$ToolInput,
|
|
280
|
+
|
|
281
|
+
[Parameter(Mandatory)]
|
|
282
|
+
[scriptblock] $CheckpointReader
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
$oldString = $null
|
|
286
|
+
if ($null -ne $ToolInput -and ($ToolInput.PSObject.Properties.Name -contains 'old_string')) {
|
|
287
|
+
$oldString = [string]$ToolInput.old_string
|
|
288
|
+
}
|
|
289
|
+
if ([string]::IsNullOrEmpty($oldString)) {
|
|
290
|
+
return $null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
$newString = ''
|
|
294
|
+
if ($ToolInput.PSObject.Properties.Name -contains 'new_string') {
|
|
295
|
+
$newString = [string]$ToolInput.new_string
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
$onDisk = & $CheckpointReader 'artifacts/orchestration/orchestrator-state.json'
|
|
299
|
+
if ([string]::IsNullOrEmpty([string]$onDisk)) {
|
|
300
|
+
# The on-disk checkpoint does not exist (or is empty); cannot patch.
|
|
301
|
+
return $null
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
$onDiskText = [string]$onDisk
|
|
305
|
+
if (-not $onDiskText.Contains($oldString)) {
|
|
306
|
+
# The old_string is not present, so the patch does not apply here.
|
|
307
|
+
return $null
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
# Apply the patch in memory using a literal (non-regex) replacement.
|
|
311
|
+
return $onDiskText.Replace($oldString, $newString)
|
|
312
|
+
}
|
|
313
|
+
|
|
216
314
|
function Invoke-CompletionConsistencyDecision {
|
|
217
315
|
<#
|
|
218
316
|
.SYNOPSIS
|
|
@@ -222,11 +320,20 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
222
320
|
[CmdletBinding()]
|
|
223
321
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
224
322
|
param(
|
|
225
|
-
[string] $ToolInputRaw
|
|
323
|
+
[string] $ToolInputRaw,
|
|
324
|
+
|
|
325
|
+
[Parameter(Mandatory = $false)]
|
|
326
|
+
[scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container },
|
|
327
|
+
|
|
328
|
+
[Parameter(Mandatory = $false)]
|
|
329
|
+
[scriptblock] $CheckpointReader = { param($Path) Get-CheckpointFileContent -Path $Path },
|
|
330
|
+
|
|
331
|
+
[Parameter(Mandatory = $false)]
|
|
332
|
+
[scriptblock] $RoutingMatrixReader
|
|
226
333
|
)
|
|
227
334
|
|
|
228
335
|
if (-not $ToolInputRaw) {
|
|
229
|
-
return [ordered]@{
|
|
336
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
230
337
|
}
|
|
231
338
|
|
|
232
339
|
try {
|
|
@@ -238,19 +345,25 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
238
345
|
|
|
239
346
|
$filePath = $toolInput.file_path
|
|
240
347
|
if (-not $filePath) {
|
|
241
|
-
return [ordered]@{
|
|
348
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
242
349
|
}
|
|
243
350
|
|
|
244
351
|
$normalized = $filePath -replace '\\', '/'
|
|
245
352
|
if (-not (Test-IsCheckpointPath -NormalizedPath $normalized)) {
|
|
246
|
-
return [ordered]@{
|
|
353
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
247
354
|
}
|
|
248
355
|
|
|
249
|
-
# Write tool: validate the content payload. Edit tool:
|
|
250
|
-
#
|
|
356
|
+
# Write tool: validate the content payload directly. Edit tool: no content is
|
|
357
|
+
# supplied, so read the on-disk checkpoint through the injectable seam and
|
|
358
|
+
# apply the old_string -> new_string patch in memory (read-then-validate).
|
|
251
359
|
$content = $toolInput.content
|
|
252
360
|
if (-not $content) {
|
|
253
|
-
|
|
361
|
+
$content = Resolve-EditedCheckpointContent -ToolInput $toolInput -CheckpointReader $CheckpointReader
|
|
362
|
+
if (-not $content) {
|
|
363
|
+
# No content, and the Edit could not be resolved against on-disk
|
|
364
|
+
# state (missing file or non-matching patch): defer and allow.
|
|
365
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
366
|
+
}
|
|
254
367
|
}
|
|
255
368
|
|
|
256
369
|
try {
|
|
@@ -259,26 +372,29 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
259
372
|
catch {
|
|
260
373
|
# The content itself is not valid JSON. Let downstream tools surface the
|
|
261
374
|
# error rather than blocking with a misleading reason here.
|
|
262
|
-
return [ordered]@{
|
|
375
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
263
376
|
}
|
|
264
377
|
|
|
265
378
|
if (-not (Test-CompletionAsserted -Payload $payload)) {
|
|
266
|
-
return [ordered]@{
|
|
379
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
267
380
|
}
|
|
268
381
|
|
|
269
|
-
$
|
|
270
|
-
if ($
|
|
271
|
-
|
|
382
|
+
$missingArgs = @{ Payload = $payload; FolderExistsCheck = $FolderExistsCheck }
|
|
383
|
+
if ($PSBoundParameters.ContainsKey('RoutingMatrixReader') -and $null -ne $RoutingMatrixReader) {
|
|
384
|
+
$missingArgs['RoutingMatrixReader'] = $RoutingMatrixReader
|
|
272
385
|
}
|
|
273
|
-
|
|
274
|
-
$
|
|
275
|
-
|
|
276
|
-
$issueContext = ' Issue #232 requires pr_gate evidence and current-head ci_gate evidence.'
|
|
386
|
+
$missing = Get-MissingCompletionEvidence @missingArgs
|
|
387
|
+
if ($missing.Count -eq 0) {
|
|
388
|
+
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
277
389
|
}
|
|
278
390
|
|
|
391
|
+
$reason = "COMPLETION_CONSISTENCY_BLOCKED: the checkpoint asserts completion but is missing required completion evidence: $($missing -join ', '). A completion-asserting checkpoint must include a non-empty issue-num, a non-empty feature-folder, and a ci_gate object with conclusion == 'success' and a non-empty head_sha; routes whose requires_pr_gate is true must also include pr_gate evidence with a matching head_sha. Supply the missing evidence or remove the completion assertion."
|
|
279
392
|
return [ordered]@{
|
|
280
|
-
|
|
281
|
-
|
|
393
|
+
hookSpecificOutput = [ordered]@{
|
|
394
|
+
hookEventName = 'PreToolUse'
|
|
395
|
+
permissionDecision = 'deny'
|
|
396
|
+
permissionDecisionReason = $reason
|
|
397
|
+
}
|
|
282
398
|
}
|
|
283
399
|
}
|
|
284
400
|
|
|
@@ -295,6 +411,6 @@ catch {
|
|
|
295
411
|
exit 1
|
|
296
412
|
}
|
|
297
413
|
|
|
298
|
-
$decision | ConvertTo-Json -Compress | Write-Output
|
|
414
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
299
415
|
|
|
300
416
|
exit 0
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Dot-sourced helper functions for enforce-completion-consistency.ps1.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Provides testable validation helpers used by the completion-consistency
|
|
7
|
+
PreToolUse hook:
|
|
8
|
+
|
|
9
|
+
- Test-IsValidIssueNum: rejects sentinel/placeholder and non-digit issue
|
|
10
|
+
numbers; accepts digits-only strings.
|
|
11
|
+
- Test-IsValidFeatureFolder: rejects sentinel/placeholder feature folders
|
|
12
|
+
and folders not anchored under docs/features/active/<segment>; optionally
|
|
13
|
+
verifies on-disk existence through an injectable scriptblock seam.
|
|
14
|
+
|
|
15
|
+
This script is dot-sourced by enforce-completion-consistency.ps1. It contains
|
|
16
|
+
no entrypoint logic, so dot-sourcing it in tests has no side effects.
|
|
17
|
+
|
|
18
|
+
.NOTES
|
|
19
|
+
Compatible with PowerShell 7+.
|
|
20
|
+
#>
|
|
21
|
+
[CmdletBinding()]
|
|
22
|
+
param()
|
|
23
|
+
|
|
24
|
+
# Sentinel/placeholder values that must never satisfy a presence check.
|
|
25
|
+
$script:CompletionEvidenceSentinels = @('n/a', 'none', 'tbd')
|
|
26
|
+
|
|
27
|
+
function Test-IsValidIssueNum {
|
|
28
|
+
<#
|
|
29
|
+
.SYNOPSIS
|
|
30
|
+
Returns $true only for a digits-only issue number.
|
|
31
|
+
.DESCRIPTION
|
|
32
|
+
Returns $false when the value is empty, whitespace-only, a sentinel
|
|
33
|
+
(n/a, none, tbd; case-insensitive), or contains any non-digit character.
|
|
34
|
+
Returns $true only when the trimmed value matches ^\d+$.
|
|
35
|
+
#>
|
|
36
|
+
[CmdletBinding()]
|
|
37
|
+
[OutputType([bool])]
|
|
38
|
+
param(
|
|
39
|
+
[Parameter(Mandatory = $true)]
|
|
40
|
+
[AllowNull()]
|
|
41
|
+
[AllowEmptyString()]
|
|
42
|
+
[string] $Value
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
if ([string]::IsNullOrWhiteSpace($Value)) {
|
|
46
|
+
return $false
|
|
47
|
+
}
|
|
48
|
+
$trimmed = $Value.Trim()
|
|
49
|
+
# Sentinel placeholders are explicitly rejected even though they are
|
|
50
|
+
# non-empty strings; the comparison is case-insensitive.
|
|
51
|
+
if ($script:CompletionEvidenceSentinels -contains $trimmed.ToLowerInvariant()) {
|
|
52
|
+
return $false
|
|
53
|
+
}
|
|
54
|
+
return $trimmed -match '^\d+$'
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function Test-IsValidFeatureFolder {
|
|
58
|
+
<#
|
|
59
|
+
.SYNOPSIS
|
|
60
|
+
Returns $true only for a sentinel-free feature folder anchored under
|
|
61
|
+
docs/features/active/ with a non-empty trailing segment that exists.
|
|
62
|
+
.DESCRIPTION
|
|
63
|
+
Returns $false when the value is empty, whitespace-only, or a sentinel
|
|
64
|
+
(n/a, none, tbd; case-insensitive). Requires the value to start with
|
|
65
|
+
'docs/features/active/' and to carry at least one additional non-empty
|
|
66
|
+
path segment after that prefix. Invokes the injectable FolderExistsCheck
|
|
67
|
+
scriptblock (default Test-Path -PathType Container) and returns $false
|
|
68
|
+
when it reports the folder does not exist.
|
|
69
|
+
#>
|
|
70
|
+
[CmdletBinding()]
|
|
71
|
+
[OutputType([bool])]
|
|
72
|
+
param(
|
|
73
|
+
[Parameter(Mandatory = $true)]
|
|
74
|
+
[AllowNull()]
|
|
75
|
+
[AllowEmptyString()]
|
|
76
|
+
[string] $Value,
|
|
77
|
+
|
|
78
|
+
[Parameter(Mandatory = $false)]
|
|
79
|
+
[scriptblock] $FolderExistsCheck = { param($p) Test-Path -LiteralPath $p -PathType Container }
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if ([string]::IsNullOrWhiteSpace($Value)) {
|
|
83
|
+
return $false
|
|
84
|
+
}
|
|
85
|
+
$trimmed = $Value.Trim()
|
|
86
|
+
if ($script:CompletionEvidenceSentinels -contains $trimmed.ToLowerInvariant()) {
|
|
87
|
+
return $false
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
$prefix = 'docs/features/active/'
|
|
91
|
+
$normalized = $trimmed -replace '\\', '/'
|
|
92
|
+
if (-not $normalized.StartsWith($prefix)) {
|
|
93
|
+
return $false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
# Require a non-empty segment after the active/ prefix so the bare prefix is
|
|
97
|
+
# not accepted as a valid folder.
|
|
98
|
+
$suffix = $normalized.Substring($prefix.Length).TrimEnd('/')
|
|
99
|
+
if ([string]::IsNullOrWhiteSpace($suffix)) {
|
|
100
|
+
return $false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return [bool](& $FolderExistsCheck $normalized)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function Test-RouteRequiresPrGate {
|
|
107
|
+
<#
|
|
108
|
+
.SYNOPSIS
|
|
109
|
+
Returns $true when the payload's selected route opts into the PR gate.
|
|
110
|
+
.DESCRIPTION
|
|
111
|
+
Resolves the route id from the payload (route_id, falling back to
|
|
112
|
+
path_selected), looks it up in the routing matrix returned by the
|
|
113
|
+
injectable RoutingMatrixReader seam, and returns $true only when that
|
|
114
|
+
route's requires_pr_gate value is the boolean $true. A missing route id,
|
|
115
|
+
an unknown route, a matrix without routes, or a missing/false
|
|
116
|
+
requires_pr_gate returns $false. This generalizes the former issue-232
|
|
117
|
+
special-casing into a route-driven check.
|
|
118
|
+
#>
|
|
119
|
+
[CmdletBinding()]
|
|
120
|
+
[OutputType([bool])]
|
|
121
|
+
param(
|
|
122
|
+
[Parameter(Mandatory)]
|
|
123
|
+
[AllowNull()]
|
|
124
|
+
$Payload,
|
|
125
|
+
|
|
126
|
+
[Parameter(Mandatory = $false)]
|
|
127
|
+
[scriptblock] $RoutingMatrixReader = {
|
|
128
|
+
$configPath = Join-Path $PSScriptRoot '../../config/orchestration-routing.json'
|
|
129
|
+
if (-not (Test-Path -LiteralPath $configPath)) { return $null }
|
|
130
|
+
Get-Content -LiteralPath $configPath -Raw | ConvertFrom-Json
|
|
131
|
+
}
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if ($null -eq $Payload) {
|
|
135
|
+
return $false
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# Resolve the selected route id, preferring route_id over path_selected.
|
|
139
|
+
$routeId = ''
|
|
140
|
+
if ($Payload.PSObject.Properties.Name -contains 'route_id') {
|
|
141
|
+
$routeId = ([string]$Payload.route_id).Trim()
|
|
142
|
+
}
|
|
143
|
+
if (-not $routeId -and ($Payload.PSObject.Properties.Name -contains 'path_selected')) {
|
|
144
|
+
$routeId = ([string]$Payload.path_selected).Trim()
|
|
145
|
+
}
|
|
146
|
+
if (-not $routeId) {
|
|
147
|
+
return $false
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
$matrix = & $RoutingMatrixReader
|
|
151
|
+
if ($null -eq $matrix -or -not ($matrix.PSObject.Properties.Name -contains 'routes')) {
|
|
152
|
+
return $false
|
|
153
|
+
}
|
|
154
|
+
$routes = $matrix.routes
|
|
155
|
+
if ($null -eq $routes -or -not ($routes.PSObject.Properties.Name -contains $routeId)) {
|
|
156
|
+
return $false
|
|
157
|
+
}
|
|
158
|
+
$route = $routes.$routeId
|
|
159
|
+
if ($null -eq $route -or -not ($route.PSObject.Properties.Name -contains 'requires_pr_gate')) {
|
|
160
|
+
return $false
|
|
161
|
+
}
|
|
162
|
+
return ([bool]$route.requires_pr_gate -eq $true)
|
|
163
|
+
}
|