@danmoisan/drm-copilot-mcp 1.1.10 → 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.
Files changed (82) hide show
  1. package/out/mcp-server.js +3517 -1104
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
  4. package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
  5. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +15 -2
  6. package/resources/claude-customizations/.claude/agents/parallel-planner.md +3 -0
  7. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +82 -51
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
  11. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  14. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -28
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +103 -6
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +124 -8
  17. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +171 -15
  18. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +50 -10
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
  20. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
  21. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
  22. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +35 -7
  23. package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +483 -0
  24. package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +483 -0
  25. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +218 -6
  26. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +18 -75
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +290 -0
  28. package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +415 -0
  29. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
  30. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMerge.psm1 +355 -0
  31. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMergeGrammar.psm1 +318 -0
  32. package/resources/claude-customizations/.claude/lib/project-file-merge/Resolve-MergeableConflict.ps1 +229 -0
  33. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
  34. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
  35. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
  36. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
  37. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
  38. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
  39. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
  40. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +323 -16
  41. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
  42. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
  43. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +46 -1
  44. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
  45. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +75 -8
  46. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +6 -0
  47. package/resources/claude-customizations/.claude/skills/powershell-orchestration-state-machine/SKILL.md +29 -1
  48. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
  49. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
  50. package/resources/claude-customizations/config/blast-radius.json +7 -0
  51. package/resources/claude-customizations/pack-manifests/core.json +16 -0
  52. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
  53. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +41 -0
  54. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -0
  55. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +26 -0
  56. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  57. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +242 -3
  58. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +67 -12
  59. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +34 -8
  60. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  61. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -1
  62. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +34 -7
  63. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +483 -0
  64. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +483 -0
  65. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +130 -2
  66. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
  67. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
  68. package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
  69. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
  70. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
  71. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
  72. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
  73. package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
  74. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
  75. package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
  76. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
  77. package/resources/codex-and-agents-customizations/pack-manifests/core.json +23 -1
  78. package/resources/config/orchestration-handoff-registry.json +138 -0
  79. package/resources/config/orchestration-handoff.schema.json +472 -0
  80. package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
  81. package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
  82. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +64 -0
