@danmoisan/drm-copilot-mcp 1.1.22 → 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.
- package/out/mcp-server.js +18758 -12030
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agent-memory/orchestrator/MEMORY.md +1 -1
- package/resources/claude-customizations/.claude/agent-memory/orchestrator/feedback_test_files_count_against_500_cap.md +4 -4
- package/resources/claude-customizations/.claude/agents/csharp-typed-engineer.md +1 -1
- package/resources/claude-customizations/.claude/agents/discovery-capability-sentinel.md +34 -0
- package/resources/claude-customizations/.claude/agents/feature-review.md +8 -0
- package/resources/claude-customizations/.claude/agents/legacy-parity-analyst.md +101 -43
- package/resources/claude-customizations/.claude/agents/migration-coverage-reviewer.md +107 -43
- package/resources/claude-customizations/.claude/agents/orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +60 -6
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +21 -1
- package/resources/claude-customizations/.claude/agents/powershell-typed-engineer.md +1 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +48 -7
- package/resources/claude-customizations/.claude/agents/python-typed-engineer.md +1 -1
- package/resources/claude-customizations/.claude/agents/requirements-reconciler.md +117 -42
- package/resources/claude-customizations/.claude/agents/runtime-characterization-analyst.md +103 -43
- package/resources/claude-customizations/.claude/hooks/discovery-capability-probe.ps1 +102 -0
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +69 -194
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-planning-gate.ps1 +87 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-green-ci.ps1 +325 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +29 -7
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate-resolution.ps1 +23 -10
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +29 -21
- package/resources/claude-customizations/.claude/hooks/enforce-file-size-limit.ps1 +244 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +51 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +168 -48
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +1 -1
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +22 -11
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +59 -20
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-command-allowlist.ps1 +51 -4
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +72 -11
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.current-branch.ps1 +149 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +4 -2
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +59 -20
- package/resources/claude-customizations/.claude/hooks/record-discovery-agent-start.ps1 +132 -0
- package/resources/claude-customizations/.claude/hooks/validate-discovery-artifact-gate.ps1 +56 -212
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-feature-folder.ps1 +146 -0
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-rejection.ps1 +172 -0
- package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output.ps1 +34 -4
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +9 -3
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusAggregate.psm1 +154 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusCombination.psm1 +228 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +1 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +4 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +63 -16
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusTokenShape.psm1 +56 -1
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +4 -1
- package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +33 -6
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDeployment.psm1 +5 -1
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
- package/resources/claude-customizations/.claude/lib/codex-routing/CodexTopology.psm1 +25 -2
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
- package/resources/claude-customizations/.claude/lib/discovery-validation/DiscoveryValidation.psm1 +188 -341
- package/resources/claude-customizations/.claude/lib/file-size-policy/FileSizePolicy.psm1 +434 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1 +166 -0
- package/resources/claude-customizations/.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1 +8 -3
- package/resources/claude-customizations/.claude/lib/worktree-resolution/HookTargetWorktree.psm1 +391 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunRecordPath.psm1 +185 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeRunResolution.psm1 +11 -21
- package/resources/claude-customizations/.claude/rules/general-code-change.md +4 -1
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +29 -5
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +152 -20
- package/resources/claude-customizations/.claude/rules/powershell.md +1 -1
- package/resources/claude-customizations/.claude/rules/shell.md +1 -1
- package/resources/claude-customizations/.claude/settings.json +36 -3
- package/resources/claude-customizations/.claude/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
- package/resources/claude-customizations/.claude/skills/characterize-runtime-behavior/SKILL.md +134 -0
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +2 -2
- package/resources/claude-customizations/.claude/skills/discovery-behavior-reconciliation/SKILL.md +28 -47
- package/resources/claude-customizations/.claude/skills/discovery-coverage-ledger/SKILL.md +32 -48
- package/resources/claude-customizations/.claude/skills/discovery-parity-matrix/SKILL.md +28 -45
- package/resources/claude-customizations/.claude/skills/discovery-repo-inventory/SKILL.md +28 -62
- package/resources/claude-customizations/.claude/skills/discovery-runtime-characterization/SKILL.md +28 -45
- package/resources/claude-customizations/.claude/skills/discovery-validate-artifacts/SKILL.md +28 -61
- package/resources/claude-customizations/.claude/skills/discovery-workflow/SKILL.md +55 -139
- package/resources/claude-customizations/.claude/skills/execution-environment/SKILL.md +25 -10
- package/resources/claude-customizations/.claude/skills/extract-feature-contract/SKILL.md +133 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +1 -0
- package/resources/claude-customizations/.claude/skills/generate-acceptance-scenarios/SKILL.md +132 -0
- package/resources/claude-customizations/.claude/skills/initialize-discovery-workspace/SKILL.md +131 -0
- package/resources/claude-customizations/.claude/skills/inventory-legacy-system/SKILL.md +131 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +15 -7
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +25 -2
- package/resources/claude-customizations/.claude/skills/parallel-close/SKILL.md +11 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +124 -14
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +72 -7
- package/resources/claude-customizations/.claude/skills/parallel-remove/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/parallel-run/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +56 -9
- package/resources/claude-customizations/.claude/skills/reconcile-parity-matrix/SKILL.md +131 -0
- package/resources/claude-customizations/.claude/skills/review-discovery-coverage/SKILL.md +129 -0
- package/resources/claude-customizations/.claude/skills/validate-discovery-artifacts/SKILL.md +127 -0
- package/resources/claude-customizations/.claude-variants/csharp-legacy/agents/csharp-typed-engineer.md +1 -1
- package/resources/claude-customizations/config/blast-radius.json +6 -5
- package/resources/claude-customizations/config/orchestration-routing.json +61 -1
- package/resources/claude-customizations/pack-manifests/core.json +40 -1
- package/resources/codex-and-agents-customizations/.agents/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
- package/resources/codex-and-agents-customizations/.agents/skills/atomic-planner/SKILL.md +7 -0
- package/resources/codex-and-agents-customizations/.agents/skills/characterize-runtime-behavior/SKILL.md +134 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-coverage-ledger/SKILL.md +50 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-parity-matrix/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-repo-inventory/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-runtime-characterization/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-validate-artifacts/SKILL.md +46 -0
- package/resources/codex-and-agents-customizations/.agents/skills/discovery-workflow/SKILL.md +62 -0
- package/resources/codex-and-agents-customizations/.agents/skills/execution-environment/SKILL.md +25 -10
- package/resources/codex-and-agents-customizations/.agents/skills/extract-feature-contract/SKILL.md +133 -0
- package/resources/codex-and-agents-customizations/.agents/skills/feature-promotion-lifecycle/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/feature-review/SKILL.md +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/general-code-change/SKILL.md +4 -1
- package/resources/codex-and-agents-customizations/.agents/skills/generate-acceptance-scenarios/SKILL.md +132 -0
- package/resources/codex-and-agents-customizations/.agents/skills/initialize-discovery-workspace/SKILL.md +131 -0
- package/resources/codex-and-agents-customizations/.agents/skills/inventory-legacy-system/SKILL.md +131 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-workflow/SKILL.md +8 -0
- package/resources/codex-and-agents-customizations/.agents/skills/powershell/SKILL.md +1 -1
- package/resources/codex-and-agents-customizations/.agents/skills/reconcile-parity-matrix/SKILL.md +131 -0
- package/resources/codex-and-agents-customizations/.agents/skills/review-discovery-coverage/SKILL.md +129 -0
- package/resources/codex-and-agents-customizations/.agents/skills/validate-discovery-artifacts/SKILL.md +127 -0
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/csharp-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/discovery-capability-sentinel.toml +51 -0
- package/resources/codex-and-agents-customizations/.codex/agents/feature-review.toml +8 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c1.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c2.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3-elevated.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c3.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst-c4.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/legacy-parity-analyst.toml +148 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c1.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c2.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3-elevated.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c3.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer-c4.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/migration-coverage-reviewer.toml +154 -0
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/powershell-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c1.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c2.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3-elevated.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c3.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer-c4.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/python-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c1.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c2.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3-elevated.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c3.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler-c4.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/requirements-reconciler.toml +164 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c1.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c2.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3-elevated.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c3.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst-c4.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/agents/runtime-characterization-analyst.toml +150 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +39 -3
- package/resources/codex-and-agents-customizations/.codex/hooks/codex-agent-profile-attestation.ps1 +22 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/discovery-capability-probe.ps1 +97 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/discovery-start-attestation.ps1 +171 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-codex-model-routing.ps1 +4 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-artifact-gate.ps1 +187 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-discovery-planning-gate.ps1 +149 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-file-size-limit.ps1 +282 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +239 -2
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -15
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-powershell-batch-budget.ps1 +54 -15
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-python-batch-budget.ps1 +54 -15
- package/resources/codex-and-agents-customizations/.codex/hooks/file-size-policy.ps1 +421 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-target-worktree.ps1 +369 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/record-subagent-routing-attestation.ps1 +8 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-codex-subagent-routing.ps1 +22 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-discovery-agent-output.ps1 +178 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +11 -1
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +1 -1
- package/resources/codex-and-agents-customizations/AGENTS.md +4 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +65 -1
- package/resources/config/orchestration-routing.json +61 -1
- package/resources/customizations/.github/agents/commentary-remediation.agent.md +1 -1
- package/resources/customizations/.github/agents/discovery-capability-sentinel.agent.md +34 -0
- package/resources/customizations/.github/agents/legacy-parity-analyst.agent.md +122 -0
- package/resources/customizations/.github/agents/migration-coverage-reviewer.agent.md +128 -0
- package/resources/customizations/.github/agents/python-execution-only-typed.agent.md +1 -1
- package/resources/customizations/.github/agents/python-typed-engineer.agent.md +1 -1
- package/resources/customizations/.github/agents/requirements-reconciler.agent.md +138 -0
- package/resources/customizations/.github/agents/runtime-characterization-analyst.agent.md +124 -0
- package/resources/customizations/.github/hooks/discovery-agent-start.ps1 +139 -0
- package/resources/customizations/.github/hooks/discovery-agent-stop.ps1 +123 -0
- package/resources/customizations/.github/hooks/discovery-capability-probe.ps1 +98 -0
- package/resources/customizations/.github/hooks/discovery-planning.ps1 +91 -0
- package/resources/customizations/.github/hooks/discovery-pre-tool.ps1 +123 -0
- package/resources/customizations/.github/hooks/discovery.json +35 -0
- package/resources/customizations/.github/instructions/general-code-change.instructions.md +5 -2
- package/resources/customizations/.github/instructions/mermaid.instructions.md +85 -0
- package/resources/customizations/.github/instructions/powershell-code-change.instructions.md +1 -1
- package/resources/customizations/.github/prompts/execute-hard-lock.prompt.md +105 -0
- package/resources/customizations/.github/prompts/remediate-comments.prompt.md +1 -1
- package/resources/customizations/.github/skills/adjudicate-unspecified-behavior/SKILL.md +135 -0
- package/resources/customizations/.github/skills/characterize-runtime-behavior/SKILL.md +134 -0
- package/resources/customizations/.github/skills/discovery-behavior-reconciliation/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-coverage-ledger/SKILL.md +50 -0
- package/resources/customizations/.github/skills/discovery-parity-matrix/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-repo-inventory/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-runtime-characterization/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-validate-artifacts/SKILL.md +46 -0
- package/resources/customizations/.github/skills/discovery-workflow/SKILL.md +62 -0
- package/resources/customizations/.github/skills/execution-environment/SKILL.md +25 -10
- package/resources/customizations/.github/skills/extract-feature-contract/SKILL.md +133 -0
- package/resources/customizations/.github/skills/feature-review-workflow/SKILL.md +8 -0
- package/resources/customizations/.github/skills/generate-acceptance-scenarios/SKILL.md +132 -0
- package/resources/customizations/.github/skills/initialize-discovery-workspace/SKILL.md +131 -0
- package/resources/customizations/.github/skills/inventory-legacy-system/SKILL.md +131 -0
- package/resources/customizations/.github/skills/reconcile-parity-matrix/SKILL.md +131 -0
- package/resources/customizations/.github/skills/review-discovery-coverage/SKILL.md +129 -0
- package/resources/customizations/.github/skills/validate-discovery-artifacts/SKILL.md +127 -0
- package/resources/lib/codex-routing/CodexDeployment.psm1 +5 -1
- package/resources/lib/codex-routing/CodexDiscoveryRouting.psm1 +161 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +25 -2
- package/resources/lib/discovery-validation/DiscoveryAuthority.psd1 +90 -0
- package/resources/lib/discovery-validation/DiscoveryAuthority.psm1 +493 -0
- package/resources/lib/discovery-validation/DiscoveryAuthorization.psm1 +479 -0
- package/resources/lib/discovery-validation/DiscoveryCapability.psm1 +468 -0
- package/resources/lib/discovery-validation/DiscoveryEncoding.psm1 +156 -0
- package/resources/lib/discovery-validation/DiscoveryManifest.psm1 +384 -0
- package/resources/lib/discovery-validation/DiscoveryObservation.psm1 +217 -0
- package/resources/lib/discovery-validation/DiscoveryPayload.psm1 +276 -0
- package/resources/lib/discovery-validation/DiscoveryPlanning.psm1 +336 -0
- package/resources/lib/discovery-validation/DiscoveryRootGrant.psm1 +164 -0
- package/resources/lib/discovery-validation/DiscoveryStart.psm1 +440 -0
- package/resources/lib/discovery-validation/DiscoveryStartRecord.psm1 +296 -0
- package/resources/lib/discovery-validation/DiscoveryStop.psm1 +336 -0
- package/resources/lib/discovery-validation/DiscoveryToolRouting.psm1 +228 -0
- package/resources/powershell/PoshQC/PoshQC.Parallel.psm1 +452 -0
- package/resources/powershell/PoshQC/PoshQC.Shard.psm1 +106 -8
- package/resources/powershell/PoshQC/PoshQC.Testing.psm1 +12 -0
- package/resources/powershell/PoshQC/PoshQC.psd1 +6 -1
- package/resources/powershell/PoshQC/PoshQC.psm1 +7 -1
- package/resources/powershell/PoshQC/README.md +20 -1
- package/resources/templates/policy_audit/code-review.yyyy-MM-ddTHH-mm.md +4 -4
- package/resources/templates/policy_audit/policy-audit.yyyy-MM-ddTHH-mm.md +4 -4
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Adopted feature-folder probe for the orchestrator SubagentStop hook validate-orchestrator-output.ps1 (issue #992).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Dot-sourced by validate-orchestrator-output.ps1 through a guarded load whose catch records the
|
|
7
|
+
failure with Add-HookDependencyFailure, so a missing or broken copy of this file blocks the hook
|
|
8
|
+
instead of failing open. It defines Invoke-FeatureFolderGitExe, the single git seam, and
|
|
9
|
+
Get-OrchestratorOutputFeatureFolderDecision.
|
|
10
|
+
|
|
11
|
+
When an orchestrator-state checkpoint waives new_active_feature_folder through
|
|
12
|
+
issue_adoption.waived_tools, the validators accept a declared feature_folder_record but cannot
|
|
13
|
+
read the repository. This probe confirms that HEAD of the run worktree holds a tree at the
|
|
14
|
+
checkpoint feature-folder, so a staged-only, untracked, or absent folder blocks. It proves only
|
|
15
|
+
that a tree exists at that path at stop time; it does not prove which tool created the folder
|
|
16
|
+
or that its contents are correct.
|
|
17
|
+
|
|
18
|
+
The file imports nothing. Test-OrchestratorStateFeatureFolderPath comes from
|
|
19
|
+
OrchestratorStateFeatureFolderRecord.psm1, which the main hook imports through its own guarded
|
|
20
|
+
load. The file invokes no Python.
|
|
21
|
+
#>
|
|
22
|
+
|
|
23
|
+
$script:FeatureFolderProbeTool = 'new_active_feature_folder'
|
|
24
|
+
|
|
25
|
+
function Invoke-FeatureFolderGitExe {
|
|
26
|
+
<#
|
|
27
|
+
.SYNOPSIS
|
|
28
|
+
Run git with the given arguments. The single executable seam of the feature-folder probe.
|
|
29
|
+
.DESCRIPTION
|
|
30
|
+
Never throws: a git that cannot start, or an invoker that throws, returns ExitCode -1 so the
|
|
31
|
+
caller blocks instead of failing open. The default invoker merges standard error into Output;
|
|
32
|
+
success requires the exact output tree, so error text is never read as success.
|
|
33
|
+
.PARAMETER GitArgs
|
|
34
|
+
The git arguments.
|
|
35
|
+
.PARAMETER GitInvoker
|
|
36
|
+
The scriptblock that runs git; a unit test passes a test-local stand-in that sets
|
|
37
|
+
$global:LASTEXITCODE, so this body runs without starting a process.
|
|
38
|
+
.OUTPUTS
|
|
39
|
+
System.Collections.Hashtable with keys ExitCode (int) and Output (string, trimmed).
|
|
40
|
+
#>
|
|
41
|
+
[CmdletBinding()]
|
|
42
|
+
[OutputType([hashtable])]
|
|
43
|
+
param(
|
|
44
|
+
[Parameter(Mandatory = $true)]
|
|
45
|
+
[string[]] $GitArgs,
|
|
46
|
+
|
|
47
|
+
[Parameter(Mandatory = $false)]
|
|
48
|
+
[ValidateNotNull()]
|
|
49
|
+
[scriptblock] $GitInvoker = { git @args 2>&1 }
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
$lines = @(& $GitInvoker @GitArgs)
|
|
54
|
+
$code = $LASTEXITCODE
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return @{ ExitCode = -1; Output = '' }
|
|
58
|
+
}
|
|
59
|
+
if ($null -eq $code) { $code = -1 }
|
|
60
|
+
return @{ ExitCode = [int]$code; Output = ((@($lines | ForEach-Object { [string]$_ }) -join "`n").Trim()) }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Test-FeatureFolderProbeWaived {
|
|
64
|
+
<#
|
|
65
|
+
.SYNOPSIS
|
|
66
|
+
Report whether the checkpoint's issue_adoption.waived_tools lists new_active_feature_folder.
|
|
67
|
+
.PARAMETER Checkpoint
|
|
68
|
+
The parsed checkpoint object. May be $null.
|
|
69
|
+
.OUTPUTS
|
|
70
|
+
System.Boolean
|
|
71
|
+
#>
|
|
72
|
+
[CmdletBinding()]
|
|
73
|
+
[OutputType([bool])]
|
|
74
|
+
param(
|
|
75
|
+
[Parameter(Mandatory = $true)]
|
|
76
|
+
[AllowNull()]
|
|
77
|
+
[object] $Checkpoint
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
if ($Checkpoint -isnot [System.Management.Automation.PSCustomObject]) { return $false }
|
|
81
|
+
$adoptionProperty = $Checkpoint.PSObject.Properties['issue_adoption']
|
|
82
|
+
if ($null -eq $adoptionProperty -or $adoptionProperty.Value -isnot [System.Management.Automation.PSCustomObject]) { return $false }
|
|
83
|
+
$waivedProperty = $adoptionProperty.Value.PSObject.Properties['waived_tools']
|
|
84
|
+
if ($null -eq $waivedProperty -or $waivedProperty.Value -isnot [System.Array]) { return $false }
|
|
85
|
+
return (@($waivedProperty.Value | Where-Object { $_ -is [string] }) -ccontains $script:FeatureFolderProbeTool)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Get-OrchestratorOutputFeatureFolderDecision {
|
|
89
|
+
<#
|
|
90
|
+
.SYNOPSIS
|
|
91
|
+
Confirm that HEAD of the run worktree holds a tree at the adopted feature-folder.
|
|
92
|
+
.DESCRIPTION
|
|
93
|
+
Returns @{ Ok = $true; Message = $null } when the checkpoint does not waive
|
|
94
|
+
new_active_feature_folder. Otherwise the checkpoint feature-folder is re-checked against the
|
|
95
|
+
shared path pattern before any git call (a failing path never reaches git), and
|
|
96
|
+
git -C <WorktreeRoot> cat-file -t HEAD:<feature-folder> must exit 0 and print exactly tree.
|
|
97
|
+
Every other outcome (non-zero exit, -1, empty output, other output) blocks with a repairable
|
|
98
|
+
ROUTING_CONTRACT_BLOCKED message naming the path.
|
|
99
|
+
.PARAMETER Checkpoint
|
|
100
|
+
The parsed checkpoint object. May be $null.
|
|
101
|
+
.PARAMETER WorktreeRoot
|
|
102
|
+
The root of the worktree whose run stopped.
|
|
103
|
+
.PARAMETER GitInvoker
|
|
104
|
+
Optional. Forwarded to Invoke-FeatureFolderGitExe; omitted, the seam's default runs git.
|
|
105
|
+
.OUTPUTS
|
|
106
|
+
System.Collections.Hashtable with keys Ok, Message, and (on a block) Repairable.
|
|
107
|
+
#>
|
|
108
|
+
[CmdletBinding()]
|
|
109
|
+
[OutputType([hashtable])]
|
|
110
|
+
param(
|
|
111
|
+
[Parameter(Mandatory = $true)]
|
|
112
|
+
[AllowNull()]
|
|
113
|
+
[object] $Checkpoint,
|
|
114
|
+
|
|
115
|
+
[Parameter(Mandatory = $true)]
|
|
116
|
+
[string] $WorktreeRoot,
|
|
117
|
+
|
|
118
|
+
[Parameter(Mandatory = $false)]
|
|
119
|
+
[scriptblock] $GitInvoker
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if (-not (Test-FeatureFolderProbeWaived -Checkpoint $Checkpoint)) {
|
|
123
|
+
return @{ Ok = $true; Message = $null }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
$folderProperty = $Checkpoint.PSObject.Properties['feature-folder']
|
|
127
|
+
$folder = if ($null -ne $folderProperty) { $folderProperty.Value } else { $null }
|
|
128
|
+
$blocked = @{ Ok = $false; Repairable = $true; Message = ('ROUTING_CONTRACT_BLOCKED: Checkpoint feature-folder is not committed in HEAD: {0}.' -f [string]$folder) }
|
|
129
|
+
# Defense in depth: the validators already require the record path to match the pattern and to
|
|
130
|
+
# equal feature-folder, but a value that fails the pattern must never become a git argument.
|
|
131
|
+
if (-not (Test-OrchestratorStateFeatureFolderPath -Value $folder)) {
|
|
132
|
+
return $blocked
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
$gitArgs = [string[]]@('-C', $WorktreeRoot, 'cat-file', '-t', ('HEAD:' + [string]$folder))
|
|
136
|
+
$probe = if ($PSBoundParameters.ContainsKey('GitInvoker') -and $null -ne $GitInvoker) {
|
|
137
|
+
Invoke-FeatureFolderGitExe -GitArgs $gitArgs -GitInvoker $GitInvoker
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
Invoke-FeatureFolderGitExe -GitArgs $gitArgs
|
|
141
|
+
}
|
|
142
|
+
if (($probe.ExitCode -eq 0) -and [string]::Equals([string]$probe.Output, 'tree', [System.StringComparison]::Ordinal)) {
|
|
143
|
+
return @{ Ok = $true; Message = $null }
|
|
144
|
+
}
|
|
145
|
+
return $blocked
|
|
146
|
+
}
|
package/resources/claude-customizations/.claude/hooks/validate-orchestrator-output-rejection.ps1
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Parallel mutation rejection helpers for validate-orchestrator-output.ps1 (issue #1012).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Dot-sourced by validate-orchestrator-output.ps1. A parallel skill that rejects a mutation before
|
|
7
|
+
any run checkpoint exists stops without writing state and ends its final output with the line
|
|
8
|
+
PARALLEL_MUTATION_REJECTED: <code>. These helpers parse that signal, scan the live worktrees for
|
|
9
|
+
parallel checkpoint text that would contradict the claim that no state was written, decide
|
|
10
|
+
whether the unresolved parallel stop may be allowed, and compose the hint appended to the block
|
|
11
|
+
message when it may not.
|
|
12
|
+
|
|
13
|
+
The signal grammar, the closed code set, and the hook behavior are defined once in the section
|
|
14
|
+
`## Mutation Rejection Signal (issue #1012)` of .claude/rules/parallel-orchestration.md.
|
|
15
|
+
|
|
16
|
+
Imports no module, invokes no Python, and starts no process. Relies on
|
|
17
|
+
Find-WorktreeRunIdentitySignal, Get-WorktreeItemLiveRoot, Get-WorktreeRunCheckpointText, and
|
|
18
|
+
Get-WorktreeRunCheckpointPath, which the hook imports before it dot-sources this file. Reads no
|
|
19
|
+
file except through Get-WorktreeRunCheckpointText.
|
|
20
|
+
#>
|
|
21
|
+
|
|
22
|
+
# The closed rejection code set, in rule order. It mirrors the table of
|
|
23
|
+
# `## Mutation Rejection Signal (issue #1012)` in .claude/rules/parallel-orchestration.md, which owns
|
|
24
|
+
# the set; a Pester parity row pins this array to that table.
|
|
25
|
+
$script:ParallelMutationRejectionCodes = @('kickoff_artifact_missing', 'run_not_started', 'items_in_flight', 'unknown_item')
|
|
26
|
+
$script:ParallelMutationRejectionPrefix = 'PARALLEL_MUTATION_REJECTED:'
|
|
27
|
+
$script:ParallelMutationRejectionPattern = '^PARALLEL_MUTATION_REJECTED: (?<code>[a-z_]+)$'
|
|
28
|
+
$script:ParallelMutationRejectionHeading = '## Mutation Rejection Signal (issue #1012)'
|
|
29
|
+
$script:ParallelMutationRejectionArtifactType = 'parallel-orchestrator-state'
|
|
30
|
+
|
|
31
|
+
function Get-ParallelMutationRejectionCode {
|
|
32
|
+
<#
|
|
33
|
+
.SYNOPSIS
|
|
34
|
+
Return the code of a valid final-line rejection signal, or $null.
|
|
35
|
+
.DESCRIPTION
|
|
36
|
+
Pure. Only the last non-blank line of the output is considered, after trailing whitespace
|
|
37
|
+
(including a carriage return) is trimmed. The line must match
|
|
38
|
+
^PARALLEL_MUTATION_REJECTED: (?<code>[a-z_]+)$ case-sensitively, and the code must be an
|
|
39
|
+
ordinal member of the closed code set. Any other form, including formatting or trailing
|
|
40
|
+
text, returns $null.
|
|
41
|
+
.PARAMETER AgentOutput
|
|
42
|
+
The agent's final output text.
|
|
43
|
+
#>
|
|
44
|
+
[CmdletBinding()]
|
|
45
|
+
[OutputType([string])]
|
|
46
|
+
param(
|
|
47
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $AgentOutput
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
$trimmed = ([string]$AgentOutput).TrimEnd()
|
|
51
|
+
if ($trimmed.Length -eq 0) { return $null }
|
|
52
|
+
$lastLine = $trimmed.Substring($trimmed.LastIndexOf("`n") + 1)
|
|
53
|
+
$match = [regex]::Match($lastLine, $script:ParallelMutationRejectionPattern)
|
|
54
|
+
if (-not $match.Success) { return $null }
|
|
55
|
+
$code = $match.Groups['code'].Value
|
|
56
|
+
foreach ($member in $script:ParallelMutationRejectionCodes) {
|
|
57
|
+
if ([string]::Equals($member, $code, [System.StringComparison]::Ordinal)) { return $member }
|
|
58
|
+
}
|
|
59
|
+
return $null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function Test-ParallelRunStateAbsent {
|
|
63
|
+
<#
|
|
64
|
+
.SYNOPSIS
|
|
65
|
+
Return $true when no live worktree holds parallel checkpoint text that contradicts a no-state stop.
|
|
66
|
+
.DESCRIPTION
|
|
67
|
+
For every live root, the text at the canonical parallel checkpoint path must be null or blank,
|
|
68
|
+
or parse as a JSON object with route_id 'parallel' and a non-blank string parallel_slug that
|
|
69
|
+
differs (ordinally) from the parallel_slug: value the output names, when it names one. Any
|
|
70
|
+
other non-blank text (unparseable, a non-object root, off-route, or without a slug) and a
|
|
71
|
+
checkpoint recording the named slug return $false.
|
|
72
|
+
.PARAMETER AgentOutput
|
|
73
|
+
The agent's final output text, scanned for a parallel_slug: literal.
|
|
74
|
+
.PARAMETER SessionRoot
|
|
75
|
+
The calling process's path, used only to find the repository.
|
|
76
|
+
#>
|
|
77
|
+
[CmdletBinding()]
|
|
78
|
+
[OutputType([bool])]
|
|
79
|
+
param(
|
|
80
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $AgentOutput,
|
|
81
|
+
[Parameter(Mandatory = $true)] [string] $SessionRoot
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
$namedSlug = (Find-WorktreeRunIdentitySignal -Text ([string]$AgentOutput)).ParallelSlug
|
|
85
|
+
# The seam returns its array as one object, so it is assigned before it is iterated.
|
|
86
|
+
$liveRoots = Get-WorktreeItemLiveRoot -SessionRoot $SessionRoot
|
|
87
|
+
foreach ($root in $liveRoots) {
|
|
88
|
+
if ([string]::IsNullOrWhiteSpace($root)) { continue }
|
|
89
|
+
$text = Get-WorktreeRunCheckpointText -Path (Get-WorktreeRunCheckpointPath -Kind 'parallel' -WorktreeRoot $root)
|
|
90
|
+
if ([string]::IsNullOrWhiteSpace($text)) { continue }
|
|
91
|
+
try { $parsed = ConvertFrom-Json -InputObject $text -NoEnumerate -ErrorAction Stop } catch { return $false }
|
|
92
|
+
if ($parsed -isnot [System.Management.Automation.PSCustomObject]) { return $false }
|
|
93
|
+
$names = @($parsed.PSObject.Properties.Name)
|
|
94
|
+
if (-not ($names -ccontains 'route_id') -or ([string]$parsed.route_id -cne 'parallel')) { return $false }
|
|
95
|
+
if (-not ($names -ccontains 'parallel_slug')) { return $false }
|
|
96
|
+
$slug = $parsed.parallel_slug
|
|
97
|
+
if ($slug -isnot [string] -or [string]::IsNullOrWhiteSpace($slug)) { return $false }
|
|
98
|
+
if (-not [string]::IsNullOrWhiteSpace($namedSlug) -and ($slug -ceq $namedSlug)) { return $false }
|
|
99
|
+
}
|
|
100
|
+
return $true
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function Get-OrchestratorOutputRejectionDecision {
|
|
104
|
+
<#
|
|
105
|
+
.SYNOPSIS
|
|
106
|
+
Return the allow decision for an unresolved parallel stop that carries a valid rejection signal, or $null.
|
|
107
|
+
.DESCRIPTION
|
|
108
|
+
Returns @{ Ok = $true; Message = $null } only when every gate holds: the artifact type is
|
|
109
|
+
parallel-orchestrator-state, the resolution status is NoTarget, the final line is a valid
|
|
110
|
+
rejection signal, and the state-absence scan passes. Otherwise returns $null, and the caller
|
|
111
|
+
applies its existing block.
|
|
112
|
+
.PARAMETER ArtifactType
|
|
113
|
+
The artifact type the hook validates.
|
|
114
|
+
.PARAMETER Resolution
|
|
115
|
+
The unresolved result of Resolve-OrchestratorOutputCheckpointPath.
|
|
116
|
+
.PARAMETER AgentOutput
|
|
117
|
+
The agent's final output text.
|
|
118
|
+
.PARAMETER SessionRoot
|
|
119
|
+
The calling process's path, used only to find the repository.
|
|
120
|
+
#>
|
|
121
|
+
[CmdletBinding()]
|
|
122
|
+
[OutputType([hashtable])]
|
|
123
|
+
param(
|
|
124
|
+
[Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $ArtifactType,
|
|
125
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [object] $Resolution,
|
|
126
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $AgentOutput,
|
|
127
|
+
[Parameter(Mandatory = $true)] [string] $SessionRoot
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if ($ArtifactType -cne $script:ParallelMutationRejectionArtifactType) { return $null }
|
|
131
|
+
if ($null -eq $Resolution -or [string]$Resolution.Status -cne 'NoTarget') { return $null }
|
|
132
|
+
if ($null -eq (Get-ParallelMutationRejectionCode -AgentOutput $AgentOutput)) { return $null }
|
|
133
|
+
if (-not (Test-ParallelRunStateAbsent -AgentOutput $AgentOutput -SessionRoot $SessionRoot)) { return $null }
|
|
134
|
+
return @{ Ok = $true; Message = $null }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function Get-ParallelMutationRejectionHint {
|
|
138
|
+
<#
|
|
139
|
+
.SYNOPSIS
|
|
140
|
+
Return the text appended to an unresolved block message.
|
|
141
|
+
.DESCRIPTION
|
|
142
|
+
Pure. Returns an empty string unless the artifact type is parallel-orchestrator-state and the
|
|
143
|
+
status is NoTarget. Otherwise returns one newline-prefixed hint line naming the signal form
|
|
144
|
+
and the rule heading and, when the last non-blank line begins with the signal prefix but
|
|
145
|
+
carries no valid code, a second line listing the allowed codes.
|
|
146
|
+
.PARAMETER ArtifactType
|
|
147
|
+
The artifact type the hook validates.
|
|
148
|
+
.PARAMETER Status
|
|
149
|
+
The resolution status of the block.
|
|
150
|
+
.PARAMETER AgentOutput
|
|
151
|
+
The agent's final output text.
|
|
152
|
+
#>
|
|
153
|
+
[CmdletBinding()]
|
|
154
|
+
[OutputType([string])]
|
|
155
|
+
param(
|
|
156
|
+
[Parameter(Mandatory = $true)] [AllowEmptyString()] [string] $ArtifactType,
|
|
157
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $Status,
|
|
158
|
+
[Parameter(Mandatory = $true)] [AllowNull()] [AllowEmptyString()] [string] $AgentOutput
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
if ($ArtifactType -cne $script:ParallelMutationRejectionArtifactType -or $Status -cne 'NoTarget') { return '' }
|
|
162
|
+
$hint = ("`norchestrator hook: a parallel skill that stopped without writing state ends its final output with the line 'PARALLEL_MUTATION_REJECTED: <code>' as the last line, with no formatting; see '{0}' in .claude/rules/parallel-orchestration.md." -f
|
|
163
|
+
$script:ParallelMutationRejectionHeading)
|
|
164
|
+
$trimmed = ([string]$AgentOutput).TrimEnd()
|
|
165
|
+
$lastLine = $trimmed.Substring($trimmed.LastIndexOf("`n") + 1)
|
|
166
|
+
if ($lastLine.StartsWith($script:ParallelMutationRejectionPrefix, [System.StringComparison]::Ordinal) -and
|
|
167
|
+
$null -eq (Get-ParallelMutationRejectionCode -AgentOutput $AgentOutput)) {
|
|
168
|
+
$hint += ("`norchestrator hook: the final line carries the {0} prefix with an invalid code; allowed codes: {1}." -f
|
|
169
|
+
$script:ParallelMutationRejectionPrefix, ($script:ParallelMutationRejectionCodes -join ', '))
|
|
170
|
+
}
|
|
171
|
+
return $hint
|
|
172
|
+
}
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
ambiguous, or mismatched target blocks with ORCHESTRATOR_CHECKPOINT_UNRESOLVED:
|
|
26
26
|
before any checkpoint is read. For epic-orchestrator-state, the Layer 2 wave-barrier
|
|
27
27
|
ordering check (OrchestratorStateEpicWaveBarrier.psm1, issue #840) runs after the
|
|
28
|
-
routing dispatch passes.
|
|
28
|
+
routing dispatch passes. For orchestrator-state, the adopted feature-folder probe
|
|
29
|
+
(validate-orchestrator-output-feature-folder.ps1, issue #992) runs after the routing
|
|
30
|
+
dispatch passes, and only when the checkpoint waives new_active_feature_folder.
|
|
29
31
|
|
|
30
32
|
.NOTES
|
|
31
33
|
Reads the SubagentStop envelope from stdin through Read-SubagentStopEnvelope
|
|
@@ -55,12 +57,17 @@ Set-StrictMode -Version Latest
|
|
|
55
57
|
|
|
56
58
|
try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-resolution.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-resolution.ps1' -ErrorRecord $_ }
|
|
57
59
|
try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-routing.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-routing.ps1' -ErrorRecord $_ }
|
|
60
|
+
try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-rejection.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-rejection.ps1' -ErrorRecord $_ }
|
|
58
61
|
try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeItemResolution.psm1' -ErrorRecord $_ }
|
|
59
62
|
try { Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeRunResolution.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'WorktreeRunResolution.psm1' -ErrorRecord $_ }
|
|
60
63
|
try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateEpicWaveBarrier.psm1') -Force -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateEpicWaveBarrier.psm1' -ErrorRecord $_ }
|
|
61
64
|
try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateCompletion.psm1') -Force -Global -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateCompletion.psm1' -ErrorRecord $_ }
|
|
62
65
|
try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateUnconditional.psm1') -Force -Global -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateUnconditional.psm1' -ErrorRecord $_ }
|
|
63
66
|
try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorState.psm1') -Force -Global -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorState.psm1' -ErrorRecord $_ }
|
|
67
|
+
# Imported after the completion modules without -Force, so the instance they already loaded through
|
|
68
|
+
# the adoption module stays the one in use (issue #992).
|
|
69
|
+
try { Import-Module (Join-Path $PSScriptRoot '../lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1') -ErrorAction Stop } catch { Add-HookDependencyFailure -Name 'OrchestratorStateFeatureFolderRecord.psm1' -ErrorRecord $_ }
|
|
70
|
+
try { . (Join-Path $PSScriptRoot 'validate-orchestrator-output-feature-folder.ps1') } catch { Add-HookDependencyFailure -Name 'validate-orchestrator-output-feature-folder.ps1' -ErrorRecord $_ }
|
|
64
71
|
try { . (Join-Path $PSScriptRoot 'subagentstop-envelope.ps1') } catch { Add-HookDependencyFailure -Name 'subagentstop-envelope.ps1' -ErrorRecord $_ }
|
|
65
72
|
$ErrorActionPreference = 'Stop'
|
|
66
73
|
$script:SubagentStopReportAndHaltLine = 'This condition is not repairable by the subagent. Report it to the operator and halt.'
|
|
@@ -194,7 +201,10 @@ function Invoke-OrchestratorOutputValidation {
|
|
|
194
201
|
[string] $SessionRoot = (Get-Location).Path,
|
|
195
202
|
|
|
196
203
|
[Parameter(Mandatory = $false)]
|
|
197
|
-
[scriptblock] $RoutingInvoker
|
|
204
|
+
[scriptblock] $RoutingInvoker,
|
|
205
|
+
|
|
206
|
+
[Parameter(Mandatory = $false)]
|
|
207
|
+
[scriptblock] $FeatureFolderGitInvoker
|
|
198
208
|
)
|
|
199
209
|
$dependencyDecision = Get-HookDependencyFailureDecision -HookEvent SubagentStop -ReasonPrefix 'ORCHESTRATOR_CHECKPOINT_UNRESOLVED:'
|
|
200
210
|
if ($null -ne $dependencyDecision) { return $dependencyDecision }
|
|
@@ -205,6 +215,7 @@ function Invoke-OrchestratorOutputValidation {
|
|
|
205
215
|
$agentOutput = $envelope.AssistantText
|
|
206
216
|
$blockArguments = @{ AgentOutput = ([string]$agentOutput); CheckpointPath = $CheckpointPath; ArtifactType = $ArtifactType; SessionRoot = $SessionRoot }
|
|
207
217
|
if ($PSBoundParameters.ContainsKey('RoutingInvoker')) { $blockArguments['RoutingInvoker'] = $RoutingInvoker }
|
|
218
|
+
if ($PSBoundParameters.ContainsKey('FeatureFolderGitInvoker')) { $blockArguments['FeatureFolderGitInvoker'] = $FeatureFolderGitInvoker }
|
|
208
219
|
$decision = Get-OrchestratorOutputBlockDecision @blockArguments
|
|
209
220
|
}
|
|
210
221
|
if ($decision.Ok) { return @{ Ok = $true; Message = $null } }
|
|
@@ -225,6 +236,10 @@ function Get-OrchestratorOutputBlockDecision {
|
|
|
225
236
|
the condition (message content, its checkpoint, or its runbook) and $false when it cannot
|
|
226
237
|
(a registration argument, an internal evaluation failure, or a state it may not clear).
|
|
227
238
|
The caller applies the re-entry policy to that flag.
|
|
239
|
+
An unresolved parallel-orchestrator-state checkpoint (status NoTarget) is allowed when the
|
|
240
|
+
output ends with a valid PARALLEL_MUTATION_REJECTED: <code> line and no live worktree holds
|
|
241
|
+
contradicting parallel checkpoint text (Get-OrchestratorOutputRejectionDecision, issue #1012);
|
|
242
|
+
otherwise the unresolved block carries the hint from Get-ParallelMutationRejectionHint.
|
|
228
243
|
#>
|
|
229
244
|
[CmdletBinding()]
|
|
230
245
|
[OutputType([hashtable])]
|
|
@@ -235,7 +250,10 @@ function Get-OrchestratorOutputBlockDecision {
|
|
|
235
250
|
[string] $SessionRoot = (Get-Location).Path,
|
|
236
251
|
|
|
237
252
|
[Parameter(Mandatory = $false)]
|
|
238
|
-
[scriptblock] $RoutingInvoker
|
|
253
|
+
[scriptblock] $RoutingInvoker,
|
|
254
|
+
|
|
255
|
+
[Parameter(Mandatory = $false)]
|
|
256
|
+
[scriptblock] $FeatureFolderGitInvoker
|
|
239
257
|
)
|
|
240
258
|
|
|
241
259
|
if ([string]::IsNullOrWhiteSpace($agentOutput)) {
|
|
@@ -251,9 +269,12 @@ function Get-OrchestratorOutputBlockDecision {
|
|
|
251
269
|
return @{ Ok = $false; Repairable = $false; Message = ('ORCHESTRATOR_CHECKPOINT_UNRESOLVED: {0}: NoTarget (RESOLVER_FAILED): {1}' -f $ArtifactType, $_.Exception.Message) }
|
|
252
270
|
}
|
|
253
271
|
if (-not $resolution.Resolved) {
|
|
272
|
+
# Issue #1012: a parallel stop that wrote no state and ends with a valid rejection signal is allowed.
|
|
273
|
+
$rejectionDecision = Get-OrchestratorOutputRejectionDecision -ArtifactType $ArtifactType -Resolution $resolution -AgentOutput ([string]$agentOutput) -SessionRoot $SessionRoot
|
|
274
|
+
if ($null -ne $rejectionDecision) { return $rejectionDecision }
|
|
254
275
|
$resolutionRepairable = ($resolution.Status -in @('NoTarget', 'Ambiguous')) -and ($ArtifactType -in @('epic-orchestrator-state', 'parallel-orchestrator-state'))
|
|
255
276
|
return @{ Ok = $false; Repairable = $resolutionRepairable; Message = ('ORCHESTRATOR_CHECKPOINT_UNRESOLVED: {0}: {1} ({2}): {3}' -f
|
|
256
|
-
$ArtifactType, $resolution.Status, $resolution.ReasonCode, $resolution.Detail) }
|
|
277
|
+
$ArtifactType, $resolution.Status, $resolution.ReasonCode, $resolution.Detail) + (Get-ParallelMutationRejectionHint -ArtifactType $ArtifactType -Status ([string]$resolution.Status) -AgentOutput ([string]$agentOutput)) }
|
|
257
278
|
}
|
|
258
279
|
|
|
259
280
|
$file = Get-CheckpointFileContent -Path $resolution.CheckpointPath
|
|
@@ -322,6 +343,15 @@ function Get-OrchestratorOutputBlockDecision {
|
|
|
322
343
|
return @{ Ok = $false; Repairable = $true; Message = "ROUTING_CONTRACT_BLOCKED: $($routingResult.ErrorText)" }
|
|
323
344
|
}
|
|
324
345
|
|
|
346
|
+
# Issue #992: a waived new_active_feature_folder must name a folder committed in HEAD of the run
|
|
347
|
+
# worktree. The validators only see the declared record, so the hook probes git once.
|
|
348
|
+
if ($ArtifactType -eq 'orchestrator-state') {
|
|
349
|
+
$folderArgs = @{ Checkpoint = $checkpoint; WorktreeRoot = [string]$resolution.WorktreeRoot }
|
|
350
|
+
if ($PSBoundParameters.ContainsKey('FeatureFolderGitInvoker') -and $null -ne $FeatureFolderGitInvoker) { $folderArgs['GitInvoker'] = $FeatureFolderGitInvoker }
|
|
351
|
+
$folderDecision = Get-OrchestratorOutputFeatureFolderDecision @folderArgs
|
|
352
|
+
if (-not $folderDecision.Ok) { return @{ Ok = $false; Repairable = $folderDecision.Repairable; Message = $folderDecision.Message } }
|
|
353
|
+
}
|
|
354
|
+
|
|
325
355
|
# Layer 2 (issue #840): the epic checkpoint only, after the routing dispatch passes,
|
|
326
356
|
# on the checkpoint text already read.
|
|
327
357
|
if ($ArtifactType -eq 'epic-orchestrator-state') {
|
|
@@ -73,6 +73,7 @@ Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusValidation.p
|
|
|
73
73
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusConflict.psm1') -Force -ErrorAction Stop
|
|
74
74
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusScheduling.psm1') -Force -ErrorAction Stop
|
|
75
75
|
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusWriteIntent.psm1') -Force -ErrorAction Stop
|
|
76
|
+
Import-Module (Join-Path -Path $PSScriptRoot -ChildPath 'BlastRadiusCombination.psm1') -Force -ErrorAction Stop
|
|
76
77
|
|
|
77
78
|
# Feature-folder handling. Every radius contains its own feature folder, and a
|
|
78
79
|
# caller may pass either a bare folder name or an already-qualified path.
|
|
@@ -246,7 +247,8 @@ function Get-NormalizedDeclaredRadius {
|
|
|
246
247
|
|
|
247
248
|
.OUTPUTS
|
|
248
249
|
System.Collections.Hashtable. A new radius carrying the surviving paths,
|
|
249
|
-
contracts re-filtered by the ASCII-letter rule
|
|
250
|
+
contracts re-filtered by the ASCII-letter rule and the generic sheet-tab
|
|
251
|
+
rule of Test-GenericContractToken (issue #1006), and modules and shared
|
|
250
252
|
surfaces re-resolved from the surviving paths. Throws when the radius
|
|
251
253
|
source is observed: an observed radius records a diff listing rather than
|
|
252
254
|
a plan-text harvest, so the plan-text acceptance rules do not apply to it
|
|
@@ -297,9 +299,12 @@ function Get-NormalizedDeclaredRadius {
|
|
|
297
299
|
-MandateRead ([string[]]@(Get-ConfigMandateRead -Config $Config)))
|
|
298
300
|
$concrete = [string[]]@(Get-ConcreteEntry -Entry $paths)
|
|
299
301
|
|
|
302
|
+
# Keep a recorded contract only when it carries an ASCII letter and is not a
|
|
303
|
+
# default spreadsheet sheet-tab name (issue #1006).
|
|
300
304
|
$contract = [System.Collections.Generic.List[string]]::new()
|
|
301
305
|
foreach ($identifier in @($normalized['contracts'])) {
|
|
302
|
-
if ($script:ContractLetterPattern.IsMatch([string]$identifier)
|
|
306
|
+
if ($script:ContractLetterPattern.IsMatch([string]$identifier) -and
|
|
307
|
+
-not (Test-GenericContractToken -Token ([string]$identifier))) {
|
|
303
308
|
$contract.Add([string]$identifier)
|
|
304
309
|
}
|
|
305
310
|
}
|
|
@@ -472,4 +477,5 @@ Export-ModuleMember -Function `
|
|
|
472
477
|
Get-WriteIntentPlanPath, `
|
|
473
478
|
Get-WriteIntentSpecContract, `
|
|
474
479
|
Select-WriteIntentPathEntry, `
|
|
475
|
-
Get-PlanPathForConfig
|
|
480
|
+
Get-PlanPathForConfig, `
|
|
481
|
+
Get-BlastRadiusCombination
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Aggregate integration-cost cap over tolerated overlaps (issue #1013, D-2).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Destination-runtime PowerShell mirror of
|
|
7
|
+
scripts/dev_tools/_blast_radius_aggregate.py. The pairwise edge rule judges
|
|
8
|
+
each pair alone, so one item can accumulate many tolerated overlaps whose
|
|
9
|
+
summed cost exceeds what running it serially would cost. Get-AggregateCappedPair
|
|
10
|
+
caps each item's summed tolerated cost and returns the pairs to promote to
|
|
11
|
+
scheduling edges; Get-BlastRadiusConflictEdge applies the result.
|
|
12
|
+
|
|
13
|
+
Cap rule: total(i) is the summed cost of the remaining tolerated overlaps
|
|
14
|
+
that include item i. Item i is over its cap when
|
|
15
|
+
total(i) * 100 > band_duration(i) * tolerance_percent, evaluated in
|
|
16
|
+
integers; a missing band reads as default_band. While an item is over its
|
|
17
|
+
cap, the smallest-key such item promotes its overlap with the minimum key
|
|
18
|
+
(benefit ascending, cost descending, a ascending, b ascending).
|
|
19
|
+
|
|
20
|
+
Parity notes for maintainers:
|
|
21
|
+
- The Python module is the reference implementation; the committed
|
|
22
|
+
aggregate fixtures bind the two runtimes together.
|
|
23
|
+
- Every function is pure: no filesystem, subprocess, network, or clock
|
|
24
|
+
access, and no input is mutated. Integers only.
|
|
25
|
+
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
26
|
+
#>
|
|
27
|
+
|
|
28
|
+
Set-StrictMode -Version Latest
|
|
29
|
+
$ErrorActionPreference = 'Stop'
|
|
30
|
+
|
|
31
|
+
# tolerance_percent is a percentage of the band duration.
|
|
32
|
+
$script:Percent = 100
|
|
33
|
+
|
|
34
|
+
# Resolve an item's band duration, reading a missing band as default_band.
|
|
35
|
+
function Get-AggregateBandDuration {
|
|
36
|
+
[CmdletBinding()]
|
|
37
|
+
[OutputType([long])]
|
|
38
|
+
param(
|
|
39
|
+
[AllowNull()]
|
|
40
|
+
[object] $Band,
|
|
41
|
+
[Parameter(Mandatory = $true)]
|
|
42
|
+
[hashtable] $Tolerance
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
$resolved = if ($null -eq $Band) { $Tolerance['default_band'] } else { $Band }
|
|
46
|
+
$duration = $Tolerance['band_durations']
|
|
47
|
+
if ($resolved -isnot [string] -or -not $duration.ContainsKey($resolved)) {
|
|
48
|
+
throw "band '$resolved' must be one of $(@($duration.Keys | Sort-Object) -join ', ')."
|
|
49
|
+
}
|
|
50
|
+
return [long]$duration[$resolved]
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Return the smallest item key whose tolerated total exceeds its cap, or $null.
|
|
54
|
+
function Get-FirstOverCapItem {
|
|
55
|
+
[CmdletBinding()]
|
|
56
|
+
[OutputType([long])]
|
|
57
|
+
param(
|
|
58
|
+
[Parameter(Mandatory = $true)]
|
|
59
|
+
[AllowEmptyCollection()]
|
|
60
|
+
[object[]] $Remaining,
|
|
61
|
+
[Parameter(Mandatory = $true)]
|
|
62
|
+
[hashtable] $BandByKey,
|
|
63
|
+
[Parameter(Mandatory = $true)]
|
|
64
|
+
[hashtable] $Tolerance
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Each overlap's cost counts toward both of its items.
|
|
68
|
+
$total = @{}
|
|
69
|
+
foreach ($overlap in $Remaining) {
|
|
70
|
+
foreach ($key in @($overlap['a'], $overlap['b'])) {
|
|
71
|
+
$current = if ($total.ContainsKey($key)) { $total[$key] } else { [long]0 }
|
|
72
|
+
$total[$key] = $current + $overlap['cost']
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
$percent = [long]$Tolerance['tolerance_percent']
|
|
76
|
+
foreach ($key in @($total.Keys | Sort-Object)) {
|
|
77
|
+
$band = if ($BandByKey.ContainsKey($key)) { $BandByKey[$key] } else { $null }
|
|
78
|
+
$cap = (Get-AggregateBandDuration -Band $band -Tolerance $Tolerance) * $percent
|
|
79
|
+
if ($total[$key] * $script:Percent -gt $cap) {
|
|
80
|
+
return $key
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return $null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Get-AggregateCappedPair {
|
|
87
|
+
<#
|
|
88
|
+
.SYNOPSIS
|
|
89
|
+
Return the tolerated pairs to promote so every item is within its cap.
|
|
90
|
+
|
|
91
|
+
.DESCRIPTION
|
|
92
|
+
Port of select_capped_pairs. Repeatedly takes the smallest-key item that
|
|
93
|
+
is over its cap, promotes its cheapest-to-serialize overlap, and
|
|
94
|
+
recomputes the totals, until no item is over its cap.
|
|
95
|
+
|
|
96
|
+
.PARAMETER ToleratedOverlap
|
|
97
|
+
The pairwise tolerated overlaps, in any order: records carrying a, b,
|
|
98
|
+
cost, and benefit.
|
|
99
|
+
|
|
100
|
+
.PARAMETER Band
|
|
101
|
+
Item key to complexity band; a key that is absent or maps to null reads
|
|
102
|
+
as default_band.
|
|
103
|
+
|
|
104
|
+
.PARAMETER Tolerance
|
|
105
|
+
A value returned by Get-ConfigConflictTolerance.
|
|
106
|
+
|
|
107
|
+
.OUTPUTS
|
|
108
|
+
System.Collections.Specialized.OrderedDictionary. The promoted pairs in
|
|
109
|
+
promotion order, each an ordered record with keys a and b, written to
|
|
110
|
+
the pipeline; nothing when no item is over its cap. Wrap the call in
|
|
111
|
+
@() to read the result as an array. Throws when a band is not a
|
|
112
|
+
configured band.
|
|
113
|
+
#>
|
|
114
|
+
[CmdletBinding()]
|
|
115
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
116
|
+
param(
|
|
117
|
+
[Parameter(Mandatory = $true)]
|
|
118
|
+
[AllowEmptyCollection()]
|
|
119
|
+
[object[]] $ToleratedOverlap,
|
|
120
|
+
[Parameter(Mandatory = $true)]
|
|
121
|
+
[hashtable] $Band,
|
|
122
|
+
[Parameter(Mandatory = $true)]
|
|
123
|
+
[hashtable] $Tolerance
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
# Copy every overlap and band key into Int64 records, so integer keys of any
|
|
127
|
+
# width compare equal and no input is mutated.
|
|
128
|
+
$bandByKey = @{}
|
|
129
|
+
foreach ($key in $Band.Keys) { $bandByKey[[long]$key] = $Band[$key] }
|
|
130
|
+
$copied = foreach ($overlap in $ToleratedOverlap) {
|
|
131
|
+
[ordered]@{ a = [long]$overlap['a']; b = [long]$overlap['b']; cost = [long]$overlap['cost']; benefit = [long]$overlap['benefit'] }
|
|
132
|
+
}
|
|
133
|
+
$remaining = [System.Collections.Generic.List[object]]::new()
|
|
134
|
+
foreach ($overlap in @($copied | Sort-Object -Property { $_['a'] }, { $_['b'] })) {
|
|
135
|
+
if ($null -ne $overlap) { $remaining.Add($overlap) }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
$promoted = [System.Collections.Generic.List[object]]::new()
|
|
139
|
+
while ($true) {
|
|
140
|
+
$item = Get-FirstOverCapItem -Remaining @($remaining.ToArray()) -BandByKey $bandByKey -Tolerance $Tolerance
|
|
141
|
+
if ($null -eq $item) {
|
|
142
|
+
# Callers wrap the enumerated records with @() so an empty result is
|
|
143
|
+
# an empty array.
|
|
144
|
+
return $promoted.ToArray()
|
|
145
|
+
}
|
|
146
|
+
# Cheapest to serialize: least benefit, then most cost, then pair keys.
|
|
147
|
+
$cheapest = @($remaining | Where-Object { $_['a'] -eq $item -or $_['b'] -eq $item } |
|
|
148
|
+
Sort-Object -Property { $_['benefit'] }, { - $_['cost'] }, { $_['a'] }, { $_['b'] })[0]
|
|
149
|
+
[void]$remaining.Remove($cheapest)
|
|
150
|
+
$promoted.Add([ordered]@{ a = $cheapest['a']; b = $cheapest['b'] })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
Export-ModuleMember -Function Get-AggregateCappedPair
|