@danmoisan/drm-copilot-mcp 1.1.19 → 1.1.21

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 (170) hide show
  1. package/out/mcp-server.js +172 -11
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +17 -6
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +10 -2
  5. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +5 -0
  6. package/resources/claude-customizations/.claude/agents/orchestrator.md +4 -1
  7. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +4 -0
  8. package/resources/claude-customizations/.claude/agents/task-researcher.md +8 -0
  9. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-command.ps1 +190 -0
  10. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +22 -53
  11. package/resources/claude-customizations/.claude/hooks/hook-docs-only-diff.ps1 +5 -1
  12. package/resources/claude-customizations/.claude/hooks/premortem-section.ps1 +149 -0
  13. package/resources/claude-customizations/.claude/hooks/validate-executor-output.ps1 +210 -84
  14. package/resources/claude-customizations/.claude/hooks/validate-planner-output.ps1 +13 -1
  15. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +24 -5
  16. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-MergeConfirmationParser.ps1 +203 -0
  17. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +10 -10
  18. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +46 -10
  19. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +48 -16
  20. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +7 -3
  21. package/resources/claude-customizations/.claude/skills/csharp-change-budget-router/SKILL.md +2 -2
  22. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +12 -3
  23. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -2
  24. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +134 -0
  25. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +22 -9
  26. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +29 -5
  27. package/resources/claude-customizations/.claude/skills/powershell-change-budget-router/SKILL.md +1 -1
  28. package/resources/claude-customizations/.claude/skills/python-change-budget-router/SKILL.md +1 -1
  29. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +14 -6
  30. package/resources/claude-customizations/.claude/skills/research-issue/SKILL.md +6 -0
  31. package/resources/claude-customizations/config/orchestration-routing.json +7 -7
  32. package/resources/claude-customizations/pack-manifests/core.json +4 -0
  33. package/resources/codex-and-agents-customizations/.agents/skills/README.md +1 -0
  34. package/resources/codex-and-agents-customizations/.agents/skills/atomic-executor/SKILL.md +8 -7
  35. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +39 -4
  36. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +8 -6
  37. package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +6 -6
  38. package/resources/codex-and-agents-customizations/.agents/skills/csharp-change-budget-router/SKILL.md +2 -2
  39. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +15 -8
  40. package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +2 -2
  41. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +134 -0
  42. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +16 -6
  43. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +1 -1
  44. package/resources/codex-and-agents-customizations/.agents/skills/powershell-change-budget-router/SKILL.md +1 -1
  45. package/resources/codex-and-agents-customizations/.agents/skills/python-change-budget-router/SKILL.md +1 -1
  46. package/resources/codex-and-agents-customizations/.agents/skills/remediation-handoff-atomic-planner/SKILL.md +7 -3
  47. package/resources/codex-and-agents-customizations/.agents/skills/research-issue/SKILL.md +6 -0
  48. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +11 -7
  49. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +11 -7
  50. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +11 -7
  51. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +11 -7
  52. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +11 -7
  53. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +11 -7
  54. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +6 -3
  55. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +6 -3
  56. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +6 -3
  57. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +6 -3
  58. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +6 -3
  59. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +6 -3
  60. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planning.toml +1 -1
  61. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c1.toml +1 -1
  62. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c2.toml +1 -1
  63. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3-elevated.toml +1 -1
  64. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c3.toml +1 -1
  65. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward-c4.toml +1 -1
  66. package/resources/codex-and-agents-customizations/.codex/agents/commit-steward.toml +1 -1
  67. package/resources/codex-and-agents-customizations/.codex/agents/csharp-atomic-planning.toml +1 -1
  68. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
  69. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
  70. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
  71. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
  72. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
  73. package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
  74. package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +4 -2
  75. package/resources/codex-and-agents-customizations/.codex/agents/epic-planner.toml +2 -2
  76. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c1.toml +1 -1
  77. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c2.toml +1 -1
  78. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3-elevated.toml +1 -1
  79. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c3.toml +1 -1
  80. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer-c4.toml +1 -1
  81. package/resources/codex-and-agents-customizations/.codex/agents/feature-reviewer.toml +1 -1
  82. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +8 -5
  83. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +8 -5
  84. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +8 -5
  85. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +8 -5
  86. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +8 -5
  87. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +8 -5
  88. package/resources/codex-and-agents-customizations/.codex/agents/powershell-atomic-planning.toml +1 -1
  89. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
  90. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
  91. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
  92. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
  93. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
  94. package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
  95. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c1.toml +1 -1
  96. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c2.toml +1 -1
  97. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c3-elevated.toml +1 -1
  98. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c3.toml +1 -1
  99. package/resources/codex-and-agents-customizations/.codex/agents/pr-author-c4.toml +1 -1
  100. package/resources/codex-and-agents-customizations/.codex/agents/pr-author.toml +1 -1
  101. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c1.toml +1 -1
  102. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c2.toml +1 -1
  103. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c3-elevated.toml +1 -1
  104. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c3.toml +1 -1
  105. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature-c4.toml +1 -1
  106. package/resources/codex-and-agents-customizations/.codex/agents/prd-feature.toml +1 -1
  107. package/resources/codex-and-agents-customizations/.codex/agents/python-atomic-planning.toml +1 -1
  108. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
  109. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
  110. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
  111. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
  112. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
  113. package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
  114. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c1.toml +7 -1
  115. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c2.toml +7 -1
  116. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c3-elevated.toml +7 -1
  117. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c3.toml +7 -1
  118. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher-c4.toml +7 -1
  119. package/resources/codex-and-agents-customizations/.codex/agents/task-researcher.toml +7 -1
  120. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c1.toml +1 -1
  121. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c2.toml +1 -1
  122. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c3-elevated.toml +1 -1
  123. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c3.toml +1 -1
  124. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer-c4.toml +1 -1
  125. package/resources/codex-and-agents-customizations/.codex/agents/typescript-engineer.toml +1 -1
  126. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  127. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate-command.ps1 +190 -0
  128. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +17 -38
  129. package/resources/codex-and-agents-customizations/.codex/hooks/hook-docs-only-diff.ps1 +5 -1
  130. package/resources/codex-and-agents-customizations/.codex/hooks/record-subagent-routing-attestation.ps1 +82 -70
  131. package/resources/codex-and-agents-customizations/.codex/prompts/feature-review-remediate.md +6 -4
  132. package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
  133. package/resources/codex-and-agents-customizations/pack-manifests/core.json +2 -0
  134. package/resources/config/orchestration-routing.json +7 -7
  135. package/resources/customizations/.github/agents/atomic_executor.agent.md +21 -11
  136. package/resources/customizations/.github/agents/atomic_planning.agent.md +8 -3
  137. package/resources/customizations/.github/agents/csharp-atomic-executor.agent.md +13 -9
  138. package/resources/customizations/.github/agents/csharp-atomic-planning.agent.md +2 -2
  139. package/resources/customizations/.github/agents/csharp-orchestrator.agent.md +2 -2
  140. package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +3 -3
  141. package/resources/customizations/.github/agents/orchestrator.agent.md +8 -5
  142. package/resources/customizations/.github/agents/powershell-atomic-executor.agent.md +13 -9
  143. package/resources/customizations/.github/agents/powershell-atomic-planning.agent.md +2 -2
  144. package/resources/customizations/.github/agents/powershell-orchestrator.agent.md +2 -2
  145. package/resources/customizations/.github/agents/powershell-typed-engineer.agent.md +3 -3
  146. package/resources/customizations/.github/agents/python-atomic-executor.agent.md +13 -9
  147. package/resources/customizations/.github/agents/python-atomic-planning.agent.md +2 -2
  148. package/resources/customizations/.github/agents/python-orchestrator.agent.md +2 -2
  149. package/resources/customizations/.github/agents/task-researcher.agent.md +6 -0
  150. package/resources/customizations/.github/prompts/generate-atomic-plan.prompt.md +16 -10
  151. package/resources/customizations/.github/prompts/orchestrate-csharp-work.prompt.md +2 -2
  152. package/resources/customizations/.github/prompts/orchestrate-powershell-work.prompt.md +1 -1
  153. package/resources/customizations/.github/prompts/orchestrate-python-work.prompt.md +1 -1
  154. package/resources/customizations/.github/prompts/orchestrate-work.prompt.md +2 -2
  155. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +39 -4
  156. package/resources/customizations/.github/skills/csharp-change-budget-router/SKILL.md +1 -1
  157. package/resources/customizations/.github/skills/execution-environment/SKILL.md +134 -0
  158. package/resources/customizations/.github/skills/powershell-change-budget-router/SKILL.md +1 -1
  159. package/resources/customizations/.github/skills/remediation-handoff-atomic-planner/SKILL.md +7 -3
  160. package/resources/feature-templates/bug/plan.yyyy-MM-ddTHH-mm.md +10 -0
  161. package/resources/feature-templates/feature/plan.yyyy-MM-ddTHH-mm.md +11 -0
  162. package/resources/feature-templates/refactor/plan.yyyy-MM-ddTHH-mm.md +11 -0
  163. package/resources/lib/codex-routing/CodexDeployment.psm1 +10 -10
  164. package/resources/powershell/PoshQC/PoshQC.CoverageMerge.psm1 +163 -0
  165. package/resources/powershell/PoshQC/PoshQC.Shard.psm1 +296 -0
  166. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +23 -4
  167. package/resources/powershell/PoshQC/PoshQC.psd1 +7 -1
  168. package/resources/powershell/PoshQC/PoshQC.psm1 +17 -5
  169. package/resources/powershell/PoshQC/README.md +13 -1
  170. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +3 -0
