@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,229 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Resolve mechanically-mergeable project-file conflicts in a worktree.
4
+
5
+ .DESCRIPTION
6
+ Entry point for the parent-side project-file merge step of issue #643. It
7
+ lists the conflicted paths of a worktree left by an in-progress merge,
8
+ refuses the whole set unless every path belongs to the mergeable class, and
9
+ otherwise rewrites each conflicted file as the keyed union of the two sides.
10
+
11
+ The script stages nothing and creates no revision: the caller owns those
12
+ steps, so a resolution this script produces is always inspectable before it
13
+ becomes history. It writes one compressed JSON object to stdout and exits 0
14
+ for both verdicts; the verdict is that object's result field. A non-zero exit
15
+ means an unexpected error, whose message is on stderr.
16
+
17
+ .PARAMETER Worktree
18
+ Absolute or relative path to the worktree holding the conflicted merge.
19
+
20
+ .PARAMETER ConfigPath
21
+ Truth table to read mergeable_paths from. Defaults to the worktree's own
22
+ config/blast-radius.json.
23
+ #>
24
+ param(
25
+ [string] $Worktree,
26
+ [string] $ConfigPath
27
+ )
28
+
29
+ Set-StrictMode -Version Latest
30
+ $ErrorActionPreference = 'Stop'
31
+
32
+ # The merge module is imported before the grammar module it nests. Reversing the
33
+ # two would leave the grammar unavailable here: the nested -Force import inside
34
+ # ProjectFileMerge.psm1 removes an already-loaded grammar module and re-imports it
35
+ # into that module's own scope, taking its commands out of this scope.
36
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'ProjectFileMerge.psm1') -Force -ErrorAction Stop
37
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'ProjectFileMergeGrammar.psm1') -Force -ErrorAction Stop
38
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath '../blast-radius/BlastRadiusConflict.psm1') -Force -ErrorAction Stop
39
+
40
+ # The three bytes a UTF-8 byte-order mark occupies, kept as a constant so the
41
+ # reader and the writer agree on one prefix.
42
+ $script:ByteOrderMark = [byte[]] @(0xEF, 0xBB, 0xBF)
43
+
44
+ # Splits a text into lines that each retain their own terminator, which is what
45
+ # lets a CRLF file survive a merge unchanged.
46
+ $script:LinePattern = '[^\r\n]*(?:\r\n|\n|\r)'
47
+
48
+ function Invoke-GitExe {
49
+ <#
50
+ .SYNOPSIS
51
+ The single executable seam of this script.
52
+ .DESCRIPTION
53
+ Every git invocation goes through here, so a test mocks one function
54
+ rather than an executable. A non-zero exit throws with the output.
55
+ #>
56
+ [CmdletBinding()]
57
+ # The unary comma wraps the result so the pipeline does not unroll a
58
+ # one-element collection, which makes the emitted object an Object[]
59
+ # carrying strings rather than a bare string[]. Both are declared.
60
+ [OutputType([string[]], [System.Object[]])]
61
+ param([Parameter(Mandatory = $true)][AllowEmptyCollection()][string[]] $GitArgs)
62
+
63
+ $output = & git @GitArgs 2>&1
64
+ # A failed git call is never a resolvable state, so it stops the run.
65
+ if ($LASTEXITCODE -ne 0) {
66
+ throw ('git {0} exited {1}: {2}' -f ($GitArgs -join ' '), $LASTEXITCODE, ($output -join ' '))
67
+ }
68
+ return , @($output | ForEach-Object { [string] $_ })
69
+ }
70
+
71
+ function Read-ConflictedFile {
72
+ <#
73
+ .SYNOPSIS
74
+ Read a conflicted file as bytes, a mark flag, and terminator-bearing lines.
75
+ .DESCRIPTION
76
+ Decoding is strict UTF-8. A file that does not decode returns $null, which
77
+ the caller turns into an escalation rather than a lossy rewrite.
78
+ #>
79
+ [CmdletBinding()]
80
+ [OutputType([pscustomobject])]
81
+ param([Parameter(Mandatory = $true)][string] $Path)
82
+
83
+ $bytes = [System.IO.File]::ReadAllBytes($Path)
84
+ $hasBom = $bytes.Length -ge 3 -and $bytes[0] -eq $script:ByteOrderMark[0] -and $bytes[1] -eq $script:ByteOrderMark[1] -and $bytes[2] -eq $script:ByteOrderMark[2]
85
+ try {
86
+ $text = [System.Text.UTF8Encoding]::new($false, $true).GetString($bytes)
87
+ } catch [System.Text.DecoderFallbackException] {
88
+ return $null
89
+ }
90
+ # The decoded mark is metadata, not content; the writer restores it.
91
+ if ($hasBom) { $text = $text.Substring(1) }
92
+
93
+ $line = [System.Collections.Generic.List[string]]::new()
94
+ foreach ($match in [regex]::Matches($text, $script:LinePattern)) { $line.Add($match.Value) }
95
+ # A final line with no terminator is not matched above and is added here.
96
+ $consumed = ($line -join '').Length
97
+ if ($consumed -lt $text.Length) { $line.Add($text.Substring($consumed)) }
98
+
99
+ return [pscustomobject]@{ Bytes = $bytes; HasBom = $hasBom; Line = $line.ToArray() }
100
+ }
101
+
102
+ function Write-MergedFile {
103
+ <#
104
+ .SYNOPSIS
105
+ Write merged lines back over the conflicted file.
106
+ .DESCRIPTION
107
+ The byte array is assembled before the ShouldProcess guard, so -WhatIf
108
+ exercises the whole assembly and suppresses only the write itself.
109
+ #>
110
+ [CmdletBinding(SupportsShouldProcess = $true)]
111
+ [OutputType([void])]
112
+ param(
113
+ [Parameter(Mandatory = $true)][string] $Path,
114
+ [Parameter(Mandatory = $true)][AllowEmptyCollection()][AllowEmptyString()][string[]] $Line,
115
+ [Parameter(Mandatory = $true)][bool] $HasBom
116
+ )
117
+
118
+ $body = [System.Text.UTF8Encoding]::new($false).GetBytes($Line -join '')
119
+ $payload = [System.Collections.Generic.List[byte]]::new()
120
+ # A file that carried a mark keeps it, because dropping it is a byte change
121
+ # no reviewer asked for.
122
+ if ($HasBom) { $payload.AddRange($script:ByteOrderMark) }
123
+ $payload.AddRange($body)
124
+ if ($PSCmdlet.ShouldProcess($Path, 'Write the merged project file')) {
125
+ [System.IO.File]::WriteAllBytes($Path, $payload.ToArray())
126
+ }
127
+ }
128
+
129
+ function ConvertTo-ResolutionResult {
130
+ <#
131
+ .SYNOPSIS
132
+ Build the single result record this script emits.
133
+ .DESCRIPTION
134
+ The three keys are the whole published contract, so they are written in
135
+ one place rather than at each return site.
136
+ #>
137
+ [CmdletBinding()]
138
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
139
+ param(
140
+ [Parameter(Mandatory = $true)][string] $ResultName,
141
+ [AllowEmptyCollection()][object[]] $Resolved = @(),
142
+ [AllowEmptyCollection()][AllowEmptyString()][string[]] $EscalatePath = @()
143
+ )
144
+
145
+ return [ordered]@{ result = $ResultName; resolved = $Resolved; escalate_paths = $EscalatePath }
146
+ }
147
+
148
+ function Invoke-MergeableConflictResolution {
149
+ <#
150
+ .SYNOPSIS
151
+ Resolve every conflicted path of a worktree, or escalate the whole set.
152
+ .DESCRIPTION
153
+ The set is all-or-nothing. One non-mergeable path escalates before any
154
+ file is read, and one unresolvable file escalates before any file is
155
+ written, so a partially merged worktree is not a reachable state.
156
+ .PARAMETER Worktree
157
+ The worktree holding the conflicted merge.
158
+ .PARAMETER ConfigPath
159
+ Truth table carrying mergeable_paths; the worktree default is used when
160
+ this is blank.
161
+ .OUTPUTS
162
+ An ordered record carrying result, resolved, and escalate_paths.
163
+ #>
164
+ [CmdletBinding()]
165
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
166
+ param(
167
+ [Parameter(Mandatory = $true)][string] $Worktree,
168
+ [AllowNull()][AllowEmptyString()][string] $ConfigPath
169
+ )
170
+
171
+ if ([string]::IsNullOrWhiteSpace($ConfigPath)) { $ConfigPath = Join-Path -Path $Worktree -ChildPath 'config/blast-radius.json' }
172
+ $config = Get-Content -LiteralPath $ConfigPath -Raw | ConvertFrom-Json
173
+ $mergeable = @(Get-ConfigMergeablePath -Config $config)
174
+
175
+ # The listing is bound before it is filtered: Invoke-GitExe returns one array
176
+ # object, so piping it directly would hand the whole array to the filter.
177
+ $listed = Invoke-GitExe -GitArgs @('-C', $Worktree, 'diff', '--name-only', '--diff-filter=U')
178
+ $conflicted = @($listed | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
179
+ $outside = @($conflicted | Where-Object { -not (Test-MergeablePath -Entry $_ -MergeablePath $mergeable) })
180
+ # Classification happens first so a non-mergeable set costs no file read.
181
+ if ($outside.Count -gt 0) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath $outside) }
182
+
183
+ $pending = [System.Collections.Generic.List[object]]::new()
184
+ foreach ($path in $conflicted) {
185
+ $kind = Get-ProjectFileKind -Path $path
186
+ if ($null -eq $kind) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath @($path)) }
187
+ $file = Read-ConflictedFile -Path (Join-Path -Path $Worktree -ChildPath $path)
188
+ if ($null -eq $file) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath @($path)) }
189
+
190
+ $outcome = Merge-ConflictedText -Line $file.Line -Kind $kind
191
+ if ($outcome.Escalate) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath @($path)) }
192
+
193
+ # The three stages are the only authority on what the merge must keep.
194
+ # Each is bound directly, because Invoke-GitExe already returns one array
195
+ # and an array subexpression around it would nest that array inside another.
196
+ $ours = Invoke-GitExe -GitArgs @('-C', $Worktree, 'show', ":2:$path")
197
+ $theirs = Invoke-GitExe -GitArgs @('-C', $Worktree, 'show', ":3:$path")
198
+ $base = Invoke-GitExe -GitArgs @('-C', $Worktree, 'show', ":1:$path")
199
+ $isKept = Test-NeverDropPostCondition -MergedLine $outcome.Lines -OursLine $ours -TheirsLine $theirs -BaseLine $base -Kind $kind
200
+ if (-not $isKept) { return (ConvertTo-ResolutionResult -ResultName 'escalate' -EscalatePath @($path)) }
201
+
202
+ $pending.Add([pscustomobject]@{
203
+ Path = $path
204
+ HasBom = $file.HasBom
205
+ Line = $outcome.Lines
206
+ Record = [ordered]@{
207
+ path = $path
208
+ entries_added_from_ours = @($outcome.EntriesAddedFromOurs)
209
+ entries_added_from_theirs = @($outcome.EntriesAddedFromTheirs)
210
+ version_resolutions = @($outcome.VersionResolutions)
211
+ }
212
+ })
213
+ }
214
+
215
+ # Every path resolved, so the worktree can be rewritten in one pass.
216
+ foreach ($item in $pending) {
217
+ Write-MergedFile -Path (Join-Path -Path $Worktree -ChildPath $item.Path) -Line $item.Line -HasBom $item.HasBom
218
+ }
219
+ return (ConvertTo-ResolutionResult -ResultName 'resolved' -Resolved @($pending | ForEach-Object { $_.Record }))
220
+ }
221
+
222
+ # Dot-sourcing loads the functions for a test host; any other invocation runs the
223
+ # step. The guard is the precedent set by Invoke-ReleaseReconciliation.ps1.
224
+ if ($MyInvocation.InvocationName -ne '.') {
225
+ if ([string]::IsNullOrWhiteSpace($Worktree)) { throw 'Worktree is required.' }
226
+ $result = Invoke-MergeableConflictResolution -Worktree $Worktree -ConfigPath $ConfigPath
227
+ Write-Output (ConvertTo-Json -InputObject $result -Depth 6 -Compress)
228
+ exit 0
229
+ }
@@ -260,6 +260,61 @@ wildcard occupies or truncates the feature-folder segment, a contract token carr
260
260
  letter, and a token containing a placeholder or interpolation marker. `artifacts/` is not a known
