@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
@@ -1,4 +1,4 @@
1
- <#
1
+ <#
2
2
  .SYNOPSIS
3
3
  SubagentStop hook for the orchestrator subagent.
4
4
 
@@ -11,7 +11,7 @@
11
11
  skills, the orchestrator must persist progress to the checkpoint file
12
12
  after every completed step. This hook confirms that:
13
13
 
14
- - the hook payload is well-formed JSON,
14
+ - the SubagentStop envelope read from stdin through Read-SubagentStopEnvelope is valid,
15
15
  - the orchestrator's final output is non-empty,
16
16
  - the checkpoint file exists on disk,
17
17
  - the checkpoint is valid JSON,
@@ -19,11 +19,26 @@
19
19
  objective, completed_steps, next_step, last_updated,
20
20
  - the `objective` field is non-empty.
21
21
 
22
+ The checkpoint is resolved through WorktreeRunResolution.psm1 by the dot-sourced
23
+ sibling validate-orchestrator-output-resolution.ps1 (issue #787), so every read uses
24
+ an absolute path beneath the worktree of the run that stopped. An unresolved,
25
+ ambiguous, or mismatched target blocks with ORCHESTRATOR_CHECKPOINT_UNRESOLVED:
26
+ before any checkpoint is read. For epic-orchestrator-state, the Layer 2 wave-barrier
27
+ ordering check (OrchestratorStateEpicWaveBarrier.psm1, issue #840) runs after the
28
+ routing dispatch passes.
29
+
22
30
  .NOTES
23
- Reads the hook payload from CLAUDE_HOOK_INPUT as JSON. Exits 0 to allow
24
- termination; exits 1 with an error message to block. Filesystem reads go
25
- through Get-CheckpointFileContent so tests can mock the boundary without
26
- writing temporary files.
31
+ Reads the SubagentStop envelope from stdin through Read-SubagentStopEnvelope
32
+ (subagentstop-envelope.ps1); the assistant text comes from the helper's
33
+ AssistantText. A non-repairable condition blocks once: when StopHookActive is
34
+ set, the re-entered stop is allowed. Exits 0 to allow termination; a block is
35
+ written to stderr with exit 2 through Exit-SubagentStopBlock. Filesystem reads
36
+ go through Get-CheckpointFileContent so tests can mock the boundary without
37
+ writing temporary files. The routing sibling, the resolution sibling, the E1
38
+ helper, and the modules are guarded loads: a failure is recorded and blocks
39
+ every validation, naming the file, before any read. A failed Layer 2 import
40
+ blocks the epic leg only.
41
+ An unhandled error during validation blocks with exit 2 and a reason (issue #928).
27
42
  #>
28
43
 
29
44
  [CmdletBinding()]
@@ -34,11 +49,21 @@ param(
34
49
  [Parameter(Mandatory = $false)]
35
50
  [string] $ArtifactType = 'orchestrator-state'
36
51
  )
52
+ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
37
53
 
38
54
  Set-StrictMode -Version Latest
39
- $ErrorActionPreference = 'Stop'
40
55
 
41
- Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorState.psm1') -Force
56
+ try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-resolution.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-resolution.ps1' -ErrorRecord $_ }
57
+ try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-routing.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-routing.ps1' -ErrorRecord $_ }
58
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeItemResolution.psm1' -ErrorRecord $_ }
59
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeRunResolution.psm1' -ErrorRecord $_ }
60
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateEpicWaveBarrier.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateEpicWaveBarrier.psm1' -ErrorRecord $_ }
61
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateCompletion.psm1') -Force -Global -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateCompletion.psm1' -ErrorRecord $_ }
62
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateUnconditional.psm1') -Force -Global -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateUnconditional.psm1' -ErrorRecord $_ }
63
+ try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorState.psm1') -Force -Global -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorState.psm1' -ErrorRecord $_ }
64
+ try { . (Join-Path $PSScriptRoot 'subagentstop-envelope.ps1') } catch { Add-HookDependencyFailure -Name 'subagentstop-envelope.ps1' -ErrorRecord $_ }
65
+ $ErrorActionPreference = 'Stop'
66
+ $script:SubagentStopReportAndHaltLine = 'This condition is not repairable by the subagent. Report it to the operator and halt.'
42
67
 
