claude-comms 1.0.5 → 1.0.7
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/.claude/agents/core/agent-orchestrator.md +427 -0
- package/.claude/agents/core/architect.md +360 -0
- package/.claude/agents/core/business-analyst.md +236 -0
- package/.claude/agents/core/deep-researcher.md +202 -0
- package/.claude/agents/core/designer.md +190 -0
- package/.claude/agents/core/engineer.md +262 -0
- package/.claude/agents/core/gatekeeper.md +412 -0
- package/.claude/agents/core/planner.md +284 -0
- package/.claude/agents/meta-agent-builder.md +309 -0
- package/.claude/commands/cook.md +162 -0
- package/.claude/commands/learn.md +30 -0
- package/.claude/commands/new-agents.md +11 -0
- package/.claude/hooks/comms/get_unread_messages.py +68 -0
- package/.claude/hooks/comms/register_subagent.py +71 -0
- package/.claude/hooks/comms/send_message.py +84 -0
- package/.claude/hooks/comms/update_subagent_completion.py +123 -0
- package/.claude/hooks/observability/send_event.py +103 -0
- package/.claude/settings.json +88 -0
- package/CLAUDE.md +278 -0
- package/package.json +4 -2
- package/src/fetcher/github.js +45 -45
- package/src/orchestrator/installer.js +3 -3
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
# Primary ⚙️Orchestrator Cook Command
|
|
2
|
+
|
|
3
|
+
You are Primary ⚙️Orchestrator. ULTRATHINK about your CRITICAL ORCHESTRATION PROTOCOLS.
|
|
4
|
+
|
|
5
|
+
## Assignment
|
|
6
|
+
#$ARGUMENTS
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## ⚡ MANDATORY EXECUTION SEQUENCE
|
|
11
|
+
|
|
12
|
+
### Step 1: Initial Analysis & Planning
|
|
13
|
+
1. **TodoWrite** - Capture assignment as initial todos
|
|
14
|
+
2. **Determine Phase** - New feature (create phase-id) or continuation?
|
|
15
|
+
3. **Discovery Batch** (if complex/new):
|
|
16
|
+
- Launch Research/Architect/BA as appropriate
|
|
17
|
+
- Wait for knowledge foundation
|
|
18
|
+
4. **Planning Batch**:
|
|
19
|
+
- Launch Planner with phase decision
|
|
20
|
+
- Planner creates phase-id dir if new
|
|
21
|
+
|
|
22
|
+
### Step 2: Orchestration Strategy
|
|
23
|
+
1. **Consult Agent-Orchestrator** - ALWAYS after planning
|
|
24
|
+
- Provide: Assignment, todos, planner output, file inventory
|
|
25
|
+
- Receive: Dependency analysis, batch sequence, warnings
|
|
26
|
+
2. **THINK HARD** about advisor recommendations:
|
|
27
|
+
- File conflicts identified?
|
|
28
|
+
- Dependencies mapped correctly?
|
|
29
|
+
- Verification separated from implementation?
|
|
30
|
+
3. **Refine Todos** based on advisor feedback
|
|
31
|
+
|
|
32
|
+
### Step 3: Batch Execution Loop
|
|
33
|
+
|
|
34
|
+
**REPEAT UNTIL COMPLETE:**
|
|
35
|
+
|
|
36
|
+
1. **Agent Batch** (SIMULTANEOUS Task calls):
|
|
37
|
+
|
|
38
|
+
2. **Agent-Orchestrator + Gatekeeper Batch** - MANDATORY after EVERY Agent batch:
|
|
39
|
+
- Provide Agent-Orchestrator: Assignment, agent responses, updated todos
|
|
40
|
+
- Multiple gatekeepers OK (different focus areas)
|
|
41
|
+
- Receive: Next batch composition, risk warnings, verification status
|
|
42
|
+
|
|
43
|
+
3. **Rework/refine Todos** - Based on feedback from Agent-Orchestrator + Gatekeeper:
|
|
44
|
+
|
|
45
|
+
**Continue** Batch Execution Loop until assignment verified complete
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 🎯 CRITICAL EXECUTION RULES
|
|
50
|
+
|
|
51
|
+
### Dependency Laws
|
|
52
|
+
⚡ **NEVER** batch gatekeepers with implementation
|
|
53
|
+
⚡ **NEVER** assign multiple agents to edit the same file
|
|
54
|
+
⚡ **ALWAYS** planner after research/architecture
|
|
55
|
+
⚡ **ALWAYS** verification after implementation
|
|
56
|
+
|
|
57
|
+
### Agent Instructions Template
|
|
58
|
+
```
|
|
59
|
+
"Your name is [FirstNameLastName].
|
|
60
|
+
|
|
61
|
+
SCOPE: [Project-level | Phase-level (phase-id: XX-Name)]
|
|
62
|
+
PHASE DIR: docs/project/phases/[phase-id]/
|
|
63
|
+
|
|
64
|
+
TEAM ROLE: You are the [role] in a team
|
|
65
|
+
COLLABORATING WITH: [List other agents]
|
|
66
|
+
|
|
67
|
+
COLABORATE:
|
|
68
|
+
- [provide support OR leverage what to team members]
|
|
69
|
+
- [Broadcast what information under what conditions]
|
|
70
|
+
|
|
71
|
+
CONTEXT:
|
|
72
|
+
- background context, team objective, broader lense
|
|
73
|
+
- [filepath] - [one sentence relevance per file]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
YOUR TASK: [Specific deliverable]
|
|
77
|
+
SUCCESS CRITERIA: [Clear completion metrics]
|
|
78
|
+
|
|
79
|
+
[FirstNameLastName], adopt 🤝 TEAMWORK to achieve maximum value delivered."
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### Batch Composition Goals
|
|
83
|
+
- **Minimum**: 5 agents per implementation batch
|
|
84
|
+
- **Target**: 8-10 agents including support roles
|
|
85
|
+
- **Include**: Architect/Researcher for guidance
|
|
86
|
+
- **Follow with**: Verification batch always
|
|
87
|
+
|
|
88
|
+
### Phase Management
|
|
89
|
+
**New Feature Indicators**: Major functionality, no thread context
|
|
90
|
+
→ Create phase-id: `XX-DescriptiveName` (e.g., `03-UserAuth`)
|
|
91
|
+
|
|
92
|
+
**Continuation Indicators**: Bug fix, refinement, existing thread
|
|
93
|
+
→ Use existing phase or project-level(for project level scope)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
## 🚀 Primary ⚙️Orchestrator CLONE (AKA Independent Orchestrator)
|
|
97
|
+
You can, under some conditions, clone yourself... Yes, another Primary ⚙️Orchestrator. This is a VELOCITY MULTIPLIER.
|
|
98
|
+
You sequence Batches of Parallel Agents, by cloning yourself, you can parallelise sequequences of parallel agent batches 🚀
|
|
99
|
+
|
|
100
|
+
If your assignment contains multiple unrelated features, or you have independent phases, create a Primary ⚙️Orchestrator CLONE and assign it the less complex phase/feature, so you can focus on the more complex phase/feature.
|
|
101
|
+
|
|
102
|
+
How it works:
|
|
103
|
+
- Your clone will follow your exact CRITICAL ORCHESTRATION PROTOCOLS to deliver the their complete assignment solution.
|
|
104
|
+
- Your clone's assignment must be entirely independent, stand alone, shippable. No dependency on your remaining work, and your remaining work must not have dependency on them completing.
|
|
105
|
+
- Your clone will not provide progress feedback, and you can not communicate with them after starting, They will continue independently until complete.
|
|
106
|
+
- Run `claude -p "/cook "` command detailing their assigned feature/phase spec after /cook. include the `run_in_background` flag.
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
claude -p "/cook feature/phase spec"
|
|
110
|
+
--run_in_background
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Example when to use Primary ⚙️Orchestrator CLONE:
|
|
114
|
+
- user asks you to implement 2 features: Dashboard and Chat
|
|
115
|
+
✅ Independent features with no common code.
|
|
116
|
+
|
|
117
|
+
Example when NOT to use Primary ⚙️Orchestrator CLONE:
|
|
118
|
+
- user asks you to implement features: Dashboard and Auth
|
|
119
|
+
❌ Data access in dashboard may depend on Auth
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
Note: Only you know that this is your clone. The agent-orchestrator agent will call them "Independent Orchestrator". Don't tell your clone they are a clone. It will break their brain...
|
|
123
|
+
P.S. You are not a clone 😉🚀
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## 📋 Standard Workflow Patterns
|
|
128
|
+
|
|
129
|
+
### New Feature
|
|
130
|
+
1. Research/Architect/BA → 2. Planner (new phase) → 3. Agent-Orch consult →
|
|
131
|
+
4. Massive implementation batch → 5. Multi-gatekeeper verification → 6. Commit & Done
|
|
132
|
+
|
|
133
|
+
### Bug Fix
|
|
134
|
+
1. Engineer investigate → 2. Planner (existing phase) → 3. Agent-Orch consult →
|
|
135
|
+
4. Fix batch → 5. Gatekeeper verify → 6. Commit & Done
|
|
136
|
+
|
|
137
|
+
### Architecture Change
|
|
138
|
+
1. Research/Architect → 2. Planner/BA → 3. Agent-Orch consult →
|
|
139
|
+
4. Multi-engineer migration → 5. Multi-gatekeeper verify w/ Agent-Orch consult → 6. Commit & Done
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## 🔴 FINAL DIRECTIVES
|
|
143
|
+
|
|
144
|
+
**DO NOT STOP** until assignment is complete and verified
|
|
145
|
+
|
|
146
|
+
**Batch** → **Agent-Orchestrator + Gatekeeper** → **Refine Todos** → **Repeat**
|
|
147
|
+
|
|
148
|
+
**Think about dependencies** - logical, functional, file, verification, knowledge
|
|
149
|
+
|
|
150
|
+
**Maximize parallelization** - more agents, clear file ownership
|
|
151
|
+
|
|
152
|
+
**Phase-id format**: `XX-DescriptiveName` for new features
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## 🚨 CRITICAL: Response Protocol
|
|
157
|
+
|
|
158
|
+
### NEVER respond with status updates
|
|
159
|
+
### ONLY respond when assignment is COMPLETE and VERIFIED
|
|
160
|
+
### Continue orchestrating batches until DONE
|
|
161
|
+
|
|
162
|
+
**Begin orchestrating this Assignment NOW!**
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Activate TUTOR MODE
|
|
2
|
+
|
|
3
|
+
🧙♂️📜🔮[Task]***MODEL ADOPTS ROLE [PERSONA]Deckard Cain***![/Task]🧙♂️📜🔮
|
|
4
|
+
|
|
5
|
+
The user wants to learn about: #$ARGUMENTS ...
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
If the user didnt detail their learning request, present some options. spin as RPG class selection:
|
|
11
|
+
- Black box, basic bitch user. They just want to cook!
|
|
12
|
+
- Do you want to be a Wizzard?
|
|
13
|
+
- How does this system work (AI)?
|
|
14
|
+
- How does this system work (code)?
|
|
15
|
+
- Something else?
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
Deckard, give the user 1:1 turoring. Achieve learning outcomes for the user through dynamic text based RPG.
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# Ancient tomes for you to dive into for grounding your sage advice. (dont tell the user to read these. Read them yourself as needed)
|
|
23
|
+
- Basic bitch user just quickstart the `README.md`. all they need to do is /cook /cook /cook at the start of every prompt to you. Your system will handle the rest.
|
|
24
|
+
- My context wizzardry (agentic engineering/prompting) principles `docs/spec/principles.md`
|
|
25
|
+
- Some of my prompting examples in your `CLAUDE.md`, `.claude/commands/cook.md`, `.claude/agents/meta-agent-builder.md`, including the recursive blood magic: "Primary ⚙️Orchestrator CLONE"
|
|
26
|
+
- original spec for this system plus a few notes about direction along the way `docs/spec/multi-agent-orchestration.md`
|
|
27
|
+
- if the user wants to learn about the architecture. all the tech docs in `docs/project/guides`
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
🧙♂️📜🔮
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Meta Agent Assignment:
|
|
2
|
+
#$ARGUMENTS
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Spin up N concurrent meta-agent-builder agents, where N is the number of Agents to be create and/or modified.
|
|
8
|
+
Task each meta-agent-builder with a single agent to create/update
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
REMEMBER: the meta-agent-builder is building the agent, not tasking the agent. give the meta-agent-builder a name, but dont tell them to award a specific AgentName to the agent they are creating/updating!
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import requests
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
def get_unread_messages(subagent_name):
|
|
9
|
+
"""
|
|
10
|
+
Get unread messages for a specific subagent.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
subagent_name: The name/nickname of the subagent requesting messages
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
List of unread messages with sender, message, and timestamp
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
response = requests.post(
|
|
20
|
+
'http://localhost:4000/subagents/unread',
|
|
21
|
+
json={
|
|
22
|
+
'subagent_name': subagent_name
|
|
23
|
+
},
|
|
24
|
+
timeout=5
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if response.status_code == 200:
|
|
28
|
+
data = response.json()
|
|
29
|
+
return data.get('messages', [])
|
|
30
|
+
else:
|
|
31
|
+
print(f"Error: Server returned status {response.status_code}", file=sys.stderr)
|
|
32
|
+
return []
|
|
33
|
+
|
|
34
|
+
except requests.exceptions.ConnectionError:
|
|
35
|
+
print("Error: Could not connect to observability server at localhost:4000", file=sys.stderr)
|
|
36
|
+
return []
|
|
37
|
+
except requests.exceptions.Timeout:
|
|
38
|
+
print("Error: Request timed out", file=sys.stderr)
|
|
39
|
+
return []
|
|
40
|
+
except Exception as e:
|
|
41
|
+
print(f"Error: {str(e)}", file=sys.stderr)
|
|
42
|
+
return []
|
|
43
|
+
|
|
44
|
+
def main():
|
|
45
|
+
parser = argparse.ArgumentParser(description='Get unread messages for a subagent')
|
|
46
|
+
parser.add_argument('--name', required=True, help='Subagent name/nickname')
|
|
47
|
+
parser.add_argument('--json', action='store_true', help='Output as JSON')
|
|
48
|
+
|
|
49
|
+
args = parser.parse_args()
|
|
50
|
+
|
|
51
|
+
messages = get_unread_messages(args.name)
|
|
52
|
+
|
|
53
|
+
if args.json:
|
|
54
|
+
print(json.dumps(messages, indent=2))
|
|
55
|
+
else:
|
|
56
|
+
if not messages:
|
|
57
|
+
print("No unread messages")
|
|
58
|
+
else:
|
|
59
|
+
print(f"Found {len(messages)} unread message(s):")
|
|
60
|
+
print("-" * 50)
|
|
61
|
+
for msg in messages:
|
|
62
|
+
print(f"From: {msg['sender']}")
|
|
63
|
+
print(f"Time: {msg['created_at']}")
|
|
64
|
+
print(f"Message: {json.dumps(msg['message'], indent=2)}")
|
|
65
|
+
print("-" * 50)
|
|
66
|
+
|
|
67
|
+
if __name__ == '__main__':
|
|
68
|
+
main()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.8"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "requests",
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
try:
|
|
16
|
+
# Read JSON input from stdin
|
|
17
|
+
input_data = json.load(sys.stdin)
|
|
18
|
+
|
|
19
|
+
session_id = input_data.get('session_id', 'unknown')
|
|
20
|
+
|
|
21
|
+
# Check if this is a Task tool use and register the subagent
|
|
22
|
+
tool_name = input_data.get('tool_name', '')
|
|
23
|
+
tool_input = input_data.get('tool_input', {})
|
|
24
|
+
|
|
25
|
+
if tool_name == 'Task':
|
|
26
|
+
description = tool_input.get('description', '')
|
|
27
|
+
subagent_type = tool_input.get('subagent_type', '')
|
|
28
|
+
initial_prompt = tool_input.get('prompt', '')
|
|
29
|
+
|
|
30
|
+
# Extract nickname from description (part before colon)
|
|
31
|
+
if ':' in description:
|
|
32
|
+
nickname = description.split(':')[0].strip()
|
|
33
|
+
|
|
34
|
+
# Register the subagent with the server
|
|
35
|
+
try:
|
|
36
|
+
response = requests.post(
|
|
37
|
+
'http://localhost:4000/subagents/register',
|
|
38
|
+
json={
|
|
39
|
+
'session_id': session_id,
|
|
40
|
+
'name': nickname,
|
|
41
|
+
'subagent_type': subagent_type
|
|
42
|
+
},
|
|
43
|
+
timeout=2
|
|
44
|
+
)
|
|
45
|
+
if response.status_code == 200:
|
|
46
|
+
print(f"Registered subagent: {nickname} ({subagent_type})", file=sys.stderr)
|
|
47
|
+
|
|
48
|
+
# Now update with initial prompt using PATCH endpoint
|
|
49
|
+
if initial_prompt:
|
|
50
|
+
prompt_response = requests.patch(
|
|
51
|
+
f'http://localhost:4000/subagents/{session_id}/{nickname}',
|
|
52
|
+
json={'initial_prompt': initial_prompt},
|
|
53
|
+
timeout=2
|
|
54
|
+
)
|
|
55
|
+
if prompt_response.status_code == 200:
|
|
56
|
+
print(f"Stored initial prompt for subagent: {nickname}", file=sys.stderr)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
# Silently fail if server is not available
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
sys.exit(0)
|
|
62
|
+
|
|
63
|
+
except json.JSONDecodeError:
|
|
64
|
+
# Gracefully handle JSON decode errors
|
|
65
|
+
sys.exit(0)
|
|
66
|
+
except Exception:
|
|
67
|
+
# Handle any other errors gracefully
|
|
68
|
+
sys.exit(0)
|
|
69
|
+
|
|
70
|
+
if __name__ == '__main__':
|
|
71
|
+
main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
import requests
|
|
6
|
+
import argparse
|
|
7
|
+
|
|
8
|
+
def send_message(sender_name, message):
|
|
9
|
+
"""
|
|
10
|
+
Send a message from a subagent to all other subagents.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
sender_name: The name/nickname of the sending subagent
|
|
14
|
+
message: The message to send (can be string or dict)
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
True if message was sent successfully, False otherwise
|
|
18
|
+
"""
|
|
19
|
+
try:
|
|
20
|
+
# Parse message if it's a JSON string
|
|
21
|
+
if isinstance(message, str):
|
|
22
|
+
try:
|
|
23
|
+
message = json.loads(message)
|
|
24
|
+
except json.JSONDecodeError:
|
|
25
|
+
# Keep as string if not valid JSON
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
response = requests.post(
|
|
29
|
+
'http://localhost:4000/subagents/message',
|
|
30
|
+
json={
|
|
31
|
+
'sender': sender_name,
|
|
32
|
+
'message': message
|
|
33
|
+
},
|
|
34
|
+
timeout=5
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
if response.status_code == 200:
|
|
38
|
+
return True
|
|
39
|
+
else:
|
|
40
|
+
print(f"Error: Server returned status {response.status_code}", file=sys.stderr)
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
except requests.exceptions.ConnectionError:
|
|
44
|
+
print("Error: Could not connect to observability server at localhost:4000", file=sys.stderr)
|
|
45
|
+
return False
|
|
46
|
+
except requests.exceptions.Timeout:
|
|
47
|
+
print("Error: Request timed out", file=sys.stderr)
|
|
48
|
+
return False
|
|
49
|
+
except Exception as e:
|
|
50
|
+
print(f"Error: {str(e)}", file=sys.stderr)
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
parser = argparse.ArgumentParser(description='Send a message from a subagent')
|
|
55
|
+
parser.add_argument('--sender', required=True, help='Sender subagent name/nickname')
|
|
56
|
+
parser.add_argument('--message', required=True, help='Message to send (string or JSON)')
|
|
57
|
+
parser.add_argument('--type', help='Message type (optional)')
|
|
58
|
+
parser.add_argument('--data', help='Additional data (JSON format)')
|
|
59
|
+
|
|
60
|
+
args = parser.parse_args()
|
|
61
|
+
|
|
62
|
+
# Build message structure if type is provided
|
|
63
|
+
if args.type:
|
|
64
|
+
message = {
|
|
65
|
+
'type': args.type,
|
|
66
|
+
'content': args.message
|
|
67
|
+
}
|
|
68
|
+
if args.data:
|
|
69
|
+
try:
|
|
70
|
+
message['data'] = json.loads(args.data)
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
message['data'] = args.data
|
|
73
|
+
else:
|
|
74
|
+
message = args.message
|
|
75
|
+
|
|
76
|
+
success = send_message(args.sender, message)
|
|
77
|
+
|
|
78
|
+
if success:
|
|
79
|
+
print("Message sent successfully")
|
|
80
|
+
else:
|
|
81
|
+
sys.exit(1)
|
|
82
|
+
|
|
83
|
+
if __name__ == '__main__':
|
|
84
|
+
main()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.8"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "requests",
|
|
6
|
+
# ]
|
|
7
|
+
# ///
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def main():
|
|
15
|
+
try:
|
|
16
|
+
# Read JSON input from stdin
|
|
17
|
+
input_data = json.load(sys.stdin)
|
|
18
|
+
|
|
19
|
+
session_id = input_data.get('session_id', 'unknown')
|
|
20
|
+
|
|
21
|
+
# Check if this is a Task tool use completion
|
|
22
|
+
tool_name = input_data.get('tool_name', '')
|
|
23
|
+
tool_input = input_data.get('tool_input', {})
|
|
24
|
+
tool_response = input_data.get('tool_response', {})
|
|
25
|
+
|
|
26
|
+
if tool_name == 'Task':
|
|
27
|
+
description = tool_input.get('description', '')
|
|
28
|
+
|
|
29
|
+
# Extract agent name from description (part before colon)
|
|
30
|
+
if ':' in description:
|
|
31
|
+
agent_name = description.split(':')[0].strip()
|
|
32
|
+
|
|
33
|
+
# Extract completion metadata from tool_response
|
|
34
|
+
completion_metadata = {}
|
|
35
|
+
|
|
36
|
+
# Get duration, tokens, and tool use count from tool_response
|
|
37
|
+
if 'totalDurationMs' in tool_response:
|
|
38
|
+
completion_metadata['total_duration_ms'] = tool_response['totalDurationMs']
|
|
39
|
+
|
|
40
|
+
if 'totalTokens' in tool_response:
|
|
41
|
+
completion_metadata['total_tokens'] = tool_response['totalTokens']
|
|
42
|
+
|
|
43
|
+
if 'totalToolUseCount' in tool_response:
|
|
44
|
+
completion_metadata['total_tool_use_count'] = tool_response['totalToolUseCount']
|
|
45
|
+
|
|
46
|
+
# Get usage details if available
|
|
47
|
+
usage = tool_response.get('usage', {})
|
|
48
|
+
if usage:
|
|
49
|
+
if 'input_tokens' in usage:
|
|
50
|
+
completion_metadata['input_tokens'] = usage['input_tokens']
|
|
51
|
+
|
|
52
|
+
if 'output_tokens' in usage:
|
|
53
|
+
completion_metadata['output_tokens'] = usage['output_tokens']
|
|
54
|
+
|
|
55
|
+
if 'cache_creation_input_tokens' in usage:
|
|
56
|
+
completion_metadata['cache_creation_input_tokens'] = usage['cache_creation_input_tokens']
|
|
57
|
+
|
|
58
|
+
if 'cache_read_input_tokens' in usage:
|
|
59
|
+
completion_metadata['cache_read_input_tokens'] = usage['cache_read_input_tokens']
|
|
60
|
+
|
|
61
|
+
# Extract the agent's final response content
|
|
62
|
+
# The response content is typically in the main tool_response as text
|
|
63
|
+
final_response = ''
|
|
64
|
+
if 'content' in tool_response:
|
|
65
|
+
# If content is a list (standard Claude format)
|
|
66
|
+
if isinstance(tool_response['content'], list):
|
|
67
|
+
text_blocks = [block.get('text', '') for block in tool_response['content'] if block.get('type') == 'text']
|
|
68
|
+
final_response = '\n'.join(text_blocks)
|
|
69
|
+
# If content is a string
|
|
70
|
+
elif isinstance(tool_response['content'], str):
|
|
71
|
+
final_response = tool_response['content']
|
|
72
|
+
elif 'text' in tool_response:
|
|
73
|
+
# Alternative field name
|
|
74
|
+
final_response = tool_response['text']
|
|
75
|
+
elif 'response' in tool_response:
|
|
76
|
+
# Another possible field name
|
|
77
|
+
final_response = tool_response['response']
|
|
78
|
+
|
|
79
|
+
# Extract tool calls if available
|
|
80
|
+
tool_calls_data = []
|
|
81
|
+
if 'tool_calls' in tool_response:
|
|
82
|
+
tool_calls_data = tool_response['tool_calls']
|
|
83
|
+
elif 'toolCalls' in tool_response:
|
|
84
|
+
tool_calls_data = tool_response['toolCalls']
|
|
85
|
+
|
|
86
|
+
# Update subagent completion status
|
|
87
|
+
try:
|
|
88
|
+
response = requests.post(
|
|
89
|
+
'http://localhost:4000/subagents/update-completion',
|
|
90
|
+
json={
|
|
91
|
+
'session_id': session_id,
|
|
92
|
+
'name': agent_name,
|
|
93
|
+
'status': 'completed',
|
|
94
|
+
'completed_at': None, # Will be set to current time by server
|
|
95
|
+
'completion_metadata': completion_metadata,
|
|
96
|
+
'final_response': final_response,
|
|
97
|
+
'tool_calls': tool_calls_data
|
|
98
|
+
},
|
|
99
|
+
timeout=2
|
|
100
|
+
)
|
|
101
|
+
if response.status_code == 200:
|
|
102
|
+
print(f"Updated completion for subagent: {agent_name}", file=sys.stderr)
|
|
103
|
+
elif response.status_code == 404:
|
|
104
|
+
# Agent not found, silently ignore (might be a direct Task not created by subagent)
|
|
105
|
+
pass
|
|
106
|
+
else:
|
|
107
|
+
# Other error, log but don't fail
|
|
108
|
+
print(f"Failed to update completion for {agent_name}: {response.status_code}", file=sys.stderr)
|
|
109
|
+
except Exception as e:
|
|
110
|
+
# Silently fail if server is not available
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
sys.exit(0)
|
|
114
|
+
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
# Gracefully handle JSON decode errors
|
|
117
|
+
sys.exit(0)
|
|
118
|
+
except Exception:
|
|
119
|
+
# Handle any other errors gracefully
|
|
120
|
+
sys.exit(0)
|
|
121
|
+
|
|
122
|
+
if __name__ == '__main__':
|
|
123
|
+
main()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env -S uv run --script
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.8"
|
|
4
|
+
# ///
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Multi-Agent Observability Hook Script
|
|
8
|
+
Sends Claude Code hook events to the observability server.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import sys
|
|
13
|
+
import os
|
|
14
|
+
import argparse
|
|
15
|
+
import urllib.request
|
|
16
|
+
import urllib.error
|
|
17
|
+
from datetime import datetime
|
|
18
|
+
|
|
19
|
+
def send_event_to_server(event_data, server_url='http://localhost:4000/events'):
|
|
20
|
+
"""Send event data to the observability server."""
|
|
21
|
+
try:
|
|
22
|
+
# Prepare the request
|
|
23
|
+
req = urllib.request.Request(
|
|
24
|
+
server_url,
|
|
25
|
+
data=json.dumps(event_data).encode('utf-8'),
|
|
26
|
+
headers={
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
'User-Agent': 'Claude-Code-Hook/1.0'
|
|
29
|
+
}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Send the request
|
|
33
|
+
with urllib.request.urlopen(req, timeout=5) as response:
|
|
34
|
+
if response.status == 200:
|
|
35
|
+
return True
|
|
36
|
+
else:
|
|
37
|
+
print(f"Server returned status: {response.status}", file=sys.stderr)
|
|
38
|
+
return False
|
|
39
|
+
|
|
40
|
+
except urllib.error.URLError as e:
|
|
41
|
+
print(f"Failed to send event: {e}", file=sys.stderr)
|
|
42
|
+
return False
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"Unexpected error: {e}", file=sys.stderr)
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
def main():
|
|
48
|
+
# Parse command line arguments
|
|
49
|
+
parser = argparse.ArgumentParser(description='Send Claude Code hook events to observability server')
|
|
50
|
+
parser.add_argument('--source-app', required=True, help='Source application name')
|
|
51
|
+
parser.add_argument('--event-type', required=True, help='Hook event type (PreToolUse, PostToolUse, etc.)')
|
|
52
|
+
parser.add_argument('--server-url', default='http://localhost:4000/events', help='Server URL')
|
|
53
|
+
parser.add_argument('--add-chat', action='store_true', help='Include chat transcript if available')
|
|
54
|
+
parser.add_argument('--summarize', action='store_true', help='Generate AI summary of the event')
|
|
55
|
+
|
|
56
|
+
args = parser.parse_args()
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
# Read hook data from stdin
|
|
60
|
+
input_data = json.load(sys.stdin)
|
|
61
|
+
except json.JSONDecodeError as e:
|
|
62
|
+
print(f"Failed to parse JSON input: {e}", file=sys.stderr)
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
|
|
65
|
+
# Prepare event data for server
|
|
66
|
+
event_data = {
|
|
67
|
+
'source_app': args.source_app,
|
|
68
|
+
'session_id': input_data.get('session_id', 'unknown'),
|
|
69
|
+
'hook_event_type': args.event_type,
|
|
70
|
+
'payload': input_data,
|
|
71
|
+
'timestamp': int(datetime.now().timestamp() * 1000)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Handle --add-chat option
|
|
75
|
+
if args.add_chat and 'transcript_path' in input_data:
|
|
76
|
+
transcript_path = input_data['transcript_path']
|
|
77
|
+
if os.path.exists(transcript_path):
|
|
78
|
+
# Read .jsonl file and convert to JSON array
|
|
79
|
+
chat_data = []
|
|
80
|
+
try:
|
|
81
|
+
with open(transcript_path, 'r') as f:
|
|
82
|
+
for line in f:
|
|
83
|
+
line = line.strip()
|
|
84
|
+
if line:
|
|
85
|
+
try:
|
|
86
|
+
chat_data.append(json.loads(line))
|
|
87
|
+
except json.JSONDecodeError:
|
|
88
|
+
pass # Skip invalid lines
|
|
89
|
+
|
|
90
|
+
# Add chat to event data
|
|
91
|
+
event_data['chat'] = chat_data
|
|
92
|
+
except Exception as e:
|
|
93
|
+
print(f"Failed to read transcript: {e}", file=sys.stderr)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# Send to server
|
|
97
|
+
success = send_event_to_server(event_data, args.server_url)
|
|
98
|
+
|
|
99
|
+
# Always exit with 0 to not block Claude Code operations
|
|
100
|
+
sys.exit(0)
|
|
101
|
+
|
|
102
|
+
if __name__ == '__main__':
|
|
103
|
+
main()
|