@@ -10,9 +10,14 @@
10
10
  - the SubagentStop envelope read from stdin carries a non-empty last_assistant_message,
11
11
  - output advertises `plan-path: <path>` (or supported equivalent forms),
12
12
  - the advertised plan exists on disk,
13
- - preflight-only runs return an exact preflight signal and may not claim
14
- non-canonical blocked text,
15
- - non-preflight runs may not stop in a blocked state,
13
+ - a run is classified as preflight from the delegation prompt in the subagent
14
+ transcript (Test-IsPreflightRun), falling back to the plan file or one output signal,
15
+ - preflight runs return exactly one preflight signal consistent with their
16
+ PREFLIGHT-DEFECT lines, name an existing fix record for any PREFLIGHT-FIX line,
17
+ and may not claim non-canonical blocked text,
18
+ - non-preflight runs may not stop in a blocked state, except a canonical preflight
19
+ block with a plan delta and no checked task (combined proportionate run); only the
20
+ uppercase BLOCKED token counts as a blocked claim, and lowercase prose does not,
16
21
  - non-preflight plans contain checkboxes and all are checked,
17
22
  - non-preflight output includes `AC Status Summary`,
18
23
  - non-preflight output includes command/status reporting,
@@ -43,33 +48,21 @@ $script:SubagentStopReportAndHaltLine = 'This condition is not repairable by the
43
48
  function Get-PlanFileContent {
44
49
  [CmdletBinding()]
45
50
  [OutputType([hashtable])]
46
- param(
47
- [Parameter(Mandatory = $true)]
48
- [string] $Path
49
- )
51
+ param([Parameter(Mandatory = $true)] [string] $Path)
50
52
 
51
53
  if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
52
54
  return @{ Exists = $false; Lines = @() }
53
55
  }
