@danmoisan/drm-copilot-mcp 1.1.6 → 1.1.7

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 (18) hide show
  1. package/package.json +1 -1
  2. package/resources/claude-customizations/.claude/agents/atomic-planner.md +2 -0
  3. package/resources/claude-customizations/.claude/agents/prd-feature.md +5 -0
  4. package/resources/claude-customizations/.claude/agents/task-researcher.md +3 -0
  5. package/resources/claude-customizations/.claude/hooks/validate-planner-output.ps1 +122 -0
  6. package/resources/claude-customizations/.claude/hooks/validate-prd-feature-output.ps1 +91 -0
  7. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +69 -1
  8. package/resources/claude-customizations/.claude/lib/requirements/GeneratedDocumentCounters.psm1 +32 -0
  9. package/resources/claude-customizations/.claude/settings.json +9 -0
  10. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +2 -0
  11. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +4 -0
  12. package/resources/claude-customizations/.claude/skills/fill-feature-docs/SKILL.md +3 -0
  13. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +2 -0
  14. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +2 -0
  15. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +2 -0
  16. package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +3 -0
  17. package/resources/claude-customizations/pack-manifests/core.json +2 -0
  18. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danmoisan/drm-copilot-mcp",
3
- "version": "1.1.6",
3
+ "version": "1.1.7",
4
4
  "description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -57,6 +57,8 @@ Generate plans using the atomic plan contract defined in the `atomic-plan-contra
57
57
 
58
58
  ## Preflight Validation
59
59
 
60
+ Before handing the plan to executor preflight, return the exact bounded line record defined in `atomic-plan-contract`, starting with `PLANNER-INTERNAL-REVIEW: PASS` and ending with the required `PREFLIGHT:` signal. The bounded record must include exactly one passing `CITATION-TO-TREE`, `AC-TRACEABILITY`, and `SCOPE-BOUNDARY` declaration; one or more current-tree `CITATION: <repository-relative path> | <locator>` records; one complete unique `AC-INVENTORY:`; one complete `AC-MAPPING: <ID> | IMPLEMENTATION: <identifier> | TESTS: <identifier> | EVIDENCE: <identifier>` for every and only inventory ID; and exactly one `UNRESOLVED-GAPS: NONE`. A missing, duplicate, malformed, failed, blocked, or out-of-bounds record declaration requires `SELF-REVIEW: BLOCKED` and stops handoff.
61
+
60
62
  Return the finalized plan for validation-only preflight through `atomic-executor` and preserve the same target file path across revision loops. Do not claim nested worker delegation from within planner execution.
61
63
 
62
64
  ## Output
@@ -16,6 +16,8 @@ hooks:
16
16
  hooks:
17
17
  - type: command
18
18
  command: pwsh -NoProfile -File .claude/hooks/validate-required-artifact-output.ps1 -AgentName prd-feature -RequiredArtifact 'spec-path|^docs/features/active/.+/spec\.md$|feature spec artifact' -RequiredArtifact 'user-story-path|^docs/features/active/.+/user-story\.md$|feature user story artifact'
19
+ - type: command
20
+ command: pwsh -NoProfile -File .claude/hooks/validate-prd-feature-output.ps1
19
21
  ---
20
22
 
21
23
  # PRD Feature Agent
@@ -27,12 +29,15 @@ Produce feature-document outputs for the active feature folder.
27
29
  - `docs/features/active/<feature>/spec.md`
28
30
  - `docs/features/active/<feature>/user-story.md`
29
31
 
32
+ When an approved `spec.md` acceptance criterion contains a numeric count, enumeration, or population, require the supplied research record to include complete `## Numeric Derivation Evidence`: `Complete Family`, `Exhaustive Search Scope`, `Inclusion Rules`, `Exclusion Rules`, `Primary Search Strategy or Query Expression`, `Primary Member Set`, `Primary Count`, `Cross-check Search Strategy or Query Expression`, `Cross-check Member Set`, `Cross-check Count`, and `Member-set Comparison`. The two derivations must be non-empty, independently constructed, use distinct search strategies or query expressions, independently enumerate member sets, and explicitly compare those sets. The exhaustive scope must cover the complete family, including all relevant overloads and members. Omit the numeric assertion when the record is missing, repeated, incomplete, non-exhaustive, narrow, or disagrees; equal totals, distinct query text, or equal member sets alone are insufficient.
33
+
30
34
  ## Output Reporting
31
35
 
32
36
  Report the final artifact paths as:
33
37
 
34
38
  - `spec-path: docs/features/active/<feature>/spec.md`
35
39
  - `user-story-path: docs/features/active/<feature>/user-story.md`
40
+ - `research-path: docs/features/active/<feature>/research/<timestamp>-<short-name>-research.md` when numeric acceptance criteria are present
36
41
 
37
42
  ## Evidence Location Invariant
38
43
 
@@ -64,6 +64,8 @@ The orchestrator resolves which root to use from whether an active feature folde
64
64
  ### 4. Requirements Mapping
65
65
 
66
66
  - Map acceptance criteria into a concrete design with proposed state model, transitions, and required file changes.
