@danmoisan/drm-copilot-mcp 1.1.24 → 1.1.26
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/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +7 -3
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +6 -2
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +41 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +69 -3
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +2 -2
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.cleanup.ps1 +246 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +1 -0
- package/resources/claude-customizations/.claude/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 +626 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmission.psm1 +508 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1 +141 -0
- package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionPrecedence.psm1 +508 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +17 -1
- package/resources/claude-customizations/.claude/settings.json +1 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +93 -14
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +23 -10
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +54 -46
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh +234 -0
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +15 -5
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +71 -7
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +26 -2
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +82 -12
- package/resources/claude-customizations/pack-manifests/core.json +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +15 -5
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +4 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-cleanup-context.ps1 +343 -0
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +15 -5
- package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +3 -1
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -10
package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1
ADDED
|
@@ -0,0 +1,626 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Prepared-item admission helper for /parallel-add and /parallel-plan (issue #1047).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Entry point the parallel-planner and parallel-orchestrator forks call as
|
|
7
|
+
`pwsh -NoProfile -NonInteractive -File .claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 <subcommand> ...`
|
|
8
|
+
with the subcommands guard-run, assess, order-cohorts, test-cohorts, record,
|
|
9
|
+
and reuse-edges (feature spec `## API / CLI Surface`). The decisions are the
|
|
10
|
+
pure functions of ParallelAdmission.psm1 and ParallelAdmissionPrecedence.psm1,
|
|
11
|
+
and ParallelAdmissionJson.psm1 reads and writes the JSON; this script owns the
|
|
12
|
+
I/O through three seams: Invoke-ParallelAdmissionGit (every git call),
|
|
13
|
+
Read-ParallelAdmissionJsonText (every file read), and
|
|
14
|
+
Get-ParallelAdmissionUtcNow (the clock).
|
|
15
|
+
|
|
16
|
+
Side effects: assess runs `git fetch --quiet origin`, which updates the
|
|
17
|
+
remote-tracking refs and the object store and needs network access; a fetch
|
|
18
|
+
failure exits 1 with the failure prefix. Every other git call reads by ref,
|
|
19
|
+
and the helper writes no file. No parameter is Mandatory, so a missing option
|
|
20
|
+
exits 2.
|
|
21
|
+
|
|
22
|
+
Exit codes: 0 prints one JSON object; 1 is a rule rejection with one stderr
|
|
23
|
+
line "PARALLEL_ADMISSION_REJECTED: <code>: <detail>" (run_closed,
|
|
24
|
+
precedence_cycle, precedence_unknown_item, precedence_violated) or an
|
|
25
|
+
unreadable input with one stderr line prefixed "parallel admission failed: ";
|
|
26
|
+
2 is a usage error prefixed "parallel admission usage error: ".
|
|
27
|
+
#>
|
|
28
|
+
[CmdletBinding(PositionalBinding = $false)]
|
|
29
|
+
param(
|
|
30
|
+
[Parameter(Position = 0)][string] $Subcommand,
|
|
31
|
+
[string] $CheckpointPath, [string] $ManifestMode, [string] $Branch, [string] $FeatureFolder, [string] $Receipt,
|
|
32
|
+
[string] $Band, [string] $PlannerCheckpointPath, [string] $Main, [string] $Cohorts, [string] $Edges,
|
|
33
|
+
[string] $Precedence, [string] $Pinned, [string] $Floor, [string] $RunFolder, [string] $Entry,
|
|
34
|
+
[string] $Slug, [string] $IssueNum, [string] $PlanHash, [string] $Ref,
|
|
35
|
+
[Parameter(ValueFromRemainingArguments = $true)][string[]] $Remaining
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
Set-StrictMode -Version Latest
|
|
39
|
+
$ErrorActionPreference = 'Stop'
|
|
40
|
+
|
|
41
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'ParallelAdmission.psm1') -Force -ErrorAction Stop
|
|
42
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'ParallelAdmissionPrecedence.psm1') -Force -ErrorAction Stop
|
|
43
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'ParallelAdmissionJson.psm1') -Force -ErrorAction Stop
|
|
44
|
+
|
|
45
|
+
$script:AdmissionUsagePrefix = 'parallel admission usage error: '
|
|
46
|
+
$script:AdmissionFailurePrefix = 'parallel admission failed: '
|
|
47
|
+
$script:AdmissionRejectionPrefix = 'PARALLEL_ADMISSION_REJECTED: '
|
|
48
|
+
$script:AdmissionRejectionCode = @('run_closed', 'precedence_cycle', 'precedence_unknown_item', 'precedence_violated')
|
|
49
|
+
$script:AdmissionDefaultMain = 'origin/main'
|
|
50
|
+
$script:AdmissionTimestampFormat = 'yyyy-MM-ddTHH-mm'
|
|
51
|
+
$script:AdmissionBandName = @('C1', 'C2', 'C3', 'C4')
|
|
52
|
+
$script:AdmissionRequiredOption = @{
|
|
53
|
+
'guard-run' = @('CheckpointPath', 'ManifestMode')
|
|
54
|
+
'assess' = @('Branch', 'FeatureFolder')
|
|
55
|
+
'order-cohorts' = @('Cohorts')
|
|
56
|
+
'test-cohorts' = @('CheckpointPath')
|
|
57
|
+
'record' = @('RunFolder', 'Entry')
|
|
58
|
+
'reuse-edges' = @('Slug', 'IssueNum', 'PlanHash')
|
|
59
|
+
}
|
|
60
|
+
$script:AdmissionSpecTemplatePath = 'docs/features/templates/feature/spec.md'
|
|
61
|
+
$script:AdmissionPlanTemplatePath = 'docs/features/templates/feature/plan.yyyy-MM-ddTHH-mm.md'
|
|
62
|
+
|
|
63
|
+
function Invoke-ParallelAdmissionGit {
|
|
64
|
+
<#
|
|
65
|
+
.SYNOPSIS
|
|
66
|
+
Run one git command and return ExitCode, Output (stdout lines), and Stderr (stderr lines); the only process call and its test seam.
|
|
67
|
+
#>
|
|
68
|
+
[CmdletBinding()]
|
|
69
|
+
[OutputType([pscustomobject])]
|
|
70
|
+
param([string[]] $GitArgs)
|
|
71
|
+
|
|
72
|
+
# 2>&1 interleaves the streams; stderr lines arrive as ErrorRecord objects and are split off by type.
|
|
73
|
+
$all = @(& git @GitArgs 2>&1)
|
|
74
|
+
return [pscustomobject]@{ ExitCode = $LASTEXITCODE; Output = [string[]]@($all | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }); Stderr = [string[]]@($all | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] } | ForEach-Object { $_.ToString() }) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function Read-ParallelAdmissionJsonText {
|
|
78
|
+
<#
|
|
79
|
+
.SYNOPSIS
|
|
80
|
+
Read one document as UTF-8 text; the script's only filesystem read and its test seam.
|
|
81
|
+
#>
|
|
82
|
+
[CmdletBinding()]
|
|
83
|
+
[OutputType([string])]
|
|
84
|
+
param([string] $Path)
|
|
85
|
+
|
|
86
|
+
return [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function Get-ParallelAdmissionUtcNow {
|
|
90
|
+
<#
|
|
91
|
+
.SYNOPSIS
|
|
92
|
+
Return the current UTC instant; the script's only clock read and its test seam.
|
|
93
|
+
#>
|
|
94
|
+
[CmdletBinding()]
|
|
95
|
+
[OutputType([datetime])]
|
|
96
|
+
param()
|
|
97
|
+
|
|
98
|
+
return [datetime]::UtcNow
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function Resolve-ParallelAdmissionPath {
|
|
102
|
+
<#
|
|
103
|
+
.SYNOPSIS
|
|
104
|
+
Return a rooted path unchanged and join a relative path to the current location.
|
|
105
|
+
#>
|
|
106
|
+
[CmdletBinding()]
|
|
107
|
+
[OutputType([string])]
|
|
108
|
+
param([string] $Path)
|
|
109
|
+
|
|
110
|
+
if ([System.IO.Path]::IsPathRooted($Path)) { return $Path }
|
|
111
|
+
return Join-Path -Path (Get-Location).Path -ChildPath $Path
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function Read-ParallelAdmissionDocument {
|
|
115
|
+
<#
|
|
116
|
+
.SYNOPSIS
|
|
117
|
+
Read and parse one JSON document from disk and require an object root.
|
|
118
|
+
#>
|
|
119
|
+
[CmdletBinding()]
|
|
120
|
+
[OutputType([System.Collections.IDictionary])]
|
|
121
|
+
param([string] $Path, [string] $Label)
|
|
122
|
+
|
|
123
|
+
$value = ConvertFrom-ParallelAdmissionJson -Text (Read-ParallelAdmissionJsonText -Path (Resolve-ParallelAdmissionPath -Path $Path))
|
|
124
|
+
if ($value -isnot [System.Collections.IDictionary]) { throw "$Label at $Path must be a JSON object." }
|
|
125
|
+
return $value
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ConvertTo-ParallelAdmissionCliOutcome {
|
|
129
|
+
<#
|
|
130
|
+
.SYNOPSIS
|
|
131
|
+
Build the result record the guarded entry body writes and exits with.
|
|
132
|
+
#>
|
|
133
|
+
[CmdletBinding()]
|
|
134
|
+
[OutputType([pscustomobject])]
|
|
135
|
+
param([int] $ExitCode, [string] $Stdout = '', [string] $Stderr = '')
|
|
136
|
+
|
|
137
|
+
return [pscustomobject]@{ ExitCode = $ExitCode; Stdout = $Stdout; Stderr = $Stderr }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function ConvertTo-ParallelAdmissionRejection {
|
|
141
|
+
<#
|
|
142
|
+
.SYNOPSIS
|
|
143
|
+
Turn a rule rejection ("<code>: <detail>", or a detail plus Code) into exit 1 with one PARALLEL_ADMISSION_REJECTED line.
|
|
144
|
+
#>
|
|
145
|
+
[CmdletBinding()]
|
|
146
|
+
[OutputType([pscustomobject])]
|
|
147
|
+
param([string] $Message, [string] $Code)
|
|
148
|
+
|
|
149
|
+
$detail = ($Message -replace "\r?\n", ' ').Trim()
|
|
150
|
+
if ([string]::IsNullOrEmpty($Code)) {
|
|
151
|
+
$Code = $detail.Substring(0, $detail.IndexOf(':'))
|
|
152
|
+
$detail = $detail.Substring($detail.IndexOf(':') + 1).Trim()
|
|
153
|
+
}
|
|
154
|
+
return ConvertTo-ParallelAdmissionCliOutcome -ExitCode 1 -Stderr "$($script:AdmissionRejectionPrefix)${Code}: $detail"
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function Test-ParallelAdmissionRejectionMessage {
|
|
158
|
+
<#
|
|
159
|
+
.SYNOPSIS
|
|
160
|
+
Report whether an exception message starts with one of the four rejection codes.
|
|
161
|
+
#>
|
|
162
|
+
[CmdletBinding()]
|
|
163
|
+
[OutputType([bool])]
|
|
164
|
+
param([string] $Message)
|
|
165
|
+
|
|
166
|
+
foreach ($code in $script:AdmissionRejectionCode) {
|
|
167
|
+
if ($Message.StartsWith("${code}:", [System.StringComparison]::Ordinal)) { return $true }
|
|
168
|
+
}
|
|
169
|
+
return $false
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function Get-ParallelAdmissionUsageError {
|
|
173
|
+
<#
|
|
174
|
+
.SYNOPSIS
|
|
175
|
+
Return the usage-error text for the arguments, or null when they are usable.
|
|
176
|
+
#>
|
|
177
|
+
[CmdletBinding()]
|
|
178
|
+
[OutputType([string])]
|
|
179
|
+
param([string] $Subcommand, [hashtable] $Option, [AllowNull()][string[]] $Remaining)
|
|
180
|
+
|
|
181
|
+
if ([string]::IsNullOrWhiteSpace($Subcommand)) { return "a subcommand is required; use one of $(@($script:AdmissionRequiredOption.Keys | Sort-Object) -join ', ')." }
|
|
182
|
+
if (-not $script:AdmissionRequiredOption.ContainsKey($Subcommand)) { return "unknown subcommand '$Subcommand'." }
|
|
183
|
+
foreach ($argument in @($Remaining)) {
|
|
184
|
+
if ($null -eq $argument) { continue }
|
|
185
|
+
if ($argument.StartsWith('-')) { return "unrecognized parameter '$argument'." }
|
|
186
|
+
return "unexpected argument '$argument'."
|
|
187
|
+
}
|
|
188
|
+
foreach ($name in $script:AdmissionRequiredOption[$Subcommand]) {
|
|
189
|
+
if ([string]::IsNullOrWhiteSpace($Option[$name])) { return "$Subcommand requires -$name." }
|
|
190
|
+
}
|
|
191
|
+
# A ref value is passed to git as a bare argument, so a leading '-' would be read as a git option.
|
|
192
|
+
foreach ($name in @('Branch', 'Main', 'Ref')) {
|
|
193
|
+
if (-not [string]::IsNullOrEmpty($Option[$name]) -and $Option[$name].StartsWith('-', [System.StringComparison]::Ordinal)) { return "-$name must not begin with '-'; got '$($Option[$name])'." }
|
|
194
|
+
}
|
|
195
|
+
if (-not [string]::IsNullOrWhiteSpace($Option['Band']) -and $script:AdmissionBandName -cnotcontains $Option['Band']) {
|
|
196
|
+
return "-Band must be one of $($script:AdmissionBandName -join ', '); got '$($Option['Band'])'."
|
|
197
|
+
}
|
|
198
|
+
$floorValue = 0
|
|
199
|
+
if (-not [string]::IsNullOrWhiteSpace($Option['Floor']) -and -not ($Option['Floor'] -match '^[0-9]+$' -and [int]::TryParse($Option['Floor'], [ref] $floorValue))) {
|
|
200
|
+
return "-Floor must be a non-negative integer; got '$($Option['Floor'])'."
|
|
201
|
+
}
|
|
202
|
+
foreach ($name in @('Edges', 'Precedence')) {
|
|
203
|
+
foreach ($token in (Split-ParallelAdmissionToken -Text $Option[$name])) {
|
|
204
|
+
if ($token -notmatch '^[0-9]+:[0-9]+$') { return "-$name entries must be <key>:<key>; got '$token'." }
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
foreach ($token in (Split-ParallelAdmissionToken -Text $Option['Pinned'])) {
|
|
208
|
+
if ($token -notmatch '^[0-9]+$') { return "-Pinned entries must be integer item keys; got '$token'." }
|
|
209
|
+
}
|
|
210
|
+
if ($Subcommand -ceq 'record' -and $Option['RunFolder'] -notmatch '^docs/features/parallel/[^/\s]+/?$') {
|
|
211
|
+
return "-RunFolder must be docs/features/parallel/<slug>; got '$($Option['RunFolder'])'."
|
|
212
|
+
}
|
|
213
|
+
if (-not [string]::IsNullOrEmpty($Option['Slug']) -and $Option['Slug'] -match '[/\s]') {
|
|
214
|
+
return "-Slug must be a run slug without '/' or whitespace; got '$($Option['Slug'])'."
|
|
215
|
+
}
|
|
216
|
+
$issueValue = [long]0
|
|
217
|
+
if (-not [string]::IsNullOrEmpty($Option['IssueNum']) -and -not ($Option['IssueNum'] -match '^[0-9]+$' -and [long]::TryParse($Option['IssueNum'], [ref] $issueValue) -and $issueValue -gt 0)) {
|
|
218
|
+
return "-IssueNum must be a positive integer; got '$($Option['IssueNum'])'."
|
|
219
|
+
}
|
|
220
|
+
return $null
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function Split-ParallelAdmissionToken {
|
|
224
|
+
<#
|
|
225
|
+
.SYNOPSIS
|
|
226
|
+
Split a space- or comma-separated option value into its non-empty tokens.
|
|
227
|
+
#>
|
|
228
|
+
[CmdletBinding()]
|
|
229
|
+
[OutputType([string[]], [System.Object[]])]
|
|
230
|
+
param([AllowNull()][AllowEmptyString()][string] $Text)
|
|
231
|
+
|
|
232
|
+
if ([string]::IsNullOrWhiteSpace($Text)) { return , [string[]]@() }
|
|
233
|
+
return , [string[]]@($Text.Trim() -split '[\s,]+' | Where-Object { $_ })
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function Invoke-ParallelAdmissionGuardRun {
|
|
237
|
+
<#
|
|
238
|
+
.SYNOPSIS
|
|
239
|
+
guard-run: reject an addition to a closed run (Test-ParallelAdmissionRunOpen).
|
|
240
|
+
.DESCRIPTION
|
|
241
|
+
An unreadable or unparseable checkpoint is closed (fail closed). The
|
|
242
|
+
rejection emits run_closed and never the mutation rejection signal.
|
|
243
|
+
#>
|
|
244
|
+
[CmdletBinding()]
|
|
245
|
+
[OutputType([pscustomobject])]
|
|
246
|
+
param([hashtable] $Option)
|
|
247
|
+
|
|
248
|
+
$checkpoint = $null
|
|
249
|
+
$readError = $null
|
|
250
|
+
try {
|
|
251
|
+
$checkpoint = ConvertFrom-ParallelAdmissionJson -Text (Read-ParallelAdmissionJsonText -Path (Resolve-ParallelAdmissionPath -Path $Option.CheckpointPath))
|
|
252
|
+
} catch {
|
|
253
|
+
$readError = ($_.Exception.Message -replace "\r?\n", ' ').Trim()
|
|
254
|
+
}
|
|
255
|
+
if ($null -ne $readError) {
|
|
256
|
+
return ConvertTo-ParallelAdmissionRejection -Code 'run_closed' -Message "checkpoint $($Option.CheckpointPath) could not be read ($readError)"
|
|
257
|
+
}
|
|
258
|
+
$verdict = Test-ParallelAdmissionRunOpen -ManifestMode $Option.ManifestMode -Checkpoint $checkpoint
|
|
259
|
+
if (-not $verdict.Open) { return ConvertTo-ParallelAdmissionRejection -Code 'run_closed' -Message $verdict.Detail }
|
|
260
|
+
$checkedAt = (Get-ParallelAdmissionUtcNow).ToString($script:AdmissionTimestampFormat, [System.Globalization.CultureInfo]::InvariantCulture)
|
|
261
|
+
$result = [ordered]@{ checked_at = $checkedAt; checkpoint_mode = $verdict.CheckpointMode; manifest_mode = $verdict.ManifestMode; result = 'open' }
|
|
262
|
+
return ConvertTo-ParallelAdmissionCliOutcome -ExitCode 0 -Stdout (ConvertTo-ParallelAdmissionJson -Value $result)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function Invoke-ParallelAdmissionGitChecked {
|
|
266
|
+
<#
|
|
267
|
+
.SYNOPSIS
|
|
268
|
+
Run one git command through the seam and throw when it exits non-zero, naming the first non-blank stderr line.
|
|
269
|
+
#>
|
|
270
|
+
[CmdletBinding()]
|
|
271
|
+
[OutputType([string[]], [System.Object[]])]
|
|
272
|
+
param([string[]] $GitArgs)
|
|
273
|
+
|
|
274
|
+
$result = Invoke-ParallelAdmissionGit -GitArgs $GitArgs
|
|
275
|
+
if ($result.ExitCode -ne 0) {
|
|
276
|
+
# A result without a Stderr property (an older seam double) reads as empty; strict mode would throw on the access.
|
|
277
|
+
$stderrLine = @(if ($result.PSObject.Properties.Name -contains 'Stderr') { $result.Stderr | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } })
|
|
278
|
+
if ($stderrLine.Count -gt 0) { throw "git $($GitArgs -join ' ') exited $($result.ExitCode): $($stderrLine[0].Trim())" }
|
|
279
|
+
throw "git $($GitArgs -join ' ') exited $($result.ExitCode)."
|
|
280
|
+
}
|
|
281
|
+
return , [string[]]@($result.Output | Where-Object { $null -ne $_ })
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function Get-ParallelAdmissionBlobText {
|
|
285
|
+
<#
|
|
286
|
+
.SYNOPSIS
|
|
287
|
+
Read one file by ref (git show <ref>:<path>) and return its text with LF line breaks.
|
|
288
|
+
#>
|
|
289
|
+
[CmdletBinding()]
|
|
290
|
+
[OutputType([string])]
|
|
291
|
+
param([string] $Ref, [string] $Path)
|
|
292
|
+
|
|
293
|
+
return (Invoke-ParallelAdmissionGitChecked -GitArgs @('show', "${Ref}:$Path")) -join "`n"
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function Get-ParallelAdmissionTemplateLine {
|
|
297
|
+
<#
|
|
298
|
+
.SYNOPSIS
|
|
299
|
+
Read the spec placeholder lines (non-blank, not starting with '#', '-', '|') and plan template task lines from main.
|
|
300
|
+
#>
|
|
301
|
+
[CmdletBinding()]
|
|
302
|
+
[OutputType([pscustomobject])]
|
|
303
|
+
param([string] $Main)
|
|
304
|
+
|
|
305
|
+
$specLine = @((Get-ParallelAdmissionBlobText -Ref $Main -Path $script:AdmissionSpecTemplatePath) -split "`n" | Where-Object {
|
|
306
|
+
$trimmed = $_.Trim()
|
|
307
|
+
$trimmed -and -not ($trimmed.StartsWith('#') -or $trimmed.StartsWith('-') -or $trimmed.StartsWith('|'))
|
|
308
|
+
})
|
|
309
|
+
$taskLine = @((Get-ParallelAdmissionBlobText -Ref $Main -Path $script:AdmissionPlanTemplatePath) -split "`n" | Where-Object { $_ -match '^- \[[ x]\] \[P\d+-T\d+\]' })
|
|
310
|
+
return [pscustomobject]@{ SpecPlaceholderLine = [string[]]$specLine; PlanTemplateTaskLine = [string[]]$taskLine }
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function Get-ParallelAdmissionPlannerItem {
|
|
314
|
+
<#
|
|
315
|
+
.SYNOPSIS
|
|
316
|
+
Return the planner checkpoint item whose feature_folder is FeatureFolder, or null.
|
|
317
|
+
#>
|
|
318
|
+
[CmdletBinding()]
|
|
319
|
+
[OutputType([object])]
|
|
320
|
+
param([AllowEmptyString()][string] $Path, [string] $FeatureFolder)
|
|
321
|
+
|
|
322
|
+
if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
|
|
323
|
+
$checkpoint = Read-ParallelAdmissionDocument -Path $Path -Label 'Parallel planner checkpoint'
|
|
324
|
+
foreach ($item in @(Get-ParallelAdmissionMapValue -Map $checkpoint -Key 'items')) {
|
|
325
|
+
$folder = Get-ParallelAdmissionMapValue -Map $item -Key 'feature_folder'
|
|
326
|
+
if ($folder -is [string] -and $folder.TrimEnd('/') -ceq $FeatureFolder) { return $item }
|
|
327
|
+
}
|
|
328
|
+
return $null
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function Read-ParallelAdmissionBranchFile {
|
|
332
|
+
<#
|
|
333
|
+
.SYNOPSIS
|
|
334
|
+
Read research, spec, plan, and preflight-evidence texts, and the plan and preflight commit times.
|
|
335
|
+
#>
|
|
336
|
+
[CmdletBinding()]
|
|
337
|
+
[OutputType([pscustomobject])]
|
|
338
|
+
param([string] $Branch, [string] $FeatureFolder, [string[]] $TreePath)
|
|
339
|
+
|
|
340
|
+
$text = @{}
|
|
341
|
+
$time = @{}
|
|
342
|
+
foreach ($path in $TreePath) {
|
|
343
|
+
$relative = $path.Substring($FeatureFolder.Length + 1)
|
|
344
|
+
$isResearch = $relative.StartsWith('research/') -and $relative.EndsWith('.md')
|
|
345
|
+
$isPlan = -not $relative.Contains('/') -and $relative.StartsWith('plan.') -and $relative.EndsWith('.md')
|
|
346
|
+
$isPreflight = $relative.StartsWith('evidence/') -and $relative.Substring($relative.LastIndexOf('/') + 1).Contains('preflight')
|
|
347
|
+
if ($isResearch -or $isPlan -or $isPreflight -or $relative -ceq 'spec.md') { $text[$path] = Get-ParallelAdmissionBlobText -Ref $Branch -Path $path }
|
|
348
|
+
if ($isPlan -or $isPreflight) {
|
|
349
|
+
$stamp = [string[]](Invoke-ParallelAdmissionGitChecked -GitArgs @('log', '-1', '--format=%ct', $Branch, '--', $path))
|
|
350
|
+
if ($stamp.Count -gt 0 -and $stamp[0] -match '^[0-9]+$') { $time[$path] = [long]$stamp[0] }
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return [pscustomobject]@{ FileText = $text; CommitTime = $time }
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function Get-ParallelAdmissionAssessStaleness {
|
|
357
|
+
<#
|
|
358
|
+
.SYNOPSIS
|
|
359
|
+
Gather the git facts for the staleness verdict and apply Get-ParallelAdmissionStaleness.
|
|
360
|
+
#>
|
|
361
|
+
[CmdletBinding()]
|
|
362
|
+
[OutputType([pscustomobject])]
|
|
363
|
+
param([string] $Main, [string] $BaseCommit, [string] $PlanText, [string[]] $DeclaredPath, [string] $FeatureFolder)
|
|
364
|
+
|
|
365
|
+
$isAncestor = (Invoke-ParallelAdmissionGit -GitArgs @('merge-base', '--is-ancestor', $BaseCommit, $Main)).ExitCode -eq 0
|
|
366
|
+
$changed = Invoke-ParallelAdmissionGitChecked -GitArgs @('diff', '--name-only', '--no-renames', $BaseCommit, $Main)
|
|
367
|
+
$cited = Get-ParallelAdmissionCitedPath -PlanText $PlanText -DeclaredPath $DeclaredPath -FeatureFolder $FeatureFolder
|
|
368
|
+
$atBase = [System.Collections.Generic.List[string]]::new()
|
|
369
|
+
$atMain = [System.Collections.Generic.List[string]]::new()
|
|
370
|
+
foreach ($path in $cited) {
|
|
371
|
+
if ($path.IndexOfAny([char[]]'*?[') -ge 0) { continue }
|
|
372
|
+
if ((Invoke-ParallelAdmissionGit -GitArgs @('cat-file', '-e', "${BaseCommit}:$path")).ExitCode -eq 0) { $atBase.Add($path) }
|
|
373
|
+
if ((Invoke-ParallelAdmissionGit -GitArgs @('cat-file', '-e', "${Main}:$path")).ExitCode -eq 0) { $atMain.Add($path) }
|
|
374
|
+
}
|
|
375
|
+
return Get-ParallelAdmissionStaleness -CitedPath $cited -ChangedPath $changed -PresentAtBase $atBase.ToArray() -PresentAtMain $atMain.ToArray() -BaseIsAncestor $isAncestor -FeatureFolder $FeatureFolder
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function Invoke-ParallelAdmissionAssess {
|
|
379
|
+
<#
|
|
380
|
+
.SYNOPSIS
|
|
381
|
+
assess: report how far an item is prepared, whether main moved under its plan, and the next action.
|
|
382
|
+
.DESCRIPTION
|
|
383
|
+
Fetches origin first (git fetch --quiet origin), then reads by ref through
|
|
384
|
+
Invoke-ParallelAdmissionGit. The newest receipt
|
|
385
|
+
takes precedence over inference; plan_hash is the plan blob hash at the
|
|
386
|
+
branch head; base_commit is the receipt's when it is a 40- or 64-character hex object name, else the merge base with main.
|
|
387
|
+
#>
|
|
388
|
+
[CmdletBinding()]
|
|
389
|
+
[OutputType([pscustomobject])]
|
|
390
|
+
param([hashtable] $Option)
|
|
391
|
+
|
|
392
|
+
$branch = $Option.Branch
|
|
393
|
+
$main = $Option.Main
|
|
394
|
+
$folder = $Option.FeatureFolder.Trim().TrimEnd('/')
|
|
395
|
+
[void](Invoke-ParallelAdmissionGitChecked -GitArgs @('fetch', '--quiet', 'origin'))
|
|
396
|
+
# Seam results are parenthesized so the pipeline enumerates their lines rather than one array object.
|
|
397
|
+
$tree = [string[]]@((Invoke-ParallelAdmissionGitChecked -GitArgs @('ls-tree', '-r', '--name-only', $branch, '--', $folder)) | Where-Object { $_.StartsWith("$folder/", [System.StringComparison]::Ordinal) })
|
|
398
|
+
$template = Get-ParallelAdmissionTemplateLine -Main $main
|
|
399
|
+
$files = Read-ParallelAdmissionBranchFile -Branch $branch -FeatureFolder $folder -TreePath $tree
|
|
400
|
+
$inferred = Get-ParallelAdmissionInferredStage -TreePath $tree -FileText $files.FileText -CommitTime $files.CommitTime `
|
|
401
|
+
-SpecPlaceholderLine $template.SpecPlaceholderLine -PlanTemplateTaskLine $template.PlanTemplateTaskLine -FeatureFolder $folder
|
|
402
|
+
|
|
403
|
+
$receiptPath = if ([string]::IsNullOrWhiteSpace($Option.Receipt)) { Select-ParallelAdmissionReceipt -ReceiptPath @($tree | Where-Object { $_.StartsWith("$folder/evidence/other/", [System.StringComparison]::Ordinal) }) } else { $Option.Receipt }
|
|
404
|
+
$receipt = $null
|
|
405
|
+
if (-not [string]::IsNullOrWhiteSpace($receiptPath)) {
|
|
406
|
+
$receipt = ConvertFrom-ParallelAdmissionJson -Text (Get-ParallelAdmissionBlobText -Ref $branch -Path $receiptPath)
|
|
407
|
+
if ($receipt -isnot [System.Collections.IDictionary]) { throw "Preparation receipt $receiptPath must be a JSON object." }
|
|
408
|
+
}
|
|
409
|
+
$planPath = $inferred.plan_path
|
|
410
|
+
if ($null -eq $planPath) { $planPath = Get-ParallelAdmissionMapValue -Map $receipt -Key 'plan_path' }
|
|
411
|
+
$planHash = $null
|
|
412
|
+
if (-not [string]::IsNullOrWhiteSpace($planPath)) {
|
|
413
|
+
$hashResult = Invoke-ParallelAdmissionGit -GitArgs @('rev-parse', "${branch}:$planPath")
|
|
414
|
+
if ($hashResult.ExitCode -eq 0 -and @($hashResult.Output).Count -gt 0) { $planHash = $hashResult.Output[0].Trim() }
|
|
415
|
+
}
|
|
416
|
+
$stage = Resolve-ParallelAdmissionStage -Receipt $receipt -InferredStage $inferred.stage -CurrentPlanHash $planHash
|
|
417
|
+
$preflightCleared = $stage.stage -ceq 'cleared' -and ($stage.source -ceq 'inference' -or (Get-ParallelAdmissionMapValue -Map $receipt -Key 'preflight_status') -ceq 'PREFLIGHT: ALL CLEAR')
|
|
418
|
+
|
|
419
|
+
$baseCommit = Get-ParallelAdmissionMapValue -Map $receipt -Key 'base_commit'
|
|
420
|
+
# Only a full object name may reach git; any other value is treated as absent, so an option-shaped value is never passed.
|
|
421
|
+
if ([string]$baseCommit -cnotmatch '\A(?:[0-9a-fA-F]{40}|[0-9a-fA-F]{64})\z') { $baseCommit =[string[]](Invoke-ParallelAdmissionGitChecked -GitArgs @('merge-base', $main, $branch)) | Select-Object -First 1 }
|
|
422
|
+
$plannerItem = Get-ParallelAdmissionPlannerItem -Path $Option.PlannerCheckpointPath -FeatureFolder $folder
|
|
423
|
+
$staleness = [pscustomobject]@{ verdict = $null; changed_cited_paths = [string[]]@(); removed_cited_paths = [string[]]@() }
|
|
424
|
+
if (@('plan', 'cleared') -ccontains $stage.stage -and -not [string]::IsNullOrWhiteSpace($planPath)) {
|
|
425
|
+
$planText = if ($files.FileText.ContainsKey($planPath)) { $files.FileText[$planPath] } else { Get-ParallelAdmissionBlobText -Ref $branch -Path $planPath }
|
|
426
|
+
$declared = [string[]]@(Get-ParallelAdmissionMapValue -Map (Get-ParallelAdmissionMapValue -Map $plannerItem -Key 'blast_radius') -Key 'paths' | Where-Object { $_ -is [string] })
|
|
427
|
+
$staleness = Get-ParallelAdmissionAssessStaleness -Main $main -BaseCommit $baseCommit -PlanText $planText -DeclaredPath $declared -FeatureFolder $folder
|
|
428
|
+
}
|
|
429
|
+
$band = Resolve-ParallelAdmissionBand -CallerBand $Option.Band -PlannerBand ([string](Get-ParallelAdmissionMapValue -Map $plannerItem -Key 'complexity_band'))
|
|
430
|
+
$decision = Get-ParallelAdmissionDecision -Stage $stage.stage -Verdict $staleness.verdict -PreflightCleared $preflightCleared
|
|
431
|
+
|
|
432
|
+
$result = [ordered]@{
|
|
433
|
+
preparation_status = $stage.stage
|
|
434
|
+
resume_stage = $decision.resume_stage
|
|
435
|
+
research_path = $inferred.research_path
|
|
436
|
+
spec_path = $inferred.spec_path
|
|
437
|
+
plan_path = $planPath
|
|
438
|
+
plan_hash = $planHash
|
|
439
|
+
base_commit = $baseCommit
|
|
440
|
+
staleness = [ordered]@{ verdict = $staleness.verdict; changed_cited_paths = [object[]]@($staleness.changed_cited_paths); removed_cited_paths = [object[]]@($staleness.removed_cited_paths) }
|
|
441
|
+
band = [ordered]@{ value = $band.value; source = $band.source }
|
|
442
|
+
next_action = $decision.next_action
|
|
443
|
+
}
|
|
444
|
+
return ConvertTo-ParallelAdmissionCliOutcome -ExitCode 0 -Stdout (ConvertTo-ParallelAdmissionJson -Value $result)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function ConvertTo-ParallelAdmissionPrecedenceEntry {
|
|
448
|
+
<#
|
|
449
|
+
.SYNOPSIS
|
|
450
|
+
Turn "<before>:<after>" tokens into precedence entries.
|
|
451
|
+
#>
|
|
452
|
+
[CmdletBinding()]
|
|
453
|
+
[OutputType([System.Object[]])]
|
|
454
|
+
param([AllowNull()][AllowEmptyString()][string] $Text)
|
|
455
|
+
|
|
456
|
+
$entries = [System.Collections.Generic.List[object]]::new()
|
|
457
|
+
foreach ($token in (Split-ParallelAdmissionToken -Text $Text)) {
|
|
458
|
+
$ends = $token -split ':'
|
|
459
|
+
$entries.Add(@{ before = [long]$ends[0]; after = [long]$ends[1] })
|
|
460
|
+
}
|
|
461
|
+
return , $entries.ToArray()
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function ConvertFrom-ParallelAdmissionCohortJson {
|
|
465
|
+
<#
|
|
466
|
+
.SYNOPSIS
|
|
467
|
+
Read a cohort table from JSON: an array of key arrays, an array of cohort objects, or an object with cohorts.
|
|
468
|
+
#>
|
|
469
|
+
[CmdletBinding()]
|
|
470
|
+
[OutputType([System.Object[]])]
|
|
471
|
+
param([string] $Text)
|
|
472
|
+
|
|
473
|
+
$value = ConvertFrom-ParallelAdmissionJson -Text $Text
|
|
474
|
+
if ($value -is [System.Collections.IDictionary]) {
|
|
475
|
+
# Indexed directly: a function return would unroll a one-cohort array into its only entry.
|
|
476
|
+
$cohortList = if ($value.Contains('cohorts')) { , $value['cohorts'] } else { $null }
|
|
477
|
+
$value = if ($cohortList -is [array]) { , $cohortList } else { $null }
|
|
478
|
+
}
|
|
479
|
+
if ($value -isnot [array]) { throw '-Cohorts must be a JSON array of cohorts or an object with a cohorts array.' }
|
|
480
|
+
$table = [System.Collections.Generic.List[object]]::new()
|
|
481
|
+
foreach ($cohort in $value) {
|
|
482
|
+
$members = if ($cohort -is [System.Collections.IDictionary]) { Get-ParallelAdmissionMapValue -Map $cohort -Key 'item_keys' } else { $cohort }
|
|
483
|
+
$table.Add([object[]]@($members))
|
|
484
|
+
}
|
|
485
|
+
return , $table.ToArray()
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function Invoke-ParallelAdmissionCohortOrder {
|
|
489
|
+
<#
|
|
490
|
+
.SYNOPSIS
|
|
491
|
+
order-cohorts: apply the precedence lift to a computed cohort table.
|
|
492
|
+
#>
|
|
493
|
+
[CmdletBinding()]
|
|
494
|
+
[OutputType([pscustomobject])]
|
|
495
|
+
param([hashtable] $Option)
|
|
496
|
+
|
|
497
|
+
$entries = ConvertTo-ParallelAdmissionPrecedenceEntry -Text $Option.Precedence
|
|
498
|
+
$floorValue = if ([string]::IsNullOrWhiteSpace($Option.Floor)) { 0 } else { [int]$Option.Floor }
|
|
499
|
+
$pinnedKey = [long[]]@((Split-ParallelAdmissionToken -Text $Option.Pinned) | ForEach-Object { [long]$_ })
|
|
500
|
+
$table = Invoke-ParallelPrecedenceLift -Cohort (ConvertFrom-ParallelAdmissionCohortJson -Text $Option.Cohorts) -Edge (Split-ParallelAdmissionToken -Text $Option.Edges) `
|
|
501
|
+
-Precedence $entries -Pinned $pinnedKey -Floor $floorValue
|
|
502
|
+
$result = [ordered]@{ cohorts = [object[]]$table; precedence_edges = [object[]](ConvertTo-ParallelPrecedenceEdge -Entry $entries) }
|
|
503
|
+
return ConvertTo-ParallelAdmissionCliOutcome -ExitCode 0 -Stdout (ConvertTo-ParallelAdmissionJson -Value $result)
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function Invoke-ParallelAdmissionCohortTest {
|
|
507
|
+
<#
|
|
508
|
+
.SYNOPSIS
|
|
509
|
+
test-cohorts: reject a current-generation cohort table that violates an active precedence entry.
|
|
510
|
+
#>
|
|
511
|
+
[CmdletBinding()]
|
|
512
|
+
[OutputType([pscustomobject])]
|
|
513
|
+
param([hashtable] $Option)
|
|
514
|
+
|
|
515
|
+
$checkpoint = Read-ParallelAdmissionDocument -Path $Option.CheckpointPath -Label 'Parallel checkpoint'
|
|
516
|
+
$check = Test-ParallelPrecedenceCohortTable -Checkpoint $checkpoint
|
|
517
|
+
if (-not $check.Satisfied) { return ConvertTo-ParallelAdmissionRejection -Code 'precedence_violated' -Message ($check.Violation -join '; ') }
|
|
518
|
+
$result = [ordered]@{ active_precedence = [object[]]@($check.Active); result = 'satisfied' }
|
|
519
|
+
return ConvertTo-ParallelAdmissionCliOutcome -ExitCode 0 -Stdout (ConvertTo-ParallelAdmissionJson -Value $result)
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function Invoke-ParallelAdmissionRecord {
|
|
523
|
+
<#
|
|
524
|
+
.SYNOPSIS
|
|
525
|
+
record: print the parallel-scheduling.json content; the caller writes and commits it, so no git command runs.
|
|
526
|
+
#>
|
|
527
|
+
[CmdletBinding()]
|
|
528
|
+
[OutputType([pscustomobject])]
|
|
529
|
+
param([hashtable] $Option)
|
|
530
|
+
|
|
531
|
+
$slug = $Option.RunFolder.TrimEnd('/').Substring('docs/features/parallel/'.Length)
|
|
532
|
+
$entry = ConvertFrom-ParallelAdmissionJson -Text $Option.Entry
|
|
533
|
+
if ($entry -isnot [System.Collections.IDictionary]) { throw '-Entry must be a JSON object.' }
|
|
534
|
+
$record = New-ParallelSchedulingRecord -Slug $slug -Edge @(Get-ParallelAdmissionMapValue -Map $entry -Key 'edges') `
|
|
535
|
+
-ToleratedOverlap @(Get-ParallelAdmissionMapValue -Map $entry -Key 'tolerated_overlaps') `
|
|
536
|
+
-Precedence @(Get-ParallelAdmissionMapValue -Map $entry -Key 'precedence') -Item @(Get-ParallelAdmissionMapValue -Map $entry -Key 'items')
|
|
537
|
+
return ConvertTo-ParallelAdmissionCliOutcome -ExitCode 0 -Stdout (ConvertTo-ParallelAdmissionJson -Value $record)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
function Get-ParallelAdmissionRecordedEdge {
|
|
541
|
+
<#
|
|
542
|
+
.SYNOPSIS
|
|
543
|
+
Read a run's committed scheduling record by ref and apply the edge-reuse rule for one item.
|
|
544
|
+
.DESCRIPTION
|
|
545
|
+
Reads the record from origin/parallel/<slug>-plan (or -Ref) through the git
|
|
546
|
+
seam; a missing record gives a recompute marker with reason record_missing.
|
|
547
|
+
#>
|
|
548
|
+
[CmdletBinding()]
|
|
549
|
+
[OutputType([pscustomobject])]
|
|
550
|
+
param([string] $Slug, [long] $IssueNum, [AllowEmptyString()][string] $CurrentPlanHash, [string] $Ref = '')
|
|
551
|
+
|
|
552
|
+
if ([string]::IsNullOrWhiteSpace($Ref)) { $Ref = "origin/parallel/$Slug-plan" }
|
|
553
|
+
$recordPath = "docs/features/parallel/$Slug/parallel-scheduling.json"
|
|
554
|
+
$shown = Invoke-ParallelAdmissionGit -GitArgs @('show', "${Ref}:$recordPath")
|
|
555
|
+
if ($shown.ExitCode -ne 0) { return [pscustomobject]@{ action = 'recompute'; reason = 'record_missing'; edges = [object[]]@() } }
|
|
556
|
+
$record = ConvertFrom-ParallelAdmissionJson -Text (@($shown.Output) -join "`n")
|
|
557
|
+
if ($record -isnot [System.Collections.IDictionary]) { throw "Scheduling record ${Ref}:$recordPath must be a JSON object." }
|
|
558
|
+
return Get-ParallelSchedulingRecordReuse -Record $record -IssueNum $IssueNum -CurrentPlanHash $CurrentPlanHash
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function Invoke-ParallelAdmissionEdgeReuse {
|
|
562
|
+
<#
|
|
563
|
+
.SYNOPSIS
|
|
564
|
+
reuse-edges: apply the edge-reuse rule to one item from the committed scheduling record; no fetch runs.
|
|
565
|
+
#>
|
|
566
|
+
[CmdletBinding()]
|
|
567
|
+
[OutputType([pscustomobject])]
|
|
568
|
+
param([hashtable] $Option)
|
|
569
|
+
|
|
570
|
+
$reuse = Get-ParallelAdmissionRecordedEdge -Slug $Option.Slug -IssueNum ([long]$Option.IssueNum) -CurrentPlanHash $Option.PlanHash -Ref ([string]$Option.Ref)
|
|
571
|
+
$result = [ordered]@{ action = $reuse.action; edges = [object[]]@($reuse.edges); reason = $reuse.reason }
|
|
572
|
+
return ConvertTo-ParallelAdmissionCliOutcome -ExitCode 0 -Stdout (ConvertTo-ParallelAdmissionJson -Value $result)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function Invoke-ParallelAdmissionCli {
|
|
576
|
+
<#
|
|
577
|
+
.SYNOPSIS
|
|
578
|
+
Validate the arguments, run one subcommand, and return ExitCode, Stdout, and Stderr.
|
|
579
|
+
.DESCRIPTION
|
|
580
|
+
Usage errors are reported before any read. A rule rejection becomes exit 1
|
|
581
|
+
with the rejection line; any other read or data error exit 1 with the failure prefix.
|
|
582
|
+
#>
|
|
583
|
+
[CmdletBinding()]
|
|
584
|
+
[OutputType([pscustomobject])]
|
|
585
|
+
param(
|
|
586
|
+
[string] $Subcommand, [string] $CheckpointPath, [string] $ManifestMode, [string] $Branch, [string] $FeatureFolder,
|
|
587
|
+
[string] $Receipt, [string] $Band, [string] $PlannerCheckpointPath, [string] $Main, [string] $Cohorts, [string] $Edges,
|
|
588
|
+
[string] $Precedence, [string] $Pinned, [string] $Floor, [string] $RunFolder, [string] $Entry,
|
|
589
|
+
[string] $Slug, [string] $IssueNum, [string] $PlanHash, [string] $Ref, [AllowNull()][string[]] $Remaining
|
|
590
|
+
)
|
|
591
|
+
|
|
592
|
+
$option = @{ CheckpointPath = $CheckpointPath; ManifestMode = $ManifestMode; Branch = $Branch; FeatureFolder = $FeatureFolder; Receipt = $Receipt; Band = $Band
|
|
593
|
+
PlannerCheckpointPath = $PlannerCheckpointPath; Main = $Main; Cohorts = $Cohorts; Edges = $Edges; Precedence = $Precedence; Pinned = $Pinned; Floor = $Floor; RunFolder = $RunFolder; Entry = $Entry
|
|
594
|
+
Slug = $Slug; IssueNum = $IssueNum; PlanHash = $PlanHash; Ref = $Ref }
|
|
595
|
+
$usage = Get-ParallelAdmissionUsageError -Subcommand $Subcommand -Option $option -Remaining $Remaining
|
|
596
|
+
if ($null -ne $usage) { return ConvertTo-ParallelAdmissionCliOutcome -ExitCode 2 -Stderr "$($script:AdmissionUsagePrefix)$usage" }
|
|
597
|
+
if ([string]::IsNullOrWhiteSpace($option.Main)) { $option.Main = $script:AdmissionDefaultMain }
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
switch ($Subcommand) {
|
|
601
|
+
'guard-run' { return Invoke-ParallelAdmissionGuardRun -Option $option }
|
|
602
|
+
'assess' { return Invoke-ParallelAdmissionAssess -Option $option }
|
|
603
|
+
'order-cohorts' { return Invoke-ParallelAdmissionCohortOrder -Option $option }
|
|
604
|
+
'test-cohorts' { return Invoke-ParallelAdmissionCohortTest -Option $option }
|
|
605
|
+
'record' { return Invoke-ParallelAdmissionRecord -Option $option }
|
|
606
|
+
'reuse-edges' { return Invoke-ParallelAdmissionEdgeReuse -Option $option }
|
|
607
|
+
}
|
|
608
|
+
} catch {
|
|
609
|
+
$message = $_.Exception.Message
|
|
610
|
+
if (Test-ParallelAdmissionRejectionMessage -Message $message) { return ConvertTo-ParallelAdmissionRejection -Message $message }
|
|
611
|
+
return ConvertTo-ParallelAdmissionCliOutcome -ExitCode 1 -Stderr "$($script:AdmissionFailurePrefix)$(($message -replace "\r?\n", ' ').Trim())"
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
# Dot-sourcing loads the functions for a test host; any other invocation runs
|
|
616
|
+
# the subcommand. Parameters are passed by name rather than by splatting
|
|
617
|
+
# $PSBoundParameters, which under CmdletBinding can carry common parameters.
|
|
618
|
+
if ($MyInvocation.InvocationName -ne '.') {
|
|
619
|
+
$outcome = Invoke-ParallelAdmissionCli -Subcommand $Subcommand -CheckpointPath $CheckpointPath -ManifestMode $ManifestMode `
|
|
620
|
+
-Branch $Branch -FeatureFolder $FeatureFolder -Receipt $Receipt -Band $Band -PlannerCheckpointPath $PlannerCheckpointPath `
|
|
621
|
+
-Main $Main -Cohorts $Cohorts -Edges $Edges -Precedence $Precedence -Pinned $Pinned -Floor $Floor -RunFolder $RunFolder `
|
|
622
|
+
-Entry $Entry -Slug $Slug -IssueNum $IssueNum -PlanHash $PlanHash -Ref $Ref -Remaining $Remaining
|
|
623
|
+
if (-not [string]::IsNullOrEmpty($outcome.Stdout)) { [Console]::Out.WriteLine($outcome.Stdout) }
|
|
624
|
+
if (-not [string]::IsNullOrEmpty($outcome.Stderr)) { [Console]::Error.WriteLine($outcome.Stderr) }
|
|
625
|
+
exit $outcome.ExitCode
|
|
626
|
+
}
|