Close the clawability backlog with deterministic CLI output and lane lineage

Finish the remaining roadmap work by making direct CLI JSON output deterministic across the non-interactive surface, restoring the degraded-startup MCP test as a real workspace test, and adding branch-lock plus commit-lineage primitives so downstream lane consumers can distinguish superseded worktree commits from canonical lineage.

Constraint: Keep the user-facing config namespace centered on .claw while preserving legacy fallback discovery for compatibility
Constraint: Verification needed to stay clean-room and reproducible from the checked-in workspace alone
Rejected: Leave the output-format contract implied by ad-hoc smoke runs only | too easy for direct CLI regressions to slip back into prose-only output
Rejected: Keep commit provenance as free-form detail text | downstream consumers need structured branch/worktree/supersession metadata
Confidence: medium
Scope-risk: moderate
Directive: Extend the JSON contract through the same direct CLI entrypoints instead of adding one-off serializers on parallel code paths
Tested: python .github/scripts/check_doc_source_of_truth.py
Tested: cd rust && cargo fmt --all --check
Tested: cd rust && cargo test --workspace
Tested: cd rust && cargo clippy -p commands -p tools -p rusty-claude-cli --all-targets --no-deps -- -D warnings
Not-tested: full cargo clippy --workspace --all-targets -- -D warnings still reports unrelated pre-existing runtime lint debt outside this change set
This commit is contained in:
Yeachan-Heo
2026-04-05 18:40:33 +00:00
parent 93e979261e
commit 19c6b29524
14 changed files with 954 additions and 138 deletions

View File

