@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.
Files changed (91) 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-completion-consistency.ps1 +4 -3
  13. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +2 -1
  14. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-green-ci.ps1 +325 -0
  15. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +22 -6
  16. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +23 -10
  17. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +29 -21
  18. package/resources/claude-customizations/.claude/hooks/enforce-file-size-limit.ps1 +244 -0
  19. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +48 -0
  20. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +22 -11
  21. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +2 -1
  22. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +8 -2
  23. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +15 -10
  24. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.current-branch.ps1 +149 -0
  25. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +2 -1
  26. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-feature-folder.ps1 +146 -0
  27. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +25 -3
  28. package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +33 -6
  29. package/resources/claude-customizations/.claude/lib/file-size-policy/FileSizePolicy.psm1 +434 -0
  30. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1 +166 -0
  31. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +8 -3
  32. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunRecordPath.psm1 +185 -0
  33. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +11 -21
  34. package/resources/claude-customizations/.claude/rules/general-code-change.md +4 -1
  35. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +28 -5
  36. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +2 -2
  37. package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
  38. package/resources/claude-customizations/.claude/rules/shell.md +1 -1
  39. package/resources/claude-customizations/.claude/settings.json +4 -0
  40. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +2 -2
  41. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +2 -1
  42. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -0
  43. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +11 -4
  44. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +4 -1
  45. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +90 -14
  46. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +3 -1
  47. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +39 -9
  48. package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +1 -1
  49. package/resources/claude-customizations/pack-manifests/core.json +14 -0
  50. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +2 -1
  51. package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  52. package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +8 -0
  53. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +4 -1
  54. package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -1
  55. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
  56. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
  57. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
  58. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
  59. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
  60. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
  61. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +8 -0
  62. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
  63. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
  64. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
  65. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
  66. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
  67. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
  68. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
  69. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
  70. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
  71. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
  72. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
  73. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
  74. package/resources/codex-and-agents-customizations/.codex/config.toml +7 -1
  75. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-file-size-limit.ps1 +282 -0
  76. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +2 -1
  77. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +2 -1
  78. package/resources/codex-and-agents-customizations/.codex/hooks/file-size-policy.ps1 +421 -0
  79. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
  80. package/resources/codex-and-agents-customizations/AGENTS.md +4 -1
  81. package/resources/codex-and-agents-customizations/pack-manifests/core.json +3 -0
  82. package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
  83. package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +1 -1
  84. package/resources/customizations/.github/agents/python-typed-engineer.agent.md +1 -1
  85. package/resources/customizations/.github/instructions/general-code-change.instructions.md +5 -2
  86. package/resources/customizations/.github/instructions/powershell-code-change.instructions.md +1 -1
  87. package/resources/customizations/.github/prompts/remediate-comments.prompt.md +1 -1
  88. package/resources/customizations/.github/skills/execution-environment/SKILL.md +2 -1
  89. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +8 -0
  90. package/resources/templates/policy_audit/code-review.yyyy-MM-ddTHH-mm.md +4 -4
  91. package/resources/templates/policy_audit/policy-audit.yyyy-MM-ddTHH-mm.md +4 -4
@@ -24,8 +24,8 @@
24
24
  throwing.
25
25
 
26
26
  .NOTES
