@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/claude-customizations/.claude/lib/worktree-resolution/HookTargetWorktree.psm1
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pure target-worktree resolver for PreToolUse hooks (issue #1009).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Resolves the registered git worktree that a hook call acts on, from the first present
|
|
7
|
+
source in a strict order: (1) file-tool target paths, (2) command operand paths or a
|
|
8
|
+
branch the caller has already parsed, (3) the identity carried by an Agent delegation
|
|
9
|
+
prompt, and (4) the session root as the final fallback. Registered worktrees come only
|
|
10
|
+
from `git worktree list --porcelain` text returned by the injectable
|
|
11
|
+
WorktreeListInvoker seam, and a path resolves to its nearest enclosing registered
|
|
12
|
+
worktree (the longest normalized root prefix). Paths are compared after forward-slash
|
|
13
|
+
normalization with OrdinalIgnoreCase on every platform.
|
|
14
|
+
|
|
15
|
+
Known limitation: an 8.3 short-name path, or a path reached through a junction, that
|
|
16
|
+
differs textually from the long path git reports matches no registered worktree and
|
|
17
|
+
falls back to the session root.
|
|
18
|
+
|
|
19
|
+
The TARGET_WORKTREE_AMBIGUOUS and TARGET_WORKTREE_NOT_DERIVABLE literals are restated
|
|
20
|
+
inside Resolve-HookTargetWorktree rather than read from WorktreeResolution.psm1,
|
|
21
|
+
because this module imports nothing. A test pins both literals equal to the
|
|
22
|
+
WorktreeResolution.psm1 accessors, following the precedent of
|
|
23
|
+
validate-orchestrator-output-resolution.ps1.
|
|
24
|
+
|
|
25
|
+
.NOTES
|
|
26
|
+
Compatible with PowerShell 7+. No module dependency, no file read, no environment
|
|
27
|
+
read, and no clock read; the only process is the default WorktreeListInvoker body.
|
|
28
|
+
Functions reference no module-scope state, so the Codex twin
|
|
29
|
+
.codex/hooks/hook-target-worktree.ps1 carries byte-identical function text.
|
|
30
|
+
Mirrored byte-identically under extensions/drm-copilot/resources/claude-customizations/.
|
|
31
|
+
CONVENTION: this module fails fast at module scope and imports its siblings with -ErrorAction Stop.
|
|
32
|
+
#>
|
|
33
|
+
|
|
34
|
+
Set-StrictMode -Version Latest
|
|
35
|
+
$ErrorActionPreference = 'Stop'
|
|
36
|
+
|
|
37
|
+
function ConvertTo-HookTargetWorktreeNormalizedPath {
|
|
38
|
+
<#
|
|
39
|
+
.SYNOPSIS
|
|
40
|
+
Normalize a path to forward slashes with repeated and trailing slashes removed.
|
|
41
|
+
.DESCRIPTION
|
|
42
|
+
Backslashes become forward slashes, runs of slashes collapse to one, and a trailing
|
|
43
|
+
slash is trimmed except from a bare root ('/' or a drive root such as 'C:/'). Blank
|
|
44
|
+
input returns an empty string. Case is preserved; callers compare with
|
|
45
|
+
OrdinalIgnoreCase.
|
|
46
|
+
.PARAMETER Path
|
|
47
|
+
The path to normalize.
|
|
48
|
+
#>
|
|
49
|
+
[CmdletBinding()]
|
|
50
|
+
[OutputType([string])]
|
|
51
|
+
param(
|
|
52
|
+
[AllowNull()] [AllowEmptyString()] [string] $Path
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if ([string]::IsNullOrWhiteSpace($Path)) { return '' }
|
|
56
|
+
$normalized = ($Path.Trim() -replace '\\', '/') -replace '/{2,}', '/'
|
|
57
|
+
if ($normalized.Length -gt 1 -and $normalized.EndsWith('/') -and $normalized -notmatch '^[A-Za-z]:/$') {
|
|
58
|
+
$normalized = $normalized.Substring(0, $normalized.Length - 1)
|
|
59
|
+
}
|
|
60
|
+
return $normalized
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function Get-HookTargetWorktreeRelativePath {
|
|
64
|
+
<#
|
|
65
|
+
.SYNOPSIS
|
|
66
|
+
Return a path relative to a worktree root, or $null when it lies outside the root.
|
|
67
|
+
.DESCRIPTION
|
|
68
|
+
Both values are normalized first. A path equal to the root returns an empty string;
|
|
69
|
+
a path under the root returns its forward-slash remainder; any other path, or a
|
|
70
|
+
blank value, returns $null.
|
|
71
|
+
.PARAMETER Path
|
|
72
|
+
The path to express relative to the root.
|
|
73
|
+
.PARAMETER WorktreeRoot
|
|
74
|
+
The worktree root.
|
|
75
|
+
#>
|
|
76
|
+
[CmdletBinding()]
|
|
77
|
+
[OutputType([string])]
|
|
78
|
+
param(
|
|
79
|
+
[AllowNull()] [AllowEmptyString()] [string] $Path,
|
|
80
|
+
[AllowNull()] [AllowEmptyString()] [string] $WorktreeRoot
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
$normalizedPath = ConvertTo-HookTargetWorktreeNormalizedPath -Path $Path
|
|
84
|
+
$normalizedRoot = ConvertTo-HookTargetWorktreeNormalizedPath -Path $WorktreeRoot
|
|
85
|
+
if ([string]::IsNullOrEmpty($normalizedPath) -or [string]::IsNullOrEmpty($normalizedRoot)) { return $null }
|
|
86
|
+
if ([string]::Equals($normalizedPath, $normalizedRoot, [System.StringComparison]::OrdinalIgnoreCase)) { return '' }
|
|
87
|
+
$prefix = if ($normalizedRoot.EndsWith('/')) { $normalizedRoot } else { $normalizedRoot + '/' }
|
|
88
|
+
if ($normalizedPath.StartsWith($prefix, [System.StringComparison]::OrdinalIgnoreCase)) {
|
|
89
|
+
return $normalizedPath.Substring($prefix.Length)
|
|
90
|
+
}
|
|
91
|
+
return $null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function Get-HookTargetWorktreeEnclosingRoot {
|
|
95
|
+
<#
|
|
96
|
+
.SYNOPSIS
|
|
97
|
+
Return the nearest root that encloses a path, or $null when none does.
|
|
98
|
+
.DESCRIPTION
|
|
99
|
+
A root encloses a path when the normalized path equals the normalized root or
|
|
100
|
+
starts with the root followed by a slash. When several roots enclose the path, the
|
|
101
|
+
longest one wins, so a worktree nested inside another wins for paths under it.
|
|
102
|
+
.PARAMETER Path
|
|
103
|
+
The path to place.
|
|
104
|
+
.PARAMETER Roots
|
|
105
|
+
The candidate worktree roots.
|
|
106
|
+
#>
|
|
107
|
+
[CmdletBinding()]
|
|
108
|
+
[OutputType([string])]
|
|
109
|
+
param(
|
|
110
|
+
[AllowNull()] [AllowEmptyString()] [string] $Path,
|
|
111
|
+
[AllowNull()] [AllowEmptyCollection()] [string[]] $Roots
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
$best = $null
|
|
115
|
+
foreach ($root in @($Roots)) {
|
|
116
|
+
$normalizedRoot = ConvertTo-HookTargetWorktreeNormalizedPath -Path $root
|
|
117
|
+
if ([string]::IsNullOrEmpty($normalizedRoot)) { continue }
|
|
118
|
+
$relative = Get-HookTargetWorktreeRelativePath -Path $Path -WorktreeRoot $normalizedRoot
|
|
119
|
+
if ($null -ne $relative -and ($null -eq $best -or $normalizedRoot.Length -gt $best.Length)) { $best = $normalizedRoot }
|
|
120
|
+
}
|
|
121
|
+
return $best
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function Get-HookTargetWorktreeKey {
|
|
125
|
+
<#
|
|
126
|
+
.SYNOPSIS
|
|
127
|
+
Return the stable key '<sanitized leaf>-<8 hex>' that names a worktree root.
|
|
128
|
+
.DESCRIPTION
|
|
129
|
+
The hex part is the first four bytes of the SHA-256 of the normalized root with no
|
|
130
|
+
trailing slash, and the leaf is reduced to file-name-safe characters. The algorithm
|
|
131
|
+
equals the worktree-derived batch-budget session id, which prefixes 'worktree-'.
|
|
132
|
+
.PARAMETER WorktreeRoot
|
|
133
|
+
The worktree root to key.
|
|
134
|
+
#>
|
|
135
|
+
[CmdletBinding()]
|
|
136
|
+
[OutputType([string])]
|
|
137
|
+
param(
|
|
138
|
+
[Parameter(Mandatory)] [string] $WorktreeRoot
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
$normalizedRoot = (ConvertTo-HookTargetWorktreeNormalizedPath -Path $WorktreeRoot).TrimEnd('/')
|
|
142
|
+
$leaf = $normalizedRoot.Substring($normalizedRoot.LastIndexOf('/') + 1) -replace '[^A-Za-z0-9._-]', '_'
|
|
143
|
+
$hashBytes = [System.Security.Cryptography.SHA256]::HashData([System.Text.Encoding]::UTF8.GetBytes($normalizedRoot))
|
|
144
|
+
$shortHash = -join (@($hashBytes[0..3]) | ForEach-Object { $_.ToString('x2') })
|
|
145
|
+
return "$leaf-$shortHash"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function ConvertFrom-HookTargetWorktreePorcelain {
|
|
149
|
+
<#
|
|
150
|
+
.SYNOPSIS
|
|
151
|
+
Parse `git worktree list --porcelain` text into worktree records.
|
|
152
|
+
.DESCRIPTION
|
|
153
|
+
Emits one record per non-bare stanza with Root (normalized), Branch (the short
|
|
154
|
+
branch name, or $null when detached), Bare, and Prunable. A bare stanza is skipped.
|
|
155
|
+
Blank lines separate stanzas, and CRLF line endings are tolerated.
|
|
156
|
+
.PARAMETER Text
|
|
157
|
+
The porcelain text.
|
|
158
|
+
#>
|
|
159
|
+
[CmdletBinding()]
|
|
160
|
+
[OutputType([pscustomobject])]
|
|
161
|
+
param(
|
|
162
|
+
[AllowNull()] [AllowEmptyString()] [string] $Text
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
$records = [System.Collections.Generic.List[object]]::new()
|
|
166
|
+
$stanza = $null
|
|
167
|
+
foreach ($rawLine in @(([string] $Text -split "`r?`n") + '')) {
|
|
168
|
+
$line = $rawLine.Trim()
|
|
169
|
+
if ($line -eq '' -or $line.StartsWith('worktree ')) {
|
|
170
|
+
if ($null -ne $stanza -and -not $stanza.Bare) {
|
|
171
|
+
$records.Add([pscustomobject]@{ Root = $stanza.Root; Branch = $stanza.Branch; Bare = $false; Prunable = $stanza.Prunable })
|
|
172
|
+
}
|
|
173
|
+
$stanza = $null
|
|
174
|
+
if ($line -ne '') {
|
|
175
|
+
$stanza = @{ Root = (ConvertTo-HookTargetWorktreeNormalizedPath -Path $line.Substring(9)); Branch = $null; Bare = $false; Prunable = $false }
|
|
176
|
+
}
|
|
177
|
+
continue
|
|
178
|
+
}
|
|
179
|
+
if ($null -eq $stanza) { continue }
|
|
180
|
+
if ($line -eq 'bare') {
|
|
181
|
+
$stanza.Bare = $true
|
|
182
|
+
} elseif ($line.StartsWith('branch ')) {
|
|
183
|
+
$reference = $line.Substring(7).Trim()
|
|
184
|
+
$stanza.Branch = if ($reference.StartsWith('refs/heads/')) { $reference.Substring(11) } else { $reference }
|
|
185
|
+
} elseif ($line -eq 'detached') {
|
|
186
|
+
$stanza.Branch = $null
|
|
187
|
+
} elseif ($line -eq 'prunable' -or $line.StartsWith('prunable ')) {
|
|
188
|
+
$stanza.Prunable = $true
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return $records.ToArray()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function Resolve-HookTargetWorktree {
|
|
195
|
+
<#
|
|
196
|
+
.SYNOPSIS
|
|
197
|
+
Resolve the registered worktree that a hook call acts on.
|
|
198
|
+
.DESCRIPTION
|
|
199
|
+
The first present source decides, in the order FilePath, CommandOperand (paths
|
|
200
|
+
and/or branch), DelegationIdentity, SessionRoot; a later source is never consulted.
|
|
201
|
+
Relative paths map to the session root without enumerating worktrees. An absolute
|
|
202
|
+
path maps to its nearest enclosing registered worktree, and a path inside none, or
|
|
203
|
+
an invoker that throws or returns no worktree, falls back to the session root.
|
|
204
|
+
Disagreeing components yield TARGET_WORKTREE_AMBIGUOUS; a present branch or
|
|
205
|
+
identity that names no worktree yields TARGET_WORKTREE_NOT_DERIVABLE and never the
|
|
206
|
+
session root. The result carries WorktreeRoot, Source, Ambiguous, ReasonCode,
|
|
207
|
+
Detail, and Candidates. The invoker runs at most once.
|
|
208
|
+
.PARAMETER SessionRoot
|
|
209
|
+
The absolute session root (the hook's root); the fallback root.
|
|
210
|
+
.PARAMETER FilePath
|
|
211
|
+
File-tool target paths.
|
|
212
|
+
.PARAMETER CommandOperandPath
|
|
213
|
+
Command operand paths parsed by the caller.
|
|
214
|
+
.PARAMETER CommandBranch
|
|
215
|
+
A command branch operand parsed by the caller.
|
|
216
|
+
.PARAMETER DelegationPrompt
|
|
217
|
+
Agent delegation prompt text; its 'branch:' label and canonical issue line are read.
|
|
218
|
+
.PARAMETER CheckpointIssueReader
|
|
219
|
+
Optional { param($WorktreeRoot) <issue number or $null> } used only for an issue identity.
|
|
220
|
+
.PARAMETER WorktreeListInvoker
|
|
221
|
+
{ param($SessionRoot) <porcelain text> }; the default runs git worktree list --porcelain.
|
|
222
|
+
#>
|
|
223
|
+
[CmdletBinding()]
|
|
224
|
+
[OutputType([pscustomobject])]
|
|
225
|
+
param(
|
|
226
|
+
[Parameter(Mandatory)] [string] $SessionRoot,
|
|
227
|
+
[AllowNull()] [AllowEmptyCollection()] [string[]] $FilePath,
|
|
228
|
+
[AllowNull()] [AllowEmptyCollection()] [string[]] $CommandOperandPath,
|
|
229
|
+
[AllowNull()] [AllowEmptyString()] [string] $CommandBranch,
|
|
230
|
+
[AllowNull()] [AllowEmptyString()] [string] $DelegationPrompt,
|
|
231
|
+
[scriptblock] $CheckpointIssueReader,
|
|
232
|
+
[scriptblock] $WorktreeListInvoker = { param($SessionRoot) $porcelain = & git -C $SessionRoot worktree list --porcelain 2>$null; if ($LASTEXITCODE -ne 0) { return '' }; return ($porcelain -join "`n") }
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
$ambiguousCode = 'TARGET_WORKTREE_AMBIGUOUS'
|
|
236
|
+
$notDerivableCode = 'TARGET_WORKTREE_NOT_DERIVABLE'
|
|
237
|
+
$ignoreCase = [System.StringComparison]::OrdinalIgnoreCase
|
|
238
|
+
$absolutePattern = '^(/|[A-Za-z]:/)'
|
|
239
|
+
$session = ConvertTo-HookTargetWorktreeNormalizedPath -Path $SessionRoot
|
|
240
|
+
|
|
241
|
+
# Nested helpers are called by constant name and read this function's variables.
|
|
242
|
+
function Get-HookTargetResult {
|
|
243
|
+
param($Root, [string] $Source, $Code, [string] $Detail = '', [string[]] $Candidates = @())
|
|
244
|
+
[pscustomobject]@{
|
|
245
|
+
WorktreeRoot = $Root
|
|
246
|
+
Source = $Source
|
|
247
|
+
Ambiguous = ($Code -eq $ambiguousCode)
|
|
248
|
+
ReasonCode = $Code
|
|
249
|
+
Detail = $Detail
|
|
250
|
+
Candidates = [string[]] @($Candidates)
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
function Add-HookTargetDistinctValue {
|
|
254
|
+
param([System.Collections.Generic.List[string]] $List, [string] $Value)
|
|
255
|
+
foreach ($existing in $List) { if ([string]::Equals($existing, $Value, $ignoreCase)) { return } }
|
|
256
|
+
$List.Add($Value)
|
|
257
|
+
}
|
|
258
|
+
function Get-HookTargetRecordedIssue {
|
|
259
|
+
param([string] $Root)
|
|
260
|
+
try { $value = ([string] (& $CheckpointIssueReader $Root)).Trim().TrimStart('#') } catch { return $null }
|
|
261
|
+
if ($value -notmatch '^\d+$') { return $null }
|
|
262
|
+
$digits = $value.TrimStart('0')
|
|
263
|
+
return $(if ($digits -eq '') { '0' } else { $digits })
|
|
264
|
+
}
|
|
265
|
+
function Resolve-HookTargetPathSource {
|
|
266
|
+
param([string[]] $Paths, [string] $Source)
|
|
267
|
+
$resolved = [System.Collections.Generic.List[string]]::new()
|
|
268
|
+
$details = [System.Collections.Generic.List[string]]::new()
|
|
269
|
+
$fellBack = $false
|
|
270
|
+
foreach ($candidate in $Paths) {
|
|
271
|
+
$normalized = ConvertTo-HookTargetWorktreeNormalizedPath -Path $candidate
|
|
272
|
+
$root = $session
|
|
273
|
+
if ($normalized -notmatch $absolutePattern) {
|
|
274
|
+
$fellBack = $true
|
|
275
|
+
$details.Add("relative path '$normalized' maps to the session root")
|
|
276
|
+
} else {
|
|
277
|
+
$enclosing = Get-HookTargetWorktreeEnclosingRoot -Path $normalized -Roots $roots
|
|
278
|
+
if ($null -eq $enclosing) {
|
|
279
|
+
$fellBack = $true
|
|
280
|
+
$details.Add($(if ($listDetail) { $listDetail } else { "path '$normalized' is inside no registered worktree" }))
|
|
281
|
+
} else {
|
|
282
|
+
$root = $enclosing
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
Add-HookTargetDistinctValue -List $resolved -Value $root
|
|
286
|
+
}
|
|
287
|
+
if ($resolved.Count -gt 1) {
|
|
288
|
+
return (Get-HookTargetResult -Root $null -Source $Source -Code $ambiguousCode -Detail ('the paths resolve to {0} different worktrees' -f $resolved.Count) -Candidates $resolved.ToArray())
|
|
289
|
+
}
|
|
290
|
+
$decidedBy = if ($fellBack) { 'SessionRoot' } else { $Source }
|
|
291
|
+
return (Get-HookTargetResult -Root $resolved[0] -Source $decidedBy -Code $null -Detail ($details -join '; '))
|
|
292
|
+
}
|
|
293
|
+
function Resolve-HookTargetBranchSource {
|
|
294
|
+
param([string] $Branch, [string] $Source)
|
|
295
|
+
$matched = @($records | Where-Object { $null -ne $_.Branch -and [string]::Equals($_.Branch, $Branch, $ignoreCase) } | ForEach-Object { $_.Root })
|
|
296
|
+
if ($matched.Count -eq 0) { return (Get-HookTargetResult -Root $null -Source $Source -Code $notDerivableCode -Detail "branch '$Branch' is checked out in no registered worktree") }
|
|
297
|
+
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) }
|
|
298
|
+
return (Get-HookTargetResult -Root $matched[0] -Source $Source -Code $null)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
# Select the deciding source: the first present one in precedence order.
|
|
302
|
+
$filePaths = @($FilePath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
303
|
+
$operandPaths = @($CommandOperandPath | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
|
304
|
+
$branchOperand = if ([string]::IsNullOrWhiteSpace($CommandBranch)) { '' } else { $CommandBranch.Trim() }
|
|
305
|
+
$branches = [System.Collections.Generic.List[string]]::new()
|
|
306
|
+
$issues = [System.Collections.Generic.List[string]]::new()
|
|
307
|
+
if ($filePaths.Count -gt 0) {
|
|
308
|
+
$source = 'FilePath'
|
|
309
|
+
} elseif ($operandPaths.Count -gt 0 -or $branchOperand) {
|
|
310
|
+
$source = 'CommandOperand'
|
|
311
|
+
} elseif (-not [string]::IsNullOrWhiteSpace($DelegationPrompt)) {
|
|
312
|
+
$source = 'DelegationIdentity'
|
|
313
|
+
foreach ($match in [regex]::Matches($DelegationPrompt, '(?i)\bbranch:\s*(?<name>[A-Za-z0-9][A-Za-z0-9._/-]*)')) {
|
|
314
|
+
Add-HookTargetDistinctValue -List $branches -Value $match.Groups['name'].Value.TrimEnd('.', '/')
|
|
315
|
+
}
|
|
316
|
+
foreach ($match in [regex]::Matches($DelegationPrompt, 'Canonical issue number for this feature is #?(?<number>\d+)')) {
|
|
317
|
+
$digits = $match.Groups['number'].Value.TrimStart('0')
|
|
318
|
+
Add-HookTargetDistinctValue -List $issues -Value $(if ($digits -eq '') { '0' } else { $digits })
|
|
319
|
+
}
|
|
320
|
+
} else {
|
|
321
|
+
return (Get-HookTargetResult -Root $session -Source 'SessionRoot' -Code $null -Detail 'no target source is present; the session root applies')
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
# Enumerate registered worktrees at most once, and only when the deciding source needs them.
|
|
325
|
+
$needsList = switch ($source) {
|
|
326
|
+
'FilePath' { @($filePaths | Where-Object { (ConvertTo-HookTargetWorktreeNormalizedPath -Path $_) -match $absolutePattern }).Count -gt 0 }
|
|
327
|
+
'CommandOperand' { [bool] $branchOperand -or @($operandPaths | Where-Object { (ConvertTo-HookTargetWorktreeNormalizedPath -Path $_) -match $absolutePattern }).Count -gt 0 }
|
|
328
|
+
default { ($branches.Count -eq 1 -and $issues.Count -le 1) -or ($branches.Count -eq 0 -and $issues.Count -eq 1 -and $null -ne $CheckpointIssueReader) }
|
|
329
|
+
}
|
|
330
|
+
$records = @()
|
|
331
|
+
$listDetail = ''
|
|
332
|
+
if ($needsList) {
|
|
333
|
+
try {
|
|
334
|
+
$records = @(ConvertFrom-HookTargetWorktreePorcelain -Text (@(& $WorktreeListInvoker $SessionRoot) -join "`n"))
|
|
335
|
+
if ($records.Count -eq 0) { $listDetail = 'the worktree list returned no registered worktree' }
|
|
336
|
+
} catch {
|
|
337
|
+
$records = @()
|
|
338
|
+
$listDetail = "the worktree list failed: $($_.Exception.Message)"
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
$roots = @($records | ForEach-Object { $_.Root })
|
|
342
|
+
|
|
343
|
+
if ($source -eq 'FilePath') { return (Resolve-HookTargetPathSource -Paths $filePaths -Source 'FilePath') }
|
|
344
|
+
|
|
345
|
+
if ($source -eq 'CommandOperand') {
|
|
346
|
+
$pathResult = if ($operandPaths.Count -gt 0) { Resolve-HookTargetPathSource -Paths $operandPaths -Source 'CommandOperand' } else { $null }
|
|
347
|
+
if (-not $branchOperand) { return $pathResult }
|
|
348
|
+
$branchResult = Resolve-HookTargetBranchSource -Branch $branchOperand -Source 'CommandOperand'
|
|
349
|
+
if ($null -eq $pathResult) { return $branchResult }
|
|
350
|
+
if ($pathResult.Ambiguous) { return $pathResult }
|
|
351
|
+
if ($null -ne $branchResult.ReasonCode) { return $branchResult }
|
|
352
|
+
if (-not [string]::Equals($pathResult.WorktreeRoot, $branchResult.WorktreeRoot, $ignoreCase)) {
|
|
353
|
+
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))
|
|
354
|
+
}
|
|
355
|
+
return $branchResult
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
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 ', '))) }
|
|
359
|
+
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 ', '))) }
|
|
360
|
+
if ($branches.Count -eq 1) {
|
|
361
|
+
$branchResult = Resolve-HookTargetBranchSource -Branch $branches[0] -Source $source
|
|
362
|
+
if ($null -ne $branchResult.ReasonCode -or $issues.Count -eq 0 -or $null -eq $CheckpointIssueReader) { return $branchResult }
|
|
363
|
+
$target = $branchResult.WorktreeRoot
|
|
364
|
+
$recorded = Get-HookTargetRecordedIssue -Root $target
|
|
365
|
+
if ($null -ne $recorded) {
|
|
366
|
+
if ($recorded -eq $issues[0]) { return $branchResult }
|
|
367
|
+
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))
|
|
368
|
+
}
|
|
369
|
+
$elsewhere = @($records | Where-Object { -not [string]::Equals($_.Root, $target, $ignoreCase) -and (Get-HookTargetRecordedIssue -Root $_.Root) -eq $issues[0] } | ForEach-Object { $_.Root })
|
|
370
|
+
if ($elsewhere.Count -gt 0) {
|
|
371
|
+
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))
|
|
372
|
+
}
|
|
373
|
+
return $branchResult
|
|
374
|
+
}
|
|
375
|
+
if ($issues.Count -eq 1) {
|
|
376
|
+
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") }
|
|
377
|
+
$matched = @($records | Where-Object { (Get-HookTargetRecordedIssue -Root $_.Root) -eq $issues[0] } | ForEach-Object { $_.Root })
|
|
378
|
+
if ($matched.Count -eq 0) { return (Get-HookTargetResult -Root $null -Source $source -Code $notDerivableCode -Detail "issue $($issues[0]) is recorded by no registered worktree") }
|
|
379
|
+
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) }
|
|
380
|
+
return (Get-HookTargetResult -Root $matched[0] -Source $source -Code $null)
|
|
381
|
+
}
|
|
382
|
+
return (Get-HookTargetResult -Root $null -Source $source -Code $notDerivableCode -Detail 'the prompt carries no branch: label and no canonical issue line')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
Export-ModuleMember -Function `
|
|
386
|
+
Resolve-HookTargetWorktree, `
|
|
387
|
+
ConvertFrom-HookTargetWorktreePorcelain, `
|
|
388
|
+
Get-HookTargetWorktreeEnclosingRoot, `
|
|
389
|
+
ConvertTo-HookTargetWorktreeNormalizedPath, `
|
|
390
|
+
Get-HookTargetWorktreeRelativePath, `
|
|
391
|
+
Get-HookTargetWorktreeKey
|
|
@@ -329,3 +329,4 @@ Fail-closed behavior: any adoption error waives nothing, so each missing receipt
|
|
|
329
329
|
does not currently validate standalone_merge_authorizations
|
|
330
330
|
entries; adding that check is recorded as follow-up FU-3.
|
|
331
331
|
- The `issue_adoption` invariants are enforced at completion by the routing-contract check in all three runtimes: `scripts/dev_tools/_orchestrator_state_issue_adoption.py` (Python authority), `extensions/drm-copilot/src/lib/validate/orchestrator-state-issue-adoption.ts`, and `.claude/lib/orchestrator-state/OrchestratorStateIssueAdoption.psm1`, with byte-identical error strings pinned by the shared corpus under `tests/fixtures/orchestrator_state_issue_adoption/`. Rules 10 and 11 live in the record modules `scripts/dev_tools/_orchestrator_state_feature_folder_record.py`, `extensions/drm-copilot/src/lib/validate/orchestrator-state-feature-folder-record.ts`, and `.claude/lib/orchestrator-state/OrchestratorStateFeatureFolderRecord.psm1`. The `HEAD` probe for a waived `new_active_feature_folder` lives in the hook sibling `.claude/hooks/validate-orchestrator-output-feature-folder.ps1`, loaded by `.claude/hooks/validate-orchestrator-output.ps1`.
|
|
332
|
+
- The batch-budget hooks (`.claude/hooks/enforce-python-batch-budget.ps1`, `.claude/hooks/enforce-powershell-batch-budget.ps1`, and the Codex copies under `.codex/hooks/`) read the checkpoint of the target worktree that `Resolve-HookTargetWorktree` resolves for the edited file, so a coordinating session never needs a checkpoint at its own root for them.
|
|
@@ -13,6 +13,7 @@ paths:
|
|
|
13
13
|
- "scripts/dev_tools/validate_orchestration_artifacts.py"
|
|
14
14
|
- "extensions/drm-copilot/src/lib/validate/orchestration-artifacts.ts"
|
|
15
15
|
- ".claude/hooks/enforce-epic-merge-gate.ps1"
|
|
16
|
+
- ".claude/hooks/validate-orchestrator-output-rejection.ps1"
|
|
16
17
|
- ".claude/agents/parallel-orchestrator.md"
|
|
17
18
|
- ".claude/agents/parallel-planner.md"
|
|
18
19
|
- ".claude/skills/parallel-*/SKILL.md"
|
|
@@ -213,6 +214,38 @@ The wave-4 features — F6 (mutation protocol), F7 (enforcement hooks), and F8 (
|
|
|
213
214
|
|
|
214
215
|
Integration-cost scheduling (issue #722) adds no `conflict_edges[].reason` member; the four members above are unchanged. The scheduling data travels in tolerated-not-validated fields: an edge may carry the extra fields `hard`, `cost`, and `benefit`, and the planner and orchestrator checkpoints may carry a `tolerated_overlaps` list. No validator reads these fields, and the Python validators and the TypeScript validator port accept them with zero errors.
|
|
215
216
|
|
|
217
|
+
## Mutation Rejection Signal (issue #1012)
|
|
218
|
+
|
|
219
|
+
A parallel skill that rejects a mutation before any run checkpoint exists stops without writing state. Its final output then carries a rejection signal so that the completion stop hook `.claude/hooks/validate-orchestrator-output.ps1` can release the stop without demanding a checkpoint that the skill correctly did not write.
|
|
220
|
+
|
|
221
|
+
**Line grammar.** The signal is the last non-blank line of the final output, after trailing whitespace (including a carriage return) is trimmed, and it matches `^PARALLEL_MUTATION_REJECTED: (?<code>[a-z_]+)$` case-sensitively: the uppercase prefix, a colon, exactly one space, and one code from the table below, with no Markdown formatting (no backticks, no bold), no leading text, and no trailing text. A token on an earlier line, inside prose, or in any other form is not a signal. Where the run's slug is known, the line `parallel_slug: <slug>` precedes the signal.
|
|
222
|
+
|
|
223
|
+
**Closed code set.** The set is defined here and nowhere else:
|
|
224
|
+
|
|
225
|
+
| Code | Meaning |
|
|
226
|
+
| --- | --- |
|
|
227
|
+
| `kickoff_artifact_missing` | `/parallel-run` found no committed kickoff artifact for the run and delegated nothing. |
|
|
228
|
+
| `run_not_started` | The run is pending or not started, or no checkpoint tracks the named `parallel_slug`, so there is no running run to mutate. |
|
|
229
|
+
| `items_in_flight` | The mutation is rejected because an item is in flight: a close while any item is in flight, or a removal of an in-flight item without `--disposition`. |
|
|
230
|
+
| `unknown_item` | The removal target is not a live item: it is unknown, `withdrawn`, or `blocked`. |
|
|
231
|
+
|
|
232
|
+
**Output signal, not a schema member.** The signal is an output signal, not a checkpoint field, and it is not one of the nine enums owned by `## Enum Ownership (F6/F7/F8 consume, never extend)`; that table and its count are unchanged. No validator reads the signal and no checkpoint records it.
|
|
233
|
+
|
|
234
|
+
**Hook behavior.** The hook evaluates the signal only for `parallel-orchestrator-state` when the checkpoint resolution status is `NoTarget`:
|
|
235
|
+
|
|
236
|
+
| Condition | Result |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| parallel, `NoTarget`, valid signal, no contradicting state in any live worktree | allow, regardless of `stop_hook_active` |
|
|
239
|
+
| parallel, `NoTarget`, no signal or an invalid signal | block as before (repairable); the message gains a hint line naming the signal form and this section, and lists the allowed codes when the final line carries the prefix with an invalid code |
|
|
240
|
+
| parallel, `NoTarget`, valid signal, and a live worktree holds parallel checkpoint text that is unparseable, has a non-object root, is off-route, lacks a `parallel_slug`, or records the slug the output names | block as before |
|
|
241
|
+
| parallel, `Ambiguous` or `Rejected`, any signal | block as before |
|
|
242
|
+
| parallel, resolved checkpoint, any signal | full validation as before |
|
|
243
|
+
| `orchestrator-state` or `epic-orchestrator-state`, any signal | unchanged |
|
|
244
|
+
|
|
245
|
+
A signal never relaxes validation when a checkpoint resolves, and a path that wrote state validates fully. The parser, the state-absence scan, and the decision live in `.claude/hooks/validate-orchestrator-output-rejection.ps1`, whose code array mirrors the table above and is pinned to it by a parity test.
|
|
246
|
+
|
|
247
|
+
**Ownership.** The parallel skills consume these codes and never redefine them. Each mapped STOP path in `parallel-run`, `parallel-add`, `parallel-close`, and `parallel-remove` names only its own code and cites this section by heading; no skill restates the full set. A new code requires an amendment to this section, to the hook array, and to the parity test in the same change.
|
|
248
|
+
|
|
216
249
|
## F7 Seam
|
|
217
250
|
|
|
218
251
|
The retrospective cohort-ordering invariant `PARALLEL_COHORT_BARRIER_VIOLATION` (design section 9, Layer 2) is F7's explicitly assigned addition to the orchestrator validator. It is NOT implemented here. The entry point of `scripts/dev_tools/validate_parallel_orchestrator_state.py` contains a clearly delimited, appendable helper-invocation block, marked with explicit begin and end comments that name F7 and the invariant token, so that F7's edit is one appended helper call with no reflow of existing code. The TypeScript core `extensions/drm-copilot/src/lib/validate/parallel-orchestrator-state-core.ts` carries the matching comment-delimited seam. Existing helper calls sit outside the block.
|
|
@@ -255,19 +288,45 @@ Three constraints bound the mandate-read exclusion:
|
|
|
255
288
|
1. **The planner remains obliged to enumerate a genuine write explicitly.** An exclusion describes
|
|
256
289
|
the default reading relationship, not a permanent ban. When an item's plan will actually write an
|
|
257
290
|
excluded path, the planner appends that exact path to the declared radius after normalization.
|
|
258
|
-
2. **`quality-tiers.yml`
|
|
259
|
-
|
|
260
|
-
|
|
291
|
+
2. **`quality-tiers.yml` is an append-only registry and a mandate read (issue #1006).** Every item
|
|
292
|
+
that adds a module appends one entry to it, so both committed truth tables list it in
|
|
293
|
+
`conflict_tolerance.append_only_paths` and in `mandate_reads`, not in `shared_surfaces`. When an
|
|
294
|
+
item really writes it, the planner-appended or observed path contends on `path_overlap` at the
|
|
295
|
+
`append_only` weight and is never a hard edge; when an item merely cites it, the mandate-read
|
|
296
|
+
exclusion drops the citation. Separator-free token acceptance for it through the root-surface rule
|
|
297
|
+
(`config_root_surfaces`, `Get-ConfigRootSurface`) no longer applies, because that rule reads only
|
|
298
|
+
the separator-free entries of `shared_surfaces`; the planner append obligation in constraint 1 and
|
|
299
|
+
`detect_escaped_paths` in constraint 3 are the backstops.
|
|
261
300
|
3. **`detect_escaped_paths` makes the read/write distinction exact at execution time.** The
|
|
262
301
|
derivation heuristic reads intent from plan text and can be wrong in either direction; drift
|
|
263
302
|
detection compares the declared radius against the paths a diff actually touched, so an item that
|
|
264
303
|
wrote an excluded path is caught against observed evidence rather than against prose.
|
|
265
304
|
|
|
266
|
-
The extractor additionally rejects
|
|
305
|
+
The extractor additionally rejects five token shapes that were never write claims: a wildcard-free
|
|
267
306
|
token whose final component names a directory rather than a file, a `docs/features/` glob whose
|
|
268
307
|
wildcard occupies or truncates the feature-folder segment, a contract token carrying no ASCII
|
|
269
|
-
letter,
|
|
270
|
-
|
|
308
|
+
letter, a token containing a placeholder or interpolation marker, and a contract token that is a
|
|
309
|
+
default spreadsheet sheet name (`Sheet` or `Chart` followed by ASCII digits, compared
|
|
310
|
+
case-insensitively). `artifacts/` is not a known top-level segment, so a bare `artifacts/**`
|
|
311
|
+
subtree claim no longer satisfies the shape rules.
|
|
312
|
+
|
|
313
|
+
#### Generic contract-token rejection (issue #1006)
|
|
314
|
+
|
|
315
|
+
A spreadsheet application names a new worksheet or chart sheet with a default stem followed by a
|
|
316
|
+
number, so a spec that mentions `Sheet1` in an interface section names a worksheet, not an
|
|
317
|
+
exported symbol, schema, or CLI identifier. Two unrelated items that both named `Sheet1` received a
|
|
318
|
+
hard `contract_dependency` edge. The stems are a code constant, not a truth-table key:
|
|
319
|
+
`GENERIC_CONTRACT_STEMS` in `scripts/dev_tools/_blast_radius_token_shapes.py` and
|
|
320
|
+
`$script:GenericContractStem` in `.claude/lib/blast-radius/BlastRadiusTokenShape.psm1`, pinned equal
|
|
321
|
+
across Python and PowerShell by a Pester parity test that reads the Python source. Workbook and
|
|
322
|
+
object names such as `Book1` and `Table1` are a different class and are not rejected. The predicate
|
|
323
|
+
(`is_generic_contract_token`, `Test-GenericContractToken`) first requires an ASCII token, because a
|
|
324
|
+
.NET case-insensitive match folds the Kelvin sign and the long s to ASCII letters while Python
|
|
325
|
+
`re.ASCII` does not (the issue #903 precedent). It is applied at all six contract-filtering sites:
|
|
326
|
+
`extract_contract_identifiers`, `extract_write_intent_contracts`, and `normalize_declared_radius` in
|
|
327
|
+
Python, and `Get-ContractIdentifier`, `Get-WriteIntentSpecContract`, and
|
|
328
|
+
`Get-NormalizedDeclaredRadius` in PowerShell, so a previously recorded `Sheet1` is dropped on
|
|
329
|
+
recompute. TypeScript has no contract extractor, so no TypeScript parity applies.
|
|
271
330
|
|
|
272
331
|
### Mechanically-mergeable path class (issue #643)
|
|
273
332
|
|
|
@@ -454,12 +513,16 @@ narrowing. The only sanctioned mechanisms for relaxing contention are the config
|
|
|
454
513
|
radius: editing a declared radius to suppress an edge remains prohibited.
|
|
455
514
|
|
|
456
515
|
**Configuration.** `config/blast-radius.json` carries an optional `conflict_tolerance` object with
|
|
457
|
-
tolerance_percent (integer, at least 0), `weights` (same_file, possible_overlap, append_only,
|
|
458
|
-
module, each an integer of at least 1), `band_durations` (C1 through C4, each an
|
|
459
|
-
1), `default_band` (one of C1 through C4),
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
516
|
+
tolerance_percent (integer, at least 0), `weights` (same_file, possible_overlap, append_only,
|
|
517
|
+
module, and contract, each an integer of at least 1), `band_durations` (C1 through C4, each an
|
|
518
|
+
integer of at least 1), `default_band` (one of C1 through C4), append_only_paths (a list of exact
|
|
519
|
+
entries and `**` globs), and the optional `combination_threshold_percent` (integer, at least 0;
|
|
520
|
+
default 400 when absent). The `contract` weight (issue #1013) is required and validated like the
|
|
521
|
+
other weights; its committed value is 8, and a destination overrides it, like every other member,
|
|
522
|
+
through `config/blast-radius.local.json`. `combination_threshold_percent` is committed as 400 and is
|
|
523
|
+
overridden through the same overlay. The reader fails fast with an error naming the key for any other
|
|
524
|
+
shape; booleans are rejected wherever an integer is required. An absent key means strict scheduling,
|
|
525
|
+
identical to tolerance_percent 0.
|
|
463
526
|
|
|
464
527
|
**Edge rule.** For each unordered pair of items:
|
|
465
528
|
|
|
@@ -473,7 +536,14 @@ tolerance_percent 0.
|
|
|
473
536
|
possible_overlap weight for a glob or directory-prefix overlap, and the module weight times the
|
|
474
537
|
number of shared modules. The append_only_paths check runs before the same_file check (append-only
|
|
475
538
|
precedence), so an append-only file stays low cost even when both items name it. Mergeable paths
|
|
476
|
-
contribute 0 because they are excluded before enumeration.
|
|
539
|
+
contribute 0 because they are excluded before enumeration. Add the contract weight times the
|
|
540
|
+
number of distinct shared contract identifiers: the set the relation intersects to report
|
|
541
|
+
`contract_dependency`, each identifier counted once however often either radius lists it.
|
|
542
|
+
`contract_dependency` remains a hard class, so the contract term changes recorded edge costs and
|
|
543
|
+
combination eligibility and never an edge set. Invariant: at tolerance_percent 0 the edge set
|
|
544
|
+
equals strict scheduling for any `contract` weight, and at every tolerance the edge set with the
|
|
545
|
+
contract term equals the edge set without it; only the `cost` of edges whose pair shares a
|
|
546
|
+
contract differs.
|
|
477
547
|
4. **Benefit** (integer, pairwise). The smaller of the two items' band durations; a missing band uses
|
|
478
548
|
`default_band`. Running the pair concurrently saves at most one cohort step of the shorter item.
|
|
479
549
|
5. **Rule.** The pair is an edge if and only if it conflicts and either it is hard or
|
|
@@ -485,8 +555,27 @@ least 1. A detected conflict that is not hard therefore has a cost of at least 1
|
|
|
485
555
|
tolerance_percent 0 the inequality `cost * 100 > 0` always holds and the edge set equals the
|
|
486
556
|
detected-conflict set exactly; cohort coloring of that set is identical to strict scheduling. At
|
|
487
557
|
every tolerance an edge requires a conflict, so every edge implies a conflict and the edge set is
|
|
488
|
-
always a subset of the detected-conflict set. Raising tolerance_percent never
|
|
489
|
-
decision for (a, b) equals the decision for (b, a).
|
|
558
|
+
always a subset of the detected-conflict set. Raising tolerance_percent never turns a pairwise
|
|
559
|
+
decision into an edge, and the decision for (a, b) equals the decision for (b, a). Because the
|
|
560
|
+
aggregate cap below can promote a tolerated overlap, this monotonicity holds for the pairwise
|
|
561
|
+
decision only, not for the capped edge set.
|
|
562
|
+
|
|
563
|
+
**Aggregate cap (issue #1013).** The pairwise rule judges each pair alone, so the single entry point
|
|
564
|
+
in each runtime (`schedule_conflict_edges` through `select_capped_pairs` in
|
|
565
|
+
`scripts/dev_tools/_blast_radius_aggregate.py`, and Get-BlastRadiusConflictEdge through
|
|
566
|
+
Get-AggregateCappedPair in `.claude/lib/blast-radius/BlastRadiusAggregate.psm1`) applies a post-pass
|
|
567
|
+
that planners and `parallel-add` cannot skip. For item `i`, `total(i)` is the summed cost of its
|
|
568
|
+
remaining tolerated overlaps, and `i` is over its cap when
|
|
569
|
+
`total(i) * 100 > band_duration(i) * tolerance_percent` (integers only; a missing band reads as
|
|
570
|
+
`default_band`). While any item is over its cap, the smallest-key such item promotes the overlap with
|
|
571
|
+
the minimum key (benefit ascending, cost descending, a ascending, b ascending) to an edge carrying
|
|
572
|
+
`hard` false and the overlap's first reason, and the totals are recomputed. At tolerance_percent 0
|
|
573
|
+
nothing is tolerated, so the pass is a no-op and strict identity holds. A single tolerated overlap
|
|
574
|
+
never exceeds the cap, so the cap binds only for an item with two or more tolerated overlaps. The
|
|
575
|
+
capped scheduling output has five pinned set-level properties: the capped edges are a superset of
|
|
576
|
+
the pairwise edges and a subset of the detected conflicts; the output at tolerance 0 equals the
|
|
577
|
+
uncapped output; every item is within its cap after the pass; the output is invariant to input
|
|
578
|
+
order; and a second pass changes nothing.
|
|
490
579
|
|
|
491
580
|
**Soft-overlap handling.** A detected, non-hard pair within tolerance is a tolerated overlap, not an
|
|
492
581
|
edge. The planner records it in a tolerated_overlaps list on the planner checkpoint (and the
|
|
@@ -502,7 +591,39 @@ the checkpoint (`default_band` when absent), through the helper module
|
|
|
502
591
|
yields an edge and the pair is not already a recorded edge. A tolerated pair whose observed overlap
|
|
503
592
|
stays within tolerance halts neither item; one whose observed overlap exceeds tolerance, or becomes
|
|
504
593
|
hard, is reported. An unevaluable peer radius still counts as an edge (fail closed). At
|
|
505
|
-
tolerance_percent 0 the drift output equals strict drift output.
|
|
594
|
+
tolerance_percent 0 the drift output equals strict drift output. Drift detection stays pairwise: it
|
|
595
|
+
does not apply the aggregate cap, and a cap-promoted pair is a recorded edge, so drift never reports
|
|
596
|
+
it as newly conflicting.
|
|
597
|
+
|
|
598
|
+
**Telemetry (issue #1013).** Three fields are tolerated and not validated; no validator reads them:
|
|
599
|
+
`tolerated_overlaps[].observed_resolution` on the orchestrator checkpoint, `items[].combined_issue_nums`
|
|
600
|
+
on the planner and orchestrator checkpoints and the run manifest, and a top-level `combinations` list
|
|
601
|
+
on the planner checkpoint. `observed_resolution` has the shape `later_item` (integer), `conflicted`
|
|
602
|
+
(boolean), `conflict_detected_at` (timestamp or null), `resolved_at` (timestamp or null),
|
|
603
|
+
`resolution_minutes` (integer), and `recorded_at` (timestamp). The parallel orchestrator writes it for
|
|
604
|
+
the later-merging item of a tolerated pair. Attribution is non-exclusive: one conflict episode is
|
|
605
|
+
attributed to every tolerated peer that merged before it. Nothing reads `observed_resolution`, and no
|
|
606
|
+
weight changes automatically from it.
|
|
607
|
+
|
|
608
|
+
**Issue combination (issue #1013).** At planning time only, `find_combinations` in
|
|
609
|
+
`scripts/dev_tools/_blast_radius_combination.py` and Get-BlastRadiusCombination in
|
|
610
|
+
`.claude/lib/blast-radius/BlastRadiusCombination.psm1` find pairs that are cheaper to deliver as one
|
|
611
|
+
item. Combination is disabled when `conflict_tolerance` is absent or tolerance_percent is 0. A
|
|
612
|
+
conflicting pair is a candidate when its full reasons contain `contract_dependency` or it has a
|
|
613
|
+
same-file overlap (two equal concrete entries, after the mergeable exclusion, neither matching
|
|
614
|
+
append_only_paths), and it qualifies when `cost * 100 >= benefit * combination_threshold_percent`.
|
|
615
|
+
A contract-only pair is a candidate and qualifies when its contract-term cost meets the threshold;
|
|
616
|
+
one that does not qualify still serializes as a hard `contract_dependency` edge. Qualifying pairs
|
|
617
|
+
are grouped into connected components; each becomes one combination with ascending members, the
|
|
618
|
+
smallest member as primary, its pairs sorted by `(a, b)`, and `estimated_savings` equal to the sum
|
|
619
|
+
of its pair costs. There is no size cap on a combined item. The planner re-plans the primary
|
|
620
|
+
member's branch to cover every member, removes the other members from `items[]`, and records
|
|
621
|
+
`combined_issue_nums` and the `combinations` entry. No umbrella issue is created: the member issues
|
|
622
|
+
stay separate on GitHub, and the one PR closes every member through its author-asserted autoclose
|
|
623
|
+
line. A combined item remediates as one unit (one child, one `remediation_loop`, one PR) and never
|
|
624
|
+
splits back into separate PRs. A larger combined PR to review and a shared remediation budget are an
|
|
625
|
+
accepted trade; a destination tunes `combination_threshold_percent` to change how often items
|
|
626
|
+
combine.
|
|
506
627
|
|
|
507
628
|
### Write-intent extraction (issue #722)
|
|
508
629
|
|
|
@@ -674,8 +795,19 @@ keys are appended after the base keys. An overlay `version` must equal the base
|
|
|
674
795
|
forbidden-glob guard (`**`, `docs/**`, `tests/**`) applies to the composed module map, including
|
|
675
796
|
overlay-authored modules. A malformed overlay, a `version` mismatch, or a forbidden glob raises an
|
|
676
797
|
error before any write, so `config/blast-radius.json` keeps its prior bytes. With no overlay
|
|
677
|
-
present, the written file is byte-identical to the base document.
|
|
678
|
-
|
|
798
|
+
present, the written file is byte-identical to the base document. A destination demotes a shipped
|
|
799
|
+
shared surface with the overlay directive `shared_surfaces_demote`, a list of strings (issue
|
|
800
|
+
#1006). It is applied after all other keys are composed: each named entry is removed from
|
|
801
|
+
`shared_surfaces`, with the order of the remaining entries preserved, and unioned into
|
|
802
|
+
`conflict_tolerance.append_only_paths`, base entries first. The directive is consumed, not emitted,
|
|
803
|
+
so the composed file stays within the truth-table key set. An entry that is already an append-only
|
|
804
|
+
path and not a shared surface is accepted as a no-op, so composing a composed result again yields
|
|
805
|
+
identical output. The directive fails closed, before any write, on a non-list value, a non-string
|
|
806
|
+
member, a composed document without `conflict_tolerance.append_only_paths` (no partial
|
|
807
|
+
`conflict_tolerance` is created), or an unknown entry that is neither a shared surface nor an
|
|
808
|
+
append-only path. Demotion never touches `modules`, so the forbidden-glob guard is unchanged. A
|
|
809
|
+
demoted entry that a `shared_surface_globs` pattern still matches stays shared through the glob; the
|
|
810
|
+
bundled glob list is empty. The overlay still cannot remove a derived module.
|
|
679
811
|
|
|
680
812
|
## Enforcement
|
|
681
813
|
|