67
+ - Before a numeric count, enumeration, or population can be proposed for an approved `spec.md` acceptance criterion, add a complete `## Numeric Derivation Evidence` section. For each numeric claim, record `Complete Family`, `Exhaustive Search Scope`, `Inclusion Rules`, `Exclusion Rules`, `Primary Search Strategy or Query Expression`, `Primary Member Set`, `Primary Count`, `Cross-check Search Strategy or Query Expression`, `Cross-check Member Set`, `Cross-check Count`, and `Member-set Comparison`.
68
+ - The primary and cross-check records must both be non-empty, independently enumerate their member sets, and name distinct search strategies or query expressions. The exhaustive scope must cover the complete family, including every relevant overload or member; a single grep or query that matches only one named pattern in the declared family is insufficient even if it yields the same count. Explicitly compare the normalized primary and cross-check member sets before proposing the numeric assertion. Withhold the assertion if either record is missing, incomplete, duplicated, non-exhaustive, narrow, or disagrees.
67
69
 
68
70
  ### 5. Testing Implications
69
71
 
@@ -75,6 +77,7 @@ The orchestrator resolves which root to use from whether an active feature folde
75
77
  - Ground all findings in verified evidence.
76
78
  - Keep discussion of non-selected approaches brief.
77
79
  - Do not claim nested worker delegation.
80
+ - Do not present a numeric `spec.md` acceptance criterion when the required numeric derivation evidence is absent, incomplete, or has disagreeing counts.
78
81
 
79
82
  ## Evidence Location Invariant
80
83
 
@@ -110,6 +110,123 @@ function Test-HasPreflightSignal {
110
110
  )
111
111
  }
112
112
 
