@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.
- package/out/mcp-server.js +1139 -133
- package/package.json +4 -3
- package/resources/claude-customizations/.claude/agents/feature-review.md +10 -1
- package/resources/claude-customizations/.claude/agents/orchestrator.md +16 -3
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +18 -6
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
- package/resources/claude-customizations/.claude/hooks/{enforce-powershell-batch-budget-route.ps1 → enforce-batch-budget-route.ps1} +12 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +3 -3
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
- package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
- package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +4 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -13
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +375 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +22 -15
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +337 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +9 -1
- package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
- package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
- package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +119 -3
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
- package/resources/claude-customizations/.claude/rules/quality-tiers.md +1 -1
- package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +8 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +2 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +11 -3
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +11 -2
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +169 -5
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +1 -1
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +6 -43
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +4 -15
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +15 -6
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +10 -0
- package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +33 -13
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +50 -21
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
- package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
- package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +5 -1
- package/resources/claude-customizations/pack-manifests/core.json +10 -0
- package/resources/claude-customizations/pack-manifests/powershell.json +0 -1
- package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +5 -3
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +30 -6
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +3 -1
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -1
- package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +30 -7
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -2
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +18 -3
- package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
- package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -0
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +4 -124
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
- package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
- package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
- package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
- package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +8 -0
- package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -1
package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1
ADDED
|
@@ -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
|