The Best App for Claude Code Control Plane for AI Agent Teams Open Source — Apache 2.0 By Mattia Calastri / Astra Digital
Claude Code is designed for 1 developer, 1 project, 1 terminal. Power users running 8-15 concurrent sessions across multiple projects face:
- No team structure: flat list of sessions with no logical grouping
- No orchestration: can't spawn, command, or coordinate sessions from a central point
- No inter-session awareness: each session is an island
- Resource waste: N sessions x M MCP servers = N*M duplicated processes (57 observed for 8 sessions)
- Cognitive overload: tab-switching across 15 terminals is unsustainable
InkPulse v3 evolves from a passive heartbeat monitor to an active control plane for multi-agent Claude Code teams.
+----------------------------------------------------------+
| InkPulse.app (Swift) |
| |
| +------------------+ +--------------------+ |
| | UI Layer | | WebSocket Server | |
| | (SwiftUI) | | (NIO, :9999) | |
| | | | | |
| | - Team View | | - Session registry| |
| | - Org Chart | | - Command dispatch| |
| | - Spawn Panel | | - Status receive | |
| | - Notifications | +--------------------+ |
| +------------------+ |
| |
| +------------------+ +--------------------+ |
| | JSONL Monitor | | MCP Proxy | |
| | (existing) | | (NIO + stdio) | |
| | | | | |
| | - File tailer | | - Shared pool | |
| | - Metrics | | - Tool routing | |
| | - Health/EGI | | - ~10 processes | |
| +------------------+ +--------------------+ |
+----------------------------------------------------------+
| |
v v
Claude Code Sessions MCP Servers (shared)
(Terminal.app windows) (fal, telegram, github, ...)
Single binary. Everything inside InkPulse.app.
From flat agent list to living org chart.
Each pillar is a team. Each team has up to 3 roles: PM + Dev + 1 custom.
Bot Team [~/btc_predictions]
PM — roadmap, fix priority
Dev — code, deploy, debug
Researcher — crazy ideas, R&D
AuraHome Team [~/projects/aurahome]
PM — product roadmap
Dev — WP/WooCommerce deploy
Content — SEO, copy, i18n
Astra Team [~/Downloads/Astra Digital Marketing]
PM — MRR, pipeline, deadlines
ClientOps — email, invoices, reminders
Researcher — AI Souls, positioning
Brand OS Team [~/claude_voice]
PM — infra roadmap
Dev — Railway, MCP, hooks
Content — TG, LinkedIn, social
Config: ~/.inkpulse/teams.json
{
"teams": [
{
"id": "bot",
"name": "BTC Bot",
"cwd": "~/btc_predictions",
"color": "#FF6B35",
"roles": [
{
"id": "pm",
"name": "PM",
"prompt": "You are the Project Manager for the BTC Predictor Bot. Your job is to read the current roadmap, prioritize fixes and features, and coordinate with the Dev agent. Start by reading session_current.md and CLAUDE.md.",
"icon": "chart.bar.fill"
},
{
"id": "dev",
"name": "Dev",
"prompt": "You are the Lead Developer for the BTC Predictor Bot. Your job is to implement fixes, features, and deploy. Start by reading CLAUDE.md and checking git status.",
"icon": "hammer.fill"
},
{
"id": "researcher",
"name": "Researcher",
"prompt": "You are the R&D Researcher for the BTC Predictor Bot. Explore crazy ideas, test hypotheses, research new approaches. Read the crazy ideas garden and propose experiments.",
"icon": "magnifyingglass"
}
]
}
]
}UI behavior:
- PopoverView shows teams as collapsible sections (replacing flat "ACTIVE AGENTS" list)
- Each role slot shows: occupied (session active) or vacant (grey, "not running")
- Team-level aggregate stats: total cost, combined health, active count
- Clicking a role: Open terminal (existing cwd-match + miniaturize behavior)
- Full window (LiveTab) shows expanded org chart with detail panels per team
"Spawn Team" button per team.
Click: InkPulse opens N Terminal.app windows (one per role), each running:
cd <team.cwd> && claude --prompt "<role.prompt>"If --prompt is not supported by claude CLI, fallback:
- Open terminal, cd to cwd
- Run
claude - Inject prompt via AppleScript keystroke after 2s delay
Spawn behavior:
- Each role gets its own Terminal.app window (separate windows, not tabs)
- Window title set via AppleScript: "InkPulse — Bot/PM"
- InkPulse tracks which sessions it spawned (by PID or TTY)
- "Spawn" button greys out for roles already running
- Individual role spawn: click vacant role slot to spawn just that one
InkPulse runs a WebSocket server on localhost:9999.
A Claude Code hook (SessionStart) auto-connects each session:
# ~/.inkpulse/hooks/ws_client.sh
# Launched by Claude Code SessionStart hook
# Maintains WebSocket connection for bidirectional controlProtocol (JSON over WebSocket):
// Session -> InkPulse (status update)
{
"type": "status",
"session_id": "abc123",
"cwd": "/Users/.../btc_predictions",
"state": "working",
"current_tool": "Edit",
"current_target": "main.py",
"task": "Implementing phase engine fix"
}
// InkPulse -> Session (command)
{
"type": "command",
"action": "task",
"prompt": "Prioritize the council rebalance fix over the cache optimization"
}
// InkPulse -> Session (notification from another agent)
{
"type": "notify",
"from_team": "bot",
"from_role": "pm",
"message": "Deploy approved, proceed with railway push"
}Single proxy inside InkPulse that serves all MCP tools to all sessions.
Before: Session1 -> [fal, tg, github, linkedin, x, bridge, ...] (7 procs)
Session2 -> [fal, tg, github, linkedin, x, bridge, ...] (7 procs)
= N * 7 processes
After: Session1 -> InkPulse MCP Proxy -> [fal, tg, github, ...] (shared)
Session2 -> InkPulse MCP Proxy -> [same instances]
= 1 proxy + 7 server processes total
Implementation:
- InkPulse reads
~/.claude.jsonto know which MCP servers are configured - Launches each server once, maintains stdio pipes via NIO
- Exposes a local MCP-compatible endpoint (stdio proxy or HTTP+SSE)
- Sessions point their MCP config to the proxy instead of individual servers
- Proxy handles request routing and response multiplexing
Session config change (~/.claude.json):
{
"mcpServers": {
"inkpulse-proxy": {
"type": "stdio",
"command": "~/.inkpulse/mcp-proxy-client",
"args": ["--port", "9997"]
}
}
}Significant events detected from JSONL patterns:
- Deploy completed (git push, railway deploy)
- Email sent (gmail_create_draft)
- Build passed/failed (swift build, npm test)
- Task completed (TaskUpdate status=completed)
- Error spike (error rate > 10%)
Native macOS notification via UNUserNotificationCenter. Click opens the terminal for that session.
+-------------------------------------------------------+
| InkPulse 68 HEALTH [gear] |
| 4 teams - 8 agents - 2h uptime |
+-------------------------------------------------------+
| 1266 tok/min | 3514 peak | 98% cache | E2.40 cost |
+-------------------------------------------------------+
| ECG ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ |
+-------------------------------------------------------+
| |
| v Bot Team [Spawn] |
| +-------------+ +-------------+ +-------------+ |
| | * PM | | * Dev | | o Researcher | |
| | Working | | Forging | | (vacant) | |
| | roadmap.md | | main.py | | | |
| | 45 E0.12 | | 89 E0.80 | | [Spawn] | |
| | [Open] | | [Open] | | | |
| +-------------+ +-------------+ +-------------+ |
| |
| > AuraHome Team [Spawn] |
| > Astra Team [2 active][Spawn] |
| > Brand OS Team [1 active][Spawn] |
| |
+-------------------------------------------------------+
| [Report] [Pause] [Config] |
+-------------------------------------------------------+
struct TeamConfig: Codable, Identifiable {
let id: String
let name: String
let cwd: String
let color: String
let roles: [RoleConfig]
}
struct RoleConfig: Codable, Identifiable {
let id: String
let name: String
let prompt: String
let icon: String
}
struct TeamState: Identifiable {
let id: String // matches TeamConfig.id
let config: TeamConfig
var slots: [RoleSlot]
}
struct RoleSlot: Identifiable {
let id: String // matches RoleConfig.id
let role: RoleConfig
var session: MetricsSnapshot? // nil = vacant
var sessionId: String?
}Sources/
Config/
TeamConfig.swift (NEW — team/role data model + Codable)
TeamsLoader.swift (NEW — reads ~/.inkpulse/teams.json)
UI/
PopoverView.swift (MODIFIED — team sections replace flat list)
TeamSectionView.swift (NEW — collapsible team with role cards)
RoleCardView.swift (NEW — role slot card, vacant or occupied)
SpawnButton.swift (NEW — spawn team or individual role)
LiveTab.swift (MODIFIED — org chart layout)
Actions/
TerminalOpener.swift (MODIFIED — window title setting)
TeamSpawner.swift (NEW — spawn N Terminal windows for team)
WebSocket/
WSServer.swift (NEW — NIO WebSocket server :9999)
SessionRegistry.swift (NEW — connected sessions, role mapping)
WSProtocol.swift (NEW — message types JSON codable)
MCP/
MCPProxy.swift (NEW — shared MCP server pool manager)
MCPServerManager.swift (NEW — launch/maintain stdio server procs)
MCPRouter.swift (NEW — route tool calls session -> server)
Notifications/
NotificationManager.swift (NEW — UNUserNotificationCenter)
EventDetector.swift (NEW — detect significant events from JSONL)
~/.inkpulse/
teams.json (team configuration file)
hooks/
ws_client.sh (Claude Code SessionStart hook script)
- TeamConfig + TeamsLoader — read teams.json
- TeamSectionView + RoleCardView — replace flat agent list
- Match existing sessions to teams/roles by cwd
- Collapsible sections in PopoverView
- Team-level aggregate stats
- TeamSpawner — AppleScript to open Terminal windows with claude
- Spawn button per team and per vacant role
- Track spawned sessions by PID/TTY
- Window title setting via AppleScript
- WSServer with SwiftNIO WebSocket
- SessionRegistry — connect/disconnect/role mapping
- WSProtocol — typed messages
- Claude Code hook for auto-connect
- Command dispatch UI (send task to specific agent)
- MCPServerManager — read .claude.json, launch servers once
- MCPProxy — stdio pipe management with NIO
- MCPRouter — multiplex tool calls from N sessions to shared servers
- Proxy client script for session config
- Migration guide for .claude.json
- NotificationManager — UNUserNotificationCenter
- EventDetector — pattern matching on JSONL
- Click-to-focus from notification
- UI animations, team health aggregation
- Multi-terminal support (iTerm2, Warp, Ghostty) — Terminal.app only
- Persistent state across restarts — fresh scan every launch
- More than 3 roles per team — fixed at PM + Dev + 1 custom
- Auto-generated role prompts — user defines in teams.json
- Remote/cloud orchestration — local only
- Windows/Linux — macOS only (SwiftUI + AppleScript)
- Repo: github.com/astra-digital/inkpulse
- License: Apache 2.0
- README: screenshots, GIF demo, quick start
- Config: teams.json generic, no hardcoded pillars
- Example: ships with example teams.json for typical multi-project setup
- Business: OSS free -> AstraAI enterprise custom
- 4 teams visible in popover with collapsible sections
- One-click spawn opens 3 Terminal windows with claude running
- WebSocket connects sessions to InkPulse bidirectionally
- MCP proxy reduces process count from N*M to M+1
- macOS notification on deploy/error/task completion
- README + Apache 2.0 + published on GitHub
- Power user can manage 12+ agents across 4+ teams without cognitive overload
Spec: sess.524, 26 Mar 2026 Mattia Calastri + Claude — Astra Digital