113
+ function Get-PlannerInternalReviewValidation {
114
+ [CmdletBinding()]
115
+ [OutputType([hashtable])]
116
+ param([Parameter(Mandatory = $true)][string] $AgentOutput)
117
+
118
+ $lines = @($AgentOutput -split "`r?`n")
119
+ $labelPattern = '^\s*(?:PLANNER-INTERNAL-REVIEW|CITATION-TO-TREE|AC-TRACEABILITY|SCOPE-BOUNDARY|CITATION|AC-INVENTORY|AC-MAPPING|UNRESOLVED-GAPS|PREFLIGHT)\s*:'
120
+ $headerPattern = '^\s*PLANNER-INTERNAL-REVIEW\s*:\s*(?<Value>.*?)\s*$'
121
+ $preflightPattern = '^\s*PREFLIGHT\s*:\s*(?:ALL CLEAR|REVISIONS REQUIRED)\s*$'
122
+ $labelIndexes = [System.Collections.Generic.List[int]]::new()
123
+ $headerIndexes = [System.Collections.Generic.List[int]]::new()
124
+ $preflightIndexes = [System.Collections.Generic.List[int]]::new()
125
+
126
+ for ($index = 0; $index -lt $lines.Count; $index++) {
127
+ if ($lines[$index] -match $labelPattern) {
128
+ $labelIndexes.Add($index)
129
+ }
130
+ if ($lines[$index] -match $headerPattern) {
131
+ $headerIndexes.Add($index)
132
+ }
133
+ if ($lines[$index] -match $preflightPattern) {
134
+ $preflightIndexes.Add($index)
135
+ }
136
+ }
137
+
138
+ if ($headerIndexes.Count -ne 1) {
139
+ return @{ Ok = $false; Message = 'planner internal review must contain exactly one `PLANNER-INTERNAL-REVIEW:` declaration.' }
140
+ }
141
+
142
+ $headerIndex = $headerIndexes[0]
143
+ $headerMatch = [regex]::Match($lines[$headerIndex], $headerPattern)
144
+ if ($headerMatch.Groups['Value'].Value.Trim() -ne 'PASS') {
145
+ return @{ Ok = $false; Message = 'planner internal review header must be exactly `PLANNER-INTERNAL-REVIEW: PASS`.' }
146
+ }
147
+
148
+ $preflightAfterHeader = @($preflightIndexes | Where-Object { $_ -gt $headerIndex })
149
+ if ($preflightAfterHeader.Count -ne 1) {
150
+ return @{ Ok = $false; Message = 'planner internal review must terminate at exactly one required `PREFLIGHT:` signal after its header.' }
151
+ }
152
+
153
+ $preflightIndex = $preflightAfterHeader[0]
154
+ foreach ($labelIndex in $labelIndexes) {
155
+ if ($labelIndex -lt $headerIndex -or $labelIndex -gt $preflightIndex) {
156
+ return @{ Ok = $false; Message = 'planner internal review declarations must not occur outside the bounded record.' }
157
+ }
158
+ }
159
+
160
+ $recordLines = @($lines[$headerIndex..$preflightIndex])
161
+ $requiredDeclarations = @('CITATION-TO-TREE', 'AC-TRACEABILITY', 'SCOPE-BOUNDARY')
162
+ foreach ($declaration in $requiredDeclarations) {
163
+ $declarationMatches = @($recordLines | Where-Object { $_ -match "^\s*$declaration\s*:" })
164
+ if ($declarationMatches.Count -ne 1) {
165
+ return @{ Ok = $false; Message = "planner internal review must contain exactly one `$declaration: PASS` declaration." }
166
+ }
167
+ if ($declarationMatches[0] -notmatch "^\s*$declaration\s*:\s*PASS\s*$") {
168
+ return @{ Ok = $false; Message = "planner internal review declaration `$declaration must be exactly PASS." }
169
+ }
170
+ }
171
+
172
+ $citationLines = @($recordLines | Where-Object { $_ -match '^\s*CITATION\s*:' })
173
+ if ($citationLines.Count -eq 0) {
174
+ return @{ Ok = $false; Message = 'planner internal review must contain at least one `CITATION:` record.' }
175
+ }
176
+ foreach ($citationLine in $citationLines) {
177
+ $citation = [regex]::Match($citationLine, '^\s*CITATION\s*:\s*(?<Path>[^|\s]+)\s*\|\s*(?<Locator>.+?\S)\s*$')
178
+ if (-not $citation.Success -or $citation.Groups['Path'].Value -notmatch '^(?![A-Za-z]:)(?!/)(?!\\)(?:\.?[^/\\|\s]+)(?:/[^/\\|\s]+)+$') {
179
+ return @{ Ok = $false; Message = 'planner internal review citations require a repository-relative path and nonblank locator.' }
180
+ }
181
+ }
182
+
183
+ $inventoryLines = @($recordLines | Where-Object { $_ -match '^\s*AC-INVENTORY\s*:' })
184
+ if ($inventoryLines.Count -ne 1) {
185
+ return @{ Ok = $false; Message = 'planner internal review must contain exactly one nonblank `AC-INVENTORY:` declaration.' }
186
+ }
187
+ $inventoryValue = ($inventoryLines[0] -replace '^\s*AC-INVENTORY\s*:\s*', '').Trim()
188
+ if ([string]::IsNullOrWhiteSpace($inventoryValue)) {
189
+ return @{ Ok = $false; Message = 'planner internal review `AC-INVENTORY:` must contain nonblank unique IDs.' }
190
+ }
191
+ $inventoryIds = @($inventoryValue -split ',' | ForEach-Object { $_.Trim() })
192
+ if ((@($inventoryIds | Where-Object { [string]::IsNullOrWhiteSpace($_) }).Count -gt 0) -or
193
+ (@($inventoryIds | Select-Object -Unique).Count -ne $inventoryIds.Count)) {
194
+ return @{ Ok = $false; Message = 'planner internal review `AC-INVENTORY:` must contain nonblank unique IDs.' }
195
+ }
196
+
197
+ $mappingLines = @($recordLines | Where-Object { $_ -match '^\s*AC-MAPPING\s*:' })
198
+ if ($mappingLines.Count -eq 0) {
199
+ return @{ Ok = $false; Message = 'planner internal review must contain one `AC-MAPPING:` record for every inventory ID.' }
200
+ }
201
+ $mappingIds = [System.Collections.Generic.List[string]]::new()
202
+ foreach ($mappingLine in $mappingLines) {
203
+ $mapping = [regex]::Match($mappingLine, '^\s*AC-MAPPING\s*:\s*(?<Id>[^|]*?)\s*\|\s*IMPLEMENTATION\s*:\s*(?<Implementation>[^|]*?)\s*\|\s*TESTS\s*:\s*(?<Tests>[^|]*?)\s*\|\s*EVIDENCE\s*:\s*(?<Evidence>.*?)\s*$')
204
+ if (-not $mapping.Success -or
205
+ [string]::IsNullOrWhiteSpace($mapping.Groups['Id'].Value) -or
206
+ [string]::IsNullOrWhiteSpace($mapping.Groups['Implementation'].Value) -or
207
+ [string]::IsNullOrWhiteSpace($mapping.Groups['Tests'].Value) -or
208
+ [string]::IsNullOrWhiteSpace($mapping.Groups['Evidence'].Value)) {
209
+ return @{ Ok = $false; Message = 'planner internal review `AC-MAPPING:` requires nonblank ID, IMPLEMENTATION, TESTS, and EVIDENCE fields.' }
210
+ }
211
+ $mappingIds.Add($mapping.Groups['Id'].Value.Trim())
212
+ }
213
+ if (@($mappingIds | Select-Object -Unique).Count -ne $mappingIds.Count) {
214
+ return @{ Ok = $false; Message = 'planner internal review `AC-MAPPING:` identifiers must be unique.' }
215
+ }
216
+ if ($inventoryIds.Count -ne $mappingIds.Count -or
217
+ (@($inventoryIds | Where-Object { $_ -notin $mappingIds }).Count -gt 0) -or
218
+ (@($mappingIds | Where-Object { $_ -notin $inventoryIds }).Count -gt 0)) {
219
+ return @{ Ok = $false; Message = 'planner internal review AC inventory and mapping identifiers must match exactly.' }
220
+ }
221
+
222
+ $gapLines = @($recordLines | Where-Object { $_ -match '^\s*UNRESOLVED-GAPS\s*:' })
223
+ if ($gapLines.Count -ne 1 -or $gapLines[0] -notmatch '^\s*UNRESOLVED-GAPS\s*:\s*NONE\s*$') {
224
+ return @{ Ok = $false; Message = 'planner internal review must contain exactly one `UNRESOLVED-GAPS: NONE` declaration.' }
225
+ }
226
+
227
+ return @{ Ok = $true; Message = $null }
228
+ }
229
+
113
230
  function Get-PlanStructureValidationReport {
114
231
  [CmdletBinding()]
115
232
  [OutputType([string[]])]
@@ -272,6 +389,11 @@ function Invoke-PlannerOutputValidation {
272
389
  return @{ Ok = $false; Message = $message }
273
390
  }
274
391
 
392
+ $review = Get-PlannerInternalReviewValidation -AgentOutput $agentOutput
393
+ if (-not $review.Ok) {
394
+ return @{ Ok = $false; Message = "atomic-planner hook: $($review.Message)" }
395
+ }
396
+
275
397
  return @{ Ok = $true; Message = $null }
276
398
  }
