## Context `rusty-claude-cli/src/main.rs` (13,292 lines) contains the entire CLI application logic. It currently imports three sub-modules (init, input, render), but everything else lives in the single file. The file has grown organically as features were added, with no enforced module boundaries. **Current module layout:** ``` src/ ├── init.rs (~200 lines) — workspace initialization ├── input.rs (~200 lines) — rustyline REPL editor ├── render.rs (~200 lines) — terminal rendering, markdown, spinner └── main.rs (~13,292 lines) — everything else ``` **Internal structure of main.rs:** | Region | Lines | Content | |--------|-------|---------| | Imports, consts, type aliases | 1-200 | `#![allow]`, `mod`, `use`, `const`, `type` | | `ModelSource/Provenance` | 60-150 | Model resolution types | | `main()`, `run()`, helpers | 205-535 | Entry point + CLI action dispatch | | `CliAction`, `CliOutputFormat` | 506-645 | Action enum definitions | | `parse_args()` + 50 helpers | 645-1830 | Argument parsing and validation | | `Doctor` system | 1831-2529 | Health check infrastructure | | Status/Session functions | 2835-3700 | State snapshot and git parsing | | `SessionHandle`, `LiveCli`, `BuiltRuntime` structs | 3678-4180 | Core data structures | | `LiveCli` impl + `run_repl` | 4181-5550 | REPL loop and command dispatch | | Formatting helpers | 5550-6900 | Report rendering (config, export, diff, etc.) | | Runtime construction | 6906-7600 | `build_runtime`, plugin/MCP boot | | `AnthropicRuntimeClient` | 7497-7910 | API client wrapper | | Output event formatting | 7910-8730 | tool call/result formatting | | `CliToolExecutor` | 8731-8923 | Tool execution adapter | | `print_help_to` | 8923-9122 | Help text | | Tests | 9123-13291 | `#[cfg(test)] mod tests { ... }` | ## Goals / Non-Goals **Goals:** - Split `main.rs` into ≤ 2000 lines by moving code into responsibility-focused modules - Every new file must document its abstraction boundary (via its name, public API surface, and module-level structure) - Zero behavior change — this is a pure move-and-rename refactoring - All existing tests pass without modification (except import path updates) - Each individual migration step compiles independently (no long-lived broken state) **Non-Goals:** - No new features or capabilities - No architectural pattern changes (trait extraction, Command Handler pattern, etc.) — those belong in a future Phase 2 - No changes to `init.rs`, `input.rs`, `render.rs` — existing modules are already well-factored - No Cargo dependency changes - No performance optimization - No public API changes (this is an internal binary crate, not a library) ## Decisions ### D1: Module granularity — fine-grained (one concern per file) **Chosen:** ~17 new files, each with a single thematic responsibility. **Rationale:** - Each file will be 150–1200 lines, well under the 2000-line limit - File names directly map to the concern they contain (e.g., `doctor.rs` → diagnostic functions) - Easier to navigate during development — you know exactly which file to open - The `claw-cli` sibling crate demonstrates this pattern (app.rs, args.rs, init.rs, input.rs, main.rs, render.rs) **Alternatives considered:** - *Fewer, larger files* (e.g., 5-6 files at 2000 lines each) — rejected because it just postpones the problem and doesn't improve navigability - *Single `lib.rs` with sub-modules* — the crate currently has no `lib.rs` and is a standalone binary; adding one would be a larger structural change ### D2: impl LiveCli — concentrate in `live_cli.rs`, REPL commands as free functions in `repl_commands.rs` **Chosen:** `LiveCli` struct definition + all core methods live in `live_cli.rs`. Command handler functions (`handle_repl_command`, `handle_session_command`, `set_model`, etc.) become free functions in `repl_commands.rs` that accept `&mut LiveCli`. **Rationale:** - Rust allows `impl` blocks in multiple files, but this requires careful coordination and makes the struct's API surface hard to discover - Free functions with an explicit receiver are simpler to understand and maintain - No orphan rule violations — `repl_commands.rs` functions simply take `cli: &mut LiveCli` as their first parameter **Implementation pattern:** ```rust // repl_commands.rs pub(crate) fn handle_repl_command( cli: &mut LiveCli, command: SlashCommand, ) -> Result> { ... } pub(crate) fn set_model( cli: &mut LiveCli, model: Option, ) -> Result> { ... } ``` ### D3: Visibility strategy — `pub(crate)` for cross-module access, never `pub` **Chosen:** All functions that need to be called from outside their module are `pub(crate)`. Nothing is `pub` (this is a binary crate, not a library). **Rationale:** - `main.rs` is the crate root that re-exports or `use`s sub-modules - Test code (in `#[cfg(test)] mod tests`) accesses items via `use super::*` — making items `pub(crate)` is sufficient since tests are inline or in `tests/` directory - Keeps the crate's internal API surface explicit ### D4: Module dependency order — low-level first, then layered **Chosen:** Build modules in strict dependency order (constants → model → args → ... → live_cli → repl_commands → main). **Rationale:** - Avoids circular dependencies between modules - Each module only depends on modules that are already compiled - Follows the principle of "stable dependencies": lower-level modules change less frequently **Dependency graph:** ``` layer 0: constants layer 1: model layer 2: permission, progress, doctor layer 3: args, session layer 4: status, export, config, diff layer 5: formatting layer 6: api_client, tool_executor layer 7: runtime_builder layer 8: live_cli layer 9: repl_commands layer 10: main (thin dispatch) ``` ### D5: Test strategy — inline tests stay with source **Chosen:** The `#[cfg(test)] mod tests { ... }` block at the bottom of `main.rs` stays attached to `main.rs`. As functions migrate to new modules, their test cases are NOT migrated in Phase 1 — `main.rs` tests continue to import migrated functions via `use super::*` which can reach them through module paths. **Rationale:** - Moving tests adds risk of breaking them and increases the scope of each step - After Phase 1 is complete and stable, Phase 1.5 can migrate tests to their respective source modules - Rust's module system allows `use crate::module_name::function_name` from within `main.rs` tests - This minimizes per-step risk ### D6: `main.rs` re-export strategy **Chosen:** `main.rs` will declare `pub(crate) use module_name::*;` for key modules so existing code (especially tests) can reference items without full path qualification. **Rationale:** - Minimizes churn in test code during the migration - Provides a single reference point for all public crate items - Can be cleaned up in a follow-up pass ## Module API Surface Summary | Module | `pub(crate)` items | Dependencies | |--------|-------------------|--------------| | `constants.rs` | all `const`, `type` aliases | none | | `model.rs` | `ModelSource`, `ModelProvenance`, `resolve_model_alias*`, `resolve_repl_model`, `validate_model_syntax`, `config_model_for_current_dir`, `config_alias_for_current_dir`, `max_tokens_for_model` | `constants`, `api` (external) | | `permission.rs` | `CliPermissionPrompter`, `permission_policy` | `constants`, `runtime` (external) | | `progress.rs` | `InternalPromptProgressState/Event/Shared/Reporter/Run`, `CliHookProgressReporter`, `describe_tool_progress`, `format_internal_prompt_progress_line` | `constants` | | `doctor.rs` | `DiagnosticLevel`, `DiagnosticCheck`, `DoctorReport`, `render_doctor_report`, `run_doctor`, `check_*_health` (6 fns), `render_diagnostic_check` | `constants`, `runtime` (external) | | `args.rs` | `CliAction`, `CliOutputFormat`, `LocalHelpTopic`, `parse_args`, `parse_export_args`, `parse_resume_args`, `parse_system_prompt_args`, `parse_dump_manifests_args`, `parse_local_help_action`, `parse_direct_slash_cli_action`, `parse_acp_args`, `try_resolve_bare_skill_prompt`, `join_optional_args`, `format_unknown_*`, `render_suggestion_line`, `suggest_*`, `levenshtein_distance`, `common_prefix_len`, `normalize_allowed_tools`, `current_tool_registry`, `parse_permission_mode_arg`, `permission_mode_from_label/resolved/default`, `config_permission_mode_for_current_dir`, `provider_label`, `format_connected_line`, `filter_tool_specs`, `is_help_flag`, `parse_single_word_command_alias`, `bare_slash_command_guidance`, `removed_auth_surface_error`, `looks_like_subcommand_typo`, `ranked_suggestions`, `omc_compatibility_note_for_unknown_slash_command` | `constants`, `model`, `runtime`/`tools`/`api` (external) | | `session.rs` | `SessionHandle`, `ManagedSessionSummary`, `sessions_dir`, `current_session_store`, `new_cli_session`, `create_managed_session_handle`, `resolve_session_reference`, `resolve_managed_session_path`, `list_managed_sessions`, `latest_managed_session`, `load_session_reference`, `delete_managed_session`, `confirm_session_deletion`, `render_session_list`, `format_session_modified_age`, `write_session_clear_backup`, `session_clear_backup_path` | `constants`, `runtime` (external) | | `status.rs` | `StatusContext`, `StatusUsage`, `GitWorkspaceSummary`, `print_status_snapshot`, `status_json_value`, `status_context`, `format_status_report`, `format_sandbox_report`, `print_sandbox_status_snapshot`, `sandbox_json_value`, `parse_git_status_metadata`, `parse_git_status_branch`, `parse_git_workspace_summary`, `resolve_git_branch_for`, `run_git_capture_in`, `find_git_root_in`, `parse_git_status_metadata_for` | `constants`, `session` | | `formatting.rs` | `format_tool_call_start`, `format_tool_result`, `format_bash_call/result`, `format_read/write/edit/glob/grep_result`, `format_search_start`, `format_patch_preview`, `format_structured_patch_preview`, `format_generic_tool_result`, `summarize_tool_payload`, `truncate_for_summary`, `truncate_output_for_display`, `render_thinking_block_summary`, `push_output_block`, `response_to_events`, `push_prompt_cache_record`, `prompt_cache_record_to_runtime_event`, `final_assistant_text`, `collect_tool_uses`, `collect_tool_results`, `collect_prompt_cache_events`, `short_tool_id`, `extract_tool_path`, `first_visible_line`, `print_help_to`, `print_help`, `render_help_topic`, `print_help_topic` | `constants` | | `config.rs` | `render_config_report`, `render_config_json`, `render_memory_report`, `render_memory_json` | `runtime` (external) | | `diff.rs` | `render_diff_report`, `render_diff_report_for`, `render_diff_json_for`, `run_git_diff_command_in` | none (std + external) | | `export.rs` | `run_export`, `render_export_text`, `render_session_markdown`, `default_export_filename`, `resolve_export_path`, `summarize_tool_payload_for_markdown` | `session`, `status` | | `api_client.rs` | `AnthropicRuntimeClient`, `resolve_cli_auth_source`, `resolve_cli_auth_source_for_cwd`, `request_ends_with_tool_result`, `format_user_visible_api_error`, `format_context_window_blocked_error` | `progress`, `api` (external) | | `tool_executor.rs` | `CliToolExecutor`, `ToolSearchRequest`, `McpToolRequest`, `ListMcpResourcesRequest`, `ReadMcpResourceRequest`, `permission_policy`, `convert_messages` | `formatting`, `runtime`/`tools` (external) | | `runtime_builder.rs` | `RuntimePluginState`, `RuntimeMcpState`, `BuiltRuntime`, `HookAbortMonitor`, `build_runtime`, `build_runtime_with_plugin_state`, `build_runtime_plugin_state`, `build_runtime_plugin_state_with_loader`, `build_plugin_manager`, `resolve_plugin_path`, `runtime_hook_config_from_plugin_hooks`, `build_system_prompt`, `build_runtime_mcp_state`, `mcp_runtime_tool_definition`, `mcp_wrapper_tool_definitions`, `permission_mode_for_mcp_tool`, `mcp_annotation_flag` | `api_client`, `tool_executor`, `progress`, `permission`, `plugins` (external) | | `live_cli.rs` | `LiveCli`, `PromptHistoryEntry`, `run_repl`, `run_resume_command`, `resume_session`, `detect_broad_cwd`, `enforce_broad_cwd_policy`, `run_stale_base_preflight` | `runtime_builder`, `session`, `status`, `repl_commands` | | `repl_commands.rs` | `handle_repl_command`, `handle_session_command`, `handle_plugins_command`, `set_model`, `set_permissions`, `clear_session`, `compact`, `reload_runtime_features`, `print_status`, `print_cost`, `print_prompt_history`, `resume_session`, `export_session`, `print_*` (config/memory/agents/mcp/skills/plugins/diff/version/sandbox), `run_bughunter/ultraplan/teleport/debug_tool_call/commit/pr/issue`, `format_*` (bughunter/ultraplan/pr/issue/commit/auto_compaction/compact/unknown_slash_command_message/model/model_switch/permissions/permissions_switch/cost/resume/compact/history), `render_repl_help`, `render_resume_usage`, `render_teleport_report`, `render_last_tool_debug_report`, `render_prompt_history_report`, `render_version_report`, `collect_session_prompt_history`, `recent_user_context`, `parse_history_count`, `format_history_timestamp`, `civil_from_days`, `git_output`, `git_status_ok`, `command_exists`, `write_temp_text_file`, `sanitize_generated_message`, `parse_titled_body`, `indent_block`, `validate_no_args`, `init_claude_md`, `normalize_permission_mode`, `run_init`, `init_json_value`, `print_acp_status`, `run_worker_state`, `run_mcp_serve`, `print_bootstrap_plan`, `print_system_prompt`, `print_version`, `version_json_value`, `slash_command_completion_candidates_with_sessions`, `truncate_for_prompt` | `live_cli`, `status`, `config`, `diff`, `export`, `doctor`, `commands` (external) | ### `main.rs` (post-split) — what remains **Items that stay in main.rs:** ```rust mod init; mod input; mod render; // existing mod constants; mod model; mod permission; // new mod progress; mod doctor; mod args; mod session; mod status; mod formatting; mod config; mod diff; mod export; mod api_client; mod tool_executor; mod runtime_builder; mod live_cli; mod repl_commands; // Re-exports for test compatibility pub(crate) use constants::*; pub(crate) use model::*; pub(crate) use args::*; // ... etc for all modules used by tests fn main() { ... } // unchanged fn run() -> ... { ... } // only dispatches CliAction variants fn classify_error_kind() // stays — used in main() fn split_error_hint() // stays — used in main() fn read_piped_stdin() // stays — used in run() fn merge_prompt_with_stdin() // stays — used in run() fn write_mcp_server_fixture() // stays — test utility // Test module stays here, unchanged, referencing functions via crate:: paths ``` ## Risks / Trade-offs | Risk | Likelihood | Impact | Mitigation | |------|-----------|--------|------------| | **Missing a function during split** | Medium | High — broken build | Each step compiles independently; CI catches missing items | | **Circular dependency between modules** | Low | High — redesign needed | Strict dependency ordering (low-level → high-level) prevents this | | **Test breakage from visibility changes** | Medium | Medium — tests fail | Keep `pub(crate) use module::*` re-exports in main.rs; tests continue to work | | **Merge conflicts with parallel work** | High (active project) | Medium | Execute refactoring as a focused session; each step is a small diff | | **Accidental behavior change during move** | Low | High — user-facing bug | Pure copy-paste moves; no logic modification; review each diff carefully | | **File count increase reduces discoverability** | Medium | Low — developer adaptation | Consistent naming convention; module names map 1:1 to concerns | ## Open Questions 1. **Should `STUB_COMMANDS` (80+ entries) stay in `constants.rs` or be in its own file?** — Currently planned for `constants.rs` (~80 lines of data), can be extracted later if it grows. 2. **Should `write_mcp_server_fixture` be moved to the test support module?** — It's only used in tests. Keep in `main.rs` for now; it's a short function. 3. **How to handle the `#[allow(...)]` attributes?** — The top-level `#![allow(...)]` applies to the crate root. Some module-level allows may need to be duplicated.