261
261
  top-level segment, so a bare `artifacts/**` subtree claim no longer satisfies the shape rules.
262
262
 
263
+ ### Mechanically-mergeable path class (issue #643)
264
+
265
+ Some contended paths are not a design collision. A `.csproj` item list, a `packages.config` package
266
+ list, and an `app.config` binding-redirect list are additive registries: two items that each append a
267
+ distinct entry produce a textual conflict that a deterministic union resolves without judgement.
268
+ Serializing those items buys nothing.
269
+
270
+ `config/blast-radius.json` carries an optional `mergeable_paths` list of exact entries and `**`
271
+ globs. That list is the mechanically-mergeable path class. Its published default is the five-entry
272
+ list `**/*.csproj`, `**/packages.config`, `**/app.config`, `**/*.vbproj`, `**/*.props`.
273
+
274
+ A matching path **stays in the declared `paths`**. The exclusion is applied at exactly one place per
275
+ runtime — inside `conflicts` (`scripts/dev_tools/_blast_radius_conflicts.py`) and inside
276
+ `Test-BlastRadiusConflict` (`.claude/lib/blast-radius/BlastRadius.psm1`), on both radii's `paths`
277
+ immediately before the smallest-path-overlap computation. Derivation, normalization,
278
+ observed-radius construction, V1, V2, V3, and `detect_escaped_paths` are therefore unchanged: the
279
+ class narrows one comparison and nothing else. The key is optional and fail-closed: a truth table
280
+ that omits it excludes nothing and reproduces pre-change behaviour exactly.
281
+
282
+ The matcher is local to this comparison and does not extend the glob engine. A declared path matches
283
+ the class when, against some configured entry, any of the following holds: the two strings are
284
+ ordinally equal; or the declared path is concrete (carries no wildcard) and the configured entry is
285
+ a glob that contains it; or the declared path is concrete and, for a configured entry beginning with
286
+ `**/`, the same two-step test succeeds against the entry with that prefix removed, which is what
287
+ makes a root-level `Directory.Build.props` match `**/*.props`. A declared path that is itself a glob
288
+ is mergeable only when it equals a configured entry verbatim, because a glob can expand over
289
+ non-mergeable files. `_glob_to_regex_text` (`scripts/dev_tools/_blast_radius_glob.py`) and its
290
+ PowerShell mirror `BlastRadiusGlob.psm1` are unchanged by this class; if either is ever changed, both
291
+ it and this matcher must be re-examined together, since the containment step depends on the glob
292
+ engine's semantics.
293
+
294
+ Three constraints bound the mergeable-path class:
295
+
296
+ 1. **The planner remains obliged to enumerate a genuine write explicitly.** The class describes how a
297
+ contention is scored, not what an item may touch. An item that will write a `.csproj` still
298
+ declares that exact path in its radius.
299
+ 2. **The path stays in the declared radius, so the audits still see it.** Nothing is removed from any
300
+ stored record. Over-breadth, shared-surface, and manifest audits read the same `paths` they read
301
+ before; only the pairwise overlap set is narrowed.
302
+ 3. **`detect_escaped_paths` remains the backstop at execution time.** A mergeable classification is a
303
+ scheduling decision taken from declared text; `detect_escaped_paths` still compares the declared
304
+ radius against the paths a diff actually touched, so an item that wrote outside its radius is
305
+ caught against observed evidence regardless of this class.
306
+
307
+ An item record of the parallel-orchestrator checkpoint MAY carry an optional
308
+ `mergeable_conflicts_resolved` list recording the mergeable conflicts an item resolved mechanically.
309
+ The field is additive and tolerated-not-validated: no validator in Python, TypeScript, or bash reads
310
+ it, and it follows the `expected_conflict_components` diagnostic precedent — a record written for a
311
+ human reader and for post-hoc audit, never a scheduling input. Each entry carries `path`,
312
+ `resolved_at`, `merged_against`, `merge_commit_sha`, `entries_added_from_ours`,
313
+ `entries_added_from_theirs`, and `version_resolutions`. Entry keys inside those three lists use
314
+ `<ItemType>:<Include>` for MSBuild items, `package:<id>` for `packages.config` packages, and
315
+ `bindingRedirect:<name>` for `app.config` binding redirects. Invariant 9 (the six `blast_radius`
316
+ keys) and invariant 15 (the four edge reasons) are unchanged by this field.
317
+
263
318
  ### Placeholder-shape rejection (issue #502)
