@danmoisan/drm-copilot-mcp 1.1.11 → 1.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/out/mcp-server.js +366 -159
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +33 -37
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +12 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
- package/resources/claude-customizations/pack-manifests/core.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +18 -1
- package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +26 -0
package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1
ADDED
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Worktree locator and path normaliser for target-worktree resolution (issue #669).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Locates the worktree that contains a path by an upward walk for a .git entry,
|
|
7
|
+
enumerates the worktrees reachable from a session root through the repository
|
|
8
|
+
administrative directory, and converts paths between absolute and repo-relative
|
|
9
|
+
form without truncating them. It also publishes the single ambiguity reason
|
|
10
|
+
code that downstream gates concatenate into a deny reason.
|
|
11
|
+
|
|
12
|
+
A level is a worktree root when its .git child is a directory (the main
|
|
13
|
+
checkout) or a file whose first line is a gitdir: line (a linked worktree). A
|
|
14
|
+
linked worktree may live outside the main checkout, so containment is never
|
|
15
|
+
decided by comparing a path against one known root.
|
|
16
|
+
|
|
17
|
+
.NOTES
|
|
18
|
+
Compatible with PowerShell 7+. No external module dependencies, no subprocess,
|
|
19
|
+
no network, no clock read, and no environment read. All filesystem contact
|
|
20
|
+
passes through three exported seams so tests model any worktree topology
|
|
21
|
+
without writing a temporary file. Mirrored byte-identically under
|
|
22
|
+
extensions/drm-copilot/resources/claude-customizations/.
|
|
23
|
+
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
24
|
+
#>
|
|
25
|
+
|
|
26
|
+
Set-StrictMode -Version Latest
|
|
27
|
+
$ErrorActionPreference = 'Stop'
|
|
28
|
+
|
|
29
|
+
# The name of the entry that marks a worktree root. Only this one name is probed,
|
|
30
|
+
# because the repository layout never uses a separate administrative directory.
|
|
31
|
+
$script:GitEntryName = '.git'
|
|
32
|
+
|
|
33
|
+
# The first-line prefix of a linked worktree's .git file, and the pattern that
|
|
34
|
+
# captures its target. Anything else in a .git file is not a root marker.
|
|
35
|
+
$script:GitDirPrefix = 'gitdir:'
|
|
36
|
+
$script:GitDirLinePattern = '^' + [regex]::Escape($script:GitDirPrefix) + '\s*(.+)$'
|
|
37
|
+
|
|
38
|
+
# The per-worktree administrative files read during enumeration. commondir leads
|
|
39
|
+
# from a linked worktree's administrative directory back to the main .git
|
|
40
|
+
# directory; worktrees holds one administrative directory per linked worktree.
|
|
41
|
+
$script:CommonDirFileName = 'commondir'
|
|
42
|
+
$script:WorktreeAdminDirectoryName = 'worktrees'
|
|
43
|
+
$script:WorktreeGitDirFileName = 'gitdir'
|
|
44
|
+
$script:HeadFileName = 'HEAD'
|
|
45
|
+
|
|
46
|
+
# The only HEAD form that names a branch. A detached HEAD names no branch, so it
|
|
47
|
+
# never matches a branch filter.
|
|
48
|
+
$script:BranchReferencePrefix = 'ref: refs/heads/'
|
|
49
|
+
|
|
50
|
+
# Upper bound on the upward walk, so a malformed input cannot loop.
|
|
51
|
+
$script:MaximumAscentDepth = 64
|
|
52
|
+
|
|
53
|
+
# The single cause code emitted when the correct target cannot be identified. It
|
|
54
|
+
# carries no _BLOCKED suffix because this module owns no gate.
|
|
55
|
+
$script:AmbiguityReasonCode = 'TARGET_WORKTREE_AMBIGUOUS'
|
|
56
|
+
# The no-target reason code (issue #687). Without it a NoTarget result carried nothing a gate
|
|
57
|
+
# could emit, leaving only wrong options: substitute the session root, which in a parallel or
|
|
58
|
+
# epic topology is a sibling item's state, or deny without naming why.
|
|
59
|
+
$script:NoTargetReasonCode = 'TARGET_WORKTREE_NOT_DERIVABLE'
|
|
60
|
+
|
|
61
|
+
function Get-WorktreeResolutionGitEntryKind {
|
|
62
|
+
<#
|
|
63
|
+
.SYNOPSIS
|
|
64
|
+
Classify a path as Directory, File, or None. The module's only existence probe (seam).
|
|
65
|
+
.PARAMETER Path
|
|
66
|
+
The absolute path to classify.
|
|
67
|
+
#>
|
|
68
|
+
[CmdletBinding()]
|
|
69
|
+
[OutputType([string])]
|
|
70
|
+
param(
|
|
71
|
+
[Parameter(Mandatory = $true)]
|
|
72
|
+
[string] $Path
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
if (Test-Path -LiteralPath $Path -PathType Container) {
|
|
76
|
+
return 'Directory'
|
|
77
|
+
}
|
|
78
|
+
if (Test-Path -LiteralPath $Path -PathType Leaf) {
|
|
79
|
+
return 'File'
|
|
80
|
+
}
|
|
81
|
+
return 'None'
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function Get-WorktreeResolutionGitFileText {
|
|
85
|
+
<#
|
|
86
|
+
.SYNOPSIS
|
|
87
|
+
Read a file's raw text, or $null when unreadable. The module's only content read (seam).
|
|
88
|
+
.PARAMETER Path
|
|
89
|
+
The absolute path of the file to read.
|
|
90
|
+
#>
|
|
91
|
+
[CmdletBinding()]
|
|
92
|
+
[OutputType([string])]
|
|
93
|
+
param(
|
|
94
|
+
[Parameter(Mandatory = $true)]
|
|
95
|
+
[string] $Path
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return (Get-Content -LiteralPath $Path -Raw -ErrorAction SilentlyContinue)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function Get-WorktreeResolutionDirectoryChildName {
|
|
102
|
+
<#
|
|
103
|
+
.SYNOPSIS
|
|
104
|
+
List child directory names as an array (empty when absent). The module's only directory enumeration (seam).
|
|
105
|
+
.PARAMETER Path
|
|
106
|
+
The absolute path of the directory to list.
|
|
107
|
+
#>
|
|
108
|
+
[CmdletBinding()]
|
|
109
|
+
[OutputType([string[]], [object[]])]
|
|
110
|
+
param(
|
|
111
|
+
[Parameter(Mandatory = $true)]
|
|
112
|
+
[string] $Path
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
return , [string[]] @(Get-ChildItem -LiteralPath $Path -Directory -Name -ErrorAction SilentlyContinue)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ConvertTo-WorktreeResolutionNormalizedPath {
|
|
119
|
+
<#
|
|
120
|
+
.SYNOPSIS
|
|
121
|
+
Normalise a path to forward slashes, no repeated separator, no trailing slash, no leading ./.
|
|
122
|
+
.DESCRIPTION
|
|
123
|
+
Pure. Returns $null for a null, empty, or whitespace input. A UNC share prefix
|
|
124
|
+
and the filesystem root / are preserved.
|
|
125
|
+
.PARAMETER Path
|
|
126
|
+
The path to normalise, in either separator style.
|
|
127
|
+
#>
|
|
128
|
+
[CmdletBinding()]
|
|
129
|
+
[OutputType([string])]
|
|
130
|
+
param(
|
|
131
|
+
[Parameter(Mandatory = $true)]
|
|
132
|
+
[AllowNull()]
|
|
133
|
+
[AllowEmptyString()]
|
|
134
|
+
[string] $Path
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
if ([string]::IsNullOrWhiteSpace($Path)) {
|
|
138
|
+
return $null
|
|
139
|
+
}
|
|
140
|
+
$normalized = $Path.Trim() -replace '\\', '/'
|
|
141
|
+
$uncPrefix = if ($normalized.StartsWith('//')) { '/' } else { '' }
|
|
142
|
+
$normalized = $uncPrefix + ($normalized -replace '/{2,}', '/')
|
|
143
|
+
while ($normalized.StartsWith('./')) {
|
|
144
|
+
$normalized = $normalized.Substring(2)
|
|
145
|
+
}
|
|
146
|
+
if ($normalized.Length -gt 1) {
|
|
147
|
+
$normalized = $normalized.TrimEnd('/')
|
|
148
|
+
}
|
|
149
|
+
if ($normalized.Length -eq 0) {
|
|
150
|
+
return $null
|
|
151
|
+
}
|
|
152
|
+
return $normalized
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function Test-WorktreeResolutionAbsolutePath {
|
|
156
|
+
# Pure: true for a drive-rooted (C:/...) or slash-rooted normalised path.
|
|
157
|
+
[OutputType([bool])]
|
|
158
|
+
param([string] $Path)
|
|
159
|
+
|
|
160
|
+
return ($Path -match '^([A-Za-z]:(/|$)|/)')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function Join-WorktreeResolutionSegment {
|
|
164
|
+
# Pure: append a child to a normalised base without doubling the separator.
|
|
165
|
+
[OutputType([string])]
|
|
166
|
+
param([string] $BasePath, [string] $ChildPath)
|
|
167
|
+
|
|
168
|
+
$separator = if ($BasePath.EndsWith('/')) { '' } else { '/' }
|
|
169
|
+
return $BasePath + $separator + $ChildPath
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function Get-WorktreeResolutionParentPath {
|
|
173
|
+
# Pure: the parent of a normalised absolute path, or $null at the filesystem or
|
|
174
|
+
# drive root. The parent of C:/a is C:, C: has no parent, and the parent of /a is /.
|
|
175
|
+
[OutputType([string])]
|
|
176
|
+
param([string] $Path)
|
|
177
|
+
|
|
178
|
+
$index = $Path.LastIndexOf('/')
|
|
179
|
+
if ($index -lt 0 -or $Path -eq '/') {
|
|
180
|
+
return $null
|
|
181
|
+
}
|
|
182
|
+
return $Path.Substring(0, [math]::Max($index, 1))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function Resolve-WorktreeResolutionPathAgainst {
|
|
186
|
+
# Pure: resolve a path relative to a normalised absolute base, collapsing . and
|
|
187
|
+
# .. segments. An absolute path is normalised and collapsed without the base.
|
|
188
|
+
[OutputType([string])]
|
|
189
|
+
param([string] $BasePath, [string] $Path)
|
|
190
|
+
|
|
191
|
+
$normalized = ConvertTo-WorktreeResolutionNormalizedPath -Path $Path
|
|
192
|
+
if ($null -eq $normalized) {
|
|
193
|
+
return $null
|
|
194
|
+
}
|
|
195
|
+
if (-not (Test-WorktreeResolutionAbsolutePath -Path $normalized)) {
|
|
196
|
+
$normalized = Join-WorktreeResolutionSegment -BasePath $BasePath -ChildPath $normalized
|
|
197
|
+
}
|
|
198
|
+
$kept = [System.Collections.Generic.List[string]]::new()
|
|
199
|
+
foreach ($segment in $normalized.Split('/')) {
|
|
200
|
+
if ($segment -eq '..') {
|
|
201
|
+
if ($kept.Count -gt 1) {
|
|
202
|
+
$kept.RemoveAt($kept.Count - 1)
|
|
203
|
+
}
|
|
204
|
+
} elseif ($segment -ne '.') {
|
|
205
|
+
$kept.Add($segment)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
$joined = $kept -join '/'
|
|
209
|
+
if ($joined.Length -eq 0) {
|
|
210
|
+
return '/'
|
|
211
|
+
}
|
|
212
|
+
return $joined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function Get-WorktreeResolutionGitDirTarget {
|
|
216
|
+
# Pure: the target named by the first line of .git file text when that line is a
|
|
217
|
+
# well-formed gitdir: line, otherwise $null.
|
|
218
|
+
[OutputType([string])]
|
|
219
|
+
param([AllowNull()][string] $Text)
|
|
220
|
+
|
|
221
|
+
$firstLine = Get-WorktreeResolutionFirstLine -Text $Text
|
|
222
|
+
if ($null -ne $firstLine -and $firstLine -match $script:GitDirLinePattern) {
|
|
223
|
+
return $Matches[1].Trim()
|
|
224
|
+
}
|
|
225
|
+
return $null
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function Test-WorktreeResolutionRootMarker {
|
|
229
|
+
<#
|
|
230
|
+
.SYNOPSIS
|
|
231
|
+
Test whether a directory level is a worktree root.
|
|
232
|
+
.DESCRIPTION
|
|
233
|
+
True when the level's .git child is a directory, or a file whose first line is
|
|
234
|
+
a well-formed gitdir: line. Reaches the filesystem only through the seams.
|
|
235
|
+
.PARAMETER Path
|
|
236
|
+
The absolute directory level to test.
|
|
237
|
+
#>
|
|
238
|
+
[CmdletBinding()]
|
|
239
|
+
[OutputType([bool])]
|
|
240
|
+
param(
|
|
241
|
+
[Parameter(Mandatory = $true)]
|
|
242
|
+
[string] $Path
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
$level = ConvertTo-WorktreeResolutionNormalizedPath -Path $Path
|
|
246
|
+
if ($null -eq $level) {
|
|
247
|
+
return $false
|
|
248
|
+
}
|
|
249
|
+
$gitEntry = Join-WorktreeResolutionSegment -BasePath $level -ChildPath $script:GitEntryName
|
|
250
|
+
$kind = Get-WorktreeResolutionGitEntryKind -Path $gitEntry
|
|
251
|
+
if ($kind -ne 'File') {
|
|
252
|
+
return ($kind -eq 'Directory')
|
|
253
|
+
}
|
|
254
|
+
$target = Get-WorktreeResolutionGitDirTarget -Text (Get-WorktreeResolutionGitFileText -Path $gitEntry)
|
|
255
|
+
return ($null -ne $target)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function Find-WorktreeResolutionRoot {
|
|
259
|
+
<#
|
|
260
|
+
.SYNOPSIS
|
|
261
|
+
Ascend from a path to the first level that is a worktree root.
|
|
262
|
+
.DESCRIPTION
|
|
263
|
+
Returns the normalised root, or $null when the input is not absolute (it would
|
|
264
|
+
depend on the current directory), when the ascent reaches the filesystem or
|
|
265
|
+
drive root without a marker, or when MaximumDepth levels have been tested.
|
|
266
|
+
.PARAMETER Path
|
|
267
|
+
The absolute path to start from; the path itself is level 0.
|
|
268
|
+
.PARAMETER MaximumDepth
|
|
269
|
+
The highest level above the input that is tested.
|
|
270
|
+
#>
|
|
271
|
+
[CmdletBinding()]
|
|
272
|
+
[OutputType([string])]
|
|
273
|
+
param(
|
|
274
|
+
[Parameter(Mandatory = $true)]
|
|
275
|
+
[AllowEmptyString()]
|
|
276
|
+
[string] $Path,
|
|
277
|
+
|
|
278
|
+
[ValidateRange(0, 4096)]
|
|
279
|
+
[int] $MaximumDepth = $script:MaximumAscentDepth
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
$current = ConvertTo-WorktreeResolutionNormalizedPath -Path $Path
|
|
283
|
+
if ($null -eq $current -or -not (Test-WorktreeResolutionAbsolutePath -Path $current)) {
|
|
284
|
+
return $null
|
|
285
|
+
}
|
|
286
|
+
for ($depth = 0; ($null -ne $current) -and ($depth -le $MaximumDepth); $depth++) {
|
|
287
|
+
if (Test-WorktreeResolutionRootMarker -Path $current) {
|
|
288
|
+
return $current
|
|
289
|
+
}
|
|
290
|
+
$current = Get-WorktreeResolutionParentPath -Path $current
|
|
291
|
+
}
|
|
292
|
+
return $null
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function Get-WorktreeResolutionFirstLine {
|
|
296
|
+
# Pure: the trimmed first line of a text, or $null when there is none.
|
|
297
|
+
[OutputType([string])]
|
|
298
|
+
param([AllowNull()][string] $Text)
|
|
299
|
+
|
|
300
|
+
if ([string]::IsNullOrWhiteSpace($Text)) {
|
|
301
|
+
return $null
|
|
302
|
+
}
|
|
303
|
+
return ($Text -split "`r?`n", 2)[0].Trim()
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function Get-WorktreeResolutionCommonGitDirectory {
|
|
307
|
+
# The main .git directory reachable from a worktree root, or $null. A .git
|
|
308
|
+
# directory is the main checkout's own. A .git file's gitdir: target is the
|
|
309
|
+
# per-worktree admin directory, whose commondir file leads back to main .git.
|
|
310
|
+
[OutputType([string])]
|
|
311
|
+
param([string] $WorktreeRoot)
|
|
312
|
+
|
|
313
|
+
$gitEntry = Join-WorktreeResolutionSegment -BasePath $WorktreeRoot -ChildPath $script:GitEntryName
|
|
314
|
+
$kind = Get-WorktreeResolutionGitEntryKind -Path $gitEntry
|
|
315
|
+
if ($kind -eq 'Directory') {
|
|
316
|
+
return $gitEntry
|
|
317
|
+
}
|
|
318
|
+
if ($kind -ne 'File') {
|
|
319
|
+
return $null
|
|
320
|
+
}
|
|
321
|
+
$adminTarget = Get-WorktreeResolutionGitDirTarget -Text (Get-WorktreeResolutionGitFileText -Path $gitEntry)
|
|
322
|
+
if ($null -eq $adminTarget) {
|
|
323
|
+
return $null
|
|
324
|
+
}
|
|
325
|
+
$adminDirectory = Resolve-WorktreeResolutionPathAgainst -BasePath $WorktreeRoot -Path $adminTarget
|
|
326
|
+
$commonDirPath = Join-WorktreeResolutionSegment -BasePath $adminDirectory -ChildPath $script:CommonDirFileName
|
|
327
|
+
$commonTarget = Get-WorktreeResolutionFirstLine -Text (Get-WorktreeResolutionGitFileText -Path $commonDirPath)
|
|
328
|
+
if ($null -eq $commonTarget) {
|
|
329
|
+
return $null
|
|
330
|
+
}
|
|
331
|
+
return (Resolve-WorktreeResolutionPathAgainst -BasePath $adminDirectory -Path $commonTarget)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function Get-WorktreeResolutionWorktreeRoot {
|
|
335
|
+
<#
|
|
336
|
+
.SYNOPSIS
|
|
337
|
+
Enumerate the candidate worktree roots reachable from a session root.
|
|
338
|
+
.DESCRIPTION
|
|
339
|
+
Reads the administrative layout through the seams and starts no process. The
|
|
340
|
+
main checkout is always a candidate. Each worktrees/<name>/gitdir file names a
|
|
341
|
+
linked worktree's own .git file, whose parent directory is that worktree's
|
|
342
|
+
root and may lie outside the main checkout. Always returns an array.
|
|
343
|
+
.PARAMETER SessionRoot
|
|
344
|
+
The worktree root the invoking process runs in.
|
|
345
|
+
.PARAMETER Branch
|
|
346
|
+
Optional. Keep only candidates whose HEAD names this branch.
|
|
347
|
+
.PARAMETER RepoRelativePath
|
|
348
|
+
Optional. Keep only candidates under which this repo-relative path exists.
|
|
349
|
+
#>
|
|
350
|
+
[CmdletBinding()]
|
|
351
|
+
[OutputType([string[]], [object[]])]
|
|
352
|
+
param(
|
|
353
|
+
[Parameter(Mandatory = $true)]
|
|
354
|
+
[AllowEmptyString()]
|
|
355
|
+
[string] $SessionRoot,
|
|
356
|
+
[string] $Branch,
|
|
357
|
+
[string] $RepoRelativePath
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
$sessionLevel = ConvertTo-WorktreeResolutionNormalizedPath -Path $SessionRoot
|
|
361
|
+
$commonGitDirectory = if ($null -ne $sessionLevel) { Get-WorktreeResolutionCommonGitDirectory -WorktreeRoot $sessionLevel }
|
|
362
|
+
if ($null -eq $commonGitDirectory) {
|
|
363
|
+
return , [string[]] @()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# Deduplication below is case-insensitive, matching the Windows filesystem.
|
|
367
|
+
$entries = [System.Collections.Generic.List[hashtable]]::new()
|
|
368
|
+
if ($commonGitDirectory.EndsWith('/' + $script:GitEntryName)) {
|
|
369
|
+
$mainHead = Join-WorktreeResolutionSegment -BasePath $commonGitDirectory -ChildPath $script:HeadFileName
|
|
370
|
+
$entries.Add(@{ Root = (Get-WorktreeResolutionParentPath -Path $commonGitDirectory); Head = $mainHead })
|
|
371
|
+
}
|
|
372
|
+
$adminRoot = Join-WorktreeResolutionSegment -BasePath $commonGitDirectory -ChildPath $script:WorktreeAdminDirectoryName
|
|
373
|
+
# Assigned before enumeration: the seam emits its array as one object.
|
|
374
|
+
$adminNames = Get-WorktreeResolutionDirectoryChildName -Path $adminRoot
|
|
375
|
+
foreach ($name in @($adminNames)) {
|
|
376
|
+
$adminDirectory = Join-WorktreeResolutionSegment -BasePath $adminRoot -ChildPath $name
|
|
377
|
+
$gitDirFile = Join-WorktreeResolutionSegment -BasePath $adminDirectory -ChildPath $script:WorktreeGitDirFileName
|
|
378
|
+
$gitFileTarget = Get-WorktreeResolutionFirstLine -Text (Get-WorktreeResolutionGitFileText -Path $gitDirFile)
|
|
379
|
+
if ($null -eq $gitFileTarget) {
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
$gitFilePath = Resolve-WorktreeResolutionPathAgainst -BasePath $adminDirectory -Path $gitFileTarget
|
|
383
|
+
$linkedHead = Join-WorktreeResolutionSegment -BasePath $adminDirectory -ChildPath $script:HeadFileName
|
|
384
|
+
$entries.Add(@{ Root = (Get-WorktreeResolutionParentPath -Path $gitFilePath); Head = $linkedHead })
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
$relative = ConvertTo-WorktreeResolutionNormalizedPath -Path $RepoRelativePath
|
|
388
|
+
$wantedHead = if ([string]::IsNullOrWhiteSpace($Branch)) { $null } else { $script:BranchReferencePrefix + $Branch.Trim() }
|
|
389
|
+
$seen = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
|
|
390
|
+
$roots = [System.Collections.Generic.List[string]]::new()
|
|
391
|
+
foreach ($entry in $entries) {
|
|
392
|
+
if ($null -eq $entry.Root -or -not $seen.Add($entry.Root)) {
|
|
393
|
+
continue
|
|
394
|
+
}
|
|
395
|
+
if ($null -ne $wantedHead -and (Get-WorktreeResolutionFirstLine -Text (Get-WorktreeResolutionGitFileText -Path $entry.Head)) -cne $wantedHead) {
|
|
396
|
+
continue
|
|
397
|
+
}
|
|
398
|
+
if ($null -ne $relative -and (Get-WorktreeResolutionGitEntryKind -Path (Join-WorktreeResolutionSegment -BasePath $entry.Root -ChildPath $relative)) -eq 'None') {
|
|
399
|
+
continue
|
|
400
|
+
}
|
|
401
|
+
$roots.Add($entry.Root)
|
|
402
|
+
}
|
|
403
|
+
return , [string[]] $roots.ToArray()
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function ConvertTo-WorktreeResolutionNormalizationResult {
|
|
407
|
+
# Pure: the single constructor of the normalisation result. ReasonCode is the
|
|
408
|
+
# ambiguity literal exactly when the path was not normalised.
|
|
409
|
+
[OutputType([pscustomobject])]
|
|
410
|
+
param([string] $RepoRelativePath, [string] $WorktreeRoot, [bool] $IsNormalized, [string] $Detail)
|
|
411
|
+
|
|
412
|
+
return [pscustomobject]@{
|
|
413
|
+
IsNormalized = $IsNormalized
|
|
414
|
+
RepoRelativePath = if ($IsNormalized) { $RepoRelativePath } else { $null }
|
|
415
|
+
WorktreeRoot = if ($IsNormalized) { $WorktreeRoot } else { $null }
|
|
416
|
+
ReasonCode = if ($IsNormalized) { $null } else { $script:AmbiguityReasonCode }
|
|
417
|
+
Detail = $Detail
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function ConvertTo-WorktreeResolutionRepoRelativePath {
|
|
422
|
+
<#
|
|
423
|
+
.SYNOPSIS
|
|
424
|
+
Convert a path to its repo-relative form by locating its containing worktree.
|
|
425
|
+
.DESCRIPTION
|
|
426
|
+
Always returns an object. An absolute path is placed by the upward walk and
|
|
427
|
+
keeps its full remainder below the located root; no segment is dropped. A
|
|
428
|
+
relative path is normalised only against an explicit WorktreeRoot, and is
|
|
429
|
+
otherwise refused (Ruling A) because it would depend on the current directory.
|
|
430
|
+
.PARAMETER Path
|
|
431
|
+
The path to convert, relative or absolute, in either separator style.
|
|
432
|
+
.PARAMETER WorktreeRoot
|
|
433
|
+
Optional. The worktree root a relative path is normalised against.
|
|
434
|
+
#>
|
|
435
|
+
[CmdletBinding()]
|
|
436
|
+
[OutputType([pscustomobject])]
|
|
437
|
+
param(
|
|
438
|
+
[Parameter(Mandatory = $true)]
|
|
439
|
+
[AllowEmptyString()]
|
|
440
|
+
[string] $Path,
|
|
441
|
+
[string] $WorktreeRoot
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
$normalized = ConvertTo-WorktreeResolutionNormalizedPath -Path $Path
|
|
445
|
+
if ($null -eq $normalized) {
|
|
446
|
+
return (ConvertTo-WorktreeResolutionNormalizationResult -IsNormalized $false -Detail 'the path to normalise is empty, so no containing worktree can be located')
|
|
447
|
+
}
|
|
448
|
+
if (Test-WorktreeResolutionAbsolutePath -Path $normalized) {
|
|
449
|
+
$root = Find-WorktreeResolutionRoot -Path $normalized
|
|
450
|
+
if ($null -eq $root) {
|
|
451
|
+
return (ConvertTo-WorktreeResolutionNormalizationResult -IsNormalized $false -Detail "no worktree root was found on the upward walk from '$normalized'")
|
|
452
|
+
}
|
|
453
|
+
$remainder = $normalized.Substring($root.Length).TrimStart('/')
|
|
454
|
+
return (ConvertTo-WorktreeResolutionNormalizationResult -IsNormalized $true -WorktreeRoot $root -RepoRelativePath $remainder -Detail "path '$normalized' lies in worktree '$root'")
|
|
455
|
+
}
|
|
456
|
+
$suppliedRoot = ConvertTo-WorktreeResolutionNormalizedPath -Path $WorktreeRoot
|
|
457
|
+
if ($null -eq $suppliedRoot) {
|
|
458
|
+
return (ConvertTo-WorktreeResolutionNormalizationResult -IsNormalized $false -Detail "relative path '$normalized' was supplied without a worktree root, so its containing worktree cannot be identified")
|
|
459
|
+
}
|
|
460
|
+
return (ConvertTo-WorktreeResolutionNormalizationResult -IsNormalized $true -WorktreeRoot $suppliedRoot -RepoRelativePath $normalized -Detail "relative path '$normalized' was normalised against the supplied worktree '$suppliedRoot'")
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function Get-WorktreeResolutionAmbiguityReasonCode {
|
|
464
|
+
<#
|
|
465
|
+
.SYNOPSIS
|
|
466
|
+
Return the ambiguity reason code, TARGET_WORKTREE_AMBIGUOUS, so callers never restate it.
|
|
467
|
+
#>
|
|
468
|
+
[CmdletBinding()]
|
|
469
|
+
[OutputType([string])]
|
|
470
|
+
param()
|
|
471
|
+
|
|
472
|
+
return $script:AmbiguityReasonCode
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function Get-WorktreeResolutionNoTargetReasonCode {
|
|
476
|
+
<#
|
|
477
|
+
.SYNOPSIS
|
|
478
|
+
Return the no-target reason code, TARGET_WORKTREE_NOT_DERIVABLE, so callers never restate it.
|
|
479
|
+
.DESCRIPTION
|
|
480
|
+
Distinct from the ambiguity code: that means signals disagreed, this means the
|
|
481
|
+
payload carried no signal at all, and a gate reports a different remedy for each.
|
|
482
|
+
#>
|
|
483
|
+
[CmdletBinding()]
|
|
484
|
+
[OutputType([string])]
|
|
485
|
+
param()
|
|
486
|
+
|
|
487
|
+
return $script:NoTargetReasonCode
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
Export-ModuleMember -Function `
|
|
491
|
+
Get-WorktreeResolutionGitEntryKind, `
|
|
492
|
+
Get-WorktreeResolutionGitFileText, `
|
|
493
|
+
Get-WorktreeResolutionDirectoryChildName, `
|
|
494
|
+
ConvertTo-WorktreeResolutionNormalizedPath, `
|
|
495
|
+
Test-WorktreeResolutionRootMarker, `
|
|
496
|
+
Find-WorktreeResolutionRoot, `
|
|
497
|
+
Get-WorktreeResolutionWorktreeRoot, `
|
|
498
|
+
ConvertTo-WorktreeResolutionRepoRelativePath, `
|
|
499
|
+
Get-WorktreeResolutionAmbiguityReasonCode, `
|
|
500
|
+
Get-WorktreeResolutionNoTargetReasonCode
|