54
56
 
55
- $lines = Get-Content -LiteralPath $Path -ErrorAction Stop
56
- if ($null -eq $lines) {
57
- $lines = @()
58
- }
59
- elseif ($lines -isnot [array]) {
60
- $lines = @($lines)
61
- }
62
-
57
+ # @() yields an array for any line count; the filter drops a $null emitted at the read boundary.
58
+ $lines = @(Get-Content -LiteralPath $Path -ErrorAction Stop | Where-Object { $null -ne $_ })
63
59
  return @{ Exists = $true; Lines = $lines }
64
60
  }
65
61
 
66
62
  function Get-PlanPathFromOutput {
67
63
  [CmdletBinding()]
68
64
  [OutputType([string])]
69
- param(
70
- [Parameter(Mandatory = $true)]
71
- [string] $AgentOutput
72
- )
65
+ param([Parameter(Mandatory = $true)] [string] $AgentOutput)
73
66
 
74
67
  $pattern = 'plan-path\s*[:=]\s*["'']?([^\s"''\)`]+)|\[plan-path\]\(([^)]+)\)'
75
68
  $match = [regex]::Match($AgentOutput, $pattern, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
@@ -89,22 +82,180 @@ function Get-PlanPathFromOutput {
89
82
  function Test-IsPreflightPlan {
90
83
  [CmdletBinding()]
91
84
  [OutputType([bool])]
92
- param(
93
- [Parameter(Mandatory = $true)]
94
- [AllowEmptyString()] [string[]] $Lines
95
- )
85
+ param([Parameter(Mandatory = $true)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $Lines)
96
86
 
97
87
  return @($Lines | Where-Object { $_ -match '(?i)^\s*DIRECTIVE:\s*PREFLIGHT VALIDATION ONLY\s*$' }).Count -gt 0
98
88
  }
99
89
 
100
- function Get-TouchedLanguagesFromPlan {
90
+ function Get-JsonPropertyValue {
91
+ # Returns a named property of a parsed JSON object, or $null; safe under StrictMode.
92
+ [CmdletBinding()]
93
+ [OutputType([object])]
94
+ param([AllowNull()] [object] $Object, [Parameter(Mandatory = $true)] [string] $Name)
95
+
96
+ if ($null -eq $Object -or -not ($Object -is [System.Management.Automation.PSCustomObject])) {
97
+ return $null
98
+ }
99
+ $property = @($Object.PSObject.Properties | Where-Object { $_.Name -ceq $Name })
100
+ if ($property.Count -eq 0) {
101
+ return $null
102
+ }
103
+ return $property[0].Value
104
+ }
105
+
106
+ function Get-TranscriptPathFromEnvelope {
107
+ # Returns agent_transcript_path from the SubagentStop envelope with a leading ~ expanded, or $null.
108
+ [CmdletBinding()]
109
+ [OutputType([string])]
110
+ param([AllowNull()] [object] $Envelope)
111
+
112
+ $path = Get-JsonPropertyValue -Object $Envelope -Name 'agent_transcript_path'
113
+ if (-not ($path -is [string]) -or [string]::IsNullOrWhiteSpace($path)) {
114
+ return $null
115
+ }
116
+ if ($path.StartsWith('~')) {
117
+ $path = $HOME + $path.Substring(1)
118
+ }
119
+ return $path
120
+ }
121
+
122
+ function Get-TranscriptFirstUserText {
123
+ # Returns the text of the first `user` entry of transcript JSONL lines, or $null when none parses.
124
+ [CmdletBinding()]
125
+ [OutputType([string])]
126
+ param([AllowEmptyString()] [string[]] $Lines = @())
127
+
128
+ foreach ($line in $Lines) {
129
+ if ([string]::IsNullOrWhiteSpace($line)) {
130
+ continue
131
+ }
132
+ try {
133
+ $entry = ConvertFrom-Json -InputObject $line -ErrorAction Stop
134
+ }
135
+ catch {
136
+ continue
137
+ }
138
+ if ((Get-JsonPropertyValue -Object $entry -Name 'type') -ne 'user') {
139
+ continue
140
+ }
141
+ $container = Get-JsonPropertyValue -Object $entry -Name 'message'
142
+ if ($null -eq $container) {
143
+ $container = $entry
144
+ }
145
+ $content = Get-JsonPropertyValue -Object $container -Name 'content'
146
+ if ($content -is [string]) {
147
+ return $content
148
+ }
149
+ $textBlocks = @(@($content) | Where-Object { (Get-JsonPropertyValue -Object $_ -Name 'type') -eq 'text' })
150
+ $texts = @($textBlocks | ForEach-Object { [string](Get-JsonPropertyValue -Object $_ -Name 'text') })
151
+ if ($texts.Count -gt 0) {
152
+ return ($texts -join "`n")
153
+ }
154
+ }
155
+ return $null
156
+ }
157
+
158
+ function Read-TranscriptHead {
159
+ # File seam: returns the first 50 lines of the transcript at Path, or $null when no file exists.
101
160
  [CmdletBinding()]
102
161
  [OutputType([string[]])]
162
+ param([Parameter(Mandatory = $true)] [string] $Path)
163
+
164
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
165
+ return $null
166
+ }
167
+ return [string[]]@(Get-Content -LiteralPath $Path -TotalCount 50 -ErrorAction Stop)
168
+ }
169
+
170
+ function Test-IsPreflightRun {
171
+ <#
172
+ .SYNOPSIS
173
+ Classifies an executor run as preflight or execution (issue #964).
174
+ .DESCRIPTION
175
+ A readable transcript is authoritative: its first `user` entry (the delegation prompt) decides, even
176
+ against a stale plan-file directive. Otherwise the plan-file directive, then exactly one output
177
+ `PREFLIGHT:` signal, decide (self-declared). ReadTranscriptHead returns JSONL lines or $null.
178
+ #>
179
+ [CmdletBinding()]
180
+ [OutputType([bool])]
103
181
  param(
104
- [Parameter(Mandatory = $true)]
105
- [AllowEmptyString()] [string[]] $Lines
182
+ [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $AgentOutput,
183
+ [Parameter(Mandatory = $true)] [AllowEmptyCollection()] [AllowEmptyString()] [string[]] $PlanLines,
184
+ [AllowNull()] [AllowEmptyString()] [string] $TranscriptPath,
185
+ [scriptblock] $ReadTranscriptHead = ${function:Read-TranscriptHead}
106
186
  )
107
187
 
188
+ if (-not [string]::IsNullOrWhiteSpace($TranscriptPath)) {
189
+ $head = $null
190
+ try {
191
+ $head = & $ReadTranscriptHead $TranscriptPath
192
+ }
193
+ catch {
194
+ $head = $null
195
+ }
196
+ if ($null -ne $head) {
197
+ $prompt = Get-TranscriptFirstUserText -Lines ([string[]]@($head))
198
+ if ($null -ne $prompt) {
199
+ return [regex]::IsMatch($prompt, '(?im)^\s*DIRECTIVE:\s*PREFLIGHT VALIDATION ONLY\s*$')
200
+ }
201
+ }
202
+ }
203
+ if (Test-IsPreflightPlan -Lines $PlanLines) {
204
+ return $true
205
+ }
206
+ return [regex]::Matches($AgentOutput, '(?m)^\s*PREFLIGHT:\s*(ALL CLEAR|REVISIONS REQUIRED)\s*$').Count -eq 1
207
+ }
208
+
209
+ function Test-PreflightFixRecord {
210
+ # File seam for the preflight-fix-record path.
211
+ [CmdletBinding()]
212
+ [OutputType([bool])]
213
+ param([Parameter(Mandatory = $true)] [string] $Path)
214
+ return [bool](Test-Path -LiteralPath $Path -PathType Leaf)
215
+ }
216
+
217
+ function Get-PreflightRunBlockDecision {
218
+ <#
219
+ .SYNOPSIS
220
+ Applies the single-pass preflight output rules (issue #964); every block is repairable.
221
+ #>
222
+ [CmdletBinding()]
223
+ [OutputType([hashtable])]
224
+ param([Parameter(Mandatory = $true)] [string] $AgentOutput)
225
+
226
+ if (-not (Test-HasCanonicalBlockedText -AgentOutput $AgentOutput)) {
227
+ return @{ Ok = $false; Repairable = $true; Message = 'atomic-executor hook: blocked preflight output must include the exact text `BLOCKED at preflight (before [P0-T1])` plus a concrete plan delta.' }
228
+ }
229
+ if ($AgentOutput -cmatch 'BLOCKED') {
230
+ return @{ Ok = $true; Message = $null }
231
+ }
232
+
233
+ $signals = @([regex]::Matches($AgentOutput, '(?m)^\s*PREFLIGHT:\s*(?<Signal>ALL CLEAR|REVISIONS REQUIRED)\s*$'))
234
+ if ($signals.Count -ne 1) {
235
+ return @{ Ok = $false; Repairable = $true; Message = "atomic-executor hook: preflight output must carry exactly one ``PREFLIGHT: ALL CLEAR`` or ``PREFLIGHT: REVISIONS REQUIRED`` signal line; found $($signals.Count)." }
236
+ }
237
+ $defectCount = [regex]::Matches($AgentOutput, '(?m)^\s*PREFLIGHT-DEFECT:\s*\S').Count
238
+ if ($signals[0].Groups['Signal'].Value -eq 'ALL CLEAR' -and $defectCount -gt 0) {
239
+ return @{ Ok = $false; Repairable = $true; Message = 'atomic-executor hook: `PREFLIGHT: ALL CLEAR` must not carry a `PREFLIGHT-DEFECT:` line; fix trivial defects in place or return `PREFLIGHT: REVISIONS REQUIRED`.' }
240
+ }
241
+ if ($signals[0].Groups['Signal'].Value -eq 'REVISIONS REQUIRED' -and $defectCount -eq 0) {
242
+ return @{ Ok = $false; Repairable = $true; Message = 'atomic-executor hook: `PREFLIGHT: REVISIONS REQUIRED` must list every substantive defect as a `PREFLIGHT-DEFECT:` line.' }
243
+ }
244
+ if ([regex]::IsMatch($AgentOutput, '(?m)^\s*PREFLIGHT-FIX:\s*\S')) {
245
+ $record = [regex]::Match($AgentOutput, '(?im)^\s*preflight-fix-record:\s*`?(?<Path>[^\s`]+)`?\s*$')
246
+ $recordPath = if ($record.Success) { ($record.Groups['Path'].Value -replace '\\', '/') -replace '^\./', '' } else { '' }
247
+ if (-not $recordPath.StartsWith('docs/features/', [System.StringComparison]::OrdinalIgnoreCase) -or -not (Test-PreflightFixRecord -Path $recordPath)) {
248
+ return @{ Ok = $false; Repairable = $true; Message = 'atomic-executor hook: a `PREFLIGHT-FIX:` line requires a `preflight-fix-record: <path>` line naming an existing record under docs/features/.' }
249
+ }
250
+ }
251
+ return @{ Ok = $true; Message = $null }
252
+ }
253
+
254
+ function Get-TouchedLanguagesFromPlan {
255
+ [CmdletBinding()]
256
+ [OutputType([string[]])]
257
+ param([Parameter(Mandatory = $true)] [AllowEmptyString()] [string[]] $Lines)
258
+
108
259
  $languages = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
109
260
  $pathPattern = '(?i)(?:[.\w*?-]+[\\/])+[.\w*?-]+'
110
261
 
@@ -126,13 +277,7 @@ function Get-TouchedLanguagesFromPlan {
126
277
  function Test-OutputHasLanguageStatus {
127
278
  [CmdletBinding()]
128
279
  [OutputType([bool])]
129
- param(
130
- [Parameter(Mandatory = $true)]
131
- [string] $AgentOutput,
132
-
133
- [Parameter(Mandatory = $true)]
134
- [string] $Language
135
- )
280
+ param([Parameter(Mandatory = $true)] [string] $AgentOutput, [Parameter(Mandatory = $true)] [string] $Language)
136
281
 
137
282
  $labelMap = @{
138
283
  'TypeScript' = @('TypeScript', 'typescript')
@@ -149,34 +294,28 @@ function Test-OutputHasLanguageStatus {
149
294
  function Test-HasPreflightSignal {
150
295
  [CmdletBinding()]
151
296
  [OutputType([bool])]
152
- param(
153
- [Parameter(Mandatory = $true)]
154
- [string] $AgentOutput
155
- )
297
+ param([Parameter(Mandatory = $true)] [string] $AgentOutput)
156
298
 
157
- return [regex]::IsMatch(
158
- $AgentOutput,
159
- '(?m)^\s*PREFLIGHT:\s*(ALL CLEAR|REVISIONS REQUIRED)\s*$'
160
- )
299
+ return [regex]::IsMatch($AgentOutput, '(?m)^\s*PREFLIGHT:\s*(ALL CLEAR|REVISIONS REQUIRED)\s*$')
161
300
  }
162
301
 
163
302
  function Test-HasCanonicalBlockedText {
164
303
  [CmdletBinding()]
165
304
  [OutputType([bool])]
166
- param(
167
- [Parameter(Mandatory = $true)]
168
- [string] $AgentOutput
169
- )
305
+ param([Parameter(Mandatory = $true)] [string] $AgentOutput)
170
306
 
171
- if ($AgentOutput -notmatch 'BLOCKED') {
307
+ # Case-sensitive: lowercase prose such as `blocked_reason` is not a BLOCKED claim (issue #964).
308
+ if ($AgentOutput -cnotmatch 'BLOCKED') {
172
309
  return $true
173
310
  }
174
311
 
175
- if ($AgentOutput -notmatch 'BLOCKED at preflight \(before \[P0-T1\]\)') {
312
+ if ($AgentOutput -cnotmatch 'BLOCKED at preflight \(before \[P0-T1\]\)') {
176
313
  return $false
177
314
  }
178
315
 
179
- if ($AgentOutput -notmatch '(?i)plan delta' -and $AgentOutput -notmatch '\[P\d+-T\d+\]') {
316
+ # The canonical phrase itself carries [P0-T1], so it is removed before looking for a plan delta.
317
+ $remainder = $AgentOutput -creplace 'BLOCKED at preflight \(before \[P0-T1\]\)', ''
318
+ if ($remainder -notmatch '(?i)plan delta' -and $remainder -notmatch '\[P\d+-T\d+\]') {
180
319
  return $false
181
320
  }
182
321
 
@@ -190,15 +329,13 @@ function Invoke-ExecutorOutputValidation {
190
329
  #>
191
330
  [CmdletBinding()]
192
331
  [OutputType([hashtable])]
193
- param(
194
- [AllowNull()] [AllowEmptyString()] [string] $InputText
195
- )
332
+ param([AllowNull()] [AllowEmptyString()] [string] $InputText)
196
333
 
197
334
  $envelope = if ($PSBoundParameters.ContainsKey('InputText')) { Read-SubagentStopEnvelope -InputText $InputText } else { Read-SubagentStopEnvelope }
198
335
  if (-not $envelope.IsValid) { $decision = @{ Ok = $false; Repairable = $false; Message = ('{0} payload anomaly - {1}' -f 'atomic-executor hook:', (Get-SubagentStopEnvelopeAnomalyReason -Anomaly $envelope.Anomaly)) } }
199
336
  else {
200
337
  $agentOutput = $envelope.AssistantText
201
- $decision = Get-ExecutorOutputBlockDecision -AgentOutput ([string]$agentOutput)
338
+ $decision = Get-ExecutorOutputBlockDecision -AgentOutput ([string]$agentOutput) -TranscriptPath (Get-TranscriptPathFromEnvelope -Envelope $envelope.Envelope)
202
339
  }
203
340
  if ($decision.Ok) { return @{ Ok = $true; Message = $null } }
204
341
  if (-not $decision.Repairable) {
@@ -216,13 +353,16 @@ function Get-ExecutorOutputBlockDecision {
216
353
  Returns @{ Ok = $true; Message = $null } to allow, or a hashtable with Ok = $false, a
217
354
  Repairable boolean, and Message. Repairable is $false for a non-preflight BLOCKED claim
218
355
  and for a plan with no task checkboxes; every other block is repairable by the subagent.
356
+ TranscriptPath and ReadTranscriptHead feed Test-IsPreflightRun. An execution run that
357
+ stops with the canonical preflight BLOCKED text, a plan delta, and no checked task is a
358
+ preflight block inside a combined proportionate run and is allowed.
219
359
  #>
220
360
  [CmdletBinding()]
221
361
  [OutputType([hashtable])]
222
362
  param(
223
- [Parameter(Mandatory = $true)]
224
- [AllowEmptyString()]
225
- [string] $AgentOutput
363
+ [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $AgentOutput,
364
+ [AllowNull()] [AllowEmptyString()] [string] $TranscriptPath,
365
+ [scriptblock] $ReadTranscriptHead
226
366
  )
227
367
 
228
368
  if ([string]::IsNullOrWhiteSpace($agentOutput)) {
@@ -239,35 +379,29 @@ function Get-ExecutorOutputBlockDecision {
239
379
  return @{ Ok = $false; Repairable = $true; Message = "atomic-executor hook: executor advertised plan-path '$planPath' but no file exists at that location." }
240
380
  }
241
381
 
242
- $isPreflight = Test-IsPreflightPlan -Lines $file.Lines
243
- if ($isPreflight) {
244
- if (-not (Test-HasCanonicalBlockedText -AgentOutput $agentOutput)) {
245
- return @{ Ok = $false; Repairable = $true; Message = 'atomic-executor hook: blocked preflight output must include the exact text `BLOCKED at preflight (before [P0-T1])` plus a concrete plan delta.' }
246
- }
247
- if ($agentOutput -notmatch 'BLOCKED' -and -not (Test-HasPreflightSignal -AgentOutput $agentOutput)) {
382
+ $runArguments = @{ AgentOutput = $agentOutput; PlanLines = [string[]]@($file.Lines); TranscriptPath = $TranscriptPath }
383
+ if ($null -ne $ReadTranscriptHead) { $runArguments['ReadTranscriptHead'] = $ReadTranscriptHead }
384
+ if (Test-IsPreflightRun @runArguments) {
385
+ if ($agentOutput -cnotmatch 'BLOCKED' -and -not (Test-HasPreflightSignal -AgentOutput $agentOutput)) {
248
386
  return @{ Ok = $false; Repairable = $true; Message = 'atomic-executor hook: preflight output must include `PREFLIGHT: ALL CLEAR` or `PREFLIGHT: REVISIONS REQUIRED`.' }
249
387
  }
250
- return @{ Ok = $true; Message = $null }
388
+ return Get-PreflightRunBlockDecision -AgentOutput $agentOutput
251
389
  }
252
390
 
253
- if ($agentOutput -match 'BLOCKED') {
391
+ if ($agentOutput -cmatch 'BLOCKED') {
392
+ $hasCheckedTask = @($file.Lines | Where-Object { $_ -match '^\s*-\s\[[xX]\]\s\[P\d+-T\d+\]' }).Count -gt 0
393
+ if ($agentOutput -cmatch 'BLOCKED at preflight \(before \[P0-T1\]\)' -and (Test-HasCanonicalBlockedText -AgentOutput $agentOutput) -and -not $hasCheckedTask) {
394
+ return @{ Ok = $true; Message = $null }
395
+ }
254
396
  return @{ Ok = $false; Repairable = $false; Message = 'atomic-executor hook: blocking is permitted only during preflight validation (before [P0-T1]).' }
255
397
  }
256
398
 
257
- $checkboxLineCount = 0
399
+ $checkboxLineCount = @($file.Lines | Where-Object { $_ -match '^\s*-\s\[(?:\s|[xX])\]\s' }).Count
258
400
  $uncheckedLineNumbers = [System.Collections.Generic.List[int]]::new()
259
- $uncheckedPattern = '^\s*-\s\[\s\]\s'
260
- $checkedPattern = '^\s*-\s\[[xX]\]\s'
261
-
262
401
  for ($i = 0; $i -lt $file.Lines.Count; $i++) {
263
- $line = $file.Lines[$i]
264
- if ($line -match $uncheckedPattern) {
265
- $checkboxLineCount++
402
+ if ($file.Lines[$i] -match '^\s*-\s\[\s\]\s') {
266
403
  $uncheckedLineNumbers.Add($i + 1)
267
404
  }
268
- elseif ($line -match $checkedPattern) {
269
- $checkboxLineCount++
270
- }
271
405
  }
272
406
 
273
407
  if ($checkboxLineCount -eq 0) {
@@ -281,8 +415,7 @@ function Get-ExecutorOutputBlockDecision {
281
415
  $reported = $uncheckedLineNumbers.GetRange(0, 5)
282
416
  $suffix = ' (showing first 5)'
283
417
  }
284
- $lineList = ($reported -join ', ')
285
- $message = "atomic-executor hook: plan file '$planPath' has $($uncheckedLineNumbers.Count) unchecked task(s) at line(s) ${lineList}${suffix}. All `- [ ]` items must be ticked to `- [x]` before termination."
418
+ $message = "atomic-executor hook: plan file '$planPath' has $($uncheckedLineNumbers.Count) unchecked task(s) at line(s) $($reported -join ', ')${suffix}. All `- [ ]` items must be ticked to `- [x]` before termination."
286
419
  return @{ Ok = $false; Repairable = $true; Message = $message }
287
420
  }
288
421
 
@@ -296,16 +429,9 @@ function Get-ExecutorOutputBlockDecision {
296
429
  return @{ Ok = $false; Repairable = $true; Message = 'atomic-executor hook: completion output must report commands run and pass/fail status results.' }
297
430
  }
298
431
 
299
- $missingLanguageStatuses = [System.Collections.Generic.List[string]]::new()
300
- foreach ($language in Get-TouchedLanguagesFromPlan -Lines $file.Lines) {
301
- if (-not (Test-OutputHasLanguageStatus -AgentOutput $agentOutput -Language $language)) {
302
- $missingLanguageStatuses.Add($language)
303
- }
304
- }
305
-
432
+ $missingLanguageStatuses = @(Get-TouchedLanguagesFromPlan -Lines $file.Lines | Where-Object { -not (Test-OutputHasLanguageStatus -AgentOutput $agentOutput -Language $_) })
306
433
  if ($missingLanguageStatuses.Count -gt 0) {
307
- $missingList = $missingLanguageStatuses -join ', '
308
- return @{ Ok = $false; Repairable = $true; Message = "atomic-executor hook: completion output is missing explicit PASS/FAIL toolchain status lines for: $missingList." }
434
+ return @{ Ok = $false; Repairable = $true; Message = "atomic-executor hook: completion output is missing explicit PASS/FAIL toolchain status lines for: $($missingLanguageStatuses -join ', ')." }
309
435
  }
310
436
 
311
437
  return @{ Ok = $true; Message = $null }
@@ -19,7 +19,9 @@
19
19
  - each task uses `- [ ] [P#-T#]` (or checked equivalent),
20
20
  - task numbering is sequential within each phase,
21
21
  - every task includes at least one explicit path token,
22
- - the final phase contains QA-oriented work.
22
+ - the final phase contains QA-oriented work,
23
+ - the plan carries a complete `## Pre-Mortem` section (Test-PreMortemSection, issue #964),
24
+ - no internal-review `CITATION:` locator is a line reference (issue #964).
23
25
 
24
26
  .NOTES
25
27
  Reads the SubagentStop envelope from stdin through Read-SubagentStopEnvelope
@@ -39,6 +41,7 @@ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot
39
41
 
40
42
  Set-StrictMode -Version Latest
41
43
  try { . (Join-Path $PSScriptRoot 'subagentstop-envelope.ps1') } catch { Add-HookDependencyFailure -Name 'subagentstop-envelope.ps1' -ErrorRecord $_ }
44
+ try { . (Join-Path $PSScriptRoot 'premortem-section.ps1') } catch { Add-HookDependencyFailure -Name 'premortem-section.ps1' -ErrorRecord $_ }
42
45
  $ErrorActionPreference = 'Stop'
43
46
  $script:SubagentStopReportAndHaltLine = 'This condition is not repairable by the subagent. Report it to the operator and halt.'
44
47
 
@@ -186,6 +189,10 @@ function Get-PlannerInternalReviewValidation {
186
189
  if (-not $citation.Success -or $citation.Groups['Path'].Value -notmatch '^(?![A-Za-z]:)(?!/)(?!\\)(?:\.?[^/\\|\s]+)(?:/[^/\\|\s]+)+$') {
187
190
  return @{ Ok = $false; Message = 'planner internal review citations require a repository-relative path and nonblank locator.' }
188
191
  }
192
+ # Issue #964: a line reference goes stale when main moves (consistent with validator rule G11).
193
+ if ($citation.Groups['Locator'].Value.Trim() -match '(?i)^(?:\d+(?:\s*-\s*\d+)?|#?L\d+(?:-L?\d+)?|lines?\s+\d+.*|[^\s:|]+:\d+(?:-\d+)?)$') {
194
+ return @{ Ok = $false; Message = "planner internal review citation locator ``$($citation.Groups['Locator'].Value.Trim())`` is a line reference; cite a symbol, heading, or test name instead." }
195
+ }
189
196
  }
190
197
 
191
198
  $inventoryLines = @($recordLines | Where-Object { $_ -match '^\s*AC-INVENTORY\s*:' })
@@ -421,6 +428,11 @@ function Get-PlannerOutputBlockDecision {
421
428
  return @{ Ok = $false; Repairable = $true; Message = $message }
422
429
  }
423
430
 
431
+ $preMortem = Test-PreMortemSection -Content ($file.Lines -join "`n")
432
+ if (-not $preMortem.Ok) {
433
+ return @{ Ok = $false; Repairable = $true; Message = "atomic-planner hook: plan '$planPath' $($preMortem.Message) See ``## Design-Level Plan Contract (Mandatory)`` in atomic-plan-contract." }
434
+ }
435
+
424
436
  $review = Get-PlannerInternalReviewValidation -AgentOutput $agentOutput
425
437
  if (-not $review.Ok) {
426
438
  return @{ Ok = $false; Repairable = $true; Message = "atomic-planner hook: $($review.Message)" }
@@ -8,7 +8,10 @@
8
8
  under one of the two tracked research roots
9
9
  (docs/features/<feature>/research/ for feature-associated research, or
10
10
  docs/research/ for one-off research), matches the documented filename
11
- convention, and the file exists on disk.
11
+ convention, and the file exists on disk. Feature-associated research under
12
+ docs/features/ must also carry a complete `## Pre-Mortem` section
13
+ (Test-PreMortemSection in premortem-section.ps1, issue #964); one-off
14
+ research under docs/research/ is exempt.
12
15
 
13
16
  .NOTES
14
17
  Reads the SubagentStop envelope JSON from stdin through the shared
@@ -18,8 +21,8 @@
18
21
  exits 2 through the shared envelope helper; an invalid envelope blocks
19
22
  with a payload anomaly reason. A hook dependency that fails to load
20
23
  blocks with exit 2 and the validate-task-researcher-output: prefix.
21
- Filesystem reads go through Test-ResearchFile so tests can mock the
22
- boundary without writing temporary files.
24
+ Filesystem reads go through Test-ResearchFile and Get-ResearchFileContent
25
+ so tests can mock the boundary without writing temporary files.
23
26
  An unhandled error during validation blocks with exit 2 and a reason (issue #928).
24
27
  #>
25
28
 
@@ -27,6 +30,7 @@
27
30
  param()
28
31
  $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
29
32
  try { . (Join-Path $PSScriptRoot 'subagentstop-envelope.ps1') } catch { Add-HookDependencyFailure -Name 'subagentstop-envelope.ps1' -ErrorRecord $_ }
33
+ try { . (Join-Path $PSScriptRoot 'premortem-section.ps1') } catch { Add-HookDependencyFailure -Name 'premortem-section.ps1' -ErrorRecord $_ }
30
34
 
31
35
  Set-StrictMode -Version Latest
32
36
  $ErrorActionPreference = 'Stop'
@@ -42,6 +46,16 @@ function Test-ResearchFile {
42
46
  return [bool](Test-Path -LiteralPath $Path -PathType Leaf)
43
47
  }
44
48
 
49
+ function Get-ResearchFileContent {
50
+ # File seam: returns the research file text, or $null when no file exists at the path.
51
+ [CmdletBinding()]
52
+ [OutputType([string])]
53
+ param([Parameter(Mandatory = $true)] [string] $Path)
54
+
55
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
56
+ return Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
57
+ }
58
+
45
59
  function Get-ResearchPathFromOutput {
46
60
  [CmdletBinding()]
47
61
  [OutputType([string])]
@@ -268,12 +282,17 @@ function Invoke-TaskResearcherOutputValidation {
268
282
  return @{ Ok = $false; Message = $feasibilityResult.Message }
269
283
  }
270
284
 
271
- if (Test-Path -LiteralPath $researchPath -PathType Leaf) {
272
- try { $researchContent = Get-Content -LiteralPath $researchPath -Raw -ErrorAction Stop } catch { return @{ Ok = $false; Message = "task-researcher hook: failed to read research-path '$researchPath': $($_.Exception.Message)" } }
285
+ try { $researchContent = Get-ResearchFileContent -Path $researchPath } catch { return @{ Ok = $false; Message = "task-researcher hook: failed to read research-path '$researchPath': $($_.Exception.Message)" } }
286
+ if ($null -ne $researchContent) {
273
287
  $numericEvidenceResult = Test-NumericDerivationEvidence -Content $researchContent
274
288
  if (-not $numericEvidenceResult.Ok) {
275
289
  return $numericEvidenceResult
276
290
  }
291
+ # Issue #964: feature research carries a Pre-Mortem; one-off research under docs/research/ is exempt.
292
+ if ((($researchPath -replace '\\', '/') -replace '^\./', '').StartsWith('docs/features/', [System.StringComparison]::OrdinalIgnoreCase)) {
293
+ $preMortem = Test-PreMortemSection -Content ([string]$researchContent)
294
+ if (-not $preMortem.Ok) { return @{ Ok = $false; Message = "task-researcher hook: feature research '$researchPath' $($preMortem.Message)" } }
295
+ }
277
296
  }
278
297
 
279
298
  return @{ Ok = $true; Message = $null }