@danmoisan/drm-copilot-mcp 1.1.23 → 1.1.25

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 (199) hide show
  1. package/out/mcp-server.js +13347 -7926
  2. package/package.json +1 -1
  3. package/resources/claude-customizations/.claude/agents/discovery-capability-sentinel.md +34 -0
  4. package/resources/claude-customizations/.claude/agents/legacy-parity-analyst.md +101 -43
  5. package/resources/claude-customizations/.claude/agents/migration-coverage-reviewer.md +107 -43
  6. package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +14 -0
  7. package/resources/claude-customizations/.claude/agents/parallel-planner.md +21 -1
  8. package/resources/claude-customizations/.claude/agents/pr-author.md +17 -1
  9. package/resources/claude-customizations/.claude/agents/requirements-reconciler.md +117 -42
  10. package/resources/claude-customizations/.claude/agents/runtime-characterization-analyst.md +103 -43
  11. package/resources/claude-customizations/.claude/hooks/discovery-capability-probe.ps1 +102 -0
  12. package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +69 -194
  13. package/resources/claude-customizations/.claude/hooks/enforce-discovery-planning-gate.ps1 +87 -0
  14. package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +7 -1
  15. package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +6 -3
  16. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +168 -48
  17. package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +1 -1
  18. package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +59 -20
  19. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +44 -3
  20. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +60 -4
  21. package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +4 -2
  22. package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +59 -20
  23. package/resources/claude-customizations/.claude/hooks/record-discovery-agent-start.ps1 +132 -0
  24. package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +56 -212
  25. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-rejection.ps1 +172 -0
  26. package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +9 -1
  27. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +9 -3
  28. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusAggregate.psm1 +154 -0
  29. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusCombination.psm1 +228 -0
  30. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +1 -0
  31. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +4 -2
  32. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +63 -16
  33. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +56 -1
  34. package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +4 -1
  35. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +5 -1
  36. package/resources/claude-customizations/.claude/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
  37. package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +25 -2
  38. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
  39. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
  40. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
  41. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
  42. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
  43. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
  44. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
  45. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
  46. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
  47. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
  48. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
  49. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
  50. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
  51. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
  52. package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +188 -341
  53. package/resources/claude-customizations/.claude/lib/worktree-resolution/HookTargetWorktree.psm1 +391 -0
  54. package/resources/claude-customizations/.claude/rules/orchestrator-state.md +1 -0
  55. package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +150 -18
  56. package/resources/claude-customizations/.claude/settings.json +32 -3
  57. package/resources/claude-customizations/.claude/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
  58. package/resources/claude-customizations/.claude/skills/characterize-runtime-behavior/SKILL.md +134 -0
  59. package/resources/claude-customizations/.claude/skills/discovery-behavior-reconciliation/SKILL.md +28 -47
  60. package/resources/claude-customizations/.claude/skills/discovery-coverage-ledger/SKILL.md +32 -48
  61. package/resources/claude-customizations/.claude/skills/discovery-parity-matrix/SKILL.md +28 -45
  62. package/resources/claude-customizations/.claude/skills/discovery-repo-inventory/SKILL.md +28 -62
  63. package/resources/claude-customizations/.claude/skills/discovery-runtime-characterization/SKILL.md +28 -45
  64. package/resources/claude-customizations/.claude/skills/discovery-validate-artifacts/SKILL.md +28 -61
  65. package/resources/claude-customizations/.claude/skills/discovery-workflow/SKILL.md +55 -139
  66. package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +24 -9
  67. package/resources/claude-customizations/.claude/skills/extract-feature-contract/SKILL.md +133 -0
  68. package/resources/claude-customizations/.claude/skills/generate-acceptance-scenarios/SKILL.md +132 -0
  69. package/resources/claude-customizations/.claude/skills/initialize-discovery-workspace/SKILL.md +131 -0
  70. package/resources/claude-customizations/.claude/skills/inventory-legacy-system/SKILL.md +131 -0
  71. package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +5 -4
  72. package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +21 -1
  73. package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +11 -1
  74. package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +34 -0
  75. package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +69 -6
  76. package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +15 -0
  77. package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +6 -0
  78. package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +21 -4
  79. package/resources/claude-customizations/.claude/skills/reconcile-parity-matrix/SKILL.md +131 -0
  80. package/resources/claude-customizations/.claude/skills/review-discovery-coverage/SKILL.md +129 -0
  81. package/resources/claude-customizations/.claude/skills/validate-discovery-artifacts/SKILL.md +127 -0
  82. package/resources/claude-customizations/config/blast-radius.json +6 -5
  83. package/resources/claude-customizations/config/orchestration-routing.json +61 -1
  84. package/resources/claude-customizations/pack-manifests/core.json +33 -1
  85. package/resources/codex-and-agents-customizations/.agents/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
  86. package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +7 -0
  87. package/resources/codex-and-agents-customizations/.agents/skills/characterize-runtime-behavior/SKILL.md +134 -0
  88. package/resources/codex-and-agents-customizations/.agents/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
  89. package/resources/codex-and-agents-customizations/.agents/skills/discovery-coverage-ledger/SKILL.md +50 -0
  90. package/resources/codex-and-agents-customizations/.agents/skills/discovery-parity-matrix/SKILL.md +46 -0
  91. package/resources/codex-and-agents-customizations/.agents/skills/discovery-repo-inventory/SKILL.md +46 -0
  92. package/resources/codex-and-agents-customizations/.agents/skills/discovery-runtime-characterization/SKILL.md +46 -0
  93. package/resources/codex-and-agents-customizations/.agents/skills/discovery-validate-artifacts/SKILL.md +46 -0
  94. package/resources/codex-and-agents-customizations/.agents/skills/discovery-workflow/SKILL.md +62 -0
  95. package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +24 -9
  96. package/resources/codex-and-agents-customizations/.agents/skills/extract-feature-contract/SKILL.md +133 -0
  97. package/resources/codex-and-agents-customizations/.agents/skills/generate-acceptance-scenarios/SKILL.md +132 -0
  98. package/resources/codex-and-agents-customizations/.agents/skills/initialize-discovery-workspace/SKILL.md +131 -0
  99. package/resources/codex-and-agents-customizations/.agents/skills/inventory-legacy-system/SKILL.md +131 -0
  100. package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +8 -0
  101. package/resources/codex-and-agents-customizations/.agents/skills/reconcile-parity-matrix/SKILL.md +131 -0
  102. package/resources/codex-and-agents-customizations/.agents/skills/review-discovery-coverage/SKILL.md +129 -0
  103. package/resources/codex-and-agents-customizations/.agents/skills/validate-discovery-artifacts/SKILL.md +127 -0
  104. package/resources/codex-and-agents-customizations/.codex/agents/discovery-capability-sentinel.toml +51 -0
  105. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c1.toml +148 -0
  106. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c2.toml +148 -0
  107. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3-elevated.toml +148 -0
  108. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3.toml +148 -0
  109. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c4.toml +148 -0
  110. package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst.toml +148 -0
  111. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c1.toml +154 -0
  112. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c2.toml +154 -0
  113. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3-elevated.toml +154 -0
  114. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3.toml +154 -0
  115. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c4.toml +154 -0
  116. package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer.toml +154 -0
  117. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c1.toml +164 -0
  118. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c2.toml +164 -0
  119. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3-elevated.toml +164 -0
  120. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3.toml +164 -0
  121. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c4.toml +164 -0
  122. package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler.toml +164 -0
  123. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c1.toml +150 -0
  124. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c2.toml +150 -0
  125. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3-elevated.toml +150 -0
  126. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3.toml +150 -0
  127. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c4.toml +150 -0
  128. package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst.toml +150 -0
  129. package/resources/codex-and-agents-customizations/.codex/config.toml +33 -3
  130. package/resources/codex-and-agents-customizations/.codex/hooks/codex-agent-profile-attestation.ps1 +22 -0
  131. package/resources/codex-and-agents-customizations/.codex/hooks/discovery-capability-probe.ps1 +97 -0
  132. package/resources/codex-and-agents-customizations/.codex/hooks/discovery-start-attestation.ps1 +171 -0
  133. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +4 -1
  134. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-artifact-gate.ps1 +187 -0
  135. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-planning-gate.ps1 +149 -0
  136. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +239 -2
  137. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -15
  138. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +54 -15
  139. package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +54 -15
  140. package/resources/codex-and-agents-customizations/.codex/hooks/hook-target-worktree.ps1 +369 -0
  141. package/resources/codex-and-agents-customizations/.codex/hooks/record-subagent-routing-attestation.ps1 +8 -0
  142. package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +22 -1
  143. package/resources/codex-and-agents-customizations/.codex/hooks/validate-discovery-agent-output.ps1 +178 -0
  144. package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +11 -1
  145. package/resources/codex-and-agents-customizations/pack-manifests/core.json +63 -1
  146. package/resources/config/orchestration-routing.json +61 -1
  147. package/resources/customizations/.github/agents/discovery-capability-sentinel.agent.md +34 -0
  148. package/resources/customizations/.github/agents/legacy-parity-analyst.agent.md +122 -0
  149. package/resources/customizations/.github/agents/migration-coverage-reviewer.agent.md +128 -0
  150. package/resources/customizations/.github/agents/requirements-reconciler.agent.md +138 -0
  151. package/resources/customizations/.github/agents/runtime-characterization-analyst.agent.md +124 -0
  152. package/resources/customizations/.github/hooks/discovery-agent-start.ps1 +139 -0
  153. package/resources/customizations/.github/hooks/discovery-agent-stop.ps1 +123 -0
  154. package/resources/customizations/.github/hooks/discovery-capability-probe.ps1 +98 -0
  155. package/resources/customizations/.github/hooks/discovery-planning.ps1 +91 -0
  156. package/resources/customizations/.github/hooks/discovery-pre-tool.ps1 +123 -0
  157. package/resources/customizations/.github/hooks/discovery.json +35 -0
  158. package/resources/customizations/.github/instructions/mermaid.instructions.md +85 -0
  159. package/resources/customizations/.github/prompts/execute-hard-lock.prompt.md +105 -0
  160. package/resources/customizations/.github/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
  161. package/resources/customizations/.github/skills/characterize-runtime-behavior/SKILL.md +134 -0
  162. package/resources/customizations/.github/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
  163. package/resources/customizations/.github/skills/discovery-coverage-ledger/SKILL.md +50 -0
  164. package/resources/customizations/.github/skills/discovery-parity-matrix/SKILL.md +46 -0
  165. package/resources/customizations/.github/skills/discovery-repo-inventory/SKILL.md +46 -0
  166. package/resources/customizations/.github/skills/discovery-runtime-characterization/SKILL.md +46 -0
  167. package/resources/customizations/.github/skills/discovery-validate-artifacts/SKILL.md +46 -0
  168. package/resources/customizations/.github/skills/discovery-workflow/SKILL.md +62 -0
  169. package/resources/customizations/.github/skills/execution-environment/SKILL.md +24 -9
  170. package/resources/customizations/.github/skills/extract-feature-contract/SKILL.md +133 -0
  171. package/resources/customizations/.github/skills/generate-acceptance-scenarios/SKILL.md +132 -0
  172. package/resources/customizations/.github/skills/initialize-discovery-workspace/SKILL.md +131 -0
  173. package/resources/customizations/.github/skills/inventory-legacy-system/SKILL.md +131 -0
  174. package/resources/customizations/.github/skills/reconcile-parity-matrix/SKILL.md +131 -0
  175. package/resources/customizations/.github/skills/review-discovery-coverage/SKILL.md +129 -0
  176. package/resources/customizations/.github/skills/validate-discovery-artifacts/SKILL.md +127 -0
  177. package/resources/lib/codex-routing/CodexDeployment.psm1 +5 -1
  178. package/resources/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
  179. package/resources/lib/codex-routing/CodexTopology.psm1 +25 -2
  180. package/resources/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
  181. package/resources/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
  182. package/resources/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
  183. package/resources/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
  184. package/resources/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
  185. package/resources/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
  186. package/resources/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
  187. package/resources/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
  188. package/resources/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
  189. package/resources/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
  190. package/resources/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
  191. package/resources/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
  192. package/resources/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
  193. package/resources/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
  194. package/resources/powershell/PoshQC/PoshQC.Parallel.psm1 +452 -0
  195. package/resources/powershell/PoshQC/PoshQC.Shard.psm1 +106 -8
  196. package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -0
  197. package/resources/powershell/PoshQC/PoshQC.psd1 +6 -1
  198. package/resources/powershell/PoshQC/PoshQC.psm1 +7 -1
  199. package/resources/powershell/PoshQC/README.md +20 -1
