@danmoisan/drm-copilot-mcp 1.1.12 → 1.1.15

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 (38) hide show
  1. package/out/mcp-server.js +30 -78
  2. package/package.json +2 -2
  3. package/resources/claude-customizations/.claude/agents/parallel-planner.md +18 -1
  4. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
  5. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
  6. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +43 -6
  7. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
  8. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
  9. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
  10. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
  11. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -2
  12. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
  13. package/resources/claude-customizations/.claude/rules/shell.md +8 -7
  14. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
  15. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
  16. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
  17. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
  18. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
  19. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
  20. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
  21. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
  22. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
  23. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
  24. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
  25. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
  26. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +11 -0
  27. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
  28. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
  29. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
  30. package/resources/claude-customizations/config/blast-radius.json +14 -1
  31. package/resources/claude-customizations/pack-manifests/core.json +13 -0
  32. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  33. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
  34. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
  35. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
  36. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
  37. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  38. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -0
@@ -0,0 +1,493 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Epic-scope resolution for the Codex preimplementation gate (issue #707).
4
+
5
+ .DESCRIPTION
6
+ A Codex-local port of the issue #663 resolver (D1). Scope is decided only by the
7
+ session-root epic checkpoint artifacts/orchestration/epic-orchestrator-state.json
8
+ plus an effective-worktree HEAD match with its integration_branch (D3, D4). The
9
+ effective worktree is the -C selector worktree when supplied, otherwise the session
10
+ root. The checkpoint path is composed from the resolved session root, never read from
11
+ command text, and read once per resolution.
12
+
13
+ It performs no subprocess, network, clock, or environment read. Every filesystem
14
+ contact passes through Get-WorktreeResolutionGitEntryKind (the only existence probe)
15
+ and Get-WorktreeResolutionGitFileText (the only content read), so tests mock those.
16
+
17
+ .NOTES
18
+ PowerShell 7+. Functions only, with no StrictMode and no preference assignment.
19
+ Dot-sourced by enforce-orchestration-preimplementation-gate-epic-scope.ps1.
20
+ Mirrored byte-identically under extensions/drm-copilot/resources/codex-and-agents-customizations/.
21
+ AUTHORITY: PowerShell-authoritative. The epic readiness predicate has no reference implementation in another language, and this hook starts no interpreter process (issue #707, D15).
22
+ #>
23
+
24
+ # The one definition of the epic checkpoint location relative to a worktree root.
25
+ $script:EpicScopeCheckpointRelativePath = 'artifacts/orchestration/epic-orchestrator-state.json'
26
+
27
+ # The only route id that makes an epic checkpoint scope-defining. Compared case-sensitively.
28
+ $script:EpicScopeRouteId = 'epic'
29
+
30
+ # The in-progress merge marker probed inside a worktree's git directory.
31
+ $script:EpicScopeMergeHeadFileName = 'MERGE_HEAD'
32
+
33
+ # Git layout names read from a worktree: the root marker, the linked-worktree pointer
34
+ # line, the HEAD file, and the branch reference prefix.
35
+ $script:WorktreeResolutionGitEntryName = '.git'
36
+ $script:WorktreeResolutionGitDirLinePattern = '^gitdir:\s*(?<target>.+)$'
37
+ $script:WorktreeResolutionHeadFileName = 'HEAD'
38
+ $script:WorktreeResolutionBranchReferencePrefix = 'ref: refs/heads/'
39
+
40
+ # A drive-rooted or slash-rooted normalised path.
41
+ $script:WorktreeResolutionAbsolutePathPattern = '^([A-Za-z]:(/|$)|/)'
42
+
43
+ # Upper bound on the upward walk, so a malformed input cannot loop.
44
+ $script:WorktreeResolutionMaximumAscentDepth = 64
45
+
46
+ function Get-WorktreeResolutionGitEntryKind {
47
+ <#
48
+ .SYNOPSIS
49
+ Classify a path as Directory, File, or None. The only existence probe (seam).
50
+ .PARAMETER Path
51
+ The absolute path to classify.
52
+ #>
53
+ [CmdletBinding()]
54
+ [OutputType([string])]
55
+ param(
56
+ [Parameter(Mandatory = $true)]
57
+ [string] $Path
58
+ )
59
+
60
+ if (Test-Path -LiteralPath $Path -PathType Container) {
61
+ return 'Directory'
62
+ }
63
+ if (Test-Path -LiteralPath $Path -PathType Leaf) {
64
+ return 'File'
65
+ }
66
+ return 'None'
67
+ }
68
+
69
+ function Get-WorktreeResolutionGitFileText {
70
+ <#
71
+ .SYNOPSIS
72
+ Read a file's raw text, or $null when unreadable. The only content read (seam).
73
+ .PARAMETER Path
74
+ The absolute path of the file to read.
75
+ #>
76
+ [CmdletBinding()]
77
+ [OutputType([string])]
78
+ param(
79
+ [Parameter(Mandatory = $true)]
80
+ [string] $Path
81
+ )
82
+
83
+ return (Get-Content -LiteralPath $Path -Raw -ErrorAction SilentlyContinue)
84
+ }
85
+
86
+ function ConvertTo-WorktreeResolutionNormalizedPath {
87
+ # Pure: forward slashes, no repeated separator, no trailing slash, no leading ./;
88
+ # $null for a null, empty, or whitespace input. A UNC prefix and / are preserved.
89
+ [CmdletBinding()]
90
+ [OutputType([string])]
91
+ param(
92
+ [Parameter(Mandatory = $true)]
93
+ [AllowNull()]
94
+ [AllowEmptyString()]
95
+ [string] $Path
96
+ )
97
+
98
+ if ([string]::IsNullOrWhiteSpace($Path)) {
99
+ return $null
100
+ }
101
+ $normalized = $Path.Trim() -replace '\\', '/'
102
+ $uncPrefix = if ($normalized.StartsWith('//')) { '/' } else { '' }
103
+ $normalized = $uncPrefix + ($normalized -replace '/{2,}', '/')
104
+ while ($normalized.StartsWith('./')) {
105
+ $normalized = $normalized.Substring(2)
106
+ }
107
+ if ($normalized.Length -gt 1) {
108
+ $normalized = $normalized.TrimEnd('/')
109
+ }
110
+ if ($normalized.Length -eq 0) {
111
+ return $null
112
+ }
113
+ return $normalized
114
+ }
115
+
116
+ function Test-WorktreeResolutionAbsolutePath {
117
+ # Pure: true for a drive-rooted (<drive>:/...) or slash-rooted normalised path.
118
+ [OutputType([bool])]
119
+ param([string] $Path)
120
+
121
+ return ($Path -match $script:WorktreeResolutionAbsolutePathPattern)
122
+ }
123
+
124
+ function Join-WorktreeResolutionSegment {
125
+ # Pure: append a child to a normalised base without doubling the separator.
126
+ [OutputType([string])]
127
+ param([string] $BasePath, [string] $ChildPath)
128
+
129
+ $separator = if ($BasePath.EndsWith('/')) { '' } else { '/' }
130
+ return $BasePath + $separator + $ChildPath
131
+ }
132
+
133
+ function Get-WorktreeResolutionParentPath {
134
+ # Pure: the parent of a normalised absolute path, or $null at the filesystem or
135
+ # drive root. The parent of <drive>:/a is <drive>:, <drive>: has no parent, and the
136
+ # parent of /a is /.
137
+ [OutputType([string])]
138
+ param([string] $Path)
139
+
140
+ $index = $Path.LastIndexOf('/')
141
+ if ($index -lt 0 -or $Path -eq '/') {
142
+ return $null
143
+ }
144
+ return $Path.Substring(0, [math]::Max($index, 1))
145
+ }
146
+
147
+ function Get-WorktreeResolutionFirstLine {
148
+ # Pure: the trimmed first line of a text, or $null when there is none.
149
+ [OutputType([string])]
150
+ param([AllowNull()][string] $Text)
151
+
152
+ if ([string]::IsNullOrWhiteSpace($Text)) {
153
+ return $null
154
+ }
155
+ return ($Text -split "`r?`n", 2)[0].Trim()
156
+ }
157
+
158
+ function Get-WorktreeResolutionGitDirTarget {
159
+ # Pure: the target named by the first line of .git file text when that line is a
160
+ # well-formed gitdir: line, otherwise $null.
161
+ [OutputType([string])]
162
+ param([AllowNull()][string] $Text)
163
+
164
+ $firstLine = Get-WorktreeResolutionFirstLine -Text $Text
165
+ if ($null -ne $firstLine -and $firstLine -match $script:WorktreeResolutionGitDirLinePattern) {
166
+ return $Matches['target'].Trim()
167
+ }
168
+ return $null
169
+ }
170
+
171
+ function Test-WorktreeResolutionRootMarker {
172
+ # True when the level's .git child is a directory, or a file whose first line is a
173
+ # well-formed gitdir: line. Reaches the filesystem only through the seams.
174
+ [CmdletBinding()]
175
+ [OutputType([bool])]
176
+ param(
177
+ [Parameter(Mandatory = $true)]
178
+ [string] $Path
179
+ )
180
+
181
+ $level = ConvertTo-WorktreeResolutionNormalizedPath -Path $Path
182
+ if ($null -eq $level) {
183
+ return $false
184
+ }
185
+ $gitEntry = Join-WorktreeResolutionSegment -BasePath $level -ChildPath $script:WorktreeResolutionGitEntryName
186
+ $kind = Get-WorktreeResolutionGitEntryKind -Path $gitEntry
187
+ if ($kind -ne 'File') {
188
+ return ($kind -eq 'Directory')
189
+ }
190
+ $target = Get-WorktreeResolutionGitDirTarget -Text (Get-WorktreeResolutionGitFileText -Path $gitEntry)
191
+ return ($null -ne $target)
192
+ }
193
+
194
+ function Find-WorktreeResolutionRoot {
195
+ # Ascend from an absolute path to the first worktree-root level; $null for a relative
196
+ # input, an ascent past the filesystem or drive root, or MaximumDepth levels tested.
197
+ [CmdletBinding()]
198
+ [OutputType([string])]
199
+ param(
200
+ [Parameter(Mandatory = $true)]
201
+ [AllowEmptyString()]
202
+ [string] $Path,
203
+
204
+ [ValidateRange(0, 4096)]
205
+ [int] $MaximumDepth = $script:WorktreeResolutionMaximumAscentDepth
206
+ )
207
+
208
+ $current = ConvertTo-WorktreeResolutionNormalizedPath -Path $Path
209
+ if ($null -eq $current -or -not (Test-WorktreeResolutionAbsolutePath -Path $current)) {
210
+ return $null
211
+ }
212
+ for ($depth = 0; ($null -ne $current) -and ($depth -le $MaximumDepth); $depth++) {
213
+ if (Test-WorktreeResolutionRootMarker -Path $current) {
214
+ return $current
215
+ }
216
+ $current = Get-WorktreeResolutionParentPath -Path $current
217
+ }
218
+ return $null
219
+ }
220
+
221
+ function Join-WorktreeResolutionPath {
222
+ # Compose an absolute forward-slash path from a worktree root and a repo-relative
223
+ # path; throws when the root is not absolute.
224
+ [CmdletBinding()]
225
+ [OutputType([string])]
226
+ param(
227
+ [Parameter(Mandatory = $true)]
228
+ [string] $WorktreeRoot,
229
+ [Parameter(Mandatory = $true)]
230
+ [string] $RepoRelativePath
231
+ )
232
+
233
+ $root = ConvertTo-WorktreeResolutionNormalizedPath -Path $WorktreeRoot
234
+ if ($root -notmatch $script:WorktreeResolutionAbsolutePathPattern) {
235
+ throw "WorktreeRoot must be an absolute path: '$WorktreeRoot'."
236
+ }
237
+ $relative = ConvertTo-WorktreeResolutionNormalizedPath -Path $RepoRelativePath
238
+ if ($null -eq $relative) {
239
+ return $root
240
+ }
241
+ $separator = if ($root.EndsWith('/')) { '' } else { '/' }
242
+ return $root + $separator + $relative.TrimStart('/')
243
+ }
244
+
245
+ function Get-EpicScopePropertyValue {
246
+ # Pure: the named property of an object, or $null when the object or the property
247
+ # is absent.
248
+ [OutputType([object])]
249
+ param([AllowNull()][object] $InputObject, [string] $Name)
250
+
251
+ if ($null -eq $InputObject) {
252
+ return $null
253
+ }
254
+ $property = $InputObject.PSObject.Properties[$Name]
255
+ if ($null -eq $property) {
256
+ return $null
257
+ }
258
+ return $property.Value
259
+ }
260
+
261
+ function New-EpicScopeResult {
262
+ # Pure: the single constructor of the resolution result shape.
263
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'Pure in-memory factory that changes no system state.')]
264
+ [OutputType([pscustomobject])]
265
+ param(
266
+ [bool] $IsEpicScope,
267
+ [AllowNull()][string] $CheckpointPath,
268
+ [AllowNull()][object] $Checkpoint,
269
+ [AllowNull()][string] $WorktreeRoot,
270
+ [AllowNull()][string] $Branch,
271
+ [bool] $MergeInProgress,
272
+ [string] $Reason
273
+ )
274
+
275
+ return [pscustomobject]@{
276
+ IsEpicScope = $IsEpicScope
277
+ CheckpointPath = $CheckpointPath
278
+ Checkpoint = $Checkpoint
279
+ WorktreeRoot = $WorktreeRoot
280
+ Branch = $Branch
281
+ MergeInProgress = $MergeInProgress
282
+ Reason = $Reason
283
+ }
284
+ }
285
+
286
+ function Get-EpicScopeCheckpointRelativePath {
287
+ # Return the repository-relative path of the epic checkpoint.
288
+ [CmdletBinding()]
289
+ [OutputType([string])]
290
+ param()
291
+
292
+ return $script:EpicScopeCheckpointRelativePath
293
+ }
294
+
295
+ function Get-EpicScopeCheckpointText {
296
+ <#
297
+ .SYNOPSIS
298
+ Read the epic checkpoint text, or $null when no such file exists (seam).
299
+ .PARAMETER Path
300
+ The absolute path of the epic checkpoint.
301
+ #>
302
+ [CmdletBinding()]
303
+ [OutputType([string])]
304
+ param(
305
+ [Parameter(Mandatory = $true)]
306
+ [string] $Path
307
+ )
308
+
309
+ if ((Get-WorktreeResolutionGitEntryKind -Path $Path) -ne 'File') {
310
+ return $null
311
+ }
312
+ return (Get-WorktreeResolutionGitFileText -Path $Path)
313
+ }
314
+
315
+ function ConvertFrom-EpicScopeCheckpointText {
316
+ # Pure: parse checkpoint text into an object, or $null for null, empty, unparseable,
317
+ # or non-object JSON (an array or a scalar). Never throws.
318
+ [CmdletBinding()]
319
+ [OutputType([pscustomobject])]
320
+ param(
321
+ [Parameter(Mandatory = $true)]
322
+ [AllowNull()]
323
+ [AllowEmptyString()]
324
+ [string] $Text
325
+ )
326
+
327
+ if ([string]::IsNullOrWhiteSpace($Text)) {
328
+ return $null
329
+ }
330
+ try {
331
+ $parsed = ConvertFrom-Json -InputObject $Text -ErrorAction Stop
332
+ } catch {
333
+ # Unparseable text is a fail-closed "not epic scope", never a hook error.
334
+ Write-Debug "EPIC_SCOPE_CHECKPOINT_UNPARSEABLE: $($_.Exception.Message)"
335
+ return $null
336
+ }
337
+ if ($parsed -isnot [System.Management.Automation.PSCustomObject]) {
338
+ return $null
339
+ }
340
+ return $parsed
341
+ }
342
+
343
+ function Get-EpicScopeWorktreeGitDirectory {
344
+ # A worktree's git directory: the .git directory of a main checkout, or the gitdir
345
+ # target of a linked worktree joined to the root when relative; otherwise $null.
346
+ [CmdletBinding()]
347
+ [OutputType([string])]
348
+ param(
349
+ [Parameter(Mandatory = $true)]
350
+ [AllowEmptyString()]
351
+ [string] $WorktreeRoot
352
+ )
353
+
354
+ $root = ConvertTo-WorktreeResolutionNormalizedPath -Path $WorktreeRoot
355
+ if ($null -eq $root -or $root -notmatch $script:WorktreeResolutionAbsolutePathPattern) {
356
+ return $null
357
+ }
358
+ $gitEntry = Join-WorktreeResolutionPath -WorktreeRoot $root -RepoRelativePath $script:WorktreeResolutionGitEntryName
359
+ $kind = Get-WorktreeResolutionGitEntryKind -Path $gitEntry
360
+ # Decide by entry kind: a directory is the git directory itself, a file points at it,
361
+ # and a missing entry means the root carries no git state.
362
+ if ($kind -eq 'Directory') {
363
+ return $gitEntry
364
+ } elseif ($kind -ne 'File') {
365
+ return $null
366
+ }
367
+ $firstLine = Get-WorktreeResolutionFirstLine -Text (Get-WorktreeResolutionGitFileText -Path $gitEntry)
368
+ if ($null -eq $firstLine -or $firstLine -notmatch $script:WorktreeResolutionGitDirLinePattern) {
369
+ return $null
370
+ }
371
+ $target = ConvertTo-WorktreeResolutionNormalizedPath -Path $Matches['target']
372
+ if ($target -match $script:WorktreeResolutionAbsolutePathPattern) {
373
+ return $target
374
+ }
375
+ return (Join-WorktreeResolutionPath -WorktreeRoot $root -RepoRelativePath $target)
376
+ }
377
+
378
+ function Get-EpicScopeWorktreeHeadBranch {
379
+ <#
380
+ .SYNOPSIS
381
+ Return the branch checked out in a worktree, or $null for a detached or unreadable HEAD (seam).
382
+ .PARAMETER WorktreeRoot
383
+ The absolute worktree root.
384
+ #>
385
+ [CmdletBinding()]
386
+ [OutputType([string])]
387
+ param(
388
+ [Parameter(Mandatory = $true)]
389
+ [AllowEmptyString()]
390
+ [string] $WorktreeRoot
391
+ )
392
+
393
+ $gitDirectory = Get-EpicScopeWorktreeGitDirectory -WorktreeRoot $WorktreeRoot
394
+ if ($null -eq $gitDirectory) {
395
+ return $null
396
+ }
397
+ $headPath = Join-WorktreeResolutionPath -WorktreeRoot $gitDirectory -RepoRelativePath $script:WorktreeResolutionHeadFileName
398
+ $firstLine = Get-WorktreeResolutionFirstLine -Text (Get-WorktreeResolutionGitFileText -Path $headPath)
399
+ if ($null -eq $firstLine -or -not $firstLine.StartsWith($script:WorktreeResolutionBranchReferencePrefix)) {
400
+ return $null
401
+ }
402
+ $branch = $firstLine.Substring($script:WorktreeResolutionBranchReferencePrefix.Length).Trim()
403
+ if ($branch.Length -eq 0) {
404
+ return $null
405
+ }
406
+ return $branch
407
+ }
408
+
409
+ function Test-EpicScopeMergeInProgress {
410
+ <#
411
+ .SYNOPSIS
412
+ Report whether a merge is in progress in a worktree (MERGE_HEAD exists) (seam).
413
+ .PARAMETER WorktreeRoot
414
+ The absolute worktree root.
415
+ #>
416
+ [CmdletBinding()]
417
+ [OutputType([bool])]
418
+ param(
419
+ [Parameter(Mandatory = $true)]
420
+ [AllowEmptyString()]
421
+ [string] $WorktreeRoot
422
+ )
423
+
424
+ $gitDirectory = Get-EpicScopeWorktreeGitDirectory -WorktreeRoot $WorktreeRoot
425
+ if ($null -eq $gitDirectory) {
426
+ return $false
427
+ }
428
+ $mergeHeadPath = Join-WorktreeResolutionPath -WorktreeRoot $gitDirectory -RepoRelativePath $script:EpicScopeMergeHeadFileName
429
+ return ((Get-WorktreeResolutionGitEntryKind -Path $mergeHeadPath) -eq 'File')
430
+ }
431
+
432
+ function Resolve-EpicScopeCheckpoint {
433
+ <#
434
+ .SYNOPSIS
435
+ Decide whether a gated command or path leg is an epic-level operation and return the governing epic checkpoint.
436
+ .DESCRIPTION
437
+ Never throws; fixed head matching (D3). Returns IsEpicScope, CheckpointPath,
438
+ Checkpoint, WorktreeRoot, Branch, MergeInProgress, and Reason. The first matching
439
+ step decides: session-root-unresolved; epic-checkpoint-absent-or-unparseable;
440
+ route_id (not epic); integration_branch (empty); selector-unresolved;
441
+ branch-mismatch (effective HEAD detached or different). Otherwise the reason is
442
+ epic-scope and the effective worktree is probed for a merge in progress.
443
+ .PARAMETER SessionRoot
444
+ The session directory; defaults to the current location.
445
+ .PARAMETER WorktreeSelector
446
+ The -C selector value of a command leg; its worktree's HEAD then decides.
447
+ #>
448
+ [CmdletBinding()]
449
+ [OutputType([pscustomobject])]
450
+ param(
451
+ [AllowNull()]
452
+ [AllowEmptyString()]
453
+ [string] $SessionRoot = (Get-Location).ProviderPath,
454
+
455
+ [AllowNull()]
456
+ [AllowEmptyString()]
457
+ [string] $WorktreeSelector
458
+ )
459
+
460
+ $root = Find-WorktreeResolutionRoot -Path $SessionRoot
461
+ if ($null -eq $root) {
462
+ return (New-EpicScopeResult -IsEpicScope $false -Reason 'session-root-unresolved')
463
+ }
464
+ $path = Join-WorktreeResolutionPath -WorktreeRoot $root -RepoRelativePath (Get-EpicScopeCheckpointRelativePath)
465
+ $checkpoint = ConvertFrom-EpicScopeCheckpointText -Text (Get-EpicScopeCheckpointText -Path $path)
466
+ if ($null -eq $checkpoint) {
467
+ return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'epic-checkpoint-absent-or-unparseable')
468
+ }
469
+ if ([string](Get-EpicScopePropertyValue -InputObject $checkpoint -Name 'route_id') -cne $script:EpicScopeRouteId) {
470
+ return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'route_id')
471
+ }
472
+ $integrationBranch = [string](Get-EpicScopePropertyValue -InputObject $checkpoint -Name 'integration_branch')
473
+ if ([string]::IsNullOrWhiteSpace($integrationBranch)) {
474
+ return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'integration_branch')
475
+ }
476
+
477
+ # The effective worktree's HEAD decides, the selector worktree taking precedence, so the
478
+ # merge probe inspects the worktree the call operates on.
479
+ $effectiveRoot = $root
480
+ if (-not [string]::IsNullOrWhiteSpace($WorktreeSelector)) {
481
+ $effectiveRoot = Find-WorktreeResolutionRoot -Path $WorktreeSelector
482
+ }
483
+ if ($null -eq $effectiveRoot) {
484
+ return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'selector-unresolved')
485
+ }
486
+ $candidate = Get-EpicScopeWorktreeHeadBranch -WorktreeRoot $effectiveRoot
487
+ if ($null -eq $candidate -or $candidate -cne $integrationBranch) {
488
+ return (New-EpicScopeResult -IsEpicScope $false -CheckpointPath $path -Reason 'branch-mismatch')
489
+ }
490
+
491
+ $mergeInProgress = [bool](Test-EpicScopeMergeInProgress -WorktreeRoot $effectiveRoot)
492
+ return (New-EpicScopeResult -IsEpicScope $true -CheckpointPath $path -Checkpoint $checkpoint -WorktreeRoot $effectiveRoot -Branch $candidate -MergeInProgress $mergeInProgress -Reason 'epic-scope')
493
+ }
@@ -0,0 +1,189 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Epic-scope command and path legs of the Codex preimplementation gate (issue #707).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by enforce-orchestration-preimplementation-gate.ps1. The Codex counterpart
7
+ of the issue #663 Claude sibling, holding two groups of functions:
8
+
9
+ - The two per-mode read seams of issue #554 (Get-EpicCheckpointContent and
10
+ Get-ParallelCheckpointContent), relocated verbatim from the gate file so the gate
11
+ stays inside the 500-line cap. Their names and behaviour are unchanged, so tests
12
+ that mock or shadow them by name are unaffected.
13
+ - The epic-scope decision for the command, apply_patch, and path legs. A call is epic
14
+ scope when the effective worktree's HEAD (the -C selector worktree when present,
15
+ otherwise the session root) equals the integration_branch of
16
+ artifacts/orchestration/epic-orchestrator-state.json. In epic scope the call is
17
+ decided by the command-leg readiness predicate, and under issue #663 decision D2 an
18
+ implementation-classified operand is allowed only while a merge is in progress in
19
+ that worktree. Outside epic scope the decision function returns $null and the gate's
20
+ single-feature path runs unchanged.
21
+
22
+ .NOTES
23
+ PowerShell 7+. Functions only: no StrictMode and no preference assignment, because a
24
+ dot-sourced file changes its caller's scope. Call-time dependencies:
25
+ Split-OrchestrationCommandLine and ConvertTo-OrchestrationCommandToken (helpers file),
26
+ Get-OrchestrationDelegationCheckpointPath (modes file), and the gate's allow and block
27
+ decision constructors Get-OrchestrationPreimplementationGateAllowDecision and
28
+ Get-OrchestrationPreimplementationGateBlockDecision. The resolver and its seams come
29
+ from the resolution sibling dot-sourced below.
30
+ Mirrored byte-identically under extensions/drm-copilot/resources/codex-and-agents-customizations/.
31
+ AUTHORITY: PowerShell-authoritative. The epic readiness predicate has no reference implementation in another language, and this hook starts no interpreter process (issue #707, D15).
32
+ #>
33
+
34
+ . (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-epic-resolution.ps1')
35
+
36
+ # An epic manifest lives under the epics tree; the pattern is tested after backslash
37
+ # normalisation so either separator spelling is accepted.
38
+ $script:EpicScopeManifestPattern = '(^|/)docs/features/epics/'
39
+
40
+ # The two per-mode read seams (issue #554). Each takes its path from the fixed mode
41
+ # table and never from a delegation's own text; an absent file returns an empty
42
+ # string, which the readiness predicate then treats as a deny.
43
+ function Get-EpicCheckpointContent {
44
+ [CmdletBinding()]
45
+ [OutputType([string])]
46
+ param()
47
+
48
+ $path = Get-OrchestrationDelegationCheckpointPath -Mode 'epic'
49
+ if (-not (Test-Path -LiteralPath $path)) {
50
+ return ''
51
+ }
52
+ return Get-Content -Raw -LiteralPath $path
53
+ }
54
+
55
+ function Get-ParallelCheckpointContent {
56
+ [CmdletBinding()]
57
+ [OutputType([string])]
58
+ param()
59
+
60
+ $path = Get-OrchestrationDelegationCheckpointPath -Mode 'parallel'
61
+ if (-not (Test-Path -LiteralPath $path)) {
62
+ return ''
63
+ }
64
+ return Get-Content -Raw -LiteralPath $path
65
+ }
66
+
67
+ function Get-OrchestrationEpicScopeSelector {
68
+ <#
69
+ .SYNOPSIS
70
+ Returns the value of a leading `git -C <value>` selector, or $null.
71
+ .DESCRIPTION
72
+ Pure. Reads only the first segment of the command line, split and tokenized with
73
+ the helpers-file parser so quoting is handled exactly as the staging exemption
74
+ handles it. An unbalanced command line has no decidable selector and returns $null.
75
+ .PARAMETER Command
76
+ The full command line.
77
+ .OUTPUTS
78
+ System.String
79
+ #>
80
+ [CmdletBinding()]
81
+ [OutputType([string])]
82
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Command)
83
+
84
+ $split = Split-OrchestrationCommandLine -CommandText $Command
85
+ if (-not $split.Balanced -or @($split.Segments).Count -eq 0) {
86
+ return $null
87
+ }
88
+ $token = @(ConvertTo-OrchestrationCommandToken -Segment ([string]@($split.Segments)[0]).Trim())
89
+ if ($token.Count -ge 3 -and $token[0] -ceq 'git' -and $token[1] -ceq '-C' -and $token[2]) {
90
+ return $token[2]
91
+ }
92
+ return $null
93
+ }
94
+
95
+ function Get-EpicCommandLegReadinessFailure {
96
+ <#
97
+ .SYNOPSIS
98
+ Name the first failed conjunct of the epic command-leg readiness shape, or ''.
99
+ .DESCRIPTION
100
+ Conjuncts, in order: checkpoint-absent; route_id; epic_feature_folder (empty);
101
+ epic_manifest_path (empty, or not under docs/features/epics/ after backslash
102
+ normalisation); integration_branch (empty); features (absent or empty);
103
+ merge-in-progress (no merge in progress in the effective worktree, D2). Returns
104
+ the empty string when every conjunct holds, as the issue #663 predicate does.
105
+ .PARAMETER Checkpoint
106
+ The parsed epic checkpoint object, or $null.
107
+ .PARAMETER MergeInProgress
108
+ Whether MERGE_HEAD exists in the effective worktree's git directory.
109
+ .OUTPUTS
110
+ System.String
111
+ #>
112
+ [CmdletBinding()]
113
+ [OutputType([string])]
114
+ param(
115
+ [Parameter(Mandatory = $true)]
116
+ [AllowNull()]
117
+ [object] $Checkpoint,
118
+
119
+ [Parameter(Mandatory = $true)]
120
+ [bool] $MergeInProgress
121
+ )
122
+
123
+ if ($null -eq $Checkpoint) {
124
+ return 'checkpoint-absent'
125
+ }
126
+ if ([string](Get-EpicScopePropertyValue -InputObject $Checkpoint -Name 'route_id') -cne $script:EpicScopeRouteId) {
127
+ return 'route_id'
128
+ }
129
+ if ([string]::IsNullOrWhiteSpace([string](Get-EpicScopePropertyValue -InputObject $Checkpoint -Name 'epic_feature_folder'))) {
130
+ return 'epic_feature_folder'
131
+ }
132
+ $manifestPath = ([string](Get-EpicScopePropertyValue -InputObject $Checkpoint -Name 'epic_manifest_path')) -replace '\\', '/'
133
+ if ([string]::IsNullOrWhiteSpace($manifestPath) -or $manifestPath -notmatch $script:EpicScopeManifestPattern) {
134
+ return 'epic_manifest_path'
135
+ }
136
+ if ([string]::IsNullOrWhiteSpace([string](Get-EpicScopePropertyValue -InputObject $Checkpoint -Name 'integration_branch'))) {
137
+ return 'integration_branch'
138
+ }
139
+ # An empty features array unrolls to $null through the property read.
140
+ $features = Get-EpicScopePropertyValue -InputObject $Checkpoint -Name 'features'
141
+ if ($null -eq $features -or @($features).Count -eq 0) {
142
+ return 'features'
143
+ }
144
+ if (-not $MergeInProgress) {
145
+ return 'merge-in-progress'
146
+ }
147
+ return ''
148
+ }
149
+
150
+ function Get-OrchestrationEpicScopeDecision {
151
+ <#
152
+ .SYNOPSIS
153
+ Returns the epic-scope decision for an implementation-classified command or path, or $null.
154
+ .DESCRIPTION
155
+ Resolves epic scope with worktree-HEAD matching and the command's -C selector. When
156
+ the call is not epic scope, returns $null so the caller's single-feature path runs
157
+ unchanged. In epic scope, returns an allow decision when the epic command-leg
158
+ readiness predicate passes, and otherwise a deny naming the epic checkpoint and the
159
+ failed conjunct.
160
+ .PARAMETER Command
161
+ The command line of a command or apply_patch leg; empty for a path leg.
162
+ .PARAMETER FilePath
163
+ The file path of a path leg; empty for a command leg.
164
+ .OUTPUTS
165
+ System.Collections.Specialized.OrderedDictionary, or $null outside epic scope.
166
+ #>
167
+ [CmdletBinding()]
168
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
169
+ param(
170
+ [AllowNull()][AllowEmptyString()][string] $Command = '',
171
+ [AllowNull()][AllowEmptyString()][string] $FilePath = ''
172
+ )
173
+
174
+ # A call carrying neither a command nor a path has no leg to decide.
175
+ if (-not $Command -and -not $FilePath) {
176
+ return $null
177
+ }
178
+ $selector = if ($Command) { Get-OrchestrationEpicScopeSelector -Command $Command } else { $null }
179
+ $scope = Resolve-EpicScopeCheckpoint -SessionRoot (Get-Location).Path -WorktreeSelector $selector
180
+ if (-not $scope.IsEpicScope) {
181
+ return $null
182
+ }
183
+
184
+ $failure = Get-EpicCommandLegReadinessFailure -Checkpoint $scope.Checkpoint -MergeInProgress $scope.MergeInProgress
185
+ if (-not $failure) {
186
+ return Get-OrchestrationPreimplementationGateAllowDecision
187
+ }
188
+ return Get-OrchestrationPreimplementationGateBlockDecision -Reason ("PREIMPLEMENTATION_GATE_BLOCKED: this epic-scope operation was evaluated against $($scope.CheckpointPath), and the failed readiness predicate is '$failure'. Implementation operations in epic scope require that checkpoint to satisfy every readiness predicate, and a production path may be staged or edited only while a merge is in progress.")
189
+ }