Skip to main content

harn_cli/commands/
mod.rs

1pub(crate) mod agents_conformance;
2pub(crate) mod app;
3mod app_host_assets;
4pub(crate) mod bench;
5pub(crate) mod canon;
6pub(crate) mod check;
7pub(crate) mod codemod;
8pub(crate) mod config_cmd;
9pub(crate) mod conformance_helper;
10pub(crate) mod connect;
11pub(crate) mod connector;
12pub(crate) mod connector_schema_codegen;
13pub(crate) mod contracts;
14pub(crate) mod counterfactual;
15pub(crate) mod crystallize;
16pub mod demo;
17pub(crate) mod dev;
18pub(crate) mod diagnostics_catalog;
19pub(crate) mod dispatch_explain;
20pub(crate) mod doc;
21pub(crate) mod doctor;
22pub(crate) mod dump_highlight_keywords;
23pub(crate) mod dump_portable_benchmark_schema;
24pub(crate) mod dump_prompt_grammar;
25pub(crate) mod dump_protocol_artifacts;
26pub(crate) mod dump_trigger_quickref;
27pub(crate) mod embedded_report;
28pub mod eval_coding_agent;
29pub(crate) mod eval_coding_agent_preset;
30pub mod eval_context;
31pub(crate) mod eval_model_selector;
32pub mod eval_prompt;
33pub(crate) mod eval_prompt_context;
34pub(crate) mod eval_scope_triage;
35pub mod eval_skill_gate;
36pub(crate) mod eval_tool_calls;
37pub(crate) mod explain;
38pub(crate) mod fix;
39pub mod flow;
40pub(crate) mod generate;
41pub(crate) mod graph;
42pub(crate) mod guard;
43pub(crate) mod hardware;
44pub(crate) mod harness_migration_codegen;
45pub(crate) mod host;
46pub(crate) mod init;
47pub(crate) mod json_schemas;
48pub(crate) mod local;
49pub(crate) mod local_readiness;
50pub(crate) mod mcp;
51pub(crate) mod merge_captain;
52pub(crate) mod merge_captain_mock;
53pub(crate) mod models;
54pub mod orchestrator;
55pub mod pack;
56pub(crate) mod package_scaffold;
57pub(crate) mod package_verify;
58pub(crate) mod parse_tokens;
59pub mod persona;
60pub mod persona_activation;
61pub mod persona_apply;
62pub mod persona_dispatch;
63pub mod persona_doctor;
64pub mod persona_prompt;
65pub mod persona_scaffold;
66pub mod persona_supervision;
67#[cfg(test)]
68pub(crate) mod persona_test_support;
69pub(crate) mod pg_codegen;
70pub mod playground;
71pub(crate) mod portable;
72pub(crate) mod portable_source;
73pub(crate) mod portal;
74pub mod precompile;
75pub(crate) mod protocol_conformance;
76pub(crate) mod provider;
77pub(crate) mod provider_capabilities;
78pub(crate) mod provider_limits;
79pub(crate) mod provider_report;
80pub(crate) mod provider_support;
81pub(crate) mod provider_tool_calibrate;
82pub(crate) mod providers;
83pub(crate) use provider_tool_calibrate::run as run_tool_calibrate;
84pub(crate) mod quickstart;
85pub(crate) mod repl;
86pub(crate) mod replay;
87pub(crate) mod routes;
88pub(crate) mod rule;
89pub(crate) mod rules_cli;
90pub mod run;
91pub(crate) mod run_report;
92pub(crate) mod run_review;
93pub(crate) mod runs_export_training;
94pub(crate) mod scaffold_common;
95pub(crate) mod scan;
96pub(crate) mod serve;
97pub(crate) mod session;
98pub(crate) mod skill;
99pub(crate) mod skills;
100pub(crate) mod supervisor;
101pub(crate) mod test;
102pub mod test_bench;
103pub(crate) mod test_worker;
104pub mod time;
105pub(crate) mod tool;
106pub(crate) mod tool_mode_parity;
107pub(crate) mod trace;
108pub mod trigger;
109pub(crate) mod trust;
110pub(crate) mod try_cmd;
111pub(crate) mod upgrade;
112pub(crate) mod usage;
113pub(crate) mod viz;
114pub(crate) mod workflow;
115
116use std::path::{Path, PathBuf};
117
118use harn_vm::ignore_policy::{self, IgnorePolicy};
119use ignore::WalkBuilder;
120
121/// Nearest-rank percentile over a pre-sorted slice. `quantile` is clamped to
122/// `[0, 1]`; returns `None` for an empty slice. Shared by the latency
123/// summaries in the eval and orchestrator stats commands.
124pub(crate) fn nearest_rank_percentile(sorted: &[u64], quantile: f64) -> Option<u64> {
125    if sorted.is_empty() {
126        return None;
127    }
128    let rank = ((sorted.len() as f64 * quantile.clamp(0.0, 1.0)).ceil() as usize)
129        .saturating_sub(1)
130        .min(sorted.len() - 1);
131    sorted.get(rank).copied()
132}
133
134const GENERATED_SOURCE_WALK_DIRS: &[&str] = &[
135    ".burin",
136    ".build",
137    ".claude",
138    ".codex",
139    ".git",
140    ".next",
141    ".svelte-kit",
142    ".turbo",
143    ".venv",
144    "build",
145    "coverage",
146    "dist",
147    "node_modules",
148    "target",
149];
150
151pub(crate) fn should_skip_recursive_source_dir(dir: &Path) -> bool {
152    let Some(name) = dir.file_name().and_then(|name| name.to_str()) else {
153        return false;
154    };
155    GENERATED_SOURCE_WALK_DIRS.contains(&name)
156        || crate::path_policy::is_harn_internal_entry(
157            name,
158            crate::path_policy::PathEntryKind::Directory,
159        )
160}
161
162pub(crate) fn should_skip_recursive_source_file(file: &Path) -> bool {
163    let Some(name) = file.file_name().and_then(|name| name.to_str()) else {
164        return false;
165    };
166    // Harn can emit generated source snapshots such as `.harn-eval-*.harn`.
167    // This source-file rule is separate from directory-owned runtime state.
168    name.starts_with(".harn-")
169}
170
171#[derive(Default)]
172pub(crate) struct SourceTargets {
173    pub(crate) harn: Vec<PathBuf>,
174    pub(crate) prompts: Vec<PathBuf>,
175}
176
177impl SourceTargets {
178    fn sort_and_dedup(&mut self) {
179        self.harn.sort();
180        self.harn.dedup();
181        self.prompts.sort();
182        self.prompts.dedup();
183    }
184}
185
186pub(crate) fn collect_source_targets(
187    targets: &[&str],
188    include_harn: bool,
189    include_prompts: bool,
190) -> SourceTargets {
191    let mut files = SourceTargets::default();
192    for target in targets {
193        let path = Path::new(target);
194        if path.is_dir() {
195            collect_source_targets_dir(path, include_harn, include_prompts, &mut files);
196        } else {
197            push_matching_source_target(path, include_harn, include_prompts, false, &mut files);
198        }
199    }
200    files.sort_and_dedup();
201    files
202}
203
204fn collect_source_targets_dir(
205    dir: &Path,
206    include_harn: bool,
207    include_prompts: bool,
208    files: &mut SourceTargets,
209) {
210    let root = dir.to_path_buf();
211    let mut walker = WalkBuilder::new(dir);
212    // `harn_vm::ignore_policy` owns what a Harn walk skips, and this walk is
213    // no exception even though it enumerates lint targets rather than user
214    // data. Configuring the `ignore` crate here by hand is what broke it: the
215    // crate's upward search for ignore files is only bounded when a
216    // repository anchors it, so `require_git(false)` plus `parents(true)` let
217    // a `.gitignore` *above* the checkout decide which of the project's own
218    // sources exist. A repository cloned under an ignoring parent — an agent
219    // worktree beneath `~/.cursor`, whose `.gitignore` is `*` — enumerated as
220    // empty, so every directory target expanded to nothing.
221    //
222    // Hidden entries stay visible: `.github/` and friends hold real sources,
223    // and dotfile filtering is a separate axis from the ignore stack.
224    //
225    // A configuration error means only that the built-in directory layer
226    // could not be materialized. This function has no error channel, and the
227    // walk is already correct without that layer — it would over-include some
228    // build directories, which the caller then reports on and a reader can
229    // see. Returning nothing is the failure worth avoiding.
230    let _ = ignore_policy::configure(&mut walker, dir, IgnorePolicy::Project, true);
231    walker.follow_links(false).filter_entry(move |entry| {
232        let path = entry.path();
233        if path == root {
234            return true;
235        }
236        if path.is_dir() {
237            !should_skip_recursive_source_dir(path)
238        } else {
239            !should_skip_recursive_source_file(path)
240        }
241    });
242
243    for entry in walker.build().filter_map(Result::ok) {
244        let path = entry.path();
245        if entry
246            .file_type()
247            .is_some_and(|file_type| file_type.is_file())
248        {
249            push_matching_source_target(path, include_harn, include_prompts, true, files);
250        }
251    }
252}
253
254fn push_matching_source_target(
255    path: &Path,
256    include_harn: bool,
257    include_prompts: bool,
258    honor_skip_marker: bool,
259    files: &mut SourceTargets,
260) {
261    if include_prompts && is_harn_prompt_file(path) {
262        files.prompts.push(path.to_path_buf());
263    } else if include_harn && is_harn_program_file(path) {
264        let skip_marker = path.with_extension("conformance-skip");
265        if !honor_skip_marker || !skip_marker.exists() {
266            files.harn.push(path.to_path_buf());
267        }
268    }
269}
270
271pub(crate) fn is_harn_program_file(path: &Path) -> bool {
272    let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
273        return false;
274    };
275    if name.ends_with(".harn.prompt") || name.ends_with(".prompt") {
276        return false;
277    }
278    name.ends_with(".harn") || name.ends_with(".harn.txt")
279}
280
281pub(crate) fn is_harn_prompt_file(path: &Path) -> bool {
282    let Some(name) = path.file_name().and_then(|name| name.to_str()) else {
283        return false;
284    };
285    name.ends_with(".harn.prompt") || name.ends_with(".prompt")
286}
287
288/// Recursively collect `.harn` files under `dir`, sorted by path. Files with a
289/// sibling `<name>.conformance-skip` marker are excluded — used to temporarily
290/// park tests that are tracking a known regression in an issue so `make test`
291/// + `harn test conformance` can stay green while the fix is in flight.
292pub(crate) fn collect_harn_files(dir: &Path, out: &mut Vec<PathBuf>) {
293    if let Ok(entries) = std::fs::read_dir(dir) {
294        let mut entries: Vec<_> = entries.filter_map(|e| e.ok()).collect();
295        entries.sort_by_key(|e| e.path());
296        for entry in entries {
297            let path = entry.path();
298            if path.is_dir() {
299                if should_skip_recursive_source_dir(&path) {
300                    continue;
301                }
302                collect_harn_files(&path, out);
303            } else if should_skip_recursive_source_file(&path) {
304                continue;
305            } else if path.extension().is_some_and(|ext| ext == "harn") {
306                let skip_marker = path.with_extension("conformance-skip");
307                if skip_marker.exists() {
308                    continue;
309                }
310                out.push(path);
311            }
312        }
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::nearest_rank_percentile;
319
320    #[test]
321    fn percentile_handles_empty_and_bounds() {
322        assert_eq!(nearest_rank_percentile(&[], 0.5), None);
323        let data = [10u64, 20, 30, 40, 50];
324        assert_eq!(nearest_rank_percentile(&data, 0.0), Some(10));
325        assert_eq!(nearest_rank_percentile(&data, 0.5), Some(30));
326        assert_eq!(nearest_rank_percentile(&data, 1.0), Some(50));
327        // Out-of-range quantiles clamp instead of panicking.
328        assert_eq!(nearest_rank_percentile(&data, 2.0), Some(50));
329        assert_eq!(nearest_rank_percentile(&[7u64], 0.99), Some(7));
330    }
331}