@danmoisan/drm-copilot-mcp 1.1.9 → 1.1.11

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 (46) hide show
  1. package/out/mcp-server.js +3526 -1037
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +15 -2
  4. package/resources/claude-customizations/.claude/agents/parallel-planner.md +3 -0
  5. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +49 -14
  6. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
  7. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +7 -1
  8. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +103 -6
  9. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +58 -3
  10. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +37 -5
  11. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +13 -2
  12. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +35 -7
  13. package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +483 -0
  14. package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +483 -0
  15. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +254 -5
  16. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +18 -75
  17. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +290 -0
  18. package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +415 -0
  19. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMerge.psm1 +355 -0
  20. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMergeGrammar.psm1 +318 -0
  21. package/resources/claude-customizations/.claude/lib/project-file-merge/Resolve-MergeableConflict.ps1 +229 -0
  22. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
  23. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +311 -16
  24. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +42 -0
  25. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
  26. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +36 -2
  27. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +6 -0
  28. package/resources/claude-customizations/.claude/skills/powershell-orchestration-state-machine/SKILL.md +29 -1
  29. package/resources/claude-customizations/config/blast-radius.json +7 -0
  30. package/resources/claude-customizations/pack-manifests/core.json +7 -0
  31. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +41 -0
  32. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -0
  33. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +26 -0
  34. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  35. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +49 -3
  36. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +57 -12
  37. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +34 -8
  38. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -1
  39. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +34 -7
  40. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +483 -0
  41. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +483 -0
  42. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +130 -2
  43. package/resources/codex-and-agents-customizations/pack-manifests/core.json +6 -1
  44. package/resources/config/orchestration-handoff-registry.json +138 -0
  45. package/resources/config/orchestration-handoff.schema.json +472 -0
  46. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +38 -0
