Skip to content
This repository was archived by the owner on May 2, 2026. It is now read-only.

Commit 76f6988

Browse files
RoberdanCopilot
andauthored
feat(mesh): handoff protocol v10.3.0
* docs: dashboard delegation, auto-sync, WoL, tmux integration - README: mesh quick start, delegation workflow, power management, auto-sync - docs/mesh-networking.md: preflight checks, SSE streaming, tmux sessions, peers.conf reference - scripts/mesh/README.md: dashboard delegation section, peers.conf mac_address field Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * docs: add Dashboards section with setup instructions and aliases - Control Room (web) on localhost:8420 - pianits (terminal TUI) with interactive keys - Shell aliases for macOS/Linux/Windows - Auto-start snippet for background server Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat(mesh): handoff protocol v10.3.0 — direction-aware sync, crash recovery, rsync Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent feb2470 commit 76f6988

11 files changed

Lines changed: 2178 additions & 193 deletions

File tree

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
# Changelog
22

3+
## [10.3.0] — 2026-03-03
4+
5+
### Added
6+
7+
- **Handoff Protocol**: Direction-aware sync with delegation lock and crash recovery
8+
- **Rsync-based Config Sync**: Replaces git-based sync (3.6GB→10MB, <5s)
9+
- **Preflight Auto-fix**: SSH resolution, WoL fallback, rsync check, DB sync
10+
11+
### Fixed
12+
13+
- `execution_host` now uses `peer_name` for clean dashboard display
14+
- `mesh-migrate.sh` missing `peers_load()` call
15+
- Rsync exclude updated (skip `projects/`, `data/`, `file-history/`)
16+
317
## [10.2.0] — 2026-03-03
418

519
### Added

README.md

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,58 @@ The coordinator scores peers by cost, load, and privacy constraints, then routes
240240

241241
All peers sync via SSH/Tailscale. Config, repos, credentials, and the plan DB stay aligned across machines with one command: `mesh-sync-all.sh`. Live migration moves a running plan to another peer mid-execution.
242242

243+
### Dashboard Delegation
244+
245+
Delegate plans directly from the Control Room — click the 🚀 icon on any active mission:
246+
247+
1. **Select target node** — see OS, CPU load, active tasks, online status
248+
2. **Auto preflight** — 6 streaming checks run and self-heal:
249+
- SSH reachability, heartbeat (auto-restarts if stale), config sync (auto-syncs if diverged), Claude CLI, disk space
250+
3. **One-click delegate** — full sync (Phase 0) + migration (Phase 1-5) streamed live to a modal
251+
4. **tmux session** — plan runs in `plan-{ID}` on target; terminal icons auto-attach
252+
253+
### Node Power Management
254+
255+
| Button | When | What it does |
256+
|--------|------|-------------|
257+
| ⚡ Wake | Node offline | Sends Wake-on-LAN magic packet (needs `mac_address` in peers.conf) |
258+
| 🔄 Reboot | Node frozen | SSH `sudo reboot` with post-reboot polling |
259+
260+
### Auto-Sync Protocol
261+
262+
No manual sync needed — everything propagates automatically:
263+
264+
| Event | Action |
265+
|-------|--------|
266+
| **Plan completes** | Results pushed to all online peers |
267+
| **Node boots / reconnects** | Heartbeat daemon pulls latest from coordinator |
268+
| **Every ~5 minutes** | Heartbeat loop checks for updates |
269+
| **Before delegation** | Full sync (config + DB + repos) to target |
270+
271+
### Quick Start: Mesh Setup
272+
273+
```bash
274+
# 1. Install MyConvergio on each machine
275+
curl -fsSL https://raw.githubusercontent.com/Roberdan/MyConvergio/master/install.sh | bash
276+
277+
# 2. Configure peers (edit with your real hosts)
278+
cp config/peers.conf.example ~/.claude/config/peers.conf
279+
# Set: ssh_alias, user, os, tailscale_ip, capabilities, role, mac_address
280+
281+
# 3. Bootstrap remote peer
282+
scripts/mesh/bootstrap-peer.sh my-linux
283+
284+
# 4. Push credentials
285+
scripts/mesh/mesh-auth-sync.sh push --peer my-linux
286+
287+
# 5. Start heartbeat daemon (auto-syncs on start)
288+
scripts/mesh/mesh-heartbeat.sh start
289+
290+
# 6. Launch Control Room
291+
python3 scripts/dashboard_web/server.py --port 8420
292+
# Open http://localhost:8420
293+
```
294+
243295
---
244296

