mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
merge main: resolve bootstrap and router init conflicts
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
# dmux Hooks System - Agent Reference
|
||||
|
||||
**Auto-generated documentation for AI agents**
|
||||
|
||||
This document contains everything an AI agent needs to create, modify, and understand dmux hooks. It is automatically generated from the dmux source code and embedded in the binary.
|
||||
|
||||
## What You're Working On
|
||||
|
||||
You are editing hooks for **dmux**, a tmux pane manager that creates AI-powered development workflows. Each pane runs in its own git worktree with an AI agent.
|
||||
|
||||
## Your Goal
|
||||
|
||||
Create executable bash scripts in `.dmux-hooks/` that run automatically at key lifecycle events.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Create a hook file**: `touch .dmux-hooks/worktree_created`
|
||||
2. **Make it executable**: `chmod +x .dmux-hooks/worktree_created`
|
||||
3. **Add shebang**: Start with `#!/bin/bash`
|
||||
4. **Use environment variables**: Access `$DMUX_ROOT`, `$DMUX_WORKTREE_PATH`, etc.
|
||||
5. **Test it**: Set env vars manually and run the script
|
||||
|
||||
## Hook Execution Model
|
||||
|
||||
- **Mostly non-blocking**: Most hooks run in background (detached processes)
|
||||
- **Bootstrap-gated**: `worktree_created` blocks agent launch so setup can finish first, with no fixed timeout
|
||||
- **Live bootstrap output**: During `worktree_created`, stdout/stderr is streamed into the new pane's setup UI
|
||||
- **Failure behavior**: Background hook errors are logged; gated hooks can abort the operation
|
||||
- **Environment-based**: All context passed via environment variables
|
||||
- **Version controlled**: Hooks in `.dmux-hooks/` are shared with team
|
||||
- **Priority resolution**: `.dmux-hooks/` → `.dmux/hooks/` → `~/.dmux/hooks/`
|
||||
|
||||
## Available Hooks
|
||||
|
||||
### Pane Lifecycle Hooks
|
||||
|
||||
| Hook | When | Common Use Cases |
|
||||
|------|------|------------------|
|
||||
| `before_pane_create` | Before pane creation | Validation, notifications, pre-flight checks |
|
||||
| `pane_created` | After pane, before worktree | Configure tmux settings, prepare environment |
|
||||
| `worktree_created` | After worktree creation, before agent launch | Install deps, copy configs, setup git |
|
||||
| `before_pane_close` | Before closing | Save state, backup uncommitted work |
|
||||
| `pane_closed` | After closed | Cleanup resources, analytics, notifications |
|
||||
|
||||
### Worktree Lifecycle Hooks
|
||||
|
||||
| Hook | When | Common Use Cases |
|
||||
|------|------|------------------|
|
||||
| `before_worktree_remove` | Before worktree removal | Archive worktree, save artifacts |
|
||||
| `worktree_removed` | After worktree removed | Cleanup external references |
|
||||
|
||||
### Merge Lifecycle Hooks
|
||||
|
||||
| Hook | When | Common Use Cases |
|
||||
|------|------|------------------|
|
||||
| `pre_merge` | Before merge operation | Run final tests, create backups |
|
||||
| `post_merge` | After successful merge | Deploy, close issues, notify team |
|
||||
|
||||
### Interactive Hooks (with HTTP callbacks)
|
||||
|
||||
| Hook | When | Common Use Cases |
|
||||
|------|------|------------------|
|
||||
| `run_test` | When tests triggered | Run test suite, report status via HTTP |
|
||||
| `run_dev` | When dev server triggered | Start dev server, create tunnel, report URL |
|
||||
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Always Available
|
||||
```bash
|
||||
DMUX_ROOT="/path/to/project" # Project root directory
|
||||
DMUX_SERVER_PORT="3142" # HTTP server port
|
||||
```
|
||||
|
||||
### Pane Context (most hooks)
|
||||
```bash
|
||||
DMUX_PANE_ID="dmux-1234567890" # dmux pane identifier
|
||||
DMUX_SLUG="fix-auth-bug" # Branch/worktree name
|
||||
DMUX_PROMPT="Fix authentication bug" # User's prompt
|
||||
DMUX_AGENT="claude" # Agent type (registry id, e.g. claude, codex, opencode)
|
||||
DMUX_TMUX_PANE_ID="%38" # tmux pane ID
|
||||
```
|
||||
|
||||
### Worktree Context
|
||||
```bash
|
||||
DMUX_WORKTREE_PATH="/path/.dmux/worktrees/fix-auth-bug"
|
||||
DMUX_BRANCH="fix-auth-bug" # Same as slug
|
||||
```
|
||||
|
||||
### Bootstrap Progress Context
|
||||
```bash
|
||||
DMUX_PROGRESS="1" # Set when output is shown in the new pane setup UI
|
||||
DMUX_STATUS_PREFIX="DMUX_STATUS:" # Prefix for clean status messages
|
||||
```
|
||||
|
||||
`worktree_created` can emit progress while it runs. Any stdout/stderr line is shown in the setup UI; lines prefixed with `$DMUX_STATUS_PREFIX` are displayed without the prefix.
|
||||
|
||||
```bash
|
||||
status() {
|
||||
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
|
||||
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
|
||||
else
|
||||
echo "[Hook] $*"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### Merge Context
|
||||
```bash
|
||||
DMUX_TARGET_BRANCH="main" # Branch being merged into
|
||||
```
|
||||
|
||||
## HTTP Callback API
|
||||
|
||||
Interactive hooks (`run_test` and `run_dev`) can update dmux UI via HTTP.
|
||||
|
||||
### Update Test Status
|
||||
```bash
|
||||
curl -X PUT "http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test" -H "Content-Type: application/json" -d '{"status": "running", "output": "optional test output"}'
|
||||
|
||||
# Status values: "running" | "passed" | "failed"
|
||||
```
|
||||
|
||||
### Update Dev Server
|
||||
```bash
|
||||
curl -X PUT "http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev" -H "Content-Type: application/json" -d '{"status": "running", "url": "http://localhost:3000"}'
|
||||
|
||||
# Status values: "running" | "stopped"
|
||||
# url: Can be localhost or tunnel URL (ngrok, cloudflared, etc.)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Install Dependencies
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/worktree_created
|
||||
|
||||
cd "$DMUX_WORKTREE_PATH"
|
||||
|
||||
status() {
|
||||
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
|
||||
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
|
||||
else
|
||||
echo "[Hook] $*"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
status "Installing dependencies with pnpm"
|
||||
pnpm install --prefer-offline
|
||||
elif [ -f "package-lock.json" ]; then
|
||||
status "Installing dependencies with npm"
|
||||
npm install
|
||||
elif [ -f "yarn.lock" ]; then
|
||||
status "Installing dependencies with yarn"
|
||||
yarn install
|
||||
elif [ -f "Gemfile" ]; then
|
||||
status "Installing gems"
|
||||
bundle install
|
||||
elif [ -f "requirements.txt" ]; then
|
||||
status "Installing Python dependencies"
|
||||
pip install -r requirements.txt
|
||||
elif [ -f "Cargo.toml" ]; then
|
||||
status "Building Rust project"
|
||||
cargo build
|
||||
fi
|
||||
```
|
||||
|
||||
### Pattern 2: Copy Configuration
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/worktree_created
|
||||
|
||||
# Copy environment file
|
||||
if [ -f "$DMUX_ROOT/.env.local" ]; then
|
||||
cp "$DMUX_ROOT/.env.local" "$DMUX_WORKTREE_PATH/.env.local"
|
||||
fi
|
||||
|
||||
# Copy other config files
|
||||
for file in .env.development .npmrc .yarnrc; do
|
||||
if [ -f "$DMUX_ROOT/$file" ]; then
|
||||
cp "$DMUX_ROOT/$file" "$DMUX_WORKTREE_PATH/$file"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Pattern 3: Run Tests with Status Updates
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/run_test
|
||||
|
||||
set -e
|
||||
cd "$DMUX_WORKTREE_PATH"
|
||||
API="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test"
|
||||
|
||||
# Update: starting
|
||||
curl -s -X PUT "$API" -H "Content-Type: application/json" -d '{"status": "running"}' > /dev/null
|
||||
|
||||
# Run tests and capture output
|
||||
OUTPUT_FILE="/tmp/dmux-test-$DMUX_PANE_ID.txt"
|
||||
if pnpm test > "$OUTPUT_FILE" 2>&1; then
|
||||
STATUS="passed"
|
||||
else
|
||||
STATUS="failed"
|
||||
fi
|
||||
|
||||
# Get output (truncate if too long)
|
||||
OUTPUT=$(head -c 5000 "$OUTPUT_FILE")
|
||||
|
||||
# Update: complete
|
||||
curl -s -X PUT "$API" -H "Content-Type: application/json" -d "$(jq -n --arg status "$STATUS" --arg output "$OUTPUT" '{status: $status, output: $output}')" > /dev/null
|
||||
|
||||
rm -f "$OUTPUT_FILE"
|
||||
```
|
||||
|
||||
### Pattern 4: Dev Server with Tunnel
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/run_dev
|
||||
|
||||
set -e
|
||||
cd "$DMUX_WORKTREE_PATH"
|
||||
API="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev"
|
||||
|
||||
# Start dev server in background
|
||||
LOG_FILE="/tmp/dmux-dev-$DMUX_PANE_ID.log"
|
||||
pnpm dev > "$LOG_FILE" 2>&1 &
|
||||
DEV_PID=$!
|
||||
|
||||
# Wait for server to start
|
||||
sleep 5
|
||||
|
||||
# Detect port from logs
|
||||
PORT=$(grep -oP 'localhost:Kd+' "$LOG_FILE" | head -1)
|
||||
[ -z "$PORT" ] && PORT=3000
|
||||
|
||||
# Optional: Create tunnel with cloudflared
|
||||
if command -v cloudflared &> /dev/null; then
|
||||
TUNNEL=$(cloudflared tunnel --url "http://localhost:$PORT" 2>&1 | grep -oP 'https://[a-z0-9-]+.trycloudflare.com' | head -1)
|
||||
URL="${TUNNEL:-http://localhost:$PORT}"
|
||||
else
|
||||
URL="http://localhost:$PORT"
|
||||
fi
|
||||
|
||||
# Report status
|
||||
curl -s -X PUT "$API" -H "Content-Type: application/json" -d "{"status": "running", "url": "$URL"}" > /dev/null
|
||||
|
||||
echo "[Hook] Dev server running at $URL (PID: $DEV_PID)"
|
||||
```
|
||||
|
||||
### Pattern 5: Post-Merge Deployment
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/post_merge
|
||||
|
||||
set -e
|
||||
cd "$DMUX_ROOT"
|
||||
|
||||
# Only deploy from main/master
|
||||
if [ "$DMUX_TARGET_BRANCH" != "main" ] && [ "$DMUX_TARGET_BRANCH" != "master" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Push to remote
|
||||
git push origin "$DMUX_TARGET_BRANCH"
|
||||
|
||||
# Trigger deployment (example: Vercel)
|
||||
if [ -n "$VERCEL_TOKEN" ]; then
|
||||
curl -s -X POST "https://api.vercel.com/v1/deployments" -H "Authorization: Bearer $VERCEL_TOKEN" -H "Content-Type: application/json" -d '{"name": "my-project"}' > /dev/null
|
||||
fi
|
||||
|
||||
# Close GitHub issue if prompt contains #123
|
||||
ISSUE=$(echo "$DMUX_PROMPT" | grep -oP '#Kd+' | head -1)
|
||||
if [ -n "$ISSUE" ] && command -v gh &> /dev/null; then
|
||||
gh issue close "$ISSUE" -c "Resolved in $DMUX_SLUG, merged to $DMUX_TARGET_BRANCH" 2>/dev/null || true
|
||||
fi
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always start with shebang**: `#!/bin/bash`
|
||||
2. **Set error handling**: `set -e` (exit on error)
|
||||
3. **Make executable**: `chmod +x .dmux-hooks/hook_name`
|
||||
4. **Background long operations**: Append `&` to avoid blocking
|
||||
5. **Check for required tools**: `command -v tool &> /dev/null`
|
||||
6. **Log for debugging**: `echo "[Hook] message" >> "$DMUX_ROOT/.dmux/hooks.log"`
|
||||
7. **Handle missing vars gracefully**: `[ -z "$VAR" ] && exit 0`
|
||||
8. **Use silent curl**: `curl -s` to avoid noise in logs
|
||||
9. **Clean up temp files**: Remove files in `/tmp/`
|
||||
10. **Test before committing**: Run hooks manually with mock env vars
|
||||
|
||||
## Testing Hooks
|
||||
|
||||
### Manual Testing
|
||||
```bash
|
||||
# 1. Set environment variables
|
||||
export DMUX_ROOT="$(pwd)"
|
||||
export DMUX_PANE_ID="test-pane"
|
||||
export DMUX_SLUG="test-branch"
|
||||
export DMUX_WORKTREE_PATH="$(pwd)"
|
||||
export DMUX_SERVER_PORT="3142"
|
||||
export DMUX_AGENT="claude"
|
||||
export DMUX_PROMPT="Test prompt"
|
||||
|
||||
# 2. Run hook directly
|
||||
./.dmux-hooks/worktree_created
|
||||
|
||||
# 3. Check exit code
|
||||
echo $? # Should be 0 for success
|
||||
```
|
||||
|
||||
### Syntax Check
|
||||
```bash
|
||||
# Check for syntax errors without running
|
||||
bash -n ./.dmux-hooks/worktree_created
|
||||
```
|
||||
|
||||
### Shellcheck (if available)
|
||||
```bash
|
||||
shellcheck ./.dmux-hooks/worktree_created
|
||||
```
|
||||
|
||||
## Project Context Analysis
|
||||
|
||||
Before creating hooks, analyze these files in the project:
|
||||
|
||||
### Package Manager Detection
|
||||
```bash
|
||||
# Check which package manager is used
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
# Use: pnpm install, pnpm test, pnpm dev
|
||||
elif [ -f "package-lock.json" ]; then
|
||||
# Use: npm install, npm test, npm run dev
|
||||
elif [ -f "yarn.lock" ]; then
|
||||
# Use: yarn install, yarn test, yarn dev
|
||||
fi
|
||||
```
|
||||
|
||||
### Test Command Discovery
|
||||
```bash
|
||||
# Read package.json to find test command
|
||||
cat package.json | grep '"test"'
|
||||
# Or with jq:
|
||||
jq -r '.scripts.test' package.json
|
||||
```
|
||||
|
||||
### Dev Command Discovery
|
||||
```bash
|
||||
# Read package.json to find dev command
|
||||
cat package.json | grep '"dev"'
|
||||
# Or with jq:
|
||||
jq -r '.scripts.dev' package.json
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# Check for .env files to copy
|
||||
ls -la | grep '.env'
|
||||
```
|
||||
|
||||
### Build System
|
||||
```bash
|
||||
# Detect build system
|
||||
if [ -f "vite.config.ts" ]; then
|
||||
# Vite project
|
||||
elif [ -f "next.config.js" ]; then
|
||||
# Next.js project
|
||||
elif [ -f "nuxt.config.ts" ]; then
|
||||
# Nuxt project
|
||||
fi
|
||||
```
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
❌ **Blocking operations**: `sleep 60` (blocks dmux)
|
||||
✅ **Background long tasks**: `slow_operation &`
|
||||
|
||||
❌ **Hardcoded paths**: `/Users/me/project`
|
||||
✅ **Use variables**: `"$DMUX_ROOT"`
|
||||
|
||||
❌ **Assuming tools exist**: `pnpm install`
|
||||
✅ **Check first**: `command -v pnpm && pnpm install`
|
||||
|
||||
❌ **No error handling**: Script fails silently
|
||||
✅ **Set error mode**: `set -e` or check exit codes
|
||||
|
||||
❌ **Forgetting executable bit**: Hook won't run
|
||||
✅ **Make executable**: `chmod +x`
|
||||
|
||||
❌ **Noisy output**: Clutters dmux logs
|
||||
✅ **Silent operations**: `curl -s`, `> /dev/null 2>&1`
|
||||
|
||||
❌ **Not testing**: Deploy and hope
|
||||
✅ **Test manually**: Run with mock env vars first
|
||||
|
||||
## Debugging
|
||||
|
||||
If a hook isn't working:
|
||||
|
||||
1. **Check if file exists**: `ls -la .dmux-hooks/`
|
||||
2. **Check permissions**: Should show `x` in `rwxr-xr-x`
|
||||
3. **Check syntax**: `bash -n .dmux-hooks/hook_name`
|
||||
4. **Test manually**: Set env vars and run
|
||||
5. **Check logs**: dmux logs to stderr with `[Hooks]` prefix
|
||||
6. **Simplify**: Remove complex parts, test basic version
|
||||
7. **Check tool availability**: `command -v required_tool`
|
||||
|
||||
### Debug Mode
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Add to top of hook for debugging
|
||||
set -x # Print each command before executing
|
||||
set -e # Exit on error
|
||||
|
||||
# Your hook logic here
|
||||
```
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
When creating a new hook:
|
||||
|
||||
- [ ] Create file in `.dmux-hooks/`
|
||||
- [ ] Add shebang: `#!/bin/bash`
|
||||
- [ ] Make executable: `chmod +x`
|
||||
- [ ] Add `set -e` for error handling
|
||||
- [ ] Use environment variables (never hardcode paths)
|
||||
- [ ] Keep blocking hooks chatty with status output
|
||||
- [ ] Background long operations with `&` only when the hook does not gate the operation
|
||||
- [ ] Check for required tools before using
|
||||
- [ ] Test manually with mock env vars
|
||||
- [ ] Add comments explaining what it does
|
||||
- [ ] Commit to version control
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Full documentation**: See `HOOKS.md` in project root
|
||||
- **Claude-specific tips**: See `CLAUDE.md` in `.dmux-hooks/`
|
||||
- **Examples**: Check `.dmux-hooks/examples/` directory
|
||||
- **dmux API**: See `API.md` for REST endpoints
|
||||
|
||||
---
|
||||
|
||||
*This documentation was auto-generated from dmux source code.*
|
||||
*Version: 2026-05-25*
|
||||
@@ -0,0 +1,445 @@
|
||||
# dmux Hooks System - Agent Reference
|
||||
|
||||
**Auto-generated documentation for AI agents**
|
||||
|
||||
This document contains everything an AI agent needs to create, modify, and understand dmux hooks. It is automatically generated from the dmux source code and embedded in the binary.
|
||||
|
||||
## What You're Working On
|
||||
|
||||
You are editing hooks for **dmux**, a tmux pane manager that creates AI-powered development workflows. Each pane runs in its own git worktree with an AI agent.
|
||||
|
||||
## Your Goal
|
||||
|
||||
Create executable bash scripts in `.dmux-hooks/` that run automatically at key lifecycle events.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Create a hook file**: `touch .dmux-hooks/worktree_created`
|
||||
2. **Make it executable**: `chmod +x .dmux-hooks/worktree_created`
|
||||
3. **Add shebang**: Start with `#!/bin/bash`
|
||||
4. **Use environment variables**: Access `$DMUX_ROOT`, `$DMUX_WORKTREE_PATH`, etc.
|
||||
5. **Test it**: Set env vars manually and run the script
|
||||
|
||||
## Hook Execution Model
|
||||
|
||||
- **Mostly non-blocking**: Most hooks run in background (detached processes)
|
||||
- **Bootstrap-gated**: `worktree_created` blocks agent launch so setup can finish first, with no fixed timeout
|
||||
- **Live bootstrap output**: During `worktree_created`, stdout/stderr is streamed into the new pane's setup UI
|
||||
- **Failure behavior**: Background hook errors are logged; gated hooks can abort the operation
|
||||
- **Environment-based**: All context passed via environment variables
|
||||
- **Version controlled**: Hooks in `.dmux-hooks/` are shared with team
|
||||
- **Priority resolution**: `.dmux-hooks/` → `.dmux/hooks/` → `~/.dmux/hooks/`
|
||||
|
||||
## Available Hooks
|
||||
|
||||
### Pane Lifecycle Hooks
|
||||
|
||||
| Hook | When | Common Use Cases |
|
||||
|------|------|------------------|
|
||||
| `before_pane_create` | Before pane creation | Validation, notifications, pre-flight checks |
|
||||
| `pane_created` | After pane, before worktree | Configure tmux settings, prepare environment |
|
||||
| `worktree_created` | After worktree creation, before agent launch | Install deps, copy configs, setup git |
|
||||
| `before_pane_close` | Before closing | Save state, backup uncommitted work |
|
||||
| `pane_closed` | After closed | Cleanup resources, analytics, notifications |
|
||||
|
||||
### Worktree Lifecycle Hooks
|
||||
|
||||
| Hook | When | Common Use Cases |
|
||||
|------|------|------------------|
|
||||
| `before_worktree_remove` | Before worktree removal | Archive worktree, save artifacts |
|
||||
| `worktree_removed` | After worktree removed | Cleanup external references |
|
||||
|
||||
### Merge Lifecycle Hooks
|
||||
|
||||
| Hook | When | Common Use Cases |
|
||||
|------|------|------------------|
|
||||
| `pre_merge` | Before merge operation | Run final tests, create backups |
|
||||
| `post_merge` | After successful merge | Deploy, close issues, notify team |
|
||||
|
||||
### Interactive Hooks (with HTTP callbacks)
|
||||
|
||||
| Hook | When | Common Use Cases |
|
||||
|------|------|------------------|
|
||||
| `run_test` | When tests triggered | Run test suite, report status via HTTP |
|
||||
| `run_dev` | When dev server triggered | Start dev server, create tunnel, report URL |
|
||||
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### Always Available
|
||||
```bash
|
||||
DMUX_ROOT="/path/to/project" # Project root directory
|
||||
DMUX_SERVER_PORT="3142" # HTTP server port
|
||||
```
|
||||
|
||||
### Pane Context (most hooks)
|
||||
```bash
|
||||
DMUX_PANE_ID="dmux-1234567890" # dmux pane identifier
|
||||
DMUX_SLUG="fix-auth-bug" # Branch/worktree name
|
||||
DMUX_PROMPT="Fix authentication bug" # User's prompt
|
||||
DMUX_AGENT="claude" # Agent type (registry id, e.g. claude, codex, opencode)
|
||||
DMUX_TMUX_PANE_ID="%38" # tmux pane ID
|
||||
```
|
||||
|
||||
### Worktree Context
|
||||
```bash
|
||||
DMUX_WORKTREE_PATH="/path/.dmux/worktrees/fix-auth-bug"
|
||||
DMUX_BRANCH="fix-auth-bug" # Same as slug
|
||||
```
|
||||
|
||||
### Bootstrap Progress Context
|
||||
```bash
|
||||
DMUX_PROGRESS="1" # Set when output is shown in the new pane setup UI
|
||||
DMUX_STATUS_PREFIX="DMUX_STATUS:" # Prefix for clean status messages
|
||||
```
|
||||
|
||||
`worktree_created` can emit progress while it runs. Any stdout/stderr line is shown in the setup UI; lines prefixed with `$DMUX_STATUS_PREFIX` are displayed without the prefix.
|
||||
|
||||
```bash
|
||||
status() {
|
||||
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
|
||||
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
|
||||
else
|
||||
echo "[Hook] $*"
|
||||
fi
|
||||
}
|
||||
```
|
||||
|
||||
### Merge Context
|
||||
```bash
|
||||
DMUX_TARGET_BRANCH="main" # Branch being merged into
|
||||
```
|
||||
|
||||
## HTTP Callback API
|
||||
|
||||
Interactive hooks (`run_test` and `run_dev`) can update dmux UI via HTTP.
|
||||
|
||||
### Update Test Status
|
||||
```bash
|
||||
curl -X PUT "http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test" -H "Content-Type: application/json" -d '{"status": "running", "output": "optional test output"}'
|
||||
|
||||
# Status values: "running" | "passed" | "failed"
|
||||
```
|
||||
|
||||
### Update Dev Server
|
||||
```bash
|
||||
curl -X PUT "http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev" -H "Content-Type: application/json" -d '{"status": "running", "url": "http://localhost:3000"}'
|
||||
|
||||
# Status values: "running" | "stopped"
|
||||
# url: Can be localhost or tunnel URL (ngrok, cloudflared, etc.)
|
||||
```
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern 1: Install Dependencies
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/worktree_created
|
||||
|
||||
cd "$DMUX_WORKTREE_PATH"
|
||||
|
||||
status() {
|
||||
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
|
||||
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
|
||||
else
|
||||
echo "[Hook] $*"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
status "Installing dependencies with pnpm"
|
||||
pnpm install --prefer-offline
|
||||
elif [ -f "package-lock.json" ]; then
|
||||
status "Installing dependencies with npm"
|
||||
npm install
|
||||
elif [ -f "yarn.lock" ]; then
|
||||
status "Installing dependencies with yarn"
|
||||
yarn install
|
||||
elif [ -f "Gemfile" ]; then
|
||||
status "Installing gems"
|
||||
bundle install
|
||||
elif [ -f "requirements.txt" ]; then
|
||||
status "Installing Python dependencies"
|
||||
pip install -r requirements.txt
|
||||
elif [ -f "Cargo.toml" ]; then
|
||||
status "Building Rust project"
|
||||
cargo build
|
||||
fi
|
||||
```
|
||||
|
||||
### Pattern 2: Copy Configuration
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/worktree_created
|
||||
|
||||
# Copy environment file
|
||||
if [ -f "$DMUX_ROOT/.env.local" ]; then
|
||||
cp "$DMUX_ROOT/.env.local" "$DMUX_WORKTREE_PATH/.env.local"
|
||||
fi
|
||||
|
||||
# Copy other config files
|
||||
for file in .env.development .npmrc .yarnrc; do
|
||||
if [ -f "$DMUX_ROOT/$file" ]; then
|
||||
cp "$DMUX_ROOT/$file" "$DMUX_WORKTREE_PATH/$file"
|
||||
fi
|
||||
done
|
||||
```
|
||||
|
||||
### Pattern 3: Run Tests with Status Updates
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/run_test
|
||||
|
||||
set -e
|
||||
cd "$DMUX_WORKTREE_PATH"
|
||||
API="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test"
|
||||
|
||||
# Update: starting
|
||||
curl -s -X PUT "$API" -H "Content-Type: application/json" -d '{"status": "running"}' > /dev/null
|
||||
|
||||
# Run tests and capture output
|
||||
OUTPUT_FILE="/tmp/dmux-test-$DMUX_PANE_ID.txt"
|
||||
if pnpm test > "$OUTPUT_FILE" 2>&1; then
|
||||
STATUS="passed"
|
||||
else
|
||||
STATUS="failed"
|
||||
fi
|
||||
|
||||
# Get output (truncate if too long)
|
||||
OUTPUT=$(head -c 5000 "$OUTPUT_FILE")
|
||||
|
||||
# Update: complete
|
||||
curl -s -X PUT "$API" -H "Content-Type: application/json" -d "$(jq -n --arg status "$STATUS" --arg output "$OUTPUT" '{status: $status, output: $output}')" > /dev/null
|
||||
|
||||
rm -f "$OUTPUT_FILE"
|
||||
```
|
||||
|
||||
### Pattern 4: Dev Server with Tunnel
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/run_dev
|
||||
|
||||
set -e
|
||||
cd "$DMUX_WORKTREE_PATH"
|
||||
API="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev"
|
||||
|
||||
# Start dev server in background
|
||||
LOG_FILE="/tmp/dmux-dev-$DMUX_PANE_ID.log"
|
||||
pnpm dev > "$LOG_FILE" 2>&1 &
|
||||
DEV_PID=$!
|
||||
|
||||
# Wait for server to start
|
||||
sleep 5
|
||||
|
||||
# Detect port from logs
|
||||
PORT=$(grep -oP 'localhost:Kd+' "$LOG_FILE" | head -1)
|
||||
[ -z "$PORT" ] && PORT=3000
|
||||
|
||||
# Optional: Create tunnel with cloudflared
|
||||
if command -v cloudflared &> /dev/null; then
|
||||
TUNNEL=$(cloudflared tunnel --url "http://localhost:$PORT" 2>&1 | grep -oP 'https://[a-z0-9-]+.trycloudflare.com' | head -1)
|
||||
URL="${TUNNEL:-http://localhost:$PORT}"
|
||||
else
|
||||
URL="http://localhost:$PORT"
|
||||
fi
|
||||
|
||||
# Report status
|
||||
curl -s -X PUT "$API" -H "Content-Type: application/json" -d "{"status": "running", "url": "$URL"}" > /dev/null
|
||||
|
||||
echo "[Hook] Dev server running at $URL (PID: $DEV_PID)"
|
||||
```
|
||||
|
||||
### Pattern 5: Post-Merge Deployment
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .dmux-hooks/post_merge
|
||||
|
||||
set -e
|
||||
cd "$DMUX_ROOT"
|
||||
|
||||
# Only deploy from main/master
|
||||
if [ "$DMUX_TARGET_BRANCH" != "main" ] && [ "$DMUX_TARGET_BRANCH" != "master" ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Push to remote
|
||||
git push origin "$DMUX_TARGET_BRANCH"
|
||||
|
||||
# Trigger deployment (example: Vercel)
|
||||
if [ -n "$VERCEL_TOKEN" ]; then
|
||||
curl -s -X POST "https://api.vercel.com/v1/deployments" -H "Authorization: Bearer $VERCEL_TOKEN" -H "Content-Type: application/json" -d '{"name": "my-project"}' > /dev/null
|
||||
fi
|
||||
|
||||
# Close GitHub issue if prompt contains #123
|
||||
ISSUE=$(echo "$DMUX_PROMPT" | grep -oP '#Kd+' | head -1)
|
||||
if [ -n "$ISSUE" ] && command -v gh &> /dev/null; then
|
||||
gh issue close "$ISSUE" -c "Resolved in $DMUX_SLUG, merged to $DMUX_TARGET_BRANCH" 2>/dev/null || true
|
||||
fi
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always start with shebang**: `#!/bin/bash`
|
||||
2. **Set error handling**: `set -e` (exit on error)
|
||||
3. **Make executable**: `chmod +x .dmux-hooks/hook_name`
|
||||
4. **Background long operations**: Append `&` to avoid blocking
|
||||
5. **Check for required tools**: `command -v tool &> /dev/null`
|
||||
6. **Log for debugging**: `echo "[Hook] message" >> "$DMUX_ROOT/.dmux/hooks.log"`
|
||||
7. **Handle missing vars gracefully**: `[ -z "$VAR" ] && exit 0`
|
||||
8. **Use silent curl**: `curl -s` to avoid noise in logs
|
||||
9. **Clean up temp files**: Remove files in `/tmp/`
|
||||
10. **Test before committing**: Run hooks manually with mock env vars
|
||||
|
||||
## Testing Hooks
|
||||
|
||||
### Manual Testing
|
||||
```bash
|
||||
# 1. Set environment variables
|
||||
export DMUX_ROOT="$(pwd)"
|
||||
export DMUX_PANE_ID="test-pane"
|
||||
export DMUX_SLUG="test-branch"
|
||||
export DMUX_WORKTREE_PATH="$(pwd)"
|
||||
export DMUX_SERVER_PORT="3142"
|
||||
export DMUX_AGENT="claude"
|
||||
export DMUX_PROMPT="Test prompt"
|
||||
|
||||
# 2. Run hook directly
|
||||
./.dmux-hooks/worktree_created
|
||||
|
||||
# 3. Check exit code
|
||||
echo $? # Should be 0 for success
|
||||
```
|
||||
|
||||
### Syntax Check
|
||||
```bash
|
||||
# Check for syntax errors without running
|
||||
bash -n ./.dmux-hooks/worktree_created
|
||||
```
|
||||
|
||||
### Shellcheck (if available)
|
||||
```bash
|
||||
shellcheck ./.dmux-hooks/worktree_created
|
||||
```
|
||||
|
||||
## Project Context Analysis
|
||||
|
||||
Before creating hooks, analyze these files in the project:
|
||||
|
||||
### Package Manager Detection
|
||||
```bash
|
||||
# Check which package manager is used
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
# Use: pnpm install, pnpm test, pnpm dev
|
||||
elif [ -f "package-lock.json" ]; then
|
||||
# Use: npm install, npm test, npm run dev
|
||||
elif [ -f "yarn.lock" ]; then
|
||||
# Use: yarn install, yarn test, yarn dev
|
||||
fi
|
||||
```
|
||||
|
||||
### Test Command Discovery
|
||||
```bash
|
||||
# Read package.json to find test command
|
||||
cat package.json | grep '"test"'
|
||||
# Or with jq:
|
||||
jq -r '.scripts.test' package.json
|
||||
```
|
||||
|
||||
### Dev Command Discovery
|
||||
```bash
|
||||
# Read package.json to find dev command
|
||||
cat package.json | grep '"dev"'
|
||||
# Or with jq:
|
||||
jq -r '.scripts.dev' package.json
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
```bash
|
||||
# Check for .env files to copy
|
||||
ls -la | grep '.env'
|
||||
```
|
||||
|
||||
### Build System
|
||||
```bash
|
||||
# Detect build system
|
||||
if [ -f "vite.config.ts" ]; then
|
||||
# Vite project
|
||||
elif [ -f "next.config.js" ]; then
|
||||
# Next.js project
|
||||
elif [ -f "nuxt.config.ts" ]; then
|
||||
# Nuxt project
|
||||
fi
|
||||
```
|
||||
|
||||
## Common Mistakes to Avoid
|
||||
|
||||
❌ **Blocking operations**: `sleep 60` (blocks dmux)
|
||||
✅ **Background long tasks**: `slow_operation &`
|
||||
|
||||
❌ **Hardcoded paths**: `/Users/me/project`
|
||||
✅ **Use variables**: `"$DMUX_ROOT"`
|
||||
|
||||
❌ **Assuming tools exist**: `pnpm install`
|
||||
✅ **Check first**: `command -v pnpm && pnpm install`
|
||||
|
||||
❌ **No error handling**: Script fails silently
|
||||
✅ **Set error mode**: `set -e` or check exit codes
|
||||
|
||||
❌ **Forgetting executable bit**: Hook won't run
|
||||
✅ **Make executable**: `chmod +x`
|
||||
|
||||
❌ **Noisy output**: Clutters dmux logs
|
||||
✅ **Silent operations**: `curl -s`, `> /dev/null 2>&1`
|
||||
|
||||
❌ **Not testing**: Deploy and hope
|
||||
✅ **Test manually**: Run with mock env vars first
|
||||
|
||||
## Debugging
|
||||
|
||||
If a hook isn't working:
|
||||
|
||||
1. **Check if file exists**: `ls -la .dmux-hooks/`
|
||||
2. **Check permissions**: Should show `x` in `rwxr-xr-x`
|
||||
3. **Check syntax**: `bash -n .dmux-hooks/hook_name`
|
||||
4. **Test manually**: Set env vars and run
|
||||
5. **Check logs**: dmux logs to stderr with `[Hooks]` prefix
|
||||
6. **Simplify**: Remove complex parts, test basic version
|
||||
7. **Check tool availability**: `command -v required_tool`
|
||||
|
||||
### Debug Mode
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Add to top of hook for debugging
|
||||
set -x # Print each command before executing
|
||||
set -e # Exit on error
|
||||
|
||||
# Your hook logic here
|
||||
```
|
||||
|
||||
## Summary Checklist
|
||||
|
||||
When creating a new hook:
|
||||
|
||||
- [ ] Create file in `.dmux-hooks/`
|
||||
- [ ] Add shebang: `#!/bin/bash`
|
||||
- [ ] Make executable: `chmod +x`
|
||||
- [ ] Add `set -e` for error handling
|
||||
- [ ] Use environment variables (never hardcode paths)
|
||||
- [ ] Keep blocking hooks chatty with status output
|
||||
- [ ] Background long operations with `&` only when the hook does not gate the operation
|
||||
- [ ] Check for required tools before using
|
||||
- [ ] Test manually with mock env vars
|
||||
- [ ] Add comments explaining what it does
|
||||
- [ ] Commit to version control
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Full documentation**: See `HOOKS.md` in project root
|
||||
- **Claude-specific tips**: See `CLAUDE.md` in `.dmux-hooks/`
|
||||
- **Examples**: Check `.dmux-hooks/examples/` directory
|
||||
- **dmux API**: See `API.md` for REST endpoints
|
||||
|
||||
---
|
||||
|
||||
*This documentation was auto-generated from dmux source code.*
|
||||
*Version: 2026-05-25*
|
||||
@@ -0,0 +1,53 @@
|
||||
# dmux Hooks
|
||||
|
||||
This directory contains hooks that run automatically at key lifecycle events in dmux.
|
||||
|
||||
## Quick Start
|
||||
|
||||
1. **Read the documentation**:
|
||||
- `AGENTS.md` - Complete reference (for any AI agent)
|
||||
- `CLAUDE.md` - Same content (Claude Code looks for this filename)
|
||||
|
||||
2. **Check examples**:
|
||||
- `examples/` directory contains starter templates
|
||||
|
||||
3. **Create a hook**:
|
||||
```bash
|
||||
touch worktree_created
|
||||
chmod +x worktree_created
|
||||
nano worktree_created
|
||||
```
|
||||
|
||||
4. **Test it**:
|
||||
```bash
|
||||
export DMUX_ROOT="$(pwd)"
|
||||
export DMUX_WORKTREE_PATH="$(pwd)"
|
||||
./worktree_created
|
||||
```
|
||||
|
||||
## Available Hooks
|
||||
|
||||
- `before_pane_create` - Before pane creation
|
||||
- `pane_created` - After pane created
|
||||
- `worktree_created` - After worktree setup
|
||||
- `before_pane_close` - Before closing
|
||||
- `pane_closed` - After closed
|
||||
- `before_worktree_remove` - Before worktree removal
|
||||
- `worktree_removed` - After worktree removed
|
||||
- `pre_merge` - Before merge
|
||||
- `post_merge` - After merge
|
||||
- `run_test` - When running tests
|
||||
- `run_dev` - When starting dev server
|
||||
|
||||
## Documentation
|
||||
|
||||
See `AGENTS.md` or `CLAUDE.md` for complete documentation including:
|
||||
- Environment variables
|
||||
- HTTP callback API
|
||||
- Common patterns
|
||||
- Best practices
|
||||
- Testing strategies
|
||||
|
||||
## Note
|
||||
|
||||
This directory is **version controlled**. Hooks you create here will be shared with your team.
|
||||
Executable
+66
@@ -0,0 +1,66 @@
|
||||
#!/bin/bash
|
||||
# Example: post_merge hook
|
||||
#
|
||||
# This hook runs after a successful merge into the target branch.
|
||||
# Use it to trigger deployments, close issues, notify teams, etc.
|
||||
|
||||
set -e
|
||||
|
||||
echo "[Hook] Post-merge processing for $DMUX_SLUG → $DMUX_TARGET_BRANCH"
|
||||
|
||||
cd "$DMUX_ROOT"
|
||||
|
||||
# Push to remote if merging to main/master
|
||||
if [ "$DMUX_TARGET_BRANCH" = "main" ] || [ "$DMUX_TARGET_BRANCH" = "master" ]; then
|
||||
echo "[Hook] Pushing to origin/$DMUX_TARGET_BRANCH"
|
||||
git push origin "$DMUX_TARGET_BRANCH"
|
||||
|
||||
# Optional: Trigger deployment
|
||||
# if [ -n "$VERCEL_TOKEN" ]; then
|
||||
# echo "[Hook] Triggering Vercel deployment..."
|
||||
# curl -X POST "https://api.vercel.com/v1/deployments" \
|
||||
# -H "Authorization: Bearer $VERCEL_TOKEN" \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -d '{
|
||||
# "name": "my-project",
|
||||
# "gitSource": {
|
||||
# "type": "github",
|
||||
# "ref": "main"
|
||||
# }
|
||||
# }'
|
||||
# fi
|
||||
fi
|
||||
|
||||
# Close related GitHub issue (if prompt contains #123 format)
|
||||
ISSUE_NUM=$(echo "$DMUX_PROMPT" | grep -oP '#\K\d+' | head -1)
|
||||
if [ -n "$ISSUE_NUM" ]; then
|
||||
echo "[Hook] Closing GitHub issue #$ISSUE_NUM"
|
||||
if command -v gh &> /dev/null; then
|
||||
gh issue close "$ISSUE_NUM" \
|
||||
-c "Resolved in branch $DMUX_SLUG, merged to $DMUX_TARGET_BRANCH" \
|
||||
2>/dev/null || echo "[Hook] Warning: Failed to close issue (maybe already closed?)"
|
||||
else
|
||||
echo "[Hook] GitHub CLI (gh) not found, skipping issue close"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Send notification to Slack
|
||||
# if [ -n "$SLACK_WEBHOOK" ]; then
|
||||
# echo "[Hook] Sending Slack notification"
|
||||
# curl -s -X POST "$SLACK_WEBHOOK" \
|
||||
# -H "Content-Type: application/json" \
|
||||
# -d "{
|
||||
# \"text\": \"Merged: $DMUX_SLUG → $DMUX_TARGET_BRANCH\",
|
||||
# \"blocks\": [
|
||||
# {
|
||||
# \"type\": \"section\",
|
||||
# \"text\": {
|
||||
# \"type\": \"mrkdwn\",
|
||||
# \"text\": \"*Branch Merged* :rocket:\n\n*From:* \`$DMUX_SLUG\`\n*To:* \`$DMUX_TARGET_BRANCH\`\n*Task:* $DMUX_PROMPT\"
|
||||
# }
|
||||
# }
|
||||
# ]
|
||||
# }" > /dev/null
|
||||
# fi
|
||||
|
||||
echo "[Hook] Post-merge processing complete"
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/bin/bash
|
||||
# Example: run_dev hook
|
||||
#
|
||||
# This hook starts a dev server and optionally creates a tunnel for sharing.
|
||||
# It reports the server URL back to dmux via the HTTP API.
|
||||
|
||||
set -e
|
||||
|
||||
echo "[Hook] Starting dev server for $DMUX_SLUG"
|
||||
|
||||
cd "$DMUX_WORKTREE_PATH"
|
||||
API_URL="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/dev"
|
||||
|
||||
# Update status: starting
|
||||
curl -s -X PUT "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "running"}' > /dev/null
|
||||
|
||||
# Start dev server in background
|
||||
# Adjust the command for your project (pnpm dev, npm run dev, vite, etc.)
|
||||
LOG_FILE="/tmp/dmux-dev-$DMUX_PANE_ID.log"
|
||||
pnpm dev > "$LOG_FILE" 2>&1 &
|
||||
DEV_PID=$!
|
||||
|
||||
# Wait for server to be ready
|
||||
echo "[Hook] Waiting for dev server to start..."
|
||||
sleep 5
|
||||
|
||||
# Detect port from log output
|
||||
# Adjust the grep pattern for your dev server's output format
|
||||
PORT=$(grep -oP '(?<=localhost:)\d+' "$LOG_FILE" | head -1)
|
||||
|
||||
if [ -z "$PORT" ]; then
|
||||
echo "[Hook] Warning: Could not detect port from logs, using default 3000"
|
||||
PORT=3000
|
||||
fi
|
||||
|
||||
LOCAL_URL="http://localhost:$PORT"
|
||||
echo "[Hook] Dev server running at $LOCAL_URL"
|
||||
|
||||
# Optional: Create a public tunnel (uncomment to enable)
|
||||
# Requires ngrok, cloudflared, or another tunneling tool
|
||||
|
||||
# Example with cloudflared:
|
||||
# TUNNEL_URL=$(cloudflared tunnel --url "$LOCAL_URL" 2>&1 | \
|
||||
# grep -oP 'https://[a-z0-9-]+\.trycloudflare\.com' | head -1)
|
||||
|
||||
# Example with ngrok:
|
||||
# TUNNEL_URL=$(ngrok http $PORT --log=stdout 2>&1 | \
|
||||
# grep -oP 'url=https://[^"]+' | head -1 | cut -d= -f2)
|
||||
|
||||
# For now, just use local URL (uncomment tunnel code above to enable)
|
||||
FINAL_URL="$LOCAL_URL"
|
||||
|
||||
# Report status back to dmux
|
||||
curl -s -X PUT "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"status\": \"running\", \"url\": \"$FINAL_URL\"}" > /dev/null
|
||||
|
||||
echo "[Hook] Dev server ready at: $FINAL_URL"
|
||||
echo "[Hook] Dev server PID: $DEV_PID"
|
||||
echo "[Hook] Log file: $LOG_FILE"
|
||||
Executable
+61
@@ -0,0 +1,61 @@
|
||||
#!/bin/bash
|
||||
# Example: run_test hook
|
||||
#
|
||||
# This hook runs tests and reports the status back to dmux via the HTTP API.
|
||||
# Status updates appear in real-time in the dmux UI.
|
||||
|
||||
set -e
|
||||
|
||||
echo "[Hook] Running tests for $DMUX_SLUG"
|
||||
|
||||
cd "$DMUX_WORKTREE_PATH"
|
||||
API_URL="http://localhost:$DMUX_SERVER_PORT/api/panes/$DMUX_PANE_ID/test"
|
||||
|
||||
# Update status: running
|
||||
curl -s -X PUT "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "running"}' > /dev/null
|
||||
|
||||
echo "[Hook] Running test suite..."
|
||||
|
||||
# Capture test output
|
||||
OUTPUT_FILE="/tmp/dmux-test-$DMUX_PANE_ID.txt"
|
||||
|
||||
# Run tests (adjust command for your project)
|
||||
# Examples:
|
||||
# - pnpm test
|
||||
# - npm test
|
||||
# - vitest run
|
||||
# - jest
|
||||
# - pytest
|
||||
# - cargo test
|
||||
if pnpm test > "$OUTPUT_FILE" 2>&1; then
|
||||
STATUS="passed"
|
||||
echo "[Hook] Tests passed ✓"
|
||||
else
|
||||
STATUS="failed"
|
||||
echo "[Hook] Tests failed ✗"
|
||||
fi
|
||||
|
||||
# Get output (truncate if too long)
|
||||
OUTPUT=$(head -c 5000 "$OUTPUT_FILE")
|
||||
|
||||
# Report results back to dmux
|
||||
curl -s -X PUT "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n \
|
||||
--arg status "$STATUS" \
|
||||
--arg output "$OUTPUT" \
|
||||
'{status: $status, output: $output}')" > /dev/null
|
||||
|
||||
# Cleanup
|
||||
rm -f "$OUTPUT_FILE"
|
||||
|
||||
echo "[Hook] Test results reported to dmux"
|
||||
|
||||
# Exit with test status
|
||||
if [ "$STATUS" = "passed" ]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
#!/bin/bash
|
||||
# Example: worktree_created hook
|
||||
#
|
||||
# This hook runs after a new worktree is created and before the agent launches.
|
||||
# Use it to set up the worktree environment (install deps, copy configs, etc.)
|
||||
# Stdout/stderr is streamed into the new pane's setup UI while this hook runs.
|
||||
# dmux waits for this hook without a fixed timeout.
|
||||
|
||||
set -e # Exit on error
|
||||
|
||||
status() {
|
||||
if [ "${DMUX_PROGRESS:-0}" = "1" ]; then
|
||||
echo "${DMUX_STATUS_PREFIX:-DMUX_STATUS:} $*"
|
||||
else
|
||||
echo "[Hook] $*"
|
||||
fi
|
||||
}
|
||||
|
||||
status "Setting up worktree: $DMUX_SLUG"
|
||||
|
||||
cd "$DMUX_WORKTREE_PATH"
|
||||
|
||||
# Install dependencies before the agent launches.
|
||||
if [ -f "pnpm-lock.yaml" ]; then
|
||||
status "Installing dependencies with pnpm"
|
||||
pnpm install --prefer-offline
|
||||
elif [ -f "package-lock.json" ]; then
|
||||
status "Installing dependencies with npm"
|
||||
npm install
|
||||
elif [ -f "yarn.lock" ]; then
|
||||
status "Installing dependencies with yarn"
|
||||
yarn install
|
||||
fi
|
||||
|
||||
# Copy environment file if it exists
|
||||
if [ -f "$DMUX_ROOT/.env.local" ]; then
|
||||
status "Copying .env.local"
|
||||
cp "$DMUX_ROOT/.env.local" "$DMUX_WORKTREE_PATH/.env.local"
|
||||
fi
|
||||
|
||||
# Keep existing git author identity.
|
||||
# Do not set git user.name/user.email in this hook.
|
||||
|
||||
# Create a log entry
|
||||
echo "[$(date)] Created worktree: $DMUX_SLUG | Agent: $DMUX_AGENT | Prompt: $DMUX_PROMPT" \
|
||||
>> "$DMUX_ROOT/.dmux/worktree_history.log"
|
||||
|
||||
status "Worktree setup complete"
|
||||
@@ -58,3 +58,4 @@ s3_cache
|
||||
/*-source/
|
||||
/.cache/
|
||||
/internal/router/root/dist/
|
||||
.dmux/
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"description": "dmux pane status hooks for Grok Build",
|
||||
"hooks": {
|
||||
"Stop": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node '/Users/ryan/DEV/Go/Wavelet/.dmux/worktrees/handler-model-logics-user/.grok/hooks/dmux-status-hook.cjs'",
|
||||
"timeout": 5,
|
||||
"env": {
|
||||
"DMUX_PANE_ID": "dmux-1781749411085",
|
||||
"DMUX_TMUX_PANE_ID": "%2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"Notification": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node '/Users/ryan/DEV/Go/Wavelet/.dmux/worktrees/handler-model-logics-user/.grok/hooks/dmux-status-hook.cjs'",
|
||||
"timeout": 5,
|
||||
"env": {
|
||||
"DMUX_PANE_ID": "dmux-1781749411085",
|
||||
"DMUX_TMUX_PANE_ID": "%2"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
|
||||
function normalizeHookEventName(value) {
|
||||
const raw = String(value || '');
|
||||
const normalized = raw.trim().toLowerCase().replace(/-/g, '_');
|
||||
switch (normalized) {
|
||||
case 'stop':
|
||||
return 'Stop';
|
||||
case 'notification':
|
||||
return 'Notification';
|
||||
case 'user_prompt_submit':
|
||||
case 'userpromptsubmit':
|
||||
return 'UserPromptSubmit';
|
||||
case 'pre_tool_use':
|
||||
case 'pretooluse':
|
||||
return 'PreToolUse';
|
||||
case 'post_tool_use':
|
||||
case 'posttooluse':
|
||||
return 'PostToolUse';
|
||||
case 'post_tool_use_failure':
|
||||
case 'posttoolusefailure':
|
||||
return 'PostToolUseFailure';
|
||||
case 'session_start':
|
||||
case 'sessionstart':
|
||||
return 'SessionStart';
|
||||
case 'session_end':
|
||||
case 'sessionend':
|
||||
return 'SessionEnd';
|
||||
default:
|
||||
return raw;
|
||||
}
|
||||
}
|
||||
|
||||
function stringValue(...values) {
|
||||
for (const value of values) {
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
let input = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => {
|
||||
input += chunk;
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
let payload = {};
|
||||
try {
|
||||
payload = input.trim() ? JSON.parse(input) : {};
|
||||
} catch (error) {
|
||||
payload = { parse_error: String(error), raw: input };
|
||||
}
|
||||
|
||||
const hookEventName = normalizeHookEventName(
|
||||
payload.hookEventName || payload.hook_event_name || process.env.GROK_HOOK_EVENT
|
||||
);
|
||||
const sessionId = stringValue(
|
||||
payload.sessionId,
|
||||
payload.session_id,
|
||||
process.env.GROK_SESSION_ID
|
||||
);
|
||||
const message = stringValue(
|
||||
payload.lastAssistantMessage,
|
||||
payload.last_assistant_message,
|
||||
payload.message,
|
||||
payload.notificationMessage,
|
||||
payload.notification_message
|
||||
);
|
||||
|
||||
const event = {
|
||||
source: 'grok-status-hook',
|
||||
dmuxPaneId: process.env.DMUX_PANE_ID || '',
|
||||
tmuxPaneId: process.env.DMUX_TMUX_PANE_ID || '',
|
||||
expectedDmuxPaneId: 'dmux-1781749411085',
|
||||
expectedTmuxPaneId: '%2',
|
||||
hookEventName,
|
||||
sessionId,
|
||||
turnId: stringValue(payload.turnId, payload.turn_id, sessionId),
|
||||
lastAssistantMessage: message || null,
|
||||
transcriptPath: stringValue(payload.transcriptPath, payload.transcript_path) || null,
|
||||
cwd: stringValue(payload.cwd, payload.workspaceRoot, process.env.GROK_WORKSPACE_ROOT) || process.cwd(),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
if (event.dmuxPaneId !== event.expectedDmuxPaneId) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
try {
|
||||
fs.writeFileSync('/Users/ryan/DEV/Go/Wavelet/.dmux/worktrees/handler-model-logics-user/.grok/dmux/dmux-1781749411085.json', JSON.stringify(event, null, 2));
|
||||
} catch (error) {
|
||||
process.exit(0);
|
||||
}
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/admin/push"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/listener"
|
||||
)
|
||||
|
||||
// AdminLogin is the metadata definition for the admin login event.
|
||||
@@ -24,20 +24,15 @@ var AdminLogin = push.EventMetadata{
|
||||
Description: "当管理员成功登录系统时触发此通知",
|
||||
}
|
||||
|
||||
func init() {
|
||||
push.RegisterBuiltInEvent(AdminLogin)
|
||||
}
|
||||
|
||||
// TriggerAdminLoginEvent triggers the admin login event asynchronously.
|
||||
func TriggerAdminLoginEvent(ctx context.Context, user *model.User, ip string) {
|
||||
if user == nil || !user.IsAdmin {
|
||||
func handleAdminLogin(ctx context.Context, event listener.AdminLoggedIn) {
|
||||
if event.User == nil {
|
||||
return
|
||||
}
|
||||
|
||||
body := map[string]any{
|
||||
"user": user,
|
||||
"ip": ip,
|
||||
"user": event.User,
|
||||
"ip": event.IP,
|
||||
"time": time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
push.DefaultTrigger.Trigger(ctx, AdminLogin, body)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package custom_events
|
||||
|
||||
import (
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/admin/push"
|
||||
"github.com/Rain-kl/Wavelet/internal/listener"
|
||||
)
|
||||
|
||||
// Register wires push notification handlers for domain events and registers
|
||||
// built-in event metadata. Must be called once during application bootstrap
|
||||
// before push.SyncEvents.
|
||||
func Register() {
|
||||
push.RegisterBuiltInEvent(AdminLogin)
|
||||
listener.OnAdminLoggedIn(handleAdminLogin)
|
||||
}
|
||||
@@ -15,8 +15,9 @@ import (
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
)
|
||||
|
||||
func init() {
|
||||
task.OnTaskCompleted = handleTaskCompleted
|
||||
// RegisterTaskListeners subscribes push notification handlers to task completion events.
|
||||
func RegisterTaskListeners() {
|
||||
task.OnTaskCompleted(handleTaskCompleted)
|
||||
}
|
||||
|
||||
// handleTaskCompleted handles task completions and triggers appropriate push events.
|
||||
|
||||
@@ -13,7 +13,6 @@ import ("fmt"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/admin"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/task"
|
||||
taskhandlers "github.com/Rain-kl/Wavelet/internal/task/handlers"
|
||||
"github.com/Rain-kl/Wavelet/internal/task/scheduler"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -21,10 +20,6 @@ import ("fmt"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response")
|
||||
|
||||
func init() {
|
||||
taskhandlers.Register()
|
||||
}
|
||||
|
||||
// ListTaskTypes 获取支持的任务类型列表
|
||||
// @Summary 获取支持的任务类型
|
||||
// @Description 返回系统支持的所有可调度任务类型列表,包括任务名称、描述、是否支持时间范围等元数据,需要管理员权限
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/admin/push/custom_events"
|
||||
"github.com/Rain-kl/Wavelet/internal/common"
|
||||
"github.com/Rain-kl/Wavelet/internal/common/response"
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/listener"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
@@ -678,7 +678,7 @@ func handleCallbackLogin(ctx context.Context, c *gin.Context, source *model.Auth
|
||||
|
||||
logger.InfoF(ctx, "[LoginAudit] successful OAuth login via source: %s, external ID: %s, user: %s, ID: %d, IP: %s", source.Name, userInfo.Sub, user.Username, user.ID, c.ClientIP())
|
||||
|
||||
custom_events.TriggerAdminLoginEvent(ctx, &user, c.ClientIP())
|
||||
listener.EmitAdminLoggedIn(ctx, &user, c.ClientIP())
|
||||
|
||||
c.JSON(http.StatusOK, response.OK(buildCallbackResult(&user, "logged_in")))
|
||||
}
|
||||
|
||||
@@ -8,12 +8,12 @@ import ("context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/admin/push/custom_events"
|
||||
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
|
||||
"github.com/Rain-kl/Wavelet/internal/common"
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/db"
|
||||
"github.com/Rain-kl/Wavelet/internal/db/idgen"
|
||||
"github.com/Rain-kl/Wavelet/internal/listener"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/util"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
@@ -194,7 +194,7 @@ func Login(c *gin.Context) {
|
||||
|
||||
logger.InfoF(ctx, "[LoginAudit] successful login for user: %s, ID: %d, IP: %s", user.Username, user.ID, c.ClientIP())
|
||||
|
||||
custom_events.TriggerAdminLoginEvent(ctx, &user, c.ClientIP())
|
||||
listener.EmitAdminLoggedIn(ctx, &user, c.ClientIP())
|
||||
|
||||
c.JSON(http.StatusOK, response.OK(oauth.BuildBasicUserInfo(&user, needChangePassword)))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package listener provides domain event dispatch for cross-module integration.
|
||||
// Core domains emit events here; operational modules (push, webhooks, etc.)
|
||||
// subscribe at the application composition root.
|
||||
package listener
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
)
|
||||
|
||||
// AdminLoggedIn is emitted when an administrator successfully authenticates.
|
||||
type AdminLoggedIn struct {
|
||||
User *model.User
|
||||
IP string
|
||||
}
|
||||
|
||||
// AdminLoggedInHandler handles administrator login domain events.
|
||||
type AdminLoggedInHandler func(ctx context.Context, event AdminLoggedIn)
|
||||
|
||||
var adminLoggedInHandlers []AdminLoggedInHandler
|
||||
|
||||
// OnAdminLoggedIn registers a handler for administrator login events.
|
||||
// Handlers must be registered during application bootstrap before serving traffic.
|
||||
func OnAdminLoggedIn(handler AdminLoggedInHandler) {
|
||||
adminLoggedInHandlers = append(adminLoggedInHandlers, handler)
|
||||
}
|
||||
|
||||
// EmitAdminLoggedIn dispatches an administrator login event to all registered handlers.
|
||||
func EmitAdminLoggedIn(ctx context.Context, user *model.User, ip string) {
|
||||
if user == nil || !user.IsAdmin {
|
||||
return
|
||||
}
|
||||
|
||||
event := AdminLoggedIn{User: user, IP: ip}
|
||||
for _, handler := range adminLoggedInHandlers {
|
||||
handler(ctx, event)
|
||||
}
|
||||
}
|
||||
@@ -26,8 +26,16 @@ import (
|
||||
// handlerRegistry 已注册的任务处理器
|
||||
var handlerRegistry = make(map[string]TaskHandler)
|
||||
|
||||
// OnTaskCompleted is a hook called when a task execution completes.
|
||||
var OnTaskCompleted func(ctx context.Context, execution *model.TaskExecution, result *TaskResult, execErr error)
|
||||
// CompletedHandler is called when a task execution completes.
|
||||
type CompletedHandler func(ctx context.Context, execution *model.TaskExecution, result *TaskResult, execErr error)
|
||||
|
||||
var taskCompletedHandlers []CompletedHandler
|
||||
|
||||
// OnTaskCompleted registers a handler for task completion events.
|
||||
// Handlers must be registered during application bootstrap before processing tasks.
|
||||
func OnTaskCompleted(handler CompletedHandler) {
|
||||
taskCompletedHandlers = append(taskCompletedHandlers, handler)
|
||||
}
|
||||
|
||||
// RegisterHandler 注册任务处理器
|
||||
// 传入任务类型标识(对应 constants.go 中的 AsynqTask 常量)和 TaskHandler 实现
|
||||
@@ -405,9 +413,17 @@ func completeTaskExecution(ctx context.Context, execution *model.TaskExecution,
|
||||
}
|
||||
}
|
||||
|
||||
if OnTaskCompleted != nil {
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
go OnTaskCompleted(asyncCtx, execution, result, execErr)
|
||||
notifyTaskCompleted(ctx, execution, result, execErr)
|
||||
}
|
||||
|
||||
func notifyTaskCompleted(ctx context.Context, execution *model.TaskExecution, result *TaskResult, execErr error) {
|
||||
if len(taskCompletedHandlers) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
asyncCtx := context.WithoutCancel(ctx)
|
||||
for _, handler := range taskCompletedHandlers {
|
||||
go handler(asyncCtx, execution, result, execErr)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/bootstrap"
|
||||
"github.com/Rain-kl/Wavelet/internal/model"
|
||||
"github.com/Rain-kl/Wavelet/internal/task"
|
||||
"github.com/Rain-kl/Wavelet/pkg/logger"
|
||||
@@ -32,6 +33,8 @@ func GetAsynqClient() *asynq.Client {
|
||||
|
||||
// StartScheduler 启动调度器 (该函数阻塞,直到调度器退出)
|
||||
func StartScheduler() error {
|
||||
bootstrap.RegisterScheduler()
|
||||
|
||||
var err error
|
||||
schedulerOnce.Do(func() {
|
||||
quitChan = make(chan struct{})
|
||||
|
||||
@@ -7,22 +7,18 @@ package worker
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Rain-kl/Wavelet/internal/bootstrap"
|
||||
"github.com/Rain-kl/Wavelet/internal/config"
|
||||
"github.com/Rain-kl/Wavelet/internal/task"
|
||||
taskhandlers "github.com/Rain-kl/Wavelet/internal/task/handlers"
|
||||
"github.com/hibiken/asynq"
|
||||
)
|
||||
|
||||
// workerShutdownTimeout Worker 优雅关闭超时时间
|
||||
const workerShutdownTimeout = 3 * time.Minute
|
||||
|
||||
func init() {
|
||||
// 注册所有任务处理器
|
||||
taskhandlers.Register()
|
||||
}
|
||||
|
||||
// StartWorker 启动任务处理服务器
|
||||
func StartWorker() error {
|
||||
bootstrap.RegisterWorker()
|
||||
asynqServer := asynq.NewServer(
|
||||
task.RedisOpt,
|
||||
asynq.Config{
|
||||
|
||||
Reference in New Issue
Block a user