use crate::franken_sync::Connection;
use clap::builder::StyledStr;
use clap::{Args, Parser, Subcommand, ValueEnum};
use clap_complete::engine::{ArgValueCompleter, CompletionCandidate};
use fsqlite_types::SqliteValue;
use serde::Deserialize;
use std::collections::BTreeSet;
use std::ffi::OsStr;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};
use std::sync::OnceLock;
use crate::config;
use crate::format::{format_status_label, truncate_title};
use crate::model::{IssueType, Status};
pub mod commands;
pub(crate) const DEFAULT_LIST_LIMIT: usize = 0;
pub(crate) const DEFAULT_LIST_OFFSET: usize = 0;
pub(crate) const DEFAULT_SEARCH_LIMIT: usize = 50;
#[derive(Clone, Copy)]
enum IssueCompletionFilter {
Any,
Open,
Closed,
}
impl IssueCompletionFilter {
fn matches(self, status: &Status) -> bool {
match self {
Self::Any => !matches!(status, Status::Tombstone),
Self::Open => !status.is_terminal(),
Self::Closed => matches!(status, Status::Closed),
}
}
}
#[derive(Deserialize, Debug)]
struct CompletionIssue {
id: String,
title: String,
#[serde(default)]
status: Status,
#[serde(default)]
issue_type: IssueType,
#[serde(default)]
labels: Vec<String>,
#[serde(default)]
assignee: Option<String>,
#[serde(default)]
owner: Option<String>,
}
#[derive(Default, Debug)]
struct CompletionIndex {
issues: Vec<CompletionIssue>,
labels: Vec<String>,
assignees: Vec<String>,
owners: Vec<String>,
types: Vec<String>,
}
#[derive(Default, Debug)]
struct CompletionConfigIndex {
config_keys: Vec<String>,
saved_queries: Vec<String>,
}
static COMPLETION_INDEX: OnceLock<CompletionIndex> = OnceLock::new();
static CONFIG_INDEX: OnceLock<CompletionConfigIndex> = OnceLock::new();
const STATUS_CANDIDATES: &[(&str, &str)] = &[
("open", "Open issue"),
("in_progress", "In progress"),
("blocked", "Blocked"),
("deferred", "Deferred"),
("draft", "Draft"),
("closed", "Closed"),
("tombstone", "Deleted"),
("pinned", "Pinned"),
];
const STATUS_WITH_ALL_CANDIDATES: &[(&str, &str)] = &[
("all", "All statuses"),
("open", "Open issue"),
("in_progress", "In progress"),
("blocked", "Blocked"),
("deferred", "Deferred"),
("draft", "Draft"),
("closed", "Closed"),
("tombstone", "Deleted"),
("pinned", "Pinned"),
];
const ISSUE_TYPE_CANDIDATES: &[(&str, &str)] = &[
("task", "Task"),
("bug", "Bug"),
("feature", "Feature"),
("epic", "Epic"),
("chore", "Chore"),
("docs", "Docs"),
("question", "Question"),
];
const PRIORITY_CANDIDATES: &[(&str, &str)] = &[
("0", "Critical (P0)"),
("1", "High (P1)"),
("2", "Medium (P2)"),
("3", "Low (P3)"),
("4", "Backlog (P4)"),
("P0", "Critical (0)"),
("P1", "High (1)"),
("P2", "Medium (2)"),
("P3", "Low (3)"),
("P4", "Backlog (4)"),
];
const PRIORITY_NUMERIC_CANDIDATES: &[(&str, &str)] = &[
("0", "Critical (P0)"),
("1", "High (P1)"),
("2", "Medium (P2)"),
("3", "Low (P3)"),
("4", "Backlog (P4)"),
];
const DEP_TYPE_CANDIDATES: &[(&str, &str)] = &[
("blocks", "Blocks (default)"),
("parent-child", "Parent child"),
("conditional-blocks", "Conditional blocks"),
("waits-for", "Waits for"),
("related", "Related"),
("discovered-from", "Discovered from"),
("replies-to", "Replies to"),
("relates-to", "Relates to"),
("duplicates", "Duplicates"),
("supersedes", "Supersedes"),
("caused-by", "Caused by"),
];
const SORT_KEY_CANDIDATES: &[(&str, &str)] = &[
("priority", "Priority"),
("created_at", "Created at"),
("updated_at", "Updated at"),
("title", "Title"),
("created", "Alias for created_at"),
("updated", "Alias for updated_at"),
];
const DEP_TREE_FORMAT_CANDIDATES: &[(&str, &str)] =
&[("text", "Text output"), ("mermaid", "Mermaid graph")];
const CSV_FIELD_CANDIDATES: &[(&str, &str)] = &[
("id", "Issue ID"),
("title", "Title"),
("description", "Description"),
("status", "Status"),
("priority", "Priority"),
("issue_type", "Issue type"),
("assignee", "Assignee"),
("owner", "Owner"),
("created_at", "Created at"),
("updated_at", "Updated at"),
("closed_at", "Closed at"),
("due_at", "Due at"),
("defer_until", "Defer until"),
("notes", "Notes"),
("external_ref", "External ref"),
];
const EXPORT_ERROR_POLICY_CANDIDATES: &[(&str, &str)] = &[
("strict", "Abort export on any error (default)"),
(
"best-effort",
"Skip problematic records, export what we can",
),
("partial", "Export valid records, report failures"),
(
"required-core",
"Only export core issues, tolerate non-core errors",
),
];
const ORPHAN_MODE_CANDIDATES: &[(&str, &str)] = &[
("strict", "Fail if any issue references a missing parent"),
("resurrect", "Attempt to resurrect missing parents if found"),
("skip", "Skip orphaned issues"),
("allow", "Allow orphans (no parent validation)"),
];
const SAVED_QUERY_PREFIX: &str = "saved_query:";
fn completion_index() -> &'static CompletionIndex {
COMPLETION_INDEX.get_or_init(build_completion_index)
}
fn config_index() -> &'static CompletionConfigIndex {
CONFIG_INDEX.get_or_init(build_config_index)
}
fn add_layer_keys(keys: &mut BTreeSet<String>, layer: &config::ConfigLayer) {
keys.extend(layer.runtime.keys().cloned());
keys.extend(layer.startup.keys().cloned());
}
fn resolve_completion_paths_for_beads_dir(beads_dir: &Path) -> Option<config::ConfigPaths> {
config::resolve_paths(beads_dir, None).ok()
}
fn completion_paths() -> Option<config::ConfigPaths> {
let beads_dir = config::discover_beads_dir(None).ok()?;
resolve_completion_paths_for_beads_dir(&beads_dir)
}
fn saved_queries_from_db(db_path: &Path) -> BTreeSet<String> {
if !db_path.is_file() {
return BTreeSet::new();
}
let Ok(queries) = config::with_database_family_snapshot(db_path, |snapshot_db_path| {
let conn = Connection::open(snapshot_db_path.to_string_lossy().into_owned())?;
let _ = conn.execute("PRAGMA busy_timeout=0");
let rows = conn.query("SELECT key FROM config")?;
let mut queries = BTreeSet::new();
for row in &rows {
let Some(key) = row.get(0).and_then(SqliteValue::as_text) else {
continue;
};
if let Some(name) = key.strip_prefix(SAVED_QUERY_PREFIX)
&& !name.trim().is_empty()
{
queries.insert(name.to_string());
}
}
conn.close()?;
Ok(queries)
}) else {
return BTreeSet::new();
};
queries
}
fn build_completion_index() -> CompletionIndex {
let Some(paths) = completion_paths() else {
return CompletionIndex::default();
};
let Ok(file) = File::open(&paths.jsonl_path) else {
return CompletionIndex::default();
};
let reader = BufReader::new(file);
let mut issues = Vec::new();
let mut labels = BTreeSet::new();
let mut assignees = BTreeSet::new();
let mut owners = BTreeSet::new();
let mut types = BTreeSet::new();
for line_result in reader.lines() {
let Ok(line) = line_result else {
break;
};
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let issue: CompletionIssue = match serde_json::from_str(trimmed) {
Ok(issue) => issue,
Err(_) => continue,
};
for label in &issue.labels {
let label = label.trim();
if !label.is_empty() {
labels.insert(label.to_string());
}
}
if let Some(assignee) = issue.assignee.as_deref() {
let assignee = assignee.trim();
if !assignee.is_empty() {
assignees.insert(assignee.to_string());
}
}
if let Some(owner) = issue.owner.as_deref() {
let owner = owner.trim();
if !owner.is_empty() {
owners.insert(owner.to_string());
}
}
let issue_type = issue.issue_type.as_str().trim();
if !issue_type.is_empty() {
types.insert(issue_type.to_string());
}
issues.push(issue);
}
issues.sort_by(|a, b| a.id.cmp(&b.id));
CompletionIndex {
issues,
labels: labels.into_iter().collect(),
assignees: assignees.into_iter().collect(),
owners: owners.into_iter().collect(),
types: types.into_iter().collect(),
}
}
fn build_config_index() -> CompletionConfigIndex {
let mut keys = BTreeSet::new();
let mut saved_queries = BTreeSet::new();
add_layer_keys(&mut keys, &config::default_config_layer());
if let Ok(legacy_user) = config::load_legacy_user_config() {
add_layer_keys(&mut keys, &legacy_user);
}
if let Ok(user) = config::load_user_config() {
add_layer_keys(&mut keys, &user);
}
add_layer_keys(&mut keys, &config::ConfigLayer::from_env());
if let Some(paths) = completion_paths() {
if let Ok(project) = config::load_project_config(&paths.beads_dir) {
add_layer_keys(&mut keys, &project);
}
saved_queries.extend(saved_queries_from_db(&paths.db_path));
}
CompletionConfigIndex {
config_keys: keys.into_iter().collect(),
saved_queries: saved_queries.into_iter().collect(),
}
}
fn matches_prefix_case_insensitive(value: &str, prefix: &str) -> bool {
if prefix.is_empty() {
return true;
}
value
.to_ascii_lowercase()
.starts_with(&prefix.to_ascii_lowercase())
}
fn static_candidates(
prefix: &str,
values: &[(&'static str, &'static str)],
) -> Vec<CompletionCandidate> {
values
.iter()
.filter(|(value, _)| matches_prefix_case_insensitive(value, prefix))
.map(|(value, help)| CompletionCandidate::new(*value).help(Some(StyledStr::from(*help))))
.collect()
}
fn static_candidates_with_suffix(
prefix: &str,
values: &[(&'static str, &'static str)],
suffix: &str,
) -> Vec<CompletionCandidate> {
values
.iter()
.filter(|(value, _)| matches_prefix_case_insensitive(value, prefix))
.map(|(value, help)| {
CompletionCandidate::new(format!("{value}{suffix}")).help(Some(StyledStr::from(*help)))
})
.collect()
}
fn dynamic_candidates(prefix: &str, values: &[String]) -> Vec<CompletionCandidate> {
values
.iter()
.filter(|value| matches_prefix_case_insensitive(value, prefix))
.map(CompletionCandidate::new)
.collect()
}
fn split_delimited_prefix(current: &str, delimiter: char) -> (String, &str) {
current.rfind(delimiter).map_or_else(
|| (String::new(), current.trim_start()),
|idx| {
let (head, tail) = current.split_at(idx + delimiter.len_utf8());
let trimmed_tail = tail.trim_start();
let ws_len = tail.len().saturating_sub(trimmed_tail.len());
let mut prefix = String::with_capacity(head.len() + ws_len);
prefix.push_str(head);
prefix.push_str(&tail[..ws_len]);
(prefix, trimmed_tail)
},
)
}
fn split_key_prefix(current: &str, delimiter: char) -> Option<(String, &str)> {
let idx = current.find(delimiter)?;
let (head, tail) = current.split_at(idx + delimiter.len_utf8());
let trimmed_tail = tail.trim_start();
let ws_len = tail.len().saturating_sub(trimmed_tail.len());
let mut prefix = String::with_capacity(head.len() + ws_len);
prefix.push_str(head);
prefix.push_str(&tail[..ws_len]);
Some((prefix, trimmed_tail))
}
fn static_candidates_delimited(
current: &OsStr,
delimiter: char,
values: &[(&'static str, &'static str)],
) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
let (prefix, needle) = split_delimited_prefix(current, delimiter);
static_candidates(needle, values)
.into_iter()
.map(|candidate| candidate.add_prefix(prefix.clone()))
.collect()
}
fn dynamic_candidates_delimited(
current: &OsStr,
delimiter: char,
values: &[String],
) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
let (prefix, needle) = split_delimited_prefix(current, delimiter);
dynamic_candidates(needle, values)
.into_iter()
.map(|candidate| candidate.add_prefix(prefix.clone()))
.collect()
}
fn issue_id_completer(current: &OsStr) -> Vec<CompletionCandidate> {
issue_id_completer_with_filter(current, IssueCompletionFilter::Any)
}
fn open_issue_id_completer(current: &OsStr) -> Vec<CompletionCandidate> {
issue_id_completer_with_filter(current, IssueCompletionFilter::Open)
}
fn closed_issue_id_completer(current: &OsStr) -> Vec<CompletionCandidate> {
issue_id_completer_with_filter(current, IssueCompletionFilter::Closed)
}
fn issue_id_completer_with_filter(
current: &OsStr,
filter: IssueCompletionFilter,
) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
issue_id_candidates(prefix, filter)
}
fn issue_id_candidates(prefix: &str, filter: IssueCompletionFilter) -> Vec<CompletionCandidate> {
let mut candidates = Vec::new();
for issue in &completion_index().issues {
if !prefix.is_empty() && !issue.id.starts_with(prefix) {
continue;
}
if filter.matches(&issue.status) {
let title = truncate_title(&issue.title, 60);
let help = format!("{} | {}", format_status_label(&issue.status, false), title);
candidates.push(CompletionCandidate::new(&issue.id).help(Some(StyledStr::from(help))));
}
}
candidates
}
fn status_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, STATUS_CANDIDATES)
}
fn status_completer_delimited(current: &OsStr) -> Vec<CompletionCandidate> {
static_candidates_delimited(current, ',', STATUS_CANDIDATES)
}
fn status_or_all_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, STATUS_WITH_ALL_CANDIDATES)
}
fn issue_type_is_standard(value: &str) -> bool {
ISSUE_TYPE_CANDIDATES
.iter()
.any(|(candidate, _)| candidate.eq_ignore_ascii_case(value))
}
fn issue_type_candidates(prefix: &str) -> Vec<CompletionCandidate> {
let mut candidates = static_candidates(prefix, ISSUE_TYPE_CANDIDATES);
candidates.extend(
completion_index()
.types
.iter()
.filter(|value| !issue_type_is_standard(value))
.filter(|value| matches_prefix_case_insensitive(value, prefix))
.map(CompletionCandidate::new),
);
candidates
}
fn issue_type_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
issue_type_candidates(prefix)
}
fn issue_type_completer_delimited(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
let (prefix, needle) = split_delimited_prefix(current, ',');
issue_type_candidates(needle)
.into_iter()
.map(|candidate| candidate.add_prefix(prefix.clone()))
.collect()
}
fn issue_type_standard_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, ISSUE_TYPE_CANDIDATES)
}
fn priority_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, PRIORITY_CANDIDATES)
}
fn priority_completer_delimited(current: &OsStr) -> Vec<CompletionCandidate> {
static_candidates_delimited(current, ',', PRIORITY_CANDIDATES)
}
fn priority_numeric_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, PRIORITY_NUMERIC_CANDIDATES)
}
fn label_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &completion_index().labels)
}
fn label_completer_delimited(current: &OsStr) -> Vec<CompletionCandidate> {
dynamic_candidates_delimited(current, ',', &completion_index().labels)
}
fn assignee_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &completion_index().assignees)
}
fn owner_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &completion_index().owners)
}
fn dep_type_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, DEP_TYPE_CANDIDATES)
}
fn deps_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(current) = current.to_str() else {
return Vec::new();
};
let (outer_prefix, tail) = split_delimited_prefix(current, ',');
if let Some((type_prefix, id_prefix)) = split_key_prefix(tail, ':') {
let mut prefix = outer_prefix;
prefix.push_str(&type_prefix);
return issue_id_candidates(id_prefix, IssueCompletionFilter::Any)
.into_iter()
.map(|candidate| candidate.add_prefix(prefix.clone()))
.collect();
}
static_candidates_with_suffix(tail, DEP_TYPE_CANDIDATES, ":")
.into_iter()
.map(|candidate| candidate.add_prefix(outer_prefix.clone()))
.collect()
}
fn dep_tree_format_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, DEP_TREE_FORMAT_CANDIDATES)
}
fn saved_query_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &config_index().saved_queries)
}
fn config_key_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
dynamic_candidates(prefix, &config_index().config_keys)
}
fn config_key_assignment_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
if prefix.contains('=') {
return Vec::new();
}
let mut candidates = Vec::new();
for key in &config_index().config_keys {
if matches_prefix_case_insensitive(key, prefix) {
candidates.push(CompletionCandidate::new(key));
candidates.push(CompletionCandidate::new(format!("{key}=")));
}
}
candidates
}
fn export_error_policy_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, EXPORT_ERROR_POLICY_CANDIDATES)
}
fn orphan_mode_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, ORPHAN_MODE_CANDIDATES)
}
fn sort_key_completer(current: &OsStr) -> Vec<CompletionCandidate> {
let Some(prefix) = current.to_str() else {
return Vec::new();
};
static_candidates(prefix, SORT_KEY_CANDIDATES)
}
fn csv_fields_completer(current: &OsStr) -> Vec<CompletionCandidate> {
static_candidates_delimited(current, ',', CSV_FIELD_CANDIDATES)
}
#[derive(Parser, Debug)]
#[command(name = "br", author, version, about, long_about = None)]
#[allow(clippy::struct_excessive_bools)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
#[arg(long, global = true)]
pub db: Option<PathBuf>,
#[arg(long, global = true)]
pub actor: Option<String>,
#[arg(long, global = true)]
pub json: bool,
#[arg(long, global = true)]
pub no_daemon: bool,
#[arg(long, global = true)]
pub no_auto_flush: bool,
#[arg(long, global = true)]
pub no_auto_import: bool,
#[arg(long, global = true)]
pub allow_stale: bool,
#[arg(long, global = true)]
pub lock_timeout: Option<u64>,
#[arg(long, global = true)]
pub no_db: bool,
#[arg(short, long, action = clap::ArgAction::Count, global = true)]
pub verbose: u8,
#[arg(short, long, global = true)]
pub quiet: bool,
#[arg(long, global = true)]
pub no_color: bool,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
Agents(AgentsArgs),
Audit {
#[command(subcommand)]
command: AuditCommands,
},
Blocked(BlockedArgs),
Capabilities(CapabilitiesArgs),
Capacity {
#[command(subcommand)]
command: CapacityCommands,
},
Changelog(ChangelogArgs),
Close(CloseArgs),
#[command(alias = "comment")]
Comments(CommentsArgs),
#[command(alias = "completion")]
Completions(CompletionsArgs),
Config {
#[command(subcommand)]
command: ConfigCommands,
},
#[command(alias = "coord")]
Coordination {
#[command(subcommand)]
command: CoordinationCommands,
},
Count(CountArgs),
Create(CreateArgs),
Defer(DeferArgs),
Delete(DeleteArgs),
Dep {
#[command(subcommand)]
command: DepCommands,
},
Doctor(DoctorArgs),
Epic {
#[command(subcommand)]
command: EpicCommands,
},
Gate {
#[command(subcommand)]
command: GateCommands,
},
Graph(GraphArgs),
History(HistoryArgs),
Info(InfoArgs),
Init {
#[arg(long)]
prefix: Option<String>,
#[arg(long)]
force: bool,
#[arg(long)]
backend: Option<String>,
},
Label {
#[command(subcommand)]
command: LabelCommands,
},
Lint(LintArgs),
List(ListArgs),
Orphans(OrphansArgs),
Q(QuickArgs),
Query {
#[command(subcommand)]
command: QueryCommands,
},
Ready(ReadyArgs),
Reopen(ReopenArgs),
#[command(name = "robot-docs", alias = "robot_docs")]
RobotDocs {
#[command(subcommand)]
command: RobotDocsCommands,
},
#[command(alias = "schedule")]
Scheduler(SchedulerArgs),
Schema(SchemaArgs),
Search(SearchArgs),
Show(ShowArgs),
Stale(StaleArgs),
Stats(StatsArgs),
Status(StatsArgs),
#[command(long_about = "Sync database with JSONL file (export or import).
SAFETY GUARANTEES:
• br sync NEVER executes git commands or auto-commits
• br sync NEVER modifies files outside .beads/ (unless --allow-external-jsonl)
• All writes use atomic temp-file-then-rename pattern
• Safety guards prevent accidental data loss
MODES (one required):
--flush-only Export database to JSONL (safe by default)
--import-only Import JSONL into database (validates first)
--merge Three-way merge .beads/beads.base.jsonl + DB + JSONL
--status Show sync status (read-only)
--witness Emit deterministic JSONL chunk witness (read-only)
--reconcile-additive
Plan/apply exact-ID additive reconciliation
SAFETY GUARDS:
Export guards (bypassed with --force):
• Empty DB Guard: Refuses to export empty DB over non-empty JSONL
• Stale DB Guard: Refuses to export if JSONL has issues missing from DB
Import guards (cannot be bypassed):
• Conflict markers: Rejects files with git merge conflict markers
• Invalid JSON: Rejects malformed JSONL entries
Merge guards:
• Semantic conflicts require --force-db, --force-jsonl, or --force
• --force-db keeps the local SQLite version
• --force-jsonl keeps the JSONL version
• --force keeps the newer timestamp
Rebuild:
• --rebuild is import-only and treats JSONL as authoritative
• Removes DB entries absent from JSONL while preserving tombstones
VERBOSE LOGGING:
-v Show INFO-level safety guard decisions
-vv Show DEBUG-level file operations
EXAMPLES:
br sync --flush-only Export database to .beads/issues.jsonl
br sync --flush-only -v Export with safety logging
br sync --import-only Import from JSONL (validates first)
br sync --merge Merge DB and JSONL changes
br sync --merge --force-db Keep local DB conflicts
br sync --merge --force-jsonl Keep JSONL conflicts
br sync --import-only --rebuild Import + remove DB entries not in JSONL
br sync --status Show current sync status
br sync --witness --json Emit JSONL chunk witness
br vcs-status --json Explicitly inspect JSONL Git visibility")]
Sync(SyncArgs),
Undefer(UndeferArgs),
Update(UpdateArgs),
#[command(name = "vcs-status")]
VcsStatus(VcsStatusArgs),
#[cfg(feature = "mcp")]
Serve(crate::mcp::ServeArgs),
#[cfg(feature = "self_update")]
Upgrade(UpgradeArgs),
Version(VersionArgs),
Where,
}
#[derive(Args, Debug, Clone)]
pub struct CompletionsArgs {
#[arg(value_enum)]
pub shell: ShellType,
#[arg(long, short = 'o')]
pub output: Option<std::path::PathBuf>,
}
#[derive(ValueEnum, Debug, Clone, Copy, Eq, PartialEq)]
pub enum ShellType {
Bash,
Zsh,
Fish,
#[value(name = "powershell")]
#[value(alias = "pwsh")]
PowerShell,
Elvish,
}
#[derive(Args, Debug, Default)]
pub struct CreateArgs {
pub title: Option<String>,
#[arg(long = "title", conflicts_with = "title")]
pub title_flag: Option<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Option<String>,
#[arg(long)]
pub slug: Option<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Option<String>,
#[arg(long, short = 'd', visible_alias = "body", allow_hyphen_values = true)]
pub description: Option<String>,
#[arg(long, value_name = "PATH", conflicts_with = "description")]
pub description_file: Option<std::path::PathBuf>,
#[arg(long, short = 'a', add = ArgValueCompleter::new(assignee_completer))]
pub assignee: Option<String>,
#[arg(long, add = ArgValueCompleter::new(owner_completer))]
pub owner: Option<String>,
#[arg(long, short = 'l', value_delimiter = ',', add = ArgValueCompleter::new(label_completer_delimited))]
pub labels: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub parent: Option<String>,
#[arg(long, value_delimiter = ',', add = ArgValueCompleter::new(deps_completer))]
pub deps: Vec<String>,
#[arg(long, short = 'e')]
pub estimate: Option<i32>,
#[arg(long)]
pub due: Option<String>,
#[arg(long)]
pub defer: Option<String>,
#[arg(long)]
pub external_ref: Option<String>,
#[arg(long)]
pub ephemeral: bool,
#[arg(long, short = 's', add = ArgValueCompleter::new(status_completer))]
pub status: Option<String>,
#[arg(long, visible_alias = "acceptance", allow_hyphen_values = true)]
pub acceptance_criteria: Option<String>,
#[arg(long = "agent-context")]
pub agent_context: Option<String>,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub silent: bool,
#[arg(long, short = 'f')]
pub file: Option<std::path::PathBuf>,
#[arg(long, value_name = "NAME", env = "BR_AGENT_NAME")]
pub agent_name: Option<String>,
#[arg(long, value_name = "HARNESS", env = "BR_HARNESS")]
pub harness: Option<String>,
#[arg(long, value_name = "MODEL", env = "BR_MODEL")]
pub model: Option<String>,
}
#[derive(Args, Debug)]
pub struct QuickArgs {
pub title: Vec<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Option<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Option<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub labels: Vec<String>,
#[arg(long, short = 'd', visible_alias = "body", allow_hyphen_values = true)]
pub description: Option<String>,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub parent: Option<String>,
#[arg(long, short = 'e')]
pub estimate: Option<i32>,
}
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct UpdateArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub ids: Vec<String>,
#[arg(long)]
pub title: Option<String>,
#[arg(long, short = 'd', visible_alias = "body", allow_hyphen_values = true)]
pub description: Option<String>,
#[arg(long, value_name = "PATH", conflicts_with = "description")]
pub description_file: Option<PathBuf>,
#[arg(long, allow_hyphen_values = true)]
pub design: Option<String>,
#[arg(long, visible_alias = "acceptance", allow_hyphen_values = true)]
pub acceptance_criteria: Option<String>,
#[arg(long, allow_hyphen_values = true)]
pub notes: Option<String>,
#[arg(long, value_name = "COMMENT", allow_hyphen_values = true)]
pub transition_comment: Option<String>,
#[arg(long, short = 's', add = ArgValueCompleter::new(status_completer))]
pub status: Option<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Option<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Option<String>,
#[arg(long, add = ArgValueCompleter::new(assignee_completer))]
pub assignee: Option<String>,
#[arg(long, add = ArgValueCompleter::new(owner_completer))]
pub owner: Option<String>,
#[arg(long)]
pub claim: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub due: Option<String>,
#[arg(long)]
pub defer: Option<String>,
#[arg(long)]
pub estimate: Option<i32>,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub add_label: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub remove_label: Vec<String>,
#[arg(long, visible_alias = "labels", add = ArgValueCompleter::new(label_completer_delimited))]
pub set_labels: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub parent: Option<String>,
#[arg(long)]
pub external_ref: Option<String>,
#[arg(long = "source-repo")]
pub source_repo: Option<String>,
#[arg(long = "source-repo-path")]
pub source_repo_path: Option<String>,
#[arg(long = "agent-context")]
pub agent_context: Option<String>,
#[arg(long)]
pub session: Option<String>,
#[arg(long, value_name = "NAME", env = "BR_AGENT_NAME")]
pub agent_name: Option<String>,
#[arg(long, value_name = "HARNESS", env = "BR_HARNESS")]
pub harness: Option<String>,
#[arg(long, value_name = "MODEL", env = "BR_MODEL")]
pub model: Option<String>,
}
#[derive(Args, Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct DeleteArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, default_value = "delete")]
pub reason: String,
#[arg(long)]
pub from_file: Option<PathBuf>,
#[arg(long)]
pub cascade: bool,
#[arg(long, conflicts_with = "cascade")]
pub force: bool,
#[arg(long)]
pub hard: bool,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Args, Debug, Default, Clone)]
pub struct InfoArgs {
#[arg(long)]
pub schema: bool,
#[arg(long)]
pub projections: bool,
#[arg(long = "whats-new", conflicts_with = "thanks")]
pub whats_new: bool,
#[arg(long, conflicts_with = "whats_new")]
pub thanks: bool,
}
#[derive(Args, Debug, Clone)]
pub struct VcsStatusArgs {
#[arg(long, value_name = "PATH")]
pub jsonl: Option<PathBuf>,
#[arg(long)]
pub allow_external_jsonl: bool,
#[arg(long, default_value_t = 2_000, value_name = "MILLISECONDS")]
pub timeout_ms: u64,
#[arg(long)]
pub robot: bool,
}
impl Default for VcsStatusArgs {
fn default() -> Self {
Self {
jsonl: None,
allow_external_jsonl: false,
timeout_ms: 2_000,
robot: false,
}
}
}
#[derive(Args, Debug, Default, Clone)]
pub struct SchemaArgs {
#[arg(value_enum, default_value_t)]
pub target: SchemaTarget,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
}
#[derive(Subcommand, Debug)]
pub enum CoordinationCommands {
Status(CoordinationStatusArgs),
}
#[derive(Args, Debug, Clone, Default)]
pub struct CoordinationStatusArgs {
#[arg(long, value_enum, default_value_t)]
pub owner_kind: CoordinationOwnerKindArg,
#[arg(long, default_value_t = 2)]
pub comments: usize,
#[arg(long)]
pub reservations: Option<PathBuf>,
#[arg(long)]
pub agents: Option<PathBuf>,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum CoordinationOwnerKindArg {
#[default]
SwarmAgent,
Human,
Unknown,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum SchemaTarget {
#[default]
All,
Issue,
IssueWithCounts,
IssueDetails,
ReadyIssue,
StaleIssue,
BlockedIssue,
TreeNode,
Statistics,
CoordinationStatus,
AdditiveReconciliation,
VcsStatus,
Error,
Commands,
}
#[derive(Args, Debug, Default, Clone)]
pub struct CapabilitiesArgs {
#[arg(long, visible_alias = "for", value_name = "COMMAND_PATH")]
pub command: Option<String>,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum RobotDocsCommands {
Guide(RobotDocsGuideArgs),
}
#[derive(Args, Debug, Default, Clone)]
pub struct RobotDocsGuideArgs {
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum OutputFormat {
#[default]
Text,
Json,
Csv,
Toon,
}
impl OutputFormat {
#[must_use]
pub fn from_env() -> Option<Self> {
if let Ok(value) = std::env::var("BR_OUTPUT_FORMAT")
&& let Some(format) = Self::parse_env_value(&value)
{
return Some(format);
}
if let Ok(value) = std::env::var("TOON_DEFAULT_FORMAT")
&& let Some(format) = Self::parse_env_value(&value)
{
return Some(format);
}
None
}
fn parse_env_value(value: &str) -> Option<Self> {
match value.trim().to_ascii_lowercase().as_str() {
"text" | "plain" => Some(Self::Text),
"json" => Some(Self::Json),
"csv" => Some(Self::Csv),
"toon" => Some(Self::Toon),
_ => None,
}
}
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum OutputFormatBasic {
#[default]
Text,
Json,
Toon,
}
impl From<OutputFormatBasic> for OutputFormat {
fn from(format: OutputFormatBasic) -> Self {
match format {
OutputFormatBasic::Text => Self::Text,
OutputFormatBasic::Json => Self::Json,
OutputFormatBasic::Toon => Self::Toon,
}
}
}
#[must_use]
pub fn resolve_output_format(
requested: Option<OutputFormat>,
json: bool,
robot: bool,
) -> OutputFormat {
if json || robot {
OutputFormat::Json
} else if let Some(requested) = requested {
requested
} else {
OutputFormat::from_env().unwrap_or(OutputFormat::Text)
}
}
#[must_use]
pub const fn command_requests_robot_json(cmd: &Commands) -> bool {
match cmd {
Commands::Close(args) => args.robot,
Commands::Coordination { command } => coordination_command_requests_robot_json(command),
Commands::Reopen(args) => args.robot,
Commands::Ready(args) => args.robot,
Commands::Scheduler(args) => args.robot,
Commands::Blocked(args) => args.robot,
Commands::Stats(args) | Commands::Status(args) => args.robot,
Commands::Defer(args) => args.robot,
Commands::Undefer(args) => args.robot,
Commands::Orphans(args) => args.robot,
Commands::Changelog(args) => args.robot,
Commands::Sync(args) => args.robot,
Commands::VcsStatus(args) => args.robot,
Commands::Doctor(args) => args.robot_triage,
Commands::Dep { command } => match command {
DepCommands::Import(args) => args.robot,
DepCommands::Add(_)
| DepCommands::Remove(_)
| DepCommands::List(_)
| DepCommands::Tree(_)
| DepCommands::Cycles(_) => false,
},
Commands::Gate { command } => match command {
GateCommands::Report(args) => args.robot,
GateCommands::List(args) => args.robot,
},
Commands::Capacity { command } => match command {
CapacityCommands::Exempt(args) => args.robot,
CapacityCommands::Renew(args) => args.robot,
CapacityCommands::Revoke(args) => args.robot,
CapacityCommands::Exemptions(args) => args.robot,
},
_ => false,
}
}
const fn coordination_command_requests_robot_json(command: &CoordinationCommands) -> bool {
match command {
CoordinationCommands::Status(args) => args.robot,
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum InheritedOutputMode {
None,
Quiet,
Json,
Toon,
}
#[must_use]
pub fn resolve_output_format_with_outer_mode(
requested: Option<OutputFormat>,
inherited_mode: InheritedOutputMode,
robot: bool,
) -> OutputFormat {
if matches!(inherited_mode, InheritedOutputMode::Json) || robot {
OutputFormat::Json
} else if let Some(requested) = requested {
requested
} else if matches!(inherited_mode, InheritedOutputMode::Toon) {
OutputFormat::Toon
} else if matches!(inherited_mode, InheritedOutputMode::Quiet) {
OutputFormat::Text
} else {
OutputFormat::from_env().unwrap_or(OutputFormat::Text)
}
}
#[must_use]
pub fn resolve_output_format_basic(
requested: Option<OutputFormatBasic>,
json: bool,
robot: bool,
) -> OutputFormat {
let resolved = resolve_output_format(requested.map(Into::into), json, robot);
match resolved {
OutputFormat::Csv => OutputFormat::Text,
other => other,
}
}
#[must_use]
pub fn resolve_output_format_basic_with_outer_mode(
requested: Option<OutputFormatBasic>,
inherited_mode: InheritedOutputMode,
robot: bool,
) -> OutputFormat {
let resolved =
resolve_output_format_with_outer_mode(requested.map(Into::into), inherited_mode, robot);
match resolved {
OutputFormat::Csv => OutputFormat::Text,
other => other,
}
}
#[derive(Args, Debug, Default, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct ListArgs {
#[arg(long, short = 's', add = ArgValueCompleter::new(status_completer))]
pub status: Vec<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(assignee_completer))]
pub assignee: Option<String>,
#[arg(long)]
pub unassigned: bool,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub id: Vec<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub label_any: Vec<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(priority_numeric_completer))]
pub priority_min: Option<u8>,
#[arg(long, add = ArgValueCompleter::new(priority_numeric_completer))]
pub priority_max: Option<u8>,
#[arg(long)]
pub title_contains: Option<String>,
#[arg(long)]
pub desc_contains: Option<String>,
#[arg(long)]
pub notes_contains: Option<String>,
#[arg(long, short = 'a')]
pub all: bool,
#[arg(long)]
pub limit: Option<usize>,
#[arg(long)]
pub offset: Option<usize>,
#[arg(long, add = ArgValueCompleter::new(sort_key_completer))]
pub sort: Option<String>,
#[arg(long, short = 'r')]
pub reverse: bool,
#[arg(long)]
pub deferred: bool,
#[arg(long)]
pub overdue: bool,
#[arg(long)]
pub long: bool,
#[arg(long)]
pub pretty: bool,
#[arg(long)]
pub wrap: bool,
#[arg(long, value_enum)]
pub format: Option<OutputFormat>,
#[arg(long)]
pub stats: bool,
#[arg(long, value_name = "FIELDS", add = ArgValueCompleter::new(csv_fields_completer))]
pub fields: Option<String>,
}
#[derive(Args, Debug, Default)]
pub struct SearchArgs {
pub query: String,
#[command(flatten)]
pub filters: ListArgs,
}
#[derive(Args, Debug, Clone, Default)]
pub struct ShowArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub wrap: bool,
#[arg(long, conflicts_with = "wrap")]
pub no_wrap: bool,
#[arg(long)]
pub stats: bool,
}
#[derive(Subcommand, Debug)]
pub enum DepCommands {
Add(DepAddArgs),
Import(DepImportArgs),
#[command(visible_alias = "rm")]
Remove(DepRemoveArgs),
List(DepListArgs),
Tree(DepTreeArgs),
Cycles(DepCyclesArgs),
}
#[derive(Subcommand, Debug)]
pub enum EpicCommands {
Status(EpicStatusArgs),
#[command(name = "close-eligible")]
CloseEligible(EpicCloseEligibleArgs),
}
#[derive(Args, Debug, Clone, Default)]
pub struct EpicStatusArgs {
#[arg(long)]
pub eligible_only: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct EpicCloseEligibleArgs {
#[arg(long)]
pub dry_run: bool,
#[arg(long, value_name = "COMMENT", allow_hyphen_values = true)]
pub transition_comment: Option<String>,
}
#[derive(Subcommand, Debug)]
pub enum GateCommands {
Report(GateReportArgs),
List(GateListArgs),
}
#[derive(ValueEnum, Debug, Clone, Copy, Eq, PartialEq)]
pub enum GateStatus {
Pass,
Fail,
}
#[derive(Args, Debug, Clone)]
pub struct GateReportArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
#[arg(long)]
pub gate: String,
#[arg(long)]
pub provider: String,
#[arg(long, value_enum)]
pub status: GateStatus,
#[arg(long, value_name = "STATUS", add = ArgValueCompleter::new(status_completer))]
pub to: Option<String>,
#[arg(long)]
pub note: Option<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone)]
pub struct GateListArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
#[arg(long)]
pub robot: bool,
}
#[derive(Subcommand, Debug)]
pub enum CapacityCommands {
Exempt(CapacityExemptArgs),
Renew(CapacityRenewArgs),
Revoke(CapacityRevokeArgs),
Exemptions(CapacityExemptionsArgs),
}
#[derive(Args, Debug, Clone)]
pub struct CapacityExemptArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
#[arg(long, value_name = "STATUS", conflicts_with = "group", add = ArgValueCompleter::new(status_completer))]
pub status: Option<String>,
#[arg(long, value_name = "GROUP", conflicts_with = "status")]
pub group: Option<String>,
#[arg(long)]
pub provider: String,
#[arg(long)]
pub reason: String,
#[arg(long, value_name = "WHEN")]
pub expires: Option<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone)]
pub struct CapacityRenewArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
#[arg(long, value_name = "STATUS", conflicts_with = "group", add = ArgValueCompleter::new(status_completer))]
pub status: Option<String>,
#[arg(long, value_name = "GROUP", conflicts_with = "status")]
pub group: Option<String>,
#[arg(long)]
pub provider: String,
#[arg(long, value_name = "WHEN")]
pub expires: Option<String>,
#[arg(long)]
pub reason: Option<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone)]
pub struct CapacityRevokeArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
#[arg(long, value_name = "STATUS", conflicts_with = "group", add = ArgValueCompleter::new(status_completer))]
pub status: Option<String>,
#[arg(long, value_name = "GROUP", conflicts_with = "status")]
pub group: Option<String>,
#[arg(long)]
pub provider: String,
#[arg(long)]
pub reason: Option<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone)]
pub struct CapacityExemptionsArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: Option<String>,
#[arg(long)]
pub history: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Default)]
pub struct DepAddArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: String,
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub depends_on: String,
#[arg(long = "type", short = 't', default_value = "blocks", add = ArgValueCompleter::new(dep_type_completer))]
pub dep_type: String,
#[arg(long)]
pub metadata: Option<String>,
}
#[derive(Args, Debug)]
pub struct DepImportArgs {
pub path: PathBuf,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug)]
pub struct DepRemoveArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: String,
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub depends_on: String,
}
#[derive(Args, Debug)]
pub struct DepListArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: String,
#[arg(long, default_value = "down", value_enum)]
pub direction: DepDirection,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(dep_type_completer))]
pub dep_type: Option<String>,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum DepDirection {
#[default]
Down,
Up,
Both,
}
#[derive(Args, Debug)]
pub struct DepTreeArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: String,
#[arg(long, short = 'd', default_value = "down", value_enum)]
pub direction: DepDirection,
#[arg(long, default_value_t = 10)]
pub max_depth: usize,
#[arg(long, default_value = "text", add = ArgValueCompleter::new(dep_tree_format_completer))]
pub format: String,
}
#[derive(Args, Debug)]
pub struct DepCyclesArgs {
#[arg(long)]
pub blocking_only: bool,
#[arg(long)]
pub include_closed: bool,
}
#[derive(Subcommand, Debug)]
pub enum LabelCommands {
Add(LabelAddArgs),
Remove(LabelRemoveArgs),
List(LabelListArgs),
#[command(name = "list-all")]
ListAll,
Rename(LabelRenameArgs),
}
#[derive(Args, Debug)]
pub struct LabelAddArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issues: Vec<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Option<String>,
}
#[derive(Args, Debug)]
pub struct LabelRemoveArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issues: Vec<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Option<String>,
}
#[derive(Args, Debug)]
pub struct LabelListArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub issue: Option<String>,
}
#[derive(Args, Debug)]
pub struct LabelRenameArgs {
#[arg(add = ArgValueCompleter::new(label_completer))]
pub old_name: String,
pub new_name: String,
}
#[derive(Args, Debug)]
pub struct CommentsArgs {
#[command(subcommand)]
pub command: Option<CommentCommands>,
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: Option<String>,
#[arg(long)]
pub wrap: bool,
}
#[derive(Subcommand, Debug)]
pub enum CommentCommands {
Add(CommentAddArgs),
List(CommentListArgs),
}
#[derive(Args, Debug)]
pub struct CommentAddArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
pub text: Vec<String>,
#[arg(short = 'f', long = "file")]
pub file: Option<PathBuf>,
#[arg(long)]
pub author: Option<String>,
#[arg(
long = "message",
short = 'm',
visible_alias = "content",
allow_hyphen_values = true
)]
pub message: Option<String>,
}
#[derive(Args, Debug)]
pub struct CommentListArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
#[arg(long)]
pub wrap: bool,
}
#[derive(Subcommand, Debug)]
pub enum AuditCommands {
Record(AuditRecordArgs),
Coordination(AuditCoordinationArgs),
Label(AuditLabelArgs),
Log(AuditLogArgs),
Summary(AuditSummaryArgs),
}
#[derive(Args, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct AuditRecordArgs {
#[arg(long)]
pub kind: Option<String>,
#[arg(long = "issue-id", add = ArgValueCompleter::new(issue_id_completer))]
pub issue_id: Option<String>,
#[arg(long)]
pub model: Option<String>,
#[arg(long)]
pub prompt: Option<String>,
#[arg(long)]
pub response: Option<String>,
#[arg(long = "tool-name")]
pub tool_name: Option<String>,
#[arg(long = "exit-code")]
pub exit_code: Option<i32>,
#[arg(long)]
pub error: Option<String>,
#[arg(long)]
pub stdin: bool,
}
#[derive(Args, Debug, Clone)]
pub struct AuditCoordinationArgs {
#[arg(long)]
pub stdin: bool,
#[arg(long, default_value = "br coordination status")]
pub command: String,
}
#[derive(Args, Debug, Clone)]
pub struct AuditLabelArgs {
pub entry_id: String,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub label: Option<String>,
#[arg(long)]
pub reason: Option<String>,
}
#[derive(Args, Debug, Clone)]
pub struct AuditLogArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub id: String,
}
#[derive(Args, Debug, Clone, Default)]
pub struct AuditSummaryArgs {
#[arg(long, default_value_t = 30)]
pub days: u32,
}
#[derive(Args, Debug, Clone)]
#[allow(clippy::struct_excessive_bools)]
pub struct CountArgs {
#[arg(long, value_enum)]
pub by: Option<CountBy>,
#[arg(long)]
pub by_status: bool,
#[arg(long)]
pub by_priority: bool,
#[arg(long)]
pub by_type: bool,
#[arg(long)]
pub by_assignee: bool,
#[arg(long)]
pub by_label: bool,
#[arg(long, value_delimiter = ',', add = ArgValueCompleter::new(status_completer_delimited))]
pub status: Vec<String>,
#[arg(long = "type", value_delimiter = ',', add = ArgValueCompleter::new(issue_type_completer_delimited))]
pub types: Vec<String>,
#[arg(long, value_delimiter = ',', add = ArgValueCompleter::new(priority_completer_delimited))]
pub priority: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(assignee_completer))]
pub assignee: Option<String>,
#[arg(long)]
pub unassigned: bool,
#[arg(long)]
pub include_closed: bool,
#[arg(long)]
pub include_templates: bool,
#[arg(long)]
pub title_contains: Option<String>,
}
#[derive(ValueEnum, Debug, Clone, Copy, Eq, PartialEq)]
pub enum CountBy {
Status,
Priority,
Type,
Assignee,
Label,
}
#[derive(Args, Debug, Clone)]
pub struct StaleArgs {
#[arg(long, default_value_t = 30)]
pub days: i64,
#[arg(long, value_delimiter = ',', add = ArgValueCompleter::new(status_completer_delimited))]
pub status: Vec<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub struct LintArgs {
#[arg(add = ArgValueCompleter::new(issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, short = 't', add = ArgValueCompleter::new(issue_type_standard_completer))]
pub type_: Option<String>,
#[arg(long, short = 's', add = ArgValueCompleter::new(status_or_all_completer))]
pub status: Option<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub struct DeferArgs {
#[arg(add = ArgValueCompleter::new(open_issue_id_completer))]
pub ids: Vec<String>,
#[arg(long)]
pub until: Option<String>,
#[arg(long)]
pub robot: bool,
#[arg(long, value_name = "COMMENT", allow_hyphen_values = true)]
pub transition_comment: Option<String>,
#[arg(long, value_name = "NAME", env = "BR_AGENT_NAME")]
pub agent_name: Option<String>,
#[arg(long, value_name = "HARNESS", env = "BR_HARNESS")]
pub harness: Option<String>,
#[arg(long, value_name = "MODEL", env = "BR_MODEL")]
pub model: Option<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub struct UndeferArgs {
#[arg(add = ArgValueCompleter::new(open_issue_id_completer))]
pub ids: Vec<String>,
#[arg(long)]
pub robot: bool,
#[arg(long, value_name = "COMMENT", allow_hyphen_values = true)]
pub transition_comment: Option<String>,
#[arg(long, value_name = "NAME", env = "BR_AGENT_NAME")]
pub agent_name: Option<String>,
#[arg(long, value_name = "HARNESS", env = "BR_HARNESS")]
pub harness: Option<String>,
#[arg(long, value_name = "MODEL", env = "BR_MODEL")]
pub model: Option<String>,
}
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct ReadyArgs {
#[arg(long, default_value_t = 0)]
pub limit: usize,
#[arg(
long,
num_args = 0..=1,
default_missing_value = "",
conflicts_with = "unassigned",
add = ArgValueCompleter::new(assignee_completer)
)]
pub assignee: Option<String>,
#[arg(long, conflicts_with = "assignee")]
pub unassigned: bool,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Vec<String>,
#[arg(long, add = ArgValueCompleter::new(label_completer))]
pub label_any: Vec<String>,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Vec<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Vec<String>,
#[arg(long, default_value = "hybrid", value_enum)]
pub sort: SortPolicy,
#[arg(long)]
pub include_deferred: bool,
#[arg(long, add = ArgValueCompleter::new(issue_id_completer))]
pub parent: Option<String>,
#[arg(long, short = 'r')]
pub recursive: bool,
#[arg(long, conflicts_with = "parent", add = ArgValueCompleter::new(issue_id_completer))]
pub epic: Option<String>,
#[arg(long)]
pub wrap: bool,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct SchedulerArgs {
#[arg(long, default_value_t = 0)]
pub limit: usize,
#[arg(long, default_value_t = 512)]
pub candidate_limit: usize,
#[arg(long, default_value_t = 2)]
pub stale_claim_hours: i64,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
#[arg(long)]
pub robot: bool,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Args, Debug, Clone, Default)]
pub struct BlockedArgs {
#[arg(long, default_value_t = 50)]
pub limit: usize,
#[arg(long)]
pub detailed: bool,
#[arg(long)]
pub wrap: bool,
#[arg(long = "type", short = 't', add = ArgValueCompleter::new(issue_type_completer))]
pub type_: Vec<String>,
#[arg(long, short = 'p', add = ArgValueCompleter::new(priority_completer))]
pub priority: Vec<String>,
#[arg(long, short = 'l', add = ArgValueCompleter::new(label_completer))]
pub label: Vec<String>,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct CloseArgs {
#[arg(add = ArgValueCompleter::new(open_issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, short = 'r', allow_hyphen_values = true)]
pub reason: Option<String>,
#[arg(long, value_name = "COMMENT", allow_hyphen_values = true)]
pub transition_comment: Option<String>,
#[arg(long, short = 'f')]
pub force: bool,
#[arg(long)]
pub suggest_next: bool,
#[arg(long)]
pub session: Option<String>,
#[arg(long)]
pub robot: bool,
#[arg(long, value_name = "NAME", env = "BR_AGENT_NAME")]
pub agent_name: Option<String>,
#[arg(long, value_name = "HARNESS", env = "BR_HARNESS")]
pub harness: Option<String>,
#[arg(long, value_name = "MODEL", env = "BR_MODEL")]
pub model: Option<String>,
#[arg(long)]
pub bypass_policy: bool,
#[arg(long, value_name = "REASON")]
pub bypass_reason: Option<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub struct ReopenArgs {
#[arg(add = ArgValueCompleter::new(closed_issue_id_completer))]
pub ids: Vec<String>,
#[arg(long, short = 'r', allow_hyphen_values = true)]
pub reason: Option<String>,
#[arg(long)]
pub robot: bool,
#[arg(long, value_name = "NAME", env = "BR_AGENT_NAME")]
pub agent_name: Option<String>,
#[arg(long, value_name = "HARNESS", env = "BR_HARNESS")]
pub harness: Option<String>,
#[arg(long, value_name = "MODEL", env = "BR_MODEL")]
pub model: Option<String>,
}
#[derive(ValueEnum, Debug, Clone, Copy, Default, Eq, PartialEq)]
pub enum SortPolicy {
#[default]
Hybrid,
Priority,
Oldest,
}
pub const DEFAULT_WITNESS_PARALLELISM: usize = 64;
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct SyncArgs {
#[arg(long, group = "sync_action")]
pub flush_only: bool,
#[arg(long)]
pub import_only: bool,
#[arg(long, requires = "import_only")]
pub skip_invalid_records: bool,
#[arg(long)]
pub merge: bool,
#[arg(long)]
pub reconcile: bool,
#[arg(long, requires = "reconcile")]
pub dry_run: bool,
#[arg(long)]
pub status: bool,
#[arg(long)]
pub witness: bool,
#[arg(long = "reconcile-additive")]
pub reconcile_additive: bool,
#[arg(long = "migrate-source-repo-path")]
pub migrate_source_repo_path: bool,
#[arg(long)]
pub apply: bool,
#[arg(long = "expect-plan-sha256", value_name = "SHA256", requires = "apply")]
pub expect_plan_sha256: Option<String>,
#[arg(
long = "resolve-source-id",
value_name = "ISSUE_ID",
requires = "reconcile_additive"
)]
pub resolve_source_ids: Vec<String>,
#[arg(
long = "witness-chunk-lines",
default_value_t = 1024,
value_name = "LINES",
requires = "witness"
)]
pub witness_chunk_lines: usize,
#[arg(
long = "witness-parallelism",
value_name = "WORKERS",
requires = "witness"
)]
pub witness_parallelism: Option<usize>,
#[arg(long = "export-parallelism", value_name = "WORKERS")]
pub export_parallelism: Option<usize>,
#[arg(long, short = 'f')]
pub force: bool,
#[arg(long, requires = "merge", conflicts_with_all = ["force", "force_jsonl"])]
pub force_db: bool,
#[arg(long, requires = "merge", conflicts_with_all = ["force", "force_db"])]
pub force_jsonl: bool,
#[arg(long)]
pub allow_external_jsonl: bool,
#[arg(long)]
pub manifest: bool,
#[arg(long = "error-policy", add = ArgValueCompleter::new(export_error_policy_completer))]
pub error_policy: Option<String>,
#[arg(long, add = ArgValueCompleter::new(orphan_mode_completer))]
pub orphans: Option<String>,
#[arg(long)]
pub rename_prefix: bool,
#[arg(long)]
pub rebuild: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Subcommand, Debug, Clone)]
pub enum ConfigCommands {
List {
#[arg(long, conflicts_with = "user")]
project: bool,
#[arg(long, conflicts_with = "project")]
user: bool,
},
Get {
#[arg(add = ArgValueCompleter::new(config_key_completer))]
key: String,
},
Set {
#[arg(
num_args = 1..=2,
value_name = "KV",
add = ArgValueCompleter::new(config_key_assignment_completer)
)]
args: Vec<String>,
},
#[command(visible_alias = "unset")]
Delete {
#[arg(add = ArgValueCompleter::new(config_key_completer))]
key: String,
},
Edit,
Path,
}
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct StatsArgs {
#[arg(long)]
pub by_type: bool,
#[arg(long)]
pub by_priority: bool,
#[arg(long)]
pub by_assignee: bool,
#[arg(long)]
pub by_label: bool,
#[arg(long)]
pub activity: bool,
#[arg(long)]
pub no_activity: bool,
#[arg(long, default_value_t = 24)]
pub activity_hours: u32,
#[arg(long, value_enum)]
pub format: Option<OutputFormatBasic>,
#[arg(long)]
pub stats: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug)]
pub struct HistoryArgs {
#[command(subcommand)]
pub command: Option<HistoryCommands>,
}
#[derive(Subcommand, Debug)]
pub enum HistoryCommands {
List,
Diff {
file: String,
},
Restore {
file: String,
#[arg(long, short = 'f')]
force: bool,
},
Prune {
#[arg(long, default_value_t = 100)]
keep: usize,
#[arg(long)]
older_than: Option<u32>,
#[arg(long, value_name = "BYTES")]
max_bytes: Option<u64>,
},
}
#[derive(Args, Debug, Clone, Default)]
pub struct VersionArgs {
#[arg(long, short = 'c')]
pub check: bool,
#[arg(long, short = 's')]
pub short: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct DoctorArgs {
#[arg(long, visible_alias = "fix")]
pub repair: bool,
#[arg(long, conflicts_with = "repair")]
pub repair_indexes: bool,
#[arg(long)]
pub allow_repeated_repair: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long = "robot-triage")]
pub robot_triage: bool,
#[arg(long)]
pub quick: bool,
#[arg(long, value_delimiter = ',', num_args = 1..)]
pub only: Vec<String>,
#[arg(long, value_delimiter = ',', num_args = 1..)]
pub skip: Vec<String>,
#[arg(long = "unsafe-auto-fix")]
pub unsafe_auto_fix: bool,
#[command(subcommand)]
pub subcommand: Option<DoctorSubcommand>,
}
#[derive(Subcommand, Debug, Clone)]
pub enum DoctorSubcommand {
Capabilities(DoctorCapabilitiesArgs),
#[command(name = "robot-docs", alias = "robot_docs")]
RobotDocs(DoctorRobotDocsArgs),
Health(DoctorHealthArgs),
Ls(DoctorLsArgs),
Undo(DoctorUndoArgs),
#[command(name = "migrate-schema")]
MigrateSchema(DoctorMigrateSchemaArgs),
Explain(DoctorExplainArgs),
}
#[derive(Args, Debug, Clone, Default)]
pub struct DoctorCapabilitiesArgs {
#[arg(long, value_enum, default_value_t = OutputFormatBasic::Text)]
pub format: OutputFormatBasic,
#[arg(long)]
pub command: Option<String>,
}
#[derive(Args, Debug, Clone, Default)]
pub struct DoctorRobotDocsArgs {
#[arg(long, value_enum, default_value_t = OutputFormatBasic::Text)]
pub format: OutputFormatBasic,
}
#[derive(Args, Debug, Clone, Default)]
pub struct DoctorHealthArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct DoctorLsArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Args, Debug, Clone)]
pub struct DoctorUndoArgs {
pub run_id: String,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub json: bool,
}
#[derive(Args, Debug, Clone)]
pub struct DoctorMigrateSchemaArgs {
#[command(subcommand)]
pub command: DoctorMigrateSchemaCommand,
}
#[derive(Subcommand, Debug, Clone)]
pub enum DoctorMigrateSchemaCommand {
Plan(DoctorMigrateSchemaPlanArgs),
Apply(DoctorMigrateSchemaApplyArgs),
Undo(DoctorMigrateSchemaUndoArgs),
}
#[derive(Args, Debug, Clone, Default)]
pub struct DoctorMigrateSchemaPlanArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Args, Debug, Clone)]
pub struct DoctorMigrateSchemaApplyArgs {
#[arg(long)]
pub plan_token: String,
#[arg(long)]
pub json: bool,
}
#[derive(Args, Debug, Clone)]
pub struct DoctorMigrateSchemaUndoArgs {
pub run_id: String,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub json: bool,
}
#[derive(Args, Debug, Clone)]
pub struct DoctorExplainArgs {
pub finding_id: String,
#[arg(long)]
pub json: bool,
}
#[cfg(feature = "self_update")]
#[derive(Args, Debug, Clone, Default)]
pub struct UpgradeArgs {
#[arg(long)]
pub check: bool,
#[arg(long)]
pub force: bool,
#[arg(long)]
pub version: Option<String>,
#[arg(long)]
pub dry_run: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct OrphansArgs {
#[arg(long)]
pub details: bool,
#[arg(long)]
pub fix: bool,
#[arg(long)]
pub robot: bool,
}
#[derive(Args, Debug, Clone, Default)]
pub struct ChangelogArgs {
#[arg(long)]
pub since: Option<String>,
#[arg(long, conflicts_with = "since")]
pub since_tag: Option<String>,
#[arg(long, conflicts_with_all = ["since", "since_tag"])]
pub since_commit: Option<String>,
#[arg(long)]
pub robot: bool,
}
#[derive(Subcommand, Debug)]
pub enum QueryCommands {
Save(QuerySaveArgs),
Run(QueryRunArgs),
List,
Delete(QueryDeleteArgs),
}
#[derive(Args, Debug, Clone)]
pub struct QuerySaveArgs {
pub name: String,
#[arg(long, short = 'd')]
pub description: Option<String>,
#[command(flatten)]
pub filters: ListArgs,
}
#[derive(Args, Debug, Clone)]
pub struct QueryRunArgs {
#[arg(add = ArgValueCompleter::new(saved_query_completer))]
pub name: String,
#[command(flatten)]
pub filters: ListArgs,
}
#[derive(Args, Debug, Clone)]
pub struct QueryDeleteArgs {
#[arg(add = ArgValueCompleter::new(saved_query_completer))]
pub name: String,
}
#[derive(Args, Debug, Clone, Default)]
pub struct GraphArgs {
#[arg(add = ArgValueCompleter::new(open_issue_id_completer))]
pub issue: Option<String>,
#[arg(long)]
pub all: bool,
#[arg(long, conflicts_with = "all")]
pub dependencies: bool,
#[arg(long)]
pub compact: bool,
#[arg(long)]
pub dot: bool,
}
#[derive(Args, Debug, Clone, Default)]
#[allow(clippy::struct_excessive_bools)]
pub struct AgentsArgs {
#[arg(long, conflicts_with_all = ["remove", "update", "check"])]
pub add: bool,
#[arg(long, conflicts_with_all = ["add", "update", "check"])]
pub remove: bool,
#[arg(long, conflicts_with_all = ["add", "remove", "check"])]
pub update: bool,
#[arg(long, conflicts_with_all = ["add", "remove", "update"])]
pub check: bool,
#[arg(long)]
pub dry_run: bool,
#[arg(long, short = 'f')]
pub force: bool,
}
#[cfg(test)]
mod tests {
use super::{
Cli, Commands, DoctorMigrateSchemaArgs, DoctorMigrateSchemaCommand,
DoctorMigrateSchemaPlanArgs, DoctorMigrateSchemaUndoArgs, DoctorSubcommand,
InheritedOutputMode, OutputFormat, OutputFormatBasic, issue_type_completer,
issue_type_completer_delimited, resolve_output_format_basic_with_outer_mode,
resolve_output_format_with_outer_mode,
};
use crate::storage::sqlite::SqliteStorage;
use clap::{CommandFactory, Parser};
use clap_complete::engine::CompletionCandidate;
use std::ffi::OsStr;
use tempfile::TempDir;
const CLI_REFERENCE: &str = include_str!("../../docs/CLI_REFERENCE.md");
#[test]
fn test_list_limit_is_none_when_omitted() {
let cli = Cli::parse_from(["br", "list"]);
assert!(
matches!(&cli.command, Commands::List(_)),
"expected list command"
);
let Commands::List(args) = cli.command else {
return;
};
assert_eq!(args.limit, None);
}
#[test]
fn test_list_limit_zero_parses_as_unlimited() {
let cli = Cli::parse_from(["br", "list", "--limit", "0"]);
assert!(
matches!(&cli.command, Commands::List(_)),
"expected list command"
);
let Commands::List(args) = cli.command else {
return;
};
assert_eq!(args.limit, Some(0));
}
#[test]
fn test_ready_assignee_flag_accepts_missing_value() {
let cli = Cli::parse_from(["br", "ready", "--assignee"]);
assert!(
matches!(&cli.command, Commands::Ready(_)),
"expected ready command"
);
let Commands::Ready(args) = cli.command else {
return;
};
assert_eq!(args.assignee.as_deref(), Some(""));
}
#[test]
fn test_ready_assignee_conflicts_with_unassigned() {
let err = Cli::try_parse_from(["br", "ready", "--assignee", "alice", "--unassigned"])
.expect_err("ready filters should conflict");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn test_doctor_fix_alias_parses_as_repair() {
let cli = Cli::parse_from([
"br",
"doctor",
"--fix",
"--only",
"fm-state_files-merge-artifact-stuck",
]);
let Commands::Doctor(args) = cli.command else {
panic!("expected doctor command");
};
assert!(args.repair);
assert_eq!(args.only, vec!["fm-state_files-merge-artifact-stuck"]);
}
#[test]
fn test_doctor_fix_alias_conflicts_with_repair_indexes() {
let err = Cli::try_parse_from(["br", "doctor", "--fix", "--repair-indexes"])
.expect_err("--fix must share --repair's repair-index conflict");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn test_doctor_migrate_schema_lifecycle_parses() {
let plan = Cli::parse_from(["br", "doctor", "migrate-schema", "plan", "--json"]);
let Commands::Doctor(plan_args) = plan.command else {
panic!("expected doctor command");
};
assert!(matches!(
plan_args.subcommand,
Some(DoctorSubcommand::MigrateSchema(DoctorMigrateSchemaArgs {
command: DoctorMigrateSchemaCommand::Plan(DoctorMigrateSchemaPlanArgs {
json: true
})
}))
));
let apply = Cli::parse_from([
"br",
"doctor",
"migrate-schema",
"apply",
"--plan-token",
"receipt-token",
]);
let Commands::Doctor(apply_args) = apply.command else {
panic!("expected doctor command");
};
let Some(DoctorSubcommand::MigrateSchema(DoctorMigrateSchemaArgs {
command: DoctorMigrateSchemaCommand::Apply(apply),
})) = apply_args.subcommand
else {
panic!("expected migrate-schema apply");
};
assert_eq!(apply.plan_token, "receipt-token");
assert!(!apply.json);
let undo = Cli::parse_from([
"br",
"doctor",
"migrate-schema",
"undo",
"run-id",
"--dry-run",
]);
let Commands::Doctor(undo_args) = undo.command else {
panic!("expected doctor command");
};
assert!(matches!(
undo_args.subcommand,
Some(DoctorSubcommand::MigrateSchema(DoctorMigrateSchemaArgs {
command: DoctorMigrateSchemaCommand::Undo(DoctorMigrateSchemaUndoArgs {
ref run_id,
dry_run: true,
json: false,
})
})) if run_id == "run-id"
));
}
#[test]
fn test_create_positional_title_conflicts_with_title_flag() {
let err =
Cli::try_parse_from(["br", "create", "positional title", "--title", "flag title"])
.expect_err("create should reject ambiguous title sources");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn test_issue_type_delimited_completion_preserves_plain_candidate_order() {
let plain = candidate_values(issue_type_completer(OsStr::new("bu")));
let delimited = candidate_values(issue_type_completer_delimited(OsStr::new("task, bu")));
let expected = plain
.iter()
.map(|value| format!("task, {value}"))
.collect::<Vec<_>>();
assert_eq!(plain.first().map(String::as_str), Some("bug"));
assert_eq!(delimited, expected);
}
#[test]
fn test_agents_add_conflicts_with_check() {
let err = Cli::try_parse_from(["br", "agents", "--add", "--check"])
.expect_err("agents actions should conflict");
assert_eq!(err.kind(), clap::error::ErrorKind::ArgumentConflict);
}
#[test]
fn test_saved_queries_from_db_reads_saved_query_names_without_config_scan() {
let temp = TempDir::new().expect("tempdir");
let db_path = temp.path().join("beads.db");
let mut storage = SqliteStorage::open(&db_path).expect("open db");
storage
.set_config("saved_query:mine", r#"{"name":"mine"}"#)
.expect("save query");
storage
.set_config("saved_query:stale", r#"{"name":"stale"}"#)
.expect("save query");
storage
.set_config("ui.theme", "amber")
.expect("save regular config");
let saved_queries = super::saved_queries_from_db(&db_path);
assert_eq!(
saved_queries.into_iter().collect::<Vec<_>>(),
vec!["mine".to_string(), "stale".to_string()]
);
}
#[test]
fn test_resolve_output_format_with_outer_mode_inherits_toon() {
let resolved =
resolve_output_format_with_outer_mode(None, InheritedOutputMode::Toon, false);
assert_eq!(resolved, OutputFormat::Toon);
}
#[test]
fn test_resolve_output_format_with_outer_mode_keeps_quiet_over_env_defaults() {
let resolved =
resolve_output_format_with_outer_mode(None, InheritedOutputMode::Quiet, false);
assert_eq!(resolved, OutputFormat::Text);
}
#[test]
fn test_resolve_output_format_basic_with_outer_mode_honors_explicit_format() {
let resolved = resolve_output_format_basic_with_outer_mode(
Some(OutputFormatBasic::Json),
InheritedOutputMode::Toon,
false,
);
assert_eq!(resolved, OutputFormat::Json);
}
#[test]
fn test_cli_reference_documents_current_clap_surface() {
assert_all_top_level_commands_are_documented();
assert_doc_contains_all(CLAP_DRIFT_SENTINELS);
}
const CLAP_DRIFT_SENTINELS: &[&str] = &[
"--lock-timeout <LOCK_TIMEOUT>",
"`--from-file <PATH>` | Read IDs from file",
"`--cascade` | Delete dependents recursively",
"`--force` | Bypass dependent checks, orphaning dependents",
"`--hard` | Prune tombstones from JSONL immediately",
"br config <COMMAND>",
"`set <KEY=VALUE>` or `set <KEY> <VALUE>`",
"`delete <KEY>` | Delete a config value; `unset` is an alias",
"`save <NAME> [FILTERS...]`",
"no free-form query string argument",
"`--allow-external-jsonl` | Allow JSONL path outside `.beads/`",
"`--rename-prefix` | During import, rewrite mismatched issue-ID prefixes",
"`--rebuild` | During import, rebuild SQLite from JSONL",
"`--notes-contains <TEXT>` | Notes contains substring",
"`--format <FMT>` | Output format: text, json, csv, toon",
"`--days <N>` | Issues not updated in N days (default: 30)",
"`--reservations <PATH>` | Offline Agent Mail reservation snapshot",
"`--agents <PATH>` | Offline Agent Mail agent snapshot",
"br coordination status --reservations reservations.json --agents agents.jsonl --json",
"beads://coordination/status",
"`issue-with-counts`, `issue-details`",
];
fn assert_all_top_level_commands_are_documented() {
let command = Cli::command();
let missing = command
.get_subcommands()
.map(clap::Command::get_name)
.filter(|name| !is_generated_help_command(name))
.filter(|name| !top_level_command_is_documented(name))
.collect::<Vec<_>>();
assert!(
missing.is_empty(),
"docs/CLI_REFERENCE.md is missing top-level command headings: {missing:?}"
);
}
fn is_generated_help_command(name: &str) -> bool {
name == "help"
}
fn top_level_command_is_documented(name: &str) -> bool {
if name == "status" {
return CLI_REFERENCE.contains("### stats / status");
}
if name == "undefer" {
return CLI_REFERENCE.contains("### defer / undefer");
}
CLI_REFERENCE.contains(&format!("### {name}"))
}
fn assert_doc_contains_all(needles: &[&str]) {
for needle in needles {
assert!(
CLI_REFERENCE.contains(needle),
"docs/CLI_REFERENCE.md is missing Clap drift sentinel: {needle}"
);
}
}
fn candidate_values(candidates: Vec<CompletionCandidate>) -> Vec<String> {
candidates
.into_iter()
.map(|candidate| candidate.get_value().to_string_lossy().into_owned())
.collect()
}
}