@danmoisan/drm-copilot-mcp 1.1.16 → 1.1.18

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 (105) hide show
  1. package/out/mcp-server.js +1139 -133
  2. package/package.json +4 -3
  3. package/resources/claude-customizations/.claude/agents/feature-review.md +10 -1
  4. package/resources/claude-customizations/.claude/agents/orchestrator.md +16 -3
  5. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +18 -6
  6. package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +3 -3
  7. package/resources/claude-customizations/.claude/hooks/{enforce-powershell-batch-budget-route.ps1 → enforce-batch-budget-route.ps1} +12 -10
  8. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +221 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +42 -66
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +58 -10
  11. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +144 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +36 -47
  13. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +179 -27
  14. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -38
  15. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +4 -4
  16. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +58 -10
  17. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +59 -9
  18. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +114 -37
  19. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +3 -3
  20. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +99 -64
  21. package/resources/claude-customizations/.claude/lib/bash/abandon-parallel-item.sh +176 -0
  22. package/resources/claude-customizations/.claude/lib/bash/parallel-lane-assertion.sh +4 -2
  23. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +193 -14
  24. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusGlob.psm1 +51 -28
  25. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +5 -8
  26. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorState.psm1 +6 -13
  27. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +375 -0
  28. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateReceipts.psm1 +22 -15
  29. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRemediationAccounting.psm1 +337 -0
  30. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateRoutingContract.psm1 +9 -1
  31. package/resources/claude-customizations/.claude/lib/parallel-drift/Invoke-ParallelDriftDetection.ps1 +336 -0
  32. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDrift.psm1 +467 -0
  33. package/resources/claude-customizations/.claude/lib/parallel-drift/ParallelDriftHalt.psm1 +388 -0
  34. package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +53 -32
  35. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +19 -4
  36. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +497 -0
  37. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +119 -3
  38. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +21 -0
  39. package/resources/claude-customizations/.claude/rules/quality-tiers.md +1 -1
  40. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  41. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +2 -0
  42. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +11 -3
  43. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup-worktrees.sh +11 -2
  44. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +169 -5
  45. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_preserve_lib.sh +1 -1
  46. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_report_records_lib.sh +6 -43
  47. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +4 -15
  48. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +15 -6
  49. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  50. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +10 -0
  51. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +3 -1
  52. package/resources/claude-customizations/.claude/skills/invoke-csharp-engineer/SKILL.md +4 -0
  53. package/resources/claude-customizations/.claude/skills/invoke-powershell-engineer/SKILL.md +4 -0
  54. package/resources/claude-customizations/.claude/skills/invoke-python-engineer/SKILL.md +6 -3
  55. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +33 -13
  56. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +50 -21
  57. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +10 -10
  58. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +11 -35
  59. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +5 -1
  60. package/resources/claude-customizations/pack-manifests/core.json +10 -0
  61. package/resources/claude-customizations/pack-manifests/powershell.json +0 -1
  62. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  63. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +5 -3
  64. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  65. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  66. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +30 -6
  67. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +3 -1
  68. package/resources/codex-and-agents-customizations/.agents/skills/invoke-powershell-engineer/SKILL.md +0 -1
  69. package/resources/codex-and-agents-customizations/.agents/skills/invoke-python-engineer/SKILL.md +2 -3
  70. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +30 -7
  71. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -2
  72. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +18 -3
  73. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +10 -34
  74. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +1 -1
  75. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
  76. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -0
  77. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -0
  78. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -0
  79. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -0
  80. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -0
  81. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -0
  82. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +1 -1
  83. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +1 -1
  84. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +1 -1
  85. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +1 -1
  86. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +1 -1
  87. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +1 -1
  88. package/resources/codex-and-agents-customizations/.codex/agents/python-orchestrator.toml +2 -2
  89. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +3 -3
  90. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +3 -3
  91. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +3 -3
  92. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +3 -3
  93. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +3 -3
  94. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +3 -3
  95. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  96. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-batch-budget-route.ps1 +139 -0
  97. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +4 -124
  98. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +169 -79
  99. package/resources/codex-and-agents-customizations/pack-manifests/core.json +1 -0
  100. package/resources/customizations/.github/agents/python-orchestrator.agent.md +4 -4
  101. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +7 -8
  102. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +3 -3
  103. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +8 -0
  104. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +4 -0
  105. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +16 -1