@@ -89,6 +89,10 @@ const CLI_OPTION_SUGGESTIONS: &[&str] = &[
];
type AllowedToolSet = BTreeSet<String>;
type RuntimePluginStateBuildOutput = (
Option<Arc<Mutex<RuntimeMcpState>>>,
Vec<RuntimeToolDefinition>,
);
fn main() {
if let Err(error) = run() {
@@ -109,9 +113,12 @@ Run `claw --help` for usage."
fn run() -> Result<(), Box<dyn std::error::Error>> {
let args: Vec<String> = env::args().skip(1).collect();
match parse_args(&args)? {
CliAction::DumpManifests => dump_manifests(),
CliAction::BootstrapPlan => print_bootstrap_plan(),
CliAction::Agents { args } => LiveCli::print_agents(args.as_deref())?,
CliAction::DumpManifests { output_format } => dump_manifests(output_format)?,
CliAction::BootstrapPlan { output_format } => print_bootstrap_plan(output_format)?,
CliAction::Agents {
args,
output_format,
} => LiveCli::print_agents(args.as_deref(), output_format)?,
CliAction::Mcp {
args,
output_format,
@@ -120,12 +127,17 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
args,
output_format,
} => LiveCli::print_skills(args.as_deref(), output_format)?,
CliAction::PrintSystemPrompt { cwd, date } => print_system_prompt(cwd, date),
CliAction::Version => print_version(),
CliAction::PrintSystemPrompt {
cwd,
date,
output_format,
} => print_system_prompt(cwd, date, output_format)?,
CliAction::Version { output_format } => print_version(output_format)?,
CliAction::ResumeSession {
session_path,
commands,
} => resume_session(&session_path, &commands),
output_format,
} => resume_session(&session_path, &commands, output_format),
CliAction::Status {
model,
permission_mode,
@@ -140,27 +152,32 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
permission_mode,
} => LiveCli::new(model, true, allowed_tools, permission_mode)?
.run_turn_with_output(&prompt, output_format)?,
CliAction::Login => run_login()?,
CliAction::Logout => run_logout()?,
CliAction::Doctor => run_doctor()?,
CliAction::Init => run_init()?,
CliAction::Login { output_format } => run_login(output_format)?,
CliAction::Logout { output_format } => run_logout(output_format)?,
CliAction::Doctor { output_format } => run_doctor(output_format)?,
CliAction::Init { output_format } => run_init(output_format)?,
CliAction::Repl {
model,
allowed_tools,
permission_mode,
} => run_repl(model, allowed_tools, permission_mode)?,
CliAction::HelpTopic(topic) => print_help_topic(topic),
CliAction::Help => print_help(),
CliAction::Help { output_format } => print_help(output_format)?,
}
Ok(())
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum CliAction {
DumpManifests,
BootstrapPlan,
DumpManifests {
output_format: CliOutputFormat,
},
BootstrapPlan {
output_format: CliOutputFormat,
},
Agents {
args: Option<String>,
output_format: CliOutputFormat,
},
Mcp {
args: Option<String>,
@@ -173,11 +190,15 @@ enum CliAction {
PrintSystemPrompt {
cwd: PathBuf,
date: String,
output_format: CliOutputFormat,
},
Version {
output_format: CliOutputFormat,
},
Version,
ResumeSession {
session_path: PathBuf,
commands: Vec<String>,
output_format: CliOutputFormat,
},
Status {
model: String,
@@ -194,10 +215,18 @@ enum CliAction {
allowed_tools: Option<AllowedToolSet>,
permission_mode: PermissionMode,
},
Login,
Logout,
Doctor,
Init,
Login {
output_format: CliOutputFormat,
},
Logout {
output_format: CliOutputFormat,
},
Doctor {
output_format: CliOutputFormat,
},
Init {
output_format: CliOutputFormat,
},
Repl {
model: String,
allowed_tools: Option<AllowedToolSet>,
@@ -205,7 +234,9 @@ enum CliAction {
},
HelpTopic(LocalHelpTopic),
// prompt-mode formatting is only supported for non-interactive runs
Help,
Help {
output_format: CliOutputFormat,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -346,11 +377,11 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
}
if wants_help {
return Ok(CliAction::Help);
return Ok(CliAction::Help { output_format });
}
if wants_version {
return Ok(CliAction::Version);
return Ok(CliAction::Version { output_format });
}
let allowed_tools = normalize_allowed_tools(&allowed_tool_values)?;
@@ -364,22 +395,25 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
});
}
if rest.first().map(String::as_str) == Some("--resume") {
return parse_resume_args(&rest[1..]);
return parse_resume_args(&rest[1..], output_format);
}
if let Some(action) = parse_local_help_action(&rest) {
return action;
}
if let Some(action) = parse_single_word_command_alias(&rest, &model, permission_mode_override, output_format) {
if let Some(action) =
parse_single_word_command_alias(&rest, &model, permission_mode_override, output_format)
{
return action;
}
let permission_mode = permission_mode_override.unwrap_or_else(default_permission_mode);
match rest[0].as_str() {
"dump-manifests" => Ok(CliAction::DumpManifests),
"bootstrap-plan" => Ok(CliAction::BootstrapPlan),
"dump-manifests" => Ok(CliAction::DumpManifests { output_format }),
"bootstrap-plan" => Ok(CliAction::BootstrapPlan { output_format }),
"agents" => Ok(CliAction::Agents {
args: join_optional_args(&rest[1..]),
output_format,
}),
"mcp" => Ok(CliAction::Mcp {
args: join_optional_args(&rest[1..]),
@@ -389,10 +423,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
args: join_optional_args(&rest[1..]),
output_format,
}),
"system-prompt" => parse_system_prompt_args(&rest[1..]),
"login" => Ok(CliAction::Login),
"logout" => Ok(CliAction::Logout),
"init" => Ok(CliAction::Init),
"system-prompt" => parse_system_prompt_args(&rest[1..], output_format),
"login" => Ok(CliAction::Login { output_format }),
"logout" => Ok(CliAction::Logout { output_format }),
"init" => Ok(CliAction::Init { output_format }),
"prompt" => {
let prompt = rest[1..].join(" ");
if prompt.trim().is_empty() {
@@ -446,15 +480,15 @@ fn parse_single_word_command_alias(
}
match rest[0].as_str() {
"help" => Some(Ok(CliAction::Help)),
"version" => Some(Ok(CliAction::Version)),
"help" => Some(Ok(CliAction::Help { output_format })),
"version" => Some(Ok(CliAction::Version { output_format })),
"status" => Some(Ok(CliAction::Status {
model: model.to_string(),
permission_mode: permission_mode_override.unwrap_or_else(default_permission_mode),
output_format,
})),
"sandbox" => Some(Ok(CliAction::Sandbox { output_format })),
"doctor" => Some(Ok(CliAction::Doctor)),
"doctor" => Some(Ok(CliAction::Doctor { output_format })),
other => bare_slash_command_guidance(other).map(Err),
}
}
@@ -502,8 +536,11 @@ fn parse_direct_slash_cli_action(
) -> Result<CliAction, String> {
let raw = rest.join(" ");
match SlashCommand::parse(&raw) {
Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help),
Ok(Some(SlashCommand::Agents { args })) => Ok(CliAction::Agents { args }),
Ok(Some(SlashCommand::Help)) => Ok(CliAction::Help { output_format }),
Ok(Some(SlashCommand::Agents { args })) => Ok(CliAction::Agents {
args,
output_format,
}),
Ok(Some(SlashCommand::Mcp { action, target })) => Ok(CliAction::Mcp {
args: match (action, target) {
(None, None) => None,
@@ -727,7 +764,10 @@ fn filter_tool_specs(
tool_registry.definitions(allowed_tools)
}
fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
fn parse_system_prompt_args(
args: &[String],
output_format: CliOutputFormat,
) -> Result<CliAction, String> {
let mut cwd = env::current_dir().map_err(|error| error.to_string())?;
let mut date = DEFAULT_DATE.to_string();
let mut index = 0;
@@ -752,10 +792,14 @@ fn parse_system_prompt_args(args: &[String]) -> Result<CliAction, String> {
}
}
Ok(CliAction::PrintSystemPrompt { cwd, date })
Ok(CliAction::PrintSystemPrompt {
cwd,
date,
output_format,
})
}
fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
fn parse_resume_args(args: &[String], output_format: CliOutputFormat) -> Result<CliAction, String> {
let (session_path, command_tokens): (PathBuf, &[String]) = match args.first() {
None => (PathBuf::from(LATEST_SESSION_REFERENCE), &[]),
Some(first) if looks_like_slash_command_token(first) => {
@@ -795,6 +839,7 @@ fn parse_resume_args(args: &[String]) -> Result<CliAction, String> {
Ok(CliAction::ResumeSession {
session_path,
commands,
output_format,
})
}
@@ -930,9 +975,21 @@ fn render_doctor_report() -> Result<DoctorReport, Box<dyn std::error::Error>> {
})
}
fn run_doctor() -> Result<(), Box<dyn std::error::Error>> {
fn run_doctor(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let report = render_doctor_report()?;
println!("{}", report.render());
let message = report.render();
match output_format {
CliOutputFormat::Text => println!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "doctor",
"message": message,
"report": message,
"has_failures": report.has_failures(),
}))?
),
}
if report.has_failures() {
return Err("doctor found failing checks".into());
}
@@ -1212,26 +1269,54 @@ fn looks_like_slash_command_token(token: &str) -> bool {
.any(|spec| spec.name == name || spec.aliases.contains(&name))
}
fn dump_manifests() {
fn dump_manifests(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let workspace_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let paths = UpstreamPaths::from_workspace_dir(&workspace_dir);
match extract_manifest(&paths) {
Ok(manifest) => {
println!("commands: {}", manifest.commands.entries().len());
println!("tools: {}", manifest.tools.entries().len());
println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
}
Err(error) => {
eprintln!("failed to extract manifests: {error}");
std::process::exit(1);
match output_format {
CliOutputFormat::Text => {
println!("commands: {}", manifest.commands.entries().len());
println!("tools: {}", manifest.tools.entries().len());
println!("bootstrap phases: {}", manifest.bootstrap.phases().len());
}
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "dump-manifests",
"commands": manifest.commands.entries().len(),
"tools": manifest.tools.entries().len(),
"bootstrap_phases": manifest.bootstrap.phases().len(),
}))?
),
}
Ok(())
}
Err(error) => Err(format!("failed to extract manifests: {error}").into()),
}
}
fn print_bootstrap_plan() {
for phase in runtime::BootstrapPlan::claude_code_default().phases() {
println!("- {phase:?}");
fn print_bootstrap_plan(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let phases = runtime::BootstrapPlan::claude_code_default()
.phases()
.iter()
.map(|phase| format!("{phase:?}"))
.collect::<Vec<_>>();
match output_format {
CliOutputFormat::Text => {
for phase in &phases {
println!("- {phase}");
}
}
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "bootstrap-plan",
"phases": phases,
}))?
),
}
Ok(())
}
fn default_oauth_config() -> OAuthConfig {
@@ -1249,7 +1334,7 @@ fn default_oauth_config() -> OAuthConfig {
}
}
fn run_login() -> Result<(), Box<dyn std::error::Error>> {
fn run_login(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
let config = ConfigLoader::default_for(&cwd).load()?;
let default_oauth = default_oauth_config();
@@ -1262,8 +1347,10 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
OAuthAuthorizationRequest::from_config(oauth, redirect_uri.clone(), state.clone(), &pkce)
.build_url();
println!("Starting Claude OAuth login...");
println!("Listening for callback on {redirect_uri}");
if output_format == CliOutputFormat::Text {
println!("Starting Claude OAuth login...");
println!("Listening for callback on {redirect_uri}");
}
if let Err(error) = open_browser(&authorize_url) {
eprintln!("warning: failed to open browser automatically: {error}");
println!("Open this URL manually:\n{authorize_url}");
@@ -1287,8 +1374,13 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
}
let client = AnthropicClient::from_auth(AuthSource::None).with_base_url(api::read_base_url());
let exchange_request =
OAuthTokenExchangeRequest::from_config(oauth, code, state, pkce.verifier, redirect_uri);
let exchange_request = OAuthTokenExchangeRequest::from_config(
oauth,
code,
state,
pkce.verifier,
redirect_uri.clone(),
);
let runtime = tokio::runtime::Runtime::new()?;
let token_set = runtime.block_on(client.exchange_oauth_code(oauth, &exchange_request))?;
save_oauth_credentials(&runtime::OAuthTokenSet {
@@ -1297,13 +1389,33 @@ fn run_login() -> Result<(), Box<dyn std::error::Error>> {
expires_at: token_set.expires_at,
scopes: token_set.scopes,
})?;
println!("Claude OAuth login complete.");
match output_format {
CliOutputFormat::Text => println!("Claude OAuth login complete."),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "login",
"callback_port": callback_port,
"redirect_uri": redirect_uri,
"message": "Claude OAuth login complete.",
}))?
),
}
Ok(())
}
fn run_logout() -> Result<(), Box<dyn std::error::Error>> {
fn run_logout(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
clear_oauth_credentials()?;
println!("Claude OAuth credentials cleared.");
match output_format {
CliOutputFormat::Text => println!("Claude OAuth credentials cleared."),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "logout",
"message": "Claude OAuth credentials cleared.",
}))?
),
}
Ok(())
}
@@ -1361,21 +1473,50 @@ fn wait_for_oauth_callback(
Ok(callback)
}
fn print_system_prompt(cwd: PathBuf, date: String) {
match load_system_prompt(cwd, date, env::consts::OS, "unknown") {
Ok(sections) => println!("{}", sections.join("\n\n")),
Err(error) => {
eprintln!("failed to build system prompt: {error}");
std::process::exit(1);
}
fn print_system_prompt(
cwd: PathBuf,
date: String,
output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let sections = load_system_prompt(cwd, date, env::consts::OS, "unknown")?;
let message = sections.join(
"
",
);
match output_format {
CliOutputFormat::Text => println!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "system-prompt",
"message": message,
"sections": sections,
}))?
),
}
Ok(())
}
fn print_version() {
println!("{}", render_version_report());
fn print_version(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let report = render_version_report();
match output_format {
CliOutputFormat::Text => println!("{report}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "version",
"message": report,
"version": VERSION,
"git_sha": GIT_SHA,
"target": BUILD_TARGET,
}))?
),
}
Ok(())
}
fn resume_session(session_path: &Path, commands: &[String]) {
fn resume_session(session_path: &Path, commands: &[String], output_format: CliOutputFormat) {
let resolved_path = if session_path.exists() {
session_path.to_path_buf()
} else {
@@ -1425,7 +1566,35 @@ fn resume_session(session_path: &Path, commands: &[String]) {
}) => {
session = next_session;
if let Some(message) = message {
println!("{message}");
if output_format == CliOutputFormat::Json
&& matches!(command, SlashCommand::Status)
{
let tracker = UsageTracker::from_session(&session);
let usage = tracker.cumulative_usage();
let context = status_context(Some(&resolved_path)).expect("status context");
let value = json!({
"kind": "status",
"messages": session.messages.len(),
"turns": tracker.turns(),
"latest_total": tracker.current_turn_usage().total_tokens(),
"cumulative_input": usage.input_tokens,
"cumulative_output": usage.output_tokens,
"cumulative_total": usage.total_tokens(),
"workspace": {
"cwd": context.cwd,
"project_root": context.project_root,
"git_branch": context.git_branch,
"git_state": context.git_summary.headline(),
"session": context.session_path.as_ref().map_or_else(|| "live-repl".to_string(), |path| path.display().to_string()),
}
});
println!(
"{}",
serde_json::to_string_pretty(&value).expect("status json")
);
} else {
println!("{message}");
}
}
}
Err(error) => {
@@ -2357,13 +2526,7 @@ impl RuntimeMcpState {
fn build_runtime_mcp_state(
runtime_config: &runtime::RuntimeConfig,
) -> Result<
(
Option<Arc<Mutex<RuntimeMcpState>>>,
Vec<RuntimeToolDefinition>,
),
Box<dyn std::error::Error>,
> {
) -> Result<RuntimePluginStateBuildOutput, Box<dyn std::error::Error>> {
let Some((mcp_state, discovery)) = RuntimeMcpState::new(runtime_config)? else {
return Ok((None, Vec::new()));
};
@@ -2809,7 +2972,7 @@ impl LiveCli {
false
}
SlashCommand::Init => {
run_init()?;
run_init(CliOutputFormat::Text)?;
false
}
SlashCommand::Diff => {
@@ -2817,7 +2980,7 @@ impl LiveCli {
false
}
SlashCommand::Version => {
Self::print_version();
Self::print_version(CliOutputFormat::Text);
false
}
SlashCommand::Export { path } => {
@@ -2831,7 +2994,7 @@ impl LiveCli {
self.handle_plugins_command(action.as_deref(), target.as_deref())?
}
SlashCommand::Agents { args } => {
Self::print_agents(args.as_deref())?;
Self::print_agents(args.as_deref(), CliOutputFormat::Text)?;
false
}
SlashCommand::Skills { args } => {
@@ -3113,9 +3276,23 @@ impl LiveCli {
Ok(())
}
fn print_agents(args: Option<&str>) -> Result<(), Box<dyn std::error::Error>> {
fn print_agents(
args: Option<&str>,
output_format: CliOutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let cwd = env::current_dir()?;
println!("{}", handle_agents_slash_command(args, &cwd)?);
let message = handle_agents_slash_command(args, &cwd)?;
match output_format {
CliOutputFormat::Text => println!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "agents",
"message": message,
"args": args,
}))?
),
}
Ok(())
}
@@ -3154,8 +3331,8 @@ impl LiveCli {
Ok(())
}
fn print_version() {
println!("{}", render_version_report());
fn print_version(output_format: CliOutputFormat) {
let _ = crate::print_version(output_format);
}
fn export_session(
@@ -4060,8 +4237,18 @@ fn init_claude_md() -> Result<String, Box<dyn std::error::Error>> {
Ok(initialize_repo(&cwd)?.render())
}
fn run_init() -> Result<(), Box<dyn std::error::Error>> {
println!("{}", init_claude_md()?);
fn run_init(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let message = init_claude_md()?;
match output_format {
CliOutputFormat::Text => println!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "init",
"message": message,
}))?
),
}
Ok(())
}
@@ -5916,16 +6103,13 @@ impl CliToolExecutor {
fn execute_search_tool(&self, value: serde_json::Value) -> Result<String, ToolError> {
let input: ToolSearchRequest = serde_json::from_value(value)
.map_err(|error| ToolError::new(format!("invalid tool input JSON: {error}")))?;
let (pending_mcp_servers, mcp_degraded) = self
.mcp_state
.as_ref()
.map(|state| {
let (pending_mcp_servers, mcp_degraded) =
self.mcp_state.as_ref().map_or((None, None), |state| {
let state = state
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
(state.pending_servers(), state.degraded_report())
})
.unwrap_or((None, None));
});
serde_json::to_string_pretty(&self.tool_registry.search(
&input.query,
input.max_results.unwrap_or(5),
@@ -6203,8 +6387,21 @@ fn print_help_to(out: &mut impl Write) -> io::Result<()> {
Ok(())
}
fn print_help() {
let _ = print_help_to(&mut io::stdout());
fn print_help(output_format: CliOutputFormat) -> Result<(), Box<dyn std::error::Error>> {
let mut buffer = Vec::new();
print_help_to(&mut buffer)?;
let message = String::from_utf8(buffer)?;
match output_format {
CliOutputFormat::Text => print!("{message}"),
CliOutputFormat::Json => println!(
"{}",
serde_json::to_string_pretty(&json!({
"kind": "help",
"message": message,
}))?
),
}
Ok(())
}
#[cfg(test)]
@@ -6513,11 +6710,15 @@ mod tests {
fn parses_version_flags_without_initializing_prompt_mode() {
assert_eq!(
parse_args(&["--version".to_string()]).expect("args should parse"),
CliAction::Version
CliAction::Version {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["-V".to_string()]).expect("args should parse"),
CliAction::Version
CliAction::Version {
output_format: CliOutputFormat::Text,
}
);
}
@@ -6579,6 +6780,7 @@ mod tests {
CliAction::PrintSystemPrompt {
cwd: PathBuf::from("/tmp/project"),
date: "2026-04-01".to_string(),
output_format: CliOutputFormat::Text,
}
);
}
@@ -6587,23 +6789,34 @@ mod tests {
fn parses_login_and_logout_subcommands() {
assert_eq!(
parse_args(&["login".to_string()]).expect("login should parse"),
CliAction::Login
CliAction::Login {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["logout".to_string()]).expect("logout should parse"),
CliAction::Logout
CliAction::Logout {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["doctor".to_string()]).expect("doctor should parse"),
CliAction::Doctor
CliAction::Doctor {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["init".to_string()]).expect("init should parse"),
CliAction::Init
CliAction::Init {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["agents".to_string()]).expect("agents should parse"),
CliAction::Agents { args: None }
CliAction::Agents {
args: None,
output_format: CliOutputFormat::Text
}
);
assert_eq!(
parse_args(&["mcp".to_string()]).expect("mcp should parse"),
@@ -6623,7 +6836,8 @@ mod tests {
parse_args(&["agents".to_string(), "--help".to_string()])
.expect("agents help should parse"),
CliAction::Agents {
args: Some("--help".to_string())
args: Some("--help".to_string()),
output_format: CliOutputFormat::Text,
}
);
}
@@ -6653,11 +6867,15 @@ mod tests {
std::env::remove_var("RUSTY_CLAUDE_PERMISSION_MODE");
assert_eq!(
parse_args(&["help".to_string()]).expect("help should parse"),
CliAction::Help
CliAction::Help {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["version".to_string()]).expect("version should parse"),
CliAction::Version
CliAction::Version {
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
parse_args(&["status".to_string()]).expect("status should parse"),
@@ -6727,7 +6945,10 @@ mod tests {
fn parses_direct_agents_mcp_and_skills_slash_commands() {
assert_eq!(
parse_args(&["/agents".to_string()]).expect("/agents should parse"),
CliAction::Agents { args: None }
CliAction::Agents {
args: None,
output_format: CliOutputFormat::Text
}
);
assert_eq!(
parse_args(&["/mcp".to_string(), "show".to_string(), "demo".to_string()])
@@ -6807,6 +7028,7 @@ mod tests {
CliAction::ResumeSession {
session_path: PathBuf::from("session.jsonl"),
commands: vec!["/compact".to_string()],
output_format: CliOutputFormat::Text,
}
);
}
@@ -6818,6 +7040,7 @@ mod tests {
CliAction::ResumeSession {
session_path: PathBuf::from("latest"),
commands: vec![],
output_format: CliOutputFormat::Text,
}
);
assert_eq!(
@@ -6826,6 +7049,7 @@ mod tests {
CliAction::ResumeSession {
session_path: PathBuf::from("latest"),
commands: vec!["/status".to_string()],
output_format: CliOutputFormat::Text,
}
);
}
@@ -6848,6 +7072,7 @@ mod tests {
"/compact".to_string(),
"/cost".to_string(),
],
output_format: CliOutputFormat::Text,
}
);
}
@@ -6878,6 +7103,7 @@ mod tests {
"/export notes.txt".to_string(),
"/clear --confirm".to_string(),
],
output_format: CliOutputFormat::Text,
}
);
}
@@ -6896,6 +7122,7 @@ mod tests {
CliAction::ResumeSession {
session_path: PathBuf::from("session.jsonl"),
commands: vec!["/export /tmp/notes.txt".to_string(), "/status".to_string()],
output_format: CliOutputFormat::Text,
}
);
}
@@ -8005,6 +8232,7 @@ UU conflicted.rs",
}
#[test]
#[allow(clippy::too_many_lines)]
fn build_runtime_plugin_state_discovers_mcp_tools_and_surfaces_pending_servers() {
let config_home = temp_dir();
let workspace = temp_dir();