@danmoisan/drm-copilot-mcp 1.1.10 → 1.1.12
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 +3517 -1104
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +15 -2
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +3 -0
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +82 -51
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -28
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +103 -6
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +124 -8
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +171 -15
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +50 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +35 -7
- package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +483 -0
- package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +483 -0
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +218 -6
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +18 -75
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +290 -0
- package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +415 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMerge.psm1 +355 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMergeGrammar.psm1 +318 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/Resolve-MergeableConflict.ps1 +229 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +323 -16
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +46 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +75 -8
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/powershell-orchestration-state-machine/SKILL.md +29 -1
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
- package/resources/claude-customizations/config/blast-radius.json +7 -0
- package/resources/claude-customizations/pack-manifests/core.json +16 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +41 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -0
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +26 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +242 -3
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +67 -12
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +34 -8
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +34 -7
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +483 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +483 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +130 -2
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +23 -1
- package/resources/config/orchestration-handoff-registry.json +138 -0
- package/resources/config/orchestration-handoff.schema.json +472 -0
- package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +64 -0
package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md
CHANGED
|
@@ -50,6 +50,32 @@ Use these semantic MCP tools when the corresponding operation is required:
|
|
|
50
50
|
- `run_poshqc_analyze`
|
|
51
51
|
- `run_poshqc_analyze_autofix`
|
|
52
52
|
- `run_poshqc_test`
|
|
53
|
+
- `resolve_orchestration_topology`
|
|
54
|
+
- `resolve_provider_routing`
|
|
55
|
+
- `transition_prepared_orchestration`
|
|
56
|
+
|
|
57
|
+
## Independent Expected Context for Portable Handoff Tools
|
|
58
|
+
|
|
59
|
+
The three portable prepared-orchestration tools bind a handoff to one specific
|
|
60
|
+
destination checkout. Every call to `resolve_orchestration_topology`,
|
|
61
|
+
`resolve_provider_routing`, and `transition_prepared_orchestration` must carry
|
|
62
|
+
the complete caller-controlled independent expected context:
|
|
63
|
+
|
|
64
|
+
- `expected_repository_id`
|
|
65
|
+
- `expected_workspace_root`
|
|
66
|
+
- `expected_branch`
|
|
67
|
+
- `expected_source_head_sha`
|
|
68
|
+
- `allowed_head_relationship`
|
|
69
|
+
- `expected_issue_number`
|
|
70
|
+
- `expected_feature_folder`
|
|
71
|
+
- `expected_work_mode`
|
|
72
|
+
- `expected_plan_path`
|
|
73
|
+
- `expected_plan_sha256`
|
|
74
|
+
|
|
75
|
+
Derive each value from the destination checkout and the caller's own record.
|
|
76
|
+
The envelope under validation is never the source of the values it is
|
|
77
|
+
validated against. A call that omits a value, or that copies one from the
|
|
78
|
+
envelope, is rejected before the service is invoked.
|
|
53
79
|
|
|
54
80
|
Legacy VS Code command IDs are historical source material only and must not be invoked:
|
|
55
81
|
|
|
@@ -5,6 +5,20 @@
|
|
|
5
5
|
[CmdletBinding()]
|
|
6
6
|
param()
|
|
7
7
|
|
|
8
|
+
# Shared command-line parser (issue #545). The scope filter and the PR-number resolver below
|
|
9
|
+
# both run against the segment that structurally invokes `gh pr merge`, which is what keeps
|
|
10
|
+
# the two runtimes on one implementation of the same concern.
|
|
11
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
12
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
13
|
+
|
|
14
|
+
# Reason codes for the standalone-merge authorization branch (issue #670), spelled
|
|
15
|
+
# byte-identically to the Claude helpers file. Each of the four assignments below is the only
|
|
16
|
+
# place its code literal appears in this file.
|
|
17
|
+
$script:CodexStandaloneAbsentReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_ABSENT'
|
|
18
|
+
$script:CodexStandalonePrMismatchReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_PR_MISMATCH'
|
|
19
|
+
$script:CodexStandaloneMalformedReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_MALFORMED'
|
|
20
|
+
$script:CodexStandaloneNotPrSpecificReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_NOT_PR_SPECIFIC'
|
|
21
|
+
|
|
8
22
|
function ConvertFrom-CodexMergeJson {
|
|
9
23
|
[CmdletBinding()]
|
|
10
24
|
param([AllowNull()][AllowEmptyString()][string] $Raw, [Parameter(Mandatory)][string] $Name, [switch] $Optional)
|
|
@@ -26,13 +40,37 @@ function ConvertFrom-CodexMergeJson {
|
|
|
26
40
|
}
|
|
27
41
|
|
|
28
42
|
function Get-CodexMergeCommandPrNumber {
|
|
43
|
+
<#
|
|
44
|
+
.SYNOPSIS
|
|
45
|
+
Resolve the explicit pull-request number of a gh pr merge invocation, or $null.
|
|
46
|
+
.DESCRIPTION
|
|
47
|
+
The number is taken from the segment that structurally invokes `gh pr merge` and
|
|
48
|
+
from nowhere else: the positional spelling through Get-CommandLineOperand, the
|
|
49
|
+
flag-led and equals-joined spellings through Get-CommandLineFlagValue. A leading
|
|
50
|
+
`cd <path>` segment contributes no operand and no flag value, so no digit run
|
|
51
|
+
outside the merge segment can be mistaken for a pull-request number.
|
|
52
|
+
|
|
53
|
+
This copy has never carried the Claude copy's unanchored whole-text digit scan and
|
|
54
|
+
deliberately does not acquire one. No regular expression matching a bare digit run
|
|
55
|
+
is introduced here; the only pattern below anchors an all-digit token end to end.
|
|
56
|
+
.OUTPUTS
|
|
57
|
+
System.Nullable[int]
|
|
58
|
+
#>
|
|
29
59
|
[CmdletBinding()]
|
|
30
60
|
[OutputType([int])]
|
|
31
61
|
param([Parameter(Mandatory)][string] $Command)
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
|
|
63
|
+
foreach ($operand in @(Get-CommandLineOperand -CommandText $Command -CommandWord 'gh' -SubcommandPath @('pr', 'merge'))) {
|
|
64
|
+
if ($operand -match '^\d+$') {
|
|
65
|
+
return [int]$operand
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
$flagValue = Get-CommandLineFlagValue -CommandText $Command -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
|
|
70
|
+
if ($null -ne $flagValue -and $flagValue -match '^\d+$') {
|
|
71
|
+
return [int]$flagValue
|
|
35
72
|
}
|
|
73
|
+
|
|
36
74
|
return $null
|
|
37
75
|
}
|
|
38
76
|
|
|
@@ -95,7 +133,23 @@ function Invoke-CodexEpicMergeDecision {
|
|
|
95
133
|
return $null
|
|
96
134
|
}
|
|
97
135
|
$command = [string]$payload.tool_input.command
|
|
98
|
-
|
|
136
|
+
# Both legs are read structurally from the segment that invokes the command, so a
|
|
137
|
+
# quoted mention of the phrase no longer brings a command into scope. The flag leg needs
|
|
138
|
+
# a raw-scan fallback: a wrapper's quoted argument collapses into ONE token, so a flag
|
|
139
|
+
# inside it is never read as a token, and the token-only read took
|
|
140
|
+
# bash -c "gh pr merge --merge 688" out of scope.
|
|
141
|
+
$isMergeInvocation = Test-CommandLineInvocation -CommandText $command -CommandWord 'gh' -SubcommandPath @('pr', 'merge')
|
|
142
|
+
$hasMergeFlag = Test-CommandLineFlag -CommandText $command -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
|
|
143
|
+
if (-not $hasMergeFlag) {
|
|
144
|
+
foreach ($segment in @(Read-CommandLineSegment -CommandText $command)) {
|
|
145
|
+
if ((Test-CommandLineSegmentRawScan -Segment $segment) -and
|
|
146
|
+
$segment.ScanText.IndexOf('--merge', [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
|
|
147
|
+
$hasMergeFlag = $true
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (-not $isMergeInvocation -or -not $hasMergeFlag) {
|
|
99
153
|
return $null
|
|
100
154
|
}
|
|
101
155
|
|
|
@@ -109,6 +163,24 @@ function Invoke-CodexEpicMergeDecision {
|
|
|
109
163
|
return $null
|
|
110
164
|
}
|
|
111
165
|
|
|
166
|
+
# Standalone branch (issue #670), evaluated last and only for an explicit PR number: a
|
|
167
|
+
# PR-specific authorization record bound to the payload session. Its deny keeps the gate
|
|
168
|
+
# token first.
|
|
169
|
+
if ($null -ne $prNumber) {
|
|
170
|
+
$sessionId = [string]$payload.session_id
|
|
171
|
+
$verdict = Test-CodexStandaloneMergeAuthorization -Checkpoints @($child, $epic) -CommandPrNumber $prNumber -SessionId $sessionId
|
|
172
|
+
if ($verdict.Allowed) {
|
|
173
|
+
return $null
|
|
174
|
+
}
|
|
175
|
+
return [ordered]@{
|
|
176
|
+
hookSpecificOutput = [ordered]@{
|
|
177
|
+
hookEventName = 'PreToolUse'
|
|
178
|
+
permissionDecision = 'deny'
|
|
179
|
+
permissionDecisionReason = 'EPIC_MERGE_GATE_BLOCKED: ' + $verdict.ReasonCode + ': ' + $verdict.Message
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
112
184
|
return [ordered]@{
|
|
113
185
|
hookSpecificOutput = [ordered]@{
|
|
114
186
|
hookEventName = 'PreToolUse'
|
|
@@ -118,6 +190,173 @@ function Invoke-CodexEpicMergeDecision {
|
|
|
118
190
|
}
|
|
119
191
|
}
|
|
120
192
|
|
|
193
|
+
function Test-CodexStandalonePositiveInteger {
|
|
194
|
+
<#
|
|
195
|
+
.SYNOPSIS
|
|
196
|
+
Report whether a parsed JSON value is an integer greater than zero; a digit string fails.
|
|
197
|
+
#>
|
|
198
|
+
[CmdletBinding()]
|
|
199
|
+
[OutputType([bool])]
|
|
200
|
+
param([AllowNull()] $Value)
|
|
201
|
+
|
|
202
|
+
return (($Value -is [int] -or $Value -is [long]) -and $Value -gt 0)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function Test-CodexStandaloneText {
|
|
206
|
+
<#
|
|
207
|
+
.SYNOPSIS
|
|
208
|
+
Report whether a value is a string whose trimmed length reaches a minimum.
|
|
209
|
+
#>
|
|
210
|
+
[CmdletBinding()]
|
|
211
|
+
[OutputType([bool])]
|
|
212
|
+
param([AllowNull()] $Value, [int] $MinimumLength = 1)
|
|
213
|
+
|
|
214
|
+
return ($Value -is [string] -and $Value.Trim().Length -ge $MinimumLength)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Get-CodexStandaloneField {
|
|
218
|
+
<#
|
|
219
|
+
.SYNOPSIS
|
|
220
|
+
Read a named field off a parsed record, or $null; the unary comma keeps a
|
|
221
|
+
one-element array from unrolling into its scalar.
|
|
222
|
+
#>
|
|
223
|
+
[CmdletBinding()]
|
|
224
|
+
[OutputType([object], [object[]])]
|
|
225
|
+
param([Parameter(Mandatory)] $Record, [Parameter(Mandatory)][string] $Name)
|
|
226
|
+
|
|
227
|
+
$property = $Record.PSObject.Properties[$Name]
|
|
228
|
+
if ($null -eq $property) {
|
|
229
|
+
return $null
|
|
230
|
+
}
|
|
231
|
+
return , $property.Value
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function Get-CodexStandaloneRecordFailure {
|
|
235
|
+
<#
|
|
236
|
+
.SYNOPSIS
|
|
237
|
+
Return the first failing field of a matched record, or $null when every check passes.
|
|
238
|
+
.DESCRIPTION
|
|
239
|
+
Uses the same fixed order as the Claude helpers file: pr_url, issue_num, branch_name,
|
|
240
|
+
authorized_by, authorized_at, basis, run_slug, session_id. branch_name is checked for
|
|
241
|
+
presence only. A blank payload session fails closed.
|
|
242
|
+
#>
|
|
243
|
+
[CmdletBinding()]
|
|
244
|
+
[OutputType([string])]
|
|
245
|
+
param([Parameter(Mandatory)] $Record, [AllowNull()][AllowEmptyString()][string] $SessionId)
|
|
246
|
+
|
|
247
|
+
$prUrl = Get-CodexStandaloneField -Record $Record -Name 'pr_url'
|
|
248
|
+
if (-not (Test-CodexStandaloneText -Value $prUrl) -or
|
|
249
|
+
-not $prUrl.EndsWith('/pull/' + [string]$Record.pr_number, [System.StringComparison]::Ordinal)) {
|
|
250
|
+
return 'pr_url'
|
|
251
|
+
}
|
|
252
|
+
if (-not (Test-CodexStandalonePositiveInteger -Value (Get-CodexStandaloneField -Record $Record -Name 'issue_num'))) {
|
|
253
|
+
return 'issue_num'
|
|
254
|
+
}
|
|
255
|
+
foreach ($name in @('branch_name', 'authorized_by')) {
|
|
256
|
+
if (-not (Test-CodexStandaloneText -Value (Get-CodexStandaloneField -Record $Record -Name $name))) {
|
|
257
|
+
return $name
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
$authorizedAt = Get-CodexStandaloneField -Record $Record -Name 'authorized_at'
|
|
261
|
+
$parsedAt = [DateTime]::MinValue
|
|
262
|
+
$timestampValid = $authorizedAt -is [DateTime] -or $authorizedAt -is [DateTimeOffset]
|
|
263
|
+
if (-not $timestampValid -and (Test-CodexStandaloneText -Value $authorizedAt)) {
|
|
264
|
+
$timestampValid = [DateTime]::TryParse(
|
|
265
|
+
$authorizedAt,
|
|
266
|
+
[System.Globalization.CultureInfo]::InvariantCulture,
|
|
267
|
+
[System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
|
|
268
|
+
[ref] $parsedAt)
|
|
269
|
+
}
|
|
270
|
+
if (-not $timestampValid) {
|
|
271
|
+
return 'authorized_at'
|
|
272
|
+
}
|
|
273
|
+
if (-not (Test-CodexStandaloneText -Value (Get-CodexStandaloneField -Record $Record -Name 'basis') -MinimumLength 20)) {
|
|
274
|
+
return 'basis'
|
|
275
|
+
}
|
|
276
|
+
if ($null -ne $Record.PSObject.Properties['run_slug'] -and
|
|
277
|
+
-not (Test-CodexStandaloneText -Value (Get-CodexStandaloneField -Record $Record -Name 'run_slug'))) {
|
|
278
|
+
return 'run_slug'
|
|
279
|
+
}
|
|
280
|
+
$recordSession = Get-CodexStandaloneField -Record $Record -Name 'session_id'
|
|
281
|
+
if (-not (Test-CodexStandaloneText -Value $recordSession) -or
|
|
282
|
+
[string]::IsNullOrWhiteSpace($SessionId) -or
|
|
283
|
+
-not [string]::Equals($recordSession, $SessionId, [System.StringComparison]::Ordinal)) {
|
|
284
|
+
return 'session_id'
|
|
285
|
+
}
|
|
286
|
+
return $null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function Test-CodexStandaloneMergeAuthorization {
|
|
290
|
+
<#
|
|
291
|
+
.SYNOPSIS
|
|
292
|
+
Codex counterpart of the Claude standalone-merge authorization predicate.
|
|
293
|
+
.DESCRIPTION
|
|
294
|
+
Evaluates the parsed child and epic checkpoints with the same activation condition
|
|
295
|
+
as the Claude side: a PR-specific record naming CommandPrNumber, with well-formed
|
|
296
|
+
fields and a session_id equal to the payload session. Returns an object whose
|
|
297
|
+
Allowed member is $true on success, or $false with ReasonCode and Message.
|
|
298
|
+
#>
|
|
299
|
+
[CmdletBinding()]
|
|
300
|
+
[OutputType([pscustomobject])]
|
|
301
|
+
param(
|
|
302
|
+
[AllowNull()][AllowEmptyCollection()][object[]] $Checkpoints,
|
|
303
|
+
[Parameter(Mandatory)][int] $CommandPrNumber,
|
|
304
|
+
[AllowNull()][AllowEmptyString()][string] $SessionId
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
$key = 'standalone_merge_authorizations'
|
|
308
|
+
$present = $false
|
|
309
|
+
$specific = $true
|
|
310
|
+
$match = $null
|
|
311
|
+
foreach ($checkpoint in @($Checkpoints)) {
|
|
312
|
+
if ($checkpoint -isnot [System.Management.Automation.PSCustomObject] -or
|
|
313
|
+
$null -eq $checkpoint.PSObject.Properties[$key]) {
|
|
314
|
+
continue
|
|
315
|
+
}
|
|
316
|
+
$present = $true
|
|
317
|
+
$block = $checkpoint.PSObject.Properties[$key].Value
|
|
318
|
+
if ($block -isnot [System.Array] -or $block.Count -eq 0) {
|
|
319
|
+
$specific = $false
|
|
320
|
+
continue
|
|
321
|
+
}
|
|
322
|
+
foreach ($entry in $block) {
|
|
323
|
+
$number = $null
|
|
324
|
+
if ($entry -is [System.Management.Automation.PSCustomObject]) {
|
|
325
|
+
$number = Get-CodexStandaloneField -Record $entry -Name 'pr_number'
|
|
326
|
+
}
|
|
327
|
+
if (-not (Test-CodexStandalonePositiveInteger -Value $number)) {
|
|
328
|
+
$specific = $false
|
|
329
|
+
} elseif ($null -eq $match -and $number -eq $CommandPrNumber) {
|
|
330
|
+
$match = $entry
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (-not $present) {
|
|
336
|
+
$code = $script:CodexStandaloneAbsentReasonCode
|
|
337
|
+
$message = 'no orchestrator checkpoint carries an authorization record, but an authorized standalone merge path exists.'
|
|
338
|
+
} elseif (-not $specific) {
|
|
339
|
+
$code = $script:CodexStandaloneNotPrSpecificReasonCode
|
|
340
|
+
$message = 'an orchestrator checkpoint carries the authorization key but names no specific pull request; a blanket flag is not an authorization record.'
|
|
341
|
+
} elseif ($null -eq $match) {
|
|
342
|
+
$code = $script:CodexStandalonePrMismatchReasonCode
|
|
343
|
+
$message = "authorization records exist, but none names pull request $CommandPrNumber."
|
|
344
|
+
} else {
|
|
345
|
+
$field = Get-CodexStandaloneRecordFailure -Record $match -SessionId $SessionId
|
|
346
|
+
if ($null -eq $field) {
|
|
347
|
+
return [pscustomobject]@{ Allowed = $true; ReasonCode = $null; Message = $null }
|
|
348
|
+
}
|
|
349
|
+
$code = $script:CodexStandaloneMalformedReasonCode
|
|
350
|
+
$message = "the matched authorization record has an invalid '$field'."
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return [pscustomobject]@{
|
|
354
|
+
Allowed = $false
|
|
355
|
+
ReasonCode = $code
|
|
356
|
+
Message = "$message To authorize a standalone merge, write a $key record naming this pull request and the current session_id into an orchestrator checkpoint, as described in .claude/rules/orchestrator-state.md."
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
121
360
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
122
361
|
return
|
|
123
362
|
}
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1
CHANGED
|
@@ -16,8 +16,13 @@ $script:AllowedPreparationMcpTools = @(
|
|
|
16
16
|
'mcp__drm-copilot__potential_to_issue',
|
|
17
17
|
'mcp__drm-copilot__new_active_feature_folder',
|
|
18
18
|
'mcp__drm-copilot__resolve_atomic_plan_prompt',
|
|
19
|
-
'mcp__drm-copilot__resolve_execute_hard_lock_prompt'
|
|
20
|
-
|
|
19
|
+
'mcp__drm-copilot__resolve_execute_hard_lock_prompt'
|
|
20
|
+
)
|
|
21
|
+
$script:PreparationSemanticMcpIds = @(
|
|
22
|
+
'drm-copilot.validate_orchestration_artifacts',
|
|
23
|
+
'drm-copilot.resolve_orchestration_topology',
|
|
24
|
+
'drm-copilot.resolve_provider_routing',
|
|
25
|
+
'drm-copilot.transition_prepared_orchestration'
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
function ConvertFrom-EpicPlanningJson {
|
|
@@ -41,6 +46,44 @@ function ConvertFrom-EpicPlanningJson {
|
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
function Get-EpicPlanningRegisteredMcpTool {
|
|
50
|
+
[CmdletBinding()]
|
|
51
|
+
[OutputType([string[]])]
|
|
52
|
+
param(
|
|
53
|
+
[Parameter(Mandatory)][string] $RegistryPath,
|
|
54
|
+
[Parameter(Mandatory)][string[]] $SemanticIds
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (-not (Test-Path -LiteralPath $RegistryPath -PathType Leaf)) {
|
|
58
|
+
throw "EPIC_PLANNING_ONLY_BLOCKED: semantic MCP registry '$RegistryPath' does not exist."
|
|
59
|
+
}
|
|
60
|
+
$registry = ConvertFrom-EpicPlanningJson `
|
|
61
|
+
-Raw (Get-Content -Raw -LiteralPath $RegistryPath) `
|
|
62
|
+
-Name 'semantic MCP registry'
|
|
63
|
+
$registered = [System.Collections.Generic.List[string]]::new()
|
|
64
|
+
foreach ($semanticId in $SemanticIds) {
|
|
65
|
+
$property = $registry.semantic_tools.PSObject.Properties[$semanticId]
|
|
66
|
+
if ($null -eq $property) {
|
|
67
|
+
throw "EPIC_PLANNING_ONLY_BLOCKED: semantic MCP id '$semanticId' is not registered."
|
|
68
|
+
}
|
|
69
|
+
$entry = $property.Value
|
|
70
|
+
$operation = $semanticId.Substring($semanticId.LastIndexOf('.') + 1)
|
|
71
|
+
if ([string]$entry.operation -ne $operation) {
|
|
72
|
+
throw "EPIC_PLANNING_ONLY_BLOCKED: semantic MCP id '$semanticId' has an invalid operation."
|
|
73
|
+
}
|
|
74
|
+
foreach ($alias in @($entry.transport_aliases)) {
|
|
75
|
+
$escapedOperation = [regex]::Escape($operation)
|
|
76
|
+
if ([string]$alias -notmatch "^mcp__(?:drm-copilot|drm_copilot)__$escapedOperation`$") {
|
|
77
|
+
throw "EPIC_PLANNING_ONLY_BLOCKED: semantic MCP id '$semanticId' has an invalid transport alias."
|
|
78
|
+
}
|
|
79
|
+
$registered.Add([string]$alias)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [string[]]($registered | Sort-Object -Unique)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
$script:EpicPlanningRepositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
86
|
+
|
|
44
87
|
function Get-EpicPlanningDenyDecision {
|
|
45
88
|
[CmdletBinding()]
|
|
46
89
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -163,7 +206,8 @@ function Invoke-EpicPlanningOnlyDecision {
|
|
|
163
206
|
[AllowNull()][AllowEmptyString()][string] $CheckpointRaw,
|
|
164
207
|
[AllowNull()][AllowEmptyString()][string] $EpicExecutionContext,
|
|
165
208
|
[AllowNull()][string[]] $StagedPaths,
|
|
166
|
-
[AllowNull()][AllowEmptyString()][string] $CurrentBranch
|
|
209
|
+
[AllowNull()][AllowEmptyString()][string] $CurrentBranch,
|
|
210
|
+
[string] $RegistryPath = (Join-Path $script:EpicPlanningRepositoryRoot 'config/orchestration-handoff-registry.json')
|
|
167
211
|
)
|
|
168
212
|
|
|
169
213
|
$payload = ConvertFrom-EpicPlanningJson -Raw $PayloadRaw -Name 'PreToolUse input'
|
|
@@ -223,11 +267,21 @@ function Invoke-EpicPlanningOnlyDecision {
|
|
|
223
267
|
return Get-EpicPlanningDenyDecision -Reason 'the Bash command is outside the preparation read, branch, commit, push, or validator allowlist.'
|
|
224
268
|
}
|
|
225
269
|
|
|
226
|
-
|
|
227
|
-
|
|
270
|
+
# Lifecycle tools are decided first and never need the registry. Only a
|
|
271
|
+
# registry-dependent mcp__ call reads it, so a destination without the
|
|
272
|
+
# registry keeps every other decision and fails closed for semantic MCP.
|
|
273
|
+
$allowedRepositoryMcp = $script:AllowedPreparationMcpTools -contains $toolName
|
|
274
|
+
$allowedSemanticMcp = $false
|
|
275
|
+
if (-not $allowedRepositoryMcp -and $toolName -like 'mcp__*') {
|
|
276
|
+
if (-not (Test-Path -LiteralPath $RegistryPath -PathType Leaf)) {
|
|
277
|
+
return Get-EpicPlanningDenyDecision -Reason "semantic MCP registry '$RegistryPath' does not exist; registry-dependent MCP tool '$toolName' is denied in preparation mode."
|
|
278
|
+
}
|
|
279
|
+
$registeredSemanticMcp = @(Get-EpicPlanningRegisteredMcpTool `
|
|
280
|
+
-RegistryPath $RegistryPath `
|
|
281
|
+
-SemanticIds $script:PreparationSemanticMcpIds)
|
|
282
|
+
$allowedSemanticMcp = $registeredSemanticMcp -contains $toolName
|
|
228
283
|
}
|
|
229
|
-
|
|
230
|
-
if ($toolName -like 'mcp__drm-copilot__*') {
|
|
284
|
+
if ($allowedRepositoryMcp -or $allowedSemanticMcp) {
|
|
231
285
|
if ($attestedPreparation) {
|
|
232
286
|
$workspaceRoot = [string]$payload.tool_input.workspace_root
|
|
233
287
|
if ([string]::IsNullOrWhiteSpace($workspaceRoot) -or
|
|
@@ -236,10 +290,11 @@ function Invoke-EpicPlanningOnlyDecision {
|
|
|
236
290
|
return Get-EpicPlanningDenyDecision -Reason 'preparation MCP calls require workspace_root equal to the attested session cwd.'
|
|
237
291
|
}
|
|
238
292
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
293
|
+
return $null
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if ($toolName -like 'mcp__*') {
|
|
297
|
+
return Get-EpicPlanningDenyDecision -Reason "MCP tool '$toolName' is outside the registered preparation lifecycle and semantic allowlist."
|
|
243
298
|
}
|
|
244
299
|
|
|
245
300
|
return Get-EpicPlanningDenyDecision -Reason "tool '$toolName' is not classified for preparation mode."
|
|
@@ -273,7 +328,7 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
273
328
|
|
|
274
329
|
try {
|
|
275
330
|
$payloadRaw = [Console]::In.ReadToEnd()
|
|
276
|
-
$repositoryRoot =
|
|
331
|
+
$repositoryRoot = $script:EpicPlanningRepositoryRoot
|
|
277
332
|
$checkpointPath = Join-Path $repositoryRoot 'artifacts/orchestration/orchestrator-state.json'
|
|
278
333
|
$checkpointRaw = if (Test-Path -LiteralPath $checkpointPath -PathType Leaf) {
|
|
279
334
|
Get-Content -Raw -LiteralPath $checkpointPath
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
[CmdletBinding()]
|
|
6
6
|
param()
|
|
7
7
|
|
|
8
|
+
# Shared command-line parser (issue #545). The scope filter and the operand extractor below
|
|
9
|
+
# both run against the segment that structurally invokes `git worktree remove`, which is what
|
|
10
|
+
# keeps the two runtimes on one implementation of the same concern.
|
|
11
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
12
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
13
|
+
|
|
8
14
|
$script:SafeWorktreeStatuses = @('merged', 'worktree_removed')
|
|
9
15
|
|
|
10
16
|
function ConvertFrom-CodexWorktreeJson {
|
|
@@ -28,18 +34,35 @@ function ConvertFrom-CodexWorktreeJson {
|
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
function Get-CodexWorktreeRemovalPath {
|
|
37
|
+
<#
|
|
38
|
+
.SYNOPSIS
|
|
39
|
+
Extract the target worktree path from a git worktree remove command.
|
|
40
|
+
.DESCRIPTION
|
|
41
|
+
The operand comes from the segment that structurally invokes git worktree remove, so
|
|
42
|
+
a chained 'cd <path> &&' segment contributes nothing and a quoted mention of the
|
|
43
|
+
phrase resolves to no operand. The tokenizer strips balanced double and single
|
|
44
|
+
quotes, which is what the previous pattern's `double` and `single` alternatives did.
|
|
45
|
+
|
|
46
|
+
The previous pattern accepted '--force' only immediately after 'remove'. That
|
|
47
|
+
spelling is preserved and the trailing spelling now works too, because '--force' is a
|
|
48
|
+
zero-argument flag that never contributes an operand wherever it is written. Its
|
|
49
|
+
presence is read structurally through Test-CommandLineFlag rather than by a raw-text
|
|
50
|
+
search. The empty-string-on-miss contract is unchanged: callers test `if ($target)`.
|
|
51
|
+
.OUTPUTS
|
|
52
|
+
System.String
|
|
53
|
+
#>
|
|
31
54
|
[CmdletBinding()]
|
|
32
55
|
[OutputType([string])]
|
|
33
56
|
param([Parameter(Mandatory)][AllowEmptyString()][string] $Command)
|
|
34
57
|
|
|
35
|
-
$
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
$hasForce = Test-CommandLineFlag -CommandText $Command -CommandWord 'git' -SubcommandPath @('worktree', 'remove') -FlagName '--force'
|
|
59
|
+
$operands = @(Get-CommandLineOperand -CommandText $Command -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))
|
|
60
|
+
|
|
61
|
+
if ($operands.Count -gt 0) {
|
|
62
|
+
return [string]$operands[0]
|
|
38
63
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return [string]$Matches[$name]
|
|
42
|
-
}
|
|
64
|
+
if ($hasForce) {
|
|
65
|
+
return '--force'
|
|
43
66
|
}
|
|
44
67
|
return ''
|
|
45
68
|
}
|
|
@@ -97,7 +120,10 @@ function Invoke-CodexWorktreeRemovalDecision {
|
|
|
97
120
|
return $null
|
|
98
121
|
}
|
|
99
122
|
$command = [string]$payload.tool_input.command
|
|
100
|
-
|
|
123
|
+
# Scope filter. Structural, so a relocating spelling such as
|
|
124
|
+
# 'git -C <dir> worktree remove <path>' is in scope and quoted prose that merely mentions
|
|
125
|
+
# the phrase is not (issue #545).
|
|
126
|
+
if (-not (Test-CommandLineInvocation -CommandText $command -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))) {
|
|
101
127
|
return $null
|
|
102
128
|
}
|
|
103
129
|
$target = Get-CodexWorktreeRemovalPath -Command $command
|