@danmoisan/drm-copilot-mcp 1.1.11 → 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 +366 -159
- 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/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 +33 -37
- 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 +14 -27
- package/resources/claude-customizations/.claude/hooks/enforce-parallel-worktree-removal-gate.ps1 +66 -5
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill-helpers.ps1 +134 -10
- package/resources/claude-customizations/.claude/hooks/enforce-pr-author-skill.epic-base-branch.ps1 +37 -8
- 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/lib/codex-routing/CodexDeployment.psm1 +1 -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/skills/cleanup-merged-worktrees/SKILL.md +12 -0
- 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 +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
- 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/pack-manifests/core.json +9 -0
- package/resources/codex-and-agents-customizations/.agents/skills/codex-model-routing/SKILL.md +13 -14
- package/resources/codex-and-agents-customizations/.codex/config.toml +1 -1
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-merge-gate.ps1 +193 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-epic-planning-only.ps1 +15 -5
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +163 -15
- 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 +18 -1
- 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 +26 -0
package/resources/claude-customizations/.claude/hooks/enforce-prd-feature-before-planner.ps1
CHANGED
|
@@ -12,7 +12,18 @@
|
|
|
12
12
|
'atomic-planner'.
|
|
13
13
|
|
|
14
14
|
Feature folder resolution order:
|
|
15
|
-
1.
|
|
15
|
+
1. Resolve the worktree this call pertains to from the assembled prompt and
|
|
16
|
+
description text, by portable identity only: the canonical issue-number
|
|
17
|
+
line, and a branch signal. The resolution belongs to the
|
|
18
|
+
worktree-resolution library and is not re-implemented here. No path
|
|
19
|
+
signal selects a worktree, because a merged feature folder exists in
|
|
20
|
+
every checkout branched from main and so places a call in many at once.
|
|
21
|
+
When the call carries no identity, or an identity the library cannot
|
|
22
|
+
narrow to one live worktree, the gate denies at once with that library's
|
|
23
|
+
own reason code -- the no-target code or the ambiguity code -- before any
|
|
24
|
+
document probe runs. A probe at that point would validate the call
|
|
25
|
+
against whichever root happened to be current.
|
|
26
|
+
2. Scan the prompt text for any path matching
|
|
16
27
|
docs/features/active/<token>, accepting both forward-slash and
|
|
17
28
|
backslash separators. Truncate every match to two segments past the
|
|
18
29
|
docs/features/active/ prefix -- that is, to exactly four path segments:
|
|
@@ -22,16 +33,30 @@
|
|
|
22
33
|
the same folder. A match that truncates to fewer than four segments is
|
|
23
34
|
rejected. Candidates are deduplicated preserving first-occurrence
|
|
24
35
|
order.
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
36
|
+
3. Choose among the distinct candidates by asking which one the resolved
|
|
37
|
+
worktree's own orchestrator checkpoint names. That checkpoint is the only
|
|
38
|
+
disambiguator, and neither prompt position nor a foreign session's record
|
|
39
|
+
takes that role: position would make selection an accident of wording,
|
|
40
|
+
and another session's checkpoint records its own unrelated work.
|
|
41
|
+
4. When more than one folder was cited and that checkpoint names none of
|
|
42
|
+
them, the tie is unresolved and the gate denies with the ambiguity
|
|
43
|
+
reason code rather than selecting any one of the candidates.
|
|
44
|
+
5. When the prompt named no folder at all, the resolved worktree's
|
|
45
|
+
checkpoint supplies one. It is the item's own checkpoint by
|
|
46
|
+
construction, because identity selected the worktree it sits in, so the
|
|
47
|
+
conditional guard that used to protect this step is unnecessary and has
|
|
48
|
+
been removed.
|
|
49
|
+
6. If no folder is resolved by any of the steps above, block with a reason
|
|
50
|
+
instructing the caller to reference a feature folder explicitly.
|
|
51
|
+
7. Once a folder is resolved, anchor every document probe to the resolved
|
|
52
|
+
target root, and only when that root differs from the session root. When
|
|
53
|
+
the two coincide the bare repo-relative spelling is kept, because
|
|
54
|
+
prefixing unconditionally would break every call that legitimately
|
|
55
|
+
resolves where it runs. A folder that is absent under the resolved target
|
|
56
|
+
root therefore denies on the missing-document reason for that root. It
|
|
57
|
+
never denies on the marker-is-broken reason, which is reachable only when
|
|
58
|
+
the folder does exist under that root and its work-mode marker cannot be
|
|
59
|
+
read.
|
|
35
60
|
|
|
36
61
|
Known limitation: resolution stops at the feature-folder segment, so it does
|
|
37
62
|
not descend into a version folder (v1/, v2/). No versioned folder exists
|
|
@@ -76,6 +101,16 @@ param()
|
|
|
76
101
|
|
|
77
102
|
|
|
78
103
|
Import-Module (Join-Path $PSScriptRoot '../lib/hook-payload/HookPayload.psm1') -Force
|
|
104
|
+
# Issue #669 owns worktree location and path normalisation; issue #673 owns the
|
|
105
|
+
# portable-identity resolution this gate now selects a worktree with. Both imports are
|
|
106
|
+
# unguarded on purpose: a resolution module that cannot be loaded is itself the
|
|
107
|
+
# target-not-resolvable state, and the gate must fail closed on it rather than degrade to
|
|
108
|
+
# a permissive path. Both are required rather than one: the absolute-path join the probe
|
|
109
|
+
# step below calls is exported only by the first, because a module whose exports are pinned
|
|
110
|
+
# by an explicit list does not re-export a sibling's functions to its own importer.
|
|
111
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeTargetResolution.psm1') -Force
|
|
112
|
+
Import-Module (Join-Path $PSScriptRoot '../lib/worktree-resolution/WorktreeItemResolution.psm1') -Force
|
|
113
|
+
. (Join-Path $PSScriptRoot 'enforce-prd-feature-before-planner-helpers.ps1')
|
|
79
114
|
function Get-PrdFeatureFileExistence {
|
|
80
115
|
<#
|
|
81
116
|
.SYNOPSIS
|
|
@@ -117,75 +152,6 @@ function Get-PrdFeatureIssueContent {
|
|
|
117
152
|
}
|
|
118
153
|
}
|
|
119
154
|
|
|
120
|
-
function Resolve-PrdFeatureWorkMode {
|
|
121
|
-
<#
|
|
122
|
-
.SYNOPSIS
|
|
123
|
-
Parses the persisted `- Work Mode: ...` marker out of issue.md content
|
|
124
|
-
and returns the canonical mode, or $null when the marker is absent,
|
|
125
|
-
unreadable, or unrecognized.
|
|
126
|
-
.DESCRIPTION
|
|
127
|
-
Recognizes minor-audit, full-feature, full-bug, and the legacy full
|
|
128
|
-
marker (normalized to full-feature), mirroring the regex convention
|
|
129
|
-
used by scripts/dev_tools/prompt_mode_contract.py so both runtimes
|
|
130
|
-
agree on what counts as a valid marker line.
|
|
131
|
-
#>
|
|
132
|
-
[CmdletBinding()]
|
|
133
|
-
[OutputType([string])]
|
|
134
|
-
param(
|
|
135
|
-
[AllowNull()]
|
|
136
|
-
[string] $IssueContent
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
if ([string]::IsNullOrWhiteSpace($IssueContent)) {
|
|
140
|
-
return $null
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
$match = [regex]::Match($IssueContent, '(?im)^-\s*Work Mode:\s*(minor-audit|full-feature|full-bug|full)\s*$')
|
|
144
|
-
if (-not $match.Success) {
|
|
145
|
-
return $null
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
$rawMode = $match.Groups[1].Value
|
|
149
|
-
if ($rawMode -eq 'full') {
|
|
150
|
-
return 'full-feature'
|
|
151
|
-
}
|
|
152
|
-
return $rawMode
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function Get-PrdFeatureRequiredFile {
|
|
156
|
-
<#
|
|
157
|
-
.SYNOPSIS
|
|
158
|
-
Maps a resolved work mode to the set of prd-feature output files the
|
|
159
|
-
target folder must contain before an atomic-planner delegation is
|
|
160
|
-
allowed.
|
|
161
|
-
.DESCRIPTION
|
|
162
|
-
full-feature requires spec.md and user-story.md; full-bug requires
|
|
163
|
-
spec.md only; minor-audit requires neither.
|
|
164
|
-
|
|
165
|
-
The default arm returns spec.md alone. It is not reached from the
|
|
166
|
-
decision path for an undeterminable mode, which denies on its own branch
|
|
167
|
-
without probing at all; the arm exists so a direct caller passing a $null
|
|
168
|
-
or unrecognized mode never receives a permissive empty set, and it must
|
|
169
|
-
not name user-story.md, because that document is required to be ABSENT
|
|
170
|
-
for full-bug and minor-audit work.
|
|
171
|
-
#>
|
|
172
|
-
[CmdletBinding()]
|
|
173
|
-
[OutputType([string[]])]
|
|
174
|
-
param(
|
|
175
|
-
[AllowNull()]
|
|
176
|
-
[string] $WorkMode
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
# Route on the canonical mode; anything outside the three known values
|
|
180
|
-
# (including $null) falls through to the fail-closed default case.
|
|
181
|
-
switch ($WorkMode) {
|
|
182
|
-
'full-feature' { return [string[]]@('spec.md', 'user-story.md') }
|
|
183
|
-
'full-bug' { return [string[]]@('spec.md') }
|
|
184
|
-
'minor-audit' { return [string[]]@() }
|
|
185
|
-
default { return [string[]]@('spec.md') }
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
|
|
189
155
|
function Get-PrdFeatureCheckpointFolder {
|
|
190
156
|
<#
|
|
191
157
|
.SYNOPSIS
|
|
@@ -195,7 +161,8 @@ function Get-PrdFeatureCheckpointFolder {
|
|
|
195
161
|
[CmdletBinding()]
|
|
196
162
|
[OutputType([string])]
|
|
197
163
|
param(
|
|
198
|
-
[
|
|
164
|
+
[Parameter(Mandatory)]
|
|
165
|
+
[string] $CheckpointPath
|
|
199
166
|
)
|
|
200
167
|
|
|
201
168
|
if (-not (Test-Path -LiteralPath $CheckpointPath -PathType Leaf)) {
|
|
@@ -216,133 +183,138 @@ function Get-PrdFeatureCheckpointFolder {
|
|
|
216
183
|
return $null
|
|
217
184
|
}
|
|
218
185
|
|
|
219
|
-
function
|
|
186
|
+
function Get-PrdFeatureTargetDecision {
|
|
220
187
|
<#
|
|
221
188
|
.SYNOPSIS
|
|
222
|
-
|
|
223
|
-
truncates each to exactly four path segments, and returns the selected
|
|
224
|
-
feature folder. Returns $null when no token truncates to four segments.
|
|
189
|
+
Builds a target-resolution deny, carrying the caller's reason code.
|
|
225
190
|
.DESCRIPTION
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
191
|
+
The code is supplied by the caller rather than chosen here, because two are
|
|
192
|
+
now reachable: the no-target code when the call carries no identity or an
|
|
193
|
+
identity that places in no live worktree, and the ambiguity code when
|
|
194
|
+
several live worktrees match or two identities disagree. Both are read from
|
|
195
|
+
the resolution library's accessors, so the gate and the library can never
|
|
196
|
+
disagree about either spelling.
|
|
197
|
+
|
|
198
|
+
Every reason built here is distinct from the missing-document and marker
|
|
199
|
+
reasons, which name a resolved folder rather than a resolution failure.
|
|
200
|
+
|
|
201
|
+
The remedy sentence names the two identity lines rather than an absolute
|
|
202
|
+
path. Citing a path is no longer an action that selects anything, so the
|
|
203
|
+
previous remedy would have instructed the caller to do something inert.
|
|
204
|
+
.PARAMETER ReasonCode
|
|
205
|
+
The resolution reason code to carry, from one of the library's accessors.
|
|
206
|
+
.PARAMETER Detail
|
|
207
|
+
The library's own detail text for the unresolved state.
|
|
238
208
|
#>
|
|
239
209
|
[CmdletBinding()]
|
|
240
|
-
[OutputType([
|
|
210
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
241
211
|
param(
|
|
242
212
|
[Parameter(Mandatory)]
|
|
243
213
|
[AllowEmptyString()]
|
|
244
|
-
[string] $
|
|
245
|
-
)
|
|
214
|
+
[string] $ReasonCode,
|
|
246
215
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
# Allow forward or backslash separators inside the matched path token.
|
|
252
|
-
$pattern = 'docs[\\/]+features[\\/]+active[\\/]+[^\s"''`]+'
|
|
253
|
-
$matchList = [regex]::Matches($Prompt, $pattern)
|
|
254
|
-
if ($matchList.Count -eq 0) {
|
|
255
|
-
return $null
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
# Deduplicate preserving FIRST-OCCURRENCE order. A [hashtable] must not be
|
|
259
|
-
# used here: PowerShell hashtable key enumeration order is unspecified, so a
|
|
260
|
-
# first-occurrence selection rule fed by a hashtable is not deterministic.
|
|
261
|
-
[System.Collections.Generic.List[string]] $candidates = [System.Collections.Generic.List[string]]::new()
|
|
262
|
-
foreach ($m in $matchList) {
|
|
263
|
-
$normalized = ($m.Value -replace '\\', '/').TrimEnd('/')
|
|
264
|
-
|
|
265
|
-
# Truncate to exactly two segments past the docs/features/active/ prefix,
|
|
266
|
-
# that is, to the four segments docs, features, active, and the feature
|
|
267
|
-
# folder name. Truncation is depth-insensitive, so a folder path, a
|
|
268
|
-
# spec.md path, a research/ artifact path, and an evidence/ artifact path
|
|
269
|
-
# all reduce to the same value. A '.' component is a path no-op and is
|
|
270
|
-
# discarded first, so a degenerate token such as docs/features/active/.
|
|
271
|
-
# yields three segments and is rejected rather than resolved.
|
|
272
|
-
$segments = @($normalized -split '/' | Where-Object { $_ -ne '' -and $_ -ne '.' })
|
|
273
|
-
if ($segments.Count -lt 4) {
|
|
274
|
-
continue
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
$truncated = ($segments[0..3] -join '/')
|
|
278
|
-
if (-not $candidates.Contains($truncated)) {
|
|
279
|
-
$candidates.Add($truncated)
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if ($candidates.Count -eq 0) {
|
|
284
|
-
return $null
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
# One distinct candidate is used directly, so the common case never consults
|
|
288
|
-
# the checkpoint.
|
|
289
|
-
if ($candidates.Count -eq 1) {
|
|
290
|
-
return $candidates[0]
|
|
291
|
-
}
|
|
216
|
+
[Parameter(Mandatory)]
|
|
217
|
+
[AllowEmptyString()]
|
|
218
|
+
[string] $Detail
|
|
219
|
+
)
|
|
292
220
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
return $checkpointNormalized
|
|
221
|
+
return [ordered]@{
|
|
222
|
+
hookSpecificOutput = [ordered]@{
|
|
223
|
+
hookEventName = 'PreToolUse'
|
|
224
|
+
permissionDecision = 'deny'
|
|
225
|
+
permissionDecisionReason = "PRD_FEATURE_BLOCKED: $ReasonCode - $Detail. " +
|
|
226
|
+
"Put the line 'Canonical issue number for this feature is <N>.' and a " +
|
|
227
|
+
"'branch: <item branch>' label in the delegation prompt so the gate can identify the item."
|
|
301
228
|
}
|
|
302
229
|
}
|
|
303
|
-
|
|
304
|
-
# Tiebreak of last resort: the orchestrator supplies the active feature folder
|
|
305
|
-
# among its delegation inputs and names it before citing artifacts inside it,
|
|
306
|
-
# so a cross-reference to another feature appears later in the prompt.
|
|
307
|
-
return $candidates[0]
|
|
308
230
|
}
|
|
309
231
|
|
|
310
|
-
function
|
|
232
|
+
function Resolve-PrdFeatureWorktreeTarget {
|
|
311
233
|
<#
|
|
312
234
|
.SYNOPSIS
|
|
313
|
-
|
|
314
|
-
|
|
235
|
+
Resolve the delegation's target worktree. Tests mock this function (seam).
|
|
236
|
+
.DESCRIPTION
|
|
237
|
+
A seam rather than a direct call because the resolver reads real git state: the
|
|
238
|
+
worktree list, each registration's branch, and the filesystem. A test that drove
|
|
239
|
+
it directly would depend on whichever worktrees exist on the machine running it.
|
|
240
|
+
.PARAMETER Text
|
|
241
|
+
The assembled prompt and description text to read identity from.
|
|
242
|
+
.OUTPUTS
|
|
243
|
+
The target result object from Resolve-WorktreeItemTarget.
|
|
315
244
|
#>
|
|
316
245
|
[CmdletBinding()]
|
|
317
|
-
[OutputType([
|
|
246
|
+
[OutputType([pscustomobject])]
|
|
318
247
|
param(
|
|
319
248
|
[Parameter(Mandatory)]
|
|
320
|
-
[
|
|
249
|
+
[AllowEmptyString()]
|
|
250
|
+
[string] $Text
|
|
251
|
+
)
|
|
321
252
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
253
|
+
return (Resolve-WorktreeItemTarget -Text $Text -SessionRoot (Get-Location).Path)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function Get-PrdFeatureCallTarget {
|
|
257
|
+
<#
|
|
258
|
+
.SYNOPSIS
|
|
259
|
+
Resolves the worktree this tool call pertains to, or $null when the call
|
|
260
|
+
supplies no text to resolve from.
|
|
261
|
+
.DESCRIPTION
|
|
262
|
+
The resolution itself belongs to the worktree-resolution library and is not
|
|
263
|
+
re-implemented here: this function only assembles the text it reads, from the
|
|
264
|
+
envelope root and the nested tool_input, and supplies the session root from
|
|
265
|
+
the envelope's own cwd field when the runtime sets one.
|
|
266
|
+
|
|
267
|
+
The assembled text is handed to the derivation unconditionally. Which
|
|
268
|
+
citations are eligible to be placed is the derivation's decision and not
|
|
269
|
+
this hook's: it answers NoTarget for a call it cannot place at all, and
|
|
270
|
+
Ambiguous for a placed signal that resolves to no worktree or to several.
|
|
271
|
+
A repo-relative citation therefore has a real placement channel, because
|
|
272
|
+
the derivation keeps only the worktrees under which that repo-relative
|
|
273
|
+
path exists. The outcome the caller sees is fail-closed:
|
|
274
|
+
a call that places in no worktree, or in several, denies before any probe
|
|
275
|
+
runs, rather than being validated against whichever root happened to be
|
|
276
|
+
current.
|
|
277
|
+
#>
|
|
278
|
+
[CmdletBinding()]
|
|
279
|
+
[OutputType([object])]
|
|
280
|
+
param(
|
|
281
|
+
[AllowNull()]
|
|
282
|
+
[object] $ToolInput
|
|
325
283
|
)
|
|
326
284
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
285
|
+
$prompt = Get-ClaudeHookToolInputString -ToolInput $ToolInput -Name 'prompt'
|
|
286
|
+
$description = Get-ClaudeHookToolInputString -ToolInput $ToolInput -Name 'description'
|
|
287
|
+
$text = (@($prompt, $description) | Where-Object { $_ }) -join ' '
|
|
288
|
+
|
|
289
|
+
# An empty call resolves nothing and, importantly, performs no enumeration and no
|
|
290
|
+
# file read. The caller treats the null as the no-target state.
|
|
291
|
+
if (-not $text) {
|
|
292
|
+
return $null
|
|
333
293
|
}
|
|
334
|
-
|
|
294
|
+
|
|
295
|
+
return (Resolve-PrdFeatureWorktreeTarget -Text $text)
|
|
335
296
|
}
|
|
336
297
|
|
|
337
298
|
function Invoke-PrdFeatureBeforePlannerDecision {
|
|
338
299
|
<#
|
|
339
300
|
.SYNOPSIS
|
|
340
301
|
Parses the envelope's nested tool_input and returns an allow-or-block decision.
|
|
302
|
+
.PARAMETER ToolInputRaw
|
|
303
|
+
The raw PreToolUse payload.
|
|
304
|
+
.PARAMETER ResolvedTarget
|
|
305
|
+
Test-only injection seam for the derived call target, following the
|
|
306
|
+
-CheckpointRaw precedent in enforce-orchestration-preimplementation-gate.ps1.
|
|
307
|
+
Binding is decided with $PSBoundParameters.ContainsKey rather than a
|
|
308
|
+
truthiness test, so an explicitly supplied empty value suppresses the
|
|
309
|
+
derivation seam instead of falling through to it.
|
|
341
310
|
#>
|
|
342
311
|
[CmdletBinding()]
|
|
343
312
|
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
344
313
|
param(
|
|
345
|
-
[string] $ToolInputRaw
|
|
314
|
+
[string] $ToolInputRaw,
|
|
315
|
+
|
|
316
|
+
[AllowNull()]
|
|
317
|
+
[object] $ResolvedTarget
|
|
346
318
|
)
|
|
347
319
|
|
|
348
320
|
$envelope = Resolve-ClaudeHookToolInput -Raw $ToolInputRaw
|
|
@@ -364,9 +336,47 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
364
336
|
}
|
|
365
337
|
|
|
366
338
|
$prompt = Get-ClaudeHookToolInputString -ToolInput $envelope.Value -Name 'prompt'
|
|
367
|
-
|
|
339
|
+
|
|
340
|
+
$target = if ($PSBoundParameters.ContainsKey('ResolvedTarget')) {
|
|
341
|
+
$ResolvedTarget
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
Get-PrdFeatureCallTarget -ToolInput $envelope.Value
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
# Unresolved identity denies before any document probe runs; a probe here would
|
|
348
|
+
# validate the call against whichever root happened to be current. A null target has
|
|
349
|
+
# two origins that mean the same thing -- the injection seam bound to null, and a call
|
|
350
|
+
# carrying no text at all -- so both take the no-target code.
|
|
351
|
+
if ($null -eq $target -or $target.Status -eq 'NoTarget' -or $target.Status -eq 'Ambiguous') {
|
|
352
|
+
$code = if ($null -eq $target) { Get-WorktreeResolutionNoTargetReasonCode } else { $target.ReasonCode }
|
|
353
|
+
$detail = if ($null -eq $target) { 'the call target was not resolved' } else { $target.Detail }
|
|
354
|
+
return (Get-PrdFeatureTargetDecision -ReasonCode $code -Detail $detail)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
$candidates = @(Find-PrdFeatureFolderCandidate -Prompt $prompt)
|
|
358
|
+
|
|
359
|
+
# The checkpoint is read only when it is needed. Exactly one cited candidate is used
|
|
360
|
+
# directly, so the common case performs no checkpoint read at all; zero or several
|
|
361
|
+
# candidates need the resolved worktree's own checkpoint, which is read once here.
|
|
362
|
+
$checkpointFolder = $null
|
|
363
|
+
if ($candidates.Count -ne 1) {
|
|
364
|
+
$checkpointFolder = Get-PrdFeatureCheckpointFolder -CheckpointPath (Get-WorktreeItemCheckpointPath -WorktreeRoot $target.WorktreeRoot)
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
$folder = Find-PrdFeatureFolderFromPrompt -Prompt $prompt -CheckpointFolder $checkpointFolder
|
|
368
|
+
|
|
369
|
+
# More than one folder was cited and the resolved worktree's checkpoint names none of
|
|
370
|
+
# them. The tie is unresolved, which is a resolution failure rather than a document one.
|
|
371
|
+
if (-not $folder -and $candidates.Count -gt 1) {
|
|
372
|
+
return (Get-PrdFeatureTargetDecision -ReasonCode (Get-WorktreeResolutionAmbiguityReasonCode) -Detail ("the call cites $($candidates.Count) feature folders ($($candidates -join ', ')) and the resolved worktree's checkpoint chooses between none of them"))
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
# The prompt named no folder, so the resolved worktree's checkpoint supplies one. It is
|
|
376
|
+
# the item's own checkpoint by construction, because identity selected the worktree it
|
|
377
|
+
# sits in, so no conditional guard is needed before trusting it.
|
|
368
378
|
if (-not $folder) {
|
|
369
|
-
$folder =
|
|
379
|
+
$folder = $checkpointFolder
|
|
370
380
|
}
|
|
371
381
|
|
|
372
382
|
if (-not $folder) {
|
|
@@ -374,18 +384,37 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
374
384
|
hookSpecificOutput = [ordered]@{
|
|
375
385
|
hookEventName = 'PreToolUse'
|
|
376
386
|
permissionDecision = 'deny'
|
|
377
|
-
permissionDecisionReason = "PRD_FEATURE_BLOCKED: atomic-planner delegation must reference a feature folder
|
|
387
|
+
permissionDecisionReason = "PRD_FEATURE_BLOCKED: atomic-planner delegation must reference a feature folder, either in the prompt or in the orchestrator checkpoint of the worktree the call resolves to, so spec.md and user-story.md prerequisites can be verified."
|
|
378
388
|
}
|
|
379
389
|
}
|
|
380
390
|
}
|
|
381
391
|
|
|
382
392
|
$folderNormalized = ($folder -replace '\\', '/').TrimEnd('/')
|
|
383
393
|
|
|
394
|
+
# Anchor every probe to the resolved target root, and only when that root differs
|
|
395
|
+
# from the session root. When the two coincide the bare repo-relative spelling is
|
|
396
|
+
# kept: prefixing unconditionally would break every call that legitimately resolves
|
|
397
|
+
# where it runs.
|
|
398
|
+
$probeFolder = $folderNormalized
|
|
399
|
+
if ($null -ne $target -and $target.Status -eq 'OtherWorktree' -and $target.WorktreeRoot) {
|
|
400
|
+
$probeFolder = Join-WorktreeResolutionPath -WorktreeRoot $target.WorktreeRoot -RepoRelativePath $folderNormalized
|
|
401
|
+
}
|
|
402
|
+
|
|
384
403
|
# Derive the prerequisite set from the persisted work-mode marker rather
|
|
385
404
|
# than a fixed spec.md/user-story.md pair. A marker that cannot be read or
|
|
386
405
|
# recognized must fail closed, not fail open: it denies on its own branch
|
|
387
406
|
# below, naming no prerequisite set and probing for no required file.
|
|
388
|
-
$issueContent = Get-PrdFeatureIssueContent -FeatureFolder $
|
|
407
|
+
$issueContent = Get-PrdFeatureIssueContent -FeatureFolder $probeFolder
|
|
408
|
+
|
|
409
|
+
# A folder that is absent from the resolved target root is a resolution failure,
|
|
410
|
+
# not a broken marker. Reporting it as a broken marker would describe a folder the
|
|
411
|
+
# gate probed at a root the call never named. The conjunct on the guard above is
|
|
412
|
+
# load-bearing: without it, an unreadable issue.md would take this branch on every
|
|
413
|
+
# session-root call instead of falling through to the work-mode branch.
|
|
414
|
+
if ($null -eq $issueContent -and $probeFolder -ne $folderNormalized) {
|
|
415
|
+
return (Get-PrdFeatureTargetDecision -ReasonCode (Get-WorktreeResolutionAmbiguityReasonCode) -Detail ("the resolved feature folder '$probeFolder' does not exist under the resolved target worktree, so the call's target cannot be confirmed"))
|
|
416
|
+
}
|
|
417
|
+
|
|
389
418
|
$workMode = Resolve-PrdFeatureWorkMode -IssueContent $issueContent
|
|
390
419
|
|
|
391
420
|
# An indeterminate mode is its own decision path, and it deliberately does NOT
|
|
@@ -400,8 +429,8 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
400
429
|
hookSpecificOutput = [ordered]@{
|
|
401
430
|
hookEventName = 'PreToolUse'
|
|
402
431
|
permissionDecision = 'deny'
|
|
403
|
-
permissionDecisionReason = "PRD_FEATURE_BLOCKED: resolved feature folder '$
|
|
404
|
-
"but its work mode could not be determined from '$
|
|
432
|
+
permissionDecisionReason = "PRD_FEATURE_BLOCKED: resolved feature folder '$probeFolder', " +
|
|
433
|
+
"but its work mode could not be determined from '$probeFolder/issue.md' " +
|
|
405
434
|
'(the ''- Work Mode:'' marker is absent, unreadable, or unrecognized). ' +
|
|
406
435
|
'Confirm that is the intended feature folder, then add or correct the ' +
|
|
407
436
|
'''- Work Mode:'' marker in that file so the prerequisite set can be derived.'
|
|
@@ -414,7 +443,7 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
414
443
|
# -RequiredFile parameter on Get-PrdFeatureMissingFile for minor-audit mode.
|
|
415
444
|
$required = @(Get-PrdFeatureRequiredFile -WorkMode $workMode)
|
|
416
445
|
|
|
417
|
-
$missing = Get-PrdFeatureMissingFile -FeatureFolder $
|
|
446
|
+
$missing = Get-PrdFeatureMissingFile -FeatureFolder $probeFolder -RequiredFile $required
|
|
418
447
|
if ($missing.Count -eq 0) {
|
|
419
448
|
return [ordered]@{ hookSpecificOutput = [ordered]@{ hookEventName = 'PreToolUse'; permissionDecision = 'allow' } }
|
|
420
449
|
}
|
|
@@ -423,7 +452,7 @@ function Invoke-PrdFeatureBeforePlannerDecision {
|
|
|
423
452
|
# folder they did not intend diagnoses a path problem immediately instead of
|
|
424
453
|
# re-running a step that has already completed correctly.
|
|
425
454
|
$list = ($missing -join ', ')
|
|
426
|
-
$reason = "PRD_FEATURE_BLOCKED: resolved feature folder '$
|
|
455
|
+
$reason = "PRD_FEATURE_BLOCKED: resolved feature folder '$probeFolder' is missing: " +
|
|
427
456
|
"$list (work mode: $workMode). Confirm that is the intended feature folder, then " +
|
|
428
457
|
'invoke the prd-feature subagent to produce the missing output(s).'
|
|
429
458
|
|