Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Unreleased

### Added
- Configurable terminal multiplexer support: `tmux` remains the default, with
Zellij available via `[orchestrator].multiplexer_type = "zellij"`.
- Zellij-backed launch, attach, prompt send, pane capture, health/workflow
waits, and shutdown paths for agent sessions.
- Integration coverage for Zellij `tt send --auto-up`, `tt peek`, and `tt down`.

Comment thread
sysadmin-metrum-ai marked this conversation as resolved.
Resolved
## [0.10.1] - 2026-05-05

### Added
Expand Down
25 changes: 21 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ tt init --template rust-cli # 3-agent Rust project team
tt init --template minimal # 2-agent starter
```

**Prerequisites:** Rust toolchain, tmux, and at least one AI coding CLI installed for agent sessions (Claude Code, Codex, or Aider). Command-only workflows such as the Rust template's `verify` can run before you launch agents. For non-Rust repos, start with `tt init` or `tt init --template minimal`, then add the smallest verification command your project already trusts.
**Prerequisites:** Rust toolchain, tmux or Zellij, and at least one AI coding CLI installed for agent sessions (Claude Code, Codex, or Aider). Command-only workflows such as the Rust template's `verify` can run before you launch agents. For non-Rust repos, start with `tt init` or `tt init --template minimal`, then add the smallest verification command your project already trusts.

## The Agent Ops Loop

Expand All @@ -55,7 +55,7 @@ The built-in Tutti-for-Tutti workflow uses GitHub issue intake and CodeRabbit re
## What Tutti Does

- **Defines agent operations as code** in `tutti.toml`: roles, runtimes, workflows, hooks, gates, and policies
- **Spawns and manages** multiple AI coding agent sessions (Claude Code, Codex, Aider) in tmux
- **Spawns and manages** multiple AI coding agent sessions (Claude Code, Codex, Aider) in tmux or Zellij
- **Isolates each agent** in its own git worktree to prevent merge conflicts
- **Orchestrates workflows** — chain prompt steps, shell commands, and agent coordination into repeatable pipelines defined in `tutti.toml`
- **Web dashboard** at `:4040` — factory-floor view of all agents with real-time SSE updates, state-driven visuals (working/idle/stopped/blocked), and workflow run tracking
Expand Down Expand Up @@ -447,10 +447,22 @@ Reusable prompt components and skills are **phrases**. A phrase might be a CLAUD

### Tool Packs (Built, Opt-in)
- Declarative `[[tool_pack]]` blocks in `tutti.toml` (`required_commands`, `required_env`)
- `tt doctor` reports pass/warn/fail for tmux, profile wiring, runtime binaries, and tool-pack prerequisites
- `tt doctor` reports pass/warn/fail for the configured multiplexer, profile wiring, runtime binaries, and tool-pack prerequisites
- `tt doctor` also probes running agents for auth health (`auth/<agent>` checks)
- `tt doctor` validates serve readiness (`serve/state_dir`, `serve/events_log`, `serve/scheduler`, `serve/port`)
- CI smoke profile (`.github/workflows/ci.yml`) runs headless `tt doctor --strict` + `tt run smoke-check --strict`
- GitHub CI smoke profile (`.github/workflows/ci.yml`) runs headless `tt doctor --strict` + `tt run smoke-check --strict`

### Terminal Multiplexers
Tutti defaults to tmux for backwards compatibility. Set `[orchestrator].multiplexer_type = "zellij"` to launch, attach, send prompts to, and capture output from Zellij sessions instead.

```toml
[orchestrator]
multiplexer_type = "zellij"

