@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.
- package/out/mcp-server.js +30 -78
- package/package.json +2 -2
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +18 -1
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +43 -6
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
- package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +330 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -2
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
- package/resources/claude-customizations/.claude/rules/shell.md +8 -7
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +8 -8
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +234 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_actions_lib.sh +451 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +301 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_dirt_lib.sh +495 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +252 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_lib.sh +496 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_eol_lib.sh +212 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +492 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +476 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +182 -0
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +11 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +1 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
- package/resources/claude-customizations/config/blast-radius.json +14 -1
- package/resources/claude-customizations/pack-manifests/core.json +13 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-resolution.ps1 +493 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +27 -27
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
- 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
|
+
}
|