@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,276 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Host event-envelope normalization and trusted principal attribution for discovery hooks.
4
+
5
+ .DESCRIPTION
6
+ Claude Code, Codex and the GitHub Copilot harness deliver hook events as JSON
7
+ envelopes with host-specific key spellings. This module normalizes each
8
+ supported envelope into one shape (host, event, session, agent, parent, tool
9
+ name, tool input, working directory) and derives the trusted principal from
10
+ the payload alone: the host is fixed by which hook file runs, the session is
11
+ the envelope session, a specialist is an envelope that carries an `agent_id`
12
+ with a discovery `agent_type`, the root is an envelope without `agent_id`,
13
+ and the parent is the envelope parent when the host supplies one, else the
14
+ literal `root`. Identity never comes from caller arguments, tool input or the
15
+ process environment; when the envelope carries an anomaly the principal is
16
+ unattributed and the calling hook fails closed.
17
+
18
+ Malformed envelopes are typed anomalies, never silent nulls, so a hook can emit
19
+ one deny reason per contract leg that drifted.
20
+
21
+ .NOTES
22
+ Issue #980. Compatible with PowerShell 7+. No filesystem access, no subprocess,
23
+ no network, no wall-clock read, no environment read.
24
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
25
+ #>
26
+
27
+ Set-StrictMode -Version Latest
28
+ $ErrorActionPreference = 'Stop'
29
+
30
+ $script:SupportedHosts = @('claude-code', 'codex', 'github-copilot')
31
+ $script:RootParentAgentId = 'root'
32
+ # Identifier grammar shared with the sibling modules: one leading alphanumeric,
33
+ # then alphanumerics, dot, underscore, colon or hyphen.
34
+ $script:CorrelationIdPattern = '^[A-Za-z0-9][A-Za-z0-9._:-]*$'
35
+ # The four discovery roles with their optional Codex complexity suffix.
36
+ $script:DiscoveryAgentTypePattern = '^(?<role>legacy-parity-analyst|runtime-characterization-analyst|requirements-reconciler|migration-coverage-reviewer)(?:-c(?:1|2|3|4|3-elevated))?$'
37
+ # Per-host envelope key spellings, most specific first. Claude Code and Codex use
38
+ # snake_case; the Copilot harness uses camelCase and may fall back to snake_case
39
+ # for fields it shares with the Claude shape. Only Copilot documents a parent
40
+ # identifier; the snake_case spelling is read on every host.
41
+ $script:EnvelopeKeys = @{
42
+ 'claude-code' = @{ SessionId = @('session_id'); Event = @('hook_event_name'); ToolName = @('tool_name'); ToolInput = @('tool_input'); Cwd = @('cwd'); AgentId = @('agent_id'); AgentType = @('agent_type'); ToolUseId = @('tool_use_id'); ParentAgentId = @('parent_agent_id') }
43
+ 'codex' = @{ SessionId = @('session_id'); Event = @('hook_event_name'); ToolName = @('tool_name'); ToolInput = @('tool_input'); Cwd = @('cwd'); AgentId = @('agent_id'); AgentType = @('agent_type'); ToolUseId = @('tool_use_id'); ParentAgentId = @('parent_agent_id') }
44
+ 'github-copilot' = @{ SessionId = @('sessionId', 'session_id'); Event = @('hookEventName', 'hook_event_name'); ToolName = @('toolName', 'tool_name'); ToolInput = @('toolInput', 'tool_input'); Cwd = @('cwd'); AgentId = @('agentId', 'agent_id'); AgentType = @('agentType', 'agent_type'); ToolUseId = @('toolUseId', 'tool_use_id'); ParentAgentId = @('parentAgentId', 'parent_agent_id') }
45
+ }
46
+ $script:AnomalyReasonText = @{
47
+ 'EmptyEnvelope' = 'the hook received no envelope object'
48
+ 'UnsupportedHost' = 'the hook received an envelope for a host that has no native discovery adapter'
49
+ 'MissingSessionId' = 'the hook received an envelope without a trusted session identifier'
50
+ 'MissingEvent' = 'the hook received an envelope without a hook event name'
51
+ 'NonObjectToolInput' = 'the hook received a tool event whose tool input is not an object'
52
+ }
53
+
54
+ function Get-DiscoveryEnvelopeMember {
55
+ <#
56
+ .SYNOPSIS
57
+ Returns the first present member among Names on a hashtable or PSCustomObject, else $null.
58
+ #>
59
+ [CmdletBinding()]
60
+ [OutputType([object], [object[]])]
61
+ param(
62
+ [Parameter(Mandatory = $true)][object]$Envelope,
63
+ [Parameter(Mandatory = $true)][string[]]$Names
64
+ )
65
+
66
+ # The first spelling that exists wins, even when its value is null; the unary
67
+ # comma keeps an array value intact across the function boundary.
68
+ foreach ($name in $Names) {
69
+ if ($Envelope -is [System.Collections.IDictionary]) {
70
+ if ($Envelope.Contains($name)) {
71
+ if ($null -eq $Envelope[$name]) { return $null }
72
+ return , $Envelope[$name]
73
+ }
74
+ } elseif ($null -ne $Envelope.PSObject.Properties[$name]) {
75
+ $member = $Envelope.PSObject.Properties[$name].Value
76
+ if ($null -eq $member) { return $null }
77
+ return , $member
78
+ }
79
+ }
80
+ return $null
81
+ }
82
+
83
+ function Get-DiscoveryPayloadAnomalyReason {
84
+ <#
85
+ .SYNOPSIS
86
+ Maps an anomaly code to the deny reason text a hook emits.
87
+ #>
88
+ [CmdletBinding()]
89
+ [OutputType([string])]
90
+ param([Parameter(Mandatory = $true)][string]$Code)
91
+
92
+ if (-not $script:AnomalyReasonText.ContainsKey($Code)) {
93
+ throw "Unknown discovery payload anomaly code: $Code"
94
+ }
95
+ return $script:AnomalyReasonText[$Code]
96
+ }
97
+
98
+ function Test-DiscoverySupportedHost {
99
+ <#
100
+ .SYNOPSIS
101
+ True when HostName names a host with a native discovery adapter.
102
+ #>
103
+ [CmdletBinding()]
104
+ [OutputType([bool])]
105
+ param([Parameter(Mandatory = $false)][AllowNull()][AllowEmptyString()][string]$HostName)
106
+
107
+ return $script:SupportedHosts -contains $HostName
108
+ }
109
+
110
+ function Get-DiscoveryAgentRole {
111
+ <#
112
+ .SYNOPSIS
113
+ Returns the discovery role named by a native agent type, or $null for any other agent.
114
+ .DESCRIPTION
115
+ `legacy-parity-analyst-c3` and `legacy-parity-analyst` both name the
116
+ legacy-parity-analyst role; the comparison is case-sensitive. The agent
117
+ type is the payload `agent_type` the host reports for the running agent.
118
+ #>
119
+ [CmdletBinding()]
120
+ [OutputType([string])]
121
+ param([Parameter(Mandatory = $false)][AllowNull()][AllowEmptyString()][string]$AgentType)
122
+
123
+ if ([string]::IsNullOrWhiteSpace($AgentType)) { return $null }
124
+ $match = [regex]::Match($AgentType, $script:DiscoveryAgentTypePattern)
125
+ if (-not $match.Success) { return $null }
126
+ return $match.Groups['role'].Value
127
+ }
128
+
129
+ function ConvertFrom-DiscoveryHostEnvelope {
130
+ <#
131
+ .SYNOPSIS
132
+ Normalizes one host event envelope into the shared discovery shape.
133
+ .DESCRIPTION
134
+ Returns an object with HostName, Event, SessionId, AgentId, AgentType,
135
+ ParentAgentId, ToolName, ToolInput, Cwd, ToolUseId and Anomaly. Anomaly is
136
+ $null for a usable envelope or one of EmptyEnvelope, UnsupportedHost,
137
+ MissingSessionId, MissingEvent and NonObjectToolInput (a PreToolUse event
138
+ whose tool input is absent or not an object). Property-level absence in a
139
+ well-formed tool input is not an anomaly; that is each hook's own scope filter.
140
+ #>
141
+ [CmdletBinding()]
142
+ [OutputType([pscustomobject])]
143
+ param(
144
+ [Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][string]$HostName,
145
+ [Parameter(Mandatory = $false)][AllowNull()][object]$Envelope
146
+ )
147
+
148
+ $result = [ordered]@{
149
+ HostName = $HostName; Event = $null; SessionId = $null; AgentId = $null; AgentType = $null; ParentAgentId = $null
150
+ ToolName = $null; ToolInput = $null; Cwd = $null; ToolUseId = $null; Anomaly = $null
151
+ }
152
+ # Decision order: host support, envelope presence, then the trusted fields
153
+ # a hook cannot proceed without (session, event), then tool-input shape.
154
+ if (-not (Test-DiscoverySupportedHost -HostName $HostName)) {
155
+ $result.Anomaly = 'UnsupportedHost'
156
+ return [pscustomobject]$result
157
+ }
158
+ $isObject = ($Envelope -is [System.Collections.IDictionary]) -or ($Envelope -is [System.Management.Automation.PSCustomObject])
159
+ if (-not $isObject) {
160
+ $result.Anomaly = 'EmptyEnvelope'
161
+ return [pscustomobject]$result
162
+ }
163
+ $keys = $script:EnvelopeKeys[$HostName]
164
+ $result.SessionId = Get-DiscoveryEnvelopeMember -Envelope $Envelope -Names $keys.SessionId
165
+ $result.Event = Get-DiscoveryEnvelopeMember -Envelope $Envelope -Names $keys.Event
166
+ $result.ToolName = Get-DiscoveryEnvelopeMember -Envelope $Envelope -Names $keys.ToolName
167
+ $result.ToolInput = Get-DiscoveryEnvelopeMember -Envelope $Envelope -Names $keys.ToolInput
168
+ $result.Cwd = Get-DiscoveryEnvelopeMember -Envelope $Envelope -Names $keys.Cwd
169
+ $result.AgentId = Get-DiscoveryEnvelopeMember -Envelope $Envelope -Names $keys.AgentId
170
+ $result.AgentType = Get-DiscoveryEnvelopeMember -Envelope $Envelope -Names $keys.AgentType
171
+ $result.ParentAgentId = Get-DiscoveryEnvelopeMember -Envelope $Envelope -Names $keys.ParentAgentId
172
+ $result.ToolUseId = Get-DiscoveryEnvelopeMember -Envelope $Envelope -Names $keys.ToolUseId
173
+ if ($result.SessionId -isnot [string] -or [string]::IsNullOrWhiteSpace($result.SessionId)) {
174
+ $result.Anomaly = 'MissingSessionId'
175
+ } elseif ($result.Event -isnot [string] -or [string]::IsNullOrWhiteSpace($result.Event)) {
176
+ $result.Anomaly = 'MissingEvent'
177
+ } elseif ($result.Event -eq 'PreToolUse') {
178
+ $toolInputIsObject = ($result.ToolInput -is [System.Collections.IDictionary]) -or ($result.ToolInput -is [System.Management.Automation.PSCustomObject])
179
+ if (-not $toolInputIsObject) { $result.Anomaly = 'NonObjectToolInput' }
180
+ }
181
+ return [pscustomobject]$result
182
+ }
183
+
184
+ function ConvertFrom-DiscoveryHostPayloadText {
185
+ <#
186
+ .SYNOPSIS
187
+ Parses raw envelope JSON text and normalizes it for HostName.
188
+ .DESCRIPTION
189
+ Empty, whitespace-only or unparseable text yields the EmptyEnvelope anomaly
190
+ rather than an exception, so a hook can deny with a reason.
191
+ #>
192
+ [CmdletBinding()]
193
+ [OutputType([pscustomobject])]
194
+ param(
195
+ [Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][string]$HostName,
196
+ [Parameter(Mandatory = $false)][AllowNull()][AllowEmptyString()][string]$Text
197
+ )
198
+
199
+ $envelope = $null
200
+ if (-not [string]::IsNullOrWhiteSpace($Text)) {
201
+ try {
202
+ $envelope = ConvertFrom-Json -InputObject $Text -Depth 16 -NoEnumerate
203
+ } catch {
204
+ $envelope = $null
205
+ }
206
+ }
207
+ return ConvertFrom-DiscoveryHostEnvelope -HostName $HostName -Envelope $envelope
208
+ }
209
+
210
+ function Get-DiscoveryPayloadIdentifier {
211
+ <#
212
+ .SYNOPSIS
213
+ Returns Value as a string when it is a non-blank string matching the identifier grammar, else $null.
214
+ #>
215
+ [CmdletBinding()]
216
+ [OutputType([string])]
217
+ param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
218
+
219
+ if ($Value -isnot [string] -or [string]::IsNullOrWhiteSpace($Value)) { return $null }
220
+ if ($Value -notmatch $script:CorrelationIdPattern) { return $null }
221
+ return [string]$Value
222
+ }
223
+
224
+ function Get-DiscoveryTrustedPrincipal {
225
+ <#
226
+ .SYNOPSIS
227
+ Derives the trusted principal from a normalized envelope alone.
228
+ .DESCRIPTION
229
+ The host is the adapter that received the envelope and the session is the
230
+ envelope session. An envelope without `agent_id` is the root of its
231
+ session (IsRoot). An envelope whose `agent_id` matches the identifier
232
+ grammar and whose `agent_type` names a discovery role is a specialist, and
233
+ AgentId and Role are set; every other agent leaves both null. ParentAgentId
234
+ is the envelope parent when the host supplies a valid one, else the literal
235
+ `root`. The principal is attributed for every anomaly-free envelope, which
236
+ always carries a session; an anomalous envelope is unattributed with the
237
+ anomaly reason. Nothing in tool input or the process environment supplies
238
+ any of these values.
239
+ .OUTPUTS
240
+ [pscustomobject] HostName, SessionId, ParentAgentId, AgentId, Role, IsRoot,
241
+ Attributed, Reason.
242
+ #>
243
+ [CmdletBinding()]
244
+ [OutputType([pscustomobject])]
245
+ param([Parameter(Mandatory = $true)][pscustomobject]$Normalized)
246
+
247
+ $agentId = Get-DiscoveryPayloadIdentifier -Value $Normalized.AgentId
248
+ $role = $(if ($Normalized.AgentType -is [string]) { Get-DiscoveryAgentRole -AgentType $Normalized.AgentType } else { $null })
249
+ $parent = Get-DiscoveryPayloadIdentifier -Value $Normalized.ParentAgentId
250
+ $isSpecialist = ($null -ne $agentId) -and ($null -ne $role)
251
+ $principal = [ordered]@{
252
+ HostName = $Normalized.HostName
253
+ SessionId = $Normalized.SessionId
254
+ ParentAgentId = $(if ($null -ne $parent) { $parent } else { $script:RootParentAgentId })
255
+ AgentId = $(if ($isSpecialist) { $agentId } else { $null })
256
+ Role = $(if ($isSpecialist) { $role } else { $null })
257
+ IsRoot = ($null -eq $Normalized.AgentId)
258
+ Attributed = $false
259
+ Reason = $null
260
+ }
261
+ # Attribution requires an anomaly-free envelope; an anomaly carries its reason.
262
+ if ($null -ne $Normalized.Anomaly) {
263
+ $principal.Reason = Get-DiscoveryPayloadAnomalyReason -Code $Normalized.Anomaly
264
+ } else {
265
+ $principal.Attributed = $true
266
+ }
267
+ return [pscustomobject]$principal
268
+ }
269
+
270
+ Export-ModuleMember -Function `
271
+ Test-DiscoverySupportedHost, `
272
+ Get-DiscoveryAgentRole, `
273
+ ConvertFrom-DiscoveryHostEnvelope, `
274
+ ConvertFrom-DiscoveryHostPayloadText, `
275
+ Get-DiscoveryTrustedPrincipal, `
276
+ Get-DiscoveryPayloadAnomalyReason
@@ -0,0 +1,336 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Native PreToolUse gating of implementation planning on current configured discovery authority.
4
+
5
+ .DESCRIPTION
6
+ Every native discovery planning adapter (Claude Code, Codex, GitHub Copilot)
7
+ takes its decision from this module. A planning delegation is a native agent
8
+ launch whose target is an atomic-planner or orchestrator profile. When the
9
+ workspace has no protected activation, or a deliberate deactivation, the
10
+ delegation is allowed and the existing planning gates apply unchanged. When the
11
+ workspace is active, the delegation is admitted only when the workspace still
12
+ carries the activated locator (`config/discovery.json`) and domain profile with
13
+ the activated bytes, and the protected session holds a current gateway-written
14
+ `validation-receipt` for mode `planning` issued to the native-root principal of
15
+ this host and session, bound to the activation and workspace, passing, with
16
+ every input it read unchanged and the profile's planning prerequisite
17
+ (`none`, `discovery-complete`, `parity-complete`) reflected in its manifest. A
18
+ structural, agent-output, completion-mode or receipt-version-1 receipt, a
19
+ failing receipt, a stale receipt, a missing, deleted, disabled or malformed
20
+ locator or profile all block before the delegation runs.
21
+
22
+ The hooks verify authority only: no Python runtime is invoked and no
23
+ validation is started here. Store and workspace access flows through injected
24
+ seams.
25
+
26
+ .NOTES
27
+ Issue #980. Compatible with PowerShell 7+. Depends only on the sibling
28
+ DiscoveryEncoding, DiscoveryAuthority, DiscoveryManifest, DiscoveryPayload,
29
+ DiscoveryAuthorization and DiscoveryStartRecord modules.
30
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
31
+ #>
32
+
33
+ Set-StrictMode -Version Latest
34
+ $ErrorActionPreference = 'Stop'
35
+
36
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryEncoding.psm1') -ErrorAction Stop
37
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryAuthority.psm1') -ErrorAction Stop
38
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryManifest.psm1') -ErrorAction Stop
39
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryPayload.psm1') -ErrorAction Stop
40
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryToolRouting.psm1') -ErrorAction Stop
41
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryAuthorization.psm1') -ErrorAction Stop
42
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryStartRecord.psm1') -ErrorAction Stop
43
+
44
+ $script:PlanningAgentPattern = '^(?:atomic-planner|orchestrator)(?:-c(?:1|2|3|4|3-elevated))?$'
45
+ $script:PlanningTargetKeys = @('subagent_type', 'agent_type', 'agent')
46
+ $script:LocatorPath = 'config/discovery.json'
47
+ $script:ReceiptsPartition = 'receipts'
48
+ $script:Prerequisites = @('none', 'discovery-complete', 'parity-complete')
49
+ $script:PrerequisitePattern = '(?m)^\s*"?planning_prerequisite"?\s*[:=]\s*"?(?<value>none|discovery-complete|parity-complete)"?\s*,?\s*(?:#.*)?$'
50
+
51
+ function Get-DiscoveryPlanningOutcome {
52
+ <#
53
+ .SYNOPSIS
54
+ Builds one planning outcome (allow or deny) with its code, reason and the authorizing receipt.
55
+ #>
56
+ [CmdletBinding()]
57
+ [OutputType([pscustomobject])]
58
+ param(
59
+ [Parameter(Mandatory = $true)][ValidateSet('allow', 'deny')][string]$Decision,
60
+ [Parameter(Mandatory = $true)][string]$Code,
61
+ [Parameter(Mandatory = $true)][string]$Reason,
62
+ [Parameter(Mandatory = $false)][AllowNull()][object]$Receipt = $null,
63
+ [Parameter(Mandatory = $false)][AllowNull()][string]$Prerequisite = $null
64
+ )
65
+
66
+ return [pscustomobject]@{ Decision = $Decision; Code = $Code; Reason = $Reason; Receipt = $Receipt; Prerequisite = $Prerequisite }
67
+ }
68
+
69
+ function Get-DiscoveryPlanningTarget {
70
+ <#
71
+ .SYNOPSIS
72
+ Returns the planner or orchestrator profile a native agent launch targets, or $null for any other call.
73
+ #>
74
+ [CmdletBinding()]
75
+ [OutputType([string])]
76
+ param([Parameter(Mandatory = $true)][pscustomobject]$Normalized)
77
+
78
+ $tool = Resolve-DiscoveryToolName -ToolName $Normalized.ToolName
79
+ if ($tool.Family -ne 'spawn') { return $null }
80
+ $toolInput = $Normalized.ToolInput
81
+ foreach ($key in $script:PlanningTargetKeys) {
82
+ $value = $null
83
+ if ($toolInput -is [System.Collections.IDictionary]) { if ($toolInput.Contains($key)) { $value = $toolInput[$key] } }
84
+ elseif ($toolInput -is [System.Management.Automation.PSCustomObject]) { $property = $toolInput.PSObject.Properties[$key]; if ($null -ne $property) { $value = $property.Value } }
85
+ if ($value -is [string] -and $value -cmatch $script:PlanningAgentPattern) { return $value }
86
+ }
87
+ return $null
88
+ }
89
+
90
+ function Get-DiscoveryPlanningPrerequisite {
91
+ <#
92
+ .SYNOPSIS
93
+ Reads `planning_prerequisite` from the profile text (YAML or JSON), defaulting to discovery-complete.
94
+ #>
95
+ [CmdletBinding()]
96
+ [OutputType([string])]
97
+ param([Parameter(Mandatory = $true)][AllowEmptyString()][string]$ProfileText)
98
+
99
+ $match = [regex]::Match($ProfileText, $script:PrerequisitePattern)
100
+ if ($match.Success) { return $match.Groups['value'].Value }
101
+ return 'discovery-complete'
102
+ }
103
+
104
+ function Get-DiscoveryWorkspaceFileMismatch {
105
+ <#
106
+ .SYNOPSIS
107
+ Verifies that a workspace file still exists with the activated bytes and is decodable text.
108
+ .OUTPUTS
109
+ [pscustomobject] Code, Reason, Text (the decoded text when valid).
110
+ #>
111
+ [CmdletBinding()]
112
+ [OutputType([pscustomobject])]
113
+ param(
114
+ [Parameter(Mandatory = $true)][string]$WorkspaceRoot,
115
+ [Parameter(Mandatory = $true)][string]$RelativePath,
116
+ [Parameter(Mandatory = $true)][AllowNull()][AllowEmptyString()][string]$ExpectedSha256,
117
+ [Parameter(Mandatory = $true)][string]$Label,
118
+ [Parameter(Mandatory = $true)][scriptblock]$ReadBytes
119
+ )
120
+
121
+ $bytes = & $ReadBytes (Get-DiscoveryWorkspacePath -WorkspaceRoot $WorkspaceRoot -RelativePath $RelativePath)
122
+ if ($null -eq $bytes) {
123
+ return [pscustomobject]@{ Code = "$($Label)_MISSING"; Reason = "the workspace is active but its $($Label.ToLowerInvariant()) $RelativePath is absent"; Text = $null }
124
+ }
125
+ [byte[]]$bytes = @($bytes)
126
+ if ([string]::IsNullOrEmpty($ExpectedSha256) -or (Get-DiscoveryBytesSha256Hex -Bytes $bytes) -cne $ExpectedSha256) {
127
+ return [pscustomobject]@{ Code = "$($Label)_CHANGED"; Reason = "the $($Label.ToLowerInvariant()) $RelativePath differs from the activated bytes"; Text = $null }
128
+ }
129
+ try {
130
+ $text = [System.Text.UTF8Encoding]::new($false, $true).GetString($bytes)
131
+ } catch {
132
+ return [pscustomobject]@{ Code = "$($Label)_MALFORMED"; Reason = "the $($Label.ToLowerInvariant()) $RelativePath is not valid UTF-8"; Text = $null }
133
+ }
134
+ return [pscustomobject]@{ Code = $null; Reason = $null; Text = $text }
135
+ }
136
+
137
+ function Get-DiscoveryPlanningReceiptMismatch {
138
+ <#
139
+ .SYNOPSIS
140
+ Compares one candidate receipt against the planning authority contract.
141
+ .DESCRIPTION
142
+ Returns $null when the receipt authorizes planning, otherwise an object with
143
+ Code (RECEIPT_VERSION, WRONG_MODE, IDENTITY, FORGED, BINDING, STALE_ACTIVATION,
144
+ RESULT, PREREQUISITE or STALE_INPUTS) and Reason. The receipt must be a
145
+ schema-2 `validation-receipt` of mode `planning` for the native-root
146
+ principal of this host and session.
147
+ #>
148
+ [CmdletBinding()]
149
+ [OutputType([pscustomobject])]
150
+ param(
151
+ [Parameter(Mandatory = $true)][pscustomobject]$Receipt,
152
+ [Parameter(Mandatory = $true)][pscustomobject]$Normalized,
153
+ [Parameter(Mandatory = $true)][pscustomobject]$Activation,
154
+ [Parameter(Mandatory = $true)][string]$ActivationSha256,
155
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
156
+ [Parameter(Mandatory = $true)][string]$Prerequisite,
157
+ [Parameter(Mandatory = $true)][scriptblock]$ReadBytes
158
+ )
159
+
160
+ $names = @($Receipt.PSObject.Properties.Name)
161
+ foreach ($required in @('artifact_kind', 'receipt_version', 'mode', 'identity', 'writer', 'workspace', 'activation_sha256', 'validation_result', 'input_manifest')) {
162
+ if ($names -notcontains $required) { return [pscustomobject]@{ Code = 'RECEIPT_VERSION'; Reason = "the receipt lacks '$required' and is not a schema 2.0.0 validation receipt" } }
163
+ }
164
+ if ($Receipt.artifact_kind -ne 'validation-receipt' -or [string]$Receipt.receipt_version -ne '2.0.0' -or [string]$Receipt.schema_version -ne '2.0.0') {
165
+ return [pscustomobject]@{ Code = 'RECEIPT_VERSION'; Reason = 'the receipt is not a receipt-version 2.0.0 validation receipt; a v1 check cannot authorize planning' }
166
+ }
167
+ if ([string]$Receipt.mode -ne 'planning') {
168
+ return [pscustomobject]@{ Code = 'WRONG_MODE'; Reason = "the receipt was issued for mode '$($Receipt.mode)'; only a planning-mode receipt authorizes planning" }
169
+ }
170
+ $identity = $Receipt.identity
171
+ if ($identity -isnot [System.Management.Automation.PSCustomObject] -or $identity.kind -ne 'native-root' -or $identity.host -ne $Normalized.HostName -or $identity.session_id -ne $Normalized.SessionId) {
172
+ return [pscustomobject]@{ Code = 'IDENTITY'; Reason = 'the receipt was not issued to the native-root principal of this host and session' }
173
+ }
174
+ $writer = $Receipt.writer
175
+ if ($writer -isnot [System.Management.Automation.PSCustomObject] -or $writer.kind -ne 'host-gateway' -or $writer.host -ne $Normalized.HostName) {
176
+ return [pscustomobject]@{ Code = 'FORGED'; Reason = 'the receipt was not written by the host gateway' }
177
+ }
178
+ $sameBinding = ([string]$Receipt.workspace.workspace_sha256 -eq $Location.WorkspaceSha256) -and
179
+ ([string]$Receipt.workspace.profile_path -eq [string]$Activation.profile_path) -and
180
+ ([string]$Receipt.workspace.profile_sha256 -eq [string]$Activation.profile_sha256)
181
+ if (-not $sameBinding) {
182
+ return [pscustomobject]@{ Code = 'BINDING'; Reason = 'the receipt is bound to another workspace or profile than the current activation' }
183
+ }
184
+ if ([string]$Receipt.activation_sha256 -cne $ActivationSha256) {
185
+ return [pscustomobject]@{ Code = 'STALE_ACTIVATION'; Reason = 'the activation record changed after the planning receipt was issued' }
186
+ }
187
+ if ([string]$Receipt.validation_result -ne 'pass') {
188
+ return [pscustomobject]@{ Code = 'RESULT'; Reason = "the planning receipt records '$($Receipt.validation_result)'; configured discovery is incomplete" }
189
+ }
190
+ try {
191
+ $manifest = ConvertFrom-DiscoveryInputManifest -Value $Receipt.input_manifest
192
+ } catch {
193
+ return [pscustomobject]@{ Code = 'RECEIPT_VERSION'; Reason = "the receipt input manifest is invalid: $($_.Exception.Message)" }
194
+ }
195
+ # The prerequisite the profile configures must be visible in what the runtime validated.
196
+ if ($Prerequisite -ne 'none' -and @($manifest['source_repositories']).Count -eq 0) {
197
+ return [pscustomobject]@{ Code = 'PREREQUISITE'; Reason = "the profile requires $Prerequisite but the planning receipt validated no source repository" }
198
+ }
199
+ if ($Prerequisite -eq 'parity-complete' -and @($manifest['target_repositories']).Count -eq 0) {
200
+ return [pscustomobject]@{ Code = 'PREREQUISITE'; Reason = 'the profile requires parity-complete but the planning receipt validated no target repository' }
201
+ }
202
+ $comparison = Compare-DiscoveryInputManifest -Manifest $manifest -WorkspaceRoot ([string]$Receipt.workspace.workspace_root) `
203
+ -ProfilePath ([string]$Activation.profile_path) -ProfileSha256 ([string]$Activation.profile_sha256) -ReadBytes $ReadBytes
204
+ if ($comparison.Drift.Count -gt 0) {
205
+ $summary = @($comparison.Drift | ForEach-Object { "$($_.kind):$($_.path)" }) -join ', '
206
+ return [pscustomobject]@{ Code = 'STALE_INPUTS'; Reason = "inputs changed after the planning receipt: $summary" }
207
+ }
208
+ return $null
209
+ }
210
+
211
+ function Get-DiscoveryPlanningDecision {
212
+ <#
213
+ .SYNOPSIS
214
+ Decides one PreToolUse agent launch for a native discovery planning adapter.
215
+ .DESCRIPTION
216
+ Order: envelope anomalies deny; a call that is not a planner or orchestrator
217
+ launch is allowed; a workspace without an activation or with a deliberate
218
+ deactivation is allowed (existing planning behavior); an active workspace
219
+ must still hold the activated locator and profile; then the protected
220
+ session's planning receipts are examined newest first and the first one
221
+ that passes Get-DiscoveryPlanningReceiptMismatch authorizes the launch.
222
+ When none passes, the mismatch of the newest planning-mode candidate (or the
223
+ newest candidate of any mode) is the deny reason.
224
+ .OUTPUTS
225
+ [pscustomobject] Decision, Code, Reason, Receipt, Prerequisite.
226
+ #>
227
+ [CmdletBinding()]
228
+ [OutputType([pscustomobject])]
229
+ param(
230
+ [Parameter(Mandatory = $true)][pscustomobject]$Normalized,
231
+ [Parameter(Mandatory = $true)][hashtable]$HostEnvironment,
232
+ [Parameter(Mandatory = $false)][hashtable]$Store = (Get-DiscoveryStartStore),
233
+ [Parameter(Mandatory = $false)][AllowNull()][scriptblock]$ReadBytes = $null
234
+ )
235
+
236
+ if ($null -ne $Normalized.Anomaly) {
237
+ return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code 'ENVELOPE' -Reason ('payload anomaly - ' + (Get-DiscoveryPayloadAnomalyReason -Code $Normalized.Anomaly) + '. The gate fails closed on an envelope it cannot read.')
238
+ }
239
+ $target = Get-DiscoveryPlanningTarget -Normalized $Normalized
240
+ if ($null -eq $target) {
241
+ return Get-DiscoveryPlanningOutcome -Decision 'allow' -Code 'NOT_PLANNING' -Reason 'the call is not a planner or orchestrator launch'
242
+ }
243
+ if ([string]::IsNullOrWhiteSpace([string]$Normalized.Cwd)) {
244
+ return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code 'WORKSPACE' -Reason 'the native event carries no workspace directory'
245
+ }
246
+ try {
247
+ $location = Resolve-DiscoveryAuthorityLocation -WorkspaceRoot ([string]$Normalized.Cwd) -Platform $HostEnvironment.Platform `
248
+ -UserHome $HostEnvironment.UserHome -LocalAppData $HostEnvironment.LocalAppData -XdgStateHome $HostEnvironment.XdgStateHome
249
+ } catch {
250
+ return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code 'WORKSPACE' -Reason $_.Exception.Message
251
+ }
252
+ $activation = Read-DiscoveryActivationState -Location $location -ReadText $Store.ReadText
253
+ if ($activation.Status -eq 'missing' -or $activation.Status -eq 'inactive') {
254
+ return Get-DiscoveryPlanningOutcome -Decision 'allow' -Code 'NOT_ACTIVE' -Reason "discovery is $($activation.Status) for this workspace; existing planning gates apply unchanged"
255
+ }
256
+ if ($activation.Status -ne 'active') {
257
+ return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code 'ACTIVATION' -Reason "the protected activation record is unreadable ($($activation.Reason)); the gate fails closed"
258
+ }
259
+ if ($null -eq $ReadBytes) {
260
+ $ReadBytes = {
261
+ param([string]$Path)
262
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
263
+ return [System.IO.File]::ReadAllBytes($Path)
264
+ }
265
+ }
266
+ $workspaceRoot = [string]$Normalized.Cwd
267
+ $record = $activation.Record
268
+ $locator = Get-DiscoveryWorkspaceFileMismatch -WorkspaceRoot $workspaceRoot -RelativePath $script:LocatorPath -ExpectedSha256 ([string]$record.locator_sha256) -Label 'LOCATOR' -ReadBytes $ReadBytes
269
+ if ($null -ne $locator.Code) { return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code $locator.Code -Reason $locator.Reason }
270
+ try { $locatorRecord = ConvertFrom-Json -InputObject $locator.Text -Depth 4 } catch { $locatorRecord = $null }
271
+ if ($locatorRecord -isnot [System.Management.Automation.PSCustomObject] -or $locatorRecord.enabled -ne $true -or [string]$locatorRecord.profile_path -cne [string]$record.profile_path) {
272
+ return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code 'LOCATOR_MALFORMED' -Reason "the locator $($script:LocatorPath) is disabled, malformed or names another profile than the activation"
273
+ }
274
+ $profileFile = Get-DiscoveryWorkspaceFileMismatch -WorkspaceRoot $workspaceRoot -RelativePath ([string]$record.profile_path) -ExpectedSha256 ([string]$record.profile_sha256) -Label 'PROFILE' -ReadBytes $ReadBytes
275
+ if ($null -ne $profileFile.Code) { return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code $profileFile.Code -Reason $profileFile.Reason }
276
+ if ($profileFile.Text -notmatch 'schema_version') {
277
+ return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code 'PROFILE_MALFORMED' -Reason "the profile $($record.profile_path) declares no schema_version"
278
+ }
279
+ $prerequisite = Get-DiscoveryPlanningPrerequisite -ProfileText $profileFile.Text
280
+ $activationSha256 = Get-DiscoverySha256Hex -Text ([string](Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @($activation.Path)))
281
+ $directory = "$(Get-DiscoverySessionDirectory -Location $location -SessionId $Normalized.SessionId)/$($script:ReceiptsPartition)"
282
+ $candidates = [System.Collections.Generic.List[object]]::new()
283
+ foreach ($entry in @(Invoke-DiscoveryStoreSeam -Seam $Store.ListEntries -Arguments @($directory))) {
284
+ if ($entry -notlike '*.json') { continue }
285
+ $text = Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @("$directory/$entry")
286
+ if ($null -eq $text) { continue }
287
+ try { $receipt = ConvertFrom-Json -InputObject ([string]$text) -Depth 16 } catch { continue }
288
+ if ($receipt -isnot [System.Management.Automation.PSCustomObject]) { continue }
289
+ $issued = ConvertTo-DiscoveryUtcInstant -Value $(if (@($receipt.PSObject.Properties.Name) -contains 'issued_at') { $receipt.issued_at } else { $null })
290
+ $candidates.Add([pscustomobject]@{ Receipt = $receipt; IssuedAt = $(if ($null -ne $issued) { $issued } else { [datetime]::MinValue }) })
291
+ }
292
+ if ($candidates.Count -eq 0) {
293
+ return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code 'NO_RECEIPT' -Reason "discovery is active with prerequisite $prerequisite and the session holds no planning receipt; run validate_discovery_artifacts with mode planning before delegating" -Prerequisite $prerequisite
294
+ }
295
+ $firstMismatch = $null
296
+ foreach ($candidate in @($candidates | Sort-Object -Property IssuedAt -Descending)) {
297
+ $mismatch = Get-DiscoveryPlanningReceiptMismatch -Receipt $candidate.Receipt -Normalized $Normalized -Activation $record -ActivationSha256 $activationSha256 -Location $location -Prerequisite $prerequisite -ReadBytes $ReadBytes
298
+ if ($null -eq $mismatch) {
299
+ return Get-DiscoveryPlanningOutcome -Decision 'allow' -Code 'PLANNING_AUTHORIZED' -Reason "a current planning receipt authorizes $target (prerequisite $prerequisite)" -Receipt $candidate.Receipt -Prerequisite $prerequisite
300
+ }
301
+ # The newest planning-mode candidate explains the refusal; other modes only when nothing else exists.
302
+ if ($null -eq $firstMismatch -or ($firstMismatch.Code -eq 'WRONG_MODE' -and $mismatch.Code -ne 'WRONG_MODE')) { $firstMismatch = $mismatch }
303
+ }
304
+ return Get-DiscoveryPlanningOutcome -Decision 'deny' -Code $firstMismatch.Code -Reason $firstMismatch.Reason -Prerequisite $prerequisite
305
+ }
306
+
307
+ function ConvertTo-DiscoveryPlanningHookOutput {
308
+ <#
309
+ .SYNOPSIS
310
+ Renders a planning outcome as the PreToolUse hookSpecificOutput envelope with the hook's reason prefix.
311
+ #>
312
+ [CmdletBinding()]
313
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
314
+ param(
315
+ [Parameter(Mandatory = $true)][pscustomobject]$Outcome,
316
+ [Parameter(Mandatory = $true)][string]$ReasonPrefix
317
+ )
318
+
319
+ if ($Outcome.Decision -eq 'allow') {
320
+ return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
321
+ }
322
+ return [ordered]@{
323
+ hookSpecificOutput = [ordered]@{
324
+ hookEventName = 'PreToolUse'
325
+ permissionDecision = 'deny'
326
+ permissionDecisionReason = "$ReasonPrefix $($Outcome.Reason)"
327
+ }
328
+ }
329
+ }
330
+
331
+ Export-ModuleMember -Function `
332
+ Get-DiscoveryPlanningTarget, `
333
+ Get-DiscoveryPlanningPrerequisite, `
334
+ Get-DiscoveryPlanningReceiptMismatch, `
335
+ Get-DiscoveryPlanningDecision, `
336
+ ConvertTo-DiscoveryPlanningHookOutput