@danmoisan/drm-copilot-mcp 1.0.27 → 1.1.1

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 (42) hide show
  1. package/out/mcp-server.js +0 -1
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
  4. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
  5. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
  6. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
  7. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +64 -20
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
  12. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
  13. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
  14. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +36 -24
  15. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +349 -0
  17. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +177 -20
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
  19. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
  20. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
  21. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
  22. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
  23. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
  24. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
  25. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
  26. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
  27. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
  28. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
  29. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
  30. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +34 -60
  31. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +187 -0
  32. package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
  33. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +125 -4
  34. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +27 -0
  35. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +28 -0
  36. package/resources/claude-customizations/config/blast-radius.json +9 -3
  37. package/resources/claude-customizations/pack-manifests/core.json +5 -0
  38. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  39. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +349 -0
  40. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +123 -6
  41. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  42. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +32 -1
@@ -4,9 +4,9 @@
4
4
 
5
5
  .DESCRIPTION
6
6
  This script is invoked by the Claude Code PreToolUse hook before any Write or Edit
7
- operation. It reads the tool input from the CLAUDE_TOOL_INPUT environment variable
8
- (JSON with 'file_path' plus 'content' for Write, or 'old_string'/'new_string' for
9
- Edit) and applies two independent gates:
7
+ operation. It acquires the hook payload through the shared reader and reads the
8
+ envelope's nested tool_input ('file_path' plus 'content' for Write, or
9
+ 'old_string'/'new_string' for Edit), then applies two independent gates:
10
10
 
11
11
  1. Syntax gate. On a Write of a '.mmd'/'.mermaid' file, the whole file is one