@@ -1,17 +1,35 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Pre-tool-use hook that enforces the Python per-batch change budget.
3
+ Pre-tool-use hook that routes Python changes of more than three production files to the orchestrated large path.
4
4
 
5
5
  .DESCRIPTION
6
6
  This script is invoked by the Claude Code PreToolUse hook before any Write or Edit
7
- operation. When the target file is a Python file, it classifies the file as either
8
- production or test and checks the running count against the per-batch cap:
9
- - 3 production .py files per batch
10
- - 3 test .py files per batch
7
+ operation. When the target file is a Python file (.py), it decides whether the
8
+ change may proceed in direct mode.
9
+
10
+ Direct mode counts distinct production Python paths per Claude Code session. The
11
+ running set is persisted under .claude/state/python-batch-budget.<session_id>.json,
12
+ and repeated edits to the same file are counted once. The 4th distinct production
13
+ path is denied with a PYTHON_LARGE_PATH_REQUIRED reason that instructs the caller
14
+ to route the change through /orchestrate.
15
+
16
+ The orchestrated large path is detected from
17
+ <root>/artifacts/orchestration/orchestrator-state.json. The selected route is the
18
+ route_id value when that key is present, otherwise the path_selected value, and it
19
+ is usable only as a non-blank string. When the selected route is large,
20
+ remediation, or preparation and the checkpoint is not terminal (next_step is not
21
+ complete and completed_steps does not contain S12_complete), no path is denied for
22
+ count and no state is read or written. Every other checkpoint outcome, including
23
+ an absent, unreadable, or malformed checkpoint, enforces direct mode.
24
+
25
+ Test files are never counted. They are those matching:
26
+ - tests/**/*.py
27
+ - test_*.py
11
28
 
12
- A "batch" is scoped to the current Claude Code session. The running count is
13
- persisted under .claude/state/python-batch-budget.<session_id>.json. Only distinct
14
- file paths are counted; repeated edits to the same file consume one slot.
29
+ All other .py files are production files. Non-Python paths pass through. The
30
+ threshold of three production files is a routing constant and is not configurable
31
+ at runtime. Legacy prodCap, testCap, and testFiles keys in a persisted state file
32
+ are ignored when the state is loaded.
15
33
 
16
34
  The session id is resolved from the first non-empty of: the CLAUDE_SESSION_ID
17
35
  environment variable; the contents of <root>/.claude/state/current-session-id;
@@ -26,20 +44,13 @@
26
44
  dropped when state is rehydrated, so a state file carried between worktrees
27
45
  cannot spend this worktree's budget.
28
46
 
29
- Test files are those matching:
30
- - tests/**/*.py
31
- - test_*.py
32
-
33
- All other .py files are treated as production files. Non-Python paths pass through.
34
-
35
- The cap may be overridden per session by setting the environment variable
36
- CLAUDE_PYTHON_BUDGET_PROD or CLAUDE_PYTHON_BUDGET_TEST to a positive integer before
37
- the session starts, or by writing {"prodCap": N, "testCap": M} into the state file.
47
+ When a production path is denied, the script emits a PreToolUse JSON response with
48
+ hookSpecificOutput.permissionDecision = 'deny' and exits 0. Files already counted
49
+ are always allowed through.
38
50
 
