Skip to main content

tau_cli/
lib.rs

1//! CLI entrypoint for tau: starts a harness daemon and connects as a
2//! socket client for interactive chat.
3//!
4//! Component and local-execution boundaries are summarized in `ARCH-tau-cli`.
5//! Transcript presentation follows `SPEC-tau-cli-transcript-styling`.
6
7use tau_config::settings as path_tau_config_settings;
8
9pub mod cli;
10
11mod action_commands;
12mod agent_activity;
13mod agent_navigation;
14mod chat;
15mod daemon;
16mod dev_tmux;
17mod estimated_cost;
18mod event_renderer;
19mod kill_session;
20mod line_output;
21mod list_agents;
22mod list_sessions;
23mod markdown_render;
24mod message_fact_render;
25mod papercut;
26mod peer_exit;
27mod preview_declarations;
28mod print_prompt;
29mod print_tools;
30mod prompt_history;
31mod prompt_stdin;
32mod provider_quota;
33mod render_request;
34mod renderer_handle;
35mod send;
36mod settings_registry;
37mod skill_commands;
38mod terminal_text;
39mod theme;
40mod tool_render;
41mod transcript_markers;
42mod turn_stats_projection;
43mod ui_client;
44mod ui_commands;
45mod ui_events;
46mod ui_logging;
47mod ui_prompt;
48mod unload_agent;
49mod watch_activity;
50
51use std::process::ExitCode;
52use std::sync::{Mutex, MutexGuard};
53use std::{fmt, io};
54
55use tau_harness::SessionLaunchStatus;
56
57use crate::chat::run_chat;
58use crate::daemon::resolve_resume_session_id;
59
60/// Single shared message for mutex-poison panics: every mutex in this
61/// crate is held only for short, infallible critical sections, so poison
62/// means another thread panicked mid-update and continuing is unsafe.
63pub(crate) const MUTEX_POISONED: &str = "mutex poisoned";
64
65/// Locks `mutex` and panics on poison. Centralizes the panic message so
66/// individual call sites read as `let mut g = locked(&m);` instead of
67/// repeating `.expect("... mutex poisoned")` everywhere.
68pub(crate) fn locked<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
69    mutex.lock().expect(MUTEX_POISONED)
70}
71
72mod built_info;
73
74const STARTUP_PUNS: &[&str] = &[
75    "Tau is like Pi, but twice as much.",
76    "A whole new angle on coding agents.",
77    "Tau day is every day if you care about circles enough.",
78    "Come for the agent, stay for the circumference discourse.",
79    "Tau is the irrational choice for rational Unix hackers.",
80    "Small tools, loosely joined — that’s the Tau of Unix.",
81    "In Tau, what goes around comes around over stdio.",
82    "We’ve come full τurn.",
83    "Tau keeps the loop tight and the pipes honest.",
84    "Every extension gets its turn in Tau.",
85    "Tau speaks fluent stdio with a circular accent.",
86    "Agents, tools, sockets, loops: a well-rounded lineup.",
87    "Ready, set, Tau!",
88    "Tau day to code.",
89    "Tau-tau control.",
90    "Tau-tally operational.",
91    "Tau much power in one terminal.",
92    "Tau infinity and beyond.",
93    "Tau the line between human and agent.",
94    "Tau’s what I’m talking about.",
95    "One shell to Tau them all.",
96    "Tau-powered, Unix-native.",
97    "Complete revolution.",
98    "Wrapping around nicely.",
99    "Continuous on S¹, probably.",
100    "Cohomology remains left as exercise.",
101];
102
103pub(crate) fn random_startup_pun() -> &'static str {
104    use rand::Rng;
105    let idx = rand::thread_rng().gen_range(0..STARTUP_PUNS.len());
106    STARTUP_PUNS[idx]
107}
108
109// ---------------------------------------------------------------------------
110// Error type
111// ---------------------------------------------------------------------------
112
113/// Errors returned by the CLI.
114#[derive(Debug)]
115pub enum CliError {
116    /// Offline cache analysis produced useful output with missing evidence.
117    CachePartial,
118    /// Offline cache invocation or required schema could not be inspected.
119    CacheInvalid,
120    Io(io::Error),
121    Encode(tau_proto::EncodeError),
122    Harness(tau_harness::HarnessError),
123    Inspect(tau_session_inspect::InspectError),
124    DaemonExited(String),
125    NoRunningDaemon,
126    Participant(String),
127    PromptStdin(PromptStdinError),
128    SessionNotFound(String),
129    /// The attachment stopped because terminal foreground ownership is
130    /// unconfirmed.
131    ForegroundOwnershipUnconfirmed {
132        /// Complete failure message retained for non-terminal callers.
133        message: String,
134        /// Bounded private restoration diagnostic written by persistent UIs.
135        diagnostic: tau_cli_term::ForegroundRestorationDiagnostic,
136    },
137    /// The attachment stopped after its first reported terminal output failure.
138    TerminalOutputFailed(String),
139}
140
141impl fmt::Display for CliError {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        match self {
144            Self::CachePartial => f.write_str("cache analysis is partial; see report coverage"),
145            Self::CacheInvalid => f.write_str("cache source unavailable, unsupported, or invalid; use a matching-build inspector and check the selected scope and limits"),
146            Self::Io(source) => write!(f, "I/O error: {source}"),
147            Self::Encode(source) => write!(f, "encode error: {source}"),
148            Self::Harness(source) => write!(f, "harness error: {source}"),
149            Self::Inspect(source) => write!(f, "inspect error: {source}"),
150            Self::DaemonExited(msg) => write!(f, "harness daemon exited: {msg}"),
151            Self::NoRunningDaemon => {
152                f.write_str("no running session is available; start one with `tau`")
153            }
154            Self::Participant(msg) => write!(f, "participant error: {msg}"),
155            Self::PromptStdin(error) => error.fmt(f),
156            Self::SessionNotFound(id) => write!(f, "session not found: `{id}`"),
157            Self::ForegroundOwnershipUnconfirmed { message, .. } => f.write_str(message),
158            Self::TerminalOutputFailed(message) => f.write_str(message),
159        }
160    }
161}
162
163impl CliError {
164    /// Distinguishes partial offline evidence from invalid invocation and
165    /// ordinary failures.
166    fn exit_code(&self) -> ExitCode {
167        match self {
168            Self::CachePartial => ExitCode::from(3),
169            Self::CacheInvalid => ExitCode::from(2),
170            _ => ExitCode::FAILURE,
171        }
172    }
173
174    /// Returns whether the top-level CLI may write this failure to stderr.
175    fn should_report_to_terminal(&self) -> bool {
176        !matches!(
177            self,
178            Self::ForegroundOwnershipUnconfirmed { .. } | Self::TerminalOutputFailed(_)
179        )
180    }
181}
182
183/// Terminal failure while admitting or executing a one-shot stdin prompt.
184#[derive(Debug)]
185pub enum PromptStdinError {
186    /// The harness did not acknowledge create admission before its deadline.
187    AdmissionTimeout {
188        /// Admission deadline that elapsed.
189        timeout: std::time::Duration,
190    },
191    /// The harness rejected the correlated create request.
192    Rejected {
193        /// Stable protocol rejection category.
194        reason: tau_proto::UiCreateAgentRejection,
195        /// Bounded harness-authored diagnostic.
196        message: String,
197    },
198    /// The accepted initial prompt failed after create admission completed.
199    PromptFailed {
200        /// Lifecycle stage that failed.
201        stage: tau_proto::AgentPromptFailureStage,
202        /// Bounded harness-authored diagnostic.
203        message: String,
204    },
205    /// The correlated provider execution reached an unsuccessful terminal.
206    ExecutionFailed {
207        /// Provider-authored or harness-classified user-facing diagnostic.
208        message: String,
209    },
210}
211
212impl fmt::Display for PromptStdinError {
213    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        match self {
215            Self::AdmissionTimeout { timeout } => write!(
216                f,
217                "timed out after {}s waiting for create-agent admission",
218                timeout.as_secs()
219            ),
220            Self::Rejected { reason, message } => {
221                write!(f, "create-agent request failed ({}): {message}", reason)
222            }
223            Self::PromptFailed { stage, message } => {
224                write!(f, "initial prompt failed ({}): {message}", stage)
225            }
226            Self::ExecutionFailed { message } => {
227                write!(f, "initial prompt failed (execution): {message}")
228            }
229        }
230    }
231}
232
233impl std::error::Error for PromptStdinError {}
234impl std::error::Error for CliError {}
235
236impl From<io::Error> for CliError {
237    fn from(source: io::Error) -> Self {
238        Self::Io(source)
239    }
240}
241
242impl From<tau_harness::HarnessError> for CliError {
243    fn from(source: tau_harness::HarnessError) -> Self {
244        Self::Harness(source)
245    }
246}
247
248impl From<tau_session_inspect::InspectError> for CliError {
249    fn from(source: tau_session_inspect::InspectError) -> Self {
250        Self::Inspect(source)
251    }
252}
253
254// ---------------------------------------------------------------------------
255// Build labels and version helpers (shared by chat banner, EventRenderer
256// banner, and `tau --version`).
257// ---------------------------------------------------------------------------
258
259fn run_harness_component_default() -> Result<(), Box<dyn std::error::Error>> {
260    run_harness_component(false)
261}
262
263fn run_harness_component(initial_ui_stdio: bool) -> Result<(), Box<dyn std::error::Error>> {
264    if initial_ui_stdio {
265        tau_harness::run_component_with_internal_tools_and_initial_ui_stdio(
266            tau_harness_tools::builtin_handlers(),
267        )
268    } else {
269        tau_harness::run_component_with_internal_tools(tau_harness_tools::builtin_handlers())
270    }
271}
272
273fn build_revision() -> String {
274    tau_harness::version::build_revision()
275}
276
277fn build_last_modified() -> Option<String> {
278    // Fall back to the locally-formatted `built` timestamp when the
279    // harness can't produce a date (e.g. a `cargo build` outside of
280    // Nix where the date placeholder is unpatched and the harness'
281    // `built` snapshot only has the RFC2822 string).
282    tau_harness::version::build_last_modified()
283        .or_else(|| short_built_time(built_info::BUILT_TIME_UTC))
284        .filter(|date| date != "1980-01-01 00:00")
285}
286
287fn short_built_time(time: &str) -> Option<String> {
288    let input_format = time::macros::format_description!(
289        "[weekday repr:short], [day padding:none] [month repr:short] [year] [hour]:[minute]:[second] [offset_hour sign:mandatory][offset_minute]"
290    );
291    let output_format = time::macros::format_description!("[year]-[month]-[day] [hour]:[minute]");
292    time::OffsetDateTime::parse(time, input_format)
293        .ok()?
294        .format(output_format)
295        .ok()
296}
297
298pub(crate) fn build_label_parts() -> (String, String) {
299    let version = format!("tau {}", env!("CARGO_PKG_VERSION"));
300    let build = match build_last_modified() {
301        Some(date) => format!("({}, {})", build_revision(), date),
302        None => format!("({})", build_revision()),
303    };
304    (version, build)
305}
306
307fn version_label() -> String {
308    let (version, build) = build_label_parts();
309    format!("{version} {build}")
310}
311
312/// Build the two-line startup banner: logo + name/version/build on the
313/// first line, logo continuation + random pun on the second.
314pub(crate) fn build_banner(theme: &tau_themes::Theme) -> tau_cli_term::StyledText {
315    use tau_themes::names;
316    let logo = tau_cli_term::resolve::resolve(theme, names::BANNER_LOGO);
317    let name = tau_cli_term::resolve::resolve(theme, names::BANNER_NAME);
318    let version_style = tau_cli_term::resolve::resolve(theme, names::BANNER_VERSION);
319    let build_style = tau_cli_term::resolve::resolve(theme, names::BANNER_BUILD);
320    let pun_style = tau_cli_term::resolve::resolve(theme, names::BANNER_PUN);
321    let pun = random_startup_pun();
322    let (version, build) = build_label_parts();
323    tau_cli_term::StyledText::from(vec![
324        tau_cli_term::Span::new("▝▜▛▀ ", logo),
325        tau_cli_term::Span::new("tau", name),
326        tau_cli_term::Span::new(version.trim_start_matches("tau"), version_style),
327        tau_cli_term::Span::new(" ", Default::default()),
328        tau_cli_term::Span::new(build, build_style),
329        tau_cli_term::Span::new("\n", Default::default()),
330        tau_cli_term::Span::new(" ▐▙▖ ", logo),
331        tau_cli_term::Span::new(pun, pun_style),
332    ])
333}
334
335// ---------------------------------------------------------------------------
336// Short-id minting (used for both session ids and per-UI log dir ids)
337// ---------------------------------------------------------------------------
338
339/// Build an id of the form `<prefix>-<6 base36 chars>`. Used for both
340/// session and UI ids so the visual shape is consistent.
341pub(crate) fn mint_short_id(prefix: &str) -> String {
342    use rand::distributions::Distribution;
343
344    struct Base36;
345    impl Distribution<char> for Base36 {
346        fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> char {
347            let n: u8 = rng.gen_range(0..36);
348            if n < 10 {
349                (b'0' + n) as char
350            } else {
351                (b'a' + (n - 10)) as char
352            }
353        }
354    }
355
356    let suffix: String = Base36.sample_iter(rand::thread_rng()).take(6).collect();
357    format!("{prefix}-{suffix}")
358}
359
360// ---------------------------------------------------------------------------
361// `tau init`
362// ---------------------------------------------------------------------------
363
364const SAMPLE_CLI: &str = include_str!("../release-resources/cli.yaml");
365const SAMPLE_HARNESS: &str = include_str!("../release-resources/harness.yaml");
366
367fn run_init(force: bool) -> Result<(), CliError> {
368    let Some(dir) = tau_config::settings::config_dir() else {
369        return Err(CliError::Io(io::Error::new(
370            io::ErrorKind::NotFound,
371            "could not determine config directory",
372        )));
373    };
374    std::fs::create_dir_all(&dir)?;
375
376    let files = [("cli.yaml", SAMPLE_CLI), ("harness.yaml", SAMPLE_HARNESS)];
377
378    for (name, content) in &files {
379        let path = dir.join(name);
380        if path.exists() && !force {
381            eprintln!(
382                "skip: {} (exists, use --force to overwrite)",
383                path.display()
384            );
385        } else {
386            std::fs::write(&path, content)?;
387            eprintln!("wrote: {}", path.display());
388        }
389    }
390
391    eprintln!("next: use `tau provider add` to log in to a hosted LLM provider");
392
393    Ok(())
394}
395
396// ---------------------------------------------------------------------------
397// Entrypoint
398// ---------------------------------------------------------------------------
399
400pub type ComponentRunner = fn() -> Result<(), Box<dyn std::error::Error>>;
401
402fn parse_role_cli_overrides<I, S>(args: I) -> Vec<tau_config::settings::RoleCliOverride>
403where
404    I: IntoIterator<Item = S>,
405    S: Into<std::ffi::OsString>,
406{
407    let mut overrides = Vec::new();
408    let mut args = args.into_iter().map(Into::into);
409    let _program = args.next();
410    while let Some(arg) = args.next() {
411        let arg = arg.to_string_lossy();
412        if arg == "--" {
413            break;
414        }
415        if arg == "--disable-roles-all" {
416            overrides.push(path_tau_config_settings::RoleCliOverride::DisableAll);
417        } else if let Some(role) = arg.strip_prefix("--enable-role=") {
418            overrides.push(path_tau_config_settings::RoleCliOverride::Enable(
419                role.to_owned(),
420            ));
421        } else if arg == "--enable-role" {
422            if let Some(role) = args.next() {
423                overrides.push(path_tau_config_settings::RoleCliOverride::Enable(
424                    role.to_string_lossy().into_owned(),
425                ));
426            }
427        } else if let Some(role) = arg.strip_prefix("--disable-role=") {
428            overrides.push(path_tau_config_settings::RoleCliOverride::Disable(
429                role.to_owned(),
430            ));
431        } else if arg == "--disable-role"
432            && let Some(role) = args.next()
433        {
434            overrides.push(path_tau_config_settings::RoleCliOverride::Disable(
435                role.to_string_lossy().into_owned(),
436            ));
437        }
438    }
439    overrides
440}
441
442fn reject_harness_config_overrides(
443    overrides: &[tau_config::settings::HarnessConfigCliOverride],
444    command: &str,
445) -> Result<(), CliError> {
446    if overrides.is_empty() {
447        return Ok(());
448    }
449    Err(CliError::Participant(format!(
450        "--harness-config can only be used when starting a new harness instance; `{command}` cannot apply it to an existing or absent harness"
451    )))
452}
453
454/// Tracks which public startup-only alias inputs the invocation supplied.
455#[derive(Clone, Copy, Default)]
456struct ModelReferenceAliasInputPresence {
457    /// Whether `--provider-alias` was supplied.
458    provider_flag: bool,
459    /// Whether `--model-alias` was supplied.
460    model_flag: bool,
461    /// Whether `TAU_PROVIDER_ALIASES` was present.
462    provider_environment: bool,
463    /// Whether `TAU_MODEL_ALIASES` was present.
464    model_environment: bool,
465}
466
467/// Rejects startup-only alias sources without misreporting them as generic
468/// `--harness-config` input.
469fn reject_model_reference_alias_inputs(
470    presence: ModelReferenceAliasInputPresence,
471    command: &str,
472) -> Result<(), CliError> {
473    let source = if presence.provider_flag {
474        "--provider-alias"
475    } else if presence.model_flag {
476        "--model-alias"
477    } else if presence.provider_environment {
478        tau_config::settings::TAU_PROVIDER_ALIASES_ENV
479    } else if presence.model_environment {
480        tau_config::settings::TAU_MODEL_ALIASES_ENV
481    } else {
482        return Ok(());
483    };
484    Err(CliError::Participant(format!(
485        "{source} can only be used when starting a new harness instance; `{command}` cannot apply it to an existing or absent harness"
486    )))
487}
488
489fn validate_agent_trace_mode(
490    format: cli::AgentTraceFormat,
491    mode: cli::AgentTraceMode,
492) -> Result<(), CliError> {
493    if mode == cli::AgentTraceMode::Full
494        && !matches!(
495            format,
496            cli::AgentTraceFormat::AgentToolsJsonl | cli::AgentTraceFormat::AgentToolsToon
497        )
498    {
499        return Err(CliError::Participant(
500            "`agent trace --mode full` requires `--format agent-tools-jsonl` or \
501             `--format agent-tools-toon`"
502                .to_owned(),
503        ));
504    }
505    Ok(())
506}
507
508fn reject_dev_tmux_startup_overrides(
509    profile: Option<&str>,
510    startup_role: Option<&str>,
511    role_cli_overrides: &[tau_config::settings::RoleCliOverride],
512    extension_cli_overrides: &[tau_config::settings::ExtensionCliOverride],
513    harness_config_overrides: &[tau_config::settings::HarnessConfigCliOverride],
514) -> Result<(), CliError> {
515    if profile.is_some() {
516        return Err(CliError::Participant(
517            "`tau dev tmux` cannot use a configuration profile because the outer helper must not load normal user harness config before spawning the scratch Tau".to_owned(),
518        ));
519    }
520    if startup_role.is_some() {
521        return Err(CliError::Participant(
522            "`tau dev tmux` cannot use --role because the outer helper must not load normal user harness config before spawning the scratch Tau".to_owned(),
523        ));
524    }
525    if !role_cli_overrides.is_empty() {
526        return Err(CliError::Participant(
527            "`tau dev tmux` cannot use role enable/disable overrides because the outer helper must not load normal user harness config before spawning the scratch Tau".to_owned(),
528        ));
529    }
530    if !extension_cli_overrides.is_empty() {
531        return Err(CliError::Participant(
532            "`tau dev tmux` cannot use extension enable/disable overrides because the outer helper must not load normal user harness config before spawning the scratch Tau".to_owned(),
533        ));
534    }
535    reject_harness_config_overrides(harness_config_overrides, "dev tmux")
536}
537
538fn reject_legacy_config_path(config: Option<&std::path::Path>) -> Result<(), CliError> {
539    if let Some(path) = config {
540        return Err(CliError::Participant(format!(
541            "--config is no longer supported (got `{}`); use harness.yaml or --harness-config KEY=VALUE overrides",
542            path.display()
543        )));
544    }
545    Ok(())
546}
547
548fn reject_attach_startup_overrides(
549    prompt_stdin: bool,
550    profile_selected: bool,
551    startup_role: Option<&str>,
552    role_cli_overrides: &[tau_config::settings::RoleCliOverride],
553    extension_cli_overrides: &[tau_config::settings::ExtensionCliOverride],
554) -> Result<(), CliError> {
555    if profile_selected {
556        return Err(CliError::Participant(
557            "`tau attach` cannot apply --profile to an already-running daemon".to_owned(),
558        ));
559    }
560    if startup_role.is_some() && !prompt_stdin {
561        return Err(CliError::Participant(
562            "`tau attach` cannot apply --role to an already-running interactive daemon; use --prompt-stdin if you want --role for the submitted prompt".to_owned(),
563        ));
564    }
565    if !role_cli_overrides.is_empty() {
566        return Err(CliError::Participant(
567            "`tau attach` cannot apply role enable/disable overrides to an already-running daemon"
568                .to_owned(),
569        ));
570    }
571    if !extension_cli_overrides.is_empty() {
572        return Err(CliError::Participant(
573            "`tau attach` cannot apply extension enable/disable overrides to an already-running daemon"
574                .to_owned(),
575        ));
576    }
577    Ok(())
578}
579
580fn reject_ephemeral_incompatible(
581    ephemeral: bool,
582    startup_mode: &StartupMode,
583) -> Result<(), CliError> {
584    if ephemeral {
585        let command = match startup_mode {
586            StartupMode::New => return Ok(()),
587            StartupMode::Attach(_) => "attach",
588            StartupMode::Resume(_) => "resume",
589        };
590        return Err(CliError::Participant(format!(
591            "--ephemeral cannot be combined with `tau {command}`"
592        )));
593    }
594    Ok(())
595}
596
597fn required_harness_role<'a>(role: Option<&'a str>, command: &str) -> Result<&'a str, CliError> {
598    role.ok_or_else(|| CliError::Participant(format!("tau dev {command} requires --role <role>")))
599}
600
601fn parse_extension_cli_overrides<I, S>(args: I) -> Vec<tau_config::settings::ExtensionCliOverride>
602where
603    I: IntoIterator<Item = S>,
604    S: Into<std::ffi::OsString>,
605{
606    let mut overrides = Vec::new();
607    let mut args = args.into_iter().map(Into::into);
608    let _program = args.next();
609    while let Some(arg) = args.next() {
610        let arg = arg.to_string_lossy();
611        if arg == "--" {
612            break;
613        }
614        if arg == "--enable-extensions-all" {
615            overrides.push(path_tau_config_settings::ExtensionCliOverride::EnableAll);
616        } else if arg == "--disable-extensions-all" {
617            overrides.push(path_tau_config_settings::ExtensionCliOverride::DisableAll);
618        } else if let Some(extension) = arg.strip_prefix("--enable-extension=") {
619            overrides.push(path_tau_config_settings::ExtensionCliOverride::Enable(
620                extension.to_owned(),
621            ));
622        } else if arg == "--enable-extension" {
623            if let Some(extension) = args.next() {
624                overrides.push(path_tau_config_settings::ExtensionCliOverride::Enable(
625                    extension.to_string_lossy().into_owned(),
626                ));
627            }
628        } else if let Some(extension) = arg.strip_prefix("--disable-extension=") {
629            overrides.push(path_tau_config_settings::ExtensionCliOverride::Disable(
630                extension.to_owned(),
631            ));
632        } else if arg == "--disable-extension"
633            && let Some(extension) = args.next()
634        {
635            overrides.push(path_tau_config_settings::ExtensionCliOverride::Disable(
636                extension.to_string_lossy().into_owned(),
637            ));
638        }
639    }
640    overrides
641}
642
643fn parse_harness_config_cli_overrides<I, S>(
644    args: I,
645) -> Result<Vec<tau_config::settings::HarnessConfigCliOverride>, String>
646where
647    I: IntoIterator<Item = S>,
648    S: Into<std::ffi::OsString>,
649{
650    let mut overrides = Vec::new();
651    let mut args = args.into_iter().map(Into::into);
652    let _program = args.next();
653    for arg in args {
654        let arg = arg.to_string_lossy();
655        if arg == "--" {
656            break;
657        }
658        if let Some(value) = arg.strip_prefix("--harness-config=") {
659            overrides.push(value.parse()?);
660        }
661    }
662    Ok(overrides)
663}
664
665/// Describes how a bundled component gets its global tracing subscriber.
666#[derive(Clone, Copy, Debug, Eq, PartialEq)]
667pub enum ComponentLogging {
668    /// `tau-cli` installs a stderr subscriber before invoking the component.
669    CliStderr,
670    /// The component installs its own subscriber, or does not emit tracing
671    /// logs.
672    RunnerManaged,
673}
674
675/// Target semantics selected by the public session-startup command.
676#[derive(Clone, Debug, Eq, PartialEq)]
677enum StartupMode {
678    /// Mint and own a fresh session.
679    New,
680    /// Attach to an existing live session, selecting interactively when absent.
681    Attach(Option<tau_proto::SessionId>),
682    /// Resume persisted state, selecting interactively when absent.
683    Resume(Option<tau_proto::SessionId>),
684}
685
686/// A startup request or one of the non-startup CLI commands.
687enum DispatchCommand {
688    Startup {
689        args: cli::RunArgs,
690        mode: StartupMode,
691    },
692    Other(cli::Command),
693}
694
695/// Whether dispatch must accept a fresh effective harness settings snapshot.
696///
697/// Commands that only inspect local state or connect to an existing daemon must
698/// remain usable when configuration changes after that daemon's accepted
699/// startup.
700fn consumes_harness_settings(command: &DispatchCommand) -> bool {
701    match command {
702        DispatchCommand::Startup {
703            mode: StartupMode::Attach(_),
704            ..
705        } => false,
706        DispatchCommand::Startup { .. } => true,
707        DispatchCommand::Other(cli::Command::Dev {
708            command:
709                cli::DevCommand::DumpInitialPrompt { .. }
710                | cli::DevCommand::PrintPrompt { .. }
711                | cli::DevCommand::PrintSystemPrompt
712                | cli::DevCommand::PrintTools
713                | cli::DevCommand::PreviewDeclarations,
714        }) => true,
715        DispatchCommand::Other(cli::Command::Component { name, .. }) => name == "harness",
716        DispatchCommand::Other(cli::Command::Serve { .. }) => true,
717        _ => false,
718    }
719}
720
721/// Returns whether a dispatch path must reject all public alias inputs even if
722/// another internal validation path happens to inspect harness settings.
723fn rejects_model_reference_alias_inputs(command: &DispatchCommand) -> bool {
724    !consumes_harness_settings(command)
725        || matches!(
726            command,
727            DispatchCommand::Other(cli::Command::Dev {
728                command: cli::DevCommand::DumpInitialPrompt { .. }
729            })
730        )
731}
732
733pub struct Component {
734    /// Name accepted by the `tau component <name>` dispatcher.
735    pub name: &'static str,
736    /// Function that runs the component over stdin/stdout.
737    pub runner: ComponentRunner,
738    /// Owner of the component's tracing initialization.
739    pub logging: ComponentLogging,
740}
741
742/// Parses CLI arguments via clap and dispatches to the appropriate
743/// command.
744pub fn main_with_args() -> std::process::ExitCode {
745    main_with_args_and_components(&[])
746}
747
748/// Parses CLI arguments via clap and dispatches to the appropriate
749/// command, using caller-provided component registrations for
750/// `component` dispatch.
751pub fn main_with_args_and_components(components: &[Component]) -> std::process::ExitCode {
752    tau_ext_provider_builtin::initialize_cache_diagnostic_build(build_revision());
753    use std::process::ExitCode;
754
755    use clap::{CommandFactory, FromArgMatches};
756
757    let run = || -> Result<(), CliError> {
758        let role_cli_overrides = parse_role_cli_overrides(std::env::args_os());
759        let extension_cli_overrides = parse_extension_cli_overrides(std::env::args_os());
760        let mut harness_config_overrides = parse_harness_config_cli_overrides(std::env::args_os())
761            .map_err(CliError::Participant)?;
762        let clap_command = cli::Cli::command();
763        let clap_matches = clap_command.get_matches();
764        let profile_was_explicitly_selected = matches!(
765            clap_matches.value_source("profile"),
766            Some(clap::parser::ValueSource::CommandLine)
767        );
768        let cli::Cli {
769            version,
770            harness,
771            run,
772            command,
773        } = cli::Cli::from_arg_matches(&clap_matches)
774            .expect("arguments accepted by Cli::command must parse as Cli");
775        let provider_alias_environment =
776            std::env::var_os(tau_config::settings::TAU_PROVIDER_ALIASES_ENV);
777        let model_alias_environment = std::env::var_os(tau_config::settings::TAU_MODEL_ALIASES_ENV);
778        let alias_input_presence = ModelReferenceAliasInputPresence {
779            provider_flag: !harness.provider_alias.is_empty(),
780            model_flag: !harness.model_alias.is_empty(),
781            provider_environment: provider_alias_environment.is_some(),
782            model_environment: model_alias_environment.is_some(),
783        };
784        if version {
785            reject_model_reference_alias_inputs(alias_input_presence, "version")?;
786            println!("{}", version_label());
787            return Ok(());
788        }
789        reject_legacy_config_path(run.config.as_deref())?;
790        if matches!(&command, Some(cli::Command::Serve { .. }))
791            && (run.ephemeral || run.prompt_stdin)
792        {
793            return Err(CliError::Participant(
794                "`tau serve` does not accept --ephemeral or --prompt-stdin".to_owned(),
795            ));
796        }
797        let command = match command {
798            Some(cli::Command::Run(args)) => DispatchCommand::Startup {
799                args,
800                mode: StartupMode::New,
801            },
802            Some(cli::Command::Attach { session }) => DispatchCommand::Startup {
803                args: run,
804                mode: StartupMode::Attach(session),
805            },
806            Some(cli::Command::Resume { session }) => DispatchCommand::Startup {
807                args: run,
808                mode: StartupMode::Resume(session),
809            },
810            Some(command) => DispatchCommand::Other(command),
811            None => DispatchCommand::Startup {
812                args: run,
813                mode: StartupMode::New,
814            },
815        };
816        let direct_harness_component = matches!(
817            &command,
818            DispatchCommand::Other(cli::Command::Component { name, .. }) if name == "harness"
819        );
820        let attaching = matches!(
821            &command,
822            DispatchCommand::Startup {
823                mode: StartupMode::Attach(_),
824                ..
825            }
826        );
827        let dump_initial_prompt = matches!(
828            &command,
829            DispatchCommand::Other(cli::Command::Dev {
830                command: cli::DevCommand::DumpInitialPrompt { .. }
831            })
832        );
833        if rejects_model_reference_alias_inputs(&command) || direct_harness_component {
834            reject_model_reference_alias_inputs(
835                ModelReferenceAliasInputPresence {
836                    provider_environment: !direct_harness_component
837                        && !attaching
838                        && alias_input_presence.provider_environment,
839                    model_environment: !direct_harness_component
840                        && !attaching
841                        && alias_input_presence.model_environment,
842                    ..alias_input_presence
843                },
844                if direct_harness_component {
845                    "component harness"
846                } else if dump_initial_prompt {
847                    "dev dump-initial-prompt"
848                } else {
849                    "this command"
850                },
851            )?;
852        }
853        let alias_config_overrides = tau_config::settings::model_reference_alias_config_overrides(
854            tau_config::settings::ModelReferenceAliasSources {
855                provider_environment: (!direct_harness_component && !attaching)
856                    .then_some(provider_alias_environment)
857                    .flatten(),
858                model_environment: (!direct_harness_component && !attaching)
859                    .then_some(model_alias_environment)
860                    .flatten(),
861                provider_cli: &harness.provider_alias,
862                model_cli: &harness.model_alias,
863            },
864        )
865        .map_err(|error| CliError::Participant(error.to_string()))?;
866        harness_config_overrides.extend(alias_config_overrides);
867        if let DispatchCommand::Startup {
868            args,
869            mode: StartupMode::Attach(_),
870        } = &command
871        {
872            reject_harness_config_overrides(&harness_config_overrides, "attach")?;
873            reject_attach_startup_overrides(
874                args.prompt_stdin,
875                profile_was_explicitly_selected,
876                harness.role.as_deref(),
877                &role_cli_overrides,
878                &extension_cli_overrides,
879            )?;
880        }
881        let profile_was_selected = profile_was_explicitly_selected
882            || std::env::var_os(tau_config::settings::TAU_PROFILE_ENV).is_some();
883        let reads_extension_environment = match &command {
884            DispatchCommand::Startup {
885                mode: StartupMode::New | StartupMode::Resume(_),
886                ..
887            } => true,
888            DispatchCommand::Other(cli::Command::Dev {
889                command:
890                    cli::DevCommand::PrintPrompt { .. }
891                    | cli::DevCommand::PrintSystemPrompt
892                    | cli::DevCommand::PrintTools
893                    | cli::DevCommand::PreviewDeclarations
894                    | cli::DevCommand::Tmux { .. },
895            }) => true,
896            DispatchCommand::Other(cli::Command::Component { name, .. }) => name == "harness",
897            DispatchCommand::Other(cli::Command::Serve { .. }) => true,
898            _ => false,
899        };
900        let environment_extension_names = if reads_extension_environment {
901            tau_config::settings::parse_enable_extensions_env(std::env::var_os(
902                tau_config::settings::TAU_ENABLE_EXTENSIONS_ENV,
903            ))
904            .map_err(|error| CliError::Participant(error.to_string()))?
905        } else {
906            Vec::new()
907        };
908        if reads_extension_environment {
909            tau_config::settings::parse_tau_state_access_env(std::env::var_os(
910                tau_config::settings::TAU_EXTENSION_TAU_STATE_ACCESS_ENV,
911            ))
912            .map_err(|error| CliError::Participant(error.to_string()))?;
913        }
914        match &command {
915            DispatchCommand::Startup { .. }
916            | DispatchCommand::Other(cli::Command::Dev {
917                command:
918                    cli::DevCommand::PrintPrompt { .. }
919                    | cli::DevCommand::PrintSystemPrompt
920                    | cli::DevCommand::PrintTools
921                    | cli::DevCommand::PreviewDeclarations,
922            }) => {}
923            DispatchCommand::Other(cli::Command::Session { command }) => {
924                let command_name = match command {
925                    cli::SessionCommand::List(_) => "session list",
926                    cli::SessionCommand::Kill { .. } => "session kill",
927                    cli::SessionCommand::Show { .. } => "session show",
928                    cli::SessionCommand::Stats { .. } => "session stats",
929                    cli::SessionCommand::Cache(_) => "session cache",
930                };
931                reject_harness_config_overrides(&harness_config_overrides, command_name)?;
932            }
933            DispatchCommand::Other(cli::Command::Agent { command }) => {
934                let command_name = match command {
935                    cli::AgentCommand::List(_) => "agent list",
936                    cli::AgentCommand::Unload(_) => "agent unload",
937                    cli::AgentCommand::Trace(_) => "agent trace",
938                    cli::AgentCommand::Cache(_) => "agent cache",
939                    cli::AgentCommand::Export(_) => "agent export",
940                };
941                reject_harness_config_overrides(&harness_config_overrides, command_name)?;
942            }
943            DispatchCommand::Other(cli::Command::Init { .. }) => {
944                reject_harness_config_overrides(&harness_config_overrides, "init")?;
945            }
946            DispatchCommand::Other(cli::Command::Provider { .. }) => {
947                reject_harness_config_overrides(&harness_config_overrides, "provider")?;
948            }
949            DispatchCommand::Other(cli::Command::Dev {
950                command: cli::DevCommand::Send { .. },
951            }) => {
952                reject_harness_config_overrides(&harness_config_overrides, "dev send")?;
953            }
954            DispatchCommand::Other(cli::Command::Dev {
955                command: cli::DevCommand::Papercut { .. },
956            }) => {
957                reject_harness_config_overrides(&harness_config_overrides, "dev papercut")?;
958            }
959            DispatchCommand::Other(cli::Command::Dev {
960                command: cli::DevCommand::Tmux { .. },
961            }) => {
962                reject_dev_tmux_startup_overrides(
963                    profile_was_selected.then_some("selected"),
964                    harness.role.as_deref(),
965                    &role_cli_overrides,
966                    &extension_cli_overrides,
967                    &harness_config_overrides,
968                )?;
969            }
970            DispatchCommand::Other(cli::Command::Dev {
971                command: cli::DevCommand::DumpInitialPrompt { .. },
972            }) => {
973                reject_harness_config_overrides(
974                    &harness_config_overrides,
975                    "dev dump-initial-prompt",
976                )?;
977            }
978            DispatchCommand::Other(cli::Command::Component { .. }) => {
979                reject_harness_config_overrides(&harness_config_overrides, "component")?;
980            }
981            DispatchCommand::Other(cli::Command::Serve { .. }) => {}
982            DispatchCommand::Other(cli::Command::Run(_))
983            | DispatchCommand::Other(cli::Command::Attach { .. })
984            | DispatchCommand::Other(cli::Command::Resume { .. }) => {
985                unreachable!("startup variants normalize to DispatchCommand::Startup")
986            }
987        }
988
989        if let DispatchCommand::Other(cli::Command::Dev {
990            command: cli::DevCommand::Tmux { command },
991        }) = command
992        {
993            return dev_tmux::run(command);
994        }
995
996        // Declaration inspection owns a config-only loader. Do not route it
997        // through ordinary validators that also consult state-owned settings.
998        if matches!(
999            &command,
1000            DispatchCommand::Other(cli::Command::Dev {
1001                command: cli::DevCommand::PreviewDeclarations,
1002            })
1003        ) {
1004            return preview_declarations::run(
1005                harness.profile.as_deref(),
1006                &role_cli_overrides,
1007                &extension_cli_overrides,
1008                &environment_extension_names,
1009                &harness_config_overrides,
1010            );
1011        }
1012        let consumes_harness_settings = consumes_harness_settings(&command);
1013        let selected_profile = if consumes_harness_settings {
1014            tau_config::settings::selected_profile(harness.profile.as_deref())
1015                .map_err(|error| CliError::Participant(error.to_string()))?
1016        } else {
1017            None
1018        };
1019        if consumes_harness_settings {
1020            tau_harness::validate_cli_overrides_with_profile(
1021                selected_profile.as_ref(),
1022                &role_cli_overrides,
1023                &extension_cli_overrides,
1024                &harness_config_overrides,
1025            )
1026            .map_err(|error| CliError::Participant(error.to_string()))?;
1027        }
1028        if reads_extension_environment && consumes_harness_settings {
1029            tau_harness::validate_extension_environment_and_cli_overrides_with_profile(
1030                selected_profile.as_ref(),
1031                &environment_extension_names,
1032                &extension_cli_overrides,
1033                &role_cli_overrides,
1034                &harness_config_overrides,
1035            )
1036            .map_err(|error| CliError::Participant(error.to_string()))?;
1037        }
1038        match command {
1039            DispatchCommand::Startup {
1040                args:
1041                    cli::RunArgs {
1042                        config,
1043                        prompt_stdin,
1044                        ephemeral,
1045                    },
1046                mode: startup_mode,
1047            } => {
1048                reject_legacy_config_path(config.as_deref())?;
1049                reject_ephemeral_incompatible(ephemeral, &startup_mode)?;
1050                let (session_id, session_status) = match &startup_mode {
1051                    StartupMode::Attach(session) => (
1052                        crate::daemon::resolve_attach_session_id(session.as_ref())?,
1053                        SessionLaunchStatus::Resumed,
1054                    ),
1055                    StartupMode::Resume(session) => (
1056                        resolve_resume_session_id(session.as_ref())?,
1057                        SessionLaunchStatus::Resumed,
1058                    ),
1059                    StartupMode::New => (
1060                        crate::daemon::mint_session_id(&std::env::current_dir()?),
1061                        SessionLaunchStatus::New,
1062                    ),
1063                };
1064                let attach = matches!(startup_mode, StartupMode::Attach(_));
1065                if prompt_stdin {
1066                    prompt_stdin::run_prompt_stdin(
1067                        &session_id,
1068                        attach,
1069                        session_status,
1070                        harness.role.as_deref(),
1071                        crate::daemon::DaemonCliOverrides {
1072                            profile: selected_profile.as_ref(),
1073                            role: &role_cli_overrides,
1074                            extension: &extension_cli_overrides,
1075                            extension_environment: None,
1076                            harness_config: &harness_config_overrides,
1077                            memory_only_agent_store: false,
1078                        },
1079                        ephemeral,
1080                    )
1081                } else {
1082                    run_chat(
1083                        &session_id,
1084                        attach,
1085                        session_status,
1086                        harness.role.as_deref(),
1087                        crate::daemon::DaemonCliOverrides {
1088                            profile: selected_profile.as_ref(),
1089                            role: &role_cli_overrides,
1090                            extension: &extension_cli_overrides,
1091                            extension_environment: None,
1092                            harness_config: &harness_config_overrides,
1093                            memory_only_agent_store: false,
1094                        },
1095                        ephemeral,
1096                    )
1097                }
1098            }
1099
1100            DispatchCommand::Other(cli::Command::Session {
1101                command: cli::SessionCommand::Kill { session_id },
1102            }) => {
1103                reject_harness_config_overrides(&harness_config_overrides, "session kill")?;
1104                kill_session::run(&session_id)
1105            }
1106
1107            DispatchCommand::Other(cli::Command::Session {
1108                command: cli::SessionCommand::List(args),
1109            }) => {
1110                reject_harness_config_overrides(&harness_config_overrides, "session list")?;
1111                list_sessions::run(&args)
1112            }
1113
1114            DispatchCommand::Other(cli::Command::Session {
1115                command:
1116                    cli::SessionCommand::Show {
1117                        session_id,
1118                        sessions_dir,
1119                    },
1120            }) => {
1121                reject_harness_config_overrides(&harness_config_overrides, "session show")?;
1122                for line in tau_session_inspect::session_lines(sessions_dir, &session_id)? {
1123                    println!("{line}");
1124                }
1125                Ok(())
1126            }
1127
1128            DispatchCommand::Other(cli::Command::Session {
1129                command:
1130                    cli::SessionCommand::Stats {
1131                        session,
1132                        sessions_dir,
1133                    },
1134            }) => {
1135                reject_harness_config_overrides(&harness_config_overrides, "session stats")?;
1136                let stats = tau_session_inspect::read_session_stats(&sessions_dir, &session)?
1137                    .ok_or_else(|| {
1138                        CliError::Participant(format!("session `{session}` not found"))
1139                    })?;
1140                let output = serde_toon::to_string(&stats).map_err(|error| {
1141                    CliError::Participant(format!("failed to serialize session stats: {error}"))
1142                })?;
1143                println!("{output}");
1144                Ok(())
1145            }
1146
1147            DispatchCommand::Other(cli::Command::Agent {
1148                command: cli::AgentCommand::List(args),
1149            }) => {
1150                reject_harness_config_overrides(&harness_config_overrides, "agent list")?;
1151                list_agents::run(&args)
1152            }
1153            DispatchCommand::Other(cli::Command::Agent {
1154                command: cli::AgentCommand::Unload(args),
1155            }) => {
1156                reject_harness_config_overrides(&harness_config_overrides, "agent unload")?;
1157                unload_agent::run(&args)
1158            }
1159            DispatchCommand::Other(cli::Command::Agent {
1160                command: cli::AgentCommand::Cache(args),
1161            }) => run_cache_report(
1162                args.options,
1163                tau_session_inspect::CacheScope::Agent {
1164                    agent_id: args.agent_id,
1165                    include_descendants: args.include_descendants,
1166                },
1167            ),
1168            DispatchCommand::Other(cli::Command::Agent {
1169                command:
1170                    cli::AgentCommand::Export(cli::AgentExportArgs {
1171                        command: cli::AgentExportCommand::Chat(args),
1172                    }),
1173            }) => {
1174                reject_harness_config_overrides(&harness_config_overrides, "agent export chat")?;
1175                let chat = tau_session_inspect::load_agent_chat(&args.agents_dir, &args.agent_id)?;
1176                let output = if args.toons {
1177                    chat.to_toon()?
1178                } else {
1179                    chat.to_markdown()
1180                };
1181                line_output::stream_stdout(|writer| writer.write_all(output.as_bytes()))
1182            }
1183            DispatchCommand::Other(cli::Command::Session {
1184                command: cli::SessionCommand::Cache(args),
1185            }) => run_cache_report(
1186                args.options,
1187                tau_session_inspect::CacheScope::Session(args.session_id),
1188            ),
1189            DispatchCommand::Other(cli::Command::Agent {
1190                command: cli::AgentCommand::Trace(args),
1191            }) => {
1192                reject_harness_config_overrides(&harness_config_overrides, "agent trace")?;
1193                let mode = match args.mode {
1194                    cli::AgentTraceMode::Lite => tau_session_inspect::AgentTraceMode::Lite,
1195                    cli::AgentTraceMode::Full => tau_session_inspect::AgentTraceMode::Full,
1196                };
1197                let format = match args.format {
1198                    cli::AgentTraceFormat::TauJsonl => {
1199                        tau_session_inspect::AgentTraceFormat::TauJsonl
1200                    }
1201                    cli::AgentTraceFormat::OtlpJson => {
1202                        tau_session_inspect::AgentTraceFormat::OtlpJson
1203                    }
1204                    cli::AgentTraceFormat::AgentToolsJsonl => {
1205                        tau_session_inspect::AgentTraceFormat::AgentToolsJsonl(mode)
1206                    }
1207                    cli::AgentTraceFormat::AgentToolsToon => {
1208                        tau_session_inspect::AgentTraceFormat::AgentToolsToon(mode)
1209                    }
1210                    cli::AgentTraceFormat::AgentPerformanceJsonl => {
1211                        tau_session_inspect::AgentTraceFormat::AgentPerformanceJsonl
1212                    }
1213                };
1214                validate_agent_trace_mode(args.format, args.mode)?;
1215                let mut output = tau_session_inspect::prepare_agent_trace(
1216                    &args.agents_dir,
1217                    &args.agent_id,
1218                    if args.include_descendants {
1219                        tau_session_inspect::DescendantSelection::Include
1220                    } else {
1221                        tau_session_inspect::DescendantSelection::RootOnly
1222                    },
1223                    format,
1224                )?;
1225                line_output::stream_stdout(|writer| output.copy_to(writer).map(|_| ()))
1226            }
1227
1228            DispatchCommand::Other(cli::Command::Init { force }) => {
1229                reject_harness_config_overrides(&harness_config_overrides, "init")?;
1230                run_init(force)
1231            }
1232
1233            DispatchCommand::Other(cli::Command::Provider { args }) => {
1234                reject_harness_config_overrides(&harness_config_overrides, "provider")?;
1235                tau_ext_provider_builtin::run_provider_cli(&args)
1236                    .map_err(|e| CliError::Participant(e.to_string()))
1237            }
1238
1239            DispatchCommand::Other(cli::Command::Dev { command }) => match command {
1240                cli::DevCommand::Send { session_id, line } => {
1241                    reject_harness_config_overrides(&harness_config_overrides, "dev send")?;
1242                    send::run_send(&session_id, &line.join(" "))
1243                }
1244                cli::DevCommand::DumpInitialPrompt { out, message } => {
1245                    reject_harness_config_overrides(
1246                        &harness_config_overrides,
1247                        "dev dump-initial-prompt",
1248                    )?;
1249                    tau_harness::dump_initial_prompt(&out, &message)?;
1250                    println!("wrote {}", out.display());
1251                    Ok(())
1252                }
1253                cli::DevCommand::PrintPrompt { enable_agents_md } => {
1254                    print_prompt::run_print_prompt(
1255                        harness.role.as_deref(),
1256                        enable_agents_md,
1257                        selected_profile.as_ref(),
1258                        &role_cli_overrides,
1259                        &extension_cli_overrides,
1260                        &environment_extension_names,
1261                        &harness_config_overrides,
1262                    )
1263                }
1264                cli::DevCommand::PrintSystemPrompt => {
1265                    let role =
1266                        required_harness_role(harness.role.as_deref(), "print-system-prompt")?;
1267                    print_prompt::run_print_system_prompt(
1268                        role,
1269                        selected_profile.as_ref(),
1270                        &role_cli_overrides,
1271                        &extension_cli_overrides,
1272                        &environment_extension_names,
1273                        &harness_config_overrides,
1274                    )
1275                }
1276                cli::DevCommand::PrintTools => print_tools::run_print_tools(
1277                    harness.role.as_deref(),
1278                    selected_profile.as_ref(),
1279                    &role_cli_overrides,
1280                    &extension_cli_overrides,
1281                    &environment_extension_names,
1282                    &harness_config_overrides,
1283                ),
1284                cli::DevCommand::Papercut { command } => papercut::run(command),
1285                cli::DevCommand::PreviewDeclarations => {
1286                    unreachable!("declaration inspection returns before runtime validation")
1287                }
1288                cli::DevCommand::Tmux { command } => {
1289                    let _ = command;
1290                    unreachable!("dev tmux dispatch returns before harness config validation")
1291                }
1292            },
1293
1294            DispatchCommand::Other(cli::Command::Component {
1295                name,
1296                initial_ui_stdio,
1297            }) => {
1298                reject_harness_config_overrides(&harness_config_overrides, "component")?;
1299                if harness.profile.is_some() {
1300                    return Err(CliError::Participant(
1301                        "`tau component` cannot apply --profile in-process; set TAU_PROFILE before starting the component"
1302                            .to_owned(),
1303                    ));
1304                }
1305                if initial_ui_stdio && name != "harness" {
1306                    return Err(CliError::Participant(
1307                        "--initial-ui-stdio is only valid for `tau component harness`".to_owned(),
1308                    ));
1309                }
1310                if name == "harness" && initial_ui_stdio {
1311                    ui_logging::init_stderr_from_env("tau_harness=info,tau_cli=info,warn");
1312                    return run_harness_component(true)
1313                        .map_err(|e| CliError::Participant(e.to_string()));
1314                }
1315                if name == "harness" {
1316                    ui_logging::init_stderr_from_env("tau_harness=info,tau_cli=info,warn");
1317                    return tau_harness::run_component_with_internal_tools_and_extension_cli_overrides(
1318                        tau_harness_tools::builtin_handlers(),
1319                        extension_cli_overrides.clone(),
1320                    )
1321                    .map_err(|error| CliError::Participant(error.to_string()));
1322                }
1323                let built_in_components = [Component {
1324                    name: "harness",
1325                    runner: run_harness_component_default,
1326                    logging: ComponentLogging::CliStderr,
1327                }];
1328                let component = built_in_components
1329                    .iter()
1330                    .chain(components)
1331                    .find(|component| component.name == name)
1332                    .ok_or_else(|| {
1333                        let available = built_in_components
1334                            .iter()
1335                            .chain(components)
1336                            .map(|component| component.name)
1337                            .collect::<Vec<_>>()
1338                            .join(", ");
1339                        CliError::Participant(format!(
1340                            "unknown component: {name}\navailable: {available}"
1341                        ))
1342                    })?;
1343                match component.logging {
1344                    ComponentLogging::CliStderr => {
1345                        ui_logging::init_stderr_from_env("tau_harness=info,tau_cli=info,warn")
1346                    }
1347                    ComponentLogging::RunnerManaged => {}
1348                }
1349                (component.runner)().map_err(|e| CliError::Participant(e.to_string()))
1350            }
1351            DispatchCommand::Other(cli::Command::Serve {
1352                session,
1353                create,
1354                existing,
1355                create_or_existing: _,
1356                bootstrap_prompt_file,
1357                bootstrap_id,
1358                mirror_extension_stderr,
1359            }) => {
1360                ui_logging::init_stderr_from_env("tau_harness=info,tau_cli=info,warn");
1361                let options = tau_harness::FixedSessionServeOptions {
1362                    session_id: &session,
1363                    profile_selection: selected_profile.as_ref(),
1364                    startup_role: harness.role.as_deref(),
1365                    environment_extension_names: &environment_extension_names,
1366                    extension_cli_overrides: &extension_cli_overrides,
1367                    role_cli_overrides: &role_cli_overrides,
1368                    harness_config_overrides: &harness_config_overrides,
1369                    internal_tool_handlers: tau_harness_tools::builtin_handlers(),
1370                    bootstrap: bootstrap_prompt_file
1371                        .as_deref()
1372                        .zip(bootstrap_id.as_ref())
1373                        .map(|(prompt_file, id)| tau_harness::BootstrapPromptOptions {
1374                            prompt_file,
1375                            id,
1376                        }),
1377                    mirror_extension_stderr,
1378                };
1379                if create {
1380                    tau_harness::run_create_session_component_with_internal_tools(options)
1381                } else if existing {
1382                    tau_harness::run_existing_session_component_with_internal_tools(options)
1383                } else {
1384                    tau_harness::run_create_or_existing_session_component_with_internal_tools(
1385                        options,
1386                    )
1387                }
1388                .map_err(|error| CliError::Participant(error.to_string()))
1389            }
1390            DispatchCommand::Other(
1391                cli::Command::Run(_) | cli::Command::Attach { .. } | cli::Command::Resume { .. },
1392            ) => {
1393                unreachable!("startup variants normalize to DispatchCommand::Startup")
1394            }
1395        }
1396    };
1397
1398    match run() {
1399        Ok(()) => ExitCode::SUCCESS,
1400        Err(error) => {
1401            if error.should_report_to_terminal() {
1402                eprintln!("error: {error}");
1403            }
1404            error.exit_code()
1405        }
1406    }
1407}
1408
1409/// Runs the dependency-light cache inspector without connecting to a daemon.
1410fn run_cache_report(
1411    args: cli::CacheArgs,
1412    scope: tau_session_inspect::CacheScope,
1413) -> Result<(), CliError> {
1414    let options = tau_session_inspect::CacheOptions {
1415        state_dir: args.state_dir,
1416        scope,
1417        prompt: args.prompt,
1418        selection: tau_session_inspect::CacheSelection {
1419            since_unix_micros: args.since,
1420            until_unix_micros: args.until,
1421            model: args.model,
1422            operation: args.operation.map(|operation| match operation {
1423                cli::CacheOperation::Inference => tau_session_inspect::CacheOperation::Inference,
1424                cli::CacheOperation::StandaloneCompaction => {
1425                    tau_session_inspect::CacheOperation::StandaloneCompaction
1426                }
1427                cli::CacheOperation::CacheRefresh => {
1428                    tau_session_inspect::CacheOperation::CacheRefresh
1429                }
1430            }),
1431            attempt: args.attempt,
1432            require_exact_chain: args.require_exact_chain,
1433            group_by: args
1434                .group_by
1435                .into_iter()
1436                .map(|group| match group {
1437                    cli::CacheGroup::Model => tau_session_inspect::CacheGroup::Model,
1438                    cli::CacheGroup::Backend => tau_session_inspect::CacheGroup::Backend,
1439                    cli::CacheGroup::Controls => tau_session_inspect::CacheGroup::Controls,
1440                })
1441                .collect(),
1442        },
1443        view: match args.view {
1444            cli::CacheView::Summary => tau_session_inspect::CacheView::Summary,
1445            cli::CacheView::Attribution => tau_session_inspect::CacheView::Attribution,
1446            cli::CacheView::Continuity => tau_session_inspect::CacheView::Continuity,
1447            cli::CacheView::Geometry => tau_session_inspect::CacheView::Geometry,
1448            cli::CacheView::Gaps => tau_session_inspect::CacheView::Gaps,
1449        },
1450        limits: tau_session_inspect::CacheScanLimits {
1451            compressed_file_bytes: args.max_compressed_bytes,
1452            decompressed_file_bytes: args.max_decompressed_bytes,
1453            total_decompressed_bytes: args.max_total_bytes,
1454            working_memory_bytes: args.max_memory_bytes,
1455        },
1456        producer_build: build_revision(),
1457        index: args.index,
1458    };
1459    let report =
1460        tau_session_inspect::read_cache_report(&options).map_err(|_| CliError::CacheInvalid)?;
1461    line_output::stream_stdout(|writer| match (args.format, args.view) {
1462        (cli::CacheFormat::Summary, cli::CacheView::Summary) => report.write_summary(writer),
1463        (cli::CacheFormat::Summary, _) | (cli::CacheFormat::Jsonl, _) => report.write_jsonl(writer),
1464    })?;
1465    if report.index_written() {
1466        eprintln!(
1467            "Wrote a private disposable cache index; it contains linkable evidence and must be deleted manually when no longer needed."
1468        );
1469    }
1470    if report.is_partial() {
1471        Err(CliError::CachePartial)
1472    } else {
1473        Ok(())
1474    }
1475}
1476
1477// ---------------------------------------------------------------------------
1478// Tests
1479// ---------------------------------------------------------------------------
1480
1481#[cfg(test)]
1482mod test_support;
1483#[cfg(test)]
1484mod tests;