@danmoisan/drm-copilot-mcp 1.1.18 → 1.1.19

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 (206) hide show
  1. package/out/mcp-server.js +989 -662
  2. package/package.json +3 -2
  3. package/resources/claude-customizations/.claude/agents/atomic-executor.md +11 -0
  4. package/resources/claude-customizations/.claude/agents/atomic-planner.md +10 -0
  5. package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +12 -4
  6. package/resources/claude-customizations/.claude/agents/epic-planner.md +5 -4
  7. package/resources/claude-customizations/.claude/agents/feature-review.md +6 -4
  8. package/resources/claude-customizations/.claude/agents/orchestrator.md +5 -4
  9. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +31 -13
  10. package/resources/claude-customizations/.claude/agents/parallel-planner.md +9 -6
  11. package/resources/claude-customizations/.claude/agents/pr-author.md +15 -2
  12. package/resources/claude-customizations/.claude/agents/prd-feature.md +1 -1
  13. package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +7 -1
  14. package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +7 -1
  15. package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
  16. package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +111 -46
  17. package/resources/claude-customizations/.claude/hooks/enforce-completion-helpers.ps1 +105 -0
  18. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +8 -1
  19. package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +7 -1
  20. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-resolution.ps1 +116 -59
  21. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +55 -23
  22. package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +74 -75
  23. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +127 -35
  24. package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +61 -57
  25. package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +7 -1
  26. package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +129 -39
  27. package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +7 -1
  28. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +9 -3
  29. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +39 -51
  30. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
  31. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
  32. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
  33. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +17 -12
  34. package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +9 -3
  35. package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +65 -50
  36. package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +49 -42
  37. package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +54 -72
  38. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +8 -2
  39. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +279 -0
  40. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +129 -75
  41. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.artifact-root.ps1 +233 -0
  42. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.docs-only.ps1 +224 -0
  43. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +69 -24
  44. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +29 -18
  45. package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +10 -4
  46. package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +14 -5
  47. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +8 -2
  48. package/resources/claude-customizations/.claude/hooks/feature-folder-resolution.ps1 +373 -0
  49. package/resources/claude-customizations/.claude/hooks/feature-review-coverage-thresholds.ps1 +85 -0
  50. package/resources/claude-customizations/.claude/hooks/hook-command-heredoc.ps1 +119 -0
  51. package/resources/claude-customizations/.claude/hooks/hook-command-invocation-operands.ps1 +198 -0
  52. package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +254 -255
  53. package/resources/claude-customizations/.claude/hooks/hook-command-payload-powershell.ps1 +201 -0
  54. package/resources/claude-customizations/.claude/hooks/hook-command-payload.ps1 +484 -0
  55. package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +27 -110
  56. package/resources/claude-customizations/.claude/hooks/hook-dependency-guard.ps1 +114 -0
  57. package/resources/claude-customizations/.claude/hooks/hook-docs-only-diff.ps1 +493 -0
  58. package/resources/claude-customizations/.claude/hooks/subagentstop-envelope.ps1 +192 -0
  59. package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +9 -3
  60. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +40 -42
  61. package/resources/claude-customizations/.claude/hooks/validate-executor-output.ps1 +66 -38
  62. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-metrics.ps1 +180 -0
  63. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage-rules.ps1 +195 -0
  64. package/resources/claude-customizations/.claude/hooks/validate-feature-review-coverage.ps1 +141 -395
  65. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-resolution.ps1 +311 -0
  66. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-routing.ps1 +170 -0
  67. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +115 -190
  68. package/resources/claude-customizations/.claude/hooks/validate-planner-output.ps1 +64 -32
  69. package/resources/claude-customizations/.claude/hooks/validate-pr-author-output.ps1 +39 -44
  70. package/resources/claude-customizations/.claude/hooks/validate-prd-feature-output.ps1 +43 -14
  71. package/resources/claude-customizations/.claude/hooks/validate-required-artifact-output.ps1 +66 -36
  72. package/resources/claude-customizations/.claude/hooks/validate-task-researcher-output.ps1 +31 -34
  73. package/resources/claude-customizations/.claude/lib/bash/mutate-parallel-run.sh +191 -0
  74. package/resources/claude-customizations/.claude/lib/bash/parallel-cohorts.sh +13 -3
  75. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-cli.sh +152 -0
  76. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation-ops.sh +193 -0
  77. package/resources/claude-customizations/.claude/lib/bash/parallel-mutation.sh +322 -0
  78. package/resources/claude-customizations/.claude/lib/bash/remove-parallel-item.sh +146 -0
  79. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +23 -33
  80. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +86 -6
  81. package/resources/claude-customizations/.claude/lib/ci-gate/Invoke-CiGateParser.ps1 +91 -19
  82. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateEpicWaveBarrier.psm1 +346 -0
  83. package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +24 -3
  84. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +79 -2
  85. package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +3 -3
  86. package/resources/claude-customizations/.claude/rules/architecture-boundaries.md +5 -5
  87. package/resources/claude-customizations/.claude/rules/csharp.md +2 -2
  88. package/resources/claude-customizations/.claude/rules/general-code-change.md +1 -1
  89. package/resources/claude-customizations/.claude/rules/general-unit-test.md +3 -2
  90. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +37 -4
  91. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +50 -5
  92. package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +47 -8
  93. package/resources/claude-customizations/.claude/rules/quality-tiers.md +11 -7
  94. package/resources/claude-customizations/.claude/rules/shell.md +10 -8
  95. package/resources/claude-customizations/.claude/rules/typescript.md +1 -1
  96. package/resources/claude-customizations/.claude/settings.json +3 -4
  97. package/resources/claude-customizations/.claude/skills/acceptance-criteria-tracking/SKILL.md +1 -1
  98. package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +5 -1
  99. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_detached_lib.sh +1 -1
  100. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_enumerate_lib.sh +2 -0
  101. package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/scripts/cleanup_worktrees_scan_helper.sh +2 -0
  102. package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +33 -6
  103. package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +1 -1
  104. package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +10 -1
  105. package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -1
  106. package/resources/claude-customizations/.claude/skills/feature-review-workflow/SKILL.md +4 -3
  107. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +14 -6
  108. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +52 -14
  109. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +22 -9
  110. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +79 -16
  111. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +63 -7
  112. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +35 -12
  113. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +3 -1
  114. package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +1 -1
  115. package/resources/claude-customizations/.claude/skills/remediation-handoff-atomic-planner/SKILL.md +2 -2
  116. package/resources/claude-customizations/pack-manifests/core.json +23 -0
  117. package/resources/codex-and-agents-customizations/.agents/skills/acceptance-criteria-tracking/SKILL.md +1 -1
  118. package/resources/codex-and-agents-customizations/.agents/skills/architecture-boundaries/SKILL.md +5 -5
  119. package/resources/codex-and-agents-customizations/.agents/skills/atomic-executor/SKILL.md +11 -0
  120. package/resources/codex-and-agents-customizations/.agents/skills/atomic-plan-contract/SKILL.md +4 -0
  121. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +10 -0
  122. package/resources/codex-and-agents-customizations/.agents/skills/csharp/SKILL.md +2 -2
  123. package/resources/codex-and-agents-customizations/.agents/skills/csharp-qa-gate/SKILL.md +2 -2
  124. package/resources/codex-and-agents-customizations/.agents/skills/epic-orchestrate/SKILL.md +8 -0
  125. package/resources/codex-and-agents-customizations/.agents/skills/epic-plan/SKILL.md +17 -1
  126. package/resources/codex-and-agents-customizations/.agents/skills/epic-run/SKILL.md +2 -1
  127. package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +10 -1
  128. package/resources/codex-and-agents-customizations/.agents/skills/feature-review-workflow/SKILL.md +13 -5
  129. package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +1 -1
  130. package/resources/codex-and-agents-customizations/.agents/skills/general-unit-test/SKILL.md +3 -2
  131. package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +15 -6
  132. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +1 -1
  133. package/resources/codex-and-agents-customizations/.agents/skills/quality-tiers/SKILL.md +11 -7
  134. package/resources/codex-and-agents-customizations/.agents/skills/translate-claude-to-codex/SKILL.md +1 -1
  135. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp/SKILL.md +2 -2
  136. package/resources/codex-and-agents-customizations/.agents-variants/csharp-legacy/skills/csharp-qa-gate/SKILL.md +2 -2
  137. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c1.toml +11 -0
  138. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c2.toml +11 -0
  139. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3-elevated.toml +11 -0
  140. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c3.toml +11 -0
  141. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor-c4.toml +11 -0
  142. package/resources/codex-and-agents-customizations/.codex/agents/atomic-executor.toml +11 -0
  143. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c1.toml +10 -0
  144. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c2.toml +10 -0
  145. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3-elevated.toml +10 -0
  146. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c3.toml +10 -0
  147. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner-c4.toml +10 -0
  148. package/resources/codex-and-agents-customizations/.codex/agents/atomic-planner.toml +10 -0
  149. package/resources/codex-and-agents-customizations/.codex/agents/epic-orchestrator.toml +2 -1
  150. package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +6 -6
  151. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c1.toml +2 -2
  152. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c2.toml +2 -2
  153. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3-elevated.toml +2 -2
  154. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c3.toml +2 -2
  155. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator-c4.toml +2 -2
  156. package/resources/codex-and-agents-customizations/.codex/agents/orchestrator.toml +2 -2
  157. package/resources/codex-and-agents-customizations/.codex/codex-web-setup.sh +318 -273
  158. package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
  159. package/resources/codex-and-agents-customizations/.codex/hooks/check-powershell-test-purity.ps1 +7 -1
  160. package/resources/codex-and-agents-customizations/.codex/hooks/check-python-test-purity.ps1 +7 -1
  161. package/resources/codex-and-agents-customizations/.codex/hooks/codex-epic-child-launch-attestation.ps1 +1 -1
  162. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-checkpoint-monotonic.ps1 +7 -1
  163. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +8 -2
  164. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-consistency.ps1 +97 -44
  165. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-completion-helpers.ps1 +105 -0
  166. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-child-worktree-binding.ps1 +7 -1
  167. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +37 -5
  168. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +6 -0
  169. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-root-invocation.ps1 +7 -1
  170. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-wave-barrier.ps1 +7 -1
  171. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +54 -41
  172. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-evidence-locations.ps1 +7 -1
  173. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +35 -14
  174. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +31 -58
  175. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-modes.ps1 +63 -47
  176. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-targets.ps1 +433 -0
  177. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +24 -11
  178. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +8 -2
  179. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +13 -4
  180. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +8 -2
  181. package/resources/codex-and-agents-customizations/.codex/hooks/feature-folder-resolution.ps1 +373 -0
  182. package/resources/codex-and-agents-customizations/.codex/hooks/feature-review-coverage-thresholds.ps1 +85 -0
  183. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-heredoc.ps1 +119 -0
  184. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation-operands.ps1 +198 -0
  185. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +254 -255
  186. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload-powershell.ps1 +201 -0
  187. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-payload.ps1 +484 -0
  188. package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +27 -110
  189. package/resources/codex-and-agents-customizations/.codex/hooks/hook-dependency-guard.ps1 +114 -0
  190. package/resources/codex-and-agents-customizations/.codex/hooks/hook-docs-only-diff.ps1 +493 -0
  191. package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +8 -2
  192. package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +7 -1
  193. package/resources/codex-and-agents-customizations/.codex/hooks/validate-feature-review-coverage.ps1 +24 -8
  194. package/resources/codex-and-agents-customizations/pack-manifests/core.json +9 -0
  195. package/resources/customizations/.github/agents/csharp-typed-engineer.agent.md +2 -2
  196. package/resources/customizations/.github/instructions/csharp-code-change.instructions.md +4 -4
  197. package/resources/customizations/.github/instructions/csharp-unit-test.instructions.md +2 -2
  198. package/resources/customizations/.github/skills/acceptance-criteria-tracking/SKILL.md +1 -1
  199. package/resources/customizations/.github/skills/atomic-plan-contract/SKILL.md +4 -0
  200. package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +10 -1
  201. package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +1 -1
  202. package/resources/powershell/PoshQC/PoshQC.Coverage.psm1 +314 -0
  203. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +25 -28
  204. package/resources/powershell/PoshQC/PoshQC.psm1 +1 -0
  205. package/resources/powershell/PoshQC/README.md +8 -2
  206. package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +2 -319
