@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.
Files changed (36) hide show
  1. package/package.json +1 -1
  2. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +7 -3
  3. package/resources/claude-customizations/.claude/agents/parallel-planner.md +6 -2
  4. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +41 -0
  5. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
  6. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +3 -1
  7. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +69 -3
  8. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +2 -2
  9. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.cleanup.ps1 +246 -0
  10. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +1 -0
  11. package/resources/claude-customizations/.claude/hooks/hook-cleanup-context.ps1 +343 -0
  12. package/resources/claude-customizations/.claude/lib/parallel-admission/Invoke-ParallelAdmission.ps1 +626 -0
  13. package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmission.psm1 +508 -0
  14. package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionJson.psm1 +141 -0
  15. package/resources/claude-customizations/.claude/lib/parallel-admission/ParallelAdmissionPrecedence.psm1 +508 -0
  16. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +17 -1
  17. package/resources/claude-customizations/.claude/settings.json +1 -0
  18. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +1 -1
  19. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +93 -14
  20. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +23 -10
  21. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +54 -46
  22. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_merge_lib.sh +234 -0
  23. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +15 -5
  24. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +71 -7
  25. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +26 -2
  26. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +82 -12
  27. package/resources/claude-customizations/pack-manifests/core.json +8 -0
  28. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +15 -5
  29. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  30. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-cleanup.ps1 +481 -0
  31. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +4 -2
  32. package/resources/codex-and-agents-customizations/.codex/hooks/hook-cleanup-context.ps1 +343 -0
  33. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  34. package/resources/customizations/.github/skills/execution-environment/SKILL.md +15 -5
  35. package/resources/powershell/PoshQC/PoshQC.ScanConfig.psm1 +3 -1
  36. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -10
@@ -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
+ }