@@ -0,0 +1,483 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Bash command-line segment scanner shared by the Claude and Codex enforcement hooks.
4
+ .DESCRIPTION
5
+ Realizes Piece 1 and Piece 2 of the D2 behavior contract in
6
+ docs/features/active/2026-08-25-enforcement-hook-trigger-matches-whole-command-text-545/spec.md.
7
+
8
+ A single left-to-right scan of a raw Bash command line produces an ordered list of
9
+ segment records. Each record carries the segment's raw text, a masked text in which
10
+ every quoted span and every attached heredoc body has been replaced by spaces, a
11
+ quote-stripped token list, the leading command word, and the flags a caller needs in
12
+ order to decide whether the masked text is safe to scan.
13
+
14
+ Pure string logic only: no disk, process, network, clock, or environment access. It is
15
+ dot-sourced as: . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
16
+ #>
17
+
18
+ # The wrapper carve-out set (D2 Piece 2). A segment led by one of these names keeps its raw
19
+ # text as scan text, because a wrapper's quoted argument is a nested command line rather
20
+ # than inert data. Pinned by test through Get-CommandLineWrapperName (rule R6).
21
+ $script:CommandLineWrapperNames = @(
22
+ 'sh', 'bash', 'zsh', 'dash', 'ksh', 'pwsh', 'powershell',
23
+ 'xargs', 'env', 'command', 'eval', 'nohup', 'time', 'timeout'
24
+ )
25
+
26
+ # Characters that end a segment when they occur outside a quoted span and outside a heredoc
27
+ # body (D2 Piece 1). The subshell, group, and substitution openers and closers are
28
+ # delimiters by design: treating them as such is what closes the '(git add .)' and
29
+ # '$(git add .)' non-match bypasses. The two-character '$(' opener is handled alongside
30
+ # this table rather than in it.
31
+ $script:CommandLineSegmentDelimiters = [char[]]@(';', '&', '|', "`n", '(', ')', '{', '}', '`')
32
+
33
+ function Get-CommandLineWrapperName {
34
+ <#
35
+ .SYNOPSIS
36
+ Return the wrapper carve-out set of D2 Piece 2.
37
+ .DESCRIPTION
38
+ A getter rather than a bare variable, so the R6 membership test pins a public surface.
39
+ .OUTPUTS
40
+ System.String[]
41
+ #>
42
+ [CmdletBinding()]
43
+ [OutputType([string[]])]
44
+ param()
45
+
46
+ return [string[]]$script:CommandLineWrapperNames
47
+ }
48
+
49
+ function ConvertTo-CommandLineToken {
50
+ <#
51
+ .SYNOPSIS
52
+ Split one segment into whitespace-delimited tokens with balanced quotes stripped.
53
+ .DESCRIPTION
54
+ The issue #539 ConvertTo-OrchestrationCommandToken idiom, reproduced here so the
55
+ parser carries no dependency on the orchestration gate's helper file. A quoted span
56
+ contributes to the token it sits in and its delimiting quotes are removed, and
57
+ whitespace inside it does not split a token, so `-m "rm -rf x"` yields the two
58
+ tokens `-m` and `rm -rf x` and never an adjacent token pair equal to a literal.
59
+ .OUTPUTS
60
+ System.String[]
61
+ #>
62
+ [CmdletBinding()]
63
+ [OutputType([string[]])]
64
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Segment)
65
+
66
+ $tokens = [System.Collections.Generic.List[string]]::new()
67
+ $current = [System.Text.StringBuilder]::new()
68
+ $hasToken = $false
69
+ $openQuote = [char]0
70
+
71
+ foreach ($character in $Segment.ToCharArray()) {
72
+ if ($openQuote -ne [char]0) {
73
+ if ($character -eq $openQuote) { $openQuote = [char]0 } else { [void]$current.Append($character) }
74
+ continue
75
+ }
76
+
77
+ if ($character -eq '"' -or $character -eq "'") {
78
+ $openQuote = $character
79
+ $hasToken = $true
80
+ continue
81
+ }
82
+
83
+ if ([char]::IsWhiteSpace($character)) {
84
+ if ($hasToken) {
85
+ $tokens.Add($current.ToString())
86
+ [void]$current.Clear()
87
+ $hasToken = $false
88
+ }
89
+ continue
90
+ }
91
+
92
+ $hasToken = $true
93
+ [void]$current.Append($character)
94
+ }
95
+ if ($hasToken) { $tokens.Add($current.ToString()) }
96
+ return $tokens.ToArray()
97
+ }
98
+
99
+ function Get-CommandLineSegmentCommandWord {
100
+ <#
101
+ .SYNOPSIS
102
+ Return the leading command word of a token list, skipping VAR=value prefixes.
103
+ .DESCRIPTION
104
+ Realizes step 1 of D2 Piece 3 for the scanner's own CommandWord property. Returns
105
+ the empty string when the segment carries no command word.
106
+ .OUTPUTS
107
+ System.String
108
+ #>
109
+ [CmdletBinding()]
110
+ [OutputType([string])]
111
+ param([Parameter(Mandatory)][AllowEmptyCollection()][AllowEmptyString()][AllowNull()][string[]] $Token)
112
+
113
+ foreach ($candidate in $Token) {
114
+ if ($candidate -match '^[A-Za-z_][A-Za-z0-9_]*=') { continue }
115
+ return $candidate
116
+ }
117
+ return ''
118
+ }
119
+
120
+ function ConvertTo-CommandLineSegmentRecord {
121
+ <#
122
+ .SYNOPSIS
123
+ Assemble one segment record and apply the three ordered ScanText clauses.
124
+ .DESCRIPTION
125
+ ScanText selection follows D2 Piece 2 in order: RawText when the segment is
126
+ Unbalanced or carries a live substitution; RawText when it is wrapper-led;
127
+ MaskedText otherwise. TokenText is the segment text with heredoc bodies replaced by
128
+ spaces and quoted spans intact. Unbalanced also carries the unmaskable-delimiter
129
+ case of rule R3, because such a heredoc cannot be shown to close.
130
+ .OUTPUTS
131
+ System.Management.Automation.PSCustomObject
132
+ #>
133
+ [CmdletBinding()]
134
+ [OutputType([pscustomobject])]
135
+ param(
136
+ [Parameter(Mandatory)][AllowEmptyString()][string] $RawText,
137
+ [Parameter(Mandatory)][AllowEmptyString()][string] $MaskedText,
138
+ [Parameter(Mandatory)][AllowEmptyString()][string] $TokenText,
139
+ [Parameter(Mandatory)][bool] $HasLiveSubstitution,
140
+ [Parameter(Mandatory)][bool] $Unbalanced
141
+ )
142
+
143
+ $tokens = @(ConvertTo-CommandLineToken -Segment $TokenText)
144
+ $commandWord = Get-CommandLineSegmentCommandWord -Token $tokens
145
+ $isWrapperLed = $false
146
+ if (-not [string]::IsNullOrEmpty($commandWord)) {
147
+ $isWrapperLed = $script:CommandLineWrapperNames -contains $commandWord
148
+ }
149
+
150
+ $scanText = $MaskedText
151
+ if ($Unbalanced -or $HasLiveSubstitution -or $isWrapperLed) { $scanText = $RawText }
152
+
153
+ return [pscustomobject]@{
154
+ RawText = $RawText
155
+ MaskedText = $MaskedText
156
+ Tokens = $tokens
157
+ CommandWord = $commandWord
158
+ IsWrapperLed = $isWrapperLed
159
+ HasLiveSubstitution = $HasLiveSubstitution
160
+ Unbalanced = $Unbalanced
161
+ ScanText = $scanText
162
+ }
163
+ }
164
+
165
+ function Test-CommandLineSegmentRawScan {
166
+ <#
167
+ .SYNOPSIS
168
+ Report whether the scanner selected RawText as this segment's ScanText.
169
+ .DESCRIPTION
170
+ The three disjuncts below are the same three clauses ConvertTo-CommandLineSegmentRecord
171
+ applies when it selects ScanText, so a caller that guards a raw-text read with this
172
+ predicate reads raw on exactly the segments the scanner already scans raw, and on no
173
+ others.
174
+
175
+ It exists for one fail-open class the token readers cannot close by themselves.
176
+ ConvertTo-CommandLineToken collapses a balanced quoted span into ONE token, so a flag
177
+ carried inside a wrapper's quoted argument never appears as a token and every
178
+ token-based flag read reports it absent. A call site that reads flag absence as OUT OF
179
+ SCOPE therefore allows a command the pre-parser whole-text scan denied. A call site
180
+ that reads flag absence as MISSING AUTHORIZATION is already fail-closed and must not
181
+ use this predicate.
182
+ .PARAMETER Segment
183
+ One segment record produced by Read-CommandLineSegment.
184
+ .OUTPUTS
185
+ System.Boolean
186
+ #>
187
+ [CmdletBinding()]
188
+ [OutputType([bool])]
189
+ param([Parameter(Mandatory)][AllowNull()] $Segment)
190
+
191
+ if ($null -eq $Segment) {
192
+ return $false
193
+ }
194
+
195
+ return ([bool]$Segment.IsWrapperLed -or [bool]$Segment.HasLiveSubstitution -or [bool]$Segment.Unbalanced)
196
+ }
197
+
198
+ function Read-CommandLineHeredocHeader {
199
+ <#
200
+ .SYNOPSIS
201
+ Read a heredoc header beginning at a '<<' operator and report its delimiter.
202
+ .DESCRIPTION
203
+ Handles the plain '<<' form, the tab-stripping '<<-' form, and an optionally quoted
204
+ delimiter word. A delimiter produced by expansion is reported as non-literal, which
205
+ makes the segment unresolvable under R3. Returns NextIndex, Delimiter, StripTabs,
206
+ and IsLiteral.
207
+ .OUTPUTS
208
+ System.Management.Automation.PSCustomObject
209
+ #>
210
+ [CmdletBinding()]
211
+ [OutputType([pscustomobject])]
212
+ param(
213
+ [Parameter(Mandatory)][string] $Text,
214
+ [Parameter(Mandatory)][int] $StartIndex
215
+ )
216
+
217
+ $cursor = $StartIndex + 2
218
+ $stripTabs = $false
219
+ if ($cursor -lt $Text.Length -and $Text[$cursor] -eq '-') {
220
+ $stripTabs = $true
221
+ $cursor++
222
+ }
223
+
224
+ while ($cursor -lt $Text.Length -and ($Text[$cursor] -eq ' ' -or $Text[$cursor] -eq "`t")) { $cursor++ }
225
+
226
+ $word = [System.Text.StringBuilder]::new()
227
+ $isLiteral = $true
228
+ $openQuote = [char]0
229
+ while ($cursor -lt $Text.Length) {
230
+ $character = $Text[$cursor]
231
+ if ($openQuote -ne [char]0) {
232
+ if ($character -eq $openQuote) {
233
+ $openQuote = [char]0
234
+ } else {
235
+ if ($openQuote -eq '"' -and ($character -eq '$' -or $character -eq '`')) { $isLiteral = $false }
236
+ [void]$word.Append($character)
237
+ }
238
+ $cursor++
239
+ continue
240
+ }
241
+
242
+ if ($character -eq '"' -or $character -eq "'") {
243
+ $openQuote = $character
244
+ $cursor++
245
+ continue
246
+ }
247
+
248
+ if ([char]::IsWhiteSpace($character) -or $script:CommandLineSegmentDelimiters -contains $character) { break }
249
+ if ($character -eq '$' -or $character -eq '`') { $isLiteral = $false }
250
+ [void]$word.Append($character)
251
+ $cursor++
252
+ }
253
+
254
+ $delimiter = $word.ToString()
255
+ if ($openQuote -ne [char]0 -or [string]::IsNullOrEmpty($delimiter)) { $isLiteral = $false }
256
+
257
+ return [pscustomobject]@{
258
+ NextIndex = $cursor
259
+ Delimiter = $delimiter
260
+ StripTabs = $stripTabs
261
+ IsLiteral = $isLiteral
262
+ }
263
+ }
264
+
265
+ function Read-CommandLineHeredocBody {
266
+ <#
267
+ .SYNOPSIS
268
+ Consume one heredoc body from a start index and report where it ends.
269
+ .DESCRIPTION
270
+ Consumes lines until a line whose content - after optional leading tabs for the
271
+ '<<-' form - equals the delimiter exactly, case-sensitively as in the shell. An
272
+ unterminated body is consumed to end of text and reported through Unbalanced.
273
+ .OUTPUTS
274
+ System.Management.Automation.PSCustomObject
275
+ #>
276
+ [CmdletBinding()]
277
+ [OutputType([pscustomobject])]
278
+ param(
279
+ [Parameter(Mandatory)][string] $Text,
280
+ [Parameter(Mandatory)][int] $StartIndex,
281
+ [Parameter(Mandatory)][AllowEmptyString()][string] $Delimiter,
282
+ [Parameter(Mandatory)][bool] $StripTabs
283
+ )
284
+
285
+ $cursor = $StartIndex
286
+ $length = $Text.Length
287
+ while ($cursor -lt $length) {
288
+ $lineEnd = $Text.IndexOf("`n", $cursor)
289
+ if ($lineEnd -lt 0) { $lineEnd = $length }
290
+
291
+ $line = $Text.Substring($cursor, $lineEnd - $cursor).TrimEnd("`r")
292
+ if ($StripTabs) { $line = $line.TrimStart("`t") }
293
+
294
+ $cursor = if ($lineEnd -lt $length) { $lineEnd + 1 } else { $length }
295
+ if ($line -ceq $Delimiter) {
296
+ return [pscustomobject]@{ NextIndex = $cursor; Terminated = $true }
297
+ }
298
+ }
299
+
300
+ return [pscustomobject]@{ NextIndex = $length; Terminated = $false }
301
+ }
302
+
303
+ function Read-CommandLineSegment {
304
+ <#
305
+ .SYNOPSIS
306
+ Scan a raw Bash command line into an ordered list of segment records.
307
+ .DESCRIPTION
308
+ A single left-to-right scan tracking single-quote spans, double-quote spans,
309
+ backslash escapes, and a heredoc delimiter state machine. Segment delimiters,
310
+ recognized only outside quoted spans and heredoc bodies: ';', '&', '|', newline,
311
+ and the subshell/group/substitution openers and closers '(', ')', '{', '}',
312
+ '$(' and the backtick.
313
+
314
+ Pure: reads no file, starts no process, reads no clock, mutates no input.
315
+ .PARAMETER CommandText
316
+ The raw Bash command text, exactly as delivered by
317
+ Get-ClaudeHookToolInputString -Name 'command'.
318
+ .OUTPUTS
319
+ System.Management.Automation.PSCustomObject[] - one record per segment, in
320
+ source order. Each record carries:
321
+ RawText [string] the segment's original text, unmodified
322
+ MaskedText [string] quoted spans and attached heredoc bodies
323
+ replaced by single spaces
324
+ Tokens [string[]] quote-stripped, whitespace-delimited tokens
325
+ CommandWord [string] the first token after any VAR=value prefixes,
326
+ or '' when the segment has no command word
327
+ IsWrapperLed [bool] $true when CommandWord is a member of the
328
+ wrapper carve-out set
329
+ HasLiveSubstitution [bool] $true when '$(' or a backtick occurred inside
330
+ one of the segment's double-quoted spans
331
+ Unbalanced [bool] $true when a quote span or heredoc did not
332
+ close before end of text
333
+ ScanText [string] the clause-ordered selection: RawText when
334
+ Unbalanced or HasLiveSubstitution or
335
+ IsWrapperLed; MaskedText otherwise
336
+ Returns an empty array for null, empty, or whitespace-only input.
337
+ #>
338
+ [CmdletBinding()]
339
+ [OutputType([pscustomobject[]])]
340
+ param(
341
+ [Parameter(Mandatory)]
342
+ [AllowEmptyString()]
343
+ [AllowNull()]
344
+ [string] $CommandText
345
+ )
346
+
347
+ $records = [System.Collections.Generic.List[pscustomobject]]::new()
348
+ if ([string]::IsNullOrWhiteSpace($CommandText)) { return $records.ToArray() }
349
+
350
+ $raw = [System.Text.StringBuilder]::new()
351
+ $masked = [System.Text.StringBuilder]::new()
352
+ $tokenText = [System.Text.StringBuilder]::new()
353
+ $pending = [System.Collections.Generic.List[pscustomobject]]::new()
354
+ $hasLiveSubstitution = $false
355
+ $unbalanced = $false
356
+ $inSingle = $false
357
+ $inDouble = $false
358
+ $index = 0
359
+ $length = $CommandText.Length
360
+
361
+ while ($index -lt $length) {
362
+ $character = $CommandText[$index]
363
+ $next = if ($index + 1 -lt $length) { $CommandText[$index + 1] } else { [char]0 }
364
+
365
+ if ($inSingle) {
366
+ if ($character -eq "'") { $inSingle = $false }
367
+ [void]$raw.Append($character)
368
+ [void]$tokenText.Append($character)
369
+ [void]$masked.Append(' ')
370
+ $index++
371
+ continue
372
+ }
373
+
374
+ if ($inDouble) {
375
+ if ($character -eq '\' -and $next -ne [char]0) {
376
+ [void]$raw.Append($character).Append($next)
377
+ [void]$tokenText.Append($character).Append($next)
378
+ [void]$masked.Append(' ')
379
+ $index += 2
380
+ continue
381
+ }
382
+ if ($character -eq '`' -or ($character -eq '$' -and $next -eq '(')) { $hasLiveSubstitution = $true }
383
+ if ($character -eq '"') { $inDouble = $false }
384
+ [void]$raw.Append($character)
385
+ [void]$tokenText.Append($character)
386
+ [void]$masked.Append(' ')
387
+ $index++
388
+ continue
389
+ }
390
+
391
+ if ($character -eq '\' -and $next -ne [char]0) {
392
+ [void]$raw.Append($character).Append($next)
393
+ [void]$tokenText.Append($character).Append($next)
394
+ [void]$masked.Append($character).Append($next)
395
+ $index += 2
396
+ continue
397
+ }
398
+
399
+ if ($character -eq "'" -or $character -eq '"') {
400
+ if ($character -eq "'") { $inSingle = $true } else { $inDouble = $true }
401
+ [void]$raw.Append($character)
402
+ [void]$tokenText.Append($character)
403
+ [void]$masked.Append(' ')
404
+ $index++
405
+ continue
406
+ }
407
+
408
+ if ($character -eq '<' -and $next -eq '<') {
409
+ if ($index + 2 -lt $length -and $CommandText[$index + 2] -eq '<') {
410
+ # '<<<' is a here-string. It has no body and is not a heredoc.
411
+ [void]$raw.Append('<<<')
412
+ [void]$tokenText.Append('<<<')
413
+ [void]$masked.Append('<<<')
414
+ $index += 3
415
+ continue
416
+ }
417
+
418
+ $header = Read-CommandLineHeredocHeader -Text $CommandText -StartIndex $index
419
+ $headerText = $CommandText.Substring($index, $header.NextIndex - $index)
420
+ [void]$raw.Append($headerText)
421
+ [void]$tokenText.Append($headerText)
422
+ [void]$masked.Append($headerText)
423
+ if ($header.IsLiteral) {
424
+ $pending.Add($header)
425
+ } else {
426
+ # A delimiter produced by expansion is unmaskable, so the containing
427
+ # segment is unresolvable and falls back to a raw scan (rule R3).
428
+ $unbalanced = $true
429
+ }
430
+ $index = $header.NextIndex
431
+ continue
432
+ }
433
+
434
+ $isSubstitutionOpener = $character -eq '$' -and $next -eq '('
435
+ if ($isSubstitutionOpener -or $script:CommandLineSegmentDelimiters -contains $character) {
436
+ if ($character -eq "`n" -and $pending.Count -gt 0) {
437
+ [void]$raw.Append($character)
438
+ [void]$tokenText.Append($character)
439
+ [void]$masked.Append($character)
440
+ $index++
441
+ foreach ($heredoc in $pending) {
442
+ $body = Read-CommandLineHeredocBody -Text $CommandText -StartIndex $index -Delimiter $heredoc.Delimiter -StripTabs $heredoc.StripTabs
443
+ $bodyText = $CommandText.Substring($index, $body.NextIndex - $index)
444
+ [void]$raw.Append($bodyText)
445
+ [void]$tokenText.Append(' ' * $bodyText.Length)
446
+ [void]$masked.Append(' ' * $bodyText.Length)
447
+ if (-not $body.Terminated) { $unbalanced = $true }
448
+ $index = $body.NextIndex
449
+ }
450
+ $pending.Clear()
451
+ } elseif ($isSubstitutionOpener) {
452
+ $index += 2
453
+ } else {
454
+ $index++
455
+ }
456
+
457
+ $rawText = $raw.ToString()
458
+ if (-not [string]::IsNullOrWhiteSpace($rawText)) {
459
+ $records.Add((ConvertTo-CommandLineSegmentRecord -RawText $rawText -MaskedText $masked.ToString() -TokenText $tokenText.ToString() -HasLiveSubstitution $hasLiveSubstitution -Unbalanced $unbalanced))
460
+ }
461
+ [void]$raw.Clear()
462
+ [void]$masked.Clear()
463
+ [void]$tokenText.Clear()
464
+ $hasLiveSubstitution = $false
465
+ $unbalanced = $false
466
+ continue
467
+ }
468
+
469
+ [void]$raw.Append($character)
470
+ [void]$tokenText.Append($character)
471
+ [void]$masked.Append($character)
472
+ $index++
473
+ }
474
+
475
+ if ($inSingle -or $inDouble -or $pending.Count -gt 0) { $unbalanced = $true }
476
+
477
+ $rawText = $raw.ToString()
478
+ if (-not [string]::IsNullOrWhiteSpace($rawText)) {
479
+ $records.Add((ConvertTo-CommandLineSegmentRecord -RawText $rawText -MaskedText $masked.ToString() -TokenText $tokenText.ToString() -HasLiveSubstitution $hasLiveSubstitution -Unbalanced $unbalanced))
480
+ }
481
+
482
+ return $records.ToArray()
483
+ }