@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
@@ -8,14 +8,26 @@
8
8
  operation. When the target file is a Python file (.py), it decides whether the
9
9
  change may proceed in direct mode.
10
10
 
11
- Direct mode counts distinct production Python paths per Codex session. The running
12
- set is persisted under .codex/state/python-batch-budget.<session_id>.json, and
13
- repeated edits to the same file are counted once. The 4th distinct production path
14
- is denied with a PYTHON_LARGE_PATH_REQUIRED reason that instructs the caller to
15
- route the change through .codex/prompts/orchestrate-work.md.
16
-
17
- The orchestrated large path is detected from
18
- <root>/artifacts/orchestration/orchestrator-state.json. The selected route is the
11
+ Direct mode counts distinct production Python paths per Codex session and per
12
+ resolved target worktree, and repeated edits to the same file are counted once. The
13
+ 4th distinct production path in one worktree is denied with a
14
+ PYTHON_LARGE_PATH_REQUIRED reason that instructs the caller to route the change
15
+ through .codex/prompts/orchestrate-work.md.
16
+
17
+ Each evaluated path is placed with Resolve-HookTargetWorktree from the dot-sourced
18
+ hook-target-worktree.ps1. An absolute path resolves to its nearest enclosing
19
+ registered git worktree; a relative path, a path inside no registered worktree, and
20
+ an ambiguous result use the session root. The route is read from
21
+ artifacts/orchestration/orchestrator-state.json in the resolved target worktree.
22
+ The running set is kept under the session root, in
23
+ .codex/state/python-batch-budget.<session_id>.json when the resolved target worktree
24
+ is the session root and in .codex/state/python-batch-budget.<session_id>.<worktree key>.json
25
+ otherwise; no state is written under another worktree. A path under the root-level
26
+ artifacts/ directory of the resolved target worktree is never counted and writes no
27
+ state. No containment filter applies: a path outside every registered worktree is
28
+ counted against the session root, as before.
29
+
30
+ The selected route is the
19
31
  route_id value when that key is present, otherwise the path_selected value, and it
20
32
  is usable only as a non-blank string. When the selected route is large,