@@ -0,0 +1,229 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Resolve mechanically-mergeable project-file conflicts in a worktree.
4
+
5
+ .DESCRIPTION
6
+ Entry point for the parent-side project-file merge step of issue #643. It
7
+ lists the conflicted paths of a worktree left by an in-progress merge,
8
+ refuses the whole set unless every path belongs to the mergeable class, and
9
+ otherwise rewrites each conflicted file as the keyed union of the two sides.
10
+
11
+ The script stages nothing and creates no revision: the caller owns those
12
+ steps, so a resolution this script produces is always inspectable before it
13
+ becomes history. It writes one compressed JSON object to stdout and exits 0
14
+ for both verdicts; the verdict is that object's result field. A non-zero exit
15
+ means an unexpected error, whose message is on stderr.
16
+
17
+ .PARAMETER Worktree
18
+ Absolute or relative path to the worktree holding the conflicted merge.
19
+
20
+ .PARAMETER ConfigPath
21
+ Truth table to read mergeable_paths from. Defaults to the worktree's own
22
+ config/blast-radius.json.
23
+ #>
24
+ param(
25
+ [string] $Worktree,
26
+ [string] $ConfigPath
27
+ )
28
+
29
+ Set-StrictMode -Version Latest
30
+ $ErrorActionPreference = 'Stop'
31
+
32
+ # The merge module is imported before the grammar module it nests. Reversing the
33
+ # two would leave the grammar unavailable here: the nested -Force import inside
34
+ # ProjectFileMerge.psm1 removes an already-loaded grammar module and re-imports it
35
+ # into that module's own scope, taking its commands out of this scope.
36
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'ProjectFileMerge.psm1') -Force -ErrorAction Stop
37
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'ProjectFileMergeGrammar.psm1') -Force -ErrorAction Stop
38
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../blast-radius/BlastRadiusConflict.psm1') -Force -ErrorAction Stop
39
+
40
+ # The three bytes a UTF-8 byte-order mark occupies, kept as a constant so the
41
+ # reader and the writer agree on one prefix.
42
+ $script:ByteOrderMark = [byte[]] @(0xEF, 0xBB, 0xBF)
43
+
44
+ # Splits a text into lines that each retain their own terminator, which is what
45
+ # lets a CRLF file survive a merge unchanged.
46
+ $script:LinePattern = '[^\r\n]*(?:\r\n|\n|\r)'
47
+
48
+ function Invoke-GitExe {
49
+ <#
50
+ .SYNOPSIS
51
+ The single executable seam of this script.
52
+ .DESCRIPTION
53
+ Every git invocation goes through here, so a test mocks one function
54
+ rather than an executable. A non-zero exit throws with the output.
55
+ #>
56
+ [CmdletBinding()]
57
+ # The unary comma wraps the result so the pipeline does not unroll a
58
+ # one-element collection, which makes the emitted object an Object[]
59
+ # carrying strings rather than a bare string[]. Both are declared.
60
+ [OutputType([string[]], [System.Object[]])]
61
+ param([Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]] $GitArgs)
62
+
63
+ $output = & git @GitArgs 2>&1
64
+ # A failed git call is never a resolvable state, so it stops the run.
65
+ if ($LASTEXITCODE -ne 0) {
66
+ throw ('git {0} exited {1}: {2}' -f ($GitArgs -join ' '), $LASTEXITCODE, ($output -join ' '))
67
+ }
68
+ return , @($output | ForEach-Object { [string] $_ })
69
+ }
70
+
71
+ function Read-ConflictedFile {
72
+ <#
73
+ .SYNOPSIS
74
+ Read a conflicted file as bytes, a mark flag, and terminator-bearing lines.
75
+ .DESCRIPTION
76
+ Decoding is strict UTF-8. A file that does not decode returns $null, which
77
+ the caller turns into an escalation rather than a lossy rewrite.
78
+ #>
79
+ [CmdletBinding()]
80
+ [OutputType([pscustomobject])]
81
+ param([Parameter(Mandatory = $true)][string] $Path)
82
+
83
+ $bytes = [System.IO.File]::ReadAllBytes($Path)
84
+ $hasBom = $bytes.Length -ge 3 -and $bytes[0] -eq $script:ByteOrderMark[0] -and $bytes[1] -eq $script:ByteOrderMark[1] -and $bytes[2] -eq $script:ByteOrderMark[2]
85
+ try {
86
+ $text = [System.Text.UTF8Encoding]::new($false, $true).GetString($bytes)
87
+ } catch [System.Text.DecoderFallbackException] {
88
+ return $null
89
+ }
90
+ # The decoded mark is metadata, not content; the writer restores it.
91
+ if ($hasBom) { $text = $text.Substring(1) }
92
+
93
+ $line = [System.Collections.Generic.List[string]]::new()
94
+ foreach ($match in [regex]::Matches($text, $script:LinePattern)) { $line.Add($match.Value) }
95
+ # A final line with no terminator is not matched above and is added here.
96
+ $consumed = ($line -join '').Length
97
+ if ($consumed -lt $text.Length) { $line.Add($text.Substring($consumed)) }
98
+
99
+ return [pscustomobject]@{ Bytes = $bytes; HasBom = $hasBom; Line = $line.ToArray() }
100
+ }
101
+
102
+ function Write-MergedFile {
103
+ <#
104
+ .SYNOPSIS
105
+ Write merged lines back over the conflicted file.
106
+ .DESCRIPTION
107
+ The byte array is assembled before the ShouldProcess guard, so -WhatIf
108
+ exercises the whole assembly and suppresses only the write itself.
109
+ #>
110
+ [CmdletBinding(SupportsShouldProcess = $true)]
111
+ [OutputType([void])]
112
+ param(
113
+ [Parameter(Mandatory = $true)][string] $Path,
114
+ [Parameter(Mandatory = $true)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Line,
115
+ [Parameter(Mandatory = $true)][bool] $HasBom
116
+ )
117
+
118
+ $body = [System.Text.UTF8Encoding]::new($false).GetBytes($Line -join '')
119
+ $payload = [System.Collections.Generic.List[byte]]::new()
120
+ # A file that carried a mark keeps it, because dropping it is a byte change
121
+ # no reviewer asked for.
122
+ if ($HasBom) { $payload.AddRange($script:ByteOrderMark) }
123
+ $payload.AddRange($body)
124
+ if ($PSCmdlet.ShouldProcess($Path, 'Write the merged project file')) {
125
+ [System.IO.File]::WriteAllBytes($Path, $payload.ToArray())
126
+ }
127
+ }
128
+
129
+ function ConvertTo-ResolutionResult {
130
+ <#
131
+ .SYNOPSIS
132
+ Build the single result record this script emits.
133
+ .DESCRIPTION
134
+ The three keys are the whole published contract, so they are written in
135
+ one place rather than at each return site.
136
+ #>
137
+ [CmdletBinding()]
138
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
139
+ param(
140
+ [Parameter(Mandatory = $true)][string] $ResultName,
141
+ [AllowEmptyCollection()][object[]] $Resolved = @(),
142
+ [AllowEmptyCollection()][AllowEmptyString()][string[]] $EscalatePath = @()
143
+ )
144
+
145
+ return [ordered]@{ result = $ResultName; resolved = $Resolved; escalate_paths = $EscalatePath }
146
+ }
147
+
148
+ function Invoke-MergeableConflictResolution {
149
+ <#
150
+ .SYNOPSIS
151
+ Resolve every conflicted path of a worktree, or escalate the whole set.
152
+ .DESCRIPTION
153
+ The set is all-or-nothing. One non-mergeable path escalates before any
154
+ file is read, and one unresolvable file escalates before any file is
155
+ written, so a partially merged worktree is not a reachable state.
156
+ .PARAMETER Worktree
157
+ The worktree holding the conflicted merge.
158
+ .PARAMETER ConfigPath
159
+ Truth table carrying mergeable_paths; the worktree default is used when
160
+ this is blank.
161
+ .OUTPUTS
162
+ An ordered record carrying result, resolved, and escalate_paths.
163
+ #>
164
+ [CmdletBinding()]
165
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
166
+ param(
167
+ [Parameter(Mandatory = $true)][string] $Worktree,
168
+ [AllowNull()][AllowEmptyString()][string] $ConfigPath
169
+ )
170
+
171
+ if ([string]::IsNullOrWhiteSpace($ConfigPath)) { $ConfigPath = Join-Path -Path $Worktree -ChildPath 'config/blast-radius.json' }
172
+ $config = Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json
173
+ $mergeable = @(Get-ConfigMergeablePath -Config $config)
174
+
175
+ # The listing is bound before it is filtered: Invoke-GitExe returns one array
176
+ # object, so piping it directly would hand the whole array to the filter.
177
+ $listed = Invoke-GitExe -GitArgs @('-C', $Worktree, 'diff', '--name-only', '--diff-filter=U')
178
+ $conflicted = @($listed | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
179
+ $outside = @($conflicted | Where-Object { -not (Test-MergeablePath -Entry $_ -MergeablePath $mergeable) })
180
+ # Classification happens first so a non-mergeable set costs no file read.
181
+ if ($outside.Count -gt 0) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath $outside) }
182
+
183
+ $pending = [System.Collections.Generic.List[object]]::new()
184
+ foreach ($path in $conflicted) {
185
+ $kind = Get-ProjectFileKind -Path $path
186
+ if ($null -eq $kind) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath @($path)) }
187
+ $file = Read-ConflictedFile -Path (Join-Path -Path $Worktree -ChildPath $path)
188
+ if ($null -eq $file) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath @($path)) }
189
+
190
+ $outcome = Merge-ConflictedText -Line $file.Line -Kind $kind
191
+ if ($outcome.Escalate) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath @($path)) }
192
+
193
+ # The three stages are the only authority on what the merge must keep.
194
+ # Each is bound directly, because Invoke-GitExe already returns one array
195
+ # and an array subexpression around it would nest that array inside another.
196
+ $ours = Invoke-GitExe -GitArgs @('-C', $Worktree, 'show', ":2:$path")
197
+ $theirs = Invoke-GitExe -GitArgs @('-C', $Worktree, 'show', ":3:$path")
198
+ $base = Invoke-GitExe -GitArgs @('-C', $Worktree, 'show', ":1:$path")
199
+ $isKept = Test-NeverDropPostCondition -MergedLine $outcome.Lines -OursLine $ours -TheirsLine $theirs -BaseLine $base -Kind $kind
200
+ if (-not $isKept) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath @($path)) }
201
+
202
+ $pending.Add([pscustomobject]@{
203
+ Path = $path
204
+ HasBom = $file.HasBom
205
+ Line = $outcome.Lines
206
+ Record = [ordered]@{
207
+ path = $path
208
+ entries_added_from_ours = @($outcome.EntriesAddedFromOurs)
209
+ entries_added_from_theirs = @($outcome.EntriesAddedFromTheirs)
210
+ version_resolutions = @($outcome.VersionResolutions)
211
+ }
212
+ })
213
+ }
214
+
215
+ # Every path resolved, so the worktree can be rewritten in one pass.
216
+ foreach ($item in $pending) {
217
+ Write-MergedFile -Path (Join-Path -Path $Worktree -ChildPath $item.Path) -Line $item.Line -HasBom $item.HasBom
218
+ }
219
+ return (ConvertTo-ResolutionResult -ResultName 'resolved' -Resolved @($pending | ForEach-Object { $_.Record }))
220
+ }
221
+
222
+ # Dot-sourcing loads the functions for a test host; any other invocation runs the
223
+ # step. The guard is the precedent set by Invoke-ReleaseReconciliation.ps1.
224
+ if ($MyInvocation.InvocationName -ne '.') {
225
+ if ([string]::IsNullOrWhiteSpace($Worktree)) { throw 'Worktree is required.' }
226
+ $result = Invoke-MergeableConflictResolution -Worktree $Worktree -ConfigPath $ConfigPath
227
+ Write-Output (ConvertTo-Json -InputObject $result -Depth 6 -Compress)
228
+ exit 0
229
+ }
@@ -0,0 +1,176 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pure epic readiness predicates for epic-scope gate decisions (issue #663).
4
+
5
+ .DESCRIPTION
6
+ Evaluates the epic checkpoint artifacts/orchestration/epic-orchestrator-state.json
7
+ for the two epic-scope legs that previously had no epic seam: the integration pull
8
+ request (gate 1) and the preimplementation gate's command and path legs (gate 4).
9
+ Each predicate returns an empty string when the checkpoint is ready, or the name of
10
+ the first failed conjunct, so a gate can name the conjunct in its denial reason.
11
+
12
+ The shared conjunct names (route_id, epic_feature_folder, epic_manifest_path,
13
+ integration_branch, features) match Get-EpicOrchestrationReadinessFailure in
14
+ .claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1, without its
15
+ delegation-specific target-record conjuncts. That hook-local file is neither imported
16
+ nor modified.
17
+
18
+ .NOTES
19
+ PowerShell 7+. Pure: no import, no filesystem read, no subprocess, no network, no
20
+ clock read, and no environment read. Mirrored byte-identically under
21
+ extensions/drm-copilot/resources/claude-customizations/.
22
+ AUTHORITY: PowerShell-authoritative. The epic readiness predicates have no Python reference implementation, and no enforcement hook invokes Python (issue #663, D5).
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 only route id that describes an epic checkpoint. Compared case-sensitively.
30
+ $script:EpicRouteId = 'epic'
31
+
32
+ # The feature merge states that count as integrated. Any other value, including a
33
+ # missing one, means a child has not landed on the integration branch.
34
+ $script:TerminalMergeStatuses = @('merged', 'worktree_removed')
35
+
36
+ # An epic manifest lives under the epics tree; the pattern is tested after backslash
37
+ # normalisation so either separator spelling is accepted.
38
+ $script:EpicManifestPattern = '(^|/)docs/features/epics/'
39
+
40
+ function Get-EpicReadinessPropertyValue {
41
+ # Pure: the named property of an object, or $null when the object or the property
42
+ # is absent. StrictMode forbids reading a missing property directly.
43
+ [OutputType([object])]
44
+ param([AllowNull()][object] $InputObject, [string] $Name)
45
+
46
+ if ($null -eq $InputObject) {
47
+ return $null
48
+ }
49
+ $property = $InputObject.PSObject.Properties[$Name]
50
+ if ($null -eq $property) {
51
+ return $null
52
+ }
53
+ return $property.Value
54
+ }
55
+
56
+ function Get-EpicReadinessFeatureList {
57
+ # Pure: the features array as a list, empty when it is absent or empty.
58
+ [OutputType([object[]])]
59
+ param([AllowNull()][object] $Checkpoint)
60
+
61
+ $features = Get-EpicReadinessPropertyValue -InputObject $Checkpoint -Name 'features'
62
+ if ($null -eq $features) {
63
+ return , @()
64
+ }
65
+ return , @($features)
66
+ }
67
+
68
+ function Get-EpicPrCreationReadinessFailure {
69
+ <#
70
+ .SYNOPSIS
71
+ Name the first failed conjunct of the epic integration-PR readiness shape, or ''.
72
+ .DESCRIPTION
73
+ Conjuncts, in order: checkpoint-absent (null checkpoint); route_id (not exactly
74
+ epic); integration_branch (empty, or different from a non-empty HeadBranch,
75
+ case-sensitive); features (absent or empty); merge_status (any feature whose
76
+ merge_status is not merged or worktree_removed).
77
+ .PARAMETER Checkpoint
78
+ The parsed epic checkpoint object, or $null.
79
+ .PARAMETER HeadBranch
80
+ The --head branch of the pull request; not compared when empty.
81
+ .OUTPUTS
82
+ System.String
83
+ #>
84
+ [CmdletBinding()]
85
+ [OutputType([string])]
86
+ param(
87
+ [Parameter(Mandatory = $true)]
88
+ [AllowNull()]
89
+ [object] $Checkpoint,
90
+
91
+ [AllowNull()]
92
+ [AllowEmptyString()]
93
+ [string] $HeadBranch = ''
94
+ )
95
+
96
+ if ($null -eq $Checkpoint) {
97
+ return 'checkpoint-absent'
98
+ }
99
+ if ([string](Get-EpicReadinessPropertyValue -InputObject $Checkpoint -Name 'route_id') -cne $script:EpicRouteId) {
100
+ return 'route_id'
101
+ }
102
+ $integrationBranch = [string](Get-EpicReadinessPropertyValue -InputObject $Checkpoint -Name 'integration_branch')
103
+ # Decide on the integration branch: it must be present, and when the caller names a
104
+ # head branch the two must be identical.
105
+ if ([string]::IsNullOrWhiteSpace($integrationBranch)) {
106
+ return 'integration_branch'
107
+ } elseif (-not [string]::IsNullOrWhiteSpace($HeadBranch) -and ($integrationBranch -cne $HeadBranch)) {
108
+ return 'integration_branch'
109
+ }
110
+ $features = Get-EpicReadinessFeatureList -Checkpoint $Checkpoint
111
+ if ($features.Count -eq 0) {
112
+ return 'features'
113
+ }
114
+ # Every child feature must have landed; the first non-terminal status fails the set.
115
+ foreach ($feature in $features) {
116
+ $status = [string](Get-EpicReadinessPropertyValue -InputObject $feature -Name 'merge_status')
117
+ if ($script:TerminalMergeStatuses -notcontains $status) {
118
+ return 'merge_status'
119
+ }
120
+ }
121
+ return ''
122
+ }
123
+
124
+ function Get-EpicCommandLegReadinessFailure {
125
+ <#
126
+ .SYNOPSIS
127
+ Name the first failed conjunct of the epic command-leg readiness shape, or ''.
128
+ .DESCRIPTION
129
+ Conjuncts, in order: checkpoint-absent; route_id; epic_feature_folder (empty);
130
+ epic_manifest_path (empty, or not under docs/features/epics/ after backslash
131
+ normalisation); integration_branch (empty); features (absent or empty);
132
+ merge-in-progress (no merge in progress in the effective worktree, D2).
133
+ .PARAMETER Checkpoint
134
+ The parsed epic checkpoint object, or $null.
135
+ .PARAMETER MergeInProgress
136
+ Whether MERGE_HEAD exists in the effective worktree's git directory.
137
+ .OUTPUTS
138
+ System.String
139
+ #>
140
+ [CmdletBinding()]
141
+ [OutputType([string])]
142
+ param(
143
+ [Parameter(Mandatory = $true)]
144
+ [AllowNull()]
145
+ [object] $Checkpoint,
146
+
147
+ [Parameter(Mandatory = $true)]
148
+ [bool] $MergeInProgress
149
+ )
150
+
151
+ if ($null -eq $Checkpoint) {
152
+ return 'checkpoint-absent'
153
+ }
154
+ if ([string](Get-EpicReadinessPropertyValue -InputObject $Checkpoint -Name 'route_id') -cne $script:EpicRouteId) {
155
+ return 'route_id'
156
+ }
157
+ if ([string]::IsNullOrWhiteSpace([string](Get-EpicReadinessPropertyValue -InputObject $Checkpoint -Name 'epic_feature_folder'))) {
158
+ return 'epic_feature_folder'
159
+ }
160
+ $manifestPath = ([string](Get-EpicReadinessPropertyValue -InputObject $Checkpoint -Name 'epic_manifest_path')) -replace '\\', '/'
161
+ if ([string]::IsNullOrWhiteSpace($manifestPath) -or $manifestPath -notmatch $script:EpicManifestPattern) {
162
+ return 'epic_manifest_path'
163
+ }
164
+ if ([string]::IsNullOrWhiteSpace([string](Get-EpicReadinessPropertyValue -InputObject $Checkpoint -Name 'integration_branch'))) {
165
+ return 'integration_branch'
166
+ }
167
+ if ((Get-EpicReadinessFeatureList -Checkpoint $Checkpoint).Count -eq 0) {
168
+ return 'features'
169
+ }
170
+ if (-not $MergeInProgress) {
171
+ return 'merge-in-progress'
172
+ }
173
+ return ''
174
+ }
175
+
176
+ Export-ModuleMember -Function Get-EpicPrCreationReadinessFailure, Get-EpicCommandLegReadinessFailure