[multiplexer.zellij]
layout_path = "./.tutti/layouts/agent_default.kdl"
theme = "dracula"
```

### Community (Planned)
- Share and discover arrangements (team configs)
Expand Down Expand Up @@ -542,6 +554,11 @@ Tutti is early. If this resonates with how you work, we want to hear from you.

MIT

## Notes

The Zellij multiplexer path was refined from downstream operator use in the
Metrum AI fork.

---

*In music, tutti means "all together" — the moment every voice in the ensemble plays as one. That's what your agents should feel like.*
4 changes: 4 additions & 0 deletions src/automation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5021,6 +5021,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![AgentConfig {
name: "backend".to_string(),
Expand Down Expand Up @@ -6239,6 +6241,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![
AgentConfig {
Expand Down
2 changes: 2 additions & 0 deletions src/budget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![AgentConfig {
name: "backend".to_string(),
Expand Down
24 changes: 17 additions & 7 deletions src/cli/doctor.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use crate::config::{GlobalConfig, LaunchMode, LaunchPolicyMode, ToolPackConfig, TuttiConfig};
use crate::config::{
GlobalConfig, LaunchMode, LaunchPolicyMode, MultiplexerType, ToolPackConfig, TuttiConfig,
};
use crate::error::{Result, TuttiError};
use crate::health;
use crate::permissions::has_configured_policy;
Expand Down Expand Up @@ -89,17 +91,21 @@ fn evaluate_checks(
let mut checks = Vec::new();
let launch_settings = resolve_launch_settings(config);

checks.push(if command_exists("tmux") {
let multiplexer_command = match config.orchestrator.multiplexer_type {
MultiplexerType::Tmux => "tmux",
MultiplexerType::Zellij => "zellij",
};
checks.push(if command_exists(multiplexer_command) {
DoctorCheck {
check: "tmux".to_string(),
check: "multiplexer".to_string(),
status: DoctorStatus::Pass,
detail: "tmux is available".to_string(),
detail: format!("{multiplexer_command} is available"),
}
} else {
DoctorCheck {
check: "tmux".to_string(),
check: "multiplexer".to_string(),
status: DoctorStatus::Fail,
detail: "tmux not found on PATH".to_string(),
detail: format!("{multiplexer_command} not found on PATH"),
}
});

Expand Down Expand Up @@ -490,7 +496,9 @@ fn print_report(report: &DoctorReport) {

fn suggestion_for_check(check: &DoctorCheck) -> Option<&'static str> {
match check.check.as_str() {
"tmux" if check.status == DoctorStatus::Fail => Some("Install tmux and re-run doctor"),
"multiplexer" if check.status == DoctorStatus::Fail => {
Some("Install the configured multiplexer and re-run doctor")
}
"auth profile" if check.status == DoctorStatus::Fail => {
Some("Set workspace.auth.default_profile to an existing profile")
}
Expand Down Expand Up @@ -579,6 +587,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![AgentConfig {
name: "backend".to_string(),
Expand Down
2 changes: 2 additions & 0 deletions src/cli/handoff.rs
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![AgentConfig {
name: "backend".to_string(),
Expand Down
20 changes: 20 additions & 0 deletions src/cli/up.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1793,6 +1793,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![
AgentConfig {
Expand Down Expand Up @@ -1941,6 +1943,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![],
tool_packs: vec![],
Expand Down Expand Up @@ -1977,6 +1981,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![],
tool_packs: vec![],
Expand Down Expand Up @@ -2014,6 +2020,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![],
tool_packs: vec![],
Expand Down Expand Up @@ -2066,6 +2074,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![],
tool_packs: vec![],
Expand Down Expand Up @@ -2130,6 +2140,8 @@ mod tests {
worktree: true,
runtime: Some("codex".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![],
tool_packs: vec![],
Expand Down Expand Up @@ -2183,6 +2195,8 @@ mod tests {
worktree: true,
runtime: Some("codex".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![],
tool_packs: vec![],
Expand Down Expand Up @@ -2245,6 +2259,8 @@ mod tests {
worktree: true,
runtime: Some("codex".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![],
tool_packs: vec![],
Expand Down Expand Up @@ -2329,6 +2345,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: Some(LaunchConfig {
mode: LaunchMode::Safe,
policy: LaunchPolicyMode::Bypass,
Expand Down Expand Up @@ -2366,6 +2384,8 @@ mod tests {
worktree: true,
runtime: Some("claude-code".to_string()),
},
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: vec![make_agent("backend", vec![])],
tool_packs: vec![],
Expand Down
2 changes: 2 additions & 0 deletions src/cli/watch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,8 @@ mod tests {
}),
},
defaults: DefaultsConfig::default(),
orchestrator: Default::default(),
multiplexer: Default::default(),
launch: None,
agents: agents
.iter()
Expand Down
12 changes: 12 additions & 0 deletions src/config/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,18 @@ runtime = "claude-code" # default runtime for agents
# - constrained mode requires [permissions] in ~/.config/tutti/config.toml.
# - mode = "unattended" + policy = "bypass" is highest autonomy and highest risk.

# Terminal multiplexer settings (optional)
# [orchestrator]
# multiplexer_type = "tmux" # tmux | zellij
#
# [multiplexer.tmux]
# socket_name = "tutti"
# base_index = 1
#
# [multiplexer.zellij]
# layout_path = "./.tutti/layouts/agent_default.kdl"
# theme = "dracula"

# Budget guardrails (optional, API profiles only)
# [budget]
# mode = "warn" # warn | enforce
Expand Down
Loading