12
12
  diagram. On a Write of a Markdown file, every fenced ```mermaid block is a
@@ -21,8 +21,8 @@
21
21
  Blocking a valid diagram is worse than missing an invalid one, so the hook declines
22
22
  to judge rather than rejecting whenever it cannot classify content confidently:
23
23
 
24
- - empty, absent, or unparseable CLAUDE_TOOL_INPUT: allow;
25
- - missing 'file_path', or a path outside the '.mmd'/'.mermaid'/Markdown scope: allow;
24
+ - missing 'file_path' inside a well-formed tool_input, or a path outside the
25
+ '.mmd'/'.mermaid'/Markdown scope: allow;
26
26
  - the validation module absent from disk: allow;
27
27
  - an Edit payload (the syntax check needs the whole file, which an
28
28
  old_string/new_string fragment cannot supply): allow;
@@ -32,13 +32,17 @@
32
32
  - a fence immediately preceded by '<!-- mermaid-validator: ignore -->': skip that
33
33
  block, and only that block.
34
34
 
35
- DELIBERATE DIVERGENCE FROM enforce-evidence-locations.ps1: that hook throws on
36
- malformed CLAUDE_TOOL_INPUT JSON and its entry point exits 1. This hook allows
37
- instead. The difference is intentional and must not be "fixed" into a hard failure:
38
- a content gate that hard-fails on input it cannot parse converts an unparseable
39
- payload into a blocked write, which is the false-positive failure mode this feature
40
- exists to avoid. The evidence-location hook gates a path, which is always parseable
41
- when present; this hook gates content, which is not.
35
+ ENVELOPE VERSUS CONTENT (revised by issue #501). The hook now distinguishes two
36
+ kinds of unreadability that the original note conflated. An ENVELOPE-level anomaly
37
+ -- empty payload on every transport, unparseable envelope JSON, or a payload with
38
+ no usable tool_input -- means the PreToolUse contract itself drifted, which is the
39
+ exact defect #501 fixed and which must fail loudly rather than silently allow; the
40
+ hook therefore denies. A CONTENT-level judgement remains permissive, and that half
41
+ of the original note stands unchanged and must not be "fixed" into a hard failure:
42
+ an Edit fragment, a Markdown file with no fence, an absent validation module, and a
43
+ diagram the classifier cannot classify all still allow, because blocking a valid
44
+ diagram is worse than missing an invalid one. The prior blanket allow-on-unparseable
45
+ made the gate inert against the payload the harness actually sends.
42
46
 
43
47
  The extension scope check runs before any content scan, so a write outside the
44
48
  Mermaid scope pays only the JSON parse.
@@ -55,6 +59,8 @@ param()
55
59
 
56
60
  Set-StrictMode -Version Latest
57
61
 
62
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
63
+
58
64
  $script:MermaidModulePath = Join-Path -Path $PSScriptRoot -ChildPath '../lib/mermaid/MermaidValidation.psm1'
59
65
  $script:MermaidSkillPointer = 'See .claude/skills/mermaid-diagram/SKILL.md.'
60
66
  $script:MermaidSyncPointer = 'Change it through the Mermaid Chart sync workflow in VS Code (Mermaid Chart extension: Sync Diagram with Mermaid, then Review Mermaid Sync) and pull the synced result instead of hand-editing. See .claude/rules/mermaid.md.'
@@ -288,7 +294,7 @@ function Invoke-MermaidValidationDecision {
288
294
  business (out of scope, unparseable, or a Markdown file with no fence), which the
289
295
  entry point treats as a silent allow.
290
296
  .PARAMETER ToolInputRaw
291
- The raw JSON string from $env:CLAUDE_TOOL_INPUT.
297
+ The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
292
298
  #>
293
299
  [CmdletBinding()]
294
300
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -298,16 +304,18 @@ function Invoke-MermaidValidationDecision {
298
304
  [string] $ToolInputRaw
299
305
  )
300
306
 
301
- if ([string]::IsNullOrWhiteSpace($ToolInputRaw)) { return $null }
302
-
303
- try {
304
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
305
- } catch {
306
- # Fail open. See the DELIBERATE DIVERGENCE note in this file's header before
307
- # changing this to a throw.
308
- return $null
307
+ # Envelope-level anomaly: fail closed. See the ENVELOPE VERSUS CONTENT note in
308
+ # this file's header before changing this back to a silent allow.
309
+ $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
310
+ if (-not $envelope.IsValid) {
311
+ return Get-MermaidDenyDecision -Reason (
312
+ 'MERMAID_VALIDATION_BLOCKED: payload anomaly - ' +
313
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
314
+ '. The gate fails closed on an envelope it cannot read.')
309
315
  }
310
316
 
317
+ $toolInput = $envelope.Value
318
+
311
319
  $filePath = [string](Get-MermaidToolInputField -InputObject $toolInput -Name 'file_path')
312
320
  if ([string]::IsNullOrWhiteSpace($filePath)) { return $null }
313
321
 
@@ -362,16 +370,20 @@ function Invoke-MermaidValidationEntryPoint {
362
370
  output stream as the JSON, so the caller would consume the JSON instead of
363
371
  printing it and the decision would never reach Claude Code.
364
372
  .PARAMETER ToolInputRaw
365
- The raw JSON string from $env:CLAUDE_TOOL_INPUT.
373
+ The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
366
374
  #>
367
375
  [CmdletBinding()]
368
376
  [OutputType([string])]
369
377
  param(
370
378
  [AllowEmptyString()]
371
379
  [AllowNull()]
372
- [string] $ToolInputRaw = $env:CLAUDE_TOOL_INPUT
380
+ [string] $ToolInputRaw
373
381
  )
374
382
 
383
+ if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
384
+ $ToolInputRaw = Read-ClaudeHookRawPayload
385
+ }
386
+
375
387
  $decision = Invoke-MermaidValidationDecision -ToolInputRaw $ToolInputRaw
376
388
  if ($null -eq $decision) { return }
377
389
 
@@ -383,7 +395,7 @@ if ($MyInvocation.InvocationName -eq '.') {
383
395
  return
384
396
  }
385
397
 
386
- Invoke-MermaidValidationEntryPoint -ToolInputRaw $env:CLAUDE_TOOL_INPUT
398
+ Invoke-MermaidValidationEntryPoint -ToolInputRaw (Read-ClaudeHookRawPayload)
387
399
 
388
400
  # Exit 0 on allow and on deny alike: the decision travels in the JSON, never in the
389
401
  # exit code.
@@ -5,7 +5,7 @@
5
5
 
6
6
  .DESCRIPTION
7
7
  Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
8
- tool input JSON from the CLAUDE_TOOL_INPUT environment variable and the
8
+ tool input JSON from the the envelope's nested tool_input environment variable and the
9
9
  orchestrator checkpoint from artifacts/orchestration/orchestrator-state.json.
10
10
 
11
11
  The hook enforces presence only: it cannot read the delegate's chosen
@@ -32,6 +32,8 @@
32
32
  [CmdletBinding()]
33
33
  param()
34
34
 
35
+
36
+ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
35
37
  function Get-ModelRoutingCheckpoint {
36
38
  <#
37
39
  .SYNOPSIS
@@ -118,7 +120,7 @@ function Test-ModelRoutingReceiptPresent {
118
120
  function Invoke-ModelRoutingReceiptDecision {
119
121
  <#
120
122
  .SYNOPSIS
121
- Parses CLAUDE_TOOL_INPUT and returns an allow-or-block decision object.
123
+ Parses the envelope's nested tool_input and returns an allow-or-block decision object.
122
124
  #>
123
125
  [CmdletBinding()]
124
126
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -128,21 +130,23 @@ function Invoke-ModelRoutingReceiptDecision {
128
130
 
129
131
  $allow = [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
130
132
 
131
- # Empty or absent tool input is not a delegation this hook can gate.
132
- if (-not $ToolInputRaw) {
133
- return $allow
134
- }
135
-
136
- # Malformed tool-input JSON is allowed through gracefully; this hook is a
137
- # deterrent, not the authoritative validator.
138
- try {
139
- $toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
140
- }
141
- catch {
142
- return $allow
133
+ # Envelope-level anomalies fail closed (issue #501): an unreadable envelope means
134
+ # the PreToolUse contract drifted, which is exactly the condition that made every
135
+ # gate in this runtime inert, so it must deny rather than pass through.
136
+ $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
137
+ if (-not $envelope.IsValid) {
138
+ return [ordered]@{
139
+ hookSpecificOutput = [ordered]@{
140
+ hookEventName = 'PreToolUse'
141
+ permissionDecision = 'deny'
142
+ permissionDecisionReason = 'MODEL_ROUTING_RECEIPT_BLOCKED: payload anomaly - ' +
143
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
144
+ '. The gate fails closed on an envelope it cannot read.'
145
+ }
146
+ }
143
147
  }
144
148
 
145
- $subagent = [string]$toolInput.subagent_type
149
+ $subagent = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'subagent_type'
146
150
 
147
151
  # Only the gated Agent-tool delegates are receipt-checked; any other
148
152
  # subagent_type (including orchestrator) passes through.
@@ -169,13 +173,7 @@ if ($MyInvocation.InvocationName -eq '.') {
169
173
  return
170
174
  }
171
175
 
172
- try {
173
- $decision = Invoke-ModelRoutingReceiptDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
174
- }
175
- catch {
176
- Write-Error $_
177
- exit 1
178
- }
176
+ $decision = Invoke-ModelRoutingReceiptDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
179
177
 
180
178
  $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
181
179
 
@@ -0,0 +1,349 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pathspec classifier for the orchestration-bookkeeping staging exemption (issue #539).
4
+ .DESCRIPTION
5
+ Pure string logic only: no disk, process, network, or environment access. The single
6
+ entry predicate `Test-ExemptOrchestrationStagingCommand` answers one question - does
7
+ this command text parse, in its entirety, as recognized staging or integration
8
+ invocations whose every pathspec operand resolves inside an orchestration-bookkeeping
9
+ tree? Every parse ambiguity answers false, so the caller's pre-change deny is the
10
+ fallback for every unmodeled form.
11
+
12
+ The normative contract is the D4 fail-closed rule table in
13
+ docs/features/active/2026-08-24-preimplementation-gate-blocks-planner-integration-commits-539/spec.md.
14
+ Rule-table row numbers are cited inline against the code that realizes them.
15
+
16
+ This file is dot-sourced by the sibling gate hook, following the headroom-split
17
+ precedent set by enforce-pr-author-skill.ps1.
18
+ #>
19
+
20
+ # The five exempt orchestration-bookkeeping trees (D2). Directory prefixes, repo-relative,
21
+ # forward-slash spelled. No glob, no absolute entry, no literal-file entry.
22
+ $script:OrchestrationBookkeepingTrees = @(
23
+ 'docs/features/epics/'
24
+ 'docs/features/parallel/'
25
+ 'docs/features/active/'
26
+ 'docs/features/potential/'
27
+ 'artifacts/orchestration/'
28
+ )
29
+
30
+ # Characters that make a command line statically unresolvable: shell interpolation and
31
+ # redirection (D4 row 12). Tested across the whole line rather than per operand, because a
32
+ # redirection anywhere in the line moves content the operand list cannot describe.
33
+ $script:UnresolvableCommandCharacters = [char[]]@('$', '`', '>', '<')
34
+
35
+ # Wildcards that make an operand a glob (D4 row 15). Only the literal prefix before the
36
+ # first of these is prefix-tested.
37
+ $script:PathspecWildcardCharacters = [char[]]@('*', '?', '[')
38
+
39
+ function Split-OrchestrationCommandLine {
40
+ <#
41
+ .SYNOPSIS
42
+ Splits a command line into segments on chain operators outside quotes.
43
+ .DESCRIPTION
44
+ Realizes D4 row 13. Quote state is tracked while scanning so a chain operator
45
+ inside a quoted span does not split. The returned `Balanced` flag reports whether
46
+ the scan ended outside every quote; unbalanced text is not splittable and the
47
+ caller denies (D4 rows 11 and 13). Empty and whitespace-only segments are dropped.
48
+ .OUTPUTS
49
+ System.Collections.Hashtable with keys `Balanced` (bool) and `Segments` (string[]).
50
+ #>
51
+ [CmdletBinding()]
52
+ [OutputType([hashtable])]
53
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
54
+
55
+ $segments = [System.Collections.Generic.List[string]]::new()
56
+ $current = [System.Text.StringBuilder]::new()
57
+ $openQuote = [char]0
58
+
59
+ foreach ($character in $CommandText.ToCharArray()) {
60
+ if ($openQuote -ne [char]0) {
61
+ if ($character -eq $openQuote) {
62
+ $openQuote = [char]0
63
+ }
64
+ [void]$current.Append($character)
65
+ continue
66
+ }
67
+
68
+ if ($character -eq '"' -or $character -eq "'") {
69
+ $openQuote = $character
70
+ [void]$current.Append($character)
71
+ continue
72
+ }
73
+
74
+ if ($character -eq ';' -or $character -eq '&' -or $character -eq '|' -or
75
+ $character -eq "`n" -or $character -eq "`r") {
76
+ $segments.Add($current.ToString())
77
+ [void]$current.Clear()
78
+ continue
79
+ }
80
+
81
+ [void]$current.Append($character)
82
+ }
83
+ $segments.Add($current.ToString())
84
+
85
+ return @{
86
+ Balanced = ($openQuote -eq [char]0)
87
+ Segments = @($segments | Where-Object { $_.Trim() })
88
+ }
89
+ }
90
+
91
+ function ConvertTo-OrchestrationCommandToken {
92
+ <#
93
+ .SYNOPSIS
94
+ Splits one segment into whitespace-delimited tokens with balanced quotes stripped.
95
+ .DESCRIPTION
96
+ Realizes the quote handling of D4 row 11. A quoted span contributes to the token it
97
+ sits in, so `-m "epic scaffold"` yields the two tokens `-m` and `epic scaffold`, and
98
+ a quoted operand arrives at the prefix test unquoted. The caller has already
99
+ rejected unbalanced text, so an unterminated quote simply ends the final token.
100
+ .OUTPUTS
101
+ System.String[]
102
+ #>
103
+ [CmdletBinding()]
104
+ [OutputType([string[]])]
105
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Segment)
106
+
107
+ $tokens = [System.Collections.Generic.List[string]]::new()
108
+ $current = [System.Text.StringBuilder]::new()
109
+ $hasToken = $false
110
+ $openQuote = [char]0
111
+
112
+ foreach ($character in $Segment.ToCharArray()) {
113
+ if ($openQuote -ne [char]0) {
114
+ if ($character -eq $openQuote) {
115
+ $openQuote = [char]0
116
+ } else {
117
+ [void]$current.Append($character)
118
+ }
119
+ continue
120
+ }
121
+
122
+ if ($character -eq '"' -or $character -eq "'") {
123
+ $openQuote = $character
124
+ $hasToken = $true
125
+ continue
126
+ }
127
+
128
+ if ([char]::IsWhiteSpace($character)) {
129
+ if ($hasToken) {
130
+ $tokens.Add($current.ToString())
131
+ [void]$current.Clear()
132
+ $hasToken = $false
133
+ }
134
+ continue
135
+ }
136
+
137
+ $hasToken = $true
138
+ [void]$current.Append($character)
139
+ }
140
+
141
+ if ($hasToken) {
142
+ $tokens.Add($current.ToString())
143
+ }
144
+
145
+ return $tokens.ToArray()
146
+ }
147
+
148
+ function Test-ExemptOrchestrationOperand {
149
+ <#
150
+ .SYNOPSIS
151
+ Tests one pathspec operand against the five exempt orchestration trees.
152
+ .DESCRIPTION
153
+ Realizes D4 rows 3, 9, 15, 16, 17, and 18. Backslashes normalize to forward slashes
154
+ before the prefix test (row 18); pathspec magic, absolute spellings, parent-directory
155
+ segments, and globs whose literal prefix escapes the exempt set all deny.
156
+ .OUTPUTS
157
+ System.Boolean
158
+ #>
159
+ [CmdletBinding()]
160
+ [OutputType([bool])]
161
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Operand)
162
+
163
+ if (-not $Operand) {
164
+ return $false
165
+ }
166
+
167
+ # Row 3b and row 9: any leading colon is pathspec magic, which can escape or invert the
168
+ # tree scope, so no colon-led operand is ever resolvable by a prefix test.
169
+ if ($Operand.StartsWith(':')) {
170
+ return $false
171
+ }
172
+
173
+ # Row 18: separator normalization precedes every prefix comparison below.
174
+ $normalized = $Operand -replace '\\', '/'
175
+
176
+ # Row 16: rooted, drive-lettered, and UNC spellings all deny; the exempt prefixes stay
177
+ # repo-relative (this is the posture issue #516 later composes with).
178
+ if ($normalized.StartsWith('/')) {
179
+ return $false
180
+ }
181
+ if ($normalized -match '^[A-Za-z]:') {
182
+ return $false
183
+ }
184
+
185
+ # Rows 15c and 17: any parent-directory component escapes the prefix.
186
+ if (($normalized -split '/') -contains '..') {
187
+ return $false
188
+ }
189
+
190
+ # Row 15: only the literal prefix before the first wildcard is prefix-tested, so a glob
191
+ # whose wildcard occupies or truncates an ancestor segment cannot pass.
192
+ $literalPrefix = $normalized
193
+ $wildcardIndex = $normalized.IndexOfAny($script:PathspecWildcardCharacters)
194
+ if ($wildcardIndex -ge 0) {
195
+ $literalPrefix = $normalized.Substring(0, $wildcardIndex)
196
+ }
197
+
198
+ foreach ($tree in $script:OrchestrationBookkeepingTrees) {
199
+ if ($literalPrefix.StartsWith($tree)) {
200
+ return $true
201
+ }
202
+ }
203
+ return $false
204
+ }
205
+
206
+ function Test-ExemptOrchestrationSegmentToken {
207
+ <#
208
+ .SYNOPSIS
209
+ Tests one already-tokenized segment as a recognized all-exempt invocation.
210
+ .DESCRIPTION
211
+ Realizes D4 rows 1, 2, 4, 5, 6, 7, 8, 10, 14, and 19. The command name must lead the
212
+ segment and the subcommand must follow it immediately (row 14), the option table is
213
+ modelled positively so any unmodelled dash-leading token denies (rows 2, 5, 6, 8, 10),
214
+ tokens after the double-dash separator are pathspecs (row 7), at least one operand is
215
+ required (rows 1, 4, 7), and every operand must pass (row 19).
216
+ .OUTPUTS
217
+ System.Boolean
218
+ #>
219
+ [CmdletBinding()]
220
+ [OutputType([bool])]
221
+ param([Parameter(Mandatory)][AllowEmptyCollection()][string[]] $Token)
222
+
223
+ if ($Token.Count -lt 2) {
224
+ return $false
225
+ }
226
+
227
+ # Row 14: the command name leads and the subcommand follows immediately. Anything in
228
+ # between - a relocating option or an env-style prefix - moves the pathspec base and is
229
+ # rejected here rather than modelled.
230
+ if ($Token[0] -cne 'git') {
231
+ return $false
232
+ }
233
+ $subcommand = $Token[1]
234
+ if ($subcommand -cne 'add' -and $subcommand -cne 'commit') {
235
+ return $false
236
+ }
237
+
238
+ $operands = [System.Collections.Generic.List[string]]::new()
239
+ $afterSeparator = $false
240
+ $index = 2
241
+
242
+ while ($index -lt $Token.Count) {
243
+ $candidate = $Token[$index]
244
+
245
+ if (-not $afterSeparator) {
246
+ if ($candidate -ceq '--') {
247
+ # Row 7: every remaining token is a pathspec, dash-leading or not.
248
+ $afterSeparator = $true
249
+ $index++
250
+ continue
251
+ }
252
+
253
+ if ($candidate.StartsWith('-')) {
254
+ # The message option is the only modelled option on either subcommand. Rows
255
+ # 2, 5, 6, 8, and 10 all land here and deny, including a dash-leading
256
+ # operand supplied without a preceding separator.
257
+ if ($subcommand -cne 'commit') {
258
+ return $false
259
+ }
260
+ if ($candidate -ceq '-m' -or $candidate -ceq '--message') {
261
+ # The message value is the following token and is not a pathspec.
262
+ $index += 2
263
+ if ($index -gt $Token.Count) {
264
+ return $false
265
+ }
266
+ continue
267
+ }
268
+ if ($candidate.StartsWith('--message=') -or
269
+ ($candidate.Length -gt 2 -and $candidate.StartsWith('-m'))) {
270
+ $index++
271
+ continue
272
+ }
273
+ return $false
274
+ }
275
+ }
276
+
277
+ $operands.Add($candidate)
278
+ $index++
279
+ }
280
+
281
+ # Rows 1, 4, and 7: an invocation with no pathspec operand claims no path, so there is
282
+ # nothing for the prefix test to scope and the exemption is not available.
283
+ if ($operands.Count -eq 0) {
284
+ return $false
285
+ }
286
+
287
+ # Row 19: all-operands-exempt is the invariant; one non-exempt operand denies the set.
288
+ foreach ($operand in $operands) {
289
+ if (-not (Test-ExemptOrchestrationOperand -Operand $operand)) {
290
+ return $false
291
+ }
292
+ }
293
+ return $true
294
+ }
295
+
296
+ function Test-ExemptOrchestrationStagingCommand {
297
+ <#
298
+ .SYNOPSIS
299
+ Reports whether a command line is an orchestration-bookkeeping staging invocation.
300
+ .DESCRIPTION
301
+ The entry predicate of the issue #539 exemption. Returns true only when the whole
302
+ command line splits cleanly into segments and EVERY segment parses as a complete,
303
+ recognized staging or integration invocation carrying at least one pathspec operand,
304
+ with every operand resolving inside one of the five exempt orchestration-bookkeeping
305
+ trees after balanced-quote stripping and separator normalization.
306
+
307
+ The all-segments reading is deliberate and fail-closed: a chained line denies unless
308
+ each of its segments is independently a recognized all-exempt invocation, so a
309
+ relocating spelling or a prose fragment anywhere in the line withholds the exemption
310
+ even when another segment on the same line would have qualified on its own.
311
+
312
+ Consumed allow-side only. A false result restores the caller's unchanged
313
+ classification; it never suppresses the trigger.
314
+ .OUTPUTS
315
+ System.Boolean
316
+ #>
317
+ [CmdletBinding()]
318
+ [OutputType([bool])]
319
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $CommandText)
320
+
321
+ if (-not $CommandText) {
322
+ return $false
323
+ }
324
+
325
+ # Row 12: interpolation and redirection are not statically resolvable, so the operand
326
+ # list cannot be trusted to describe what the line actually touches.
327
+ if ($CommandText.IndexOfAny($script:UnresolvableCommandCharacters) -ge 0) {
328
+ return $false
329
+ }
330
+
331
+ $split = Split-OrchestrationCommandLine -CommandText $CommandText
332
+ if (-not $split.Balanced) {
333
+ # Rows 11 and 13: unbalanced quoting makes the segment boundaries ambiguous.
334
+ return $false
335
+ }
336
+
337
+ $segments = @($split.Segments)
338
+ if ($segments.Count -eq 0) {
339
+ return $false
340
+ }
341
+
342
+ foreach ($segment in $segments) {
343
+ $tokens = @(ConvertTo-OrchestrationCommandToken -Segment $segment)
344
+ if (-not (Test-ExemptOrchestrationSegmentToken -Token $tokens)) {
345
+ return $false
346
+ }
347
+ }
348
+ return $true
349
+ }