@danmoisan/drm-copilot-mcp 1.1.10 → 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 +3116 -910
- 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 +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/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
package/package.json
CHANGED
|
@@ -18,6 +18,10 @@ tools:
|
|
|
18
18
|
- "Bash(bash .claude/lib/bash/compute-cohorts.sh*)"
|
|
19
19
|
- "Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"
|
|
20
20
|
- "Bash(bash .claude/lib/bash/validate-parallel-manifest.sh*)"
|
|
21
|
+
- "Bash(pwsh -NoProfile -File .claude/lib/project-file-merge/Resolve-MergeableConflict.ps1*)"
|
|
22
|
+
- "Bash(dotnet tool restore*)"
|
|
23
|
+
- "Bash(dotnet csharpier check *)"
|
|
24
|
+
- "Bash(dotnet build *)"
|
|
21
25
|
- "mcp__drm-copilot__collect_pr_context"
|
|
22
26
|
- "mcp__drm-copilot__validate_orchestration_artifacts"
|
|
23
27
|
skills:
|
|
@@ -222,12 +226,21 @@ and `mcp_call_receipts[]` populated with the `parallel` route's required names f
|
|
|
222
226
|
|
|
223
227
|
The checkpoint schema is owned by F3 and is defined once in
|
|
224
228
|
`.claude/rules/parallel-orchestration.md`, enforced by
|
|
225
|
-
`scripts/dev_tools/validate_parallel_orchestrator_state.py`. You consume that schema and
|
|
226
|
-
field to it
|
|
229
|
+
`scripts/dev_tools/validate_parallel_orchestrator_state.py`. You consume that schema and
|
|
230
|
+
add no field to it that the rule file does not declare.
|
|
231
|
+
`mergeable_conflicts_resolved` is the one declared optional item field.
|
|
232
|
+
The `parallel-orchestrate` skill's `## Parallel-Level Checkpoint` section carries the
|
|
227
233
|
full enumeration, the `merge_status` transition chain you write, and the arrays that are read-only
|
|
228
234
|
to you. Validate through `mcp__drm-copilot__validate_orchestration_artifacts` with
|
|
229
235
|
`artifact_type: "parallel-orchestrator-state"`.
|
|
230
236
|
|
|
237
|
+
You run the project-file merge step of the `parallel-orchestrate` skill yourself rather than
|
|
238
|
+
delegating it. The four entries added to your `tools:` list above for the resolution script and
|
|
239
|
+
for the three .NET verification commands are scoped to that step and to no other, and they are the
|
|
240
|
+
only reason those commands appear in your allowlist at all. That step's one optional item field is
|
|
241
|
+
`mergeable_conflicts_resolved`, declared in the rule file and appended to an item after a
|
|
242
|
+
parent-side resolution.
|
|
243
|
+
|
|
231
244
|
## Documentation Maintenance
|
|
232
245
|
|
|
233
246
|
Maintain `docs/features/parallel/<slug>/parallel-status.md` as a human-readable projection of the
|
|
@@ -160,6 +160,9 @@ The facade exports `Get-PlanPaths`, `Get-BlastRadius`, `Get-BlastRadiusFromObser
|
|
|
160
160
|
`Test-BlastRadius`, and `Test-BlastRadiusConflict`. Its truth table is
|
|
161
161
|
`config/blast-radius.json`, which push-down publishes alongside `.claude`.
|
|
162
162
|
|
|
163
|
+
`Test-BlastRadiusConflict` reads that table's optional `mergeable_paths` list and contributes no
|
|
164
|
+
`path_overlap` edge for a path matching it, while the path stays in the declared radius.
|
|
165
|
+
|
|
163
166
|
**Cohort seeding and concurrency batching — bash entry points.** The bash library is granted as
|
|
164
167
|
four entry-point-specific allowlist entries — `"Bash(bash .claude/lib/bash/compute-cohorts.sh*)"`,
|
|
165
168
|
`"Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"`,
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
param()
|
|
41
41
|
|
|
42
42
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
43
|
+
# Shared command-line parser (issue #545), consumed by the scope filter in
|
|
44
|
+
# Invoke-EpicMergeGateDecision and by Get-EpicMergeGateCommandPrNumber.
|
|
45
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
46
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
43
47
|
|
|
44
48
|
$script:ChildCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
45
49
|
$script:EpicCheckpointPath = 'artifacts/orchestration/epic-orchestrator-state.json'
|
|
@@ -128,6 +132,22 @@ function Get-EpicMergeGateCommandPrNumber {
|
|
|
128
132
|
<#
|
|
129
133
|
.SYNOPSIS
|
|
130
134
|
Extract an explicit PR number argument from a gh pr merge command, or $null.
|
|
135
|
+
.DESCRIPTION
|
|
136
|
+
Both spellings are read from the segment that structurally invokes gh pr merge,
|
|
137
|
+
never from the whole command line. The positional form ("gh pr merge 410 --merge")
|
|
138
|
+
resolves through Get-CommandLineOperand and takes the first all-digit operand; the
|
|
139
|
+
flag-led forms ("gh pr merge --merge 410" and "gh pr merge --merge=410") resolve
|
|
140
|
+
through Get-CommandLineFlagValue. A bare "gh pr merge --merge" yields $null, which
|
|
141
|
+
the parallel branch treats as fail-closed.
|
|
142
|
+
|
|
143
|
+
The deleted unanchored branch scanned the WHOLE command text for the first run of
|
|
144
|
+
digits once "gh pr merge" appeared anywhere in it, and that failed in both
|
|
145
|
+
directions. Fail-closed: a leading "cd <path>" whose path carries a timestamp
|
|
146
|
+
component supplied a digit run that was not a pull request number, so an authorized
|
|
147
|
+
merge was blocked. False-allow: with authorized item 501 and unauthorized item 777,
|
|
148
|
+
"cd /repo/worktrees/501 && gh pr merge --merge 777" extracted 501, matched the
|
|
149
|
+
authorized item, and permitted the merge of PR 777. Taking the number from the
|
|
150
|
+
matched segment's own operand or flag value closes both directions.
|
|
131
151
|
.PARAMETER CommandText
|
|
132
152
|
The Bash command text under evaluation.
|
|
133
153
|
.OUTPUTS
|
|
@@ -140,20 +160,17 @@ function Get-EpicMergeGateCommandPrNumber {
|
|
|
140
160
|
[string] $CommandText
|
|
141
161
|
)
|
|
142
162
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
return [int]$Matches[1]
|
|
163
|
+
foreach ($operand in @(Get-CommandLineOperand -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge'))) {
|
|
164
|
+
if ($operand -match '^\d+$') {
|
|
165
|
+
return [int]$operand
|
|
166
|
+
}
|
|
148
167
|
}
|
|
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]
|
|
168
|
+
|
|
169
|
+
$flagValue = Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
|
|
170
|
+
if ($null -ne $flagValue -and $flagValue -match '^\d+$') {
|
|
171
|
+
return [int]$flagValue
|
|
156
172
|
}
|
|
173
|
+
|
|
157
174
|
return $null
|
|
158
175
|
}
|
|
159
176
|
|
|
@@ -373,8 +390,26 @@ function Invoke-EpicMergeGateDecision {
|
|
|
373
390
|
}
|
|
374
391
|
|
|
375
392
|
# Only a gh pr merge invocation carrying --merge is in scope for this gate; every
|
|
376
|
-
# other Bash command is unaffected.
|
|
377
|
-
|
|
393
|
+
# other Bash command is unaffected. Both legs are read structurally from the segment
|
|
394
|
+
# that invokes the command, so a quoted mention of the phrase and a --merge token
|
|
395
|
+
# belonging to some other segment no longer bring a command into scope.
|
|
396
|
+
#
|
|
397
|
+
# The flag leg needs a raw-scan fallback: a wrapper's quoted argument collapses into ONE
|
|
398
|
+
# token, so a flag inside it is never read as a token, and the token-only read took
|
|
399
|
+
# bash -c "gh pr merge --merge 688" out of scope. The fallback reads only the segments the
|
|
400
|
+
# scanner already scans raw, so a masked mention in a non-wrapper segment stays out.
|
|
401
|
+
$isMergeInvocation = Test-CommandLineInvocation -CommandText $commandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge')
|
|
402
|
+
$hasMergeFlag = Test-CommandLineFlag -CommandText $commandText -CommandWord 'gh' -SubcommandPath @('pr', 'merge') -FlagName '--merge'
|
|
403
|
+
if (-not $hasMergeFlag) {
|
|
404
|
+
foreach ($segment in @(Read-CommandLineSegment -CommandText $commandText)) {
|
|
405
|
+
if ((Test-CommandLineSegmentRawScan -Segment $segment) -and
|
|
406
|
+
$segment.ScanText.IndexOf('--merge', [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
|
|
407
|
+
$hasMergeFlag = $true
|
|
408
|
+
break
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (-not $isMergeInvocation -or -not $hasMergeFlag) {
|
|
378
413
|
return Get-EpicMergeGateAllowDecision
|
|
379
414
|
}
|
|
380
415
|
|
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
|
|
|
@@ -19,6 +19,10 @@ Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -
|
|
|
19
19
|
# proof the issue #539 exemption is behaviourally unchanged.
|
|
20
20
|
. (Join-Path $PSScriptRoot 'enforce-orchestration-preimplementation-gate-modes.ps1')
|
|
21
21
|
|
|
22
|
+
# Shared command-line parser (issue #545): per-segment scan text and structural matching.
|
|
23
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
24
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
25
|
+
|
|
22
26
|
# The readiness checkpoint this gate reads and names in its block message.
|
|
23
27
|
$script:CheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
24
28
|
|
|
@@ -132,9 +136,11 @@ function Test-ImplementationCommand {
|
|
|
132
136
|
'(^|\s)npx\s+(prettier|eslint|tsc|jest)\b',
|
|
133
137
|
'(^|\s)pwsh\s+.*(Invoke-Pester|tests/scripts/)'
|
|
134
138
|
)
|
|
139
|
+
$scanText = @(Read-CommandLineSegment -CommandText $normalizedCommand).ScanText
|
|
140
|
+
$isStaging = @('add', 'commit').Where({ Test-CommandLineInvocation -CommandText $normalizedCommand -CommandWord 'git' -SubcommandPath @($_) }).Count -gt 0
|
|
135
141
|
|
|
136
142
|
for ($index = 0; $index -lt $implementationCommandPatterns.Count; $index++) {
|
|
137
|
-
if ($
|
|
143
|
+
if (-not (($scanText | Where-Object { $_ -match $implementationCommandPatterns[$index] }) -or ($index -eq 0 -and $isStaging))) {
|
|
138
144
|
continue
|
|
139
145
|
}
|
|
140
146
|
# Allow-side only (issue #539). Index 0 is the git staging trigger, whose pattern
|
|
@@ -44,6 +44,13 @@ $script:AbandonConfirmToken = '--confirm-abandon'
|
|
|
44
44
|
# Literal prefix on the deny reason, so the reason code is greppable in transcripts.
|
|
45
45
|
$script:AbandonBlockedReasonCode = 'PARALLEL_ABANDON_BLOCKED'
|
|
46
46
|
|
|
47
|
+
# Shared command-line parser (issue #545). Dot-sourced BELOW the two token assignments
|
|
48
|
+
# rather than above them, deliberately: the assignments keep their existing line numbers,
|
|
49
|
+
# so the diff shows no change at either line. Both are still declared before any function
|
|
50
|
+
# body runs, because the file executes top to bottom before any of its functions is called.
|
|
51
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
52
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
53
|
+
|
|
47
54
|
function Get-ParallelAbandonGateToolInput {
|
|
48
55
|
<#
|
|
49
56
|
.SYNOPSIS
|
|
@@ -85,10 +92,77 @@ function Get-ParallelAbandonNormalizedCommand {
|
|
|
85
92
|
return ($CommandText -replace '\s+', ' ').Trim()
|
|
86
93
|
}
|
|
87
94
|
|
|
95
|
+
function Test-ParallelAbandonSegmentDisposition {
|
|
96
|
+
<#
|
|
97
|
+
.SYNOPSIS
|
|
98
|
+
Report whether one segment's tokens request the abandon disposition.
|
|
99
|
+
.DESCRIPTION
|
|
100
|
+
The option name and its value are derived from $script:AbandonDispositionToken by
|
|
101
|
+
splitting it on whitespace, so this function restates neither literal and the seam
|
|
102
|
+
test's single-source-of-truth property is preserved. Both spellings the producer's
|
|
103
|
+
argparse registration accepts are recognized: the separated pair
|
|
104
|
+
'--disposition' 'abandon' as two adjacent tokens, and the equals-joined single token
|
|
105
|
+
'--disposition=abandon'.
|
|
106
|
+
|
|
107
|
+
Comparison is by whole-token equality, which is case-insensitive in PowerShell and
|
|
108
|
+
therefore preserves the OrdinalIgnoreCase behaviour of the containment test it
|
|
109
|
+
replaces. Because a quoted span is one token, a grep whose quoted search term is the
|
|
110
|
+
disposition token no longer requests an abandon.
|
|
111
|
+
|
|
112
|
+
A raw-scan leg runs after the token loop, on the segments the scanner itself scans
|
|
113
|
+
raw, so a disposition carried inside a wrapper's quoted argument is still recognized.
|
|
114
|
+
.PARAMETER Segment
|
|
115
|
+
One segment record produced by Read-CommandLineSegment.
|
|
116
|
+
.OUTPUTS
|
|
117
|
+
System.Boolean
|
|
118
|
+
#>
|
|
119
|
+
[CmdletBinding()]
|
|
120
|
+
[OutputType([bool])]
|
|
121
|
+
param([Parameter(Mandatory)][AllowNull()] $Segment)
|
|
122
|
+
|
|
123
|
+
$parts = @($script:AbandonDispositionToken -split '\s+' | Where-Object { $_ })
|
|
124
|
+
if ($parts.Count -lt 2) {
|
|
125
|
+
return $false
|
|
126
|
+
}
|
|
127
|
+
$optionName = $parts[0]
|
|
128
|
+
$optionValue = $parts[1]
|
|
129
|
+
$joined = $optionName + '=' + $optionValue
|
|
130
|
+
|
|
131
|
+
$token = @($Segment.Tokens)
|
|
132
|
+
|
|
133
|
+
for ($index = 0; $index -lt $token.Count; $index++) {
|
|
134
|
+
if ($token[$index] -eq $joined) {
|
|
135
|
+
return $true
|
|
136
|
+
}
|
|
137
|
+
if ($token[$index] -eq $optionName -and
|
|
138
|
+
$index + 1 -lt $token.Count -and
|
|
139
|
+
$token[$index + 1] -eq $optionValue) {
|
|
140
|
+
return $true
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (-not (Test-CommandLineSegmentRawScan -Segment $Segment)) {
|
|
145
|
+
return $false
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# A wrapper's quoted argument is a nested command line and collapses into ONE token, so
|
|
149
|
+
# neither the adjacent pair nor the equals-joined token can form inside it. Both accepted
|
|
150
|
+
# spellings are reconstructed from the split constant rather than restated, so the
|
|
151
|
+
# single-source-of-truth seam test still finds each literal exactly once in this file.
|
|
152
|
+
$scanText = [string]$Segment.ScanText
|
|
153
|
+
$comparison = [System.StringComparison]::OrdinalIgnoreCase
|
|
154
|
+
return ($scanText.IndexOf($joined, $comparison) -ge 0 -or
|
|
155
|
+
$scanText.IndexOf(($optionName + ' ' + $optionValue), $comparison) -ge 0)
|
|
156
|
+
}
|
|
157
|
+
|
|
88
158
|
function Test-ParallelAbandonCommandInScope {
|
|
89
159
|
<#
|
|
90
160
|
.SYNOPSIS
|
|
91
161
|
Report whether a command carries the abandon disposition token at all.
|
|
162
|
+
.DESCRIPTION
|
|
163
|
+
Evaluated per segment through the shared parser rather than as a containment test
|
|
164
|
+
over the whole command string. In scope when ANY segment requests the abandon
|
|
165
|
+
disposition in either accepted spelling.
|
|
92
166
|
.PARAMETER NormalizedCommand
|
|
93
167
|
The whitespace-normalized command text.
|
|
94
168
|
.OUTPUTS
|
|
@@ -105,15 +179,25 @@ function Test-ParallelAbandonCommandInScope {
|
|
|
105
179
|
if ([string]::IsNullOrWhiteSpace($NormalizedCommand)) {
|
|
106
180
|
return $false
|
|
107
181
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
182
|
+
|
|
183
|
+
foreach ($segment in @(Read-CommandLineSegment -CommandText $NormalizedCommand)) {
|
|
184
|
+
if (Test-ParallelAbandonSegmentDisposition -Segment $segment) {
|
|
185
|
+
return $true
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return $false
|
|
111
190
|
}
|
|
112
191
|
|
|
113
192
|
function Test-ParallelAbandonCommandConfirmed {
|
|
114
193
|
<#
|
|
115
194
|
.SYNOPSIS
|
|
116
195
|
Report whether a command carries the confirmation marker.
|
|
196
|
+
.DESCRIPTION
|
|
197
|
+
Confirmed only when a SINGLE segment carries both the abandon disposition and the
|
|
198
|
+
confirmation marker. The confirmation is the caller's deliberate acknowledgement of
|
|
199
|
+
this abandon, so a marker echoed in an unrelated segment is not an acknowledgement
|
|
200
|
+
of the command that follows it.
|
|
117
201
|
.PARAMETER NormalizedCommand
|
|
118
202
|
The whitespace-normalized command text.
|
|
119
203
|
.OUTPUTS
|
|
@@ -130,9 +214,22 @@ function Test-ParallelAbandonCommandConfirmed {
|
|
|
130
214
|
if ([string]::IsNullOrWhiteSpace($NormalizedCommand)) {
|
|
131
215
|
return $false
|
|
132
216
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
217
|
+
|
|
218
|
+
foreach ($segment in @(Read-CommandLineSegment -CommandText $NormalizedCommand)) {
|
|
219
|
+
$tokens = @($segment.Tokens)
|
|
220
|
+
if (-not (Test-ParallelAbandonSegmentDisposition -Segment $segment)) {
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
if ($tokens -contains $script:AbandonConfirmToken) {
|
|
224
|
+
return $true
|
|
225
|
+
}
|
|
226
|
+
if ((Test-CommandLineSegmentRawScan -Segment $segment) -and
|
|
227
|
+
([string]$segment.ScanText).IndexOf($script:AbandonConfirmToken, [System.StringComparison]::OrdinalIgnoreCase) -ge 0) {
|
|
228
|
+
return $true
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return $false
|
|
136
233
|
}
|
|
137
234
|
|
|
138
235
|
function Get-ParallelAbandonGateAllowDecision {
|
package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1
CHANGED
|
@@ -30,8 +30,21 @@ param()
|
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
33
|
+
# Sanctioned-removal manifest reader (issue #635), consumed by the manifest branch in
|
|
34
|
+
# Invoke-ParallelWorktreeRemovalGateDecision. Same module the epic gate imports.
|
|
35
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/cleanup-manifest/CleanupWorktreeManifest.psm1') -Force
|
|
36
|
+
# Shared command-line parser (issue #545), consumed by the scope filter in
|
|
37
|
+
# Invoke-ParallelWorktreeRemovalGateDecision and by Get-ParallelWorktreeRemovalCommandPath.
|
|
38
|
+
# Both call sites are byte-for-byte the calls the epic gate makes, so the duplicated
|
|
39
|
+
# concern now has one implementation.
|
|
40
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
41
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
33
42
|
$script:ParallelCheckpointPath = 'artifacts/orchestration/parallel-orchestrator-state.json'
|
|
34
43
|
$script:AllowedMergeStatuses = @('merged', 'worktree_removed')
|
|
44
|
+
# Sanctioned-removal manifest location. Recorded here beside the checkpoint path so
|
|
45
|
+
# every hook-read document this gate consults is named in one place; the module owns
|
|
46
|
+
# the read itself.
|
|
47
|
+
$script:CleanupWorktreeManifestPath = 'artifacts/orchestration/cleanup-worktrees-manifest.json'
|
|
35
48
|
|
|
36
49
|
function Get-ParallelWorktreeRemovalGateCheckpointContent {
|
|
37
50
|
<#
|
|
@@ -55,6 +68,24 @@ function Get-ParallelWorktreeRemovalCommandPath {
|
|
|
55
68
|
<#
|
|
56
69
|
.SYNOPSIS
|
|
57
70
|
Extract the target worktree path argument from a git worktree remove command.
|
|
71
|
+
.DESCRIPTION
|
|
72
|
+
The operand comes from the segment that structurally invokes git worktree remove,
|
|
73
|
+
so a 'cd <path> &&' segment chained before the removal contributes nothing and a
|
|
74
|
+
quoted mention of the phrase resolves to no operand at all. Quotes around the path
|
|
75
|
+
are already stripped by the tokenizer.
|
|
76
|
+
|
|
77
|
+
'--force' is a zero-argument flag, so it never contributes an operand and may be
|
|
78
|
+
written on either side of the target path. Its presence is read structurally through
|
|
79
|
+
Test-CommandLineFlag rather than by searching the raw text, so a '--force' spelling
|
|
80
|
+
that appears inside an unrelated quoted argument cannot change how the operand list
|
|
81
|
+
is read. When no operand resolves, a present '--force' is reported in its place, so
|
|
82
|
+
the checkpoint lookup fails closed on a value that matches no recorded worktree_path
|
|
83
|
+
- the same value the previous raw-text pattern returned for that input.
|
|
84
|
+
|
|
85
|
+
This body is identical to Get-EpicWorktreeRemovalCommandPath in
|
|
86
|
+
enforce-epic-worktree-removal-gate.ps1. The two gates fire on the same command and
|
|
87
|
+
now delegate the shared concern to one parser rather than to two patterns that had
|
|
88
|
+
already diverged from the Codex copy.
|
|
58
89
|
.PARAMETER CommandText
|
|
59
90
|
The Bash command text under evaluation.
|
|
60
91
|
.OUTPUTS
|
|
@@ -67,8 +98,14 @@ function Get-ParallelWorktreeRemovalCommandPath {
|
|
|
67
98
|
[string] $CommandText
|
|
68
99
|
)
|
|
69
100
|
|
|
70
|
-
|
|
71
|
-
|
|
101
|
+
$hasForce = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove') -FlagName '--force'
|
|
102
|
+
$operands = @(Get-CommandLineOperand -CommandText $CommandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))
|
|
103
|
+
|
|
104
|
+
if ($operands.Count -gt 0) {
|
|
105
|
+
return $operands[0]
|
|
106
|
+
}
|
|
107
|
+
if ($hasForce) {
|
|
108
|
+
return '--force'
|
|
72
109
|
}
|
|
73
110
|
return $null
|
|
74
111
|
}
|
|
@@ -203,7 +240,10 @@ function Invoke-ParallelWorktreeRemovalGateDecision {
|
|
|
203
240
|
return Get-ParallelWorktreeGateAllowDecision
|
|
204
241
|
}
|
|
205
242
|
|
|
206
|
-
|
|
243
|
+
# Scope filter. The test is structural, so a relocating spelling such as
|
|
244
|
+
# 'git -C <dir> worktree remove <path>' is now in scope and quoted prose that merely
|
|
245
|
+
# mentions the phrase is not (issue #545). This is the same call the epic gate makes.
|
|
246
|
+
if (-not (Test-CommandLineInvocation -CommandText $commandText -CommandWord 'git' -SubcommandPath @('worktree', 'remove'))) {
|
|
207
247
|
return Get-ParallelWorktreeGateAllowDecision
|
|
208
248
|
}
|
|
209
249
|
|
|
@@ -224,6 +264,21 @@ function Invoke-ParallelWorktreeRemovalGateDecision {
|
|
|
224
264
|
return Get-ParallelWorktreeGateAllowDecision
|
|
225
265
|
}
|
|
226
266
|
|
|
267
|
+
# Sanctioned-removal manifest branch (issue #635). It runs last, so a
|
|
268
|
+
# checkpoint-authorized removal still allows at the same decision point it does
|
|
269
|
+
# today and no transcript attribution changes.
|
|
270
|
+
#
|
|
271
|
+
# The coverage test is a PRESENCE test, deliberately not an authorization test. A
|
|
272
|
+
# target this checkpoint records at all is excluded from this branch regardless of
|
|
273
|
+
# that record's merge_status, so a removal the checkpoint does not authorize still
|
|
274
|
+
# reaches the unchanged deny below. This gate defines only the parallel checkpoint
|
|
275
|
+
# seam and has no epic-checkpoint seam, so its exclusion covers the items array
|
|
276
|
+
# only; the epic gate covers the features array with its own exclusion.
|
|
277
|
+
if (-not (Test-CleanupManifestCheckpointCoversPath -Checkpoint $checkpoint -RecordArrayName 'items' -WorktreePath $worktreePath) -and
|
|
278
|
+
(Test-CleanupWorktreeManifestAuthorizesRemoval -WorktreePath $worktreePath)) {
|
|
279
|
+
return Get-ParallelWorktreeGateAllowDecision
|
|
280
|
+
}
|
|
281
|
+
|
|
227
282
|
return Get-ParallelWorktreeGateBlockDecision -Reason "PARALLEL_WORKTREE_REMOVAL_BLOCKED: git worktree remove for '$worktreePath' requires a matching parallel checkpoint items[] record with merge_status in {merged, worktree_removed}. The checkpoint was unreadable, no matching record was found, or merge_status was not yet safe for removal."
|
|
228
283
|
}
|
|
229
284
|
|
|
@@ -26,6 +26,11 @@
|
|
|
26
26
|
[CmdletBinding()]
|
|
27
27
|
param()
|
|
28
28
|
|
|
29
|
+
# Shared command-line parser (issue #545). Dot-sourced here as well as from the parent hook
|
|
30
|
+
# so this file keeps working when a test dot-sources it directly.
|
|
31
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
32
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
33
|
+
|
|
29
34
|
function Test-PrAuthorReceiptVerification {
|
|
30
35
|
<#
|
|
31
36
|
.SYNOPSIS
|
|
@@ -166,16 +171,43 @@ function Get-PrAuthorBypassReason {
|
|
|
166
171
|
[bool] $ContextExists
|
|
167
172
|
)
|
|
168
173
|
|
|
169
|
-
# Only act on gh pr create or gh pr edit subcommands.
|
|
170
|
-
|
|
171
|
-
|
|
174
|
+
# Only act on gh pr create or gh pr edit subcommands. The test is structural: a segment
|
|
175
|
+
# must INVOKE gh with the subcommand path, so quoted prose that merely mentions the
|
|
176
|
+
# phrase no longer triggers the gate, and a relocating spelling that carries a gh global
|
|
177
|
+
# option between the command word and the subcommand now does.
|
|
178
|
+
$isPrCreate = Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create')
|
|
179
|
+
$isPrEdit = Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'edit')
|
|
172
180
|
|
|
173
181
|
if (-not $isPrCreate -and -not $isPrEdit) {
|
|
174
182
|
return $null
|
|
175
183
|
}
|
|
176
184
|
|
|
177
|
-
|
|
178
|
-
|
|
185
|
+
# Flag presence comes from the matched segment's tokens. Exact token comparison is what
|
|
186
|
+
# keeps '--body' from matching '--body-file'; the previous negative lookahead expressed
|
|
187
|
+
# the same distinction over raw text.
|
|
188
|
+
$subcommandPath = if ($isPrCreate) { @('pr', 'create') } else { @('pr', 'edit') }
|
|
189
|
+
$hasBodyFile = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--body-file'
|
|
190
|
+
$hasInlineBody = Test-CommandLineFlag -CommandText $CommandText -CommandWord 'gh' -SubcommandPath $subcommandPath -FlagName '--body'
|
|
191
|
+
|
|
192
|
+
# Raw-scan fallback for a wrapper-led, live-substitution, or unbalanced segment. A
|
|
193
|
+
# wrapper's quoted argument collapses into ONE token, so both flags read absent there and
|
|
194
|
+
# the gh pr edit no-body branch below allowed bash -c "gh pr edit 42 --body 'x'". The
|
|
195
|
+
# --body-file test runs first and wins, so a --body-file carried inside a wrapper is never
|
|
196
|
+
# misread as an inline body. That ordering restores the pre-parser routing, in which one
|
|
197
|
+
# whole-text --body-file match set $hasBodyFile for exactly this input.
|
|
198
|
+
if (-not $hasBodyFile -and -not $hasInlineBody) {
|
|
199
|
+
$comparison = [System.StringComparison]::OrdinalIgnoreCase
|
|
200
|
+
foreach ($segment in @(Read-CommandLineSegment -CommandText $CommandText)) {
|
|
201
|
+
if (-not (Test-CommandLineSegmentRawScan -Segment $segment)) {
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
if ($segment.ScanText.IndexOf('--body-file', $comparison) -ge 0) {
|
|
205
|
+
$hasBodyFile = $true
|
|
206
|
+
} elseif ($segment.ScanText.IndexOf('--body', $comparison) -ge 0) {
|
|
207
|
+
$hasInlineBody = $true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
179
211
|
|
|
180
212
|
# Case A: gh pr create OR gh pr edit with inline --body (not --body-file). Evaluated before the
|
|
181
213
|
# gh pr edit no-body allow short-circuit so inline-body edits are blocked, not allowed.
|
package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1
CHANGED
|
@@ -9,6 +9,11 @@
|
|
|
9
9
|
structural extraction only: no logic, decision behavior, or reason-string wording changed.
|
|
10
10
|
#>
|
|
11
11
|
|
|
12
|
+
# Shared command-line parser (issue #545). Dot-sourced here as well as from the parent hook
|
|
13
|
+
# so this file keeps working when a test dot-sources it directly.
|
|
14
|
+
. (Join-Path $PSScriptRoot 'hook-command-scanner.ps1')
|
|
15
|
+
. (Join-Path $PSScriptRoot 'hook-command-invocation.ps1')
|
|
16
|
+
|
|
12
17
|
function Get-PrAuthorCheckpointContent {
|
|
13
18
|
<#
|
|
14
19
|
.SYNOPSIS
|
|
@@ -64,7 +69,9 @@ function Test-EpicBaseBranchOverride {
|
|
|
64
69
|
|
|
65
70
|
# The epic-mode base-branch override only constrains gh pr create; gh pr edit does not
|
|
66
71
|
# re-target the base branch, so non-create commands are out of scope for this check.
|
|
67
|
-
|
|
72
|
+
# The test is structural, so a quoted mention of the phrase is out of scope and a
|
|
73
|
+
# relocating spelling carrying a gh global option is in scope (issue #545).
|
|
74
|
+
if (-not (Test-CommandLineInvocation -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create'))) {
|
|
68
75
|
return $null
|
|
69
76
|
}
|
|
70
77
|
|
|
@@ -96,7 +103,11 @@ function Test-EpicBaseBranchOverride {
|
|
|
96
103
|
return "EPIC_BASE_BRANCH_MISMATCH: checkpoint has epic_mode == true but no ``epic_context.integration_branch`` is recorded; ``gh pr create`` cannot be verified against the required ``--base`` value."
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
|
|
106
|
+
# The --base value is taken from the matched segment's tokens, so a chained segment that
|
|
107
|
+
# happens to carry a --base token contributes nothing here. The comparison stays
|
|
108
|
+
# case-sensitive, as the previous -cnotmatch form was, because a branch name is.
|
|
109
|
+
$baseValue = Get-CommandLineFlagValue -CommandText $CommandText -CommandWord 'gh' -SubcommandPath @('pr', 'create') -FlagName '--base'
|
|
110
|
+
if ($null -eq $baseValue -or $baseValue -cne $integrationBranch) {
|
|
100
111
|
return "EPIC_BASE_BRANCH_MISMATCH: ``gh pr create`` must pass ``--base $integrationBranch`` (``epic_context.integration_branch``) under ``epic_mode``; the command does not carry a matching ``--base`` argument."
|
|
101
112
|
}
|
|
102
113
|
|