27
- Compatible with PowerShell 7+. No external module dependencies, no subprocess,
28
- and no network. The single filesystem read and the single wall-clock read each
27
+ Compatible with PowerShell 7+. Imports only the in-repository sibling
28
+ WorktreeRunRecordPath.psm1 (issue #995); no subprocess and no network. The single filesystem read and the single wall-clock read each
29
29
  sit behind an injectable seam so tests drive them without writing a temporary
30
30
  file. Mirrored byte-identically under
31
31
  extensions/drm-copilot/resources/claude-customizations/.
@@ -35,6 +35,9 @@
35
35
  Set-StrictMode -Version Latest
36
36
  $ErrorActionPreference = 'Stop'
37
37
 
38
+ # The shared recorded-path matcher (issue #995), used by the checkpoint-coverage test.
39
+ Import-Module (Join-Path $PSScriptRoot '../worktree-resolution/WorktreeRunRecordPath.psm1') -ErrorAction Stop
40
+
38
41
  # Repo-relative location of the sanctioned-removal manifest. Kept as a script-scope
39
42
  # constant so the read seam is the only function that names the path.
40
43
  $script:CleanupWorktreeManifestPath = 'artifacts/orchestration/cleanup-worktrees-manifest.json'
@@ -355,6 +358,14 @@ function Test-CleanupManifestCheckpointCoversPath {
355
358
  the gate's existing deny. Reusing the gates' merge_status predicate here
356
359
  would reopen the one path by which the manifest could widen what the gates
357
360
  protect.
361
+
362
+ Case rule: a record covers the target when the shared matcher holds on the
363
+ inputs as given, or holds on the same inputs lower-cased with the invariant
364
+ culture. This keeps the exclusion at parity with
365
+ Find-CleanupWorktreeManifestRemovalRecord, which ignores case for every path,
366
+ so a record that differs from the target only by case still blocks the
367
+ manifest branch. The shared matcher itself stays ordinal for single-slash
368
+ POSIX paths because the gates' allow lookups must remain fail-closed.
358
369
  .PARAMETER Checkpoint
359
370
  Parsed checkpoint object, or $null when absent or unreadable.
360
371
  .PARAMETER RecordArrayName
@@ -362,6 +373,10 @@ function Test-CleanupManifestCheckpointCoversPath {
362
373
  for the parallel checkpoint.
363
374
  .PARAMETER WorktreePath
364
375
  The removal target extracted from the command text.
376
+ .PARAMETER BaseRoot
377
+ Optional. The roots a relative recorded worktree_path is joined to: the main
378
+ checkout root and the root that holds the checkpoint (issue #995). With none, a
379
+ relative record covers nothing and a rooted record compares as before.
365
380
  .OUTPUTS
366
381
  System.Boolean
367
382
  #>
@@ -377,7 +392,11 @@ function Test-CleanupManifestCheckpointCoversPath {
377
392
 
378
393
  [AllowNull()]
379
394
  [AllowEmptyString()]
380
- [string] $WorktreePath
395
+ [string] $WorktreePath,
396
+
397
+ [AllowNull()]
398
+ [AllowEmptyCollection()]
399
+ [string[]] $BaseRoot = @()
381
400
  )
382
401
 
383
402
  if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
@@ -388,8 +407,10 @@ function Test-CleanupManifestCheckpointCoversPath {
388
407
  return $false
389
408
  }
390
409
 
391
- $normalizedTarget = ConvertTo-CleanupWorktreeManifestNormalizedPath -Path $WorktreePath
410
+ $lowerBaseRoot = [string[]] @(@($BaseRoot) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | ForEach-Object { $_.ToLowerInvariant() })
392
411
 
412
+ # Every record is compared through the shared matcher, so a relative record covers
413
+ # the absolute target it names under a base root, and an unsafe one covers nothing.
393
414
  foreach ($record in @($Checkpoint.PSObject.Properties[$RecordArrayName].Value)) {
394
415
  if ($null -eq $record) {
395
416
  continue
@@ -398,8 +419,14 @@ function Test-CleanupManifestCheckpointCoversPath {
398
419
  if ($recordProperties -notcontains 'worktree_path') {
399
420
  continue
400
421
  }
401
- $normalizedRecordPath = ConvertTo-CleanupWorktreeManifestNormalizedPath -Path ([string]$record.worktree_path)
402
- if ($normalizedRecordPath -eq $normalizedTarget) {
422
+ $recordedPath = [string]$record.worktree_path
423
+ if (Test-WorktreeRunRecordedPathMatchesTarget -RecordedPath $recordedPath -TargetPath $WorktreePath -BaseRoot $BaseRoot) {
424
+ return $true
425
+ }
426
+ # Case-insensitive parity with Find-CleanupWorktreeManifestRemovalRecord: the same
427
+ # inputs lower-cased with the invariant culture, so the exclusion is never narrower
428
+ # than the manifest lookup it guards.
429
+ if (Test-WorktreeRunRecordedPathMatchesTarget -RecordedPath $recordedPath.ToLowerInvariant() -TargetPath $WorktreePath.ToLowerInvariant() -BaseRoot $lowerBaseRoot) {
403
430
  return $true
404
431
  }
405
432
  }
@@ -0,0 +1,434 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pure functions of the tiered file-size policy (500 guidance, 700 advisory, 800 hard limit).
4
+
5
+ .DESCRIPTION
6
+ Single source of truth for the repository file-size policy (issue #990). The
7
+ policy has three tiers: at 500 logical lines an author looks for a logical split,
8
+ at 700 lines an author splits more aggressively and reviewers report a
9
+ non-blocking Warning, and above 800 lines a split is enforced regardless of
10
+ logical separability. A file of exactly 800 lines passes.
11
+
12
+ Lines are counted as logical lines: the content is split on CRLF or LF, and a
13
+ trailing line terminator does not add a line. CRLF and LF content of the same
14
+ text therefore count identically, and the count equals the number of strings
15
+ Get-Content returns for the same file.
16
+
17
+ Covered files are identified by an extension allowlist; every other file
18
+ (Markdown, JSON, YAML, TOML, text fixtures) is outside the policy. Paths under
19
+ fixed build, cache, dependency, documentation, and virtual-environment
20
+ directory names, and paths under .claude/worktrees/, are not covered. The excluded
21
+ directory names are defined once, in Get-FileSizeExcludedSegment.
22
+
23
+ Every function is pure: no filesystem, process, network, environment, or clock
24
+ access. Callers supply the content, the paths, and the recorded sizes. The
25
+ policy constants live inside the functions rather than at module scope, so the
26
+ function definitions are self-contained and can be mirrored text-identically
27
+ into the dot-sourced Codex twin .codex/hooks/file-size-policy.ps1.
28
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
29
+ #>
30
+
31
+ Set-StrictMode -Version Latest
32
+ $ErrorActionPreference = 'Stop'
33
+
34
+ function Get-FileSizePolicyLimit {
35
+ <#
36
+ .SYNOPSIS
37
+ Return the three tier limits of the file-size policy.
38
+ .OUTPUTS
39
+ A PSCustomObject with Guidance (500), Advisory (700), and HardLimit (800).
40
+ #>
41
+ [CmdletBinding()]
42
+ [OutputType([pscustomobject])]
43
+ param()
44
+
45
+ return [pscustomobject]@{
46
+ Guidance = 500
47
+ Advisory = 700
48
+ HardLimit = 800
49
+ }
50
+ }
51
+
52
+ function Get-FileSizeExcludedSegment {
53
+ <#
54
+ .SYNOPSIS
55
+ Return the directory names excluded from the file-size policy.
56
+ .DESCRIPTION
57
+ A path with any of these names as a directory segment is not covered. This
58
+ function is the single source of the excluded names: Test-FileSizeCoveredPath
59
+ and the repository gate traversal both read it. Each call returns a new array.
60
+ .OUTPUTS
61
+ System.String[].
62
+ #>
63
+ [CmdletBinding()]
64
+ [OutputType([string[]])]
65
+ param()
66
+
67
+ return [string[]]@('.git', 'node_modules', 'artifacts', 'coverage', 'dist', 'out', 'virtual', 'docs',
68
+ 'build', '.venv', 'venv', 'env', '__pycache__', '.pytest_cache', '.mypy_cache', '.ruff_cache', '.tox',
69
+ '.nox', 'htmlcov', '.vscode-test')
70
+ }
71
+
72
+ function Get-FileSizeLogicalLineCount {
73
+ <#
74
+ .SYNOPSIS
75
+ Count the logical lines of a text.
76
+ .DESCRIPTION
77
+ Splits the content on CRLF or LF. A trailing line terminator does not add a
78
+ line, and empty or null content has zero lines.
79
+ .PARAMETER Content
80
+ The file content.
81
+ .OUTPUTS
82
+ System.Int32.
83
+ #>
84
+ [CmdletBinding()]
85
+ [OutputType([int])]
86
+ param(
87
+ [Parameter(Mandatory)]
88
+ [AllowNull()]
89
+ [AllowEmptyString()]
90
+ [string] $Content
91
+ )
92
+
93
+ if ([string]::IsNullOrEmpty($Content)) {
94
+ return 0
95
+ }
96
+
97
+ $count = [regex]::Split($Content, '\r?\n').Count
98
+ if ($Content.EndsWith("`n")) {
99
+ # The split yields one empty trailing element after a final terminator.
100
+ $count--
101
+ }
102
+ return $count
103
+ }
104
+
105
+ function Get-FileSizeTier {
106
+ <#
107
+ .SYNOPSIS
108
+ Classify a logical line count into a policy tier.
109
+ .DESCRIPTION
110
+ Returns 'Below' (under 500), 'Guidance' (500 through 699), 'Advisory' (700
111
+ through 799), or 'HardLimit' (800 and above). The hard block applies only
112
+ above 800; a count of exactly 800 is in the HardLimit tier and still passes.
113
+ .PARAMETER LineCount
114
+ A non-negative logical line count.
115
+ .OUTPUTS
116
+ System.String.
117
+ #>
118
+ [CmdletBinding()]
119
+ [OutputType([string])]
120
+ param(
121
+ [Parameter(Mandatory)]
122
+ [int] $LineCount
123
+ )
124
+
125
+ if ($LineCount -lt 0) {
126
+ throw "Get-FileSizeTier: LineCount must be non-negative; received $LineCount."
127
+ }
128
+
129
+ $limit = Get-FileSizePolicyLimit
130
+ if ($LineCount -ge $limit.HardLimit) { return 'HardLimit' }
131
+ if ($LineCount -ge $limit.Advisory) { return 'Advisory' }
132
+ if ($LineCount -ge $limit.Guidance) { return 'Guidance' }
133
+ return 'Below'
134
+ }
135
+
136
+ function ConvertTo-FileSizeRelativePath {
137
+ <#
138
+ .SYNOPSIS
139
+ Convert a file path to a repository-relative forward-slash path.
140
+ .DESCRIPTION
141
+ Backslashes are normalized to forward slashes. A relative path is returned
142
+ without a leading './'. An absolute path under the repository root is
143
+ returned relative to the root (compared case-insensitively, because the
144
+ root may come from a case-insensitive file system). A path outside the root,
145
+ a relative path that climbs above the root, and the root itself return
146
+ $null, which callers treat as not covered.
147
+ .PARAMETER Path
148
+ The file path from a tool payload or a discovery.
149
+ .PARAMETER RepositoryRoot
150
+ The repository root directory.
151
+ .OUTPUTS
152
+ System.String, or $null for a path outside the repository root.
153
+ #>
154
+ [CmdletBinding()]
155
+ [OutputType([string])]
156
+ param(
157
+ [Parameter(Mandatory)]
158
+ [AllowNull()]
159
+ [AllowEmptyString()]
160
+ [string] $Path,
161
+
162
+ [Parameter(Mandatory)]
163
+ [string] $RepositoryRoot
164
+ )
165
+
166
+ if ([string]::IsNullOrWhiteSpace($Path)) {
167
+ return $null
168
+ }
169
+
170
+ $normalized = $Path.Replace('\', '/')
171
+ $isRooted = $normalized.StartsWith('/') -or ($normalized -match '^[A-Za-z]:/')
172
+ if (-not $isRooted) {
173
+ while ($normalized.StartsWith('./')) {
174
+ $normalized = $normalized.Substring(2)
175
+ }
176
+ if ($normalized -eq '..' -or $normalized.StartsWith('../') -or $normalized.Length -eq 0) {
177
+ return $null
178
+ }
179
+ return $normalized
180
+ }
181
+
182
+ $root = $RepositoryRoot.Replace('\', '/').TrimEnd('/')
183
+ $prefix = $root + '/'
184
+ if (-not $normalized.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
185
+ return $null
186
+ }
187
+
188
+ $relative = $normalized.Substring($prefix.Length)
189
+ if ($relative.Length -eq 0) {
190
+ return $null
191
+ }
192
+ return $relative
193
+ }
194
+
195
+ function Test-FileSizeCoveredPath {
196
+ <#
197
+ .SYNOPSIS
198
+ Report whether a repository-relative path is covered by the file-size policy.
199
+ .DESCRIPTION
200
+ A path is covered when its extension (case-insensitive) is in the allowlist
201
+ (py, ps1, psm1, ts, tsx, js, cjs, mjs, cs, sh, and bats), no directory segment
202
+ is an excluded name, and it is not under the worktrees folder of .claude.
203
+ Excluded directory names are matched exactly. Null or empty input is not covered.
204
+ .PARAMETER RelativePath
205
+ A repository-relative path, for example from ConvertTo-FileSizeRelativePath.
206
+ .OUTPUTS
207
+ System.Boolean.
208
+ #>
209
+ [CmdletBinding()]
210
+ [OutputType([bool])]
211
+ param(
212
+ [Parameter(Mandatory)]
213
+ [AllowNull()]
214
+ [AllowEmptyString()]
215
+ [string] $RelativePath
216
+ )
217
+
218
+ if ([string]::IsNullOrWhiteSpace($RelativePath)) {
219
+ return $false
220
+ }
221
+
222
+ $allowedExtension = @('.py', '.ps1', '.psm1', '.ts', '.tsx', '.js', '.cjs', '.mjs', '.cs', '.sh', '.bats')
223
+ $excludedSegment = @(Get-FileSizeExcludedSegment)
224
+
225
+ $normalized = $RelativePath.Replace('\', '/')
226
+ if ($normalized.StartsWith('.claude/worktrees/')) {
227
+ return $false
228
+ }
229
+
230
+ $segment = @($normalized.Split('/'))
231
+ for ($index = 0; $index -lt ($segment.Count - 1); $index++) {
232
+ if ($excludedSegment -ccontains $segment[$index]) {
233
+ return $false
234
+ }
235
+ }
236
+
237
+ $extension = [System.IO.Path]::GetExtension($segment[-1]).ToLowerInvariant()
238
+ return ($allowedExtension -contains $extension)
239
+ }
240
+
241
+ function Compare-FileSizeBaselineEntry {
242
+ <#
243
+ .SYNOPSIS
244
+ Compare one file's current size with its baseline entry under the strict ratchet.
245
+ .DESCRIPTION
246
+ Rows (S is the current size, B the recorded size):
247
+ S <= 800, no entry -> pass
248
+ S <= 800, entry -> 'stale-entry' (remove the entry)
249
+ S > 800, no entry -> 'new-over-limit'
250
+ S > 800, entry, S > B -> 'grew'
251
+ S > 800, entry, S == B -> pass
252
+ S > 800, entry, S < B -> 'tighten' (tighten the entry to S)
253
+ no file, entry -> 'missing-file' (stale entry for a deleted or renamed file)
254
+ no file, no entry -> pass
255
+ .PARAMETER Path
256
+ The repository-relative path, used in the violation message.
257
+ .PARAMETER CurrentSize
258
+ The current logical line count, or $null when no file exists on disk.
259
+ .PARAMETER BaselineSize
260
+ The recorded size, or $null when the baseline has no entry.
261
+ .OUTPUTS
262
+ $null on pass; otherwise a PSCustomObject with Path, Rule, and Message.
263
+ #>
264
+ [CmdletBinding()]
265
+ [OutputType([pscustomobject])]
266
+ param(
267
+ [Parameter(Mandatory)]
268
+ [string] $Path,
269
+
270
+ [AllowNull()]
271
+ [Nullable[int]] $CurrentSize,
272
+
273
+ [AllowNull()]
274
+ [Nullable[int]] $BaselineSize
275
+ )
276
+
277
+ $hardLimit = (Get-FileSizePolicyLimit).HardLimit
278
+ $rule = $null
279
+ $message = $null
280
+
281
+ if ($null -eq $CurrentSize) {
282
+ if ($null -ne $BaselineSize) {
283
+ $rule = 'missing-file'
284
+ $message = "stale baseline entry: '$Path' has no file on disk (deleted or renamed); remove the entry or move it with its recorded size unchanged."
285
+ }
286
+ } elseif ($CurrentSize -le $hardLimit) {
287
+ if ($null -ne $BaselineSize) {
288
+ $rule = 'stale-entry'
289
+ $message = "stale baseline entry: '$Path' has $CurrentSize lines, at or below the $hardLimit-line hard limit; remove the entry."
290
+ }
291
+ } elseif ($null -eq $BaselineSize) {
292
+ $rule = 'new-over-limit'
293
+ $message = "new file over the hard limit: '$Path' has $CurrentSize lines, above the $hardLimit-line hard limit; split the file."
294
+ } elseif ($CurrentSize -gt $BaselineSize) {
295
+ $rule = 'grew'
296
+ $message = "baselined file grew: '$Path' has $CurrentSize lines, above its recorded $BaselineSize; a baselined file may not grow."
297
+ } elseif ($CurrentSize -lt $BaselineSize) {
298
+ $rule = 'tighten'
299
+ $message = "baselined file shrank: '$Path' has $CurrentSize lines, below its recorded $BaselineSize; tighten the entry to $CurrentSize."
300
+ }
301
+
302
+ if ($null -eq $rule) {
303
+ return $null
304
+ }
305
+ return [pscustomobject]@{ Path = $Path; Rule = $rule; Message = $message }
306
+ }
307
+
308
+ function Resolve-FileSizeEditContent {
309
+ <#
310
+ .SYNOPSIS
311
+ Apply an Edit-tool old_string to new_string replacement to pre-edit content in memory.
312
+ .DESCRIPTION
313
+ Follows the replacement approach of Resolve-EditedCheckpointContent in
314
+ .claude/hooks/enforce-completion-consistency.ps1: CRLF is normalized to LF in
315
+ all three strings, matching is ordinal (no regular expression, so '$' in the
316
+ new text stays literal), one occurrence is replaced unless ReplaceAll is true,
317
+ and a second match after the first is ambiguous. Failure is 'no-old-string'
318
+ (absent or empty old string), 'not-found', or 'ambiguous'.
319
+ .PARAMETER Content
320
+ The pre-edit file content.
321
+ .PARAMETER OldString
322
+ The text to replace.
323
+ .PARAMETER NewString
324
+ The replacement text; null is treated as empty.
325
+ .PARAMETER ReplaceAll
326
+ Replace every occurrence instead of exactly one.
327
+ .OUTPUTS
328
+ A PSCustomObject with Content (the edited text, or $null on failure) and
329
+ Failure (a failure code, or $null on success).
330
+ #>
331
+ [CmdletBinding()]
332
+ [OutputType([pscustomobject])]
333
+ param(
334
+ [Parameter(Mandatory)]
335
+ [AllowNull()]
336
+ [AllowEmptyString()]
337
+ [string] $Content,
338
+
339
+ [Parameter(Mandatory)]
340
+ [AllowNull()]
341
+ [AllowEmptyString()]
342
+ [string] $OldString,
343
+
344
+ [AllowNull()]
345
+ [AllowEmptyString()]
346
+ [string] $NewString = '',
347
+
348
+ [bool] $ReplaceAll = $false
349
+ )
350
+
351
+ if ([string]::IsNullOrEmpty($OldString)) {
352
+ return [pscustomobject]@{ Content = $null; Failure = 'no-old-string' }
353
+ }
354
+
355
+ $text = ([string]$Content).Replace("`r`n", "`n")
356
+ $old = $OldString.Replace("`r`n", "`n")
357
+ $new = ([string]$NewString).Replace("`r`n", "`n")
358
+
359
+ $first = $text.IndexOf($old, [System.StringComparison]::Ordinal)
360
+ if ($first -lt 0) {
361
+ return [pscustomobject]@{ Content = $null; Failure = 'not-found' }
362
+ }
363
+ if ($ReplaceAll) {
364
+ return [pscustomobject]@{ Content = $text.Replace($old, $new); Failure = $null }
365
+ }
366
+
367
+ $second = $text.IndexOf($old, $first + $old.Length, [System.StringComparison]::Ordinal)
368
+ if ($second -ge 0) {
369
+ return [pscustomobject]@{ Content = $null; Failure = 'ambiguous' }
370
+ }
371
+
372
+ $edited = $text.Substring(0, $first) + $new + $text.Substring($first + $old.Length)
373
+ return [pscustomobject]@{ Content = $edited; Failure = $null }
374
+ }
375
+
376
+ function Get-FileSizeHookDecision {
377
+ <#
378
+ .SYNOPSIS
379
+ Decide whether a Write or Edit may leave a file at its resulting size.
380
+ .DESCRIPTION
381
+ Baseline-free pre-edit-size ratchet. A path that is not covered is allowed.
382
+ A covered result of at most 800 lines is allowed. A covered result above 800
383
+ lines is allowed only when the file already existed and the result is not
384
+ larger than its pre-edit size, so an already-large file may shrink or stay
385
+ the same size but may not grow; every other case is denied. The deny reason
386
+ starts with 'FILE_SIZE_LIMIT_BLOCKED:' and names the path, the resulting
387
+ count, the three tiers, and the split requirement.
388
+ .PARAMETER RelativePath
389
+ The repository-relative path; $null or empty (outside the repository) is not covered.
390
+ .PARAMETER ResultLineCount
391
+ The logical line count the file would have after the operation.
392
+ .PARAMETER PreEditLineCount
393
+ The logical line count before the operation, or $null for a new file.
394
+ .OUTPUTS
395
+ A PSCustomObject with Allowed (bool) and Reason ($null when allowed).
396
+ #>
397
+ [CmdletBinding()]
398
+ [OutputType([pscustomobject])]
399
+ param(
400
+ [Parameter(Mandatory)]
401
+ [AllowNull()]
402
+ [AllowEmptyString()]
403
+ [string] $RelativePath,
404
+
405
+ [Parameter(Mandatory)]
406
+ [int] $ResultLineCount,
407
+
408
+ [AllowNull()]
409
+ [Nullable[int]] $PreEditLineCount
410
+ )
411
+
412
+ $limit = Get-FileSizePolicyLimit
413
+ $isAllowed = (-not (Test-FileSizeCoveredPath -RelativePath $RelativePath)) -or
414
+ ($ResultLineCount -le $limit.HardLimit) -or
415
+ (($null -ne $PreEditLineCount) -and ($ResultLineCount -le $PreEditLineCount))
416
+ if ($isAllowed) {
417
+ return [pscustomobject]@{ Allowed = $true; Reason = $null }
418
+ }
419
+
420
+ $growth = ''
421
+ if ($null -ne $PreEditLineCount) {
422
+ $growth = " It has $PreEditLineCount lines now; a file already above the hard limit may shrink or stay the same size but may not grow."
423
+ }
424
+ $reason = "FILE_SIZE_LIMIT_BLOCKED: '$RelativePath' would have $ResultLineCount logical lines, above the " +
425
+ "$($limit.HardLimit)-line hard limit.$growth File-size policy tiers: $($limit.Guidance) lines is guidance " +
426
+ "(look for a logical split), $($limit.Advisory) lines is advisory (split more aggressively; reviewers report " +
427
+ "a non-blocking Warning), and above $($limit.HardLimit) lines a split is enforced regardless of logical " +
428
+ "separability. Split the file into smaller files so each stays at or below $($limit.HardLimit) lines."
429
+ return [pscustomobject]@{ Allowed = $false; Reason = $reason }
430
+ }
431
+
432
+ Export-ModuleMember -Function Get-FileSizePolicyLimit, Get-FileSizeLogicalLineCount, Get-FileSizeTier,
433
+ Get-FileSizeExcludedSegment, ConvertTo-FileSizeRelativePath, Test-FileSizeCoveredPath, Compare-FileSizeBaselineEntry,
434
+ Resolve-FileSizeEditContent, Get-FileSizeHookDecision
@@ -0,0 +1,166 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Feature-folder adoption record validation (issue #992).
4
+
5
+ .DESCRIPTION
6
+ Destination-runtime PowerShell port of `feature_folder_record_errors` in
7
+ `scripts/dev_tools/_orchestrator_state_feature_folder_record.py`. The optional
8
+ `issue_adoption.feature_folder_record` object declares that an adopted feature
9
+ folder already existed and was committed before the finishing run started. A
10
+ valid record lets the adoption waive the `new_active_feature_folder` receipt.
11
+
12
+ Invariants:
13
+ - Rule 10: waiving `new_active_feature_folder` requires an object record.
14
+ - Rules 11a to 11e run whenever the record is an object, waived or not;
15
+ rule 11b runs only when `path` passed rule 11a.
16
+ - Every string comparison is ordinal and case-sensitive; the path pattern is
17
+ applied with -cmatch and the \A...\z anchors.
18
+ - The record is a declaration. The Claude SubagentStop hook performs the git
19
+ probe separately.
20
+
21
+ Every function is pure: it reads no file, starts no process, and never mutates
22
+ its input.
23
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
24
+ #>
25
+
26
+ Set-StrictMode -Version Latest
27
+ $ErrorActionPreference = 'Stop'
28
+
29
+ # Import the shared checkpoint-value primitives, resolved relative to this
30
+ # module's directory so the import travels with the pushed-down pack.
31
+ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'OrchestratorStateCheckpointValue.psm1') -Force -ErrorAction Stop
32
+
33
+ $script:FEATURE_FOLDER_TOOL = 'new_active_feature_folder'
34
+ $script:FEATURE_FOLDER_RECORD_KEY = 'feature_folder_record'
35
+ $script:FEATURE_FOLDER_PATH_PATTERN = '\Adocs/features/active/[A-Za-z0-9][A-Za-z0-9._-]*(/[A-Za-z0-9][A-Za-z0-9._-]*)*\z'
36
+ $script:FEATURE_FOLDER_VERIFIED_VIA = [string[]]@('git_cat_file_head', 'git_ls_tree_head')
37
+
38
+ $script:ERROR_RECORD_NOT_OBJECT = 'Checkpoint issue_adoption.feature_folder_record must be an object when waiving new_active_feature_folder.'
39
+ $script:ERROR_PATH_FORMAT = 'Checkpoint issue_adoption.feature_folder_record.path must be a folder path under docs/features/active/ without dot segments or a trailing slash.'
40
+ $script:ERROR_PATH_EQUAL = 'Checkpoint issue_adoption.feature_folder_record.path must equal the checkpoint feature-folder.'
41
+ $script:ERROR_VERIFIED_VIA = 'Checkpoint issue_adoption.feature_folder_record.verified_via must be one of git_cat_file_head, git_ls_tree_head.'
42
+ $script:ERROR_VERIFIED_AT = 'Checkpoint issue_adoption.feature_folder_record.verified_at must be present.'
43
+ $script:ERROR_EVIDENCE = 'Checkpoint issue_adoption.feature_folder_record.evidence must be a non-empty string.'
44
+
45
+
46
+ function Test-OrchestratorStateFeatureFolderPath {
47
+ <#
48
+ .SYNOPSIS
49
+ Report whether a value is a valid adopted feature-folder path.
50
+ .DESCRIPTION
51
+ True only for a string that fully matches the shared path pattern: a folder
52
+ under docs/features/active/ whose every segment starts with a letter or
53
+ digit, so dot segments, empty segments, a trailing slash, backslashes,
54
+ drive letters, glob characters, and a leading hyphen are rejected.
55
+ .PARAMETER Value
56
+ The deserialized JSON value to inspect. May be $null.
57
+ .OUTPUTS
58
+ System.Boolean
59
+ #>
60
+ [CmdletBinding()]
61
+ [OutputType([bool])]
62
+ param(
63
+ [Parameter(Mandatory = $true)]
64
+ [AllowNull()]
65
+ [AllowEmptyString()]
66
+ [object] $Value
67
+ )
68
+
69
+ return (($Value -is [string]) -and ([string]$Value -cmatch $script:FEATURE_FOLDER_PATH_PATTERN))
70
+ }
71
+
72
+ function Get-FeatureFolderRecordFieldError {
73
+ <#
74
+ .SYNOPSIS
75
+ Validate the fields of an object-shaped record (rules 11a to 11e).
76
+ .PARAMETER Record
77
+ The feature_folder_record object.
78
+ .PARAMETER State
79
+ The parsed checkpoint, read for its feature-folder member.
80
+ .OUTPUTS
81
+ System.String[] - zero or more error strings in rule order.
82
+ #>
83
+ [CmdletBinding()]
84
+ [OutputType([string[]])]
85
+ param(
86
+ [Parameter(Mandatory = $true)]
87
+ [psobject] $Record,
88
+
89
+ [Parameter(Mandatory = $true)]
90
+ [psobject] $State
91
+ )
92
+
93
+ $errors = [System.Collections.Generic.List[string]]::new()
94
+ $path = (Get-CheckpointObjectMember -Owner $Record -Name 'path').Value
95
+ # Rule 11b compares a well-formed path only; a malformed path is reported once by rule 11a.
96
+ if (-not (Test-OrchestratorStateFeatureFolderPath -Value $path)) {
97
+ $errors.Add($script:ERROR_PATH_FORMAT)
98
+ }
99
+ else {
100
+ $checkpointFolder = (Get-CheckpointObjectMember -Owner $State -Name 'feature-folder').Value
101
+ if (-not (($checkpointFolder -is [string]) -and [string]::Equals([string]$checkpointFolder, [string]$path, [System.StringComparison]::Ordinal))) {
102
+ $errors.Add($script:ERROR_PATH_EQUAL)
103
+ }
104
+ }
105
+
106
+ $verifiedVia = (Get-CheckpointObjectMember -Owner $Record -Name 'verified_via').Value
107
+ if (-not (($verifiedVia -is [string]) -and ($script:FEATURE_FOLDER_VERIFIED_VIA -ccontains [string]$verifiedVia))) {
108
+ $errors.Add($script:ERROR_VERIFIED_VIA)
109
+ }
110
+
111
+ # Presence-only, matching the adoption-level verified_at rule; a System.DateTime
112
+ # produced by ConvertFrom-Json passes.
113
+ $verifiedAt = (Get-CheckpointObjectMember -Owner $Record -Name 'verified_at').Value
114
+ if (($null -ceq $verifiedAt) -or (($verifiedAt -is [string]) -and [string]::IsNullOrWhiteSpace([string]$verifiedAt))) {
115
+ $errors.Add($script:ERROR_VERIFIED_AT)
116
+ }
117
+
118
+ $evidence = (Get-CheckpointObjectMember -Owner $Record -Name 'evidence').Value
119
+ if (-not (($evidence -is [string]) -and -not [string]::IsNullOrWhiteSpace([string]$evidence))) {
120
+ $errors.Add($script:ERROR_EVIDENCE)
121
+ }
122
+ return $errors.ToArray()
123
+ }
124
+
125
+ function Get-OrchestratorStateFeatureFolderRecordError {
126
+ <#
127
+ .SYNOPSIS
128
+ Validate issue_adoption.feature_folder_record (rules 10 and 11a-11e).
129
+ .DESCRIPTION
130
+ An object record is always validated (rules 11a to 11e). A missing or
131
+ non-object record yields the rule-10 error only when waived_tools contains
132
+ new_active_feature_folder; otherwise nothing is reported.
133
+ .PARAMETER Adoption
134
+ The issue_adoption object.
135
+ .PARAMETER State
136
+ The parsed checkpoint, read for its feature-folder member.
137
+ .PARAMETER WaivedTool
138
+ The well-formed waived_tools list, in checkpoint order.
139
+ .OUTPUTS
140
+ System.String[] - zero or more error strings in rule order.
141
+ #>
142
+ [CmdletBinding()]
143
+ [OutputType([string[]])]
144
+ param(
145
+ [Parameter(Mandatory = $true)]
146
+ [psobject] $Adoption,
147
+
148
+ [Parameter(Mandatory = $true)]
149
+ [psobject] $State,
150
+
151
+ [Parameter(Mandatory = $true)]
152
+ [AllowEmptyCollection()]
153
+ [string[]] $WaivedTool
154
+ )
155
+
156
+ $record = (Get-CheckpointObjectMember -Owner $Adoption -Name $script:FEATURE_FOLDER_RECORD_KEY).Value
157
+ if (Test-CheckpointObjectValue -Value $record) {
158
+ return [string[]]@(Get-FeatureFolderRecordFieldError -Record $record -State $State)
159
+ }
160
+ if ($WaivedTool -ccontains $script:FEATURE_FOLDER_TOOL) {
161
+ return [string[]]@($script:ERROR_RECORD_NOT_OBJECT)
162
+ }
163
+ return [string[]]@()
164
+ }
165
+
166
+ Export-ModuleMember -Function Test-OrchestratorStateFeatureFolderPath, Get-OrchestratorStateFeatureFolderRecordError