@danmoisan/drm-copilot-mcp 1.1.10 → 1.1.12

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 (82) hide show
  1. package/out/mcp-server.js +3517 -1104
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
  4. package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
  5. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +15 -2
  6. package/resources/claude-customizations/.claude/agents/parallel-planner.md +3 -0
  7. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +82 -51
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
  11. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
  12. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  14. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -28
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +103 -6
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +124 -8
  17. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +171 -15
  18. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +50 -10
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
  20. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
  21. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
  22. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +35 -7
  23. package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +483 -0
  24. package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +483 -0
  25. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +218 -6
  26. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +18 -75
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +290 -0
  28. package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +415 -0
  29. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +1 -0
  30. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMerge.psm1 +355 -0
  31. package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMergeGrammar.psm1 +318 -0
  32. package/resources/claude-customizations/.claude/lib/project-file-merge/Resolve-MergeableConflict.ps1 +229 -0
  33. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
  34. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
  35. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
  36. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
  37. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
  38. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
  39. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
  40. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +323 -16
  41. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
  42. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
  43. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +46 -1
  44. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
  45. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +75 -8
  46. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +6 -0
  47. package/resources/claude-customizations/.claude/skills/powershell-orchestration-state-machine/SKILL.md +29 -1
  48. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
  49. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
  50. package/resources/claude-customizations/config/blast-radius.json +7 -0
  51. package/resources/claude-customizations/pack-manifests/core.json +16 -0
  52. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
  53. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +41 -0
  54. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -0
  55. package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +26 -0
  56. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  57. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +242 -3
  58. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +67 -12
  59. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +34 -8
  60. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
  61. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -1
  62. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +34 -7
  63. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +483 -0
  64. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +483 -0
  65. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +130 -2
  66. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
  67. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
  68. package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
  69. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
  70. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
  71. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
  72. package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
  73. package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
  74. package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
  75. package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
  76. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
  77. package/resources/codex-and-agents-customizations/pack-manifests/core.json +23 -1
  78. package/resources/config/orchestration-handoff-registry.json +138 -0
  79. package/resources/config/orchestration-handoff.schema.json +472 -0
  80. package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
  81. package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
  82. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +64 -0
