Skip to main content

harn_cli/commands/
mod.rs

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