@danmoisan/drm-copilot-mcp 1.1.22 → 1.1.23

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 (85) hide show
  1. package/out/mcp-server.js +2568 -1261
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agent-memory/orchestrator/MEMORY.md +1 -1
  4. package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_test_files_count_against_500_cap.md +4 -4
  5. package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -1
  6. package/resources/claude-customizations/.claude/agents/feature-review.md +8 -0
  7. package/resources/claude-customizations/.claude/agents/orchestrator.md +1 -1
  8. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +46 -6
  9. package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -1
  10. package/resources/claude-customizations/.claude/agents/pr-author.md +32 -7
  11. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -1
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-green-ci.ps1 +325 -0
  13. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +22 -6
  14. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +23 -10
  15. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +29 -21
  16. package/resources/claude-customizations/.claude/hooks/enforce-file-size-limit.ps1 +244 -0
  17. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +48 -0
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +22 -11
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +8 -2
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +15 -10
  21. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.current-branch.ps1 +149 -0
  22. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-feature-folder.ps1 +146 -0
  23. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +25 -3
  24. package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +33 -6
  25. package/resources/claude-customizations/.claude/lib/file-size-policy/FileSizePolicy.psm1 +434 -0
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1 +166 -0
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +8 -3
  28. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunRecordPath.psm1 +185 -0
  29. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +11 -21
  30. package/resources/claude-customizations/.claude/rules/general-code-change.md +4 -1
  31. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -5
  32. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +2 -2
  33. package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
  34. package/resources/claude-customizations/.claude/rules/shell.md +1 -1
  35. package/resources/claude-customizations/.claude/settings.json +4 -0
  36. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +2 -2
  37. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +1 -1
  38. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -0
  39. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +11 -4
  40. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +4 -1
  41. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +90 -14
  42. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +3 -1
  43. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +39 -9
  44. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +1 -1
  45. package/resources/claude-customizations/pack-manifests/core.json +7 -0
  46. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +1 -1
  47. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  48. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +8 -0
  49. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +4 -1
  50. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -1
  51. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
  52. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
  53. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
  54. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
  55. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
  56. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
  57. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +8 -0
  58. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
  59. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
  60. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
  61. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
  62. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
  63. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
  64. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
  65. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
  66. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
  67. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
  68. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
  69. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
  70. package/resources/codex-and-agents-customizations/.codex/config.toml +7 -1
  71. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-file-size-limit.ps1 +282 -0
  72. package/resources/codex-and-agents-customizations/.codex/hooks/file-size-policy.ps1 +421 -0
  73. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
  74. package/resources/codex-and-agents-customizations/AGENTS.md +4 -1
  75. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  76. package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
  77. package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +1 -1
  78. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +1 -1
  79. package/resources/customizations/.github/instructions/general-code-change.instructions.md +5 -2
  80. package/resources/customizations/.github/instructions/powershell-code-change.instructions.md +1 -1
  81. package/resources/customizations/.github/prompts/remediate-comments.prompt.md +1 -1
  82. package/resources/customizations/.github/skills/execution-environment/SKILL.md +1 -1
  83. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +8 -0
  84. package/resources/templates/policy_audit/code-review.yyyy-MM-ddTHH-mm.md +4 -4
  85. package/resources/templates/policy_audit/policy-audit.yyyy-MM-ddTHH-mm.md +4 -4
@@ -20,6 +20,11 @@
20
20
  entry whose merge_status == "ci_green". A parallel run always names an explicit PR
21
21
  number (each item merges from its own isolated worktree), so a bare command with no
22
22
  PR number cannot satisfy this branch.