264
319
 
265
320
  The fourth shape is a token containing any member of the marker set
@@ -353,6 +408,15 @@ reading the file is not told something false, and because
353
408
  `tests/scripts/dev_tools/test_blast_radius_config.py` calls `load_module_globs` on it and that
354
409
  helper raises on an absent key. Nothing schedules on it.
355
410
 
411
+ **The .NET manifest family is a structure signal, not a module source (issue #643).** `.csproj`,
412
+ `.fsproj`, `.vbproj`, `.sln`, and `.slnx` are classified in
413
+ `extensions/drm-copilot/src/lib/push-down/claude-blast-radius-derive-manifests.ts` as
414
+ structure-observing manifests: each one marks a project root and suppresses the
415
+ top-level-directory fallback, and none of them contributes a module. A .NET solution layout
416
+ therefore derives exactly `{ "config": ["config/**"] }` from `PAYLOAD_MODULES` alone. The reason is
417
+ the granularity criterion: a per-assembly module would fire for the majority of work items in that
418
+ solution, so it discriminates nothing and belongs in neither the module map nor the surface list.
419
+
356
420
  **`PAYLOAD_MODULES` carries `config` only.** `claude-runtime` was removed from it by the same
357
421
  granularity criterion that removed it from this repository's own map. The criterion transfers
358
422
  without modification: every agent in the runtime is instructed to read the policy rules and process
@@ -366,8 +430,8 @@ non-vacuous input.
366
430
  subset, not a copy of the self-hosted sets.** They were authored narrow when the bundled copy was
367
431
  created and were never a copy that fell behind, so the correct gate is portable-set equality against
368
432
  a declared constant plus a subset relation against the self-hosted list — never byte-equality with
369
- the self-hosted file. Only `version`, `over_breadth_fraction`, and `mandate_reads` are byte-equal
370
- across the two copies.
433
+ the self-hosted file. Only `version`, `over_breadth_fraction`, `mandate_reads`, and `mergeable_paths`
434
+ are byte-equal across the two copies.
371
435
 
372
436
  The reason the two key groups take different relations is an asymmetry between surfaces and modules.
373
437
  An over-matching MODULE glob costs concurrency on every pair of items it touches, because a module