@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
@@ -5,7 +5,7 @@
5
5
  .DESCRIPTION
6
6
  Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
7
7
  command runs. Regex-matches gh pr merge with a --merge flag against
8
- the envelope's tool_input.command and, when matched, allows the merge only when one of three
8
+ the envelope's tool_input.command and, when matched, allows the merge only when one of four
9
9
  checkpoint-only conditions holds:
10
10
 
11
11
  1. Child-feature path: artifacts/orchestration/orchestrator-state.json exists,
@@ -19,12 +19,19 @@
19
19
  entry whose merge_status == "ci_green". A parallel run always names an explicit PR
20
20
  number (each item merges from its own isolated worktree), so a bare command with no
21
21
  PR number cannot satisfy this branch.
22
-
23
- Otherwise the command is denied with reason EPIC_MERGE_GATE_BLOCKED. A missing or
24
- unreadable checkpoint in any branch fails closed (denies); standalone (non-epic,
25
- non-parallel) orchestration never sets epic_mode, populates epic_merge_pr, or writes a
26
- parallel checkpoint with route_id == "parallel", so it is structurally prevented from
27
- invoking gh pr merge --merge at all.
22
+ 4. Standalone path (issue #670), evaluated last: the command names an explicit PR
23
+ number and one of the three checkpoints above carries a
24
+ standalone_merge_authorizations record whose pr_number equals it, whose fields are
25
+ well formed, and whose session_id equals the live envelope's session_id. The
26
+ dot-sourced helpers file implements this branch.
27
+
28
+ Otherwise the command is denied with reason EPIC_MERGE_GATE_BLOCKED; a denial from
29
+ condition 4 also carries a standalone reason code after that token. A missing or
30
+ unreadable checkpoint in any branch fails closed (denies). A standalone (non-epic,
31
+ non-parallel) run never satisfies conditions 1-3, so its only route to a merge is an
32
+ explicit, PR-specific, session-bound record under condition 4; a bare command with no
33
+ PR number never satisfies condition 4, and a blanket flag is rejected rather than read.
34
+ Trigger scope: --squash is out of scope for this gate and is allowed on both runtimes.
28
35
 
29
36
  Design decision: this gate trusts the on-disk checkpoint rather than shelling out live
30
37
  to gh pr view for a real-time head-SHA check, matching the same non-adversarial,
@@ -35,11 +42,23 @@
35
42
  Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
36
43
  through injectable wrapper functions so tests can mock the boundary without writing
37
44
  temporary files.
45
+
46
+ Honest disclosure: the standalone-merge authorization record is a policy-level,
47
+ auditable declaration and is not a cryptographic or security control. authorized_by is
48
+ a declaration the hook does not verify. The record is not tamper-proof against a writer
49
+ inside the authorizing session. The session_id cross-check rejects stale or copied
50
+ records from other sessions; it does not stop same-session forgery.
38
51
  #>
39
52
  [CmdletBinding()]
40
53
  param()
41
54
 
42
55
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
56
+ # Shared command-line parser (issue #545), consumed by the scope filter in
57
+ # Invoke-EpicMergeGateDecision and by Get-EpicMergeGateCommandPrNumber.
58
+ . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
59
+ . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
60
+ # Standalone-merge authorization predicates and the two decision-envelope factories (issue #670).
61
+ . (Join-Path $PSScriptRoot 'enforce-epic-merge-gate-authorization.ps1')
43
62
 
44
63
  $script:ChildCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
45
64
  $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
@@ -128,6 +147,22 @@ function Get-EpicMergeGateCommandPrNumber {
128
147
  <#
129
148
  .SYNOPSIS
130
149
  Extract an explicit PR number argument from a gh pr merge command, or $null.
150
+ .DESCRIPTION
151
+ Both spellings are read from the segment that structurally invokes gh pr merge,
152
+ never from the whole command line. The positional form ("gh pr merge 410 --merge")
153
+ resolves through Get-CommandLineOperand and takes the first all-digit operand; the
154
+ flag-led forms ("gh pr merge --merge 410" and "gh pr merge --merge=410") resolve
155
+ through Get-CommandLineFlagValue. A bare "gh pr merge --merge" yields $null, which
156
+ the parallel branch treats as fail-closed.
157
+
158
+ The deleted unanchored branch scanned the WHOLE command text for the first run of
159
+ digits once "gh pr merge" appeared anywhere in it, and that failed in both
160
+ directions. Fail-closed: a leading "cd <path>" whose path carries a timestamp
161
+ component supplied a digit run that was not a pull request number, so an authorized
162
+ merge was blocked. False-allow: with authorized item 501 and unauthorized item 777,
163
+ "cd /repo/worktrees/501 && gh pr merge --merge 777" extracted 501, matched the
164
+ authorized item, and permitted the merge of PR 777. Taking the number from the
165
+ matched segment's own operand or flag value closes both directions.
131
166
  .PARAMETER CommandText
132
167
  The Bash command text under evaluation.
133
168
  .OUTPUTS
@@ -140,20 +175,17 @@ function Get-EpicMergeGateCommandPrNumber {
140
175
  [string] $CommandText
141
176
  )
142
177
 
143
- # Original form: the PR number appears immediately after "merge"
144
- # (e.g. "gh pr merge 410 --merge"). Preserved verbatim so epic-path outcomes
145
- # for the forms the epic path uses are unchanged.
146
- if ($CommandText -match '(?i)\bgh\s+pr\s+merge\s+(\d+)\b') {
147
- return [int]$Matches[1]
178
+ foreach ($operand in @(Get-CommandLineOperand -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge'))) {
179
+ if ($operand -match '^\d+$') {
180
+ return [int]$operand
181
+ }
148
182
  }
149
- # Broadened, additive form: the parallel command places the flag before the
150
- # number (e.g. "gh pr merge --merge 410"). Once "gh pr merge" is confirmed,
151
- # capture the first standalone run of digits that is not preceded by "-" or a
152
- # word character, so a flag token such as "--merge" is not treated as a number
153
- # and a bare "gh pr merge --merge" still yields $null.
154
- if ($CommandText -match '(?i)\bgh\s+pr\s+merge\b' -and $CommandText -match '(?<![-\w])(\d+)\b') {
155
- return [int]$Matches[1]
183
+
184
+ $flagValue = Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
185
+ if ($null -ne $flagValue -and $flagValue -match '^\d+$') {
186
+ return [int]$flagValue
156
187
  }
188
+
157
189
  return $null
158
190
  }
159
191
 
@@ -307,36 +339,6 @@ function Test-ParallelCheckpointAllowsMerge {
307
339
  return $false
308
340
  }
309
341
 
310
- function Get-EpicMergeGateAllowDecision {
311
- [CmdletBinding()]
312
- [OutputType([System.Collections.Specialized.OrderedDictionary])]
313
- param()
314
-
315
- return [ordered]@{
316
- hookSpecificOutput = [ordered]@{
317
- hookEventName = 'PreToolUse'
318
- permissionDecision = 'allow'
319
- }
320
- }
321
- }
322
-
323
- function Get-EpicMergeGateBlockDecision {
324
- [CmdletBinding()]
325
- [OutputType([System.Collections.Specialized.OrderedDictionary])]
326
- param(
327
- [Parameter(Mandatory)]
328
- [string] $Reason
329
- )
330
-
331
- return [ordered]@{
332
- hookSpecificOutput = [ordered]@{
333
- hookEventName = 'PreToolUse'
334
- permissionDecision = 'deny'
335
- permissionDecisionReason = $Reason
336
- }
337
- }
338
- }
339
-
340
342
  function Invoke-EpicMergeGateDecision {
341
343
  <#
342
344
  .SYNOPSIS
@@ -373,8 +375,26 @@ function Invoke-EpicMergeGateDecision {
373
375
  }
374
376
 
375
377
  # Only a gh pr merge invocation carrying --merge is in scope for this gate; every
376
- # other Bash command is unaffected.
377
- if ($commandText -notmatch '(?i)\bgh\s+pr\s+merge\b' -or $commandText -notmatch '--merge\b') {
378
+ # other Bash command is unaffected. Both legs are read structurally from the segment
379
+ # that invokes the command, so a quoted mention of the phrase and a --merge token
380
+ # belonging to some other segment no longer bring a command into scope.
381
+ #
382
+ # The flag leg needs a raw-scan fallback: a wrapper's quoted argument collapses into ONE
383
+ # token, so a flag inside it is never read as a token, and the token-only read took
384
+ # bash -c "gh pr merge --merge 688" out of scope. The fallback reads only the segments the
385
+ # scanner already scans raw, so a masked mention in a non-wrapper segment stays out.
386
+ $isMergeInvocation = Test-CommandLineInvocation -CommandText $commandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge')
387
+ $hasMergeFlag = Test-CommandLineFlag -CommandText $commandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
388
+ if (-not $hasMergeFlag) {
389
+ foreach ($segment in @(Read-CommandLineSegment -CommandText $commandText)) {
390
+ if ((Test-CommandLineSegmentRawScan -Segment $segment) -and
391
+ $segment.ScanText.IndexOf('--merge', [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
392
+ $hasMergeFlag = $true
393
+ break
394
+ }
395
+ }
396
+ }
397
+ if (-not $isMergeInvocation -or -not $hasMergeFlag) {
378
398
  return Get-EpicMergeGateAllowDecision
379
399
  }
380
400
 
@@ -395,6 +415,17 @@ function Invoke-EpicMergeGateDecision {
395
415
  return Get-EpicMergeGateAllowDecision
396
416
  }
397
417
 
418
+ # Branch 4, evaluated last and only for an explicit PR number: a PR-specific standalone
419
+ # authorization record bound to the live session. Its deny keeps the gate token first.
420
+ if ($null -ne $commandPrNumber) {
421
+ $envelopeSessionId = [string](Get-ClaudeHookEnvelopeValue -Envelope $payload.Envelope -Name 'session_id')
422
+ $standalone = Test-StandaloneCheckpointAllowsMerge -Checkpoints @($childCheckpoint, $epicCheckpoint, $parallelCheckpoint) -CommandPrNumber $commandPrNumber -EnvelopeSessionId $envelopeSessionId
423
+ if ($standalone.Allowed) {
424
+ return Get-EpicMergeGateAllowDecision
425
+ }
426
+ return Get-EpicMergeGateBlockDecision -Reason ('EPIC_MERGE_GATE_BLOCKED: ' + $standalone.ReasonCode + ': ' + $standalone.Message)
427
+ }
428
+
398
429
  return Get-EpicMergeGateBlockDecision -Reason 'EPIC_MERGE_GATE_BLOCKED: gh pr merge --merge requires either a per-feature checkpoint with epic_mode == true and step9_status == "passed", an epic checkpoint with epic_merge_pr.ci_gate.conclusion == "success" and a matching pr_number, or a parallel-orchestrator checkpoint with route_id == "parallel" whose target item (matched by pr_number) has merge_status == "ci_green". No checkpoint satisfied this gate.'
399
430
  }
400
431
 
@@ -60,9 +60,20 @@ param()
60
60
 
61
61
 
62
62
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
63
+ # Sanctioned-removal manifest reader (issue #635), consumed by the manifest branch in
64
+ # Invoke-EpicWorktreeRemovalGateDecision.
65
+ Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeManifest.psm1') -Force
66
+ # Shared command-line parser (issue #545), consumed by the scope filter in
67
+ # Invoke-EpicWorktreeRemovalGateDecision and by Get-EpicWorktreeRemovalCommandPath.
68
+ . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
69
+ . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
63
70
  $script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
64
71
  $script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
65
72
  $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
73
+ # Sanctioned-removal manifest location. Recorded here beside the two checkpoint paths
74
+ # so every hook-read document this gate consults is named in one place; the module
75
+ # owns the read itself.
76
+ $script:CleanupWorktreeManifestPath = 'artifacts/orchestration/cleanup-worktrees-manifest.json'
66
77
 
67
78
  function Get-EpicWorktreeGateCheckpointContent {
68
79
  <#
@@ -132,6 +143,19 @@ function Get-EpicWorktreeRemovalCommandPath {
132
143
  <#
133
144
  .SYNOPSIS
134
145
  Extract the target worktree path argument from a git worktree remove command.
146
+ .DESCRIPTION
147
+ The operand comes from the segment that structurally invokes git worktree remove,
148
+ so a 'cd <path> &&' segment chained before the removal contributes nothing and a
149
+ quoted mention of the phrase resolves to no operand at all. Quotes around the path
150
+ are already stripped by the tokenizer.
151
+
152
+ '--force' is a zero-argument flag, so it never contributes an operand and may be
153
+ written on either side of the target path. Its presence is read structurally through
154
+ Test-CommandLineFlag rather than by searching the raw text, so a '--force' spelling
155
+ that appears inside an unrelated quoted argument cannot change how the operand list
156
+ is read. When no operand resolves, a present '--force' is reported in its place, so
157
+ the checkpoint lookup fails closed on a value that matches no recorded worktree_path
158
+ - the same value the previous raw-text pattern returned for that input.
135
159
  .PARAMETER CommandText
136
160
  The Bash command text under evaluation.
137
161
  .OUTPUTS
@@ -144,8 +168,14 @@ function Get-EpicWorktreeRemovalCommandPath {
144
168
  [string] $CommandText
145
169
  )
146
170
 
147
- if ($CommandText -match '(?i)\bgit\s+worktree\s+remove\s+(?<path>\S+)') {
148
- return $Matches['path'].Trim('"''')
171
+ $hasForce = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove') -FlagName '--force'
172
+ $operands = @(Get-CommandLineOperand -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))
173
+
174
+ if ($operands.Count -gt 0) {
175
+ return $operands[0]
176
+ }
177
+ if ($hasForce) {
178
+ return '--force'
149
179
  }
150
180
  return $null
151
181
  }
@@ -344,7 +374,10 @@ function Invoke-EpicWorktreeRemovalGateDecision {
344
374
  return Get-EpicWorktreeGateAllowDecision
345
375
  }
346
376
 
347
- if ($commandText -notmatch '(?i)\bgit\s+worktree\s+remove\b') {
377
+ # Scope filter. The test is structural, so a relocating spelling such as
378
+ # 'git -C <dir> worktree remove <path>' is now in scope and quoted prose that merely
379
+ # mentions the phrase is not (issue #545).
380
+ if (-not (Test-CommandLineInvocation -CommandText $commandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))) {
348
381
  return Get-EpicWorktreeGateAllowDecision
349
382
  }
350
383
 
@@ -362,6 +395,22 @@ function Invoke-EpicWorktreeRemovalGateDecision {
362
395
  return Get-EpicWorktreeGateAllowDecision
363
396
  }
364
397
 
398
+ # Sanctioned-removal manifest branch (issue #635). It runs last, so an
399
+ # epic-checkpoint-authorized or parallel-checkpoint-authorized removal still allows
400
+ # at the same decision point it does today and no transcript attribution changes.
401
+ #
402
+ # The two coverage tests are PRESENCE tests, deliberately not authorization tests.
403
+ # A target either checkpoint records at all is excluded from this branch regardless
404
+ # of that record's merge_status, so a removal no checkpoint authorizes still reaches
405
+ # the unchanged deny below. That exclusion is the only path by which the manifest
406
+ # could widen what the gates protect, which is why it is required rather than
407
+ # advisory, and why it is evaluated before the manifest predicate.
408
+ if (-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $checkpoint -RecordArrayName 'features' -WorktreePath $worktreePath) -and
409
+ -not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $parallelCheckpoint -RecordArrayName 'items' -WorktreePath $worktreePath) -and
410
+ (Test-CleanupWorktreeManifestAuthorizesRemoval -WorktreePath $worktreePath)) {
411
+ return Get-EpicWorktreeGateAllowDecision
412
+ }
413
+
365
414
  return Get-EpicWorktreeGateBlockDecision -Reason "EPIC_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires either an epic checkpoint features[] record with merge_status in {merged, worktree_removed}, or a parallel-orchestrator checkpoint with route_id == ""parallel"" whose matching items[] record (matched by worktree_path) has merge_status in {merged, worktree_removed}. No checkpoint authorized this removal."
366
415
  }
367
416
 
@@ -5,8 +5,15 @@
5
5
 
6
6
  .DESCRIPTION
7
7
  Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
8
- tool input JSON from the the envelope's nested tool_input environment variable and the
9
- orchestrator checkpoint from artifacts/orchestration/orchestrator-state.json.
8
+ tool input JSON from the envelope's nested tool_input, resolves which item the
9
+ delegation is about, and reads that item's orchestrator checkpoint.
10
+
11
+ The item is identified by portable identity only (issue #673): the canonical
12
+ issue-number line in the delegation prompt, and a branch label. A feature-folder
13
+ path never selects the worktree, because a merged folder exists in every checkout
14
+ branched from main. A delegation the gate cannot identify is denied with a named
15
+ reason code rather than checked against whichever checkpoint occupies the calling
16
+ process's directory, which is how a sibling item's state used to produce an allow.
10
17
 
11
18
  The hook enforces presence only: it cannot read the delegate's chosen
12
19
  `model` (no `model` field is exposed in the tool input), so it verifies that
@@ -24,7 +31,14 @@
24
31
  empty or absent tool input, and malformed tool-input JSON.
25
32
 
26
33
  The checkpoint read goes through Get-ModelRoutingCheckpoint so tests can
27
- inject a synthetic checkpoint without touching disk.
34
+ inject a synthetic checkpoint without touching disk. Its path is mandatory and
35
+ carries no default: the caller supplies the absolute path of the resolved
36
+ worktree's checkpoint, so the gate cannot fall back to a relative location.
37
+
38
+ The delegation-identity contract this gate depends on is stated in
39
+ .claude/skills/orchestrate/SKILL.md under `## Issue Number Consistency`: every
40
+ delegation prompt to a receipt-gated subagent type carries the canonical issue
41
+ number line and a `branch:` label naming the item's branch.
28
42
 
29
43
  .NOTES
30
44
  Compatible with PowerShell 7+. Read-only presence-gating deterrent.
@@ -34,6 +48,11 @@ param()
34
48
 
35
49
 
36
50
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
51
+ # Portable-identity resolution (issue #673). Unguarded and fail-closed on purpose: a gate
52
+ # that cannot load its resolver must not degrade into the cwd-relative read it replaces.
53
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force -ErrorAction Stop
54
+ # Epic scope (issue #663): a delegation for the epic integration branch is gated against the epic checkpoint.
55
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
37
56
  function Get-ModelRoutingCheckpoint {
38
57
  <#
39
58
  .SYNOPSIS
@@ -43,7 +62,8 @@ function Get-ModelRoutingCheckpoint {
43
62
  [CmdletBinding()]
44
63
  [OutputType([object])]
45
64
  param(
46
- [string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
65
+ [Parameter(Mandatory)]
66
+ [string] $CheckpointPath
47
67
  )
48
68
 
49
69
  if (-not (Test-Path -LiteralPath $CheckpointPath -PathType Leaf)) {
@@ -117,6 +137,66 @@ function Test-ModelRoutingReceiptPresent {
117
137
  return $false
118
138
  }
119
139
 
140
+ function Resolve-ModelRoutingWorktreeTarget {
141
+ <#
142
+ .SYNOPSIS
143
+ Resolve the delegation's target worktree. Tests mock this function (resolution seam).
144
+ .DESCRIPTION
145
+ A seam rather than a direct call because the resolver reads real git state: the
146
+ worktree list, each registration's branch, and the filesystem. A test that drove it
147
+ directly would depend on whichever worktrees exist on the machine running it.
148
+ .PARAMETER PromptText
149
+ The delegation prompt, which is the only field scanned for identity.
150
+ .OUTPUTS
151
+ The target result object from Resolve-WorktreeItemTarget.
152
+ #>
153
+ [CmdletBinding()]
154
+ [OutputType([pscustomobject])]
155
+ param(
156
+ [Parameter(Mandatory)]
157
+ [AllowEmptyString()]
158
+ [string] $PromptText
159
+ )
160
+
161
+ return (Resolve-WorktreeItemTarget -Text $PromptText -SessionRoot (Get-Location).Path)
162
+ }
163
+
164
+ function Get-ModelRoutingTargetCheckpointResolution {
165
+ <#
166
+ .SYNOPSIS
167
+ Resolve the checkpoint path this delegation must be checked against.
168
+ .DESCRIPTION
169
+ Maps the four resolution states onto a checkpoint path or a deny reason. The two
170
+ resolved states share one branch because the path is composed the same way for
171
+ both: identity selects the worktree, and the checkpoint is read beneath it. The two
172
+ unresolved states deny, carrying the code the worktree-resolution accessors supply;
173
+ neither literal appears in this file.
174
+ .PARAMETER PromptText
175
+ The delegation prompt to resolve identity from.
176
+ .OUTPUTS
177
+ System.Collections.Specialized.OrderedDictionary with CheckpointPath and Reason.
178
+ #>
179
+ [CmdletBinding()]
180
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
181
+ param(
182
+ [Parameter(Mandatory)]
183
+ [AllowEmptyString()]
184
+ [string] $PromptText
185
+ )
186
+
187
+ $target = Resolve-ModelRoutingWorktreeTarget -PromptText $PromptText
188
+
189
+ switch ($target.Status) {
190
+ { $_ -in @('SessionRoot', 'OtherWorktree') } {
191
+ return [ordered]@{ CheckpointPath = (Get-WorktreeItemCheckpointPath -WorktreeRoot $target.WorktreeRoot); Reason = $null }
192
+ }
193
+ default {
194
+ $reason = "$($target.ReasonCode): $($target.Detail) The model-routing gate will not check this delegation against a checkpoint that may belong to a different item. Put the line 'Canonical issue number for this feature is <N>.' and a 'branch: <item branch>' label in the delegation prompt so the gate can identify the item."
195
+ return [ordered]@{ CheckpointPath = $null; Reason = $reason }
196
+ }
197
+ }
198
+ }
199
+
120
200
  function Invoke-ModelRoutingReceiptDecision {
121
201
  <#
122
202
  .SYNOPSIS
@@ -154,7 +234,42 @@ function Invoke-ModelRoutingReceiptDecision {
154
234
  return $allow
155
235
  }
156
236
 
157
- $checkpoint = Get-ModelRoutingCheckpoint
237
+ # Identity resolution sits after the scope filter and before the checkpoint read, so a
238
+ # delegation outside the gated set is still allowed however unresolvable its target is,
239
+ # and a gated delegation the gate cannot identify is refused rather than answered from
240
+ # unrelated state. An absent prompt is passed as an empty string, which resolves to no
241
+ # identity and therefore to the same deny.
242
+ $prompt = [string](Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'prompt')
243
+
244
+ # Decision: a prompt whose branch label equals the epic checkpoint's integration_branch is
245
+ # epic scope (issue #663), and the receipt is looked up in the epic checkpoint without
246
+ # running per-feature identity resolution. Any other prompt falls through unchanged.
247
+ $epicScope = Resolve-EpicScopeCheckpoint -Text $prompt -SessionRoot (Get-Location).Path
248
+ if ($epicScope.IsEpicScope) {
249
+ if (Test-ModelRoutingReceiptPresent -Checkpoint $epicScope.Checkpoint -Subagent $subagent) {
250
+ return $allow
251
+ }
252
+ return [ordered]@{
253
+ hookSpecificOutput = [ordered]@{
254
+ hookEventName = 'PreToolUse'
255
+ permissionDecision = 'deny'
256
+ permissionDecisionReason = "MODEL_ROUTING_RECEIPT_BLOCKED: cannot delegate to '$subagent' in epic scope before a model_routing_receipts entry for it is recorded in $($epicScope.CheckpointPath); the failed readiness predicate is 'model_routing_receipts'."
257
+ }
258
+ }
259
+ }
260
+
261
+ $resolution = Get-ModelRoutingTargetCheckpointResolution -PromptText $prompt
262
+ if ($resolution.Reason) {
263
+ return [ordered]@{
264
+ hookSpecificOutput = [ordered]@{
265
+ hookEventName = 'PreToolUse'
266
+ permissionDecision = 'deny'
267
+ permissionDecisionReason = $resolution.Reason
268
+ }
269
+ }
270
+ }
271
+
272
+ $checkpoint = Get-ModelRoutingCheckpoint -CheckpointPath $resolution.CheckpointPath
158
273
  if (Test-ModelRoutingReceiptPresent -Checkpoint $checkpoint -Subagent $subagent) {
159
274
  return $allow
160
275
  }
@@ -0,0 +1,127 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Epic-scope command and path legs of the preimplementation gate (issue #663).
4
+
5
+ .DESCRIPTION
6
+ Dot-sourced by enforce-orchestration-preimplementation-gate.ps1. Holds two groups of
7
+ functions:
8
+
9
+ - The two per-mode read seams of issue #554 (Get-EpicCheckpointContent and
10
+ Get-ParallelCheckpointContent), relocated verbatim from the gate file by issue #663
11
+ so the gate stays inside the 500-line cap. Their names and behaviour are unchanged,
12
+ so tests that mock or shadow them by name are unaffected.
13
+ - The epic-scope decision for the command and path legs. A staging command or a
14
+ Write/Edit call is epic scope when the effective worktree's HEAD (the -C selector
15
+ worktree when present, 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 epic command-leg readiness predicate, and under 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+. Depends on functions the gate defines or dot-sources before any call:
24
+ Split-OrchestrationCommandLine and ConvertTo-OrchestrationCommandToken (helpers file),
25
+ Get-OrchestrationDelegationCheckpointPath (modes file), and the gate's allow and block
26
+ decision constructors. Mirrored byte-identically under
27
+ extensions/drm-copilot/resources/claude-customizations/.
28
+ #>
29
+
30
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
31
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeReadiness.psm1') -Force -ErrorAction Stop
32
+
33
+ # The two per-mode read seams (issue #554). Each takes its path from the fixed mode
34
+ # table and never from a delegation's own text; an absent file returns an empty
35
+ # string, which the readiness predicate then treats as a deny.
36
+ function Get-EpicCheckpointContent {
37
+ [CmdletBinding()]
38
+ [OutputType([string])]
39
+ param()
40
+
41
+ $path = Get-OrchestrationDelegationCheckpointPath -Mode 'epic'
42
+ if (-not (Test-Path -LiteralPath $path)) {
43
+ return ''
44
+ }
45
+ return Get-Content -Raw -LiteralPath $path
46
+ }
47
+
48
+ function Get-ParallelCheckpointContent {
49
+ [CmdletBinding()]
50
+ [OutputType([string])]
51
+ param()
52
+
53
+ $path = Get-OrchestrationDelegationCheckpointPath -Mode 'parallel'
54
+ if (-not (Test-Path -LiteralPath $path)) {
55
+ return ''
56
+ }
57
+ return Get-Content -Raw -LiteralPath $path
58
+ }
59
+
60
+ function Get-OrchestrationEpicScopeSelector {
61
+ <#
62
+ .SYNOPSIS
63
+ Returns the value of a leading `git -C <value>` selector, or $null.
64
+ .DESCRIPTION
65
+ Pure. Reads only the first segment of the command line, split and tokenized with
66
+ the helpers-file parser so quoting is handled exactly as the staging exemption
67
+ handles it. An unbalanced command line has no decidable selector and returns $null.
68
+ .PARAMETER Command
69
+ The full command line.
70
+ .OUTPUTS
71
+ System.String
72
+ #>
73
+ [CmdletBinding()]
74
+ [OutputType([string])]
75
+ param([Parameter(Mandatory)][AllowEmptyString()][string] $Command)
76
+
77
+ $split = Split-OrchestrationCommandLine -CommandText $Command
78
+ if (-not $split.Balanced -or @($split.Segments).Count -eq 0) {
79
+ return $null
80
+ }
81
+ $token = @(ConvertTo-OrchestrationCommandToken -Segment ([string]@($split.Segments)[0]).Trim())
82
+ if ($token.Count -ge 3 -and $token[0] -ceq 'git' -and $token[1] -ceq '-C' -and $token[2]) {
83
+ return $token[2]
84
+ }
85
+ return $null
86
+ }
87
+
88
+ function Get-OrchestrationEpicScopeDecision {
89
+ <#
90
+ .SYNOPSIS
91
+ Returns the epic-scope decision for an implementation-classified command or path, or $null.
92
+ .DESCRIPTION
93
+ Resolves epic scope with worktree-HEAD matching and the command's -C selector. When
94
+ the call is not epic scope, returns $null so the caller's single-feature path runs
95
+ unchanged. In epic scope, returns an allow decision when the epic command-leg
96
+ readiness predicate passes, and otherwise a deny naming the epic checkpoint and the
97
+ failed conjunct.
98
+ .PARAMETER Command
99
+ The Bash command line; empty for a path leg.
100
+ .PARAMETER FilePath
101
+ The Write/Edit file path; empty for a command leg.
102
+ .OUTPUTS
103
+ System.Collections.Specialized.OrderedDictionary, or $null outside epic scope.
104
+ #>
105
+ [CmdletBinding()]
106
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
107
+ param(
108
+ [AllowNull()][AllowEmptyString()][string] $Command = '',
109
+ [AllowNull()][AllowEmptyString()][string] $FilePath = ''
110
+ )
111
+
112
+ # A call carrying neither a command nor a path has no leg to decide.
113
+ if (-not $Command -and -not $FilePath) {
114
+ return $null
115
+ }
116
+ $selector = if ($Command) { Get-OrchestrationEpicScopeSelector -Command $Command } else { $null }
117
+ $scope = Resolve-EpicScopeCheckpoint -Text ([string]$Command) -SessionRoot (Get-Location).Path -WorktreeSelector $selector -MatchWorktreeHead
118
+ if (-not $scope.IsEpicScope) {
119
+ return $null
120
+ }
121
+
122
+ $failure = Get-EpicCommandLegReadinessFailure -Checkpoint $scope.Checkpoint -MergeInProgress $scope.MergeInProgress
123
+ if (-not $failure) {
124
+ return Get-OrchestrationPreimplementationGateAllowDecision
125
+ }
126
+ 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.")
127
+ }