@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
@@ -3,14 +3,15 @@
3
3
  Dot-sourced record-resolution and barrier helpers for enforce-parallel-cohort-barrier.ps1.
4
4
 
5
5
  .DESCRIPTION
6
- Holds the five resolution helpers the Layer 1 parallel cohort barrier uses once a
6
+ Holds the six resolution helpers the Layer 1 parallel cohort barrier uses once a
7
7
  delegation has been identified as a parallel-mode orchestrator call:
8
8
 
9
9
  - Find-ParallelCohortBarrierItemRecord: resolves the items[] record for a folder.
10
10
  - Find-ParallelCohortBarrierItemByKey: resolves an items[] record by issue_num.
11
11
  - Find-ParallelCohortBarrierCohortIndex: projects the current-generation coloring.
12
12
  - Get-ParallelCohortBarrierConflictNeighborList: collects conflict-edge neighbours.
13
- - Test-ParallelCohortBarrierClear: applies the per-edge barrier rule.
13
+ - Get-ParallelCohortBarrierPrecedencePredecessorList: collects precedence[] predecessors (issue #1047).
14
+ - Test-ParallelCohortBarrierClear: applies the per-edge barrier rule and the precedence rule.
14
15
 
15
16
  They are split out of the parent hook, .claude/hooks/enforce-parallel-cohort-barrier.ps1,
16
17
  because that file stood at 499 lines -- one below the 500-line limit -- with no room
@@ -215,11 +216,60 @@ function Get-ParallelCohortBarrierConflictNeighborList {
215
216
  return $neighborList
216
217
  }
217
218
 
219
+ function Get-ParallelCohortBarrierPrecedencePredecessorList {
220
+ <#
221
+ .SYNOPSIS
222
+ Return the before keys of every precedence[] entry whose after equals the supplied item key.
223
+ .DESCRIPTION
224
+ precedence[] is the top-level operator ordering input of issue #1047
225
+ ({before, after, source, recorded_at}); it is a separating input, not a
226
+ depends_on key. An absent precedence key yields an empty list.
227
+ .PARAMETER Checkpoint
228
+ Parsed parallel checkpoint, or $null when absent/unreadable.
229
+ .PARAMETER IssueKey
230
+ The item primary key (issue_num) rendered as a string.
231
+ .OUTPUTS
232
+ System.Object[] holding each predecessor item key rendered as a string.
233
+ #>
234
+ [CmdletBinding()]
235
+ [OutputType([object[]])]
236
+ param(
237
+ [AllowNull()]
238
+ $Checkpoint,
239
+
240
+ [AllowNull()]
241
+ [string] $IssueKey
242
+ )
243
+
244
+ $predecessorList = @()
245
+ if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($IssueKey)) {
246
+ return $predecessorList
247
+ }
248
+ if (@($Checkpoint.PSObject.Properties.Name) -notcontains 'precedence') {
249
+ return $predecessorList
250
+ }
251
+ foreach ($entry in @($Checkpoint.precedence)) {
252
+ if ($null -eq $entry) {
253
+ continue
254
+ }
255
+ $entryProps = @($entry.PSObject.Properties.Name)
256
+ if ($entryProps -notcontains 'before' -or $entryProps -notcontains 'after') {
257
+ continue
258
+ }
259
+ if (([string]$entry.after) -eq $IssueKey) {
260
+ $predecessorList += [string]$entry.before
261
+ }
262
+ }
263
+ return $predecessorList
264
+ }
265
+
218
266
  function Test-ParallelCohortBarrierClear {
219
267
  <#
220
268
  .SYNOPSIS
221
269
  Decision logic: true only when every conflicting neighbor in a strictly prior
222
- current-generation cohort has merge_status merged or worktree_removed.
270
+ current-generation cohort has merge_status merged or worktree_removed, and every
271
+ precedence[] predecessor has merge_status merged or worktree_removed or state
272
+ withdrawn (a blocked predecessor holds; a missing predecessor record fails closed).
223
273
  .PARAMETER Checkpoint
224
274
  Parsed parallel checkpoint, or $null when absent/unreadable.
225
275
  .PARAMETER ItemRecord
@@ -274,5 +324,21 @@ function Test-ParallelCohortBarrierClear {
274
324
  return $false
275
325
  }
276
326
  }
