@danmoisan/drm-copilot-mcp 1.1.11 → 1.1.14
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 +368 -209
- package/package.json +2 -2
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +1 -1
- package/resources/claude-customizations/.claude/agents/epic-planner.md +4 -1
- package/resources/claude-customizations/.claude/agents/parallel-planner.md +18 -1
- package/resources/claude-customizations/.claude/agents/pr-author.md +15 -0
- package/resources/claude-customizations/.claude/hooks/enforce-completion-consistency.ps1 +16 -8
- 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 +172 -24
- 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/blast-radius/BlastRadius.psm1 +43 -6
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusScheduling.psm1 +486 -0
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusValidation.psm1 +5 -2
- package/resources/claude-customizations/.claude/lib/blast-radius/BlastRadiusWriteIntent.psm1 +446 -0
- 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/rules/parallel-orchestration.md +150 -2
- package/resources/claude-customizations/.claude/rules/plan-acceptance-gates.md +5 -5
- 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 +15 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +4 -1
- package/resources/claude-customizations/.claude/skills/parallel-add/SKILL.md +32 -17
- package/resources/claude-customizations/.claude/skills/parallel-orchestrate/SKILL.md +39 -6
- package/resources/claude-customizations/.claude/skills/parallel-plan/SKILL.md +39 -10
- package/resources/claude-customizations/.claude/skills/pr-author/SKILL.md +15 -0
- package/resources/claude-customizations/.claude/skills/quota-throttling/SKILL.md +221 -0
- package/resources/claude-customizations/config/blast-radius.json +14 -1
- package/resources/claude-customizations/pack-manifests/core.json +11 -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-epic-resolution.ps1 +493 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-epic-scope.ps1 +189 -0
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate-helpers.ps1 +172 -24
- package/resources/codex-and-agents-customizations/.codex/hooks/enforce-orchestration-preimplementation-gate.ps1 +14 -27
- 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 +20 -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 +37 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danmoisan/drm-copilot-mcp",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.14",
|
|
4
4
|
"description": "Stdio MCP server exposing drm-copilot repo-automation tools.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"overrides": {
|
|
41
41
|
"fast-uri": "^3.1.4",
|
|
42
42
|
"hono": "^4.12.27",
|
|
43
|
-
"ip-address": "^10.2
|
|
43
|
+
"ip-address": "^10.7.2",
|
|
44
44
|
"qs": "^6.15.2",
|
|
45
45
|
"@hono/node-server": "^2.0.5"
|
|
46
46
|
},
|
|
@@ -134,7 +134,7 @@ full schema enforced by `validate_epic_orchestrator_state_text`, implemented in
|
|
|
134
134
|
`epic_feature_folder`,
|
|
135
135
|
`epic_manifest_path`, `epic_status_doc_path`, `integration_branch`, `completed_steps`, `next_step`,
|
|
136
136
|
`last_updated`, `current_wave`, `waves[]`, `features[]` (including `merge_status` and the four
|
|
137
|
-
lifecycle timestamps), `epic_merge_pr`, and the three receipt arrays
|
|
137
|
+
lifecycle timestamps), `epic_merge_pr`, `epic_issue_num`, `model_routing_receipts[]` (including the `pr-author` receipt recorded before the integration-to-`main` pull request, issue #663), and the three receipt arrays
|
|
138
138
|
(`delegation_receipts[]`, `skill_receipts[]`, `mcp_call_receipts[]`) populated with the `epic`
|
|
139
139
|
route's required names from `config/orchestration-routing.json`.
|
|
140
140
|
|
|
@@ -9,11 +9,13 @@ tools:
|
|
|
9
9
|
- Glob
|
|
10
10
|
- "Write(docs/features/epics/**)"
|
|
11
11
|
- "Edit(docs/features/epics/**)"
|
|
12
|
+
- "Write(docs/features/potential/**)"
|
|
12
13
|
- "Write(artifacts/orchestration/**)"
|
|
13
14
|
- "Edit(artifacts/orchestration/**)"
|
|
14
15
|
- "Bash(git *)"
|
|
15
16
|
- "Bash(gh *)"
|
|
16
17
|
- "mcp__drm-copilot__validate_orchestration_artifacts"
|
|
18
|
+
- "mcp__drm-copilot__potential_to_issue"
|
|
17
19
|
skills:
|
|
18
20
|
- policy-compliance-order
|
|
19
21
|
- epic-plan
|
|
@@ -102,10 +104,11 @@ the prepared epic, per the template in the `epic-plan` skill.
|
|
|
102
104
|
## Checkpoint Persistence
|
|
103
105
|
|
|
104
106
|
Update `artifacts/orchestration/epic-planner-state.json` after every completed step with:
|
|
105
|
-
`objective`, `epic_feature_folder`, `epic_manifest_path`, `integration_branch`,
|
|
107
|
+
`objective`, `epic_feature_folder`, `epic_manifest_path`, `integration_branch`, `epic_issue_num`,
|
|
106
108
|
`epic_worthiness` (`{verdict, rationale}`), `features[]` (per-feature `issue_num`,
|
|
107
109
|
`feature_folder`, `depends_on`, `wave`, `complexity_band`, `preparation_status`, `plan_path`,
|
|
108
110
|
`preflight_status`), `kickoff_prompt_path`, `completed_steps`, `next_step`, and `last_updated`.
|
|
111
|
+
`epic_issue_num` is the epic-level issue `epic-planner` promotes at planning time through `mcp__drm-copilot__potential_to_issue` with `promotion_type: epic` (issue #663; see `## Epic-Level Issue Promotion` in the `epic-plan` skill).
|
|
109
112
|
|
|
110
113
|
## Completion Requirements
|
|
111
114
|
|
|
@@ -110,7 +110,8 @@ your own initiative; withdrawal is a caller decision made through F6's remove op
|
|
|
110
110
|
Update `artifacts/orchestration/parallel-planner-state.json` after every completed step with:
|
|
111
111
|
`objective`, `parallel_slug`, `parallel_manifest_path`, `mode`, `max_concurrency`,
|
|
112
112
|
`plan_home_branch`, `items[]`, `cohorts[]`, `conflict_edges[]`, `recolor_generation`,
|
|
113
|
-
`kickoff_prompt_path`, `completed_steps`, `next_step`, and `last_updated`.
|
|
113
|
+
`kickoff_prompt_path`, `completed_steps`, `next_step`, and `last_updated`. Also record the
|
|
114
|
+
`tolerated_overlaps` list returned by Get-BlastRadiusConflictEdge (tolerated-not-validated).
|
|
114
115
|
|
|
115
116
|
Each `items[]` entry carries `issue_num`, `feature_folder`, `kind`, `state`, `complexity_band`,
|
|
116
117
|
`preparation_status`, `research_path`, `plan_path`, `preflight_status`, `branch_name`,
|
|
@@ -163,6 +164,22 @@ The facade exports `Get-PlanPaths`, `Get-BlastRadius`, `Get-BlastRadiusFromObser
|
|
|
163
164
|
`Test-BlastRadiusConflict` reads that table's optional `mergeable_paths` list and contributes no
|
|
164
165
|
`path_overlap` edge for a path matching it, while the path stays in the declared radius.
|
|
165
166
|
|
|
167
|
+
The facade also exports the scheduling entry point Get-BlastRadiusConflictEdge (with its per-pair
|
|
168
|
+
helper `Get-BlastRadiusPairDecision`), the PowerShell port of `schedule_conflict_edges` in
|
|
169
|
+
`scripts/dev_tools/_blast_radius_scheduling.py`. Build the conflict edges with one call,
|
|
170
|
+
`Get-BlastRadiusConflictEdge -Item <records carrying key, radius, band> -Config <parsed truth table> -Relation ${function:Test-BlastRadiusConflict}`,
|
|
171
|
+
rather than applying `Test-BlastRadiusConflict` to each pair by hand. The -Relation argument is
|
|
172
|
+
required: the scheduling module does not resolve the relation itself, and an omitted relation fails
|
|
173
|
+
fast. It applies the detection
|
|
174
|
+
relation to every unordered pair and then the integration-cost edge rule of
|
|
175
|
+
`.claude/rules/parallel-orchestration.md` under the table's configured `conflict_tolerance`, and
|
|
176
|
+
returns `edges` and `tolerated_overlaps`. Record the edges in `conflict_edges[]` (each entry may
|
|
177
|
+
carry the tolerated extra fields `hard`, `cost`, and `benefit`) and record every returned tolerated
|
|
178
|
+
overlap in a `tolerated_overlaps` list on the planner checkpoint; tolerated overlaps are required
|
|
179
|
+
checkpoint content, never discarded. A tolerated pair runs in the same or adjacent cohorts without a
|
|
180
|
+
barrier, and the later-merging item merges `origin/main` and re-passes CI. The configured tolerance
|
|
181
|
+
is the only sanctioned relaxation: never hand-narrow a radius to suppress an edge.
|
|
182
|
+
|
|
166
183
|
**Cohort seeding and concurrency batching — bash entry points.** The bash library is granted as
|
|
167
184
|
four entry-point-specific allowlist entries — `"Bash(bash .claude/lib/bash/compute-cohorts.sh*)"`,
|
|
168
185
|
`"Bash(bash .claude/lib/bash/compute-concurrency-batches.sh*)"`,
|
|
@@ -92,3 +92,18 @@ deliberate, documented act to circumvent. It is not tamper-proof and is not a se
|
|
|
92
92
|
Repository tone is defined in `CLAUDE.md` and `.claude/rules/tonality.md`. Reference only files
|
|
93
93
|
listed under "Additional context files" in the PR-context bundle; do not cite or summarize files
|
|
94
94
|
outside that enumeration. Do not invent issue or PR numbers.
|
|
95
|
+
|
|
96
|
+
## Required `--head` on `gh pr create` (issue #687)
|
|
97
|
+
|
|
98
|
+
Every `gh pr create` command this agent issues MUST carry an explicit
|
|
99
|
+
`--head <branch>` naming the branch the pull request is opened from.
|
|
100
|
+
|
|
101
|
+
This is what makes the call's target derivable. `enforce-pr-author-skill.ps1` resolves the
|
|
102
|
+
worktree the call pertains to from the command text, and validates the orchestrator checkpoint
|
|
103
|
+
belonging to that worktree. Without `--head`, `gh pr create` infers its head branch from the
|
|
104
|
+
current checkout and the command carries no signal at all, so the gate denies with
|
|
105
|
+
`TARGET_WORKTREE_NOT_DERIVABLE` rather than validating against whichever checkpoint occupies the
|
|
106
|
+
session root, which in a parallel or epic topology belongs to a different item.
|
|
107
|
+
|
|
108
|
+
Pass `--head` even when the branch is the current one: the gate resolves it to the session root
|
|
109
|
+
and behaves exactly as before, and the command stays correct if it is ever replayed elsewhere.
|
|
@@ -29,9 +29,13 @@
|
|
|
29
29
|
completion is not asserted, the write is allowed via
|
|
30
30
|
hookSpecificOutput.permissionDecision='allow' (backward compatibility).
|
|
31
31
|
|
|
32
|
-
Edit tool calls supply only old_string/new_string (a partial patch)
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
Edit tool calls supply only old_string/new_string (a partial patch). An Edit
|
|
33
|
+
call is validated by reading the checkpoint at the Edit's
|
|
34
|
+
targeted file_path through the injectable CheckpointReader seam and
|
|
35
|
+
applying the old_string -> new_string replacement in memory; the resulting
|
|
36
|
+
content is then evaluated like a Write. When the patch cannot be resolved
|
|
37
|
+
(the targeted file is missing or empty, the Edit has no old_string, or the
|
|
38
|
+
old_string is absent from the targeted content), the call is allowed. For
|
|
35
39
|
Write calls whose content is not valid JSON, the hook allows the operation
|
|
36
40
|
and defers to downstream tools to surface the error.
|
|
37
41
|
|
|
@@ -266,10 +270,11 @@ function Resolve-EditedCheckpointContent {
|
|
|
266
270
|
when the patch cannot be applied against the on-disk checkpoint.
|
|
267
271
|
.DESCRIPTION
|
|
268
272
|
Implements the read-then-validate Edit path. When the tool input carries
|
|
269
|
-
an old_string (an Edit patch), the
|
|
273
|
+
an old_string (an Edit patch), the checkpoint at the caller-supplied
|
|
274
|
+
CheckpointPath (the Edit's targeted file_path) is read through the
|
|
270
275
|
injectable CheckpointReader seam and the old_string -> new_string
|
|
271
276
|
replacement is applied in memory (no on-disk mutation). Returns $null
|
|
272
|
-
when there is no old_string, the
|
|
277
|
+
when there is no old_string, the targeted file does not exist or is empty, or the
|
|
273
278
|
old_string is not present in the on-disk content, signalling the caller
|
|
274
279
|
to allow (defer).
|
|
275
280
|
#>
|
|
@@ -281,7 +286,10 @@ function Resolve-EditedCheckpointContent {
|
|
|
281
286
|
$ToolInput,
|
|
282
287
|
|
|
283
288
|
[Parameter(Mandatory)]
|
|
284
|
-
[scriptblock] $CheckpointReader
|
|
289
|
+
[scriptblock] $CheckpointReader,
|
|
290
|
+
|
|
291
|
+
[Parameter(Mandatory)]
|
|
292
|
+
[string] $CheckpointPath
|
|
285
293
|
)
|
|
286
294
|
|
|
287
295
|
$oldString = $null
|
|
@@ -297,7 +305,7 @@ function Resolve-EditedCheckpointContent {
|
|
|
297
305
|
$newString = [string]$ToolInput.new_string
|
|
298
306
|
}
|
|
299
307
|
|
|
300
|
-
$onDisk = & $CheckpointReader
|
|
308
|
+
$onDisk = & $CheckpointReader $CheckpointPath
|
|
301
309
|
if ([string]::IsNullOrEmpty([string]$onDisk)) {
|
|
302
310
|
# The on-disk checkpoint does not exist (or is empty); cannot patch.
|
|
303
311
|
return $null
|
|
@@ -363,7 +371,7 @@ function Invoke-CompletionConsistencyDecision {
|
|
|
363
371
|
# apply the old_string -> new_string patch in memory (read-then-validate).
|
|
364
372
|
$content = Get-ClaudeHookToolInputString -ToolInput $toolInput -Name 'content'
|
|
365
373
|
if (-not $content) {
|
|
366
|
-
$content = Resolve-EditedCheckpointContent -ToolInput $toolInput -CheckpointReader $CheckpointReader
|
|
374
|
+
$content = Resolve-EditedCheckpointContent -ToolInput $toolInput -CheckpointReader $CheckpointReader -CheckpointPath $filePath
|
|
367
375
|
if (-not $content) {
|
|
368
376
|
# No content, and the Edit could not be resolved against on-disk
|
|
369
377
|
# state (missing file or non-matching patch): defer and allow.
|
package/resources/claude-customizations/.claude/hooks/enforce-epic-merge-gate-authorization.ps1
ADDED
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Dot-sourced standalone-merge authorization helpers for enforce-epic-merge-gate.ps1.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Holds the fourth allow condition of the epic merge gate, the standalone-merge
|
|
7
|
+
authorization record, together with the two pure decision-envelope factories the gate
|
|
8
|
+
returns on every path.
|
|
9
|
+
|
|
10
|
+
This file is split out of the parent hook, .claude/hooks/enforce-epic-merge-gate.ps1,
|
|
11
|
+
because that file had 13 lines of headroom under the 500-line limit, which a fourth allow
|
|
12
|
+
condition, its predicates, and the rewritten header could not fit (issue #670). The two
|
|
13
|
+
envelope factories were moved here unchanged: they read no script-scoped state and no
|
|
14
|
+
test calls them by name, so the move is a pure relocation with no behaviour change.
|
|
15
|
+
|
|
16
|
+
Every function in this file is pure. It reads no file, starts no process, and reads no
|
|
17
|
+
clock; the parent hook supplies the already-parsed checkpoint objects and the live
|
|
18
|
+
envelope session_id. A test can therefore dot-source this file on its own.
|
|
19
|
+
|
|
20
|
+
Honest disclosure. The standalone-merge authorization record is a policy-level,
|
|
21
|
+
auditable declaration and is
|
|
22
|
+
not a cryptographic or security control.
|
|
23
|
+
It names a specific pull request, a specific session, an authorizer, a time, and a
|
|
24
|
+
stated basis, so that a standalone merge leaves a reviewable trail and cannot be reached
|
|
25
|
+
by accident or by a record written for a different pull request. authorized_by is a
|
|
26
|
+
declaration only: the hook checks that it is present and non-empty and does not verify
|
|
27
|
+
the identity it names, because the runtime exposes no attested agent identity at Bash
|
|
28
|
+
PreToolUse time. The record is not tamper-proof: any actor able to write
|
|
29
|
+
artifacts/orchestration/*.json inside the authorizing session can write a record,
|
|
30
|
+
because all agents share one filesystem and defaultPermissionMode is bypassPermissions.
|
|
31
|
+
The session_id cross-check binds a record to the session that emitted it, so a stale
|
|
32
|
+
record from a previous run or a record copied between runs authorizes nothing; it does
|
|
33
|
+
not stop same-session forgery. The mechanism converts an untraceable bypass into a
|
|
34
|
+
deliberate, attributable, auditable act. This is a documented accepted trade, not an
|
|
35
|
+
unexamined gap.
|
|
36
|
+
|
|
37
|
+
.NOTES
|
|
38
|
+
Compatible with PowerShell 7+. No external module dependencies.
|
|
39
|
+
#>
|
|
40
|
+
[CmdletBinding()]
|
|
41
|
+
param()
|
|
42
|
+
|
|
43
|
+
# Reason codes for the standalone-merge authorization branch. Each of the four assignments
|
|
44
|
+
# below is the only place its code literal appears in this file; every other use reads the
|
|
45
|
+
# variable, and the Codex hook's spelling is compared against these assignments by a test.
|
|
46
|
+
$script:StandaloneAbsentReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_ABSENT'
|
|
47
|
+
$script:StandalonePrMismatchReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_PR_MISMATCH'
|
|
48
|
+
$script:StandaloneMalformedReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_MALFORMED'
|
|
49
|
+
$script:StandaloneNotPrSpecificReasonCode = 'STANDALONE_MERGE_AUTHORIZATION_NOT_PR_SPECIFIC'
|
|
50
|
+
|
|
51
|
+
# The checkpoint key that carries the authorization records, and the pointer every
|
|
52
|
+
# standalone deny message ends with.
|
|
53
|
+
$script:StandaloneAuthorizationKey = 'standalone_merge_authorizations'
|
|
54
|
+
$script:StandaloneAuthorizationGuidance = 'To authorize a standalone merge, write a standalone_merge_authorizations record naming this pull request and the current session_id into an orchestrator checkpoint, as described in the Standalone-Merge-Authorization section of .claude/rules/orchestrator-state.md.'
|
|
55
|
+
|
|
56
|
+
function Get-EpicMergeGateAllowDecision {
|
|
57
|
+
[CmdletBinding()]
|
|
58
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
59
|
+
param()
|
|
60
|
+
|
|
61
|
+
return [ordered]@{
|
|
62
|
+
hookSpecificOutput = [ordered]@{
|
|
63
|
+
hookEventName = 'PreToolUse'
|
|
64
|
+
permissionDecision = 'allow'
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function Get-EpicMergeGateBlockDecision {
|
|
70
|
+
[CmdletBinding()]
|
|
71
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
72
|
+
param(
|
|
73
|
+
[Parameter(Mandatory)]
|
|
74
|
+
[string] $Reason
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return [ordered]@{
|
|
78
|
+
hookSpecificOutput = [ordered]@{
|
|
79
|
+
hookEventName = 'PreToolUse'
|
|
80
|
+
permissionDecision = 'deny'
|
|
81
|
+
permissionDecisionReason = $Reason
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function Test-StandalonePositiveJsonInteger {
|
|
87
|
+
<#
|
|
88
|
+
.SYNOPSIS
|
|
89
|
+
Report whether a parsed JSON value is an integer strictly greater than zero.
|
|
90
|
+
.DESCRIPTION
|
|
91
|
+
ConvertFrom-Json yields a 32- or 64-bit integer for a JSON integer, a double for a
|
|
92
|
+
JSON number with a fraction, and a string for a quoted value. Only the two integer
|
|
93
|
+
types qualify, so a string that spells digits is rejected on its type.
|
|
94
|
+
.OUTPUTS
|
|
95
|
+
System.Boolean
|
|
96
|
+
#>
|
|
97
|
+
[CmdletBinding()]
|
|
98
|
+
[OutputType([bool])]
|
|
99
|
+
param(
|
|
100
|
+
[AllowNull()]
|
|
101
|
+
$Value
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
if (-not ($Value -is [int] -or $Value -is [long])) {
|
|
105
|
+
return $false
|
|
106
|
+
}
|
|
107
|
+
return ($Value -gt 0)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function Test-StandaloneJsonObject {
|
|
111
|
+
<#
|
|
112
|
+
.SYNOPSIS
|
|
113
|
+
Report whether a parsed JSON value is an object rather than a scalar or an array.
|
|
114
|
+
.OUTPUTS
|
|
115
|
+
System.Boolean
|
|
116
|
+
#>
|
|
117
|
+
[CmdletBinding()]
|
|
118
|
+
[OutputType([bool])]
|
|
119
|
+
param(
|
|
120
|
+
[AllowNull()]
|
|
121
|
+
$Value
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return ($Value -is [System.Management.Automation.PSCustomObject])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function Get-StandaloneRecordField {
|
|
128
|
+
<#
|
|
129
|
+
.SYNOPSIS
|
|
130
|
+
Read a named field off a parsed record, returning $null when the field is absent.
|
|
131
|
+
.OUTPUTS
|
|
132
|
+
System.Object, System.Object[], or $null
|
|
133
|
+
#>
|
|
134
|
+
[CmdletBinding()]
|
|
135
|
+
[OutputType([object], [object[]])]
|
|
136
|
+
param(
|
|
137
|
+
[Parameter(Mandatory)]
|
|
138
|
+
$Record,
|
|
139
|
+
|
|
140
|
+
[Parameter(Mandatory)]
|
|
141
|
+
[string] $Name
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
$property = $Record.PSObject.Properties[$Name]
|
|
145
|
+
if ($null -eq $property) {
|
|
146
|
+
return $null
|
|
147
|
+
}
|
|
148
|
+
# The unary comma keeps a one-element array from unrolling into its scalar, so a
|
|
149
|
+
# pr_number spelled as [691] is still seen as an array and rejected.
|
|
150
|
+
return , $property.Value
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function Get-StandaloneMergeAuthorizationRecord {
|
|
154
|
+
<#
|
|
155
|
+
.SYNOPSIS
|
|
156
|
+
Select the authorization record for one pull request from a parsed checkpoint.
|
|
157
|
+
.DESCRIPTION
|
|
158
|
+
Returns the first entry of the checkpoint's authorization array that is an object
|
|
159
|
+
whose pr_number is a positive JSON integer equal to PrNumber, or nothing when no
|
|
160
|
+
such entry exists. Reads no file, starts no process, and reads no clock.
|
|
161
|
+
.PARAMETER Checkpoint
|
|
162
|
+
Parsed checkpoint object, or $null when the checkpoint is absent or unreadable.
|
|
163
|
+
.PARAMETER PrNumber
|
|
164
|
+
The pull request number extracted from the command under evaluation.
|
|
165
|
+
.OUTPUTS
|
|
166
|
+
System.Object or nothing
|
|
167
|
+
#>
|
|
168
|
+
[CmdletBinding()]
|
|
169
|
+
param(
|
|
170
|
+
[AllowNull()]
|
|
171
|
+
$Checkpoint,
|
|
172
|
+
|
|
173
|
+
[Parameter(Mandatory)]
|
|
174
|
+
[int] $PrNumber
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
if (-not (Test-StandaloneJsonObject -Value $Checkpoint)) {
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
$property = $Checkpoint.PSObject.Properties[$script:StandaloneAuthorizationKey]
|
|
181
|
+
if ($null -eq $property -or $property.Value -isnot [System.Array]) {
|
|
182
|
+
return
|
|
183
|
+
}
|
|
184
|
+
foreach ($entry in $property.Value) {
|
|
185
|
+
if (-not (Test-StandaloneJsonObject -Value $entry)) {
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
$entryNumber = Get-StandaloneRecordField -Record $entry -Name 'pr_number'
|
|
189
|
+
if ((Test-StandalonePositiveJsonInteger -Value $entryNumber) -and $entryNumber -eq $PrNumber) {
|
|
190
|
+
return $entry
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function Test-StandaloneNonBlankString {
|
|
196
|
+
<#
|
|
197
|
+
.SYNOPSIS
|
|
198
|
+
Report whether a value is a string whose trimmed length reaches a minimum.
|
|
199
|
+
.OUTPUTS
|
|
200
|
+
System.Boolean
|
|
201
|
+
#>
|
|
202
|
+
[CmdletBinding()]
|
|
203
|
+
[OutputType([bool])]
|
|
204
|
+
param(
|
|
205
|
+
[AllowNull()]
|
|
206
|
+
$Value,
|
|
207
|
+
|
|
208
|
+
[int] $MinimumLength = 1
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
if ($Value -isnot [string]) {
|
|
212
|
+
return $false
|
|
213
|
+
}
|
|
214
|
+
return ($Value.Trim().Length -ge $MinimumLength)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function Test-StandaloneAuthorizedAt {
|
|
218
|
+
<#
|
|
219
|
+
.SYNOPSIS
|
|
220
|
+
Report whether an authorized_at value is a parseable date-time.
|
|
221
|
+
.DESCRIPTION
|
|
222
|
+
ConvertFrom-Json already converts an ISO 8601 string into a date-time value, so
|
|
223
|
+
such a value is accepted as parsed. Any other string must parse with the invariant
|
|
224
|
+
culture, assuming and adjusting to UTC. No clock is read.
|
|
225
|
+
.OUTPUTS
|
|
226
|
+
System.Boolean
|
|
227
|
+
#>
|
|
228
|
+
[CmdletBinding()]
|
|
229
|
+
[OutputType([bool])]
|
|
230
|
+
param(
|
|
231
|
+
[AllowNull()]
|
|
232
|
+
$Value
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
if ($Value -is [DateTime] -or $Value -is [DateTimeOffset]) {
|
|
236
|
+
return $true
|
|
237
|
+
}
|
|
238
|
+
if (-not (Test-StandaloneNonBlankString -Value $Value)) {
|
|
239
|
+
return $false
|
|
240
|
+
}
|
|
241
|
+
$parsed = [DateTime]::MinValue
|
|
242
|
+
return [DateTime]::TryParse(
|
|
243
|
+
$Value,
|
|
244
|
+
[System.Globalization.CultureInfo]::InvariantCulture,
|
|
245
|
+
[System.Globalization.DateTimeStyles]::AdjustToUniversal -bor [System.Globalization.DateTimeStyles]::AssumeUniversal,
|
|
246
|
+
[ref] $parsed)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function Get-StandaloneRecordFailure {
|
|
250
|
+
<#
|
|
251
|
+
.SYNOPSIS
|
|
252
|
+
Build the malformed-record result naming the failing field.
|
|
253
|
+
.OUTPUTS
|
|
254
|
+
System.Management.Automation.PSCustomObject
|
|
255
|
+
#>
|
|
256
|
+
[CmdletBinding()]
|
|
257
|
+
[OutputType([pscustomobject])]
|
|
258
|
+
param(
|
|
259
|
+
[Parameter(Mandatory)]
|
|
260
|
+
[string] $Field,
|
|
261
|
+
|
|
262
|
+
[Parameter(Mandatory)]
|
|
263
|
+
[string] $Rule
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
return [pscustomobject]@{
|
|
267
|
+
ReasonCode = $script:StandaloneMalformedReasonCode
|
|
268
|
+
Message = "the matched authorization record has an invalid '$Field': $Rule"
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function Test-StandaloneMergeAuthorizationRecord {
|
|
273
|
+
<#
|
|
274
|
+
.SYNOPSIS
|
|
275
|
+
Validate the fields of a selected authorization record.
|
|
276
|
+
.DESCRIPTION
|
|
277
|
+
Runs the field checks in the fixed order pr_url, issue_num, branch_name,
|
|
278
|
+
authorized_by, authorized_at, basis, run_slug, session_id. Returns nothing when
|
|
279
|
+
every check passes; otherwise returns the first failure as an object carrying
|
|
280
|
+
ReasonCode and a Message that names the failing field. branch_name is checked for
|
|
281
|
+
presence only and is never compared against the live branch or working directory.
|
|
282
|
+
.PARAMETER Record
|
|
283
|
+
The parsed record selected by pr_number.
|
|
284
|
+
.PARAMETER EnvelopeSessionId
|
|
285
|
+
The session_id read from the live PreToolUse envelope. A blank value fails closed.
|
|
286
|
+
.OUTPUTS
|
|
287
|
+
System.Management.Automation.PSCustomObject or nothing
|
|
288
|
+
#>
|
|
289
|
+
[CmdletBinding()]
|
|
290
|
+
param(
|
|
291
|
+
[Parameter(Mandatory)]
|
|
292
|
+
$Record,
|
|
293
|
+
|
|
294
|
+
[AllowNull()]
|
|
295
|
+
[AllowEmptyString()]
|
|
296
|
+
[string] $EnvelopeSessionId
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
$prUrl = Get-StandaloneRecordField -Record $Record -Name 'pr_url'
|
|
300
|
+
$pullSuffix = '/pull/' + [string](Get-StandaloneRecordField -Record $Record -Name 'pr_number')
|
|
301
|
+
if (-not (Test-StandaloneNonBlankString -Value $prUrl) -or
|
|
302
|
+
-not $prUrl.EndsWith($pullSuffix, [System.StringComparison]::Ordinal)) {
|
|
303
|
+
return Get-StandaloneRecordFailure -Field 'pr_url' -Rule "it must end with $pullSuffix."
|
|
304
|
+
}
|
|
305
|
+
if (-not (Test-StandalonePositiveJsonInteger -Value (Get-StandaloneRecordField -Record $Record -Name 'issue_num'))) {
|
|
306
|
+
return Get-StandaloneRecordFailure -Field 'issue_num' -Rule 'it must be a JSON integer greater than zero.'
|
|
307
|
+
}
|
|
308
|
+
foreach ($name in @('branch_name', 'authorized_by')) {
|
|
309
|
+
if (-not (Test-StandaloneNonBlankString -Value (Get-StandaloneRecordField -Record $Record -Name $name))) {
|
|
310
|
+
return Get-StandaloneRecordFailure -Field $name -Rule 'it must be a non-empty string.'
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (-not (Test-StandaloneAuthorizedAt -Value (Get-StandaloneRecordField -Record $Record -Name 'authorized_at'))) {
|
|
314
|
+
return Get-StandaloneRecordFailure -Field 'authorized_at' -Rule 'it must be a parseable date-time string.'
|
|
315
|
+
}
|
|
316
|
+
if (-not (Test-StandaloneNonBlankString -Value (Get-StandaloneRecordField -Record $Record -Name 'basis') -MinimumLength 20)) {
|
|
317
|
+
return Get-StandaloneRecordFailure -Field 'basis' -Rule 'it must be at least 20 characters after trimming.'
|
|
318
|
+
}
|
|
319
|
+
if ($null -ne $Record.PSObject.Properties['run_slug'] -and
|
|
320
|
+
-not (Test-StandaloneNonBlankString -Value (Get-StandaloneRecordField -Record $Record -Name 'run_slug'))) {
|
|
321
|
+
return Get-StandaloneRecordFailure -Field 'run_slug' -Rule 'when present it must be a non-empty string.'
|
|
322
|
+
}
|
|
323
|
+
$recordSessionId = Get-StandaloneRecordField -Record $Record -Name 'session_id'
|
|
324
|
+
if (-not (Test-StandaloneNonBlankString -Value $recordSessionId) -or
|
|
325
|
+
[string]::IsNullOrWhiteSpace($EnvelopeSessionId) -or
|
|
326
|
+
-not [string]::Equals($recordSessionId, $EnvelopeSessionId, [System.StringComparison]::Ordinal)) {
|
|
327
|
+
return Get-StandaloneRecordFailure -Field 'session_id' -Rule 'it must be non-empty and equal the session_id of the live hook envelope.'
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function Get-StandaloneAuthorizationBlockState {
|
|
332
|
+
<#
|
|
333
|
+
.SYNOPSIS
|
|
334
|
+
Classify one parsed checkpoint as Absent, NotPrSpecific, or PrSpecific.
|
|
335
|
+
.DESCRIPTION
|
|
336
|
+
Absent means the checkpoint is missing or carries no authorization key. The block
|
|
337
|
+
is PrSpecific only when it is a non-empty array whose every entry is an object with
|
|
338
|
+
a positive integer pr_number; any other shape, including a blanket boolean, is
|
|
339
|
+
NotPrSpecific, so a blanket flag is rejected visibly rather than read as absent.
|
|
340
|
+
.OUTPUTS
|
|
341
|
+
System.String
|
|
342
|
+
#>
|
|
343
|
+
[CmdletBinding()]
|
|
344
|
+
[OutputType([string])]
|
|
345
|
+
param(
|
|
346
|
+
[AllowNull()]
|
|
347
|
+
$Checkpoint
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
if (-not (Test-StandaloneJsonObject -Value $Checkpoint)) {
|
|
351
|
+
return 'Absent'
|
|
352
|
+
}
|
|
353
|
+
$property = $Checkpoint.PSObject.Properties[$script:StandaloneAuthorizationKey]
|
|
354
|
+
if ($null -eq $property) {
|
|
355
|
+
return 'Absent'
|
|
356
|
+
}
|
|
357
|
+
$block = $property.Value
|
|
358
|
+
if ($block -isnot [System.Array] -or $block.Count -eq 0) {
|
|
359
|
+
return 'NotPrSpecific'
|
|
360
|
+
}
|
|
361
|
+
foreach ($entry in $block) {
|
|
362
|
+
if (-not (Test-StandaloneJsonObject -Value $entry)) {
|
|
363
|
+
return 'NotPrSpecific'
|
|
364
|
+
}
|
|
365
|
+
if (-not (Test-StandalonePositiveJsonInteger -Value (Get-StandaloneRecordField -Record $entry -Name 'pr_number'))) {
|
|
366
|
+
return 'NotPrSpecific'
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
return 'PrSpecific'
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function Test-StandaloneCheckpointAllowsMerge {
|
|
373
|
+
<#
|
|
374
|
+
.SYNOPSIS
|
|
375
|
+
Decision logic for the standalone-merge authorization path (branch 4).
|
|
376
|
+
.DESCRIPTION
|
|
377
|
+
Evaluates the three parsed orchestrator checkpoints for a record that authorizes
|
|
378
|
+
CommandPrNumber in the live session. Returns an object whose Allowed member is
|
|
379
|
+
$true on success; otherwise Allowed is $false and ReasonCode and Message describe
|
|
380
|
+
the denial:
|
|
381
|
+
- absent: no checkpoint carries the authorization key;
|
|
382
|
+
- not PR-specific: a checkpoint carries the key but names no specific pull request;
|
|
383
|
+
- PR mismatch: records exist, but none names CommandPrNumber;
|
|
384
|
+
- malformed: the matched record fails a field check, and the message names the field.
|
|
385
|
+
.PARAMETER Checkpoints
|
|
386
|
+
The three parsed checkpoint objects; any element may be $null.
|
|
387
|
+
.PARAMETER CommandPrNumber
|
|
388
|
+
The explicit pull request number extracted from the command.
|
|
389
|
+
.PARAMETER EnvelopeSessionId
|
|
390
|
+
The session_id read from the live PreToolUse envelope.
|
|
391
|
+
.OUTPUTS
|
|
392
|
+
System.Management.Automation.PSCustomObject
|
|
393
|
+
#>
|
|
394
|
+
[CmdletBinding()]
|
|
395
|
+
[OutputType([pscustomobject])]
|
|
396
|
+
param(
|
|
397
|
+
[AllowNull()]
|
|
398
|
+
[AllowEmptyCollection()]
|
|
399
|
+
[object[]] $Checkpoints,
|
|
400
|
+
|
|
401
|
+
[Parameter(Mandatory)]
|
|
402
|
+
[int] $CommandPrNumber,
|
|
403
|
+
|
|
404
|
+
[AllowNull()]
|
|
405
|
+
[AllowEmptyString()]
|
|
406
|
+
[string] $EnvelopeSessionId
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
$states = @(foreach ($checkpoint in @($Checkpoints)) { Get-StandaloneAuthorizationBlockState -Checkpoint $checkpoint })
|
|
410
|
+
$reasonCode = $null
|
|
411
|
+
$message = $null
|
|
412
|
+
if ($states -contains 'NotPrSpecific') {
|
|
413
|
+
$reasonCode = $script:StandaloneNotPrSpecificReasonCode
|
|
414
|
+
$message = 'an orchestrator checkpoint carries the authorization key but names no specific pull request; a blanket flag is not an authorization record.'
|
|
415
|
+
} elseif ($states -notcontains 'PrSpecific') {
|
|
416
|
+
$reasonCode = $script:StandaloneAbsentReasonCode
|
|
417
|
+
$message = 'no orchestrator checkpoint carries an authorization record, but an authorized standalone merge path exists.'
|
|
418
|
+
} else {
|
|
419
|
+
foreach ($checkpoint in @($Checkpoints)) {
|
|
420
|
+
$record = Get-StandaloneMergeAuthorizationRecord -Checkpoint $checkpoint -PrNumber $CommandPrNumber
|
|
421
|
+
if ($null -eq $record) {
|
|
422
|
+
continue
|
|
423
|
+
}
|
|
424
|
+
$failure = Test-StandaloneMergeAuthorizationRecord -Record $record -EnvelopeSessionId $EnvelopeSessionId
|
|
425
|
+
if ($null -eq $failure) {
|
|
426
|
+
return [pscustomobject]@{ Allowed = $true; ReasonCode = $null; Message = $null }
|
|
427
|
+
}
|
|
428
|
+
$reasonCode = $failure.ReasonCode
|
|
429
|
+
$message = $failure.Message
|
|
430
|
+
break
|
|
431
|
+
}
|
|
432
|
+
if ($null -eq $reasonCode) {
|
|
433
|
+
$reasonCode = $script:StandalonePrMismatchReasonCode
|
|
434
|
+
$message = "authorization records exist, but none names pull request $CommandPrNumber."
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
return [pscustomobject]@{
|
|
439
|
+
Allowed = $false
|
|
440
|
+
ReasonCode = $reasonCode
|
|
441
|
+
Message = "$message $script:StandaloneAuthorizationGuidance"
|
|
442
|
+
}
|
|
443
|
+
}
|