@@ -0,0 +1,391 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Pure target-worktree resolver for PreToolUse hooks (issue #1009).
4
+
5
+ .DESCRIPTION
6
+ Resolves the registered git worktree that a hook call acts on, from the first present
7
+ source in a strict order: (1) file-tool target paths, (2) command operand paths or a
8
+ branch the caller has already parsed, (3) the identity carried by an Agent delegation
9
+ prompt, and (4) the session root as the final fallback. Registered worktrees come only
10
+ from `git worktree list --porcelain` text returned by the injectable
11
+ WorktreeListInvoker seam, and a path resolves to its nearest enclosing registered
12
+ worktree (the longest normalized root prefix). Paths are compared after forward-slash
13
+ normalization with OrdinalIgnoreCase on every platform.
14
+
15
+ Known limitation: an 8.3 short-name path, or a path reached through a junction, that
16
+ differs textually from the long path git reports matches no registered worktree and
17
+ falls back to the session root.
18
+
19
+ The TARGET_WORKTREE_AMBIGUOUS and TARGET_WORKTREE_NOT_DERIVABLE literals are restated
20
+ inside Resolve-HookTargetWorktree rather than read from WorktreeResolution.psm1,
21
+ because this module imports nothing. A test pins both literals equal to the
22
+ WorktreeResolution.psm1 accessors, following the precedent of
23
+ validate-orchestrator-output-resolution.ps1.
24
+
25
+ .NOTES
26
+ Compatible with PowerShell 7+. No module dependency, no file read, no environment
27
+ read, and no clock read; the only process is the default WorktreeListInvoker body.
28
+ Functions reference no module-scope state, so the Codex twin
29
+ .codex/hooks/hook-target-worktree.ps1 carries byte-identical function text.
30
+ Mirrored byte-identically under extensions/drm-copilot/resources/claude-customizations/.
31
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
32
+ #>
33
+
34
+ Set-StrictMode -Version Latest
35
+ $ErrorActionPreference = 'Stop'
36
+
37
+ function ConvertTo-HookTargetWorktreeNormalizedPath {
38
+ <#
39
+ .SYNOPSIS
40
+ Normalize a path to forward slashes with repeated and trailing slashes removed.
41
+ .DESCRIPTION
42
+ Backslashes become forward slashes, runs of slashes collapse to one, and a trailing
43
+ slash is trimmed except from a bare root ('/' or a drive root such as 'C:/'). Blank
44
+ input returns an empty string. Case is preserved; callers compare with
45
+ OrdinalIgnoreCase.
46
+ .PARAMETER Path
47
+ The path to normalize.
48
+ #>
49
+ [CmdletBinding()]
50
+ [OutputType([string])]
51
+ param(
52
+ [AllowNull()] [AllowEmptyString()] [string] $Path
53
+ )
54
+
55
+ if ([string]::IsNullOrWhiteSpace($Path)) { return '' }
56
+ $normalized = ($Path.Trim() -replace '\\', '/') -replace '/{2,}', '/'
57
+ if ($normalized.Length -gt 1 -and $normalized.EndsWith('/') -and $normalized -notmatch '^[A-Za-z]:/$') {
58
+ $normalized = $normalized.Substring(0, $normalized.Length - 1)
59
+ }
60
+ return $normalized
61
+ }
62
+
63
+ function Get-HookTargetWorktreeRelativePath {
64
+ <#
65
+ .SYNOPSIS
66
+ Return a path relative to a worktree root, or $null when it lies outside the root.
67
+ .DESCRIPTION
68
+ Both values are normalized first. A path equal to the root returns an empty string;
69
+ a path under the root returns its forward-slash remainder; any other path, or a
70
+ blank value, returns $null.
71
+ .PARAMETER Path
72
+ The path to express relative to the root.
73
+ .PARAMETER WorktreeRoot
74
+ The worktree root.
75
+ #>
76
+ [CmdletBinding()]
77
+ [OutputType([string])]
78
+ param(
79
+ [AllowNull()] [AllowEmptyString()] [string] $Path,
80
+ [AllowNull()] [AllowEmptyString()] [string] $WorktreeRoot
81
+ )
82
+
83
+ $normalizedPath = ConvertTo-HookTargetWorktreeNormalizedPath -Path $Path
84
+ $normalizedRoot = ConvertTo-HookTargetWorktreeNormalizedPath -Path $WorktreeRoot
85
+ if ([string]::IsNullOrEmpty($normalizedPath) -or [string]::IsNullOrEmpty($normalizedRoot)) { return $null }
86
+ if ([string]::Equals($normalizedPath, $normalizedRoot, [System.StringComparison]::OrdinalIgnoreCase)) { return '' }
87
+ $prefix = if ($normalizedRoot.EndsWith('/')) { $normalizedRoot } else { $normalizedRoot + '/' }
88
+ if ($normalizedPath.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
89
+ return $normalizedPath.Substring($prefix.Length)
90
+ }
91
+ return $null
92
+ }
93
+
94
+ function Get-HookTargetWorktreeEnclosingRoot {
95
+ <#
96
+ .SYNOPSIS
97
+ Return the nearest root that encloses a path, or $null when none does.
98
+ .DESCRIPTION
99
+ A root encloses a path when the normalized path equals the normalized root or
100
+ starts with the root followed by a slash. When several roots enclose the path, the
101
+ longest one wins, so a worktree nested inside another wins for paths under it.
102
+ .PARAMETER Path
103
+ The path to place.
104
+ .PARAMETER Roots
105
+ The candidate worktree roots.
106
+ #>
107
+ [CmdletBinding()]
108
+ [OutputType([string])]
109
+ param(
110
+ [AllowNull()] [AllowEmptyString()] [string] $Path,
111
+ [AllowNull()] [AllowEmptyCollection()] [string[]] $Roots
112
+ )
113
+
114
+ $best = $null
115
+ foreach ($root in @($Roots)) {
116
+ $normalizedRoot = ConvertTo-HookTargetWorktreeNormalizedPath -Path $root
117
+ if ([string]::IsNullOrEmpty($normalizedRoot)) { continue }
118
+ $relative = Get-HookTargetWorktreeRelativePath -Path $Path -WorktreeRoot $normalizedRoot
119
+ if ($null -ne $relative -and ($null -eq $best -or $normalizedRoot.Length -gt $best.Length)) { $best = $normalizedRoot }
120
+ }
121
+ return $best
122
+ }
123
+
124
+ function Get-HookTargetWorktreeKey {
125
+ <#
126
+ .SYNOPSIS
127
+ Return the stable key '<sanitized leaf>-<8 hex>' that names a worktree root.
128
+ .DESCRIPTION
129
+ The hex part is the first four bytes of the SHA-256 of the normalized root with no
130
+ trailing slash, and the leaf is reduced to file-name-safe characters. The algorithm
131
+ equals the worktree-derived batch-budget session id, which prefixes 'worktree-'.
132
+ .PARAMETER WorktreeRoot
133
+ The worktree root to key.
134
+ #>
135
+ [CmdletBinding()]
136
+ [OutputType([string])]
137
+ param(
138
+ [Parameter(Mandatory)] [string] $WorktreeRoot
139
+ )
140
+
141
+ $normalizedRoot = (ConvertTo-HookTargetWorktreeNormalizedPath -Path $WorktreeRoot).TrimEnd('/')
142
+ $leaf = $normalizedRoot.Substring($normalizedRoot.LastIndexOf('/') + 1) -replace '[^A-Za-z0-9._-]', '_'
143
+ $hashBytes = [System.Security.Cryptography.SHA256]::HashData([System.Text.Encoding]::UTF8.GetBytes($normalizedRoot))
144
+ $shortHash = -join (@($hashBytes[0..3]) | ForEach-Object { $_.ToString('x2') })
145
+ return "$leaf-$shortHash"
146
+ }
147
+
148
+ function ConvertFrom-HookTargetWorktreePorcelain {
149
+ <#
150
+ .SYNOPSIS
151
+ Parse `git worktree list --porcelain` text into worktree records.
152
+ .DESCRIPTION
153
+ Emits one record per non-bare stanza with Root (normalized), Branch (the short
154
+ branch name, or $null when detached), Bare, and Prunable. A bare stanza is skipped.
155
+ Blank lines separate stanzas, and CRLF line endings are tolerated.
156
+ .PARAMETER Text
157
+ The porcelain text.
158
+ #>
159
+ [CmdletBinding()]
160
+ [OutputType([pscustomobject])]
161
+ param(
162
+ [AllowNull()] [AllowEmptyString()] [string] $Text
163
+ )
164
+
165
+ $records = [System.Collections.Generic.List[object]]::new()
166
+ $stanza = $null
167
+ foreach ($rawLine in @(([string] $Text -split "`r?`n") + '')) {
168
+ $line = $rawLine.Trim()
169
+ if ($line -eq '' -or $line.StartsWith('worktree ')) {
170
+ if ($null -ne $stanza -and -not $stanza.Bare) {
171
+ $records.Add([pscustomobject]@{ Root = $stanza.Root; Branch = $stanza.Branch; Bare = $false; Prunable = $stanza.Prunable })
172
+ }
173
+ $stanza = $null
174
+ if ($line -ne '') {
175
+ $stanza = @{ Root = (ConvertTo-HookTargetWorktreeNormalizedPath -Path $line.Substring(9)); Branch = $null; Bare = $false; Prunable = $false }
176
+ }
177
+ continue
178
+ }
179
+ if ($null -eq $stanza) { continue }
180
+ if ($line -eq 'bare') {
181
+ $stanza.Bare = $true
182
+ } elseif ($line.StartsWith('branch ')) {
183
+ $reference = $line.Substring(7).Trim()
184
+ $stanza.Branch = if ($reference.StartsWith('refs/heads/')) { $reference.Substring(11) } else { $reference }
185
+ } elseif ($line -eq 'detached') {
186
+ $stanza.Branch = $null
187
+ } elseif ($line -eq 'prunable' -or $line.StartsWith('prunable ')) {
188
+ $stanza.Prunable = $true
189
+ }
190
+ }
191
+ return $records.ToArray()
192
+ }
193
+
194
+ function Resolve-HookTargetWorktree {
195
+ <#
196
+ .SYNOPSIS
197
+ Resolve the registered worktree that a hook call acts on.
198
+ .DESCRIPTION
199
+ The first present source decides, in the order FilePath, CommandOperand (paths
200
+ and/or branch), DelegationIdentity, SessionRoot; a later source is never consulted.
201
+ Relative paths map to the session root without enumerating worktrees. An absolute
202
+ path maps to its nearest enclosing registered worktree, and a path inside none, or
203
+ an invoker that throws or returns no worktree, falls back to the session root.
204
+ Disagreeing components yield TARGET_WORKTREE_AMBIGUOUS; a present branch or
205
+ identity that names no worktree yields TARGET_WORKTREE_NOT_DERIVABLE and never the
206
+ session root. The result carries WorktreeRoot, Source, Ambiguous, ReasonCode,
207
+ Detail, and Candidates. The invoker runs at most once.
208
+ .PARAMETER SessionRoot
209
+ The absolute session root (the hook's root); the fallback root.
210
+ .PARAMETER FilePath
211
+ File-tool target paths.
212
+ .PARAMETER CommandOperandPath
213
+ Command operand paths parsed by the caller.
214
+ .PARAMETER CommandBranch
215
+ A command branch operand parsed by the caller.
216
+ .PARAMETER DelegationPrompt
217
+ Agent delegation prompt text; its 'branch:' label and canonical issue line are read.
218
+ .PARAMETER CheckpointIssueReader
219
+ Optional { param($WorktreeRoot) <issue number or $null> } used only for an issue identity.
220
+ .PARAMETER WorktreeListInvoker
221
+ { param($SessionRoot) <porcelain text> }; the default runs git worktree list --porcelain.
222
+ #>
223
+ [CmdletBinding()]
224
+ [OutputType([pscustomobject])]
225
+ param(
226
+ [Parameter(Mandatory)] [string] $SessionRoot,
227
+ [AllowNull()] [AllowEmptyCollection()] [string[]] $FilePath,
228
+ [AllowNull()] [AllowEmptyCollection()] [string[]] $CommandOperandPath,
229
+ [AllowNull()] [AllowEmptyString()] [string] $CommandBranch,
230
+ [AllowNull()] [AllowEmptyString()] [string] $DelegationPrompt,
231
+ [scriptblock] $CheckpointIssueReader,
232
+ [scriptblock] $WorktreeListInvoker = { param($SessionRoot) $porcelain = & git -C $SessionRoot worktree list --porcelain 2>$null; if ($LASTEXITCODE -ne 0) { return '' }; return ($porcelain -join "`n") }
233
+ )
234
+
235
+ $ambiguousCode = 'TARGET_WORKTREE_AMBIGUOUS'
236
+ $notDerivableCode = 'TARGET_WORKTREE_NOT_DERIVABLE'
237
+ $ignoreCase = [System.StringComparison]::OrdinalIgnoreCase
238
+ $absolutePattern = '^(/|[A-Za-z]:/)'
239
+ $session = ConvertTo-HookTargetWorktreeNormalizedPath -Path $SessionRoot
240
+
241
+ # Nested helpers are called by constant name and read this function's variables.
242
+ function Get-HookTargetResult {
243
+ param($Root, [string] $Source, $Code, [string] $Detail = '', [string[]] $Candidates = @())
244
+ [pscustomobject]@{
245
+ WorktreeRoot = $Root
246
+ Source = $Source
247
+ Ambiguous = ($Code -eq $ambiguousCode)
248
+ ReasonCode = $Code
249
+ Detail = $Detail
250
+ Candidates = [string[]] @($Candidates)
251
+ }
252
+ }
253
+ function Add-HookTargetDistinctValue {
254
+ param([System.Collections.Generic.List[string]] $List, [string] $Value)
255
+ foreach ($existing in $List) { if ([string]::Equals($existing, $Value, $ignoreCase)) { return } }
256
+ $List.Add($Value)
257
+ }
258
+ function Get-HookTargetRecordedIssue {
259
+ param([string] $Root)
260
+ try { $value = ([string] (& $CheckpointIssueReader $Root)).Trim().TrimStart('#') } catch { return $null }
261
+ if ($value -notmatch '^\d+$') { return $null }
262
+ $digits = $value.TrimStart('0')
263
+ return $(if ($digits -eq '') { '0' } else { $digits })
264
+ }
265
+ function Resolve-HookTargetPathSource {
266
+ param([string[]] $Paths, [string] $Source)
267
+ $resolved = [System.Collections.Generic.List[string]]::new()
268
+ $details = [System.Collections.Generic.List[string]]::new()
269
+ $fellBack = $false
270
+ foreach ($candidate in $Paths) {
271
+ $normalized = ConvertTo-HookTargetWorktreeNormalizedPath -Path $candidate
272
+ $root = $session
273
+ if ($normalized -notmatch $absolutePattern) {
274
+ $fellBack = $true
275
+ $details.Add("relative path '$normalized' maps to the session root")
276
+ } else {
277
+ $enclosing = Get-HookTargetWorktreeEnclosingRoot -Path $normalized -Roots $roots
278
+ if ($null -eq $enclosing) {
279
+ $fellBack = $true
280
+ $details.Add($(if ($listDetail) { $listDetail } else { "path '$normalized' is inside no registered worktree" }))
281
+ } else {
282
+ $root = $enclosing
283
+ }
284
+ }
285
+ Add-HookTargetDistinctValue -List $resolved -Value $root
286
+ }
287
+ if ($resolved.Count -gt 1) {
288
+ return (Get-HookTargetResult -Root $null -Source $Source -Code $ambiguousCode -Detail ('the paths resolve to {0} different worktrees' -f $resolved.Count) -Candidates $resolved.ToArray())
289
+ }
290
+ $decidedBy = if ($fellBack) { 'SessionRoot' } else { $Source }
291
+ return (Get-HookTargetResult -Root $resolved[0] -Source $decidedBy -Code $null -Detail ($details -join '; '))
292
+ }
293
+ function Resolve-HookTargetBranchSource {
294
+ param([string] $Branch, [string] $Source)
295
+ $matched = @($records | Where-Object { $null -ne $_.Branch -and [string]::Equals($_.Branch, $Branch, $ignoreCase) } | ForEach-Object { $_.Root })
296
+ if ($matched.Count -eq 0) { return (Get-HookTargetResult -Root $null -Source $Source -Code $notDerivableCode -Detail "branch '$Branch' is checked out in no registered worktree") }
297
+ if ($matched.Count -gt 1) { return (Get-HookTargetResult -Root $null -Source $Source -Code $ambiguousCode -Detail "branch '$Branch' is checked out in $($matched.Count) registered worktrees" -Candidates $matched) }
298
+ return (Get-HookTargetResult -Root $matched[0] -Source $Source -Code $null)
299
+ }
300
+
301
+ # Select the deciding source: the first present one in precedence order.
302
+ $filePaths = @($FilePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
303
+ $operandPaths = @($CommandOperandPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
304
+ $branchOperand = if ([string]::IsNullOrWhiteSpace($CommandBranch)) { '' } else { $CommandBranch.Trim() }
305
+ $branches = [System.Collections.Generic.List[string]]::new()
306
+ $issues = [System.Collections.Generic.List[string]]::new()
307
+ if ($filePaths.Count -gt 0) {
308
+ $source = 'FilePath'
309
+ } elseif ($operandPaths.Count -gt 0 -or $branchOperand) {
310
+ $source = 'CommandOperand'
311
+ } elseif (-not [string]::IsNullOrWhiteSpace($DelegationPrompt)) {
312
+ $source = 'DelegationIdentity'
313
+ foreach ($match in [regex]::Matches($DelegationPrompt, '(?i)\bbranch:\s*(?<name>[A-Za-z0-9][A-Za-z0-9._/-]*)')) {
314
+ Add-HookTargetDistinctValue -List $branches -Value $match.Groups['name'].Value.TrimEnd('.', '/')
315
+ }
316
+ foreach ($match in [regex]::Matches($DelegationPrompt, 'Canonical issue number for this feature is #?(?<number>\d+)')) {
317
+ $digits = $match.Groups['number'].Value.TrimStart('0')
318
+ Add-HookTargetDistinctValue -List $issues -Value $(if ($digits -eq '') { '0' } else { $digits })
319
+ }
320
+ } else {
321
+ return (Get-HookTargetResult -Root $session -Source 'SessionRoot' -Code $null -Detail 'no target source is present; the session root applies')
322
+ }
323
+
324
+ # Enumerate registered worktrees at most once, and only when the deciding source needs them.
325
+ $needsList = switch ($source) {
326
+ 'FilePath' { @($filePaths | Where-Object { (ConvertTo-HookTargetWorktreeNormalizedPath -Path $_) -match $absolutePattern }).Count -gt 0 }
327
+ 'CommandOperand' { [bool] $branchOperand -or @($operandPaths | Where-Object { (ConvertTo-HookTargetWorktreeNormalizedPath -Path $_) -match $absolutePattern }).Count -gt 0 }
328
+ default { ($branches.Count -eq 1 -and $issues.Count -le 1) -or ($branches.Count -eq 0 -and $issues.Count -eq 1 -and $null -ne $CheckpointIssueReader) }
329
+ }
330
+ $records = @()
331
+ $listDetail = ''
332
+ if ($needsList) {
333
+ try {
334
+ $records = @(ConvertFrom-HookTargetWorktreePorcelain -Text (@(& $WorktreeListInvoker $SessionRoot) -join "`n"))
335
+ if ($records.Count -eq 0) { $listDetail = 'the worktree list returned no registered worktree' }
336
+ } catch {
337
+ $records = @()
338
+ $listDetail = "the worktree list failed: $($_.Exception.Message)"
339
+ }
340
+ }
341
+ $roots = @($records | ForEach-Object { $_.Root })
342
+
343
+ if ($source -eq 'FilePath') { return (Resolve-HookTargetPathSource -Paths $filePaths -Source 'FilePath') }
344
+
345
+ if ($source -eq 'CommandOperand') {
346
+ $pathResult = if ($operandPaths.Count -gt 0) { Resolve-HookTargetPathSource -Paths $operandPaths -Source 'CommandOperand' } else { $null }
347
+ if (-not $branchOperand) { return $pathResult }
348
+ $branchResult = Resolve-HookTargetBranchSource -Branch $branchOperand -Source 'CommandOperand'
349
+ if ($null -eq $pathResult) { return $branchResult }
350
+ if ($pathResult.Ambiguous) { return $pathResult }
351
+ if ($null -ne $branchResult.ReasonCode) { return $branchResult }
352
+ if (-not [string]::Equals($pathResult.WorktreeRoot, $branchResult.WorktreeRoot, $ignoreCase)) {
353
+ return (Get-HookTargetResult -Root $null -Source 'CommandOperand' -Code $ambiguousCode -Detail "the operand paths and branch '$branchOperand' resolve to different worktrees" -Candidates @($pathResult.WorktreeRoot, $branchResult.WorktreeRoot))
354
+ }
355
+ return $branchResult
356
+ }
357
+
358
+ if ($branches.Count -gt 1) { return (Get-HookTargetResult -Root $null -Source $source -Code $ambiguousCode -Detail ('the prompt names {0} different branches: {1}' -f $branches.Count, ($branches -join ', '))) }
359
+ if ($issues.Count -gt 1) { return (Get-HookTargetResult -Root $null -Source $source -Code $ambiguousCode -Detail ('the prompt names {0} different issue numbers: {1}' -f $issues.Count, ($issues -join ', '))) }
360
+ if ($branches.Count -eq 1) {
361
+ $branchResult = Resolve-HookTargetBranchSource -Branch $branches[0] -Source $source
362
+ if ($null -ne $branchResult.ReasonCode -or $issues.Count -eq 0 -or $null -eq $CheckpointIssueReader) { return $branchResult }
363
+ $target = $branchResult.WorktreeRoot
364
+ $recorded = Get-HookTargetRecordedIssue -Root $target
365
+ if ($null -ne $recorded) {
366
+ if ($recorded -eq $issues[0]) { return $branchResult }
367
+ return (Get-HookTargetResult -Root $null -Source $source -Code $ambiguousCode -Detail "branch '$($branches[0])' places the call in a worktree whose checkpoint records issue $recorded, not issue $($issues[0])" -Candidates @($target))
368
+ }
369
+ $elsewhere = @($records | Where-Object { -not [string]::Equals($_.Root, $target, $ignoreCase) -and (Get-HookTargetRecordedIssue -Root $_.Root) -eq $issues[0] } | ForEach-Object { $_.Root })
370
+ if ($elsewhere.Count -gt 0) {
371
+ return (Get-HookTargetResult -Root $null -Source $source -Code $ambiguousCode -Detail "branch '$($branches[0])' and issue $($issues[0]) place the call in different worktrees" -Candidates (@($target) + $elsewhere))
372
+ }
373
+ return $branchResult
374
+ }
375
+ if ($issues.Count -eq 1) {
376
+ if ($null -eq $CheckpointIssueReader) { return (Get-HookTargetResult -Root $null -Source $source -Code $notDerivableCode -Detail "issue $($issues[0]) cannot be placed without a checkpoint issue reader") }
377
+ $matched = @($records | Where-Object { (Get-HookTargetRecordedIssue -Root $_.Root) -eq $issues[0] } | ForEach-Object { $_.Root })
378
+ if ($matched.Count -eq 0) { return (Get-HookTargetResult -Root $null -Source $source -Code $notDerivableCode -Detail "issue $($issues[0]) is recorded by no registered worktree") }
379
+ if ($matched.Count -gt 1) { return (Get-HookTargetResult -Root $null -Source $source -Code $ambiguousCode -Detail "issue $($issues[0]) is recorded by $($matched.Count) registered worktrees" -Candidates $matched) }
380
+ return (Get-HookTargetResult -Root $matched[0] -Source $source -Code $null)
381
+ }
382
+ return (Get-HookTargetResult -Root $null -Source $source -Code $notDerivableCode -Detail 'the prompt carries no branch: label and no canonical issue line')
383
+ }
384
+
385
+ Export-ModuleMember -Function `
386
+ Resolve-HookTargetWorktree, `
387
+ ConvertFrom-HookTargetWorktreePorcelain, `
388
+ Get-HookTargetWorktreeEnclosingRoot, `
389
+ ConvertTo-HookTargetWorktreeNormalizedPath, `
390
+ Get-HookTargetWorktreeRelativePath, `
391
+ Get-HookTargetWorktreeKey
@@ -329,3 +329,4 @@ Fail-closed behavior: any adoption error waives nothing, so each missing receipt
329
329
  does not currently validate standalone_merge_authorizations
330
330
  entries; adding that check is recorded as follow-up FU-3.
331
331
  - The `issue_adoption` invariants are enforced at completion by the routing-contract check in all three runtimes: `scripts/dev_tools/_orchestrator_state_issue_adoption.py` (Python authority), `extensions/drm-copilot/src/lib/validate/orchestrator-state-issue-adoption.ts`, and `.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1`, with byte-identical error strings pinned by the shared corpus under `tests/fixtures/orchestrator_state_issue_adoption/`. Rules 10 and 11 live in the record modules `scripts/dev_tools/_orchestrator_state_feature_folder_record.py`, `extensions/drm-copilot/src/lib/validate/orchestrator-state-feature-folder-record.ts`, and `.claude/lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1`. The `HEAD` probe for a waived `new_active_feature_folder` lives in the hook sibling `.claude/hooks/validate-orchestrator-output-feature-folder.ps1`, loaded by `.claude/hooks/validate-orchestrator-output.ps1`.
332
+ - The batch-budget hooks (`.claude/hooks/enforce-python-batch-budget.ps1`, `.claude/hooks/enforce-powershell-batch-budget.ps1`, and the Codex copies under `.codex/hooks/`) read the checkpoint of the target worktree that `Resolve-HookTargetWorktree` resolves for the edited file, so a coordinating session never needs a checkpoint at its own root for them.
@@ -13,6 +13,7 @@ paths:
13
13
  - "scripts/dev_tools/validate_orchestration_artifacts.py"
14
14
  - "extensions/drm-copilot/src/lib/validate/orchestration-artifacts.ts"
15
15
  - ".claude/hooks/enforce-epic-merge-gate.ps1"
16
+ - ".claude/hooks/validate-orchestrator-output-rejection.ps1"
16
17
  - ".claude/agents/parallel-orchestrator.md"
17
18
  - ".claude/agents/parallel-planner.md"
18
19
  - ".claude/skills/parallel-*/SKILL.md"
@@ -213,6 +214,38 @@ The wave-4 features — F6 (mutation protocol), F7 (enforcement hooks), and F8 (
213
214
 
214
215
  Integration-cost scheduling (issue #722) adds no `conflict_edges[].reason` member; the four members above are unchanged. The scheduling data travels in tolerated-not-validated fields: an edge may carry the extra fields `hard`, `cost`, and `benefit`, and the planner and orchestrator checkpoints may carry a `tolerated_overlaps` list. No validator reads these fields, and the Python validators and the TypeScript validator port accept them with zero errors.
215
216
 
217
+ ## Mutation Rejection Signal (issue #1012)
218
+
219
+ A parallel skill that rejects a mutation before any run checkpoint exists stops without writing state. Its final output then carries a rejection signal so that the completion stop hook `.claude/hooks/validate-orchestrator-output.ps1` can release the stop without demanding a checkpoint that the skill correctly did not write.
220
+
221
+ **Line grammar.** The signal is the last non-blank line of the final output, after trailing whitespace (including a carriage return) is trimmed, and it matches `^PARALLEL_MUTATION_REJECTED: (?<code>[a-z_]+)$` case-sensitively: the uppercase prefix, a colon, exactly one space, and one code from the table below, with no Markdown formatting (no backticks, no bold), no leading text, and no trailing text. A token on an earlier line, inside prose, or in any other form is not a signal. Where the run's slug is known, the line `parallel_slug: <slug>` precedes the signal.
222
+
223
+ **Closed code set.** The set is defined here and nowhere else:
224
+
225
+ | Code | Meaning |
226
+ | --- | --- |
227
+ | `kickoff_artifact_missing` | `/parallel-run` found no committed kickoff artifact for the run and delegated nothing. |
228
+ | `run_not_started` | The run is pending or not started, or no checkpoint tracks the named `parallel_slug`, so there is no running run to mutate. |
229
+ | `items_in_flight` | The mutation is rejected because an item is in flight: a close while any item is in flight, or a removal of an in-flight item without `--disposition`. |
230
+ | `unknown_item` | The removal target is not a live item: it is unknown, `withdrawn`, or `blocked`. |
231
+
232
+ **Output signal, not a schema member.** The signal is an output signal, not a checkpoint field, and it is not one of the nine enums owned by `## Enum Ownership (F6/F7/F8 consume, never extend)`; that table and its count are unchanged. No validator reads the signal and no checkpoint records it.
233
+
234
+ **Hook behavior.** The hook evaluates the signal only for `parallel-orchestrator-state` when the checkpoint resolution status is `NoTarget`:
235
+
236
+ | Condition | Result |
237
+ | --- | --- |
238
+ | parallel, `NoTarget`, valid signal, no contradicting state in any live worktree | allow, regardless of `stop_hook_active` |
239
+ | parallel, `NoTarget`, no signal or an invalid signal | block as before (repairable); the message gains a hint line naming the signal form and this section, and lists the allowed codes when the final line carries the prefix with an invalid code |
240
+ | parallel, `NoTarget`, valid signal, and a live worktree holds parallel checkpoint text that is unparseable, has a non-object root, is off-route, lacks a `parallel_slug`, or records the slug the output names | block as before |
241
+ | parallel, `Ambiguous` or `Rejected`, any signal | block as before |
242
+ | parallel, resolved checkpoint, any signal | full validation as before |
243
+ | `orchestrator-state` or `epic-orchestrator-state`, any signal | unchanged |
244
+
245
+ A signal never relaxes validation when a checkpoint resolves, and a path that wrote state validates fully. The parser, the state-absence scan, and the decision live in `.claude/hooks/validate-orchestrator-output-rejection.ps1`, whose code array mirrors the table above and is pinned to it by a parity test.
246
+
247
+ **Ownership.** The parallel skills consume these codes and never redefine them. Each mapped STOP path in `parallel-run`, `parallel-add`, `parallel-close`, and `parallel-remove` names only its own code and cites this section by heading; no skill restates the full set. A new code requires an amendment to this section, to the hook array, and to the parity test in the same change.
248
+
216
249
  ## F7 Seam
217
250
 
218
251
  The retrospective cohort-ordering invariant `PARALLEL_COHORT_BARRIER_VIOLATION` (design section 9, Layer 2) is F7's explicitly assigned addition to the orchestrator validator. It is NOT implemented here. The entry point of `scripts/dev_tools/validate_parallel_orchestrator_state.py` contains a clearly delimited, appendable helper-invocation block, marked with explicit begin and end comments that name F7 and the invariant token, so that F7's edit is one appended helper call with no reflow of existing code. The TypeScript core `extensions/drm-copilot/src/lib/validate/parallel-orchestrator-state-core.ts` carries the matching comment-delimited seam. Existing helper calls sit outside the block.
@@ -255,19 +288,45 @@ Three constraints bound the mandate-read exclusion:
255
288
  1. **The planner remains obliged to enumerate a genuine write explicitly.** An exclusion describes
256
289
  the default reading relationship, not a permanent ban. When an item's plan will actually write an
257
290
  excluded path, the planner appends that exact path to the declared radius after normalization.
258
- 2. **`quality-tiers.yml` stays a shared surface.** It is listed in both `shared_surfaces` and
259
- `mandate_reads`: the first governs what happens when an item really writes it, the second governs
260
- what happens when an item merely cites it.
291
+ 2. **`quality-tiers.yml` is an append-only registry and a mandate read (issue #1006).** Every item
292
+ that adds a module appends one entry to it, so both committed truth tables list it in
293
+ `conflict_tolerance.append_only_paths` and in `mandate_reads`, not in `shared_surfaces`. When an
294
+ item really writes it, the planner-appended or observed path contends on `path_overlap` at the
295
+ `append_only` weight and is never a hard edge; when an item merely cites it, the mandate-read
296
+ exclusion drops the citation. Separator-free token acceptance for it through the root-surface rule
297
+ (`config_root_surfaces`, `Get-ConfigRootSurface`) no longer applies, because that rule reads only
298
+ the separator-free entries of `shared_surfaces`; the planner append obligation in constraint 1 and
299
+ `detect_escaped_paths` in constraint 3 are the backstops.
261
300
  3. **`detect_escaped_paths` makes the read/write distinction exact at execution time.** The
262
301
  derivation heuristic reads intent from plan text and can be wrong in either direction; drift
263
302
  detection compares the declared radius against the paths a diff actually touched, so an item that
264
303
  wrote an excluded path is caught against observed evidence rather than against prose.
265
304
 
266
- The extractor additionally rejects four token shapes that were never write claims: a wildcard-free
305
+ The extractor additionally rejects five token shapes that were never write claims: a wildcard-free
267
306
  token whose final component names a directory rather than a file, a `docs/features/` glob whose
268
307
  wildcard occupies or truncates the feature-folder segment, a contract token carrying no ASCII
269
- letter, and a token containing a placeholder or interpolation marker. `artifacts/` is not a known
270
- top-level segment, so a bare `artifacts/**` subtree claim no longer satisfies the shape rules.
308
+ letter, a token containing a placeholder or interpolation marker, and a contract token that is a
309
+ default spreadsheet sheet name (`Sheet` or `Chart` followed by ASCII digits, compared
310
+ case-insensitively). `artifacts/` is not a known top-level segment, so a bare `artifacts/**`
311
+ subtree claim no longer satisfies the shape rules.
312
+
313
+ #### Generic contract-token rejection (issue #1006)
314
+
315
+ A spreadsheet application names a new worksheet or chart sheet with a default stem followed by a
316
+ number, so a spec that mentions `Sheet1` in an interface section names a worksheet, not an
317
+ exported symbol, schema, or CLI identifier. Two unrelated items that both named `Sheet1` received a
318
+ hard `contract_dependency` edge. The stems are a code constant, not a truth-table key:
319
+ `GENERIC_CONTRACT_STEMS` in `scripts/dev_tools/_blast_radius_token_shapes.py` and
320
+ `$script:GenericContractStem` in `.claude/lib/blast-radius/BlastRadiusTokenShape.psm1`, pinned equal
321
+ across Python and PowerShell by a Pester parity test that reads the Python source. Workbook and
322
+ object names such as `Book1` and `Table1` are a different class and are not rejected. The predicate
323
+ (`is_generic_contract_token`, `Test-GenericContractToken`) first requires an ASCII token, because a
324
+ .NET case-insensitive match folds the Kelvin sign and the long s to ASCII letters while Python
325
+ `re.ASCII` does not (the issue #903 precedent). It is applied at all six contract-filtering sites:
326
+ `extract_contract_identifiers`, `extract_write_intent_contracts`, and `normalize_declared_radius` in
327
+ Python, and `Get-ContractIdentifier`, `Get-WriteIntentSpecContract`, and
328
+ `Get-NormalizedDeclaredRadius` in PowerShell, so a previously recorded `Sheet1` is dropped on
329
+ recompute. TypeScript has no contract extractor, so no TypeScript parity applies.
271
330
 
272
331
  ### Mechanically-mergeable path class (issue #643)
273
332
 
@@ -454,12 +513,16 @@ narrowing. The only sanctioned mechanisms for relaxing contention are the config
454
513
  radius: editing a declared radius to suppress an edge remains prohibited.
455
514
 
456
515
  **Configuration.** `config/blast-radius.json` carries an optional `conflict_tolerance` object with
457
- tolerance_percent (integer, at least 0), `weights` (same_file, possible_overlap, append_only, and
458
- module, each an integer of at least 1), `band_durations` (C1 through C4, each an integer of at least
459
- 1), `default_band` (one of C1 through C4), and append_only_paths (a list of exact entries and `**`
460
- globs). The reader fails fast with an error naming the key for any other shape; booleans are
461
- rejected wherever an integer is required. An absent key means strict scheduling, identical to
462
- tolerance_percent 0.
516
+ tolerance_percent (integer, at least 0), `weights` (same_file, possible_overlap, append_only,
517
+ module, and contract, each an integer of at least 1), `band_durations` (C1 through C4, each an
518
+ integer of at least 1), `default_band` (one of C1 through C4), append_only_paths (a list of exact
519
+ entries and `**` globs), and the optional `combination_threshold_percent` (integer, at least 0;
520
+ default 400 when absent). The `contract` weight (issue #1013) is required and validated like the
521
+ other weights; its committed value is 8, and a destination overrides it, like every other member,
522
+ through `config/blast-radius.local.json`. `combination_threshold_percent` is committed as 400 and is
523
+ overridden through the same overlay. The reader fails fast with an error naming the key for any other
524
+ shape; booleans are rejected wherever an integer is required. An absent key means strict scheduling,
525
+ identical to tolerance_percent 0.
463
526
 
464
527
  **Edge rule.** For each unordered pair of items:
465
528
 
@@ -473,7 +536,14 @@ tolerance_percent 0.
473
536
  possible_overlap weight for a glob or directory-prefix overlap, and the module weight times the
474
537
  number of shared modules. The append_only_paths check runs before the same_file check (append-only
475
538
  precedence), so an append-only file stays low cost even when both items name it. Mergeable paths
476
- contribute 0 because they are excluded before enumeration.
539
+ contribute 0 because they are excluded before enumeration. Add the contract weight times the
540
+ number of distinct shared contract identifiers: the set the relation intersects to report
541
+ `contract_dependency`, each identifier counted once however often either radius lists it.
542
+ `contract_dependency` remains a hard class, so the contract term changes recorded edge costs and
543
+ combination eligibility and never an edge set. Invariant: at tolerance_percent 0 the edge set
544
+ equals strict scheduling for any `contract` weight, and at every tolerance the edge set with the
545
+ contract term equals the edge set without it; only the `cost` of edges whose pair shares a
546
+ contract differs.
477
547
  4. **Benefit** (integer, pairwise). The smaller of the two items' band durations; a missing band uses
478
548
  `default_band`. Running the pair concurrently saves at most one cohort step of the shorter item.
479
549
  5. **Rule.** The pair is an edge if and only if it conflicts and either it is hard or
@@ -485,8 +555,27 @@ least 1. A detected conflict that is not hard therefore has a cost of at least 1
485
555
  tolerance_percent 0 the inequality `cost * 100 > 0` always holds and the edge set equals the
486
556
  detected-conflict set exactly; cohort coloring of that set is identical to strict scheduling. At
487
557
  every tolerance an edge requires a conflict, so every edge implies a conflict and the edge set is
488
- always a subset of the detected-conflict set. Raising tolerance_percent never adds an edge, and the
489
- decision for (a, b) equals the decision for (b, a).
558
+ always a subset of the detected-conflict set. Raising tolerance_percent never turns a pairwise
559
+ decision into an edge, and the decision for (a, b) equals the decision for (b, a). Because the
560
+ aggregate cap below can promote a tolerated overlap, this monotonicity holds for the pairwise
561
+ decision only, not for the capped edge set.
562
+
563
+ **Aggregate cap (issue #1013).** The pairwise rule judges each pair alone, so the single entry point
564
+ in each runtime (`schedule_conflict_edges` through `select_capped_pairs` in
565
+ `scripts/dev_tools/_blast_radius_aggregate.py`, and Get-BlastRadiusConflictEdge through
566
+ Get-AggregateCappedPair in `.claude/lib/blast-radius/BlastRadiusAggregate.psm1`) applies a post-pass
567
+ that planners and `parallel-add` cannot skip. For item `i`, `total(i)` is the summed cost of its
568
+ remaining tolerated overlaps, and `i` is over its cap when
569
+ `total(i) * 100 > band_duration(i) * tolerance_percent` (integers only; a missing band reads as
570
+ `default_band`). While any item is over its cap, the smallest-key such item promotes the overlap with
571
+ the minimum key (benefit ascending, cost descending, a ascending, b ascending) to an edge carrying
572
+ `hard` false and the overlap's first reason, and the totals are recomputed. At tolerance_percent 0
573
+ nothing is tolerated, so the pass is a no-op and strict identity holds. A single tolerated overlap
574
+ never exceeds the cap, so the cap binds only for an item with two or more tolerated overlaps. The
575
+ capped scheduling output has five pinned set-level properties: the capped edges are a superset of
576
+ the pairwise edges and a subset of the detected conflicts; the output at tolerance 0 equals the
577
+ uncapped output; every item is within its cap after the pass; the output is invariant to input
578
+ order; and a second pass changes nothing.
490
579
 
491
580
  **Soft-overlap handling.** A detected, non-hard pair within tolerance is a tolerated overlap, not an
492
581
  edge. The planner records it in a tolerated_overlaps list on the planner checkpoint (and the
@@ -502,7 +591,39 @@ the checkpoint (`default_band` when absent), through the helper module
502
591
  yields an edge and the pair is not already a recorded edge. A tolerated pair whose observed overlap
503
592
  stays within tolerance halts neither item; one whose observed overlap exceeds tolerance, or becomes
504
593
  hard, is reported. An unevaluable peer radius still counts as an edge (fail closed). At
505
- tolerance_percent 0 the drift output equals strict drift output.
594
+ tolerance_percent 0 the drift output equals strict drift output. Drift detection stays pairwise: it
595
+ does not apply the aggregate cap, and a cap-promoted pair is a recorded edge, so drift never reports
596
+ it as newly conflicting.
597
+
598
+ **Telemetry (issue #1013).** Three fields are tolerated and not validated; no validator reads them:
599
+ `tolerated_overlaps[].observed_resolution` on the orchestrator checkpoint, `items[].combined_issue_nums`
600
+ on the planner and orchestrator checkpoints and the run manifest, and a top-level `combinations` list
601
+ on the planner checkpoint. `observed_resolution` has the shape `later_item` (integer), `conflicted`
602
+ (boolean), `conflict_detected_at` (timestamp or null), `resolved_at` (timestamp or null),
603
+ `resolution_minutes` (integer), and `recorded_at` (timestamp). The parallel orchestrator writes it for
604
+ the later-merging item of a tolerated pair. Attribution is non-exclusive: one conflict episode is
605
+ attributed to every tolerated peer that merged before it. Nothing reads `observed_resolution`, and no
606
+ weight changes automatically from it.
607
+
608
+ **Issue combination (issue #1013).** At planning time only, `find_combinations` in
609
+ `scripts/dev_tools/_blast_radius_combination.py` and Get-BlastRadiusCombination in
610
+ `.claude/lib/blast-radius/BlastRadiusCombination.psm1` find pairs that are cheaper to deliver as one
611
+ item. Combination is disabled when `conflict_tolerance` is absent or tolerance_percent is 0. A
612
+ conflicting pair is a candidate when its full reasons contain `contract_dependency` or it has a
613
+ same-file overlap (two equal concrete entries, after the mergeable exclusion, neither matching
614
+ append_only_paths), and it qualifies when `cost * 100 >= benefit * combination_threshold_percent`.
615
+ A contract-only pair is a candidate and qualifies when its contract-term cost meets the threshold;
616
+ one that does not qualify still serializes as a hard `contract_dependency` edge. Qualifying pairs
617
+ are grouped into connected components; each becomes one combination with ascending members, the
618
+ smallest member as primary, its pairs sorted by `(a, b)`, and `estimated_savings` equal to the sum
619
+ of its pair costs. There is no size cap on a combined item. The planner re-plans the primary
620
+ member's branch to cover every member, removes the other members from `items[]`, and records
621
+ `combined_issue_nums` and the `combinations` entry. No umbrella issue is created: the member issues
622
+ stay separate on GitHub, and the one PR closes every member through its author-asserted autoclose
623
+ line. A combined item remediates as one unit (one child, one `remediation_loop`, one PR) and never
624
+ splits back into separate PRs. A larger combined PR to review and a shared remediation budget are an
625
+ accepted trade; a destination tunes `combination_threshold_percent` to change how often items
626
+ combine.
506
627
 
507
628
  ### Write-intent extraction (issue #722)
508
629
 
@@ -674,8 +795,19 @@ keys are appended after the base keys. An overlay `version` must equal the base
674
795
  forbidden-glob guard (`**`, `docs/**`, `tests/**`) applies to the composed module map, including
675
796
  overlay-authored modules. A malformed overlay, a `version` mismatch, or a forbidden glob raises an
676
797
  error before any write, so `config/blast-radius.json` keeps its prior bytes. With no overlay
677
- present, the written file is byte-identical to the base document. The overlay cannot remove a
678
- shipped surface or a derived module.
798
+ present, the written file is byte-identical to the base document. A destination demotes a shipped
799
+ shared surface with the overlay directive `shared_surfaces_demote`, a list of strings (issue
800
+ #1006). It is applied after all other keys are composed: each named entry is removed from
801
+ `shared_surfaces`, with the order of the remaining entries preserved, and unioned into
802
+ `conflict_tolerance.append_only_paths`, base entries first. The directive is consumed, not emitted,
803
+ so the composed file stays within the truth-table key set. An entry that is already an append-only
804
+ path and not a shared surface is accepted as a no-op, so composing a composed result again yields
805
+ identical output. The directive fails closed, before any write, on a non-list value, a non-string
806
+ member, a composed document without `conflict_tolerance.append_only_paths` (no partial
807
+ `conflict_tolerance` is created), or an unknown entry that is neither a shared surface nor an
808
+ append-only path. Demotion never touches `modules`, so the forbidden-glob guard is unchanged. A
809
+ demoted entry that a `shared_surface_globs` pattern still matches stays shared through the glob; the
810
+ bundled glob list is empty. The overlay still cannot remove a derived module.
679
811
 
680
812
  ## Enforcement
681
813