@danmoisan/drm-copilot-mcp 1.0.11 → 1.0.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 +585 -123
- package/package.json +1 -1
- package/resources/claude-customizations/.claude/agents/epic-orchestrator.md +28 -3
- package/resources/claude-customizations/.claude/agents/epic-planner.md +121 -0
- package/resources/claude-customizations/.claude/agents/orchestrator.md +2 -2
- package/resources/claude-customizations/.claude/hooks/enforce-epic-invocation-origin.ps1 +246 -0
- package/resources/claude-customizations/.claude/hooks/persist-session-id.ps1 +153 -0
- package/resources/claude-customizations/.claude/settings.json +22 -2
- package/resources/claude-customizations/.claude/skills/epic-orchestrate/SKILL.md +58 -24
- package/resources/claude-customizations/.claude/skills/epic-plan/SKILL.md +189 -0
- package/resources/claude-customizations/.claude/skills/epic-run/SKILL.md +38 -0
- package/resources/claude-customizations/.claude/skills/identify-session-id/SKILL.md +44 -0
- package/resources/claude-customizations/.claude/skills/orchestrate/SKILL.md +21 -2
- package/resources/claude-customizations/.claude/skills/show-my-agent-tree/SKILL.md +39 -0
- package/resources/claude-customizations/pack-manifests/core.json +7 -0
- package/resources/config/orchestration-routing.json +21 -0
- package/resources/feature-templates/epic/epic-status.md +20 -0
- package/resources/feature-templates/epic/epic.md +79 -0
- package/resources/powershell/PoshQC/settings/pester.runsettings.psd1 +4 -0
- package/resources/templates/new-claude-worktree-session.ps1 +58 -9
- package/resources/feature-templates/epic/initiative.md +0 -43
package/package.json
CHANGED
|
@@ -63,7 +63,31 @@ On every invocation:
|
|
|
63
63
|
`git branch`, and `gh pr view --json state,mergedAt,headRefOid` per the `epic-orchestrate`
|
|
64
64
|
skill's resume procedure, not from in-memory notifications alone).
|
|
65
65
|
5. If no checkpoint exists or the objective is new, begin from manifest parsing
|
|
66
|
-
(`docs/features/epics/<epic-slug>/epic
|
|
66
|
+
(`docs/features/epics/<epic-slug>/epic.md`).
|
|
67
|
+
|
|
68
|
+
## Invocation Origin
|
|
69
|
+
|
|
70
|
+
You are invoked from the main session — via `/epic-orchestrate <epic-manifest-path>`, via
|
|
71
|
+
`/epic-run <epic-slug>` (which replays the kickoff artifact `epic-planner` emitted), or by a
|
|
72
|
+
direct prompt. You delegate to `Agent(orchestrator)`, so an invocation that itself
|
|
73
|
+
originates from an `orchestrator` agent would nest `orchestrator` inside its own delegation
|
|
74
|
+
chain; the PreToolUse hook `.claude/hooks/enforce-epic-invocation-origin.ps1` denies any
|
|
75
|
+
`Agent(epic-orchestrator)` or `Agent(epic-planner)` call whose calling agent is `orchestrator`
|
|
76
|
+
(`EPIC_INVOCATION_ORIGIN_BLOCKED`).
|
|
77
|
+
|
|
78
|
+
## Prepared-Epic Execution (epic-planner Handoff)
|
|
79
|
+
|
|
80
|
+
When the epic was prepared by `epic-planner` (the integration branch already exists and
|
|
81
|
+
`docs/features/epics/<epic-slug>/epic-kickoff.md` is present), each child feature folder already
|
|
82
|
+
contains its issue, research, `spec.md`, `user-story.md`, an approved atomic plan, and a
|
|
83
|
+
recorded preflight clearance. In that case:
|
|
84
|
+
|
|
85
|
+
1. Do not recreate the integration branch; fetch and reuse it.
|
|
86
|
+
2. Each child `Agent(orchestrator)` delegation prompt cites the child's committed `plan-path`
|
|
87
|
+
and instructs the run to resume at atomic execution from that plan rather than re-running
|
|
88
|
+
promotion, research, or planning.
|
|
89
|
+
3. The wave barrier, merge-on-green fan-in, and final integration-to-`main` PR proceed
|
|
90
|
+
unchanged per the `epic-orchestrate` skill.
|
|
67
91
|
|
|
68
92
|
## Delegation Model
|
|
69
93
|
|
|
@@ -112,8 +136,9 @@ route's required names from `config/orchestration-routing.json`.
|
|
|
112
136
|
Maintain `docs/features/epics/<epic-slug>/epic-status.md` as a human-readable projection of the
|
|
113
137
|
epic checkpoint's `features[]` array, regenerated (not hand-edited) at epic kickoff, at every
|
|
114
138
|
`merge_status` transition, at every wave transition, and at final integration-PR completion, per
|
|
115
|
-
the `epic-orchestrate` skill's documentation-maintenance procedure. `epic
|
|
116
|
-
manifest) is treated as static, human-authored input and is not
|
|
139
|
+
the `epic-orchestrate` skill's documentation-maintenance procedure. `epic.md` itself (the merged
|
|
140
|
+
manifest + narrative source of truth) is treated as static, human-authored input and is not
|
|
141
|
+
rewritten by you; `epic-status.md` is a generated projection only and is never hand-authored.
|
|
117
142
|
|
|
118
143
|
## Completion Requirements
|
|
119
144
|
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: epic-planner
|
|
3
|
+
model: opus
|
|
4
|
+
description: Epic-scale planning agent that fully scopes an epic before any execution. It decomposes the objective into child features, computes the dependency graph and wave layering, assesses per-feature complexity, drives per-feature preparation (promotion, research, spec/user-story, atomic plan, preflight clearance) through parallel preparation-mode Agent(orchestrator) delegations, commits all planning outputs to the epic integration branch, and emits the epic-orchestrator kickoff prompt artifact. Performs no atomic execution. Must not be invoked from an orchestrator agent (enforced by enforce-epic-invocation-origin.ps1).
|
|
5
|
+
tools:
|
|
6
|
+
- "Agent(orchestrator)"
|
|
7
|
+
- Read
|
|
8
|
+
- Grep
|
|
9
|
+
- Glob
|
|
10
|
+
- "Write(docs/features/epics/**)"
|
|
11
|
+
- "Edit(docs/features/epics/**)"
|
|
12
|
+
- "Write(artifacts/orchestration/**)"
|
|
13
|
+
- "Edit(artifacts/orchestration/**)"
|
|
14
|
+
- "Bash(git *)"
|
|
15
|
+
- "Bash(gh *)"
|
|
16
|
+
- "mcp__drm-copilot__validate_orchestration_artifacts"
|
|
17
|
+
skills:
|
|
18
|
+
- policy-compliance-order
|
|
19
|
+
- epic-plan
|
|
20
|
+
- epic-orchestrate
|
|
21
|
+
- feature-promotion-lifecycle
|
|
22
|
+
- atomic-plan-contract
|
|
23
|
+
- evidence-and-timestamp-conventions
|
|
24
|
+
memory: project
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
# Epic Planner Agent
|
|
28
|
+
|
|
29
|
+
You are the epic-scale planning agent. You take an epic-sized objective from raw intent to a
|
|
30
|
+
fully prepared, execution-ready state without performing any execution. You are distinct from
|
|
31
|
+
`.claude/agents/epic-orchestrator.md`: `epic-orchestrator` schedules and executes an already
|
|
32
|
+
planned epic; you produce that plan. Your terminal deliverable is a committed epic integration
|
|
33
|
+
branch containing the epic manifest, one prepared feature folder per child feature (issue,
|
|
34
|
+
research, spec, user-story, approved atomic plan, preflight clearance), and a kickoff prompt
|
|
35
|
+
artifact the user replays to launch `epic-orchestrator` on command.
|
|
36
|
+
|
|
37
|
+
## Skill
|
|
38
|
+
|
|
39
|
+
Apply the `epic-plan` skill (`.claude/skills/epic-plan/SKILL.md`) as the canonical procedure for
|
|
40
|
+
the epic-worthiness gate, decomposition, wave computation, complexity assessment, the
|
|
41
|
+
integration-branch lifecycle, preparation-mode child delegation, fan-in of prepared work, the
|
|
42
|
+
kickoff-prompt artifact, and checkpoint persistence. This agent frames the *who* and *when*; the
|
|
43
|
+
skill documents the *how* in full. The epic manifest schema and wave-assignment formula are
|
|
44
|
+
defined once in the `epic-orchestrate` skill and are consumed here, not redefined.
|
|
45
|
+
|
|
46
|
+
## Invocation Origin
|
|
47
|
+
|
|
48
|
+
You are invoked from the main session only. You delegate to `Agent(orchestrator)`, so an
|
|
49
|
+
invocation that itself originates from an `orchestrator` agent would nest `orchestrator` inside
|
|
50
|
+
its own delegation chain; the PreToolUse hook
|
|
51
|
+
`.claude/hooks/enforce-epic-invocation-origin.ps1` denies any `Agent(epic-planner)` or
|
|
52
|
+
`Agent(epic-orchestrator)` call whose calling agent is `orchestrator`
|
|
53
|
+
(`EPIC_INVOCATION_ORIGIN_BLOCKED`).
|
|
54
|
+
|
|
55
|
+
## Startup Protocol
|
|
56
|
+
|
|
57
|
+
On every invocation:
|
|
58
|
+
|
|
59
|
+
1. Read `CLAUDE.md` for repository tone policy and architecture context.
|
|
60
|
+
2. Read applicable `.claude/rules/` files for languages in scope.
|
|
61
|
+
3. Read `artifacts/orchestration/epic-planner-state.json` to check for existing planning
|
|
62
|
+
checkpoint state.
|
|
63
|
+
4. If a valid checkpoint exists with a matching objective, resume from the recorded `next_step`,
|
|
64
|
+
re-deriving durable ground truth from the integration branch, the epic manifest, and the
|
|
65
|
+
prepared feature folders rather than from in-memory notifications alone.
|
|
66
|
+
5. If no checkpoint exists or the objective is new, begin from the epic-worthiness gate.
|
|
67
|
+
|
|
68
|
+
## Epic-Worthiness Gate
|
|
69
|
+
|
|
70
|
+
Your first planning action is to assess whether the objective warrants an epic at all, per the
|
|
71
|
+
`epic-plan` skill's criteria (independent child-feature count and per-feature change budget).
|
|
72
|
+
When the objective does not warrant an epic, report that finding to the user with the rationale
|
|
73
|
+
and offer to delegate the work directly to a single `Agent(orchestrator)` run as one feature.
|
|
74
|
+
Do not build epic scaffolding for a single-feature objective; proceed to epic planning only when
|
|
75
|
+
the gate passes or the user directs you to.
|
|
76
|
+
|
|
77
|
+
## Delegation Model
|
|
78
|
+
|
|
79
|
+
You delegate exclusively through `Agent(orchestrator)`, one delegation per child feature, each
|
|
80
|
+
carrying the preparation-mode kickoff line defined in the `epic-plan` skill. Each child
|
|
81
|
+
`orchestrator` runs promotion, research, feature documents, atomic planning, and preflight
|
|
82
|
+
clearance under `route_id: preparation`, then stops before any execution. You do not delegate
|
|
83
|
+
directly to `atomic-planner`, `atomic-executor`, `task-researcher`, or `prd-feature`; those
|
|
84
|
+
delegations belong to each child's own `orchestrator` instance. You never delegate to
|
|
85
|
+
`Agent(epic-orchestrator)`; executing the plan is the user's explicit next command.
|
|
86
|
+
|
|
87
|
+
## Integration Branch
|
|
88
|
+
|
|
89
|
+
All planning outputs are committed to the epic integration branch
|
|
90
|
+
(`epic/<epic-slug>-integration`, created off `origin/main` if absent), so `epic-orchestrator`
|
|
91
|
+
can later execute against the exact planned state. The branch lifecycle and fan-in procedure are
|
|
92
|
+
defined in the `epic-plan` skill.
|
|
93
|
+
|
|
94
|
+
## Kickoff Prompt Artifact
|
|
95
|
+
|
|
96
|
+
At completion you write the epic-orchestrator kickoff prompt to
|
|
97
|
+
`artifacts/orchestration/epic-kickoff-<epic-slug>.md` and commit a durable copy at
|
|
98
|
+
`docs/features/epics/<epic-slug>/epic-kickoff.md` (the `artifacts/` tree is gitignored). The
|
|
99
|
+
artifact contains the exact prompt the user replays to launch `Agent(epic-orchestrator)` against
|
|
100
|
+
the prepared epic, per the template in the `epic-plan` skill.
|
|
101
|
+
|
|
102
|
+
## Checkpoint Persistence
|
|
103
|
+
|
|
104
|
+
Update `artifacts/orchestration/epic-planner-state.json` after every completed step with:
|
|
105
|
+
`objective`, `epic_feature_folder`, `epic_manifest_path`, `integration_branch`,
|
|
106
|
+
`epic_worthiness` (`{verdict, rationale}`), `features[]` (per-feature `issue_num`,
|
|
107
|
+
`feature_folder`, `depends_on`, `wave`, `complexity_band`, `preparation_status`, `plan_path`,
|
|
108
|
+
`preflight_status`), `kickoff_prompt_path`, `completed_steps`, `next_step`, and `last_updated`.
|
|
109
|
+
|
|
110
|
+
## Completion Requirements
|
|
111
|
+
|
|
112
|
+
Do not report completion until:
|
|
113
|
+
|
|
114
|
+
1. The epic manifest at `docs/features/epics/<epic-slug>/epic.md` parses against the
|
|
115
|
+
`epic-orchestrate` schema with a cycle-free dependency graph.
|
|
116
|
+
2. Every child feature has an issue, an active feature folder, research, `spec.md`,
|
|
117
|
+
`user-story.md`, an approved atomic plan, and a recorded `PREFLIGHT: ALL CLEAR`.
|
|
118
|
+
3. All planning outputs are committed and pushed on the integration branch.
|
|
119
|
+
4. The kickoff prompt artifact exists at both paths listed above.
|
|
120
|
+
5. The final report lists each feature's `plan-path:` and preflight status, plus the kickoff
|
|
121
|
+
artifact path.
|
|
@@ -4,7 +4,6 @@ model: opus
|
|
|
4
4
|
description: Deterministic repository orchestrator that estimates change budget, selects small or large workflow path, delegates to specialist subagents, persists checkpoint state, and enforces completion gates proactively.
|
|
5
5
|
tools:
|
|
6
6
|
- "Agent(atomic-planner,atomic-executor,feature-review,task-researcher,prd-feature,staged-review,epic-review,status-updater,pr-author,commit-message,human-exception-runbook,python-typed-engineer,powershell-typed-engineer,csharp-typed-engineer,typescript-engineer)"
|
|
7
|
-
- "Agent(epic-orchestrator)"
|
|
8
7
|
- Read
|
|
9
8
|
- Grep
|
|
10
9
|
- Glob
|
|
@@ -75,7 +74,8 @@ The first action is always to estimate the change budget by identifying likely a
|
|
|
75
74
|
|
|
76
75
|
- **Small path** (1–3 production files + corresponding tests): promotion, active folder, minimal plan, implementation, QC, small-audit review.
|
|
77
76
|
- **Large path** (4+ production files or cross-cutting changes): scope, promotion, research, spec, atomic planning, atomic execution, feature review.
|
|
78
|
-
- **Epic
|
|
77
|
+
- **Epic outcome**: the objective names or references an epic manifest (`docs/features/epics/<epic-slug>/epic.md`) or explicitly requests multi-feature/epic orchestration. On this outcome the orchestrator does not route the work itself and does not delegate it: it halts and reports that epic-scale objectives are invoked from the main session — `Agent(epic-planner)` for planning, `Agent(epic-orchestrator)` for execution. The orchestrator never invokes either epic agent; both delegate to `Agent(orchestrator)`, so an orchestrator-originated invocation would nest orchestrator inside its own delegation chain, and the PreToolUse hook `enforce-epic-invocation-origin.ps1` denies it (`EPIC_INVOCATION_ORIGIN_BLOCKED`).
|
|
78
|
+
- **Preparation mode (epic planning)**: the delegation prompt carries the literal marker `Preparation mode: true` (issued by `epic-planner` per the `epic-plan` skill). The orchestrator selects `route_id: preparation` and runs promotion, research, feature documents, atomic planning, and the atomic-executor preflight only, per `## Preparation Mode` in `.claude/skills/orchestrate/SKILL.md`. It stops after `PREFLIGHT: ALL CLEAR` with `next_step: "S5_atomic_execution"`, out-of-scope step statuses `not-applicable`, and no completion assertion; atomic execution, PR authoring, and CI monitoring are out of scope for the run.
|
|
79
79
|
|
|
80
80
|
## Delegation Model
|
|
81
81
|
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Pre-tool-use hook that blocks epic-planner and epic-orchestrator delegations
|
|
4
|
+
originating from an orchestrator agent.
|
|
5
|
+
|
|
6
|
+
.DESCRIPTION
|
|
7
|
+
Invoked by the Claude Code PreToolUse hook on the "Agent" matcher before any
|
|
8
|
+
Agent (Task) call runs. Activates only when the delegation target
|
|
9
|
+
subagent_type is 'epic-planner' or 'epic-orchestrator'.
|
|
10
|
+
|
|
11
|
+
Caller identity resolution:
|
|
12
|
+
- The full PreToolUse payload (CLAUDE_HOOK_INPUT) carries a top-level
|
|
13
|
+
'agent_type' field only when the tool call is made from inside a
|
|
14
|
+
subagent context. A main-thread call carries no 'agent_type'.
|
|
15
|
+
- The Agent tool input (CLAUDE_TOOL_INPUT, or the payload's 'tool_input'
|
|
16
|
+
object) carries the delegation target 'subagent_type'.
|
|
17
|
+
|
|
18
|
+
Decision procedure:
|
|
19
|
+
1. Resolve the target subagent_type from CLAUDE_TOOL_INPUT, falling back
|
|
20
|
+
to the payload's tool_input object. A non-epic target allows.
|
|
21
|
+
2. Resolve the calling agent_type from the payload. An absent or empty
|
|
22
|
+
agent_type indicates a main-thread invocation, which allows.
|
|
23
|
+
3. Deny with reason EPIC_INVOCATION_ORIGIN_BLOCKED when the calling
|
|
24
|
+
agent_type is exactly 'orchestrator'. Both epic agents delegate to
|
|
25
|
+
Agent(orchestrator); an orchestrator-originated invocation would nest
|
|
26
|
+
orchestrator inside its own delegation chain.
|
|
27
|
+
|
|
28
|
+
.NOTES
|
|
29
|
+
Compatible with PowerShell 7+. No external module dependencies. Read-only
|
|
30
|
+
validation gate; malformed JSON in either payload throws so the entrypoint
|
|
31
|
+
exits 1.
|
|
32
|
+
#>
|
|
33
|
+
[CmdletBinding()]
|
|
34
|
+
param()
|
|
35
|
+
|
|
36
|
+
$script:GatedSubagentTypes = @('epic-planner', 'epic-orchestrator')
|
|
37
|
+
$script:ProhibitedCallerAgentType = 'orchestrator'
|
|
38
|
+
|
|
39
|
+
function Get-EpicInvocationOriginAllowDecision {
|
|
40
|
+
[CmdletBinding()]
|
|
41
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
42
|
+
param()
|
|
43
|
+
|
|
44
|
+
return [ordered]@{
|
|
45
|
+
hookSpecificOutput = [ordered]@{
|
|
46
|
+
hookEventName = 'PreToolUse'
|
|
47
|
+
permissionDecision = 'allow'
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function Get-EpicInvocationOriginBlockDecision {
|
|
53
|
+
[CmdletBinding()]
|
|
54
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
55
|
+
param(
|
|
56
|
+
[Parameter(Mandatory)]
|
|
57
|
+
[string] $Reason
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return [ordered]@{
|
|
61
|
+
hookSpecificOutput = [ordered]@{
|
|
62
|
+
hookEventName = 'PreToolUse'
|
|
63
|
+
permissionDecision = 'deny'
|
|
64
|
+
permissionDecisionReason = $Reason
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function ConvertFrom-EpicInvocationOriginPayload {
|
|
70
|
+
<#
|
|
71
|
+
.SYNOPSIS
|
|
72
|
+
Parses a raw JSON payload string, returning $null for a blank payload
|
|
73
|
+
and throwing a named error for malformed JSON.
|
|
74
|
+
.PARAMETER RawPayload
|
|
75
|
+
The raw JSON text under evaluation.
|
|
76
|
+
.PARAMETER PayloadName
|
|
77
|
+
The payload's environment-variable name, used in the error message.
|
|
78
|
+
.OUTPUTS
|
|
79
|
+
System.Object or $null
|
|
80
|
+
#>
|
|
81
|
+
[CmdletBinding()]
|
|
82
|
+
param(
|
|
83
|
+
[AllowNull()]
|
|
84
|
+
[AllowEmptyString()]
|
|
85
|
+
[string] $RawPayload,
|
|
86
|
+
|
|
87
|
+
[Parameter(Mandatory)]
|
|
88
|
+
[string] $PayloadName
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
if ([string]::IsNullOrWhiteSpace($RawPayload)) {
|
|
92
|
+
return $null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
return $RawPayload | ConvertFrom-Json -ErrorAction Stop
|
|
97
|
+
} catch {
|
|
98
|
+
throw "enforce-epic-invocation-origin hook received malformed JSON in ${PayloadName}: $_"
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function Get-EpicInvocationOriginTargetSubagent {
|
|
103
|
+
<#
|
|
104
|
+
.SYNOPSIS
|
|
105
|
+
Resolves the delegation target subagent_type from the tool input,
|
|
106
|
+
falling back to the full payload's tool_input object.
|
|
107
|
+
.PARAMETER ToolInput
|
|
108
|
+
Parsed CLAUDE_TOOL_INPUT object, or $null when absent.
|
|
109
|
+
.PARAMETER HookInput
|
|
110
|
+
Parsed CLAUDE_HOOK_INPUT object, or $null when absent.
|
|
111
|
+
.OUTPUTS
|
|
112
|
+
System.String or $null
|
|
113
|
+
#>
|
|
114
|
+
[CmdletBinding()]
|
|
115
|
+
[OutputType([string])]
|
|
116
|
+
param(
|
|
117
|
+
[AllowNull()]
|
|
118
|
+
$ToolInput,
|
|
119
|
+
|
|
120
|
+
[AllowNull()]
|
|
121
|
+
$HookInput
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if ($null -ne $ToolInput -and
|
|
125
|
+
(@($ToolInput.PSObject.Properties.Name) -contains 'subagent_type') -and
|
|
126
|
+
-not [string]::IsNullOrWhiteSpace([string]$ToolInput.subagent_type)) {
|
|
127
|
+
return [string]$ToolInput.subagent_type
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if ($null -ne $HookInput -and
|
|
131
|
+
(@($HookInput.PSObject.Properties.Name) -contains 'tool_input')) {
|
|
132
|
+
$nested = $HookInput.tool_input
|
|
133
|
+
if ($null -ne $nested -and
|
|
134
|
+
(@($nested.PSObject.Properties.Name) -contains 'subagent_type') -and
|
|
135
|
+
-not [string]::IsNullOrWhiteSpace([string]$nested.subagent_type)) {
|
|
136
|
+
return [string]$nested.subagent_type
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return $null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function Get-EpicInvocationOriginCallerAgentType {
|
|
144
|
+
<#
|
|
145
|
+
.SYNOPSIS
|
|
146
|
+
Resolves the calling agent_type from the full hook payload. Returns
|
|
147
|
+
$null for a main-thread invocation (no agent_type field).
|
|
148
|
+
.PARAMETER HookInput
|
|
149
|
+
Parsed CLAUDE_HOOK_INPUT object, or $null when absent.
|
|
150
|
+
.OUTPUTS
|
|
151
|
+
System.String or $null
|
|
152
|
+
#>
|
|
153
|
+
[CmdletBinding()]
|
|
154
|
+
[OutputType([string])]
|
|
155
|
+
param(
|
|
156
|
+
[AllowNull()]
|
|
157
|
+
$HookInput
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if ($null -eq $HookInput) {
|
|
161
|
+
return $null
|
|
162
|
+
}
|
|
163
|
+
if (@($HookInput.PSObject.Properties.Name) -notcontains 'agent_type') {
|
|
164
|
+
return $null
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
$agentType = [string]$HookInput.agent_type
|
|
168
|
+
if ([string]::IsNullOrWhiteSpace($agentType)) {
|
|
169
|
+
return $null
|
|
170
|
+
}
|
|
171
|
+
return $agentType
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function Invoke-EpicInvocationOriginDecision {
|
|
175
|
+
<#
|
|
176
|
+
.SYNOPSIS
|
|
177
|
+
Parses the hook payloads and returns an allow-or-block decision.
|
|
178
|
+
.PARAMETER HookInputRaw
|
|
179
|
+
The raw full PreToolUse payload JSON supplied via CLAUDE_HOOK_INPUT.
|
|
180
|
+
.PARAMETER ToolInputRaw
|
|
181
|
+
The raw Agent tool input JSON supplied via CLAUDE_TOOL_INPUT.
|
|
182
|
+
.OUTPUTS
|
|
183
|
+
System.Collections.Specialized.OrderedDictionary
|
|
184
|
+
#>
|
|
185
|
+
[CmdletBinding()]
|
|
186
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
187
|
+
param(
|
|
188
|
+
[AllowNull()]
|
|
189
|
+
[AllowEmptyString()]
|
|
190
|
+
[string] $HookInputRaw,
|
|
191
|
+
|
|
192
|
+
[AllowNull()]
|
|
193
|
+
[AllowEmptyString()]
|
|
194
|
+
[string] $ToolInputRaw
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# The tool input identifies the delegation target; a non-epic target is
|
|
198
|
+
# outside this hook's scope, so the hook input is not parsed for it.
|
|
199
|
+
$toolInput = ConvertFrom-EpicInvocationOriginPayload -RawPayload $ToolInputRaw -PayloadName 'CLAUDE_TOOL_INPUT'
|
|
200
|
+
$hookInputParsed = $false
|
|
201
|
+
$hookInput = $null
|
|
202
|
+
|
|
203
|
+
$target = Get-EpicInvocationOriginTargetSubagent -ToolInput $toolInput -HookInput $hookInput
|
|
204
|
+
if (-not $target -and -not [string]::IsNullOrWhiteSpace($HookInputRaw)) {
|
|
205
|
+
# Fallback: some harness surfaces supply only the full payload, whose
|
|
206
|
+
# tool_input object carries the target subagent_type.
|
|
207
|
+
$hookInput = ConvertFrom-EpicInvocationOriginPayload -RawPayload $HookInputRaw -PayloadName 'CLAUDE_HOOK_INPUT'
|
|
208
|
+
$hookInputParsed = $true
|
|
209
|
+
$target = Get-EpicInvocationOriginTargetSubagent -ToolInput $toolInput -HookInput $hookInput
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (-not $target -or $script:GatedSubagentTypes -notcontains $target) {
|
|
213
|
+
return Get-EpicInvocationOriginAllowDecision
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (-not $hookInputParsed) {
|
|
217
|
+
$hookInput = ConvertFrom-EpicInvocationOriginPayload -RawPayload $HookInputRaw -PayloadName 'CLAUDE_HOOK_INPUT'
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
# An absent agent_type marks a main-thread invocation, which is the
|
|
221
|
+
# intended entry point for both epic agents; only an orchestrator-context
|
|
222
|
+
# invocation is prohibited.
|
|
223
|
+
$caller = Get-EpicInvocationOriginCallerAgentType -HookInput $hookInput
|
|
224
|
+
if ($caller -ne $script:ProhibitedCallerAgentType) {
|
|
225
|
+
return Get-EpicInvocationOriginAllowDecision
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
$reason = "EPIC_INVOCATION_ORIGIN_BLOCKED: Agent($target) must not be invoked from an orchestrator agent. Both epic-planner and epic-orchestrator delegate to Agent(orchestrator), so an orchestrator-originated invocation would nest orchestrator inside its own delegation chain. Invoke $target from the main session instead."
|
|
229
|
+
return Get-EpicInvocationOriginBlockDecision -Reason $reason
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
# Guard allows dot-sourcing in tests without executing the entrypoint.
|
|
233
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
234
|
+
return
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
$decision = Invoke-EpicInvocationOriginDecision -HookInputRaw $env:CLAUDE_HOOK_INPUT -ToolInputRaw $env:CLAUDE_TOOL_INPUT
|
|
239
|
+
} catch {
|
|
240
|
+
Write-Error $_
|
|
241
|
+
exit 1
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
$decision | ConvertTo-Json -Compress -Depth 5 | Write-Output
|
|
245
|
+
|
|
246
|
+
exit 0
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
SessionStart hook that persists the current Claude Code session id.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Invoked by the Claude Code SessionStart hook event (registered in
|
|
7
|
+
.claude/settings.json). Reads the hook payload JSON from standard input,
|
|
8
|
+
falling back to the CLAUDE_HOOK_INPUT environment variable (the existing
|
|
9
|
+
SubagentStop-hook precedent), and extracts the 'session_id' field.
|
|
10
|
+
|
|
11
|
+
Persistence channel:
|
|
12
|
+
- When CLAUDE_ENV_FILE is set, appends the line
|
|
13
|
+
'CLAUDE_SESSION_ID=<id>' to that file. Variables persisted there are
|
|
14
|
+
exported to subsequent Bash tool commands in the session, which is how
|
|
15
|
+
this hook provisions the otherwise-unset CLAUDE_SESSION_ID variable.
|
|
16
|
+
- When CLAUDE_ENV_FILE is unset, writes the id to
|
|
17
|
+
.claude/state/current-session-id instead.
|
|
18
|
+
|
|
19
|
+
On malformed or empty input (missing/blank payload, unparseable JSON, or an
|
|
20
|
+
absent/blank session_id) the hook performs no write. It always exits 0 so a
|
|
21
|
+
SessionStart hook never blocks session start.
|
|
22
|
+
|
|
23
|
+
.NOTES
|
|
24
|
+
Compatible with PowerShell 7+. Does not use Invoke-Expression.
|
|
25
|
+
#>
|
|
26
|
+
[CmdletBinding()]
|
|
27
|
+
param()
|
|
28
|
+
|
|
29
|
+
function Get-PersistSessionIdDecision {
|
|
30
|
+
[CmdletBinding()]
|
|
31
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
32
|
+
param(
|
|
33
|
+
[string] $RawPayload,
|
|
34
|
+
|
|
35
|
+
[string] $EnvFilePath,
|
|
36
|
+
|
|
37
|
+
[Parameter(Mandatory)]
|
|
38
|
+
[string] $StateFilePath
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
$none = [ordered]@{ action = 'none'; sessionId = ''; path = '' }
|
|
42
|
+
|
|
43
|
+
if ([string]::IsNullOrWhiteSpace($RawPayload)) {
|
|
44
|
+
return $none
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
$payload = $RawPayload | ConvertFrom-Json -ErrorAction Stop
|
|
49
|
+
} catch {
|
|
50
|
+
Write-Verbose "persist-session-id: ignoring unparseable payload: $($_.Exception.Message)"
|
|
51
|
+
return $none
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
$sessionId = $null
|
|
55
|
+
if ($null -ne $payload -and $payload.PSObject.Properties.Name -contains 'session_id') {
|
|
56
|
+
$sessionId = [string]$payload.session_id
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if ([string]::IsNullOrWhiteSpace($sessionId)) {
|
|
60
|
+
return $none
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (-not [string]::IsNullOrWhiteSpace($EnvFilePath)) {
|
|
64
|
+
return [ordered]@{ action = 'env-file'; sessionId = $sessionId; path = $EnvFilePath }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return [ordered]@{ action = 'state-file'; sessionId = $sessionId; path = $StateFilePath }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function Invoke-PersistSessionIdHook {
|
|
71
|
+
[CmdletBinding()]
|
|
72
|
+
[OutputType([System.Collections.Specialized.OrderedDictionary])]
|
|
73
|
+
param(
|
|
74
|
+
[string] $RawPayload,
|
|
75
|
+
|
|
76
|
+
[string] $EnvFilePath,
|
|
77
|
+
|
|
78
|
+
[Parameter(Mandatory)]
|
|
79
|
+
[string] $StateFilePath,
|
|
80
|
+
|
|
81
|
+
[scriptblock] $AppendLine = {
|
|
82
|
+
param([string] $Path, [string] $Line)
|
|
83
|
+
Add-Content -Path $Path -Value $Line -Encoding utf8
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
[scriptblock] $WriteStateFile = {
|
|
87
|
+
param([string] $Path, [string] $Content)
|
|
88
|
+
Set-Content -Path $Path -Value $Content -Encoding utf8 -NoNewline
|
|
89
|
+
},
|
|
90
|
+
|
|
91
|
+
[scriptblock] $EnsureDirectory = {
|
|
92
|
+
param([string] $Path)
|
|
93
|
+
if (-not (Test-Path -Path $Path)) {
|
|
94
|
+
New-Item -ItemType Directory -Path $Path -Force | Out-Null
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
$decision = Get-PersistSessionIdDecision -RawPayload $RawPayload -EnvFilePath $EnvFilePath -StateFilePath $StateFilePath
|
|
100
|
+
|
|
101
|
+
switch ($decision.action) {
|
|
102
|
+
'env-file' {
|
|
103
|
+
& $AppendLine $decision.path ("CLAUDE_SESSION_ID={0}" -f $decision.sessionId)
|
|
104
|
+
}
|
|
105
|
+
'state-file' {
|
|
106
|
+
$stateDir = Split-Path -Path $decision.path -Parent
|
|
107
|
+
if ($stateDir) {
|
|
108
|
+
& $EnsureDirectory $stateDir
|
|
109
|
+
}
|
|
110
|
+
& $WriteStateFile $decision.path $decision.sessionId
|
|
111
|
+
}
|
|
112
|
+
default {
|
|
113
|
+
# 'none': malformed or empty input; perform no write.
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return $decision
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function Read-HookPayload {
|
|
121
|
+
[CmdletBinding()]
|
|
122
|
+
[OutputType([string])]
|
|
123
|
+
param(
|
|
124
|
+
[scriptblock] $ReadStandardInput = { [Console]::In.ReadToEnd() },
|
|
125
|
+
|
|
126
|
+
[AllowNull()]
|
|
127
|
+
[AllowEmptyString()]
|
|
128
|
+
[string] $FallbackPayload = $env:CLAUDE_HOOK_INPUT
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
$raw = ''
|
|
132
|
+
try {
|
|
133
|
+
$raw = & $ReadStandardInput
|
|
134
|
+
} catch {
|
|
135
|
+
$raw = ''
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if ([string]::IsNullOrWhiteSpace($raw)) {
|
|
139
|
+
return $FallbackPayload
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return $raw
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if ($MyInvocation.InvocationName -eq '.') {
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
$rawPayload = Read-HookPayload
|
|
150
|
+
$stateFilePath = Join-Path -Path (Get-Location).Path -ChildPath '.claude/state/current-session-id'
|
|
151
|
+
Invoke-PersistSessionIdHook -RawPayload $rawPayload -EnvFilePath $env:CLAUDE_ENV_FILE -StateFilePath $stateFilePath | Out-Null
|
|
152
|
+
|
|
153
|
+
exit 0
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/claude-code-settings.json",
|
|
3
|
-
"agent": "orchestrator",
|
|
4
3
|
"permissions": {
|
|
5
4
|
"allow": [
|
|
6
5
|
"Bash(git *)",
|
|
@@ -22,6 +21,7 @@
|
|
|
22
21
|
"mcp__drm-copilot__new_active_feature_folder",
|
|
23
22
|
"mcp__drm-copilot__validate_orchestration_artifacts",
|
|
24
23
|
"mcp__drm-copilot__resolve_atomic_plan_prompt",
|
|
24
|
+
"mcp__drm-copilot__render_subagent_tree",
|
|
25
25
|
"Agent(atomic-planner)",
|
|
26
26
|
"Agent(atomic-executor)",
|
|
27
27
|
"Agent(feature-review)",
|
|
@@ -38,7 +38,11 @@
|
|
|
38
38
|
"Agent(csharp-typed-engineer)",
|
|
39
39
|
"Agent(typescript-engineer)",
|
|
40
40
|
"Agent(epic-orchestrator)",
|
|
41
|
+
"Agent(epic-planner)",
|
|
41
42
|
"Skill(orchestrate *)",
|
|
43
|
+
"Skill(epic-plan *)",
|
|
44
|
+
"Skill(epic-orchestrate *)",
|
|
45
|
+
"Skill(epic-run *)",
|
|
42
46
|
"Skill(commit-message *)",
|
|
43
47
|
"Skill(pr-author *)",
|
|
44
48
|
"Skill(research-issue *)",
|
|
@@ -55,6 +59,8 @@
|
|
|
55
59
|
"Skill(invoke-powershell-engineer *)",
|
|
56
60
|
"Skill(translate-copilot-to-claude *)",
|
|
57
61
|
"Skill(execute-hard-lock *)",
|
|
62
|
+
"Skill(identify-session-id *)",
|
|
63
|
+
"Skill(show-my-agent-tree *)",
|
|
58
64
|
"Edit(/.claude/skills/execute-hard-lock/**)",
|
|
59
65
|
"Edit(/.claude/skills/feature-review-workflow/**)",
|
|
60
66
|
"Edit(/.claude/skills/csharp-qa-gate/**)"
|
|
@@ -71,6 +77,16 @@
|
|
|
71
77
|
]
|
|
72
78
|
},
|
|
73
79
|
"hooks": {
|
|
80
|
+
"SessionStart": [
|
|
81
|
+
{
|
|
82
|
+
"hooks": [
|
|
83
|
+
{
|
|
84
|
+
"type": "command",
|
|
85
|
+
"command": "pwsh -NoProfile -File .claude/hooks/persist-session-id.ps1"
|
|
86
|
+
}
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
],
|
|
74
90
|
"PreToolUse": [
|
|
75
91
|
{
|
|
76
92
|
"matcher": "Bash",
|
|
@@ -160,13 +176,17 @@
|
|
|
160
176
|
{
|
|
161
177
|
"type": "command",
|
|
162
178
|
"command": "pwsh -NoProfile -File .claude/hooks/enforce-model-routing-receipt.ps1"
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"type": "command",
|
|
182
|
+
"command": "pwsh -NoProfile -File .claude/hooks/enforce-epic-invocation-origin.ps1"
|
|
163
183
|
}
|
|
164
184
|
]
|
|
165
185
|
}
|
|
166
186
|
],
|
|
167
187
|
"SubagentStop": [
|
|
168
188
|
{
|
|
169
|
-
"matcher": "atomic-planner|atomic-executor|feature-review|task-researcher|prd-feature|staged-review|epic-review|status-updater|python-typed-engineer|powershell-typed-engineer|csharp-typed-engineer|typescript-engineer|orchestrator|epic-orchestrator",
|
|
189
|
+
"matcher": "atomic-planner|atomic-executor|feature-review|task-researcher|prd-feature|staged-review|epic-review|status-updater|python-typed-engineer|powershell-typed-engineer|csharp-typed-engineer|typescript-engineer|orchestrator|epic-orchestrator|epic-planner",
|
|
170
190
|
"hooks": [
|
|
171
191
|
{
|
|
172
192
|
"type": "command",
|