@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.
Files changed (82) hide show
  1. package/out/mcp-server.js +3517 -1104
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
  4. package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
  5. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +15 -2
  6. package/resources/claude-customizations/.claude/agents/parallel-planner.md +3 -0
  7. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +82 -51
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
  11. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  14. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -28
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +103 -6
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +124 -8
  17. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +171 -15
  18. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +50 -10
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
  20. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
  21. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
  22. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +35 -7
  23. package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +483 -0
  24. package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +483 -0
  25. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +218 -6
  26. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +18 -75
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +290 -0
  28. package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +415 -0
  29. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
  30. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMerge.psm1 +355 -0
  31. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMergeGrammar.psm1 +318 -0
  32. package/resources/claude-customizations/.claude/lib/project-file-merge/Resolve-MergeableConflict.ps1 +229 -0
  33. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
  34. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
  35. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
  36. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
  37. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
  38. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
  39. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
  40. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +323 -16
  41. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
  42. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
  43. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +46 -1
  44. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
  45. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +75 -8
  46. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +6 -0
  47. package/resources/claude-customizations/.claude/skills/powershell-orchestration-state-machine/SKILL.md +29 -1
  48. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
  49. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
  50. package/resources/claude-customizations/config/blast-radius.json +7 -0
  51. package/resources/claude-customizations/pack-manifests/core.json +16 -0
  52. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
  53. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +41 -0
  54. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -0
  55. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +26 -0
  56. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  57. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +242 -3
  58. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +67 -12
  59. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +34 -8
  60. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  61. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -1
  62. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +34 -7
  63. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +483 -0
  64. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +483 -0
  65. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +130 -2
  66. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
  67. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
  68. package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
  69. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
  70. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
  71. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
  72. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
  73. package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
  74. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
  75. package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
  76. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
  77. package/resources/codex-and-agents-customizations/pack-manifests/core.json +23 -1
  78. package/resources/config/orchestration-handoff-registry.json +138 -0
  79. package/resources/config/orchestration-handoff.schema.json +472 -0
  80. package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
  81. package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
  82. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +64 -0
@@ -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
 
@@ -2,7 +2,7 @@ default_permissions = ":danger-full-access"
2
2
 
3
3
  [mcp_servers.drm-copilot]
4
4
  command = "npx"
5
- args = ["-y", "@danmoisan/drm-copilot-mcp@1.1.10"]
5
+ args = ["-y", "@danmoisan/drm-copilot-mcp@1.1.12"]
6
6
  required = true
7
7
  enabled_tools = [
8
8
  "collect_commit_context",
@@ -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
- if ($Command -match '(?i)\bgh\s+pr\s+merge\s+(\d+)\b') {
34
- return [int]$Matches[1]
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
- if ($command -notmatch '(?i)\bgh\s+pr\s+merge\b' -or $command -notmatch '(?i)--merge\b') {
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
  }
@@ -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
- 'mcp__drm-copilot__validate_orchestration_artifacts'
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
- if ($toolName -like 'mcp__*' -and $toolName -notlike 'mcp__drm-copilot__*') {
227
- return Get-EpicPlanningDenyDecision -Reason "MCP tool '$toolName' is not part of the repository-scoped preparation surface."
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
- if ($script:AllowedPreparationMcpTools -contains $toolName) {
240
- return $null
241
- }
242
- return Get-EpicPlanningDenyDecision -Reason "MCP tool '$toolName' is outside the preparation lifecycle and validator allowlist."
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 = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
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
- $pattern = '(?i)\bgit\s+worktree\s+remove(?:\s+--force)?\s+(?:"(?<double>[^"]+)"|''(?<single>[^'']+)''|(?<bare>\S+))'
36
- if ($Command -notmatch $pattern) {
37
- return ''
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
- foreach ($name in @('double', 'single', 'bare')) {
40
- if ($Matches[$name]) {
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
- if ($command -notmatch '(?i)\bgit\s+worktree\s+remove\b') {
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