@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.
Files changed (66) hide show
  1. package/out/mcp-server.js +368 -209
  2. package/package.json +2 -2
  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-planner.md +18 -1
  6. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
  7. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
  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 +33 -37
  10. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
  11. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +172 -24
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  14. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
  15. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
  16. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
  17. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
  18. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
  19. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
  20. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +43 -6
  21. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
  22. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
  23. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
  24. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
  25. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
  26. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
  27. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
  29. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
  30. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
  31. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -2
  32. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
  33. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +12 -0
  34. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
  35. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +15 -0
  36. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
  37. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
  38. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
  39. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
  40. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
  41. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
  42. package/resources/claude-customizations/config/blast-radius.json +14 -1
  43. package/resources/claude-customizations/pack-manifests/core.json +11 -0
  44. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
  45. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  46. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
  47. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
  48. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
  49. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
  50. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +172 -24
  51. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  52. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
  53. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
  54. package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
  55. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
  56. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
  57. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
  58. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
  59. package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
  60. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
  61. package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
  62. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
  63. package/resources/codex-and-agents-customizations/pack-manifests/core.json +20 -1
  64. package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
  65. package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
  66. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +37 -0
@@ -20,19 +20,24 @@ function Get-PrAuthorCheckpointContent {
20
20
  Read the raw JSON text of the per-feature orchestrator checkpoint. Tests mock this
21
21
  function (read seam) for the epic-mode base-branch override check.
22
22
  .DESCRIPTION
23
- Returns the raw text content of artifacts/orchestration/orchestrator-state.json, or
24
- $null when the file is absent. This is the injectable boundary for checkpoint content
25
- used by Test-EpicBaseBranchOverride; no test writes the checkpoint file to disk.
23
+ Returns the raw text content of the checkpoint at the supplied path, or $null when the
24
+ file is absent. This is the injectable boundary for checkpoint content used by
25
+ Test-EpicBaseBranchOverride; no test writes the checkpoint file to disk.
26
+
27
+ The path is mandatory and carries no default (issue #673). A relative default was the
28
+ second of this feature's three bindings: it made check 6 read whichever checkpoint
29
+ occupied the process directory even when target resolution had already selected a
30
+ different worktree, so the epic verdict could come from an unrelated item.
26
31
  .PARAMETER CheckpointPath
27
- The relative path to the per-feature orchestrator checkpoint.
32
+ The absolute path of the checkpoint belonging to the resolved worktree.
28
33
  .OUTPUTS
29
34
  System.String or $null
30
35
  #>
31
36
  [CmdletBinding()]
32
37
  [OutputType([string])]
33
38
  param(
34
- [Parameter(Mandatory = $false)]
35
- [string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
39
+ [Parameter(Mandatory)]
40
+ [string] $CheckpointPath
36
41
  )
37
42
 
38
43
  if (-not (Test-Path -LiteralPath $CheckpointPath)) {
@@ -57,6 +62,13 @@ function Test-EpicBaseBranchOverride {
57
62
  unchanged.
58
63
  .PARAMETER CommandText
59
64
  The Bash command text under evaluation.
65
+ .PARAMETER CheckpointPath
66
+ The absolute checkpoint path identity resolution selected, passed straight through to
67
+ the read seam so this check and the orchestrator-state preflight read one file.
68
+ .PARAMETER EpicScope
69
+ Optional. The epic-scope result gate 1 resolved (issue #663). When it reports epic
70
+ scope, the integration pull request must pass --base main and the per-feature
71
+ checkpoint seam is not read; $null keeps the per-feature epic_mode rule unchanged.
60
72
  .OUTPUTS
61
73
  System.String or $null
62
74
  #>
@@ -64,7 +76,13 @@ function Test-EpicBaseBranchOverride {
64
76
  [OutputType([string])]
65
77
  param(
66
78
  [Parameter(Mandatory)]
67
- [string] $CommandText
79
+ [string] $CommandText,
80
+
81
+ [Parameter(Mandatory)]
82
+ [string] $CheckpointPath,
83
+
84
+ [AllowNull()]
85
+ [object] $EpicScope
68
86
  )
69
87
 
70
88
  # The epic-mode base-branch override only constrains gh pr create; gh pr edit does not
@@ -75,7 +93,18 @@ function Test-EpicBaseBranchOverride {
75
93
  return $null
76
94
  }
77
95
 
78
- $checkpointRaw = Get-PrAuthorCheckpointContent
96
+ # Epic scope (issue #663): the epic's own integration pull request targets main. The
97
+ # scope object comes from gate 1, so the epic checkpoint is not read a second time and
98
+ # the per-feature checkpoint is not consulted.
99
+ if ($null -ne $EpicScope -and $EpicScope.IsEpicScope) {
100
+ $epicBaseValue = Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create') -FlagName '--base'
101
+ if ($null -eq $epicBaseValue -or $epicBaseValue -cne 'main') {
102
+ return "EPIC_BASE_BRANCH_MISMATCH: this epic-scope gh pr create was evaluated against $($EpicScope.CheckpointPath); the integration pull request must pass --base main."
103
+ }
104
+ return $null
105
+ }
106
+
107
+ $checkpointRaw = Get-PrAuthorCheckpointContent -CheckpointPath $CheckpointPath
79
108
  if ([string]::IsNullOrWhiteSpace($checkpointRaw)) {
80
109
  return $null
81
110
  }
@@ -46,7 +46,9 @@ param()
46
46
 
47
47
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
48
48
  $script:PrContextArtifactPath = 'artifacts/pr_context.summary.txt'
49
- $script:OrchestratorStateCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
49
+ # Null until the resolution step assigns the absolute path of the resolved worktree's
50
+ # checkpoint (issue #673); a relative default here was the process-directory binding.
51
+ $script:OrchestratorStateCheckpointPath = $null
50
52
 
51
53
  Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorState.psm1') -Force
52
54
 
@@ -0,0 +1,332 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Prompt-side feature-folder resolution for the prd-feature gate (issue #672).
4
+ .DESCRIPTION
5
+ This file is dot-sourced by its parent hook enforce-prd-feature-before-planner.ps1,
6
+ following the headroom-split precedent set by
7
+ enforce-orchestration-preimplementation-gate-helpers.ps1. The parent keeps the
8
+ decision entrypoint and the seams the existing suites mock; this file carries the
9
+ resolution and prerequisite-mapping logic, so neither file approaches the 500-line
10
+ cap as target resolution grows.
11
+
12
+ The file declares no file-scope parameter block, no requires directive, and no
13
+ entrypoint. Apart from its function declarations,
14
+ its only file-scope statement is the resolution-module import
15
+ that follows this block, which brings in issue #669's path normalisation and
16
+ ambiguity reason code. That import is deliberately unguarded, for the same
17
+ reason the parent hook's is: a resolution module that cannot be loaded is
18
+ itself the target-not-resolvable state, so letting the failure surface fails
19
+ the gate closed rather than degrading it to a permissive path.
20
+ .NOTES
21
+ Compatible with PowerShell 7+. Read-only resolution logic.
22
+ #>
23
+
24
+ # Issue #669 owns path normalisation and the ambiguity reason code. The import is
25
+ # unguarded for the same reason the parent's is: a resolution module that cannot be
26
+ # loaded is itself the target-not-resolvable state.
27
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeResolution.psm1') -Force
28
+
29
+ function Resolve-PrdFeatureWorkMode {
30
+ <#
31
+ .SYNOPSIS
32
+ Parses the persisted `- Work Mode: ...` marker out of issue.md content
33
+ and returns the canonical mode, or $null when the marker is absent,
34
+ unreadable, or unrecognized.
35
+ .DESCRIPTION
36
+ Recognizes minor-audit, full-feature, full-bug, and the legacy full
37
+ marker (normalized to full-feature), mirroring the regex convention
38
+ used by scripts/dev_tools/prompt_mode_contract.py so both runtimes
39
+ agree on what counts as a valid marker line.
40
+ #>
41
+ [CmdletBinding()]
42
+ [OutputType([string])]
43
+ param(
44
+ [AllowNull()]
45
+ [string] $IssueContent
46
+ )
47
+
48
+ if ([string]::IsNullOrWhiteSpace($IssueContent)) {
49
+ return $null
50
+ }
51
+
52
+ $match = [regex]::Match($IssueContent, '(?im)^-\s*Work Mode:\s*(minor-audit|full-feature|full-bug|full)\s*$')
53
+ if (-not $match.Success) {
54
+ return $null
55
+ }
56
+
57
+ $rawMode = $match.Groups[1].Value
58
+ if ($rawMode -eq 'full') {
59
+ return 'full-feature'
60
+ }
61
+ return $rawMode
62
+ }
63
+
64
+ function Get-PrdFeatureRequiredFile {
65
+ <#
66
+ .SYNOPSIS
67
+ Maps a resolved work mode to the set of prd-feature output files the
68
+ target folder must contain before an atomic-planner delegation is
69
+ allowed.
70
+ .DESCRIPTION
71
+ full-feature requires spec.md and user-story.md; full-bug requires
72
+ spec.md only; minor-audit requires neither.
73
+
74
+ The default arm returns spec.md alone. It is not reached from the
75
+ decision path for an undeterminable mode, which denies on its own branch
76
+ without probing at all; the arm exists so a direct caller passing a $null
77
+ or unrecognized mode never receives a permissive empty set, and it must
78
+ not name user-story.md, because that document is required to be ABSENT
79
+ for full-bug and minor-audit work.
80
+ #>
81
+ [CmdletBinding()]
82
+ [OutputType([string[]])]
83
+ param(
84
+ [AllowNull()]
85
+ [string] $WorkMode
86
+ )
87
+
88
+ # Route on the canonical mode; anything outside the three known values
89
+ # (including $null) falls through to the fail-closed default case.
90
+ switch ($WorkMode) {
91
+ 'full-feature' { return [string[]]@('spec.md', 'user-story.md') }
92
+ 'full-bug' { return [string[]]@('spec.md') }
93
+ 'minor-audit' { return [string[]]@() }
94
+ default { return [string[]]@('spec.md') }
95
+ }
96
+ }
97
+
98
+ function ConvertTo-PrdFeatureFolderToken {
99
+ <#
100
+ .SYNOPSIS
101
+ Reduces one cited path token to its four-segment repo-relative feature
102
+ folder, or $null when the token does not name one.
103
+ .DESCRIPTION
104
+ The prefix half is issue #669's path normalisation, not a local
105
+ re-implementation: ConvertTo-WorktreeResolutionRepoRelativePath places an
106
+ absolute token by locating its containing worktree and returns the full
107
+ remainder below that root, so no segment is dropped and no prefix is
108
+ discarded by pattern shape. A token that does not normalise is already
109
+ repo-relative (or names no worktree), and the repo-relative remainder is
110
+ then read from the token itself.
111
+
112
+ The depth-collapsing half below is carried byte-unmodified from the fix
113
+ for issue #518. It is depth normalisation, not the segment-count
114
+ truncation the epic prohibits: it maps a folder path, a spec.md path, a
115
+ research/ artifact path, and an evidence/ artifact path onto the same
116
+ folder.
117
+ #>
118
+ [CmdletBinding()]
119
+ [OutputType([string])]
120
+ param(
121
+ [Parameter(Mandatory)]
122
+ [AllowEmptyString()]
123
+ [string] $Path
124
+ )
125
+
126
+ if (-not $Path) {
127
+ return $null
128
+ }
129
+
130
+ $placed = ConvertTo-WorktreeResolutionRepoRelativePath -Path $Path
131
+ $normalized = if ($placed.IsNormalized) {
132
+ $placed.RepoRelativePath
133
+ }
134
+ else {
135
+ ($Path -replace '\\', '/')
136
+ }
137
+ $normalized = $normalized.TrimEnd('/')
138
+
139
+ # Read the feature-folder remainder from wherever it starts in the normalised
140
+ # token. A normalised token starts at it already; a token that named no
141
+ # worktree keeps whatever prefix it was cited with.
142
+ $start = $normalized.IndexOf('docs/features/active/')
143
+ if ($start -lt 0) {
144
+ return $null
145
+ }
146
+ $normalized = $normalized.Substring($start)
147
+
148
+ # Truncate to exactly two segments past the docs/features/active/ prefix,
149
+ # that is, to the four segments docs, features, active, and the feature
150
+ # folder name. Truncation is depth-insensitive, so a folder path, a
151
+ # spec.md path, a research/ artifact path, and an evidence/ artifact path
152
+ # all reduce to the same value. A '.' component is a path no-op and is
153
+ # discarded first, so a degenerate token such as docs/features/active/.
154
+ # yields three segments and is rejected rather than resolved.
155
+ $segments = @($normalized -split '/' | Where-Object { $_ -ne '' -and $_ -ne '.' })
156
+ if ($segments.Count -lt 4) {
157
+ return $null
158
+ }
159
+
160
+ return ($segments[0..3] -join '/')
161
+ }
162
+
163
+ function Find-PrdFeatureFolderCandidate {
164
+ <#
165
+ .SYNOPSIS
166
+ Returns the distinct feature folders a prompt cites, in first-occurrence
167
+ order.
168
+ .DESCRIPTION
169
+ The scan pattern keeps any prefix the token was cited with, so an absolute
170
+ citation reaches the normalisation in ConvertTo-PrdFeatureFolderToken
171
+ rather than being discarded by the pattern itself.
172
+ #>
173
+ [CmdletBinding()]
174
+ [OutputType([string[]])]
175
+ param(
176
+ [Parameter(Mandatory)]
177
+ [AllowEmptyString()]
178
+ [string] $Prompt
179
+ )
180
+
181
+ if (-not $Prompt) {
182
+ return [string[]]@()
183
+ }
184
+
185
+ # Allow forward or backslash separators inside the matched path token, and
186
+ # keep any prefix the token carries.
187
+ $pattern = '(?:[^\s"''`]*[\\/])?docs[\\/]+features[\\/]+active[\\/]+[^\s"''`]+'
188
+ $matchList = [regex]::Matches($Prompt, $pattern)
189
+ if ($matchList.Count -eq 0) {
190
+ return [string[]]@()
191
+ }
192
+
193
+ # Deduplicate preserving FIRST-OCCURRENCE order. A [hashtable] must not be
194
+ # used here: PowerShell hashtable key enumeration order is unspecified, so a
195
+ # first-occurrence selection rule fed by a hashtable is not deterministic.
196
+ [System.Collections.Generic.List[string]] $candidates = [System.Collections.Generic.List[string]]::new()
197
+ foreach ($m in $matchList) {
198
+ $truncated = ConvertTo-PrdFeatureFolderToken -Path $m.Value
199
+ if (-not $truncated) {
200
+ continue
201
+ }
202
+
203
+ if (-not $candidates.Contains($truncated)) {
204
+ $candidates.Add($truncated)
205
+ }
206
+ }
207
+
208
+ return [string[]] $candidates.ToArray()
209
+ }
210
+
211
+ function Select-PrdFeatureFolderByCheckpoint {
212
+ <#
213
+ .SYNOPSIS
214
+ Returns the candidate the resolved worktree's checkpoint names, or $null when
215
+ the checkpoint names none of them.
216
+ .DESCRIPTION
217
+ The disambiguator is the orchestrator checkpoint of the worktree identity
218
+ resolution selected, which is the item's own. Neither prompt position nor a
219
+ foreign session's record takes that role: position would make selection an
220
+ accident of wording, and a checkpoint belonging to some other session would
221
+ choose between two cited folders on the strength of that session's record of
222
+ its own unrelated work.
223
+
224
+ Before issue #673 this function read a signal value off a call target the gate
225
+ had derived from a path token. That is gone: a path no longer selects a
226
+ worktree, so there is no derived signal to read.
227
+ .PARAMETER Candidate
228
+ The distinct feature folders cited in the prompt.
229
+ .PARAMETER CheckpointFolder
230
+ The feature-folder value read from the resolved worktree's checkpoint.
231
+ #>
232
+ [CmdletBinding()]
233
+ [OutputType([string])]
234
+ param(
235
+ [Parameter(Mandatory)]
236
+ [AllowEmptyCollection()]
237
+ [string[]] $Candidate,
238
+
239
+ [AllowNull()]
240
+ [string] $CheckpointFolder
241
+ )
242
+
243
+ $folder = ConvertTo-PrdFeatureFolderToken -Path $CheckpointFolder
244
+ if ($folder -and ($Candidate -contains $folder)) {
245
+ return $folder
246
+ }
247
+
248
+ return $null
249
+ }
250
+
251
+ function Find-PrdFeatureFolderFromPrompt {
252
+ <#
253
+ .SYNOPSIS
254
+ Scans a prompt string for docs/features/active/<...> path tokens,
255
+ truncates each to exactly four path segments, and returns the selected
256
+ feature folder. Returns $null when no token truncates to four segments
257
+ and when a multi-candidate tie cannot be resolved against the resolved
258
+ worktree's checkpoint.
259
+ .DESCRIPTION
260
+ Truncation to four segments -- docs, features, active, and the
261
+ feature-folder name -- is two segments past the docs/features/active/
262
+ prefix, so the depth at which an artifact is cited cannot change which
263
+ folder is resolved. Candidates are deduplicated preserving
264
+ first-occurrence order; selection among two or more distinct candidates
265
+ is made against the checkpoint of the worktree identity resolution
266
+ selected, which is the item's own, and an unresolved tie returns $null so
267
+ the caller can deny with the ambiguity code rather than guess.
268
+
269
+ The return value is a repo-relative path normalized to forward slashes,
270
+ or $null. The function reads no file except through issue #669's
271
+ normalisation, and it is deterministic for a given prompt and checkpoint
272
+ value.
273
+ .PARAMETER Prompt
274
+ The delegation prompt to scan.
275
+ .PARAMETER CheckpointFolder
276
+ Optional. The feature-folder value read from the resolved worktree's
277
+ checkpoint, used only to break a multi-candidate tie.
278
+ #>
279
+ [CmdletBinding()]
280
+ [OutputType([string])]
281
+ param(
282
+ [Parameter(Mandatory)]
283
+ [AllowEmptyString()]
284
+ [string] $Prompt,
285
+
286
+ [AllowNull()]
287
+ [string] $CheckpointFolder
288
+ )
289
+
290
+ $candidates = @(Find-PrdFeatureFolderCandidate -Prompt $Prompt)
291
+ if ($candidates.Count -eq 0) {
292
+ return $null
293
+ }
294
+
295
+ # One distinct candidate is used directly, so the common case never consults
296
+ # the checkpoint at all.
297
+ if ($candidates.Count -eq 1) {
298
+ return $candidates[0]
299
+ }
300
+
301
+ # More than one distinct feature folder was cited. The resolved worktree's own
302
+ # checkpoint is the only disambiguator; an unresolved tie is ambiguous and is
303
+ # reported as such by returning $null, rather than resolved positionally.
304
+ return (Select-PrdFeatureFolderByCheckpoint -Candidate $candidates -CheckpointFolder $CheckpointFolder)
305
+ }
306
+
307
+ function Get-PrdFeatureMissingFile {
308
+ <#
309
+ .SYNOPSIS
310
+ Returns the subset of $RequiredFile that is missing from the target
311
+ folder.
312
+ #>
313
+ [CmdletBinding()]
314
+ [OutputType([string[]])]
315
+ param(
316
+ [Parameter(Mandatory)]
317
+ [string] $FeatureFolder,
318
+
319
+ [Parameter(Mandatory)]
320
+ [AllowEmptyCollection()]
321
+ [string[]] $RequiredFile
322
+ )
323
+
324
+ [System.Collections.Generic.List[string]] $missing = [System.Collections.Generic.List[string]]::new()
325
+ foreach ($name in $RequiredFile) {
326
+ $candidate = "$FeatureFolder/$name"
327
+ if (-not (Get-PrdFeatureFileExistence -Path $candidate)) {
328
+ $missing.Add($name)
329
+ }
330
+ }
331
+ return [string[]] $missing.ToArray()
332
+ }