@danmoisan/drm-copilot-mcp 1.1.16 → 1.1.18

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 (105) hide show
  1. package/out/mcp-server.js +1139 -133
  2. package/package.json +4 -3
  3. package/resources/claude-customizations/.claude/agents/feature-review.md +10 -1
  4. package/resources/claude-customizations/.claude/agents/orchestrator.md +16 -3
  5. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +18 -6
  6. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
  7. package/resources/claude-customizations/.claude/hooks/{enforce-powershell-batch-budget-route.ps1 → enforce-batch-budget-route.ps1} +12 -10
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
  14. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
  19. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +3 -3
  20. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
  21. package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
  22. package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +4 -2
  23. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
  24. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
  25. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -13
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +375 -0
  28. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +22 -15
  29. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +337 -0
  30. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +9 -1
  31. package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
  32. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
  33. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
  34. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
  35. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
  36. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
  37. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +119 -3
  38. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
  39. package/resources/claude-customizations/.claude/rules/quality-tiers.md +1 -1
  40. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  41. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +2 -0
  42. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +11 -3
  43. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +11 -2
  44. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +169 -5
  45. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +1 -1
  46. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +6 -43
  47. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +4 -15
  48. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +15 -6
  49. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  50. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +10 -0
  51. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +3 -1
  52. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
  53. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -0
  54. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
  55. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +33 -13
  56. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +50 -21
  57. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
  58. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
  59. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +5 -1
  60. package/resources/claude-customizations/pack-manifests/core.json +10 -0
  61. package/resources/claude-customizations/pack-manifests/powershell.json +0 -1
  62. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  63. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +5 -3
  64. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  65. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  66. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +30 -6
  67. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +3 -1
  68. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -1
  69. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
  70. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +30 -7
  71. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -2
  72. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +18 -3
  73. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
  74. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +1 -1
  75. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
  76. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -0
  77. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -0
  78. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -0
  79. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -0
  80. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -0
  81. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -0
  82. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +1 -1
  83. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +1 -1
  84. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +1 -1
  85. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +1 -1
  86. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +1 -1
  87. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +1 -1
  88. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
  89. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
  90. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
  91. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
  92. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
  93. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
  94. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
  95. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  96. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
  97. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +4 -124
  98. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
  99. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  100. package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
  101. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
  102. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
  103. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  104. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  105. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -1