21
33
  remediation, or preparation and the checkpoint is not terminal (next_step is not
@@ -36,9 +48,12 @@
36
48
  hookSpecificOutput.permissionDecision = 'deny' and exits 0. Files already counted
37
49
  are always allowed through.
38
50
 
39
- Known limitation: a stale non-terminal large-path checkpoint left at the root
40
- exempts a later direct-mode session at that root. Orchestrator checkpoint hygiene
41
- (issue #673) moves foreign checkpoints aside before a new run.
51
+ Known limitations: an 8.3 short-name path, or a path reached through a junction,
52
+ that differs textually from the long path git reports matches no registered
53
+ worktree and falls back to the session root. A stale non-terminal large-path
54
+ checkpoint in a worktree exempts a later direct-mode session editing that worktree;
55
+ orchestrator checkpoint hygiene (issue #673) moves foreign checkpoints aside before
56
+ a new run.
42
57
 
43
58
  .NOTES
44
59
  Compatible with PowerShell 7+.
@@ -51,6 +66,9 @@ $script:HookDependencyGuardLoadFailed = $false; try { . (Join-Path $PSScriptRoot
51
66
  # mapping for every tool name the ^(apply_patch|Edit|Write)$ matcher admits.
52
67
  try { . (Join-Path $PSScriptRoot 'codex-pretooluse-file-mapping.ps1') } catch { Add-HookDependencyFailure -Name 'codex-pretooluse-file-mapping.ps1' -ErrorRecord $_ }
53
68
 
69
+ # Shared target-worktree resolver; the Codex twin of HookTargetWorktree.psm1.
70
+ try { . (Join-Path $PSScriptRoot 'hook-target-worktree.ps1') } catch { Add-HookDependencyFailure -Name 'hook-target-worktree.ps1' -ErrorRecord $_ }
71
+
54
72
  # Shared route helpers; this file is byte-identical to the Claude runtime copy.
55
73
  $script:PythonBatchBudgetRouteImportFailure = $null; try { . (Join-Path $PSScriptRoot 'enforce-batch-budget-route.ps1') } catch { $script:PythonBatchBudgetRouteImportFailure = 'enforce-batch-budget-route.ps1' }
56
74
 
@@ -197,7 +215,8 @@ function Invoke-PythonBatchBudgetHook {
197
215
  [scriptblock] $WriteState = {
198
216
  param([string] $Path, [System.Collections.IDictionary] $State)
199
217
  $State | ConvertTo-Json -Depth 5 | Set-Content -Path $Path -Encoding UTF8
200
- }
218
+ },
219
+ [scriptblock] $WorktreeListInvoker
201
220
  )
202
221
 
203
222
  if (-not $ToolInputRaw) {
@@ -221,12 +240,31 @@ function Invoke-PythonBatchBudgetHook {
221
240
  }
222
241
  if ($script:PythonBatchBudgetRouteImportFailure) { return Get-PythonBatchBudgetBlockDecision -Reason "PYTHON_LARGE_PATH_REQUIRED: the route helper '$($script:PythonBatchBudgetRouteImportFailure)' failed to load, so the batch budget for '$normalized' cannot be evaluated; the gate fails closed." }
223
242
 
243
+ # The path is evaluated against the registered worktree that holds it; the session root
244
+ # is the fallback for a relative path, a path in no registered worktree, and an ambiguous result.
245
+ $resolverArguments = @{ SessionRoot = $Root; FilePath = @($filePath) }
246
+ if ($PSBoundParameters.ContainsKey('WorktreeListInvoker')) { $resolverArguments['WorktreeListInvoker'] = $WorktreeListInvoker }
247
+ $resolution = Resolve-HookTargetWorktree @resolverArguments
248
+ $targetRoot = $Root
249
+ $isSessionRoot = $true
250
+ if (-not $resolution.Ambiguous -and $null -ne $resolution.WorktreeRoot -and -not [string]::Equals($resolution.WorktreeRoot, (ConvertTo-HookTargetWorktreeNormalizedPath -Path $Root), [System.StringComparison]::OrdinalIgnoreCase)) {
251
+ $targetRoot = $resolution.WorktreeRoot
252
+ $isSessionRoot = $false
253
+ }
254
+
255
+ # The root-level artifacts/ directory is gitignored scratch, never a production file.
256
+ $relativePath = if ($normalized -match '^(/|[A-Za-z]:/)') { Get-HookTargetWorktreeRelativePath -Path $normalized -WorktreeRoot $targetRoot } else { $normalized }
257
+ if ($null -ne $relativePath -and $relativePath.StartsWith('artifacts/', [System.StringComparison]::OrdinalIgnoreCase)) {
258
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
259
+ }
260
+
224
261
  $stateDir = Join-Path -Path $Root -ChildPath '.codex/state'
225
- $stateFile = Join-Path -Path $stateDir -ChildPath ("python-batch-budget.$SessionId.json")
262
+ $stateLeaf = if ($isSessionRoot) { "python-batch-budget.$SessionId.json" } else { "python-batch-budget.$SessionId.$(Get-HookTargetWorktreeKey -WorktreeRoot $targetRoot).json" }
263
+ $stateFile = Join-Path -Path $stateDir -ChildPath $stateLeaf
226
264
 
227
265
  # The checkpoint is read before any state operation, so the large path neither
228
266
  # creates the state directory nor reads or writes the state file.
229
- $checkpointPath = Join-Path -Path $Root -ChildPath 'artifacts/orchestration/orchestrator-state.json'
267
+ $checkpointPath = Join-Path -Path $targetRoot -ChildPath 'artifacts/orchestration/orchestrator-state.json'
230
268
  $checkpointText = ''
231
269
  try {
232
270
  $checkpointText = [string](& $ReadCheckpoint $checkpointPath)
@@ -282,7 +320,8 @@ function Invoke-PythonBatchBudgetCodexEntryPoint {
282
320
  .PARAMETER PayloadRaw
283
321
  Raw PreToolUse payload text.
284
322
  .PARAMETER RepositoryRoot
285
- Root used to locate the Codex state directory and the orchestrator checkpoint.
323
+ Session root: it locates the Codex state directory and is the fallback root. The
324
+ orchestrator checkpoint comes from the resolved target worktree of each path.
286
325
  .PARAMETER HookSeams
287
326
  Optional seam overrides splatted into Invoke-PythonBatchBudgetHook.
288
327
  #>
@@ -0,0 +1,369 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Codex twin of the HookTargetWorktree target-worktree resolver functions (issue #1009).
4
+
5
+ .DESCRIPTION
6
+ Source: .claude/lib/worktree-resolution/HookTargetWorktree.psm1. This file carries the
7
+ six functions of that module with byte-identical function text, because Codex hooks
8
+ cannot import .claude/lib in a consumer repository: a consumer receives the .codex
9
+ runtime without the Claude library. tests/scripts/codex-hooks/hook-target-worktree.Tests.ps1
10
+ holds the function names and each function's text equal to the module and runs the
11
+ shared resolver behavior rows against this file.
12
+
13
+ Functions-only: the file has no entry point and no statement outside the function
14
+ definitions. It is dot-sourced by .codex/hooks/enforce-python-batch-budget.ps1 and
15
+ .codex/hooks/enforce-powershell-batch-budget.ps1. Edit the module first, then copy the
16
+ changed function text here.
17
+
18
+ .NOTES
19
+ Compatible with PowerShell 7+. Mirrored byte-identically under
20
+ extensions/drm-copilot/resources/codex-and-agents-customizations/.
21
+ #>
22
+
23
+ function ConvertTo-HookTargetWorktreeNormalizedPath {
24
+ <#
25
+ .SYNOPSIS
26
+ Normalize a path to forward slashes with repeated and trailing slashes removed.
27
+ .DESCRIPTION
28
+ Backslashes become forward slashes, runs of slashes collapse to one, and a trailing
29
+ slash is trimmed except from a bare root ('/' or a drive root such as 'C:/'). Blank
30
+ input returns an empty string. Case is preserved; callers compare with
31
+ OrdinalIgnoreCase.
32
+ .PARAMETER Path
33
+ The path to normalize.
34
+ #>
35
+ [CmdletBinding()]
36
+ [OutputType([string])]
37
+ param(
38
+ [AllowNull()] [AllowEmptyString()] [string] $Path
39
+ )
40
+
41
+ if ([string]::IsNullOrWhiteSpace($Path)) { return '' }
42
+ $normalized = ($Path.Trim() -replace '\\', '/') -replace '/{2,}', '/'
43
+ if ($normalized.Length -gt 1 -and $normalized.EndsWith('/') -and $normalized -notmatch '^[A-Za-z]:/$') {
44
+ $normalized = $normalized.Substring(0, $normalized.Length - 1)
45
+ }
46
+ return $normalized
47
+ }
48
+
49
+ function Get-HookTargetWorktreeRelativePath {
50
+ <#
51
+ .SYNOPSIS
52
+ Return a path relative to a worktree root, or $null when it lies outside the root.
53
+ .DESCRIPTION
54
+ Both values are normalized first. A path equal to the root returns an empty string;
55
+ a path under the root returns its forward-slash remainder; any other path, or a
56
+ blank value, returns $null.
57
+ .PARAMETER Path
58
+ The path to express relative to the root.
59
+ .PARAMETER WorktreeRoot
60
+ The worktree root.
61
+ #>
62
+ [CmdletBinding()]
63
+ [OutputType([string])]
64
+ param(
65
+ [AllowNull()] [AllowEmptyString()] [string] $Path,
66
+ [AllowNull()] [AllowEmptyString()] [string] $WorktreeRoot
67
+ )
68
+
69
+ $normalizedPath = ConvertTo-HookTargetWorktreeNormalizedPath -Path $Path
70
+ $normalizedRoot = ConvertTo-HookTargetWorktreeNormalizedPath -Path $WorktreeRoot
71
+ if ([string]::IsNullOrEmpty($normalizedPath) -or [string]::IsNullOrEmpty($normalizedRoot)) { return $null }
72
+ if ([string]::Equals($normalizedPath, $normalizedRoot, [System.StringComparison]::OrdinalIgnoreCase)) { return '' }
73
+ $prefix = if ($normalizedRoot.EndsWith('/')) { $normalizedRoot } else { $normalizedRoot + '/' }
74
+ if ($normalizedPath.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
75
+ return $normalizedPath.Substring($prefix.Length)
76
+ }
77
+ return $null
78
+ }
79
+
80
+ function Get-HookTargetWorktreeEnclosingRoot {
81
+ <#
82
+ .SYNOPSIS
83
+ Return the nearest root that encloses a path, or $null when none does.
84
+ .DESCRIPTION
85
+ A root encloses a path when the normalized path equals the normalized root or
86
+ starts with the root followed by a slash. When several roots enclose the path, the
87
+ longest one wins, so a worktree nested inside another wins for paths under it.
88
+ .PARAMETER Path
89
+ The path to place.
90
+ .PARAMETER Roots
91
+ The candidate worktree roots.
92
+ #>
93
+ [CmdletBinding()]
94
+ [OutputType([string])]
95
+ param(
96
+ [AllowNull()] [AllowEmptyString()] [string] $Path,
97
+ [AllowNull()] [AllowEmptyCollection()] [string[]] $Roots
98
+ )
99
+
100
+ $best = $null
101
+ foreach ($root in @($Roots)) {
102
+ $normalizedRoot = ConvertTo-HookTargetWorktreeNormalizedPath -Path $root
103
+ if ([string]::IsNullOrEmpty($normalizedRoot)) { continue }
104
+ $relative = Get-HookTargetWorktreeRelativePath -Path $Path -WorktreeRoot $normalizedRoot
105
+ if ($null -ne $relative -and ($null -eq $best -or $normalizedRoot.Length -gt $best.Length)) { $best = $normalizedRoot }
106
+ }
107
+ return $best
108
+ }
109
+
110
+ function Get-HookTargetWorktreeKey {
111
+ <#
112
+ .SYNOPSIS
113
+ Return the stable key '<sanitized leaf>-<8 hex>' that names a worktree root.
114
+ .DESCRIPTION
115
+ The hex part is the first four bytes of the SHA-256 of the normalized root with no
116
+ trailing slash, and the leaf is reduced to file-name-safe characters. The algorithm
117
+ equals the worktree-derived batch-budget session id, which prefixes 'worktree-'.
118
+ .PARAMETER WorktreeRoot
119
+ The worktree root to key.
120
+ #>
121
+ [CmdletBinding()]
122
+ [OutputType([string])]
123
+ param(
124
+ [Parameter(Mandatory)] [string] $WorktreeRoot
125
+ )
126
+
127
+ $normalizedRoot = (ConvertTo-HookTargetWorktreeNormalizedPath -Path $WorktreeRoot).TrimEnd('/')
128
+ $leaf = $normalizedRoot.Substring($normalizedRoot.LastIndexOf('/') + 1) -replace '[^A-Za-z0-9._-]', '_'
129
+ $hashBytes = [System.Security.Cryptography.SHA256]::HashData([System.Text.Encoding]::UTF8.GetBytes($normalizedRoot))
130
+ $shortHash = -join (@($hashBytes[0..3]) | ForEach-Object { $_.ToString('x2') })
131
+ return "$leaf-$shortHash"
132
+ }
133
+
134
+ function ConvertFrom-HookTargetWorktreePorcelain {
135
+ <#
136
+ .SYNOPSIS
137
+ Parse `git worktree list --porcelain` text into worktree records.
138
+ .DESCRIPTION
139
+ Emits one record per non-bare stanza with Root (normalized), Branch (the short
140
+ branch name, or $null when detached), Bare, and Prunable. A bare stanza is skipped.
141
+ Blank lines separate stanzas, and CRLF line endings are tolerated.
142
+ .PARAMETER Text
143
+ The porcelain text.
144
+ #>
145
+ [CmdletBinding()]
146
+ [OutputType([pscustomobject])]
147
+ param(
148
+ [AllowNull()] [AllowEmptyString()] [string] $Text
149
+ )
150
+
151
+ $records = [System.Collections.Generic.List[object]]::new()
152
+ $stanza = $null
153
+ foreach ($rawLine in @(([string] $Text -split "`r?`n") + '')) {
154
+ $line = $rawLine.Trim()
155
+ if ($line -eq '' -or $line.StartsWith('worktree ')) {
156
+ if ($null -ne $stanza -and -not $stanza.Bare) {
157
+ $records.Add([pscustomobject]@{ Root = $stanza.Root; Branch = $stanza.Branch; Bare = $false; Prunable = $stanza.Prunable })
158
+ }
159
+ $stanza = $null
160
+ if ($line -ne '') {
161
+ $stanza = @{ Root = (ConvertTo-HookTargetWorktreeNormalizedPath -Path $line.Substring(9)); Branch = $null; Bare = $false; Prunable = $false }
162
+ }
163
+ continue
164
+ }
165
+ if ($null -eq $stanza) { continue }
166
+ if ($line -eq 'bare') {
167
+ $stanza.Bare = $true
168
+ } elseif ($line.StartsWith('branch ')) {
169
+ $reference = $line.Substring(7).Trim()
170
+ $stanza.Branch = if ($reference.StartsWith('refs/heads/')) { $reference.Substring(11) } else { $reference }
171
+ } elseif ($line -eq 'detached') {
172
+ $stanza.Branch = $null
173
+ } elseif ($line -eq 'prunable' -or $line.StartsWith('prunable ')) {
174
+ $stanza.Prunable = $true
175
+ }
176
+ }
177
+ return $records.ToArray()
178
+ }
179
+
180
+ function Resolve-HookTargetWorktree {
181
+ <#
182
+ .SYNOPSIS
183
+ Resolve the registered worktree that a hook call acts on.
184
+ .DESCRIPTION
185
+ The first present source decides, in the order FilePath, CommandOperand (paths
186
+ and/or branch), DelegationIdentity, SessionRoot; a later source is never consulted.
187
+ Relative paths map to the session root without enumerating worktrees. An absolute
188
+ path maps to its nearest enclosing registered worktree, and a path inside none, or
189
+ an invoker that throws or returns no worktree, falls back to the session root.
190
+ Disagreeing components yield TARGET_WORKTREE_AMBIGUOUS; a present branch or
191
+ identity that names no worktree yields TARGET_WORKTREE_NOT_DERIVABLE and never the
192
+ session root. The result carries WorktreeRoot, Source, Ambiguous, ReasonCode,
193
+ Detail, and Candidates. The invoker runs at most once.
194
+ .PARAMETER SessionRoot
195
+ The absolute session root (the hook's root); the fallback root.
196
+ .PARAMETER FilePath
197
+ File-tool target paths.
198
+ .PARAMETER CommandOperandPath
199
+ Command operand paths parsed by the caller.
200
+ .PARAMETER CommandBranch
201
+ A command branch operand parsed by the caller.
202
+ .PARAMETER DelegationPrompt
203
+ Agent delegation prompt text; its 'branch:' label and canonical issue line are read.
204
+ .PARAMETER CheckpointIssueReader
205
+ Optional { param($WorktreeRoot) <issue number or $null> } used only for an issue identity.
206
+ .PARAMETER WorktreeListInvoker
207
+ { param($SessionRoot) <porcelain text> }; the default runs git worktree list --porcelain.
208
+ #>
209
+ [CmdletBinding()]
210
+ [OutputType([pscustomobject])]
211
+ param(
212
+ [Parameter(Mandatory)] [string] $SessionRoot,
213
+ [AllowNull()] [AllowEmptyCollection()] [string[]] $FilePath,
214
+ [AllowNull()] [AllowEmptyCollection()] [string[]] $CommandOperandPath,
215
+ [AllowNull()] [AllowEmptyString()] [string] $CommandBranch,
216
+ [AllowNull()] [AllowEmptyString()] [string] $DelegationPrompt,
217
+ [scriptblock] $CheckpointIssueReader,
218
+ [scriptblock] $WorktreeListInvoker = { param($SessionRoot) $porcelain = & git -C $SessionRoot worktree list --porcelain 2>$null; if ($LASTEXITCODE -ne 0) { return '' }; return ($porcelain -join "`n") }
219
+ )
220
+
221
+ $ambiguousCode = 'TARGET_WORKTREE_AMBIGUOUS'
222
+ $notDerivableCode = 'TARGET_WORKTREE_NOT_DERIVABLE'
223
+ $ignoreCase = [System.StringComparison]::OrdinalIgnoreCase
224
+ $absolutePattern = '^(/|[A-Za-z]:/)'
225
+ $session = ConvertTo-HookTargetWorktreeNormalizedPath -Path $SessionRoot
226
+
227
+ # Nested helpers are called by constant name and read this function's variables.
228
+ function Get-HookTargetResult {
229
+ param($Root, [string] $Source, $Code, [string] $Detail = '', [string[]] $Candidates = @())
230
+ [pscustomobject]@{
231
+ WorktreeRoot = $Root
232
+ Source = $Source
233
+ Ambiguous = ($Code -eq $ambiguousCode)
234
+ ReasonCode = $Code
235
+ Detail = $Detail
236
+ Candidates = [string[]] @($Candidates)
237
+ }
238
+ }
239
+ function Add-HookTargetDistinctValue {
240
+ param([System.Collections.Generic.List[string]] $List, [string] $Value)
241
+ foreach ($existing in $List) { if ([string]::Equals($existing, $Value, $ignoreCase)) { return } }
242
+ $List.Add($Value)
243
+ }
244
+ function Get-HookTargetRecordedIssue {
245
+ param([string] $Root)
246
+ try { $value = ([string] (& $CheckpointIssueReader $Root)).Trim().TrimStart('#') } catch { return $null }
247
+ if ($value -notmatch '^\d+$') { return $null }
248
+ $digits = $value.TrimStart('0')
249
+ return $(if ($digits -eq '') { '0' } else { $digits })
250
+ }
251
+ function Resolve-HookTargetPathSource {
252
+ param([string[]] $Paths, [string] $Source)
253
+ $resolved = [System.Collections.Generic.List[string]]::new()
254
+ $details = [System.Collections.Generic.List[string]]::new()
255
+ $fellBack = $false
256
+ foreach ($candidate in $Paths) {
257
+ $normalized = ConvertTo-HookTargetWorktreeNormalizedPath -Path $candidate
258
+ $root = $session
259
+ if ($normalized -notmatch $absolutePattern) {
260
+ $fellBack = $true
261
+ $details.Add("relative path '$normalized' maps to the session root")
262
+ } else {
263
+ $enclosing = Get-HookTargetWorktreeEnclosingRoot -Path $normalized -Roots $roots
264
+ if ($null -eq $enclosing) {
265
+ $fellBack = $true
266
+ $details.Add($(if ($listDetail) { $listDetail } else { "path '$normalized' is inside no registered worktree" }))
267
+ } else {
268
+ $root = $enclosing
269
+ }
270
+ }
271
+ Add-HookTargetDistinctValue -List $resolved -Value $root
272
+ }
273
+ if ($resolved.Count -gt 1) {
274
+ return (Get-HookTargetResult -Root $null -Source $Source -Code $ambiguousCode -Detail ('the paths resolve to {0} different worktrees' -f $resolved.Count) -Candidates $resolved.ToArray())
275
+ }
276
+ $decidedBy = if ($fellBack) { 'SessionRoot' } else { $Source }
277
+ return (Get-HookTargetResult -Root $resolved[0] -Source $decidedBy -Code $null -Detail ($details -join '; '))
278
+ }
279
+ function Resolve-HookTargetBranchSource {
280
+ param([string] $Branch, [string] $Source)
281
+ $matched = @($records | Where-Object { $null -ne $_.Branch -and [string]::Equals($_.Branch, $Branch, $ignoreCase) } | ForEach-Object { $_.Root })
282
+ if ($matched.Count -eq 0) { return (Get-HookTargetResult -Root $null -Source $Source -Code $notDerivableCode -Detail "branch '$Branch' is checked out in no registered worktree") }
283
+ 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) }
284
+ return (Get-HookTargetResult -Root $matched[0] -Source $Source -Code $null)
285
+ }
286
+
287
+ # Select the deciding source: the first present one in precedence order.
288
+ $filePaths = @($FilePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
289
+ $operandPaths = @($CommandOperandPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
290
+ $branchOperand = if ([string]::IsNullOrWhiteSpace($CommandBranch)) { '' } else { $CommandBranch.Trim() }
291
+ $branches = [System.Collections.Generic.List[string]]::new()
292
+ $issues = [System.Collections.Generic.List[string]]::new()
293
+ if ($filePaths.Count -gt 0) {
294
+ $source = 'FilePath'
295
+ } elseif ($operandPaths.Count -gt 0 -or $branchOperand) {
296
+ $source = 'CommandOperand'
297
+ } elseif (-not [string]::IsNullOrWhiteSpace($DelegationPrompt)) {
298
+ $source = 'DelegationIdentity'
299
+ foreach ($match in [regex]::Matches($DelegationPrompt, '(?i)\bbranch:\s*(?<name>[A-Za-z0-9][A-Za-z0-9._/-]*)')) {
300
+ Add-HookTargetDistinctValue -List $branches -Value $match.Groups['name'].Value.TrimEnd('.', '/')
301
+ }
302
+ foreach ($match in [regex]::Matches($DelegationPrompt, 'Canonical issue number for this feature is #?(?<number>\d+)')) {
303
+ $digits = $match.Groups['number'].Value.TrimStart('0')
304
+ Add-HookTargetDistinctValue -List $issues -Value $(if ($digits -eq '') { '0' } else { $digits })
305
+ }
306
+ } else {
307
+ return (Get-HookTargetResult -Root $session -Source 'SessionRoot' -Code $null -Detail 'no target source is present; the session root applies')
308
+ }
309
+
310
+ # Enumerate registered worktrees at most once, and only when the deciding source needs them.
311
+ $needsList = switch ($source) {
312
+ 'FilePath' { @($filePaths | Where-Object { (ConvertTo-HookTargetWorktreeNormalizedPath -Path $_) -match $absolutePattern }).Count -gt 0 }
313
+ 'CommandOperand' { [bool] $branchOperand -or @($operandPaths | Where-Object { (ConvertTo-HookTargetWorktreeNormalizedPath -Path $_) -match $absolutePattern }).Count -gt 0 }
314
+ default { ($branches.Count -eq 1 -and $issues.Count -le 1) -or ($branches.Count -eq 0 -and $issues.Count -eq 1 -and $null -ne $CheckpointIssueReader) }
315
+ }
316
+ $records = @()
317
+ $listDetail = ''
318
+ if ($needsList) {
319
+ try {
320
+ $records = @(ConvertFrom-HookTargetWorktreePorcelain -Text (@(& $WorktreeListInvoker $SessionRoot) -join "`n"))
321
+ if ($records.Count -eq 0) { $listDetail = 'the worktree list returned no registered worktree' }
322
+ } catch {
323
+ $records = @()
324
+ $listDetail = "the worktree list failed: $($_.Exception.Message)"
325
+ }
326
+ }
327
+ $roots = @($records | ForEach-Object { $_.Root })
328
+
329
+ if ($source -eq 'FilePath') { return (Resolve-HookTargetPathSource -Paths $filePaths -Source 'FilePath') }
330
+
331
+ if ($source -eq 'CommandOperand') {
332
+ $pathResult = if ($operandPaths.Count -gt 0) { Resolve-HookTargetPathSource -Paths $operandPaths -Source 'CommandOperand' } else { $null }
333
+ if (-not $branchOperand) { return $pathResult }
334
+ $branchResult = Resolve-HookTargetBranchSource -Branch $branchOperand -Source 'CommandOperand'
335
+ if ($null -eq $pathResult) { return $branchResult }
336
+ if ($pathResult.Ambiguous) { return $pathResult }
337
+ if ($null -ne $branchResult.ReasonCode) { return $branchResult }
338
+ if (-not [string]::Equals($pathResult.WorktreeRoot, $branchResult.WorktreeRoot, $ignoreCase)) {
339
+ 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))
340
+ }
341
+ return $branchResult
342
+ }
343
+
344
+ 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 ', '))) }
345
+ 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 ', '))) }
346
+ if ($branches.Count -eq 1) {
347
+ $branchResult = Resolve-HookTargetBranchSource -Branch $branches[0] -Source $source
348
+ if ($null -ne $branchResult.ReasonCode -or $issues.Count -eq 0 -or $null -eq $CheckpointIssueReader) { return $branchResult }
349
+ $target = $branchResult.WorktreeRoot
350
+ $recorded = Get-HookTargetRecordedIssue -Root $target
351
+ if ($null -ne $recorded) {
352
+ if ($recorded -eq $issues[0]) { return $branchResult }
353
+ 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))
354
+ }
355
+ $elsewhere = @($records | Where-Object { -not [string]::Equals($_.Root, $target, $ignoreCase) -and (Get-HookTargetRecordedIssue -Root $_.Root) -eq $issues[0] } | ForEach-Object { $_.Root })
356
+ if ($elsewhere.Count -gt 0) {
357
+ 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))
358
+ }
359
+ return $branchResult
360
+ }
361
+ if ($issues.Count -eq 1) {
362
+ 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") }
363
+ $matched = @($records | Where-Object { (Get-HookTargetRecordedIssue -Root $_.Root) -eq $issues[0] } | ForEach-Object { $_.Root })
364
+ if ($matched.Count -eq 0) { return (Get-HookTargetResult -Root $null -Source $source -Code $notDerivableCode -Detail "issue $($issues[0]) is recorded by no registered worktree") }
365
+ 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) }
366
+ return (Get-HookTargetResult -Root $matched[0] -Source $source -Code $null)
367
+ }
368
+ return (Get-HookTargetResult -Root $null -Source $source -Code $notDerivableCode -Detail 'the prompt carries no branch: label and no canonical issue line')
369
+ }
@@ -13,6 +13,7 @@ param()
13
13
  . (Join-Path $PSScriptRoot 'codex-authority-store.ps1')