39
- When the cap would be exceeded by a new file, the script emits a PreToolUse JSON
40
- response with hookSpecificOutput.permissionDecision = 'deny' and exits 0. The session
41
- must explicitly reset the counter by deleting the state file before starting a new
42
- batch. Files already counted are always allowed through.
51
+ Known limitation: a stale non-terminal large-path checkpoint left at the root
52
+ exempts a later direct-mode session at that root. Orchestrator checkpoint hygiene
53
+ (issue #673) moves foreign checkpoints aside before a new run.
43
54
 
44
55
  .NOTES
45
56
  Compatible with PowerShell 7+.
@@ -49,6 +60,7 @@ param()
49
60
 
50
61
 
51
62
  Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
63
+ . (Join-Path $PSScriptRoot 'enforce-batch-budget-route.ps1')
52
64
 
53
65
  function Test-PythonBatchBudgetPathInRoot {
54
66
  <#
@@ -171,25 +183,24 @@ function Get-PythonBatchBudgetSessionId {
171
183
  }
172
184
 
173
185
  function Get-PythonBatchBudgetState {
186
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'TestCap', Justification = 'Accepted and ignored for callers written against the removed test-file cap.')]
174
187
  [CmdletBinding()]
175
188
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
176
189
  param(
177
190
  [Parameter(Mandatory)]
178
191
  [int] $ProdCap,
179
192
 
180
- [Parameter(Mandatory)]
181
- [int] $TestCap
193
+ [int] $TestCap = 0
182
194
  )
183
195
 
184
196
  [ordered]@{
185
197
  prodCap = $ProdCap
186
- testCap = $TestCap
187
198
  prodFiles = @()
188
- testFiles = @()
189
199
  }
190
200
  }
191
201
 
192
202
  function ConvertTo-PythonBatchBudgetState {
203
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'TestCap', Justification = 'Accepted and ignored for callers written against the removed test-file cap.')]
193
204
  [CmdletBinding()]
194
205
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
195
206
  param(
@@ -199,17 +210,16 @@ function ConvertTo-PythonBatchBudgetState {
199
210
  [Parameter(Mandatory)]
200
211
  [int] $ProdCap,
201
212
 
202
- [Parameter(Mandatory)]
203
- [int] $TestCap,
213
+ [int] $TestCap = 0,
204
214
 
205
215
  [AllowNull()]
206
216
  [AllowEmptyString()]
207
217
  [string] $Root = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent)
208
218
  )
209
219
 
210
- $state = Get-PythonBatchBudgetState -ProdCap $ProdCap -TestCap $TestCap
211
- if ($null -ne $InputObject.prodCap) { $state.prodCap = [int]$InputObject.prodCap }
212
- if ($null -ne $InputObject.testCap) { $state.testCap = [int]$InputObject.testCap }
220
+ # Only prodFiles is carried over. Persisted prodCap, testCap, and testFiles keys
221
+ # come from the removed test-file cap and runtime overrides and are ignored.
222
+ $state = Get-PythonBatchBudgetState -ProdCap $ProdCap
213
223
 
214
224
  # Persisted entries that resolve outside this root belong to another worktree
215
225
  # and are dropped, so a state file carried across worktrees cannot spend this
@@ -217,9 +227,6 @@ function ConvertTo-PythonBatchBudgetState {
217
227
  if ($null -ne $InputObject.prodFiles) {
218
228
  $state.prodFiles = @(@($InputObject.prodFiles) | Where-Object { Test-PythonBatchBudgetPathInRoot -Path $_ -Root $Root })
219
229
  }
220
- if ($null -ne $InputObject.testFiles) {
221
- $state.testFiles = @(@($InputObject.testFiles) | Where-Object { Test-PythonBatchBudgetPathInRoot -Path $_ -Root $Root })
222
- }
223
230
 
224
231
  return $state
225
232
  }
@@ -249,6 +256,7 @@ function Get-PythonBatchBudgetBlockDecision {
249
256
  }
250
257
 