245297
## Enforcement layer
@@ -343,6 +395,48 @@ Open your terminal with Claude Code or Copilot CLI and type:
343395

344396
MyConvergio extracts requirements, asks clarifying questions, generates a structured plan with parallel tasks, executes each task in isolation with TDD, validates through Thor's 9 quality gates, and auto-merges to main. You approve the plan — the system does the rest.
345397

398+
### Dashboards
399+
400+
MyConvergio includes two dashboards for monitoring plans, agents, and mesh nodes:
401+
402+
| Dashboard | What | How to run |
403+
|-----------|------|------------|
404+
| **Control Room** (web) | Full browser UI with plan drill-down, mesh topology, integrated terminals, cost analytics | `python3 ~/.claude/scripts/dashboard_web/server.py` then open `http://localhost:8420` |
405+
| **pianits** (terminal) | Lightweight TUI for quick checks inside tmux/SSH sessions — auto-refresh, drill-down, quit with `q` | `~/.claude/scripts/pianits` |
406+
407+
#### Recommended aliases
408+
409+
Add these to your shell profile for quick access:
410+
411+
<details>
412+
<summary><strong>macOS / Linux</strong> (~/.zshrc or ~/.bashrc)</summary>
413+
414+
```bash
415+
# Convergio dashboards
416+
alias piani='open http://localhost:8420' # macOS: opens browser
417+
# alias piani='xdg-open http://localhost:8420' # Linux: opens browser
418+
alias pianits='~/.claude/scripts/pianits'
419+
```
420+
</details>
421+
422+
<details>
423+
<summary><strong>Windows (PowerShell profile)</strong></summary>
424+
425+
```powershell
426+
# Convergio dashboards
427+
function piani { Start-Process "http://localhost:8420" }
428+
Set-Alias pianits "$env:USERPROFILE\.claude\scripts\pianits"
429+
```
430+
</details>
431+
432+
> **pianits** interactive keys: `q` quit · `r` refresh · `<number>` + Enter = drill-down · `b` back · auto-refreshes every 10s.
433+
>
434+
> To run the Control Room server on startup, add to your shell profile:
435+
> ```bash
436+
> # Start Control Room in background (if not already running)
437+
> pgrep -f "dashboard_web/server.py" >/dev/null || python3 ~/.claude/scripts/dashboard_web/server.py &>/dev/null &
438+
> ```
439+
346440
---
347441
348442
## Documentation

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
SYSTEM_VERSION=10.2.0
1+
SYSTEM_VERSION=10.3.0