14
14
  . (Join-Path $PSScriptRoot 'codex-agent-profile-attestation.ps1')
15
15
  . (Join-Path $PSScriptRoot 'codex-epic-child-launch-attestation.ps1')
16
+ . (Join-Path $PSScriptRoot 'discovery-start-attestation.ps1')
16
17
  $script:EpicPersonas = @('epic-planner', 'epic-orchestrator')
17
18
 
18
19
  function ConvertFrom-SubagentAttestationJson {
@@ -365,6 +366,13 @@ function Invoke-CodexSubagentAttestationCommand {
365
366
  )
366
367
  try {
367
368
  $payload = ConvertFrom-SubagentAttestationJson -Raw (& $ReadInput) -Name 'SubagentStart input'
369
+ if (Test-CodexDiscoveryAgentType -AgentType ([string]$payload.agent_type)) {
370
+ # A discovery specialist is attested against its protected invocation only;
371
+ # no checkpoint, HEAD or epic receipt is consulted (issue #980).
372
+ $attestation = Get-CodexDiscoveryStartAttestation -Payload $payload -RepositoryRoot $repositoryRoot -Now (& $GetNow)
373
+ $attestationPath = Write-CodexSubagentAttestation -RepositoryRoot $repositoryRoot -Attestation $attestation
374
+ return [ordered]@{ exit_code = 0; stdout = (Get-CodexDiscoveryStartCommandOutput -Attestation $attestation -AttestationPath $attestationPath); stderr = $null }
375
+ }
368
376
  $stateRoot = Join-Path $repositoryRoot 'artifacts/orchestration'
369
377
  $receiptPath = Get-CodexAuthorityReceiptPath `
370
378
  -RepositoryRoot $repositoryRoot `
@@ -31,7 +31,7 @@ function Test-CodexStopGatedAgent {
31
31
  [OutputType([bool])]
32
32
  param([Parameter(Mandatory)][string] $AgentType)
33
33
 
34
- if ($AgentType -match '-c(?:1|2|3|4|3-elevated)$') {
34
+ if ($AgentType -match '-c(?:1|2|3|4|3-elevated)$' -or (Test-CodexStopDiscoveryAgent -AgentType $AgentType)) {
35
35
  return $true
36
36
  }
37
37
  return @(
@@ -42,6 +42,19 @@ function Test-CodexStopGatedAgent {
42
42
  ) -contains $AgentType
43
43
  }
44
44
 
45
+ function Test-CodexStopDiscoveryAgent {
46
+ <#
47
+ .SYNOPSIS
48
+ True for a discovery specialist, whose stop requires the discovery-invocation start attestation (issue #980).
49
+ #>
50
+ [CmdletBinding()]
51
+ [OutputType([bool])]
52
+ param([AllowNull()][AllowEmptyString()][string] $AgentType)
53
+
54
+ return -not [string]::IsNullOrWhiteSpace($AgentType) -and
55
+ $AgentType -cmatch '^(legacy-parity-analyst|runtime-characterization-analyst|requirements-reconciler|migration-coverage-reviewer)(-c(1|2|3|4|3-elevated))?$'
56
+ }
57
+
45
58
  function Get-CodexStopContinuation {
46
59
  [CmdletBinding()]
47
60
  [OutputType([System.Collections.Specialized.OrderedDictionary])]
@@ -98,6 +111,14 @@ function Invoke-CodexSubagentStopDecision {
98
111
  return Get-CodexStopContinuation -Reason "EPIC_INVOCATION_ORIGIN_BLOCKED: '$agentType' lacks valid root provenance." -AlreadyContinued $alreadyContinued
99
112
  }
100
113
  }
114
+ if (Test-CodexStopDiscoveryAgent -AgentType $agentType) {
115
+ # A specialist's attestation must be the protected discovery-invocation receipt kind;
116
+ # the output authority itself is validated by validate-discovery-agent-output.ps1.
117
+ $attestationNames = @($attestation.PSObject.Properties.Name)
118
+ if ($attestationNames -notcontains 'receipt_kind' -or [string]$attestation.receipt_kind -cne 'discovery-invocation') {
119
+ return Get-CodexStopContinuation -Reason "DISCOVERY_START_ATTESTATION_BLOCKED: '$agentType' was not attested against a protected discovery invocation." -AlreadyContinued $alreadyContinued
120
+ }
121
+ }
101
122
  if ($attestation.routing_valid -isnot [bool] -or -not [bool]$attestation.routing_valid) {
102
123
  return Get-CodexStopContinuation -Reason "MODEL_ROUTING_ATTESTATION_BLOCKED: '$agentType' did not run under its recorded deployment model." -AlreadyContinued $alreadyContinued
103
124
  }