251
258
  function Invoke-PythonBatchBudgetDecision {
259
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', 'TestCap', Justification = 'Accepted and ignored for callers written against the removed test-file cap.')]
252
260
  [CmdletBinding()]
253
261
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
254
262
  param(
@@ -258,12 +266,19 @@ function Invoke-PythonBatchBudgetDecision {
258
266
  [Parameter(Mandatory)]
259
267
  [System.Collections.IDictionary] $State,
260
268
 
261
- [Parameter(Mandatory)]
262
- [string] $StateFile,
269
+ [AllowEmptyString()]
270
+ [string] $StateFile = '',
271
+
272
+ [int] $TestCap = 0,
263
273
 
264
274
  [AllowNull()]
265
275
  [AllowEmptyString()]
266
- [string] $Root = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent)
276
+ [string] $Root = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent),
277
+
278
+ [switch] $LargePathRoute,
279
+
280
+ [AllowEmptyString()]
281
+ [string] $ObservedRoute = ''
267
282
  )
268
283
 
269
284
  $normalized = $FilePath -replace '\\', '/'
@@ -278,27 +293,32 @@ function Invoke-PythonBatchBudgetDecision {
278
293
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
279
294
  }
280
295
 
296
+ # The orchestrated large path has no production-file cap, so nothing is counted.
297
+ if ($LargePathRoute) {
298
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
299
+ }
300
+
301
+ # Test files never count toward the routing threshold.
281
302
  $isTestFile = ($normalized -match '(^|/)tests/.*\.py$') -or ($normalized -match '(^|/)test_[^/]+\.py$')
282
- $targetList = if ($isTestFile) { @($State.testFiles) } else { @($State.prodFiles) }
283
- $cap = if ($isTestFile) { [int]$State.testCap } else { [int]$State.prodCap }
284
- $kind = if ($isTestFile) { 'test' } else { 'production' }
303
+ if ($isTestFile) {
304
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
305
+ }
285
306
 
286
- if ($targetList -contains $normalized) {
307
+ $countedFiles = @($State.prodFiles)
308
+ if ($countedFiles -contains $normalized) {
287
309
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $false }
288
310
  }
289
311
 
290
- if ($targetList.Count -ge $cap) {
291
- $currentFiles = ($targetList -join ', ')
292
- $kindUpper = $kind.ToUpperInvariant()
293
- $reason = "Python per-batch budget exceeded: $kind file cap is $cap and is already full ($currentFiles). Requested new file: $normalized. Split the work into a new batch, raise the cap via CLAUDE_PYTHON_BUDGET_$kindUpper environment variable with approved scope, or reset the batch by deleting $StateFile."
312
+ $cap = [int]$State.prodCap
313
+ if ($countedFiles.Count -ge $cap) {
314
+ Write-Verbose "Denying production Python path '$normalized' in direct mode; counted paths are recorded in '$StateFile'."
315
+ $counted = $countedFiles -join ', '
316
+ $route = if ([string]::IsNullOrWhiteSpace($ObservedRoute)) { 'none' } else { $ObservedRoute }
317
+ $reason = "PYTHON_LARGE_PATH_REQUIRED: this change touches more than $cap production Python files (already counted: $counted; requested: $normalized). A change of this size belongs on the orchestrated large path, which has no production-file cap. Route the change through /orchestrate. Checkpoint route observed: $route."
294
318
  return Get-PythonBatchBudgetBlockDecision -Reason $reason -State $State
295
319
  }
296
320
 
297
- if ($isTestFile) {
298
- $State.testFiles = @($State.testFiles) + @($normalized)
299
- } else {
300
- $State.prodFiles = @($State.prodFiles) + @($normalized)
301
- }
321
+ $State.prodFiles = $countedFiles + @($normalized)
302
322
 
303
323
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' }; state = $State; shouldWriteState = $true }
304
324
  }
