use std::net::SocketAddr;
use std::path::PathBuf;
use std::time::Duration as StdDuration;
use clap::{ArgAction, Args, Parser, Subcommand, ValueEnum};
#[derive(Debug, Parser)]
#[command(
name = "harn",
about = "The agent harness language",
version,
disable_help_subcommand = false,
arg_required_else_help = true
)]
pub(crate) struct Cli {
#[command(subcommand)]
pub command: Option<Command>,
}
#[derive(Debug, Subcommand)]
pub(crate) enum Command {
#[command(long_about = "\
Execute a .harn file or an inline expression.
USAGE
harn run script.harn
harn run -e 'println(\"hello\")'
harn run script.harn -- arg1 arg2 (script reads `argv` as list<string>)
CONCURRENCY
Harn supports first-class concurrency primitives:
- spawn { ... } — launch a task, return a handle
- parallel each LIST — concurrent map
- parallel settle LIST — concurrent map, collect Ok/Err
- parallel N — N-way fan-out
- with { max_concurrent: N } — cap in-flight workers
- channels, retry, select
https://harnlang.com/concurrency.html
LLM THROTTLING
Providers can be rate-limited via `rpm:` in harn.toml / providers.toml
or via `HARN_RATE_LIMIT_<PROVIDER>=N`. Rate limits control throughput
(RPM); `max_concurrent` on `parallel` caps simultaneous in-flight jobs.
SCRIPTING
LLM-readable one-pager: https://harnlang.com/docs/llm/harn-quickref.md
Human cheatsheet: https://harnlang.com/scripting-cheatsheet.html
Full docs: https://harnlang.com/
")]
Run(RunArgs),
Check(CheckArgs),
Explain(ExplainArgs),
Contracts(ContractsArgs),
Lint(PathTargetsArgs),
Fmt(FmtArgs),
Test(TestArgs),
Init(InitArgs),
New(InitArgs),
Doctor(DoctorArgs),
Connect(ConnectArgs),
Serve(ServeArgs),
Acp(AcpArgs),
#[command(hide = true, name = "mcp-serve")]
McpServe(LegacyMcpServeArgs),
Mcp(McpArgs),
Watch(WatchArgs),
Portal(PortalArgs),
Trigger(TriggerArgs),
Trust(TrustArgs),
Orchestrator(OrchestratorArgs),
Playground(PlaygroundArgs),
Runs(RunsArgs),
Replay(ReplayArgs),
Eval(EvalArgs),
Repl,
Bench(BenchArgs),
Viz(VizArgs),
Install(InstallArgs),
Add(AddArgs),
Update(UpdateArgs),
Remove(RemoveArgs),
Lock,
ModelInfo(ModelInfoArgs),
Skills(SkillsArgs),
Skill(SkillArgs),
Version,
#[command(hide = true, name = "dump-highlight-keywords")]
DumpHighlightKeywords(DumpHighlightKeywordsArgs),
}
#[derive(Debug, Args)]
pub(crate) struct RunArgs {
#[arg(long)]
pub trace: bool,
#[arg(long, conflicts_with = "allow")]
pub deny: Option<String>,
#[arg(long, conflicts_with = "deny")]
pub allow: Option<String>,
#[arg(short = 'e')]
pub eval: Option<String>,
#[arg(long = "skill-dir", value_name = "PATH")]
pub skill_dir: Vec<String>,
#[arg(
long = "llm-mock",
value_name = "PATH",
conflicts_with = "llm_mock_record"
)]
pub llm_mock: Option<String>,
#[arg(
long = "llm-mock-record",
value_name = "PATH",
conflicts_with = "llm_mock"
)]
pub llm_mock_record: Option<String>,
pub file: Option<String>,
#[arg(last = true)]
pub argv: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct CheckArgs {
#[arg(long = "host-capabilities")]
pub host_capabilities: Option<String>,
#[arg(long = "bundle-root")]
pub bundle_root: Option<String>,
#[arg(long = "strict-types")]
pub strict_types: bool,
#[arg(long = "workspace")]
pub workspace: bool,
#[arg(long = "preflight", value_name = "SEVERITY")]
pub preflight: Option<String>,
#[arg(long = "invariants")]
pub invariants: bool,
pub targets: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct ExplainArgs {
#[arg(long = "invariant", value_name = "NAME")]
pub invariant: String,
pub function: String,
pub file: String,
}
#[derive(Debug, Args)]
pub(crate) struct ContractsArgs {
#[command(subcommand)]
pub command: ContractsCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum ContractsCommand {
Builtins(ContractsOutputArgs),
HostCapabilities(ContractsHostCapabilitiesArgs),
Bundle(ContractsBundleArgs),
}
#[derive(Debug, Args)]
pub(crate) struct ContractsOutputArgs {
#[arg(long, default_value_t = true, action = ArgAction::Set)]
pub pretty: bool,
}
#[derive(Debug, Args)]
pub(crate) struct ContractsHostCapabilitiesArgs {
#[arg(long = "host-capabilities")]
pub host_capabilities: Option<String>,
#[arg(long, default_value_t = true, action = ArgAction::Set)]
pub pretty: bool,
}
#[derive(Debug, Args)]
pub(crate) struct ContractsBundleArgs {
#[arg(long = "host-capabilities")]
pub host_capabilities: Option<String>,
#[arg(long = "bundle-root")]
pub bundle_root: Option<String>,
#[arg(long)]
pub verify: bool,
#[arg(long, default_value_t = true, action = ArgAction::Set)]
pub pretty: bool,
#[arg(required = true)]
pub targets: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct PathTargetsArgs {
#[arg(long)]
pub fix: bool,
#[arg(long = "require-file-header")]
pub require_file_header: bool,
#[arg(required = true)]
pub targets: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct FmtArgs {
#[arg(long)]
pub check: bool,
#[arg(long = "line-width")]
pub line_width: Option<usize>,
#[arg(long = "separator-width")]
pub separator_width: Option<usize>,
#[arg(required = true)]
pub targets: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct TestArgs {
#[arg(long)]
pub filter: Option<String>,
#[arg(long)]
pub junit: Option<String>,
#[arg(long, default_value_t = 30_000)]
pub timeout: u64,
#[arg(long)]
pub parallel: bool,
#[arg(long)]
pub watch: bool,
#[arg(short = 'v', long = "verbose", action = ArgAction::SetTrue)]
pub verbose: bool,
#[arg(long, action = ArgAction::SetTrue)]
pub timing: bool,
#[arg(long)]
pub record: bool,
#[arg(long)]
pub replay: bool,
#[arg(long = "skill-dir", value_name = "PATH")]
pub skill_dir: Vec<String>,
pub target: Option<String>,
pub selection: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct InitArgs {
pub name: Option<String>,
#[arg(long, value_enum, default_value_t = ProjectTemplate::Basic)]
pub template: ProjectTemplate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum ProjectTemplate {
Basic,
Agent,
#[value(name = "mcp-server")]
McpServer,
Eval,
#[value(name = "pipeline-lab")]
PipelineLab,
}
#[derive(Debug, Args)]
pub(crate) struct DoctorArgs {
#[arg(long)]
pub no_network: bool,
}
#[derive(Debug, Args)]
pub(crate) struct ConnectArgs {
#[command(subcommand)]
pub command: ConnectCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum ConnectCommand {
Linear(ConnectLinearArgs),
}
#[derive(Debug, Args)]
pub(crate) struct ConnectLinearArgs {
#[arg(long)]
pub url: String,
#[arg(long)]
pub config: Option<String>,
#[arg(long, conflicts_with = "all_public_teams")]
pub team_id: Option<String>,
#[arg(long, conflicts_with = "team_id")]
pub all_public_teams: bool,
#[arg(long)]
pub label: Option<String>,
#[arg(long)]
pub api_base_url: Option<String>,
#[arg(long, conflicts_with_all = ["api_key_secret", "access_token", "access_token_secret"])]
pub api_key: Option<String>,
#[arg(long, conflicts_with_all = ["api_key", "access_token", "access_token_secret"])]
pub api_key_secret: Option<String>,
#[arg(long, conflicts_with_all = ["api_key", "api_key_secret", "access_token_secret"])]
pub access_token: Option<String>,
#[arg(long, conflicts_with_all = ["api_key", "api_key_secret", "access_token"])]
pub access_token_secret: Option<String>,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub(crate) struct VizArgs {
pub file: String,
#[arg(short, long)]
pub output: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct BenchArgs {
pub file: String,
#[arg(short = 'n', long, default_value_t = 10)]
pub iterations: usize,
}
#[derive(Debug, Args)]
pub(crate) struct ServeArgs {
#[command(subcommand)]
pub command: ServeCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum ServeCommand {
A2a(A2aServeArgs),
Mcp(ServeMcpArgs),
}
#[derive(Debug, Args)]
pub(crate) struct A2aServeArgs {
#[arg(long, default_value_t = 8080)]
pub port: u16,
pub file: String,
}
#[derive(Debug, Args)]
pub(crate) struct ServeMcpArgs {
#[arg(long, value_enum, default_value_t = McpServeTransport::Stdio)]
pub transport: McpServeTransport,
#[arg(
long,
env = "HARN_SERVE_MCP_BIND",
default_value = "127.0.0.1:8765",
value_name = "ADDR"
)]
pub bind: SocketAddr,
#[arg(long, default_value = "/mcp", value_name = "PATH")]
pub path: String,
#[arg(long = "sse-path", default_value = "/sse", value_name = "PATH")]
pub sse_path: String,
#[arg(
long = "messages-path",
default_value = "/messages",
value_name = "PATH"
)]
pub messages_path: String,
#[arg(long = "api-key", env = "HARN_SERVE_API_KEY", value_delimiter = ',')]
pub api_key: Vec<String>,
#[arg(long = "hmac-secret", env = "HARN_SERVE_HMAC_SECRET")]
pub hmac_secret: Option<String>,
pub file: String,
}
#[derive(Debug, Args)]
pub(crate) struct AcpArgs {
pub pipeline: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct LegacyMcpServeArgs {
pub file: String,
#[arg(long = "card", value_name = "PATH_OR_JSON")]
pub card: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum McpServeTransport {
Stdio,
Http,
}
#[derive(Debug, Args)]
pub(crate) struct McpArgs {
#[command(subcommand)]
pub command: McpCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum McpCommand {
Serve(McpServeArgs),
Login(McpLoginArgs),
Logout(McpServerRefArgs),
Status(McpServerRefArgs),
RedirectUri,
}
#[derive(Debug, Args)]
pub(crate) struct McpServeArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
#[arg(long, value_enum, default_value_t = McpServeTransport::Stdio)]
pub transport: McpServeTransport,
#[arg(
long,
env = "HARN_MCP_SERVE_BIND",
default_value = "127.0.0.1:8765",
value_name = "ADDR"
)]
pub bind: SocketAddr,
#[arg(long, default_value = "/mcp", value_name = "PATH")]
pub path: String,
#[arg(long = "sse-path", default_value = "/sse", value_name = "PATH")]
pub sse_path: String,
#[arg(
long = "messages-path",
default_value = "/messages",
value_name = "PATH"
)]
pub messages_path: String,
}
#[derive(Debug, Args)]
pub(crate) struct McpLoginArgs {
pub target: Option<String>,
#[arg(long)]
pub url: Option<String>,
#[arg(long = "client-id")]
pub client_id: Option<String>,
#[arg(long = "client-secret")]
pub client_secret: Option<String>,
#[arg(long = "scope")]
pub scope: Option<String>,
#[arg(
long = "redirect-uri",
default_value = "http://127.0.0.1:9783/oauth/callback"
)]
pub redirect_uri: String,
}
#[derive(Debug, Args)]
pub(crate) struct McpServerRefArgs {
pub target: Option<String>,
#[arg(long)]
pub url: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct WatchArgs {
#[arg(long, conflicts_with = "allow")]
pub deny: Option<String>,
#[arg(long, conflicts_with = "deny")]
pub allow: Option<String>,
pub file: String,
}
#[derive(Debug, Args)]
pub(crate) struct PortalArgs {
#[arg(long, default_value = ".harn-runs")]
pub dir: String,
#[arg(long, default_value = "127.0.0.1")]
pub host: String,
#[arg(long, default_value_t = 4721)]
pub port: u16,
#[arg(long, default_value_t = true, action = ArgAction::Set)]
pub open: bool,
}
#[derive(Debug, Args)]
pub(crate) struct TriggerArgs {
#[command(subcommand)]
pub command: TriggerCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum TriggerCommand {
Replay(TriggerReplayArgs),
Cancel(TriggerCancelArgs),
}
#[derive(Debug, Args)]
pub(crate) struct TriggerReplayArgs {
#[arg(required_unless_present = "where_expr", conflicts_with = "where_expr")]
pub event_id: Option<String>,
#[arg(long = "where", value_name = "EXPR", conflicts_with = "event_id")]
pub where_expr: Option<String>,
#[arg(long)]
pub diff: bool,
#[arg(long = "as-of", value_name = "TIMESTAMP")]
pub as_of: Option<String>,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub progress: bool,
#[arg(long = "rate-limit", value_name = "OPS_PER_SEC")]
pub rate_limit: Option<f64>,
}
#[derive(Debug, Args)]
pub(crate) struct TriggerCancelArgs {
#[arg(required_unless_present = "where_expr", conflicts_with = "where_expr")]
pub event_id: Option<String>,
#[arg(long = "where", value_name = "EXPR", conflicts_with = "event_id")]
pub where_expr: Option<String>,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub progress: bool,
#[arg(long = "rate-limit", value_name = "OPS_PER_SEC")]
pub rate_limit: Option<f64>,
}
#[derive(Debug, Args)]
pub(crate) struct TrustArgs {
#[command(subcommand)]
pub command: TrustCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum TrustCommand {
Query(TrustQueryArgs),
Promote(TrustPromoteArgs),
Demote(TrustDemoteArgs),
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum TrustTierArg {
Shadow,
Suggest,
ActWithApproval,
ActAuto,
}
impl From<TrustTierArg> for harn_vm::AutonomyTier {
fn from(value: TrustTierArg) -> Self {
match value {
TrustTierArg::Shadow => harn_vm::AutonomyTier::Shadow,
TrustTierArg::Suggest => harn_vm::AutonomyTier::Suggest,
TrustTierArg::ActWithApproval => harn_vm::AutonomyTier::ActWithApproval,
TrustTierArg::ActAuto => harn_vm::AutonomyTier::ActAuto,
}
}
}
#[derive(Debug, Clone, Copy, ValueEnum)]
pub(crate) enum TrustOutcomeArg {
Success,
Failure,
Denied,
Timeout,
}
impl From<TrustOutcomeArg> for harn_vm::TrustOutcome {
fn from(value: TrustOutcomeArg) -> Self {
match value {
TrustOutcomeArg::Success => harn_vm::TrustOutcome::Success,
TrustOutcomeArg::Failure => harn_vm::TrustOutcome::Failure,
TrustOutcomeArg::Denied => harn_vm::TrustOutcome::Denied,
TrustOutcomeArg::Timeout => harn_vm::TrustOutcome::Timeout,
}
}
}
#[derive(Debug, Args)]
pub(crate) struct TrustQueryArgs {
#[arg(long)]
pub agent: Option<String>,
#[arg(long)]
pub action: Option<String>,
#[arg(long)]
pub since: Option<String>,
#[arg(long)]
pub until: Option<String>,
#[arg(long, value_enum)]
pub tier: Option<TrustTierArg>,
#[arg(long, value_enum)]
pub outcome: Option<TrustOutcomeArg>,
#[arg(long)]
pub limit: Option<usize>,
#[arg(long)]
pub grouped_by_trace: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub summary: bool,
}
#[derive(Debug, Args)]
pub(crate) struct TrustPromoteArgs {
pub agent: String,
#[arg(long, value_enum)]
pub to: TrustTierArg,
}
#[derive(Debug, Args)]
pub(crate) struct TrustDemoteArgs {
pub agent: String,
#[arg(long, value_enum)]
pub to: TrustTierArg,
#[arg(long)]
pub reason: String,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorArgs {
#[command(subcommand)]
pub command: OrchestratorCommand,
}
#[derive(Debug, Args, Clone)]
pub(crate) struct OrchestratorLocalArgs {
#[arg(
long,
visible_alias = "manifest",
env = "HARN_ORCHESTRATOR_MANIFEST",
default_value = "harn.toml",
value_name = "PATH"
)]
pub config: PathBuf,
#[arg(
long = "state-dir",
env = "HARN_ORCHESTRATOR_STATE_DIR",
default_value = ".harn/orchestrator",
value_name = "PATH"
)]
pub state_dir: PathBuf,
}
#[derive(Debug, Subcommand)]
pub(crate) enum OrchestratorCommand {
Serve(OrchestratorServeArgs),
Deploy(OrchestratorDeployArgs),
Reload(OrchestratorReloadArgs),
Inspect(OrchestratorInspectArgs),
Fire(OrchestratorFireArgs),
Replay(OrchestratorReplayArgs),
Resume(OrchestratorResumeArgs),
Dlq(OrchestratorDlqArgs),
Queue(OrchestratorQueueArgs),
Recover(OrchestratorRecoverArgs),
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorServeArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
#[arg(
long,
visible_alias = "listen",
env = "HARN_ORCHESTRATOR_LISTEN",
default_value = "127.0.0.1:8080",
value_name = "ADDR"
)]
pub bind: SocketAddr,
#[arg(long, env = "HARN_ORCHESTRATOR_CERT", value_name = "PATH")]
pub cert: Option<PathBuf>,
#[arg(long, env = "HARN_ORCHESTRATOR_KEY", value_name = "PATH")]
pub key: Option<PathBuf>,
#[arg(long = "shutdown-timeout", default_value_t = 30, value_name = "SECS")]
pub shutdown_timeout: u64,
#[arg(long = "drain-max-items", value_name = "COUNT")]
pub drain_max_items: Option<usize>,
#[arg(long = "drain-deadline", value_name = "SECS")]
pub drain_deadline: Option<u64>,
#[arg(long)]
pub watch: bool,
#[arg(
long,
env = "HARN_ORCHESTRATOR_ROLE",
value_enum,
default_value_t = crate::commands::orchestrator::role::OrchestratorRole::SingleTenant
)]
pub role: crate::commands::orchestrator::role::OrchestratorRole,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorDeployArgs {
#[arg(long, value_enum)]
pub provider: OrchestratorDeployProvider,
#[arg(
long,
visible_alias = "config",
env = "HARN_ORCHESTRATOR_MANIFEST",
default_value = "harn.toml",
value_name = "PATH"
)]
pub manifest: PathBuf,
#[arg(long, default_value = "harn-orchestrator", value_name = "NAME")]
pub name: String,
#[arg(
long,
default_value = "ghcr.io/burin-labs/harn:latest",
value_name = "IMAGE"
)]
pub image: String,
#[arg(long = "deploy-dir", default_value = "deploy", value_name = "DIR")]
pub deploy_dir: PathBuf,
#[arg(long, default_value_t = 8080, value_name = "PORT")]
pub port: u16,
#[arg(long = "data-dir", default_value = "/data", value_name = "PATH")]
pub data_dir: String,
#[arg(long = "disk-size-gb", default_value_t = 10, value_name = "GB")]
pub disk_size_gb: u16,
#[arg(long = "shutdown-timeout", default_value_t = 30, value_name = "SECS")]
pub shutdown_timeout: u64,
#[arg(long, value_name = "REGION")]
pub region: Option<String>,
#[arg(long = "render-service", value_name = "SERVICE")]
pub render_service: Option<String>,
#[arg(long = "railway-service", value_name = "SERVICE")]
pub railway_service: Option<String>,
#[arg(long = "railway-environment", value_name = "ENV")]
pub railway_environment: Option<String>,
#[arg(long)]
pub build: bool,
#[arg(long = "no-push")]
pub no_push: bool,
#[arg(long = "env", value_name = "KEY=VALUE")]
pub env: Vec<String>,
#[arg(long = "secret", value_name = "KEY=VALUE")]
pub secret: Vec<String>,
#[arg(long = "no-secret-sync")]
pub no_secret_sync: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub print: bool,
#[arg(long = "health-url", value_name = "URL")]
pub health_url: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
pub(crate) enum OrchestratorDeployProvider {
Render,
Fly,
Railway,
}
impl OrchestratorDeployProvider {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Render => "render",
Self::Fly => "fly",
Self::Railway => "railway",
}
}
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorReloadArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
#[arg(long = "admin-url", value_name = "URL")]
pub admin_url: Option<String>,
#[arg(long, default_value_t = 10, value_name = "SECS")]
pub timeout: u64,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorInspectArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorFireArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
pub binding_id: String,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorReplayArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
pub event_id: String,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorResumeArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
pub event_id: String,
#[arg(long, default_value = "manual")]
pub reviewer: String,
#[arg(long)]
pub reason: Option<String>,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorDlqArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
#[arg(long)]
pub json: bool,
#[arg(
long,
default_value_t = false,
action = ArgAction::SetTrue,
conflicts_with_all = ["replay", "discard"]
)]
pub list: bool,
#[arg(long, value_name = "ID", conflicts_with = "discard")]
pub replay: Option<String>,
#[arg(long, value_name = "ID", conflicts_with = "replay")]
pub discard: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorQueueArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
#[command(subcommand)]
pub command: Option<OrchestratorQueueCommand>,
}
#[derive(Debug, Subcommand)]
pub(crate) enum OrchestratorQueueCommand {
Ls(OrchestratorQueueLsArgs),
Drain(OrchestratorQueueDrainArgs),
Purge(OrchestratorQueuePurgeArgs),
}
#[derive(Debug, Args, Default)]
pub(crate) struct OrchestratorQueueLsArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorQueueDrainArgs {
pub queue: String,
#[arg(long, value_name = "ID")]
pub consumer_id: Option<String>,
#[arg(long = "claim-ttl", value_name = "DURATION", value_parser = parse_duration_arg, default_value = "5m")]
pub claim_ttl: StdDuration,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorQueuePurgeArgs {
pub queue: String,
#[arg(long, default_value_t = false, action = ArgAction::SetTrue)]
pub confirm: bool,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub(crate) struct OrchestratorRecoverArgs {
#[command(flatten)]
pub local: OrchestratorLocalArgs,
#[arg(long = "envelope-age", value_name = "DURATION", value_parser = parse_duration_arg)]
pub envelope_age: StdDuration,
#[arg(long, default_value_t = false, action = ArgAction::SetTrue)]
pub dry_run: bool,
#[arg(long, default_value_t = false, action = ArgAction::SetTrue)]
pub yes: bool,
}
#[derive(Debug, Args)]
pub(crate) struct PlaygroundArgs {
#[arg(long, default_value = "host.harn")]
pub host: String,
#[arg(long, default_value = "pipeline.harn")]
pub script: String,
#[arg(long)]
pub task: Option<String>,
#[arg(long)]
pub llm: Option<String>,
#[arg(
long = "llm-mock",
value_name = "PATH",
conflicts_with = "llm_mock_record"
)]
pub llm_mock: Option<String>,
#[arg(
long = "llm-mock-record",
value_name = "PATH",
conflicts_with = "llm_mock"
)]
pub llm_mock_record: Option<String>,
#[arg(long)]
pub watch: bool,
}
fn parse_duration_arg(raw: &str) -> Result<StdDuration, String> {
let raw = raw.trim();
if raw.is_empty() {
return Err("duration cannot be empty".to_string());
}
let (digits, unit) = raw
.chars()
.position(|ch| !ch.is_ascii_digit())
.map(|index| raw.split_at(index))
.ok_or_else(|| {
"duration must include a unit suffix like ms, s, m, h, d, or w".to_string()
})?;
if digits.is_empty() || unit.is_empty() {
return Err("duration must be formatted like 30s, 5m, 2h, or 7d".to_string());
}
let value = digits
.parse::<u64>()
.map_err(|error| format!("invalid duration '{raw}': {error}"))?;
match unit {
"ms" => Ok(StdDuration::from_millis(value)),
"s" => Ok(StdDuration::from_secs(value)),
"m" => Ok(StdDuration::from_secs(value.saturating_mul(60))),
"h" => Ok(StdDuration::from_secs(value.saturating_mul(60 * 60))),
"d" => Ok(StdDuration::from_secs(value.saturating_mul(60 * 60 * 24))),
"w" => Ok(StdDuration::from_secs(
value.saturating_mul(60 * 60 * 24 * 7),
)),
_ => Err(format!(
"unsupported duration unit '{unit}'; expected ms, s, m, h, d, or w"
)),
}
}
#[derive(Debug, Args)]
pub(crate) struct RunsArgs {
#[command(subcommand)]
pub command: RunsCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum RunsCommand {
Inspect(RunsInspectArgs),
}
#[derive(Debug, Args)]
pub(crate) struct RunsInspectArgs {
pub path: String,
#[arg(long)]
pub compare: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct ReplayArgs {
pub path: String,
}
#[derive(Debug, Args)]
pub(crate) struct EvalArgs {
pub path: String,
#[arg(long)]
pub compare: Option<String>,
#[arg(long = "structural-experiment")]
pub structural_experiment: Option<String>,
#[arg(
long = "llm-mock",
value_name = "PATH",
conflicts_with = "llm_mock_record"
)]
pub llm_mock: Option<String>,
#[arg(
long = "llm-mock-record",
value_name = "PATH",
conflicts_with = "llm_mock"
)]
pub llm_mock_record: Option<String>,
#[arg(last = true)]
pub argv: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct DumpHighlightKeywordsArgs {
#[arg(long, default_value = "docs/theme/harn-keywords.js")]
pub output: String,
#[arg(long)]
pub check: bool,
}
#[derive(Debug, Args)]
pub(crate) struct InstallArgs {
#[arg(long, default_value_t = false, action = ArgAction::SetTrue)]
pub frozen: bool,
#[arg(long, value_name = "ALIAS|all")]
pub refetch: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct AddArgs {
pub name_or_spec: String,
#[arg(long)]
pub alias: Option<String>,
#[arg(long, conflicts_with = "path")]
pub git: Option<String>,
#[arg(long, conflicts_with_all = ["rev", "branch"])]
pub tag: Option<String>,
#[arg(long, conflicts_with = "branch")]
pub rev: Option<String>,
#[arg(long, conflicts_with_all = ["rev", "tag"])]
pub branch: Option<String>,
#[arg(long, conflicts_with = "git")]
pub path: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct UpdateArgs {
#[arg(long, default_value_t = false, action = ArgAction::SetTrue)]
pub all: bool,
pub alias: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct RemoveArgs {
pub alias: String,
}
#[derive(Debug, Args)]
pub(crate) struct ModelInfoArgs {
pub model: String,
}
#[derive(Debug, Args)]
pub(crate) struct SkillsArgs {
#[command(subcommand)]
pub command: SkillsCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum SkillsCommand {
List(SkillsListArgs),
Inspect(SkillsInspectArgs),
#[command(name = "match")]
Match(SkillsMatchArgs),
Install(SkillsInstallArgs),
New(SkillsNewArgs),
}
#[derive(Debug, Args)]
pub(crate) struct SkillsListArgs {
#[arg(long)]
pub json: bool,
#[arg(long = "from", value_name = "PATH")]
pub from: Option<String>,
#[arg(long = "skill-dir", value_name = "PATH")]
pub skill_dir: Vec<String>,
#[arg(long)]
pub all: bool,
}
#[derive(Debug, Args)]
pub(crate) struct SkillsInspectArgs {
pub name: String,
#[arg(long)]
pub json: bool,
#[arg(long = "from", value_name = "PATH")]
pub from: Option<String>,
#[arg(long = "skill-dir", value_name = "PATH")]
pub skill_dir: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct SkillsMatchArgs {
pub query: String,
#[arg(long, default_value_t = 5)]
pub top_n: usize,
#[arg(long)]
pub json: bool,
#[arg(long = "working-file", value_name = "PATH")]
pub working_files: Vec<String>,
#[arg(long = "from", value_name = "PATH")]
pub from: Option<String>,
#[arg(long = "skill-dir", value_name = "PATH")]
pub skill_dir: Vec<String>,
}
#[derive(Debug, Args)]
pub(crate) struct SkillsInstallArgs {
pub spec: String,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub tag: Option<String>,
#[arg(long)]
pub namespace: Option<String>,
}
#[derive(Debug, Args)]
pub(crate) struct SkillsNewArgs {
pub name: String,
#[arg(long)]
pub description: Option<String>,
#[arg(long = "dir", value_name = "PATH")]
pub dir: Option<String>,
#[arg(long)]
pub force: bool,
}
#[derive(Debug, Args)]
pub(crate) struct SkillArgs {
#[command(subcommand)]
pub command: SkillCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum SkillCommand {
Key(SkillKeyArgs),
Sign(SkillSignArgs),
Verify(SkillVerifyArgs),
Trust(SkillTrustArgs),
}
#[derive(Debug, Args)]
pub(crate) struct SkillKeyArgs {
#[command(subcommand)]
pub command: SkillKeyCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum SkillKeyCommand {
Generate(SkillKeyGenerateArgs),
}
#[derive(Debug, Args)]
pub(crate) struct SkillKeyGenerateArgs {
#[arg(long, value_name = "PATH")]
pub out: String,
}
#[derive(Debug, Args)]
pub(crate) struct SkillSignArgs {
pub skill: String,
#[arg(long, value_name = "PATH")]
pub key: String,
}
#[derive(Debug, Args)]
pub(crate) struct SkillVerifyArgs {
pub skill: String,
}
#[derive(Debug, Args)]
pub(crate) struct SkillTrustArgs {
#[command(subcommand)]
pub command: SkillTrustCommand,
}
#[derive(Debug, Subcommand)]
pub(crate) enum SkillTrustCommand {
Add(SkillTrustAddArgs),
List(SkillTrustListArgs),
}
#[derive(Debug, Args)]
pub(crate) struct SkillTrustAddArgs {
#[arg(long = "from", value_name = "URL|FILE")]
pub from: String,
}
#[derive(Debug, Args, Default)]
pub(crate) struct SkillTrustListArgs {}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use std::time::Duration as StdDuration;
use super::{
Cli, Command, McpCommand, OrchestratorCommand, OrchestratorDeployProvider,
OrchestratorQueueCommand, ProjectTemplate, RunsCommand, SkillCommand, SkillKeyCommand,
SkillTrustCommand, SkillsCommand, TriggerCommand, TrustCommand, TrustOutcomeArg,
TrustTierArg,
};
use clap::Parser;
#[test]
fn test_parses_conformance_target_selection() {
let cli = Cli::parse_from([
"harn",
"test",
"conformance",
"tests/worktree_runtime.harn",
"--verbose",
]);
let Command::Test(args) = cli.command.unwrap() else {
panic!("expected test command");
};
assert_eq!(args.target.as_deref(), Some("conformance"));
assert_eq!(
args.selection.as_deref(),
Some("tests/worktree_runtime.harn")
);
assert!(args.verbose);
}
#[test]
fn test_run_rejects_deny_allow_conflict() {
let err = Cli::try_parse_from([
"harn",
"run",
"--deny",
"read_file",
"--allow",
"exec",
"main.harn",
])
.unwrap_err();
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn test_parses_run_llm_mock_flags() {
let cli = Cli::parse_from(["harn", "run", "--llm-mock", "fixtures.jsonl", "main.harn"]);
let Command::Run(args) = cli.command.unwrap() else {
panic!("expected run command");
};
assert_eq!(args.llm_mock.as_deref(), Some("fixtures.jsonl"));
assert_eq!(args.llm_mock_record, None);
let cli = Cli::parse_from(["harn", "run", "--llm-mock-record", "out.jsonl", "main.harn"]);
let Command::Run(args) = cli.command.unwrap() else {
panic!("expected run command");
};
assert_eq!(args.llm_mock_record.as_deref(), Some("out.jsonl"));
assert_eq!(args.llm_mock, None);
}
#[test]
fn test_parses_mcp_login_flags() {
let cli = Cli::parse_from([
"harn",
"mcp",
"login",
"notion",
"--url",
"https://example.com/mcp",
"--client-id",
"abc",
]);
let Command::Mcp(args) = cli.command.unwrap() else {
panic!("expected mcp command");
};
let McpCommand::Login(login) = args.command else {
panic!("expected mcp login");
};
assert_eq!(login.target.as_deref(), Some("notion"));
assert_eq!(login.url.as_deref(), Some("https://example.com/mcp"));
assert_eq!(login.client_id.as_deref(), Some("abc"));
}
#[test]
fn test_parses_mcp_serve_flags() {
let cli = Cli::parse_from([
"harn",
"mcp",
"serve",
"--config",
"workspace/harn.toml",
"--state-dir",
"state/orchestrator",
"--transport",
"http",
"--bind",
"127.0.0.1:9000",
"--path",
"/rpc",
"--sse-path",
"/events",
"--messages-path",
"/legacy/messages",
]);
let Command::Mcp(args) = cli.command.unwrap() else {
panic!("expected mcp command");
};
let McpCommand::Serve(serve) = args.command else {
panic!("expected mcp serve");
};
assert_eq!(serve.local.config, PathBuf::from("workspace/harn.toml"));
assert_eq!(serve.local.state_dir, PathBuf::from("state/orchestrator"));
assert_eq!(serve.transport, crate::cli::McpServeTransport::Http);
assert_eq!(serve.bind.to_string(), "127.0.0.1:9000");
assert_eq!(serve.path, "/rpc");
assert_eq!(serve.sse_path, "/events");
assert_eq!(serve.messages_path, "/legacy/messages");
}
#[test]
fn test_parses_serve_mcp_flags() {
let cli = Cli::parse_from([
"harn",
"serve",
"mcp",
"--transport",
"http",
"--bind",
"127.0.0.1:9001",
"--path",
"/rpc",
"--sse-path",
"/events",
"--messages-path",
"/legacy/messages",
"--api-key",
"alpha,beta",
"--hmac-secret",
"shared",
"server.harn",
]);
let Command::Serve(args) = cli.command.unwrap() else {
panic!("expected serve command");
};
let crate::cli::ServeCommand::Mcp(serve) = args.command else {
panic!("expected serve mcp");
};
assert_eq!(serve.transport, crate::cli::McpServeTransport::Http);
assert_eq!(serve.bind.to_string(), "127.0.0.1:9001");
assert_eq!(serve.path, "/rpc");
assert_eq!(serve.sse_path, "/events");
assert_eq!(serve.messages_path, "/legacy/messages");
assert_eq!(serve.api_key, vec!["alpha".to_string(), "beta".to_string()]);
assert_eq!(serve.hmac_secret.as_deref(), Some("shared"));
assert_eq!(serve.file, "server.harn");
}
#[test]
fn test_parses_runs_inspect_compare() {
let cli = Cli::parse_from([
"harn",
"runs",
"inspect",
"run.json",
"--compare",
"baseline.json",
]);
let Command::Runs(args) = cli.command.unwrap() else {
panic!("expected runs command");
};
let RunsCommand::Inspect(inspect) = args.command;
assert_eq!(inspect.path, "run.json");
assert_eq!(inspect.compare.as_deref(), Some("baseline.json"));
}
#[test]
fn test_parses_trigger_replay_flags() {
let cli = Cli::parse_from([
"harn",
"trigger",
"replay",
"trigger_evt_123",
"--diff",
"--as-of",
"2026-04-19T18:00:00Z",
]);
let Command::Trigger(args) = cli.command.unwrap() else {
panic!("expected trigger command");
};
let TriggerCommand::Replay(replay) = args.command else {
panic!("expected trigger replay");
};
assert_eq!(replay.event_id.as_deref(), Some("trigger_evt_123"));
assert!(replay.diff);
assert_eq!(replay.as_of.as_deref(), Some("2026-04-19T18:00:00Z"));
assert!(replay.where_expr.is_none());
}
#[test]
fn test_parses_trigger_bulk_cancel_flags() {
let cli = Cli::parse_from([
"harn",
"trigger",
"cancel",
"--where",
"event.payload.tenant == 'acme' AND attempt.handler == 'handlers::risky'",
"--dry-run",
"--progress",
"--rate-limit",
"4",
]);
let Command::Trigger(args) = cli.command.unwrap() else {
panic!("expected trigger command");
};
let TriggerCommand::Cancel(cancel) = args.command else {
panic!("expected trigger cancel");
};
assert!(cancel.event_id.is_none());
assert_eq!(
cancel.where_expr.as_deref(),
Some("event.payload.tenant == 'acme' AND attempt.handler == 'handlers::risky'")
);
assert!(cancel.dry_run);
assert!(cancel.progress);
assert_eq!(cancel.rate_limit, Some(4.0));
}
#[test]
fn test_parses_trust_query_flags() {
let cli = Cli::parse_from([
"harn",
"trust",
"query",
"--agent",
"github-triage-bot",
"--action",
"github.issue.opened",
"--since",
"2026-04-19T18:00:00Z",
"--until",
"2026-04-19T19:00:00Z",
"--tier",
"act-auto",
"--outcome",
"success",
"--limit",
"500",
"--grouped-by-trace",
"--json",
"--summary",
]);
let Command::Trust(args) = cli.command.unwrap() else {
panic!("expected trust command");
};
let TrustCommand::Query(query) = args.command else {
panic!("expected trust query");
};
assert_eq!(query.agent.as_deref(), Some("github-triage-bot"));
assert_eq!(query.action.as_deref(), Some("github.issue.opened"));
assert_eq!(query.since.as_deref(), Some("2026-04-19T18:00:00Z"));
assert_eq!(query.until.as_deref(), Some("2026-04-19T19:00:00Z"));
assert!(matches!(query.tier, Some(TrustTierArg::ActAuto)));
assert!(matches!(query.outcome, Some(TrustOutcomeArg::Success)));
assert_eq!(query.limit, Some(500));
assert!(query.grouped_by_trace);
assert!(query.json);
assert!(query.summary);
}
#[test]
fn test_parses_trust_demote_flags() {
let cli = Cli::parse_from([
"harn",
"trust",
"demote",
"github-triage-bot",
"--to",
"shadow",
"--reason",
"unexpected mutation",
]);
let Command::Trust(args) = cli.command.unwrap() else {
panic!("expected trust command");
};
let TrustCommand::Demote(demote) = args.command else {
panic!("expected trust demote");
};
assert_eq!(demote.agent, "github-triage-bot");
assert!(matches!(demote.to, TrustTierArg::Shadow));
assert_eq!(demote.reason, "unexpected mutation");
}
#[test]
fn test_parses_portal_flags() {
let cli = Cli::parse_from([
"harn", "portal", "--dir", "runs", "--host", "0.0.0.0", "--port", "4900", "--open",
"false",
]);
let Command::Portal(args) = cli.command.unwrap() else {
panic!("expected portal command");
};
assert_eq!(args.dir, "runs");
assert_eq!(args.host, "0.0.0.0");
assert_eq!(args.port, 4900);
assert!(!args.open);
}
#[test]
fn test_parses_orchestrator_serve_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"serve",
"--config",
"workspace/harn.toml",
"--state-dir",
"state/orchestrator",
"--bind",
"0.0.0.0:8080",
"--cert",
"tls/cert.pem",
"--key",
"tls/key.pem",
"--shutdown-timeout",
"45",
"--drain-max-items",
"256",
"--drain-deadline",
"9",
"--role",
"single-tenant",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Serve(serve) = args.command else {
panic!("expected orchestrator serve");
};
assert_eq!(serve.local.config, PathBuf::from("workspace/harn.toml"));
assert_eq!(serve.local.state_dir, PathBuf::from("state/orchestrator"));
assert_eq!(serve.bind.to_string(), "0.0.0.0:8080");
assert_eq!(serve.cert, Some(PathBuf::from("tls/cert.pem")));
assert_eq!(serve.key, Some(PathBuf::from("tls/key.pem")));
assert_eq!(serve.shutdown_timeout, 45);
assert_eq!(serve.drain_max_items, Some(256));
assert_eq!(serve.drain_deadline, Some(9));
assert_eq!(
serve.role,
crate::commands::orchestrator::role::OrchestratorRole::SingleTenant
);
}
#[test]
fn test_parses_orchestrator_serve_container_aliases() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"serve",
"--manifest",
"/etc/harn/triggers.toml",
"--state-dir",
"/var/lib/harn/state",
"--listen",
"0.0.0.0:8080",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Serve(serve) = args.command else {
panic!("expected orchestrator serve");
};
assert_eq!(serve.local.config, PathBuf::from("/etc/harn/triggers.toml"));
assert_eq!(serve.local.state_dir, PathBuf::from("/var/lib/harn/state"));
assert_eq!(serve.bind.to_string(), "0.0.0.0:8080");
}
#[test]
fn test_parses_orchestrator_deploy_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"deploy",
"--provider",
"fly",
"--manifest",
"workspace/harn.toml",
"--name",
"harn-prod",
"--image",
"ghcr.io/acme/harn-prod:latest",
"--deploy-dir",
"ops/deploy",
"--port",
"8443",
"--data-dir",
"/data",
"--disk-size-gb",
"20",
"--shutdown-timeout",
"60",
"--region",
"sjc",
"--build",
"--env",
"RUST_LOG=debug",
"--secret",
"OPENAI_API_KEY=sk-test",
"--dry-run",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Deploy(deploy) = args.command else {
panic!("expected orchestrator deploy");
};
assert_eq!(deploy.provider, OrchestratorDeployProvider::Fly);
assert_eq!(deploy.manifest, PathBuf::from("workspace/harn.toml"));
assert_eq!(deploy.name, "harn-prod");
assert_eq!(deploy.image, "ghcr.io/acme/harn-prod:latest");
assert_eq!(deploy.deploy_dir, PathBuf::from("ops/deploy"));
assert_eq!(deploy.port, 8443);
assert_eq!(deploy.data_dir, "/data");
assert_eq!(deploy.disk_size_gb, 20);
assert_eq!(deploy.shutdown_timeout, 60);
assert_eq!(deploy.region.as_deref(), Some("sjc"));
assert!(deploy.build);
assert_eq!(deploy.env, vec!["RUST_LOG=debug".to_string()]);
assert_eq!(deploy.secret, vec!["OPENAI_API_KEY=sk-test".to_string()]);
assert!(deploy.dry_run);
}
#[test]
fn test_parses_orchestrator_inspect_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"inspect",
"--config",
"workspace/harn.toml",
"--state-dir",
"state/orchestrator",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Inspect(inspect) = args.command else {
panic!("expected orchestrator inspect");
};
assert_eq!(inspect.local.config, PathBuf::from("workspace/harn.toml"));
assert_eq!(inspect.local.state_dir, PathBuf::from("state/orchestrator"));
assert!(!inspect.json);
}
#[test]
fn test_parses_orchestrator_fire_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"fire",
"github-new-issue",
"--config",
"workspace/harn.toml",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Fire(fire) = args.command else {
panic!("expected orchestrator fire");
};
assert_eq!(fire.binding_id, "github-new-issue");
assert_eq!(fire.local.config, PathBuf::from("workspace/harn.toml"));
}
#[test]
fn test_parses_orchestrator_replay_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"replay",
"trigger_evt_123",
"--state-dir",
"state/orchestrator",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Replay(replay) = args.command else {
panic!("expected orchestrator replay");
};
assert_eq!(replay.event_id, "trigger_evt_123");
assert_eq!(replay.local.state_dir, PathBuf::from("state/orchestrator"));
assert!(!replay.json);
}
#[test]
fn test_parses_orchestrator_dlq_replay_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"dlq",
"--replay",
"dlq_123",
"--config",
"workspace/harn.toml",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Dlq(dlq) = args.command else {
panic!("expected orchestrator dlq");
};
assert_eq!(dlq.replay.as_deref(), Some("dlq_123"));
assert!(dlq.discard.is_none());
assert!(!dlq.list);
assert_eq!(dlq.local.config, PathBuf::from("workspace/harn.toml"));
assert!(!dlq.json);
}
#[test]
fn test_parses_orchestrator_json_flags() {
let inspect_cli = Cli::parse_from(["harn", "orchestrator", "inspect", "--json"]);
let Command::Orchestrator(inspect_args) = inspect_cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Inspect(inspect) = inspect_args.command else {
panic!("expected orchestrator inspect");
};
assert!(inspect.json);
let replay_cli = Cli::parse_from([
"harn",
"orchestrator",
"replay",
"trigger_evt_123",
"--json",
]);
let Command::Orchestrator(replay_args) = replay_cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Replay(replay) = replay_args.command else {
panic!("expected orchestrator replay");
};
assert!(replay.json);
let dlq_cli = Cli::parse_from(["harn", "orchestrator", "dlq", "--json", "--list"]);
let Command::Orchestrator(dlq_args) = dlq_cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Dlq(dlq) = dlq_args.command else {
panic!("expected orchestrator dlq");
};
assert!(dlq.json);
assert!(dlq.list);
}
#[test]
fn test_parses_orchestrator_resume_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"resume",
"hitl_escalation_trigger_evt_123_1",
"--reviewer",
"ops-lead",
"--reason",
"manual escalation ack",
"--json",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Resume(resume) = args.command else {
panic!("expected orchestrator resume");
};
assert_eq!(resume.event_id, "hitl_escalation_trigger_evt_123_1");
assert_eq!(resume.reviewer, "ops-lead");
assert_eq!(resume.reason.as_deref(), Some("manual escalation ack"));
assert!(resume.json);
}
#[test]
fn test_parses_orchestrator_queue_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"queue",
"--state-dir",
"state/orchestrator",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Queue(queue) = args.command else {
panic!("expected orchestrator queue");
};
assert_eq!(queue.local.state_dir, PathBuf::from("state/orchestrator"));
assert!(queue.command.is_none());
}
#[test]
fn test_parses_orchestrator_queue_drain_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"queue",
"--state-dir",
"state/orchestrator",
"drain",
"triage",
"--consumer-id",
"worker-a",
"--claim-ttl",
"30s",
"--json",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Queue(queue) = args.command else {
panic!("expected orchestrator queue");
};
let Some(OrchestratorQueueCommand::Drain(drain)) = queue.command else {
panic!("expected orchestrator queue drain");
};
assert_eq!(queue.local.state_dir, PathBuf::from("state/orchestrator"));
assert_eq!(drain.queue, "triage");
assert_eq!(drain.consumer_id.as_deref(), Some("worker-a"));
assert_eq!(drain.claim_ttl, StdDuration::from_secs(30));
assert!(drain.json);
}
#[test]
fn test_parses_orchestrator_queue_purge_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"queue",
"--state-dir",
"state/orchestrator",
"purge",
"triage",
"--confirm",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Queue(queue) = args.command else {
panic!("expected orchestrator queue");
};
let Some(OrchestratorQueueCommand::Purge(purge)) = queue.command else {
panic!("expected orchestrator queue purge");
};
assert_eq!(queue.local.state_dir, PathBuf::from("state/orchestrator"));
assert_eq!(purge.queue, "triage");
assert!(purge.confirm);
}
#[test]
fn test_parses_orchestrator_recover_args() {
let cli = Cli::parse_from([
"harn",
"orchestrator",
"recover",
"--config",
"workspace/harn.toml",
"--state-dir",
"state/orchestrator",
"--envelope-age",
"15m",
"--dry-run",
]);
let Command::Orchestrator(args) = cli.command.unwrap() else {
panic!("expected orchestrator command");
};
let OrchestratorCommand::Recover(recover) = args.command else {
panic!("expected orchestrator recover");
};
assert_eq!(recover.local.config, PathBuf::from("workspace/harn.toml"));
assert_eq!(recover.local.state_dir, PathBuf::from("state/orchestrator"));
assert_eq!(recover.envelope_age, StdDuration::from_secs(15 * 60));
assert!(recover.dry_run);
assert!(!recover.yes);
}
#[test]
fn test_parses_new_template() {
let cli = Cli::parse_from(["harn", "new", "review-bot", "--template", "agent"]);
let Command::New(args) = cli.command.unwrap() else {
panic!("expected new command");
};
assert_eq!(args.name.as_deref(), Some("review-bot"));
assert_eq!(args.template, ProjectTemplate::Agent);
}
#[test]
fn test_parses_pipeline_lab_template() {
let cli = Cli::parse_from([
"harn",
"new",
"pipeline-lab-demo",
"--template",
"pipeline-lab",
]);
let Command::New(args) = cli.command.unwrap() else {
panic!("expected new command");
};
assert_eq!(args.template, ProjectTemplate::PipelineLab);
}
#[test]
fn test_parses_playground_args() {
let cli = Cli::parse_from([
"harn",
"playground",
"--host",
"examples/playground/host.harn",
"--script",
"examples/playground/echo.harn",
"--task",
"hi",
"--llm",
"ollama:qwen2.5-coder:latest",
"--watch",
]);
let Command::Playground(args) = cli.command.unwrap() else {
panic!("expected playground command");
};
assert_eq!(args.host, "examples/playground/host.harn");
assert_eq!(args.script, "examples/playground/echo.harn");
assert_eq!(args.task.as_deref(), Some("hi"));
assert_eq!(args.llm.as_deref(), Some("ollama:qwen2.5-coder:latest"));
assert_eq!(args.llm_mock, None);
assert_eq!(args.llm_mock_record, None);
assert!(args.watch);
}
#[test]
fn test_parses_playground_llm_mock_flags() {
let cli = Cli::parse_from([
"harn",
"playground",
"--llm-mock",
"fixtures.jsonl",
"--host",
"host.harn",
]);
let Command::Playground(args) = cli.command.unwrap() else {
panic!("expected playground command");
};
assert_eq!(args.llm_mock.as_deref(), Some("fixtures.jsonl"));
assert_eq!(args.llm_mock_record, None);
let cli = Cli::parse_from(["harn", "playground", "--llm-mock-record", "recorded.jsonl"]);
let Command::Playground(args) = cli.command.unwrap() else {
panic!("expected playground command");
};
assert_eq!(args.llm_mock, None);
assert_eq!(args.llm_mock_record.as_deref(), Some("recorded.jsonl"));
}
#[test]
fn test_parses_doctor_flags() {
let cli = Cli::parse_from(["harn", "doctor", "--no-network"]);
let Command::Doctor(args) = cli.command.unwrap() else {
panic!("expected doctor command");
};
assert!(args.no_network);
}
#[test]
fn test_parses_viz_args() {
let cli = Cli::parse_from(["harn", "viz", "main.harn", "--output", "graph.mmd"]);
let Command::Viz(args) = cli.command.unwrap() else {
panic!("expected viz command");
};
assert_eq!(args.file, "main.harn");
assert_eq!(args.output.as_deref(), Some("graph.mmd"));
}
#[test]
fn test_parses_bench_args() {
let cli = Cli::parse_from(["harn", "bench", "main.harn", "--iterations", "25"]);
let Command::Bench(args) = cli.command.unwrap() else {
panic!("expected bench command");
};
assert_eq!(args.file, "main.harn");
assert_eq!(args.iterations, 25);
}
#[test]
fn test_parses_skills_subcommands() {
let cli = Cli::parse_from(["harn", "skills", "list", "--json", "--all"]);
let Command::Skills(args) = cli.command.unwrap() else {
panic!("expected skills command");
};
let SkillsCommand::List(list) = args.command else {
panic!("expected skills list");
};
assert!(list.json);
assert!(list.all);
let cli = Cli::parse_from(["harn", "skills", "match", "deploy the app", "--top-n", "3"]);
let Command::Skills(args) = cli.command.unwrap() else {
panic!("expected skills command");
};
let SkillsCommand::Match(matcher) = args.command else {
panic!("expected skills match");
};
assert_eq!(matcher.query, "deploy the app");
assert_eq!(matcher.top_n, 3);
let cli = Cli::parse_from([
"harn",
"skills",
"install",
"https://example.com/acme/harn-skills.git",
"--tag",
"v1.0",
"--namespace",
"acme",
]);
let Command::Skills(args) = cli.command.unwrap() else {
panic!("expected skills command");
};
let SkillsCommand::Install(install) = args.command else {
panic!("expected skills install");
};
assert_eq!(install.tag.as_deref(), Some("v1.0"));
assert_eq!(install.namespace.as_deref(), Some("acme"));
let cli = Cli::parse_from([
"harn",
"skills",
"new",
"deploy",
"--description",
"Ship things",
]);
let Command::Skills(args) = cli.command.unwrap() else {
panic!("expected skills command");
};
let SkillsCommand::New(new_args) = args.command else {
panic!("expected skills new");
};
assert_eq!(new_args.name, "deploy");
assert_eq!(new_args.description.as_deref(), Some("Ship things"));
}
#[test]
fn test_parses_skill_provenance_subcommands() {
let cli = Cli::parse_from(["harn", "skill", "key", "generate", "--out", "signer.pem"]);
let Command::Skill(args) = cli.command.unwrap() else {
panic!("expected skill command");
};
let SkillCommand::Key(key_args) = args.command else {
panic!("expected skill key");
};
let SkillKeyCommand::Generate(generate) = key_args.command;
assert_eq!(generate.out, "signer.pem");
let cli = Cli::parse_from(["harn", "skill", "sign", "SKILL.md", "--key", "signer.pem"]);
let Command::Skill(args) = cli.command.unwrap() else {
panic!("expected skill command");
};
let SkillCommand::Sign(sign) = args.command else {
panic!("expected skill sign");
};
assert_eq!(sign.skill, "SKILL.md");
assert_eq!(sign.key, "signer.pem");
let cli = Cli::parse_from(["harn", "skill", "verify", "SKILL.md"]);
let Command::Skill(args) = cli.command.unwrap() else {
panic!("expected skill command");
};
let SkillCommand::Verify(verify) = args.command else {
panic!("expected skill verify");
};
assert_eq!(verify.skill, "SKILL.md");
let cli = Cli::parse_from([
"harn",
"skill",
"trust",
"add",
"--from",
"https://example.com/signer.pub",
]);
let Command::Skill(args) = cli.command.unwrap() else {
panic!("expected skill command");
};
let SkillCommand::Trust(trust) = args.command else {
panic!("expected skill trust");
};
let SkillTrustCommand::Add(add) = trust.command else {
panic!("expected skill trust add");
};
assert_eq!(add.from, "https://example.com/signer.pub");
let cli = Cli::parse_from(["harn", "skill", "trust", "list"]);
let Command::Skill(args) = cli.command.unwrap() else {
panic!("expected skill command");
};
let SkillCommand::Trust(trust) = args.command else {
panic!("expected skill trust");
};
assert!(matches!(trust.command, SkillTrustCommand::List(_)));
}
#[test]
fn test_parses_model_info_args() {
let cli = Cli::parse_from(["harn", "model-info", "tog-gemma4-31b"]);
let Command::ModelInfo(args) = cli.command.unwrap() else {
panic!("expected model-info command");
};
assert_eq!(args.model, "tog-gemma4-31b");
}
}