277
399
 
@@ -0,0 +1,91 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Validates numeric acceptance criteria produced by the prd-feature worker.
4
+ #>
5
+
6
+ [CmdletBinding()]
7
+ param()
8
+
9
+ Set-StrictMode -Version Latest
10
+ $ErrorActionPreference = 'Stop'
11
+
12
+ function Get-ArtifactPathFromOutput {
13
+ [CmdletBinding()]
14
+ [OutputType([string])]
15
+ param([Parameter(Mandatory = $true)][string] $Output, [Parameter(Mandatory = $true)][string] $Label)
16
+
17
+ $match = [regex]::Match($Output, "(?im)^\s*$([regex]::Escape($Label))\s*:\s*(?<path>\S+)")
18
+ if ($match.Success) { return $match.Groups['path'].Value }
19
+ return $null
20
+ }
21
+
22
+ function Test-NumericDerivationEvidence {
23
+ [CmdletBinding()]
24
+ [OutputType([hashtable])]
25
+ param([Parameter(Mandatory = $true)][AllowEmptyString()][string] $Content)
26
+
27
+ $section = [regex]::Match($Content, '(?ims)^##\s+Numeric\s+Derivation\s+Evidence\s*$.*?(?=^##\s|\z)')
28
+ if (-not $section.Success) { return @{ Ok = $false; Message = 'prd-feature hook: numeric criterion requires ## Numeric Derivation Evidence.' } }
29
+ $requiredLabels = @(
30
+ 'Complete Family', 'Exhaustive Search Scope', 'Inclusion Rules', 'Exclusion Rules',
31
+ 'Primary Search Strategy or Query Expression', 'Primary Member Set', 'Primary Count',
32
+ 'Cross-check Search Strategy or Query Expression', 'Cross-check Member Set', 'Cross-check Count',
33
+ 'Member-set Comparison'
34
+ )
35
+ $values = @{}
36
+ foreach ($label in $requiredLabels) {
37
+ $match = [regex]::Match($section.Value, "(?im)^[\t ]*[-*]?[\t ]*$([regex]::Escape($label))[\t ]*:[\t ]*(?<value>\S(?:.*\S)?)[\t ]*$")
38
+ if (-not $match.Success) { return @{ Ok = $false; Message = "prd-feature hook: numeric derivation evidence is missing $label." } }
39
+ $values[$label] = $match.Groups['value'].Value.Trim()
40
+ }
41
+ if ($values['Exhaustive Search Scope'] -notmatch '(?i)\b(entire|all|complete)\b.*\b(repository|repo|source tree|tree)\b') { return @{ Ok = $false; Message = 'prd-feature hook: numeric derivation evidence does not declare an exhaustive repository search scope.' } }
42
+ $primaryStrategy = $values['Primary Search Strategy or Query Expression']
43
+ $crossCheckStrategy = $values['Cross-check Search Strategy or Query Expression']
44
+ if ($primaryStrategy -match '(?i)\b(single|narrow|named[- ]?pattern)\b' -or $crossCheckStrategy -match '(?i)\b(single|narrow|named[- ]?pattern)\b') { return @{ Ok = $false; Message = 'prd-feature hook: numeric derivation evidence uses a narrow named-pattern search.' } }
45
+ if ([regex]::Replace($primaryStrategy, '\s+', '').ToLowerInvariant() -eq [regex]::Replace($crossCheckStrategy, '\s+', '').ToLowerInvariant()) { return @{ Ok = $false; Message = 'prd-feature hook: numeric derivation cross-check repeats the primary search strategy or query expression.' } }
46
+ $familyMembers = @($values['Complete Family'].Split(',') | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
47
+ foreach ($familyMember in $familyMembers) {
48
+ if ($primaryStrategy -notmatch [regex]::Escape($familyMember) -or $crossCheckStrategy -notmatch [regex]::Escape($familyMember)) { return @{ Ok = $false; Message = "prd-feature hook: numeric derivation search does not cover complete family member '$familyMember'." } }
49
+ }
50
+ if ($values['Primary Count'] -notmatch '^\d+$' -or $values['Cross-check Count'] -notmatch '^\d+$') { return @{ Ok = $false; Message = 'prd-feature hook: numeric derivation counts must be numeric.' } }
51
+ $primaryMembers = @($values['Primary Member Set'].Split(',') | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
52
+ $crossCheckMembers = @($values['Cross-check Member Set'].Split(',') | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
53
+ if ([int]$values['Primary Count'] -ne $primaryMembers.Count -or [int]$values['Cross-check Count'] -ne $crossCheckMembers.Count) { return @{ Ok = $false; Message = 'prd-feature hook: numeric derivation count does not match its independently enumerated member set.' } }
54
+ $normalizedPrimaryMembers = @($primaryMembers | ForEach-Object { $_.ToLowerInvariant() } | Sort-Object -Unique) -join '|'
55
+ $normalizedCrossCheckMembers = @($crossCheckMembers | ForEach-Object { $_.ToLowerInvariant() } | Sort-Object -Unique) -join '|'
56
+ if ($normalizedPrimaryMembers -ne $normalizedCrossCheckMembers) { return @{ Ok = $false; Message = 'prd-feature hook: numeric derivation primary and cross-check member sets disagree.' } }
57
+ if ($values['Member-set Comparison'] -notmatch '(?i)\b(equal|match|identical)\b') { return @{ Ok = $false; Message = 'prd-feature hook: numeric derivation evidence is missing an explicit member-set comparison.' } }
58
+ return @{ Ok = $true; Message = $null }
59
+ }
60
+
61
+ function Test-SpecNumericCriterion {
62
+ [CmdletBinding()]
63
+ [OutputType([bool])]
64
+ param([Parameter(Mandatory = $true)][AllowEmptyString()][string] $SpecContent)
65
+
66
+ $section = [regex]::Match($SpecContent, '(?ims)^##\s+Acceptance\s+Criteria\s*$.*?(?=^##\s|\z)')
67
+ return $section.Success -and [regex]::IsMatch($section.Value, '(?m)^\s*[-*]\s*\[.\].*\b\d+\b')
68
+ }
69
+
70
+ function Invoke-PrdFeatureOutputValidation {
71
+ [CmdletBinding()]
72
+ [OutputType([hashtable])]
73
+ param([string] $RawPayload)
74
+
75
+ if ([string]::IsNullOrWhiteSpace($RawPayload)) { return @{ Ok = $false; Message = 'prd-feature hook: CLAUDE_HOOK_INPUT is empty.' } }
76
+ try { $payload = $RawPayload | ConvertFrom-Json -ErrorAction Stop } catch { return @{ Ok = $false; Message = "prd-feature hook: failed to parse CLAUDE_HOOK_INPUT as JSON: $($_.Exception.Message)" } }
77
+ $output = if ($null -ne $payload.PSObject.Properties['output']) { $payload.output } else { $null }
78
+ if ([string]::IsNullOrWhiteSpace($output)) { return @{ Ok = $false; Message = 'prd-feature hook: agent output is empty.' } }
79
+ $specPath = Get-ArtifactPathFromOutput -Output $output -Label 'spec-path'
80
+ if ([string]::IsNullOrWhiteSpace($specPath) -or -not (Test-Path -LiteralPath $specPath -PathType Leaf)) { return @{ Ok = $false; Message = 'prd-feature hook: spec-path is required and must exist.' } }
81
+ $specContent = Get-Content -LiteralPath $specPath -Raw -ErrorAction Stop
82
+ if (-not (Test-SpecNumericCriterion -SpecContent $specContent)) { return @{ Ok = $true; Message = $null } }
83
+ $researchPath = Get-ArtifactPathFromOutput -Output $output -Label 'research-path'
84
+ if ([string]::IsNullOrWhiteSpace($researchPath) -or -not (Test-Path -LiteralPath $researchPath -PathType Leaf)) { return @{ Ok = $false; Message = 'prd-feature hook: numeric acceptance criterion requires an existing research-path.' } }
85
+ return Test-NumericDerivationEvidence -Content (Get-Content -LiteralPath $researchPath -Raw -ErrorAction Stop)
86
+ }
87
+
88
+ if ($MyInvocation.InvocationName -eq '.') { return }
89
+ $result = Invoke-PrdFeatureOutputValidation -RawPayload $env:CLAUDE_HOOK_INPUT
90
+ if (-not $result.Ok) { Write-Error $result.Message; exit 1 }
91
+ exit 0
@@ -161,6 +161,68 @@ function Test-AutomationFeasibilitySection {
161
161
  return @{ Ok = $true; Message = $null }
162
162
  }
163
163
 
164
+ function Test-NumericDerivationEvidence {
165
+ [CmdletBinding()]
166
+ [OutputType([hashtable])]
167
+ param(
168
+ [Parameter(Mandatory = $true)]
169
+ [AllowEmptyString()]
170
+ [string] $Content
171
+ )
172
+
173
+ $numericClaimPattern = '(?im)^\s*[-*]\s*Numeric\s+spec\.md\s+acceptance\s+criterion:\s*.*\b\d+\b'
174
+ if (-not [regex]::IsMatch($Content, $numericClaimPattern)) {
175
+ return @{ Ok = $true; Message = $null }
176
+ }
177
+
178
+ $section = [regex]::Match($Content, '(?ims)^##\s+Numeric\s+Derivation\s+Evidence\s*$.*?(?=^##\s|\z)')
179
+ if (-not $section.Success) {
180
+ return @{ Ok = $false; Message = 'task-researcher hook: numeric spec.md acceptance criterion is missing ## Numeric Derivation Evidence.' }
181
+ }
182
+
183
+ $requiredLabels = @(
184
+ 'Complete Family', 'Exhaustive Search Scope', 'Inclusion Rules', 'Exclusion Rules',
185
+ 'Primary Search Strategy or Query Expression', 'Primary Member Set', 'Primary Count',
186
+ 'Cross-check Search Strategy or Query Expression', 'Cross-check Member Set', 'Cross-check Count',
187
+ 'Member-set Comparison'
188
+ )
189
+ $values = @{}
190
+ foreach ($label in $requiredLabels) {
191
+ $match = [regex]::Match($section.Value, "(?im)^[\t ]*[-*]?[\t ]*$([regex]::Escape($label))[\t ]*:[\t ]*(?<value>\S(?:.*\S)?)[\t ]*$")
192
+ if (-not $match.Success) { return @{ Ok = $false; Message = "task-researcher hook: numeric derivation evidence is missing $label." } }
193
+ $values[$label] = $match.Groups['value'].Value.Trim()
194
+ }
195
+ if ($values['Exhaustive Search Scope'] -notmatch '(?i)\b(entire|all|complete)\b.*\b(repository|repo|source tree|tree)\b') {
196
+ return @{ Ok = $false; Message = 'task-researcher hook: numeric derivation evidence does not declare an exhaustive repository search scope.' }
197
+ }
198
+ $primaryStrategy = $values['Primary Search Strategy or Query Expression']
199
+ $crossCheckStrategy = $values['Cross-check Search Strategy or Query Expression']
200
+ if ($primaryStrategy -match '(?i)\b(single|narrow|named[- ]?pattern)\b' -or $crossCheckStrategy -match '(?i)\b(single|narrow|named[- ]?pattern)\b') {
201
+ return @{ Ok = $false; Message = 'task-researcher hook: numeric derivation evidence uses a narrow named-pattern search.' }
202
+ }
203
+ if ([regex]::Replace($primaryStrategy, '\s+', '').ToLowerInvariant() -eq [regex]::Replace($crossCheckStrategy, '\s+', '').ToLowerInvariant()) {
204
+ return @{ Ok = $false; Message = 'task-researcher hook: numeric derivation cross-check repeats the primary search strategy or query expression.' }
205
+ }
206
+ $familyMembers = @($values['Complete Family'].Split(',') | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
207
+ foreach ($familyMember in $familyMembers) {
208
+ if ($primaryStrategy -notmatch [regex]::Escape($familyMember) -or $crossCheckStrategy -notmatch [regex]::Escape($familyMember)) {
209
+ return @{ Ok = $false; Message = "task-researcher hook: numeric derivation search does not cover complete family member '$familyMember'." }
210
+ }
211
+ }
212
+ if ($values['Primary Count'] -notmatch '^\d+$' -or $values['Cross-check Count'] -notmatch '^\d+$') { return @{ Ok = $false; Message = 'task-researcher hook: numeric derivation counts must be numeric.' } }
213
+ $primaryMembers = @($values['Primary Member Set'].Split(',') | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
214
+ $crossCheckMembers = @($values['Cross-check Member Set'].Split(',') | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
215
+ if ([int]$values['Primary Count'] -ne $primaryMembers.Count -or [int]$values['Cross-check Count'] -ne $crossCheckMembers.Count) {
216
+ return @{ Ok = $false; Message = 'task-researcher hook: numeric derivation count does not match its independently enumerated member set.' }
217
+ }
218
+ $normalizedPrimaryMembers = @($primaryMembers | ForEach-Object { $_.ToLowerInvariant() } | Sort-Object -Unique) -join '|'
219
+ $normalizedCrossCheckMembers = @($crossCheckMembers | ForEach-Object { $_.ToLowerInvariant() } | Sort-Object -Unique) -join '|'
220
+ if ($normalizedPrimaryMembers -ne $normalizedCrossCheckMembers) { return @{ Ok = $false; Message = 'task-researcher hook: numeric derivation primary and cross-check member sets disagree.' } }
221
+ if ($values['Member-set Comparison'] -notmatch '(?i)\b(equal|match|identical)\b') { return @{ Ok = $false; Message = 'task-researcher hook: numeric derivation evidence is missing an explicit member-set comparison.' } }
222
+
223
+ return @{ Ok = $true; Message = $null }
224
+ }
225
+
164
226
  function Invoke-TaskResearcherOutputValidation {
165
227
  [CmdletBinding()]
166
228
  [OutputType([hashtable])]
@@ -209,6 +271,13 @@ function Invoke-TaskResearcherOutputValidation {
209
271
  return @{ Ok = $false; Message = $feasibilityResult.Message }
210
272
  }
211
273
 
274
+ if (Test-Path -LiteralPath $researchPath -PathType Leaf) {
275
+ $numericEvidenceResult = Test-NumericDerivationEvidence -Content (Get-Content -LiteralPath $researchPath -Raw -ErrorAction Stop)
276
+ if (-not $numericEvidenceResult.Ok) {
277
+ return $numericEvidenceResult
278
+ }
279
+ }
280
+
212
281
  return @{ Ok = $true; Message = $null }
213
282
  }
214
283
 
@@ -223,4 +292,3 @@ if (-not $result.Ok) {
223
292
  }
224
293
 
225
294
  exit 0
226
-
@@ -0,0 +1,32 @@
1
+ Set-StrictMode -Version Latest
2
+
3
+ <#
4
+ .SYNOPSIS
5
+ Counts markdown checkbox items contained by a named heading section.
6
+ #>
7
+ function Get-NamedSectionCheckboxCount {
8
+ [CmdletBinding()]
9
+ [OutputType([int])]
10
+ param(
11
+ [Parameter(Mandatory = $true)][AllowEmptyString()][string] $Document,
12
+ [Parameter(Mandatory = $true)][string] $Heading
13
+ )
14
+
15
+ $headingPattern = '^(?<marks>#{1,6})\s+' + [regex]::Escape($Heading) + '\s*$'
16
+ $inside = $false
17
+ $level = 0
18
+ $count = 0
19
+ foreach ($line in ($Document -split "`r?`n")) {
20
+ if (-not $inside) {
21
+ $match = [regex]::Match($line, $headingPattern)
22
+ if ($match.Success) { $inside = $true; $level = $match.Groups['marks'].Value.Length }
23
+ continue
24
+ }
25
+ $nextHeading = [regex]::Match($line, '^(?<marks>#{1,6})\s+')
26
+ if ($nextHeading.Success -and $nextHeading.Groups['marks'].Value.Length -le $level) { break }
27
+ if ($line -match '^\s*[-*]\s+\[[ xX]\]\s+') { $count++ }
28
+ }
29
+ return $count
30
+ }
31
+
32
+ Export-ModuleMember -Function Get-NamedSectionCheckboxCount
@@ -239,6 +239,15 @@
239
239
  }
240
240
  ]
241
241
  },
242
+ {
243
+ "matcher": "prd-feature",
244
+ "hooks": [
245
+ {
246
+ "type": "command",
247
+ "command": "pwsh -NoProfile -File .claude/hooks/validate-prd-feature-output.ps1"
248
+ }
249
+ ]
250
+ },
242
251
  {
243
252
  "matcher": "pr-author",
244
253
  "hooks": [
@@ -35,6 +35,8 @@ When multiple AC source files exist, track checkboxes in **each** applicable fil
35
35
 
36
36
  ## AC Identification
37
37
 
38
+ For generated-document summaries, call `.claude/lib/requirements/GeneratedDocumentCounters.psm1` and supply `Acceptance Criteria` as the named section. The counter begins after that heading and ends at the next equal-or-shallower heading. Do not use `scripts/dev_tools/plan_progress_report.py`, which counts plan tasks rather than generated requirements criteria.
39
+
38
40
  Acceptance criteria are markdown checkbox items within AC source files.
39
41
 
40
42
  Deterministic heading rule:
@@ -155,6 +155,10 @@ Declaration requirement. Every plan handoff MUST carry exactly one of these two
155
155
 
156
156
  ## Preflight Validation (Planner ↔ Executor)
157
157
 
158
+ ### Planner Internal Review Record
159
+
160
+ Before executor preflight, the planner must emit exactly one bounded record between `PLANNER-INTERNAL-REVIEW: PASS` and the existing `PREFLIGHT:` signal. The record requires exactly one passing declaration each for `CITATION-TO-TREE`, `AC-TRACEABILITY`, and `SCOPE-BOUNDARY`; one or more `CITATION: <repository-relative path> | <nonblank locator>` entries; exactly one `AC-INVENTORY:` declaration containing unique nonblank IDs; one `AC-MAPPING: <ID> | IMPLEMENTATION: <nonblank> | TESTS: <nonblank> | EVIDENCE: <nonblank>` for every and only inventory ID; and exactly one `UNRESOLVED-GAPS: NONE`. Missing, blank, duplicate, non-passing, out-of-bounds, or inventory/mapping-disagreeing declarations block handoff. If review cannot pass, emit `SELF-REVIEW: BLOCKED` and do not hand the plan to preflight. `SELF-REVIEW: RE-DERIVED THIS PASS` remains distinct and does not replace executor clearance.
161
+
158
162
  When validating or handing off plans for execution:
159
163
  - Use the directive line: `DIRECTIVE: PREFLIGHT VALIDATION ONLY`.
160
164
  - Require one of the exact signals:
@@ -12,6 +12,8 @@ This direct-use wrapper delegates feature-document work to the `prd-feature` wor
12
12
  - Feature folder issue and research context
13
13
  - Existing spec and user-story files when present
14
14
 
15
+ For a numeric `spec.md` acceptance criterion, the research context must contain complete `## Numeric Derivation Evidence` with `Complete Family`, `Exhaustive Search Scope`, `Inclusion Rules`, `Exclusion Rules`, `Primary Search Strategy or Query Expression`, `Primary Member Set`, `Primary Count`, `Cross-check Search Strategy or Query Expression`, `Cross-check Member Set`, `Cross-check Count`, and `Member-set Comparison`. The two derivations must be non-empty, independently constructed, distinct in strategy or query expression, exhaustive across the complete family, independently enumerated, and explicitly compared. The worker must withhold a numeric assertion for missing, copied, incomplete, non-exhaustive, narrow, or disagreeing evidence; a single grep, a named-pattern-only query, matching totals, distinct query text, or equal member sets alone does not approve a number.
16
+
15
17
  ## Output Paths
16
18
 
17
19
  - `docs/features/active/<feature>/spec.md`
@@ -20,3 +22,4 @@ This direct-use wrapper delegates feature-document work to the `prd-feature` wor
20
22
  ## Worker Routing
21
23
 
22
24
  - Worker: `prd-feature`
25
+ - Require the worker to report the authoritative `research-path` when a numeric acceptance criterion is written.
@@ -21,6 +21,8 @@ this operation may and may not disturb. Read that section before applying anythi
21
21
 
22
22
  ## Prerequisites
23
23
 
24
+ - Reject a pending or not-started run with guidance to consolidate the initial set through `/parallel-plan`. Admit exactly one item only after execution has started in an open run; closed runs are not eligible.
25
+
24
26
  - A parallel run is in progress and `artifacts/orchestration/parallel-orchestrator-state.json`
25
27
  tracks its `parallel_slug`. This skill does not start a run; use `/parallel-plan` and
26
28
  `/parallel-run` for that.
@@ -32,6 +32,8 @@ Before proceeding, `parallel-planner` must:
32
32
 
33
33
  ## Item Intake
34
34
 
35
+ Initial intake must provide the complete item set in one `/parallel-plan <slug> <item> [<item> ...]` invocation before waves are calculated. `/parallel-add` is not an initial-intake path.
36
+
35
37
  Invocation shape: `/parallel-plan <slug> <item> [<item> ...]`, where each `<item>` is either a
36
38
  GitHub issue number (already-promoted work) or a potential-entry path (unpromoted work). This is
37
39
  the same intake domain as `/parallel-add`, so initial intake here and F6's add operation accept
@@ -106,6 +106,8 @@ After the plan is authored, `atomic-executor` runs preflight under the directive
106
106
 
107
107
  The orchestrator records the preflight outcome in `remediation_loop.cycles[current_cycle].preflight` with `iterations` (counter) and `final_status` (`clear|changes_requested|pending`).
108
108
 
109
+ For a well-scoped item where `preflight.iterations > 1`, record a process-defect investigation that identifies the incomplete planner internal-review dimension. Treat excess rounds as a process signal, not routine iteration.
110
+
109
111
  The exhaustive-pass, defect-enumeration, and delta-self-check rules that govern how `atomic-executor` conducts preflight are defined in the `## Preflight Validation (Planner ↔ Executor)` section of `.claude/skills/atomic-plan-contract/SKILL.md` and are not restated here.
110
112
 
111
113
  Alongside `iterations` and `final_status`, the orchestrator also records in `remediation_loop.cycles[current_cycle].preflight` the convergence line `atomic-executor` returned on that round, which is one of `CONVERGENCE: NO FURTHER ROUNDS EXPECTED` or `CONVERGENCE: FURTHER ROUNDS LIKELY`. This convergence field extends the field set already recorded at `remediation_loop.cycles[current_cycle].preflight` rather than replacing it: `iterations` and `final_status` continue to be recorded exactly as stated above, and the convergence field is written in addition to them.
@@ -55,6 +55,8 @@ Create or update a single research file at one of the two tracked research roots
55
55
 
56
56
  - Map acceptance criteria into a concrete design.
57
57
  - Propose state model, transitions, internal API boundaries, and required file changes.
58
+ - For every numeric count, enumeration, or population proposed for an approved `spec.md` acceptance criterion, add complete `## Numeric Derivation Evidence`. Each record must identify `Complete Family`, `Exhaustive Search Scope`, `Inclusion Rules`, `Exclusion Rules`, `Primary Search Strategy or Query Expression`, `Primary Member Set`, `Primary Count`, `Cross-check Search Strategy or Query Expression`, `Cross-check Member Set`, `Cross-check Count`, and `Member-set Comparison`.
59
+ - Both derivations must be non-empty and independently constructed. They must use distinct search strategies or query expressions, independently enumerate the member sets, and explicitly compare normalized member sets. The scope must cover the entire declared family, including all relevant overloads and members; reject a single grep, a narrow named-pattern search, or a query that covers only one family member even when totals and member sets appear equal. Withhold the numeric assertion if the records are incomplete, duplicated, non-exhaustive, narrow, or disagree.
58
60
 
59
61
  ### 5. Testing Implications
60
62
 
@@ -67,3 +69,4 @@ Create or update a single research file at one of the two tracked research roots
67
69
  - Ground all findings in verified evidence from the codebase and authoritative external sources.
68
70
  - Keep discussion of non-selected approaches brief.
69
71
  - Do not claim or perform nested worker delegation.
72
+ - Omit numeric acceptance-criterion facts when the numeric derivation record is absent, incomplete, or disagrees.
@@ -53,6 +53,7 @@
53
53
  ".claude/hooks/validate-feature-review-coverage.ps1",
54
54
  ".claude/hooks/validate-orchestrator-output.ps1",
55
55
  ".claude/hooks/validate-planner-output.ps1",
56
+ ".claude/hooks/validate-prd-feature-output.ps1",
56
57
  ".claude/hooks/validate-required-artifact-output.ps1",
57
58
  ".claude/hooks/validate-task-researcher-output.ps1",
58
59
  ".claude/rules/architecture-boundaries.md",
@@ -110,6 +111,7 @@
110
111
  ".claude/skills/translate-copilot-to-claude/SKILL.md",
111
112
  ".claude/skills/update-status/SKILL.md",
112
113
  ".claude/lib/hook-payload/HookPayload.psm1",
114
+ ".claude/lib/requirements/GeneratedDocumentCounters.psm1",
113
115
  ".claude/lib/model-routing/ModelRouting.psm1",
114
116
  ".claude/lib/orchestrator-state/OrchestratorState.psm1",
115
117
  ".claude/lib/orchestrator-state/OrchestratorStateCompletion.psm1",
@@ -2,7 +2,7 @@ default_permissions = ":danger-full-access"
2
2
 
3
3
  [mcp_servers.drm-copilot]
4
4
  command = "npx"
5
- args = ["-y", "@danmoisan/drm-copilot-mcp@1.1.6"]
5
+ args = ["-y", "@danmoisan/drm-copilot-mcp@1.1.7"]
6
6
  required = true
7
7
  enabled_tools = [
8
8
  "collect_commit_context",