harness-app + harness-tui: composition root and harness run -p debug command
App::init wires config loading, an in-memory Store, EventBus, PermissionService with the M1 auto-approve stub frontend, the full built-in ToolRegistry, and a ProviderRegistry populated with AnthropicProvider when an API key is available (config or ANTHROPIC_API_KEY). App::run_prompt creates a root session, appends the prompt as a user message, and drives engine::run_session to completion; final_text reads back the concatenated text parts of the last message. harness-tui's `harness` binary gains a `run -p "<prompt>" [-m provider/model]` subcommand built on this. Manually verified end-to-end against the real Anthropic API: an empty API key produced a genuine 401 that our SSE error path correctly classified as ProviderError::Auth and surfaced as a clean CLI error message (exit 1) -- confirming the full request/header/error- handling pipeline works against the live service, not just fixtures. This closes M1 (docs/10-milestones.md): headless core loop + Anthropic provider, config loading, all six built-in tools, and the debug CLI. 131 tests passing across the workspace, clippy clean, fmt clean.
This commit is contained in:
@@ -10,6 +10,8 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
harness-app = { workspace = true }
|
||||
harness-core = { workspace = true }
|
||||
tokio = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -1,3 +1,86 @@
|
||||
fn main() {
|
||||
println!("harness {}", env!("CARGO_PKG_VERSION"));
|
||||
use harness_core::event::RunOutcome;
|
||||
|
||||
const DEFAULT_MODEL: &str = "anthropic/claude-sonnet-4-5";
|
||||
|
||||
fn print_usage() {
|
||||
eprintln!("usage: harness run -p \"<prompt>\" [-m provider/model]");
|
||||
}
|
||||
|
||||
fn parse_run_args(args: &[String]) -> Option<(String, Option<String>)> {
|
||||
let mut prompt = None;
|
||||
let mut model = None;
|
||||
let mut i = 0;
|
||||
while i < args.len() {
|
||||
match args[i].as_str() {
|
||||
"-p" | "--prompt" => {
|
||||
prompt = args.get(i + 1).cloned();
|
||||
i += 2;
|
||||
}
|
||||
"-m" | "--model" => {
|
||||
model = args.get(i + 1).cloned();
|
||||
i += 2;
|
||||
}
|
||||
_ => i += 1,
|
||||
}
|
||||
}
|
||||
prompt.map(|p| (p, model))
|
||||
}
|
||||
|
||||
async fn run(args: &[String]) -> i32 {
|
||||
let Some((prompt, model_arg)) = parse_run_args(args) else {
|
||||
print_usage();
|
||||
return 2;
|
||||
};
|
||||
|
||||
let cwd = match std::env::current_dir() {
|
||||
Ok(cwd) => cwd,
|
||||
Err(e) => {
|
||||
eprintln!("error: could not read cwd: {e}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
let app = match harness_app::App::init(cwd) {
|
||||
Ok(app) => app,
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
return 1;
|
||||
}
|
||||
};
|
||||
|
||||
let model_ref = model_arg
|
||||
.or_else(|| app.config.model.clone())
|
||||
.unwrap_or_else(|| DEFAULT_MODEL.to_string());
|
||||
|
||||
match app.run_prompt(prompt, &model_ref).await {
|
||||
Ok((RunOutcome::Stopped, session_id)) => {
|
||||
let text = app.final_text(&session_id).await.unwrap_or_default();
|
||||
println!("{text}");
|
||||
0
|
||||
}
|
||||
Ok((RunOutcome::Aborted, _)) => {
|
||||
eprintln!("run aborted");
|
||||
1
|
||||
}
|
||||
Ok((RunOutcome::Errored { message }, _)) => {
|
||||
eprintln!("error: {message}");
|
||||
1
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("error: {e}");
|
||||
1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main(flavor = "current_thread")]
|
||||
async fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
let exit_code = if args.first().map(String::as_str) == Some("run") {
|
||||
run(&args[1..]).await
|
||||
} else {
|
||||
println!("harness {}", env!("CARGO_PKG_VERSION"));
|
||||
0
|
||||
};
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user