@danmoisan/drm-copilot-mcp 1.1.22 → 1.1.23

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 (85) hide show
  1. package/out/mcp-server.js +2568 -1261
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agent-memory/orchestrator/MEMORY.md +1 -1
  4. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_test_files_count_against_500_cap.md +4 -4
  5. package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -1
  6. package/resources/claude-customizations/.claude/agents/feature-review.md +8 -0
  7. package/resources/claude-customizations/.claude/agents/orchestrator.md +1 -1
  8. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +46 -6
  9. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -1
  10. package/resources/claude-customizations/.claude/agents/pr-author.md +32 -7
  11. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -1
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-green-ci.ps1 +325 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +22 -6
  14. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +23 -10
  15. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +29 -21
  16. package/resources/claude-customizations/.claude/hooks/enforce-file-size-limit.ps1 +244 -0
  17. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +48 -0
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +22 -11
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +8 -2
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +15 -10
  21. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.current-branch.ps1 +149 -0
  22. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-feature-folder.ps1 +146 -0
  23. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +25 -3
  24. package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +33 -6
  25. package/resources/claude-customizations/.claude/lib/file-size-policy/FileSizePolicy.psm1 +434 -0
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1 +166 -0
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +8 -3
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunRecordPath.psm1 +185 -0
  29. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +11 -21
  30. package/resources/claude-customizations/.claude/rules/general-code-change.md +4 -1
  31. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -5
  32. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +2 -2
  33. package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
  34. package/resources/claude-customizations/.claude/rules/shell.md +1 -1
  35. package/resources/claude-customizations/.claude/settings.json +4 -0
  36. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +2 -2
  37. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +1 -1
  38. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -0
  39. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +11 -4
  40. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +4 -1
  41. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +90 -14
  42. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +3 -1
  43. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +39 -9
  44. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +1 -1
  45. package/resources/claude-customizations/pack-manifests/core.json +7 -0
  46. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +1 -1
  47. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  48. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +8 -0
  49. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +4 -1
  50. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -1
  51. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
  52. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
  53. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
  54. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
  55. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
  56. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
  57. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +8 -0
  58. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
  59. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
  60. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
  61. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
  62. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
  63. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
  64. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
  65. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
  66. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
  67. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
  68. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
  69. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
  70. package/resources/codex-and-agents-customizations/.codex/config.toml +7 -1
  71. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-file-size-limit.ps1 +282 -0
  72. package/resources/codex-and-agents-customizations/.codex/hooks/file-size-policy.ps1 +421 -0
  73. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
  74. package/resources/codex-and-agents-customizations/AGENTS.md +4 -1
  75. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  76. package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
  77. package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +1 -1
  78. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +1 -1
  79. package/resources/customizations/.github/instructions/general-code-change.instructions.md +5 -2
  80. package/resources/customizations/.github/instructions/powershell-code-change.instructions.md +1 -1
  81. package/resources/customizations/.github/prompts/remediate-comments.prompt.md +1 -1
  82. package/resources/customizations/.github/skills/execution-environment/SKILL.md +1 -1
  83. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +8 -0
  84. package/resources/templates/policy_audit/code-review.yyyy-MM-ddTHH-mm.md +4 -4
  85. package/resources/templates/policy_audit/policy-audit.yyyy-MM-ddTHH-mm.md +4 -4
