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 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.1.0"
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 docker >/dev/null 2>&1 || { echo "Error: docker is required. Install Docker Desktop first."; exit 1; }
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/anthropics/tanuki-telemetry.git "$TANUKI_DIR" 2>/dev/null || {
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 directory
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
- # Build Docker images
41
- echo "[2/4] Building Docker images..."
42
- docker build -t telemetry-mcp:latest -t telemetry-dashboard:latest . -q
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
- # Stop existing dashboard if running
47
+ # Start dashboard (native Node, no Docker)
45
48
  echo "[3/4] Starting dashboard..."
46
- docker rm -f telemetry-dashboard 2>/dev/null || true
47
- docker run -d --rm \
48
- --name telemetry-dashboard \
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
- # Configure Claude Code MCP
54
- echo "[4/4] Configuring Claude Code..."
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
- echo " MCP already configured in .claude.json"
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
- echo " Add this to your .claude.json mcpServers:"
62
- echo ""
63
- echo ' "telemetry": {'
64
- echo ' "type": "stdio",'
65
- echo ' "command": "docker",'
66
- echo ' "args": ["run", "--rm", "-i", "-v", "'$DATA_DIR':/data", "--entrypoint", "node", "telemetry-mcp:latest", "dist/index.js"]'
67
- echo ' }'
68
- echo ""
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 " No .claude.json found — create one with:"
102
+ echo " Add this to your .claude.json:"
72
103
  echo ""
73
- echo ' { "mcpServers": { "telemetry": { "type": "stdio", "command": "docker", "args": ["run", "--rm", "-i", "-v", "'$DATA_DIR':/data", "--entrypoint", "node", "telemetry-mcp:latest", "dist/index.js"] } } }'
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
- # Wait for dashboard to start
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tanuki-telemetry",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Workflow monitor and telemetry dashboard for Claude Code autonomous agents",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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