43
68
  function Get-CheckpointFileContent {
44
69
  <#
@@ -107,7 +132,7 @@ function Test-HumanInteractionShape {
107
132
 
108
133
  $hiProps = @($HumanInteraction.PSObject.Properties.Name)
109
134
  if ($hiProps -notcontains 'requirements') {
110
- return @{ Ok = $false; Message = "orchestrator hook: 'human_interaction' is present but 'requirements' array is missing." }
135
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: 'human_interaction' is present but 'requirements' array is missing." }
111
136
  }
112
137
 
113
138
  $requirements = @($HumanInteraction.requirements)
@@ -121,15 +146,15 @@ function Test-HumanInteractionShape {
121
146
  if ($reqProps -contains 'response') { $response = [string]$req.response }
122
147
 
123
148
  if ([string]::IsNullOrWhiteSpace($response)) {
124
- return @{ Ok = $false; Message = "orchestrator hook: 'human_interaction.requirements[$i]' has no resolved 'response'. Every unautomatable requirement must resolve to one of: scope_change, exception, halt." }
149
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: 'human_interaction.requirements[$i]' has no resolved 'response'. Every unautomatable requirement must resolve to one of: scope_change, exception, halt." }
125
150
  }
126
151
 
127
152
  if ($allowedResponses -notcontains $response) {
128
- return @{ Ok = $false; Message = "orchestrator hook: 'human_interaction.requirements[$i]' has 'response' value '$response' outside the allowed set (scope_change, exception, halt)." }
153
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: 'human_interaction.requirements[$i]' has 'response' value '$response' outside the allowed set (scope_change, exception, halt)." }
129
154
  }
130
155
 
131
156
  if ($response -eq 'halt') {
132
- return @{ Ok = $false; Message = "orchestrator hook: 'human_interaction.requirements[$i]' has 'response' == 'halt'; DONE is blocked while a halt is present." }
157
+ return @{ Ok = $false; Repairable = $false; Message = "orchestrator hook: 'human_interaction.requirements[$i]' has 'response' == 'halt'; DONE is blocked while a halt is present." }
133
158
  }
134
159
 
135
160
  if ($response -eq 'exception') {
@@ -137,11 +162,11 @@ function Test-HumanInteractionShape {
137
162
  if ($reqProps -contains 'runbook_path') { $runbookPath = [string]$req.runbook_path }
138
163
 
139
164
  if ([string]::IsNullOrWhiteSpace($runbookPath)) {
140
- return @{ Ok = $false; Message = "orchestrator hook: 'human_interaction.requirements[$i]' has 'response' == 'exception' but no non-empty 'runbook_path'. A permitted exception requires a runbook." }
165
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: 'human_interaction.requirements[$i]' has 'response' == 'exception' but no non-empty 'runbook_path'. A permitted exception requires a runbook." }
141
166
  }
142
167
 
143
168
  if (-not (& $FileExistsCheck $runbookPath)) {
144
- return @{ Ok = $false; Message = "orchestrator hook: 'human_interaction.requirements[$i]' references runbook_path '$runbookPath' but no file exists at that location." }
169
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: 'human_interaction.requirements[$i]' references runbook_path '$runbookPath' but no file exists at that location." }
145
170
  }
146
171
  }
147
172
  }
@@ -149,211 +174,101 @@ function Test-HumanInteractionShape {
149
174
  return @{ Ok = $true; Message = $null }
150
175
  }
151
176
 
152
- function Test-OrchestratorCheckpointStructure {
153
- <#
154
- .SYNOPSIS
155
- Type-scoped structural check for the epic and parallel checkpoint types.
156
- .DESCRIPTION
157
- PD-3 implementation. The Python reference exposes no validation surface for
158
- `epic-orchestrator-state` or `parallel-orchestrator-state` under this hook's
159
- flag pair: argparse rejects the pair and exits 2 without running a single
160
- check, so parity is UNDEFINED in this region. Rather than inherit an
161
- undefined behavior, this hook defines it: the checkpoint must exist, parse
162
- as JSON, and have an object root. That is the largest assertion that holds
163
- for every checkpoint type without importing a schema this hook does not own.
164
-
165
- Deliberately NOT applied here:
166
- - the standard-checkpoint REQUIRED_STATE_KEYS presence block, whose key
167
- set belongs to the standard checkpoint and would produce false
168
- ROUTING_CONTRACT_BLOCKED verdicts against a well-formed epic or
169
- parallel checkpoint (defect D-1),
170
- - the model-routing gate, whose receipts live on the standard checkpoint.
171
-
172
- The check fails closed: a missing file, unreadable content, invalid JSON, or
173
- a non-object root all yield ExitCode 1 with the load error as Output.
174
- .PARAMETER CheckpointPath
175
- The path to the checkpoint JSON file.
176
- .OUTPUTS
177
- System.Collections.Hashtable with keys ExitCode (int, 0 or 1) and Output (string).
178
- #>
179
- [CmdletBinding()]
180
- [OutputType([hashtable])]
181
- param(
182
- [Parameter(Mandatory = $true)]
183
- [string] $CheckpointPath
184
- )
185
-
186
- # Get-OrchestratorStateCheckpoint already implements exactly the three
187
- # structural conditions (exists, parses, object root) and reports each as a
188
- # fail-closed Error string, so the structural leg reuses it rather than
189
- # duplicating the load contract.
190
- $loaded = Get-OrchestratorStateCheckpoint -CheckpointPath $CheckpointPath
191
- if (-not $loaded.Ok) {
192
- return @{ ExitCode = 1; Output = $loaded.Error }
193
- }
194
-
195
- return @{ ExitCode = 0; Output = '' }
196
- }
197
-
198
- function Invoke-RoutingContractValidation {
177
+ function Invoke-OrchestratorOutputValidation {
199
178
  <#
200
179
  .SYNOPSIS
201
- Runs the portable routing-contract validation against the on-disk
202
- checkpoint and reports whether it emitted errors.
180
+ Reads the SubagentStop envelope, validates the assistant text, and applies the re-entry policy.
203
181
  .DESCRIPTION
204
- Invokes the validation through an injectable scriptblock seam. As of issue
205
- #475 the default Invoker names no interpreter and starts no subprocess: the
206
- portable PowerShell path is the ONLY path, so the hook behaves identically
207
- in this repository and in every destination that received only the
208
- pushed-down `.claude` pack. The former capability-detection probe and the
209
- interpreter-subprocess leg it guarded are both gone.
210
-
211
- The default Invoker dispatches on ArtifactType:
212
-
213
- orchestrator-state
214
- the COMPLETE-PARITY completion validation
215
- (Test-OrchestratorStateCompletionReadiness), a row-by-row port of the
216
- Python call surface `--require-complete --require-model-routing`.
217
-
218
- epic-orchestrator-state, parallel-orchestrator-state
219
- the type-scoped structural check
220
- (Test-OrchestratorCheckpointStructure): exists, parses as JSON,
221
- object root. PD-3: this is DEFINED behavior in a region where Python
222
- parity is UNDEFINED (argparse exit 2, zero checks run). It is a
223
- design decision, not a deferral. The standard-checkpoint
224
- REQUIRED_STATE_KEYS block and the model-routing gate are deliberately
225
- not applied, which is the D-1 fix.
226
-
227
- anything else
228
- fail closed, naming the unsupported type. An unrecognized type must
229
- never read as a clean pass.
230
-
231
- ArtifactType defaults to 'orchestrator-state' so every existing caller of
232
- this hook keeps its current behavior.
233
-
234
182
  Returns a hashtable with keys:
235
- - HasErrors: $true only when the validation reported a non-zero exit
236
- code; $false when it exited 0. The exit code is the sole
237
- discriminator.
238
- - ErrorText: the validation's output text, carried through unchanged:
239
- the error lines on a failure, empty on a clean pass.
183
+ - Ok: $true to allow termination, $false to block.
184
+ - Message: error message when blocking; $null on success.
185
+ SessionRoot is the calling process's path, used only to find the repository
186
+ when the run checkpoint is resolved.
240
187
  #>
241
188
  [CmdletBinding()]
242
189
  [OutputType([hashtable])]
243
190
  param(
244
- [Parameter(Mandatory = $true)]
245
- [string] $CheckpointPath,
246
-
247
- [Parameter(Mandatory = $false)]
191
+ [AllowNull()] [AllowEmptyString()] [string] $InputText,
192
+ [string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json',
248
193
  [string] $ArtifactType = 'orchestrator-state',
194
+ [string] $SessionRoot = (Get-Location).Path,
249
195
 
250
196
  [Parameter(Mandatory = $false)]
251
- [scriptblock] $Invoker = {
252
- param($Path, $Type)
253
- switch ($Type) {
254
- 'orchestrator-state' {
255
- # Import the portable completion module only when its function is not
256
- # already available, so a repeated call (or a test that pre-imports and
257
- # mocks the function) does not reload the module and reset the seam.
258
- if (-not (Get-Command -Name Test-OrchestratorStateCompletionReadiness -ErrorAction SilentlyContinue)) {
259
- Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateCompletion.psm1') -Force
260
- }
261
- $portable = Test-OrchestratorStateCompletionReadiness -CheckpointPath $Path
262
- [pscustomobject]@{
263
- ExitCode = $portable.ExitCode
264
- Output = $portable.Output
265
- }
266
- }
267
- { $_ -in @('epic-orchestrator-state', 'parallel-orchestrator-state') } {
268
- # PD-3: defined fail-closed structural behavior in an
269
- # undefined-parity region. See Test-OrchestratorCheckpointStructure.
270
- $structural = Test-OrchestratorCheckpointStructure -CheckpointPath $Path
271
- [pscustomobject]@{
272
- ExitCode = $structural.ExitCode
273
- Output = $structural.Output
274
- }
275
- }
276
- default {
277
- # Fail closed on an unwired type: an unrecognized artifact type is
278
- # not a clean pass.
279
- [pscustomobject]@{
280
- ExitCode = 1
281
- Output = "orchestrator hook: unsupported artifact type '$Type'; no validation surface is wired for it. Supported types: orchestrator-state, epic-orchestrator-state, parallel-orchestrator-state."
282
- }
283
- }
284
- }
285
- }
197
+ [scriptblock] $RoutingInvoker
286
198
  )
287
-
288
- $result = & $Invoker $CheckpointPath $ArtifactType
289
- $exitCode = 0
290
- if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'ExitCode')) {
291
- $exitCode = [int]$result.ExitCode
199
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent SubagentStop -ReasonPrefix 'ORCHESTRATOR_CHECKPOINT_UNRESOLVED:'
200
+ if ($null -ne $dependencyDecision) { return $dependencyDecision }
201
+
202
+ $envelope = if ($PSBoundParameters.ContainsKey('InputText')) { Read-SubagentStopEnvelope -InputText $InputText } else { Read-SubagentStopEnvelope }
203
+ if (-not $envelope.IsValid) { $decision = @{ Ok = $false; Repairable = $false; Message = ('{0} payload anomaly - {1}' -f 'orchestrator hook:', (Get-SubagentStopEnvelopeAnomalyReason -Anomaly $envelope.Anomaly)) } }
204
+ else {
205
+ $agentOutput = $envelope.AssistantText
206
+ $blockArguments = @{ AgentOutput = ([string]$agentOutput); CheckpointPath = $CheckpointPath; ArtifactType = $ArtifactType; SessionRoot = $SessionRoot }
207
+ if ($PSBoundParameters.ContainsKey('RoutingInvoker')) { $blockArguments['RoutingInvoker'] = $RoutingInvoker }
208
+ $decision = Get-OrchestratorOutputBlockDecision @blockArguments
292
209
  }
293
- $outputText = ''
294
- if ($null -ne $result -and ($result.PSObject.Properties.Name -contains 'Output')) {
295
- $outputText = ([string]$result.Output).Trim()
210
+ if ($decision.Ok) { return @{ Ok = $true; Message = $null } }
211
+ if (-not $decision.Repairable) {
212
+ if ($envelope.StopHookActive) { return @{ Ok = $true; Message = $null } }
213
+ if (-not $decision.Message.Contains('to the operator and halt.')) { $decision.Message = $decision.Message + "`n" + $script:SubagentStopReportAndHaltLine }
296
214
  }
297
-
298
- # The exit code is the complete failure discriminator: every dispatch leg
299
- # returns a non-zero ExitCode with its error text on failure and ExitCode 0
300
- # with empty Output on a clean pass, so output text must not influence this
301
- # decision.
302
- $hasErrors = ($exitCode -ne 0)
303
- return @{ HasErrors = $hasErrors; ErrorText = $outputText }
215
+ return @{ Ok = $false; Message = $decision.Message }
304
216
  }
305
217
 
306
- function Invoke-OrchestratorOutputValidation {
218
+ function Get-OrchestratorOutputBlockDecision {
307
219
  <#
308
220
  .SYNOPSIS
309
- Parses the hook payload and returns an ok-or-block decision.
221
+ Applies the orchestrator completion checks to the assistant text and returns the block decision.
310
222
  .DESCRIPTION
311
- Returns a hashtable with keys:
312
- - Ok: $true to allow termination, $false to block.
313
- - Message: error message when blocking; $null on success.
223
+ Returns @{ Ok = $true; Message = $null } to allow, or a hashtable with Ok = $false, a
224
+ Repairable boolean, and Message. Repairable is $true when the resumed subagent can clear
225
+ the condition (message content, its checkpoint, or its runbook) and $false when it cannot
226
+ (a registration argument, an internal evaluation failure, or a state it may not clear).
227
+ The caller applies the re-entry policy to that flag.
314
228
  #>
315
229
  [CmdletBinding()]
316
230
  [OutputType([hashtable])]
317
231
  param(
318
- [string] $RawPayload,
232
+ [Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $AgentOutput,
319
233
  [string] $CheckpointPath = 'artifacts/orchestration/orchestrator-state.json',
320
234
  [string] $ArtifactType = 'orchestrator-state',
235
+ [string] $SessionRoot = (Get-Location).Path,
321
236
 
322
237
  [Parameter(Mandatory = $false)]
323
238
  [scriptblock] $RoutingInvoker
324
239
  )
325
240
 
326
- if ([string]::IsNullOrWhiteSpace($RawPayload)) {
327
- return @{ Ok = $false; Message = 'orchestrator hook: CLAUDE_HOOK_INPUT is empty; cannot validate orchestrator output.' }
241
+ if ([string]::IsNullOrWhiteSpace($agentOutput)) {
242
+ return @{ Ok = $false; Repairable = $true; Message = 'orchestrator hook: agent output is empty (last_assistant_message carries no text); orchestrator must report final completion summary before termination.' }
328
243
  }
329
244
 
245
+ # Resolve the run checkpoint before any read (issue #787). A failed import, or an
246
+ # unresolved, ambiguous, or mismatched target, blocks here, so nothing is read.
330
247
  try {
331
- $payload = $RawPayload | ConvertFrom-Json -ErrorAction Stop
332
- } catch {
333
- return @{ Ok = $false; Message = "orchestrator hook: failed to parse CLAUDE_HOOK_INPUT as JSON: $($_.Exception.Message)" }
248
+ $resolution = Resolve-OrchestratorOutputCheckpointPath -ArtifactType $ArtifactType -CheckpointPath $CheckpointPath -AgentOutput ([string]$agentOutput) -SessionRoot $SessionRoot
334
249
  }
335
-
336
- $agentOutput = $null
337
- if ($payload.PSObject.Properties.Name -contains 'output') {
338
- $agentOutput = $payload.output
250
+ catch {
251
+ return @{ Ok = $false; Repairable = $false; Message = ('ORCHESTRATOR_CHECKPOINT_UNRESOLVED: {0}: NoTarget (RESOLVER_FAILED): {1}' -f $ArtifactType, $_.Exception.Message) }
339
252
  }
340
- if ([string]::IsNullOrWhiteSpace($agentOutput)) {
341
- return @{ Ok = $false; Message = 'orchestrator hook: agent output is empty; orchestrator must report final completion summary before termination.' }
253
+ if (-not $resolution.Resolved) {
254
+ $resolutionRepairable = ($resolution.Status -in @('NoTarget', 'Ambiguous')) -and ($ArtifactType -in @('epic-orchestrator-state', 'parallel-orchestrator-state'))
255
+ return @{ Ok = $false; Repairable = $resolutionRepairable; Message = ('ORCHESTRATOR_CHECKPOINT_UNRESOLVED: {0}: {1} ({2}): {3}' -f
256
+ $ArtifactType, $resolution.Status, $resolution.ReasonCode, $resolution.Detail) }
342
257
  }
343
258
 
344
- $file = Get-CheckpointFileContent -Path $CheckpointPath
259
+ $file = Get-CheckpointFileContent -Path $resolution.CheckpointPath
345
260
  if (-not $file.Exists) {
346
- return @{ Ok = $false; Message = "orchestrator hook: checkpoint file '$CheckpointPath' does not exist. Orchestrator must persist progress per powershell-orchestration-state-machine before termination." }
261
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: checkpoint file '$($resolution.CheckpointPath)' does not exist. Orchestrator must persist progress per powershell-orchestration-state-machine before termination." }
347
262
  }
348
263
 
349
264
  if ([string]::IsNullOrWhiteSpace($file.Content)) {
350
- return @{ Ok = $false; Message = "orchestrator hook: checkpoint file '$CheckpointPath' is empty; cannot validate orchestrator progress." }
265
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: checkpoint file '$($resolution.CheckpointPath)' is empty; cannot validate orchestrator progress." }
351
266
  }
352
267
 
353
268
  try {
354
269
  $checkpoint = $file.Content | ConvertFrom-Json -ErrorAction Stop
355
270
  } catch {
356
- return @{ Ok = $false; Message = "orchestrator hook: checkpoint file '$CheckpointPath' is not valid JSON: $($_.Exception.Message)" }
271
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: checkpoint file '$($resolution.CheckpointPath)' is not valid JSON: $($_.Exception.Message)" }
357
272
  }
358
273
 
359
274
  $requiredFields = @('objective', 'completed_steps', 'next_step', 'last_updated')
@@ -367,41 +282,51 @@ function Invoke-OrchestratorOutputValidation {
367
282
 
368
283
  if ($missingFields.Count -gt 0) {
369
284
  $missingList = ($missingFields -join ', ')
370
- return @{ Ok = $false; Message = "orchestrator hook: checkpoint file '$CheckpointPath' is missing required field(s): $missingList. Orchestrator must persist objective, completed_steps, next_step, and last_updated." }
285
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: checkpoint file '$($resolution.CheckpointPath)' is missing required field(s): $missingList. Orchestrator must persist objective, completed_steps, next_step, and last_updated." }
371
286
  }
372
287
 
373
288
  if ([string]::IsNullOrWhiteSpace([string]$checkpoint.objective)) {
374
- return @{ Ok = $false; Message = "orchestrator hook: checkpoint file '$CheckpointPath' has an empty 'objective' field; orchestrator must record the active objective." }
289
+ return @{ Ok = $false; Repairable = $true; Message = "orchestrator hook: checkpoint file '$($resolution.CheckpointPath)' has an empty 'objective' field; orchestrator must record the active objective." }
375
290
  }
376
291
 
377
292
  $humanInteraction = $null
378
293
  if ($checkpointProps -contains 'human_interaction') {
379
294
  $humanInteraction = $checkpoint.human_interaction
380
295
  }
381
- $hiResult = Test-HumanInteractionShape -HumanInteraction $humanInteraction
296
+ # A relative runbook_path is checked beneath the resolved root, not the process location.
297
+ $hiResult = Test-HumanInteractionShape -HumanInteraction $humanInteraction -FileExistsCheck {
298
+ param($Path) Test-OrchestratorOutputRunbookFile -Path (Resolve-OrchestratorOutputRunbookPath -WorktreeRoot $resolution.WorktreeRoot -RunbookPath $Path)
299
+ }
382
300
  if (-not $hiResult.Ok) {
383
- return @{ Ok = $false; Message = $hiResult.Message }
301
+ return @{ Ok = $false; Repairable = $hiResult.Repairable; Message = $hiResult.Message }
384
302
  }
385
303
 
386
304
  # Delegate to the portable routing-contract validation, dispatched on
387
305
  # ArtifactType. The optional RoutingInvoker seam lets tests inject a mock; the
388
306
  # default seam runs the in-process PowerShell validation and starts no
389
307
  # subprocess.
390
- $routingArgs = @{ CheckpointPath = $CheckpointPath; ArtifactType = $ArtifactType }
308
+ $routingArgs = @{ CheckpointPath = $resolution.CheckpointPath; ArtifactType = $ArtifactType }
391
309
  if ($PSBoundParameters.ContainsKey('RoutingInvoker') -and $null -ne $RoutingInvoker) {
392
310
  $routingArgs['Invoker'] = $RoutingInvoker
393
311
  }
394
312
  $routingResult = Invoke-RoutingContractValidation @routingArgs
395
313
  if ($routingResult.HasErrors) {
396
- # One subprocess call now covers both --require-complete and
397
- # --require-model-routing. Surface a model-routing gate failure under its
398
- # own block reason (its errors name model_routing_receipts or
399
- # complexity_assessments); otherwise fall back to the routing-contract
314
+ # The in-process validation (issue #475) covers both the completion check
315
+ # and the model-routing check in one call. Surface a model-routing gate
316
+ # failure under its own block reason (its errors name model_routing_receipts
317
+ # or complexity_assessments); otherwise fall back to the routing-contract
400
318
  # block reason for a generic completion/routing failure.
401
319
  if ($routingResult.ErrorText -match 'model_routing_receipts|complexity_assessments') {
402
- return @{ Ok = $false; Message = "MODEL_ROUTING_BLOCKED: $($routingResult.ErrorText)" }
320
+ return @{ Ok = $false; Repairable = $true; Message = "MODEL_ROUTING_BLOCKED: $($routingResult.ErrorText)" }
403
321
  }
404
- return @{ Ok = $false; Message = "ROUTING_CONTRACT_BLOCKED: $($routingResult.ErrorText)" }
322
+ return @{ Ok = $false; Repairable = $true; Message = "ROUTING_CONTRACT_BLOCKED: $($routingResult.ErrorText)" }
323
+ }
324
+
325
+ # Layer 2 (issue #840): the epic checkpoint only, after the routing dispatch passes,
326
+ # on the checkpoint text already read.
327
+ if ($ArtifactType -eq 'epic-orchestrator-state') {
328
+ $barrier = Get-OrchestratorOutputWaveBarrierDecision -CheckpointText $file.Content
329
+ if (-not $barrier.Ok) { return @{ Ok = $false; Repairable = $barrier.Repairable; Message = $barrier.Message } }
405
330
  }
406
331
 
407
332
  return @{ Ok = $true; Message = $null }
@@ -411,11 +336,11 @@ function Invoke-OrchestratorOutputValidation {
411
336
  if ($MyInvocation.InvocationName -eq '.') {
412
337
  return
413
338
  }
339
+ if ($script:HookDependencyGuardLoadFailed) { [Console]::Error.WriteLine('ORCHESTRATOR_CHECKPOINT_UNRESOLVED: hook-dependency-guard.ps1 failed to load; the gate fails closed.'); exit 2 }
340
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent SubagentStop -ReasonPrefix 'ORCHESTRATOR_CHECKPOINT_UNRESOLVED:'
341
+ if ($null -ne $dependencyDecision) { [Console]::Error.WriteLine($dependencyDecision.Reason); exit $dependencyDecision.ExitCode }
414
342
 
415
- $result = Invoke-OrchestratorOutputValidation -RawPayload $env:CLAUDE_HOOK_INPUT -CheckpointPath $CheckpointPath -ArtifactType $ArtifactType
416
- if (-not $result.Ok) {
417
- Write-Error $result.Message
418
- exit 1
419
- }
420
-
343
+ try { $result = Invoke-OrchestratorOutputValidation -CheckpointPath $CheckpointPath -ArtifactType $ArtifactType }
344
+ catch { Exit-SubagentStopBlock -Message ('ORCHESTRATOR_CHECKPOINT_UNRESOLVED: validator error: ' + ([string]$_.Exception.Message -split "`r?`n")[0] + '; the gate fails closed.' + "`n" + $script:SubagentStopReportAndHaltLine) }
345
+ if (-not $result.Ok) { Exit-SubagentStopBlock -Message $result.Message }
421
346
  exit 0
@@ -9,7 +9,7 @@
9
9
  structure requirements.
10
10
 
11
11
  Enforced checks:
12
- - hook payload is valid JSON and contains non-empty output,
12
+ - the SubagentStop envelope read from stdin carries a non-empty last_assistant_message,
13
13
  - output advertises `plan-path: <path>` (or supported equivalent forms),
14
14
  - output contains `PREFLIGHT: ALL CLEAR` or
15
15
  `PREFLIGHT: REVISIONS REQUIRED`,
@@ -22,17 +22,25 @@
22
22
  - the final phase contains QA-oriented work.
23
23
 
24
24
  .NOTES
25
- Reads the hook payload from CLAUDE_HOOK_INPUT as JSON. Exits 0 to allow
26
- termination; exits 1 with an error message to block. Filesystem reads go
27
- through Get-PlanFileContent so tests can mock the boundary without writing
28
- temporary files.
25
+ Reads the SubagentStop envelope from stdin through Read-SubagentStopEnvelope
26
+ (subagentstop-envelope.ps1); the assistant text comes from the helper's
27
+ AssistantText. A non-repairable condition blocks once: when StopHookActive is
28
+ set, the re-entered stop is allowed. Exits 0 to allow termination; a block is
29
+ written to stderr with exit 2 through Exit-SubagentStopBlock. The dependency
30
+ guard and the E1 helper are guarded loads: a failure blocks with exit 2,
31
+ naming the file. Filesystem reads go through Get-PlanFileContent so tests can
32
+ mock the boundary without writing temporary files.
33
+ An unhandled error during validation blocks with exit 2 and a reason (issue #928).
29
34
  #>
30
35
 
31
36
  [CmdletBinding()]
32
37
  param()
38
+ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot 'hook-dependency-guard.ps1') } catch { $script:HookDependencyGuardLoadFailed = $true }
33
39
 
34
40
  Set-StrictMode -Version Latest
41
+ try { . (Join-Path $PSScriptRoot 'subagentstop-envelope.ps1') } catch { Add-HookDependencyFailure -Name 'subagentstop-envelope.ps1' -ErrorRecord $_ }
35
42
  $ErrorActionPreference = 'Stop'
43
+ $script:SubagentStopReportAndHaltLine = 'This condition is not repairable by the subagent. Report it to the operator and halt.'
36
44
 
37
45
  function Get-PlanFileContent {
38
46
  [CmdletBinding()]
@@ -232,7 +240,7 @@ function Get-PlanStructureValidationReport {
232
240
  [OutputType([string[]])]
233
241
  param(
234
242
  [Parameter(Mandatory = $true)]
235
- [string[]] $Lines
243
+ [AllowEmptyString()] [string[]] $Lines
236
244
  )
237
245
 
238
246
  $phasePattern = '^### Phase (?<Phase>\d+)\s+—\s+(?<Title>.+)$'
@@ -243,12 +251,14 @@ function Get-PlanStructureValidationReport {
243
251
  $currentPhase = $null
244
252
  $expectedPhase = 0
245
253
  $foundTask = $false
254
+ $insidePhaseSection = $false
246
255
 
247
256
  for ($i = 0; $i -lt $Lines.Count; $i++) {
248
257
  $lineNumber = $i + 1
249
258
  $line = $Lines[$i]
250
259
 
251
260
  if ($line -match '^### Phase ') {
261
+ $insidePhaseSection = $true
252
262
  $phaseMatch = [regex]::Match($line, $phasePattern)
253
263
  if (-not $phaseMatch.Success) {
254
264
  $errors.Add("Line ${lineNumber}: phase heading must match `### Phase N — <Title>`.")
@@ -269,10 +279,12 @@ function Get-PlanStructureValidationReport {
269
279
  continue
270
280
  }
271
281
 
282
+ if ($line -match '^#{1,2} ') { $insidePhaseSection = $false; continue }
272
283
  if ($line -notmatch '^- \[') {
273
284
  continue
274
285
  }
275
286
 
287
+ if (-not $insidePhaseSection -and $line -notmatch $taskPattern) { continue }
276
288
  $foundTask = $true
277
289
  $taskMatch = [regex]::Match($line, $taskPattern)
278
290
  if (-not $taskMatch.Success) {
@@ -344,54 +356,74 @@ function Get-PlanStructureValidationReport {
344
356
  }
345
357
 
346
358
  function Invoke-PlannerOutputValidation {
359
+ <#
360
+ .SYNOPSIS
361
+ Reads the SubagentStop envelope, validates the assistant text, and applies the re-entry policy.
362
+ #>
347
363
  [CmdletBinding()]
348
364
  [OutputType([hashtable])]
349
365
  param(
350
- [string] $RawPayload
366
+ [AllowNull()] [AllowEmptyString()] [string] $InputText
351
367
  )
352
-
353
- if ([string]::IsNullOrWhiteSpace($RawPayload)) {
354
- return @{ Ok = $false; Message = 'atomic-planner hook: CLAUDE_HOOK_INPUT is empty; cannot validate planner output.' }
355
- }
356
-
357
- try {
358
- $payload = $RawPayload | ConvertFrom-Json -ErrorAction Stop
368
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent SubagentStop -ReasonPrefix 'validate-planner-output:'
369
+ if ($null -ne $dependencyDecision) { return $dependencyDecision }
370
+ $envelope = if ($PSBoundParameters.ContainsKey('InputText')) { Read-SubagentStopEnvelope -InputText $InputText } else { Read-SubagentStopEnvelope }
371
+ if (-not $envelope.IsValid) { $decision = @{ Ok = $false; Repairable = $false; Message = ('{0} payload anomaly - {1}' -f 'atomic-planner hook:', (Get-SubagentStopEnvelopeAnomalyReason -Anomaly $envelope.Anomaly)) } }
372
+ else {
373
+ $agentOutput = $envelope.AssistantText
374
+ $decision = Get-PlannerOutputBlockDecision -AgentOutput ([string]$agentOutput)
359
375
  }
360
- catch {
361
- return @{ Ok = $false; Message = "atomic-planner hook: failed to parse CLAUDE_HOOK_INPUT as JSON: $($_.Exception.Message)" }
376
+ if ($decision.Ok) { return @{ Ok = $true; Message = $null } }
377
+ if (-not $decision.Repairable) {
378
+ if ($envelope.StopHookActive) { return @{ Ok = $true; Message = $null } }
379
+ if (-not $decision.Message.Contains('to the operator and halt.')) { $decision.Message = $decision.Message + "`n" + $script:SubagentStopReportAndHaltLine }
362
380
  }
381
+ return @{ Ok = $false; Message = $decision.Message }
382
+ }
383
+
384
+ function Get-PlannerOutputBlockDecision {
385
+ <#
386
+ .SYNOPSIS
387
+ Applies the planner completion checks to the assistant text and returns the block decision.
388
+ .DESCRIPTION
389
+ Returns @{ Ok = $true; Message = $null } to allow, or a hashtable with Ok = $false, a
390
+ Repairable boolean, and Message. Every planner content check is repairable by the subagent.
391
+ #>
392
+ [CmdletBinding()]
393
+ [OutputType([hashtable])]
394
+ param(
395
+ [Parameter(Mandatory = $true)]
396
+ [AllowEmptyString()]
397
+ [string] $AgentOutput
398
+ )
363
399
 
364
- $agentOutput = $null
365
- if ($null -ne $payload.PSObject.Properties['output']) {
366
- $agentOutput = $payload.output
367
- }
368
400
  if ([string]::IsNullOrWhiteSpace($agentOutput)) {
369
- return @{ Ok = $false; Message = 'atomic-planner hook: agent output is empty; planner must return plan-path and final preflight signal.' }
401
+ return @{ Ok = $false; Repairable = $true; Message = 'atomic-planner hook: agent output is empty (last_assistant_message carries no text); planner must return plan-path and final preflight signal.' }
370
402
  }
371
403
 
372
404
  if (-not (Test-HasPreflightSignal -AgentOutput $agentOutput)) {
373
- return @{ Ok = $false; Message = 'atomic-planner hook: agent output must include `PREFLIGHT: ALL CLEAR` or `PREFLIGHT: REVISIONS REQUIRED`.' }
405
+ return @{ Ok = $false; Repairable = $true; Message = 'atomic-planner hook: agent output must include `PREFLIGHT: ALL CLEAR` or `PREFLIGHT: REVISIONS REQUIRED`.' }
374
406
  }
375
407
 
376
408
  $planPath = Get-PlanPathFromOutput -AgentOutput $agentOutput
377
409
  if ($null -eq $planPath) {
378
- return @{ Ok = $false; Message = 'atomic-planner hook: agent output does not advertise a plan-path. Planner must report `plan-path: <path>` per atomic-plan-contract.' }
410
+ return @{ Ok = $false; Repairable = $true; Message = 'atomic-planner hook: agent output does not advertise a plan-path. Planner must report `plan-path: <path>` per atomic-plan-contract.' }
379
411
  }
380
412
 
381
413
  $file = Get-PlanFileContent -Path $planPath
382
414
  if (-not $file.Exists) {
383
- return @{ Ok = $false; Message = "atomic-planner hook: planner advertised plan-path '$planPath' but no file exists at that location." }
415
+ return @{ Ok = $false; Repairable = $true; Message = "atomic-planner hook: planner advertised plan-path '$planPath' but no file exists at that location." }
384
416
  }
385
417
 
386
418
  $errors = @(Get-PlanStructureValidationReport -Lines $file.Lines)
387
419
  if ($errors.Count -gt 0) {
388
420
  $message = "atomic-planner hook: plan '$planPath' violates the atomic plan contract:`n - " + ($errors -join "`n - ")
389
- return @{ Ok = $false; Message = $message }
421
+ return @{ Ok = $false; Repairable = $true; Message = $message }
390
422
  }
391
423
 
392
424
  $review = Get-PlannerInternalReviewValidation -AgentOutput $agentOutput
393
425
  if (-not $review.Ok) {
394
- return @{ Ok = $false; Message = "atomic-planner hook: $($review.Message)" }
426
+ return @{ Ok = $false; Repairable = $true; Message = "atomic-planner hook: $($review.Message)" }
395
427
  }
396
428
 
397
429
  return @{ Ok = $true; Message = $null }
@@ -400,11 +432,11 @@ function Invoke-PlannerOutputValidation {
400
432
  if ($MyInvocation.InvocationName -eq '.') {
401
433
  return
402
434
  }
435
+ if ($script:HookDependencyGuardLoadFailed) { [Console]::Error.WriteLine('validate-planner-output: hook-dependency-guard.ps1 failed to load; the gate fails closed.'); exit 2 }
436
+ $dependencyDecision = Get-HookDependencyFailureDecision -HookEvent SubagentStop -ReasonPrefix 'validate-planner-output:'
437
+ if ($null -ne $dependencyDecision) { [Console]::Error.WriteLine($dependencyDecision.Reason); exit $dependencyDecision.ExitCode }
403
438
 
404
- $result = Invoke-PlannerOutputValidation -RawPayload $env:CLAUDE_HOOK_INPUT
405
- if (-not $result.Ok) {
406
- Write-Error $result.Message
407
- exit 1
408
- }
409
-
439
+ try { $result = Invoke-PlannerOutputValidation }
440
+ catch { Exit-SubagentStopBlock -Message ('validate-planner-output: validator error: ' + ([string]$_.Exception.Message -split "`r?`n")[0] + '; the gate fails closed.' + "`n" + $script:SubagentStopReportAndHaltLine) }
441
+ if (-not $result.Ok) { Exit-SubagentStopBlock -Message $result.Message }
410
442
  exit 0