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
4 changes: 2 additions & 2 deletions crates/kild-config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ pub use include_config::{CopyOptions, IncludeConfig, PatternRule, default_includ
pub use keybindings::{Keybindings, NavigationKeybindings, TerminalKeybindings};
pub use loading::{get_agent_command, load_hierarchy, merge_configs};
pub use types::{
AgentConfig, AgentSettings, Config, DaemonRuntimeConfig, EditorConfig, GitConfig, HealthConfig,
KildConfig, TerminalConfig, UiConfig,
AgentConfig, AgentSettings, Config, DaemonRuntimeConfig, EditorConfig, FleetConfig, GitConfig,
HealthConfig, KildConfig, TerminalConfig, UiConfig,
};
pub use validation::{VALID_TERMINALS, validate_config};

Expand Down
4 changes: 3 additions & 1 deletion crates/kild-config/src/loading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
use crate::agent_data;
use crate::include_config::IncludeConfig;
use crate::types::{
AgentConfig, DaemonRuntimeConfig, GitConfig, HealthConfig, KildConfig, TerminalConfig, UiConfig,
AgentConfig, DaemonRuntimeConfig, FleetConfig, GitConfig, HealthConfig, KildConfig,
TerminalConfig, UiConfig,
};
use crate::validation::validate_config;
use std::fs;
Expand Down Expand Up @@ -174,6 +175,7 @@ pub fn merge_configs(base: KildConfig, override_config: KildConfig) -> KildConfi
editor: base.editor.merge(override_config.editor),
daemon: DaemonRuntimeConfig::merge(&base.daemon, &override_config.daemon),
ui: UiConfig::merge(&base.ui, &override_config.ui),
fleet: FleetConfig::merge(&base.fleet, &override_config.fleet),
}
}

Expand Down
28 changes: 28 additions & 0 deletions crates/kild-config/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,10 @@ pub struct KildConfig {
/// UI configuration (keybindings, navigation).
#[serde(default)]
pub ui: UiConfig,

/// Fleet configuration (channels, communication).
#[serde(default)]
pub fleet: FleetConfig,
}

impl Default for KildConfig {
Expand All @@ -122,6 +126,7 @@ impl Default for KildConfig {
editor: <EditorConfig as Default>::default(),
daemon: DaemonRuntimeConfig::default(),
ui: UiConfig::default(),
fleet: FleetConfig::default(),
}
}
}
Expand All @@ -141,6 +146,29 @@ impl UiConfig {
}
}

/// Fleet configuration for inter-agent communication.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct FleetConfig {
/// Enable MCP channel server for near-real-time fleet communication.
/// Requires Bun runtime. Default: false (research preview).
pub channels: Option<bool>,
}

impl FleetConfig {
/// Whether fleet channels are enabled (default: false).
pub fn channels(&self) -> bool {
self.channels.unwrap_or(false)
}

/// Merge two fleet configs. Override takes precedence for set fields.
pub fn merge(base: &Self, override_config: &Self) -> Self {
Self {
channels: override_config.channels.or(base.channels),
}
}
}

/// Daemon runtime configuration.
///
/// Controls whether the daemon is the default runtime for new sessions
Expand Down
4 changes: 2 additions & 2 deletions crates/kild-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ pub use git::types::{
};
pub use kild_config::ConfigError;
pub use kild_config::{
AgentConfig, AgentSettings, Config, DaemonRuntimeConfig, EditorConfig, GitConfig, HealthConfig,
Keybindings, KildConfig, TerminalConfig, UiConfig, VALID_TERMINALS,
AgentConfig, AgentSettings, Config, DaemonRuntimeConfig, EditorConfig, FleetConfig, GitConfig,
HealthConfig, Keybindings, KildConfig, TerminalConfig, UiConfig, VALID_TERMINALS,
};
pub use kild_config::{CopyOptions, IncludeConfig, PatternRule};
pub use projects::{Project, ProjectError, ProjectRegistry, ProjectsData};
Expand Down
3 changes: 2 additions & 1 deletion crates/kild-core/src/sessions/daemon_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ pub use super::attach::spawn_and_save_attach_window;
// Shim setup
pub(crate) use super::shim_setup::ensure_shim_binary;

