@danmoisan/drm-copilot-mcp 1.0.26 → 1.1.0
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 +542 -42
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/hooks/check-powershell-test-purity.ps1 +17 -21
- package/resources/claude-customizations/.claude/hooks/check-python-test-purity.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-checkpoint-monotonic.ps1 +17 -19
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-discovery-artifact-gate.ps1 +18 -19
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +54 -32
- package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate.ps1 +172 -24
- package/resources/claude-customizations/.claude/hooks/enforce-epic-wave-barrier.ps1 +51 -22
- package/resources/claude-customizations/.claude/hooks/enforce-epic-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-evidence-locations.ps1 +71 -28
- package/resources/claude-customizations/.claude/hooks/enforce-feature-folder-order.ps1 +68 -23
- package/resources/claude-customizations/.claude/hooks/enforce-mermaid-validation.ps1 +402 -0
- package/resources/claude-customizations/.claude/hooks/enforce-model-routing-receipt.ps1 +20 -22
- package/resources/claude-customizations/.claude/hooks/enforce-orchestration-preimplementation-gate.ps1 +59 -14
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-abandon-gate.ps1 +17 -20
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier-helpers.ps1 +278 -0
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-cohort-barrier.ps1 +55 -271
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-drift-gate.ps1 +57 -22
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +56 -19
- package/resources/claude-customizations/.claude/hooks/enforce-powershell-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +228 -0
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.ps1 +68 -225
- package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1 +161 -34
- package/resources/claude-customizations/.claude/hooks/enforce-promotion-mcp-only.ps1 +59 -22
- package/resources/claude-customizations/.claude/hooks/enforce-python-batch-budget.ps1 +70 -27
- package/resources/claude-customizations/.claude/hooks/validate-bash.ps1 +32 -18
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadius.psm1 +105 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusConfig.psm1 +32 -52
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusExtraction.psm1 +107 -99
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusNormalization.psm1 +295 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +9 -3
- package/resources/claude-customizations/.claude/lib/hook-payload/HookPayload.psm1 +494 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidGrammar.psm1 +491 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidLineScanner.psm1 +488 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidMarkdownFences.psm1 +298 -0
- package/resources/claude-customizations/.claude/lib/mermaid/MermaidValidation.psm1 +496 -0
- package/resources/claude-customizations/.claude/rules/mermaid.md +142 -0
- package/resources/claude-customizations/.claude/rules/parallel-orchestration.md +124 -1
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +116 -0
- package/resources/claude-customizations/.claude/settings.json +5 -0
- package/resources/claude-customizations/.claude/skills/atomic-plan-contract/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/claude-customizations/.claude/skills/feature-promotion-lifecycle/SKILL.md +6 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/SKILL.md +184 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/c4.md +50 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/class.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/er.md +56 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/flowchart.md +68 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/gantt.md +51 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/other-types.md +82 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/pie.md +32 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/sequence.md +63 -0
- package/resources/claude-customizations/.claude/skills/mermaid-diagram/references/state.md +49 -0
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +8 -7
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +24 -4
- package/resources/claude-customizations/config/blast-radius.json +16 -2
- package/resources/claude-customizations/pack-manifests/core.json +22 -1
- package/resources/codex-and-agents-customizations/.agents/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/customizations/.github/skills/evidence-and-timestamp-conventions/SKILL.md +13 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +31 -1
|
@@ -4,9 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
.DESCRIPTION
|
|
6
6
|
Invoked by the Claude Code PreToolUse hook before any Bash command runs. Reads
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
pr-author skill workflow.
|
|
7
|
+
the hook payload through the shared reader, extracts the attempted command from the
|
|
8
|
+
envelope's nested tool_input, and blocks gh pr create / gh pr edit commands that
|
|
9
|
+
bypass the pr-author skill workflow.
|
|
10
10
|
|
|
11
11
|
Required sequence:
|
|
12
12
|
1. mcp__drm-copilot__collect_pr_context writes artifacts/pr_context.summary.txt
|
|
@@ -43,6 +43,8 @@
|
|
|
43
43
|
[CmdletBinding()]
|
|
44
44
|
param()
|
|
45
45
|
|
|
46
|
+
|
|
47
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
46
48
|
$script:PrContextArtifactPath = 'artifacts/pr_context.summary.txt'
|
|
47
49
|
$script:OrchestratorStateCheckpointPath = 'artifacts/orchestration/orchestrator-state.json'
|
|
48
50
|
|
|
@@ -141,235 +143,39 @@ function Get-PrContextSummaryLastWriteUtc {
|
|
|
141
143
|
|
|
142
144
|
. (Join-Path $PSScriptRoot 'enforce-pr-author-skill.epic-base-branch.ps1')
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
Verify the SHA-256 receipt and return a block-reason string, or $null when verified.
|
|
148
|
-
.DESCRIPTION
|
|
149
|
-
Runs the six ordered receipt checks on the --body-file-with-context path. Each check is its
|
|
150
|
-
own short-circuiting branch; the first failure returns its reason code:
|
|
151
|
-
1. PR_BODY_PATH_NONCANONICAL - --body-file path does not match the canonical
|
|
152
|
-
artifacts/pr_body_<N>.md pattern (case-sensitive).
|
|
153
|
-
2. PR_AUTHOR_RECEIPT_MISSING - sibling artifacts/pr_body_<N>.receipt.json absent.
|
|
154
|
-
3. PR_AUTHOR_RECEIPT_NUMBER_MISMATCH- receipt.number (integer) != <N> from the path.
|
|
155
|
-
4. PR_AUTHOR_RECEIPT_HASH_MISMATCH - inline SHA-256 (lowercase hex) of the body bytes
|
|
156
|
-
!= receipt.sha256.
|
|
157
|
-
5. PR_AUTHOR_RECEIPT_STALE - receipt.created_at (UTC) not strictly newer than the
|
|
158
|
-
context summary last-write time.
|
|
159
|
-
6. EPIC_BASE_BRANCH_MISMATCH - under epic_mode, gh pr create does not carry a
|
|
160
|
-
matching --base <epic_context.integration_branch>.
|
|
161
|
-
Returns $null when all six checks pass (allow). All disk access flows through the four
|
|
162
|
-
injectable seams (Get-PrBodyFileBytes, Get-PrAuthorReceiptContent,
|
|
163
|
-
Get-PrContextSummaryLastWriteUtc, Get-PrAuthorCheckpointContent); SHA-256 is computed
|
|
164
|
-
inline. This is a policy-level integrity check, not a cryptographic control: any actor
|
|
165
|
-
with Write access to artifacts/ can replace the body file and the receipt together.
|
|
166
|
-
.PARAMETER CommandText
|
|
167
|
-
The Bash command text containing the --body-file argument.
|
|
168
|
-
.OUTPUTS
|
|
169
|
-
System.String or $null
|
|
170
|
-
#>
|
|
171
|
-
[CmdletBinding()]
|
|
172
|
-
[OutputType([string])]
|
|
173
|
-
param(
|
|
174
|
-
[Parameter(Mandatory)]
|
|
175
|
-
[string] $CommandText
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
# Check 1: the --body-file argument must match the canonical artifacts/pr_body_<N>.md pattern.
|
|
179
|
-
# The match is case-sensitive (-cmatch) so a non-canonical path is rejected before any read.
|
|
180
|
-
if ($CommandText -cnotmatch '--body-file\s+artifacts/pr_body_(\d+)\.md\b') {
|
|
181
|
-
return "PR_BODY_PATH_NONCANONICAL: ``--body-file`` must reference a canonical ``artifacts/pr_body_<N>.md`` file produced by the pr-author skill. The path supplied does not match ``artifacts/pr_body_<N>.md``."
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
$bodyNumber = [int]$Matches[1]
|
|
185
|
-
$bodyFilePath = "artifacts/pr_body_$bodyNumber.md"
|
|
186
|
-
$receiptFilePath = "artifacts/pr_body_$bodyNumber.receipt.json"
|
|
187
|
-
|
|
188
|
-
# Check 2: the sibling receipt file must exist (read via the injectable seam).
|
|
189
|
-
$receiptRaw = Get-PrAuthorReceiptContent -ReceiptFilePath $receiptFilePath
|
|
190
|
-
if ([string]::IsNullOrWhiteSpace($receiptRaw)) {
|
|
191
|
-
return "PR_AUTHOR_RECEIPT_MISSING: ``$receiptFilePath`` is absent. The pr-author agent must write the SHA-256 receipt alongside ``$bodyFilePath`` before issuing ``gh pr create``/``gh pr edit --body-file``."
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
# A receipt that is present but not valid JSON cannot be verified; treat it as missing content.
|
|
195
|
-
try {
|
|
196
|
-
$receipt = $receiptRaw | ConvertFrom-Json -ErrorAction Stop
|
|
197
|
-
} catch {
|
|
198
|
-
return "PR_AUTHOR_RECEIPT_MISSING: ``$receiptFilePath`` is not valid JSON. The pr-author agent must write a well-formed receipt with ``number``, ``sha256``, and ``created_at``."
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
# Check 3: the receipt number must equal the <N> extracted from the canonical path.
|
|
202
|
-
$receiptNumber = $null
|
|
203
|
-
$parsedNumber = 0
|
|
204
|
-
if ([int]::TryParse([string]$receipt.number, [ref] $parsedNumber)) {
|
|
205
|
-
$receiptNumber = $parsedNumber
|
|
206
|
-
}
|
|
207
|
-
if ($receiptNumber -ne $bodyNumber) {
|
|
208
|
-
return "PR_AUTHOR_RECEIPT_NUMBER_MISMATCH: ``$receiptFilePath`` ``number`` ($($receipt.number)) does not equal the body-file number ($bodyNumber). The receipt must bind to ``$bodyFilePath``."
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
# Check 4: the inline SHA-256 (lowercase hex) of the body bytes must equal receipt.sha256.
|
|
212
|
-
$bodyBytes = Get-PrBodyFileBytes -BodyFilePath $bodyFilePath
|
|
213
|
-
if ($null -eq $bodyBytes) {
|
|
214
|
-
return "PR_AUTHOR_RECEIPT_HASH_MISMATCH: ``$bodyFilePath`` could not be read to verify its SHA-256 against ``$receiptFilePath``."
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
$sha256 = [System.Security.Cryptography.SHA256]::Create()
|
|
218
|
-
try {
|
|
219
|
-
$hashBytes = $sha256.ComputeHash($bodyBytes)
|
|
220
|
-
} finally {
|
|
221
|
-
$sha256.Dispose()
|
|
222
|
-
}
|
|
223
|
-
$computedHash = ([System.BitConverter]::ToString($hashBytes) -replace '-', '').ToLowerInvariant()
|
|
224
|
-
|
|
225
|
-
if ($computedHash -ne ([string]$receipt.sha256).ToLowerInvariant()) {
|
|
226
|
-
return "PR_AUTHOR_RECEIPT_HASH_MISMATCH: the SHA-256 of ``$bodyFilePath`` does not equal ``sha256`` in ``$receiptFilePath``. The body file was modified after the receipt was written."
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
# Check 5: receipt.created_at (UTC) must be strictly newer than the context summary last-write.
|
|
230
|
-
$createdAt = [DateTime]::MinValue
|
|
231
|
-
$createdParsed = [DateTime]::TryParse(
|
|
232
|
-
[string]$receipt.created_at,
|
|
233
|
-
[System.Globalization.CultureInfo]::InvariantCulture,
|
|
234
|
-
[System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
|
|
235
|
-
[ref] $createdAt)
|
|
236
|
-
if (-not $createdParsed) {
|
|
237
|
-
return "PR_AUTHOR_RECEIPT_STALE: ``$receiptFilePath`` has a missing or unparseable ``created_at``. The pr-author agent must record a UTC ISO-8601 ``created_at`` strictly newer than ``$script:PrContextArtifactPath``."
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
$contextLastWrite = Get-PrContextSummaryLastWriteUtc
|
|
241
|
-
if (($null -eq $contextLastWrite) -or ($createdAt -le $contextLastWrite)) {
|
|
242
|
-
return "PR_AUTHOR_RECEIPT_STALE: ``$receiptFilePath`` ``created_at`` is not strictly newer than the last-write time of ``$script:PrContextArtifactPath``. The pr-author agent must regenerate the body and receipt after refreshing the PR context."
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
# Check 6: under epic_mode, gh pr create must carry a matching --base override.
|
|
246
|
-
$epicBaseBranchReason = Test-EpicBaseBranchOverride -CommandText $CommandText
|
|
247
|
-
if ($epicBaseBranchReason) {
|
|
248
|
-
return $epicBaseBranchReason
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return $null
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function Get-PrAuthorBypassReason {
|
|
255
|
-
<#
|
|
256
|
-
.SYNOPSIS
|
|
257
|
-
Inspect the command text and return a block reason string, or $null when the command is allowed.
|
|
258
|
-
.DESCRIPTION
|
|
259
|
-
Returns PR_AUTHOR_SKILL_BLOCKED when gh pr create or gh pr edit is run with --body (inline,
|
|
260
|
-
no --body-file), or when gh pr create is run with no body flag at all. Returns
|
|
261
|
-
PR_CONTEXT_MISSING when --body-file is present but the context artifact does not exist on
|
|
262
|
-
disk. When --body-file is present and the context artifact exists, verifies the SHA-256
|
|
263
|
-
receipt via Test-PrAuthorReceiptVerification and returns its block reason
|
|
264
|
-
(PR_BODY_PATH_NONCANONICAL / PR_AUTHOR_RECEIPT_*) when verification fails. Returns $null for
|
|
265
|
-
all allowed patterns. Cases A, B, and C are evaluated first and unchanged; receipt
|
|
266
|
-
verification only extends the previously-allowed --body-file-with-context path.
|
|
267
|
-
.PARAMETER CommandText
|
|
268
|
-
The Bash command text extracted from CLAUDE_TOOL_INPUT.
|
|
269
|
-
.PARAMETER ContextExists
|
|
270
|
-
Whether artifacts/pr_context.summary.txt currently exists on disk.
|
|
271
|
-
.OUTPUTS
|
|
272
|
-
System.String or $null
|
|
273
|
-
#>
|
|
274
|
-
[CmdletBinding()]
|
|
275
|
-
[OutputType([string])]
|
|
276
|
-
param(
|
|
277
|
-
[Parameter(Mandatory)]
|
|
278
|
-
[string] $CommandText,
|
|
279
|
-
|
|
280
|
-
[Parameter(Mandatory)]
|
|
281
|
-
[bool] $ContextExists
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
# Only act on gh pr create or gh pr edit subcommands.
|
|
285
|
-
$isPrCreate = $CommandText -match '(?i)\bgh\s+pr\s+create\b'
|
|
286
|
-
$isPrEdit = $CommandText -match '(?i)\bgh\s+pr\s+edit\b'
|
|
287
|
-
|
|
288
|
-
if (-not $isPrCreate -and -not $isPrEdit) {
|
|
289
|
-
return $null
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
$hasBodyFile = $CommandText -match '(?i)--body-file\b'
|
|
293
|
-
$hasInlineBody = $CommandText -match '(?i)--body(?!-file)\b'
|
|
294
|
-
|
|
295
|
-
# Case A: gh pr create OR gh pr edit with inline --body (not --body-file). Evaluated before the
|
|
296
|
-
# gh pr edit no-body allow short-circuit so inline-body edits are blocked, not allowed.
|
|
297
|
-
if (($isPrCreate -or $isPrEdit) -and $hasInlineBody -and -not $hasBodyFile) {
|
|
298
|
-
return "PR_AUTHOR_SKILL_BLOCKED: ``gh pr create`` and ``gh pr edit`` must use ``--body-file`` with a file produced by the pr-author skill from ``$script:PrContextArtifactPath``. Run ``mcp__drm-copilot__collect_pr_context`` to generate the context file, apply the pr-author skill to produce ``artifacts/pr_body_<N>.md``, then pass that file via ``--body-file``."
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if ($isPrCreate) {
|
|
302
|
-
# Case B: gh pr create with no body flag at all.
|
|
303
|
-
if (-not $hasInlineBody -and -not $hasBodyFile) {
|
|
304
|
-
return "PR_AUTHOR_SKILL_BLOCKED: New PRs require ``--body-file``. Run ``mcp__drm-copilot__collect_pr_context`` to generate ``$script:PrContextArtifactPath``, apply the pr-author skill to produce ``artifacts/pr_body_<N>.md``, then pass that file via ``--body-file``."
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if ($isPrEdit) {
|
|
309
|
-
# gh pr edit with no --body or --body-file (e.g., --title, --add-label, --reviewer) is allowed.
|
|
310
|
-
if (-not $hasInlineBody -and -not $hasBodyFile) {
|
|
311
|
-
return $null
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
# Case C: --body-file present but context artifact is absent.
|
|
316
|
-
if ($hasBodyFile -and -not $ContextExists) {
|
|
317
|
-
return "PR_CONTEXT_MISSING: ``$script:PrContextArtifactPath`` is absent. Run ``mcp__drm-copilot__collect_pr_context`` before creating or editing the PR body."
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
# Orchestrator-state preflight: runs inside this same PreToolUse hook (so it cannot be
|
|
321
|
-
# bypassed by invoking gh pr create/edit directly) before receipt verification.
|
|
322
|
-
if ($hasBodyFile -and $ContextExists) {
|
|
323
|
-
$preflightResult = Invoke-OrchestratorStatePreflight -CheckpointPath $script:OrchestratorStateCheckpointPath
|
|
324
|
-
if ($preflightResult.HasErrors) {
|
|
325
|
-
$preflightSummary = if ([string]::IsNullOrWhiteSpace($preflightResult.ErrorText)) {
|
|
326
|
-
"checkpoint missing at $script:OrchestratorStateCheckpointPath"
|
|
327
|
-
} else {
|
|
328
|
-
$preflightResult.ErrorText
|
|
329
|
-
}
|
|
330
|
-
return "ORCHESTRATOR_STATE_PREFLIGHT_FAILED: $preflightSummary"
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
# Receipt verification: extends, and does not replace, the previously-allowed path.
|
|
335
|
-
if ($hasBodyFile -and $ContextExists) {
|
|
336
|
-
$receiptReason = Test-PrAuthorReceiptVerification -CommandText $CommandText
|
|
337
|
-
if ($receiptReason) {
|
|
338
|
-
return $receiptReason
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
return $null
|
|
343
|
-
}
|
|
146
|
+
# Dot-source the receipt-verification and bypass-reason helpers. Guarded so dot-sourcing this
|
|
147
|
+
# hook in tests loads the helpers too (issue #501 headroom split).
|
|
148
|
+
. (Join-Path $PSScriptRoot 'enforce-pr-author-skill-helpers.ps1')
|
|
344
149
|
|
|
345
150
|
function Invoke-PrAuthorSkillDecision {
|
|
346
151
|
<#
|
|
347
152
|
.SYNOPSIS
|
|
348
|
-
Parse
|
|
153
|
+
Parse the PreToolUse envelope and return an allow-or-block decision.
|
|
154
|
+
.DESCRIPTION
|
|
155
|
+
Envelope-level anomalies fail closed as a deny; missing command text inside a
|
|
156
|
+
well-formed tool_input remains an allow, because that is this gate's scope filter.
|
|
349
157
|
.PARAMETER ToolInputRaw
|
|
350
|
-
The raw JSON
|
|
158
|
+
The raw JSON hook payload acquired by Read-ClaudeHookRawPayload.
|
|
351
159
|
.OUTPUTS
|
|
352
160
|
System.Collections.Specialized.OrderedDictionary
|
|
353
|
-
.NOTES
|
|
354
|
-
Missing tool input or missing command text is treated as allow.
|
|
355
161
|
#>
|
|
356
162
|
[CmdletBinding()]
|
|
357
163
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
358
164
|
param(
|
|
165
|
+
[AllowNull()]
|
|
166
|
+
[AllowEmptyString()]
|
|
359
167
|
[string] $ToolInputRaw
|
|
360
168
|
)
|
|
361
169
|
|
|
362
|
-
|
|
363
|
-
|
|
170
|
+
$payload = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
171
|
+
if (-not $payload.IsValid) {
|
|
172
|
+
return Get-PrAuthorSkillBlockDecision -Reason (
|
|
173
|
+
'PR_AUTHOR_SKILL_BLOCKED: payload anomaly - ' +
|
|
174
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $payload.Anomaly) +
|
|
175
|
+
'. The gate fails closed on an envelope it cannot read.')
|
|
364
176
|
}
|
|
365
177
|
|
|
366
|
-
|
|
367
|
-
$toolInput = $ToolInputRaw | ConvertFrom-Json -ErrorAction Stop
|
|
368
|
-
} catch {
|
|
369
|
-
throw "enforce-pr-author-skill hook received malformed JSON in CLAUDE_TOOL_INPUT: $_"
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
$commandText = $toolInput.command
|
|
178
|
+
$commandText = Get-ClaudeHookToolInputString -ToolInput $payload.Value -Name 'command'
|
|
373
179
|
if (-not $commandText) {
|
|
374
180
|
return Get-PrAuthorSkillAllowDecision
|
|
375
181
|
}
|
|
@@ -433,7 +239,7 @@ function Test-PrAuthorBypassRequired {
|
|
|
433
239
|
.SYNOPSIS
|
|
434
240
|
Return $true when a Bash command requires the pr-author skill to run first.
|
|
435
241
|
.PARAMETER CommandText
|
|
436
|
-
The Bash command text extracted from
|
|
242
|
+
The Bash command text extracted from the envelope's tool_input.
|
|
437
243
|
.PARAMETER ContextExists
|
|
438
244
|
Whether artifacts/pr_context.summary.txt currently exists on disk.
|
|
439
245
|
.OUTPUTS
|
|
@@ -452,18 +258,55 @@ function Test-PrAuthorBypassRequired {
|
|
|
452
258
|
return ($null -ne (Get-PrAuthorBypassReason -CommandText $CommandText -ContextExists $ContextExists))
|
|
453
259
|
}
|
|
454
260
|
|
|
261
|
+
function Invoke-PrAuthorSkillEntryPoint {
|
|
262
|
+
<#
|
|
263
|
+
.SYNOPSIS
|
|
264
|
+
Runs the hook decision and returns the process exit code.
|
|
265
|
+
.DESCRIPTION
|
|
266
|
+
Acquires the payload through the shared reader unless the caller supplies
|
|
267
|
+
one, emits the compact decision JSON, and returns 0. It never returns 1:
|
|
268
|
+
exit 1 is non-blocking for PreToolUse, so every anomaly is already a deny
|
|
269
|
+
decision by the time control reaches here. The function does not call exit;
|
|
270
|
+
the thin tail converts the returned code into a process exit.
|
|
271
|
+
.PARAMETER ToolInputRaw
|
|
272
|
+
Optional pre-acquired payload text. When omitted the ReadPayload seam runs.
|
|
273
|
+
.PARAMETER ReadPayload
|
|
274
|
+
Seam for payload acquisition, so tests can drive the empty-on-all-transports
|
|
275
|
+
case without touching a console.
|
|
276
|
+
.OUTPUTS
|
|
277
|
+
System.Int32
|
|
278
|
+
#>
|
|
279
|
+
[CmdletBinding()]
|
|
280
|
+
[OutputType([int])]
|
|
281
|
+
param(
|
|
282
|
+
[AllowNull()]
|
|
283
|
+
[AllowEmptyString()]
|
|
284
|
+
[string] $ToolInputRaw,
|
|
285
|
+
|
|
286
|
+
[scriptblock] $ReadPayload = { Read-ClaudeHookRawPayload }
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
if (-not $PSBoundParameters.ContainsKey('ToolInputRaw')) {
|
|
290
|
+
$ToolInputRaw = [string](& $ReadPayload)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
$decision = Invoke-PrAuthorSkillDecision -ToolInputRaw $ToolInputRaw
|
|
294
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
295
|
+
|
|
296
|
+
return 0
|
|
297
|
+
}
|
|
298
|
+
|
|
455
299
|
# Allow dot-sourcing in tests without executing the entrypoint.
|
|
456
300
|
if ($MyInvocation.InvocationName -eq '.') {
|
|
457
301
|
return
|
|
458
302
|
}
|
|
459
303
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
304
|
+
# The entry point returns its [int] exit code as the last pipeline element and the
|
|
305
|
+
# decision JSON before it. `exit (<call>)` would capture BOTH into the exit
|
|
306
|
+
# expression and emit nothing, so the decision is written explicitly here first.
|
|
307
|
+
$entryPointResult = @(Invoke-PrAuthorSkillEntryPoint)
|
|
308
|
+
if ($entryPointResult.Count -gt 1) {
|
|
309
|
+
$entryPointResult[0..($entryPointResult.Count - 2)] | Write-Output
|
|
465
310
|
}
|
|
466
311
|
|
|
467
|
-
$
|
|
468
|
-
|
|
469
|
-
exit 0
|
|
312
|
+
exit ([int]$entryPointResult[-1])
|
package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
<#
|
|
2
2
|
.SYNOPSIS
|
|
3
3
|
Pre-tool-use hook that blocks atomic-planner delegations when the target
|
|
4
|
-
feature folder does not yet contain
|
|
4
|
+
feature folder does not yet contain the prd-feature outputs its persisted
|
|
5
|
+
work mode requires.
|
|
5
6
|
|
|
6
7
|
.DESCRIPTION
|
|
7
8
|
Invoked by the Claude Code PreToolUse hook on the Agent (Task) tool. Reads
|
|
8
|
-
tool
|
|
9
|
-
|
|
9
|
+
the tool payload through the shared hook-payload reader (stdin first, with
|
|
10
|
+
environment-variable fallback) and takes the tool arguments from the
|
|
11
|
+
envelope's nested tool_input object. Activates only when subagent_type is
|
|
12
|
+
'atomic-planner'.
|
|
10
13
|
|
|
11
14
|
Feature folder resolution order:
|
|
12
15
|
1. Scan the prompt text for any path matching
|
|
@@ -18,11 +21,25 @@
|
|
|
18
21
|
3. If neither yields a folder, block with a reason instructing the caller
|
|
19
22
|
to reference a feature folder explicitly.
|
|
20
23
|
|
|
21
|
-
Once the folder is resolved, the hook
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
Once the folder is resolved, the hook reads the persisted work-mode marker
|
|
25
|
+
(`- Work Mode: minor-audit|full-feature|full-bug|full`) from that folder's
|
|
26
|
+
issue.md, per the mode contract in
|
|
27
|
+
.claude/skills/feature-promotion-lifecycle/SKILL.md, and derives the
|
|
28
|
+
required prerequisite set:
|
|
29
|
+
- full-feature -> spec.md and user-story.md are both required.
|
|
30
|
+
- full-bug -> spec.md only is required.
|
|
31
|
+
- minor-audit -> neither is required; issue.md carries the acceptance
|
|
32
|
+
criteria for this mode.
|
|
33
|
+
- marker absent, unreadable, or unrecognized -> fail closed to the
|
|
34
|
+
strictest set (spec.md and user-story.md), and the block reason states
|
|
35
|
+
that the work mode could not be determined so the operator can tell
|
|
36
|
+
this case apart from a genuine missing prerequisite. The legacy `full`
|
|
37
|
+
marker normalizes to full-feature's requirement set.
|
|
38
|
+
|
|
39
|
+
If any required file is missing, the script emits a PreToolUse JSON
|
|
40
|
+
response with hookSpecificOutput.permissionDecision='deny' and a reason
|
|
41
|
+
naming the missing file(s) and instructing the orchestrator to invoke
|
|
42
|
+
prd-feature first. Allowed delegations emit
|
|
26
43
|
hookSpecificOutput.permissionDecision='allow'.
|
|
27
44
|
|
|
28
45
|
Filesystem reads and orchestrator-state lookups go through wrapper functions
|
|
@@ -34,6 +51,8 @@
|
|
|
34
51
|
[CmdletBinding()]
|
|
35
52
|
param()
|
|
36
53
|
|
|
54
|
+
|
|
55
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
37
56
|
function Get-PrdFeatureFileExistence {
|
|
38
57
|
<#
|
|
39
58
|
.SYNOPSIS
|
|
@@ -49,6 +68,96 @@ function Get-PrdFeatureFileExistence {
|
|
|
49
68
|
return [bool](Test-Path -LiteralPath $Path -PathType Leaf)
|
|
50
69
|
}
|
|
51
70
|
|
|
71
|
+
function Get-PrdFeatureIssueContent {
|
|
72
|
+
<#
|
|
73
|
+
.SYNOPSIS
|
|
74
|
+
Wrapper around Get-Content for a feature folder's issue.md. Tests mock
|
|
75
|
+
this directly to inject work-mode marker content without touching disk.
|
|
76
|
+
#>
|
|
77
|
+
[CmdletBinding()]
|
|
78
|
+
[OutputType([string])]
|
|
79
|
+
param(
|
|
80
|
+
[Parameter(Mandatory)]
|
|
81
|
+
[string] $FeatureFolder
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
$issuePath = "$FeatureFolder/issue.md"
|
|
85
|
+
if (-not (Test-Path -LiteralPath $issuePath -PathType Leaf)) {
|
|
86
|
+
return $null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
return Get-Content -LiteralPath $issuePath -Raw -ErrorAction Stop
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return $null
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function Resolve-PrdFeatureWorkMode {
|
|
98
|
+
<#
|
|
99
|
+
.SYNOPSIS
|
|
100
|
+
Parses the persisted `- Work Mode: ...` marker out of issue.md content
|
|
101
|
+
and returns the canonical mode, or $null when the marker is absent,
|
|
102
|
+
unreadable, or unrecognized.
|
|
103
|
+
.DESCRIPTION
|
|
104
|
+
Recognizes minor-audit, full-feature, full-bug, and the legacy full
|
|
105
|
+
marker (normalized to full-feature), mirroring the regex convention
|
|
106
|
+
used by scripts/dev_tools/prompt_mode_contract.py so both runtimes
|
|
107
|
+
agree on what counts as a valid marker line.
|
|
108
|
+
#>
|
|
109
|
+
[CmdletBinding()]
|
|
110
|
+
[OutputType([string])]
|
|
111
|
+
param(
|
|
112
|
+
[AllowNull()]
|
|
113
|
+
[string] $IssueContent
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if ([string]::IsNullOrWhiteSpace($IssueContent)) {
|
|
117
|
+
return $null
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$match = [regex]::Match($IssueContent, '(?im)^-\s*Work Mode:\s*(minor-audit|full-feature|full-bug|full)\s*$')
|
|
121
|
+
if (-not $match.Success) {
|
|
122
|
+
return $null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
$rawMode = $match.Groups[1].Value
|
|
126
|
+
if ($rawMode -eq 'full') {
|
|
127
|
+
return 'full-feature'
|
|
128
|
+
}
|
|
129
|
+
return $rawMode
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Get-PrdFeatureRequiredFile {
|
|
133
|
+
<#
|
|
134
|
+
.SYNOPSIS
|
|
135
|
+
Maps a resolved work mode to the set of prd-feature output files the
|
|
136
|
+
target folder must contain before an atomic-planner delegation is
|
|
137
|
+
allowed.
|
|
138
|
+
.DESCRIPTION
|
|
139
|
+
full-feature requires spec.md and user-story.md; full-bug requires
|
|
140
|
+
spec.md only; minor-audit requires neither. A $null or unrecognized
|
|
141
|
+
mode fails closed to the strictest set (spec.md and user-story.md) so
|
|
142
|
+
an undeterminable mode never becomes permissive.
|
|
143
|
+
#>
|
|
144
|
+
[CmdletBinding()]
|
|
145
|
+
[OutputType([string[]])]
|
|
146
|
+
param(
|
|
147
|
+
[AllowNull()]
|
|
148
|
+
[string] $WorkMode
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Route on the canonical mode; anything outside the three known values
|
|
152
|
+
# (including $null) falls through to the fail-closed default case.
|
|
153
|
+
switch ($WorkMode) {
|
|
154
|
+
'full-feature' { return [string[]]@('spec.md', 'user-story.md') }
|
|
155
|
+
'full-bug' { return [string[]]@('spec.md') }
|
|
156
|
+
'minor-audit' { return [string[]]@() }
|
|
157
|
+
default { return [string[]]@('spec.md', 'user-story.md') }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
52
161
|
function Get-PrdFeatureCheckpointFolder {
|
|
53
162
|
<#
|
|
54
163
|
.SYNOPSIS
|
|
@@ -126,19 +235,22 @@ function Find-PrdFeatureFolderFromPrompt {
|
|
|
126
235
|
function Get-PrdFeatureMissingFile {
|
|
127
236
|
<#
|
|
128
237
|
.SYNOPSIS
|
|
129
|
-
Returns the
|
|
130
|
-
|
|
238
|
+
Returns the subset of $RequiredFile that is missing from the target
|
|
239
|
+
folder.
|
|
131
240
|
#>
|
|
132
241
|
[CmdletBinding()]
|
|
133
242
|
[OutputType([string[]])]
|
|
134
243
|
param(
|
|
135
244
|
[Parameter(Mandatory)]
|
|
136
|
-
[string] $FeatureFolder
|
|
245
|
+
[string] $FeatureFolder,
|
|
246
|
+
|
|
247
|
+
[Parameter(Mandatory)]
|
|
248
|
+
[AllowEmptyCollection()]
|
|
249
|
+
[string[]] $RequiredFile
|
|
137
250
|
)
|
|
138
251
|
|
|
139
|
-
$required = @('spec.md', 'user-story.md')
|
|
140
252
|
[System.Collections.Generic.List[string]] $missing = [System.Collections.Generic.List[string]]::new()
|
|
141
|
-
foreach ($name in $
|
|
253
|
+
foreach ($name in $RequiredFile) {
|
|
142
254
|
$candidate = "$FeatureFolder/$name"
|
|
143
255
|
if (-not (Get-PrdFeatureFileExistence -Path $candidate)) {
|
|
144
256
|
$missing.Add($name)
|
|
@@ -150,7 +262,7 @@ function Get-PrdFeatureMissingFile {
|
|
|
150
262
|
function Invoke-PrdFeatureBeforePlannerDecision {
|
|
151
263
|
<#
|
|
152
264
|
.SYNOPSIS
|
|
153
|
-
Parses
|
|
265
|
+
Parses the envelope's nested tool_input and returns an allow-or-block decision.
|
|
154
266
|
#>
|
|
155
267
|
[CmdletBinding()]
|
|
156
268
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
@@ -158,23 +270,25 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
158
270
|
[string] $ToolInputRaw
|
|
159
271
|
)
|
|
160
272
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
273
|
+
$envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
274
|
+
if (-not $envelope.IsValid) {
|
|
275
|
+
return [ordered]@{
|
|
276
|
+
hookSpecificOutput = [ordered]@{
|
|
277
|
+
hookEventName = 'PreToolUse'
|
|
278
|
+
permissionDecision = 'deny'
|
|
279
|
+
permissionDecisionReason = 'PRD_FEATURE_BLOCKED: payload anomaly - ' +
|
|
280
|
+
(Get-ClaudeHookPayloadAnomalyReason -Anomaly $envelope.Anomaly) +
|
|
281
|
+
'. The gate fails closed on an envelope it cannot read.'
|
|
282
|
+
}
|
|
283
|
+
}
|
|
170
284
|
}
|
|
171
285
|
|
|
172
|
-
$subagent = $
|
|
286
|
+
$subagent = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'subagent_type'
|
|
173
287
|
if (-not $subagent -or $subagent -ne 'atomic-planner') {
|
|
174
288
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
175
289
|
}
|
|
176
290
|
|
|
177
|
-
$prompt =
|
|
291
|
+
$prompt = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'prompt'
|
|
178
292
|
$folder = Find-PrdFeatureFolderFromPrompt -Prompt $prompt
|
|
179
293
|
if (-not $folder) {
|
|
180
294
|
$folder = Get-PrdFeatureCheckpointFolder
|
|
@@ -191,17 +305,36 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
191
305
|
}
|
|
192
306
|
|
|
193
307
|
$folderNormalized = ($folder -replace '\\', '/').TrimEnd('/')
|
|
194
|
-
|
|
308
|
+
|
|
309
|
+
# Derive the prerequisite set from the persisted work-mode marker rather
|
|
310
|
+
# than a fixed spec.md/user-story.md pair. A marker that cannot be read or
|
|
311
|
+
# recognized must fail closed to the strictest set, not fail open.
|
|
312
|
+
$issueContent = Get-PrdFeatureIssueContent -FeatureFolder $folderNormalized
|
|
313
|
+
$workMode = Resolve-PrdFeatureWorkMode -IssueContent $issueContent
|
|
314
|
+
$modeDetermined = [bool]$workMode
|
|
315
|
+
# Force array wrapping: PowerShell unravels a zero-element array return down
|
|
316
|
+
# the pipeline to $null, which would otherwise fail the Mandatory
|
|
317
|
+
# -RequiredFile parameter on Get-PrdFeatureMissingFile for minor-audit mode.
|
|
318
|
+
$required = @(Get-PrdFeatureRequiredFile -WorkMode $workMode)
|
|
319
|
+
|
|
320
|
+
$missing = Get-PrdFeatureMissingFile -FeatureFolder $folderNormalized -RequiredFile $required
|
|
195
321
|
if ($missing.Count -eq 0) {
|
|
196
322
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
197
323
|
}
|
|
198
324
|
|
|
199
325
|
$list = ($missing -join ', ')
|
|
326
|
+
if ($modeDetermined) {
|
|
327
|
+
$reason = "PRD_FEATURE_BLOCKED: cannot delegate to atomic-planner before prd-feature outputs are present in '$folderNormalized'. Missing: $list (work mode: $workMode). Invoke the prd-feature subagent first."
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
$reason = "PRD_FEATURE_BLOCKED: cannot delegate to atomic-planner before prd-feature outputs are present in '$folderNormalized'. Missing: $list. Work mode could not be determined from '$folderNormalized/issue.md' (marker absent, unreadable, or unrecognized); failing closed to the strictest prerequisite set (spec.md, user-story.md). Invoke the prd-feature subagent first."
|
|
331
|
+
}
|
|
332
|
+
|
|
200
333
|
return [ordered]@{
|
|
201
334
|
hookSpecificOutput = [ordered]@{
|
|
202
335
|
hookEventName = 'PreToolUse'
|
|
203
336
|
permissionDecision = 'deny'
|
|
204
|
-
permissionDecisionReason =
|
|
337
|
+
permissionDecisionReason = $reason
|
|
205
338
|
}
|
|
206
339
|
}
|
|
207
340
|
}
|
|
@@ -211,13 +344,7 @@ if ($MyInvocation.InvocationName -eq '.') {
|
|
|
211
344
|
return
|
|
212
345
|
}
|
|
213
346
|
|
|
214
|
-
|
|
215
|
-
$decision = Invoke-PrdFeatureBeforePlannerDecision -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
216
|
-
}
|
|
217
|
-
catch {
|
|
218
|
-
Write-Error $_
|
|
219
|
-
exit 1
|
|
220
|
-
}
|
|
347
|
+
$decision = Invoke-PrdFeatureBeforePlannerDecision -ToolInputRaw (Read-ClaudeHookRawPayload)
|
|
221
348
|
|
|
222
349
|
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
223
350
|
|