@@ -15,10 +15,13 @@
15
15
  payload-reader migration (issue #501) added the module import, the envelope-anomaly
16
16
  mapping, and the entry-point seam. The split is a pure move with no behaviour change.
17
17
 
18
- Both helpers read the script-scoped configuration variables the parent hook assigns
19
- ($script:PrContextArtifactPath, $script:OrchestratorStateCheckpointPath) and call the
20
- parent hook's injectable read seams, so this file is only ever dot-sourced from that
21
- hook and never invoked on its own.
18
+ Both helpers read the script-scoped configuration the parent hook declares and call the
19
+ parent hook's injectable read seams, so this file is only ever dot-sourced from that hook
20
+ and never invoked on its own. $script:PrContextArtifactPath is a process-directory-relative
21
+ artifact path and stays one. $script:OrchestratorStateCheckpointPath is different after
22
+ issue #673: the parent declares it null, and Get-PrAuthorBypassReason assigns it the
23
+ absolute checkpoint path of the worktree identity resolution selected, so every later
24
+ reader receives one resolved path rather than deriving its own.
22
25
 
23
26
  .NOTES
24
27
  Compatible with PowerShell 7+. No external module dependencies.
@@ -26,6 +29,97 @@
26
29
  [CmdletBinding()]
27
30
  param()
28
31
 
32
+ # Shared command-line parser (issue #545). Dot-sourced here as well as from the parent hook
33
+ # so this file keeps working when a test dot-sources it directly.
34
+ . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
35
+ . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
36
+ # Worktree target resolution (issue #687). The checkpoint this gate validates must belong to
37
+ # the item the call is about. Reading a session-root-relative path made that the item whose
38
+ # worktree happened to occupy the session root, so in a parallel or epic topology the gate
39
+ # could return allow on the strength of a sibling item's state -- a false green, which is
40
+ # worse than a false denial because it reports success for a lifecycle it never checked.
41
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeResolution.psm1') -Force -ErrorAction Stop
42
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeTargetResolution.psm1') -Force -ErrorAction Stop
43
+ # Portable-identity resolution (issue #673). A path signal cannot select a worktree: a merged
44
+ # feature folder exists in every checkout branched from main, so it places a call in many at
45
+ # once. The item is identified by its canonical issue number and its branch instead.
46
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force -ErrorAction Stop
47
+ # Epic scope (issue #663): the epic integration pull request is gated against the epic checkpoint.
48
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
49
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeReadiness.psm1') -Force -ErrorAction Stop
50
+
51
+ function Resolve-PrAuthorWorktreeTarget {
52
+ <#
53
+ .SYNOPSIS
54
+ Resolve the call's target worktree. Tests mock this function (resolution seam).
55
+ .DESCRIPTION
56
+ A seam rather than a direct call because the resolver reads real git state: the
57
+ worktree list, each registration's branch, and the filesystem. A test that drove it
58
+ directly would depend on the machine's live worktrees and would pass or fail
59
+ according to what other sessions happen to have checked out, which the repository's
60
+ determinism policy forbids. Mocking here lets a test state the resolution outcome it
61
+ is exercising and nothing else.
62
+ .OUTPUTS
63
+ The target result object from Resolve-WorktreeItemTarget.
64
+ #>
65
+ [CmdletBinding()]
66
+ param(
67
+ [Parameter(Mandatory)]
68
+ [string] $CommandText
69
+ )
70
+
71
+ return (Resolve-WorktreeItemTarget -Text $CommandText -SessionRoot (Get-Location).Path)
72
+ }
73
+
74
+ function Get-PrAuthorTargetCheckpointResolution {
75
+ <#
76
+ .SYNOPSIS
77
+ Resolve the checkpoint path this call must be validated against.
78
+ .DESCRIPTION
79
+ Resolves the target worktree from the command text by portable identity and maps the
80
+ four resolution states onto a checkpoint path or a deny reason:
81
+
82
+ SessionRoot -> that worktree's own checkpoint, composed absolutely.
83
+ OtherWorktree -> that worktree's own checkpoint, composed the same way.
84
+ NoTarget -> deny, carrying the code Get-WorktreeResolutionNoTargetReasonCode
85
+ returns. pr-author passes --head on every gh pr create, so a call
86
+ reaching here omitted the explicit target; the remedy is to supply
87
+ it, never to fall back to whichever checkpoint occupies the
88
+ session root.
89
+ Ambiguous -> deny, carrying the code
90
+ Get-WorktreeResolutionAmbiguityReasonCode returns.
91
+
92
+ The two resolved states share one branch because the path is now composed the same
93
+ way for both: identity selects the worktree, and the checkpoint is read beneath it.
94
+ A session-root result is not a special case, it is the case where identity happened
95
+ to select the worktree the process is already running in.
96
+
97
+ Returning a deny for the two unresolved states is what keeps the gate honest: it
98
+ refuses to answer rather than answering from unrelated state. Neither reason-code
99
+ literal appears in this file; both come from the accessors named above.
100
+ .OUTPUTS
101
+ System.Collections.Specialized.OrderedDictionary with CheckpointPath and Reason.
102
+ #>
103
+ [CmdletBinding()]
104
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
105
+ param(
106
+ [Parameter(Mandatory)]
107
+ [string] $CommandText
108
+ )
109
+
110
+ $target = Resolve-PrAuthorWorktreeTarget -CommandText $CommandText
111
+
112
+ switch ($target.Status) {
113
+ { $_ -in @('SessionRoot', 'OtherWorktree') } {
114
+ return [ordered]@{ CheckpointPath = (Get-WorktreeItemCheckpointPath -WorktreeRoot $target.WorktreeRoot); Reason = $null }
115
+ }
116
+ default {
117
+ $reason = "$($target.ReasonCode): $($target.Detail) The pr-author gate will not validate this call against the session root's checkpoint, because that checkpoint may belong to a different item. Pass --head <branch> on the gh pr create command so the call names its own target."
118
+ return [ordered]@{ CheckpointPath = $null; Reason = $reason }
119
+ }
120
+ }
121
+ }
122
+
29
123
  function Test-PrAuthorReceiptVerification {
30
124
  <#
31
125
  .SYNOPSIS
@@ -50,6 +144,12 @@ function Test-PrAuthorReceiptVerification {
50
144
  with Write access to artifacts/ can replace the body file and the receipt together.
51
145
  .PARAMETER CommandText
52
146
  The Bash command text containing the --body-file argument.
147
+ .PARAMETER CheckpointPath
148
+ The absolute checkpoint path identity resolution selected, passed through to check 6
149
+ so that check reads the resolved worktree's checkpoint rather than deriving its own.
150
+ .PARAMETER EpicScope
151
+ Optional. The epic-scope result Get-PrAuthorBypassReason resolved (issue #663), passed
152
+ through to check 6 so the call is resolved once; $null outside epic scope.
53
153
  .OUTPUTS
54
154
  System.String or $null
55
155
  #>
@@ -57,7 +157,13 @@ function Test-PrAuthorReceiptVerification {
57
157
  [OutputType([string])]
58
158
  param(
59
159
  [Parameter(Mandatory)]
60
- [string] $CommandText
160
+ [string] $CommandText,
161
+
162
+ [Parameter(Mandatory)]
163
+ [string] $CheckpointPath,
164
+
165
+ [AllowNull()]
166
+ [object] $EpicScope
61
167
  )
62
168
 
63
169
  # Check 1: the --body-file argument must match the canonical artifacts/pr_body_<N>.md pattern.
@@ -128,7 +234,7 @@ function Test-PrAuthorReceiptVerification {
128
234
  }
129
235
 
130
236
  # Check 6: under epic_mode, gh pr create must carry a matching --base override.
131
- $epicBaseBranchReason = Test-EpicBaseBranchOverride -CommandText $CommandText
237
+ $epicBaseBranchReason = Test-EpicBaseBranchOverride -CommandText $CommandText -CheckpointPath $CheckpointPath -EpicScope $EpicScope
132
238
  if ($epicBaseBranchReason) {
133
239
  return $epicBaseBranchReason
134
240
  }
@@ -166,16 +272,43 @@ function Get-PrAuthorBypassReason {
166
272
  [bool] $ContextExists
167
273
  )
168
274
 
169
- # Only act on gh pr create or gh pr edit subcommands.
170
- $isPrCreate = $CommandText -match '(?i)\bgh\s+pr\s+create\b'
171
- $isPrEdit = $CommandText -match '(?i)\bgh\s+pr\s+edit\b'
275
+ # Only act on gh pr create or gh pr edit subcommands. The test is structural: a segment
276
+ # must INVOKE gh with the subcommand path, so quoted prose that merely mentions the
277
+ # phrase no longer triggers the gate, and a relocating spelling that carries a gh global
278
+ # option between the command word and the subcommand now does.
279
+ $isPrCreate = Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create')
280
+ $isPrEdit = Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'edit')
172
281
 
173
282
  if (-not $isPrCreate -and -not $isPrEdit) {
174
283
  return $null
175
284
  }
176
285
 
177
- $hasBodyFile = $CommandText -match '(?i)--body-file\b'
178
- $hasInlineBody = $CommandText -match '(?i)--body(?!-file)\b'
286
+ # Flag presence comes from the matched segment's tokens. Exact token comparison is what
287
+ # keeps '--body' from matching '--body-file'; the previous negative lookahead expressed
288
+ # the same distinction over raw text.
289
+ $subcommandPath = if ($isPrCreate) { @('pr', 'create') } else { @('pr', 'edit') }
290
+ $hasBodyFile = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--body-file'
291
+ $hasInlineBody = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--body'
292
+
293
+ # Raw-scan fallback for a wrapper-led, live-substitution, or unbalanced segment. A
294
+ # wrapper's quoted argument collapses into ONE token, so both flags read absent there and
295
+ # the gh pr edit no-body branch below allowed bash -c "gh pr edit 42 --body 'x'". The
296
+ # --body-file test runs first and wins, so a --body-file carried inside a wrapper is never
297
+ # misread as an inline body. That ordering restores the pre-parser routing, in which one
298
+ # whole-text --body-file match set $hasBodyFile for exactly this input.
299
+ if (-not $hasBodyFile -and -not $hasInlineBody) {
300
+ $comparison = [System.StringComparison]::OrdinalIgnoreCase
301
+ foreach ($segment in @(Read-CommandLineSegment -CommandText $CommandText)) {
302
+ if (-not (Test-CommandLineSegmentRawScan -Segment $segment)) {
303
+ continue
304
+ }
305
+ if ($segment.ScanText.IndexOf('--body-file', $comparison) -ge 0) {
306
+ $hasBodyFile = $true
307
+ } elseif ($segment.ScanText.IndexOf('--body', $comparison) -ge 0) {
308
+ $hasInlineBody = $true
309
+ }
310
+ }
311
+ }
179
312
 
180
313
  # Case A: gh pr create OR gh pr edit with inline --body (not --body-file). Evaluated before the
181
314
  # gh pr edit no-body allow short-circuit so inline-body edits are blocked, not allowed.
@@ -202,13 +335,36 @@ function Get-PrAuthorBypassReason {
202
335
  return "PR_CONTEXT_MISSING: ``$script:PrContextArtifactPath`` is absent. Run ``mcp__drm-copilot__collect_pr_context`` before creating or editing the PR body."
203
336
  }
204
337
 
338
+ # Epic scope (issue #663): when the call's --head equals the epic checkpoint's
339
+ # integration_branch, the integration pull request is gated by the epic PR-creation
340
+ # readiness predicate on artifacts/orchestration/epic-orchestrator-state.json, and no
341
+ # per-feature checkpoint is resolved or read. Resolved once; check 6 reuses the result.
342
+ $epicScope = $null
343
+ if ($hasBodyFile -and $ContextExists) {
344
+ $epicScope = Resolve-EpicScopeCheckpoint -Text $CommandText -SessionRoot (Get-Location).Path
345
+ }
346
+
205
347
  # Orchestrator-state preflight: runs inside this same PreToolUse hook (so it cannot be
206
348
  # bypassed by invoking gh pr create/edit directly) before receipt verification.
207
- if ($hasBodyFile -and $ContextExists) {
208
- $preflightResult = Invoke-OrchestratorStatePreflight -CheckpointPath $script:OrchestratorStateCheckpointPath
349
+ if ($null -ne $epicScope -and $epicScope.IsEpicScope) {
350
+ $failure = Get-EpicPrCreationReadinessFailure -Checkpoint $epicScope.Checkpoint -HeadBranch $epicScope.Branch
351
+ if ($failure) {
352
+ return "ORCHESTRATOR_STATE_PREFLIGHT_FAILED: this epic-scope pull request was evaluated against $($epicScope.CheckpointPath), and the failed readiness predicate is '$failure'."
353
+ }
354
+ $script:OrchestratorStateCheckpointPath = $epicScope.CheckpointPath
355
+ } elseif ($hasBodyFile -and $ContextExists) {
356
+ $resolution = Get-PrAuthorTargetCheckpointResolution -CommandText $CommandText
357
+ if ($resolution.Reason) {
358
+ return $resolution.Reason
359
+ }
360
+ # Assigned once here and read by every later consumer, so the preflight, the receipt
361
+ # verifier, and the epic base-branch check cannot disagree about which item is gated.
362
+ $script:OrchestratorStateCheckpointPath = $resolution.CheckpointPath
363
+ $checkpointPath = $script:OrchestratorStateCheckpointPath
364
+ $preflightResult = Invoke-OrchestratorStatePreflight -CheckpointPath $checkpointPath
209
365
  if ($preflightResult.HasErrors) {
210
366
  $preflightSummary = if ([string]::IsNullOrWhiteSpace($preflightResult.ErrorText)) {
211
- "checkpoint missing at $script:OrchestratorStateCheckpointPath"
367
+ "checkpoint missing at $checkpointPath"
212
368
  } else {
213
369
  $preflightResult.ErrorText
214
370
  }
@@ -218,7 +374,7 @@ function Get-PrAuthorBypassReason {
218
374
 
219
375
  # Receipt verification: extends, and does not replace, the previously-allowed path.
220
376
  if ($hasBodyFile -and $ContextExists) {
221
- $receiptReason = Test-PrAuthorReceiptVerification -CommandText $CommandText
377
+ $receiptReason = Test-PrAuthorReceiptVerification -CommandText $CommandText -CheckpointPath $script:OrchestratorStateCheckpointPath -EpicScope $epicScope
222
378
  if ($receiptReason) {
223
379
  return $receiptReason
224
380
  }
@@ -9,25 +9,35 @@
9
9
  structural extraction only: no logic, decision behavior, or reason-string wording changed.
10
10
  #>
11
11
 
12
+ # Shared command-line parser (issue #545). Dot-sourced here as well as from the parent hook
13
+ # so this file keeps working when a test dot-sources it directly.
14
+ . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
15
+ . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
16
+
12
17
  function Get-PrAuthorCheckpointContent {
13
18
  <#
14
19
  .SYNOPSIS
15
20
  Read the raw JSON text of the per-feature orchestrator checkpoint. Tests mock this
16
21
  function (read seam) for the epic-mode base-branch override check.
17
22
  .DESCRIPTION
18
- Returns the raw text content of artifacts/orchestration/orchestrator-state.json, or
19
- $null when the file is absent. This is the injectable boundary for checkpoint content
20
- used by Test-EpicBaseBranchOverride; no test writes the checkpoint file to disk.
23
+ Returns the raw text content of the checkpoint at the supplied path, or $null when the
24
+ file is absent. This is the injectable boundary for checkpoint content used by
25
+ Test-EpicBaseBranchOverride; no test writes the checkpoint file to disk.
26
+
27
+ The path is mandatory and carries no default (issue #673). A relative default was the
28
+ second of this feature's three bindings: it made check 6 read whichever checkpoint
29
+ occupied the process directory even when target resolution had already selected a
30
+ different worktree, so the epic verdict could come from an unrelated item.
21
31
  .PARAMETER CheckpointPath
22
- The relative path to the per-feature orchestrator checkpoint.
32
+ The absolute path of the checkpoint belonging to the resolved worktree.
23
33
  .OUTPUTS
24
34
  System.String or $null
25
35
  #>
26
36
  [CmdletBinding()]
27
37
  [OutputType([string])]
28
38
  param(
29
- [Parameter(Mandatory = $false)]
30
- [string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
39
+ [Parameter(Mandatory)]
40
+ [string] $CheckpointPath
31
41
  )
32
42
 
33
43
  if (-not (Test-Path -LiteralPath $CheckpointPath)) {
@@ -52,6 +62,13 @@ function Test-EpicBaseBranchOverride {
52
62
  unchanged.
53
63
  .PARAMETER CommandText
54
64
  The Bash command text under evaluation.
65
+ .PARAMETER CheckpointPath
66
+ The absolute checkpoint path identity resolution selected, passed straight through to
67
+ the read seam so this check and the orchestrator-state preflight read one file.
68
+ .PARAMETER EpicScope
69
+ Optional. The epic-scope result gate 1 resolved (issue #663). When it reports epic
70
+ scope, the integration pull request must pass --base main and the per-feature
71
+ checkpoint seam is not read; $null keeps the per-feature epic_mode rule unchanged.
55
72
  .OUTPUTS
56
73
  System.String or $null
57
74
  #>
@@ -59,16 +76,35 @@ function Test-EpicBaseBranchOverride {
59
76
  [OutputType([string])]
60
77
  param(
61
78
  [Parameter(Mandatory)]
62
- [string] $CommandText
79
+ [string] $CommandText,
80
+
81
+ [Parameter(Mandatory)]
82
+ [string] $CheckpointPath,
83
+
84
+ [AllowNull()]
85
+ [object] $EpicScope
63
86
  )
64
87
 
65
88
  # The epic-mode base-branch override only constrains gh pr create; gh pr edit does not
66
89
  # re-target the base branch, so non-create commands are out of scope for this check.
67
- if ($CommandText -notmatch '(?i)\bgh\s+pr\s+create\b') {
90
+ # The test is structural, so a quoted mention of the phrase is out of scope and a
91
+ # relocating spelling carrying a gh global option is in scope (issue #545).
92
+ if (-not (Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create'))) {
93
+ return $null
94
+ }
95
+
96
+ # Epic scope (issue #663): the epic's own integration pull request targets main. The
97
+ # scope object comes from gate 1, so the epic checkpoint is not read a second time and
98
+ # the per-feature checkpoint is not consulted.
99
+ if ($null -ne $EpicScope -and $EpicScope.IsEpicScope) {
100
+ $epicBaseValue = Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create') -FlagName '--base'
101
+ if ($null -eq $epicBaseValue -or $epicBaseValue -cne 'main') {
102
+ return "EPIC_BASE_BRANCH_MISMATCH: this epic-scope gh pr create was evaluated against $($EpicScope.CheckpointPath); the integration pull request must pass --base main."
103
+ }
68
104
  return $null
69
105
  }
70
106
 
71
- $checkpointRaw = Get-PrAuthorCheckpointContent
107
+ $checkpointRaw = Get-PrAuthorCheckpointContent -CheckpointPath $CheckpointPath
72
108
  if ([string]::IsNullOrWhiteSpace($checkpointRaw)) {
73
109
  return $null
74
110
  }
@@ -96,7 +132,11 @@ function Test-EpicBaseBranchOverride {
96
132
  return "EPIC_BASE_BRANCH_MISMATCH: checkpoint has epic_mode == true but no ``epic_context.integration_branch`` is recorded; ``gh pr create`` cannot be verified against the required ``--base`` value."
97
133
  }
98
134
 
99
- if ($CommandText -cnotmatch [regex]::Escape("--base $integrationBranch")) {
135
+ # The --base value is taken from the matched segment's tokens, so a chained segment that
136
+ # happens to carry a --base token contributes nothing here. The comparison stays
137
+ # case-sensitive, as the previous -cnotmatch form was, because a branch name is.
138
+ $baseValue = Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create') -FlagName '--base'
139
+ if ($null -eq $baseValue -or $baseValue -cne $integrationBranch) {
100
140
  return "EPIC_BASE_BRANCH_MISMATCH: ``gh pr create`` must pass ``--base $integrationBranch`` (``epic_context.integration_branch``) under ``epic_mode``; the command does not carry a matching ``--base`` argument."
101
141
  }
102
142
 
@@ -46,7 +46,9 @@ param()
46
46
 
47
47
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
48
48
  $script:PrContextArtifactPath = 'artifacts/pr_context.summary.txt'
49
- $script:OrchestratorStateCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
49
+ # Null until the resolution step assigns the absolute path of the resolved worktree's
50
+ # checkpoint (issue #673); a relative default here was the process-directory binding.
51
+ $script:OrchestratorStateCheckpointPath = $null
50
52
 
51
53
  Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorState.psm1') -Force
52
54