From 39292c52455dbb1fd2ee5415f75f7b37c8d9a42c Mon Sep 17 00:00:00 2001 From: Edwin Date: Sat, 6 Jun 2026 14:30:41 -0700 Subject: [PATCH 1/3] Rename project and default harness to construct and smith --- crates/adapter-zarvis/src/agent.rs | 8 +- crates/adapter-zarvis/src/interactive.rs | 45 +++++-- crates/adapter-zarvis/src/interval_suggest.rs | 10 +- crates/adapter-zarvis/src/model_limits.rs | 27 ++-- crates/adapter-zarvis/src/observe.rs | 25 ++-- .../src/provider/codex_oauth.rs | 120 +++++++++--------- crates/adapter-zarvis/src/provider/gemini.rs | 4 +- crates/adapter-zarvis/src/tasks.rs | 46 ++++--- crates/adapter-zarvis/src/title_mode.rs | 28 ++-- crates/adapter-zarvis/src/tools/mod.rs | 12 +- crates/adapter-zarvis/src/tools/proc.rs | 5 +- crates/adapter-zarvis/src/tools/shell.rs | 10 +- crates/cli/src/app.rs | 116 +++++++++-------- crates/cli/src/main.rs | 21 ++- crates/cli/src/pty_render.rs | 33 ++--- crates/cli/src/ui.rs | 110 +++++++++------- crates/cli/src/upgrade.rs | 22 ++-- crates/cli/tests/reconnect.rs | 10 +- crates/client/src/lib.rs | 20 ++- crates/daemon/src/config.rs | 26 ++-- crates/daemon/src/loops.rs | 38 ++---- crates/daemon/src/server.rs | 8 +- crates/daemon/src/session.rs | 64 +++++++--- crates/e2e/src/lib.rs | 33 +++-- crates/e2e/tests/key_latency.rs | 26 ++-- crates/e2e/tests/multi_session_latency.rs | 22 +++- crates/e2e/tests/remote_control.rs | 5 +- crates/e2e/tests/restart.rs | 18 ++- crates/e2e/tests/tui_smoke.rs | 15 ++- crates/e2e/tests/web_smoke.rs | 17 ++- crates/e2e/tests/zoom_latency.rs | 5 +- crates/mcp/src/main.rs | 6 +- crates/protocol/src/adapter/policy.rs | 5 +- crates/protocol/src/jsonrpc.rs | 11 +- crates/protocol/src/paths.rs | 4 +- crates/protocol/src/slash.rs | 29 +++-- 36 files changed, 569 insertions(+), 435 deletions(-) diff --git a/crates/adapter-zarvis/src/agent.rs b/crates/adapter-zarvis/src/agent.rs index a4277e12..2e2ebfda 100644 --- a/crates/adapter-zarvis/src/agent.rs +++ b/crates/adapter-zarvis/src/agent.rs @@ -6,7 +6,7 @@ use crate::context; use crate::persist::{self, Persist}; use crate::provider::{self, Content, LlmProvider, Message, Role, StopReason, TextSink, ToolCall}; -use crate::tools::{ToolCtx, ToolOutcome, ToolRegistry, truncate_for_model}; +use crate::tools::{truncate_for_model, ToolCtx, ToolOutcome, ToolRegistry}; use agentd_protocol::adapter::{AdapterContext, AdapterInboxMsg, EventEmitter}; use agentd_protocol::{MessageRole, SessionEvent, SessionStartParams, SessionState, ToolRisk}; use anyhow::Result; @@ -1462,10 +1462,8 @@ mod tests { #[test] fn auto_review_prompt_guides_model_toward_routine_repo_work() { assert!(AUTO_REVIEW_SYSTEM_PROMPT.contains("active git worktree")); - assert!( - AUTO_REVIEW_SYSTEM_PROMPT - .contains("git makes those changes inspectable and reversible") - ); + assert!(AUTO_REVIEW_SYSTEM_PROMPT + .contains("git makes those changes inspectable and reversible")); assert!(AUTO_REVIEW_SYSTEM_PROMPT.contains("cargo fmt --all")); assert!(AUTO_REVIEW_SYSTEM_PROMPT.contains("cargo test")); assert!(AUTO_REVIEW_SYSTEM_PROMPT.contains("git diff --name-only")); diff --git a/crates/adapter-zarvis/src/interactive.rs b/crates/adapter-zarvis/src/interactive.rs index bd1d71d9..9bd6a5cc 100644 --- a/crates/adapter-zarvis/src/interactive.rs +++ b/crates/adapter-zarvis/src/interactive.rs @@ -10,11 +10,11 @@ //! The TUI's `vt100`-backed terminal pane parses these bytes the same //! way it parses any other PTY-backed adapter's output. -use crate::agent::{ResolvedModel, push_msg, system_prompt_for_env}; +use crate::agent::{push_msg, system_prompt_for_env, ResolvedModel}; use crate::context; use crate::persist::{self, Persist}; use crate::provider::{self, Content, Message, Role, StopReason, TextSink, ToolCall}; -use crate::tools::{ToolCtx, ToolOutcome, ToolRegistry, truncate_for_model}; +use crate::tools::{truncate_for_model, ToolCtx, ToolOutcome, ToolRegistry}; use agentd_protocol::adapter::{AdapterContext, AdapterInboxMsg, EventEmitter}; use agentd_protocol::{ApprovalMode, SessionEvent, SessionStartParams, SessionState, ToolRisk}; use anyhow::Result; @@ -1368,7 +1368,10 @@ mod tests { let s = String::from_utf8(observation_panel_echo(obs).unwrap()).unwrap(); assert!(s.contains("ambient monitor flagged:"), "{s}"); assert!(s.contains("s277 \"x\": blocked at prompt"), "{s}"); - assert!(!s.contains("Decide whether to surface"), "boilerplate kept:\n{s}"); + assert!( + !s.contains("Decide whether to surface"), + "boilerplate kept:\n{s}" + ); } #[test] @@ -1396,7 +1399,11 @@ mod tests { let now = chrono::Utc::now(); let stale = now.timestamp_millis() - 15 * 60_000; // quiet 15m → idle let fresh = now.timestamp_millis(); - fn summary(id: &str, state: &str, last_pty_ms: Option) -> agentd_protocol::SessionSummary { + fn summary( + id: &str, + state: &str, + last_pty_ms: Option, + ) -> agentd_protocol::SessionSummary { let mut v = serde_json::json!({ "id": id, "harness": "claude", "cwd": "/x", "state": state, "created_at": "2026-06-06T00:00:00Z" @@ -3202,9 +3209,11 @@ fn ambient_session_is_active( let recent_event = session .last_event_at .is_some_and(|last| (now - last).num_milliseconds().max(0) <= window_ms); - recent_pty || recent_event || session.state == agentd_protocol::SessionState::Running - && session.last_pty_at_ms.is_none() - && session.last_event_at.is_none() + recent_pty + || recent_event + || session.state == agentd_protocol::SessionState::Running + && session.last_pty_at_ms.is_none() + && session.last_event_at.is_none() } /// Build the ambient-tick observation text. Pulls a live fleet snapshot from @@ -3377,7 +3386,10 @@ async fn fetch_session_preview( return None; } msgs.reverse(); - Some(truncate_keep_tail(&format!(" {}", msgs.join("\n ")), max_bytes)) + Some(truncate_keep_tail( + &format!(" {}", msgs.join("\n ")), + max_bytes, + )) } /// Render the recent PTY-log tail through a `vt100` parser and return the @@ -3411,7 +3423,10 @@ async fn pty_screen_preview( if lines.is_empty() { return None; } - Some(truncate_keep_tail(&format!(" {}", lines.join("\n ")), max_bytes)) + Some(truncate_keep_tail( + &format!(" {}", lines.join("\n ")), + max_bytes, + )) } /// Pure snapshot builder: counts active sessions by state (including idle @@ -3484,7 +3499,11 @@ fn compute_ambient_snapshot( format!("{} running but quiet {pm}m (idle/waiting?)", label(s)), )); } else { - let ago = if pm < 0 { "?".to_string() } else { format!("{pm}m") }; + let ago = if pm < 0 { + "?".to_string() + } else { + format!("{pm}m") + }; active_list.push(( pm.max(0), s.id.clone(), @@ -3501,8 +3520,10 @@ fn compute_ambient_snapshot( } SessionState::Errored => { errored += 1; - errored_list - .push((s.id.clone(), format!("{} errored {}m ago", label(s), event_idle_min(s)))); + errored_list.push(( + s.id.clone(), + format!("{} errored {}m ago", label(s), event_idle_min(s)), + )); } _ => {} } diff --git a/crates/adapter-zarvis/src/interval_suggest.rs b/crates/adapter-zarvis/src/interval_suggest.rs index 42d12b6c..1d044c4a 100644 --- a/crates/adapter-zarvis/src/interval_suggest.rs +++ b/crates/adapter-zarvis/src/interval_suggest.rs @@ -57,11 +57,7 @@ impl TextSink for CaptureSink { /// Ask the model for an interval in seconds for this loop's prompt. /// Returns a clamped value; failures fall back to the lower bound. -pub async fn suggest( - provider: &dyn LlmProvider, - model: &str, - user_prompt: &str, -) -> Result { +pub async fn suggest(provider: &dyn LlmProvider, model: &str, user_prompt: &str) -> Result { let messages = vec![Message { role: Role::User, content: Content::Text { @@ -73,7 +69,9 @@ pub async fn suggest( let _turn = provider .complete(model, SYSTEM_PROMPT, &messages, &tools, &mut sink) .await?; - Ok(parse_secs(&sink.text).map(clamp).unwrap_or_else(|| bounds().0)) + Ok(parse_secs(&sink.text) + .map(clamp) + .unwrap_or_else(|| bounds().0)) } /// Extract the first run of digits in the response, parse it, diff --git a/crates/adapter-zarvis/src/model_limits.rs b/crates/adapter-zarvis/src/model_limits.rs index 7f348fe1..d871bef4 100644 --- a/crates/adapter-zarvis/src/model_limits.rs +++ b/crates/adapter-zarvis/src/model_limits.rs @@ -124,8 +124,7 @@ impl ModelLimits { if estimated_tokens < threshold { return false; } - now_ms - entry.last_probed_at_ms - >= PROBE_INTERVAL_SECS * 1_000 + now_ms - entry.last_probed_at_ms >= PROBE_INTERVAL_SECS * 1_000 } /// Called after a provider returns a context-overflow error. @@ -145,8 +144,9 @@ impl ModelLimits { now_ms: i64, ) -> u64 { let k = key(provider, model); - let entry = self.entries.entry(k.clone()).or_insert_with(|| { - ModelEntry { key: k.clone(), ..Default::default() } + let entry = self.entries.entry(k.clone()).or_insert_with(|| ModelEntry { + key: k.clone(), + ..Default::default() }); let new_limit = match extracted { Some(n) if n > 0 => n, @@ -182,8 +182,9 @@ impl ModelLimits { now_ms: i64, ) { let k = key(provider, model); - let entry = self.entries.entry(k.clone()).or_insert_with(|| { - ModelEntry { key: k.clone(), ..Default::default() } + let entry = self.entries.entry(k.clone()).or_insert_with(|| ModelEntry { + key: k.clone(), + ..Default::default() }); if entry.learned_input_tokens == 0 { entry.learned_input_tokens = fallback; @@ -194,8 +195,7 @@ impl ModelLimits { // Bump only if the probe actually pushed past the prior // limit — otherwise the probe didn't test anything. if actual_input_tokens > entry.learned_input_tokens { - entry.learned_input_tokens = - ((actual_input_tokens as f64) * 1.05) as u64; + entry.learned_input_tokens = ((actual_input_tokens as f64) * 1.05) as u64; } } else { entry.calls_since_probe = entry.calls_since_probe.saturating_add(1); @@ -255,10 +255,15 @@ mod tests { #[test] fn serde_round_trip_preserves_learned_limit() { let mut s = ModelLimits::default(); - s.record_overflow("openai", "gpt-5.5", Some(272_000), 400_000, 1_700_000_000_000); + s.record_overflow( + "openai", + "gpt-5.5", + Some(272_000), + 400_000, + 1_700_000_000_000, + ); let json = serde_json::to_string(&s).expect("serialize"); - let restored: ModelLimits = - serde_json::from_str(&json).expect("deserialize"); + let restored: ModelLimits = serde_json::from_str(&json).expect("deserialize"); assert_eq!(restored.get("openai", "gpt-5.5"), Some(272_000)); let e = restored .entries diff --git a/crates/adapter-zarvis/src/observe.rs b/crates/adapter-zarvis/src/observe.rs index aa7ebe3d..7a7f1383 100644 --- a/crates/adapter-zarvis/src/observe.rs +++ b/crates/adapter-zarvis/src/observe.rs @@ -90,25 +90,20 @@ pub fn spawn(self_id: String) -> mpsc::UnboundedReceiver { continue; } let Some(params) = n.params else { continue }; - let payload: EventNotificationPayload = - match serde_json::from_value(params) { - Ok(p) => p, - Err(e) => { - tracing::warn!(error = %e, "orchestrator observe: bad payload"); - continue; - } - }; + let payload: EventNotificationPayload = match serde_json::from_value(params) { + Ok(p) => p, + Err(e) => { + tracing::warn!(error = %e, "orchestrator observe: bad payload"); + continue; + } + }; if payload.session_id == self_id { continue; } let Some(msg) = format_observation(&payload.event) else { continue; }; - let short = payload - .session_id - .chars() - .take(10) - .collect::(); + let short = payload.session_id.chars().take(10).collect::(); if tx .send(Observation { session_id: payload.session_id, @@ -143,9 +138,7 @@ fn format_observation(ev: &SessionEvent) -> Option { .unwrap_or_default(); Some(format!("{label}{suffix}")) } - SessionEvent::Done { exit_code } => { - Some(format!("ended (exit={exit_code})")) - } + SessionEvent::Done { exit_code } => Some(format!("ended (exit={exit_code})")), _ => None, } } diff --git a/crates/adapter-zarvis/src/provider/codex_oauth.rs b/crates/adapter-zarvis/src/provider/codex_oauth.rs index d12553c7..7d7b89fd 100644 --- a/crates/adapter-zarvis/src/provider/codex_oauth.rs +++ b/crates/adapter-zarvis/src/provider/codex_oauth.rs @@ -25,14 +25,14 @@ use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; use eventsource_stream::Eventsource; use futures::{SinkExt, StreamExt}; -use tokio_tungstenite::tungstenite::client::IntoClientRequest; -use tokio_tungstenite::tungstenite::Message as WsMessage; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::path::PathBuf; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use tokio::sync::Mutex; +use tokio_tungstenite::tungstenite::client::IntoClientRequest; +use tokio_tungstenite::tungstenite::Message as WsMessage; use super::{ Content, LlmProvider, Message, ProviderTurn, ReasoningItem, Role, StopReason, TextSink, @@ -146,8 +146,7 @@ pub fn auth_json_path() -> Result { /// (i.e. the user is in API-key mode, not the subscription mode this /// provider serves). pub fn load_auth_json(path: &std::path::Path) -> Result { - let bytes = std::fs::read(path) - .with_context(|| format!("read {}", path.display()))?; + let bytes = std::fs::read(path).with_context(|| format!("read {}", path.display()))?; let auth: AuthDotJson = serde_json::from_slice(&bytes) .with_context(|| format!("parse {} as JSON", path.display()))?; let tokens = auth.tokens.as_ref().ok_or_else(|| { @@ -173,10 +172,7 @@ pub fn load_auth_json(path: &std::path::Path) -> Result { /// truncate and write leaves the file empty and the user is logged /// out. The same dance Codex CLI does in /// `codex-rs/login/src/auth/manager.rs`. -pub fn save_auth_json_atomic( - path: &std::path::Path, - auth: &AuthDotJson, -) -> Result<()> { +pub fn save_auth_json_atomic(path: &std::path::Path, auth: &AuthDotJson) -> Result<()> { use std::io::Write as _; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent) @@ -196,9 +192,8 @@ pub fn save_auth_json_atomic( f.sync_all() .with_context(|| format!("fsync {}", tmp_path.display()))?; } - std::fs::rename(&tmp_path, path).with_context(|| { - format!("rename {} -> {}", tmp_path.display(), path.display()) - })?; + std::fs::rename(&tmp_path, path) + .with_context(|| format!("rename {} -> {}", tmp_path.display(), path.display()))?; Ok(()) } @@ -276,7 +271,8 @@ impl CodexOauth { map.insert("type".into(), json!("response.create")); map.insert("stream".into(), json!(true)); map.entry("tool_choice").or_insert_with(|| json!("auto")); - map.entry("parallel_tool_calls").or_insert_with(|| json!(true)); + map.entry("parallel_tool_calls") + .or_insert_with(|| json!(true)); } let frame = body.to_string(); @@ -424,7 +420,10 @@ impl CodexOauth { } } "response.function_call_arguments.delta" => { - let item_id = chunk.get("item_id").and_then(|v| v.as_str()).unwrap_or_default(); + let item_id = chunk + .get("item_id") + .and_then(|v| v.as_str()) + .unwrap_or_default(); if let Some(acc) = fn_calls.get_mut(item_id) { if let Some(d) = chunk.get("delta").and_then(|v| v.as_str()) { acc.args.push_str(d); @@ -432,7 +431,10 @@ impl CodexOauth { } } "response.function_call_arguments.done" => { - let item_id = chunk.get("item_id").and_then(|v| v.as_str()).unwrap_or_default(); + let item_id = chunk + .get("item_id") + .and_then(|v| v.as_str()) + .unwrap_or_default(); if let Some(acc) = fn_calls.get_mut(item_id) { if let Some(a) = chunk.get("arguments").and_then(|v| v.as_str()) { if !a.is_empty() { @@ -505,7 +507,9 @@ impl CodexOauth { } } if !completed { - return Err(anyhow!("codex-oauth ws stream ended before response.completed")); + return Err(anyhow!( + "codex-oauth ws stream ended before response.completed" + )); } let calls: Vec = fn_call_order .iter() @@ -641,8 +645,7 @@ impl CodexOauth { return true; }; let elapsed = chrono::Utc::now().signed_duration_since(last); - elapsed.num_seconds() + ACCESS_TOKEN_REFRESH_LEEWAY_SECS - >= 24 * 60 * 60 + elapsed.num_seconds() + ACCESS_TOKEN_REFRESH_LEEWAY_SECS >= 24 * 60 * 60 } } @@ -903,7 +906,10 @@ impl LlmProvider for CodexOauth { let ws_enabled = std::env::var("AGENTD_ZARVIS_CODEX_WS").as_deref() == Ok("1") && !self.ws_disabled.load(Ordering::Relaxed); if ws_enabled { - match self.try_ws(body.clone(), &access_token, &account_id, sink).await { + match self + .try_ws(body.clone(), &access_token, &account_id, sink) + .await + { WsResult::Done(turn) => return Ok(turn), WsResult::Error(e) => return Err(e), WsResult::Fallback(e) => { @@ -1008,7 +1014,9 @@ impl LlmProvider for CodexOauth { // items — record the id/name so subsequent // arguments-delta events can find the accumulator. "response.output_item.added" => { - let Some(item) = chunk.get("item") else { continue }; + let Some(item) = chunk.get("item") else { + continue; + }; if item.get("type").and_then(|v| v.as_str()) != Some("function_call") { continue; } @@ -1061,9 +1069,7 @@ impl LlmProvider for CodexOauth { .and_then(|v| v.as_array()) .map(|a| { a.iter() - .filter_map(|s| { - s.get("text").and_then(|t| t.as_str()) - }) + .filter_map(|s| s.get("text").and_then(|t| t.as_str())) .map(|t| t.to_string()) .collect() }) @@ -1084,9 +1090,7 @@ impl LlmProvider for CodexOauth { .and_then(|v| v.as_str()) .unwrap_or_default(); if let Some(acc) = fn_calls.get_mut(item_id) { - if let Some(delta) = - chunk.get("delta").and_then(|v| v.as_str()) - { + if let Some(delta) = chunk.get("delta").and_then(|v| v.as_str()) { acc.args.push_str(delta); } } @@ -1101,9 +1105,7 @@ impl LlmProvider for CodexOauth { .and_then(|v| v.as_str()) .unwrap_or_default(); if let Some(acc) = fn_calls.get_mut(item_id) { - if let Some(args) = - chunk.get("arguments").and_then(|v| v.as_str()) - { + if let Some(args) = chunk.get("arguments").and_then(|v| v.as_str()) { if !args.is_empty() { acc.args = args.to_string(); } @@ -1209,7 +1211,10 @@ async fn connect_codex_ws(access_token: &str, account_id: &str) -> Result Result<()> { - h.insert(k, HeaderValue::from_str(&v).with_context(|| format!("ws header {k}"))?); + h.insert( + k, + HeaderValue::from_str(&v).with_context(|| format!("ws header {k}"))?, + ); Ok(()) } let h = req.headers_mut(); @@ -1248,7 +1253,10 @@ where use tokio::io::{AsyncReadExt, AsyncWriteExt}; let purl = reqwest::Url::parse(proxy).with_context(|| format!("parse proxy url {proxy}"))?; - let phost = purl.host_str().context("proxy url has no host")?.to_string(); + let phost = purl + .host_str() + .context("proxy url has no host")? + .to_string(); let pport = purl.port_or_known_default().unwrap_or(80); let turl = reqwest::Url::parse(CODEX_RESPONSES_WS_URL).context("ws url")?; @@ -1294,7 +1302,9 @@ where let head = String::from_utf8_lossy(&head); let status_line = head.lines().next().unwrap_or_default(); if !status_line.contains(" 200") { - return Err(anyhow!("codex-oauth ws: proxy CONNECT failed: {status_line}")); + return Err(anyhow!( + "codex-oauth ws: proxy CONNECT failed: {status_line}" + )); } let (ws, _resp) = tokio_tungstenite::client_async_tls(req, tcp) @@ -1327,7 +1337,10 @@ fn response_error_message(chunk: &Value) -> String { fn failed_response_error(chunk: &Value) -> anyhow::Error { let msg = response_error_message(chunk); if let Some(extracted) = super::parse_overflow(&msg) { - return anyhow::Error::new(super::ContextOverflow { extracted, raw: msg }); + return anyhow::Error::new(super::ContextOverflow { + extracted, + raw: msg, + }); } anyhow!("codex-oauth response failed: {msg}") } @@ -1383,10 +1396,8 @@ mod tests { /// Missing file → clear actionable error mentioning the path. #[test] fn load_auth_json_missing_file_errors_with_path() { - let bogus = std::env::temp_dir().join(format!( - "agentd-codex-oauth-missing-{}", - std::process::id() - )); + let bogus = + std::env::temp_dir().join(format!("agentd-codex-oauth-missing-{}", std::process::id())); let _ = std::fs::remove_file(&bogus); let err = load_auth_json(&bogus).unwrap_err(); let msg = format!("{err:#}"); @@ -1401,8 +1412,7 @@ mod tests { "agentd-codex-oauth-noauth-{}.json", std::process::id() )); - std::fs::write(&tmp, r#"{ "OPENAI_API_KEY": "sk-platform-only" }"#) - .unwrap(); + std::fs::write(&tmp, r#"{ "OPENAI_API_KEY": "sk-platform-only" }"#).unwrap(); let err = load_auth_json(&tmp).unwrap_err(); let msg = format!("{err:#}"); assert!(msg.contains("codex login"), "{msg}"); @@ -1434,10 +1444,8 @@ mod tests { /// that's an integration concern with a fault-injecting FS. #[test] fn save_auth_json_atomic_round_trips() { - let dir = std::env::temp_dir().join(format!( - "agentd-codex-oauth-save-{}", - std::process::id() - )); + let dir = + std::env::temp_dir().join(format!("agentd-codex-oauth-save-{}", std::process::id())); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).unwrap(); let target = dir.join("auth.json"); @@ -1461,10 +1469,7 @@ mod tests { let t = loaded.tokens.unwrap(); assert_eq!(t.access_token, "A1"); assert_eq!(t.refresh_token, "R1"); - assert_eq!( - loaded.last_refresh.as_deref(), - Some("2026-05-18T01:02:03Z") - ); + assert_eq!(loaded.last_refresh.as_deref(), Some("2026-05-18T01:02:03Z")); let _ = std::fs::remove_dir_all(&dir); } @@ -1472,18 +1477,14 @@ mod tests { fn user(text: &str) -> Message { Message { role: Role::User, - content: Content::Text { - text: text.into(), - }, + content: Content::Text { text: text.into() }, } } fn assistant(text: &str) -> Message { Message { role: Role::Assistant, - content: Content::Text { - text: text.into(), - }, + content: Content::Text { text: text.into() }, } } @@ -1578,7 +1579,11 @@ mod tests { ]; let body = build_responses_body("gpt-5-codex", "sys", &msgs, &[]); let input = body["input"].as_array().unwrap(); - assert_eq!(input.len(), 4, "user + assistant-prose + fn_call + fn_output"); + assert_eq!( + input.len(), + 4, + "user + assistant-prose + fn_call + fn_output" + ); assert_eq!(input[0]["role"], "user"); // Assistant prose item. assert_eq!(input[1]["type"], "message"); @@ -1669,8 +1674,7 @@ mod tests { auth.last_refresh = Some(chrono::Utc::now().to_rfc3339()); assert!(!CodexOauth::needs_refresh(&auth)); // Old timestamp → refresh. - auth.last_refresh = - Some((chrono::Utc::now() - chrono::Duration::days(2)).to_rfc3339()); + auth.last_refresh = Some((chrono::Utc::now() - chrono::Duration::days(2)).to_rfc3339()); assert!(CodexOauth::needs_refresh(&auth)); // Malformed timestamp → conservatively refresh. auth.last_refresh = Some("not-a-date".to_string()); @@ -1714,7 +1718,8 @@ mod tests { }); let err = failed_response_error(&chunk); assert!( - err.downcast_ref::().is_some(), + err.downcast_ref::() + .is_some(), "context-window overflow via response.failed must be a ContextOverflow; got: {err:#}" ); } @@ -1726,7 +1731,8 @@ mod tests { }); let err = failed_response_error(&chunk); assert!( - err.downcast_ref::().is_none(), + err.downcast_ref::() + .is_none(), "a non-overflow failure must not be misclassified as ContextOverflow" ); assert_eq!( diff --git a/crates/adapter-zarvis/src/provider/gemini.rs b/crates/adapter-zarvis/src/provider/gemini.rs index 21a077c6..07febc9d 100644 --- a/crates/adapter-zarvis/src/provider/gemini.rs +++ b/crates/adapter-zarvis/src/provider/gemini.rs @@ -305,9 +305,7 @@ mod tests { let messages = vec![ Message { role: Role::User, - content: Content::Text { - text: "hi".into(), - }, + content: Content::Text { text: "hi".into() }, }, Message { role: Role::Assistant, diff --git a/crates/adapter-zarvis/src/tasks.rs b/crates/adapter-zarvis/src/tasks.rs index d2660c4d..84b2b591 100644 --- a/crates/adapter-zarvis/src/tasks.rs +++ b/crates/adapter-zarvis/src/tasks.rs @@ -34,8 +34,7 @@ use tokio::sync::{mpsc, oneshot, Mutex}; /// auto-backgrounds. The real result lands later as an /// `OBSERVATION:` injection. Keep stable — the system prompt /// references this exact phrasing so the model knows what to expect. -pub const BG_PLACEHOLDER_OUTPUT: &str = - "(running in background; will report when complete)"; +pub const BG_PLACEHOLDER_OUTPUT: &str = "(running in background; will report when complete)"; /// Default auto-background threshold. Overridable via /// `AGENTD_TOOL_BG_AFTER_MS`. @@ -283,11 +282,7 @@ fn spawn_background_watcher( /// `call_id` in the running map and forwards the control message /// via its `control_tx`. Returns `true` if a matching supervisor /// was found. -pub async fn forward_control( - tasks: &Tasks, - call_id: &str, - control: ToolControl, -) -> bool { +pub async fn forward_control(tasks: &Tasks, call_id: &str, control: ToolControl) -> bool { let g = tasks.running.lock().await; match g.get(call_id) { Some(entry) => entry.control_tx.send(control).is_ok(), @@ -351,7 +346,10 @@ mod tests { Duration::from_secs(60), async { tokio::time::sleep(Duration::from_secs(10)).await; - Ok(ToolOutcome { ok: true, output: "never".into() }) + Ok(ToolOutcome { + ok: true, + output: "never".into(), + }) }, ) .await; @@ -371,18 +369,18 @@ mod tests { Duration::from_millis(50), async { tokio::time::sleep(Duration::from_millis(200)).await; - Ok(ToolOutcome { ok: true, output: "delayed".into() }) + Ok(ToolOutcome { + ok: true, + output: "delayed".into(), + }) }, ) .await; assert!(matches!(outcome, SupervisorOutcome::Backgrounded)); - let completion = tokio::time::timeout( - Duration::from_millis(500), - rx.recv(), - ) - .await - .expect("watcher should report") - .expect("channel should not close"); + let completion = tokio::time::timeout(Duration::from_millis(500), rx.recv()) + .await + .expect("watcher should report") + .expect("channel should not close"); assert_eq!(completion.call_id, "c1"); match completion.outcome { Ok(o) => assert_eq!(o.output, "delayed"), @@ -408,18 +406,18 @@ mod tests { Duration::from_secs(60), // wouldn't auto-bg in this window async { tokio::time::sleep(Duration::from_millis(80)).await; - Ok(ToolOutcome { ok: true, output: "delayed".into() }) + Ok(ToolOutcome { + ok: true, + output: "delayed".into(), + }) }, ) .await; assert!(matches!(outcome, SupervisorOutcome::Backgrounded)); - let completion = tokio::time::timeout( - Duration::from_millis(500), - rx.recv(), - ) - .await - .expect("watcher reports") - .unwrap(); + let completion = tokio::time::timeout(Duration::from_millis(500), rx.recv()) + .await + .expect("watcher reports") + .unwrap(); match completion.outcome { Ok(o) => assert_eq!(o.output, "delayed"), Err(e) => panic!("expected ok, got {e}"), diff --git a/crates/adapter-zarvis/src/title_mode.rs b/crates/adapter-zarvis/src/title_mode.rs index 9a77c634..7a8b214b 100644 --- a/crates/adapter-zarvis/src/title_mode.rs +++ b/crates/adapter-zarvis/src/title_mode.rs @@ -60,9 +60,7 @@ fn provider_for(p: Provider) -> Result> { // emits openai/anthropic/ollama. Bail loudly if we ever get // here so the contract doesn't drift silently. Provider::CodexOauth => { - return Err(anyhow!( - "title-gen does not support codex-oauth provider" - )); + return Err(anyhow!("title-gen does not support codex-oauth provider")); } }) } @@ -83,7 +81,13 @@ pub async fn suggest_title(user_prompt: &str) -> Result { let tools: Vec = Vec::new(); let mut sink = CaptureSink::default(); let _turn = provider - .complete(&spec.model, TITLE_SYSTEM_PROMPT, &messages, &tools, &mut sink) + .complete( + &spec.model, + TITLE_SYSTEM_PROMPT, + &messages, + &tools, + &mut sink, + ) .await?; Ok(sanitize_title(&sink.text)) } @@ -94,9 +98,9 @@ pub async fn suggest_title(user_prompt: &str) -> Result { /// safety buffer. fn sanitize_title(raw: &str) -> String { let line = raw.lines().next().unwrap_or(""); - let mut s = line.trim().trim_matches(|c: char| { - c == '"' || c == '\'' || c == '`' || c == '*' || c == '#' - }); + let mut s = line + .trim() + .trim_matches(|c: char| c == '"' || c == '\'' || c == '`' || c == '*' || c == '#'); // Strip a single pair of surrounding quotes after the trim-match above // catches the easy cases. while let (Some('"'), Some('"')) = (s.chars().next(), s.chars().last()) { @@ -113,13 +117,19 @@ mod tests { #[test] fn sanitize_strips_quotes_and_markdown() { - assert_eq!(sanitize_title("\"Refactor Adapter Spawning\""), "Refactor Adapter Spawning"); + assert_eq!( + sanitize_title("\"Refactor Adapter Spawning\""), + "Refactor Adapter Spawning" + ); assert_eq!(sanitize_title("`Add Pty Logging`"), "Add Pty Logging"); assert_eq!(sanitize_title("**Plan The Refactor**"), "Plan The Refactor"); } #[test] fn sanitize_first_line_only() { - assert_eq!(sanitize_title("Title Here\nextra explanation"), "Title Here"); + assert_eq!( + sanitize_title("Title Here\nextra explanation"), + "Title Here" + ); } #[test] fn sanitize_caps_length() { diff --git a/crates/adapter-zarvis/src/tools/mod.rs b/crates/adapter-zarvis/src/tools/mod.rs index da50988a..91901731 100644 --- a/crates/adapter-zarvis/src/tools/mod.rs +++ b/crates/adapter-zarvis/src/tools/mod.rs @@ -374,7 +374,11 @@ mod tests { // read_only: true → Safe (fans out, skips the gate). assert!(matches!( - effective_risk(&shell, &json!({ "command": "cat a.rs", "read_only": true }), &cwd), + effective_risk( + &shell, + &json!({ "command": "cat a.rs", "read_only": true }), + &cwd + ), ToolRisk::Safe )); @@ -384,7 +388,11 @@ mod tests { ToolRisk::Risky )); assert!(matches!( - effective_risk(&shell, &json!({ "command": "cat a.rs", "read_only": false }), &cwd), + effective_risk( + &shell, + &json!({ "command": "cat a.rs", "read_only": false }), + &cwd + ), ToolRisk::Risky )); diff --git a/crates/adapter-zarvis/src/tools/proc.rs b/crates/adapter-zarvis/src/tools/proc.rs index 5127ece5..79600e15 100644 --- a/crates/adapter-zarvis/src/tools/proc.rs +++ b/crates/adapter-zarvis/src/tools/proc.rs @@ -198,7 +198,10 @@ mod tests { .write("proc-999", "x", false, Duration::from_millis(10)) .await .is_none()); - assert!(reg.drain("proc-999", Duration::from_millis(10)).await.is_none()); + assert!(reg + .drain("proc-999", Duration::from_millis(10)) + .await + .is_none()); } #[tokio::test] diff --git a/crates/adapter-zarvis/src/tools/shell.rs b/crates/adapter-zarvis/src/tools/shell.rs index 376dccb3..a7cf4038 100644 --- a/crates/adapter-zarvis/src/tools/shell.rs +++ b/crates/adapter-zarvis/src/tools/shell.rs @@ -209,7 +209,10 @@ impl Tool for WriteStdin { ToolRisk::Risky } fn args_summary(&self, input: &Value) -> String { - let id = input.get("session_id").and_then(|s| s.as_str()).unwrap_or("?"); + let id = input + .get("session_id") + .and_then(|s| s.as_str()) + .unwrap_or("?"); let data = input.get("data").and_then(|s| s.as_str()).unwrap_or(""); let eof = input.get("eof").and_then(|b| b.as_bool()).unwrap_or(false); let preview: String = data.chars().take(60).collect(); @@ -372,7 +375,10 @@ mod tests { ); let done = WriteStdin - .run(json!({"session_id": id, "eof": true, "timeout_sec": 1}), &ctx) + .run( + json!({"session_id": id, "eof": true, "timeout_sec": 1}), + &ctx, + ) .await .expect("run returns Ok"); assert!( diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index 6cf96086..86cc7805 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -14,11 +14,11 @@ use crossterm::event::{ }; use crossterm::execute; use crossterm::terminal::{ - EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode, + disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen, }; use futures::{FutureExt, StreamExt}; -use ratatui::Terminal; use ratatui::backend::CrosstermBackend; +use ratatui::Terminal; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::io::{Stdout, Write}; @@ -3854,9 +3854,10 @@ impl App { agentd_protocol::ApprovalMode::UnsafeAuto => agentd_protocol::ApprovalMode::Manual, }; match self.client.set_approval_mode(&id, next).await { - Ok(()) if show_status => { - self.set_status(format!("approval mode {}", next.badge().unwrap_or("manual"))) - } + Ok(()) if show_status => self.set_status(format!( + "approval mode {}", + next.badge().unwrap_or("manual") + )), Ok(()) => {} Err(e) => self.set_status(format!("set_approval_mode failed: {e}")), } @@ -6764,6 +6765,7 @@ impl App { .split_once(char::is_whitespace) .map(|(v, a)| (v, a.trim())) .unwrap_or((cmd, "")); + let verb = verb.trim_start_matches('/'); match verb { "" => {} "quit" | "exit" => self.should_quit = true, @@ -6853,16 +6855,16 @@ impl App { .collect(); self.set_status(format!("harnesses: {}", names.join(", "))); } - "agentd" => { + "agentd" | "construct" => { // Subcommand dispatch: // - // /agentd restart [binary path] + // /construct restart [binary path] // → daemon.restart; exec the daemon's own binary, // or the given one (e.g. a freshly-built worktree // binary). The path is validated daemon-side. // // Other subcommands are reserved for future use - // (e.g. `/agentd info` to print build version). The + // (e.g. `/construct info` to print build version). The // daemon.restart RPC will close the IPC connection // as the new process replaces the old; the TUI // observes that as a "daemon disconnected" status @@ -6875,8 +6877,8 @@ impl App { "restart" => { let exe = (!rest.is_empty()).then(|| rest.to_string()); match self.client.daemon_restart(exe).await { - Ok(r) => self.set_status(format!( - "agentd: restart requested (exe={}, pid={}) — reconnect when ready", + Ok(r) => self.set_status(format!( + "construct: restart requested (exe={}, pid={}) — reconnect when ready", r.exe, r.pid )), // BrokenPipe / connection closed is the @@ -6891,17 +6893,17 @@ impl App { || msg.contains("closed") { self.set_status( - "agentd: restart in flight (socket closed) — reconnect when ready".to_string(), + "construct: restart in flight (socket closed) — reconnect when ready".to_string(), ); } else { - self.set_status(format!("agentd restart failed: {e}")); + self.set_status(format!("construct restart failed: {e}")); } } } } - "" => self.set_status("agentd: subcommand required (e.g. `restart`)".into()), + "" => self.set_status("construct: subcommand required (e.g. `restart`)".into()), other => self.set_status(format!( - "agentd: unknown subcommand '{other}'; try `restart`" + "construct: unknown subcommand '{other}'; try `restart`" )), } } @@ -8538,7 +8540,9 @@ mod tests { }); let mut showing = false; terminal - .draw(|f| showing = crate::ui::render_operator_monolog(f, area, &mut app, Instant::now())) + .draw(|f| { + showing = crate::ui::render_operator_monolog(f, area, &mut app, Instant::now()) + }) .expect("draw"); assert!(showing, "monolog should be showing mid-cycle"); let screen = rendered_text(terminal.backend().buffer()); @@ -8552,10 +8556,15 @@ mod tests { started_at: Instant::now() - std::time::Duration::from_secs(30), }); terminal - .draw(|f| showing = crate::ui::render_operator_monolog(f, area, &mut app, Instant::now())) + .draw(|f| { + showing = crate::ui::render_operator_monolog(f, area, &mut app, Instant::now()) + }) .expect("draw"); assert!(!showing, "monolog should have expired"); - assert!(app.operator_monolog.is_none(), "expired monolog not cleared"); + assert!( + app.operator_monolog.is_none(), + "expired monolog not cleared" + ); server.abort(); } @@ -8584,7 +8593,10 @@ mod tests { .expect("draw"); assert!(!drew, "monolog should be skipped while the panel is open"); let screen = rendered_text(terminal.backend().buffer()); - assert!(!screen.contains("session"), "should not draw over rain:\n{screen}"); + assert!( + !screen.contains("session"), + "should not draw over rain:\n{screen}" + ); server.abort(); } @@ -8672,7 +8684,7 @@ mod tests { let screen = rendered_text(terminal.backend().buffer()); assert!( - screen.contains("Welcome to agentd"), + screen.contains("Welcome to construct"), "missing welcome:\n{screen}" ); assert!( @@ -8688,7 +8700,7 @@ mod tests { "missing quit shortcut:\n{screen}" ); assert!( - !screen.contains("q exit agentd"), + !screen.contains("q exit construct"), "empty state should not show q as the quit shortcut:\n{screen}" ); assert!( @@ -8704,37 +8716,33 @@ mod tests { "expected clickable shortcuts, got {:?}", app.layout.shortcut_hints ); - assert!( - app.layout - .shortcut_hints - .iter() - .any(|h| h.action == KeyAction::OpenNewSession) - ); - assert!( - app.layout - .shortcut_hints - .iter() - .any(|h| h.action == KeyAction::OpenCommandPalette) - ); - assert!( - app.layout - .shortcut_hints - .iter() - .any(|h| h.action == KeyAction::ToggleHelp) - ); - assert!( - app.layout - .shortcut_hints - .iter() - .any(|h| h.action == KeyAction::Quit) - ); + assert!(app + .layout + .shortcut_hints + .iter() + .any(|h| h.action == KeyAction::OpenNewSession)); + assert!(app + .layout + .shortcut_hints + .iter() + .any(|h| h.action == KeyAction::OpenCommandPalette)); + assert!(app + .layout + .shortcut_hints + .iter() + .any(|h| h.action == KeyAction::ToggleHelp)); + assert!(app + .layout + .shortcut_hints + .iter() + .any(|h| h.action == KeyAction::Quit)); server.abort(); } #[tokio::test] async fn update_notice_renders_right_aligned_in_modeline() { let (mut app, _dir, server) = empty_app().await; - app.update_notice = Some("↑ agentd 9.9.9 · agent upgrade".to_string()); + app.update_notice = Some("↑ construct 9.9.9 · agent upgrade".to_string()); let backend = ratatui::backend::TestBackend::new(120, 36); let mut terminal = ratatui::Terminal::new(backend).expect("terminal"); @@ -8745,7 +8753,7 @@ mod tests { let screen = rendered_text(terminal.backend().buffer()); let modeline = screen .lines() - .find(|l| l.contains("↑ agentd 9.9.9 · agent upgrade")) + .find(|l| l.contains("↑ construct 9.9.9 · agent upgrade")) .expect("update notice should be on screen"); // Right-aligned: only padding follows it to the right edge. @@ -9372,10 +9380,8 @@ mod tests { app.sessions.push(orch); app.refresh_orchestrator_id(); app.matrix_rain_hidden = false; - app.pending_tool_approvals.insert( - "orch".into(), - HashSet::from(["call-1".to_string()]), - ); + app.pending_tool_approvals + .insert("orch".into(), HashSet::from(["call-1".to_string()])); let backend = ratatui::backend::TestBackend::new(120, 40); let mut term = ratatui::Terminal::new(backend).expect("terminal"); @@ -9709,7 +9715,7 @@ mod tests { use tempfile::tempdir; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::UnixListener; - use tokio::sync::{Notify, mpsc}; + use tokio::sync::{mpsc, Notify}; let dir = tempdir().expect("tempdir"); let sock = dir.path().join("agentd.sock"); @@ -9975,7 +9981,7 @@ mod tests { use tempfile::tempdir; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; use tokio::net::UnixListener; - use tokio::sync::{Notify, mpsc}; + use tokio::sync::{mpsc, Notify}; let dir = tempdir().expect("tempdir"); let sock = dir.path().join("agentd.sock"); @@ -11104,8 +11110,8 @@ fn drainable_mouse_burst_kind(kind: &MouseEventKind) -> bool { #[cfg(test)] mod drain_gate_tests { use super::{ - PaneFocus, ZoomMode, should_autofocus_view_from_list, should_drain_after, url_range_at_col, - url_ranges, + should_autofocus_view_from_list, should_drain_after, url_range_at_col, url_ranges, + PaneFocus, ZoomMode, }; use crossterm::event::{ Event as CtEvent, KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers, @@ -11353,7 +11359,7 @@ pub fn parse_group_delete_choice(input: &str) -> GroupDeleteChoice { #[cfg(test)] mod group_delete_prompt_tests { - use super::{GroupDeleteChoice, parse_group_delete_choice}; + use super::{parse_group_delete_choice, GroupDeleteChoice}; /// `y` / `yes` (any case, with whitespace) → orphan members /// (original pre-cascade behavior). diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 8900c650..c5b73112 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -15,11 +15,7 @@ use agentd_client::Client; use agentd_protocol::paths::Paths; #[derive(Debug, Parser)] -#[command( - name = "agent", - about = "agent: TUI client for agentd", - version, -)] +#[command(name = "agent", about = "agent: TUI client for agentd", version)] struct Cli { /// Override the daemon socket path. #[arg(long, global = true)] @@ -61,10 +57,7 @@ enum Command { worktree: bool, }, /// Send input to a session. - Send { - session_id: String, - text: String, - }, + Send { session_id: String, text: String }, /// Internal: `PreToolUse` hook body for the AskUserQuestion chat-gate. /// Reads the hook payload on stdin; if a chat viewer is active for /// `$AGENTD_SESSION_ID`, prints a `deny` decision that degrades Claude's @@ -118,7 +111,9 @@ enum Command { fn init_tracing() { use tracing_subscriber::{fmt, EnvFilter}; - let filter = EnvFilter::try_from_default_env().or_else(|_| EnvFilter::try_new("warn")).unwrap(); + let filter = EnvFilter::try_from_default_env() + .or_else(|_| EnvFilter::try_new("warn")) + .unwrap(); let _ = fmt().with_env_filter(filter).with_target(false).try_init(); } @@ -204,7 +199,11 @@ async fn main() -> Result<()> { .create(agentd_protocol::CreateSessionParams { harness, cwd, - prompt: if prompt.trim().is_empty() { None } else { Some(prompt) }, + prompt: if prompt.trim().is_empty() { + None + } else { + Some(prompt) + }, model, title, mode, diff --git a/crates/cli/src/pty_render.rs b/crates/cli/src/pty_render.rs index 89373696..890e24f3 100644 --- a/crates/cli/src/pty_render.rs +++ b/crates/cli/src/pty_render.rs @@ -1134,14 +1134,12 @@ impl ItemHistory { return None; } match self.items.get(c.processed_count) { - Some(Item::PtyChunk(bytes)) if bytes.len() >= c.pending_consumed => { - Some(( - c.processed_count, - c.pending_consumed, - c.pending_visible_lines, - c.pending_end_col, - )) - } + Some(Item::PtyChunk(bytes)) if bytes.len() >= c.pending_consumed => Some(( + c.processed_count, + c.pending_consumed, + c.pending_visible_lines, + c.pending_end_col, + )), _ => None, } }); @@ -3266,10 +3264,8 @@ mod tests { let mut chat = Vec::with_capacity(900); for j in 0..8 { chat.extend_from_slice( - format!( - "\x1b[33mhistory {i}.{j} before the clicked tool block\x1b[0m\r\n" - ) - .as_bytes(), + format!("\x1b[33mhistory {i}.{j} before the clicked tool block\x1b[0m\r\n") + .as_bytes(), ); } h.feed_pty(&chat); @@ -3303,10 +3299,7 @@ mod tests { let expanded = h.replay(cols, rows, 0); let expand_us = expand_t.elapsed().as_micros(); assert!( - expanded - .blocks - .iter() - .any(|hit| hit.call_id == target), + expanded.blocks.iter().any(|hit| hit.call_id == target), "expanded block hit rect should survive suffix rebuild: {:?}", expanded.blocks ); @@ -3316,10 +3309,7 @@ mod tests { let collapsed = h.replay(cols, rows, 0); let collapse_us = collapse_t.elapsed().as_micros(); assert!( - collapsed - .blocks - .iter() - .any(|hit| hit.call_id == target), + collapsed.blocks.iter().any(|hit| hit.call_id == target), "collapsed block hit rect should survive suffix rebuild" ); @@ -3395,8 +3385,7 @@ mod tests { let mut chat = Vec::with_capacity(900); for j in 0..8 { chat.extend_from_slice( - format!("\x1b[33mhistory {i}.{j} before partial pending\x1b[0m\r\n") - .as_bytes(), + format!("\x1b[33mhistory {i}.{j} before partial pending\x1b[0m\r\n").as_bytes(), ); } h.feed_pty(&chat); diff --git a/crates/cli/src/ui.rs b/crates/cli/src/ui.rs index 90381dea..f0aaf5a5 100644 --- a/crates/cli/src/ui.rs +++ b/crates/cli/src/ui.rs @@ -8,12 +8,12 @@ use crate::app::{ use crate::keymap::KeyAction; use crate::theme::Theme; use agentd_protocol::{MessageRole, SessionEvent, SessionState, SessionSummary, TimestampedEvent}; -use ratatui::Frame; use ratatui::buffer::Buffer; use ratatui::layout::{Constraint, Direction, Layout, Margin, Position, Rect}; use ratatui::style::{Color, Modifier, Style}; use ratatui::text::{Line, Span}; use ratatui::widgets::{Block, Borders, Clear, List, ListItem, ListState, Paragraph, Wrap}; +use ratatui::Frame; use std::collections::{HashMap, HashSet}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; use unicode_width::UnicodeWidthStr; @@ -678,7 +678,13 @@ fn render_list_title_button_tooltips(f: &mut Frame, app: &App) { if let Some(rain) = app.layout.matrix_rain_area { if let Some((xs, xe, y)) = matrix_rain_close_button_range(rain) { if my == y && mx >= xs && mx < xe { - render_button_tooltip(f, &app.theme, " Hide Operator ", xs, y.saturating_add(2)); + render_button_tooltip( + f, + &app.theme, + " Hide Operator ", + xs, + y.saturating_add(2), + ); } } } @@ -2010,9 +2016,9 @@ fn render_matrix_rain_header(f: &mut Frame, area: Rect, app: &mut App, now: Inst let label_x = area.x.saturating_add(1); let operator_start = label_x.saturating_add(1); let operator_end = operator_start.saturating_add(UnicodeWidthStr::width(operator_text) as u16); - let operator_hovered = app.mouse_pos.is_some_and(|(mx, my)| { - my == area.y && mx >= operator_start && mx < operator_end - }); + let operator_hovered = app + .mouse_pos + .is_some_and(|(mx, my)| my == area.y && mx >= operator_start && mx < operator_end); let operator_style = if approval_pending { Style::default() .fg(app.theme.warning) @@ -2031,7 +2037,8 @@ fn render_matrix_rain_header(f: &mut Frame, area: Rect, app: &mut App, now: Inst let selected_id = app.matrix_widget_selected.clone(); let separator_x = operator_end.saturating_add(1); if !panels.is_empty() { - f.buffer_mut().set_string(separator_x, area.y, "─", line_style); + f.buffer_mut() + .set_string(separator_x, area.y, "─", line_style); } let mut icon_x = separator_x.saturating_add(2); let icon_limit = area.x + area.width.saturating_sub(5); @@ -2398,7 +2405,11 @@ fn render_pinned_letters_at( } else { let since_pin_ms = elapsed_ms.saturating_sub(pinned_at); let brightness = if elapsed_ms < fade_start { - if since_pin_ms < 220 { 1.0 } else { 0.76 } + if since_pin_ms < 220 { + 1.0 + } else { + 0.76 + } } else { (0.12 + fade_level * 0.64).clamp(0.0, 1.0) }; @@ -2813,14 +2824,14 @@ fn render_empty_session_state(f: &mut Frame, area: Rect, app: &mut App) { let lines = vec![ Line::from(Span::styled( - "Welcome to agentd", + "Welcome to construct", Style::default() .fg(app.theme.text) .add_modifier(Modifier::BOLD), )), Line::raw(""), Line::from(Span::styled( - "Start with a session. Sessions are the live terminals agentd tracks.", + "Start with a session. Sessions are the live terminals construct tracks.", Style::default().fg(app.theme.dim), )), Line::raw(""), @@ -3996,7 +4007,13 @@ fn render_markdown_table( total -= 1; } let mut out = Vec::with_capacity(table.rows.len() + 2); - out.push(table_row_line(&table.header, &widths, &table.aligns, theme, true)); + out.push(table_row_line( + &table.header, + &widths, + &table.aligns, + theme, + true, + )); out.push(table_rule_line(&widths, theme)); for row in &table.rows { out.push(table_row_line(row, &widths, &table.aligns, theme, false)); @@ -4012,7 +4029,9 @@ fn table_row_line( header: bool, ) -> Line<'static> { let cell_style = if header { - Style::default().fg(theme.accent).add_modifier(Modifier::BOLD) + Style::default() + .fg(theme.accent) + .add_modifier(Modifier::BOLD) } else { Style::default().fg(theme.text) }; @@ -5340,7 +5359,7 @@ fn render_modeline(f: &mut Frame, area: Rect, app: &mut App) { "" }; let modeline_before_approval_mode = format!( - " agentd focus:{focus} {sel} {model} {remote}", + " construct focus:{focus} {sel} {model} {remote}", focus = focus_label, remote = remote_badge, sel = match s { @@ -5365,12 +5384,11 @@ fn render_modeline(f: &mut Frame, area: Rect, app: &mut App) { .saturating_add(width) .min(area.x.saturating_add(area.width)); if end_col > start_col { - app.layout.modeline_approval_mode_hit = - Some(crate::app::ModelineApprovalModeHit { - row: area.y, - start_col, - end_col, - }); + app.layout.modeline_approval_mode_hit = Some(crate::app::ModelineApprovalModeHit { + row: area.y, + start_col, + end_col, + }); } } } @@ -5442,7 +5460,11 @@ fn render_modeline(f: &mut Frame, area: Rect, app: &mut App) { fn approval_mode_modeline_label(s: &SessionSummary) -> Option<&'static str> { s.approval_mode .badge() - .or_else(|| (s.harness == "zarvis").then_some("manual")) + .or_else(|| is_smith_like_harness(&s.harness).then_some("manual")) +} + +fn is_smith_like_harness(name: &str) -> bool { + matches!(name, "smith" | "zarvis") } fn render_modeline_approval_mode_tooltip(f: &mut Frame, app: &App) { @@ -5669,8 +5691,8 @@ const HELP_TEXT: &str = " emacs keymap (default; AGENTD_KEYMAP=vim for vim profile) getting started - A session is one live task or terminal that agentd keeps in the list. - A harness is the runtime for a session: zarvis, codex, claude, or shell. + A session is one live task or terminal that construct keeps in the list. + A harness is the runtime for a session: smith, codex, claude, or shell. The left pane selects sessions; the right pane shows the selected session. Use C-x C-f to create a session, then choose a harness. Use C-x x for the command palette when you forget a shortcut. @@ -5719,12 +5741,12 @@ emacs keymap (default; AGENTD_KEYMAP=vim for vim profile) palette commands: new send delete rename diff border zoom interrupt refresh harnesses help ? toggle this help - C-x C-c / q quit + C-x C-c quit When the right pane is showing a PTY-backed session (shell / interactive claude / interactive codex) and focus is on the view, keystrokes go to the child. `C-x` is the escape prefix — start any `C-x …` chord above to run -an agentd command without changing focus. +an construct command without changing focus. "; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -6438,7 +6460,7 @@ fn session_should_animate_status(s: &SessionSummary, pty_active: bool, agent_act // // Shell / PTY-only harnesses have no agent-status signal and also sit // in `Running` while idle, so they keep the short PTY-activity gate. - if s.harness == "zarvis" { + if is_smith_like_harness(&s.harness) { agent_active } else { pty_active @@ -7149,31 +7171,21 @@ mod tests { true, ); let rendered: Vec<_> = lines.iter().map(line_text).collect(); - assert!( - rendered - .iter() - .any(|line| line.contains("◉ [d] Start demo")) - ); - assert!( - rendered - .iter() - .any(|line| line.contains("│ ✓ Prepare demo workspace")) - ); - assert!( - rendered - .iter() - .any(|line| line.contains("│ ○ Record demo")) - ); - assert!( - rendered - .iter() - .any(|line| line.contains("○ [r] Run checks")) - ); - assert!( - rendered - .iter() - .any(|line| line.contains("• Plain milestone")) - ); + assert!(rendered + .iter() + .any(|line| line.contains("◉ [d] Start demo"))); + assert!(rendered + .iter() + .any(|line| line.contains("│ ✓ Prepare demo workspace"))); + assert!(rendered + .iter() + .any(|line| line.contains("│ ○ Record demo"))); + assert!(rendered + .iter() + .any(|line| line.contains("○ [r] Run checks"))); + assert!(rendered + .iter() + .any(|line| line.contains("• Plain milestone"))); assert_eq!(hits.len(), 2); assert_eq!(hits[0].action.id, "start-demo"); assert_eq!(hits[0].action.key.as_deref(), Some("d")); diff --git a/crates/cli/src/upgrade.rs b/crates/cli/src/upgrade.rs index 6485e98a..8dcfccf3 100644 --- a/crates/cli/src/upgrade.rs +++ b/crates/cli/src/upgrade.rs @@ -98,7 +98,9 @@ async fn fetch_latest() -> Option { .build() .ok()?; let resp = client - .get(format!("https://api.github.com/repos/{REPO}/releases/latest")) + .get(format!( + "https://api.github.com/repos/{REPO}/releases/latest" + )) .header("User-Agent", "agentd-cli") .header("Accept", "application/vnd.github+json") .send() @@ -143,7 +145,7 @@ pub fn cached_update_notice() -> Option { /// The notice string for `latest` vs `current`, or `None` when `latest` is /// not strictly newer (or unparseable). fn notice_for(latest: &str, current: &str) -> Option { - is_newer(latest, current).then(|| format!("↑ agentd {latest} · agent upgrade")) + is_newer(latest, current).then(|| format!("↑ construct {latest} · agent upgrade")) } // --- `agent upgrade` ------------------------------------------------------- @@ -161,11 +163,15 @@ pub async fn run( if check { match fetch_latest().await { Some(latest) if is_newer(&latest, current) => { - println!("agentd {latest} available (you have {current}). Run `agent upgrade`."); + println!("construct {latest} available (you have {current}). Run `agent upgrade`."); + } + Some(latest) => { + println!("up to date (construct {current}; latest {latest}).") } - Some(latest) => println!("up to date (agentd {current}; latest {latest})."), None => { - println!("could not determine the latest version (offline, or no public release yet).") + println!( + "could not determine the latest version (offline, or no public release yet)." + ) } } return Ok(()); @@ -198,7 +204,7 @@ pub async fn run( } let target = version.as_deref().unwrap_or("latest"); - println!("Upgrading agentd to {target} in {}", dir.display()); + println!("Upgrading construct to {target} in {}", dir.display()); let mut cmd = tokio::process::Command::new("sh"); cmd.arg(script.path()).env("AGENTD_BIN_DIR", &dir); @@ -222,7 +228,7 @@ pub async fn run( Err(_) => println!("Upgraded. (No running daemon to restart.)"), } } else { - println!("Upgraded. Run `/agentd restart` in the TUI (or restart the daemon) to apply."); + println!("Upgraded. Run `/construct restart` in the TUI (or restart the daemon) to apply."); } Ok(()) } @@ -257,7 +263,7 @@ mod tests { fn notice_only_when_a_newer_version_exists() { assert_eq!( notice_for("0.2.0", "0.1.0").as_deref(), - Some("↑ agentd 0.2.0 · agent upgrade") + Some("↑ construct 0.2.0 · agent upgrade") ); assert_eq!(notice_for("0.1.0", "0.1.0"), None); assert_eq!(notice_for("0.1.0", "0.2.0"), None); diff --git a/crates/cli/tests/reconnect.rs b/crates/cli/tests/reconnect.rs index 40a3717d..fb57f2af 100644 --- a/crates/cli/tests/reconnect.rs +++ b/crates/cli/tests/reconnect.rs @@ -1,10 +1,10 @@ -use tempfile::tempdir; -use tokio::net::UnixListener; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, split}; -use serde_json::Value; use agentd_client::Client; use agentd_protocol::ipc_method; +use serde_json::Value; use std::time::Instant; +use tempfile::tempdir; +use tokio::io::{split, AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::net::UnixListener; // Robust reconnect integration test using a mock Unix-socket daemon. // - Each mock server accepts one connection, replies to RPCs with @@ -128,6 +128,6 @@ async fn test_reconnect_flow() { match res { Ok(Ok(_)) => {} Ok(Err(e)) => panic!("test inner error: {e}"), - Err(_) => panic!("test timed out") + Err(_) => panic!("test timed out"), } } diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 67d8b6f1..53064029 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -3,17 +3,15 @@ use agentd_protocol::jsonrpc::{self, MessageKind}; use agentd_protocol::{ ipc_method, transport, ChatViewerActiveResult, ClientView, CreateSessionParams, DiffResult, - ErrorObject, GroupCreateParams, - GroupDeleteParams, GroupMoveParams, GroupRenameParams, GroupSetCollapsedParams, GroupSummary, - HarnessInfo, MoveDirection, Notification, PingResult, ProjectCreateParams, ProjectCreateResult, - ProjectDeleteParams, ProjectMoveParams, ProjectRenameParams, ProjectSetCollapsedParams, - ProjectSummary, PtyReplayResult, Request, Response, SessionAttachClipboardParams, - SessionAttachClipboardResult, SessionDetail, SessionEmitEventParams, SessionIdParams, - SessionInputParams, SessionMoveParams, SessionPtyInputParams, SessionPtyResizeParams, - SessionSetApprovalModeParams, SessionSetPinnedParams, SessionSetProjectParams, - SessionSetTitleParams, SessionSetViewParams, SessionSummary, SessionToolDecisionParams, - SubscribeParams, - TranscriptParams, TranscriptResult, + ErrorObject, GroupCreateParams, GroupDeleteParams, GroupMoveParams, GroupRenameParams, + GroupSetCollapsedParams, GroupSummary, HarnessInfo, MoveDirection, Notification, PingResult, + ProjectCreateParams, ProjectCreateResult, ProjectDeleteParams, ProjectMoveParams, + ProjectRenameParams, ProjectSetCollapsedParams, ProjectSummary, PtyReplayResult, Request, + Response, SessionAttachClipboardParams, SessionAttachClipboardResult, SessionDetail, + SessionEmitEventParams, SessionIdParams, SessionInputParams, SessionMoveParams, + SessionPtyInputParams, SessionPtyResizeParams, SessionSetApprovalModeParams, + SessionSetPinnedParams, SessionSetProjectParams, SessionSetTitleParams, SessionSetViewParams, + SessionSummary, SessionToolDecisionParams, SubscribeParams, TranscriptParams, TranscriptResult, }; use anyhow::{anyhow, Context, Result}; use serde::de::DeserializeOwned; diff --git a/crates/daemon/src/config.rs b/crates/daemon/src/config.rs index 14842196..84f7fbec 100644 --- a/crates/daemon/src/config.rs +++ b/crates/daemon/src/config.rs @@ -6,7 +6,7 @@ use anyhow::{Context, Result}; use serde::Deserialize; use std::collections::{BTreeMap, HashMap}; -pub const DEFAULT_CONFIG_TOML: &str = r#"# agentd configuration +pub const DEFAULT_CONFIG_TOML: &str = r#"# construct configuration # Each [adapters.] entry registers a harness. The daemon looks up the # binary in PATH or alongside the daemon binary. Use `binary = "/abs/path"` # to override. @@ -23,12 +23,14 @@ pub const DEFAULT_CONFIG_TOML: &str = r#"# agentd configuration # binary = "agentd-adapter-codex" # description = "OpenAI Codex" -# [adapters.zarvis.env] +# [adapters.smith.env] # # Per-harness env vars merged into every spawned session. Lets # # operators set defaults like the model from config.toml instead # # of needing to export them in the shell that launches the daemon. # # Per-session env (`agent new --env KEY=VAL`) takes precedence. # # AGENTD_ZARVIS_MODEL = "codex-oauth:gpt-5.5" +# # +# # "zarvis" remains supported as a compatibility alias. # [defaults] # worktree = false # default value of session.worktree if not specified @@ -49,7 +51,7 @@ pub struct OrchestratorConfig { /// Which harness backs the daemon-created orchestrator session. /// `None` (TOML: `harness = ""` or `enabled = false`) disables /// the orchestrator entirely — clients then fall back to the - /// static command palette. Default: `"zarvis"`. + /// static command palette. Default: `"smith"`. #[serde(default)] pub harness: Option, /// Hard kill switch; set to `false` to disable the orchestrator @@ -65,7 +67,7 @@ fn default_orchestrator_enabled() -> bool { impl Default for OrchestratorConfig { fn default() -> Self { Self { - harness: Some("zarvis".to_string()), + harness: Some("smith".to_string()), enabled: true, } } @@ -97,11 +99,13 @@ pub struct AdapterConfig { /// per-session env takes precedence so an explicit /// `agent new --env KEY=VAL` still overrides. /// - /// Example: pin every new zarvis session to use the Codex OAuth + /// Example: pin every new smith (formerly zarvis) session to use the + /// Codex OAuth path (subscription-billed) instead of the heuristic + /// fallback: /// path (subscription-billed) instead of the heuristic fallback: /// /// ```toml - /// [adapters.zarvis] + /// [adapters.smith] /// env = { AGENTD_ZARVIS_MODEL = "codex-oauth:gpt-5.5" } /// ``` #[serde(default)] @@ -142,10 +146,15 @@ pub const BUILTIN_ADAPTERS: &[BuiltinAdapter] = &[ description: "Google Antigravity (wraps the `agy` CLI)", }, BuiltinAdapter { - name: "zarvis", + name: "smith", binary: "agentd-adapter-zarvis", description: "Built-in multi-provider agent (OpenAI / Anthropic / Ollama)", }, + BuiltinAdapter { + name: "zarvis", + binary: "agentd-adapter-zarvis", + description: "Compatibility alias for smith", + }, ]; impl Config { @@ -154,8 +163,7 @@ impl Config { let mut cfg = if path.exists() { let s = std::fs::read_to_string(&path) .with_context(|| format!("read {}", path.display()))?; - toml::from_str::(&s) - .with_context(|| format!("parse {}", path.display()))? + toml::from_str::(&s).with_context(|| format!("parse {}", path.display()))? } else { Self::default() }; diff --git a/crates/daemon/src/loops.rs b/crates/daemon/src/loops.rs index 05f08579..be0c5c39 100644 --- a/crates/daemon/src/loops.rs +++ b/crates/daemon/src/loops.rs @@ -131,18 +131,14 @@ impl LoopRegistry { /// state. Called after every mutation. async fn persist_session(&self, session_id: &str) -> Result<()> { let g = self.inner.read().await; - let session_loops: Vec<&Loop> = - g.values().filter(|l| l.session_id == session_id).collect(); + let session_loops: Vec<&Loop> = g.values().filter(|l| l.session_id == session_id).collect(); let path = self.path_for(session_id); let parent = path.parent().context("loops.json parent")?; - std::fs::create_dir_all(parent) - .with_context(|| format!("create {}", parent.display()))?; + std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?; let tmp = path.with_extension("json.tmp"); let json = serde_json::to_string_pretty(&session_loops)?; - std::fs::write(&tmp, json) - .with_context(|| format!("write {}", tmp.display()))?; - std::fs::rename(&tmp, &path) - .with_context(|| format!("rename {}", path.display()))?; + std::fs::write(&tmp, json).with_context(|| format!("write {}", tmp.display()))?; + std::fs::rename(&tmp, &path).with_context(|| format!("rename {}", path.display()))?; Ok(()) } @@ -162,7 +158,11 @@ impl LoopRegistry { pub async fn list(&self, session_id: Option<&str>) -> Vec { let g = self.inner.read().await; match session_id { - Some(sid) => g.values().filter(|l| l.session_id == sid).cloned().collect(), + Some(sid) => g + .values() + .filter(|l| l.session_id == sid) + .cloned() + .collect(), None => g.values().cloned().collect(), } } @@ -273,9 +273,7 @@ pub async fn run_scheduler( registry: Arc, ) { let mut interval = tokio::time::interval(Duration::from_millis(SCHEDULER_TICK_MS)); - interval.set_missed_tick_behavior( - tokio::time::MissedTickBehavior::Delay, - ); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); loop { interval.tick().await; let now_ms = Utc::now().timestamp_millis(); @@ -306,10 +304,7 @@ pub async fn run_scheduler( } // Fire: synthesize a user input. The adapter's inbox // queues it for the next turn boundary. - match manager - .send_input(&l.session_id, l.prompt.clone()) - .await - { + match manager.send_input(&l.session_id, l.prompt.clone()).await { Ok(()) => { if let Err(e) = registry.mark_fired(&l.id, now_ms).await { tracing::warn!( @@ -347,10 +342,7 @@ pub async fn run_scheduler( /// - `every 10s hi` → 10s interval, no expiry /// - `30s for 5min check` → 30s interval, expires in 5min, prompt="check" /// - `hello` → None (no interval token recognized) -pub fn parse_slash_spec( - input: &str, - now_ms: i64, -) -> Option<(LoopSpec, Option, String)> { +pub fn parse_slash_spec(input: &str, now_ms: i64) -> Option<(LoopSpec, Option, String)> { let mut tokens = input.split_whitespace().peekable(); // Strip optional "every" if matches!(tokens.peek().copied(), Some("every")) { @@ -417,8 +409,7 @@ mod tests { #[test] fn parses_for_expiry() { - let (spec, exp, prompt) = - parse_slash_spec("30s for 5min check", 1000).unwrap(); + let (spec, exp, prompt) = parse_slash_spec("30s for 5min check", 1000).unwrap(); assert!(matches!(spec, LoopSpec::Interval { seconds: 30 })); assert_eq!(exp, Some(1000 + 5 * 60 * 1000)); assert_eq!(prompt, "check"); @@ -431,8 +422,7 @@ mod tests { #[test] fn parses_hour_unit() { - let (spec, _, prompt) = - parse_slash_spec("2hours summarize the day", 0).unwrap(); + let (spec, _, prompt) = parse_slash_spec("2hours summarize the day", 0).unwrap(); assert!(matches!(spec, LoopSpec::Interval { seconds: 7200 })); assert_eq!(prompt, "summarize the day"); } diff --git a/crates/daemon/src/server.rs b/crates/daemon/src/server.rs index a017a5be..253dd9dd 100644 --- a/crates/daemon/src/server.rs +++ b/crates/daemon/src/server.rs @@ -6,17 +6,15 @@ use crate::session::{BroadcastMsg, SessionManager}; use agentd_protocol::jsonrpc::{self, MessageKind}; use agentd_protocol::{ ipc_method, ipc_notif, transport, ChatViewerActiveResult, CreateSessionParams, ErrorObject, - GroupCreateParams, - GroupCreateResult, GroupDeleteParams, GroupMoveParams, GroupRenameParams, + GroupCreateParams, GroupCreateResult, GroupDeleteParams, GroupMoveParams, GroupRenameParams, GroupSetCollapsedParams, Notification, PingResult, ProjectCreateParams, ProjectCreateResult, ProjectDeleteParams, ProjectDeletedNotificationPayload, ProjectMoveParams, ProjectRenameParams, ProjectSetCollapsedParams, ProjectStateNotificationPayload, PtyReplayParams, Request, Response, SessionAttachClipboardParams, SessionIdParams, SessionInputParams, SessionMoveParams, SessionPtyInputParams, SessionPtyResizeParams, SessionSetApprovalModeParams, SessionSetGroupParams, SessionSetPinnedParams, SessionSetProjectParams, SessionSetTitleParams, - SessionSetViewParams, - SessionToolActionParams, SessionToolDecisionParams, SubscribeParams, TranscriptParams, - IPC_VERSION, + SessionSetViewParams, SessionToolActionParams, SessionToolDecisionParams, SubscribeParams, + TranscriptParams, IPC_VERSION, }; use anyhow::{Context, Result}; use futures::{SinkExt as _, StreamExt as _}; diff --git a/crates/daemon/src/session.rs b/crates/daemon/src/session.rs index 55e49ce7..c7c37687 100644 --- a/crates/daemon/src/session.rs +++ b/crates/daemon/src/session.rs @@ -5,12 +5,12 @@ use crate::config::Config; use crate::storage::Storage; use crate::worktree; use agentd_protocol::{ - ahp_method, ClientView, CreateSessionParams, DeletedNotificationPayload, EventNotificationPayload, - GroupDeletedNotificationPayload, GroupStateNotificationPayload, GroupSummary, HarnessInfo, - MessageRole, MoveDirection, PtyReplayResult, PtySize, SessionAttachClipboardParams, - SessionAttachClipboardResult, SessionDetail, SessionEmitEventParams, SessionEvent, - SessionStartParams, SessionState, SessionSummary, SessionWidgetDeleteParams, - StateNotificationPayload, TimestampedEvent, TranscriptResult, + ahp_method, ClientView, CreateSessionParams, DeletedNotificationPayload, + EventNotificationPayload, GroupDeletedNotificationPayload, GroupStateNotificationPayload, + GroupSummary, HarnessInfo, MessageRole, MoveDirection, PtyReplayResult, PtySize, + SessionAttachClipboardParams, SessionAttachClipboardResult, SessionDetail, + SessionEmitEventParams, SessionEvent, SessionStartParams, SessionState, SessionSummary, + SessionWidgetDeleteParams, StateNotificationPayload, TimestampedEvent, TranscriptResult, }; use anyhow::{anyhow, Context, Result}; use base64::Engine as _; @@ -424,6 +424,14 @@ fn should_record_pty_user_message(harness: &str) -> bool { matches!(harness, "claude" | "antigravity") } +fn smith_adapter_name(name: &str) -> &str { + if name == "zarvis" { + "smith" + } else { + name + } +} + impl SessionEntry { pub fn is_deleted(&self) -> bool { self.deleted.load(Ordering::SeqCst) @@ -1243,16 +1251,17 @@ impl SessionManager { } pub async fn create(self: &Arc, params: CreateSessionParams) -> Result { + let harness = smith_adapter_name(¶ms.harness); let adapter_cfg = self .config .adapters - .get(¶ms.harness) + .get(harness) .ok_or_else(|| anyhow!("unknown harness: {}", params.harness))? .clone(); let binary_spec = adapter_cfg .binary .clone() - .unwrap_or_else(|| params.harness.clone()); + .unwrap_or_else(|| harness.to_string()); let binary = locate_binary(&binary_spec) .ok_or_else(|| anyhow!("adapter binary not found: {}", binary_spec))?; @@ -1279,7 +1288,7 @@ impl SessionManager { let mut summary = SessionSummary { id: id.clone(), - harness: params.harness.clone(), + harness: harness.to_string(), cwd: effective_cwd.to_string_lossy().to_string(), title: params.title.clone(), state: SessionState::Pending, @@ -1368,7 +1377,7 @@ impl SessionManager { self.install_memory_env(&mut env_with_meta, params.group_id.as_deref()); let (adapter, info) = Adapter::spawn_reconnectable( - params.harness.clone(), + harness.to_string(), binary, combined_args, env_with_meta.clone(), @@ -1376,7 +1385,7 @@ impl SessionManager { msg_tx.clone(), ) .await - .with_context(|| format!("spawn adapter for {}", params.harness))?; + .with_context(|| format!("spawn adapter for {}", harness))?; // Apply capability-derived info. if summary.model.is_none() { @@ -1805,8 +1814,7 @@ impl SessionManager { loop { tokio::time::sleep(RESPAWN_REDRAW_POLL).await; let last = entry_for_redraw.summary.read().await.last_pty_at_ms; - if resume_redraw_ready(last, Utc::now().timestamp_millis(), started.elapsed()) - { + if resume_redraw_ready(last, Utc::now().timestamp_millis(), started.elapsed()) { break; } } @@ -2353,10 +2361,16 @@ impl SessionManager { if prompt.trim().is_empty() { return; } - let Some(zarvis_cfg) = self.config.adapters.get("zarvis").cloned() else { + let Some(zi) = self + .config + .adapters + .get("smith") + .or_else(|| self.config.adapters.get("zarvis")) + .cloned() + else { return; }; - let binary_spec = zarvis_cfg + let binary_spec = zi .binary .clone() .unwrap_or_else(|| "agentd-adapter-zarvis".to_string()); @@ -3430,7 +3444,7 @@ fn effective_mode(params: &CreateSessionParams) -> String { fn builtin_harness_capabilities(name: &str) -> agentd_protocol::Capabilities { match name { - "shell" | "claude" | "codex" | "zarvis" => agentd_protocol::Capabilities { + "shell" | "claude" | "codex" | "smith" | "zarvis" => agentd_protocol::Capabilities { supports_pty: true, ..Default::default() }, @@ -3597,11 +3611,23 @@ mod tests { // Nothing drawn yet, well under the cap → wait. assert!(!resume_redraw_ready(None, now, Duration::from_millis(0))); // Output 50ms ago (< settle) → still drawing, wait. - assert!(!resume_redraw_ready(Some(now - 50), now, Duration::from_secs(1))); + assert!(!resume_redraw_ready( + Some(now - 50), + now, + Duration::from_secs(1) + )); // Quiet for exactly the settle window → fire. - assert!(resume_redraw_ready(Some(now - settle), now, Duration::from_secs(1))); + assert!(resume_redraw_ready( + Some(now - settle), + now, + Duration::from_secs(1) + )); // Quiet well past settle → fire. - assert!(resume_redraw_ready(Some(now - 5_000), now, Duration::from_secs(1))); + assert!(resume_redraw_ready( + Some(now - 5_000), + now, + Duration::from_secs(1) + )); // Never settles (recent output) but hit the hard cap → fire anyway. assert!(resume_redraw_ready(Some(now), now, RESPAWN_REDRAW_MAX_WAIT)); // Never drew anything, but hit the hard cap → fire anyway. diff --git a/crates/e2e/src/lib.rs b/crates/e2e/src/lib.rs index 1af13bb0..3f2b4d26 100644 --- a/crates/e2e/src/lib.rs +++ b/crates/e2e/src/lib.rs @@ -188,10 +188,7 @@ impl Daemon { let deadline = Instant::now() + Duration::from_secs(15); while !socket.exists() { if Instant::now() > deadline { - anyhow::bail!( - "daemon did not bind {} within 15s", - socket.display() - ); + anyhow::bail!("daemon did not bind {} within 15s", socket.display()); } tokio::time::sleep(Duration::from_millis(50)).await; } @@ -345,8 +342,7 @@ impl Tui { .map_err(|e| anyhow!("clone_reader: {e}"))?; let parser = Arc::new(Mutex::new(vt100::Parser::new( - size.rows, - size.cols, + size.rows, size.cols, // Scrollback budget: 1000 lines is plenty for any // assertion the tests do today and small enough not // to hold huge memory. @@ -403,11 +399,8 @@ impl Tui { // byte-for-byte log. Most PTY // output is UTF-8 anyway. let chunk = String::from_utf8_lossy(&buf[..n]); - let event = serde_json::json!([ - start.elapsed().as_secs_f64(), - "o", - chunk, - ]); + let event = + serde_json::json!([start.elapsed().as_secs_f64(), "o", chunk,]); let _ = writeln!(w, "{event}"); } } @@ -545,15 +538,27 @@ fn bin_path(name: &str) -> Result { // spawns release daemons/TUIs (which is what perf benchmarks // want — debug renders are an order of magnitude slower and // not representative of the real experience). - let profile = if cfg!(debug_assertions) { "debug" } else { "release" }; + let profile = if cfg!(debug_assertions) { + "debug" + } else { + "release" + }; let p = target_dir().join(profile).join(&exe); if !p.exists() { anyhow::bail!( "expected {} — run `cargo build --workspace{}` before \ `cargo test -p agentd-e2e{}`", p.display(), - if profile == "release" { " --release" } else { "" }, - if profile == "release" { " --release" } else { "" }, + if profile == "release" { + " --release" + } else { + "" + }, + if profile == "release" { + " --release" + } else { + "" + }, ); } Ok(p) diff --git a/crates/e2e/tests/key_latency.rs b/crates/e2e/tests/key_latency.rs index 67ccb6f7..c749462b 100644 --- a/crates/e2e/tests/key_latency.rs +++ b/crates/e2e/tests/key_latency.rs @@ -55,9 +55,8 @@ async fn repeated_key_latency() { .expect("create shell session"); eprintln!("created shell session {session_id}"); - let mut tui = Tui::spawn_with_recording(&d.socket, "key_latency") - .expect("spawn TUI"); - tui.wait_for("agentd focus:", Duration::from_secs(15)) + let mut tui = Tui::spawn_with_recording(&d.socket, "key_latency").expect("spawn TUI"); + tui.wait_for("construct focus:", Duration::from_secs(15)) .await .expect("modeline"); @@ -175,9 +174,13 @@ async fn measure_type_burst(tui: &mut Tui) -> Duration { for _ in 0..MARKER_LEN { tui.send(b"Z").ok(); } - wait_until(tui, |s| count_char(s, 'Z') >= MARKER_LEN, Duration::from_secs(30)) - .await - .expect("type burst never fully echoed"); + wait_until( + tui, + |s| count_char(s, 'Z') >= MARKER_LEN, + Duration::from_secs(30), + ) + .await + .expect("type burst never fully echoed"); t0.elapsed() } @@ -203,9 +206,13 @@ async fn measure_arrow_burst(tui: &mut Tui) -> Duration { for _ in 0..MARKER_LEN { tui.send(b"Y").ok(); } - wait_until(tui, |s| count_char(s, 'Y') >= MARKER_LEN, Duration::from_secs(10)) - .await - .expect("arrow marker never rendered"); + wait_until( + tui, + |s| count_char(s, 'Y') >= MARKER_LEN, + Duration::from_secs(10), + ) + .await + .expect("arrow marker never rendered"); let t0 = Instant::now(); for _ in 0..MARKER_LEN { @@ -234,4 +241,3 @@ async fn wait_until( tokio::time::sleep(Duration::from_millis(1)).await; } } - diff --git a/crates/e2e/tests/multi_session_latency.rs b/crates/e2e/tests/multi_session_latency.rs index 867144ff..9b79ad54 100644 --- a/crates/e2e/tests/multi_session_latency.rs +++ b/crates/e2e/tests/multi_session_latency.rs @@ -68,9 +68,8 @@ async fn typing_latency_under_background_floods() { ); } - let mut tui = Tui::spawn_with_recording(&d.socket, "multi_session_latency") - .expect("spawn TUI"); - tui.wait_for("agentd focus:", Duration::from_secs(15)) + let mut tui = Tui::spawn_with_recording(&d.socket, "multi_session_latency").expect("spawn TUI"); + tui.wait_for("construct focus:", Duration::from_secs(15)) .await .expect("modeline"); @@ -79,7 +78,11 @@ async fn typing_latency_under_background_floods() { tokio::time::sleep(Duration::from_millis(400)).await; tui.send(b"\r").ok(); tokio::time::sleep(Duration::from_millis(800)).await; - assert!(probe(&mut tui).await, "PTY capture not active:\n{}", tui.screen()); + assert!( + probe(&mut tui).await, + "PTY capture not active:\n{}", + tui.screen() + ); tui.send(b"\x15").ok(); // clear line tokio::time::sleep(Duration::from_millis(300)).await; @@ -104,7 +107,10 @@ async fn typing_latency_under_background_floods() { let under_load = measure_backspace_burst(&mut tui).await; eprintln!("\n=== focused typing: backspace x{MARKER_LEN} settle ==="); - eprintln!("baseline (idle bg): {:.1} ms", baseline.as_secs_f64() * 1000.0); + eprintln!( + "baseline (idle bg): {:.1} ms", + baseline.as_secs_f64() * 1000.0 + ); match under_load { Some(d) => eprintln!( "under {BG_SESSIONS} flooding bg sessions: {:.1} ms ({:.1}x)", @@ -164,7 +170,11 @@ async fn measure_backspace_burst(tui: &mut Tui) -> Option { Some(t0.elapsed()) } -async fn wait_until(tui: &Tui, pred: impl Fn(&str) -> bool, timeout: Duration) -> anyhow::Result<()> { +async fn wait_until( + tui: &Tui, + pred: impl Fn(&str) -> bool, + timeout: Duration, +) -> anyhow::Result<()> { let deadline = Instant::now() + timeout; loop { if pred(&tui.screen()) { diff --git a/crates/e2e/tests/remote_control.rs b/crates/e2e/tests/remote_control.rs index e2bc9e14..bd3e7864 100644 --- a/crates/e2e/tests/remote_control.rs +++ b/crates/e2e/tests/remote_control.rs @@ -43,7 +43,10 @@ async fn remote_control_security_and_lifecycle() { "expected local URL, got {}", r.url ); - assert!(!r.password.is_empty(), "auto-gen password should be non-empty"); + assert!( + !r.password.is_empty(), + "auto-gen password should be non-empty" + ); let http = reqwest::Client::builder() .timeout(Duration::from_secs(5)) diff --git a/crates/e2e/tests/restart.rs b/crates/e2e/tests/restart.rs index 06e0e5ce..0dfadd6e 100644 --- a/crates/e2e/tests/restart.rs +++ b/crates/e2e/tests/restart.rs @@ -25,7 +25,9 @@ use agentd_e2e::{Daemon, Tui}; /// runs the new bytes, with the PID preserved. #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn restart_reloads_updated_binary() { - let d = Daemon::spawn_relocatable().await.expect("spawn relocatable daemon"); + let d = Daemon::spawn_relocatable() + .await + .expect("spawn relocatable daemon"); let pid = d.pid().expect("daemon pid"); // On Linux we can directly observe which inode the process is @@ -87,7 +89,9 @@ async fn restart_reloads_updated_binary() { // Non-Linux (local macOS dev): no /proc. The PID-preserved // + daemon-responsive checks above still demonstrate that // exec() of the on-disk path succeeded after the swap. - eprintln!("note: /proc unavailable; skipped inode assertion (PID + liveness checks passed)"); + eprintln!( + "note: /proc unavailable; skipped inode assertion (PID + liveness checks passed)" + ); } } @@ -100,11 +104,10 @@ async fn restart_reloads_updated_binary() { #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn tui_auto_reconnects_after_restart() { let d = Daemon::spawn().await.expect("spawn daemon"); - let mut tui = Tui::spawn_with_recording(&d.socket, "restart_tui_reconnect") - .expect("spawn TUI"); + let mut tui = Tui::spawn_with_recording(&d.socket, "restart_tui_reconnect").expect("spawn TUI"); // Connected: modeline drawn. - tui.wait_for("agentd focus:", Duration::from_secs(15)) + tui.wait_for("construct focus:", Duration::from_secs(15)) .await .expect("modeline never rendered"); @@ -207,7 +210,10 @@ async fn web_client_reconnects_to_same_url_after_restart() { .expect("evaluate") .into_value::() .unwrap_or(false); - assert!(xterm_present, "web client lost its bundled xterm after reconnect"); + assert!( + xterm_present, + "web client lost its bundled xterm after reconnect" + ); } // --------------------------------------------------------------------------- diff --git a/crates/e2e/tests/tui_smoke.rs b/crates/e2e/tests/tui_smoke.rs index 5adc06dc..6d366626 100644 --- a/crates/e2e/tests/tui_smoke.rs +++ b/crates/e2e/tests/tui_smoke.rs @@ -25,12 +25,11 @@ use agentd_e2e::{Daemon, Tui}; #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn tui_starts_and_quits() { let d = Daemon::spawn().await.expect("spawn daemon"); - let mut tui = Tui::spawn_with_recording(&d.socket, "tui_starts_and_quits") - .expect("spawn TUI"); + let mut tui = Tui::spawn_with_recording(&d.socket, "tui_starts_and_quits").expect("spawn TUI"); - // Modeline. The format starts with " agentd focus:" — see + // Modeline. The format starts with " construct focus:" — see // `render_modeline` in crates/cli/src/ui.rs. - tui.wait_for("agentd focus:", Duration::from_secs(15)) + tui.wait_for("construct focus:", Duration::from_secs(15)) .await .expect("modeline never rendered"); @@ -62,7 +61,7 @@ async fn tui_remote_control_popup_via_palette() { let mut tui = Tui::spawn_with_recording(&d.socket, "tui_remote_control_popup_via_palette") .expect("spawn TUI"); - tui.wait_for("agentd focus:", Duration::from_secs(15)) + tui.wait_for("construct focus:", Duration::from_secs(15)) .await .expect("modeline never rendered"); @@ -110,5 +109,9 @@ async fn tui_remote_control_popup_via_palette() { .wait_exit(Duration::from_secs(5)) .await .expect("TUI did not exit after q"); - assert!(status.success(), "TUI exited with non-success status: {:?}", status); + assert!( + status.success(), + "TUI exited with non-success status: {:?}", + status + ); } diff --git a/crates/e2e/tests/web_smoke.rs b/crates/e2e/tests/web_smoke.rs index 0d69826c..d96ef4c5 100644 --- a/crates/e2e/tests/web_smoke.rs +++ b/crates/e2e/tests/web_smoke.rs @@ -265,9 +265,20 @@ async fn web_client_loads_and_websocket_connects() { .expect("evaluate terminal fast-open") .into_value() .expect("json value"); - assert_eq!(fast_open["replayCalls"][0]["max_bytes"], 128 * 1024, "{fast_open:?}"); - assert_eq!(fast_open["replayCalls"][1]["max_bytes"], 128 * 1024, "{fast_open:?}"); - assert_eq!(fast_open["replayCalls"][1]["before_offset"], 1048576, "{fast_open:?}"); + assert_eq!( + fast_open["replayCalls"][0]["max_bytes"], + 128 * 1024, + "{fast_open:?}" + ); + assert_eq!( + fast_open["replayCalls"][1]["max_bytes"], + 128 * 1024, + "{fast_open:?}" + ); + assert_eq!( + fast_open["replayCalls"][1]["before_offset"], 1048576, + "{fast_open:?}" + ); assert_eq!(fast_open["afterInitialHidden"], false); assert_eq!(fast_open["afterOlderHidden"], true); diff --git a/crates/e2e/tests/zoom_latency.rs b/crates/e2e/tests/zoom_latency.rs index 4fd31f3c..1afb88d8 100644 --- a/crates/e2e/tests/zoom_latency.rs +++ b/crates/e2e/tests/zoom_latency.rs @@ -50,9 +50,8 @@ async fn zoom_toggle_latency() { .await .expect("create shell session"); - let mut tui = Tui::spawn_with_recording(&d.socket, "zoom_latency") - .expect("spawn TUI"); - tui.wait_for("agentd focus:", Duration::from_secs(15)) + let mut tui = Tui::spawn_with_recording(&d.socket, "zoom_latency").expect("spawn TUI"); + tui.wait_for("construct focus:", Duration::from_secs(15)) .await .expect("modeline"); diff --git a/crates/mcp/src/main.rs b/crates/mcp/src/main.rs index ea3b183f..a8730052 100644 --- a/crates/mcp/src/main.rs +++ b/crates/mcp/src/main.rs @@ -80,11 +80,7 @@ async fn run(client: Arc, session_id: Option) -> Result<()> { } } -async fn handle_request( - client: &Arc, - session_id: Option<&str>, - req: Request, -) -> Response { +async fn handle_request(client: &Arc, session_id: Option<&str>, req: Request) -> Response { let id = req.id.clone(); let params = req.params.clone().unwrap_or(serde_json::Value::Null); diff --git a/crates/protocol/src/adapter/policy.rs b/crates/protocol/src/adapter/policy.rs index 80e8dda4..3d6f8682 100644 --- a/crates/protocol/src/adapter/policy.rs +++ b/crates/protocol/src/adapter/policy.rs @@ -118,10 +118,7 @@ fn normalize(path: &Path) -> Vec { out } -fn starts_with_components( - path: &[std::ffi::OsString], - prefix: &[std::ffi::OsString], -) -> bool { +fn starts_with_components(path: &[std::ffi::OsString], prefix: &[std::ffi::OsString]) -> bool { path.len() >= prefix.len() && path.iter().zip(prefix).all(|(a, b)| a == b) } diff --git a/crates/protocol/src/jsonrpc.rs b/crates/protocol/src/jsonrpc.rs index bb79da52..11572b94 100644 --- a/crates/protocol/src/jsonrpc.rs +++ b/crates/protocol/src/jsonrpc.rs @@ -20,7 +20,11 @@ pub struct Request { } impl Request { - pub fn new(id: impl Into, method: impl Into, params: Option) -> Self { + pub fn new( + id: impl Into, + method: impl Into, + params: Option, + ) -> Self { Self { jsonrpc: JSONRPC_VERSION.to_string(), id: id.into(), @@ -101,7 +105,10 @@ impl ErrorObject { } pub fn method_not_found(method: &str) -> Self { - Self::new(error_codes::METHOD_NOT_FOUND, format!("method not found: {method}")) + Self::new( + error_codes::METHOD_NOT_FOUND, + format!("method not found: {method}"), + ) } pub fn invalid_params(msg: impl Into) -> Self { diff --git a/crates/protocol/src/paths.rs b/crates/protocol/src/paths.rs index 018cd835..5877a0be 100644 --- a/crates/protocol/src/paths.rs +++ b/crates/protocol/src/paths.rs @@ -137,5 +137,7 @@ pub fn locate_sibling_binary(name: &str) -> Option { } fn env_dir(name: &str) -> Option { - std::env::var_os(name).map(PathBuf::from).filter(|p| !p.as_os_str().is_empty()) + std::env::var_os(name) + .map(PathBuf::from) + .filter(|p| !p.as_os_str().is_empty()) } diff --git a/crates/protocol/src/slash.rs b/crates/protocol/src/slash.rs index 6b2ce42b..2e230811 100644 --- a/crates/protocol/src/slash.rs +++ b/crates/protocol/src/slash.rs @@ -284,14 +284,14 @@ pub const COMMANDS: &[SlashCommand] = &[ }, SlashCommand { id: CommandId::Agentd, - name: "/agentd", - aliases: &[], + name: "/construct", + aliases: &["agentd"], args: Args::Required, routing: Routing::Client, visibility: ModelVisibility::Hidden, - transcript: TranscriptPolicy::AuditOnly, // forensics: e.g. `/agentd restart` + transcript: TranscriptPolicy::AuditOnly, // forensics: e.g. `/construct restart` render: Render::SystemNote, - help: "Daemon control (e.g. /agentd restart)", + help: "Daemon control (e.g. /construct restart)", in_popup: true, }, SlashCommand { @@ -395,7 +395,11 @@ mod tests { // Name and every alias must resolve back to this exact row. assert_eq!(SlashCommand::resolve(c.name).map(|r| r.id), Some(c.id)); for a in c.aliases { - assert_eq!(SlashCommand::resolve(a).map(|r| r.id), Some(c.id), "alias {a}"); + assert_eq!( + SlashCommand::resolve(a).map(|r| r.id), + Some(c.id), + "alias {a}" + ); } assert_eq!(SlashCommand::by_id(c.id).name, c.name); } @@ -403,9 +407,18 @@ mod tests { #[test] fn resolve_is_slash_and_case_insensitive() { - assert_eq!(SlashCommand::resolve("/Zoom").map(|c| c.id), Some(CommandId::Zoom)); - assert_eq!(SlashCommand::resolve("zoom").map(|c| c.id), Some(CommandId::Zoom)); - assert_eq!(SlashCommand::resolve("fullscreen").map(|c| c.id), Some(CommandId::Zoom)); + assert_eq!( + SlashCommand::resolve("/Zoom").map(|c| c.id), + Some(CommandId::Zoom) + ); + assert_eq!( + SlashCommand::resolve("zoom").map(|c| c.id), + Some(CommandId::Zoom) + ); + assert_eq!( + SlashCommand::resolve("fullscreen").map(|c| c.id), + Some(CommandId::Zoom) + ); assert!(SlashCommand::resolve("/definitely-not-a-command").is_none()); assert!(SlashCommand::resolve("/").is_none()); } From 40aad43d9142dc2392975ee947fbc2f7940ebf0b Mon Sep 17 00:00:00 2001 From: Edwin Date: Sat, 6 Jun 2026 15:39:09 -0700 Subject: [PATCH 2/3] Remove /agentd slash alias --- crates/cli/src/app.rs | 2 +- crates/protocol/src/slash.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index 86cc7805..9f088a77 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -6855,7 +6855,7 @@ impl App { .collect(); self.set_status(format!("harnesses: {}", names.join(", "))); } - "agentd" | "construct" => { + "construct" => { // Subcommand dispatch: // // /construct restart [binary path] diff --git a/crates/protocol/src/slash.rs b/crates/protocol/src/slash.rs index 2e230811..4424bffc 100644 --- a/crates/protocol/src/slash.rs +++ b/crates/protocol/src/slash.rs @@ -285,7 +285,7 @@ pub const COMMANDS: &[SlashCommand] = &[ SlashCommand { id: CommandId::Agentd, name: "/construct", - aliases: &["agentd"], + aliases: &[], args: Args::Required, routing: Routing::Client, visibility: ModelVisibility::Hidden, From e43dbe9f176b24be17fae9b5d513f267fcb7bf8e Mon Sep 17 00:00:00 2001 From: Edwin Date: Sat, 6 Jun 2026 17:19:07 -0700 Subject: [PATCH 3/3] Improve legacy migration notice with copy/paste command --- .github/workflows/release.yml | 18 +- Cargo.lock | 184 ++++++++--------- README.md | 58 +++--- crates/adapter-antigravity/Cargo.toml | 4 +- crates/adapter-antigravity/src/main.rs | 28 +-- crates/adapter-claude/Cargo.toml | 4 +- crates/adapter-claude/src/main.rs | 46 ++--- crates/adapter-codex/Cargo.toml | 4 +- crates/adapter-codex/src/main.rs | 36 ++-- crates/adapter-shell/Cargo.toml | 4 +- crates/adapter-shell/src/main.rs | 10 +- crates/adapter-zarvis/Cargo.toml | 4 +- crates/adapter-zarvis/src/agent.rs | 18 +- crates/adapter-zarvis/src/compact.rs | 2 +- crates/adapter-zarvis/src/hooks.rs | 20 +- crates/adapter-zarvis/src/interactive.rs | 26 +-- crates/adapter-zarvis/src/interval_suggest.rs | 12 +- crates/adapter-zarvis/src/main.rs | 14 +- crates/adapter-zarvis/src/observe.rs | 2 +- crates/adapter-zarvis/src/persist.rs | 4 +- crates/adapter-zarvis/src/project_guide.rs | 10 +- .../src/provider/codex_oauth.rs | 18 +- .../adapter-zarvis/src/provider_watchdog.rs | 4 +- crates/adapter-zarvis/src/skills.rs | 12 +- crates/adapter-zarvis/src/tasks.rs | 8 +- crates/adapter-zarvis/src/title_mode.rs | 6 +- crates/adapter-zarvis/src/tools/agentd.rs | 4 +- crates/adapter-zarvis/src/tools/subagent.rs | 6 +- crates/cli/Cargo.toml | 4 +- crates/cli/src/app.rs | 83 +++++--- crates/cli/src/keymap.rs | 4 +- crates/cli/src/main.rs | 12 +- crates/cli/src/pty_render.rs | 4 +- crates/cli/src/theme.rs | 2 +- crates/cli/src/ui.rs | 28 +-- crates/cli/src/upgrade.rs | 30 +-- crates/cli/tests/reconnect.rs | 2 +- crates/client/src/lib.rs | 2 +- crates/daemon/Cargo.toml | 2 +- crates/daemon/assets/index.html | 4 +- crates/daemon/src/adapter.rs | 2 +- crates/daemon/src/config.rs | 76 ++++--- crates/daemon/src/loops.rs | 12 +- crates/daemon/src/main.rs | 186 +++++++++++++++++- crates/daemon/src/remote_supervisor.rs | 4 +- crates/daemon/src/session.rs | 76 +++---- crates/e2e/Cargo.toml | 2 +- crates/e2e/src/lib.rs | 70 +++---- crates/e2e/tests/key_latency.rs | 2 +- crates/e2e/tests/restart.rs | 4 +- crates/e2e/tests/tui_smoke.rs | 2 +- crates/e2e/tests/web_smoke.rs | 4 +- crates/mcp/Cargo.toml | 4 +- crates/mcp/src/main.rs | 19 +- crates/mcp/src/tools.rs | 16 +- crates/mcp/src/tools/browser.rs | 4 +- crates/protocol/src/adapter.rs | 50 ++--- crates/protocol/src/adapter/policy.rs | 4 +- crates/protocol/src/agent_context.rs | 20 +- crates/protocol/src/lib.rs | 8 +- crates/protocol/src/paths.rs | 61 ++++-- docs/RELEASING.md | 16 +- docs/architecture.md | 16 +- docs/configuration.md | 48 ++--- docs/generative-widgets.md | 4 +- docs/harnesses.md | 86 ++++---- docs/memory.md | 6 +- docs/remote-control.md | 4 +- docs/{zarvis.md => smith.md} | 30 +-- docs/unified-tool-layer.md | 40 ++-- examples/config.toml | 29 +-- install.sh | 34 ++-- scripts/smoke.sh | 34 ++-- scripts/test_agent.sh | 26 +-- scripts/test_agentd.sh | 57 +++--- 75 files changed, 1006 insertions(+), 793 deletions(-) rename docs/{zarvis.md => smith.md} (76%) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72219fcd..52980660 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,7 +21,7 @@ env: # The binaries that make up a release. The daemon resolves adapters as # siblings of its own path (see `locate_binary`), so all of these must # ship together and land in the same directory. - BINS: "agent agentd agentd-mcp agentd-adapter-shell agentd-adapter-claude agentd-adapter-codex agentd-adapter-antigravity agentd-adapter-zarvis" + BINS: "construct constructd construct-mcp construct-adapter-shell construct-adapter-claude construct-adapter-codex construct-adapter-antigravity construct-adapter-smith" jobs: verify: @@ -85,15 +85,15 @@ jobs: CC_aarch64_unknown_linux_gnu: ${{ matrix.cc }} run: | cargo build --release --locked --target "${{ matrix.target }}" \ - -p agentd -p agentd-cli -p agentd-mcp \ - -p agentd-adapter-shell -p agentd-adapter-claude \ - -p agentd-adapter-codex -p agentd-adapter-antigravity \ - -p agentd-adapter-zarvis + -p agentd -p agentd-cli -p construct-mcp \ + -p construct-adapter-shell -p construct-adapter-claude \ + -p construct-adapter-codex -p construct-adapter-antigravity \ + -p construct-adapter-smith - name: Package tarball + checksum run: | set -eu - stage="agentd-${{ matrix.target }}" + stage="constructd-${{ matrix.target }}" mkdir -p "$stage" for b in $BINS; do cp "target/${{ matrix.target }}/release/$b" "$stage/" @@ -109,10 +109,10 @@ jobs: - uses: actions/upload-artifact@v4 with: - name: agentd-${{ matrix.target }} + name: constructd-${{ matrix.target }} path: | - agentd-${{ matrix.target }}.tar.gz - agentd-${{ matrix.target }}.tar.gz.sha256 + constructd-${{ matrix.target }}.tar.gz + constructd-${{ matrix.target }}.tar.gz.sha256 if-no-files-found: error release: diff --git a/Cargo.lock b/Cargo.lock index 6209b028..1f7b6cbf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -35,78 +35,6 @@ dependencies = [ "which", ] -[[package]] -name = "agentd-adapter-antigravity" -version = "0.8.3" -dependencies = [ - "agentd-protocol", - "anyhow", - "serde_json", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "agentd-adapter-claude" -version = "0.8.3" -dependencies = [ - "agentd-protocol", - "anyhow", - "serde_json", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "agentd-adapter-codex" -version = "0.8.3" -dependencies = [ - "agentd-protocol", - "anyhow", - "serde_json", - "tokio", - "tracing", -] - -[[package]] -name = "agentd-adapter-shell" -version = "0.8.3" -dependencies = [ - "agentd-protocol", - "anyhow", - "serde_json", - "tokio", - "tracing", -] - -[[package]] -name = "agentd-adapter-zarvis" -version = "0.8.3" -dependencies = [ - "agentd-client", - "agentd-protocol", - "anyhow", - "async-trait", - "base64", - "bytes", - "chrono", - "eventsource-stream", - "futures", - "image", - "reqwest", - "serde", - "serde_json", - "tempfile", - "tokio", - "tokio-tungstenite", - "tracing", - "unicode-width", - "uuid", - "vt100", -] - [[package]] name = "agentd-cli" version = "0.8.3" @@ -166,26 +94,6 @@ dependencies = [ "vt100", ] -[[package]] -name = "agentd-mcp" -version = "0.8.3" -dependencies = [ - "agentd-client", - "agentd-protocol", - "anyhow", - "base64", - "futures", - "image", - "reqwest", - "serde", - "serde_json", - "thiserror 1.0.69", - "tokio", - "tokio-tungstenite", - "tracing", - "tracing-subscriber", -] - [[package]] name = "agentd-protocol" version = "0.8.3" @@ -585,6 +493,98 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "construct-adapter-antigravity" +version = "0.8.3" +dependencies = [ + "agentd-protocol", + "anyhow", + "serde_json", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "construct-adapter-claude" +version = "0.8.3" +dependencies = [ + "agentd-protocol", + "anyhow", + "serde_json", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "construct-adapter-codex" +version = "0.8.3" +dependencies = [ + "agentd-protocol", + "anyhow", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "construct-adapter-shell" +version = "0.8.3" +dependencies = [ + "agentd-protocol", + "anyhow", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "construct-adapter-smith" +version = "0.8.3" +dependencies = [ + "agentd-client", + "agentd-protocol", + "anyhow", + "async-trait", + "base64", + "bytes", + "chrono", + "eventsource-stream", + "futures", + "image", + "reqwest", + "serde", + "serde_json", + "tempfile", + "tokio", + "tokio-tungstenite", + "tracing", + "unicode-width", + "uuid", + "vt100", +] + +[[package]] +name = "construct-mcp" +version = "0.8.3" +dependencies = [ + "agentd-client", + "agentd-protocol", + "anyhow", + "base64", + "futures", + "image", + "reqwest", + "serde", + "serde_json", + "thiserror 1.0.69", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-subscriber", +] + [[package]] name = "convert_case" version = "0.10.0" diff --git a/README.md b/README.md index d187806c..b38656d0 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,34 @@ -# agentd +# construct **Command a fleet of agents, designed for hackers cracking the matrix.** -Create Codex, Claude Code, Antigravity, and Zarvis sessions all in one place. Or +Create Codex, Claude Code, Antigravity, and smith sessions all in one place. Or let your agent coordinate them in a terminal crafted for hardcore hackers like you. Remote control from your phone when you're in motion. -![agentd TUI demo](https://raw.githubusercontent.com/zarvis-ai/agentd/73525a653d1969474f02f0ac699867a68565ac99/demos/browser-thumbnail.gif) +![construct TUI demo](https://raw.githubusercontent.com/zarvis-ai/agentd/73525a653d1969474f02f0ac699867a68565ac99/demos/browser-thumbnail.gif) -## Why agentd? +## Why construct? - **One cockpit for every agent** — attach to Claude Code, Codex, Antigravity, - Zarvis, or a shell process from one focused workspace that rewards attention. + smith, or a shell process from one focused workspace that rewards attention. - agentd new session demo + construct new session demo - **A delightful way to manage multiple Claude Code and Codex sessions** — switch sessions instantly, pin multiple sessions to monitor, or let an agent observe all your sessions across different harnesses. - **Agent-to-agent orchestration** — MCP tools let an agent list sessions, read output, spawn helpers, send input, inspect diffs, and drive Chrome. -- **Generative widgets** — agentd generates and updates widgets for your task, +- **Generative widgets** — construct generates and updates widgets for your task, so you can track progress, review outputs, and take action without leaving the TUI or web client. - agentd generative widgets demo + construct generative widgets demo - **[Remote control](docs/remote-control.md) when you step away** — `/remote-control` opens a browser-accessible web client with a QR code. Connect from your phone, no service signup, no setup required. - agentd remote control demo     →     agentd web client on a phone + construct remote control demo     →     construct web client on a phone - **Extensible harness protocol** — adapters are separate processes speaking JSON-RPC over stdio, so new tools can plug in without changing the daemon. @@ -36,18 +36,18 @@ you. Remote control from your phone when you're in motion. ### 1. Requirements -Bring the agents you want to run. `agentd` wraps the CLIs already on your +Bring the agents you want to run. `construct` wraps the CLIs already on your machine, so install whichever harnesses you use, keep them on `PATH`, and log in first: - **Codex** — install the `codex` CLI and complete its OAuth login. - **Claude Code** — install the `claude` CLI and complete its OAuth login. - **Antigravity** — install the `agy` CLI and complete its OAuth login. -- **Zarvis** — built in to agentd; no separate CLI to install. Talks to OpenAI, +- **smith** — built in to construct; no separate CLI to install. Talks to OpenAI, Anthropic, or Google Gemini via API key, a local Ollama, or a ChatGPT subscription via Codex OAuth. -Once those CLIs are available and authenticated, `agentd` can create and resume +Once those CLIs are available and authenticated, `construct` can create and resume their sessions from the fleet TUI. ### 2. Install @@ -59,13 +59,13 @@ SHA-256 checksum, and drops every binary into one directory on your PATH: curl -fsSL https://raw.githubusercontent.com/zarvis-ai/agentd/main/install.sh | sh ``` -Pin a version or change the directory with `AGENTD_VERSION=v0.2.0` / -`AGENTD_BIN_DIR=/usr/local/bin`. +Pin a version or change the directory with `CONSTRUCT_VERSION=v0.2.0` / +`CONSTRUCT_BIN_DIR=/usr/local/bin`. ### 3. Start the daemon ```sh -agentd +constructd ``` Leave this running. It owns sessions, persists state, and exposes the local IPC @@ -76,7 +76,7 @@ socket used by clients. In a second shell: ```sh -agent +construct ``` Use `?` for help and `M-x` for the command palette. From the TUI you can create @@ -86,23 +86,23 @@ work without leaving the flow. ### 5. Start crack the matrix Happy hacking. Chase the dream idea from your terminal: ask Codex, Claude Code, -Antigravity, and [Zarvis](docs/zarvis.md) to dive into the hard parts, then keep +Antigravity, and [smith](docs/smith.md) to dive into the hard parts, then keep steering from your phone when you're in motion. ## Upgrading ```sh -agent upgrade # install the latest release (atomic in-place replace) -agent upgrade --check # just compare your version against the latest -agent upgrade --restart # upgrade, then restart a running daemon to apply +construct upgrade # install the latest release (atomic in-place replace) +construct upgrade --check # just compare your version against the latest +construct upgrade --restart # upgrade, then restart a running daemon to apply ``` -`agent upgrade` re-runs the installer for you (pin a release with +`construct upgrade` re-runs the installer for you (pin a release with `--version vX.Y.Z`); re-running the install one-liner does the same thing. A running daemon keeps the old code until it restarts — pass `--restart`, or run -`/agentd restart` in the TUI, to pick up the upgrade without losing sessions. +`/construct restart` in the TUI, to pick up the upgrade without losing sessions. The TUI also surfaces a one-line notice when a newer release is available -(disable with `AGENTD_NO_UPDATE_CHECK=1`). +(disable with `CONSTRUCT_NO_UPDATE_CHECK=1`). ## Building from source @@ -114,10 +114,10 @@ cargo build --workspace Debug binaries land in `target/debug/`: -- `target/debug/agentd` — daemon / session supervisor -- `target/debug/agent` — TUI and control CLI -- `target/debug/agentd-mcp` — MCP bridge for agents -- `target/debug/agentd-adapter-*` — harness adapters +- `target/debug/constructd` — daemon / session supervisor +- `target/debug/construct` — TUI and control CLI +- `target/debug/construct-mcp` — MCP bridge for agents +- `target/debug/construct-adapter-*` — harness adapters For an optimized build, use `cargo build --workspace --release` and replace `target/debug` with `target/release`. @@ -128,7 +128,7 @@ For an optimized build, use `cargo build --workspace --release` and replace Agent Harness Protocol (AHP). - [Harnesses and session modes](docs/harnesses.md) — supported adapters, interactive vs. headless modes, worktree isolation, and resume behavior. -- [Zarvis built-in agent](docs/zarvis.md) — providers, model selection, tools, +- [smith built-in agent](docs/smith.md) — providers, model selection, tools, approvals, automode, and hooks. - [Unified tool layer](docs/unified-tool-layer.md) — MCP servers and shared tools for fleet control, browser automation, and agent coordination. @@ -136,7 +136,7 @@ For an optimized build, use `cargo build --workspace --release` and replace for compact session-scoped task state, timelines, and action links. - [Memory](docs/memory.md) — durable Markdown context for project workflows, decisions, preferences, and pitfalls. -- [Configuration](docs/configuration.md) — XDG paths, `AGENTD_*` overrides, and +- [Configuration](docs/configuration.md) — XDG paths, `CONSTRUCT_*` overrides, and TUI theme customization. - [Remote control](docs/remote-control.md) — phone/browser access, QR setup, credentials, and local debug mode. diff --git a/crates/adapter-antigravity/Cargo.toml b/crates/adapter-antigravity/Cargo.toml index cad29fec..bc57e35a 100644 --- a/crates/adapter-antigravity/Cargo.toml +++ b/crates/adapter-antigravity/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "agentd-adapter-antigravity" +name = "construct-adapter-antigravity" version.workspace = true edition.workspace = true license.workspace = true @@ -9,7 +9,7 @@ rust-version.workspace = true description = "Google Antigravity CLI adapter for agentd (wraps the `agy` CLI)" [[bin]] -name = "agentd-adapter-antigravity" +name = "construct-adapter-antigravity" path = "src/main.rs" [dependencies] diff --git a/crates/adapter-antigravity/src/main.rs b/crates/adapter-antigravity/src/main.rs index e022a0a1..3dde834f 100644 --- a/crates/adapter-antigravity/src/main.rs +++ b/crates/adapter-antigravity/src/main.rs @@ -42,11 +42,11 @@ //! //! `` defaults to `$HOME/.gemini/antigravity-cli` //! (antigravity currently nests under the gemini dir); override with -//! `AGENTD_ANTIGRAVITY_HOME`. +//! `CONSTRUCT_ANTIGRAVITY_HOME`. //! -//! Env overrides: `AGENTD_ANTIGRAVITY_CMD` (full command prefix), -//! `AGENTD_ANTIGRAVITY_BIN` (binary, default `agy`), -//! `AGENTD_ANTIGRAVITY_MODE` (`interactive`|`headless`). +//! Env overrides: `CONSTRUCT_ANTIGRAVITY_CMD` (full command prefix), +//! `CONSTRUCT_ANTIGRAVITY_BIN` (binary, default `agy`), +//! `CONSTRUCT_ANTIGRAVITY_MODE` (`interactive`|`headless`). use agentd_protocol::adapter::pty::{run_session as run_pty, PtySpec}; use agentd_protocol::adapter::{run, AdapterContext, AdapterInboxMsg, EventEmitter}; @@ -92,7 +92,7 @@ enum Mode { } fn resolve_mode(params: &SessionStartParams) -> Mode { - if let Ok(m) = std::env::var("AGENTD_ANTIGRAVITY_MODE") { + if let Ok(m) = std::env::var("CONSTRUCT_ANTIGRAVITY_MODE") { match m.as_str() { "interactive" => return Mode::Interactive, "headless" => return Mode::Headless, @@ -109,13 +109,13 @@ fn resolve_mode(params: &SessionStartParams) -> Mode { fn command_override() -> agentd_protocol::adapter::CommandOverride { agentd_protocol::adapter::resolve_command_override( - "AGENTD_ANTIGRAVITY_CMD", - "AGENTD_ANTIGRAVITY_BIN", + "CONSTRUCT_ANTIGRAVITY_CMD", + "CONSTRUCT_ANTIGRAVITY_BIN", "agy", ) } -// The daemon's auto-approval policy (`AGENTD_AUTO_APPROVE_PATHS`, see +// The daemon's auto-approval policy (`CONSTRUCT_AUTO_APPROVE_PATHS`, see // `agentd_protocol::adapter::policy`) is set, but `agy` only exposes a // global `--dangerously-skip-permissions` and no path-scoped allow-list, so // there's no native translation to apply in interactive mode. Headless mode @@ -124,7 +124,7 @@ fn command_override() -> agentd_protocol::adapter::CommandOverride { // agy feature or for agentd to intercept its tool calls. fn session_data_dir() -> Option { - std::env::var("AGENTD_SESSION_DATA_DIR") + std::env::var("CONSTRUCT_SESSION_DATA_DIR") .ok() .map(PathBuf::from) } @@ -132,9 +132,9 @@ fn session_data_dir() -> Option { /// Antigravity's home dir, where per-conversation `brain/` trees /// (and their `.system_generated/logs/transcript.jsonl`) live. Defaults /// to `$HOME/.gemini/antigravity-cli`; override with -/// `AGENTD_ANTIGRAVITY_HOME`. +/// `CONSTRUCT_ANTIGRAVITY_HOME`. fn antigravity_home() -> Option { - if let Ok(h) = std::env::var("AGENTD_ANTIGRAVITY_HOME") { + if let Ok(h) = std::env::var("CONSTRUCT_ANTIGRAVITY_HOME") { return Some(PathBuf::from(h)); } let home = std::env::var_os("HOME")?; @@ -198,7 +198,7 @@ async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { let mut args = command.args.clone(); args.extend(params.args.clone()); - let resuming = std::env::var("AGENTD_RESUME").as_deref() == Ok("1"); + let resuming = std::env::var("CONSTRUCT_RESUME").as_deref() == Ok("1"); let log_path = session_data_dir().map(|d| d.join("agy.log")); if let Some(lp) = &log_path { args.push("--log-file".into()); @@ -235,7 +235,7 @@ async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); - env.push(("AGENTD_SESSION_ID".into(), ctx.session_id.clone())); + env.push(("CONSTRUCT_SESSION_ID".into(), ctx.session_id.clone())); let label = command.argv_preview(); let bin = command.bin; @@ -311,7 +311,7 @@ async fn run_session(params: SessionStartParams, ctx: AdapterContext) { .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); - env.push(("AGENTD_SESSION_ID".into(), session_id.clone())); + env.push(("CONSTRUCT_SESSION_ID".into(), session_id.clone())); // Resume bookkeeping: known conversation id + how many transcript // steps we've already emitted (so we only forward NEW steps each diff --git a/crates/adapter-claude/Cargo.toml b/crates/adapter-claude/Cargo.toml index 0340e236..abda55f5 100644 --- a/crates/adapter-claude/Cargo.toml +++ b/crates/adapter-claude/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "agentd-adapter-claude" +name = "construct-adapter-claude" version.workspace = true edition.workspace = true license.workspace = true @@ -9,7 +9,7 @@ rust-version.workspace = true description = "Claude Code adapter for agentd (wraps the `claude` CLI)" [[bin]] -name = "agentd-adapter-claude" +name = "construct-adapter-claude" path = "src/main.rs" [dependencies] diff --git a/crates/adapter-claude/src/main.rs b/crates/adapter-claude/src/main.rs index dc2be9b5..62ed07a6 100644 --- a/crates/adapter-claude/src/main.rs +++ b/crates/adapter-claude/src/main.rs @@ -12,12 +12,12 @@ //! plus `--resume ` for follow-up turns. Emits structured //! `Message` / `ToolUse` / `Cost` events. //! -//! Pick mode via `--mode interactive|headless` on `agent new`, or via -//! `AGENTD_CLAUDE_MODE=interactive|headless`. Default is interactive when the +//! Pick mode via `--mode interactive|headless` on `construct new`, or via +//! `CONSTRUCT_CLAUDE_MODE=interactive|headless`. Default is interactive when the //! client supplies a PTY size (the TUI always does); otherwise headless. //! -//! Honors `AGENTD_CLAUDE_CMD` for a full command prefix, falling back to -//! `AGENTD_CLAUDE_BIN` for a binary path. +//! Honors `CONSTRUCT_CLAUDE_CMD` for a full command prefix, falling back to +//! `CONSTRUCT_CLAUDE_BIN` for a binary path. use agentd_protocol::adapter::pty::{run_session as run_pty, PtySpec}; use agentd_protocol::adapter::{run, AdapterContext, AdapterInboxMsg, EventEmitter}; @@ -64,7 +64,7 @@ enum Mode { } fn resolve_mode(params: &SessionStartParams) -> Mode { - if let Ok(m) = std::env::var("AGENTD_CLAUDE_MODE") { + if let Ok(m) = std::env::var("CONSTRUCT_CLAUDE_MODE") { match m.as_str() { "interactive" => return Mode::Interactive, "headless" => return Mode::Headless, @@ -81,20 +81,20 @@ fn resolve_mode(params: &SessionStartParams) -> Mode { /// Generate a minimal Claude `--settings` file registering the AskUserQuestion /// chat-gate `PreToolUse` hook, and return its path. The hook shells out to -/// `agent ask-gate`, which degrades the picker to a plain-text question only +/// `construct ask-gate`, which degrades the picker to a plain-text question only /// when a chat viewer is active for this session (otherwise it allows, so the /// native picker behaves normally for terminal viewers). /// /// Returns `None` — no injection — when the `agent` binary or session data dir -/// can't be located, or when disabled via `AGENTD_CLAUDE_ASKGATE=0`. Verified +/// can't be located, or when disabled via `CONSTRUCT_CLAUDE_ASKGATE=0`. Verified /// that `--settings` *merges* with the user's existing settings/hooks, so this /// never clobbers their setup. fn askgate_settings_path() -> Option { - if std::env::var("AGENTD_CLAUDE_ASKGATE").as_deref() == Ok("0") { + if std::env::var("CONSTRUCT_CLAUDE_ASKGATE").as_deref() == Ok("0") { return None; } - let agent = agentd_protocol::paths::locate_sibling_binary("agent")?; - let dir = std::env::var("AGENTD_SESSION_DATA_DIR") + let client = agentd_protocol::paths::locate_sibling_binary("construct")?; + let dir = std::env::var("CONSTRUCT_SESSION_DATA_DIR") .ok() .filter(|s| !s.is_empty())?; let path = PathBuf::from(dir).join("agentd-askgate-settings.json"); @@ -104,7 +104,7 @@ fn askgate_settings_path() -> Option { "matcher": "AskUserQuestion", "hooks": [{ "type": "command", - "command": format!("\"{}\" ask-gate", agent.display()), + "command": format!("\"{}\" ask-gate", client.display()), }], }], } @@ -115,8 +115,8 @@ fn askgate_settings_path() -> Option { async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { let command = agentd_protocol::adapter::resolve_command_override( - "AGENTD_CLAUDE_CMD", - "AGENTD_CLAUDE_BIN", + "CONSTRUCT_CLAUDE_CMD", + "CONSTRUCT_CLAUDE_BIN", "claude", ); let mut args = command.args.clone(); @@ -127,7 +127,7 @@ async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { } // Auto-inject the agentd MCP server so the agent inside this session // can drive the daemon (list other sessions, send input, spawn helpers, - // etc.). Opt out with AGENTD_INJECT_MCP=0. + // etc.). Opt out with CONSTRUCT_INJECT_MCP=0. if let Some(cfg) = agentd_protocol::adapter::maybe_inject_mcp_config(&ctx.session_id) { args.push("--mcp-config".into()); args.push(cfg.to_string_lossy().to_string()); @@ -145,12 +145,12 @@ async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { agentd_protocol::adapter::policy::AutoApprovePolicy::from_env().claude_allowed_tools_args(), ); // Resume support: stash our own UUID under - // $AGENTD_SESSION_DATA_DIR/claude_session_id.txt at first spawn (passed + // $CONSTRUCT_SESSION_DATA_DIR/claude_session_id.txt at first spawn (passed // to claude as --session-id), then pass it back as --resume when the // daemon respawns us after a restart. claude's own session-persistence // makes the conversation pick up where it left off. - let resuming = std::env::var("AGENTD_RESUME").as_deref() == Ok("1"); - let sid_file = std::env::var("AGENTD_SESSION_DATA_DIR") + let resuming = std::env::var("CONSTRUCT_RESUME").as_deref() == Ok("1"); + let sid_file = std::env::var("CONSTRUCT_SESSION_DATA_DIR") .ok() .map(|d| std::path::PathBuf::from(d).join("claude_session_id.txt")); let claude_session_id = match (resuming, sid_file.as_ref()) { @@ -182,13 +182,13 @@ async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { } } // Surface the session id to the child's env so agents that aren't using - // MCP (or the user, via `echo $AGENTD_SESSION_ID`) can still tell. + // MCP (or the user, via `echo $CONSTRUCT_SESSION_ID`) can still tell. let mut env: Vec<(String, String)> = params .env .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); - env.push(("AGENTD_SESSION_ID".into(), ctx.session_id.clone())); + env.push(("CONSTRUCT_SESSION_ID".into(), ctx.session_id.clone())); if let Some(session_id) = watch_session_id { spawn_interactive_transcript_watcher( session_id, @@ -239,7 +239,7 @@ fn spawn_interactive_transcript_watcher( } fn claude_transcript_path(cwd: &Path, session_id: &str) -> Option { - let home = std::env::var("AGENTD_CLAUDE_HOME") + let home = std::env::var("CONSTRUCT_CLAUDE_HOME") .ok() .filter(|s| !s.is_empty()) .or_else(|| std::env::var("CLAUDE_HOME").ok().filter(|s| !s.is_empty())) @@ -298,8 +298,8 @@ async fn run_session(params: SessionStartParams, ctx: AdapterContext) { } = ctx; let command_override = agentd_protocol::adapter::resolve_command_override( - "AGENTD_CLAUDE_CMD", - "AGENTD_CLAUDE_BIN", + "CONSTRUCT_CLAUDE_CMD", + "CONSTRUCT_CLAUDE_BIN", "claude", ); let cwd = PathBuf::from(¶ms.cwd); @@ -390,7 +390,7 @@ async fn run_session(params: SessionStartParams, ctx: AdapterContext) { for (k, v) in &env { command.env(k, v); } - command.env("AGENTD_SESSION_ID", &agentd_session_id); + command.env("CONSTRUCT_SESSION_ID", &agentd_session_id); let mut child = match command.spawn() { Ok(c) => c, diff --git a/crates/adapter-codex/Cargo.toml b/crates/adapter-codex/Cargo.toml index 39d5c02d..84031a34 100644 --- a/crates/adapter-codex/Cargo.toml +++ b/crates/adapter-codex/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "agentd-adapter-codex" +name = "construct-adapter-codex" version.workspace = true edition.workspace = true license.workspace = true @@ -9,7 +9,7 @@ rust-version.workspace = true description = "OpenAI Codex adapter for agentd (wraps the `codex` CLI)" [[bin]] -name = "agentd-adapter-codex" +name = "construct-adapter-codex" path = "src/main.rs" [dependencies] diff --git a/crates/adapter-codex/src/main.rs b/crates/adapter-codex/src/main.rs index 0d3d8387..27136d87 100644 --- a/crates/adapter-codex/src/main.rs +++ b/crates/adapter-codex/src/main.rs @@ -7,13 +7,13 @@ //! //! - **headless (opt-in)** — multi-turn structured mode that spawns //! `codex exec ` per turn. Best-effort: if your codex build -//! supports session resumption, set `AGENTD_CODEX_RESUME_FLAG` to the flag +//! supports session resumption, set `CONSTRUCT_CODEX_RESUME_FLAG` to the flag //! name (e.g. `--session-id`) and the adapter will pass any captured //! `session_id` back in for subsequent turns. //! -//! Pick mode via `--mode interactive|headless` on `agent new`, or via -//! `AGENTD_CODEX_MODE=interactive|headless`. Honors `AGENTD_CODEX_CMD` for a -//! full command prefix, falling back to `AGENTD_CODEX_BIN` for a binary path. +//! Pick mode via `--mode interactive|headless` on `construct new`, or via +//! `CONSTRUCT_CODEX_MODE=interactive|headless`. Honors `CONSTRUCT_CODEX_CMD` for a +//! full command prefix, falling back to `CONSTRUCT_CODEX_BIN` for a binary path. use agentd_protocol::adapter::pty::{run_session as run_pty, PtySpec}; use agentd_protocol::adapter::{run, AdapterContext, AdapterInboxMsg, EventEmitter}; @@ -59,7 +59,7 @@ enum Mode { } fn resolve_mode(params: &SessionStartParams) -> Mode { - if let Ok(m) = std::env::var("AGENTD_CODEX_MODE") { + if let Ok(m) = std::env::var("CONSTRUCT_CODEX_MODE") { match m.as_str() { "interactive" => return Mode::Interactive, "headless" => return Mode::Headless, @@ -76,13 +76,13 @@ fn resolve_mode(params: &SessionStartParams) -> Mode { async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { let command = agentd_protocol::adapter::resolve_command_override( - "AGENTD_CODEX_CMD", - "AGENTD_CODEX_BIN", + "CONSTRUCT_CODEX_CMD", + "CONSTRUCT_CODEX_BIN", "codex", ); let mut args = command.args.clone(); args.extend(params.args.clone()); - // The daemon's auto-approval policy (`AGENTD_AUTO_APPROVE_PATHS`, see + // The daemon's auto-approval policy (`CONSTRUCT_AUTO_APPROVE_PATHS`, see // `agentd_protocol::adapter::policy`) is set, but the upstream codex CLI // does not currently expose a path-scoped allow-list flag, so there's no // native translation to apply here. Either upstream gains the knob or we @@ -93,7 +93,7 @@ async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { // When we see it, we persist codex's UUID to // `/codex_session_id.txt`; on daemon-restart respawn we // pass it back as `codex resume `. The explicit override - // `AGENTD_CODEX_RESUME_ID` still wins if set. + // `CONSTRUCT_CODEX_RESUME_ID` still wins if set. // // We deliberately do NOT fall back to `codex resume --last` when no id // was captured: `--last` resolves globally across every codex session @@ -101,13 +101,13 @@ async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { // would attach to the same upstream codex and from that moment paint // identical PTY content. Starting a fresh codex loses one session's // conversation but never conflates two of them. - let resuming = std::env::var("AGENTD_RESUME").as_deref() == Ok("1"); - let sid_file = std::env::var("AGENTD_SESSION_DATA_DIR") + let resuming = std::env::var("CONSTRUCT_RESUME").as_deref() == Ok("1"); + let sid_file = std::env::var("CONSTRUCT_SESSION_DATA_DIR") .ok() .map(|d| std::path::PathBuf::from(d).join("codex_session_id.txt")); let mut captured_id: Option = None; if resuming { - let explicit = std::env::var("AGENTD_CODEX_RESUME_ID").ok(); + let explicit = std::env::var("CONSTRUCT_CODEX_RESUME_ID").ok(); let from_file = sid_file.as_ref().and_then(|p| { std::fs::read_to_string(p) .ok() @@ -131,7 +131,7 @@ async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { } // Auto-inject agentd MCP server via codex's `-c` override (codex has no // `--mcp-config` flag — MCP servers live in `[mcp_servers.]`). - // Opt out with AGENTD_INJECT_MCP=0. + // Opt out with CONSTRUCT_INJECT_MCP=0. for a in agentd_protocol::adapter::maybe_inject_codex_mcp_args(&ctx.session_id) { args.push(a); } @@ -149,7 +149,7 @@ async fn run_interactive(params: SessionStartParams, ctx: AdapterContext) { .iter() .map(|(k, v)| (k.clone(), v.clone())) .collect(); - env.push(("AGENTD_SESSION_ID".into(), ctx.session_id.clone())); + env.push(("CONSTRUCT_SESSION_ID".into(), ctx.session_id.clone())); // Tag this codex's rollout with a unique originator we can grep for. // Codex stamps `payload.originator` in the rollout's session_meta line // from this internal env var (found by string-grep on the binary; not @@ -484,11 +484,11 @@ async fn run_session(params: SessionStartParams, ctx: AdapterContext) { } = ctx; let command_override = agentd_protocol::adapter::resolve_command_override( - "AGENTD_CODEX_CMD", - "AGENTD_CODEX_BIN", + "CONSTRUCT_CODEX_CMD", + "CONSTRUCT_CODEX_BIN", "codex", ); - let resume_flag = std::env::var("AGENTD_CODEX_RESUME_FLAG").ok(); + let resume_flag = std::env::var("CONSTRUCT_CODEX_RESUME_FLAG").ok(); let cwd = PathBuf::from(¶ms.cwd); let model = params.model.clone(); let extra_args = params.args.clone(); @@ -562,7 +562,7 @@ async fn run_session(params: SessionStartParams, ctx: AdapterContext) { for (k, v) in &env { command.env(k, v); } - command.env("AGENTD_SESSION_ID", &agentd_session_id); + command.env("CONSTRUCT_SESSION_ID", &agentd_session_id); let mut child = match command.spawn() { Ok(c) => c, diff --git a/crates/adapter-shell/Cargo.toml b/crates/adapter-shell/Cargo.toml index b8b57df0..50a8297a 100644 --- a/crates/adapter-shell/Cargo.toml +++ b/crates/adapter-shell/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "agentd-adapter-shell" +name = "construct-adapter-shell" version.workspace = true edition.workspace = true license.workspace = true @@ -9,7 +9,7 @@ rust-version.workspace = true description = "Generic shell command adapter for agentd" [[bin]] -name = "agentd-adapter-shell" +name = "construct-adapter-shell" path = "src/main.rs" [dependencies] diff --git a/crates/adapter-shell/src/main.rs b/crates/adapter-shell/src/main.rs index af08d110..2eb2f76d 100644 --- a/crates/adapter-shell/src/main.rs +++ b/crates/adapter-shell/src/main.rs @@ -7,8 +7,8 @@ //! - Empty prompt → `$SHELL -il` (interactive login shell). //! - Non-empty prompt → `$SHELL -lc ` (one-shot login shell). //! -//! Honors `AGENTD_SHELL_CMD` for a full command prefix, falling back to -//! `AGENTD_SHELL_BIN`, then `$SHELL`, then `/bin/bash`. +//! Honors `CONSTRUCT_SHELL_CMD` for a full command prefix, falling back to +//! `CONSTRUCT_SHELL_BIN`, then `$SHELL`, then `/bin/bash`. use agentd_protocol::adapter::pty::{run_session, PtySpec}; use agentd_protocol::adapter::run; @@ -30,8 +30,8 @@ async fn main() -> anyhow::Result<()> { run(metadata, |params, ctx| async move { let default_shell = std::env::var("SHELL").unwrap_or_else(|_| "/bin/bash".to_string()); let command = agentd_protocol::adapter::resolve_command_override( - "AGENTD_SHELL_CMD", - "AGENTD_SHELL_BIN", + "CONSTRUCT_SHELL_CMD", + "CONSTRUCT_SHELL_BIN", &default_shell, ); @@ -39,7 +39,7 @@ async fn main() -> anyhow::Result<()> { // (it already ran in the previous incarnation). Re-spawn a fresh // interactive login shell in the same cwd so the user can keep // working. - let resuming = std::env::var("AGENTD_RESUME").as_deref() == Ok("1"); + let resuming = std::env::var("CONSTRUCT_RESUME").as_deref() == Ok("1"); let mut args: Vec = command.args.clone(); match params.prompt.as_deref() { Some(p) if !p.trim().is_empty() && !resuming => { diff --git a/crates/adapter-zarvis/Cargo.toml b/crates/adapter-zarvis/Cargo.toml index 016ec9af..ed0e28e5 100644 --- a/crates/adapter-zarvis/Cargo.toml +++ b/crates/adapter-zarvis/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "agentd-adapter-zarvis" +name = "construct-adapter-smith" version.workspace = true edition.workspace = true license.workspace = true @@ -9,7 +9,7 @@ rust-version.workspace = true description = "Built-in multi-provider agent harness for agentd (talks to OpenAI, Anthropic, Ollama directly)" [[bin]] -name = "agentd-adapter-zarvis" +name = "construct-adapter-smith" path = "src/main.rs" [dependencies] diff --git a/crates/adapter-zarvis/src/agent.rs b/crates/adapter-zarvis/src/agent.rs index 2e2ebfda..0b60a28c 100644 --- a/crates/adapter-zarvis/src/agent.rs +++ b/crates/adapter-zarvis/src/agent.rs @@ -232,10 +232,10 @@ SURFACING: a short text reply is shown to the user as a brief typewriter "monolo Be concise. The minibuffer panel is small; aim for one to three short lines per turn, longer only when the user explicitly asks for detail. Risky tool calls (delete / kill / send) still gate through approval unless the session is in unsafe-auto."#; /// Pick the right system prompt for this session's kind. The daemon -/// sets `AGENTD_SESSION_KIND` at spawn time; default is `user` so old +/// sets `CONSTRUCT_SESSION_KIND` at spawn time; default is `user` so old /// callers keep working. pub(crate) fn system_prompt_for_env() -> &'static str { - match std::env::var("AGENTD_SESSION_KIND").as_deref() { + match std::env::var("CONSTRUCT_SESSION_KIND").as_deref() { Ok("orchestrator") => SYSTEM_PROMPT_ORCHESTRATOR, _ => SYSTEM_PROMPT_USER, } @@ -447,7 +447,7 @@ pub async fn run( .await; // Per-session approval mode. Defaults to unsafe-auto when the legacy env override is set. - let mut approval_mode = if std::env::var("AGENTD_ZARVIS_AUTOMODE").as_deref() == Ok("1") { + let mut approval_mode = if std::env::var("CONSTRUCT_SMITH_AUTOMODE").as_deref() == Ok("1") { agentd_protocol::ApprovalMode::UnsafeAuto } else { agentd_protocol::ApprovalMode::Manual @@ -563,14 +563,14 @@ pub async fn run( // Loop/thrash guard (model-agnostic): bound a runaway turn and nudge // the model when it stops making progress. The caps are opt-in via env // (0 = unlimited) so default behavior is unchanged; the non-progress - // nudge is always on. `AGENTD_ZARVIS_MAX_STEPS` caps model calls per - // turn; `AGENTD_ZARVIS_MAX_TURN_SECS` caps wall-clock per turn (lets a + // nudge is always on. `CONSTRUCT_SMITH_MAX_STEPS` caps model calls per + // turn; `CONSTRUCT_SMITH_MAX_TURN_SECS` caps wall-clock per turn (lets a // session stop itself gracefully before an external timeout SIGKILL). - let max_steps: usize = std::env::var("AGENTD_ZARVIS_MAX_STEPS") + let max_steps: usize = std::env::var("CONSTRUCT_SMITH_MAX_STEPS") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(0); - let max_turn_secs: i64 = std::env::var("AGENTD_ZARVIS_MAX_TURN_SECS") + let max_turn_secs: i64 = std::env::var("CONSTRUCT_SMITH_MAX_TURN_SECS") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(0); @@ -1374,7 +1374,7 @@ impl ResolvedModel { /// Resolve `--model` (or its absence) to a provider instance and a /// model name. Order of precedence: /// 1. `params.model` if provided. -/// 2. `AGENTD_ZARVIS_MODEL`. +/// 2. `CONSTRUCT_SMITH_MODEL`. /// 3. ANTHROPIC_API_KEY set → `claude-opus-4-8`. /// 4. OPENAI_API_KEY set → `gpt-5`. /// 5. GEMINI_API_KEY (or GOOGLE_API_KEY) set → `gemini-2.5-pro`. @@ -1384,7 +1384,7 @@ pub fn resolve_model(params: &SessionStartParams) -> Result { .model .clone() .filter(|s| !s.trim().is_empty()) - .or_else(|| std::env::var("AGENTD_ZARVIS_MODEL").ok()) + .or_else(|| std::env::var("CONSTRUCT_SMITH_MODEL").ok()) .unwrap_or_else(|| { if std::env::var("ANTHROPIC_API_KEY").is_ok() { "anthropic:claude-opus-4-8".to_string() diff --git a/crates/adapter-zarvis/src/compact.rs b/crates/adapter-zarvis/src/compact.rs index 547a4fd0..8db9fa03 100644 --- a/crates/adapter-zarvis/src/compact.rs +++ b/crates/adapter-zarvis/src/compact.rs @@ -41,7 +41,7 @@ use anyhow::{anyhow, Result}; /// available). Auto is on by default — set this to `0`, `false`, or /// `off` to fall back to pure rolling-prune behavior. Mainly an escape /// hatch for users hitting unexpected summarizer-call costs. -const ENV_AUTO_COMPACT: &str = "AGENTD_ZARVIS_AUTO_COMPACT"; +const ENV_AUTO_COMPACT: &str = "CONSTRUCT_SMITH_AUTO_COMPACT"; /// Whether auto-compact should run this session. Default on. pub fn auto_compact_enabled() -> bool { diff --git a/crates/adapter-zarvis/src/hooks.rs b/crates/adapter-zarvis/src/hooks.rs index bb3320eb..94949d69 100644 --- a/crates/adapter-zarvis/src/hooks.rs +++ b/crates/adapter-zarvis/src/hooks.rs @@ -48,12 +48,12 @@ impl Hooks { } fn try_load(cwd: &Path) -> Result { - let raw = if let Ok(s) = std::env::var("AGENTD_ZARVIS_HOOKS_JSON") { + let raw = if let Ok(s) = std::env::var("CONSTRUCT_SMITH_HOOKS_JSON") { if s.trim().is_empty() { return Ok(Self::default()); } s - } else if let Ok(path) = std::env::var("AGENTD_ZARVIS_HOOKS_CONFIG") { + } else if let Ok(path) = std::env::var("CONSTRUCT_SMITH_HOOKS_CONFIG") { if path.trim().is_empty() { return Ok(Self::default()); } @@ -128,7 +128,7 @@ impl HookCommand { async fn run_capture(&self, event: &str, cwd: &Path, payload: Value) -> Result { let timeout = Duration::from_millis(self.timeout_ms.unwrap_or(DEFAULT_TIMEOUT_MS)); - let shell = std::env::var("AGENTD_SHELL_BIN") + let shell = std::env::var("CONSTRUCT_SHELL_BIN") .ok() .filter(|s| !s.trim().is_empty()) .unwrap_or_else(|| "/bin/sh".to_string()); @@ -136,7 +136,7 @@ impl HookCommand { .arg("-lc") .arg(&self.command) .current_dir(cwd) - .env("AGENTD_ZARVIS_HOOK_EVENT", event) + .env("CONSTRUCT_SMITH_HOOK_EVENT", event) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) @@ -219,12 +219,12 @@ mod tests { fn parses_inline_hook_config() { let _guard = env_lock().lock().unwrap(); std::env::set_var( - "AGENTD_ZARVIS_HOOKS_JSON", + "CONSTRUCT_SMITH_HOOKS_JSON", r#"{"hooks":{"pre_tool_use":[{"command":"echo ok","timeout_ms":123}]}}"#, ); - std::env::remove_var("AGENTD_ZARVIS_HOOKS_CONFIG"); + std::env::remove_var("CONSTRUCT_SMITH_HOOKS_CONFIG"); let hooks = Hooks::try_load(Path::new("/tmp")).expect("load hooks"); - std::env::remove_var("AGENTD_ZARVIS_HOOKS_JSON"); + std::env::remove_var("CONSTRUCT_SMITH_HOOKS_JSON"); assert_eq!(hooks.hooks["pre_tool_use"][0].command, "echo ok"); assert_eq!(hooks.hooks["pre_tool_use"][0].timeout_ms, Some(123)); } @@ -238,10 +238,10 @@ mod tests { r#"{"hooks":{"session_stop":[{"command":"printf stop"}]}}"#, ) .expect("write hooks config"); - std::env::remove_var("AGENTD_ZARVIS_HOOKS_JSON"); - std::env::set_var("AGENTD_ZARVIS_HOOKS_CONFIG", "hooks.json"); + std::env::remove_var("CONSTRUCT_SMITH_HOOKS_JSON"); + std::env::set_var("CONSTRUCT_SMITH_HOOKS_CONFIG", "hooks.json"); let hooks = Hooks::try_load(dir.path()).expect("load hooks"); - std::env::remove_var("AGENTD_ZARVIS_HOOKS_CONFIG"); + std::env::remove_var("CONSTRUCT_SMITH_HOOKS_CONFIG"); assert_eq!(hooks.hooks["session_stop"][0].command, "printf stop"); } diff --git a/crates/adapter-zarvis/src/interactive.rs b/crates/adapter-zarvis/src/interactive.rs index 9bd6a5cc..91708e95 100644 --- a/crates/adapter-zarvis/src/interactive.rs +++ b/crates/adapter-zarvis/src/interactive.rs @@ -1972,7 +1972,7 @@ pub async fn run( let base_hook_payload = crate::hooks::base_payload(&session_id, &cwd, "interactive"); let registry = std::sync::Arc::new(ToolRegistry::with_defaults()); let specs = registry.specs(); - let mut approval_mode = if std::env::var("AGENTD_ZARVIS_AUTOMODE").as_deref() == Ok("1") { + let mut approval_mode = if std::env::var("CONSTRUCT_SMITH_AUTOMODE").as_deref() == Ok("1") { ApprovalMode::UnsafeAuto } else { ApprovalMode::Manual @@ -2106,7 +2106,7 @@ pub async fn run( // sessions get `None` here and skip the obs branch in the inner // select. Rate-limited so a burst of events can't fire a turn // per event. - let is_orchestrator = std::env::var("AGENTD_SESSION_KIND").as_deref() == Ok("orchestrator"); + let is_orchestrator = std::env::var("CONSTRUCT_SESSION_KIND").as_deref() == Ok("orchestrator"); let mut obs_rx = if is_orchestrator { Some(crate::observe::spawn(self_id_for_obs)) } else { @@ -2124,7 +2124,7 @@ pub async fn run( // Ambient monitor model: the fleet scan + triage runs as a one-shot // completion off the operator's own conversation, so the bulky snapshot / // previews never accumulate in the operator's context and only escalations - // reach it. Configure a cheaper model via AGENTD_OPERATOR_MONITOR_MODEL; + // reach it. Configure a cheaper model via CONSTRUCT_OPERATOR_MONITOR_MODEL; // otherwise it falls back to the operator's own model. // Resolve the monitor model (orchestrator-only — that's the only session // that ambient-ticks). The explicit override wins; otherwise default to a @@ -2133,7 +2133,7 @@ pub async fn run( // falls back to the operator's own model when the chosen model can't be // resolved or doesn't actually answer. let monitor_model = if is_orchestrator { - let candidate = std::env::var("AGENTD_OPERATOR_MONITOR_MODEL") + let candidate = std::env::var("CONSTRUCT_OPERATOR_MONITOR_MODEL") .ok() .filter(|s| !s.is_empty()) .or_else(|| default_monitor_spec(provider_name, &model)) @@ -3088,7 +3088,7 @@ struct OperatorAmbientLoop { } fn operator_ambient_loop_interval() -> Duration { - let secs = std::env::var("AGENTD_OPERATOR_AMBIENT_LOOP_SECS") + let secs = std::env::var("CONSTRUCT_OPERATOR_AMBIENT_LOOP_SECS") .ok() .and_then(|raw| raw.parse::().ok()) .unwrap_or(60) @@ -3131,7 +3131,7 @@ fn observation_panel_echo(user_text: &str) -> Option> { const IDLE_RUNNING_MINS: i64 = 10; fn ambient_active_window() -> Duration { - let secs = std::env::var("AGENTD_OPERATOR_ACTIVE_WINDOW_SECS") + let secs = std::env::var("CONSTRUCT_OPERATOR_ACTIVE_WINDOW_SECS") .ok() .and_then(|raw| raw.parse::().ok()) .unwrap_or(IDLE_RUNNING_MINS as u64 * 60) @@ -3139,18 +3139,18 @@ fn ambient_active_window() -> Duration { Duration::from_secs(secs) } -/// Max sessions previewed per tick. Override with `AGENTD_OPERATOR_PREVIEW_SESSIONS`. +/// Max sessions previewed per tick. Override with `CONSTRUCT_OPERATOR_PREVIEW_SESSIONS`. fn preview_session_cap() -> usize { - std::env::var("AGENTD_OPERATOR_PREVIEW_SESSIONS") + std::env::var("CONSTRUCT_OPERATOR_PREVIEW_SESSIONS") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(10) .min(50) } -/// Per-session preview byte budget. Override with `AGENTD_OPERATOR_PREVIEW_BYTES`. +/// Per-session preview byte budget. Override with `CONSTRUCT_OPERATOR_PREVIEW_BYTES`. /// When the recent messages exceed it, the older part is truncated. fn preview_byte_cap() -> usize { - std::env::var("AGENTD_OPERATOR_PREVIEW_BYTES") + std::env::var("CONSTRUCT_OPERATOR_PREVIEW_BYTES") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(800) @@ -3259,7 +3259,7 @@ If anything qualifies, reply with at most 3 one-line findings, each: ''. If nothing qualifies, reply with exactly the single \ word: nothing"; -/// Default monitor model when `AGENTD_OPERATOR_MONITOR_MODEL` is unset: a +/// Default monitor model when `CONSTRUCT_OPERATOR_MONITOR_MODEL` is unset: a /// cheaper tier on the **same provider** as the operator (so auth/keys are /// already present), using model names the codebase/provider is known to /// accept. Returns `None` — keep the operator's own model — when the operator @@ -4665,11 +4665,11 @@ async fn handle_slash_loop( /// adapter doesn't share that module — but the env-var keys /// match so a deployment-time override applies to both sides. fn clamp_interval_for_slash(secs: u64) -> (u64, bool) { - let min = std::env::var("AGENTD_LOOP_MIN_SECS") + let min = std::env::var("CONSTRUCT_LOOP_MIN_SECS") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(30u64); - let max = std::env::var("AGENTD_LOOP_MAX_SECS") + let max = std::env::var("CONSTRUCT_LOOP_MAX_SECS") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(24 * 3600u64); diff --git a/crates/adapter-zarvis/src/interval_suggest.rs b/crates/adapter-zarvis/src/interval_suggest.rs index 1d044c4a..29abcfc4 100644 --- a/crates/adapter-zarvis/src/interval_suggest.rs +++ b/crates/adapter-zarvis/src/interval_suggest.rs @@ -28,11 +28,11 @@ Default to 300s (5 minutes) if you genuinely can't tell. Stay within [30, 86400] /// bounds, but clamping at the adapter level gives a cleaner /// error path + lets us mention the clamp in the tool result. fn bounds() -> (u64, u64) { - let min = std::env::var("AGENTD_LOOP_MIN_SECS") + let min = std::env::var("CONSTRUCT_LOOP_MIN_SECS") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(30u64); - let max = std::env::var("AGENTD_LOOP_MAX_SECS") + let max = std::env::var("CONSTRUCT_LOOP_MAX_SECS") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(24 * 3600u64); @@ -112,12 +112,12 @@ mod tests { #[test] fn clamp_respects_bounds() { - std::env::set_var("AGENTD_LOOP_MIN_SECS", "30"); - std::env::set_var("AGENTD_LOOP_MAX_SECS", "3600"); + std::env::set_var("CONSTRUCT_LOOP_MIN_SECS", "30"); + std::env::set_var("CONSTRUCT_LOOP_MAX_SECS", "3600"); assert_eq!(clamp(5), 30); assert_eq!(clamp(5000), 3600); assert_eq!(clamp(60), 60); - std::env::remove_var("AGENTD_LOOP_MIN_SECS"); - std::env::remove_var("AGENTD_LOOP_MAX_SECS"); + std::env::remove_var("CONSTRUCT_LOOP_MIN_SECS"); + std::env::remove_var("CONSTRUCT_LOOP_MAX_SECS"); } } diff --git a/crates/adapter-zarvis/src/main.rs b/crates/adapter-zarvis/src/main.rs index 9885dd30..28756a13 100644 --- a/crates/adapter-zarvis/src/main.rs +++ b/crates/adapter-zarvis/src/main.rs @@ -1,4 +1,4 @@ -//! Zarvis — agentd's built-in multi-provider agent harness. +//! Smith — construct's built-in multi-provider agent harness. //! //! Talks to OpenAI / Anthropic / Gemini / Ollama directly (no vendor CLI required), //! runs its own agent loop, and executes shell + filesystem + @@ -32,7 +32,7 @@ enum Mode { } fn resolve_mode(params: &SessionStartParams) -> Mode { - if let Ok(m) = std::env::var("AGENTD_ZARVIS_MODE") { + if let Ok(m) = std::env::var("CONSTRUCT_SMITH_MODE") { match m.as_str() { "interactive" => return Mode::Interactive, "headless" => return Mode::Headless, @@ -43,7 +43,7 @@ fn resolve_mode(params: &SessionStartParams) -> Mode { Some("interactive") => Mode::Interactive, Some("headless") => Mode::Headless, // Default: interactive when the client supplied a PTY size (the - // TUI always does), else headless (so `agent new zarvis "..."` + // TUI always does), else headless (so `construct new smith "..."` // from a non-TUI client gets the structured stream). _ if params.pty_size.is_some() => Mode::Interactive, _ => Mode::Headless, @@ -52,7 +52,7 @@ fn resolve_mode(params: &SessionStartParams) -> Mode { #[tokio::main] async fn main() -> anyhow::Result<()> { - // CLI sub-mode: `agentd-adapter-zarvis --title-mode ""` runs + // CLI sub-mode: `construct-adapter-smith --title-mode ""` runs // one LLM completion that returns a short conversation title on // stdout. Used by the daemon to auto-name sessions on first input. let args: Vec = std::env::args().collect(); @@ -71,7 +71,7 @@ async fn main() -> anyhow::Result<()> { } let metadata = InitializeResult { - name: "zarvis".into(), + name: "smith".into(), version: env!("CARGO_PKG_VERSION").into(), capabilities: Capabilities { supports_input: true, @@ -88,7 +88,7 @@ async fn main() -> anyhow::Result<()> { Err(e) => { ctx.emit.emit(SessionEvent::Error { message: format!( - "{e}\n\nzarvis needs one of: AGENTD_ZARVIS_MODEL set, \ + "{e}\n\nsmith needs one of: CONSTRUCT_SMITH_MODEL set, \ ANTHROPIC_API_KEY set, OPENAI_API_KEY set, \ GEMINI_API_KEY set, or a local Ollama (set OLLAMA_HOST \ if not at localhost:11434)." @@ -104,7 +104,7 @@ async fn main() -> anyhow::Result<()> { Mode::Headless => agent::run(params, ctx, resolved).await, }; if let Err(e) = result { - tracing::warn!(error = ?e, "zarvis agent loop returned with error"); + tracing::warn!(error = ?e, "smith agent loop returned with error"); } }) .await diff --git a/crates/adapter-zarvis/src/observe.rs b/crates/adapter-zarvis/src/observe.rs index 7a7f1383..68b873de 100644 --- a/crates/adapter-zarvis/src/observe.rs +++ b/crates/adapter-zarvis/src/observe.rs @@ -1,7 +1,7 @@ //! Orchestrator-only event observer. //! //! When the zarvis adapter is running as the daemon's orchestrator -//! session (`AGENTD_SESSION_KIND=orchestrator`), it opens a second +//! session (`CONSTRUCT_SESSION_KIND=orchestrator`), it opens a second //! IPC connection to the daemon and subscribes to events from every //! other session. Filtered, those events flow back to the interactive //! agent loop as [`Observation`]s — the orchestrator surfaces them as diff --git a/crates/adapter-zarvis/src/persist.rs b/crates/adapter-zarvis/src/persist.rs index 9b7a71b6..51eca3d8 100644 --- a/crates/adapter-zarvis/src/persist.rs +++ b/crates/adapter-zarvis/src/persist.rs @@ -160,14 +160,14 @@ impl Persist { /// Resolve the session data dir from env, if any. pub fn session_data_dir_from_env() -> Option { - std::env::var("AGENTD_SESSION_DATA_DIR") + std::env::var("CONSTRUCT_SESSION_DATA_DIR") .ok() .map(PathBuf::from) } /// True if the daemon signaled this is a resumed session. pub fn is_resume() -> bool { - std::env::var("AGENTD_RESUME").as_deref() == Ok("1") + std::env::var("CONSTRUCT_RESUME").as_deref() == Ok("1") } #[cfg(test)] diff --git a/crates/adapter-zarvis/src/project_guide.rs b/crates/adapter-zarvis/src/project_guide.rs index f75536f6..64c3173b 100644 --- a/crates/adapter-zarvis/src/project_guide.rs +++ b/crates/adapter-zarvis/src/project_guide.rs @@ -8,7 +8,7 @@ //! `## Project guide` section. The file is the user's voice; the //! model is told to honor it unless explicitly overridden. //! -//! Disable with `AGENTD_ZARVIS_PROJECT_GUIDE=off`. +//! Disable with `CONSTRUCT_SMITH_PROJECT_GUIDE=off`. use std::path::{Path, PathBuf}; @@ -29,7 +29,7 @@ const MAX_BYTES: usize = 32 * 1024; /// above the user's home directory, to avoid pulling in system-wide /// files the user didn't intend). pub fn find(cwd: &Path) -> Option { - if std::env::var("AGENTD_ZARVIS_PROJECT_GUIDE").as_deref() == Ok("off") { + if std::env::var("CONSTRUCT_SMITH_PROJECT_GUIDE").as_deref() == Ok("off") { return None; } let home = std::env::var_os("HOME").map(PathBuf::from); @@ -97,7 +97,7 @@ mod tests { /// Serialize env-touching tests within this crate's test /// binary. Tests run in parallel by default; one test - /// setting `AGENTD_ZARVIS_PROJECT_GUIDE=off` was racing with + /// setting `CONSTRUCT_SMITH_PROJECT_GUIDE=off` was racing with /// another that expected it unset. The mutex makes /// `set_var` + `find` + `remove_var` atomic w.r.t. peers. static ENV_LOCK: Mutex<()> = Mutex::new(()); @@ -142,9 +142,9 @@ mod tests { let tmp = tempdir(); let path = tmp.join("AGENTS.md"); std::fs::write(&path, b"hello").unwrap(); - std::env::set_var("AGENTD_ZARVIS_PROJECT_GUIDE", "off"); + std::env::set_var("CONSTRUCT_SMITH_PROJECT_GUIDE", "off"); let result = find(&tmp); - std::env::remove_var("AGENTD_ZARVIS_PROJECT_GUIDE"); + std::env::remove_var("CONSTRUCT_SMITH_PROJECT_GUIDE"); assert!(result.is_none()); } diff --git a/crates/adapter-zarvis/src/provider/codex_oauth.rs b/crates/adapter-zarvis/src/provider/codex_oauth.rs index 7d7b89fd..130fac29 100644 --- a/crates/adapter-zarvis/src/provider/codex_oauth.rs +++ b/crates/adapter-zarvis/src/provider/codex_oauth.rs @@ -75,7 +75,7 @@ const CODEX_WS_BETA: &str = "responses_websockets=2026-02-06"; /// `system` arg the agent loop already builds (same shape every /// other provider uses). The Codex backend rejects empty /// `instructions`, so the final value must be non-empty either way. -const INSTRUCTIONS_ENV: &str = "AGENTD_ZARVIS_CODEX_INSTRUCTIONS"; +const INSTRUCTIONS_ENV: &str = "CONSTRUCT_SMITH_CODEX_INSTRUCTIONS"; /// Refresh tokens are good for ~30 days but the server-side window can /// be tighter under load. We refresh when the access_token is within @@ -128,7 +128,7 @@ pub struct Tokens { /// Returns the path to `auth.json`. Honors `$CODEX_HOME` first, then /// falls back to `$HOME/.codex/auth.json`. Mirrors what -/// `agentd-adapter-codex` already does to find rollouts so the two +/// `construct-adapter-codex` already does to find rollouts so the two /// crates agree on where codex stores its credential file. pub fn auth_json_path() -> Result { if let Ok(home) = std::env::var("CODEX_HOME") { @@ -212,7 +212,7 @@ pub struct CodexOauth { state: Arc>, http: reqwest::Client, /// [P0 spike] Reused Responses WebSocket connection (opt-in via - /// AGENTD_ZARVIS_CODEX_WS=1) to test whether a warm connection lifts + /// CONSTRUCT_SMITH_CODEX_WS=1) to test whether a warm connection lifts /// the prompt-cache hit-rate vs a stateless HTTP POST per request. ws: Arc>>, /// Set after a WS connect/transport failure so the session stops retrying @@ -651,7 +651,7 @@ impl CodexOauth { /// Resolve the `instructions` field for the Codex backend. Order: /// -/// 1. `AGENTD_ZARVIS_CODEX_INSTRUCTIONS` env var, if set and +/// 1. `CONSTRUCT_SMITH_CODEX_INSTRUCTIONS` env var, if set and /// non-empty — explicit operator override (e.g. to mirror Codex /// CLI exactly with the upstream `gpt_5_codex_prompt.md`). /// 2. The `system` argument the agent loop passes — same prompt @@ -801,10 +801,10 @@ pub fn build_responses_body( let input: Vec = messages.iter().flat_map(message_to_input_items).collect(); // Asks the server to emit reasoning summary deltas (surfaced via // `sink.reasoning_delta`). Optionally pin an explicit reasoning effort - // (low|medium|high) via `AGENTD_ZARVIS_REASONING_EFFORT`, mirroring Codex + // (low|medium|high) via `CONSTRUCT_SMITH_REASONING_EFFORT`, mirroring Codex // CLI's `model_reasoning_effort`; unset = the backend default. let mut reasoning = json!({ "summary": "auto" }); - if let Ok(effort) = std::env::var("AGENTD_ZARVIS_REASONING_EFFORT") { + if let Ok(effort) = std::env::var("CONSTRUCT_SMITH_REASONING_EFFORT") { let effort = effort.trim(); if !effort.is_empty() { reasoning["effort"] = json!(effort); @@ -837,7 +837,7 @@ pub fn build_responses_body( // requests to the same prompt-cache node so the prefix actually hits. // Without it, automatic prefix caching still works but routing is unstable // under load — measured as a low/erratic hit-rate (~31% vs Codex's ~97%). - if let Ok(key) = std::env::var("AGENTD_SESSION_ID") { + if let Ok(key) = std::env::var("CONSTRUCT_SESSION_ID") { if !key.is_empty() { body["prompt_cache_key"] = json!(key); } @@ -873,7 +873,7 @@ impl LlmProvider for CodexOauth { // Codex's Responses API uses `instructions` instead of an // inline system message. We pass the agent's `system` arg // through as-is, matching what every other provider does - // for the equivalent slot. `AGENTD_ZARVIS_CODEX_INSTRUCTIONS` + // for the equivalent slot. `CONSTRUCT_SMITH_CODEX_INSTRUCTIONS` // is an optional operator override (handy for mirroring // Codex CLI exactly with the upstream prompt). let instructions = resolve_instructions(system)?; @@ -903,7 +903,7 @@ impl LlmProvider for CodexOauth { // far better (~97%) than a stateless HTTP POST per request. On a connect // / pre-stream failure it falls back to HTTP and disables WS for the // rest of the session; a mid-stream failure propagates (no double-emit). - let ws_enabled = std::env::var("AGENTD_ZARVIS_CODEX_WS").as_deref() == Ok("1") + let ws_enabled = std::env::var("CONSTRUCT_SMITH_CODEX_WS").as_deref() == Ok("1") && !self.ws_disabled.load(Ordering::Relaxed); if ws_enabled { match self diff --git a/crates/adapter-zarvis/src/provider_watchdog.rs b/crates/adapter-zarvis/src/provider_watchdog.rs index e41f5ded..581141ab 100644 --- a/crates/adapter-zarvis/src/provider_watchdog.rs +++ b/crates/adapter-zarvis/src/provider_watchdog.rs @@ -15,8 +15,8 @@ use std::sync::{ }; use std::time::Duration; -const ENV_PROVIDER_IDLE_TIMEOUT_SECS: &str = "AGENTD_ZARVIS_PROVIDER_IDLE_TIMEOUT_SECS"; -const ENV_PROVIDER_RETRY_ATTEMPTS: &str = "AGENTD_ZARVIS_PROVIDER_RETRY_ATTEMPTS"; +const ENV_PROVIDER_IDLE_TIMEOUT_SECS: &str = "CONSTRUCT_SMITH_PROVIDER_IDLE_TIMEOUT_SECS"; +const ENV_PROVIDER_RETRY_ATTEMPTS: &str = "CONSTRUCT_SMITH_PROVIDER_RETRY_ATTEMPTS"; /// Idle = no stream activity from upstream (see `WatchdogSink`: any /// `delta` / `reasoning_delta` / `progress` ping resets it). 90s leaves /// headroom for the gap between the first event and the first content on diff --git a/crates/adapter-zarvis/src/skills.rs b/crates/adapter-zarvis/src/skills.rs index 06327d5d..ac09f9d5 100644 --- a/crates/adapter-zarvis/src/skills.rs +++ b/crates/adapter-zarvis/src/skills.rs @@ -6,7 +6,7 @@ //! selected skill file on demand, instead of eagerly stuffing every //! skill body into context. //! -//! Disable with `AGENTD_ZARVIS_SKILLS=off`. +//! Disable with `CONSTRUCT_SMITH_SKILLS=off`. use std::path::{Path, PathBuf}; @@ -25,7 +25,7 @@ pub(crate) struct Skill { } pub(crate) fn discover(cwd: &Path) -> Vec { - if std::env::var("AGENTD_ZARVIS_SKILLS").as_deref() == Ok("off") { + if std::env::var("CONSTRUCT_SMITH_SKILLS").as_deref() == Ok("off") { return Vec::new(); } @@ -248,7 +248,7 @@ metadata: std::fs::create_dir_all(&cwd).unwrap(); std::env::set_var("CODEX_HOME", &tmp); std::env::set_var("CLAUDE_HOME", tmp.join("claude-home")); - std::env::remove_var("AGENTD_ZARVIS_SKILLS"); + std::env::remove_var("CONSTRUCT_SMITH_SKILLS"); write_skill( &tmp.join("skills/.system/imagegen/SKILL.md"), @@ -287,7 +287,7 @@ metadata: let tmp = tempdir(); std::env::set_var("CODEX_HOME", &tmp); std::env::remove_var("CLAUDE_HOME"); - std::env::remove_var("AGENTD_ZARVIS_SKILLS"); + std::env::remove_var("CONSTRUCT_SMITH_SKILLS"); let path = tmp.join("skills/.system/openai-docs/SKILL.md"); write_skill(&path, "openai-docs", "Use official OpenAI docs."); @@ -305,7 +305,7 @@ metadata: let tmp = tempdir(); std::env::set_var("CODEX_HOME", &tmp); std::env::set_var("CLAUDE_HOME", tmp.join("claude-home")); - std::env::set_var("AGENTD_ZARVIS_SKILLS", "off"); + std::env::set_var("CONSTRUCT_SMITH_SKILLS", "off"); write_skill( &tmp.join("skills/.system/imagegen/SKILL.md"), "imagegen", @@ -315,7 +315,7 @@ metadata: assert!(discover(&tmp).is_empty()); std::env::remove_var("CODEX_HOME"); std::env::remove_var("CLAUDE_HOME"); - std::env::remove_var("AGENTD_ZARVIS_SKILLS"); + std::env::remove_var("CONSTRUCT_SMITH_SKILLS"); } fn write_skill(path: &Path, name: &str, description: &str) { diff --git a/crates/adapter-zarvis/src/tasks.rs b/crates/adapter-zarvis/src/tasks.rs index 84b2b591..87d38945 100644 --- a/crates/adapter-zarvis/src/tasks.rs +++ b/crates/adapter-zarvis/src/tasks.rs @@ -3,7 +3,7 @@ //! Every tool invocation goes through a [`Supervisor`] that races the //! tool's `tokio::spawn` handle against three signals: //! -//! - **Auto-bg timer** (60 s by default; `AGENTD_TOOL_BG_AFTER_MS`): +//! - **Auto-bg timer** (60 s by default; `CONSTRUCT_TOOL_BG_AFTER_MS`): //! the agent decides the tool's been running long enough that the //! conversation shouldn't keep blocking on it. The handle is //! detached, a placeholder `ToolResult` is synthesized, and the @@ -37,10 +37,10 @@ use tokio::sync::{mpsc, oneshot, Mutex}; pub const BG_PLACEHOLDER_OUTPUT: &str = "(running in background; will report when complete)"; /// Default auto-background threshold. Overridable via -/// `AGENTD_TOOL_BG_AFTER_MS`. +/// `CONSTRUCT_TOOL_BG_AFTER_MS`. pub const DEFAULT_BG_AFTER_MS: u64 = 60_000; /// Default time before the `[bg]` / `[kill]` buttons appear on a -/// running tool block. Overridable via `AGENTD_TOOL_BUTTONS_AFTER_MS`. +/// running tool block. Overridable via `CONSTRUCT_TOOL_BUTTONS_AFTER_MS`. /// Read by the TUI's `synth_block` — the adapter doesn't act on /// this directly, it's exported here for a single source of truth. pub const DEFAULT_BUTTONS_AFTER_MS: u64 = 15_000; @@ -125,7 +125,7 @@ pub type BgCompletionRx = mpsc::UnboundedReceiver; /// Read the auto-bg threshold from env or fall back to the default. pub fn bg_after_duration() -> Duration { - let ms = std::env::var("AGENTD_TOOL_BG_AFTER_MS") + let ms = std::env::var("CONSTRUCT_TOOL_BG_AFTER_MS") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(DEFAULT_BG_AFTER_MS); diff --git a/crates/adapter-zarvis/src/title_mode.rs b/crates/adapter-zarvis/src/title_mode.rs index 7a8b214b..f56c8bd9 100644 --- a/crates/adapter-zarvis/src/title_mode.rs +++ b/crates/adapter-zarvis/src/title_mode.rs @@ -2,9 +2,9 @@ //! //! When the daemon detects a session's first user message and no //! user-set title exists, it shells out to -//! `agentd-adapter-zarvis --title-mode ""`. This module powers +//! `construct-adapter-smith --title-mode ""`. This module powers //! that invocation: it picks a model the same way the regular adapter -//! does (AGENTD_ZARVIS_MODEL → API-key fallback), runs a single +//! does (CONSTRUCT_SMITH_MODEL → API-key fallback), runs a single //! tools-disabled completion with a short system prompt, and prints //! the cleaned-up title to stdout. @@ -31,7 +31,7 @@ impl TextSink for CaptureSink { } fn pick_default_spec_str() -> String { - if let Ok(s) = std::env::var("AGENTD_ZARVIS_MODEL") { + if let Ok(s) = std::env::var("CONSTRUCT_SMITH_MODEL") { if !s.trim().is_empty() { return s; } diff --git a/crates/adapter-zarvis/src/tools/agentd.rs b/crates/adapter-zarvis/src/tools/agentd.rs index 6b0dd09d..4663b0ac 100644 --- a/crates/adapter-zarvis/src/tools/agentd.rs +++ b/crates/adapter-zarvis/src/tools/agentd.rs @@ -587,7 +587,7 @@ simple_write_tool!( /// the daemon. The agentd-control tools default to "this" session /// when the LLM doesn't supply a session_id explicitly. fn calling_session_id() -> Option { - std::env::var("AGENTD_SESSION_ID").ok() + std::env::var("CONSTRUCT_SESSION_ID").ok() } pub struct LoopCreate; @@ -633,7 +633,7 @@ impl Tool for LoopCreate { .and_then(|v| v.as_str()) .map(|s| s.to_string()) .or_else(calling_session_id) - .ok_or_else(|| anyhow!("session_id required (and AGENTD_SESSION_ID unset)"))?; + .ok_or_else(|| anyhow!("session_id required (and CONSTRUCT_SESSION_ID unset)"))?; let secs = input .get("interval_seconds") .and_then(|v| v.as_u64()) diff --git a/crates/adapter-zarvis/src/tools/subagent.rs b/crates/adapter-zarvis/src/tools/subagent.rs index 3d6863c5..3f267499 100644 --- a/crates/adapter-zarvis/src/tools/subagent.rs +++ b/crates/adapter-zarvis/src/tools/subagent.rs @@ -51,8 +51,8 @@ fn now_ms() -> i64 { } fn registry_path() -> Result { - let dir = std::env::var("AGENTD_SESSION_DATA_DIR") - .context("AGENTD_SESSION_DATA_DIR is not set; subagents require an agentd session")?; + let dir = std::env::var("CONSTRUCT_SESSION_DATA_DIR") + .context("CONSTRUCT_SESSION_DATA_DIR is not set; subagents require an agentd session")?; Ok(PathBuf::from(dir).join("zarvis-subagents.json")) } @@ -181,7 +181,7 @@ impl Tool for Create { .or_else(|| Some(format!("subagent:{}", harness))); let mut env = std::collections::HashMap::new(); env.insert( - "AGENTD_PARENT_SESSION_ID".to_string(), + "CONSTRUCT_PARENT_SESSION_ID".to_string(), ctx.session_id.clone(), ); let params = CreateSessionParams { diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index d60577f2..2170f5dd 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -6,10 +6,10 @@ license.workspace = true authors.workspace = true repository.workspace = true rust-version.workspace = true -description = "agent: TUI client for agentd" +description = "construct: TUI client for constructd" [[bin]] -name = "agent" +name = "construct" path = "src/main.rs" [dependencies] diff --git a/crates/cli/src/app.rs b/crates/cli/src/app.rs index 9f088a77..bf4434a4 100644 --- a/crates/cli/src/app.rs +++ b/crates/cli/src/app.rs @@ -337,7 +337,7 @@ pub enum MinibufferIntent { }, /// Confirmation prompt for restarting a terminated (`Done` / /// `Errored`) session. Single-key dispatch: `y`/Enter respawns - /// the adapter (with `AGENTD_RESUME=1` so persistent harnesses + /// the adapter (with `CONSTRUCT_RESUME=1` so persistent harnesses /// reload state); anything else cancels. RestartConfirm { session_id: String, @@ -1693,7 +1693,7 @@ pub async fn run_with_socket(socket: std::path::PathBuf) -> Result<()> { // never blocks startup (a stale cache refreshes in the background for the // next launch). Held in a dedicated field so it persists in the modeline // until the user upgrades, rather than expiring after a few seconds like a - // transient status. Opt out with AGENTD_NO_UPDATE_CHECK=1. + // transient status. Opt out with CONSTRUCT_NO_UPDATE_CHECK=1. app.update_notice = crate::upgrade::cached_update_notice(); if app.selected_needs_hydration() { @@ -1812,7 +1812,9 @@ async fn run_loop( if app.connected && app.client.is_disconnected() { app.connected = false; reconnect = Some(ReconnectState::new(Instant::now())); - app.set_status("daemon disconnected — reconnecting… (press q to quit)".to_string()); + app.set_status( + "daemon disconnected — reconnecting… (press C-x C-c to quit TUI)".to_string(), + ); } app.prune_finished_transitions(); app.poll_remote_control_task().await; @@ -1838,7 +1840,7 @@ async fn run_loop( Err(e) => { state.schedule_next(now); app.set_status(format!( - "daemon disconnected — reconnecting… (press q to quit; last error: {e})" + "daemon disconnected — reconnecting… (press C-x C-c to quit TUI; last error: {e})" )); } } @@ -2111,7 +2113,8 @@ async fn run_loop( app.connected = false; reconnect = Some(ReconnectState::new(Instant::now())); app.set_status( - "daemon disconnected — reconnecting… (press q to quit)".to_string(), + "daemon disconnected — reconnecting… (press C-x C-c to quit TUI)" + .to_string(), ); } } @@ -3828,7 +3831,7 @@ impl App { || self .sessions .iter() - .any(|s| s.id == session_id && s.harness == "zarvis") + .any(|s| s.id == session_id && s.harness == "smith") } /// Cycle the selected session's approval mode. @@ -5396,8 +5399,24 @@ impl App { // /tasks modal: Esc closes it; everything else falls through // (the popup itself is read-only at the keyboard layer in // v1 — mouse-only row interactions). - if !self.connected && matches!(key.code, KeyCode::Char('q')) { - self.should_quit = true; + // In disconnected state, still allow standard keymap chords for + // quitting and quick palette access. `/` commands are not + // accepted here, but `C-x C-c` remains available. + if !self.connected { + let res = self.chord_state.handle(key, &self.keymap); + self.chord_label = self.chord_state.label(); + match res { + KeymapResult::Action(KeyAction::Quit) => { + self.should_quit = true; + } + KeymapResult::Action(action) => { + self.run_action(action).await; + } + KeymapResult::Pending(label) => { + self.chord_label = label; + } + KeymapResult::Unhandled => {} + } return; } if self.tasks_popup.is_some() { @@ -6868,7 +6887,7 @@ impl App { // daemon.restart RPC will close the IPC connection // as the new process replaces the old; the TUI // observes that as a "daemon disconnected" status - // and the user must re-run `agent` to reconnect + // and the user must re-run `construct` to reconnect // (auto-reconnect is follow-up work, see issue #90). let mut parts = arg.trim().splitn(2, char::is_whitespace); let sub = parts.next().unwrap_or(""); @@ -8304,9 +8323,9 @@ mod tests { } #[tokio::test] - async fn approval_prompt_does_not_open_for_zarvis_session() { + async fn approval_prompt_does_not_open_for_smith_session() { let (mut app, _dir, server) = captured_app().await; - app.sessions[0].harness = "zarvis".into(); + app.sessions[0].harness = "smith".into(); app.maybe_open_approval_prompt( "s1".into(), @@ -8319,7 +8338,7 @@ mod tests { assert!( app.minibuffer.is_none(), - "zarvis renders approval inline in the session PTY" + "smith renders approval inline in the session PTY" ); server.abort(); } @@ -8451,7 +8470,7 @@ mod tests { async fn captured_app() -> (App, tempfile::TempDir, tokio::task::JoinHandle<()>) { use tokio::net::UnixListener; let dir = tempfile::tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { loop { @@ -8470,7 +8489,7 @@ mod tests { async fn empty_app() -> (App, tempfile::TempDir, tokio::task::JoinHandle<()>) { use tokio::net::UnixListener; let dir = tempfile::tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { loop { @@ -8742,7 +8761,7 @@ mod tests { #[tokio::test] async fn update_notice_renders_right_aligned_in_modeline() { let (mut app, _dir, server) = empty_app().await; - app.update_notice = Some("↑ construct 9.9.9 · agent upgrade".to_string()); + app.update_notice = Some("↑ construct 9.9.9 · construct upgrade".to_string()); let backend = ratatui::backend::TestBackend::new(120, 36); let mut terminal = ratatui::Terminal::new(backend).expect("terminal"); @@ -8753,12 +8772,12 @@ mod tests { let screen = rendered_text(terminal.backend().buffer()); let modeline = screen .lines() - .find(|l| l.contains("↑ construct 9.9.9 · agent upgrade")) + .find(|l| l.contains("↑ construct 9.9.9 · construct upgrade")) .expect("update notice should be on screen"); // Right-aligned: only padding follows it to the right edge. assert!( - modeline.trim_end().ends_with("agent upgrade"), + modeline.trim_end().ends_with("construct upgrade"), "notice should sit at the right edge:\n{modeline}" ); // ...and it lives in the right half, not inline on the left. @@ -8999,11 +9018,11 @@ mod tests { } #[tokio::test] - async fn disconnected_q_quits_even_when_pty_would_capture_keys() { + async fn disconnected_c_x_c_quits_even_when_pty_would_capture_keys() { use tokio::net::UnixListener; let dir = tempfile::tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { let _ = listener.accept().await; @@ -9016,7 +9035,9 @@ mod tests { let mut app = test_app(client, vec![summary]); app.connected = false; - app.on_key(KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE)) + app.on_key(KeyEvent::new(KeyCode::Char('x'), KeyModifiers::CONTROL)) + .await; + app.on_key(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)) .await; assert!(app.should_quit); @@ -9031,7 +9052,7 @@ mod tests { use tokio::net::UnixListener; let dir = tempfile::tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { let _ = listener.accept().await; @@ -9094,7 +9115,7 @@ mod tests { use tokio::net::UnixListener; let dir = tempfile::tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { let _ = listener.accept().await; @@ -9166,7 +9187,7 @@ mod tests { use tokio::net::UnixListener; let dir = tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { loop { @@ -9487,7 +9508,7 @@ mod tests { use tokio::net::UnixListener; let dir = tempfile::tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { let _ = listener.accept().await; @@ -9617,7 +9638,7 @@ mod tests { async fn subagents_render_under_parent_and_default_expanded() { use tokio::net::UnixListener; let dir = tempfile::tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let _server = tokio::spawn(async move { loop { @@ -9718,7 +9739,7 @@ mod tests { use tokio::sync::{mpsc, Notify}; let dir = tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let release_input = Arc::new(Notify::new()); let (input_seen_tx, mut input_seen_rx) = mpsc::unbounded_channel(); @@ -9797,7 +9818,7 @@ mod tests { use tokio::sync::oneshot; let dir = tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let (seen_tx, seen_rx) = oneshot::channel(); let server = tokio::spawn(async move { @@ -9878,7 +9899,7 @@ mod tests { use tokio::net::UnixListener; let dir = tempfile::tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { let _ = listener.accept().await; @@ -9887,7 +9908,7 @@ mod tests { let client = Client::connect(&sock).await.expect("client connects"); let mut summary = summary_with_kind(agentd_protocol::SessionKind::User); - summary.harness = "zarvis".into(); + summary.harness = "smith".into(); summary.has_pty = true; let mut app = test_app(client, vec![summary]); app.view = ViewMode::Terminal; @@ -9984,7 +10005,7 @@ mod tests { use tokio::sync::{mpsc, Notify}; let dir = tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let release_transcript = Arc::new(Notify::new()); let (transcript_seen_tx, mut transcript_seen_rx) = mpsc::unbounded_channel(); @@ -10114,7 +10135,7 @@ mod tests { use tokio::net::UnixListener; let dir = tempdir().expect("tempdir"); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { loop { diff --git a/crates/cli/src/keymap.rs b/crates/cli/src/keymap.rs index 0360a571..6711dcf6 100644 --- a/crates/cli/src/keymap.rs +++ b/crates/cli/src/keymap.rs @@ -81,7 +81,7 @@ impl Profile { } pub fn from_env() -> Self { - match std::env::var("AGENTD_KEYMAP").as_deref() { + match std::env::var("CONSTRUCT_KEYMAP").as_deref() { Ok("vim") => Profile::Vim, _ => Profile::Emacs, } @@ -188,7 +188,6 @@ fn emacs() -> Keymap { let bindings = vec![ // Quit (Chord(vec![ctrl('x'), ctrl('c')]), Quit), - (Chord(vec![ch('q')]), Quit), // Session navigation (Chord(vec![ctrl('n')]), NextSession), (Chord(vec![ctrl('p')]), PrevSession), @@ -266,7 +265,6 @@ fn emacs() -> Keymap { fn vim() -> Keymap { use KeyAction::*; let bindings = vec![ - (Chord(vec![ch('q')]), Quit), (Chord(vec![ch('j')]), NextSession), (Chord(vec![ch('k')]), PrevSession), (Chord(vec![key(KeyCode::Down)]), NextSession), diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index c5b73112..e52880aa 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -15,7 +15,11 @@ use agentd_client::Client; use agentd_protocol::paths::Paths; #[derive(Debug, Parser)] -#[command(name = "agent", about = "agent: TUI client for agentd", version)] +#[command( + name = "construct", + about = "construct: TUI client for constructd", + version +)] struct Cli { /// Override the daemon socket path. #[arg(long, global = true)] @@ -60,7 +64,7 @@ enum Command { Send { session_id: String, text: String }, /// Internal: `PreToolUse` hook body for the AskUserQuestion chat-gate. /// Reads the hook payload on stdin; if a chat viewer is active for - /// `$AGENTD_SESSION_ID`, prints a `deny` decision that degrades Claude's + /// `$CONSTRUCT_SESSION_ID`, prints a `deny` decision that degrades Claude's /// picker to a plain-text question. Fails open (allow) on any error. #[command(hide = true)] AskGate, @@ -96,7 +100,7 @@ enum Command { /// Install a specific release tag (e.g. v0.2.0). Default: latest. #[arg(long)] version: Option, - /// Install directory. Default: the directory of the running `agent`. + /// Install directory. Default: the directory of the running `construct`. #[arg(long)] bin_dir: Option, /// After upgrading, ask the running daemon to restart so the new @@ -231,7 +235,7 @@ async fn main() -> Result<()> { use std::io::Read as _; let mut buf = String::new(); let _ = std::io::stdin().read_to_string(&mut buf); - let session_id = std::env::var("AGENTD_SESSION_ID") + let session_id = std::env::var("CONSTRUCT_SESSION_ID") .ok() .filter(|s| !s.is_empty()) .or_else(|| { diff --git a/crates/cli/src/pty_render.rs b/crates/cli/src/pty_render.rs index 890e24f3..858b8299 100644 --- a/crates/cli/src/pty_render.rs +++ b/crates/cli/src/pty_render.rs @@ -128,13 +128,13 @@ struct SynthOutput { } /// How long a block must be running before the keyboard-control hint -/// appears. Overridable via `AGENTD_TOOL_BUTTONS_AFTER_MS`. +/// appears. Overridable via `CONSTRUCT_TOOL_BUTTONS_AFTER_MS`. /// Default lowered from 15s to 7s so the affordance is visible /// before the typical user gives up on the tool. Auto-promote /// still defaults to 60s — the hint just announces itself /// earlier. fn buttons_after_ms() -> u64 { - std::env::var("AGENTD_TOOL_BUTTONS_AFTER_MS") + std::env::var("CONSTRUCT_TOOL_BUTTONS_AFTER_MS") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(7_000) diff --git a/crates/cli/src/theme.rs b/crates/cli/src/theme.rs index 1fd65039..e4325306 100644 --- a/crates/cli/src/theme.rs +++ b/crates/cli/src/theme.rs @@ -3,7 +3,7 @@ //! Defaults to a Matrix-inspired palette. Users can override any slot in: //! //! ```toml -//! # $AGENTD_CONFIG_DIR/theme.toml, default ~/.config/agentd/theme.toml +//! # $CONSTRUCT_CONFIG_DIR/theme.toml, default ~/.config/construct/theme.toml //! [colors] //! text = "#b8ffcc" //! accent = "#39ff88" diff --git a/crates/cli/src/ui.rs b/crates/cli/src/ui.rs index f0aaf5a5..abd94565 100644 --- a/crates/cli/src/ui.rs +++ b/crates/cli/src/ui.rs @@ -5464,7 +5464,7 @@ fn approval_mode_modeline_label(s: &SessionSummary) -> Option<&'static str> { } fn is_smith_like_harness(name: &str) -> bool { - matches!(name, "smith" | "zarvis") + matches!(name, "smith") } fn render_modeline_approval_mode_tooltip(f: &mut Frame, app: &App) { @@ -5688,7 +5688,7 @@ fn render_help(f: &mut Frame, area: Rect, theme: &Theme) -> Rect { } const HELP_TEXT: &str = " -emacs keymap (default; AGENTD_KEYMAP=vim for vim profile) +emacs keymap (default; CONSTRUCT_KEYMAP=vim for vim profile) getting started A session is one live task or terminal that construct keeps in the list. @@ -7923,9 +7923,9 @@ mod tests { #[test] fn is_headless_only_for_headless_mode() { - assert!(is_headless(&summary_with_mode("zarvis", Some("headless")))); + assert!(is_headless(&summary_with_mode("smith", Some("headless")))); assert!(!is_headless(&summary_with_mode( - "zarvis", + "smith", Some("interactive") ))); // Missing mode is treated as not-headless (older sessions @@ -7938,20 +7938,20 @@ mod tests { // Headless sessions get a "(headless) " prefix so the list / // title bar visibly distinguish them from interactive ones. assert_eq!( - harness_label(&summary_with_mode("zarvis", Some("headless"))), - "(headless) zarvis" + harness_label(&summary_with_mode("smith", Some("headless"))), + "(headless) smith" ); // Interactive and mode-less sessions render the bare harness. assert_eq!( - harness_label(&summary_with_mode("zarvis", Some("interactive"))), - "zarvis" + harness_label(&summary_with_mode("smith", Some("interactive"))), + "smith" ); assert_eq!(harness_label(&summary_with_mode("shell", None)), "shell"); } #[test] - fn approval_mode_modeline_label_shows_manual_for_zarvis() { - let s = summary_with_mode("zarvis", Some("interactive")); + fn approval_mode_modeline_label_shows_manual_for_smith() { + let s = summary_with_mode("smith", Some("interactive")); assert_eq!(approval_mode_modeline_label(&s), Some("manual")); } @@ -7963,14 +7963,14 @@ mod tests { #[test] fn approval_mode_modeline_label_uses_non_manual_badge() { - let mut s = summary_with_mode("zarvis", Some("interactive")); + let mut s = summary_with_mode("smith", Some("interactive")); s.approval_mode = agentd_protocol::ApprovalMode::UnsafeAuto; assert_eq!(approval_mode_modeline_label(&s), Some("unsafe-auto")); } #[test] - fn zarvis_running_animates_only_while_agent_active() { - let mut s = summary_with_mode("zarvis", Some("interactive")); + fn smith_running_animates_only_while_agent_active() { + let mut s = summary_with_mode("smith", Some("interactive")); s.state = SessionState::Running; // Mid-turn: agent active → animate, even with no recent PTY bytes. assert!(session_should_animate_status(&s, false, true)); @@ -7993,7 +7993,7 @@ mod tests { #[test] fn awaiting_input_status_stays_static() { - let mut s = summary_with_mode("zarvis", Some("interactive")); + let mut s = summary_with_mode("smith", Some("interactive")); s.state = SessionState::AwaitingInput; // Not Running → never animates, regardless of activity signals. assert!(!session_should_animate_status(&s, true, true)); diff --git a/crates/cli/src/upgrade.rs b/crates/cli/src/upgrade.rs index 8dcfccf3..879a1483 100644 --- a/crates/cli/src/upgrade.rs +++ b/crates/cli/src/upgrade.rs @@ -1,8 +1,8 @@ -//! Self-update for the `agent` CLI. +//! Self-update for the `construct` CLI. //! -//! `agent upgrade` re-runs the project installer (embedded at build time, so +//! `construct upgrade` re-runs the project installer (embedded at build time, so //! the download + checksum + atomic-replace logic lives in exactly one place) -//! against the directory the running `agent` binary lives in — which, per the +//! against the directory the running `construct` binary lives in — which, per the //! install layout, is where all the agentd binaries sit together. A separate, //! cached, fail-silent check powers the "update available" notice the TUI //! shows on startup. @@ -16,7 +16,7 @@ use agentd_protocol::paths::Paths; /// GitHub `owner/repo` the release assets and installer come from. const REPO: &str = "zarvis-ai/agentd"; -/// The installer, baked in at build time so `agent upgrade` and `install.sh` +/// The installer, baked in at build time so `construct upgrade` and `install.sh` /// can never drift apart. const INSTALL_SH: &str = include_str!("../../../install.sh"); /// How long a cached update check stays fresh before a background refresh. @@ -128,11 +128,11 @@ async fn refresh_cache() { /// A one-line "newer version available" notice for the TUI to surface, or /// `None`. Reads only the on-disk cache (instant, never blocks startup); if /// the cache is stale it kicks off a background refresh for the next launch. -/// Opt out entirely with `AGENTD_NO_UPDATE_CHECK=1`. +/// Opt out entirely with `CONSTRUCT_NO_UPDATE_CHECK=1`. /// /// Must be called from within a Tokio runtime (it may spawn the refresh). pub fn cached_update_notice() -> Option { - if std::env::var_os("AGENTD_NO_UPDATE_CHECK").is_some() { + if std::env::var_os("CONSTRUCT_NO_UPDATE_CHECK").is_some() { return None; } let cache = read_cache().unwrap_or_default(); @@ -145,12 +145,12 @@ pub fn cached_update_notice() -> Option { /// The notice string for `latest` vs `current`, or `None` when `latest` is /// not strictly newer (or unparseable). fn notice_for(latest: &str, current: &str) -> Option { - is_newer(latest, current).then(|| format!("↑ construct {latest} · agent upgrade")) + is_newer(latest, current).then(|| format!("↑ construct {latest} · construct upgrade")) } -// --- `agent upgrade` ------------------------------------------------------- +// --- `construct upgrade` ------------------------------------------------------- -/// Run the `agent upgrade` subcommand. +/// Run the `construct upgrade` subcommand. pub async fn run( version: Option, bin_dir: Option, @@ -163,7 +163,9 @@ pub async fn run( if check { match fetch_latest().await { Some(latest) if is_newer(&latest, current) => { - println!("construct {latest} available (you have {current}). Run `agent upgrade`."); + println!( + "construct {latest} available (you have {current}). Run `construct upgrade`." + ); } Some(latest) => { println!("up to date (construct {current}; latest {latest}).") @@ -177,7 +179,7 @@ pub async fn run( return Ok(()); } - // Install into the directory the running `agent` lives in — that's where + // Install into the directory the running `construct` lives in — that's where // the whole binary set sits, and the daemon resolves adapters as siblings // of its own path. let dir = match bin_dir { @@ -207,9 +209,9 @@ pub async fn run( println!("Upgrading construct to {target} in {}", dir.display()); let mut cmd = tokio::process::Command::new("sh"); - cmd.arg(script.path()).env("AGENTD_BIN_DIR", &dir); + cmd.arg(script.path()).env("CONSTRUCT_BIN_DIR", &dir); if let Some(v) = &version { - cmd.env("AGENTD_VERSION", v); + cmd.env("CONSTRUCT_VERSION", v); } let status = cmd.status().await.context("run installer")?; if !status.success() { @@ -263,7 +265,7 @@ mod tests { fn notice_only_when_a_newer_version_exists() { assert_eq!( notice_for("0.2.0", "0.1.0").as_deref(), - Some("↑ construct 0.2.0 · agent upgrade") + Some("↑ construct 0.2.0 · construct upgrade") ); assert_eq!(notice_for("0.1.0", "0.1.0"), None); assert_eq!(notice_for("0.1.0", "0.2.0"), None); diff --git a/crates/cli/tests/reconnect.rs b/crates/cli/tests/reconnect.rs index fb57f2af..7ebf1f1c 100644 --- a/crates/cli/tests/reconnect.rs +++ b/crates/cli/tests/reconnect.rs @@ -18,7 +18,7 @@ async fn test_reconnect_flow() { let global_timeout = tokio::time::Duration::from_secs(10); let res = tokio::time::timeout(global_timeout, async { let dir = tempdir().unwrap(); - let sock = dir.path().join("agentd.sock"); + let sock = dir.path().join("construct.sock"); let _ = std::fs::remove_file(&sock); // helper to run a one-shot mock server that accepts a single diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 53064029..8bcef7b4 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -455,7 +455,7 @@ impl Client { /// Respawn a session's adapter (TUI restart-confirm flow). Used /// on a `Done` session to bring it back to life so the user can /// keep typing. The daemon launches the new adapter with - /// `AGENTD_RESUME=1`. + /// `CONSTRUCT_RESUME=1`. pub async fn restart(&self, id: &str) -> Result<()> { let _: serde_json::Value = self .request( diff --git a/crates/daemon/Cargo.toml b/crates/daemon/Cargo.toml index 5bc19856..003bf31d 100644 --- a/crates/daemon/Cargo.toml +++ b/crates/daemon/Cargo.toml @@ -9,7 +9,7 @@ rust-version.workspace = true description = "agentd daemon: supervises agent sessions across harnesses" [[bin]] -name = "agentd" +name = "constructd" path = "src/main.rs" [dependencies] diff --git a/crates/daemon/assets/index.html b/crates/daemon/assets/index.html index dd55676a..e86336c7 100644 --- a/crates/daemon/assets/index.html +++ b/crates/daemon/assets/index.html @@ -5648,7 +5648,7 @@ function hasSemanticChatView(s) { const harness = (s && s.harness) || ""; return ( - harness === "zarvis" || + harness === "smith" || harness === "codex" || harness === "claude" || harness === "antigravity" @@ -5662,7 +5662,7 @@ // `pty_replay` screen snapshot, so past tool calls show on switch. // Mirrors the desktop TUI's transcript-preferred hydration. (issue #134) function isStructuredToolSession(s) { - return !!(s && s.harness === "zarvis"); + return !!(s && s.harness === "smith"); } function sessionHarnessLabel(s) { diff --git a/crates/daemon/src/adapter.rs b/crates/daemon/src/adapter.rs index 55bb5e10..014b6db0 100644 --- a/crates/daemon/src/adapter.rs +++ b/crates/daemon/src/adapter.rs @@ -261,7 +261,7 @@ impl Adapter { } let _ = std::fs::remove_file(&socket_path); env.insert( - "AGENTD_ADAPTER_SOCKET".to_string(), + "CONSTRUCT_ADAPTER_SOCKET".to_string(), socket_path.to_string_lossy().to_string(), ); diff --git a/crates/daemon/src/config.rs b/crates/daemon/src/config.rs index 84f7fbec..452c6efe 100644 --- a/crates/daemon/src/config.rs +++ b/crates/daemon/src/config.rs @@ -1,4 +1,4 @@ -//! Config loading. Looks at `~/.config/agentd/config.toml`, merging built-in +//! Config loading. Looks at `~/.config/construct/config.toml`, merging built-in //! adapter defaults underneath. use agentd_protocol::paths::Paths; @@ -12,25 +12,25 @@ pub const DEFAULT_CONFIG_TOML: &str = r#"# construct configuration # to override. # [adapters.shell] -# binary = "agentd-adapter-shell" +# binary = "construct-adapter-shell" # description = "Generic shell command runner" # [adapters.claude] -# binary = "agentd-adapter-claude" +# binary = "construct-adapter-claude" # description = "Claude Code" # [adapters.codex] -# binary = "agentd-adapter-codex" +# binary = "construct-adapter-codex" # description = "OpenAI Codex" # [adapters.smith.env] # # Per-harness env vars merged into every spawned session. Lets # # operators set defaults like the model from config.toml instead # # of needing to export them in the shell that launches the daemon. -# # Per-session env (`agent new --env KEY=VAL`) takes precedence. -# # AGENTD_ZARVIS_MODEL = "codex-oauth:gpt-5.5" +# # Per-session env (`construct new --env KEY=VAL`) takes precedence. +# # CONSTRUCT_SMITH_MODEL = "codex-oauth:gpt-5.5" # # -# # "zarvis" remains supported as a compatibility alias. +# smith is the native built-in harness. # [defaults] # worktree = false # default value of session.worktree if not specified @@ -97,16 +97,16 @@ pub struct AdapterConfig { /// shell where the daemon launches. Merged INTO the per-session /// `env_with_meta` (see `daemon/src/session.rs`); existing /// per-session env takes precedence so an explicit - /// `agent new --env KEY=VAL` still overrides. + /// `construct new --env KEY=VAL` still overrides. /// - /// Example: pin every new smith (formerly zarvis) session to use the + /// Example: pin every new smith session to use the /// Codex OAuth path (subscription-billed) instead of the heuristic /// fallback: /// path (subscription-billed) instead of the heuristic fallback: /// /// ```toml /// [adapters.smith] - /// env = { AGENTD_ZARVIS_MODEL = "codex-oauth:gpt-5.5" } + /// env = { CONSTRUCT_SMITH_MODEL = "codex-oauth:gpt-5.5" } /// ``` #[serde(default)] pub env: HashMap, @@ -127,34 +127,29 @@ pub struct BuiltinAdapter { pub const BUILTIN_ADAPTERS: &[BuiltinAdapter] = &[ BuiltinAdapter { name: "shell", - binary: "agentd-adapter-shell", + binary: "construct-adapter-shell", description: "Generic shell command runner", }, BuiltinAdapter { name: "claude", - binary: "agentd-adapter-claude", + binary: "construct-adapter-claude", description: "Claude Code (wraps the `claude` CLI)", }, BuiltinAdapter { name: "codex", - binary: "agentd-adapter-codex", + binary: "construct-adapter-codex", description: "OpenAI Codex (wraps the `codex` CLI)", }, BuiltinAdapter { name: "antigravity", - binary: "agentd-adapter-antigravity", + binary: "construct-adapter-antigravity", description: "Google Antigravity (wraps the `agy` CLI)", }, BuiltinAdapter { name: "smith", - binary: "agentd-adapter-zarvis", + binary: "construct-adapter-smith", description: "Built-in multi-provider agent (OpenAI / Anthropic / Ollama)", }, - BuiltinAdapter { - name: "zarvis", - binary: "agentd-adapter-zarvis", - description: "Compatibility alias for smith", - }, ]; impl Config { @@ -169,7 +164,7 @@ impl Config { }; // Layer in built-ins so users don't have to declare them. // Important: we layer at the FIELD level, not the entry level - // — a user who declared `[adapters.zarvis] env = {...}` (only + // — a user who declared `[adapters.smith] env = {...}` (only // to set per-harness env defaults) still needs the builtin // `binary` + `description` to fill in. Without this, // declaring an `[adapters.]` block to set ONE field @@ -198,17 +193,17 @@ mod tests { #[test] fn adapter_env_table_parses() { let toml = r#" - [adapters.zarvis] - binary = "agentd-adapter-zarvis" - env = { AGENTD_ZARVIS_MODEL = "codex-oauth:gpt-5.5", DEBUG = "1" } + [adapters.smith] + binary = "construct-adapter-smith" + env = { CONSTRUCT_SMITH_MODEL = "codex-oauth:gpt-5.5", DEBUG = "1" } "#; let cfg: Config = toml::from_str(toml).expect("parse"); - let zarvis = cfg.adapters.get("zarvis").expect("zarvis adapter"); + let smith = cfg.adapters.get("smith").expect("smith adapter"); assert_eq!( - zarvis.env.get("AGENTD_ZARVIS_MODEL").map(String::as_str), + smith.env.get("CONSTRUCT_SMITH_MODEL").map(String::as_str), Some("codex-oauth:gpt-5.5"), ); - assert_eq!(zarvis.env.get("DEBUG").map(String::as_str), Some("1")); + assert_eq!(smith.env.get("DEBUG").map(String::as_str), Some("1")); } /// Omitting `env` is fine — it defaults to empty rather than @@ -216,15 +211,15 @@ mod tests { #[test] fn adapter_env_defaults_to_empty() { let toml = r#" - [adapters.zarvis] - binary = "agentd-adapter-zarvis" + [adapters.smith] + binary = "construct-adapter-smith" "#; let cfg: Config = toml::from_str(toml).expect("parse"); - let zarvis = cfg.adapters.get("zarvis").expect("zarvis adapter"); - assert!(zarvis.env.is_empty()); + let smith = cfg.adapters.get("smith").expect("smith adapter"); + assert!(smith.env.is_empty()); } - /// REGRESSION: declaring `[adapters.zarvis] env = {…}` (to set + /// REGRESSION: declaring `[adapters.smith] env = {…}` (to set /// per-harness env defaults — the motivating use case for that /// field) must NOT drop the built-in `binary` / `description` /// values that the daemon needs to actually spawn the adapter. @@ -233,13 +228,13 @@ mod tests { /// `or_insert_with`, which only fired when the entry was /// missing entirely. A user-supplied entry that lacked `binary` /// got NO `binary` field, the daemon fell back to looking up a - /// binary named bare `zarvis` (not `agentd-adapter-zarvis`), + /// binary named bare `smith` (not `construct-adapter-smith`), /// and session create failed with "adapter binary not found". #[test] fn user_partial_adapter_config_keeps_builtin_binary() { let toml = r#" - [adapters.zarvis] - env = { AGENTD_ZARVIS_MODEL = "codex-oauth:gpt-5.5" } + [adapters.smith] + env = { CONSTRUCT_SMITH_MODEL = "codex-oauth:gpt-5.5" } "#; let mut cfg: Config = toml::from_str(toml).expect("parse"); // Mimic Config::load's builtin-layering step (the actual @@ -254,16 +249,15 @@ mod tests { entry.description = Some(b.description.to_string()); } } - let zarvis = cfg.adapters.get("zarvis").expect("zarvis adapter"); + let smith = cfg.adapters.get("smith").expect("smith adapter"); assert_eq!( - zarvis.binary.as_deref(), - Some("agentd-adapter-zarvis"), - "user-supplied [adapters.zarvis] with only `env` set must \ - still pick up the built-in binary path", + smith.binary.as_deref(), + Some("construct-adapter-smith"), + "user-supplied [adapters.smith] with only `env` set must still pick up the built-in binary path", ); // And the user's env stays in place. assert_eq!( - zarvis.env.get("AGENTD_ZARVIS_MODEL").map(String::as_str), + smith.env.get("CONSTRUCT_SMITH_MODEL").map(String::as_str), Some("codex-oauth:gpt-5.5"), ); } diff --git a/crates/daemon/src/loops.rs b/crates/daemon/src/loops.rs index be0c5c39..e26e552e 100644 --- a/crates/daemon/src/loops.rs +++ b/crates/daemon/src/loops.rs @@ -40,17 +40,17 @@ use std::time::Duration; use tokio::sync::RwLock; /// Minimum interval; below this is just spam. Configurable for -/// tests via `AGENTD_LOOP_MIN_SECS`. +/// tests via `CONSTRUCT_LOOP_MIN_SECS`. pub fn min_interval_secs() -> u64 { - std::env::var("AGENTD_LOOP_MIN_SECS") + std::env::var("CONSTRUCT_LOOP_MIN_SECS") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(30) } -/// Maximum interval. Configurable via `AGENTD_LOOP_MAX_SECS`. +/// Maximum interval. Configurable via `CONSTRUCT_LOOP_MAX_SECS`. pub fn max_interval_secs() -> u64 { - std::env::var("AGENTD_LOOP_MAX_SECS") + std::env::var("CONSTRUCT_LOOP_MAX_SECS") .ok() .and_then(|s| s.parse().ok()) .unwrap_or(24 * 3600) @@ -429,10 +429,10 @@ mod tests { #[test] fn clamp_respects_min() { - std::env::set_var("AGENTD_LOOP_MIN_SECS", "30"); + std::env::set_var("CONSTRUCT_LOOP_MIN_SECS", "30"); let (v, clamped) = clamp_interval(5); assert_eq!(v, 30); assert!(clamped); - std::env::remove_var("AGENTD_LOOP_MIN_SECS"); + std::env::remove_var("CONSTRUCT_LOOP_MIN_SECS"); } } diff --git a/crates/daemon/src/main.rs b/crates/daemon/src/main.rs index 14987f03..01a9041a 100644 --- a/crates/daemon/src/main.rs +++ b/crates/daemon/src/main.rs @@ -17,7 +17,7 @@ mod worktree; use agentd_protocol::paths::Paths; #[derive(Debug, Parser)] -#[command(name = "agentd", about = "agentd daemon", version)] +#[command(name = "constructd", about = "construct daemon", version)] struct Cli { #[command(subcommand)] command: Option, @@ -70,7 +70,7 @@ async fn main() -> Result<()> { async fn run(socket_override: Option) -> Result<()> { // Capture the executable path now, before anything can replace - // the binary on disk. `/agentd restart` re-`exec()`s this path; + // the binary on disk. `/construct restart` re-`exec()`s this path; // resolving it lazily at restart time is unreliable once an // upgrade has rename-replaced the file (Linux's // `current_exe()` then reads as "… (deleted)"). See @@ -78,6 +78,7 @@ async fn run(socket_override: Option) -> Result<()> { session::capture_startup_exe(); let paths = Paths::discover(); + warn_legacy_paths(&paths); std::fs::create_dir_all(&paths.state_dir).ok(); std::fs::create_dir_all(&paths.data_dir).ok(); std::fs::create_dir_all(&paths.runtime_dir).ok(); @@ -154,13 +155,13 @@ async fn run(socket_override: Option) -> Result<()> { } // Auto-start the remote WS listener at boot when - // `AGENTD_REMOTE_WS_PORT` is set — the headless / scripted + // `CONSTRUCT_REMOTE_WS_PORT` is set — the headless / scripted // entry point. Interactive users get the same machinery via // the TUI's `/remote-control` slash (which calls // `remote.start` over IPC and shows a QR), so the env var is // only needed when nobody is at the terminal to type the // command. - if let Ok(port_raw) = std::env::var("AGENTD_REMOTE_WS_PORT") { + if let Ok(port_raw) = std::env::var("CONSTRUCT_REMOTE_WS_PORT") { match port_raw.parse::() { Ok(port) => { let mgr = manager.clone(); @@ -187,11 +188,11 @@ async fn run(socket_override: Option) -> Result<()> { } Err(_) => tracing::warn!( value = %port_raw, - "AGENTD_REMOTE_WS_PORT is not a valid u16; skipping ws listener" + "CONSTRUCT_REMOTE_WS_PORT is not a valid u16; skipping ws listener" ), } } else if paths.runtime_dir.join("remote.json").exists() { - // `/agentd restart` path: the prior daemon had the remote + // `/construct restart` path: the prior daemon had the remote // listener up and persisted a snapshot. Resume ONLY if that // snapshot is still adoptable (fresh, and its cloudflared // tunnel PID — if any — still alive), picking the port + @@ -274,6 +275,179 @@ async fn run(socket_override: Option) -> Result<()> { } } +fn warn_legacy_paths(current: &Paths) { + if let Some(msg) = legacy_migration_notice(current) { + eprint!("{}", msg); + } +} + +fn shell_quote(path: &std::path::Path) -> String { + format!("'{}'", path.display().to_string().replace('\'', "'\\''")) +} + +fn legacy_migration_notice(current: &Paths) -> Option { + legacy_migration_notice_with_paths(current, &Paths::discover_legacy()) +} + +fn legacy_migration_notice_with_paths(current: &Paths, legacy: &Paths) -> Option { + let mut found = Vec::<(&str, &std::path::Path)>::new(); + let legacy_items = [ + ("config directory", legacy.config_dir.as_path()), + ("state directory", legacy.state_dir.as_path()), + ("data directory", legacy.data_dir.as_path()), + ("runtime directory", legacy.runtime_dir.as_path()), + ]; + + for (label, p) in legacy_items { + if p.exists() { + found.push((label, p)); + } + } + + let legacy_config = legacy.config_file(); + if legacy_config.exists() { + found.push(("legacy config file", legacy_config.as_path())); + } + if found.is_empty() { + return None; + } + + let mut out = String::new(); + out.push_str("\n[construct] Detected existing legacy `agentd` layout;\nthis daemon no longer reads those locations.\n"); + for (label, path) in found { + out.push_str(&format!(" - legacy {label}: {}\n", path.display())); + } + + out.push_str("\nSuggested migration:\n"); + out.push_str(&format!( + " config: {old} -> {new}\n", + old = legacy.config_dir.display(), + new = current.config_dir.display() + )); + out.push_str(&format!( + " state: {old} -> {new}\n", + old = legacy.state_dir.display(), + new = current.state_dir.display() + )); + out.push_str(&format!( + " data: {old} -> {new}\n", + old = legacy.data_dir.display(), + new = current.data_dir.display() + )); + out.push_str(&format!( + " runtime: {old} -> {new}\n", + old = legacy.runtime_dir.display(), + new = current.runtime_dir.display() + )); + + out.push_str("\nCopy/paste migration command:\n"); + out.push_str(&format!( + " mkdir -p {} {} {} {}\n", + shell_quote(¤t.config_dir), + shell_quote(¤t.state_dir), + shell_quote(¤t.data_dir), + shell_quote(¤t.runtime_dir) + )); + + let migration_dirs = [ + (legacy.config_dir.as_path(), current.config_dir.as_path()), + (legacy.state_dir.as_path(), current.state_dir.as_path()), + (legacy.data_dir.as_path(), current.data_dir.as_path()), + (legacy.runtime_dir.as_path(), current.runtime_dir.as_path()), + ]; + for (old_dir, new_dir) in migration_dirs { + if old_dir.exists() { + out.push_str(&format!( + " cp -a {old}/. {new}/\n", + old = shell_quote(old_dir), + new = shell_quote(new_dir) + )); + } + } + if legacy_config.exists() { + out.push_str(&format!( + " cp -a {old_config} {new_config}\n", + old_config = shell_quote(&legacy_config), + new_config = shell_quote(¤t.config_file()) + )); + } + + out.push_str("\nMove or copy what you need; restart after migration.\n"); + + Some(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::tempdir; + + fn path(base: &std::path::Path, tail: &str) -> std::path::PathBuf { + base.join(tail) + } + + #[test] + fn legacy_migration_notice_omits_output_when_no_legacy_layout() { + let root = tempdir().unwrap(); + let current = Paths { + config_dir: path(root.path(), "construct-config"), + state_dir: path(root.path(), "construct-state"), + data_dir: path(root.path(), "construct-data"), + runtime_dir: path(root.path(), "construct-runtime"), + }; + let legacy = Paths { + config_dir: path(root.path(), "agentd-config"), + state_dir: path(root.path(), "agentd-state"), + data_dir: path(root.path(), "agentd-data"), + runtime_dir: path(root.path(), "agentd-runtime"), + }; + assert!(super::legacy_migration_notice_with_paths(¤t, &legacy).is_none()); + } + + #[test] + fn legacy_migration_notice_includes_detected_items() { + let root = tempdir().unwrap(); + let legacy = Paths { + config_dir: path(root.path(), "agentd-config"), + state_dir: path(root.path(), "agentd-state"), + data_dir: path(root.path(), "agentd-data"), + runtime_dir: path(root.path(), "agentd-runtime"), + }; + let current = Paths { + config_dir: path(root.path(), "construct-config"), + state_dir: path(root.path(), "construct-state"), + data_dir: path(root.path(), "construct-data"), + runtime_dir: path(root.path(), "construct-runtime"), + }; + std::fs::create_dir_all(&legacy.config_dir).unwrap(); + std::fs::create_dir_all(&legacy.state_dir).unwrap(); + std::fs::write(legacy.config_file(), "configured = true").unwrap(); + + let msg = super::legacy_migration_notice_with_paths(¤t, &legacy).expect("notice"); + assert!(msg.contains("Detected existing legacy `agentd` layout")); + assert!(msg.contains(&format!( + "legacy config file: {}", + legacy.config_file().display() + ))); + assert!(msg.contains("state directory")); + assert!(msg.contains("legacy config file")); + assert!(msg.contains(&format!("config: {}", legacy.config_dir.display()))); + assert!(msg.contains(&format!( + " config: {} -> {}", + legacy.config_dir.display(), + current.config_dir.display() + ))); + assert!(msg.contains(&format!( + " state: {} -> {}", + legacy.state_dir.display(), + current.state_dir.display() + ))); + assert!(msg.contains("Copy/paste migration command:")); + assert!(msg.contains("mkdir -p")); + assert!(msg.contains("cp -a")); + } +} + enum MainOutcome { Server(Result<()>), Signal(DaemonSignal), diff --git a/crates/daemon/src/remote_supervisor.rs b/crates/daemon/src/remote_supervisor.rs index 8d3b0bdc..9687e00a 100644 --- a/crates/daemon/src/remote_supervisor.rs +++ b/crates/daemon/src/remote_supervisor.rs @@ -206,7 +206,7 @@ async fn handle_start( // clears `tunnel_task`, so a subsequent start re-spawns // cloudflared with the (new) token. if spawn_tunnel && tunnel_task.is_none() { - if std::env::var("AGENTD_REMOTE_NO_TUNNEL").is_err() { + if std::env::var("CONSTRUCT_REMOTE_NO_TUNNEL").is_err() { // `adopt_pid` is non-zero only after a `/agentd // restart`: the snapshot captured a still-running // cloudflared PID and `bind_and_install` rehydrated @@ -219,7 +219,7 @@ async fn handle_start( }); *tunnel_task = Some(handle); } else { - tracing::info!("AGENTD_REMOTE_NO_TUNNEL is set; skipping cloudflared spawn"); + tracing::info!("CONSTRUCT_REMOTE_NO_TUNNEL is set; skipping cloudflared spawn"); } } diff --git a/crates/daemon/src/session.rs b/crates/daemon/src/session.rs index c7c37687..e3798a09 100644 --- a/crates/daemon/src/session.rs +++ b/crates/daemon/src/session.rs @@ -62,9 +62,9 @@ fn resume_redraw_ready(last_pty_at_ms: Option, now_ms: i64, elapsed: Durati } } const MAX_CLIPBOARD_ATTACHMENT_BYTES: usize = 50 * 1024 * 1024; -const ENV_GLOBAL_MEMORY_FILE: &str = "AGENTD_GLOBAL_MEMORY_FILE"; -const ENV_PROJECT_MEMORY_FILE: &str = "AGENTD_PROJECT_MEMORY_FILE"; -const ENV_PROJECT_ID: &str = "AGENTD_PROJECT_ID"; +const ENV_GLOBAL_MEMORY_FILE: &str = "CONSTRUCT_GLOBAL_MEMORY_FILE"; +const ENV_PROJECT_MEMORY_FILE: &str = "CONSTRUCT_PROJECT_MEMORY_FILE"; +const ENV_PROJECT_ID: &str = "CONSTRUCT_PROJECT_ID"; const WIDGET_WATCH_INTERVAL: Duration = Duration::from_millis(700); fn should_resume_on_startup(state: SessionState) -> bool { @@ -424,14 +424,6 @@ fn should_record_pty_user_message(harness: &str) -> bool { matches!(harness, "claude" | "antigravity") } -fn smith_adapter_name(name: &str) -> &str { - if name == "zarvis" { - "smith" - } else { - name - } -} - impl SessionEntry { pub fn is_deleted(&self) -> bool { self.deleted.load(Ordering::SeqCst) @@ -557,7 +549,7 @@ pub struct SessionManager { /// `index.html` + `static/*` from this directory (with a live-reload /// poller injected) instead of the binary's embedded assets. Set via /// the `dev.set_assets` IPC method (debug builds only) or the - /// `AGENTD_ASSETS_DIR` env var at boot. Lets you iterate on the web + /// `CONSTRUCT_ASSETS_DIR` env var at boot. Lets you iterate on the web /// UI in a worktree against a running daemon without rebuilding. dev_assets: std::sync::Mutex>, widget_snapshots: tokio::sync::Mutex>, @@ -752,10 +744,10 @@ impl SessionManager { let (remote_tx, remote_rx) = tokio::sync::mpsc::unbounded_channel(); let (restart_tx, restart_rx) = tokio::sync::mpsc::unbounded_channel(); let remote_snapshot_path = runtime_dir.join("remote.json"); - // Honor AGENTD_ASSETS_DIR at boot in debug builds only — release + // Honor CONSTRUCT_ASSETS_DIR at boot in debug builds only — release // always serves the embedded, tamper-proof assets. let dev_assets = if cfg!(debug_assertions) { - std::env::var_os("AGENTD_ASSETS_DIR").map(PathBuf::from) + std::env::var_os("CONSTRUCT_ASSETS_DIR").map(PathBuf::from) } else { None }; @@ -985,7 +977,7 @@ impl SessionManager { /// `port_hint` is honored when set (env-var-at-boot path); /// otherwise an ephemeral localhost port is bound. The cloudflared /// supervisor is also launched on first call (skipped when - /// `AGENTD_REMOTE_NO_TUNNEL` is set, same as the boot path). + /// `CONSTRUCT_REMOTE_NO_TUNNEL` is set, same as the boot path). /// /// `wait_for_tunnel` caps how long we wait for cloudflared to /// publish its `*.trycloudflare.com` URL before returning the @@ -1132,9 +1124,9 @@ impl SessionManager { // we can muster. The CLI surfaces this verbatim in the // popup so the user knows why the tunnel didn't come up. let cloudflared_available = which::which("cloudflared").is_ok(); - let no_tunnel_env = std::env::var("AGENTD_REMOTE_NO_TUNNEL").is_ok(); + let no_tunnel_env = std::env::var("CONSTRUCT_REMOTE_NO_TUNNEL").is_ok(); let msg = if no_tunnel_env { - "AGENTD_REMOTE_NO_TUNNEL is set; unset it and rerun \ + "CONSTRUCT_REMOTE_NO_TUNNEL is set; unset it and rerun \ `/remote-control`. Use `/remote-control debug` for the \ local-only URL." } else if !cloudflared_available { @@ -1251,7 +1243,7 @@ impl SessionManager { } pub async fn create(self: &Arc, params: CreateSessionParams) -> Result { - let harness = smith_adapter_name(¶ms.harness); + let harness = params.harness.as_str(); let adapter_cfg = self .config .adapters @@ -1311,7 +1303,7 @@ impl SessionManager { parent_session_id: params .parent_session_id .clone() - .or_else(|| params.env.get("AGENTD_PARENT_SESSION_ID").cloned()), + .or_else(|| params.env.get("CONSTRUCT_PARENT_SESSION_ID").cloned()), last_pty_at_ms: None, approval_mode: agentd_protocol::ApprovalMode::Manual, kind: params.kind, @@ -1327,7 +1319,7 @@ impl SessionManager { // Build the full env (adapter-config + user-provided + daemon // meta) BEFORE spawn so the adapter process inherits - // AGENTD_SESSION_DATA_DIR / AGENTD_SESSION_KIND — not just + // CONSTRUCT_SESSION_DATA_DIR / CONSTRUCT_SESSION_KIND — not just // the session.start params.env. The codex adapter (and // claude) reads these via std::env::var, so leaving them // only in session.start meant their first-spawn bookkeeping @@ -1337,7 +1329,7 @@ impl SessionManager { // // Precedence: `[adapters.].env` is the per-harness // baseline (operator-set default model, etc.), overridden - // by the per-session `params.env` (explicit `agent new + // by the per-session `params.env` (explicit `construct new // --env KEY=VAL`), overridden in turn by daemon-meta. So a // CLI flag always wins over config.toml, and daemon meta // always wins over both. @@ -1351,11 +1343,11 @@ impl SessionManager { self.storage.widgets_dir(&id) }); env_with_meta.insert( - "AGENTD_SESSION_DATA_DIR".to_string(), + "CONSTRUCT_SESSION_DATA_DIR".to_string(), session_dir.to_string_lossy().to_string(), ); env_with_meta.insert( - "AGENTD_SESSION_WIDGETS_DIR".to_string(), + "CONSTRUCT_SESSION_WIDGETS_DIR".to_string(), widgets_dir.to_string_lossy().to_string(), ); // Single auto-approval policy the daemon defines once; each adapter @@ -1366,7 +1358,7 @@ impl SessionManager { widgets_dir.to_string_lossy().to_string(), ); env_with_meta.insert( - "AGENTD_SESSION_KIND".to_string(), + "CONSTRUCT_SESSION_KIND".to_string(), match params.kind { agentd_protocol::SessionKind::User => "user", agentd_protocol::SessionKind::Orchestrator => "orchestrator", @@ -1553,7 +1545,7 @@ impl SessionManager { /// sessions are retried because an error can mean the previous adapter or /// machine died rather than the underlying agent conversation ending. /// Each adapter - /// receives `AGENTD_RESUME=1` in its env plus the same start params it + /// receives `CONSTRUCT_RESUME=1` in its env plus the same start params it /// was originally launched with (cwd, model, prompt, etc.) — the /// adapter decides what "resume" means for its harness. Sessions that /// can't be re-spawned (missing start.json, missing adapter binary, @@ -1592,8 +1584,8 @@ impl SessionManager { /// Spawn an adapter for an already-existing session entry (i.e. on /// daemon restart). Reuses the start params persisted at create time - /// and signals `AGENTD_RESUME=1` so the adapter can pull its own - /// prior state from `AGENTD_SESSION_DATA_DIR`. + /// and signals `CONSTRUCT_RESUME=1` so the adapter can pull its own + /// prior state from `CONSTRUCT_SESSION_DATA_DIR`. async fn respawn(self: Arc, id: &str) -> Result<()> { let entry = self .get_entry(id) @@ -1602,11 +1594,11 @@ impl SessionManager { let mut start_params = self.storage.load_start_params(id)?; start_params .env - .insert("AGENTD_RESUME".to_string(), "1".to_string()); + .insert("CONSTRUCT_RESUME".to_string(), "1".to_string()); // Make sure the data-dir env is present even if start.json predates // the meta env injection. start_params.env.insert( - "AGENTD_SESSION_DATA_DIR".to_string(), + "CONSTRUCT_SESSION_DATA_DIR".to_string(), self.storage.session_dir(id).to_string_lossy().to_string(), ); let project_id = { @@ -1619,7 +1611,7 @@ impl SessionManager { self.storage.widgets_dir(id) }); start_params.env.insert( - "AGENTD_SESSION_WIDGETS_DIR".to_string(), + "CONSTRUCT_SESSION_WIDGETS_DIR".to_string(), widgets_dir.to_string_lossy().to_string(), ); start_params.env.insert( @@ -1707,7 +1699,7 @@ impl SessionManager { // Merge `[adapters.].env` underneath the persisted // start-params env so config.toml-driven defaults apply on - // respawn too. Per-session env (from `agent new --env`) + // respawn too. Per-session env (from `construct new --env`) // still wins because start_params.env was constructed with // it on top of adapter_cfg.env at create time and gets the // same treatment again here. @@ -1839,7 +1831,7 @@ impl SessionManager { /// — those are running, not done. /// /// Internally just calls [`Manager::respawn`], which sets - /// `AGENTD_RESUME=1` in the adapter env so harnesses that + /// `CONSTRUCT_RESUME=1` in the adapter env so harnesses that /// persist conversation state (zarvis) reload it on the new /// process. pub async fn restart(self: Arc, id: &str) -> Result<()> { @@ -2351,7 +2343,7 @@ impl SessionManager { /// has not set a title yet (i.e. the `title` field is `None` — the /// hash shown in the UI is just `primary_label`'s display fallback), /// (b) we haven't already attempted this incarnation, (c) the - /// prompt is non-empty, and (d) the zarvis adapter binary is + /// prompt is non-empty, and (d) the smith adapter binary is /// configured + locatable. Silently no-ops on any miss. fn maybe_spawn_auto_title(&self, entry: Arc, prompt: String) { // Cheap checks first so we don't burn the per-session attempt @@ -2361,19 +2353,13 @@ impl SessionManager { if prompt.trim().is_empty() { return; } - let Some(zi) = self - .config - .adapters - .get("smith") - .or_else(|| self.config.adapters.get("zarvis")) - .cloned() - else { + let Some(smith_adapter) = self.config.adapters.get("smith").cloned() else { return; }; - let binary_spec = zi + let binary_spec = smith_adapter .binary .clone() - .unwrap_or_else(|| "agentd-adapter-zarvis".to_string()); + .unwrap_or_else(|| "construct-adapter-smith".to_string()); let Some(binary) = locate_binary(&binary_spec) else { return; }; @@ -2723,7 +2709,7 @@ impl SessionManager { } // Best-effort: remove the per-session MCP config the adapter may - // have written for an injected `agentd-mcp` server. + // have written for an injected `construct-mcp` server. let mcp_path = agentd_protocol::paths::Paths::discover() .state_dir .join("mcp") @@ -3368,7 +3354,7 @@ impl SessionManager { } } -/// Shell out to `agentd-adapter-zarvis --title-mode ""`, capture +/// Shell out to `construct-adapter-smith --title-mode ""`, capture /// stdout, and apply the title to the session summary. Best-effort: /// any failure (zarvis missing keys, network error, non-zero exit, /// empty output) leaves the session's title unset. @@ -3444,7 +3430,7 @@ fn effective_mode(params: &CreateSessionParams) -> String { fn builtin_harness_capabilities(name: &str) -> agentd_protocol::Capabilities { match name { - "shell" | "claude" | "codex" | "smith" | "zarvis" => agentd_protocol::Capabilities { + "shell" | "claude" | "codex" | "smith" => agentd_protocol::Capabilities { supports_pty: true, ..Default::default() }, diff --git a/crates/e2e/Cargo.toml b/crates/e2e/Cargo.toml index 632cf46d..ec59d7a7 100644 --- a/crates/e2e/Cargo.toml +++ b/crates/e2e/Cargo.toml @@ -6,7 +6,7 @@ license.workspace = true authors.workspace = true repository.workspace = true rust-version.workspace = true -description = "End-to-end test harness for agentd — spawns the real daemon binary against a temp $AGENTD_*_DIR and drives it via the IPC client (and optionally the TUI in a pseudo-terminal)." +description = "End-to-end test harness for agentd — spawns the real daemon binary against a temp $CONSTRUCT_*_DIR and drives it via the IPC client (and optionally the TUI in a pseudo-terminal)." publish = false [lib] diff --git a/crates/e2e/src/lib.rs b/crates/e2e/src/lib.rs index 3f2b4d26..68adbbd3 100644 --- a/crates/e2e/src/lib.rs +++ b/crates/e2e/src/lib.rs @@ -1,13 +1,13 @@ -//! End-to-end test harness for `agentd`. +//! End-to-end test harness for `constructd`. //! -//! Spawns the **real** `agentd` binary out of the workspace's +//! Spawns the **real** `constructd` binary out of the workspace's //! `target/debug/` against a fresh tempdir for every test (so -//! the test never touches the developer's actual `$AGENTD_*_DIR` +//! the test never touches the developer's actual `$CONSTRUCT_*_DIR` //! state), waits for the IPC socket to come up, and returns a //! connected `agentd_client::Client` plus the path to the //! socket. Drop kills the daemon and cleans the tempdir. //! -//! There's a sibling [`Tui`] helper that spawns the `agent` TUI +//! There's a sibling [`Tui`] helper that spawns the `construct` TUI //! inside a pseudo-terminal so tests can scrape rendered output //! (via `vt100`) and send keystrokes back. Together they let a //! single test exercise the full stack: TUI → IPC → daemon → @@ -32,7 +32,7 @@ use tokio::process::{Child, Command}; use agentd_client::Client; -/// One isolated `agentd` instance + a connected IPC client. +/// One isolated `constructd` instance + a connected IPC client. /// `Drop` kills the daemon (via tokio's `kill_on_drop`) and /// cleans up the tempdir (via `TempDir`). pub struct Daemon { @@ -45,7 +45,7 @@ pub struct Daemon { /// passing to the TUI helper. pub socket: PathBuf, /// Path the daemon binary was launched from. For `spawn()` - /// this is the workspace `target/debug/agentd`. For + /// this is the workspace `target/debug/constructd`. For /// `spawn_relocatable()` it's a private copy under the /// tempdir that the test can swap to exercise the /// "exec picks up an upgraded binary" path. @@ -57,9 +57,9 @@ pub struct Daemon { } impl Daemon { - /// Spawn a fresh `agentd` against a tempdir and wait for its + /// Spawn a fresh `constructd` against a tempdir and wait for its /// IPC socket to come up. Always sets - /// `AGENTD_REMOTE_NO_TUNNEL=1` because the e2e tests should + /// `CONSTRUCT_REMOTE_NO_TUNNEL=1` because the e2e tests should /// never spawn a real cloudflared subprocess (would publish a /// real tunnel URL and the CI runner can't reach a `*.try /// cloudflare.com` host anyway). @@ -71,7 +71,7 @@ impl Daemon { Self::spawn_inner(false).await } - /// Like `spawn`, but copies the `agentd` binary into the + /// Like `spawn`, but copies the `constructd` binary into the /// tempdir and launches that copy instead of the workspace /// binary. The copy lives at `Daemon::binary_path`, so a /// test can atomically swap it (write-then-rename) and then @@ -119,14 +119,14 @@ impl Daemon { // to the orchestrator's editor instead of the global // keymap, and chords like `Ctrl-x x` (palette) silently // type into the editor. Local dev environments where - // the zarvis adapter isn't on PATH skip this naturally, + // the smith adapter isn't on PATH skip this naturally, // which is why the test passed locally but failed on CI. std::fs::write( config_dir.join("config.toml"), "[orchestrator]\nenabled = false\n", ) .context("write e2e config.toml")?; - let socket = runtime_dir.join("agentd.sock"); + let socket = runtime_dir.join("construct.sock"); // For the relocatable case, copy the workspace binary into // the tempdir's `bin/`. Adapter binaries are resolved by @@ -137,19 +137,19 @@ impl Daemon { let binary_path = if relocatable { let bin_dir = dir.path().join("bin"); std::fs::create_dir_all(&bin_dir)?; - let src = agentd_bin_path()?; - let dst = bin_dir.join("agentd"); + let src = constructd_bin_path()?; + let dst = bin_dir.join("constructd"); std::fs::copy(&src, &dst) .with_context(|| format!("copy {} -> {}", src.display(), dst.display()))?; copy_executable_perms(&dst)?; // Best-effort copy of sibling adapter binaries. if let Some(src_dir) = src.parent() { for name in [ - "agentd-adapter-shell", - "agentd-adapter-claude", - "agentd-adapter-codex", - "agentd-adapter-zarvis", - "agentd-mcp", + "construct-adapter-shell", + "construct-adapter-claude", + "construct-adapter-codex", + "construct-adapter-smith", + "construct-mcp", ] { let from = src_dir.join(name); if from.exists() { @@ -162,17 +162,17 @@ impl Daemon { } dst } else { - agentd_bin_path()? + constructd_bin_path()? }; let mut cmd = Command::new(&binary_path); - cmd.env("AGENTD_RUNTIME_DIR", &runtime_dir) - .env("AGENTD_STATE_DIR", &state_dir) - .env("AGENTD_DATA_DIR", &data_dir) - .env("AGENTD_CONFIG_DIR", &config_dir) + cmd.env("CONSTRUCT_RUNTIME_DIR", &runtime_dir) + .env("CONSTRUCT_STATE_DIR", &state_dir) + .env("CONSTRUCT_DATA_DIR", &data_dir) + .env("CONSTRUCT_CONFIG_DIR", &config_dir) // Skip cloudflared in every e2e test — its absence // from CI runners is not a test failure. - .env("AGENTD_REMOTE_NO_TUNNEL", "1") + .env("CONSTRUCT_REMOTE_NO_TUNNEL", "1") .args(["run", "--socket"]) .arg(&socket) // Silence the daemon's stderr / stdout in tests by @@ -239,7 +239,7 @@ impl Daemon { } } -/// A live `agent` TUI bound to a particular daemon socket, with +/// A live `construct` TUI bound to a particular daemon socket, with /// the underlying PTY parsed by `vt100` so tests can scrape the /// rendered screen contents. /// @@ -268,7 +268,7 @@ impl Drop for Tui { // Best-effort kill. `portable_pty::Child::kill` is // synchronous + idempotent — if the child has already // exited this is a no-op. Without this, a panicking - // `wait_for` from a test leaves the agent process alive + // `wait_for` from a test leaves the construct process alive // and the tokio runtime hangs waiting on the PTY reader. if let Some(child) = self.child.as_mut() { let _ = child.kill(); @@ -277,7 +277,7 @@ impl Drop for Tui { } impl Tui { - /// Spawn `agent tui --socket ` in a 30x100 PTY. The + /// Spawn `construct tui --socket ` in a 30x100 PTY. The /// dimensions are arbitrary but match what the TUI tests /// expect for layout assertions. /// @@ -299,7 +299,7 @@ impl Tui { } fn spawn_inner(socket: &Path, cast_path: Option) -> Result { - let agent = agent_bin_path()?; + let client = construct_bin_path()?; let pty_system = portable_pty::native_pty_system(); let size = portable_pty::PtySize { rows: 30, @@ -311,7 +311,7 @@ impl Tui { .openpty(size) .map_err(|e| anyhow!("openpty: {e}"))?; - let mut cmd = portable_pty::CommandBuilder::new(&agent); + let mut cmd = portable_pty::CommandBuilder::new(&client); cmd.args(["tui", "--socket"]); cmd.arg(socket); // The TUI looks at TERM for color handling. xterm-256color @@ -326,7 +326,7 @@ impl Tui { let child = pair .slave .spawn_command(cmd) - .map_err(|e| anyhow!("spawn agent tui: {e}"))?; + .map_err(|e| anyhow!("spawn construct tui: {e}"))?; // Slave handle is no longer needed after spawn; dropping // it ensures EOF propagates to the child if the parent // closes. @@ -516,16 +516,16 @@ fn copy_executable_perms(path: &Path) -> Result<()> { Ok(()) } -/// Locate the `agentd` binary in the workspace `target/` +/// Locate the `constructd` binary in the workspace `target/` /// directory. Honors `CARGO_TARGET_DIR` first, then falls back /// to walking up two levels from `CARGO_MANIFEST_DIR` /// (`crates/e2e` → workspace root). -fn agentd_bin_path() -> Result { - bin_path("agentd") +fn constructd_bin_path() -> Result { + bin_path("constructd") } -fn agent_bin_path() -> Result { - bin_path("agent") +fn construct_bin_path() -> Result { + bin_path("construct") } fn bin_path(name: &str) -> Result { diff --git a/crates/e2e/tests/key_latency.rs b/crates/e2e/tests/key_latency.rs index c749462b..966eb252 100644 --- a/crates/e2e/tests/key_latency.rs +++ b/crates/e2e/tests/key_latency.rs @@ -9,7 +9,7 @@ //! //! cargo test -p agentd-e2e --test key_latency -- --ignored --nocapture //! -//! It drives the real `agent` TUI in a PTY against a real daemon +//! It drives the real `construct` TUI in a PTY against a real daemon //! running an interactive shell session (`$SHELL -il` via the //! shell adapter), so it exercises the entire pipeline: //! diff --git a/crates/e2e/tests/restart.rs b/crates/e2e/tests/restart.rs index 0dfadd6e..5819fd83 100644 --- a/crates/e2e/tests/restart.rs +++ b/crates/e2e/tests/restart.rs @@ -4,7 +4,7 @@ //! 1. **Binary reload** — `daemon.restart` `exec()`s the //! current on-disk binary, so a replaced/upgraded binary is //! picked up. PID is preserved (exec, not respawn). -//! 2. **TUI auto-reconnect** — the `agent` TUI notices the +//! 2. **TUI auto-reconnect** — the `construct` TUI notices the //! socket drop and reconnects on its own, no manual re-run. //! 3. **Web reconnect to the same URL** — the bundled web //! client's WebSocket drops and reconnects to the *same* @@ -100,7 +100,7 @@ async fn restart_reloads_updated_binary() { // --------------------------------------------------------------------------- /// After a daemon restart, the TUI should reconnect on its own — -/// the user shouldn't have to quit and re-launch `agent`. +/// the user shouldn't have to quit and re-launch `construct`. #[tokio::test(flavor = "multi_thread", worker_threads = 4)] async fn tui_auto_reconnects_after_restart() { let d = Daemon::spawn().await.expect("spawn daemon"); diff --git a/crates/e2e/tests/tui_smoke.rs b/crates/e2e/tests/tui_smoke.rs index 6d366626..74f3551d 100644 --- a/crates/e2e/tests/tui_smoke.rs +++ b/crates/e2e/tests/tui_smoke.rs @@ -1,4 +1,4 @@ -//! End-to-end: drive the `agent` TUI inside a pseudo-terminal +//! End-to-end: drive the `construct` TUI inside a pseudo-terminal //! against a real `agentd`, type a slash command, observe the //! resulting popup, and quit cleanly. //! diff --git a/crates/e2e/tests/web_smoke.rs b/crates/e2e/tests/web_smoke.rs index d96ef4c5..30f0f43a 100644 --- a/crates/e2e/tests/web_smoke.rs +++ b/crates/e2e/tests/web_smoke.rs @@ -146,7 +146,7 @@ async fn web_client_loads_and_websocket_connects() { try { state.sessions = [{ id: 's-parent', - cwd: '/tmp/agentd-project', + cwd: '/tmp/construct-project', group_id: 'project-123', kind: 'user', }]; @@ -158,7 +158,7 @@ async fn web_client_loads_and_websocket_connects() { }]; newSessionHarnessEl.innerHTML = ''; newSessionHarnessEl.value = 'shell'; - newSessionCwdEl.value = '/tmp/agentd-project'; + newSessionCwdEl.value = '/tmp/construct-project'; newSessionPromptEl.value = ''; state.ws = { readyState: 1, diff --git a/crates/mcp/Cargo.toml b/crates/mcp/Cargo.toml index 072222fc..b7d7531e 100644 --- a/crates/mcp/Cargo.toml +++ b/crates/mcp/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "agentd-mcp" +name = "construct-mcp" version.workspace = true edition.workspace = true license.workspace = true @@ -9,7 +9,7 @@ rust-version.workspace = true description = "MCP stdio server for agentd: lets an agent inside an agentd session control the daemon" [[bin]] -name = "agentd-mcp" +name = "construct-mcp" path = "src/main.rs" [dependencies] diff --git a/crates/mcp/src/main.rs b/crates/mcp/src/main.rs index a8730052..f57350a8 100644 --- a/crates/mcp/src/main.rs +++ b/crates/mcp/src/main.rs @@ -1,4 +1,4 @@ -//! `agentd-mcp` — MCP stdio server that lets an agent (running inside an +//! `construct-mcp` — MCP stdio server that lets an agent (running inside an //! agentd session) control the agentd daemon: list sessions, read their //! output, send input, spawn new sessions, etc. //! @@ -7,8 +7,8 @@ //! `agentd_protocol::jsonrpc` for envelope types. //! //! Environment: -//! - `AGENTD_SOCKET` — override the daemon's Unix socket path -//! - `AGENTD_SESSION_ID` — the calling agent's session id (returned by the +//! - `CONSTRUCT_SOCKET` — override the daemon's Unix socket path +//! - `CONSTRUCT_SESSION_ID` — the calling agent's session id (returned by the //! `agentd_whoami` tool). Set by the agentd adapter when it spawns the //! child CLI. @@ -27,16 +27,19 @@ const MCP_PROTOCOL_VERSION: &str = "2024-11-05"; #[tokio::main] async fn main() -> Result<()> { - let socket = std::env::var("AGENTD_SOCKET") + let socket = std::env::var("CONSTRUCT_SOCKET") .ok() .map(PathBuf::from) .unwrap_or_else(|| Paths::discover().socket()); - let session_id = std::env::var("AGENTD_SESSION_ID").ok(); + let session_id = std::env::var("CONSTRUCT_SESSION_ID").ok(); let client = match Client::connect(&socket).await { Ok(c) => c, Err(e) => { - eprintln!("agentd-mcp: failed to connect to {}: {e}", socket.display()); + eprintln!( + "construct-mcp: failed to connect to {}: {e}", + socket.display() + ); std::process::exit(1); } }; @@ -53,7 +56,7 @@ async fn run(client: Arc, session_id: Option) -> Result<()> { Ok(Some(v)) => v, Ok(None) => return Ok(()), // EOF Err(e) => { - eprintln!("agentd-mcp: invalid JSON on stdin: {e}"); + eprintln!("construct-mcp: invalid JSON on stdin: {e}"); continue; } }; @@ -90,7 +93,7 @@ async fn handle_request(client: &Arc, session_id: Option<&str>, req: Req serde_json::json!({ "protocolVersion": MCP_PROTOCOL_VERSION, "serverInfo": { - "name": "agentd-mcp", + "name": "construct-mcp", "version": env!("CARGO_PKG_VERSION"), }, "capabilities": { diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index d7aef12d..02cde6ff 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -22,7 +22,7 @@ pub fn catalog() -> Vec { ), tool( "agentd_whoami", - "Returns the AGENTD_SESSION_ID env var visible to this MCP server, which is the agentd session id that the calling agent is running inside. Returns null if unset (the MCP server is running outside an agentd-managed session).", + "Returns the CONSTRUCT_SESSION_ID env var visible to this MCP server, which is the agentd session id that the calling agent is running inside. Returns null if unset (the MCP server is running outside an agentd-managed session).", schema_empty(), ), tool( @@ -484,7 +484,7 @@ pub async fn call(client: &Arc, session_id: Option<&str>, params: Value) .unwrap_or_else(|_| ".".to_string()) }); let mut env = HashMap::new(); - env.insert("AGENTD_PARENT_SESSION_ID".to_string(), parent_id.clone()); + env.insert("CONSTRUCT_PARENT_SESSION_ID".to_string(), parent_id.clone()); let params = CreateSessionParams { title: arg_str(&args, "title") .ok() @@ -602,7 +602,7 @@ fn arg_usize(args: &Value, name: &str) -> Option { fn require_session_id(session_id: Option<&str>) -> Result { session_id .map(ToOwned::to_owned) - .ok_or_else(|| anyhow!("subagent tools require AGENTD_SESSION_ID")) + .ok_or_else(|| anyhow!("subagent tools require CONSTRUCT_SESSION_ID")) } async fn owned_subagent_detail( @@ -700,11 +700,13 @@ mod tests { #[tokio::test] async fn subagent_tools_flow_through_mcp_with_parent_scope() { - let dir = - std::env::temp_dir().join(format!("agentd-mcp-subagent-test-{}", std::process::id())); + let dir = std::env::temp_dir().join(format!( + "construct-mcp-subagent-test-{}", + std::process::id() + )); let _ = std::fs::remove_dir_all(&dir); std::fs::create_dir_all(&dir).expect("test dir"); - let sock = dir.join("agentd.sock"); + let sock = dir.join("construct.sock"); let listener = UnixListener::bind(&sock).expect("bind mock daemon"); let server = tokio::spawn(async move { let (stream, _) = listener.accept().await.expect("accept client"); @@ -726,7 +728,7 @@ mod tests { assert_eq!(p.kind, SessionKind::Subagent); assert_eq!(p.parent_session_id.as_deref(), Some("sparent")); assert_eq!( - p.env.get("AGENTD_PARENT_SESSION_ID").map(String::as_str), + p.env.get("CONSTRUCT_PARENT_SESSION_ID").map(String::as_str), Some("sparent") ); assert_eq!(p.mode.as_deref(), Some("headless")); diff --git a/crates/mcp/src/tools/browser.rs b/crates/mcp/src/tools/browser.rs index 7a4a976e..e631db2b 100644 --- a/crates/mcp/src/tools/browser.rs +++ b/crates/mcp/src/tools/browser.rs @@ -1,4 +1,4 @@ -//! Chrome DevTools browser tools exposed through agentd-mcp so every harness +//! Chrome DevTools browser tools exposed through construct-mcp so every harness //! with injected MCP can browse and refresh the TUI browser preview overlay. use agentd_client::Client; @@ -365,7 +365,7 @@ async fn start_chrome(endpoint: &Endpoint) -> Result<()> { )); } let chrome = chrome_path().ok_or_else(|| anyhow!("Chrome/Chromium binary not found"))?; - let profile = format!("/tmp/agentd-chrome-debug-{}", endpoint.port); + let profile = format!("/tmp/construct-chrome-debug-{}", endpoint.port); let mut cmd = Command::new(chrome); cmd.arg(format!("--remote-debugging-port={}", endpoint.port)) .arg(format!("--user-data-dir={profile}")) diff --git a/crates/protocol/src/adapter.rs b/crates/protocol/src/adapter.rs index 63f7d60c..43858fba 100644 --- a/crates/protocol/src/adapter.rs +++ b/crates/protocol/src/adapter.rs @@ -54,7 +54,7 @@ pub mod policy; use crate::paths; -/// A command prefix supplied through an adapter's `AGENTD_*_CMD` override. +/// A command prefix supplied through an adapter's `CONSTRUCT_*_CMD` override. /// /// The first token is the executable to spawn; remaining tokens are prepended /// before the adapter's generated CLI arguments. This lets users configure @@ -76,8 +76,8 @@ impl CommandOverride { } /// Resolve a child CLI command from either a full command override env var -/// (for example `AGENTD_CODEX_CMD=exec codex`) or a binary-only fallback env -/// var (for example `AGENTD_CODEX_BIN=/opt/bin/codex`). +/// (for example `CONSTRUCT_CODEX_CMD=exec codex`) or a binary-only fallback env +/// var (for example `CONSTRUCT_CODEX_BIN=/opt/bin/codex`). /// /// The parser intentionally implements simple shell-like quoting for spaces, /// single quotes, double quotes, and backslash escapes without evaluating a @@ -93,7 +93,7 @@ pub fn resolve_command_override( return cmd; } eprintln!( - "agentd adapter: ignoring invalid {command_env}; falling back to {binary_env}/{default_bin}" + "construct adapter: ignoring invalid {command_env}; falling back to {binary_env}/{default_bin}" ); } CommandOverride { @@ -179,32 +179,32 @@ fn parse_command_words(raw: &str) -> Option { pub fn missing_bin_hint(bin: &str, source: &std::io::Error) -> String { format!( "Failed to start `{bin}`: {source}.\n\ - Make sure `{bin}` is on PATH in the shell you started agentd from \ + Make sure `{bin}` is on PATH in the shell you started constructd from \ (try `which {bin}` there). If you use a version manager (nvm, asdf, \ pyenv, …), activate it in your shell's startup file so PATH is set \ - before launching agentd." + before launching constructd." ) } -/// Returns codex `-c key=value` flag pairs that register `agentd-mcp` as a +/// Returns codex `-c key=value` flag pairs that register `construct-mcp` as a /// session-scoped MCP server. Codex has no `--mcp-config` flag; MCP servers /// live in `[mcp_servers.]` in `config.toml`, and the per-invocation /// override surface is `-c =`. /// /// The returned `Vec` is appended to codex's argv (`-c`, ``, -/// `-c`, ``, ...). Empty when `AGENTD_INJECT_MCP=0` or the -/// `agentd-mcp` binary cannot be located. +/// `-c`, ``, ...). Empty when `CONSTRUCT_INJECT_MCP=0` or the +/// `construct-mcp` binary cannot be located. pub fn maybe_inject_codex_mcp_args(session_id: &str) -> Vec { - if std::env::var("AGENTD_INJECT_MCP").as_deref() == Ok("0") { + if std::env::var("CONSTRUCT_INJECT_MCP").as_deref() == Ok("0") { return Vec::new(); } - let Some(bin) = paths::locate_sibling_binary("agentd-mcp") else { + let Some(bin) = paths::locate_sibling_binary("construct-mcp") else { return Vec::new(); }; let bin_lit = toml_quote(&bin.to_string_lossy()); let env_lit = mcp_env_toml(session_id); let inline = format!("{{ command = {bin_lit}, args = [], env = {env_lit} }}"); - vec!["-c".into(), format!("mcp_servers.agentd={inline}")] + vec!["-c".into(), format!("mcp_servers.construct={inline}")] } fn toml_quote(s: &str) -> String { @@ -239,22 +239,22 @@ fn mcp_env_toml_from(session_id: &str, lookup: impl Fn(&str) -> Option) format!("{{ {} }}", pairs.join(", ")) } -/// If `AGENTD_INJECT_MCP` is not set to `"0"`, attempt to write a per-session +/// If `CONSTRUCT_INJECT_MCP` is not set to `"0"`, attempt to write a per-session /// MCP config (under `state_dir/mcp/.json`) that registers -/// `agentd-mcp` as an MCP server. Returns the config path on success; pass +/// `construct-mcp` as an MCP server. Returns the config path on success; pass /// it to the child CLI via `--mcp-config ` (claude-style). /// /// Used by the claude adapter. Codex uses /// [`maybe_inject_codex_mcp_args`] instead. pub fn maybe_inject_mcp_config(session_id: &str) -> Option { - if std::env::var("AGENTD_INJECT_MCP").as_deref() == Ok("0") { + if std::env::var("CONSTRUCT_INJECT_MCP").as_deref() == Ok("0") { return None; } - let mcp_bin = paths::locate_sibling_binary("agentd-mcp")?; + let mcp_bin = paths::locate_sibling_binary("construct-mcp")?; let paths = paths::Paths::discover(); let dir = paths.state_dir.join("mcp"); if let Err(e) = std::fs::create_dir_all(&dir) { - eprintln!("agentd MCP inject: mkdir {} failed: {e}", dir.display()); + eprintln!("construct MCP inject: mkdir {} failed: {e}", dir.display()); return None; } let cfg_path = dir.join(format!("{session_id}.json")); @@ -270,7 +270,7 @@ pub fn maybe_inject_mcp_config(session_id: &str) -> Option { } let config = serde_json::json!({ "mcpServers": { - "agentd": { + "construct": { "command": mcp_bin.to_string_lossy(), "args": [], "env": env, @@ -280,7 +280,7 @@ pub fn maybe_inject_mcp_config(session_id: &str) -> Option { let text = serde_json::to_string_pretty(&config).ok()?; if let Err(e) = std::fs::write(&cfg_path, text) { eprintln!( - "agentd MCP inject: write {} failed: {e}", + "construct MCP inject: write {} failed: {e}", cfg_path.display() ); return None; @@ -379,7 +379,7 @@ where F: FnOnce(SessionStartParams, AdapterContext) -> Fut + Send + 'static, Fut: Future + Send + 'static, { - if let Some(socket) = std::env::var_os("AGENTD_ADAPTER_SOCKET") { + if let Some(socket) = std::env::var_os("CONSTRUCT_ADAPTER_SOCKET") { return run_reconnectable(metadata, handler, PathBuf::from(socket)).await; } let reader = BufReader::new(tokio::io::stdin()); @@ -387,7 +387,7 @@ where run_with_io(metadata, handler, reader, writer).await } -/// Socket-backed adapter runner used when `AGENTD_ADAPTER_SOCKET` is set. +/// Socket-backed adapter runner used when `CONSTRUCT_ADAPTER_SOCKET` is set. /// /// Unlike stdio mode, daemon disconnect is not adapter shutdown: the session /// task keeps running, outgoing events are retained in memory, and a restarted @@ -1081,10 +1081,10 @@ mod tests { _ => None, }); - assert!(got.contains("AGENTD_SESSION_ID = \"s123\"")); - assert!(got.contains("AGENTD_GLOBAL_MEMORY_FILE = \"/tmp/global.md\"")); - assert!(got.contains("AGENTD_PROJECT_MEMORY_FILE = \"/tmp/project.md\"")); - assert!(got.contains("AGENTD_PROJECT_ID = \"g123\"")); + assert!(got.contains("CONSTRUCT_SESSION_ID = \"s123\"")); + assert!(got.contains("CONSTRUCT_GLOBAL_MEMORY_FILE = \"/tmp/global.md\"")); + assert!(got.contains("CONSTRUCT_PROJECT_MEMORY_FILE = \"/tmp/project.md\"")); + assert!(got.contains("CONSTRUCT_PROJECT_ID = \"g123\"")); } /// Symptom-level repro for the stuck-zarvis-prompt bug. The user diff --git a/crates/protocol/src/adapter/policy.rs b/crates/protocol/src/adapter/policy.rs index 3d6f8682..331070af 100644 --- a/crates/protocol/src/adapter/policy.rs +++ b/crates/protocol/src/adapter/policy.rs @@ -1,7 +1,7 @@ //! Adapter-side auto-approval policy. //! //! The daemon defines a single auto-approval policy per session and exposes it -//! to adapters via the `AGENTD_AUTO_APPROVE_PATHS` env var (colon-separated +//! to adapters via the `CONSTRUCT_AUTO_APPROVE_PATHS` env var (colon-separated //! absolute directories). A file-mutating tool whose target path is under any //! of those directories may run without prompting the user. //! @@ -25,7 +25,7 @@ use std::path::{Path, PathBuf}; /// Env var holding the list of directories whose contents are auto-approved /// for harness writes. Colon-separated (Unix path separator) absolute paths. -pub const ENV_AUTO_APPROVE_PATHS: &str = "AGENTD_AUTO_APPROVE_PATHS"; +pub const ENV_AUTO_APPROVE_PATHS: &str = "CONSTRUCT_AUTO_APPROVE_PATHS"; /// Centralized auto-approval policy. Built from the /// [`ENV_AUTO_APPROVE_PATHS`] env var by [`Self::from_env`], or directly with diff --git a/crates/protocol/src/agent_context.rs b/crates/protocol/src/agent_context.rs index 7ade761d..23b3aa6d 100644 --- a/crates/protocol/src/agent_context.rs +++ b/crates/protocol/src/agent_context.rs @@ -1,18 +1,18 @@ //! Shared agentd context surfaced to agents through `agentd_context`. //! //! The daemon passes memory file paths in env vars. This module reads those -//! files and formats one stable JSON shape used by both `agentd-mcp` and +//! files and formats one stable JSON shape used by both `construct-mcp` and //! zarvis's native tool layer. use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; pub const TOOL_NAME: &str = "agentd_context"; -pub const ENV_GLOBAL_MEMORY_FILE: &str = "AGENTD_GLOBAL_MEMORY_FILE"; -pub const ENV_PROJECT_MEMORY_FILE: &str = "AGENTD_PROJECT_MEMORY_FILE"; -pub const ENV_PROJECT_ID: &str = "AGENTD_PROJECT_ID"; -pub const ENV_SESSION_ID: &str = "AGENTD_SESSION_ID"; -pub const ENV_SESSION_WIDGETS_DIR: &str = "AGENTD_SESSION_WIDGETS_DIR"; +pub const ENV_GLOBAL_MEMORY_FILE: &str = "CONSTRUCT_GLOBAL_MEMORY_FILE"; +pub const ENV_PROJECT_MEMORY_FILE: &str = "CONSTRUCT_PROJECT_MEMORY_FILE"; +pub const ENV_PROJECT_ID: &str = "CONSTRUCT_PROJECT_ID"; +pub const ENV_SESSION_ID: &str = "CONSTRUCT_SESSION_ID"; +pub const ENV_SESSION_WIDGETS_DIR: &str = "CONSTRUCT_SESSION_WIDGETS_DIR"; pub const MAX_MEMORY_BYTES: usize = 24 * 1024; pub const MCP_CONTEXT_ENV_VARS: &[&str] = &[ @@ -20,10 +20,10 @@ pub const MCP_CONTEXT_ENV_VARS: &[&str] = &[ ENV_PROJECT_MEMORY_FILE, ENV_PROJECT_ID, ENV_SESSION_WIDGETS_DIR, - "AGENTD_RUNTIME_DIR", - "AGENTD_STATE_DIR", - "AGENTD_DATA_DIR", - "AGENTD_CONFIG_DIR", + "CONSTRUCT_RUNTIME_DIR", + "CONSTRUCT_STATE_DIR", + "CONSTRUCT_DATA_DIR", + "CONSTRUCT_CONFIG_DIR", ]; pub const TOOL_DESCRIPTION: &str = diff --git a/crates/protocol/src/lib.rs b/crates/protocol/src/lib.rs index 428165f7..edfbf8fa 100644 --- a/crates/protocol/src/lib.rs +++ b/crates/protocol/src/lib.rs @@ -453,7 +453,7 @@ pub enum SessionEvent { args_summary: String, }, /// The tool's foreground budget elapsed (auto-bg at - /// `AGENTD_TOOL_BG_AFTER_MS`) or the user clicked `[bg]` / + /// `CONSTRUCT_TOOL_BG_AFTER_MS`) or the user clicked `[bg]` / /// invoked `session.tool_action { action: "background" }`. The /// adapter detached the join handle into its background pool; /// the agent's conversation got a placeholder result and moved @@ -729,7 +729,7 @@ pub mod ipc_method { pub const SESSION_WIDGET_DELETE: &str = "session.widget.delete"; /// Respawn a session's adapter — typically used to bring a `Done` /// session back to life so the user can continue typing. The - /// adapter is launched with `AGENTD_RESUME=1` so harnesses that + /// adapter is launched with `CONSTRUCT_RESUME=1` so harnesses that /// persist conversation state (e.g. zarvis) can pick up where /// they left off. pub const SESSION_RESTART: &str = "session.restart"; @@ -740,7 +740,7 @@ pub mod ipc_method { pub const SESSION_TOOL_ACTION: &str = "session.tool_action"; pub const SESSION_LIST_TASKS: &str = "session.list_tasks"; /// Append/broadcast a structured event for a session. Intended for trusted - /// local helpers such as agentd-mcp that run outside an adapter but need to + /// local helpers such as construct-mcp that run outside an adapter but need to /// surface UI-only state (for example browser previews) in the caller's /// session. pub const SESSION_EMIT_EVENT: &str = "session.emit_event"; @@ -770,7 +770,7 @@ pub mod ipc_method { /// can answer `session.chat_viewer_active`. pub const SESSION_SET_VIEW: &str = "session.set_view"; /// Query whether any connected client is watching the session in the chat - /// view. Used by the `agent ask-gate` hook to decide whether to degrade + /// view. Used by the `construct ask-gate` hook to decide whether to degrade /// `AskUserQuestion` to a plain-text question. pub const SESSION_CHAT_VIEWER_ACTIVE: &str = "session.chat_viewer_active"; pub const SUBSCRIBE_EVENTS: &str = "subscribe.events"; diff --git a/crates/protocol/src/paths.rs b/crates/protocol/src/paths.rs index 5877a0be..e342e35f 100644 --- a/crates/protocol/src/paths.rs +++ b/crates/protocol/src/paths.rs @@ -1,7 +1,7 @@ //! XDG-style path conventions shared between daemon and client. //! -//! Each layer respects `AGENTD_*_DIR` env overrides, then `XDG_*_HOME`, -//! falling back to standard `$HOME/.config|.local/state|.local/share/agentd`. +//! Each layer respects `CONSTRUCT_*_DIR` env overrides, then `XDG_*_HOME`, +//! falling back to standard `$HOME/.config|.local/state|.local/share/construct`. use std::path::PathBuf; @@ -17,23 +17,50 @@ impl Paths { pub fn discover() -> Self { let home = home_dir(); - let config_dir = env_dir("AGENTD_CONFIG_DIR").unwrap_or_else(|| { + let config_dir = env_dir("CONSTRUCT_CONFIG_DIR").unwrap_or_else(|| { env_dir("XDG_CONFIG_HOME") .unwrap_or_else(|| home.join(".config")) - .join("agentd") + .join("construct") }); - let state_dir = env_dir("AGENTD_STATE_DIR").unwrap_or_else(|| { + let state_dir = env_dir("CONSTRUCT_STATE_DIR").unwrap_or_else(|| { env_dir("XDG_STATE_HOME") .unwrap_or_else(|| home.join(".local").join("state")) - .join("agentd") + .join("construct") }); - let data_dir = env_dir("AGENTD_DATA_DIR").unwrap_or_else(|| { + let data_dir = env_dir("CONSTRUCT_DATA_DIR").unwrap_or_else(|| { env_dir("XDG_DATA_HOME") .unwrap_or_else(|| home.join(".local").join("share")) - .join("agentd") + .join("construct") }); - let runtime_dir = env_dir("AGENTD_RUNTIME_DIR") - .or_else(|| env_dir("XDG_RUNTIME_DIR").map(|p| p.join("agentd"))) + let runtime_dir = env_dir("CONSTRUCT_RUNTIME_DIR") + .or_else(|| env_dir("XDG_RUNTIME_DIR").map(|p| p.join("construct"))) + .unwrap_or_else(|| state_dir.clone()); + + Self { + config_dir, + state_dir, + data_dir, + runtime_dir, + } + } + + /// Resolve the legacy `agentd` layout so startup can offer a migration + /// message when existing `~/.config|.local|XDG_*` directories are still + /// using pre-rename names. + pub fn discover_legacy() -> Self { + let home = home_dir(); + + let config_dir = env_dir("XDG_CONFIG_HOME") + .unwrap_or_else(|| home.join(".config")) + .join("agentd"); + let state_dir = env_dir("XDG_STATE_HOME") + .unwrap_or_else(|| home.join(".local").join("state")) + .join("agentd"); + let data_dir = env_dir("XDG_DATA_HOME") + .unwrap_or_else(|| home.join(".local").join("share")) + .join("agentd"); + let runtime_dir = env_dir("XDG_RUNTIME_DIR") + .map(|p| p.join("agentd")) .unwrap_or_else(|| state_dir.clone()); Self { @@ -45,7 +72,7 @@ impl Paths { } pub fn socket(&self) -> PathBuf { - self.runtime_dir.join("agentd.sock") + self.runtime_dir.join("construct.sock") } pub fn pid_file(&self) -> PathBuf { @@ -91,14 +118,14 @@ fn home_dir() -> PathBuf { } /// Default port for the localhost-only browser UI. Override with the -/// `AGENTD_WEBUI_PORT` env var. The daemon binds `127.0.0.1:`; the -/// CLI's `agent paths` prints the resolved URL. +/// `CONSTRUCT_WEBUI_PORT` env var. The daemon binds `127.0.0.1:`; the +/// CLI's `construct paths` prints the resolved URL. pub const DEFAULT_WEBUI_PORT: u16 = 5746; -/// Resolve the localhost web-UI port from `AGENTD_WEBUI_PORT`, falling back +/// Resolve the localhost web-UI port from `CONSTRUCT_WEBUI_PORT`, falling back /// to [`DEFAULT_WEBUI_PORT`] when the var is unset or unparseable. pub fn local_webui_port() -> u16 { - std::env::var("AGENTD_WEBUI_PORT") + std::env::var("CONSTRUCT_WEBUI_PORT") .ok() .and_then(|s| s.parse::().ok()) .unwrap_or(DEFAULT_WEBUI_PORT) @@ -109,10 +136,10 @@ pub fn local_webui_url() -> String { format!("http://127.0.0.1:{}/", local_webui_port()) } -/// Resolve a sibling binary (an adapter, `agentd-mcp`, etc.) by name. +/// Resolve a sibling binary (an adapter, `construct-mcp`, etc.) by name. /// Search order: absolute path → next to the current executable → `$PATH`. /// Returns `None` if not found. Used by the daemon to find adapter -/// binaries and by adapters to find auxiliary tools like `agentd-mcp`. +/// binaries and by adapters to find auxiliary tools like `construct-mcp`. pub fn locate_sibling_binary(name: &str) -> Option { let p = PathBuf::from(name); if p.is_absolute() { diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 1278945b..3d9cada2 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -8,8 +8,8 @@ checksums, and publishes a GitHub Release with auto-generated release notes. ## Versioning The single source of truth is `version` under `[workspace.package]` in the -root `Cargo.toml`. Every binary inherits it, so `agentd --version` and -`agent --version` always report the workspace version. Use [semver](https://semver.org/) +root `Cargo.toml`. `constructd --version` and +`construct --version` always report the workspace version. Use [semver](https://semver.org/) (`MAJOR.MINOR.PATCH`). The release workflow's `verify` job refuses to build unless the pushed tag @@ -39,14 +39,14 @@ never publish a mislabelled binary. 3. The workflow runs. When it finishes, a GitHub Release for `v0.2.0` exists with these assets: - - `agentd-aarch64-apple-darwin.tar.gz` (macOS, Apple Silicon) - - `agentd-x86_64-apple-darwin.tar.gz` (macOS, Intel) - - `agentd-x86_64-unknown-linux-musl.tar.gz` (Linux x86_64, static) - - `agentd-aarch64-unknown-linux-gnu.tar.gz` (Linux arm64) + - `constructd-aarch64-apple-darwin.tar.gz` (macOS, Apple Silicon) + - `constructd-x86_64-apple-darwin.tar.gz` (macOS, Intel) + - `constructd-x86_64-unknown-linux-musl.tar.gz` (Linux x86_64, static) + - `constructd-aarch64-unknown-linux-gnu.tar.gz` (Linux arm64) - `SHA256SUMS` - Each tarball contains all release binaries (`agent`, `agentd`, - `agentd-mcp`, `agentd-adapter-*`) plus `README.md` and `LICENSE`. + Each tarball contains all release binaries (`construct`, `constructd`, + `construct-mcp`, `construct-adapter-*`) plus `README.md` and `LICENSE`. 4. Review the release notes. The workflow passes `generate_release_notes: true` to the release step, so GitHub fills the release body automatically from the diff --git a/docs/architecture.md b/docs/architecture.md index 6a135f05..8e234089 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -18,8 +18,8 @@ Five layers, each replaceable: └──────────────────────────────────────────────┘ ``` -- **Daemon** (`agentd`) owns sessions, spawns adapters, persists transcripts. Speaks JSON-RPC over a Unix socket to clients. -- **Client** (`agent`) is the TUI plus a set of one-shot subcommands. Multiple clients can attach concurrently. +- **Daemon** (`constructd`) owns sessions, spawns adapters, persists transcripts. Speaks JSON-RPC over a Unix socket to clients. +- **Client** (`construct`) is the TUI plus a set of one-shot subcommands. Multiple clients can attach concurrently. - **Adapter** binaries are independent processes. They implement the AHP over stdio. Anyone can ship one in any language. ## Crates @@ -27,12 +27,12 @@ Five layers, each replaceable: | Crate | Binary | Purpose | |---|---|---| | `crates/protocol` | — (lib) | AHP + IPC types, transport, adapter SDK | -| `crates/daemon` | `agentd` | Session supervisor, IPC server | -| `crates/cli` | `agent` | TUI client + control subcommands | -| `crates/adapter-shell` | `agentd-adapter-shell` | Generic shell command runner | -| `crates/adapter-claude` | `agentd-adapter-claude` | Wraps the `claude` CLI | -| `crates/adapter-codex` | `agentd-adapter-codex` | Wraps the `codex` CLI | -| `crates/adapter-zarvis` | `agentd-adapter-zarvis` | Built-in multi-provider agent (OpenAI / Anthropic / Gemini / Ollama) | +| `crates/daemon` | `constructd` | Session supervisor, IPC server | +| `crates/cli` | `construct` | TUI client + control subcommands | +| `crates/adapter-shell` | `construct-adapter-shell` | Generic shell command runner | +| `crates/adapter-claude` | `construct-adapter-claude` | Wraps the `claude` CLI | +| `crates/adapter-codex` | `construct-adapter-codex` | Wraps the `codex` CLI | +| `crates/adapter-zarvis` | `construct-adapter-smith` | Built-in multi-provider agent (OpenAI / Anthropic / Gemini / Ollama) | ## Adapter protocol (AHP) diff --git a/docs/configuration.md b/docs/configuration.md index cf7dbe27..03621820 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -2,16 +2,16 @@ ## Paths -`agentd` reads/writes under XDG-style directories, with `AGENTD_*_DIR` overrides: +`construct` reads/writes under XDG-style directories, with `CONSTRUCT_*_DIR` overrides: | Use | Default | Override | |---|---|---| -| Config | `~/.config/agentd` | `AGENTD_CONFIG_DIR` | -| State (pid/log) | `~/.local/state/agentd` | `AGENTD_STATE_DIR` | -| Data (sessions, projects, memory) | `~/.local/share/agentd` | `AGENTD_DATA_DIR` | -| Socket | `$XDG_RUNTIME_DIR/agentd/agentd.sock` (falls back to state) | `AGENTD_RUNTIME_DIR` | +| Config | `~/.config/construct` | `CONSTRUCT_CONFIG_DIR` | +| State (pid/log) | `~/.local/state/construct` | `CONSTRUCT_STATE_DIR` | +| Data (sessions, projects, memory) | `~/.local/share/construct` | `CONSTRUCT_DATA_DIR` | +| Socket | `$XDG_RUNTIME_DIR/construct/construct.sock` (falls back to state) | `CONSTRUCT_RUNTIME_DIR` | -`agentd paths` prints the resolved layout. +`construct paths` prints the resolved layout. The data directory stores durable, user-editable runtime data: @@ -40,54 +40,54 @@ the public, token-protected path is `/remote-control`, see | Use | Default | Override | |---|---|---| -| Web UI port | `5746` (binds `http://127.0.0.1:5746/`) | `AGENTD_WEBUI_PORT` | +| Web UI port | `5746` (binds `http://127.0.0.1:5746/`) | `CONSTRUCT_WEBUI_PORT` | -`agent paths` (and `agentd paths`) print the resolved URL on the `webui:` line, so +`construct paths` prints the resolved URL on the `webui:` line, so you don't have to dig it out of the daemon log: ```text -$ agent paths -config: ~/.config/agentd -state: ~/.local/state/agentd -data: ~/.local/share/agentd -runtime: ~/.local/state/agentd -socket: ~/.local/state/agentd/agentd.sock +$ construct paths +config: ~/.config/construct +state: ~/.local/state/construct +data: ~/.local/share/construct +runtime: ~/.local/state/construct +socket: ~/.local/state/construct/construct.sock webui: http://127.0.0.1:5746/ ``` ## Built-in harness child command overrides Built-in adapters spawn their underlying CLI directly (no shell). For a -binary-only override, set the existing `AGENTD_*_BIN` env var in +binary-only override, set the existing `CONSTRUCT_*_BIN` env var in `config.toml`: ```toml [adapters.codex] -env = { AGENTD_CODEX_BIN = "/opt/homebrew/bin/codex" } +env = { CONSTRUCT_CODEX_BIN = "/opt/homebrew/bin/codex" } ``` When the command needs a prefix or extra executable before the real CLI, use -`AGENTD_*_CMD` instead. It is split shell-style for whitespace, quotes, and +`CONSTRUCT_*_CMD` instead. It is split shell-style for whitespace, quotes, and backslashes, but is still executed directly without shell expansion: ```toml [adapters.codex] -env = { AGENTD_CODEX_CMD = "exec codex" } +env = { CONSTRUCT_CODEX_CMD = "exec codex" } ``` -`AGENTD_*_CMD` wins over `AGENTD_*_BIN`. Supported names: +`CONSTRUCT_*_CMD` wins over `CONSTRUCT_*_BIN`. Supported names: | Harness | Full command override | Binary-only fallback | |---|---|---| -| `codex` | `AGENTD_CODEX_CMD` | `AGENTD_CODEX_BIN` | -| `claude` | `AGENTD_CLAUDE_CMD` | `AGENTD_CLAUDE_BIN` | -| `antigravity` | `AGENTD_ANTIGRAVITY_CMD` | `AGENTD_ANTIGRAVITY_BIN` | -| `shell` | `AGENTD_SHELL_CMD` | `AGENTD_SHELL_BIN` | +| `codex` | `CONSTRUCT_CODEX_CMD` | `CONSTRUCT_CODEX_BIN` | +| `claude` | `CONSTRUCT_CLAUDE_CMD` | `CONSTRUCT_CLAUDE_BIN` | +| `antigravity` | `CONSTRUCT_ANTIGRAVITY_CMD` | `CONSTRUCT_ANTIGRAVITY_BIN` | +| `shell` | `CONSTRUCT_SHELL_CMD` | `CONSTRUCT_SHELL_BIN` | ## TUI Theme The TUI uses a built-in Matrix theme by default. Override any color slot in -`$AGENTD_CONFIG_DIR/theme.toml` (default `~/.config/agentd/theme.toml`): +`$CONSTRUCT_CONFIG_DIR/theme.toml` (default `~/.config/construct/theme.toml`): ```toml [colors] diff --git a/docs/generative-widgets.md b/docs/generative-widgets.md index 473349e0..bf09a6ff 100644 --- a/docs/generative-widgets.md +++ b/docs/generative-widgets.md @@ -54,7 +54,7 @@ widgets after reconnect without replaying the model conversation. ## Markdown subset -Widgets use "agentd Markdown": normal Markdown plus a small set of semantic +Widgets use "construct Markdown": normal Markdown plus a small set of semantic extensions. Renderers parse the pieces they understand and degrade the rest to plain text. @@ -146,7 +146,7 @@ A typical task status widget: Write it to the current session's widget directory: ```bash -cat >"$AGENTD_SESSION_WIDGETS_DIR/pr-cleanup.md" <<'EOF' +cat >"$CONSTRUCT_SESSION_WIDGETS_DIR/pr-cleanup.md" <<'EOF' # PR cleanup :::timeline diff --git a/docs/harnesses.md b/docs/harnesses.md index d5f2b39b..06c08560 100644 --- a/docs/harnesses.md +++ b/docs/harnesses.md @@ -1,39 +1,39 @@ # Harnesses -A **harness** is an agent or shell runner inside agentd. Harnesses let you run -Zarvis, Claude, Codex, Antigravity, and local shells side by side while agentd +A **harness** is an agent or shell runner inside construct. Harnesses let you run +smith, Claude, Codex, Antigravity, and local shells side by side while construct gives them one UI, history, widgets, control plane, and shared approval surface where supported. -A **fleet** is the set of sessions managed by one agentd daemon. For example, +A **fleet** is the set of sessions managed by one construct daemon. For example, you can keep a shell running tests, ask Codex to implement a fix, ask Claude to -review it, and use Zarvis as the built-in coordinator. +review it, and use smith as the built-in coordinator. ## Which harness should I use? | Harness | What it is | Use it when | | --- | --- | --- | -| `zarvis` | agentd's built-in agent | You want the deepest agentd integration: native tools, approvals, skills, widgets, orchestration, and model-provider routing. | +| `smith` | construct's built-in agent | You want the deepest construct integration: native tools, approvals, skills, widgets, orchestration, and model-provider routing. | | `shell` | Your local shell | You need long-running commands, logs, REPLs, servers, or manual debugging. | -| `claude` | The Claude CLI | You already use Claude Code and want it inside the same agentd UI and session fleet. | -| `codex` | The Codex CLI | You already use Codex and want it inside the same agentd UI and session fleet. | +| `claude` | The Claude CLI | You already use Claude Code and want it inside the same construct UI and session fleet. | +| `codex` | The Codex CLI | You already use Codex and want it inside the same construct UI and session fleet. | | `antigravity` | The Antigravity CLI | You want Antigravity sessions inside the same UI and daemon. | Create a session with: ```sh -agent new zarvis "review this repo" -agent new shell "" -agent new codex "implement the failing test" +construct new smith "review this repo" +construct new shell "" +construct new codex "implement the failing test" ``` CLI-backed harnesses require the matching CLI to be installed and discoverable on `PATH`. Use the `*_BIN` or `*_CMD` environment variables below when you need to -point agentd at a specific binary or command. +point construct at a specific binary or command. -## What agentd gives every harness +## What construct gives every harness -agentd gives every harness the same shared session model, then lets each adapter +construct gives every harness the same shared session model, then lets each adapter translate that model into the underlying agent or shell. | Capability | Why it matters | Support and details | @@ -41,15 +41,15 @@ translate that model into the underlying agent or shell. | Session identity and lifecycle | Every harness has the same id, title, state, cwd, mode, transcript, and lifecycle. | All harnesses. | | Transcript and scrollback | You can inspect session history from the TUI, Web UI, and remote APIs, even after restart. | All harnesses; fidelity depends on what the harness emits. | | Shared UI | Different CLIs appear in one session list instead of separate terminals. | All harnesses. | -| Approval flow | Risky actions can use agentd's approval UI instead of each session inventing its own workflow. | Native in `zarvis`; translated where CLI-backed harnesses expose enough control. | -| Widgets | Agents can publish Markdown status/action panels once and every client can render them. | All harnesses can write widgets via `AGENTD_SESSION_WIDGETS_DIR`; see [Generative widgets](generative-widgets.md). | +| Approval flow | Risky actions can use construct's approval UI instead of each session inventing its own workflow. | Native in `smith`; translated where CLI-backed harnesses expose enough control. | +| Widgets | Agents can publish Markdown status/action panels once and every client can render them. | All harnesses can write widgets via `CONSTRUCT_SESSION_WIDGETS_DIR`; see [Generative widgets](generative-widgets.md). | | Session context | Sessions receive shared cwd, environment, data dirs, widget dirs, memory pointers, and resume flags. | All harnesses receive the context; each upstream CLI decides what to do with it. | -| Skills | Reusable instructions can be defined once for the built-in agent. | Native in `zarvis`; CLI-backed harnesses use their own upstream skill/plugin systems today. | -| Unified tools | Agents can inspect and coordinate the fleet without shelling out to `agent` commands. | Native in `zarvis`; injected through MCP where supported; see [Unified tool layer](unified-tool-layer.md). | -| Resume | Restarts do not wipe out what you were looking at, and some upstream CLIs can continue the same conversation. | `zarvis` resumes from agentd state; `shell` restarts in the same cwd; CLI-backed harnesses resume when their CLI exposes a reliable mechanism. | +| Skills | Reusable instructions can be defined once for the built-in agent. | Native in `smith`; CLI-backed harnesses use their own upstream skill/plugin systems today. | +| Unified tools | Agents can inspect and coordinate the fleet without shelling out to `construct` commands. | Native in `smith`; injected through MCP where supported; see [Unified tool layer](unified-tool-layer.md). | +| Resume | Restarts do not wipe out what you were looking at, and some upstream CLIs can continue the same conversation. | `smith` resumes from construct state; `shell` restarts in the same cwd; CLI-backed harnesses resume when their CLI exposes a reliable mechanism. | The adapter is the translation layer between these fleet-wide capabilities and a -specific harness. Some capabilities are native in Zarvis, some are injected into +specific harness. Some capabilities are native in smith, some are injected into CLI-backed harnesses, and some depend on what the upstream CLI exposes. ## Built-in vs CLI-backed harnesses @@ -58,20 +58,20 @@ There are two kinds of harnesses: ### Built-in harness -`zarvis` is native to agentd. Use it when you want access to the most agentd +`smith` is native to construct. Use it when you want access to the most construct features: tools, approvals, skills, widgets, background tasks, and structured status updates. -See [Zarvis built-in agent](zarvis.md) for details. +See [smith built-in agent](smith.md) for details. ### CLI-backed harnesses `claude`, `codex`, and `antigravity` wrap existing CLIs. Use them when you want -those tools exactly as installed on your machine, but inside the same agentd +those tools exactly as installed on your machine, but inside the same construct fleet. CLI-backed harnesses keep their native behavior. If an upstream CLI does not -expose a setting — for example, path-scoped tool auto-approval — agentd cannot +expose a setting — for example, path-scoped tool auto-approval — construct cannot always force that behavior from outside the process. In those cases the session still gets the shared UI, transcript, lifecycle, and environment, but the upstream CLI keeps control of its own internals. @@ -81,18 +81,18 @@ upstream CLI keeps control of its own internals. Most harnesses can run in two modes: - **Interactive**: the harness owns a PTY, so its normal terminal UI appears in - the agentd pane. This is the default when you create sessions from the TUI. + the construct pane. This is the default when you create sessions from the TUI. - **Headless**: the harness emits structured events instead of a terminal UI. This is useful for automation and non-PTY clients. **How the mode is chosen.** An explicit `--mode` always wins. Otherwise the mode is *interactive* when the creating client supplied a PTY size and *headless* when it did not. The TUI always supplies one, so TUI sessions default to interactive. -The `agent new` CLI never supplies one, so **every `agent new` session is headless +The `construct new` CLI never supplies one, so **every `construct new` session is headless unless you pass `--mode interactive`** — regardless of the prompt. -**The initial prompt does not pick the mode.** `agent new ""` -and `agent new ""` are both headless from the CLI; the prompt only +**The initial prompt does not pick the mode.** `construct new ""` +and `construct new ""` are both headless from the CLI; the prompt only decides what the session does once it starts: - A non-empty prompt is recorded as the first user turn and run immediately. For @@ -105,25 +105,25 @@ decides what the session does once it starts: Pass `--mode` to choose explicitly (optionally alongside a seed prompt): ```sh -agent new claude --mode interactive "" -agent new zarvis --mode headless "summarize the last run" +construct new claude --mode interactive "" +construct new smith --mode headless "summarize the last run" ``` -`zarvis`, `claude`, `codex`, and `antigravity` support both modes. `shell` always +`smith`, `claude`, `codex`, and `antigravity` support both modes. `shell` always owns a PTY (there is no structured "headless" shell), so it presents a terminal regardless of the mode label. ## Resume after restart -When agentd restarts, it restores sessions from saved start parameters: +When construct restarts, it restores sessions from saved start parameters: - PTY scrollback and transcripts remain readable. - `shell` starts a fresh shell in the original cwd. -- `zarvis` reloads its persisted conversation state. +- `smith` reloads its persisted conversation state. - CLI-backed harnesses resume when their upstream CLI provides a reliable session id or resume command. -If a harness cannot be restarted — for example, its binary is missing — agentd +If a harness cannot be restarted — for example, its binary is missing — construct marks the session errored while keeping the transcript available. ## Common knobs @@ -133,16 +133,16 @@ You normally do not need these, but they are useful for scripting and debugging: | Setting | Purpose | | --- | --- | | `--mode interactive\|headless` | Choose the session mode at creation time. | -| `AGENTD_ZARVIS_MODE`, `AGENTD_CLAUDE_MODE`, `AGENTD_CODEX_MODE`, `AGENTD_ANTIGRAVITY_MODE` | Default mode per harness. | -| `AGENTD_CLAUDE_CMD`, `AGENTD_CODEX_CMD`, `AGENTD_ANTIGRAVITY_CMD`, `AGENTD_SHELL_CMD` | Override the full command used for a CLI-backed harness or shell. | -| `AGENTD_CLAUDE_BIN`, `AGENTD_CODEX_BIN`, `AGENTD_ANTIGRAVITY_BIN`, `AGENTD_SHELL_BIN` | Override just the binary path when no full command override is set. | -| `AGENTD_ZARVIS_MODEL` | Default model for the built-in Zarvis harness. | -| `AGENTD_AUTO_APPROVE_PATHS` | Path allow-list injected into adapters that can translate it. | -| `AGENTD_SESSION_WIDGETS_DIR` | Directory where a session writes Markdown widgets. | -| `AGENTD_INJECT_MCP=0` | Disable automatic MCP tool injection for MCP-capable harnesses. | - -Set these in the daemon environment, or in whatever process starts `agentd`. See +| `CONSTRUCT_SMITH_MODE`, `CONSTRUCT_CLAUDE_MODE`, `CONSTRUCT_CODEX_MODE`, `CONSTRUCT_ANTIGRAVITY_MODE` | Default mode per harness. | +| `CONSTRUCT_CLAUDE_CMD`, `CONSTRUCT_CODEX_CMD`, `CONSTRUCT_ANTIGRAVITY_CMD`, `CONSTRUCT_SHELL_CMD` | Override the full command used for a CLI-backed harness or shell. | +| `CONSTRUCT_CLAUDE_BIN`, `CONSTRUCT_CODEX_BIN`, `CONSTRUCT_ANTIGRAVITY_BIN`, `CONSTRUCT_SHELL_BIN` | Override just the binary path when no full command override is set. | +| `CONSTRUCT_SMITH_MODEL` | Default model for the built-in smith harness. | +| `CONSTRUCT_AUTO_APPROVE_PATHS` | Path allow-list injected into adapters that can translate it. | +| `CONSTRUCT_SESSION_WIDGETS_DIR` | Directory where a session writes Markdown widgets. | +| `CONSTRUCT_INJECT_MCP=0` | Disable automatic MCP tool injection for MCP-capable harnesses. | + +Set these in the daemon environment, or in whatever process starts `construct`. See [Configuration](configuration.md) for general configuration patterns. -Prefer the normal `agent new ...` flow unless you are integrating agentd into a +Prefer the normal `construct new ...` flow unless you are integrating construct into a larger script or testing a custom harness setup. diff --git a/docs/memory.md b/docs/memory.md index c6c4708a..a7f1a805 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -1,11 +1,11 @@ # Memory -agentd gives agents a small, durable memory surface for facts that should carry +construct gives agents a small, durable memory surface for facts that should carry across turns, sessions, or future work in the same project. Memory is plain Markdown so it stays readable, editable, and easy to audit. -Memory is shared across all agentd harness types in the same scope, so Codex, -Claude Code, Zarvis, and other agents can build on the same durable context. +Memory is shared across all construct harness types in the same scope, so Codex, +Claude Code, smith, and other agents can build on the same durable context. Memory is intentionally separate from transcripts: diff --git a/docs/remote-control.md b/docs/remote-control.md index 09698907..3ab1da8a 100644 --- a/docs/remote-control.md +++ b/docs/remote-control.md @@ -11,8 +11,8 @@ shows a `remote` badge while remote clients are attached. | `/remote-control ` | Start remote control with a user-chosen password. | | `/remote-control stop` | Stop the remote listener/tunnel and rotate credentials for the next start. | | `/remote-control debug` | Start a local-only URL without a public tunnel; mostly retained for troubleshooting remote-control credentials/tokens. For normal local browser use, open the always-on local web UI instead. | -| `AGENTD_REMOTE_WS_PORT=` | Start the remote WebSocket listener on daemon boot for scripted/headless use. | -| `AGENTD_WEBUI_PORT=` | Override the always-on localhost web UI port. Defaults to `5746`. | +| `CONSTRUCT_REMOTE_WS_PORT=` | Start the remote WebSocket listener on daemon boot for scripted/headless use. | +| `CONSTRUCT_WEBUI_PORT=` | Override the always-on localhost web UI port. Defaults to `5746`. | The daemon also starts a localhost-only browser UI at `http://127.0.0.1:5746/` by default. This local UI is bound to loopback and does **not** require the diff --git a/docs/zarvis.md b/docs/smith.md similarity index 76% rename from docs/zarvis.md rename to docs/smith.md index 4199747f..92ebf6b2 100644 --- a/docs/zarvis.md +++ b/docs/smith.md @@ -1,10 +1,10 @@ -# zarvis built-in agent +# smith built-in agent -`zarvis` is the built-in agent that ships with agentd. It talks to OpenAI, +`smith` is the built-in agent that ships with construct. It talks to OpenAI, Anthropic, Google Gemini, or a local Ollama directly and runs its own agent loop with shell + -filesystem + agentd-control tools. No external CLI install required. Many PRs -for the agentd repository have already been made from Zarvis sessions running -inside agentd. +filesystem + construct-control tools. No external CLI install required. Many PRs +for the construct repository have already been made from smith sessions running +inside construct. ### Quick start @@ -15,12 +15,12 @@ export ANTHROPIC_API_KEY=sk-ant-... # or export GEMINI_API_KEY=... # (or GOOGLE_API_KEY) # or run a local ollama (default http://localhost:11434) -agent new zarvis "list the rust files in this repo and summarize what each crate does" +construct new smith "list the rust files in this repo and summarize what each crate does" ``` ### Model selection -Pass `--model ` on `agent new` (or set `AGENTD_ZARVIS_MODEL`). +Pass `--model ` on `construct new` (or set `CONSTRUCT_SMITH_MODEL`). The spec is one of: - `openai:` — e.g. `openai:gpt-5-mini` @@ -32,7 +32,7 @@ Bare names auto-detect: `gpt-*` / `o[1-5]*` → OpenAI, `claude-*` → Anthropic, `gemini-*` → Gemini, anything else → Ollama. When in doubt, use the explicit prefix. -If you don't pass a model and `AGENTD_ZARVIS_MODEL` isn't set, zarvis +If you don't pass a model and `CONSTRUCT_SMITH_MODEL` isn't set, smith picks: `ANTHROPIC_API_KEY` → `claude-opus-4-8`, else `OPENAI_API_KEY` → `gpt-5`, else `GEMINI_API_KEY` (or `GOOGLE_API_KEY`) → `gemini-2.5-pro`, else `ollama:llama3.1`. The initial Status event @@ -43,22 +43,22 @@ records the chosen `provider:model` so you can verify. Local: `shell`, `read_file`, `write_file`, `edit_file` (search/replace with required uniqueness), `list_dir`, `find_files`. -Agentd-control (16 tools, same surface as `agentd-mcp`): +Agentd-control (16 tools, same surface as `construct-mcp`): `agentd_list_sessions`, `agentd_create_session`, `agentd_send_input`, `agentd_get_output`, `agentd_get_diff`, `agentd_pin_session`, `agentd_rename_session`, … — full read + write access to other sessions on the same daemon. `agentd_whoami` returns the session id -this zarvis is running inside (auto-injected via env). +this smith is running inside (auto-injected via env). Browser: `browser_open`, `browser_inspect`, `browser_screenshot`, and `browser_eval` drive Chrome through DevTools and emit the same browser preview thumbnail that the TUI renders above the session. These tools -are native to zarvis and are also exposed through `agentd-mcp` for +are native to smith and are also exposed through `construct-mcp` for MCP-capable harnesses. ### Approval / automode -Tool calls run with your permissions, so zarvis classifies each tool +Tool calls run with your permissions, so smith classifies each tool as **Safe** (read-only — `read_file`, `list_dir`, `find_files`, all `agentd_get_*`/`agentd_list_*`) or **Risky** (mutates fs/sessions — everything else, including `shell`). @@ -73,7 +73,7 @@ flip automode on for this session**. Toggle automode anytime with `C-x A` (emacs) / `A` (vim). Denied calls return a synthetic "user denied" result to the model so it can pivot rather than crash. -Override the initial state with `AGENTD_ZARVIS_AUTOMODE=1` (useful for +Override the initial state with `CONSTRUCT_SMITH_AUTOMODE=1` (useful for scripted/batch runs). ### Long output handling @@ -89,8 +89,8 @@ two most-recent. ### Opt-out / customization -- `AGENTD_ZARVIS_AUTOMODE=1` — start with automode on. -- `AGENTD_ZARVIS_MODEL=` — default model when `--model` is +- `CONSTRUCT_SMITH_AUTOMODE=1` — start with automode on. +- `CONSTRUCT_SMITH_MODEL=` — default model when `--model` is omitted. - `GEMINI_API_KEY` / `GOOGLE_API_KEY` — Gemini credentials (either is accepted). diff --git a/docs/unified-tool-layer.md b/docs/unified-tool-layer.md index 26f8c69c..e56997a7 100644 --- a/docs/unified-tool-layer.md +++ b/docs/unified-tool-layer.md @@ -1,9 +1,9 @@ # Unified tool layer This page is the detailed reference for the **Unified tools** capability in -[Harnesses](harnesses.md#what-agentd-gives-every-harness). +[Harnesses](harnesses.md#what-construct-gives-every-harness). -Unified tools let agents inspect, control, and coordinate the agentd fleet. For +Unified tools let agents inspect, control, and coordinate the construct fleet. For example: - A review agent can read the implementer's diff before commenting. @@ -12,29 +12,29 @@ example: - A session can publish a status widget without knowing whether the user is in the TUI or Web UI. -Zarvis uses these tools natively. MCP-capable harnesses receive the same tools -through `agentd-mcp`, so they can coordinate the fleet without shelling out to -ad-hoc `agent` CLI commands. +Smith uses these tools natively. MCP-capable harnesses receive the same tools +through `construct-mcp`, so they can coordinate the fleet without shelling out to +ad-hoc `construct` CLI commands. ## Using unified tools -There is usually nothing to configure. Zarvis sees these tools natively. Claude -Code and Codex receive them automatically when agentd can find `agentd-mcp`; set -`AGENTD_INJECT_MCP=0` in the daemon environment to opt out. +There is usually nothing to configure. smith sees these tools natively. Claude +Code and Codex receive them automatically when construct can find `construct-mcp`; set +`CONSTRUCT_INJECT_MCP=0` in the daemon environment to opt out. Agents invoke these tools during tasks, just like their other tools. A quick way -to verify injection is to ask a Claude or Codex session to list available agentd +to verify injection is to ask a Claude or Codex session to list available construct sessions; it should be able to use `agentd_list_sessions` without running the -`agent` CLI in a shell. +`construct` CLI in a shell. ## Harness support | Harness | User-facing status | Implementation notes | |---|---|---| -| Zarvis | Built in. | Uses the same tool set without an external MCP process. | -| Claude Code | Enabled by default when `agentd-mcp` is available. | Adapter writes a config under `AGENTD_STATE_DIR` and passes `--mcp-config `. | -| Codex | Enabled by default when `agentd-mcp` is available. | Adapter passes Codex a `-c mcp_servers.agentd=...` TOML override. | -| Antigravity | Not injected yet. | Receives `AGENTD_SESSION_ID`; browser/tools can be injected once `agy` exposes an MCP config flag. | +| Smith | Built in. | Uses the same tool set without an external MCP process. | +| Claude Code | Enabled by default when `construct-mcp` is available. | Adapter writes a config under `CONSTRUCT_STATE_DIR` and passes `--mcp-config `. | +| Codex | Enabled by default when `construct-mcp` is available. | Adapter passes Codex a `-c mcp_servers.construct=...` TOML override. | +| Antigravity | Not injected yet. | Receives `CONSTRUCT_SESSION_ID`; browser/tools can be injected once `agy` exposes an MCP config flag. | ## Fleet-control tools @@ -72,7 +72,7 @@ sessions; it should be able to use `agentd_list_sessions` without running the Browser tools emit a `BrowserPreview` event back to the calling session, so the TUI thumbnail updates for MCP-capable harnesses the same way it does for -Zarvis-native browser calls. +smith-native browser calls. ## Memory and session context @@ -86,16 +86,16 @@ belong to. | Variable | Purpose | |---|---| -| `AGENTD_SESSION_ID` | Identifies the calling session, so tools can avoid acting on themselves. | -| `AGENTD_RUNTIME_DIR` / `AGENTD_STATE_DIR` / `AGENTD_DATA_DIR` / `AGENTD_CONFIG_DIR` | Point tools at the same daemon and storage layout as the parent session. | -| `AGENTD_GLOBAL_MEMORY_FILE` / `AGENTD_PROJECT_MEMORY_FILE` / `AGENTD_PROJECT_ID` | Point `agentd_context` at the Markdown memory files for the session. | -| `AGENTD_SESSION_WIDGETS_DIR` | Points agents at the current session's file-backed widget directory. Prefer reading it from `agentd_context` so the agent also sees widget policy and supported Markdown extensions. | +| `CONSTRUCT_SESSION_ID` | Identifies the calling session, so tools can avoid acting on themselves. | +| `CONSTRUCT_RUNTIME_DIR` / `CONSTRUCT_STATE_DIR` / `CONSTRUCT_DATA_DIR` / `CONSTRUCT_CONFIG_DIR` | Point tools at the same daemon and storage layout as the parent session. | +| `CONSTRUCT_GLOBAL_MEMORY_FILE` / `CONSTRUCT_PROJECT_MEMORY_FILE` / `CONSTRUCT_PROJECT_ID` | Point `agentd_context` at the Markdown memory files for the session. | +| `CONSTRUCT_SESSION_WIDGETS_DIR` | Points agents at the current session's file-backed widget directory. Prefer reading it from `agentd_context` so the agent also sees widget policy and supported Markdown extensions. | ## Generative widgets Agents can create session-scoped UI widgets by writing Markdown files into the `session_widgets.dir` returned by `agentd_context`. The same directory is also -available as `AGENTD_SESSION_WIDGETS_DIR`, but `agentd_context` is preferred +available as `CONSTRUCT_SESSION_WIDGETS_DIR`, but `agentd_context` is preferred because it includes widget policy and supported Markdown extensions. See [Generative widgets](generative-widgets.md) for the file format, lifecycle, rendering behavior, and action-link semantics. diff --git a/examples/config.toml b/examples/config.toml index 99ea4707..8f286af3 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -1,39 +1,40 @@ -# Example agentd config. +# Example construct config. # -# Drop this at $AGENTD_CONFIG_DIR/config.toml (default -# ~/.config/agentd/config.toml). All adapter entries here are optional — +# Drop this at $CONSTRUCT_CONFIG_DIR/config.toml (default +# ~/.config/construct/config.toml). All adapter entries here are optional — # `shell`, `claude`, and `codex` are registered automatically. Override the # binary path when the adapter binary is not on $PATH and not next to the -# `agentd` binary. +# `constructd` binary. # [adapters.shell] -# binary = "agentd-adapter-shell" +# binary = "construct-adapter-shell" # description = "Generic shell command runner" # [adapters.claude] -# binary = "/opt/agentd/agentd-adapter-claude" +# binary = "/opt/construct/construct-adapter-claude" # description = "Claude Code" # Override the child command a built-in adapter launches without writing a -# wrapper script. `AGENTD_*_CMD` is split shell-style (quotes/backslashes only; -# no shell expansion) and prepended before the adapter's generated arguments. -# This wins over the older binary-only `AGENTD_*_BIN` env var. +# wrapper script. `CONSTRUCT_*_CMD` is split shell-style (quotes/backslashes +# only; no shell expansion) and prepended before the adapter's generated +# arguments. +# This wins over the binary-only `CONSTRUCT_*_BIN` env var. # # [adapters.codex] -# env = { AGENTD_CODEX_CMD = "exec codex" } +# env = { CONSTRUCT_CODEX_CMD = "exec codex" } # -# Other built-ins use AGENTD_CLAUDE_CMD, AGENTD_ANTIGRAVITY_CMD, and -# AGENTD_SHELL_CMD. +# Other built-ins use CONSTRUCT_CLAUDE_CMD, CONSTRUCT_ANTIGRAVITY_CMD, and +# CONSTRUCT_SHELL_CMD. # Add a custom adapter — just point to any binary that speaks the AHP # protocol. See `crates/protocol/src/adapter.rs` for the SDK helper. # # [adapters.aider] -# binary = "agentd-adapter-aider" +# binary = "construct-adapter-aider" # description = "Aider (community adapter)" [defaults] # When true, every new session whose cwd is a git repo gets its own # `git worktree add`-style isolated copy. Override per-session with -# `agent new ... --worktree`. +# `construct new ... --worktree`. worktree = false diff --git a/install.sh b/install.sh index c9ae82ca..7881262c 100755 --- a/install.sh +++ b/install.sh @@ -1,5 +1,5 @@ #!/bin/sh -# agentd installer. +# construct installer. # # curl -fsSL https://raw.githubusercontent.com/zarvis-ai/agentd/main/install.sh | sh # @@ -9,17 +9,17 @@ # everything must live together — this script keeps them together. # # Environment overrides: -# AGENTD_VERSION release tag to install (e.g. v0.2.0). Default: latest. -# AGENTD_BIN_DIR install directory. Default: $HOME/.local/bin. -# AGENTD_BASE_URL download base URL (mirror / testing). Default: the -# GitHub release for AGENTD_VERSION. The script fetches -# /agentd-.tar.gz and /SHA256SUMS. +# CONSTRUCT_VERSION release tag to install (e.g. v0.2.0). Default: latest. +# CONSTRUCT_BIN_DIR install directory. Default: $HOME/.local/bin. +# CONSTRUCT_BASE_URL download base URL (mirror / testing). Default: the +# GitHub release for CONSTRUCT_VERSION. The script fetches +# /constructd-.tar.gz and /SHA256SUMS. set -eu REPO="zarvis-ai/agentd" -VERSION="${AGENTD_VERSION:-latest}" -BIN_DIR="${AGENTD_BIN_DIR:-$HOME/.local/bin}" -BINS="agent agentd agentd-mcp agentd-adapter-shell agentd-adapter-claude agentd-adapter-codex agentd-adapter-antigravity agentd-adapter-zarvis" +VERSION="${CONSTRUCT_VERSION:-latest}" +BIN_DIR="${CONSTRUCT_BIN_DIR:-$HOME/.local/bin}" +BINS="construct constructd construct-mcp construct-adapter-shell construct-adapter-claude construct-adapter-codex construct-adapter-antigravity construct-adapter-smith" say() { printf '%s\n' "$*"; } err() { printf 'error: %s\n' "$*" >&2; exit 1; } @@ -61,9 +61,9 @@ else fi # --- resolve URLs --------------------------------------------------------- -asset="agentd-${target}.tar.gz" -if [ -n "${AGENTD_BASE_URL:-}" ]; then - base="${AGENTD_BASE_URL%/}" +asset="constructd-${target}.tar.gz" +if [ -n "${CONSTRUCT_BASE_URL:-}" ]; then + base="${CONSTRUCT_BASE_URL%/}" elif [ "$VERSION" = "latest" ]; then base="https://github.com/${REPO}/releases/latest/download" else @@ -73,7 +73,7 @@ fi tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT -say "Installing agentd ($VERSION) for $target" +say "Installing constructd ($VERSION) for $target" if ! fetch "${base}/${asset}" "${tmp}/${asset}"; then err "could not download ${base}/${asset} @@ -83,7 +83,7 @@ fi fetch "${base}/SHA256SUMS" "${tmp}/SHA256SUMS" || err "could not download SHA256SUMS" # --- verify checksum ------------------------------------------------------ -want="$(grep " ${asset}\$" "${tmp}/SHA256SUMS" | awk '{print $1}')" +want="$(grep " ${asset}$" "${tmp}/SHA256SUMS" | awk '{print $1}')" [ -n "$want" ] || err "no checksum for ${asset} in SHA256SUMS" got="$(sha256 "${tmp}/${asset}")" [ "$want" = "$got" ] || err "checksum mismatch for ${asset} @@ -93,8 +93,8 @@ say "Checksum OK" # --- install -------------------------------------------------------------- tar -xzf "${tmp}/${asset}" -C "$tmp" -src="${tmp}/agentd-${target}" -[ -d "$src" ] || err "unexpected archive layout (no agentd-${target}/ inside ${asset})" +src="${tmp}/constructd-${target}" +[ -d "$src" ] || err "unexpected archive layout (no constructd-${target}/ inside ${asset})" mkdir -p "$BIN_DIR" # Validate the whole set before touching anything (all-or-nothing-ish). @@ -128,4 +128,4 @@ case ":${PATH}:" in esac say "" -say "Done. Try: agentd run (in another terminal) agent" +say "Done. Try: constructd run (in another terminal) construct" diff --git a/scripts/smoke.sh b/scripts/smoke.sh index 5671cce3..118ca7ed 100755 --- a/scripts/smoke.sh +++ b/scripts/smoke.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Quick end-to-end smoke test against a freshly built workspace. # -# Spins up the daemon under an isolated $AGENTD_*_DIR sandbox, exercises the +# Spins up the daemon under an isolated $CONSTRUCT_*_DIR sandbox, exercises the # IPC surface (ping / harnesses / create / list / show / send / stop), and # tears down. Run from the workspace root: # @@ -10,43 +10,43 @@ set -euo pipefail ROOT=$(cd "$(dirname "$0")/.." && pwd) -SANDBOX=${AGENTD_SMOKE_DIR:-/tmp/agentd-smoke} +SANDBOX=${CONSTRUCT_SMOKE_DIR:-/tmp/construct-smoke} rm -rf "$SANDBOX" mkdir -p "$SANDBOX"/{state,data,config,runtime} -export AGENTD_STATE_DIR="$SANDBOX/state" -export AGENTD_DATA_DIR="$SANDBOX/data" -export AGENTD_CONFIG_DIR="$SANDBOX/config" -export AGENTD_RUNTIME_DIR="$SANDBOX/runtime" +export CONSTRUCT_STATE_DIR="$SANDBOX/state" +export CONSTRUCT_DATA_DIR="$SANDBOX/data" +export CONSTRUCT_CONFIG_DIR="$SANDBOX/config" +export CONSTRUCT_RUNTIME_DIR="$SANDBOX/runtime" -AGENTD="$ROOT/target/debug/agentd" -AGENT="$ROOT/target/debug/agent" -[ -x "$AGENTD" ] || { echo "build first: cargo build --workspace" >&2; exit 1; } -[ -x "$AGENT" ] || { echo "build first: cargo build --workspace" >&2; exit 1; } +CONSTRUCTD="$ROOT/target/debug/constructd" +CONSTRUCT_CLI="$ROOT/target/debug/construct" +[ -x "$CONSTRUCTD" ] || { echo "build first: cargo build --workspace" >&2; exit 1; } +[ -x "$CONSTRUCT_CLI" ] || { echo "build first: cargo build --workspace" >&2; exit 1; } -"$AGENTD" run >"$SANDBOX/daemon.log" 2>&1 & +"$CONSTRUCTD" run >"$SANDBOX/daemon.log" 2>&1 & DAEMON_PID=$! trap 'kill $DAEMON_PID 2>/dev/null || true' EXIT sleep 0.4 echo "==> ping" -"$AGENT" ping +"$CONSTRUCT_CLI" ping echo "==> harnesses" -"$AGENT" harnesses +"$CONSTRUCT_CLI" harnesses echo "==> shell session" -SID=$("$AGENT" new shell "echo hello-from-shell; echo and-another-line" --cwd "$SANDBOX") +SID=$("$CONSTRUCT_CLI" new shell "echo hello-from-shell; echo and-another-line" --cwd "$SANDBOX") echo " session: $SID" sleep 0.6 echo "==> list" -"$AGENT" list +"$CONSTRUCT_CLI" list echo "==> show" -"$AGENT" show "$SID" +"$CONSTRUCT_CLI" show "$SID" echo "==> stop (idempotent on done sessions)" -"$AGENT" stop "$SID" 2>/dev/null || true +"$CONSTRUCT_CLI" stop "$SID" 2>/dev/null || true echo "OK" diff --git a/scripts/test_agent.sh b/scripts/test_agent.sh index cc88429d..65343d1a 100755 --- a/scripts/test_agent.sh +++ b/scripts/test_agent.sh @@ -4,29 +4,29 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WT="$(cd "$SCRIPT_DIR/.." && pwd)" BIN_DIR="$WT/target/debug" -AGENT_BIN="$BIN_DIR/agent" +CLIENT_BIN="$BIN_DIR/construct" -if [[ ! -x "$AGENT_BIN" ]]; then - echo "missing $AGENT_BIN; run: cargo build" >&2 +if [[ ! -x "$CLIENT_BIN" ]]; then + echo "missing $CLIENT_BIN; run: cargo build" >&2 exit 1 fi -if [[ -n "${AGENTD_TEST_DIR:-}" ]]; then - DEMO_DIR="$AGENTD_TEST_DIR" +if [[ -n "${CONSTRUCT_TEST_DIR:-}" ]]; then + DEMO_DIR="$CONSTRUCT_TEST_DIR" else WT_NAME="$(basename "$WT")" SAFE_WT_NAME="$(printf '%s' "$WT_NAME" | tr -c 'A-Za-z0-9_.-' '-')" - DEMO_DIR="/tmp/agentd-test-${SAFE_WT_NAME}" + DEMO_DIR="/tmp/construct-test-${SAFE_WT_NAME}" fi -export AGENTD_RUNTIME_DIR="$DEMO_DIR/run" -export AGENTD_STATE_DIR="$DEMO_DIR/state" -export AGENTD_DATA_DIR="$DEMO_DIR/data" -export AGENTD_CONFIG_DIR="$DEMO_DIR/config" -export AGENTD_SHELL_BIN="${AGENTD_SHELL_BIN:-/bin/bash}" +export CONSTRUCT_RUNTIME_DIR="$DEMO_DIR/run" +export CONSTRUCT_STATE_DIR="$DEMO_DIR/state" +export CONSTRUCT_DATA_DIR="$DEMO_DIR/data" +export CONSTRUCT_CONFIG_DIR="$DEMO_DIR/config" +export CONSTRUCT_SHELL_BIN="${CONSTRUCT_SHELL_BIN:-/bin/bash}" export PATH="$BIN_DIR:$PATH" -if ! "$AGENT_BIN" ping >/dev/null 2>&1; then +if ! "$CLIENT_BIN" ping >/dev/null 2>&1; then "$SCRIPT_DIR/test_agentd.sh" >/dev/null fi -exec "$AGENT_BIN" "$@" +exec "$CLIENT_BIN" "$@" diff --git a/scripts/test_agentd.sh b/scripts/test_agentd.sh index 75405e1a..abd938b2 100755 --- a/scripts/test_agentd.sh +++ b/scripts/test_agentd.sh @@ -4,52 +4,53 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" WT="$(cd "$SCRIPT_DIR/.." && pwd)" BIN_DIR="$WT/target/debug" -AGENTD_BIN="$BIN_DIR/agentd" -AGENT_BIN="$BIN_DIR/agent" +CLIENT_BIN="$BIN_DIR/construct" +CONSTRUCTD_BIN="$BIN_DIR/constructd" -if [[ ! -x "$AGENTD_BIN" || ! -x "$AGENT_BIN" ]]; then +if [[ ! -x "$CONSTRUCTD_BIN" || ! -x "$CLIENT_BIN" ]]; then echo "missing $BIN_DIR binaries; run: cargo build" >&2 exit 1 fi -if [[ -n "${AGENTD_TEST_DIR:-}" ]]; then - DEMO_DIR="$AGENTD_TEST_DIR" +if [[ -n "${CONSTRUCT_TEST_DIR:-}" ]]; then + DEMO_DIR="$CONSTRUCT_TEST_DIR" else WT_NAME="$(basename "$WT")" SAFE_WT_NAME="$(printf '%s' "$WT_NAME" | tr -c 'A-Za-z0-9_.-' '-')" - DEMO_DIR="/tmp/agentd-test-${SAFE_WT_NAME}" + DEMO_DIR="/tmp/construct-test-${SAFE_WT_NAME}" fi -export AGENTD_RUNTIME_DIR="$DEMO_DIR/run" -export AGENTD_STATE_DIR="$DEMO_DIR/state" -export AGENTD_DATA_DIR="$DEMO_DIR/data" -export AGENTD_CONFIG_DIR="$DEMO_DIR/config" -export AGENTD_SHELL_BIN="${AGENTD_SHELL_BIN:-/bin/bash}" +export CONSTRUCT_RUNTIME_DIR="$DEMO_DIR/run" +export CONSTRUCT_STATE_DIR="$DEMO_DIR/state" +export CONSTRUCT_DATA_DIR="$DEMO_DIR/data" +export CONSTRUCT_CONFIG_DIR="$DEMO_DIR/config" +export CONSTRUCT_SHELL_BIN="${CONSTRUCT_SHELL_BIN:-/bin/bash}" +export CONSTRUCT_REMOTE_NO_TUNNEL=1 export PATH="$BIN_DIR:$PATH" -LOG="$DEMO_DIR/agentd.log" -PID_FILE="$DEMO_DIR/agentd.pid" +LOG="$DEMO_DIR/construct.log" +PID_FILE="$DEMO_DIR/construct.pid" SEED_FILE="$DEMO_DIR/.seeded" seed_sessions() { - if [[ "${AGENTD_TEST_SEED:-1}" == "0" || -e "$SEED_FILE" ]]; then + if [[ "${CONSTRUCT_TEST_SEED:-1}" == "0" || -e "$SEED_FILE" ]]; then return fi echo "seeding sessions..." local sid - sid="$($AGENT_BIN new --title "welcome shell" shell "" | tr -d '[:space:]')" + sid="$($CLIENT_BIN new --title "welcome shell" shell "" | tr -d '[:space:]')" if [[ -n "$sid" ]]; then - $AGENT_BIN send "$sid" "printf 'Welcome to isolated agentd test env\\nworktree: %s\\n\\n' '$WT'; pwd; ls scripts; echo ready" >/dev/null || true + $CLIENT_BIN send "$sid" "printf 'Welcome to isolated construct test env\\nworktree: %s\\n\\n' '$WT'; pwd; ls scripts; echo ready" >/dev/null || true fi - sid="$($AGENT_BIN new --title "activity shell" shell "" | tr -d '[:space:]')" + sid="$($CLIENT_BIN new --title "activity shell" shell "" | tr -d '[:space:]')" if [[ -n "$sid" ]]; then - $AGENT_BIN send "$sid" 'for i in 1 2 3 4; do echo "[$i] reading files, running checks"; sleep 0.5; done; echo "activity complete"' >/dev/null || true + $CLIENT_BIN send "$sid" 'for i in 1 2 3 4; do echo "[$i] reading files, running checks"; sleep 0.5; done; echo "activity complete"' >/dev/null || true fi - sid="$($AGENT_BIN new --title "browser preview prompt" --mode headless zarvis "Use browser_open to open https://example.com with preview true, then summarize what changed in the UI." | tr -d '[:space:]' || true)" + sid="$($CLIENT_BIN new --title "browser preview prompt" --mode headless smith "Use browser_open to open https://example.com with preview true, then summarize what changed in the UI." | tr -d '[:space:]' || true)" if [[ -n "$sid" ]]; then : fi @@ -57,41 +58,41 @@ seed_sessions() { touch "$SEED_FILE" } -if [[ -S "$AGENTD_RUNTIME_DIR/agentd.sock" ]] && "$AGENT_BIN" ping >/dev/null 2>&1; then +if [[ -S "$CONSTRUCT_RUNTIME_DIR/construct.sock" ]] && "$CLIENT_BIN" ping >/dev/null 2>&1; then seed_sessions - echo "agentd already running for test env" + echo "construct already running for test env" echo "dir: $DEMO_DIR" echo "pid: $(cat "$PID_FILE" 2>/dev/null || echo unknown)" echo "log: $LOG" exit 0 fi -if [[ "${AGENTD_TEST_KEEP:-0}" != "1" ]]; then +if [[ "${CONSTRUCT_TEST_KEEP:-0}" != "1" ]]; then rm -rf "$DEMO_DIR" fi mkdir -p "$DEMO_DIR/run" "$DEMO_DIR/state" "$DEMO_DIR/data" "$DEMO_DIR/config" -"$AGENTD_BIN" run >"$LOG" 2>&1 & +"$CONSTRUCTD_BIN" run >"$LOG" 2>&1 & PID=$! echo "$PID" >"$PID_FILE" for _ in $(seq 1 100); do - if "$AGENT_BIN" ping >/dev/null 2>&1; then + if "$CLIENT_BIN" ping >/dev/null 2>&1; then seed_sessions - echo "agentd pid=$PID" + echo "construct pid=$PID" echo "dir: $DEMO_DIR" echo "log: $LOG" - echo "export AGENTD_TEST_DIR=$DEMO_DIR" + echo "export CONSTRUCT_TEST_DIR=$DEMO_DIR" exit 0 fi if ! kill -0 "$PID" 2>/dev/null; then - echo "agentd exited while starting; log follows:" >&2 + echo "construct exited while starting; log follows:" >&2 cat "$LOG" >&2 || true exit 1 fi sleep 0.2 done -echo "agentd did not become ready; log follows:" >&2 +echo "construct did not become ready; log follows:" >&2 cat "$LOG" >&2 || true exit 1