mod adr;
mod backlog;
mod backlog_order;
mod boot;
mod catalog;
mod clock;
mod commands;
mod concept_map;
mod conduct;
mod contentset;
mod corpus;
mod coverage;
mod coverage_scan;
mod coverage_store;
mod coverage_verify;
mod coverage_view;
mod dep_seq;
mod dispatch;
mod dtoml;
mod entity;
mod fsutil;
mod git;
mod governance;
mod input;
mod install;
mod integrity;
mod knowledge;
mod ledger;
mod lexical;
mod lifecycle;
mod listing;
mod map_server;
mod memory;
mod meta;
mod plan;
mod policy;
mod priority;
mod projection;
mod rec;
mod reconcile;
mod registry;
mod relation;
mod relation_graph;
mod requirement;
mod retrieve;
mod review;
mod revision;
mod root;
mod skills;
mod slice;
mod spec;
mod standard;
mod state;
mod status;
mod tomlfmt;
mod tty;
mod verify;
mod worktree;
use std::path::PathBuf;
use std::str::FromStr;
use clap::{Args, Parser, Subcommand};
use crate::commands::map::MapServeArgs;
use crate::listing::{Format, ListArgs};
#[derive(Parser)]
#[command(name = "doctrine", about = "doctrine CLI")]
struct Cli {
#[arg(long, default_value = "auto", global = true)]
color: clap::ColorChoice,
#[command(subcommand)]
command: Command,
}
#[derive(Args, Debug)]
pub(crate) struct CommonListArgs {
#[arg(long, short = 'f')]
pub(crate) filter: Option<String>,
#[arg(long, short = 'r')]
pub(crate) regexp: Option<String>,
#[arg(long, short = 'i')]
pub(crate) case_insensitive: bool,
#[arg(long, short = 's', value_delimiter = ',')]
pub(crate) status: Vec<String>,
#[arg(long, short = 't')]
pub(crate) tag: Vec<String>,
#[arg(long, short = 'a')]
pub(crate) all: bool,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
pub(crate) format: Format,
#[arg(long)]
pub(crate) json: bool,
#[arg(long, value_delimiter = ',')]
pub(crate) columns: Option<Vec<String>>,
}
impl CommonListArgs {
pub(crate) fn into_list_args(self, color: bool) -> ListArgs {
ListArgs {
substr: self.filter,
regexp: self.regexp,
case_insensitive: self.case_insensitive,
status: self.status,
tags: self.tag,
all: self.all,
format: self.format,
json: self.json,
columns: self.columns,
render: crate::listing::RenderOpts {
color,
term_width: crate::tty::stdout_terminal_width(),
},
}
}
}
#[derive(Args, Debug)]
pub(crate) struct FindRetrieveArgs {
#[arg(long = "path-scope")]
pub(crate) path_scope: Vec<String>,
#[arg(long = "glob")]
pub(crate) glob: Vec<String>,
#[arg(long = "command")]
pub(crate) command: Vec<String>,
#[arg(long = "tag")]
pub(crate) tag: Vec<String>,
#[arg(long = "query")]
pub(crate) flag_query: Option<String>,
#[arg(long = "type", value_parser = memory::MemoryType::parse)]
pub(crate) memory_type: Option<memory::MemoryType>,
#[arg(long, value_parser = memory::Status::parse)]
pub(crate) status: Option<memory::Status>,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
pub(crate) format: Format,
#[arg(long)]
pub(crate) json: bool,
#[arg(long = "include-draft")]
pub(crate) include_draft: bool,
#[arg(long, default_value_t = 0)]
pub(crate) offset: usize,
#[arg(long, conflicts_with = "offset")]
pub(crate) page: Option<usize>,
#[arg(long)]
pub(crate) limit: Option<usize>,
#[arg(short = 'p', long)]
pub(crate) path: Option<PathBuf>,
}
#[derive(Subcommand)]
enum Command {
Install {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
#[arg(long)]
dry_run: bool,
#[arg(short = 'y', long)]
yes: bool,
},
Catalog {
#[command(subcommand)]
command: CatalogCommand,
},
Claude {
#[command(subcommand)]
command: ClaudeCommand,
},
#[command(hide = true)]
Skills {
#[command(subcommand)]
command: SkillsCommand,
},
Map {
#[command(subcommand)]
command: MapCommand,
},
ConceptMap {
#[command(subcommand)]
command: ConceptMapCommand,
},
Slice {
#[command(subcommand)]
command: SliceCommand,
},
Memory {
#[command(subcommand)]
command: MemoryCommand,
},
Review {
#[command(subcommand)]
command: ReviewCommand,
},
Rec {
#[command(subcommand)]
command: RecCommand,
},
Revision {
#[command(subcommand)]
command: RevisionCommand,
},
Reconcile {
req: String,
#[arg(long)]
slice: String,
#[arg(long = "move", value_parser = rec::RecMove::parse)]
r#move: rec::RecMove,
#[arg(long, value_enum)]
to: Option<requirement::ReqStatus>,
#[arg(long)]
note: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Coverage {
#[command(subcommand)]
command: CoverageCommand,
},
Inspect {
id: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Survey {
#[arg(long)]
all: bool,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Next {
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Blockers {
id: String,
#[arg(long)]
transitive: bool,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Explain {
id: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Adr {
#[command(subcommand)]
command: AdrCommand,
},
Policy {
#[command(subcommand)]
command: PolicyCommand,
},
Standard {
#[command(subcommand)]
command: StandardCommand,
},
Spec {
#[command(subcommand)]
command: SpecCommand,
},
Backlog {
#[command(subcommand)]
command: BacklogCommand,
},
Knowledge {
#[command(subcommand)]
command: KnowledgeCommand,
},
Boot {
#[command(subcommand)]
command: Option<BootCommand>,
#[arg(long)]
check: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Worktree {
#[command(subcommand)]
command: WorktreeCommand,
},
Dispatch {
#[command(subcommand)]
command: DispatchCommand,
},
Validate {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Reseat {
reference: String,
#[arg(long)]
to: Option<u32>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Link {
source: String,
label: String,
target: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Unlink {
source: String,
label: String,
target: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Needs {
source: String,
target: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
After {
source: String,
target: String,
#[arg(long, default_value_t = 0)]
rank: i32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Supersede {
new: String,
old: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum CatalogCommand {
Scan {
#[arg(long)]
root: Option<PathBuf>,
},
Graph {
#[arg(long)]
root: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum WorktreeCommand {
Provision {
fork: PathBuf,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
CheckAllowlist {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
BranchPointCheck {
#[arg(long)]
base: String,
#[arg(long)]
head: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Fork {
#[arg(long)]
base: String,
#[arg(long)]
branch: String,
#[arg(long)]
dir: PathBuf,
#[arg(long)]
worker: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Coordinate {
#[arg(long)]
slice: u32,
#[arg(long)]
dir: PathBuf,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Import {
#[arg(long)]
base: String,
#[arg(long)]
fork: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Land {
#[arg(long)]
fork: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Gc {
#[arg(long)]
fork: String,
#[arg(long)]
superseded_head: Option<String>,
#[arg(long)]
force: bool,
#[arg(long)]
dry_run: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
#[arg(long)]
assert: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
VerifyWorker {
#[arg(long)]
base: String,
#[arg(long)]
dir: PathBuf,
},
Marker {
#[arg(long)]
clear: bool,
#[arg(long)]
operator: bool,
#[arg(long)]
stamp_subagent: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum DispatchCommand {
Sync {
#[arg(long)]
slice: u32,
#[arg(long, group = "stage", required = true)]
prepare_review: bool,
#[arg(long, group = "stage", required = true)]
integrate: bool,
#[arg(long, requires = "integrate")]
trunk: Option<String>,
#[arg(long, requires = "integrate")]
edge: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
RecordBoundary {
#[arg(long)]
slice: u32,
#[arg(long)]
phase: String,
#[arg(long)]
code_start: String,
#[arg(long)]
code_end: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Candidate {
#[command(subcommand)]
command: CandidateCommand,
},
}
#[derive(Subcommand)]
enum CandidateCommand {
Create {
#[arg(long)]
slice: u32,
#[arg(long, visible_alias = "target")]
label: String,
#[arg(long, default_value = "audit")]
kind: String,
#[arg(long)]
role: String,
#[arg(long)]
payload: String,
#[arg(long)]
base: String,
#[arg(long)]
source: Option<String>,
#[arg(long)]
supersedes: Option<String>,
#[arg(long)]
worktree: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
#[arg(long)]
slice: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Admit {
#[arg(long)]
slice: u32,
#[arg(long)]
role: String,
#[arg(long)]
candidate: String,
#[arg(long)]
review: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum BootCommand {
Install {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
#[arg(long = "agent")]
agent: Vec<String>,
#[arg(long)]
dry_run: bool,
#[arg(short = 'y', long)]
yes: bool,
},
}
#[derive(Subcommand)]
enum AdrCommand {
New {
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
id: u32,
#[arg(long)]
status: adr::AdrStatus,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum PolicyCommand {
New {
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
id: u32,
#[arg(long)]
status: policy::PolicyStatus,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum StandardCommand {
New {
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
id: u32,
#[arg(long)]
status: standard::StandardStatus,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum SpecCommand {
New {
subtype: spec::SpecSubtype,
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
spec_ref: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Validate {
spec_ref: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Req {
#[command(subcommand)]
command: SpecReqCommand,
},
}
#[derive(Subcommand)]
enum SpecReqCommand {
Add {
spec_ref: String,
title: Option<String>,
#[arg(long)]
kind: requirement::ReqKind,
#[arg(long)]
label: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
req_ref: String,
#[arg(long)]
to: requirement::ReqStatus,
#[arg(long)]
note: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
spec_ref: String,
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum BacklogCommand {
New {
kind: backlog::ItemKind,
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[arg(long)]
kind: Option<backlog::ItemKind>,
#[arg(long = "by", value_enum, default_value_t = backlog::OrderBy::Sequence)]
by: backlog::OrderBy,
#[command(flatten)]
list: CommonListArgs,
substr: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
id: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Edit {
id: String,
#[arg(long)]
status: backlog::Status,
#[arg(long)]
resolution: Option<backlog::Resolution>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Needs {
id: String,
#[arg(required = true)]
prereqs: Vec<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
After {
id: String,
to: String,
#[arg(long, default_value_t = 0)]
rank: i32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Tag {
id: String,
tags: Vec<String>,
#[arg(long = "remove", short = 'd')]
remove: Vec<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum KnowledgeCommand {
New {
kind: knowledge::RecordKind,
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
id: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
id: String,
state: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum CoverageCommand {
Show {
reference: String,
#[arg(long, value_delimiter = ',')]
columns: Option<Vec<String>>,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Record {
#[arg(long)]
slice: String,
#[arg(long)]
requirement: String,
#[arg(long)]
change: String,
#[arg(long)]
mode: String,
#[arg(long, value_parser = coverage_store::parse_status)]
status: Option<requirement::CoverageStatus>,
#[arg(long)]
alias: Option<String>,
#[arg(long = "command")]
command: Vec<String>,
#[arg(long = "extra-args")]
extra_args: Vec<String>,
#[arg(long = "matcher-source")]
matcher_source: Option<String>,
#[arg(long = "matcher-pattern")]
matcher_pattern: Option<String>,
#[arg(long)]
regex: bool,
#[arg(long = "attested-date")]
attested_date: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Verify {
slice: Option<String>,
#[arg(long)]
all: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Forget {
#[arg(long)]
slice: String,
#[arg(long)]
requirement: String,
#[arg(long)]
change: String,
#[arg(long)]
mode: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum MemoryCommand {
#[command(visible_alias = "new")]
Record {
title: String,
#[arg(long = "type", value_parser = memory::MemoryType::parse)]
memory_type: memory::MemoryType,
#[arg(long)]
key: Option<String>,
#[arg(long, default_value = "active", value_parser = memory::Status::parse)]
status: memory::Status,
#[arg(long)]
summary: Option<String>,
#[arg(long = "tag")]
tag: Vec<String>,
#[arg(long = "path-scope")]
path_scope: Vec<String>,
#[arg(long = "glob")]
glob: Vec<String>,
#[arg(long = "command")]
command: Vec<String>,
#[arg(long = "repo")]
repo: Option<String>,
#[arg(long = "global")]
global: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Verify {
reference: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[arg(long = "type", value_parser = memory::MemoryType::parse)]
memory_type: Option<memory::MemoryType>,
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Find {
query: Option<String>,
#[command(flatten)]
args: FindRetrieveArgs,
},
Retrieve {
#[command(flatten)]
args: FindRetrieveArgs,
#[arg(long = "min-trust", value_parser = retrieve::parse_min_trust)]
min_trust: Option<String>,
},
Sync {
#[command(subcommand)]
command: Option<SyncCommand>,
#[arg(long)]
dry_run: bool,
#[arg(short = 'y', long)]
yes: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum SyncCommand {
Install {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
#[arg(long)]
dry_run: bool,
#[arg(short = 'y', long)]
yes: bool,
},
}
#[derive(Subcommand)]
enum SliceCommand {
New {
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Design {
id: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Plan {
id: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Phases {
id: u32,
#[arg(long)]
prune: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Notes {
id: u32,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Phase {
id: u32,
phase_id: String,
#[arg(long)]
status: state::PhaseStatus,
#[arg(long)]
note: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
id: u32,
state: slice::SliceStatus,
#[arg(long)]
note: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum ConceptMapCommand {
New {
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
edges: bool,
#[arg(long)]
nodes: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Check {
id: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Add {
id: String,
source: String,
rel: String,
target: String,
#[arg(long)]
force: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Remove {
id: String,
source: String,
rel: String,
target: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
RenameNode {
id: String,
old: String,
new: String,
#[arg(long)]
dry_run: bool,
#[arg(long)]
case_sensitive: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Export {
id: String,
#[arg(long, value_enum)]
format: concept_map::ExportFormat,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum ReviewCommand {
New {
#[arg(long, value_parser = review::Facet::parse)]
facet: review::Facet,
#[arg(long)]
target: String,
#[arg(long)]
phase: Option<String>,
#[arg(long)]
title: Option<String>,
#[arg(long)]
raiser: Option<String>,
#[arg(long)]
responder: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Raise {
reference: String,
#[arg(long, value_parser = review::Severity::parse)]
severity: review::Severity,
#[arg(long)]
title: String,
#[arg(long)]
detail: String,
#[arg(long = "as")]
role: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Dispose {
reference: String,
#[arg(long)]
finding: String,
#[arg(long)]
disposition: String,
#[arg(long)]
response: String,
#[arg(long = "as")]
role: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Verify {
reference: String,
#[arg(long)]
finding: String,
#[arg(long)]
note: Option<String>,
#[arg(long = "as")]
role: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Contest {
reference: String,
#[arg(long)]
finding: String,
#[arg(long)]
note: Option<String>,
#[arg(long = "as")]
role: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Withdraw {
reference: String,
#[arg(long)]
finding: String,
#[arg(long = "as")]
role: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
reference: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Prime {
reference: String,
#[arg(long)]
seed: bool,
#[arg(long)]
from: Option<PathBuf>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Unlock {
reference: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum RecCommand {
New {
#[arg(long = "move", value_parser = rec::RecMove::parse)]
r#move: rec::RecMove,
#[arg(long)]
owning_slice: Option<String>,
#[arg(long = "decision")]
decision_ref: Option<String>,
#[arg(long)]
title: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
List {
#[command(flatten)]
list: CommonListArgs,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum RevisionCommand {
New {
title: Option<String>,
#[arg(long)]
slug: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Show {
reference: String,
#[arg(long, value_parser = Format::from_str, default_value_t = Format::Table)]
format: Format,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Status {
reference: String,
state: revision::RevStatus,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Change {
#[command(subcommand)]
command: RevisionChangeCommand,
},
Approve {
reference: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Apply {
reference: String,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum RevisionChangeCommand {
Add {
reference: String,
#[arg(long)]
action: revision::ChangeAction,
#[arg(long)]
target: Option<String>,
#[arg(long = "to-status")]
to_status: Option<String>,
#[arg(long = "new-label")]
new_label: Option<String>,
#[arg(long = "member-of")]
member_of: Option<String>,
#[arg(long = "new-statement")]
new_statement: Option<String>,
#[arg(long)]
primary: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
#[derive(Subcommand)]
enum MapCommand {
Serve(MapServeArgs),
}
#[derive(Subcommand)]
enum SkillsCommand {
List {
#[arg(short = 'a', long)]
agent: Option<String>,
#[arg(long)]
installed: bool,
},
Install {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
#[arg(short = 'a', long)]
agent: Vec<String>,
#[arg(short = 's', long)]
skill: Vec<String>,
#[arg(short = 'd', long)]
domain: Vec<String>,
#[arg(long, conflicts_with_all = ["skill", "domain"])]
only_memory: bool,
#[arg(short = 'g', long)]
global: bool,
#[arg(long)]
dry_run: bool,
#[arg(short = 'y', long)]
yes: bool,
},
}
#[derive(Subcommand)]
enum ClaudeCommand {
Install {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
#[arg(short = 'a', long)]
agent: Vec<String>,
#[arg(short = 's', long)]
skill: Vec<String>,
#[arg(short = 'd', long)]
domain: Vec<String>,
#[arg(long, conflicts_with_all = ["skill", "domain"])]
only_memory: bool,
#[arg(short = 'g', long)]
global: bool,
#[arg(long)]
dry_run: bool,
#[arg(short = 'y', long)]
yes: bool,
},
}
enum WriteClass {
Read,
Write(&'static str),
Orchestrator(&'static str),
MarkerClear,
Hookmint(&'static str),
}
fn write_class(cmd: &Command) -> WriteClass {
use WriteClass::{Hookmint, MarkerClear, Orchestrator, Read, Write};
match cmd {
Command::Install { .. } => Write("install"),
Command::Claude { command } => match command {
ClaudeCommand::Install { .. } => Write("claude install"),
},
Command::Skills { command } => match command {
SkillsCommand::List { .. } => Read,
SkillsCommand::Install { .. } => Write("claude install"),
},
Command::Map { .. } => Write("map"),
Command::ConceptMap { command } => match command {
ConceptMapCommand::New { .. } => Write("concept-map new"),
ConceptMapCommand::Add { .. } => Write("concept-map add"),
ConceptMapCommand::Remove { .. } => Write("concept-map remove"),
ConceptMapCommand::RenameNode { .. } => Write("concept-map rename-node"),
ConceptMapCommand::List { .. }
| ConceptMapCommand::Show { .. }
| ConceptMapCommand::Check { .. }
| ConceptMapCommand::Export { .. } => Read,
},
Command::Slice { command } => match command {
SliceCommand::New { .. } => Write("slice new"),
SliceCommand::Design { .. } => Write("slice design"),
SliceCommand::Plan { .. } => Write("slice plan"),
SliceCommand::Phases { .. } => Write("slice phases"),
SliceCommand::Notes { .. } => Write("slice notes"),
SliceCommand::Phase { .. } => Write("slice phase"),
SliceCommand::Status { .. } => Write("slice status"),
SliceCommand::List { .. } | SliceCommand::Show { .. } => Read,
},
Command::Memory { command } => match command {
MemoryCommand::Record { .. } => Write("memory record"),
MemoryCommand::Verify { .. } => Write("memory verify"),
MemoryCommand::Sync { command, .. } => match command {
None => Write("memory sync"),
Some(SyncCommand::Install { .. }) => Write("memory sync install"),
},
MemoryCommand::Show { .. }
| MemoryCommand::List { .. }
| MemoryCommand::Find { .. }
| MemoryCommand::Retrieve { .. } => Read,
},
Command::Review { command } => match command {
ReviewCommand::New { .. } => Write("review new"),
ReviewCommand::Raise { .. } => Write("review raise"),
ReviewCommand::Dispose { .. } => Write("review dispose"),
ReviewCommand::Verify { .. } => Write("review verify"),
ReviewCommand::Contest { .. } => Write("review contest"),
ReviewCommand::Withdraw { .. } => Write("review withdraw"),
ReviewCommand::Unlock { .. } => Write("review unlock"),
ReviewCommand::List { .. }
| ReviewCommand::Show { .. }
| ReviewCommand::Status { .. }
| ReviewCommand::Prime { .. } => Read,
},
Command::Rec { command } => match command {
RecCommand::New { .. } => Write("rec new"),
RecCommand::List { .. } | RecCommand::Show { .. } => Read,
},
Command::Revision { command } => match command {
RevisionCommand::New { .. } => Write("revision new"),
RevisionCommand::Status { .. } => Write("revision status"),
RevisionCommand::Show { .. } => Read,
RevisionCommand::Change { command } => match command {
RevisionChangeCommand::Add { .. } => Write("revision change add"),
},
RevisionCommand::Approve { .. } => Write("revision approve"),
RevisionCommand::Apply { .. } => Write("revision apply"),
},
Command::Reconcile { .. } => Write("reconcile"),
Command::Adr { command } => match command {
AdrCommand::New { .. } => Write("adr new"),
AdrCommand::Status { .. } => Write("adr status"),
AdrCommand::List { .. } | AdrCommand::Show { .. } => Read,
},
Command::Policy { command } => match command {
PolicyCommand::New { .. } => Write("policy new"),
PolicyCommand::Status { .. } => Write("policy status"),
PolicyCommand::List { .. } | PolicyCommand::Show { .. } => Read,
},
Command::Standard { command } => match command {
StandardCommand::New { .. } => Write("standard new"),
StandardCommand::Status { .. } => Write("standard status"),
StandardCommand::List { .. } | StandardCommand::Show { .. } => Read,
},
Command::Spec { command } => match command {
SpecCommand::New { .. } => Write("spec new"),
SpecCommand::Req { command } => match command {
SpecReqCommand::Add { .. } => Write("spec req add"),
SpecReqCommand::Status { .. } => Write("spec req status"),
SpecReqCommand::List { .. } => Read,
},
SpecCommand::List { .. } | SpecCommand::Show { .. } | SpecCommand::Validate { .. } => {
Read
}
},
Command::Backlog { command } => match command {
BacklogCommand::New { .. } => Write("backlog new"),
BacklogCommand::Edit { .. } => Write("backlog edit"),
BacklogCommand::Needs { .. } => Write("backlog needs"),
BacklogCommand::After { .. } => Write("backlog after"),
BacklogCommand::Tag { .. } => Write("backlog tag"),
BacklogCommand::List { .. } | BacklogCommand::Show { .. } => Read,
},
Command::Knowledge { command } => match command {
KnowledgeCommand::New { .. } => Write("knowledge new"),
KnowledgeCommand::Status { .. } => Write("knowledge status"),
KnowledgeCommand::List { .. } | KnowledgeCommand::Show { .. } => Read,
},
Command::Boot { command, .. } => match command {
None => Write("boot"),
Some(BootCommand::Install { .. }) => Write("boot install"),
},
Command::Worktree { command } => match command {
WorktreeCommand::Provision { .. }
| WorktreeCommand::CheckAllowlist { .. }
| WorktreeCommand::BranchPointCheck { .. }
| WorktreeCommand::VerifyWorker { .. }
| WorktreeCommand::Status { .. } => Read,
WorktreeCommand::Fork { .. } => Orchestrator("fork"),
WorktreeCommand::Coordinate { .. } => Orchestrator("coordinate"),
WorktreeCommand::Import { .. } => Orchestrator("import"),
WorktreeCommand::Land { .. } => Orchestrator("land"),
WorktreeCommand::Gc { .. } => Orchestrator("gc"),
WorktreeCommand::Marker {
stamp_subagent: true,
..
} => Hookmint("marker --stamp-subagent"),
WorktreeCommand::Marker { .. } => MarkerClear,
},
Command::Dispatch { command } => match command {
DispatchCommand::Sync { .. } => Orchestrator("dispatch-sync"),
DispatchCommand::RecordBoundary { .. } => Orchestrator("dispatch-record-boundary"),
DispatchCommand::Candidate { command } => match command {
CandidateCommand::Create { .. } => Orchestrator("dispatch-candidate-create"),
CandidateCommand::Status { .. } => Read,
CandidateCommand::Admit { .. } => Orchestrator("dispatch-candidate-admit"),
},
},
Command::Coverage { command } => match command {
CoverageCommand::Show { .. } => Read,
CoverageCommand::Record { .. } => Write("coverage record"),
CoverageCommand::Verify { .. } => Write("coverage verify"),
CoverageCommand::Forget { .. } => Write("coverage forget"),
},
Command::Catalog { .. }
| Command::Validate { .. }
| Command::Inspect { .. }
| Command::Survey { .. }
| Command::Next { .. }
| Command::Blockers { .. }
| Command::Explain { .. }
| Command::Status { .. } => Read,
Command::Reseat { .. } => Write("reseat"),
Command::Link { .. } => Write("link"),
Command::Unlink { .. } => Write("unlink"),
Command::Needs { .. } => Write("needs"),
Command::After { .. } => Write("after"),
Command::Supersede { .. } => Write("supersede"),
}
}
fn run_catalog_scan(root_arg: Option<PathBuf>) -> anyhow::Result<()> {
use std::io::Write;
let root = crate::root::find(root_arg, &crate::root::default_markers())?;
if !root.join(".doctrine").is_dir() {
anyhow::bail!("no .doctrine directory found at '{}'", root.display());
}
let catalog = crate::catalog::hydrate::scan_catalog(&root)?;
let json = serde_json::to_string_pretty(&catalog)
.map_err(|e| anyhow::anyhow!("failed to serialize catalog: {e}"))?;
write!(std::io::stdout(), "{json}")?;
Ok(())
}
fn run_catalog_graph(root_arg: Option<PathBuf>) -> anyhow::Result<()> {
use std::io::Write;
let root = crate::root::find(root_arg, &crate::root::default_markers())?;
if !root.join(".doctrine").is_dir() {
anyhow::bail!("no .doctrine directory found at '{}'", root.display());
}
let catalog = crate::catalog::hydrate::scan_catalog(&root)?;
let graph = crate::catalog::graph::CatalogGraph::from_catalog(&catalog);
let json = serde_json::to_string_pretty(&graph)
.map_err(|e| anyhow::anyhow!("failed to serialize graph: {e}"))?;
write!(std::io::stdout(), "{json}")?;
Ok(())
}
fn run_inspect(path: Option<PathBuf>, id: &str, format: Format, json: bool) -> anyhow::Result<()> {
use std::io::Write;
let root = crate::root::find(path, &crate::root::default_markers())?;
let resolved = if json { Format::Json } else { format };
let scanned = relation_graph::scan_entities(&root)?;
let out = match resolved {
Format::Table => {
let relation = relation_graph::render_from(&scanned, &root, id, Format::Table)?;
let block = priority::surface::actionability_block_from(&scanned, &root, id)?;
let block = priority::render::actionability_block_human(&block);
format!("{relation}{block}")
}
Format::Json => {
let view = relation_graph::inspect_from(&scanned, &root, id)?;
let block = priority::surface::actionability_block_from(&scanned, &root, id)?;
let mut value = relation_graph::inspect_value(&view);
if let Some(obj) = value.as_object_mut() {
obj.insert(
"actionability".to_string(),
priority::render::actionability_block_value(&block),
);
}
serde_json::to_string_pretty(&value)
.map_err(|e| anyhow::anyhow!("failed to serialize inspect JSON: {e}"))?
}
};
write!(std::io::stdout(), "{out}")?;
Ok(())
}
fn worker_guard(cmd: &Command) -> anyhow::Result<()> {
let verb = match write_class(cmd) {
WriteClass::Write(verb) | WriteClass::Orchestrator(verb) | WriteClass::Hookmint(verb) => {
verb
}
WriteClass::Read | WriteClass::MarkerClear => return Ok(()),
};
let Ok(root) = root::find(None, &root::default_markers()) else {
if worktree::env_worker_set() {
anyhow::bail!("{}: refusing authored write `{verb}`", worktree::DUAL_CAUSE);
}
return Ok(());
};
let mode = worktree::resolve_mode(&root);
if !mode.refused {
return Ok(());
}
if mode.is_env_on_nonlinked() {
anyhow::bail!("{}: refusing authored write `{verb}`", worktree::DUAL_CAUSE);
}
anyhow::bail!(
"worker fork (signal: {}): refusing authored write `{verb}` — workers return a source delta; doctrine-mediated writes funnel through the orchestrator.",
mode.cause_token()
);
}
fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let color = crate::tty::resolve_color(cli.color);
worker_guard(&cli.command)?;
match cli.command {
Command::Install { path, dry_run, yes } => install::run(path, dry_run, yes),
Command::Claude { command } => match command {
ClaudeCommand::Install {
path,
agent,
skill,
domain,
only_memory,
global,
dry_run,
yes,
} => skills::run_install(
path,
&skills::InstallArgs {
agents: &agent,
skills: &skill,
domains: &domain,
only_memory,
global,
dry_run,
yes,
},
),
},
Command::Skills { command } => match command {
SkillsCommand::List { agent, installed } => {
skills::run_list(agent.as_deref(), installed)
}
SkillsCommand::Install {
path,
agent,
skill,
domain,
only_memory,
global,
dry_run,
yes,
} => skills::run_install(
path,
&skills::InstallArgs {
agents: &agent,
skills: &skill,
domains: &domain,
only_memory,
global,
dry_run,
yes,
},
),
},
Command::ConceptMap { command } => match command {
ConceptMapCommand::New { title, slug, path } => concept_map::run_new(path, title, slug),
ConceptMapCommand::List { list, path } => {
concept_map::run_list(path, list.into_list_args(color))
}
ConceptMapCommand::Show {
reference,
format,
edges,
nodes,
path,
} => concept_map::run_show(path, &reference, format, edges, nodes),
ConceptMapCommand::Check { id, path } => concept_map::run_check(path, &id),
ConceptMapCommand::Add {
id,
source,
rel,
target,
force,
path,
} => concept_map::run_add(path, &id, &source, &rel, &target, force),
ConceptMapCommand::Remove {
id,
source,
rel,
target,
path,
} => concept_map::run_remove(path, &id, &source, &rel, &target),
ConceptMapCommand::RenameNode {
id,
old,
new,
dry_run,
case_sensitive,
path,
} => concept_map::run_rename_node(path, &id, &old, &new, dry_run, case_sensitive),
ConceptMapCommand::Export { id, format, path } => {
concept_map::run_export(path, &id, &format)
}
},
Command::Slice { command } => match command {
SliceCommand::New { title, slug, path } => slice::run_new(path, title, slug),
SliceCommand::Design { id, path } => slice::run_design(path, id),
SliceCommand::Plan { id, path } => slice::run_plan(path, id),
SliceCommand::Phases { id, prune, path } => slice::run_phases(path, id, prune),
SliceCommand::Notes { id, path } => slice::run_notes(path, id),
SliceCommand::Phase {
id,
phase_id,
status,
note,
path,
} => slice::run_phase(path, id, &phase_id, status, note.as_deref()),
SliceCommand::Status {
id,
state,
note,
path,
} => slice::run_status(path, id, state, note.as_deref()),
SliceCommand::List { list, path } => slice::run_list(path, list.into_list_args(color)),
SliceCommand::Show {
reference,
format,
json,
path,
} => slice::run_show(path, &reference, if json { Format::Json } else { format }),
},
Command::Memory { command } => match command {
MemoryCommand::Record {
title,
memory_type,
key,
status,
summary,
tag,
path_scope,
glob,
command,
repo,
global,
path,
} => memory::run_record(
path,
&memory::RecordArgs {
title: &title,
memory_type,
key: key.as_deref(),
status,
summary: summary.as_deref(),
tags: &tag,
paths: &path_scope,
globs: &glob,
commands: &command,
repo: repo.as_deref(),
global,
},
),
MemoryCommand::Show {
reference,
format,
json,
path,
} => memory::run_show(path, &reference, if json { Format::Json } else { format }),
MemoryCommand::Verify { reference, path } => memory::run_verify(path, &reference),
MemoryCommand::List {
memory_type,
list,
path,
} => memory::run_list(path, memory_type, list.into_list_args(color)),
MemoryCommand::Find { query, args } => {
let free_query = match (query, args.flag_query) {
(Some(_), Some(_)) => {
anyhow::bail!("cannot specify both a positional query and --query")
}
(q, None) | (None, q) => q,
};
if args.limit == Some(0) {
anyhow::bail!("--limit must be >= 1");
}
let page_size = args.limit.unwrap_or(retrieve::RETRIEVE_LIMIT_DEFAULT);
let offset = match args.page {
Some(0) => anyhow::bail!("--page must be >= 1"),
Some(p) => (p - 1) * page_size,
None => args.offset,
};
let resolved_format = if args.json { Format::Json } else { args.format };
retrieve::run_find(
args.path,
args.path_scope,
args.glob,
args.command,
args.tag,
free_query,
args.memory_type,
args.status,
args.include_draft,
resolved_format,
offset,
args.limit,
)
}
MemoryCommand::Retrieve { args, min_trust } => {
if args.limit == Some(0) {
anyhow::bail!("--limit must be >= 1");
}
let retrieve_limit = args
.limit
.unwrap_or(retrieve::RETRIEVE_LIMIT_DEFAULT)
.min(retrieve::RETRIEVE_LIMIT_MAX);
let page_size = args.limit.unwrap_or(retrieve::RETRIEVE_LIMIT_DEFAULT);
let offset = match args.page {
Some(0) => anyhow::bail!("--page must be >= 1"),
Some(p) => (p - 1) * page_size,
None => args.offset,
};
let resolved_format = if args.json { Format::Json } else { args.format };
retrieve::run_retrieve(
args.path,
args.path_scope,
args.glob,
args.command,
args.tag,
args.flag_query,
args.memory_type,
args.status,
args.include_draft,
retrieve_limit,
min_trust.as_deref(),
offset,
resolved_format,
)
}
MemoryCommand::Sync {
command,
dry_run: sync_dry_run,
yes: sync_yes,
path: sync_path,
} => match command {
None => corpus::run_sync(sync_path, sync_dry_run, sync_yes),
Some(SyncCommand::Install { path, dry_run, yes }) => {
corpus::run_sync_install(path, dry_run, yes)
}
},
},
Command::Review { command } => match command {
ReviewCommand::New {
facet,
target,
phase,
title,
raiser,
responder,
path,
} => review::run_new(
path,
&review::NewArgs {
facet,
target,
phase,
title,
raiser,
responder,
},
),
ReviewCommand::List { list, path } => {
review::run_list(path, list.into_list_args(color))
}
ReviewCommand::Show {
reference,
format,
json,
path,
} => review::run_show(path, &reference, if json { Format::Json } else { format }),
ReviewCommand::Raise {
reference,
severity,
title,
detail,
role,
path,
} => {
let role = review::parse_role(role.as_deref(), review::Role::Raiser)?;
review::run_raise(
path,
&review::RaiseArgs {
reference,
severity,
title,
detail,
},
role,
)
}
ReviewCommand::Dispose {
reference,
finding,
disposition,
response,
role,
path,
} => {
let role = review::parse_role(role.as_deref(), review::Role::Responder)?;
review::run_dispose(
path,
&review::DisposeArgs {
reference,
finding,
disposition,
response,
},
role,
)
}
ReviewCommand::Verify {
reference,
finding,
note,
role,
path,
} => {
let role = review::parse_role(role.as_deref(), review::Role::Raiser)?;
review::run_verify(path, &reference, &finding, note.as_deref(), role)
}
ReviewCommand::Contest {
reference,
finding,
note,
role,
path,
} => {
let role = review::parse_role(role.as_deref(), review::Role::Raiser)?;
review::run_contest(path, &reference, &finding, note.as_deref(), role)
}
ReviewCommand::Withdraw {
reference,
finding,
role,
path,
} => {
let role = review::parse_role(role.as_deref(), review::Role::Raiser)?;
review::run_withdraw(path, &reference, &finding, role)
}
ReviewCommand::Status { reference, path } => review::run_status(path, &reference),
ReviewCommand::Prime {
reference,
seed,
from,
path,
} => review::run_prime(
path,
&review::PrimeArgs {
reference,
seed,
from,
},
),
ReviewCommand::Unlock { reference, path } => review::run_unlock(path, &reference),
},
Command::Rec { command } => match command {
RecCommand::New {
r#move,
owning_slice,
decision_ref,
title,
path,
} => rec::run_new(
path,
&rec::NewArgs {
r#move,
owning_slice,
decision_ref,
title,
},
),
RecCommand::List { list, path } => rec::run_list(path, list.into_list_args(color)),
RecCommand::Show {
reference,
format,
json,
path,
} => rec::run_show(path, &reference, if json { Format::Json } else { format }),
},
Command::Revision { command } => match command {
RevisionCommand::New { title, slug, path } => revision::run_new(path, title, slug),
RevisionCommand::Show {
reference,
format,
json,
path,
} => revision::run_show(path, &reference, if json { Format::Json } else { format }),
RevisionCommand::Status {
reference,
state,
path,
} => revision::run_status(path, &reference, state, color),
RevisionCommand::Change { command } => match command {
RevisionChangeCommand::Add {
reference,
action,
target,
to_status,
new_label,
member_of,
new_statement,
primary,
path,
} => revision::run_change_add(
path,
&reference,
&revision::ChangeAddArgs {
action,
target,
to_status,
new_label,
member_of,
new_statement,
primary,
},
),
},
RevisionCommand::Approve { reference, path } => revision::run_approve(path, &reference),
RevisionCommand::Apply { reference, path } => revision::run_apply(path, &reference),
},
Command::Reconcile {
req,
slice,
r#move,
to,
note,
path,
} => reconcile::run(
path,
&reconcile::ReconcileArgs {
req,
slice,
r#move,
to,
note,
},
),
Command::Coverage { command } => match command {
CoverageCommand::Show {
reference,
columns,
format,
json,
path,
} => coverage_view::run(path, &reference, columns.as_deref(), format, json, color),
CoverageCommand::Record {
slice,
requirement,
change,
mode,
status,
alias,
command,
extra_args,
matcher_source,
matcher_pattern,
regex,
attested_date,
path,
} => coverage_store::run_record(
path,
&coverage_store::CoverageRecordArgs {
slice: &slice,
requirement: &requirement,
change: &change,
mode: &mode,
status,
alias: alias.as_deref(),
command: &command,
extra_args: &extra_args,
matcher_source: matcher_source.as_deref(),
matcher_pattern: matcher_pattern.as_deref(),
regex,
attested_date: attested_date.as_deref(),
},
),
CoverageCommand::Verify { slice, all, path } => {
coverage_verify::run_cli(path, slice.as_deref(), all)
}
CoverageCommand::Forget {
slice,
requirement,
change,
mode,
path,
} => coverage_store::run_forget(path, &slice, &requirement, &change, &mode),
},
Command::Inspect {
id,
format,
json,
path,
} => run_inspect(path, &id, format, json),
Command::Survey {
all,
format,
json,
path,
} => priority::run_survey(
path,
all,
format,
json,
crate::listing::RenderOpts {
color,
term_width: crate::tty::stdout_terminal_width(),
},
),
Command::Next { format, json, path } => priority::run_next(
path,
format,
json,
crate::listing::RenderOpts {
color,
term_width: crate::tty::stdout_terminal_width(),
},
),
Command::Blockers {
id,
transitive,
format,
json,
path,
} => priority::run_blockers(
path,
&id,
transitive,
format,
json,
crate::listing::RenderOpts {
color,
term_width: crate::tty::stdout_terminal_width(),
},
),
Command::Explain {
id,
format,
json,
path,
} => priority::run_explain(
path,
&id,
format,
json,
crate::listing::RenderOpts {
color,
term_width: crate::tty::stdout_terminal_width(),
},
),
Command::Adr { command } => match command {
AdrCommand::New { title, slug, path } => adr::run_new(path, title, slug),
AdrCommand::List { list, path } => adr::run_list(path, list.into_list_args(color)),
AdrCommand::Show {
reference,
format,
json,
path,
} => adr::run_show(path, &reference, if json { Format::Json } else { format }),
AdrCommand::Status { id, status, path } => adr::run_status(path, id, status, color),
},
Command::Policy { command } => match command {
PolicyCommand::New { title, slug, path } => policy::run_new(path, title, slug),
PolicyCommand::List { list, path } => {
policy::run_list(path, list.into_list_args(color))
}
PolicyCommand::Show {
reference,
format,
json,
path,
} => policy::run_show(path, &reference, if json { Format::Json } else { format }),
PolicyCommand::Status { id, status, path } => {
policy::run_status(path, id, status, color)
}
},
Command::Standard { command } => match command {
StandardCommand::New { title, slug, path } => standard::run_new(path, title, slug),
StandardCommand::List { list, path } => {
standard::run_list(path, list.into_list_args(color))
}
StandardCommand::Show {
reference,
format,
json,
path,
} => standard::run_show(path, &reference, if json { Format::Json } else { format }),
StandardCommand::Status { id, status, path } => {
standard::run_status(path, id, status, color)
}
},
Command::Spec { command } => match command {
SpecCommand::New {
subtype,
title,
slug,
path,
} => spec::run_new(path, subtype, title, slug),
SpecCommand::List { list, path } => spec::run_list(path, list.into_list_args(color)),
SpecCommand::Show {
spec_ref,
format,
json,
path,
} => spec::run_show(path, &spec_ref, if json { Format::Json } else { format }),
SpecCommand::Validate { spec_ref, path } => {
spec::run_validate(path, spec_ref.as_deref())
}
SpecCommand::Req { command } => match command {
SpecReqCommand::Add {
spec_ref,
title,
kind,
label,
slug,
path,
} => spec::run_req_add(path, &spec_ref, title, kind, label, slug),
SpecReqCommand::Status {
req_ref,
to,
note,
path,
} => spec::run_req_status(path, &req_ref, to, note),
SpecReqCommand::List {
spec_ref,
list,
path,
} => spec::run_req_list(path, &spec_ref, list.into_list_args(color)),
},
},
Command::Backlog { command } => match command {
BacklogCommand::New {
kind,
title,
slug,
path,
} => backlog::run_new(path, kind, title, slug),
BacklogCommand::List {
kind,
by,
mut list,
substr,
path,
} => {
if list.filter.is_none() {
list.filter = substr;
}
backlog::run_list(path, kind, by, list.into_list_args(color))
}
BacklogCommand::Show {
id,
format,
json,
path,
} => backlog::run_show(path, &id, if json { Format::Json } else { format }),
BacklogCommand::Edit {
id,
status,
resolution,
path,
} => backlog::run_edit(path, &id, status, resolution),
BacklogCommand::Needs { id, prereqs, path } => backlog::run_needs(path, &id, &prereqs),
BacklogCommand::After { id, to, rank, path } => {
backlog::run_after(path, &id, &to, rank)
}
BacklogCommand::Tag {
id,
tags,
remove,
path,
} => backlog::run_tag(path, &id, &tags, &remove),
},
Command::Knowledge { command } => match command {
KnowledgeCommand::New {
kind,
title,
slug,
path,
} => knowledge::run_new(path, kind, title, slug),
KnowledgeCommand::List { list, path } => {
knowledge::run_list(path, list.into_list_args(color))
}
KnowledgeCommand::Show {
id,
format,
json,
path,
} => knowledge::run_show(path, &id, if json { Format::Json } else { format }),
KnowledgeCommand::Status { id, state, path } => {
knowledge::run_status(path, &id, &state, color)
}
},
Command::Boot {
command,
check,
path: boot_path,
} => match command {
None if check => boot::run_check(boot_path),
None => boot::run(boot_path),
Some(BootCommand::Install {
path,
agent,
dry_run,
yes,
}) => boot::run_install(path, &agent, dry_run, yes),
},
Command::Catalog { command } => match command {
CatalogCommand::Scan { root } => run_catalog_scan(root),
CatalogCommand::Graph { root } => run_catalog_graph(root),
},
Command::Worktree { command } => match command {
WorktreeCommand::Provision { fork, path } => worktree::run_provision(path, &fork),
WorktreeCommand::CheckAllowlist { path } => worktree::run_check_allowlist(path),
WorktreeCommand::BranchPointCheck { base, head, path } => {
worktree::run_branch_point_check(path, &base, head)
}
WorktreeCommand::Fork {
base,
branch,
dir,
worker,
path,
} => worktree::run_fork(path, &base, &branch, &dir, worker),
WorktreeCommand::Coordinate { slice, dir, path } => {
worktree::run_coordinate(path, slice, &dir)
}
WorktreeCommand::Import { base, fork, path } => {
worktree::run_import(path, &base, &fork)
}
WorktreeCommand::Land { fork, path } => worktree::run_land(path, &fork),
WorktreeCommand::Gc {
fork,
superseded_head,
force,
dry_run,
path,
} => worktree::run_gc(path, &fork, superseded_head.as_deref(), force, dry_run),
WorktreeCommand::Status { assert, path } => worktree::run_status(path, assert),
WorktreeCommand::VerifyWorker { base, dir } => worktree::run_verify_worker(&base, &dir),
WorktreeCommand::Marker {
clear,
operator,
stamp_subagent,
path,
} => {
if stamp_subagent {
worktree::run_stamp_subagent(path)
} else if clear {
worktree::run_marker_clear(path, operator)
} else {
anyhow::bail!("`worktree marker` requires `--clear` or `--stamp-subagent`")
}
}
},
Command::Dispatch { command } => match command {
DispatchCommand::Sync {
slice,
integrate,
trunk,
edge,
path,
..
} => {
if integrate {
dispatch::run_integrate(path, slice, trunk.as_deref(), edge.as_deref())
} else {
dispatch::run_prepare_review(path, slice)
}
}
DispatchCommand::RecordBoundary {
slice,
phase,
code_start,
code_end,
path,
} => dispatch::run_record_boundary(path, slice, &phase, &code_start, &code_end),
DispatchCommand::Candidate { command } => match command {
CandidateCommand::Create {
slice,
label,
kind,
role,
payload,
base,
source,
supersedes,
worktree,
path,
} => {
let req = dispatch::CreateRequest {
slice,
label,
kind: dispatch::parse_kind(&kind)?,
role: dispatch::parse_role(&role)?,
payload: dispatch::parse_payload(&payload)?,
base,
source,
supersedes,
worktree,
created_at: clock::today(),
};
dispatch::run_candidate_create(path, &req)
}
CandidateCommand::Status { slice, path } => {
dispatch::run_candidate_status(path, slice)
}
CandidateCommand::Admit {
slice,
role,
candidate,
review,
path,
} => {
let req = dispatch::AdmitRequest {
slice,
role: dispatch::parse_role(&role)?,
candidate,
review,
admitted_at: clock::today(),
};
dispatch::run_candidate_admit(path, &req)
}
},
},
Command::Validate { path } => run_validate(path),
Command::Reseat {
reference,
to,
path,
} => integrity::run_reseat(path, &reference, to),
Command::Link {
source,
label,
target,
path,
} => run_link(path, &source, &label, &target),
Command::Unlink {
source,
label,
target,
path,
} => run_unlink(path, &source, &label, &target),
Command::Needs {
source,
target,
path,
} => run_needs_edge(path, &source, &target),
Command::After {
source,
target,
rank,
path,
} => run_after_edge(path, &source, &target, rank),
Command::Status { format, json, path } => status::run(path, format, json),
Command::Supersede { new, old, path } => run_supersede(path, &new, &old),
Command::Map { command } => match command {
MapCommand::Serve(args) => commands::map::run_serve(None, args),
},
}
}
fn resolve_link_path(
root: &std::path::Path,
source: &str,
label: &str,
) -> anyhow::Result<(PathBuf, &'static relation::RelationRule)> {
let (kref, id) = integrity::parse_canonical_ref(source)?;
let rule = relation::validate_link(kref.kind, label)?;
let name = format!("{id:03}");
let toml_path = root
.join(kref.kind.dir)
.join(&name)
.join(format!("{}-{name}.toml", kref.stem));
Ok((toml_path, rule))
}
fn run_link(path: Option<PathBuf>, source: &str, label: &str, target: &str) -> anyhow::Result<()> {
use std::io::Write;
let root = crate::root::find(path, &crate::root::default_markers())?;
let (toml_path, rule) = resolve_link_path(&root, source, label)?;
if !matches!(rule.target, relation::TargetSpec::Unvalidated) {
integrity::ensure_ref_resolves(&root, target)?;
let (tkref, _tid) = integrity::parse_canonical_ref(target)?;
let (skref, _sid) = integrity::parse_canonical_ref(source)?;
relation::check_target_kind(rule, skref.kind, tkref.kind.prefix)?;
}
let outcome = relation::append_edge(&toml_path, rule.label, target)?;
match outcome {
relation::AppendOutcome::Wrote => {
writeln!(std::io::stdout(), "linked: {source} {label} {target}")?;
}
relation::AppendOutcome::Noop => {
writeln!(
std::io::stdout(),
"already linked: {source} {label} {target}"
)?;
}
}
Ok(())
}
fn run_unlink(
path: Option<PathBuf>,
source: &str,
label: &str,
target: &str,
) -> anyhow::Result<()> {
use std::io::Write;
let root = crate::root::find(path, &crate::root::default_markers())?;
let (toml_path, rule) = resolve_link_path(&root, source, label)?;
let outcome = relation::remove_edge(&toml_path, rule.label, target)?;
match outcome {
relation::RemoveOutcome::Removed => {
writeln!(std::io::stdout(), "unlinked: {source} {label} {target}")?;
}
relation::RemoveOutcome::Absent => {
writeln!(std::io::stdout(), "not linked: {source} {label} {target}")?;
}
}
Ok(())
}
fn is_work_like(kind: &'static entity::Kind) -> bool {
matches!(
kind.prefix,
"SL" | "ISS" | "IMP" | "CHR" | "RSK" | "IDE" | "REV"
)
}
fn resolve_dep_seq_src(
root: &std::path::Path,
source: &str,
target: &str,
) -> anyhow::Result<PathBuf> {
let (skref, sid) = integrity::parse_canonical_ref(source)?;
anyhow::ensure!(
is_work_like(skref.kind),
"`{source}` is a {} entity, which cannot author needs/after — only a slice or a backlog item (issue/improvement/chore/risk/idea) carries dep/seq",
skref.kind.prefix
);
let (tkref, tid) = integrity::parse_canonical_ref(target)?;
integrity::ensure_ref_resolves(root, target)?;
anyhow::ensure!(
is_work_like(tkref.kind),
"`{target}` is a {} entity — needs/after may only target work (a slice or a backlog item); cross-tier dep/seq is not yet allowed",
tkref.kind.prefix
);
anyhow::ensure!(
!(skref.kind.prefix == tkref.kind.prefix && sid == tid),
"a {source} edge to itself is not a dependency — self-edges are refused"
);
let name = format!("{sid:03}");
Ok(root
.join(skref.kind.dir)
.join(&name)
.join(format!("{}-{name}.toml", skref.stem)))
}
fn run_needs_edge(path: Option<PathBuf>, source: &str, target: &str) -> anyhow::Result<()> {
use std::io::Write;
let root = crate::root::find(path, &crate::root::default_markers())?;
let toml_path = resolve_dep_seq_src(&root, source, target)?;
dep_seq::append(&toml_path, &dep_seq::RelEdit::Needs(&[target.to_string()]))?;
writeln!(std::io::stdout(), "{source} needs {target}")?;
Ok(())
}
fn run_after_edge(
path: Option<PathBuf>,
source: &str,
target: &str,
rank: i32,
) -> anyhow::Result<()> {
use std::io::Write;
let root = crate::root::find(path, &crate::root::default_markers())?;
let toml_path = resolve_dep_seq_src(&root, source, target)?;
dep_seq::append(&toml_path, &dep_seq::RelEdit::After { to: target, rank })?;
let suffix = if rank == 0 {
String::new()
} else {
format!(" (rank {rank})")
};
writeln!(std::io::stdout(), "{source} after {target}{suffix}")?;
Ok(())
}
fn resolve_supersede_path(
root: &std::path::Path,
kref: &integrity::KindRef,
id: u32,
) -> (PathBuf, String) {
let name = format!("{id:03}");
let toml_path = root
.join(kref.kind.dir)
.join(&name)
.join(format!("{}-{name}.toml", kref.stem));
(toml_path, listing::canonical_id(kref.kind.prefix, id))
}
fn run_supersede(path: Option<PathBuf>, new: &str, old: &str) -> anyhow::Result<()> {
use anyhow::Context;
use std::io::Write;
let root = crate::root::find(path, &crate::root::default_markers())?;
let (new_kref, new_id) = integrity::parse_canonical_ref(new)?;
let (old_kref, old_id) = integrity::parse_canonical_ref(old)?;
anyhow::ensure!(
!(new_kref.kind.prefix == old_kref.kind.prefix && new_id == old_id),
"`{new}` cannot supersede itself — a self-supersession is not a decision change"
);
anyhow::ensure!(
new_kref.kind.prefix == old_kref.kind.prefix,
"cross-kind supersession is refused: `{new}` is a {} but `{old}` is a {} — supersession is within one kind",
new_kref.kind.prefix,
old_kref.kind.prefix
);
let policy = adr::supersede_policy(new_kref.kind).with_context(|| {
format!(
"supersession not yet supported for {} (follow-up F2)",
new_kref.kind.prefix
)
})?;
let (new_path, new_ref) = resolve_supersede_path(&root, new_kref, new_id);
let (old_path, old_ref) = resolve_supersede_path(&root, old_kref, old_id);
let new_text = std::fs::read_to_string(&new_path)
.with_context(|| format!("supersede: {new} not found at {}", new_path.display()))?;
let mut new_doc = new_text
.parse::<toml_edit::DocumentMut>()
.with_context(|| format!("Failed to parse {}", new_path.display()))?;
let old_text = std::fs::read_to_string(&old_path)
.with_context(|| format!("supersede: {old} not found at {}", old_path.display()))?;
let mut old_doc = old_text
.parse::<toml_edit::DocumentMut>()
.with_context(|| format!("Failed to parse {}", old_path.display()))?;
let new_sup = rel_array(&new_doc, policy.supersedes_field);
anyhow::ensure!(
new_sup.is_some(),
"malformed `{new}` at {}: missing seeded `[relationships].{}` array — restore the seeded `[relationships]` arrays before superseding; the file is left untouched",
new_path.display(),
policy.supersedes_field
);
let old_carveout = rel_array(&old_doc, policy.carveout_field);
anyhow::ensure!(
old_carveout.is_some(),
"malformed `{old}` at {}: missing seeded `[relationships].{}` array — restore the seeded `[relationships]` arrays before superseding; the file is left untouched",
old_path.display(),
policy.carveout_field
);
anyhow::ensure!(
old_doc
.get("status")
.and_then(toml_edit::Item::as_str)
.is_some(),
"malformed `{old}` at {}: missing seeded top-level `status` — restore the seeded keys before superseding; the file is left untouched",
old_path.display()
);
anyhow::ensure!(
old_doc
.get("updated")
.and_then(toml_edit::Item::as_str)
.is_some(),
"malformed `{old}` at {}: missing seeded top-level `updated` — restore the seeded keys before superseding; the file is left untouched",
old_path.display()
);
let old_status = old_doc
.get("status")
.and_then(toml_edit::Item::as_str)
.unwrap_or_default();
if old_status == policy.superseded_status {
let carveout = old_carveout.unwrap_or_default();
let new_lists_old = new_sup.unwrap_or_default().contains(&old_ref);
let single_self = carveout.len() == 1 && carveout.first() == Some(&new_ref);
if single_self && new_lists_old {
writeln!(
std::io::stdout(),
"already recorded: {new} supersedes {old}"
)?;
return Ok(());
}
if let Some(other) = carveout.iter().find(|x| **x != new_ref) {
anyhow::bail!("{old} already superseded by {other}; reopening is deferred");
}
anyhow::bail!(
"{old} status is superseded but its superseded_by carve-out is empty/inconsistent — run `doctrine validate`"
);
}
let today = clock::today();
let status_hint = format!(
"malformed `{old}`: missing seeded top-level `status`/`updated` — restore the seeded keys; the file is left untouched"
);
dep_seq::apply_string_append(&mut new_doc, policy.supersedes_field, &old_ref)?;
dep_seq::apply_string_append(&mut old_doc, policy.carveout_field, &new_ref)?;
dep_seq::apply_status(
&mut old_doc,
&[("status", policy.superseded_status), ("updated", &today)],
&status_hint,
)?;
std::fs::write(&new_path, new_doc.to_string())
.with_context(|| format!("Failed to write {}", new_path.display()))?;
std::fs::write(&old_path, old_doc.to_string())
.with_context(|| format!("Failed to write {}", old_path.display()))?;
writeln!(std::io::stdout(), "{new} supersedes {old}")?;
Ok(())
}
fn rel_array(doc: &toml_edit::DocumentMut, field: &str) -> Option<Vec<String>> {
doc.get("relationships")
.and_then(toml_edit::Item::as_table)
.and_then(|t| t.get(field))
.and_then(toml_edit::Item::as_array)
.map(|a| {
a.iter()
.filter_map(|v| v.as_str().map(str::to_string))
.collect()
})
}
fn run_validate(path: Option<PathBuf>) -> anyhow::Result<()> {
use std::io::Write;
let root = crate::root::find(path, &crate::root::default_markers())?;
let mut findings = integrity::id_integrity_findings(&root)?;
findings.extend(relation_graph::validate_relations(&root)?);
writeln!(
std::io::stdout(),
"validate: scanned {}",
integrity::scanned_kinds()
)?;
if findings.is_empty() {
writeln!(std::io::stdout(), "validate: corpus clean")?;
return Ok(());
}
for f in &findings {
writeln!(std::io::stdout(), " {f}")?;
}
anyhow::bail!("validate: {} finding(s)", findings.len())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn is_work_like_is_exactly_slice_plus_backlog_plus_revision() {
assert!(is_work_like(&slice::SLICE_KIND));
for k in integrity::KINDS
.iter()
.filter(|k| matches!(k.kind.prefix, "ISS" | "IMP" | "CHR" | "RSK" | "IDE" | "REV"))
{
assert!(is_work_like(k.kind), "{} is work-like", k.kind.prefix);
}
for k in integrity::KINDS.iter().filter(|k| {
!matches!(
k.kind.prefix,
"SL" | "ISS" | "IMP" | "CHR" | "RSK" | "IDE" | "REV"
)
}) {
assert!(
!is_work_like(k.kind),
"{} must NOT be work-like (off the allowlist)",
k.kind.prefix
);
}
}
#[test]
fn only_memory_conflicts_with_skill() {
let r = Cli::try_parse_from([
"doctrine",
"skills",
"install",
"--only-memory",
"--skill",
"code-review",
]);
assert!(r.is_err());
}
#[test]
fn only_memory_conflicts_with_domain() {
let r = Cli::try_parse_from([
"doctrine",
"skills",
"install",
"--only-memory",
"--domain",
"doctrine",
]);
assert!(r.is_err());
}
#[test]
fn only_memory_alone_parses() {
let r = Cli::try_parse_from(["doctrine", "skills", "install", "--only-memory"]);
assert!(r.is_ok());
}
#[test]
fn supersede_recovery_from_torn_new_only_state() {
let tmp = catalog::test_helpers::tmp();
let root = tmp.path();
catalog::test_helpers::write(
root,
".doctrine/adr/001/adr-001.toml",
"id = 1\nslug = \"a1\"\ntitle = \"A1\"\nstatus = \"accepted\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[relationships]\nsupersedes = [\"ADR-002\"]\nsuperseded_by = []\n",
);
catalog::test_helpers::write(root, ".doctrine/adr/001/adr-001.md", "body\n");
catalog::test_helpers::write(
root,
".doctrine/adr/002/adr-002.toml",
"id = 2\nslug = \"a2\"\ntitle = \"A2\"\nstatus = \"accepted\"\n\
created = \"2026-01-01\"\nupdated = \"2026-01-01\"\n\
[relationships]\nsupersedes = []\nsuperseded_by = []\n",
);
catalog::test_helpers::write(root, ".doctrine/adr/002/adr-002.md", "body\n");
run_supersede(Some(root.to_path_buf()), "ADR-001", "ADR-002")
.expect("recovery supersede should succeed");
let old_toml =
std::fs::read_to_string(root.join(".doctrine/adr/002/adr-002.toml")).unwrap();
assert!(
old_toml.contains("status = \"superseded\""),
"OLD.status should be superseded, got: {old_toml}"
);
assert!(
old_toml.contains("superseded_by = [\"ADR-001\"]"),
"OLD.superseded_by should contain ADR-001, got: {old_toml}"
);
let new_toml =
std::fs::read_to_string(root.join(".doctrine/adr/001/adr-001.toml")).unwrap();
let supersedes_count = new_toml.matches("ADR-002").count();
assert_eq!(
supersedes_count, 1,
"NEW.supersedes should contain ADR-002 exactly once, got {supersedes_count}: {new_toml}"
);
}
}
#[cfg(test)]
mod write_class_tests {
use super::*;
fn cls(cmd: Command) -> Option<&'static str> {
match write_class(&cmd) {
WriteClass::Read => None,
WriteClass::Write(v) | WriteClass::Orchestrator(v) | WriteClass::Hookmint(v) => Some(v),
WriteClass::MarkerClear => None,
}
}
fn clist() -> CommonListArgs {
CommonListArgs {
filter: None,
regexp: None,
case_insensitive: false,
status: Vec::new(),
tag: Vec::new(),
all: false,
format: Format::Table,
json: false,
columns: None,
}
}
#[test]
fn install_is_write() {
assert_eq!(
cls(Command::Install {
path: None,
dry_run: false,
yes: false
}),
Some("install")
);
}
#[test]
fn skills_split() {
assert_eq!(
cls(Command::Skills {
command: SkillsCommand::List {
agent: None,
installed: false
}
}),
None
);
assert_eq!(
cls(Command::Skills {
command: SkillsCommand::Install {
path: None,
agent: Vec::new(),
skill: Vec::new(),
domain: Vec::new(),
only_memory: false,
global: false,
dry_run: false,
yes: false,
}
}),
Some("claude install")
);
}
#[test]
fn claude_install_is_write() {
assert_eq!(
cls(Command::Claude {
command: ClaudeCommand::Install {
path: None,
agent: Vec::new(),
skill: Vec::new(),
domain: Vec::new(),
only_memory: false,
global: false,
dry_run: false,
yes: false,
}
}),
Some("claude install")
);
}
#[test]
fn slice_split() {
let w = |c| cls(Command::Slice { command: c });
assert_eq!(
w(SliceCommand::New {
title: None,
slug: None,
path: None
}),
Some("slice new")
);
assert_eq!(
w(SliceCommand::Design { id: 0, path: None }),
Some("slice design")
);
assert_eq!(
w(SliceCommand::Plan { id: 0, path: None }),
Some("slice plan")
);
assert_eq!(
w(SliceCommand::Phases {
id: 0,
prune: false,
path: None
}),
Some("slice phases")
);
assert_eq!(
w(SliceCommand::Notes { id: 0, path: None }),
Some("slice notes")
);
assert_eq!(
w(SliceCommand::Phase {
id: 0,
phase_id: String::new(),
status: state::PhaseStatus::Planned,
note: None,
path: None,
}),
Some("slice phase")
);
assert_eq!(
w(SliceCommand::Status {
id: 0,
state: slice::SliceStatus::Proposed,
note: None,
path: None,
}),
Some("slice status")
);
assert_eq!(
w(SliceCommand::List {
list: clist(),
path: None
}),
None
);
assert_eq!(
w(SliceCommand::Show {
reference: String::new(),
format: Format::Table,
json: false,
path: None,
}),
None
);
}
#[test]
fn memory_split() {
let w = |c| cls(Command::Memory { command: c });
assert_eq!(
w(MemoryCommand::Record {
title: String::new(),
memory_type: memory::MemoryType::Concept,
key: None,
status: memory::Status::Active,
summary: None,
tag: Vec::new(),
path_scope: Vec::new(),
glob: Vec::new(),
command: Vec::new(),
repo: None,
global: false,
path: None,
}),
Some("memory record")
);
assert_eq!(
w(MemoryCommand::Verify {
reference: String::new(),
path: None
}),
Some("memory verify")
);
assert_eq!(
w(MemoryCommand::Show {
reference: String::new(),
format: Format::Table,
json: false,
path: None,
}),
None
);
assert_eq!(
w(MemoryCommand::List {
memory_type: None,
list: clist(),
path: None,
}),
None
);
assert_eq!(
w(MemoryCommand::Find {
query: None,
args: FindRetrieveArgs {
path_scope: Vec::new(),
glob: Vec::new(),
command: Vec::new(),
tag: Vec::new(),
flag_query: None,
memory_type: None,
status: None,
include_draft: false,
format: Format::Table,
json: false,
offset: 0,
page: None,
limit: None,
path: None,
},
}),
None
);
assert_eq!(
w(MemoryCommand::Retrieve {
args: FindRetrieveArgs {
path_scope: Vec::new(),
glob: Vec::new(),
command: Vec::new(),
tag: Vec::new(),
flag_query: None,
memory_type: None,
status: None,
include_draft: false,
format: Format::Table,
json: false,
offset: 0,
page: None,
limit: None,
path: None,
},
min_trust: None,
}),
None
);
assert_eq!(
w(MemoryCommand::Sync {
command: None,
dry_run: false,
yes: false,
path: None,
}),
Some("memory sync")
);
assert_eq!(
w(MemoryCommand::Sync {
command: Some(SyncCommand::Install {
path: None,
dry_run: false,
yes: false,
}),
dry_run: false,
yes: false,
path: None,
}),
Some("memory sync install")
);
}
#[test]
fn adr_split() {
let w = |c| cls(Command::Adr { command: c });
assert_eq!(
w(AdrCommand::New {
title: None,
slug: None,
path: None
}),
Some("adr new")
);
assert_eq!(
w(AdrCommand::Status {
id: 0,
status: adr::AdrStatus::Proposed,
path: None,
}),
Some("adr status")
);
assert_eq!(
w(AdrCommand::List {
list: clist(),
path: None
}),
None
);
assert_eq!(
w(AdrCommand::Show {
reference: String::new(),
format: Format::Table,
json: false,
path: None,
}),
None
);
}
#[test]
fn policy_split() {
let w = |c| cls(Command::Policy { command: c });
assert_eq!(
w(PolicyCommand::New {
title: None,
slug: None,
path: None
}),
Some("policy new")
);
assert_eq!(
w(PolicyCommand::Status {
id: 0,
status: policy::PolicyStatus::Draft,
path: None,
}),
Some("policy status")
);
assert_eq!(
w(PolicyCommand::List {
list: clist(),
path: None
}),
None
);
assert_eq!(
w(PolicyCommand::Show {
reference: String::new(),
format: Format::Table,
json: false,
path: None,
}),
None
);
}
#[test]
fn standard_split() {
let w = |c| cls(Command::Standard { command: c });
assert_eq!(
w(StandardCommand::New {
title: None,
slug: None,
path: None
}),
Some("standard new")
);
assert_eq!(
w(StandardCommand::Status {
id: 0,
status: standard::StandardStatus::Draft,
path: None,
}),
Some("standard status")
);
assert_eq!(
w(StandardCommand::List {
list: clist(),
path: None
}),
None
);
assert_eq!(
w(StandardCommand::Show {
reference: String::new(),
format: Format::Table,
json: false,
path: None,
}),
None
);
}
#[test]
fn spec_split() {
let w = |c| cls(Command::Spec { command: c });
assert_eq!(
w(SpecCommand::New {
subtype: spec::SpecSubtype::Product,
title: None,
slug: None,
path: None,
}),
Some("spec new")
);
assert_eq!(
w(SpecCommand::Req {
command: SpecReqCommand::Add {
spec_ref: String::new(),
title: None,
kind: requirement::ReqKind::Functional,
label: None,
slug: None,
path: None,
}
}),
Some("spec req add")
);
assert_eq!(
w(SpecCommand::Req {
command: SpecReqCommand::Status {
req_ref: String::new(),
to: requirement::ReqStatus::Active,
note: None,
path: None,
}
}),
Some("spec req status")
);
assert_eq!(
w(SpecCommand::List {
list: clist(),
path: None
}),
None
);
assert_eq!(
w(SpecCommand::Show {
spec_ref: String::new(),
format: Format::Table,
json: false,
path: None,
}),
None
);
assert_eq!(
w(SpecCommand::Validate {
spec_ref: None,
path: None
}),
None
);
}
#[test]
fn backlog_split() {
let w = |c| cls(Command::Backlog { command: c });
assert_eq!(
w(BacklogCommand::New {
kind: backlog::ItemKind::Issue,
title: None,
slug: None,
path: None,
}),
Some("backlog new")
);
assert_eq!(
w(BacklogCommand::Edit {
id: String::new(),
status: backlog::Status::Open,
resolution: None,
path: None,
}),
Some("backlog edit")
);
assert_eq!(
w(BacklogCommand::List {
kind: None,
by: backlog::OrderBy::Sequence,
list: clist(),
substr: None,
path: None,
}),
None
);
assert_eq!(
w(BacklogCommand::Show {
id: String::new(),
format: Format::Table,
json: false,
path: None,
}),
None
);
}
#[test]
fn knowledge_split() {
let w = |c| cls(Command::Knowledge { command: c });
assert_eq!(
w(KnowledgeCommand::New {
kind: knowledge::RecordKind::Assumption,
title: None,
slug: None,
path: None,
}),
Some("knowledge new")
);
assert_eq!(
w(KnowledgeCommand::Status {
id: String::new(),
state: String::new(),
path: None,
}),
Some("knowledge status")
);
assert_eq!(
w(KnowledgeCommand::List {
list: clist(),
path: None,
}),
None
);
assert_eq!(
w(KnowledgeCommand::Show {
id: String::new(),
format: Format::Table,
json: false,
path: None,
}),
None
);
}
#[test]
fn boot_split() {
assert_eq!(
cls(Command::Boot {
command: None,
check: false,
path: None
}),
Some("boot")
);
assert_eq!(
cls(Command::Boot {
command: None,
check: true,
path: None
}),
Some("boot")
);
assert_eq!(
cls(Command::Boot {
command: Some(BootCommand::Install {
path: None,
agent: Vec::new(),
dry_run: false,
yes: false,
}),
check: false,
path: None,
}),
Some("boot install")
);
}
#[test]
fn worktree_is_read() {
assert_eq!(
cls(Command::Worktree {
command: WorktreeCommand::Provision {
fork: PathBuf::from("x"),
path: None,
}
}),
None
);
assert_eq!(
cls(Command::Worktree {
command: WorktreeCommand::CheckAllowlist { path: None }
}),
None
);
assert_eq!(
cls(Command::Worktree {
command: WorktreeCommand::Status {
assert: false,
path: None,
}
}),
None
);
}
#[test]
fn worktree_marker_is_bespoke_class() {
let c = Command::Worktree {
command: WorktreeCommand::Marker {
clear: true,
operator: false,
stamp_subagent: false,
path: None,
},
};
assert!(
matches!(write_class(&c), WriteClass::MarkerClear),
"marker --clear must be the bespoke MarkerClear class"
);
assert_eq!(cls(c), None);
}
#[test]
fn worktree_marker_stamp_subagent_is_hookmint() {
let c = Command::Worktree {
command: WorktreeCommand::Marker {
clear: false,
operator: false,
stamp_subagent: true,
path: None,
},
};
assert!(
matches!(
write_class(&c),
WriteClass::Hookmint("marker --stamp-subagent")
),
"marker --stamp-subagent must be the Hookmint class"
);
assert_eq!(cls(c), Some("marker --stamp-subagent"));
}
#[test]
fn worktree_fork_is_orchestrator() {
let c = Command::Worktree {
command: WorktreeCommand::Fork {
base: "B".to_string(),
branch: "wkr".to_string(),
dir: PathBuf::from("x"),
worker: false,
path: None,
},
};
assert!(
matches!(write_class(&c), WriteClass::Orchestrator("fork")),
"fork must be Orchestrator(\"fork\")"
);
assert_eq!(cls(c), Some("fork"));
}
#[test]
fn dispatch_sync_is_orchestrator() {
let c = Command::Dispatch {
command: DispatchCommand::Sync {
slice: 64,
prepare_review: true,
integrate: false,
trunk: None,
edge: None,
path: None,
},
};
assert!(
matches!(write_class(&c), WriteClass::Orchestrator("dispatch-sync")),
"dispatch sync must be Orchestrator(\"dispatch-sync\")"
);
assert_eq!(cls(c), Some("dispatch-sync"));
}
#[test]
fn dispatch_sync_integrate_is_orchestrator() {
let c = Command::Dispatch {
command: DispatchCommand::Sync {
slice: 64,
prepare_review: false,
integrate: true,
trunk: None,
edge: None,
path: None,
},
};
assert!(
matches!(write_class(&c), WriteClass::Orchestrator("dispatch-sync")),
"dispatch sync --integrate must be Orchestrator(\"dispatch-sync\")"
);
assert_eq!(cls(c), Some("dispatch-sync"));
}
#[test]
fn inspect_is_read() {
assert_eq!(
cls(Command::Inspect {
id: "SL-046".to_string(),
format: Format::Table,
json: false,
path: None,
}),
None
);
}
#[test]
fn validate_is_read_reseat_is_write() {
assert_eq!(cls(Command::Validate { path: None }), None);
assert_eq!(
cls(Command::Reseat {
reference: "SL-001".to_string(),
to: None,
path: None,
}),
Some("reseat")
);
}
}