1#![recursion_limit = "256"]
2
3pub mod acp;
4mod bootstrap;
5pub mod cli;
6mod cli_bytecode;
7pub mod commands;
8mod compiler_context;
9#[doc(hidden)]
10pub mod dispatch;
11pub mod env_guard;
12pub mod format;
13pub mod json_envelope;
14mod net;
15pub mod package;
16mod provider_bootstrap;
17mod runtime;
18pub mod skill_loader;
19pub mod skill_provenance;
20pub mod test_report;
21pub mod test_runner;
22pub mod test_timing;
23#[doc(hidden)]
24pub mod tests;
25mod typecheck_imports;
26pub use commands::dispatch_explain::DISPATCH_AUDIT_SCHEMA_VERSION;
27pub(crate) use compiler_context::{
28 compiler_for_source, compiler_with_imported_enum_candidates,
29 ensure_builtin_signatures_installed, imported_enum_candidates_for_source,
30};
31pub use harn_skills::{get_embedded_skill, list_embedded_skills, EmbeddedSkill, SkillFrontmatter};
32
33use clap::{error::ErrorKind, CommandFactory, Parser as ClapParser};
34use std::path::{Path, PathBuf};
35use std::sync::{Arc, Once};
36use std::{env, fs, panic, process, thread};
37
38use cli::{
39 Cli, Command, CompletionShell, EvalCommand, GuardCommand, MergeCaptainCommand,
40 MergeCaptainMockCommand, ModelInfoArgs, PackageArtifactsCommand, PackageCacheCommand,
41 PackageCommand, PackageScaffoldCommand, PgCommand, ProviderCommand, SkillCommand,
42 SkillKeyCommand, SkillTrustCommand, TimeCommand, ToolCommand,
43};
44use harn_lexer::Lexer;
45use harn_modules::project_config;
46use harn_parser::{DiagnosticSeverity, Parser, TypeChecker};
47use runtime::{build_cli_runtime, cli_runtime_mode, CliRuntimeMode};
48
49pub const CLI_RUNTIME_STACK_SIZE: usize = 16 * 1024 * 1024;
50static BROKEN_PIPE_PANIC_HOOK: Once = Once::new();
51
52#[cfg(feature = "hostlib")]
53pub(crate) fn install_default_hostlib(vm: &mut harn_vm::Vm) {
54 let _ = harn_hostlib::install_default(vm);
55 harn_rules_hostlib::install(vm);
59}
60
61#[cfg(not(feature = "hostlib"))]
62pub(crate) fn install_default_hostlib(_vm: &mut harn_vm::Vm) {}
63
64pub fn run() {
67 install_broken_pipe_panic_hook();
68 harn_vm::initialize_runtime_assets();
69 let raw_args = normalize_serve_args(bootstrap::args_after_pre_runtime_command());
70 harn_vm::stdlib::force_link();
75
76 ensure_builtin_signatures_installed();
77
78 let runtime_mode = cli_runtime_mode(&raw_args);
79
80 let handle = thread::Builder::new()
81 .name("harn-cli".to_string())
82 .stack_size(CLI_RUNTIME_STACK_SIZE)
83 .spawn(move || {
84 let runtime = build_cli_runtime(runtime_mode);
85 runtime.block_on(async_main(raw_args, runtime_mode));
86 if let Err(error) = harn_vm::events::shutdown_otel_sink() {
93 eprintln!("[harn] OTel exporter shutdown failed: {error}");
94 }
95 })
96 .unwrap_or_else(|error| {
97 eprintln!("failed to start CLI runtime thread: {error}");
98 process::exit(1);
99 });
100
101 if let Err(payload) = handle.join() {
102 if runtime::is_broken_pipe_panic_payload(payload.as_ref()) {
103 process::exit(0);
104 }
105 std::panic::resume_unwind(payload);
106 }
107}
108
109fn install_broken_pipe_panic_hook() {
110 BROKEN_PIPE_PANIC_HOOK.call_once(|| {
111 let previous = panic::take_hook();
112 panic::set_hook(Box::new(move |info| {
113 if runtime::is_broken_pipe_panic_payload(info.payload()) {
114 return;
115 }
116 previous(info);
117 }));
118 });
119}
120
121#[allow(clippy::large_stack_frames)] async fn async_main(raw_args: Vec<String>, runtime_mode: CliRuntimeMode) {
123 if runtime_mode.enables_tokio_io() {
130 if let Err(error) = harn_vm::events::install_otel_sink_from_env() {
131 eprintln!("[harn] OTel exporter disabled: {error}");
132 }
133 }
134
135 if raw_args.len() == 2 && raw_args[1].ends_with(".harn") {
136 provider_bootstrap::maybe_seed_ollama_for_run_file(Path::new(&raw_args[1]), false, false)
137 .await;
138 commands::run::run_file(
139 &raw_args[1],
140 false,
141 std::collections::HashSet::new(),
142 Vec::new(),
143 commands::run::CliLlmMockMode::Off,
144 None,
145 commands::run::RunProfileOptions::default(),
146 )
147 .await;
148 return;
149 }
150
151 let cli = match Cli::try_parse_from(&raw_args) {
152 Ok(cli) => cli,
153 Err(error) => {
154 if matches!(
155 error.kind(),
156 ErrorKind::DisplayHelp | ErrorKind::DisplayVersion
157 ) {
158 error.exit();
159 }
160 error.exit();
161 }
162 };
163
164 if cli.json_schemas {
165 commands::json_schemas::run(cli.schema_command.as_deref());
166 return;
167 }
168
169 let Some(subcommand) = cli.command else {
170 let mut cmd = Cli::command();
174 cmd.print_help().ok();
175 return;
176 };
177 match subcommand {
178 Command::Version(args) => {
179 let exit = run_version(args).await;
180 if exit != 0 {
181 process::exit(exit);
182 }
183 }
184 Command::Upgrade(args) => {
185 if let Err(error) = commands::upgrade::run(args).await {
186 eprintln!("error: {error}");
187 process::exit(1);
188 }
189 }
190 Command::Dap(_) => run_dap_adapter(),
191 Command::ConformanceHelper(args) => {
192 if let Err(error) = commands::conformance_helper::run(args).await {
193 eprintln!("error: {error}");
194 process::exit(1);
195 }
196 }
197 Command::Skill(args) => match args.command {
198 SkillCommand::List(list) => commands::skills::run_list(&list),
199 SkillCommand::Get(get) => commands::skills::run_get(&get),
200 SkillCommand::Dump(dump) => commands::skills::run_dump(&dump),
201 SkillCommand::Resolved(resolved) => commands::skills::run_resolved(&resolved),
202 SkillCommand::Inspect(inspect) => commands::skills::run_inspect(&inspect),
203 SkillCommand::Match(matcher) => commands::skills::run_match(&matcher),
204 SkillCommand::Install(install) => commands::skills::run_install(&install),
205 SkillCommand::New(new_args) => commands::skills::run_new(&new_args),
206 SkillCommand::Key(key_args) => match key_args.command {
207 SkillKeyCommand::Generate(generate) => commands::skill::run_key_generate(&generate),
208 },
209 SkillCommand::Sign(sign) => commands::skill::run_sign(&sign),
210 SkillCommand::Endorse(endorse) => commands::skill::run_endorse(&endorse),
211 SkillCommand::Verify(verify) => commands::skill::run_verify(&verify),
212 SkillCommand::WhoSigned(who_signed) => {
213 commands::skill::run_who_signed(&who_signed).await;
214 }
215 SkillCommand::Trust(trust_args) => match trust_args.command {
216 SkillTrustCommand::Add(add) => commands::skill::run_trust_add(&add),
217 SkillTrustCommand::List(list) => commands::skill::run_trust_list(&list),
218 },
219 },
220 Command::Guard(args) => match args.command {
221 GuardCommand::List(list_args) => commands::guard::run_list(&list_args),
222 GuardCommand::Install(install_args) => {
223 commands::guard::run_install(&install_args).await;
224 }
225 GuardCommand::Status(status_args) => commands::guard::run_status(&status_args),
226 GuardCommand::Remove(remove_args) => commands::guard::run_remove(&remove_args),
227 },
228 Command::Run(args) => {
229 let _operator_approval_guard = args.install_operator_approval_grant();
230 if !args.explain_cost {
231 match (args.eval.as_deref(), args.file.as_deref()) {
232 (Some(code), None) => {
233 provider_bootstrap::maybe_seed_ollama_for_inline(
234 code,
235 args.yes,
236 args.llm_mock.is_some(),
237 )
238 .await;
239 }
240 (None, Some(file)) => {
241 provider_bootstrap::maybe_seed_ollama_for_run_file(
242 Path::new(file),
243 args.yes,
244 args.llm_mock.is_some(),
245 )
246 .await;
247 }
248 _ => {}
249 }
250 }
251 let denied =
252 commands::run::build_denied_builtins(args.deny.as_deref(), args.allow.as_deref());
253 let llm_mock_mode = if let Some(path) = args.llm_mock.as_ref() {
254 commands::run::CliLlmMockMode::Replay {
255 fixture_path: PathBuf::from(path),
256 }
257 } else if let Some(path) = args.llm_mock_record.as_ref() {
258 commands::run::CliLlmMockMode::Record {
259 fixture_path: PathBuf::from(path),
260 }
261 } else {
262 commands::run::CliLlmMockMode::Off
263 };
264 let attestation = args.attest.then(|| commands::run::RunAttestationOptions {
265 receipt_out: args.receipt_out.as_ref().map(PathBuf::from),
266 agent_id: args.attest_agent.clone(),
267 });
268 let profile_options = run_profile_options(&args.profile);
269 let sandbox_options = commands::run::sandbox::sandbox_options_from_args(&args.sandbox);
270 let json_options = args
271 .json
272 .then_some(commands::run::RunJsonOptions { quiet: args.quiet });
273 let aux_options = commands::run::run_aux_options_from_args(&args);
274 let control_options = commands::run::run_control_options_from_args(&args);
275 let harnpack_options = commands::run::harnpack::HarnpackRunOptions {
276 allow_unsigned: args.allow_unsigned,
277 dry_run_verify: args.dry_run_verify,
278 };
279 if let Some(resume_target) = args.resume.as_deref() {
280 let exit_code = commands::run::run_resume_with_skill_dirs(
281 resume_target,
282 args.trace,
283 denied,
284 args.argv.clone(),
285 args.skill_dir.clone(),
286 llm_mock_mode,
287 attestation,
288 profile_options,
289 sandbox_options.clone(),
290 json_options,
291 aux_options,
292 control_options,
293 )
294 .await;
295 runtime::exit_on_error(exit_code);
296 return;
297 }
298
299 if args.as_job {
300 run_as_job(&args).await;
301 return;
302 }
303
304 match (args.eval.as_deref(), args.file.as_deref()) {
305 (Some(code), None) => {
306 if args.allow_unsigned || args.dry_run_verify {
307 command_error(
308 "`--allow-unsigned` and `--dry-run-verify` apply to `.harnpack` inputs; \
309 they cannot be combined with `-e`",
310 );
311 }
312 let (wrapped, tmp) = commands::run::prepare_eval_temp_file(code)
313 .unwrap_or_else(|e| command_error(&e));
314 let tmp_path: PathBuf = tmp.path().to_path_buf();
315 if let Err(error) = fs::write(&tmp_path, &wrapped) {
316 drop(tmp);
317 command_error(&format!("failed to write temp file for -e: {error}"));
318 }
319 let tmp_str = tmp_path.to_string_lossy().into_owned();
320 let exit_code = if args.explain_cost {
321 commands::run::run_explain_cost_file_with_skill_dirs(&tmp_str)
322 } else {
323 commands::run::run_file_with_skill_dirs(
324 &tmp_str,
325 args.trace,
326 denied,
327 args.argv.clone(),
328 args.skill_dir.clone(),
329 llm_mock_mode.clone(),
330 attestation.clone(),
331 profile_options.clone(),
332 sandbox_options.clone(),
333 json_options.clone(),
334 aux_options.clone(),
335 control_options.clone(),
336 harnpack_options.clone(),
337 )
338 .await
339 };
340 drop(tmp);
341 runtime::exit_on_error(exit_code);
342 }
343 (None, Some(file)) => {
344 let exit_code = if args.explain_cost {
345 commands::run::run_explain_cost_file_with_skill_dirs(file)
346 } else {
347 commands::run::run_file_with_skill_dirs(
348 file,
349 args.trace,
350 denied,
351 args.argv.clone(),
352 args.skill_dir.clone(),
353 llm_mock_mode,
354 attestation,
355 profile_options,
356 sandbox_options,
357 json_options,
358 aux_options,
359 control_options,
360 harnpack_options,
361 )
362 .await
363 };
364 runtime::exit_on_error(exit_code);
365 }
366 (Some(_), Some(_)) => command_error(
367 "`harn run` accepts either `-e <code>` or `<file.harn>`, not both",
368 ),
369 (None, None) => command_error(
370 "`harn run` requires `--resume <snapshot>`, `-e <code>`, or `<file.harn>`",
371 ),
372 }
373 }
374 Command::Check(args) => commands::check::run_check_command(args),
375 Command::Parse(args) => {
376 if let Err(error) = commands::parse_tokens::run_parse(&args) {
377 command_error(&error);
378 }
379 }
380 Command::Tokens(args) => {
381 if let Err(error) = commands::parse_tokens::run_tokens(&args) {
382 command_error(&error);
383 }
384 }
385 Command::Config(args) => {
386 if let Err(error) = commands::config_cmd::run(args).await {
387 command_error(&error);
388 }
389 }
390 Command::Explain(args) => {
391 let code = commands::explain::run_explain(&args).await;
392 if code != 0 {
393 process::exit(code);
394 }
395 }
396 Command::Fix(args) => {
397 if let Err(error) = commands::fix::run(&args) {
398 if error.is_partial_failure() {
399 eprintln!("error: {}", error.message());
400 process::exit(1);
401 }
402 command_error(error.message());
403 }
404 }
405 Command::Contracts(args) => {
406 commands::contracts::handle_contracts_command(args).await;
407 }
408 Command::Connect(args) => {
409 commands::connect::run_connect(*args).await;
410 }
411 Command::Lint(args) => {
412 if commands::check::run_changed_lint_command(&args).await {
413 return;
414 }
415 let targets: Vec<&str> = args.targets.iter().map(String::as_str).collect();
416 let (files, prompt_files) = commands::check::collect_lint_targets(&targets);
417 if files.is_empty() && prompt_files.is_empty() {
418 if args.json {
419 print_lint_error(
420 "no_lint_targets",
421 "no .harn, .harn.txt, or .harn.prompt files found under the given target(s)",
422 );
423 }
424 command_error(
425 "no .harn, .harn.txt, or .harn.prompt files found under the given target(s)",
426 );
427 }
428 if args.json {
429 let outcome = commands::check::run_lint_json(
430 &files,
431 commands::check::LintJsonOptions {
432 strict: args.strict,
433 require_file_header: args.require_file_header,
434 require_public_api_types: args.require_public_api_types,
435 },
436 )
437 .await;
438 println!("{}", json_envelope::to_string_pretty(&outcome.envelope));
439 runtime::exit_on_error(outcome.exit_code);
440 return;
441 }
442 let mut analysis = harn_parser::analysis::AnalysisDatabase::new();
443 let module_graph =
444 commands::check::build_module_graph_and_seed_analysis(&files, &mut analysis);
445 let cross_file_imports = commands::check::collect_cross_file_imports(&module_graph);
446 let script_rule_diags = commands::check::run_project_script_rules(&files).await;
448 let script_diags_for = |file: &std::path::Path| -> &[harn_lint::LintDiagnostic] {
449 script_rule_diags
450 .get(file)
451 .map(Vec::as_slice)
452 .unwrap_or(&[])
453 };
454 if args.fix {
455 let mut should_fail = false;
456 for file in &files {
457 let mut config = package::load_check_config(Some(file));
458 let mut lint_config = commands::check::load_harn_lint_config(file);
459 lint_config.require_file_header |= args.require_file_header;
460 lint_config.require_public_api_types |= args.require_public_api_types;
461 commands::check::apply_loaded_harn_lint_config(&lint_config, &mut config);
462 let outcome = commands::check::lint_fix_file(
463 &mut analysis,
464 file,
465 &config,
466 &cross_file_imports,
467 &module_graph,
468 &lint_config,
469 );
470 should_fail |= outcome.should_fail(config.strict || args.strict);
471 }
472 for file in &prompt_files {
473 let lint_config = commands::check::load_harn_lint_config(file);
474 let config = package::load_check_config(Some(file));
475 let outcome = commands::check::lint_prompt_file_inner(
477 file,
478 lint_config.template_variant_branch_threshold,
479 &lint_config.disabled,
480 );
481 should_fail |= outcome.should_fail(config.strict || args.strict);
482 }
483 if should_fail {
489 process::exit(1);
490 }
491 } else {
492 let mut should_fail = false;
493 let mut total_findings = 0usize;
494 let mut total_fixable = 0usize;
495 for file in &files {
496 let mut config = package::load_check_config(Some(file));
497 let mut lint_config = commands::check::load_harn_lint_config(file);
498 lint_config.require_file_header |= args.require_file_header;
499 lint_config.require_public_api_types |= args.require_public_api_types;
500 commands::check::apply_loaded_harn_lint_config(&lint_config, &mut config);
501 let outcome = commands::check::lint_file_inner(
502 &mut analysis,
503 file,
504 &config,
505 &cross_file_imports,
506 &module_graph,
507 &lint_config,
508 script_diags_for(file.as_path()),
509 );
510 total_findings += outcome.findings;
511 total_fixable += outcome.fixable;
512 should_fail |= outcome.should_fail(config.strict || args.strict);
513 }
514 for file in &prompt_files {
515 let lint_config = commands::check::load_harn_lint_config(file);
516 let config = package::load_check_config(Some(file));
517 let outcome = commands::check::lint_prompt_file_inner(
518 file,
519 lint_config.template_variant_branch_threshold,
520 &lint_config.disabled,
521 );
522 total_findings += outcome.findings;
523 total_fixable += outcome.fixable;
524 should_fail |= outcome.should_fail(config.strict || args.strict);
525 }
526 if total_fixable > 0 {
529 if total_fixable == total_findings {
530 eprintln!(
531 "\nAll {total_fixable} finding(s) are auto-fixable — run `harn lint --fix` to apply them."
532 );
533 } else {
534 eprintln!(
535 "\n{total_fixable} of {total_findings} finding(s) are auto-fixable — run `harn lint --fix` to apply them."
536 );
537 }
538 }
539 if should_fail {
540 process::exit(1);
541 }
542 }
543 }
544 Command::Fmt(args) => {
545 let targets: Vec<&str> = args.targets.iter().map(String::as_str).collect();
546 let anchor = targets.first().map(Path::new).unwrap_or(Path::new("."));
549 let loaded = match project_config::load_for_path(anchor) {
550 Ok(c) => c,
551 Err(e) => {
552 eprintln!("warning: {e}");
553 project_config::HarnConfig::default()
554 }
555 };
556 let mut opts = harn_fmt::FmtOptions::default();
557 if let Some(w) = loaded.fmt.line_width {
558 opts.line_width = w;
559 }
560 if let Some(w) = loaded.fmt.separator_width {
561 opts.separator_width = w;
562 }
563 if let Some(w) = args.line_width {
564 opts.line_width = w;
565 }
566 if let Some(w) = args.separator_width {
567 opts.separator_width = w;
568 }
569 let mode = commands::check::FmtMode::from_check_flag(args.check);
570 if args.json {
571 let envelope = commands::check::fmt_targets_json(&targets, mode, &opts);
572 let failed = !envelope.ok;
573 println!("{}", json_envelope::to_string_pretty(&envelope));
574 if failed {
575 process::exit(1);
576 }
577 } else {
578 commands::check::fmt_targets(&targets, mode, &opts);
579 }
580 }
581 Command::Test(args) => commands::test::run_command(args).await,
582 Command::Init(args) => {
583 commands::init::init_project(args.name.as_deref(), args.template).await;
584 }
585 Command::New(args) => match commands::init::resolve_new_args(&args) {
586 Ok((name, template)) => commands::init::init_project(name.as_deref(), template).await,
587 Err(error) => {
588 eprintln!("error: {error}");
589 process::exit(1);
590 }
591 },
592 Command::Doctor(args) => {
593 commands::doctor::run_doctor_with_options(commands::doctor::DoctorOptions {
594 json: args.json,
595 check_providers: args.check_providers,
596 check_targets: args.check_targets,
597 })
598 .await;
599 }
600 Command::Host(args) => {
601 let exit = commands::host::run(args).await;
602 if exit != 0 {
603 process::exit(exit);
604 }
605 }
606 Command::Models(args) => commands::models::run(args).await,
607 Command::Local(args) => commands::local::run(args).await,
608 Command::Provider(args) => match args.command {
609 ProviderCommand::Capabilities(capabilities) => {
610 commands::provider_capabilities::run_or_exit(capabilities);
611 }
612 ProviderCommand::Catalog(catalog) => {
613 commands::providers::dispatch_catalog(catalog.command).await;
614 }
615 ProviderCommand::Ready(ready) => {
616 run_provider_ready(
617 &ready.provider,
618 ready.model.as_deref(),
619 ready.base_url.as_deref(),
620 ready.json,
621 )
622 .await;
623 }
624 ProviderCommand::Probe(probe) => commands::provider::run_provider_probe(probe).await,
625 ProviderCommand::ToolProbe(args) => commands::provider::run_tool_probe(args).await,
626 ProviderCommand::ToolCalibrate(args) => commands::run_tool_calibrate(args).await,
627 ProviderCommand::ToolProbeAudit(args) => commands::providers::run_audit(args),
628 ProviderCommand::ToolScorecard(tool_scorecard) => {
629 commands::provider::run_provider_tool_scorecard(tool_scorecard).await;
630 }
631 ProviderCommand::CacheProbe(cache_probe) => {
632 commands::provider::run_provider_cache_probe(cache_probe).await;
633 }
634 ProviderCommand::DispatchExplain(explain) => commands::dispatch_explain::run(&explain),
635 ProviderCommand::DispatchAudit(audit) => commands::dispatch_explain::run_audit(&audit),
636 ProviderCommand::Limits(limits) => {
637 commands::provider_limits::run(&limits);
638 }
639 },
640 Command::Scan(args) => commands::scan::run(args).await,
641 Command::Codemod(args) => commands::codemod::run(args).await,
642 Command::Rule(args) => commands::rule::run(args).await,
643 Command::Try(args) => commands::try_cmd::run(args).await,
644 Command::Quickstart(args) => {
645 if let Err(error) = commands::quickstart::run_quickstart(&args).await {
646 command_error(&error);
647 }
648 }
649 Command::Demo(args) => {
650 let code = commands::demo::run(args).await;
651 if code != 0 {
652 process::exit(code);
653 }
654 }
655 Command::Serve(args) => commands::serve::run_command(args.command).await,
656 Command::Connector(args) => {
657 if let Err(error) = commands::connector::handle_connector_command(args).await {
658 eprintln!("error: {error}");
659 process::exit(1);
660 }
661 }
662 Command::Mcp(args) => commands::mcp::handle_mcp_command(&args.command).await,
663 Command::Watch(args) => {
664 let denied =
665 commands::run::build_denied_builtins(args.deny.as_deref(), args.allow.as_deref());
666 commands::run::run_watch(&args.file, denied).await;
667 }
668 Command::Dev(args) => {
669 commands::dev::run(args).await;
670 }
671 Command::Portal(args) => {
672 commands::portal::run_portal(
673 &args.dir,
674 args.manifest,
675 args.persona_state_dir,
676 &args.host,
677 args.port,
678 args.open,
679 args.allow_remote_launch,
680 )
681 .await;
682 }
683 Command::Trigger(args) => {
684 if let Err(error) = commands::trigger::handle(args).await {
685 eprintln!("error: {error}");
686 process::exit(1);
687 }
688 }
689 Command::Graph(args) => {
690 let code = commands::graph::run(args).await;
691 if code != 0 {
692 process::exit(code);
693 }
694 }
695 Command::Doc(args) => {
696 let code = commands::doc::run(&args.path, args.output.as_deref());
697 if code != 0 {
698 process::exit(code);
699 }
700 }
701 Command::Routes(args) => {
702 let code = commands::routes::run(args).await;
703 if code != 0 {
704 process::exit(code);
705 }
706 }
707 Command::Usage(args) => {
708 let code = commands::usage::run(args).await;
709 if code != 0 {
710 process::exit(code);
711 }
712 }
713 Command::Flow(args) => match commands::flow::run_flow(&args) {
714 Ok(code) => {
715 if code != 0 {
716 process::exit(code);
717 }
718 }
719 Err(error) => command_error(&error),
720 },
721 Command::Canon(args) => commands::canon::run(args).await,
722 Command::Workflow(args) => match commands::workflow::handle(args) {
723 Ok(code) => {
724 if code != 0 {
725 process::exit(code);
726 }
727 }
728 Err(error) => command_error(&error),
729 },
730 Command::Supervisor(args) => {
731 if let Err(error) = commands::supervisor::handle(args).await {
732 eprintln!("error: {error}");
733 process::exit(1);
734 }
735 }
736 Command::Trace(args) => {
737 if let Err(error) = commands::trace::handle(args).await {
738 eprintln!("error: {error}");
739 process::exit(1);
740 }
741 }
742 Command::Crystallize(args) => {
743 if let Err(error) = commands::crystallize::run(args) {
744 eprintln!("error: {error}");
745 process::exit(1);
746 }
747 }
748 Command::Trust(args) => {
749 if let Err(error) = commands::trust::handle(args).await {
750 eprintln!("error: {error}");
751 process::exit(1);
752 }
753 }
754 Command::Verify(args) => {
755 if let Err(error) = verify_provenance_receipt(&args.receipt, args.json) {
756 eprintln!("error: {error}");
757 process::exit(1);
758 }
759 }
760 Command::Completion(args) => print_completions(args.shell),
761 Command::Orchestrator(args) => {
762 if let Err(error) = commands::orchestrator::handle(args).await {
763 eprintln!("error: {error}");
764 process::exit(1);
765 }
766 }
767 Command::Playground(args) => {
768 provider_bootstrap::maybe_seed_ollama_for_playground(
769 Path::new(&args.host),
770 Path::new(&args.script),
771 args.yes,
772 args.llm.is_some(),
773 args.llm_mock.is_some(),
774 )
775 .await;
776 let llm_mock_mode = if let Some(path) = args.llm_mock.as_ref() {
777 commands::run::CliLlmMockMode::Replay {
778 fixture_path: PathBuf::from(path),
779 }
780 } else if let Some(path) = args.llm_mock_record.as_ref() {
781 commands::run::CliLlmMockMode::Record {
782 fixture_path: PathBuf::from(path),
783 }
784 } else {
785 commands::run::CliLlmMockMode::Off
786 };
787 if let Err(error) = commands::playground::run_command(args, llm_mock_mode).await {
788 eprint!("{error}");
789 process::exit(1);
790 }
791 }
792 Command::Runs(args) => cli::run_runs_command(args).await,
793 Command::Session(args) => commands::session::run(args),
794 Command::Replay(args) => {
795 let exit = commands::replay::run(args);
796 if exit != 0 {
797 process::exit(exit);
798 }
799 }
800 Command::Eval(args) => match args.command {
801 Some(EvalCommand::CodingAgent(coding_agent_args)) => {
802 let code = commands::eval_coding_agent::run(coding_agent_args).await;
803 if code != 0 {
804 process::exit(code);
805 }
806 }
807 Some(EvalCommand::Context(context_args)) => {
808 let code = commands::eval_context::run(context_args).await;
809 if code != 0 {
810 process::exit(code);
811 }
812 }
813 Some(EvalCommand::Prompt(prompt_args)) => {
814 let code = commands::eval_prompt::run(prompt_args).await;
815 if code != 0 {
816 process::exit(code);
817 }
818 }
819 Some(EvalCommand::SkillGate(skill_gate_args)) => {
820 let code = commands::eval_skill_gate::run(skill_gate_args).await;
821 if code != 0 {
822 process::exit(code);
823 }
824 }
825 Some(EvalCommand::ScopeTriage(scope_args)) => {
826 process::exit(commands::eval_scope_triage::run(scope_args).await)
827 }
828 Some(EvalCommand::ToolCalls(tool_calls_args)) => {
829 let code = commands::eval_tool_calls::run(tool_calls_args).await;
830 if code != 0 {
831 process::exit(code);
832 }
833 }
834 None => {
835 let Some(path) = args.path else {
836 eprintln!("error: `harn eval` requires a path or a subcommand (e.g. `prompt`).\nSee `harn eval --help`.");
837 process::exit(2);
838 };
839 let llm_mock_mode = if let Some(path) = args.llm_mock.as_ref() {
840 commands::run::CliLlmMockMode::Replay {
841 fixture_path: PathBuf::from(path),
842 }
843 } else if let Some(path) = args.llm_mock_record.as_ref() {
844 commands::run::CliLlmMockMode::Record {
845 fixture_path: PathBuf::from(path),
846 }
847 } else {
848 commands::run::CliLlmMockMode::Off
849 };
850 eval_run_record(
851 &path,
852 args.compare.as_deref(),
853 args.structural_experiment.as_deref(),
854 &args.argv,
855 &llm_mock_mode,
856 );
857 }
858 },
859 Command::Repl => commands::repl::run_repl().await,
860 Command::Bench(args) => commands::bench::run(args).await,
861 Command::Precompile(args) => commands::precompile::run(args).await,
862 Command::Pack(args) => commands::pack::run(args),
863 Command::TestBench(args) => commands::test_bench::run(args.command).await,
864 Command::Viz(args) => commands::viz::run_viz(&args.file, args.output.as_deref()),
865 Command::Install(args) => package::install_packages(
866 args.frozen || args.locked || args.offline,
867 args.refetch.as_deref(),
868 args.offline,
869 args.json,
870 ),
871 Command::Add(args) => package::add_package_with_registry(
872 &args.name_or_spec,
873 args.alias.as_deref(),
874 args.git.as_deref(),
875 args.tag.as_deref(),
876 args.rev.as_deref(),
877 args.branch.as_deref(),
878 args.path.as_deref(),
879 args.registry.as_deref(),
880 ),
881 Command::Update(args) => {
882 package::update_packages(args.alias.as_deref(), args.all, args.json);
883 }
884 Command::Remove(args) => package::remove_package(&args.alias),
885 Command::Lock => package::lock_packages(),
886 Command::Package(args) => match args.command {
887 PackageCommand::List(list) => package::list_packages(list.json),
888 PackageCommand::Doctor(doctor) => package::doctor_packages(doctor.json),
889 PackageCommand::Search(search) => package::search_package_registry(
890 search.query.as_deref(),
891 search.registry.as_deref(),
892 search.json,
893 ),
894 PackageCommand::Info(info) => {
895 package::show_package_registry_info(
896 &info.name,
897 info.registry.as_deref(),
898 info.json,
899 );
900 }
901 PackageCommand::Check(check) => {
902 package::check_package(check.package.as_deref(), check.json);
903 }
904 PackageCommand::Pack(pack) => package::pack_package(
905 pack.package.as_deref(),
906 pack.output.as_deref(),
907 pack.dry_run,
908 pack.json,
909 ),
910 PackageCommand::Docs(docs) => package::generate_package_docs(
911 docs.package.as_deref(),
912 docs.output.as_deref(),
913 docs.check,
914 ),
915 PackageCommand::Cache(cache) => match cache.command {
916 PackageCacheCommand::List => package::list_package_cache(),
917 PackageCacheCommand::Clean(clean) => package::clean_package_cache(clean.all),
918 PackageCacheCommand::Verify(verify) => {
919 package::verify_package_cache(verify.materialized);
920 }
921 },
922 PackageCommand::Outdated(args) => package::outdated_packages(
923 args.refresh,
924 args.remote,
925 args.registry.as_deref(),
926 args.json,
927 ),
928 PackageCommand::Audit(args) => {
929 package::audit_packages(
930 args.registry.as_deref(),
931 args.skip_materialized,
932 args.json,
933 );
934 }
935 PackageCommand::Artifacts(args) => match args.command {
936 PackageArtifactsCommand::Manifest(manifest) => {
937 package::artifacts_manifest(manifest.output.as_deref());
938 }
939 PackageArtifactsCommand::Check(check) => {
940 package::artifacts_check(&check.manifest, check.json);
941 }
942 },
943 PackageCommand::Scaffold(args) => match args.command {
944 PackageScaffoldCommand::Openapi(openapi) => {
945 if let Err(error) = commands::package_scaffold::run_openapi(&openapi).await {
946 eprintln!("error: {error}");
947 process::exit(1);
948 }
949 }
950 },
951 },
952 Command::Publish(args) => package::publish_package(
953 args.package.as_deref(),
954 args.dry_run,
955 &args.remote,
956 &args.index_repo,
957 &args.index_path,
958 args.registry_name.as_deref(),
959 args.skip_index_pr,
960 args.registry.as_deref(),
961 args.json,
962 ),
963 Command::MergeCaptain(args) => match args.command {
964 MergeCaptainCommand::Run(run) => {
965 let code = commands::merge_captain::run_driver(&run);
966 if code != 0 {
967 process::exit(code);
968 }
969 }
970 MergeCaptainCommand::Ladder(ladder) => {
971 let code = commands::merge_captain::run_ladder(&ladder);
972 if code != 0 {
973 process::exit(code);
974 }
975 }
976 MergeCaptainCommand::Iterate(iterate) => {
977 let code = commands::merge_captain::run_iterate(&iterate);
978 if code != 0 {
979 process::exit(code);
980 }
981 }
982 MergeCaptainCommand::Audit(audit) => {
983 let code = commands::merge_captain::run_audit(&audit);
984 if code != 0 {
985 process::exit(code);
986 }
987 }
988 MergeCaptainCommand::Mock(mock) => {
989 let code = match mock {
990 MergeCaptainMockCommand::Init(args) => {
991 commands::merge_captain_mock::run_init(&args)
992 }
993 MergeCaptainMockCommand::Step(args) => {
994 commands::merge_captain_mock::run_step(&args)
995 }
996 MergeCaptainMockCommand::Status(args) => {
997 commands::merge_captain_mock::run_status(&args)
998 }
999 MergeCaptainMockCommand::Serve(args) => {
1000 commands::merge_captain_mock::run_serve(&args).await
1001 }
1002 MergeCaptainMockCommand::Cleanup(args) => {
1003 commands::merge_captain_mock::run_cleanup(&args)
1004 }
1005 MergeCaptainMockCommand::Scenarios => {
1006 commands::merge_captain_mock::run_scenarios()
1007 }
1008 };
1009 if code != 0 {
1010 process::exit(code);
1011 }
1012 }
1013 },
1014 Command::Pg(args) => match args.command {
1015 PgCommand::Codegen(codegen) => {
1016 let code = commands::pg_codegen::run(&codegen);
1017 if code != 0 {
1018 process::exit(code);
1019 }
1020 }
1021 },
1022 Command::Persona(args) => {
1023 if let Err(error) = commands::persona_dispatch::run(args).await {
1024 eprintln!("error: {error}");
1025 process::exit(1);
1026 }
1027 }
1028 Command::Tool(args) => match args.command {
1029 ToolCommand::New(new_args) => {
1030 if let Err(error) = commands::tool::run_new(&new_args).await {
1031 eprintln!("error: {error}");
1032 process::exit(1);
1033 }
1034 }
1035 },
1036 Command::DumpHighlightKeywords(args) => {
1037 commands::dump_highlight_keywords::run(&args.output, args.check);
1038 }
1039 Command::DumpTriggerQuickref(args) => {
1040 commands::dump_trigger_quickref::run(&args.output, args.check);
1041 }
1042 Command::DumpConnectorMatrix(args) => {
1043 commands::check::connector_matrix::run_docs(&args.output, &args.sources, args.check);
1044 }
1045 Command::DumpProtocolArtifacts(args) => {
1046 commands::dump_protocol_artifacts::run(
1047 &args.output_dir,
1048 args.check,
1049 args.artifact_version.as_deref(),
1050 );
1051 }
1052 Command::ConnectorSchemaCodegen(args) => {
1053 let code = commands::connector_schema_codegen::run(&args);
1054 if code != 0 {
1055 process::exit(code);
1056 }
1057 }
1058 Command::Time(args) => match args.command {
1059 TimeCommand::Run(time_args) => commands::time::run(time_args).await,
1060 },
1061 }
1062}
1063
1064fn run_profile_options(args: &cli::ProfileArgs) -> commands::run::RunProfileOptions {
1065 commands::run::RunProfileOptions {
1066 text: args.text,
1067 json_path: args.json_path.clone(),
1068 }
1069}
1070
1071fn print_completions(shell: CompletionShell) {
1072 let mut command = Cli::command();
1073 let shell = clap_complete::Shell::from(shell);
1074 clap_complete::generate(shell, &mut command, "harn", &mut std::io::stdout());
1075}
1076
1077fn normalize_serve_args(mut raw_args: Vec<String>) -> Vec<String> {
1086 if raw_args.len() > 2 && raw_args.get(1).is_some_and(|arg| arg == "serve") {
1087 let token = raw_args.get(2).map(String::as_str).unwrap_or_default();
1088 let is_explicit = token == "-h"
1089 || token == "--help"
1090 || serve_subcommand_names().iter().any(|name| name == token);
1091 if !is_explicit {
1092 raw_args.insert(2, "a2a".to_string());
1093 }
1094 }
1095 raw_args
1096}
1097
1098fn serve_subcommand_names() -> Vec<String> {
1100 use clap::CommandFactory;
1101 Cli::command()
1102 .find_subcommand("serve")
1103 .map(|serve| {
1104 serve
1105 .get_subcommands()
1106 .map(|sub| sub.get_name().to_string())
1107 .collect()
1108 })
1109 .unwrap_or_default()
1110}
1111
1112pub(crate) const VERSION_SCHEMA_VERSION: u32 = 1;
1115
1116fn run_dap_adapter() {
1125 thread::Builder::new()
1126 .name("harn-dap".to_string())
1127 .spawn(harn_dap::run)
1128 .expect("spawn harn-dap adapter thread")
1129 .join()
1130 .expect("harn-dap adapter thread panicked");
1131}
1132
1133async fn run_version(args: cli::VersionArgs) -> i32 {
1134 let _name = env_guard::ScopedEnvVar::set("HARN_BUILD_NAME", env!("CARGO_PKG_NAME"));
1135 let _version = env_guard::ScopedEnvVar::set("HARN_BUILD_VERSION", env!("CARGO_PKG_VERSION"));
1136 let _description =
1137 env_guard::ScopedEnvVar::set("HARN_BUILD_DESCRIPTION", env!("CARGO_PKG_DESCRIPTION"));
1138 let _revision =
1139 env_guard::ScopedEnvVar::set("HARN_BUILD_REVISION", env!("HARN_BUILD_REVISION"));
1140 let argv = if args.json {
1141 vec!["--json".to_string()]
1142 } else {
1143 Vec::new()
1144 };
1145 dispatch::dispatch_to_embedded_script("version", argv, args.json).await
1146}
1147
1148pub(crate) async fn print_model_info(args: &ModelInfoArgs) -> bool {
1149 let resolved = harn_vm::llm_config::resolve_model_info(&args.model);
1150 let api_key_result = harn_vm::llm::resolve_api_key(&resolved.provider);
1151 let api_key_set = api_key_result.is_ok();
1152 let api_key = api_key_result.unwrap_or_default();
1153 let context_window =
1154 harn_vm::llm::fetch_provider_max_context(&resolved.provider, &resolved.id, &api_key).await;
1155 let readiness = local_provider_readiness(&resolved.provider, &resolved.id, &api_key).await;
1156 let catalog = harn_vm::llm_config::model_catalog_entry(&resolved.id);
1157 let runtime_context_window = catalog
1158 .as_ref()
1159 .and_then(|entry| entry.runtime_context_window);
1160 let capabilities = harn_vm::llm::capabilities::lookup(&resolved.provider, &resolved.id);
1161 let batch_api =
1162 harn_vm::llm_config::effective_batch_api_supported(&resolved.provider, &capabilities);
1163 let mut payload = serde_json::json!({
1164 "alias": args.model,
1165 "id": resolved.id,
1166 "provider": resolved.provider,
1167 "resolved_alias": resolved.alias,
1168 "tool_format": resolved.tool_format,
1169 "tier": resolved.tier,
1170 "api_key_set": api_key_set,
1171 "context_window": context_window,
1172 "runtime_context_window": runtime_context_window,
1173 "readiness": readiness,
1174 "catalog": catalog,
1175 "capabilities": {
1176 "native_tools": capabilities.native_tools,
1177 "defer_loading": capabilities.defer_loading,
1178 "tool_search": capabilities.tool_search,
1179 "max_tools": capabilities.max_tools,
1180 "prompt_caching": capabilities.prompt_caching,
1181 "batch_api": batch_api,
1182 "batch_wire_format": if batch_api { capabilities.batch_wire_format } else { None },
1183 "batch_input_mode": if batch_api { capabilities.batch_input_mode } else { None },
1184 "batch_discount_percent": if batch_api { capabilities.batch_discount_percent } else { None },
1185 "batch_turnaround_hours": if batch_api { capabilities.batch_turnaround_hours } else { None },
1186 "vision": capabilities.vision,
1187 "vision_supported": capabilities.vision_supported,
1188 "audio": capabilities.audio,
1189 "pdf": capabilities.pdf,
1190 "files_api_supported": capabilities.files_api_supported,
1191 "json_schema": capabilities.json_schema,
1192 "prefers_xml_scaffolding": capabilities.prefers_xml_scaffolding,
1193 "prefers_markdown_scaffolding": capabilities.prefers_markdown_scaffolding,
1194 "structured_output_mode": capabilities.structured_output_mode,
1195 "supports_assistant_prefill": capabilities.supports_assistant_prefill,
1196 "prefers_role_developer": capabilities.prefers_role_developer,
1197 "prefers_xml_tools": capabilities.prefers_xml_tools,
1198 "thinking": !capabilities.thinking_modes.is_empty(),
1199 "thinking_block_style": capabilities.thinking_block_style,
1200 "thinking_modes": capabilities.thinking_modes,
1201 "interleaved_thinking_supported": capabilities.interleaved_thinking_supported,
1202 "anthropic_beta_features": capabilities.anthropic_beta_features,
1203 "preserve_thinking": capabilities.preserve_thinking,
1204 "server_parser": capabilities.server_parser,
1205 "honors_chat_template_kwargs": capabilities.honors_chat_template_kwargs,
1206 "recommended_endpoint": capabilities.recommended_endpoint,
1207 "text_tool_wire_format_supported": capabilities.text_tool_wire_format_supported,
1208 "preferred_tool_format": capabilities.preferred_tool_format,
1209 "tool_mode_parity": capabilities.tool_mode_parity,
1210 "tool_mode_parity_notes": capabilities.tool_mode_parity_notes,
1211 },
1212 "qc_default_model": harn_vm::llm_config::qc_default_model(&resolved.provider),
1213 });
1214
1215 let should_verify = args.verify || args.warm;
1216 let mut ok = true;
1217 if should_verify {
1218 if commands::local::runtime::uses_ollama_wire_protocol(&resolved.provider) {
1219 let mut readiness = harn_vm::llm::OllamaReadinessOptions::new(resolved.id.clone());
1220 readiness.warm = args.warm;
1221 readiness.observe_loaded = true;
1222 readiness.keep_alive = args
1223 .keep_alive
1224 .as_deref()
1225 .and_then(harn_vm::llm::normalize_ollama_keep_alive);
1226 let result = harn_vm::llm::ollama_readiness(readiness).await;
1227 ok = result.valid;
1228 payload["readiness"] = serde_json::to_value(&result).unwrap_or_else(|error| {
1229 serde_json::json!({
1230 "valid": false,
1231 "status": "serialization_error",
1232 "message": format!("failed to serialize readiness result: {error}"),
1233 })
1234 });
1235 } else {
1236 ok = false;
1237 payload["readiness"] = serde_json::json!({
1238 "valid": false,
1239 "status": "unsupported_provider",
1240 "message": format!(
1241 "models info --verify is only supported for models whose local runtime declares the Ollama API protocol; resolved provider is '{}'",
1242 resolved.provider
1243 ),
1244 "provider": resolved.provider,
1245 });
1246 }
1247 }
1248
1249 println!(
1250 "{}",
1251 serde_json::to_string(&payload).unwrap_or_else(|error| {
1252 command_error(&format!("failed to serialize model info: {error}"))
1253 })
1254 );
1255 ok
1256}
1257
1258async fn local_provider_readiness(
1259 provider: &str,
1260 model: &str,
1261 api_key: &str,
1262) -> Option<serde_json::Value> {
1263 let def = harn_vm::llm_config::provider_config(provider)?;
1264 if def.auth_style != "none" || !harn_vm::llm::supports_model_readiness_probe(&def) {
1265 return None;
1266 }
1267 let readiness = harn_vm::llm::readiness::probe_provider_readiness_with_options(
1268 provider,
1269 harn_vm::llm::readiness::ProviderReadinessOptions {
1270 requested_model: Some(model),
1271 base_url_override: None,
1272 api_key_override: Some(api_key),
1273 },
1274 )
1275 .await;
1276 Some(serde_json::to_value(readiness).unwrap_or_else(|error| {
1277 serde_json::json!({
1278 "ok": false,
1279 "status": "bad_response",
1280 "message": format!("failed to serialize readiness result: {error}"),
1281 "provider": provider,
1282 })
1283 }))
1284}
1285
1286fn build_provider_catalog_payload(available_only: bool) -> serde_json::Value {
1287 let provider_names = if available_only {
1288 harn_vm::llm::available_provider_names()
1289 } else {
1290 harn_vm::llm_config::provider_names()
1291 };
1292 let providers: Vec<_> = provider_names
1293 .into_iter()
1294 .filter_map(|name| {
1295 harn_vm::llm_config::provider_config(&name).map(|def| {
1296 serde_json::json!({
1297 "name": name,
1298 "display_name": def.display_name,
1299 "icon": def.icon,
1300 "base_url": harn_vm::llm_config::resolve_base_url(&def),
1301 "base_url_env": def.base_url_env,
1302 "region_env": def.region_env,
1303 "regions": def.regions,
1304 "auth_style": def.auth_style,
1305 "auth_envs": harn_vm::llm_config::auth_env_names(&def.auth_env),
1306 "auth_available": harn_vm::llm::provider_auth_status(&name).available,
1307 "features": def.features,
1308 "cost_per_1k_in": def.cost_per_1k_in,
1309 "cost_per_1k_out": def.cost_per_1k_out,
1310 "latency_p50_ms": def.latency_p50_ms,
1311 "performance": def.performance,
1312 })
1313 })
1314 })
1315 .collect();
1316 let models: Vec<_> = harn_vm::llm_config::model_catalog_entries()
1317 .into_iter()
1318 .map(|(id, model)| {
1319 serde_json::json!({
1320 "id": id,
1321 "name": model.name,
1322 "provider": model.provider,
1323 "context_window": model.context_window,
1324 "runtime_context_window": model.runtime_context_window,
1325 "stream_timeout": model.stream_timeout,
1326 "capabilities": model.capabilities,
1327 "pricing": model.pricing,
1328 "performance": model.performance,
1329 })
1330 })
1331 .collect();
1332 let aliases: Vec<_> = harn_vm::llm_config::alias_entries()
1333 .into_iter()
1334 .map(|(name, alias)| {
1335 serde_json::json!({
1336 "name": name,
1337 "id": alias.id,
1338 "provider": alias.provider,
1339 "tool_format": alias.tool_format,
1340 "tool_calling": harn_vm::llm_config::alias_tool_calling_entry(&name),
1341 })
1342 })
1343 .collect();
1344 let routing_routes = harn_vm::provider_catalog::artifact().routing_routes;
1345 serde_json::json!({
1346 "providers": providers,
1347 "known_model_names": harn_vm::llm_config::known_model_names(),
1348 "available_providers": harn_vm::llm::available_provider_names(),
1349 "aliases": aliases,
1350 "models": models,
1351 "routing_routes": routing_routes,
1352 "qc_defaults": harn_vm::llm_config::qc_defaults(),
1353 })
1354}
1355
1356async fn dispatch_provider_catalog(available_only: bool) -> i32 {
1365 static DISPATCH_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
1366 let payload = build_provider_catalog_payload(available_only);
1367 let payload_json = match serde_json::to_string(&payload) {
1368 Ok(json) => json,
1369 Err(error) => {
1370 eprintln!("error: failed to serialise provider catalog payload: {error}");
1371 return 1;
1372 }
1373 };
1374 let _guard = DISPATCH_LOCK.lock().await;
1375 let _payload_guard =
1376 crate::env_guard::ScopedEnvVar::set("HARN_PROVIDER_CATALOG_PAYLOAD_JSON", &payload_json);
1377 crate::dispatch::dispatch_to_embedded_script("providers/catalog", Vec::new(), true).await
1381}
1382
1383async fn run_provider_ready(
1384 provider: &str,
1385 model: Option<&str>,
1386 base_url: Option<&str>,
1387 json: bool,
1388) {
1389 let readiness =
1390 harn_vm::llm::readiness::probe_provider_readiness(provider, model, base_url).await;
1391 if json {
1392 match serde_json::to_string_pretty(&readiness) {
1393 Ok(payload) => println!("{payload}"),
1394 Err(error) => command_error(&format!("failed to serialize readiness result: {error}")),
1395 }
1396 } else if readiness.ok {
1397 println!("{}", readiness.message);
1398 } else {
1399 eprintln!("{}", readiness.message);
1400 }
1401 if !readiness.ok {
1402 process::exit(1);
1403 }
1404}
1405
1406pub(crate) fn command_error(message: &str) -> ! {
1407 Cli::command()
1408 .error(ErrorKind::ValueValidation, message)
1409 .exit()
1410}
1411
1412async fn run_as_job(args: &cli::RunArgs) {
1417 let Some(file) = args.file.as_deref() else {
1418 command_error("`--as-job` requires a `.harn` file path");
1419 };
1420 let Some(job) = args.job.as_deref() else {
1421 command_error("`--as-job` requires `--job <name>`");
1422 };
1423 let Some(request) = args.request.as_deref() else {
1424 command_error("`--as-job` requires `--request <path>`");
1425 };
1426
1427 let script_path = PathBuf::from(file);
1428 let job = job.to_string();
1429 let request = request.to_path_buf();
1430 let result_out = args.result_out.clone();
1431
1432 let local = tokio::task::LocalSet::new();
1438 let outcome = local
1439 .run_until(async move {
1440 harn_serve::run_job_from_files(
1441 &script_path,
1442 &job,
1443 &request,
1444 result_out.as_deref(),
1445 false,
1446 )
1447 .await
1448 })
1449 .await;
1450
1451 match outcome {
1452 Ok((outcome, rendered)) => {
1453 println!("{rendered}");
1454 if !outcome.succeeded() {
1455 process::exit(1);
1456 }
1457 }
1458 Err(error) => {
1459 eprintln!("[harn] job failed: {}", error.message());
1460 process::exit(1);
1461 }
1462 }
1463}
1464
1465fn print_check_error(code: &str, message: &str) -> ! {
1466 let envelope: json_envelope::JsonEnvelope<commands::check::CheckReport> =
1467 json_envelope::JsonEnvelope::err(commands::check::CHECK_SCHEMA_VERSION, code, message);
1468 println!("{}", json_envelope::to_string_pretty(&envelope));
1469 process::exit(1);
1470}
1471
1472fn print_lint_error(code: &str, message: &str) -> ! {
1473 let envelope: json_envelope::JsonEnvelope<commands::check::LintReport> =
1474 json_envelope::JsonEnvelope::err(commands::check::LINT_SCHEMA_VERSION, code, message);
1475 println!("{}", json_envelope::to_string_pretty(&envelope));
1476 process::exit(1);
1477}
1478
1479fn verify_provenance_receipt(path: &str, json: bool) -> Result<(), String> {
1480 let raw =
1481 fs::read_to_string(path).map_err(|error| format!("failed to read {path}: {error}"))?;
1482 let receipt: harn_vm::ProvenanceReceipt = serde_json::from_str(&raw)
1483 .map_err(|error| format!("failed to parse provenance receipt {path}: {error}"))?;
1484 let report = harn_vm::verify_receipt(&receipt);
1485 if json {
1486 println!(
1487 "{}",
1488 serde_json::to_string_pretty(&report).map_err(|error| error.to_string())?
1489 );
1490 } else if report.verified {
1491 println!(
1492 "verified receipt={} events={} receipt_hash={} event_root_hash={}",
1493 report.receipt_id.unwrap_or_else(|| "-".to_string()),
1494 report.event_count,
1495 report.receipt_hash.unwrap_or_else(|| "-".to_string()),
1496 report.event_root_hash.unwrap_or_else(|| "-".to_string())
1497 );
1498 } else {
1499 println!(
1500 "failed receipt={} events={}",
1501 report.receipt_id.unwrap_or_else(|| "-".to_string()),
1502 report.event_count
1503 );
1504 for error in &report.errors {
1505 println!(" {error}");
1506 }
1507 return Err("provenance receipt verification failed".to_string());
1508 }
1509 Ok(())
1510}
1511
1512pub(crate) fn load_run_record_or_exit(path: &Path) -> harn_vm::orchestration::RunRecord {
1513 match harn_vm::orchestration::load_run_record(path) {
1514 Ok(run) => run,
1515 Err(error) => {
1516 eprintln!("Failed to load run record: {error}");
1517 process::exit(1);
1518 }
1519 }
1520}
1521
1522fn load_eval_suite_manifest_or_exit(path: &Path) -> harn_vm::orchestration::EvalSuiteManifest {
1523 harn_vm::orchestration::load_eval_suite_manifest(path).unwrap_or_else(|error| {
1524 eprintln!("Failed to load eval manifest {}: {error}", path.display());
1525 process::exit(1);
1526 })
1527}
1528
1529fn load_eval_pack_manifest_or_exit(path: &Path) -> harn_vm::orchestration::EvalPackManifest {
1530 harn_vm::orchestration::load_eval_pack_manifest(path).unwrap_or_else(|error| {
1531 eprintln!("Failed to load eval pack {}: {error}", path.display());
1532 process::exit(1);
1533 })
1534}
1535
1536fn load_persona_eval_ladder_manifest_or_exit(
1537 path: &Path,
1538) -> harn_vm::orchestration::PersonaEvalLadderManifest {
1539 harn_vm::orchestration::load_persona_eval_ladder_manifest(path).unwrap_or_else(|error| {
1540 eprintln!(
1541 "Failed to load persona eval ladder {}: {error}",
1542 path.display()
1543 );
1544 process::exit(1);
1545 })
1546}
1547
1548fn file_looks_like_eval_manifest(path: &Path) -> bool {
1549 if path.file_name().and_then(|name| name.to_str()) == Some("harn.eval.toml") {
1550 return true;
1551 }
1552 if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
1553 let Ok(content) = fs::read_to_string(path) else {
1554 return false;
1555 };
1556 return toml::from_str::<harn_vm::orchestration::EvalPackManifest>(&content)
1557 .is_ok_and(|manifest| !manifest.cases.is_empty() || !manifest.ladders.is_empty());
1558 }
1559 let Ok(content) = fs::read_to_string(path) else {
1560 return false;
1561 };
1562 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
1563 return false;
1564 };
1565 json.get("_type").and_then(|value| value.as_str()) == Some("eval_suite_manifest")
1566 || json.get("cases").is_some()
1567}
1568
1569fn file_looks_like_eval_pack_manifest(path: &Path) -> bool {
1570 if path.file_name().and_then(|name| name.to_str()) == Some("harn.eval.toml") {
1571 return true;
1572 }
1573 if path.extension().and_then(|ext| ext.to_str()) == Some("toml") {
1574 return file_looks_like_eval_manifest(path);
1575 }
1576 let Ok(content) = fs::read_to_string(path) else {
1577 return false;
1578 };
1579 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
1580 return false;
1581 };
1582 json.get("version").is_some()
1583 && (json.get("cases").is_some() || json.get("ladders").is_some())
1584 && json.get("_type").and_then(|value| value.as_str()) != Some("eval_suite_manifest")
1585}
1586
1587fn file_looks_like_persona_eval_ladder_manifest(path: &Path) -> bool {
1588 let Ok(content) = fs::read_to_string(path) else {
1589 return false;
1590 };
1591 if path.extension().and_then(|ext| ext.to_str()) == Some("json") {
1592 let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
1593 return false;
1594 };
1595 return json.get("_type").and_then(|value| value.as_str())
1596 == Some("persona_eval_ladder_manifest")
1597 || json.get("timeout_tiers").is_some()
1598 || json.get("timeout-tiers").is_some();
1599 }
1600 toml::from_str::<harn_vm::orchestration::PersonaEvalLadderManifest>(&content).is_ok_and(
1601 |manifest| {
1602 manifest
1603 .type_name
1604 .eq_ignore_ascii_case("persona_eval_ladder_manifest")
1605 || (!manifest.timeout_tiers.is_empty() && manifest.backend.path.is_some())
1606 },
1607 )
1608}
1609
1610pub(crate) fn collect_run_record_paths(path: &str) -> Vec<PathBuf> {
1611 let path = Path::new(path);
1612 if path.is_file() {
1613 return vec![path.to_path_buf()];
1614 }
1615 if path.is_dir() {
1616 let mut entries: Vec<PathBuf> = fs::read_dir(path)
1617 .unwrap_or_else(|error| {
1618 eprintln!("Failed to read run directory {}: {error}", path.display());
1619 process::exit(1);
1620 })
1621 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
1622 .filter(|entry| entry.extension().and_then(|ext| ext.to_str()) == Some("json"))
1623 .collect();
1624 entries.sort();
1625 return entries;
1626 }
1627 eprintln!("Run path does not exist: {}", path.display());
1628 process::exit(1);
1629}
1630
1631fn print_run_diff(diff: &harn_vm::orchestration::RunDiffReport) {
1632 println!(
1633 "Diff: {} -> {} [{} -> {}]",
1634 diff.left_run_id, diff.right_run_id, diff.left_status, diff.right_status
1635 );
1636 println!("Identical: {}", diff.identical);
1637 println!("Stage diffs: {}", diff.stage_diffs.len());
1638 println!("Tool diffs: {}", diff.tool_diffs.len());
1639 println!("Observability diffs: {}", diff.observability_diffs.len());
1640 println!("Transition delta: {}", diff.transition_count_delta);
1641 println!("Artifact delta: {}", diff.artifact_count_delta);
1642 println!("Checkpoint delta: {}", diff.checkpoint_count_delta);
1643 for stage in &diff.stage_diffs {
1644 println!("- {} [{}]", stage.node_id, stage.change);
1645 for detail in &stage.details {
1646 println!(" {detail}");
1647 }
1648 }
1649 for tool in &diff.tool_diffs {
1650 println!("- tool {} [{}]", tool.tool_name, tool.args_hash);
1651 println!(" left: {:?}", tool.left_result);
1652 println!(" right: {:?}", tool.right_result);
1653 }
1654 for item in &diff.observability_diffs {
1655 println!("- {} [{}]", item.label, item.section);
1656 for detail in &item.details {
1657 println!(" {detail}");
1658 }
1659 }
1660}
1661
1662fn inspect_run_record(path: &str, compare: Option<&str>) {
1663 let run = load_run_record_or_exit(Path::new(path));
1664 println!("Run: {}", run.id);
1665 println!(
1666 "Workflow: {}",
1667 run.workflow_name
1668 .clone()
1669 .unwrap_or_else(|| run.workflow_id.clone())
1670 );
1671 println!("Status: {}", run.status);
1672 println!("Task: {}", run.task);
1673 println!("Stages: {}", run.stages.len());
1674 println!("Artifacts: {}", run.artifacts.len());
1675 println!("Transitions: {}", run.transitions.len());
1676 println!("Checkpoints: {}", run.checkpoints.len());
1677 println!("HITL questions: {}", run.hitl_questions.len());
1678 if let Some(observability) = &run.observability {
1679 println!("Planner rounds: {}", observability.planner_rounds.len());
1680 println!("Research facts: {}", observability.research_fact_count);
1681 println!("Workers: {}", observability.worker_lineage.len());
1682 println!(
1683 "Action graph: {} nodes / {} edges",
1684 observability.action_graph_nodes.len(),
1685 observability.action_graph_edges.len()
1686 );
1687 println!(
1688 "Transcript pointers: {}",
1689 observability.transcript_pointers.len()
1690 );
1691 println!("Daemon events: {}", observability.daemon_events.len());
1692 }
1693 if let Some(parent_worker_id) = run
1694 .metadata
1695 .get("parent_worker_id")
1696 .and_then(|value| value.as_str())
1697 {
1698 println!("Parent worker: {parent_worker_id}");
1699 }
1700 if let Some(parent_stage_id) = run
1701 .metadata
1702 .get("parent_stage_id")
1703 .and_then(|value| value.as_str())
1704 {
1705 println!("Parent stage: {parent_stage_id}");
1706 }
1707 if run
1708 .metadata
1709 .get("delegated")
1710 .and_then(|value| value.as_bool())
1711 .unwrap_or(false)
1712 {
1713 println!("Delegated: true");
1714 }
1715 println!(
1716 "Pending nodes: {}",
1717 if run.pending_nodes.is_empty() {
1718 "-".to_string()
1719 } else {
1720 run.pending_nodes.join(", ")
1721 }
1722 );
1723 println!(
1724 "Replay fixture: {}",
1725 if run.replay_fixture.is_some() {
1726 "embedded"
1727 } else {
1728 "derived"
1729 }
1730 );
1731 for stage in &run.stages {
1732 let worker = stage.metadata.get("worker");
1733 let worker_suffix = worker
1734 .and_then(|value| value.get("name"))
1735 .and_then(|value| value.as_str())
1736 .map(|name| format!(" worker={name}"))
1737 .unwrap_or_default();
1738 println!(
1739 "- {} [{}] status={} outcome={} branch={}{}",
1740 stage.node_id,
1741 stage.kind,
1742 stage.status,
1743 stage.outcome,
1744 stage.branch.clone().unwrap_or_else(|| "-".to_string()),
1745 worker_suffix,
1746 );
1747 if let Some(worker) = worker {
1748 if let Some(worker_id) = worker.get("id").and_then(|value| value.as_str()) {
1749 println!(" worker_id: {worker_id}");
1750 }
1751 if let Some(child_run_id) = worker.get("child_run_id").and_then(|value| value.as_str())
1752 {
1753 println!(" child_run_id: {child_run_id}");
1754 }
1755 if let Some(child_run_path) = worker
1756 .get("child_run_path")
1757 .and_then(|value| value.as_str())
1758 {
1759 println!(" child_run_path: {child_run_path}");
1760 }
1761 }
1762 }
1763 if let Some(observability) = &run.observability {
1764 for round in &observability.planner_rounds {
1765 println!(
1766 "- planner {} iterations={} llm_calls={} tools={} research_facts={}",
1767 round.node_id,
1768 round.iteration_count,
1769 round.llm_call_count,
1770 round.tool_execution_count,
1771 round.research_facts.len()
1772 );
1773 }
1774 for pointer in &observability.transcript_pointers {
1775 println!(
1776 "- transcript {} [{}] available={} {}",
1777 pointer.label,
1778 pointer.kind,
1779 pointer.available,
1780 pointer
1781 .path
1782 .clone()
1783 .unwrap_or_else(|| pointer.location.clone())
1784 );
1785 }
1786 for event in &observability.daemon_events {
1787 println!(
1788 "- daemon {} [{:?}] at {}",
1789 event.name, event.kind, event.timestamp
1790 );
1791 println!(" id: {}", event.daemon_id);
1792 println!(" persist_path: {}", event.persist_path);
1793 if let Some(summary) = &event.payload_summary {
1794 println!(" payload: {summary}");
1795 }
1796 }
1797 }
1798 if let Some(compare_path) = compare {
1799 let baseline = load_run_record_or_exit(Path::new(compare_path));
1800 print_run_diff(&harn_vm::orchestration::diff_run_records(&baseline, &run));
1801 }
1802}
1803
1804fn eval_run_record(
1805 path: &str,
1806 compare: Option<&str>,
1807 structural_experiment: Option<&str>,
1808 argv: &[String],
1809 llm_mock_mode: &commands::run::CliLlmMockMode,
1810) {
1811 if let Some(experiment) = structural_experiment {
1812 let path_buf = PathBuf::from(path);
1813 if !path_buf.is_file() || path_buf.extension().and_then(|ext| ext.to_str()) != Some("harn")
1814 {
1815 eprintln!(
1816 "--structural-experiment currently requires a .harn pipeline path, got {path}"
1817 );
1818 process::exit(1);
1819 }
1820 if compare.is_some() {
1821 eprintln!("--compare cannot be combined with --structural-experiment");
1822 process::exit(1);
1823 }
1824 if matches!(llm_mock_mode, commands::run::CliLlmMockMode::Record { .. }) {
1825 eprintln!("--llm-mock-record cannot be combined with --structural-experiment");
1826 process::exit(1);
1827 }
1828 let path_buf = fs::canonicalize(&path_buf).unwrap_or_else(|error| {
1829 command_error(&format!(
1830 "failed to canonicalize structural eval pipeline {}: {error}",
1831 path_buf.display()
1832 ))
1833 });
1834 run_structural_experiment_eval(&path_buf, experiment, argv, llm_mock_mode);
1835 return;
1836 }
1837
1838 let path_buf = PathBuf::from(path);
1839 if path_buf.is_file() && file_looks_like_persona_eval_ladder_manifest(&path_buf) {
1840 if compare.is_some() {
1841 eprintln!("--compare is not supported with persona eval ladder manifests");
1842 process::exit(1);
1843 }
1844 let manifest = load_persona_eval_ladder_manifest_or_exit(&path_buf);
1845 let report =
1846 harn_vm::orchestration::run_persona_eval_ladder(&manifest).unwrap_or_else(|error| {
1847 eprintln!(
1848 "Failed to evaluate persona eval ladder {}: {error}",
1849 path_buf.display()
1850 );
1851 process::exit(1);
1852 });
1853 print_persona_ladder_report(&report);
1854 if !report.pass {
1855 process::exit(1);
1856 }
1857 return;
1858 }
1859
1860 if path_buf.is_file() && file_looks_like_eval_pack_manifest(&path_buf) {
1861 if compare.is_some() {
1862 eprintln!("--compare is not supported with eval pack manifests");
1863 process::exit(1);
1864 }
1865 let manifest = load_eval_pack_manifest_or_exit(&path_buf);
1866 let report = harn_vm::orchestration::evaluate_eval_pack_manifest_resumable(&manifest, None)
1867 .unwrap_or_else(|error| {
1868 eprintln!(
1869 "Failed to evaluate eval pack {}: {error}",
1870 path_buf.display()
1871 );
1872 process::exit(1);
1873 });
1874 print_eval_pack_report(&report);
1875 if !report.pass {
1876 process::exit(1);
1877 }
1878 return;
1879 }
1880
1881 if path_buf.is_file() && file_looks_like_eval_manifest(&path_buf) {
1882 if compare.is_some() {
1883 eprintln!("--compare is not supported with eval suite manifests");
1884 process::exit(1);
1885 }
1886 let manifest = load_eval_suite_manifest_or_exit(&path_buf);
1887 let suite = harn_vm::orchestration::evaluate_run_suite_manifest(&manifest).unwrap_or_else(
1888 |error| {
1889 eprintln!(
1890 "Failed to evaluate manifest {}: {error}",
1891 path_buf.display()
1892 );
1893 process::exit(1);
1894 },
1895 );
1896 println!(
1897 "{} {} passed, {} failed, {} total",
1898 if suite.pass { "PASS" } else { "FAIL" },
1899 suite.passed,
1900 suite.failed,
1901 suite.total
1902 );
1903 for case in &suite.cases {
1904 println!(
1905 "- {} [{}] {}",
1906 case.label.clone().unwrap_or_else(|| case.run_id.clone()),
1907 case.workflow_id,
1908 if case.pass { "PASS" } else { "FAIL" }
1909 );
1910 if let Some(path) = &case.source_path {
1911 println!(" path: {path}");
1912 }
1913 if let Some(comparison) = &case.comparison {
1914 println!(" baseline identical: {}", comparison.identical);
1915 if !comparison.identical {
1916 println!(
1917 " baseline status: {} -> {}",
1918 comparison.left_status, comparison.right_status
1919 );
1920 }
1921 }
1922 for failure in &case.failures {
1923 println!(" {failure}");
1924 }
1925 }
1926 if !suite.pass {
1927 process::exit(1);
1928 }
1929 return;
1930 }
1931
1932 let paths = collect_run_record_paths(path);
1933 if paths.len() > 1 {
1934 let mut cases = Vec::new();
1935 for path in &paths {
1936 let run = load_run_record_or_exit(path);
1937 let fixture = run
1938 .replay_fixture
1939 .clone()
1940 .unwrap_or_else(|| harn_vm::orchestration::replay_fixture_from_run(&run));
1941 cases.push((run, fixture, Some(path.display().to_string())));
1942 }
1943 let suite = harn_vm::orchestration::evaluate_run_suite(cases);
1944 println!(
1945 "{} {} passed, {} failed, {} total",
1946 if suite.pass { "PASS" } else { "FAIL" },
1947 suite.passed,
1948 suite.failed,
1949 suite.total
1950 );
1951 for case in &suite.cases {
1952 println!(
1953 "- {} [{}] {}",
1954 case.run_id,
1955 case.workflow_id,
1956 if case.pass { "PASS" } else { "FAIL" }
1957 );
1958 if let Some(path) = &case.source_path {
1959 println!(" path: {path}");
1960 }
1961 if let Some(comparison) = &case.comparison {
1962 println!(" baseline identical: {}", comparison.identical);
1963 }
1964 for failure in &case.failures {
1965 println!(" {failure}");
1966 }
1967 }
1968 if !suite.pass {
1969 process::exit(1);
1970 }
1971 return;
1972 }
1973
1974 let run = load_run_record_or_exit(&paths[0]);
1975 let fixture = run
1976 .replay_fixture
1977 .clone()
1978 .unwrap_or_else(|| harn_vm::orchestration::replay_fixture_from_run(&run));
1979 let report = harn_vm::orchestration::evaluate_run_against_fixture(&run, &fixture);
1980 println!("{}", if report.pass { "PASS" } else { "FAIL" });
1981 println!("Stages: {}", report.stage_count);
1982 if let Some(compare_path) = compare {
1983 let baseline = load_run_record_or_exit(Path::new(compare_path));
1984 print_run_diff(&harn_vm::orchestration::diff_run_records(&baseline, &run));
1985 }
1986 if !report.failures.is_empty() {
1987 for failure in &report.failures {
1988 println!("- {failure}");
1989 }
1990 }
1991 if !report.pass {
1992 process::exit(1);
1993 }
1994}
1995
1996fn print_eval_pack_report(report: &harn_vm::orchestration::EvalPackReport) {
1997 println!(
1998 "{} {} passed, {} blocking failed, {} warning, {} informational, {} total",
1999 if report.pass { "PASS" } else { "FAIL" },
2000 report.passed,
2001 report.blocking_failed,
2002 report.warning_failed,
2003 report.informational_failed,
2004 report.total
2005 );
2006 for case in &report.cases {
2007 println!(
2008 "- {} [{}] {} ({})",
2009 case.label,
2010 case.workflow_id,
2011 if case.pass { "PASS" } else { "FAIL" },
2012 case.severity
2013 );
2014 if let Some(path) = &case.source_path {
2015 println!(" path: {path}");
2016 }
2017 if let Some(comparison) = &case.comparison {
2018 println!(" baseline identical: {}", comparison.identical);
2019 if !comparison.identical {
2020 println!(
2021 " baseline status: {} -> {}",
2022 comparison.left_status, comparison.right_status
2023 );
2024 }
2025 }
2026 for failure in &case.failures {
2027 println!(" {failure}");
2028 }
2029 for warning in &case.warnings {
2030 println!(" warning: {warning}");
2031 }
2032 for item in &case.informational {
2033 println!(" info: {item}");
2034 }
2035 }
2036 for ladder in &report.ladders {
2037 println!(
2038 "- ladder {} [{}] {} ({}) first_correct={}/{}",
2039 ladder.id,
2040 ladder.persona,
2041 if ladder.pass { "PASS" } else { "FAIL" },
2042 ladder.severity,
2043 ladder.first_correct_route.as_deref().unwrap_or("<none>"),
2044 ladder.first_correct_tier.as_deref().unwrap_or("<none>")
2045 );
2046 println!(" artifacts: {}", ladder.artifact_root);
2047 for tier in &ladder.tiers {
2048 println!(
2049 " - {} [{}] {} tools={} models={} latency={}ms cost=${:.6}",
2050 tier.timeout_tier,
2051 tier.route_id,
2052 tier.outcome,
2053 tier.tool_calls,
2054 tier.model_calls,
2055 tier.latency_ms,
2056 tier.cost_usd
2057 );
2058 for reason in &tier.degradation_reasons {
2059 println!(" {reason}");
2060 }
2061 }
2062 }
2063}
2064
2065fn print_persona_ladder_report(report: &harn_vm::orchestration::PersonaEvalLadderReport) {
2066 println!(
2067 "{} ladder {} passed, {} degraded/looped, {} total",
2068 if report.pass { "PASS" } else { "FAIL" },
2069 report.passed,
2070 report.failed,
2071 report.total
2072 );
2073 println!(
2074 "first_correct: {}/{}",
2075 report.first_correct_route.as_deref().unwrap_or("<none>"),
2076 report.first_correct_tier.as_deref().unwrap_or("<none>")
2077 );
2078 println!("artifacts: {}", report.artifact_root);
2079 for tier in &report.tiers {
2080 println!(
2081 "- {} [{}] {} tools={} models={} latency={}ms cost=${:.6}",
2082 tier.timeout_tier,
2083 tier.route_id,
2084 tier.outcome,
2085 tier.tool_calls,
2086 tier.model_calls,
2087 tier.latency_ms,
2088 tier.cost_usd
2089 );
2090 for reason in &tier.degradation_reasons {
2091 println!(" {reason}");
2092 }
2093 }
2094}
2095
2096pub(crate) fn run_package_evals() {
2097 let paths = package::load_package_eval_pack_paths(None).unwrap_or_else(|error| {
2098 eprintln!("{error}");
2099 process::exit(1);
2100 });
2101 let mut all_pass = true;
2102 for path in &paths {
2103 println!("Eval pack: {}", path.display());
2104 let manifest = load_eval_pack_manifest_or_exit(path);
2105 let report = harn_vm::orchestration::evaluate_eval_pack_manifest_resumable(&manifest, None)
2106 .unwrap_or_else(|error| {
2107 eprintln!("Failed to evaluate eval pack {}: {error}", path.display());
2108 process::exit(1);
2109 });
2110 print_eval_pack_report(&report);
2111 all_pass &= report.pass;
2112 }
2113 if !all_pass {
2114 process::exit(1);
2115 }
2116}
2117
2118fn run_structural_experiment_eval(
2119 path: &Path,
2120 experiment: &str,
2121 argv: &[String],
2122 llm_mock_mode: &commands::run::CliLlmMockMode,
2123) {
2124 let baseline_dir = tempfile::Builder::new()
2125 .prefix("harn-eval-baseline-")
2126 .tempdir()
2127 .unwrap_or_else(|error| {
2128 command_error(&format!("failed to create baseline tempdir: {error}"))
2129 });
2130 let variant_dir = tempfile::Builder::new()
2131 .prefix("harn-eval-variant-")
2132 .tempdir()
2133 .unwrap_or_else(|error| {
2134 command_error(&format!("failed to create variant tempdir: {error}"))
2135 });
2136
2137 let baseline = spawn_eval_pipeline_run(path, baseline_dir.path(), None, argv, llm_mock_mode);
2138 if !baseline.status.success() {
2139 relay_subprocess_failure("baseline", &baseline);
2140 }
2141
2142 let variant = spawn_eval_pipeline_run(
2143 path,
2144 variant_dir.path(),
2145 Some(experiment),
2146 argv,
2147 llm_mock_mode,
2148 );
2149 if !variant.status.success() {
2150 relay_subprocess_failure("variant", &variant);
2151 }
2152
2153 let baseline_runs = collect_structural_eval_runs(baseline_dir.path());
2154 let variant_runs = collect_structural_eval_runs(variant_dir.path());
2155 if baseline_runs.is_empty() || variant_runs.is_empty() {
2156 eprintln!(
2157 "structural eval expected workflow run records under {} and {}, but one side was empty",
2158 baseline_dir.path().display(),
2159 variant_dir.path().display()
2160 );
2161 process::exit(1);
2162 }
2163 if baseline_runs.len() != variant_runs.len() {
2164 eprintln!(
2165 "structural eval produced different run counts: baseline={} variant={}",
2166 baseline_runs.len(),
2167 variant_runs.len()
2168 );
2169 process::exit(1);
2170 }
2171
2172 let mut baseline_ok = 0usize;
2173 let mut variant_ok = 0usize;
2174 let mut any_failures = false;
2175
2176 println!("Structural experiment: {experiment}");
2177 println!("Cases: {}", baseline_runs.len());
2178 for (baseline_run, variant_run) in baseline_runs.iter().zip(variant_runs.iter()) {
2179 let baseline_fixture = baseline_run
2180 .replay_fixture
2181 .clone()
2182 .unwrap_or_else(|| harn_vm::orchestration::replay_fixture_from_run(baseline_run));
2183 let variant_fixture = variant_run
2184 .replay_fixture
2185 .clone()
2186 .unwrap_or_else(|| harn_vm::orchestration::replay_fixture_from_run(variant_run));
2187 let baseline_report =
2188 harn_vm::orchestration::evaluate_run_against_fixture(baseline_run, &baseline_fixture);
2189 let variant_report =
2190 harn_vm::orchestration::evaluate_run_against_fixture(variant_run, &variant_fixture);
2191 let diff = harn_vm::orchestration::diff_run_records(baseline_run, variant_run);
2192 if baseline_report.pass {
2193 baseline_ok += 1;
2194 }
2195 if variant_report.pass {
2196 variant_ok += 1;
2197 }
2198 any_failures |= !baseline_report.pass || !variant_report.pass;
2199 println!(
2200 "- {} [{}]",
2201 variant_run
2202 .workflow_name
2203 .clone()
2204 .unwrap_or_else(|| variant_run.workflow_id.clone()),
2205 variant_run.task
2206 );
2207 println!(
2208 " baseline: {}",
2209 if baseline_report.pass { "PASS" } else { "FAIL" }
2210 );
2211 for failure in &baseline_report.failures {
2212 println!(" {failure}");
2213 }
2214 println!(
2215 " variant: {}",
2216 if variant_report.pass { "PASS" } else { "FAIL" }
2217 );
2218 for failure in &variant_report.failures {
2219 println!(" {failure}");
2220 }
2221 println!(" diff identical: {}", diff.identical);
2222 println!(" stage diffs: {}", diff.stage_diffs.len());
2223 println!(" tool diffs: {}", diff.tool_diffs.len());
2224 println!(" observability diffs: {}", diff.observability_diffs.len());
2225 }
2226
2227 println!("Baseline {} / {} passed", baseline_ok, baseline_runs.len());
2228 println!("Variant {} / {} passed", variant_ok, variant_runs.len());
2229
2230 if any_failures {
2231 process::exit(1);
2232 }
2233}
2234
2235fn spawn_eval_pipeline_run(
2236 path: &Path,
2237 run_dir: &Path,
2238 structural_experiment: Option<&str>,
2239 argv: &[String],
2240 llm_mock_mode: &commands::run::CliLlmMockMode,
2241) -> std::process::Output {
2242 let exe = env::current_exe().unwrap_or_else(|error| {
2243 command_error(&format!("failed to resolve current executable: {error}"))
2244 });
2245 let mut command = std::process::Command::new(exe);
2246 command.current_dir(path.parent().unwrap_or_else(|| Path::new(".")));
2247 command.arg("run");
2248 match llm_mock_mode {
2249 commands::run::CliLlmMockMode::Off => {}
2250 commands::run::CliLlmMockMode::Replay { fixture_path } => {
2251 command
2252 .arg("--llm-mock")
2253 .arg(absolute_cli_path(fixture_path));
2254 }
2255 commands::run::CliLlmMockMode::Record { fixture_path } => {
2256 command
2257 .arg("--llm-mock-record")
2258 .arg(absolute_cli_path(fixture_path));
2259 }
2260 }
2261 command.arg(path);
2262 if !argv.is_empty() {
2263 command.arg("--");
2264 command.args(argv);
2265 }
2266 command.env(harn_vm::runtime_paths::HARN_RUN_DIR_ENV, run_dir);
2267 if let Some(experiment) = structural_experiment {
2268 command.env("HARN_STRUCTURAL_EXPERIMENT", experiment);
2269 }
2270 command.output().unwrap_or_else(|error| {
2271 command_error(&format!(
2272 "failed to spawn `harn run {}` for structural eval: {error}",
2273 path.display()
2274 ))
2275 })
2276}
2277
2278fn absolute_cli_path(path: &Path) -> PathBuf {
2279 if path.is_absolute() {
2280 return path.to_path_buf();
2281 }
2282 env::current_dir()
2283 .unwrap_or_else(|_| PathBuf::from("."))
2284 .join(path)
2285}
2286
2287fn relay_subprocess_failure(label: &str, output: &std::process::Output) -> ! {
2288 let stdout = String::from_utf8_lossy(&output.stdout);
2289 let stderr = String::from_utf8_lossy(&output.stderr);
2290 if !stdout.trim().is_empty() {
2291 eprintln!("[{label}] stdout:\n{stdout}");
2292 }
2293 if !stderr.trim().is_empty() {
2294 eprintln!("[{label}] stderr:\n{stderr}");
2295 }
2296 process::exit(output.status.code().unwrap_or(1));
2297}
2298
2299fn collect_structural_eval_runs(dir: &Path) -> Vec<harn_vm::orchestration::RunRecord> {
2300 let mut paths: Vec<PathBuf> = fs::read_dir(dir)
2301 .unwrap_or_else(|error| {
2302 command_error(&format!(
2303 "failed to read structural eval run dir {}: {error}",
2304 dir.display()
2305 ))
2306 })
2307 .filter_map(|entry| entry.ok().map(|entry| entry.path()))
2308 .filter(|entry| entry.extension().and_then(|ext| ext.to_str()) == Some("json"))
2309 .collect();
2310 paths.sort();
2311 let mut runs: Vec<_> = paths
2312 .iter()
2313 .map(|path| load_run_record_or_exit(path))
2314 .collect();
2315 runs.sort_by(|left, right| {
2316 (
2317 left.started_at.as_str(),
2318 left.workflow_id.as_str(),
2319 left.task.as_str(),
2320 )
2321 .cmp(&(
2322 right.started_at.as_str(),
2323 right.workflow_id.as_str(),
2324 right.task.as_str(),
2325 ))
2326 });
2327 runs
2328}
2329
2330pub(crate) fn parse_source_file(path: &str) -> (String, Vec<harn_parser::SNode>) {
2332 ensure_builtin_signatures_installed();
2333
2334 let source = match fs::read_to_string(path) {
2335 Ok(s) => s,
2336 Err(e) => {
2337 eprintln!("Error reading {path}: {e}");
2338 process::exit(1);
2339 }
2340 };
2341
2342 let mut lexer = Lexer::new(&source);
2343 let tokens = match lexer.tokenize() {
2344 Ok(t) => t,
2345 Err(e) => {
2346 let diagnostic = harn_parser::diagnostic::render_diagnostic_with_code(
2347 &source,
2348 path,
2349 &error_span_from_lex(&e),
2350 "error",
2351 harn_parser::diagnostic::lexer_error_code(&e),
2352 &e.to_string(),
2353 Some("here"),
2354 None,
2355 );
2356 eprint!("{diagnostic}");
2357 process::exit(1);
2358 }
2359 };
2360
2361 let mut parser = Parser::new(tokens);
2362 let program = match parser.parse() {
2363 Ok(p) => p,
2364 Err(err) => {
2365 if parser.all_errors().is_empty() {
2366 let span = error_span_from_parse(&err);
2367 let diagnostic = harn_parser::diagnostic::render_diagnostic_with_code(
2368 &source,
2369 path,
2370 &span,
2371 "error",
2372 harn_parser::diagnostic::parser_error_code(&err),
2373 &harn_parser::diagnostic::parser_error_message(&err),
2374 Some(harn_parser::diagnostic::parser_error_label(&err)),
2375 harn_parser::diagnostic::parser_error_help(&err),
2376 );
2377 eprint!("{diagnostic}");
2378 } else {
2379 for e in parser.all_errors() {
2380 let span = error_span_from_parse(e);
2381 let diagnostic = harn_parser::diagnostic::render_diagnostic_with_code(
2382 &source,
2383 path,
2384 &span,
2385 "error",
2386 harn_parser::diagnostic::parser_error_code(e),
2387 &harn_parser::diagnostic::parser_error_message(e),
2388 Some(harn_parser::diagnostic::parser_error_label(e)),
2389 harn_parser::diagnostic::parser_error_help(e),
2390 );
2391 eprint!("{diagnostic}");
2392 }
2393 }
2394 process::exit(1);
2395 }
2396 };
2397
2398 (source, program)
2399}
2400
2401fn error_span_from_lex(e: &harn_lexer::LexerError) -> harn_lexer::Span {
2402 match e {
2403 harn_lexer::LexerError::UnexpectedCharacter(_, span)
2404 | harn_lexer::LexerError::UnterminatedString(span)
2405 | harn_lexer::LexerError::IntegerLiteralOutOfRange(_, span)
2406 | harn_lexer::LexerError::UnterminatedBlockComment(span) => *span,
2407 }
2408}
2409
2410fn error_span_from_parse(e: &harn_parser::ParserError) -> harn_lexer::Span {
2411 match e {
2412 harn_parser::ParserError::Unexpected { span, .. } => *span,
2413 harn_parser::ParserError::UnexpectedEof { span, .. } => *span,
2414 }
2415}
2416
2417#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2422pub(crate) enum ExecStage {
2423 Parse,
2424 Typecheck,
2425 Compile,
2426 Runtime,
2427}
2428
2429impl ExecStage {
2430 pub(crate) fn label(self) -> &'static str {
2432 match self {
2433 ExecStage::Parse => "parse error",
2434 ExecStage::Typecheck => "type error",
2435 ExecStage::Compile => "compile error",
2436 ExecStage::Runtime => "runtime error",
2437 }
2438 }
2439}
2440
2441#[derive(Debug, Clone)]
2446pub(crate) struct ExecError {
2447 pub(crate) stage: ExecStage,
2448 pub(crate) message: String,
2449}
2450
2451impl ExecError {
2452 fn new(stage: ExecStage, message: impl Into<String>) -> Self {
2453 Self {
2454 stage,
2455 message: message.into(),
2456 }
2457 }
2458}
2459
2460impl std::fmt::Display for ExecError {
2461 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2462 f.write_str(&self.message)
2463 }
2464}
2465
2466pub(crate) async fn execute(source: &str, source_path: Option<&Path>) -> Result<String, ExecError> {
2468 execute_with_skill_dirs(source, source_path, &[]).await
2469}
2470
2471pub(crate) async fn execute_with_skill_dirs(
2472 source: &str,
2473 source_path: Option<&Path>,
2474 cli_skill_dirs: &[PathBuf],
2475) -> Result<String, ExecError> {
2476 execute_with_skill_dirs_and_optional_harness(source, source_path, cli_skill_dirs, None).await
2477}
2478
2479pub(crate) async fn execute_with_skill_dirs_and_harness(
2480 source: &str,
2481 source_path: Option<&Path>,
2482 cli_skill_dirs: &[PathBuf],
2483 harness: harn_vm::Harness,
2484) -> Result<String, ExecError> {
2485 execute_with_skill_dirs_and_optional_harness(source, source_path, cli_skill_dirs, Some(harness))
2486 .await
2487}
2488
2489async fn execute_with_skill_dirs_and_optional_harness(
2490 source: &str,
2491 source_path: Option<&Path>,
2492 cli_skill_dirs: &[PathBuf],
2493 harness: Option<harn_vm::Harness>,
2494) -> Result<String, ExecError> {
2495 let mut lexer = Lexer::new(source);
2496 let tokens = lexer
2497 .tokenize()
2498 .map_err(|e| ExecError::new(ExecStage::Parse, e.to_string()))?;
2499 let mut parser = Parser::new(tokens);
2500 let program = parser
2501 .parse()
2502 .map_err(|e| ExecError::new(ExecStage::Parse, e.to_string()))?;
2503
2504 let mut checker = TypeChecker::new();
2509 if let Some(path) = source_path {
2510 checker = crate::typecheck_imports::checker_with_resolved_imports(checker, path);
2511 }
2512 let type_diagnostics = checker.check(&program);
2513 let mut warning_lines = Vec::new();
2514 for diag in &type_diagnostics {
2515 match diag.severity {
2516 DiagnosticSeverity::Error => {
2517 return Err(ExecError::new(ExecStage::Typecheck, diag.message.clone()))
2518 }
2519 DiagnosticSeverity::Warning => {
2520 warning_lines.push(format!("warning: {}", diag.message));
2521 }
2522 }
2523 }
2524
2525 let compiler = source_path
2526 .map(|path| compiler_for_source(path, source))
2527 .unwrap_or_default();
2528 let chunk = compiler
2529 .compile(&program)
2530 .map_err(|e| ExecError::new(ExecStage::Compile, e.to_string()))?;
2531
2532 let local = tokio::task::LocalSet::new();
2533 local
2534 .run_until(async {
2535 let mut vm = harn_vm::Vm::new();
2536 harn_vm::register_vm_stdlib(&mut vm);
2537 install_default_hostlib(&mut vm);
2538 let source_parent = source_path
2539 .and_then(|p| p.parent())
2540 .unwrap_or(std::path::Path::new("."));
2541 let project_root = harn_vm::stdlib::process::find_project_root(source_parent);
2542 let store_base = project_root.as_deref().unwrap_or(source_parent);
2543 let execution_cwd = std::env::current_dir()
2544 .unwrap_or_else(|_| std::path::PathBuf::from("."))
2545 .to_string_lossy()
2546 .into_owned();
2547 let source_dir = source_parent.to_string_lossy().into_owned();
2548 if source_path.is_some_and(is_conformance_path) {
2549 harn_vm::event_log::install_memory_for_current_thread(64);
2550 }
2551 harn_vm::register_store_builtins(&mut vm, store_base);
2552 harn_vm::register_metadata_builtins(&mut vm, store_base);
2553 let pipeline_name = source_path
2554 .and_then(|p| p.file_stem())
2555 .and_then(|s| s.to_str())
2556 .unwrap_or("default");
2557 harn_vm::register_checkpoint_builtins(&mut vm, store_base, pipeline_name);
2558 harn_vm::stdlib::process::set_thread_execution_context(Some(
2559 harn_vm::orchestration::RunExecutionRecord {
2560 cwd: Some(execution_cwd),
2561 project_root: project_root
2562 .as_ref()
2563 .map(|root| root.to_string_lossy().into_owned()),
2564 source_dir: Some(source_dir),
2565 ..Default::default()
2566 },
2567 ));
2568 if let Some(ref root) = project_root {
2569 vm.set_project_root(root);
2570 }
2571 if let Some(path) = source_path {
2572 if let Some(parent) = path.parent() {
2573 if !parent.as_os_str().is_empty() {
2574 vm.set_source_dir(parent);
2575 }
2576 }
2577 }
2578 let loaded = skill_loader::load_skills(&skill_loader::SkillLoaderInputs {
2582 cli_dirs: cli_skill_dirs.to_vec(),
2583 source_path: source_path.map(Path::to_path_buf),
2584 });
2585 skill_loader::emit_loader_warnings(&loaded.loader_warnings);
2586 skill_loader::install_skills_global(&mut vm, &loaded);
2587 let runtime_harness = match harness {
2588 Some(harness) => harness,
2589 None => {
2590 let resolved = match source_path {
2591 Some(path) => default_harness_for_manifest_or_base_dir(path, store_base),
2592 None => default_harness_for_base_dir(store_base),
2593 };
2594 resolved.map_err(|error| {
2595 ExecError::new(
2596 ExecStage::Runtime,
2597 format!("failed to configure harness secret provider: {error}"),
2598 )
2599 })
2600 }?,
2601 };
2602 vm.set_harness(runtime_harness);
2603 if let Some(path) = source_path {
2604 let extensions = package::load_runtime_extensions(path);
2605 package::install_runtime_extensions(&extensions);
2606 package::install_manifest_triggers(&mut vm, &extensions)
2607 .await
2608 .map_err(|error| {
2609 ExecError::new(
2610 ExecStage::Runtime,
2611 format!("failed to install manifest triggers: {error}"),
2612 )
2613 })?;
2614 package::install_manifest_hooks(&mut vm, &extensions)
2615 .await
2616 .map_err(|error| {
2617 ExecError::new(
2618 ExecStage::Runtime,
2619 format!("failed to install manifest hooks: {error}"),
2620 )
2621 })?;
2622 }
2623 let _event_log = harn_vm::event_log::active_event_log()
2624 .unwrap_or_else(|| harn_vm::event_log::install_memory_for_current_thread(64));
2625 let connector_clients_installed =
2626 should_install_default_connector_clients(source, source_path);
2627 if connector_clients_installed {
2628 install_default_connector_clients(store_base)
2629 .await
2630 .map_err(|error| {
2631 ExecError::new(
2632 ExecStage::Runtime,
2633 format!("failed to initialize connector clients: {error}"),
2634 )
2635 })?;
2636 }
2637 let execution_result = vm
2638 .execute(&chunk)
2639 .await
2640 .map_err(|e| ExecError::new(ExecStage::Runtime, e.to_string()));
2641 harn_vm::egress::reset_egress_policy_for_host();
2642 if connector_clients_installed {
2643 harn_vm::clear_active_connector_clients();
2644 }
2645 harn_vm::stdlib::process::set_thread_execution_context(None);
2646 execution_result?;
2647 let mut output = String::new();
2648 for wl in &warning_lines {
2649 output.push_str(wl);
2650 output.push('\n');
2651 }
2652 output.push_str(vm.output());
2653 Ok(output)
2654 })
2655 .await
2656}
2657
2658fn should_install_default_connector_clients(source: &str, source_path: Option<&Path>) -> bool {
2659 if !source_path.is_some_and(is_conformance_path) {
2660 return true;
2661 }
2662 source.contains("connector_call")
2663 || source.contains("std/connectors")
2664 || source.contains("connectors/")
2665}
2666
2667fn is_conformance_path(path: &Path) -> bool {
2668 path.components()
2669 .any(|component| component.as_os_str() == "conformance")
2670}
2671
2672async fn install_default_connector_clients(base_dir: &Path) -> Result<(), String> {
2673 let event_log = harn_vm::event_log::active_event_log()
2674 .unwrap_or_else(|| harn_vm::event_log::install_memory_for_current_thread(64));
2675 let secret_namespace = connector_secret_namespace(base_dir);
2676 let secrets: Arc<dyn harn_vm::secrets::SecretProvider> = Arc::new(
2677 harn_vm::secrets::configured_default_chain(secret_namespace)
2678 .map_err(|error| format!("failed to configure secret providers: {error}"))?,
2679 );
2680
2681 let registry = harn_vm::ConnectorRegistry::default();
2682 let metrics = Arc::new(harn_vm::MetricsRegistry::default());
2683 let inbox = Arc::new(
2684 harn_vm::InboxIndex::new(event_log.clone(), metrics.clone())
2685 .await
2686 .map_err(|error| error.to_string())?,
2687 );
2688 registry
2689 .init_all(harn_vm::ConnectorCtx {
2690 event_log,
2691 secrets,
2692 inbox,
2693 metrics,
2694 rate_limiter: Arc::new(harn_vm::RateLimiterFactory::default()),
2695 })
2696 .await
2697 .map_err(|error| error.to_string())?;
2698 let clients = registry.client_map().await;
2699 harn_vm::install_active_connector_clients(clients);
2700 Ok(())
2701}
2702
2703fn connector_secret_namespace(base_dir: &Path) -> String {
2704 match std::env::var("HARN_SECRET_NAMESPACE") {
2705 Ok(namespace) if !namespace.trim().is_empty() => namespace,
2706 _ => {
2707 let leaf = base_dir
2708 .file_name()
2709 .and_then(|name| name.to_str())
2710 .filter(|name| !name.is_empty())
2711 .unwrap_or("workspace");
2712 format!("harn/{leaf}")
2713 }
2714 }
2715}
2716
2717pub(crate) fn default_harness_for_base_dir(base_dir: &Path) -> Result<harn_vm::Harness, String> {
2718 let secret_namespace = connector_secret_namespace(base_dir);
2719 default_harness_for_secret_namespace(secret_namespace)
2720}
2721
2722pub(crate) fn default_harness_for_manifest_or_base_dir(
2723 source_path: &Path,
2724 base_dir: &Path,
2725) -> Result<harn_vm::Harness, String> {
2726 let secret_namespace = package::find_nearest_manifest_dir(source_path)
2727 .map(|manifest_dir| connector_secret_namespace(&manifest_dir))
2728 .unwrap_or_else(|| connector_secret_namespace(base_dir));
2729 default_harness_for_secret_namespace(secret_namespace)
2730}
2731
2732fn default_harness_for_secret_namespace(
2733 secret_namespace: String,
2734) -> Result<harn_vm::Harness, String> {
2735 let secret_provider = Arc::new(
2736 harn_vm::secrets::configured_default_chain(secret_namespace)
2737 .map_err(|error| error.to_string())?,
2738 );
2739 Ok(harn_vm::Harness::real().with_secret_provider(secret_provider))
2740}
2741
2742#[cfg(test)]
2743mod main_tests {
2744 use super::{
2745 normalize_serve_args, serve_subcommand_names, should_install_default_connector_clients,
2746 };
2747 use std::path::Path;
2748
2749 #[test]
2750 fn normalize_serve_args_inserts_a2a_for_legacy_shape() {
2751 let args = normalize_serve_args(vec![
2752 "harn".to_string(),
2753 "serve".to_string(),
2754 "--port".to_string(),
2755 "3000".to_string(),
2756 "agent.harn".to_string(),
2757 ]);
2758 assert_eq!(
2759 args,
2760 vec![
2761 "harn".to_string(),
2762 "serve".to_string(),
2763 "a2a".to_string(),
2764 "--port".to_string(),
2765 "3000".to_string(),
2766 "agent.harn".to_string(),
2767 ]
2768 );
2769 }
2770
2771 #[test]
2772 fn normalize_serve_args_preserves_explicit_subcommands() {
2773 for transport in serve_subcommand_names() {
2778 let args = normalize_serve_args(vec![
2779 "harn".to_string(),
2780 "serve".to_string(),
2781 transport.clone(),
2782 "server.harn".to_string(),
2783 ]);
2784 assert_eq!(
2785 args,
2786 vec![
2787 "harn".to_string(),
2788 "serve".to_string(),
2789 transport.clone(),
2790 "server.harn".to_string(),
2791 ],
2792 "transport `{transport}` should not be rewritten",
2793 );
2794 }
2795 }
2796
2797 #[test]
2798 fn normalize_serve_args_recognizes_site_subcommand() {
2799 let args = normalize_serve_args(vec![
2800 "harn".to_string(),
2801 "serve".to_string(),
2802 "site".to_string(),
2803 "server.harn".to_string(),
2804 ]);
2805 assert_eq!(args.get(2).map(String::as_str), Some("site"));
2806 }
2807
2808 #[test]
2809 fn conformance_skips_connector_clients_unless_fixture_uses_connectors() {
2810 let path = Path::new("conformance/tests/language/basic.harn");
2811 assert!(!should_install_default_connector_clients(
2812 "__io_println(1)",
2813 Some(path)
2814 ));
2815 assert!(!should_install_default_connector_clients(
2816 "trust_graph_verify_chain()",
2817 Some(path)
2818 ));
2819 assert!(should_install_default_connector_clients(
2820 "import { post_message } from \"std/connectors/slack\"",
2821 Some(path)
2822 ));
2823 assert!(should_install_default_connector_clients(
2824 "__io_println(1)",
2825 Some(Path::new("examples/demo.harn"))
2826 ));
2827 }
2828}