@danmoisan/drm-copilot-mcp 1.1.22 → 1.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-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-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/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 +1 -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 +7 -0
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +1 -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/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 +2 -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 +1 -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
|
@@ -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
|
|
@@ -29,17 +29,20 @@ $ErrorActionPreference = 'Stop'
|
|
|
29
29
|
# Import the shared checkpoint-value primitives, resolved relative to this
|
|
30
30
|
# module's directory so the import travels with the pushed-down pack.
|
|
31
31
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'OrchestratorStateCheckpointValue.psm1') -Force -ErrorAction Stop
|
|
32
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'OrchestratorStateFeatureFolderRecord.psm1') -Force -ErrorAction Stop
|
|
32
33
|
|
|
33
34
|
$script:ISSUE_ADOPTION_KEY = 'issue_adoption'
|
|
34
35
|
$script:POTENTIAL_TO_ISSUE_TOOL = 'potential_to_issue'
|
|
35
36
|
$script:FEATURE_PROMOTION_ENTRY_TOOL = 'new_potential_entry'
|
|
36
37
|
$script:BUG_PROMOTION_ENTRY_TOOL = 'new_potential_bug_entry'
|
|
38
|
+
$script:FEATURE_FOLDER_TOOL = 'new_active_feature_folder'
|
|
37
39
|
$script:ORIGIN_VALUES = [string[]]@('transferred', 'filed_before_orchestration', 'epic_decomposition')
|
|
38
40
|
$script:VERIFIED_VIA_VALUES = [string[]]@('gh_issue_view', 'gh_api_get', 'github_mcp_issue_read')
|
|
39
41
|
|
|
40
|
-
# The closed set of tools an adoption record may waive: the issue-creation tool
|
|
41
|
-
#
|
|
42
|
-
|
|
42
|
+
# The closed set of tools an adoption record may waive: the issue-creation tool,
|
|
43
|
+
# the two promotion-type-specific promotion-entry tools, and the feature-folder
|
|
44
|
+
# tool, which also requires a valid feature_folder_record (rules 10 and 11).
|
|
45
|
+
$script:WAIVABLE_TOOLS = [string[]]@($script:POTENTIAL_TO_ISSUE_TOOL, $script:FEATURE_PROMOTION_ENTRY_TOOL, $script:BUG_PROMOTION_ENTRY_TOOL, $script:FEATURE_FOLDER_TOOL)
|
|
43
46
|
$script:PROMOTION_ENTRY_TOOLS = [string[]]@($script:FEATURE_PROMOTION_ENTRY_TOOL, $script:BUG_PROMOTION_ENTRY_TOOL)
|
|
44
47
|
$script:POTENTIAL_RECORD_PREFIX = 'docs/features/potential/'
|
|
45
48
|
$script:POTENTIAL_RECORD_SUFFIX = '.md'
|
|
@@ -379,6 +382,8 @@ function Get-OrchestratorStateIssueAdoptionResult {
|
|
|
379
382
|
$recordMember = Get-CheckpointObjectMember -Owner $adoption -Name 'potential_record'
|
|
380
383
|
$origin = (Get-CheckpointObjectMember -Owner $adoption -Name 'origin').Value
|
|
381
384
|
$errors.AddRange([string[]]@(Get-AdoptionPotentialRecordError -WaivedTool $waived.Value -PotentialRecord $recordMember.Value -Origin $origin -RecordPresent $recordMember.Present))
|
|
385
|
+
# Rules 10 and 11a-11e run under the same rule-8 shape gate as rule 9.
|
|
386
|
+
$errors.AddRange([string[]]@(Get-OrchestratorStateFeatureFolderRecordError -Adoption $adoption -State $State -WaivedTool $waived.Value))
|
|
382
387
|
|
|
383
388
|
if ($errors.Count -gt 0) {
|
|
384
389
|
return @{ Errors = $errors.ToArray(); WaivedTools = [string[]]@() }
|