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