@@ -0,0 +1,497 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Portable-identity resolution of the epic or parallel run a gated call belongs to (issue #690).
4
+
5
+ .DESCRIPTION
6
+ Selects the worktree whose run checkpoint governs a call from portable identity
7
+ alone: the integration_branch:, epic_feature_folder:, and parallel_slug: literals of
8
+ a kickoff prompt, or a pull request number or worktree path a command names. A
9
+ checkpoint path is always composed beneath a resolved root, so no read binds to the
10
+ calling process's directory.
11
+
12
+ A live worktree matches an epic run when its epic checkpoint records route_id epic
13
+ and the integration branch; several matches are narrowed to the worktree that has
14
+ that branch checked out, because git checks a branch out in at most one worktree. A
15
+ parallel run has no branch, so several matches are ambiguous. A record match keeps
16
+ the live worktrees whose run checkpoint records the pull request number or worktree
17
+ path. There is no session-root-first shortcut: a stale copy at the session root never
18
+ wins over the worktree that owns the run.
19
+
20
+ .NOTES
21
+ PowerShell 7+. Re-uses its siblings for enumeration, the ascent, normalisation, the
22
+ join, the result constructor, the SessionRoot/OtherWorktree labelling, and both
23
+ reason-code accessors, re-implementing none. Its only filesystem read is the
24
+ checkpoint-text seam. Mirrored byte-identically.
25
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
26
+ #>
27
+
28
+ Set-StrictMode -Version Latest
29
+ $ErrorActionPreference = 'Stop'
30
+
31
+ Import-Module (Join-Path $PSScriptRoot 'WorktreeResolution.psm1') -ErrorAction Stop
32
+ Import-Module (Join-Path $PSScriptRoot 'WorktreeTargetResolution.psm1') -ErrorAction Stop
33
+ Import-Module (Join-Path $PSScriptRoot 'WorktreeItemResolution.psm1') -ErrorAction Stop
34
+
35
+ # The run checkpoint locations relative to a worktree root.
36
+ $script:EpicCheckpointRelativePath = 'artifacts/orchestration/epic-orchestrator-state.json'
37
+ $script:ParallelCheckpointRelativePath = 'artifacts/orchestration/parallel-orchestrator-state.json'
38
+
39
+ # The route_id each run checkpoint must record.
40
+ $script:EpicRouteId = 'epic'
41
+ $script:ParallelRouteId = 'parallel'
42
+
43
+ # Where a stale checkpoint copy is moved so it stops competing with the owning worktree.
44
+ $script:HandoffRemedy = 'artifacts/orchestration/handoff/'
45
+
46
+ # The kickoff literals, matched case-sensitively and never as the tail of a longer word.
47
+ $script:IntegrationBranchPattern = '(?<![A-Za-z0-9_])integration_branch:[ \t]*(?<value>\S+)'
48
+ $script:EpicSlugPattern = '(?<![A-Za-z0-9_])epic_feature_folder:[ \t]*(?<value>\S+)'
49
+ $script:ParallelSlugPattern = '(?<![A-Za-z0-9_])parallel_slug:[ \t]*(?<value>\S+)'
50
+
51
+ # A drive-rooted or slash-rooted path; anything else is relative to some directory.
52
+ $script:AbsolutePathPattern = '^([A-Za-z]:(/|$)|/)'
53
+
54
+ # Private: the single value a pattern names in a text, or $null when it names none or
55
+ # more than one distinct value (a prompt that names two runs identifies neither).
56
+ function Get-WorktreeRunSignalValue {
57
+ [CmdletBinding()]
58
+ [OutputType([string])]
59
+ param(
60
+ [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $Text,
61
+ [Parameter(Mandatory = $true)] [string] $Pattern
62
+ )
63
+
64
+ $values = [System.Collections.Generic.List[string]]::new()
65
+ foreach ($match in [regex]::Matches($Text, $Pattern)) {
66
+ $value = $match.Groups['value'].Value
67
+ # The kickoff sentence ends the value with a full stop; one is removed.
68
+ if ($value.EndsWith('.')) { $value = $value.Substring(0, $value.Length - 1) }
69
+ if ($value.Length -gt 0 -and -not $values.Contains($value)) { $values.Add($value) }
70
+ }
71
+ if ($values.Count -eq 1) { return $values[0] }
72
+ return $null
73
+ }
74
+
75
+ function Find-WorktreeRunIdentitySignal {
76
+ <#
77
+ .SYNOPSIS
78
+ Read the run identity literals from a prompt.
79
+ .DESCRIPTION
80
+ Pure. Returns an object with IntegrationBranch, EpicSlug, and ParallelSlug, each
81
+ the single distinct value the prompt names for that literal, or $null.
82
+ .PARAMETER Text
83
+ The prompt text to scan.
84
+ #>
85
+ [CmdletBinding()]
86
+ [OutputType([pscustomobject])]
87
+ param(
88
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Text
89
+ )
90
+
91
+ $source = [string] $Text
92
+ return [pscustomobject]@{
93
+ IntegrationBranch = Get-WorktreeRunSignalValue -Text $source -Pattern $script:IntegrationBranchPattern
94
+ EpicSlug = Get-WorktreeRunSignalValue -Text $source -Pattern $script:EpicSlugPattern
95
+ ParallelSlug = Get-WorktreeRunSignalValue -Text $source -Pattern $script:ParallelSlugPattern
96
+ }
97
+ }
98
+
99
+ function Get-WorktreeRunCheckpointText {
100
+ <#
101
+ .SYNOPSIS
102
+ Return the raw text of a file, or $null when it is absent or unreadable; an unreadable file also writes a diagnostic to stderr.
103
+ .DESCRIPTION
104
+ The module's only filesystem read, isolated as the seam a test mocks.
105
+ .PARAMETER Path
106
+ The absolute path to read.
107
+ #>
108
+ [CmdletBinding()]
109
+ [OutputType([string])]
110
+ param(
111
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Path
112
+ )
113
+
114
+ if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
115
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction SilentlyContinue)) { return $null }
116
+ # An unreadable file is reported on stderr and treated as absent, so resolution stays fail-closed and the hook's stdout stays JSON-only.
117
+ try { return [System.IO.File]::ReadAllText($Path) }
118
+ catch { [Console]::Error.WriteLine(("WORKTREE_RUN_CHECKPOINT_UNREADABLE: '{0}': {1}" -f $Path, $_.Exception.Message)); return $null }
119
+ }
120
+
121
+ function Get-WorktreeRunCheckpointPath {
122
+ <#
123
+ .SYNOPSIS
124
+ Compose the absolute path of a run or item checkpoint beneath a worktree root.
125
+ .DESCRIPTION
126
+ The single composition of an absolute checkpoint path for the gates. Throws when
127
+ the root is not absolute, through the shared join.
128
+ .PARAMETER Kind
129
+ epic, parallel, or item.
130
+ .PARAMETER WorktreeRoot
131
+ The absolute worktree root, in either separator style.
132
+ #>
133
+ [CmdletBinding()]
134
+ [OutputType([string])]
135
+ param(
136
+ [Parameter(Mandatory = $true)] [ValidateSet('epic', 'parallel', 'item')] [string] $Kind,
137
+ [Parameter(Mandatory = $true)] [string] $WorktreeRoot
138
+ )
139
+
140
+ if ($Kind -eq 'item') { return (Get-WorktreeItemCheckpointPath -WorktreeRoot $WorktreeRoot) }
141
+ $relative = if ($Kind -eq 'epic') { $script:EpicCheckpointRelativePath } else { $script:ParallelCheckpointRelativePath }
142
+ return (Join-WorktreeResolutionPath -WorktreeRoot $WorktreeRoot -RepoRelativePath $relative)
143
+ }
144
+
145
+ # Private: parse checkpoint text into an object, or $null for blank, unparseable, or
146
+ # non-object text. -NoEnumerate keeps a one-element JSON array from reading as an object.
147
+ function ConvertFrom-WorktreeRunCheckpointText {
148
+ [CmdletBinding()]
149
+ [OutputType([pscustomobject])]
150
+ param(
151
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Text
152
+ )
153
+
154
+ if ([string]::IsNullOrWhiteSpace($Text)) { return $null }
155
+ $parsed = $null
156
+ try { $parsed = ConvertFrom-Json -InputObject $Text -NoEnumerate } catch { return $null }
157
+ # The full type name is tested, because a wrapped array also satisfies [pscustomobject].
158
+ if ($null -eq $parsed -or $parsed -isnot [System.Management.Automation.PSCustomObject]) { return $null }
159
+ return $parsed
160
+ }
161
+
162
+ # Private: a named property of a parsed object, or $null when absent (strict mode would
163
+ # otherwise throw on a missing member).
164
+ function Get-WorktreeRunPropertyValue {
165
+ [CmdletBinding()]
166
+ [OutputType([object])]
167
+ param(
168
+ [Parameter(Mandatory = $true)] [AllowNull()] [object] $InputObject,
169
+ [Parameter(Mandatory = $true)] [string] $Name
170
+ )
171
+
172
+ if ($null -eq $InputObject -or $InputObject -isnot [System.Management.Automation.PSCustomObject]) { return $null }
173
+ if ($InputObject.PSObject.Properties.Name -notcontains $Name) { return $null }
174
+ return $InputObject.$Name
175
+ }
176
+
177
+ # Private: read and parse a live root's run checkpoint of one kind, keeping it only when
178
+ # it records the kind's route_id.
179
+ function Get-WorktreeRunCheckpoint {
180
+ [CmdletBinding()]
181
+ [OutputType([pscustomobject])]
182
+ param(
183
+ [Parameter(Mandatory = $true)] [ValidateSet('epic', 'parallel')] [string] $Kind,
184
+ [Parameter(Mandatory = $true)] [string] $WorktreeRoot
185
+ )
186
+
187
+ $text = Get-WorktreeRunCheckpointText -Path (Get-WorktreeRunCheckpointPath -Kind $Kind -WorktreeRoot $WorktreeRoot)
188
+ $checkpoint = ConvertFrom-WorktreeRunCheckpointText -Text $text
189
+ $routeId = if ($Kind -eq 'epic') { $script:EpicRouteId } else { $script:ParallelRouteId }
190
+ if ([string] (Get-WorktreeRunPropertyValue -InputObject $checkpoint -Name 'route_id') -cne $routeId) { return $null }
191
+ return $checkpoint
192
+ }
193
+
194
+ # Private: the live roots reachable from the session path, assigned before wrapping
195
+ # because the seam returns its array as one object.
196
+ function Get-WorktreeRunLiveRoot {
197
+ [CmdletBinding()]
198
+ [OutputType([string[]], [object[]])]
199
+ param(
200
+ [Parameter(Mandatory = $true)] [string] $SessionRoot,
201
+ [string] $Branch
202
+ )
203
+
204
+ $liveRoots = if ([string]::IsNullOrWhiteSpace($Branch)) {
205
+ Get-WorktreeItemLiveRoot -SessionRoot $SessionRoot
206
+ }
207
+ else {
208
+ Get-WorktreeItemLiveRoot -SessionRoot $SessionRoot -Branch $Branch
209
+ }
210
+ return , [string[]] @($liveRoots | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
211
+ }
212
+
213
+ # Private: the zero/one/many decision shared by the parallel and record resolvers.
214
+ function Resolve-WorktreeRunMatch {
215
+ [CmdletBinding()]
216
+ [OutputType([pscustomobject])]
217
+ param(
218
+ [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [string[]] $Matched,
219
+ [Parameter(Mandatory = $true)] [string] $SessionRoot,
220
+ [Parameter(Mandatory = $true)] [string] $Description
221
+ )
222
+
223
+ if ($Matched.Count -eq 0) {
224
+ return (New-WorktreeResolutionTargetResult -Status 'NoTarget' -SessionRoot $SessionRoot -Detail (
225
+ 'no live worktree''s run checkpoint records {0}' -f $Description))
226
+ }
227
+ if ($Matched.Count -eq 1) {
228
+ return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $Matched[0] -SessionRoot $SessionRoot -Detail (
229
+ '{0} resolves to the live worktree whose run checkpoint records it' -f $Description))
230
+ }
231
+ $detail = ('{0} is recorded by the run checkpoints of {1} live worktrees; move the stale copy to {2}' -f
232
+ $Description, $Matched.Count, $script:HandoffRemedy)
233
+ return (New-WorktreeResolutionTargetResult -Status 'Ambiguous' -SessionRoot $SessionRoot -Candidate $Matched -Detail $detail)
234
+ }
235
+
236
+ function Resolve-WorktreeEpicTarget {
237
+ <#
238
+ .SYNOPSIS
239
+ Resolve the live worktree whose epic checkpoint governs an integration branch.
240
+ .DESCRIPTION
241
+ Keeps each live root whose epic checkpoint records route_id epic and the branch.
242
+ Zero matches resolve NoTarget; one resolves; several are narrowed to the root that
243
+ has the branch checked out, and anything other than one survivor is Ambiguous. A
244
+ supplied slug that a match's epic_feature_folder disagrees with is Ambiguous.
245
+ .PARAMETER IntegrationBranch
246
+ The integration branch the call names.
247
+ .PARAMETER EpicSlug
248
+ Optional. The epic_feature_folder the call names, used as a cross-check only.
249
+ .PARAMETER SessionRoot
250
+ The calling process's path, used only to find the repository and label a result.
251
+ #>
252
+ [CmdletBinding()]
253
+ [OutputType([pscustomobject])]
254
+ param(
255
+ [AllowNull()] [AllowEmptyString()] [string] $IntegrationBranch,
256
+ [AllowNull()] [AllowEmptyString()] [string] $EpicSlug,
257
+ [Parameter(Mandatory = $true)] [string] $SessionRoot
258
+ )
259
+
260
+ if ([string]::IsNullOrWhiteSpace($IntegrationBranch)) {
261
+ return (New-WorktreeResolutionTargetResult -Status 'NoTarget' -SessionRoot $SessionRoot -Detail (
262
+ 'the call carries no integration_branch: value, so the epic run it belongs to cannot be identified'))
263
+ }
264
+
265
+ $kept = [System.Collections.Generic.List[string]]::new()
266
+ $slugs = [System.Collections.Generic.List[string]]::new()
267
+ foreach ($root in (Get-WorktreeRunLiveRoot -SessionRoot $SessionRoot)) {
268
+ $checkpoint = Get-WorktreeRunCheckpoint -Kind 'epic' -WorktreeRoot $root
269
+ if ($null -eq $checkpoint) { continue }
270
+ if ([string] (Get-WorktreeRunPropertyValue -InputObject $checkpoint -Name 'integration_branch') -cne $IntegrationBranch) { continue }
271
+ $kept.Add($root)
272
+ $recordedSlug = Get-WorktreeRunPropertyValue -InputObject $checkpoint -Name 'epic_feature_folder'
273
+ if ($null -ne $recordedSlug) { $slugs.Add([string] $recordedSlug) }
274
+ }
275
+
276
+ # A slug the call names that a matching checkpoint contradicts is not guessed past.
277
+ if (-not [string]::IsNullOrWhiteSpace($EpicSlug)) {
278
+ $disagreeing = @($slugs | Where-Object { $_ -cne $EpicSlug })
279
+ if ($disagreeing.Count -gt 0) {
280
+ return (New-WorktreeResolutionTargetResult -Status 'Ambiguous' -SessionRoot $SessionRoot `
281
+ -Signal 'Branch' -SignalValue $IntegrationBranch -Candidate $kept.ToArray() -Detail (
282
+ "the call names epic_feature_folder '{0}', but an epic checkpoint recording integration_branch '{1}' records epic_feature_folder '{2}'" -f
283
+ $EpicSlug, $IntegrationBranch, $disagreeing[0]))
284
+ }
285
+ }
286
+
287
+ if ($kept.Count -eq 0) {
288
+ return (New-WorktreeResolutionTargetResult -Status 'NoTarget' -SessionRoot $SessionRoot -Detail (
289
+ "no live worktree's epic checkpoint records integration_branch '{0}'" -f $IntegrationBranch))
290
+ }
291
+ if ($kept.Count -eq 1) {
292
+ return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $kept[0] -SessionRoot $SessionRoot -Branch $IntegrationBranch -Detail (
293
+ "integration_branch '{0}' resolves to the live worktree whose epic checkpoint records it" -f $IntegrationBranch))
294
+ }
295
+
296
+ # Several copies: the owning worktree is the one with the integration branch checked out.
297
+ $branchRoots = Get-WorktreeRunLiveRoot -SessionRoot $SessionRoot -Branch $IntegrationBranch
298
+ $checkedOut = @(foreach ($root in $branchRoots) { ConvertTo-WorktreeResolutionNormalizedPath -Path $root })
299
+ $survivors = @($kept | Where-Object { $checkedOut -contains (ConvertTo-WorktreeResolutionNormalizedPath -Path $_) })
300
+ if ($survivors.Count -eq 1) {
301
+ return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $survivors[0] -SessionRoot $SessionRoot -Branch $IntegrationBranch -Detail (
302
+ "integration_branch '{0}' resolves to the live worktree that has it checked out" -f $IntegrationBranch))
303
+ }
304
+ $detail = ("the epic checkpoints of {0} live worktrees record integration_branch '{1}', and {2} of them have it checked out; move the stale copy to {3}" -f
305
+ $kept.Count, $IntegrationBranch, $survivors.Count, $script:HandoffRemedy)
306
+ return (New-WorktreeResolutionTargetResult -Status 'Ambiguous' -SessionRoot $SessionRoot `
307
+ -Signal 'Branch' -SignalValue $IntegrationBranch -Candidate $kept.ToArray() -Detail $detail)
308
+ }
309
+
310
+ function Resolve-WorktreeParallelTarget {
311
+ <#
312
+ .SYNOPSIS
313
+ Resolve the live worktree whose parallel checkpoint records a parallel slug.
314
+ .DESCRIPTION
315
+ Keeps each live root whose parallel checkpoint records route_id parallel and the
316
+ slug. Zero matches resolve NoTarget, one resolves, and several are Ambiguous.
317
+ .PARAMETER ParallelSlug
318
+ The parallel_slug the call names.
319
+ .PARAMETER SessionRoot
320
+ The calling process's path, used only to find the repository and label a result.
321
+ #>
322
+ [CmdletBinding()]
323
+ [OutputType([pscustomobject])]
324
+ param(
325
+ [AllowNull()] [AllowEmptyString()] [string] $ParallelSlug,
326
+ [Parameter(Mandatory = $true)] [string] $SessionRoot
327
+ )
328
+
329
+ if ([string]::IsNullOrWhiteSpace($ParallelSlug)) {
330
+ return (New-WorktreeResolutionTargetResult -Status 'NoTarget' -SessionRoot $SessionRoot -Detail (
331
+ 'the call carries no parallel_slug: value, so the parallel run it belongs to cannot be identified'))
332
+ }
333
+ $matched = [System.Collections.Generic.List[string]]::new()
334
+ foreach ($root in (Get-WorktreeRunLiveRoot -SessionRoot $SessionRoot)) {
335
+ $checkpoint = Get-WorktreeRunCheckpoint -Kind 'parallel' -WorktreeRoot $root
336
+ if ($null -eq $checkpoint) { continue }
337
+ if ([string] (Get-WorktreeRunPropertyValue -InputObject $checkpoint -Name 'parallel_slug') -ceq $ParallelSlug) { $matched.Add($root) }
338
+ }
339
+ return (Resolve-WorktreeRunMatch -Matched $matched.ToArray() -SessionRoot $SessionRoot -Description ("parallel_slug '{0}'" -f $ParallelSlug))
340
+ }
341
+
342
+ # Private: compare two worktree paths, insensitive to separator style and trailing
343
+ # slashes, and case-insensitive only for drive-letter paths.
344
+ function Test-WorktreeRunPathEqual {
345
+ [CmdletBinding()]
346
+ [OutputType([bool])]
347
+ param(
348
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Left,
349
+ [Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Right
350
+ )
351
+
352
+ if ([string]::IsNullOrWhiteSpace($Left) -or [string]::IsNullOrWhiteSpace($Right)) { return $false }
353
+ $a = ($Left.Trim() -replace '\\', '/').TrimEnd('/')
354
+ $b = ($Right.Trim() -replace '\\', '/').TrimEnd('/')
355
+ $isDrivePath = ($a -match '^[A-Za-z]:') -or ($b -match '^[A-Za-z]:')
356
+ $comparison = if ($isDrivePath) { [System.StringComparison]::OrdinalIgnoreCase } else { [System.StringComparison]::Ordinal }
357
+ return [string]::Equals($a, $b, $comparison)
358
+ }
359
+
360
+ # Private: compare two pull request numbers as integers.
361
+ function Test-WorktreeRunPrNumberEqual {
362
+ [CmdletBinding()]
363
+ [OutputType([bool])]
364
+ param(
365
+ [Parameter(Mandatory = $true)] [AllowNull()] [object] $Recorded,
366
+ [Parameter(Mandatory = $true)] [long] $Expected
367
+ )
368
+
369
+ if ($null -eq $Recorded) { return $false }
370
+ $number = [long] 0
371
+ if (-not [long]::TryParse(([string] $Recorded).Trim(), [ref] $number)) { return $false }
372
+ return ($number -eq $Expected)
373
+ }
374
+
375
+ # Private: whether a parsed run checkpoint records a pull request number or worktree path.
376
+ function Test-WorktreeRunCheckpointRecord {
377
+ [CmdletBinding()]
378
+ [OutputType([bool])]
379
+ param(
380
+ [Parameter(Mandatory = $true)] [pscustomobject] $Checkpoint,
381
+ [Parameter(Mandatory = $true)] [ValidateSet('epic', 'parallel')] [string] $Kind,
382
+ [Parameter(Mandatory = $true)] [ValidateSet('pr_number', 'worktree_path')] [string] $RecordField,
383
+ [Parameter(Mandatory = $true)] [string] $Value
384
+ )
385
+
386
+ $values = [System.Collections.Generic.List[object]]::new()
387
+ $arrayName = if ($Kind -eq 'epic') { 'features' } else { 'items' }
388
+ foreach ($record in @(Get-WorktreeRunPropertyValue -InputObject $Checkpoint -Name $arrayName)) {
389
+ $values.Add((Get-WorktreeRunPropertyValue -InputObject $record -Name $RecordField))
390
+ }
391
+ if ($Kind -eq 'epic' -and $RecordField -eq 'pr_number') {
392
+ $mergePr = Get-WorktreeRunPropertyValue -InputObject $Checkpoint -Name 'epic_merge_pr'
393
+ $values.Add((Get-WorktreeRunPropertyValue -InputObject $mergePr -Name 'pr_number'))
394
+ }
395
+
396
+ foreach ($recorded in $values) {
397
+ if ($RecordField -eq 'pr_number') {
398
+ if (Test-WorktreeRunPrNumberEqual -Recorded $recorded -Expected ([long] $Value)) { return $true }
399
+ }
400
+ elseif (Test-WorktreeRunPathEqual -Left ([string] $recorded) -Right $Value) { return $true }
401
+ }
402
+ return $false
403
+ }
404
+
405
+ function Resolve-WorktreeRunTargetByRecord {
406
+ <#
407
+ .SYNOPSIS
408
+ Resolve the live worktree whose run checkpoint records a pull request number or worktree path.
409
+ .DESCRIPTION
410
+ Epic checkpoints are searched in epic_merge_pr.pr_number and features[].pr_number, or
411
+ features[].worktree_path; parallel checkpoints in items[].pr_number or
412
+ items[].worktree_path. Zero matches resolve NoTarget, one resolves, and several are
413
+ Ambiguous. A blank value, or a pr_number that is not all digits or does not fit a 64-bit integer, is NoTarget.
414
+ .PARAMETER Kind
415
+ epic or parallel.
416
+ .PARAMETER RecordField
417
+ pr_number or worktree_path.
418
+ .PARAMETER Value
419
+ The pull request number or worktree path the command names.
420
+ .PARAMETER SessionRoot
421
+ The calling process's path, used only to find the repository and label a result.
422
+ #>
423
+ [CmdletBinding()]
424
+ [OutputType([pscustomobject])]
425
+ param(
426
+ [Parameter(Mandatory = $true)] [ValidateSet('epic', 'parallel')] [string] $Kind,
427
+ [Parameter(Mandatory = $true)] [ValidateSet('pr_number', 'worktree_path')] [string] $RecordField,
428
+ [AllowNull()] [AllowEmptyString()] [string] $Value,
429
+ [Parameter(Mandatory = $true)] [string] $SessionRoot
430
+ )
431
+
432
+ $description = "{0} '{1}' in the {2} checkpoint" -f $RecordField, $Value, $Kind
433
+ $isBlank = [string]::IsNullOrWhiteSpace($Value)
434
+ $parsedNumber = [long] 0
435
+ $isNumber = -not $isBlank -and $Value.Trim() -match '^\d+$' -and [long]::TryParse($Value.Trim(), [ref] $parsedNumber)
436
+ if ($isBlank -or ($RecordField -eq 'pr_number' -and -not $isNumber)) {
437
+ return (New-WorktreeResolutionTargetResult -Status 'NoTarget' -SessionRoot $SessionRoot -Detail (
438
+ 'the command names no usable {0}, so the {1} run it belongs to cannot be identified' -f $RecordField, $Kind))
439
+ }
440
+ $needle = $Value.Trim()
441
+ $matched = [System.Collections.Generic.List[string]]::new()
442
+ foreach ($root in (Get-WorktreeRunLiveRoot -SessionRoot $SessionRoot)) {
443
+ $checkpoint = Get-WorktreeRunCheckpoint -Kind $Kind -WorktreeRoot $root
444
+ if ($null -eq $checkpoint) { continue }
445
+ if (Test-WorktreeRunCheckpointRecord -Checkpoint $checkpoint -Kind $Kind -RecordField $RecordField -Value $needle) { $matched.Add($root) }
446
+ }
447
+ return (Resolve-WorktreeRunMatch -Matched $matched.ToArray() -SessionRoot $SessionRoot -Description $description)
448
+ }
449
+
450
+ function Resolve-WorktreeOperandTarget {
451
+ <#
452
+ .SYNOPSIS
453
+ Place a file_path or git -C operand in the worktree that contains it.
454
+ .DESCRIPTION
455
+ Ascends from the operand to its worktree root. A blank operand, and an operand
456
+ inside no worktree, resolve to the session worktree; a relative operand is joined
457
+ to the session root first.
458
+ .PARAMETER Path
459
+ The operand path, absolute or relative, or blank.
460
+ .PARAMETER SessionRoot
461
+ The calling process's path.
462
+ #>
463
+ [CmdletBinding()]
464
+ [OutputType([pscustomobject])]
465
+ param(
466
+ [AllowNull()] [AllowEmptyString()] [string] $Path,
467
+ [Parameter(Mandatory = $true)] [string] $SessionRoot
468
+ )
469
+
470
+ $sessionWorktree = Find-WorktreeResolutionRoot -Path $SessionRoot
471
+ if ($null -eq $sessionWorktree) { $sessionWorktree = ConvertTo-WorktreeResolutionNormalizedPath -Path $SessionRoot }
472
+ if ([string]::IsNullOrWhiteSpace($Path)) {
473
+ return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $sessionWorktree -SessionRoot $SessionRoot -Detail (
474
+ 'the call names no operand, so it acts on the session worktree'))
475
+ }
476
+
477
+ $operand = ConvertTo-WorktreeResolutionNormalizedPath -Path $Path
478
+ if ($operand -notmatch $script:AbsolutePathPattern) {
479
+ $operand = Join-WorktreeResolutionPath -WorktreeRoot $SessionRoot -RepoRelativePath $operand
480
+ }
481
+ $found = Find-WorktreeResolutionRoot -Path $operand
482
+ if ($null -eq $found) {
483
+ return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $sessionWorktree -SessionRoot $SessionRoot -Detail (
484
+ "the operand '{0}' lies in no worktree, so the call is evaluated against the session worktree" -f $operand))
485
+ }
486
+ return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $found -SessionRoot $SessionRoot -Detail (
487
+ "the operand '{0}' lies in the worktree '{1}'" -f $operand, $found))
488
+ }
489
+
490
+ Export-ModuleMember -Function `
491
+ Find-WorktreeRunIdentitySignal, `
492
+ Get-WorktreeRunCheckpointText, `
493
+ Get-WorktreeRunCheckpointPath, `
494
+ Resolve-WorktreeEpicTarget, `
495
+ Resolve-WorktreeParallelTarget, `
496
+ Resolve-WorktreeRunTargetByRecord, `
497
+ Resolve-WorktreeOperandTarget