slack-claude-code 0.1.1__tar.gz
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.
- slack_claude_code-0.1.1/PKG-INFO +163 -0
- slack_claude_code-0.1.1/README.md +142 -0
- slack_claude_code-0.1.1/pyproject.toml +29 -0
- slack_claude_code-0.1.1/src/__init__.py +1 -0
- slack_claude_code-0.1.1/src/agents/__init__.py +10 -0
- slack_claude_code-0.1.1/src/agents/orchestrator.py +316 -0
- slack_claude_code-0.1.1/src/agents/roles.py +164 -0
- slack_claude_code-0.1.1/src/app.py +696 -0
- slack_claude_code-0.1.1/src/approval/__init__.py +1 -0
- slack_claude_code-0.1.1/src/approval/handler.py +304 -0
- slack_claude_code-0.1.1/src/approval/plan_manager.py +228 -0
- slack_claude_code-0.1.1/src/approval/slack_ui.py +312 -0
- slack_claude_code-0.1.1/src/claude/__init__.py +2 -0
- slack_claude_code-0.1.1/src/claude/executor.py +291 -0
- slack_claude_code-0.1.1/src/claude/streaming.py +349 -0
- slack_claude_code-0.1.1/src/claude/subprocess_executor.py +516 -0
- slack_claude_code-0.1.1/src/config.py +180 -0
- slack_claude_code-0.1.1/src/database/__init__.py +1 -0
- slack_claude_code-0.1.1/src/database/migrations.py +185 -0
- slack_claude_code-0.1.1/src/database/models.py +262 -0
- slack_claude_code-0.1.1/src/database/repository.py +762 -0
- slack_claude_code-0.1.1/src/exceptions.py +218 -0
- slack_claude_code-0.1.1/src/git/__init__.py +1 -0
- slack_claude_code-0.1.1/src/git/models.py +62 -0
- slack_claude_code-0.1.1/src/git/service.py +353 -0
- slack_claude_code-0.1.1/src/handlers/__init__.py +55 -0
- slack_claude_code-0.1.1/src/handlers/actions.py +1049 -0
- slack_claude_code-0.1.1/src/handlers/agents.py +270 -0
- slack_claude_code-0.1.1/src/handlers/base.py +163 -0
- slack_claude_code-0.1.1/src/handlers/basic.py +137 -0
- slack_claude_code-0.1.1/src/handlers/claude_cli.py +372 -0
- slack_claude_code-0.1.1/src/handlers/git.py +317 -0
- slack_claude_code-0.1.1/src/handlers/mode.py +124 -0
- slack_claude_code-0.1.1/src/handlers/notifications.py +195 -0
- slack_claude_code-0.1.1/src/handlers/parallel.py +71 -0
- slack_claude_code-0.1.1/src/handlers/pty.py +87 -0
- slack_claude_code-0.1.1/src/handlers/queue.py +278 -0
- slack_claude_code-0.1.1/src/handlers/session_management.py +49 -0
- slack_claude_code-0.1.1/src/hooks/__init__.py +4 -0
- slack_claude_code-0.1.1/src/hooks/registry.py +201 -0
- slack_claude_code-0.1.1/src/hooks/types.py +61 -0
- slack_claude_code-0.1.1/src/pty/__init__.py +1 -0
- slack_claude_code-0.1.1/src/pty/parser.py +273 -0
- slack_claude_code-0.1.1/src/pty/pool.py +287 -0
- slack_claude_code-0.1.1/src/pty/process.py +210 -0
- slack_claude_code-0.1.1/src/pty/session.py +317 -0
- slack_claude_code-0.1.1/src/pty/types.py +66 -0
- slack_claude_code-0.1.1/src/question/__init__.py +1 -0
- slack_claude_code-0.1.1/src/question/manager.py +436 -0
- slack_claude_code-0.1.1/src/question/slack_ui.py +328 -0
- slack_claude_code-0.1.1/src/tasks/__init__.py +1 -0
- slack_claude_code-0.1.1/src/tasks/manager.py +351 -0
- slack_claude_code-0.1.1/src/utils/__init__.py +1 -0
- slack_claude_code-0.1.1/src/utils/detail_cache.py +89 -0
- slack_claude_code-0.1.1/src/utils/file_downloader.py +140 -0
- slack_claude_code-0.1.1/src/utils/formatters/__init__.py +1 -0
- slack_claude_code-0.1.1/src/utils/formatters/base.py +180 -0
- slack_claude_code-0.1.1/src/utils/formatters/command.py +148 -0
- slack_claude_code-0.1.1/src/utils/formatters/directory.py +64 -0
- slack_claude_code-0.1.1/src/utils/formatters/job.py +184 -0
- slack_claude_code-0.1.1/src/utils/formatters/markdown.py +85 -0
- slack_claude_code-0.1.1/src/utils/formatters/plan.py +183 -0
- slack_claude_code-0.1.1/src/utils/formatters/queue.py +103 -0
- slack_claude_code-0.1.1/src/utils/formatters/session.py +137 -0
- slack_claude_code-0.1.1/src/utils/formatters/streaming.py +85 -0
- slack_claude_code-0.1.1/src/utils/formatters/tool_blocks.py +343 -0
- slack_claude_code-0.1.1/src/utils/formatting.py +148 -0
- slack_claude_code-0.1.1/src/utils/slack_helpers.py +330 -0
- slack_claude_code-0.1.1/src/utils/streaming.py +223 -0
- slack_claude_code-0.1.1/src/utils/validators.py +36 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: slack-claude-code
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Slack app for running Claude Code CLI commands
|
|
5
|
+
Author: Dan
|
|
6
|
+
Requires-Python: >=3.10,<4.0
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
13
|
+
Requires-Dist: aiofiles (>=24.1.0,<25.0.0)
|
|
14
|
+
Requires-Dist: aiohttp (>=3.13.2,<4.0.0)
|
|
15
|
+
Requires-Dist: aiosqlite (>=0.21.0,<0.22.0)
|
|
16
|
+
Requires-Dist: pexpect (>=4.9.0,<5.0.0)
|
|
17
|
+
Requires-Dist: python-dotenv (>=1.2.1,<2.0.0)
|
|
18
|
+
Requires-Dist: slack-bolt (>=1.27.0,<2.0.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<img src="assets/repo_logo.png" alt="Slack Claude Code Bot" width="1000">
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
**Claude Code, but in Slack.** Access Claude Code remotely from any device, or use it full-time for a better UI experience.
|
|
27
|
+
|
|
28
|
+
## Why Slack?
|
|
29
|
+
|
|
30
|
+
| Feature | Terminal | Slack |
|
|
31
|
+
|---------|----------|-------|
|
|
32
|
+
| **Code blocks** | Plain text | Syntax-highlighted with copy button |
|
|
33
|
+
| **Long output** | Scrolls off screen | "View Details" modal |
|
|
34
|
+
| **Permissions** | Y/n prompts | Approve/Deny buttons |
|
|
35
|
+
| **Parallel work** | Multiple terminals | Threads with isolated sessions |
|
|
36
|
+
| **File sharing** | `cat` or copy-paste | Drag & drop with preview |
|
|
37
|
+
| **Notifications** | Watch the terminal | Alerts when tasks complete |
|
|
38
|
+
|
|
39
|
+
All Claude Code commands work the same way: `/clear`, `/compact`, `/model`, `/mode`, `/add-dir`, `/review`, plus filesystem and git commands.
|
|
40
|
+
|
|
41
|
+
## Commands
|
|
42
|
+
|
|
43
|
+
| Category | Command | Description |
|
|
44
|
+
|----------|---------|-------------|
|
|
45
|
+
| CLI | `/init` | Initialize Claude project configuration |
|
|
46
|
+
| CLI | `/memory` | View/edit Claude's memory and context |
|
|
47
|
+
| CLI | `/review` | Review code changes with Claude |
|
|
48
|
+
| CLI | `/doctor` | Diagnose Claude Code installation issues |
|
|
49
|
+
| CLI | `/stats` | Show session statistics |
|
|
50
|
+
| CLI | `/context` | Display current context information |
|
|
51
|
+
| CLI | `/todos` | List and manage todos |
|
|
52
|
+
| Session | `/clear`, `/compact` | Clear session or compact context |
|
|
53
|
+
| Session | `/cost` | Show session cost |
|
|
54
|
+
| Session | `/pty`, `/sessions`, `/session-cleanup` | PTY session management |
|
|
55
|
+
| Navigation | `/ls`, `/cd`, `/pwd`, `/add-dir` | Directory navigation |
|
|
56
|
+
| Git | `/status`, `/diff`, `/commit`, `/branch` | Git operations |
|
|
57
|
+
| Config | `/model`, `/mode`, `/permissions`, `/notifications` | Configuration |
|
|
58
|
+
| Queue | `/q <cmd>`, `/qv`, `/qc`, `/qr <id>` | Command queue management |
|
|
59
|
+
| Jobs | `/st`, `/cc`, `/esc` | Job control |
|
|
60
|
+
| Multi-Agent | `/task`, `/tasks`, `/task-cancel` | Multi-agent task management |
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
### Prerequisites
|
|
65
|
+
- Python 3.10+
|
|
66
|
+
- [Claude Code CLI](https://github.com/anthropics/claude-code) installed and authenticated
|
|
67
|
+
|
|
68
|
+
### 1. Install dependencies
|
|
69
|
+
```bash
|
|
70
|
+
cd slack-claude-code
|
|
71
|
+
poetry install
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2. Create Slack App
|
|
75
|
+
Go to https://api.slack.com/apps → "Create New App" → "From scratch"
|
|
76
|
+
|
|
77
|
+
**Socket Mode**: Enable and create an app-level token with `connections:write` scope (save the `xapp-` token)
|
|
78
|
+
|
|
79
|
+
**Bot Token Scopes** (OAuth & Permissions):
|
|
80
|
+
- `chat:write`, `commands`, `channels:history`, `app_mentions:read`, `files:write`
|
|
81
|
+
|
|
82
|
+
**Event Subscriptions**: Enable and add `message.channels`, `app_mention`
|
|
83
|
+
|
|
84
|
+
**App Icon**: In "Basic Information" → "Display Information", upload `assets/claude_logo.png` from this repo as the app icon
|
|
85
|
+
|
|
86
|
+
**Slash Commands** (optional): Create commands like `/clear`, `/model`, `/ls`, `/cd`, `/status`, `/diff`, etc.
|
|
87
|
+
|
|
88
|
+
### 3. Configure and run
|
|
89
|
+
```bash
|
|
90
|
+
cp .env.example .env
|
|
91
|
+
# Add your tokens: SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_SIGNING_SECRET
|
|
92
|
+
poetry run python run.py
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Usage
|
|
96
|
+
|
|
97
|
+
Type messages in any channel where the bot is present. Each Slack thread maintains an independent Claude session with its own working directory and context.
|
|
98
|
+
|
|
99
|
+
### Key Features
|
|
100
|
+
|
|
101
|
+
- **Threads = Sessions**: Each thread has isolated context; `/clear` only affects that thread
|
|
102
|
+
- **File Uploads**: Drag & drop files—Claude sees them instantly (code, images, PDFs)
|
|
103
|
+
- **Smart Context**: Frequently-used files are automatically included in prompts
|
|
104
|
+
- **Streaming**: Watch Claude's responses as they're generated
|
|
105
|
+
|
|
106
|
+
### Plan Mode
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
/mode plan
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Claude creates a detailed plan before execution, shown with Approve/Reject buttons. Ideal for complex implementations where you want to review the approach first.
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
## Configuration
|
|
116
|
+
|
|
117
|
+
Key environment variables (see `.env.example` for full list):
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Required
|
|
121
|
+
SLACK_BOT_TOKEN=xoxb-...
|
|
122
|
+
SLACK_APP_TOKEN=xapp-...
|
|
123
|
+
SLACK_SIGNING_SECRET=...
|
|
124
|
+
|
|
125
|
+
# Optional
|
|
126
|
+
DEFAULT_WORKING_DIR=/path/to/projects
|
|
127
|
+
COMMAND_TIMEOUT=300 # 5 min default
|
|
128
|
+
CLAUDE_PERMISSION_MODE=approve-all # or: prompt, deny
|
|
129
|
+
AUTO_APPROVE_TOOLS=Read,Glob,Grep,LSP
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Architecture
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
src/
|
|
136
|
+
├── app.py # Main entry point
|
|
137
|
+
├── config.py # Configuration
|
|
138
|
+
├── database/ # SQLite persistence (models, migrations, repository)
|
|
139
|
+
├── claude/ # Claude CLI integration (executor, streaming)
|
|
140
|
+
├── pty/ # PTY session management (session, pool, parser)
|
|
141
|
+
├── handlers/ # Slack command handlers
|
|
142
|
+
├── agents/ # Multi-agent orchestration (planner→worker→evaluator)
|
|
143
|
+
├── approval/ # Permission & plan approval handling
|
|
144
|
+
├── git/ # Git operations (status, diff, commit, branch)
|
|
145
|
+
├── hooks/ # Event hook system
|
|
146
|
+
├── question/ # AskUserQuestion tool support
|
|
147
|
+
├── tasks/ # Background task management
|
|
148
|
+
└── utils/ # Formatters, helpers, validators
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Troubleshooting
|
|
152
|
+
|
|
153
|
+
| Problem | Solution |
|
|
154
|
+
|---------|----------|
|
|
155
|
+
| Configuration errors on startup | Check `.env` has all required tokens |
|
|
156
|
+
| Commands not appearing | Verify slash commands in Slack app settings |
|
|
157
|
+
| Timeouts | Increase `COMMAND_TIMEOUT` |
|
|
158
|
+
| PTY session errors | Use `/pty` → "Restart Session" |
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
|
163
|
+
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/repo_logo.png" alt="Slack Claude Code Bot" width="1000">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
**Claude Code, but in Slack.** Access Claude Code remotely from any device, or use it full-time for a better UI experience.
|
|
7
|
+
|
|
8
|
+
## Why Slack?
|
|
9
|
+
|
|
10
|
+
| Feature | Terminal | Slack |
|
|
11
|
+
|---------|----------|-------|
|
|
12
|
+
| **Code blocks** | Plain text | Syntax-highlighted with copy button |
|
|
13
|
+
| **Long output** | Scrolls off screen | "View Details" modal |
|
|
14
|
+
| **Permissions** | Y/n prompts | Approve/Deny buttons |
|
|
15
|
+
| **Parallel work** | Multiple terminals | Threads with isolated sessions |
|
|
16
|
+
| **File sharing** | `cat` or copy-paste | Drag & drop with preview |
|
|
17
|
+
| **Notifications** | Watch the terminal | Alerts when tasks complete |
|
|
18
|
+
|
|
19
|
+
All Claude Code commands work the same way: `/clear`, `/compact`, `/model`, `/mode`, `/add-dir`, `/review`, plus filesystem and git commands.
|
|
20
|
+
|
|
21
|
+
## Commands
|
|
22
|
+
|
|
23
|
+
| Category | Command | Description |
|
|
24
|
+
|----------|---------|-------------|
|
|
25
|
+
| CLI | `/init` | Initialize Claude project configuration |
|
|
26
|
+
| CLI | `/memory` | View/edit Claude's memory and context |
|
|
27
|
+
| CLI | `/review` | Review code changes with Claude |
|
|
28
|
+
| CLI | `/doctor` | Diagnose Claude Code installation issues |
|
|
29
|
+
| CLI | `/stats` | Show session statistics |
|
|
30
|
+
| CLI | `/context` | Display current context information |
|
|
31
|
+
| CLI | `/todos` | List and manage todos |
|
|
32
|
+
| Session | `/clear`, `/compact` | Clear session or compact context |
|
|
33
|
+
| Session | `/cost` | Show session cost |
|
|
34
|
+
| Session | `/pty`, `/sessions`, `/session-cleanup` | PTY session management |
|
|
35
|
+
| Navigation | `/ls`, `/cd`, `/pwd`, `/add-dir` | Directory navigation |
|
|
36
|
+
| Git | `/status`, `/diff`, `/commit`, `/branch` | Git operations |
|
|
37
|
+
| Config | `/model`, `/mode`, `/permissions`, `/notifications` | Configuration |
|
|
38
|
+
| Queue | `/q <cmd>`, `/qv`, `/qc`, `/qr <id>` | Command queue management |
|
|
39
|
+
| Jobs | `/st`, `/cc`, `/esc` | Job control |
|
|
40
|
+
| Multi-Agent | `/task`, `/tasks`, `/task-cancel` | Multi-agent task management |
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
### Prerequisites
|
|
45
|
+
- Python 3.10+
|
|
46
|
+
- [Claude Code CLI](https://github.com/anthropics/claude-code) installed and authenticated
|
|
47
|
+
|
|
48
|
+
### 1. Install dependencies
|
|
49
|
+
```bash
|
|
50
|
+
cd slack-claude-code
|
|
51
|
+
poetry install
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### 2. Create Slack App
|
|
55
|
+
Go to https://api.slack.com/apps → "Create New App" → "From scratch"
|
|
56
|
+
|
|
57
|
+
**Socket Mode**: Enable and create an app-level token with `connections:write` scope (save the `xapp-` token)
|
|
58
|
+
|
|
59
|
+
**Bot Token Scopes** (OAuth & Permissions):
|
|
60
|
+
- `chat:write`, `commands`, `channels:history`, `app_mentions:read`, `files:write`
|
|
61
|
+
|
|
62
|
+
**Event Subscriptions**: Enable and add `message.channels`, `app_mention`
|
|
63
|
+
|
|
64
|
+
**App Icon**: In "Basic Information" → "Display Information", upload `assets/claude_logo.png` from this repo as the app icon
|
|
65
|
+
|
|
66
|
+
**Slash Commands** (optional): Create commands like `/clear`, `/model`, `/ls`, `/cd`, `/status`, `/diff`, etc.
|
|
67
|
+
|
|
68
|
+
### 3. Configure and run
|
|
69
|
+
```bash
|
|
70
|
+
cp .env.example .env
|
|
71
|
+
# Add your tokens: SLACK_BOT_TOKEN, SLACK_APP_TOKEN, SLACK_SIGNING_SECRET
|
|
72
|
+
poetry run python run.py
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Usage
|
|
76
|
+
|
|
77
|
+
Type messages in any channel where the bot is present. Each Slack thread maintains an independent Claude session with its own working directory and context.
|
|
78
|
+
|
|
79
|
+
### Key Features
|
|
80
|
+
|
|
81
|
+
- **Threads = Sessions**: Each thread has isolated context; `/clear` only affects that thread
|
|
82
|
+
- **File Uploads**: Drag & drop files—Claude sees them instantly (code, images, PDFs)
|
|
83
|
+
- **Smart Context**: Frequently-used files are automatically included in prompts
|
|
84
|
+
- **Streaming**: Watch Claude's responses as they're generated
|
|
85
|
+
|
|
86
|
+
### Plan Mode
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
/mode plan
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Claude creates a detailed plan before execution, shown with Approve/Reject buttons. Ideal for complex implementations where you want to review the approach first.
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
Key environment variables (see `.env.example` for full list):
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
# Required
|
|
101
|
+
SLACK_BOT_TOKEN=xoxb-...
|
|
102
|
+
SLACK_APP_TOKEN=xapp-...
|
|
103
|
+
SLACK_SIGNING_SECRET=...
|
|
104
|
+
|
|
105
|
+
# Optional
|
|
106
|
+
DEFAULT_WORKING_DIR=/path/to/projects
|
|
107
|
+
COMMAND_TIMEOUT=300 # 5 min default
|
|
108
|
+
CLAUDE_PERMISSION_MODE=approve-all # or: prompt, deny
|
|
109
|
+
AUTO_APPROVE_TOOLS=Read,Glob,Grep,LSP
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Architecture
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
src/
|
|
116
|
+
├── app.py # Main entry point
|
|
117
|
+
├── config.py # Configuration
|
|
118
|
+
├── database/ # SQLite persistence (models, migrations, repository)
|
|
119
|
+
├── claude/ # Claude CLI integration (executor, streaming)
|
|
120
|
+
├── pty/ # PTY session management (session, pool, parser)
|
|
121
|
+
├── handlers/ # Slack command handlers
|
|
122
|
+
├── agents/ # Multi-agent orchestration (planner→worker→evaluator)
|
|
123
|
+
├── approval/ # Permission & plan approval handling
|
|
124
|
+
├── git/ # Git operations (status, diff, commit, branch)
|
|
125
|
+
├── hooks/ # Event hook system
|
|
126
|
+
├── question/ # AskUserQuestion tool support
|
|
127
|
+
├── tasks/ # Background task management
|
|
128
|
+
└── utils/ # Formatters, helpers, validators
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Troubleshooting
|
|
132
|
+
|
|
133
|
+
| Problem | Solution |
|
|
134
|
+
|---------|----------|
|
|
135
|
+
| Configuration errors on startup | Check `.env` has all required tokens |
|
|
136
|
+
| Commands not appearing | Verify slash commands in Slack app settings |
|
|
137
|
+
| Timeouts | Increase `COMMAND_TIMEOUT` |
|
|
138
|
+
| PTY session errors | Use `/pty` → "Restart Session" |
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "slack-claude-code"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Slack app for running Claude Code CLI commands"
|
|
5
|
+
authors = ["Dan"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "src"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.scripts]
|
|
10
|
+
ccslack = "run:main"
|
|
11
|
+
|
|
12
|
+
[tool.poetry.dependencies]
|
|
13
|
+
python = "^3.10"
|
|
14
|
+
slack-bolt = "^1.27.0"
|
|
15
|
+
aiosqlite = "^0.21.0"
|
|
16
|
+
python-dotenv = "^1.2.1"
|
|
17
|
+
pexpect = "^4.9.0"
|
|
18
|
+
aiohttp = "^3.13.2"
|
|
19
|
+
aiofiles = "^24.1.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
[tool.poetry.group.dev.dependencies]
|
|
23
|
+
pytest = "^9.0.2"
|
|
24
|
+
pytest-asyncio = "^1.3.0"
|
|
25
|
+
pytest-cov = "^7.0.0"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
29
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Slack Claude Code Bot
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Multi-agent workflow orchestrator.
|
|
2
|
+
|
|
3
|
+
Coordinates Planner -> Worker -> Evaluator pipeline for complex tasks.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from typing import Awaitable, Callable, Optional
|
|
12
|
+
|
|
13
|
+
from ..claude.subprocess_executor import ExecutionResult, SubprocessExecutor
|
|
14
|
+
from .roles import AgentRole, format_task_prompt
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TaskStatus(Enum):
|
|
20
|
+
"""Status of an agent task."""
|
|
21
|
+
|
|
22
|
+
PENDING = "pending"
|
|
23
|
+
PLANNING = "planning"
|
|
24
|
+
WORKING = "working"
|
|
25
|
+
EVALUATING = "evaluating"
|
|
26
|
+
COMPLETED = "completed"
|
|
27
|
+
FAILED = "failed"
|
|
28
|
+
CANCELLED = "cancelled"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class EvalResult(Enum):
|
|
32
|
+
"""Evaluation result from evaluator agent."""
|
|
33
|
+
|
|
34
|
+
COMPLETE = "complete"
|
|
35
|
+
PARTIAL = "partial"
|
|
36
|
+
INCOMPLETE = "incomplete"
|
|
37
|
+
FAILED = "failed"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class AgentTask:
|
|
42
|
+
"""A task being processed by the multi-agent workflow."""
|
|
43
|
+
|
|
44
|
+
task_id: str
|
|
45
|
+
description: str
|
|
46
|
+
channel_id: str
|
|
47
|
+
working_directory: str = "~"
|
|
48
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
49
|
+
plan_output: Optional[str] = None
|
|
50
|
+
work_output: Optional[str] = None
|
|
51
|
+
eval_output: Optional[str] = None
|
|
52
|
+
eval_result: Optional[EvalResult] = None
|
|
53
|
+
error_message: Optional[str] = None
|
|
54
|
+
turn_count: int = 0
|
|
55
|
+
max_turns: int = 50
|
|
56
|
+
slack_thread_ts: Optional[str] = None
|
|
57
|
+
message_ts: Optional[str] = None
|
|
58
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
59
|
+
started_at: Optional[datetime] = None
|
|
60
|
+
completed_at: Optional[datetime] = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class WorkflowResult:
|
|
65
|
+
"""Result of a complete workflow execution."""
|
|
66
|
+
|
|
67
|
+
task: AgentTask
|
|
68
|
+
success: bool
|
|
69
|
+
plan: Optional[str] = None
|
|
70
|
+
work_output: Optional[str] = None
|
|
71
|
+
evaluation: Optional[str] = None
|
|
72
|
+
eval_result: Optional[EvalResult] = None
|
|
73
|
+
total_turns: int = 0
|
|
74
|
+
duration_ms: Optional[int] = None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class MultiAgentOrchestrator:
|
|
78
|
+
"""Orchestrates multi-agent workflows.
|
|
79
|
+
|
|
80
|
+
Runs tasks through Planner -> Worker -> Evaluator pipeline.
|
|
81
|
+
Each agent uses its own isolated PTY session.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(
|
|
85
|
+
self,
|
|
86
|
+
executor: SubprocessExecutor,
|
|
87
|
+
max_iterations: int = 3,
|
|
88
|
+
) -> None:
|
|
89
|
+
"""Initialize orchestrator.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
executor: Claude executor for running agents
|
|
93
|
+
max_iterations: Max worker-evaluator iterations
|
|
94
|
+
"""
|
|
95
|
+
self.executor = executor
|
|
96
|
+
self.max_iterations = max_iterations
|
|
97
|
+
self._active_tasks: dict[str, AgentTask] = {}
|
|
98
|
+
|
|
99
|
+
async def execute_workflow(
|
|
100
|
+
self,
|
|
101
|
+
task: AgentTask,
|
|
102
|
+
on_status_update: Optional[Callable[[AgentTask], Awaitable[None]]] = None,
|
|
103
|
+
) -> WorkflowResult:
|
|
104
|
+
"""Execute the full multi-agent workflow.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
task: The task to execute
|
|
108
|
+
on_status_update: Optional callback for status updates
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
WorkflowResult with outcomes
|
|
112
|
+
"""
|
|
113
|
+
self._active_tasks[task.task_id] = task
|
|
114
|
+
task.started_at = datetime.now()
|
|
115
|
+
start_time = asyncio.get_running_loop().time()
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
# Phase 1: Planning
|
|
119
|
+
task.status = TaskStatus.PLANNING
|
|
120
|
+
if on_status_update:
|
|
121
|
+
await on_status_update(task)
|
|
122
|
+
|
|
123
|
+
plan_result = await self._run_planner(task)
|
|
124
|
+
if not plan_result.success:
|
|
125
|
+
task.status = TaskStatus.FAILED
|
|
126
|
+
task.error_message = plan_result.error
|
|
127
|
+
return self._create_result(task, False, start_time)
|
|
128
|
+
|
|
129
|
+
task.plan_output = plan_result.output
|
|
130
|
+
task.turn_count += 1
|
|
131
|
+
|
|
132
|
+
# Phase 2: Working (with potential re-iterations)
|
|
133
|
+
for iteration in range(self.max_iterations):
|
|
134
|
+
task.status = TaskStatus.WORKING
|
|
135
|
+
if on_status_update:
|
|
136
|
+
await on_status_update(task)
|
|
137
|
+
|
|
138
|
+
work_result = await self._run_worker(task)
|
|
139
|
+
if not work_result.success:
|
|
140
|
+
task.status = TaskStatus.FAILED
|
|
141
|
+
task.error_message = work_result.error
|
|
142
|
+
return self._create_result(task, False, start_time)
|
|
143
|
+
|
|
144
|
+
task.work_output = work_result.output
|
|
145
|
+
task.turn_count += 1
|
|
146
|
+
|
|
147
|
+
# Phase 3: Evaluation
|
|
148
|
+
task.status = TaskStatus.EVALUATING
|
|
149
|
+
if on_status_update:
|
|
150
|
+
await on_status_update(task)
|
|
151
|
+
|
|
152
|
+
eval_result = await self._run_evaluator(task)
|
|
153
|
+
task.eval_output = eval_result.output
|
|
154
|
+
task.turn_count += 1
|
|
155
|
+
|
|
156
|
+
# Parse evaluation result
|
|
157
|
+
eval_verdict = self._parse_eval_result(eval_result.output)
|
|
158
|
+
task.eval_result = eval_verdict
|
|
159
|
+
|
|
160
|
+
if eval_verdict == EvalResult.COMPLETE:
|
|
161
|
+
task.status = TaskStatus.COMPLETED
|
|
162
|
+
task.completed_at = datetime.now()
|
|
163
|
+
return self._create_result(task, True, start_time)
|
|
164
|
+
|
|
165
|
+
elif eval_verdict == EvalResult.FAILED:
|
|
166
|
+
task.status = TaskStatus.FAILED
|
|
167
|
+
task.error_message = "Evaluation returned FAILED"
|
|
168
|
+
return self._create_result(task, False, start_time)
|
|
169
|
+
|
|
170
|
+
# For PARTIAL or INCOMPLETE, continue to next iteration
|
|
171
|
+
logger.info(
|
|
172
|
+
f"Task {task.task_id} eval: {eval_verdict.value}, "
|
|
173
|
+
f"iteration {iteration + 1}/{self.max_iterations}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Max iterations reached
|
|
177
|
+
task.status = TaskStatus.COMPLETED
|
|
178
|
+
task.completed_at = datetime.now()
|
|
179
|
+
return self._create_result(task, True, start_time)
|
|
180
|
+
|
|
181
|
+
except asyncio.CancelledError:
|
|
182
|
+
task.status = TaskStatus.CANCELLED
|
|
183
|
+
raise
|
|
184
|
+
|
|
185
|
+
except Exception as e:
|
|
186
|
+
task.status = TaskStatus.FAILED
|
|
187
|
+
task.error_message = str(e)
|
|
188
|
+
logger.error(f"Workflow failed for task {task.task_id}: {e}")
|
|
189
|
+
return self._create_result(task, False, start_time)
|
|
190
|
+
|
|
191
|
+
finally:
|
|
192
|
+
self._active_tasks.pop(task.task_id, None)
|
|
193
|
+
# Cleanup agent sessions
|
|
194
|
+
await self._cleanup_sessions(task.task_id)
|
|
195
|
+
|
|
196
|
+
async def _run_planner(self, task: AgentTask) -> ExecutionResult:
|
|
197
|
+
"""Run the planner agent."""
|
|
198
|
+
prompt = format_task_prompt(
|
|
199
|
+
role=AgentRole.PLANNER,
|
|
200
|
+
task=task.description,
|
|
201
|
+
working_directory=task.working_directory,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Use task-specific session for planner
|
|
205
|
+
session_id = f"{task.task_id}-planner"
|
|
206
|
+
|
|
207
|
+
return await self.executor.execute(
|
|
208
|
+
prompt=prompt,
|
|
209
|
+
working_directory=task.working_directory,
|
|
210
|
+
session_id=session_id,
|
|
211
|
+
execution_id=session_id,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
async def _run_worker(self, task: AgentTask) -> ExecutionResult:
|
|
215
|
+
"""Run the worker agent."""
|
|
216
|
+
prompt = format_task_prompt(
|
|
217
|
+
role=AgentRole.WORKER,
|
|
218
|
+
task=task.description,
|
|
219
|
+
plan=task.plan_output,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Use task-specific session for worker
|
|
223
|
+
session_id = f"{task.task_id}-worker"
|
|
224
|
+
|
|
225
|
+
return await self.executor.execute(
|
|
226
|
+
prompt=prompt,
|
|
227
|
+
working_directory=task.working_directory,
|
|
228
|
+
session_id=session_id,
|
|
229
|
+
execution_id=session_id,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
async def _run_evaluator(self, task: AgentTask) -> ExecutionResult:
|
|
233
|
+
"""Run the evaluator agent."""
|
|
234
|
+
prompt = format_task_prompt(
|
|
235
|
+
role=AgentRole.EVALUATOR,
|
|
236
|
+
task=task.description,
|
|
237
|
+
plan=task.plan_output,
|
|
238
|
+
work_output=task.work_output,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
# Use task-specific session for evaluator
|
|
242
|
+
session_id = f"{task.task_id}-evaluator"
|
|
243
|
+
|
|
244
|
+
return await self.executor.execute(
|
|
245
|
+
prompt=prompt,
|
|
246
|
+
working_directory=task.working_directory,
|
|
247
|
+
session_id=session_id,
|
|
248
|
+
execution_id=session_id,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
def _parse_eval_result(self, output: str) -> EvalResult:
|
|
252
|
+
"""Parse evaluation verdict from output."""
|
|
253
|
+
output_upper = output.upper()
|
|
254
|
+
|
|
255
|
+
if "COMPLETE" in output_upper and "INCOMPLETE" not in output_upper:
|
|
256
|
+
return EvalResult.COMPLETE
|
|
257
|
+
elif "INCOMPLETE" in output_upper:
|
|
258
|
+
return EvalResult.INCOMPLETE
|
|
259
|
+
elif "PARTIAL" in output_upper:
|
|
260
|
+
return EvalResult.PARTIAL
|
|
261
|
+
elif "FAILED" in output_upper or "FAIL" in output_upper:
|
|
262
|
+
return EvalResult.FAILED
|
|
263
|
+
|
|
264
|
+
# Default to partial if can't determine
|
|
265
|
+
return EvalResult.PARTIAL
|
|
266
|
+
|
|
267
|
+
def _create_result(
|
|
268
|
+
self,
|
|
269
|
+
task: AgentTask,
|
|
270
|
+
success: bool,
|
|
271
|
+
start_time: float,
|
|
272
|
+
) -> WorkflowResult:
|
|
273
|
+
"""Create a workflow result."""
|
|
274
|
+
duration_ms = int((asyncio.get_running_loop().time() - start_time) * 1000)
|
|
275
|
+
|
|
276
|
+
return WorkflowResult(
|
|
277
|
+
task=task,
|
|
278
|
+
success=success,
|
|
279
|
+
plan=task.plan_output,
|
|
280
|
+
work_output=task.work_output,
|
|
281
|
+
evaluation=task.eval_output,
|
|
282
|
+
eval_result=task.eval_result,
|
|
283
|
+
total_turns=task.turn_count,
|
|
284
|
+
duration_ms=duration_ms,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
async def _cleanup_sessions(self, task_id: str) -> None:
|
|
288
|
+
"""Cancel any active subprocess executions for a task."""
|
|
289
|
+
for role in ["planner", "worker", "evaluator"]:
|
|
290
|
+
session_id = f"{task_id}-{role}"
|
|
291
|
+
await self.executor.cancel(session_id)
|
|
292
|
+
|
|
293
|
+
async def cancel_task(self, task_id: str) -> bool:
|
|
294
|
+
"""Cancel an active task.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
task_id: The task ID to cancel
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
True if task was found and cancelled
|
|
301
|
+
"""
|
|
302
|
+
task = self._active_tasks.get(task_id)
|
|
303
|
+
if not task:
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
task.status = TaskStatus.CANCELLED
|
|
307
|
+
await self._cleanup_sessions(task_id)
|
|
308
|
+
return True
|
|
309
|
+
|
|
310
|
+
def get_active_tasks(self) -> list[AgentTask]:
|
|
311
|
+
"""Get list of active tasks."""
|
|
312
|
+
return list(self._active_tasks.values())
|
|
313
|
+
|
|
314
|
+
def get_task(self, task_id: str) -> Optional[AgentTask]:
|
|
315
|
+
"""Get a specific task by ID."""
|
|
316
|
+
return self._active_tasks.get(task_id)
|