23
+ 3b. Green-CI standalone path (issue #997): the command names an explicit PR number, no
24
+ epic or parallel checkpoint records that number or the item worktree, the per-feature
25
+ checkpoint records step9_status == "passed", ci_gate.conclusion == "success", and a
26
+ pr_gate.pr_number equal to it, and the live PR head equals ci_gate.head_sha. The
27
+ dot-sourced enforce-epic-merge-gate-green-ci.ps1 implements this branch.
23
28
  4. Standalone path (issue #670), evaluated last: the command names an explicit PR
24
29
  number and one of the three checkpoints above carries a
25
30
  standalone_merge_authorizations record whose pr_number equals it, whose fields are
@@ -29,15 +34,20 @@
29
34
  Otherwise the command is denied with reason EPIC_MERGE_GATE_BLOCKED; a denial from
30
35
  condition 4 also carries a standalone reason code after that token. A missing or
31
36
  unreadable checkpoint in any branch fails closed (denies). A standalone (non-epic,
32
- non-parallel) run never satisfies conditions 1-3, so its only route to a merge is an
33
- explicit, PR-specific, session-bound record under condition 4; a bare command with no
34
- PR number never satisfies condition 4, and a blanket flag is rejected rather than read.
37
+ non-parallel) run never satisfies conditions 1-3; it merges through the green-CI branch
38
+ 3b or through an explicit, PR-specific, session-bound record under condition 4. A bare
39
+ command with no PR number satisfies neither, and a blanket flag is rejected rather than
40
+ read. A 3b decline after its live-head lookup appends a GREEN_CI_* code to the deny.
35
41
  Trigger scope (issue #952): every merge method (--merge/-m, --squash/-s, --rebase/-r), --auto,
36
42
  --admin, and a bare gh pr merge are gated alike, because under a merge queue each spelling
37
43
  enqueues the pull request; only a pure --disable-auto is out of scope. The predicate and the
38
44
  PR selector reader live in enforce-epic-merge-gate-command.ps1.
39
45
 
40
- Design decision: branches 1-4 trust the on-disk checkpoint and never call gh. Issue #927
46
+ Design decision: branches 1-4 trust the on-disk checkpoint and never call gh. The green-CI
47
+ branch makes at most one bounded gh pr view call, only after its pure stages A to C pass.
48
+ Its parallel exclusion is best-effort: the Parallel mode: true marker is in the delegation
49
+ prompt, not in a Bash payload, so a parallel child that no checkpoint records yet is not
50
+ excluded mechanically (residual window; see enforce-epic-merge-gate-green-ci.ps1). Issue #927
41
51
  adds a docs-only branch after branch 4 fails, only for an explicit PR number whose item,
42
52
  epic, and parallel targets are all NoTarget (no checkpoint records the PR): it calls
43
53
  Get-DocsOnlyMergeExemption (hook-docs-only-diff.ps1), which runs gh pr view and git in the
@@ -86,6 +96,8 @@ try { . (Join-Path $PSScriptRoot 'enforce-epic-merge-gate-resolution.ps1') } cat
86
96
  try { . (Join-Path $PSScriptRoot 'enforce-epic-merge-gate-command.ps1') } catch { Add-HookDependencyFailure -Name 'enforce-epic-merge-gate-command.ps1' -ErrorRecord $_ }
87
97
  # Shared docs-only diff classification and the merge-side docs-only exemption (issue #927).
88
98
  try { . (Join-Path $PSScriptRoot 'hook-docs-only-diff.ps1') } catch { Add-HookDependencyFailure -Name 'hook-docs-only-diff.ps1' -ErrorRecord $_ }
99
+ # Green-CI standalone merge branch: pure checks, the live-head seam, and the stage orchestrator (issue #997).
100
+ try { . (Join-Path $PSScriptRoot 'enforce-epic-merge-gate-green-ci.ps1') } catch { Add-HookDependencyFailure -Name 'enforce-epic-merge-gate-green-ci.ps1' -ErrorRecord $_ }
89
101
 
90
102
  function ConvertFrom-EpicMergeGateJson {
91
103
  <#
@@ -376,6 +388,10 @@ function Invoke-EpicMergeGateDecision {
376
388
  return Get-EpicMergeGateAllowDecision
377
389
  }
378
390
 
391
+ # Branch 3b (issue #997): a standalone run whose checkpoint records a green S9 gate merges its own PR.
392
+ $greenCi = Get-EpicMergeGateGreenCiDecision -CommandText $commandText -CommandPrNumber $commandPrNumber -ChildCheckpoint $childCheckpoint -ItemTarget $itemTarget -EpicTarget $epicTarget -ParallelTarget $parallelTarget -SessionRoot $sessionRoot
393
+ if ($greenCi.Allowed) { return Get-EpicMergeGateAllowDecision }
394
+
379
395
  # Branch 4, evaluated last and only for an explicit PR number: a PR-specific standalone
380
396
  # authorization record bound to the live session. Its deny keeps the gate token first.
381
397
  if ($null -ne $commandPrNumber) {
@@ -397,10 +413,10 @@ function Invoke-EpicMergeGateDecision {
397
413
  # three details precede the standalone reason (issue #850).
398
414
  $unresolved = Get-EpicMergeGateUnresolvedReason -ItemTarget $itemTarget -EpicTarget $epicTarget -ParallelTarget $parallelTarget
399
415
  $prefix = if ($unresolved) { $unresolved + '; ' } else { '' }
400
- return Get-EpicMergeGateBlockDecision -Reason ('EPIC_MERGE_GATE_BLOCKED: ' + $prefix + $standalone.ReasonCode + ': ' + $standalone.Message + $docsOnlySuffix)
416
+ return Get-EpicMergeGateBlockDecision -Reason ('EPIC_MERGE_GATE_BLOCKED: ' + $prefix + $standalone.ReasonCode + ': ' + $standalone.Message + $docsOnlySuffix + $greenCi.Suffix)
401
417
  }
402
418
 
403
- return Get-EpicMergeGateBlockDecision -Reason 'EPIC_MERGE_GATE_BLOCKED: a pull-request merge or merge-queue enqueue (gh pr merge with any method, --auto, or --admin, or a gh api merge or enqueue mutation) 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.'
419
+ return Get-EpicMergeGateBlockDecision -Reason 'EPIC_MERGE_GATE_BLOCKED: a pull-request merge or merge-queue enqueue (gh pr merge with any method, --auto, or --admin, or a gh api merge or enqueue mutation) 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, a parallel-orchestrator checkpoint with route_id == "parallel" whose target item (matched by pr_number) has merge_status == "ci_green", or a standalone per-feature checkpoint (epic_mode absent or false, no parallel marker) with step9_status == "passed", ci_gate.conclusion == "success", pr_gate.pr_number equal to the explicit PR number, and ci_gate.head_sha equal to the live pull-request head. No checkpoint satisfied this gate.'
404
420
  }
405
421
 
406
422
  function Invoke-EpicMergeGateEntryPoint {
@@ -21,6 +21,9 @@
21
21
  for the removal target, and Get-EpicWorktreeGateDenyDiagnostics, which builds the
22
22
  diagnostics clause of the final deny from two read results without reading any
23
23
  file (issue #851).
24
+ - The import guard for WorktreeRunRecordPath.psm1, whose Get-WorktreeRunRecordBaseRoot
25
+ pairs the main checkout root with a read's own root so a relative recorded
26
+ worktree_path is compared through the shared matcher (issue #995).
24
27
 
25
28
  .NOTES
26
29
  PowerShell 7+. Depends on Get-EpicWorktreeGateBlockDecision,
@@ -29,6 +32,8 @@
29
32
  extensions/drm-copilot/resources/claude-customizations/.
30
33
  #>
31
34
 
35
+ # Shared recorded-path matcher and main-checkout-root seam (issue #995).
36
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunRecordPath.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeRunRecordPath.psm1' -ErrorRecord $_ }
32
37
  try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeRunResolution.psm1' -ErrorRecord $_ }
33
38
 
34
39
  function Get-EpicWorktreeGateCheckpointContent {
@@ -132,13 +137,15 @@ function Find-EpicWorktreeGateParallelItemRecord {
132
137
  .SYNOPSIS
133
138
  Locate the parallel items[] record whose worktree_path matches the target path.
134
139
  .DESCRIPTION
135
- Pure. Normalizes both paths exactly as Test-ParallelCheckpointAllowsWorktreeRemoval
136
- does (backslash to slash, trailing slash trimmed, PowerShell -eq) and returns the
137
- first matching entry (issue #851).
140
+ Pure. Compares through Test-WorktreeRunRecordedPathMatchesTarget, exactly as
141
+ Test-ParallelCheckpointAllowsWorktreeRemoval does, and returns the first matching
142
+ entry (issues #851, #995).
138
143
  .PARAMETER Checkpoint
139
144
  Parsed parallel-orchestrator checkpoint, or $null when absent/unreadable.
140
145
  .PARAMETER WorktreePath
141
146
  The removal target the command names.
147
+ .PARAMETER BaseRoot
148
+ Optional. The roots a relative recorded worktree_path is joined to.
142
149
  .OUTPUTS
143
150
  System.Object or $null
144
151
  #>
@@ -148,7 +155,9 @@ function Find-EpicWorktreeGateParallelItemRecord {
148
155
  $Checkpoint,
149
156
 
150
157
  [AllowNull()]
151
- [string] $WorktreePath
158
+ [string] $WorktreePath,
159
+
160
+ [AllowNull()][AllowEmptyCollection()][string[]] $BaseRoot = @()
152
161
  )
153
162
 
154
163
  if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
@@ -159,7 +168,7 @@ function Find-EpicWorktreeGateParallelItemRecord {
159
168
  return $null
160
169
  }
161
170
 
162
- $normalizedTarget = ($WorktreePath -replace '\\', '/').TrimEnd('/')
171
+ # Scan the items for the first record the shared matcher accepts for the target.
163
172
  foreach ($item in @($Checkpoint.items)) {
164
173
  if ($null -eq $item) {
165
174
  continue
@@ -168,8 +177,7 @@ function Find-EpicWorktreeGateParallelItemRecord {
168
177
  if ($itemProps -notcontains 'worktree_path') {
169
178
  continue
170
179
  }
171
- $normalizedItemPath = (([string]$item.worktree_path) -replace '\\', '/').TrimEnd('/')
172
- if ($normalizedItemPath -eq $normalizedTarget) {
180
+ if (Test-WorktreeRunRecordedPathMatchesTarget -RecordedPath ([string]$item.worktree_path) -TargetPath $WorktreePath -BaseRoot $BaseRoot) {
173
181
  return $item
174
182
  }
175
183
  }
@@ -191,6 +199,9 @@ function Get-EpicWorktreeGateDenyDiagnostics {
191
199
  The parallel read result (Target, Checkpoint, Path).
192
200
  .PARAMETER WorktreePath
193
201
  The removal target the command names.
202
+ .PARAMETER MainRoot
203
+ Optional. The main checkout root the gate derived; with each read's own root it
204
+ forms the base roots a relative recorded worktree_path is joined to (issue #995).
194
205
  .OUTPUTS
195
206
  System.String
196
207
  #>
@@ -200,7 +211,8 @@ function Get-EpicWorktreeGateDenyDiagnostics {
200
211
  param(
201
212
  [Parameter(Mandatory)] $EpicRead,
202
213
  [Parameter(Mandatory)] $ParallelRead,
203
- [AllowNull()][AllowEmptyString()][string] $WorktreePath
214
+ [AllowNull()][AllowEmptyString()][string] $WorktreePath,
215
+ [AllowNull()][AllowEmptyString()][string] $MainRoot
204
216
  )
205
217
 
206
218
  $parts = foreach ($kind in @('epic', 'parallel')) {
@@ -213,11 +225,12 @@ function Get-EpicWorktreeGateDenyDiagnostics {
213
225
  $recordText = 'checkpoint absent or unparseable'
214
226
  }
215
227
  else {
228
+ $baseRoot = Get-WorktreeRunRecordBaseRoot -MainRoot $MainRoot -Read $read
216
229
  $record = if ($kind -eq 'epic') {
217
- Find-EpicWorktreeFeatureRecord -Checkpoint $read.Checkpoint -WorktreePath $WorktreePath
230
+ Find-EpicWorktreeFeatureRecord -Checkpoint $read.Checkpoint -WorktreePath $WorktreePath -BaseRoot $baseRoot
218
231
  }
219
232
  else {
220
- Find-EpicWorktreeGateParallelItemRecord -Checkpoint $read.Checkpoint -WorktreePath $WorktreePath
233
+ Find-EpicWorktreeGateParallelItemRecord -Checkpoint $read.Checkpoint -WorktreePath $WorktreePath -BaseRoot $baseRoot
221
234
  }
222
235
  $arrayName = if ($kind -eq 'epic') { 'features' } else { 'items' }
223
236
  $recordText = if ($null -eq $record) {
@@ -26,8 +26,9 @@
26
26
  before either checkpoint is read.
27
27
 
28
28
  Both branches key on the worktree PATH because the command names a path, and both
29
- normalize separators and trim a trailing slash on each side of the comparison, so
30
- Windows- and POSIX-style paths compare equal. Path is also why the two branches are
29
+ compare through Test-WorktreeRunRecordedPathMatchesTarget (issue #995), so Windows-
30
+ and POSIX-style paths compare equal and a relative record is joined to the main
31
+ checkout root and the checkpoint's own root. Path is also why the two branches are
31
32
  mutually exclusive in practice: an epic run and a parallel run allocate worktrees
32
33
  under distinct per-run, per-item paths, so a path recorded in one checkpoint is not a
33
34
  path recorded in the other, and adding branch 2 cannot widen what branch 1 authorizes.
@@ -121,6 +122,8 @@ function Find-EpicWorktreeFeatureRecord {
121
122
  Parsed epic checkpoint, or $null when absent/unreadable.
122
123
  .PARAMETER WorktreePath
123
124
  The target worktree path extracted from the command text.
125
+ .PARAMETER BaseRoot
126
+ Optional. The roots a relative recorded worktree_path is joined to (issue #995).
124
127
  .OUTPUTS
125
128
  System.Object or $null
126
129
  #>
@@ -130,7 +133,9 @@ function Find-EpicWorktreeFeatureRecord {
130
133
  $Checkpoint,
131
134
 
132
135
  [AllowNull()]
133
- [string] $WorktreePath
136
+ [string] $WorktreePath,
137
+
138
+ [AllowNull()][AllowEmptyCollection()][string[]] $BaseRoot = @()
134
139
  )
135
140
 
136
141
  if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
@@ -141,17 +146,14 @@ function Find-EpicWorktreeFeatureRecord {
141
146
  return $null
142
147
  }
143
148
 
144
- $normalizedTarget = ($WorktreePath -replace '\\', '/').TrimEnd('/')
145
-
146
- # Scan every recorded feature for a worktree_path that matches the removal target;
147
- # path separators are normalized so Windows- and POSIX-style paths compare equal.
149
+ # Scan every recorded feature for a worktree_path that names the removal target
150
+ # through the shared matcher (separator, trailing slash, case rule, relative join).
148
151
  foreach ($feature in @($Checkpoint.features)) {
149
152
  $featureProps = @($feature.PSObject.Properties.Name)
150
153
  if ($featureProps -notcontains 'worktree_path') {
151
154
  continue
152
155
  }
153
- $normalizedFeaturePath = (([string]$feature.worktree_path) -replace '\\', '/').TrimEnd('/')
154
- if ($normalizedFeaturePath -eq $normalizedTarget) {
156
+ if (Test-WorktreeRunRecordedPathMatchesTarget -RecordedPath ([string]$feature.worktree_path) -TargetPath $WorktreePath -BaseRoot $BaseRoot) {
155
157
  return $feature
156
158
  }
157
159
  }
@@ -198,6 +200,8 @@ function Test-ParallelCheckpointAllowsWorktreeRemoval {
198
200
  Parsed parallel-orchestrator checkpoint, or $null when absent/unreadable.
199
201
  .PARAMETER WorktreePath
200
202
  The target worktree path extracted from the command text.
203
+ .PARAMETER BaseRoot
204
+ Optional. The roots a relative recorded worktree_path is joined to (issue #995).
201
205
  .OUTPUTS
202
206
  System.Boolean
203
207
  #>
@@ -208,7 +212,9 @@ function Test-ParallelCheckpointAllowsWorktreeRemoval {
208
212
  $Checkpoint,
209
213
 
210
214
  [AllowNull()]
211
- [string] $WorktreePath
215
+ [string] $WorktreePath,
216
+
217
+ [AllowNull()][AllowEmptyCollection()][string[]] $BaseRoot = @()
212
218
  )
213
219
 
214
220
  if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
@@ -224,10 +230,8 @@ function Test-ParallelCheckpointAllowsWorktreeRemoval {
224
230
  return $false
225
231
  }
226
232
 
227
- $normalizedTarget = ($WorktreePath -replace '\\', '/').TrimEnd('/')
228
-
229
- # Scan every recorded item for a worktree_path that matches the removal target;
230
- # path separators are normalized exactly as the epic branch normalizes them.
233
+ # Scan every recorded item for a worktree_path that names the removal target through
234
+ # the same shared matcher the epic branch uses.
231
235
  foreach ($item in @($Checkpoint.items)) {
232
236
  if ($null -eq $item) {
233
237
  continue
@@ -236,8 +240,7 @@ function Test-ParallelCheckpointAllowsWorktreeRemoval {
236
240
  if ($itemProps -notcontains 'worktree_path') {
237
241
  continue
238
242
  }
239
- $normalizedItemPath = (([string]$item.worktree_path) -replace '\\', '/').TrimEnd('/')
240
- if ($normalizedItemPath -ne $normalizedTarget) {
243
+ if (-not (Test-WorktreeRunRecordedPathMatchesTarget -RecordedPath ([string]$item.worktree_path) -TargetPath $WorktreePath -BaseRoot $BaseRoot)) {
241
244
  continue
242
245
  }
243
246
  if ($itemProps -notcontains 'merge_status') {
@@ -366,14 +369,19 @@ function Get-EpicWorktreeRemovalTargetDenial {
366
369
  }
367
370
  }
368
371
  $checkpoint = $epicRead.Checkpoint
372
+ # A relative record is joined to the main checkout root and to the root holding the
373
+ # checkpoint read (issue #995); the main root is derived once per target.
374
+ $mainRoot = Get-WorktreeRunMainCheckoutRoot -SessionRoot (Get-Location).Path
375
+ $epicBaseRoot = Get-WorktreeRunRecordBaseRoot -MainRoot $mainRoot -Read $epicRead
376
+ $parallelBaseRoot = Get-WorktreeRunRecordBaseRoot -MainRoot $mainRoot -Read $parallelRead
369
377
 
370
- $featureRecord = Find-EpicWorktreeFeatureRecord -Checkpoint $checkpoint -WorktreePath $worktreePath
378
+ $featureRecord = Find-EpicWorktreeFeatureRecord -Checkpoint $checkpoint -WorktreePath $worktreePath -BaseRoot $epicBaseRoot
371
379
  if (Test-EpicWorktreeRemovalAllowed -FeatureRecord $featureRecord) {
372
380
  return $null
373
381
  }
374
382
 
375
383
  $parallelCheckpoint = $parallelRead.Checkpoint
376
- if (Test-ParallelCheckpointAllowsWorktreeRemoval -Checkpoint $parallelCheckpoint -WorktreePath $worktreePath) {
384
+ if (Test-ParallelCheckpointAllowsWorktreeRemoval -Checkpoint $parallelCheckpoint -WorktreePath $worktreePath -BaseRoot $parallelBaseRoot) {
377
385
  return $null
378
386
  }
379
387
 
@@ -387,8 +395,8 @@ function Get-EpicWorktreeRemovalTargetDenial {
387
395
  # the unchanged deny below. That exclusion is the only path by which the manifest
388
396
  # could widen what the gates protect, which is why it is required rather than
389
397
  # advisory, and why it is evaluated before the manifest predicate.
390
- if (-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $checkpoint -RecordArrayName 'features' -WorktreePath $worktreePath) -and
391
- -not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $parallelCheckpoint -RecordArrayName 'items' -WorktreePath $worktreePath) -and
398
+ if (-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $checkpoint -RecordArrayName 'features' -WorktreePath $worktreePath -BaseRoot $epicBaseRoot) -and
399
+ -not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $parallelCheckpoint -RecordArrayName 'items' -WorktreePath $worktreePath -BaseRoot $parallelBaseRoot) -and
392
400
  (Test-CleanupWorktreeManifestAuthorizesRemoval -WorktreePath $worktreePath)) {
393
401
  return $null
394
402
  }
@@ -400,7 +408,7 @@ function Get-EpicWorktreeRemovalTargetDenial {
400
408
  if ($epicRead.Target.Status -eq 'NoTarget' -and $parallelRead.Target.Status -eq 'NoTarget') {
401
409
  $prefix = "$($epicRead.Target.ReasonCode): $($epicRead.Target.Detail). "
402
410
  }
403
- $diagnostics = Get-EpicWorktreeGateDenyDiagnostics -EpicRead $epicRead -ParallelRead $parallelRead -WorktreePath $worktreePath
411
+ $diagnostics = Get-EpicWorktreeGateDenyDiagnostics -EpicRead $epicRead -ParallelRead $parallelRead -WorktreePath $worktreePath -MainRoot $mainRoot
404
412
  return Get-EpicWorktreeGateBlockDecision -Reason ('EPIC_WORKTREE_REMOVAL_BLOCKED: ' + $prefix + "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. " + $diagnostics)
405
413
  }
406
414
 
@@ -0,0 +1,244 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pre-tool-use hook that blocks a Write or Edit leaving a covered file above 800 lines.
4
+
5
+ .DESCRIPTION
6
+ This script is invoked by the Claude Code PreToolUse hook before any Write or Edit
7
+ operation. It enforces the hard tier of the tiered file-size policy: 500 lines is
8
+ guidance, 700 lines is advisory, and above 800 lines a split is enforced regardless
9
+ of logical separability. The policy functions live in the shared module
10
+ ../lib/file-size-policy/FileSizePolicy.psm1.
11
+
12
+ Decision order:
13
+ 1. A dependency-load failure denies (fail closed).
14
+ 2. An unreadable envelope denies.
15
+ 3. A path that is not covered (outside the extension allowlist, under an excluded
16
+ directory, or outside the repository root) allows with no file read.
17
+ 4. Write: the resulting size is the logical line count of content. The pre-write
18
+ file is read only when that size is above 800.
19
+ Edit: the pre-edit file is read through the ReadFileContent seam and the
20
+ old_string to new_string replacement is applied in memory. An unreadable or
21
+ missing file, or an edit that cannot be resolved, denies.
22
+ 5. The baseline-free pre-edit-size ratchet decides: a result above 800 lines is
23
+ denied unless the file already existed and does not grow.
24
+
25
+ The hook does not read config/file-size-baseline.json, because it ships to
26
+ repositories that have no baseline; the repository's CI gate is the
27
+ baseline-aware check. The deny shape is hookSpecificOutput.permissionDecision =
28
+ 'deny' with permissionDecisionReason, exit 0.
29
+
30
+ .NOTES
31
+ Compatible with PowerShell 7+.
32
+ This script must not modify any state; it is a read-only validation gate.
33
+ #>
34
+ [CmdletBinding()]
35
+ param()
36
+ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
37
+
38
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'HookPayload.psm1' -ErrorRecord $_ }
39
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/file-size-policy/FileSizePolicy.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'FileSizePolicy.psm1' -ErrorRecord $_ }
40
+
41
+ function Get-FileSizeLimitBlockDecision {
42
+ <#
43
+ .SYNOPSIS
44
+ Constructs a deny-decision ordered dictionary carrying the supplied reason.
45
+ .PARAMETER Reason
46
+ The deny reason; it starts with FILE_SIZE_LIMIT_BLOCKED:.
47
+ #>
48
+ [CmdletBinding()]
49
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
50
+ param(
51
+ [Parameter(Mandatory)]
52
+ [string] $Reason
53
+ )
54
+
55
+ return [ordered]@{
56
+ hookSpecificOutput = [ordered]@{
57
+ hookEventName = 'PreToolUse'
58
+ permissionDecision = 'deny'
59
+ permissionDecisionReason = $Reason
60
+ }
61
+ }
62
+ }
63
+
64
+ function Get-FileSizeLimitAllowDecision {
65
+ <#
66
+ .SYNOPSIS
67
+ Constructs the allow-decision ordered dictionary.
68
+ #>
69
+ [CmdletBinding()]
70
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
71
+ param()
72
+
73
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
74
+ }
75
+
76
+ function Get-FileSizeLimitReplaceAllFlag {
77
+ <#
78
+ .SYNOPSIS
79
+ Returns the boolean value of the replace_all property of an Edit tool input.
80
+ .DESCRIPTION
81
+ A boolean is returned as is; a string is true only when its trimmed value is
82
+ 'true' (case-insensitive); every other value, and an absent property, is false.
83
+ No [bool] cast is used because [bool]'false' is $true in PowerShell.
84
+ .PARAMETER ToolInput
85
+ The tool_input object.
86
+ #>
87
+ [CmdletBinding()]
88
+ [OutputType([bool])]
89
+ param(
90
+ [AllowNull()]
91
+ [object] $ToolInput
92
+ )
93
+
94
+ $value = Get-ClaudeHookEnvelopeValue -Envelope $ToolInput -Name 'replace_all'
95
+ if ($value -is [bool]) {
96
+ return $value
97
+ }
98
+ if ($value -is [string]) {
99
+ return ($value.Trim() -ieq 'true')
100
+ }
101
+ return $false
102
+ }
103
+
104
+ function Invoke-FileSizeLimitDecision {
105
+ <#
106
+ .SYNOPSIS
107
+ Parses the PreToolUse envelope and returns an allow-or-deny decision.
108
+ .PARAMETER ToolInputRaw
109
+ The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
110
+ .PARAMETER RepositoryRoot
111
+ The repository root used to resolve relative paths. Defaults to the current
112
+ location, which is the project root when Claude Code runs the hook.
113
+ .PARAMETER ReadFileContent
114
+ Seam that returns the text of a file, $null when the file does not exist, and
115
+ throws when it cannot be read. Tests supply in-memory content.
116
+ #>
117
+ [CmdletBinding()]
118
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
119
+ param(
120
+ [AllowNull()]
121
+ [AllowEmptyString()]
122
+ [string] $ToolInputRaw,
123
+
124
+ [string] $RepositoryRoot = (Get-Location).ProviderPath,
125
+
126
+ [scriptblock] $ReadFileContent = {
127
+ param([string] $Path)
128
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
129
+ return [System.IO.File]::ReadAllText($Path)
130
+ }
131
+ )
132
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'FILE_SIZE_LIMIT_BLOCKED:'
133
+ if ($null -ne $dependencyDecision) { return $dependencyDecision }
134
+
135
+ $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
136
+ if (-not $payload.IsValid) {
137
+ return Get-FileSizeLimitBlockDecision -Reason ('FILE_SIZE_LIMIT_BLOCKED: payload anomaly - ' +
138
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
139
+ '. The gate fails closed on an envelope it cannot read.')
140
+ }
141
+
142
+ $toolInput = $payload.Value
143
+ $filePath = Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'file_path'
144
+ $relativePath = ConvertTo-FileSizeRelativePath -Path $filePath -RepositoryRoot $RepositoryRoot
145
+ if (-not (Test-FileSizeCoveredPath -RelativePath $relativePath)) {
146
+ return Get-FileSizeLimitAllowDecision
147
+ }
148
+
149
+ $isRooted = ($filePath -match '^[\\/]') -or ($filePath -match '^[A-Za-z]:[\\/]')
150
+ $readPath = if ($isRooted) { $filePath } else { Join-Path $RepositoryRoot $relativePath }
151
+ $toolName = Get-ClaudeHookToolInputString -ToolInput $payload.Envelope -Name 'tool_name'
152
+ $isEdit = ($toolName -eq 'Edit')
153
+
154
+ $resultCount = 0
155
+ if (-not $isEdit) {
156
+ $resultCount = Get-FileSizeLogicalLineCount -Content (Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'content')
157
+ if ($resultCount -le (Get-FileSizePolicyLimit).HardLimit) {
158
+ return Get-FileSizeLimitAllowDecision
159
+ }
160
+ }
161
+
162
+ try {
163
+ $preEditContent = & $ReadFileContent $readPath
164
+ } catch {
165
+ return Get-FileSizeLimitBlockDecision -Reason "FILE_SIZE_LIMIT_BLOCKED: '$relativePath' could not be read to measure its size ($($_.Exception.Message)). The gate fails closed for a covered file."
166
+ }
167
+
168
+ $preEditCount = $null
169
+ if ($null -ne $preEditContent) {
170
+ $preEditCount = Get-FileSizeLogicalLineCount -Content ([string]$preEditContent)
171
+ }
172
+
173
+ if ($isEdit) {
174
+ if ($null -eq $preEditContent) {
175
+ return Get-FileSizeLimitBlockDecision -Reason "FILE_SIZE_LIMIT_BLOCKED: '$relativePath' does not exist, so the Edit cannot be measured. The gate fails closed for a covered file."
176
+ }
177
+ $edit = Resolve-FileSizeEditContent -Content ([string]$preEditContent) `
178
+ -OldString (Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'old_string') `
179
+ -NewString (Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'new_string') `
180
+ -ReplaceAll (Get-FileSizeLimitReplaceAllFlag -ToolInput $toolInput)
181
+ if ($null -ne $edit.Failure) {
182
+ return Get-FileSizeLimitBlockDecision -Reason "FILE_SIZE_LIMIT_BLOCKED: the Edit to '$relativePath' could not be resolved in memory ($($edit.Failure)). The gate fails closed for a covered file."
183
+ }
184
+ $resultCount = Get-FileSizeLogicalLineCount -Content $edit.Content
185
+ }
186
+
187
+ $decision = Get-FileSizeHookDecision -RelativePath $relativePath -ResultLineCount $resultCount -PreEditLineCount $preEditCount
188
+ if ($decision.Allowed) {
189
+ return Get-FileSizeLimitAllowDecision
190
+ }
191
+ return Get-FileSizeLimitBlockDecision -Reason $decision.Reason
192
+ }
193
+
194
+ function Invoke-FileSizeLimitEntryPoint {
195
+ <#
196
+ .SYNOPSIS
197
+ Runs the file-size decision and returns the process exit code.
198
+ .DESCRIPTION
199
+ Acquires the payload through the shared reader unless the caller supplies one,
200
+ writes the compact JSON decision to the output stream, and returns 0. It never
201
+ returns 1: exit 1 is non-blocking for PreToolUse, so every anomaly is already a
202
+ deny decision by the time control reaches here.
203
+ .PARAMETER ToolInputRaw
204
+ Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
205
+ .PARAMETER ReadPayload
206
+ Seam for payload acquisition.
207
+ #>
208
+ [CmdletBinding()]
209
+ [OutputType([int])]
210
+ param(
211
+ [AllowNull()]
212
+ [AllowEmptyString()]
213
+ [string] $ToolInputRaw,
214
+
215
+ [scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
216
+ )
217
+
218
+ if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
219
+ $ToolInputRaw = [string](& $ReadPayload)
220
+ }
221
+
222
+ $decision = Invoke-FileSizeLimitDecision -ToolInputRaw $ToolInputRaw
223
+
224
+ $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
225
+
226
+ return 0
227
+ }
228
+
229
+ # Guard allows dot-sourcing in tests without executing the entrypoint.
230
+ if ($MyInvocation.InvocationName -eq '.') {
231
+ return
232
+ }
233
+ if ($script:HookDependencyGuardLoadFailed) { [Console]::Error.WriteLine('FILE_SIZE_LIMIT_BLOCKED: hook-dependency-guard.ps1 failed to load; the gate fails closed.'); exit 2 }
234
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'FILE_SIZE_LIMIT_BLOCKED:'
235
+ if ($null -ne $dependencyDecision) { $dependencyDecision | ConvertTo-Json -Compress -Depth 5 | Write-Output; exit 0 }
236
+
237
+ # The entry point returns its [int] exit code as the last pipeline element and the
238
+ # decision JSON before it, so the decision is written explicitly here first.
239
+ $entryPointResult = @(Invoke-FileSizeLimitEntryPoint)
240
+ if ($entryPointResult.Count -gt 1) {
241
+ $entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
242
+ }
243
+
244
+ exit ([int]$entryPointResult[-1])
@@ -40,6 +40,13 @@
40
40
  delegation prompt to a receipt-gated subagent type carries the canonical issue
41
41
  number line and a `branch:` label naming the item's branch.
42
42
 
43
+ Author-asserted autoclose line (issue #989): for subagent `pr-author` only, once the
44
+ per-feature receipt is confirmed, a checkpoint that records an `issue-num` and whose
45
+ `epic_mode` is not true requires the prompt line
46
+ `Author-asserted autoclose issues: #<issue-num>` at the start of a line. Its absence denies
47
+ with the AUTHOR_ASSERTED_AUTOCLOSE_MISSING sub-code (Get-ModelRoutingAuthorAssertedReason).
48
+ The epic-scope path is not subject to this rule.
49
+
43
50
  .NOTES
44
51
  Compatible with PowerShell 7+. Read-only presence-gating deterrent.
45
52
  #>
@@ -198,6 +205,42 @@ function Get-ModelRoutingTargetCheckpointResolution {
198
205
  }
199
206
  }
200
207
 
208
+ function Get-ModelRoutingAuthorAssertedReason {
209
+ <#
210
+ .SYNOPSIS
211
+ Return the deny reason for a pr-author delegation that omits the author-asserted line, or $null.
212
+ .DESCRIPTION
213
+ Returns $null when the checkpoint records no usable issue-num, when it carries an
214
+ epic_mode property whose value is true (an epic child whose integration PR owns the
215
+ closing keyword), or when a prompt line starts with 'Author-asserted autoclose issues:'
216
+ and names '#<issue-num>' later on that line with no digit after the number. Property
217
+ presence is tested first because strict mode throws on a missing property.
218
+ .PARAMETER Checkpoint
219
+ The resolved per-feature checkpoint object.
220
+ .PARAMETER PromptText
221
+ The delegation prompt.
222
+ .OUTPUTS
223
+ System.String or $null
224
+ #>
225
+ [CmdletBinding()]
226
+ [OutputType([string])]
227
+ param(
228
+ [Parameter(Mandatory)][AllowNull()] $Checkpoint,
229
+ [Parameter(Mandatory)][AllowEmptyString()][string] $PromptText
230
+ )
231
+
232
+ $names = @($Checkpoint.PSObject.Properties.Name)
233
+ $issue = if ($names -contains 'issue-num') { ConvertTo-WorktreeItemIssueNumber -Value $Checkpoint.'issue-num' } else { $null }
234
+ if ($null -eq $issue -or ($names -contains 'epic_mode' -and $Checkpoint.epic_mode -eq $true)) {
235
+ return $null
236
+ }
237
+ $line = "Author-asserted autoclose issues: #$issue"
238
+ if ([regex]::IsMatch($PromptText, '(?m)^Author-asserted autoclose issues:[^\r\n]*#' + $issue + '(?!\d)')) {
239
+ return $null
240
+ }
241
+ return "MODEL_ROUTING_RECEIPT_BLOCKED: AUTHOR_ASSERTED_AUTOCLOSE_MISSING: the checkpoint records issue-num $issue, so the Agent(pr-author) delegation prompt must carry the line '$line' so the PR body emits '- Closes #$issue'. Add that line on its own line, or omit it only for an epic child (epic_mode true)."
242
+ }
243
+
201
244
  function Invoke-ModelRoutingReceiptDecision {
202
245
  <#
203
246
  .SYNOPSIS
@@ -274,6 +317,11 @@ function Invoke-ModelRoutingReceiptDecision {
274
317
 
275
318
  $checkpoint = Get-ModelRoutingCheckpoint -CheckpointPath $resolution.CheckpointPath
276
319
  if (Test-ModelRoutingReceiptPresent -Checkpoint $checkpoint -Subagent $subagent) {
320
+ # Issue #989: the pr-author delegation names the item's issue as author-asserted.
321
+ $authorAssertedReason = if ($subagent -eq 'pr-author') { Get-ModelRoutingAuthorAssertedReason -Checkpoint $checkpoint -PromptText $prompt } else { $null }
322
+ if ($authorAssertedReason) {
323
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'deny'; permissionDecisionReason = $authorAssertedReason } }
324
+ }
277
325
  return $allow
278
326
  }
279
327