config/mesh-rsync-exclude.txt

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# mesh-rsync-exclude.txt — rsync exclude patterns for mesh-migrate.sh
2+
# Used by: _migrate_rsync() in lib/mesh-migrate-sync.sh
3+
# Purpose: skip files that should not be transferred or are managed separately
4+
5+
# Git internals — use git pull on target instead of syncing objects
6+
.git/objects/**
7+
.git/lfs/**
8+
.git/refs/**
9+
.git/logs/**
10+
11+
# Node dependencies — run npm ci on target after sync
12+
node_modules/
13+
.npm/
14+
15+
# macOS and Windows cruft
16+
.DS_Store
17+
Thumbs.db
18+
._*
19+
20+
# Python bytecode
21+
__pycache__/
22+
*.pyc
23+
*.pyo
24+
*.pyd
25+
26+
# SQLite WAL files — DB is synced separately via SCP + integrity_check
27+
*.db-wal
28+
*.db-shm
29+
*.db-journal
30+
31+
# Local caches — not portable across machines
32+
.cache/
33+
paste-cache/
34+
image-cache/
35+
debug/
36+
telemetry/
37+
statsig/
38+
stats-cache.json
39+
40+
# Machine-local data — DB synced separately via SCP
41+
data/
42+
projects/
43+
file-history/
44+
plugins/
45+
plans/
46+
todos/
47+
history.jsonl
48+
plan.db
49+
plans.db
50+
plan-db.sqlite
51+
52+
# Test and build artifacts
53+
backups/
54+
test-results/
55+
playwright-report/
56+
.next/
57+
dist/
58+
build/
59+
60+
# Machine-specific shell history snapshots
61+
shell-snapshots/
62+
63+
# Log files
64+
*.log
65+
logs/

docs/mesh-networking.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,4 +240,87 @@ sqlite3 ~/.claude/data/dashboard.db \
240240

241241
---
242242

243+
## Dashboard Delegation
244+
245+
Delegate plans to mesh nodes from the Convergio Control Room web dashboard.
246+
247+
### Workflow
248+
249+
```
250+
Plan Card → 🚀 Delegate → Select Peer → Preflight (auto-fix) → Sync → Migrate → tmux session
251+
```
252+
253+
### Preflight Checks
254+
255+
All checks stream via SSE (Server-Sent Events) — you see each one appear in real-time. Failures are auto-fixed when possible:
256+
257+
| Check | Auto-Fix | Blocking |
258+
|-------|----------|----------|
259+
| Plan status (todo/doing) || Yes |
260+
| SSH reachable via `ssh_alias` || Yes |
261+
| Heartbeat stale | Restarts daemon via SSH | No (auto-fixed) |
262+
| Config out of sync | Runs `mesh-sync-all.sh --peer` | No (auto-fixed) |
263+
| Claude CLI available | Searches `~/.local/bin`, `/opt/homebrew/bin` | Yes |
264+
| Disk space ≥ 5GB || Yes |
265+
266+
### Delegation Process
267+
268+
1. **Phase 0 (auto-sync)**: `mesh-sync-all.sh --peer <target>` — config, DB, repos
269+
2. **Phase 1-5**: `mesh-migrate.sh` — preflight, file sync, DB migration, tmux launch, verify
270+
271+
All output streams live to the browser modal.
272+
273+
### tmux Sessions
274+
275+
Each delegated plan runs in `tmux plan-{ID}` on the target node:
276+
- Dashboard terminal icons auto-attach to the plan's tmux session
277+
- `openAllTerminals()` connects each peer to its active plan session
278+
- Manual: `ssh <peer> -t "tmux attach -t plan-{ID}"`
279+
280+
---
281+
282+
## Power Management
283+
284+
Control node power from the dashboard:
285+
286+
| Action | When to use | How it works |
287+
|--------|-------------|-------------|
288+
| **⚡ Wake** | Node is offline/sleeping | Sends 3x Wake-on-LAN magic packets (pure Python, broadcast UDP:9). Polls SSH for 15s. Requires `mac_address` in `peers.conf`. |
289+
| **🔄 Reboot** | Node is online but frozen | Sends `sudo reboot` via SSH (OS-aware: macOS/Linux/Windows). Polls SSH for 40s to confirm comeback. |
290+
291+
Wake button appears on **offline** nodes. Reboot button on **online** nodes.
292+
293+
---
294+
295+
## Auto-Sync Protocol
296+
297+
Sync happens automatically — no manual intervention needed:
298+
299+
| Trigger | Direction | What syncs |
300+
|---------|-----------|-----------|
301+
| **Plan completes** | Coordinator → all online peers | Config + DB + repo changes |
302+
| **Heartbeat starts** | Peer → coordinator | Pulls latest config (git bundle) |
303+
| **Every ~5 minutes** | Peer → coordinator | Heartbeat loop pulls updates |
304+
| **Before delegation** | Coordinator → target peer | Full sync (Phase 0) |
305+
306+
**Conflict resolution**: Remote dirty files → auto-stash before merge. Diverged git history → force-reset + rsync fallback. GitHub token expired → git bundle over SSH.
307+
308+
---
309+
310+
## peers.conf Reference
311+
312+
```ini
313+
[my-node]
314+
ssh_alias=my-node.tailnet.ts.net # SSH config alias (required)
315+
user=myuser # SSH username (required)
316+
os=macos # macos | linux | windows (required)
317+
tailscale_ip=100.x.x.x # Tailscale IP (optional)
318+
capabilities=claude,copilot,ollama # Comma-separated (optional)
319+
role=worker # coordinator | worker | hybrid (required)
320+
status=active # active | inactive (default: active)
321+
mac_address=AA:BB:CC:DD:EE:FF # For Wake-on-LAN (optional)
322+
```
323+
324+
---
325+
243326
[README](../README.md) | [Getting Started](getting-started.md) | [Infrastructure](infrastructure.md) | [Concepts](concepts.md) | [Workflow](workflow.md)

scripts/dashboard_web/app.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,37 @@
66
const $ = (s) => document.querySelector(s);
77
let tokenChart, modelChart, distChart;
88
let lastMissionData = null;
9+
let _hostToPeer = {}; // hostname → peer_name mapping
10+
11+
// Build hostname→peer_name map from /api/mesh
12+
async function _refreshHostMap() {
13+
try {
14+
const peers = await fetchJson("/api/mesh");
15+
if (Array.isArray(peers)) {
16+
_hostToPeer = {};
17+
const localHost = location.hostname;
18+
peers.forEach((p) => {
19+
// Map dns_name, tailscale_ip, and peer_name itself
20+
_hostToPeer[p.peer_name] = p.peer_name;
21+
if (p.dns_name) _hostToPeer[p.dns_name] = p.peer_name;
22+
if (p.is_local) _hostToPeer["local"] = p.peer_name;
23+
});
24+
}
25+
} catch (_) {}
26+
}
27+
28+
function _resolveHost(host) {
29+
if (!host) return "local";
30+
// Direct match
31+
if (_hostToPeer[host]) return _hostToPeer[host];
32+
// Partial match (hostname contains peer_name or vice versa)
33+
const h = host.toLowerCase();
34+
for (const [key, name] of Object.entries(_hostToPeer)) {
35+
if (h.includes(key.toLowerCase()) || key.toLowerCase().includes(h)) return name;
36+
}
37+
// Shorten long hostnames
38+
return host.length > 20 ? host.substring(0, 16) + "…" : host;
39+
}
940

1041
function fmt(n) {
1142
if (!n && n !== 0) return "—";
@@ -76,6 +107,14 @@ async function refreshAll() {
76107
ov.mesh_total = mesh ? mesh.length : 0;
77108
renderKpi(ov);
78109
}
110+
// Update hostname→peer_name map for plan cards
111+
if (Array.isArray(mesh)) {
112+
_hostToPeer = {};
113+
mesh.forEach((p) => {
114+
_hostToPeer[p.peer_name] = p.peer_name;
115+
if (p.is_local) _hostToPeer["local"] = p.peer_name;
116+
});
117+
}
79118
renderMission(mission);
80119
if (daily) renderTokenChart(daily);
81120
if (models) renderModelChart(models);
@@ -179,7 +218,7 @@ function _renderOnePlan(m) {
179218
<div style="display:flex;gap:12px;font-size:10px;color:var(--text-dim);margin-top:2px">
180219
<span>${inProgCount > 0 ? `<span style="color:var(--gold)">${inProgCount} running</span>` : ""}</span>
181220
<span>${blockedCount > 0 ? `<span style="color:var(--red)">${blockedCount} blocked</span>` : ""}</span>
182-
<span class="host-badge" style="font-size:9px;padding:0 6px">${esc(p.execution_host || "local")}</span>
221+
<span class="host-badge" style="font-size:9px;padding:0 6px">${esc(p.execution_peer || _resolveHost(p.execution_host))}</span>
183222
</div>
184223
</div>
185224
</div>`;
@@ -801,7 +840,7 @@ window.openPlanSidebar = async function (planId) {
801840
let html = `<div class="sb-meta">
802841
<strong>Status:</strong> ${statusDot(p.status)} <span style="color:${sColor}">${p.status.toUpperCase()}</span>
803842
<br><strong>Progress:</strong> ${p.tasks_done}/${p.tasks_total} (${pct}%)
804-
<br><strong>Host:</strong> ${esc(p.execution_host || "local")}
843+
<br><strong>Host:</strong> ${esc(_resolveHost(p.execution_host))}
805844
${p.parallel_mode ? `<br><strong>Mode:</strong> ${esc(p.parallel_mode)}` : ""}
806845
${p.started_at ? `<br><strong>Started:</strong> ${p.started_at}` : ""}
807846
${p.completed_at ? `<br><strong>Completed:</strong> ${p.completed_at}` : ""}
@@ -957,11 +996,7 @@ window.openAllTerminals = function () {
957996
return;
958997
}
959998
online.forEach((p) => {
960-
const activePlan = (p.plans || []).find(
961-
(pl) => pl.status === "doing" || pl.status === "todo",
962-
);
963-
const tmuxSession = activePlan ? `plan-${activePlan.id}` : undefined;
964-
termMgr.open(p.peer_name, p.peer_name, tmuxSession);
999+
termMgr.open(p.peer_name, p.peer_name, "Convergio");
9651000
});
9661001
if (online.length > 1) termMgr.setMode("grid");
9671002
else termMgr.setMode("dock");

0 commit comments

Comments
 (0)