@danmoisan/drm-copilot-mcp 1.1.21 → 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.
- package/out/mcp-server.js +2568 -1261
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agent-memory/orchestrator/MEMORY.md +1 -1
- package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_test_files_count_against_500_cap.md +4 -4
- package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -1
- package/resources/claude-customizations/.claude/agents/feature-review.md +8 -0
- package/resources/claude-customizations/.claude/agents/orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +46 -6
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +32 -7
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -1
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +4 -3
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +2 -1
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-green-ci.ps1 +325 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +22 -6
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +23 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +29 -21
- package/resources/claude-customizations/.claude/hooks/enforce-file-size-limit.ps1 +244 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +48 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +22 -11
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +2 -1
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +8 -2
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +15 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.current-branch.ps1 +149 -0
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +2 -1
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-feature-folder.ps1 +146 -0
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +25 -3
- package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +33 -6
- package/resources/claude-customizations/.claude/lib/file-size-policy/FileSizePolicy.psm1 +434 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1 +166 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +8 -3
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunRecordPath.psm1 +185 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +11 -21
- package/resources/claude-customizations/.claude/rules/general-code-change.md +4 -1
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -5
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +2 -2
- package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
- package/resources/claude-customizations/.claude/rules/shell.md +1 -1
- package/resources/claude-customizations/.claude/settings.json +4 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +2 -2
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +2 -1
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +11 -4
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +90 -14
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +39 -9
- package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +1 -1
- package/resources/claude-customizations/pack-manifests/core.json +14 -0
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +2 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +4 -1
- package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +8 -0
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/config.toml +7 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-file-size-limit.ps1 +282 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +2 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +2 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/file-size-policy.ps1 +421 -0
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/AGENTS.md +4 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +3 -0
- package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
- package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +1 -1
- package/resources/customizations/.github/agents/python-typed-engineer.agent.md +1 -1
- package/resources/customizations/.github/instructions/general-code-change.instructions.md +5 -2
- package/resources/customizations/.github/instructions/powershell-code-change.instructions.md +1 -1
- package/resources/customizations/.github/prompts/remediate-comments.prompt.md +1 -1
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +2 -1
- package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +8 -0
- package/resources/templates/policy_audit/code-review.yyyy-MM-ddTHH-mm.md +4 -4
- 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
|
|
33
|
-
explicit, PR-specific, session-bound record under condition 4
|
|
34
|
-
PR number
|
|
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.
|
|
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,
|
|
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.
|
|
136
|
-
does
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1
CHANGED
|
@@ -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
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|