@@ -10,13 +10,16 @@
10
10
  cap as target resolution grows.
11
11
 
12
12
  The file declares no file-scope parameter block, no requires directive, and no
13
- entrypoint. Apart from its function declarations,
14
- its only file-scope statement is the resolution-module import
15
- that follows this block, which brings in issue #669's path normalisation and
16
- ambiguity reason code. That import is deliberately unguarded, for the same
17
- reason the parent hook's is: a resolution module that cannot be loaded is
18
- itself the target-not-resolvable state, so letting the failure surface fails
19
- the gate closed rather than degrading it to a permissive path.
13
+ entrypoint. Apart from its function declarations, its file-scope statements are
14
+ the resolution-module import that follows this block, which brings in issue
15
+ #669's path normalisation and ambiguity reason code, and the guarded dot-source
16
+ of the pure shared resolver feature-folder-resolution.ps1 (issue #565), which
17
+ owns the single work-mode parser. The module import is deliberately unguarded,
18
+ for the same reason the parent hook's is: a resolution module that cannot be
19
+ loaded is itself the target-not-resolvable state, so letting the failure surface
20
+ fails the gate closed rather than degrading it to a permissive path. A failed
21
+ dot-source of the shared resolver makes Resolve-PrdFeatureWorkMode return $null,
22
+ which the planner gate's deny-on-unknown branch treats as an unreadable marker.
20
23
  .NOTES
21
24
  Compatible with PowerShell 7+. Read-only resolution logic.
22
25
  #>
@@ -24,7 +27,17 @@
24
27
  # Issue #669 owns path normalisation and the ambiguity reason code. The import is
25
28
  # unguarded for the same reason the parent's is: a resolution module that cannot be
26
29
  # loaded is itself the target-not-resolvable state.
27
- Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeResolution.psm1') -Force
30
+ Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeResolution.psm1') -Force -ErrorAction Stop
31
+
32
+ # Shared work-mode parser (issue #565). Guarded so a failed dot-source fails closed
33
+ # through the planner gate's deny-on-unknown branch.
34
+ $script:PrdFeatureFolderResolutionImportFailure = $null
35
+ try {
36
+ . (Join-Path $PSScriptRoot 'feature-folder-resolution.ps1')
37
+ }
38
+ catch {
39
+ $script:PrdFeatureFolderResolutionImportFailure = 'feature-folder-resolution.ps1'
40
+ }
28
41
 
29
42
  function Resolve-PrdFeatureWorkMode {
30
43
  <#
@@ -36,7 +49,10 @@ function Resolve-PrdFeatureWorkMode {
36
49
  Recognizes minor-audit, full-feature, full-bug, and the legacy full
37
50
  marker (normalized to full-feature), mirroring the regex convention
38
51
  used by scripts/dev_tools/prompt_mode_contract.py so both runtimes
39
- agree on what counts as a valid marker line.
52
+ agree on what counts as a valid marker line. Delegates to the single
53
+ shared parser, Resolve-FeatureFolderWorkMode (issue #565), with an empty
54
+ -UnresolvedMode so an unresolved marker still returns $null here; a
55
+ failed dot-source of the shared resolver also returns $null.
40
56
  #>
41
57
  [CmdletBinding()]
42
58
  [OutputType([string])]
@@ -45,20 +61,15 @@ function Resolve-PrdFeatureWorkMode {
45
61
  [string] $IssueContent
46
62
  )
47
63
 
48
- if ([string]::IsNullOrWhiteSpace($IssueContent)) {
64
+ if ($script:PrdFeatureFolderResolutionImportFailure) {
49
65
  return $null
50
66
  }
51
67
 
52
- $match = [regex]::Match($IssueContent, '(?im)^-\s*Work Mode:\s*(minor-audit|full-feature|full-bug|full)\s*$')
53
- if (-not $match.Success) {
68
+ $mode = Resolve-FeatureFolderWorkMode -IssueContent $IssueContent -UnresolvedMode ''
69
+ if (-not $mode) {
54
70
  return $null
55
71
  }
56
-
57
- $rawMode = $match.Groups[1].Value
58
- if ($rawMode -eq 'full') {
59
- return 'full-feature'
60
- }
61
- return $rawMode
72
+ return $mode
62
73
  }
63
74
 
64
75
  function Get-PrdFeatureRequiredFile {
@@ -98,9 +98,10 @@
98
98
  #>
99
99
  [CmdletBinding()]
100
100
  param()
101
+ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
101
102
 
102
103
 
103
- Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
104
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'HookPayload.psm1' -ErrorRecord $_ }
104
105
  # Issue #669 owns worktree location and path normalisation; issue #673 owns the
105
106
  # portable-identity resolution this gate now selects a worktree with. Both imports are
106
107
  # unguarded on purpose: a resolution module that cannot be loaded is itself the
@@ -108,9 +109,9 @@ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -
108
109
  # a permissive path. Both are required rather than one: the absolute-path join the probe
109
110
  # step below calls is exported only by the first, because a module whose exports are pinned
110
111
  # by an explicit list does not re-export a sibling's functions to its own importer.
111
- Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeTargetResolution.psm1') -Force
112
- Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force
113
- . (Join-Path $PSScriptRoot 'enforce-prd-feature-before-planner-helpers.ps1')
112
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeTargetResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeTargetResolution.psm1' -ErrorRecord $_ }
113
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeItemResolution.psm1' -ErrorRecord $_ }
114
+ try { . (Join-Path $PSScriptRoot 'enforce-prd-feature-before-planner-helpers.ps1') } catch { Add-HookDependencyFailure -Name 'enforce-prd-feature-before-planner-helpers.ps1' -ErrorRecord $_ }
114
115
  function Get-PrdFeatureFileExistence {
115
116
  <#
116
117
  .SYNOPSIS
@@ -316,6 +317,8 @@ function Invoke-PrdFeatureBeforePlannerDecision {
316
317
  [AllowNull()]
317
318
  [object] $ResolvedTarget
318
319
  )
320
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'PRD_FEATURE_BLOCKED:'
321
+ if ($null -ne $dependencyDecision) { return $dependencyDecision }
319
322
 
320
323
  $envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
321
324
  if (-not $envelope.IsValid) {
@@ -469,6 +472,9 @@ function Invoke-PrdFeatureBeforePlannerDecision {
469
472
  if ($MyInvocation.InvocationName -eq '.') {
470
473
  return
471
474
  }
475
+ if ($script:HookDependencyGuardLoadFailed) { [Console]::Error.WriteLine('PRD_FEATURE_BLOCKED: hook-dependency-guard.ps1 failed to load; the gate fails closed.'); exit 2 }
476
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'PRD_FEATURE_BLOCKED:'
477
+ if ($null -ne $dependencyDecision) { $dependencyDecision | ConvertTo-Json -Compress -Depth 5 | Write-Output; exit 0 }
472
478
 
473
479
  $decision = Invoke-PrdFeatureBeforePlannerDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
474
480
 
@@ -28,15 +28,16 @@
28
28
  #>
29
29
  [CmdletBinding()]
30
30
  param()
31
+ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
31
32
 
32
33
 
33
- Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
34
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'HookPayload.psm1' -ErrorRecord $_ }
34
35
 
35
36
  # Shared command-line parser (issue #545). Every forbidden-token scan and both `gh`
36
37
  # expressions below run against each segment's clause-ordered scan text, and the
37
38
  # structural matcher reaches the relocating `gh` spellings adjacency cannot.
38
- . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
39
- . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
39
+ try { . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1') } catch { Add-HookDependencyFailure -Name 'hook-command-scanner.ps1' -ErrorRecord $_ }
40
+ try { . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1') } catch { Add-HookDependencyFailure -Name 'hook-command-invocation.ps1' -ErrorRecord $_ }
40
41
 
41
42
  $script:PromotionMcpOnlyBlockedReason = 'PROMOTION_MCP_ONLY_BLOCKED: Direct Bash promotion-script execution is not allowed in agent sessions. Use the drm-copilot MCP promotion tools instead.'
42
43
 
@@ -112,9 +113,12 @@ function Get-PromotionBypassReason {
112
113
  }
113
114
 
114
115
  # `gh issue create` and `gh issue new` are direct bypasses of the MCP
115
- # promotion path. Tolerate any flags after the subcommand.
116
+ # promotion path. Tolerate any flags after the subcommand. The adjacency
117
+ # expression reads the masked text, so a quoted mention is not a match; wrapper
118
+ # payloads and substitution bodies are covered by the structural check below
119
+ # (issue #824).
116
120
  foreach ($segment in $segments) {
117
- if ($segment.ScanText -match '(?i)\bgh\s+issue\s+(?:create|new)\b') {
121
+ if ($segment.MaskedText -match '(?i)\bgh\s+issue\s+(?:create|new)\b') {
118
122
  return (Get-PromotionMcpOnlyGhIssueBlockedReason)
119
123
  }
120
124
  }
@@ -227,6 +231,8 @@ function Invoke-PromotionMcpOnlyDecision {
227
231
  param(
228
232
  [string] $ToolInputRaw
229
233
  )
234
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'PROMOTION_MCP_ONLY_BLOCKED:'
235
+ if ($null -ne $dependencyDecision) { return $dependencyDecision }
230
236
 
231
237
  $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
232
238
  if (-not $payload.IsValid) {
@@ -291,6 +297,9 @@ function Invoke-PromotionMcpOnlyEntryPoint {
291
297
  if ($MyInvocation.InvocationName -eq '.') {
292
298
  return
293
299
  }
300
+ if ($script:HookDependencyGuardLoadFailed) { [Console]::Error.WriteLine('PROMOTION_MCP_ONLY_BLOCKED: hook-dependency-guard.ps1 failed to load; the gate fails closed.'); exit 2 }
301
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'PROMOTION_MCP_ONLY_BLOCKED:'
302
+ if ($null -ne $dependencyDecision) { $dependencyDecision | ConvertTo-Json -Compress -Depth 5 | Write-Output; exit 0 }
294
303
 
295
304
  # The entry point returns its [int] exit code as the last pipeline element and the
296
305
  # decision JSON before it. `exit (<call>)` would capture BOTH into the exit
@@ -57,10 +57,11 @@
57
57
  #>
58
58
  [CmdletBinding()]
59
59
  param()
60
+ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
60
61
 
61
62
 
62
- Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
63
- . (Join-Path $PSScriptRoot 'enforce-batch-budget-route.ps1')
63
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'HookPayload.psm1' -ErrorRecord $_ }
64
+ try { . (Join-Path $PSScriptRoot 'enforce-batch-budget-route.ps1') } catch { Add-HookDependencyFailure -Name 'enforce-batch-budget-route.ps1' -ErrorRecord $_ }
64
65
 
65
66
  function Test-PythonBatchBudgetPathInRoot {
66
67
  <#
@@ -280,6 +281,8 @@ function Invoke-PythonBatchBudgetDecision {
280
281
  [AllowEmptyString()]
281
282
  [string] $ObservedRoute = ''
282
283
  )
284
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'PYTHON_LARGE_PATH_REQUIRED:'
285
+ if ($null -ne $dependencyDecision) { return $dependencyDecision }
283
286
 
284
287
  $normalized = $FilePath -replace '\\', '/'
285
288
  if ($normalized -notmatch '\.py$') {
@@ -477,6 +480,9 @@ function Invoke-PythonBatchBudgetEntryPoint {
477
480
  if ($MyInvocation.InvocationName -eq '.') {
478
481
  return
479
482
  }
483
+ if ($script:HookDependencyGuardLoadFailed) { [Console]::Error.WriteLine('PYTHON_LARGE_PATH_REQUIRED: hook-dependency-guard.ps1 failed to load; the gate fails closed.'); exit 2 }
484
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'PYTHON_LARGE_PATH_REQUIRED:'
485
+ if ($null -ne $dependencyDecision) { $dependencyDecision | ConvertTo-Json -Compress -Depth 5 | Write-Output; exit 0 }
480
486
 
481
487
  # The entry point returns its [int] exit code as the last pipeline element and the
482
488
  # decision JSON before it. `exit (<call>)` would capture BOTH into the exit
@@ -0,0 +1,373 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pure feature-folder target resolution shared by the Claude and Codex enforcement hooks
4
+ (issue #565).
5
+
6
+ .DESCRIPTION
7
+ One resolution rule, implemented once and parameterized per gate. A feature folder is
8
+ identified structurally, as the single path segment immediately below
9
+ docs/features/active/, and never by string length or prompt position.
10
+
11
+ - Find-FeatureFolderCandidate extracts the distinct cited folder basenames (R1).
12
+ - ConvertTo-FeatureFolderBasename normalizes a record value or token (R2).
13
+ - Find-FeatureFolderRecord is the union-index record lookup (issue_num or folder).
14
+ - Select-FeatureFolderTarget applies dependency pruning, the declared-issue-number
15
+ tie-break, and the fallback issue number, and reports Resolved, NoTarget, or
16
+ Ambiguous (R3-R6).
17
+ - Resolve-FeatureFolderWorkMode parses the persisted work-mode marker of issue.md.
18
+ - Get-FeatureFolderPlanPrerequisite maps a work mode to its plan prerequisites.
19
+
20
+ The file is byte-identical in .claude/hooks/ and .codex/hooks/ and is dot-sourced by its
21
+ callers, each of which guards the dot-source and fails closed when it cannot be loaded.
22
+
23
+ .NOTES
24
+ Compatible with PowerShell 7+. PURITY: every function is pure string and object logic.
25
+ The file opens no file, probes no path, reads no environment value, launches no process,
26
+ issues no web request, and loads no module. It declares functions only.
27
+ #>
28
+
29
+ function Find-FeatureFolderCandidate {
30
+ <#
31
+ .SYNOPSIS
32
+ Returns the distinct feature-folder basenames cited in a text, in first-occurrence
33
+ order (R1).
34
+ .DESCRIPTION
35
+ Scans for docs/features/active/ path tokens with any prefix and either separator.
36
+ Each token is normalized to forward slashes and split into segments; empty and '.'
37
+ segments are dropped. A token with fewer than four segments yields no candidate.
38
+ The fourth segment is the folder, with trailing '.', ',', ';', ':', ')', ']', '>', '*', and '_' trimmed.
39
+ Duplicates are removed ordinally, keeping the first occurrence.
40
+ .PARAMETER Text
41
+ The prompt or other text to scan. Null or empty text yields no output.
42
+ .OUTPUTS
43
+ System.String[] (no output when no qualifying token is present)
44
+ #>
45
+ [CmdletBinding()]
46
+ [OutputType([string[]])]
47
+ param(
48
+ [AllowNull()]
49
+ [AllowEmptyString()]
50
+ [string] $Text
51
+ )
52
+
53
+ if ([string]::IsNullOrEmpty($Text)) {
54
+ return
55
+ }
56
+
57
+ $found = [System.Collections.Generic.List[string]]::new()
58
+ foreach ($match in [regex]::Matches($Text, 'docs[\\/]+features[\\/]+active[\\/]+[^\s"''`]+')) {
59
+ $segments = @(($match.Value -replace '\\', '/') -split '/' | Where-Object { $_ -and $_ -ne '.' })
60
+ if ($segments.Count -lt 4) {
61
+ continue
62
+ }
63
+ $folder = $segments[3].TrimEnd('.', ',', ';', ':', ')', ']', '>', '*', '_')
64
+ if (-not $folder -or $found.Contains($folder)) {
65
+ continue
66
+ }
67
+ $found.Add($folder)
68
+ }
69
+ return $found.ToArray()
70
+ }
71
+
72
+ function ConvertTo-FeatureFolderBasename {
73
+ <#
74
+ .SYNOPSIS
75
+ Normalizes a feature-folder record value or token to its basename (R2).
76
+ .DESCRIPTION
77
+ Separators are normalized to forward slashes, surrounding whitespace and trailing
78
+ slashes are trimmed, and one leading docs/features/<lifecycle>/, active/, or
79
+ completed/ prefix is stripped. The last remaining segment is returned.
80
+ .PARAMETER Value
81
+ The record value or token to normalize.
82
+ .OUTPUTS
83
+ System.String, or $null when no folder segment remains.
84
+ #>
85
+ [CmdletBinding()]
86
+ [OutputType([string])]
87
+ param(
88
+ [AllowNull()]
89
+ [AllowEmptyString()]
90
+ [string] $Value
91
+ )
92
+
93
+ if ([string]::IsNullOrWhiteSpace($Value)) {
94
+ return $null
95
+ }
96
+ $normalized = ($Value -replace '\\', '/').Trim().TrimEnd('/')
97
+ $normalized = $normalized -replace '^(docs/features/[^/]+(/|$)|active/|completed/)', ''
98
+ $segment = ($normalized -split '/')[-1]
99
+ if (-not $segment -or $segment -eq '.') {
100
+ return $null
101
+ }
102
+ return $segment
103
+ }
104
+
105
+ function Find-FeatureFolderRecord {
106
+ <#
107
+ .SYNOPSIS
108
+ Finds the single record a reference identifies, through a union index of issue_num
109
+ and normalized feature_folder.
110
+ .DESCRIPTION
111
+ A non-string reference, or a string of digits only, matches records whose issue_num
112
+ string form equals it. Any other string matches records whose normalized
113
+ feature_folder basename ordinally equals the reference's normalized basename.
114
+ .PARAMETER Records
115
+ The checkpoint records (features[] or items[]).
116
+ .PARAMETER Reference
117
+ An issue number, a folder basename, or a folder path.
118
+ .OUTPUTS
119
+ The matching record, or $null when zero or several records match.
120
+ #>
121
+ [CmdletBinding()]
122
+ [OutputType([object])]
123
+ param(
124
+ [AllowNull()]
125
+ [AllowEmptyCollection()]
126
+ [object[]] $Records,
127
+
128
+ [AllowNull()]
129
+ [object] $Reference
130
+ )
131
+
132
+ if ($null -eq $Records -or $null -eq $Reference) {
133
+ return $null
134
+ }
135
+
136
+ $byIssue = (-not ($Reference -is [string])) -or ($Reference -match '^\d+$')
137
+ $key = if ($byIssue) { [string]$Reference } else { ConvertTo-FeatureFolderBasename -Value $Reference }
138
+ if (-not $key) {
139
+ return $null
140
+ }
141
+ $field = if ($byIssue) { 'issue_num' } else { 'feature_folder' }
142
+
143
+ $matched = @(foreach ($record in $Records) {
144
+ if ($null -eq $record) {
145
+ continue
146
+ }
147
+ $property = $record.PSObject.Properties[$field]
148
+ if ($null -eq $property -or $null -eq $property.Value) {
149
+ continue
150
+ }
151
+ $value = if ($byIssue) { [string]$property.Value } else { ConvertTo-FeatureFolderBasename -Value ([string]$property.Value) }
152
+ if ($value -ceq $key) {
153
+ $record
154
+ }
155
+ })
156
+ if ($matched.Count -ne 1) {
157
+ return $null
158
+ }
159
+ return $matched[0]
160
+ }
161
+
162
+ function Select-FeatureFolderTarget {
163
+ <#
164
+ .SYNOPSIS
165
+ Selects the target feature folder from the cited candidates and the run records
166
+ (R2-R6).
167
+ .DESCRIPTION
168
+ Each candidate maps to at most one record. Unmatched candidates stay in the set.
169
+ With -DependencyAware, a matched candidate is removed when its record lies in the
170
+ transitive depends_on closure of another cited candidate's record and that record
171
+ does not lie in its own closure, so a cycle removes neither. One remaining candidate
172
+ is Resolved. Several remaining candidates are Resolved only when
173
+ -DeclaredIssueNumber names exactly one remaining record. Zero candidates resolve
174
+ through -FallbackIssueNumber when it names a record. Otherwise the result is
175
+ NoTarget for zero and Ambiguous for several. Neither string length nor candidate
176
+ position affects the status.
177
+ .PARAMETER Records
178
+ The checkpoint records (features[] or items[]).
179
+ .PARAMETER Candidate
180
+ The cited folder basenames, in first-occurrence order.
181
+ .PARAMETER DeclaredIssueNumber
182
+ Optional tie-break from the canonical issue-number line.
183
+ .PARAMETER FallbackIssueNumber
184
+ Optional issue number used only when no candidate is cited.
185
+ .PARAMETER DependencyAware
186
+ Removes cited candidates that are dependencies of another cited candidate.
187
+ .OUTPUTS
188
+ System.Management.Automation.PSCustomObject with Status, Record, Basename,
189
+ Remaining, and Detail.
190
+ #>
191
+ [CmdletBinding()]
192
+ [OutputType([pscustomobject])]
193
+ param(
194
+ [AllowNull()]
195
+ [AllowEmptyCollection()]
196
+ [object[]] $Records,
197
+
198
+ [AllowNull()]
199
+ [AllowEmptyCollection()]
200
+ [string[]] $Candidate,
201
+
202
+ [int] $DeclaredIssueNumber,
203
+
204
+ [int] $FallbackIssueNumber,
205
+
206
+ [switch] $DependencyAware
207
+ )
208
+
209
+ $recordList = @($Records | Where-Object { $null -ne $_ })
210
+ $entries = @(foreach ($name in @($Candidate | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })) {
211
+ [pscustomobject]@{ Basename = $name; Record = Find-FeatureFolderRecord -Records $recordList -Reference $name }
212
+ })
213
+
214
+ if ($DependencyAware -and $entries.Count -gt 1) {
215
+ # Transitive depends_on closure of each matched candidate's record.
216
+ $closures = @(foreach ($entry in $entries) {
217
+ $closure = [System.Collections.Generic.List[object]]::new()
218
+ $pending = [System.Collections.Generic.Stack[object]]::new()
219
+ if ($null -ne $entry.Record) {
220
+ $pending.Push($entry.Record)
221
+ }
222
+ while ($pending.Count -gt 0) {
223
+ $dependsOn = $pending.Pop().PSObject.Properties['depends_on']
224
+ if ($null -eq $dependsOn) {
225
+ continue
226
+ }
227
+ foreach ($reference in @($dependsOn.Value)) {
228
+ $dependency = Find-FeatureFolderRecord -Records $recordList -Reference $reference
229
+ if ($null -ne $dependency -and -not $closure.Contains($dependency)) {
230
+ $closure.Add($dependency)
231
+ $pending.Push($dependency)
232
+ }
233
+ }
234
+ }
235
+ , $closure
236
+ })
237
+
238
+ $kept = for ($i = 0; $i -lt $entries.Count; $i++) {
239
+ $pruned = $false
240
+ for ($j = 0; $j -lt $entries.Count -and $null -ne $entries[$i].Record; $j++) {
241
+ if ($i -eq $j -or $null -eq $entries[$j].Record) {
242
+ continue
243
+ }
244
+ if ($closures[$j].Contains($entries[$i].Record) -and -not $closures[$i].Contains($entries[$j].Record)) {
245
+ $pruned = $true
246
+ break
247
+ }
248
+ }
249
+ if (-not $pruned) {
250
+ $entries[$i]
251
+ }
252
+ }
253
+ $entries = @($kept)
254
+ }
255
+
256
+ $remaining = [string[]]@($entries | ForEach-Object { $_.Basename })
257
+ $selected = $null
258
+ if ($entries.Count -eq 1) {
259
+ $selected = $entries[0]
260
+ }
261
+ elseif ($entries.Count -gt 1 -and $PSBoundParameters.ContainsKey('DeclaredIssueNumber')) {
262
+ $declared = [string]$DeclaredIssueNumber
263
+ $byIssue = @($entries | Where-Object {
264
+ $null -ne $_.Record -and $null -ne $_.Record.PSObject.Properties['issue_num'] -and
265
+ ([string]$_.Record.issue_num) -ceq $declared
266
+ })
267
+ if ($byIssue.Count -eq 1) {
268
+ $selected = $byIssue[0]
269
+ }
270
+ }
271
+ elseif ($entries.Count -eq 0 -and $PSBoundParameters.ContainsKey('FallbackIssueNumber')) {
272
+ $fallback = Find-FeatureFolderRecord -Records $recordList -Reference $FallbackIssueNumber
273
+ if ($null -ne $fallback) {
274
+ $folder = $fallback.PSObject.Properties['feature_folder']
275
+ $basename = if ($null -ne $folder) { ConvertTo-FeatureFolderBasename -Value ([string]$folder.Value) } else { $null }
276
+ $selected = [pscustomobject]@{ Basename = $basename; Record = $fallback }
277
+ }
278
+ }
279
+
280
+ if ($null -ne $selected) {
281
+ return [pscustomobject]@{
282
+ Status = 'Resolved'
283
+ Record = $selected.Record
284
+ Basename = $selected.Basename
285
+ Remaining = $remaining
286
+ Detail = "resolved target feature folder '$($selected.Basename)'"
287
+ }
288
+ }
289
+ if ($entries.Count -eq 0) {
290
+ return [pscustomobject]@{
291
+ Status = 'NoTarget'
292
+ Record = $null
293
+ Basename = $null
294
+ Remaining = $remaining
295
+ Detail = 'no target feature folder is cited'
296
+ }
297
+ }
298
+ return [pscustomobject]@{
299
+ Status = 'Ambiguous'
300
+ Record = $null
301
+ Basename = $null
302
+ Remaining = $remaining
303
+ Detail = 'ambiguous target feature folder; remaining candidates: ' + ($remaining -join ', ')
304
+ }
305
+ }
306
+
307
+ function Resolve-FeatureFolderWorkMode {
308
+ <#
309
+ .SYNOPSIS
310
+ Parses the persisted '- Work Mode:' marker out of issue.md content.
311
+ .DESCRIPTION
312
+ Returns minor-audit, full-bug, or full-feature. The legacy full marker normalizes to
313
+ full-feature. Missing, empty, malformed, or unrecognized content returns
314
+ -UnresolvedMode.
315
+ .PARAMETER IssueContent
316
+ The raw issue.md text, or $null when it could not be read.
317
+ .PARAMETER UnresolvedMode
318
+ The value returned when no recognized marker is present. Defaults to full-feature.
319
+ .OUTPUTS
320
+ System.String
321
+ #>
322
+ [CmdletBinding()]
323
+ [OutputType([string])]
324
+ param(
325
+ [AllowNull()]
326
+ [AllowEmptyString()]
327
+ [string] $IssueContent,
328
+
329
+ [AllowNull()]
330
+ [AllowEmptyString()]
331
+ [string] $UnresolvedMode = 'full-feature'
332
+ )
333
+
334
+ if ([string]::IsNullOrWhiteSpace($IssueContent)) {
335
+ return $UnresolvedMode
336
+ }
337
+ $match = [regex]::Match($IssueContent, '(?im)^-\s*Work Mode:\s*(minor-audit|full-feature|full-bug|full)\s*$')
338
+ if (-not $match.Success) {
339
+ return $UnresolvedMode
340
+ }
341
+ $rawMode = $match.Groups[1].Value
342
+ if ($rawMode -eq 'full') {
343
+ return 'full-feature'
344
+ }
345
+ return $rawMode
346
+ }
347
+
348
+ function Get-FeatureFolderPlanPrerequisite {
349
+ <#
350
+ .SYNOPSIS
351
+ Maps a work mode to the files a feature folder must contain before a plan is written.
352
+ .DESCRIPTION
353
+ minor-audit requires issue.md; full-bug requires issue.md and spec.md; full-feature
354
+ and every other value require issue.md, spec.md, and user-story.md.
355
+ .PARAMETER WorkMode
356
+ The resolved work mode.
357
+ .OUTPUTS
358
+ System.String[]
359
+ #>
360
+ [CmdletBinding()]
361
+ [OutputType([string[]])]
362
+ param(
363
+ [AllowNull()]
364
+ [AllowEmptyString()]
365
+ [string] $WorkMode
366
+ )
367
+
368
+ switch ($WorkMode) {
369
+ 'minor-audit' { return [string[]]@('issue.md') }
370
+ 'full-bug' { return [string[]]@('issue.md', 'spec.md') }
371
+ default { return [string[]]@('issue.md', 'spec.md', 'user-story.md') }
372
+ }
373
+ }
@@ -0,0 +1,85 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Resolve the coverage thresholds that govern the feature-review coverage validation.
4
+ .DESCRIPTION
5
+ Implements the threshold precedence of the pushed quality-tiers rule for the
6
+ feature-review SubagentStop hook (issue #824, follow-up FU-823-1). When the
7
+ repository's root CLAUDE.md states a line or branch coverage threshold, that threshold
8
+ governs; otherwise the default governs (85 percent line, 75 percent branch). Each
9
+ metric is resolved on its own, so a root CLAUDE.md that states only a line threshold
10
+ leaves the branch default in force.
11
+
12
+ Pure string logic only: the caller supplies the CLAUDE.md text. No disk, process,
13
+ network, clock, or environment access. It is dot-sourced by
14
+ validate-feature-review-coverage.ps1 as:
15
+ . (Join-Path $PSScriptRoot 'feature-review-coverage-thresholds.ps1')
16
+ #>
17
+
18
+
19
+ function Get-FeatureReviewCoverageThreshold {
20
+ <#
21
+ .SYNOPSIS
22
+ Return the governing line and branch coverage thresholds for a root CLAUDE.md text.
23
+ .DESCRIPTION
24
+ For each metric M in 'line' and 'branch', a figure is read only when a comparator
25
+ or floor phrase links it to the metric on the same line as 'M coverage'. Two forms
26
+ are accepted, and any text without a percent sign may sit between 'M coverage' and
27
+ the comparator or the figure:
28
+
29
+ - Comparator before the figure: one of '>=', the U+2265 sign, 'at least',
30
+ 'minimum', 'minimum of', or 'no less than', an optional colon, and the figure
31
+ followed by a percent sign (for example 'line coverage >= 85%').
32
+ - Trailing floor phrase after the figure: the figure followed by a percent sign,
33
+ then one of 'minimum', 'at least', 'or more', 'or higher', 'or above', or
34
+ 'or greater' (for example 'line coverage: 70% minimum').
35
+
36
+ Prose such as 'line coverage was 62% last release' is not read. The first match in
37
+ text order across both forms whose value parses (invariant culture) to a number
38
+ from 0 to 100 sets that metric and its source to 'claude-md'. Otherwise the metric
39
+ keeps its default (line 85, branch 75) and its source is 'default'.
40
+
41
+ Known limitation: a combined statement such as 'line and branch coverage >= 70%'
42
+ matches neither metric, because the metric name must be followed directly by
43
+ 'coverage' and must not be joined to the other metric name by 'and', 'or', '&',
44
+ or '/'. Both metrics then keep their defaults, which is the stricter direction.
45
+ .PARAMETER ClaudeMdText
46
+ The text of the repository's root CLAUDE.md; $null or empty when it is absent.
47
+ .OUTPUTS
48
+ System.Management.Automation.PSCustomObject - Line, Branch, LineSource, BranchSource.
49
+ #>
50
+ [CmdletBinding()]
51
+ [OutputType([pscustomobject])]
52
+ param(
53
+ [Parameter(Mandatory)][AllowNull()][AllowEmptyString()][string] $ClaudeMdText
54
+ )
55
+
56
+ $defaults = [ordered]@{ line = 85.0; branch = 75.0 }
57
+ $resolved = @{}
58
+ $culture = [System.Globalization.CultureInfo]::InvariantCulture
59
+ $style = [System.Globalization.NumberStyles]::Float
60
+
61
+ foreach ($metric in $defaults.Keys) {
62
+ $value = [double]$defaults[$metric]
63
+ $source = 'default'
64
+ if (-not [string]::IsNullOrEmpty($ClaudeMdText)) {
65
+ $pattern = '(?im)(?<!\b(?:line|branch)\s*(?:and|or|&|/)\s*)\b' + $metric + '\s+coverage\b[^\r\n%]*?(?:(?:>=|' + [char]0x2265 + '|\bat\s+least\b|\bminimum(?:\s+of)?\b|\bno\s+less\s+than\b)\s*:?\s*(?<![\d.])(?<pct>\d{1,3}(?:\.\d+)?)\s*%|(?<![\d.])(?<pct>\d{1,3}(?:\.\d+)?)\s*%\s*(?:minimum|at\s+least|or\s+(?:more|higher|above|greater))\b)'
66
+ foreach ($match in [regex]::Matches($ClaudeMdText, $pattern)) {
67
+ $parsed = 0.0
68
+ $isNumber = [double]::TryParse($match.Groups['pct'].Value, $style, $culture, [ref]$parsed)
69
+ if ($isNumber -and $parsed -ge 0 -and $parsed -le 100) {
70
+ $value = $parsed
71
+ $source = 'claude-md'
72
+ break
73
+ }
74
+ }
75
+ }
76
+ $resolved[$metric] = [pscustomobject]@{ Value = $value; Source = $source }
77
+ }
78
+
79
+ return [pscustomobject]@{
80
+ Line = $resolved['line'].Value
81
+ Branch = $resolved['branch'].Value
82
+ LineSource = $resolved['line'].Source
83
+ BranchSource = $resolved['branch'].Source
84
+ }
85
+ }