327
+
328
+ # Precedence (issue #1047): the target starts only after each predecessor has merged or
329
+ # been withdrawn, whatever its cohort. An absent precedence key leaves the decision unchanged.
330
+ foreach ($predecessorKey in @(Get-ParallelCohortBarrierPrecedencePredecessorList -Checkpoint $Checkpoint -IssueKey $targetKey)) {
331
+ $predecessorRecord = Find-ParallelCohortBarrierItemByKey -Checkpoint $Checkpoint -IssueKey $predecessorKey
332
+ if ($null -eq $predecessorRecord) {
333
+ return $false
334
+ }
335
+ $predecessorProps = @($predecessorRecord.PSObject.Properties.Name)
336
+ if ($predecessorProps -contains 'state' -and ([string]$predecessorRecord.state) -eq 'withdrawn') {
337
+ continue
338
+ }
339
+ if ($predecessorProps -notcontains 'merge_status' -or $script:AllowedMergeStatuses -notcontains ([string]$predecessorRecord.merge_status)) {
340
+ return $false
341
+ }
342
+ }
277
343
  return $true
278
344
  }
@@ -374,7 +374,7 @@ function Get-PrAuthorBypassReason {
374
374
  is resolved once (issue #850), and the deny precedence is: the resolution reason
375
375
  (no-target or ambiguity code, or an underivable epic worktree), PR_CONTEXT_MISSING when the
376
376
  context summary is absent beneath the resolved worktree, ORCHESTRATOR_STATE_PREFLIGHT_FAILED
377
- unless Get-PrAuthorDocsOnlyExemption exempts a docs-only gh pr create (issue #901), then
377
+ unless Get-PrAuthorPreflightExemption exempts a docs-only gh pr create (issue #901), then
378
378
  receipt checks 1-6 via Test-PrAuthorReceiptVerification. Returns $null for all allowed
379
379
  patterns.
380
380
  .PARAMETER CommandText
@@ -482,7 +482,7 @@ function Get-PrAuthorBypassReason {
482
482
  } else {
483
483
  $preflightResult.ErrorText
484
484
  }
485
- $exemption = Get-PrAuthorDocsOnlyExemption -CommandText $CommandText -WorktreeRoot $artifact.ArtifactRoot -CheckpointPath $checkpointPath
485
+ $exemption = Get-PrAuthorPreflightExemption -CommandText $CommandText -WorktreeRoot $artifact.ArtifactRoot -CheckpointPath $checkpointPath
486
486
  if (-not $exemption.Exempt) {
487
487
  return "ORCHESTRATOR_STATE_PREFLIGHT_FAILED: $preflightSummary (docs-only exemption not applied: $($exemption.Reason))"
488
488
  }
@@ -0,0 +1,246 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Cleanup exemption from the checkpoint preflight of enforce-pr-author-skill.ps1 (issue #1026).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by enforce-pr-author-skill.docs-only.ps1. Get-PrAuthorBypassReason calls
7
+ Get-PrAuthorPreflightExemption, which has the parameters and the return shape of
8
+ Get-PrAuthorDocsOnlyExemption, only in its non-epic branch and only after
9
+ Invoke-OrchestratorStatePreflight reports errors. The wrapper evaluates the docs-only
10
+ exemption first and returns its result unchanged when it is exempt, or when the matched
11
+ gh pr create or gh pr edit segment names no consolidation branch: no token equals
12
+ documentationandmemories or ends with '=documentationandmemories' or
13
+ ':documentationandmemories' (ordinal). Inside that cleanup context the cleanup exemption is
14
+ evaluated, and the first failed condition names the sub-code appended to the docs-only
15
+ reason after '; cleanup exemption not applied: '.
16
+
17
+ K1 CLEANUP_EXEMPTION_NOT_CREATE - the command is a structural gh pr create.
18
+ K2 CLEANUP_EXEMPTION_CHECKPOINT_PRESENT - no checkpoint file exists at the resolved path.
19
+ K3 CLEANUP_EXEMPTION_HEAD_UNRESOLVED - exactly one --head token, its value ordinal-equal
20
+ to documentationandmemories, with no --head= form
21
+ and no single-dash token containing H.
22
+ K4 CLEANUP_EXEMPTION_BASE_UNRESOLVED - Get-PrAuthorDocsOnlyBaseName returns exactly main.
23
+ K5 CLEANUP_EXEMPTION_MANIFEST_INVALID - the session-root manifest read through
24
+ Get-CleanupWorktreeManifestContent passes
25
+ Test-CleanupManifestText with the
26
+ Get-CleanupWorktreeManifestUtcNow clock.
27
+ K6 CLEANUP_EXEMPTION_REMOTE_HEAD_MISMATCH - refs/remotes/origin/documentationandmemories and
28
+ HEAD resolve to the same commit
29
+ (Get-PrAuthorCleanupPublishedHead), so the push
30
+ precedes gh pr create.
31
+ CLEANUP_EXEMPTION_DIFF_FAILED - refs/remotes/origin/main resolves to a commit and
32
+ git diff <commit>...HEAD exits 0 with output that
33
+ ends with NUL when non-empty.
34
+ CLEANUP_EXEMPTION_EMPTY_DIFF - the diff lists at least one path.
35
+ CLEANUP_EXEMPTION_NON_ALLOWLISTED_PATH - every path is under docs/ or .claude/agent-memory/
36
+ (Test-CleanupConsolidationRelativePath).
37
+
38
+ The receipt checks run unchanged after the exemption. A pull request that names no
39
+ consolidation branch receives the docs-only result unchanged, so its denial text is the
40
+ text it is today.
41
+
42
+ .NOTES
43
+ PowerShell 7+. Call-time dependencies from the docs-only sibling and the parsers the helpers
44
+ load: Get-PrAuthorDocsOnlyExemption, Test-PrAuthorDocsOnlyCheckpointPresence,
45
+ Get-PrAuthorDocsOnlyBaseName, Invoke-PrAuthorDocsOnlyGitExe, ConvertFrom-DocsOnlyNameList,
46
+ Test-CommandLineInvocation, and Get-CommandLineStructuralMatch. git runs only through
47
+ Invoke-PrAuthorDocsOnlyGitExe, and every git call is wrapped so a throwing seam denies.
48
+ Mirrored byte-identically under extensions/drm-copilot/resources/claude-customizations/.
49
+ #>
50
+ [CmdletBinding()]
51
+ param()
52
+ try { . (Join-Path $PSScriptRoot 'hook-cleanup-context.ps1') } catch { Add-HookDependencyFailure -Name 'hook-cleanup-context.ps1' -ErrorRecord $_ }
53
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeManifest.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'CleanupWorktreeManifest.psm1' -ErrorRecord $_ }
54
+
55
+ function Get-PrAuthorCleanupSegmentToken {
56
+ <#
57
+ .SYNOPSIS
58
+ Return the tokens of the matched gh pr create segment, else of gh pr edit, or an empty array.
59
+ .OUTPUTS
60
+ System.String[]
61
+ #>
62
+ [CmdletBinding()]
63
+ [OutputType([string[]])]
64
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
65
+
66
+ foreach ($subcommand in @('create', 'edit')) {
67
+ $match = Get-CommandLineStructuralMatch -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', $subcommand)
68
+ if ($null -ne $match) {
69
+ return [string[]]@($match.Segment.Tokens | ForEach-Object { [string]$_ })
70
+ }
71
+ }
72
+ return [string[]]@()
73
+ }
74
+
75
+ function Test-PrAuthorCleanupContextCommand {
76
+ <#
77
+ .SYNOPSIS
78
+ Return $true when the matched gh pr segment names the consolidation branch.
79
+ .OUTPUTS
80
+ System.Boolean
81
+ #>
82
+ [CmdletBinding()]
83
+ [OutputType([bool])]
84
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
85
+
86
+ $branch = $script:CleanupContextBranch
87
+ foreach ($token in @(Get-PrAuthorCleanupSegmentToken -CommandText $CommandText)) {
88
+ if ($token -ceq $branch -or $token.EndsWith("=$branch", [System.StringComparison]::Ordinal) -or $token.EndsWith(":$branch", [System.StringComparison]::Ordinal)) {
89
+ return $true
90
+ }
91
+ }
92
+ return $false
93
+ }
94
+
95
+ function Get-PrAuthorCleanupPublishedHead {
96
+ <#
97
+ .SYNOPSIS
98
+ Resolve the published consolidation branch and HEAD of a worktree (test seam).
99
+ .DESCRIPTION
100
+ Runs git rev-parse --verify --quiet <ref>^{commit} through Invoke-PrAuthorDocsOnlyGitExe
101
+ for refs/remotes/origin/<consolidation branch> and for HEAD. Each value is the printed
102
+ commit, or an empty string when the call throws, exits non-zero, or prints a value that
103
+ is not a 40- or 64-digit lowercase hex commit. Never throws. Tests mock this function.
104
+ .PARAMETER WorktreeRoot
105
+ The consolidation worktree root.
106
+ .OUTPUTS
107
+ System.Management.Automation.PSCustomObject with Remote and Head.
108
+ #>
109
+ [CmdletBinding()]
110
+ [OutputType([pscustomobject])]
111
+ param([AllowNull()][AllowEmptyString()][string] $WorktreeRoot)
112
+
113
+ $resolved = foreach ($ref in @("refs/remotes/origin/$($script:CleanupContextBranch)", 'HEAD')) {
114
+ try {
115
+ $result = Invoke-PrAuthorDocsOnlyGitExe -WorktreeRoot $WorktreeRoot -GitArgs @('rev-parse', '--verify', '--quiet', ($ref + '^{commit}'))
116
+ }
117
+ catch {
118
+ $result = $null
119
+ }
120
+ $value = if ($null -ne $result -and $result.ExitCode -eq 0) { ([string]$result.Output).Trim() } else { '' }
121
+ if ($value -cmatch '^[0-9a-f]{40}([0-9a-f]{24})?$') { $value } else { '' }
122
+ }
123
+ return [pscustomobject]@{ Remote = [string]$resolved[0]; Head = [string]$resolved[1] }
124
+ }
125
+
126
+ function Get-PrAuthorCleanupExemptionFailure {
127
+ <#
128
+ .SYNOPSIS
129
+ Evaluate K1 to K6 of the file header and return the first failure text, or $null.
130
+ .OUTPUTS
131
+ System.String or $null
132
+ #>
133
+ [CmdletBinding()]
134
+ [OutputType([string])]
135
+ param(
136
+ [Parameter(Mandatory)][string] $CommandText,
137
+ [AllowNull()][AllowEmptyString()][string] $WorktreeRoot,
138
+ [AllowNull()][AllowEmptyString()][string] $CheckpointPath
139
+ )
140
+
141
+ $branch = $script:CleanupContextBranch
142
+ if (-not (Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create'))) {
143
+ return 'CLEANUP_EXEMPTION_NOT_CREATE: only gh pr create is eligible.'
144
+ }
145
+ if (Test-PrAuthorDocsOnlyCheckpointPresence -CheckpointPath $CheckpointPath) {
146
+ return "CLEANUP_EXEMPTION_CHECKPOINT_PRESENT: a checkpoint file exists at '$CheckpointPath', so its preflight governs."
147
+ }
148
+ $tokens = @(Get-PrAuthorCleanupSegmentToken -CommandText $CommandText)
149
+ $headIndex = @(for ($index = 0; $index -lt $tokens.Count; $index++) { if ($tokens[$index] -ceq '--head') { $index } })
150
+ $headVariant = @($tokens | Where-Object { $_.StartsWith('--head=', [System.StringComparison]::Ordinal) -or ($_ -cmatch '^-[^-]' -and $_.Substring(1).Contains('H')) })
151
+ if ($headIndex.Count -ne 1 -or $headVariant.Count -gt 0 -or $headIndex[0] + 1 -ge $tokens.Count -or $tokens[$headIndex[0] + 1] -cne $branch) {
152
+ return "CLEANUP_EXEMPTION_HEAD_UNRESOLVED: pass exactly one explicit --head $branch; -H, --head=, and a repeated --head are not accepted."
153
+ }
154
+ if ((Get-PrAuthorDocsOnlyBaseName -CommandText $CommandText) -cne 'main') {
155
+ return 'CLEANUP_EXEMPTION_BASE_UNRESOLVED: pass exactly one explicit --base main and no base or repository override.'
156
+ }
157
+ try {
158
+ $manifestValid = Test-CleanupManifestText -Raw (Get-CleanupWorktreeManifestContent) -UtcNow (Get-CleanupWorktreeManifestUtcNow)
159
+ }
160
+ catch {
161
+ $manifestValid = $false
162
+ }
163
+ if (-not $manifestValid) {
164
+ return "CLEANUP_EXEMPTION_MANIFEST_INVALID: the session-root manifest $($script:CleanupContextManifestRelativePath) is absent, invalid, older than $($script:CleanupContextFreshnessHours) hours, in the future, or has no run_id."
165
+ }
166
+
167
+ try {
168
+ $published = Get-PrAuthorCleanupPublishedHead -WorktreeRoot $WorktreeRoot
169
+ }
170
+ catch {
171
+ $published = $null
172
+ }
173
+ if ($null -eq $published -or [string]::IsNullOrEmpty([string]$published.Remote)) {
174
+ return "CLEANUP_EXEMPTION_REMOTE_HEAD_MISMATCH: refs/remotes/origin/$branch does not resolve to a commit in the consolidation worktree; push the branch before gh pr create."
175
+ }
176
+ if ([string]::IsNullOrEmpty([string]$published.Head) -or [string]$published.Remote -cne [string]$published.Head) {
177
+ return "CLEANUP_EXEMPTION_REMOTE_HEAD_MISMATCH: refs/remotes/origin/$branch differs from the consolidation worktree HEAD; push the branch again before gh pr create."
178
+ }
179
+
180
+ try {
181
+ $revParse = Invoke-PrAuthorDocsOnlyGitExe -WorktreeRoot $WorktreeRoot -GitArgs @('rev-parse', '--verify', '--quiet', 'refs/remotes/origin/main^{commit}')
182
+ }
183
+ catch {
184
+ $revParse = $null
185
+ }
186
+ $commit = if ($null -ne $revParse) { ([string]$revParse.Output).Trim() } else { '' }
187
+ if ($null -eq $revParse -or $revParse.ExitCode -ne 0 -or $commit -cnotmatch '^[0-9a-f]{40}([0-9a-f]{24})?$') {
188
+ return 'CLEANUP_EXEMPTION_DIFF_FAILED: refs/remotes/origin/main does not resolve to a commit in the consolidation worktree.'
189
+ }
190
+ try {
191
+ $diff = Invoke-PrAuthorDocsOnlyGitExe -WorktreeRoot $WorktreeRoot -GitArgs @('diff', '--name-only', '-z', '--no-renames', '--no-color', '--no-ext-diff', '--no-relative', ($commit + '...HEAD'))
192
+ }
193
+ catch {
194
+ $diff = $null
195
+ }
196
+ $text = if ($null -ne $diff) { [string]$diff.Output } else { '' }
197
+ if ($null -eq $diff -or $diff.ExitCode -ne 0 -or ($text.Length -gt 0 -and -not $text.EndsWith([char]0))) {
198
+ return 'CLEANUP_EXEMPTION_DIFF_FAILED: git diff against origin/main could not be computed or did not return NUL-terminated output.'
199
+ }
200
+ $paths = @(ConvertFrom-DocsOnlyNameList -Text $text)
201
+ if ($paths.Count -eq 0) {
202
+ return 'CLEANUP_EXEMPTION_EMPTY_DIFF: the branch diff lists no path.'
203
+ }
204
+ foreach ($path in $paths) {
205
+ if (-not (Test-CleanupConsolidationRelativePath -RelativePath $path)) {
206
+ return "CLEANUP_EXEMPTION_NON_ALLOWLISTED_PATH: '$path' is outside docs/ and .claude/agent-memory/."
207
+ }
208
+ }
209
+ return $null
210
+ }
211
+
212
+ function Get-PrAuthorPreflightExemption {
213
+ <#
214
+ .SYNOPSIS
215
+ Decide whether a failed preflight is exempt as docs-only or as a cleanup consolidation PR.
216
+ .DESCRIPTION
217
+ Returns the docs-only result unchanged when it is exempt or when the command names no
218
+ consolidation branch. Otherwise evaluates the cleanup exemption and, on failure, returns
219
+ the docs-only reason followed by '; cleanup exemption not applied: ' and the sub-code.
220
+ .PARAMETER CommandText
221
+ The Bash command text under evaluation.
222
+ .PARAMETER WorktreeRoot
223
+ The resolved worktree root the diff is computed in.
224
+ .PARAMETER CheckpointPath
225
+ The resolved checkpoint path whose absence both exemptions require.
226
+ .OUTPUTS
227
+ System.Collections.Specialized.OrderedDictionary with Exempt and Reason.
228
+ #>
229
+ [CmdletBinding()]
230
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
231
+ param(
232
+ [Parameter(Mandatory)][string] $CommandText,
233
+ [AllowNull()][AllowEmptyString()][string] $WorktreeRoot,
234
+ [AllowNull()][AllowEmptyString()][string] $CheckpointPath
235
+ )
236
+
237
+ $docsOnly = Get-PrAuthorDocsOnlyExemption -CommandText $CommandText -WorktreeRoot $WorktreeRoot -CheckpointPath $CheckpointPath
238
+ if ($docsOnly.Exempt -or -not (Test-PrAuthorCleanupContextCommand -CommandText $CommandText)) {
239
+ return $docsOnly
240
+ }
241
+ $failure = Get-PrAuthorCleanupExemptionFailure -CommandText $CommandText -WorktreeRoot $WorktreeRoot -CheckpointPath $CheckpointPath
242
+ if ($null -eq $failure) {
243
+ return [ordered]@{ Exempt = $true; Reason = $null }
244
+ }
245
+ return [ordered]@{ Exempt = $false; Reason = "$($docsOnly.Reason); cleanup exemption not applied: $failure" }
246
+ }
@@ -42,6 +42,7 @@
42
42
  [CmdletBinding()]
43
43
  param()
44
44
  . (Join-Path $PSScriptRoot 'hook-docs-only-diff.ps1')
45
+ try { . (Join-Path $PSScriptRoot 'enforce-pr-author-skill.cleanup.ps1') } catch { Add-HookDependencyFailure -Name 'enforce-pr-author-skill.cleanup.ps1' -ErrorRecord $_ }
45
46
 
46
47
  function Invoke-PrAuthorDocsOnlyGitExe {
47
48
  <#