@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
@@ -7,49 +7,29 @@
7
7
  dot-sources. Holds:
8
8
 
9
9
  - The import guard for WorktreeRunResolution.psm1. A failed import is recorded in
10
- $script:EpicWorktreeGateResolutionImportFailure, and
11
- Get-EpicWorktreeGateImportFailureDecision turns it into a deny, so a missing module
10
+ $script:HookDependencyFailures, and the gate's call to
11
+ Get-HookDependencyFailureDecision turns it into a deny, so a missing module
12
12
  cannot make the gate exit non-zero and fail open.
13
13
  - The two checkpoint read seams, relocated from the gate file with their names kept.
14
14
  Each takes a mandatory absolute path composed beneath a resolved worktree root.
15
15
  - The resolution seam Resolve-EpicWorktreeGateRunTarget, which locates the epic or
16
16
  parallel checkpoint that records the removal target under worktree_path.
17
- - Read-EpicWorktreeGateRunCheckpoint, which returns the target and the parsed
18
- checkpoint beneath it ($null when the target is unresolved).
17
+ - Read-EpicWorktreeGateRunCheckpoint, which returns the target, the parsed checkpoint
18
+ beneath it ($null when the target is unresolved), and the Path it read ($null when
19
+ the target is unresolved; issue #851).
20
+ - Find-EpicWorktreeGateParallelItemRecord, which locates the parallel items[] record
21
+ for the removal target, and Get-EpicWorktreeGateDenyDiagnostics, which builds the
22
+ diagnostics clause of the final deny from two read results without reading any
23
+ file (issue #851).
19
24
 
20
25
  .NOTES
21
- PowerShell 7+. Depends on Get-EpicWorktreeGateBlockDecision and
22
- ConvertFrom-EpicWorktreeGateJson from the gate file, resolved at call time. Mirrored
23
- byte-identically under extensions/drm-copilot/resources/claude-customizations/.
26
+ PowerShell 7+. Depends on Get-EpicWorktreeGateBlockDecision,
27
+ ConvertFrom-EpicWorktreeGateJson, and Find-EpicWorktreeFeatureRecord from the gate
28
+ file, resolved at call time. Mirrored byte-identically under
29
+ extensions/drm-copilot/resources/claude-customizations/.
24
30
  #>
25
31
 
26
- # Import guard (issue #690): a failed import denies instead of failing open.
27
- $script:EpicWorktreeGateResolutionImportFailure = $null
28
- try {
29
- Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop
30
- }
31
- catch {
32
- $script:EpicWorktreeGateResolutionImportFailure = 'WorktreeRunResolution.psm1'
33
- }
34
-
35
- function Get-EpicWorktreeGateImportFailureDecision {
36
- <#
37
- .SYNOPSIS
38
- Returns the deny for a failed worktree-resolution import, or $null.
39
- .OUTPUTS
40
- System.Collections.Specialized.OrderedDictionary, or $null when the import succeeded.
41
- #>
42
- [CmdletBinding()]
43
- [OutputType([System.Collections.Specialized.OrderedDictionary])]
44
- param()
45
-
46
- if (-not $script:EpicWorktreeGateResolutionImportFailure) {
47
- return $null
48
- }
49
- return Get-EpicWorktreeGateBlockDecision -Reason (
50
- "EPIC_WORKTREE_REMOVAL_BLOCKED: the worktree-resolution module '$($script:EpicWorktreeGateResolutionImportFailure)' " +
51
- 'failed to import, so the run checkpoint that governs this removal cannot be located; the gate fails closed.')
52
- }
32
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeRunResolution.psm1' -ErrorRecord $_ }
53
33
 
54
34
  function Get-EpicWorktreeGateCheckpointContent {
55
35
  <#
@@ -121,7 +101,9 @@ function Read-EpicWorktreeGateRunCheckpoint {
121
101
  .PARAMETER WorktreePath
122
102
  The removal target the command names.
123
103
  .OUTPUTS
124
- System.Management.Automation.PSCustomObject with Target and Checkpoint ($null when unresolved).
104
+ System.Management.Automation.PSCustomObject with Target, Checkpoint ($null when
105
+ unresolved, absent, or unparseable), and Path (the composed absolute checkpoint
106
+ path, or $null when the target is unresolved).
125
107
  #>
126
108
  [CmdletBinding()]
127
109
  [OutputType([pscustomobject])]
@@ -132,6 +114,7 @@ function Read-EpicWorktreeGateRunCheckpoint {
132
114
 
133
115
  $target = Resolve-EpicWorktreeGateRunTarget -Kind $Kind -WorktreePath $WorktreePath
134
116
  $checkpoint = $null
117
+ $path = $null
135
118
  if ($target.Status -eq 'SessionRoot' -or $target.Status -eq 'OtherWorktree') {
136
119
  $path = Get-WorktreeRunCheckpointPath -Kind $Kind -WorktreeRoot $target.WorktreeRoot
137
120
  $raw = if ($Kind -eq 'epic') { Get-EpicWorktreeGateCheckpointContent -Path $path } else { Get-EpicWorktreeGateParallelCheckpointContent -Path $path }
@@ -140,5 +123,114 @@ function Read-EpicWorktreeGateRunCheckpoint {
140
123
  return [pscustomobject]@{
141
124
  Target = $target
142
125
  Checkpoint = $checkpoint
126
+ Path = $path
127
+ }
128
+ }
129
+
130
+ function Find-EpicWorktreeGateParallelItemRecord {
131
+ <#
132
+ .SYNOPSIS
133
+ Locate the parallel items[] record whose worktree_path matches the target path.
134
+ .DESCRIPTION
135
+ Pure. Normalizes both paths exactly as Test-ParallelCheckpointAllowsWorktreeRemoval
136
+ does (backslash to slash, trailing slash trimmed, PowerShell -eq) and returns the
137
+ first matching entry (issue #851).
138
+ .PARAMETER Checkpoint
139
+ Parsed parallel-orchestrator checkpoint, or $null when absent/unreadable.
140
+ .PARAMETER WorktreePath
141
+ The removal target the command names.
142
+ .OUTPUTS
143
+ System.Object or $null
144
+ #>
145
+ [CmdletBinding()]
146
+ param(
147
+ [AllowNull()]
148
+ $Checkpoint,
149
+
150
+ [AllowNull()]
151
+ [string] $WorktreePath
152
+ )
153
+
154
+ if ($null -eq $Checkpoint -or [string]::IsNullOrWhiteSpace($WorktreePath)) {
155
+ return $null
156
+ }
157
+ $props = @($Checkpoint.PSObject.Properties.Name)
158
+ if ($props -notcontains 'items' -or $null -eq $Checkpoint.items) {
159
+ return $null
160
+ }
161
+
162
+ $normalizedTarget = ($WorktreePath -replace '\\', '/').TrimEnd('/')
163
+ foreach ($item in @($Checkpoint.items)) {
164
+ if ($null -eq $item) {
165
+ continue
166
+ }
167
+ $itemProps = @($item.PSObject.Properties.Name)
168
+ if ($itemProps -notcontains 'worktree_path') {
169
+ continue
170
+ }
171
+ $normalizedItemPath = (([string]$item.worktree_path) -replace '\\', '/').TrimEnd('/')
172
+ if ($normalizedItemPath -eq $normalizedTarget) {
173
+ return $item
174
+ }
175
+ }
176
+ return $null
177
+ }
178
+
179
+ function Get-EpicWorktreeGateDenyDiagnostics {
180
+ <#
181
+ .SYNOPSIS
182
+ Build the diagnostics clause of the final deny from the two read results (issue #851).
183
+ .DESCRIPTION
184
+ Pure; reads no file. Names, for each run kind, the target status, the checkpoint
185
+ path read (or that none was read), and the outcome of the record match: the
186
+ matched record's merge_status, an absent merge_status, no matching record, an
187
+ absent or unparseable checkpoint, or not evaluated when no checkpoint was read.
188
+ .PARAMETER EpicRead
189
+ The epic read result (Target, Checkpoint, Path).
190
+ .PARAMETER ParallelRead
191
+ The parallel read result (Target, Checkpoint, Path).
192
+ .PARAMETER WorktreePath
193
+ The removal target the command names.
194
+ .OUTPUTS
195
+ System.String
196
+ #>
197
+ [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '', Justification = 'The name is fixed by the issue #851 plan contract; the plural noun names the one diagnostics clause it returns.')]
198
+ [CmdletBinding()]
199
+ [OutputType([string])]
200
+ param(
201
+ [Parameter(Mandatory)] $EpicRead,
202
+ [Parameter(Mandatory)] $ParallelRead,
203
+ [AllowNull()][AllowEmptyString()][string] $WorktreePath
204
+ )
205
+
206
+ $parts = foreach ($kind in @('epic', 'parallel')) {
207
+ $read = if ($kind -eq 'epic') { $EpicRead } else { $ParallelRead }
208
+ $pathText = if ($null -ne $read.Path) { "checkpoint '$($read.Path)'" } else { 'no checkpoint read' }
209
+ if ($null -eq $read.Path) {
210
+ $recordText = 'not evaluated'
211
+ }
212
+ elseif ($null -eq $read.Checkpoint) {
213
+ $recordText = 'checkpoint absent or unparseable'
214
+ }
215
+ else {
216
+ $record = if ($kind -eq 'epic') {
217
+ Find-EpicWorktreeFeatureRecord -Checkpoint $read.Checkpoint -WorktreePath $WorktreePath
218
+ }
219
+ else {
220
+ Find-EpicWorktreeGateParallelItemRecord -Checkpoint $read.Checkpoint -WorktreePath $WorktreePath
221
+ }
222
+ $arrayName = if ($kind -eq 'epic') { 'features' } else { 'items' }
223
+ $recordText = if ($null -eq $record) {
224
+ "no matching $arrayName[] record"
225
+ }
226
+ elseif (@($record.PSObject.Properties.Name) -contains 'merge_status') {
227
+ "merge_status '$([string]$record.merge_status)'"
228
+ }
229
+ else {
230
+ 'merge_status absent'
231
+ }
232
+ }
233
+ '{0} run {1} ({2}), {3}' -f $kind, $read.Target.Status, $pathText, $recordText
143
234
  }
235
+ return ('Diagnostics: {0}; {1}.' -f $parts[0], $parts[1])
144
236
  }
@@ -55,6 +55,10 @@
55
55
  enforce-epic-worktree-removal-gate-resolution.ps1. An ambiguous target denies before
56
56
  any allow; when neither kind resolves, the deny names TARGET_WORKTREE_NOT_DERIVABLE.
57
57
 
58
+ Issues #789 and #851: the final deny carries one leading EPIC_WORKTREE_REMOVAL_BLOCKED
59
+ token and ends with a diagnostics clause naming each run kind's status, checkpoint
60
+ path, and matched record; the allow-or-deny decision is unchanged.
61
+
58
62
  .NOTES
59
63
  Compatible with PowerShell 7+. Depends on WorktreeRunResolution.psm1 through the
60
64
  dot-sourced resolution sibling, whose import guard denies on a failed import.
@@ -63,18 +67,19 @@
63
67
  #>
64
68
  [CmdletBinding()]
65
69
  param()
70
+ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
66
71
 
67
72
 
68
- Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
73
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'HookPayload.psm1' -ErrorRecord $_ }
69
74
  # Sanctioned-removal manifest reader (issue #635), consumed by the manifest branch in
70
75
  # Invoke-EpicWorktreeRemovalGateDecision.
71
- Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeManifest.psm1') -Force
76
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeManifest.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'CleanupWorktreeManifest.psm1' -ErrorRecord $_ }
72
77
  # Shared command-line parser (issue #545), consumed by the scope filter in
73
- # Invoke-EpicWorktreeRemovalGateDecision and by Get-EpicWorktreeRemovalCommandPath.
74
- . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
75
- . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
78
+ # Invoke-EpicWorktreeRemovalGateDecision and by Resolve-CommandLineInvocationTarget.
79
+ try { . (Join-Path $PSScriptRoot 'hook-command-scanner.ps1') } catch { Add-HookDependencyFailure -Name 'hook-command-scanner.ps1' -ErrorRecord $_ }
80
+ try { . (Join-Path $PSScriptRoot 'hook-command-invocation.ps1') } catch { Add-HookDependencyFailure -Name 'hook-command-invocation.ps1' -ErrorRecord $_ }
76
81
  # Checkpoint read seams, the import guard, and run-target resolution (issue #690).
77
- . (Join-Path $PSScriptRoot 'enforce-epic-worktree-removal-gate-resolution.ps1')
82
+ try { . (Join-Path $PSScriptRoot 'enforce-epic-worktree-removal-gate-resolution.ps1') } catch { Add-HookDependencyFailure -Name 'enforce-epic-worktree-removal-gate-resolution.ps1' -ErrorRecord $_ }
78
83
  $script:AllowedMergeStatuses = @('merged', 'worktree_removed')
79
84
  # Sanctioned-removal manifest location. Recorded here so every hook-read document this
80
85
  # gate consults is named; the module owns the read itself.
@@ -108,47 +113,6 @@ function ConvertFrom-EpicWorktreeGateJson {
108
113
  }
109
114
  }
110
115
 
111
- function Get-EpicWorktreeRemovalCommandPath {
112
- <#
113
- .SYNOPSIS
114
- Extract the target worktree path argument from a git worktree remove command.
115
- .DESCRIPTION
116
- The operand comes from the segment that structurally invokes git worktree remove,
117
- so a 'cd <path> &&' segment chained before the removal contributes nothing and a
118
- quoted mention of the phrase resolves to no operand at all. Quotes around the path
119
- are already stripped by the tokenizer.
120
-
121
- '--force' is a zero-argument flag, so it never contributes an operand and may be
122
- written on either side of the target path. Its presence is read structurally through
123
- Test-CommandLineFlag rather than by searching the raw text, so a '--force' spelling
124
- that appears inside an unrelated quoted argument cannot change how the operand list
125
- is read. When no operand resolves, a present '--force' is reported in its place, so
126
- the checkpoint lookup fails closed on a value that matches no recorded worktree_path
127
- - the same value the previous raw-text pattern returned for that input.
128
- .PARAMETER CommandText
129
- The Bash command text under evaluation.
130
- .OUTPUTS
131
- System.String or $null
132
- #>
133
- [CmdletBinding()]
134
- [OutputType([string])]
135
- param(
136
- [Parameter(Mandatory)]
137
- [string] $CommandText
138
- )
139
-
140
- $hasForce = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove') -FlagName '--force'
141
- $operands = @(Get-CommandLineOperand -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))
142
-
143
- if ($operands.Count -gt 0) {
144
- return $operands[0]
145
- }
146
- if ($hasForce) {
147
- return '--force'
148
- }
149
- return $null
150
- }
151
-
152
116
  function Find-EpicWorktreeFeatureRecord {
153
117
  <#
154
118
  .SYNOPSIS
@@ -331,10 +295,8 @@ function Invoke-EpicWorktreeRemovalGateDecision {
331
295
  )
332
296
 
333
297
  # A failed worktree-resolution import denies before any other logic (issue #690).
334
- $importFailure = Get-EpicWorktreeGateImportFailureDecision
335
- if ($null -ne $importFailure) {
336
- return $importFailure
337
- }
298
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'EPIC_WORKTREE_REMOVAL_BLOCKED:'
299
+ if ($null -ne $dependencyDecision) { return $dependencyDecision }
338
300
 
339
301
  $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
340
302
  if (-not $payload.IsValid) {
@@ -356,7 +318,43 @@ function Invoke-EpicWorktreeRemovalGateDecision {
356
318
  return Get-EpicWorktreeGateAllowDecision
357
319
  }
358
320
 
359
- $worktreePath = Get-EpicWorktreeRemovalCommandPath -CommandText $commandText
321
+ # Issue #824: the removal targets are derived structurally from every invocation.
322
+ # A target that cannot be derived denies before any checkpoint is read, and every
323
+ # derived target must be authorized on its own.
324
+ $resolution = Resolve-CommandLineInvocationTarget -CommandText $commandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove')
325
+ if ($resolution.Status -eq 'NoMatch') {
326
+ return Get-EpicWorktreeGateAllowDecision
327
+ }
328
+ if ($resolution.Status -ne 'Targets') {
329
+ return Get-EpicWorktreeGateBlockDecision -Reason 'EPIC_WORKTREE_REMOVAL_BLOCKED: TARGET_WORKTREE_NOT_DERIVABLE: the git worktree remove target cannot be derived from the command text. No checkpoint can authorize this removal.'
330
+ }
331
+ foreach ($target in @($resolution.Targets)) {
332
+ $denial = Get-EpicWorktreeRemovalTargetDenial -WorktreePath $target
333
+ if ($null -ne $denial) {
334
+ return $denial
335
+ }
336
+ }
337
+ return Get-EpicWorktreeGateAllowDecision
338
+ }
339
+
340
+ function Get-EpicWorktreeRemovalTargetDenial {
341
+ <#
342
+ .SYNOPSIS
343
+ Evaluate one derived removal target and return its deny decision, or $null when authorized.
344
+ .DESCRIPTION
345
+ Holds the per-path authorization cascade (epic checkpoint, parallel checkpoint,
346
+ sanctioned-removal manifest) for a single worktree path (issue #824).
347
+ .PARAMETER WorktreePath
348
+ One removal target derived by Resolve-CommandLineInvocationTarget.
349
+ .OUTPUTS
350
+ System.Collections.Specialized.OrderedDictionary or $null
351
+ #>
352
+ [CmdletBinding()]
353
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
354
+ param(
355
+ [Parameter(Mandatory)]
356
+ [string] $WorktreePath
357
+ )
360
358
 
361
359
  # Each run checkpoint is read beneath the worktree that records this path (issue #690);
362
360
  # an ambiguous target denies before any allow or manifest evaluation.
@@ -371,12 +369,12 @@ function Invoke-EpicWorktreeRemovalGateDecision {
371
369
 
372
370
  $featureRecord = Find-EpicWorktreeFeatureRecord -Checkpoint $checkpoint -WorktreePath $worktreePath
373
371
  if (Test-EpicWorktreeRemovalAllowed -FeatureRecord $featureRecord) {
374
- return Get-EpicWorktreeGateAllowDecision
372
+ return $null
375
373
  }
376
374
 
377
375
  $parallelCheckpoint = $parallelRead.Checkpoint
378
376
  if (Test-ParallelCheckpointAllowsWorktreeRemoval -Checkpoint $parallelCheckpoint -WorktreePath $worktreePath) {
379
- return Get-EpicWorktreeGateAllowDecision
377
+ return $null
380
378
  }
381
379
 
382
380
  # Sanctioned-removal manifest branch (issue #635). It runs last, so an
@@ -392,15 +390,18 @@ function Invoke-EpicWorktreeRemovalGateDecision {
392
390
  if (-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $checkpoint -RecordArrayName 'features' -WorktreePath $worktreePath) -and
393
391
  -not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $parallelCheckpoint -RecordArrayName 'items' -WorktreePath $worktreePath) -and
394
392
  (Test-CleanupWorktreeManifestAuthorizesRemoval -WorktreePath $worktreePath)) {
395
- return Get-EpicWorktreeGateAllowDecision
393
+ return $null
396
394
  }
397
395
 
398
396
  # When neither kind resolved, the deny names the resolution reason first (issue #690).
397
+ # The gate token leads once and a diagnostics clause for this target closes the text
398
+ # (issues #789, #851).
399
399
  $prefix = ''
400
400
  if ($epicRead.Target.Status -eq 'NoTarget' -and $parallelRead.Target.Status -eq 'NoTarget') {
401
- $prefix = "EPIC_WORKTREE_REMOVAL_BLOCKED: $($epicRead.Target.ReasonCode): $($epicRead.Target.Detail). "
401
+ $prefix = "$($epicRead.Target.ReasonCode): $($epicRead.Target.Detail). "
402
402
  }
403
- return Get-EpicWorktreeGateBlockDecision -Reason ($prefix + "EPIC_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires either an epic checkpoint features[] record with merge_status in {merged, worktree_removed}, or a parallel-orchestrator checkpoint with route_id == ""parallel"" whose matching items[] record (matched by worktree_path) has merge_status in {merged, worktree_removed}. No checkpoint authorized this removal.")
403
+ $diagnostics = Get-EpicWorktreeGateDenyDiagnostics -EpicRead $epicRead -ParallelRead $parallelRead -WorktreePath $worktreePath
404
+ return Get-EpicWorktreeGateBlockDecision -Reason ('EPIC_WORKTREE_REMOVAL_BLOCKED: ' + $prefix + "git worktree remove for '$worktreePath' requires either an epic checkpoint features[] record with merge_status in {merged, worktree_removed}, or a parallel-orchestrator checkpoint with route_id == ""parallel"" whose matching items[] record (matched by worktree_path) has merge_status in {merged, worktree_removed}. No checkpoint authorized this removal. " + $diagnostics)
404
405
  }
405
406
 
406
407
  function Invoke-EpicWorktreeRemovalGateEntryPoint {
@@ -445,6 +446,9 @@ function Invoke-EpicWorktreeRemovalGateEntryPoint {
445
446
  if ($MyInvocation.InvocationName -eq '.') {
446
447
  return
447
448
  }
449
+ if ($script:HookDependencyGuardLoadFailed) { [Console]::Error.WriteLine('EPIC_WORKTREE_REMOVAL_BLOCKED: hook-dependency-guard.ps1 failed to load; the gate fails closed.'); exit 2 }
450
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'EPIC_WORKTREE_REMOVAL_BLOCKED:'
451
+ if ($null -ne $dependencyDecision) { $dependencyDecision | ConvertTo-Json -Compress -Depth 5 | Write-Output; exit 0 }
448
452
 
449
453
  # The entry point returns its [int] exit code as the last pipeline element and the
450
454
  # decision JSON before it. `exit (<call>)` would capture BOTH into the exit
@@ -41,8 +41,9 @@
41
41
  #>
42
42
  [CmdletBinding()]
43
43
  param()
44
+ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
44
45
 
45
- Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
46
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'HookPayload.psm1' -ErrorRecord $_ }
46
47
 
47
48
  function Test-EvidenceLocationForbidden {
48
49
  <#
@@ -150,6 +151,8 @@ function Invoke-EvidenceLocationDecision {
150
151
  [AllowEmptyString()]
151
152
  [string] $ToolInputRaw
152
153
  )
154
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'EVIDENCE_LOCATION_BLOCKED:'
155
+ if ($null -ne $dependencyDecision) { return $dependencyDecision }
153
156
 
154
157
  $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
155
158
  if (-not $payload.IsValid) {
@@ -211,6 +214,9 @@ function Invoke-EvidenceLocationEntryPoint {
211
214
  if ($MyInvocation.InvocationName -eq '.') {
212
215
  return
213
216
  }
217
+ if ($script:HookDependencyGuardLoadFailed) { [Console]::Error.WriteLine('EVIDENCE_LOCATION_BLOCKED: hook-dependency-guard.ps1 failed to load; the gate fails closed.'); exit 2 }
218
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'EVIDENCE_LOCATION_BLOCKED:'
219
+ if ($null -ne $dependencyDecision) { $dependencyDecision | ConvertTo-Json -Compress -Depth 5 | Write-Output; exit 0 }
214
220
 
215
221
  # The entry point returns its [int] exit code as the last pipeline element and the
216
222
  # decision JSON before it. `exit (<call>)` would capture BOTH into the exit
@@ -1,32 +1,59 @@
1
1
  <#
2
2
  .SYNOPSIS
3
- Pre-tool-use hook that blocks writes to a feature folder's plan.md when issue.md,
4
- spec.md, or user-story.md are not yet present in that same folder.
3
+ Pre-tool-use hook that blocks writes to a feature folder's plan file until the
4
+ prerequisite documents its persisted work mode requires exist in that same folder.
5
5
 
6
6
  .DESCRIPTION
7
7
  Invoked by the Claude Code PreToolUse hook on Write or Edit operations. Acquires
8
8
  the hook payload through the shared reader and reads file_path from the envelope's
9
- nested tool_input. When the
10
- target file_path matches a feature-folder plan.md path under
11
- docs/features/(active|archive)/<folder>/plan.md, the script verifies that
12
- each of issue.md, spec.md, and user-story.md exists in the same folder.
13
-
14
- If any of the three sibling files is missing, the script emits a PreToolUse
15
- JSON response with hookSpecificOutput.permissionDecision='deny' and exits 0 so
16
- Claude Code surfaces the reason. All other paths pass through with
9
+ nested tool_input. The gate applies to a feature-folder plan file under
10
+ docs/features/(active|archive)/<folder>/: a literal plan.md or a timestamped
11
+ plan.<yyyy-MM-ddTHH-mm>.md (issue #568). Other paths pass through with
17
12
  permissionDecision='allow'.
18
13
 
19
- Filesystem reads go through Get-FeatureFolderFileExistence so tests can
20
- inject a fake without touching disk.
14
+ For a plan file, the script reads the folder's issue.md through
15
+ Get-FeatureFolderIssueContent, resolves the persisted '- Work Mode:' marker with the
16
+ shared Resolve-FeatureFolderWorkMode, and takes the required set from the shared
17
+ Get-FeatureFolderPlanPrerequisite:
18
+ - minor-audit -> issue.md
19
+ - full-bug -> issue.md, spec.md
20
+ - full-feature and legacy full -> issue.md, spec.md, user-story.md
21
+ A missing, empty, malformed, or unrecognized marker, or an unreadable issue.md,
22
+ fails closed to the full-feature set. Because issue.md is in every set, an absent
23
+ issue.md always denies.
24
+
25
+ If any required file is missing, the script emits a PreToolUse JSON response with
26
+ hookSpecificOutput.permissionDecision='deny' whose reason names the plan file, the
27
+ resolved work mode, and the missing files, and exits 0 so Claude Code surfaces it.
28
+
29
+ Filesystem reads go through Get-FeatureFolderFileExistence and
30
+ Get-FeatureFolderIssueContent so tests can inject fakes without touching disk.
21
31
 
22
32
  .NOTES
23
- Compatible with PowerShell 7+. Read-only validation gate; no state mutation.
33
+ Compatible with PowerShell 7+. Read-only validation gate; no state mutation. The pure
34
+ sibling feature-folder-resolution.ps1 (issue #565) is dot-sourced inside a guard: when
35
+ it cannot be loaded, every plan-file write denies and every other path stays allowed.
24
36
  #>
25
37
  [CmdletBinding()]
26
38
  param()
39
+ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
40
+
41
+
42
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'HookPayload.psm1' -ErrorRecord $_ }
43
+
44
+ # Shared work-mode parser and prerequisite map (issue #565). Guarded so a failed
45
+ # dot-source denies plan writes rather than failing open.
46
+ $script:FeatureFolderOrderResolutionImportFailure = $null
47
+ try {
48
+ . (Join-Path $PSScriptRoot 'feature-folder-resolution.ps1')
49
+ }
50
+ catch {
51
+ $script:FeatureFolderOrderResolutionImportFailure = 'feature-folder-resolution.ps1'
52
+ }
27
53
 
54
+ # The plan leaf a gated path ends in: plan.md or plan.<yyyy-MM-ddTHH-mm>.md (issue #568).
55
+ $script:FeaturePlanLeafPattern = '/plan(\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2})?\.md$'
28
56
 
29
- Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
30
57
  function Get-FeatureFolderFileExistence {
31
58
  <#
32
59
  .SYNOPSIS
@@ -44,25 +71,57 @@ function Get-FeatureFolderFileExistence {
44
71
  return [bool](Test-Path -LiteralPath $Path -PathType Leaf)
45
72
  }
46
73
 
74
+ function Get-FeatureFolderIssueContent {
75
+ <#
76
+ .SYNOPSIS
77
+ Reads a feature folder's issue.md, or returns $null when the file is absent or
78
+ cannot be read. Tests mock this seam; a $null result fails closed to the
79
+ full-feature prerequisite set.
80
+ .PARAMETER FeatureFolder
81
+ Normalized (forward-slash) feature-folder path.
82
+ #>
83
+ [CmdletBinding()]
84
+ [OutputType([string])]
85
+ param(
86
+ [Parameter(Mandatory)]
87
+ [string] $FeatureFolder
88
+ )
89
+
90
+ $issuePath = "$FeatureFolder/issue.md"
91
+ if (-not (Test-Path -LiteralPath $issuePath -PathType Leaf)) {
92
+ return $null
93
+ }
94
+
95
+ try {
96
+ return Get-Content -LiteralPath $issuePath -Raw -ErrorAction Stop
97
+ }
98
+ catch {
99
+ return $null
100
+ }
101
+ }
102
+
47
103
  function Get-FeatureFolderMissingFile {
48
104
  <#
49
105
  .SYNOPSIS
50
- Returns the list of required sibling files missing alongside plan.md.
106
+ Returns the list of required sibling files missing alongside the plan file.
51
107
  .PARAMETER PlanFilePath
52
- Normalized (forward-slash) path to the plan.md target.
108
+ Normalized (forward-slash) path to the plan.md or plan.<timestamp>.md target.
109
+ .PARAMETER RequiredFile
110
+ The prerequisite file names to probe. Defaults to the full-feature set.
53
111
  #>
54
112
  [CmdletBinding()]
55
113
  [OutputType([string[]])]
56
114
  param(
57
115
  [Parameter(Mandatory)]
58
- [string] $PlanFilePath
116
+ [string] $PlanFilePath,
117
+
118
+ [string[]] $RequiredFile = @('issue.md', 'spec.md', 'user-story.md')
59
119
  )
60
120
 
61
- $folder = $PlanFilePath -replace '/plan\.md$', ''
62
- $required = @('issue.md', 'spec.md', 'user-story.md')
121
+ $folder = $PlanFilePath -replace $script:FeaturePlanLeafPattern, ''
63
122
  [System.Collections.Generic.List[string]] $missing = [System.Collections.Generic.List[string]]::new()
64
123
 
65
- foreach ($name in $required) {
124
+ foreach ($name in $RequiredFile) {
66
125
  $siblingPath = "$folder/$name"
67
126
  if (-not (Get-FeatureFolderFileExistence -Path $siblingPath)) {
68
127
  $missing.Add($name)
@@ -75,7 +134,9 @@ function Get-FeatureFolderMissingFile {
75
134
  function Test-IsFeaturePlanPath {
76
135
  <#
77
136
  .SYNOPSIS
78
- Returns $true if the normalized path targets a feature-folder plan.md.
137
+ Returns $true if the normalized path targets a feature-folder plan.md or a
138
+ timestamped plan.<yyyy-MM-ddTHH-mm>.md directly inside an active or archive
139
+ feature folder.
79
140
  #>
80
141
  [CmdletBinding()]
81
142
  [OutputType([bool])]
@@ -84,7 +145,30 @@ function Test-IsFeaturePlanPath {
84
145
  [string] $NormalizedPath
85
146
  )
86
147
 
87
- return $NormalizedPath -match '(^|/)docs/features/(active|archive)/[^/]+/plan\.md$'
148
+ return $NormalizedPath -match '(^|/)docs/features/(active|archive)/[^/]+/plan(\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2})?\.md$'
149
+ }
150
+
151
+ function Get-FeatureFolderOrderDenyDecision {
152
+ <#
153
+ .SYNOPSIS
154
+ Builds the PreToolUse deny decision carrying the given reason.
155
+ .PARAMETER Reason
156
+ The deny reason, beginning with FEATURE_FOLDER_ORDER_BLOCKED:.
157
+ #>
158
+ [CmdletBinding()]
159
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
160
+ param(
161
+ [Parameter(Mandatory)]
162
+ [string] $Reason
163
+ )
164
+
165
+ return [ordered]@{
166
+ hookSpecificOutput = [ordered]@{
167
+ hookEventName = 'PreToolUse'
168
+ permissionDecision = 'deny'
169
+ permissionDecisionReason = $Reason
170
+ }
171
+ }
88
172
  }
89
173
 
90
174
  function Invoke-FeatureFolderOrderDecision {
@@ -103,18 +187,15 @@ function Invoke-FeatureFolderOrderDecision {
103
187
  [AllowEmptyString()]
104
188
  [string] $ToolInputRaw
105
189
  )
190
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'FEATURE_FOLDER_ORDER_BLOCKED:'
191
+ if ($null -ne $dependencyDecision) { return $dependencyDecision }
106
192
 
107
193
  $payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
108
194
  if (-not $payload.IsValid) {
109
- return [ordered]@{
110
- hookSpecificOutput = [ordered]@{
111
- hookEventName = 'PreToolUse'
112
- permissionDecision = 'deny'
113
- permissionDecisionReason = 'FEATURE_FOLDER_ORDER_BLOCKED: payload anomaly - ' +
114
- (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
115
- '. The gate fails closed on an envelope it cannot read.'
116
- }
117
- }
195
+ return Get-FeatureFolderOrderDenyDecision -Reason (
196
+ 'FEATURE_FOLDER_ORDER_BLOCKED: payload anomaly - ' +
197
+ (Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
198
+ '. The gate fails closed on an envelope it cannot read.')
118
199
  }
119
200
 
120
201
  $filePath = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'file_path'
@@ -128,19 +209,25 @@ function Invoke-FeatureFolderOrderDecision {
128
209
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
129
210
  }
130
211
 
131
- $missing = Get-FeatureFolderMissingFile -PlanFilePath $normalized
212
+ $planLeaf = ($normalized -split '/')[-1]
213
+ if ($script:FeatureFolderOrderResolutionImportFailure) {
214
+ return Get-FeatureFolderOrderDenyDecision -Reason (
215
+ "FEATURE_FOLDER_ORDER_BLOCKED: the shared resolver '$($script:FeatureFolderOrderResolutionImportFailure)' " +
216
+ "failed to load, so the prerequisite documents for $planLeaf cannot be determined; the gate fails closed.")
217
+ }
218
+
219
+ $folder = $normalized -replace $script:FeaturePlanLeafPattern, ''
220
+ $workMode = Resolve-FeatureFolderWorkMode -IssueContent (Get-FeatureFolderIssueContent -FeatureFolder $folder)
221
+ $required = @(Get-FeatureFolderPlanPrerequisite -WorkMode $workMode)
222
+ $missing = @(Get-FeatureFolderMissingFile -PlanFilePath $normalized -RequiredFile $required)
132
223
  if ($missing.Count -eq 0) {
133
224
  return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
134
225
  }
135
226
 
136
227
  $list = ($missing -join ', ')
137
- return [ordered]@{
138
- hookSpecificOutput = [ordered]@{
139
- hookEventName = 'PreToolUse'
140
- permissionDecision = 'deny'
141
- permissionDecisionReason = "FEATURE_FOLDER_ORDER_BLOCKED: cannot write plan.md before producing prerequisite documents. Missing in feature folder: $list. Invoke the prd-feature subagent to generate the missing file(s) before authoring plan.md."
142
- }
143
- }
228
+ return Get-FeatureFolderOrderDenyDecision -Reason (
229
+ "FEATURE_FOLDER_ORDER_BLOCKED: cannot write $planLeaf before producing prerequisite documents for work mode '$workMode'. " +
230
+ "Missing in feature folder: $list. Invoke the prd-feature subagent to generate the missing file(s) before authoring the plan.")
144
231
  }
145
232
 
146
233
  function Invoke-FeatureFolderOrderEntryPoint {
@@ -185,6 +272,9 @@ function Invoke-FeatureFolderOrderEntryPoint {
185
272
  if ($MyInvocation.InvocationName -eq '.') {
186
273
  return
187
274
  }
275
+ if ($script:HookDependencyGuardLoadFailed) { [Console]::Error.WriteLine('FEATURE_FOLDER_ORDER_BLOCKED: hook-dependency-guard.ps1 failed to load; the gate fails closed.'); exit 2 }
276
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent PreToolUse -ReasonPrefix 'FEATURE_FOLDER_ORDER_BLOCKED:'
277
+ if ($null -ne $dependencyDecision) { $dependencyDecision | ConvertTo-Json -Compress -Depth 5 | Write-Output; exit 0 }
188
278
 
189
279
  # The entry point returns its [int] exit code as the last pipeline element and the
190
280
  # decision JSON before it. `exit (<call>)` would capture BOTH into the exit