use std::{collections::BTreeSet, path::PathBuf};
use crate::{config, message};
use anyhow::{Result, bail};
use brgr_protocol::{AttemptId, EvidenceSpec, TaskId, TaskSpec};
use clap::{Args, Parser, Subcommand};
use config::WorkerPlacement;
#[derive(Parser)]
#[command(name = "brgr", version, about = "Durable local agent task bridge")]
pub(crate) struct Cli {
#[arg(long, global = true, env = "BRGR_HOME")]
pub(crate) home: Option<PathBuf>,
#[arg(long, global = true)]
pub(crate) json: bool,
#[arg(long, global = true, alias = "as")]
pub(crate) owner_session: Option<String>,
#[arg(long, global = true)]
pub(crate) source_pane: Option<String>,
#[arg(long, global = true)]
pub(crate) headless: bool,
#[command(subcommand)]
pub(crate) command: Command,
}
#[derive(Subcommand)]
pub(crate) enum Command {
Run(RunArgs),
Revise(ReviseArgs),
Status {
task: Option<TaskId>,
#[arg(long)]
tree: bool,
},
Result {
task: TaskId,
#[arg(long)]
ack: bool,
},
Artifact {
#[command(subcommand)]
command: ArtifactCommand,
},
Wait {
task: TaskId,
#[arg(long, default_value_t = 3_600)]
timeout_seconds: u64,
},
Message {
#[command(subcommand)]
command: message::MessageCommand,
},
Debate {
#[command(subcommand)]
command: crate::debate::DebateCommand,
},
Input {
task: TaskId,
#[arg(long, conflicts_with = "key", required_unless_present = "key")]
text: Option<String>,
#[arg(long, conflicts_with = "text", required_unless_present = "text")]
key: Option<String>,
},
Cancel {
task: TaskId,
#[arg(long)]
tree: bool,
},
Bind {
task: TaskId,
#[arg(long)]
session: Option<String>,
},
Accept {
task: TaskId,
#[arg(long, default_value = "acceptance criteria verified")]
reason: String,
},
Reject {
task: TaskId,
#[arg(long)]
reason: String,
},
Diff {
task: TaskId,
#[arg(long)]
stat: bool,
},
Apply {
task: TaskId,
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
execute: bool,
#[arg(long = "3way")]
three_way: bool,
},
Harness {
#[command(subcommand)]
command: HarnessCommand,
},
Integrate {
#[command(subcommand)]
command: IntegrateCommand,
},
Doctor,
Errors {
#[command(subcommand)]
command: Option<ErrorsCommand>,
},
Config {
#[command(subcommand)]
command: ConfigCommand,
},
Plugin {
#[command(subcommand)]
command: PluginCommand,
},
Prune {
#[arg(long)]
apply: bool,
#[arg(long)]
include_ignored: bool,
},
Cleanup {
#[command(subcommand)]
command: CleanupCommand,
},
#[command(name = "__supervise", hide = true)]
Supervise { launch: PathBuf },
#[command(name = "__hook", hide = true)]
Hook { event: HookEvent },
#[command(name = "__notify", hide = true)]
Notify { task: TaskId },
#[command(name = "__pane-run", hide = true)]
PaneRun(PaneRunArgs),
#[command(name = "__tui-host", hide = true)]
TuiHost { config: PathBuf },
#[command(name = "__session", hide = true)]
Session { task: TaskId },
#[command(name = "__seal-report", hide = true)]
SealReport { task: TaskId, revision: u32 },
Report {
task: TaskId,
#[arg(long)]
body: String,
},
#[command(name = "__native-result", hide = true)]
NativeResult {
task: TaskId,
revision: u32,
kind: String,
},
#[command(name = "__omp-run", hide = true)]
OmpRun {
#[arg(long)]
prompt_file: PathBuf,
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
task: TaskId,
#[arg(long)]
revision: u32,
#[arg(long)]
launcher: PathBuf,
#[arg(long)]
model: Option<String>,
#[arg(long)]
effort: Option<String>,
#[arg(long)]
keep_pane: bool,
},
}
#[derive(Subcommand)]
pub(crate) enum PluginCommand {
Open {
#[arg(long)]
no_focus: bool,
#[arg(long)]
codex: bool,
},
Board {
#[arg(long)]
once: bool,
},
Codex,
#[command(hide = true)]
Worker,
}
#[derive(Subcommand)]
pub(crate) enum ConfigCommand {
Init,
Show,
Set {
key: String,
value: String,
#[arg(long)]
harness: Option<String>,
},
Check,
SetCodexExecutable {
executable: PathBuf,
},
ClearCodexExecutable,
SetWorkerPlacement {
placement: WorkerPlacement,
},
SetAutoWorkerPane {
#[arg(action = clap::ArgAction::Set)]
enabled: bool,
},
SetPaneMode {
#[arg(action = clap::ArgAction::Set)]
enabled: bool,
},
SetAutoRecertify {
#[arg(action = clap::ArgAction::Set)]
enabled: bool,
},
SetIssueReporting {
repo: String,
#[arg(long, default_value_t = 2)]
min_count: u64,
},
ClearIssueReporting,
}
#[derive(Subcommand)]
pub(crate) enum ErrorsCommand {
List,
Preview,
File,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
pub(crate) enum PermissionArg {
ReadOnly,
Edits,
Full,
}
impl From<PermissionArg> for brgr_protocol::PermissionLevel {
fn from(value: PermissionArg) -> Self {
match value {
PermissionArg::ReadOnly => Self::ReadOnly,
PermissionArg::Edits => Self::Edits,
PermissionArg::Full => Self::Full,
}
}
}
#[derive(Subcommand)]
pub(crate) enum ArtifactCommand {
Export {
task: TaskId,
index: usize,
#[arg(long)]
output: PathBuf,
},
}
#[derive(Args)]
pub(crate) struct RunArgs {
pub(crate) objective: String,
#[command(flatten)]
pub(crate) delegation: DelegationArgs,
#[arg(long)]
pub(crate) harness: Option<String>,
#[arg(long, value_enum, hide = true)]
pub(crate) permission: Option<PermissionArg>,
#[arg(long)]
pub(crate) model: Option<String>,
#[arg(long)]
pub(crate) effort: Option<String>,
#[arg(long = "criterion")]
pub(crate) criteria: Vec<String>,
#[arg(long = "scope")]
pub(crate) scopes: Vec<String>,
#[arg(long = "role-instruction")]
pub(crate) role_instructions: Vec<String>,
#[command(flatten)]
pub(crate) capabilities: CapabilityArgs,
#[command(flatten)]
pub(crate) evidence: EvidenceArgs,
#[arg(long)]
pub(crate) deadline_seconds: Option<u64>,
#[arg(long)]
pub(crate) max_children: Option<u8>,
#[arg(long)]
pub(crate) workspace: Option<PathBuf>,
#[arg(long)]
pub(crate) allow_clean_head_snapshot: bool,
#[arg(long = "snapshot-path", conflicts_with = "allow_clean_head_snapshot")]
pub(crate) snapshot_paths: Vec<PathBuf>,
#[arg(long, hide = true)]
pub(crate) foreground: bool,
#[command(flatten)]
pub(crate) keep: RetentionArgs,
}
impl RunArgs {
pub(crate) fn forwards_criteria(&self) -> bool {
!self.criteria.is_empty() || !self.scopes.is_empty() || !self.role_instructions.is_empty()
}
}
#[derive(Args)]
pub(crate) struct RetentionArgs {
#[arg(long)]
pub(crate) keep_pane: bool,
#[arg(long)]
pub(crate) keep_worktree: bool,
}
#[derive(Args)]
pub(crate) struct DelegationArgs {
#[arg(long, requires = "parent_attempt")]
pub(crate) parent_task: Option<TaskId>,
#[arg(long, requires = "parent_task")]
pub(crate) parent_attempt: Option<AttemptId>,
#[arg(long)]
pub(crate) enable_delegation: bool,
}
#[derive(Args)]
pub(crate) struct ReviseArgs {
pub(crate) task: TaskId,
#[arg(long, value_enum, hide = true)]
pub(crate) permission: Option<PermissionArg>,
pub(crate) objective: String,
#[arg(long = "criterion")]
pub(crate) criteria: Vec<String>,
#[arg(long = "scope")]
pub(crate) scopes: Vec<String>,
#[arg(long = "role-instruction")]
pub(crate) role_instructions: Vec<String>,
#[command(flatten)]
pub(crate) capabilities: CapabilityArgs,
#[command(flatten)]
pub(crate) evidence: EvidenceArgs,
#[arg(long)]
pub(crate) max_children: Option<u8>,
#[arg(long)]
pub(crate) workspace: Option<PathBuf>,
#[arg(long)]
pub(crate) allow_clean_head_snapshot: bool,
#[arg(long = "snapshot-path", conflicts_with = "allow_clean_head_snapshot")]
pub(crate) snapshot_paths: Vec<PathBuf>,
#[arg(long, hide = true)]
pub(crate) foreground: bool,
#[command(flatten)]
pub(crate) keep: RetentionArgs,
}
impl ReviseArgs {
pub(crate) fn forwards_criteria(&self) -> bool {
!self.criteria.is_empty() || !self.scopes.is_empty() || !self.role_instructions.is_empty()
}
}
#[derive(Args, Default)]
pub(crate) struct CapabilityArgs {
#[arg(long)]
pub(crate) requires_write: bool,
#[arg(long)]
pub(crate) requires_browser: bool,
#[arg(long = "requires-mcp")]
pub(crate) requires_mcp: Vec<String>,
#[arg(long = "require-capability")]
pub(crate) required: Vec<String>,
}
impl CapabilityArgs {
pub(crate) fn required_names(&self) -> Result<Vec<String>> {
let mut names = BTreeSet::new();
names.insert("completion".to_owned());
if self.requires_write {
names.insert("workspace_write".to_owned());
}
if self.requires_browser {
names.insert("browser".to_owned());
}
for server in &self.requires_mcp {
names.insert(format!("mcp:{server}"));
}
names.extend(self.required.iter().cloned());
if names.iter().any(|name| {
name.is_empty()
|| name.len() > 128
|| !name.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b':' | b'.' | b'_' | b'-')
})
}) {
bail!("required capability names must be bounded ASCII identifiers");
}
Ok(names.into_iter().collect())
}
}
#[derive(Args, Default)]
pub(crate) struct EvidenceArgs {
#[arg(long)]
pub(crate) capture_diff: bool,
#[arg(long)]
pub(crate) capture_logs: bool,
#[arg(long = "evidence-file")]
pub(crate) files: Vec<PathBuf>,
}
impl EvidenceArgs {
pub(crate) fn spec(&self) -> EvidenceSpec {
EvidenceSpec {
capture_diff: self.capture_diff,
capture_logs: self.capture_logs,
base_commit: None,
base_tree: None,
files: self
.files
.iter()
.map(|path| path.to_string_lossy().into_owned())
.collect(),
}
}
pub(crate) fn extend_task(&self, task: &mut TaskSpec) {
task.evidence.capture_diff |= self.capture_diff;
task.evidence.capture_logs |= self.capture_logs;
task.evidence.files.extend(self.spec().files);
task.evidence.files.sort();
task.evidence.files.dedup();
if !task.evidence.is_empty() {
task.artifact_contract.max_bytes =
task.artifact_contract.max_bytes.max(8 * 1024 * 1024);
}
}
pub(crate) fn artifact_limit(&self, base: u64) -> u64 {
if self.spec().is_empty() {
base
} else {
base.max(8 * 1024 * 1024)
}
}
}
#[derive(Clone, Copy, Subcommand)]
pub(crate) enum CleanupCommand {
Status { task: TaskId },
Run { task: TaskId },
}
#[derive(Subcommand)]
pub(crate) enum HarnessCommand {
Add(AddHarnessArgs),
Draft { executable: PathBuf },
Test {
executable: Option<PathBuf>,
#[arg(long)]
manifest: Option<PathBuf>,
},
Activate {
executable: Option<PathBuf>,
#[arg(long)]
manifest: Option<PathBuf>,
#[arg(long)]
workspace: PathBuf,
#[arg(long)]
prompt: String,
#[arg(long)]
model: Option<String>,
#[arg(long)]
effort: Option<String>,
},
Recertify {
harness: String,
#[arg(long)]
workspace: Option<PathBuf>,
#[arg(long)]
prompt: Option<String>,
},
Status {
harness: Option<String>,
},
}
#[derive(Args)]
pub(crate) struct AddHarnessArgs {
pub(crate) executable: PathBuf,
#[arg(long)]
pub(crate) workspace: Option<PathBuf>,
#[arg(long)]
pub(crate) prompt: Option<String>,
#[arg(long)]
pub(crate) model: Option<String>,
#[arg(long)]
pub(crate) effort: Option<String>,
#[arg(long)]
pub(crate) presentation_only: bool,
}
#[derive(Subcommand)]
pub(crate) enum IntegrateCommand {
Codex {
#[command(subcommand)]
command: CodexCommand,
},
Claude {
#[command(subcommand)]
command: CodexCommand,
},
}
#[derive(Subcommand)]
pub(crate) enum CodexCommand {
Install,
Status,
Uninstall,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
pub(crate) enum HookEvent {
SessionStart,
UserPromptSubmit,
Stop,
}
#[derive(Args)]
pub(crate) struct PaneRunArgs {
#[arg(long)]
pub(crate) caller: Option<String>,
#[arg(long)]
pub(crate) native_executable: Option<PathBuf>,
#[arg(long)]
pub(crate) prompt_file: PathBuf,
#[arg(long)]
pub(crate) workspace: PathBuf,
#[arg(long)]
pub(crate) task: TaskId,
#[arg(long)]
pub(crate) revision: u32,
#[arg(long)]
pub(crate) kind: String,
#[arg(long = "agent-arg", allow_hyphen_values = true)]
pub(crate) agent_args: Vec<String>,
#[arg(long)]
pub(crate) keep_pane: bool,
}