@danmoisan/drm-copilot-mcp 1.1.9 → 1.1.11
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 +3526 -1037
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/parallel-orchestrator.md +15 -2
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +3 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +49 -14
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +7 -1
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +103 -6
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +58 -3
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +37 -5
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +13 -2
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +35 -7
- package/resources/claude-customizations/.claude/hooks/hook-command-invocation.ps1 +483 -0
- package/resources/claude-customizations/.claude/hooks/hook-command-scanner.ps1 +483 -0
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +254 -5
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +18 -75
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConflict.psm1 +290 -0
- package/resources/claude-customizations/.claude/lib/cleanup-manifest/CleanupWorktreeManifest.psm1 +415 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMerge.psm1 +355 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/ProjectFileMergeGrammar.psm1 +318 -0
- package/resources/claude-customizations/.claude/lib/project-file-merge/Resolve-MergeableConflict.ps1 +229 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +311 -16
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +42 -0
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +36 -2
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/powershell-orchestration-state-machine/SKILL.md +29 -1
- package/resources/claude-customizations/config/blast-radius.json +7 -0
- package/resources/claude-customizations/pack-manifests/core.json +7 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrate/SKILL.md +41 -0
- package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md +41 -0
- package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md +26 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +49 -3
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +57 -12
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-worktree-removal-gate.ps1 +34 -8
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +6 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1 +34 -7
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-invocation.ps1 +483 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/hook-command-scanner.ps1 +483 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/validate-bash.ps1 +130 -2
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +6 -1
- package/resources/config/orchestration-handoff-registry.json +138 -0
- package/resources/config/orchestration-handoff.schema.json +472 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +38 -0
|
@@ -39,6 +39,47 @@ On every invocation, the main session must:
|
|
|
39
39
|
selected route's required agents, skills, and MCP tools into checkpoint
|
|
40
40
|
state.
|
|
41
41
|
|
|
42
|
+
## Prepared-State Portable Handoff Intake
|
|
43
|
+
|
|
44
|
+
Treat a portable prepared-state checkpoint as a resume request, not as a new
|
|
45
|
+
orchestration intake. Before delegation:
|
|
46
|
+
|
|
47
|
+
1. Validate the portable envelope through the published, workspace-explicit
|
|
48
|
+
orchestration authority. Require the repository, workspace, branch lineage,
|
|
49
|
+
issue, feature folder, work mode, scheduler context, capabilities, and exact
|
|
50
|
+
next transition to match the current checkout. Supply the complete
|
|
51
|
+
caller-controlled independent expected context on every call to
|
|
52
|
+
`resolve_orchestration_topology`, `resolve_provider_routing`, and
|
|
53
|
+
`transition_prepared_orchestration`: `expected_repository_id`,
|
|
54
|
+
`expected_workspace_root`, `expected_branch`, `expected_source_head_sha`,
|
|
55
|
+
`allowed_head_relationship`, `expected_issue_number`,
|
|
56
|
+
`expected_feature_folder`, `expected_work_mode`, `expected_plan_path`, and
|
|
57
|
+
`expected_plan_sha256`. Derive each value from the destination checkout and
|
|
58
|
+
the caller's own record. A call that omits a value, or that copies one from
|
|
59
|
+
the envelope under validation, is rejected before any service invocation.
|
|
60
|
+
2. Prove the plan from the independently expected repository-relative path and
|
|
61
|
+
raw-byte SHA-256 the caller supplied in `expected_plan_path` and
|
|
62
|
+
`expected_plan_sha256`. Do not search for another plan, rewrite the path,
|
|
63
|
+
follow a symlink outside the repository, substitute a newer plan, or read
|
|
64
|
+
the plan identity from the envelope.
|
|
65
|
+
3. Use `transition_prepared_orchestration` as the only authority permitted to
|
|
66
|
+
materialize the provider-native destination checkpoint. A dry run may
|
|
67
|
+
validate and project the destination state but must not archive or replace
|
|
68
|
+
checkpoint bytes.
|
|
69
|
+
4. Preserve the ordered completed phases and reject any request to replay one
|
|
70
|
+
of them. Do not rerun promotion, research, feature-document authoring,
|
|
71
|
+
atomic planning, or preflight when the envelope records them as complete.
|
|
72
|
+
5. Preserve source-provider model, profile, topology, launch, and receipt data
|
|
73
|
+
as opaque historical evidence. Resolve Codex topology, model routing, and
|
|
74
|
+
launch evidence only for the first new destination delegation after
|
|
75
|
+
materialization; never synthesize Codex receipts for source-provider work.
|
|
76
|
+
|
|
77
|
+
For an ordinary handoff whose recorded next transition is `atomic_execution`,
|
|
78
|
+
resume the exact approved plan through the ordinary Codex atomic-executor path
|
|
79
|
+
at `atomic_execution`. Do not invoke a parent scheduler or rediscover lifecycle
|
|
80
|
+
work. Any contract, authority, plan, replay, or materialization failure returns
|
|
81
|
+
the deterministic blocked result before destination delegation.
|
|
82
|
+
|
|
42
83
|
## Epic Entry Boundary
|
|
43
84
|
|
|
44
85
|
This standalone workflow must not invoke `epic-planner` or `epic-orchestrator`. If intake names
|
package/resources/codex-and-agents-customizations/.agents/skills/orchestrator-state/SKILL.md
CHANGED
|
@@ -17,6 +17,47 @@ A hardened snapshot from another repository contains a JSON Schema for the orche
|
|
|
17
17
|
|
|
18
18
|
This prohibition is specific to the disqualified foreign schema identified by the `drmoisan.github.io/mix-calculator/` `$id`. A schema whose `$id` is repo-local and whose required-field set and `additionalProperties` policy match this repository's checkpoint contract is not the disqualified foreign artifact; even so, the repository's enforcement mechanism remains the Python validator prose-and-logic above, not an imported schema file.
|
|
19
19
|
|
|
20
|
+
## Portable Handoff Projection Invariants
|
|
21
|
+
|
|
22
|
+
A provider-native destination checkpoint materialized from the portable handoff
|
|
23
|
+
contract must retain the following linkage:
|
|
24
|
+
|
|
25
|
+
- `provider`, `checkpoint_expression`, and `destination_projector` identify the
|
|
26
|
+
destination expression and selected adapter.
|
|
27
|
+
- `plan-path` and `next_step` equal the portable envelope's exact plan path and
|
|
28
|
+
recorded lifecycle transition. The destination must not rediscover a plan or
|
|
29
|
+
replace either value from local convention.
|
|
30
|
+
- `portable_handoff` retains the handoff ID, envelope SHA-256, latest history
|
|
31
|
+
entry SHA-256, adapter identity, source validator, identity and binding
|
|
32
|
+
fields, source checkpoint/archive facts, exact plan proof, lifecycle,
|
|
33
|
+
capabilities, and scheduler context.
|
|
34
|
+
- Historical source receipts remain opaque references under
|
|
35
|
+
`portable_handoff.source.expression.historical_receipts`. They must not be
|
|
36
|
+
rewritten as destination-provider receipts.
|
|
37
|
+
- `destination_evidence` starts as `pending_first_delegation` with an empty
|
|
38
|
+
receipt list. Destination routing, topology, model, and receipts may be
|
|
39
|
+
recorded only for the first new delegation after checkpoint materialization.
|
|
40
|
+
|
|
41
|
+
The portable lifecycle permits only the registered state transitions:
|
|
42
|
+
`legacy_v1` to migration, `preparation_complete` to
|
|
43
|
+
`prepared_to_atomic_execution`, `validated` to destination materialization,
|
|
44
|
+
`materialized` to `atomic_execution`, and a bounded scheduled-child return from
|
|
45
|
+
an authorized child execution phase. An attempted replay of a completed phase
|
|
46
|
+
is invalid.
|
|
47
|
+
|
|
48
|
+
A failed contract, binding, capability, authority, plan, replay, dirty-worktree,
|
|
49
|
+
candidate, archive, or replacement check must produce the deterministic blocked
|
|
50
|
+
result and leave the source checkpoint authoritative. A blocked result must
|
|
51
|
+
retain the primary `HANDOFF_*` code and affected paths where applicable; it must
|
|
52
|
+
not record a completed transition or destination delegation.
|
|
53
|
+
|
|
54
|
+
For parallel and epic children, `return_to_scheduler` accepts only a result
|
|
55
|
+
whose run, item, parent checkpoint path/hash, scheduler owner, child execution
|
|
56
|
+
owner, return contract, plan hash, child checkpoint hash, and result hash match
|
|
57
|
+
the envelope. The child may return that bounded result but may not assume
|
|
58
|
+
cohort or wave ordering, barriers, fan-in, integration, cleanup, or parent
|
|
59
|
+
completion authority.
|
|
60
|
+
|
|
20
61
|
## Scope and Backward Compatibility
|
|
21
62
|
|
|
22
63
|
These invariants apply only when the checkpoint contains a top-level `remediation_loop` with a `cycles` array. A checkpoint with no `remediation_loop` (the existing step-based checkpoint shape) is unaffected: it validates exactly as before and produces no new errors. The invariants are additive.
|
package/resources/codex-and-agents-customizations/.agents/skills/repo-automation-adapter/SKILL.md
CHANGED
|
@@ -50,6 +50,32 @@ Use these semantic MCP tools when the corresponding operation is required:
|
|
|
50
50
|
- `run_poshqc_analyze`
|
|
51
51
|
- `run_poshqc_analyze_autofix`
|
|
52
52
|
- `run_poshqc_test`
|
|
53
|
+
- `resolve_orchestration_topology`
|
|
54
|
+
- `resolve_provider_routing`
|
|
55
|
+
- `transition_prepared_orchestration`
|
|
56
|
+
|
|
57
|
+
## Independent Expected Context for Portable Handoff Tools
|
|
58
|
+
|
|
59
|
+
The three portable prepared-orchestration tools bind a handoff to one specific
|
|
60
|
+
destination checkout. Every call to `resolve_orchestration_topology`,
|
|
61
|
+
`resolve_provider_routing`, and `transition_prepared_orchestration` must carry
|
|
62
|
+
the complete caller-controlled independent expected context:
|
|
63
|
+
|
|
64
|
+
- `expected_repository_id`
|
|
65
|
+
- `expected_workspace_root`
|
|
66
|
+
- `expected_branch`
|
|
67
|
+
- `expected_source_head_sha`
|
|
68
|
+
- `allowed_head_relationship`
|
|
69
|
+
- `expected_issue_number`
|
|
70
|
+
- `expected_feature_folder`
|
|
71
|
+
- `expected_work_mode`
|
|
72
|
+
- `expected_plan_path`
|
|
73
|
+
- `expected_plan_sha256`
|
|
74
|
+
|
|
75
|
+
Derive each value from the destination checkout and the caller's own record.
|
|
76
|
+
The envelope under validation is never the source of the values it is
|
|
77
|
+
validated against. A call that omits a value, or that copies one from the
|
|
78
|
+
envelope, is rejected before the service is invoked.
|
|
53
79
|
|
|
54
80
|
Legacy VS Code command IDs are historical source material only and must not be invoked:
|
|
55
81
|
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
[CmdletBinding()]
|
|
6
6
|
param()
|
|
7
7
|
|
|
8
|
+
# Shared command-line parser (issue #545). The scope filter and the PR-number resolver below
|
|
9
|
+
# both run against the segment that structurally invokes `gh pr merge`, which is what keeps
|
|
10
|
+
# the two runtimes on one implementation of the same concern.
|
|
11
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
12
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
13
|
+
|
|
8
14
|
function ConvertFrom-CodexMergeJson {
|
|
9
15
|
[CmdletBinding()]
|
|
10
16
|
param([AllowNull()][AllowEmptyString()][string] $Raw, [Parameter(Mandatory)][string] $Name, [switch] $Optional)
|
|
@@ -26,13 +32,37 @@ function ConvertFrom-CodexMergeJson {
|
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
function Get-CodexMergeCommandPrNumber {
|
|
35
|
+
<#
|
|
36
|
+
.SYNOPSIS
|
|
37
|
+
Resolve the explicit pull-request number of a gh pr merge invocation, or $null.
|
|
38
|
+
.DESCRIPTION
|
|
39
|
+
The number is taken from the segment that structurally invokes `gh pr merge` and
|
|
40
|
+
from nowhere else: the positional spelling through Get-CommandLineOperand, the
|
|
41
|
+
flag-led and equals-joined spellings through Get-CommandLineFlagValue. A leading
|
|
42
|
+
`cd <path>` segment contributes no operand and no flag value, so no digit run
|
|
43
|
+
outside the merge segment can be mistaken for a pull-request number.
|
|
44
|
+
|
|
45
|
+
This copy has never carried the Claude copy's unanchored whole-text digit scan and
|
|
46
|
+
deliberately does not acquire one. No regular expression matching a bare digit run
|
|
47
|
+
is introduced here; the only pattern below anchors an all-digit token end to end.
|
|
48
|
+
.OUTPUTS
|
|
49
|
+
System.Nullable[int]
|
|
50
|
+
#>
|
|
29
51
|
[CmdletBinding()]
|
|
30
52
|
[OutputType([int])]
|
|
31
53
|
param([Parameter(Mandatory)][string] $Command)
|
|
32
54
|
|
|
33
|
-
|
|
34
|
-
|
|
55
|
+
foreach ($operand in @(Get-CommandLineOperand -CommandText $Command -CommandWord 'gh' -SubcommandPath @('pr', 'merge'))) {
|
|
56
|
+
if ($operand -match '^\d+$') {
|
|
57
|
+
return [int]$operand
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
$flagValue = Get-CommandLineFlagValue -CommandText $Command -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
|
|
62
|
+
if ($null -ne $flagValue -and $flagValue -match '^\d+$') {
|
|
63
|
+
return [int]$flagValue
|
|
35
64
|
}
|
|
65
|
+
|
|
36
66
|
return $null
|
|
37
67
|
}
|
|
38
68
|
|
|
@@ -95,7 +125,23 @@ function Invoke-CodexEpicMergeDecision {
|
|
|
95
125
|
return $null
|
|
96
126
|
}
|
|
97
127
|
$command = [string]$payload.tool_input.command
|
|
98
|
-
|
|
128
|
+
# Both legs are read structurally from the segment that invokes the command, so a
|
|
129
|
+
# quoted mention of the phrase no longer brings a command into scope. The flag leg needs
|
|
130
|
+
# a raw-scan fallback: a wrapper's quoted argument collapses into ONE token, so a flag
|
|
131
|
+
# inside it is never read as a token, and the token-only read took
|
|
132
|
+
# bash -c "gh pr merge --merge 688" out of scope.
|
|
133
|
+
$isMergeInvocation = Test-CommandLineInvocation -CommandText $command -CommandWord 'gh' -SubcommandPath @('pr', 'merge')
|
|
134
|
+
$hasMergeFlag = Test-CommandLineFlag -CommandText $command -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
|
|
135
|
+
if (-not $hasMergeFlag) {
|
|
136
|
+
foreach ($segment in @(Read-CommandLineSegment -CommandText $command)) {
|
|
137
|
+
if ((Test-CommandLineSegmentRawScan -Segment $segment) -and
|
|
138
|
+
$segment.ScanText.IndexOf('--merge', [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
|
|
139
|
+
$hasMergeFlag = $true
|
|
140
|
+
break
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if (-not $isMergeInvocation -or -not $hasMergeFlag) {
|
|
99
145
|
return $null
|
|
100
146
|
}
|
|
101
147
|
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1
CHANGED
|
@@ -16,8 +16,13 @@ $script:AllowedPreparationMcpTools = @(
|
|
|
16
16
|
'mcp__drm-copilot__potential_to_issue',
|
|
17
17
|
'mcp__drm-copilot__new_active_feature_folder',
|
|
18
18
|
'mcp__drm-copilot__resolve_atomic_plan_prompt',
|
|
19
|
-
'mcp__drm-copilot__resolve_execute_hard_lock_prompt'
|
|
20
|
-
|
|
19
|
+
'mcp__drm-copilot__resolve_execute_hard_lock_prompt'
|
|
20
|
+
)
|
|
21
|
+
$script:PreparationSemanticMcpIds = @(
|
|
22
|
+
'drm-copilot.validate_orchestration_artifacts',
|
|
23
|
+
'drm-copilot.resolve_orchestration_topology',
|
|
24
|
+
'drm-copilot.resolve_provider_routing',
|
|
25
|
+
'drm-copilot.transition_prepared_orchestration'
|
|
21
26
|
)
|
|
22
27
|
|
|
23
28
|
function ConvertFrom-EpicPlanningJson {
|
|
@@ -41,6 +46,47 @@ function ConvertFrom-EpicPlanningJson {
|
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
function Get-EpicPlanningRegisteredMcpTool {
|
|
50
|
+
[CmdletBinding()]
|
|
51
|
+
[OutputType([string[]])]
|
|
52
|
+
param(
|
|
53
|
+
[Parameter(Mandatory)][string] $RegistryPath,
|
|
54
|
+
[Parameter(Mandatory)][string[]] $SemanticIds
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
if (-not (Test-Path -LiteralPath $RegistryPath -PathType Leaf)) {
|
|
58
|
+
throw "EPIC_PLANNING_ONLY_BLOCKED: semantic MCP registry '$RegistryPath' does not exist."
|
|
59
|
+
}
|
|
60
|
+
$registry = ConvertFrom-EpicPlanningJson `
|
|
61
|
+
-Raw (Get-Content -Raw -LiteralPath $RegistryPath) `
|
|
62
|
+
-Name 'semantic MCP registry'
|
|
63
|
+
$registered = [System.Collections.Generic.List[string]]::new()
|
|
64
|
+
foreach ($semanticId in $SemanticIds) {
|
|
65
|
+
$property = $registry.semantic_tools.PSObject.Properties[$semanticId]
|
|
66
|
+
if ($null -eq $property) {
|
|
67
|
+
throw "EPIC_PLANNING_ONLY_BLOCKED: semantic MCP id '$semanticId' is not registered."
|
|
68
|
+
}
|
|
69
|
+
$entry = $property.Value
|
|
70
|
+
$operation = $semanticId.Substring($semanticId.LastIndexOf('.') + 1)
|
|
71
|
+
if ([string]$entry.operation -ne $operation) {
|
|
72
|
+
throw "EPIC_PLANNING_ONLY_BLOCKED: semantic MCP id '$semanticId' has an invalid operation."
|
|
73
|
+
}
|
|
74
|
+
foreach ($alias in @($entry.transport_aliases)) {
|
|
75
|
+
$escapedOperation = [regex]::Escape($operation)
|
|
76
|
+
if ([string]$alias -notmatch "^mcp__(?:drm-copilot|drm_copilot)__$escapedOperation`$") {
|
|
77
|
+
throw "EPIC_PLANNING_ONLY_BLOCKED: semantic MCP id '$semanticId' has an invalid transport alias."
|
|
78
|
+
}
|
|
79
|
+
$registered.Add([string]$alias)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return [string[]]($registered | Sort-Object -Unique)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
$script:EpicPlanningRepositoryRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
|
|
86
|
+
$script:AllowedPreparationSemanticMcpTools = @(Get-EpicPlanningRegisteredMcpTool `
|
|
87
|
+
-RegistryPath (Join-Path $script:EpicPlanningRepositoryRoot 'config/orchestration-handoff-registry.json') `
|
|
88
|
+
-SemanticIds $script:PreparationSemanticMcpIds)
|
|
89
|
+
|
|
44
90
|
function Get-EpicPlanningDenyDecision {
|
|
45
91
|
[CmdletBinding()]
|
|
46
92
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -223,11 +269,9 @@ function Invoke-EpicPlanningOnlyDecision {
|
|
|
223
269
|
return Get-EpicPlanningDenyDecision -Reason 'the Bash command is outside the preparation read, branch, commit, push, or validator allowlist.'
|
|
224
270
|
}
|
|
225
271
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if ($toolName -like 'mcp__drm-copilot__*') {
|
|
272
|
+
$allowedRepositoryMcp = $script:AllowedPreparationMcpTools -contains $toolName
|
|
273
|
+
$allowedSemanticMcp = $script:AllowedPreparationSemanticMcpTools -contains $toolName
|
|
274
|
+
if ($allowedRepositoryMcp -or $allowedSemanticMcp) {
|
|
231
275
|
if ($attestedPreparation) {
|
|
232
276
|
$workspaceRoot = [string]$payload.tool_input.workspace_root
|
|
233
277
|
if ([string]::IsNullOrWhiteSpace($workspaceRoot) -or
|
|
@@ -236,10 +280,11 @@ function Invoke-EpicPlanningOnlyDecision {
|
|
|
236
280
|
return Get-EpicPlanningDenyDecision -Reason 'preparation MCP calls require workspace_root equal to the attested session cwd.'
|
|
237
281
|
}
|
|
238
282
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
283
|
+
return $null
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if ($toolName -like 'mcp__*') {
|
|
287
|
+
return Get-EpicPlanningDenyDecision -Reason "MCP tool '$toolName' is outside the registered preparation lifecycle and semantic allowlist."
|
|
243
288
|
}
|
|
244
289
|
|
|
245
290
|
return Get-EpicPlanningDenyDecision -Reason "tool '$toolName' is not classified for preparation mode."
|
|
@@ -273,7 +318,7 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
273
318
|
|
|
274
319
|
try {
|
|
275
320
|
$payloadRaw = [Console]::In.ReadToEnd()
|
|
276
|
-
$repositoryRoot =
|
|
321
|
+
$repositoryRoot = $script:EpicPlanningRepositoryRoot
|
|
277
322
|
$checkpointPath = Join-Path $repositoryRoot 'artifacts/orchestration/orchestrator-state.json'
|
|
278
323
|
$checkpointRaw = if (Test-Path -LiteralPath $checkpointPath -PathType Leaf) {
|
|
279
324
|
Get-Content -Raw -LiteralPath $checkpointPath
|
|
@@ -5,6 +5,12 @@
|
|
|
5
5
|
[CmdletBinding()]
|
|
6
6
|
param()
|
|
7
7
|
|
|
8
|
+
# Shared command-line parser (issue #545). The scope filter and the operand extractor below
|
|
9
|
+
# both run against the segment that structurally invokes `git worktree remove`, which is what
|
|
10
|
+
# keeps the two runtimes on one implementation of the same concern.
|
|
11
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
12
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
13
|
+
|
|
8
14
|
$script:SafeWorktreeStatuses = @('merged', 'worktree_removed')
|
|
9
15
|
|
|
10
16
|
function ConvertFrom-CodexWorktreeJson {
|
|
@@ -28,18 +34,35 @@ function ConvertFrom-CodexWorktreeJson {
|
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
function Get-CodexWorktreeRemovalPath {
|
|
37
|
+
<#
|
|
38
|
+
.SYNOPSIS
|
|
39
|
+
Extract the target worktree path from a git worktree remove command.
|
|
40
|
+
.DESCRIPTION
|
|
41
|
+
The operand comes from the segment that structurally invokes git worktree remove, so
|
|
42
|
+
a chained 'cd <path> &&' segment contributes nothing and a quoted mention of the
|
|
43
|
+
phrase resolves to no operand. The tokenizer strips balanced double and single
|
|
44
|
+
quotes, which is what the previous pattern's `double` and `single` alternatives did.
|
|
45
|
+
|
|
46
|
+
The previous pattern accepted '--force' only immediately after 'remove'. That
|
|
47
|
+
spelling is preserved and the trailing spelling now works too, because '--force' is a
|
|
48
|
+
zero-argument flag that never contributes an operand wherever it is written. Its
|
|
49
|
+
presence is read structurally through Test-CommandLineFlag rather than by a raw-text
|
|
50
|
+
search. The empty-string-on-miss contract is unchanged: callers test `if ($target)`.
|
|
51
|
+
.OUTPUTS
|
|
52
|
+
System.String
|
|
53
|
+
#>
|
|
31
54
|
[CmdletBinding()]
|
|
32
55
|
[OutputType([string])]
|
|
33
56
|
param([Parameter(Mandatory)][AllowEmptyString()][string] $Command)
|
|
34
57
|
|
|
35
|
-
$
|
|
36
|
-
|
|
37
|
-
|
|
58
|
+
$hasForce = Test-CommandLineFlag -CommandText $Command -CommandWord 'git' -SubcommandPath @('worktree', 'remove') -FlagName '--force'
|
|
59
|
+
$operands = @(Get-CommandLineOperand -CommandText $Command -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))
|
|
60
|
+
|
|
61
|
+
if ($operands.Count -gt 0) {
|
|
62
|
+
return [string]$operands[0]
|
|
38
63
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
return [string]$Matches[$name]
|
|
42
|
-
}
|
|
64
|
+
if ($hasForce) {
|
|
65
|
+
return '--force'
|
|
43
66
|
}
|
|
44
67
|
return ''
|
|
45
68
|
}
|
|
@@ -97,7 +120,10 @@ function Invoke-CodexWorktreeRemovalDecision {
|
|
|
97
120
|
return $null
|
|
98
121
|
}
|
|
99
122
|
$command = [string]$payload.tool_input.command
|
|
100
|
-
|
|
123
|
+
# Scope filter. Structural, so a relocating spelling such as
|
|
124
|
+
# 'git -C <dir> worktree remove <path>' is in scope and quoted prose that merely mentions
|
|
125
|
+
# the phrase is not (issue #545).
|
|
126
|
+
if (-not (Test-CommandLineInvocation -CommandText $command -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))) {
|
|
101
127
|
return $null
|
|
102
128
|
}
|
|
103
129
|
$target = Get-CodexWorktreeRemovalPath -Command $command
|
|
@@ -20,6 +20,9 @@ param()
|
|
|
20
20
|
# normative contract and which lacks headroom; leaving that file byte-untouched is the
|
|
21
21
|
# proof the issue #539 exemption is behaviourally unchanged.
|
|
22
22
|
. (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-modes.ps1')
|
|
23
|
+
# Shared command-line parser (issue #545): per-segment scan text and structural matching.
|
|
24
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
25
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
23
26
|
|
|
24
27
|
# The readiness checkpoint this gate reads and names in its block message.
|
|
25
28
|
$script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
@@ -151,9 +154,11 @@ function Test-ImplementationCommand {
|
|
|
151
154
|
'(^|\s)npx\s+(prettier|eslint|tsc|jest)\b',
|
|
152
155
|
'(^|\s)pwsh\s+.*(Invoke-Pester|tests/scripts/)'
|
|
153
156
|
)
|
|
157
|
+
$scanText = @(Read-CommandLineSegment -CommandText $normalizedCommand).ScanText
|
|
158
|
+
$isStaging = @('add', 'commit').Where({ Test-CommandLineInvocation -CommandText $normalizedCommand -CommandWord 'git' -SubcommandPath @($_) }).Count -gt 0
|
|
154
159
|
|
|
155
160
|
for ($index = 0; $index -lt $implementationCommandPatterns.Count; $index++) {
|
|
156
|
-
if ($
|
|
161
|
+
if (-not (($scanText | Where-Object { $_ -match $implementationCommandPatterns[$index] }) -or ($index -eq 0 -and $isStaging))) {
|
|
157
162
|
continue
|
|
158
163
|
}
|
|
159
164
|
# Allow-side only (issue #539). Index 0 is the git staging trigger, whose pattern
|
package/resources/codex-and-agents-customizations/.codex/hooks/enforce-promotion-mcp-only.ps1
CHANGED
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
[CmdletBinding()]
|
|
30
30
|
param()
|
|
31
31
|
|
|
32
|
+
# Shared command-line parser (issue #545). Every forbidden-token scan and both `gh`
|
|
33
|
+
# expressions below run against each segment's clause-ordered scan text, and the
|
|
34
|
+
# structural matcher reaches the relocating `gh` spellings adjacency cannot.
|
|
35
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
36
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
37
|
+
|
|
32
38
|
$script:PromotionMcpOnlyBlockedReason = 'PROMOTION_MCP_ONLY_BLOCKED: Direct Bash promotion-script execution is not allowed in agent sessions. Use the drm-copilot MCP promotion tools instead.'
|
|
33
39
|
|
|
34
40
|
$script:PromotionMcpOnlyGhIssueBlockedReason = 'PROMOTION_MCP_ONLY_BLOCKED: Direct GitHub issue creation via `gh` bypasses the approved drm-copilot MCP promotion path (`mcp__drm-copilot__new_potential_entry` -> `mcp__drm-copilot__potential_to_issue` -> `mcp__drm-copilot__new_active_feature_folder`). Use those MCP tools instead.'
|
|
@@ -88,15 +94,34 @@ function Get-PromotionBypassReason {
|
|
|
88
94
|
'new_active_feature_folder'
|
|
89
95
|
)
|
|
90
96
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
97
|
+
# Issue #545: the scan text of a segment is its raw text only when the segment is
|
|
98
|
+
# wrapper-led, carries a live substitution, or could not be balanced; otherwise
|
|
99
|
+
# quoted spans and attached heredoc bodies are masked. A promotion tool name that
|
|
100
|
+
# appears only as a JSON receipt value is therefore a mention, not an invocation.
|
|
101
|
+
$segments = @(Read-CommandLineSegment -CommandText $CommandText)
|
|
102
|
+
|
|
103
|
+
foreach ($segment in $segments) {
|
|
104
|
+
foreach ($token in $forbiddenTokens) {
|
|
105
|
+
if ($segment.ScanText.IndexOf($token, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
|
|
106
|
+
return (Get-PromotionMcpOnlyBlockedReason)
|
|
107
|
+
}
|
|
94
108
|
}
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
# `gh issue create` and `gh issue new` are direct bypasses of the MCP
|
|
98
112
|
# promotion path. Tolerate any flags after the subcommand.
|
|
99
|
-
|
|
113
|
+
foreach ($segment in $segments) {
|
|
114
|
+
if ($segment.ScanText -match '(?i)\bgh\s+issue\s+(?:create|new)\b') {
|
|
115
|
+
return (Get-PromotionMcpOnlyGhIssueBlockedReason)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
# The adjacency-requiring expression above cannot see a relocating spelling, in
|
|
120
|
+
# which a `gh` global option such as --repo or -R sits between the command word
|
|
121
|
+
# and its subcommand (D10). The structural matcher absorbs those options and
|
|
122
|
+
# reads the subcommand path positionally, so both spellings classify.
|
|
123
|
+
if ((Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('issue', 'create')) -or
|
|
124
|
+
(Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('issue', 'new'))) {
|
|
100
125
|
return (Get-PromotionMcpOnlyGhIssueBlockedReason)
|
|
101
126
|
}
|
|
102
127
|
|
|
@@ -104,10 +129,12 @@ function Get-PromotionBypassReason {
|
|
|
104
129
|
# explicit POST method is supplied. `gh api` defaults to GET, so we only
|
|
105
130
|
# block when -X POST or --method POST is present, to avoid false positives
|
|
106
131
|
# on issue read operations. Use a single regex with lookaheads against the
|
|
107
|
-
#
|
|
132
|
+
# scan text of one segment.
|
|
108
133
|
$ghApiIssuesPostPattern = '(?i)(?=.*\bgh\s+api\b)(?=.*repos/[^/\s]+/[^/\s]+/issues(?:\b|/[^/\s]*$))(?=.*(?:-X\s+POST|--method\s+POST))'
|
|
109
|
-
|
|
110
|
-
|
|
134
|
+
foreach ($segment in $segments) {
|
|
135
|
+
if ($segment.ScanText -match $ghApiIssuesPostPattern) {
|
|
136
|
+
return (Get-PromotionMcpOnlyGhIssueBlockedReason)
|
|
137
|
+
}
|
|
111
138
|
}
|
|
112
139
|
|
|
113
140
|
return $null
|