Skip to main content

harn_cli/
lib.rs

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