@danmoisan/drm-copilot-mcp 1.1.11 → 1.1.14
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 +368 -209
- package/package.json +2 -2
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +18 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
- 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 +172 -24
- 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/blast-radius/BlastRadius.psm1 +43 -6
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
- 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/rules/parallel-orchestration.md +150 -2
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
- 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 +15 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
- package/resources/claude-customizations/config/blast-radius.json +14 -1
- package/resources/claude-customizations/pack-manifests/core.json +11 -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-epic-resolution.ps1 +493 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +172 -24
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- 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 +20 -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 +37 -0
package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Portable-identity resolution of the item a gated call acts on (issue #673).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Selects the worktree a call pertains to from portable identity alone: the
|
|
7
|
+
canonical issue number in the call text, matched against the issue-num of each
|
|
8
|
+
live worktree's orchestrator checkpoint, and a branch signal. A feature-folder
|
|
9
|
+
path and a file path are never read as worktree selectors, because a merged
|
|
10
|
+
feature folder exists in every checkout branched from main and so places a call
|
|
11
|
+
in many worktrees at once; it names what the work is, not where it lives.
|
|
12
|
+
|
|
13
|
+
A worktree is live when it is registered in the repository's worktree
|
|
14
|
+
administrative layout and its root still carries a worktree marker, so a deleted
|
|
15
|
+
directory is excluded. No identity, or one matching no live worktree, resolves
|
|
16
|
+
NoTarget; several live matches or disagreeing identities resolve Ambiguous. A
|
|
17
|
+
branch signal is the only tie-breaker, because git checks a branch out in at most
|
|
18
|
+
one worktree; no timestamp or lifecycle step is ever read to break a tie.
|
|
19
|
+
|
|
20
|
+
.NOTES
|
|
21
|
+
PowerShell 7+. Re-uses its siblings for enumeration, the marker test, the ascent,
|
|
22
|
+
the branch reader, normalisation, the join, the result constructor, and both
|
|
23
|
+
reason-code accessors, re-implementing none. Neither reason-code literal appears
|
|
24
|
+
here. Its only filesystem read is the 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
|
+
|
|
34
|
+
# The one definition of the checkpoint location relative to a worktree root; every
|
|
35
|
+
# consumer composes an absolute path from it and a resolved root.
|
|
36
|
+
$script:CheckpointRelativePath = 'artifacts/orchestration/orchestrator-state.json'
|
|
37
|
+
|
|
38
|
+
# The canonical issue-number line an orchestrator writes into a delegation prompt.
|
|
39
|
+
# Case-sensitive on purpose: a lowercase near-miss is not the contract sentence.
|
|
40
|
+
$script:IssueSignalPattern = 'Canonical issue number for this feature is #?(?<number>\d+)'
|
|
41
|
+
|
|
42
|
+
# An issue number as a caller may spell it: optional hash, then digits only.
|
|
43
|
+
$script:IssueNumberPattern = '^#?(?<digits>\d+)$'
|
|
44
|
+
|
|
45
|
+
function Get-WorktreeItemCheckpointRelativePath {
|
|
46
|
+
<#
|
|
47
|
+
.SYNOPSIS
|
|
48
|
+
Return the repository-relative path of the orchestrator checkpoint.
|
|
49
|
+
.DESCRIPTION
|
|
50
|
+
The single definition of that literal; an absolute path comes from Get-WorktreeItemCheckpointPath.
|
|
51
|
+
#>
|
|
52
|
+
[CmdletBinding()]
|
|
53
|
+
[OutputType([string])]
|
|
54
|
+
param()
|
|
55
|
+
|
|
56
|
+
return $script:CheckpointRelativePath
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Get-WorktreeItemCheckpointPath {
|
|
60
|
+
<#
|
|
61
|
+
.SYNOPSIS
|
|
62
|
+
Compose the absolute checkpoint path under a worktree root.
|
|
63
|
+
.DESCRIPTION
|
|
64
|
+
Returns a forward-slash absolute path, and throws when the root is not absolute:
|
|
65
|
+
a relative root would reintroduce the process-directory binding this removes.
|
|
66
|
+
.PARAMETER WorktreeRoot
|
|
67
|
+
The absolute worktree root, in either separator style.
|
|
68
|
+
#>
|
|
69
|
+
[CmdletBinding()]
|
|
70
|
+
[OutputType([string])]
|
|
71
|
+
param(
|
|
72
|
+
[Parameter(Mandatory = $true)] [string] $WorktreeRoot
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
return (Join-WorktreeResolutionPath -WorktreeRoot $WorktreeRoot -RepoRelativePath (Get-WorktreeItemCheckpointRelativePath))
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ConvertTo-WorktreeItemIssueNumber {
|
|
79
|
+
<#
|
|
80
|
+
.SYNOPSIS
|
|
81
|
+
Normalise a value to a positive issue number as a digit string, or $null.
|
|
82
|
+
.DESCRIPTION
|
|
83
|
+
Pure. Accepts an integer, or a string of optional hash plus digits with
|
|
84
|
+
whitespace trimmed, and returns the number without leading zeros. Returns $null
|
|
85
|
+
for anything else, including zero, a negative value, none, and an empty value.
|
|
86
|
+
.PARAMETER Value
|
|
87
|
+
The candidate value, of any type.
|
|
88
|
+
#>
|
|
89
|
+
[CmdletBinding()]
|
|
90
|
+
[OutputType([string])]
|
|
91
|
+
param(
|
|
92
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [object] $Value
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
if ($null -eq $Value) { return $null }
|
|
96
|
+
$match = [regex]::Match(([string] $Value).Trim(), $script:IssueNumberPattern)
|
|
97
|
+
if (-not $match.Success) { return $null }
|
|
98
|
+
# A digit run wider than the largest integer is rejected rather than wrapped.
|
|
99
|
+
$number = [long] 0
|
|
100
|
+
if (-not [long]::TryParse($match.Groups['digits'].Value, [ref] $number)) { return $null }
|
|
101
|
+
if ($number -le 0) { return $null }
|
|
102
|
+
return $number.ToString([System.Globalization.CultureInfo]::InvariantCulture)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function Find-WorktreeItemIssueSignal {
|
|
106
|
+
<#
|
|
107
|
+
.SYNOPSIS
|
|
108
|
+
Return the distinct issue numbers named by canonical issue lines in a text.
|
|
109
|
+
.DESCRIPTION
|
|
110
|
+
Scans case-sensitively for the canonical issue-number sentence, returning each
|
|
111
|
+
distinct number in first-appearance order as one object a caller assigns first.
|
|
112
|
+
.PARAMETER Text
|
|
113
|
+
The prompt or command text to scan.
|
|
114
|
+
#>
|
|
115
|
+
[CmdletBinding()]
|
|
116
|
+
[OutputType([string[]], [object[]])]
|
|
117
|
+
param(
|
|
118
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Text
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
$found = [System.Collections.Generic.List[string]]::new()
|
|
122
|
+
# Collect each canonical line's number in first-appearance order, discarding a
|
|
123
|
+
# repeat. Order matters: the ambiguity detail lists them as the caller wrote them.
|
|
124
|
+
foreach ($match in [regex]::Matches([string] $Text, $script:IssueSignalPattern)) {
|
|
125
|
+
$number = ConvertTo-WorktreeItemIssueNumber -Value $match.Groups['number'].Value
|
|
126
|
+
if ($null -ne $number -and -not $found.Contains($number)) { $found.Add($number) }
|
|
127
|
+
}
|
|
128
|
+
return , [string[]] $found.ToArray()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function Get-WorktreeItemCheckpointText {
|
|
132
|
+
<#
|
|
133
|
+
.SYNOPSIS
|
|
134
|
+
Return the raw text of a file, or $null when it is absent or unreadable.
|
|
135
|
+
.DESCRIPTION
|
|
136
|
+
The module's only filesystem read, isolated as the seam a test mocks.
|
|
137
|
+
.PARAMETER Path
|
|
138
|
+
The absolute path to read.
|
|
139
|
+
#>
|
|
140
|
+
[CmdletBinding()]
|
|
141
|
+
[OutputType([string])]
|
|
142
|
+
param(
|
|
143
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Path
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
|
|
147
|
+
if (-not (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction SilentlyContinue)) { return $null }
|
|
148
|
+
try { return [System.IO.File]::ReadAllText($Path) } catch { return $null }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function Get-WorktreeItemCheckpointIssue {
|
|
152
|
+
<#
|
|
153
|
+
.SYNOPSIS
|
|
154
|
+
Return the issue number recorded by a worktree's checkpoint, or $null.
|
|
155
|
+
.DESCRIPTION
|
|
156
|
+
Reads only the canonical checkpoint path beneath the given root, so an archived
|
|
157
|
+
checkpoint is never matched. Returns $null when the file is absent, empty,
|
|
158
|
+
unparseable, not an object, or carries no issue-num key.
|
|
159
|
+
.PARAMETER WorktreeRoot
|
|
160
|
+
The absolute worktree root whose checkpoint is read.
|
|
161
|
+
#>
|
|
162
|
+
[CmdletBinding()]
|
|
163
|
+
[OutputType([string])]
|
|
164
|
+
param(
|
|
165
|
+
[Parameter(Mandatory = $true)] [string] $WorktreeRoot
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
$text = Get-WorktreeItemCheckpointText -Path (Get-WorktreeItemCheckpointPath -WorktreeRoot $WorktreeRoot)
|
|
169
|
+
if ([string]::IsNullOrWhiteSpace($text)) { return $null }
|
|
170
|
+
$payload = $null
|
|
171
|
+
try { $payload = $text | ConvertFrom-Json } catch { return $null }
|
|
172
|
+
# A parse that yielded nothing, one that yielded a scalar or array, and a missing
|
|
173
|
+
# key all mean the same to a caller: this worktree records no identifiable issue.
|
|
174
|
+
if ($null -eq $payload -or $payload -isnot [pscustomobject]) { return $null }
|
|
175
|
+
if ($payload.PSObject.Properties.Name -notcontains 'issue-num') { return $null }
|
|
176
|
+
return (ConvertTo-WorktreeItemIssueNumber -Value $payload.'issue-num')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function Get-WorktreeItemLiveRoot {
|
|
180
|
+
<#
|
|
181
|
+
.SYNOPSIS
|
|
182
|
+
Return the live worktree roots reachable from a session path.
|
|
183
|
+
.DESCRIPTION
|
|
184
|
+
Seam. Ascends from the session path to its enclosing worktree root, enumerates
|
|
185
|
+
the registered roots, and keeps those still carrying a worktree marker. Empty
|
|
186
|
+
when the path lies inside no worktree, and returned as one object.
|
|
187
|
+
.PARAMETER SessionRoot
|
|
188
|
+
The calling process's path, used only to find the repository.
|
|
189
|
+
.PARAMETER Branch
|
|
190
|
+
Optional. Keep only the roots that have this branch checked out.
|
|
191
|
+
#>
|
|
192
|
+
[CmdletBinding()]
|
|
193
|
+
[OutputType([string[]], [object[]])]
|
|
194
|
+
param(
|
|
195
|
+
[Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $SessionRoot,
|
|
196
|
+
[string] $Branch
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
$ascended = Find-WorktreeResolutionRoot -Path $SessionRoot
|
|
200
|
+
if ($null -eq $ascended) { return , [string[]] @() }
|
|
201
|
+
# The enumerator applies the branch filter, being the only code reading each HEAD.
|
|
202
|
+
$registered = if ([string]::IsNullOrWhiteSpace($Branch)) {
|
|
203
|
+
Get-WorktreeResolutionWorktreeRoot -SessionRoot $ascended
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
Get-WorktreeResolutionWorktreeRoot -SessionRoot $ascended -Branch $Branch
|
|
207
|
+
}
|
|
208
|
+
# Drop a registration whose directory is gone; it can hold no checkpoint to match.
|
|
209
|
+
$live = [System.Collections.Generic.List[string]]::new()
|
|
210
|
+
foreach ($root in @($registered)) {
|
|
211
|
+
if (Test-WorktreeResolutionRootMarker -Path $root) { $live.Add($root) }
|
|
212
|
+
}
|
|
213
|
+
return , [string[]] $live.ToArray()
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
# Private: convert a resolved root and session path into a result, labelling it
|
|
217
|
+
# SessionRoot or OtherWorktree so that test is written once, as the sibling normalisation
|
|
218
|
+
# constructor does. The ConvertTo verb is deliberate: a New- verb is state-changing to
|
|
219
|
+
# PSScriptAnalyzer and this function changes nothing.
|
|
220
|
+
function ConvertTo-WorktreeItemResolvedResult {
|
|
221
|
+
[CmdletBinding()]
|
|
222
|
+
[OutputType([pscustomobject])]
|
|
223
|
+
param(
|
|
224
|
+
[Parameter(Mandatory = $true)] [string] $WorktreeRoot,
|
|
225
|
+
[Parameter(Mandatory = $true)] [string] $SessionRoot,
|
|
226
|
+
[Parameter(Mandatory = $true)] [string] $Detail,
|
|
227
|
+
[string] $Branch
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
$root = ConvertTo-WorktreeResolutionNormalizedPath -Path $WorktreeRoot
|
|
231
|
+
# The root is the session's own when it equals the session path or the worktree that
|
|
232
|
+
# path sits inside. The ascent runs only on failure, because it reaches the disk.
|
|
233
|
+
$isSession = ($null -ne $root -and $root -eq (ConvertTo-WorktreeResolutionNormalizedPath -Path $SessionRoot))
|
|
234
|
+
if (-not $isSession) {
|
|
235
|
+
$ascended = Find-WorktreeResolutionRoot -Path $SessionRoot
|
|
236
|
+
$isSession = ($null -ne $ascended -and $root -eq $ascended)
|
|
237
|
+
}
|
|
238
|
+
$status = if ($isSession) { 'SessionRoot' } else { 'OtherWorktree' }
|
|
239
|
+
$signal = if ([string]::IsNullOrWhiteSpace($Branch)) { '' } else { 'Branch' }
|
|
240
|
+
return (New-WorktreeResolutionTargetResult -Status $status -SessionRoot $SessionRoot `
|
|
241
|
+
-WorktreeRoot $WorktreeRoot -Signal $signal -SignalValue $Branch -Candidate @($WorktreeRoot) -Detail $Detail)
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
# Private: decide the target when the call carries a branch signal. A branch is checked
|
|
245
|
+
# out in at most one worktree, so two live matches is repository state, not caller error.
|
|
246
|
+
function Resolve-WorktreeItemTargetByBranch {
|
|
247
|
+
[CmdletBinding()]
|
|
248
|
+
[OutputType([pscustomobject])]
|
|
249
|
+
param(
|
|
250
|
+
[Parameter(Mandatory = $true)] [string] $Session,
|
|
251
|
+
[Parameter(Mandatory = $true)] [string] $Branch,
|
|
252
|
+
[Parameter(Mandatory = $true)] [AllowEmptyCollection()] [string[]] $Issue
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Assigned before wrapping, because the seam returns its array as one object;
|
|
256
|
+
# wrapping the call would yield a one-element array holding the array.
|
|
257
|
+
$branchSignalRoots = Get-WorktreeItemLiveRoot -SessionRoot $Session -Branch $Branch
|
|
258
|
+
$branchRoots = @($branchSignalRoots)
|
|
259
|
+
# Zero live matches names no place to act on; more than one is a double checkout.
|
|
260
|
+
if ($branchRoots.Count -eq 0) {
|
|
261
|
+
return (New-WorktreeResolutionTargetResult -Status 'NoTarget' -SessionRoot $Session -Detail (
|
|
262
|
+
"branch '{0}' is checked out in no live worktree" -f $Branch))
|
|
263
|
+
}
|
|
264
|
+
if ($branchRoots.Count -gt 1) {
|
|
265
|
+
return (New-WorktreeResolutionTargetResult -Status 'Ambiguous' -SessionRoot $Session `
|
|
266
|
+
-Signal 'Branch' -SignalValue $Branch -Candidate $branchRoots -Detail (
|
|
267
|
+
"branch '{0}' is checked out in {1} live worktrees" -f $Branch, $branchRoots.Count))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
$target = $branchRoots[0]
|
|
271
|
+
$branchOnly = "branch '{0}' resolves to the live worktree that has it checked out" -f $Branch
|
|
272
|
+
if ($Issue.Count -eq 0) {
|
|
273
|
+
return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $target -SessionRoot $Session -Branch $Branch -Detail $branchOnly)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
$number = $Issue[0]
|
|
277
|
+
$recorded = Get-WorktreeItemCheckpointIssue -WorktreeRoot $target
|
|
278
|
+
# The branch worktree agrees with the issue, disagrees, or records none. Agreement
|
|
279
|
+
# breaks a stale-attempt tie; disagreement must not be guessed past.
|
|
280
|
+
if ($null -ne $recorded) {
|
|
281
|
+
if ($recorded -eq $number) {
|
|
282
|
+
return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $target -SessionRoot $Session -Branch $Branch -Detail (
|
|
283
|
+
"branch '{0}' and issue {1} resolve to the same live worktree" -f $Branch, $number))
|
|
284
|
+
}
|
|
285
|
+
return (New-WorktreeResolutionTargetResult -Status 'Ambiguous' -SessionRoot $Session `
|
|
286
|
+
-Signal 'Branch' -SignalValue $Branch -Candidate @($target) -Detail (
|
|
287
|
+
"branch '{0}' places the call in a worktree whose checkpoint records issue {1}, not issue {2}" -f $Branch, $recorded, $number))
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
# The branch worktree records no issue, so the number is checked against the other
|
|
291
|
+
# live worktrees; a match there means the two signals disagree about placement.
|
|
292
|
+
$elsewhere = [System.Collections.Generic.List[string]]::new()
|
|
293
|
+
$liveRoots = Get-WorktreeItemLiveRoot -SessionRoot $Session
|
|
294
|
+
foreach ($root in @($liveRoots)) {
|
|
295
|
+
if ($root -ne $target -and (Get-WorktreeItemCheckpointIssue -WorktreeRoot $root) -eq $number) { $elsewhere.Add($root) }
|
|
296
|
+
}
|
|
297
|
+
if ($elsewhere.Count -gt 0) {
|
|
298
|
+
return (New-WorktreeResolutionTargetResult -Status 'Ambiguous' -SessionRoot $Session `
|
|
299
|
+
-Signal 'Branch' -SignalValue $Branch -Candidate (@($target) + $elsewhere.ToArray()) -Detail (
|
|
300
|
+
"branch '{0}' and issue {1} place the call in different worktrees" -f $Branch, $number))
|
|
301
|
+
}
|
|
302
|
+
return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $target -SessionRoot $Session -Branch $Branch -Detail $branchOnly)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
# Private: decide the target when the call carries an issue number and no branch. Two
|
|
306
|
+
# live worktrees recording one issue is stale-attempt, and its detail names both remedies.
|
|
307
|
+
function Resolve-WorktreeItemTargetByIssue {
|
|
308
|
+
[CmdletBinding()]
|
|
309
|
+
[OutputType([pscustomobject])]
|
|
310
|
+
param(
|
|
311
|
+
[Parameter(Mandatory = $true)] [string] $Session,
|
|
312
|
+
[Parameter(Mandatory = $true)] [string] $Issue
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
# Keep every live worktree whose own checkpoint records this issue. An absent or
|
|
316
|
+
# unreadable checkpoint records nothing, which becomes a deny, not a silent pick.
|
|
317
|
+
$matched = [System.Collections.Generic.List[string]]::new()
|
|
318
|
+
$liveRoots = Get-WorktreeItemLiveRoot -SessionRoot $Session
|
|
319
|
+
foreach ($root in @($liveRoots)) {
|
|
320
|
+
if ((Get-WorktreeItemCheckpointIssue -WorktreeRoot $root) -eq $Issue) { $matched.Add($root) }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
# Zero matches denies for want of a place, one resolves, more is stale-attempt.
|
|
324
|
+
if ($matched.Count -eq 0) {
|
|
325
|
+
return (New-WorktreeResolutionTargetResult -Status 'NoTarget' -SessionRoot $Session -Detail (
|
|
326
|
+
"issue {0} is recorded in the orchestrator checkpoint of no live worktree" -f $Issue))
|
|
327
|
+
}
|
|
328
|
+
if ($matched.Count -eq 1) {
|
|
329
|
+
return (ConvertTo-WorktreeItemResolvedResult -WorktreeRoot $matched[0] -SessionRoot $Session -Detail (
|
|
330
|
+
"issue {0} resolves to the live worktree recording it" -f $Issue))
|
|
331
|
+
}
|
|
332
|
+
$detail = ("issue {0} is recorded in the orchestrator checkpoints of {1} live worktrees; " -f $Issue, $matched.Count) +
|
|
333
|
+
'move the stale checkpoint to artifacts/orchestration/handoff/ or add a branch: label naming the item''s branch'
|
|
334
|
+
return (New-WorktreeResolutionTargetResult -Status 'Ambiguous' -SessionRoot $Session -Candidate $matched.ToArray() -Detail $detail)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function Resolve-WorktreeItemTarget {
|
|
338
|
+
<#
|
|
339
|
+
.SYNOPSIS
|
|
340
|
+
Resolve the worktree a gated call acts on, from portable identity only.
|
|
341
|
+
.DESCRIPTION
|
|
342
|
+
Reads the canonical issue number and a branch signal from the call text and
|
|
343
|
+
decides one of the four target states. No path signal is read. The session path
|
|
344
|
+
only finds the repository and labels a resolved result SessionRoot.
|
|
345
|
+
.PARAMETER Text
|
|
346
|
+
The prompt or command text to read identity from.
|
|
347
|
+
.PARAMETER SessionRoot
|
|
348
|
+
Optional. The calling process's path. Defaults to the current location.
|
|
349
|
+
#>
|
|
350
|
+
[CmdletBinding()]
|
|
351
|
+
[OutputType([pscustomobject])]
|
|
352
|
+
param(
|
|
353
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Text,
|
|
354
|
+
[string] $SessionRoot
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
$session = if ([string]::IsNullOrWhiteSpace($SessionRoot)) { (Get-Location).ProviderPath } else { $SessionRoot }
|
|
358
|
+
# Assigned before it is wrapped, for the reason recorded in the branch resolver.
|
|
359
|
+
$issueSignal = Find-WorktreeItemIssueSignal -Text $Text
|
|
360
|
+
$issues = @($issueSignal)
|
|
361
|
+
$branch = Find-WorktreeResolutionBranchSignal -Text $Text
|
|
362
|
+
$hasBranch = -not [string]::IsNullOrWhiteSpace($branch)
|
|
363
|
+
|
|
364
|
+
# Two identity-shape verdicts precede any enumeration or file read: several issue
|
|
365
|
+
# numbers cannot name one item, and no identity cannot name any.
|
|
366
|
+
if ($issues.Count -gt 1) {
|
|
367
|
+
return (New-WorktreeResolutionTargetResult -Status 'Ambiguous' -SessionRoot $session -Detail (
|
|
368
|
+
"the call names {0} different issue numbers ({1}), and an item has exactly one" -f $issues.Count, ($issues -join ', ')))
|
|
369
|
+
}
|
|
370
|
+
if ($issues.Count -eq 0 -and -not $hasBranch) {
|
|
371
|
+
return (New-WorktreeResolutionTargetResult -Status 'NoTarget' -SessionRoot $session -Detail (
|
|
372
|
+
'the call carries neither a canonical issue number line nor a branch signal ' +
|
|
373
|
+
'(--head, --branch, or branch:), so the item it acts on cannot be identified'))
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
# A branch signal is authoritative about placement and can break a stale-attempt
|
|
377
|
+
# tie, so it selects the branch-present rows; otherwise the issue number does.
|
|
378
|
+
if ($hasBranch) {
|
|
379
|
+
return (Resolve-WorktreeItemTargetByBranch -Session $session -Branch $branch -Issue $issues)
|
|
380
|
+
}
|
|
381
|
+
return (Resolve-WorktreeItemTargetByIssue -Session $session -Issue $issues[0])
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
Export-ModuleMember -Function `
|
|
385
|
+
Get-WorktreeItemCheckpointRelativePath, `
|
|
386
|
+
Get-WorktreeItemCheckpointPath, `
|
|
387
|
+
Find-WorktreeItemIssueSignal, `
|
|
388
|
+
ConvertTo-WorktreeItemIssueNumber, `
|
|
389
|
+
Get-WorktreeItemCheckpointText, `
|
|
390
|
+
Get-WorktreeItemCheckpointIssue, `
|
|
391
|
+
Get-WorktreeItemLiveRoot, `
|
|
392
|
+
Resolve-WorktreeItemTarget
|