@@ -311,7 +331,6 @@ function Invoke-PythonBatchBudgetHook {
311
331
  [string] $SessionId = '',
312
332
  [string] $Root = (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent),
313
333
  [int] $ProdCap = 3,
314
- [int] $TestCap = 3,
315
334
  [scriptblock] $ReadSessionIdFile = {
316
335
  param([string] $Path)
317
336
  if (Test-Path -LiteralPath $Path -PathType Leaf) {
@@ -319,6 +338,13 @@ function Invoke-PythonBatchBudgetHook {
319
338
  }
320
339
  return ''
321
340
  },
341
+ [scriptblock] $ReadCheckpoint = {
342
+ param([string] $Path)
343
+ if (Test-Path -LiteralPath $Path -PathType Leaf) {
344
+ return (Get-Content -LiteralPath $Path -Raw)
345
+ }
346
+ return ''
347
+ },
322
348
  [scriptblock] $TestPathExists = { param([string] $Path) Test-Path -Path $Path },
323
349
  [scriptblock] $EnsureDirectory = { param([string] $Path) New-Item -ItemType Directory -Path $Path -Force | Out-Null },
324
350
  [scriptblock] $ReadState = { param([string] $Path) Get-Content -Path $Path -Raw },
@@ -356,23 +382,41 @@ function Invoke-PythonBatchBudgetHook {
356
382
  -SessionIdFilePath (Join-Path -Path $stateDir -ChildPath 'current-session-id') `
357
383
  -ReadSessionIdFile $ReadSessionIdFile
358
384
 
385
+ $stateFile = Join-Path -Path $stateDir -ChildPath ("python-batch-budget.$resolvedSessionId.json")
386
+
387
+ # The checkpoint is read before any state operation, so the large path neither
388
+ # creates the state directory nor reads or writes the state file.
389
+ $checkpointPath = Join-Path -Path $Root -ChildPath 'artifacts/orchestration/orchestrator-state.json'
390
+ $checkpointText = ''
391
+ try {
392
+ $checkpointText = [string](& $ReadCheckpoint $checkpointPath)
393
+ } catch {
394
+ Write-Verbose "Treating unreadable orchestrator checkpoint '$checkpointPath' as direct mode: $($_.Exception.Message)"
395
+ $checkpointText = ''
396
+ }
397
+
398
+ $isLargePath = Test-BatchBudgetLargePathRoute -CheckpointText $checkpointText
399
+ $observedRoute = Get-BatchBudgetSelectedRoute -CheckpointText $checkpointText
400
+ if ($isLargePath) {
401
+ return Invoke-PythonBatchBudgetDecision -FilePath $filePath -State (Get-PythonBatchBudgetState -ProdCap $ProdCap) -StateFile $stateFile -Root $Root -LargePathRoute
402
+ }
403
+
359
404
  if (-not (& $TestPathExists $stateDir)) {
360
405
  & $EnsureDirectory $stateDir
361
406
  }
362
407
 
363
- $stateFile = Join-Path -Path $stateDir -ChildPath ("python-batch-budget.$resolvedSessionId.json")
364
- $state = Get-PythonBatchBudgetState -ProdCap $ProdCap -TestCap $TestCap
408
+ $state = Get-PythonBatchBudgetState -ProdCap $ProdCap
365
409
 
366
410
  if (& $TestPathExists $stateFile) {
367
411
  try {
368
412
  $loaded = & $ReadState $stateFile | ConvertFrom-Json -ErrorAction Stop
369
- $state = ConvertTo-PythonBatchBudgetState -InputObject $loaded -ProdCap $ProdCap -TestCap $TestCap -Root $Root
413
+ $state = ConvertTo-PythonBatchBudgetState -InputObject $loaded -ProdCap $ProdCap -Root $Root
370
414
  } catch {
371
415
  Write-Verbose "Ignoring unreadable Python batch-budget state file '$stateFile': $($_.Exception.Message)"
372
416
  }
373
417
  }
374
418
 
375
- $decision = Invoke-PythonBatchBudgetDecision -FilePath $filePath -State $state -StateFile $stateFile -Root $Root
419
+ $decision = Invoke-PythonBatchBudgetDecision -FilePath $filePath -State $state -StateFile $stateFile -Root $Root -ObservedRoute $observedRoute
376
420
  if ($decision.shouldWriteState) {
377
421
  try {
378
422
  & $WriteState $stateFile $decision.state
@@ -420,16 +464,7 @@ function Invoke-PythonBatchBudgetEntryPoint {
420
464
  # worktree-derived identifier.
421
465
  $sessionId = [string]$env:CLAUDE_SESSION_ID
422
466
 
423
- $prodCap = 3
424
- $testCap = 3
425
- if ($env:CLAUDE_PYTHON_BUDGET_PROD -match '^\d+$') {
426
- $prodCap = [int]$env:CLAUDE_PYTHON_BUDGET_PROD
427
- }
428
- if ($env:CLAUDE_PYTHON_BUDGET_TEST -match '^\d+$') {
429
- $testCap = [int]$env:CLAUDE_PYTHON_BUDGET_TEST
430
- }
431
-
432
- $decision = Invoke-PythonBatchBudgetHook -ToolInputRaw $ToolInputRaw -SessionId $sessionId -ProdCap $prodCap -TestCap $testCap
467
+ $decision = Invoke-PythonBatchBudgetHook -ToolInputRaw $ToolInputRaw -SessionId $sessionId
433
468
  if ($decision.hookSpecificOutput.permissionDecision -eq 'deny') {
434
469
  $decision.Remove('state')
435
470
  $decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env bash
2
+ # abandon-parallel-item.sh: bundled destination-runtime port of
3
+ # scripts/dev_tools/parallel_mutation_abandon_cli.py (issue #763). It executes the
4
+ # abandon disposition of /parallel-remove -- close the item's pull request, then
5
+ # remove its worktree -- with nothing but bash, gh, and git on PATH: no Python, no
6
+ # Poetry, no repository checkout.
7
+ #
8
+ # Usage:
9
+ # bash .claude/lib/bash/abandon-parallel-item.sh --item <key> --disposition abandon --confirm-abandon --pr <pr-number> --worktree <worktree-path>
10
+ #
11
+ # The PreToolUse hook .claude/hooks/enforce-parallel-abandon-gate.ps1 matches the
12
+ # command text of that invocation: a command carrying the disposition token
13
+ # without the confirmation marker is denied before it runs. The option tokens are
14
+ # declared once each below, and tests/scripts/dev_tools/test_parallel_abandon_token_seam.py
15
+ # binds them to the Python reference and to the hook.
16
+ #
17
+ # Exit codes:
18
+ # 0 success; both side effects ran and nothing was written to stdout
19
+ # 1 a side effect failed; the first failure is reported and nothing after it runs
20
+ # 2 refusal (a disposition other than abandon, or no confirmation marker) or a
21
+ # usage error; both are reported before any side effect
22
+ #
23
+ # Declared divergences from the Python reference, all within "usage errors exit 2":
24
+ # - argparse usage text is not reproduced; only exit code 2 is matched, with one
25
+ # stderr line prefixed "PARALLEL_ABANDON_ERROR: usage error:".
26
+ # - option abbreviations are rejected (argparse accepts a unique prefix).
27
+ # - --item and --pr accept only canonical decimal integers (0 or [1-9][0-9]*).
28
+ # - -h and --help are unknown options.
29
+ # - an option value beginning with -- is treated as a missing value.
30
+ # Repeated options keep the last value and the joined --name=value form is
31
+ # accepted, as argparse does.
32
+ set -euo pipefail
33
+
34
+ # The option tokens and messages the abandon surface is built from. The first
35
+ # three lines are read by the seam test and by tests/shell/parallel_abandon.bats.
36
+ readonly ABANDON_DISPOSITION_OPTION='--disposition'
37
+ readonly ABANDON_DISPOSITION_VALUE='abandon'
38
+ readonly ABANDON_CONFIRM_OPTION='--confirm-abandon'
39
+ readonly ABANDON_ITEM_OPTION='--item'
40
+ readonly ABANDON_PR_OPTION='--pr'
41
+ readonly ABANDON_WORKTREE_OPTION='--worktree'
42
+ readonly ABANDON_VALID_DISPOSITIONS='detach abandon'
43
+ readonly ABANDON_ERROR_PREFIX='PARALLEL_ABANDON_ERROR:'
44
+
45
+ abandon_usage_error() {
46
+ # Report a usage error on stderr and exit 2 before any side effect.
47
+ #
48
+ # Args: $1 = the detail naming the offending argument.
49
+ printf '%s usage error: %s\n' "$ABANDON_ERROR_PREFIX" "$1" >&2
50
+ exit 2
51
+ }
52
+
53
+ abandon_require_integer() {
54
+ # Require a canonical decimal integer, as the reference's type=int requires
55
+ # an integer; leading zeros, signs, and blanks are usage errors here.
56
+ #
57
+ # Args: $1 = the value, $2 = the option it was given for.
58
+ if [[ ! $1 =~ ^(0|[1-9][0-9]*)$ ]]; then
59
+ abandon_usage_error "$2 requires a canonical decimal integer; got '$1'"
60
+ fi
61
+ }
62
+
63
+ abandon_require_disposition() {
64
+ # Require a member of the disposition vocabulary, as the reference's
65
+ # argparse choices do. Membership is not the abandon refusal: a valid
66
+ # disposition other than abandon is refused later with its own message.
67
+ #
68
+ # Args: $1 = the disposition value.
69
+ local candidate
70
+ # Scan the space-separated vocabulary for an exact match.
71
+ for candidate in $ABANDON_VALID_DISPOSITIONS; do
72
+ if [ "$candidate" = "$1" ]; then
73
+ return 0
74
+ fi
75
+ done
76
+ abandon_usage_error "$ABANDON_DISPOSITION_OPTION must be one of: $ABANDON_VALID_DISPOSITIONS; got '$1'"
77
+ }
78
+
79
+ abandon_side_effect_failed() {
80
+ # Report a failed side effect and exit 1, so nothing after it runs.
81
+ #
82
+ # Args: $1 = the exit code (-1 when the executable is not on PATH), then the
83
+ # command vector, which is rendered joined by single spaces.
84
+ local code="$1"
85
+ shift
86
+ printf '%s abandon side effect failed with exit code %s: %s\n' "$ABANDON_ERROR_PREFIX" "$code" "$*" >&2
87
+ exit 1
88
+ }
89
+
90
+ abandon_run_side_effect() {
91
+ # Run one side-effect command resolved through PATH. A non-zero exit is
92
+ # captured rather than aborting under set -e, so it can be reported.
93
+ #
94
+ # Args: the command vector, executable name first.
95
+ local executable status=0
96
+ if ! executable="$(command -v -- "$1")"; then
97
+ abandon_side_effect_failed -1 "$@"
98
+ fi
99
+ "$executable" "${@:2}" || status=$?
100
+ if [ "$status" -ne 0 ]; then
101
+ abandon_side_effect_failed "$status" "$@"
102
+ fi
103
+ }
104
+
105
+ item=''
106
+ disposition=''
107
+ pr=''
108
+ worktree=''
109
+ confirmed=0
110
+ have_item=0
111
+ have_disposition=0
112
+ have_pr=0
113
+ have_worktree=0
114
+
115
+ # Parse the argument vector. The bare confirmation flag sets the marker; each of
116
+ # the four valued options takes its value joined (--name=value) or as the next
117
+ # argument; anything else -- an unknown option, an abbreviation, a value on the
118
+ # confirmation flag, or a positional argument -- is a usage error.
119
+ while [ "$#" -gt 0 ]; do
120
+ argument="$1"
121
+ shift
122
+ if [ "$argument" = "$ABANDON_CONFIRM_OPTION" ]; then
123
+ confirmed=1
124
+ continue
125
+ fi
126
+ name="${argument%%=*}"
127
+ case "$name" in
128
+ "$ABANDON_ITEM_OPTION" | "$ABANDON_DISPOSITION_OPTION" | "$ABANDON_PR_OPTION" | "$ABANDON_WORKTREE_OPTION") ;;
129
+ *) abandon_usage_error "unrecognized argument: $argument" ;;
130
+ esac
131
+ # Take the joined value when present, otherwise the next argument; an absent
132
+ # value is represented by the option prefix so one check rejects both forms.
133
+ if [ "$name" != "$argument" ]; then
134
+ value="${argument#*=}"
135
+ elif [ "$#" -gt 0 ]; then
136
+ value="$1"
137
+ shift
138
+ else
139
+ value='--'
140
+ fi
141
+ if [[ $value == --* ]]; then
142
+ abandon_usage_error "$name requires a value"
143
+ fi
144
+ # Store the value on its option; a repeated option keeps the last value.
145
+ case "$name" in
146
+ "$ABANDON_ITEM_OPTION") item="$value" have_item=1 ;;
147
+ "$ABANDON_DISPOSITION_OPTION") disposition="$value" have_disposition=1 ;;
148
+ "$ABANDON_PR_OPTION") pr="$value" have_pr=1 ;;
149
+ *) worktree="$value" have_worktree=1 ;;
150
+ esac
151
+ done
152
+
153
+ # Every valued option is required, as in the reference parser.
154
+ if [ "$have_item" -eq 0 ] || [ "$have_disposition" -eq 0 ] || [ "$have_pr" -eq 0 ] || [ "$have_worktree" -eq 0 ]; then
155
+ abandon_usage_error "the options $ABANDON_ITEM_OPTION, $ABANDON_DISPOSITION_OPTION, $ABANDON_PR_OPTION, and $ABANDON_WORKTREE_OPTION are required"
156
+ fi
157
+ abandon_require_integer "$item" "$ABANDON_ITEM_OPTION"
158
+ abandon_require_integer "$pr" "$ABANDON_PR_OPTION"
159
+ abandon_require_disposition "$disposition"
160
+
161
+ # The two refusals come before any side effect: this entry point exists only to
162
+ # execute the confirmed abandon path, so any other request does nothing.
163
+ if [ "$disposition" != "$ABANDON_DISPOSITION_VALUE" ]; then
164
+ printf "%s this CLI executes the '%s' disposition only; got '%s'.\n" "$ABANDON_ERROR_PREFIX" "$ABANDON_DISPOSITION_VALUE" "$disposition" >&2
165
+ exit 2
166
+ fi
167
+ if [ "$confirmed" -ne 1 ]; then
168
+ printf '%s refusing to abandon item %s without the explicit %s confirmation marker; no side effect was performed.\n' "$ABANDON_ERROR_PREFIX" "$item" "$ABANDON_CONFIRM_OPTION" >&2
169
+ exit 2
170
+ fi
171
+
172
+ # Close the pull request before removing the worktree: removing the worktree
173
+ # first would leave an open pull request whose checkout is gone.
174
+ abandon_run_side_effect gh pr close "$pr"
175
+ abandon_run_side_effect git worktree remove "$worktree"
176
+ exit 0
@@ -82,8 +82,10 @@ pla_parse_edges() {
82
82
  PLA_EDGES=()
83
83
  local -a tokens=()
84
84
  # read -ra rather than an unquoted expansion, so a token carrying a glob
85
- # character is never subjected to pathname expansion.
86
- read -ra tokens <<<"$text"
85
+ # character is never subjected to pathname expansion. Newline, CR, VT, and FF
86
+ # become spaces first: read consumes one newline-terminated record and splits
87
+ # it on IFS only, so the Python str.split() whitespace set would otherwise differ.
88
+ read -ra tokens <<<"${text//[$'\n\r\v\f']/ }"
87
89
  for token in "${tokens[@]}"; do
88
90
  # Partition on the FIRST colon, matching str.partition: a token with two
89
91
  # colons yields a second endpoint that still carries one, which the lexis