// Agent integrations — public ensure functions (used by CLI init-hooks)
// Agent integrations — public ensure functions (used by CLI init-hooks/init-channels)
pub use super::integrations::channel::ensure_channel_server_installed;
pub use super::integrations::{
ensure_claude_settings, ensure_claude_status_hook, ensure_opencode_config,
ensure_opencode_package_json, ensure_opencode_plugin_in_worktree,
Expand Down
21 changes: 15 additions & 6 deletions crates/kild-core/src/sessions/daemon_spawn.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use kild_config::{Config, KildConfig};

use super::daemon_request::build_daemon_create_request;
use super::integrations::{
setup_claude_integration, setup_codex_integration, setup_fleet_instructions,
setup_opencode_integration,
setup_channel_integration, setup_claude_integration, setup_codex_integration,
setup_fleet_instructions, setup_opencode_integration,
};
use super::{fleet, inbox};

Expand Down Expand Up @@ -63,6 +63,13 @@ pub(super) fn spawn_daemon_agent(
setup_opencode_integration(params.agent, params.worktree_path);
setup_claude_integration(params.agent);
setup_fleet_instructions(params.agent, params.worktree_path, params.use_main_worktree);
setup_channel_integration(
params.agent,
params.worktree_path,
params.branch,
params.use_main_worktree,
params.kild_config,
);

// 3. Fleet member + inbox setup
fleet::ensure_fleet_member(params.branch, params.worktree_path, params.agent);
Expand All @@ -72,10 +79,12 @@ pub(super) fn spawn_daemon_agent(
inbox::ensure_inbox(&paths, params.project_id, params.branch, params.agent);

// 4. Fleet agent flags → augmented command
let fleet_command = match fleet::fleet_agent_flags(params.branch, params.agent) {
Some(flags) => format!("{} {}", params.agent_command, flags),
None => params.agent_command.to_string(),
};
let channels_enabled = params.kild_config.fleet.channels();
let fleet_command =
match fleet::fleet_agent_flags(params.branch, params.agent, channels_enabled) {
Some(flags) => format!("{} {}", params.agent_command, flags),
None => params.agent_command.to_string(),
};

// 5. Build daemon create request
let mut req_params = build_daemon_create_request(
Expand Down
5 changes: 5 additions & 0 deletions crates/kild-core/src/sessions/destroy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,11 @@ pub fn destroy_session(name: &str, force: bool) -> Result<(), SessionError> {
// 3e. Clean up fleet inbox file and team config entry
super::fleet::remove_fleet_member(&session.branch);

// 3f. Clean up .mcp.json for --main sessions (worktree deletion handles non-main)
if session.use_main_worktree {
super::integrations::channel::cleanup_mcp_json(&session.worktree_path);
}

// 4. Resolve main repo path before worktree removal (needed for branch cleanup)
let main_repo_path = git::removal::find_main_repo_root(&session.worktree_path);

Expand Down
26 changes: 15 additions & 11 deletions crates/kild-core/src/sessions/fleet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,13 @@ pub fn fleet_mode_active(branch: &str) -> bool {
///
/// Returns None if fleet mode does not apply (wrong agent, not active, etc.).
/// The returned string is appended to the existing agent command.
pub fn fleet_agent_flags(branch: &str, agent: &str) -> Option<String> {
pub fn fleet_agent_flags(branch: &str, agent: &str, channels_enabled: bool) -> Option<String> {
if !is_claude_fleet_agent(agent) || !fleet_mode_active(branch) {
return None;
}

let safe_name = fleet_safe_name(branch);
let flags = if branch == BRAIN_BRANCH {
let mut flags = if branch == BRAIN_BRANCH {
// Brain loads the kild-brain agent definition and joins as team lead.
format!(
"--agent kild-brain --agent-id {safe_name}@{TEAM_NAME} \
Expand All @@ -107,6 +107,10 @@ pub fn fleet_agent_flags(branch: &str, agent: &str) -> Option<String> {
)
};

if channels_enabled {
flags.push_str(" --dangerously-load-development-channels server:kild-fleet");
}

Some(flags)
}

Expand Down Expand Up @@ -604,7 +608,7 @@ mod tests {
#[test]
fn fleet_agent_flags_brain_gets_kild_brain_flag() {
with_team_dir("brain_flag", |_| {
let flags = fleet_agent_flags(BRAIN_BRANCH, "claude").unwrap();
let flags = fleet_agent_flags(BRAIN_BRANCH, "claude", false).unwrap();
assert!(
flags.contains("--agent kild-brain"),
"brain should get --agent kild-brain, got: {}",
Expand All @@ -621,7 +625,7 @@ mod tests {
#[test]
fn fleet_agent_flags_worker_does_not_get_brain_flag() {
with_team_dir("worker_no_brain_flag", |_| {
let flags = fleet_agent_flags("my-feature", "claude").unwrap();
let flags = fleet_agent_flags("my-feature", "claude", false).unwrap();
assert!(
!flags.contains("--agent kild-brain"),
"worker should not get --agent kild-brain, got: {}",
Expand All @@ -638,7 +642,7 @@ mod tests {
#[test]
fn fleet_agent_flags_slashed_branch_sanitized_in_agent_name() {
with_team_dir("slashed_branch_flags", |_| {
let flags = fleet_agent_flags("refactor/consolidate-ipc", "claude").unwrap();
let flags = fleet_agent_flags("refactor/consolidate-ipc", "claude", false).unwrap();
assert!(
flags.contains("--agent-id refactor-consolidate-ipc@honryu"),
"slashed branch should be sanitized in agent-id, got: {}",
Expand All @@ -660,18 +664,18 @@ mod tests {
#[test]
fn fleet_agent_flags_non_claude_returns_none() {
with_team_dir("non_claude_none", |_| {
assert!(fleet_agent_flags("my-feature", "amp").is_none());
assert!(fleet_agent_flags("my-feature", "codex").is_none());
assert!(fleet_agent_flags("my-feature", "kiro").is_none());
assert!(fleet_agent_flags("my-feature", "gemini").is_none());
assert!(fleet_agent_flags("my-feature", "amp", false).is_none());
assert!(fleet_agent_flags("my-feature", "codex", false).is_none());
assert!(fleet_agent_flags("my-feature", "kiro", false).is_none());
assert!(fleet_agent_flags("my-feature", "gemini", false).is_none());
});
}

#[test]
fn fleet_agent_flags_returns_none_when_no_team_dir_and_not_brain() {
without_team_dir("no_dir_worker", |_| {
assert!(
fleet_agent_flags("my-feature", "claude").is_none(),
fleet_agent_flags("my-feature", "claude", false).is_none(),
"should be None when team dir absent and branch is not brain"
);
});
Expand All @@ -681,7 +685,7 @@ mod tests {
fn fleet_agent_flags_brain_returns_flags_even_without_team_dir() {
without_team_dir("no_dir_brain", |_| {
// Brain creates the team — fleet activates unconditionally for the brain branch.
let flags = fleet_agent_flags(BRAIN_BRANCH, "claude");
let flags = fleet_agent_flags(BRAIN_BRANCH, "claude", false);
assert!(
flags.is_some(),
"brain should get flags even when team dir absent"
Expand Down
11 changes: 11 additions & 0 deletions crates/kild-core/src/sessions/inbox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ pub struct InboxState {
pub status: String,
pub task: Option<String>,
pub report: Option<String>,
/// Whether the MCP channel server is connected (`.channel` breadcrumb exists).
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub channel_connected: bool,
}

/// A single session's fleet status for the prime context.
Expand Down Expand Up @@ -81,6 +84,12 @@ pub fn ensure_inbox(paths: &KildPaths, project_id: &str, branch: &str, agent: &s
);
}

// Remove stale channel breadcrumb from previous session.
let channel_file = inbox_dir.join(".channel");
if channel_file.exists() {
let _ = std::fs::remove_file(&channel_file);
}

info!(
event = "core.fleet.inbox_ensured",
branch = branch,
Expand Down Expand Up @@ -141,12 +150,14 @@ pub fn read_inbox_state(project_id: &str, branch: &str) -> Result<Option<InboxSt

let task = std::fs::read_to_string(inbox_dir.join("task.md")).ok();
let report = std::fs::read_to_string(inbox_dir.join("report.md")).ok();
let channel_connected = inbox_dir.join(".channel").exists();

Ok(Some(InboxState {
branch: branch.to_string(),
status,
task,
report,
channel_connected,
}))
}

Expand Down
Loading