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