Skip to main content

tau_cli/
cli.rs

1use std::path::{Path, PathBuf};
2
3use clap::{Args, Parser, Subcommand, ValueEnum};
4use tau_proto::SessionId;
5use tau_session_inspect::{
6    default_agents_dir, default_session_id, default_sessions_dir, default_state_dir,
7};
8
9#[cfg(test)]
10mod tests;
11
12#[derive(Parser)]
13#[command(
14    name = "tau",
15    about = "Unix-native LLM agent harness",
16    disable_version_flag = true
17)]
18pub struct Cli {
19    /// Print version, build revision, and build date.
20    #[arg(short = 'V', long = "version", global = true)]
21    pub version: bool,
22
23    #[command(flatten)]
24    pub harness: HarnessArgs,
25
26    #[command(flatten)]
27    pub run: RunArgs,
28
29    #[command(subcommand)]
30    pub command: Option<Command>,
31}
32
33#[derive(Args)]
34pub struct HarnessArgs {
35    #[command(flatten)]
36    pub role_overrides: RoleOverrideArgs,
37
38    #[command(flatten)]
39    pub extension_overrides: ExtensionOverrideArgs,
40
41    /// Select the startup/rendered role.
42    #[arg(short = 'r', long = "role")]
43    pub role: Option<String>,
44
45    /// Select comma-separated configuration profiles before CLI overrides.
46    #[arg(long = "profile", value_name = "PROFILE")]
47    pub profile: Option<String>,
48
49    /// Override one harness config key after all config files are loaded.
50    #[arg(
51        long = "harness-config",
52        value_name = "KEY=VALUE",
53        require_equals = true
54    )]
55    pub harness_config: Vec<tau_config::settings::HarnessConfigCliOverride>,
56
57    /// Override one provider alias for this harness startup.
58    ///
59    /// `TAU_PROVIDER_ALIASES` supplies a JSON object of lower-precedence
60    /// environment overrides.
61    #[arg(long = "provider-alias", value_name = "FROM=TO")]
62    pub provider_alias: Vec<tau_config::settings::ProviderAliasCliOverride>,
63
64    /// Override one exact model-name alias for this harness startup.
65    ///
66    /// `TAU_MODEL_ALIASES` supplies a JSON object of lower-precedence
67    /// environment overrides.
68    #[arg(long = "model-alias", value_name = "FROM=TO")]
69    pub model_alias: Vec<tau_config::settings::ModelAliasCliOverride>,
70}
71
72#[derive(Args)]
73pub struct RoleOverrideArgs {
74    /// Enable a configured role after all config files are loaded.
75    #[arg(long = "enable-role")]
76    pub enable_role: Vec<String>,
77
78    /// Disable a configured role after all config files are loaded.
79    #[arg(long = "disable-role")]
80    pub disable_role: Vec<String>,
81
82    /// Disable every configured role before later CLI role overrides.
83    #[arg(long = "disable-roles-all", action = clap::ArgAction::Count)]
84    pub disable_roles_all: u8,
85}
86
87#[derive(Args)]
88pub struct ExtensionOverrideArgs {
89    /// Enable non-test configured extensions before later CLI extension
90    /// overrides. The built-in test-dummy fixture still requires explicit
91    /// `--enable-extension test-dummy`.
92    #[arg(long = "enable-extensions-all", action = clap::ArgAction::Count)]
93    pub enable_extensions_all: u8,
94
95    /// Disable every configured extension before later CLI extension overrides.
96    #[arg(long = "disable-extensions-all", action = clap::ArgAction::Count)]
97    pub disable_extensions_all: u8,
98
99    /// Enable a configured extension after all config files are loaded.
100    ///
101    /// `TAU_ENABLE_EXTENSIONS=NAME[,NAME...]` additively enables exact
102    /// configured names before ordered CLI overrides. Space/tab around
103    /// names is allowed; malformed or unknown names fail startup. CLI
104    /// enable/disable flags win.
105    #[arg(long = "enable-extension")]
106    pub enable_extension: Vec<String>,
107
108    /// Disable a configured extension after all config files are loaded.
109    #[arg(long = "disable-extension")]
110    pub disable_extension: Vec<String>,
111}
112
113#[derive(Args)]
114/// Options shared by new, attach, and resume session startup.
115pub struct RunArgs {
116    /// Deprecated legacy extension config path; use `--harness-config`
117    /// overrides instead.
118    #[arg(long, hide = true)]
119    pub config: Option<PathBuf>,
120
121    /// Read one prompt from stdin, submit it, print final output, and exit.
122    ///
123    /// Answers go to stdout; reasoning, headers, and errors go to stderr.
124    /// Each destination's terminal state is checked independently, and dynamic
125    /// bodies are sanitized only when that destination is a terminal. Pipes and
126    /// files retain semantic UTF-8 bytes and existing framing.
127    #[arg(long = "prompt-stdin")]
128    pub prompt_stdin: bool,
129
130    /// Run without writing session membership, session metadata, session debug
131    /// events, per-session logs, session-scoped extension data, or the terminal
132    /// UI log to disk.
133    ///
134    /// Agent transcripts, provider state, credentials, user/cache extension
135    /// data, runtime sockets, and configuration state keep their normal
136    /// persistence behavior.
137    #[arg(long)]
138    pub ephemeral: bool,
139}
140
141#[derive(Subcommand)]
142pub enum Command {
143    /// Run an interactive agent session.
144    ///
145    /// `tau` spawns a new harness daemon and attaches for this process.
146    #[command(hide = true)]
147    Run(RunArgs),
148
149    /// Attach to a running session without taking daemon ownership.
150    Attach {
151        /// Running session id; omit to choose interactively.
152        session: Option<SessionId>,
153    },
154
155    /// Resume a persisted session in a new harness daemon.
156    Resume {
157        /// Persisted session id; omission auto-selects the sole unlocked
158        /// target, otherwise shows the eligible-session picker.
159        session: Option<SessionId>,
160    },
161
162    /// Serve one fixed session in the foreground without an initial UI.
163    Serve {
164        /// Exact session id to create or resume.
165        #[arg(long)]
166        session: SessionId,
167
168        /// Require the session directory to be completely absent, then create
169        /// it.
170        #[arg(
171            long,
172            required_unless_present_any = ["existing", "create_or_existing"],
173            conflicts_with_all = ["existing", "create_or_existing"]
174        )]
175        create: bool,
176
177        /// Require and strictly resume valid existing session state.
178        #[arg(
179            long,
180            required_unless_present_any = ["create", "create_or_existing"],
181            conflicts_with_all = ["create", "create_or_existing"]
182        )]
183        existing: bool,
184
185        /// Resume valid existing state or atomically create an absent session.
186        #[arg(
187            long,
188            required_unless_present_any = ["create", "existing"],
189            conflicts_with_all = ["create", "existing"]
190        )]
191        create_or_existing: bool,
192
193        /// Read one literal bootstrap prompt from this UTF-8 file after
194        /// startup.
195        ///
196        /// `-` reads stdin through EOF once. The paired bootstrap id makes this
197        /// submission durable and at-most-once across restarts.
198        #[arg(
199            long = "bootstrap-prompt-file",
200            value_name = "PATH",
201            requires = "bootstrap_id"
202        )]
203        bootstrap_prompt_file: Option<PathBuf>,
204
205        /// Durable bootstrap generation id.
206        #[arg(
207            long = "bootstrap-id",
208            value_name = "ID",
209            requires = "bootstrap_prompt_file"
210        )]
211        bootstrap_id: Option<tau_harness::BootstrapId>,
212
213        /// Mirror framed, escaped extension stderr to this process's stderr.
214        ///
215        /// Private per-session extension log files remain authoritative. Custom
216        /// extension stderr is unredacted and may reach a wider journal
217        /// audience.
218        #[arg(long)]
219        mirror_extension_stderr: bool,
220    },
221
222    /// Inspect sessions.
223    Session {
224        #[command(subcommand)]
225        command: SessionCommand,
226    },
227
228    /// Inspect agents.
229    Agent {
230        #[command(subcommand)]
231        command: AgentCommand,
232    },
233
234    /// Copy sample config files to ~/.config/tau/
235    Init {
236        /// Overwrite existing config files
237        #[arg(long)]
238        force: bool,
239    },
240
241    /// Manage LLM providers (add, remove, list)
242    Provider {
243        /// Subcommand and arguments (e.g. `add`, `remove <name>`, `list`)
244        #[arg(trailing_var_arg = true)]
245        args: Vec<String>,
246    },
247
248    /// Developer-only commands.
249    #[command(hide = true, hide_possible_values = true)]
250    Dev {
251        #[command(subcommand)]
252        command: DevCommand,
253    },
254
255    /// Run a bundled Tau component as a standalone process.
256    ///
257    /// Bundled extensions are components too, but not every component is an
258    /// extension; for example, the harness is a component.
259    Component {
260        /// Component name (harness or a bundled extension such as ext-shell,
261        /// ext-provider-builtin, ext-websearch, ext-rhai,
262        /// ext-std-notifications, or ext-test-dummy)
263        name: String,
264
265        /// Use stdin/stdout as the initial UI connection before starting
266        /// harness extensions. Only valid with `tau component harness`.
267        #[arg(long, hide = true)]
268        initial_ui_stdio: bool,
269    },
270}
271
272#[derive(Subcommand)]
273pub enum SessionCommand {
274    /// Inspect durable cache accounting and private capture coverage offline.
275    Cache(SessionCacheArgs),
276    /// Gracefully shut down one exact running session.
277    Kill {
278        /// Exact session identifier to shut down
279        session_id: tau_proto::SessionId,
280    },
281    /// List currently running sessions.
282    List(SessionListArgs),
283
284    /// Show a single session's history.
285    Show {
286        /// Session identifier
287        #[arg(
288            long,
289            default_value_t = tau_proto::SessionId::parse(default_session_id())
290                .expect("configured default session id must be valid")
291        )]
292        session_id: tau_proto::SessionId,
293
294        /// Path to per-session storage root (`<state-dir>/sessions/`)
295        #[arg(long, default_value_os_t = default_sessions_dir())]
296        sessions_dir: PathBuf,
297    },
298
299    /// Print exact durable activity accounting for one session as TOON.
300    Stats {
301        /// Session identifier to account.
302        #[arg(long)]
303        session: tau_proto::SessionId,
304
305        /// Path to per-session storage root (`<state-dir>/sessions/`).
306        #[arg(long, default_value_os_t = default_sessions_dir())]
307        sessions_dir: PathBuf,
308    },
309}
310
311/// Output and exact-directory filters for `tau session list`.
312#[derive(Args, Clone, Debug, Default)]
313pub struct SessionListArgs {
314    /// List only harnesses whose canonical startup root is this directory.
315    #[arg(long, value_name = "DIR", value_parser = parse_canonical_directory)]
316    pub dir: Option<PathBuf>,
317
318    /// Emit one JSON array with session id and canonical project root fields.
319    #[arg(long)]
320    pub json: bool,
321}
322
323/// Canonicalizes one existing directory during CLI parsing.
324fn parse_canonical_directory(value: &str) -> Result<PathBuf, String> {
325    let canonical = Path::new(value)
326        .canonicalize()
327        .map_err(|error| format!("cannot access directory `{value}`: {error}"))?;
328    if !canonical.is_dir() {
329        return Err(format!("path is not a directory: `{value}`"));
330    }
331    Ok(canonical)
332}
333
334#[derive(Subcommand)]
335pub enum AgentCommand {
336    /// Inspect durable cache accounting and private capture coverage offline.
337    Cache(AgentCacheArgs),
338    /// Export a durable agent artifact for offline use.
339    Export(AgentExportArgs),
340    /// List agents known to a running session.
341    List(AgentListArgs),
342    /// Unload one idle saved agent without deleting its transcript or session
343    /// history.
344    ///
345    /// Only durable agents are supported. After the request is sent, timeout or
346    /// disconnect is indeterminate; retrying the same session and agent is
347    /// safe.
348    Unload(AgentUnloadArgs),
349    /// Project a validated durable agent snapshot (defaults to compact TOON
350    /// lite).
351    Trace(AgentTraceArgs),
352}
353
354/// Options for `tau agent export`.
355#[derive(Args, Clone)]
356pub struct AgentExportArgs {
357    /// Artifact to export.
358    #[command(subcommand)]
359    pub command: AgentExportCommand,
360}
361
362/// Durable agent artifacts available for export.
363#[derive(Subcommand, Clone)]
364pub enum AgentExportCommand {
365    /// Export user prompts and agent responses from the selected branch.
366    Chat(AgentExportChatArgs),
367}
368
369/// Options for `tau agent export chat`.
370#[derive(Args, Clone)]
371pub struct AgentExportChatArgs {
372    /// Durable agent journal to export.
373    pub agent_id: tau_proto::AgentId,
374    /// Emit the existing strict TOON serialization instead of Markdown.
375    #[arg(long, conflicts_with = "markdown")]
376    pub toons: bool,
377    /// Explicitly select Markdown, which is already the default.
378    #[arg(long, conflicts_with = "toons")]
379    pub markdown: bool,
380    /// Durable agent journal root.
381    #[arg(long, default_value_os_t = default_agents_dir())]
382    pub agents_dir: PathBuf,
383}
384
385/// Offline cache report options shared by agent and session scopes.
386#[derive(Args, Clone)]
387pub struct CacheArgs {
388    /// Emit a compact summary or internal version-zero JSON Lines.
389    #[arg(long, value_enum, default_value_t)]
390    pub format: CacheFormat,
391    /// Restrict canonical responses to this exact local prompt.
392    #[arg(long)]
393    pub prompt: Option<tau_proto::AgentPromptId>,
394    /// Include observations at or after this absolute RFC3339 timestamp.
395    #[arg(long, value_parser = parse_cache_since)]
396    pub since: Option<u64>,
397    /// Include observations at or before this absolute RFC3339 timestamp.
398    #[arg(long, value_parser = parse_cache_until)]
399    pub until: Option<u64>,
400    /// Restrict evidence to this exact configured or effective model.
401    #[arg(long)]
402    pub model: Option<String>,
403    /// Restrict evidence to one closed provider operation.
404    #[arg(long, value_enum)]
405    pub operation: Option<CacheOperation>,
406    /// Restrict evidence to one logical/provider attempt ordinal.
407    #[arg(long)]
408    pub attempt: Option<u64>,
409    /// Exclude exact comparisons without a proven captured response-chain edge.
410    #[arg(long)]
411    pub require_exact_chain: bool,
412    /// Geometry grouping dimensions, as a comma-separated list.
413    #[arg(
414        long,
415        value_enum,
416        value_delimiter = ',',
417        default_value = "model,backend,controls"
418    )]
419    pub group_by: Vec<CacheGroup>,
420    /// Select summary, attribution, continuity, geometry, or gap evidence.
421    #[arg(long, value_enum, default_value_t)]
422    pub view: CacheView,
423    /// Existing Tau state root; inspection never creates it.
424    #[arg(long, default_value_os_t = tau_session_inspect::default_state_dir())]
425    pub state_dir: PathBuf,
426    /// Inclusive compressed byte limit per capture.
427    #[arg(long, default_value_t = 16 * 1024 * 1024)]
428    pub max_compressed_bytes: u64,
429    /// Inclusive decompressed byte limit per capture.
430    #[arg(long, default_value_t = 64 * 1024 * 1024)]
431    pub max_decompressed_bytes: u64,
432    /// Inclusive cumulative decompressed capture bytes.
433    #[arg(long, default_value_t = 1024 * 1024 * 1024)]
434    pub max_total_bytes: u64,
435    /// Capture parser and report working-memory budget in bytes.
436    #[arg(long, default_value_t = 512 * 1024 * 1024)]
437    pub max_memory_bytes: u64,
438    /// Replace one disposable owner-private index for later comparisons.
439    #[arg(long, value_name = "PATH")]
440    pub index: Option<PathBuf>,
441}
442
443/// Closed cache operation accepted by offline selection.
444#[derive(Clone, Copy, ValueEnum)]
445pub enum CacheOperation {
446    /// Ordinary provider inference.
447    Inference,
448    /// Provider-backed standalone compaction.
449    StandaloneCompaction,
450    /// Provider cache refresh or prewarm work.
451    CacheRefresh,
452}
453
454/// Closed empirical geometry grouping dimension.
455#[derive(Clone, Copy, ValueEnum)]
456pub enum CacheGroup {
457    /// Effective provider model.
458    Model,
459    /// Backend adapter and transport.
460    Backend,
461    /// Reasoning, tool, tier, and cache controls.
462    Controls,
463}
464
465/// Parses an inclusive lower RFC3339 bound, rounding toward later observations.
466pub(crate) fn parse_cache_since(value: &str) -> Result<u64, String> {
467    let nanos = parse_cache_rfc3339_nanos(value)?;
468    u64::try_from((nanos + 999) / 1_000)
469        .map_err(|_| "timestamp exceeds the supported Unix-microsecond range".to_owned())
470}
471
472/// Parses an inclusive upper RFC3339 bound, rounding toward earlier
473/// observations.
474pub(crate) fn parse_cache_until(value: &str) -> Result<u64, String> {
475    let nanos = parse_cache_rfc3339_nanos(value)?;
476    u64::try_from(nanos / 1_000)
477        .map_err(|_| "timestamp exceeds the supported Unix-microsecond range".to_owned())
478}
479
480/// Parses one nonnegative absolute RFC3339 timestamp at nanosecond precision.
481fn parse_cache_rfc3339_nanos(value: &str) -> Result<i128, String> {
482    use time::format_description::well_known::Rfc3339;
483
484    let timestamp = time::OffsetDateTime::parse(value, &Rfc3339)
485        .map_err(|error| format!("must be an absolute RFC3339 timestamp: {error}"))?;
486    let nanos = timestamp.unix_timestamp_nanos();
487    if nanos < 0 {
488        return Err("timestamp must not precede the Unix epoch".to_owned());
489    }
490    Ok(nanos)
491}
492
493/// Offline cache evidence projection.
494#[derive(Clone, Copy, Debug, Default, ValueEnum)]
495pub enum CacheView {
496    /// Canonical accounting and overall capture coverage.
497    #[default]
498    Summary,
499    /// Provider-reported per-item attribution evidence.
500    Attribution,
501    /// Attempt, dispatch, anchor, connection, and repair facts.
502    Continuity,
503    /// Empirical reported-token distributions by scalar regime.
504    Geometry,
505    /// Encountered evidence gaps only.
506    Gaps,
507}
508
509/// Available first-delivery content-free cache report encodings.
510#[derive(Clone, Copy, Default, ValueEnum)]
511pub enum CacheFormat {
512    /// Counts and explicit evidence gaps.
513    #[default]
514    Summary,
515    /// Canonical per-response facts, recorded costs, and coverage.
516    Jsonl,
517}
518
519/// Offline cache inspection rooted at one durable agent.
520#[derive(Args, Clone)]
521pub struct AgentCacheArgs {
522    /// Existing durable agent identity.
523    pub agent_id: tau_proto::AgentId,
524    /// Include authenticated creator descendants recursively.
525    #[arg(long)]
526    pub include_descendants: bool,
527    /// Shared read-only report controls.
528    #[command(flatten)]
529    pub options: CacheArgs,
530}
531
532/// Offline cache inspection of durable session membership.
533#[derive(Args, Clone)]
534pub struct SessionCacheArgs {
535    /// Existing durable session identity.
536    pub session_id: tau_proto::SessionId,
537    /// Shared read-only report controls.
538    #[command(flatten)]
539    pub options: CacheArgs,
540}
541
542/// Options for `tau agent unload`.
543#[derive(Args, Clone)]
544pub struct AgentUnloadArgs {
545    /// Running session to mutate.
546    pub session_id: SessionId,
547    /// Saved agent to unload.
548    pub agent_id: tau_proto::AgentId,
549}
550
551/// Options for `tau agent trace`.
552#[derive(Args, Clone)]
553pub struct AgentTraceArgs {
554    /// Durable agent journal to export.
555    pub agent_id: tau_proto::AgentId,
556
557    /// Recursively include agents created by the requested workflow.
558    #[arg(long)]
559    pub include_descendants: bool,
560
561    /// Machine-readable export format.
562    #[arg(long, value_enum, default_value_t)]
563    pub format: AgentTraceFormat,
564
565    /// Compact semantic text and tool-output detail.
566    #[arg(long, value_enum, default_value_t)]
567    pub mode: AgentTraceMode,
568
569    /// Durable agent journal root.
570    #[arg(long, default_value_os_t = default_agents_dir())]
571    pub agents_dir: PathBuf,
572}
573
574/// Machine-readable agent trace export format.
575#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
576pub enum AgentTraceFormat {
577    /// Complete canonical Tau JSON Lines.
578    TauJsonl,
579    /// Lossy OTLP/OpenInference JSON visualization adapter.
580    OtlpJson,
581    /// Compact assistant, message, reasoning, and tool-call timeline as JSON
582    /// Lines.
583    AgentToolsJsonl,
584    /// Compact assistant, message, reasoning, and tool-call timeline as TOON.
585    #[default]
586    AgentToolsToon,
587    /// Content-free provider, tool, wait, outer-turn, and compaction accounting
588    /// as JSON Lines.
589    AgentPerformanceJsonl,
590}
591
592/// Content detail for compact semantic trace formats.
593#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
594pub enum AgentTraceMode {
595    /// Report complete metrics and at most 4 KiB of each text/output item.
596    #[default]
597    Lite,
598    /// Report complete metrics and complete semantic text/normalized output.
599    Full,
600}
601
602/// Filters for `tau agent list`.
603#[derive(Args, Clone)]
604pub struct AgentListArgs {
605    /// Running session to query.
606    pub session_id: SessionId,
607
608    /// Include suspended live agents.
609    #[arg(long)]
610    pub include_suspended: bool,
611
612    /// Include current unavailable agents and rows with missing, invalid, or
613    /// unreadable creation facts.
614    #[arg(long)]
615    pub include_unavailable: bool,
616
617    /// Include previously loaded and now-unloaded agents.
618    #[arg(long)]
619    pub include_unloaded: bool,
620
621    /// Include every supported agent category.
622    #[arg(long)]
623    pub all: bool,
624}
625
626#[derive(Subcommand)]
627pub enum DevCommand {
628    /// Send one line to a running session.
629    Send {
630        /// Running session identifier.
631        session_id: SessionId,
632
633        /// Line to submit. Commands are interpreted like the TUI.
634        #[arg(required = true, trailing_var_arg = true)]
635        line: Vec<String>,
636    },
637
638    /// Dump the initial provider prompt built from local config.
639    DumpInitialPrompt {
640        /// Output path.
641        #[arg(long, default_value = "tmp/initial_prompt.txt")]
642        out: PathBuf,
643
644        /// Synthetic first user message.
645        #[arg(long, default_value = "hello")]
646        message: String,
647    },
648
649    /// Print the effective provider-visible prompt context.
650    ///
651    /// Configures ordinary extensions, initializes one fresh ephemeral agent,
652    /// and waits boundedly for its context without calling a provider.
653    /// Extensions retain ordinary persistent state access and side effects.
654    /// Omitting `--role` uses the configured startup role.
655    PrintPrompt {
656        /// Include harness-injected AGENTS.md context.
657        #[arg(long = "enable-agents-md", default_value_t = true, action = clap::ArgAction::Set)]
658        enable_agents_md: bool,
659    },
660
661    /// Print only the rendered system prompt for a role.
662    ///
663    /// Uses a stable fake agent id as the explicit `agent_id` input for custom
664    /// templates; built-in templates intentionally omit agent identity.
665    PrintSystemPrompt,
666
667    /// Print the effective tool definitions.
668    ///
669    /// Uses the same fresh ephemeral-agent lifecycle and effective model/tool
670    /// snapshot as `print-prompt`, without calling a provider. Extensions
671    /// retain ordinary persistent state access and side effects. Omitting
672    /// `--role` uses the configured startup role.
673    PrintTools,
674
675    /// Print the effective skills available to a role.
676    ///
677    /// Uses the same fresh ephemeral-agent lifecycle and effective context
678    /// snapshot as `print-prompt`, without calling a provider. Extensions
679    /// retain ordinary persistent state access and side effects. Omitting
680    /// `--role` uses the configured startup role.
681    PrintSkills {
682        /// Output encoding for the effective skill list.
683        #[arg(long, value_enum, default_value_t)]
684        format: SkillOutputFormat,
685    },
686
687    /// Preview config-derived declarations from explicitly opted-in extensions.
688    ///
689    /// Does not start a harness, read extension state or credentials, or
690    /// discover agent context. Unsupported or incomplete inventories exit
691    /// unsuccessfully.
692    PreviewDeclarations,
693
694    /// Inspect or clear reports recorded by the standard papercut reporter.
695    Papercut {
696        /// Papercut operation to run.
697        #[command(subcommand)]
698        command: PapercutCommand,
699    },
700
701    /// Manage a manual Tau end-to-end session in a private tmux server.
702    Tmux {
703        /// Tmux helper action to run.
704        #[command(subcommand)]
705        command: DevTmuxCommand,
706    },
707}
708
709/// Encodings supported by `tau dev print-skills`.
710#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, ValueEnum)]
711pub enum SkillOutputFormat {
712    /// Human-readable Markdown document.
713    #[default]
714    Markdown,
715    /// Machine-readable JSON array.
716    Json,
717}
718
719/// Commands that inspect or clear the standard papercut reporter's records.
720#[derive(Subcommand)]
721pub enum PapercutCommand {
722    /// List recorded papercut reports.
723    List {
724        /// Render the reports as copyable Markdown.
725        #[arg(long)]
726        markdown: bool,
727
728        /// Tau state directory containing the standard reporter's records.
729        #[arg(long, default_value_os_t = default_state_dir())]
730        state_dir: PathBuf,
731    },
732
733    /// Archive every active papercut report at this command's serialized clear
734    /// boundary and print the preserved file's path.
735    Clear {
736        /// Tau state directory containing the standard reporter's records.
737        #[arg(long, default_value_os_t = default_state_dir())]
738        state_dir: PathBuf,
739    },
740}
741
742/// Hidden tmux helper subcommands for manual Tau end-to-end sessions.
743#[derive(Subcommand)]
744pub enum DevTmuxCommand {
745    /// Start Tau in an isolated scratch environment inside tmux.
746    Start(DevTmuxStartArgs),
747
748    /// Capture the current tmux pane contents.
749    Capture(DevTmuxTargetArgs),
750
751    /// Send text to the tmux pane, followed by Enter by default.
752    Send(DevTmuxSendArgs),
753
754    /// Stop the private tmux server.
755    Stop(DevTmuxStopArgs),
756}
757
758/// Shared tmux target arguments used by the manual E2E helper.
759#[derive(Args)]
760pub struct DevTmuxCommonArgs {
761    /// Scratch root containing the tmux socket and isolated Tau environment.
762    /// When omitted, `start` generates a fresh temporary root; target commands
763    /// use the historical static fallback root.
764    #[arg(long = "scratch-root", visible_alias = "root")]
765    pub scratch_root: Option<PathBuf>,
766
767    /// Private tmux session name.
768    #[arg(long, default_value = "tau-e2e")]
769    pub session: String,
770}
771
772/// Arguments for starting a new isolated Tau tmux session.
773#[derive(Args)]
774pub struct DevTmuxStartArgs {
775    /// Shared tmux socket/session selection.
776    #[command(flatten)]
777    pub common: DevTmuxCommonArgs,
778
779    /// Tau binary to run inside tmux.
780    #[arg(long)]
781    pub tau_bin: Option<PathBuf>,
782
783    /// Working directory for Tau and core-shell.
784    #[arg(long)]
785    pub workdir: Option<PathBuf>,
786
787    /// Initial tmux pane width.
788    #[arg(long, default_value_t = 120)]
789    pub width: u16,
790
791    /// Initial tmux pane height.
792    #[arg(long, default_value_t = 40)]
793    pub height: u16,
794}
795
796/// Arguments that identify an existing Tau tmux session.
797#[derive(Args)]
798pub struct DevTmuxTargetArgs {
799    /// Shared tmux socket/session selection.
800    #[command(flatten)]
801    pub common: DevTmuxCommonArgs,
802}
803
804/// Arguments for sending literal input to an existing Tau tmux session.
805#[derive(Args)]
806pub struct DevTmuxSendArgs {
807    /// Existing tmux session to receive input.
808    #[command(flatten)]
809    pub target: DevTmuxTargetArgs,
810
811    /// Do not send Enter after the text.
812    #[arg(long)]
813    pub no_enter: bool,
814
815    /// Text to send literally to the Tau prompt.
816    #[arg(required = true, trailing_var_arg = true)]
817    pub text: Vec<String>,
818}
819
820/// Arguments for stopping an existing Tau tmux session.
821#[derive(Args)]
822pub struct DevTmuxStopArgs {
823    /// Existing tmux session to stop.
824    #[command(flatten)]
825    pub target: DevTmuxTargetArgs,
826
827    /// Remove the scratch root after stopping tmux.
828    #[arg(long)]
829    pub remove_scratch: bool,
830}