@danmoisan/drm-copilot-mcp 1.1.10 → 1.1.12
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 +3517 -1104
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -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/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1 +443 -0
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +82 -51
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +52 -3
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +120 -5
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +127 -0
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +21 -28
- 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 +124 -8
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +171 -15
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +50 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +3 -1
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner-helpers.ps1 +332 -0
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +211 -182
- 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 +218 -6
- 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/codex-routing/CodexDeployment.psm1 +1 -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/lib/worktree-resolution/EpicScopeReadiness.psm1 +176 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/EpicScopeResolution.psm1 +369 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeItemResolution.psm1 +392 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeResolution.psm1 +500 -0
- package/resources/claude-customizations/.claude/lib/worktree-resolution/WorktreeTargetResolution.psm1 +347 -0
- package/resources/claude-customizations/.claude/rules/orchestrator-state.md +27 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +66 -2
- package/resources/claude-customizations/.claude/skills/cleanup-merged-worktrees/SKILL.md +323 -16
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +14 -1
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +4 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +46 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +3 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +75 -8
- 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/.claude/skills/pr-author/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
- package/resources/claude-customizations/config/blast-radius.json +7 -0
- package/resources/claude-customizations/pack-manifests/core.json +16 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- 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 +242 -3
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +67 -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-helpers.ps1 +163 -15
- 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/.codex/scripts/Resolve-CodexDeployment.ps1 +87 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/Resolve-CodexTopology.ps1 +96 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/codex-routing-cli-common.ps1 +370 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-contract.ps1 +475 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-launch-runtime.ps1 +445 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-persistence-runtime.ps1 +118 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/epic-child-sandbox-preflight.ps1 +69 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/launch-epic-child-wave.ps1 +471 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/post-codex-worktree-session.ps1 +280 -0
- package/resources/codex-and-agents-customizations/.codex/scripts/resume-epic-child.ps1 +263 -0
- package/resources/codex-and-agents-customizations/.codex-variants/csharp-legacy/agents/csharp-typed-engineer.toml +2 -1
- package/resources/codex-and-agents-customizations/pack-manifests/core.json +23 -1
- package/resources/config/orchestration-handoff-registry.json +138 -0
- package/resources/config/orchestration-handoff.schema.json +472 -0
- package/resources/lib/codex-routing/CodexDeployment.psm1 +315 -0
- package/resources/lib/codex-routing/CodexTopology.psm1 +394 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +64 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
Invoked by the Claude Code PreToolUse hook on the "Bash" matcher before any Bash
|
|
7
7
|
command runs. Regex-matches gh pr merge with a --merge flag against
|
|
8
|
-
the envelope's tool_input.command and, when matched, allows the merge only when one of
|
|
8
|
+
the envelope's tool_input.command and, when matched, allows the merge only when one of four
|
|
9
9
|
checkpoint-only conditions holds:
|
|
10
10
|
|
|
11
11
|
1. Child-feature path: artifacts/orchestration/orchestrator-state.json exists,
|
|
@@ -19,12 +19,19 @@
|
|
|
19
19
|
entry whose merge_status == "ci_green". A parallel run always names an explicit PR
|
|
20
20
|
number (each item merges from its own isolated worktree), so a bare command with no
|
|
21
21
|
PR number cannot satisfy this branch.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
4. Standalone path (issue #670), evaluated last: the command names an explicit PR
|
|
23
|
+
number and one of the three checkpoints above carries a
|
|
24
|
+
standalone_merge_authorizations record whose pr_number equals it, whose fields are
|
|
25
|
+
well formed, and whose session_id equals the live envelope's session_id. The
|
|
26
|
+
dot-sourced helpers file implements this branch.
|
|
27
|
+
|
|
28
|
+
Otherwise the command is denied with reason EPIC_MERGE_GATE_BLOCKED; a denial from
|
|
29
|
+
condition 4 also carries a standalone reason code after that token. A missing or
|
|
30
|
+
unreadable checkpoint in any branch fails closed (denies). A standalone (non-epic,
|
|
31
|
+
non-parallel) run never satisfies conditions 1-3, so its only route to a merge is an
|
|
32
|
+
explicit, PR-specific, session-bound record under condition 4; a bare command with no
|
|
33
|
+
PR number never satisfies condition 4, and a blanket flag is rejected rather than read.
|
|
34
|
+
Trigger scope: --squash is out of scope for this gate and is allowed on both runtimes.
|
|
28
35
|
|
|
29
36
|
Design decision: this gate trusts the on-disk checkpoint rather than shelling out live
|
|
30
37
|
to gh pr view for a real-time head-SHA check, matching the same non-adversarial,
|
|
@@ -35,11 +42,23 @@
|
|
|
35
42
|
Compatible with PowerShell 7+. No external module dependencies. Filesystem reads go
|
|
36
43
|
through injectable wrapper functions so tests can mock the boundary without writing
|
|
37
44
|
temporary files.
|
|
45
|
+
|
|
46
|
+
Honest disclosure: the standalone-merge authorization record is a policy-level,
|
|
47
|
+
auditable declaration and is not a cryptographic or security control. authorized_by is
|
|
48
|
+
a declaration the hook does not verify. The record is not tamper-proof against a writer
|
|
49
|
+
inside the authorizing session. The session_id cross-check rejects stale or copied
|
|
50
|
+
records from other sessions; it does not stop same-session forgery.
|
|
38
51
|
#>
|
|
39
52
|
[CmdletBinding()]
|
|
40
53
|
param()
|
|
41
54
|
|
|
42
55
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
56
|
+
# Shared command-line parser (issue #545), consumed by the scope filter in
|
|
57
|
+
# Invoke-EpicMergeGateDecision and by Get-EpicMergeGateCommandPrNumber.
|
|
58
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
59
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
60
|
+
# Standalone-merge authorization predicates and the two decision-envelope factories (issue #670).
|
|
61
|
+
. (Join-Path $PSScriptRoot 'enforce-epic-merge-gate-authorization.ps1')
|
|
43
62
|
|
|
44
63
|
$script:ChildCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
45
64
|
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
@@ -128,6 +147,22 @@ function Get-EpicMergeGateCommandPrNumber {
|
|
|
128
147
|
<#
|
|
129
148
|
.SYNOPSIS
|
|
130
149
|
Extract an explicit PR number argument from a gh pr merge command, or $null.
|
|
150
|
+
.DESCRIPTION
|
|
151
|
+
Both spellings are read from the segment that structurally invokes gh pr merge,
|
|
152
|
+
never from the whole command line. The positional form ("gh pr merge 410 --merge")
|
|
153
|
+
resolves through Get-CommandLineOperand and takes the first all-digit operand; the
|
|
154
|
+
flag-led forms ("gh pr merge --merge 410" and "gh pr merge --merge=410") resolve
|
|
155
|
+
through Get-CommandLineFlagValue. A bare "gh pr merge --merge" yields $null, which
|
|
156
|
+
the parallel branch treats as fail-closed.
|
|
157
|
+
|
|
158
|
+
The deleted unanchored branch scanned the WHOLE command text for the first run of
|
|
159
|
+
digits once "gh pr merge" appeared anywhere in it, and that failed in both
|
|
160
|
+
directions. Fail-closed: a leading "cd <path>" whose path carries a timestamp
|
|
161
|
+
component supplied a digit run that was not a pull request number, so an authorized
|
|
162
|
+
merge was blocked. False-allow: with authorized item 501 and unauthorized item 777,
|
|
163
|
+
"cd /repo/worktrees/501 && gh pr merge --merge 777" extracted 501, matched the
|
|
164
|
+
authorized item, and permitted the merge of PR 777. Taking the number from the
|
|
165
|
+
matched segment's own operand or flag value closes both directions.
|
|
131
166
|
.PARAMETER CommandText
|
|
132
167
|
The Bash command text under evaluation.
|
|
133
168
|
.OUTPUTS
|
|
@@ -140,20 +175,17 @@ function Get-EpicMergeGateCommandPrNumber {
|
|
|
140
175
|
[string] $CommandText
|
|
141
176
|
)
|
|
142
177
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return [int]$Matches[1]
|
|
178
|
+
foreach ($operand in @(Get-CommandLineOperand -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge'))) {
|
|
179
|
+
if ($operand -match '^\d+$') {
|
|
180
|
+
return [int]$operand
|
|
181
|
+
}
|
|
148
182
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
# and a bare "gh pr merge --merge" still yields $null.
|
|
154
|
-
if ($CommandText -match '(?i)\bgh\s+pr\s+merge\b' -and $CommandText -match '(?<![-\w])(\d+)\b') {
|
|
155
|
-
return [int]$Matches[1]
|
|
183
|
+
|
|
184
|
+
$flagValue = Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
|
|
185
|
+
if ($null -ne $flagValue -and $flagValue -match '^\d+$') {
|
|
186
|
+
return [int]$flagValue
|
|
156
187
|
}
|
|
188
|
+
|
|
157
189
|
return $null
|
|
158
190
|
}
|
|
159
191
|
|
|
@@ -307,36 +339,6 @@ function Test-ParallelCheckpointAllowsMerge {
|
|
|
307
339
|
return $false
|
|
308
340
|
}
|
|
309
341
|
|
|
310
|
-
function Get-EpicMergeGateAllowDecision {
|
|
311
|
-
[CmdletBinding()]
|
|
312
|
-
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
313
|
-
param()
|
|
314
|
-
|
|
315
|
-
return [ordered]@{
|
|
316
|
-
hookSpecificOutput = [ordered]@{
|
|
317
|
-
hookEventName = 'PreToolUse'
|
|
318
|
-
permissionDecision = 'allow'
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function Get-EpicMergeGateBlockDecision {
|
|
324
|
-
[CmdletBinding()]
|
|
325
|
-
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
326
|
-
param(
|
|
327
|
-
[Parameter(Mandatory)]
|
|
328
|
-
[string] $Reason
|
|
329
|
-
)
|
|
330
|
-
|
|
331
|
-
return [ordered]@{
|
|
332
|
-
hookSpecificOutput = [ordered]@{
|
|
333
|
-
hookEventName = 'PreToolUse'
|
|
334
|
-
permissionDecision = 'deny'
|
|
335
|
-
permissionDecisionReason = $Reason
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
342
|
function Invoke-EpicMergeGateDecision {
|
|
341
343
|
<#
|
|
342
344
|
.SYNOPSIS
|
|
@@ -373,8 +375,26 @@ function Invoke-EpicMergeGateDecision {
|
|
|
373
375
|
}
|
|
374
376
|
|
|
375
377
|
# Only a gh pr merge invocation carrying --merge is in scope for this gate; every
|
|
376
|
-
# other Bash command is unaffected.
|
|
377
|
-
|
|
378
|
+
# other Bash command is unaffected. Both legs are read structurally from the segment
|
|
379
|
+
# that invokes the command, so a quoted mention of the phrase and a --merge token
|
|
380
|
+
# belonging to some other segment no longer bring a command into scope.
|
|
381
|
+
#
|
|
382
|
+
# The flag leg needs a raw-scan fallback: a wrapper's quoted argument collapses into ONE
|
|
383
|
+
# token, so a flag inside it is never read as a token, and the token-only read took
|
|
384
|
+
# bash -c "gh pr merge --merge 688" out of scope. The fallback reads only the segments the
|
|
385
|
+
# scanner already scans raw, so a masked mention in a non-wrapper segment stays out.
|
|
386
|
+
$isMergeInvocation = Test-CommandLineInvocation -CommandText $commandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge')
|
|
387
|
+
$hasMergeFlag = Test-CommandLineFlag -CommandText $commandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
|
|
388
|
+
if (-not $hasMergeFlag) {
|
|
389
|
+
foreach ($segment in @(Read-CommandLineSegment -CommandText $commandText)) {
|
|
390
|
+
if ((Test-CommandLineSegmentRawScan -Segment $segment) -and
|
|
391
|
+
$segment.ScanText.IndexOf('--merge', [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
|
|
392
|
+
$hasMergeFlag = $true
|
|
393
|
+
break
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (-not $isMergeInvocation -or -not $hasMergeFlag) {
|
|
378
398
|
return Get-EpicMergeGateAllowDecision
|
|
379
399
|
}
|
|
380
400
|
|
|
@@ -395,6 +415,17 @@ function Invoke-EpicMergeGateDecision {
|
|
|
395
415
|
return Get-EpicMergeGateAllowDecision
|
|
396
416
|
}
|
|
397
417
|
|
|
418
|
+
# Branch 4, evaluated last and only for an explicit PR number: a PR-specific standalone
|
|
419
|
+
# authorization record bound to the live session. Its deny keeps the gate token first.
|
|
420
|
+
if ($null -ne $commandPrNumber) {
|
|
421
|
+
$envelopeSessionId = [string](Get-ClaudeHookEnvelopeValue -Envelope $payload.Envelope -Name 'session_id')
|
|
422
|
+
$standalone = Test-StandaloneCheckpointAllowsMerge -Checkpoints @($childCheckpoint, $epicCheckpoint, $parallelCheckpoint) -CommandPrNumber $commandPrNumber -EnvelopeSessionId $envelopeSessionId
|
|
423
|
+
if ($standalone.Allowed) {
|
|
424
|
+
return Get-EpicMergeGateAllowDecision
|
|
425
|
+
}
|
|
426
|
+
return Get-EpicMergeGateBlockDecision -Reason ('EPIC_MERGE_GATE_BLOCKED: ' + $standalone.ReasonCode + ': ' + $standalone.Message)
|
|
427
|
+
}
|
|
428
|
+
|
|
398
429
|
return Get-EpicMergeGateBlockDecision -Reason 'EPIC_MERGE_GATE_BLOCKED: gh pr merge --merge requires either a per-feature checkpoint with epic_mode == true and step9_status == "passed", an epic checkpoint with epic_merge_pr.ci_gate.conclusion == "success" and a matching pr_number, or a parallel-orchestrator checkpoint with route_id == "parallel" whose target item (matched by pr_number) has merge_status == "ci_green". No checkpoint satisfied this gate.'
|
|
399
430
|
}
|
|
400
431
|
|
package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1
CHANGED
|
@@ -60,9 +60,20 @@ param()
|
|
|
60
60
|
|
|
61
61
|
|
|
62
62
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
63
|
+
# Sanctioned-removal manifest reader (issue #635), consumed by the manifest branch in
|
|
64
|
+
# Invoke-EpicWorktreeRemovalGateDecision.
|
|
65
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeManifest.psm1') -Force
|
|
66
|
+
# Shared command-line parser (issue #545), consumed by the scope filter in
|
|
67
|
+
# Invoke-EpicWorktreeRemovalGateDecision and by Get-EpicWorktreeRemovalCommandPath.
|
|
68
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
69
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
63
70
|
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
64
71
|
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
65
72
|
$script:AllowedMergeStatuses = @('merged', 'worktree_removed')
|
|
73
|
+
# Sanctioned-removal manifest location. Recorded here beside the two checkpoint paths
|
|
74
|
+
# so every hook-read document this gate consults is named in one place; the module
|
|
75
|
+
# owns the read itself.
|
|
76
|
+
$script:CleanupWorktreeManifestPath = 'artifacts/orchestration/cleanup-worktrees-manifest.json'
|
|
66
77
|
|
|
67
78
|
function Get-EpicWorktreeGateCheckpointContent {
|
|
68
79
|
<#
|
|
@@ -132,6 +143,19 @@ function Get-EpicWorktreeRemovalCommandPath {
|
|
|
132
143
|
<#
|
|
133
144
|
.SYNOPSIS
|
|
134
145
|
Extract the target worktree path argument from a git worktree remove command.
|
|
146
|
+
.DESCRIPTION
|
|
147
|
+
The operand comes from the segment that structurally invokes git worktree remove,
|
|
148
|
+
so a 'cd <path> &&' segment chained before the removal contributes nothing and a
|
|
149
|
+
quoted mention of the phrase resolves to no operand at all. Quotes around the path
|
|
150
|
+
are already stripped by the tokenizer.
|
|
151
|
+
|
|
152
|
+
'--force' is a zero-argument flag, so it never contributes an operand and may be
|
|
153
|
+
written on either side of the target path. Its presence is read structurally through
|
|
154
|
+
Test-CommandLineFlag rather than by searching the raw text, so a '--force' spelling
|
|
155
|
+
that appears inside an unrelated quoted argument cannot change how the operand list
|
|
156
|
+
is read. When no operand resolves, a present '--force' is reported in its place, so
|
|
157
|
+
the checkpoint lookup fails closed on a value that matches no recorded worktree_path
|
|
158
|
+
- the same value the previous raw-text pattern returned for that input.
|
|
135
159
|
.PARAMETER CommandText
|
|
136
160
|
The Bash command text under evaluation.
|
|
137
161
|
.OUTPUTS
|
|
@@ -144,8 +168,14 @@ function Get-EpicWorktreeRemovalCommandPath {
|
|
|
144
168
|
[string] $CommandText
|
|
145
169
|
)
|
|
146
170
|
|
|
147
|
-
|
|
148
|
-
|
|
171
|
+
$hasForce = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove') -FlagName '--force'
|
|
172
|
+
$operands = @(Get-CommandLineOperand -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))
|
|
173
|
+
|
|
174
|
+
if ($operands.Count -gt 0) {
|
|
175
|
+
return $operands[0]
|
|
176
|
+
}
|
|
177
|
+
if ($hasForce) {
|
|
178
|
+
return '--force'
|
|
149
179
|
}
|
|
150
180
|
return $null
|
|
151
181
|
}
|
|
@@ -344,7 +374,10 @@ function Invoke-EpicWorktreeRemovalGateDecision {
|
|
|
344
374
|
return Get-EpicWorktreeGateAllowDecision
|
|
345
375
|
}
|
|
346
376
|
|
|
347
|
-
|
|
377
|
+
# Scope filter. The test is structural, so a relocating spelling such as
|
|
378
|
+
# 'git -C <dir> worktree remove <path>' is now in scope and quoted prose that merely
|
|
379
|
+
# mentions the phrase is not (issue #545).
|
|
380
|
+
if (-not (Test-CommandLineInvocation -CommandText $commandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))) {
|
|
348
381
|
return Get-EpicWorktreeGateAllowDecision
|
|
349
382
|
}
|
|
350
383
|
|
|
@@ -362,6 +395,22 @@ function Invoke-EpicWorktreeRemovalGateDecision {
|
|
|
362
395
|
return Get-EpicWorktreeGateAllowDecision
|
|
363
396
|
}
|
|
364
397
|
|
|
398
|
+
# Sanctioned-removal manifest branch (issue #635). It runs last, so an
|
|
399
|
+
# epic-checkpoint-authorized or parallel-checkpoint-authorized removal still allows
|
|
400
|
+
# at the same decision point it does today and no transcript attribution changes.
|
|
401
|
+
#
|
|
402
|
+
# The two coverage tests are PRESENCE tests, deliberately not authorization tests.
|
|
403
|
+
# A target either checkpoint records at all is excluded from this branch regardless
|
|
404
|
+
# of that record's merge_status, so a removal no checkpoint authorizes still reaches
|
|
405
|
+
# the unchanged deny below. That exclusion is the only path by which the manifest
|
|
406
|
+
# could widen what the gates protect, which is why it is required rather than
|
|
407
|
+
# advisory, and why it is evaluated before the manifest predicate.
|
|
408
|
+
if (-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $checkpoint -RecordArrayName 'features' -WorktreePath $worktreePath) -and
|
|
409
|
+
-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $parallelCheckpoint -RecordArrayName 'items' -WorktreePath $worktreePath) -and
|
|
410
|
+
(Test-CleanupWorktreeManifestAuthorizesRemoval -WorktreePath $worktreePath)) {
|
|
411
|
+
return Get-EpicWorktreeGateAllowDecision
|
|
412
|
+
}
|
|
413
|
+
|
|
365
414
|
return Get-EpicWorktreeGateBlockDecision -Reason "EPIC_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires either an epic checkpoint features[] record with merge_status in {merged, worktree_removed}, or a parallel-orchestrator checkpoint with route_id == ""parallel"" whose matching items[] record (matched by worktree_path) has merge_status in {merged, worktree_removed}. No checkpoint authorized this removal."
|
|
366
415
|
}
|
|
367
416
|
|
|
@@ -5,8 +5,15 @@
|
|
|
5
5
|
|
|
6
6
|
.DESCRIPTION
|
|
7
7
|
Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
|
|
8
|
-
tool input JSON from the
|
|
9
|
-
|
|
8
|
+
tool input JSON from the envelope's nested tool_input, resolves which item the
|
|
9
|
+
delegation is about, and reads that item's orchestrator checkpoint.
|
|
10
|
+
|
|
11
|
+
The item is identified by portable identity only (issue #673): the canonical
|
|
12
|
+
issue-number line in the delegation prompt, and a branch label. A feature-folder
|
|
13
|
+
path never selects the worktree, because a merged folder exists in every checkout
|
|
14
|
+
branched from main. A delegation the gate cannot identify is denied with a named
|
|
15
|
+
reason code rather than checked against whichever checkpoint occupies the calling
|
|
16
|
+
process's directory, which is how a sibling item's state used to produce an allow.
|
|
10
17
|
|
|
11
18
|
The hook enforces presence only: it cannot read the delegate's chosen
|
|
12
19
|
`model` (no `model` field is exposed in the tool input), so it verifies that
|
|
@@ -24,7 +31,14 @@
|
|
|
24
31
|
empty or absent tool input, and malformed tool-input JSON.
|
|
25
32
|
|
|
26
33
|
The checkpoint read goes through Get-ModelRoutingCheckpoint so tests can
|
|
27
|
-
inject a synthetic checkpoint without touching disk.
|
|
34
|
+
inject a synthetic checkpoint without touching disk. Its path is mandatory and
|
|
35
|
+
carries no default: the caller supplies the absolute path of the resolved
|
|
36
|
+
worktree's checkpoint, so the gate cannot fall back to a relative location.
|
|
37
|
+
|
|
38
|
+
The delegation-identity contract this gate depends on is stated in
|
|
39
|
+
.claude/skills/orchestrate/SKILL.md under `## Issue Number Consistency`: every
|
|
40
|
+
delegation prompt to a receipt-gated subagent type carries the canonical issue
|
|
41
|
+
number line and a `branch:` label naming the item's branch.
|
|
28
42
|
|
|
29
43
|
.NOTES
|
|
30
44
|
Compatible with PowerShell 7+. Read-only presence-gating deterrent.
|
|
@@ -34,6 +48,11 @@ param()
|
|
|
34
48
|
|
|
35
49
|
|
|
36
50
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
51
|
+
# Portable-identity resolution (issue #673). Unguarded and fail-closed on purpose: a gate
|
|
52
|
+
# that cannot load its resolver must not degrade into the cwd-relative read it replaces.
|
|
53
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force -ErrorAction Stop
|
|
54
|
+
# Epic scope (issue #663): a delegation for the epic integration branch is gated against the epic checkpoint.
|
|
55
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
|
|
37
56
|
function Get-ModelRoutingCheckpoint {
|
|
38
57
|
<#
|
|
39
58
|
.SYNOPSIS
|
|
@@ -43,7 +62,8 @@ function Get-ModelRoutingCheckpoint {
|
|
|
43
62
|
[CmdletBinding()]
|
|
44
63
|
[OutputType([object])]
|
|
45
64
|
param(
|
|
46
|
-
[
|
|
65
|
+
[Parameter(Mandatory)]
|
|
66
|
+
[string] $CheckpointPath
|
|
47
67
|
)
|
|
48
68
|
|
|
49
69
|
if (-not (Test-Path -LiteralPath $CheckpointPath -PathType Leaf)) {
|
|
@@ -117,6 +137,66 @@ function Test-ModelRoutingReceiptPresent {
|
|
|
117
137
|
return $false
|
|
118
138
|
}
|
|
119
139
|
|
|
140
|
+
function Resolve-ModelRoutingWorktreeTarget {
|
|
141
|
+
<#
|
|
142
|
+
.SYNOPSIS
|
|
143
|
+
Resolve the delegation's target worktree. Tests mock this function (resolution seam).
|
|
144
|
+
.DESCRIPTION
|
|
145
|
+
A seam rather than a direct call because the resolver reads real git state: the
|
|
146
|
+
worktree list, each registration's branch, and the filesystem. A test that drove it
|
|
147
|
+
directly would depend on whichever worktrees exist on the machine running it.
|
|
148
|
+
.PARAMETER PromptText
|
|
149
|
+
The delegation prompt, which is the only field scanned for identity.
|
|
150
|
+
.OUTPUTS
|
|
151
|
+
The target result object from Resolve-WorktreeItemTarget.
|
|
152
|
+
#>
|
|
153
|
+
[CmdletBinding()]
|
|
154
|
+
[OutputType([pscustomobject])]
|
|
155
|
+
param(
|
|
156
|
+
[Parameter(Mandatory)]
|
|
157
|
+
[AllowEmptyString()]
|
|
158
|
+
[string] $PromptText
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return (Resolve-WorktreeItemTarget -Text $PromptText -SessionRoot (Get-Location).Path)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function Get-ModelRoutingTargetCheckpointResolution {
|
|
165
|
+
<#
|
|
166
|
+
.SYNOPSIS
|
|
167
|
+
Resolve the checkpoint path this delegation must be checked against.
|
|
168
|
+
.DESCRIPTION
|
|
169
|
+
Maps the four resolution states onto a checkpoint path or a deny reason. The two
|
|
170
|
+
resolved states share one branch because the path is composed the same way for
|
|
171
|
+
both: identity selects the worktree, and the checkpoint is read beneath it. The two
|
|
172
|
+
unresolved states deny, carrying the code the worktree-resolution accessors supply;
|
|
173
|
+
neither literal appears in this file.
|
|
174
|
+
.PARAMETER PromptText
|
|
175
|
+
The delegation prompt to resolve identity from.
|
|
176
|
+
.OUTPUTS
|
|
177
|
+
System.Collections.Specialized.OrderedDictionary with CheckpointPath and Reason.
|
|
178
|
+
#>
|
|
179
|
+
[CmdletBinding()]
|
|
180
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
181
|
+
param(
|
|
182
|
+
[Parameter(Mandatory)]
|
|
183
|
+
[AllowEmptyString()]
|
|
184
|
+
[string] $PromptText
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
$target = Resolve-ModelRoutingWorktreeTarget -PromptText $PromptText
|
|
188
|
+
|
|
189
|
+
switch ($target.Status) {
|
|
190
|
+
{ $_ -in @('SessionRoot', 'OtherWorktree') } {
|
|
191
|
+
return [ordered]@{ CheckpointPath = (Get-WorktreeItemCheckpointPath -WorktreeRoot $target.WorktreeRoot); Reason = $null }
|
|
192
|
+
}
|
|
193
|
+
default {
|
|
194
|
+
$reason = "$($target.ReasonCode): $($target.Detail) The model-routing gate will not check this delegation against a checkpoint that may belong to a different item. Put the line 'Canonical issue number for this feature is <N>.' and a 'branch: <item branch>' label in the delegation prompt so the gate can identify the item."
|
|
195
|
+
return [ordered]@{ CheckpointPath = $null; Reason = $reason }
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
120
200
|
function Invoke-ModelRoutingReceiptDecision {
|
|
121
201
|
<#
|
|
122
202
|
.SYNOPSIS
|
|
@@ -154,7 +234,42 @@ function Invoke-ModelRoutingReceiptDecision {
|
|
|
154
234
|
return $allow
|
|
155
235
|
}
|
|
156
236
|
|
|
157
|
-
|
|
237
|
+
# Identity resolution sits after the scope filter and before the checkpoint read, so a
|
|
238
|
+
# delegation outside the gated set is still allowed however unresolvable its target is,
|
|
239
|
+
# and a gated delegation the gate cannot identify is refused rather than answered from
|
|
240
|
+
# unrelated state. An absent prompt is passed as an empty string, which resolves to no
|
|
241
|
+
# identity and therefore to the same deny.
|
|
242
|
+
$prompt = [string](Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'prompt')
|
|
243
|
+
|
|
244
|
+
# Decision: a prompt whose branch label equals the epic checkpoint's integration_branch is
|
|
245
|
+
# epic scope (issue #663), and the receipt is looked up in the epic checkpoint without
|
|
246
|
+
# running per-feature identity resolution. Any other prompt falls through unchanged.
|
|
247
|
+
$epicScope = Resolve-EpicScopeCheckpoint -Text $prompt -SessionRoot (Get-Location).Path
|
|
248
|
+
if ($epicScope.IsEpicScope) {
|
|
249
|
+
if (Test-ModelRoutingReceiptPresent -Checkpoint $epicScope.Checkpoint -Subagent $subagent) {
|
|
250
|
+
return $allow
|
|
251
|
+
}
|
|
252
|
+
return [ordered]@{
|
|
253
|
+
hookSpecificOutput = [ordered]@{
|
|
254
|
+
hookEventName = 'PreToolUse'
|
|
255
|
+
permissionDecision = 'deny'
|
|
256
|
+
permissionDecisionReason = "MODEL_ROUTING_RECEIPT_BLOCKED: cannot delegate to '$subagent' in epic scope before a model_routing_receipts entry for it is recorded in $($epicScope.CheckpointPath); the failed readiness predicate is 'model_routing_receipts'."
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
$resolution = Get-ModelRoutingTargetCheckpointResolution -PromptText $prompt
|
|
262
|
+
if ($resolution.Reason) {
|
|
263
|
+
return [ordered]@{
|
|
264
|
+
hookSpecificOutput = [ordered]@{
|
|
265
|
+
hookEventName = 'PreToolUse'
|
|
266
|
+
permissionDecision = 'deny'
|
|
267
|
+
permissionDecisionReason = $resolution.Reason
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
$checkpoint = Get-ModelRoutingCheckpoint -CheckpointPath $resolution.CheckpointPath
|
|
158
273
|
if (Test-ModelRoutingReceiptPresent -Checkpoint $checkpoint -Subagent $subagent) {
|
|
159
274
|
return $allow
|
|
160
275
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Epic-scope command and path legs of the preimplementation gate (issue #663).
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Dot-sourced by enforce-orchestration-preimplementation-gate.ps1. Holds two groups of
|
|
7
|
+
functions:
|
|
8
|
+
|
|
9
|
+
- The two per-mode read seams of issue #554 (Get-EpicCheckpointContent and
|
|
10
|
+
Get-ParallelCheckpointContent), relocated verbatim from the gate file by issue #663
|
|
11
|
+
so the gate stays inside the 500-line cap. Their names and behaviour are unchanged,
|
|
12
|
+
so tests that mock or shadow them by name are unaffected.
|
|
13
|
+
- The epic-scope decision for the command and path legs. A staging command or a
|
|
14
|
+
Write/Edit call is epic scope when the effective worktree's HEAD (the -C selector
|
|
15
|
+
worktree when present, otherwise the session root) equals the integration_branch of
|
|
16
|
+
artifacts/orchestration/epic-orchestrator-state.json. In epic scope the call is
|
|
17
|
+
decided by the epic command-leg readiness predicate, and under decision D2 an
|
|
18
|
+
implementation-classified operand is allowed only while a merge is in progress in
|
|
19
|
+
that worktree. Outside epic scope the decision function returns $null and the gate's
|
|
20
|
+
single-feature path runs unchanged.
|
|
21
|
+
|
|
22
|
+
.NOTES
|
|
23
|
+
PowerShell 7+. Depends on functions the gate defines or dot-sources before any call:
|
|
24
|
+
Split-OrchestrationCommandLine and ConvertTo-OrchestrationCommandToken (helpers file),
|
|
25
|
+
Get-OrchestrationDelegationCheckpointPath (modes file), and the gate's allow and block
|
|
26
|
+
decision constructors. Mirrored byte-identically under
|
|
27
|
+
extensions/drm-copilot/resources/claude-customizations/.
|
|
28
|
+
#>
|
|
29
|
+
|
|
30
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeResolution.psm1') -Force -ErrorAction Stop
|
|
31
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/EpicScopeReadiness.psm1') -Force -ErrorAction Stop
|
|
32
|
+
|
|
33
|
+
# The two per-mode read seams (issue #554). Each takes its path from the fixed mode
|
|
34
|
+
# table and never from a delegation's own text; an absent file returns an empty
|
|
35
|
+
# string, which the readiness predicate then treats as a deny.
|
|
36
|
+
function Get-EpicCheckpointContent {
|
|
37
|
+
[CmdletBinding()]
|
|
38
|
+
[OutputType([string])]
|
|
39
|
+
param()
|
|
40
|
+
|
|
41
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'epic'
|
|
42
|
+
if (-not (Test-Path -LiteralPath $path)) {
|
|
43
|
+
return ''
|
|
44
|
+
}
|
|
45
|
+
return Get-Content -Raw -LiteralPath $path
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function Get-ParallelCheckpointContent {
|
|
49
|
+
[CmdletBinding()]
|
|
50
|
+
[OutputType([string])]
|
|
51
|
+
param()
|
|
52
|
+
|
|
53
|
+
$path = Get-OrchestrationDelegationCheckpointPath -Mode 'parallel'
|
|
54
|
+
if (-not (Test-Path -LiteralPath $path)) {
|
|
55
|
+
return ''
|
|
56
|
+
}
|
|
57
|
+
return Get-Content -Raw -LiteralPath $path
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Get-OrchestrationEpicScopeSelector {
|
|
61
|
+
<#
|
|
62
|
+
.SYNOPSIS
|
|
63
|
+
Returns the value of a leading `git -C <value>` selector, or $null.
|
|
64
|
+
.DESCRIPTION
|
|
65
|
+
Pure. Reads only the first segment of the command line, split and tokenized with
|
|
66
|
+
the helpers-file parser so quoting is handled exactly as the staging exemption
|
|
67
|
+
handles it. An unbalanced command line has no decidable selector and returns $null.
|
|
68
|
+
.PARAMETER Command
|
|
69
|
+
The full command line.
|
|
70
|
+
.OUTPUTS
|
|
71
|
+
System.String
|
|
72
|
+
#>
|
|
73
|
+
[CmdletBinding()]
|
|
74
|
+
[OutputType([string])]
|
|
75
|
+
param([Parameter(Mandatory)][AllowEmptyString()][string] $Command)
|
|
76
|
+
|
|
77
|
+
$split = Split-OrchestrationCommandLine -CommandText $Command
|
|
78
|
+
if (-not $split.Balanced -or @($split.Segments).Count -eq 0) {
|
|
79
|
+
return $null
|
|
80
|
+
}
|
|
81
|
+
$token = @(ConvertTo-OrchestrationCommandToken -Segment ([string]@($split.Segments)[0]).Trim())
|
|
82
|
+
if ($token.Count -ge 3 -and $token[0] -ceq 'git' -and $token[1] -ceq '-C' -and $token[2]) {
|
|
83
|
+
return $token[2]
|
|
84
|
+
}
|
|
85
|
+
return $null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function Get-OrchestrationEpicScopeDecision {
|
|
89
|
+
<#
|
|
90
|
+
.SYNOPSIS
|
|
91
|
+
Returns the epic-scope decision for an implementation-classified command or path, or $null.
|
|
92
|
+
.DESCRIPTION
|
|
93
|
+
Resolves epic scope with worktree-HEAD matching and the command's -C selector. When
|
|
94
|
+
the call is not epic scope, returns $null so the caller's single-feature path runs
|
|
95
|
+
unchanged. In epic scope, returns an allow decision when the epic command-leg
|
|
96
|
+
readiness predicate passes, and otherwise a deny naming the epic checkpoint and the
|
|
97
|
+
failed conjunct.
|
|
98
|
+
.PARAMETER Command
|
|
99
|
+
The Bash command line; empty for a path leg.
|
|
100
|
+
.PARAMETER FilePath
|
|
101
|
+
The Write/Edit file path; empty for a command leg.
|
|
102
|
+
.OUTPUTS
|
|
103
|
+
System.Collections.Specialized.OrderedDictionary, or $null outside epic scope.
|
|
104
|
+
#>
|
|
105
|
+
[CmdletBinding()]
|
|
106
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
107
|
+
param(
|
|
108
|
+
[AllowNull()][AllowEmptyString()][string] $Command = '',
|
|
109
|
+
[AllowNull()][AllowEmptyString()][string] $FilePath = ''
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# A call carrying neither a command nor a path has no leg to decide.
|
|
113
|
+
if (-not $Command -and -not $FilePath) {
|
|
114
|
+
return $null
|
|
115
|
+
}
|
|
116
|
+
$selector = if ($Command) { Get-OrchestrationEpicScopeSelector -Command $Command } else { $null }
|
|
117
|
+
$scope = Resolve-EpicScopeCheckpoint -Text ([string]$Command) -SessionRoot (Get-Location).Path -WorktreeSelector $selector -MatchWorktreeHead
|
|
118
|
+
if (-not $scope.IsEpicScope) {
|
|
119
|
+
return $null
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
$failure = Get-EpicCommandLegReadinessFailure -Checkpoint $scope.Checkpoint -MergeInProgress $scope.MergeInProgress
|
|
123
|
+
if (-not $failure) {
|
|
124
|
+
return Get-OrchestrationPreimplementationGateAllowDecision
|
|
125
|
+
}
|
|
126
|
+
return Get-OrchestrationPreimplementationGateBlockDecision -Reason ("PREIMPLEMENTATION_GATE_BLOCKED: this epic-scope operation was evaluated against $($scope.CheckpointPath), and the failed readiness predicate is '$failure'. Implementation operations in epic scope require that checkpoint to satisfy every readiness predicate, and a production path may be staged or edited only while a merge is in progress.")
|
|
127
|
+
}
|