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