tanuki-telemetry 1.4.0 → 1.4.1
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/install.sh +65 -33
- package/package.json +1 -1
- package/skills/commands/compare-image.md +414 -0
- package/skills/commands/coordinate.md +283 -0
- package/skills/commands/live-browser.md +45 -0
- package/skills/commands/marathon.md +111 -0
- package/skills/commands/revive.md +144 -0
- package/skills/commands/speak.md +49 -0
- package/skills/scripts/revive-watcher.sh +149 -0
package/install.sh
CHANGED
|
@@ -7,7 +7,7 @@ set -e
|
|
|
7
7
|
|
|
8
8
|
TANUKI_DIR="${TANUKI_DIR:-$HOME/.claude/mcp-servers/telemetry}"
|
|
9
9
|
DATA_DIR="${DATA_DIR:-$HOME/.tanuki/data}"
|
|
10
|
-
VERSION="1.
|
|
10
|
+
VERSION="1.4.0"
|
|
11
11
|
|
|
12
12
|
echo ""
|
|
13
13
|
echo " ┌─────────────────────────┐"
|
|
@@ -15,8 +15,8 @@ echo " │ TANUKI // v${VERSION} │"
|
|
|
15
15
|
echo " └─────────────────────────┘"
|
|
16
16
|
echo ""
|
|
17
17
|
|
|
18
|
-
# Check prerequisites
|
|
19
|
-
command -v
|
|
18
|
+
# Check prerequisites (Node only — no Docker required)
|
|
19
|
+
command -v node >/dev/null 2>&1 || { echo "Error: node is required. Install Node.js 18+ first."; exit 1; }
|
|
20
20
|
command -v git >/dev/null 2>&1 || { echo "Error: git is required."; exit 1; }
|
|
21
21
|
|
|
22
22
|
# Clone or update
|
|
@@ -27,58 +27,90 @@ if [ -d "$TANUKI_DIR/.git" ]; then
|
|
|
27
27
|
else
|
|
28
28
|
echo "[1/4] Cloning tanuki..."
|
|
29
29
|
mkdir -p "$(dirname "$TANUKI_DIR")"
|
|
30
|
-
git clone https://github.com/
|
|
30
|
+
git clone https://github.com/ykim-24/tanuki-telemetry.git "$TANUKI_DIR" 2>/dev/null || {
|
|
31
31
|
echo " Repo not accessible — using local copy"
|
|
32
32
|
}
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
35
|
cd "$TANUKI_DIR"
|
|
36
36
|
|
|
37
|
-
# Create data
|
|
38
|
-
mkdir -p "$DATA_DIR"
|
|
37
|
+
# Create data directories
|
|
38
|
+
mkdir -p "$DATA_DIR/screenshots"
|
|
39
|
+
mkdir -p "$DATA_DIR/artifacts"
|
|
40
|
+
mkdir -p "$DATA_DIR/walkthrough-screenshots"
|
|
39
41
|
|
|
40
|
-
#
|
|
41
|
-
echo "[2/4] Building
|
|
42
|
-
|
|
42
|
+
# Install and build
|
|
43
|
+
echo "[2/4] Building..."
|
|
44
|
+
npm ci --silent 2>/dev/null || npm install --silent
|
|
45
|
+
npm run build
|
|
43
46
|
|
|
44
|
-
#
|
|
47
|
+
# Start dashboard (native Node, no Docker)
|
|
45
48
|
echo "[3/4] Starting dashboard..."
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-p 3333:3333 \
|
|
50
|
-
-v "$DATA_DIR:/data" \
|
|
51
|
-
telemetry-dashboard:latest
|
|
49
|
+
chmod +x start-dashboard.sh
|
|
50
|
+
DATA_DIR="$DATA_DIR" ./start-dashboard.sh stop 2>/dev/null || true
|
|
51
|
+
DATA_DIR="$DATA_DIR" ./start-dashboard.sh start
|
|
52
52
|
|
|
53
|
-
#
|
|
54
|
-
echo "[4/
|
|
53
|
+
# Install skills
|
|
54
|
+
echo "[4/5] Installing skills..."
|
|
55
|
+
mkdir -p "$HOME/.claude/commands" "$HOME/.claude/scripts"
|
|
56
|
+
for cmd in skills/commands/*.md; do
|
|
57
|
+
[ -f "$cmd" ] && cp "$cmd" "$HOME/.claude/commands/" && echo " ✓ $(basename "$cmd")"
|
|
58
|
+
done
|
|
59
|
+
for script in skills/scripts/*; do
|
|
60
|
+
[ -f "$script" ] && cp "$script" "$HOME/.claude/scripts/" && chmod +x "$HOME/.claude/scripts/$(basename "$script")" && echo " ✓ $(basename "$script")"
|
|
61
|
+
done
|
|
62
|
+
|
|
63
|
+
# Configure Claude Code MCP (native Node, no Docker)
|
|
64
|
+
echo "[5/5] Configuring Claude Code..."
|
|
55
65
|
CLAUDE_CONFIG="$HOME/.claude.json"
|
|
56
66
|
if [ -f "$CLAUDE_CONFIG" ]; then
|
|
57
|
-
# Check if telemetry MCP is already configured
|
|
58
67
|
if grep -q '"telemetry"' "$CLAUDE_CONFIG" 2>/dev/null; then
|
|
59
|
-
|
|
68
|
+
# Update to native mode
|
|
69
|
+
python3 -c "
|
|
70
|
+
import json
|
|
71
|
+
with open('$CLAUDE_CONFIG', 'r') as f:
|
|
72
|
+
config = json.load(f)
|
|
73
|
+
config['mcpServers']['telemetry'] = {
|
|
74
|
+
'type': 'stdio',
|
|
75
|
+
'command': 'node',
|
|
76
|
+
'args': ['$TANUKI_DIR/dist/index.js'],
|
|
77
|
+
'env': {'DATA_DIR': '$DATA_DIR'}
|
|
78
|
+
}
|
|
79
|
+
with open('$CLAUDE_CONFIG', 'w') as f:
|
|
80
|
+
json.dump(config, f, indent=2)
|
|
81
|
+
"
|
|
82
|
+
echo " ✓ Updated MCP config to native mode"
|
|
60
83
|
else
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
python3 -c "
|
|
85
|
+
import json
|
|
86
|
+
with open('$CLAUDE_CONFIG', 'r') as f:
|
|
87
|
+
config = json.load(f)
|
|
88
|
+
if 'mcpServers' not in config:
|
|
89
|
+
config['mcpServers'] = {}
|
|
90
|
+
config['mcpServers']['telemetry'] = {
|
|
91
|
+
'type': 'stdio',
|
|
92
|
+
'command': 'node',
|
|
93
|
+
'args': ['$TANUKI_DIR/dist/index.js'],
|
|
94
|
+
'env': {'DATA_DIR': '$DATA_DIR'}
|
|
95
|
+
}
|
|
96
|
+
with open('$CLAUDE_CONFIG', 'w') as f:
|
|
97
|
+
json.dump(config, f, indent=2)
|
|
98
|
+
"
|
|
99
|
+
echo " ✓ Added telemetry MCP to .claude.json"
|
|
69
100
|
fi
|
|
70
101
|
else
|
|
71
|
-
echo "
|
|
102
|
+
echo " Add this to your .claude.json:"
|
|
72
103
|
echo ""
|
|
73
|
-
echo
|
|
104
|
+
echo " \"mcpServers\": { \"telemetry\": { \"type\": \"stdio\", \"command\": \"node\", \"args\": [\"$TANUKI_DIR/dist/index.js\"], \"env\": { \"DATA_DIR\": \"$DATA_DIR\" } } }"
|
|
74
105
|
echo ""
|
|
75
106
|
fi
|
|
76
107
|
|
|
77
|
-
#
|
|
108
|
+
# Verify
|
|
78
109
|
sleep 2
|
|
79
|
-
if curl -s http://localhost:3333/health | grep -q '"ok":true'; then
|
|
110
|
+
if curl -s http://localhost:3333/health 2>/dev/null | grep -q '"ok":true'; then
|
|
80
111
|
echo ""
|
|
81
|
-
echo " Tanuki is running at http://localhost:3333"
|
|
112
|
+
echo " ✓ Tanuki is running at http://localhost:3333"
|
|
113
|
+
echo " ✓ Restart Claude Code to connect the MCP server"
|
|
82
114
|
echo ""
|
|
83
115
|
else
|
|
84
116
|
echo ""
|
package/package.json
CHANGED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
# Compare Image — Visual Diff with Qualitative Annotations
|
|
2
|
+
|
|
3
|
+
Compare two sets of images (reference vs actual) with pixel-diff heatmaps and qualitative callouts. Works for any before/after image comparison: UI screenshots, design mockups, rendered templates, chart output, etc.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
/compare-image <ref-dir> <actual-dir>
|
|
9
|
+
/compare-image ./mockups ./screenshots
|
|
10
|
+
/compare-image --ref ./expected --actual ./output --session-id=<existing-session>
|
|
11
|
+
/compare-image --ref ./v1-screenshots --actual ./v2-screenshots --output-dir=./diffs
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Arguments:**
|
|
15
|
+
- `ref-dir`: Directory containing reference (expected) images — PNGs, numbered or named.
|
|
16
|
+
- `actual-dir`: Directory containing actual (generated/current) images to compare against.
|
|
17
|
+
- `--session-id=<id>`: Attach to an existing telemetry session instead of creating a new one.
|
|
18
|
+
- `--output-dir=<path>`: Override output directory (default: `$TANUKI_OUTPUTS/comparisons/`)
|
|
19
|
+
- `--label=<name>`: Label for this comparison set (default: derived from directory names).
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- **Python packages:** `fitz` (PyMuPDF — only if comparing PDFs), `PIL` (Pillow), `numpy`
|
|
26
|
+
- **agent-browser** via `npx agent-browser` (only if capturing live screenshots)
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Workflow
|
|
31
|
+
|
|
32
|
+
### Phase 1: Setup & Discovery
|
|
33
|
+
|
|
34
|
+
1. **Parse arguments** — extract directories and flags.
|
|
35
|
+
2. **Create telemetry session** (unless `--session-id` provided):
|
|
36
|
+
```
|
|
37
|
+
mcp__telemetry__log_session_start({ worktree_name: "image-comparison-<date>" })
|
|
38
|
+
```
|
|
39
|
+
3. **Discover image pairs** — match reference and actual images by filename or index:
|
|
40
|
+
- Sort both directories by filename
|
|
41
|
+
- Pair them 1:1 (ref-01.png ↔ actual-01.png, or by matching name stems)
|
|
42
|
+
- Report any unmatched images
|
|
43
|
+
4. **Log event** with pair count and any mismatches.
|
|
44
|
+
|
|
45
|
+
### Phase 2: Prepare Reference Images
|
|
46
|
+
|
|
47
|
+
Depending on your source format, prepare reference PNGs:
|
|
48
|
+
|
|
49
|
+
- **Already PNGs:** Use directly — no conversion needed.
|
|
50
|
+
- **From PDF:** Render pages to PNGs via PyMuPDF:
|
|
51
|
+
```python
|
|
52
|
+
import fitz
|
|
53
|
+
doc = fitz.open(pdf_path)
|
|
54
|
+
for i, page in enumerate(doc):
|
|
55
|
+
zoom = 1920 / page.rect.width
|
|
56
|
+
mat = fitz.Matrix(zoom, zoom)
|
|
57
|
+
pix = page.get_pixmap(matrix=mat)
|
|
58
|
+
pix.save(f'ref/image-{i+1:02d}.png')
|
|
59
|
+
```
|
|
60
|
+
- **From live URL:** Capture with agent-browser:
|
|
61
|
+
```bash
|
|
62
|
+
npx agent-browser --url "http://localhost:3000/page" --width 1920 --height 1080 --output ref/page.png
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Phase 3: Prepare Actual Images
|
|
66
|
+
|
|
67
|
+
Same as Phase 2 — get actual/generated images as PNGs by whatever method fits your use case (screenshots, renders, exports, etc.).
|
|
68
|
+
|
|
69
|
+
### Phase 4: Qualitative Analysis (visual review)
|
|
70
|
+
|
|
71
|
+
For each image pair, **read both images** and identify every meaningful difference. Think of this as a visual code review — call out specifics, not just "things changed."
|
|
72
|
+
|
|
73
|
+
| Category | What to look for |
|
|
74
|
+
|----------|-----------------|
|
|
75
|
+
| **Layout** | Element positioning, spacing, alignment, column/grid structure |
|
|
76
|
+
| **Text** | Content differences, missing text, placeholder values, truncation |
|
|
77
|
+
| **Images/icons** | Missing assets, wrong variants, broken renders, placeholder boxes |
|
|
78
|
+
| **Color/style** | Background, accent colors, borders, gradients, opacity |
|
|
79
|
+
| **Typography** | Font size, weight, color, line height changes |
|
|
80
|
+
| **Data** | Missing values, wrong numbers, empty states |
|
|
81
|
+
| **Chrome/UI** | Headers, footers, navigation, page numbers, timestamps |
|
|
82
|
+
|
|
83
|
+
**Severity classification:**
|
|
84
|
+
- **CRITICAL** (red): Missing content, broken layout, data that should exist but doesn't
|
|
85
|
+
- **NOTABLE** (yellow): Important differences — content changes, removed elements, placeholder values
|
|
86
|
+
- **MINOR** (blue): Rendering differences — font antialiasing, sub-pixel spacing, minor color shifts
|
|
87
|
+
- **GOOD** (green): Things that match correctly — always include at least one positive finding per pair
|
|
88
|
+
|
|
89
|
+
Build a `callouts` list for each pair: `[{severity, title, details}]` (max 4 per image).
|
|
90
|
+
|
|
91
|
+
### Phase 5: Generate Comparison Images
|
|
92
|
+
|
|
93
|
+
Each comparison image has three columns plus qualitative callout boxes.
|
|
94
|
+
|
|
95
|
+
**Layout:**
|
|
96
|
+
```
|
|
97
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
98
|
+
│ Title: "Page 3 — Dashboard" [DIFF 18.2%] │
|
|
99
|
+
├────────────────────────┬──────────────────────┬──────────────────────────┤
|
|
100
|
+
│ REFERENCE │ PIXEL DIFF HEATMAP │ ACTUAL │
|
|
101
|
+
│ (Expected) │ (red = changes) │ (Current) │
|
|
102
|
+
│ [green border] │ [red border] │ [blue border] │
|
|
103
|
+
│ [533x450] │ [533x450] │ [533x450] │
|
|
104
|
+
├────────────────────────┴──────────────────────┴──────────────────────────┤
|
|
105
|
+
│ DIFFERENCES: │
|
|
106
|
+
│ ┌─CRITICAL────────┐ ┌─NOTABLE─────────┐ ┌─MINOR──────────┐ ┌─GOOD───┐│
|
|
107
|
+
│ │ Title │ │ Title │ │ Title │ │ Title ││
|
|
108
|
+
│ │ Details... │ │ Details... │ │ Details... │ │ Details ││
|
|
109
|
+
│ └─────────────────┘ └─────────────────┘ └────────────────┘ └─────────┘│
|
|
110
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
**Python implementation** (Pillow + numpy):
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from PIL import Image, ImageDraw, ImageFont, ImageFilter
|
|
117
|
+
import numpy as np
|
|
118
|
+
|
|
119
|
+
# --- Constants ---
|
|
120
|
+
COL_W, COL_H = 533, 450 # 3 equal columns
|
|
121
|
+
CALLOUT_H = 170
|
|
122
|
+
PAD = 20
|
|
123
|
+
|
|
124
|
+
COLORS = {
|
|
125
|
+
"critical": ((200, 40, 40), (255, 80, 80)),
|
|
126
|
+
"notable": ((180, 130, 0), (255, 200, 50)),
|
|
127
|
+
"minor": ((60, 130, 180), (100, 180, 240)),
|
|
128
|
+
"good": ((40, 150, 40), (80, 200, 80)),
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
FONT_TITLE = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 28)
|
|
133
|
+
FONT_LABEL = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 18)
|
|
134
|
+
FONT_CALLOUT = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 14)
|
|
135
|
+
FONT_SMALL = ImageFont.truetype("/System/Library/Fonts/Helvetica.ttc", 12)
|
|
136
|
+
except Exception:
|
|
137
|
+
FONT_TITLE = FONT_LABEL = FONT_CALLOUT = FONT_SMALL = ImageFont.load_default()
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def normalize_to_size(img, target_w, target_h, bg_color=(255, 255, 255)):
|
|
141
|
+
"""
|
|
142
|
+
Scale image to fit within target_w x target_h preserving aspect ratio,
|
|
143
|
+
then center it on a background. No stretching, no black bars.
|
|
144
|
+
"""
|
|
145
|
+
img_w, img_h = img.size
|
|
146
|
+
scale = min(target_w / img_w, target_h / img_h)
|
|
147
|
+
new_w = int(img_w * scale)
|
|
148
|
+
new_h = int(img_h * scale)
|
|
149
|
+
scaled = img.resize((new_w, new_h), Image.LANCZOS)
|
|
150
|
+
|
|
151
|
+
canvas = Image.new("RGB", (target_w, target_h), bg_color)
|
|
152
|
+
offset_x = (target_w - new_w) // 2
|
|
153
|
+
offset_y = (target_h - new_h) // 2
|
|
154
|
+
canvas.paste(scaled, (offset_x, offset_y))
|
|
155
|
+
return canvas
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def compute_diff_heatmap(ref, gen, threshold=25):
|
|
159
|
+
"""
|
|
160
|
+
Compute a red pixel-diff heatmap overlaid on the actual image.
|
|
161
|
+
Returns (overlay_image, diff_percentage).
|
|
162
|
+
"""
|
|
163
|
+
ref_arr = np.array(ref.convert("RGB"), dtype=np.float32)
|
|
164
|
+
gen_arr = np.array(gen.convert("RGB"), dtype=np.float32)
|
|
165
|
+
diff = np.abs(ref_arr - gen_arr).mean(axis=2) # grayscale diff per pixel
|
|
166
|
+
|
|
167
|
+
diff_pct = (diff > threshold).sum() / diff.size * 100
|
|
168
|
+
|
|
169
|
+
# Normalize diff to 0-255 intensity
|
|
170
|
+
diff_norm = np.clip(diff * (255 / max(diff.max(), 1)), 0, 255).astype(np.uint8)
|
|
171
|
+
|
|
172
|
+
# Create red overlay: high-diff pixels get bright red, low-diff transparent
|
|
173
|
+
overlay_rgba = np.zeros((*diff_norm.shape, 4), dtype=np.uint8)
|
|
174
|
+
mask = diff_norm > threshold
|
|
175
|
+
overlay_rgba[mask, 0] = 255 # R
|
|
176
|
+
overlay_rgba[mask, 1] = 0 # G
|
|
177
|
+
overlay_rgba[mask, 2] = 0 # B
|
|
178
|
+
overlay_rgba[mask, 3] = np.clip(diff_norm[mask], 60, 180) # A (semi-transparent)
|
|
179
|
+
|
|
180
|
+
red_overlay = Image.fromarray(overlay_rgba, mode="RGBA")
|
|
181
|
+
heatmap = Image.alpha_composite(gen.convert("RGBA"), red_overlay).convert("RGB")
|
|
182
|
+
|
|
183
|
+
return heatmap, diff_pct
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def create_comparison(ref_path, gen_path, out_path, label, callouts):
|
|
187
|
+
"""
|
|
188
|
+
Generate a full comparison image with 3 columns side by side:
|
|
189
|
+
REFERENCE | HEATMAP | ACTUAL — all same height, equal width.
|
|
190
|
+
Plus qualitative callout boxes below.
|
|
191
|
+
"""
|
|
192
|
+
ref = normalize_to_size(Image.open(ref_path), COL_W, COL_H)
|
|
193
|
+
gen = normalize_to_size(Image.open(gen_path), COL_W, COL_H)
|
|
194
|
+
|
|
195
|
+
heatmap, diff_pct = compute_diff_heatmap(ref, gen)
|
|
196
|
+
heatmap_col = heatmap
|
|
197
|
+
|
|
198
|
+
content_w = COL_W * 3 + PAD * 4
|
|
199
|
+
total_h = PAD + 40 + 24 + COL_H + PAD + 24 + CALLOUT_H + PAD
|
|
200
|
+
canvas = Image.new("RGB", (content_w, total_h), (25, 25, 25))
|
|
201
|
+
draw = ImageDraw.Draw(canvas)
|
|
202
|
+
|
|
203
|
+
y = PAD
|
|
204
|
+
|
|
205
|
+
# --- Title bar + diff badge ---
|
|
206
|
+
draw.text((PAD, y), label, fill=(255, 255, 255), font=FONT_TITLE)
|
|
207
|
+
badge_text = f"DIFF {diff_pct:.1f}%"
|
|
208
|
+
if diff_pct < 5:
|
|
209
|
+
badge_color = (40, 150, 40)
|
|
210
|
+
elif diff_pct < 20:
|
|
211
|
+
badge_color = (180, 130, 0)
|
|
212
|
+
else:
|
|
213
|
+
badge_color = (200, 40, 40)
|
|
214
|
+
badge_x = content_w - PAD - 140
|
|
215
|
+
draw.rectangle([badge_x, y, badge_x + 130, y + 30], fill=badge_color)
|
|
216
|
+
draw.text((badge_x + 8, y + 6), badge_text, fill=(255, 255, 255), font=FONT_LABEL)
|
|
217
|
+
y += 44
|
|
218
|
+
|
|
219
|
+
# --- Column labels ---
|
|
220
|
+
col1_x = PAD
|
|
221
|
+
col2_x = PAD * 2 + COL_W
|
|
222
|
+
col3_x = PAD * 3 + COL_W * 2
|
|
223
|
+
draw.text((col1_x, y), "REFERENCE (Expected)", fill=(140, 200, 140), font=FONT_LABEL)
|
|
224
|
+
draw.text((col2_x, y), "PIXEL DIFF HEATMAP", fill=(200, 120, 120), font=FONT_LABEL)
|
|
225
|
+
draw.text((col3_x, y), "ACTUAL (Current)", fill=(140, 160, 240), font=FONT_LABEL)
|
|
226
|
+
y += 24
|
|
227
|
+
|
|
228
|
+
# --- 3 images side by side ---
|
|
229
|
+
canvas.paste(ref, (col1_x, y))
|
|
230
|
+
canvas.paste(heatmap_col, (col2_x, y))
|
|
231
|
+
canvas.paste(gen, (col3_x, y))
|
|
232
|
+
draw.rectangle([col1_x - 1, y - 1, col1_x + COL_W, y + COL_H], outline=(100, 200, 100), width=2)
|
|
233
|
+
draw.rectangle([col2_x - 1, y - 1, col2_x + COL_W, y + COL_H], outline=(200, 60, 60), width=2)
|
|
234
|
+
draw.rectangle([col3_x - 1, y - 1, col3_x + COL_W, y + COL_H], outline=(100, 120, 240), width=2)
|
|
235
|
+
y += COL_H + PAD
|
|
236
|
+
|
|
237
|
+
# --- Callout boxes ---
|
|
238
|
+
draw.text((PAD, y), "DIFFERENCES:", fill=(220, 220, 220), font=FONT_LABEL)
|
|
239
|
+
y += 24
|
|
240
|
+
num = min(len(callouts), 4)
|
|
241
|
+
if num > 0:
|
|
242
|
+
box_w = (content_w - PAD * (num + 1)) // num
|
|
243
|
+
box_h = CALLOUT_H - 30
|
|
244
|
+
for i, c in enumerate(callouts[:4]):
|
|
245
|
+
bx = PAD + i * (box_w + PAD)
|
|
246
|
+
bg, accent = COLORS.get(c["severity"], COLORS["minor"])
|
|
247
|
+
draw.rectangle([bx, y, bx + box_w, y + box_h], fill=(40, 40, 40), outline=accent, width=2)
|
|
248
|
+
draw.rectangle([bx + 2, y + 2, bx + 80, y + 20], fill=bg)
|
|
249
|
+
draw.text((bx + 6, y + 3), c["severity"].upper(), fill=(255, 255, 255), font=FONT_SMALL)
|
|
250
|
+
draw.text((bx + 8, y + 24), c["title"], fill=accent, font=FONT_CALLOUT)
|
|
251
|
+
# Wrap details text
|
|
252
|
+
words = c["details"].split()
|
|
253
|
+
lines, current = [], ""
|
|
254
|
+
for w in words:
|
|
255
|
+
test = f"{current} {w}".strip()
|
|
256
|
+
bbox = draw.textbbox((0, 0), test, font=FONT_SMALL)
|
|
257
|
+
if bbox[2] - bbox[0] > box_w - 16 and current:
|
|
258
|
+
lines.append(current)
|
|
259
|
+
current = w
|
|
260
|
+
else:
|
|
261
|
+
current = test
|
|
262
|
+
if current:
|
|
263
|
+
lines.append(current)
|
|
264
|
+
for j, line in enumerate(lines[:4]):
|
|
265
|
+
draw.text((bx + 8, y + 42 + j * 16), line, fill=(200, 200, 200), font=FONT_SMALL)
|
|
266
|
+
|
|
267
|
+
canvas.save(out_path, quality=95)
|
|
268
|
+
return diff_pct
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Phase 6: Upload to Telemetry (structured findings)
|
|
272
|
+
|
|
273
|
+
Each image comparison produces telemetry artifacts: a screenshot, structured finding events per callout, and an image-level summary event.
|
|
274
|
+
|
|
275
|
+
#### 6a. Screenshots per image pair
|
|
276
|
+
|
|
277
|
+
**The comparison image is always the primary output:**
|
|
278
|
+
```
|
|
279
|
+
mcp__telemetry__log_screenshot({
|
|
280
|
+
session_id,
|
|
281
|
+
phase: "verification",
|
|
282
|
+
description: "[COMPARISON] <label> <N> — <highest severity>: <key finding>",
|
|
283
|
+
file_path: "<absolute path to comparison PNG>"
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Also log as an artifact for download/browsing on the dashboard:
|
|
288
|
+
```
|
|
289
|
+
mcp__telemetry__log_artifact({
|
|
290
|
+
session_id,
|
|
291
|
+
file_path: "<absolute path to comparison PNG>",
|
|
292
|
+
artifact_type: "comparison",
|
|
293
|
+
description: "<label> image <N> comparison",
|
|
294
|
+
metadata: { label: "<label>", image_number: <N>, diff_pct: <X.X> }
|
|
295
|
+
})
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
#### 6b. Structured finding event per callout
|
|
299
|
+
|
|
300
|
+
For **each individual finding**, log a `comparison_finding` event with queryable metadata:
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
mcp__telemetry__log_event({
|
|
304
|
+
session_id,
|
|
305
|
+
phase: "verification",
|
|
306
|
+
event_type: "info",
|
|
307
|
+
message: "<severity>: <title> — <label> image <N>",
|
|
308
|
+
metadata: {
|
|
309
|
+
type: "comparison_finding",
|
|
310
|
+
label: "<label>",
|
|
311
|
+
image_number: <N>,
|
|
312
|
+
image_name: "<filename>",
|
|
313
|
+
severity: "<critical|notable|minor|good>",
|
|
314
|
+
finding_title: "<short title>",
|
|
315
|
+
finding_details: "<full description>",
|
|
316
|
+
diff_pct: <X.X>,
|
|
317
|
+
comparison_image: "<absolute path>",
|
|
318
|
+
ref_image: "<absolute path>",
|
|
319
|
+
actual_image: "<absolute path>"
|
|
320
|
+
}
|
|
321
|
+
})
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
#### 6c. Image-level summary event
|
|
325
|
+
|
|
326
|
+
After logging all findings for an image pair:
|
|
327
|
+
|
|
328
|
+
```
|
|
329
|
+
mcp__telemetry__log_event({
|
|
330
|
+
session_id,
|
|
331
|
+
phase: "verification",
|
|
332
|
+
event_type: "info",
|
|
333
|
+
message: "Compared <label> image <N> (<name>) — <highest severity>, diff <X.X>%",
|
|
334
|
+
metadata: {
|
|
335
|
+
type: "comparison_image_summary",
|
|
336
|
+
label: "<label>",
|
|
337
|
+
image_number: <N>,
|
|
338
|
+
image_name: "<filename>",
|
|
339
|
+
diff_pct: <X.X>,
|
|
340
|
+
highest_severity: "<critical|notable|minor|good>",
|
|
341
|
+
finding_count: { critical: <N>, notable: <N>, minor: <N>, good: <N> },
|
|
342
|
+
comparison_image: "<absolute path>"
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
#### 6d. Final rollup event
|
|
348
|
+
|
|
349
|
+
After all image pairs:
|
|
350
|
+
|
|
351
|
+
```
|
|
352
|
+
mcp__telemetry__log_event({
|
|
353
|
+
session_id,
|
|
354
|
+
phase: "deliverables",
|
|
355
|
+
event_type: "info",
|
|
356
|
+
message: "Image comparison complete — <N> images, <C> critical, <N> notable findings",
|
|
357
|
+
metadata: {
|
|
358
|
+
type: "comparison_rollup",
|
|
359
|
+
label: "<label>",
|
|
360
|
+
total_images: <N>,
|
|
361
|
+
total_findings: <N>,
|
|
362
|
+
by_severity: { critical: <N>, notable: <N>, minor: <N>, good: <N> },
|
|
363
|
+
avg_diff_pct: <X.X>
|
|
364
|
+
}
|
|
365
|
+
})
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
#### Querying findings programmatically
|
|
369
|
+
|
|
370
|
+
```sql
|
|
371
|
+
-- All critical findings across sessions
|
|
372
|
+
SELECT * FROM events
|
|
373
|
+
WHERE metadata->>'type' = 'comparison_finding'
|
|
374
|
+
AND metadata->>'severity' = 'critical';
|
|
375
|
+
|
|
376
|
+
-- All findings for a specific comparison
|
|
377
|
+
SELECT * FROM events
|
|
378
|
+
WHERE metadata->>'type' = 'comparison_finding'
|
|
379
|
+
AND metadata->>'label' = 'homepage-redesign';
|
|
380
|
+
|
|
381
|
+
-- Image summaries sorted by diff percentage
|
|
382
|
+
SELECT * FROM events
|
|
383
|
+
WHERE metadata->>'type' = 'comparison_image_summary'
|
|
384
|
+
ORDER BY (metadata->>'diff_pct')::float DESC;
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
### Phase 7: Summary Output
|
|
388
|
+
|
|
389
|
+
```markdown
|
|
390
|
+
## Image Comparison Results
|
|
391
|
+
|
|
392
|
+
| Image | Diff % | Severity | Key Finding |
|
|
393
|
+
|-------|:------:|----------|-------------|
|
|
394
|
+
| 01 — Homepage | 12.3% | NOTABLE | Header layout shifted, CTA button color changed |
|
|
395
|
+
| 02 — Dashboard | 18.2% | CRITICAL | Chart data missing, sidebar collapsed |
|
|
396
|
+
| ... | ... | ... | ... |
|
|
397
|
+
|
|
398
|
+
**Critical:** <count> images
|
|
399
|
+
**Notable:** <count> images
|
|
400
|
+
**Good:** <count> images
|
|
401
|
+
|
|
402
|
+
**Output:** <output-dir>/comparisons/
|
|
403
|
+
**Telemetry:** Session <id>
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Common Use Cases
|
|
409
|
+
|
|
410
|
+
- **UI regression testing:** Compare screenshots before/after a code change
|
|
411
|
+
- **Design fidelity:** Compare design mockup PNGs against implemented page screenshots
|
|
412
|
+
- **Generated content:** Compare expected output against LLM/AI-generated output
|
|
413
|
+
- **Email templates:** Compare HTML email reference renders against actual sends
|
|
414
|
+
- **Chart/data viz:** Compare expected chart renders against actual output
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: |
|
|
3
|
+
Central coordinator mode. Become the hub that manages all cmux workspaces — dispatch work, monitor progress, and persist state across conversations.
|
|
4
|
+
Use this to initialize or resume a coordinator session.
|
|
5
|
+
allowed-tools: Bash, Read, Glob, Grep, Write, Edit, Agent, AskUserQuestion, mcp__telemetry__*, mcp__linear__*
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Coordinate — Central Workspace Coordinator
|
|
9
|
+
|
|
10
|
+
## Role
|
|
11
|
+
|
|
12
|
+
You are the **central coordinator**. The user talks to you, and you manage all cmux workspaces. You do NOT write code directly — you dispatch, monitor, and integrate.
|
|
13
|
+
|
|
14
|
+
## ⛔ CRITICAL RULES
|
|
15
|
+
|
|
16
|
+
1. **You are the hub.** All communication to other workspaces goes through you via `cmux send` + `cmux send-key Enter`.
|
|
17
|
+
2. **Be concise.** Don't dump raw screen output. Summarize what's happening in each workspace.
|
|
18
|
+
3. **Protect context.** This is your biggest constraint. Minimize context usage by:
|
|
19
|
+
- Summarizing `cmux read-screen` output instead of showing it raw
|
|
20
|
+
- Using `mcp__telemetry__get_session_summary` instead of screen reads when possible
|
|
21
|
+
- Periodically compacting state (see Phase 3)
|
|
22
|
+
4. **Persist state.** Save coordinator state to telemetry so you can resume across conversations.
|
|
23
|
+
5. **Be proactive.** After dispatching work, monitor it without being asked. Alert the user when workspaces finish or hit errors.
|
|
24
|
+
6. **Always target the Claude surface.** Run `/cmux-guide` if unsure about any cmux operation. Key rules: never omit `--surface`, use full `"workspace:N"` IDs for new workspaces, always discover surfaces before interacting. See "Surface Discovery" below.
|
|
25
|
+
7. **⛔ LOG EVERYTHING TO THE LIVE FEED.** Every dispatch, every monitor check, every status change, every decision — call `mcp__telemetry__log_event` immediately. The dashboard live feed is useless without constant logging. If you did something and didn't log it, go back and log it now. Target 5+ events per user interaction. Use these event types:
|
|
26
|
+
- `action` — dispatched work, checked workspace, created workspace
|
|
27
|
+
- `decision` — chose which workspace, prioritized a task, skipped something
|
|
28
|
+
- `info` — status update, workspace completed, workspace idle
|
|
29
|
+
- `error` — workspace failed, send failed, workspace unreachable
|
|
30
|
+
8. **⛔ SYNC STATE AFTER EVERY CHANGE.** Call `save_coordinator_state` after EVERY workspace status change, dispatch, or completion — not just "every 5-10 interactions". The dashboard reads state directly. Stale state = stale dashboard.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Phase 1: Initialize
|
|
35
|
+
|
|
36
|
+
### If resuming (default — check for prior state first):
|
|
37
|
+
```
|
|
38
|
+
state = mcp__telemetry__get_coordinator_state()
|
|
39
|
+
```
|
|
40
|
+
If state exists:
|
|
41
|
+
- Load workspace statuses, pending tasks, decisions
|
|
42
|
+
- Run `cmux list-workspaces` to verify what's actually running
|
|
43
|
+
- Reconcile (workspaces may have changed since last session)
|
|
44
|
+
- Present a brief status update to the user
|
|
45
|
+
|
|
46
|
+
### If fresh start:
|
|
47
|
+
```
|
|
48
|
+
cmux list-workspaces
|
|
49
|
+
```
|
|
50
|
+
- Map all active workspaces
|
|
51
|
+
- Create coordinator state:
|
|
52
|
+
```
|
|
53
|
+
mcp__telemetry__save_coordinator_state({
|
|
54
|
+
session_id: "<generated-id>",
|
|
55
|
+
state: {
|
|
56
|
+
workspaces: { ... },
|
|
57
|
+
pending_tasks: [],
|
|
58
|
+
decisions: [],
|
|
59
|
+
notes: ""
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
- Present workspace map to user
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Surface Discovery
|
|
68
|
+
|
|
69
|
+
Workspaces often have multiple surfaces (panes) — Claude terminals, web browsers, dev servers, idle shells. **You must always identify and target the Claude surface** for reads and sends.
|
|
70
|
+
|
|
71
|
+
### How to discover surfaces
|
|
72
|
+
```bash
|
|
73
|
+
cmux list-pane-surfaces --workspace <id>
|
|
74
|
+
```
|
|
75
|
+
This returns all surfaces in a workspace. Identify the Claude surface by:
|
|
76
|
+
- Label contains "Claude Code"
|
|
77
|
+
- When read, shows the `❯` prompt with `bypass permissions` text
|
|
78
|
+
- NOT a browser (URL in label), NOT a dev server (logs), NOT "idle"
|
|
79
|
+
|
|
80
|
+
### Cache surface IDs
|
|
81
|
+
Store the Claude surface ID per workspace in coordinator state:
|
|
82
|
+
```json
|
|
83
|
+
{
|
|
84
|
+
"workspace:1": { "name": "Telemetry MCP", "status": "idle", "claude_surface": "surface:2" },
|
|
85
|
+
"workspace:5": { "name": "CDD Slides", "status": "idle", "claude_surface": "surface:7" }
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Always use `--surface`
|
|
90
|
+
**Every** `read-screen`, `send`, and `send-key` command MUST include `--surface <surface_id>`:
|
|
91
|
+
```bash
|
|
92
|
+
cmux read-screen --workspace <id> --surface <surface_id> --lines 10
|
|
93
|
+
cmux send --workspace <id> --surface <surface_id> "<text>"
|
|
94
|
+
cmux send-key --workspace <id> --surface <surface_id> Enter
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### When to re-discover
|
|
98
|
+
- On initialization (Phase 1) — discover all surfaces for all workspaces
|
|
99
|
+
- After creating a new workspace
|
|
100
|
+
- If a `read-screen` or `send` returns an error or unexpected content (browser HTML, ASCII art, server logs)
|
|
101
|
+
- If a surface ID returns "Surface is not a terminal" error
|
|
102
|
+
|
|
103
|
+
### During Phase 1 initialization
|
|
104
|
+
After `cmux list-workspaces`, run `cmux list-pane-surfaces --workspace <id>` for **every** workspace. Build the full surface map before presenting status.
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Phase 2: Active Coordination
|
|
109
|
+
|
|
110
|
+
### Creating new workspaces
|
|
111
|
+
When spinning up a new workspace:
|
|
112
|
+
1. `cmux new-workspace --cwd <path> --command "claude"` — creates the workspace
|
|
113
|
+
2. **Immediately rename it:** `cmux rename-workspace --workspace <id> "<descriptive-name>"`
|
|
114
|
+
3. Discover surfaces: `cmux list-pane-surfaces --workspace <id>`
|
|
115
|
+
4. Cache the Claude surface ID in coordinator state
|
|
116
|
+
|
|
117
|
+
### Forwarding messages verbatim
|
|
118
|
+
When the user sends a message via `/speak` that is clearly intended for a workspace (e.g., instructions, corrections, feedback), forward it **verbatim** — copy their exact text. Do NOT paraphrase, summarize, or reinterpret. The user chose their words deliberately.
|
|
119
|
+
|
|
120
|
+
### Dispatching work
|
|
121
|
+
When the user asks you to send work to a workspace:
|
|
122
|
+
1. Get the Claude surface from state (or discover via `cmux list-pane-surfaces` if not cached)
|
|
123
|
+
2. Check if workspace is idle: `cmux read-screen --workspace <id> --surface <claude_surface> --lines 5`
|
|
124
|
+
3. If busy, add to `pending_tasks` in state and tell the user
|
|
125
|
+
4. If idle, send via: `cmux send --workspace <id> --surface <claude_surface> "<message>"` then `cmux send-key --workspace <id> --surface <claude_surface> Enter`
|
|
126
|
+
4. Log the dispatch to telemetry:
|
|
127
|
+
```
|
|
128
|
+
mcp__telemetry__log_event({
|
|
129
|
+
session_id: coordinator_session_id,
|
|
130
|
+
phase: "coordination",
|
|
131
|
+
event_type: "action",
|
|
132
|
+
message: "Dispatched to <workspace-name>: <summary>",
|
|
133
|
+
metadata: { workspace: "<id>", full_message: "<what was sent>" }
|
|
134
|
+
})
|
|
135
|
+
```
|
|
136
|
+
5. Update workspace status in state
|
|
137
|
+
|
|
138
|
+
### Monitoring
|
|
139
|
+
When checking on workspaces:
|
|
140
|
+
- **Prefer telemetry over screen reads** — `get_session_summary` is cheaper on context
|
|
141
|
+
- **Screen reads for non-telemetry workspaces** — always use `--surface <claude_surface>`: `cmux read-screen --workspace <id> --surface <claude_surface> --lines 10` (not 40+)
|
|
142
|
+
- **If read returns unexpected content** (HTML, ASCII art, server logs, "Surface is not a terminal") — you're on the wrong surface. Re-discover with `cmux list-pane-surfaces`
|
|
143
|
+
- **Summarize aggressively** — "Telemetry MCP: building screenshot thumbnails, 3min in" not the full output
|
|
144
|
+
- **⛔ Log EVERY monitor check:**
|
|
145
|
+
```
|
|
146
|
+
mcp__telemetry__log_event({
|
|
147
|
+
session_id: coordinator_session_id,
|
|
148
|
+
phase: "coordination",
|
|
149
|
+
event_type: "info",
|
|
150
|
+
message: "<workspace-name>: <what you observed>",
|
|
151
|
+
metadata: { workspace: "<id>", status: "<idle|working|done|failed>" }
|
|
152
|
+
})
|
|
153
|
+
```
|
|
154
|
+
- After monitoring, update state AND call `save_coordinator_state` immediately
|
|
155
|
+
|
|
156
|
+
### Session Linking (for live feed)
|
|
157
|
+
The telemetry dashboard live feed shows events from all workspace sessions — but ONLY if the coordinator state has their `session_id` linked. **You must actively link sessions.**
|
|
158
|
+
|
|
159
|
+
After dispatching `/start-work` to a workspace:
|
|
160
|
+
1. Wait ~30 seconds for the session to be created
|
|
161
|
+
2. Run `mcp__telemetry__list_sessions({ status: "in_progress", limit: 5 })`
|
|
162
|
+
3. Match the new session to the workspace by worktree name
|
|
163
|
+
4. Update coordinator state with the `session_id` on that workspace
|
|
164
|
+
|
|
165
|
+
**Periodically re-link:** Every 5-10 interactions, run `list_sessions` and reconcile — new sessions may have been created, old ones may have completed.
|
|
166
|
+
|
|
167
|
+
Without this linking, the coordinator live feed will be empty even though workspaces are logging events.
|
|
168
|
+
|
|
169
|
+
### Responding to the user
|
|
170
|
+
- Lead with the answer, not the process
|
|
171
|
+
- Use tables for multi-workspace status
|
|
172
|
+
- Don't repeat what the user said
|
|
173
|
+
|
|
174
|
+
### Intent Routing (auto-detect workspace)
|
|
175
|
+
|
|
176
|
+
The user will often just say what they want without naming a workspace. **You must infer the target workspace** from context. Do NOT ask "which workspace?" unless genuinely ambiguous.
|
|
177
|
+
|
|
178
|
+
**Routing priority:**
|
|
179
|
+
1. **Explicit name** — user says "telemetry" or "slides" → direct match
|
|
180
|
+
2. **Topic match** — match keywords to workspace descriptions in state:
|
|
181
|
+
- Screenshots, dashboard, events, sessions, thumbnails → Telemetry MCP
|
|
182
|
+
- Slides, generation, template, PPTX, quality score → Slide Generation
|
|
183
|
+
- Database, JWT, auth, migration → DB Migration
|
|
184
|
+
- (Update these mappings as workspaces change)
|
|
185
|
+
3. **Recency** — if user just saw output from a workspace and responds, it's about that workspace
|
|
186
|
+
4. **Active work** — if only one workspace is actively working, comments about "it" or "that" refer to it
|
|
187
|
+
5. **Ambiguous** — only ask if 2+ workspaces are equally likely. Present as numbered options:
|
|
188
|
+
```
|
|
189
|
+
That could be about:
|
|
190
|
+
1. Telemetry MCP (working on thumbnails)
|
|
191
|
+
2. Slide Generation (idle, last worked on quality)
|
|
192
|
+
Which one?
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**Build the keyword map from workspace state.** When saving state, include a `topics` array per workspace:
|
|
196
|
+
```json
|
|
197
|
+
{
|
|
198
|
+
"workspace:1": { "name": "Telemetry MCP", "topics": ["telemetry", "dashboard", "screenshots", "events", "sessions", "MCP"] },
|
|
199
|
+
"workspace:4": { "name": "Slide Generation", "topics": ["slides", "pptx", "generation", "template", "quality"] }
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**The user should feel like they're just talking, and messages magically go to the right place.**
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Phase 3: Context Management
|
|
208
|
+
|
|
209
|
+
### Periodic state saves
|
|
210
|
+
After EVERY workspace status change, save state immediately. At minimum every 2-3 interactions:
|
|
211
|
+
```
|
|
212
|
+
mcp__telemetry__save_coordinator_state({
|
|
213
|
+
session_id: coordinator_session_id,
|
|
214
|
+
state: { workspaces: {...}, pending_tasks: [...], decisions: [...], notes: "..." }
|
|
215
|
+
})
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### When context is getting heavy
|
|
219
|
+
Signs: conversation is long, lots of screen reads accumulated, you're near context limits.
|
|
220
|
+
|
|
221
|
+
1. Save everything important:
|
|
222
|
+
```
|
|
223
|
+
mcp__telemetry__compact_coordinator_context({
|
|
224
|
+
session_id: coordinator_session_id,
|
|
225
|
+
context: {
|
|
226
|
+
summary: "<what happened this conversation>",
|
|
227
|
+
decisions: ["<key decisions made>"],
|
|
228
|
+
workspace_states: { ... },
|
|
229
|
+
pending_work: ["<things still to do>"],
|
|
230
|
+
user_preferences_learned: ["<any new preferences>"]
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
2. Tell the user: "Context is getting heavy. I've saved state — you can start a fresh `/coordinate` and I'll pick up where we left off."
|
|
235
|
+
3. The user starts a new conversation and runs `/coordinate` — Phase 1 loads the saved state.
|
|
236
|
+
|
|
237
|
+
---
|
|
238
|
+
|
|
239
|
+
## Phase 4: Pending Task Management
|
|
240
|
+
|
|
241
|
+
When a workspace finishes and has pending tasks:
|
|
242
|
+
1. Check the workspace is actually idle
|
|
243
|
+
2. Send the next pending task
|
|
244
|
+
3. Remove from pending list
|
|
245
|
+
4. Update state
|
|
246
|
+
|
|
247
|
+
When the user queues something for a busy workspace:
|
|
248
|
+
1. Add to `pending_tasks` with workspace ID and message
|
|
249
|
+
2. Confirm: "Queued for <workspace-name> — will send when it's free"
|
|
250
|
+
3. Check periodically if it's free
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Workspace Command Reference
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
cmux list-workspaces # see all workspaces
|
|
258
|
+
cmux list-pane-surfaces --workspace <id> # list all surfaces in a workspace
|
|
259
|
+
cmux read-screen --workspace <id> --surface <surface_id> --lines 10 # check output (ALWAYS use --surface)
|
|
260
|
+
cmux send --workspace <id> --surface <surface_id> "<text>" # send message (ALWAYS use --surface)
|
|
261
|
+
cmux send-key --workspace <id> --surface <surface_id> Enter # press Enter (ALWAYS use --surface)
|
|
262
|
+
cmux new-workspace --cwd <path> --command "claude" # spawn new workspace
|
|
263
|
+
cmux rename-workspace --workspace <id> "<name>" # rename a workspace
|
|
264
|
+
cmux notify "<message>" # macOS notification
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**⚠️ Never omit `--surface`.** Bare commands without `--surface` will target whatever surface is "selected" — which may be a browser, dev server, or idle shell, not Claude.
|
|
268
|
+
|
|
269
|
+
**⚠️ Always name new workspaces.** After `cmux new-workspace`, immediately run `cmux rename-workspace --workspace <id> "<descriptive-name>"`. Unnamed workspaces show as "Claude Code" which is useless.
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## On Conversation Start
|
|
274
|
+
|
|
275
|
+
Always begin with:
|
|
276
|
+
1. Load prior coordinator state (or initialize fresh)
|
|
277
|
+
2. `cmux list-workspaces` to get current workspace map
|
|
278
|
+
3. `cmux list-pane-surfaces --workspace <id>` for **every** workspace — build the surface map
|
|
279
|
+
4. Identify the Claude surface in each workspace (label contains "Claude Code", or read to verify)
|
|
280
|
+
5. Cache `claude_surface` per workspace in coordinator state
|
|
281
|
+
6. Quick status check on active workspaces (using `--surface` for all reads)
|
|
282
|
+
7. Present status table to user (include surface IDs for reference)
|
|
283
|
+
8. Ask: "What should we work on?" (or pick up pending tasks)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: |
|
|
3
|
+
Launch agent-browser in headed (visible) mode so you can watch it work in real time.
|
|
4
|
+
Use when you want to see what agent-browser is doing — demos, debugging, or verification.
|
|
5
|
+
allowed-tools: Bash
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# /live-browser — Watch Agent Browser Live
|
|
9
|
+
|
|
10
|
+
Toggles agent-browser between headed (visible) and headless modes.
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
- `/live-browser` or `/live-browser on` — Enable headed mode (browser window visible)
|
|
15
|
+
- `/live-browser off` — Disable headed mode (back to headless)
|
|
16
|
+
- `/live-browser status` — Check current mode
|
|
17
|
+
|
|
18
|
+
## On Invoke
|
|
19
|
+
|
|
20
|
+
Parse the argument (default: "on").
|
|
21
|
+
|
|
22
|
+
### Enable (on)
|
|
23
|
+
```bash
|
|
24
|
+
# Set env for current session
|
|
25
|
+
export AGENT_BROWSER_HEADED=1
|
|
26
|
+
echo "AGENT_BROWSER_HEADED=1" >> ~/.claude/.env 2>/dev/null
|
|
27
|
+
echo "✓ Agent browser is now in HEADED mode — you'll see the Chrome window"
|
|
28
|
+
echo " Note: The browser will take focus when agent-browser runs"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Disable (off)
|
|
32
|
+
```bash
|
|
33
|
+
unset AGENT_BROWSER_HEADED
|
|
34
|
+
sed -i '' '/AGENT_BROWSER_HEADED/d' ~/.claude/.env 2>/dev/null
|
|
35
|
+
echo "✓ Agent browser is back to HEADLESS mode"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Status
|
|
39
|
+
```bash
|
|
40
|
+
if [ "$AGENT_BROWSER_HEADED" = "1" ]; then
|
|
41
|
+
echo "Mode: HEADED (visible browser window)"
|
|
42
|
+
else
|
|
43
|
+
echo "Mode: HEADLESS (no visible window)"
|
|
44
|
+
fi
|
|
45
|
+
```
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: |
|
|
3
|
+
Autonomous workspace monitoring. Checks inbox + workspace screens on a recurring interval and takes action when sessions complete — dispatches queued work, restarts stalled sessions, reports status.
|
|
4
|
+
allowed-tools: Bash, Read, Glob, Grep, CronCreate, CronDelete, AskUserQuestion, mcp__telemetry__*
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /monitor — Autonomous Workspace Monitoring
|
|
8
|
+
|
|
9
|
+
You are a monitoring daemon for the coordinator. You check workspace status periodically and take action when needed.
|
|
10
|
+
|
|
11
|
+
## Arguments
|
|
12
|
+
|
|
13
|
+
- No args → monitor all active workspaces every 5 minutes
|
|
14
|
+
- `<interval>` → custom interval (e.g., `2m`, `10m`)
|
|
15
|
+
- `stop` → cancel all monitoring crons
|
|
16
|
+
|
|
17
|
+
## On Invoke
|
|
18
|
+
|
|
19
|
+
### 1. Discover active workspaces
|
|
20
|
+
```bash
|
|
21
|
+
cmux list-workspaces
|
|
22
|
+
```
|
|
23
|
+
For each non-coordinator workspace, get the Claude surface:
|
|
24
|
+
```bash
|
|
25
|
+
cmux list-pane-surfaces --workspace "workspace:N"
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 2. Set up the monitoring cron
|
|
29
|
+
```
|
|
30
|
+
CronCreate({
|
|
31
|
+
cron: "*/5 * * * *", // or custom interval
|
|
32
|
+
prompt: "MONITOR CHECK: Read coordinator inbox and check all workspace screens",
|
|
33
|
+
recurring: true
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 3. On each cron fire
|
|
38
|
+
|
|
39
|
+
#### Check inbox
|
|
40
|
+
```bash
|
|
41
|
+
cat ~/.claude/coordinator-inbox.jsonl 2>/dev/null | tail -10
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
#### For each active workspace, check screen
|
|
45
|
+
```bash
|
|
46
|
+
cmux read-screen --workspace "workspace:N" --surface surface:X --lines 5
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### Determine status
|
|
50
|
+
| Signal | Status | Action |
|
|
51
|
+
|--------|--------|--------|
|
|
52
|
+
| `esc to interrupt` | Working | No action needed |
|
|
53
|
+
| `❯` prompt only (idle) | Finished or stuck | Check inbox for completion event |
|
|
54
|
+
| `session_end` in inbox | Completed | Dispatch next queued task if any |
|
|
55
|
+
| Same screen for 3+ checks | Possibly stuck | Nudge: "Are you still working? If stuck, /clear and retry." |
|
|
56
|
+
| Error visible on screen | Failed | Log error, notify coordinator |
|
|
57
|
+
|
|
58
|
+
#### If a workspace completed
|
|
59
|
+
1. Read the inbox for details
|
|
60
|
+
2. Check git log for new commits
|
|
61
|
+
3. **Check the task queue for pending tasks:**
|
|
62
|
+
```bash
|
|
63
|
+
~/.claude/scripts/task-queue.sh next
|
|
64
|
+
```
|
|
65
|
+
If a pending task exists, dispatch it to the idle workspace:
|
|
66
|
+
```bash
|
|
67
|
+
TASK_JSON=$(~/.claude/scripts/task-queue.sh claim "<workspace-name>")
|
|
68
|
+
TASK=$(echo "$TASK_JSON" | jq -r '.task')
|
|
69
|
+
CONTEXT=$(echo "$TASK_JSON" | jq -r '.context')
|
|
70
|
+
TASK_ID=$(echo "$TASK_JSON" | jq -r '.id')
|
|
71
|
+
```
|
|
72
|
+
Then send the task to the workspace:
|
|
73
|
+
```bash
|
|
74
|
+
cmux send --workspace "<workspace-id>" "/start-work --context=\"$CONTEXT\" $TASK"
|
|
75
|
+
```
|
|
76
|
+
4. **Track re-queue counts:** If a task has been re-queued 3+ times (check `requeue_count`), do NOT dispatch it again. Instead:
|
|
77
|
+
```bash
|
|
78
|
+
~/.claude/scripts/task-queue.sh fail "$TASK_ID" "Max re-queues reached, needs human review"
|
|
79
|
+
cmux notify "ESCALATE: Task $TASK_ID failed after 3 attempts"
|
|
80
|
+
```
|
|
81
|
+
5. Clear processed inbox messages
|
|
82
|
+
6. Log status to telemetry
|
|
83
|
+
|
|
84
|
+
#### If a workspace seems stuck
|
|
85
|
+
1. Check if it's waiting on something (Inngest job, API call, build)
|
|
86
|
+
2. If idle for 3+ checks with no progress, send a nudge
|
|
87
|
+
3. If nudge doesn't help after 2 more checks, restart the session
|
|
88
|
+
|
|
89
|
+
### 4. Status report
|
|
90
|
+
Every 30 minutes (or 6 checks), output a summary including the task queue:
|
|
91
|
+
```
|
|
92
|
+
MONITOR STATUS:
|
|
93
|
+
- ws:8 (CDD Marathon): Working, 3 commits since last report
|
|
94
|
+
- ws:11 (Import Fix): Completed, dispatched next task
|
|
95
|
+
- Inbox: 2 messages processed
|
|
96
|
+
- Task queue: 3 pending, 1 in_progress, 2 completed, 0 failed
|
|
97
|
+
```
|
|
98
|
+
Check the queue:
|
|
99
|
+
```bash
|
|
100
|
+
~/.claude/scripts/task-queue.sh list
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Stopping
|
|
104
|
+
To stop monitoring:
|
|
105
|
+
```
|
|
106
|
+
CronDelete <job-id>
|
|
107
|
+
```
|
|
108
|
+
Or invoke `/monitor stop` which deletes all monitoring crons.
|
|
109
|
+
|
|
110
|
+
## Key principle
|
|
111
|
+
**Don't just observe — act.** If a workspace finishes and there's queued work, dispatch it immediately. If a workspace is stuck, nudge it. The coordinator shouldn't have to manually check — that's your job.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: |
|
|
3
|
+
Spawn a watcher process that monitors the current workspace and auto-sends a resume command after /clear.
|
|
4
|
+
Keeps coordinator sessions alive across context clears by auto-restarting them.
|
|
5
|
+
allowed-tools: Bash, Read, Write, Edit, AskUserQuestion
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Revive — Auto-Resume After /clear
|
|
9
|
+
|
|
10
|
+
Spawn a background watcher that monitors this workspace and automatically sends a resume command when Claude becomes idle (after /clear or session restart).
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
/revive # Start watcher with defaults (/coordinate)
|
|
16
|
+
/revive /start-work --resume # Resume a start-work session instead
|
|
17
|
+
/revive --stop # Stop the watcher
|
|
18
|
+
/revive --status # Check if watcher is running
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Instructions
|
|
22
|
+
|
|
23
|
+
Parse the user's arguments from `$ARGUMENTS`:
|
|
24
|
+
- If empty or no special flags → start the watcher with default command `/coordinate`
|
|
25
|
+
- If `--stop` → stop the running watcher
|
|
26
|
+
- If `--status` → report watcher status
|
|
27
|
+
- Otherwise → use the argument as the resume command
|
|
28
|
+
|
|
29
|
+
### Starting the Watcher
|
|
30
|
+
|
|
31
|
+
1. **Check if a watcher is already running:**
|
|
32
|
+
```bash
|
|
33
|
+
if [ -f ~/.claude/revive-watcher.pid ]; then
|
|
34
|
+
PID=$(cat ~/.claude/revive-watcher.pid)
|
|
35
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
36
|
+
echo "Watcher already running (PID $PID). Stop it first with /revive --stop"
|
|
37
|
+
# Ask user if they want to restart
|
|
38
|
+
fi
|
|
39
|
+
fi
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. **Detect the current workspace and surface:**
|
|
43
|
+
The watcher needs to know which cmux workspace and surface to monitor. Use `cmux list-workspaces` and identify the current one.
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# List workspaces to find the right one
|
|
47
|
+
cmux list-workspaces
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Then identify the Claude surface within that workspace:
|
|
51
|
+
```bash
|
|
52
|
+
# Try reading common surfaces to find the Claude prompt
|
|
53
|
+
for s in surface:5 surface:3 surface:1; do
|
|
54
|
+
SCREEN=$(cmux read-screen --workspace <workspace_id> --surface "$s" --lines 3 2>/dev/null)
|
|
55
|
+
if echo "$SCREEN" | grep -qE "claude|bypass|❯"; then
|
|
56
|
+
echo "Found Claude on $s"
|
|
57
|
+
break
|
|
58
|
+
fi
|
|
59
|
+
done
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
3. **Start the watcher in the background:**
|
|
63
|
+
```bash
|
|
64
|
+
RESUME_CMD="${ARGUMENTS:-/coordinate}"
|
|
65
|
+
# Strip --stop/--status flags if present
|
|
66
|
+
if [[ "$RESUME_CMD" == "--stop" ]] || [[ "$RESUME_CMD" == "--status" ]]; then
|
|
67
|
+
# Handle these cases separately (see below)
|
|
68
|
+
:
|
|
69
|
+
else
|
|
70
|
+
nohup ~/.claude/scripts/revive-watcher.sh \
|
|
71
|
+
--workspace "$WORKSPACE" \
|
|
72
|
+
--surface "$SURFACE" \
|
|
73
|
+
--command "$RESUME_CMD" \
|
|
74
|
+
> ~/.claude/revive-watcher.log 2>&1 &
|
|
75
|
+
|
|
76
|
+
echo "Revive watcher started (PID $!)"
|
|
77
|
+
echo " Monitoring: $WORKSPACE $SURFACE"
|
|
78
|
+
echo " Resume command: $RESUME_CMD"
|
|
79
|
+
echo " Log: ~/.claude/revive-watcher.log"
|
|
80
|
+
echo ""
|
|
81
|
+
echo "When you /clear, the watcher will detect the idle prompt and"
|
|
82
|
+
echo "automatically send '$RESUME_CMD' to restart the session."
|
|
83
|
+
echo ""
|
|
84
|
+
echo "To stop: /revive --stop"
|
|
85
|
+
fi
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Stopping the Watcher
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
if [ -f ~/.claude/revive-watcher.pid ]; then
|
|
92
|
+
PID=$(cat ~/.claude/revive-watcher.pid)
|
|
93
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
94
|
+
kill "$PID"
|
|
95
|
+
echo "Watcher stopped (PID $PID)."
|
|
96
|
+
else
|
|
97
|
+
echo "Watcher was not running (stale PID file)."
|
|
98
|
+
rm -f ~/.claude/revive-watcher.pid
|
|
99
|
+
fi
|
|
100
|
+
else
|
|
101
|
+
echo "No watcher running."
|
|
102
|
+
fi
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Checking Status
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
if [ -f ~/.claude/revive-watcher.pid ]; then
|
|
109
|
+
PID=$(cat ~/.claude/revive-watcher.pid)
|
|
110
|
+
if kill -0 "$PID" 2>/dev/null; then
|
|
111
|
+
echo "Watcher is running (PID $PID)"
|
|
112
|
+
echo "Last 5 log lines:"
|
|
113
|
+
tail -5 ~/.claude/revive-watcher.log 2>/dev/null || echo "(no log yet)"
|
|
114
|
+
else
|
|
115
|
+
echo "Watcher is NOT running (stale PID file)."
|
|
116
|
+
rm -f ~/.claude/revive-watcher.pid
|
|
117
|
+
fi
|
|
118
|
+
else
|
|
119
|
+
echo "No watcher configured."
|
|
120
|
+
fi
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## How It Works
|
|
124
|
+
|
|
125
|
+
The watcher polls the workspace screen every 10 seconds. It detects idle state by checking:
|
|
126
|
+
- The prompt marker (`❯`) is visible
|
|
127
|
+
- "esc to interrupt" is NOT visible (meaning Claude isn't actively working)
|
|
128
|
+
- "bypass permissions" or "What can I help" text appears (indicating a fresh prompt)
|
|
129
|
+
|
|
130
|
+
When idle is detected, it sends the resume command and enters a 60-second cooldown to avoid double-sending.
|
|
131
|
+
|
|
132
|
+
## Configuration
|
|
133
|
+
|
|
134
|
+
The watcher script (`~/.claude/scripts/revive-watcher.sh`) accepts these options:
|
|
135
|
+
- `--workspace <id>` — Which cmux workspace to monitor
|
|
136
|
+
- `--surface <id>` — Which surface within the workspace (default: auto-detect)
|
|
137
|
+
- `--command <cmd>` — What to send on resume (default: `/coordinate`)
|
|
138
|
+
- `--interval <secs>` — Poll frequency (default: 10)
|
|
139
|
+
- `--once` — Send once and exit (for one-shot revival)
|
|
140
|
+
- `--quiet` — Suppress log output
|
|
141
|
+
|
|
142
|
+
## Implementation
|
|
143
|
+
|
|
144
|
+
Execute the above steps based on the parsed arguments. Present the result to the user clearly.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: |
|
|
3
|
+
Send a message to the coordinator workspace from a separate terminal. Sets up a simple input loop that routes your messages to the active coordinator.
|
|
4
|
+
allowed-tools: Bash
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# /speak — Talk to the Coordinator
|
|
8
|
+
|
|
9
|
+
Sets up a message relay from this terminal to the coordinator workspace. Your messages get sent directly to the coordinator's Claude session.
|
|
10
|
+
|
|
11
|
+
## On Invoke
|
|
12
|
+
|
|
13
|
+
1. Find the coordinator target (saved by /coordinate on startup):
|
|
14
|
+
```bash
|
|
15
|
+
cat ~/.claude/coordinator-target 2>/dev/null
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
2. If no target file exists, discover it:
|
|
19
|
+
```bash
|
|
20
|
+
# List workspaces and find the one named "Main Conversation" or the coordinator
|
|
21
|
+
cmux list-workspaces
|
|
22
|
+
# Ask the user which workspace is the coordinator
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
3. Start the relay loop:
|
|
26
|
+
```bash
|
|
27
|
+
echo "Connected to coordinator. Type messages below. Ctrl+C to exit."
|
|
28
|
+
echo "---"
|
|
29
|
+
while IFS= read -r -p "> " msg; do
|
|
30
|
+
if [ -z "$msg" ]; then continue; fi
|
|
31
|
+
target=$(cat ~/.claude/coordinator-target 2>/dev/null)
|
|
32
|
+
if [ -z "$target" ]; then
|
|
33
|
+
echo "No coordinator target found. Run /coordinate first."
|
|
34
|
+
continue
|
|
35
|
+
fi
|
|
36
|
+
tw=${target% *}
|
|
37
|
+
ts=${target#* }
|
|
38
|
+
cmux send --workspace $tw --surface $ts "$msg" 2>/dev/null
|
|
39
|
+
cmux send-key --workspace $tw --surface $ts Enter 2>/dev/null
|
|
40
|
+
echo " → sent"
|
|
41
|
+
done
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## If coordinator-target doesn't exist
|
|
45
|
+
|
|
46
|
+
Guide the user:
|
|
47
|
+
1. The coordinator needs to run `/coordinate` first, which writes `~/.claude/coordinator-target`
|
|
48
|
+
2. Or manually set it: `echo "workspace:1 surface:5" > ~/.claude/coordinator-target`
|
|
49
|
+
3. Find the right values with: `cmux list-workspaces` then `cmux list-pane-surfaces --workspace <id>`
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Revive Watcher — monitors a cmux workspace and auto-sends a resume command after /clear
|
|
3
|
+
#
|
|
4
|
+
# Usage: revive-watcher.sh [options]
|
|
5
|
+
# --workspace <id> Workspace to monitor (default: auto-detect current)
|
|
6
|
+
# --surface <id> Surface to monitor (default: auto-detect Claude surface)
|
|
7
|
+
# --command <cmd> Command to send on resume (default: /coordinate)
|
|
8
|
+
# --interval <secs> Poll interval in seconds (default: 10)
|
|
9
|
+
# --once Send the resume command once, then exit
|
|
10
|
+
# --quiet Suppress output except errors
|
|
11
|
+
#
|
|
12
|
+
# The watcher detects when Claude Code is idle (fresh prompt after /clear) and
|
|
13
|
+
# sends the configured resume command to restart the session.
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
# Defaults
|
|
18
|
+
WORKSPACE=""
|
|
19
|
+
SURFACE=""
|
|
20
|
+
RESUME_CMD="/coordinate"
|
|
21
|
+
INTERVAL=10
|
|
22
|
+
ONCE=false
|
|
23
|
+
QUIET=false
|
|
24
|
+
PID_FILE="${HOME}/.claude/revive-watcher.pid"
|
|
25
|
+
|
|
26
|
+
log() {
|
|
27
|
+
if [ "$QUIET" = false ]; then
|
|
28
|
+
echo "$(date '+%H:%M:%S') [revive] $*"
|
|
29
|
+
fi
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
err() {
|
|
33
|
+
echo "$(date '+%H:%M:%S') [revive] ERROR: $*" >&2
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Parse args
|
|
37
|
+
while [[ $# -gt 0 ]]; do
|
|
38
|
+
case "$1" in
|
|
39
|
+
--workspace) WORKSPACE="$2"; shift 2 ;;
|
|
40
|
+
--surface) SURFACE="$2"; shift 2 ;;
|
|
41
|
+
--command) RESUME_CMD="$2"; shift 2 ;;
|
|
42
|
+
--interval) INTERVAL="$2"; shift 2 ;;
|
|
43
|
+
--once) ONCE=true; shift ;;
|
|
44
|
+
--quiet) QUIET=true; shift ;;
|
|
45
|
+
--help|-h)
|
|
46
|
+
head -14 "$0" | tail -12
|
|
47
|
+
exit 0
|
|
48
|
+
;;
|
|
49
|
+
*) err "Unknown option: $1"; exit 1 ;;
|
|
50
|
+
esac
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
# Auto-detect workspace if not specified
|
|
54
|
+
if [ -z "$WORKSPACE" ]; then
|
|
55
|
+
# Try to find the current workspace from cmux
|
|
56
|
+
CURRENT=$(cmux list-workspaces 2>/dev/null | grep -o 'workspace:[0-9]*' | head -1 || true)
|
|
57
|
+
if [ -z "$CURRENT" ]; then
|
|
58
|
+
err "Could not auto-detect workspace. Use --workspace <id>"
|
|
59
|
+
exit 1
|
|
60
|
+
fi
|
|
61
|
+
WORKSPACE="$CURRENT"
|
|
62
|
+
log "Auto-detected workspace: $WORKSPACE"
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Auto-detect Claude surface if not specified
|
|
66
|
+
if [ -z "$SURFACE" ]; then
|
|
67
|
+
# Read all surfaces and find the one running Claude
|
|
68
|
+
SURFACES=$(cmux list-workspaces 2>/dev/null || true)
|
|
69
|
+
# Try common Claude surface patterns
|
|
70
|
+
for s in "surface:5" "surface:3" "surface:1"; do
|
|
71
|
+
SCREEN=$(cmux read-screen --workspace "$WORKSPACE" --surface "$s" --lines 5 2>/dev/null || true)
|
|
72
|
+
if echo "$SCREEN" | grep -qi "claude\|bypass\|permission\|❯"; then
|
|
73
|
+
SURFACE="$s"
|
|
74
|
+
break
|
|
75
|
+
fi
|
|
76
|
+
done
|
|
77
|
+
if [ -z "$SURFACE" ]; then
|
|
78
|
+
# Default to surface:5 (most common for Claude in cmux)
|
|
79
|
+
SURFACE="surface:5"
|
|
80
|
+
log "Could not auto-detect surface, defaulting to $SURFACE"
|
|
81
|
+
else
|
|
82
|
+
log "Auto-detected Claude surface: $SURFACE"
|
|
83
|
+
fi
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# Write PID file for management
|
|
87
|
+
echo $$ > "$PID_FILE"
|
|
88
|
+
trap 'rm -f "$PID_FILE"; log "Watcher stopped."; exit 0' EXIT INT TERM
|
|
89
|
+
|
|
90
|
+
log "Watcher started"
|
|
91
|
+
log " Workspace: $WORKSPACE"
|
|
92
|
+
log " Surface: $SURFACE"
|
|
93
|
+
log " Command: $RESUME_CMD"
|
|
94
|
+
log " Interval: ${INTERVAL}s"
|
|
95
|
+
log " PID: $$"
|
|
96
|
+
|
|
97
|
+
# Track state to avoid spamming
|
|
98
|
+
LAST_SEND_TIME=0
|
|
99
|
+
COOLDOWN=60 # seconds between sends
|
|
100
|
+
|
|
101
|
+
is_idle() {
|
|
102
|
+
local screen
|
|
103
|
+
screen=$(cmux read-screen --workspace "$WORKSPACE" --surface "$SURFACE" --lines 5 2>/dev/null || true)
|
|
104
|
+
|
|
105
|
+
if [ -z "$screen" ]; then
|
|
106
|
+
return 1 # Can't read screen — not idle
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
# Idle = has the prompt marker but NO "esc to interrupt" (which means Claude is working)
|
|
110
|
+
if echo "$screen" | grep -q "esc to interrupt"; then
|
|
111
|
+
return 1 # Claude is actively working
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# Check for idle prompt indicators:
|
|
115
|
+
# - Empty prompt (❯ with nothing after)
|
|
116
|
+
# - "bypass permissions" text (settings line visible = idle)
|
|
117
|
+
# - Just the prompt line with no active output
|
|
118
|
+
if echo "$screen" | grep -qE "(^❯\s*$|bypass permissions|What can I help)"; then
|
|
119
|
+
return 0 # Idle
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
return 1 # Not clearly idle
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
while true; do
|
|
126
|
+
sleep "$INTERVAL"
|
|
127
|
+
|
|
128
|
+
if is_idle; then
|
|
129
|
+
NOW=$(date +%s)
|
|
130
|
+
ELAPSED=$((NOW - LAST_SEND_TIME))
|
|
131
|
+
|
|
132
|
+
if [ "$ELAPSED" -ge "$COOLDOWN" ]; then
|
|
133
|
+
log "Workspace idle — sending resume command: $RESUME_CMD"
|
|
134
|
+
cmux send --workspace "$WORKSPACE" --surface "$SURFACE" "$RESUME_CMD" 2>/dev/null || {
|
|
135
|
+
err "Failed to send to $WORKSPACE $SURFACE"
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
sleep 1
|
|
139
|
+
cmux send-key --workspace "$WORKSPACE" --surface "$SURFACE" Enter 2>/dev/null || true
|
|
140
|
+
LAST_SEND_TIME=$NOW
|
|
141
|
+
log "Resume command sent. Cooling down ${COOLDOWN}s."
|
|
142
|
+
|
|
143
|
+
if [ "$ONCE" = true ]; then
|
|
144
|
+
log "One-shot mode — exiting."
|
|
145
|
+
exit 0
|
|
146
|
+
fi
|
|
147
|
+
fi
|
|
148
|
+
fi
|
|
149
|
+
done
|