@danmoisan/drm-copilot-mcp 1.1.23 → 1.1.24

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,296 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Agent-profile, record and store primitives for native discovery start attestation.
4
+
5
+ .DESCRIPTION
6
+ The SubagentStart adapters of Claude Code, Codex and GitHub Copilot share these
7
+ primitives: the per-host agent profile location and its Markdown front-matter
8
+ reader, UTC instant coercion for record timestamps, the started transition of
9
+ a protected invocation record (System.Text.Json preserves every untouched
10
+ value verbatim), the `start-attestation` record and the real filesystem store
11
+ seams. The discovery agent-type grammar lives in DiscoveryPayload.psm1
12
+ (`Get-DiscoveryAgentRole`). Nothing here reads an environment value or
13
+ decides; the decision lives in DiscoveryStart.psm1.
14
+
15
+ .NOTES
16
+ Issue #980. Compatible with PowerShell 7+. Depends only on the sibling
17
+ DiscoveryEncoding module.
18
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
19
+ #>
20
+
21
+ Set-StrictMode -Version Latest
22
+ $ErrorActionPreference = 'Stop'
23
+
24
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryEncoding.psm1') -ErrorAction Stop
25
+
26
+ $script:StartAttestationSchema = 'urn:drm-copilot:discovery:2.0.0:start-attestation'
27
+ $script:StartAttestationFile = 'start-attestation.json'
28
+ # Where each host keeps the generated specialist profile the root launches.
29
+ $script:AgentProfileLayout = @{
30
+ 'claude-code' = @{ Directory = '.claude/agents'; Suffix = '.md' }
31
+ 'codex' = @{ Directory = '.codex/agents'; Suffix = '.toml' }
32
+ 'github-copilot' = @{ Directory = '.github/agents'; Suffix = '.agent.md' }
33
+ }
34
+
35
+ function Get-DiscoveryAgentProfilePath {
36
+ <#
37
+ .SYNOPSIS
38
+ Returns the workspace-relative profile path of a native discovery agent on HostName.
39
+ #>
40
+ [CmdletBinding()]
41
+ [OutputType([string])]
42
+ param(
43
+ [Parameter(Mandatory = $true)][ValidateSet('claude-code', 'codex', 'github-copilot')][string]$HostName,
44
+ [Parameter(Mandatory = $true)][string]$AgentType
45
+ )
46
+
47
+ if ($AgentType -notmatch '^[A-Za-z0-9][A-Za-z0-9._-]*$') {
48
+ throw "AGENT_PROFILE_INVALID: '$AgentType' is not a valid agent profile name."
49
+ }
50
+ $layout = $script:AgentProfileLayout[$HostName]
51
+ return "$($layout.Directory)/$AgentType$($layout.Suffix)"
52
+ }
53
+
54
+ function ConvertFrom-DiscoveryMarkdownProfile {
55
+ <#
56
+ .SYNOPSIS
57
+ Reads name, model and effort from the YAML front matter of a Markdown agent profile.
58
+ .DESCRIPTION
59
+ Only scalar `key: value` lines of the first front-matter block are read; a
60
+ missing block or a missing `name` is a typed failure. Model and effort are
61
+ $null when the host profile does not declare them.
62
+ .OUTPUTS
63
+ [pscustomobject] ProfilePath, ProfileSha256, ProfileName, ProfileModel, ProfileReasoningEffort.
64
+ #>
65
+ [CmdletBinding()]
66
+ [OutputType([pscustomobject])]
67
+ param(
68
+ [Parameter(Mandatory = $true)][AllowEmptyString()][string]$Text,
69
+ [Parameter(Mandatory = $true)][string]$RelativePath,
70
+ [Parameter(Mandatory = $true)][string]$Sha256
71
+ )
72
+
73
+ $lines = $Text -split "`r?`n"
74
+ if ($lines.Count -lt 2 -or $lines[0].Trim() -ne '---') {
75
+ throw "AGENT_PROFILE_INVALID: '$RelativePath' has no YAML front matter."
76
+ }
77
+ $values = @{}
78
+ $closed = $false
79
+ foreach ($line in $lines[1..($lines.Count - 1)]) {
80
+ if ($line.Trim() -eq '---') { $closed = $true; break }
81
+ if ($line -match '^(?<key>[A-Za-z_][A-Za-z0-9_-]*):\s*(?<value>[^#]*?)\s*(?:#.*)?$' -and -not [string]::IsNullOrWhiteSpace($Matches['value'])) {
82
+ $values[$Matches['key']] = $Matches['value'].Trim().Trim('"', "'")
83
+ }
84
+ }
85
+ if (-not $closed) { throw "AGENT_PROFILE_INVALID: '$RelativePath' has an unterminated front-matter block." }
86
+ if (-not $values.ContainsKey('name')) { throw "AGENT_PROFILE_INVALID: '$RelativePath' declares no name." }
87
+ return [pscustomobject]@{
88
+ ProfilePath = $RelativePath
89
+ ProfileSha256 = $Sha256
90
+ ProfileName = [string]$values['name']
91
+ ProfileModel = $(if ($values.ContainsKey('model')) { [string]$values['model'] } else { $null })
92
+ ProfileReasoningEffort = $(if ($values.ContainsKey('effort')) { [string]$values['effort'] } elseif ($values.ContainsKey('reasoning_effort')) { [string]$values['reasoning_effort'] } else { $null })
93
+ }
94
+ }
95
+
96
+ function Read-DiscoveryAgentProfile {
97
+ <#
98
+ .SYNOPSIS
99
+ Reads and hashes the Markdown agent profile of AgentType under WorkspaceRoot.
100
+ .DESCRIPTION
101
+ Claude Code and GitHub Copilot profiles are Markdown with front matter; the
102
+ Codex adapter reads its TOML profiles through its own parser and converts the
103
+ result, so this reader refuses the codex host. ReadBytes is the filesystem seam.
104
+ #>
105
+ [CmdletBinding()]
106
+ [OutputType([pscustomobject])]
107
+ param(
108
+ [Parameter(Mandatory = $true)][ValidateSet('claude-code', 'github-copilot')][string]$HostName,
109
+ [Parameter(Mandatory = $true)][string]$AgentType,
110
+ [Parameter(Mandatory = $true)][string]$WorkspaceRoot,
111
+ [Parameter(Mandatory = $false)][scriptblock]$ReadBytes = {
112
+ param([string]$Path)
113
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
114
+ return [System.IO.File]::ReadAllBytes($Path)
115
+ }
116
+ )
117
+
118
+ $relativePath = Get-DiscoveryAgentProfilePath -HostName $HostName -AgentType $AgentType
119
+ $bytes = & $ReadBytes "$($WorkspaceRoot.TrimEnd('/', '\'))/$relativePath"
120
+ if ($null -eq $bytes) { throw "AGENT_PROFILE_INVALID: '$relativePath' does not exist." }
121
+ $bytes = [byte[]]$bytes
122
+ try {
123
+ $text = [System.Text.UTF8Encoding]::new($false, $true).GetString($bytes)
124
+ } catch {
125
+ throw "AGENT_PROFILE_INVALID: '$relativePath' is not valid UTF-8."
126
+ }
127
+ return ConvertFrom-DiscoveryMarkdownProfile -Text $text -RelativePath $relativePath -Sha256 (Get-DiscoveryBytesSha256Hex -Bytes $bytes)
128
+ }
129
+
130
+ function ConvertTo-DiscoveryUtcInstant {
131
+ <#
132
+ .SYNOPSIS
133
+ Coerces a decoded JSON timestamp (string or DateTime) to a UTC DateTime, or $null.
134
+ #>
135
+ [CmdletBinding()]
136
+ [OutputType([nullable[datetime]])]
137
+ param([Parameter(Mandatory = $false)][AllowNull()][object]$Value)
138
+
139
+ if ($Value -is [datetime]) { return $Value.ToUniversalTime() }
140
+ if ($Value -is [datetimeoffset]) { return $Value.UtcDateTime }
141
+ $parsed = [datetimeoffset]::MinValue
142
+ $styles = [System.Globalization.DateTimeStyles]::AssumeUniversal -bor [System.Globalization.DateTimeStyles]::AdjustToUniversal
143
+ if ($Value -is [string] -and [datetimeoffset]::TryParse($Value, [cultureinfo]::InvariantCulture, $styles, [ref]$parsed)) { return $parsed.UtcDateTime }
144
+ return $null
145
+ }
146
+
147
+ function ConvertTo-DiscoveryStartedInvocationText {
148
+ <#
149
+ .SYNOPSIS
150
+ Rewrites a prepared invocation record text to the started state.
151
+ .DESCRIPTION
152
+ Only `state`, `agent_id` and `start_attestation_ref` change; every other value,
153
+ including timestamps, keeps its original spelling because the document is
154
+ edited as a JSON node tree rather than re-serialized from PowerShell objects.
155
+ #>
156
+ [CmdletBinding()]
157
+ [OutputType([string])]
158
+ param(
159
+ [Parameter(Mandatory = $true)][string]$Text,
160
+ [Parameter(Mandatory = $true)][string]$AgentId,
161
+ [Parameter(Mandatory = $true)][string]$StartAttestationRef
162
+ )
163
+
164
+ $node = [System.Text.Json.Nodes.JsonNode]::Parse($Text)
165
+ if ($node -isnot [System.Text.Json.Nodes.JsonObject]) { throw 'INVOCATION_RECORD_INVALID: the invocation record is not a JSON object.' }
166
+ $node['state'] = [System.Text.Json.Nodes.JsonValue]::Create('started')
167
+ $node['agent_id'] = [System.Text.Json.Nodes.JsonValue]::Create($AgentId)
168
+ $node['start_attestation_ref'] = [System.Text.Json.Nodes.JsonValue]::Create($StartAttestationRef)
169
+ $options = [System.Text.Json.JsonSerializerOptions]::new()
170
+ $options.WriteIndented = $true
171
+ $options.Encoder = [System.Text.Encodings.Web.JavaScriptEncoder]::UnsafeRelaxedJsonEscaping
172
+ return $node.ToJsonString($options) + "`n"
173
+ }
174
+
175
+ function ConvertTo-DiscoveryStartAttestationRecord {
176
+ <#
177
+ .SYNOPSIS
178
+ Builds the `start-attestation` record binding the trusted principal to one prepared invocation.
179
+ .DESCRIPTION
180
+ Every identity value comes from the principal, the protected record and the
181
+ host's profile read; the SubagentStart payload contributes only the agent
182
+ identifier the host assigned. ActualModel and ActualReasoningEffort are $null
183
+ when the host does not expose them in its start event.
184
+ #>
185
+ [CmdletBinding()]
186
+ [OutputType([System.Collections.Specialized.OrderedDictionary])]
187
+ param(
188
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
189
+ [Parameter(Mandatory = $true)][string]$AgentId,
190
+ [Parameter(Mandatory = $true)][string]$AgentType,
191
+ [Parameter(Mandatory = $true)][pscustomobject]$Record,
192
+ [Parameter(Mandatory = $true)][pscustomobject]$AgentProfile,
193
+ [Parameter(Mandatory = $false)][AllowNull()][string]$ActualModel,
194
+ [Parameter(Mandatory = $false)][AllowNull()][string]$ActualReasoningEffort,
195
+ [Parameter(Mandatory = $true)][string]$HandlerSha256,
196
+ [Parameter(Mandatory = $true)][datetime]$Now,
197
+ [Parameter(Mandatory = $true)][string]$Nonce
198
+ )
199
+
200
+ return [ordered]@{
201
+ '$schema' = $script:StartAttestationSchema
202
+ schema_version = '2.0.0'
203
+ artifact_kind = 'start-attestation'
204
+ id = "start-$Nonce"
205
+ start_attestation_ref = "start-$Nonce"
206
+ invocation_ref = [string]$Record.invocation_id
207
+ workspace = [ordered]@{
208
+ workspace_root = [string]$Record.workspace.workspace_root
209
+ workspace_sha256 = [string]$Record.workspace.workspace_sha256
210
+ profile_path = [string]$Record.workspace.profile_path
211
+ profile_sha256 = [string]$Record.workspace.profile_sha256
212
+ }
213
+ host = $Principal.HostName
214
+ session_id = $Principal.SessionId
215
+ parent_agent_id = $Principal.ParentAgentId
216
+ agent_id = $AgentId
217
+ agent_type = $AgentType
218
+ role = [string]$Record.role
219
+ deployment = [ordered]@{
220
+ agent_name = [string]$Record.deployment.agent_name
221
+ model = [string]$Record.deployment.model
222
+ reasoning_effort = [string]$Record.deployment.reasoning_effort
223
+ actual_model = $(if ([string]::IsNullOrEmpty($ActualModel)) { $null } else { $ActualModel })
224
+ actual_effort = $(if ([string]::IsNullOrEmpty($ActualReasoningEffort)) { $null } else { $ActualReasoningEffort })
225
+ }
226
+ agent_profile = [ordered]@{
227
+ path = $AgentProfile.ProfilePath
228
+ sha256 = $AgentProfile.ProfileSha256
229
+ name = $AgentProfile.ProfileName
230
+ }
231
+ capability_ref = [string]$Record.capability_ref
232
+ runtime = $Record.runtime
233
+ attested_at = $Now.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")
234
+ writer = [ordered]@{ host = $Principal.HostName; event = 'SubagentStart'; handler_sha256 = $HandlerSha256 }
235
+ }
236
+ }
237
+
238
+ function Get-DiscoveryStartStore {
239
+ <#
240
+ .SYNOPSIS
241
+ Returns the real-filesystem store seams (ReadText, ListEntries, WriteText, ReplaceText).
242
+ .DESCRIPTION
243
+ WriteText creates the attestation exclusively, so a second start for the same
244
+ invocation fails; ReplaceText rewrites the invocation record in place.
245
+ #>
246
+ [CmdletBinding()]
247
+ [OutputType([hashtable])]
248
+ param()
249
+
250
+ return @{
251
+ ReadText = {
252
+ param([string]$Path)
253
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
254
+ return [System.IO.File]::ReadAllText($Path, [System.Text.Encoding]::UTF8)
255
+ }
256
+ ListEntries = {
257
+ param([string]$Directory)
258
+ if (-not (Test-Path -LiteralPath $Directory -PathType Container)) { return @() }
259
+ return @(Get-ChildItem -LiteralPath $Directory -Force | Sort-Object -Property Name | ForEach-Object { $_.Name })
260
+ }
261
+ WriteText = {
262
+ param([string]$Path, [string]$Text)
263
+ $stream = [System.IO.File]::Open($Path, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None)
264
+ try {
265
+ $bytes = [System.Text.Encoding]::UTF8.GetBytes($Text)
266
+ $stream.Write($bytes, 0, $bytes.Length)
267
+ } finally { $stream.Dispose() }
268
+ }
269
+ ReplaceText = {
270
+ param([string]$Path, [string]$Text)
271
+ [System.IO.File]::WriteAllText($Path, $Text, [System.Text.UTF8Encoding]::new($false))
272
+ }
273
+ }
274
+ }
275
+
276
+ function Get-DiscoveryStartAttestationFileName {
277
+ <#
278
+ .SYNOPSIS
279
+ Returns the file name of the start attestation inside an invocation directory.
280
+ #>
281
+ [CmdletBinding()]
282
+ [OutputType([string])]
283
+ param()
284
+
285
+ return $script:StartAttestationFile
286
+ }
287
+
288
+ Export-ModuleMember -Function `
289
+ Get-DiscoveryAgentProfilePath, `
290
+ ConvertFrom-DiscoveryMarkdownProfile, `
291
+ Read-DiscoveryAgentProfile, `
292
+ ConvertTo-DiscoveryUtcInstant, `
293
+ ConvertTo-DiscoveryStartedInvocationText, `
294
+ ConvertTo-DiscoveryStartAttestationRecord, `
295
+ Get-DiscoveryStartStore, `
296
+ Get-DiscoveryStartAttestationFileName
@@ -0,0 +1,336 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Native SubagentStop validation of a discovery specialist's protected output authority.
4
+
5
+ .DESCRIPTION
6
+ Every native discovery SubagentStop adapter (Claude Code, Codex, GitHub
7
+ Copilot) takes its decision from this module. A stop whose agent type is not
8
+ a discovery specialist is skipped, so ordinary and epic stop validation keeps
9
+ its existing path. For a discovery specialist the stop result is derived from
10
+ the protected invocation bound to the host-assigned agent identity, never from
11
+ the final assistant message: the invocation must be finalized through the
12
+ gateway, its completion receipt must be the gateway-written `validation-receipt`
13
+ for this invocation, session, role and gate mode, bound to the current
14
+ activation and workspace, with every required output still present, every
15
+ input unchanged since the receipt (re-read and re-hashed) and output_valid
16
+ true; only the migration-coverage-reviewer may stop on a valid failing report.
17
+ Filenames or success prose in the final text cannot admit a stop, and a blank
18
+ final text cannot skip the validation.
19
+
20
+ Store and workspace access flows through injected seams; no process is invoked
21
+ and no orchestrator checkpoint is read or written.
22
+
23
+ .NOTES
24
+ Issue #980. Compatible with PowerShell 7+. Depends only on the sibling
25
+ DiscoveryEncoding, DiscoveryAuthority, DiscoveryManifest, DiscoveryPayload and
26
+ DiscoveryStartRecord modules.
27
+ CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
28
+ #>
29
+
30
+ Set-StrictMode -Version Latest
31
+ $ErrorActionPreference = 'Stop'
32
+
33
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryEncoding.psm1') -ErrorAction Stop
34
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryAuthority.psm1') -ErrorAction Stop
35
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryManifest.psm1') -ErrorAction Stop
36
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryPayload.psm1') -ErrorAction Stop
37
+ Import-Module (Join-Path $PSScriptRoot 'DiscoveryStartRecord.psm1') -ErrorAction Stop
38
+
39
+ $script:ReceiptsPartition = 'receipts'
40
+ $script:ReviewerRole = 'migration-coverage-reviewer'
41
+ $script:ResultTokenPattern = '(?<![A-Za-z])(PASS|FAIL|BLOCKED)(?![A-Za-z])'
42
+ $script:CorrelationIdPattern = '^[A-Za-z0-9][A-Za-z0-9._:-]*$'
43
+ $script:RuntimeKeys = @('build_sha256', 'package_version', 'policy_sha256', 'policy_version', 'schema_version')
44
+
45
+ function Get-DiscoveryStopOutcome {
46
+ <#
47
+ .SYNOPSIS
48
+ Builds one stop outcome (skip, allow or block) with its code and reason.
49
+ #>
50
+ [CmdletBinding()]
51
+ [OutputType([pscustomobject])]
52
+ param(
53
+ [Parameter(Mandatory = $true)][ValidateSet('skip', 'allow', 'block')][string]$Decision,
54
+ [Parameter(Mandatory = $true)][string]$Code,
55
+ [Parameter(Mandatory = $true)][string]$Reason,
56
+ [Parameter(Mandatory = $false)][AllowNull()][object]$Receipt = $null,
57
+ [Parameter(Mandatory = $false)][AllowNull()][object]$Invocation = $null
58
+ )
59
+
60
+ return [pscustomobject]@{ Decision = $Decision; Code = $Code; Reason = $Reason; Receipt = $Receipt; Invocation = $Invocation }
61
+ }
62
+
63
+ function Read-DiscoveryStopJson {
64
+ <#
65
+ .SYNOPSIS
66
+ Reads one JSON object from the store, returning Text and Record or $null members.
67
+ #>
68
+ [CmdletBinding()]
69
+ [OutputType([pscustomobject])]
70
+ param(
71
+ [Parameter(Mandatory = $true)][hashtable]$Store,
72
+ [Parameter(Mandatory = $true)][string]$Path
73
+ )
74
+
75
+ $text = Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @($Path)
76
+ if ($null -eq $text) { return [pscustomobject]@{ Text = $null; Record = $null } }
77
+ try { $record = ConvertFrom-Json -InputObject ([string]$text) -Depth 16 } catch { $record = $null }
78
+ if ($record -isnot [System.Management.Automation.PSCustomObject]) { $record = $null }
79
+ return [pscustomobject]@{ Text = [string]$text; Record = $record }
80
+ }
81
+
82
+ function Find-DiscoveryAgentInvocationForStop {
83
+ <#
84
+ .SYNOPSIS
85
+ Finds the discovery invocations bound to the stopping agent (started or finalized).
86
+ .DESCRIPTION
87
+ Enumerates `<session>/invocations/*/invocation.json` and keeps every
88
+ `discovery-invocation` whose host, session, parent, agent type and agent id
89
+ equal the stopping principal's. An unreadable record is skipped.
90
+ .OUTPUTS
91
+ [pscustomobject] Count, Record, Directory.
92
+ #>
93
+ [CmdletBinding()]
94
+ [OutputType([pscustomobject])]
95
+ param(
96
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
97
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
98
+ [Parameter(Mandatory = $true)][string]$AgentType,
99
+ [Parameter(Mandatory = $true)][string]$AgentId,
100
+ [Parameter(Mandatory = $true)][hashtable]$Store
101
+ )
102
+
103
+ $directory = "$(Get-DiscoverySessionDirectory -Location $Location -SessionId $Principal.SessionId)/invocations"
104
+ $bindings = [System.Collections.Generic.List[object]]::new()
105
+ foreach ($entry in @(Invoke-DiscoveryStoreSeam -Seam $Store.ListEntries -Arguments @($directory))) {
106
+ $read = Read-DiscoveryStopJson -Store $Store -Path "$directory/$entry/invocation.json"
107
+ $record = $read.Record
108
+ if ($null -eq $record) { continue }
109
+ $names = @($record.PSObject.Properties.Name)
110
+ $missing = @(@('artifact_kind', 'host', 'session_id', 'parent_agent_id', 'agent_id', 'state', 'deployment') | Where-Object { $names -notcontains $_ })
111
+ if ($missing.Count -gt 0 -or $record.deployment -isnot [System.Management.Automation.PSCustomObject]) { continue }
112
+ $bound = ($record.artifact_kind -eq 'discovery-invocation') -and ($record.host -eq $Principal.HostName) -and
113
+ ($record.session_id -eq $Principal.SessionId) -and ($record.parent_agent_id -eq $Principal.ParentAgentId) -and
114
+ ([string]$record.deployment.agent_name -ceq $AgentType) -and ([string]$record.agent_id -ceq $AgentId) -and
115
+ (@('started', 'finalized') -contains [string]$record.state)
116
+ if ($bound) { $bindings.Add([pscustomobject]@{ Record = $record; Directory = "$directory/$entry" }) }
117
+ }
118
+ if ($bindings.Count -ne 1) { return [pscustomobject]@{ Count = $bindings.Count; Record = $null; Directory = $null } }
119
+ return [pscustomobject]@{ Count = 1; Record = $bindings[0].Record; Directory = $bindings[0].Directory }
120
+ }
121
+
122
+ function Get-DiscoveryReceiptMismatch {
123
+ <#
124
+ .SYNOPSIS
125
+ Compares the completion receipt against the invocation, principal, activation and current workspace.
126
+ .DESCRIPTION
127
+ Returns $null when the receipt authorizes the stop, otherwise an object with
128
+ Code (RECEIPT, GATE, IDENTITY, FORGED, BINDING, STALE_ACTIVATION, RUNTIME,
129
+ OUTPUTS, OUTPUT_INVALID, RESULT, STALE_INPUTS or OUTPUT_MISSING) and Reason.
130
+ #>
131
+ [CmdletBinding()]
132
+ [OutputType([pscustomobject])]
133
+ param(
134
+ [Parameter(Mandatory = $true)][pscustomobject]$Receipt,
135
+ [Parameter(Mandatory = $true)][pscustomobject]$Record,
136
+ [Parameter(Mandatory = $true)][pscustomobject]$Principal,
137
+ [Parameter(Mandatory = $true)][string]$AgentId,
138
+ [Parameter(Mandatory = $true)][pscustomobject]$Activation,
139
+ [Parameter(Mandatory = $true)][string]$ActivationSha256,
140
+ [Parameter(Mandatory = $true)][pscustomobject]$Location,
141
+ [Parameter(Mandatory = $true)][scriptblock]$ReadBytes
142
+ )
143
+
144
+ $names = @($Receipt.PSObject.Properties.Name)
145
+ foreach ($required in @('artifact_kind', 'receipt_ref', 'mode', 'identity', 'writer', 'workspace', 'activation_sha256', 'runtime', 'required_outputs', 'output_valid', 'validation_result', 'input_manifest')) {
146
+ if ($names -notcontains $required) { return [pscustomobject]@{ Code = 'RECEIPT'; Reason = "the completion receipt lacks '$required'" } }
147
+ }
148
+ if ($Receipt.artifact_kind -ne 'validation-receipt' -or [string]$Receipt.receipt_ref -cne [string]$Record.completion_receipt_ref) {
149
+ return [pscustomobject]@{ Code = 'RECEIPT'; Reason = 'the completion receipt is not the validation-receipt the invocation names' }
150
+ }
151
+ if ([string]$Receipt.mode -ne 'agent-output') {
152
+ return [pscustomobject]@{ Code = 'GATE'; Reason = "the completion receipt was issued for gate mode '$($Receipt.mode)', not agent-output" }
153
+ }
154
+ $identity = $Receipt.identity
155
+ $identityBound = ($identity -is [System.Management.Automation.PSCustomObject]) -and ($identity.kind -eq 'discovery-agent') -and
156
+ ($identity.host -eq $Principal.HostName) -and ($identity.session_id -eq $Principal.SessionId) -and
157
+ ([string]$identity.agent_id -ceq $AgentId) -and ([string]$identity.role -ceq [string]$Record.role) -and
158
+ ([string]$identity.invocation_id -ceq [string]$Record.invocation_id)
159
+ if (-not $identityBound) {
160
+ return [pscustomobject]@{ Code = 'IDENTITY'; Reason = 'the completion receipt is bound to another agent, session, role or invocation' }
161
+ }
162
+ $writer = $Receipt.writer
163
+ if ($writer -isnot [System.Management.Automation.PSCustomObject] -or $writer.kind -ne 'host-gateway' -or $writer.host -ne $Principal.HostName) {
164
+ return [pscustomobject]@{ Code = 'FORGED'; Reason = 'the completion receipt was not written by the host gateway' }
165
+ }
166
+ $sameBinding = ([string]$Receipt.workspace.workspace_sha256 -eq $Location.WorkspaceSha256) -and
167
+ ([string]$Receipt.workspace.workspace_root -eq [string]$Record.workspace.workspace_root) -and
168
+ ([string]$Receipt.workspace.profile_path -eq [string]$Activation.profile_path) -and
169
+ ([string]$Receipt.workspace.profile_sha256 -eq [string]$Activation.profile_sha256)
170
+ if (-not $sameBinding) {
171
+ return [pscustomobject]@{ Code = 'BINDING'; Reason = 'the completion receipt is bound to another workspace or profile than the current activation' }
172
+ }
173
+ if ([string]$Receipt.activation_sha256 -cne $ActivationSha256) {
174
+ return [pscustomobject]@{ Code = 'STALE_ACTIVATION'; Reason = 'the activation record changed after the completion receipt was issued' }
175
+ }
176
+ foreach ($key in $script:RuntimeKeys) {
177
+ if ([string]$Receipt.runtime.$key -cne [string]$Record.runtime.$key) {
178
+ return [pscustomobject]@{ Code = 'RUNTIME'; Reason = "runtime pin $key of the receipt differs from the invocation" }
179
+ }
180
+ }
181
+ $outputs = $Receipt.required_outputs
182
+ if ($outputs -isnot [System.Management.Automation.PSCustomObject] -or [string]$outputs.manifest_sha256 -cne [string]$Record.required_outputs.manifest_sha256) {
183
+ return [pscustomobject]@{ Code = 'OUTPUTS'; Reason = 'the completion receipt does not carry the required-output manifest of the invocation' }
184
+ }
185
+ if ($Receipt.output_valid -isnot [bool] -or -not [bool]$Receipt.output_valid) {
186
+ return [pscustomobject]@{ Code = 'OUTPUT_INVALID'; Reason = 'the completion receipt does not mark the required outputs valid' }
187
+ }
188
+ $result = [string]$Receipt.validation_result
189
+ if ($result -ne 'pass' -and -not ($result -eq 'fail' -and [string]$Record.role -eq $script:ReviewerRole)) {
190
+ return [pscustomobject]@{ Code = 'RESULT'; Reason = "validation result '$result' does not complete role $($Record.role); only the reviewer may stop on a valid failing report" }
191
+ }
192
+ try {
193
+ $manifest = ConvertFrom-DiscoveryInputManifest -Value $Receipt.input_manifest
194
+ } catch {
195
+ return [pscustomobject]@{ Code = 'RECEIPT'; Reason = "the completion receipt input manifest is invalid: $($_.Exception.Message)" }
196
+ }
197
+ $comparison = Compare-DiscoveryInputManifest -Manifest $manifest -WorkspaceRoot ([string]$Record.workspace.workspace_root) `
198
+ -ProfilePath ([string]$Activation.profile_path) -ProfileSha256 ([string]$Activation.profile_sha256) -ReadBytes $ReadBytes
199
+ if ($comparison.Drift.Count -gt 0) {
200
+ $summary = @($comparison.Drift | ForEach-Object { "$($_.kind):$($_.path)" }) -join ', '
201
+ return [pscustomobject]@{ Code = 'STALE_INPUTS'; Reason = "inputs changed after the completion receipt: $summary" }
202
+ }
203
+ foreach ($output in @($outputs.outputs)) {
204
+ $rowNames = @($output.PSObject.Properties.Name)
205
+ if (@('artifact_kind', 'id', 'path') | Where-Object { $rowNames -notcontains $_ -or [string]::IsNullOrWhiteSpace([string]$output.$_) }) {
206
+ return [pscustomobject]@{ Code = 'OUTPUTS'; Reason = 'a required output row lacks its kind, id or path' }
207
+ }
208
+ $bytes = & $ReadBytes (Get-DiscoveryWorkspacePath -WorkspaceRoot ([string]$Record.workspace.workspace_root) -RelativePath ([string]$output.path))
209
+ if ($null -eq $bytes) {
210
+ return [pscustomobject]@{ Code = 'OUTPUT_MISSING'; Reason = "required output $($output.path) ($($output.artifact_kind) $($output.id)) is absent from the workspace" }
211
+ }
212
+ }
213
+ return $null
214
+ }
215
+
216
+ function Get-DiscoveryStopDecision {
217
+ <#
218
+ .SYNOPSIS
219
+ Decides one SubagentStop event for a native discovery adapter.
220
+ .DESCRIPTION
221
+ Order: envelope anomalies block; a non-discovery agent type is skipped; a
222
+ missing agent identity in the payload blocks; the activation must be active; exactly one
223
+ started or finalized invocation must bind this agent; the invocation must be
224
+ finalized with a completion receipt that passes Get-DiscoveryReceiptMismatch;
225
+ finally the final text must state the explicit result the receipt proves.
226
+ When StopHookActive is true a block becomes an allow with code CONTINUED so
227
+ the host does not loop; the authority remains with the receipts.
228
+ .OUTPUTS
229
+ [pscustomobject] Decision, Code, Reason, Receipt, Invocation.
230
+ #>
231
+ [CmdletBinding()]
232
+ [OutputType([pscustomobject])]
233
+ param(
234
+ [Parameter(Mandatory = $true)][pscustomobject]$Normalized,
235
+ [Parameter(Mandatory = $true)][hashtable]$HostEnvironment,
236
+ [Parameter(Mandatory = $false)][AllowNull()][AllowEmptyString()][string]$AssistantText = '',
237
+ [Parameter(Mandatory = $false)][bool]$StopHookActive = $false,
238
+ [Parameter(Mandatory = $false)][hashtable]$Store = (Get-DiscoveryStartStore),
239
+ [Parameter(Mandatory = $false)][AllowNull()][scriptblock]$ReadBytes = $null
240
+ )
241
+
242
+ $outcome = Get-DiscoveryStopVerdict -Normalized $Normalized -HostEnvironment $HostEnvironment `
243
+ -AssistantText $AssistantText -Store $Store -ReadBytes $ReadBytes
244
+ if ($outcome.Decision -eq 'block' -and $StopHookActive -and $outcome.Code -ne 'ENVELOPE') {
245
+ return Get-DiscoveryStopOutcome -Decision 'allow' -Code 'CONTINUED' -Reason "$($outcome.Reason) (a continuation was already requested; the protected receipts remain authoritative)" -Receipt $outcome.Receipt -Invocation $outcome.Invocation
246
+ }
247
+ return $outcome
248
+ }
249
+
250
+ function Get-DiscoveryStopVerdict {
251
+ <#
252
+ .SYNOPSIS
253
+ Evaluates the stop without the continuation rule; see Get-DiscoveryStopDecision.
254
+ #>
255
+ [CmdletBinding()]
256
+ [OutputType([pscustomobject])]
257
+ param(
258
+ [Parameter(Mandatory = $true)][pscustomobject]$Normalized,
259
+ [Parameter(Mandatory = $true)][hashtable]$HostEnvironment,
260
+ [Parameter(Mandatory = $false)][AllowNull()][AllowEmptyString()][string]$AssistantText = '',
261
+ [Parameter(Mandatory = $true)][hashtable]$Store,
262
+ [Parameter(Mandatory = $false)][AllowNull()][scriptblock]$ReadBytes = $null
263
+ )
264
+
265
+ if ($null -ne $Normalized.Anomaly) {
266
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'ENVELOPE' -Reason ('payload anomaly - ' + (Get-DiscoveryPayloadAnomalyReason -Code $Normalized.Anomaly))
267
+ }
268
+ $agentType = [string]$Normalized.AgentType
269
+ $role = Get-DiscoveryAgentRole -AgentType $agentType
270
+ if ($null -eq $role) {
271
+ return Get-DiscoveryStopOutcome -Decision 'skip' -Code 'NOT_DISCOVERY_AGENT' -Reason 'the stopping agent is not a discovery specialist; the ordinary stop validation applies'
272
+ }
273
+ # The principal (host, session, parent, agent) comes from the stop payload alone.
274
+ $principal = Get-DiscoveryTrustedPrincipal -Normalized $Normalized
275
+ if ($null -eq $principal.AgentId) {
276
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'IDENTITY' -Reason 'the native stop event carries no agent identifier for the discovery specialist'
277
+ }
278
+ $agentId = $principal.AgentId
279
+ if ([string]::IsNullOrWhiteSpace([string]$Normalized.Cwd)) {
280
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'WORKSPACE' -Reason 'the native event carries no workspace directory'
281
+ }
282
+ try {
283
+ $location = Resolve-DiscoveryAuthorityLocation -WorkspaceRoot ([string]$Normalized.Cwd) -Platform $HostEnvironment.Platform `
284
+ -UserHome $HostEnvironment.UserHome -LocalAppData $HostEnvironment.LocalAppData -XdgStateHome $HostEnvironment.XdgStateHome
285
+ } catch {
286
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'WORKSPACE' -Reason $_.Exception.Message
287
+ }
288
+ $activation = Read-DiscoveryActivationState -Location $location -ReadText $Store.ReadText
289
+ if ($activation.Status -ne 'active') {
290
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'ACTIVATION' -Reason "discovery is not active for this workspace ($($activation.Status): $($activation.Reason))"
291
+ }
292
+ $activationText = [string](Invoke-DiscoveryStoreSeam -Seam $Store.ReadText -Arguments @($activation.Path))
293
+ $found = Find-DiscoveryAgentInvocationForStop -Location $location -Principal $principal -AgentType $agentType -AgentId $agentId -Store $Store
294
+ if ($found.Count -eq 0) {
295
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'NO_INVOCATION' -Reason "no protected invocation binds agent $agentId ($agentType) in this session; the specialist stopped without an attested start"
296
+ }
297
+ if ($found.Count -gt 1) {
298
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'AMBIGUOUS' -Reason "$($found.Count) protected invocations bind agent $agentId; the stop cannot be attributed to one"
299
+ }
300
+ $record = $found.Record
301
+ if ([string]$record.role -cne $role) {
302
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'ROLE' -Reason "agent type $agentType acts as $role but the invocation was prepared for role $($record.role)" -Invocation $record
303
+ }
304
+ if ([string]$record.state -ne 'finalized' -or [string]::IsNullOrWhiteSpace([string]$record.completion_receipt_ref)) {
305
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'NOT_FINALIZED' -Reason "invocation $($record.invocation_id) was not finalized through finalize_discovery_agent; no output authority exists for this stop" -Invocation $record
306
+ }
307
+ $read = Read-DiscoveryStopJson -Store $Store -Path "$($found.Directory)/$($script:ReceiptsPartition)/$($record.completion_receipt_ref).json"
308
+ if ($null -eq $read.Record) {
309
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'RECEIPT' -Reason "the completion receipt $($record.completion_receipt_ref) is missing or unreadable in the protected store" -Invocation $record
310
+ }
311
+ if ($null -eq $ReadBytes) {
312
+ $ReadBytes = {
313
+ param([string]$Path)
314
+ if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $null }
315
+ return [System.IO.File]::ReadAllBytes($Path)
316
+ }
317
+ }
318
+ $mismatch = Get-DiscoveryReceiptMismatch -Receipt $read.Record -Record $record -Principal $principal -AgentId $agentId -Activation $activation.Record `
319
+ -ActivationSha256 (Get-DiscoverySha256Hex -Text $activationText) -Location $location -ReadBytes $ReadBytes
320
+ if ($null -ne $mismatch) {
321
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code $mismatch.Code -Reason $mismatch.Reason -Receipt $read.Record -Invocation $record
322
+ }
323
+ # The final text never grants authority; it must only state the result the receipt proves.
324
+ $expectedToken = $(if ([string]$read.Record.validation_result -eq 'pass') { 'PASS' } else { 'FAIL' })
325
+ $tokens = @([regex]::Matches([string]$AssistantText, $script:ResultTokenPattern) | ForEach-Object { $_.Groups[1].Value })
326
+ if ($tokens -notcontains $expectedToken) {
327
+ return Get-DiscoveryStopOutcome -Decision 'block' -Code 'RESULT_TEXT' -Reason "the final message must state the explicit $expectedToken result the protected receipt proves" -Receipt $read.Record -Invocation $record
328
+ }
329
+ return Get-DiscoveryStopOutcome -Decision 'allow' -Code 'VALIDATED' -Reason "invocation $($record.invocation_id) of $role completed with a current $($read.Record.validation_result) receipt" -Receipt $read.Record -Invocation $record
330
+ }
331
+
332
+ Export-ModuleMember -Function `
333
+ Find-DiscoveryAgentInvocationForStop, `
334
+ Get-DiscoveryReceiptMismatch, `
335
+ Get-DiscoveryStopDecision, `
336
+ Get-DiscoveryStopVerdict