@@ -46,6 +46,8 @@ try { Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWor
46
46
  try { . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1') } catch { Add-HookDependencyFailure -Name 'hook-command-scanner.ps1' -ErrorRecord $_ }
47
47
  try { . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1') } catch { Add-HookDependencyFailure -Name 'hook-command-invocation.ps1' -ErrorRecord $_ }
48
48
 
49
+ # Shared recorded-path matcher and main-checkout-root seam (issue #995).
50
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunRecordPath.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeRunRecordPath.psm1' -ErrorRecord $_ }
49
51
  try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeRunResolution.psm1' -ErrorRecord $_ }
50
52
 
51
53
  $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
@@ -170,7 +172,10 @@ function Find-ParallelWorktreeItemRecord {
170
172
  Name of the record array to scan. Defaults to 'items', the parallel
171
173
  checkpoint's array, so every existing caller is unchanged; the epic branch
172
174
  added for issue #688 passes 'features'. Parameterized rather than duplicated
173
- so both topologies share one path-normalization implementation.
175
+ so both topologies share one path-comparison implementation.
176
+ .PARAMETER BaseRoot
177
+ Optional. The roots a relative recorded worktree_path is joined to (issue #995).
178
+ With none, a relative record matches nothing.
174
179
  .OUTPUTS
175
180
  System.Object or $null
176
181
  #>
@@ -183,7 +188,11 @@ function Find-ParallelWorktreeItemRecord {
183
188
  [string] $WorktreePath,
184
189
 
185
190
  [ValidateNotNullOrEmpty()]
186
- [string] $RecordArrayName = 'items'
191
+ [string] $RecordArrayName = 'items',
192
+
193
+ [AllowNull()]
194
+ [AllowEmptyCollection()]
195
+ [string[]] $BaseRoot = @()
187
196
  )
188
197
 
189
198
  if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
@@ -194,17 +203,14 @@ function Find-ParallelWorktreeItemRecord {
194
203
  return $null
195
204
  }
196
205
 
197
- $normalizedTarget = ($WorktreePath -replace '\\', '/').TrimEnd('/')
198
-
199
- # Scan every recorded item for a worktree_path that matches the removal target;
200
- # path separators are normalized so Windows- and POSIX-style paths compare equal.
206
+ # Scan every recorded item for a worktree_path that names the removal target through
207
+ # the shared matcher (separator style, trailing slash, case rule, relative join).
201
208
  foreach ($item in @($Checkpoint.$RecordArrayName)) {
202
209
  $itemProps = @($item.PSObject.Properties.Name)
203
210
  if ($itemProps -notcontains 'worktree_path') {
204
211
  continue
205
212
  }
206
- $normalizedItemPath = (([string]$item.worktree_path) -replace '\\', '/').TrimEnd('/')
207
- if ($normalizedItemPath -eq $normalizedTarget) {
213
+ if (Test-WorktreeRunRecordedPathMatchesTarget -RecordedPath ([string]$item.worktree_path) -TargetPath $WorktreePath -BaseRoot $BaseRoot) {
208
214
  return $item
209
215
  }
210
216
  }
@@ -353,8 +359,13 @@ function Get-ParallelWorktreeRemovalTargetDenial {
353
359
  }
354
360
  }
355
361
  $checkpoint = $parallelRead.Checkpoint
362
+ # A relative record is joined to the main checkout root and to the root holding the
363
+ # checkpoint read (issue #995); the main root is derived once per target.
364
+ $mainRoot = Get-WorktreeRunMainCheckoutRoot -SessionRoot (Get-Location).Path
365
+ $parallelBaseRoot = Get-WorktreeRunRecordBaseRoot -MainRoot $mainRoot -Read $parallelRead
366
+ $epicBaseRoot = Get-WorktreeRunRecordBaseRoot -MainRoot $mainRoot -Read $epicRead
356
367
 
357
- $itemRecord = Find-ParallelWorktreeItemRecord -Checkpoint $checkpoint -WorktreePath $worktreePath
368
+ $itemRecord = Find-ParallelWorktreeItemRecord -Checkpoint $checkpoint -WorktreePath $worktreePath -BaseRoot $parallelBaseRoot
358
369
  if (Test-ParallelWorktreeRemovalAllowed -ItemRecord $itemRecord) {
359
370
  return $null
360
371
  }
@@ -371,7 +382,7 @@ function Get-ParallelWorktreeRemovalTargetDenial {
371
382
  # $null, which yields no record, which is not allowed.
372
383
  $epicCheckpoint = $epicRead.Checkpoint
373
384
 
374
- $featureRecord = Find-ParallelWorktreeItemRecord -Checkpoint $epicCheckpoint -WorktreePath $worktreePath -RecordArrayName 'features'
385
+ $featureRecord = Find-ParallelWorktreeItemRecord -Checkpoint $epicCheckpoint -WorktreePath $worktreePath -RecordArrayName 'features' -BaseRoot $epicBaseRoot
375
386
  if (Test-ParallelWorktreeRemovalAllowed -ItemRecord $featureRecord) {
376
387
  return $null
377
388
  }
@@ -386,7 +397,7 @@ function Get-ParallelWorktreeRemovalTargetDenial {
386
397
  # reaches the unchanged deny below. This gate defines only the parallel checkpoint
387
398
  # seam and has no epic-checkpoint seam, so its exclusion covers the items array
388
399
  # only; the epic gate covers the features array with its own exclusion.
389
- if (-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $checkpoint -RecordArrayName 'items' -WorktreePath $worktreePath) -and
400
+ if (-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $checkpoint -RecordArrayName 'items' -WorktreePath $worktreePath -BaseRoot $parallelBaseRoot) -and
390
401
  (Test-CleanupWorktreeManifestAuthorizesRemoval -WorktreePath $worktreePath)) {
391
402
  return $null
392
403
  }
@@ -11,6 +11,7 @@
11
11
 
12
12
  - git log ... and git rev-parse ... (no git global option)
13
13
  - gh pr create ... and gh pr edit ... (no gh global option)
14
+ - gh pr view <N> --json closingIssuesReferences,body (exactly these six tokens, issue #989)
14
15
  - sha256sum artifacts/pr_body_<N>.md (only as the whole command)
15
16
  - date -u [+<format>] (only as the whole command)
16
17
 
@@ -41,7 +42,7 @@ $script:PrAuthorAllowlistWrapperWords = @(
41
42
  'sh', 'bash', 'zsh', 'dash', 'ksh', 'pwsh', 'powershell', 'eval', 'xargs',
42
43
  'env', 'timeout', 'time', 'command', 'nohup'
43
44
  )
44
- $script:PrAuthorAllowlistFormText = 'git log, git rev-parse, gh pr create, gh pr edit, sha256sum artifacts/pr_body_<N>.md, date -u [+<format>]'
45
+ $script:PrAuthorAllowlistFormText = 'git log, git rev-parse, gh pr create, gh pr edit, gh pr view <N> --json closingIssuesReferences,body, sha256sum artifacts/pr_body_<N>.md, date -u [+<format>]'
45
46
 
46
47
  function Test-PrAuthorAllowlistForm {
47
48
  <#
@@ -69,7 +70,12 @@ function Test-PrAuthorAllowlistForm {
69
70
  return ($Token.Count -ge 2 -and @('log', 'rev-parse') -ccontains $Token[1])
70
71
  }
71
72
  'gh' {
72
- return ($Token.Count -ge 3 -and $Token[1] -ceq 'pr' -and @('create', 'edit') -ccontains $Token[2])
73
+ if ($Token.Count -ge 3 -and $Token[1] -ceq 'pr' -and @('create', 'edit') -ccontains $Token[2]) {
74
+ return $true
75
+ }
76
+ # The read-only closing-reference verification form, matched exactly (issue #989).
77
+ return ($Token.Count -eq 6 -and $Token[1] -ceq 'pr' -and $Token[2] -ceq 'view' -and $Token[3] -cmatch '^\d+$' -and
78
+ $Token[4] -ceq '--json' -and $Token[5] -ceq 'closingIssuesReferences,body')
73
79
  }
74
80
  'sha256sum' {
75
81
  return ($IsWholeCommand -and $Token.Count -eq 2 -and $Token[1] -cmatch '^artifacts/pr_body_\d+\.md$')
@@ -27,9 +27,10 @@
27
27
 
28
28
  .NOTES
29
29
  Compatible with PowerShell 7+. Dependencies are in-repository only: the command parsers
30
- and worktree-resolution modules loaded below, and the artifact-root and docs-only siblings.
31
- No PowerShell Gallery module is required.
32
- The docs-only sibling runs the git executable at call time (issue #901).
30
+ and worktree-resolution modules loaded below, and the artifact-root, docs-only, and
31
+ current-branch siblings. No PowerShell Gallery module is required.
32
+ The docs-only sibling runs the git executable at call time (issue #901), and so does the
33
+ current-branch sibling when a call carries no target signal (issue #989).
33
34
  #>
34
35
  [CmdletBinding()]
35
36
  param()
@@ -56,6 +57,8 @@ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeRead
56
57
  . (Join-Path $PSScriptRoot 'enforce-pr-author-skill.artifact-root.ps1')
57
58
  # Docs-only exemption from the orchestrator-state preflight (issue #901).
58
59
  . (Join-Path $PSScriptRoot 'enforce-pr-author-skill.docs-only.ps1')
60
+ # Current-branch target derivation and the no-target guidance text (issue #989).
61
+ . (Join-Path $PSScriptRoot 'enforce-pr-author-skill.current-branch.ps1')
59
62
 
60
63
  function Resolve-PrAuthorWorktreeTarget {
61
64
  <#
@@ -67,7 +70,8 @@ function Resolve-PrAuthorWorktreeTarget {
67
70
  directly would depend on the machine's live worktrees and would pass or fail
68
71
  according to what other sessions happen to have checked out, which the repository's
69
72
  determinism policy forbids. Mocking here lets a test state the resolution outcome it
70
- is exercising and nothing else.
73
+ is exercising and nothing else. A NoTarget result for a command with no signal passes
74
+ through Resolve-PrAuthorCurrentBranchTarget, which re-resolves the current branch (#989).
71
75
  .OUTPUTS
72
76
  The target result object from Resolve-WorktreeItemTarget.
73
77
  #>
@@ -77,7 +81,8 @@ function Resolve-PrAuthorWorktreeTarget {
77
81
  [string] $CommandText
78
82
  )
79
83
 
80
- return (Resolve-WorktreeItemTarget -Text $CommandText -SessionRoot (Get-Location).Path)
84
+ $target = Resolve-WorktreeItemTarget -Text $CommandText -SessionRoot (Get-Location).Path
85
+ return (Resolve-PrAuthorCurrentBranchTarget -CommandText $CommandText -Target $target)
81
86
  }
82
87
 
83
88
  function Get-PrAuthorTargetCheckpointResolution {
@@ -91,10 +96,10 @@ function Get-PrAuthorTargetCheckpointResolution {
91
96
  SessionRoot -> that worktree's own checkpoint, composed absolutely.
92
97
  OtherWorktree -> that worktree's own checkpoint, composed the same way.
93
98
  NoTarget -> deny, carrying the code Get-WorktreeResolutionNoTargetReasonCode
94
- returns. pr-author passes --head on every gh pr create, so a call
95
- reaching here omitted the explicit target; the remedy is to supply
96
- it, never to fall back to whichever checkpoint occupies the
97
- session root.
99
+ returns. The call named no target and no branch could be derived
100
+ from the current checkout; the remedy text from
101
+ Get-PrAuthorTargetGuidanceText names the accepted forms, and the
102
+ gate never falls back to the session root's checkpoint.
98
103
  Ambiguous -> deny, carrying the code
99
104
  Get-WorktreeResolutionAmbiguityReasonCode returns.
100
105
 
@@ -125,7 +130,7 @@ function Get-PrAuthorTargetCheckpointResolution {
125
130
  return [ordered]@{ CheckpointPath = (Get-WorktreeItemCheckpointPath -WorktreeRoot $target.WorktreeRoot); Reason = $null; WorktreeRoot = $target.WorktreeRoot; Status = $target.Status }
126
131
  }
127
132
  default {
128
- $reason = "$($target.ReasonCode): $($target.Detail) The pr-author gate will not validate this call against the session root's checkpoint, because that checkpoint may belong to a different item. Pass --head <branch> on the gh pr create command so the call names its own target."
133
+ $reason = "$($target.ReasonCode): $($target.Detail) The pr-author gate will not validate this call against the session root's checkpoint, because that checkpoint may belong to a different item. $(Get-PrAuthorTargetGuidanceText)"
129
134
  return [ordered]@{ CheckpointPath = $null; Reason = $reason; WorktreeRoot = $null; Status = $target.Status }
130
135
  }
131
136
  }
@@ -0,0 +1,149 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Current-branch target derivation for enforce-pr-author-skill.ps1 (issue #989).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by enforce-pr-author-skill-helpers.ps1. Resolve-PrAuthorWorktreeTarget passes its
7
+ command-text resolution through Resolve-PrAuthorCurrentBranchTarget, which applies one
8
+ fallback: when the command carries no branch signal (--head, --branch, or branch:) and no
9
+ canonical issue line, and resolution returned NoTarget, the branch checked out in the hook
10
+ process's working directory is read and re-resolved through the existing resolver as a
11
+ 'branch: <name>' signal. gh pr edit has no --head flag, so this is how an edit run from the
12
+ item worktree names its own target.
13
+
14
+ Derivation order: explicit signal in the command, else the current branch, else the
15
+ original NoTarget result. A signal that matches no live worktree does not trigger the
16
+ fallback. A detached HEAD (git prints the literal HEAD), a non-zero git exit, or empty
17
+ output keeps the original NoTarget result and its reason code.
18
+
19
+ Get-PrAuthorTargetGuidanceText supplies the remedy sentence that follows the reason code and
20
+ detail in the TARGET_WORKTREE_NOT_DERIVABLE denial.
21
+
22
+ .NOTES
23
+ PowerShell 7+. Depends, at call time, on Find-WorktreeResolutionBranchSignal,
24
+ Find-WorktreeItemIssueSignal, and Resolve-WorktreeItemTarget from the worktree-resolution
25
+ modules the helpers file imports. It reads no file content. The git executable runs only
26
+ through Invoke-PrAuthorCurrentBranchGitExe, the single executable seam, which never throws.
27
+ Mirrored byte-identically under extensions/drm-copilot/resources/claude-customizations/.
28
+ #>
29
+ [CmdletBinding()]
30
+ param()
31
+
32
+ function Invoke-PrAuthorCurrentBranchGitExe {
33
+ <#
34
+ .SYNOPSIS
35
+ Run git with the given arguments in the current directory. The single executable seam.
36
+ .DESCRIPTION
37
+ Never throws: an uncaught error ends the hook with exit 1, which PreToolUse treats as
38
+ non-blocking, so a throw here would allow the call. A git that cannot start returns
39
+ ExitCode -1. Standard error is discarded so that it is never read as a branch name.
40
+ .PARAMETER GitArgs
41
+ The git arguments.
42
+ .PARAMETER GitInvoker
43
+ The scriptblock that runs git; a unit test passes a test-local stand-in so that this
44
+ body runs without starting a process.
45
+ .OUTPUTS
46
+ System.Management.Automation.PSCustomObject with ExitCode and Output (trimmed).
47
+ #>
48
+ [CmdletBinding()]
49
+ [OutputType([pscustomobject])]
50
+ param(
51
+ [Parameter(Mandatory)][string[]] $GitArgs,
52
+ [ValidateNotNull()][scriptblock] $GitInvoker = { git @args 2>$null }
53
+ )
54
+
55
+ try {
56
+ $lines = @(& $GitInvoker @GitArgs 2>$null)
57
+ $code = $LASTEXITCODE
58
+ }
59
+ catch {
60
+ return [pscustomobject]@{ ExitCode = -1; Output = '' }
61
+ }
62
+ return [pscustomobject]@{ ExitCode = [int]$code; Output = ($lines -join "`n").Trim() }
63
+ }
64
+
65
+ function Get-PrAuthorCurrentBranch {
66
+ <#
67
+ .SYNOPSIS
68
+ Return the branch checked out in the current directory, or $null.
69
+ .DESCRIPTION
70
+ Runs git rev-parse --abbrev-ref HEAD through Invoke-PrAuthorCurrentBranchGitExe. Returns
71
+ $null for a non-zero exit, for empty output, for output containing whitespace, and for
72
+ the literal HEAD that git prints on a detached checkout.
73
+ .PARAMETER GitInvoker
74
+ Optional. Forwarded to the git seam; omitted, the seam's default invoker runs git.
75
+ .OUTPUTS
76
+ System.String or $null
77
+ #>
78
+ [CmdletBinding()]
79
+ [OutputType([string])]
80
+ param(
81
+ [scriptblock] $GitInvoker
82
+ )
83
+
84
+ $seamArgs = @{ GitArgs = @('rev-parse', '--abbrev-ref', 'HEAD') }
85
+ if ($null -ne $GitInvoker) { $seamArgs['GitInvoker'] = $GitInvoker }
86
+ $result = Invoke-PrAuthorCurrentBranchGitExe @seamArgs
87
+ $name = [string]$result.Output
88
+ if ($result.ExitCode -ne 0 -or [string]::IsNullOrWhiteSpace($name) -or $name -match '\s' -or $name -ceq 'HEAD') {
89
+ return $null
90
+ }
91
+ return $name
92
+ }
93
+
94
+ function Resolve-PrAuthorCurrentBranchTarget {
95
+ <#
96
+ .SYNOPSIS
97
+ Apply the current-branch fallback to a command-text resolution result.
98
+ .DESCRIPTION
99
+ Returns Target unchanged unless its Status is NoTarget and the command text carries
100
+ neither a branch signal nor a canonical issue line. In that case the current branch is
101
+ read; a $null branch returns Target unchanged, and any other branch is re-resolved with
102
+ Resolve-WorktreeItemTarget as the text 'branch: <name>', so ambiguity handling and the
103
+ SessionRoot or OtherWorktree labelling stay with the existing resolver.
104
+ .PARAMETER CommandText
105
+ The Bash command text under evaluation.
106
+ .PARAMETER Target
107
+ The result Resolve-WorktreeItemTarget returned for the command text.
108
+ .OUTPUTS
109
+ The target result object from Resolve-WorktreeItemTarget.
110
+ #>
111
+ [CmdletBinding()]
112
+ [OutputType([pscustomobject])]
113
+ param(
114
+ [Parameter(Mandatory)][AllowEmptyString()][string] $CommandText,
115
+ [Parameter(Mandatory)][AllowNull()][object] $Target
116
+ )
117
+
118
+ if ($null -eq $Target -or $Target.Status -ne 'NoTarget') {
119
+ return $Target
120
+ }
121
+ # Find-WorktreeItemIssueSignal emits its array as one object, so it is assigned before counting.
122
+ $issueSignal = Find-WorktreeItemIssueSignal -Text $CommandText
123
+ if ($null -ne (Find-WorktreeResolutionBranchSignal -Text $CommandText) -or $issueSignal.Count -gt 0) {
124
+ return $Target
125
+ }
126
+ $branch = Get-PrAuthorCurrentBranch
127
+ if ($null -eq $branch) {
128
+ return $Target
129
+ }
130
+ return (Resolve-WorktreeItemTarget -Text "branch: $branch" -SessionRoot (Get-Location).Path)
131
+ }
132
+
133
+ function Get-PrAuthorTargetGuidanceText {
134
+ <#
135
+ .SYNOPSIS
136
+ Return the remedy sentence of the TARGET_WORKTREE_NOT_DERIVABLE denial.
137
+ .DESCRIPTION
138
+ Names the accepted target forms: --head <branch> on gh pr create, and for gh pr edit
139
+ either running the command from the item worktree (the branch is derived) or appending
140
+ the shell comment # branch: <branch>. States that --head is not valid for gh pr edit.
141
+ .OUTPUTS
142
+ System.String
143
+ #>
144
+ [CmdletBinding()]
145
+ [OutputType([string])]
146
+ param()
147
+
148
+ return 'Name the target: for gh pr create, pass --head <branch> (gh pr create --head <branch> ...). For gh pr edit, run the command from the item worktree so the gate derives the branch from its checkout, or append the shell comment # branch: <branch> to the command; --head is not valid for gh pr edit.'
149
+ }
@@ -0,0 +1,146 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Adopted feature-folder probe for the orchestrator SubagentStop hook validate-orchestrator-output.ps1 (issue #992).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by validate-orchestrator-output.ps1 through a guarded load whose catch records the
7
+ failure with Add-HookDependencyFailure, so a missing or broken copy of this file blocks the hook
8
+ instead of failing open. It defines Invoke-FeatureFolderGitExe, the single git seam, and
9
+ Get-OrchestratorOutputFeatureFolderDecision.
10
+
11
+ When an orchestrator-state checkpoint waives new_active_feature_folder through
12
+ issue_adoption.waived_tools, the validators accept a declared feature_folder_record but cannot
13
+ read the repository. This probe confirms that HEAD of the run worktree holds a tree at the
14
+ checkpoint feature-folder, so a staged-only, untracked, or absent folder blocks. It proves only
15
+ that a tree exists at that path at stop time; it does not prove which tool created the folder
16
+ or that its contents are correct.
17
+
18
+ The file imports nothing. Test-OrchestratorStateFeatureFolderPath comes from
19
+ OrchestratorStateFeatureFolderRecord.psm1, which the main hook imports through its own guarded
20
+ load. The file invokes no Python.
21
+ #>
22
+
23
+ $script:FeatureFolderProbeTool = 'new_active_feature_folder'
24
+
25
+ function Invoke-FeatureFolderGitExe {
26
+ <#
27
+ .SYNOPSIS
28
+ Run git with the given arguments. The single executable seam of the feature-folder probe.
29
+ .DESCRIPTION
30
+ Never throws: a git that cannot start, or an invoker that throws, returns ExitCode -1 so the
31
+ caller blocks instead of failing open. The default invoker merges standard error into Output;
32
+ success requires the exact output tree, so error text is never read as success.
33
+ .PARAMETER GitArgs
34
+ The git arguments.
35
+ .PARAMETER GitInvoker
36
+ The scriptblock that runs git; a unit test passes a test-local stand-in that sets
37
+ $global:LASTEXITCODE, so this body runs without starting a process.
38
+ .OUTPUTS
39
+ System.Collections.Hashtable with keys ExitCode (int) and Output (string, trimmed).
40
+ #>
41
+ [CmdletBinding()]
42
+ [OutputType([hashtable])]
43
+ param(
44
+ [Parameter(Mandatory = $true)]
45
+ [string[]] $GitArgs,
46
+
47
+ [Parameter(Mandatory = $false)]
48
+ [ValidateNotNull()]
49
+ [scriptblock] $GitInvoker = { git @args 2>&1 }
50
+ )
51
+
52
+ try {
53
+ $lines = @(& $GitInvoker @GitArgs)
54
+ $code = $LASTEXITCODE
55
+ }
56
+ catch {
57
+ return @{ ExitCode = -1; Output = '' }
58
+ }
59
+ if ($null -eq $code) { $code = -1 }
60
+ return @{ ExitCode = [int]$code; Output = ((@($lines | ForEach-Object { [string]$_ }) -join "`n").Trim()) }
61
+ }
62
+
63
+ function Test-FeatureFolderProbeWaived {
64
+ <#
65
+ .SYNOPSIS
66
+ Report whether the checkpoint's issue_adoption.waived_tools lists new_active_feature_folder.
67
+ .PARAMETER Checkpoint
68
+ The parsed checkpoint object. May be $null.
69
+ .OUTPUTS
70
+ System.Boolean
71
+ #>
72
+ [CmdletBinding()]
73
+ [OutputType([bool])]
74
+ param(
75
+ [Parameter(Mandatory = $true)]
76
+ [AllowNull()]
77
+ [object] $Checkpoint
78
+ )
79
+
80
+ if ($Checkpoint -isnot [System.Management.Automation.PSCustomObject]) { return $false }
81
+ $adoptionProperty = $Checkpoint.PSObject.Properties['issue_adoption']
82
+ if ($null -eq $adoptionProperty -or $adoptionProperty.Value -isnot [System.Management.Automation.PSCustomObject]) { return $false }
83
+ $waivedProperty = $adoptionProperty.Value.PSObject.Properties['waived_tools']
84
+ if ($null -eq $waivedProperty -or $waivedProperty.Value -isnot [System.Array]) { return $false }
85
+ return (@($waivedProperty.Value | Where-Object { $_ -is [string] }) -ccontains $script:FeatureFolderProbeTool)
86
+ }
87
+
88
+ function Get-OrchestratorOutputFeatureFolderDecision {
89
+ <#
90
+ .SYNOPSIS
91
+ Confirm that HEAD of the run worktree holds a tree at the adopted feature-folder.
92
+ .DESCRIPTION
93
+ Returns @{ Ok = $true; Message = $null } when the checkpoint does not waive
94
+ new_active_feature_folder. Otherwise the checkpoint feature-folder is re-checked against the
95
+ shared path pattern before any git call (a failing path never reaches git), and
96
+ git -C <WorktreeRoot> cat-file -t HEAD:<feature-folder> must exit 0 and print exactly tree.
97
+ Every other outcome (non-zero exit, -1, empty output, other output) blocks with a repairable
98
+ ROUTING_CONTRACT_BLOCKED message naming the path.
99
+ .PARAMETER Checkpoint
100
+ The parsed checkpoint object. May be $null.
101
+ .PARAMETER WorktreeRoot
102
+ The root of the worktree whose run stopped.
103
+ .PARAMETER GitInvoker
104
+ Optional. Forwarded to Invoke-FeatureFolderGitExe; omitted, the seam's default runs git.
105
+ .OUTPUTS
106
+ System.Collections.Hashtable with keys Ok, Message, and (on a block) Repairable.
107
+ #>
108
+ [CmdletBinding()]
109
+ [OutputType([hashtable])]
110
+ param(
111
+ [Parameter(Mandatory = $true)]
112
+ [AllowNull()]
113
+ [object] $Checkpoint,
114
+
115
+ [Parameter(Mandatory = $true)]
116
+ [string] $WorktreeRoot,
117
+
118
+ [Parameter(Mandatory = $false)]
119
+ [scriptblock] $GitInvoker
120
+ )
121
+
122
+ if (-not (Test-FeatureFolderProbeWaived -Checkpoint $Checkpoint)) {
123
+ return @{ Ok = $true; Message = $null }
124
+ }
125
+
126
+ $folderProperty = $Checkpoint.PSObject.Properties['feature-folder']
127
+ $folder = if ($null -ne $folderProperty) { $folderProperty.Value } else { $null }
128
+ $blocked = @{ Ok = $false; Repairable = $true; Message = ('ROUTING_CONTRACT_BLOCKED: Checkpoint feature-folder is not committed in HEAD: {0}.' -f [string]$folder) }
129
+ # Defense in depth: the validators already require the record path to match the pattern and to
130
+ # equal feature-folder, but a value that fails the pattern must never become a git argument.
131
+ if (-not (Test-OrchestratorStateFeatureFolderPath -Value $folder)) {
132
+ return $blocked
133
+ }
134
+
135
+ $gitArgs = [string[]]@('-C', $WorktreeRoot, 'cat-file', '-t', ('HEAD:' + [string]$folder))
136
+ $probe = if ($PSBoundParameters.ContainsKey('GitInvoker') -and $null -ne $GitInvoker) {
137
+ Invoke-FeatureFolderGitExe -GitArgs $gitArgs -GitInvoker $GitInvoker
138
+ }
139
+ else {
140
+ Invoke-FeatureFolderGitExe -GitArgs $gitArgs
141
+ }
142
+ if (($probe.ExitCode -eq 0) -and [string]::Equals([string]$probe.Output, 'tree', [System.StringComparison]::Ordinal)) {
143
+ return @{ Ok = $true; Message = $null }
144
+ }
145
+ return $blocked
146
+ }
@@ -25,7 +25,9 @@
25
25
  ambiguous, or mismatched target blocks with ORCHESTRATOR_CHECKPOINT_UNRESOLVED:
26
26
  before any checkpoint is read. For epic-orchestrator-state, the Layer 2 wave-barrier
27
27
  ordering check (OrchestratorStateEpicWaveBarrier.psm1, issue #840) runs after the
28
- routing dispatch passes.
28
+ routing dispatch passes. For orchestrator-state, the adopted feature-folder probe
29
+ (validate-orchestrator-output-feature-folder.ps1, issue #992) runs after the routing
30
+ dispatch passes, and only when the checkpoint waives new_active_feature_folder.
29
31
 
30
32
  .NOTES
31
33
  Reads the SubagentStop envelope from stdin through Read-SubagentStopEnvelope
@@ -61,6 +63,10 @@ try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/Orchestr
61
63
  try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateCompletion.psm1') -Force -Global -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateCompletion.psm1' -ErrorRecord $_ }
62
64
  try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateUnconditional.psm1') -Force -Global -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateUnconditional.psm1' -ErrorRecord $_ }
63
65
  try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorState.psm1') -Force -Global -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorState.psm1' -ErrorRecord $_ }
66
+ # Imported after the completion modules without -Force, so the instance they already loaded through
67
+ # the adoption module stays the one in use (issue #992).
68
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1') -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateFeatureFolderRecord.psm1' -ErrorRecord $_ }
69
+ try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-feature-folder.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-feature-folder.ps1' -ErrorRecord $_ }
64
70
  try { . (Join-Path $PSScriptRoot 'subagentstop-envelope.ps1') } catch { Add-HookDependencyFailure -Name 'subagentstop-envelope.ps1' -ErrorRecord $_ }
65
71
  $ErrorActionPreference = 'Stop'
66
72
  $script:SubagentStopReportAndHaltLine = 'This condition is not repairable by the subagent. Report it to the operator and halt.'
@@ -194,7 +200,10 @@ function Invoke-OrchestratorOutputValidation {
194
200
  [string] $SessionRoot = (Get-Location).Path,
195
201
 
196
202
  [Parameter(Mandatory = $false)]
197
- [scriptblock] $RoutingInvoker
203
+ [scriptblock] $RoutingInvoker,
204
+
205
+ [Parameter(Mandatory = $false)]
206
+ [scriptblock] $FeatureFolderGitInvoker
198
207
  )
199
208
  $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent SubagentStop -ReasonPrefix 'ORCHESTRATOR_CHECKPOINT_UNRESOLVED:'
200
209
  if ($null -ne $dependencyDecision) { return $dependencyDecision }
@@ -205,6 +214,7 @@ function Invoke-OrchestratorOutputValidation {
205
214
  $agentOutput = $envelope.AssistantText
206
215
  $blockArguments = @{ AgentOutput = ([string]$agentOutput); CheckpointPath = $CheckpointPath; ArtifactType = $ArtifactType; SessionRoot = $SessionRoot }
207
216
  if ($PSBoundParameters.ContainsKey('RoutingInvoker')) { $blockArguments['RoutingInvoker'] = $RoutingInvoker }
217
+ if ($PSBoundParameters.ContainsKey('FeatureFolderGitInvoker')) { $blockArguments['FeatureFolderGitInvoker'] = $FeatureFolderGitInvoker }
208
218
  $decision = Get-OrchestratorOutputBlockDecision @blockArguments
209
219
  }
210
220
  if ($decision.Ok) { return @{ Ok = $true; Message = $null } }
@@ -235,7 +245,10 @@ function Get-OrchestratorOutputBlockDecision {
235
245
  [string] $SessionRoot = (Get-Location).Path,
236
246
 
237
247
  [Parameter(Mandatory = $false)]
238
- [scriptblock] $RoutingInvoker
248
+ [scriptblock] $RoutingInvoker,
249
+
250
+ [Parameter(Mandatory = $false)]
251
+ [scriptblock] $FeatureFolderGitInvoker
239
252
  )
240
253
 
241
254
  if ([string]::IsNullOrWhiteSpace($agentOutput)) {
@@ -322,6 +335,15 @@ function Get-OrchestratorOutputBlockDecision {
322
335
  return @{ Ok = $false; Repairable = $true; Message = "ROUTING_CONTRACT_BLOCKED: $($routingResult.ErrorText)" }
323
336
  }
324
337
 
338
+ # Issue #992: a waived new_active_feature_folder must name a folder committed in HEAD of the run
339
+ # worktree. The validators only see the declared record, so the hook probes git once.
340
+ if ($ArtifactType -eq 'orchestrator-state') {
341
+ $folderArgs = @{ Checkpoint = $checkpoint; WorktreeRoot = [string]$resolution.WorktreeRoot }
342
+ if ($PSBoundParameters.ContainsKey('FeatureFolderGitInvoker') -and $null -ne $FeatureFolderGitInvoker) { $folderArgs['GitInvoker'] = $FeatureFolderGitInvoker }
343
+ $folderDecision = Get-OrchestratorOutputFeatureFolderDecision @folderArgs
344
+ if (-not $folderDecision.Ok) { return @{ Ok = $false; Repairable = $folderDecision.Repairable; Message = $folderDecision.Message } }
345
+ }
346
+
325
347
  # Layer 2 (issue #840): the epic checkpoint only, after the routing dispatch passes,
326
348
  # on the checkpoint text already read.
327
349
  if ($ArtifactType -eq 'epic-orchestrator-state') {
@@ -24,8 +24,8 @@
24
24
  throwing.
25
25
 
26
26
  .NOTES
27
- Compatible with PowerShell 7+. No external module dependencies, no subprocess,
28
- and no network. The single filesystem read and the single wall-clock read each
27
+ Compatible with PowerShell 7+. Imports only the in-repository sibling
28
+ WorktreeRunRecordPath.psm1 (issue #995); no subprocess and no network. The single filesystem read and the single wall-clock read each
29
29
  sit behind an injectable seam so tests drive them without writing a temporary
30
30
  file. Mirrored byte-identically under
31
31
  extensions/drm-copilot/resources/claude-customizations/.
@@ -35,6 +35,9 @@
35
35
  Set-StrictMode -Version Latest
36
36
  $ErrorActionPreference = 'Stop'
37
37
 
38
+ # The shared recorded-path matcher (issue #995), used by the checkpoint-coverage test.
39
+ Import-Module (Join-Path $PSScriptRoot '../worktree-resolution/WorktreeRunRecordPath.psm1') -ErrorAction Stop
40
+
38
41
  # Repo-relative location of the sanctioned-removal manifest. Kept as a script-scope
39
42
  # constant so the read seam is the only function that names the path.
40
43
  $script:CleanupWorktreeManifestPath = 'artifacts/orchestration/cleanup-worktrees-manifest.json'
@@ -355,6 +358,14 @@ function Test-CleanupManifestCheckpointCoversPath {
355
358
  the gate's existing deny. Reusing the gates' merge_status predicate here
356
359
  would reopen the one path by which the manifest could widen what the gates
357
360
  protect.
361
+
362
+ Case rule: a record covers the target when the shared matcher holds on the
363
+ inputs as given, or holds on the same inputs lower-cased with the invariant
364
+ culture. This keeps the exclusion at parity with
365
+ Find-CleanupWorktreeManifestRemovalRecord, which ignores case for every path,
366
+ so a record that differs from the target only by case still blocks the
367
+ manifest branch. The shared matcher itself stays ordinal for single-slash
368
+ POSIX paths because the gates' allow lookups must remain fail-closed.
358
369
  .PARAMETER Checkpoint
359
370
  Parsed checkpoint object, or $null when absent or unreadable.
360
371
  .PARAMETER RecordArrayName
@@ -362,6 +373,10 @@ function Test-CleanupManifestCheckpointCoversPath {
362
373
  for the parallel checkpoint.
363
374
  .PARAMETER WorktreePath
364
375
  The removal target extracted from the command text.
376
+ .PARAMETER BaseRoot
377
+ Optional. The roots a relative recorded worktree_path is joined to: the main
378
+ checkout root and the root that holds the checkpoint (issue #995). With none, a
379
+ relative record covers nothing and a rooted record compares as before.
365
380
  .OUTPUTS
366
381
  System.Boolean
367
382
  #>
@@ -377,7 +392,11 @@ function Test-CleanupManifestCheckpointCoversPath {
377
392
 
378
393
  [AllowNull()]
379
394
  [AllowEmptyString()]
380
- [string] $WorktreePath
395
+ [string] $WorktreePath,
396
+
397
+ [AllowNull()]
398
+ [AllowEmptyCollection()]
399
+ [string[]] $BaseRoot = @()
381
400
  )
382
401
 
383
402
  if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
@@ -388,8 +407,10 @@ function Test-CleanupManifestCheckpointCoversPath {
388
407
  return $false
389
408
  }
390
409
 
391
- $normalizedTarget = ConvertTo-CleanupWorktreeManifestNormalizedPath -Path $WorktreePath
410
+ $lowerBaseRoot = [string[]] @(@($BaseRoot) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.ToLowerInvariant() })
392
411
 
412
+ # Every record is compared through the shared matcher, so a relative record covers
413
+ # the absolute target it names under a base root, and an unsafe one covers nothing.
393
414
  foreach ($record in @($Checkpoint.PSObject.Properties[$RecordArrayName].Value)) {
394
415
  if ($null -eq $record) {
395
416
  continue
@@ -398,8 +419,14 @@ function Test-CleanupManifestCheckpointCoversPath {
398
419
  if ($recordProperties -notcontains 'worktree_path') {
399
420
  continue
400
421
  }
401
- $normalizedRecordPath = ConvertTo-CleanupWorktreeManifestNormalizedPath -Path ([string]$record.worktree_path)
402
- if ($normalizedRecordPath -eq $normalizedTarget) {
422
+ $recordedPath = [string]$record.worktree_path
423
+ if (Test-WorktreeRunRecordedPathMatchesTarget -RecordedPath $recordedPath -TargetPath $WorktreePath -BaseRoot $BaseRoot) {
424
+ return $true
425
+ }
426
+ # Case-insensitive parity with Find-CleanupWorktreeManifestRemovalRecord: the same
427
+ # inputs lower-cased with the invariant culture, so the exclusion is never narrower
428
+ # than the manifest lookup it guards.
429
+ if (Test-WorktreeRunRecordedPathMatchesTarget -RecordedPath $recordedPath.ToLowerInvariant() -TargetPath $WorktreePath.ToLowerInvariant() -BaseRoot $lowerBaseRoot) {
403
430
  return $true
404
431
  }
405
432
  }